import 'dart:io'; 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/domain/school_term.dart'; import 'package:hepan_everyday/domain/school_identity.dart'; import 'package:hepan_everyday/domain/undergraduate_exam_query.dart'; import '../support/fake_school_web.dart'; import 'undergraduate_session_test.dart' show underEvidence; void main() { TestWidgetsFlutterBinding.ensureInitialized(); late FakeSchoolPlatform platform; late UndergraduateSession session; final query = UndergraduateExamQuery( term: SchoolTerm(503, school: SupportedSchool.uestcUndergraduate), types: UndergraduateExamType.values.toSet(), ); setUp(() { platform = FakeSchoolPlatform(); WebViewPlatform.instance = platform; session = UndergraduateSession(); }); tearDown(() => session.dispose()); String exam(int type) { final text = File('test/fixtures/school/undergraduate-exams.html') .readAsStringSync(); return text .replaceAll('examType.id=1', 'examType.id=$type') .replaceAll( 'name="examType.id" value="1"', 'name="examType.id" value="$type"', ) .replaceAll( '期末考试', type == 2 ? '期中考试' : type == 3 ? '补缓考' : '期末考试', ); } Future finish() async { platform.delegate.started(platform.web.url!); platform.delegate.finished(platform.web.url!); for (var i = 0; i < 40; i++) { await Future.value(); } } test( 'all selected exam types complete before publishing one result', () async { await session.openExams(query); platform.web.javascript = (s) async { final id = int.tryParse( Uri.parse(platform.web.url!).queryParameters['examType.id'] ?? '', ); return underEvidence( platform.web.url!, html: s == undergraduateCaptureScript && id != null ? exam(id) : null, kind: 'exams', ); }; await finish(); expect( platform.web.loads.last, query.uriFor(UndergraduateExamType.finalExam).toString(), ); await finish(); expect(session.pendingExams, isNull); expect( platform.web.loads.last, query.uriFor(UndergraduateExamType.midterm).toString(), ); await finish(); expect(session.pendingExams, isNull); await finish(); expect(session.pendingExams!.records, hasLength(12)); expect(session.pendingExams!.requestedExamTypes, {1, 2, 3}); final completedGeneration = session.pageGeneration; platform.delegate.started('https://eams.uestc.edu.cn/eams/'); expect(session.pendingExams, isNull); expect(session.pageGeneration, greaterThan(completedGeneration)); }, ); test('last type failure never publishes partial results', () async { await session.openExams(query); platform.web.javascript = (s) async { final id = int.tryParse( Uri.parse(platform.web.url!).queryParameters['examType.id'] ?? '', ); return underEvidence( platform.web.url!, html: s == undergraduateCaptureScript && id != null ? (id == 3 ? exam(1) : exam(id)) : null, kind: 'exams', ); }; await finish(); await finish(); await finish(); await finish(); expect(session.pendingExams, isNull); expect(session.message, contains('未完成')); }); testWidgets('login waiting is excluded and active reads have timeouts', ( tester, ) async { await session.openExams(query); platform.web.javascript = (_) async => underEvidence(platform.web.url!, authenticated: false); await finish(); await tester.pump(const Duration(seconds: 121)); expect(session.awaitingAuthentication, isTrue); expect(session.message, isNot(contains('超时'))); platform.web.javascript = (_) async => underEvidence(platform.web.url!); await finish(); await tester.pump(const Duration(seconds: 46)); expect(session.message, contains('超时')); expect(session.pendingExams, isNull); }); test('cancelled batch ignores late page callbacks', () async { await session.openExams(query); final generation = session.pageGeneration; session.setReadingActive(false); platform.web.javascript = (_) async => underEvidence(platform.web.url!, html: exam(1), kind: 'exams'); await finish(); expect(session.pendingExams, isNull); expect(session.pageGeneration, greaterThan(generation)); }); test( 'expired authentication discards earlier parts and restarts every type', () async { await session.openExams(query); var renewed = false; platform.web.javascript = (s) async { final uri = Uri.parse(platform.web.url!), id = int.tryParse(uri.queryParameters['examType.id'] ?? ''); return underEvidence( uri.toString(), authenticated: uri.host == 'eams.uestc.edu.cn', html: s == undergraduateCaptureScript && id != null ? exam(id).replaceAll('E', '${renewed ? 'NEW' : 'OLD'}E') : null, kind: 'exams', ); }; await finish(); await finish(); expect( platform.web.loads.last, query.uriFor(UndergraduateExamType.midterm).toString(), ); platform.web.url = 'https://idas.uestc.edu.cn/authserver/login'; await finish(); expect(session.awaitingAuthentication, isTrue); renewed = true; platform.web.url = 'https://eams.uestc.edu.cn/eams/'; await finish(); expect( platform.web.loads.last, query.uriFor(UndergraduateExamType.finalExam).toString(), ); await finish(); await finish(); await finish(); expect( session.pendingExams!.records.every((r) => r.code.startsWith('NEW')), isTrue, ); }, ); testWidgets('the overall batch timeout limits individually timely requests', ( tester, ) async { await session.openExams(query); platform.web.javascript = (s) async { final id = int.tryParse( Uri.parse(platform.web.url!).queryParameters['examType.id'] ?? '', ); return underEvidence( platform.web.url!, html: s == undergraduateCaptureScript && id != null ? exam(id) : null, kind: 'exams', ); }; await finish(); await tester.pump(const Duration(seconds: 40)); await finish(); await tester.pump(const Duration(seconds: 40)); await finish(); await tester.pump(const Duration(seconds: 41)); expect(session.pendingExams, isNull); expect(session.message, contains('超时')); }); }