mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-01 04:42:10 +03:00
Extracts the routing-combo compression-mode dropdown (Default/Off/Lite/
Standard/Aggressive/Ultra) from the combo card into a shared
ComboCompressionModeSelect component, reused on both the combo card
(compact) and the Compression Combos page's "Assign to routing" list
under Context & Cache. Both surfaces persist through the existing
PUT /api/combos/{id} route -- no backend or schema change.
This commit is contained in:
committed by
GitHub
parent
69c778eb45
commit
0f10225f1d
@@ -0,0 +1 @@
|
||||
- **feat(dashboard):** add per-routing-combo compression-mode override to the Compression Combos page under Context & Cache, alongside the existing combo-card quick override. (#6760)
|
||||
@@ -188,6 +188,14 @@ Combo: "free-forever"
|
||||
This lets you use stacked compression on free/coding providers while keeping lite mode on paid
|
||||
subscriptions.
|
||||
|
||||
This "Per-Combo Override" assignment is a different control from the **routing-combo compression
|
||||
mode** override (Default/Off/Lite/Standard/Aggressive/Ultra) — that override does not pick a named
|
||||
compression-combo pipeline; it just sets the `compressionMode` field consulted by
|
||||
`resolveCompressionPlan`. It can be set either on the combo card (`Dashboard → Combos`) or, since
|
||||
#6760, per routing combo in the "Assign to routing" list on
|
||||
`Dashboard → Context & Cache → Compression Combos`, right next to the pipeline-assignment checkbox
|
||||
documented above. Both surfaces persist through the same `PUT /api/combos/{id}` endpoint.
|
||||
|
||||
### Per-request override
|
||||
|
||||
Send the `x-omniroute-compression` request header to override the compression plan for a single
|
||||
|
||||
@@ -12,6 +12,7 @@ import Input from "@/shared/components/Input";
|
||||
import Modal from "@/shared/components/Modal";
|
||||
import Toggle from "@/shared/components/Toggle";
|
||||
import Tooltip from "@/shared/components/Tooltip";
|
||||
import { ComboCompressionModeSelect } from "@/shared/components/compression/ComboCompressionModeSelect";
|
||||
import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard";
|
||||
import { filterUsableConnections } from "@/shared/utils/connectionStatus";
|
||||
import { FieldLabelWithHelp, WeightTotalBar } from "./parts";
|
||||
@@ -1576,46 +1577,6 @@ function ComboCard({
|
||||
const tc = useTranslations("common");
|
||||
const emailsVisible = useEmailPrivacyStore((s) => s.emailsVisible);
|
||||
const strategyDescription = getStrategyDescription(t, strategy);
|
||||
const hasRuntimeConfig = combo?.config && typeof combo.config === "object";
|
||||
const initialCompressionMode =
|
||||
typeof combo?.config?.compressionMode === "string"
|
||||
? combo.config.compressionMode
|
||||
: hasRuntimeConfig
|
||||
? ""
|
||||
: combo.compressionOverride || "";
|
||||
const [compressionOverride, setCompressionOverride] = useState(initialCompressionMode);
|
||||
const [isSavingCompression, setIsSavingCompression] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setCompressionOverride(initialCompressionMode);
|
||||
}, [initialCompressionMode]);
|
||||
|
||||
const handleCompressionOverrideChange = async (value) => {
|
||||
setCompressionOverride(value);
|
||||
setIsSavingCompression(true);
|
||||
const nextConfig = { ...(combo.config || {}) };
|
||||
if (value) {
|
||||
nextConfig.compressionMode = value;
|
||||
} else {
|
||||
delete nextConfig.compressionMode;
|
||||
}
|
||||
try {
|
||||
const response = await fetch(`/api/combos/${combo.id}`, {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ config: nextConfig }),
|
||||
});
|
||||
if (!response.ok) {
|
||||
console.error("Failed to update compression override");
|
||||
setCompressionOverride(initialCompressionMode);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error updating compression override:", error);
|
||||
setCompressionOverride(initialCompressionMode);
|
||||
} finally {
|
||||
setIsSavingCompression(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card
|
||||
@@ -1745,32 +1706,11 @@ function ComboCard({
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5 transition-opacity">
|
||||
{compressionEnabled && (
|
||||
<select
|
||||
value={compressionOverride}
|
||||
onChange={(e) => handleCompressionOverrideChange(e.target.value)}
|
||||
disabled={isSavingCompression}
|
||||
className="text-xs py-1 px-2 rounded border border-black/10 dark:border-white/10 bg-surface text-text-main focus:border-primary focus:outline-none transition-colors disabled:opacity-50 max-w-[130px] md:max-w-none"
|
||||
<ComboCompressionModeSelect
|
||||
combo={combo}
|
||||
title={t("compressionOverride")}
|
||||
>
|
||||
<option value="" className="bg-surface text-text-main">
|
||||
Default
|
||||
</option>
|
||||
<option value="off" className="bg-surface text-text-main">
|
||||
Off
|
||||
</option>
|
||||
<option value="lite" className="bg-surface text-text-main">
|
||||
Lite
|
||||
</option>
|
||||
<option value="standard" className="bg-surface text-text-main">
|
||||
Standard
|
||||
</option>
|
||||
<option value="aggressive" className="bg-surface text-text-main">
|
||||
Aggressive
|
||||
</option>
|
||||
<option value="ultra" className="bg-surface text-text-main">
|
||||
Ultra
|
||||
</option>
|
||||
</select>
|
||||
className="text-xs py-1 px-2 rounded border border-black/10 dark:border-white/10 bg-surface text-text-main focus:border-primary focus:outline-none transition-colors disabled:opacity-50 max-w-[130px] md:max-w-none"
|
||||
/>
|
||||
)}
|
||||
<Link
|
||||
href={`/dashboard/combos/${combo.id}`}
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { STACKED_PIPELINE_ENGINE_INTENSITIES } from "@/shared/validation/compressionConfigSchemas";
|
||||
import { CompressionPipelineEditor } from "@/shared/components/compression/CompressionPipelineEditor";
|
||||
import { ComboCompressionModeSelect } from "@/shared/components/compression/ComboCompressionModeSelect";
|
||||
import CompressionHub from "./CompressionHub";
|
||||
|
||||
type PipelineStep = { engine: string; intensity?: string };
|
||||
@@ -23,7 +24,11 @@ type CompressionCombo = {
|
||||
outputModeIntensity: string;
|
||||
isDefault: boolean;
|
||||
};
|
||||
type RoutingCombo = { id?: string; name?: string };
|
||||
type RoutingCombo = {
|
||||
id?: string;
|
||||
name?: string;
|
||||
config?: { compressionMode?: string } | null;
|
||||
};
|
||||
type LanguagePack = { language: string; ruleCount: number };
|
||||
|
||||
const EMPTY_PIPELINE: PipelineStep[] = [
|
||||
@@ -49,6 +54,7 @@ function NamedCombosManager() {
|
||||
const [assignmentIds, setAssignmentIds] = useState<string[]>([]);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [activeComboId, setActiveComboId] = useState<string | null>(null);
|
||||
const [compressionEnabled, setCompressionEnabled] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const refresh = () => {
|
||||
@@ -70,7 +76,10 @@ function NamedCombosManager() {
|
||||
.catch(() => {});
|
||||
fetch("/api/settings/compression")
|
||||
.then((res) => (res.ok ? res.json() : null))
|
||||
.then((data) => setActiveComboId(data?.activeComboId ?? null))
|
||||
.then((data) => {
|
||||
setActiveComboId(data?.activeComboId ?? null);
|
||||
setCompressionEnabled(Boolean(data?.enabled));
|
||||
})
|
||||
.catch(() => {});
|
||||
}, []);
|
||||
|
||||
@@ -255,14 +264,22 @@ function NamedCombosManager() {
|
||||
const id = combo.id ?? combo.name ?? "";
|
||||
if (!id) return null;
|
||||
return (
|
||||
<label key={id} className="flex items-center justify-between gap-2">
|
||||
<span className="truncate">{combo.name ?? id}</span>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={assignmentIds.includes(id)}
|
||||
onChange={(event) => toggleAssignment(id, event.target.checked)}
|
||||
<div key={id} className="flex items-center justify-between gap-2">
|
||||
<label className="flex min-w-0 flex-1 items-center justify-between gap-2">
|
||||
<span className="truncate">{combo.name ?? id}</span>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={assignmentIds.includes(id)}
|
||||
onChange={(event) => toggleAssignment(id, event.target.checked)}
|
||||
/>
|
||||
</label>
|
||||
<ComboCompressionModeSelect
|
||||
combo={{ id, config: combo.config }}
|
||||
disabled={!compressionEnabled}
|
||||
title="Compression override"
|
||||
className="w-24 shrink-0 rounded-lg border border-border bg-bg px-2 py-1 text-xs text-text-main disabled:opacity-50"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
)}
|
||||
|
||||
108
src/shared/components/compression/ComboCompressionModeSelect.tsx
Normal file
108
src/shared/components/compression/ComboCompressionModeSelect.tsx
Normal file
@@ -0,0 +1,108 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export interface ComboCompressionModeSelectCombo {
|
||||
id: string;
|
||||
config?: { compressionMode?: string } | null;
|
||||
compressionOverride?: string;
|
||||
}
|
||||
|
||||
export interface ComboCompressionModeSelectProps {
|
||||
combo: ComboCompressionModeSelectCombo;
|
||||
disabled?: boolean;
|
||||
title?: string;
|
||||
className?: string;
|
||||
onSaved?: (nextConfig: Record<string, unknown>) => void;
|
||||
}
|
||||
|
||||
const OPTIONS: Array<{ value: string; label: string }> = [
|
||||
{ value: "", label: "Default" },
|
||||
{ value: "off", label: "Off" },
|
||||
{ value: "lite", label: "Lite" },
|
||||
{ value: "standard", label: "Standard" },
|
||||
{ value: "aggressive", label: "Aggressive" },
|
||||
{ value: "ultra", label: "Ultra" },
|
||||
];
|
||||
|
||||
function getInitialCompressionMode(combo: ComboCompressionModeSelectCombo): string {
|
||||
const hasRuntimeConfig = combo?.config && typeof combo.config === "object";
|
||||
if (typeof combo?.config?.compressionMode === "string") {
|
||||
return combo.config.compressionMode;
|
||||
}
|
||||
return hasRuntimeConfig ? "" : combo.compressionOverride || "";
|
||||
}
|
||||
|
||||
// Extracted from src/app/(dashboard)/dashboard/combos/page.tsx so both the combo card
|
||||
// and the Compression Combos page (#6760) can persist the same per-routing-combo
|
||||
// compression-mode override through the same `PUT /api/combos/{id}` endpoint.
|
||||
//
|
||||
// Deliberately does NOT call `useTranslations` — this component is rendered from
|
||||
// CompressionCombosPageClient.tsx, which avoids page-level `useTranslations` to
|
||||
// prevent a documented production hydration regression. Callers that want localized
|
||||
// labels pass a resolved `title` string; option labels stay literal English.
|
||||
export function ComboCompressionModeSelect({
|
||||
combo,
|
||||
disabled,
|
||||
title,
|
||||
className,
|
||||
onSaved,
|
||||
}: ComboCompressionModeSelectProps) {
|
||||
const initialCompressionMode = getInitialCompressionMode(combo);
|
||||
const [compressionOverride, setCompressionOverride] = useState(initialCompressionMode);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setCompressionOverride(initialCompressionMode);
|
||||
}, [initialCompressionMode]);
|
||||
|
||||
const handleChange = async (value: string) => {
|
||||
setCompressionOverride(value);
|
||||
setIsSaving(true);
|
||||
const nextConfig: Record<string, unknown> = { ...(combo.config || {}) };
|
||||
if (value) {
|
||||
nextConfig.compressionMode = value;
|
||||
} else {
|
||||
delete nextConfig.compressionMode;
|
||||
}
|
||||
try {
|
||||
const response = await fetch(`/api/combos/${combo.id}`, {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ config: nextConfig }),
|
||||
});
|
||||
if (!response.ok) {
|
||||
console.error("Failed to update compression override");
|
||||
setCompressionOverride(initialCompressionMode);
|
||||
return;
|
||||
}
|
||||
onSaved?.(nextConfig);
|
||||
} catch (error) {
|
||||
console.error("Error updating compression override:", error);
|
||||
setCompressionOverride(initialCompressionMode);
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<select
|
||||
value={compressionOverride}
|
||||
onChange={(e) => handleChange(e.target.value)}
|
||||
disabled={disabled || isSaving}
|
||||
className={
|
||||
className ||
|
||||
"text-xs py-1 px-2 rounded border border-black/10 dark:border-white/10 bg-surface text-text-main focus:border-primary focus:outline-none transition-colors disabled:opacity-50"
|
||||
}
|
||||
title={title}
|
||||
>
|
||||
{OPTIONS.map((option) => (
|
||||
<option key={option.value} value={option.value} className="bg-surface text-text-main">
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
);
|
||||
}
|
||||
|
||||
export default ComboCompressionModeSelect;
|
||||
142
tests/unit/ui/combo-compression-mode-select.test.tsx
Normal file
142
tests/unit/ui/combo-compression-mode-select.test.tsx
Normal file
@@ -0,0 +1,142 @@
|
||||
// @vitest-environment jsdom
|
||||
import React, { act } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
||||
|
||||
const containers: HTMLElement[] = [];
|
||||
const roots: Array<{ unmount: () => void }> = [];
|
||||
|
||||
function mount(ui: React.ReactElement): HTMLElement {
|
||||
const container = document.createElement("div");
|
||||
document.body.appendChild(container);
|
||||
containers.push(container);
|
||||
const root = createRoot(container);
|
||||
roots.push(root);
|
||||
act(() => {
|
||||
root.render(ui);
|
||||
});
|
||||
return container;
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
(
|
||||
globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
|
||||
).IS_REACT_ACT_ENVIRONMENT = true;
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
vi.restoreAllMocks();
|
||||
await act(async () => {
|
||||
while (roots.length > 0) roots.pop()?.unmount();
|
||||
});
|
||||
for (let i = 0; i < 10; i++) await Promise.resolve();
|
||||
while (containers.length > 0) containers.pop()?.remove();
|
||||
document.body.innerHTML = "";
|
||||
});
|
||||
|
||||
async function flush() {
|
||||
await act(async () => {
|
||||
for (let i = 0; i < 10; i++) await Promise.resolve();
|
||||
});
|
||||
}
|
||||
|
||||
describe("ComboCompressionModeSelect (#6760)", () => {
|
||||
it("hydrates the initial value from combo.config.compressionMode", async () => {
|
||||
const { ComboCompressionModeSelect } = await import(
|
||||
"../../../src/shared/components/compression/ComboCompressionModeSelect"
|
||||
);
|
||||
const combo = { id: "c1", config: { compressionMode: "lite" } };
|
||||
const container = mount(<ComboCompressionModeSelect combo={combo} />);
|
||||
await flush();
|
||||
const select = container.querySelector("select") as HTMLSelectElement;
|
||||
expect(select.value).toBe("lite");
|
||||
});
|
||||
|
||||
it("hydrates from legacy combo.compressionOverride when config is absent", async () => {
|
||||
const { ComboCompressionModeSelect } = await import(
|
||||
"../../../src/shared/components/compression/ComboCompressionModeSelect"
|
||||
);
|
||||
const combo = { id: "c1", compressionOverride: "aggressive" };
|
||||
const container = mount(<ComboCompressionModeSelect combo={combo} />);
|
||||
await flush();
|
||||
const select = container.querySelector("select") as HTMLSelectElement;
|
||||
expect(select.value).toBe("aggressive");
|
||||
});
|
||||
|
||||
it("PUTs the correct config payload to /api/combos/{id} on selection change", async () => {
|
||||
const { ComboCompressionModeSelect } = await import(
|
||||
"../../../src/shared/components/compression/ComboCompressionModeSelect"
|
||||
);
|
||||
const calls: Array<{ url: string; init?: RequestInit }> = [];
|
||||
vi.spyOn(globalThis, "fetch").mockImplementation(async (input, init) => {
|
||||
calls.push({ url: input.toString(), init });
|
||||
return new Response(JSON.stringify({ ok: true }), { status: 200 });
|
||||
});
|
||||
const combo = { id: "c1", config: { compressionMode: "lite" } };
|
||||
const container = mount(<ComboCompressionModeSelect combo={combo} />);
|
||||
await flush();
|
||||
const select = container.querySelector("select") as HTMLSelectElement;
|
||||
await act(async () => {
|
||||
select.value = "standard";
|
||||
select.dispatchEvent(new Event("change", { bubbles: true }));
|
||||
});
|
||||
await flush();
|
||||
expect(calls).toHaveLength(1);
|
||||
expect(calls[0].url).toBe("/api/combos/c1");
|
||||
expect(calls[0].init?.method).toBe("PUT");
|
||||
const body = JSON.parse(calls[0].init?.body as string);
|
||||
expect(body).toEqual({ config: { compressionMode: "standard" } });
|
||||
});
|
||||
|
||||
it('selecting "Default" removes compressionMode from the PUT payload', async () => {
|
||||
const { ComboCompressionModeSelect } = await import(
|
||||
"../../../src/shared/components/compression/ComboCompressionModeSelect"
|
||||
);
|
||||
const calls: Array<{ init?: RequestInit }> = [];
|
||||
vi.spyOn(globalThis, "fetch").mockImplementation(async (_input, init) => {
|
||||
calls.push({ init });
|
||||
return new Response(JSON.stringify({ ok: true }), { status: 200 });
|
||||
});
|
||||
const combo = { id: "c1", config: { compressionMode: "lite" } };
|
||||
const container = mount(<ComboCompressionModeSelect combo={combo} />);
|
||||
await flush();
|
||||
const select = container.querySelector("select") as HTMLSelectElement;
|
||||
await act(async () => {
|
||||
select.value = "";
|
||||
select.dispatchEvent(new Event("change", { bubbles: true }));
|
||||
});
|
||||
await flush();
|
||||
const body = JSON.parse(calls[0].init?.body as string);
|
||||
expect(body).toEqual({ config: {} });
|
||||
});
|
||||
|
||||
it("rolls back the displayed value when the PUT response is not OK", async () => {
|
||||
const { ComboCompressionModeSelect } = await import(
|
||||
"../../../src/shared/components/compression/ComboCompressionModeSelect"
|
||||
);
|
||||
vi.spyOn(globalThis, "fetch").mockImplementation(async () => {
|
||||
return new Response(JSON.stringify({ error: "nope" }), { status: 500 });
|
||||
});
|
||||
const combo = { id: "c1", config: { compressionMode: "lite" } };
|
||||
const container = mount(<ComboCompressionModeSelect combo={combo} />);
|
||||
await flush();
|
||||
const select = container.querySelector("select") as HTMLSelectElement;
|
||||
await act(async () => {
|
||||
select.value = "ultra";
|
||||
select.dispatchEvent(new Event("change", { bubbles: true }));
|
||||
});
|
||||
await flush();
|
||||
expect(select.value).toBe("lite");
|
||||
});
|
||||
|
||||
it("disables the control when disabled=true", async () => {
|
||||
const { ComboCompressionModeSelect } = await import(
|
||||
"../../../src/shared/components/compression/ComboCompressionModeSelect"
|
||||
);
|
||||
const combo = { id: "c1", config: { compressionMode: "lite" } };
|
||||
const container = mount(<ComboCompressionModeSelect combo={combo} disabled />);
|
||||
await flush();
|
||||
const select = container.querySelector("select") as HTMLSelectElement;
|
||||
expect(select.disabled).toBe(true);
|
||||
});
|
||||
});
|
||||
164
tests/unit/ui/compression-combos-routing-mode-6760.test.tsx
Normal file
164
tests/unit/ui/compression-combos-routing-mode-6760.test.tsx
Normal file
@@ -0,0 +1,164 @@
|
||||
// @vitest-environment jsdom
|
||||
import React, { act } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
||||
|
||||
const containers: HTMLElement[] = [];
|
||||
const roots: Array<{ unmount: () => void }> = [];
|
||||
|
||||
function mount(ui: React.ReactElement): HTMLElement {
|
||||
const container = document.createElement("div");
|
||||
document.body.appendChild(container);
|
||||
containers.push(container);
|
||||
const root = createRoot(container);
|
||||
roots.push(root);
|
||||
act(() => {
|
||||
root.render(ui);
|
||||
});
|
||||
return container;
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
(
|
||||
globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
|
||||
).IS_REACT_ACT_ENVIRONMENT = true;
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
vi.restoreAllMocks();
|
||||
await act(async () => {
|
||||
while (roots.length > 0) roots.pop()?.unmount();
|
||||
});
|
||||
for (let i = 0; i < 10; i++) await Promise.resolve();
|
||||
while (containers.length > 0) containers.pop()?.remove();
|
||||
document.body.innerHTML = "";
|
||||
});
|
||||
|
||||
async function flush() {
|
||||
await act(async () => {
|
||||
for (let i = 0; i < 10; i++) await Promise.resolve();
|
||||
});
|
||||
}
|
||||
|
||||
function compressionCombo(id: string, name: string) {
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
description: `${name} desc`,
|
||||
pipeline: [{ engine: "rtk", intensity: "standard" }],
|
||||
languagePacks: ["en"],
|
||||
outputMode: false,
|
||||
outputModeIntensity: "full",
|
||||
isDefault: false,
|
||||
};
|
||||
}
|
||||
|
||||
function setupFetchMock() {
|
||||
const calls: Array<{ url: string; init?: RequestInit }> = [];
|
||||
const json = (body: unknown, status = 200) =>
|
||||
new Response(JSON.stringify(body), {
|
||||
status,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
const routingCombos = [
|
||||
{ id: "rc1", name: "Routing Alpha", config: { compressionMode: "lite" } },
|
||||
{ id: "rc2", name: "Routing Bravo" },
|
||||
];
|
||||
vi.spyOn(globalThis, "fetch").mockImplementation(
|
||||
async (input: RequestInfo | URL, init?: RequestInit) => {
|
||||
const url = input.toString();
|
||||
calls.push({ url, init });
|
||||
if (url.includes("/api/context/combos/") && url.includes("/assignments")) {
|
||||
return json({ assignments: [] });
|
||||
}
|
||||
if (url.includes("/api/context/combos")) {
|
||||
return json({ combos: [compressionCombo("cc1", "Named Combo")] });
|
||||
}
|
||||
if (url.includes("/api/combos/")) {
|
||||
return json({ ok: true });
|
||||
}
|
||||
if (url.includes("/api/combos")) {
|
||||
return json({ combos: routingCombos });
|
||||
}
|
||||
if (url.includes("/api/compression/language-packs")) {
|
||||
return json({ packs: [] });
|
||||
}
|
||||
if (url.includes("/api/settings/compression")) {
|
||||
return json({ activeComboId: null, enabled: true });
|
||||
}
|
||||
return json({}, 404);
|
||||
}
|
||||
);
|
||||
return calls;
|
||||
}
|
||||
|
||||
describe("CompressionCombosPageClient — routing-combo compression mode selector (#6760)", () => {
|
||||
async function render() {
|
||||
const { default: CompressionCombosPageClient } = await import(
|
||||
"../../../src/app/(dashboard)/dashboard/context/combos/CompressionCombosPageClient"
|
||||
);
|
||||
let container!: HTMLElement;
|
||||
await act(async () => {
|
||||
container = mount(<CompressionCombosPageClient />);
|
||||
});
|
||||
await flush();
|
||||
return container;
|
||||
}
|
||||
|
||||
it("renders a compression-mode select per routing combo, hydrated from combo.config", async () => {
|
||||
setupFetchMock();
|
||||
const container = await render();
|
||||
expect(container.textContent).toContain("Routing Alpha");
|
||||
expect(container.textContent).toContain("Routing Bravo");
|
||||
const selects = Array.from(container.querySelectorAll("select")) as HTMLSelectElement[];
|
||||
// one per routing combo (2) — the shared ComboCompressionModeSelect renders exactly
|
||||
// the 6-option Default/Off/Lite/Standard/Aggressive/Ultra set.
|
||||
const modeSelects = selects.filter((s) => {
|
||||
const values = Array.from(s.options).map((o) => o.value);
|
||||
return values.join(",") === ",off,lite,standard,aggressive,ultra";
|
||||
});
|
||||
expect(modeSelects).toHaveLength(2);
|
||||
expect(modeSelects[0].value).toBe("lite");
|
||||
expect(modeSelects[1].value).toBe("");
|
||||
});
|
||||
|
||||
it("changing the routing combo's selector PUTs /api/combos/{id} independently of the assignment checkbox", async () => {
|
||||
const calls = setupFetchMock();
|
||||
const container = await render();
|
||||
const selects = Array.from(container.querySelectorAll("select")) as HTMLSelectElement[];
|
||||
const modeSelects = selects.filter((s) => {
|
||||
const values = Array.from(s.options).map((o) => o.value);
|
||||
return values.join(",") === ",off,lite,standard,aggressive,ultra";
|
||||
});
|
||||
await act(async () => {
|
||||
modeSelects[0].value = "ultra";
|
||||
modeSelects[0].dispatchEvent(new Event("change", { bubbles: true }));
|
||||
});
|
||||
await flush();
|
||||
|
||||
const putCalls = calls.filter(
|
||||
(c) => c.url === "/api/combos/rc1" && c.init?.method === "PUT"
|
||||
);
|
||||
expect(putCalls).toHaveLength(1);
|
||||
const body = JSON.parse(putCalls[0].init?.body as string);
|
||||
expect(body).toEqual({ config: { compressionMode: "ultra" } });
|
||||
|
||||
// The assignment checkbox for the same routing combo still toggles independently.
|
||||
const checkbox = container.querySelector(
|
||||
'input[type="checkbox"]'
|
||||
) as HTMLInputElement | null;
|
||||
expect(checkbox).toBeTruthy();
|
||||
const before = checkbox!.checked;
|
||||
await act(async () => {
|
||||
checkbox!.click();
|
||||
});
|
||||
await flush();
|
||||
expect(checkbox!.checked).toBe(!before);
|
||||
|
||||
// The unrelated outputMode/outputModeIntensity fields were not touched by the change above.
|
||||
const outputModeIntensitySelect = selects.find((s) =>
|
||||
Array.from(s.options).some((o) => o.value === "full")
|
||||
) as HTMLSelectElement;
|
||||
expect(outputModeIntensitySelect.value).toBe("full");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user