Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
139 changes: 139 additions & 0 deletions src/components/accessibility.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import { describe, expect, it } from "vitest";
import { readFileSync } from "fs";
import { resolve } from "path";

/**
* Accessibility compliance tests
*
* Validates that interactive elements across the CODA UI include the required
* ARIA attributes and alt text for WCAG 2.1 AA compliance. These tests read
* the component source files and assert that the accessibility attributes are
* present, catching regressions without requiring a full browser/jsdom render.
*/

function readComponent(relativePath: string): string {
return readFileSync(resolve(__dirname, relativePath), "utf8");
}

// ---------------------------------------------------------------------------
// GPS Location Pane
// ---------------------------------------------------------------------------

describe("gps-location accessibility", () => {
const source = readComponent("panes/gps-location.tsx");

it("marker images include descriptive alt text", () => {
expect(source).toContain("alt={" + "`GPS marker icon for ${key}`}");
});

it("lock button has aria-label", () => {
expect(source).toContain('aria-label="Toggle map scroll lock"');
});
});

// ---------------------------------------------------------------------------
// Dockview Header Actions
// ---------------------------------------------------------------------------

describe("dockview-header-actions accessibility", () => {
const source = readComponent("framework/dockview/dockview-header-actions.tsx");

it("add panel button has aria-label", () => {
expect(source).toContain('aria-label="Add panel"');
});

it("collapsed controls button has aria-label", () => {
expect(source).toContain('aria-label="Toggle panel controls"');
});
});

// ---------------------------------------------------------------------------
// Dockview Watermark
// ---------------------------------------------------------------------------

describe("dockview watermark accessibility", () => {
const source = readComponent("framework/dockview/dockview.tsx");

it("watermark add panel button has aria-label", () => {
expect(source).toContain('aria-label="Add panel"');
});
});

// ---------------------------------------------------------------------------
// Header
// ---------------------------------------------------------------------------

describe("header accessibility", () => {
const source = readComponent("interface/header.tsx");

it("help menu button has role and aria-label", () => {
expect(source).toContain('role="button"');
expect(source).toContain('aria-label="Toggle help menu"');
});

it("cancel time button has aria-label", () => {
expect(source).toContain('aria-label="Cancel time edit"');
});

it("go time button has aria-label", () => {
expect(source).toContain('aria-label="Apply time change"');
});

it("CODA wordmark has role and aria-label", () => {
expect(source).toContain('aria-label="Go to CODA home"');
});
});

// ---------------------------------------------------------------------------
// Comm Pane Controls
// ---------------------------------------------------------------------------

describe("comm controls accessibility", () => {
const source = readComponent("panes/comm.tsx");

it("channel dropdown button has aria-label and aria-expanded", () => {
expect(source).toContain('aria-label="Toggle channel selection"');
expect(source).toContain("aria-expanded={dropdownOpen}");
});

it("filter button has aria-label", () => {
expect(source).toContain('aria-label="Toggle utterance filter"');
});

it("scroll lock button has aria-label", () => {
expect(source).toContain('aria-label="Toggle auto-scroll lock"');
});
});

// ---------------------------------------------------------------------------
// Video Controls
// ---------------------------------------------------------------------------

describe("video controls accessibility", () => {
const source = readComponent("panes/video/video-controls.tsx");

it("mute button has dynamic aria-label based on state", () => {
expect(source).toContain("Unmute audio");
expect(source).toContain("Mute audio");
});

it("IO info button has aria-label", () => {
expect(source).toContain('aria-label="Toggle IO information"');
});
});

// ---------------------------------------------------------------------------
// Help Button (shared component)
// ---------------------------------------------------------------------------

