// @vitest-environment jsdom import { act } from "react"; import { createRoot } from "react-dom/client"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { JsonSchemaForm } from "./JsonSchemaForm"; // eslint-disable-next-line @typescript-eslint/no-explicit-any (globalThis as any).IS_REACT_ACT_ENVIRONMENT = true; // SecretBindingPicker pulls in CompanyContext + react-query. Stub it so we can // exercise SecretField in isolation. The stub renders a select with the same // onChange contract as the real picker. vi.mock("./SecretBindingPicker", () => ({ SecretBindingPicker: ({ value, onChange, disabled, }: { value: { secretId: string } | null; onChange: (next: { secretId: string } | null) => void; disabled?: boolean; }) => ( ), })); describe("JsonSchemaForm secret-ref rendering", () => { let container: HTMLDivElement; beforeEach(() => { container = document.createElement("div"); document.body.appendChild(container); }); afterEach(() => { container.remove(); document.body.innerHTML = ""; vi.clearAllMocks(); }); it("renders multiline secret-ref fields as textareas alongside the picker", async () => { const root = createRoot(container); await act(async () => { root.render( {}} />, ); }); // The picker is always rendered, and a non-UUID raw value auto-opens the // textarea fallback. expect(container.querySelector('[data-testid="secret-binding-picker"]')).not.toBeNull(); expect(container.querySelector("textarea")).not.toBeNull(); expect(container.querySelector('input[type="password"]')).toBeNull(); await act(async () => { root.unmount(); }); }); it("renders the picker and hides the raw input when the value is a UUID secret ref", async () => { const root = createRoot(container); await act(async () => { root.render( {}} />, ); }); expect( container.querySelector('[data-testid="secret-binding-picker"]'), ).not.toBeNull(); // No raw input or textarea is visible while a secret is bound. expect(container.querySelector('input[type="password"]')).toBeNull(); expect(container.querySelector("textarea")).toBeNull(); await act(async () => { root.unmount(); }); }); it("writes the secret id to form values when the picker selects an existing secret", async () => { const root = createRoot(container); const onChange = vi.fn(); await act(async () => { root.render( , ); }); const picker = container.querySelector( '[data-testid="secret-binding-picker"]', ); expect(picker).not.toBeNull(); const setSelectValue = Object.getOwnPropertyDescriptor( window.HTMLSelectElement.prototype, "value", )?.set; expect(setSelectValue).toBeTruthy(); await act(async () => { setSelectValue!.call(picker!, "11111111-1111-4111-8111-111111111111"); picker!.dispatchEvent(new Event("change", { bubbles: true })); }); expect(onChange).toHaveBeenCalledWith({ apiKey: "11111111-1111-4111-8111-111111111111", }); await act(async () => { root.unmount(); }); }); it("auto-opens the raw input when a raw value arrives after mount", async () => { const root = createRoot(container); const schema = { type: "object" as const, properties: { apiKey: { type: "string" as const, format: "secret-ref" as const, }, }, }; // First render with empty value — picker visible, no raw input. await act(async () => { root.render( {}} />, ); }); expect(container.querySelector('input[type="password"]')).toBeNull(); // Parent fills in a previously-saved raw value (the async load case). await act(async () => { root.render( {}} />, ); }); const input = container.querySelector('input[type="password"]'); expect(input).not.toBeNull(); expect(input?.value).toBe("loaded-from-api"); await act(async () => { root.unmount(); }); }); it("keeps the password fallback for short raw values", async () => { const root = createRoot(container); await act(async () => { root.render( {}} />, ); }); const input = container.querySelector( 'input[type="password"]', ); expect(input).not.toBeNull(); expect(input?.value).toBe("raw-value"); await act(async () => { root.unmount(); }); }); });