119 lines
6.1 KiB
JavaScript
119 lines
6.1 KiB
JavaScript
const origin = process.argv[2] || 'http://localhost:5173'
|
|
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 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 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 < 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)
|
|
}
|
|
|
|
let auditId = 0
|
|
const open = async (send, route, query, selector) => {
|
|
auditId += 1
|
|
const url = `${origin}/?rootAudit=${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)}))`, `Missing ${selector}: ${route}${query}`)
|
|
}
|
|
|
|
const run = async () => {
|
|
const { socket, send } = await connect()
|
|
try {
|
|
await send('Page.enable')
|
|
await send('Runtime.enable')
|
|
|
|
// 步骤一:四个根入口必须能进入各自默认状态,继续承担基础路由与渲染冒烟职责。
|
|
await open(send, '/pages/family/f01-family-feed', '?genealogyId=1001', '.feed-state--list')
|
|
await open(send, '/pages/family/f02-publish-feed', '?genealogyId=1001', '.publish-state--form')
|
|
await open(send, '/pages/notification/n01-message-center', '', '.notice-state--list')
|
|
await open(send, '/pages/profile/m01-profile-home', '', '.profile-state--ready')
|
|
|
|
// 步骤二:N01 单条消息要先标记已读再进入详情,返回后必须保留刚才的已读状态。
|
|
await open(send, '/pages/notification/n01-message-center', '?genealogyId=1001', '.notice-state--list')
|
|
if ((await valueOf(send, "document.querySelectorAll('.notice-card__status.is-unread').length")) !== 1) {
|
|
throw new Error('N01 initial unread count is invalid')
|
|
}
|
|
await valueOf(send, "document.querySelector('.notice-card').click()")
|
|
await waitFor(send, "location.hash === '#/pages/notification/n02-message-detail?id=review-1&sourceKey=N01'", 'N01 first notice did not open the exact N02 detail route')
|
|
if (await valueOf(send, "location.hash.includes('genealogyId=')")) {
|
|
throw new Error('N01 duplicated genealogyId in the N02 detail route')
|
|
}
|
|
await waitFor(send, "Boolean(document.querySelector('.notice-state--ready'))", 'N02 detail did not render after opening the notice')
|
|
await valueOf(send, "document.querySelector('.header-back').click()")
|
|
await waitFor(send, "Boolean(document.querySelector('.notice-state--list'))", 'N01 did not return from N02')
|
|
await waitFor(send, "document.querySelectorAll('.notice-card__status.is-unread').length === 0", 'N01 did not preserve the first notice read state')
|
|
|
|
await open(send, '/pages/notification/n02-message-detail', '?id=missing', '.notice-state--expired')
|
|
if (await valueOf(send, "Boolean(document.querySelector('.action-stack'))")) {
|
|
throw new Error('N02 unknown notice exposed business actions')
|
|
}
|
|
|
|
// 步骤三:全标已读必须清除全部未读标记、隐藏页头操作,并显示项目内反馈。
|
|
await open(send, '/pages/notification/n01-message-center', '?genealogyId=1001', '.notice-state--list')
|
|
await valueOf(send, "document.querySelector('.header-action').click()")
|
|
await waitFor(send, "document.querySelectorAll('.notice-card__status.is-unread').length === 0", 'N01 mark-all did not clear unread notices')
|
|
await waitFor(send, "document.querySelector('.header-action')?.disabled === true && !document.querySelector('.header-action')?.textContent.trim()", 'N01 mark-all did not disable the completed header action')
|
|
await waitFor(send, "document.querySelector('.app-toast')?.textContent.includes('已全部标记为已读')", 'N01 mark-all did not show the project Toast')
|
|
|
|
// 步骤四:保留原有空态、成功态和错误态入口,防止新增交互覆盖挤掉页面状态冒烟。
|
|
await open(send, '/pages/family/f01-family-feed', '?state=empty&genealogyId=1001', '.feed-state--empty')
|
|
await open(send, '/pages/family/f02-publish-feed', '?state=preview&genealogyId=1001', '.publish-state--preview')
|
|
if (!(await valueOf(send, "document.querySelector('.publish-result')?.textContent.includes('尚未提交服务器')"))) {
|
|
throw new Error('F02 preview must disclose that the draft was not published')
|
|
}
|
|
await open(send, '/pages/notification/n01-message-center', '?state=empty', '.notice-state--empty')
|
|
await open(send, '/pages/profile/m01-profile-home', '?state=error', '.profile-state--error')
|
|
|
|
process.stdout.write('ROOT-PAGES-RUNTIME-SMOKE PASS\n')
|
|
} finally {
|
|
socket.close()
|
|
}
|
|
}
|
|
|
|
run().catch((error) => {
|
|
process.stderr.write(`${error.stack || error.message}\n`)
|
|
process.exit(1)
|
|
})
|