feat(sprints): surface frontend du modèle de sprints
Ticket #10 — volet frontend des sprints. - Domaine et ports frontend étendus au modèle de sprints (domain/index.ts, ports/index.ts). - Adaptateurs : ticket.ts et mock/index.ts câblent les opérations sprints. - UI tickets : useTickets + TicketsPanel exposent le rattachement/gestion des sprints, avec tests Vitest associés. Typecheck / vitest / build verts. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -347,6 +347,71 @@ describe("TicketsView", () => {
|
||||
await waitFor(() => expect(onClose).toHaveBeenCalledTimes(1));
|
||||
});
|
||||
|
||||
it("groups tickets by sprint with a « Sans sprint » bucket (#10)", async () => {
|
||||
// Two ordered sprints + one ticket in each + one loose ticket.
|
||||
ticket._seedSprint(PROJECT_ID, { id: "s1", order: 1, name: "Sprint One" });
|
||||
ticket._seedSprint(PROJECT_ID, { id: "s2", order: 2, name: "Sprint Two" });
|
||||
const t1 = await ticket.create(PROJECT_ID, { title: "In one" });
|
||||
const t2 = await ticket.create(PROJECT_ID, { title: "In two" });
|
||||
await ticket.create(PROJECT_ID, { title: "Loose" });
|
||||
await ticket.setTicketSprint(PROJECT_ID, t1.ref, "s1", t1.version);
|
||||
await ticket.setTicketSprint(PROJECT_ID, t2.ref, "s2", t2.version);
|
||||
|
||||
renderView(ticket, system, agent);
|
||||
|
||||
// Sections appear in sprint `order`, then the "Sans sprint" bucket last.
|
||||
await screen.findByRole("region", { name: "sprint section Sprint One" });
|
||||
const sections = screen
|
||||
.getAllByRole("region")
|
||||
.map((r) => r.getAttribute("aria-label"))
|
||||
.filter((l) => l?.startsWith("sprint section"));
|
||||
expect(sections).toEqual([
|
||||
"sprint section Sprint One",
|
||||
"sprint section Sprint Two",
|
||||
"sprint section Sans sprint",
|
||||
]);
|
||||
|
||||
// The loose ticket lives in the bucket, the others under their sprint.
|
||||
const bucket = screen.getByRole("region", { name: "sprint section Sans sprint" });
|
||||
expect(within(bucket).getByText("Loose")).toBeTruthy();
|
||||
const one = screen.getByRole("region", { name: "sprint section Sprint One" });
|
||||
expect(within(one).getByText("In one")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("assigns a ticket to a sprint via the row selector (#10)", async () => {
|
||||
ticket._seedSprint(PROJECT_ID, { id: "s1", order: 1, name: "Sprint One" });
|
||||
const t = await ticket.create(PROJECT_ID, { title: "Movable" });
|
||||
|
||||
renderView(ticket, system, agent);
|
||||
|
||||
// It starts in the "Sans sprint" bucket.
|
||||
const bucket = await screen.findByRole("region", {
|
||||
name: "sprint section Sans sprint",
|
||||
});
|
||||
expect(within(bucket).getByText("Movable")).toBeTruthy();
|
||||
|
||||
// Pick the sprint in the row selector → assign it.
|
||||
fireEvent.change(screen.getByLabelText(`sprint for ${t.ref}`), {
|
||||
target: { value: "s1" },
|
||||
});
|
||||
|
||||
// The gateway recorded the membership…
|
||||
await waitFor(async () => {
|
||||
const fresh = await ticket.read(PROJECT_ID, t.ref);
|
||||
expect(fresh.sprintId).toBe("s1");
|
||||
});
|
||||
// …and the UI regrouped it under Sprint One, emptying the bucket.
|
||||
await waitFor(() => {
|
||||
const one = screen.getByRole("region", {
|
||||
name: "sprint section Sprint One",
|
||||
});
|
||||
expect(within(one).getByText("Movable")).toBeTruthy();
|
||||
});
|
||||
expect(
|
||||
screen.queryByRole("region", { name: "sprint section Sans sprint" }),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it("copies a delegation context prompt (F7)", async () => {
|
||||
const writeText = stubClipboard();
|
||||
const t = await ticket.create(PROJECT_ID, { title: "Delegate me" });
|
||||
|
||||
Reference in New Issue
Block a user