160 lines
8.9 KiB
JavaScript
160 lines
8.9 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/t01-tree-overview${query}`
|
|
await send('Page.navigate', { url })
|
|
await waitFor(send, `location.href === ${JSON.stringify(url)}`, `T01 navigation failed: ${query}`)
|
|
const previousTimeOrigin = await valueOf(send, 'performance.timeOrigin')
|
|
await send('Page.reload')
|
|
await waitFor(send, `performance.timeOrigin !== ${JSON.stringify(previousTimeOrigin)}`, `T01 reload did not create a fresh document: ${query}`)
|
|
await waitFor(send, `Boolean(document.querySelector(${JSON.stringify(selector)}))`, `T01 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, 6, `T01 member nodes are incomplete at ${size.width}x${size.height}`)
|
|
assert(metrics.firstNodeRect.width > 80 && metrics.firstNodeRect.height > 30, `T01 member node 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 canvas 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 === 120", 'T01 did not render 10 generations with 12 members each')
|
|
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 > 1400, 'T01 canvas width did not grow from 12 members per generation')
|
|
assert(stressMetrics.canvasHeight > 1000, 'T01 canvas height did not grow from 10 generations')
|
|
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')
|
|
|
|
await open(send, '?genealogyId=1001', '.tree-state--tree .member-node')
|
|
await valueOf(send, "document.querySelector('.member-node')?.click()")
|
|
await valueOf(send, "document.querySelector('.member-sheet .app-button')?.click()")
|
|
await waitFor(send, "location.href.includes('/pages/tree/t03-member-profile?genealogyId=1001&personId=')", 'T01 selected member did not open T03')
|
|
process.stdout.write('T01-TREE-STATE-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)
|
|
})
|