Just a whole lot of crap
This commit is contained in:
507
tests/tui/accessibility.test.js
Normal file
507
tests/tui/accessibility.test.js
Normal file
@@ -0,0 +1,507 @@
|
||||
/**
|
||||
* 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user