feat(e2e): autologin patch (dev) + 登录态路由测试脚本

- util_web devAutologin: localStorage[_test_token]=user|pass -> 登录API写Hive+dispatch currentUser
- 延迟3s执行等 Flutter/Hive ready (否则 httpPost hang)
- main.dart 调用 devAutologin
- run_auth.js: 注入token遍历登录态路由
- 已验证: autologin API 200 OK
This commit is contained in:
2026-08-03 01:50:12 +08:00
parent 901e42a41b
commit 33b54fe454
10 changed files with 221 additions and 0 deletions

View File

@@ -43,6 +43,7 @@ void main() {
// configureApp(); // configureApp();
setPathUrlStrategy(); setPathUrlStrategy();
runApp(MyApp()); runApp(MyApp());
Future.delayed(Duration(seconds: 3), () => Util.devAutologin()); // DEV e2e: 延迟 3s 等 Flutter/Hive ready
} }
class MyApp extends StatelessWidget { class MyApp extends StatelessWidget {

View File

@@ -199,6 +199,9 @@ class Util {
return box; return box;
} }
/// DEV ONLY stub (原生不自动登录)
static void devAutologin() {}
static Widget showImage(String imageUrl, {double? width, double? height, static Widget showImage(String imageUrl, {double? width, double? height,
BoxFit? fit, Widget Function(BuildContext, String, dynamic)? errorWidget}) { BoxFit? fit, Widget Function(BuildContext, String, dynamic)? errorWidget}) {
if (imageUrl != null && imageUrl.isNotEmpty && imageUrl.startsWith('https:')) { if (imageUrl != null && imageUrl.isNotEmpty && imageUrl.startsWith('https:')) {

View File

@@ -41,6 +41,28 @@ class Util {
return box; return box;
} }
/// DEV ONLY (e2e tests): 读 localStorage['_test_token'] = 'username|password',
/// 调登录 API 写 Hive + dispatch currentUser。测试后移除。
static Future<void> devAutologin() async {
try {
final t = window.localStorage['_test_token'];
if (t == null || t.isEmpty) return;
final parts = t.split('|');
if (parts.length < 2) return;
print('CK_AUTOLOGIN_CALL');
await HttpUtil.httpPost('v1/oauth-wisetronic/access_token', (response) async {
final box = await getBox();
await box.put(Constants.KEY_ACCESS_TOKEN, response.data['access_token']);
await box.put(Constants.KEY_USER_ID, response.data['user_id']);
store.dispatch(UpdateCurrentUser(User.fromJson(response.data['user'])));
print('CK_AUTOLOGIN_OK');
},
queryParameters: {'client_id': Utils.getPlatformName(), 'grant_type': 'password'},
body: {'username': parts[0], 'password': parts[1], 'fcm_token': ''},
isFormData: true, businessId: Constants.BUSINESS_ID);
} catch (e) { print('CK_AUTOLOGIN_EX: ${e.toString().substring(0, 150)}'); }
}
static Widget showImage(String imageUrl, {double? width, double? height, static Widget showImage(String imageUrl, {double? width, double? height,
BoxFit? fit, Widget Function(BuildContext, String?, dynamic)? errorWidget}) { BoxFit? fit, Widget Function(BuildContext, String?, dynamic)? errorWidget}) {
return Image.network(imageUrl, return Image.network(imageUrl,

86
tools/e2e/run_auth.js Normal file
View File

@@ -0,0 +1,86 @@
// 登录态路由测试:注入 _test_tokenautologin 后遍历需登录路由
const { chromium } = require('playwright');
const BASE = 'http://127.0.0.1:8099';
const USER = '6477108200';
const PASS = '8818';
// 需登录路由(动态参数用合理默认)
const AUTH_ROUTES = [
'/me',
'/orders',
'/user-profile',
'/my-cards',
'/my-addresses/310',
'/coupons/310',
'/my-support/310',
'/new-ticket/310',
'/change-password',
'/checkout/310',
];
(async () => {
const browser = await chromium.launch({ headless: true });
const ctx = await browser.newContext({ viewport: { width: 1280, height: 900 } });
await ctx.addInitScript(([u, p]) => { localStorage['_test_token'] = u + '|' + p; }, [USER, PASS]);
// 先访问首页触发 autologin
const page = await ctx.newPage();
let autologinOk = false;
page.on('console', m => { if (m.text().includes('CK_AUTOLOGIN_OK')) autologinOk = true; });
console.log('触发 autologin...');
await page.goto(BASE + '/', { waitUntil: 'load', timeout: 60000 });
await page.waitForSelector('canvas', { timeout: 90000 });
await page.reload({ waitUntil: 'load' });
await page.waitForTimeout(8000);
console.log('autologin:', autologinOk ? '✓ 成功' : '✗ 未确认\n');
const results = [];
for (const route of AUTH_ROUTES) {
const errors = [];
const stacks = [];
const pageErrs = [];
page.removeAllListeners('console');
page.removeAllListeners('pageerror');
page.on('console', m => {
const t = m.text();
if (t.startsWith('CK_ERROR:')) errors.push(t.slice(0, 120));
if (t.startsWith('CK_STACK:')) {
const proj = t.match(/flutter_wisetronic\/[^\s]+:\d+/g);
if (proj) stacks.push([...new Set(proj)].slice(0, 2));
}
});
page.on('pageerror', e => pageErrs.push(e.message.slice(0, 120)));
try {
await page.goto(BASE + route, { waitUntil: 'load', timeout: 30000 });
await page.waitForTimeout(6000);
} catch (e) {
errors.push('GOTO: ' + e.message.slice(0, 80));
}
const allErr = [...errors, ...pageErrs];
const status = allErr.length === 0 ? '✓ OK' : `${allErr.length}`;
console.log(`${status.padEnd(10)} ${route}`);
if (allErr.length) {
console.log(` ${allErr[0].slice(0, 120)}`);
if (stacks.length) console.log(` 位置: ${JSON.stringify(stacks[0])}`);
}
results.push({ route, errors: allErr, stacks });
}
const failed = results.filter(r => r.errors.length > 0);
console.log(`\n========== 汇总 ==========`);
console.log(`${results.length} 个登录态路由,${failed.length} 个有错误`);
if (failed.length) {
const locMap = {};
for (const f of failed) {
const loc = f.stacks[0] ? f.stacks[0][0] : f.errors[0].slice(0, 40);
locMap[loc] = locMap[loc] || [];
locMap[loc].push(f.route);
}
for (const [loc, routes] of Object.entries(locMap)) {
console.log(` ${loc}\n${routes.join(', ')}`);
}
}
await browser.close();
})();

View File

@@ -0,0 +1,22 @@
const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch({ headless: true });
const ctx = await browser.newContext({ viewport: { width: 1280, height: 900 } });
// 注入 _test_tokenaddInitScript 在每个页面加载前执行)
await ctx.addInitScript(() => { localStorage['_test_token'] = '6477108200|8818'; });
const page = await ctx.newPage();
const logs = [];
page.on('console', m => logs.push(m.text()));
await page.goto('http://127.0.0.1:8099/', { waitUntil: 'load', timeout: 60000 });
await page.waitForTimeout(8000); // 等 autologin
const ok = logs.filter(l => l.includes('CK_AUTOLOGIN_OK'));
const fail = logs.filter(l => l.includes('CK_AUTOLOGIN_FAIL'));
console.log('autologin:', ok.length ? '✓ OK' : (fail.length ? '✗ FAIL '+fail[0].slice(0,100) : '? 未触发'));
// 访问 /me登录态
await page.goto('http://127.0.0.1:8099/me', { waitUntil: 'load' }).catch(()=>{});
await page.waitForTimeout(6000);
const errs = logs.filter(l => l.startsWith('CK_ERROR') || l.includes('PAGEERROR'));
console.log('/me 错误数:', errs.length);
if (errs.length) console.log(' ', errs[0].slice(0,150));
await browser.close();
})();

View File

@@ -0,0 +1,16 @@
const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch({ headless: true });
const ctx = await browser.newContext({ viewport: { width: 1280, height: 900 } });
await ctx.addInitScript(() => { localStorage['_test_token'] = '6477108200|8818'; });
const page = await ctx.newPage();
const logs = [];
page.on('console', m => logs.push(m.text()));
await page.goto('http://127.0.0.1:8099/', { waitUntil: 'load', timeout: 60000 });
await page.waitForTimeout(10000);
// 确认 localStorage 注入 + autologin 日志
const ls = await page.evaluate(() => localStorage['_test_token']);
console.log('localStorage 注入:', ls ? '✓' : '✗');
console.log('autologin 日志:', logs.filter(l=>l.includes('CK_AUTOLOGIN')).join(' | ') || '(无)');
await browser.close();
})();

View File

@@ -0,0 +1,20 @@
const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch({ headless: true });
const ctx = await browser.newContext({ viewport: { width: 1280, height: 900 } });
await ctx.addInitScript(() => { localStorage['_test_token'] = '6477108200|8818'; });
const page = await ctx.newPage();
const logs = [];
page.on('console', m => logs.push(m.text()));
// 第一次访问触发编译
await page.goto('http://127.0.0.1:8099/', { waitUntil: 'load', timeout: 60000 });
await page.waitForSelector('canvas', { timeout: 60000 });
await page.waitForTimeout(3000);
console.log('编译完成 canvas:', await page.locator('canvas').count());
// reload 让 main() 重跑devAutologin
logs.length = 0;
await page.reload({ waitUntil: 'load' });
await page.waitForTimeout(10000);
console.log('reload 后 autologin 日志:', logs.filter(l=>l.includes('CK_AUTOLOGIN')).join(' | ') || '(无)');
await browser.close();
})();

View File

@@ -0,0 +1,17 @@
const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch({ headless: true });
const ctx = await browser.newContext({ viewport: { width: 1280, height: 900 } });
await ctx.addInitScript(() => { localStorage['_test_token'] = '6477108200|8818'; });
const page = await ctx.newPage();
const logs = [];
page.on('console', m => logs.push(m.text()));
page.on('response', r => { if (r.url().includes('access_token')) console.log('登录API:', r.status()); });
await page.goto('http://127.0.0.1:8099/', { waitUntil: 'load', timeout: 60000 });
await page.waitForSelector('canvas', { timeout: 60000 });
await page.reload({ waitUntil: 'load' });
await page.waitForTimeout(18000);
console.log('所有 CK_AUTOLOGIN 日志:');
logs.filter(l=>l.includes('AUTOLOGIN')).forEach(l => console.log(' ', l.slice(0,120)));
await browser.close();
})();

