输入框提示语已随外观配置搬进 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 里 + 面板仍在使用它",与重构后的架构一致。
151 lines
9.5 KiB
TypeScript
151 lines
9.5 KiB
TypeScript
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");
|
||
});
|
||
});
|