feat(now-playing): plain text instead of links in the phone mini player
CI / check (push) Skipped
CI / e2e (push) Skipped
CI / check (push) Skipped
CI / e2e (push) Skipped
The bottom bar's title, artist and "Playing from X" all navigate. In a bar sized for a bar they are a few characters of text, which is not a touch target — and explore-link holds its navigation for one double-click interval and drops it if a second click arrives, a gesture that exists so double-clicking a row can play it and that means nothing on touch. Below the shell's phone breakpoint the three render as plain text. The words are unchanged: the source line still says where the queue came from, because dropping the link is the change and dropping the information would be a different and worse one. The cover art already carries the phone-only button that opens the full-screen Now Playing view, which is where the links live. This is in JS rather than in the stylesheet because what changes is the content, not its appearance — no CSS rule takes a click handler off an element. matchMedia is read in connectedCallback for the reason the reduce-motion query beside it already is, so a test can answer it first. Two smaller things. PHONE_QUERY moves out of track-list.ts into utils/breakpoints.ts: it was a private const when one component needed it, and a second reader is where a copy starts drifting from index.css. And `phone` joins geometryKey(), because crossing the breakpoint swaps a link for a bare string and the marquee travels a distance read from measuring it — the words being identical either side is not the same as the box measuring the same. Closes #61
This commit is contained in:
@@ -0,0 +1,166 @@
|
||||
/**
|
||||
* The mini player's links are a desktop affordance.
|
||||
*
|
||||
* `utils/explore-link.ts` makes every track and artist name navigate,
|
||||
* and `utils/queue-source-link.ts` makes "Playing from X" navigate — in
|
||||
* the bottom bar those are a few characters of text at a font size
|
||||
* chosen for a bar, which is not a touch target. Worse, explore-link
|
||||
* holds the navigation for one double-click interval and drops it if a
|
||||
* second click arrives: a gesture that exists so double-clicking a row
|
||||
* can play it, and which means nothing at all on touch.
|
||||
*
|
||||
* So below the shell's phone breakpoint the three render as plain text
|
||||
* and the whole bar's cover art opens the full-screen Now Playing view,
|
||||
* which is where the links live.
|
||||
*
|
||||
* The breakpoint is stubbed rather than emulated for the reason
|
||||
* track-list-phone.test.ts states: this tier's viewport is fixed at
|
||||
* 1280x800 by the runner, and the component reads matchMedia in
|
||||
* connectedCallback precisely so a test can answer it first.
|
||||
*/
|
||||
import { describe, expect, it, beforeEach } from 'vitest';
|
||||
|
||||
import '@components/now-playing/now-playing';
|
||||
import { Events } from '../../src/events';
|
||||
import { emit, flush } from '@test/support/harness';
|
||||
import { fixture, shadow, shadowAll, text } from '@test/support/render';
|
||||
import type { TrackInfo } from '@store/player-store';
|
||||
import type { QueueTrack } from '@store/queue-store';
|
||||
|
||||
const TRACK: TrackInfo = {
|
||||
fileName: 'ashes.mp3',
|
||||
filePath: '/music/ashes.mp3',
|
||||
trackLength: 215,
|
||||
seekPosition: 0,
|
||||
state: 'playing',
|
||||
title: 'Ashes to Ashes',
|
||||
artist: 'David Bowie',
|
||||
album: 'Scary Monsters',
|
||||
coverArt: '',
|
||||
coverArtSmall: '',
|
||||
coverArtMedium: '',
|
||||
coverArtLarge: '',
|
||||
trackChangeId: 1,
|
||||
artistMbid: '',
|
||||
releaseGroupMbid: '',
|
||||
recordingMbid: '',
|
||||
};
|
||||
|
||||
function queueTrack(n: number, title: string): QueueTrack {
|
||||
return {
|
||||
id: n,
|
||||
audioFileId: n,
|
||||
filePath: `/music/${n}.mp3`,
|
||||
position: n,
|
||||
title,
|
||||
artist: 'David Bowie',
|
||||
album: 'Scary Monsters',
|
||||
coverArtPath: '',
|
||||
artistMbid: '',
|
||||
releaseGroupMbid: '',
|
||||
recordingMbid: '',
|
||||
};
|
||||
}
|
||||
|
||||
/** Mount the bar with the phone breakpoint answering `matches`. */
|
||||
async function mountAt(phone: boolean) {
|
||||
const real = window.matchMedia.bind(window);
|
||||
|
||||
window.matchMedia = ((q: string) =>
|
||||
q.includes('max-width: 599px')
|
||||
? {
|
||||
matches: phone,
|
||||
media: q,
|
||||
addEventListener() {},
|
||||
removeEventListener() {},
|
||||
}
|
||||
: real(q)) as typeof window.matchMedia;
|
||||
|
||||
try {
|
||||
const el = await fixture('now-playing');
|
||||
|
||||
emit(Events.TrackChanged, { ...TRACK, trackChangeId: 20 });
|
||||
emit(Events.QueueChanged, {
|
||||
tracks: [queueTrack(1, 'Ashes to Ashes')],
|
||||
currentIndex: 0,
|
||||
source: { type: 'album', id: 7, label: 'Scary Monsters' },
|
||||
});
|
||||
await flush();
|
||||
await el.updateComplete;
|
||||
|
||||
return el;
|
||||
} finally {
|
||||
window.matchMedia = real;
|
||||
}
|
||||
}
|
||||
|
||||
describe('the mini player on a phone', () => {
|
||||
beforeEach(() => {
|
||||
emit(Events.TrackChanged, { ...TRACK, trackChangeId: 1 });
|
||||
});
|
||||
|
||||
it('renders the title and artist as plain text', async () => {
|
||||
const el = await mountAt(true);
|
||||
|
||||
expect(shadowAll(el, '.explore-link').length).toBe(0);
|
||||
|
||||
// The words are unchanged — this is about what they are, not about
|
||||
// hiding them. A fix that dropped the text would pass an assertion
|
||||
// about links alone.
|
||||
expect(text(el, '[data-testid="now-playing-title"]')).toContain(
|
||||
'Ashes to Ashes',
|
||||
);
|
||||
expect(text(el, '[data-testid="now-playing-artist"]')).toContain(
|
||||
'David Bowie',
|
||||
);
|
||||
});
|
||||
|
||||
it('does not navigate from the source line', async () => {
|
||||
const el = await mountAt(true);
|
||||
const source = shadow<HTMLElement>(el, '[data-testid="now-playing-source"]');
|
||||
|
||||
expect(source?.classList.contains('navigable')).toBe(false);
|
||||
|
||||
let navigated = false;
|
||||
el.addEventListener('navigate', () => {
|
||||
navigated = true;
|
||||
});
|
||||
|
||||
source?.click();
|
||||
|
||||
expect(navigated).toBe(false);
|
||||
});
|
||||
|
||||
it('still says where the queue came from', async () => {
|
||||
const el = await mountAt(true);
|
||||
|
||||
// Dropping the *link* is the change; dropping the information would
|
||||
// be a different and worse one.
|
||||
expect(text(el, '[data-testid="now-playing-source"]')).toBe(
|
||||
'Playing from Scary Monsters',
|
||||
);
|
||||
});
|
||||
|
||||
it('leaves the desktop bar exactly as it was', async () => {
|
||||
const el = await mountAt(false);
|
||||
|
||||
expect(shadowAll(el, '.explore-link').length).toBeGreaterThan(0);
|
||||
|
||||
const source = shadow<HTMLElement>(el, '[data-testid="now-playing-source"]');
|
||||
|
||||
expect(source?.classList.contains('navigable')).toBe(true);
|
||||
|
||||
let detail: unknown;
|
||||
el.addEventListener('navigate', (e) => {
|
||||
detail = (e as CustomEvent).detail;
|
||||
});
|
||||
|
||||
source?.click();
|
||||
|
||||
expect(detail).toEqual({
|
||||
view: 'explore-album-details',
|
||||
localAlbumId: 7,
|
||||
albumName: 'Scary Monsters',
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user