library-picturebook-activity/frontend/e2e/leai/e2e-flow.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

228 lines
7.4 KiB
TypeScript
Raw Permalink 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/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',
h5Url: 'http://localhost:3001',
phone: '13800001111',
},
}),
})
})
await loggedInPage.route('http://192.168.1.72: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', h5Url: 'http://localhost:3001', 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.72: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', h5Url: 'http://localhost:3001', phone: '13800001111' },
}),
})
})
await loggedInPage.route('http://192.168.1.72: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)
})
})