Add Skills Store version diff regression test

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-06-11 16:18:33 +00:00
1 parent a33e533dc4
commit bb87e49ab2
1 file changed
+276
+276
View File
@@ -0,0 +1,276 @@
// @vitest-environment jsdom
import type { ComponentProps, ReactNode } from "react";
import { flushSync } from "react-dom";
import { createRoot, type Root } from "react-dom/client";
import type { CompanySkillDetail, CompanySkillVersion } from "@paperclipai/shared";
import { afterEach, describe, expect, it, vi } from "vitest";
import { SkillDetailPage, getSkillVersionDiffSelection } from "./CompanySkills";
vi.mock("@/lib/router", () => ({
Link: ({ children, to, ...props }: { children: ReactNode; to: string }) => (
<a href={to} {...props}>{children}</a>
),
useNavigate: () => vi.fn(),
useParams: () => ({}),
useSearchParams: () => [new URLSearchParams(), vi.fn()],
}));
vi.mock("@/components/ui/button", () => ({
Button: ({
children,
type = "button",
variant: _variant,
size: _size,
asChild: _asChild,
...props
}: ComponentProps<"button"> & { asChild?: boolean; variant?: string; size?: string }) => (
<button type={type} {...props}>{children}</button>
),
}));
vi.mock("@/components/ui/dialog", () => ({
Dialog: ({ open, children }: { open?: boolean; children: ReactNode }) => (open ? <div>{children}</div> : null),
DialogContent: ({ children }: { children: ReactNode }) => <div role="dialog">{children}</div>,
DialogDescription: ({ children }: { children: ReactNode }) => <p>{children}</p>,
DialogFooter: ({ children }: { children: ReactNode }) => <div>{children}</div>,
DialogHeader: ({ children }: { children: ReactNode }) => <div>{children}</div>,
DialogTitle: ({ children }: { children: ReactNode }) => <h2>{children}</h2>,
}));
vi.mock("@/components/ui/dropdown-menu", () => ({
DropdownMenu: ({ children }: { children: ReactNode }) => <>{children}</>,
DropdownMenuContent: () => null,
DropdownMenuItem: ({ children, onSelect }: { children: ReactNode; onSelect?: () => void }) => (
<button type="button" onClick={onSelect}>{children}</button>
),
DropdownMenuLabel: ({ children }: { children: ReactNode }) => <div>{children}</div>,
DropdownMenuRadioGroup: ({ children }: { children: ReactNode }) => <div>{children}</div>,
DropdownMenuRadioItem: ({ children, onSelect }: { children: ReactNode; onSelect?: () => void }) => (
<button type="button" onClick={onSelect}>{children}</button>
),
DropdownMenuSeparator: () => <hr />,
DropdownMenuTrigger: ({ children }: { children: ReactNode }) => <>{children}</>,
}));
vi.mock("@/components/ui/popover", () => ({
Popover: ({ children }: { children: ReactNode }) => <>{children}</>,
PopoverContent: () => null,
PopoverTrigger: ({ children }: { children: ReactNode }) => <>{children}</>,
}));
vi.mock("@/components/ui/tooltip", () => ({
Tooltip: ({ children }: { children: ReactNode }) => <>{children}</>,
TooltipContent: ({ children }: { children: ReactNode }) => <span>{children}</span>,
TooltipTrigger: ({ children }: { children: ReactNode }) => <>{children}</>,
}));
vi.mock("@/components/ui/tabs", () => ({
Tabs: ({ children }: { children: ReactNode }) => <div>{children}</div>,
TabsList: ({ children }: { children: ReactNode }) => <div>{children}</div>,
TabsTrigger: ({ children }: { children: ReactNode }) => <button type="button">{children}</button>,
}));
vi.mock("@/components/ui/checkbox", () => ({
Checkbox: (props: ComponentProps<"input">) => <input type="checkbox" {...props} />,
}));
vi.mock("../components/MarkdownBody", () => ({
MarkdownBody: ({ children }: { children: ReactNode }) => <div>{children}</div>,
}));
vi.mock("../components/MarkdownEditor", () => ({
MarkdownEditor: ({ value }: { value: string }) => <textarea readOnly value={value} />,
}));
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(globalThis as any).IS_REACT_ACT_ENVIRONMENT = true;
let root: Root | null = null;
let container: HTMLDivElement | null = null;
async function act(callback: () => void | Promise<void>) {
let result: void | Promise<void> = undefined;
flushSync(() => {
result = callback();
});
await result;
}
afterEach(() => {
root?.unmount();
root = null;
container?.remove();
container = null;
});
function makeVersion(revisionNumber: number, content: string): CompanySkillVersion {
return {
id: `version-${revisionNumber}`,
companyId: "company-1",
companySkillId: "skill-1",
revisionNumber,
label: null,
fileInventory: [
{
path: "SKILL.md",
kind: "skill",
content,
},
],
authorAgentId: null,
authorUserId: null,
createdAt: new Date(`2026-01-0${revisionNumber}T00:00:00Z`),
};
}
function makeDetail(currentVersion: CompanySkillVersion): CompanySkillDetail {
return {
id: "skill-1",
companyId: "company-1",
key: "demo-skill",
slug: "demo-skill",
name: "Demo Skill",
description: "A demo skill.",
markdown: "# Demo Skill",
sourceType: "local_path",
sourceLocator: null,
sourceRef: null,
trustLevel: "markdown_only",
compatibility: "compatible",
fileInventory: [{ path: "SKILL.md", kind: "skill" }],
iconUrl: null,
color: null,
tagline: null,
authorName: null,
homepageUrl: null,
categories: [],
sharingScope: "private",
publicShareToken: null,
forkedFromSkillId: null,
forkedFromCompanyId: null,
starCount: 0,
installCount: 0,
forkCount: 0,
currentVersionId: currentVersion.id,
metadata: null,
createdAt: new Date("2026-01-01T00:00:00Z"),
updatedAt: new Date("2026-01-02T00:00:00Z"),
attachedAgentCount: 0,
usedByAgents: [],
editable: true,
editableReason: null,
sourceLabel: "Local",
sourceBadge: "local",
sourcePath: null,
currentVersion,
starredByCurrentActor: false,
};
}
async function renderSkillDetail(versions: CompanySkillVersion[]) {
container = document.createElement("div");
document.body.appendChild(container);
root = createRoot(container);
await act(async () => {
root?.render(
<SkillDetailPage
detail={makeDetail(versions[0]!)}
loading={false}
activeTab="versions"
onTabChange={vi.fn()}
selectedPath="SKILL.md"
file={null}
fileLoading={false}
viewMode="preview"
editMode={false}
draft=""
setViewMode={vi.fn()}
setEditMode={vi.fn()}
setDraft={vi.fn()}
onSave={vi.fn()}
savePending={false}
versions={versions}
versionsLoading={false}
attachAgents={[]}
onSubmitAttach={vi.fn()}
attachPending={false}
expandedDirs={new Set()}
onToggleDir={vi.fn()}
onSelectPath={vi.fn()}
updateStatus={null}
updateStatusLoading={false}
onCheckUpdates={vi.fn()}
checkUpdatesPending={false}
onInstallUpdate={vi.fn()}
installUpdatePending={false}
onToggleStar={vi.fn()}
starPending={false}
onFork={vi.fn()}
onUpdateSharingScope={vi.fn()}
updateSharingPending={false}
onDelete={vi.fn()}
deletePending={false}
/>,
);
});
return container;
}
function buttonsNamed(node: ParentNode, name: string) {
return Array.from(node.querySelectorAll("button")).filter((button) => button.textContent?.trim() === name);
}
async function click(button: HTMLButtonElement) {
await act(async () => {
button.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
}
describe("getSkillVersionDiffSelection", () => {
it("selects the previous saved revision or the initial baseline for row diffs", () => {
const v1 = makeVersion(1, "first");
const v2 = makeVersion(2, "second");
expect(getSkillVersionDiffSelection([v2, v1], v2.id)).toEqual({
leftVersionId: v1.id,
rightVersionId: v2.id,
});
expect(getSkillVersionDiffSelection([v2, v1], v1.id)).toEqual({
leftVersionId: null,
rightVersionId: v1.id,
});
});
});
describe("SkillDetailPage versions tab", () => {
it("opens per-row version diffs for newest and oldest revisions", async () => {
const v1 = makeVersion(1, "# Demo Skill\n\nFirst line");
const v2 = makeVersion(2, "# Demo Skill\n\nSecond line");
const node = await renderSkillDetail([v2, v1]);
const viewDiffButtons = buttonsNamed(node, "View diff") as HTMLButtonElement[];
expect(viewDiffButtons).toHaveLength(2);
await click(viewDiffButtons[0]!);
let dialog = node.querySelector('[role="dialog"]') as HTMLElement;
let selects = Array.from(dialog.querySelectorAll("select"));
expect(dialog.textContent).toContain("Diff");
expect(selects[0]?.value).toBe(v1.id);
expect(selects[1]?.value).toBe(v2.id);
expect(dialog.textContent).toContain("Second line");
await click(viewDiffButtons[1]!);
dialog = node.querySelector('[role="dialog"]') as HTMLElement;
selects = Array.from(dialog.querySelectorAll("select"));
expect(selects[0]?.value).toBe("");
expect(selects[1]?.value).toBe(v1.id);
expect(dialog.textContent).toContain("Initial");
expect(dialog.textContent).toContain("First line");
expect(dialog.textContent).not.toContain("Both sides are the same version");
});
});