| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246 |
- // 仅检查线稿自身的真实浏览器交互;不调用业务接口,不代表后端验收。
- 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; });
|