Files
OmniRoute/tests/unit/ui/CompressionHub-patch-only.test.tsx
nguyenha935 4012bac41d fix(i18n): preserve remaining Vietnamese localization (#7935)
* fix(i18n): preserve remaining Vietnamese localization

* chore(quality): rebaseline file-size cap for 9 dashboard components (i18n wiring)

Restoring the Vietnamese localization on 9 dashboard components (useTranslations
wiring + t()/tc() call-site swaps for previously hardcoded strings) grows each
file by a small, irreducible amount. Bumps the frozen file-size-baseline.json
caps to match, with a justification entry per the project's own ratchet policy.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* fix(i18n): wire weekday localization + add missing qwen CLI description

Two gaps left by this PR's own new contract tests, caught while
reconciling the branch against the release tip:

- CostOverviewTab.tsx added formatWeekdayLabel() but never called it;
  the Weekly Usage Pattern chart still showed raw English day
  abbreviations regardless of locale. Now maps weeklyPattern rows
  through it before handing them to WeeklyPatternCard.
- cliTools.toolDescriptions was missing an entry for "qwen" (a
  baseUrlSupport:"full" tool) in both en.json and vi.json, failing
  the PR's own cli-catalog-display-contract.test.ts.

Covered by the PR's existing tests/unit/dashboard-localization-contract.test.ts
and tests/unit/cli-catalog-display-contract.test.ts (both now pass).

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* i18n(vi): backfill the 4 proxySubscription keys #7299 added to en.json

#7299 (proxy subscriptions) merged while this branch was rebasing, adding
settings.proxySubscriptionsTab and settings.proxySubscription.error.{LOCAL_CORE_ENDPOINT_INVALID,
NEEDS_CORE_NOT_CONFIGURED,NO_USABLE_NODES} to en.json. This PR's own
i18n-vi-completeness contract asserts full en↔vi key parity, so the merge of the
current release tip surfaced them as missing. Adds the Vietnamese translations,
keeping the SS/VMess/Trojan/VLESS/SOCKS5 technical terms verbatim.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: nguyenha935 <208228297+nguyenha935@users.noreply.github.com>
Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
Co-authored-by: nguyenha935 <nguyenha935@users.noreply.github.com>
2026-07-21 13:41:02 -03:00

164 lines
6.0 KiB
TypeScript

// @vitest-environment jsdom
/**
* Guards fix for issue #6039:
* CompressionHub was sending the full merged settings object to PUT
* /api/settings/compression instead of only the patch. The API schema uses
* .strict() Zod validation, so any field present in CompressionConfig but
* absent from compressionSettingsUpdateSchema (e.g. contextBudget, or
* stackedPipeline steps using engines not yet in the discriminated union)
* would cause a 400 validation failure — making switching to any non-default
* combo fail silently.
*
* Fix: send `patch` (the caller-supplied partial update) instead of `next`
* (the full optimistically-merged state).
*/
import React from "react";
import { act } from "react";
import { createRoot } from "react-dom/client";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { NextIntlClientProvider } from "next-intl";
import messages from "../../../src/i18n/messages/en.json";
// ── Mocks ─────────────────────────────────────────────────────────────────────
// Stub next/navigation (not used by CompressionHub but required by module graph)
vi.mock("next/navigation", () => ({
useRouter: () => ({ push: vi.fn() }),
usePathname: () => "/",
}));
// Track all fetch calls
const fetchCalls: { url: string; init: RequestInit }[] = [];
const mockFetch = vi.fn().mockImplementation((url: string, init: RequestInit) => {
fetchCalls.push({ url, init });
return Promise.resolve({
ok: true,
json: async () => ({ combos: [] }),
});
});
vi.stubGlobal("fetch", mockFetch);
// Import after mocks are in place
import CompressionHub from "../../../src/app/(dashboard)/dashboard/context/combos/CompressionHub";
// ── Helpers ───────────────────────────────────────────────────────────────────
function getLastPutBody(): Record<string, unknown> | null {
const putCall = [...fetchCalls].reverse().find((c) => c.init?.method === "PUT");
if (!putCall) return null;
return JSON.parse(putCall.init.body as string);
}
// ── Tests ─────────────────────────────────────────────────────────────────────
describe("CompressionHub — PUT sends patch only, not full settings", () => {
let container: HTMLDivElement;
let root: ReturnType<typeof createRoot>;
beforeEach(() => {
fetchCalls.length = 0;
mockFetch.mockClear();
// GET /api/settings/compression returns a config with extra fields that
// are NOT in compressionSettingsUpdateSchema (simulates contextBudget etc.)
mockFetch.mockImplementationOnce((_url: string) =>
Promise.resolve({
ok: true,
json: async () => ({
enabled: true,
defaultMode: "lite",
activeComboId: null,
contextEditing: { enabled: false },
// Field present in CompressionConfig but NOT in the update schema:
contextBudget: { mode: "floor", floorTokens: 4096 },
// Engine step type not in stackedPipelineStepSchema discriminated union:
stackedPipeline: [
{ engine: "headroom", intensity: "standard" },
{ engine: "caveman", intensity: "lite" },
],
}),
})
);
// GET /api/settings/compression/combos
mockFetch.mockImplementationOnce((_url: string) =>
Promise.resolve({
ok: true,
json: async () => ({ combos: [{ id: "c1", name: "My Combo", pipeline: [] }] }),
})
);
container = document.createElement("div");
document.body.appendChild(container);
root = createRoot(container);
});
afterEach(() => {
act(() => {
root.unmount();
});
document.body.removeChild(container);
});
it("sends only the changed field when activeComboId is updated", async () => {
await act(async () => {
root.render(
<NextIntlClientProvider locale="en" messages={{ contextCombos: messages.contextCombos }}>
<CompressionHub />
</NextIntlClientProvider>
);
});
// Find the combo selector and change it to "c1"
const select = container.querySelector("select");
expect(select).not.toBeNull();
await act(async () => {
if (select) {
Object.defineProperty(select, "value", { writable: true, value: "c1" });
select.dispatchEvent(new Event("change", { bubbles: true }));
}
});
const body = getLastPutBody();
expect(body).not.toBeNull();
// Must contain the changed field
expect(body).toHaveProperty("activeComboId", "c1");
// Must NOT contain fields from the full settings that would fail strict
// schema validation (contextBudget, stackedPipeline with unknown engines)
expect(body).not.toHaveProperty("contextBudget");
expect(body).not.toHaveProperty("stackedPipeline");
expect(body).not.toHaveProperty("enabled");
expect(body).not.toHaveProperty("defaultMode");
});
it("sends only the toggle field when contextEditing is toggled", async () => {
await act(async () => {
root.render(
<NextIntlClientProvider locale="en" messages={{ contextCombos: messages.contextCombos }}>
<CompressionHub />
</NextIntlClientProvider>
);
});
// Find the context editing toggle button
const toggleButtons = container.querySelectorAll("button[role='switch']");
expect(toggleButtons.length).toBeGreaterThan(0);
await act(async () => {
(toggleButtons[0] as HTMLButtonElement).click();
});
const body = getLastPutBody();
expect(body).not.toBeNull();
// Should only include contextEditing, not the full settings
expect(body).toHaveProperty("contextEditing");
expect(body).not.toHaveProperty("contextBudget");
expect(body).not.toHaveProperty("stackedPipeline");
});
});