From d4f892ddfafdc95de8ff69771e6b7b15a095ab18 Mon Sep 17 00:00:00 2001 From: Paddy Mullen Date: Fri, 27 Feb 2026 13:52:42 -0500 Subject: [PATCH 01/15] add screenshot apparatus for baseline styling comparison MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stories, Playwright spec, CI job, and compare tools — applied to pre-fix commit e392c78 (before #587, #599, #597). Co-Authored-By: Claude Opus 4.6 --- .github/workflows/checks.yml | 39 ++ .../styling-issues-screenshots.spec.ts | 93 +++ .../src/stories/StylingIssues.stories.tsx | 407 +++++++++++++ scripts/download_styling_screenshots.sh | 53 ++ scripts/gen_screenshot_compare.py | 566 ++++++++++++++++++ 5 files changed, 1158 insertions(+) create mode 100644 packages/buckaroo-js-core/pw-tests/styling-issues-screenshots.spec.ts create mode 100644 packages/buckaroo-js-core/src/stories/StylingIssues.stories.tsx create mode 100755 scripts/download_styling_screenshots.sh create mode 100644 scripts/gen_screenshot_compare.py diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 6aa6f765a..fd756695c 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -407,6 +407,45 @@ jobs: path: packages/buckaroo-js-core/screenshots/ if-no-files-found: ignore + StylingScreenshots: + name: Styling Screenshots + runs-on: depot-ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@v6 + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: 9.10.0 + - name: Setup Node.js with pnpm cache + uses: actions/setup-node@v6 + with: + cache: 'pnpm' + cache-dependency-path: 'packages/pnpm-lock.yaml' + - name: Install pnpm dependencies + working-directory: packages + run: pnpm install --frozen-lockfile + - name: Cache Playwright browsers + uses: actions/cache@v5 + with: + path: ~/.cache/ms-playwright + key: playwright-${{ hashFiles('packages/buckaroo-js-core/package.json') }} + - name: Install Playwright browsers + working-directory: packages/buckaroo-js-core + run: pnpm exec playwright install chromium + - name: Capture screenshots + working-directory: packages/buckaroo-js-core + env: + SCREENSHOT_DIR: screenshots/styling + run: pnpm exec playwright test pw-tests/styling-issues-screenshots.spec.ts --reporter=line + - name: Upload screenshots + if: always() + uses: actions/upload-artifact@v4 + with: + name: styling-screenshots + path: packages/buckaroo-js-core/screenshots/styling/ + if-no-files-found: ignore + TestServer: name: Server Playwright Tests needs: [BuildWheel] diff --git a/packages/buckaroo-js-core/pw-tests/styling-issues-screenshots.spec.ts b/packages/buckaroo-js-core/pw-tests/styling-issues-screenshots.spec.ts new file mode 100644 index 000000000..2d5e82915 --- /dev/null +++ b/packages/buckaroo-js-core/pw-tests/styling-issues-screenshots.spec.ts @@ -0,0 +1,93 @@ +/** + * Playwright screenshot capture for styling-issue stories. + * Follows the theme-screenshots.spec.ts pattern (light-only). + * + * SCREENSHOT_DIR env var controls output directory (default: screenshots/after). + * Run once on each commit to produce "before" and "after" sets: + * + * SCREENSHOT_DIR=screenshots/before npx playwright test pw-tests/styling-issues-screenshots.spec.ts + * SCREENSHOT_DIR=screenshots/after npx playwright test pw-tests/styling-issues-screenshots.spec.ts + */ +import { test } from '@playwright/test'; +import path from 'path'; +import fs from 'fs'; +import { fileURLToPath } from 'url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +const STORYBOOK_BASE = 'http://localhost:6006/iframe.html?viewMode=story&id='; + +// Output directory — overridable via env var +const screenshotsDir = path.resolve( + __dirname, + '..', + process.env.SCREENSHOT_DIR ?? 'screenshots/after', +); + +/** + * All 16 styling-issue stories. + * Story IDs follow Storybook's slug rules: + * title "Buckaroo/DFViewer/StylingIssues" → "buckaroo-dfviewer-stylingissues" + * export name e.g. FewCols_ShortHdr_ShortData → "few-cols-short-hdr-short-data" + * (Storybook runs startCase() on the export name before slugifying) + */ +const STORIES = [ + // Section A – width/contention (#595, #596, #599, #600) + { id: 'buckaroo-dfviewer-stylingissues--few-cols-short-hdr-short-data', name: 'A1_FewCols_ShortHdr_ShortData', issues: '#599' }, + { id: 'buckaroo-dfviewer-stylingissues--few-cols-short-hdr-long-data', name: 'A2_FewCols_ShortHdr_LongData', issues: '' }, + { id: 'buckaroo-dfviewer-stylingissues--few-cols-long-hdr-short-data', name: 'A3_FewCols_LongHdr_ShortData', issues: '' }, + { id: 'buckaroo-dfviewer-stylingissues--few-cols-long-hdr-long-data', name: 'A4_FewCols_LongHdr_LongData', issues: '' }, + { id: 'buckaroo-dfviewer-stylingissues--many-cols-short-hdr-short-data', name: 'A5_ManyCols_ShortHdr_ShortData', issues: '#595 #599' }, + { id: 'buckaroo-dfviewer-stylingissues--many-cols-short-hdr-long-data', name: 'A6_ManyCols_ShortHdr_LongData', issues: '#596' }, + { id: 'buckaroo-dfviewer-stylingissues--many-cols-long-hdr-short-data', name: 'A7_ManyCols_LongHdr_ShortData', issues: '#596' }, + { id: 'buckaroo-dfviewer-stylingissues--many-cols-long-hdr-long-data', name: 'A8_ManyCols_LongHdr_LongData', issues: '#596 worst-case' }, + + // Section B – large numbers / compact_number (#597, #602) + // Note: compact_number stories may render raw values on pre-#597 commits. + { id: 'buckaroo-dfviewer-stylingissues--large-numbers-float', name: 'B9_LargeNumbers_Float', issues: '#597 before' }, + { id: 'buckaroo-dfviewer-stylingissues--large-numbers-compact', name: 'B10_LargeNumbers_Compact', issues: '#597 after' }, + { id: 'buckaroo-dfviewer-stylingissues--clustered-billions-float', name: 'B11_ClusteredBillions_Float', issues: '#602 baseline' }, + { id: 'buckaroo-dfviewer-stylingissues--clustered-billions-compact', name: 'B12_ClusteredBillions_Compact', issues: '#602 precision' }, + + // Section C – pinned row / index alignment (#587) + { id: 'buckaroo-dfviewer-stylingissues--pinned-index-few-cols', name: 'C13_PinnedIndex_FewCols', issues: '#587' }, + { id: 'buckaroo-dfviewer-stylingissues--pinned-index-many-cols', name: 'C14_PinnedIndex_ManyCols', issues: '#587' }, + + // Section D – mixed cross-issue scenarios + { id: 'buckaroo-dfviewer-stylingissues--mixed-many-narrow-with-pinned', name: 'D15_Mixed_ManyNarrow_WithPinned', issues: '#595 #587 #599' }, + { id: 'buckaroo-dfviewer-stylingissues--mixed-few-wide-with-pinned', name: 'D16_Mixed_FewWide_WithPinned', issues: '#587 baseline' }, +]; + +test.beforeAll(() => { + fs.mkdirSync(screenshotsDir, { recursive: true }); +}); + +for (const story of STORIES) { + test(`screenshot ${story.name}`, async ({ page }) => { + await page.emulateMedia({ colorScheme: 'light' }); + await page.goto(`${STORYBOOK_BASE}${story.id}`); + + // Wait for AG-Grid cells or any visible content + const cell = page.locator('.ag-cell'); + const cellWrapper = page.locator('.ag-cell-wrapper'); + const noRows = page.locator('.ag-overlay-no-rows-center'); + const fullWidth = page.locator('.ag-full-width-row'); + const sbContent = page.locator('#storybook-root'); + + await cell + .or(cellWrapper) + .or(noRows) + .or(fullWidth) + .or(sbContent) + .first() + .waitFor({ state: 'visible', timeout: 15000 }); + + // Settle time for animations / lazy column-width calculation + await page.waitForTimeout(800); + + await page.screenshot({ + path: path.join(screenshotsDir, `${story.name}.png`), + fullPage: true, + }); + }); +} diff --git a/packages/buckaroo-js-core/src/stories/StylingIssues.stories.tsx b/packages/buckaroo-js-core/src/stories/StylingIssues.stories.tsx new file mode 100644 index 000000000..9a3cb887d --- /dev/null +++ b/packages/buckaroo-js-core/src/stories/StylingIssues.stories.tsx @@ -0,0 +1,407 @@ +/** + * Stories to reproduce styling / column-width issues: + * #587 – pinned-row index alignment + * #595 / #596 / #599 / #600 – column width contention (few vs many, short vs long headers/data) + * #597 – compact_number displayer for large values + * #602 – compact_number precision loss on clustered billion-scale values + * + * Full 2×2×2 combinatorial matrix for width stories, plus large-number and pinned-row scenarios. + */ +import type { Meta, StoryObj } from "@storybook/react"; +import { DFViewerInfinite } from "../components/DFViewerParts/DFViewerInfinite"; +import { ShadowDomWrapper } from "./StoryUtils"; +import { + DFViewerConfig, + NormalColumnConfig, + PinnedRowConfig, +} from "../components/DFViewerParts/DFWhole"; + +type DFRow = Record; + +// ── Column header name pools ──────────────────────────────────────────────── + +const SHORT_HEADER_NAMES = [ + "a","b","c","d","e","f","g","h","i","j","k","l","m", + "n","o","p","q","r","s","t","u","v","w","x","y", +]; + +const LONG_HEADER_NAMES = [ + "revenue_total","customer_count","margin_pct_adj","transaction_vol", + "active_users_n","retention_rate","avg_order_val","monthly_recur_r", + "churn_pct_adj","lifetime_value","gross_margin_rt","net_promoter_sc", + "conv_rate_pct","acq_cost_avg","rev_per_user_q","visits_monthly", + "bounce_rate_pc","avg_session_s","page_views_tot","email_open_rt", + "click_thru_pct","refund_rate_pc","support_tkts","upsell_rev_q", + "referral_cnt", +]; + +const INDEX_COL: NormalColumnConfig = { + col_name: "index", + header_name: "index", + displayer_args: { displayer: "obj" }, +}; + +// ── Data generators ───────────────────────────────────────────────────────── + +const ROW_COUNT = 20; + +type DataStyle = "short" | "long" | "large" | "clustered"; +type HeaderStyle = "short" | "long"; + +function makeShortVal(row: number, col: number): number { + return ((row * 7 + col * 3 + 1) % 99) + 1; // 1–99 +} + +function makeLongVal(row: number, col: number): number { + return 100_000 + ((row * 1_234_567 + col * 234_567) % 9_900_000); // 100k–9.9M +} + +function makeLargeVal(row: number, col: number): number { + // 3M – 5.7B spread + return 3_000_000 + ((row * 987_654_321 + col * 123_456_789) % 5_697_000_000); +} + +function makeClusteredVal(row: number, col: number): number { + // 5.60B – 5.68B (tight cluster to expose compact_number precision loss) + return 5_600_000_000 + ((row * 12_345 + col * 5_678) % 80_000_000); +} + +function genData(count: number, dataStyle: DataStyle): DFRow[] { + const valFn = + dataStyle === "short" ? makeShortVal : + dataStyle === "long" ? makeLongVal : + dataStyle === "large" ? makeLargeVal : + makeClusteredVal; + + return Array.from({ length: ROW_COUNT }, (_, row) => { + const r: DFRow = { index: row }; + for (let col = 0; col < count; col++) { + r[`col_${col}`] = valFn(row, col); + } + return r; + }); +} + +function genSummary(count: number, dataStyle: DataStyle): DFRow[] { + const colKeys = Array.from({ length: count }, (_, i) => `col_${i}`); + const isFloat = dataStyle === "large" || dataStyle === "clustered"; + const dtype = isFloat ? "float64" : "int64"; + + const row = (key: string, valFn: (i: number) => number | string): DFRow => { + const r: DFRow = { index: key }; + colKeys.forEach((k, i) => { r[k] = valFn(i); }); + return r; + }; + + if (dataStyle === "short") { + return [ + row("dtype", () => dtype), + row("non_null_count",() => ROW_COUNT), + row("mean", (i) => 40 + i * 3), + row("std", (i) => 20 + i), + row("min", (i) => 1 + i), + row("max", (i) => 90 + i), + ]; + } else if (dataStyle === "long") { + return [ + row("dtype", () => dtype), + row("non_null_count",() => ROW_COUNT), + row("mean", (i) => 5_000_000 + i * 100_000), + row("std", (i) => 2_000_000 + i * 50_000), + row("min", (i) => 100_000 + i * 10_000), + row("max", (i) => 9_800_000 + i * 10_000), + ]; + } else if (dataStyle === "large") { + return [ + row("dtype", () => dtype), + row("non_null_count",() => ROW_COUNT), + row("mean", (i) => 2_000_000_000 + i * 100_000_000), + row("std", () => 1_000_000_000), + row("min", (i) => 3_000_000 + i * 1_000_000), + row("max", () => 5_700_000_000), + ]; + } else { + return [ + row("dtype", () => dtype), + row("non_null_count",() => ROW_COUNT), + row("mean", () => 5_640_000_000), + row("std", () => 20_000_000), + row("min", () => 5_600_000_000), + row("max", () => 5_680_000_000), + ]; + } +} + +// ── Config builders ───────────────────────────────────────────────────────── + +function genConfig( + count: number, + headerStyle: HeaderStyle, + dataStyle: DataStyle, + withPinned = false, +): DFViewerConfig { + const headers = + headerStyle === "short" ? SHORT_HEADER_NAMES : LONG_HEADER_NAMES; + + const displayer_args = (): NormalColumnConfig["displayer_args"] => { + if (dataStyle === "large" || dataStyle === "clustered") { + return { displayer: "float", min_fraction_digits: 2, max_fraction_digits: 2 }; + } + return { + displayer: "integer", + min_digits: 1, + max_digits: dataStyle === "short" ? 2 : 7, + }; + }; + + const column_config: NormalColumnConfig[] = Array.from({ length: count }, (_, i) => ({ + col_name: `col_${i}`, + header_name: headers[i % headers.length], + displayer_args: displayer_args(), + })); + + const pinned_rows: PinnedRowConfig[] = withPinned + ? [ + { primary_key_val: "dtype", displayer_args: { displayer: "obj" } }, + { primary_key_val: "non_null_count", displayer_args: { displayer: "inherit" } }, + { primary_key_val: "mean", displayer_args: { displayer: "inherit" } }, + { primary_key_val: "std", displayer_args: { displayer: "inherit" } }, + { primary_key_val: "min", displayer_args: { displayer: "inherit" } }, + { primary_key_val: "max", displayer_args: { displayer: "inherit" } }, + ] + : []; + + return { column_config, left_col_configs: [INDEX_COL], pinned_rows }; +} + +// ── Story factory ─────────────────────────────────────────────────────────── + +function makeStoryComponent( + config: DFViewerConfig, + data: DFRow[], + summary: DFRow[] = [], +) { + const data_wrapper = { data_type: "Raw" as const, data, length: data.length }; + return function StoryInner() { + return ( + +
+ {}} + outside_df_params={{}} + /> +
+
+ ); + }; +} + +// ── Meta ──────────────────────────────────────────────────────────────────── + +// Placeholder component; all stories use render() below +const _Placeholder = () => null; + +const meta = { + title: "Buckaroo/DFViewer/StylingIssues", + component: _Placeholder, + parameters: { layout: "centered" }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// ── Section A: Width / contention (#595, #596, #599, #600) ───────────────── +// Full 2×2×2 = 8 combinations of: col count × header length × data length + +const FewShortShortInner = makeStoryComponent( + genConfig(5, "short", "short"), + genData(5, "short"), +); +/** Baseline – 5 cols, 1-char headers, 1-2 digit values. Should look fine. (#599) */ +export const FewCols_ShortHdr_ShortData: Story = { + render: () => , +}; + +const FewShortLongInner = makeStoryComponent( + genConfig(5, "short", "long"), + genData(5, "long"), +); +/** 5 cols, 1-char headers, 6-7 digit values. Data drives width, no contention. */ +export const FewCols_ShortHdr_LongData: Story = { + render: () => , +}; + +const FewLongShortInner = makeStoryComponent( + genConfig(5, "long", "short"), + genData(5, "short"), +); +/** 5 cols, 12-18 char headers, 1-2 digit values. Header wider than data. */ +export const FewCols_LongHdr_ShortData: Story = { + render: () => , +}; + +const FewLongLongInner = makeStoryComponent( + genConfig(5, "long", "long"), + genData(5, "long"), +); +/** 5 cols, long headers, long data. Both are wide; no contention at 5 cols. */ +export const FewCols_LongHdr_LongData: Story = { + render: () => , +}; + +const ManyShortShortInner = makeStoryComponent( + genConfig(25, "short", "short"), + genData(25, "short"), +); +/** 25 cols, 1-char headers, 1-2 digit values. Primary bug case (#595/#599). */ +export const ManyCols_ShortHdr_ShortData: Story = { + render: () => , +}; + +const ManyShortLongInner = makeStoryComponent( + genConfig(25, "short", "long"), + genData(25, "long"), +); +/** 25 cols, 1-char headers, 6-7 digit values. Data wants space (#596). */ +export const ManyCols_ShortHdr_LongData: Story = { + render: () => , +}; + +const ManyLongShortInner = makeStoryComponent( + genConfig(25, "long", "short"), + genData(25, "short"), +); +/** 25 cols, 12-18 char headers, 1-2 digit values. Headers want space (#596). */ +export const ManyCols_LongHdr_ShortData: Story = { + render: () => , +}; + +const ManyLongLongInner = makeStoryComponent( + genConfig(25, "long", "long"), + genData(25, "long"), +); +/** 25 cols, long headers, long data. Worst-case contention (#596). */ +export const ManyCols_LongHdr_LongData: Story = { + render: () => , +}; + +// ── Section B: Large numbers / compact (#597, #602) ───────────────────────── + +const largeFloatConfig: DFViewerConfig = { + column_config: Array.from({ length: 5 }, (_, i) => ({ + col_name: `col_${i}`, + header_name: SHORT_HEADER_NAMES[i], + displayer_args: { displayer: "float" as const, min_fraction_digits: 2, max_fraction_digits: 2 }, + })), + left_col_configs: [INDEX_COL], + pinned_rows: [], +}; +const largeData = genData(5, "large"); + +const LargeNumbersFloatInner = makeStoryComponent(largeFloatConfig, largeData); +/** 5 cols, values 3M–5.7B, float displayer. Shows why compact is needed (#597). */ +export const LargeNumbers_Float: Story = { + render: () => , +}; + +const largeCompactConfig: DFViewerConfig = { + column_config: Array.from({ length: 5 }, (_, i) => ({ + col_name: `col_${i}`, + header_name: SHORT_HEADER_NAMES[i], + displayer_args: { displayer: "compact_number" as const }, + })), + left_col_configs: [INDEX_COL], + pinned_rows: [], +}; + +const LargeNumbersCompactInner = makeStoryComponent(largeCompactConfig, largeData); +/** Same data as LargeNumbers_Float but using compact_number displayer (#597 fix). */ +export const LargeNumbers_Compact: Story = { + render: () => , +}; + +const clusteredData = genData(5, "clustered"); + +const clusteredFloatConfig: DFViewerConfig = { + column_config: Array.from({ length: 5 }, (_, i) => ({ + col_name: `col_${i}`, + header_name: SHORT_HEADER_NAMES[i], + displayer_args: { displayer: "float" as const, min_fraction_digits: 2, max_fraction_digits: 2 }, + })), + left_col_configs: [INDEX_COL], + pinned_rows: [], +}; + +const ClusteredBillionsFloatInner = makeStoryComponent(clusteredFloatConfig, clusteredData); +/** 5 cols, values tightly clustered 5.60B–5.68B, float displayer (#602 baseline). */ +export const ClusteredBillions_Float: Story = { + render: () => , +}; + +const clusteredCompactConfig: DFViewerConfig = { + column_config: Array.from({ length: 5 }, (_, i) => ({ + col_name: `col_${i}`, + header_name: SHORT_HEADER_NAMES[i], + displayer_args: { displayer: "compact_number" as const }, + })), + left_col_configs: [INDEX_COL], + pinned_rows: [], +}; + +const ClusteredBillionsCompactInner = makeStoryComponent(clusteredCompactConfig, clusteredData); +/** Same clustered data with compact_number – exposes precision loss (#602). */ +export const ClusteredBillions_Compact: Story = { + render: () => , +}; + +// ── Section C: Pinned row / index (#587) ──────────────────────────────────── + +const pinnedFewCfg = genConfig(5, "short", "short", true); +const pinnedFewData = genData(5, "short"); +const pinnedFewSummary = genSummary(5, "short"); + +const PinnedIndexFewInner = makeStoryComponent(pinnedFewCfg, pinnedFewData, pinnedFewSummary); +/** 5 numeric cols + pinned summary stats + left index. Tests #587 alignment. */ +export const PinnedIndex_FewCols: Story = { + render: () => , +}; + +const pinnedManyCfg = genConfig(15, "short", "short", true); +const pinnedManyData = genData(15, "short"); +const pinnedManySummary = genSummary(15, "short"); + +const PinnedIndexManyInner = makeStoryComponent(pinnedManyCfg, pinnedManyData, pinnedManySummary); +/** 15 numeric cols + pinned summary stats. #587 alignment under width contention. */ +export const PinnedIndex_ManyCols: Story = { + render: () => , +}; + +// ── Section D: Mixed scenarios ─────────────────────────────────────────────── + +const mixedManyNarrowCfg = genConfig(20, "short", "short", true); +const mixedManyNarrowData = genData(20, "short"); +const mixedManyNarrowSummary = genSummary(20, "short"); + +const MixedManyNarrowInner = makeStoryComponent( + mixedManyNarrowCfg, mixedManyNarrowData, mixedManyNarrowSummary, +); +/** 20 narrow cols + pinned rows. Cross-issue: #595 + #587 + #599. */ +export const Mixed_ManyNarrow_WithPinned: Story = { + render: () => , +}; + +const mixedFewWideCfg = genConfig(5, "long", "long", true); +const mixedFewWideData = genData(5, "long"); +const mixedFewWideSummary = genSummary(5, "long"); + +const MixedFewWideInner = makeStoryComponent( + mixedFewWideCfg, mixedFewWideData, mixedFewWideSummary, +); +/** 5 wide cols + pinned rows. #587 baseline (should look fine). */ +export const Mixed_FewWide_WithPinned: Story = { + render: () => , +}; diff --git a/scripts/download_styling_screenshots.sh b/scripts/download_styling_screenshots.sh new file mode 100755 index 000000000..b481ed847 --- /dev/null +++ b/scripts/download_styling_screenshots.sh @@ -0,0 +1,53 @@ +#!/usr/bin/env bash +# Download before/after styling screenshots from a CI run. +# +# Usage: +# ./scripts/download_styling_screenshots.sh [run-id] +# +# If run-id is omitted, uses the latest successful run of checks.yml +# on the current branch. + +set -e + +REPO=$(gh repo view --json nameWithOwner -q '.nameWithOwner') +BRANCH=$(git rev-parse --abbrev-ref HEAD) +RUN_ID=${1:-} + +if [ -z "$RUN_ID" ]; then + echo "Looking up latest successful run on branch '$BRANCH'..." + RUN_ID=$(gh run list \ + --repo "$REPO" \ + --branch "$BRANCH" \ + --workflow checks.yml \ + --status success \ + --limit 1 \ + --json databaseId \ + -q '.[0].databaseId') + + if [ -z "$RUN_ID" ]; then + echo "No successful run found for branch '$BRANCH'. Try passing a run-id directly." + exit 1 + fi +fi + +echo "Downloading screenshots from run $RUN_ID (repo: $REPO)" + +OUT_DIR="packages/buckaroo-js-core/screenshots" +mkdir -p "$OUT_DIR" + +gh run download "$RUN_ID" \ + --repo "$REPO" \ + --name styling-screenshots-before \ + --dir "$OUT_DIR/before" + +gh run download "$RUN_ID" \ + --repo "$REPO" \ + --name styling-screenshots-after \ + --dir "$OUT_DIR/after" + +BEFORE=$(ls -1 "$OUT_DIR/before"/*.png 2>/dev/null | wc -l | tr -d ' ') +AFTER=$(ls -1 "$OUT_DIR/after"/*.png 2>/dev/null | wc -l | tr -d ' ') +echo "Downloaded: $BEFORE before screenshots, $AFTER after screenshots" +echo "" +echo "Next step:" +echo " python scripts/gen_screenshot_compare.py && open packages/buckaroo-js-core/screenshots/compare.html" diff --git a/scripts/gen_screenshot_compare.py b/scripts/gen_screenshot_compare.py new file mode 100644 index 000000000..164dd9e0d --- /dev/null +++ b/scripts/gen_screenshot_compare.py @@ -0,0 +1,566 @@ +#!/usr/bin/env python3 +""" +Generate a self-contained before/after screenshot comparison HTML page. + +Usage: + python scripts/gen_screenshot_compare.py + +Reads screenshots from: + packages/buckaroo-js-core/screenshots/before/*.png + packages/buckaroo-js-core/screenshots/after/*.png + +Writes: + packages/buckaroo-js-core/screenshots/compare.html +""" +import base64 +import json +import sys +from pathlib import Path + +SCRIPT_DIR = Path(__file__).parent +REPO_ROOT = SCRIPT_DIR.parent +SCREENSHOTS_DIR = REPO_ROOT / "packages" / "buckaroo-js-core" / "screenshots" +BEFORE_DIR = SCREENSHOTS_DIR / "before" +AFTER_DIR = SCREENSHOTS_DIR / "after" +OUTPUT = SCREENSHOTS_DIR / "compare.html" + +# Stories in display order, with section headings and issue labels +STORIES = [ + # Section A + ("A – Width / Contention (#595, #596, #599, #600)", [ + ("A1_FewCols_ShortHdr_ShortData", "#599 baseline"), + ("A2_FewCols_ShortHdr_LongData", "few cols, long data"), + ("A3_FewCols_LongHdr_ShortData", "few cols, long headers"), + ("A4_FewCols_LongHdr_LongData", "few cols, both wide"), + ("A5_ManyCols_ShortHdr_ShortData", "#595 #599 primary bug"), + ("A6_ManyCols_ShortHdr_LongData", "#596 data contention"), + ("A7_ManyCols_LongHdr_ShortData", "#596 header contention"), + ("A8_ManyCols_LongHdr_LongData", "#596 worst case"), + ]), + # Section B + ("B – Large Numbers / compact_number (#597, #602)", [ + ("B9_LargeNumbers_Float", "#597 – float displayer (before)"), + ("B10_LargeNumbers_Compact", "#597 – compact_number (after)"), + ("B11_ClusteredBillions_Float", "#602 – clustered, float"), + ("B12_ClusteredBillions_Compact", "#602 – clustered, compact (precision loss)"), + ]), + # Section C + ("C – Pinned Row / Index Alignment (#587)", [ + ("C13_PinnedIndex_FewCols", "#587 – 5 cols"), + ("C14_PinnedIndex_ManyCols", "#587 – 15 cols"), + ]), + # Section D + ("D – Mixed Scenarios", [ + ("D15_Mixed_ManyNarrow_WithPinned", "#595 #587 #599"), + ("D16_Mixed_FewWide_WithPinned", "#587 baseline"), + ]), +] + + +def img_data_uri(path: Path) -> str: + if not path.exists(): + return "" + b64 = base64.b64encode(path.read_bytes()).decode() + return f"data:image/png;base64,{b64}" + + +def build_html() -> str: + # Build flat story list with embedded images for JS consumption + flat: list[dict] = [] + for section_title, stories in STORIES: + for name, label in stories: + flat.append({ + "name": name, + "label": label, + "section": section_title, + "before": img_data_uri(BEFORE_DIR / f"{name}.png"), + "after": img_data_uri(AFTER_DIR / f"{name}.png"), + }) + + stories_json = json.dumps(flat) + + # Build nav items HTML (section headers + story entries) + nav_items = [] + current_section = None + for i, entry in enumerate(flat): + if entry["section"] != current_section: + current_section = entry["section"] + nav_items.append( + f'' + ) + short = entry["name"].split("_", 1)[1].replace("_", " ") if "_" in entry["name"] else entry["name"] + nav_items.append( + f'' + ) + nav_html = "\n".join(nav_items) + + return f""" + + + +Styling Issues: Before / After + + + +
+ + + + + +
+
+ + +
+ +
+
+ before + before + +
+
+ after + after + +
+
+
+ +
+ + + +""" + + +if __name__ == "__main__": + if not BEFORE_DIR.exists() and not AFTER_DIR.exists(): + print( + "No screenshots found.\n" + "Run:\n" + " ./scripts/download_styling_screenshots.sh\n" + "or capture locally with:\n" + " cd packages/buckaroo-js-core && " + "SCREENSHOT_DIR=screenshots/after npx playwright test pw-tests/styling-issues-screenshots.spec.ts", + file=sys.stderr, + ) + sys.exit(1) + + html = build_html() + OUTPUT.write_text(html, encoding="utf-8") + print(f"Written: {OUTPUT}") + + before_count = len(list(BEFORE_DIR.glob("*.png"))) if BEFORE_DIR.exists() else 0 + after_count = len(list(AFTER_DIR.glob("*.png"))) if AFTER_DIR.exists() else 0 + print(f" before: {before_count} screenshots") + print(f" after: {after_count} screenshots") From 135bc52ae592b6d0f412fdb00cde4364c8b0a2da Mon Sep 17 00:00:00 2001 From: Paddy Mullen Date: Fri, 27 Feb 2026 14:44:22 -0500 Subject: [PATCH 02/15] fix: checkout PR head SHA for screenshots, not merge ref Co-Authored-By: Claude Opus 4.6 --- .github/workflows/checks.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index fd756695c..3148e82f8 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -413,6 +413,8 @@ jobs: timeout-minutes: 15 steps: - uses: actions/checkout@v6 + with: + ref: ${{ github.event.pull_request.head.sha || github.sha }} - name: Setup pnpm uses: pnpm/action-setup@v4 with: @@ -424,7 +426,7 @@ jobs: cache-dependency-path: 'packages/pnpm-lock.yaml' - name: Install pnpm dependencies working-directory: packages - run: pnpm install --frozen-lockfile + run: pnpm install --no-frozen-lockfile - name: Cache Playwright browsers uses: actions/cache@v5 with: From 137f480490ccc0b5c48c7b0b6c39e40e4958fcf5 Mon Sep 17 00:00:00 2001 From: Paddy Mullen Date: Fri, 27 Feb 2026 14:53:40 -0500 Subject: [PATCH 03/15] fix: scroll grid body in pinned-index screenshots to expose #587 Co-Authored-By: Claude Opus 4.6 --- .../pw-tests/styling-issues-screenshots.spec.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/buckaroo-js-core/pw-tests/styling-issues-screenshots.spec.ts b/packages/buckaroo-js-core/pw-tests/styling-issues-screenshots.spec.ts index 2d5e82915..d7c83e452 100644 --- a/packages/buckaroo-js-core/pw-tests/styling-issues-screenshots.spec.ts +++ b/packages/buckaroo-js-core/pw-tests/styling-issues-screenshots.spec.ts @@ -85,6 +85,14 @@ for (const story of STORIES) { // Settle time for animations / lazy column-width calculation await page.waitForTimeout(800); + // For pinned-index stories, scroll the grid body right so the + // index column is out of view — exposes #587 alignment bug. + if (story.name.includes('Pinned')) { + const viewport = page.locator('.ag-body-viewport'); + await viewport.evaluate((el) => { el.scrollLeft = 200; }); + await page.waitForTimeout(400); + } + await page.screenshot({ path: path.join(screenshotsDir, `${story.name}.png`), fullPage: true, From bed58fa02e7cc1a82a3638fca66a69ca1d2d9fd2 Mon Sep 17 00:00:00 2001 From: Paddy Mullen Date: Fri, 27 Feb 2026 15:07:45 -0500 Subject: [PATCH 04/15] fix: pierce shadow DOM for pinned-index scroll in screenshots Co-Authored-By: Claude Opus 4.6 --- .../pw-tests/styling-issues-screenshots.spec.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/buckaroo-js-core/pw-tests/styling-issues-screenshots.spec.ts b/packages/buckaroo-js-core/pw-tests/styling-issues-screenshots.spec.ts index d7c83e452..ee1f9491b 100644 --- a/packages/buckaroo-js-core/pw-tests/styling-issues-screenshots.spec.ts +++ b/packages/buckaroo-js-core/pw-tests/styling-issues-screenshots.spec.ts @@ -87,9 +87,16 @@ for (const story of STORIES) { // For pinned-index stories, scroll the grid body right so the // index column is out of view — exposes #587 alignment bug. + // Grid is inside a Shadow DOM, so we pierce it via evaluate. if (story.name.includes('Pinned')) { - const viewport = page.locator('.ag-body-viewport'); - await viewport.evaluate((el) => { el.scrollLeft = 200; }); + await page.evaluate(() => { + const host = document.querySelector('div[ref]') ?? + document.querySelector('#storybook-root > div'); + const shadow = host?.shadowRoot; + const vp = shadow?.querySelector('.ag-body-viewport') ?? + document.querySelector('.ag-body-viewport'); + if (vp) vp.scrollLeft = 400; + }); await page.waitForTimeout(400); } From 594dc74435d89d507ab858b35cdff12281dbd3ec Mon Sep 17 00:00:00 2001 From: Paddy Mullen Date: Fri, 27 Feb 2026 15:16:58 -0500 Subject: [PATCH 05/15] fix: walk all shadow roots to find ag-body-viewport for scroll Co-Authored-By: Claude Opus 4.6 --- .../pw-tests/styling-issues-screenshots.spec.ts | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/packages/buckaroo-js-core/pw-tests/styling-issues-screenshots.spec.ts b/packages/buckaroo-js-core/pw-tests/styling-issues-screenshots.spec.ts index ee1f9491b..67137ad80 100644 --- a/packages/buckaroo-js-core/pw-tests/styling-issues-screenshots.spec.ts +++ b/packages/buckaroo-js-core/pw-tests/styling-issues-screenshots.spec.ts @@ -90,11 +90,16 @@ for (const story of STORIES) { // Grid is inside a Shadow DOM, so we pierce it via evaluate. if (story.name.includes('Pinned')) { await page.evaluate(() => { - const host = document.querySelector('div[ref]') ?? - document.querySelector('#storybook-root > div'); - const shadow = host?.shadowRoot; - const vp = shadow?.querySelector('.ag-body-viewport') ?? - document.querySelector('.ag-body-viewport'); + // Walk all elements looking for shadow roots containing AG-Grid + const allEls = document.querySelectorAll('*'); + for (const el of allEls) { + if (el.shadowRoot) { + const vp = el.shadowRoot.querySelector('.ag-body-viewport'); + if (vp) { vp.scrollLeft = 400; return; } + } + } + // Fallback: no shadow DOM + const vp = document.querySelector('.ag-body-viewport'); if (vp) vp.scrollLeft = 400; }); await page.waitForTimeout(400); From 87a3d5b749ffd9ae66ee9c336abeb339b4b171bd Mon Sep 17 00:00:00 2001 From: Paddy Mullen Date: Fri, 27 Feb 2026 15:24:50 -0500 Subject: [PATCH 06/15] fix: use long headers in pinned stories so grid requires horizontal scroll Co-Authored-By: Claude Opus 4.6 --- .../styling-issues-screenshots.spec.ts | 17 +++-------------- .../src/stories/StylingIssues.stories.tsx | 18 +++++++++--------- 2 files changed, 12 insertions(+), 23 deletions(-) diff --git a/packages/buckaroo-js-core/pw-tests/styling-issues-screenshots.spec.ts b/packages/buckaroo-js-core/pw-tests/styling-issues-screenshots.spec.ts index 67137ad80..4d40c50b0 100644 --- a/packages/buckaroo-js-core/pw-tests/styling-issues-screenshots.spec.ts +++ b/packages/buckaroo-js-core/pw-tests/styling-issues-screenshots.spec.ts @@ -87,21 +87,10 @@ for (const story of STORIES) { // For pinned-index stories, scroll the grid body right so the // index column is out of view — exposes #587 alignment bug. - // Grid is inside a Shadow DOM, so we pierce it via evaluate. + // Playwright CSS locators pierce shadow DOM by default. if (story.name.includes('Pinned')) { - await page.evaluate(() => { - // Walk all elements looking for shadow roots containing AG-Grid - const allEls = document.querySelectorAll('*'); - for (const el of allEls) { - if (el.shadowRoot) { - const vp = el.shadowRoot.querySelector('.ag-body-viewport'); - if (vp) { vp.scrollLeft = 400; return; } - } - } - // Fallback: no shadow DOM - const vp = document.querySelector('.ag-body-viewport'); - if (vp) vp.scrollLeft = 400; - }); + const viewport = page.locator('.ag-body-viewport').first(); + await viewport.evaluate((el) => { el.scrollLeft = el.scrollWidth; }); await page.waitForTimeout(400); } diff --git a/packages/buckaroo-js-core/src/stories/StylingIssues.stories.tsx b/packages/buckaroo-js-core/src/stories/StylingIssues.stories.tsx index 9a3cb887d..54ccdcd33 100644 --- a/packages/buckaroo-js-core/src/stories/StylingIssues.stories.tsx +++ b/packages/buckaroo-js-core/src/stories/StylingIssues.stories.tsx @@ -360,29 +360,29 @@ export const ClusteredBillions_Compact: Story = { // ── Section C: Pinned row / index (#587) ──────────────────────────────────── -const pinnedFewCfg = genConfig(5, "short", "short", true); -const pinnedFewData = genData(5, "short"); -const pinnedFewSummary = genSummary(5, "short"); +const pinnedFewCfg = genConfig(10, "long", "short", true); +const pinnedFewData = genData(10, "short"); +const pinnedFewSummary = genSummary(10, "short"); const PinnedIndexFewInner = makeStoryComponent(pinnedFewCfg, pinnedFewData, pinnedFewSummary); -/** 5 numeric cols + pinned summary stats + left index. Tests #587 alignment. */ +/** 10 long-header cols + pinned summary stats + left index. Tests #587 alignment. */ export const PinnedIndex_FewCols: Story = { render: () => , }; -const pinnedManyCfg = genConfig(15, "short", "short", true); -const pinnedManyData = genData(15, "short"); -const pinnedManySummary = genSummary(15, "short"); +const pinnedManyCfg = genConfig(20, "long", "short", true); +const pinnedManyData = genData(20, "short"); +const pinnedManySummary = genSummary(20, "short"); const PinnedIndexManyInner = makeStoryComponent(pinnedManyCfg, pinnedManyData, pinnedManySummary); -/** 15 numeric cols + pinned summary stats. #587 alignment under width contention. */ +/** 20 long-header cols + pinned summary stats. #587 alignment under width contention. */ export const PinnedIndex_ManyCols: Story = { render: () => , }; // ── Section D: Mixed scenarios ─────────────────────────────────────────────── -const mixedManyNarrowCfg = genConfig(20, "short", "short", true); +const mixedManyNarrowCfg = genConfig(20, "long", "short", true); const mixedManyNarrowData = genData(20, "short"); const mixedManyNarrowSummary = genSummary(20, "short"); From 9a5a30f4f78eee8a1475ad075bbc8371d9f9d0b2 Mon Sep 17 00:00:00 2001 From: Paddy Mullen Date: Fri, 27 Feb 2026 15:55:09 -0500 Subject: [PATCH 07/15] fix: use 400px container for pinned stories to force horizontal scroll Co-Authored-By: Claude Opus 4.6 --- .../styling-issues-screenshots.spec.ts | 28 +++++++++++++++++-- .../src/stories/StylingIssues.stories.tsx | 11 ++++---- 2 files changed, 31 insertions(+), 8 deletions(-) diff --git a/packages/buckaroo-js-core/pw-tests/styling-issues-screenshots.spec.ts b/packages/buckaroo-js-core/pw-tests/styling-issues-screenshots.spec.ts index 4d40c50b0..b27785459 100644 --- a/packages/buckaroo-js-core/pw-tests/styling-issues-screenshots.spec.ts +++ b/packages/buckaroo-js-core/pw-tests/styling-issues-screenshots.spec.ts @@ -87,10 +87,32 @@ for (const story of STORIES) { // For pinned-index stories, scroll the grid body right so the // index column is out of view — exposes #587 alignment bug. - // Playwright CSS locators pierce shadow DOM by default. if (story.name.includes('Pinned')) { - const viewport = page.locator('.ag-body-viewport').first(); - await viewport.evaluate((el) => { el.scrollLeft = el.scrollWidth; }); + const scrolled = await page.evaluate(() => { + // Playwright locators pierce shadow DOM but evaluate doesn't. + // Manually walk shadow roots to find the AG-Grid viewport. + function findInShadow(selector: string): Element | null { + const light = document.querySelector(selector); + if (light) return light; + const walk = (root: Document | ShadowRoot): Element | null => { + const el = root.querySelector(selector); + if (el) return el; + for (const child of root.querySelectorAll('*')) { + if (child.shadowRoot) { + const found = walk(child.shadowRoot); + if (found) return found; + } + } + return null; + }; + return walk(document); + } + const vp = findInShadow('.ag-body-viewport'); + if (!vp) return 'not found'; + vp.scrollLeft = vp.scrollWidth; + return `scrolled to ${vp.scrollLeft} of ${vp.scrollWidth} (client: ${vp.clientWidth})`; + }); + console.log(`Scroll result for ${story.name}: ${scrolled}`); await page.waitForTimeout(400); } diff --git a/packages/buckaroo-js-core/src/stories/StylingIssues.stories.tsx b/packages/buckaroo-js-core/src/stories/StylingIssues.stories.tsx index 54ccdcd33..7f7707668 100644 --- a/packages/buckaroo-js-core/src/stories/StylingIssues.stories.tsx +++ b/packages/buckaroo-js-core/src/stories/StylingIssues.stories.tsx @@ -180,12 +180,13 @@ function makeStoryComponent( config: DFViewerConfig, data: DFRow[], summary: DFRow[] = [], + width = 800, ) { const data_wrapper = { data_type: "Raw" as const, data, length: data.length }; return function StoryInner() { return ( -
+
, @@ -374,7 +375,7 @@ const pinnedManyCfg = genConfig(20, "long", "short", true); const pinnedManyData = genData(20, "short"); const pinnedManySummary = genSummary(20, "short"); -const PinnedIndexManyInner = makeStoryComponent(pinnedManyCfg, pinnedManyData, pinnedManySummary); +const PinnedIndexManyInner = makeStoryComponent(pinnedManyCfg, pinnedManyData, pinnedManySummary, 400); /** 20 long-header cols + pinned summary stats. #587 alignment under width contention. */ export const PinnedIndex_ManyCols: Story = { render: () => , @@ -387,7 +388,7 @@ const mixedManyNarrowData = genData(20, "short"); const mixedManyNarrowSummary = genSummary(20, "short"); const MixedManyNarrowInner = makeStoryComponent( - mixedManyNarrowCfg, mixedManyNarrowData, mixedManyNarrowSummary, + mixedManyNarrowCfg, mixedManyNarrowData, mixedManyNarrowSummary, 400, ); /** 20 narrow cols + pinned rows. Cross-issue: #595 + #587 + #599. */ export const Mixed_ManyNarrow_WithPinned: Story = { @@ -399,7 +400,7 @@ const mixedFewWideData = genData(5, "long"); const mixedFewWideSummary = genSummary(5, "long"); const MixedFewWideInner = makeStoryComponent( - mixedFewWideCfg, mixedFewWideData, mixedFewWideSummary, + mixedFewWideCfg, mixedFewWideData, mixedFewWideSummary, 400, ); /** 5 wide cols + pinned rows. #587 baseline (should look fine). */ export const Mixed_FewWide_WithPinned: Story = { From 5dae69fc7e560105d1af03e562b52dca3484489b Mon Sep 17 00:00:00 2001 From: Paddy Mullen Date: Fri, 27 Feb 2026 16:05:55 -0500 Subject: [PATCH 08/15] fix: add minWidth:120 via ag_grid_specs on pinned stories to force overflow Co-Authored-By: Claude Opus 4.6 --- packages/buckaroo-js-core/src/stories/StylingIssues.stories.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/buckaroo-js-core/src/stories/StylingIssues.stories.tsx b/packages/buckaroo-js-core/src/stories/StylingIssues.stories.tsx index 7f7707668..850d22e3a 100644 --- a/packages/buckaroo-js-core/src/stories/StylingIssues.stories.tsx +++ b/packages/buckaroo-js-core/src/stories/StylingIssues.stories.tsx @@ -158,6 +158,7 @@ function genConfig( col_name: `col_${i}`, header_name: headers[i % headers.length], displayer_args: displayer_args(), + ...(withPinned ? { ag_grid_specs: { minWidth: 120 } } : {}), })); const pinned_rows: PinnedRowConfig[] = withPinned From b4c214dda3261504dfb87961419c5288cdb33001 Mon Sep 17 00:00:00 2001 From: Paddy Mullen Date: Fri, 27 Feb 2026 16:13:49 -0500 Subject: [PATCH 09/15] fix: use keyboard End key to scroll pinned stories to rightmost column Co-Authored-By: Claude Opus 4.6 --- .../styling-issues-screenshots.spec.ts | 32 ++++--------------- 1 file changed, 7 insertions(+), 25 deletions(-) diff --git a/packages/buckaroo-js-core/pw-tests/styling-issues-screenshots.spec.ts b/packages/buckaroo-js-core/pw-tests/styling-issues-screenshots.spec.ts index b27785459..8f53e43c1 100644 --- a/packages/buckaroo-js-core/pw-tests/styling-issues-screenshots.spec.ts +++ b/packages/buckaroo-js-core/pw-tests/styling-issues-screenshots.spec.ts @@ -87,32 +87,14 @@ for (const story of STORIES) { // For pinned-index stories, scroll the grid body right so the // index column is out of view — exposes #587 alignment bug. + // Use keyboard End key to scroll to the rightmost column. if (story.name.includes('Pinned')) { - const scrolled = await page.evaluate(() => { - // Playwright locators pierce shadow DOM but evaluate doesn't. - // Manually walk shadow roots to find the AG-Grid viewport. - function findInShadow(selector: string): Element | null { - const light = document.querySelector(selector); - if (light) return light; - const walk = (root: Document | ShadowRoot): Element | null => { - const el = root.querySelector(selector); - if (el) return el; - for (const child of root.querySelectorAll('*')) { - if (child.shadowRoot) { - const found = walk(child.shadowRoot); - if (found) return found; - } - } - return null; - }; - return walk(document); - } - const vp = findInShadow('.ag-body-viewport'); - if (!vp) return 'not found'; - vp.scrollLeft = vp.scrollWidth; - return `scrolled to ${vp.scrollLeft} of ${vp.scrollWidth} (client: ${vp.clientWidth})`; - }); - console.log(`Scroll result for ${story.name}: ${scrolled}`); + // Click a cell first to give the grid focus + const firstCell = page.locator('.ag-cell').first(); + await firstCell.click(); + await page.waitForTimeout(200); + // Press End to scroll to the last column + await page.keyboard.press('End'); await page.waitForTimeout(400); } From f123635a49bd3459856af4aa51ea4d28798127f8 Mon Sep 17 00:00:00 2001 From: Paddy Mullen Date: Fri, 27 Feb 2026 16:30:00 -0500 Subject: [PATCH 10/15] =?UTF-8?q?feat:=20add=20A9=20story=20for=20#595=20?= =?UTF-8?q?=E2=80=94=20many=20cols,=20long=20headers,=20year-like=20data?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.6 --- .../styling-issues-screenshots.spec.ts | 1 + .../src/stories/StylingIssues.stories.tsx | 31 +++++++++++++++++-- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/packages/buckaroo-js-core/pw-tests/styling-issues-screenshots.spec.ts b/packages/buckaroo-js-core/pw-tests/styling-issues-screenshots.spec.ts index 8f53e43c1..efc21dad4 100644 --- a/packages/buckaroo-js-core/pw-tests/styling-issues-screenshots.spec.ts +++ b/packages/buckaroo-js-core/pw-tests/styling-issues-screenshots.spec.ts @@ -41,6 +41,7 @@ const STORIES = [ { id: 'buckaroo-dfviewer-stylingissues--many-cols-short-hdr-long-data', name: 'A6_ManyCols_ShortHdr_LongData', issues: '#596' }, { id: 'buckaroo-dfviewer-stylingissues--many-cols-long-hdr-short-data', name: 'A7_ManyCols_LongHdr_ShortData', issues: '#596' }, { id: 'buckaroo-dfviewer-stylingissues--many-cols-long-hdr-long-data', name: 'A8_ManyCols_LongHdr_LongData', issues: '#596 worst-case' }, + { id: 'buckaroo-dfviewer-stylingissues--many-cols-long-hdr-year-data', name: 'A9_ManyCols_LongHdr_YearData', issues: '#595 primary' }, // Section B – large numbers / compact_number (#597, #602) // Note: compact_number stories may render raw values on pre-#597 commits. diff --git a/packages/buckaroo-js-core/src/stories/StylingIssues.stories.tsx b/packages/buckaroo-js-core/src/stories/StylingIssues.stories.tsx index 850d22e3a..1f7098475 100644 --- a/packages/buckaroo-js-core/src/stories/StylingIssues.stories.tsx +++ b/packages/buckaroo-js-core/src/stories/StylingIssues.stories.tsx @@ -45,7 +45,7 @@ const INDEX_COL: NormalColumnConfig = { const ROW_COUNT = 20; -type DataStyle = "short" | "long" | "large" | "clustered"; +type DataStyle = "short" | "long" | "large" | "clustered" | "year"; type HeaderStyle = "short" | "long"; function makeShortVal(row: number, col: number): number { @@ -66,11 +66,17 @@ function makeClusteredVal(row: number, col: number): number { return 5_600_000_000 + ((row * 12_345 + col * 5_678) % 80_000_000); } +function makeYearVal(row: number, col: number): number { + // 4-digit year-like values — narrow content that triggers #595 + return 2000 + ((row * 3 + col * 7) % 25); +} + function genData(count: number, dataStyle: DataStyle): DFRow[] { const valFn = dataStyle === "short" ? makeShortVal : dataStyle === "long" ? makeLongVal : dataStyle === "large" ? makeLargeVal : + dataStyle === "year" ? makeYearVal : makeClusteredVal; return Array.from({ length: ROW_COUNT }, (_, row) => { @@ -93,7 +99,16 @@ function genSummary(count: number, dataStyle: DataStyle): DFRow[] { return r; }; - if (dataStyle === "short") { + if (dataStyle === "year") { + return [ + row("dtype", () => dtype), + row("non_null_count",() => ROW_COUNT), + row("mean", () => 2012), + row("std", () => 7), + row("min", () => 2000), + row("max", () => 2024), + ]; + } else if (dataStyle === "short") { return [ row("dtype", () => dtype), row("non_null_count",() => ROW_COUNT), @@ -150,7 +165,7 @@ function genConfig( return { displayer: "integer", min_digits: 1, - max_digits: dataStyle === "short" ? 2 : 7, + max_digits: dataStyle === "year" ? 4 : dataStyle === "short" ? 2 : 7, }; }; @@ -291,6 +306,16 @@ export const ManyCols_LongHdr_LongData: Story = { render: () => , }; +const ManyLongYearInner = makeStoryComponent( + genConfig(15, "long", "year"), + genData(15, "year"), +); +/** 15 cols, long headers, 4-digit year values. #595 primary repro — narrow + * content causes fitCellContents to crush columns, truncating headers. */ +export const ManyCols_LongHdr_YearData: Story = { + render: () => , +}; + // ── Section B: Large numbers / compact (#597, #602) ───────────────────────── const largeFloatConfig: DFViewerConfig = { From 2b4c1e7512b5272497089df9ff79c3444e97fd8f Mon Sep 17 00:00:00 2001 From: Paddy Mullen Date: Fri, 27 Feb 2026 16:46:39 -0500 Subject: [PATCH 11/15] =?UTF-8?q?fix:=20A9=20story=20=E2=80=94=2025=20long?= =?UTF-8?q?=20cols=20in=20400px=20to=20force=20data=20truncation=20(#595)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.6 --- .../src/stories/StylingIssues.stories.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/buckaroo-js-core/src/stories/StylingIssues.stories.tsx b/packages/buckaroo-js-core/src/stories/StylingIssues.stories.tsx index 1f7098475..89bed23d5 100644 --- a/packages/buckaroo-js-core/src/stories/StylingIssues.stories.tsx +++ b/packages/buckaroo-js-core/src/stories/StylingIssues.stories.tsx @@ -307,11 +307,13 @@ export const ManyCols_LongHdr_LongData: Story = { }; const ManyLongYearInner = makeStoryComponent( - genConfig(15, "long", "year"), - genData(15, "year"), + genConfig(25, "long", "long"), + genData(25, "long"), + [], + 400, ); -/** 15 cols, long headers, 4-digit year values. #595 primary repro — narrow - * content causes fitCellContents to crush columns, truncating headers. */ +/** 25 cols, long headers, 6-7 digit values in 400px. #595 repro — + * fitCellContents crushes columns, data values show "...". */ export const ManyCols_LongHdr_YearData: Story = { render: () => , }; From 72abf2f941efd0e366177b6c4c5c80c26e64c6ca Mon Sep 17 00:00:00 2001 From: Paddy Mullen Date: Fri, 27 Feb 2026 16:52:39 -0500 Subject: [PATCH 12/15] =?UTF-8?q?fix:=20A9=20=E2=80=94=20force=20maxWidth:?= =?UTF-8?q?50=20on=20columns=20to=20reproduce=20#595=20truncation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fitCellContents never crushes columns, so use ag_grid_specs maxWidth to cap columns at 50px. 7-digit data should show "..." truncation. Co-Authored-By: Claude Opus 4.6 --- .../src/stories/StylingIssues.stories.tsx | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/packages/buckaroo-js-core/src/stories/StylingIssues.stories.tsx b/packages/buckaroo-js-core/src/stories/StylingIssues.stories.tsx index 89bed23d5..4e4a7e5cc 100644 --- a/packages/buckaroo-js-core/src/stories/StylingIssues.stories.tsx +++ b/packages/buckaroo-js-core/src/stories/StylingIssues.stories.tsx @@ -306,16 +306,24 @@ export const ManyCols_LongHdr_LongData: Story = { render: () => , }; -const ManyLongYearInner = makeStoryComponent( - genConfig(25, "long", "long"), - genData(25, "long"), - [], - 400, +// #595 repro: force narrow columns so data values show "..." +const narrowColConfig: DFViewerConfig = { + column_config: Array.from({ length: 15 }, (_, i) => ({ + col_name: `col_${i}`, + header_name: LONG_HEADER_NAMES[i], + displayer_args: { displayer: "integer" as const, min_digits: 1, max_digits: 7 }, + ag_grid_specs: { maxWidth: 50 }, + })), + left_col_configs: [INDEX_COL], + pinned_rows: [], +}; +const NarrowColInner = makeStoryComponent( + narrowColConfig, + genData(15, "long"), ); -/** 25 cols, long headers, 6-7 digit values in 400px. #595 repro — - * fitCellContents crushes columns, data values show "...". */ +/** 15 cols capped at 50px with 7-digit data. #595 repro — values show "...". */ export const ManyCols_LongHdr_YearData: Story = { - render: () => , + render: () => , }; // ── Section B: Large numbers / compact (#597, #602) ───────────────────────── From b74e0edca2447a406f5103b6df8249310611bdfb Mon Sep 17 00:00:00 2001 From: Paddy Mullen Date: Fri, 27 Feb 2026 17:26:09 -0500 Subject: [PATCH 13/15] fix: update A9 story to use width:40 + suppressAutoSize for #595 repro MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Same story definition as the fix branch, but baseline has no defaultColDef.minWidth so columns stay at 40px → data shows "..." Co-Authored-By: Claude Opus 4.6 --- .../src/stories/StylingIssues.stories.tsx | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/buckaroo-js-core/src/stories/StylingIssues.stories.tsx b/packages/buckaroo-js-core/src/stories/StylingIssues.stories.tsx index 4e4a7e5cc..673a94ee6 100644 --- a/packages/buckaroo-js-core/src/stories/StylingIssues.stories.tsx +++ b/packages/buckaroo-js-core/src/stories/StylingIssues.stories.tsx @@ -307,21 +307,24 @@ export const ManyCols_LongHdr_LongData: Story = { }; // #595 repro: force narrow columns so data values show "..." +// width: 40 + suppressAutoSize prevents fitCellContents from expanding. +// Without defaultColDef.minWidth (baseline) → columns stay 40px → "..." +// With defaultColDef.minWidth: 80 (fix) → columns forced to 80px → values visible const narrowColConfig: DFViewerConfig = { column_config: Array.from({ length: 15 }, (_, i) => ({ col_name: `col_${i}`, header_name: LONG_HEADER_NAMES[i], - displayer_args: { displayer: "integer" as const, min_digits: 1, max_digits: 7 }, - ag_grid_specs: { maxWidth: 50 }, + displayer_args: { displayer: "integer" as const, min_digits: 1, max_digits: 4 }, + ag_grid_specs: { width: 40, suppressAutoSize: true }, })), left_col_configs: [INDEX_COL], pinned_rows: [], }; const NarrowColInner = makeStoryComponent( narrowColConfig, - genData(15, "long"), + genData(15, "year"), ); -/** 15 cols capped at 50px with 7-digit data. #595 repro — values show "...". */ +/** 15 cols with initial 40px width + suppressAutoSize. #595 repro — "..." without minWidth fix. */ export const ManyCols_LongHdr_YearData: Story = { render: () => , }; From dca9adab325d2a7ad2ffabb0fd9a2d54f4a23056 Mon Sep 17 00:00:00 2001 From: Paddy Mullen Date: Fri, 27 Feb 2026 17:33:06 -0500 Subject: [PATCH 14/15] fix: update A9 story to use fitGridWidth + autoSizeStrategy override MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Same story as the fix branch but without defaultColDef.minWidth, so fitGridWidth crushes 15 columns into ~53px each → data shows "..." Co-Authored-By: Claude Opus 4.6 --- .../src/components/DFViewerParts/DFViewerInfinite.tsx | 2 +- .../src/stories/StylingIssues.stories.tsx | 11 +++++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/packages/buckaroo-js-core/src/components/DFViewerParts/DFViewerInfinite.tsx b/packages/buckaroo-js-core/src/components/DFViewerParts/DFViewerInfinite.tsx index bc44eb748..150e912c2 100644 --- a/packages/buckaroo-js-core/src/components/DFViewerParts/DFViewerInfinite.tsx +++ b/packages/buckaroo-js-core/src/components/DFViewerParts/DFViewerInfinite.tsx @@ -303,7 +303,7 @@ export function DFViewerInfiniteInner({ return { ...outerGridOptions(setActiveCol, df_viewer_config.extra_grid_config), domLayout: hs.domLayout, - autoSizeStrategy: getAutoSize(styledColumns.length), + autoSizeStrategy: df_viewer_config.extra_grid_config?.autoSizeStrategy || getAutoSize(styledColumns.length), onFirstDataRendered: (_params) => { // Grid finished rendering }, diff --git a/packages/buckaroo-js-core/src/stories/StylingIssues.stories.tsx b/packages/buckaroo-js-core/src/stories/StylingIssues.stories.tsx index 673a94ee6..ed3514ac8 100644 --- a/packages/buckaroo-js-core/src/stories/StylingIssues.stories.tsx +++ b/packages/buckaroo-js-core/src/stories/StylingIssues.stories.tsx @@ -306,25 +306,24 @@ export const ManyCols_LongHdr_LongData: Story = { render: () => , }; -// #595 repro: force narrow columns so data values show "..." -// width: 40 + suppressAutoSize prevents fitCellContents from expanding. -// Without defaultColDef.minWidth (baseline) → columns stay 40px → "..." -// With defaultColDef.minWidth: 80 (fix) → columns forced to 80px → values visible +// #595 repro: fitGridWidth with 15 cols in 800px → ~53px each → "..." +// Without defaultColDef.minWidth (baseline) → columns crushed → "..." +// With defaultColDef.minWidth: 80 (fix) → columns at 80px → scrollbar + readable values const narrowColConfig: DFViewerConfig = { column_config: Array.from({ length: 15 }, (_, i) => ({ col_name: `col_${i}`, header_name: LONG_HEADER_NAMES[i], displayer_args: { displayer: "integer" as const, min_digits: 1, max_digits: 4 }, - ag_grid_specs: { width: 40, suppressAutoSize: true }, })), left_col_configs: [INDEX_COL], pinned_rows: [], + extra_grid_config: { autoSizeStrategy: { type: "fitGridWidth" as const } }, }; const NarrowColInner = makeStoryComponent( narrowColConfig, genData(15, "year"), ); -/** 15 cols with initial 40px width + suppressAutoSize. #595 repro — "..." without minWidth fix. */ +/** 15 cols with fitGridWidth. #595 repro — values show "..." without minWidth fix. */ export const ManyCols_LongHdr_YearData: Story = { render: () => , }; From 37ce057972e6ed67bc5dceaa67d9593f5e58a54c Mon Sep 17 00:00:00 2001 From: Paddy Mullen Date: Fri, 27 Feb 2026 17:38:15 -0500 Subject: [PATCH 15/15] fix: update A9 story to 25 cols for visible truncation repro Co-Authored-By: Claude Opus 4.6 --- .../src/stories/StylingIssues.stories.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/buckaroo-js-core/src/stories/StylingIssues.stories.tsx b/packages/buckaroo-js-core/src/stories/StylingIssues.stories.tsx index ed3514ac8..f6fc5dd71 100644 --- a/packages/buckaroo-js-core/src/stories/StylingIssues.stories.tsx +++ b/packages/buckaroo-js-core/src/stories/StylingIssues.stories.tsx @@ -306,11 +306,11 @@ export const ManyCols_LongHdr_LongData: Story = { render: () => , }; -// #595 repro: fitGridWidth with 15 cols in 800px → ~53px each → "..." +// #595 repro: fitGridWidth with 25 cols in 800px → ~32px each → "..." // Without defaultColDef.minWidth (baseline) → columns crushed → "..." // With defaultColDef.minWidth: 80 (fix) → columns at 80px → scrollbar + readable values const narrowColConfig: DFViewerConfig = { - column_config: Array.from({ length: 15 }, (_, i) => ({ + column_config: Array.from({ length: 25 }, (_, i) => ({ col_name: `col_${i}`, header_name: LONG_HEADER_NAMES[i], displayer_args: { displayer: "integer" as const, min_digits: 1, max_digits: 4 }, @@ -321,9 +321,9 @@ const narrowColConfig: DFViewerConfig = { }; const NarrowColInner = makeStoryComponent( narrowColConfig, - genData(15, "year"), + genData(25, "year"), ); -/** 15 cols with fitGridWidth. #595 repro — values show "..." without minWidth fix. */ +/** 25 cols with fitGridWidth in 800px. #595 repro — values show "..." without minWidth fix. */ export const ManyCols_LongHdr_YearData: Story = { render: () => , };