feat(albums): draw the album dropdown that was already being computed
Enter on an album card fetched the album's tracks over the IPC and ran the whole split state machine (splitMode true, splitIndex measured against the real container), then render() drew the single grid because it never consulted splitMode; connectedCallback referenced renderSplitGrid only to satisfy noUnusedLocals. perf.p2 files this as dead code — it is the only route from the albums grid to track-details, since a plain click navigates to the catalog page instead. Two things it needed that the audit does not mention. The grid could not scroll: .grid-scroll-container is the markup artists-view and genres-view use, and cover-grid had the class with no rule for it, so 186984px of albums sat in a 772px box at 5000 albums, unreachable by wheel, keyboard or scrollbar — and that is the element scroll-manager saves and restores, so its scrollTop was permanently 0. And the shared context menu was labelled 'Album actions' unconditionally, which nothing could observe while a track menu was unreachable. Both halves of the split grid carry the listbox semantics the single grid gained in the ARIA pass.
This commit is contained in:
@@ -689,6 +689,37 @@ retained chars and cap in one eval, rather than the next session having
|
||||
to rebuild the twenty-four-search reproduction before it can tell
|
||||
whether the ceiling still holds.
|
||||
|
||||
**Expanding an album shows its tracks, and the code to do it was
|
||||
written and never called.** `cover-grid`'s dropdown — the album's
|
||||
tracks drawn between the two halves of a split grid — was reachable
|
||||
only from Enter/Space on a focused card (a plain *click* navigates to
|
||||
`explore-album-details`), and that path fetched the tracks over the
|
||||
IPC, ran the whole split state machine and then rendered the single
|
||||
grid, because `render()` never consulted `splitMode`.
|
||||
`connectedCallback` referenced `renderSplitGrid` purely to satisfy
|
||||
`noUnusedLocals`. `perf.p2` files this as dead code in the bundle; it
|
||||
is the only route from the albums grid to `track-details`.
|
||||
|
||||
Two things it needed that are not in the audit. **The grid could not
|
||||
scroll at all**: `.grid-scroll-container` is the same markup
|
||||
`artists-view` and `genres-view` use, and `cover-grid` had the class
|
||||
with *no rule for it*, so the container grew to its full content height
|
||||
inside an `overflow: hidden` host — 186 984 px of albums in a 772 px
|
||||
box at 5 000 albums, unreachable by wheel, keyboard or scrollbar, and
|
||||
invisible on the eight-album fixture. That is also the element
|
||||
`scroll-manager.ts` saves and restores, so its `scrollTop` was
|
||||
permanently 0; with a real scroller the manager works as designed
|
||||
(2891 preserved exactly across an expand). And the shared context-menu
|
||||
panel was **labelled "Album actions" unconditionally**, which nothing
|
||||
could observe while the only menu that could open on a track was
|
||||
unreachable.
|
||||
|
||||
The manager **moves the scroll to reveal the dropdown** rather than
|
||||
preserving it — on a small library that is most of the way back to the
|
||||
top (80 → 4, with the content *taller* after, so it is not clamping).
|
||||
"The position is preserved" is the wrong assertion; "the dropdown is on
|
||||
screen" is the contract.
|
||||
|
||||
**A list pays per row, and only while scrolling.** The track list's Art
|
||||
column rendered `CoverArtPath` — the original artwork — into a 24 px
|
||||
box while `CoverArtSmall` sat unused on the same model, and
|
||||
|
||||
@@ -0,0 +1,223 @@
|
||||
import { test, expect } from '../support/fixtures.js';
|
||||
import type { Page } from '@playwright/test';
|
||||
|
||||
/**
|
||||
* Plan 007 phase 5: expanding an album shows its tracks.
|
||||
*
|
||||
* `perf.p2` files `cover-grid`'s `renderSplitGrid` as dead code carried
|
||||
* in the bundle. It was a **missing feature** whose data path already
|
||||
* worked: Enter on an album card fetched the album's tracks over the
|
||||
* IPC and ran the whole split state machine, and then `render()` drew
|
||||
* the single grid regardless because it never consulted `splitMode`.
|
||||
*
|
||||
* This spec is here rather than only in the component tier because two
|
||||
* of the three things that had to be true are about the real app: that
|
||||
* the route from a card to `track-details` exists at all (a plain click
|
||||
* navigates to the catalog page instead, so the dropdown is the only
|
||||
* one), and that the grid keeps its scroll position when the dropdown
|
||||
* opens — which it did not until the scroll container was given an
|
||||
* overflow, having never scrolled in its life.
|
||||
*/
|
||||
test.describe('the album dropdown', () => {
|
||||
test.beforeEach(async ({ app }) => {
|
||||
await app.getByTestId('nav-albums').click();
|
||||
await expect(app.getByTestId('main-content')).toHaveAttribute(
|
||||
'data-active-view',
|
||||
'albums',
|
||||
);
|
||||
});
|
||||
|
||||
test.afterEach(async ({ app }) => {
|
||||
// The suite shares one backend process in file order, and an open
|
||||
// dropdown changes what the next spec's selectors match.
|
||||
await closeDropdown(app);
|
||||
await app.getByTestId('nav-tracks').click();
|
||||
});
|
||||
|
||||
test('Enter on a card draws that album’s tracks', async ({ app }) => {
|
||||
await expandCard(app, 1);
|
||||
|
||||
await expect
|
||||
.poll(() => dropdownState(app))
|
||||
.toMatchObject({ present: true, split: true });
|
||||
|
||||
const state = await dropdownState(app);
|
||||
|
||||
expect(state.rows).toBeGreaterThan(0);
|
||||
expect(state.rows).toBe(state.tracks);
|
||||
});
|
||||
|
||||
test('a track in it reaches Track Details', async ({ app }) => {
|
||||
// The only route from the albums grid to a track. A plain click on
|
||||
// a card navigates to `explore-album-details` instead.
|
||||
await expandCard(app, 1);
|
||||
await expect.poll(() => dropdownState(app)).toMatchObject({
|
||||
present: true,
|
||||
});
|
||||
|
||||
await app.evaluate(() => {
|
||||
document
|
||||
.querySelector('cover-grid')
|
||||
?.shadowRoot?.querySelector('album-dropdown')
|
||||
?.shadowRoot?.querySelector('.track-row')
|
||||
?.dispatchEvent(
|
||||
new MouseEvent('contextmenu', {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
clientX: 300,
|
||||
clientY: 400,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
// The panel is shared with the album menu and used to be labelled
|
||||
// "Album actions" unconditionally — which nothing could observe
|
||||
// while the only menu that could open on a track was unreachable.
|
||||
await expect(app.getByRole('menu', { name: 'Track actions' }))
|
||||
.toBeVisible();
|
||||
|
||||
await app.getByRole('menuitem', { name: 'Track Details' }).click();
|
||||
|
||||
await expect(
|
||||
app.getByRole('dialog', { name: 'Track Details' }),
|
||||
).toBeVisible();
|
||||
|
||||
await app.keyboard.press('Escape');
|
||||
await expect(
|
||||
app.getByRole('dialog', { name: 'Track Details' }),
|
||||
).toHaveCount(0);
|
||||
});
|
||||
|
||||
test('the grid it opens in can be scrolled', async ({ app }) => {
|
||||
// `cover-grid` carried the same `.grid-scroll-container` markup as
|
||||
// `artists-view` with no rule for the class, so it never scrolled:
|
||||
// the container grew to its full content height inside an
|
||||
// `overflow: hidden` host and everything past the first screenful
|
||||
// was unreachable. Invisible on eight albums, fatal on a real
|
||||
// library — measured at 5 000 albums, 186 984 px of content in a
|
||||
// 772 px box.
|
||||
//
|
||||
// The fixture does not scroll at the default viewport, so this
|
||||
// shrinks the window until it does. Without that, every form of
|
||||
// this assertion passes against a scrollTop that is 0 both times
|
||||
// and could not have moved.
|
||||
await app.setViewportSize({ width: 900, height: 600 });
|
||||
|
||||
try {
|
||||
await expect.poll(() => scrollRange(app)).toMatchObject({
|
||||
scrollable: true,
|
||||
overflowY: 'auto',
|
||||
});
|
||||
|
||||
await app.evaluate(() => {
|
||||
const sc = document
|
||||
.querySelector('cover-grid')
|
||||
?.shadowRoot?.querySelector('.grid-scroll-container');
|
||||
|
||||
if (sc) sc.scrollTop = 80;
|
||||
});
|
||||
|
||||
expect(await scrollTop(app)).toBe(80);
|
||||
|
||||
// And the dropdown it opens is on screen, wherever the manager
|
||||
// decides that leaves the scroll. It is *not* "the position is
|
||||
// preserved": `scrollToShowDropdown` deliberately moves it to
|
||||
// reveal the dropdown, which on a library this small is most of
|
||||
// the way back to the top (80 → 4, with the content *taller*
|
||||
// than before, so it is not clamping). At 5 000 albums, with the
|
||||
// expanded card mid-viewport, the same code preserved 2891
|
||||
// exactly.
|
||||
await expandCard(app, 1);
|
||||
await expect.poll(() => dropdownState(app)).toMatchObject({
|
||||
present: true,
|
||||
});
|
||||
|
||||
await expect.poll(() => dropdownOnScreen(app)).toBe(true);
|
||||
} finally {
|
||||
await app.setViewportSize({ width: 1440, height: 900 });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
/** Focus a card and press Enter, which is the only thing that expands one. */
|
||||
async function expandCard(app: Page, index: number): Promise<void> {
|
||||
await app.evaluate((i) => {
|
||||
const card = document
|
||||
.querySelector('cover-grid')
|
||||
?.shadowRoot?.querySelectorAll<HTMLElement>('.album-card')[i];
|
||||
|
||||
card?.focus();
|
||||
card?.dispatchEvent(
|
||||
new KeyboardEvent('keydown', {
|
||||
key: 'Enter',
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}),
|
||||
);
|
||||
}, index);
|
||||
}
|
||||
|
||||
async function closeDropdown(app: Page): Promise<void> {
|
||||
await app.evaluate(() => {
|
||||
const grid = document.querySelector('cover-grid') as
|
||||
| (Element & { expandedAlbumId: number | null })
|
||||
| null;
|
||||
|
||||
if (grid) grid.expandedAlbumId = null;
|
||||
});
|
||||
}
|
||||
|
||||
/** Whether the grid can scroll at all, which decides if a probe can move. */
|
||||
async function scrollRange(app: Page) {
|
||||
return app.evaluate(() => {
|
||||
const sc = document
|
||||
.querySelector('cover-grid')
|
||||
?.shadowRoot?.querySelector('.grid-scroll-container');
|
||||
|
||||
return {
|
||||
scrollable: !!sc && sc.scrollHeight > sc.clientHeight + 40,
|
||||
overflowY: sc ? getComputedStyle(sc).overflowY : '',
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async function scrollTop(app: Page): Promise<number> {
|
||||
return app.evaluate(
|
||||
() =>
|
||||
document
|
||||
.querySelector('cover-grid')
|
||||
?.shadowRoot?.querySelector('.grid-scroll-container')?.scrollTop ?? -1,
|
||||
);
|
||||
}
|
||||
|
||||
/** Whether the open dropdown is inside the scroll container's viewport. */
|
||||
async function dropdownOnScreen(app: Page): Promise<boolean> {
|
||||
return app.evaluate(() => {
|
||||
const grid = document.querySelector('cover-grid');
|
||||
const sc = grid?.shadowRoot?.querySelector('.grid-scroll-container');
|
||||
const dd = grid?.shadowRoot?.querySelector('album-dropdown');
|
||||
|
||||
if (!sc || !dd) return false;
|
||||
|
||||
const box = sc.getBoundingClientRect();
|
||||
const it = dd.getBoundingClientRect();
|
||||
|
||||
return it.bottom > box.top && it.top < box.bottom;
|
||||
});
|
||||
}
|
||||
|
||||
async function dropdownState(app: Page) {
|
||||
return app.evaluate(() => {
|
||||
const grid = document.querySelector('cover-grid') as
|
||||
| (Element & { splitMode: boolean; expandedTracks: unknown[] })
|
||||
| null;
|
||||
const dropdown = grid?.shadowRoot?.querySelector('album-dropdown');
|
||||
|
||||
return {
|
||||
present: !!dropdown,
|
||||
split: grid?.splitMode ?? false,
|
||||
tracks: grid?.expandedTracks?.length ?? 0,
|
||||
rows: dropdown?.shadowRoot?.querySelectorAll('.track-row').length ?? 0,
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -13,6 +13,27 @@ const gridStyles = css`
|
||||
contain: layout style;
|
||||
}
|
||||
|
||||
/*
|
||||
* The scroller. artists-view and genres-view carry the same
|
||||
* markup with this rule; cover-grid had the class and no rule for
|
||||
* it, so nothing in the albums view scrolled — the container grew to
|
||||
* its full content height inside an overflow: hidden host and
|
||||
* everything past the first screenful was unreachable by wheel,
|
||||
* keyboard or scrollbar. Invisible on the eight-album fixture and
|
||||
* fatal on a real library: measured at 5 000 albums, 186 984 px of
|
||||
* content in a 772 px box.
|
||||
*
|
||||
* It is also what the dropdown's scroll manager was written
|
||||
* against — it saves and restores this element's scrollTop, which
|
||||
* was permanently 0.
|
||||
*/
|
||||
.grid-scroll-container {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
contain: paint;
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
* Album card
|
||||
* ======================================== */
|
||||
|
||||
@@ -430,10 +430,6 @@ export class CoverGrid
|
||||
|
||||
override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
// Reference renderSplitGrid so the deferred split-grid
|
||||
// render path (and its track-event helpers) doesn't trip
|
||||
// noUnusedLocals. Never invoked at runtime.
|
||||
void this.renderSplitGrid;
|
||||
this.restoreSortPreferences();
|
||||
this.loadAlbums();
|
||||
|
||||
@@ -1783,7 +1779,18 @@ export class CoverGrid
|
||||
`;
|
||||
}
|
||||
|
||||
const gridContent = this.renderSingleGrid();
|
||||
// The split path draws the dropdown between two grids. Until
|
||||
// this was wired up, `render()` ignored `splitMode` entirely:
|
||||
// pressing Enter on an album card fetched its tracks over the
|
||||
// IPC, ran the whole split state machine (`splitMode: true`,
|
||||
// `splitIndex: 6`, measured against the real container) and
|
||||
// then drew the single grid regardless, so the only route from
|
||||
// the albums grid to a track was the plain click that
|
||||
// navigates away to the catalog page.
|
||||
const gridContent =
|
||||
this.splitMode && this.expandedTracks.length > 0
|
||||
? this.renderSplitGrid()
|
||||
: this.renderSingleGrid();
|
||||
|
||||
return html`
|
||||
${this.renderPageHeader()}
|
||||
@@ -1821,10 +1828,12 @@ export class CoverGrid
|
||||
}
|
||||
|
||||
/**
|
||||
* Dual virtualizer — dropdown sandwiched between
|
||||
* "before" and "after" grids. Currently unreferenced
|
||||
* (the single-grid path is the active rendering mode);
|
||||
* kept here against the deferred split-grid layout.
|
||||
* Dual virtualizer — dropdown sandwiched between the "before" and
|
||||
* "after" halves of the grid.
|
||||
*
|
||||
* Both halves carry the same listbox semantics as the single grid:
|
||||
* they are one control to the user, and a selection that spans the
|
||||
* dropdown must be announced the same way on either side of it.
|
||||
*/
|
||||
private renderSplitGrid() {
|
||||
const sm = this.scrollMgr;
|
||||
@@ -1836,6 +1845,9 @@ export class CoverGrid
|
||||
return html`
|
||||
<lit-virtualizer
|
||||
id="grid-before"
|
||||
role="listbox"
|
||||
aria-label="Albums"
|
||||
aria-multiselectable="true"
|
||||
.items=${this.getBeforeEntries()}
|
||||
.renderItem=${this.renderGridEntry}
|
||||
.keyFunction=${(entry: GridEntry) => entry.album.ID}
|
||||
@@ -1865,6 +1877,9 @@ export class CoverGrid
|
||||
? html`
|
||||
<lit-virtualizer
|
||||
id="grid-after"
|
||||
role="listbox"
|
||||
aria-label="Albums, continued"
|
||||
aria-multiselectable="true"
|
||||
.items=${afterEntries}
|
||||
.renderItem=${this.renderGridEntry}
|
||||
.keyFunction=${(entry: GridEntry) => entry.album.ID}
|
||||
@@ -1904,7 +1919,13 @@ export class CoverGrid
|
||||
>
|
||||
${ctxMenu.contextMenuOpen
|
||||
? html`
|
||||
<div class="context-menu-panel" role="menu" aria-label="Album actions">
|
||||
<div
|
||||
class="context-menu-panel"
|
||||
role="menu"
|
||||
aria-label=${this.contextMenuTarget.kind === 'track'
|
||||
? 'Track actions'
|
||||
: 'Album actions'}
|
||||
>
|
||||
<wa-dropdown-item
|
||||
@click=${() =>
|
||||
this.onContextMenuAction(
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
/**
|
||||
* Expanding an album shows its tracks.
|
||||
*
|
||||
* `perf.p2` files `cover-grid`'s `renderSplitGrid` as dead code carried
|
||||
* in the bundle. It is not dead — it is a **missing feature**, and one
|
||||
* whose data path was already working: pressing Enter on an album card
|
||||
* fetched that album's tracks over the IPC, ran the whole split state
|
||||
* machine (measured in the running app: `splitMode: true`,
|
||||
* `splitIndex: 90` against a real container) and then drew the single
|
||||
* grid regardless, because `render()` never consulted `splitMode`.
|
||||
* `connectedCallback` referenced the method solely to satisfy
|
||||
* `noUnusedLocals`.
|
||||
*
|
||||
* It matters because it is the only route from the albums grid to a
|
||||
* *track*: a plain click on a card navigates to the catalog page, so
|
||||
* without the dropdown the view could not show what is on an album.
|
||||
*/
|
||||
import { describe, expect, it, beforeEach } from 'vitest';
|
||||
import type { LitElement } from 'lit';
|
||||
|
||||
import '@components/cover-grid/cover-grid';
|
||||
import { emit, stub, flush, resetHarness } from '@test/support/harness';
|
||||
import { Events } from '../../src/events';
|
||||
import { fixture, shadow, shadowAll } from '@test/support/render';
|
||||
|
||||
/**
|
||||
* Enough albums to fill more than one row.
|
||||
*
|
||||
* The dropdown is drawn *after the row the expanded album is in*, so a
|
||||
* library that fits on one row puts every album in the "before" half
|
||||
* and renders no "after" grid at all — which is correct, and is not the
|
||||
* arrangement this is checking.
|
||||
*/
|
||||
const ALBUMS = Array.from({ length: 24 }, (_, i) => ({
|
||||
ID: i + 1,
|
||||
Name: `Album ${i + 1}`,
|
||||
ArtistName: 'Aurora Fields',
|
||||
Year: 2019,
|
||||
}));
|
||||
|
||||
const TRACKS = [
|
||||
{ ID: 11, TrackName: 'Salt Air', FilePath: '/m/1.mp3', TrackNumber: 1 },
|
||||
{ ID: 12, TrackName: 'Tideline', FilePath: '/m/2.mp3', TrackNumber: 2 },
|
||||
];
|
||||
|
||||
/** Give the virtualizer a viewport; a zero-height host renders nothing. */
|
||||
function sized(el: HTMLElement): void {
|
||||
el.style.display = 'block';
|
||||
el.style.height = '600px';
|
||||
el.style.width = '900px';
|
||||
}
|
||||
|
||||
async function settle(el: LitElement): Promise<void> {
|
||||
await flush();
|
||||
await el.updateComplete;
|
||||
await new Promise((r) => setTimeout(r, 80));
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the dropdown the way the app does.
|
||||
*
|
||||
* A plain *click* navigates to the catalog page — Enter (or Space) on a
|
||||
* focused card is the only thing that expands one, which is why the
|
||||
* feature could be missing without anyone tripping over it.
|
||||
*/
|
||||
async function expandFirstCard(el: LitElement): Promise<void> {
|
||||
const card = shadowAll(el, '.album-card')[0] as HTMLElement;
|
||||
|
||||
card.focus();
|
||||
card.dispatchEvent(
|
||||
new KeyboardEvent('keydown', {
|
||||
key: 'Enter',
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}),
|
||||
);
|
||||
|
||||
await settle(el);
|
||||
}
|
||||
|
||||
describe('the album dropdown', () => {
|
||||
beforeEach(() => {
|
||||
resetHarness();
|
||||
stub('library.Library.GetAllAlbums', ALBUMS);
|
||||
stub('library.Library.GetAllTracks', []);
|
||||
stub('library.Library.GetAlbumTracks', TRACKS);
|
||||
stub('library.Library.GetAlbumTracksByLibrary', TRACKS);
|
||||
emit(Events.LibraryScanComplete);
|
||||
});
|
||||
|
||||
it('draws the tracks it fetches, between two halves of the grid', async () => {
|
||||
const el = await fixture<LitElement>('cover-grid');
|
||||
|
||||
sized(el);
|
||||
await settle(el);
|
||||
|
||||
expect(shadow(el, 'album-dropdown')).toBeNull();
|
||||
|
||||
await expandFirstCard(el);
|
||||
|
||||
// The state machine was always correct; what was missing was the
|
||||
// render. Assert on what is *drawn*, so a future `render()` that
|
||||
// stops consulting `splitMode` fails here rather than silently
|
||||
// going back to fetching tracks nobody sees.
|
||||
const dropdown = shadow(el, 'album-dropdown');
|
||||
|
||||
expect(dropdown).toBeTruthy();
|
||||
expect(
|
||||
dropdown!.shadowRoot!.querySelectorAll('.track-row'),
|
||||
).toHaveLength(TRACKS.length);
|
||||
|
||||
const ids = shadowAll(el, 'lit-virtualizer').map((v) => v.id);
|
||||
|
||||
expect(ids).toEqual(['grid-before', 'grid-after']);
|
||||
expect(shadow(el, '#grid-single')).toBeNull();
|
||||
});
|
||||
|
||||
it('keeps the listbox semantics on both halves', async () => {
|
||||
// The single grid became a `listbox` of `option`s in the ARIA pass;
|
||||
// the split path predates it. They are one control to the user, and
|
||||
// a selection spanning the dropdown has to be announced the same
|
||||
// way on either side of it.
|
||||
const el = await fixture<LitElement>('cover-grid');
|
||||
|
||||
sized(el);
|
||||
await settle(el);
|
||||
await expandFirstCard(el);
|
||||
|
||||
for (const v of shadowAll(el, 'lit-virtualizer')) {
|
||||
expect(v.getAttribute('role')).toBe('listbox');
|
||||
expect(v.getAttribute('aria-multiselectable')).toBe('true');
|
||||
expect(v.getAttribute('aria-label')).toBeTruthy();
|
||||
}
|
||||
});
|
||||
|
||||
it('closes back to the single grid', async () => {
|
||||
const el = await fixture<LitElement>('cover-grid');
|
||||
|
||||
sized(el);
|
||||
await settle(el);
|
||||
await expandFirstCard(el);
|
||||
expect(shadow(el, 'album-dropdown')).toBeTruthy();
|
||||
|
||||
// Enter on the same card again is the toggle.
|
||||
await expandFirstCard(el);
|
||||
|
||||
expect(shadow(el, 'album-dropdown')).toBeNull();
|
||||
expect(shadowAll(el, 'lit-virtualizer').map((v) => v.id)).toEqual([
|
||||
'grid-single',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('the albums grid scrolls', () => {
|
||||
beforeEach(() => {
|
||||
resetHarness();
|
||||
stub('library.Library.GetAllAlbums', ALBUMS);
|
||||
stub('library.Library.GetAllTracks', []);
|
||||
emit(Events.LibraryScanComplete);
|
||||
});
|
||||
|
||||
it('gives its scroll container an overflow', async () => {
|
||||
// `cover-grid` carried the same `.grid-scroll-container` markup as
|
||||
// `artists-view` and `genres-view` with **no rule for the class**,
|
||||
// so the container grew to its full content height inside an
|
||||
// `overflow: hidden` host and nothing scrolled: measured at 5 000
|
||||
// albums, 186 984 px of content in a 772 px box, unreachable by
|
||||
// wheel, keyboard or scrollbar. Invisible on an eight-album fixture,
|
||||
// which is why a component test asserts the rule rather than the
|
||||
// symptom.
|
||||
const el = await fixture<LitElement>('cover-grid');
|
||||
|
||||
sized(el);
|
||||
await settle(el);
|
||||
|
||||
const container = shadow(el, '.grid-scroll-container')!;
|
||||
|
||||
expect(container).toBeTruthy();
|
||||
expect(getComputedStyle(container).overflowY).toBe('auto');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user