feat(tickets): attribution d'un sprint à la création via popup SprintPicker (#38)

Nouveau composant SprintPicker permettant de choisir un sprint lors de
la création d'un ticket depuis TicketsPanel ; export ajouté à l'index
des features tickets. Frontend-pur.

QA vert : tsc --noEmit exit 0, vitest 63 fichiers / 634 tests passés.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-12 20:03:41 +02:00
parent a3c0dd410a
commit 0039958b82
5 changed files with 563 additions and 5 deletions

View File

@ -214,6 +214,80 @@ describe("TicketsView", () => {
expect(await screen.findByText("Live created")).toBeTruthy();
});
it("creates a ticket and assigns the sprint chosen via the picker (#38)", async () => {
ticket._seedSprint(PROJECT_ID, { id: "s1", order: 1, name: "Alpha" });
const setSpy = vi.spyOn(ticket, "setTicketSprint");
renderView(ticket, system, agent);
fireEvent.click(await screen.findByRole("button", { name: "New" }));
fireEvent.change(screen.getByLabelText("new ticket title"), {
target: { value: "With sprint" },
});
// Field defaults to "Sans sprint"; open the picker and pick Alpha.
const sprintField = screen.getByLabelText("choose sprint for new ticket");
expect(sprintField.textContent).toContain("Sans sprint");
fireEvent.click(sprintField);
fireEvent.click(await screen.findByLabelText("select sprint Alpha"));
expect(
screen.getByLabelText("choose sprint for new ticket").textContent,
).toContain("Alpha");
fireEvent.click(screen.getByRole("button", { name: "Create" }));
// create → setTicketSprint(projectId, ref, "s1", version).
await waitFor(() => expect(setSpy).toHaveBeenCalledTimes(1));
const call = setSpy.mock.calls[0];
expect(call[0]).toBe(PROJECT_ID);
expect(call[2]).toBe("s1");
await waitFor(async () => {
const fresh = await ticket.read(PROJECT_ID, call[1]);
expect(fresh.sprintId).toBe("s1");
});
});
it("creates a ticket without a sprint — setTicketSprint is not called (#38)", async () => {
const setSpy = vi.spyOn(ticket, "setTicketSprint");
renderView(ticket, system, agent);
fireEvent.click(await screen.findByRole("button", { name: "New" }));
fireEvent.change(screen.getByLabelText("new ticket title"), {
target: { value: "No sprint" },
});
fireEvent.click(screen.getByRole("button", { name: "Create" }));
await waitFor(async () => {
const list = await ticket.list(PROJECT_ID);
expect(list.items.map((i) => i.title)).toContain("No sprint");
});
expect(setSpy).not.toHaveBeenCalled();
});
it("keeps the created ticket and surfaces the error when sprint assignment fails (#38)", async () => {
ticket._seedSprint(PROJECT_ID, { id: "s1", order: 1, name: "Alpha" });
vi.spyOn(ticket, "setTicketSprint").mockRejectedValueOnce({
code: "INTERNAL",
message: "assign boom",
});
renderView(ticket, system, agent);
fireEvent.click(await screen.findByRole("button", { name: "New" }));
fireEvent.change(screen.getByLabelText("new ticket title"), {
target: { value: "Kept anyway" },
});
fireEvent.click(screen.getByLabelText("choose sprint for new ticket"));
fireEvent.click(await screen.findByLabelText("select sprint Alpha"));
fireEvent.click(screen.getByRole("button", { name: "Create" }));
// The ticket was created despite the failed assignment (no data lost)…
await waitFor(async () => {
const list = await ticket.list(PROJECT_ID);
expect(list.items.map((i) => i.title)).toContain("Kept anyway");
});
// …and the assignment failure is surfaced.
expect(await screen.findByText(/assign boom/)).toBeTruthy();
});
it("multi-selects status/priority facets (OR intra, AND inter) and clears (#12)", async () => {
const listSpy = vi.spyOn(ticket, "list");
await ticket.create(PROJECT_ID, {