${displayTitle
- ? html`
${displayTitle}`
+ ? html`
${displayTitle}`
: nothing}
${secondaryParts
- ? html`
${secondaryParts}`
: nothing}
diff --git a/frontend/src/components/track-list/track-list.ts b/frontend/src/components/track-list/track-list.ts
index 11aa5d6..424ab62 100644
--- a/frontend/src/components/track-list/track-list.ts
+++ b/frontend/src/components/track-list/track-list.ts
@@ -1839,8 +1839,12 @@ export class TrackList
display = albumLink(track.Album, track.ReleaseGroupMBID, display as any, track.ArtistName);
}
+ // `title` on the cell rather than on whatever is inside
+ // it: the value may be a link, a highlighted match or
+ // plain text, and a tooltip is inherited by descendants
+ // either way (a11y.24).
return html`
-
{
expect(list!.getAttribute('aria-multiselectable')).toBe('true');
});
});
+
+describe('a clipped value is readable somewhere', () => {
+ beforeEach(async () => {
+ resetHarness();
+ searchStore.setTerm('');
+ stub('library.Library.GetAllTracks', TRACKS);
+ stub('library.Library.GetAllAlbums', []);
+ emit(Events.LibraryScanComplete);
+ });
+
+ it('gives every track-list cell the value it may be clipping', async () => {
+ const el = await fixture('track-list');
+
+ sized(el);
+ await settle(el);
+
+ const titles = shadowAll(el, '.track-row [role="gridcell"].cell').map((c) =>
+ c.getAttribute('title'),
+ );
+
+ // `a11y.24`: `text-overflow: ellipsis` in 40+ places, and the
+ // highest-density lists were the ones without a `title`. The
+ // attribute is on the cell rather than on what is inside it,
+ // because the value may be a link or a highlighted match and a
+ // tooltip is inherited by descendants either way.
+ expect(titles.length).toBeGreaterThan(0);
+ expect(titles).toContain('Departure');
+ expect(titles.every((t) => t !== null && t !== '')).toBe(true);
+ });
+
+ it('gives track-info its own title and secondary line', async () => {
+ const el = await fixture('track-info', {
+ trackTitle: 'An Exhaustively Overlong Track Name',
+ artist: 'Aurora Fields',
+ });
+
+ await el.updateComplete;
+
+ expect(shadow(el, '.title')?.getAttribute('title'))
+ .toBe('An Exhaustively Overlong Track Name');
+ expect(shadow(el, '.secondary')?.getAttribute('title')).toBeTruthy();
+ });
+});
diff --git a/frontend/test/components/queue-reorder.test.ts b/frontend/test/components/queue-reorder.test.ts
index e60d5f2..75a8542 100644
--- a/frontend/test/components/queue-reorder.test.ts
+++ b/frontend/test/components/queue-reorder.test.ts
@@ -19,7 +19,7 @@ import { describe, expect, it, beforeEach } from 'vitest';
import '@components/queue-panel/queue-panel';
import type { QueuePanel } from '@components/queue-panel/queue-panel';
import { Events } from '../../src/events';
-import { emit, calls, flush, lastArgs } from '@test/support/harness';
+import { emit, calls, flush, lastArgs, resetHarness } from '@test/support/harness';
import { fixture, shadow, shadowAll } from '@test/support/render';
import type { QueueTrack } from '@store/queue-store';
@@ -170,3 +170,36 @@ describe(' keyboard reorder', () => {
expect(calls().some((c) => c.path.includes('Move'))).toBe(false);
});
});
+
+describe('a queue row says which track its controls act on', () => {
+ beforeEach(() => {
+ resetHarness();
+ });
+
+ it('names each remove button after its own track', async () => {
+ const el = await panelWithQueue();
+
+ const labels = shadowAll(el, '.remove-button').map((b) =>
+ b.getAttribute('aria-label'),
+ );
+
+ // `a11y.32`: `title="Remove from queue"` on every row is a name
+ // that never identifies which track — four identical buttons in a
+ // list whose whole purpose is the order.
+ expect(labels.slice(0, 4)).toEqual([
+ 'Remove First from queue',
+ 'Remove Second from queue',
+ 'Remove Third from queue',
+ 'Remove Fourth from queue',
+ ]);
+ });
+
+ it('gives the title and artist a tooltip, since the panel is resizable', async () => {
+ const el = await panelWithQueue();
+
+ // `a11y.24` calls this one acute: MIN_WIDTH is narrow enough that
+ // both lines clip routinely, and nothing else can show the value.
+ expect(shadow(el, '.track-title')?.getAttribute('title')).toBe('First');
+ expect(shadow(el, '.track-artist')?.getAttribute('title')).toBe('Artist');
+ });
+});