describe("pane help button accessibility", () => {
const source = readComponent("interface/pane-help-control-button.tsx");

it("has role='button' for the clickable div", () => {
expect(source).toContain('role="button"');
});

it("has aria-label", () => {
expect(source).toContain('aria-label="Toggle help overlay"');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ const CollapsedControls: FunctionComponent<CollapsedControlsProps> = ({
className={styles.collapseButton}
onClick={handleClick}
title="Panel controls"
aria-label="Toggle panel controls"
>
<FontAwesomeIcon icon={faSliders} />
</button>
Expand Down Expand Up @@ -179,7 +180,7 @@ export const DockviewLeftActions: FunctionComponent<IDockviewHeaderActionsProps>
}, [containerApi, dispatch, group]);

return (
<button className={styles.addButton} onClick={handleAddPanel} title="Add panel">
<button className={styles.addButton} onClick={handleAddPanel} title="Add panel" aria-label="Add panel">
<FontAwesomeIcon icon={faPlus} />
</button>
);
Expand Down
2 changes: 1 addition & 1 deletion src/components/framework/dockview/dockview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ const DockviewWatermark: FunctionComponent<IWatermarkPanelProps> = ({ containerA
return (
<div className={styles.watermark}>
<span>Use</span>
<button className={styles.watermarkButton} onClick={handleAddPanel} title="Add panel">
<button className={styles.watermarkButton} onClick={handleAddPanel} title="Add panel" aria-label="Add panel">
<FontAwesomeIcon icon={faPlus} />
</button>
<span>to add a new panel</span>
Expand Down
8 changes: 6 additions & 2 deletions src/components/interface/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ const LoaderHelpMenu: FunctionComponent<{
<>
<div
className={styles.helpMenuButton}
role="button"
aria-label="Toggle help menu"
onClick={() => {
setHelpLoaderOpen(!helpLoaderOpen);
}}
Expand Down Expand Up @@ -265,10 +267,10 @@ const Clock: FunctionComponent = () => {
className={styles.timeButtonsContainer}
style={{ top: buttonPos.top, left: buttonPos.left, width: buttonPos.width }}
>
<button className={styles.timeButtonsItems} onClick={handleCancel}>
<button className={styles.timeButtonsItems} onClick={handleCancel} aria-label="Cancel time edit">
<span>Cancel</span>
</button>
<button className={styles.timeButtonsItems} onClick={handleTimeChange}>
<button className={styles.timeButtonsItems} onClick={handleTimeChange} aria-label="Apply time change">
<span>Go</span>
</button>
</div>,
Expand Down Expand Up @@ -359,6 +361,8 @@ const Header: FunctionComponent<{
<div className={styles.verticalCenter}>
<span
className={styles.wordMark}
role="button"
aria-label="Go to CODA home"
onClick={() => {
window.location.assign(location.origin);
}}
Expand Down
2 changes: 2 additions & 0 deletions src/components/interface/pane-help-control-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ export const HelpButton: FunctionComponent<{ clickHandler: () => void; selected?
<div
className={`${styles.helpButton} ${selectedStyle}`}
title={`More info`}
role="button"
aria-label="Toggle help overlay"
onClick={() => {
if (clickHandler) {
clickHandler();
Expand Down
4 changes: 4 additions & 0 deletions src/components/panes/comm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,8 @@ export const CommControls: FunctionComponent<{
<button
className={styles.channelDropdownButton}
onClick={() => setDropdownOpen(!dropdownOpen)}
aria-label="Toggle channel selection"
aria-expanded={dropdownOpen}
>
<span>
{selectedCount === totalCount
Expand Down Expand Up @@ -247,6 +249,7 @@ export const CommControls: FunctionComponent<{
<button
className={`${styles.filterButton} ${buttonLength} ${filterButtonSelected}`}
title={`Filter utterances by words`}
aria-label="Toggle utterance filter"
onClick={() => {
dispatch(
setPaneStateDataValue({
Expand All @@ -273,6 +276,7 @@ export const CommControls: FunctionComponent<{
<button
className={`${styles.lockButton} ${buttonLength} ${lockButtonSelected}`}
title={`Scroll automatically to the last spoken utterance`}
aria-label="Toggle auto-scroll lock"
onClick={() => {
dispatch(
setPaneStateDataValue({
Expand Down
2 changes: 2 additions & 0 deletions src/components/panes/gps-location.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ export const GPSLocationControls: FunctionComponent<{
<button
className={`${styles.lockButton} ${buttonLength} ${lockButtonSelected}`}
title={`Click to toggle map scrolling in relation to GPS position`}
aria-label="Toggle map scroll lock"
onClick={() => {
dispatch(
setPaneStateDataValue({
Expand Down Expand Up @@ -628,6 +629,7 @@ const GPSLocation: FunctionComponent<{ paneInstanceId: number; groupDimensions:
<img
className="infoSectionTitleIcon"
src={`/images/marker_${key.toLowerCase()}.png`}
alt={`GPS marker icon for ${key}`}
width="30px"
/>
</div>
Expand Down
3 changes: 2 additions & 1 deletion src/components/panes/video/video-controls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export const IOInfoButton: FunctionComponent<{
<button
className={`${styles.ioButton} ${buttonLength} ${selectedStyle}`}
onClick={clickHandler}
aria-label="Toggle IO information"
>
<span className={styles.ioLabel}>
{isLargeFrame ? "IO " : ""}
Expand All @@ -53,7 +54,7 @@ export const MuteButton: FunctionComponent<{
clickHandler: () => void;
muted: boolean;
}> = ({ clickHandler, muted }) => (
<button className={styles.muteButton} onClick={clickHandler}>
<button className={styles.muteButton} onClick={clickHandler} aria-label={muted ? "Unmute audio" : "Mute audio"}>
<FontAwesomeIcon icon={muted ? faVolumeMute : faVolumeUp} />
</button>
);
Expand Down