check_wireframes.cjs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246
  1. // 仅检查线稿自身的真实浏览器交互;不调用业务接口,不代表后端验收。
  2. const assert = require('node:assert/strict');
  3. const { chromium } = require('../.work/wireframe-tools/node_modules/playwright');
  4. const baseUrl = process.env.WIREFRAME_URL || 'http://127.0.0.1:18761';
  5. async function main() {
  6. const browser = await chromium.launch({ headless: true, executablePath: process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE });
  7. try {
  8. const page = await browser.newPage({ viewport: { width: 1440, height: 1000 } });
  9. const errors = [];
  10. page.on('pageerror', error => errors.push(error.message));
  11. const checks = [];
  12. async function adminLogin(product) {
  13. await page.waitForSelector(`form[data-submit="${product}-login"]`);
  14. await page.locator('[name="account"]').fill('demo-employee');
  15. await page.locator('[name="password"]').fill('wireframe-only');
  16. await page.locator(`form[data-submit="${product}-login"] button[type="submit"]`).click();
  17. await page.waitForFunction(() => currentPage !== 'login');
  18. }
  19. async function route(key) {
  20. const [role, target] = key.split('/');
  21. const product = ['merchant', 'staff', 'supervisor'].includes(role) ? 'merchant' : role === 'platform' ? 'platform' : null;
  22. if (product && target !== 'login' && !await page.evaluate(value => adminAuth[value], product)) {
  23. await page.locator(`[data-role-home="${product}"]`).click();
  24. await adminLogin(product);
  25. }
  26. if (['merchant', 'staff', 'supervisor'].includes(role) && await page.evaluate(() => merchantRole) !== role) {
  27. await page.locator('[data-role-home="merchant"]').click();
  28. await page.waitForFunction(() => ['merchant', 'staff', 'supervisor'].includes(currentRole));
  29. if (!await page.locator('.review-sidebar').isVisible()) await page.locator('#review-toggle').click();
  30. await page.locator(`[data-employee-role="${role}"]`).click();
  31. await page.waitForFunction(value => currentRole === value, role);
  32. await page.locator('#review-toggle').click();
  33. }
  34. await page.evaluate(({ role, target }) => { location.hash = routeUrl(role, target); }, { role, target });
  35. await page.waitForFunction(value => `${currentRole}/${currentPage}` === value && document.getElementById('page-title').textContent === screens[value].title, key);
  36. }
  37. async function action(id) { await page.locator(`#product [data-action="${id}"]`).click(); }
  38. async function submit(id) { await page.locator(`form[data-submit="${id}"] button[type="submit"]`).click(); }
  39. async function confirm() { await page.locator('#dialog-confirm').click(); await page.waitForFunction(() => !document.getElementById('confirm-dialog').open && pendingConfirmation === null); }
  40. 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(); }
  41. async function value(key) { return page.evaluate(name => state[name], key); }
  42. await page.goto(baseUrl);
  43. await page.waitForSelector('#product .surface');
  44. assert.equal(await page.locator('.review-sidebar').isVisible(), false);
  45. assert.equal(await page.locator('.shop-tabs button').count(), 4);
  46. await page.locator('.goods-card').nth(1).click();
  47. await page.waitForURL('**/#customer/product');
  48. await page.locator('#product').getByRole('button', { name: '客服咨询', exact: true }).click();
  49. await page.waitForURL('**/#customer/chat');
  50. await route('customer/chat');
  51. assert.match(await page.locator('#product').innerText(), /当前咨询商品:鼠标/);
  52. await page.locator('[data-role-home="merchant"]').click();
  53. await page.waitForURL('**/#merchant/login');
  54. assert.equal(await page.locator('.admin-sidebar').count(), 0);
  55. await adminLogin('merchant');
  56. await page.waitForURL('**/#merchant/overview');
  57. await route('merchant/overview');
  58. assert.ok(await page.locator('.admin-sidebar').getByRole('button', { name: '商品管理', exact: true }).count());
  59. assert.equal(await page.locator('.metric').count(), 4);
  60. await page.locator('[data-role-home="platform"]').click();
  61. await page.waitForURL('**/#platform/login');
  62. assert.equal(await page.locator('.admin-sidebar').count(), 0);
  63. await adminLogin('platform');
  64. await page.waitForURL('**/#platform/dashboard');
  65. await route('platform/dashboard');
  66. assert.ok(await page.locator('.admin-sidebar').getByRole('button', { name: '商家管理', exact: true }).count());
  67. checks.push('商城底部导航、商品上下文 AI 入口、后台独立导航与首页');
  68. assert.equal(await page.locator('#role-nav button').count(), 3);
  69. await route('staff/queue');
  70. assert.match(await page.locator('.admin-brand').innerText(), /商家后台/);
  71. assert.equal(await page.locator('.admin-sidebar').getByRole('button', { name: '审批待办', exact: true }).count(), 0);
  72. await page.evaluate(() => { location.hash = '#merchant/approval/review'; });
  73. await page.waitForFunction(() => location.hash === '#merchant/service/queue');
  74. assert.equal(await page.evaluate(() => merchantRole), 'staff');
  75. await route('supervisor/approvals');
  76. assert.match(await page.locator('.admin-brand').innerText(), /商家后台/);
  77. assert.equal(await page.locator('.admin-sidebar').getByRole('button', { name: '客服接待', exact: true }).count(), 0);
  78. await route('merchant/overview');
  79. assert.equal(await page.locator('.admin-sidebar').getByRole('button', { name: '审批待办', exact: true }).count(), 0);
  80. checks.push('三个产品入口、商家员工菜单权限、直接访问无权页面受限');
  81. await route('merchant/rules');
  82. await page.locator('[name="citation"]').fill('登录过期前尚未提交的内容');
  83. await scenario('session-expired');
  84. assert.equal(await page.locator('input[type="password"]').inputValue(), '');
  85. await adminLogin('merchant');
  86. await route('merchant/rules');
  87. assert.equal(await page.locator('[name="citation"]').inputValue(), '登录过期前尚未提交的内容');
  88. await action('admin-logout'); await confirm();
  89. await page.waitForURL('**/#merchant/login');
  90. assert.equal(await page.evaluate(() => adminAuth.platform), true);
  91. await scenario('login-error');
  92. assert.match(await page.locator('#notice').innerText(), /登录未成功/);
  93. await adminLogin('merchant');
  94. checks.push('两个后台独立登录、受保护页面拦截、过期返回与保留草稿、退出隔离、失败反馈');
  95. await route('merchant/notifications');
  96. assert.match(await page.locator('#product').innerText(), /知识资料等待发布/);
  97. await page.locator('[data-id="admin-knowledge"]').click();
  98. await page.waitForFunction(() => currentPage === 'notification-detail');
  99. assert.equal(await page.evaluate(() => unreadCount()), 2);
  100. await page.locator('#product').getByRole('button', { name: '查看知识资料', exact: true }).click();
  101. await page.waitForFunction(() => currentPage === 'knowledge');
  102. await route('staff/notifications');
  103. assert.equal(await page.evaluate(() => unreadCount()), 3);
  104. assert.doesNotMatch(await page.locator('#product').innerText(), /知识资料等待发布/);
  105. await action('notifications-read-all'); await confirm();
  106. assert.equal(await page.evaluate(() => unreadCount()), 0);
  107. await page.locator('[data-action="notification-filter"][data-filter="未读"]').click();
  108. assert.match(await page.locator('#product').innerText(), /暂无未读通知/);
  109. await route('merchant/notifications');
  110. assert.equal(await page.evaluate(() => unreadCount()), 2);
  111. await page.locator('[data-action="notification-filter"][data-filter="全部"]').click();
  112. checks.push('通知按员工隔离、查看详情更新未读、业务跳转、全部已读与空状态');
  113. // 对所有入口检查实际 DOM、控件动作和横向布局,不使用图像判断。
  114. const keys = await page.evaluate(() => Object.keys(screens));
  115. for (const key of keys) {
  116. await route(key);
  117. assert.ok(await page.locator('#product').innerText(), key);
  118. assert.equal(await page.locator('#product h2, #product h3, #product header, #product .empty, #product table').count() > 0, true, key);
  119. const missing = await page.evaluate(() => [...document.querySelectorAll('[data-action]')].map(item => item.dataset.action).filter(id => !actionHandlers[id]));
  120. assert.deepEqual(missing, [], key);
  121. const missingPages = await page.evaluate(() => [...document.querySelectorAll('[data-page]')].map(item => `${item.dataset.role}/${item.dataset.page}`).filter(key => !screens[key]));
  122. assert.deepEqual(missingPages, [], key);
  123. const overflow = await page.evaluate(() => document.documentElement.scrollWidth > innerWidth + 1);
  124. assert.equal(overflow, false, `desktop overflow: ${key}`);
  125. }
  126. checks.push(`${keys.length} 个页面入口、动作注册和桌面宽度检查`);
  127. await route('customer/login');
  128. await page.locator('[name="password"]').fill('wireframe-only');
  129. await page.locator('[name="accept"]').check();
  130. await submit('login');
  131. await route('customer/apply');
  132. await page.locator('[name="reason"]').fill('空格键无法响应');
  133. await submit('proposal');
  134. await page.waitForURL('**/#customer/proposal');
  135. assert.equal(await value('ticket'), 'DRAFT');
  136. await page.locator('[name="confirmed"]').check();
  137. await submit('confirm-proposal');
  138. await page.waitForURL('**/#customer/ticket');
  139. assert.equal(await value('ticket'), 'WAITING_REVIEW');
  140. await route('supervisor/review');
  141. await page.locator('[name="reason"]').fill('已核对客户确认与有效规则');
  142. await page.locator('[name="checked"]').check();
  143. await submit('approval'); await confirm();
  144. await page.waitForURL('**/#merchant/approval/history');
  145. assert.equal(await value('ticket'), 'APPROVED');
  146. await route('customer/ticket');
  147. assert.match(await page.locator('#product').innerText(), /审批通过/);
  148. checks.push('客户创建、本人确认、主管审批和跨入口进度同步');
  149. await scenario('unknown');
  150. assert.equal(await page.locator('[data-action="cancel-ticket"]').isDisabled(), true);
  151. await route('customer/apply');
  152. assert.equal(await page.locator('button[type="submit"]').isDisabled(), true);
  153. await route('supervisor/review'); await scenario('self');
  154. assert.equal(await page.locator('button[type="submit"]').isDisabled(), true);
  155. await scenario('stale');
  156. assert.equal(await page.locator('button[type="submit"]').isDisabled(), true);
  157. await action('refresh-proposal');
  158. assert.equal(await value('customerConfirmed'), false);
  159. assert.equal(await value('ticket'), 'DRAFT');
  160. checks.push('结果未知禁止重复申请、自己审批受限、版本变化重新确认');
  161. await route('customer/ticket');
  162. await action('cancel-ticket'); await confirm();
  163. assert.equal(await value('ticket'), 'CANCELLED');
  164. await route('staff/conversation');
  165. await scenario('human'); await action('takeover');
  166. await page.locator('[name="message"]').fill('请检查即将发送的方案');
  167. await submit('staff-message');
  168. await route('customer/chat');
  169. assert.match(await page.locator('#product').innerText(), /请检查即将发送的方案/);
  170. await page.locator('[name="message"]').fill('连接恢复后发送的内容');
  171. await scenario('disconnect');
  172. assert.equal(await page.locator('[name="message"]').inputValue(), '连接恢复后发送的内容');
  173. await action('reconnect');
  174. assert.equal(await page.locator('[name="message"]').inputValue(), '连接恢复后发送的内容');
  175. await route('staff/conversation'); await action('release'); await confirm();
  176. assert.equal(await value('owner'), 'AGENT');
  177. checks.push('取消、接管、人工回复、结束接管与断线保留输入');
  178. await route('customer/memory'); await action('memory-consent'); await confirm();
  179. await submit('memory');
  180. assert.equal(await value('memorySaved'), true);
  181. await action('memory-clear'); await confirm();
  182. assert.equal(await value('memorySaved'), false);
  183. assert.equal(await value('memoryConsent'), true);
  184. await action('memory-revoke'); await confirm();
  185. assert.equal(await value('memoryConsent'), false);
  186. checks.push('沟通习惯授权、保存、清空与撤销');
  187. await route('merchant/knowledge'); await submit('knowledge-save');
  188. await action('knowledge-publish'); await confirm();
  189. assert.equal(await value('knowledge'), 'PUBLISHED');
  190. await route('merchant/rules');
  191. await page.locator('[name="days"]').fill('10');
  192. await submit('rule-save'); await action('rule-publish'); await confirm();
  193. assert.equal(await value('ruleVersion'), 2);
  194. assert.equal(await value('proposalRuleVersion'), 1);
  195. await route('merchant/release');
  196. await page.locator('[name="checked"]').check();
  197. await page.locator('[name="reason"]').fill('线稿评审发布流程');
  198. await submit('release'); await confirm();
  199. assert.equal(await value('release'), 'R2 已发布');
  200. checks.push('知识和规则发布、原方案规则版本保留、发布清单确认');
  201. await route('merchant/billing');
  202. await page.locator('[name="amount"]').fill('120');
  203. await page.locator('form[data-submit="budget"] [name="reason"]').fill('业务量增加');
  204. await submit('budget');
  205. assert.equal(await value('budget'), 100);
  206. assert.equal(await page.locator('form[data-submit="budget-decision"]').count(), 0);
  207. await scenario('billing-reviewer');
  208. await page.locator('form[data-submit="budget-decision"] [name="reason"]').fill('已核实');
  209. await submit('budget-decision'); await confirm();
  210. assert.equal(await value('budget'), 120);
  211. checks.push('预算申请保持原值、另一位员工审批后生效');
  212. await route('platform/capacity');
  213. await page.locator('[name="reserved"]').fill('3');
  214. await page.locator('[name="reason"]').fill('检查容量约束');
  215. await submit('capacity');
  216. assert.match(await page.locator('#notice').innerText(), /可分配总量/);
  217. await route('platform/deliveries'); await action('delivery-retry'); await confirm();
  218. assert.equal(await value('delivery'), 'QUEUED');
  219. assert.equal(await page.locator('[data-action="delivery-retry"]').isDisabled(), true);
  220. await route('platform/health');
  221. assert.equal(await page.getByText('空格键无法响应', { exact: true }).count(), 0);
  222. checks.push('额度约束、投递重试等待确认、监控正文边界');
  223. await page.setViewportSize({ width: 390, height: 844 });
  224. for (const key of keys) {
  225. await route(key);
  226. const overflow = await page.evaluate(() => document.documentElement.scrollWidth > innerWidth + 1);
  227. assert.equal(overflow, false, `mobile overflow: ${key}`);
  228. }
  229. checks.push(`${keys.length} 个页面在 390px 宽度下无整体横向溢出`);
  230. assert.deepEqual(errors, []);
  231. console.log(JSON.stringify({ pages: keys.length, checks, browserErrors: errors, backendRequests: 0, screenshots: 0 }, null, 2));
  232. } finally {
  233. await browser.close();
  234. }
  235. }
  236. main().catch(error => { console.error(error); process.exitCode = 1; });