mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-12 18:22:48 +03:00
Co-authored-by: xz-dev <xz-dev@users.noreply.github.com> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
132 lines
4.8 KiB
TypeScript
132 lines
4.8 KiB
TypeScript
// @vitest-environment jsdom
|
|
//
|
|
// Field-level icon URL validation for the compatible-provider Edit modal: an invalid
|
|
// iconUrl must surface as an inline field error BEFORE the onSave callback fires, and a
|
|
// valid data:image/*;base64 iconUrl must submit. Mirrors the shared validator
|
|
// (src/shared/validation/iconUrl.ts) used by both the UI and the server schema.
|
|
import React, { act } from "react";
|
|
import { createRoot } from "react-dom/client";
|
|
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
|
|
vi.mock("next-intl", () => ({
|
|
useTranslations: () => (key: string) => key,
|
|
}));
|
|
|
|
const { default: EditCompatibleNodeModal } =
|
|
await import("../../../src/app/(dashboard)/dashboard/providers/[id]/components/modals/EditCompatibleNodeModal");
|
|
|
|
const containers: Array<{ root: ReturnType<typeof createRoot>; el: HTMLDivElement }> = [];
|
|
|
|
function render(node: Record<string, unknown>, onSave?: () => Promise<void>) {
|
|
const el = document.createElement("div");
|
|
document.body.appendChild(el);
|
|
const root = createRoot(el);
|
|
act(() => {
|
|
root.render(
|
|
<EditCompatibleNodeModal
|
|
isOpen
|
|
node={node as any}
|
|
onSave={onSave || (async () => {})}
|
|
onClose={() => {}}
|
|
/>
|
|
);
|
|
});
|
|
containers.push({ root, el });
|
|
return el;
|
|
}
|
|
|
|
function inputByLabel(el: Element, label: string): HTMLInputElement {
|
|
const inputs = Array.from(el.querySelectorAll<HTMLInputElement>("input"));
|
|
const found = inputs.find((i) => {
|
|
const labelEl = i.previousElementSibling || i.parentElement?.previousElementSibling;
|
|
return labelEl?.textContent === label;
|
|
});
|
|
if (!found) throw new Error(`No input for label: ${label}`);
|
|
return found;
|
|
}
|
|
|
|
function setInputValue(input: HTMLInputElement, value: string) {
|
|
const setter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, "value")!.set!;
|
|
act(() => {
|
|
setter.call(input, value);
|
|
input.dispatchEvent(new Event("input", { bubbles: true }));
|
|
});
|
|
}
|
|
|
|
async function waitFor(fn: () => boolean, timeoutMs = 2000) {
|
|
const start = Date.now();
|
|
while (!fn()) {
|
|
if (Date.now() - start > timeoutMs) throw new Error("waitFor timed out");
|
|
await new Promise((r) => setTimeout(r, 20));
|
|
}
|
|
}
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
afterEach(() => {
|
|
for (const { root, el } of containers.splice(0)) {
|
|
act(() => root.unmount());
|
|
el.remove();
|
|
}
|
|
vi.unstubAllGlobals();
|
|
});
|
|
|
|
const NODE = {
|
|
id: "oc-1",
|
|
name: "My Node",
|
|
prefix: "mynode",
|
|
baseUrl: "https://api.example.com/v1",
|
|
apiType: "chat",
|
|
iconUrl: "https://example.com/logo.png",
|
|
};
|
|
|
|
describe("EditCompatibleNodeModal — iconUrl field-level validation", () => {
|
|
it("shows an inline error for an unsafe scheme and does NOT call onSave", async () => {
|
|
const onSave = vi.fn(async () => {});
|
|
const el = render({ ...NODE, iconUrl: "javascript:alert(1)" });
|
|
const modal = el.querySelector('[role="dialog"]')!;
|
|
|
|
setInputValue(inputByLabel(modal, "iconUrlLabel"), "javascript:alert(1)");
|
|
const buttons = Array.from(modal.querySelectorAll<HTMLButtonElement>("button"));
|
|
const saveBtn = buttons.find((b) => b.textContent === "save");
|
|
act(() => saveBtn!.click());
|
|
await waitFor(() => modal.textContent?.includes("iconUrlInvalid") ?? false);
|
|
|
|
expect(modal.textContent).toContain("iconUrlInvalid");
|
|
expect(onSave).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("shows an inline error for a non-image data URL and does NOT call onSave", async () => {
|
|
const onSave = vi.fn(async () => {});
|
|
const el = render({ ...NODE, iconUrl: "data:text/html;base64,QUJD" });
|
|
const modal = el.querySelector('[role="dialog"]')!;
|
|
|
|
setInputValue(inputByLabel(modal, "iconUrlLabel"), "data:text/html;base64,QUJD");
|
|
const buttons = Array.from(modal.querySelectorAll<HTMLButtonElement>("button"));
|
|
const saveBtn = buttons.find((b) => b.textContent === "save");
|
|
act(() => saveBtn!.click());
|
|
await waitFor(() => modal.textContent?.includes("iconUrlInvalid") ?? false);
|
|
|
|
expect(modal.textContent).toContain("iconUrlInvalid");
|
|
expect(onSave).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("accepts a valid data:image/*;base64 iconUrl and calls onSave with it", async () => {
|
|
const onSave = vi.fn(async () => {});
|
|
const el = render({ ...NODE, iconUrl: "" }, onSave);
|
|
const modal = el.querySelector('[role="dialog"]')!;
|
|
|
|
setInputValue(inputByLabel(modal, "iconUrlLabel"), "data:image/png;base64,iVBORw0KGgo=");
|
|
const buttons = Array.from(modal.querySelectorAll<HTMLButtonElement>("button"));
|
|
const saveBtn = buttons.find((b) => b.textContent === "save");
|
|
act(() => saveBtn!.click());
|
|
await waitFor(() => onSave.mock.calls.length > 0);
|
|
|
|
expect(modal.textContent).not.toContain("iconUrlInvalid");
|
|
const payload = onSave.mock.calls[0][0];
|
|
expect(payload.iconUrl).toBe("data:image/png;base64,iVBORw0KGgo=");
|
|
});
|
|
});
|