Files
pikasTech-HWLAB/web/hwlab-cloud-web/tests/workbench-e2e/specs/project-management.spec.ts
T

246 lines
16 KiB
TypeScript

// SPEC: PJ2026-010404 项目管理 draft-2026-06-25-p0-project-management-mdtodo; draft-2026-06-26-p1-mdtodo-web-operable.
// Responsibility: Fake-server browser regression for project root and MDTODO pages.
import { expect, saveScreenshot, test } from "../fixtures/test";
test.describe("project management pages", () => {
test("renders project root and launches Workbench through public DTOs", async ({ page }, testInfo) => {
await page.goto("/projects");
await expect(page.getByTestId("project-management-root")).toBeVisible();
await expect(page.getByTestId("project-management-summary")).toContainText("Projects");
await expect(page.getByTestId("project-list")).toContainText("HWLAB MDTODO");
await page.getByRole("link", { name: /HWLAB MDTODO/u }).first().click();
await expect(page).toHaveURL(/\/projects\/project_hwlab_v03/u);
await expect(page.locator(".project-inspector")).toContainText("mdtodo-file-tree");
await expect(page.getByText("[redacted]")).toHaveCount(0);
await page.getByTestId("open-mdtodo-page").click();
await expect(page).toHaveURL(/\/projects\/mdtodo/u);
await expect(page.getByTestId("project-management-mdtodo")).toBeVisible();
await expect(page.getByTestId("mdtodo-toolbar")).toBeVisible();
await expect(page.getByTestId("mdtodo-source-select")).toHaveValue("hwlab-v03-mdtodo");
await expect(page.getByTestId("mdtodo-file-select")).toHaveValue("file_plan");
await expect(page.getByTestId("mdtodo-file-select")).toContainText("hwlab-v03-mdtodo-web-sample.md");
await expect(page.getByTestId("mdtodo-file-select")).toContainText("20260609_频率判断_用户反馈.md");
await expect(page.getByTestId("mdtodo-file-select")).not.toContainText("Task_Report");
const workspaceBottomGap = await page.getByTestId("project-management-mdtodo").evaluate((shell) => {
const workspace = shell.querySelector(".mdtodo-workspace");
if (!workspace) return Number.POSITIVE_INFINITY;
const shellRect = shell.getBoundingClientRect();
const workspaceRect = workspace.getBoundingClientRect();
return Math.abs(shellRect.bottom - workspaceRect.bottom);
});
expect(workspaceBottomGap).toBeLessThanOrEqual(2);
const outlineRatio = await page.locator(".task-index").evaluate((panel) => {
const workspace = panel.closest(".mdtodo-workspace");
if (!workspace) return 0;
return panel.getBoundingClientRect().width / workspace.getBoundingClientRect().width;
});
expect(outlineRatio).toBeGreaterThan(0.24);
expect(outlineRatio).toBeLessThan(0.36);
await expect(page.getByTestId("mdtodo-task-tree")).toContainText("实现项目根导航");
await expect(page.getByTestId("mdtodo-task-page-fact")).toContainText("已载入");
await expect(page.locator('[data-task-id="R1"]')).toBeVisible();
await expect(page.locator('[data-task-id="R1.1"]')).toBeVisible();
await expect(page.locator('[data-task-id="R1.1.1"]')).toBeVisible();
await expect(page.getByTestId("mdtodo-task-detail")).not.toContainText("mdtodo:hwlab-v03-mdtodo:file_plan:R1");
await expect(page.getByTestId("mdtodo-body-read")).toContainText("Root notes");
await page.getByTestId("mdtodo-report-link").click();
await expect(page.getByTestId("mdtodo-report-preview")).toContainText("Report body from fake server");
await expect(page).toHaveURL(/\/projects\/mdtodo\/sources\/hwlab-v03-mdtodo\/files\/file_plan\/tasks\/R1\/reports\/lnk_r11_report/u);
await page.goBack();
await expect(page).toHaveURL(/\/files\/file_plan\/tasks\/R1(?:\?|$)/u);
await page.goForward();
await expect(page.getByTestId("mdtodo-report-preview")).toContainText("Report body from fake server");
await page.getByTestId("mdtodo-report-fullscreen").click();
await expect(page.getByTestId("mdtodo-report-fullscreen-dialog")).toContainText("R1.1 implementation report");
await page.locator('[role="dialog"]').filter({ has: page.getByTestId("mdtodo-report-fullscreen-dialog") }).getByLabel("关闭", { exact: true }).click();
await page.getByTestId("mdtodo-info-open").click();
await expect(page.getByTestId("mdtodo-info-popover")).toContainText("Source");
await expect(page.getByTestId("mdtodo-info-popover")).toContainText("Task / Window");
await expect(page.getByTestId("mdtodo-info-popover")).toContainText("Task Ref");
await expect(page.getByTestId("mdtodo-info-popover")).toContainText("mdtodo:hwlab-v03-mdtodo:file_plan:R1");
await page.getByRole("dialog", { name: "MDTODO 信息" }).getByLabel("关闭", { exact: true }).click();
await page.getByTestId("mdtodo-file-select").selectOption("file_feedback");
await expect(page).toHaveURL(/\/projects\/mdtodo\/sources\/hwlab-v03-mdtodo\/files\/file_feedback\/tasks\/R1/u);
await expect(page.getByTestId("mdtodo-title-read")).toContainText("这是用户的反馈");
await expect(page.getByTestId("mdtodo-body-read")).toContainText("sys_trip");
await page.getByTestId("mdtodo-report-link").click();
await expect(page.getByTestId("mdtodo-report-preview")).toContainText("int_trip");
await expect(page.getByTestId("mdtodo-report-preview")).toContainText("sys_trip");
const reportPreBg = await page.locator('[data-testid="mdtodo-report-preview"] pre').first().evaluate((node) => getComputedStyle(node).backgroundColor);
expect(reportPreBg).not.toBe("rgb(15, 23, 42)");
await page.getByTestId("mdtodo-file-select").selectOption("file_plan");
await expect(page).toHaveURL(/\/projects\/mdtodo\/sources\/hwlab-v03-mdtodo\/files\/file_plan\/tasks\/R1/u);
await expect(page.getByTestId("mdtodo-title-read")).toContainText("实现项目根导航");
await page.getByTestId("mdtodo-source-config-open").click();
await expect(page.getByTestId("mdtodo-source-config-dialog")).toBeVisible();
await expect(page.getByTestId("mdtodo-source-form-hwpod")).toHaveValue("constart-71freq-c");
await expect(page.getByTestId("mdtodo-source-form-root")).toHaveValue("docs/MDTODO/");
await page.getByTestId("mdtodo-source-probe").click();
await expect(page.getByTestId("mdtodo-source-message")).toContainText("Probe ok");
await page.getByTestId("mdtodo-source-reindex-dialog").click();
await expect(page.getByTestId("mdtodo-source-message")).toContainText("Reindex 3 files / 5 tasks");
await page.getByRole("dialog", { name: "MDTODO Source" }).getByLabel("关闭", { exact: true }).click();
await page.getByTestId("mdtodo-title-read").dblclick();
await page.getByTestId("mdtodo-edit-title").fill("实现项目根导航 - edited");
await page.getByTestId("mdtodo-edit-status").selectOption("in_progress");
await page.getByTestId("mdtodo-edit-save").click();
await expect(page.getByTestId("mdtodo-task-notice")).toContainText("任务已保存");
await expect(page.getByTestId("mdtodo-task-detail")).toContainText("实现项目根导航 - edited");
await expect(page.locator('[data-task-id="R1"]')).toContainText("进行中");
await page.getByTestId("mdtodo-body-read").dblclick();
await page.getByTestId("mdtodo-body-editor").fill("Body saved from Playwright.");
await page.getByTestId("mdtodo-body-save").click();
await expect(page.getByTestId("mdtodo-task-notice")).toContainText("正文已保存");
await page.getByTestId("mdtodo-new-task-open").click();
await expect(page.getByTestId("mdtodo-task-create-dialog")).toBeVisible();
await page.getByTestId("mdtodo-new-title").fill("Playwright child");
await page.getByTestId("mdtodo-new-body").fill("Child from E2E.");
await page.getByTestId("mdtodo-add-subtask").click();
await expect(page.getByTestId("mdtodo-task-notice")).toContainText("任务已创建");
await expect(page).toHaveURL(/\/tasks\/R1\.2/u);
await expect(page.locator('[data-task-id="R1.2"]')).toContainText("Playwright child");
await page.getByTestId("mdtodo-new-task-open").click();
await page.getByTestId("mdtodo-new-title").fill("Playwright sibling");
await page.getByTestId("mdtodo-continue-task").click();
await expect(page.locator('[data-task-id="R1.3"]')).toContainText("Playwright sibling");
await page.getByTestId("mdtodo-delete-task").click();
await expect(page.getByTestId("mdtodo-task-notice")).toContainText("再次点击确认删除");
await page.getByTestId("mdtodo-delete-task").click();
await expect(page.getByTestId("mdtodo-task-notice")).toContainText("任务已删除");
await expect(page).toHaveURL(/\/tasks\/R1(?:\?|$)/u);
await expect(page.locator('[data-task-id="R1.3"]')).toHaveCount(0);
await page.getByTestId("mdtodo-info-open").click();
await expect(page.getByTestId("mdtodo-info-popover")).toContainText("ses_project_seed");
await page.getByRole("dialog", { name: "MDTODO 信息" }).getByLabel("关闭", { exact: true }).click();
await expect(page.getByTestId("mdtodo-launch-workbench")).toBeEnabled();
await expect(page.locator("iframe")).toHaveCount(0);
await saveScreenshot(page, testInfo, "project-management-mdtodo");
const workbenchPagePromise = page.waitForEvent("popup");
await page.getByTestId("mdtodo-launch-workbench").click();
const workbenchPage = await workbenchPagePromise;
await expect(workbenchPage).toHaveURL(/\/workbench\/sessions\/ses_project_launch_/u);
await expect(workbenchPage.locator("#workspace")).toBeVisible();
const sessionId = workbenchPage.url().match(/\/workbench\/sessions\/([^/?#]+)/u)?.[1] ?? "";
expect(sessionId).toMatch(/^ses_project_launch_/u);
const sessionDetail = await page.request.get(`/v1/workbench/sessions/${encodeURIComponent(sessionId)}`);
expect(sessionDetail.ok()).toBeTruthy();
const sessionPayload = await sessionDetail.json();
expect(sessionPayload.session?.launchContext?.projectId ?? sessionPayload.session?.metadata?.launchContext?.projectId).toBe("project_hwlab_v03");
expect(sessionPayload.session?.launchContext?.taskRef ?? sessionPayload.session?.metadata?.launchContext?.taskRef).toBe("mdtodo:hwlab-v03-mdtodo:file_plan:R1");
expect(sessionPayload.session?.messageCount ?? sessionPayload.session?.messages?.length ?? 0).toBeGreaterThan(0);
expect(sessionPayload.session?.firstUserMessagePreview ?? "").toContain("MDTODO");
const links = await page.request.get("/v1/project-management/workbench-links?taskRef=mdtodo:hwlab-v03-mdtodo:file_plan:R1");
expect(links.ok()).toBeTruthy();
const linksPayload = await links.json();
expect(linksPayload.links.some((link: { sessionId?: string }) => link.sessionId === sessionId)).toBeTruthy();
await saveScreenshot(workbenchPage, testInfo, "project-management-workbench-launch");
});
test("restores task 205 deep link and incrementally loads the bounded task tree", async ({ page }) => {
const longTasks = Array.from({ length: 205 }, (_, index) => {
const taskId = `R${index + 1}`;
return {
taskRef: `mdtodo:hwlab-v03-mdtodo:file_long:${taskId}`,
projectId: "project_hwlab_v03",
sourceId: "hwlab-v03-mdtodo",
fileRef: "file_long",
taskId,
title: `Long tree task ${index + 1}`,
status: index === 204 ? "in_progress" : "open",
parentTaskRef: null,
depth: 0,
ordinal: index + 1,
bodyPreview: `Long tree body ${index + 1}`,
sourceFingerprint: "sha256:e2e-long"
};
});
await page.route("**/v1/project-management/mdtodo/files?**", async (route) => {
const upstream = await route.fetch();
const body = await upstream.json() as { files?: Array<Record<string, unknown>> };
await route.fulfill({ response: upstream, json: { ...body, files: [...(body.files ?? []), { sourceId: "hwlab-v03-mdtodo", fileRef: "file_long", projectId: "project_hwlab_v03", relativePath: "docs/MDTODO/long-tree.md", title: "205 task fixture", taskCount: 205, fingerprint: "sha256:e2e-long" }] } });
});
await page.route("**/v1/project-management/mdtodo/tasks?**", async (route) => {
const url = new URL(route.request().url());
if (url.searchParams.get("fileRef") !== "file_long") return route.continue();
const exactRef = url.searchParams.get("taskRef");
const search = (url.searchParams.get("search") ?? "").toLowerCase();
const status = url.searchParams.get("status");
const offset = Number(url.searchParams.get("offset") ?? 0);
const limit = Number(url.searchParams.get("limit") ?? 100);
const matched = longTasks.filter((task) => (!exactRef || task.taskRef === exactRef) && (!search || [task.taskId, task.title, task.taskRef].some((value) => value.toLowerCase().includes(search))) && (!status || task.status === status));
const tasks = matched.slice(offset, offset + limit);
await route.fulfill({ status: 200, contentType: "application/json", json: { ok: true, contractVersion: "project-management-v1", serviceId: "hwlab-project-management", tasks, page: { offset, limit, maxLimit: 500, total: matched.length, hasMore: offset + tasks.length < matched.length }, valuesRedacted: true } });
});
await page.route("**/v1/project-management/mdtodo/task-detail?**", async (route) => {
const url = new URL(route.request().url());
const task = longTasks.find((item) => item.taskRef === url.searchParams.get("taskRef"));
if (!task) return route.continue();
await route.fulfill({ status: 200, contentType: "application/json", json: { ok: true, contractVersion: "project-management-v1", serviceId: "hwlab-project-management", task, detail: { ...task, body: task.bodyPreview, links: [], valuesRedacted: false }, valuesRedacted: true } });
});
await page.goto("/projects/mdtodo/sources/hwlab-v03-mdtodo/files/file_long/tasks/R205");
await expect(page).toHaveURL((url) => url.pathname.endsWith("/files/file_long/tasks/R205"));
await expect(page.getByTestId("mdtodo-title-read")).toContainText("Long tree task 205");
await expect(page.locator('[data-task-id="R205"]')).toBeVisible();
await expect(page.getByTestId("mdtodo-task-page-fact")).toContainText("101 / 205");
await page.getByTestId("mdtodo-load-more").click();
await expect(page.getByTestId("mdtodo-task-page-fact")).toContainText("201 / 205");
await page.getByTestId("mdtodo-load-more").click();
await expect(page.getByTestId("mdtodo-load-more")).toHaveCount(0);
await expect(page.locator('[data-task-id="R204"]')).toBeVisible();
await expect(page).toHaveURL((url) => url.pathname.endsWith("/files/file_long/tasks/R205") && url.searchParams.get("provider") === "codex-api");
});
test("keeps an offline HWPOD projection read-only without requesting live reports", async ({ page }) => {
let reportPreviewRequests = 0;
await page.route("**/v1/project-management/mdtodo/task-detail?**", async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
json: {
ok: true,
contractVersion: "project-management-v1",
serviceId: "hwlab-project-management",
status: "partial",
detail: null,
detailAuthority: {
state: "unavailable",
authority: "project-management-projection",
sourceKind: "hwpod-workspace",
code: "hwpod_node_offline",
message: "HWPOD 源当前不可读;页面继续展示最近一次项目投影,正文编辑和报告预览需等待 Node 恢复。",
retryable: true,
valuesRedacted: true
},
valuesRedacted: true
}
});
});
await page.route("**/v1/project-management/mdtodo/report-preview?**", async (route) => {
reportPreviewRequests += 1;
await route.fulfill({ status: 503, contentType: "application/json", json: { ok: false, code: "hwpod_node_offline", message: "must not be requested" } });
});
await page.goto("/projects/mdtodo/sources/hwlab-v03-mdtodo/files/file_plan/tasks/R1");
await expect(page.getByTestId("mdtodo-task-detail")).toContainText("offline projection");
await expect(page.getByTestId("mdtodo-title-read")).toBeDisabled();
await expect(page.getByTestId("mdtodo-edit-status")).toBeDisabled();
await expect(page.getByTestId("mdtodo-delete-task")).toBeDisabled();
await expect(page.getByTestId("mdtodo-new-task-open")).toBeDisabled();
await page.getByTestId("mdtodo-body-read").dblclick({ force: true });
await expect(page.getByTestId("mdtodo-body-editor")).toHaveCount(0);
await expect(page.getByTestId("mdtodo-report-link")).toBeDisabled();
await expect(page.getByTestId("mdtodo-task-detail")).toContainText(/报告索引|Node 恢复/u);
expect(reportPreviewRequests).toBe(0);
await expect(page.getByTestId("mdtodo-launch-workbench")).toBeEnabled();
});
});