// @vitest-environment jsdom import React, { act } from "react"; import { createRoot, type Root } from "react-dom/client"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; const copyToClipboard = vi.fn(); vi.mock("next-intl", () => ({ useLocale: () => "en", useTranslations: (namespace: string) => (key: string) => `${namespace}.${key}`, })); vi.mock("@/shared/utils/clipboard", () => ({ copyToClipboard })); const roots: Root[] = []; async function renderViewer() { const container = document.createElement("div"); document.body.appendChild(container); const root = createRoot(container); roots.push(root); const { default: ConsoleLogViewer } = await import("../../../src/shared/components/ConsoleLogViewer"); await act(async () => { root.render(); await Promise.resolve(); }); await act(async () => { vi.advanceTimersByTime(0); await Promise.resolve(); }); return container; } describe("ConsoleLogViewer accessibility", () => { beforeEach(() => { ( globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean } ).IS_REACT_ACT_ENVIRONMENT = true; copyToClipboard.mockResolvedValue(true); vi.useFakeTimers(); globalThis.fetch = vi.fn().mockResolvedValue({ ok: true, json: async () => [ { timestamp: "2026-08-26T00:00:00.000Z", level: "info", message: "ready", }, { timestamp: "2026-08-26T00:00:01.000Z", level: "warn", message: "waiting", }, ], }); }); afterEach(() => { for (const root of roots.splice(0)) { act(() => root.unmount()); } document.body.innerHTML = ""; vi.useRealTimers(); vi.restoreAllMocks(); }); it("names icon-only controls and exposes keyboard-visible copy feedback", async () => { const container = await renderViewer(); const refresh = container.querySelector( 'button[aria-label="common.refresh"]' ); const copy = container.querySelector( 'button[aria-label="logs.consoleViewer.copyLogEntry"]' ); expect(refresh).not.toBeNull(); expect(refresh?.querySelector(".material-symbols-outlined")?.getAttribute("aria-hidden")).toBe( "true" ); expect(copy).not.toBeNull(); expect(copy?.className).toContain("focus-visible:opacity-100"); expect(copy?.querySelector(".material-symbols-outlined")?.getAttribute("aria-hidden")).toBe( "true" ); await act(async () => { copy?.click(); await Promise.resolve(); }); const status = container.querySelector('[role="status"][aria-live="polite"]'); expect(status?.textContent).toBe("common.copied"); await act(async () => { vi.advanceTimersByTime(2000); }); expect(container.querySelector('[role="status"]')).toBeNull(); }); it("keeps the latest copy announcement for its full timeout", async () => { const container = await renderViewer(); const buttons = container.querySelectorAll( 'button[aria-label="logs.consoleViewer.copyLogEntry"]' ); expect(buttons).toHaveLength(2); await act(async () => { buttons[0].click(); await Promise.resolve(); vi.advanceTimersByTime(1000); buttons[1].click(); await Promise.resolve(); vi.advanceTimersByTime(1000); }); expect(container.querySelector('[role="status"]')?.textContent).toBe("common.copied"); await act(async () => { vi.advanceTimersByTime(1000); }); expect(container.querySelector('[role="status"]')).toBeNull(); }); it("clears pending copy feedback when unmounted", async () => { const container = await renderViewer(); const copy = container.querySelector( 'button[aria-label="logs.consoleViewer.copyLogEntry"]' ); await act(async () => { copy?.click(); await Promise.resolve(); }); const root = roots.pop(); act(() => root?.unmount()); expect(vi.getTimerCount()).toBe(0); }); });