- 移除 LeaiTokenVO.h5Url 字段、LeaiConfig.h5Url 配置及 yml 中的 h5-url - 删除 LeaiAuthController.authRedirect() 方法和 LeaiAuthRedirectDTO - 移除前端 authRedirectUrl 状态及 WelcomeView 企业认证按钮死代码 - 修复 LeaiProxyController 返回 text/plain 导致前端无法解析 JSON 的问题 (改用 ResponseEntity<String> + application/json Content-Type) - 修复前端 aicreate 所有视图组件中 res.data 双重取值问题 (publicApi 拦截器已自动解包,无需再取 .data) - 同步更新 E2E 测试 mock 数据移除 h5Url Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
227 lines
7.3 KiB
TypeScript
227 lines
7.3 KiB
TypeScript
import { test, expect } from '../fixtures/leai.fixture'
|
||
import { randomWorkId } from '../fixtures/leai.fixture'
|
||
import { fileURLToPath } from 'url'
|
||
import path from 'path'
|
||
|
||
/**
|
||
* P2: 端到端完整流程测试
|
||
*/
|
||
|
||
const __filename = fileURLToPath(import.meta.url)
|
||
const __dirname = path.dirname(__filename)
|
||
|
||
const MOCK_H5_PATH = path.resolve(__dirname, '../utils/mock-h5.html')
|
||
|
||
test.describe('端到端:创作完整流程', () => {
|
||
|
||
test('E2E-1: iframe 创作主流程', async ({ loggedInPage, sendWebhook }) => {
|
||
// ── 步骤 1: 拦截 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: 'e2e_test_token',
|
||
orgId: 'gdlib',
|
||
phone: '13800001111',
|
||
},
|
||
}),
|
||
})
|
||
})
|
||
await loggedInPage.route('http://192.168.1.120:3001/**', async (route) => {
|
||
await route.fulfill({ status: 200, contentType: 'text/html', path: MOCK_H5_PATH })
|
||
})
|
||
|
||
// ── 步骤 2: 访问创作页 ──
|
||
await loggedInPage.goto('/p/create')
|
||
const iframe = loggedInPage.locator('iframe').first()
|
||
await expect(iframe).toBeVisible({ timeout: 10_000 })
|
||
|
||
// ── 步骤 3: 模拟 Webhook status=1 (PENDING) ──
|
||
const workId = randomWorkId()
|
||
const result1 = await sendWebhook({
|
||
event: 'work.status_changed',
|
||
data: { work_id: workId, status: 1, phone: '13800001111', title: 'E2E测试绘本' },
|
||
})
|
||
expect(result1.status).toBe(200)
|
||
|
||
// ── 步骤 4: 模拟 Webhook status=2 (PROCESSING) ──
|
||
const result2 = await sendWebhook({
|
||
event: 'work.progress',
|
||
data: { work_id: workId, status: 2, progress: 50, progressMessage: '正在绘制插画...' },
|
||
})
|
||
expect(result2.status).toBe(200)
|
||
|
||
// ── 步骤 5: 模拟 Webhook status=3 (COMPLETED) ──
|
||
const result3 = await sendWebhook({
|
||
event: 'work.status_changed',
|
||
data: {
|
||
work_id: workId,
|
||
status: 3,
|
||
title: 'E2E测试绘本',
|
||
pageList: [
|
||
{ imageUrl: 'https://cdn.example.com/e2e/page1.png', text: '第一页' },
|
||
{ imageUrl: 'https://cdn.example.com/e2e/page2.png', text: '第二页' },
|
||
],
|
||
},
|
||
})
|
||
expect(result3.status).toBe(200)
|
||
|
||
// ── 步骤 6: 模拟 Webhook status=5 (DUBBED) ──
|
||
const result5 = await sendWebhook({
|
||
event: 'work.status_changed',
|
||
data: {
|
||
work_id: workId,
|
||
status: 5,
|
||
title: 'E2E测试绘本',
|
||
author: 'E2E测试作者',
|
||
pageList: [
|
||
{ imageUrl: 'https://cdn.example.com/e2e/page1.png', text: '第一页', audioUrl: 'https://cdn.example.com/e2e/audio1.mp3' },
|
||
{ imageUrl: 'https://cdn.example.com/e2e/page2.png', text: '第二页', audioUrl: 'https://cdn.example.com/e2e/audio2.mp3' },
|
||
],
|
||
},
|
||
})
|
||
expect(result5.status).toBe(200)
|
||
|
||
// 全流程无报错即通过
|
||
})
|
||
|
||
test('E2E-2: Token 过期自动刷新', async ({ loggedInPage }) => {
|
||
let tokenCallCount = 0
|
||
let refreshCallCount = 0
|
||
|
||
await loggedInPage.route('**/leai-auth/token', async (route) => {
|
||
tokenCallCount++
|
||
await route.fulfill({
|
||
status: 200,
|
||
contentType: 'application/json',
|
||
body: JSON.stringify({
|
||
code: 200,
|
||
data: { token: 'initial_token', orgId: 'gdlib', phone: '13800001111' },
|
||
}),
|
||
})
|
||
})
|
||
|
||
await loggedInPage.route('**/leai-auth/refresh-token', async (route) => {
|
||
refreshCallCount++
|
||
await route.fulfill({
|
||
status: 200,
|
||
contentType: 'application/json',
|
||
body: JSON.stringify({
|
||
code: 200,
|
||
data: { token: 'refreshed_token', orgId: 'gdlib', phone: '13800001111' },
|
||
}),
|
||
})
|
||
})
|
||
|
||
await loggedInPage.route('http://192.168.1.120:3001/**', async (route) => {
|
||
await route.fulfill({ status: 200, contentType: 'text/html', path: MOCK_H5_PATH })
|
||
})
|
||
|
||
await loggedInPage.goto('/p/create')
|
||
const iframe = loggedInPage.locator('iframe').first()
|
||
await expect(iframe).toBeVisible({ timeout: 10_000 })
|
||
expect(tokenCallCount).toBe(1)
|
||
|
||
// 模拟 H5 发送 TOKEN_EXPIRED
|
||
await loggedInPage.evaluate(() => {
|
||
window.dispatchEvent(new MessageEvent('message', {
|
||
data: { source: 'leai-creation', version: 1, type: 'TOKEN_EXPIRED', payload: { messageId: 'm1' } },
|
||
origin: '*',
|
||
}))
|
||
})
|
||
|
||
await loggedInPage.waitForTimeout(2000)
|
||
expect(refreshCallCount).toBe(1)
|
||
|
||
// iframe 应继续正常显示
|
||
await expect(iframe).toBeVisible()
|
||
})
|
||
|
||
test('E2E-3: Webhook 幂等 + 状态不回退', async ({ sendWebhook }) => {
|
||
const workId = randomWorkId()
|
||
|
||
// 发送 status=1 (PENDING)
|
||
const r1 = await sendWebhook({
|
||
event: 'work.status_changed',
|
||
data: { work_id: workId, status: 1, phone: '13800001111' },
|
||
})
|
||
expect(r1.status).toBe(200)
|
||
|
||
// 发送 status=3 (COMPLETED)
|
||
const r3 = await sendWebhook({
|
||
event: 'work.status_changed',
|
||
data: { work_id: workId, status: 3, title: '幂等测试' },
|
||
})
|
||
expect(r3.status).toBe(200)
|
||
|
||
// 发送旧状态 status=2 (PROCESSING) — 应被忽略
|
||
const r2 = await sendWebhook({
|
||
event: 'work.status_changed',
|
||
data: { work_id: workId, status: 2, progress: 80 },
|
||
})
|
||
expect(r2.status).toBe(200)
|
||
// V4.0 规则:status=2 <= status=3,忽略
|
||
|
||
// 发送 status=-1 (FAILED) — 强制覆盖
|
||
const rf = await sendWebhook({
|
||
event: 'work.status_changed',
|
||
data: { work_id: workId, status: -1, failReason: '测试强制失败' },
|
||
})
|
||
expect(rf.status).toBe(200)
|
||
// V4.0 规则:FAILED 强制更新
|
||
})
|
||
|
||
test('E2E-4: 创作失败 → 重试流程', async ({ loggedInPage, sendWebhook }) => {
|
||
const failedWorkId = randomWorkId()
|
||
|
||
// 模拟 Webhook 推送失败
|
||
const r1 = await sendWebhook({
|
||
event: 'work.status_changed',
|
||
data: { work_id: failedWorkId, status: 1, phone: '13800001111' },
|
||
})
|
||
expect(r1.status).toBe(200)
|
||
|
||
const rf = await sendWebhook({
|
||
event: 'work.status_changed',
|
||
data: { work_id: failedWorkId, status: -1, failReason: 'AI处理超时' },
|
||
})
|
||
expect(rf.status).toBe(200)
|
||
|
||
// 用户回到创作页重新开始
|
||
await loggedInPage.route('**/leai-auth/token', async (route) => {
|
||
await route.fulfill({
|
||
status: 200,
|
||
contentType: 'application/json',
|
||
body: JSON.stringify({
|
||
code: 200,
|
||
data: { token: 'retry_token', orgId: 'gdlib', phone: '13800001111' },
|
||
}),
|
||
})
|
||
})
|
||
await loggedInPage.route('http://192.168.1.120:3001/**', async (route) => {
|
||
await route.fulfill({ status: 200, contentType: 'text/html', path: MOCK_H5_PATH })
|
||
})
|
||
|
||
await loggedInPage.goto('/p/create')
|
||
const iframe = loggedInPage.locator('iframe').first()
|
||
await expect(iframe).toBeVisible({ timeout: 10_000 })
|
||
|
||
// 新一轮创作成功
|
||
const newWorkId = randomWorkId()
|
||
const r2 = await sendWebhook({
|
||
event: 'work.status_changed',
|
||
data: { work_id: newWorkId, status: 1, phone: '13800001111' },
|
||
})
|
||
expect(r2.status).toBe(200)
|
||
|
||
const r3 = await sendWebhook({
|
||
event: 'work.status_changed',
|
||
data: { work_id: newWorkId, status: 3, title: '重试成功绘本' },
|
||
})
|
||
expect(r3.status).toBe(200)
|
||
})
|
||
})
|