oh-my-muse/muse-studio/e2e/knowledge-entity-crud.spec.ts
lili c119b90919 feat(knowledge): studio 图谱实体编辑 + 新建关系接通真后端 + e2e
P1 真缺口补全(3/7,价值最高):KnowledgeGraphPanel 从只读→可维护正式轨——useUpdateEntity→PUT /entities/{id}(编辑实体名,经后端 normalizedName 规范化)、useCreateRelation→POST /entities/{id}/relations(源→目标 + relationType);均带 commandId,graph node.id=实体 id 直接复用。UI:每实体 inline 编辑 + 底部源/目标/类型新建关系表单。

验证:vitest 52/52(contract 加 PUT/POST URL+body+targetEntityId 转 number)、tsc 0 错;新增 e2e knowledge-entity-crud.spec.ts 2 例真后端绿(编辑名回显规范化值、建关系回显新边),全量 26/26 无回归。反假绿:编辑回显失败暴露后端 normalizedName 去连字符规范化,e2e 适配真实规范化用稳定值断言、非掩盖。种子:global-setup #2c work2 三实体(与 graph 的 work1 隔离、按 description marker 复位)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 20:13:20 -07:00

73 lines
3.3 KiB
TypeScript

import { expect, test, type Page } from '@playwright/test';
/**
* 知识实体/关系维护 e2e(正式轨人工维护,真实后端,MSW off)。
*
* 前置:vite VITE_API_MOCK=false、真实 muse-server 48080、muse_slice_live 作品 2 下有 CRUD 种子实体
* crudshiti-jia/yi/bing(由 e2e/global-setup.ts #2c 每轮复位;用 work2 与 knowledge-graph 的 work1 fixture 隔离)。
* 目的:rendered studio 图谱面板的实体编辑(PUT /entities/{id})与新建关系(POST /entities/{id}/relations)
* 直打真后端(反假绿):编辑 jia 实体名、在 yi→bing 间建关系(各用独立实体避免互相影响)。
*/
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('编辑实体名 → 写回 Canonical(真实后端)', async ({ page }) => {
await seedToken(page);
await page.goto('/knowledge/2');
// 进入 jia 实体编辑态(按钮 accessible name 含实体名)。
const editBtn = page.getByRole('button', { name: '编辑实体 crudshiti-jia' });
await expect(editBtn).toBeVisible({ timeout: 15_000 });
await editBtn.click();
// 改名并保存,捕获真实 PUT 响应(打真后端,非 mock)。
// 注:后端 updateEntity 把 name 经 normalizedName() 规范化(NFKD+去非字母数字+小写)后存为 normalizedName,
// 图谱节点显示的正是 normalizedName;故输入用规范稳定形式(纯小写字母,无连字符),令回显值可预期、可断言。
await page.getByLabel('实体名称').fill('crudjiarenamed');
const [resp] = await Promise.all([
page.waitForResponse(
(r) => /\/entities\/\d+$/.test(r.url()) && r.request().method() === 'PUT',
{ timeout: 15_000 }
),
page.getByRole('button', { name: '保存' }).click(),
]);
// Yudao 口径:成功为 HTTP 200 + CommonResult{code:0}。
expect(resp.status()).toBe(200);
expect((await resp.json()).code).toBe(0);
// 回显:图谱重渲染显示规范化后的新实体名(证编辑真写回 Canonical,非假绿)。
await expect(page.getByText('crudjiarenamed').first()).toBeVisible({ timeout: 15_000 });
});
test('新建实体关系 → 写入 Canonical(真实后端)', async ({ page }) => {
await seedToken(page);
await page.goto('/knowledge/2');
// 等图谱加载(新建关系下拉出现)。
await expect(page.getByLabel('源实体')).toBeVisible({ timeout: 15_000 });
// 选源/目标实体(yi→bing,与编辑用的 jia 隔离)+ 关系类型。
await page.getByLabel('源实体').selectOption({ label: 'crudshiti-yi' });
await page.getByLabel('目标实体').selectOption({ label: 'crudshiti-bing' });
await page.getByLabel('关系类型').fill('e2e-shitu');
const [resp] = await Promise.all([
page.waitForResponse(
(r) => /\/entities\/\d+\/relations$/.test(r.url()) && r.request().method() === 'POST',
{ timeout: 15_000 }
),
page.getByRole('button', { name: '新建关系' }).click(),
]);
// 创建关系为 201 Created(后端 @ResponseStatus(CREATED))。
expect([200, 201]).toContain(resp.status());
expect((await resp.json()).code).toBe(0);
// 回显:关系区出现新建的关系语义。
await expect(page.getByText(/e2e-shitu/).first()).toBeVisible({ timeout: 15_000 });
});