Files
AgentHub/client/src/lib/__tests__/apiJsonGuard.test.ts
T
nntrivi2001 8fb703c6db feat(a11y): contrast tokens, accent-solid, responsive drawer, keyboard menus
- Contrast: --fg-muted reaches 4.5:1 on every surface in all nine themes
  (dark default was 2.55:1); status chips and accent text clear 4.5:1 on
  their own tints. New --accent-solid(/-hover) + tailwind `accent-solid`
  is the fill behind white text (.btn-primary, Switch on, Checkbox tick);
  --accent is now a text colour. Switch off / unchecked Checkbox get a
  >=3:1 edge.
- <html lang> follows the chosen language; a global prefers-reduced-motion
  rule; index.html body uses a token instead of text-gray-200.
- Layout: the sidebar starts collapsed below lg and becomes an off-canvas
  drawer below md (menu button, backdrop, Escape, useDialog).
- ContextMenu: focus moves in, arrows/Home/End, Escape/Tab close, focus
  returns, a keyboard-opened menu anchors to its target. Select is a real
  listbox button (aria-expanded, activedescendant, type-ahead). PageTabs
  implements APG tabs with a tabpanel. New .btn-secondary/.btn-danger/
  .btn-icon and a .select class for native selects, all with focus rings.
- StatusBar announces connection and "N need you" politely; Sidebar's
  connection modal uses useDialog; EmptyState/Skeleton/AiToolIcon a11y.
- api.ts request(): a non-JSON 200 throws a translated EBADRESPONSE
  instead of "Unexpected token '<'".
2026-10-05 13:22:00 +07:00

45 lines
1.4 KiB
TypeScript

/**
* @file apiJsonGuard.test.ts
* @description `request()` refuses a 200 that is not JSON (an index.html from a
* proxy or dev server) with a translated error instead of the raw
* "Unexpected token '<'" a bare `res.json()` would throw.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
import { describe, it, expect, vi, afterEach } from "vitest";
import i18n from "i18next";
import { api } from "../api";
afterEach(() => vi.unstubAllGlobals());
function stub(type: string | null, body: string) {
vi.stubGlobal(
"fetch",
vi
.fn()
.mockResolvedValue(
new Response(body, { status: 200, headers: type ? { "content-type": type } : {} })
)
);
}
describe("request() JSON guard", () => {
it("throws EBADRESPONSE with the translated message on an HTML 200", async () => {
stub("text/html; charset=utf-8", "<!doctype html><html></html>");
await expect(api.aiTools.list()).rejects.toMatchObject({
code: "EBADRESPONSE",
message: i18n.t("errors:badResponse"),
});
});
it("throws the same error when a JSON-less body fails to parse", async () => {
stub(null, "<html>");
await expect(api.aiTools.list()).rejects.toMatchObject({ code: "EBADRESPONSE" });
});
it("still parses a JSON 200", async () => {
stub("application/json", JSON.stringify({ default: "claude", tools: [] }));
await expect(api.aiTools.list()).resolves.toEqual({ default: "claude", tools: [] });
});
});