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.
68 lines
3.2 KiB
TypeScript
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');
|
|
});
|
|
});
|