chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
// @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";
|
||||
|
||||
const cleanupCallbacks: Array<() => void> = [];
|
||||
|
||||
function createTestStorage(): Storage {
|
||||
const entries = new Map<string, string>();
|
||||
return {
|
||||
get length() {
|
||||
return entries.size;
|
||||
},
|
||||
clear: () => entries.clear(),
|
||||
getItem: (key) => entries.get(key) ?? null,
|
||||
key: (index) => Array.from(entries.keys())[index] ?? null,
|
||||
removeItem: (key) => {
|
||||
entries.delete(key);
|
||||
},
|
||||
setItem: (key, value) => {
|
||||
entries.set(key, value);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function makeContainer(): HTMLElement {
|
||||
const container = document.createElement("div");
|
||||
document.body.appendChild(container);
|
||||
cleanupCallbacks.push(() => {
|
||||
container.remove();
|
||||
});
|
||||
return container;
|
||||
}
|
||||
|
||||
describe("AutoRoutingBanner", () => {
|
||||
beforeEach(() => {
|
||||
(
|
||||
globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
|
||||
).IS_REACT_ACT_ENVIRONMENT = true;
|
||||
vi.stubGlobal("localStorage", createTestStorage());
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
while (cleanupCallbacks.length > 0) {
|
||||
cleanupCallbacks.pop()?.();
|
||||
}
|
||||
document.body.innerHTML = "";
|
||||
localStorage.clear();
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it("renders banner on first mount", async () => {
|
||||
const { default: AutoRoutingBanner } = await import("@/shared/components/AutoRoutingBanner");
|
||||
const container = makeContainer();
|
||||
const root = createRoot(container);
|
||||
await act(async () => {
|
||||
root.render(<AutoRoutingBanner />);
|
||||
});
|
||||
expect(container.querySelector('[role="banner"]')).toBeTruthy();
|
||||
expect(container.textContent).toContain("Auto-Routing Active");
|
||||
});
|
||||
|
||||
it("includes link to Combos page", async () => {
|
||||
const { default: AutoRoutingBanner } = await import("@/shared/components/AutoRoutingBanner");
|
||||
const container = makeContainer();
|
||||
const root = createRoot(container);
|
||||
await act(async () => {
|
||||
root.render(<AutoRoutingBanner />);
|
||||
});
|
||||
const link = container.querySelector('a[href="/dashboard/combos"]');
|
||||
expect(link).toBeTruthy();
|
||||
});
|
||||
|
||||
it("can be dismissed by clicking close button", async () => {
|
||||
const { default: AutoRoutingBanner } = await import("@/shared/components/AutoRoutingBanner");
|
||||
const container = makeContainer();
|
||||
const root = createRoot(container);
|
||||
await act(async () => {
|
||||
root.render(<AutoRoutingBanner />);
|
||||
});
|
||||
expect(container.querySelector('[role="banner"]')).toBeTruthy();
|
||||
const closeButton = container.querySelector('button[aria-label="Dismiss auto-routing banner"]');
|
||||
expect(closeButton).toBeTruthy();
|
||||
await act(async () => {
|
||||
closeButton?.click();
|
||||
});
|
||||
expect(container.querySelector('[role="banner"]')).toBeFalsy();
|
||||
});
|
||||
|
||||
it("persists dismissal to localStorage", async () => {
|
||||
const { default: AutoRoutingBanner } = await import("@/shared/components/AutoRoutingBanner");
|
||||
const container = makeContainer();
|
||||
const root = createRoot(container);
|
||||
await act(async () => {
|
||||
root.render(<AutoRoutingBanner />);
|
||||
});
|
||||
const closeButton = container.querySelector('button[aria-label="Dismiss auto-routing banner"]');
|
||||
await act(async () => {
|
||||
closeButton?.click();
|
||||
});
|
||||
expect(localStorage.getItem("auto-routing-banner-dismissed")).toBe("true");
|
||||
});
|
||||
|
||||
it("remains hidden after dismissal on remount", async () => {
|
||||
localStorage.setItem("auto-routing-banner-dismissed", "true");
|
||||
const { default: AutoRoutingBanner } = await import("@/shared/components/AutoRoutingBanner");
|
||||
const container = makeContainer();
|
||||
const root = createRoot(container);
|
||||
await act(async () => {
|
||||
root.render(<AutoRoutingBanner />);
|
||||
});
|
||||
expect(container.querySelector('[role="banner"]')).toBeFalsy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,56 @@
|
||||
// @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";
|
||||
|
||||
const cleanupCallbacks: Array<() => void> = [];
|
||||
|
||||
function makeContainer(): HTMLElement {
|
||||
const container = document.createElement("div");
|
||||
document.body.appendChild(container);
|
||||
cleanupCallbacks.push(() => {
|
||||
container.remove();
|
||||
});
|
||||
return container;
|
||||
}
|
||||
|
||||
describe("KiroAuthModal", () => {
|
||||
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 = "";
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("shows Google Account and starts Google social login when clicked", async () => {
|
||||
const { default: KiroAuthModal } = await import("@/shared/components/KiroAuthModal");
|
||||
const container = makeContainer();
|
||||
const root = createRoot(container);
|
||||
const onMethodSelect = vi.fn();
|
||||
|
||||
await act(async () => {
|
||||
root.render(<KiroAuthModal isOpen onClose={vi.fn()} onMethodSelect={onMethodSelect} />);
|
||||
});
|
||||
|
||||
const googleButton = Array.from(container.querySelectorAll("button")).find((button) =>
|
||||
button.textContent?.includes("Google Account")
|
||||
);
|
||||
|
||||
expect(googleButton).toBeTruthy();
|
||||
expect(googleButton?.className).not.toContain("hidden");
|
||||
|
||||
await act(async () => {
|
||||
googleButton?.click();
|
||||
});
|
||||
|
||||
expect(onMethodSelect).toHaveBeenCalledWith("social", { provider: "google" });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,391 @@
|
||||
// @vitest-environment jsdom
|
||||
import React from "react";
|
||||
import { act } from "react";
|
||||
import { createRoot, type Root } from "react-dom/client";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
vi.mock("next-intl", () => ({
|
||||
useTranslations: () => (key: string, values?: Record<string, unknown>) => {
|
||||
if (!values) return key;
|
||||
return Object.entries(values).reduce(
|
||||
(message, [name, value]) => message.replace(`{${name}}`, String(value)),
|
||||
key
|
||||
);
|
||||
},
|
||||
}));
|
||||
|
||||
type FetchCall = {
|
||||
url: string;
|
||||
method: string;
|
||||
body: any;
|
||||
};
|
||||
|
||||
type MockResponse = {
|
||||
status?: number;
|
||||
body?: unknown;
|
||||
};
|
||||
|
||||
const cleanupCallbacks: Array<() => void> = [];
|
||||
let fetchCalls: FetchCall[] = [];
|
||||
|
||||
function makeContainer(): HTMLElement {
|
||||
const container = document.createElement("div");
|
||||
document.body.appendChild(container);
|
||||
cleanupCallbacks.push(() => {
|
||||
container.remove();
|
||||
});
|
||||
return container;
|
||||
}
|
||||
|
||||
function jsonResponse(body: unknown, status = 200) {
|
||||
return {
|
||||
ok: status >= 200 && status < 300,
|
||||
status,
|
||||
json: async () => body,
|
||||
} as Response;
|
||||
}
|
||||
|
||||
function parseBody(init?: RequestInit) {
|
||||
if (typeof init?.body !== "string") return null;
|
||||
try {
|
||||
return JSON.parse(init.body);
|
||||
} catch {
|
||||
return init.body;
|
||||
}
|
||||
}
|
||||
|
||||
function installFetchMock(handler: (url: string, init?: RequestInit) => MockResponse) {
|
||||
const fetchMock = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
|
||||
const url = typeof input === "string" ? input : input.toString();
|
||||
const method = String(init?.method || "GET").toUpperCase();
|
||||
const body = parseBody(init);
|
||||
fetchCalls.push({ url, method, body });
|
||||
const response = handler(url, init);
|
||||
return jsonResponse(response.body ?? {}, response.status ?? 200);
|
||||
});
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
return fetchMock;
|
||||
}
|
||||
|
||||
async function flushEffects() {
|
||||
await act(async () => {
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
});
|
||||
}
|
||||
|
||||
async function renderProxyConfigModal(props?: Partial<React.ComponentProps<any>>) {
|
||||
const { default: ProxyConfigModal } = await import("@/shared/components/ProxyConfigModal");
|
||||
const container = makeContainer();
|
||||
const root: Root = createRoot(container);
|
||||
cleanupCallbacks.push(() => root.unmount());
|
||||
|
||||
await act(async () => {
|
||||
root.render(
|
||||
<ProxyConfigModal
|
||||
isOpen
|
||||
onClose={vi.fn()}
|
||||
level="provider"
|
||||
levelId="claude"
|
||||
levelLabel="Claude"
|
||||
onSaved={vi.fn()}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
await waitForModalToLoad(container);
|
||||
return { container, root };
|
||||
}
|
||||
|
||||
async function waitForModalToLoad(container: HTMLElement) {
|
||||
for (let i = 0; i < 20; i++) {
|
||||
await flushEffects();
|
||||
if (!container.textContent?.includes("loading")) return;
|
||||
}
|
||||
}
|
||||
|
||||
function getInput(container: HTMLElement, placeholder: string) {
|
||||
const input = Array.from(container.querySelectorAll("input")).find(
|
||||
(item) => item.getAttribute("placeholder") === placeholder
|
||||
);
|
||||
expect(input).toBeTruthy();
|
||||
return input as HTMLInputElement;
|
||||
}
|
||||
|
||||
async function clickButton(container: HTMLElement, text: string) {
|
||||
const expected = text.toLowerCase();
|
||||
const buttons = Array.from(container.querySelectorAll("button"));
|
||||
const getText = (item: HTMLButtonElement) => item.textContent?.trim().toLowerCase() || "";
|
||||
const button =
|
||||
buttons.find((item) => getText(item) === expected) ||
|
||||
buttons.find(
|
||||
(item) => getText(item).endsWith(expected) && !getText(item).includes("savedproxy")
|
||||
) ||
|
||||
buttons.find((item) => getText(item).includes(expected));
|
||||
expect(button).toBeTruthy();
|
||||
await act(async () => {
|
||||
button?.click();
|
||||
});
|
||||
await flushEffects();
|
||||
}
|
||||
|
||||
async function waitForCall(predicate: (call: FetchCall) => boolean) {
|
||||
for (let i = 0; i < 20; i++) {
|
||||
await flushEffects();
|
||||
if (fetchCalls.some(predicate)) return;
|
||||
}
|
||||
}
|
||||
|
||||
async function setInputValue(input: HTMLInputElement, value: string) {
|
||||
await act(async () => {
|
||||
const setter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, "value")?.set;
|
||||
setter?.call(input, value);
|
||||
input.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
input.dispatchEvent(new Event("change", { bubbles: true }));
|
||||
});
|
||||
await flushEffects();
|
||||
}
|
||||
|
||||
function defaultProxyConfigResponses(url: string): MockResponse | null {
|
||||
if (url === "/api/settings/proxies") {
|
||||
return { body: { items: [], total: 0 } };
|
||||
}
|
||||
if (url.startsWith("/api/settings/proxies/assignments?") && url.includes("scope=provider")) {
|
||||
return { body: { items: [], total: 0 } };
|
||||
}
|
||||
if (url.startsWith("/api/settings/proxy?level=provider")) {
|
||||
return { body: { level: "provider", id: "claude", proxy: null } };
|
||||
}
|
||||
if (url === "/api/settings/proxy") {
|
||||
return { body: {} };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
describe("ProxyConfigModal custom registry saves", () => {
|
||||
beforeEach(() => {
|
||||
(
|
||||
globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
|
||||
).IS_REACT_ACT_ENVIRONMENT = true;
|
||||
fetchCalls = [];
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
while (cleanupCallbacks.length > 0) {
|
||||
cleanupCallbacks.pop()?.();
|
||||
}
|
||||
document.body.innerHTML = "";
|
||||
vi.unstubAllGlobals();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("creates a dashboard-custom registry proxy, assigns it, and clears matching legacy config", async () => {
|
||||
installFetchMock((url, init) => {
|
||||
const method = String(init?.method || "GET").toUpperCase();
|
||||
const body = parseBody(init);
|
||||
|
||||
if (method === "POST" && url === "/api/settings/proxies") {
|
||||
return {
|
||||
status: 201,
|
||||
body: { id: "custom-proxy-1", ...body, assignment: { proxyId: "custom-proxy-1" } },
|
||||
};
|
||||
}
|
||||
|
||||
return defaultProxyConfigResponses(url) || { status: 404, body: {} };
|
||||
});
|
||||
|
||||
const { container } = await renderProxyConfigModal();
|
||||
|
||||
await setInputValue(getInput(container, "hostPlaceholder"), "custom.local");
|
||||
await setInputValue(getInput(container, "8080"), "3128");
|
||||
await clickButton(container, "save");
|
||||
await waitForCall((call) => call.method === "POST" && call.url === "/api/settings/proxies");
|
||||
|
||||
const createCall = fetchCalls.find(
|
||||
(call) => call.method === "POST" && call.url === "/api/settings/proxies"
|
||||
);
|
||||
expect(createCall?.body).toMatchObject({
|
||||
name: "Custom Provider Proxy (Claude)",
|
||||
type: "http",
|
||||
host: "custom.local",
|
||||
port: 3128,
|
||||
status: "active",
|
||||
source: "dashboard-custom",
|
||||
assignment: {
|
||||
scope: "provider",
|
||||
scopeId: "claude",
|
||||
},
|
||||
});
|
||||
|
||||
expect(
|
||||
fetchCalls.some((call) => call.method === "PUT" && call.url === "/api/settings/proxy")
|
||||
).toBe(false);
|
||||
expect(
|
||||
fetchCalls.some(
|
||||
(call) => call.method === "PUT" && call.url === "/api/settings/proxies/assignments"
|
||||
)
|
||||
).toBe(false);
|
||||
expect(
|
||||
fetchCalls.some(
|
||||
(call) =>
|
||||
call.method === "DELETE" && call.url === "/api/settings/proxy?level=provider&id=claude"
|
||||
)
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("updates an existing scope-owned dashboard-custom proxy instead of creating a duplicate", async () => {
|
||||
installFetchMock((url, init) => {
|
||||
const method = String(init?.method || "GET").toUpperCase();
|
||||
const body = parseBody(init);
|
||||
|
||||
if (method === "GET" && url === "/api/settings/proxies") {
|
||||
return {
|
||||
body: {
|
||||
items: [
|
||||
{
|
||||
id: "custom-proxy-1",
|
||||
name: "Custom Provider Proxy (Claude)",
|
||||
type: "http",
|
||||
host: "old.local",
|
||||
port: 8080,
|
||||
source: "dashboard-custom",
|
||||
},
|
||||
],
|
||||
total: 1,
|
||||
},
|
||||
};
|
||||
}
|
||||
if (url.startsWith("/api/settings/proxies/assignments?") && url.includes("scope=provider")) {
|
||||
return {
|
||||
body: {
|
||||
items: [
|
||||
{ proxyId: "other-proxy", scope: "provider", scopeId: "other-provider" },
|
||||
{ proxyId: "custom-proxy-1", scope: "provider", scopeId: "claude" },
|
||||
],
|
||||
total: 1,
|
||||
},
|
||||
};
|
||||
}
|
||||
if (url === "/api/settings/proxies?id=custom-proxy-1&whereUsed=1") {
|
||||
return {
|
||||
body: {
|
||||
count: 1,
|
||||
assignments: [{ proxyId: "custom-proxy-1", scope: "provider", scopeId: "claude" }],
|
||||
},
|
||||
};
|
||||
}
|
||||
if (method === "PATCH" && url === "/api/settings/proxies") {
|
||||
return {
|
||||
body: { id: "custom-proxy-1", ...body, assignment: { proxyId: "custom-proxy-1" } },
|
||||
};
|
||||
}
|
||||
|
||||
return defaultProxyConfigResponses(url) || { status: 404, body: {} };
|
||||
});
|
||||
|
||||
const { container } = await renderProxyConfigModal();
|
||||
|
||||
await setInputValue(getInput(container, "hostPlaceholder"), "updated.local");
|
||||
await clickButton(container, "authOptional");
|
||||
await setInputValue(getInput(container, "usernamePlaceholder"), "***");
|
||||
await setInputValue(getInput(container, "passwordPlaceholder"), "***");
|
||||
await clickButton(container, "save");
|
||||
await waitForCall((call) => call.method === "PATCH" && call.url === "/api/settings/proxies");
|
||||
|
||||
expect(
|
||||
fetchCalls.some((call) => call.method === "POST" && call.url === "/api/settings/proxies")
|
||||
).toBe(false);
|
||||
|
||||
const updateCall = fetchCalls.find(
|
||||
(call) => call.method === "PATCH" && call.url === "/api/settings/proxies"
|
||||
);
|
||||
expect(updateCall?.body).toMatchObject({
|
||||
id: "custom-proxy-1",
|
||||
host: "updated.local",
|
||||
source: "dashboard-custom",
|
||||
assignment: {
|
||||
scope: "provider",
|
||||
scopeId: "claude",
|
||||
},
|
||||
});
|
||||
expect(updateCall?.body).not.toHaveProperty("username");
|
||||
expect(updateCall?.body).not.toHaveProperty("password");
|
||||
expect(
|
||||
fetchCalls.some(
|
||||
(call) => call.method === "PUT" && call.url === "/api/settings/proxies/assignments"
|
||||
)
|
||||
).toBe(false);
|
||||
expect(
|
||||
fetchCalls.some(
|
||||
(call) =>
|
||||
call.method === "DELETE" && call.url === "/api/settings/proxy?level=provider&id=claude"
|
||||
)
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("creates a new dashboard-custom proxy when current assignment is a reusable manual proxy", async () => {
|
||||
installFetchMock((url, init) => {
|
||||
const method = String(init?.method || "GET").toUpperCase();
|
||||
const body = parseBody(init);
|
||||
|
||||
if (method === "GET" && url === "/api/settings/proxies") {
|
||||
return {
|
||||
body: {
|
||||
items: [
|
||||
{
|
||||
id: "manual-proxy-1",
|
||||
name: "Shared Manual Proxy",
|
||||
type: "http",
|
||||
host: "shared.local",
|
||||
port: 8080,
|
||||
source: "manual",
|
||||
},
|
||||
],
|
||||
total: 1,
|
||||
},
|
||||
};
|
||||
}
|
||||
if (url.startsWith("/api/settings/proxies/assignments?") && url.includes("scope=provider")) {
|
||||
return {
|
||||
body: {
|
||||
items: [{ proxyId: "manual-proxy-1", scope: "provider", scopeId: "claude" }],
|
||||
total: 1,
|
||||
},
|
||||
};
|
||||
}
|
||||
if (method === "POST" && url === "/api/settings/proxies") {
|
||||
return {
|
||||
status: 201,
|
||||
body: { id: "custom-proxy-2", ...body, assignment: { proxyId: "custom-proxy-2" } },
|
||||
};
|
||||
}
|
||||
|
||||
return defaultProxyConfigResponses(url) || { status: 404, body: {} };
|
||||
});
|
||||
|
||||
const { container } = await renderProxyConfigModal();
|
||||
|
||||
await clickButton(container, "custom");
|
||||
await setInputValue(getInput(container, "hostPlaceholder"), "custom.local");
|
||||
await clickButton(container, "save");
|
||||
await waitForCall((call) => call.method === "POST" && call.url === "/api/settings/proxies");
|
||||
|
||||
expect(
|
||||
fetchCalls.some((call) => call.method === "PATCH" && call.url === "/api/settings/proxies")
|
||||
).toBe(false);
|
||||
expect(
|
||||
fetchCalls.some(
|
||||
(call) =>
|
||||
call.method === "POST" &&
|
||||
call.url === "/api/settings/proxies" &&
|
||||
call.body.assignment?.scope === "provider" &&
|
||||
call.body.assignment?.scopeId === "claude"
|
||||
)
|
||||
).toBe(true);
|
||||
expect(
|
||||
fetchCalls.some(
|
||||
(call) => call.method === "PUT" && call.url === "/api/settings/proxies/assignments"
|
||||
)
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user