Files
Twitch-VOD-Manager/src/renderer-accessibility.test.ts
T
Sucukdeluxe 9c17269a54 feat(accessibility): harden chat and keyboard interactions
Stream capability-authorized chat reads with cancellation, virtualize viewer rendering, centralize modal focus management, and add keyboard-accessible queue and VOD menus. Expose command palette combobox state and keep the document language synchronized with the selected locale.
2026-08-12 01:42:42 +02:00

68 lines
3.2 KiB
TypeScript

import { readFileSync } from 'node:fs';
import { join } from 'node:path';
import { runInNewContext } from 'node:vm';
import { transpileModule, ModuleKind, ScriptTarget } from 'typescript';
import { describe, expect, it } from 'vitest';
interface RendererAccessibilityApi {
RenderGeneration: new () => { next(): number; isCurrent(generation: number): boolean; cancel(): void };
getVirtualRange(scrollTop: number, viewportHeight: number, itemCount: number, rowHeight: number, overscan: number): { start: number; end: number };
getNextFocusIndex(activeIndex: number, count: number, shiftKey: boolean): number;
getNextMenuIndex(activeIndex: number, count: number, key: string): number | null;
setDocumentLanguage(language: string): string;
}
function loadAccessibility(documentElement: { lang: string } = { lang: '' }): RendererAccessibilityApi {
const source = readFileSync(join(__dirname, 'renderer-accessibility.ts'), 'utf8');
const context = {
document: { documentElement, addEventListener: () => undefined },
requestAnimationFrame: (callback: () => void) => callback(),
HTMLElement: class {},
Element: class {},
Node: class {},
};
const output = transpileModule(source, { compilerOptions: { module: ModuleKind.None, target: ScriptTarget.ES2022 } }).outputText;
runInNewContext(output, context);
return (context as unknown as { RendererAccessibility: RendererAccessibilityApi }).RendererAccessibility;
}
describe('renderer accessibility helpers', () => {
it('replaces stale chat work after rapid filters and close cancellation', () => {
const { RenderGeneration } = loadAccessibility();
const generations = new RenderGeneration();
const initial = generations.next();
const rapidFilter = generations.next();
generations.cancel();
expect(generations.isCurrent(initial)).toBe(false);
expect(generations.isCurrent(rapidFilter)).toBe(false);
});
it('keeps a large chat render window bounded to the visible rows', () => {
const { getVirtualRange } = loadAccessibility();
expect(getVirtualRange(29_000, 580, 50_000, 29, 12)).toEqual({ start: 988, end: 1_032 });
});
it('wraps dialog tab order at the first and last focusable controls', () => {
const { getNextFocusIndex } = loadAccessibility();
expect(getNextFocusIndex(2, 3, false)).toBe(0);
expect(getNextFocusIndex(0, 3, true)).toBe(2);
});
it('moves keyboard menus with arrows, Home, End and Escape semantics', () => {
const { getNextMenuIndex } = loadAccessibility();
expect(getNextMenuIndex(1, 4, 'ArrowDown')).toBe(2);
expect(getNextMenuIndex(0, 4, 'ArrowUp')).toBe(3);
expect(getNextMenuIndex(2, 4, 'Home')).toBe(0);
expect(getNextMenuIndex(1, 4, 'End')).toBe(3);
expect(getNextMenuIndex(1, 4, 'Escape')).toBeNull();
});
it('updates the document language when the application language changes', () => {
const documentElement = { lang: 'en' };
const { setDocumentLanguage } = loadAccessibility(documentElement);
expect(setDocumentLanguage('de')).toBe('de');
expect(documentElement.lang).toBe('de');
});
});