feat: keep undergraduate exams independent of course terms
This commit is contained in:
@@ -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);
|
||||
});
|
||||
}
|
||||
@@ -7,37 +7,120 @@ import 'package:hepan_everyday/domain/app_snapshot.dart';
|
||||
import 'package:hepan_everyday/domain/course.dart';
|
||||
|
||||
void main() {
|
||||
SchoolTerm under(int id) => SchoolTerm(id, school: SupportedSchool.uestcUndergraduate);
|
||||
SchoolTerm under(int id) =>
|
||||
SchoolTerm(id, school: SupportedSchool.uestcUndergraduate);
|
||||
test('undergraduate semester labels and final exam URLs', () {
|
||||
expect([483,503,523,543,563].map((id)=>under(id).label).toList(),
|
||||
['2025-2026第一学期','2025-2026第二学期','2026-2027第一学期','2026-2027第二学期','2027-2028第一学期']);
|
||||
final query = UndergraduateExamQuery(term: under(523), types: {UndergraduateExamType.finalExam});
|
||||
expect(query.uriFor(UndergraduateExamType.finalExam).toString(), 'https://eams.uestc.edu.cn/eams/stdExamTable!examTable.action?semester.id=523&examType.id=1');
|
||||
expect(() => UndergraduateExamQuery(term: under(523),types: {}),throwsArgumentError);
|
||||
expect([483, 503, 523, 543, 563].map((id) => under(id).label).toList(), [
|
||||
'2025-2026第一学期',
|
||||
'2025-2026第二学期',
|
||||
'2026-2027第一学期',
|
||||
'2026-2027第二学期',
|
||||
'2027-2028第一学期',
|
||||
]);
|
||||
final query = UndergraduateExamQuery(
|
||||
term: under(523),
|
||||
types: {UndergraduateExamType.finalExam},
|
||||
);
|
||||
expect(
|
||||
query.uriFor(UndergraduateExamType.finalExam).toString(),
|
||||
'https://eams.uestc.edu.cn/eams/stdExamTable!examTable.action?semester.id=523&examType.id=1',
|
||||
);
|
||||
expect(
|
||||
() => UndergraduateExamQuery(term: under(523), types: {}),
|
||||
throwsArgumentError,
|
||||
);
|
||||
expect(() => under(504), throwsArgumentError);
|
||||
expect(under(523), isNot(SchoolTerm(523)));
|
||||
expect(()=>under(523).examUri, throwsStateError);
|
||||
expect(SchoolTerm.fromJson({'id':73}).school, SupportedSchool.uestcGraduate);
|
||||
expect(SchoolTerm.undergraduateChoices(DateTime(2026,10)).first.id, 583);
|
||||
});
|
||||
test('undergraduate exam identity retains both types and rejects corruption', () {
|
||||
final schedule=ExamSchedule(semesterLabel:under(523).label,termId:'523',school:SupportedSchool.uestcUndergraduate,
|
||||
requestedExamTypes:{1,2},records:[const ExamRecord(code:'A',name:'课程',examTypeId:1), const ExamRecord(code:'A',name:'课程',examTypeId:2)]);
|
||||
expect(ExamSchedule.fromJson(schedule.toJson()).records,hasLength(2));
|
||||
expect(()=>ExamSchedule.fromJson({...schedule.toJson(),'requestedExamTypes':[]}),throwsFormatException);
|
||||
expect(()=>ExamSchedule.fromJson({...schedule.toJson(),'schoolId':'unknown'}),throwsFormatException);
|
||||
final old=ExamSchedule(semesterLabel:'2026秋',termId:'73',records:[const ExamRecord(code:'A',name:'课程'),const ExamRecord(code:'A',name:'课程')]);
|
||||
expect(()=>ExamSchedule.fromJson(old.toJson()),throwsFormatException);
|
||||
});
|
||||
test('snapshots persist school selection and validate period twelve source', () {
|
||||
final course=Course(id:'under-523-test',name:'课程',teacher:'教师',room:'教室',weekday:1,startPeriod:9,endPeriod:12,weeks:{1},source:CourseSource.school,courseCode:'A',notes:'备注');
|
||||
final saved=AppSnapshot(selectedSchool:SupportedSchool.uestcUndergraduate,schoolTerm:under(523),courses:[course]);
|
||||
final round=AppSnapshot.fromJson(saved.toJson());
|
||||
expect(round.selectedSchool, SupportedSchool.uestcUndergraduate);
|
||||
expect(round.courses.single.notes,'备注');
|
||||
expect(()=>AppSnapshot.fromJson({...saved.toJson(),'schoolTerm':{'id':73}}),throwsFormatException);
|
||||
expect(()=>AppSnapshot.fromJson({...saved.toJson(),'selectedSchoolId':'unknown'}),throwsFormatException);
|
||||
final legacy=AppSnapshot(schoolTerm:SchoolTerm(73)).toJson()..remove('selectedSchoolId');
|
||||
expect(AppSnapshot.fromJson(legacy).selectedSchool,SupportedSchool.uestcGraduate);
|
||||
expect(() => under(523).examUri, throwsStateError);
|
||||
expect(
|
||||
SchoolTerm.fromJson({'id': 73}).school,
|
||||
SupportedSchool.uestcGraduate,
|
||||
);
|
||||
expect(SchoolTerm.undergraduateChoices(DateTime(2026, 10)).first.id, 583);
|
||||
});
|
||||
test(
|
||||
'undergraduate exam identity retains both types and rejects corruption',
|
||||
() {
|
||||
final schedule = ExamSchedule(
|
||||
semesterLabel: under(523).label,
|
||||
termId: '523',
|
||||
school: SupportedSchool.uestcUndergraduate,
|
||||
requestedExamTypes: {1, 2},
|
||||
records: [
|
||||
const ExamRecord(code: 'A', name: '课程', examTypeId: 1),
|
||||
const ExamRecord(code: 'A', name: '课程', examTypeId: 2),
|
||||
],
|
||||
);
|
||||
expect(ExamSchedule.fromJson(schedule.toJson()).records, hasLength(2));
|
||||
expect(
|
||||
() => ExamSchedule.fromJson({
|
||||
...schedule.toJson(),
|
||||
'requestedExamTypes': [],
|
||||
}),
|
||||
throwsFormatException,
|
||||
);
|
||||
expect(
|
||||
() => ExamSchedule.fromJson({
|
||||
...schedule.toJson(),
|
||||
'schoolId': 'unknown',
|
||||
}),
|
||||
throwsFormatException,
|
||||
);
|
||||
final old = ExamSchedule(
|
||||
semesterLabel: '2026秋',
|
||||
termId: '73',
|
||||
records: [
|
||||
const ExamRecord(code: 'A', name: '课程'),
|
||||
const ExamRecord(code: 'A', name: '课程'),
|
||||
],
|
||||
);
|
||||
expect(() => ExamSchedule.fromJson(old.toJson()), throwsFormatException);
|
||||
},
|
||||
);
|
||||
test(
|
||||
'snapshots persist school selection and validate period twelve source',
|
||||
() {
|
||||
final course = Course(
|
||||
id: 'under-523-test',
|
||||
name: '课程',
|
||||
teacher: '教师',
|
||||
room: '教室',
|
||||
weekday: 1,
|
||||
startPeriod: 9,
|
||||
endPeriod: 12,
|
||||
weeks: {1},
|
||||
source: CourseSource.school,
|
||||
courseCode: 'A',
|
||||
notes: '备注',
|
||||
);
|
||||
final saved = AppSnapshot(
|
||||
selectedSchool: SupportedSchool.uestcUndergraduate,
|
||||
schoolTerm: under(523),
|
||||
courses: [course],
|
||||
);
|
||||
final round = AppSnapshot.fromJson(saved.toJson());
|
||||
expect(round.selectedSchool, SupportedSchool.uestcUndergraduate);
|
||||
expect(round.courses.single.notes, '备注');
|
||||
expect(
|
||||
() => AppSnapshot.fromJson({
|
||||
...saved.toJson(),
|
||||
'schoolTerm': {'id': 73},
|
||||
}),
|
||||
throwsFormatException,
|
||||
);
|
||||
expect(
|
||||
() => AppSnapshot.fromJson({
|
||||
...saved.toJson(),
|
||||
'selectedSchoolId': 'unknown',
|
||||
}),
|
||||
throwsFormatException,
|
||||
);
|
||||
final legacy = AppSnapshot(schoolTerm: SchoolTerm(73)).toJson()
|
||||
..remove('selectedSchoolId');
|
||||
expect(
|
||||
AppSnapshot.fromJson(legacy).selectedSchool,
|
||||
SupportedSchool.uestcGraduate,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user