Files
diegosouzapw--omniroute/tests/unit/ui/search-tools-studio.test.tsx
2026-07-13 13:39:12 +08:00

256 lines
9.3 KiB
TypeScript

// @vitest-environment jsdom
import React from "react";
import { act } from "react";
import { createRoot } from "react-dom/client";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
// ── Mocks ─────────────────────────────────────────────────────────────────────
vi.mock("next-intl", () => ({
useTranslations: () => (key: string) => key,
}));
vi.mock("next/link", () => ({
default: ({ children, href, ...props }: { children: React.ReactNode; href: string }) =>
React.createElement("a", { href, ...props }, children),
}));
// Mock next/dynamic to synchronously render the loaded module
vi.mock("next/dynamic", () => ({
default: (loader: () => Promise<{ default: React.ComponentType<Record<string, unknown>> }>) => {
// Run the loader eagerly. Since modules are already mocked via vi.mock(),
// the dynamic wrappers will just render the mocked components directly.
const Loaded = React.lazy(loader as Parameters<typeof React.lazy>[0]);
function DynamicWrapper(props: Record<string, unknown>) {
return React.createElement(
React.Suspense,
{ fallback: React.createElement("div", { "data-testid": "dyn-fallback" }) },
React.createElement(Loaded, props),
);
}
return DynamicWrapper;
},
}));
// Mock sub-components
vi.mock(
"../../../src/app/(dashboard)/dashboard/search-tools/components/SearchToolsTopBar",
() => ({
default: ({
activeTab,
onTabChange,
latencyMs,
costUsd,
}: {
activeTab: string;
onTabChange: (t: string) => void;
latencyMs?: number | null;
costUsd?: number | null;
}) =>
React.createElement(
"div",
{
"data-testid": "search-tools-topbar",
"data-active-tab": activeTab,
"data-latency": latencyMs ?? "",
"data-cost": costUsd ?? "",
},
React.createElement("button", { onClick: () => onTabChange("search"), "data-testid": "tab-search" }, "Search"),
React.createElement("button", { onClick: () => onTabChange("scrape"), "data-testid": "tab-scrape" }, "Scrape"),
React.createElement("button", { onClick: () => onTabChange("compare"), "data-testid": "tab-compare" }, "Compare"),
),
}),
);
vi.mock(
"../../../src/app/(dashboard)/dashboard/search-tools/components/SearchToolsConfigPane",
() => ({
default: ({ config, activeTab }: { config: Record<string, unknown>; activeTab: string }) =>
React.createElement("div", {
"data-testid": "config-pane",
"data-tab": activeTab,
"data-provider": String(config.provider ?? ""),
}),
ConfigState: {},
}),
);
vi.mock(
"../../../src/app/(dashboard)/dashboard/search-tools/components/SearchConceptCard",
() => ({
default: () => React.createElement("div", { "data-testid": "search-concept-card" }),
}),
);
vi.mock(
"../../../src/app/(dashboard)/dashboard/search-tools/components/tabs/SearchTab",
() => ({
default: () => React.createElement("div", { "data-testid": "search-tab-content" }),
}),
);
vi.mock(
"../../../src/app/(dashboard)/dashboard/search-tools/components/tabs/ScrapeTab",
() => ({
default: () => React.createElement("div", { "data-testid": "scrape-tab-content" }),
}),
);
vi.mock(
"../../../src/app/(dashboard)/dashboard/search-tools/components/tabs/CompareTab",
() => ({
default: () => React.createElement("div", { "data-testid": "compare-tab-content" }),
}),
);
// Mock global fetch
globalThis.fetch = vi.fn(() =>
Promise.resolve({
ok: true,
json: () => Promise.resolve({ providers: [] }),
} as Response),
);
// ── Import component after mocks ──────────────────────────────────────────────
const { default: SearchToolsClient } = await import(
"../../../src/app/(dashboard)/dashboard/search-tools/SearchToolsClient"
);
// ── Helpers ───────────────────────────────────────────────────────────────────
const containers: Array<{ root: ReturnType<typeof createRoot>; el: HTMLDivElement }> = [];
function renderClient(): HTMLDivElement {
const el = document.createElement("div");
document.body.appendChild(el);
const root = createRoot(el);
act(() => {
root.render(React.createElement(SearchToolsClient));
});
containers.push({ root, el });
return el;
}
async function waitFor(fn: () => boolean, ms = 2000): Promise<void> {
const start = Date.now();
while (!fn()) {
if (Date.now() - start > ms) throw new Error("waitFor timed out");
await act(async () => { await new Promise((r) => setTimeout(r, 30)); });
}
}
// ── Tests ─────────────────────────────────────────────────────────────────────
describe("SearchToolsClient (Studio)", () => {
beforeEach(() => {
(globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
vi.clearAllMocks();
globalThis.fetch = vi.fn(() =>
Promise.resolve({
ok: true,
json: () => Promise.resolve({ providers: [] }),
} as Response),
);
});
afterEach(() => {
for (const { root, el } of containers.splice(0)) {
act(() => root.unmount());
el.remove();
}
document.body.innerHTML = "";
});
it("renders the Studio with topbar", () => {
const el = renderClient();
expect(el.querySelector("[data-testid='search-tools-studio']")).toBeTruthy();
expect(el.querySelector("[data-testid='search-tools-topbar']")).toBeTruthy();
});
it("renders concept card", () => {
const el = renderClient();
expect(el.querySelector("[data-testid='search-concept-card']")).toBeTruthy();
});
it("renders config pane", () => {
const el = renderClient();
expect(el.querySelector("[data-testid='config-pane']")).toBeTruthy();
});
it("defaults to 'search' tab", () => {
const el = renderClient();
const topbar = el.querySelector("[data-testid='search-tools-topbar']") as HTMLElement;
expect(topbar?.getAttribute("data-active-tab")).toBe("search");
});
it("shows search tab content after loading", async () => {
const el = renderClient();
await waitFor(() => !!el.querySelector("[data-testid='search-tab-content']"));
expect(el.querySelector("[data-testid='search-tab-content']")).toBeTruthy();
});
it("switches to scrape tab when Scrape button clicked", async () => {
const el = renderClient();
const scrapeBtn = el.querySelector("[data-testid='tab-scrape']") as HTMLButtonElement;
act(() => {
scrapeBtn.click();
});
// Config pane data-tab should update immediately
const topbar = el.querySelector("[data-testid='search-tools-topbar']") as HTMLElement;
expect(topbar?.getAttribute("data-active-tab")).toBe("scrape");
// Scrape tab content should appear after lazy load
await waitFor(() => !!el.querySelector("[data-testid='scrape-tab-content']"));
expect(el.querySelector("[data-testid='scrape-tab-content']")).toBeTruthy();
expect(el.querySelector("[data-testid='search-tab-content']")).toBeNull();
});
it("switches to compare tab when Compare button clicked", async () => {
const el = renderClient();
const compareBtn = el.querySelector("[data-testid='tab-compare']") as HTMLButtonElement;
act(() => {
compareBtn.click();
});
const topbar = el.querySelector("[data-testid='search-tools-topbar']") as HTMLElement;
expect(topbar?.getAttribute("data-active-tab")).toBe("compare");
await waitFor(() => !!el.querySelector("[data-testid='compare-tab-content']"));
expect(el.querySelector("[data-testid='compare-tab-content']")).toBeTruthy();
expect(el.querySelector("[data-testid='search-tab-content']")).toBeNull();
});
it("config pane persists across tab switches", async () => {
const el = renderClient();
const scrapeBtn = el.querySelector("[data-testid='tab-scrape']") as HTMLButtonElement;
act(() => {
scrapeBtn.click();
});
expect(el.querySelector("[data-testid='config-pane']")).toBeTruthy();
const compareBtn = el.querySelector("[data-testid='tab-compare']") as HTMLButtonElement;
act(() => {
compareBtn.click();
});
expect(el.querySelector("[data-testid='config-pane']")).toBeTruthy();
});
it("config pane data-tab updates when tab switches", () => {
const el = renderClient();
const scrapeBtn = el.querySelector("[data-testid='tab-scrape']") as HTMLButtonElement;
act(() => {
scrapeBtn.click();
});
const configPane = el.querySelector("[data-testid='config-pane']") as HTMLElement;
expect(configPane?.getAttribute("data-tab")).toBe("scrape");
});
it("config pane data-tab is compare after switching to compare", () => {
const el = renderClient();
const compareBtn = el.querySelector("[data-testid='tab-compare']") as HTMLButtonElement;
act(() => {
compareBtn.click();
});
const configPane = el.querySelector("[data-testid='config-pane']") as HTMLElement;
expect(configPane?.getAttribute("data-tab")).toBe("compare");
});
});