fix(ui): one membership menu for playlists, collections and tags
Three hand-copied panels had drifted: Add to playlist said a failed load as 'no playlists yet'. ui/MembershipMenu now holds them: a loading line and an error with Retry instead of a blank or an empty list, the up/down arrows step from the name field into rows that arrived after the menu opened, and the panels opened from a menu carry their own accessible name. A write that landed flips its tick at once, so a second tap before the refetch undoes it instead of repeating it; a collection edit also refreshes a show's own page.
This commit is contained in:
@@ -113,7 +113,7 @@ function setup(
|
||||
onToggleWatched: vi.fn(),
|
||||
onRestart: vi.fn(),
|
||||
};
|
||||
const qc = new QueryClient();
|
||||
const qc = new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
||||
if (admin) qc.setQueryData(qk.me(), { role: "admin" });
|
||||
render(
|
||||
<QueryClientProvider client={qc}>
|
||||
@@ -122,7 +122,7 @@ function setup(
|
||||
</FloatingTree>
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
return { handlers };
|
||||
return { handlers, qc };
|
||||
}
|
||||
// The card root holds the menu's trigger props (right-click anywhere on it).
|
||||
const rightClick = () =>
|
||||
@@ -288,4 +288,100 @@ describe("a Plex title's menu — Add to collection", () => {
|
||||
fireEvent.keyDown(name, { key: "Enter" });
|
||||
await vi.waitFor(() => expect(create).toHaveBeenCalledWith("", "Rainy days", "m1"));
|
||||
});
|
||||
|
||||
const checkRow = async (name: string) =>
|
||||
vi.waitFor(() => {
|
||||
const found = screen
|
||||
.queryAllByRole("menuitemcheckbox", { hidden: true })
|
||||
.find((b) => b.textContent === name);
|
||||
expect(found).toBeDefined();
|
||||
return found!;
|
||||
});
|
||||
const idle = () =>
|
||||
vi.waitFor(() =>
|
||||
expect(document.querySelector("[aria-busy]")?.getAttribute("aria-busy")).toBe("false"),
|
||||
);
|
||||
|
||||
it("flips the tick as soon as a write lands, so the next tap undoes it before the list refetches", async () => {
|
||||
const list = vi.spyOn(api, "plexCollectionsFor");
|
||||
list.mockResolvedValueOnce({ collections: [coll("cB", "Beta", false)] });
|
||||
// The refetch after the write never answers: the tick must not wait for it.
|
||||
list.mockImplementation(() => new Promise(() => {}));
|
||||
const add = vi.spyOn(api, "plexCollectionAddItem").mockResolvedValue({} as PlexCollection);
|
||||
const remove = vi
|
||||
.spyOn(api, "plexCollectionRemoveItem")
|
||||
.mockResolvedValue({} as PlexCollection);
|
||||
setup(card({}), {}, { admin: true });
|
||||
rightClick();
|
||||
fireEvent.click(item("plex.collectionAdd.row"));
|
||||
fireEvent.click(await checkRow("Beta"));
|
||||
await vi.waitFor(() => expect(add).toHaveBeenCalledWith("cB", "m1"));
|
||||
await idle();
|
||||
expect((await checkRow("Beta")).getAttribute("aria-checked")).toBe("true");
|
||||
fireEvent.click(await checkRow("Beta"));
|
||||
await vi.waitFor(() => expect(remove).toHaveBeenCalledWith("cB", "m1"));
|
||||
expect(add).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("refreshes a show's own page too, whose collections come with the show", async () => {
|
||||
vi.spyOn(api, "plexCollectionsFor").mockResolvedValue({
|
||||
collections: [coll("cB", "Beta", false)],
|
||||
});
|
||||
vi.spyOn(api, "plexCollectionAddItem").mockResolvedValue({} as PlexCollection);
|
||||
const { qc } = setup(card({ type: "show" }), {}, { admin: true });
|
||||
const invalidate = vi.spyOn(qc, "invalidateQueries");
|
||||
rightClick();
|
||||
fireEvent.click(item("plex.collectionAdd.row"));
|
||||
fireEvent.click(await checkRow("Beta"));
|
||||
await idle();
|
||||
expect(invalidate).toHaveBeenCalledWith({ queryKey: qk.plexShow("m1") });
|
||||
});
|
||||
|
||||
it("is a named menu that says it is loading, not that there are no collections", async () => {
|
||||
vi.spyOn(api, "plexCollectionsFor").mockImplementation(() => new Promise(() => {}));
|
||||
setup(card({}), {}, { admin: true });
|
||||
rightClick();
|
||||
fireEvent.click(item("plex.collectionAdd.row"));
|
||||
// jsdom computes no name for the unpositioned (visibility: hidden) surface: read what names it.
|
||||
const menu = await vi.waitFor(() => {
|
||||
const found = document.querySelector('[role="menu"][aria-label="plex.collectionAdd.title"]');
|
||||
expect(found).not.toBeNull();
|
||||
return found!;
|
||||
});
|
||||
expect(menu.hasAttribute("aria-labelledby")).toBe(false);
|
||||
expect(screen.getByText("common.loading")).toBeTruthy();
|
||||
expect(screen.queryByText("plex.collectionAdd.none")).toBeNull();
|
||||
});
|
||||
|
||||
it("says a failed load as one, with a retry — not as no collections", async () => {
|
||||
const list = vi.spyOn(api, "plexCollectionsFor").mockRejectedValue(new Error("down"));
|
||||
setup(card({}), {}, { admin: true });
|
||||
rightClick();
|
||||
fireEvent.click(item("plex.collectionAdd.row"));
|
||||
const alert = await screen.findByRole("alert", { hidden: true });
|
||||
expect(alert.textContent).toContain("plex.collectionAdd.loadFailed");
|
||||
expect(screen.queryByText("plex.collectionAdd.none")).toBeNull();
|
||||
list.mockResolvedValue({ collections: [coll("cB", "Beta", false)] });
|
||||
fireEvent.click(await screen.findByText("common.retry"));
|
||||
await checkRow("Beta");
|
||||
});
|
||||
|
||||
it("steps from the name field into the rows with the arrows — rows that arrived after it opened", async () => {
|
||||
let answer: (v: { collections: (PlexCollection & { contains: boolean })[] }) => void = () => {};
|
||||
vi.spyOn(api, "plexCollectionsFor").mockImplementation(
|
||||
() => new Promise((resolve) => (answer = resolve)),
|
||||
);
|
||||
setup(card({}), {}, { admin: true });
|
||||
rightClick();
|
||||
fireEvent.click(item("plex.collectionAdd.row"));
|
||||
const name = await screen.findByLabelText("plex.collection.namePlaceholder");
|
||||
name.focus();
|
||||
answer({ collections: [coll("cA", "Alpha", true), coll("cB", "Beta", false)] });
|
||||
const alpha = await checkRow("Alpha");
|
||||
fireEvent.keyDown(name, { key: "ArrowDown" });
|
||||
expect(document.activeElement).toBe(alpha);
|
||||
name.focus();
|
||||
fireEvent.keyDown(name, { key: "ArrowUp" });
|
||||
expect(document.activeElement).toBe(await checkRow("Beta"));
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user