Files
diegosouzapw--omniroute/tests/unit/ui/playground-structured-output-editor.test.tsx
2026-07-13 13:39:12 +08:00

157 lines
5.6 KiB
TypeScript

// @vitest-environment jsdom
import React from "react";
import { act } from "react";
import { createRoot } from "react-dom/client";
import { afterEach, describe, expect, it, vi } from "vitest";
const { useStructuredOutput } = await import(
"../../../src/app/(dashboard)/dashboard/playground/hooks/useStructuredOutput"
);
const { default: StructuredOutputEditor } = await import(
"../../../src/app/(dashboard)/dashboard/playground/components/StructuredOutputEditor"
);
function setInputValue(
el: HTMLTextAreaElement | HTMLInputElement,
value: string,
): void {
const nativeSetter =
el instanceof HTMLTextAreaElement
? Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, "value")?.set
: Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, "value")?.set;
nativeSetter?.call(el, value);
el.dispatchEvent(new Event("input", { bubbles: true }));
el.dispatchEvent(new Event("change", { bubbles: true }));
}
function StructuredOutputEditorWrapper({ onReady }: {
onReady?: (so: ReturnType<typeof useStructuredOutput>) => void;
}) {
const so = useStructuredOutput();
React.useEffect(() => { onReady?.(so); });
return <StructuredOutputEditor structuredOutput={so} />;
}
const containers: Array<{ root: ReturnType<typeof createRoot>; el: HTMLDivElement }> = [];
function renderEditor(onReady?: (so: ReturnType<typeof useStructuredOutput>) => void): HTMLDivElement {
const el = document.createElement("div");
document.body.appendChild(el);
const root = createRoot(el);
act(() => {
root.render(<StructuredOutputEditorWrapper onReady={onReady} />);
});
containers.push({ root, el });
return el;
}
afterEach(() => {
for (const { root, el } of containers) {
act(() => root.unmount());
el.remove();
}
containers.length = 0;
vi.restoreAllMocks();
});
describe("StructuredOutputEditor", () => {
it("renders JSON mode toggle off by default", () => {
const el = renderEditor();
const toggle = el.querySelector("[role='switch']") as HTMLButtonElement;
expect(toggle.getAttribute("aria-checked")).toBe("false");
});
it("shows schema editor when toggle is enabled", async () => {
const el = renderEditor();
const toggle = el.querySelector("[role='switch']") as HTMLButtonElement;
await act(async () => { toggle.click(); });
// Should now show schema name field and textarea
const textareas = el.querySelectorAll("textarea");
expect(textareas.length).toBeGreaterThan(0);
expect(el.textContent).toContain("JSON schema");
});
it("hides schema editor when toggle is disabled", async () => {
const el = renderEditor();
const toggle = el.querySelector("[role='switch']") as HTMLButtonElement;
// Enable
await act(async () => { toggle.click(); });
// Disable
await act(async () => { toggle.click(); });
// Should not show textarea
const textareas = el.querySelectorAll("textarea");
expect(textareas.length).toBe(0);
});
it("validates a valid JSON schema successfully", async () => {
const el = renderEditor();
const toggle = el.querySelector("[role='switch']") as HTMLButtonElement;
await act(async () => { toggle.click(); });
// Set valid schema
const textarea = el.querySelector("textarea") as HTMLTextAreaElement;
const validSchema = JSON.stringify({ type: "object", properties: { name: { type: "string" } }, required: ["name"] });
act(() => setInputValue(textarea, validSchema));
// Click Validate
const validateBtn = el.querySelector("button:last-child") as HTMLButtonElement;
// Find the validate button specifically
const allBtns = el.querySelectorAll("button");
const validateBtnActual = Array.from(allBtns).find(
(b) => b.textContent?.includes("Validate"),
) as HTMLButtonElement;
expect(validateBtnActual).not.toBeNull();
await act(async () => { validateBtnActual.click(); });
// Should show validated status
expect(el.textContent).toContain("validated");
});
it("shows error for invalid JSON in schema textarea", async () => {
const el = renderEditor();
const toggle = el.querySelector("[role='switch']") as HTMLButtonElement;
await act(async () => { toggle.click(); });
const textarea = el.querySelector("textarea") as HTMLTextAreaElement;
act(() => setInputValue(textarea, "not valid json {{{"));
const allBtns = el.querySelectorAll("button");
const validateBtn = Array.from(allBtns).find(
(b) => b.textContent?.includes("Validate"),
) as HTMLButtonElement;
await act(async () => { validateBtn.click(); });
expect(el.textContent).toContain("Invalid JSON");
});
it("shows Zod error when schema name is empty after validation", async () => {
const el = renderEditor();
const toggle = el.querySelector("[role='switch']") as HTMLButtonElement;
await act(async () => { toggle.click(); });
// Clear the name field
const inputs = el.querySelectorAll("input[type='text']") as NodeListOf<HTMLInputElement>;
act(() => setInputValue(inputs[0], "")); // empty name
// Set valid JSON
const textarea = el.querySelector("textarea") as HTMLTextAreaElement;
act(() => setInputValue(textarea, JSON.stringify({ type: "object", properties: {} })));
const allBtns = el.querySelectorAll("button");
const validateBtn = Array.from(allBtns).find(
(b) => b.textContent?.includes("Validate"),
) as HTMLButtonElement;
await act(async () => { validateBtn.click(); });
// Empty name should use fallback "my_schema" which is valid, so no error expected
// (the component uses nameField.trim() || "my_schema")
// So it should actually succeed
expect(el.textContent).toContain("validated");
});
});