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