functional.mjs 4.6 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768
  1. import assert from 'node:assert/strict';
  2. import { readFile, mkdir, writeFile } from 'node:fs/promises';
  3. import { chromium } from 'playwright';
  4. import { checkCustomer } from './customer-functional.mjs';
  5. import { checkPlatform } from './platform-functional.mjs';
  6. import { checkLayouts } from './layout-functional.mjs';
  7. import { checkSessions } from './session-functional.mjs';
  8. import { checkComponents } from './components-functional.mjs';
  9. const baseUrl = process.env.FRONTEND_URL || 'http://127.0.0.1:18762';
  10. const accounts = JSON.parse(await readFile(new URL('../../.work/frontend-accounts.json', import.meta.url), 'utf8'));
  11. const browser = await chromium.launch({ headless: true, executablePath: process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE });
  12. const results = [];
  13. const errors = [];
  14. let requests = 0;
  15. try {
  16. for (const [name, check] of [['customer', checkCustomer], ['platform', checkPlatform]]) {
  17. const context = await browser.newContext({ viewport: { width: 1440, height: 1000 } });
  18. const page = await context.newPage();
  19. page.setDefaultTimeout(15000);
  20. page.on('pageerror', error => errors.push(`${name}: ${error.message}`));
  21. page.on('request', request => { if (/\/(openapi|admin)\//.test(request.url())) requests++; });
  22. const checks = await check(page, baseUrl, accounts);
  23. results.push({ area: name, checks });
  24. const palette = await page.locator(name === 'customer' ? '.mobile .content' : '.admin-sidebar').evaluate(element => getComputedStyle(element).backgroundColor);
  25. assert.equal(palette, name === 'customer' ? 'rgb(246, 248, 252)' : 'rgb(7, 17, 38)', `${name} 已确认背景色`);
  26. // 只检查公开 DOM 的布局,不读取组件内部状态,也不使用截图。
  27. for (const width of [1440, 390]) {
  28. await page.setViewportSize({ width, height: 1000 });
  29. const geometry = await page.locator('body').evaluate(element => ({ width: document.documentElement.clientWidth, scroll: element.scrollWidth }));
  30. assert.ok(geometry.scroll <= geometry.width + 1, `${name} ${width}px 页面横向溢出`);
  31. }
  32. await context.close();
  33. }
  34. const layoutContext = await browser.newContext();
  35. const layoutPage = await layoutContext.newPage();
  36. layoutPage.on('pageerror', error => errors.push(`layout: ${error.message}`));
  37. results.push({ area: 'layout', checks: await checkLayouts(layoutPage, baseUrl, accounts) });
  38. await layoutContext.close();
  39. const sessionContext = await browser.newContext();
  40. const sessionPage = await sessionContext.newPage();
  41. sessionPage.on('pageerror', error => errors.push(`session: ${error.message}`));
  42. results.push({ area: 'session', checks: await checkSessions(sessionPage, baseUrl, accounts) });
  43. await sessionContext.close();
  44. const componentContext = await browser.newContext();
  45. const componentPage = await componentContext.newPage();
  46. componentPage.on('pageerror', error => errors.push(`components: ${error.message}`));
  47. results.push({ area: 'components', checks: await checkComponents(componentPage, baseUrl) });
  48. await componentContext.close();
  49. assert.deepEqual(errors, []);
  50. await mkdir(new URL('../../.work/', import.meta.url), { recursive: true });
  51. await writeFile(new URL('../../.work/frontend-functional.json', import.meta.url), JSON.stringify({ results, errors, requests, screenshots: 0, generatedModelRequests: 0 }, null, 2));
  52. console.log(JSON.stringify({ areas: results.map(item => ({ area: item.area, passed: item.checks.length })), errors, requests, screenshots: 0 }, null, 2));
  53. } finally {
  54. await browser.close();
  55. }
  56. // 商家流程使用自己的真实登录与独立浏览器会话,与前面的消费者流程依次执行。
  57. await import('./merchant-functional.mjs');
  58. await import('./merchant-business.mjs');
  59. await import('./entry-isolation.mjs');
  60. await import('./platform-session.mjs');
  61. await import('./login-rate-limit.mjs');
  62. const merchant = JSON.parse(await readFile(new URL('../../.work/frontend-verification/merchant.json', import.meta.url), 'utf8'));
  63. const business = JSON.parse(await readFile(new URL('../../.work/frontend-verification/merchant-business.json', import.meta.url), 'utf8'));
  64. const extra = {};
  65. for (const name of ['entry-isolation', 'platform-session', 'login-rate-limit']) extra[name] = JSON.parse(await readFile(new URL(`../../.work/frontend-verification/${name}.json`, import.meta.url), 'utf8'));
  66. await writeFile(new URL('../../.work/frontend-verification/full.json', import.meta.url), JSON.stringify({ date: new Date().toISOString(), results, merchant, business, extra, browserErrors: errors, screenshots: 0, generatedModelRequests: 0 }, null, 2));
  67. console.log('全部前端黑盒与功能检查通过。');