Add frontend unit tests for build stores and components

This commit is contained in:
abhi1992002
2026-03-16 17:05:49 +05:30
parent 83a1578746
commit 1c18d2e991
4 changed files with 634 additions and 0 deletions

View File

@@ -0,0 +1,166 @@
import React from "react";
import { describe, it, expect, vi, beforeEach } from "vitest";
import { render, screen, fireEvent } from "@testing-library/react";
import { TooltipProvider } from "@radix-ui/react-tooltip";
import { DraftRecoveryPopup } from "../components/DraftRecoveryDialog/DraftRecoveryPopup";
const mockOnLoad = vi.fn();
const mockOnDiscard = vi.fn();
vi.mock("../components/DraftRecoveryDialog/useDraftRecoveryPopup", () => ({
useDraftRecoveryPopup: vi.fn(() => ({
isOpen: true,
popupRef: { current: null },
nodeCount: 3,
edgeCount: 2,
diff: {
nodes: { added: 1, removed: 0, modified: 2 },
edges: { added: 1, removed: 1, modified: 0 },
},
savedAt: Date.now(),
onLoad: mockOnLoad,
onDiscard: mockOnDiscard,
})),
}));
vi.mock("framer-motion", () => ({
AnimatePresence: ({ children }: { children: React.ReactNode }) => (
<>{children}</>
),
motion: {
div: React.forwardRef(function MotionDiv(
props: Record<string, unknown>,
ref: React.Ref<HTMLDivElement>,
) {
const { children, initial, animate, exit, transition, ...rest } =
props as {
children?: React.ReactNode;
initial?: unknown;
animate?: unknown;
exit?: unknown;
transition?: unknown;
[key: string]: unknown;
};
return (
<div ref={ref} {...rest}>
{children}
</div>
);
}),
},
}));
function renderWithProviders(ui: React.ReactElement) {
return render(<TooltipProvider>{ui}</TooltipProvider>);
}
beforeEach(() => {
vi.clearAllMocks();
});
describe("DraftRecoveryPopup", () => {
describe("when open with diff data", () => {
it("shows the unsaved changes message", () => {
renderWithProviders(<DraftRecoveryPopup isInitialLoadComplete={true} />);
expect(screen.getByText("Unsaved changes found")).toBeDefined();
});
it("displays diff summary", () => {
renderWithProviders(<DraftRecoveryPopup isInitialLoadComplete={true} />);
const text = document.body.textContent;
expect(text).toContain("+1/~2 blocks");
expect(text).toContain("+1/-1 connections");
});
it("renders restore and discard buttons", () => {
renderWithProviders(<DraftRecoveryPopup isInitialLoadComplete={true} />);
expect(screen.getAllByText("Restore changes").length).toBeGreaterThan(0);
expect(screen.getAllByText("Discard changes").length).toBeGreaterThan(0);
});
it("calls onLoad when restore is clicked", () => {
renderWithProviders(<DraftRecoveryPopup isInitialLoadComplete={true} />);
const buttons = screen.getAllByRole("button", {
name: /restore changes/i,
});
fireEvent.click(buttons[0]);
expect(mockOnLoad).toHaveBeenCalledOnce();
});
it("calls onDiscard when discard is clicked", () => {
renderWithProviders(<DraftRecoveryPopup isInitialLoadComplete={true} />);
const buttons = screen.getAllByRole("button", {
name: /discard changes/i,
});
fireEvent.click(buttons[0]);
expect(mockOnDiscard).toHaveBeenCalledOnce();
});
});
describe("when closed", () => {
it("renders nothing when isOpen is false", async () => {
const { useDraftRecoveryPopup } = await import(
"../components/DraftRecoveryDialog/useDraftRecoveryPopup"
);
vi.mocked(useDraftRecoveryPopup).mockReturnValue({
isOpen: false,
popupRef: { current: null },
nodeCount: 0,
edgeCount: 0,
diff: null,
savedAt: 0,
onLoad: vi.fn(),
onDiscard: vi.fn(),
});
const { container } = renderWithProviders(
<DraftRecoveryPopup isInitialLoadComplete={true} />,
);
expect(container.textContent).toBe("");
});
});
describe("when diff is null", () => {
it("falls back to node/edge count display", async () => {
const { useDraftRecoveryPopup } = await import(
"../components/DraftRecoveryDialog/useDraftRecoveryPopup"
);
vi.mocked(useDraftRecoveryPopup).mockReturnValue({
isOpen: true,
popupRef: { current: null },
nodeCount: 5,
edgeCount: 1,
diff: null,
savedAt: Date.now(),
onLoad: vi.fn(),
onDiscard: vi.fn(),
});
renderWithProviders(<DraftRecoveryPopup isInitialLoadComplete={true} />);
const text = document.body.textContent;
expect(text).toContain("5 blocks");
expect(text).toContain("1 connection");
});
it("uses singular for 1 block", async () => {
const { useDraftRecoveryPopup } = await import(
"../components/DraftRecoveryDialog/useDraftRecoveryPopup"
);
vi.mocked(useDraftRecoveryPopup).mockReturnValue({
isOpen: true,
popupRef: { current: null },
nodeCount: 1,
edgeCount: 0,
diff: null,
savedAt: Date.now(),
onLoad: vi.fn(),
onDiscard: vi.fn(),
});
renderWithProviders(<DraftRecoveryPopup isInitialLoadComplete={true} />);
const text = document.body.textContent;
expect(text).toContain("1 block,");
expect(text).toContain("0 connections");
});
});
});