View File

@@ -0,0 +1,16 @@
const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch({ headless: true });
const ctx = await browser.newContext({ viewport: { width: 1280, height: 900 } });
await ctx.addInitScript(() => { localStorage['_test_token'] = '6477108200|8818'; });
const page = await ctx.newPage();
const logs = [];
page.on('console', m => logs.push(m.text()));
page.on('response', r => { if (r.url().includes('access_token')) console.log(' 登录API:', r.status()); });
await page.goto('http://127.0.0.1:8099/', { waitUntil: 'load', timeout: 60000 });
await page.waitForSelector('canvas', { timeout: 60000 });
await page.reload({ waitUntil: 'load' });
await page.waitForTimeout(25000);
console.log('autologin 日志:', logs.filter(l=>l.includes('CK_AUTOLOGIN')).join(' | ') || '(无)');
await browser.close();
})();

View File

@@ -0,0 +1,18 @@
const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch({ headless: true });
const ctx = await browser.newContext({ viewport: { width: 1280, height: 900 } });
await ctx.addInitScript(() => { localStorage['_test_token'] = '6477108200|8818'; });
const page = await ctx.newPage();
const logs = [];
page.on('console', m => logs.push(m.text()));
page.on('request', r => { if (r.url().includes('access_token')) console.log(' →请求发出:', r.url().slice(-40)); });
page.on('response', r => { if (r.url().includes('access_token')) console.log(' ←响应:', r.status()); });
page.on('requestfailed', r => { if (r.url().includes('access_token') || r.url().includes('api.minipos')) console.log(' ✗请求失败:', r.url().slice(-50), r.failure()?.errorText); });
await page.goto('http://127.0.0.1:8099/', { waitUntil: 'load', timeout: 60000 });
await page.waitForSelector('canvas', { timeout: 90000 });
await page.reload({ waitUntil: 'load' });
await page.waitForTimeout(15000); // 等 3s 延迟 + autologin
console.log('autologin 日志:', logs.filter(l=>l.includes('CK_AUTOLOGIN')).join(' | ') || '(无)');
await browser.close();
})();