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) })