browser-verify.mjs 4.5 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889
  1. #!/usr/bin/env node
  2. /**
  3. * scripts/browser-verify.mjs —— 真实浏览器验收(本仓纪律:不能只 curl)。
  4. *
  5. * 覆盖:组件注册 / property 渲染 / Shadow DOM 样式隔离 / 事件穿透 composed /
  6. * core 计算结果 / Lit 页面宿主 / 纯 HTML 宿主 / 重复 import 幂等 / 主题变量。
  7. *
  8. * 前置:
  9. * 1) 站点根 = examples/(页面里 <base href="/core-wc/page/">)
  10. * python3 -m http.server 4000 (在 examples/ 下)
  11. * 2) 本地无 Repose 时,用 npm 装的 lit 并把两个 html 的 importmap 换成本地 node_modules
  12. * (见 ../README.md「方式 B」)
  13. * 3) npm i -D playwright && npx playwright install chromium
  14. *
  15. * 用法:node scripts/browser-verify.mjs [baseURL]
  16. */
  17. import { chromium } from 'playwright';
  18. const BASE = process.argv[2] || 'http://127.0.0.1:4000/core-wc';
  19. let fail = 0;
  20. const ok = (n, c, d = '') => { console.log(`${c ? '✓' : '✗'} ${n}${!c && d ? `\n ${d}` : ''}`); if (!c) fail++; };
  21. // favicon.ico 404 是无 favicon 时的必然噪音,不算失败
  22. const isNoise = (m) => /favicon/i.test(m.location()?.url || '') || /favicon/i.test(m.text());
  23. const browser = await chromium.launch({
  24. executablePath: process.env.CHROME_PATH || undefined,
  25. args: ['--no-sandbox', '--disable-dev-shm-usage'],
  26. });
  27. // ---------- 1) 纯 HTML 宿主(无框架)----------
  28. const page = await browser.newPage();
  29. const errors = [];
  30. page.on('console', (m) => { if (m.type() === 'error' && !isNoise(m)) errors.push(m.text()); });
  31. page.on('pageerror', (e) => errors.push('pageerror: ' + e.message));
  32. await page.goto(`${BASE}/wc/plain.html`, { waitUntil: 'networkidle' });
  33. ok('plain.html 无控制台报错', errors.length === 0, errors.join('\n '));
  34. ok('quote-calc 已注册', await page.evaluate(() => !!customElements.get('quote-calc')));
  35. const lines = await page.locator('quote-calc').evaluate((el) => el.shadowRoot.querySelectorAll('table[part=lines] tr').length);
  36. ok('property 传值渲染出行(2 行)', lines === 2, `实际 ${lines}`);
  37. const iso = await page.evaluate(() => {
  38. const s = document.createElement('style'); s.textContent = 'button{background:red !important}';
  39. document.head.appendChild(s);
  40. return getComputedStyle(document.querySelector('quote-calc').shadowRoot.querySelector('button')).backgroundColor;
  41. });
  42. ok('Shadow DOM 隔离(外部样式改不动内部按钮)', iso !== 'rgb(255, 0, 0)', `实际 ${iso}`);
  43. const detail = await page.evaluate(async () => {
  44. const el = document.querySelector('quote-calc');
  45. return new Promise((res) => { el.addEventListener('quote-done', (e) => res(e.detail)); el.shadowRoot.querySelector('button').click(); });
  46. });
  47. ok('quote-done 事件穿透 Shadow DOM 且带 detail', !!detail?.result, JSON.stringify(detail));
  48. ok('core 计算结果正确(82 小计 / 8.2 折扣)', detail?.result?.subtotal === 82 && detail?.result?.discount === 8.2, JSON.stringify(detail?.result));
  49. // ---------- 2) Lit 页面宿主 ----------
  50. const errs2 = []; const page2 = await browser.newPage();
  51. page2.on('console', (m) => { if (m.type() === 'error' && !isNoise(m)) errs2.push(m.text()); });
  52. page2.on('pageerror', (e) => errs2.push('pageerror: ' + e.message));
  53. await page2.goto(`${BASE}/page/index.html`, { waitUntil: 'networkidle' });
  54. ok('page/index.html 无控制台报错', errs2.length === 0, errs2.join('\n '));
  55. ok('Lit 页面里 app-root + quote-calc 均已注册',
  56. await page2.evaluate(() => !!customElements.get('app-root') && !!customElements.get('quote-calc')));
  57. await page2.locator('quote-calc').evaluate((el) => el.shadowRoot.querySelector('button').click());
  58. await page2.waitForTimeout(400);
  59. const log = await page2.locator('app-root').evaluate((el) => el.shadowRoot.querySelector('pre').textContent);
  60. ok('Lit 页面收到事件并写入日志', log.includes('quote-done'), log.slice(0, 120));
  61. // ---------- 3) 幂等 ----------
  62. const dup = await page.evaluate(async () => {
  63. try { await import('/wc/index.js'); await import('/wc/index.js'); return 'ok'; }
  64. catch (e) { return 'err: ' + e.message; }
  65. });
  66. ok('重复 import 组件库不抛 define 冲突', dup === 'ok', dup);
  67. // ---------- 4) 主题变量 ----------
  68. const themed = await page.evaluate(() => {
  69. const el = document.querySelector('quote-calc');
  70. el.style.setProperty('--qc-accent', 'rgb(47, 133, 90)');
  71. return getComputedStyle(el.shadowRoot.querySelector('button')).backgroundColor;
  72. });
  73. ok('CSS 变量 --qc-accent 可穿透主题', themed === 'rgb(47, 133, 90)', `实际 ${themed}`);
  74. await browser.close();
  75. console.log(fail === 0 ? '\nBROWSER OK' : `\nBROWSER FAILED(${fail})`);
  76. process.exit(fail ? 1 : 0);