const origin = '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)) if (projectPages.length !== 1) throw new Error(`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 { projectPageCount: projectPages.length, socket, send } } const valueOf = async (send, expression) => { const result = await send('Runtime.evaluate', { expression, returnByValue: true }) if (result.exceptionDetails) throw new Error(result.exceptionDetails.text || 'Runtime evaluation failed') return result.result?.value } const waitFor = async (send, expression, message) => { for (let attempt = 0; attempt < 60; attempt += 1) { try { if (await valueOf(send, expression)) return } catch (error) { if (!String(error.message).includes('Inspected target navigated or closed')) throw error } await sleep(100) } throw new Error(message) } const setSize = (send, size) => send('Emulation.setDeviceMetricsOverride', { ...size, deviceScaleFactor: 1, mobile: true, screenWidth: size.width, screenHeight: size.height }) let navigationId = 0 const openState = async (send, query = '') => { navigationId += 1 const url = `${origin}/?t07Baseline=${navigationId}#/pages/tree/t07-member-directory${query}` await send('Page.navigate', { url }) await waitFor(send, `location.href === ${JSON.stringify(url)} && Boolean(document.querySelector('.directory-page'))`, `T07 did not render ${query || 'list'}`) await sleep(250) } const metrics = (send) => valueOf(send, `(() => { const rect = (selector) => { const node = document.querySelector(selector) if (!node) return null const box = node.getBoundingClientRect() return { top: box.top, right: box.right, bottom: box.bottom, left: box.left, width: box.width, height: box.height } } return { viewport: { width: innerWidth, height: innerHeight }, context: rect('.directory-context'), search: rect('.directory-search'), firstCard: rect('.directory-card'), lastCard: rect('.directory-card:last-child'), cardCount: document.querySelectorAll('.directory-card').length, state: document.querySelector('.directory-state--loading') ? 'loading' : document.querySelector('.directory-state--empty') ? 'empty' : document.querySelector('.directory-state--error') ? 'error' : 'list', horizontalOverflow: document.documentElement.scrollWidth > innerWidth || document.body.scrollWidth > innerWidth, documentScrollHeight: document.documentElement.scrollHeight } })()`) const assert = (condition, message) => { if (!condition) throw new Error(message) } const run = async () => { const { projectPageCount, socket, send } = await connect() const runtimeErrors = [] const resourceErrors = [] socket.addEventListener('message', (event) => { const message = JSON.parse(event.data) if (message.method === 'Runtime.exceptionThrown') runtimeErrors.push(message.params.exceptionDetails?.text || 'runtime exception') if (message.method === 'Network.responseReceived' && message.params.response.status >= 400) resourceErrors.push(`${message.params.response.status} ${message.params.response.url}`) }) try { await send('Page.enable') await send('Runtime.enable') await send('Network.enable') const responsive = [] for (const size of sizes) { await setSize(send, size) await openState(send, '?genealogyId=1001') const current = await metrics(send) assert(!current.horizontalOverflow, `${size.width}x${size.height}: horizontal overflow`) assert(current.context && current.search && current.firstCard, `${size.width}x${size.height}: missing baseline content`) assert(current.cardCount === 3, `${size.width}x${size.height}: expected three members`) await valueOf(send, 'scrollTo(0, document.documentElement.scrollHeight)') await sleep(50) const lastBottom = await valueOf(send, "document.querySelector('.directory-card:last-child').getBoundingClientRect().bottom") assert(lastBottom <= size.height + 1, `${size.width}x${size.height}: last member is not reachable`) await valueOf(send, 'scrollTo(0, 0)') responsive.push(current) } await setSize(send, { width: 412, height: 915 }) await openState(send, '?state=loading&genealogyId=1001') assert((await metrics(send)).state === 'loading', 'Loading state did not render') await openState(send, '?state=empty&genealogyId=1001') assert((await metrics(send)).state === 'empty', 'Empty state did not render') await openState(send, '?state=error&genealogyId=1001') assert((await metrics(send)).state === 'error', 'Error state did not render') assert(!await valueOf(send, "Boolean(document.querySelector('.directory-search'))"), 'Error state must not expose search') await valueOf(send, "document.querySelector('.directory-content .app-button').click()") await waitFor(send, "document.querySelectorAll('.directory-card').length === 3 && Boolean(document.querySelector('.directory-search'))", 'Error retry did not restore the list') const inputSearch = async (value) => { await valueOf(send, `(() => { const input = document.querySelector('.directory-search input') input.value = ${JSON.stringify(value)} input.dispatchEvent(new Event('input', { bubbles: true })) return true })()`) await sleep(50) await valueOf(send, "document.querySelector('.directory-search__action').click()") } await inputSearch('不存在') await waitFor(send, "Boolean(document.querySelector('.directory-state--empty'))", 'No-result search did not render the empty state') await inputSearch('') await waitFor(send, "document.querySelectorAll('.directory-card').length === 3", 'Cleared search did not restore all members') await openState(send, '?genealogyId=1001') const final = await metrics(send) assert(final.viewport.width === 412 && final.viewport.height === 915 && final.state === 'list', 'Final approval state is invalid') assert(runtimeErrors.length === 0, `Runtime errors: ${runtimeErrors.join(' | ')}`) assert(resourceErrors.length === 0, `Resource errors: ${resourceErrors.join(' | ')}`) process.stdout.write(`${JSON.stringify({ projectPageCount, responsive, final, runtimeErrors, resourceErrors }, null, 2)}\n`) process.stdout.write('PASS T07 module baseline runtime smoke\n') } finally { socket.close() } } run().catch((error) => { process.stderr.write(`${error.stack || error.message}\n`) process.exit(1) })