verify-mobile-composer.mjs 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104
  1. /**
  2. * 移动端输入框高度复位验证(2026-11-10 用户反馈)
  3. *
  4. * 现象:发文件 / 发多行文本后,输入框保持撑开的高度不恢复。
  5. *
  6. * 用法:`node scripts/verify-mobile-composer.mjs`
  7. */
  8. import { chromium } from 'playwright';
  9. import { mkdirSync, writeFileSync } from 'node:fs';
  10. import { resolve } from 'node:path';
  11. import { loadDevLogin } from './dev-login.mjs';
  12. const OUT = resolve('test-results/mobile-composer-20261110');
  13. mkdirSync(OUT, { recursive: true });
  14. const login = loadDevLogin();
  15. if (!login.enabled) throw new Error(`开发态登录不可用:${login.reason}`);
  16. const results = [];
  17. const check = (n, ok, d = '') => {
  18. results.push({ n, ok });
  19. console.log(`${ok ? '✅' : '❌'} ${n}${d ? ` (${d})` : ''}`);
  20. };
  21. const FILE = resolve('/tmp', `m-composer-${Date.now()}.md`);
  22. writeFileSync(FILE, `# 移动端测试\n\n${Date.now()}\n`);
  23. const b = await chromium.launch({ headless: true, channel: 'chromium' });
  24. const ctx = await b.newContext({
  25. viewport: { width: 390, height: 844 },
  26. isMobile: true,
  27. hasTouch: true,
  28. });
  29. const page = await ctx.newPage();
  30. await page.addInitScript((d) => {
  31. localStorage.setItem('fmode_session_token', d.token);
  32. localStorage.setItem('fmode_me', JSON.stringify(d.me));
  33. }, login.payload);
  34. await page.goto('http://localhost:4200/mobile/', { waitUntil: 'domcontentloaded' });
  35. await page.waitForTimeout(6000);
  36. await page.screenshot({ path: `${OUT}/01-mobile-home.png` });
  37. /* 进入一个会话 */
  38. const item = page.locator('.m-session, .session-item, [data-session-id]').first();
  39. if ((await item.count()) > 0) await item.click({ timeout: 8000 }).catch(() => undefined);
  40. else await page.getByText(/写一份团队周报/).first().click({ timeout: 8000 }).catch(() => undefined);
  41. await page.waitForTimeout(5000);
  42. await page.screenshot({ path: `${OUT}/02-chat.png` });
  43. const ta = page.locator('textarea').first();
  44. check('① 找到输入框', (await ta.count()) > 0);
  45. if ((await ta.count()) === 0) await finish();
  46. const baseH = await ta.evaluate((el) => el.getBoundingClientRect().height);
  47. console.log(` 初始高度: ${baseH.toFixed(1)}px`);
  48. /* 输入多行文本,确认它确实会长高(证明自适应是生效的) */
  49. await ta.fill('第一行\n第二行\n第三行\n第四行\n第五行');
  50. await page.waitForTimeout(500);
  51. const grownH = await ta.evaluate((el) => el.getBoundingClientRect().height);
  52. check('② 多行输入时输入框会撑高', grownH > baseH + 5, `${baseH.toFixed(0)} → ${grownH.toFixed(0)}px`);
  53. /* 发送,然后看高度是否回落 */
  54. await ta.press('Enter');
  55. await page.waitForTimeout(3000);
  56. const afterH = await ta.evaluate((el) => el.getBoundingClientRect().height);
  57. check(
  58. '③ 发送后输入框高度恢复',
  59. Math.abs(afterH - baseH) < 8,
  60. `恢复到 ${afterH.toFixed(0)}px(初始 ${baseH.toFixed(0)}px)`,
  61. );
  62. await page.screenshot({ path: `${OUT}/03-after-send.png` });
  63. /* 再验一次附件场景 */
  64. const input = page.locator('input[type="file"]').first();
  65. if ((await input.count()) > 0) {
  66. await ta.fill('附件说明\n第二行\n第三行');
  67. await page.waitForTimeout(400);
  68. const h2 = await ta.evaluate((el) => el.getBoundingClientRect().height);
  69. await input.setInputFiles(FILE);
  70. await page.waitForTimeout(1500);
  71. await ta.press('Enter');
  72. await page.waitForTimeout(8000);
  73. const h3 = await ta.evaluate((el) => el.getBoundingClientRect().height);
  74. check(
  75. '④ 发附件后高度同样恢复',
  76. Math.abs(h3 - baseH) < 8,
  77. `撑到 ${h2.toFixed(0)}px → 恢复到 ${h3.toFixed(0)}px`,
  78. );
  79. await page.screenshot({ path: `${OUT}/04-after-file-send.png` });
  80. } else {
  81. check('④ 发附件后高度同样恢复', false, '未找到 file input');
  82. }
  83. await finish();
  84. async function finish() {
  85. const passed = results.filter((r) => r.ok).length;
  86. console.log(`\n=== 结果:${passed}/${results.length} 通过 ===`);
  87. console.log(`截图:${OUT}`);
  88. await b.close();
  89. process.exit(passed === results.length ? 0 : 1);
  90. }