200 lines
7.2 KiB
Dart
200 lines
7.2 KiB
Dart
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';
|
|
import 'package:hepan_everyday/domain/course.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('"教师"', '""'),
|
|
'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"); '
|
|
'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('missing room retains every scheduled occurrence and clearly marks it pending', () {
|
|
final original = UndergraduateTimetableAdapter.extract(fixture(), uri);
|
|
final draft = UndergraduateTimetableAdapter.extract(
|
|
fixture().replaceFirst('"教室1"', '""'),
|
|
uri,
|
|
);
|
|
Set<String> occurrences(Iterable<Course> courses) => <String>{
|
|
for (final c in courses)
|
|
for (final w in c.weeks)
|
|
for (var p = c.startPeriod; p <= c.endPeriod; p++)
|
|
'${c.courseCode}/${c.teacher}/${c.weekday}/$p/$w',
|
|
};
|
|
expect(occurrences(draft.courses), occurrences(original.courses));
|
|
final pending = draft.courses.where((c) => c.room == '教室待定');
|
|
expect(pending, isNotEmpty);
|
|
expect(pending.every((c) => c.notes.contains('学校未提供教室')), isTrue);
|
|
expect(draft.courses.any((c) => c.room == '教室2'), isTrue);
|
|
});
|
|
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 = <String>{};
|
|
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 [
|
|
'<html>登录</html>',
|
|
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 =
|
|
'<input name="semester.id" value="523"><table id="manualArrangeCourseTable"><tr><td>暂无课程</td></tr></table>';
|
|
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,
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|