/** * Accessibility Tests * Tests for screen reader support, high contrast mode, and focus indicators * Requirements: 8.1, 8.2, 8.3 */ const React = require("react"); const { render } = require("ink-testing-library"); const { AccessibilityConfig, ScreenReaderUtils, getAccessibleColors, FocusManager, KeyboardNavigation, AccessibilityAnnouncer, } = require("../../src/tui/utils/accessibility.js"); // Mock environment variables for testing const mockEnv = (envVars) => { const originalEnv = { ...process.env }; Object.assign(process.env, envVars); return () => { process.env = originalEnv; }; }; describe("Accessibility Configuration", () => { describe("Screen Reader Detection", () => { test("should detect screen reader when NVDA_ACTIVE is true", () => { const restore = mockEnv({ NVDA_ACTIVE: "true" }); expect(AccessibilityConfig.isScreenReaderActive()).toBe(true); restore(); }); test("should detect screen reader when JAWS_ACTIVE is true", () => { const restore = mockEnv({ JAWS_ACTIVE: "true" }); expect(AccessibilityConfig.isScreenReaderActive()).toBe(true); restore(); }); test("should detect screen reader when SCREEN_READER is true", () => { const restore = mockEnv({ SCREEN_READER: "true" }); expect(AccessibilityConfig.isScreenReaderActive()).toBe(true); restore(); }); test("should detect screen reader when ACCESSIBILITY_MODE is true", () => { const restore = mockEnv({ ACCESSIBILITY_MODE: "true" }); expect(AccessibilityConfig.isScreenReaderActive()).toBe(true); restore(); }); test("should not detect screen reader when no variables are set", () => { const restore = mockEnv({ NVDA_ACTIVE: undefined, JAWS_ACTIVE: undefined, SCREEN_READER: undefined, ACCESSIBILITY_MODE: undefined, }); expect(AccessibilityConfig.isScreenReaderActive()).toBe(false); restore(); }); }); describe("High Contrast Mode Detection", () => { test("should detect high contrast when HIGH_CONTRAST_MODE is true", () => { const restore = mockEnv({ HIGH_CONTRAST_MODE: "true" }); expect(AccessibilityConfig.isHighContrastMode()).toBe(true); restore(); }); test("should detect high contrast when FORCE_HIGH_CONTRAST is true", () => { const restore = mockEnv({ FORCE_HIGH_CONTRAST: "true" }); expect(AccessibilityConfig.isHighContrastMode()).toBe(true); restore(); }); test("should not detect high contrast when no variables are set", () => { const restore = mockEnv({ HIGH_CONTRAST_MODE: undefined, FORCE_HIGH_CONTRAST: undefined, }); expect(AccessibilityConfig.isHighContrastMode()).toBe(false); restore(); }); }); describe("Enhanced Focus Detection", () => { test("should show enhanced focus when screen reader is active", () => { const restore = mockEnv({ SCREEN_READER: "true" }); expect(AccessibilityConfig.shouldShowEnhancedFocus()).toBe(true); restore(); }); test("should show enhanced focus when ENHANCED_FOCUS is true", () => { const restore = mockEnv({ ENHANCED_FOCUS: "true" }); expect(AccessibilityConfig.shouldShowEnhancedFocus()).toBe(true); restore(); }); test("should not show enhanced focus when no conditions are met", () => { const restore = mockEnv({ SCREEN_READER: undefined, ENHANCED_FOCUS: undefined, NVDA_ACTIVE: undefined, JAWS_ACTIVE: undefined, ACCESSIBILITY_MODE: undefined, }); expect(AccessibilityConfig.shouldShowEnhancedFocus()).toBe(false); restore(); }); }); describe("Reduced Motion Detection", () => { test("should detect reduced motion preference", () => { const restore = mockEnv({ PREFERS_REDUCED_MOTION: "true" }); expect(AccessibilityConfig.prefersReducedMotion()).toBe(true); restore(); }); test("should not detect reduced motion when not set", () => { const restore = mockEnv({ PREFERS_REDUCED_MOTION: undefined }); expect(AccessibilityConfig.prefersReducedMotion()).toBe(false); restore(); }); }); }); describe("Screen Reader Utils", () => { describe("Menu Item Description", () => { test("should generate correct description for simple menu item", () => { const item = { label: "Configuration", description: "Edit settings" }; const description = ScreenReaderUtils.describeMenuItem(item, 0, 3, true); expect(description).toBe( "Configuration, Edit settings, Item 1 of 3, selected" ); }); test("should generate correct description without description field", () => { const item = { label: "Main Menu" }; const description = ScreenReaderUtils.describeMenuItem(item, 1, 3, false); expect(description).toBe("Main Menu, Item 2 of 3, not selected"); }); test("should handle different index positions", () => { const item = { label: "Logs" }; const description = ScreenReaderUtils.describeMenuItem(item, 2, 3, false); expect(description).toBe("Logs, Item 3 of 3, not selected"); }); }); describe("Progress Description", () => { test("should generate correct progress description", () => { const description = ScreenReaderUtils.describeProgress( 25, 100, "Processing products" ); expect(description).toBe( "Processing products: 25 of 100 complete, 25 percent" ); }); test("should handle zero progress", () => { const description = ScreenReaderUtils.describeProgress( 0, 50, "Starting operation" ); expect(description).toBe( "Starting operation: 0 of 50 complete, 0 percent" ); }); test("should handle complete progress", () => { const description = ScreenReaderUtils.describeProgress( 100, 100, "Operation complete" ); expect(description).toBe( "Operation complete: 100 of 100 complete, 100 percent" ); }); }); describe("Status Description", () => { test("should generate status description with details", () => { const description = ScreenReaderUtils.describeStatus( "connected", "API rate limit: 40/40" ); expect(description).toBe("Connected to Shopify, API rate limit: 40/40"); }); test("should generate status description without details", () => { const description = ScreenReaderUtils.describeStatus("error"); expect(description).toBe("Error occurred"); }); test("should handle unknown status", () => { const description = ScreenReaderUtils.describeStatus("custom_status"); expect(description).toBe("custom_status"); }); }); describe("Form Field Description", () => { test("should describe valid form field with value", () => { const description = ScreenReaderUtils.describeFormField( "Shop Domain", "mystore.myshopify.com", true, null ); expect(description).toBe( "Shop Domain, current value: mystore.myshopify.com, valid" ); }); test("should describe invalid form field with error", () => { const description = ScreenReaderUtils.describeFormField( "Access Token", "invalid_token", false, "Token format is incorrect" ); expect(description).toBe( "Access Token, current value: invalid_token, invalid, Token format is incorrect" ); }); test("should describe empty form field", () => { const description = ScreenReaderUtils.describeFormField( "Target Tag", "", true, null ); expect(description).toBe("Target Tag, no value entered, valid"); }); }); }); describe("Accessible Colors", () => { describe("Normal Mode Colors", () => { test("should return standard colors when high contrast is disabled", () => { const restore = mockEnv({ HIGH_CONTRAST_MODE: undefined }); const colors = getAccessibleColors(); expect(colors.accent).toBe("blue"); expect(colors.success).toBe("green"); expect(colors.error).toBe("red"); expect(colors.warning).toBe("yellow"); expect(colors.focus).toBe("blue"); expect(colors.selection).toBe("blue"); restore(); }); }); describe("High Contrast Mode Colors", () => { test("should return high contrast colors when enabled", () => { const restore = mockEnv({ HIGH_CONTRAST_MODE: "true" }); const colors = getAccessibleColors(); expect(colors.background).toBe("black"); expect(colors.foreground).toBe("white"); expect(colors.accent).toBe("yellow"); expect(colors.focus).toBe("yellow"); expect(colors.selection).toBe("white"); restore(); }); test("should use alternative high contrast scheme when specified", () => { const restore = mockEnv({ HIGH_CONTRAST_MODE: "true", HIGH_CONTRAST_SCHEME: "alternative", }); const colors = getAccessibleColors(); expect(colors.background).toBe("white"); expect(colors.foreground).toBe("black"); expect(colors.accent).toBe("blue"); expect(colors.focus).toBe("blue"); expect(colors.selection).toBe("black"); restore(); }); }); }); describe("Focus Manager", () => { describe("Focus Props Generation", () => { test("should generate standard focus props when not focused", () => { const props = FocusManager.getFocusProps(false); expect(props).toEqual({ borderStyle: "single", borderColor: "gray", }); }); test("should generate enhanced focus props when accessibility is enabled", () => { const restore = mockEnv({ ENHANCED_FOCUS: "true" }); const props = FocusManager.getFocusProps(true, "input"); expect(props.borderStyle).toBe("double"); expect(props.borderColor).toBeDefined(); // backgroundColor may be undefined for non-input components in normal mode expect(props).toHaveProperty("backgroundColor"); restore(); }); test("should generate standard focus props when accessibility is disabled", () => { const restore = mockEnv({ ENHANCED_FOCUS: undefined }); const props = FocusManager.getFocusProps(true); expect(props.borderStyle).toBe("single"); expect(props.borderColor).toBeDefined(); restore(); }); }); describe("Selection Props Generation", () => { test("should generate selection props when selected", () => { const props = FocusManager.getSelectionProps(true); expect(props.bold).toBe(true); expect(props.color).toBeDefined(); }); test("should generate normal props when not selected", () => { const props = FocusManager.getSelectionProps(false); // color may be undefined in normal mode (uses terminal default) expect(props).toHaveProperty("color"); expect(props.bold).toBeUndefined(); }); test("should include background color in high contrast mode", () => { const restore = mockEnv({ HIGH_CONTRAST_MODE: "true" }); const props = FocusManager.getSelectionProps(true); expect(props.backgroundColor).toBeDefined(); expect(props.bold).toBe(true); restore(); }); }); }); describe("Keyboard Navigation", () => { describe("Navigation Key Detection", () => { test("should detect up arrow key", () => { expect(KeyboardNavigation.isNavigationKey({ name: "up" }, "up")).toBe( true ); }); test("should detect vim-style up key", () => { expect(KeyboardNavigation.isNavigationKey({ name: "k" }, "up")).toBe( true ); }); test("should detect enter key for selection", () => { expect( KeyboardNavigation.isNavigationKey({ name: "return" }, "select") ).toBe(true); }); test("should detect space key for selection", () => { expect( KeyboardNavigation.isNavigationKey({ name: "space" }, "select") ).toBe(true); }); test("should detect ctrl+c for quit", () => { expect( KeyboardNavigation.isNavigationKey({ ctrl: true, name: "c" }, "quit") ).toBe(true); }); test("should not detect incorrect key combinations", () => { expect(KeyboardNavigation.isNavigationKey({ name: "a" }, "up")).toBe( false ); }); }); describe("Shortcut Descriptions", () => { test("should generate description for single action", () => { const description = KeyboardNavigation.describeShortcuts(["up"]); expect(description).toBe("Up arrow or K to move up"); }); test("should generate description for multiple actions", () => { const description = KeyboardNavigation.describeShortcuts([ "up", "down", "select", ]); expect(description).toContain("Up arrow or K to move up"); expect(description).toContain("Down arrow or J to move down"); expect(description).toContain("Enter or Space to select"); }); test("should handle empty actions array", () => { const description = KeyboardNavigation.describeShortcuts([]); expect(description).toBe(""); }); test("should handle unknown actions", () => { const description = KeyboardNavigation.describeShortcuts([ "unknown_action", ]); expect(description).toBe(""); }); }); }); describe("Accessibility Announcer", () => { let originalConsoleLog; beforeEach(() => { originalConsoleLog = console.log; console.log = jest.fn(); AccessibilityAnnouncer.announcements = []; }); afterEach(() => { console.log = originalConsoleLog; }); describe("Announcement Queue", () => { test("should add announcement to queue when screen reader is active", () => { const restore = mockEnv({ SCREEN_READER: "true", NODE_ENV: "development", }); AccessibilityAnnouncer.announce("Test message", "polite"); expect(AccessibilityAnnouncer.announcements).toHaveLength(1); expect(AccessibilityAnnouncer.announcements[0].message).toBe( "Test message" ); expect(AccessibilityAnnouncer.announcements[0].priority).toBe("polite"); restore(); }); test("should not add announcement when screen reader is inactive", () => { const restore = mockEnv({ SCREEN_READER: undefined }); AccessibilityAnnouncer.announce("Test message"); expect(AccessibilityAnnouncer.announcements).toHaveLength(0); restore(); }); test("should log announcement in development mode", () => { const restore = mockEnv({ SCREEN_READER: "true", NODE_ENV: "development", }); AccessibilityAnnouncer.announce("Test message", "assertive"); expect(console.log).toHaveBeenCalledWith( "[SCREEN_READER_ASSERTIVE]: Test message" ); restore(); }); }); describe("Announcement Cleanup", () => { test("should clear old announcements", () => { const restore = mockEnv({ SCREEN_READER: "true" }); // Add old announcement AccessibilityAnnouncer.announcements.push({ message: "Old message", priority: "polite", timestamp: Date.now() - 10000, // 10 seconds ago }); // Add recent announcement AccessibilityAnnouncer.announcements.push({ message: "Recent message", priority: "polite", timestamp: Date.now(), }); AccessibilityAnnouncer.clearOldAnnouncements(5000); // 5 second max age expect(AccessibilityAnnouncer.announcements).toHaveLength(1); expect(AccessibilityAnnouncer.announcements[0].message).toBe( "Recent message" ); restore(); }); }); });