test(studio): D-c block 分割/合并 e2e 真跑 + 幂等复位[P1]
- block-structure.spec.ts:真后端端到端——进 work1 workspace→章节结构条(1 小节)→分割(真实 split POST 200→2 小节)→合并(真实 merge POST 200→1 小节);'N 个小节'来自 chapterDetail 真数据,证明 split 真产生 block。 - global-setup section 8:幂等复位 work1 chapter1 为单 Block(删 split 残留,保留 order_no 最小首 Block)。 - BlockStructureBar.extractText 兼容纯文本 content(基础种子 block content 为纯文本非 ProseMirror JSON,JSON.parse 失败时退回原串,使 split 可用)。 验证:playwright block-structure 1 passed(真后端活体) + eslint 0。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
af781163a9
commit
c76cdec60a
43
muse-studio/e2e/block-structure.spec.ts
Normal file
43
muse-studio/e2e/block-structure.spec.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import { expect, test, type Page } from '@playwright/test';
|
||||
|
||||
/**
|
||||
* Block 分割/合并端到端(真实后端,MSW off)。
|
||||
*
|
||||
* 覆盖 D 链路:进 work1 workspace → BlockStructureBar 显示章节结构 → 分割首节(真实 split,1→2)→
|
||||
* 验证 2 个小节 → 合并首节与下一节(真实 merge,2→1)→ 验证回到 1 个小节。
|
||||
* globalSetup 已复位 work1 chapter1 为单 Block(幂等:即使中途失败残留也清掉)。
|
||||
*/
|
||||
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('用户分割与合并章节内 Block(真实后端结构编辑)', async ({ page }) => {
|
||||
await seedToken(page);
|
||||
|
||||
await page.goto('/workspace/1');
|
||||
await expect(page.getByRole('heading', { name: /正在创作/ })).toBeVisible({ timeout: 15_000 });
|
||||
|
||||
// 章节结构条出现,初始 1 个小节(globalSetup 已复位)。
|
||||
await expect(page.getByText(/章节结构 · 1 个小节/)).toBeVisible({ timeout: 15_000 });
|
||||
|
||||
// 分割首节(真实 split)→ chapterDetail 重拉 → 2 个小节。
|
||||
const splitResp = page.waitForResponse(
|
||||
(r) => /\/blocks\/[^/]+\/split/.test(r.url()) && r.request().method() === 'POST'
|
||||
);
|
||||
await page.getByRole('button', { name: '分割小节 1' }).click();
|
||||
expect((await splitResp).status()).toBe(200);
|
||||
await expect(page.getByText(/章节结构 · 2 个小节/)).toBeVisible({ timeout: 10_000 });
|
||||
|
||||
// 合并首节与下一节(真实 merge)→ 回到 1 个小节。
|
||||
const mergeResp = page.waitForResponse(
|
||||
(r) => /\/blocks\/[^/]+\/merge/.test(r.url()) && r.request().method() === 'POST'
|
||||
);
|
||||
await page.getByRole('button', { name: '合并小节 1 与下一节' }).click();
|
||||
expect((await mergeResp).status()).toBe(200);
|
||||
await expect(page.getByText(/章节结构 · 1 个小节/)).toBeVisible({ timeout: 10_000 });
|
||||
});
|
||||
@ -223,7 +223,14 @@ async function globalSetup(): Promise<void> {
|
||||
// work1 绑 setting(其 target_type=novel_work → 投影列出同 targetType 的 setting+worldview)。
|
||||
await client.query('UPDATE muse_content_work SET work_schema_id=$1 WHERE tenant_id=1 AND id=1', [settingSchemaId]);
|
||||
|
||||
console.log('[e2e globalSetup] 已复位每轮 fixture(graph/confirm-draft/accept/security-ack/appeal/binding/meta-schema)');
|
||||
// 8) block split/merge:复位 work1 chapter1 为单 Block——删 split 残留的多余 Block(保留 order_no 最小的首 Block),
|
||||
// 供 D block 结构编辑 e2e 幂等重跑(split→2→merge→1,中途失败的残留也清掉)。
|
||||
await client.query(
|
||||
`DELETE FROM muse_content_block WHERE tenant_id=1 AND work_id=1 AND chapter_id=1
|
||||
AND id <> (SELECT id FROM muse_content_block WHERE tenant_id=1 AND work_id=1 AND chapter_id=1 ORDER BY order_no, id LIMIT 1)`
|
||||
);
|
||||
|
||||
console.log('[e2e globalSetup] 已复位每轮 fixture(graph/confirm-draft/accept/security-ack/appeal/binding/meta-schema/block)');
|
||||
} finally {
|
||||
await client.end();
|
||||
}
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import { useSplitBlock, useMergeBlock } from '../hooks/useBlockStructure';
|
||||
import type { BlockVO } from '@/types/openapi';
|
||||
|
||||
/** 从 Tiptap JSON 字符串提取纯文本(用于摘要 + 计算分割中点)。 */
|
||||
function extractText(contentJson: string): string {
|
||||
/** 从正文提取纯文本(用于摘要 + 计算分割中点);兼容 Tiptap JSON 与纯文本 content(基础种子可能是纯文本)。 */
|
||||
function extractText(content: string): string {
|
||||
try {
|
||||
const out: string[] = [];
|
||||
const walk = (node: unknown): void => {
|
||||
@ -11,10 +11,11 @@ function extractText(contentJson: string): string {
|
||||
if (typeof n.text === 'string') out.push(n.text);
|
||||
if (Array.isArray(n.content)) n.content.forEach(walk);
|
||||
};
|
||||
walk(JSON.parse(contentJson));
|
||||
return out.join('');
|
||||
walk(JSON.parse(content));
|
||||
const joined = out.join('');
|
||||
return joined || content; // 是 JSON 但无文本节点 → 退回原串。
|
||||
} catch {
|
||||
return '';
|
||||
return content; // content 非 JSON(纯文本)→ 直接用。
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user