feat(design): a channel's and a Plex playlist's one action list (C1) — the row's "…", Delete + undo, an action toast's 8 s floor (E28 S3e2)
- Channels: the row's full-history, hide and unsubscribe buttons become its "…" at the row end (useChannelActions: open, pin, YouTube, copy name / link, full history or the admin reset, hide with an undo toast, unsubscribe behind its danger confirm); the card layout docks the "…" beside the name (ChannelLayoutGrid moreKey). The admin reset's confirm now names the reset. - Plex playlist page: a title, a show group and a season each open the page's one row menu (a title's is its usePlexCardActions list plus the removal); no X at rest; removals take an undo toast whose Undo puts every row back at its own index in one request (bulk add `positions`); j/k walk the visible rows, Delete removes the walked one; rename and delete in the header's "…". - notify: a toast with an action lives at least 8 s (the owner's call), whatever the toast setting.
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
// (E9 review). The runtime sums the WHOLE list, an unknown duration counts as nothing, and a list
|
||||
// nothing sums for says no runtime at all.
|
||||
import { describe, it, expect, vi, beforeAll, afterEach } from "vitest";
|
||||
import { render, cleanup, screen, fireEvent } from "@testing-library/react";
|
||||
import { render, cleanup, screen, fireEvent, waitFor } from "@testing-library/react";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
|
||||
vi.mock("react-i18next", async (importOriginal) => ({
|
||||
@@ -60,6 +60,20 @@ async function meta(items: PlexCard[]) {
|
||||
).textContent;
|
||||
}
|
||||
|
||||
/** Pick `label` from the menu `open` opens. The surface is visibility:hidden in jsdom (floating-ui
|
||||
* never positions it), so the row is matched on its text. */
|
||||
async function pickFromMenu(open: () => void, label: string) {
|
||||
open();
|
||||
const row = await waitFor(() => {
|
||||
const found = screen
|
||||
.getAllByRole("menuitem", { hidden: true })
|
||||
.find((b) => b.textContent === label);
|
||||
expect(found).toBeTruthy();
|
||||
return found!;
|
||||
});
|
||||
fireEvent.click(row);
|
||||
}
|
||||
|
||||
describe("PlexPlaylistView · the header's meta line", () => {
|
||||
it("sums the known runtimes and counts an unknown one as nothing", async () => {
|
||||
expect(await meta([movie("1", 5400), movie("2"), movie("3", 1800)])).toBe(
|
||||
@@ -80,8 +94,11 @@ describe("PlexPlaylistView · rename", () => {
|
||||
const write = vi.spyOn(api, "plexRenamePlaylist").mockResolvedValue({} as never);
|
||||
await meta([movie("1", 5400)]);
|
||||
for (const value of [null, " "]) {
|
||||
fireEvent.click(screen.getByRole("button", { name: "plex.playlist.rename" }));
|
||||
const field = screen.getByRole("textbox", { name: "plex.playlist.rename" });
|
||||
await pickFromMenu(
|
||||
() => fireEvent.click(screen.getByRole("button", { name: "plex.playlist.menu" })),
|
||||
"plex.playlist.rename",
|
||||
);
|
||||
const field = await screen.findByRole("textbox", { name: "plex.playlist.rename" });
|
||||
if (value !== null) fireEvent.change(field, { target: { value } });
|
||||
fireEvent.keyDown(field, { key: "Enter" });
|
||||
await screen.findByRole("heading", { name: "Films" });
|
||||
|
||||
Reference in New Issue
Block a user