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", async (importOriginal) => ({
...(await importOriginal()),
nativeClient: {
build: {
createJsxFragment: {
query: mocks.createJsxFragment,
},
},
},
}));
vi.mock("../instance-utils/insert", async (importOriginal) => ({
...(await importOriginal()),
insertWebstudioFragmentAt: mocks.insertWebstudioFragmentAt,
}));
vi.mock("../builder-api", async (importOriginal) => ({
...(await importOriginal()),
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("")).toBe(true);
expect(isLikelyWebstudioJsx('')).toBe(true);
expect(isLikelyWebstudioJsx("")).toBe(true);
expect(isLikelyWebstudioJsx("")).toBe(true);
expect(isLikelyWebstudioJsx("")).toBe(false);
expect(isLikelyWebstudioJsx("")).toBe(false);
expect(isLikelyWebstudioJsx("## Markdown")).toBe(false);
});
test("can import JSX syntax inspection without the Node evaluator", () => {
expect(() => inspectWebstudioJsxFragmentSyntax("")).not.toThrow();
expect(() =>
inspectWebstudioJsxFragmentSyntax("")
).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?.("")).resolves.toEqual(pasteHandled);
expect(mocks.createJsxFragment).toHaveBeenCalledWith({
projectId: "project-id",
source: "",
});
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?.("")).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?.("")).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?.('')).resolves.toEqual({
success: false,
error: "Invalid JSX",
});
expect(mocks.toastError).not.toHaveBeenCalled();
expect(mocks.insertWebstudioFragmentAt).not.toHaveBeenCalled();
});
});