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
58 changes: 58 additions & 0 deletions webapp/e2e/studio-nocturne.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7193,6 +7193,64 @@ test("stops the music when the listener signs out with ReplayGain off", async ({
await expect.poll(playing).toBe(0);
});

/**
* Signing out lets go of the next track too.
*
* The player loads the track after the current one into a second element so
* that skipping starts at once. That element never plays, but it holds its
* source and may still be fetching it, for an account that has just left. Every
* element that was asked to load is recorded, and none may keep a source once
* the listener is signed out.
*/
test("releases the next track's preload when the listener signs out", async ({
page,
}, testInfo) => {
test.skip(testInfo.project.name !== "desktop", "signs out from the sidebar");
await page.addInitScript(() => {
const loaded = new Set<HTMLMediaElement>();
(window as unknown as { __loaded: Set<HTMLMediaElement> }).__loaded =
loaded;
const load = HTMLMediaElement.prototype.load;
HTMLMediaElement.prototype.load = function () {
loaded.add(this);
return load.call(this);
};
});
const audio = silentWav(60);
await page.route("**/api/v2/tracks/*/stream-ticket", (route) => {
const trackId = new URL(route.request().url()).pathname.split("/")[4];
return route.fulfill({
json: {
url: `/api/v2/stream/ticket-${trackId}`,
expires_at: Date.now() + 60_000,
},
});
});
await page.route("**/api/v2/stream/*", (route) =>
route.fulfill({ body: audio, headers: { "content-type": "audio/wav" } }),
);
const sources = () =>
page.evaluate(() =>
[...(window as unknown as { __loaded: Set<HTMLMediaElement> }).__loaded]
.map((element) => element.getAttribute("src"))
.filter((source) => source !== null),
);

await page.goto("/albums/album-2");
await page.getByRole("button", { name: "Play album" }).click();
await expect(page.locator(".player-track strong")).toHaveText("Hidden Place");
// The next track, Cocoon, is being preloaded beside the one playing. Named
// rather than counted: the playing element is asked to load too, and its
// own source alone would let the sign-out run before the preload exists.
await expect.poll(sources).toContain("/api/v2/stream/ticket-song-2");

// Dispatched rather than clicked, as in the cache test: the sidebar's
// footer sits under the fixed player bar at this viewport.
await page.getByRole("button", { name: "Sign out" }).dispatchEvent("click");
await expect(page).toHaveURL(/\/login$/);
await expect.poll(sources).toEqual([]);
});

/**
* "Missing files" (RFC-021 decision 4): what the scans can no longer find, what
* purging each would cost, and a purge that takes two clicks and only what was
Expand Down
8 changes: 8 additions & 0 deletions webapp/src/player/player.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,14 @@ export function PlayerProvider({ children }: { children: ReactNode }) {
element.load();
audio.current = null;
}
// The next track's preload never plays, but it holds its source and may
// still be fetching it for an account that has just left.
const next = preloader.current;
if (next) {
next.removeAttribute("src");
next.load();
preloader.current = null;
}
};
}, []);

Expand Down
Loading