const assert = require('assert') 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 sleep = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds)) const connect = async () => { const pages = await (await fetch('http://127.0.0.1:9222/json/list')).json() const projectPages = pages.filter((page) => page.type === 'page' && page.url.startsWith(origin)) assert.strictEqual(projectPages.length, 1, `Expected one project page, found ${projectPages.length}`) const socket = new WebSocket(projectPages[0].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 result = await send('Runtime.evaluate', { expression, returnByValue: true }) if (result.exceptionDetails) throw new Error(result.exceptionDetails.exception?.description || result.exceptionDetails.text) return result.result?.value } const waitFor = async (send, expression, message) => { for (let attempt = 0; attempt < 50; attempt += 1) { if (await valueOf(send, expression)) return await sleep(80) } throw new Error(message) } let auditId = 0 const open = async (send, route, query, selector) => { auditId += 1 const url = `${origin}/?dataLayoutAudit=${auditId}#${route}${query}` await send('Page.navigate', { url }) await waitFor(send, `location.href === ${JSON.stringify(url)}`, `Navigation failed: ${route}${query}`) await waitFor(send, `Boolean(document.querySelector(${JSON.stringify(selector)}))`, `State did not render: ${selector}`) } const setSize = (send, size) => send('Emulation.setDeviceMetricsOverride', { ...size, deviceScaleFactor: 1, mobile: true, screenWidth: size.width, screenHeight: size.height }) const assertViewport = async (send, label, expectedCount, selector, allowVisualClipping = false) => { const metrics = await valueOf(send, `(() => { const items = Array.from(document.querySelectorAll(${JSON.stringify(selector)})) const last = items.at(-1) last?.scrollIntoView({ block: 'end' }) const lastRect = last?.getBoundingClientRect() return { count: items.length, documentWidth: document.documentElement.scrollWidth, viewportWidth: innerWidth, viewportHeight: innerHeight, lastBottom: lastRect?.bottom || 0, clipped: items.some((item) => item.scrollWidth > item.clientWidth + 1 || item.scrollHeight > item.clientHeight + 1) } })()`) assert.strictEqual(metrics.count, expectedCount, `${label}: data count changed`) assert(metrics.documentWidth <= metrics.viewportWidth + 1, `${label}: horizontal overflow ${JSON.stringify(metrics)}`) assert(metrics.lastBottom <= metrics.viewportHeight + 1, `${label}: last item is not reachable ${JSON.stringify(metrics)}`) if (!allowVisualClipping) assert.strictEqual(metrics.clipped, false, `${label}: a data item clips its content`) } const stressDirectory = async (send, size) => { await setSize(send, size) await open(send, '/pages/tree/t07-member-directory', '?genealogyId=1001', '.directory-state--list') const prepared = await valueOf(send, `(() => { const first = document.querySelector('.directory-card') const parent = first?.parentElement if (!first || !parent) return false const originals = Array.from(parent.querySelectorAll('.directory-card')) for (let index = originals.length; index < 50; index += 1) { const clone = originals[index % originals.length].cloneNode(true) clone.querySelector('.directory-card__name').textContent = '汤氏超长成员姓名用于三倍文案压力验证' + (index + 1) clone.querySelector('.directory-card__meta').textContent = '第十世 · 超长字辈名称 · 超长支系与地区说明用于验证数据自然换行' parent.appendChild(clone) } parent.querySelectorAll('.directory-card__name, .directory-card__meta').forEach((node) => { node.style.fontSize = (parseFloat(getComputedStyle(node).fontSize) * 1.3) + 'px' }) return true })()`) assert.strictEqual(prepared, true, 'T07 directory stress data could not be prepared') await assertViewport(send, `T07 ${size.width}x${size.height}`, 50, '.directory-card') } const stressApplications = async (send, size) => { await setSize(send, size) await open(send, '/pages/genealogy/g09-my-applications', '', '.application-state--list') assert(await valueOf(send, `(() => { const first = document.querySelector('.application-card') const parent = first?.parentElement if (!first || !parent) return false const originals = Array.from(parent.querySelectorAll('.application-card')) for (let index = originals.length; index < 50; index += 1) { const clone = originals[index % originals.length].cloneNode(true) clone.querySelector('.application-card__name').textContent = '超长家谱名称与地区支系压力验证' + (index + 1) clone.querySelector('.application-card__relation').textContent = '祖居河南南阳并迁居多地的三倍关系说明,验证卡片由数据自然撑高' parent.appendChild(clone) } parent.querySelectorAll('.application-card__name, .application-card__relation, .application-card__hint').forEach((node) => { node.style.fontSize = (parseFloat(getComputedStyle(node).fontSize) * 1.3) + 'px' }) return true })()`), 'G09 application stress data could not be prepared') await assertViewport(send, `G09 ${size.width}x${size.height}`, 50, '.application-card') } const stressMedia = async (send, size) => { await setSize(send, size) await open(send, '/pages/family/f09-media-upload', '', '.media-upload-state--initial') await valueOf(send, "document.querySelector('.media-primary-action')?.click()") await waitFor(send, "document.querySelectorAll('.media-photo-tile').length === 4", 'F09 selected media did not render') assert(await valueOf(send, `(() => { const first = document.querySelector('.media-photo-tile') const parent = first?.parentElement if (!first || !parent) return false const originals = Array.from(parent.querySelectorAll('.media-photo-tile')) for (let index = originals.length; index < 30; index += 1) parent.appendChild(originals[index % originals.length].cloneNode(true)) return true })()`), 'F09 media stress data could not be prepared') await assertViewport(send, `F09 ${size.width}x${size.height}`, 30, '.media-photo-tile', true) } const stressAutoHeightForm = async (send) => { await setSize(send, { width: 320, height: 568 }) await open(send, '/pages/genealogy/g08-join-application', '?source=search&genealogyId=2001', '.join-state--form') const metrics = await valueOf(send, `(() => { const textarea = document.querySelector('.join-field textarea') const before = textarea.getBoundingClientRect().height textarea.value = '这是用于验证表单由数据驱动自然增高的长说明。'.repeat(8) textarea.dispatchEvent(new Event('input', { bubbles: true })) return { before, after: textarea.getBoundingClientRect().height, documentWidth: document.documentElement.scrollWidth } })()`) await sleep(100) const after = await valueOf(send, "document.querySelector('.join-field textarea').getBoundingClientRect().height") assert(after > metrics.before * 1.5, `G08 auto-height textarea did not grow: ${JSON.stringify({ ...metrics, after })}`) assert(metrics.documentWidth <= 321, 'G08 long form value caused horizontal overflow') } const run = async () => { const { socket, send } = await connect() try { await send('Page.enable') await send('Runtime.enable') for (const size of sizes) await stressDirectory(send, size) for (const size of [sizes[0], sizes[3]]) { await stressApplications(send, size) await stressMedia(send, size) } await stressAutoHeightForm(send) process.stdout.write('DATA-DRIVEN-LAYOUT-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) })