library-picturebook-activity/frontend/e2e/leai/postmessage.spec.ts

238 lines
7.5 KiB
TypeScript
Raw Normal View History

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<string, unknown>) {
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<string, unknown>) => {
// 模拟从 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<string>((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)
})
})