smoke-groups.mjs 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118
  1. #!/usr/bin/env node
  2. /**
  3. * 工作组端到端冒烟 —— 11 项断言
  4. *
  5. * 用法:npm start(另开终端)→ npm run smoke:groups
  6. *
  7. * 覆盖:群列表 / 进群 / 消息渲染 / 发送者名 / 角色标记 /
  8. * @ 自动补全 / @ 筛选 / @ 回填 / 上下文开关 / 新建入口 / 角色编辑器
  9. *
  10. * 🔴 需要真实登录态与真实数据(从平台取 sessionToken)。
  11. * 这是**联调冒烟**,不是单元测试(单测见 src/app/core/*.spec.ts)。
  12. */
  13. // 端到端完整回归:把所有群聊功能串一遍
  14. import { chromium } from 'playwright';
  15. const H = { 'X-Parse-Application-Id': 'ncloudmaster', 'X-Parse-Master-Key': 'SnkK12*&sunq2#@20!' };
  16. const q = await fetch('https://server.fmode.cn/parse/classes/_Session/SGF24QCsOo', { headers: H }).then(r => r.json());
  17. const results = [];
  18. function check(name, ok, detail = '') {
  19. results.push({ name, ok, detail });
  20. console.log(`${ok ? '✅' : '❌'} ${name}${detail ? ' — ' + detail : ''}`);
  21. }
  22. const b = await chromium.launch({ executablePath: 'C:/Program Files/Google/Chrome/Application/chrome.exe' });
  23. const ctx = await b.newContext({ viewport: { width: 1500, height: 900 } });
  24. await ctx.addInitScript(([t, m]) => {
  25. localStorage.setItem('fmode_session_token', t);
  26. localStorage.setItem('fmode_me', m);
  27. }, [q.sessionToken, '{"id":"xhVRjTMwQH","name":"用户9836","mobile":"19170119836","companyId":"E4KpGvTEto"}']);
  28. const page = await ctx.newPage();
  29. const errs = [];
  30. page.on('pageerror', e => errs.push(e.message.slice(0, 100)));
  31. await page.goto('http://localhost:4200/desktop/groups', { waitUntil: 'domcontentloaded' });
  32. await page.waitForTimeout(15000);
  33. // 1. 群列表加载
  34. const groupCount = await page.locator('.group-row').count();
  35. check('群列表加载', groupCount > 0, `${groupCount} 个群`);
  36. // 2. 进入群
  37. await page.locator('.group-row').first().click();
  38. await page.waitForTimeout(4000);
  39. const inGroup = await page.evaluate(() => !!document.querySelector('.composer-row'));
  40. check('进入群会话', inGroup);
  41. // 3. 消息渲染 + 发送者名
  42. const msgStats = await page.evaluate(() => ({
  43. bubbles: document.querySelectorAll('app-message-bubble').length,
  44. senders: [...document.querySelectorAll('.sender-name')].map(e => e.textContent.trim()).slice(0, 3),
  45. }));
  46. check('消息渲染', msgStats.bubbles > 0, `${msgStats.bubbles} 条`);
  47. check('发送者名显示', msgStats.senders.length > 0, msgStats.senders.join('/'));
  48. // 4. 成员栏 + 角色
  49. const members = await page.evaluate(() =>
  50. [...document.querySelectorAll('.member-row')].map(e => ({
  51. name: e.querySelector('strong')?.textContent.trim(),
  52. role: e.querySelector('.role-tag')?.textContent.trim() || '',
  53. })),
  54. );
  55. check('成员栏 + 角色标记', members.length > 0, JSON.stringify(members));
  56. // 5. @ 边打边筛
  57. const ta = page.locator('textarea.input');
  58. await ta.fill('测试 @');
  59. await page.waitForTimeout(800);
  60. const acCount = await page.locator('.suggest-item').count();
  61. check('@ 自动补全', acCount > 0, `${acCount} 个候选`);
  62. await ta.fill('测试 @lj-t');
  63. await page.waitForTimeout(800);
  64. const filtered = await page.locator('.suggest-item').count();
  65. check('@ 筛选', filtered === 1, `${filtered} 个`);
  66. await page.locator('.suggest-item').first().click();
  67. await page.waitForTimeout(800);
  68. const filled = await page.evaluate(() => document.querySelector('textarea.input')?.value || '');
  69. check('@ 回填', filled.includes('@lj-test'), JSON.stringify(filled));
  70. // 6. 上下文开关
  71. // 🔴 开关在「我的」页,不在群聊页 —— 2026-10-28 起上下文策略改为「只给组长」,
  72. // 群聊页不再保留开关;设置入口统一收在 我的 → 工作组协作。
  73. await page.goto('http://localhost:4200/desktop/mine', { waitUntil: 'domcontentloaded' });
  74. await page.waitForTimeout(6000);
  75. const ctxToggle = await page.evaluate(() => {
  76. const el = document.querySelector('.ctx-switch input');
  77. return el ? { exists: true, checked: el.checked } : { exists: false };
  78. });
  79. check('上下文开关(我的页 · 默认开启)', ctxToggle.exists && ctxToggle.checked === true, JSON.stringify(ctxToggle));
  80. // 回到群页继续后续断言
  81. await page.goto('http://localhost:4200/desktop/groups', { waitUntil: 'domcontentloaded' });
  82. await page.waitForTimeout(8000);
  83. await page.locator('.group-row').first().click().catch(() => {});
  84. await page.waitForTimeout(4000);
  85. // 7. 新建工作组入口
  86. await ta.fill('');
  87. const createBtn = await page.locator('button[aria-label="新建工作组"]').count();
  88. check('新建工作组入口', createBtn > 0);
  89. // 8. 角色编辑
  90. await page.locator('.member-row').first().click();
  91. await page.waitForTimeout(1200);
  92. const roleEditor = await page.evaluate(() => !!document.querySelector('.role-editor'));
  93. check('角色编辑器', roleEditor);
  94. await page.keyboard.press('Escape');
  95. await page.screenshot({ path: 'E:/tmp/e2e-regression.png' });
  96. // 汇总
  97. const passed = results.filter(r => r.ok).length;
  98. console.log(`\n=== 回归结果: ${passed}/${results.length} 通过 ===`);
  99. if (errs.length) console.log('页面错误:', errs.slice(0, 3).join(' | '));
  100. await b.close();