import { test, expect } from '../fixtures/auth.fixture' import { fileURLToPath } from 'url' import path from 'path' /** * P1: postMessage 通信测试 */ const __filename = fileURLToPath(import.meta.url) const __dirname = path.dirname(__filename) const MOCK_H5_PATH = path.resolve(__dirname, '../utils/mock-h5.html') /** 向 iframe 注入 JS 并执行 postMessage */ async function sendMessageFromIframe(page: import('@playwright/test').Page, message: Record) { await page.evaluate((msg) => { const iframe = document.querySelector('iframe') if (iframe?.contentWindow) { iframe.contentWindow.postMessage(msg, '*') } }, message) } /** 模拟 iframe 内部 H5 发送消息(更真实:从 iframe 内部发出) */ async function injectMessageSender(page: import('@playwright/test').Page) { await page.evaluate(() => { const iframe = document.querySelector('iframe') if (iframe?.contentWindow) { // 注入一个可以由测试调用的函数 ;(window as any).__sendFromH5 = (msg: Record) => { // 模拟从 iframe contentWindow 发出 // 由于同源策略,这里直接用 window.parent.postMessage iframe.contentWindow!.postMessage(msg, '*') } } }) } test.describe('postMessage 通信', () => { test.beforeEach(async ({ loggedInPage }) => { // 拦截 token API await loggedInPage.route('**/leai-auth/token', async (route) => { await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ code: 200, data: { token: 'mock_token_for_postmessage_test', orgId: 'gdlib', h5Url: 'http://localhost:3001', phone: '13800001111', }, }), }) }) // 拦截 iframe src 指向的 H5 URL,返回 mock 页面 await loggedInPage.route('**/*token=mock_token_for_postmessage_test**', async (route) => { await route.fulfill({ status: 200, contentType: 'text/html', path: MOCK_H5_PATH, }) }) await loggedInPage.goto('/p/create') // 等待 iframe 出现 await loggedInPage.locator('iframe').first().waitFor({ timeout: 10_000 }) }) test('READY 事件 — 页面正常处理', async ({ loggedInPage }) => { // mock-h5.html 加载后自动发送 READY // 只需验证没有 JS 错误 const consoleErrors: string[] = [] loggedInPage.on('console', (msg) => { if (msg.type() === 'error') consoleErrors.push(msg.text()) }) // 等一小段时间让 READY 消息处理完 await loggedInPage.waitForTimeout(1000) // 不应有未捕获的异常 const criticalErrors = consoleErrors.filter( (e) => !e.includes('favicon') && !e.includes('404'), ) expect(criticalErrors.length).toBe(0) }) test('TOKEN_EXPIRED → 自动刷新 Token', async ({ loggedInPage }) => { let refreshCalled = false await loggedInPage.route('**/leai-auth/refresh-token', async (route) => { refreshCalled = true await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ code: 200, data: { token: 'refreshed_token_new', orgId: 'gdlib', phone: '13800001111', }, }), }) }) // 模拟 H5 发送 TOKEN_EXPIRED await loggedInPage.evaluate(() => { const msg = { source: 'leai-creation', version: 1, type: 'TOKEN_EXPIRED', payload: { messageId: 'msg_test_001' }, } // 从 window 层面直接触发 message 事件(模拟 iframe postMessage) window.dispatchEvent(new MessageEvent('message', { data: msg, origin: '*' })) }) // 等待 refresh-token 被调用 await loggedInPage.waitForTimeout(2000) expect(refreshCalled).toBe(true) }) test('TOKEN_EXPIRED 刷新失败 — 显示错误提示', async ({ loggedInPage }) => { await loggedInPage.route('**/leai-auth/refresh-token', async (route) => { await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ code: 500, message: 'Token刷新失败', }), }) }) await loggedInPage.evaluate(() => { const msg = { source: 'leai-creation', version: 1, type: 'TOKEN_EXPIRED', payload: { messageId: 'msg_test_002' }, } window.dispatchEvent(new MessageEvent('message', { data: msg, origin: '*' })) }) await loggedInPage.waitForTimeout(2000) // 应显示错误提示(ant-design-vue message 组件) const errorToast = loggedInPage.locator('.ant-message-error, .ant-message-custom-content') // 错误提示可能出现也可能不出现,取决于实现 // 这里只验证不会崩溃 }) test('NAVIGATE_BACK → 跳转到作品列表页', async ({ loggedInPage }) => { await loggedInPage.evaluate(() => { const msg = { source: 'leai-creation', version: 1, type: 'NAVIGATE_BACK', payload: {}, } window.dispatchEvent(new MessageEvent('message', { data: msg, origin: '*' })) }) // 验证路由跳转(作品列表页或首页) await loggedInPage.waitForTimeout(1500) const url = loggedInPage.url() // 应跳转离开 /p/create expect(url).not.toContain('/p/create') }) test('CREATION_ERROR → 显示错误消息', async ({ loggedInPage }) => { const msgPromise = loggedInPage.evaluate(() => { return new Promise((resolve) => { // 监听 ant-message 的出现 const observer = new MutationObserver(() => { const errorEl = document.querySelector('.ant-message-error') if (errorEl) { resolve(errorEl.textContent || '') observer.disconnect() } }) observer.observe(document.body, { childList: true, subtree: true }) // 5 秒超时 setTimeout(() => resolve(''), 5000) // 触发错误消息 const msg = { source: 'leai-creation', version: 1, type: 'CREATION_ERROR', payload: { error: 'AI模型处理异常' }, } window.dispatchEvent(new MessageEvent('message', { data: msg, origin: '*' })) }) }) // 只需验证不崩溃即可(message toast 可能需要 antd 渲染) await loggedInPage.waitForTimeout(2000) }) test('忽略非 leai-creation 消息', async ({ loggedInPage }) => { let refreshCalled = false await loggedInPage.route('**/leai-auth/refresh-token', async (route) => { refreshCalled = true await route.fulfill({ status: 200, body: '{}' }) }) await loggedInPage.evaluate(() => { // 发送 source 不同的消息 const msg = { source: 'other-app', type: 'TOKEN_EXPIRED', payload: {} } window.dispatchEvent(new MessageEvent('message', { data: msg, origin: '*' })) }) await loggedInPage.waitForTimeout(1000) expect(refreshCalled).toBe(false) }) test('忽略无 source 字段的消息', async ({ loggedInPage }) => { let refreshCalled = false await loggedInPage.route('**/leai-auth/refresh-token', async (route) => { refreshCalled = true await route.fulfill({ status: 200, body: '{}' }) }) await loggedInPage.evaluate(() => { const msg = { type: 'TOKEN_EXPIRED', payload: {} } window.dispatchEvent(new MessageEvent('message', { data: msg, origin: '*' })) }) await loggedInPage.waitForTimeout(1000) expect(refreshCalled).toBe(false) }) })