// 仅检查线稿自身的真实浏览器交互;不调用业务接口,不代表后端验收。 const assert = require('node:assert/strict'); const { chromium } = require('../.work/wireframe-tools/node_modules/playwright'); const baseUrl = process.env.WIREFRAME_URL || 'http://127.0.0.1:18761'; async function main() { const browser = await chromium.launch({ headless: true, executablePath: process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE }); try { const page = await browser.newPage({ viewport: { width: 1440, height: 1000 } }); const errors = []; page.on('pageerror', error => errors.push(error.message)); const checks = []; async function adminLogin(product) { await page.waitForSelector(`form[data-submit="${product}-login"]`); await page.locator('[name="account"]').fill('demo-employee'); await page.locator('[name="password"]').fill('wireframe-only'); await page.locator(`form[data-submit="${product}-login"] button[type="submit"]`).click(); await page.waitForFunction(() => currentPage !== 'login'); } async function route(key) { const [role, target] = key.split('/'); const product = ['merchant', 'staff', 'supervisor'].includes(role) ? 'merchant' : role === 'platform' ? 'platform' : null; if (product && target !== 'login' && !await page.evaluate(value => adminAuth[value], product)) { await page.locator(`[data-role-home="${product}"]`).click(); await adminLogin(product); } if (['merchant', 'staff', 'supervisor'].includes(role) && await page.evaluate(() => merchantRole) !== role) { await page.locator('[data-role-home="merchant"]').click(); await page.waitForFunction(() => ['merchant', 'staff', 'supervisor'].includes(currentRole)); if (!await page.locator('.review-sidebar').isVisible()) await page.locator('#review-toggle').click(); await page.locator(`[data-employee-role="${role}"]`).click(); await page.waitForFunction(value => currentRole === value, role); await page.locator('#review-toggle').click(); } await page.evaluate(({ role, target }) => { location.hash = routeUrl(role, target); }, { role, target }); await page.waitForFunction(value => `${currentRole}/${currentPage}` === value && document.getElementById('page-title').textContent === screens[value].title, key); } async function action(id) { await page.locator(`#product [data-action="${id}"]`).click(); } async function submit(id) { await page.locator(`form[data-submit="${id}"] button[type="submit"]`).click(); } async function confirm() { await page.locator('#dialog-confirm').click(); await page.waitForFunction(() => !document.getElementById('confirm-dialog').open && pendingConfirmation === null); } async function scenario(id) { if (!await page.locator('.review-sidebar').isVisible()) await page.locator('#review-toggle').click(); await page.locator(`[data-scenario="${id}"]`).click(); await page.locator('#review-toggle').click(); } async function value(key) { return page.evaluate(name => state[name], key); } await page.goto(baseUrl); await page.waitForSelector('#product .surface'); assert.equal(await page.locator('.review-sidebar').isVisible(), false); assert.equal(await page.locator('.shop-tabs button').count(), 4); await page.locator('.goods-card').nth(1).click(); await page.waitForURL('**/#customer/product'); await page.locator('#product').getByRole('button', { name: '客服咨询', exact: true }).click(); await page.waitForURL('**/#customer/chat'); await route('customer/chat'); assert.match(await page.locator('#product').innerText(), /当前咨询商品:鼠标/); await page.locator('[data-role-home="merchant"]').click(); await page.waitForURL('**/#merchant/login'); assert.equal(await page.locator('.admin-sidebar').count(), 0); await adminLogin('merchant'); await page.waitForURL('**/#merchant/overview'); await route('merchant/overview'); assert.ok(await page.locator('.admin-sidebar').getByRole('button', { name: '商品管理', exact: true }).count()); assert.equal(await page.locator('.metric').count(), 4); await page.locator('[data-role-home="platform"]').click(); await page.waitForURL('**/#platform/login'); assert.equal(await page.locator('.admin-sidebar').count(), 0); await adminLogin('platform'); await page.waitForURL('**/#platform/dashboard'); await route('platform/dashboard'); assert.ok(await page.locator('.admin-sidebar').getByRole('button', { name: '商家管理', exact: true }).count()); checks.push('商城底部导航、商品上下文 AI 入口、后台独立导航与首页'); assert.equal(await page.locator('#role-nav button').count(), 3); await route('staff/queue'); assert.match(await page.locator('.admin-brand').innerText(), /商家后台/); assert.equal(await page.locator('.admin-sidebar').getByRole('button', { name: '审批待办', exact: true }).count(), 0); await page.evaluate(() => { location.hash = '#merchant/approval/review'; }); await page.waitForFunction(() => location.hash === '#merchant/service/queue'); assert.equal(await page.evaluate(() => merchantRole), 'staff'); await route('supervisor/approvals'); assert.match(await page.locator('.admin-brand').innerText(), /商家后台/); assert.equal(await page.locator('.admin-sidebar').getByRole('button', { name: '客服接待', exact: true }).count(), 0); await route('merchant/overview'); assert.equal(await page.locator('.admin-sidebar').getByRole('button', { name: '审批待办', exact: true }).count(), 0); checks.push('三个产品入口、商家员工菜单权限、直接访问无权页面受限'); await route('merchant/rules'); await page.locator('[name="citation"]').fill('登录过期前尚未提交的内容'); await scenario('session-expired'); assert.equal(await page.locator('input[type="password"]').inputValue(), ''); await adminLogin('merchant'); await route('merchant/rules'); assert.equal(await page.locator('[name="citation"]').inputValue(), '登录过期前尚未提交的内容'); await action('admin-logout'); await confirm(); await page.waitForURL('**/#merchant/login'); assert.equal(await page.evaluate(() => adminAuth.platform), true); await scenario('login-error'); assert.match(await page.locator('#notice').innerText(), /登录未成功/); await adminLogin('merchant'); checks.push('两个后台独立登录、受保护页面拦截、过期返回与保留草稿、退出隔离、失败反馈'); await route('merchant/notifications'); assert.match(await page.locator('#product').innerText(), /知识资料等待发布/); await page.locator('[data-id="admin-knowledge"]').click(); await page.waitForFunction(() => currentPage === 'notification-detail'); assert.equal(await page.evaluate(() => unreadCount()), 2); await page.locator('#product').getByRole('button', { name: '查看知识资料', exact: true }).click(); await page.waitForFunction(() => currentPage === 'knowledge'); await route('staff/notifications'); assert.equal(await page.evaluate(() => unreadCount()), 3); assert.doesNotMatch(await page.locator('#product').innerText(), /知识资料等待发布/); await action('notifications-read-all'); await confirm(); assert.equal(await page.evaluate(() => unreadCount()), 0); await page.locator('[data-action="notification-filter"][data-filter="未读"]').click(); assert.match(await page.locator('#product').innerText(), /暂无未读通知/); await route('merchant/notifications'); assert.equal(await page.evaluate(() => unreadCount()), 2); await page.locator('[data-action="notification-filter"][data-filter="全部"]').click(); checks.push('通知按员工隔离、查看详情更新未读、业务跳转、全部已读与空状态'); // 对所有入口检查实际 DOM、控件动作和横向布局,不使用图像判断。 const keys = await page.evaluate(() => Object.keys(screens)); for (const key of keys) { await route(key); assert.ok(await page.locator('#product').innerText(), key); assert.equal(await page.locator('#product h2, #product h3, #product header, #product .empty, #product table').count() > 0, true, key); const missing = await page.evaluate(() => [...document.querySelectorAll('[data-action]')].map(item => item.dataset.action).filter(id => !actionHandlers[id])); assert.deepEqual(missing, [], key); const missingPages = await page.evaluate(() => [...document.querySelectorAll('[data-page]')].map(item => `${item.dataset.role}/${item.dataset.page}`).filter(key => !screens[key])); assert.deepEqual(missingPages, [], key); const overflow = await page.evaluate(() => document.documentElement.scrollWidth > innerWidth + 1); assert.equal(overflow, false, `desktop overflow: ${key}`); } checks.push(`${keys.length} 个页面入口、动作注册和桌面宽度检查`); await route('customer/login'); await page.locator('[name="password"]').fill('wireframe-only'); await page.locator('[name="accept"]').check(); await submit('login'); await route('customer/apply'); await page.locator('[name="reason"]').fill('空格键无法响应'); await submit('proposal'); await page.waitForURL('**/#customer/proposal'); assert.equal(await value('ticket'), 'DRAFT'); await page.locator('[name="confirmed"]').check(); await submit('confirm-proposal'); await page.waitForURL('**/#customer/ticket'); assert.equal(await value('ticket'), 'WAITING_REVIEW'); await route('supervisor/review'); await page.locator('[name="reason"]').fill('已核对客户确认与有效规则'); await page.locator('[name="checked"]').check(); await submit('approval'); await confirm(); await page.waitForURL('**/#merchant/approval/history'); assert.equal(await value('ticket'), 'APPROVED'); await route('customer/ticket'); assert.match(await page.locator('#product').innerText(), /审批通过/); checks.push('客户创建、本人确认、主管审批和跨入口进度同步'); await scenario('unknown'); assert.equal(await page.locator('[data-action="cancel-ticket"]').isDisabled(), true); await route('customer/apply'); assert.equal(await page.locator('button[type="submit"]').isDisabled(), true); await route('supervisor/review'); await scenario('self'); assert.equal(await page.locator('button[type="submit"]').isDisabled(), true); await scenario('stale'); assert.equal(await page.locator('button[type="submit"]').isDisabled(), true); await action('refresh-proposal'); assert.equal(await value('customerConfirmed'), false); assert.equal(await value('ticket'), 'DRAFT'); checks.push('结果未知禁止重复申请、自己审批受限、版本变化重新确认'); await route('customer/ticket'); await action('cancel-ticket'); await confirm(); assert.equal(await value('ticket'), 'CANCELLED'); await route('staff/conversation'); await scenario('human'); await action('takeover'); await page.locator('[name="message"]').fill('请检查即将发送的方案'); await submit('staff-message'); await route('customer/chat'); assert.match(await page.locator('#product').innerText(), /请检查即将发送的方案/); await page.locator('[name="message"]').fill('连接恢复后发送的内容'); await scenario('disconnect'); assert.equal(await page.locator('[name="message"]').inputValue(), '连接恢复后发送的内容'); await action('reconnect'); assert.equal(await page.locator('[name="message"]').inputValue(), '连接恢复后发送的内容'); await route('staff/conversation'); await action('release'); await confirm(); assert.equal(await value('owner'), 'AGENT'); checks.push('取消、接管、人工回复、结束接管与断线保留输入'); await route('customer/memory'); await action('memory-consent'); await confirm(); await submit('memory'); assert.equal(await value('memorySaved'), true); await action('memory-clear'); await confirm(); assert.equal(await value('memorySaved'), false); assert.equal(await value('memoryConsent'), true); await action('memory-revoke'); await confirm(); assert.equal(await value('memoryConsent'), false); checks.push('沟通习惯授权、保存、清空与撤销'); await route('merchant/knowledge'); await submit('knowledge-save'); await action('knowledge-publish'); await confirm(); assert.equal(await value('knowledge'), 'PUBLISHED'); await route('merchant/rules'); await page.locator('[name="days"]').fill('10'); await submit('rule-save'); await action('rule-publish'); await confirm(); assert.equal(await value('ruleVersion'), 2); assert.equal(await value('proposalRuleVersion'), 1); await route('merchant/release'); await page.locator('[name="checked"]').check(); await page.locator('[name="reason"]').fill('线稿评审发布流程'); await submit('release'); await confirm(); assert.equal(await value('release'), 'R2 已发布'); checks.push('知识和规则发布、原方案规则版本保留、发布清单确认'); await route('merchant/billing'); await page.locator('[name="amount"]').fill('120'); await page.locator('form[data-submit="budget"] [name="reason"]').fill('业务量增加'); await submit('budget'); assert.equal(await value('budget'), 100); assert.equal(await page.locator('form[data-submit="budget-decision"]').count(), 0); await scenario('billing-reviewer'); await page.locator('form[data-submit="budget-decision"] [name="reason"]').fill('已核实'); await submit('budget-decision'); await confirm(); assert.equal(await value('budget'), 120); checks.push('预算申请保持原值、另一位员工审批后生效'); await route('platform/capacity'); await page.locator('[name="reserved"]').fill('3'); await page.locator('[name="reason"]').fill('检查容量约束'); await submit('capacity'); assert.match(await page.locator('#notice').innerText(), /可分配总量/); await route('platform/deliveries'); await action('delivery-retry'); await confirm(); assert.equal(await value('delivery'), 'QUEUED'); assert.equal(await page.locator('[data-action="delivery-retry"]').isDisabled(), true); await route('platform/health'); assert.equal(await page.getByText('空格键无法响应', { exact: true }).count(), 0); checks.push('额度约束、投递重试等待确认、监控正文边界'); await page.setViewportSize({ width: 390, height: 844 }); for (const key of keys) { await route(key); const overflow = await page.evaluate(() => document.documentElement.scrollWidth > innerWidth + 1); assert.equal(overflow, false, `mobile overflow: ${key}`); } checks.push(`${keys.length} 个页面在 390px 宽度下无整体横向溢出`); assert.deepEqual(errors, []); console.log(JSON.stringify({ pages: keys.length, checks, browserErrors: errors, backendRequests: 0, screenshots: 0 }, null, 2)); } finally { await browser.close(); } } main().catch(error => { console.error(error); process.exitCode = 1; });