fix: follow undergraduate login links and capture live history URLs
This commit is contained in:
@@ -1,10 +1,10 @@
|
|||||||
# TPSchedule
|
# TPSchedule
|
||||||
|
|
||||||
当前Android版本0.7.3+21:本科登录后缺少公共脚本和样式时,沿用同一学校会话尝试恢复完整页面。包含实际恢复逻辑,仍需校园网内真实账号复测。详见[0.7.3验证记录](docs/tpschedule-0.7.3-validation.md)。
|
当前Android版本0.7.4+22:修复本科重复登录“继续”链接被拦截,以及完整页面显示课表后导入报“页面证据无效”。保留0.7.3完整页恢复和来源校验,详见[0.7.4验证记录](docs/tpschedule-0.7.4-validation.md)。
|
||||||
|
|
||||||
0.7.0补充:考试页查询类型与卡片保持16像素间距;反馈问题和建议可自选附带最近10分钟的登录/读取/提醒诊断日志。默认不发送,不含密码、Cookie或课表内容,单独作为邮件文本附件。服务端需按[日志附件更新说明](server/feedback/diagnostic-log-update.md)手动更新;真实学校登录及SMTP未由本次离线测试代替验证。玻璃取样偏移仍在调查,未纳入本次修复结论。
|
0.7.0补充:考试页查询类型与卡片保持16像素间距;反馈问题和建议可自选附带最近10分钟的登录/读取/提醒诊断日志。默认不发送,不含密码、Cookie或课表内容,单独作为邮件文本附件。服务端需按[日志附件更新说明](server/feedback/diagnostic-log-update.md)手动更新;真实学校登录及SMTP未由本次离线测试代替验证。玻璃取样偏移仍在调查,未纳入本次修复结论。
|
||||||
|
|
||||||
学生课表 APP,Flutter 实现,支持电子科技大学研究生和本科生系统。Android 0.7.3+21,Android 7.0 及以上。
|
学生课表 APP,Flutter 实现,支持电子科技大学研究生和本科生系统。Android 0.7.4+22,Android 7.0 及以上。
|
||||||
|
|
||||||
使用用户提供的原始 SVG 图标,蓝色渐变、黄色日历框及白色笔画保持一致;启动画面使用相同配色。0.5.1修复重复启动页闪烁:初始化期间保留原生启动屏,数据就绪后直接显示课表首帧,不再叠加Flutter品牌页或淡出动画。Android12+ 原生启动屏使用同色纯蓝背景及图标,旧版采用蓝色渐变。
|
使用用户提供的原始 SVG 图标,蓝色渐变、黄色日历框及白色笔画保持一致;启动画面使用相同配色。0.5.1修复重复启动页闪烁:初始化期间保留原生启动屏,数据就绪后直接显示课表首帧,不再叠加Flutter品牌页或淡出动画。Android12+ 原生启动屏使用同色纯蓝背景及图标,旧版采用蓝色渐变。
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Android 首版验证记录
|
# Android 首版验证记录
|
||||||
|
|
||||||
当前完整页面恢复版为0.7.3+21,验证见 [0.7.3记录](tpschedule-0.7.3-validation.md)。本科接入记录见 [0.7.0记录](tpschedule-0.7.0-validation.md)。下文为首版历史记录。
|
当前本科登录与导入修复版为0.7.4+22,验证见 [0.7.4记录](tpschedule-0.7.4-validation.md)。本科接入记录见 [0.7.0记录](tpschedule-0.7.0-validation.md)。下文为首版历史记录。
|
||||||
|
|
||||||
日期:2026-10-13。测试环境:Flutter 3.47.7 / Dart 3.13.5 / JDK 17;Android 16 API 36,Pixel 7 模拟器,1080×2400。首版版本 0.1.0+1,包 ID com.example.hepan_everyday,最低 Android 7.0(API 24)。
|
日期:2026-10-13。测试环境:Flutter 3.47.7 / Dart 3.13.5 / JDK 17;Android 16 API 36,Pixel 7 模拟器,1080×2400。首版版本 0.1.0+1,包 ID com.example.hepan_everyday,最低 Android 7.0(API 24)。
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
# 学校接入状态 · TPSchedule 0.7.3
|
# 学校接入状态 · TPSchedule 0.7.4
|
||||||
|
|
||||||
|
0.7.4将本科重复登录的准确学校HTTP继续链接升级为HTTPS,并解决完整页恢复后History API改变实际文档地址、原生历史地址未同步导致的“页面证据无效”。现按实际文档地址抓取且保留同源及精确校验,见[0.7.4记录](tpschedule-0.7.4-validation.md)。
|
||||||
|
|
||||||
0.7.2真机日志确认本科认证后缺少公共脚本和样式,框架片段输出规则与WebView请求头行为是高概率原因。0.7.3增加有界的同会话完整页恢复;已通过离线及Android原生验证,仍待校园网真实账号复测。详见 [0.7.3记录](tpschedule-0.7.3-validation.md)。
|
0.7.2真机日志确认本科认证后缺少公共脚本和样式,框架片段输出规则与WebView请求头行为是高概率原因。0.7.3增加有界的同会话完整页恢复;已通过离线及Android原生验证,仍待校园网真实账号复测。详见 [0.7.3记录](tpschedule-0.7.3-validation.md)。
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
# TPSchedule 0.7.4+22:重复登录继续链接与课表导入修复
|
||||||
|
|
||||||
|
2026-10-15,F:/program/app。用户0.7.3日志显示完整页恢复成功,学校页面已能显示;重复登录继续链接被HTTP导航策略拦截,恢复后状态读取出现FormatException,手动导入也提示“页面证据无效”。本记录不含原始HTML、原始URL查询或账号信息。
|
||||||
|
|
||||||
|
## 原因与修改
|
||||||
|
|
||||||
|
重复登录链接:原策略只将学校身份认证HTTP入口转为HTTPS,教务系统自身的HTTP继续链接被阻止。现对准确eams.uestc.edu.cn、默认HTTP端口、/eams系统路径的主页面导航升级到HTTPS,保留查询与片段;仍不直接加载HTTP,不接管POST提交,其他域名、用户信息和自定义端口不升级。
|
||||||
|
|
||||||
|
导入失败:原始学校依赖包含History API网址更新。原生模拟器复现:经现有loadDataWithBaseURL恢复页面,再调用history.replaceState,JavaScript的location.href已变为课表网址,而WebView.getUrl仍保留恢复时的地址。原实现把后者当作抓取脚本的确切地址,因而拒绝页面证据。
|
||||||
|
|
||||||
|
读取前现从实际文档取得location.href,先要求原生地址与文档地址均为准确学校HTTPS数据路径且同源,再将这一次实际地址与抓取结果严格比较。读取期间再次换网址仍拒绝;跨域、认证页、不安全地址、页面代次失效、体积上限和原数据保留规则不变。考试目标比较使用已验证的文档网址;实际文档与原生历史地址不一致时不重复执行完整页恢复。日志只记录history_state固定类别,不保存原始URL。
|
||||||
|
|
||||||
|
## 验证
|
||||||
|
|
||||||
|
先观察到重复登录链接未升级的回归失败;实现升级后通过。针对可见课表但原生历史地址未更新的会话测试,旧实现报“页面证据无效”,修复后识别18条测试课程并生成导入草稿。新测试继续确认跨域/HTTP/认证地址及抓取期间URL变化均拒绝。纯解码器保持精确地址校验。
|
||||||
|
|
||||||
|
完整Flutter套件271项通过,4个实际JavaScript脚本检查通过,静态检查无问题。Android JVM16项通过,API36实际WebView夹具检查通过,包含恢复后的中文/脚本/CSS/来源/会话/文档保护及History API使原生地址滞后的边界复现。模拟器Chromium133.0.6943.137,用户日志为156.0.8078.4。
|
||||||
|
|
||||||
|
独立复审未发现阻塞项:精确学校来源及抓取时网址校验保留,原完整页恢复原生文档保护不变。审查指出Android POST表单不会进入该主导航回调,升级范围符合点击继续链接的处理需求。
|
||||||
|
|
||||||
|
## 复测
|
||||||
|
|
||||||
|
覆盖安装0.7.4,校园网内登录本科系统。如出现重复登录提示,点击继续,确认进入完整学校页面;在网页打开课表、选择学期,再点“导入当前课表”应进入预览。线上账号与用户手机Chromium156仍需用户复测,模拟器133的复现和离线课程回归不能代替这一确认。
|
||||||
|
|
||||||
|
## 安装包
|
||||||
|
|
||||||
|
发布包com.tpschedule.app,versionName=0.7.4、versionCode=22,60155727字节。TPSchedule-0.7.4.apk与TPSchedule.apk一致,SHA256为5B529C611E625614905E972B857F31A3520F958188E5292FF1918C4903BFD3D8。与0.7.3签名证书逐字节一致,原签名文件保留。
|
||||||
|
|
||||||
|
源码包由最终提交生成,不包含原始学生HTML、私钥、运行缓存或登记材料;交付打包时重核0.5.4登记冻结18项及旧版安装包。
|
||||||
+2
-2
@@ -1,8 +1,8 @@
|
|||||||
# TPSchedule Flutter工程
|
# TPSchedule Flutter工程
|
||||||
|
|
||||||
当前版本0.7.3+21,安卓包名 `com.tpschedule.app`。Dart包名保留 `hepan_everyday`,兼容既有代码引用。
|
当前版本0.7.4+22,安卓包名 `com.tpschedule.app`。Dart包名保留 `hepan_everyday`,兼容既有代码引用。
|
||||||
|
|
||||||
学校浏览器沿用宽视口及第三方Cookie配置。0.7.3新增本科已登录缺依赖页面的同会话完整页恢复,保留原学校地址与来源;失败或切页不覆盖旧页面和课表,真实账号仍需复测。见 `../docs/tpschedule-0.7.3-validation.md`。
|
0.7.4修复本科重复登录HTTP继续链接及恢复后History API导致的网址不同步,导入绑定实际文档地址并严格校验同一学校来源。完整页恢复保留,真实账号仍需复测,见 `../docs/tpschedule-0.7.4-validation.md`。
|
||||||
|
|
||||||
## 本科使用
|
## 本科使用
|
||||||
|
|
||||||
|
|||||||
+5
@@ -82,6 +82,11 @@ public final class SchoolDocumentRenderInstrumentation extends Instrumentation {
|
|||||||
require(!applied.get(), "changed_session_accepted");
|
require(!applied.get(), "changed_session_accepted");
|
||||||
JSONObject retained = evaluate("JSON.stringify({text:document.getElementById('probe').textContent})");
|
JSONObject retained = evaluate("JSON.stringify({text:document.getElementById('probe').textContent})");
|
||||||
require("电子科技大学 • 课表 ✓".equals(retained.getString("text")), "failed_render_changed_dom");
|
require("电子科技大学 • 课表 ✓".equals(retained.getString("text")), "failed_render_changed_dom");
|
||||||
|
JSONObject history = evaluate("history.replaceState({},'', '/eams/courseTableForStd.action?semester.id=523'); JSON.stringify({href:location.href})");
|
||||||
|
AtomicReference<String> historyUrl = new AtomicReference<>();
|
||||||
|
runOnMainSync(() -> historyUrl.set(view.getUrl()));
|
||||||
|
require("https://eams.uestc.edu.cn/eams/courseTableForStd.action?semester.id=523".equals(history.getString("href")), "live_history_url_missing");
|
||||||
|
require(URL.equals(historyUrl.get()), "native_history_characterization_changed");
|
||||||
result.putString("stream", "SchoolDocument render: URL/origin/Unicode/scripts/CSS/session/document guards passed\n");
|
result.putString("stream", "SchoolDocument render: URL/origin/Unicode/scripts/CSS/session/document guards passed\n");
|
||||||
sendStatus(0,result);
|
sendStatus(0,result);
|
||||||
finishCode = Activity.RESULT_OK;
|
finishCode = Activity.RESULT_OK;
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
const appVersion = '0.7.3';
|
const appVersion = '0.7.4';
|
||||||
const appBuildNumber = 21;
|
const appBuildNumber = 22;
|
||||||
|
|||||||
@@ -17,6 +17,23 @@ class UndergraduateNavigationPolicy {
|
|||||||
uri.path.startsWith('/eams/');
|
uri.path.startsWith('/eams/');
|
||||||
static bool isLogout(Uri uri) =>
|
static bool isLogout(Uri uri) =>
|
||||||
isDataPage(uri) && uri.path == '/eams/logout.action';
|
isDataPage(uri) && uri.path == '/eams/logout.action';
|
||||||
static Uri? secureIdentityRedirect(Uri uri) =>
|
static Uri? secureIdentityRedirect(Uri uri) {
|
||||||
SchoolNavigationPolicy.secureIdentityRedirect(uri);
|
final identity = SchoolNavigationPolicy.secureIdentityRedirect(uri);
|
||||||
|
if (identity != null) return identity;
|
||||||
|
// Legacy duplicate-login links use HTTP even within the HTTPS portal.
|
||||||
|
if (uri.scheme != 'http' ||
|
||||||
|
uri.host != 'eams.uestc.edu.cn' ||
|
||||||
|
uri.port != 80 ||
|
||||||
|
uri.userInfo.isNotEmpty ||
|
||||||
|
!(uri.path == '/eams' || uri.path.startsWith('/eams/'))) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return Uri(
|
||||||
|
scheme: 'https',
|
||||||
|
host: uri.host,
|
||||||
|
path: uri.path,
|
||||||
|
query: uri.hasQuery ? uri.query : null,
|
||||||
|
fragment: uri.hasFragment ? uri.fragment : null,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,9 +67,10 @@ class UndergraduateFragment {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class UndergraduatePageEvidence {
|
class UndergraduatePageEvidence {
|
||||||
const UndergraduatePageEvidence(this.authenticated, this.pages);
|
const UndergraduatePageEvidence(this.authenticated, this.pages, this.url);
|
||||||
final bool authenticated;
|
final bool authenticated;
|
||||||
final List<UndergraduateFragment> pages;
|
final List<UndergraduateFragment> pages;
|
||||||
|
final Uri url;
|
||||||
static UndergraduatePageEvidence decode(Object raw, Uri current) {
|
static UndergraduatePageEvidence decode(Object raw, Uri current) {
|
||||||
try {
|
try {
|
||||||
dynamic value = raw;
|
dynamic value = raw;
|
||||||
@@ -116,6 +117,7 @@ class UndergraduatePageEvidence {
|
|||||||
return UndergraduatePageEvidence(
|
return UndergraduatePageEvidence(
|
||||||
value['authenticated'] == true,
|
value['authenticated'] == true,
|
||||||
List.unmodifiable(result),
|
List.unmodifiable(result),
|
||||||
|
current,
|
||||||
);
|
);
|
||||||
} on FormatException {
|
} on FormatException {
|
||||||
rethrow;
|
rethrow;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:webview_flutter/webview_flutter.dart';
|
import 'package:webview_flutter/webview_flutter.dart';
|
||||||
@@ -318,13 +319,35 @@ class UndergraduateSession extends ChangeNotifier {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<UndergraduatePageEvidence> _evidence(Uri uri, bool capture) async =>
|
Future<UndergraduatePageEvidence> _evidence(Uri uri, bool capture) async {
|
||||||
UndergraduatePageEvidence.decode(
|
// loadDataWithBaseURL can leave native getUrl at the original address after
|
||||||
await web.runJavaScriptReturningResult(
|
// the school's History API changes location. Bind capture to the live URL,
|
||||||
capture ? undergraduateCaptureScript : undergraduateStatusProbeScript,
|
// still requiring the native and live documents to share the school origin.
|
||||||
),
|
dynamic location = await web.runJavaScriptReturningResult('location.href');
|
||||||
uri,
|
if (location is String && location.startsWith('"')) {
|
||||||
|
location = jsonDecode(location);
|
||||||
|
}
|
||||||
|
final live = location is String ? Uri.tryParse(location) : null;
|
||||||
|
if (live == null ||
|
||||||
|
!UndergraduateNavigationPolicy.isDataPage(uri) ||
|
||||||
|
!UndergraduateNavigationPolicy.isDataPage(live) ||
|
||||||
|
uri.origin != live.origin) {
|
||||||
|
throw const FormatException('页面来源无效,请重新打开学校课表。');
|
||||||
|
}
|
||||||
|
if (live != uri) {
|
||||||
|
DiagnosticLog.instance.add(
|
||||||
|
'school_document_url',
|
||||||
|
fields: {'school': 'undergraduate', 'code': 'history_state'},
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
return UndergraduatePageEvidence.decode(
|
||||||
|
await web.runJavaScriptReturningResult(
|
||||||
|
capture ? undergraduateCaptureScript : undergraduateStatusProbeScript,
|
||||||
|
),
|
||||||
|
live,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _loaded(String url) async {
|
Future<void> _loaded(String url) async {
|
||||||
DiagnosticLog.instance.navigation('undergraduate', 'finished', url);
|
DiagnosticLog.instance.navigation('undergraduate', 'finished', url);
|
||||||
final g = _generation, d = _document, key = '$_generation:$_document';
|
final g = _generation, d = _document, key = '$_generation:$_document';
|
||||||
@@ -361,18 +384,20 @@ class UndergraduateSession extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
_connected = true;
|
_connected = true;
|
||||||
awaitingAuthentication = false;
|
awaitingAuthentication = false;
|
||||||
final recovery = await _pageRecovery.tryRestore(
|
final recovery = uri != status.url
|
||||||
web,
|
? FullPageRecoveryResult.unchanged
|
||||||
uri,
|
: await _pageRecovery.tryRestore(
|
||||||
authenticated: true,
|
web,
|
||||||
documentKey: '$g:$d',
|
uri,
|
||||||
isCurrent: () => _valid(g) && d == _document,
|
authenticated: true,
|
||||||
onStarted: () {
|
documentKey: '$g:$d',
|
||||||
_recoveringPage = true;
|
isCurrent: () => _valid(g) && d == _document,
|
||||||
message = '已登录,正在恢复完整教务页面…';
|
onStarted: () {
|
||||||
_emit();
|
_recoveringPage = true;
|
||||||
},
|
message = '已登录,正在恢复完整教务页面…';
|
||||||
);
|
_emit();
|
||||||
|
},
|
||||||
|
);
|
||||||
if (!_valid(g) || d != _document) return;
|
if (!_valid(g) || d != _document) return;
|
||||||
_recoveringPage = false;
|
_recoveringPage = false;
|
||||||
if (recovery == FullPageRecoveryResult.restored) {
|
if (recovery == FullPageRecoveryResult.restored) {
|
||||||
@@ -397,7 +422,7 @@ class UndergraduateSession extends ChangeNotifier {
|
|||||||
_emit();
|
_emit();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (_expected == null || uri.toString() != _expected.toString()) {
|
if (_expected == null || status.url.toString() != _expected.toString()) {
|
||||||
await _requestExam(g);
|
await _requestExam(g);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
name: hepan_everyday
|
name: hepan_everyday
|
||||||
description: TPSchedule 学生课表
|
description: TPSchedule 学生课表
|
||||||
publish_to: none
|
publish_to: none
|
||||||
version: 0.7.3+21
|
version: 0.7.4+22
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.13.5
|
sdk: ^3.13.5
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|||||||
@@ -6,6 +6,51 @@ import 'package:hepan_everyday/features/school/undergraduate/page_probe.dart';
|
|||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
final uri = Uri.parse('https://eams.uestc.edu.cn/eams/');
|
final uri = Uri.parse('https://eams.uestc.edu.cn/eams/');
|
||||||
|
test(
|
||||||
|
'legacy HTTP continuation upgrade is limited to the exact school system',
|
||||||
|
() {
|
||||||
|
expect(
|
||||||
|
UndergraduateNavigationPolicy.secureIdentityRedirect(
|
||||||
|
Uri.parse(
|
||||||
|
'http://eams.uestc.edu.cn/eams/home.action?login=1&next=%2Feams%2F#continue',
|
||||||
|
),
|
||||||
|
).toString(),
|
||||||
|
'https://eams.uestc.edu.cn/eams/home.action?login=1&next=%2Feams%2F#continue',
|
||||||
|
);
|
||||||
|
for (final address in [
|
||||||
|
'http://eams.uestc.edu.cn.attacker.test/eams/home.action',
|
||||||
|
'http://user@eams.uestc.edu.cn/eams/home.action',
|
||||||
|
'http://eams.uestc.edu.cn:8080/eams/home.action',
|
||||||
|
'http://eams.uestc.edu.cn/other/action',
|
||||||
|
]) {
|
||||||
|
expect(
|
||||||
|
UndergraduateNavigationPolicy.secureIdentityRedirect(
|
||||||
|
Uri.parse(address),
|
||||||
|
),
|
||||||
|
isNull,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
test(
|
||||||
|
'decoder still requires captured URL to match the live document URL',
|
||||||
|
() {
|
||||||
|
expect(
|
||||||
|
() => UndergraduatePageEvidence.decode({
|
||||||
|
'topUrl': 'https://eams.uestc.edu.cn/eams/courseTableForStd.action?semester.id=523',
|
||||||
|
'authenticated': true,
|
||||||
|
'pages': [
|
||||||
|
{
|
||||||
|
'kind': 'timetable',
|
||||||
|
'url': 'https://eams.uestc.edu.cn/eams/courseTableForStd.action?semester.id=523',
|
||||||
|
'html': 'table',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}, uri),
|
||||||
|
throwsFormatException,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
test(
|
test(
|
||||||
'navigation and capture distinguish official data from authentication',
|
'navigation and capture distinguish official data from authentication',
|
||||||
() {
|
() {
|
||||||
|
|||||||
@@ -31,6 +31,59 @@ void main() {
|
|||||||
session = UndergraduateSession();
|
session = UndergraduateSession();
|
||||||
});
|
});
|
||||||
tearDown(() => session.dispose());
|
tearDown(() => session.dispose());
|
||||||
|
test('live URL cannot cross origin or change again during capture', () async {
|
||||||
|
await session.openTimetable();
|
||||||
|
final content = File('test/fixtures/school/undergraduate-timetable.html')
|
||||||
|
.readAsStringSync();
|
||||||
|
platform.web.javascript = (_) async => underEvidence(
|
||||||
|
'https://eams.uestc.edu.cn/eams/courseTableForStd.action?semester.id=523',
|
||||||
|
html: content,
|
||||||
|
);
|
||||||
|
for (final address in [
|
||||||
|
'https://other.test/eams/courseTableForStd.action',
|
||||||
|
'https://idas.uestc.edu.cn/authserver/login',
|
||||||
|
'http://eams.uestc.edu.cn/eams/courseTableForStd.action',
|
||||||
|
'https://eams.uestc.edu.cn/eams/courseTableForStd.action?semester.id=503',
|
||||||
|
]) {
|
||||||
|
platform.web.documentUrl = address;
|
||||||
|
await session.readCurrentTimetable();
|
||||||
|
expect(session.pendingDraft, isNull);
|
||||||
|
expect(session.message, contains('未完成'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
test('visible timetable imports after same-document history changes native URL does not follow', () async {
|
||||||
|
await session.openTimetable();
|
||||||
|
platform.web.documentUrl = 'https://eams.uestc.edu.cn/eams/courseTableForStd.action?semester.id=523';
|
||||||
|
final content = File('test/fixtures/school/undergraduate-timetable.html')
|
||||||
|
.readAsStringSync();
|
||||||
|
platform.web.javascript = (_) async => underEvidence(
|
||||||
|
'https://eams.uestc.edu.cn/eams/courseTableForStd.action?semester.id=523',
|
||||||
|
html: content,
|
||||||
|
);
|
||||||
|
platform.delegate.finished(platform.web.url!);
|
||||||
|
await flushSchoolEvents();
|
||||||
|
expect(session.statusLabel, '已连接');
|
||||||
|
await session.readCurrentTimetable();
|
||||||
|
expect(session.pendingDraft!.courses, hasLength(18));
|
||||||
|
});
|
||||||
|
test(
|
||||||
|
'duplicate login continuation upgrades exact school HTTP to HTTPS',
|
||||||
|
() async {
|
||||||
|
await session.openTimetable();
|
||||||
|
final decision = await platform.delegate.navigation(
|
||||||
|
NavigationRequest(
|
||||||
|
url: 'http://eams.uestc.edu.cn/eams/home.action?login=1',
|
||||||
|
isMainFrame: true,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
expect(decision, NavigationDecision.prevent);
|
||||||
|
expect(
|
||||||
|
platform.web.loads.last,
|
||||||
|
'https://eams.uestc.edu.cn/eams/home.action?login=1',
|
||||||
|
);
|
||||||
|
expect(session.message, isNot(contains('未完成')));
|
||||||
|
},
|
||||||
|
);
|
||||||
test(
|
test(
|
||||||
'page-finished during an old probe is retried on the redirected portal',
|
'page-finished during an old probe is retried on the redirected portal',
|
||||||
() async {
|
() async {
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ class FakeSchoolWeb extends PlatformWebViewController {
|
|||||||
FakeSchoolWeb(super.params) : super.implementation();
|
FakeSchoolWeb(super.params) : super.implementation();
|
||||||
final loads = <String>[];
|
final loads = <String>[];
|
||||||
String? url;
|
String? url;
|
||||||
|
String? documentUrl;
|
||||||
Future<Object> Function(String)? javascript;
|
Future<Object> Function(String)? javascript;
|
||||||
Future<void> Function(Uri)? request;
|
Future<void> Function(Uri)? request;
|
||||||
@override
|
@override
|
||||||
@@ -63,7 +64,9 @@ class FakeSchoolWeb extends PlatformWebViewController {
|
|||||||
Future<String?> currentUrl() async => url;
|
Future<String?> currentUrl() async => url;
|
||||||
@override
|
@override
|
||||||
Future<Object> runJavaScriptReturningResult(String script) =>
|
Future<Object> runJavaScriptReturningResult(String script) =>
|
||||||
javascript?.call(script) ?? Future.value(false);
|
script == 'location.href'
|
||||||
|
? Future.value(documentUrl ?? url ?? 'about:blank')
|
||||||
|
: javascript?.call(script) ?? Future.value(false);
|
||||||
@override
|
@override
|
||||||
Future<void> clearCache() async {}
|
Future<void> clearCache() async {}
|
||||||
@override
|
@override
|
||||||
|
|||||||
Reference in New Issue
Block a user