/** * @vitest-environment jsdom */ import { describe, it, expect, vi, beforeEach } from "vitest"; import { render, screen, fireEvent } from "@testing-library/react"; import { useSession } from "next-auth/react"; import { useParams } from "next/navigation"; import UserManagementPage from "@/app/dashboard/users/page"; import SessionViewPage from "@/app/dashboard/sessions/[id]/page"; import ModernDonutChart from "@/components/charts/donut-chart"; // Mock dependencies vi.mock("next-auth/react"); vi.mock("next/navigation"); const mockUseSession = vi.mocked(useSession); const mockUseParams = vi.mocked(useParams); // Mock fetch global.fetch = vi.fn(); describe("Keyboard Navigation Tests", () => { describe("User Management Page Keyboard Navigation", () => { beforeEach(() => { mockUseSession.mockReturnValue({ data: { user: { role: "ADMIN" } }, status: "authenticated", }); (global.fetch as any).mockResolvedValue({ ok: true, json: () => Promise.resolve({ users: [ { id: "1", email: "admin@example.com", role: "ADMIN" }, { id: "2", email: "user@example.com", role: "USER" }, ], }), }); }); it("should support tab navigation through form elements", async () => { render(); await screen.findByText("User Management"); const emailInput = screen.getByLabelText("Email"); const roleSelect = screen.getByRole("combobox"); const submitButton = screen.getByRole("button", { name: /invite user/i }); // Test tab order emailInput.focus(); expect(document.activeElement).toBe(emailInput); fireEvent.keyDown(emailInput, { key: "Tab" }); // Role select should be focused (though actual focus behavior depends on Select component) // Tab to submit button fireEvent.keyDown(roleSelect, { key: "Tab" }); expect(document.activeElement).toBe(submitButton); }); it("should support Enter key for form submission", async () => { render(); await screen.findByText("User Management"); const emailInput = screen.getByLabelText("Email"); const submitButton = screen.getByRole("button", { name: /invite user/i }); // Fill out form fireEvent.change(emailInput, { target: { value: "test@example.com" } }); // Mock successful submission (global.fetch as any).mockResolvedValueOnce({ ok: true, json: () => Promise.resolve({ users: [ { id: "1", email: "admin@example.com", role: "ADMIN" }, { id: "2", email: "user@example.com", role: "USER" }, ], }), }).mockResolvedValueOnce({ ok: true, json: () => Promise.resolve({ message: "User invited successfully" }), }); // Submit with Enter key fireEvent.keyDown(submitButton, { key: "Enter" }); // Form should be submitted expect(global.fetch).toHaveBeenCalledWith( "/api/dashboard/users", expect.objectContaining({ method: "POST", }) ); }); it("should support Space key for button activation", async () => { render(); await screen.findByText("User Management"); const submitButton = screen.getByRole("button", { name: /invite user/i }); // Mock form data const emailInput = screen.getByLabelText("Email"); fireEvent.change(emailInput, { target: { value: "test@example.com" } }); // Mock successful submission (global.fetch as any).mockResolvedValueOnce({ ok: true, json: () => Promise.resolve({ users: [ { id: "1", email: "admin@example.com", role: "ADMIN" }, { id: "2", email: "user@example.com", role: "USER" }, ], }), }).mockResolvedValueOnce({ ok: true, json: () => Promise.resolve({ message: "User invited successfully" }), }); // Activate with Space key submitButton.focus(); fireEvent.keyDown(submitButton, { key: " " }); // Should trigger form submission expect(global.fetch).toHaveBeenCalledWith( "/api/dashboard/users", expect.objectContaining({ method: "POST", }) ); }); it("should have visible focus indicators", async () => { render(); await screen.findByText("User Management"); const emailInput = screen.getByLabelText("Email"); const submitButton = screen.getByRole("button", { name: /invite user/i }); // Focus elements and check for focus indicators emailInput.focus(); expect(emailInput).toHaveFocus(); expect(emailInput.className).toMatch(/focus/i); submitButton.focus(); expect(submitButton).toHaveFocus(); expect(submitButton.className).toMatch(/focus/i); }); it("should support Escape key for form reset", async () => { render(); await screen.findByText("User Management"); const emailInput = screen.getByLabelText("Email") as HTMLInputElement; // Enter some text fireEvent.change(emailInput, { target: { value: "test@example.com" } }); expect(emailInput.value).toBe("test@example.com"); // Press Escape fireEvent.keyDown(emailInput, { key: "Escape" }); // Field should not be cleared by Escape (browser default behavior) // But it should not cause any errors expect(emailInput.value).toBe("test@example.com"); }); it("should support arrow keys in select elements", async () => { render(); await screen.findByText("User Management"); const roleSelect = screen.getByRole("combobox"); // Focus the select roleSelect.focus(); expect(roleSelect).toHaveFocus(); // Arrow keys should work (implementation depends on Select component) fireEvent.keyDown(roleSelect, { key: "ArrowDown" }); fireEvent.keyDown(roleSelect, { key: "ArrowUp" }); // Should not throw errors expect(roleSelect).toBeInTheDocument(); }); }); describe("Session Details Page Keyboard Navigation", () => { beforeEach(() => { mockUseSession.mockReturnValue({ data: { user: { role: "ADMIN" } }, status: "authenticated", }); mockUseParams.mockReturnValue({ id: "test-session-id", }); (global.fetch as any).mockResolvedValue({ ok: true, json: () => Promise.resolve({ session: { id: "test-session-id", sessionId: "test-session-id", startTime: new Date().toISOString(), endTime: new Date().toISOString(), category: "SALARY_COMPENSATION", language: "en", country: "US", sentiment: "positive", messagesSent: 5, userId: "user-123", fullTranscriptUrl: "https://example.com/transcript", messages: [ { id: "msg-1", content: "Hello", role: "user", timestamp: new Date().toISOString(), }, ], }, }), }); }); it("should support keyboard navigation for back button", async () => { render(); await screen.findByText("Session Details"); const backButton = screen.getByRole("button", { name: /return to sessions list/i }); // Focus and activate with keyboard backButton.focus(); expect(backButton).toHaveFocus(); // Should have proper focus ring expect(backButton.className).toMatch(/focus/i); // Test Enter key activation fireEvent.keyDown(backButton, { key: "Enter" }); // Navigation behavior would be tested in integration tests }); it("should support keyboard navigation for external links", async () => { render(); await screen.findByText("Session Details"); const transcriptLink = screen.getByRole("link", { name: /open original transcript in new tab/i }); // Focus the link transcriptLink.focus(); expect(transcriptLink).toHaveFocus(); // Should have proper focus ring expect(transcriptLink.className).toMatch(/focus/i); // Test Enter key activation fireEvent.keyDown(transcriptLink, { key: "Enter" }); // Link behavior would open in new tab }); it("should support tab navigation through session details", async () => { render(); await screen.findByText("Session Details"); // Get all focusable elements const backButton = screen.getByRole("button", { name: /return to sessions list/i }); const transcriptLink = screen.getByRole("link", { name: /open original transcript in new tab/i }); // Test tab order backButton.focus(); expect(document.activeElement).toBe(backButton); // Tab to next focusable element fireEvent.keyDown(backButton, { key: "Tab" }); // Should move to next interactive element }); }); describe("Chart Component Keyboard Navigation", () => { const mockData = [ { name: "Category A", value: 30, color: "#8884d8" }, { name: "Category B", value: 20, color: "#82ca9d" }, { name: "Category C", value: 50, color: "#ffc658" }, ]; it("should support keyboard focus on chart elements", () => { render( ); const chart = screen.getByRole("img", { name: /test chart/i }); // Chart should be focusable chart.focus(); expect(chart).toHaveFocus(); // Should have proper focus styling expect(chart.className).toMatch(/focus/i); }); it("should handle keyboard interactions on chart", () => { render( ); const chart = screen.getByRole("img", { name: /test chart/i }); chart.focus(); // Test keyboard interactions fireEvent.keyDown(chart, { key: "Enter" }); fireEvent.keyDown(chart, { key: " " }); fireEvent.keyDown(chart, { key: "ArrowLeft" }); fireEvent.keyDown(chart, { key: "ArrowRight" }); // Should not throw errors expect(chart).toBeInTheDocument(); }); it("should provide keyboard alternative for chart interactions", () => { render( ); // Chart should have ARIA label for screen readers const chart = screen.getByRole("img"); expect(chart).toHaveAttribute("aria-label"); }); }); describe("Focus Management", () => { beforeEach(() => { mockUseSession.mockReturnValue({ data: { user: { role: "ADMIN" } }, status: "authenticated", }); (global.fetch as any).mockResolvedValue({ ok: true, json: () => Promise.resolve({ users: [] }), }); }); it("should maintain focus after dynamic content changes", async () => { render(); await screen.findByText("User Management"); const emailInput = screen.getByLabelText("Email"); const submitButton = screen.getByRole("button", { name: /invite user/i }); // Focus on input emailInput.focus(); expect(document.activeElement).toBe(emailInput); // Trigger form submission (which updates the UI) fireEvent.change(emailInput, { target: { value: "test@example.com" } }); // Mock successful response (global.fetch as any).mockResolvedValueOnce({ ok: true, json: () => Promise.resolve({ message: "User invited successfully" }), }).mockResolvedValueOnce({ ok: true, json: () => Promise.resolve({ users: [] }), }); fireEvent.click(submitButton); // Focus should be managed appropriately after submission // (exact behavior depends on implementation) }); it("should handle focus when elements are disabled", async () => { render(); await screen.findByText("User Management"); const submitButton = screen.getByRole("button", { name: /invite user/i }); // Button should be disabled when form is invalid expect(submitButton).toBeInTheDocument(); // Should handle focus on disabled elements gracefully submitButton.focus(); fireEvent.keyDown(submitButton, { key: "Enter" }); // Should not cause errors }); it("should skip over non-interactive elements", async () => { render(); await screen.findByText("User Management"); // Tab navigation should skip over static text and focus only on interactive elements const interactiveElements = [ screen.getByLabelText("Email"), screen.getByLabelText("Role"), screen.getByRole("button", { name: /invite user/i }), ]; interactiveElements.forEach((element) => { element.focus(); expect(document.activeElement).toBe(element); }); }); }); describe("Screen Reader Support", () => { beforeEach(() => { mockUseSession.mockReturnValue({ data: { user: { role: "ADMIN" } }, status: "authenticated", }); (global.fetch as any).mockResolvedValue({ ok: true, json: () => Promise.resolve({ users: [] }), }); }); it("should announce form validation errors", async () => { render(); await screen.findByText("User Management"); const emailInput = screen.getByLabelText("Email") as HTMLInputElement; const submitButton = screen.getByRole("button", { name: /invite user/i }); // Submit invalid form fireEvent.click(submitButton); // HTML5 validation should be triggered expect(emailInput.validity.valid).toBeFalsy(); }); it("should announce loading states", async () => { // Test loading state announcement mockUseSession.mockReturnValue({ data: null, status: "loading", }); render(); const loadingText = screen.getByText("Loading users..."); expect(loadingText).toBeInTheDocument(); }); it("should announce success and error messages", async () => { render(); await screen.findByText("User Management"); const emailInput = screen.getByLabelText("Email"); const submitButton = screen.getByRole("button", { name: /invite user/i }); // Fill form fireEvent.change(emailInput, { target: { value: "test@example.com" } }); // Mock error response (global.fetch as any).mockResolvedValueOnce({ ok: false, json: () => Promise.resolve({ message: "Email already exists" }), }); fireEvent.click(submitButton); // Error message should be announced await screen.findByText(/failed to invite user/i); }); }); describe("High Contrast Mode Support", () => { it("should maintain keyboard navigation in high contrast mode", async () => { // Mock high contrast media query Object.defineProperty(window, "matchMedia", { writable: true, value: vi.fn().mockImplementation(query => ({ matches: query === "(prefers-contrast: high)", media: query, onchange: null, addListener: vi.fn(), removeListener: vi.fn(), addEventListener: vi.fn(), removeEventListener: vi.fn(), dispatchEvent: vi.fn(), })), }); mockUseSession.mockReturnValue({ data: { user: { role: "ADMIN" } }, status: "authenticated", }); (global.fetch as any).mockResolvedValue({ ok: true, json: () => Promise.resolve({ users: [] }), }); render(); await screen.findByText("User Management"); const emailInput = screen.getByLabelText("Email"); // Focus should still work in high contrast mode emailInput.focus(); expect(emailInput).toHaveFocus(); }); }); });