layout-functional.mjs 5.9 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091
  1. import assert from 'node:assert/strict';
  2. import { readFile } from 'node:fs/promises';
  3. import { fileURLToPath, pathToFileURL } from 'node:url';
  4. import { chromium } from 'playwright';
  5. // 页面目录独立来源于批准线稿,检查全部页面在两个宽度下的可见内容和文档几何。
  6. const groups = [
  7. ['customer', 'customer01', ['home', 'categories', 'product', 'cart', 'profile', 'messages', 'address', 'favorites', 'settings', 'chat', 'login', 'orders', 'order', 'policy', 'apply', 'proposal', 'ticket', 'memory', 'identity']],
  8. ['merchant', 'admin01', ['login', 'overview', 'access', 'channels', 'rules', 'knowledge', 'release', 'billing', 'products', 'orders', 'after-sales', 'customers', 'marketing', 'service', 'notifications', 'notification-detail']],
  9. ['staff', 'staff01', ['queue', 'conversation', 'draft', 'ticket', 'customers', 'reference', 'quick-replies', 'statistics', 'notifications', 'notification-detail']],
  10. ['supervisor', 'supervisor01', ['approvals', 'review', 'history', 'team', 'statistics', 'notifications', 'notification-detail']],
  11. ['platform', 'platform-admin', ['login', 'dashboard', 'tenants', 'capacity', 'deliveries', 'health', 'audit', 'applications', 'models', 'permissions', 'settings']],
  12. ];
  13. const pathFor = (role, name) => role === 'staff' ? `merchant/service/${name}` : role === 'supervisor' ? `merchant/approval/${name}` : `${role}/${name}`;
  14. export async function checkLayouts(page, baseUrl, accounts) {
  15. const platformAccount = JSON.parse(await readFile(new URL('../../.work/platform-accounts.json', import.meta.url), 'utf8'));
  16. const results = [], failures = [];
  17. const origin = baseUrl.replace(/\/$/, '');
  18. const visit = async (role, name) => { await page.goto(`${origin}/#/${pathFor(role, name)}`); };
  19. const checkPage = async (role, name, width) => {
  20. await visit(role, name);
  21. const container = page.locator(role === 'customer' ? '.mobile' : name === 'login' ? '.login-panel' : '.admin-content');
  22. await container.waitFor({ state: 'visible', timeout: 15000 });
  23. await page.locator('.loading').waitFor({ state: 'hidden' });
  24. assert.equal(await page.getByRole('heading', { name: '页面不可访问', exact: true }).count(), 0, `${role}/${name} 可访问`);
  25. assert.ok((await container.innerText()).trim().length > 10, `${role}/${name} 显示页面内容`);
  26. // 仅测量渲染后的 DOM,不读取 Vue 数据、浏览器存储或组件对象。
  27. const geometry = await page.locator('html').evaluate(element => ({
  28. width: element.clientWidth,
  29. scroll: element.scrollWidth,
  30. overflowing: [...document.querySelectorAll('body *')].filter(node => {
  31. const box = node.getBoundingClientRect();
  32. return box.width > 0 && box.right > element.clientWidth + 1 && !node.closest('.table-wrap');
  33. }).slice(0, 8).map(node => ({ tag: node.tagName, class: node.className, text: node.textContent.slice(0, 70), right: node.getBoundingClientRect().right })),
  34. }));
  35. const nameText = `${width}px ${role}/${name}`;
  36. if (geometry.scroll > geometry.width + 1) failures.push({ name: nameText, ...geometry });
  37. results.push({ name: nameText, passed: geometry.scroll <= geometry.width + 1 });
  38. };
  39. const signIn = async (role, username) => {
  40. if (role === 'customer') {
  41. await visit('customer', 'login');
  42. await page.getByLabel('账户', { exact: true }).fill(username);
  43. await page.getByLabel('密码', { exact: true }).fill(accounts.password);
  44. await page.getByRole('checkbox').check();
  45. await page.getByRole('button', { name: '登录并继续', exact: true }).click();
  46. await page.waitForURL(url => !url.hash.endsWith('/login'));
  47. await visit('customer', 'profile');
  48. await page.getByRole('heading', { name: username, exact: true }).waitFor();
  49. return;
  50. }
  51. const product = role === 'platform' ? 'platform' : 'merchant';
  52. await visit(product, 'login');
  53. if (product === 'merchant') await page.getByLabel('商家标识', { exact: true }).fill(accounts.tenant_id);
  54. await page.getByLabel('员工账户', { exact: true }).fill(username);
  55. await page.getByLabel('密码', { exact: true }).fill(product === 'platform' ? platformAccount.password : accounts.password);
  56. await page.getByRole('button', { name: '登录', exact: true }).click();
  57. await page.locator('.admin-content').waitFor();
  58. };
  59. const logout = async () => {
  60. await page.getByRole('button', { name: '退出登录', exact: true }).click();
  61. await page.getByRole('dialog').getByRole('button', { name: '确认操作', exact: true }).click();
  62. await page.locator('.login-panel').waitFor();
  63. };
  64. assert.equal(groups.reduce((sum, [, , pages]) => sum + pages.length, 0), 63);
  65. for (const [role, username, pages] of groups) {
  66. await signIn(role, username);
  67. for (const width of [1440, 390]) {
  68. await page.setViewportSize({ width, height: 1000 });
  69. for (const name of pages) await checkPage(role, name, width);
  70. }
  71. if (role !== 'customer') {
  72. await visit(role, pages.find(name => name !== 'login'));
  73. await logout();
  74. }
  75. }
  76. if (failures.length) throw new Error(`页面文档宽度超出视口:${JSON.stringify(failures)}`);
  77. return results;
  78. }
  79. if (process.argv[1] && pathToFileURL(process.argv[1]).href === import.meta.url) {
  80. const accounts = JSON.parse(await readFile(fileURLToPath(new URL('../../.work/frontend-accounts.json', import.meta.url)), 'utf8'));
  81. const browser = await chromium.launch({ headless: true, executablePath: process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE || 'C:/Users/Administrator/AppData/Local/ms-playwright/chromium-1228/chrome-win64/chrome.exe' });
  82. try {
  83. const page = await browser.newPage();
  84. page.on('pageerror', error => { console.error(error.message); });
  85. const results = await checkLayouts(page, process.env.FRONTEND_URL || 'http://127.0.0.1:18762', accounts);
  86. console.log(JSON.stringify({ passed: results.length, pages: 63, widths: [1440, 390] }));
  87. } finally { await browser.close(); }
  88. }