Files
webstudio/apps/builder/app/shared/copy-paste/plugin-jsx.test.ts
Oleg Isonen d440e02990 feat: Add safer MCP authoring and draft-page verification (#5905)
- **Restore points for agent work**: Create named restore points before
major edits, review changes with a dry run, and safely revert project
content if needed.

- **Shared Slot authoring**: Agents can turn existing sections into
shared Slots and insert linked copies across pages, including valid
nested Slots.

- **DTCG and Figma token imports**: Import design tokens and CSS
variables through CLI, MCP, or direct paste, with aliases, modes,
composite values, and mappings supported.

- **Consistent token conflict handling**: Every paste workflow now
offers the same preserve, rename, or merge choices instead of silently
overwriting existing design tokens.

- **Draft-page previews**: Agents can generate and inspect previews of
draft pages without publishing them or exposing drafts in production
builds.

- **Safer MCP mutations**: Destructive operations require explicit
confirmation, invalid raw patches and invented record IDs are rejected,
and restore operations commit atomically.

- **More dependable agent contracts**: New tools have complete runtime
schemas, clearer validation errors, and consistent behavior across CLI,
MCP, Builder, and server APIs.
2026-07-17 23:16:39 +02:00

115 lines
3.8 KiB
TypeScript

import { beforeEach, describe, expect, test, vi } from "vitest";
import { createEmptyWebstudioFragment } from "@webstudio-is/project-build/runtime";
import { inspectWebstudioJsxFragmentSyntax } from "@webstudio-is/project-build/transfer";
import { isLikelyWebstudioJsx, jsx } from "./plugin-jsx";
import { $project } from "../sync/data-stores";
import { pasteHandled, pasteIgnored } from "./copy-paste";
const mocks = vi.hoisted(() => ({
createJsxFragment: vi.fn(),
insertWebstudioFragmentAt: vi.fn(),
toastError: vi.fn(),
}));
vi.mock("../trpc/trpc-client", () => ({
nativeClient: {
build: {
createJsxFragment: {
query: mocks.createJsxFragment,
},
},
},
}));
vi.mock("../instance-utils/insert", () => ({
insertWebstudioFragmentAt: mocks.insertWebstudioFragmentAt,
}));
vi.mock("../builder-api", () => ({
builderApi: {
toast: {
error: mocks.toastError,
warn: vi.fn(),
},
},
}));
describe("jsx paste plugin", () => {
beforeEach(() => {
vi.clearAllMocks();
$project.set({ id: "project-id" } as never);
});
test("detects only Webstudio JSX", () => {
expect(isLikelyWebstudioJsx("<$.Box />")).toBe(true);
expect(isLikelyWebstudioJsx("\n < $ .Box />")).toBe(true);
expect(isLikelyWebstudioJsx("< $ . Box />")).toBe(true);
expect(isLikelyWebstudioJsx('<ws.element ws:tag="section" />')).toBe(true);
expect(isLikelyWebstudioJsx("<radix.Switch />")).toBe(true);
expect(isLikelyWebstudioJsx("<animation.VideoAnimation />")).toBe(true);
expect(isLikelyWebstudioJsx("<$. />")).toBe(false);
expect(isLikelyWebstudioJsx("<$.>")).toBe(false);
expect(isLikelyWebstudioJsx("</$.Box>")).toBe(false);
expect(isLikelyWebstudioJsx("<section>HTML</section>")).toBe(false);
expect(isLikelyWebstudioJsx("## Markdown")).toBe(false);
});
test("can import JSX syntax inspection without the Node evaluator", () => {
expect(() => inspectWebstudioJsxFragmentSyntax("<$.Box />")).not.toThrow();
expect(() =>
inspectWebstudioJsxFragmentSyntax("<$.Box data-secret={process.env} />")
).toThrow('Do not access "process" in JSX fragments');
});
test("converts and inserts Webstudio JSX", async () => {
const fragment = createEmptyWebstudioFragment();
mocks.createJsxFragment.mockResolvedValue(fragment);
mocks.insertWebstudioFragmentAt.mockReturnValue(true);
await expect(jsx.onPaste?.("<$.Box />")).resolves.toEqual(pasteHandled);
expect(mocks.createJsxFragment).toHaveBeenCalledWith({
projectId: "project-id",
source: "<$.Box />",
});
expect(mocks.insertWebstudioFragmentAt).toHaveBeenCalledWith(
fragment,
undefined,
"theirs",
expect.objectContaining({
onBreakpointLimitMerge: expect.any(Function),
})
);
});
test("does not claim non-Webstudio JSX text", async () => {
await expect(jsx.onPaste?.("<section>HTML</section>")).resolves.toEqual(
pasteIgnored
);
expect(mocks.createJsxFragment).not.toHaveBeenCalled();
expect(mocks.insertWebstudioFragmentAt).not.toHaveBeenCalled();
});
test("does not claim JSX without a current project", async () => {
$project.set(undefined);
await expect(jsx.onPaste?.("<$.Box />")).resolves.toEqual(pasteIgnored);
expect(mocks.createJsxFragment).not.toHaveBeenCalled();
expect(mocks.insertWebstudioFragmentAt).not.toHaveBeenCalled();
});
test("reports conversion errors and stops paste fallback", async () => {
mocks.createJsxFragment.mockRejectedValue(new Error("Invalid JSX"));
await expect(jsx.onPaste?.('<$.Box ws:id="0" />')).resolves.toEqual({
success: false,
error: "Invalid JSX",
});
expect(mocks.toastError).not.toHaveBeenCalled();
expect(mocks.insertWebstudioFragmentAt).not.toHaveBeenCalled();
});
});