#!/usr/bin/env node /** * 移动端成员管理 + 无组织等待页验证(2026-11-10) * * ① 移动端成员管理:独立移动布局(卡片行 + 吸顶搜索 + 底部抽屉),不是 PC 表格 * ② 无组织用户:注入一个 companyId 为空的 me → 应被 orgGuard 拦到 /pending */ import { chromium } from 'playwright'; import { mkdir } from 'node:fs/promises'; import { dirname, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; const here = dirname(fileURLToPath(import.meta.url)); const outDir = resolve(here, '..', 'test-results', 'mobile-org'); const BASE = process.env.BASE_URL || 'http://127.0.0.1:4300'; await mkdir(outDir, { recursive: true }); const browser = await chromium.launch({ channel: 'chrome' }); const results = []; const errors = []; // ---------- ① 移动端成员管理 ---------- { const ctx = await browser.newContext({ viewport: { width: 390, height: 844 }, userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1', hasTouch: true, isMobile: true, }); const page = await ctx.newPage(); page.on('pageerror', (e) => errors.push(String(e))); await page.goto(`${BASE}/mobile/admin/users`, { waitUntil: 'domcontentloaded' }); await page.waitForTimeout(8000); await page.screenshot({ path: `${outDir}/mobile-users.png` }); const isTable = await page.locator('.table, .thead').count(); const hasSearch = await page.locator('.m-search-input').count(); const hasCards = await page.locator('.m-card, app-m-empty').count(); results.push(['移动端不是 PC 表格', isTable === 0, `table=${isTable}`]); results.push(['移动端有吸顶搜索', hasSearch > 0, `search=${hasSearch}`]); results.push(['移动端有内容/空态', hasCards > 0, `nodes=${hasCards}`]); // 打开「添加成员」抽屉 const fab = page.getByRole('button', { name: '添加成员' }); if (await fab.count()) { await fab.first().click(); await page.waitForTimeout(700); const sheet = await page.locator('.m-sheet').count(); await page.screenshot({ path: `${outDir}/mobile-add-sheet.png` }); results.push(['新增走底部抽屉(不是居中小弹窗)', sheet === 1, `sheet=${sheet}`]); } else { results.push(['新增走底部抽屉(不是居中小弹窗)', false, '加号按钮不存在']); } await ctx.close(); } // ---------- ② 无组织用户被拦到等待页 ---------- { const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } }); const page = await ctx.newPage(); page.on('pageerror', (e) => errors.push(String(e))); /* * 先把正常登录态拿出来,再改写 me 去掉 companyId —— * 相当于「账号登录了但还没被组织接纳」。 */ await page.goto(`${BASE}/desktop/chat`, { waitUntil: 'domcontentloaded' }); await page.waitForTimeout(8000); const token = await page.evaluate(() => localStorage.getItem('fmode_session_token')); await page.evaluate(() => { const raw = localStorage.getItem('fmode_me'); if (!raw) return; const me = JSON.parse(raw); delete me.companyId; delete me.companyName; localStorage.setItem('fmode_me', JSON.stringify(me)); }); await page.goto(`${BASE}/desktop/chat`, { waitUntil: 'domcontentloaded' }); await page.waitForTimeout(5000); await page.screenshot({ path: `${outDir}/pending-desktop.png` }); const url = page.url(); const onPending = url.includes('/pending'); results.push(['🔴 无组织用户被拦到等待页', onPending, url]); const shownId = await page.locator('.id-text').innerText().catch(() => ''); results.push(['等待页显示用户 ID', shownId.length > 0, shownId || '(空)']); // 直接访问业务页也应弹回等待页 await page.goto(`${BASE}/desktop/tasks`, { waitUntil: 'domcontentloaded' }); await page.waitForTimeout(3000); results.push(['直接访问业务页仍被挡', page.url().includes('/pending'), page.url()]); await ctx.close(); console.log(`(token 存在: ${!!token})`); } // ---------- 汇总 ---------- let pass = true; console.log('\n=== 结果 ==='); for (const [name, ok, detail] of results) { if (!ok) pass = false; console.log(`${ok ? '✓' : '✗'} ${name} (${detail})`); } if (errors.length) { pass = false; console.log('页面错误:', errors.slice(0, 3).join(' | ')); } console.log(`\n${pass ? '✓ 全部通过' : '✗ 存在失败项'}`); await browser.close(); process.exit(pass ? 0 : 1);