Files
jiapuapp/tests/t01-tree-state-runtime-smoke.js
T
2026-07-29 18:16:08 +08:00

179 lines
10 KiB
JavaScript

const assert = require('assert')
const sleep = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds))
const origin = process.argv[2] || 'http://localhost:5173'
const sizes = [
{ width: 320, height: 568 },
{ width: 360, height: 640 },
{ width: 360, height: 800 },
{ width: 412, height: 915 }
]
const connect = async () => {
const pages = await (await fetch('http://127.0.0.1:9222/json/list')).json()
const page = pages.find((item) => item.type === 'page' && item.url.startsWith(`${origin}/`))
if (!page) throw new Error(`Chrome debugging has no ${origin} application page`)
const socket = new WebSocket(page.webSocketDebuggerUrl)
await new Promise((resolve, reject) => {
socket.addEventListener('open', resolve, { once: true })
socket.addEventListener('error', reject, { once: true })
})
let id = 0
const pending = new Map()
socket.addEventListener('message', (event) => {
const message = JSON.parse(event.data)
const request = pending.get(message.id)
if (!request) return
pending.delete(message.id)
message.error ? request.reject(new Error(message.error.message)) : request.resolve(message.result)
})
const send = (method, params = {}) => new Promise((resolve, reject) => {
id += 1
pending.set(id, { resolve, reject })
socket.send(JSON.stringify({ id, method, params }))
})
return { socket, send }
}
const valueOf = async (send, expression) => {
const evaluation = await send('Runtime.evaluate', { expression, returnByValue: true })
if (evaluation.exceptionDetails) {
throw new Error(evaluation.exceptionDetails.exception?.description || evaluation.exceptionDetails.text)
}
return evaluation.result?.value
}
const waitFor = async (send, expression, message) => {
for (let attempt = 0; attempt < 50; attempt += 1) {
if (await valueOf(send, expression)) return
await sleep(100)
}
throw new Error(message)
}
const open = async (send, query, selector) => {
const url = `${origin}/#/pages/tree/t02-pedigree-overview${query}`
await send('Page.navigate', { url })
await waitFor(send, `location.href === ${JSON.stringify(url)}`, `T02 navigation failed: ${query}`)
const previousTimeOrigin = await valueOf(send, 'performance.timeOrigin')
await send('Page.reload')
await waitFor(send, `performance.timeOrigin !== ${JSON.stringify(previousTimeOrigin)}`, `T02 reload did not create a fresh document: ${query}`)
await waitFor(send, `Boolean(document.querySelector(${JSON.stringify(selector)}))`, `T02 state did not render: ${selector}`)
}
const run = async () => {
const { socket, send } = await connect()
try {
await send('Page.enable')
await send('Runtime.enable')
await open(send, '?state=landscape&genealogyId=1001', '.tree-state--landscape')
await open(send, '?state=empty&genealogyId=1001', '.tree-state--empty')
await open(send, '?state=error&genealogyId=1001', '.tree-state--error')
for (const size of sizes) {
await send('Emulation.setDeviceMetricsOverride', { ...size, deviceScaleFactor: 1, mobile: true, screenWidth: size.width, screenHeight: size.height })
await open(send, '?genealogyId=1001', '.tree-state--tree .member-node')
const metrics = await valueOf(send, `(() => ({
documentWidth: document.documentElement.scrollWidth,
stageDisplay: getComputedStyle(document.querySelector('.tree-stage')).display,
railPosition: getComputedStyle(document.querySelector('.generation-rail')).position,
nodePosition: getComputedStyle(document.querySelector('.member-node')).position,
nodeCount: document.querySelectorAll('.member-node').length,
firstNodeRect: (() => {
const rect = document.querySelector('.member-node').getBoundingClientRect()
return { left: rect.left, top: rect.top, width: rect.width, height: rect.height }
})(),
canvasRect: (() => {
const rect = document.querySelector('.tree-canvas').getBoundingClientRect()
return { left: rect.left, top: rect.top, width: rect.width, height: rect.height }
})()
}))()`)
assert(metrics.documentWidth <= size.width + 1, `T01 document overflows at ${size.width}x${size.height}`)
assert.strictEqual(metrics.stageDisplay, 'grid', `T01 stage is not a grid at ${size.width}x${size.height}`)
assert.strictEqual(metrics.railPosition, 'static', `T01 generation rail is positioned at ${size.width}x${size.height}`)
assert.strictEqual(metrics.nodePosition, 'static', `T01 member node is positioned at ${size.width}x${size.height}`)
assert.strictEqual(metrics.nodeCount, 5, `T01 pager must render one five-member swipe page at ${size.width}x${size.height}: ${JSON.stringify(metrics)}`)
assert(metrics.firstNodeRect.width > 30 && metrics.firstNodeRect.height > 180, `T01 pedigree column collapsed at ${size.width}x${size.height}: ${JSON.stringify(metrics)}`)
assert(metrics.firstNodeRect.top >= metrics.canvasRect.top && metrics.firstNodeRect.top < metrics.canvasRect.top + metrics.canvasRect.height, `T01 member node escaped the data-driven canvas at ${size.width}x${size.height}: ${JSON.stringify(metrics)}`)
assert(metrics.firstNodeRect.left + metrics.firstNodeRect.width > 0 && metrics.firstNodeRect.left < size.width, `T01 initial member node is outside the visible horizontal tree viewport at ${size.width}x${size.height}: ${JSON.stringify(metrics)}`)
assert(metrics.firstNodeRect.top < size.height * 0.5, `T01 generation rail and pedigree sheet were stacked instead of sharing the tree-stage row at ${size.width}x${size.height}: ${JSON.stringify(metrics)}`)
}
const stressMembers = []
for (let generation = 1; generation <= 10; generation += 1) {
for (let index = 0; index < 12; index += 1) {
stressMembers.push({
id: generation * 100 + index,
parentId: generation === 1 ? undefined : (generation - 1) * 100 + Math.floor(index / 2),
name: `压力成员${generation}-${index + 1}`,
relation: generation === 1 ? '始祖' : '后代',
years: '1900—',
generation,
branch: `第${index + 1}支长名称`,
x: 150 + index * 250,
y: 135 + (generation - 1) * 220
})
}
}
const injected = await valueOf(send, `(() => {
let instance = document.querySelector('.tree-page')?.__vueParentComponent
while (instance && !Object.prototype.hasOwnProperty.call(instance.setupState || {}, 'members')) instance = instance.parent
const state = instance?.setupState
if (!state) return false
const nextMembers = ${JSON.stringify(stressMembers)}
if (Array.isArray(state.members)) state.members = nextMembers
else if (state.members && 'value' in state.members) state.members.value = nextMembers
else return false
return true
})()`)
assert.strictEqual(injected, true, 'T01 stress data could not be injected into the active component')
await waitFor(send, "document.querySelectorAll('.member-node').length === 5", 'T01 did not render one five-member swipe page')
const stressMetrics = await valueOf(send, `(() => {
const canvas = document.querySelector('.tree-canvas')
const rail = document.querySelector('.generation-rail')
return {
canvasWidth: parseFloat(getComputedStyle(canvas).width),
canvasHeight: parseFloat(getComputedStyle(canvas).height),
railHeight: parseFloat(getComputedStyle(rail).height),
generationCount: document.querySelectorAll('.generation-band').length,
connectorCount: document.querySelectorAll('.lineage-connector').length,
documentWidth: document.documentElement.scrollWidth
}
})()`)
assert(stressMetrics.canvasWidth > 250, 'T01 swipe sheet did not fill the available viewport')
assert(stressMetrics.canvasHeight > 500, 'T01 pedigree sheet height did not reserve space for vertical details')
assert(Math.abs(stressMetrics.canvasHeight - stressMetrics.railHeight) <= 1, 'T01 generation rail did not follow the data-driven canvas height')
assert.strictEqual(stressMetrics.generationCount, 10, 'T01 generation rail did not derive all generations from data')
assert(stressMetrics.connectorCount > 30, 'T01 relationship connectors did not derive from parent data')
assert(stressMetrics.documentWidth <= 413, 'T01 stress tree escaped its scroll owner')
const swiperRect = await valueOf(send, `(() => {
const rect = document.querySelector('.pedigree-swiper').getBoundingClientRect()
return { left: rect.left, top: rect.top, width: rect.width, height: rect.height }
})()`)
await send('Input.dispatchTouchEvent', {
type: 'touchStart',
touchPoints: [{ x: swiperRect.left + swiperRect.width * 0.76, y: swiperRect.top + Math.min(180, swiperRect.height * 0.35), radiusX: 1, radiusY: 1, force: 1, id: 1 }]
})
await send('Input.dispatchTouchEvent', {
type: 'touchMove',
touchPoints: [{ x: swiperRect.left + swiperRect.width * 0.24, y: swiperRect.top + Math.min(180, swiperRect.height * 0.35), radiusX: 1, radiusY: 1, force: 1, id: 1 }]
})
await send('Input.dispatchTouchEvent', { type: 'touchEnd', touchPoints: [] })
await waitFor(send, `(() => {
let instance = document.querySelector('.tree-page')?.__vueParentComponent
while (instance && !Object.prototype.hasOwnProperty.call(instance.setupState || {}, 'pedigreePageIndex')) instance = instance.parent
const value = instance?.setupState?.pedigreePageIndex
return typeof value === 'number' ? value > 0 : value?.value > 0
})()`, 'T01 did not advance to the next page after a direct left swipe')
await open(send, '?genealogyId=1001', '.tree-state--tree .member-node__name')
await valueOf(send, "document.querySelector('.member-node__name')?.click()")
await waitFor(send, "location.href.includes('/pages/tree/t03-member-profile?genealogyId=1001&personId=')", 'T02 selected member did not open T03')
process.stdout.write('T02-PEDIGREE-RUNTIME-SMOKE PASS\n')
} finally {
try { await send('Emulation.clearDeviceMetricsOverride') } catch (_) {}
socket.close()
}
}
run().catch((error) => {
process.stderr.write(`${error.stack || error.message}\n`)
process.exit(1)
})