Measured with Accessibility.getFullAXTree against the running app with all seven sections expanded: 24 of 93 controls computed an empty name. Every config-field select and toggle, and all eighteen track-list column checkboxes, had a <label> sitting right beside them with nothing associating the two. Now 0 of 93. Not in the audit, and a11y.6 says why in its own line: it scanned every <button>, and none of these is one. Same shape as the count that sent Phase 1 looking for an unnamed sort control — the claim was answering a narrower question than it reads as. The fields use `for`/`id` rather than aria-label, for what it buys beyond the name: the label text becomes a click target for the control. A fixed id is safe only because each config-field is its own shadow root. Two more are named but identify nothing, which is a11y.32's complaint one page over: three shortcut buttons announced themselves as "S", and thirty-six column arrows as "Move up".
117 lines
3.8 KiB
TypeScript
117 lines
3.8 KiB
TypeScript
/**
|
|
* Every form control in Settings had a visible label and no name.
|
|
*
|
|
* Not in the audit, and `a11y.6` says why in its own text: it scanned
|
|
* every `<button>`, and a `<select>` is not one. Measured with
|
|
* `Accessibility.getFullAXTree` against the running app with all seven
|
|
* sections expanded — **24 of 93 controls unnamed**: every
|
|
* `config-field` select and toggle, and all eighteen track-list column
|
|
* checkboxes. In each case a `<label>` sat right beside the control
|
|
* with nothing associating the two.
|
|
*
|
|
* Two of the fixes here are about a name that exists and does not
|
|
* identify anything, which is `a11y.32`'s complaint one page over:
|
|
* three shortcut buttons announced themselves as "S", and thirty-six
|
|
* column arrows as "Move up" or "Move down".
|
|
*/
|
|
import { describe, expect, it } from 'vitest';
|
|
|
|
import '@components/config-page/config-field';
|
|
import '@components/config-page/shortcut-capture';
|
|
import { fixture, shadow } from '@test/support/render';
|
|
|
|
/** What the `<label>` in this shadow root actually points at. */
|
|
function labelledControl(host: Element): Element | null {
|
|
const forId = shadow(host, 'label[for]')?.getAttribute('for');
|
|
|
|
return forId ? shadow(host, `#${CSS.escape(forId)}`) : null;
|
|
}
|
|
|
|
describe('a config field names its control', () => {
|
|
it('associates the label with a select', async () => {
|
|
const el = await fixture('config-field', {
|
|
schema: {
|
|
key: 'theme.shade',
|
|
label: 'Shade',
|
|
type: 'select',
|
|
options: [{ value: 'dark', label: 'Dark' }],
|
|
},
|
|
value: 'dark',
|
|
});
|
|
|
|
expect(labelledControl(el)?.tagName).toBe('SELECT');
|
|
});
|
|
|
|
it('associates the label with a toggle, which is a second label deep', async () => {
|
|
const el = await fixture('config-field', {
|
|
schema: { key: 'x', label: 'Scan on startup', type: 'toggle' },
|
|
value: true,
|
|
});
|
|
|
|
// The checkbox is wrapped in a *styling* label carrying only a
|
|
// span, which contributes no text and so named nothing.
|
|
const control = labelledControl(el) as HTMLInputElement | null;
|
|
|
|
expect(control?.type).toBe('checkbox');
|
|
});
|
|
|
|
it.each(['text', 'number', 'color', 'directory'] as const)(
|
|
'associates the label with a %s field',
|
|
async (type) => {
|
|
const el = await fixture('config-field', {
|
|
schema: { key: 'k', label: 'Music folder', type },
|
|
value: '',
|
|
});
|
|
|
|
expect(labelledControl(el)).toBeTruthy();
|
|
},
|
|
);
|
|
|
|
it('says which field a Browse button browses for', async () => {
|
|
const el = await fixture('config-field', {
|
|
schema: { key: 'k', label: 'Music folder', type: 'directory' },
|
|
value: '',
|
|
});
|
|
|
|
expect(shadow(el, 'button')?.getAttribute('aria-label'))
|
|
.toBe('Browse for Music folder');
|
|
});
|
|
});
|
|
|
|
describe('a shortcut button says what it binds', () => {
|
|
it('names the action, and keeps the key as the value', async () => {
|
|
const el = await fixture('shortcut-capture', {
|
|
action: 'player.playPause',
|
|
label: 'Play / Pause',
|
|
currentKey: 'Space',
|
|
});
|
|
|
|
expect(shadow(el, 'button')?.getAttribute('aria-label'))
|
|
.toBe('Play / Pause shortcut: Space');
|
|
expect(shadow(el, 'button')?.textContent?.trim()).toBe('Space');
|
|
});
|
|
|
|
it('says so when there is no key rather than announcing nothing', async () => {
|
|
const el = await fixture('shortcut-capture', {
|
|
action: 'tracklist.delete',
|
|
label: 'Delete Track',
|
|
currentKey: '',
|
|
});
|
|
|
|
expect(shadow(el, 'button')?.getAttribute('aria-label'))
|
|
.toBe('Delete Track shortcut: not set');
|
|
});
|
|
|
|
it('names the action in the reset button too', async () => {
|
|
const el = await fixture('shortcut-capture', {
|
|
action: 'player.next',
|
|
label: 'Next Track',
|
|
currentKey: 'X',
|
|
defaultKey: 'N',
|
|
});
|
|
|
|
expect(shadow(el, '.reset-btn')?.getAttribute('aria-label'))
|
|
.toBe('Reset Next Track to N');
|
|
});
|
|
});
|