import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:timezone/data/latest.dart' as tzdata; import 'package:hepan_everyday/app/hepan_app.dart'; import 'package:hepan_everyday/app/app_controller.dart'; import 'package:hepan_everyday/features/profile/settings_page.dart'; import 'package:hepan_everyday/services/calendar_service.dart'; import '../support/fakes.dart'; import 'navigation_test.dart' show uiController; import '../services/calendar_service_test.dart' show CalendarProvider; import '../domain/schedule_engine_test.dart' show course; import 'package:hepan_everyday/services/android_calendar_service.dart'; import 'package:hepan_everyday/domain/app_snapshot.dart'; import 'package:hepan_everyday/domain/app_settings.dart'; import 'package:hepan_everyday/domain/semester.dart'; void main() { setUpAll(tzdata.initializeTimeZones); testWidgets( 'reminder times can be selected together and last choice stays selected', (tester) async { final controller = uiController(); await controller.initialize(); await tester.pumpWidget( MaterialApp(home: SettingsPage(controller: controller)), ); await tester.pumpAndSettle(); await tester.scrollUntilVisible( find.text('5 分钟'), 250, scrollable: find.byType(Scrollable).first, ); await tester.ensureVisible(find.text('5 分钟')); await tester.pumpAndSettle(); await tester.tap(find.text('5 分钟')); await tester.pumpAndSettle(); expect( (controller.snapshot.toJson()['settings'] as Map)['reminderOffsets'], [5, 15], ); await tester.tap(find.text('15 分钟')); await tester.pumpAndSettle(); expect( (controller.snapshot.toJson()['settings'] as Map)['reminderOffsets'], [5], ); await tester.tap(find.text('5 分钟')); await tester.pumpAndSettle(); expect( (controller.snapshot.toJson()['settings'] as Map)['reminderOffsets'], [5], ); controller.dispose(); }, ); testWidgets( 'partial initial calendar sync exposes working cleanup action while switch is off', (tester) async { final provider = CalendarProvider()..failAfterFirst = true; final controller = AppController( MemoryStore( AppSnapshot( courses: [ course(id: 'a'), course(id: 'b'), ], settings: AppSettings( semester: Semester( startDate: DateTime(2026, 9, 9), totalWeeks: 1, ), ), ), ), FakeNotifications(), AndroidCalendarService(provider), DateTime.now, ); await controller.initialize(); await controller.enableCalendarFromUserAction('local'); expect(controller.snapshot.settings.calendarEnabled, isFalse); expect(provider.records, hasLength(1)); await tester.pumpWidget( MaterialApp(home: SettingsPage(controller: controller)), ); await tester.pumpAndSettle(); await tester.scrollUntilVisible( find.text('重试清理课程日程'), 250, scrollable: find.byType(Scrollable).first, ); await tester.ensureVisible(find.text('重试清理课程日程')); await tester.pumpAndSettle(); await tester.tap(find.text('重试清理课程日程')); await tester.pumpAndSettle(); expect(provider.records, isEmpty); expect(controller.snapshot.calendarCleanupPending, isFalse); controller.dispose(); }, ); testWidgets('calendar provider failure is explained and keeps switch off', ( tester, ) async { final controller = AppController( MemoryStore(), FakeNotifications(), UnavailableCalendar(), DateTime.now, ); await controller.initialize(); await tester.pumpWidget( MaterialApp(home: SettingsPage(controller: controller)), ); await tester.pumpAndSettle(); await tester.scrollUntilVisible( find.text('导入手机日历'), 250, scrollable: find.byType(Scrollable).first, ); await tester.ensureVisible(find.text('导入手机日历')); await tester.pumpAndSettle(); await tester.tap(find.text('导入手机日历')); await tester.pumpAndSettle(); expect(tester.takeException(), isNull); expect(find.text('日历暂时不可用,请检查权限后重试'), findsOneWidget); expect(controller.snapshot.settings.calendarEnabled, isFalse); controller.dispose(); }); testWidgets( 'invalid semester weeks are rejected without changing saved data', (tester) async { final controller = uiController(); await controller.initialize(); await tester.pumpWidget(HepanApp(controller: controller)); await tester.pumpAndSettle(); expect(find.bySemanticsLabel('个人设置'), findsOneWidget); await tester.tap(find.bySemanticsLabel('个人设置')); await tester.pumpAndSettle(); await tester.ensureVisible(find.text('课表设置')); await tester.pumpAndSettle(); await tester.tap(find.text('课表设置')); await tester.pumpAndSettle(); await tester.scrollUntilVisible( find.byKey(const Key('semester-weeks')), 300, scrollable: find.byType(Scrollable).last, ); await tester.enterText(find.byKey(const Key('semester-weeks')), '31'); await tester.ensureVisible(find.text('保存学期设置')); await tester.tap(find.text('保存学期设置')); await tester.pumpAndSettle(); expect(find.text('周数应为 1–30'), findsOneWidget); expect(controller.snapshot.settings.semester, isNull); controller.dispose(); }, ); testWidgets('two hundred percent font demo has no overflow', (tester) async { tester.view.physicalSize = const Size(360, 800); tester.view.devicePixelRatio = 1; addTearDown(tester.view.resetPhysicalSize); addTearDown(tester.view.resetDevicePixelRatio); tester.platformDispatcher.textScaleFactorTestValue = 2; addTearDown(tester.platformDispatcher.clearTextScaleFactorTestValue); final controller = uiController(); await controller.initialize(); await controller.showDemo(); await tester.pumpWidget(HepanApp(controller: controller)); await tester.pumpAndSettle(); await tester.scrollUntilVisible( find.text('示例课表'), 250, scrollable: find.byType(Scrollable).first, ); expect(find.text('示例课表'), findsOneWidget); expect(tester.takeException(), isNull); controller.dispose(); }); } class UnavailableCalendar extends FakeCalendar { @override Future> listWritableCalendars() async => throw StateError('unavailable'); }