Files
2026-07-13 12:58:18 +08:00

264 lines
8.8 KiB
TypeScript

import { describe, it, expect } from "vitest";
import jscodeshift from "jscodeshift";
import transform from "../migrate-attachments";
const j = jscodeshift.withParser("tsx");
function run(source: string): string {
const result = transform(
{ source, path: "test.tsx" },
{ jscodeshift: j, j, stats: () => {}, report: () => {} },
);
return result ?? source;
}
describe("migrate-attachments codemod", () => {
// -----------------------------------------------------------------------
// Props transformation
// -----------------------------------------------------------------------
describe("JSX props", () => {
it("transforms imageUploadsEnabled to attachments", () => {
const input = `
import { CopilotChat } from "@copilotkit/react-ui";
<CopilotChat imageUploadsEnabled={true} />;
`;
const output = run(input);
expect(output).toContain("attachments={{");
expect(output).toContain("enabled: true");
expect(output).not.toContain("imageUploadsEnabled");
});
it("transforms inputFileAccept to attachments.accept", () => {
const input = `
import { CopilotChat } from "@copilotkit/react-ui";
<CopilotChat inputFileAccept="image/*" />;
`;
const output = run(input);
expect(output).toContain('accept: "image/*"');
expect(output).not.toContain("inputFileAccept");
});
it("merges both props into a single attachments object", () => {
const input = `
import { CopilotChat } from "@copilotkit/react-ui";
<CopilotChat imageUploadsEnabled={true} inputFileAccept="image/*,.pdf" />;
`;
const output = run(input);
expect(output).toContain("enabled: true");
expect(output).toContain('accept: "image/*,.pdf"');
expect(output).not.toContain("imageUploadsEnabled");
expect(output).not.toContain("inputFileAccept");
});
it("works on CopilotSidebar", () => {
const input = `
import { CopilotSidebar } from "@copilotkit/react-ui";
<CopilotSidebar imageUploadsEnabled={true} />;
`;
const output = run(input);
expect(output).toContain("attachments={{");
expect(output).not.toContain("imageUploadsEnabled");
});
it("works on CopilotPopup", () => {
const input = `
import { CopilotPopup } from "@copilotkit/react-ui";
<CopilotPopup imageUploadsEnabled={true} />;
`;
const output = run(input);
expect(output).toContain("attachments={{");
expect(output).not.toContain("imageUploadsEnabled");
});
it("preserves other props", () => {
const input = `
import { CopilotChat } from "@copilotkit/react-ui";
<CopilotChat className="my-chat" imageUploadsEnabled={true} labels={{ placeholder: "Ask..." }} />;
`;
const output = run(input);
expect(output).toContain('className="my-chat"');
expect(output).toContain("labels=");
expect(output).toContain("attachments={{");
});
it("preserves imageUploadsEnabled={false}", () => {
const input = `
import { CopilotChat } from "@copilotkit/react-ui";
<CopilotChat imageUploadsEnabled={false} />;
`;
const output = run(input);
expect(output).toContain("enabled: false");
expect(output).not.toContain("enabled: true");
});
it("preserves dynamic imageUploadsEnabled expression", () => {
const input = `
import { CopilotChat } from "@copilotkit/react-ui";
<CopilotChat imageUploadsEnabled={isEnabled} />;
`;
const output = run(input);
expect(output).toContain("enabled: isEnabled");
});
it("handles shorthand imageUploadsEnabled (no value)", () => {
const input = `
import { CopilotChat } from "@copilotkit/react-ui";
<CopilotChat imageUploadsEnabled />;
`;
const output = run(input);
expect(output).toContain("enabled: true");
});
it("skips if attachments prop already exists", () => {
const input = `
import { CopilotChat } from "@copilotkit/react-ui";
<CopilotChat imageUploadsEnabled={true} attachments={{ enabled: true }} />;
`;
const output = run(input);
// Should not double-add attachments — leaves as-is
expect(output).toContain("imageUploadsEnabled");
});
it("preserves dynamic inputFileAccept expression", () => {
const input = `
import { CopilotChat } from "@copilotkit/react-ui";
<CopilotChat imageUploadsEnabled={true} inputFileAccept={acceptTypes} />;
`;
const output = run(input);
expect(output).toContain("accept: acceptTypes");
expect(output).toContain("enabled: true");
expect(output).not.toContain("inputFileAccept");
});
it("ignores non-CopilotKit components", () => {
const input = `
<SomeOtherChat imageUploadsEnabled={true} />;
`;
const output = run(input);
expect(output).toContain("imageUploadsEnabled");
});
});
// -----------------------------------------------------------------------
// Import renames
// -----------------------------------------------------------------------
describe("import renames", () => {
it("renames ImageUploadQueue to AttachmentQueue", () => {
const input = `
import { ImageUploadQueue } from "@copilotkit/react-ui";
<ImageUploadQueue images={imgs} />;
`;
const output = run(input);
expect(output).toContain("AttachmentQueue");
expect(output).not.toContain("ImageUploadQueue");
});
it("renames ImageUpload type to Attachment", () => {
const input = `
import type { ImageUpload } from "@copilotkit/react-ui";
const x: ImageUpload = { contentType: "", bytes: "" };
`;
const output = run(input);
expect(output).toContain("Attachment");
expect(output).not.toContain("ImageUpload");
});
it("handles aliased imports (keeps alias, renames imported)", () => {
const input = `
import { ImageUploadQueue as MyQueue } from "@copilotkit/react-ui";
<MyQueue images={imgs} />;
`;
const output = run(input);
// Imported name changes, but local alias stays
expect(output).toContain("AttachmentQueue as MyQueue");
expect(output).toContain("<MyQueue");
});
it("ignores imports from other packages", () => {
const input = `
import { ImageUploadQueue } from "some-other-package";
`;
const output = run(input);
expect(output).toContain("ImageUploadQueue");
expect(output).not.toContain("AttachmentQueue");
});
it("does not rename local variables that shadow the import name", () => {
const input = `
import type { ImageUpload } from "@copilotkit/react-ui";
const x: ImageUpload = {} as any;
const ImageUpload = "unrelated local variable";
console.log(ImageUpload);
`;
const output = run(input);
// Import and type reference should be renamed
expect(output).toContain("import type { Attachment }");
expect(output).toContain("const x: Attachment");
// Local variable declaration and its reference should NOT be renamed
expect(output).toContain(
'const ImageUpload = "unrelated local variable"',
);
expect(output).toContain("console.log(ImageUpload)");
});
it("does not rename object property keys or member expressions", () => {
const input = `
import type { ImageUpload } from "@copilotkit/react-ui";
const x: ImageUpload = {} as any;
const config = { ImageUpload: true };
const val = obj.ImageUpload;
`;
const output = run(input);
// Import and type reference should be renamed
expect(output).toContain("import type { Attachment }");
expect(output).toContain("const x: Attachment");
// Object key and member access should NOT be renamed
expect(output).toContain("{ ImageUpload: true }");
expect(output).toContain("obj.ImageUpload");
});
});
// -----------------------------------------------------------------------
// Idempotency and no-op
// -----------------------------------------------------------------------
describe("idempotency", () => {
it("running twice produces the same result", () => {
const input = `
import { CopilotChat, ImageUploadQueue } from "@copilotkit/react-ui";
<CopilotChat imageUploadsEnabled={true} inputFileAccept="image/*" />;
<ImageUploadQueue images={imgs} />;
`;
const first = run(input);
const second = run(first);
expect(second).toBe(first);
});
});
describe("no-op on unrelated code", () => {
it("does not modify files without deprecated APIs", () => {
const input = `
import { useState } from "react";
function App() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}
`;
const output = run(input);
expect(output).toBe(input);
});
it("does not modify already-migrated code", () => {
const input = `
import { CopilotChat, AttachmentQueue } from "@copilotkit/react-ui";
import type { Attachment } from "@copilotkit/react-ui";
<CopilotChat attachments={{ enabled: true, accept: "image/*" }} />;
`;
const output = run(input);
expect(output).toBe(input);
});
});
});