/** * 移动端输入框高度复位验证(2026-11-10 用户反馈) * * 现象:发文件 / 发多行文本后,输入框保持撑开的高度不恢复。 * * 用法:`node scripts/verify-mobile-composer.mjs` */ import { chromium } from 'playwright'; import { mkdirSync, writeFileSync } from 'node:fs'; import { resolve } from 'node:path'; import { loadDevLogin } from './dev-login.mjs'; const OUT = resolve('test-results/mobile-composer-20261110'); mkdirSync(OUT, { recursive: true }); const login = loadDevLogin(); if (!login.enabled) throw new Error(`开发态登录不可用:${login.reason}`); const results = []; const check = (n, ok, d = '') => { results.push({ n, ok }); console.log(`${ok ? '✅' : '❌'} ${n}${d ? ` (${d})` : ''}`); }; const FILE = resolve('/tmp', `m-composer-${Date.now()}.md`); writeFileSync(FILE, `# 移动端测试\n\n${Date.now()}\n`); const b = await chromium.launch({ headless: true, channel: 'chromium' }); const ctx = await b.newContext({ viewport: { width: 390, height: 844 }, isMobile: true, hasTouch: true, }); const page = await ctx.newPage(); await page.addInitScript((d) => { localStorage.setItem('fmode_session_token', d.token); localStorage.setItem('fmode_me', JSON.stringify(d.me)); }, login.payload); await page.goto('http://localhost:4200/mobile/', { waitUntil: 'domcontentloaded' }); await page.waitForTimeout(6000); await page.screenshot({ path: `${OUT}/01-mobile-home.png` }); /* 进入一个会话 */ const item = page.locator('.m-session, .session-item, [data-session-id]').first(); if ((await item.count()) > 0) await item.click({ timeout: 8000 }).catch(() => undefined); else await page.getByText(/写一份团队周报/).first().click({ timeout: 8000 }).catch(() => undefined); await page.waitForTimeout(5000); await page.screenshot({ path: `${OUT}/02-chat.png` }); const ta = page.locator('textarea').first(); check('① 找到输入框', (await ta.count()) > 0); if ((await ta.count()) === 0) await finish(); const baseH = await ta.evaluate((el) => el.getBoundingClientRect().height); console.log(` 初始高度: ${baseH.toFixed(1)}px`); /* 输入多行文本,确认它确实会长高(证明自适应是生效的) */ await ta.fill('第一行\n第二行\n第三行\n第四行\n第五行'); await page.waitForTimeout(500); const grownH = await ta.evaluate((el) => el.getBoundingClientRect().height); check('② 多行输入时输入框会撑高', grownH > baseH + 5, `${baseH.toFixed(0)} → ${grownH.toFixed(0)}px`); /* 发送,然后看高度是否回落 */ await ta.press('Enter'); await page.waitForTimeout(3000); const afterH = await ta.evaluate((el) => el.getBoundingClientRect().height); check( '③ 发送后输入框高度恢复', Math.abs(afterH - baseH) < 8, `恢复到 ${afterH.toFixed(0)}px(初始 ${baseH.toFixed(0)}px)`, ); await page.screenshot({ path: `${OUT}/03-after-send.png` }); /* 再验一次附件场景 */ const input = page.locator('input[type="file"]').first(); if ((await input.count()) > 0) { await ta.fill('附件说明\n第二行\n第三行'); await page.waitForTimeout(400); const h2 = await ta.evaluate((el) => el.getBoundingClientRect().height); await input.setInputFiles(FILE); await page.waitForTimeout(1500); await ta.press('Enter'); await page.waitForTimeout(8000); const h3 = await ta.evaluate((el) => el.getBoundingClientRect().height); check( '④ 发附件后高度同样恢复', Math.abs(h3 - baseH) < 8, `撑到 ${h2.toFixed(0)}px → 恢复到 ${h3.toFixed(0)}px`, ); await page.screenshot({ path: `${OUT}/04-after-file-send.png` }); } else { check('④ 发附件后高度同样恢复', false, '未找到 file input'); } await finish(); async function finish() { const passed = results.filter((r) => r.ok).length; console.log(`\n=== 结果:${passed}/${results.length} 通过 ===`); console.log(`截图:${OUT}`); await b.close(); process.exit(passed === results.length ? 0 : 1); }