feat(sprints): interface de création et gestion des sprints (frontend)
Ticket #11 — surface UI de gestion des sprints (frontend pur). - Nouveau composant SprintManager : création (nom optionnel), édition et gestion du cycle de vie d'un sprint, contrôles accessibles (pas de DnD). - useTickets étendu aux opérations de gestion de sprint ; ports, adaptateurs (ticket.ts, mock/index.ts) et TicketsPanel câblés en conséquence. - Tests Vitest associés (tickets.test.tsx). Typecheck / vitest (497+ tests) / build verts. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -428,3 +428,155 @@ describe("TicketsView", () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("SprintManager (#11)", () => {
|
||||
let system: MockSystemGateway;
|
||||
let ticket: MockTicketGateway;
|
||||
let agent: MockAgentGateway;
|
||||
|
||||
beforeEach(() => {
|
||||
system = new MockSystemGateway();
|
||||
ticket = new MockTicketGateway(system);
|
||||
agent = new MockAgentGateway();
|
||||
});
|
||||
|
||||
/** Renders the view and opens the sprint-management overlay. */
|
||||
async function openManager(): Promise<HTMLElement> {
|
||||
renderView(ticket, system, agent);
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
screen.getByRole("button", { name: "manage sprints" }),
|
||||
).toBeTruthy(),
|
||||
);
|
||||
fireEvent.click(screen.getByRole("button", { name: "manage sprints" }));
|
||||
return screen.findByRole("dialog", { name: "manage sprints" });
|
||||
}
|
||||
|
||||
it("creates a sprint", async () => {
|
||||
const dialog = await openManager();
|
||||
|
||||
fireEvent.change(within(dialog).getByLabelText("new sprint name"), {
|
||||
target: { value: "Alpha" },
|
||||
});
|
||||
fireEvent.click(within(dialog).getByText("Create sprint"));
|
||||
|
||||
await waitFor(async () => {
|
||||
const sprints = await ticket.listSprints(PROJECT_ID);
|
||||
expect(sprints.map((s) => s.name)).toEqual(["Alpha"]);
|
||||
});
|
||||
expect(
|
||||
await within(dialog).findByLabelText("sprint row Alpha"),
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
it("creates a sprint with a default positional name when the name is empty", async () => {
|
||||
const dialog = await openManager();
|
||||
|
||||
// Leave the name field empty and create → default "Sprint N" (here N=1).
|
||||
expect(
|
||||
(within(dialog).getByLabelText("new sprint name") as HTMLInputElement)
|
||||
.value,
|
||||
).toBe("");
|
||||
fireEvent.click(within(dialog).getByText("Create sprint"));
|
||||
|
||||
await waitFor(async () => {
|
||||
const sprints = await ticket.listSprints(PROJECT_ID);
|
||||
expect(sprints.map((s) => s.name)).toEqual(["Sprint 1"]);
|
||||
});
|
||||
expect(
|
||||
await within(dialog).findByLabelText("sprint row Sprint 1"),
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
it("renames a sprint", async () => {
|
||||
ticket._seedSprint(PROJECT_ID, { id: "s1", order: 1, name: "Old" });
|
||||
const dialog = await openManager();
|
||||
|
||||
fireEvent.change(
|
||||
await within(dialog).findByLabelText("rename sprint Old"),
|
||||
{ target: { value: "New" } },
|
||||
);
|
||||
fireEvent.click(within(dialog).getByLabelText("save sprint Old"));
|
||||
|
||||
await waitFor(async () => {
|
||||
const [s] = await ticket.listSprints(PROJECT_ID);
|
||||
expect(s.name).toBe("New");
|
||||
});
|
||||
});
|
||||
|
||||
it("reorders sprints via the accessible down button", async () => {
|
||||
ticket._seedSprint(PROJECT_ID, { id: "s1", order: 1, name: "One" });
|
||||
ticket._seedSprint(PROJECT_ID, { id: "s2", order: 2, name: "Two" });
|
||||
const dialog = await openManager();
|
||||
|
||||
fireEvent.click(
|
||||
await within(dialog).findByLabelText("move sprint One down"),
|
||||
);
|
||||
|
||||
await waitFor(async () => {
|
||||
const sprints = await ticket.listSprints(PROJECT_ID);
|
||||
// "Two" is now first (order 1), "One" second.
|
||||
expect(sprints.map((s) => s.name)).toEqual(["Two", "One"]);
|
||||
});
|
||||
});
|
||||
|
||||
it("deletes a sprint and unassigns (does not delete) its tickets", async () => {
|
||||
ticket._seedSprint(PROJECT_ID, { id: "s1", order: 1, name: "Doomed" });
|
||||
const t = await ticket.create(PROJECT_ID, { title: "Kept" });
|
||||
await ticket.setTicketSprint(PROJECT_ID, t.ref, "s1", t.version);
|
||||
|
||||
const dialog = await openManager();
|
||||
|
||||
fireEvent.click(
|
||||
await within(dialog).findByLabelText("delete sprint Doomed"),
|
||||
);
|
||||
// The confirmation explains tickets are unassigned, not deleted.
|
||||
const confirm = await screen.findByRole("dialog", {
|
||||
name: "delete sprint confirmation",
|
||||
});
|
||||
expect(confirm.textContent).toMatch(/ne seront pas supprimés/i);
|
||||
fireEvent.click(within(confirm).getByLabelText("confirm delete sprint"));
|
||||
|
||||
await waitFor(async () => {
|
||||
expect(await ticket.listSprints(PROJECT_ID)).toHaveLength(0);
|
||||
});
|
||||
// The ticket still exists, just unassigned.
|
||||
const fresh = await ticket.read(PROJECT_ID, t.ref);
|
||||
expect(fresh.sprintId).toBeNull();
|
||||
expect(fresh.title).toBe("Kept");
|
||||
});
|
||||
|
||||
it("adds a backlog ticket to a sprint via the add selector", async () => {
|
||||
ticket._seedSprint(PROJECT_ID, { id: "s1", order: 1, name: "Target" });
|
||||
const t = await ticket.create(PROJECT_ID, { title: "Backlog item" });
|
||||
|
||||
const dialog = await openManager();
|
||||
|
||||
fireEvent.change(
|
||||
await within(dialog).findByLabelText("add ticket to sprint Target"),
|
||||
{ target: { value: t.ref } },
|
||||
);
|
||||
|
||||
await waitFor(async () => {
|
||||
const fresh = await ticket.read(PROJECT_ID, t.ref);
|
||||
expect(fresh.sprintId).toBe("s1");
|
||||
});
|
||||
});
|
||||
|
||||
it("removes a ticket from a sprint", async () => {
|
||||
ticket._seedSprint(PROJECT_ID, { id: "s1", order: 1, name: "Holder" });
|
||||
const t = await ticket.create(PROJECT_ID, { title: "Member" });
|
||||
await ticket.setTicketSprint(PROJECT_ID, t.ref, "s1", t.version);
|
||||
|
||||
const dialog = await openManager();
|
||||
|
||||
fireEvent.click(
|
||||
await within(dialog).findByLabelText(`remove ${t.ref} from sprint`),
|
||||
);
|
||||
|
||||
await waitFor(async () => {
|
||||
const fresh = await ticket.read(PROJECT_ID, t.ref);
|
||||
expect(fresh.sprintId).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user