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

249 lines
7.6 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/auth.fixture'
import { fileURLToPath } from 'url'
import path from 'path'
/**
* P1: 创作页 iframe 嵌入测试
*
* 测试 frontend/src/views/public/create/Index.vue
* - iframe 正确加载
* - 加载状态
* - 错误处理
* - iframe 属性allow、尺寸
*/
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
/** Mock H5 页面的文件路径 */
const MOCK_H5_PATH = path.resolve(__dirname, '../utils/mock-h5.html')
test.describe('创作页 iframe 嵌入', () => {
test.describe('未登录状态', () => {
test('访问 /p/create — 重定向到登录页', async ({ page }) => {
await page.goto('/p/create')
// 应该重定向到登录页或显示登录提示
await page.waitForTimeout(2000)
const url = page.url()
expect(url).toMatch(/\/(login|auth)/)
})
})
test.describe('已登录状态', () => {
test('显示加载中提示', async ({ loggedInPage }) => {
// 拦截 token API让它延迟
await loggedInPage.route('**/leai-auth/token', async (route) => {
await new Promise((r) => setTimeout(r, 3000))
await route.continue()
})
await loggedInPage.goto('/p/create')
// 应该显示加载状态
const loadingText = loggedInPage.locator('text=正在加载创作工坊')
await expect(loadingText).toBeVisible({ timeout: 5000 })
})
test('iframe 正确渲染', async ({ loggedInPage }) => {
// 拦截 token API 返回 mock 数据
await loggedInPage.route('**/leai-auth/token', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
code: 200,
data: {
token: 'mock_session_token_xxx',
orgId: 'gdlib',
h5Url: 'http://localhost:3001',
phone: '13800001111',
},
}),
})
})
// 拦截 iframe 加载,返回 mock H5 页面
await loggedInPage.route('**/*leai*/**', async (route) => {
await route.fulfill({
status: 200,
contentType: 'text/html',
path: MOCK_H5_PATH,
})
})
// 也拦截通配形式的 H5 URL
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')
// 等待 iframe 出现
const iframe = loggedInPage.locator('iframe.creation-iframe, iframe[allow*="camera"]')
await expect(iframe).toBeVisible({ timeout: 10_000 })
// 验证 iframe src 包含必要参数
const src = await iframe.getAttribute('src')
expect(src).toBeTruthy()
expect(src).toContain('token=')
expect(src).toContain('orgId=')
expect(src).toContain('phone=')
expect(src).toContain('embed=1')
})
test('iframe 有 camera 和 microphone 权限', async ({ loggedInPage }) => {
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',
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 allow = await iframe.getAttribute('allow')
expect(allow).toContain('camera')
expect(allow).toContain('microphone')
})
test('Token 获取失败 — 显示错误和重试按钮', async ({ loggedInPage }) => {
// 拦截 token API 返回 HTTP 500 错误
await loggedInPage.route('**/leai-auth/token', async (route) => {
await route.fulfill({
status: 500,
contentType: 'application/json',
body: JSON.stringify({
code: 500,
message: '获取创作Token失败: 连接超时',
}),
})
})
await loggedInPage.goto('/p/create')
// 应该显示错误信息(.load-error 在 v-if="loading" 容器内)
const errorText = loggedInPage.locator('.load-error')
await expect(errorText).toBeVisible({ timeout: 10_000 })
// 应该有重新加载按钮
const retryBtn = loggedInPage.locator('button:has-text("重新加载")')
await expect(retryBtn).toBeVisible()
})
test('重新加载按钮 — 可重新获取 Token', async ({ loggedInPage }) => {
let callCount = 0
await loggedInPage.route('**/leai-auth/token', async (route) => {
callCount++
if (callCount === 1) {
// 第一次失败HTTP 500
await route.fulfill({
status: 500,
contentType: 'application/json',
body: JSON.stringify({ code: 500, message: '网络错误' }),
})
} else {
// 第二次成功
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
code: 200,
data: {
token: 'retry_token_success',
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 retryBtn = loggedInPage.locator('button:has-text("重新加载")')
await expect(retryBtn).toBeVisible({ timeout: 10_000 })
// 点击重试
await retryBtn.click()
// 第二次应成功iframe 应出现
const iframe = loggedInPage.locator('iframe')
await expect(iframe).toBeVisible({ timeout: 10_000 })
expect(callCount).toBe(2)
})
test('iframe 占满内容区域', async ({ loggedInPage }) => {
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',
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 })
// iframe 应该没有边框
const frameBorder = await iframe.getAttribute('frameborder')
expect(frameBorder).toBe('0')
// iframe 高度应接近视口高度(至少 400px
const box = await iframe.boundingBox()
expect(box).toBeTruthy()
expect(box!.height).toBeGreaterThan(400)
})
})
})