View File

@@ -0,0 +1,246 @@
import { describe, it, expect, beforeEach } from "vitest";
import { useBlockMenuStore } from "../stores/blockMenuStore";
import { DefaultStateType } from "../components/NewControlPanel/NewBlockMenu/types";
import { SearchEntryFilterAnyOfItem } from "@/app/api/__generated__/models/searchEntryFilterAnyOfItem";
import { StoreAgent } from "@/app/api/__generated__/models/storeAgent";
import { SearchResponseItemsItem } from "@/app/api/__generated__/models/searchResponseItemsItem";
beforeEach(() => {
useBlockMenuStore.setState({
searchQuery: "",
searchId: undefined,
defaultState: DefaultStateType.SUGGESTION,
integration: undefined,
filters: [],
creators: [],
creators_list: [],
categoryCounts: {
blocks: 0,
integrations: 0,
marketplace_agents: 0,
my_agents: 0,
},
});
});
describe("blockMenuStore", () => {
describe("initial state", () => {
it("has empty search and suggestion default state", () => {
const state = useBlockMenuStore.getState();
expect(state.searchQuery).toBe("");
expect(state.searchId).toBeUndefined();
expect(state.defaultState).toBe("suggestion");
expect(state.integration).toBeUndefined();
expect(state.filters).toEqual([]);
expect(state.creators).toEqual([]);
});
});
describe("search state", () => {
it("sets search query", () => {
useBlockMenuStore.getState().setSearchQuery("weather");
expect(useBlockMenuStore.getState().searchQuery).toBe("weather");
});
it("sets search id", () => {
useBlockMenuStore.getState().setSearchId("abc-123");
expect(useBlockMenuStore.getState().searchId).toBe("abc-123");
});
it("clears search id", () => {
useBlockMenuStore.getState().setSearchId("abc-123");
useBlockMenuStore.getState().setSearchId(undefined);
expect(useBlockMenuStore.getState().searchId).toBeUndefined();
});
});
describe("default state", () => {
it("sets default state", () => {
useBlockMenuStore.getState().setDefaultState(DefaultStateType.ALL_BLOCKS);
expect(useBlockMenuStore.getState().defaultState).toBe(
DefaultStateType.ALL_BLOCKS,
);
});
it("changes between states", () => {
useBlockMenuStore
.getState()
.setDefaultState(DefaultStateType.INTEGRATIONS);
useBlockMenuStore.getState().setDefaultState(DefaultStateType.MY_AGENTS);
expect(useBlockMenuStore.getState().defaultState).toBe(
DefaultStateType.MY_AGENTS,
);
});
});
describe("integration", () => {
it("sets integration", () => {
useBlockMenuStore.getState().setIntegration("slack");
expect(useBlockMenuStore.getState().integration).toBe("slack");
});
it("clears integration", () => {
useBlockMenuStore.getState().setIntegration("slack");
useBlockMenuStore.getState().setIntegration(undefined);
expect(useBlockMenuStore.getState().integration).toBeUndefined();
});
});
describe("filters", () => {
it("adds a filter", () => {
useBlockMenuStore.getState().addFilter(SearchEntryFilterAnyOfItem.blocks);
expect(useBlockMenuStore.getState().filters).toEqual(["blocks"]);
});
it("adds multiple filters", () => {
useBlockMenuStore.getState().addFilter(SearchEntryFilterAnyOfItem.blocks);
useBlockMenuStore
.getState()
.addFilter(SearchEntryFilterAnyOfItem.integrations);
expect(useBlockMenuStore.getState().filters).toEqual([
"blocks",
"integrations",
]);
});
it("removes a filter", () => {
useBlockMenuStore.getState().addFilter(SearchEntryFilterAnyOfItem.blocks);
useBlockMenuStore
.getState()
.addFilter(SearchEntryFilterAnyOfItem.integrations);
useBlockMenuStore
.getState()
.removeFilter(SearchEntryFilterAnyOfItem.blocks);
expect(useBlockMenuStore.getState().filters).toEqual(["integrations"]);
});
it("sets filters directly", () => {
useBlockMenuStore
.getState()
.setFilters([
SearchEntryFilterAnyOfItem.my_agents,
SearchEntryFilterAnyOfItem.marketplace_agents,
]);
expect(useBlockMenuStore.getState().filters).toEqual([
"my_agents",
"marketplace_agents",
]);
});
it("removing a non-existent filter is a no-op", () => {
useBlockMenuStore.getState().addFilter(SearchEntryFilterAnyOfItem.blocks);
useBlockMenuStore
.getState()
.removeFilter(SearchEntryFilterAnyOfItem.integrations);
expect(useBlockMenuStore.getState().filters).toEqual(["blocks"]);
});
});
describe("creators", () => {
it("adds a creator", () => {
useBlockMenuStore.getState().addCreator("alice");
expect(useBlockMenuStore.getState().creators).toEqual(["alice"]);
});
it("removes a creator", () => {
useBlockMenuStore.getState().addCreator("alice");
useBlockMenuStore.getState().addCreator("bob");
useBlockMenuStore.getState().removeCreator("alice");
expect(useBlockMenuStore.getState().creators).toEqual(["bob"]);
});
it("sets creators directly", () => {
useBlockMenuStore.getState().setCreators(["x", "y"]);
expect(useBlockMenuStore.getState().creators).toEqual(["x", "y"]);
});
});
describe("setCreatorsList", () => {
it("extracts creators from store_agent items", () => {
const items: SearchResponseItemsItem[] = [
{
slug: "agent-1",
agent_name: "Agent 1",
creator: "alice",
} as StoreAgent,
{
slug: "agent-2",
agent_name: "Agent 2",
creator: "bob",
} as StoreAgent,
];
useBlockMenuStore.getState().setCreatorsList(items);
const list = useBlockMenuStore.getState().creators_list;
expect(list).toContain("alice");
expect(list).toContain("bob");
});
it("deduplicates creators across calls", () => {
const items1 = [
{
slug: "a1",
agent_name: "A1",
creator: "alice",
} as StoreAgent,
] as SearchResponseItemsItem[];
const items2 = [
{
slug: "a2",
agent_name: "A2",
creator: "alice",
} as StoreAgent,
] as SearchResponseItemsItem[];
useBlockMenuStore.getState().setCreatorsList(items1);
useBlockMenuStore.getState().setCreatorsList(items2);
const aliceCount = useBlockMenuStore
.getState()
.creators_list.filter((c) => c === "alice").length;
expect(aliceCount).toBe(1);
});
});
describe("categoryCounts", () => {
it("sets category counts", () => {
const counts = {
blocks: 10,
integrations: 5,
marketplace_agents: 3,
my_agents: 2,
};
useBlockMenuStore.getState().setCategoryCounts(counts);
expect(useBlockMenuStore.getState().categoryCounts).toEqual(counts);
});
});
describe("reset", () => {
it("resets search query, searchId, defaultState, and integration", () => {
useBlockMenuStore.getState().setSearchQuery("test");
useBlockMenuStore.getState().setSearchId("id-1");
useBlockMenuStore.getState().setDefaultState(DefaultStateType.ALL_BLOCKS);
useBlockMenuStore.getState().setIntegration("slack");
useBlockMenuStore.getState().addFilter(SearchEntryFilterAnyOfItem.blocks);
useBlockMenuStore.getState().addCreator("alice");
useBlockMenuStore.getState().reset();
const state = useBlockMenuStore.getState();
expect(state.searchQuery).toBe("");
expect(state.searchId).toBeUndefined();
expect(state.defaultState).toBe("suggestion");
expect(state.integration).toBeUndefined();
});
it("does not clear filters or creators", () => {
useBlockMenuStore.getState().addFilter(SearchEntryFilterAnyOfItem.blocks);
useBlockMenuStore.getState().addCreator("alice");
useBlockMenuStore.getState().reset();
expect(useBlockMenuStore.getState().filters).toEqual(["blocks"]);
expect(useBlockMenuStore.getState().creators).toEqual(["alice"]);
});
});
});

