Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 103 additions & 0 deletions webapp/e2e/studio-nocturne.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4999,6 +4999,91 @@ test("offers no seeking on a sheet that carries no timings", async ({
await expect(view.locator("[aria-current]")).toHaveCount(0);
});

/**
* The strip beside the cover reads the way the lyrics view does.
*
* It used to mark the sung line and leave it to scroll out of its own box, and
* no line could be pressed. Now it holds the sung line in view, and a synced
* line is a button that seeks to it. A sheet without timings still gets no
* buttons.
*/
test("follows the sung line in the strip beside the cover, and seeks from it", async ({
page,
}) => {
lyricSheets.set("song-1", [
{
displayArtist: "Rue Delacour",
displayTitle: "Nocturne",
lang: "eng",
synced: true,
line: Array.from({ length: 40 }, (_, index) => ({
start: index * 5_000,
value: `strip line ${index}`,
})),
},
]);

await page.goto("/playing");
const strip = page.locator(".playing .lyrics");
const column = strip.locator("ol");
await expect(column).toBeVisible();
// Every timed line is a button, named by its words and then the time.
await expect(strip.getByRole("button")).toHaveCount(40);
await expect(
strip.getByRole("button", {
name: "strip line 1, jump to 0:05",
exact: true,
}),
).toBeVisible();
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
await expect.poll(() => column.evaluate((list) => list.scrollTop)).toBe(0);

// The head moves well down the sheet: the sung line is marked, and the strip
// has scrolled itself to it rather than leaving it below its own fold.
for (let press = 0; press < 20; press += 1) {
await page.keyboard.press("ArrowRight");
}
await expect(
strip.getByRole("listitem").filter({ hasText: "strip line 20" }),
).toHaveAttribute("aria-current", "true");
await expect
.poll(() => column.evaluate((list) => list.scrollTop))
.toBeGreaterThan(0);
// The strip scrolls itself and nothing else: the page stays where it was.
expect(await page.evaluate(() => window.scrollY)).toBe(0);

// Pressing a line moves the head to it. The sung line is derived from the
// player's position, so the mark landing on the pressed line is the head
// having moved there — the arrow keys above go through the same `seek`.
await strip
.getByRole("button", { name: "strip line 5, jump to 0:25", exact: true })
.click();
await expect(
strip.getByRole("listitem").filter({ hasText: /^strip line 5$/ }),
).toHaveAttribute("aria-current", "true");
await expect(
strip.getByRole("listitem").filter({ hasText: /^strip line 20$/ }),
).not.toHaveAttribute("aria-current", "true");

await expectNoWcagViolations(page);

// Without timings, the strip offers no seeking, as the lyrics view does not.
lyricSheets.set("song-1", [
{
displayArtist: null,
displayTitle: "Nocturne",
lang: "eng",
synced: false,
line: [
{ value: "the lamps come on along the quay" },
{ value: "and the water keeps the light" },
],
},
]);
await page.reload();
await expect(strip.getByRole("listitem")).toHaveCount(2);
await expect(strip.getByRole("button")).toHaveCount(0);
});

/**
* The immersive view, and the two things that make it a mode rather than a trap.
*
Expand Down Expand Up @@ -7661,6 +7746,24 @@ test("shows this account's listening, period by period", async ({ page }) => {
await expect(columns.getByRole("link", { name: "Art Pop" })).toBeVisible();
await expect(columns.getByRole("link", { name: /Vespertine/ })).toBeVisible();
await expect(page.locator(".stats-moments")).toContainText("Cocoon, ");
// Every track, artist and album entry carries its cover (or the lettered
// placeholder when there is none); a genre has no artwork to show.
const entries = columns.locator(".stats-entry");
await expect(columns.locator(".stats-entry .stats-art")).toHaveCount(
(await entries.count()) - 1,
);
await expect(
columns
.getByRole("link", { name: "Björk", exact: true })
.locator(".stats-art-round"),
).toHaveCount(1);
await expect(
columns.getByRole("link", { name: "Art Pop" }).locator(".stats-art"),
).toHaveCount(0);
// A top track is a button, and a button holds phrasing content only: the
// lettered placeholder inside it must not be a `div`.
await expect(columns.locator("button .cover-fallback")).toHaveCount(2);
await expect(columns.locator("button div")).toHaveCount(0);
await expectNoWcagViolations(page);

await page.getByRole("combobox", { name: "Period" }).selectOption("7d");
Expand Down
7 changes: 5 additions & 2 deletions webapp/src/components/artwork.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -97,13 +97,16 @@ export function Artwork({ artworkId, title, className = "" }: ArtworkProps) {

const classes = `cover ${className}`.trim();
if (src) return <img ref={observe} className={classes} src={src} alt="" />;
// A `span`, not a `div`: a cover can sit inside a `<button>` (a top track in
// the statistics plays when pressed), and a button holds phrasing content
// only. `.cover` sets `display: grid`, so the element changes nothing drawn.
return (
<div
<span
ref={observe}
className={`${classes} cover-fallback`}
aria-hidden="true"
>
<span>{title.slice(0, 1).toLocaleUpperCase()}</span>
</div>
</span>
);
}
Loading
Loading