Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/loopover-miner-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
"@tanstack/react-router": "^1.170.18",
"react": "^19.2.8",
"react-dom": "^19.2.8",
"recharts": "^2.15.4",
"recharts": "^3.10.1",
"tailwindcss": "^4.3.3",
"tw-animate-css": "^1.4.0",
"vite-tsconfig-paths": "^6.1.1"
Expand Down
2 changes: 1 addition & 1 deletion apps/loopover-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@
"react-dom": "^19.2.8",
"react-hook-form": "^7.82.0",
"react-resizable-panels": "^4.12.2",
"recharts": "^2.15.4",
"recharts": "^3.10.1",
"sonner": "^2.0.7",
"tailwind-merge": "^3.6.0",
"tailwindcss": "^4.3.3",
Expand Down
75 changes: 75 additions & 0 deletions apps/loopover-ui/src/components/site/sparkline.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { render } from "@testing-library/react";
import { describe, expect, it } from "vitest";

import { Sparkline } from "./sparkline";
import type { TrendPoint } from "./proof-of-power-stats-model";

// The sparkline is the one place this app renders a chart library, so a major bump of that library
// (#8610: recharts 2 -> 3) needs a guard that the thing still DRAWS -- a type-clean upgrade that renders
// nothing looks identical to a passing build.
//
// ResponsiveContainer measures its parent, and jsdom reports 0x0 for everything, so the fixed pixel size
// the component already sets is stubbed onto the element's box here. Without it recharts correctly draws
// nothing and every assertion below would pass vacuously against an empty SVG.
function renderSparkline(points: TrendPoint[]) {
const original = Object.getOwnPropertyDescriptor(HTMLElement.prototype, "getBoundingClientRect");
Object.defineProperty(HTMLElement.prototype, "getBoundingClientRect", {
configurable: true,
value: () => ({
width: 64,
height: 28,
top: 0,
left: 0,
right: 64,
bottom: 28,
x: 0,
y: 0,
toJSON: () => ({}),
}),
});
try {
return render(<Sparkline points={points} color="#ff0000" />);
} finally {
if (original) Object.defineProperty(HTMLElement.prototype, "getBoundingClientRect", original);
}
}

const WEEKS: TrendPoint[] = [
{ label: "w1", value: 10 },
{ label: "w2", value: 20 },
{ label: "w3", value: 15 },
];

describe("Sparkline (#8610 recharts v3)", () => {
it("renders an accessible trend image with a drawn line path", () => {
const { container, getByRole } = renderSparkline(WEEKS);
expect(getByRole("img", { name: /trend over the last 3 weeks/i })).toBeTruthy();
// The actual regression this guards: a v3 API change that type-checks but draws nothing.
const paths = container.querySelectorAll("path.recharts-line-curve, .recharts-line path");
expect(paths.length).toBeGreaterThan(0);
});

it("renders NOTHING when every point is null — a sparkline of no data is not a flat line at zero", () => {
const { container } = renderSparkline([
{ label: "w1", value: null },
{ label: "w2", value: null },
]);
expect(container.firstChild).toBeNull();
});

it("still renders when only some weeks are below their sample floor", () => {
// connectNulls={false} is what makes a gap read as "insufficient data" rather than as a straight line
// drawn through it; the component must not bail just because one point is null.
const { getByRole } = renderSparkline([
{ label: "w1", value: 10 },
{ label: "w2", value: null },
{ label: "w3", value: 30 },
]);
expect(getByRole("img")).toBeTruthy();
});

it("labels itself with the real number of weeks it was given", () => {
const { getByRole } = renderSparkline([...WEEKS, { label: "w4", value: 12 }]);
expect(getByRole("img", { name: /last 4 weeks/i })).toBeTruthy();
});
});
12 changes: 11 additions & 1 deletion apps/loopover-ui/src/components/site/sparkline.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Line, LineChart, ResponsiveContainer } from "recharts";
import type { ComponentProps } from "react";

import type { TrendPoint } from "./proof-of-power-stats-model";

Expand All @@ -9,6 +10,11 @@ import type { TrendPoint } from "./proof-of-power-stats-model";
// line (a null point, e.g. a week below its own minimum-sample floor) is the correct rendering for "not enough
// data yet" -- recharts breaks the segment there rather than drawing a fabricated straight line through it.

/** The props recharts passes to a `dot` render function, taken from `Line`'s own prop type. */
type LineDotProps = Parameters<
Extract<ComponentProps<typeof Line>["dot"], (props: never) => unknown>
>[0];

const SPARKLINE_WIDTH = 64;
const SPARKLINE_HEIGHT = 28;

Expand Down Expand Up @@ -44,7 +50,11 @@ export function Sparkline({
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
dot={(props: { index?: number; cx?: number; cy?: number; key?: string }) => {
// The props are DERIVED from Line's own `dot` signature rather than re-typed by hand
// (#8610): recharts v3 widened `key` from `string` to React's `Key | null`, which a
// hand-written annotation silently disagreed with. Reading the library's type means the
// next such change is a compile error here instead of a drifted local copy.
dot={(props: LineDotProps) => {
const { index, cx, cy, key } = props;
if (index !== lastValueIndex || cx === undefined || cy === undefined) {
return <g key={key} />;
Expand Down
Loading
Loading