import 'dart:async'; import 'dart:convert'; import 'package:flutter_test/flutter_test.dart'; import 'package:webview_flutter/webview_flutter.dart'; import 'package:hepan_everyday/features/school/school_full_page_recovery.dart'; import 'package:hepan_everyday/features/school/undergraduate/session.dart'; import 'package:hepan_everyday/features/school/undergraduate/page_probe.dart'; import '../support/fake_school_web.dart'; const _complete = ''' 退出PRIVATE_PAGE_CONTENT'''; const _fragment = '退出'; class Gateway implements SchoolFullPageGateway { int requests = 0, renders = 0, discards = 0; String html = _complete; Completer? pending; Completer? pendingRender; bool live = false; void Function()? onRender; @override bool supports(WebViewController web) => true; @override Future fetch( WebViewController web, Uri uri, String documentToken, ) async { requests++; live = true; return pending == null ? SchoolPageCandidate('opaque-token', html) : pending!.future; } @override Future render(String token) async { renders++; if (pendingRender != null) { return await pendingRender!.future && live; } final accepted = live; onRender?.call(); return accepted; } @override Future discard(String token) async { discards++; } @override Future invalidate(WebViewController web) async { live = false; } } void main() { TestWidgetsFlutterBinding.ensureInitialized(); late FakeSchoolPlatform platform; late WebViewController web; late Gateway gateway; late SchoolFullPageRecovery recovery; final url = Uri.parse('https://eams.uestc.edu.cn/eams/home.action'); setUp(() { platform = FakeSchoolPlatform(); WebViewPlatform.instance = platform; web = WebViewController(); platform.web.url = url.toString(); platform.web.javascript = (_) async => jsonEncode({ 'scripts': 2, 'styles': 0, 'links': 0, 'loaded': 0, 'jquery': false, 'documentToken': 'fixture-document-token', 'beangle': false, 'require': false, 'script202': 0, 'style202': 0, }); gateway = Gateway(); recovery = SchoolFullPageRecovery(gateway: gateway); }); Future restore({ bool authenticated = true, bool Function()? current, }) => recovery.tryRestore( web, url, authenticated: authenticated, documentKey: 'visit:document', isCurrent: current ?? () => true, ); test( 'authenticated fragment is replaced only after a complete page is fetched', () async { expect(await restore(), FullPageRecoveryResult.restored); expect(gateway.requests, 1); expect(gateway.renders, 1); expect(gateway.discards, 1); }, ); test( 'incomplete head or password form preserves current page and does not loop', () async { for (final html in [ _fragment, _complete.replaceFirst( 'PRIVATE_PAGE_CONTENT', '', ), _complete.replaceFirst( '', '', ), _complete.replaceFirst( '', '', ), ]) { gateway.html = html; recovery.reset(); expect(await restore(), FullPageRecoveryResult.failed); expect(await restore(), FullPageRecoveryResult.unchanged); } expect(gateway.renders, 0); expect(gateway.requests, 4); expect(gateway.discards, 4); }, ); for (final started in [false, true]) { test( 'session recovers once and completes after render, started=$started', () async { final session = UndergraduateSession(pageRecovery: recovery); addTearDown(session.dispose); await session.openTimetable(); var rendered = false; platform.web.javascript = (script) async { if (script == undergraduateStatusProbeScript) { return jsonEncode({ 'topUrl': platform.web.url!, 'authenticated': true, 'pages': [], }); } return jsonEncode({ 'scripts': rendered ? 20 : 2, 'styles': 0, 'links': rendered ? 3 : 0, 'jquery': rendered, 'documentToken': 'fixture-document-token', }); }; gateway.onRender = () { rendered = true; if (started) platform.delegate.started(platform.web.url!); platform.delegate.finished(platform.web.url!); }; platform.web.url = url.toString(); platform.delegate.started(url.toString()); platform.delegate.finished(url.toString()); await flushSchoolEvents(); await Future.delayed(const Duration(milliseconds: 550)); await flushSchoolEvents(); expect(session.statusLabel, '已连接'); expect(session.busy, false); expect(session.message, UndergraduateSession.manualMessage); expect(gateway.requests, 1); expect(gateway.renders, 1); expect(session.pendingDraft, isNull); }, ); } test( 'same URL document replacement or closing during fetch cancels rendering', () async { gateway.pending = Completer(); var current = true; final task = restore(current: () => current); await flushSchoolEvents(); expect(gateway.requests, 1); current = false; gateway.pending!.complete( const SchoolPageCandidate('opaque-token', _complete), ); expect(await task, FullPageRecoveryResult.cancelled); expect(gateway.renders, 0); expect(gateway.discards, 1); }, ); test( 'reset invalidates a native render awaiting document verification', () async { gateway.pendingRender = Completer(); final task = restore(); await flushSchoolEvents(); expect(gateway.renders, 1); recovery.reset(); gateway.pendingRender!.complete(true); expect(await task, FullPageRecoveryResult.cancelled); expect(gateway.live, false); expect(gateway.discards, 1); }, ); test( 'authentication pages and complete documents are not refetched', () async { expect( await restore(authenticated: false), FullPageRecoveryResult.unchanged, ); platform.web.javascript = (_) async => jsonEncode({ 'scripts': 20, 'styles': 4, 'links': 4, 'loaded': 4, 'jquery': true, 'beangle': true, 'require': true, 'script202': 0, 'style202': 0, }); expect(await restore(), FullPageRecoveryResult.unchanged); expect(gateway.requests, 0); }, ); test( 'logout and external or credential-bearing addresses never use the gateway', () async { for (final address in [ 'https://eams.uestc.edu.cn/eams/logout.action', 'https://other.test/eams/home.action', 'https://eams.uestc.edu.cn/eams/home.action?ticket=PRIVATE_TICKET', 'http://eams.uestc.edu.cn/eams/home.action', ]) { platform.web.url = address; expect( await recovery.tryRestore( web, Uri.parse(address), authenticated: true, documentKey: address, isCurrent: () => true, ), FullPageRecoveryResult.unchanged, ); } expect(gateway.requests, 0); }, ); }