mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-17 20:32:25 +03:00
290 lines
11 KiB
TypeScript
290 lines
11 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";
|
|
|
|
// Minimal i18n stub — returns the key so tests can assert on fallback rendering
|
|
vi.mock("next-intl", () => ({
|
|
useLocale: () => "en",
|
|
useTranslations: () => {
|
|
const t = (key: string) => key;
|
|
t.has = () => false;
|
|
return t;
|
|
},
|
|
}));
|
|
|
|
// Minimal shared component stubs — Card wraps children, Tooltip passes through
|
|
vi.mock("@/shared/components", () => ({
|
|
Card: ({ children, className }: { children: React.ReactNode; className?: string }) => (
|
|
<div data-testid="card" className={className}>
|
|
{children}
|
|
</div>
|
|
),
|
|
}));
|
|
|
|
vi.mock("@/shared/components/Tooltip", () => ({
|
|
default: ({ children }: { children: React.ReactNode }) => <>{children}</>,
|
|
}));
|
|
|
|
const cleanupCallbacks: Array<() => void> = [];
|
|
|
|
function makeContainer(): HTMLElement {
|
|
const container = document.createElement("div");
|
|
document.body.appendChild(container);
|
|
cleanupCallbacks.push(() => {
|
|
container.remove();
|
|
});
|
|
return container;
|
|
}
|
|
|
|
describe("TranslatorConceptCard", () => {
|
|
beforeEach(() => {
|
|
(
|
|
globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
|
|
).IS_REACT_ACT_ENVIRONMENT = true;
|
|
});
|
|
|
|
afterEach(() => {
|
|
while (cleanupCallbacks.length > 0) {
|
|
cleanupCallbacks.pop()?.();
|
|
}
|
|
document.body.innerHTML = "";
|
|
});
|
|
|
|
it("exports a default function component", { timeout: 30000 }, async () => {
|
|
const mod =
|
|
await import("@/app/(dashboard)/dashboard/translator/components/TranslatorConceptCard");
|
|
expect(typeof mod.default).toBe("function");
|
|
});
|
|
|
|
it("renders the card with info icon and headline", async () => {
|
|
const { default: TranslatorConceptCard } =
|
|
await import("@/app/(dashboard)/dashboard/translator/components/TranslatorConceptCard");
|
|
const container = makeContainer();
|
|
const root = createRoot(container);
|
|
await act(async () => {
|
|
root.render(<TranslatorConceptCard />);
|
|
});
|
|
// Card should be in the DOM
|
|
expect(container.querySelector("[data-testid='card']")).toBeTruthy();
|
|
// Info icon should be present
|
|
const icons = container.querySelectorAll(".material-symbols-outlined");
|
|
const iconTexts = Array.from(icons).map((el) => el.textContent?.trim());
|
|
expect(iconTexts).toContain("info");
|
|
});
|
|
|
|
it("renders the flow diagram with 4 FlowNode elements (app, source, hub, target)", async () => {
|
|
const { default: TranslatorConceptCard } =
|
|
await import("@/app/(dashboard)/dashboard/translator/components/TranslatorConceptCard");
|
|
const container = makeContainer();
|
|
const root = createRoot(container);
|
|
await act(async () => {
|
|
root.render(<TranslatorConceptCard />);
|
|
});
|
|
// The diagram grid should contain 4 node icons (smart_toy, psychology, hub, auto_awesome)
|
|
const icons = container.querySelectorAll(".material-symbols-outlined");
|
|
const iconTexts = Array.from(icons).map((el) => el.textContent?.trim());
|
|
expect(iconTexts).toContain("smart_toy");
|
|
expect(iconTexts).toContain("psychology");
|
|
expect(iconTexts).toContain("hub");
|
|
expect(iconTexts).toContain("auto_awesome");
|
|
});
|
|
|
|
it("renders the diagram with arrow_forward separators", async () => {
|
|
const { default: TranslatorConceptCard } =
|
|
await import("@/app/(dashboard)/dashboard/translator/components/TranslatorConceptCard");
|
|
const container = makeContainer();
|
|
const root = createRoot(container);
|
|
await act(async () => {
|
|
root.render(<TranslatorConceptCard />);
|
|
});
|
|
const icons = container.querySelectorAll(".material-symbols-outlined");
|
|
const iconTexts = Array.from(icons).map((el) => el.textContent?.trim());
|
|
// Three arrows between 4 nodes
|
|
expect(iconTexts.filter((t) => t === "arrow_forward").length).toBeGreaterThanOrEqual(3);
|
|
});
|
|
|
|
it("toggle button starts collapsed (aria-expanded=false)", async () => {
|
|
const { default: TranslatorConceptCard } =
|
|
await import("@/app/(dashboard)/dashboard/translator/components/TranslatorConceptCard");
|
|
const container = makeContainer();
|
|
const root = createRoot(container);
|
|
await act(async () => {
|
|
root.render(<TranslatorConceptCard />);
|
|
});
|
|
const toggleBtn = container.querySelector(
|
|
"button[aria-controls='translator-concept-how-it-works']"
|
|
);
|
|
expect(toggleBtn).toBeTruthy();
|
|
expect(toggleBtn?.getAttribute("aria-expanded")).toBe("false");
|
|
// Collapsed panel should not be in the DOM yet
|
|
expect(container.querySelector("#translator-concept-how-it-works")).toBeNull();
|
|
});
|
|
|
|
it("toggle expands 'Como funciona' section and sets aria-expanded=true", async () => {
|
|
const { default: TranslatorConceptCard } =
|
|
await import("@/app/(dashboard)/dashboard/translator/components/TranslatorConceptCard");
|
|
const container = makeContainer();
|
|
const root = createRoot(container);
|
|
await act(async () => {
|
|
root.render(<TranslatorConceptCard />);
|
|
});
|
|
const toggleBtn = container.querySelector(
|
|
"button[aria-controls='translator-concept-how-it-works']"
|
|
) as HTMLButtonElement | null;
|
|
expect(toggleBtn).toBeTruthy();
|
|
|
|
// Click to expand
|
|
await act(async () => {
|
|
toggleBtn?.click();
|
|
});
|
|
|
|
expect(toggleBtn?.getAttribute("aria-expanded")).toBe("true");
|
|
const panel = container.querySelector("#translator-concept-how-it-works");
|
|
expect(panel).toBeTruthy();
|
|
});
|
|
|
|
it("toggle collapses section on second click and restores aria-expanded=false", async () => {
|
|
const { default: TranslatorConceptCard } =
|
|
await import("@/app/(dashboard)/dashboard/translator/components/TranslatorConceptCard");
|
|
const container = makeContainer();
|
|
const root = createRoot(container);
|
|
await act(async () => {
|
|
root.render(<TranslatorConceptCard />);
|
|
});
|
|
const toggleBtn = container.querySelector(
|
|
"button[aria-controls='translator-concept-how-it-works']"
|
|
) as HTMLButtonElement | null;
|
|
|
|
// Expand
|
|
await act(async () => {
|
|
toggleBtn?.click();
|
|
});
|
|
expect(toggleBtn?.getAttribute("aria-expanded")).toBe("true");
|
|
|
|
// Collapse
|
|
await act(async () => {
|
|
toggleBtn?.click();
|
|
});
|
|
expect(toggleBtn?.getAttribute("aria-expanded")).toBe("false");
|
|
expect(container.querySelector("#translator-concept-how-it-works")).toBeNull();
|
|
});
|
|
|
|
it("toggle button icon changes between expand_more and expand_less", async () => {
|
|
const { default: TranslatorConceptCard } =
|
|
await import("@/app/(dashboard)/dashboard/translator/components/TranslatorConceptCard");
|
|
const container = makeContainer();
|
|
const root = createRoot(container);
|
|
await act(async () => {
|
|
root.render(<TranslatorConceptCard />);
|
|
});
|
|
|
|
const toggleBtn = container.querySelector(
|
|
"button[aria-controls='translator-concept-how-it-works']"
|
|
) as HTMLButtonElement | null;
|
|
|
|
// Initially collapsed: should show expand_more
|
|
const btnIcons = toggleBtn?.querySelectorAll(".material-symbols-outlined");
|
|
const btnIconTexts = Array.from(btnIcons ?? []).map((el) => el.textContent?.trim());
|
|
expect(btnIconTexts).toContain("expand_more");
|
|
expect(btnIconTexts).not.toContain("expand_less");
|
|
|
|
// After click: should show expand_less
|
|
await act(async () => {
|
|
toggleBtn?.click();
|
|
});
|
|
const btnIconsAfter = toggleBtn?.querySelectorAll(".material-symbols-outlined");
|
|
const btnIconTextsAfter = Array.from(btnIconsAfter ?? []).map((el) => el.textContent?.trim());
|
|
expect(btnIconTextsAfter).toContain("expand_less");
|
|
expect(btnIconTextsAfter).not.toContain("expand_more");
|
|
});
|
|
});
|
|
|
|
describe("TranslateFlowDiagram", () => {
|
|
beforeEach(() => {
|
|
(
|
|
globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
|
|
).IS_REACT_ACT_ENVIRONMENT = true;
|
|
});
|
|
|
|
afterEach(() => {
|
|
while (cleanupCallbacks.length > 0) {
|
|
cleanupCallbacks.pop()?.();
|
|
}
|
|
document.body.innerHTML = "";
|
|
});
|
|
|
|
it("exports a default function component", async () => {
|
|
const mod =
|
|
await import("@/app/(dashboard)/dashboard/translator/components/TranslateFlowDiagram");
|
|
expect(typeof mod.default).toBe("function");
|
|
});
|
|
|
|
it("renders all 4 flow node icons (app, source, hub, target)", async () => {
|
|
const { default: TranslateFlowDiagram } =
|
|
await import("@/app/(dashboard)/dashboard/translator/components/TranslateFlowDiagram");
|
|
const container = makeContainer();
|
|
const root = createRoot(container);
|
|
await act(async () => {
|
|
root.render(<TranslateFlowDiagram />);
|
|
});
|
|
const icons = container.querySelectorAll(".material-symbols-outlined");
|
|
const iconTexts = Array.from(icons).map((el) => el.textContent?.trim());
|
|
// 4 nodes: app, source format, OpenAI hub, target provider
|
|
expect(iconTexts).toContain("smart_toy");
|
|
expect(iconTexts).toContain("psychology");
|
|
expect(iconTexts).toContain("hub");
|
|
expect(iconTexts).toContain("auto_awesome");
|
|
});
|
|
|
|
it("renders exactly 3 arrow_forward separators between 4 nodes", async () => {
|
|
const { default: TranslateFlowDiagram } =
|
|
await import("@/app/(dashboard)/dashboard/translator/components/TranslateFlowDiagram");
|
|
const container = makeContainer();
|
|
const root = createRoot(container);
|
|
await act(async () => {
|
|
root.render(<TranslateFlowDiagram />);
|
|
});
|
|
const icons = container.querySelectorAll(".material-symbols-outlined");
|
|
const iconTexts = Array.from(icons).map((el) => el.textContent?.trim());
|
|
expect(iconTexts.filter((t) => t === "arrow_forward")).toHaveLength(3);
|
|
});
|
|
|
|
it("renders a responsive grid container", async () => {
|
|
const { default: TranslateFlowDiagram } =
|
|
await import("@/app/(dashboard)/dashboard/translator/components/TranslateFlowDiagram");
|
|
const container = makeContainer();
|
|
const root = createRoot(container);
|
|
await act(async () => {
|
|
root.render(<TranslateFlowDiagram />);
|
|
});
|
|
// The grid wrapper should have grid class and responsive columns
|
|
const grid = container.querySelector(".grid");
|
|
expect(grid).toBeTruthy();
|
|
// Responsive class for sm breakpoint
|
|
expect(grid?.className).toContain("sm:grid-cols-");
|
|
});
|
|
|
|
it("i18n fallback: renders labels using fallback strings when translations return keys", async () => {
|
|
const { default: TranslateFlowDiagram } =
|
|
await import("@/app/(dashboard)/dashboard/translator/components/TranslateFlowDiagram");
|
|
const container = makeContainer();
|
|
const root = createRoot(container);
|
|
await act(async () => {
|
|
root.render(<TranslateFlowDiagram />);
|
|
});
|
|
// When mock returns key, tr() detects key === translation and uses fallback
|
|
// The fallback text should appear in the DOM
|
|
const text = container.textContent ?? "";
|
|
expect(text).toContain("Your app");
|
|
expect(text).toContain("ex: SDK Anthropic");
|
|
expect(text).toContain("Source format");
|
|
expect(text).toContain("claude");
|
|
// 4th node: OpenAI hub
|
|
expect(text).toContain("OpenAI (hub)");
|
|
expect(text).toContain("Target provider");
|
|
expect(text).toContain("Gemini");
|
|
});
|
|
});
|