Fix school browser initialization and redirect detection; improve diagnostics
This commit is contained in:
+3
-1
@@ -1,6 +1,8 @@
|
||||
# TPSchedule Flutter工程
|
||||
|
||||
当前版本0.7.0+18,安卓包名 `com.tpschedule.app`。Dart包名保留 `hepan_everyday`,兼容既有代码引用。
|
||||
当前版本0.7.1+19,安卓包名 `com.tpschedule.app`。Dart包名保留 `hepan_everyday`,兼容既有代码引用。
|
||||
|
||||
学校浏览器在首次导航前配置宽视口及第三方Cookie,显示系统Chromium版本,记录脱敏资源错误;本科认证跳转期间过期的探测会重试。255项单元测试、Android16原生浏览器测试通过,实际学校账号在线加载仍需手机验证,见 `../docs/tpschedule-0.7.1-validation.md`。
|
||||
|
||||
## 本科使用
|
||||
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
import 'package:webview_flutter/webview_flutter.dart';
|
||||
import 'package:hepan_everyday/features/school/school_browser.dart';
|
||||
import 'package:hepan_everyday/services/diagnostic_log.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
testWidgets(
|
||||
'Android school browser honors desktop viewport and keeps console details private',
|
||||
(tester) async {
|
||||
final web = WebViewController();
|
||||
await web.setJavaScriptMode(JavaScriptMode.unrestricted);
|
||||
var loaded = Completer<void>();
|
||||
await web.setNavigationDelegate(
|
||||
NavigationDelegate(
|
||||
onPageFinished: (_) {
|
||||
if (!loaded.isCompleted) loaded.complete();
|
||||
},
|
||||
),
|
||||
);
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(body: WebViewWidget(controller: web)),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
const html =
|
||||
'<!doctype html><html><head><meta name="viewport" content="width=1200"></head>'
|
||||
'<body>Legacy teaching portal fixture</body></html>';
|
||||
await web.loadHtmlString(html);
|
||||
await loaded.future.timeout(const Duration(seconds: 20));
|
||||
final before = num.parse(
|
||||
(await web.runJavaScriptReturningResult(
|
||||
'document.documentElement.clientWidth',
|
||||
)).toString(),
|
||||
);
|
||||
final info = await configureSchoolBrowser(web, 'undergraduate');
|
||||
loaded = Completer<void>();
|
||||
await web.loadHtmlString(html);
|
||||
await loaded.future.timeout(const Duration(seconds: 20));
|
||||
final after = num.parse(
|
||||
(await web.runJavaScriptReturningResult(
|
||||
'document.documentElement.clientWidth',
|
||||
)).toString(),
|
||||
);
|
||||
expect(before, lessThan(1000));
|
||||
expect(after, greaterThanOrEqualTo(1100));
|
||||
expect(info.chromiumVersion, matches(r'^\d+(\.\d+){1,3}$'));
|
||||
await web.runJavaScript('console.error("PRIVATE_NATIVE_CONSOLE_DETAIL")');
|
||||
await Future<void>.delayed(const Duration(milliseconds: 200));
|
||||
final log = utf8.decode(await DiagnosticLog.instance.export());
|
||||
expect(log, contains('school_console_error'));
|
||||
expect(log, isNot(contains('PRIVATE_NATIVE_CONSOLE_DETAIL')));
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -1,2 +1,2 @@
|
||||
const appVersion = '0.7.0';
|
||||
const appBuildNumber = 18;
|
||||
const appVersion = '0.7.1';
|
||||
const appBuildNumber = 19;
|
||||
|
||||
@@ -120,6 +120,12 @@ class HttpFeedbackSender implements FeedbackSender {
|
||||
throw const FeedbackException('服务器不支持此附件的格式或内容,请移除附件,或换用正常的图片、视频后提交');
|
||||
}
|
||||
if (response.statusCode == 422 && errorCode == 'validation_error') {
|
||||
if (submission.diagnosticLog != null &&
|
||||
result?['detail'] == 'Unexpected file field.') {
|
||||
throw const FeedbackException(
|
||||
'反馈服务器尚未更新日志附件协议,请先更新服务器,或取消附带日志后提交。填写内容已保留。',
|
||||
);
|
||||
}
|
||||
throw const FeedbackException('填写内容未通过服务器校验,请检查必填内容、长度及附件后重新提交');
|
||||
}
|
||||
if (response.statusCode == 409 && matchesSubmission) {
|
||||
|
||||
@@ -121,7 +121,10 @@ class _SchoolAuthPageState extends State<SchoolAuthPage> {
|
||||
padding: const EdgeInsets.all(12),
|
||||
color: const Color(0xFFEFF6FF),
|
||||
child: Text(
|
||||
s.message,
|
||||
[
|
||||
s.message,
|
||||
if (s.browserVersion != null) 'Chromium ${s.browserVersion}',
|
||||
].join('\n'),
|
||||
style: const TextStyle(fontSize: 12, height: 1.5),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import 'package:webview_flutter/webview_flutter.dart';
|
||||
import 'package:webview_flutter_android/webview_flutter_android.dart';
|
||||
|
||||
import '../../services/diagnostic_log.dart';
|
||||
|
||||
class SchoolBrowserInfo {
|
||||
const SchoolBrowserInfo({this.chromiumVersion});
|
||||
final String? chromiumVersion;
|
||||
}
|
||||
|
||||
typedef SchoolBrowserConfigurator = Future<SchoolBrowserInfo> Function(
|
||||
WebViewController controller,
|
||||
String school,
|
||||
);
|
||||
|
||||
/// Configure only the dedicated school browser before its first request.
|
||||
Future<SchoolBrowserInfo> configureSchoolBrowser(
|
||||
WebViewController controller,
|
||||
String school,
|
||||
) async {
|
||||
final android = controller.platform;
|
||||
if (android is! AndroidWebViewController) return const SchoolBrowserInfo();
|
||||
try {
|
||||
await android.setUseWideViewPort(true);
|
||||
final cookies = WebViewCookieManager().platform;
|
||||
if (cookies is AndroidWebViewCookieManager) {
|
||||
await cookies.setAcceptThirdPartyCookies(android, true);
|
||||
}
|
||||
await controller.setOnConsoleMessage((message) {
|
||||
final text = message.message.toLowerCase();
|
||||
final code = text.contains('mixed content')
|
||||
? 'mixed_content'
|
||||
: text.contains('content security policy')
|
||||
? 'content_security_policy'
|
||||
: text.contains('cors')
|
||||
? 'cross_origin'
|
||||
: text.contains('refused to')
|
||||
? 'resource_refused'
|
||||
: message.level == JavaScriptLogLevel.error
|
||||
? 'script_error'
|
||||
: null;
|
||||
if (code != null) {
|
||||
DiagnosticLog.instance.add(
|
||||
'school_console_error',
|
||||
fields: {'school': school, 'code': code},
|
||||
);
|
||||
}
|
||||
});
|
||||
String? version;
|
||||
try {
|
||||
version = RegExp(r'\bChrome/(\d+(?:\.\d+){0,3})\b')
|
||||
.firstMatch(await android.getUserAgent() ?? '')
|
||||
?.group(1);
|
||||
} catch (_) {
|
||||
/* Version inspection must not block authentication. */
|
||||
}
|
||||
DiagnosticLog.instance.add(
|
||||
'browser_configured',
|
||||
fields: {
|
||||
'school': school,
|
||||
'phase': 'wide_viewport',
|
||||
'result': true,
|
||||
'code': 'third_party_cookies',
|
||||
'version': version,
|
||||
},
|
||||
);
|
||||
return SchoolBrowserInfo(chromiumVersion: version);
|
||||
} catch (error) {
|
||||
DiagnosticLog.instance.add(
|
||||
'browser_setup_failed',
|
||||
fields: {'school': school, 'code': error.runtimeType.toString()},
|
||||
);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import 'school_page_adapter.dart';
|
||||
import 'school_exam_adapter.dart';
|
||||
import 'school_login_flow.dart';
|
||||
import 'school_page_probe.dart';
|
||||
import 'school_browser.dart';
|
||||
|
||||
enum SchoolConnectionState {
|
||||
signedOut,
|
||||
@@ -22,7 +23,28 @@ enum SchoolConnectionState {
|
||||
}
|
||||
|
||||
class SchoolSession extends ChangeNotifier {
|
||||
SchoolSession();
|
||||
SchoolSession({this.configureBrowser = configureSchoolBrowser});
|
||||
final SchoolBrowserConfigurator configureBrowser;
|
||||
Future<SchoolBrowserInfo>? _browserReady;
|
||||
String? browserVersion;
|
||||
Future<void> _loadRequest(Uri uri) async {
|
||||
final controller = web, generation = _pageGeneration, serial = _openSerial;
|
||||
try {
|
||||
final info = await (_browserReady ??= configureBrowser(
|
||||
controller,
|
||||
'graduate',
|
||||
));
|
||||
if (!hasOpened || generation != _pageGeneration || serial != _openSerial) {
|
||||
return;
|
||||
}
|
||||
browserVersion = info.chromiumVersion;
|
||||
await controller.loadRequest(uri);
|
||||
} catch (_) {
|
||||
_browserReady = null;
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
// Creating Android WebView starts its browser engine. Do it on school access,
|
||||
// not while the student is opening an offline timetable.
|
||||
late final WebViewController web = _createWeb();
|
||||
@@ -38,7 +60,7 @@ class SchoolSession extends ChangeNotifier {
|
||||
if (upgrade != null) {
|
||||
final generation = _pageGeneration;
|
||||
try {
|
||||
await web.loadRequest(upgrade);
|
||||
await _loadRequest(upgrade);
|
||||
} catch (_) {
|
||||
if (generation == _pageGeneration) {
|
||||
_failure(
|
||||
@@ -54,6 +76,12 @@ class SchoolSession extends ChangeNotifier {
|
||||
uri,
|
||||
isMainFrame: request.isMainFrame,
|
||||
)) {
|
||||
DiagnosticLog.instance.resourceFailure(
|
||||
'graduate',
|
||||
request.url,
|
||||
code: 'blocked_navigation',
|
||||
mainFrame: request.isMainFrame,
|
||||
);
|
||||
if (kDebugMode && request.isMainFrame) {
|
||||
debugPrint(
|
||||
'Blocked school navigation origin: ${uri?.scheme}://${uri?.host}',
|
||||
@@ -80,6 +108,13 @@ class SchoolSession extends ChangeNotifier {
|
||||
},
|
||||
onPageFinished: _loaded,
|
||||
onWebResourceError: (error) async {
|
||||
DiagnosticLog.instance.resourceFailure(
|
||||
'graduate',
|
||||
error.url,
|
||||
code: 'web_resource_error',
|
||||
status: error.errorCode,
|
||||
mainFrame: error.isForMainFrame,
|
||||
);
|
||||
if (error.isForMainFrame != true || error.url == null) return;
|
||||
final generation = _pageGeneration;
|
||||
final current = await web.currentUrl();
|
||||
@@ -94,6 +129,12 @@ class SchoolSession extends ChangeNotifier {
|
||||
}
|
||||
},
|
||||
onHttpError: (error) async {
|
||||
DiagnosticLog.instance.resourceFailure(
|
||||
'graduate',
|
||||
error.request?.uri.toString(),
|
||||
code: 'http_error',
|
||||
status: error.response?.statusCode,
|
||||
);
|
||||
final generation = _pageGeneration;
|
||||
final current = Uri.tryParse(await web.currentUrl() ?? '');
|
||||
if (hasOpened &&
|
||||
@@ -231,7 +272,7 @@ class SchoolSession extends ChangeNotifier {
|
||||
notifyListeners();
|
||||
if (needsTermSelection) return;
|
||||
try {
|
||||
await web.loadRequest(connected ? _target : authUrl);
|
||||
await _loadRequest(connected ? _target : authUrl);
|
||||
} catch (_) {
|
||||
if (serial == _openSerial && generation == _pageGeneration) {
|
||||
_failure('学校入口无法打开,请检查校园网络后重试。', code: 'entry_load_failed');
|
||||
@@ -264,7 +305,7 @@ class SchoolSession extends ChangeNotifier {
|
||||
_armTimeout();
|
||||
notifyListeners();
|
||||
try {
|
||||
await web.loadRequest(_target);
|
||||
await _loadRequest(_target);
|
||||
} catch (_) {
|
||||
if (serial == _openSerial && generation == _pageGeneration) {
|
||||
_failure('学校学期查询页面暂时无法打开,请重试。', code: 'query_load_failed');
|
||||
@@ -499,7 +540,7 @@ class SchoolSession extends ChangeNotifier {
|
||||
_armTimeout();
|
||||
notifyListeners();
|
||||
try {
|
||||
await web.loadRequest(next);
|
||||
await _loadRequest(next);
|
||||
} catch (_) {
|
||||
if (generation == _pageGeneration) {
|
||||
_failure('课表查询页面暂时无法打开,请重试。', code: 'query_load_failed');
|
||||
|
||||
@@ -168,7 +168,10 @@ class _UndergraduateAuthPageState extends State<UndergraduateAuthPage> {
|
||||
padding: const EdgeInsets.all(12),
|
||||
color: const Color(0xFFEFF6FF),
|
||||
child: Text(
|
||||
s.message,
|
||||
[
|
||||
s.message,
|
||||
if (s.browserVersion != null) 'Chromium ${s.browserVersion}',
|
||||
].join('\n'),
|
||||
style: const TextStyle(fontSize: 12, height: 1.5),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -11,8 +11,13 @@ import 'navigation_policy.dart';
|
||||
import 'page_probe.dart';
|
||||
import 'timetable_adapter.dart';
|
||||
import 'exam_adapter.dart';
|
||||
import '../school_browser.dart';
|
||||
|
||||
class UndergraduateSession extends ChangeNotifier {
|
||||
UndergraduateSession({this.configureBrowser = configureSchoolBrowser});
|
||||
final SchoolBrowserConfigurator configureBrowser;
|
||||
Future<SchoolBrowserInfo>? _browserReady;
|
||||
String? browserVersion;
|
||||
static final loginUri = Uri.parse('https://eams.uestc.edu.cn/eams/');
|
||||
static const manualMessage = '请在学校网页打开课表并选择学期,然后点击导入当前课表。';
|
||||
WebViewController? _web;
|
||||
@@ -99,6 +104,12 @@ class UndergraduateSession extends ChangeNotifier {
|
||||
uri,
|
||||
isMainFrame: request.isMainFrame,
|
||||
)) {
|
||||
DiagnosticLog.instance.resourceFailure(
|
||||
'undergraduate',
|
||||
request.url,
|
||||
code: 'blocked_navigation',
|
||||
mainFrame: request.isMainFrame,
|
||||
);
|
||||
if (request.isMainFrame) {
|
||||
_failure('学校跳转未完成,请重新认证。', code: 'blocked_navigation');
|
||||
}
|
||||
@@ -129,6 +140,13 @@ class UndergraduateSession extends ChangeNotifier {
|
||||
},
|
||||
onPageFinished: _loaded,
|
||||
onWebResourceError: (error) async {
|
||||
DiagnosticLog.instance.resourceFailure(
|
||||
'undergraduate',
|
||||
error.url,
|
||||
code: 'web_resource_error',
|
||||
status: error.errorCode,
|
||||
mainFrame: error.isForMainFrame,
|
||||
);
|
||||
if (error.isForMainFrame != true || error.url == null) return;
|
||||
final g = _generation,
|
||||
d = _document,
|
||||
@@ -142,6 +160,12 @@ class UndergraduateSession extends ChangeNotifier {
|
||||
}
|
||||
},
|
||||
onHttpError: (error) async {
|
||||
DiagnosticLog.instance.resourceFailure(
|
||||
'undergraduate',
|
||||
error.request?.uri.toString(),
|
||||
code: 'http_error',
|
||||
status: error.response?.statusCode,
|
||||
);
|
||||
final g = _generation,
|
||||
d = _document,
|
||||
current = await web.currentUrl();
|
||||
@@ -202,8 +226,16 @@ class UndergraduateSession extends ChangeNotifier {
|
||||
_loading = true;
|
||||
_emit();
|
||||
try {
|
||||
await web.loadRequest(uri);
|
||||
final controller = web;
|
||||
final info = await (_browserReady ??= configureBrowser(
|
||||
controller,
|
||||
'undergraduate',
|
||||
));
|
||||
if (!_valid(g)) return;
|
||||
browserVersion = info.chromiumVersion;
|
||||
await controller.loadRequest(uri);
|
||||
} catch (_) {
|
||||
_browserReady = null;
|
||||
if (_valid(g)) _failure('学校页面打开未完成,请重试。');
|
||||
}
|
||||
}
|
||||
@@ -283,6 +315,11 @@ class UndergraduateSession extends ChangeNotifier {
|
||||
!UndergraduateNavigationPolicy.canNavigate(uri, isMainFrame: true)) {
|
||||
return;
|
||||
}
|
||||
if (!UndergraduateNavigationPolicy.isDataPage(uri)) {
|
||||
_loading = false;
|
||||
_waitForLogin();
|
||||
return;
|
||||
}
|
||||
final status = await _evidence(uri, false);
|
||||
DiagnosticLog.instance.add(
|
||||
'school_evidence',
|
||||
@@ -346,8 +383,17 @@ class UndergraduateSession extends ChangeNotifier {
|
||||
await _requestExam(g);
|
||||
}
|
||||
} catch (error) {
|
||||
final current = await web.currentUrl();
|
||||
if (_valid(g) && d == _document) {
|
||||
_failure('$error', code: error.runtimeType.toString());
|
||||
if (current != url) {
|
||||
DiagnosticLog.instance.add(
|
||||
'school_probe_stale',
|
||||
fields: {'school': 'undergraduate', 'code': 'redirect_in_progress'},
|
||||
);
|
||||
if (current != null) _schedulePoll(g, d, current);
|
||||
} else {
|
||||
_failure('$error', code: error.runtimeType.toString());
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (_probeKey == key) _probeKey = null;
|
||||
|
||||
@@ -94,7 +94,45 @@ class DiagnosticLog {
|
||||
add('navigation_$phase', fields: {'school': school, 'page': page});
|
||||
}
|
||||
|
||||
void resourceFailure(
|
||||
String school,
|
||||
String? url, {
|
||||
required String code,
|
||||
int? status,
|
||||
bool? mainFrame,
|
||||
}) {
|
||||
final uri = Uri.tryParse(url ?? '');
|
||||
final path = uri?.path.toLowerCase() ?? '';
|
||||
final kind = path.endsWith('.js')
|
||||
? 'script'
|
||||
: path.endsWith('.css')
|
||||
? 'stylesheet'
|
||||
: RegExp(r'\.(png|jpg|jpeg|gif|webp|svg|ico)$').hasMatch(path)
|
||||
? 'image'
|
||||
: 'document_or_request';
|
||||
add(
|
||||
'school_resource_failed',
|
||||
fields: {
|
||||
'school': school,
|
||||
'phase': mainFrame == null
|
||||
? 'unknown'
|
||||
: mainFrame
|
||||
? 'main'
|
||||
: 'subresource',
|
||||
'page': kind,
|
||||
'code': code,
|
||||
'status': status,
|
||||
'type': uri?.scheme == 'http'
|
||||
? 'http'
|
||||
: uri?.scheme == 'https'
|
||||
? 'https'
|
||||
: 'other',
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> attach(Directory directory) async {
|
||||
// Retention and export use the same bounded, sanitized entries.
|
||||
final file = File('${directory.path}/diagnostics-recent.json');
|
||||
try {
|
||||
if (await file.exists() && await file.length() <= maxBytes) {
|
||||
|
||||
+1
-1
@@ -618,7 +618,7 @@ packages:
|
||||
source: hosted
|
||||
version: "4.14.1"
|
||||
webview_flutter_android:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: webview_flutter_android
|
||||
sha256: "4de8b3d1ff4ebe1bdb42e68a5e4f809194a3cb0117a8f495f590004f00da3964"
|
||||
|
||||
+2
-1
@@ -1,7 +1,7 @@
|
||||
name: hepan_everyday
|
||||
description: TPSchedule 学生课表
|
||||
publish_to: none
|
||||
version: 0.7.0+18
|
||||
version: 0.7.1+19
|
||||
environment:
|
||||
sdk: ^3.13.5
|
||||
dependencies:
|
||||
@@ -10,6 +10,7 @@ dependencies:
|
||||
flutter_localizations:
|
||||
sdk: flutter
|
||||
webview_flutter: 4.14.1
|
||||
webview_flutter_android: 4.14.1
|
||||
flutter_local_notifications: 22.3.2
|
||||
timezone: 0.11.1
|
||||
html: 0.15.7
|
||||
|
||||
@@ -33,6 +33,39 @@ class FakeFeedbackHttpTransport implements FeedbackHttpTransport {
|
||||
}
|
||||
|
||||
void main() {
|
||||
test(
|
||||
'old feedback service gets a specific diagnostic-upgrade hint',
|
||||
() async {
|
||||
final transport = FakeFeedbackHttpTransport()
|
||||
..response = FeedbackHttpResponse(
|
||||
422,
|
||||
utf8.encode(
|
||||
'{"error":"validation_error","detail":"Unexpected file field."}',
|
||||
),
|
||||
);
|
||||
await expectLater(
|
||||
HttpFeedbackSender(
|
||||
endpoint: 'https://example.com/v1/feedback',
|
||||
transport: transport,
|
||||
).send(
|
||||
FeedbackSubmission.problem(
|
||||
id: 'same-id',
|
||||
message: '问题',
|
||||
diagnosticLog: Uint8List.fromList(
|
||||
utf8.encode('TPSchedule diagnostic log\n'),
|
||||
),
|
||||
),
|
||||
),
|
||||
throwsA(
|
||||
isA<FeedbackException>().having(
|
||||
(e) => e.message,
|
||||
'upgrade hint',
|
||||
allOf(contains('日志'), contains('更新')),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
test('diagnostics use a separate UTF8 attachment field', () async {
|
||||
final transport = FakeFeedbackHttpTransport();
|
||||
await HttpFeedbackSender(
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart';
|
||||
import 'package:hepan_everyday/domain/school_term.dart';
|
||||
import 'package:hepan_everyday/features/school/school_browser.dart';
|
||||
import 'package:hepan_everyday/features/school/school_session.dart';
|
||||
import 'package:hepan_everyday/features/school/undergraduate/session.dart';
|
||||
import 'package:hepan_everyday/services/diagnostic_log.dart';
|
||||
|
||||
import '../support/fake_school_web.dart';
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
test('both school entries wait for browser compatibility settings before loading', () async {
|
||||
for (final undergraduate in [false, true]) {
|
||||
final platform = FakeSchoolPlatform();
|
||||
WebViewPlatform.instance = platform;
|
||||
final ready = Completer<SchoolBrowserInfo>();
|
||||
final graduate = SchoolSession(configureBrowser: (_, _) => ready.future);
|
||||
final under = UndergraduateSession(
|
||||
configureBrowser: (_, _) => ready.future,
|
||||
);
|
||||
addTearDown(graduate.dispose);
|
||||
addTearDown(under.dispose);
|
||||
final opening = undergraduate
|
||||
? under.openTimetable()
|
||||
: graduate.openTimetable(term: SchoolTerm(73));
|
||||
await flushSchoolEvents();
|
||||
expect(platform.web.loads, isEmpty);
|
||||
ready.complete(const SchoolBrowserInfo(chromiumVersion: '155.0.1.2'));
|
||||
await opening;
|
||||
expect(platform.web.loads, hasLength(1));
|
||||
expect(
|
||||
undergraduate ? under.browserVersion : graduate.browserVersion,
|
||||
'155.0.1.2',
|
||||
);
|
||||
}
|
||||
});
|
||||
test(
|
||||
'cancel while browser settings initialize never starts a late navigation',
|
||||
() async {
|
||||
final platform = FakeSchoolPlatform();
|
||||
WebViewPlatform.instance = platform;
|
||||
final ready = Completer<SchoolBrowserInfo>();
|
||||
final session = UndergraduateSession(
|
||||
configureBrowser: (_, _) => ready.future,
|
||||
);
|
||||
addTearDown(session.dispose);
|
||||
final opening = session.openTimetable();
|
||||
await flushSchoolEvents();
|
||||
session.setReadingActive(false);
|
||||
ready.complete(const SchoolBrowserInfo());
|
||||
await opening;
|
||||
expect(platform.web.loads, isEmpty);
|
||||
},
|
||||
);
|
||||
test('subresource failures remain diagnostic without aborting the school session', () async {
|
||||
final platform = FakeSchoolPlatform();
|
||||
WebViewPlatform.instance = platform;
|
||||
final session = UndergraduateSession();
|
||||
addTearDown(session.dispose);
|
||||
await session.openTimetable();
|
||||
platform.delegate.resourceError(
|
||||
WebResourceError(
|
||||
errorCode: -105,
|
||||
description: 'PRIVATE_COOKIE',
|
||||
isForMainFrame: false,
|
||||
url: 'http://eams.uestc.edu.cn/eams/scripts/component.js?ticket=PRIVATE_TICKET',
|
||||
),
|
||||
);
|
||||
await flushSchoolEvents();
|
||||
final log = utf8.decode(await DiagnosticLog.instance.export());
|
||||
expect(log, contains('school_resource_failed'));
|
||||
expect(log, contains('"page":"script"'));
|
||||
expect(log, contains('"status":-105'));
|
||||
expect(log, isNot(contains('PRIVATE_COOKIE')));
|
||||
expect(log, isNot(contains('PRIVATE_TICKET')));
|
||||
expect(session.message, isNot(contains('未完成')));
|
||||
});
|
||||
}
|
||||
@@ -31,6 +31,48 @@ void main() {
|
||||
session = UndergraduateSession();
|
||||
});
|
||||
tearDown(() => session.dispose());
|
||||
test(
|
||||
'page-finished during an old probe is retried on the redirected portal',
|
||||
() async {
|
||||
await session.openTimetable();
|
||||
var first = true;
|
||||
platform.web.javascript = (_) async {
|
||||
if (first) {
|
||||
first = false;
|
||||
platform.web.url = 'https://eams.uestc.edu.cn/eams/home.action';
|
||||
platform.delegate.finished(platform.web.url!);
|
||||
}
|
||||
return underEvidence(platform.web.url!);
|
||||
};
|
||||
platform.delegate.finished(platform.web.url!);
|
||||
await flushSchoolEvents();
|
||||
await Future<void>.delayed(const Duration(milliseconds: 600));
|
||||
await flushSchoolEvents();
|
||||
expect(session.statusLabel, '已连接');
|
||||
expect(session.busy, isFalse);
|
||||
},
|
||||
);
|
||||
test('a redirect between page callback and probe does not permanently stop login detection', () async {
|
||||
await session.openTimetable();
|
||||
var redirect = true;
|
||||
platform.web.javascript = (_) async {
|
||||
if (redirect) {
|
||||
redirect = false;
|
||||
platform.web.url = 'https://idas.uestc.edu.cn/authserver/login';
|
||||
return underEvidence(platform.web.url!, authenticated: false);
|
||||
}
|
||||
return underEvidence(platform.web.url!);
|
||||
};
|
||||
platform.delegate.finished(platform.web.url!);
|
||||
await flushSchoolEvents();
|
||||
expect(session.message, isNot(contains('未完成')));
|
||||
platform.delegate.finished(platform.web.url!);
|
||||
await flushSchoolEvents();
|
||||
platform.web.url = 'https://eams.uestc.edu.cn/eams/';
|
||||
platform.delegate.finished(platform.web.url!);
|
||||
await flushSchoolEvents();
|
||||
expect(session.statusLabel, '已连接');
|
||||
});
|
||||
test(
|
||||
'activating a loading browser never probes the previous blank document',
|
||||
() async {
|
||||
|
||||
Reference in New Issue
Block a user