Files
TPSchedule/mobile/test/app/app_controller_test.dart
T

124 lines
4.0 KiB
Dart

import 'package:flutter_test/flutter_test.dart';
import 'package:timezone/data/latest.dart' as tzdata;
import 'package:hepan_everyday/app/app_controller.dart';
import 'package:hepan_everyday/domain/app_snapshot.dart';
import 'package:hepan_everyday/domain/app_settings.dart';
import 'package:hepan_everyday/domain/semester.dart';
import 'package:hepan_everyday/domain/course.dart';
import 'package:hepan_everyday/data/json_schedule_import.dart';
import '../support/fakes.dart';
import '../domain/schedule_engine_test.dart' show course;
void main() {
setUpAll(tzdata.initializeTimeZones);
late MemoryStore store;
late FakeNotifications notifications;
late FakeCalendar calendar;
late AppController controller;
setUp(() {
store = MemoryStore(
AppSnapshot(
courses: [course()],
settings: AppSettings(
semester: Semester(startDate: DateTime(2026, 9, 9), totalWeeks: 5),
),
),
);
notifications = FakeNotifications();
calendar = FakeCalendar();
controller = AppController(
store,
notifications,
calendar,
() => DateTime.utc(2026, 9, 8),
);
});
tearDown(() => controller.dispose());
test('a new successful demo action clears a stale save error', () async {
await controller.initialize();
store.failSave = true;
await controller.updateSettings(
controller.snapshot.settings.copyWith(reminderMinutes: 30),
);
expect(controller.lastError, isNotNull);
await controller.showDemo();
expect(controller.lastError, isNull);
});
test(
'refresh after corrupt storage never reconciles an empty timetable',
() async {
final damaged = AppController(
DamagedStore(),
notifications,
calendar,
() => DateTime.utc(2026, 9, 8),
);
await damaged.initialize();
await damaged.refreshServices();
expect(notifications.reconciliations, 0);
expect(damaged.storageReady, isFalse);
damaged.dispose();
},
);
test('failed saving does not change currently visible settings', () async {
await controller.initialize();
store.failSave = true;
await controller.updateSettings(
controller.snapshot.settings.copyWith(reminderMinutes: 30),
);
expect(controller.snapshot.settings.reminderMinutes, 15);
expect(controller.lastError, isNotNull);
});
test('concurrent settings changes persist in user action order', () async {
await controller.initialize();
await Future.wait([
controller.updateSettings(
controller.snapshot.settings.copyWith(reminderMinutes: 30),
),
controller.updateSettings(
controller.snapshot.settings.copyWith(reminderMinutes: 60),
),
]);
expect(store.value!.settings.reminderMinutes, 60);
});
test('demo cannot replace real courses or create real reminders', () async {
await controller.initialize();
final previous = notifications.reconciliations;
await controller.showDemo();
expect(controller.isDemo, isTrue);
expect(store.value!.courses.single.id, 'math');
expect(notifications.reconciliations, previous);
expect(calendar.synchronizations, 0);
});
test('unconfirmed empty import preserves saved timetable', () async {
await controller.initialize();
await controller.applyImport(
const ImportDraft(
courses: [],
explicitlyEmpty: false,
source: CourseSource.file,
parserVersion: 'json-v1',
),
confirmedEmpty: false,
);
expect(controller.snapshot.courses.single.id, 'math');
expect(controller.lastError, isNotNull);
});
test(
'clearing courses cancels owned notifications and calendar events',
() async {
await controller.initialize();
await controller.clearCourses();
expect(store.value!.courses, isEmpty);
expect(notifications.cancellations, 1);
expect(calendar.deletions, 1);
},
);
}
class DamagedStore extends MemoryStore {
@override
Future<AppSnapshot?> load() async => throw const FormatException('damaged');
}