The shelves, the top results and the artist page's discography and similar-artists rows scrolled sideways only by a horizontal wheel or a trackpad, so a plain mouse could not reach anything past the fold. <scroll-row> wraps each of them with previous/next arrows that are hidden at the end they cannot move from, revealed on hover where there is hover, and always shown where there is not. The album card is defined once, in albumCardStyles, instead of twice. explore-view clamped its cards to 130-150px, and with square artwork that made cards in one row different heights as well as widths. The width is fixed now and each line under the art reserves its own space. Covers are inset rather than cropped (contain, not cover). The ownership badge sits over the artwork for owned and unowned alike, and catalog cards no longer dim: a grid of dimmed covers read as a page that had failed to load. The album page's tracklist still dims unowned rows, which says something different about something different. Every MusicBrainz link now goes through openMusicBrainz, which pins the origin, including the one on the album page that still called window.open directly. solid/chevron-left is bundled for the left arrow; without it the arrow drew the missing-icon fallback. Closes #264 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017HJiuc3ZZhxsPXz3ozTirT
175 lines
5.2 KiB
TypeScript
175 lines
5.2 KiB
TypeScript
/**
|
|
* The context menu on a release card on the artist page.
|
|
*
|
|
* The page already had one, for the top *tracks*, and the release cards
|
|
* — which are most of the page — had none: a right-click did whatever
|
|
* the browser does and the keyboard had no way in at all.
|
|
*
|
|
* What is worth pinning is not that the menu opens. It is that the
|
|
* items shown match what the release can actually do, because the three
|
|
* cases genuinely differ: an owned release has files to queue, an
|
|
* unowned catalog release has nothing to play but can be requested, and
|
|
* a library-only release has no catalog id, so it can be played and is
|
|
* the one case with nothing to view upstream. A menu that offers Play
|
|
* on a release with no files is the fault `library-status-indicator`
|
|
* was rewritten to stop making, one control over.
|
|
*/
|
|
import { describe, expect, it, beforeEach } from 'vitest';
|
|
import type { LitElement } from 'lit';
|
|
|
|
import '@components/explore-artist-details/explore-artist-details';
|
|
import { stub, flush, resetHarness } from '@test/support/harness';
|
|
import { fixture, shadow } from '@test/support/render';
|
|
|
|
const ARTIST = 'artist-0001';
|
|
|
|
/** The labels of the open menu's items, trimmed. */
|
|
function menuItems(el: LitElement): string[] {
|
|
// Scoped to the context menu: the artist page also has a Play/Shuffle
|
|
// dropdown, and its panel carries the same class.
|
|
const panel = shadow(el, '#context-menu .context-menu-panel');
|
|
|
|
if (!panel) return [];
|
|
|
|
return [...panel.querySelectorAll('wa-dropdown-item')].map(
|
|
(item) => item.textContent?.trim() ?? '',
|
|
);
|
|
}
|
|
|
|
/** Right-click the nth discography card and let the menu render. */
|
|
async function openMenuOnAlbum(el: LitElement, index: number): Promise<void> {
|
|
const cards = el.shadowRoot?.querySelectorAll('.album-card') ?? [];
|
|
const card = cards[index];
|
|
|
|
expect(card, `no album card at index ${index}`).toBeTruthy();
|
|
|
|
card?.dispatchEvent(
|
|
new MouseEvent('contextmenu', { bubbles: true, cancelable: true }),
|
|
);
|
|
|
|
await flush();
|
|
}
|
|
|
|
beforeEach(() => {
|
|
resetHarness();
|
|
|
|
stub('explore.Service.LookupArtist', { mbid: ARTIST, name: 'Tideline' });
|
|
stub('explore.Service.TopReleaseGroupsForArtist', []);
|
|
stub('explore.Service.TopRecordingsForArtist', []);
|
|
stub('explore.Service.SimilarArtists', []);
|
|
stub('explore.Service.PrefetchReleases', undefined);
|
|
|
|
// One of each case, in the order the assertions below index them.
|
|
stub('explore.Service.BrowseReleaseGroups', [
|
|
{
|
|
mbid: 'rg-owned',
|
|
title: 'Foreshore',
|
|
artistCredit: 'Tideline',
|
|
primaryType: 'Album',
|
|
inLibrary: true,
|
|
localId: 7,
|
|
},
|
|
{
|
|
mbid: 'rg-wanted',
|
|
title: 'Backwash',
|
|
artistCredit: 'Tideline',
|
|
primaryType: 'Album',
|
|
},
|
|
{
|
|
mbid: 'local:12',
|
|
title: 'Bootleg Tape',
|
|
artistCredit: 'Tideline',
|
|
primaryType: 'Album',
|
|
localId: 12,
|
|
},
|
|
]);
|
|
});
|
|
|
|
async function mount(): Promise<LitElement> {
|
|
const el = await fixture<LitElement>('explore-artist-details', {
|
|
artistMBID: ARTIST,
|
|
artistName: 'Tideline',
|
|
});
|
|
|
|
await flush();
|
|
|
|
return el;
|
|
}
|
|
|
|
describe('the context menu on an artist page release', () => {
|
|
it('opens on a right-click and names itself a release menu', async () => {
|
|
const el = await mount();
|
|
|
|
await openMenuOnAlbum(el, 0);
|
|
|
|
const panel = shadow(el, '#context-menu .context-menu-panel');
|
|
|
|
expect(panel).toBeTruthy();
|
|
// The panel is shared with the track menu, so a label that does not
|
|
// move with the target is confidently wrong rather than merely
|
|
// missing.
|
|
expect(panel?.getAttribute('aria-label')).toBe('Release actions');
|
|
});
|
|
|
|
it('offers playback for a release with local files', async () => {
|
|
const el = await mount();
|
|
|
|
await openMenuOnAlbum(el, 0);
|
|
|
|
const items = menuItems(el);
|
|
|
|
expect(items).toContain('Play');
|
|
expect(items).toContain('Add to Queue');
|
|
expect(items).toContain('Play Next');
|
|
// Owned: there is nothing left to ask for.
|
|
expect(items).not.toContain('Request This');
|
|
});
|
|
|
|
it('offers a request, and no playback, for a release nobody owns', async () => {
|
|
const el = await mount();
|
|
|
|
await openMenuOnAlbum(el, 1);
|
|
|
|
const items = menuItems(el);
|
|
|
|
expect(items).not.toContain('Play');
|
|
expect(items).not.toContain('Add to Queue');
|
|
expect(items).toContain('Request This');
|
|
expect(items).toContain('View on MusicBrainz');
|
|
});
|
|
|
|
it('drops the catalog items for a library-only release', async () => {
|
|
const el = await mount();
|
|
|
|
await openMenuOnAlbum(el, 2);
|
|
|
|
const items = menuItems(el);
|
|
|
|
// It has files, so it plays…
|
|
expect(items).toContain('Play');
|
|
// …but a `local:` id names nothing upstream, and wanting something
|
|
// already in the library is not a thing to offer.
|
|
expect(items).not.toContain('View on MusicBrainz');
|
|
expect(items).not.toContain('Request This');
|
|
});
|
|
|
|
it('opens from the keyboard on Shift+F10', async () => {
|
|
const el = await mount();
|
|
|
|
const card = el.shadowRoot?.querySelectorAll('.album-card')[0];
|
|
|
|
card?.dispatchEvent(
|
|
new KeyboardEvent('keydown', {
|
|
key: 'F10',
|
|
shiftKey: true,
|
|
bubbles: true,
|
|
cancelable: true,
|
|
}),
|
|
);
|
|
|
|
await flush();
|
|
|
|
expect(shadow(el, '.context-menu-panel')).toBeTruthy();
|
|
});
|
|
});
|