fix(editor): 修删除作品缺契约字段致真后端 400(反假绿挖出,纯前端修复)
前端 useWorkDelete 此前 api.delete 不传 body,而后端 deleteWork 要 @Valid RevisionCommandReqVO (commandId 幂等 + expectedRevision 作品乐观锁)→ 真后端删作品必 400(dev mock 不校验长期掩盖)。 又因列表 VO(GET /works)不含 revision 字段(仅 detail 有),无法直接取乐观锁版本;故 useWorkDelete 改为删除前先读 work 详情拿 revision,再带 commandId 提交删除(删除低频、额外一次 GET 可接受)。 补 work-create-delete.spec.ts(配对自清理 e2e:建作品→删作品,验 DELETE 200/code:0 + 列表无残留)。 curl 实证后端 work create→delete code:0(无 chapter 那类 order_no schema bug)。 tsc/eslint 0,vitest 79/79,全量 e2e 42/0(+1 chapter fixme)。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
c4d3041268
commit
59d139637a
65
muse-studio/e2e/work-create-delete.spec.ts
Normal file
65
muse-studio/e2e/work-create-delete.spec.ts
Normal file
@ -0,0 +1,65 @@
|
||||
import { expect, test, type Page } from '@playwright/test';
|
||||
|
||||
/**
|
||||
* 作品新建 + 删除写路 e2e(真实后端,MSW off)——配对自清理。
|
||||
*
|
||||
* 前置:vite VITE_API_MOCK=false、真实 muse-server 48080。
|
||||
* 目的(反假绿):删作品是写命令,后端 deleteWork 要 @Valid RevisionCommandReqVO(commandId 幂等 +
|
||||
* expectedRevision 作品乐观锁),前端 useWorkDelete 此前 api.delete 不传 body→真后端必 400(dev mock 掩盖)。
|
||||
* 补字段后真打 DELETE /works/{id} 验 200/code:0。create 复用 D1 新建作品弹窗流,delete 自清理(删掉刚建作品,不留累积)。
|
||||
* 设计:唯一标题每次产新值;建→删配对,跑完作品列表无残留。删除走 window.confirm,用 page.once('dialog') 自动接受。
|
||||
*/
|
||||
const TOKEN = 'test1';
|
||||
|
||||
async function seedToken(page: Page): Promise<void> {
|
||||
await page.addInitScript((t) => {
|
||||
window.localStorage.setItem('accessToken', t);
|
||||
window.localStorage.setItem('tenantId', '1');
|
||||
}, TOKEN);
|
||||
}
|
||||
|
||||
test('用户新建并删除作品(真实后端写命令:commandId + 作品乐观锁)', async ({ page }) => {
|
||||
await seedToken(page);
|
||||
const uniqueTitle = `作品·e2e-${Date.now()}`;
|
||||
|
||||
await page.goto('/');
|
||||
await expect(page.getByRole('heading', { name: '我的创作台' })).toBeVisible({ timeout: 15_000 });
|
||||
|
||||
// 新建作品(复用 D1 弹窗流:打开弹窗→Schema 下拉→填标题→确认创建)。
|
||||
await page.getByRole('button', { name: /新建作品|立即创建/ }).first().click();
|
||||
await expect(page.getByLabel('作品结构 Schema')).toBeVisible({ timeout: 15_000 });
|
||||
await page.getByPlaceholder('请输入小说或书籍的标题,例如:星海迷途').fill(uniqueTitle);
|
||||
const [createResp] = await Promise.all([
|
||||
page.waitForResponse(
|
||||
(r) =>
|
||||
/\/works$/.test(new URL(r.url()).pathname) && r.request().method() === 'POST',
|
||||
{ timeout: 15_000 }
|
||||
),
|
||||
page.getByRole('button', { name: '确认创建' }).click(),
|
||||
]);
|
||||
expect(createResp.status()).toBe(200);
|
||||
expect((await createResp.json()).code).toBe(0);
|
||||
|
||||
// 新作品卡片出现在创作台。
|
||||
await expect(page.getByText(uniqueTitle)).toBeVisible({ timeout: 10_000 });
|
||||
|
||||
// 自清理:删除刚建作品。hover 卡片出垃圾桶按钮,confirm 自动接受。
|
||||
page.once('dialog', (d) => d.accept());
|
||||
const card = page.locator('.group').filter({ hasText: uniqueTitle });
|
||||
await card.hover();
|
||||
|
||||
const [deleteResp] = await Promise.all([
|
||||
page.waitForResponse(
|
||||
(r) =>
|
||||
/\/works\/[^/]+$/.test(new URL(r.url()).pathname) &&
|
||||
r.request().method() === 'DELETE',
|
||||
{ timeout: 15_000 }
|
||||
),
|
||||
card.getByTitle('删除作品').click(),
|
||||
]);
|
||||
expect(deleteResp.status()).toBe(200);
|
||||
expect((await deleteResp.json()).code).toBe(0);
|
||||
|
||||
// 删除后列表刷新(onSuccess 失效 works 缓存 + navigate '/'),作品卡片消失,列表无残留。
|
||||
await expect(page.getByText(uniqueTitle)).toHaveCount(0, { timeout: 10_000 });
|
||||
});
|
||||
@ -68,7 +68,16 @@ export function useWorkDelete() {
|
||||
const queryClient = useQueryClient();
|
||||
const navigate = useNavigate();
|
||||
return useMutation({
|
||||
mutationFn: (workId: string) => api.delete<boolean>(`/works/${workId}`),
|
||||
// 删作品是写命令:后端 deleteWork 要 @Valid RevisionCommandReqVO(commandId 幂等 + expectedRevision 作品乐观锁),
|
||||
// 此前前端不传 body→真后端必 400(dev mock 掩盖)。列表 VO(GET /works)不含 revision,故删除前先读 work
|
||||
// 详情拿当前 revision,再带 commandId 提交删除(删除低频,额外一次 GET 可接受;乐观锁兜底并发改写)。
|
||||
mutationFn: async (workId: string) => {
|
||||
const detail = await api.get<WorkVO>(`/works/${workId}`);
|
||||
return api.delete<boolean>(`/works/${workId}`, {
|
||||
commandId: crypto.randomUUID(),
|
||||
expectedRevision: detail.revision,
|
||||
});
|
||||
},
|
||||
onSuccess: (_, workId) => {
|
||||
// 失效作品列表,强制拉取新列表
|
||||
queryClient.invalidateQueries({ queryKey: ['works'] });
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user