8fb703c6db
- 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 '<'".
45 lines
1.4 KiB
TypeScript
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: [] });
|
|
});
|
|
});
|