feat(tickets): multi-sélection dans TicketPicker (#41)
Le TicketPicker permet la sélection multiple de tickets ; SprintManager consomme la sélection multiple. Frontend-pur. QA vert : tsc --noEmit exit 0, vitest 62 fichiers / 620 tests passés. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -241,4 +241,108 @@ describe("TicketPicker (#18)", () => {
|
||||
expect(onClose).not.toHaveBeenCalled();
|
||||
readSpy.mockRestore();
|
||||
});
|
||||
|
||||
describe("multi-select (#41)", () => {
|
||||
it("toggles rows without closing and confirms the resolved array", async () => {
|
||||
const [a, , c] = await seedTrio(ticket);
|
||||
const { onSelect, onClose } = renderPicker(ticket, {
|
||||
selectionMode: "multi",
|
||||
confirmLabel: "Ajouter au sprint",
|
||||
});
|
||||
|
||||
await screen.findByText("Alpha bug");
|
||||
|
||||
const confirm = screen.getByLabelText(
|
||||
"confirm ticket selection",
|
||||
) as HTMLButtonElement;
|
||||
// Empty selection ⇒ confirm disabled, count reads 0.
|
||||
expect(confirm.disabled).toBe(true);
|
||||
expect(screen.getByText("0 sélectionné")).toBeTruthy();
|
||||
|
||||
// Select two rows; the picker stays open and does not fire onSelect yet.
|
||||
fireEvent.click(screen.getByLabelText(`select ticket ${a.ref}`));
|
||||
fireEvent.click(screen.getByLabelText(`select ticket ${c.ref}`));
|
||||
expect(onSelect).not.toHaveBeenCalled();
|
||||
expect(onClose).not.toHaveBeenCalled();
|
||||
|
||||
// Visual state: the toggled rows expose aria-checked.
|
||||
expect(
|
||||
screen.getByLabelText(`select ticket ${a.ref}`).getAttribute("aria-checked"),
|
||||
).toBe("true");
|
||||
expect(screen.getByText("2 sélectionnés")).toBeTruthy();
|
||||
expect(confirm.textContent).toContain("2");
|
||||
expect(confirm.disabled).toBe(false);
|
||||
|
||||
// Toggle one back off ⇒ count drops, still open.
|
||||
fireEvent.click(screen.getByLabelText(`select ticket ${a.ref}`));
|
||||
expect(screen.getByText("1 sélectionné")).toBeTruthy();
|
||||
expect(
|
||||
screen.getByLabelText(`select ticket ${a.ref}`).getAttribute("aria-checked"),
|
||||
).toBe("false");
|
||||
|
||||
// Re-select and confirm ⇒ onSelect receives the resolved array, then close.
|
||||
fireEvent.click(screen.getByLabelText(`select ticket ${a.ref}`));
|
||||
fireEvent.click(confirm);
|
||||
|
||||
await waitFor(() => expect(onSelect).toHaveBeenCalledTimes(1));
|
||||
const results = onSelect.mock.calls[0][0] as TicketPickerResult[];
|
||||
expect(Array.isArray(results)).toBe(true);
|
||||
expect(results.map((r) => r.ref).sort()).toEqual([a.ref, c.ref].sort());
|
||||
expect(results).toContainEqual(
|
||||
expect.objectContaining({ ref: a.ref, id: a.id, number: a.number }),
|
||||
);
|
||||
expect(onClose).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("confirm stays disabled when selection is empty", async () => {
|
||||
await seedTrio(ticket);
|
||||
const { onSelect } = renderPicker(ticket, { selectionMode: "multi" });
|
||||
await screen.findByText("Alpha bug");
|
||||
|
||||
const confirm = screen.getByLabelText(
|
||||
"confirm ticket selection",
|
||||
) as HTMLButtonElement;
|
||||
expect(confirm.disabled).toBe(true);
|
||||
// Clicking a disabled button is a no-op.
|
||||
fireEvent.click(confirm);
|
||||
expect(onSelect).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("respects excludeRefs and facet filters in multi mode", async () => {
|
||||
const [a, b] = await seedTrio(ticket);
|
||||
renderPicker(ticket, {
|
||||
selectionMode: "multi",
|
||||
excludeRefs: [a.ref],
|
||||
});
|
||||
|
||||
// Excluded ref is not selectable.
|
||||
await screen.findByText("Bravo task");
|
||||
expect(screen.queryByLabelText(`select ticket ${a.ref}`)).toBeNull();
|
||||
expect(screen.getByLabelText(`select ticket ${b.ref}`)).toBeTruthy();
|
||||
|
||||
// Facets still narrow the list (Open ⇒ only Alpha, which is excluded ⇒ none).
|
||||
fireEvent.click(screen.getByLabelText("filter status Open"));
|
||||
await waitFor(() =>
|
||||
expect(screen.getByText("No matching tickets.")).toBeTruthy(),
|
||||
);
|
||||
});
|
||||
|
||||
it("closes on Escape and backdrop without confirming", async () => {
|
||||
const [a] = await seedTrio(ticket);
|
||||
const { onSelect, onClose } = renderPicker(ticket, {
|
||||
selectionMode: "multi",
|
||||
});
|
||||
await screen.findByText("Alpha bug");
|
||||
fireEvent.click(screen.getByLabelText(`select ticket ${a.ref}`));
|
||||
|
||||
fireEvent.keyDown(document, { key: "Escape" });
|
||||
expect(onClose).toHaveBeenCalledTimes(1);
|
||||
expect(onSelect).not.toHaveBeenCalled();
|
||||
|
||||
const backdrop = screen.getByRole("dialog").parentElement as HTMLElement;
|
||||
fireEvent.mouseDown(backdrop);
|
||||
expect(onClose).toHaveBeenCalledTimes(2);
|
||||
expect(onSelect).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user