Files
ttstd-infinite-canvas/web/test/canvas-resource-mention-editor.test.ts
AetherNo2332 36d1825f5f test(web): 画布 Agent 面板 - 修正 Skill 引用提示语的过期断言
输入框提示语已随外观配置搬进 lib/canvas/agent-appearance.ts,面板只消费
appearance.inputPlaceholder(canvas-cloud-agent-panel.tsx 的 agentCopy(...)),而这条用例仍在
面板源码里找那句字面文案,于是 Web checks 的 Run tests 一直红:干净 upstream/main 检出上可复现
1994 pass / 1 fail,同期其它作者的 PR 也是同一条失败。

改为断言"文案在 agent-appearance 里 + 面板仍在使用它",与重构后的架构一致。
2026-09-22 01:51:11 +08:00

151 lines
9.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, expect, test } from "bun:test";
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
function source(path: string) {
return readFileSync(resolve(import.meta.dir, path), "utf8");
}
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 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("));
expect(sync).toContain('byId.get(chip.dataset.mentionReferenceId || "")');
expect(sync).toContain('preview.getAttribute("src") !== src');
expect(sync).toContain("preview.replaceWith(createInlinePreview(reference))");
expect(sync).not.toContain("replaceChildren");
expect(sync).not.toContain("onChange(");
});
test("uses stable component classes for inline media references", () => {
const component = source("../src/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}");
expect(component).not.toContain("size-[1.18em]");
});
test("clamps native media dimensions so video previews cannot cover prompt text", () => {
const css = source("../src/styles/globals.css");
const previewRule = css.match(/\.canvas-resource-inline-preview \{[^}]+}/)?.[0] || "";
expect(previewRule).toContain("width: var(--canvas-mention-chip-preview-size)");
expect(previewRule).toContain("min-width: var(--canvas-mention-chip-preview-size)");
expect(previewRule).toContain("max-width: var(--canvas-mention-chip-preview-size)");
expect(previewRule).toContain("height: var(--canvas-mention-chip-preview-size)");
expect(previewRule).toContain("flex: 0 0 var(--canvas-mention-chip-preview-size)");
expect(previewRule).toContain("object-fit: cover");
});
test("exposes inline image references as replacement drop targets", () => {
const component = source("../src/components/canvas/canvas-resource-mention-textarea.tsx");
const css = source("../src/styles/globals.css");
expect(component).toContain("activeDropReferenceId?: string | null");
expect(component).toContain("onReferenceFilesDrop?:");
expect(component).toContain("chip.dataset.mentionReferenceId = reference.id");
expect(component).toContain('chip.classList.toggle("is-replace-target"');
expect(component).toContain("onReferenceFilesDrop(reference, files)");
expect(css).toContain(".canvas-resource-inline-mention.is-replace-target");
expect(css).toContain('content: "替换"');
});
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 configComposer = source("../src/components/canvas/canvas-config-composer.tsx");
const project = source("../src/pages/canvas/project.tsx");
expect(editor).toContain("useResolvedCanvasResourceReferences");
expect(panel).toContain("<LoaderCircle className=");
expect(panel).toContain("animate-spin motion-reduce:animate-none");
expect(panel).not.toContain("isRunning ? theme.accent.danger");
expect(configComposer).toContain("wrapper.dataset.referenceToken");
expect(configComposer).not.toContain("result += `@[node:");
expect(project).not.toContain("removeCanvasResourceMention");
});
test("keeps editable mention prompts separate from normalized generation prompts", () => {
const imageExecutor = source("../src/pages/canvas/canvas-image-generation-executor.ts");
const mediaExecutors = source("../src/pages/canvas/canvas-media-generation-executors.ts");
const textExecutor = source("../src/pages/canvas/canvas-text-generation-executor.ts");
const generationExecutor = source("../src/pages/canvas/use-canvas-generation-executor.ts");
expect(imageExecutor.match(/canvasGenerationPromptMetadata\(prompt, effectivePrompt\)/g)?.length).toBeGreaterThanOrEqual(3);
expect(imageExecutor).toContain("imageBatchExpanded: count > 1 ? true : undefined");
expect(imageExecutor).toContain("imageGenerationReferenceConnections");
expect(imageExecutor).toContain("retireImageBatchChildren");
expect(mediaExecutors.match(/canvasGenerationPromptMetadata\(prompt, effectivePrompt\)/g)?.length).toBe(2);
expect(textExecutor.match(/canvasGenerationPromptMetadata\(prompt, effectivePrompt\)/g)?.length).toBe(2);
expect(generationExecutor).toContain("composerContent: prompt");
expect(generationExecutor).toContain("canvasGenerationPromptMetadata(prompt, statusPrompt)");
});
test("anchors the mention menu to the caret instead of the textarea edge", () => {
const component = source("../src/components/canvas/canvas-resource-mention-textarea.tsx");
expect(component).toContain("cursorOffset={mention.end}");
expect(component).toContain("mentionCaretRect(anchor, cursorOffset)");
expect(component).toContain("textareaCaretRect(anchor, cursorOffset)");
expect(component).toContain('transform: position.showAbove ? "translateY(-100%)" : undefined');
expect(component).toContain('window.addEventListener("scroll", updatePosition, true)');
});
test("renders skill references as descriptive workflow rows", () => {
const component = source("../src/components/canvas/canvas-resource-mention-textarea.tsx");
const css = source("../src/styles/globals.css");
expect(component).toContain('reference.kind === "skill" ? "is-skill" : ""');
expect(component).toContain("reference.skill?.description");
expect(component).toContain("reference.skill?.fileCount");
expect(component).toContain("<Workflow aria-hidden />");
expect(css).toContain(".canvas-resource-mention-item.is-skill");
expect(css).toContain(".canvas-resource-mention-meta");
});
test("agent composer attachments stay large, previewable, and mentionable", () => {
const component = source("../src/components/canvas/canvas-cloud-agent-chat-ui.tsx");
expect(component).toContain("w-20 shrink-0");
expect(component).toContain("insertAttachmentMention");
expect(component).toContain("@[attachment:");
expect(component).toContain("AgentImagePreview");
expect(component).toContain("composerReferences");
expect(component).toContain("点击放大预览");
});
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");
expect(component).toContain("const token = `@[skill:${skill.skillId}] `");
expect(component).toContain("slash.start + 1 + slash.query.length");
expect(component).toContain("buildSkillMentionReferences(availableSlashSkills)");
expect(component).toContain("[/、]([^\\s/、]*)$");
// 输入框提示语已随外观配置搬进 agent-appearance(面板只消费 appearance.inputPlaceholder),
// 这里断言"文案在、面板仍用它",而不是把文案钉在面板源码里。
expect(source("../src/lib/canvas/agent-appearance.ts")).toContain("用 / 或 、 引用 Skills");
expect(source("../src/components/canvas/canvas-cloud-agent-panel.tsx")).toContain("agentCopy(appearance.inputPlaceholder, appearance.agentName)");
});
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 css = source("../src/components/canvas/canvas-cloud-agent.css");
expect(component).toContain('const isDecorated = reference.kind === "skill" || reference.kind === "tool"');
expect(component).toContain('chip.className = `canvas-resource-inline-mention ${isDecorated ? `is-${reference.kind}` : ""}`');
expect(component).toMatch(/if \(reference.kind === "skill"\)\s*\{\s*prefix.textContent = "✦"/);
expect(component).toContain('prefix.textContent = "@"');
expect(component).toContain("if (!isDecorated) chip.appendChild(createInlinePreview(reference));");
expect(component).toContain('chip.style.setProperty("--canvas-skill-mention-color", skillMentionColor(reference))');
expect(chat).toContain('sendOnEnter={canSubmit ? "both" : false}');
expect(chat).toContain("agent-composer-resize-handle");
expect(chat).toContain("Enter 发送 · Shift+Enter 换行");
expect(css).toContain(".agent-composer-send-hint-full");
expect(css).toContain(".agent-composer-send-hint-compact");
expect(css).toContain(".agent-composer-prompt-scroll");
expect(css).not.toContain(".agent-tool-row:hover");
});
});