import { describe, expect, it } from "vitest";
import {
ACTIVITY_METRICS,
layoutActivityChart,
type ActivityMetric
} from "#lib/components/profile/profileActivityChart.ts";
const values = {
pulls: [10, 20, 30, 40, 50, 60],
commits: [600, 500, 400, 300, 200, 100],
repos: [0, 1, 0, 2, 1, 3],
issues: [5, 5, 5, 5, 5, 5]
} satisfies Record<ActivityMetric, number[]>;
describe("layoutActivityChart", () => {
it("fills wide containers and preserves the design's column proportions", () => {
const layout = layoutActivityChart(values, 1976);
expect(layout.width).toBe(1976);
expect(layout.columns.at(-1)!.left + layout.columns.at(-1)!.width).toBe(1976);
expect(layout.columns[0].width).toBe(292);
expect(layout.columns.at(-1)!.width).toBe(516);
});
it("anchors every metric 12px inside its month column", () => {
const layout = layoutActivityChart(values, 1976);
for (const [month, column] of layout.columns.entries()) {
for (const metric of ACTIVITY_METRICS) {
expect(layout.points[metric][month].x).toBe(column.left + 12);
}
}
});
it("keeps a scrollable minimum width and omits the first divider", () => {
const layout = layoutActivityChart(values, 320);
const design = layoutActivityChart(values, 720);
expect(layout.columns[0].width).toBeCloseTo(design.columns[0].width * 1.5);
expect(layout.columns.at(-1)!.width).toBeCloseTo(design.columns.at(-1)!.width);
expect(layout.columns[0].width).toBeLessThan(layout.columns.at(-1)!.width);
expect(layout.columns.at(-1)!.left + layout.columns.at(-1)!.width).toBeCloseTo(
layout.width
);
expect(design.width).toBe(720);
expect(layout.columns[0].divider).toBe(false);
expect(layout.columns.slice(1).every((column) => column.divider)).toBe(true);
});
it("normalizes every metric independently across the shared plot", () => {
const layout = layoutActivityChart(values, 988);
expect(layout.points.pulls[0].y).toBeGreaterThan(layout.points.pulls[5].y);
expect(layout.points.commits[0].y).toBeLessThan(layout.points.commits[5].y);
});
it("keeps every value card legible and bounded by the plot", () => {
const layout = layoutActivityChart(values, 988);
for (let month = 0; month < 6; month += 1) {
const stacked = ACTIVITY_METRICS.map(
(metric) => layout.points[metric][month].y
).toSorted((a, b) => a - b);
for (const [index, y] of stacked.slice(1).entries()) {
expect(y - stacked[index]).toBeGreaterThanOrEqual(30);
}
}
const flat = layout.points.issues.map((point) => point.y);
expect(Math.max(...flat) - Math.min(...flat)).toBeLessThanOrEqual(
30 * (ACTIVITY_METRICS.length - 1)
);
for (const metric of ACTIVITY_METRICS) {
for (const point of layout.points[metric]) {
expect(point.y).toBeGreaterThanOrEqual(20);
expect(point.y).toBeLessThanOrEqual(196);
}
}
});
it("uses the same points as the visible value anchors and path endpoints", () => {
const layout = layoutActivityChart(values, 988);
for (const metric of ACTIVITY_METRICS) {
const connectors = layout.segments[metric].slice(1, -1);
for (const [index, segment] of connectors.entries()) {
const point = layout.points[metric][index];
const next = layout.points[metric][index + 1];
expect(segment.d).toMatch(new RegExp(`^M ${point.x} ${point.y} C`));
expect(segment.d).toMatch(new RegExp(`${next.x} ${next.y}$`));
}
}
});
it("separates each column's metrics from top to bottom", () => {
const layout = layoutActivityChart(values, 988);
for (let month = 0; month < 6; month += 1) {
const byY = ACTIVITY_METRICS.toSorted(
(a, b) => layout.points[a][month].y - layout.points[b][month].y
);
for (const [index, metric] of byY.slice(1).entries()) {
expect(layout.points[metric][month].y).toBeGreaterThan(
layout.points[byY[index]][month].y
);
}
}
});
it("runs each metric from the left edge to the right edge", () => {
const layout = layoutActivityChart(values, 988);
for (const metric of ACTIVITY_METRICS) {
const first = layout.points[metric][0];
const last = layout.points[metric].at(-1)!;
expect(layout.segments[metric].at(0)!.d).toBe(`M 0 ${first.y} L ${first.x} ${first.y}`);
expect(layout.segments[metric].at(-1)!.d).toBe(`M ${last.x} ${last.y} L 988 ${last.y}`);
}
});
});