import { expect, test } from "@playwright/test"; import { makeView } from "../fixtures"; const view = makeView(2, 3, 2, "success"); const artifactDetail = { businessSections: [{ id: "summary", title: "主脚本规模", content: "1 个段落、1 个元素、1 条关联" }], technical: { source: "read-only-test" }, changes: { summary: "当前主脚本内容", sections: [], artifactRef: "artifact:base:current", exactness: "current-only" }, scriptTable: { snapshotRef: "artifact:base:current", exactness: "current-only", sourceOrigin: "database", counts: { paragraphs: 1, elements: 1, links: 1 }, maxDepth: 0, paragraphs: [{ id: 1, rowId: 1, index: 1, level: 1, depth: 0, name: "机制拆解", contentRange: ["正文: 第二段"], sections: { theme: { label: "主题", description: "解释份额换订单", descriptionParts: [{ type: "text", text: "解释份额换订单" }], dimensions: [{ type: "主维度", name: "核心竞争力", value: "份额换订单" }] }, form: { label: "形式", description: "案例拆解", descriptionParts: [{ type: "text", text: "案例拆解" }], dimensions: [] }, function: { label: "作用", description: "承接上文", descriptionParts: [{ type: "text", text: "承接上文" }], dimensions: [] }, feeling: { label: "感受", description: "建立可信度", descriptionParts: [{ type: "text", text: "建立可信度" }], dimensions: [] }, }, fullDescription: "通过份额换订单解释机制。", fullDescriptionParts: [{ type: "text", text: "通过" }, { type: "element", id: 10, name: "份额换订单", resolved: true, text: "[元素:10]" }, { type: "text", text: "解释机制。" }], }], elements: [{ id: 10, rowId: 10, name: "份额换订单", primaryDimension: "核心竞争力", secondaryDimension: "商业模式", paragraphs: [{ id: 1, index: 1, name: "机制拆解" }] }], }, }; test.beforeEach(async ({ page }) => { await page.route("**/api/**", async (route) => { const path = new URL(route.request().url()).pathname; if (path === "/api/script-builds") { await route.fulfill({ json: [{ id: view.header.id, status: "success", currentRound: 2, createdAt: view.header.createdAt }] }); return; } if (path.endsWith("/execution-view")) { await route.fulfill({ json: view }); return; } if (path.includes("/artifacts/")) { await route.fulfill({ json: artifactDetail }); return; } if (path.includes("/decision-prompts/")) { await route.fulfill({ json: { promptRef: "prompt:main", actor: { role: "main", label: "主 Agent" }, systemPrompt: { content: "当前主 Agent 规则正文。", source: "current-db", version: 8, accuracy: "current-not-run-snapshot", truncated: false, }, notices: [{ code: "PROMPT_NOT_SNAPSHOTTED", message: "系统没有保存这次运行所用提示词的历史快照;下方展示当前提示词。" }], } }); return; } if (path.includes("/inspector-view/")) { await route.fulfill({ json: { schemaVersion: "inspector-source-v2", detailRef: "event:test", cardKind: "test", completeness: "complete", businessProjection: { businessSections: [{ id: "summary", title: "业务记录", content: "这是按需读取的完整业务记录。" }], }, modules: [{ id: "summary", title: "业务记录", businessPath: "/businessSections/0", presentation: "text", rows: [{ id: "summary:content", businessSelector: "/businessSections/0/content", bindingIds: ["summary:source:1"], }], bindings: [{ id: "summary:source:1", sourceId: "event:test", role: "output", selector: { kind: "json-pointer", path: "/output/content" }, transform: { kind: "direct" }, evidence: { availability: "produced-by-run", adoption: "not-applicable", confidence: "exact" }, resolution: "resolved", }], runtimeRefs: ["event:test"], defaultOpen: true, }], sources: { "event:test": { id: "event:test", kind: "runtime-event", label: "测试 Event", locator: { eventId: "test" }, completeness: "complete", resultState: "present", truncated: false, selectedValues: [{ bindingId: "summary:source:1", value: "这是按需读取的完整业务记录。" }], rawRecord: { output: { content: "这是按需读取的完整业务记录。" } }, }, }, notices: [], } }); return; } await route.fulfill({ json: { businessSections: [{ id: "summary", title: "业务记录", content: "这是按需读取的完整业务记录。" }], technical: { source: "read-only-test" }, }, }); }); await page.goto("/"); }); test("V8 在桌面与平板呈现发散、取数、取舍再收敛", async ({ page }, testInfo) => { test.skip(testInfo.project.name.includes("mobile"), "移动端使用轮次列表"); await expect(page.getByRole("heading", { name: "脚本构建" })).toBeVisible(); await expect(page.locator(".react-flow__minimap")).toHaveCount(0); await page.getByRole("button", { name: /展开本轮/ }).first().click(); await expect(page.getByText("主 Agent · 规划", { exact: true })).toBeVisible(); await expect(page.getByText("实现 Agent · 多路方案", { exact: true })).toBeVisible(); await expect(page.getByText("多方案评审 Agent", { exact: true }).first()).toBeAttached(); await expect(page.getByText("主 Agent · 收敛", { exact: true })).toBeAttached(); await expect(page.getByText("整体评审 Agent", { exact: true }).first()).toBeAttached(); await expect(page.getByTestId("rf__node-multipath-review-1-1").getByText("多方案评审", { exact: true })).toBeAttached(); await expect(page.getByTestId("rf__node-multipath-1-1").getByText("第 1 批决策", { exact: true })).toBeAttached(); await expect(page.getByTestId("rf__node-multipath-1-2").getByText("第 2 批决策", { exact: true })).toBeAttached(); await page.getByRole("button", { name: /展开方案过程/ }).first().click(); const canvas = page.locator(".flowShell"); await expect(canvas.getByText("取数阶段", { exact: true }).first()).toBeAttached(); await expect(canvas.getByText("工具取数", { exact: true }).first()).toBeAttached(); await expect(canvas.getByText("Agent 取数", { exact: true }).first()).toBeAttached(); await expect(canvas.getByText("数据取舍", { exact: true }).first()).toBeAttached(); const details = page.getByRole("button", { name: /查看详情/ }).first(); await expect(details).toBeVisible(); await details.click(); await expect(page.getByRole("dialog", { name: /三列来源详情/ })).toBeVisible(); await expect(page.getByRole("columnheader", { name: "业务详情" })).toBeVisible(); await expect(page.getByRole("columnheader", { name: "数据依据" })).toBeVisible(); await expect(page.getByRole("columnheader", { name: "原始记录与运行" })).toBeVisible(); }); test("收敛卡在全量、紧凑和概览缩放下都不越出代理区域", async ({ page }, testInfo) => { test.skip(testInfo.project.name.includes("mobile"), "移动端使用轮次列表"); await page.getByRole("button", { name: /展开本轮/ }).first().click(); const assertContained = async (cardSelector: string, regionId: string) => { const region = page.locator(`[data-id="${regionId}"]`); const cards = page.locator(cardSelector); await expect(region).toBeAttached(); await expect(cards.first()).toBeAttached(); const regionBox = await region.boundingBox(); expect(regionBox).not.toBeNull(); for (let index = 0; index < await cards.count(); index += 1) { const box = await cards.nth(index).boundingBox(); expect(box).not.toBeNull(); expect(box!.y).toBeGreaterThanOrEqual(regionBox!.y - 1); expect(box!.y + box!.height).toBeLessThanOrEqual(regionBox!.y + regionBox!.height + 1); } }; const assertConvergenceLayout = async () => { await assertContained('[data-id^="multipath-review-"]', "region:round:1:multipath-review"); await assertContained('[data-id^="multipath-1-"]', "region:round:1:convergence"); }; await assertConvergenceLayout(); await page.locator(".react-flow__controls-zoomout").click(); await page.locator(".react-flow__controls-zoomout").click(); await expect(page.locator(".flowShell")).toHaveAttribute("data-detail-level", "compact"); await assertConvergenceLayout(); await page.locator(".react-flow__controls-zoomout").click(); await page.locator(".react-flow__controls-zoomout").click(); await page.locator(".react-flow__controls-zoomout").click(); await expect(page.locator(".flowShell")).toHaveAttribute("data-detail-level", "overview"); await assertConvergenceLayout(); await expect(page.locator(".react-flow__node-agentRegion")).toHaveCount(6); }); test("Agent 规则抽屉明确区分当前规则与历史运行快照", async ({ page }, testInfo) => { test.skip(testInfo.project.name.includes("mobile"), "移动端由组件测试覆盖规则抽屉"); const objective = page.getByTestId("rf__node-objective"); await objective.getByRole("button", { name: "查看详情" }).click(); const inspector = page.getByRole("dialog", { name: /三列来源详情$/ }); await expect(inspector).toBeVisible(); const trigger = inspector.getByRole("button", { name: "查看主 Agent 提示词" }); await expect(trigger).toBeVisible(); await trigger.click(); const dialog = page.getByRole("dialog", { name: "Agent 任务与提示词" }); await expect(dialog).toBeVisible(); await expect(dialog.getByRole("heading", { name: "当前提示词" })).toBeVisible(); await expect(dialog.getByText(/没有保存这次运行所用提示词的历史快照/)).toBeVisible(); await expect(dialog.getByText("当前数据库版本 · v8")).toBeVisible(); await page.keyboard.press("Escape"); await expect(dialog).toHaveCount(0); await expect(inspector).toBeVisible(); await expect(trigger).toBeFocused(); }); test("V8 移动端按轮次阅读并能展开取数树", async ({ page }, testInfo) => { test.skip(!testInfo.project.name.includes("mobile"), "仅验证移动端信息架构"); await expect(page.getByRole("heading", { name: "这次构建经历了什么" })).toBeVisible(); await page.getByRole("button", { name: "展开第 1 轮" }).click(); await expect(page.getByText("候选方案 1", { exact: true })).toBeVisible(); await page.getByRole("button", { name: "展开方案过程" }).first().click(); await expect(page.getByText("取数阶段", { exact: true }).first()).toBeVisible(); await expect(page.getByText("Agent 取数", { exact: true }).first()).toBeVisible(); await expect(page.getByText("数据取舍", { exact: true }).first()).toBeVisible(); const mobile = page.locator(".mobileTimeline"); await expect(mobile.getByText("多方案评审 Agent · 评审判断", { exact: true }).first()).toBeVisible(); await expect(mobile.getByText("评审建议", { exact: true }).first()).toBeVisible(); await expect(mobile.getByText("主 Agent · 取舍决定", { exact: true }).first()).toBeVisible(); await expect(mobile.getByText("最终决定", { exact: true }).first()).toBeVisible(); await expect(mobile.getByText("整体评审 Agent · 评审判断", { exact: true }).first()).toBeVisible(); }); test("完整主脚本在桌面、平板和手机都使用独立可滚动查看器", async ({ page }) => { const trigger = page.getByRole("button", { name: "查看完整主脚本" }); await expect(trigger).toHaveCount(1); await trigger.focus(); await trigger.evaluate((element: HTMLElement) => element.click()); const dialog = page.getByRole("dialog"); await expect(dialog).toBeVisible(); await expect(dialog.getByText("1 个段落 · 1 个元素 · 1 条关联")).toBeVisible(); await expect(dialog.getByRole("columnheader", { name: "维度类型" }).first()).toBeVisible(); await expect(dialog.getByText("正文: 第二段")).toBeVisible(); await expect(dialog.getByText("主", { exact: true }).first()).toBeVisible(); await dialog.getByRole("tab", { name: "脚本元素 1" }).click(); await expect(dialog.getByRole("row", { name: /份额换订单/ })).toContainText("商业模式"); const dimensions = await dialog.evaluate((element) => ({ width: element.getBoundingClientRect().width, viewport: window.innerWidth })); expect(dimensions.width).toBeLessThanOrEqual(dimensions.viewport); await dialog.getByRole("button", { name: "关闭完整主脚本" }).click(); await expect(dialog).toHaveCount(0); await expect(trigger).toBeFocused(); });