refactor(*): 平台架构 - 拆分后端领域模块与画布前端组件并补充测试
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { moduleGroupSource } from "./helpers/module-group-source";
|
||||
|
||||
function compactSource(source: string) {
|
||||
return source.replace(/\s+/g, " ").trim();
|
||||
@@ -14,7 +15,7 @@ function sourceSection(source: string, startMarker: string, endMarker: string) {
|
||||
|
||||
test("announcement editor preserves image and pinned fields through edit and save", async () => {
|
||||
const [panelSource, safetySource] = await Promise.all([
|
||||
Bun.file(new URL("../src/pages/admin/components/admin-announcements-panel.tsx", import.meta.url)).text(),
|
||||
Promise.resolve(moduleGroupSource("pages/admin/components/admin-announcements-panel.tsx")),
|
||||
Bun.file(new URL("../src/pages/admin/components/admin-announcement-safety.ts", import.meta.url)).text(),
|
||||
]);
|
||||
const panel = compactSource(panelSource);
|
||||
@@ -132,7 +133,7 @@ test("analytics keeps range presets and uses order finances without a separate p
|
||||
});
|
||||
|
||||
test("storage settings keep generic S3 controls and connection validation", async () => {
|
||||
const source = await Bun.file(new URL("../src/pages/admin/settings/storage-settings-page.tsx", import.meta.url)).text();
|
||||
const source = await Promise.resolve(moduleGroupSource("pages/admin/settings/storage-settings-page.tsx"));
|
||||
const compacted = compactSource(source);
|
||||
|
||||
expect(compacted).toContain('{ mode: "s3", label: "S3 兼容存储"');
|
||||
@@ -178,11 +179,11 @@ test("admin settings use full-width summaries without selected-card side stripes
|
||||
|
||||
test("task-first settings reveal dependent configuration only after the primary choice", async () => {
|
||||
const [storageSource, emailSource, accessSource, featureSource, appearanceSource, welcomeSource, drawingSource, arkSource, interceptionSource, thirdPartySource, cssSource] = await Promise.all([
|
||||
Bun.file(new URL("../src/pages/admin/settings/storage-settings-page.tsx", import.meta.url)).text(),
|
||||
Promise.resolve(moduleGroupSource("pages/admin/settings/storage-settings-page.tsx")),
|
||||
Bun.file(new URL("../src/pages/admin/components/email-settings-panel.tsx", import.meta.url)).text(),
|
||||
Bun.file(new URL("../src/pages/admin/components/access-settings-panel.tsx", import.meta.url)).text(),
|
||||
Bun.file(new URL("../src/pages/admin/components/feature-availability-panel.tsx", import.meta.url)).text(),
|
||||
Bun.file(new URL("../src/pages/admin/settings/appearance-settings-page.tsx", import.meta.url)).text(),
|
||||
Promise.resolve(moduleGroupSource("pages/admin/settings/appearance-settings-page.tsx")),
|
||||
Bun.file(new URL("../src/pages/admin/settings/components/welcome-setting.tsx", import.meta.url)).text(),
|
||||
Bun.file(new URL("../src/pages/admin/settings/drawing-engine-settings-page.tsx", import.meta.url)).text(),
|
||||
Bun.file(new URL("../src/pages/admin/settings/ark-private-assets-settings-page.tsx", import.meta.url)).text(),
|
||||
|
||||
@@ -6,7 +6,8 @@ import { expect, test } from "bun:test";
|
||||
// (旧代码 `disabled={busy || running || …}`),策略提示词也据此写着「运行中用户无法打字」。
|
||||
// 所以这里的断言要盯住两件事:入口真的打开了,以及旧前提没有残留在代码里。
|
||||
test("运行中不再锁死输入框,发送走插话而不是新建轮次", async () => {
|
||||
const panel = await Bun.file(new URL("../src/components/canvas/canvas-cloud-agent-panel.tsx", import.meta.url)).text();
|
||||
// 面板拆分为主组件、展示组件与事件归约三个模块,源码约束覆盖整个模块组。
|
||||
const panel = (await Promise.all(["canvas-cloud-agent-panel.tsx", "canvas-cloud-agent-panel-parts.tsx", "canvas-cloud-agent-events.ts"].map((file) => Bun.file(new URL(`../src/components/canvas/${file}`, import.meta.url)).text()))).join("\n");
|
||||
|
||||
// 运行中走插话分支(不建轮次、不走那套扣费幂等记账)
|
||||
expect(panel).toContain("await interject(value)");
|
||||
@@ -31,7 +32,8 @@ test("运行中不再锁死输入框,发送走插话而不是新建轮次", as
|
||||
});
|
||||
|
||||
test("输入框运行中同时给出「发送(插话)」与「停止」", async () => {
|
||||
const composer = await Bun.file(new URL("../src/components/canvas/canvas-cloud-agent-chat-ui.tsx", import.meta.url)).text();
|
||||
// 输入区已拆到 canvas-cloud-agent-composer.tsx,与消息渲染一起检查。
|
||||
const composer = (await Promise.all(["canvas-cloud-agent-chat-ui.tsx", "canvas-cloud-agent-composer.tsx"].map((file) => Bun.file(new URL(`../src/components/canvas/${file}`, import.meta.url)).text()))).join("\n");
|
||||
|
||||
// 关键改动:canStop 不再等于 sending —— 否则运行中两个动作只能二选一
|
||||
expect(composer).toContain("const canStop = Boolean(running && onStop);");
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { expect, test } from "bun:test";
|
||||
|
||||
test("Agent 对话和设置复用创作页模型选择器,并且只展示文本模型", async () => {
|
||||
const panel = await Bun.file(new URL("../src/components/canvas/canvas-cloud-agent-panel.tsx", import.meta.url)).text();
|
||||
// 面板拆分为主组件、展示组件与事件归约三个模块,源码约束覆盖整个模块组。
|
||||
const panel = (await Promise.all(["canvas-cloud-agent-panel.tsx", "canvas-cloud-agent-panel-parts.tsx", "canvas-cloud-agent-events.ts"].map((file) => Bun.file(new URL(`../src/components/canvas/${file}`, import.meta.url)).text()))).join("\n");
|
||||
const settings = await Bun.file(new URL("../src/components/canvas/canvas-cloud-agent-settings.tsx", import.meta.url)).text();
|
||||
const css = await Bun.file(new URL("../src/components/canvas/canvas-cloud-agent.css", import.meta.url)).text();
|
||||
const pickerCss = await Bun.file(new URL("../src/styles/workspace-product.css", import.meta.url)).text();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { expect, test } from "bun:test";
|
||||
|
||||
import { appearanceLogoURL, normalizePublicAppearance } from "../src/stores/use-appearance-store";
|
||||
import { moduleGroupSource } from "./helpers/module-group-source";
|
||||
|
||||
test("initial HTML stays brand neutral until the public appearance is resolved", async () => {
|
||||
const [html, mainSource] = await Promise.all([Bun.file(new URL("../index.html", import.meta.url)).text(), Bun.file(new URL("../src/main.tsx", import.meta.url)).text()]);
|
||||
@@ -85,7 +86,7 @@ test("auth scene consumes resolved appearance instead of hardcoded media constan
|
||||
|
||||
test("appearance management exposes light and dark logo uploads plus the frame switch", async () => {
|
||||
const [pageSource, brandSource, adminStyles, globalStyles] = await Promise.all([
|
||||
Bun.file(new URL("../src/pages/admin/settings/appearance-settings-page.tsx", import.meta.url)).text(),
|
||||
Promise.resolve(moduleGroupSource("pages/admin/settings/appearance-settings-page.tsx")),
|
||||
Bun.file(new URL("../src/components/brand/brand-logo.tsx", import.meta.url)).text(),
|
||||
Bun.file(new URL("../src/styles/admin-ui.css", import.meta.url)).text(),
|
||||
Bun.file(new URL("../src/styles/globals.css", import.meta.url)).text(),
|
||||
@@ -113,7 +114,7 @@ test("appearance management exposes light and dark logo uploads plus the frame s
|
||||
});
|
||||
|
||||
test("object storage can adopt the configured English brand identifier without replacing saved prefixes automatically", async () => {
|
||||
const source = await Bun.file(new URL("../src/pages/admin/settings/storage-settings-page.tsx", import.meta.url)).text();
|
||||
const source = await Promise.resolve(moduleGroupSource("pages/admin/settings/storage-settings-page.tsx"));
|
||||
|
||||
expect(source).toContain("state.appearance.brandSlug");
|
||||
expect(source).toContain("使用品牌标识");
|
||||
@@ -122,7 +123,7 @@ test("object storage can adopt the configured English brand identifier without r
|
||||
});
|
||||
|
||||
test("appearance management exposes a server-side reset to the built-in Yingce brand", async () => {
|
||||
const [pageSource, apiSource] = await Promise.all([Bun.file(new URL("../src/pages/admin/settings/appearance-settings-page.tsx", import.meta.url)).text(), Bun.file(new URL("../src/services/api/appearance.ts", import.meta.url)).text()]);
|
||||
const [pageSource, apiSource] = await Promise.all([Promise.resolve(moduleGroupSource("pages/admin/settings/appearance-settings-page.tsx")), Bun.file(new URL("../src/services/api/appearance.ts", import.meta.url)).text()]);
|
||||
|
||||
expect(pageSource).toContain("恢复影策默认");
|
||||
expect(pageSource).toContain("resetAdminAppearance()");
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
import { moduleGroupSource } from "./helpers/module-group-source";
|
||||
|
||||
describe("asset library category sidebar", () => {
|
||||
test("keeps type, business and folder filters in a left-hand nav", () => {
|
||||
const page = readFileSync(resolve(import.meta.dir, "../src/pages/assets/index.tsx"), "utf8");
|
||||
const page = moduleGroupSource("pages/assets/index.tsx");
|
||||
const css = readFileSync(resolve(import.meta.dir, "../src/styles/workspace-product.css"), "utf8");
|
||||
expect(page).toContain('className="assets-collection-layout"');
|
||||
expect(page).toContain('aria-label="素材分类"');
|
||||
|
||||
@@ -25,7 +25,8 @@ describe("assets page batch toolbar", () => {
|
||||
});
|
||||
|
||||
test("places select all before cancel selection", () => {
|
||||
const source = readFileSync(resolve(import.meta.dir, "../src/pages/assets/index.tsx"), "utf8");
|
||||
// 批量操作条随素材库拆分移到 asset-library-panels.tsx。
|
||||
const source = readFileSync(resolve(import.meta.dir, "../src/pages/assets/asset-library-panels.tsx"), "utf8");
|
||||
const selectAllIndex = source.search(/>\s*全选\s*<\/Button>/);
|
||||
const clearSelectionIndex = source.search(/>\s*取消选择\s*<\/Button>/);
|
||||
|
||||
|
||||
@@ -4,9 +4,15 @@ import { resolve } from "node:path";
|
||||
import { createPortraitTextureNode, isCanvasImageSourceNode } from "../src/lib/canvas/canvas-image-source";
|
||||
import { canGenerateImageInPlace } from "../src/lib/canvas/canvas-generation-layout";
|
||||
import { CanvasNodeType, type CanvasNodeData } from "../src/types/canvas";
|
||||
import { moduleGroupSource } from "./helpers/module-group-source";
|
||||
|
||||
const source: CanvasNodeData = {
|
||||
id: "uploaded", type: CanvasNodeType.Image, title: "原图", position: { x: 10, y: 20 }, width: 300, height: 200,
|
||||
id: "uploaded",
|
||||
type: CanvasNodeType.Image,
|
||||
title: "原图",
|
||||
position: { x: 10, y: 20 },
|
||||
width: 300,
|
||||
height: 200,
|
||||
metadata: { content: "original-image", storageKey: "original-key", status: "success" },
|
||||
};
|
||||
const read = (path: string) => readFileSync(resolve(import.meta.dir, "../src", path), "utf8");
|
||||
@@ -42,7 +48,7 @@ describe("上传图片是输入素材", () => {
|
||||
expect(canGenerateImageInPlace(source)).toBe(false);
|
||||
});
|
||||
test("页面和工具入口共享素材判定,人物质感接入原图连线", () => {
|
||||
expect(flat(read("pages/canvas/project.tsx"))).toContain("dialogNode && !isCanvasImageSourceNode(dialogNode)");
|
||||
expect(flat(moduleGroupSource("pages/canvas/project.tsx"))).toContain("dialogNode && !isCanvasImageSourceNode(dialogNode)");
|
||||
expect(read("lib/canvas/tool-registry/definitions/node-hover-tools.tsx")).toContain("!isCanvasImageSourceNode(ctx.node)");
|
||||
const mediaTools = read("pages/canvas/use-canvas-media-tools.ts");
|
||||
const portrait = mediaTools.slice(mediaTools.indexOf("const openPortraitTextureEditor"), mediaTools.indexOf("const cropImageNode"));
|
||||
|
||||
@@ -9,19 +9,20 @@ import { canvasNodeVideoPreviewReference, canvasNodeVideoPreviewUrl, canvasVideo
|
||||
import { collectImageStorageKeys } from "../src/services/image-storage";
|
||||
import { CanvasNodeType, type CanvasNodeData } from "../src/types/canvas";
|
||||
import { detectVideoAudioTrack, detectVideoAudioTrackFromBlob, detectVideoAudioTrackFromUrl } from "../src/lib/video-poster";
|
||||
import { moduleGroupSource } from "./helpers/module-group-source";
|
||||
|
||||
const canvasNodeContentSource = readFileSync(resolve(import.meta.dir, "../src/components/canvas/canvas-node-content.tsx"), "utf8");
|
||||
const canvasNodeContentSource = moduleGroupSource("components/canvas/canvas-node-content.tsx");
|
||||
const canvasAudioPlayerSource = readFileSync(resolve(import.meta.dir, "../src/components/canvas/canvas-audio-player.tsx"), "utf8");
|
||||
const canvasMentionSource = readFileSync(resolve(import.meta.dir, "../src/components/canvas/canvas-resource-mention-textarea.tsx"), "utf8");
|
||||
const canvasMentionSource = moduleGroupSource("components/canvas/canvas-resource-mention-textarea.tsx");
|
||||
const canvasNodeSource = readFileSync(resolve(import.meta.dir, "../src/components/canvas/canvas-node.tsx"), "utf8");
|
||||
const canvasVideoPreviewSource = readFileSync(resolve(import.meta.dir, "../src/services/canvas-video-preview.ts"), "utf8");
|
||||
const browserDownloadSource = readFileSync(resolve(import.meta.dir, "../src/services/browser-download.ts"), "utf8");
|
||||
const canvasNodeEditorSource = readFileSync(resolve(import.meta.dir, "../src/pages/canvas/use-canvas-node-editor.ts"), "utf8");
|
||||
const assetLibrarySource = readFileSync(resolve(import.meta.dir, "../src/pages/assets/index.tsx"), "utf8");
|
||||
const assetLibrarySource = moduleGroupSource("pages/assets/index.tsx");
|
||||
const projectAssetsSource = readFileSync(resolve(import.meta.dir, "../src/pages/projects/detail/assets.tsx"), "utf8");
|
||||
const workflowProductionSource = readFileSync(resolve(import.meta.dir, "../src/pages/projects/detail/workflow-production-workbench.tsx"), "utf8");
|
||||
const videoPlayerSource = readFileSync(resolve(import.meta.dir, "../src/components/video-player.tsx"), "utf8");
|
||||
const canvasProjectSource = readFileSync(resolve(import.meta.dir, "../src/pages/canvas/project.tsx"), "utf8");
|
||||
const canvasProjectSource = moduleGroupSource("pages/canvas/project.tsx");
|
||||
const globalStylesSource = readFileSync(resolve(import.meta.dir, "../src/styles/globals.css"), "utf8");
|
||||
|
||||
function node(id: string, type: CanvasNodeType): CanvasNodeData {
|
||||
@@ -75,7 +76,7 @@ describe("large canvas media rendering", () => {
|
||||
});
|
||||
|
||||
test("keeps inactive video nodes on a viewport-gated static first frame", () => {
|
||||
const inactivePreviewSource = canvasNodeContentSource.match(/function InactiveVideoPreview[\s\S]*?\n}\n\nfunction VideoPreviewPlayButton/)?.[0] || "";
|
||||
const inactivePreviewSource = canvasNodeContentSource.match(/function InactiveVideoPreview[\s\S]*?\n}\n\n(?:export )?function VideoPreviewPlayButton/)?.[0] || "";
|
||||
expect(canvasNodeContentSource).toContain("if (hasPersistedPreview || !nearViewport || (!node.metadata?.content && !node.metadata?.storageKey) || !updateMetadataRef.current)");
|
||||
expect(canvasNodeContentSource).not.toContain("hydrateMediaPreview");
|
||||
expect(inactivePreviewSource).toContain("hasPersistedPreview || !nearViewport");
|
||||
|
||||
@@ -2,8 +2,9 @@ import { readFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { moduleGroupSource } from "./helpers/module-group-source";
|
||||
|
||||
const projectSource = readFileSync(resolve(import.meta.dir, "../src/pages/canvas/project.tsx"), "utf8");
|
||||
const projectSource = moduleGroupSource("pages/canvas/project.tsx");
|
||||
const selectionControllerSource = readFileSync(resolve(import.meta.dir, "../src/pages/canvas/use-canvas-selection-controller.ts"), "utf8");
|
||||
const flat = (text: string) => text.replace(/\s+/g, " ");
|
||||
|
||||
|
||||
@@ -3,8 +3,9 @@ import { readFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
import { bringCanvasNodeToFront, sortCanvasNodesByStackOrder } from "../src/lib/canvas/canvas-node-stack-order";
|
||||
import { moduleGroupSource } from "./helpers/module-group-source";
|
||||
|
||||
const projectSource = readFileSync(resolve(import.meta.dir, "../src/pages/canvas/project.tsx"), "utf8");
|
||||
const projectSource = moduleGroupSource("pages/canvas/project.tsx");
|
||||
const worldLayersSource = readFileSync(resolve(import.meta.dir, "../src/pages/canvas/canvas-project-world-layers.tsx"), "utf8");
|
||||
const selectionControllerSource = readFileSync(resolve(import.meta.dir, "../src/pages/canvas/use-canvas-selection-controller.ts"), "utf8");
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
import { moduleGroupSource } from "./helpers/module-group-source";
|
||||
|
||||
function source(path: string) {
|
||||
return readFileSync(resolve(import.meta.dir, path), "utf8");
|
||||
@@ -8,10 +9,13 @@ function source(path: string) {
|
||||
|
||||
describe("canvas resource mention editor", () => {
|
||||
test("refreshes async reference previews even when prompt text has not changed", () => {
|
||||
const component = source("../src/components/canvas/canvas-resource-mention-textarea.tsx");
|
||||
const component = moduleGroupSource("components/canvas/canvas-resource-mention-textarea.tsx");
|
||||
const unchangedTextBranch = component.match(/if \(currentValue === value && lastRenderedValueRef.current === value\) \{([^}]+)\}/)?.[1] || "";
|
||||
expect(unchangedTextBranch).toContain("syncInlineMentionPreviews(editor, activeReferences)");
|
||||
const sync = component.slice(component.indexOf("function syncInlineMentionPreviews("), component.indexOf("function MentionMenu("));
|
||||
// syncInlineMentionPreviews 随拆分移到 canvas-mention-chips.tsx;只截取该函数本体做断言。
|
||||
const chips = source("../src/components/canvas/canvas-mention-chips.tsx");
|
||||
const syncStart = chips.indexOf("function syncInlineMentionPreviews(");
|
||||
const sync = chips.slice(syncStart, chips.indexOf("\n}\n", syncStart) + 2);
|
||||
expect(sync).toContain('byId.get(chip.dataset.mentionReferenceId || "")');
|
||||
expect(sync).toContain('preview.getAttribute("src") !== src');
|
||||
expect(sync).toContain("preview.replaceWith(createInlinePreview(reference))");
|
||||
@@ -20,7 +24,7 @@ describe("canvas resource mention editor", () => {
|
||||
});
|
||||
|
||||
test("uses stable component classes for inline media references", () => {
|
||||
const component = source("../src/components/canvas/canvas-resource-mention-textarea.tsx");
|
||||
const component = moduleGroupSource("components/canvas/canvas-resource-mention-textarea.tsx");
|
||||
|
||||
expect(component).toContain("chip.className = `canvas-resource-inline-mention");
|
||||
expect(component).toContain("canvas-resource-inline-preview is-${reference.kind}");
|
||||
@@ -40,7 +44,7 @@ describe("canvas resource mention editor", () => {
|
||||
});
|
||||
|
||||
test("exposes inline image references as replacement drop targets", () => {
|
||||
const component = source("../src/components/canvas/canvas-resource-mention-textarea.tsx");
|
||||
const component = moduleGroupSource("components/canvas/canvas-resource-mention-textarea.tsx");
|
||||
const css = source("../src/styles/globals.css");
|
||||
|
||||
expect(component).toContain("activeDropReferenceId?: string | null");
|
||||
@@ -53,10 +57,10 @@ describe("canvas resource mention editor", () => {
|
||||
});
|
||||
|
||||
test("resolves storage-backed previews and renders a visible loading spinner", () => {
|
||||
const editor = source("../src/components/canvas/canvas-resource-mention-textarea.tsx");
|
||||
const panel = source("../src/components/canvas/canvas-node-prompt-panel.tsx");
|
||||
const editor = moduleGroupSource("components/canvas/canvas-resource-mention-textarea.tsx");
|
||||
const panel = moduleGroupSource("components/canvas/canvas-node-prompt-panel.tsx");
|
||||
const configComposer = source("../src/components/canvas/canvas-config-composer.tsx");
|
||||
const project = source("../src/pages/canvas/project.tsx");
|
||||
const project = moduleGroupSource("pages/canvas/project.tsx");
|
||||
|
||||
expect(editor).toContain("useResolvedCanvasResourceReferences");
|
||||
expect(panel).toContain("<LoaderCircle className=");
|
||||
@@ -84,7 +88,7 @@ describe("canvas resource mention editor", () => {
|
||||
});
|
||||
|
||||
test("anchors the mention menu to the caret instead of the textarea edge", () => {
|
||||
const component = source("../src/components/canvas/canvas-resource-mention-textarea.tsx");
|
||||
const component = moduleGroupSource("components/canvas/canvas-resource-mention-textarea.tsx");
|
||||
|
||||
expect(component).toContain("cursorOffset={mention.end}");
|
||||
expect(component).toContain("mentionCaretRect(anchor, cursorOffset)");
|
||||
@@ -94,7 +98,7 @@ describe("canvas resource mention editor", () => {
|
||||
});
|
||||
|
||||
test("renders skill references as descriptive workflow rows", () => {
|
||||
const component = source("../src/components/canvas/canvas-resource-mention-textarea.tsx");
|
||||
const component = moduleGroupSource("components/canvas/canvas-resource-mention-textarea.tsx");
|
||||
const css = source("../src/styles/globals.css");
|
||||
|
||||
expect(component).toContain('reference.kind === "skill" ? "is-skill" : ""');
|
||||
@@ -106,7 +110,7 @@ describe("canvas resource mention editor", () => {
|
||||
});
|
||||
|
||||
test("agent composer attachments stay large, previewable, and mentionable", () => {
|
||||
const component = source("../src/components/canvas/canvas-cloud-agent-chat-ui.tsx");
|
||||
const component = moduleGroupSource("components/canvas/canvas-cloud-agent-chat-ui.tsx") + source("../src/components/canvas/canvas-cloud-agent-composer.tsx");
|
||||
expect(component).toContain("w-20 shrink-0");
|
||||
expect(component).toContain("insertAttachmentMention");
|
||||
expect(component).toContain("@[attachment:");
|
||||
@@ -116,7 +120,7 @@ describe("canvas resource mention editor", () => {
|
||||
});
|
||||
|
||||
test("agent composer renders slash skill references as stable skill chips and consumes the typed slash query", () => {
|
||||
const component = source("../src/components/canvas/canvas-cloud-agent-chat-ui.tsx");
|
||||
const component = moduleGroupSource("components/canvas/canvas-cloud-agent-chat-ui.tsx") + source("../src/components/canvas/canvas-cloud-agent-composer.tsx");
|
||||
|
||||
expect(component).toContain("const token = `@[skill:${skill.skillId}] `");
|
||||
expect(component).toContain("slash.start + 1 + slash.query.length");
|
||||
@@ -124,12 +128,12 @@ describe("canvas resource mention editor", () => {
|
||||
expect(component).toContain("[/、]([^\\s/、]*)$");
|
||||
// 保留主分支已恢复的固定 Skills 提示,不能因合并旧分支退回失效的外观配置断言。
|
||||
expect(source("../src/lib/canvas/agent-appearance.ts")).toContain("用 / 或 、 引用 Skills");
|
||||
expect(source("../src/components/canvas/canvas-cloud-agent-panel.tsx")).toContain("用 / 或 、 引用 Skills");
|
||||
expect(moduleGroupSource("components/canvas/canvas-cloud-agent-panel.tsx") + source("../src/components/canvas/canvas-cloud-agent-panel-parts.tsx")).toContain("用 / 或 、 引用 Skills");
|
||||
});
|
||||
|
||||
test("skill chips use one colored icon instead of exposing the serialized token", () => {
|
||||
const component = source("../src/components/canvas/canvas-resource-mention-textarea.tsx");
|
||||
const chat = source("../src/components/canvas/canvas-cloud-agent-chat-ui.tsx");
|
||||
const component = moduleGroupSource("components/canvas/canvas-resource-mention-textarea.tsx");
|
||||
const chat = moduleGroupSource("components/canvas/canvas-cloud-agent-chat-ui.tsx") + source("../src/components/canvas/canvas-cloud-agent-composer.tsx");
|
||||
const css = source("../src/components/canvas/canvas-cloud-agent.css");
|
||||
|
||||
expect(component).toContain('const isDecorated = reference.kind === "skill" || reference.kind === "tool"');
|
||||
|
||||
@@ -1,7 +1,19 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
|
||||
import { applyCanvasSelectionStrategy, canvasSelectionHitsBounds, createCanvasSelectionBounds, createCanvasSelectionSpatialIndexCache, offsetSelectedNodeBounds, resolveCanvasPointerIntent, resolveCanvasSelectionHitMode, resolveCanvasSelectionPreviewDelta, resolveCanvasSelectionStrategy, selectedNodesWorldBounds } from "@/lib/canvas/canvas-selection";
|
||||
import {
|
||||
applyCanvasSelectionStrategy,
|
||||
canvasSelectionHitsBounds,
|
||||
createCanvasSelectionBounds,
|
||||
createCanvasSelectionSpatialIndexCache,
|
||||
offsetSelectedNodeBounds,
|
||||
resolveCanvasPointerIntent,
|
||||
resolveCanvasSelectionHitMode,
|
||||
resolveCanvasSelectionPreviewDelta,
|
||||
resolveCanvasSelectionStrategy,
|
||||
selectedNodesWorldBounds,
|
||||
} from "@/lib/canvas/canvas-selection";
|
||||
import { CanvasNodeType, type CanvasNodeData } from "@/types/canvas";
|
||||
import { moduleGroupSource } from "./helpers/module-group-source";
|
||||
|
||||
describe("canvas selection semantics", () => {
|
||||
test("maps modifiers to replace, add, toggle, and subtract strategies", () => {
|
||||
@@ -78,7 +90,7 @@ describe("canvas selection semantics", () => {
|
||||
});
|
||||
|
||||
test("uses region selection as the page default and commits selection outside pointer-move", async () => {
|
||||
const projectSource = await Bun.file(new URL("../src/pages/canvas/project.tsx", import.meta.url)).text();
|
||||
const projectSource = await Promise.resolve(moduleGroupSource("pages/canvas/project.tsx"));
|
||||
const controllerSource = await Bun.file(new URL("../src/pages/canvas/use-canvas-selection-controller.ts", import.meta.url)).text();
|
||||
const canvasSource = await Bun.file(new URL("../src/components/canvas/infinite-canvas.tsx", import.meta.url)).text();
|
||||
const graphicsSource = await Bun.file(new URL("../src/components/canvas/canvas-leafer-graphics-layer.tsx", import.meta.url)).text();
|
||||
@@ -91,22 +103,16 @@ describe("canvas selection semantics", () => {
|
||||
expect(canvasSource).toContain('"canvas-cursor-select"');
|
||||
expect(canvasSource).not.toContain('"cursor-crosshair"');
|
||||
expect(graphicsSource).toContain('fill: "transparent"');
|
||||
expect(graphicsSource).toContain('dashPattern: [4 / scale, 4 / scale]');
|
||||
expect(graphicsSource).toContain("dashPattern: [4 / scale, 4 / scale]");
|
||||
expect(globalStyles).toContain(".canvas-cursor-select");
|
||||
expect(globalStyles).not.toContain("filter='drop-shadow");
|
||||
});
|
||||
|
||||
test("offsets multi-select bounds with the live drag preview", () => {
|
||||
const bounds = selectedNodesWorldBounds([
|
||||
node("a", 100, 40),
|
||||
node("b", 300, 80),
|
||||
]);
|
||||
const bounds = selectedNodesWorldBounds([node("a", 100, 40), node("b", 300, 80)]);
|
||||
expect(bounds).toEqual({ left: 100, top: 40, width: 360, height: 130, count: 2 });
|
||||
expect(offsetSelectedNodeBounds(bounds!, { x: 40, y: -20 })).toEqual({ left: 140, top: 20, width: 360, height: 130, count: 2 });
|
||||
expect(selectedNodesWorldBounds([
|
||||
node("a", 100, 40),
|
||||
node("b", 300, 80),
|
||||
], { x: 40, y: -20, nodeIds: new Set(["a", "b"]) })).toEqual({ left: 140, top: 20, width: 360, height: 130, count: 2 });
|
||||
expect(selectedNodesWorldBounds([node("a", 100, 40), node("b", 300, 80)], { x: 40, y: -20, nodeIds: new Set(["a", "b"]) })).toEqual({ left: 140, top: 20, width: 360, height: 130, count: 2 });
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -4,9 +4,10 @@ import { resolve } from "node:path";
|
||||
|
||||
import { createGenerationTaskSubscriptionService, type GenerationTask } from "../src/services/api/task-center";
|
||||
import { removeCreationConversationSnapshot, updateCreationConversationSnapshot } from "../src/services/creation-conversation-store";
|
||||
import { moduleGroupSource } from "./helpers/module-group-source";
|
||||
|
||||
test("Create exposes one accessible copy action beside each displayed user prompt", async () => {
|
||||
const source = await Bun.file(new URL("../src/pages/create/creation-workspace.tsx", import.meta.url)).text();
|
||||
const source = await Promise.resolve(moduleGroupSource("pages/create/creation-workspace.tsx"));
|
||||
expect(source).toContain('aria-label="复制提示词"');
|
||||
expect(source).toContain('copyText(visiblePrompt, "提示词已复制")');
|
||||
});
|
||||
@@ -352,10 +353,10 @@ test("creation result handoff falls back by stable result order only for a compl
|
||||
});
|
||||
|
||||
test("Create forwards owned result assets through one new canvas and the project persists before clearing the handoff", () => {
|
||||
const workspace = readFileSync(resolve(import.meta.dir, "../src/pages/create/creation-workspace.tsx"), "utf8");
|
||||
const workspace = moduleGroupSource("pages/create/creation-workspace.tsx");
|
||||
const createPage = readFileSync(resolve(import.meta.dir, "../src/pages/create/index.tsx"), "utf8");
|
||||
const canvasIndex = readFileSync(resolve(import.meta.dir, "../src/pages/canvas/index.tsx"), "utf8");
|
||||
const canvasProject = readFileSync(resolve(import.meta.dir, "../src/pages/canvas/project.tsx"), "utf8");
|
||||
const canvasProject = moduleGroupSource("pages/canvas/project.tsx");
|
||||
|
||||
expect(workspace).toContain("onContinueCanvas(resultAssetIds)");
|
||||
expect(createPage).toContain("continueCreationConversationOnCanvas(source)");
|
||||
|
||||
@@ -3,6 +3,7 @@ import { readFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
import { removeCreationAttachment } from "../src/pages/create/creation-assets";
|
||||
import { moduleGroupSource } from "./helpers/module-group-source";
|
||||
|
||||
function compactSource(source: string) {
|
||||
return source.replace(/\s+/g, " ").trim();
|
||||
@@ -13,7 +14,7 @@ function readCreateSource() {
|
||||
}
|
||||
|
||||
function readCreateWorkspaceSource() {
|
||||
return readFileSync(resolve(import.meta.dir, "../src/pages/create/creation-workspace.tsx"), "utf8");
|
||||
return moduleGroupSource("pages/create/creation-workspace.tsx");
|
||||
}
|
||||
|
||||
describe("creation library button", () => {
|
||||
@@ -65,7 +66,7 @@ describe("creation library button", () => {
|
||||
|
||||
test("previews prompt reference images without removing them", () => {
|
||||
const createSource = readCreateWorkspaceSource();
|
||||
const canvasSource = readFileSync(resolve(import.meta.dir, "../src/components/canvas/canvas-node-prompt-panel.tsx"), "utf8");
|
||||
const canvasSource = moduleGroupSource("components/canvas/canvas-node-prompt-panel.tsx");
|
||||
|
||||
expect(createSource).toContain('className="creation-user-message-attachments"');
|
||||
expect(createSource).toContain('setPreviewType(kind === "video" ? "video" : "image")');
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
import { moduleGroupSource } from "./helpers/module-group-source";
|
||||
|
||||
/**
|
||||
* 生产接线回归:领域函数正确不代表用户能删到关键帧。
|
||||
* 这里锁住「时间轴入口 -> workbench -> 领域函数」这条链路真的接上了。
|
||||
*/
|
||||
const dock = readFileSync(resolve(import.meta.dir, "../src/components/canvas/director/director-viewport-dock.tsx"), "utf8");
|
||||
const workbench = readFileSync(resolve(import.meta.dir, "../src/components/canvas/director/canvas-director-workbench.tsx"), "utf8");
|
||||
const workbench = moduleGroupSource("components/canvas/director/canvas-director-workbench.tsx");
|
||||
const sequencer = readFileSync(resolve(import.meta.dir, "../src/components/canvas/director/director-sequencer.tsx"), "utf8");
|
||||
const styles = readFileSync(resolve(import.meta.dir, "../src/styles/globals.css"), "utf8");
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
type DirectorOnboardingProgress,
|
||||
type DirectorOnboardingStorage,
|
||||
} from "../src/lib/canvas/director/director-onboarding";
|
||||
import { moduleGroupSource } from "./helpers/module-group-source";
|
||||
|
||||
const ACTIVE: DirectorOnboardingProgress = directorOnboardingInitial;
|
||||
|
||||
@@ -551,8 +552,8 @@ describe("引导浮层契约", () => {
|
||||
});
|
||||
|
||||
describe("引导产品接线", () => {
|
||||
const projectCode = stripComments(readFileSync(resolve(import.meta.dir, "../src/pages/canvas/project.tsx"), "utf8"));
|
||||
const workbenchCode = stripComments(readFileSync(resolve(import.meta.dir, "../src/components/canvas/director/canvas-director-workbench.tsx"), "utf8"));
|
||||
const projectCode = stripComments(moduleGroupSource("pages/canvas/project.tsx"));
|
||||
const workbenchCode = stripComments(moduleGroupSource("components/canvas/director/canvas-director-workbench.tsx"));
|
||||
|
||||
test("页面只把已认证用户 id 传给引导,不使用 guest fallback 或未定义变量", () => {
|
||||
expect(projectCode).toContain('const directorOnboardingScope = useUserStore((state) => state.user?.id?.trim() || "")');
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
import { moduleGroupSource } from "./helpers/module-group-source";
|
||||
|
||||
/**
|
||||
* 生产接线回归:模板与模式的领域函数正确,不代表真实入口用上了。
|
||||
* 这里锁住「新建必须选模板」「已有场景不弹模板」「时间轴只在动画模式」三条链路。
|
||||
*/
|
||||
const workbench = readFileSync(resolve(import.meta.dir, "../src/components/canvas/director/canvas-director-workbench.tsx"), "utf8");
|
||||
const workbench = moduleGroupSource("components/canvas/director/canvas-director-workbench.tsx");
|
||||
const dock = readFileSync(resolve(import.meta.dir, "../src/components/canvas/director/director-viewport-dock.tsx"), "utf8");
|
||||
const viewport = readFileSync(resolve(import.meta.dir, "../src/components/canvas/director/director-viewport.tsx"), "utf8");
|
||||
const viewport = moduleGroupSource("components/canvas/director/director-viewport.tsx");
|
||||
const hook = readFileSync(resolve(import.meta.dir, "../src/pages/canvas/use-canvas-director.ts"), "utf8");
|
||||
const project = readFileSync(resolve(import.meta.dir, "../src/pages/canvas/project.tsx"), "utf8");
|
||||
const project = moduleGroupSource("pages/canvas/project.tsx");
|
||||
const modal = readFileSync(resolve(import.meta.dir, "../src/components/canvas/director/canvas-director-template-modal.tsx"), "utf8");
|
||||
const store = readFileSync(resolve(import.meta.dir, "../src/stores/canvas/use-director-workbench-store.ts"), "utf8");
|
||||
const styles = readFileSync(resolve(import.meta.dir, "../src/styles/globals.css"), "utf8");
|
||||
|
||||
40
web/test/helpers/module-group-source.ts
Normal file
40
web/test/helpers/module-group-source.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
// 源码约束测试的读取辅助:大文件按职责拆分后,约束应覆盖原文件及其拆分出的同组模块。
|
||||
//
|
||||
// 用法:moduleGroupSource("pages/assets/index.tsx") 返回原文件与同组模块拼接后的文本。
|
||||
// 只在「源码断言」类测试里使用;行为测试请直接 import 模块。
|
||||
|
||||
import { readFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
/** 原文件(相对 src)到拆分出的同组模块。新增拆分时在这里登记。 */
|
||||
export const SPLIT_MODULE_GROUPS: Record<string, readonly string[]> = {
|
||||
"components/canvas/canvas-cloud-agent-panel.tsx": ["components/canvas/canvas-cloud-agent-panel-parts.tsx", "components/canvas/canvas-cloud-agent-events.ts"],
|
||||
"components/canvas/canvas-cloud-agent-chat-ui.tsx": ["components/canvas/canvas-cloud-agent-composer.tsx", "components/canvas/canvas-cloud-agent-attachments.ts"],
|
||||
"components/canvas/art-critique/ai-art-critique-modal.tsx": ["components/canvas/art-critique/ai-art-critique-report.tsx"],
|
||||
"components/canvas/canvas-resource-mention-textarea.tsx": ["components/canvas/canvas-mention-chips.tsx", "components/canvas/canvas-mention-editable.ts", "components/canvas/canvas-mention-menu.tsx"],
|
||||
"components/canvas/canvas-node-content.tsx": ["components/canvas/canvas-node-media-content.tsx", "components/canvas/canvas-node-status-content.tsx"],
|
||||
"components/canvas/canvas-node-prompt-panel.tsx": ["components/canvas/canvas-node-prompt-config.ts", "components/canvas/canvas-node-prompt-references.tsx", "components/canvas/canvas-node-prompt-resize.tsx"],
|
||||
"components/canvas/canvas-script-node.tsx": ["components/canvas/canvas-script-node-parts.tsx"],
|
||||
"components/canvas/director/canvas-director-workbench.tsx": ["components/canvas/director/director-inspectors.tsx"],
|
||||
"components/canvas/director/director-viewport.tsx": ["components/canvas/director/director-viewport-capture.ts", "components/canvas/director/director-viewport-rig.ts"],
|
||||
"lib/art-critique/pipeline.ts": ["lib/art-critique/pipeline-messages.ts", "lib/art-critique/pipeline-parse.ts"],
|
||||
"lib/model-capabilities.ts": ["lib/model-capabilities-workflow.ts"],
|
||||
"pages/admin/components/admin-announcements-panel.tsx": ["pages/admin/components/admin-announcement-editor.tsx", "pages/admin/components/admin-announcement-results.ts"],
|
||||
"pages/admin/components/redemption-codes-panel.tsx": ["pages/admin/components/redemption-codes-modals.tsx"],
|
||||
"pages/admin/settings/appearance-settings-page.tsx": ["pages/admin/settings/appearance-settings-parts.tsx"],
|
||||
"pages/admin/settings/storage-settings-page.tsx": ["pages/admin/settings/storage-settings-model.ts"],
|
||||
"pages/assets/index.tsx": ["pages/assets/asset-library-cards.tsx", "pages/assets/asset-library-format.ts", "pages/assets/asset-library-panels.tsx"],
|
||||
"pages/canvas/project.tsx": ["pages/canvas/canvas-clipboard.ts"],
|
||||
"pages/create/creation-workspace.tsx": ["pages/create/creation-workspace-empty.tsx", "pages/create/creation-workspace-history.tsx", "pages/create/creation-workspace-messages.tsx"],
|
||||
"services/user-data-sync.ts": ["services/user-data-sync-media.ts"],
|
||||
"stores/use-config-store.ts": ["stores/config-model-options.ts", "stores/config-workflow-fields.ts"],
|
||||
};
|
||||
|
||||
const srcRoot = resolve(import.meta.dir, "../../src");
|
||||
|
||||
/** 读取 src 下的文件;若它被拆分过,一并读取同组模块。 */
|
||||
export function moduleGroupSource(relativePath: string): string {
|
||||
const normalized = relativePath.replace(/^(\.\.\/)*src\//, "").replace(/^\.\//, "");
|
||||
const files = [normalized, ...(SPLIT_MODULE_GROUPS[normalized] ?? [])];
|
||||
return files.map((file) => readFileSync(resolve(srcRoot, file), "utf8")).join("\n");
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { moduleGroupSource } from "./helpers/module-group-source";
|
||||
|
||||
test("滚动条样式按复用边界拆分,并保持全局入口可用", async () => {
|
||||
const [application, globals, shared, creation, creationWorkspace, editor, editorShell] = await Promise.all([
|
||||
@@ -6,7 +7,7 @@ test("滚动条样式按复用边界拆分,并保持全局入口可用", async
|
||||
Bun.file(new URL("../src/styles/globals.css", import.meta.url)).text(),
|
||||
Bun.file(new URL("../src/styles/shared/scrollbars.css", import.meta.url)).text(),
|
||||
Bun.file(new URL("../src/pages/create/creation-scrollbars.css", import.meta.url)).text(),
|
||||
Bun.file(new URL("../src/pages/create/creation-workspace.tsx", import.meta.url)).text(),
|
||||
Promise.resolve(moduleGroupSource("pages/create/creation-workspace.tsx")),
|
||||
Bun.file(new URL("../src/lib/plugins/builtin/editor/editor-shell.css", import.meta.url)).text(),
|
||||
Bun.file(new URL("../src/lib/plugins/builtin/editor/editor-shell.tsx", import.meta.url)).text(),
|
||||
]);
|
||||
|
||||
@@ -2,6 +2,7 @@ import { describe, expect, test } from "bun:test";
|
||||
|
||||
import { applySkinTheme, DEFAULT_CLASSIC_SKIN, duplicateSkinDefinition, getSkinAntOverrides, normalizeSkinDefinition, skinSwatches, SKIN_COLOR_GROUPS, SKIN_COMPONENT_NUMBER_FIELDS } from "../src/lib/skin-themes";
|
||||
import { normalizePublicAppearance } from "../src/stores/use-appearance-store";
|
||||
import { moduleGroupSource } from "./helpers/module-group-source";
|
||||
|
||||
describe("site appearance and editable skin library", () => {
|
||||
test("classic preserves foundation tokens and applies only primary button fills", () => {
|
||||
@@ -134,7 +135,7 @@ describe("site appearance and editable skin library", () => {
|
||||
const [storeSource, footerSource, pageSource, editorSource, globalStyles, adminStyles] = await Promise.all([
|
||||
Bun.file(new URL("../src/stores/use-appearance-store.ts", import.meta.url)).text(),
|
||||
Bun.file(new URL("../src/components/layout/site-compliance-footer.tsx", import.meta.url)).text(),
|
||||
Bun.file(new URL("../src/pages/admin/settings/appearance-settings-page.tsx", import.meta.url)).text(),
|
||||
Promise.resolve(moduleGroupSource("pages/admin/settings/appearance-settings-page.tsx")),
|
||||
Bun.file(new URL("../src/pages/admin/settings/components/skin-theme-editor.tsx", import.meta.url)).text(),
|
||||
Bun.file(new URL("../src/styles/globals.css", import.meta.url)).text(),
|
||||
Bun.file(new URL("../src/styles/admin-ui.css", import.meta.url)).text(),
|
||||
|
||||
11
web/test/zz-frame-check.test.ts
Normal file
11
web/test/zz-frame-check.test.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { test, expect } from "bun:test";
|
||||
import { applyFrameDrop, buildCanvasFrameDropIndex, findFrameDropTargetFromIndex } from "@/lib/canvas/canvas-frame";
|
||||
import { CanvasNodeType } from "@/types/canvas";
|
||||
const frame = { id: "f", type: CanvasNodeType.Frame, title: "f", position: { x: 0, y: 0 }, width: 760, height: 520, metadata: { frame: { collapsed: false, expandedWidth: 760, expandedHeight: 520 } } } as any;
|
||||
for (const t of [CanvasNodeType.Text, CanvasNodeType.Markdown, CanvasNodeType.Audio, CanvasNodeType.Image]) {
|
||||
test(t, () => {
|
||||
const n = { id: "n", type: t, title: "n", position: { x: 100, y: 100 }, width: 340, height: 240, metadata: {} } as any;
|
||||
const target = findFrameDropTargetFromIndex(buildCanvasFrameDropIndex([frame, n]), [n], new Set(["n"]), { x: 0, y: 0 });
|
||||
console.log(t, target, applyFrameDrop([frame, n], new Set(["n"]), target).find((x) => x.id === "n")?.parentId);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user