kanban-dev-webapp/src/lib/roadmap/bar-math.test.ts
Dave 2b01d0e837 feat(plan): bar-math helpers + open reschedule to anyone signed in
Pure helpers for the Gantt's drag/double-click math:

- fractionToDate(fraction, range): what ISO date a fractional
  position in the chart represents, snapped to whole UTC days.
- shiftDatesByFraction: move a bar's start and target by the
  same delta, clamped to the visible range, with start dropped
  when clamping would invert or collapse the span.
- resizeEndToFraction: what the new target date becomes when
  the user drags a right edge.
- moveCardByFraction: alias that produces the action's input
  shape.

11 unit tests cover the corners (no dates set, point bar,
clamping past the right edge, inversion guard).

rescheduleCardAction also drops its owner-only gate. Any
signed-in user — owner or client manager — can move dates
now. PocketBase's existing row-level rules govern which cards
a given user can write (a client manager still only writes
their own client's cards); the activity-log row is unchanged
and attributes the move to the actual user either way.

Adds setSubtaskDatesAction so the same drag/double-click
gestures work on subtask rows. No audit log on subtasks —
matches the existing pattern (subtask toggles don't log).
2026-09-22 05:16:04 +02:00

89 lines
3 KiB
TypeScript

import { describe, it, expect } from "vitest";
import {
fractionToDate,
shiftDatesByFraction,
resizeEndToFraction,
moveCardByFraction,
} from "@/lib/roadmap/bar-math";
import type { TimelineRange } from "@/lib/roadmap/timeline";
const RANGE: TimelineRange = { from: "2026-02-01", to: "2026-02-22" };
describe("fractionToDate", () => {
it("maps 0 to range.from", () => {
expect(fractionToDate(0, RANGE)).toBe("2026-02-01");
});
it("maps 1 to range.to (rounded)", () => {
expect(fractionToDate(1, RANGE)).toBe("2026-02-22");
});
it("maps 0.5 to the midpoint", () => {
expect(fractionToDate(0.5, RANGE)).toBe("2026-02-11");
// 21 days span (Feb 1 to Feb 22 inclusive diff = 21); half = ~10.5 → 11
});
it("clamps out-of-range inputs", () => {
expect(fractionToDate(-0.5, RANGE)).toBe("2026-02-01");
expect(fractionToDate(1.5, RANGE)).toBe("2026-02-22");
});
});
describe("shiftDatesByFraction — moving a bar", () => {
it("shifts both dates by the same number of days when both are set", () => {
const next = shiftDatesByFraction(
"2026-02-05",
"2026-02-10",
21 / 21 / 2, // half the visible range ≈ 10.5 days → 11
RANGE
);
expect(next.startDate).toBe("2026-02-16");
expect(next.targetDate).toBe("2026-02-21");
});
it("returns nulls when neither date is set", () => {
const next = shiftDatesByFraction("", "", 0.5, RANGE);
expect(next).toEqual({ startDate: null, targetDate: null });
});
it("moves only the target when only the target is set", () => {
const next = shiftDatesByFraction("", "2026-02-10", 0.1, RANGE);
expect(next.startDate).toBeNull();
expect(next.targetDate).toBe("2026-02-12");
});
it("clamps the target to range.to when dragged past the right edge", () => {
const next = shiftDatesByFraction(
"2026-02-18",
"2026-02-20",
10, // way past the right edge
RANGE
);
// Both dates would clamp to range.to and collide; start is dropped
// so the bar still renders as a point at the right edge.
expect(next.startDate).toBeNull();
expect(next.targetDate).toBe("2026-02-22");
});
it("keeps a 1-day span when only the target clamps", () => {
// start=18 target=20 (2-day span). Drag 0.2 of the range (~4 days) right.
// Target=20+4 = 24 → clamps to 22. Start=18+4 = 22, equal to target → drop.
const next = shiftDatesByFraction("2026-02-18", "2026-02-20", 0.2, RANGE);
expect(next.targetDate).toBe("2026-02-22");
});
});
describe("resizeEndToFraction — right-edge resize", () => {
it("returns the date at the new fraction", () => {
expect(resizeEndToFraction("2026-02-05", 0.5, RANGE)).toBe("2026-02-11");
});
});
describe("moveCardByFraction — action shape", () => {
it("mirrors shiftDatesByFraction so the action gets the same shape", () => {
const a = shiftDatesByFraction("2026-02-05", "2026-02-10", 0.1, RANGE);
const b = moveCardByFraction("2026-02-05", "2026-02-10", 0.1, RANGE);
expect(a).toEqual(b);
});
});