mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-21 22:32:22 +03:00
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.
77 lines
2.5 KiB
TypeScript
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();
|
|
});
|
|
});
|