/** * 全量仪表化诊断:记录 localStorage 生命周期 + 实际加载的 bundle + 崩溃点 * 修复点 vs 上一版: * 1. 记录实际加载的 JS bundle 文件名(performance entries) * 2. 记录注入后 localStorage 的完整快照(boot 前) * 3. 全量 patch:getItem(带堆栈)/setItem/removeItem/clear * 4. 记录崩溃时完整的 localStorage 快照 */ const { test, expect } = require('@playwright/test'); var BASE_URL = 'http://cfc.iwintrue.com'; test('FULL INSTRUMENT: localStorage lifecycle + bundle identity', async ({ page }) => { var consoleMessages = []; var pageErrors = []; page.on('console', function (msg) { consoleMessages.push('[' + msg.type() + '] ' + msg.text().substring(0, 250)); }); page.on('pageerror', function (err) { pageErrors.push(String(err) + '\nSTACK: ' + (err.stack || 'no-stack')); }); // 1. 登录拿 token var loginRes = await page.request.post(BASE_URL + '/api/admin-auth/login-by-password', { data: { phone: '13800138000', password: 'admin123' }, headers: { 'Content-Type': 'application/json' } }); console.log('LOGIN_STATUS=' + loginRes.status()); var body = await loginRes.json(); console.log('LOGIN_BODY=' + JSON.stringify(body).substring(0, 400)); if (body.code === 200 && body.data && body.data.token) { var tok = body.data.token, role = body.data.role, roles = body.data.roles || [body.data.role], adminId = body.data.adminId; console.log('INJECT_RAW token.len=' + String(tok).length + ' role=' + role + ' roles=' + JSON.stringify(roles) + ' adminId=' + adminId); // addInitScript #1:注入 + 全量 storage patch + 快照 // 注意:addInitScript 只接受一个 arg,必须打包成单个对象传参 await page.addInitScript(function (auth) { var token = auth.token, role = auth.role, roles = auth.roles, adminId = auth.adminId; // 记录 boot 前快照(patch 之前) window.__preSnapshot = { token: localStorage.getItem('token'), role: localStorage.getItem('role'), roles: localStorage.getItem('roles'), adminId: localStorage.getItem('adminId') }; localStorage.setItem('token', token); localStorage.setItem('access_token', token); localStorage.setItem('role', role); localStorage.setItem('roles', JSON.stringify(roles)); localStorage.setItem('adminId', String(adminId)); // 注入后快照 window.__postSnapshot = { token: localStorage.getItem('token'), role: localStorage.getItem('role'), roles: localStorage.getItem('roles'), adminId: localStorage.getItem('adminId') }; // 全量 storage patch(此时注入已完成) window.__storageCalls = []; var OGS = Storage.prototype; var origGet = OGS.getItem, origSet = OGS.setItem, origRem = OGS.removeItem, origClear = OGS.clear; OGS.getItem = function (k) { var r = origGet.call(this, k); if (k === 'roles' || k === 'role' || k === 'token') { window.__storageCalls.push({ op: 'get', key: k, val: r === null ? 'NULL' : r, stack: new Error('get').stack.split('\n').slice(2, 5).join(' | ') }); } return r; }; OGS.setItem = function (k, v) { window.__storageCalls.push({ op: 'set', key: k, val: String(v).substring(0, 60), stack: new Error('set').stack.split('\n').slice(2, 5).join(' | ') }); return origSet.call(this, k, v); }; OGS.removeItem = function (k) { window.__storageCalls.push({ op: 'remove', key: k, stack: new Error('remove').stack.split('\n').slice(2, 5).join(' | ') }); return origRem.call(this, k); }; OGS.clear = function () { window.__storageCalls.push({ op: 'clear-all', stack: new Error('clear').stack.split('\n').slice(2, 5).join(' | ') }); return origClear.call(this); }; // JSON.parse patch var origParse = JSON.parse; window.__parseLog = []; JSON.parse = function (input) { if (input === undefined || input === null || (typeof input === 'string' && input.indexOf('undefined') >= 0)) { window.__parseLog.push({ input: typeof input === 'string' ? 'STR:' + input.substring(0, 40) : String(input), stack: new Error('captured').stack }); } return origParse.apply(this, arguments); }; }, { token: tok, role: role, roles: roles, adminId: adminId }); } // 2. 访问 dashboard await page.goto(BASE_URL + '/admin/dashboard', { waitUntil: 'domcontentloaded', timeout: 60000 }); await page.waitForTimeout(10000); // 3. 实际加载的 bundle var resources = await page.evaluate(function () { return performance.getEntriesByType('resource').map(function (r) { return r.name; }).filter(function (n) { return n.indexOf('.js') >= 0; }); }); console.log('--- LOADED JS (' + resources.length + ') ---'); resources.forEach(function (r) { console.log('JS: ' + r); }); console.log('FINAL_URL=' + page.url()); var appHtml = await page.evaluate(function () { var app = document.getElementById('app'); return app ? app.innerHTML.substring(0, 300) : 'NO #app'; }); console.log('APP_HTML=' + appHtml); console.log('--- PRE SNAPSHOT ---'); console.log(JSON.stringify(await page.evaluate(function () { return window.__preSnapshot; }))); console.log('--- POST SNAPSHOT ---'); console.log(JSON.stringify(await page.evaluate(function () { return window.__postSnapshot; }))); console.log('--- STORAGE CALLS (' + (await page.evaluate(function () { return (window.__storageCalls || []).length; })) + ') ---'); var calls = await page.evaluate(function () { return window.__storageCalls || []; }); calls.forEach(function (c) { console.log(c.op.toUpperCase() + ' ' + c.key + (c.val !== undefined ? '=' + c.val : '') + ' || ' + c.stack); }); console.log('--- JSON.PARSE CAPTURES (' + (await page.evaluate(function () { return (window.__parseLog || []).length; })) + ') ---'); var parseLog = await page.evaluate(function () { return window.__parseLog || []; }); parseLog.forEach(function (e) { console.log('PARSE input=' + e.input); console.log('PARSE stack=' + e.stack); }); // 4. 崩溃时 localStorage 完整快照 var snap = await page.evaluate(function () { var out = {}; for (var i = 0; i < localStorage.length; i++) { var k = localStorage.key(i); out[k] = localStorage.getItem(k); } return out; }); console.log('--- FINAL LS SNAPSHOT ---'); console.log(JSON.stringify(snap)); console.log('--- PAGE ERRORS (' + pageErrors.length + ') ---'); pageErrors.forEach(function (e) { console.log('ERR: ' + e.substring(0, 800)); }); console.log('--- CONSOLE (' + consoleMessages.length + ') ---'); consoleMessages.forEach(function (m) { console.log('CON: ' + m.substring(0, 300)); }); await page.screenshot({ path: 'test-artifacts/full-instrument.png', fullPage: true }); expect(true).toBeTruthy(); });