View File

@@ -0,0 +1,104 @@
import { describe, it, expect, beforeEach } from "vitest";
import { useControlPanelStore } from "../stores/controlPanelStore";
beforeEach(() => {
useControlPanelStore.getState().reset();
});
describe("controlPanelStore", () => {
describe("initial state", () => {
it("starts with all panels closed", () => {
const state = useControlPanelStore.getState();
expect(state.blockMenuOpen).toBe(false);
expect(state.saveControlOpen).toBe(false);
expect(state.forceOpenBlockMenu).toBe(false);
expect(state.forceOpenSave).toBe(false);
});
});
describe("setBlockMenuOpen", () => {
it("opens the block menu", () => {
useControlPanelStore.getState().setBlockMenuOpen(true);
expect(useControlPanelStore.getState().blockMenuOpen).toBe(true);
});
it("closes the block menu", () => {
useControlPanelStore.getState().setBlockMenuOpen(true);
useControlPanelStore.getState().setBlockMenuOpen(false);
expect(useControlPanelStore.getState().blockMenuOpen).toBe(false);
});
});
describe("setSaveControlOpen", () => {
it("opens the save control", () => {
useControlPanelStore.getState().setSaveControlOpen(true);
expect(useControlPanelStore.getState().saveControlOpen).toBe(true);
});
it("closes the save control", () => {
useControlPanelStore.getState().setSaveControlOpen(true);
useControlPanelStore.getState().setSaveControlOpen(false);
expect(useControlPanelStore.getState().saveControlOpen).toBe(false);
});
});
describe("setForceOpenBlockMenu", () => {
it("sets force open state", () => {
useControlPanelStore.getState().setForceOpenBlockMenu(true);
expect(useControlPanelStore.getState().forceOpenBlockMenu).toBe(true);
});
it("does not affect blockMenuOpen", () => {
useControlPanelStore.getState().setForceOpenBlockMenu(true);
expect(useControlPanelStore.getState().blockMenuOpen).toBe(false);
});
});
describe("setForceOpenSave", () => {
it("sets force open state", () => {
useControlPanelStore.getState().setForceOpenSave(true);
expect(useControlPanelStore.getState().forceOpenSave).toBe(true);
});
it("does not affect saveControlOpen", () => {
useControlPanelStore.getState().setForceOpenSave(true);
expect(useControlPanelStore.getState().saveControlOpen).toBe(false);
});
});
describe("independent panel state", () => {
it("opening block menu does not affect save control", () => {
useControlPanelStore.getState().setBlockMenuOpen(true);
expect(useControlPanelStore.getState().saveControlOpen).toBe(false);
});
it("opening save control does not affect block menu", () => {
useControlPanelStore.getState().setSaveControlOpen(true);
expect(useControlPanelStore.getState().blockMenuOpen).toBe(false);
});
it("both panels can be open simultaneously", () => {
useControlPanelStore.getState().setBlockMenuOpen(true);
useControlPanelStore.getState().setSaveControlOpen(true);
expect(useControlPanelStore.getState().blockMenuOpen).toBe(true);
expect(useControlPanelStore.getState().saveControlOpen).toBe(true);
});
});
describe("reset", () => {
it("resets all state to defaults", () => {
useControlPanelStore.getState().setBlockMenuOpen(true);
useControlPanelStore.getState().setSaveControlOpen(true);
useControlPanelStore.getState().setForceOpenBlockMenu(true);
useControlPanelStore.getState().setForceOpenSave(true);
useControlPanelStore.getState().reset();
const state = useControlPanelStore.getState();
expect(state.blockMenuOpen).toBe(false);
expect(state.saveControlOpen).toBe(false);
expect(state.forceOpenBlockMenu).toBe(false);
expect(state.forceOpenSave).toBe(false);
});
});
});

