test: a list key waits for the list's registration, not only its rows
gate / frontend (push) Successful in 6m5s
gate / backend (push) Successful in 7m10s

The server gate's frontend job went red on 0.96.0: PlexPlaylistView's j/Delete
test pressed j after the rows painted but before useListNav's passive effect
registered the list, so the key did nothing on the slower runner. Every test
that presses a list key after an async load now waits for [data-nav-list] on
its list first (PlexPlaylistView, Playlists, Home, PlexDiscover). Checked
against a 30 ms delayed registration: the old tests fail, the new ones pass.
This commit is contained in:
2026-09-27 23:01:57 +02:00
parent ffcea0cb07
commit 0f9ed7cdbf
4 changed files with 20 additions and 7 deletions
@@ -424,6 +424,10 @@ describe("Playlists — removing a row, and undoing it", () => {
.map((c) => c[0] as { message: string; action?: { label: string; onClick: () => void } })
.filter((n) => n.message?.startsWith("playlists.removedToast"))
.pop()!;
// The list's keys are live once its effect has registered it (`data-nav-list`), a passive effect that
// can run AFTER the rows are on screen: a key pressed on a slow machine in between did nothing (CI flake).
const keysLiveOn = (el: Element) =>
waitFor(() => expect(el.closest("[data-nav-list]")).not.toBeNull());
const pressKey = (key: string) =>
act(() => {
(document.activeElement ?? document.body).dispatchEvent(
@@ -497,7 +501,7 @@ describe("Playlists — removing a row, and undoing it", () => {
const lists = { 1: [vid("A", 0), vid("B", 0), vid("C", 0)] };
serverRemoves(lists);
mount(lists);
await screen.findByRole("button", { name: "card.play — Video C" });
await keysLiveOn(await screen.findByRole("button", { name: "card.play — Video C" }));
pressKey("j");
pressKey("j");
expect(document.activeElement?.textContent).toContain("Video B");
@@ -511,7 +515,7 @@ describe("Playlists — removing a row, and undoing it", () => {
vi.spyOn(api, "removeFromPlaylist").mockResolvedValue(undefined as never);
h.selectedId = WATCH_LATER;
mount({ [WATCH_LATER]: [vid("A", 0), vid("B", 0)] });
await screen.findByRole("button", { name: "card.play — Video B" });
await keysLiveOn(await screen.findByRole("button", { name: "card.play — Video B" }));
pressKey("j");
pressKey("Delete");
await act(async () => void (await new Promise((r) => setTimeout(r, 20))));
@@ -617,7 +621,9 @@ describe("Playlists · a narrow table", () => {
describe("Playlists — the rows are a keyboard list", () => {
it("walks the rows with j and plays the focused one on Enter", async () => {
mount({ 1: [vid("A", 0), vid("B", 0)] });
await screen.findByRole("button", { name: "card.play — Video B" });
const row = await screen.findByRole("button", { name: "card.play — Video B" });
// The keys are live once the list's effect registers it, which can land after the rows paint.
await waitFor(() => expect(row.closest("[data-nav-list]")).not.toBeNull());
const press = (key: string) =>
act(() => {
(document.activeElement ?? document.body).dispatchEvent(