u
This commit is contained in:
1 parent
e7c84a82a2
commit
025d3232c4
65 files changed
+9838
-9111
No files matched your search
@@ -0,0 +1,260 @@
|
||||
import { chromium } from '@playwright/test'
|
||||
|
||||
/**
|
||||
* /table-test 探针 v13 —— 生产构建对照(只读诊断,不改仓库代码)。
|
||||
*
|
||||
* 目的:排除「Vite dev 模式 + 未压缩 + sourcemap/HMR 开销」这一解释。
|
||||
* 应用从**生产构建**(vite build 输出,served by vite preview :5190)加载;
|
||||
* 由于 preview 不带 API 代理,这里把 /api/** 转发到 dev server(:5090)拿真实数据,
|
||||
* 并在响应里补一条 menu.table_test 授权(与前面几个探针同理,仅测试进程内生效)。
|
||||
*/
|
||||
|
||||
const PROD = process.env.PROD_URL || 'http://127.0.0.1:5190'
|
||||
const API_ORIGIN = process.env.API_ORIGIN || 'http://127.0.0.1:5090'
|
||||
const MENU_CODE = 'menu.table_test'
|
||||
|
||||
const median = (a) => {
|
||||
const s = [...a].filter((x) => x != null).sort((x, y) => x - y)
|
||||
return s.length ? s[s.length >> 1] : null
|
||||
}
|
||||
const r1 = (n) => (n == null ? null : +Number(n).toFixed(1))
|
||||
|
||||
const browser = await chromium.launch({ headless: false })
|
||||
const context = await browser.newContext({ viewport: { width: 1600, height: 900 } })
|
||||
|
||||
/**
|
||||
* 生产构建里 `import.meta.env.DEV === false`,dev 自动登录整段被 tree-shake 掉了,
|
||||
* 直奔 /table-test 会停在 /login。所以这里自己换一个 token,再用 addInitScript
|
||||
* 在应用脚本执行前种进 sessionStorage(与 web/e2e/support/fixtures.ts 同一手法)。
|
||||
*
|
||||
* 账号取自 web/.env.development(VITE_DEV_*)。
|
||||
*/
|
||||
async function freshToken() {
|
||||
const res = await fetch(`${API_ORIGIN}/api/auth/login`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
orgid: process.env.E2E_ORG || 'G3HD',
|
||||
userid: process.env.E2E_ACCOUNT || 'demo_zhangwei',
|
||||
password: process.env.E2E_PASSWORD ?? 'demo_zhangwei',
|
||||
}),
|
||||
})
|
||||
const body = await res.json()
|
||||
if (body.code !== 0 || !body.data?.token) {
|
||||
throw new Error(`登录失败 code=${body.code} ${body.message || ''}`)
|
||||
}
|
||||
return body.data
|
||||
}
|
||||
|
||||
const session = await freshToken()
|
||||
console.log('登录成功:', session.user?.account, session.orgId)
|
||||
await context.addInitScript(
|
||||
(payload) => {
|
||||
window.sessionStorage.setItem(payload.key, JSON.stringify(payload.value))
|
||||
},
|
||||
{
|
||||
key: 'g3erp:login',
|
||||
value: { loginInfo: { token: session.token, user: session.user } },
|
||||
},
|
||||
)
|
||||
|
||||
// /api/** → 转发到 dev server(拿真实后端数据),并按需注入菜单授权
|
||||
await context.route('**/api/**', async (route) => {
|
||||
const req = route.request()
|
||||
const url = new URL(req.url())
|
||||
const target = `${API_ORIGIN}${url.pathname}${url.search}`
|
||||
let viewName = ''
|
||||
try {
|
||||
viewName = JSON.parse(req.postData() || '').view_name || ''
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
const res = await route.fetch({ url: target })
|
||||
if (viewName !== 's_power' && viewName !== 's_user_power') {
|
||||
return route.fulfill({ response: res })
|
||||
}
|
||||
let json
|
||||
try {
|
||||
json = await res.json()
|
||||
} catch {
|
||||
return route.fulfill({ response: res })
|
||||
}
|
||||
const rows = Array.isArray(json?.data) ? json.data : []
|
||||
if (viewName === 's_power') {
|
||||
if (!rows.some((r) => r.b_id === MENU_CODE)) rows.push({ b_id: MENU_CODE, b_canuse: 1 })
|
||||
} else if (!rows.some((r) => r.b_power_id === MENU_CODE)) {
|
||||
rows.push({ b_user_id: 'demo_zhangwei', b_power_id: MENU_CODE, b_canuse: 1 })
|
||||
}
|
||||
json.data = rows
|
||||
await route.fulfill({
|
||||
response: res,
|
||||
body: JSON.stringify(json),
|
||||
headers: { ...res.headers(), 'content-type': 'application/json' },
|
||||
})
|
||||
})
|
||||
|
||||
const page = await context.newPage()
|
||||
page.on('pageerror', (e) => console.log('[pageerror]', String(e).slice(0, 200)))
|
||||
|
||||
console.log('打开生产构建 /table-test ...')
|
||||
await page.goto(`${PROD}/table-test`, { waitUntil: 'domcontentloaded' })
|
||||
await page.waitForSelector('.stk-table', { timeout: 90000 })
|
||||
await page.waitForTimeout(3500)
|
||||
|
||||
const env = await page.evaluate(() => ({
|
||||
url: location.href,
|
||||
scripts: [...document.querySelectorAll('script[src]')]
|
||||
.map((s) => s.getAttribute('src'))
|
||||
.filter((s) => s.includes('/assets/'))
|
||||
.slice(0, 3),
|
||||
tds: document.querySelectorAll('.stk-tbody-main td').length,
|
||||
trs: document.querySelectorAll('.stk-tbody-main tr').length,
|
||||
editorRoots: document.querySelectorAll('.g3-table__cell-editor-root').length,
|
||||
}))
|
||||
console.log('环境:', JSON.stringify(env, null, 2))
|
||||
|
||||
async function scrollFps({ axis = 'y', steps = 60, px = 60 } = {}) {
|
||||
const ok = await page.evaluate(() => !!document.querySelector('.stk-table'))
|
||||
if (!ok) return { fps: null }
|
||||
return page.evaluate(
|
||||
async ({ axis, steps, px }) => {
|
||||
const cont = document.querySelector('.stk-table')
|
||||
cont.scrollTop = 0
|
||||
cont.scrollLeft = 0
|
||||
await new Promise((r) => requestAnimationFrame(r))
|
||||
const frames = []
|
||||
let last = performance.now()
|
||||
let stop = false
|
||||
const tick = () => {
|
||||
const now = performance.now()
|
||||
frames.push(now - last)
|
||||
last = now
|
||||
if (!stop) requestAnimationFrame(tick)
|
||||
}
|
||||
requestAnimationFrame(tick)
|
||||
for (let i = 0; i < steps; i += 1) {
|
||||
await new Promise((r) => requestAnimationFrame(r))
|
||||
if (axis === 'y') {
|
||||
const max = cont.scrollHeight - cont.clientHeight
|
||||
cont.scrollTop = cont.scrollTop >= max - 1 ? 0 : Math.min(max, cont.scrollTop + px)
|
||||
} else {
|
||||
const max = cont.scrollWidth - cont.clientWidth
|
||||
cont.scrollLeft = cont.scrollLeft >= max - 1 ? 0 : Math.min(max, cont.scrollLeft + px)
|
||||
}
|
||||
}
|
||||
stop = true
|
||||
const sorted = [...frames].sort((a, b) => a - b)
|
||||
const avg = frames.reduce((a, b) => a + b, 0) / frames.length
|
||||
const q = (p) => sorted[Math.min(sorted.length - 1, Math.floor(sorted.length * p))]
|
||||
return { fps: +(1000 / avg).toFixed(1), avg: +avg.toFixed(1), p95: +q(0.95).toFixed(1) }
|
||||
},
|
||||
{ axis, steps, px },
|
||||
)
|
||||
}
|
||||
|
||||
console.log('\n--- 生产构建:纵向滚动 ×3 ---')
|
||||
const yRuns = []
|
||||
for (let i = 0; i < 3; i += 1) yRuns.push(await scrollFps({ axis: 'y', steps: 60, px: 60 }))
|
||||
console.log(JSON.stringify(yRuns), `中位 ${r1(median(yRuns.map((r) => r.fps)))}fps`)
|
||||
|
||||
console.log('--- 生产构建:横向滚动 ×3 ---')
|
||||
const xRuns = []
|
||||
for (let i = 0; i < 3; i += 1) xRuns.push(await scrollFps({ axis: 'x', steps: 50, px: 80 }))
|
||||
console.log(JSON.stringify(xRuns), `中位 ${r1(median(xRuns.map((r) => r.fps)))}fps`)
|
||||
|
||||
console.log('--- 生产构建:静止基线 ---')
|
||||
const idle = []
|
||||
for (let i = 0; i < 2; i += 1) idle.push(await scrollFps({ axis: 'y', steps: 45, px: 0 }))
|
||||
console.log(JSON.stringify(idle), `中位 ${r1(median(idle.map((r) => r.fps)))}fps`)
|
||||
|
||||
/* 点击延迟(生产) */
|
||||
console.log('\n--- 生产构建:点击进入编辑 ---')
|
||||
async function clickProbe(rowIdx, colIdx, settle = 350) {
|
||||
await page.waitForTimeout(settle)
|
||||
return page.evaluate(
|
||||
async ({ rowIdx, colIdx }) => {
|
||||
const tr = [...document.querySelectorAll('.stk-tbody-main tr')][rowIdx]
|
||||
if (!tr) return { error: 'no row' }
|
||||
const td = [...tr.querySelectorAll('td')][colIdx]
|
||||
if (!td) return { error: 'no td' }
|
||||
const r = td.getBoundingClientRect()
|
||||
if (r.top < 0 || r.bottom > window.innerHeight) return { error: 'offscreen' }
|
||||
const t0 = performance.now()
|
||||
for (const type of ['pointerdown', 'mousedown', 'pointerup', 'mouseup', 'click']) {
|
||||
const Ctor = type.startsWith('pointer') ? PointerEvent : MouseEvent
|
||||
td.dispatchEvent(
|
||||
new Ctor(type, {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
clientX: r.left + r.width / 2,
|
||||
clientY: r.top + r.height / 2,
|
||||
button: 0,
|
||||
}),
|
||||
)
|
||||
}
|
||||
let mounted = -1
|
||||
const deadline = performance.now() + 4000
|
||||
while (performance.now() < deadline) {
|
||||
if (td.querySelector('.g3-table__cell-editor-root')) {
|
||||
mounted = performance.now() - t0
|
||||
break
|
||||
}
|
||||
await new Promise((r2) => requestAnimationFrame(r2))
|
||||
}
|
||||
await new Promise((r2) => requestAnimationFrame(r2))
|
||||
return +mounted.toFixed(1)
|
||||
},
|
||||
{ rowIdx, colIdx },
|
||||
)
|
||||
}
|
||||
|
||||
await page.reload({ waitUntil: 'domcontentloaded' })
|
||||
await page.waitForSelector('.stk-table', { timeout: 90000 })
|
||||
await page.waitForTimeout(3000)
|
||||
console.log('首次点击:', JSON.stringify(await clickProbe(3, 6, 500)))
|
||||
const clicks = []
|
||||
for (const r of [5, 7, 9, 11]) clicks.push(await clickProbe(r, 6, 350))
|
||||
console.log('换行点击:', JSON.stringify(clicks), `中位 ${r1(median(clicks))}ms`)
|
||||
const same = []
|
||||
for (let i = 0; i < 3; i += 1) same.push(await clickProbe(11, 6, 250))
|
||||
console.log('同格连点:', JSON.stringify(same))
|
||||
|
||||
/* 编辑态滚动 */
|
||||
await clickProbe(3, 6, 300)
|
||||
console.log('\n--- 生产构建:编辑态纵向滚动 ×2 ---')
|
||||
const edRuns = []
|
||||
for (let i = 0; i < 2; i += 1) edRuns.push(await scrollFps({ axis: 'y', steps: 55, px: 60 }))
|
||||
console.log(JSON.stringify(edRuns), `中位 ${r1(median(edRuns.map((r) => r.fps)))}fps`)
|
||||
|
||||
/* errorCount / 保存 */
|
||||
console.log('\n--- 生产构建:保存明细 + errorCount(1000 行)---')
|
||||
const save = await page.evaluate(async () => {
|
||||
let i = document.querySelector('.booking')?.__vueParentComponent
|
||||
while (i && !i.setupState?.rows) i = i.parent
|
||||
if (!i) return { error: 'no page instance' }
|
||||
const ss = i.setupState
|
||||
const rows = ss.rows?.value ?? ss.rows
|
||||
void ss.errorCount?.value
|
||||
rows[0].freight = Number(rows[0].freight) + 0.001
|
||||
const t0 = performance.now()
|
||||
void ss.errorCount?.value
|
||||
const ecMs = performance.now() - t0
|
||||
rows[0].freight = Number(rows[0].freight) - 0.001
|
||||
const btn = [...document.querySelectorAll('.booking__btn')].find((b) =>
|
||||
b.textContent.includes('保存明细'),
|
||||
)
|
||||
const t1 = performance.now()
|
||||
btn?.dispatchEvent(new MouseEvent('click', { bubbles: true }))
|
||||
await new Promise((r) => requestAnimationFrame(r))
|
||||
await new Promise((r) => setTimeout(r, 0))
|
||||
return {
|
||||
rows: rows.length,
|
||||
errorCountRecomputeMs: +ecMs.toFixed(1),
|
||||
saveClickToNextFrameMs: +(performance.now() - t1).toFixed(1),
|
||||
}
|
||||
})
|
||||
console.log(JSON.stringify(save))
|
||||
|
||||
await page.screenshot({ path: 'probe-table-prod.png' })
|
||||
await browser.close()
|
||||
console.log('\n完成。截图 -> probe-table-prod.png')
|
||||
Reference in new issue
Block a user