Files
jiapuapp/tests/t07-module-baseline-runtime-smoke.js
T
2026-07-23 08:24:07 +08:00

167 lines
7.5 KiB
JavaScript

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 === 6, `${size.width}x${size.height}: expected six 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 === 6 && 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 === 6", '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)
})