Every track, album and artist name in the app navigates through `utils/explore-link.ts`, and every sentence of how it does that is a desktop compromise: the navigation is held for one double-click interval so double-clicking the row can still play it, and the target is a few characters of text inside a row. On touch that is a delay on an ambiguous target, and since #63 the row's own tap claims the click anyway -- so the link was unreachable as well as fiddly. So below the phone breakpoint a name renders as plain text and the row's context menu carries the destination instead: `go-to-menu.ts` draws "Go to Artist" / "Go to Album" under exactly the condition the link is not, using `explore-link`'s own exported routing so an untagged entity reaches the library page by the same lookup. Three things this leans on. Suppressing a link with no menu behind it is not a smaller affordance but a destination the phone cannot reach, so `keepOnPhone` is the exception for the three surfaces with no row menu. The items are drawn for a single selection only, which is the Play item's rule one step on. And there is no "Go to Genre", because no row renders a genre link to lose -- that would be new navigation rather than a replacement. Closes #67
136 lines
4.2 KiB
TypeScript
136 lines
4.2 KiB
TypeScript
import {
|
|
test,
|
|
expect,
|
|
callBinding,
|
|
openTheQueue,
|
|
NO_QUEUE_SOURCE,
|
|
} from '../support/fixtures.js';
|
|
import type { Page } from '@playwright/test';
|
|
|
|
/**
|
|
* #67 — a name is not a link on a phone, and the menu is where it went.
|
|
*
|
|
* The queue panel is the surface this is visible on: its rows draw a
|
|
* track title and an artist credit as `explore-link`s at every width,
|
|
* unlike `track-list`, whose phone column set stacks title over artist
|
|
* as plain text already.
|
|
*
|
|
* **The pair is what makes either assertion mean anything.** A link
|
|
* that is gone and a menu item that never arrived is not a smaller
|
|
* affordance — it is a destination the phone cannot reach, which is
|
|
* what plan 018's "no action is unreachable at any supported size"
|
|
* refuses. So each test asserts the phone and the desktop in the same
|
|
* breath: text *and* an item here, a link *and* no item there.
|
|
*
|
|
* The desktop half is also the regression guard for the change: menus
|
|
* above the breakpoint must be exactly what they were, because the name
|
|
* beside them is still a link and a menu that repeats the row is
|
|
* furniture.
|
|
*/
|
|
|
|
/** The reference device's real viewport, not a resized desktop. */
|
|
const DEVICE = { width: 424, height: 439 };
|
|
|
|
/** Wide enough that the queue is a column beside the content. */
|
|
const DESKTOP = { width: 1280, height: 800 };
|
|
|
|
const row = (app: Page, index: number) =>
|
|
app.locator(`queue-panel .track-item[data-index="${index}"]`);
|
|
|
|
/** The queue panel's own context menu, as a list of item labels. */
|
|
async function menuLabels(app: Page): Promise<string[]> {
|
|
return app.evaluate(() =>
|
|
[
|
|
...document
|
|
.querySelector('queue-panel')!
|
|
.shadowRoot!.querySelectorAll('wa-dropdown-item'),
|
|
].map((item) => item.textContent?.replace(/\s+/g, ' ').trim() ?? ''),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Queue three tracks that have an album, for the reason
|
|
* `queue-selection.spec.ts` states at length: `explore-link` routes a
|
|
* title to its *album's* page and renders plain text where it cannot
|
|
* route, so a track with no album answers this file's question with
|
|
* the wrong "no link".
|
|
*/
|
|
async function queueThree(app: Page): Promise<void> {
|
|
const paths = await app.evaluate(async () => {
|
|
const tracks = (await window.__yjEvents.call(
|
|
'library.Library.GetTracks',
|
|
[0],
|
|
10_000,
|
|
)) as { FilePath: string; Album: string; ArtistName: string }[];
|
|
|
|
return tracks
|
|
.filter((t) => t.Album !== '' && t.ArtistName !== '')
|
|
.slice(0, 3)
|
|
.map((t) => t.FilePath);
|
|
});
|
|
|
|
await callBinding(app, 'queue.Queue.SetQueue', [
|
|
paths,
|
|
0,
|
|
false,
|
|
NO_QUEUE_SOURCE,
|
|
]);
|
|
}
|
|
|
|
/** Open the row's context menu and read the items back. */
|
|
async function openRowMenu(app: Page, index: number): Promise<string[]> {
|
|
await row(app, index).click({ button: 'right' });
|
|
await expect
|
|
.poll(async () => (await menuLabels(app)).length)
|
|
.toBeGreaterThan(0);
|
|
|
|
return menuLabels(app);
|
|
}
|
|
|
|
test.describe('an inline name and the menu that replaces it', () => {
|
|
test.afterEach(async ({ app }) => {
|
|
await app.keyboard.press('Escape');
|
|
await callBinding(app, 'queue.Queue.Clear').catch(() => {
|
|
/* an empty queue is the state we were asking for */
|
|
});
|
|
await app.setViewportSize(DESKTOP);
|
|
});
|
|
|
|
test('a queue row is plain text on a phone and carries the destination', async ({
|
|
app,
|
|
}) => {
|
|
await app.setViewportSize(DEVICE);
|
|
await queueThree(app);
|
|
await openTheQueue(app);
|
|
await expect(row(app, 0)).toBeVisible();
|
|
|
|
// The name is text: nothing in the row is a link at all.
|
|
await expect(app.locator('queue-panel .track-item .explore-link')).toHaveCount(
|
|
0,
|
|
);
|
|
|
|
const labels = await openRowMenu(app, 0);
|
|
|
|
expect(labels).toContain('Go to Artist');
|
|
expect(labels).toContain('Go to Album');
|
|
});
|
|
|
|
test('the same row on a desktop is a link, and its menu is untouched', async ({
|
|
app,
|
|
}) => {
|
|
await app.setViewportSize(DESKTOP);
|
|
await queueThree(app);
|
|
await openTheQueue(app);
|
|
await expect(row(app, 0)).toBeVisible();
|
|
|
|
await expect(
|
|
row(app, 0).locator('.track-title .explore-link'),
|
|
).toHaveCount(1);
|
|
|
|
const labels = await openRowMenu(app, 0);
|
|
|
|
expect(labels).not.toContain('Go to Artist');
|
|
expect(labels).not.toContain('Go to Album');
|
|
});
|
|
});
|