library-picturebook-activity/frontend/e2e/public/work-status-split.spec.ts
En 400fc97ebb feat: 拆分 UgcWork.status 为 status(String) + leaiStatus(Integer),修复作品库 Tab 筛选失败
根因:UgcWork.status (Integer) 同时承载「乐读派创作进度」和「本地发布状态」,
前端用字符串筛选时无法匹配。

改动:
- 新增 V17 迁移脚本:拆分 status 为 VARCHAR + 新增 leai_status INT
- 新增 WorkPublishStatus 枚举 (draft/unpublished/pending_review/published/rejected)
- 新增 LeaiCreationStatus 常量类 (FAILED~DUBBED)
- LeaiSyncService:写入 leaiStatus,CATALOGED 时自动推 status 到 unpublished
- 所有公众端 Service:status 直接使用字符串枚举值,删除 Integer 映射
- 新增 Playwright E2E 测试验证 12 个场景全部通过

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-10 02:27:09 +08:00

256 lines
10 KiB
TypeScript
Raw 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, request as requestFactory, type APIRequestContext } from '@playwright/test'
/**
* UGC 作品状态字段拆分验证测试
*
* 所有 API 请求直接发到后端 localhost:8580绕过前端代理。
*/
// ── 配置 ──
const API_BASE = process.env.API_BASE_URL || 'http://localhost:8580/api'
const AUTH = {
username: process.env.TEST_USERNAME || 'demo',
password: process.env.TEST_PASSWORD || 'demo123456',
tenantCode: process.env.TEST_TENANT_CODE || 'gdlib',
}
const VALID_STATUSES = ['draft', 'unpublished', 'pending_review', 'published', 'rejected']
// ── Helper ──
function url(path: string) {
return `${API_BASE}${path}`
}
// ── Fixture ──
type Fixtures = { api: APIRequestContext }
const apiTest = test.extend<Fixtures>({
api: async ({}, use) => {
// 用 requestFactory顶层 import创建独立上下文来登录
const loginCtx = await requestFactory.newContext({})
const loginResp = await loginCtx.post(url('/public/auth/login'), {
data: { username: AUTH.username, password: AUTH.password, tenantCode: AUTH.tenantCode },
})
const loginJson = await loginResp.json()
await loginCtx.dispose()
if (loginJson.code !== 200 || !loginJson.data?.token) {
throw new Error(`登录失败: ${JSON.stringify(loginJson)}`)
}
const token = loginJson.data.token
// 创建带 auth header 的 API 上下文
const api = await requestFactory.newContext({
extraHTTPHeaders: { Authorization: `Bearer ${token}` },
})
await use(api)
await api.dispose()
},
})
// ══════════════════════════════════════════════════════
// 1. 我的作品列表 — status 是字符串枚举
// ══════════════════════════════════════════════════════
apiTest('S-01 我的作品列表 — status 字段为字符串', async ({ api }) => {
const resp = await api.get(url('/public/works?page=1&pageSize=5'))
const json = await resp.json()
expect(json.code).toBe(200)
expect(json.data.list).toBeInstanceOf(Array)
for (const work of json.data.list) {
expect(
VALID_STATUSES.includes(work.status),
`作品 id=${work.id} 的 status="${work.status}" 不是合法字符串枚举值`,
).toBe(true)
}
console.log(`✓ S-01: 返回 ${json.data.list.length} 条作品status 全部为字符串`)
})
apiTest('S-02 我的作品列表 — 按 status=draft 筛选', async ({ api }) => {
const resp = await api.get(url('/public/works?page=1&pageSize=50&status=draft'))
const json = await resp.json()
expect(json.code).toBe(200)
for (const work of json.data.list) {
expect(work.status).toBe('draft')
}
console.log(`✓ S-02: draft 筛选返回 ${json.data.list.length}`)
})
apiTest('S-03 我的作品列表 — 按 status=unpublished 筛选', async ({ api }) => {
const resp = await api.get(url('/public/works?page=1&pageSize=50&status=unpublished'))
const json = await resp.json()
expect(json.code).toBe(200)
for (const work of json.data.list) {
expect(work.status).toBe('unpublished')
}
console.log(`✓ S-03: unpublished 筛选返回 ${json.data.list.length}`)
})
apiTest('S-04 我的作品列表 — 按 status=published 筛选', async ({ api }) => {
const resp = await api.get(url('/public/works?page=1&pageSize=50&status=published'))
const json = await resp.json()
expect(json.code).toBe(200)
for (const work of json.data.list) {
expect(work.status).toBe('published')
}
console.log(`✓ S-04: published 筛选返回 ${json.data.list.length}`)
})
apiTest('S-05 我的作品列表 — 按 status=pending_review 筛选', async ({ api }) => {
const resp = await api.get(url('/public/works?page=1&pageSize=50&status=pending_review'))
const json = await resp.json()
expect(json.code).toBe(200)
for (const work of json.data.list) {
expect(work.status).toBe('pending_review')
}
console.log(`✓ S-05: pending_review 筛选返回 ${json.data.list.length}`)
})
apiTest('S-06 我的作品列表 — 按 status=rejected 筛选', async ({ api }) => {
const resp = await api.get(url('/public/works?page=1&pageSize=50&status=rejected'))
const json = await resp.json()
expect(json.code).toBe(200)
for (const work of json.data.list) {
expect(work.status).toBe('rejected')
}
console.log(`✓ S-06: rejected 筛选返回 ${json.data.list.length}`)
})
// ══════════════════════════════════════════════════════
// 2. 创建作品 — 初始 status 为 draft
// ══════════════════════════════════════════════════════
apiTest('S-07 创建作品 — 初始 status 为 draft', async ({ api }) => {
const resp = await api.post(url('/public/works'), {
data: {
title: `[E2E] 状态验证_${Date.now()}`,
description: 'Playwright 自动创建',
visibility: 'private',
},
})
const json = await resp.json()
expect(json.code).toBe(200)
expect(json.data.status).toBe('draft')
expect(typeof json.data.status).toBe('string')
console.log(`✓ S-07: 创建作品 id=${json.data.id}, status="${json.data.status}"`)
// 清理
if (json.data.id) {
await api.delete(url(`/public/works/${json.data.id}`))
}
})
// ══════════════════════════════════════════════════════
// 3. draft 不可发布
// ══════════════════════════════════════════════════════
apiTest('S-08 发布作品 — draft 状态不可发布', async ({ api }) => {
const createResp = await api.post(url('/public/works'), {
data: { title: `[E2E] 发布测试_${Date.now()}`, visibility: 'private' },
})
const createJson = await createResp.json()
expect(createJson.code).toBe(200)
const workId = createJson.data.id
const publishResp = await api.post(url(`/public/works/${workId}/publish`))
const publishJson = await publishResp.json()
expect(publishJson.code).toBe(400)
console.log(`✓ S-08: draft 发布被拒绝code=${publishJson.code}, msg="${publishJson.message}"`)
await api.delete(url(`/public/works/${workId}`))
})
// ══════════════════════════════════════════════════════
// 4. 创作历史
// ══════════════════════════════════════════════════════
apiTest('S-09 创作历史 — 作品 status 为字符串', async ({ api }) => {
const resp = await api.get(url('/public/creation/history?page=1&pageSize=5'))
const json = await resp.json()
expect(json.code).toBe(200)
if (json.data?.list?.length > 0) {
for (const work of json.data.list) {
expect(
VALID_STATUSES.includes(work.status),
`创作历史 id=${work.id} status="${work.status}" 不合法`,
).toBe(true)
}
}
console.log(`✓ S-09: 创作历史 ${json.data?.list?.length ?? 0}`)
})
// ══════════════════════════════════════════════════════
// 5. 作品详情
// ══════════════════════════════════════════════════════
apiTest('S-10 作品详情 — status + leaiStatus 字段', async ({ api }) => {
const listResp = await api.get(url('/public/works?page=1&pageSize=1'))
const listJson = await listResp.json()
if (listJson.data?.list?.length > 0) {
const workId = listJson.data.list[0].id
const detailResp = await api.get(url(`/public/works/${workId}`))
const detailJson = await detailResp.json()
expect(detailJson.code).toBe(200)
const work = detailJson.data.work
// status 必须是合法字符串
expect(VALID_STATUSES.includes(work.status)).toBe(true)
// leaiStatus 应为数字
if (work.leaiStatus != null) {
expect(typeof work.leaiStatus).toBe('number')
}
console.log(`✓ S-10: 作品详情 status="${work.status}", leaiStatus=${work.leaiStatus}`)
} else {
console.log('⚠ S-10: 没有作品可测试详情')
}
})
// ══════════════════════════════════════════════════════
// 6. 作品广场
// ══════════════════════════════════════════════════════
apiTest('S-11 作品广场 — 只返回已发布作品', async ({ api }) => {
const resp = await api.get(url('/public/gallery?page=1&pageSize=10'))
const json = await resp.json()
expect(json.code).toBe(200)
if (json.data?.list?.length > 0) {
for (const work of json.data.list) {
expect(work.status).toBe('published')
}
}
console.log(`✓ S-11: 广场 ${json.data?.list?.length ?? 0} 条已发布作品`)
})
// ══════════════════════════════════════════════════════
// 7. 收藏列表
// ══════════════════════════════════════════════════════
apiTest('S-12 我的收藏 — status 为字符串', async ({ api }) => {
const resp = await api.get(url('/public/mine/favorites?page=1&pageSize=5'))
const json = await resp.json()
expect(json.code).toBe(200)
if (json.data?.list?.length > 0) {
for (const item of json.data.list) {
if (item.status) {
expect(VALID_STATUSES.includes(item.status)).toBe(true)
}
}
}
console.log(`✓ S-12: 收藏 ${json.data?.list?.length ?? 0}`)
})