feat(tickets): conservation des tickets en sprint, affichage du statut & filtres (#37)

Les tickets restent rattachés à leur sprint ; SprintManager affiche le
statut et propose des filtres via useTicketSearch. Frontend / read-query.

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

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-12 19:55:54 +02:00
parent 338b8707de
commit 1c28754b58
3 changed files with 184 additions and 11 deletions

View File

@ -955,4 +955,84 @@ describe("SprintManager (#11)", () => {
expect(fresh.sprintId).toBeNull();
});
});
/** Seeds sprint "Holder" with one open and one closed ticket assigned. */
async function seedSprintWithOpenAndClosed(): Promise<void> {
ticket._seedSprint(PROJECT_ID, { id: "s1", order: 1, name: "Holder" });
const openT = await ticket.create(PROJECT_ID, {
title: "Open member",
priority: "high",
});
await ticket.setTicketSprint(PROJECT_ID, openT.ref, "s1", openT.version);
const closedT = await ticket.create(PROJECT_ID, {
title: "Closed member",
priority: "low",
});
const upd = await ticket.update(PROJECT_ID, closedT.ref, {
status: "closed",
expectedVersion: closedT.version,
});
await ticket.setTicketSprint(PROJECT_ID, closedT.ref, "s1", upd.version);
}
it("keeps closed tickets visible in the sprint with status/priority badges (#37)", async () => {
await seedSprintWithOpenAndClosed();
const dialog = await openManager();
const row = await within(dialog).findByLabelText("sprint row Holder");
// Both the open and the closed ticket are listed (closed is NOT hidden).
expect(within(row).getByText("Open member")).toBeTruthy();
expect(within(row).getByText("Closed member")).toBeTruthy();
// Each row carries its status + priority badge (scoped to the sprint row so
// the facet-bar labels don't collide).
expect(within(row).getByText("Closed")).toBeTruthy();
expect(within(row).getByText("Open")).toBeTruthy();
expect(within(row).getByText("High")).toBeTruthy();
expect(within(row).getByText("Low")).toBeTruthy();
});
it("filters the sprint tickets by status via its own facets bar (#37)", async () => {
await seedSprintWithOpenAndClosed();
const dialog = await openManager();
await within(dialog).findByText("Open member");
expect(within(dialog).getByText("Closed member")).toBeTruthy();
// Constrain the sprint view to Closed only → the open ticket drops out.
fireEvent.click(within(dialog).getByLabelText("filter status Closed"));
await waitFor(() =>
expect(within(dialog).queryByText("Open member")).toBeNull(),
);
expect(within(dialog).getByText("Closed member")).toBeTruthy();
// Clearing the facet brings the open ticket back.
fireEvent.click(within(dialog).getByLabelText("filter status Closed"));
await waitFor(() =>
expect(within(dialog).getByText("Open member")).toBeTruthy(),
);
});
it("sprint ticket view is independent of the main panel filter (#37)", async () => {
await seedSprintWithOpenAndClosed();
renderView(ticket, system, agent);
await waitFor(() =>
expect(
screen.getByRole("button", { name: "manage sprints" }),
).toBeTruthy(),
);
// Constrain the MAIN Tickets panel to Open only → the closed ticket leaves
// the main list (there is a single main facets bar on screen at this point).
fireEvent.click(screen.getByLabelText("filter status Open"));
await waitFor(() => expect(screen.queryByText("Closed member")).toBeNull());
// Open the sprint manager: its own query ignores the main filter, so the
// closed ticket is still shown inside its sprint.
fireEvent.click(screen.getByRole("button", { name: "manage sprints" }));
const dialog = await screen.findByRole("dialog", { name: "manage sprints" });
const row = await within(dialog).findByLabelText("sprint row Holder");
expect(await within(row).findByText("Closed member")).toBeTruthy();
expect(within(row).getByText("Open member")).toBeTruthy();
});
});