167 lines
7.5 KiB
JavaScript
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 === 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)
|
|
})
|