/** * Multi-select behavior of MediaPickerModal (`multiple` + `onSelectMany`), * used by the gallery block's "Add Images" flow. The existing * `tests/components/MediaPickerModal.test.tsx` suite covers single-select — * this file guards the multi-select accumulation/toggle/order semantics * added for galleries (#1436), plus a regression check that single-select * mode still works. */ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import * as React from "react"; import { describe, it, expect, vi, beforeEach } from "vitest"; import { MediaPickerModal } from "../src/components/MediaPickerModal"; import { render } from "./utils/render.tsx"; vi.mock("../src/lib/api", async () => { const actual = await vi.importActual("../src/lib/api"); return { ...actual, fetchMediaList: vi.fn().mockResolvedValue({ items: [ { id: "m1", filename: "photo.jpg", mimeType: "image/jpeg", url: "/media/photo.jpg", size: 1024, width: 800, height: 600, createdAt: "2024-01-01", }, { id: "m2", filename: "landscape.png", mimeType: "image/png", url: "/media/landscape.png", size: 2048, width: 1200, height: 800, createdAt: "2024-01-02", }, { id: "m3", filename: "portrait.png", mimeType: "image/png", url: "/media/portrait.png", size: 3072, width: 600, height: 900, createdAt: "2024-01-03", }, ], }), fetchMediaProviders: vi.fn().mockResolvedValue([]), fetchProviderMedia: vi.fn().mockResolvedValue({ items: [] }), uploadMedia: vi.fn().mockResolvedValue({ id: "m4", filename: "new.jpg" }), uploadToProvider: vi.fn().mockResolvedValue({}), updateMedia: vi.fn().mockResolvedValue({}), }; }); function QueryWrapper({ children }: { children: React.ReactNode }) { const qc = new QueryClient({ defaultOptions: { queries: { retry: false }, mutations: { retry: false } }, }); return {children}; } function renderModal(props: Partial> = {}) { const defaultProps: React.ComponentProps = { open: true, onOpenChange: vi.fn(), onSelect: vi.fn(), ...props, }; return render( , ); } function optionButton(screen: Awaited>, name: string) { const option = screen.getByRole("option", { name }); return option.element().querySelector("button")!; } function footerInsertButton(): HTMLButtonElement { const allInsertBtns = [...document.querySelectorAll("button")]; const insertBtns = allInsertBtns.filter((b) => b.textContent?.trim() === "Insert"); return insertBtns.at(-1) as HTMLButtonElement; } describe("MediaPickerModal — multi-select", () => { beforeEach(() => { vi.clearAllMocks(); }); it("accumulates clicked items in click order", async () => { const onSelectMany = vi.fn(); const screen = await renderModal({ multiple: true, onSelectMany }); await expect.element(screen.getByRole("option", { name: "photo.jpg" })).toBeInTheDocument(); // Click landscape, then photo, then portrait — order should be preserved // as the gallery insertion order, not the grid's display order. optionButton(screen, "landscape.png").click(); await expect .element(screen.getByRole("option", { name: "landscape.png" })) .toHaveAttribute("aria-selected", "true"); optionButton(screen, "photo.jpg").click(); await expect .element(screen.getByRole("option", { name: "photo.jpg" })) .toHaveAttribute("aria-selected", "true"); optionButton(screen, "portrait.png").click(); await expect .element(screen.getByRole("option", { name: "portrait.png" })) .toHaveAttribute("aria-selected", "true"); await vi.waitFor(() => { expect(footerInsertButton().disabled).toBe(false); }); footerInsertButton().click(); expect(onSelectMany).toHaveBeenCalledTimes(1); const selected = onSelectMany.mock.calls[0]![0] as Array<{ filename: string }>; expect(selected.map((item) => item.filename)).toEqual([ "landscape.png", "photo.jpg", "portrait.png", ]); }); it("clicking a selected item deselects it", async () => { const onSelectMany = vi.fn(); const screen = await renderModal({ multiple: true, onSelectMany }); await expect.element(screen.getByRole("option", { name: "photo.jpg" })).toBeInTheDocument(); optionButton(screen, "photo.jpg").click(); await expect .element(screen.getByRole("option", { name: "photo.jpg" })) .toHaveAttribute("aria-selected", "true"); optionButton(screen, "landscape.png").click(); await expect .element(screen.getByRole("option", { name: "landscape.png" })) .toHaveAttribute("aria-selected", "true"); // Deselect the first click optionButton(screen, "photo.jpg").click(); await expect .element(screen.getByRole("option", { name: "photo.jpg" })) .toHaveAttribute("aria-selected", "false"); await vi.waitFor(() => { expect(footerInsertButton().disabled).toBe(false); }); footerInsertButton().click(); expect(onSelectMany).toHaveBeenCalledTimes(1); const selected = onSelectMany.mock.calls[0]![0] as Array<{ filename: string }>; expect(selected.map((item) => item.filename)).toEqual(["landscape.png"]); }); it("Insert button is disabled at zero selections", async () => { await renderModal({ multiple: true }); await vi.waitFor(() => { expect(footerInsertButton().disabled).toBe(true); }); }); it("Insert button becomes enabled after one selection and disabled again after deselecting it", async () => { const screen = await renderModal({ multiple: true }); await expect.element(screen.getByRole("option", { name: "photo.jpg" })).toBeInTheDocument(); optionButton(screen, "photo.jpg").click(); await vi.waitFor(() => { expect(footerInsertButton().disabled).toBe(false); }); optionButton(screen, "photo.jpg").click(); await vi.waitFor(() => { expect(footerInsertButton().disabled).toBe(true); }); }); it("regression: single-select mode still calls onSelect with one item", async () => { const onSelect = vi.fn(); const onSelectMany = vi.fn(); const screen = await renderModal({ onSelect, onSelectMany }); await expect.element(screen.getByRole("option", { name: "photo.jpg" })).toBeInTheDocument(); optionButton(screen, "photo.jpg").click(); await expect .element(screen.getByRole("option", { name: "photo.jpg" })) .toHaveAttribute("aria-selected", "true"); await vi.waitFor(() => { expect(footerInsertButton().disabled).toBe(false); }); footerInsertButton().click(); expect(onSelect).toHaveBeenCalledTimes(1); expect(onSelect).toHaveBeenCalledWith( expect.objectContaining({ id: "m1", filename: "photo.jpg" }), ); expect(onSelectMany).not.toHaveBeenCalled(); }); });