verify-mobile-org.mjs 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115
  1. #!/usr/bin/env node
  2. /**
  3. * 移动端成员管理 + 无组织等待页验证(2026-11-10)
  4. *
  5. * ① 移动端成员管理:独立移动布局(卡片行 + 吸顶搜索 + 底部抽屉),不是 PC 表格
  6. * ② 无组织用户:注入一个 companyId 为空的 me → 应被 orgGuard 拦到 /pending
  7. */
  8. import { chromium } from 'playwright';
  9. import { mkdir } from 'node:fs/promises';
  10. import { dirname, resolve } from 'node:path';
  11. import { fileURLToPath } from 'node:url';
  12. const here = dirname(fileURLToPath(import.meta.url));
  13. const outDir = resolve(here, '..', 'test-results', 'mobile-org');
  14. const BASE = process.env.BASE_URL || 'http://127.0.0.1:4300';
  15. await mkdir(outDir, { recursive: true });
  16. const browser = await chromium.launch({ channel: 'chrome' });
  17. const results = [];
  18. const errors = [];
  19. // ---------- ① 移动端成员管理 ----------
  20. {
  21. const ctx = await browser.newContext({
  22. viewport: { width: 390, height: 844 },
  23. userAgent:
  24. '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',
  25. hasTouch: true,
  26. isMobile: true,
  27. });
  28. const page = await ctx.newPage();
  29. page.on('pageerror', (e) => errors.push(String(e)));
  30. await page.goto(`${BASE}/mobile/admin/users`, { waitUntil: 'domcontentloaded' });
  31. await page.waitForTimeout(8000);
  32. await page.screenshot({ path: `${outDir}/mobile-users.png` });
  33. const isTable = await page.locator('.table, .thead').count();
  34. const hasSearch = await page.locator('.m-search-input').count();
  35. const hasCards = await page.locator('.m-card, app-m-empty').count();
  36. results.push(['移动端不是 PC 表格', isTable === 0, `table=${isTable}`]);
  37. results.push(['移动端有吸顶搜索', hasSearch > 0, `search=${hasSearch}`]);
  38. results.push(['移动端有内容/空态', hasCards > 0, `nodes=${hasCards}`]);
  39. // 打开「添加成员」抽屉
  40. const fab = page.getByRole('button', { name: '添加成员' });
  41. if (await fab.count()) {
  42. await fab.first().click();
  43. await page.waitForTimeout(700);
  44. const sheet = await page.locator('.m-sheet').count();
  45. await page.screenshot({ path: `${outDir}/mobile-add-sheet.png` });
  46. results.push(['新增走底部抽屉(不是居中小弹窗)', sheet === 1, `sheet=${sheet}`]);
  47. } else {
  48. results.push(['新增走底部抽屉(不是居中小弹窗)', false, '加号按钮不存在']);
  49. }
  50. await ctx.close();
  51. }
  52. // ---------- ② 无组织用户被拦到等待页 ----------
  53. {
  54. const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
  55. const page = await ctx.newPage();
  56. page.on('pageerror', (e) => errors.push(String(e)));
  57. /*
  58. * 先把正常登录态拿出来,再改写 me 去掉 companyId ——
  59. * 相当于「账号登录了但还没被组织接纳」。
  60. */
  61. await page.goto(`${BASE}/desktop/chat`, { waitUntil: 'domcontentloaded' });
  62. await page.waitForTimeout(8000);
  63. const token = await page.evaluate(() => localStorage.getItem('fmode_session_token'));
  64. await page.evaluate(() => {
  65. const raw = localStorage.getItem('fmode_me');
  66. if (!raw) return;
  67. const me = JSON.parse(raw);
  68. delete me.companyId;
  69. delete me.companyName;
  70. localStorage.setItem('fmode_me', JSON.stringify(me));
  71. });
  72. await page.goto(`${BASE}/desktop/chat`, { waitUntil: 'domcontentloaded' });
  73. await page.waitForTimeout(5000);
  74. await page.screenshot({ path: `${outDir}/pending-desktop.png` });
  75. const url = page.url();
  76. const onPending = url.includes('/pending');
  77. results.push(['🔴 无组织用户被拦到等待页', onPending, url]);
  78. const shownId = await page.locator('.id-text').innerText().catch(() => '');
  79. results.push(['等待页显示用户 ID', shownId.length > 0, shownId || '(空)']);
  80. // 直接访问业务页也应弹回等待页
  81. await page.goto(`${BASE}/desktop/tasks`, { waitUntil: 'domcontentloaded' });
  82. await page.waitForTimeout(3000);
  83. results.push(['直接访问业务页仍被挡', page.url().includes('/pending'), page.url()]);
  84. await ctx.close();
  85. console.log(`(token 存在: ${!!token})`);
  86. }
  87. // ---------- 汇总 ----------
  88. let pass = true;
  89. console.log('\n=== 结果 ===');
  90. for (const [name, ok, detail] of results) {
  91. if (!ok) pass = false;
  92. console.log(`${ok ? '✓' : '✗'} ${name} (${detail})`);
  93. }
  94. if (errors.length) {
  95. pass = false;
  96. console.log('页面错误:', errors.slice(0, 3).join(' | '));
  97. }
  98. console.log(`\n${pass ? '✓ 全部通过' : '✗ 存在失败项'}`);
  99. await browser.close();
  100. process.exit(pass ? 0 : 1);