feat: keep undergraduate exams independent of course terms

This commit is contained in:
Codex
2026-10-14 23:19:11 +08:00
parent 48367b8273
commit 141436f545
9 changed files with 419 additions and 99 deletions
@@ -0,0 +1,112 @@
import 'dart:convert';
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/school_identity.dart';
import 'package:hepan_everyday/domain/school_term.dart';
import 'package:hepan_everyday/domain/exam_schedule.dart';
import 'package:hepan_everyday/data/json_schedule_import.dart';
import '../domain/schedule_engine_test.dart' show course;
import '../support/fakes.dart';
void main() {
setUpAll(tzdata.initializeTimeZones);
late MemoryStore store;
late FakeNotifications notifications;
late FakeCalendar calendar;
late AppController controller;
setUp(() {
store = MemoryStore(
AppSnapshot(
courses: [course()],
schoolTerm: SchoolTerm(73),
settings: AppSettings(
semester: Semester(startDate: DateTime(2026, 9, 7), totalWeeks: 20),
),
),
);
notifications = FakeNotifications();
calendar = FakeCalendar();
controller = AppController(
store,
notifications,
calendar,
() => DateTime.utc(2026, 9, 8),
);
});
tearDown(() => controller.dispose());
ExamSchedule exams() => ExamSchedule(
semesterLabel: '2025-2026第二学期',
termId: '503',
records: [],
school: SupportedSchool.uestcUndergraduate,
requestedExamTypes: {1},
);
test(
'historical undergraduate exams preserve courses and services',
() async {
await controller.initialize();
final before = controller.snapshot;
final count = notifications.reconciliations;
await controller.applyExams(exams());
expect(controller.lastError, isNull);
expect(controller.snapshot.courses, before.courses);
expect(controller.snapshot.schoolTerm, before.schoolTerm);
expect(controller.snapshot.settings.semester, before.settings.semester);
expect(notifications.reconciliations, count);
expect(controller.snapshot.examSchedule!.termId, '503');
},
);
test('school selection and failed exam writes are atomic', () async {
await controller.initialize();
store.failSave = true;
final old = controller.snapshot.toJson();
expect(
await controller.selectSchool(SupportedSchool.uestcUndergraduate),
isFalse,
);
expect(controller.snapshot.toJson(), old);
await controller.applyExams(exams());
expect(controller.snapshot.toJson(), old);
store.failSave = false;
expect(
await controller.selectSchool(SupportedSchool.uestcUndergraduate),
isTrue,
);
expect(store.value!.selectedSchool, SupportedSchool.uestcUndergraduate);
});
test('undergraduate course changes preserve independent exams and reset only new term dates', () async {
await controller.initialize();
await controller.applyExams(exams());
final term = SchoolTerm(523, school: SupportedSchool.uestcUndergraduate);
final draft = ImportDraft(
courses: [course()],
explicitlyEmpty: false,
source: course().source,
parserVersion: 'test',
schoolTerm: term,
);
await controller.applyImport(draft, confirmedEmpty: false);
expect(controller.snapshot.settings.semester, isNull);
expect(controller.snapshot.examSchedule!.termId, '503');
await controller.updateSettings(
(s) => s.copyWith(
semester: Semester(startDate: DateTime(2026, 9, 7), totalWeeks: 20),
),
);
final date = controller.snapshot.settings.semester;
await controller.applyImport(draft, confirmedEmpty: false);
expect(controller.snapshot.settings.semester, date);
});
test('JSON period twelve needs an explicit undergraduate term', () {
final old = course().toJson()..['endPeriod'] = 12;
final json =
'{"schemaVersion":1,"courses":[${const JsonEncoder().convert(old)}]}';
expect(() => JsonScheduleImport.parse(json), throwsFormatException);
});
}