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-action-profile')?.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) })