View File

@@ -0,0 +1,118 @@
import { describe, it, expect, beforeEach } from "vitest";
import { useTutorialStore } from "../stores/tutorialStore";
beforeEach(() => {
useTutorialStore.setState({
isTutorialRunning: false,
currentStep: 0,
forceOpenRunInputDialog: false,
tutorialInputValues: {},
});
});
describe("tutorialStore", () => {
describe("initial state", () => {
it("starts with tutorial not running at step 0", () => {
const state = useTutorialStore.getState();
expect(state.isTutorialRunning).toBe(false);
expect(state.currentStep).toBe(0);
expect(state.forceOpenRunInputDialog).toBe(false);
expect(state.tutorialInputValues).toEqual({});
});
});
describe("setIsTutorialRunning", () => {
it("starts the tutorial", () => {
useTutorialStore.getState().setIsTutorialRunning(true);
expect(useTutorialStore.getState().isTutorialRunning).toBe(true);
});
it("stops the tutorial", () => {
useTutorialStore.getState().setIsTutorialRunning(true);
useTutorialStore.getState().setIsTutorialRunning(false);
expect(useTutorialStore.getState().isTutorialRunning).toBe(false);
});
});
describe("setCurrentStep", () => {
it("advances to a step", () => {
useTutorialStore.getState().setCurrentStep(3);
expect(useTutorialStore.getState().currentStep).toBe(3);
});
it("can go back to a previous step", () => {
useTutorialStore.getState().setCurrentStep(5);
useTutorialStore.getState().setCurrentStep(2);
expect(useTutorialStore.getState().currentStep).toBe(2);
});
it("can reset to step 0", () => {
useTutorialStore.getState().setCurrentStep(4);
useTutorialStore.getState().setCurrentStep(0);
expect(useTutorialStore.getState().currentStep).toBe(0);
});
});
describe("setForceOpenRunInputDialog", () => {
it("forces the dialog open", () => {
useTutorialStore.getState().setForceOpenRunInputDialog(true);
expect(useTutorialStore.getState().forceOpenRunInputDialog).toBe(true);
});
it("closes the forced dialog", () => {
useTutorialStore.getState().setForceOpenRunInputDialog(true);
useTutorialStore.getState().setForceOpenRunInputDialog(false);
expect(useTutorialStore.getState().forceOpenRunInputDialog).toBe(false);
});
});
describe("setTutorialInputValues", () => {
it("sets input values", () => {
useTutorialStore
.getState()
.setTutorialInputValues({ topic: "AI agents" });
expect(useTutorialStore.getState().tutorialInputValues).toEqual({
topic: "AI agents",
});
});
it("replaces previous values entirely", () => {
useTutorialStore.getState().setTutorialInputValues({ a: "1" });
useTutorialStore.getState().setTutorialInputValues({ b: "2" });
expect(useTutorialStore.getState().tutorialInputValues).toEqual({
b: "2",
});
});
it("clears values with empty object", () => {
useTutorialStore.getState().setTutorialInputValues({ x: "y" });
useTutorialStore.getState().setTutorialInputValues({});
expect(useTutorialStore.getState().tutorialInputValues).toEqual({});
});
});
describe("step progression lifecycle", () => {
it("simulates a full tutorial run", () => {
useTutorialStore.getState().setIsTutorialRunning(true);
expect(useTutorialStore.getState().isTutorialRunning).toBe(true);
useTutorialStore.getState().setCurrentStep(1);
useTutorialStore.getState().setCurrentStep(2);
useTutorialStore.getState().setCurrentStep(3);
useTutorialStore.getState().setForceOpenRunInputDialog(true);
useTutorialStore
.getState()
.setTutorialInputValues({ prompt: "test prompt" });
useTutorialStore.getState().setForceOpenRunInputDialog(false);
useTutorialStore.getState().setCurrentStep(4);
useTutorialStore.getState().setIsTutorialRunning(false);
useTutorialStore.getState().setCurrentStep(0);
const state = useTutorialStore.getState();
expect(state.isTutorialRunning).toBe(false);
expect(state.currentStep).toBe(0);
});
});
});