Files
OmniRoute/tests/unit/ui/api-manager-loading-status-12066.test.tsx
Paco Cartones 7ad5e1120e fix(api-manager): accessible loading status on the skeleton gate (#12541)
A real accessibility hole, not a cosmetic one: for the whole loading window the page exposed nothing but the sidebar, which is exactly the "API Keys link does nothing" report. Reusing the `role="status" aria-live="polite"` container the other dashboard loading states already use keeps it consistent.

---

Validated in one consolidated worktree cut from `release/v3.8.51`, boarded with the rest of this batch — zero conflicts between the 19 PRs.

- `typecheck:core` clean; `check:dashboard-typecheck` OK (206 pre-existing, all within the frozen baseline); `check:changelog-integrity` OK
- complexity 2802 / baseline 3218 and cognitive-complexity 1267 / baseline 1437 — both under baseline
- 226 of 228 focused assertions green across the batch's 23 test files. The 2 remaining belong to #12551, which is held separately.

Two batch-owned defects were found and fixed in flight, both pure base drift: `173_xp_action_counts.sql` collided with `173_call_logs_video_content_removed.sql` (renumbered to 176 on #12651 — it aborted every DB open, which is what 53 of the first run's failures were), and the feature-flag catalog was missing the `SERVER_OWNED_TOOL_LOOP_ENABLED` row the base gained after #12552 was written.

⚠️ base-red inherited: #12732 — `Docs Gates`, `Merge integrity`, `No new ESLint warnings`, `Unit Tests fast-path` and `Fast Quality Gates` reproduce on the pure tip (provider count 356 vs the 358 the modules define, SKILL.md drift, and `open-sse/utils/stream.ts` at 3115 > frozen 3098, which this batch does not touch).

Thanks @pacocartones — the `file:line` citations and the explicit out-of-scope notes on every one of these made a 19-PR batch reviewable in one pass.
2026-09-11 17:42:05 -03:00

77 lines
2.5 KiB
TypeScript

// @vitest-environment jsdom
import React, { act } from "react";
import { createRoot } from "react-dom/client";
import { afterEach, describe, expect, it, vi } from "vitest";
(
globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
).IS_REACT_ACT_ENVIRONMENT = true;
const translate = (key: string) => key;
vi.mock("next-intl", () => ({
useLocale: () => "en",
useTranslations: () => Object.assign(translate, { has: () => false, rich: translate }),
}));
const { default: ApiManagerPageClient } =
await import("@/app/(dashboard)/dashboard/api-manager/ApiManagerPageClient");
const roots: Array<{ root: ReturnType<typeof createRoot>; container: HTMLDivElement }> = [];
function mountPage() {
const container = document.createElement("div");
document.body.appendChild(container);
const root = createRoot(container);
roots.push({ root, container });
act(() => root.render(<ApiManagerPageClient />));
return container;
}
afterEach(() => {
for (const { root, container } of roots.splice(0)) {
act(() => root.unmount());
container.remove();
}
vi.restoreAllMocks();
vi.unstubAllGlobals();
});
describe("API manager loading gate accessibility (#12066)", () => {
it("exposes a busy polite status while the initial /api/keys fetch is pending", () => {
// Never settles: the page stays on its skeleton gate for the whole test.
vi.stubGlobal(
"fetch",
vi.fn(() => new Promise(() => undefined))
);
const container = mountPage();
const status = container.querySelector('[role="status"]');
expect(status).not.toBeNull();
expect(status?.getAttribute("aria-live")).toBe("polite");
expect(status?.getAttribute("aria-busy")).toBe("true");
// The only text in the accessibility tree during the gate is the loading label.
expect(status?.textContent).toContain("loading");
// The skeleton cards themselves stay decorative.
expect(container.querySelectorAll('[aria-hidden="true"]').length).toBeGreaterThan(0);
});
it("drops the loading status once /api/keys has settled", async () => {
vi.stubGlobal(
"fetch",
vi.fn(async () => ({ ok: true, json: async () => ({}) }))
);
const container = mountPage();
for (let i = 0; i < 40 && container.querySelector('[role="status"]'); i++) {
await act(async () => {
await new Promise((resolve) => setTimeout(resolve, 10));
});
}
expect(container.querySelector('[role="status"]')).toBeNull();
expect(container.querySelector("h1")).not.toBeNull();
});
});