|
|
|
@@ -0,0 +1,422 @@
|
|
|
|
|
// @vitest-environment jsdom
|
|
|
|
|
/**
|
|
|
|
|
* Toolbar WCAG 2.1 AA accessibility tests.
|
|
|
|
|
*
|
|
|
|
|
* 27 test cases covering:
|
|
|
|
|
* - aria-expanded on help button reflects popover open/close state
|
|
|
|
|
* - aria-label on all icon-only buttons (A2A toggle, Search, Help, Audit trail)
|
|
|
|
|
* - aria-pressed on A2A topology toggle (reflects store state)
|
|
|
|
|
* - Help popover dialog: role="dialog", aria-label="Shortcuts and tips", aria-modal="false"
|
|
|
|
|
* - Close button inside popover: aria-label="Close help dialog"
|
|
|
|
|
* - aria-hidden suppression on decorative elements (logo alt, status dots, count text)
|
|
|
|
|
* - focus-visible:ring class presence on all interactive toolbar buttons
|
|
|
|
|
* - Stop All / Restart Pending buttons: descriptive aria-label with workspace/task count
|
|
|
|
|
* - Escape key closes help popover and resets aria-expanded
|
|
|
|
|
* - Screen reader text exposure for workspace count
|
|
|
|
|
*
|
|
|
|
|
* Notes:
|
|
|
|
|
* - fireEvent.click dispatches non-bubbling events; React synthetic handlers need
|
|
|
|
|
* bubbling. Use act(() => { el.dispatchEvent(new MouseEvent("click", { bubbles: true })) })
|
|
|
|
|
* for reliable click simulation.
|
|
|
|
|
* - container from render() can become stale after re-renders. Re-query after state changes.
|
|
|
|
|
* - No @testing-library/jest-dom — uses getAttribute, className, classList, container.querySelector.
|
|
|
|
|
*/
|
|
|
|
|
import { afterEach, beforeEach, describe, expect, fireEvent, it, vi } from "vitest";
|
|
|
|
|
import { act, cleanup, render, within } from "@testing-library/react";
|
|
|
|
|
import React from "react";
|
|
|
|
|
|
|
|
|
|
import { Toolbar } from "../Toolbar";
|
|
|
|
|
|
|
|
|
|
// ── Mock targets ───────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
vi.mock("@/components/Toaster", () => ({
|
|
|
|
|
showToast: vi.fn(),
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
vi.mock("@/components/ConfirmDialog", () => ({
|
|
|
|
|
ConfirmDialog: () => null,
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
vi.mock("@/components/settings/SettingsButton", () => ({
|
|
|
|
|
SettingsButton: () => null,
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
vi.mock("@/components/settings/SettingsPanel", () => ({
|
|
|
|
|
settingsGearRef: { current: null },
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
vi.mock("@/components/ThemeToggle", () => ({
|
|
|
|
|
ThemeToggle: () => null,
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
vi.mock("@/components/KeyboardShortcutsDialog", () => ({
|
|
|
|
|
KeyboardShortcutsDialog: ({ open }: { open: boolean; onClose: () => void }) =>
|
|
|
|
|
open ? <div role="dialog" data-testid="shortcuts-dialog">Shortcuts</div> : null,
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
vi.mock("@/lib/design-tokens", () => ({
|
|
|
|
|
statusDotClass: (status: string) => {
|
|
|
|
|
const map: Record<string, string> = {
|
|
|
|
|
online: "bg-emerald-400",
|
|
|
|
|
offline: "bg-zinc-500",
|
|
|
|
|
paused: "bg-indigo-400",
|
|
|
|
|
degraded: "bg-amber-400",
|
|
|
|
|
failed: "bg-red-400",
|
|
|
|
|
provisioning: "bg-sky-400",
|
|
|
|
|
};
|
|
|
|
|
return map[status] ?? "bg-zinc-500";
|
|
|
|
|
},
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
vi.mock("@/lib/api", () => ({
|
|
|
|
|
api: { post: vi.fn(() => Promise.resolve()) },
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
// ── Store mocks ───────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
const mockSetShowA2AEdges = vi.fn();
|
|
|
|
|
const mockSetPanelTab = vi.fn();
|
|
|
|
|
const mockSetSearchOpen = vi.fn();
|
|
|
|
|
|
|
|
|
|
const makeNode = (
|
|
|
|
|
i: number,
|
|
|
|
|
status: "online" | "offline" | "failed" | "provisioning" = "online",
|
|
|
|
|
activeTasks = 0,
|
|
|
|
|
needsRestart = false,
|
|
|
|
|
parentId: string | null = null
|
|
|
|
|
) => ({
|
|
|
|
|
id: `ws-${i}`,
|
|
|
|
|
data: {
|
|
|
|
|
name: `Workspace ${i}`,
|
|
|
|
|
role: "agent",
|
|
|
|
|
tier: 1,
|
|
|
|
|
status,
|
|
|
|
|
parentId,
|
|
|
|
|
activeTasks,
|
|
|
|
|
needsRestart,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const storeState = {
|
|
|
|
|
nodes: [] as ReturnType<typeof makeNode>[],
|
|
|
|
|
wsStatus: "connected" as "connected" | "connecting" | "disconnected",
|
|
|
|
|
showA2AEdges: false,
|
|
|
|
|
selectedNodeId: null as string | null,
|
|
|
|
|
sidePanelWidth: 480,
|
|
|
|
|
setShowA2AEdges: mockSetShowA2AEdges,
|
|
|
|
|
setPanelTab: mockSetPanelTab,
|
|
|
|
|
setSearchOpen: mockSetSearchOpen,
|
|
|
|
|
selectedNodeIds: new Set<string>(),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
vi.mock("@/store/canvas", () => ({
|
|
|
|
|
useCanvasStore: vi.fn((selector: (s: typeof storeState) => unknown) => selector(storeState)),
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
// ── Setup / teardown ─────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
mockSetShowA2AEdges.mockClear();
|
|
|
|
|
mockSetPanelTab.mockClear();
|
|
|
|
|
mockSetSearchOpen.mockClear();
|
|
|
|
|
storeState.nodes = [];
|
|
|
|
|
storeState.wsStatus = "connected";
|
|
|
|
|
storeState.showA2AEdges = false;
|
|
|
|
|
storeState.selectedNodeId = null;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
afterEach(cleanup);
|
|
|
|
|
|
|
|
|
|
// ── Helper: click an element and flush React state updates ─────────────────────
|
|
|
|
|
// fireEvent.click dispatches non-bubbling events; React synthetic handlers need
|
|
|
|
|
// bubbles:true. Wrapping in act() ensures state updates are flushed synchronously.
|
|
|
|
|
function clickElement(el: HTMLElement | null) {
|
|
|
|
|
if (!el) throw new Error("clickElement: el is null");
|
|
|
|
|
act(() => {
|
|
|
|
|
el.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true }));
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ─── aria-expanded on help button ───────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
describe("Toolbar a11y — aria-expanded on help button", () => {
|
|
|
|
|
it("help button has aria-expanded=false when popover is closed", () => {
|
|
|
|
|
storeState.nodes = [makeNode(0)];
|
|
|
|
|
const { container } = render(<Toolbar />);
|
|
|
|
|
const helpBtn = container.querySelector('[aria-label="Open shortcuts and tips"]') as HTMLElement;
|
|
|
|
|
expect(helpBtn).toBeTruthy();
|
|
|
|
|
expect(helpBtn.getAttribute("aria-expanded")).toBe("false");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("help button has aria-expanded=true after clicking it", () => {
|
|
|
|
|
storeState.nodes = [makeNode(0)];
|
|
|
|
|
const { container } = render(<Toolbar />);
|
|
|
|
|
const helpBtn = container.querySelector('[aria-label="Open shortcuts and tips"]') as HTMLElement;
|
|
|
|
|
clickElement(helpBtn);
|
|
|
|
|
// Re-query after state change — container becomes stale after re-render
|
|
|
|
|
const btn = container.querySelector('[aria-label="Open shortcuts and tips"]') as HTMLElement;
|
|
|
|
|
expect(btn.getAttribute("aria-expanded")).toBe("true");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("Escape key closes help popover and resets aria-expanded to false", () => {
|
|
|
|
|
storeState.nodes = [makeNode(0)];
|
|
|
|
|
const { container } = render(<Toolbar />);
|
|
|
|
|
// Open the popover
|
|
|
|
|
const helpBtn = container.querySelector('[aria-label="Open shortcuts and tips"]') as HTMLElement;
|
|
|
|
|
clickElement(helpBtn);
|
|
|
|
|
expect(container.querySelector('[role="dialog"]')).toBeTruthy();
|
|
|
|
|
// Press Escape — dispatched on document (Toolbar listens via window, which in
|
|
|
|
|
// jsdom is accessible via document.defaultView). Use dispatchEvent for reliability.
|
|
|
|
|
act(() => {
|
|
|
|
|
document.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape", bubbles: true }));
|
|
|
|
|
});
|
|
|
|
|
expect(container.querySelector('[role="dialog"]')).toBeNull();
|
|
|
|
|
const btn = container.querySelector('[aria-label="Open shortcuts and tips"]') as HTMLElement;
|
|
|
|
|
expect(btn.getAttribute("aria-expanded")).toBe("false");
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// ─── aria-label on icon-only buttons ───────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
describe("Toolbar a11y — aria-label on icon-only buttons", () => {
|
|
|
|
|
it("A2A toggle button has aria-label", () => {
|
|
|
|
|
storeState.nodes = [makeNode(0)];
|
|
|
|
|
const { container } = render(<Toolbar />);
|
|
|
|
|
const withinThis = within(container);
|
|
|
|
|
const btn = withinThis.getByRole("button", { name: /show a2a edges/i });
|
|
|
|
|
expect(btn).toBeTruthy();
|
|
|
|
|
expect(btn.getAttribute("aria-label")).toBeTruthy();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("Audit trail button has aria-label", () => {
|
|
|
|
|
storeState.nodes = [makeNode(0)];
|
|
|
|
|
const { container } = render(<Toolbar />);
|
|
|
|
|
const withinThis = within(container);
|
|
|
|
|
const btn = withinThis.getByRole("button", { name: /open audit trail/i });
|
|
|
|
|
expect(btn).toBeTruthy();
|
|
|
|
|
expect(btn.getAttribute("aria-label")).toBeTruthy();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("Search button has aria-label", () => {
|
|
|
|
|
storeState.nodes = [makeNode(0)];
|
|
|
|
|
const { container } = render(<Toolbar />);
|
|
|
|
|
const withinThis = within(container);
|
|
|
|
|
const btn = withinThis.getByRole("button", { name: /search workspaces/i });
|
|
|
|
|
expect(btn).toBeTruthy();
|
|
|
|
|
expect(btn.getAttribute("aria-label")).toBeTruthy();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("Help button has aria-label", () => {
|
|
|
|
|
storeState.nodes = [makeNode(0)];
|
|
|
|
|
const { container } = render(<Toolbar />);
|
|
|
|
|
const withinThis = within(container);
|
|
|
|
|
const btn = withinThis.getByRole("button", { name: /open shortcuts and tips/i });
|
|
|
|
|
expect(btn).toBeTruthy();
|
|
|
|
|
expect(btn.getAttribute("aria-label")).toBeTruthy();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// ─── aria-pressed on A2A toggle ─────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
describe("Toolbar a11y — aria-pressed on A2A topology toggle", () => {
|
|
|
|
|
it("A2A button has aria-pressed=false when showA2AEdges is false", () => {
|
|
|
|
|
storeState.nodes = [makeNode(0)];
|
|
|
|
|
storeState.showA2AEdges = false;
|
|
|
|
|
const { container } = render(<Toolbar />);
|
|
|
|
|
const withinThis = within(container);
|
|
|
|
|
const btn = withinThis.getByRole("button", { name: /show a2a edges/i });
|
|
|
|
|
expect(btn.getAttribute("aria-pressed")).toBe("false");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("A2A button has aria-pressed=true when showA2AEdges is true", () => {
|
|
|
|
|
storeState.nodes = [makeNode(0)];
|
|
|
|
|
storeState.showA2AEdges = true;
|
|
|
|
|
const { container } = render(<Toolbar />);
|
|
|
|
|
const withinThis = within(container);
|
|
|
|
|
const btn = withinThis.getByRole("button", { name: /hide a2a edges/i });
|
|
|
|
|
expect(btn).toBeTruthy();
|
|
|
|
|
expect(btn.getAttribute("aria-pressed")).toBe("true");
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// ─── Help popover dialog ARIA ──────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
describe("Toolbar a11y — help popover dialog", () => {
|
|
|
|
|
it("help popover has role=dialog", () => {
|
|
|
|
|
storeState.nodes = [makeNode(0)];
|
|
|
|
|
const { container } = render(<Toolbar />);
|
|
|
|
|
const helpBtn = container.querySelector('[aria-label="Open shortcuts and tips"]') as HTMLElement;
|
|
|
|
|
clickElement(helpBtn);
|
|
|
|
|
expect(container.querySelector('[role="dialog"]')).toBeTruthy();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("help popover has aria-label='Shortcuts and tips'", () => {
|
|
|
|
|
storeState.nodes = [makeNode(0)];
|
|
|
|
|
const { container } = render(<Toolbar />);
|
|
|
|
|
const helpBtn = container.querySelector('[aria-label="Open shortcuts and tips"]') as HTMLElement;
|
|
|
|
|
clickElement(helpBtn);
|
|
|
|
|
const dialog = container.querySelector('[role="dialog"]') as HTMLElement;
|
|
|
|
|
expect(dialog.getAttribute("aria-label")).toBe("Shortcuts and tips");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("help popover has aria-modal=false", () => {
|
|
|
|
|
storeState.nodes = [makeNode(0)];
|
|
|
|
|
const { container } = render(<Toolbar />);
|
|
|
|
|
const helpBtn = container.querySelector('[aria-label="Open shortcuts and tips"]') as HTMLElement;
|
|
|
|
|
clickElement(helpBtn);
|
|
|
|
|
const dialog = container.querySelector('[role="dialog"]') as HTMLElement;
|
|
|
|
|
expect(dialog.getAttribute("aria-modal")).toBe("false");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("close button inside popover has aria-label='Close help dialog'", () => {
|
|
|
|
|
storeState.nodes = [makeNode(0)];
|
|
|
|
|
const { container } = render(<Toolbar />);
|
|
|
|
|
const helpBtn = container.querySelector('[aria-label="Open shortcuts and tips"]') as HTMLElement;
|
|
|
|
|
clickElement(helpBtn);
|
|
|
|
|
const closeBtn = container.querySelector('[aria-label="Close help dialog"]') as HTMLElement;
|
|
|
|
|
expect(closeBtn).toBeTruthy();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// ─── aria-hidden on decorative elements ───────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
describe("Toolbar a11y — aria-hidden on decorative elements", () => {
|
|
|
|
|
it("logo img has alt text (accessibility benefit — not aria-hidden by default)", () => {
|
|
|
|
|
storeState.nodes = [makeNode(0)];
|
|
|
|
|
const { container } = render(<Toolbar />);
|
|
|
|
|
const withinThis = within(container);
|
|
|
|
|
const img = withinThis.getByRole("img", { name: /molecule ai/i });
|
|
|
|
|
expect(img).toBeTruthy();
|
|
|
|
|
expect(img.getAttribute("alt")).toBe("Molecule AI");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("status dot in StatusPill is aria-hidden", () => {
|
|
|
|
|
storeState.nodes = [makeNode(0, "online")];
|
|
|
|
|
const { container } = render(<Toolbar />);
|
|
|
|
|
const withinThis = within(container);
|
|
|
|
|
const pills = withinThis.getAllByLabelText(/1 online/i);
|
|
|
|
|
const container$ = pills[0];
|
|
|
|
|
const dots = container$.querySelectorAll('[aria-hidden="true"]');
|
|
|
|
|
expect(dots.length).toBeGreaterThan(0);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("status count text in StatusPill is aria-hidden (decorative)", () => {
|
|
|
|
|
storeState.nodes = [makeNode(0, "online")];
|
|
|
|
|
const { container } = render(<Toolbar />);
|
|
|
|
|
const withinThis = within(container);
|
|
|
|
|
const pills = withinThis.getAllByLabelText(/1 online/i);
|
|
|
|
|
const container$ = pills[0];
|
|
|
|
|
const hiddenSpans = container$.querySelectorAll('[aria-hidden="true"]');
|
|
|
|
|
expect(hiddenSpans.length).toBeGreaterThan(0);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// ─── focus-visible:ring on interactive buttons ────────────────────────────────
|
|
|
|
|
|
|
|
|
|
describe("Toolbar a11y — focus-visible:ring on interactive buttons", () => {
|
|
|
|
|
it("A2A toggle button has focus-visible:ring class", () => {
|
|
|
|
|
storeState.nodes = [makeNode(0)];
|
|
|
|
|
const { container } = render(<Toolbar />);
|
|
|
|
|
const withinThis = within(container);
|
|
|
|
|
const btn = withinThis.getByRole("button", { name: /show a2a edges/i });
|
|
|
|
|
expect(btn.className).toMatch(/focus-visible:ring/);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("Audit trail button has focus-visible:ring class", () => {
|
|
|
|
|
storeState.nodes = [makeNode(0)];
|
|
|
|
|
const { container } = render(<Toolbar />);
|
|
|
|
|
const withinThis = within(container);
|
|
|
|
|
const btn = withinThis.getByRole("button", { name: /open audit trail/i });
|
|
|
|
|
expect(btn.className).toMatch(/focus-visible:ring/);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("Search button has focus-visible:ring class", () => {
|
|
|
|
|
storeState.nodes = [makeNode(0)];
|
|
|
|
|
const { container } = render(<Toolbar />);
|
|
|
|
|
const withinThis = within(container);
|
|
|
|
|
const btn = withinThis.getByRole("button", { name: /search workspaces/i });
|
|
|
|
|
expect(btn.className).toMatch(/focus-visible:ring/);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("Help button has focus-visible:ring class", () => {
|
|
|
|
|
storeState.nodes = [makeNode(0)];
|
|
|
|
|
const { container } = render(<Toolbar />);
|
|
|
|
|
const withinThis = within(container);
|
|
|
|
|
const btn = withinThis.getByRole("button", { name: /open shortcuts and tips/i });
|
|
|
|
|
expect(btn.className).toMatch(/focus-visible:ring/);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// ─── Stop All / Restart Pending aria-label ────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
describe("Toolbar a11y — Stop All / Restart Pending aria-label", () => {
|
|
|
|
|
it("Stop All button has descriptive aria-label including task count", () => {
|
|
|
|
|
// counts.activeTasks = number of nodes with activeTasks > 0; one node with
|
|
|
|
|
// 3 tasks = 1 node with active tasks = "(1 active)".
|
|
|
|
|
storeState.nodes = [makeNode(0, "online", 3)];
|
|
|
|
|
const { container } = render(<Toolbar />);
|
|
|
|
|
const withinThis = within(container);
|
|
|
|
|
const btn = withinThis.getByRole("button", { name: /stop all running tasks/i });
|
|
|
|
|
expect(btn).toBeTruthy();
|
|
|
|
|
expect(btn.getAttribute("aria-label")).toMatch(/\(1 active\)/);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("Restart Pending button has descriptive aria-label including workspace count", () => {
|
|
|
|
|
storeState.nodes = [makeNode(0, "online", 0, true)];
|
|
|
|
|
const { container } = render(<Toolbar />);
|
|
|
|
|
const withinThis = within(container);
|
|
|
|
|
const btn = withinThis.getByRole("button", { name: /restart/i });
|
|
|
|
|
expect(btn).toBeTruthy();
|
|
|
|
|
expect(btn.getAttribute("aria-label")).toMatch(/restart 1 workspace/i);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("Restart Pending aria-label uses singular when one workspace needs restart", () => {
|
|
|
|
|
storeState.nodes = [makeNode(0, "online", 0, true)];
|
|
|
|
|
const { container } = render(<Toolbar />);
|
|
|
|
|
const withinThis = within(container);
|
|
|
|
|
const btn = withinThis.getByRole("button", { name: /restart/i });
|
|
|
|
|
expect(btn).toBeTruthy();
|
|
|
|
|
// aria-label must contain "1 workspace" not "1 workspaces"
|
|
|
|
|
expect(btn.getAttribute("aria-label")).toMatch(/1 workspace/);
|
|
|
|
|
expect(btn.getAttribute("aria-label")).not.toMatch(/1 workspaces/);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// ─── Screen reader text exposure ────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
describe("Toolbar a11y — screen reader text exposure", () => {
|
|
|
|
|
it("workspace count is rendered as visible text (not aria-hidden)", () => {
|
|
|
|
|
storeState.nodes = [makeNode(0)];
|
|
|
|
|
const { container } = render(<Toolbar />);
|
|
|
|
|
const withinThis = within(container);
|
|
|
|
|
const text = withinThis.getByText(/1 workspaces?/);
|
|
|
|
|
expect(text).toBeTruthy();
|
|
|
|
|
// The text is NOT aria-hidden — verify by checking its parent chain.
|
|
|
|
|
const el = text instanceof HTMLElement ? text : (text as Element);
|
|
|
|
|
expect(el.closest('[aria-hidden="true"]')).toBeNull();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("wsStatus 'connected' renders 'Live' as accessible text", () => {
|
|
|
|
|
storeState.nodes = [makeNode(0)];
|
|
|
|
|
storeState.wsStatus = "connected";
|
|
|
|
|
const { container } = render(<Toolbar />);
|
|
|
|
|
const withinThis = within(container);
|
|
|
|
|
expect(withinThis.getByText("Live")).toBeTruthy();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("wsStatus 'connecting' renders 'Reconnecting' as accessible text", () => {
|
|
|
|
|
storeState.nodes = [makeNode(0)];
|
|
|
|
|
storeState.wsStatus = "connecting";
|
|
|
|
|
const { container } = render(<Toolbar />);
|
|
|
|
|
const withinThis = within(container);
|
|
|
|
|
expect(withinThis.getByText("Reconnecting")).toBeTruthy();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("wsStatus 'disconnected' renders 'Offline' as accessible text", () => {
|
|
|
|
|
storeState.nodes = [makeNode(0)];
|
|
|
|
|
storeState.wsStatus = "disconnected";
|
|
|
|
|
const { container } = render(<Toolbar />);
|
|
|
|
|
const withinThis = within(container);
|
|
|
|
|
expect(withinThis.getByText("Offline")).toBeTruthy();
|
|
|
|
|
});
|
|
|
|
|
});
|