Playlists slotted three buttons totalling 390px into a header that gets 700px at 900x600, so "New Smart Playlist" rendered 114 of its 162px with the queue closed, and 158 of 162 at the 800x600 enforced minimum. On a phone none of the three could be reached at all, which is what the Android report said. Plan 018's size matrix promises the opposite: no action is ever unreachable at any supported size. The header could not fix that for slotted markup, and that is a fact about the API rather than an effort estimate — a component cannot move another component's light-DOM children into a dropdown and keep their behaviour, and arbitrary markup offers nothing generic to render as a menu item. So a host passes `PageAction[]` and the header chooses the rendering; the slot survives for markup a data list cannot express, at the stated cost that a slotted action does not collapse. All three hosts that slot actions migrated, which also normalises the plain-<button>/<wa-button> split between them onto one shape the header styles — and lets it measure a button that has already upgraded, rather than a wa-button whose shadow DOM arrives in its own first update. Four things in it are load-bearing: - Every measuring pass starts from all-visible, so the collapsed set is a pure function of the current width and an action comes back when the window grows. It flips `hidden` imperatively rather than re-rendering between steps, or the intermediate state paints and the fix flashes the overflow it exists to prevent. - "Fits" means nothing is clipped, not that the header does not overflow. Once the title can ellipsis it absorbs the pressure and scrollWidth reports a perfect fit while the heading reads "Playlis…" — this bug moved from the button to the title, and invisible to the same measurement that missed it the first time. - New Playlist has the highest priority because it is the drop target and a closed menu cannot be one. `PageAction.drop` therefore carries the host's own handlers; the affordance is absent from the overflow rather than approximated there. - The overflow trigger is a named button with aria-expanded and an aria-controls naming a panel that is always in the DOM, and the keyboard model is the shared `MenuKeyboard`. `layout-overflow.spec.ts` passes on the broken build — it asserts the shell needs no sideways scrolling, and clipping inside a component is invisible to it, which is why this defect survived a spec named for it. The new spec measures each button against its own header at four viewports and asserts buttons plus menu account for every declared action, without which it would pass vacuously on a build rendering none. Closes #69
186 lines
5.6 KiB
TypeScript
186 lines
5.6 KiB
TypeScript
/**
|
|
* The home page renders whatever `backend/home` decided, and nothing
|
|
* else: the shelves, their stated reasons, and two things a cover can
|
|
* do. So these tests are about the contract between the two — that a
|
|
* shelf's reason is displayed rather than swallowed, that a card opens
|
|
* the album it names, and that a play button plays it instead of
|
|
* opening it.
|
|
*/
|
|
import { describe, expect, it, beforeEach } from 'vitest';
|
|
import type { LitElement } from 'lit';
|
|
|
|
import '@components/home-view/home-view';
|
|
import { stub, calls, lastArgs, stubFailure } from '@test/support/harness';
|
|
import { fixture, shadow, shadowAll, texts, text } from '@test/support/render';
|
|
|
|
function album(id: number, name: string, artist = 'Artist') {
|
|
return {
|
|
ID: id,
|
|
Name: name,
|
|
ArtistName: artist,
|
|
MBID: '',
|
|
Year: 2000,
|
|
ReleaseYear: 2000,
|
|
CoverArtPath: '',
|
|
CoverArtSmall: '',
|
|
CoverArtMedium: '',
|
|
CoverArtLarge: '',
|
|
ArtistMBID: '',
|
|
};
|
|
}
|
|
|
|
const SHELVES = [
|
|
{
|
|
id: 'recently-played',
|
|
kind: 'recently-played',
|
|
title: 'Pick up where you left off',
|
|
subtitle: 'The last albums you played',
|
|
albums: [album(1, 'Kid A'), album(2, 'Amnesiac')],
|
|
},
|
|
{
|
|
id: 'genre-Doom Jazz',
|
|
kind: 'genre',
|
|
title: 'Doom Jazz',
|
|
subtitle: 'Because your library is full of it',
|
|
albums: [album(3, 'Black Ships')],
|
|
},
|
|
];
|
|
|
|
describe('home view', () => {
|
|
beforeEach(() => {
|
|
stub('home.Service.GetShelves', SHELVES);
|
|
stub('library.Library.GetAlbumTracks', [
|
|
{ FilePath: '/music/1.mp3' },
|
|
{ FilePath: '/music/2.mp3' },
|
|
]);
|
|
});
|
|
|
|
it('renders a row per shelf, each with the reason it exists', async () => {
|
|
const el = await fixture('home-view');
|
|
await el.updateComplete;
|
|
|
|
expect(texts(el, '.shelf-title')).toEqual([
|
|
'Pick up where you left off',
|
|
'Doom Jazz',
|
|
]);
|
|
|
|
// The subtitle is the whole difference between a shelf and a grid.
|
|
expect(texts(el, '.shelf-sub')).toEqual([
|
|
'The last albums you played',
|
|
'Because your library is full of it',
|
|
]);
|
|
});
|
|
|
|
it('draws a letter tile for an album with no cover, not a hole', async () => {
|
|
// H-9: the missing-art placeholder was a small dim icon on a
|
|
// surface the same colour as the page, so a shelf read as having
|
|
// gaps in it — while the Albums and Artists grids both drew a
|
|
// letter tile. Plainly visible in any Home screenshot, and
|
|
// invisible to every assertion in this file until now.
|
|
const el = await fixture('home-view');
|
|
|
|
await el.updateComplete;
|
|
await new Promise((r) => setTimeout(r, 20));
|
|
|
|
const placeholders = shadowAll(el, '.art .placeholder');
|
|
|
|
expect(placeholders.length, 'no placeholders for coverless albums').toBeGreaterThan(
|
|
0,
|
|
);
|
|
// The initial of the album's own name, as cover-grid does it.
|
|
expect(placeholders[0]?.textContent?.trim()).toBe('K');
|
|
// And it is a tile: it fills the art box rather than sitting in it.
|
|
expect(getComputedStyle(placeholders[0]!).backgroundImage).not.toBe('none');
|
|
});
|
|
|
|
it('keys each row by the kind the backend assigned', async () => {
|
|
const el = await fixture('home-view');
|
|
await el.updateComplete;
|
|
|
|
expect(
|
|
shadowAll(el, '.shelf').map((s) => s.getAttribute('data-kind')),
|
|
).toEqual(['recently-played', 'genre']);
|
|
});
|
|
|
|
it('opens the album a card names, by local id', async () => {
|
|
const el = await fixture('home-view');
|
|
await el.updateComplete;
|
|
|
|
const seen: unknown[] = [];
|
|
el.addEventListener('navigate', (e) => seen.push((e as CustomEvent).detail));
|
|
|
|
shadow<HTMLElement>(el, '.card')!.click();
|
|
|
|
expect(seen).toEqual([
|
|
{
|
|
view: 'explore-album-details',
|
|
releaseGroupMBID: '',
|
|
albumName: 'Kid A',
|
|
artistName: 'Artist',
|
|
localAlbumId: 1,
|
|
},
|
|
]);
|
|
});
|
|
|
|
it('plays the album from the play button without navigating', async () => {
|
|
const el = await fixture('home-view');
|
|
await el.updateComplete;
|
|
|
|
const seen: unknown[] = [];
|
|
el.addEventListener('navigate', (e) => seen.push(e));
|
|
|
|
shadow<HTMLElement>(el, '.play')!.click();
|
|
await new Promise((r) => setTimeout(r, 0));
|
|
|
|
expect(seen).toEqual([]);
|
|
expect(lastArgs('library.Library.GetAlbumTracks')).toEqual([1, 0]);
|
|
expect(lastArgs('queue.Queue.SetQueue')).toEqual([
|
|
['/music/1.mp3', '/music/2.mp3'],
|
|
0,
|
|
true,
|
|
{ type: 'album', id: 1, label: 'Kid A' },
|
|
]);
|
|
});
|
|
|
|
it('says so rather than rendering an empty page when there is nothing', async () => {
|
|
stub('home.Service.GetShelves', []);
|
|
|
|
const el = await fixture('home-view');
|
|
await el.updateComplete;
|
|
|
|
expect(text(el, '.empty')).toContain('Nothing to suggest yet');
|
|
});
|
|
|
|
it('reports a backend failure instead of pretending the library is empty', async () => {
|
|
stubFailure('home.Service.GetShelves');
|
|
|
|
const el = await fixture('home-view');
|
|
await el.updateComplete;
|
|
await el.updateComplete;
|
|
|
|
expect(text(el, '.empty')).toContain('Could not read your library');
|
|
});
|
|
|
|
it('rebuilds on demand', async () => {
|
|
const el = await fixture('home-view');
|
|
await el.updateComplete;
|
|
|
|
const before = calls('home.Service.GetShelves').length;
|
|
|
|
// The action is declared to `page-header` rather than slotted as
|
|
// markup (#69), so it is a button in *that* shadow root now.
|
|
const header = shadow<HTMLElement>(el, 'page-header')!;
|
|
|
|
await (header as LitElement).updateComplete;
|
|
|
|
header.shadowRoot!
|
|
.querySelector<HTMLButtonElement>(
|
|
'[data-testid="page-action-shuffle-suggestions"]',
|
|
)!
|
|
.click();
|
|
await el.updateComplete;
|
|
|
|
expect(calls('home.Service.GetShelves').length).toBe(before + 1);
|
|
});
|
|
});
|