通过 VITE_AUTO_FILL_TEST 环境变量控制,在 .env.test 中启用, 使测试环境构建后登录框也能自动填充测试账号,方便测试人员使用。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
237 lines
7.5 KiB
TypeScript
237 lines
7.5 KiB
TypeScript
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',
|
||
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)
|
||
})
|
||
})
|