fix: import unpublished exam rooms and add opt-in school HTML feedback

This commit is contained in:
Codex
2026-10-16 16:38:00 +08:00
parent defe7fd50e
commit 437ed14dbe
31 changed files with 969 additions and 39 deletions
@@ -12,6 +12,29 @@ void main() {
String fixture() =>
File('test/fixtures/school/undergraduate-exams.html').readAsStringSync();
final term = SchoolTerm(503, school: SupportedSchool.uestcUndergraduate);
test('unpublished room and seat merged by school keep the dated exam', () {
final page = fixture().replaceFirst(
RegExp(r'<td>示例考场</td>\s*<td>1</td>'),
'<td colspan="2">[考试情况尚未发布]</td>',
);
expect(page, contains('colspan="2"'));
final result = UndergraduateExamAdapter.extract(page);
expect(result.records, hasLength(4));
final record = result.records.first;
expect(record.date, '2026-06-22');
expect(record.time, contains('14:00-16:00'));
expect(record.room, '[考试情况尚未发布]');
expect(record.seat, '');
expect(record.status, '正常');
expect(record.examTypeId, 1);
});
test('unexpected merged exam columns remain a failed import', () {
final page = fixture().replaceFirst(
'<td>示例考场</td>',
'<td colspan="3">[考试情况尚未发布]</td>',
);
expect(() => UndergraduateExamAdapter.extract(page), throwsFormatException);
});
test('actual exam result term wins over the outer default', () {
final schedule = UndergraduateExamAdapter.extract(fixture());
expect(schedule.termId, '503');
@@ -68,17 +91,7 @@ void main() {
);
test('only recognized explicit empty exam tables are empty success', () {
final empty =
'<form id="stdExamTableForm"><input name="semester.id" value="503"><input name="examType.id" value="1"></form><table><tr>${[
'课程序号',
'课程名称',
'考试日期',
'考试安排',
'考试地点',
'座位号',
'考试情况',
'考试类型',
'其它说明',
].map((s) => '<td>$s</td>').join()}</tr><tr><td colspan="9">暂无考试安排</td></tr></table>';
'<form id="stdExamTableForm"><input name="semester.id" value="503"><input name="examType.id" value="1"></form><table><tr>${['课程序号', '课程名称', '考试日期', '考试安排', '考试地点', '座位号', '考试情况', '考试类型', '其它说明'].map((s) => '<td>$s</td>').join()}</tr><tr><td colspan="9">暂无考试安排</td></tr></table>';
expect(UndergraduateExamAdapter.extract(empty).records, isEmpty);
expect(
() => UndergraduateExamAdapter.extract(empty.replaceAll('暂无考试安排', '')),
@@ -7,6 +7,7 @@ import 'package:hepan_everyday/features/school/undergraduate/page_probe.dart';
import 'package:hepan_everyday/domain/school_term.dart';
import 'package:hepan_everyday/domain/school_identity.dart';
import 'package:hepan_everyday/domain/undergraduate_exam_query.dart';
import 'package:hepan_everyday/services/school_page_diagnostic.dart';
import '../support/fake_school_web.dart';
import 'undergraduate_session_test.dart' show underEvidence;
@@ -123,6 +124,7 @@ void main() {
await finish();
await tester.pump(const Duration(seconds: 46));
expect(session.message, contains('超时'));
SchoolPageDiagnostic.instance.clear();
expect(session.pendingExams, isNull);
});
test('cancelled batch ignores late page callbacks', () async {
@@ -199,5 +201,6 @@ void main() {
await tester.pump(const Duration(seconds: 41));
expect(session.pendingExams, isNull);
expect(session.message, contains('超时'));
SchoolPageDiagnostic.instance.clear();
});
}
@@ -6,6 +6,7 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart';
import 'package:hepan_everyday/features/school/undergraduate/session.dart';
import 'package:hepan_everyday/features/school/undergraduate/page_probe.dart';
import 'package:hepan_everyday/services/school_page_diagnostic.dart';
import '../support/fake_school_web.dart';
@@ -31,6 +32,21 @@ void main() {
session = UndergraduateSession();
});
tearDown(() => session.dispose());
test('failed import retains diagnostic evidence separately and logout removes it', () async {
addTearDown(SchoolPageDiagnostic.instance.clear);
await session.openTimetable();
final content = File('test/fixtures/school/undergraduate-timetable.html')
.readAsStringSync()
.replaceFirst('"教师1"', '""');
platform.web.javascript = (_) async =>
underEvidence(platform.web.url!, html: content);
await session.readCurrentTimetable();
expect(session.pendingDraft, isNull);
expect(session.message, contains('教师'));
expect(SchoolPageDiagnostic.instance.export(), isNotNull);
session.invalidateAuthentication();
expect(SchoolPageDiagnostic.instance.export(), isNull);
});
test('live URL cannot cross origin or change again during capture', () async {
await session.openTimetable();
final content = File('test/fixtures/school/undergraduate-timetable.html')
@@ -9,6 +9,27 @@ import 'package:hepan_everyday/data/schedule_file_import.dart';
import 'package:hepan_everyday/domain/school_identity.dart';
void main() {
test('missing course fields identify the failing field without disclosing values', () {
const base =
'new TaskActivity("T", "教师", "C", "课程(C.01)", "R", "教室", "010"); '
'index=0*unitCount+0;table0.activities[index][table0.activities[index].length]=activity;';
for (final entry in {
'teacher_missing': base.replaceFirst('"教师"', '""'),
'room_missing': base.replaceFirst('"教室"', '""'),
'course_sequence_invalid': base.replaceFirst('课程(C.01)', '课程'),
}.entries) {
expect(
() => parseActivityData(entry.value, unitCount: 12),
throwsA(
isA<UndergraduateActivityException>().having(
(e) => e.code,
'reason',
entry.key,
),
),
);
}
});
test('unrecognized activity writes and incomplete declarations reject the entire timetable', () {
const valid =
'activity = new TaskActivity("T", "教师", "C", "课程(C.01)", "R", "教室", "010"); '