feat: deliver Riverside Schedule seven-day grid and school HTML import
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:hepan_everyday/features/school/school_page_adapter.dart';
|
||||
import 'package:hepan_everyday/features/school/school_navigation_policy.dart';
|
||||
import 'package:hepan_everyday/data/schedule_file_import.dart';
|
||||
import 'package:hepan_everyday/domain/course.dart';
|
||||
|
||||
import 'recognized_timetable_test.dart' show fixture;
|
||||
|
||||
void main() {
|
||||
final target = Uri.parse(
|
||||
'https://yjsjy.uestc.edu.cn/pyxx/pygl/xskbcx/index/1',
|
||||
);
|
||||
test('in-flight read cannot authorize a different origin', () async {
|
||||
final reader = SchoolReader(SchoolPageAdapter()),
|
||||
body = Completer<String>();
|
||||
final valid = reader.read(target, () => body.future);
|
||||
final rejected = reader.read(
|
||||
Uri.parse('https://evil.example/pyxx/pygl/xskbcx/index/1'),
|
||||
() async => fixture(),
|
||||
);
|
||||
body.complete(fixture());
|
||||
await expectLater(rejected, throwsA(isA<UnsupportedSchoolLayout>()));
|
||||
await valid;
|
||||
});
|
||||
test('invalidated read cannot become a fresh authenticated draft', () async {
|
||||
final reader = SchoolReader(SchoolPageAdapter()),
|
||||
body = Completer<String>();
|
||||
final stale = reader.read(target, () => body.future);
|
||||
final check = expectLater(stale, throwsA(isA<UnsupportedSchoolLayout>()));
|
||||
reader.invalidate();
|
||||
final fresh = await reader.read(target, () async => fixture());
|
||||
body.complete(fixture());
|
||||
await check;
|
||||
expect(fresh.courses.length, 4);
|
||||
expect(reader.busy, isFalse);
|
||||
});
|
||||
test('official numeric semester routes are allowed but alternate endpoints are not', () {
|
||||
expect(
|
||||
SchoolNavigationPolicy.isTimetable(
|
||||
Uri.parse('https://yjsjy.uestc.edu.cn/pyxx/pygl/xskbcx/index/73'),
|
||||
),
|
||||
isTrue,
|
||||
);
|
||||
for (final url in [
|
||||
'http://yjsjy.uestc.edu.cn/pyxx/pygl/xskbcx/index/1',
|
||||
'https://user@yjsjy.uestc.edu.cn/pyxx/pygl/xskbcx/index/1',
|
||||
'https://yjsjy.uestc.edu.cn:444/pyxx/pygl/xskbcx/index/1',
|
||||
'https://yjsjy.uestc.edu.cn/pyxx/pygl/xskbcx/index/73/other',
|
||||
'https://yjsjy.uestc.edu.cn/pyxx/pygl/xskbcx/index/73evil',
|
||||
]) {
|
||||
expect(SchoolNavigationPolicy.isTimetable(Uri.parse(url)), isFalse);
|
||||
}
|
||||
});
|
||||
test('BOM saved HTML file imports without claiming live authentication', () {
|
||||
final draft = ScheduleFileImport.parse('\uFEFF${fixture()}');
|
||||
expect(draft.courses.length, 4);
|
||||
expect(draft.semesterLabel, '2026-2027秋');
|
||||
expect(draft.source, CourseSource.file);
|
||||
expect(draft.courses.every((c) => c.source == CourseSource.file), isTrue);
|
||||
expect(
|
||||
() => ScheduleFileImport.parse('<html>登录</html>'),
|
||||
throwsA(isA<UnsupportedSchoolLayout>()),
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:hepan_everyday/features/school/school_page_adapter.dart';
|
||||
import 'package:hepan_everyday/domain/course.dart';
|
||||
|
||||
String fixture() {
|
||||
const records = {
|
||||
't51': '101/示例集成电路设计(1班)/32/2/示例教师甲/1-3周/1~2节/示例楼B212<br>101/示例集成电路设计(1班)/32/2/示例教师甲/6-6周/1~2节/示例楼B212<br>101/示例集成电路设计(1班)/32/2/示例教师甲/7-9周/1~2节/示例楼B212',
|
||||
't13': '101/示例集成电路设计(1班)/32/2/示例教师甲/1-5周/3~4节/示例楼B212',
|
||||
't23': '102/示例信号检测与估计(2班)/32/2/示例教师乙、示例教师丙/1-5周/3~4节/示例楼B102<br>102/示例信号检测与估计(2班)/32/2/示例教师乙、示例教师丙/7-9周/3~4节/示例楼B102',
|
||||
't29': '103/示例现代化理论与实践(3班)/36/2/示例教师丁/1-12周/9~11节/示例楼B107',
|
||||
};
|
||||
return '<select id="drpxq"><option value="73" selected>2026-2027秋</option></select><table id="tbl"><tr><th>节次</th>${['一', '二', '三', '四', '五', '六', '日'].map((d) => '<th>星期$d</th>').join()}</tr>${[1, 3, 5, 7, 9].map((p) => '<tr><td id="t0$p">$p-${p == 9 ? 11 : p + 1}节</td>${List.generate(7, (i) => '<td id="t${i + 1}$p">${records['t${i + 1}$p'] ?? ''}</td>').join()}</tr>').join()}</table>';
|
||||
}
|
||||
|
||||
void main() {
|
||||
final url = Uri.parse('https://yjsjy.uestc.edu.cn/pyxx/pygl/xskbcx/index/1');
|
||||
test('school DOM merges week segments without losing full metadata or evening periods', () {
|
||||
final draft = SchoolPageAdapter().extract(fixture(), url);
|
||||
expect(draft.courses.length, 4);
|
||||
final friday = draft.courses.singleWhere((c) => c.weekday == 5);
|
||||
expect(friday.weeks, {1, 2, 3, 6, 7, 8, 9});
|
||||
final signal = draft.courses.singleWhere(
|
||||
(c) => c.weekday == 2 && c.startPeriod == 3,
|
||||
);
|
||||
expect(signal.teacher, '示例教师乙、示例教师丙');
|
||||
expect(signal.room, '示例楼B102');
|
||||
final evening = draft.courses.singleWhere((c) => c.startPeriod == 9);
|
||||
expect(evening.endPeriod, 11);
|
||||
expect(draft.source, CourseSource.school);
|
||||
expect(draft.explicitlyEmpty, isFalse);
|
||||
expect(
|
||||
SchoolPageAdapter().extract(fixture(), url).courses.map((c) => c.id),
|
||||
draft.courses.map((c) => c.id),
|
||||
);
|
||||
});
|
||||
test('invalid cell, missing weekday, mismatched period and empty grid fail closed', () {
|
||||
for (final body in [
|
||||
fixture().replaceFirst('1-3周', '未知周'),
|
||||
fixture().replaceFirst('id="t71"', 'id="missing"'),
|
||||
fixture().replaceFirst('1~2节', '5~6节'),
|
||||
fixture().replaceAll(RegExp(r'\d+/示例[^<]+'), ''),
|
||||
fixture().replaceFirst('星期日', '星期八'),
|
||||
]) {
|
||||
expect(
|
||||
() => SchoolPageAdapter().extract(body, url),
|
||||
throwsA(isA<UnsupportedSchoolLayout>()),
|
||||
);
|
||||
}
|
||||
});
|
||||
test('saved HTML uses file provenance and does not authenticate a school session', () {
|
||||
final draft = SchoolPageAdapter().extract(
|
||||
fixture(),
|
||||
url,
|
||||
source: CourseSource.file,
|
||||
);
|
||||
expect(draft.source, CourseSource.file);
|
||||
expect(draft.courses.every((c) => c.source == CourseSource.file), isTrue);
|
||||
});
|
||||
test('actual attached HTML imports eleven arrangements (local optional verification)', () {
|
||||
final file = File(
|
||||
Platform.environment['RIVERSIDE_HTML_FIXTURE'] ?? 'not-present',
|
||||
);
|
||||
if (!file.existsSync()) return;
|
||||
final draft = SchoolPageAdapter().extract(file.readAsStringSync(), url);
|
||||
expect(draft.courses.length, 11);
|
||||
expect(draft.courses.map((c) => c.name).toSet().length, 7);
|
||||
expect(
|
||||
draft.courses
|
||||
.singleWhere((c) => c.weekday == 2 && c.startPeriod == 9)
|
||||
.endPeriod,
|
||||
11,
|
||||
);
|
||||
expect(
|
||||
draft.courses
|
||||
.singleWhere((c) => c.weekday == 2 && c.startPeriod == 3)
|
||||
.teacher
|
||||
.split('、')
|
||||
.length,
|
||||
2,
|
||||
);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user