verify.mjs 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164
  1. /* 验证脚本:逐路由截图 + 断言(控制台 0 error / 无横向溢出 / 配色与暗色能切换)
  2. 用法:node verify.mjs (产物在 ./shots/) */
  3. import { chromium } from 'playwright';
  4. import fs from 'fs';
  5. import path from 'path';
  6. const dir = path.dirname(new URL(import.meta.url).pathname.replace(/^\/([A-Za-z]:)/, '$1'));
  7. const out = path.join(dir, 'shots');
  8. fs.mkdirSync(out, { recursive: true });
  9. const ROUTES = [
  10. ['01-messages', '#/messages'],
  11. ['04-contacts', '#/contacts'], ['05-agent', '#/agent/a-nova'], ['06-projects', '#/projects'],
  12. ['07-project-detail', '#/projects/pr-1'], ['08-tasks', '#/tasks'], ['09-task-blocked', '#/tasks/t-1'],
  13. ['10-deliverables', '#/deliverables'], ['11-events', '#/events'], ['12-workbench', '#/workbench'],
  14. ['13-mine', '#/mine'],
  15. ];
  16. const browser = await chromium.launch({ channel: 'msedge' });
  17. const page = await browser.newPage({ viewport: { width: 390, height: 844 }, deviceScaleFactor: 2 });
  18. const errors = [];
  19. page.on('pageerror', e => errors.push('pageerror: ' + e.message));
  20. page.on('console', m => { if (m.type() === 'error') errors.push('console: ' + m.text()); });
  21. const url = 'file:///' + path.join(dir, 'index.html').replace(/\\/g, '/');
  22. await page.goto(url + '#/messages', { waitUntil: 'load' });
  23. await page.waitForTimeout(400);
  24. const report = { routes: [], overflow: [], palettes: {} };
  25. const wx = {};
  26. // ---- 登录流程:未登录先落到 #/login,手机号 → 验证码 → 同意 → 登录 ----
  27. await page.evaluate(() => localStorage.clear());
  28. await page.goto(url, { waitUntil: 'load' });
  29. await page.waitForTimeout(300);
  30. wx.loginRedirect = await page.evaluate(() => location.hash);
  31. await page.screenshot({ path: path.join(out, '00-login-phone.png') });
  32. wx.loginBtnDisabledBeforePhone = await page.locator('[data-act="login-next"][data-step="code"]').isDisabled();
  33. await page.locator('[data-act="login-phone"]').fill('13800000000');
  34. await page.waitForTimeout(150);
  35. wx.loginPhoneValid = await page.locator('[data-act="login-next"][data-step="code"]').isEnabled();
  36. await page.locator('[data-act="login-next"][data-step="code"]').click();
  37. await page.waitForTimeout(200);
  38. await page.screenshot({ path: path.join(out, '00-login-code.png') });
  39. await page.locator('[data-act="login-code"]').fill('123456');
  40. await page.waitForTimeout(150);
  41. wx.loginCodeValid = await page.locator('[data-act="login-submit"]').isEnabled();
  42. await page.locator('[data-act="login-agree"]').click();
  43. await page.waitForTimeout(150);
  44. await page.locator('[data-act="login-submit"]').click();
  45. await page.waitForTimeout(400);
  46. wx.afterLogin = await page.evaluate(() => location.hash);
  47. // ---- 底部输入坞不随内容滚动 ----
  48. await page.evaluate(() => { location.hash = '#/chat/s-1'; });
  49. await page.waitForTimeout(250);
  50. const posA = await page.locator('.dock').boundingBox();
  51. await page.evaluate(() => { const b = document.querySelector('.page-body'); b.scrollTop = b.scrollHeight; });
  52. await page.waitForTimeout(200);
  53. const posB = await page.locator('.dock').boundingBox();
  54. wx.dockFixedDuringScroll = !!posA && !!posB && Math.abs(posA.y - posB.y) < 1;
  55. wx.dockInsideViewport = await page.evaluate(() => {
  56. const el = document.querySelector('.dock');
  57. if (!el) return true; // 该页没有输入坞
  58. return Math.round(el.getBoundingClientRect().bottom) <= window.innerHeight;
  59. });
  60. // 有 TabBar 的页面:输入坞底边不得压住 TabBar
  61. await page.evaluate(() => { location.hash = '#/messages'; });
  62. await page.waitForTimeout(250);
  63. wx.homeDockAboveTabbar = await page.evaluate(() => {
  64. const d = document.querySelector('.dock');
  65. const t = document.querySelector('.tabbar');
  66. if (!d || !t) return true;
  67. return getComputedStyle(t).display === 'flex' && d.getBoundingClientRect().bottom <= t.getBoundingClientRect().top + 1;
  68. });
  69. for (const [name, hash] of ROUTES) {
  70. await page.evaluate(h => { location.hash = h; }, hash);
  71. await page.waitForTimeout(180);
  72. await page.screenshot({ path: path.join(out, name + '.png') });
  73. const info = await page.evaluate(() => ({
  74. hasView: !!document.querySelector('.view.active .page'),
  75. hOverflow: document.documentElement.scrollWidth > window.innerWidth + 1,
  76. title: (document.querySelector('.navbar .title') || {}).textContent?.trim() || '',
  77. }));
  78. if (info.hOverflow) report.overflow.push(name);
  79. report.routes.push([name, info.hasView, info.title]);
  80. }
  81. // 抽屉 + 会话内成果抽屉 + 预览层
  82. await page.evaluate(() => { location.hash = '#/messages'; });
  83. await page.waitForTimeout(150);
  84. await page.locator('[data-act="drawer"]').click();
  85. await page.waitForTimeout(350);
  86. await page.screenshot({ path: path.join(out, '14-drawer.png') });
  87. const drawerOk = await page.locator('.drawer.show').count();
  88. await page.evaluate(() => { document.querySelectorAll('#layers > div').forEach(w => w.remove()); document.querySelectorAll('#layers > .preview').forEach(x => x.remove()); });
  89. await page.waitForTimeout(300);
  90. await page.evaluate(() => { location.hash = '#/chat/s-2'; });
  91. await page.waitForTimeout(150);
  92. await page.locator('[data-act="sheet-chat"]').click();
  93. await page.waitForTimeout(350);
  94. await page.screenshot({ path: path.join(out, '15-chat-sheet.png') });
  95. await page.locator('[data-tab="deliver"]').click();
  96. await page.waitForTimeout(150);
  97. await page.locator('[data-sheet-body] .row').first().click();
  98. await page.waitForTimeout(300);
  99. await page.screenshot({ path: path.join(out, '16-preview.png') });
  100. const previewOk = await page.locator('.preview').count();
  101. await page.locator('.preview [data-act="close"]').click();
  102. await page.waitForTimeout(300);
  103. // 配色 × 明暗(先回首页,避免在预览层级上操作)
  104. await page.evaluate(() => { location.hash = '#/messages'; document.querySelectorAll('#layers > *').forEach(x => x.remove()); });
  105. await page.waitForTimeout(300);
  106. for (const p of ['platinum', 'ice', 'clay', 'slate']) {
  107. for (const dark of [false, true]) {
  108. await page.evaluate(([p, dark]) => {
  109. document.documentElement.dataset.palette = p;
  110. document.documentElement.classList.toggle('dark', dark);
  111. }, [p, dark]);
  112. const bg = await page.evaluate(() => getComputedStyle(document.body).backgroundColor);
  113. report.palettes[p + (dark ? '-dark' : '-light')] = bg;
  114. }
  115. await page.evaluate(p => {
  116. document.documentElement.dataset.palette = p;
  117. document.documentElement.classList.remove('dark');
  118. location.hash = '#/messages';
  119. }, p);
  120. await page.waitForTimeout(150);
  121. await page.screenshot({ path: path.join(out, `17-palette-${p}.png`) });
  122. }
  123. // 用页面自身入口切一次,验证持久化路径
  124. await page.evaluate(() => { location.hash = '#/mine'; });
  125. await page.waitForTimeout(150);
  126. await page.locator('[data-act="palette"][data-key="ice"]').click();
  127. await page.waitForTimeout(150);
  128. report.paletteAfterClick = await page.evaluate(() => document.documentElement.dataset.palette);
  129. await page.locator('[data-act="toggle-dark"]').click();
  130. await page.waitForTimeout(150);
  131. report.darkAfterClick = await page.evaluate(() => document.documentElement.classList.contains('dark'));
  132. await page.evaluate(() => { localStorage.clear(); });
  133. // 设置页截图(深色 + ice)
  134. await page.evaluate(() => { document.documentElement.dataset.palette = 'ice'; document.documentElement.classList.add('dark'); location.hash = '#/mine'; });
  135. await page.waitForTimeout(200);
  136. await page.screenshot({ path: path.join(out, '18-mine-dark-ice.png') });
  137. const distinct = new Set(Object.values(report.palettes)).size;
  138. console.log(JSON.stringify({
  139. ...wx,
  140. routes: report.routes,
  141. overflow: report.overflow,
  142. drawerOk, previewOk,
  143. paletteColors: report.palettes,
  144. distinctPaletteColors: distinct,
  145. paletteAfterClick: report.paletteAfterClick,
  146. darkAfterClick: report.darkAfterClick,
  147. errors,
  148. }, null, 1));
  149. await browser.close();
  150. process.exit(errors.length || report.overflow.length || distinct < 8 ? 1 : 0);