Skip to content
Merged
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
80 changes: 66 additions & 14 deletions packages/loopover-ui-kit/src/components/chart.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ import { describe, expect, it, vi } from "vitest";
// asserting against a mock rather than against `chart.tsx`.
vi.mock("recharts", async (importOriginal) => ({
...(await importOriginal<typeof import("recharts")>()),
ResponsiveContainer: ({ children }: { children?: React.ReactNode }) => <div data-testid="responsive">{children}</div>,
ResponsiveContainer: ({ children }: { children?: React.ReactNode }) => (
<div data-testid="responsive">{children}</div>
),
}));

import {
Expand All @@ -35,8 +37,20 @@ const config = {
} satisfies ChartConfig;

const payload = [
{ dataKey: "revenue", name: "revenue", value: 120, color: "#0ea5e9", payload: { month: "Jan", revenue: 120 } },
{ dataKey: "cost", name: "cost", value: 80, color: "#111827", payload: { month: "Jan", cost: 80 } },
{
dataKey: "revenue",
name: "revenue",
value: 120,
color: "#0ea5e9",
payload: { month: "Jan", revenue: 120 },
},
{
dataKey: "cost",
name: "cost",
value: 80,
color: "#111827",
payload: { month: "Jan", cost: 80 },
},
];

/** ChartTooltipContent calls useChart(), so it must render inside a provider. ChartContainer is that
Expand All @@ -51,14 +65,19 @@ function renderInChart(node: React.ReactNode) {
);
// Scoped through this render's OWN container, not the shared document: a test that renders twice would
// otherwise match both slots and fail on "found multiple elements".
return { ...result, slot: () => result.container.querySelector('[data-testid="slot"]')! };
return {
...result,
slot: () => result.container.querySelector('[data-testid="slot"]')!,
};
}

describe("ChartTooltipContent (recharts v3, #8610)", () => {
it("renders one row per payload entry, labelled from the chart config", () => {
// The config's `label` -- not the raw dataKey -- is what a reader sees. Regressing to the dataKey is the
// most likely silent breakage, and it still "renders fine".
const chart = renderInChart(<ChartTooltipContent active payload={payload} label="Jan" />);
const chart = renderInChart(
<ChartTooltipContent active payload={payload} label="Jan" />,
);

expect(chart.getByText("Revenue")).toBeTruthy();
expect(chart.getByText("Cost")).toBeTruthy();
Expand All @@ -69,35 +88,62 @@ describe("ChartTooltipContent (recharts v3, #8610)", () => {
it("INVARIANT: renders nothing when inactive or when the payload is empty", () => {
// A tooltip that paints on an empty payload follows the cursor around an empty chart. Both arms, because
// `!active || !payload?.length` is two conditions and only testing one leaves the other free to invert.
const inactive = renderInChart(<ChartTooltipContent active={false} payload={payload} />);
const inactive = renderInChart(
<ChartTooltipContent active={false} payload={payload} />,
);
expect(inactive.slot().textContent).toBe("");

const empty = renderInChart(<ChartTooltipContent active payload={[]} />);
expect(empty.slot().textContent).toBe("");
});

it("hides the label when asked, and formats it through labelFormatter otherwise", () => {
const formatted = renderInChart(<ChartTooltipContent active payload={payload} label="Jan" labelFormatter={(value) => `Month: ${String(value)}`} />);
const formatted = renderInChart(
<ChartTooltipContent
active
payload={payload}
label="Jan"
labelFormatter={(value) => `Month: ${String(value)}`}
/>,
);
expect(formatted.getByText("Month: Jan")).toBeTruthy();

const hidden = renderInChart(<ChartTooltipContent active payload={payload} label="Jan" hideLabel />);
const hidden = renderInChart(
<ChartTooltipContent active payload={payload} label="Jan" hideLabel />,
);
expect(hidden.queryByText("Jan")).toBeNull();
});

it("routes the value through a custom formatter when one is supplied", () => {
const chart = renderInChart(<ChartTooltipContent active payload={[payload[0]!]} formatter={(value) => `$${String(value)}`} />);
const chart = renderInChart(
<ChartTooltipContent
active
payload={[payload[0]!]}
formatter={(value) => `$${String(value)}`}
/>,
);
expect(chart.getByText("$120")).toBeTruthy();
});

it("resolves a series by nameKey when the payload's own key is not the config key", () => {
const chart = renderInChart(<ChartTooltipContent active payload={[{ ...payload[0]!, dataKey: "unmapped", name: "unmapped" }]} nameKey="revenue" />);
const chart = renderInChart(
<ChartTooltipContent
active
payload={[{ ...payload[0]!, dataKey: "unmapped", name: "unmapped" }]}
nameKey="revenue"
/>,
);
expect(chart.getByText("Revenue")).toBeTruthy();
});
});

describe("ChartLegendContent (recharts v3, #8610)", () => {
it("labels each legend entry from the config", () => {
const chart = renderInChart(<ChartLegendContent payload={[{ value: "revenue", dataKey: "revenue", color: "#0ea5e9" }]} />);
const chart = renderInChart(
<ChartLegendContent
payload={[{ value: "revenue", dataKey: "revenue", color: "#0ea5e9" }]}
/>,
);
expect(chart.getByText("Revenue")).toBeTruthy();
});

Expand All @@ -111,7 +157,9 @@ describe("ChartStyle", () => {
it("emits per-theme CSS variables only for series that declare a colour", () => {
// The `theme` and `color` arms of ChartConfig are a union, and both have to reach the stylesheet -- a
// series configured with `theme` produces one variable per theme selector.
const { container } = render(<ChartStyle id="chart-test" config={config} />);
const { container } = render(
<ChartStyle id="chart-test" config={config} />,
);
const css = container.querySelector("style")?.innerHTML ?? "";

expect(css).toContain("--color-revenue: #0ea5e9");
Expand All @@ -121,7 +169,9 @@ describe("ChartStyle", () => {
});

it("emits no stylesheet at all when no series declares a colour", () => {
const { container } = render(<ChartStyle id="chart-empty" config={{ plain: { label: "Plain" } }} />);
const { container } = render(
<ChartStyle id="chart-empty" config={{ plain: { label: "Plain" } }} />,
);
expect(container.querySelector("style")).toBeNull();
});
});
Expand All @@ -130,6 +180,8 @@ describe("useChart", () => {
it("REGRESSION: throws a named error outside a ChartContainer rather than reading null", () => {
// Without the guard this is a `Cannot read properties of null` deep inside a tooltip render, which is a
// materially harder thing to diagnose than the message the component actually throws.
expect(() => render(<ChartTooltipContent active payload={payload} />)).toThrow(/useChart must be used within a <ChartContainer \/>/);
expect(() =>
render(<ChartTooltipContent active payload={payload} />),
).toThrow(/useChart must be used within a <ChartContainer \/>/);
});
});
Loading