library-picturebook-activity/frontend/e2e/leai/postmessage.spec.ts
En 922f650365 feat: 添加乐读派(leai)集成模块及E2E测试基础设施
后端:
- 新增 leai 模块:认证、Webhook、数据同步、定时对账
- 新增 LeaiConfig/RestTemplateConfig/SchedulingConfig 配置
- 新增 FlywayRepairConfig 处理迁移修复
- 新增 V5__leai_integration.sql 迁移脚本
- 扩展所有实体类添加 tenantId 等字段
- 更新 SecurityConfig 放行 leai 公开接口
- 添加 application-test.yml 测试环境配置

前端:
- 添加乐读派认证 API (public.ts)
- 优化 Generating.vue 生成页
- 添加 Playwright E2E 测试配置及依赖
- 添加测试 fixtures、utils、mock-h5.html
- 添加 leai 模块完整 E2E 测试套件

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-07 21:52:32 +08:00

238 lines
7.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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)
})
})