import 'dart:io'; import 'dart:convert'; import 'package:html/parser.dart' as html; import 'package:hepan_everyday/features/school/undergraduate/activity_data.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:hepan_everyday/features/school/undergraduate/timetable_adapter.dart'; 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().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"); ' 'index = 0*unitCount+0; table0.activities[index][table0.activities[index].length]=activity;'; for (final extra in [ 'table0.activities[11][table0.activities[11].length]=activity;', 'activity = new TaskActivity("T", "教师", "C", "另一课程(C.02)", "R", "教室", "010")\n' 'index = 0*unitCount+1; table0.activities[index][table0.activities[index].length]=activity;', ]) { expect( () => parseActivityData('$valid $extra', unitCount: 12), throwsFormatException, ); } }); final uri = Uri.parse( 'https://eams.uestc.edu.cn/eams/courseTableForStd.action', ); String fixture() => File('test/fixtures/school/undergraduate-timetable.html') .readAsStringSync(); test('undergraduate activity data preserves teachers rooms weeks and period twelve', () { final draft = UndergraduateTimetableAdapter.extract(fixture(), uri); expect(draft.schoolTerm!.id, 523); expect(draft.schoolTerm!.school, SupportedSchool.uestcUndergraduate); final activities = parseActivityData( html.parse(fixture()).querySelector('script')!.text, unitCount: 12, ); final expected = {}; for (final a in activities.where((a) => !a.cancelled)) { for (final index in a.indices) { final cancelled = activities .where( (c) => c.cancelled && c.courseCode == a.courseCode && c.indices.contains(index), ) .expand((c) => c.weeks) .toSet(); for (final week in a.weeks.difference(cancelled)) { expected.add( jsonEncode([ a.courseCode.split('.').first, a.teacher, a.room, index, week, ]), ); } } } final actual = { for (final c in draft.courses) for (var p = c.startPeriod; p <= c.endPeriod; p++) for (final w in c.weeks) jsonEncode([ c.courseCode, c.teacher, c.room, (c.weekday - 1) * 12 + p - 1, w, ]), }; expect( actual, expected, reason: 'continuous grouping must preserve every original active index and week', ); expect(draft.courses, hasLength(18)); expect(draft.courses.map((c) => c.courseCode).toSet(), hasLength(10)); expect(draft.courses.where((c) => c.endPeriod == 12), hasLength(3)); expect(draft.courses.any((c) => c.room == '停课'), isFalse); expect(draft.courses.any((c) => c.teacher.contains('教师9')), isTrue); expect(draft.courses.any((c) => c.notes.contains('19:00-22:00')), isTrue); expect(ScheduleFileImport.parse(fixture()).schoolTerm, draft.schoolTerm); expect( UndergraduateTimetableAdapter.extract( fixture(), uri, ).courses.map((c) => c.id), draft.courses.map((c) => c.id), ); }); test( 'invalid or incomplete activity data never becomes an empty timetable', () { for (final html in [ '登录', fixture().replaceAll('unitCount = 12', 'unitCount = 11'), fixture().replaceFirst( RegExp(r'index = \d+\s*\*\s*unitCount\s*\+\s*\d+'), 'index = unknown()', ), fixture().replaceFirst('value="523"', 'value="504"'), fixture().replaceFirst('"教师1"', '""'), ]) { expect( () => UndergraduateTimetableAdapter.extract(html, uri), throwsFormatException, ); } expect( () => UndergraduateTimetableAdapter.extract( fixture(), Uri.parse('https://idas.uestc.edu.cn/'), ), throwsFormatException, ); }, ); test('explicit empty pages require recognized table and actual term', () { final empty = '
暂无课程
'; expect( UndergraduateTimetableAdapter.extract(empty, uri).explicitlyEmpty, isTrue, ); expect( () => UndergraduateTimetableAdapter.extract( empty.replaceAll('暂无课程', ''), uri, ), throwsFormatException, ); }); final original = Platform.environment['UESTC_UNDERGRADUATE_TIMETABLE_FIXTURE']; if (original != null) { test( 'actual supplied undergraduate HTML has complete normalized arrangements', () { final draft = UndergraduateTimetableAdapter.extract( File(original).readAsStringSync(), uri, ); expect(draft.courses, hasLength(18)); expect(draft.courses.map((c) => c.courseCode).toSet(), hasLength(10)); expect(draft.courses.where((c) => c.endPeriod == 12), hasLength(3)); expect( draft.courses.every((c) => c.teacher.isNotEmpty && c.room.isNotEmpty), isTrue, ); }, ); } }