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() { 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, ); }, ); } }