Files
xiaoxia-saas/apps/web/e2e/templates.spec.ts
xiaoxia 0fcb77b991
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Successful in 1m6s
CI Build & Deploy Pipeline / Build Staging Web Image (push) Successful in 1m49s
CI Build & Deploy Pipeline / Build Production API Image (push) Has been skipped
CI Build & Deploy Pipeline / Build Production Web Image (push) Has been skipped
CI Build & Deploy Pipeline / Build Production Worker Image (push) Has been skipped
CI Build & Deploy Pipeline / Deploy Production (push) Has been skipped
CI Build & Deploy Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Validate Code Quality And Tests (push) Successful in 3m1s
CI/CD Pipeline / Unit Tests (push) Successful in 3m20s
CI/CD Pipeline / Integration Tests (push) Successful in 1m29s
CI Build & Deploy Pipeline / Build Staging API Image (push) Successful in 7m4s
CI Build & Deploy Pipeline / Build Staging Worker Image (push) Successful in 7m54s
CI Build & Deploy Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 45s
CI Build & Deploy Pipeline / Staging E2E Tests (push) Failing after 2m50s
CI Build & Deploy Pipeline / Staging API Integration Tests (push) Successful in 3m28s
ci: Prettier纳入两层防御体系 (#520)
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-07-18 18:08:19 +08:00

614 lines
18 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.
/**
* 模板库页面 E2E 测试
*
* 覆盖:模板列表加载、分类切换、模板详情、收藏/取消收藏、
* 使用模板入口、搜索功能、我的模板tab、未登录重定向
*
* 每个测试独立,先注册登录获取 auth token。
*/
import { expect, test, type APIRequestContext, type Page } from "@playwright/test"
const PASSWORD = "Test123456!"
const apiBase = process.env.E2E_API_BASE || "/api/v1"
const apiOrigin = apiBase.endsWith("/api/v1") ? apiBase.slice(0, -"/api/v1".length) : ""
const routeBrowserApiToTestApi = async (page: Page) => {
if (!apiOrigin) return
await page.route("**/api/v1/**", async (route) => {
const sourceUrl = new URL(route.request().url())
const response = await route.fetch({
url: `${apiOrigin}${sourceUrl.pathname}${sourceUrl.search}`,
})
await route.fulfill({ response })
})
}
function uniqueEmail(prefix: string): string {
return `${prefix}_${Date.now()}_${Math.random().toString(36).slice(2, 8)}@example.com`
}
function uniqueUsername(prefix: string): string {
return `${prefix}_${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`
}
/** 登录操作,遇到 429 限流自动等待重试 */
async function loginWithRetry(
request: APIRequestContext,
email: string,
password: string,
maxRetries = 2,
) {
for (let i = 0; i <= maxRetries; i++) {
const response = await request.post(`${apiBase}/auth/login`, {
data: { email, password },
})
if (response.status() !== 429) return response
console.log(`[login] 触发限流,等待 65s 后重试 (${i + 1}/${maxRetries})`)
await new Promise((r) => setTimeout(r, 65000))
}
return request.post(`${apiBase}/auth/login`, {
data: { email, password },
})
}
/** 注册并登录,返回 { headers, email, username, userId, accessToken } */
async function createAuthedUser(request: APIRequestContext, label: string) {
const email = uniqueEmail(label)
const username = uniqueUsername(label)
const reg = await request.post(`${apiBase}/auth/register`, {
data: { email, password: PASSWORD, username, display_name: `E2E ${label}` },
})
expect(reg.ok(), `注册应成功: ${await reg.text()}`).toBeTruthy()
const regData = await reg.json()
const login = await loginWithRetry(request, email, PASSWORD)
expect(login.ok(), `登录应成功: ${await login.text()}`).toBeTruthy()
const loginData = await login.json()
return {
headers: { Authorization: `Bearer ${loginData.access_token}` },
email,
username,
userId: regData.user_id,
accessToken: loginData.access_token,
}
}
/** 设置页面认证状态(localStorage */
async function setupAuth(
page: Page,
token: string,
user: { id: string; email: string; username: string; display_name: string },
) {
await page.addInitScript(
({ token, user }) => {
localStorage.setItem("access_token", token)
localStorage.setItem(
"auth-storage",
JSON.stringify({
state: { user, isAuthenticated: true },
version: 0,
}),
)
},
{
token,
user: {
id: user.id,
user_id: user.id,
email: user.email,
username: user.username,
display_name: user.display_name,
is_email_verified: true,
email_verified: true,
},
},
)
}
test.describe("模板库页面 - 未登录重定向", () => {
test("未登录访问重定向到登录页", async ({ page }) => {
await page.goto("/app/templates")
await expect(page).toHaveURL(/\/login/)
})
})
test.describe("模板库页面 - 页面加载", () => {
test.describe.configure({ timeout: 120_000 })
test("模板库页面加载成功", async ({ page, request }) => {
await routeBrowserApiToTestApi(page)
const { accessToken, userId, email, username } = await createAuthedUser(request, "tpl-load")
await setupAuth(page, accessToken, {
id: userId,
email,
username,
display_name: "E2E tpl-load",
})
await page.goto("/app/templates")
await expect(page.locator(".xx-templates-page")).toBeVisible({
timeout: 20_000,
})
})
test("模板库头部和搜索栏存在", async ({ page, request }) => {
await routeBrowserApiToTestApi(page)
const { accessToken, userId, email, username } = await createAuthedUser(request, "tpl-head")
await setupAuth(page, accessToken, {
id: userId,
email,
username,
display_name: "E2E tpl-head",
})
await page.goto("/app/templates")
await expect(page.locator(".xx-templates-page")).toBeVisible({
timeout: 20_000,
})
// 验证搜索框
const searchInput = page.locator(".xx-templates-search-input")
await expect(searchInput).toBeVisible({ timeout: 10_000 })
})
test("分类切换按钮存在", async ({ page, request }) => {
await routeBrowserApiToTestApi(page)
const { accessToken, userId, email, username } = await createAuthedUser(request, "tpl-cat")
await setupAuth(page, accessToken, {
id: userId,
email,
username,
display_name: "E2E tpl-cat",
})
await page.goto("/app/templates")
await expect(page.locator(".xx-templates-page")).toBeVisible({
timeout: 20_000,
})
// 验证分类按钮存在
const categoryBtns = page.locator(".xx-templates-cat-btn")
await expect(categoryBtns.first()).toBeVisible({ timeout: 10_000 })
const count = await categoryBtns.count()
expect(count).toBeGreaterThan(0)
})
})
test.describe("模板库 - 模板展示", () => {
test.describe.configure({ timeout: 120_000 })
test("模板卡片展示", async ({ page, request }) => {
await routeBrowserApiToTestApi(page)
const { accessToken, userId, email, username, headers } = await createAuthedUser(
request,
"tpl-cards",
)
const suffix = Date.now().toString(36)
// 创建一个模板
await request.post(`${apiBase}/templates`, {
headers,
data: {
name: `E2E 模板展示 ${suffix}`,
mode: "pip",
estimated_duration: 30,
description: "测试模板展示",
segments: [
{
segment_order: 1,
duration_min: 5,
duration_max: 15,
material_type: "video",
},
],
tags: ["e2e", "展示"],
category: "种草",
},
})
await setupAuth(page, accessToken, {
id: userId,
email,
username,
display_name: "E2E tpl-cards",
})
await page.goto("/app/templates")
await expect(page.locator(".xx-templates-page")).toBeVisible({
timeout: 20_000,
})
// 等待模板卡片出现
const cards = page.locator(".xx-template-card")
await expect(cards.first()).toBeVisible({ timeout: 15_000 })
const count = await cards.count()
expect(count).toBeGreaterThan(0)
})
test("模板卡片包含名称和类型", async ({ page, request }) => {
await routeBrowserApiToTestApi(page)
const { accessToken, userId, email, username, headers } = await createAuthedUser(
request,
"tpl-info",
)
const suffix = Date.now().toString(36)
await request.post(`${apiBase}/templates`, {
headers,
data: {
name: `模板信息测试 ${suffix}`,
mode: "voice_over",
estimated_duration: 60,
description: "测试信息展示",
segments: [
{
segment_order: 1,
duration_min: 10,
duration_max: 30,
material_type: "video",
},
],
},
})
await setupAuth(page, accessToken, {
id: userId,
email,
username,
display_name: "E2E tpl-info",
})
await page.goto("/app/templates")
await expect(page.locator(".xx-templates-page")).toBeVisible({
timeout: 20_000,
})
const firstCard = page.locator(".xx-template-card").first()
if (await firstCard.isVisible({ timeout: 15_000 })) {
// 验证信息区域存在
const info = firstCard.locator(".xx-template-info")
await expect(info).toBeVisible()
}
})
test("模板预览弹窗功能", async ({ page, request }) => {
await routeBrowserApiToTestApi(page)
const { accessToken, userId, email, username, headers } = await createAuthedUser(
request,
"tpl-preview",
)
const suffix = Date.now().toString(36)
await request.post(`${apiBase}/templates`, {
headers,
data: {
name: `预览测试模板 ${suffix}`,
mode: "pip",
estimated_duration: 30,
description: "预览测试描述",
segments: [
{
segment_order: 1,
duration_min: 5,
duration_max: 15,
material_type: "video",
description: "片段一",
},
],
},
})
await setupAuth(page, accessToken, {
id: userId,
email,
username,
display_name: "E2E tpl-preview",
})
await page.goto("/app/templates")
await expect(page.locator(".xx-templates-page")).toBeVisible({
timeout: 20_000,
})
// 点击第一个模板卡片打开预览
const firstCard = page.locator(".xx-template-card").first()
if (await firstCard.isVisible({ timeout: 15_000 })) {
await firstCard.click()
// 预览弹窗应该出现
const modal = page.locator(".xx-template-modal")
if (await modal.isVisible({ timeout: 5_000 })) {
await expect(modal).toBeVisible()
// 验证预览内容存在
await expect(modal.locator(".xx-template-modal-title-row")).toBeVisible()
}
}
})
})
test.describe("模板库 - 分类切换", () => {
test.describe.configure({ timeout: 120_000 })
test("切换分类筛选", async ({ page, request }) => {
await routeBrowserApiToTestApi(page)
const { accessToken, userId, email, username } = await createAuthedUser(request, "tpl-switch")
await setupAuth(page, accessToken, {
id: userId,
email,
username,
display_name: "E2E tpl-switch",
})
await page.goto("/app/templates")
await expect(page.locator(".xx-templates-page")).toBeVisible({
timeout: 20_000,
})
const categoryBtns = page.locator(".xx-templates-cat-btn")
const firstBtn = categoryBtns.first()
if (await firstBtn.isVisible({ timeout: 10_000 })) {
await firstBtn.click()
// 验证按钮被选中
await expect(firstBtn).toHaveClass(/active/)
}
})
})
test.describe("模板库 - 搜索", () => {
test.describe.configure({ timeout: 120_000 })
test("搜索框可输入并筛选", async ({ page, request }) => {
await routeBrowserApiToTestApi(page)
const { accessToken, userId, email, username, headers } = await createAuthedUser(
request,
"tpl-search",
)
const suffix = Date.now().toString(36)
const templateName = `E2E 搜索测试模板 ${suffix}`
await request.post(`${apiBase}/templates`, {
headers,
data: {
name: templateName,
mode: "pip",
estimated_duration: 30,
description: "搜索测试专用模板",
segments: [
{
segment_order: 1,
duration_min: 5,
duration_max: 15,
material_type: "video",
},
],
},
})
await setupAuth(page, accessToken, {
id: userId,
email,
username,
display_name: "E2E tpl-search",
})
await page.goto("/app/templates")
await expect(page.locator(".xx-templates-page")).toBeVisible({
timeout: 20_000,
})
const searchInput = page.locator(".xx-templates-search-input")
if (await searchInput.isVisible({ timeout: 10_000 })) {
await searchInput.fill(suffix)
// 验证页面正常响应
await expect(page.locator(".xx-templates-page")).toBeVisible()
}
})
})
test.describe("模板库 - API 操作", () => {
test.describe.configure({ timeout: 120_000 })
test("获取模板列表 - 正向", async ({ request }) => {
const { headers } = await createAuthedUser(request, "tpl-api-list")
const suffix = Date.now().toString(36)
await request.post(`${apiBase}/templates`, {
headers,
data: {
name: `API 列表测试 ${suffix}`,
mode: "pip",
estimated_duration: 30,
segments: [
{
segment_order: 1,
duration_min: 5,
duration_max: 15,
material_type: "video",
},
],
},
})
const response = await request.get(`${apiBase}/templates`, { headers })
expect(response.ok(), `获取模板列表应返回 2xx,实际: ${response.status()}`).toBeTruthy()
const data = await response.json()
const items = data.items || data.templates || []
expect(Array.isArray(items), "模板列表应为数组").toBeTruthy()
expect(items.length).toBeGreaterThan(0)
})
test("收藏/取消收藏模板 - 正向", async ({ request }) => {
const { headers } = await createAuthedUser(request, "tpl-fav")
const suffix = Date.now().toString(36)
// 创建模板
const createResp = await request.post(`${apiBase}/templates`, {
headers,
data: {
name: `收藏测试 ${suffix}`,
mode: "pip",
estimated_duration: 30,
segments: [
{
segment_order: 1,
duration_min: 5,
duration_max: 15,
material_type: "video",
},
],
},
})
expect(createResp.ok()).toBeTruthy()
const created = await createResp.json()
const templateId = created.id
// 收藏
const favResp = await request.post(`${apiBase}/templates/${templateId}/favorite`, { headers })
// 收藏可能成功或接口不存在
expect(favResp.status() < 500, "收藏请求应返回 2xx 或 4xx").toBeTruthy()
// 取消收藏
const unfavResp = await request.delete(`${apiBase}/templates/${templateId}/favorite`, {
headers,
})
expect(unfavResp.status() < 500, "取消收藏请求应返回 2xx 或 4xx").toBeTruthy()
})
test("获取模板详情 - 正向", async ({ request }) => {
const { headers } = await createAuthedUser(request, "tpl-api-detail")
const suffix = Date.now().toString(36)
const createResp = await request.post(`${apiBase}/templates`, {
headers,
data: {
name: `详情测试 ${suffix}`,
mode: "voice_over",
estimated_duration: 60,
description: "详情测试描述",
segments: [
{
segment_order: 1,
duration_min: 10,
duration_max: 30,
material_type: "video",
description: "测试片段",
},
],
},
})
expect(createResp.ok()).toBeTruthy()
const created = await createResp.json()
const detailResp = await request.get(`${apiBase}/templates/${created.id}`, {
headers,
})
expect(detailResp.ok(), "获取详情应成功").toBeTruthy()
const detail = await detailResp.json()
expect(detail.id).toBe(created.id)
expect(detail.name).toBe(`详情测试 ${suffix}`)
})
test("使用模板接口 - 正向", async ({ request }) => {
const { headers } = await createAuthedUser(request, "tpl-use")
const suffix = Date.now().toString(36)
const createResp = await request.post(`${apiBase}/templates`, {
headers,
data: {
name: `使用测试 ${suffix}`,
mode: "pip",
estimated_duration: 30,
segments: [
{
segment_order: 1,
duration_min: 5,
duration_max: 15,
material_type: "video",
},
],
},
})
expect(createResp.ok()).toBeTruthy()
const created = await createResp.json()
// 使用模板(生成)
const genResp = await request.post(`${apiBase}/templates/${created.id}/generate`, {
headers,
data: {},
})
// 生成可能成功或返回业务错误
expect(genResp.status() < 500, "使用模板应返回 2xx 或 4xx").toBeTruthy()
})
test("未登录获取模板列表 - 反向", async ({ request }) => {
const response = await request.get(`${apiBase}/templates`)
expect([401, 403]).toContain(response.status())
})
})
test.describe("模板库 - 我的模板 Tab", () => {
test.describe.configure({ timeout: 120_000 })
test("我的模板页面可访问", async ({ page, request }) => {
await routeBrowserApiToTestApi(page)
const { accessToken, userId, email, username } = await createAuthedUser(request, "tpl-my")
await setupAuth(page, accessToken, {
id: userId,
email,
username,
display_name: "E2E tpl-my",
})
await page.goto("/app/my-templates")
await expect(page.locator(".mt-page")).toBeVisible({
timeout: 20_000,
})
})
test("我的模板页面展示已创建的模板", async ({ page, request }) => {
await routeBrowserApiToTestApi(page)
const { accessToken, userId, email, username, headers } = await createAuthedUser(
request,
"tpl-my-data",
)
const suffix = Date.now().toString(36)
await request.post(`${apiBase}/templates`, {
headers,
data: {
name: `我的模板测试 ${suffix}`,
mode: "pip",
estimated_duration: 30,
description: "我的模板展示测试",
segments: [
{
segment_order: 1,
duration_min: 5,
duration_max: 15,
material_type: "video",
},
],
},
})
await setupAuth(page, accessToken, {
id: userId,
email,
username,
display_name: "E2E tpl-my-data",
})
await page.goto("/app/my-templates")
await expect(page.locator(".mt-page")).toBeVisible({
timeout: 20_000,
})
// 验证卡片容器存在
const cards = page.locator(".mt-card")
await expect(cards.first()).toBeVisible({ timeout: 15_000 })
})
})