mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 04:50:59 +00:00
test(charts): mobile-viewport e2e spec for header layout + tick density
Adds a Playwright spec that runs only on the chromium-mobile project (Pixel 5, 393 × 851) and gates the two regressions the previous chart commit cleared: - Every chart card on the dashboard keeps its range tabs on a single horizontal row (asserts that all `[data-slot=chart-range-tab]` inside a card share one rounded `top` coordinate). Mock data feeds every chart 30 days of measurements + 30 days of compliance points so the controls row actually paints with all chips and tabs. - Every recharts x-axis paints at most seven visible tick labels at Pixel 5. The helper caps at six; the seven-tick allowance covers Recharts' preserveStartEnd interaction with rounding. The PROD probe script that drove the audit is also committed at .planning/v1419-a2-prod-probe.mjs for future regression runs, with the findings recorded in .planning/phase-A2-mobile-findings.md. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
60a91afaee
commit
a7390852f8
3 files changed
+537
No files matched your search
@@ -0,0 +1,127 @@
|
||||
# Phase A2 — Mobile chart findings (PROD audit + post-fix verification)
|
||||
|
||||
Marathon: v1.4.19 Wave-A
|
||||
Agent: A2 (parallel with A1, A3, A4, A5, A6, A7, A8)
|
||||
Probe script: `.planning/v1419-a2-prod-probe.mjs`
|
||||
Probe output (JSON + screenshots): `/tmp/v1419-a2-prod/`
|
||||
|
||||
## Method
|
||||
|
||||
Playwright headless against `https://healthlog.bombeck.io` with Marc's
|
||||
session cookie, four mobile viewports:
|
||||
|
||||
- Pixel 5 — 393 × 851
|
||||
- iPhone 12 — 390 × 844
|
||||
- iPhone SE — 375 × 667
|
||||
- Galaxy Fold compact — 280 × 653
|
||||
|
||||
For each chart card on `/` (dashboard) and `/insights`:
|
||||
|
||||
- Header height (px from top of card to bottom of controls row)
|
||||
- Number of distinct vertical rows the range tabs occupy
|
||||
(`new Set(tabRects.map(t => Math.round(t.top))).size`)
|
||||
- `card.scrollWidth > card.clientWidth` (horizontal overflow)
|
||||
- `XAxis` tick count visible inside the SVG
|
||||
|
||||
## Pre-fix readings (live PROD before A2 ship)
|
||||
|
||||
```
|
||||
viewport page chart tabs rows header overflow
|
||||
pixel5 insights Blood Pressure 4 2 92 false
|
||||
pixel5 insights Pulse 4 2 92 false
|
||||
iphone12 dashboard Blood Pressure 4 2 92 false
|
||||
iphone12 dashboard Pulse 4 2 92 false
|
||||
iphone12 dashboard Mood 4 1 108 false
|
||||
iphone12 dashboard Medications 3 2 92 false
|
||||
fold-compact dashboard Blood Pressure 4 4 188 false
|
||||
fold-compact dashboard Pulse 4 3 188 false
|
||||
fold-compact dashboard Mood 4 2 108 false
|
||||
fold-compact dashboard Medications 3 3 188 true ←
|
||||
```
|
||||
|
||||
(Charts with header == 44 px and rows == 1 — Weight, BMI — already
|
||||
fitted because they had no bucket-aggregation chip and no comparison
|
||||
caption to push the tabs around. The bug was specific to charts whose
|
||||
title row carried extra chips.)
|
||||
|
||||
Two distinct symptoms confirmed:
|
||||
|
||||
**1. Header label-band overflow.** Marc's complaint about
|
||||
"Wochendurchschnitt + 7T/30T/90T/Alle wraps and breaks the layout" was
|
||||
exactly this: the bucket-aggregation chip ("Weekly avg" / "Monthly
|
||||
avg") + the optional comparison-caption chip + 4 range tabs + the cog
|
||||
dropdown all sat on one `flex justify-between` row. On Pixel 5 / iPhone
|
||||
12 the chips ate enough horizontal space that the 4 tabs wrapped to a
|
||||
2nd row inside the same flex container — header height jumped 44 → 92.
|
||||
On Galaxy Fold compact (280 px) the tabs split into 3-4 rows and one
|
||||
chart overflowed horizontally.
|
||||
|
||||
**2. X-axis tick density inconsistent.** Medication chart used the
|
||||
default Recharts tick rendering with `interval="preserveStartEnd"` —
|
||||
that's _one tick per data point_, so a 30-day Pixel 5 window painted 30
|
||||
overlapping date labels. Weight / BMI / pulse / mood charts didn't show
|
||||
the same density because they auto-bucketed daily → weekly above 90
|
||||
days, but at the 30-day range they still drew a tick every couple of
|
||||
days.
|
||||
|
||||
## Fixes shipped
|
||||
|
||||
**Fix 1 — header layout** (commit `fix(charts): mobile header layout + unified x-axis tick density`):
|
||||
|
||||
- `<sm`: stack the header into two rows. Title + chips on row 1; range
|
||||
tabs + cog right-aligned on row 2. Tabs use `flex-nowrap` and
|
||||
`px-2 sm:px-3` so they always fit a single row at 280 px.
|
||||
- Bucket-aggregation chip + comparison caption + comparison-unavailable
|
||||
fallback all `hidden sm:inline-flex` — they're decorative and the
|
||||
range tabs already communicate the visible window. The cog dropdown
|
||||
surfaces overlays explicitly.
|
||||
- `≥sm`: original side-by-side layout, all chips visible.
|
||||
- Applied to HealthChart (BP / weight / pulse / BMI / sleep / steps),
|
||||
MoodChart, MedicationComplianceChart consistently.
|
||||
|
||||
**Fix 2 — universal X-axis tick-density helper** (same commit):
|
||||
|
||||
- New `src/lib/charts/x-axis-density.ts` with `chooseTickInterval(N, w)`:
|
||||
- 280 px (Fold) → max 4 ticks
|
||||
- 393 px (Pixel 5) / 390 px (iPhone 12) → max 6 ticks
|
||||
- 481-768 px (small tablet) → max 8 ticks
|
||||
- > 768 px (desktop) → max 10 ticks
|
||||
- New `src/hooks/use-viewport-width.ts` for SSR-safe reactive viewport
|
||||
width with desktop default during SSR.
|
||||
- Wired into all 4 chart wrappers (HealthChart, MoodChart,
|
||||
MedicationComplianceChart, ComplianceLineChart). The
|
||||
ScatterCorrelationChart was deliberately left alone — its X-axis is
|
||||
numeric (BP / weight values), not a time series, and already supplies
|
||||
explicit `ticks={...}` arrays.
|
||||
|
||||
## Post-fix gating
|
||||
|
||||
The two regressions are gated by an e2e spec at
|
||||
`e2e/charts-mobile.spec.ts` that runs only on the `chromium-mobile`
|
||||
project (Pixel 5 profile). Two assertions per chart:
|
||||
|
||||
- All `[data-slot=chart-range-tab]` inside a card share one rounded
|
||||
`top` coordinate — i.e. one horizontal row.
|
||||
- Each `recharts-xAxis` paints ≤ 7 visible tick labels.
|
||||
|
||||
Helper unit tests at `src/lib/charts/__tests__/x-axis-density.test.ts`
|
||||
pin the interval math for Fold / Pixel 5 / tablet / desktop widths
|
||||
across 7 / 30 / 90 / 365-point ranges.
|
||||
|
||||
## Constraints honoured
|
||||
|
||||
Touched only the assigned A2 surface (chart wrappers + new
|
||||
helper/hook). Did not touch `src/lib/insights/*`, `src/components/insights/*`,
|
||||
`src/lib/ai/prompts/*`, `src/components/settings/*`, or
|
||||
`src/components/admin/*`. No new dependencies. Pre-commit hooks pass
|
||||
on every commit.
|
||||
|
||||
## Note on parallel-agent race
|
||||
|
||||
During the second commit a parallel agent staged + committed against
|
||||
the same index, the first attempt at commit 2 picked up their
|
||||
`phase-A5-report.md` instead of my files (the staged set got swapped
|
||||
between my `git add` and `git commit`). The polluted commit was
|
||||
dropped via `git rebase --onto` before push; the parallel agent's work
|
||||
was preserved by cherry-picking their amended commit forward. The
|
||||
final history is clean and ordered by commit time.
|
||||
@@ -0,0 +1,177 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* v1.4.19 A2 — pre-fix mobile chart audit against PROD.
|
||||
*
|
||||
* Captures header layout + x-axis tick density for all chart cards
|
||||
* across 4 mobile viewports. Output: per-viewport screenshots +
|
||||
* machine-readable findings file.
|
||||
*/
|
||||
import { chromium } from "@playwright/test";
|
||||
import { writeFileSync, mkdirSync } from "node:fs";
|
||||
|
||||
const SESSION = "cmox4d6fj000101p8w9ykhcnm";
|
||||
const ORIGIN = "https://healthlog.bombeck.io";
|
||||
const OUTDIR = "/tmp/v1419-a2-prod";
|
||||
mkdirSync(OUTDIR, { recursive: true });
|
||||
|
||||
const VIEWPORTS = [
|
||||
{ name: "pixel5", width: 393, height: 851, deviceScale: 2.75 },
|
||||
{ name: "iphone12", width: 390, height: 844, deviceScale: 3 },
|
||||
{ name: "iphone-se", width: 375, height: 667, deviceScale: 2 },
|
||||
{ name: "fold-compact", width: 280, height: 653, deviceScale: 3 },
|
||||
];
|
||||
|
||||
const PAGES = [
|
||||
{ path: "/", label: "dashboard" },
|
||||
{ path: "/insights", label: "insights" },
|
||||
];
|
||||
|
||||
const browser = await chromium.launch();
|
||||
const findings = {};
|
||||
|
||||
for (const vp of VIEWPORTS) {
|
||||
findings[vp.name] = {};
|
||||
const ctx = await browser.newContext({
|
||||
viewport: { width: vp.width, height: vp.height },
|
||||
userAgent:
|
||||
"Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0 Mobile Safari/537.36",
|
||||
deviceScaleFactor: vp.deviceScale,
|
||||
hasTouch: true,
|
||||
isMobile: true,
|
||||
colorScheme: "dark",
|
||||
storageState: {
|
||||
cookies: [
|
||||
{
|
||||
name: "healthlog_session",
|
||||
value: SESSION,
|
||||
domain: "healthlog.bombeck.io",
|
||||
path: "/",
|
||||
httpOnly: true,
|
||||
secure: true,
|
||||
sameSite: "Lax",
|
||||
expires: -1,
|
||||
},
|
||||
],
|
||||
origins: [],
|
||||
},
|
||||
});
|
||||
|
||||
for (const pg of PAGES) {
|
||||
const page = await ctx.newPage();
|
||||
page.on("console", () => {});
|
||||
page.on("pageerror", () => {});
|
||||
try {
|
||||
await page.goto(`${ORIGIN}${pg.path}`, {
|
||||
waitUntil: "domcontentloaded",
|
||||
timeout: 30000,
|
||||
});
|
||||
// Wait for at least one chart card or main content
|
||||
await page
|
||||
.waitForSelector("main, [data-slot=medication-compliance-chart], svg", {
|
||||
timeout: 15000,
|
||||
})
|
||||
.catch(() => {});
|
||||
await page.waitForTimeout(2500);
|
||||
|
||||
// Scroll to make sure all charts render
|
||||
await page.evaluate(() => window.scrollTo(0, 0));
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
const screenshotPath = `${OUTDIR}/${pg.label}-${vp.name}.png`;
|
||||
await page.screenshot({ path: screenshotPath, fullPage: true });
|
||||
|
||||
// Find all chart cards
|
||||
const data = await page.evaluate(() => {
|
||||
const cards = Array.from(
|
||||
document.querySelectorAll(
|
||||
"div[data-slot=medication-compliance-chart], [data-slot=chart-mini], div.bg-card",
|
||||
),
|
||||
).filter((el) => el.querySelector("svg") !== null);
|
||||
|
||||
const out = [];
|
||||
for (const card of cards) {
|
||||
const rect = card.getBoundingClientRect();
|
||||
if (rect.height < 100) continue;
|
||||
|
||||
// Find header row + check overflow
|
||||
const titleEl = card.querySelector("h3, [data-slot=card-title]");
|
||||
const title = titleEl?.textContent?.trim() ?? "(unknown)";
|
||||
const titleRect = titleEl?.getBoundingClientRect();
|
||||
|
||||
// Range tabs — use data-slot when present; fall back to
|
||||
// text-content sniffing for charts that haven't been
|
||||
// tagged yet.
|
||||
const taggedTabs = Array.from(
|
||||
card.querySelectorAll("[data-slot=chart-range-tab]"),
|
||||
);
|
||||
const sniffedTabs = Array.from(
|
||||
card.querySelectorAll("button"),
|
||||
).filter((b) =>
|
||||
/^(7|30|90)\s*(T|Pkt|pts)$|^Alle$|^All$|^(7|30|90)D$/i.test(
|
||||
(b.textContent ?? "").trim(),
|
||||
),
|
||||
);
|
||||
const rangeTabs = taggedTabs.length > 0 ? taggedTabs : sniffedTabs;
|
||||
const tabRects = rangeTabs.map((b) => {
|
||||
const r = b.getBoundingClientRect();
|
||||
return {
|
||||
text: b.textContent?.trim(),
|
||||
top: r.top,
|
||||
right: r.right,
|
||||
width: r.width,
|
||||
};
|
||||
});
|
||||
const tabTops = new Set(tabRects.map((t) => Math.round(t.top)));
|
||||
|
||||
// Header bar height
|
||||
const headerEl = card.querySelector(".mb-4, .pb-2");
|
||||
const headerRect = headerEl?.getBoundingClientRect();
|
||||
|
||||
// Check overflow
|
||||
const overflowsHorizontally = card.scrollWidth > card.clientWidth + 1;
|
||||
|
||||
// X-axis ticks
|
||||
const xTicks = Array.from(
|
||||
card.querySelectorAll(
|
||||
".recharts-xAxis .recharts-cartesian-axis-tick-value",
|
||||
),
|
||||
).map((t) => t.textContent?.trim());
|
||||
|
||||
out.push({
|
||||
title,
|
||||
cardRect: {
|
||||
top: rect.top,
|
||||
height: rect.height,
|
||||
width: rect.width,
|
||||
},
|
||||
titleTop: titleRect?.top,
|
||||
tabs: tabRects,
|
||||
tabUniqueRows: tabTops.size,
|
||||
overflowsHorizontally,
|
||||
cardScrollWidth: card.scrollWidth,
|
||||
cardClientWidth: card.clientWidth,
|
||||
headerHeight: headerRect?.height,
|
||||
xTickCount: xTicks.length,
|
||||
xTicks,
|
||||
});
|
||||
}
|
||||
return out;
|
||||
});
|
||||
|
||||
findings[vp.name][pg.label] = {
|
||||
screenshot: screenshotPath,
|
||||
cards: data,
|
||||
};
|
||||
} catch (err) {
|
||||
findings[vp.name][pg.label] = { error: String(err) };
|
||||
}
|
||||
await page.close();
|
||||
}
|
||||
await ctx.close();
|
||||
}
|
||||
|
||||
await browser.close();
|
||||
|
||||
writeFileSync(`${OUTDIR}/findings.json`, JSON.stringify(findings, null, 2));
|
||||
console.log("---PROD-AUDIT-COMPLETE---");
|
||||
console.log(JSON.stringify(findings, null, 2));
|
||||
@@ -0,0 +1,233 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
import { STORAGE_STATE_PATH } from "./setup/global-setup";
|
||||
|
||||
/**
|
||||
* v1.4.19 A2 — mobile-viewport regression spec for chart cards.
|
||||
*
|
||||
* Two assertions per chart visible in the dashboard's initial paint:
|
||||
*
|
||||
* 1. Header layout integrity — the range tabs (`7 pts / 30 pts /
|
||||
* 90 pts / All`) and the cog dropdown sit on a SINGLE horizontal
|
||||
* row inside the chart card. Pre-fix the bucket-aggregation chip
|
||||
* ("Weekly avg" / "Monthly avg") + comparison caption pushed the
|
||||
* tabs onto a second row on Pixel 5 (and 3-4 rows on Galaxy Fold
|
||||
* compact). The fix stacks title + chips above tabs + cog on
|
||||
* mobile so the tabs always own the full controls row.
|
||||
*
|
||||
* 2. X-axis tick density — Recharts' default rendered ONE tick per
|
||||
* data point, so a 30-day Pixel 5 window painted 30 overlapping
|
||||
* labels. The universal helper `chooseTickInterval` caps that at
|
||||
* 6 ticks for ≤ 480px viewports.
|
||||
*
|
||||
* The spec runs only on the `chromium-mobile` project (Pixel 5,
|
||||
* 393×851). Desktop sizes are exercised separately by the unit-test
|
||||
* suite for the helper.
|
||||
*/
|
||||
|
||||
test.describe("charts — mobile-viewport regression", () => {
|
||||
test.use({ storageState: STORAGE_STATE_PATH });
|
||||
|
||||
test.beforeEach(async ({ page }, testInfo) => {
|
||||
test.skip(testInfo.project.name !== "chromium-mobile", "mobile-only spec");
|
||||
|
||||
// Mock analytics + measurements so the chart wrappers all clear
|
||||
// their data-floor gates. Same pattern as `dashboard.spec.ts`.
|
||||
await page.route("**/api/analytics", (route) =>
|
||||
route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
data: {
|
||||
summaries: {
|
||||
WEIGHT: {
|
||||
latest: 78.5,
|
||||
avg7: 78.2,
|
||||
avg30: 77.9,
|
||||
slope30: { slope: -0.05, direction: "down" },
|
||||
count: 30,
|
||||
},
|
||||
BLOOD_PRESSURE_SYS: {
|
||||
latest: 124,
|
||||
avg7: 122,
|
||||
avg30: 121,
|
||||
slope30: { slope: 0.1, direction: "flat" },
|
||||
count: 30,
|
||||
},
|
||||
BLOOD_PRESSURE_DIA: {
|
||||
latest: 80,
|
||||
avg7: 79,
|
||||
avg30: 78,
|
||||
slope30: { slope: 0.05, direction: "flat" },
|
||||
count: 30,
|
||||
},
|
||||
PULSE: {
|
||||
latest: 68,
|
||||
avg7: 70,
|
||||
avg30: 71,
|
||||
slope30: { slope: -0.2, direction: "down" },
|
||||
count: 30,
|
||||
},
|
||||
},
|
||||
bpInTargetPct: 78,
|
||||
glucoseByContext: {},
|
||||
},
|
||||
error: null,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
await page.route("**/api/mood/analytics", (route) =>
|
||||
route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
data: { entries: [], summary: { count: 0 } },
|
||||
error: null,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
// Return 30 days of measurements per type so each chart paints a
|
||||
// line with enough points that tick-density actually matters.
|
||||
await page.route("**/api/measurements*", (route) => {
|
||||
const url = new URL(route.request().url());
|
||||
const type = url.searchParams.get("type") ?? "WEIGHT";
|
||||
const baseValue =
|
||||
type === "BLOOD_PRESSURE_SYS"
|
||||
? 124
|
||||
: type === "BLOOD_PRESSURE_DIA"
|
||||
? 80
|
||||
: type === "PULSE"
|
||||
? 70
|
||||
: 78.5;
|
||||
const measurements = Array.from({ length: 30 }, (_, i) => ({
|
||||
id: `m_${type}_${i}`,
|
||||
type,
|
||||
value: baseValue + (i % 5) - 2,
|
||||
measuredAt: new Date(Date.now() - i * 86_400_000).toISOString(),
|
||||
notes: null,
|
||||
}));
|
||||
return route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
data: { measurements, meta: { total: measurements.length } },
|
||||
error: null,
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
// Medication compliance — return 30 days of mostly-perfect
|
||||
// compliance so the chart clears the sparse-data threshold and
|
||||
// paints a line with the full window.
|
||||
await page.route("**/api/medications/intake*", (route) => {
|
||||
const points = Array.from({ length: 30 }, (_, i) => {
|
||||
const d = new Date(Date.now() - i * 86_400_000);
|
||||
const yyyy = d.getUTCFullYear();
|
||||
const mm = String(d.getUTCMonth() + 1).padStart(2, "0");
|
||||
const dd = String(d.getUTCDate()).padStart(2, "0");
|
||||
return {
|
||||
date: `${yyyy}-${mm}-${dd}`,
|
||||
scheduled: 2,
|
||||
taken: i % 7 === 0 ? 1 : 2,
|
||||
};
|
||||
});
|
||||
return route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({ data: points, error: null }),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test("chart header keeps range tabs on a single row at Pixel 5", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto("/", { waitUntil: "domcontentloaded" });
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
// Wait for at least one chart-range-tab to mount.
|
||||
await page
|
||||
.locator("[data-slot=chart-range-tab]")
|
||||
.first()
|
||||
.waitFor({ timeout: 10_000 });
|
||||
|
||||
// Group the visible range tabs by their card and assert each
|
||||
// card's tabs share a single Y coordinate (== one row).
|
||||
const findings = await page.evaluate(() => {
|
||||
const cards = Array.from(document.querySelectorAll("div.bg-card")).filter(
|
||||
(c) => c.querySelector("[data-slot=chart-range-tab]") !== null,
|
||||
);
|
||||
return cards.map((card) => {
|
||||
const title =
|
||||
card
|
||||
.querySelector("h3, [data-slot=card-title]")
|
||||
?.textContent?.trim() ?? "(unknown)";
|
||||
const tabs = Array.from(
|
||||
card.querySelectorAll("[data-slot=chart-range-tab]"),
|
||||
);
|
||||
const tops = tabs.map((t) => Math.round(t.getBoundingClientRect().top));
|
||||
const uniqueRows = new Set(tops).size;
|
||||
const rect = card.getBoundingClientRect();
|
||||
return {
|
||||
title,
|
||||
tabCount: tabs.length,
|
||||
uniqueRows,
|
||||
overflow: card.scrollWidth > card.clientWidth + 1,
|
||||
width: rect.width,
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
// We must find at least one charted card.
|
||||
expect(findings.length, "no chart cards detected").toBeGreaterThan(0);
|
||||
|
||||
for (const f of findings) {
|
||||
expect(
|
||||
f.uniqueRows,
|
||||
`${f.title}: tabs split across ${f.uniqueRows} rows (expected 1)`,
|
||||
).toBe(1);
|
||||
expect(f.overflow, `${f.title} card overflows horizontally`).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
test("chart x-axis paints ≤ 7 ticks at Pixel 5 with a 30-day window", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto("/", { waitUntil: "domcontentloaded" });
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
// Wait for at least one Recharts SVG to mount.
|
||||
await page.locator(".recharts-xAxis").first().waitFor({ timeout: 10_000 });
|
||||
|
||||
// Recharts lays its visible tick labels under
|
||||
// `.recharts-xAxis .recharts-cartesian-axis-tick`. Count the ones
|
||||
// that actually have rendered text (the helper also produces minor
|
||||
// structural ticks without text labels, which we don't care about
|
||||
// for legibility).
|
||||
const tickCounts = await page.evaluate(() => {
|
||||
const charts = Array.from(document.querySelectorAll(".recharts-xAxis"));
|
||||
return charts.map((axis) => {
|
||||
const ticks = Array.from(
|
||||
axis.querySelectorAll(".recharts-cartesian-axis-tick"),
|
||||
);
|
||||
const visibleTicks = ticks.filter(
|
||||
(t) => (t.querySelector("text")?.textContent?.trim() ?? "").length > 0,
|
||||
);
|
||||
return visibleTicks.length;
|
||||
});
|
||||
});
|
||||
|
||||
expect(tickCounts.length, "no recharts axes detected").toBeGreaterThan(0);
|
||||
|
||||
for (const count of tickCounts) {
|
||||
// Pixel 5 = 393 px → max 6 ticks per the helper. Allow 7 for
|
||||
// Recharts' preserveStartEnd interaction.
|
||||
expect(
|
||||
count,
|
||||
`axis paints ${count} ticks (max 7 allowed)`,
|
||||
).toBeLessThanOrEqual(7);
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user