A row's grip carries it, or every selected row with a selected one, onto another list, under a sort or a filter too, and the toast undoes. The rail and the page are separate React trees, so the page reads the rail by what lies under the pointer. An unpinned rail peeks by the drag's own hover and scrolls at its edges. Many rows dropped inside the list stay; a key still moves its own row.
120 lines
3.8 KiB
TypeScript
120 lines
3.8 KiB
TypeScript
// @vitest-environment jsdom
|
|
//
|
|
// The rail's rows as targets of a drag that starts in another tree: which ones are, and the one lit up.
|
|
import { describe, it, expect, vi, afterEach, beforeAll } from "vitest";
|
|
import { render, screen, cleanup, act } from "@testing-library/react";
|
|
import { FloatingTree } from "@floating-ui/react";
|
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
import type { ReactNode } from "react";
|
|
|
|
vi.mock("../../lib/useRail", () => ({
|
|
useRail: () => ({
|
|
layout: { order: ["manage", "list"], collapsed: {}, hidden: {} },
|
|
setLayout: () => {},
|
|
collapsed: false,
|
|
onToggleCollapse: () => {},
|
|
}),
|
|
}));
|
|
vi.mock("react-i18next", async (importOriginal) => {
|
|
const actual = await importOriginal<typeof import("react-i18next")>();
|
|
return { ...actual, useTranslation: () => ({ t: (k: string) => k, i18n: { language: "en" } }) };
|
|
});
|
|
vi.mock("./playlistsContext", () => ({
|
|
usePlaylists: () => ({ search: "", selectedId: 1 }),
|
|
usePlaylistsActions: () => ({ setSelectedId: vi.fn() }),
|
|
}));
|
|
vi.mock("../../lib/useMe", () => ({ useMe: () => ({ can_write: true }) }));
|
|
vi.mock("../../features/comfort/comfortPin", () => ({
|
|
useComfortPin: () => ({ item: () => [] }),
|
|
pinPlaylist: (id: number) => id,
|
|
}));
|
|
vi.mock("../../components/ui/confirmContext", () => ({
|
|
useChoice: () => async () => null,
|
|
useConfirm: () => async () => false,
|
|
}));
|
|
vi.mock("../../components/ui/FilterRail", () => ({
|
|
default: ({ items }: { items: { id: string; body: ReactNode }[] }) => (
|
|
<>
|
|
{items.map((i) => (
|
|
<div key={i.id}>{i.body}</div>
|
|
))}
|
|
</>
|
|
),
|
|
}));
|
|
|
|
import PlaylistsRail from "./PlaylistsRail";
|
|
import { api, type Playlist } from "../../lib/api";
|
|
import { startRailDrag, stopRailDrag } from "./railDrop";
|
|
|
|
beforeAll(() => {
|
|
class Stub {
|
|
observe() {}
|
|
unobserve() {}
|
|
disconnect() {}
|
|
takeRecords() {
|
|
return [];
|
|
}
|
|
}
|
|
globalThis.ResizeObserver ??= Stub as unknown as typeof ResizeObserver;
|
|
globalThis.IntersectionObserver ??= Stub as unknown as typeof IntersectionObserver;
|
|
Element.prototype.scrollIntoView ??= () => {};
|
|
});
|
|
|
|
afterEach(() => {
|
|
stopRailDrag();
|
|
cleanup();
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
const pl = (id: number, name: string) =>
|
|
({
|
|
id,
|
|
name,
|
|
kind: "user",
|
|
source: "local",
|
|
yt_playlist_id: null,
|
|
item_count: 1,
|
|
total_duration_seconds: 0,
|
|
cover_thumbnail: null,
|
|
dirty: false,
|
|
}) as unknown as Playlist;
|
|
|
|
async function mount() {
|
|
vi.spyOn(api, "playlists").mockResolvedValue([pl(1, "Open"), pl(2, "Other")] as never);
|
|
const qc = new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
|
render(
|
|
<QueryClientProvider client={qc}>
|
|
<FloatingTree>
|
|
<PlaylistsRail />
|
|
</FloatingTree>
|
|
</QueryClientProvider>,
|
|
);
|
|
const wrap = async (name: string) =>
|
|
(await screen.findByText(name)).closest("[class*='group']") as HTMLElement;
|
|
return { open: await wrap("Open"), other: await wrap("Other") };
|
|
}
|
|
|
|
describe("PlaylistsRail — a row as a drop target", () => {
|
|
it("offers every row but the open list", async () => {
|
|
const { open, other } = await mount();
|
|
expect(other.getAttribute("data-playlist-drop")).toBe("2");
|
|
expect(open.hasAttribute("data-playlist-drop")).toBe(false);
|
|
expect(open.closest("[data-playlist-drop-area]")).toBeTruthy();
|
|
});
|
|
|
|
it("lights up the row under the drag, and only while it is", async () => {
|
|
const { open, other } = await mount();
|
|
document.elementsFromPoint = () => [other];
|
|
expect(other.className).not.toContain("ring-2");
|
|
act(() => {
|
|
startRailDrag(0, 0);
|
|
});
|
|
expect(other.className).toContain("ring-2");
|
|
expect(open.className).not.toContain("ring-2");
|
|
act(() => {
|
|
stopRailDrag();
|
|
});
|
|
expect(other.className).not.toContain("ring-2");
|
|
});
|
|
});
|