// @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();
});
});
});