Skip to content

fix(plugin-dashboard): bind drill-title drawer assertions to content, not a cumulative render count - #4708

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-4706-drill-title-render-count
Aug 15, 2026
Merged

fix(plugin-dashboard): bind drill-title drawer assertions to content, not a cumulative render count#4708
yinlianghui merged 1 commit into
mainfrom
claude/issue-4706-drill-title-render-count

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes#4706

What was wrong

DatasetWidget.drillTitleLabel.test.tsx pinned drawerProps.length to exactly 1
after a single drill click, in five places. drawerProps is a recording array of the
mocked DrillDownDrawer's render props — it only ever grows. useDatasetDimensionMeta
issues its own dimension-metadata fetch independently of the click; when that fetch's
setMeta lands its React commit strictly AFTER the click's own commit (a real
possibility under a saturated event loop, since the two updates are only incidentally
ordered), DrillDownDrawer renders a second time with the SAME title/filter, and
drawerProps.length overshoots to 2. Once it overshoots, waitFor can never recover
— nothing shrinks the array. This is what CI observed on an unrelated PR's shard 2
(run 31885228851): expected 2 to be 1.

The fix

Each site now waits for the drawer to have opened AT LEAST once
(toBeGreaterThan(0)) instead of pinning the exact cumulative count. The content
assertions immediately after (lastDrawer().title / .filter) are unchanged and
remain the substantive check — each case's own name states what it verifies (which
title the drawer shows), never how many times React chose to render it. A drawer that
never opens still times out exactly as before; only the overshoot-vulnerable exact-count
pin is relaxed.

Sites fixed (all in DatasetWidget.drillTitleLabel.test.tsx, same isomorphic
await waitFor(() => expect(drawerProps.length).toBe(1)) idiom immediately followed by
content assertions):

  • :193 — "titles an identity-keyed group…" (the issue's named site)
  • :223 — "titles the OTHER colliding group…"
  • :253 — BOUNDARY "an ordinary group's title is byte-identical to before"
  • :280 — BOUNDARY "a click carrying no label still titles from the key"
  • :324 — BOUNDARY "a NON-pivoted widget still omits the series from its title"

All five share byte-identical exposure (confirmed empirically, not just by inspection —
see Verification below), so all five were fixed the same way.

Verification

Deterministic reproduction (pre-fix, all 5 sites red). Temporarily gated the
dimension-metadata fetch mock behind a manually-releasable promise, wrapped the drill
click in its own act() so its render fully commits first, then released the gate and
flushed inside a second act() — forcing the click's commit and the metadata commit
into two SEPARATE React passes instead of letting React 18 automatic batching coalesce
them into one (which is why a naive forced-delay attempt without the act() boundary
did not reproduce it: both updates landed in the same commit). With the OLD assertion,
all 5 sites failed with the exact CI failure text — expected 2 to be 1.

Fix re-verified under the same forced race. With the fix applied and the forced
overshoot still active, all 7 tests in the file pass.

Positive control (assertion bite-check). With the fix in place, temporarily reverted
objectui#4682's production fix (ev?.seriesLabel ?? ev?.seriesev?.series) in
DatasetWidget.tsx's handleChartDrill. The two identity-collision cases correctly
went red on the WRONG title (Backlog / [null] / Backlog / ["(None)"] instead of
Backlog / (None)), while the three ordinary-group boundary cases correctly stayed
green (label equals key there, so the reverted line can't move their title) —
confirming the rebound assertions still catch the real defect the file exists to pin.

All temporary repro/gate/positive-control changes were reverted before the commit in
this PR — git diff against origin/main is the fix alone (test file + changeset).

Gates run (final HEAD 83da6212d)

  • pnpm --filter '@object-ui/plugin-dashboard' lint — 0 errors (334 pre-existing
    any/fast-refresh warnings, none introduced by this change; the changed file's own
    warnings are all pre-existing, outside the edited lines)
  • pnpm --filter '@object-ui/plugin-dashboard' type-check — clean
  • pnpm exec vitest run packages/plugin-dashboard/58 files / 462 tests passed
  • node scripts/check-control-bytes.mjs — OK
  • node scripts/check-changeset-presence.mjs — OK (empty-frontmatter changeset declared)
  • node scripts/check-changeset-fixed.mjs / check-changeset-no-major.mjs — OK

Changeset

Test-only change → empty-frontmatter changeset
(.changeset/dataset-widget-drill-title-render-count.md). No published behaviour
changes.

Not in scope

#4705 and #4682 are unrelated to this change (#4705 only triggered the shard-2 red that
surfaced this issue; #4682 is the drill-title feature these tests pin, untouched here
beyond the temporary, reverted positive control). #4694 is also unrelated. None of them
are addressed or closed by this PR.


Generated by Claude Code

… not a cumulative render count (#4706)
DatasetWidget.drillTitleLabel.test.tsx pinned `drawerProps.length` to exactly
1 after a single drill click. That count only ever grows (the recording
array is never trimmed), and a late, unrelated `setMeta` re-render from
`useDatasetDimensionMeta`'s own dimension-metadata fetch — which resolves
independently of the click, and can land its commit strictly after the
click's own commit under load — pushes the same drawer content a second
time. Once the count overshoots 1, `waitFor` can never recover, since
nothing about it shrinks. Observed on an unrelated PR's shard 2
(run 31885228851): `expected 2 to be 1`.
Fixed by waiting for the drawer to have opened AT LEAST once
(`toBeGreaterThan(0)`) instead of pinning the exact count, and keeping the
existing content assertions (`lastDrawer().title` / `.filter`) as the
substantive check — which is what each case's own name says it is testing.
No invariant is weakened: an extra identical render was never something
these cases claimed to guard, and a drawer that never opens still times out
the same as before.
Reproduced deterministically pre-fix (all 5 sites, gating the metadata
fetch and forcing its resolution into a separate React commit after the
click's own commit, via `act()`) and re-verified positive: with the fix in
place but the repro's forced overshoot still active, all 7 cases pass, and
with objectui#4682's `seriesLabel ?? series` fix temporarily reverted the
two identity-collision cases correctly go red on the wrong title — proving
the rebound assertions still catch the defect the file exists to pin. All
temporary repro/positive-control changes were reverted before this commit;
this diff is the fix alone.
Sites fixed, all in DatasetWidget.drillTitleLabel.test.tsx (line numbers as
committed):
- :190 (the issue's named site — "titles an identity-keyed group…")
- :222 ("titles the OTHER colliding group…")
- :253 (BOUNDARY — an ordinary group's title)
- :278 (BOUNDARY — a click carrying no label)
- :323 (BOUNDARY — a NON-pivoted widget)
Test-only; empty-frontmatter changeset.
Claude-Session: https://claude.ai/code/session_01RnQd8iMMUwXQEV1crFmQiQ
Co-authored-by: Claude <noreply@anthropic.com>
@vercel

vercelBot commented Aug 15, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectuiIgnoredIgnoredAug 15, 2026 1:27pm

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Main entry (gzip)24.7 KB350 KB
Entry fileindex-CWaHiCE9.js
StatusPASS

📦 Bundle Size Report

PackageSizeGzipped
app-shell (index.js)9.56KB3.59KB
app-shell (runtime-config.js)7.42KB2.32KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)8.92KB3.41KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)1.17KB0.53KB
auth (AuthProvider.js)25.13KB5.40KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.13KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.64KB2.21KB
auth (SocialSignInButtons.js)9.60KB3.89KB
auth (UserMenu.js)3.40KB1.22KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)38.46KB10.17KB
auth (createAuthenticatedFetch.js)6.34KB2.43KB
auth (index.js)2.35KB1.07KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.02KB0.88KB
auth (useIsWorkspaceAdmin.js)1.61KB0.85KB
collaboration (CommentThread.js)26.07KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.65KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)489.91KB108.67KB
core (index.js)3.79KB1.52KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)163.56KB44.83KB
fields (index.js)230.37KB57.17KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (i18n.js)4.32KB1.77KB
i18n (index.js)3.35KB1.38KB
i18n (pickLocalized.js)3.69KB1.73KB
i18n (provider.js)23.12KB7.62KB
i18n (useDisplayLocale.js)2.84KB1.45KB
i18n (useObjectLabel.js)27.59KB6.63KB
i18n (useSafeTranslation.js)7.77KB3.13KB
layout (index.js)38.98KB10.85KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.74KB
mobile (index.js)1.50KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.71KB0.42KB
mobile (useResponsiveConfig.js)1.36KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)8.75KB3.06KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)3.67KB1.12KB
permissions (evaluator.js)4.41KB1.44KB
permissions (index.js)0.91KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.52KB
permissions (usePermissions.js)1.55KB0.71KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.86KB12.91KB
plugin-charts (index.js)64.44KB18.28KB
plugin-chatbot (index.js)181.21KB43.14KB
plugin-dashboard (index.js)121.90KB31.76KB
plugin-designer (index.js)212.39KB42.83KB
plugin-detail (index.js)239.93KB60.01KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)114.72KB27.70KB
plugin-gantt (index.js)164.30KB40.02KB
plugin-grid (index.js)192.10KB51.24KB
plugin-kanban (index.js)52.74KB14.53KB
plugin-list (index.js)111.14KB26.92KB
plugin-map (index.js)18.16KB5.81KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)41.38KB11.09KB
plugin-timeline (index.js)26.68KB7.66KB
plugin-tree (index.js)8.50KB2.88KB
plugin-view (index.js)83.81KB20.49KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.71KB3.53KB
providers (index.js)0.44KB0.22KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.67KB2.37KB
react (LazyPluginLoader.js)3.77KB1.33KB
react (SchemaRenderer.js)27.64KB9.44KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)1.28KB0.68KB
react (schema-input.js)1.45KB0.83KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)4.09KB1.74KB
sdui-parser (index.js)4.47KB2.03KB
sdui-parser (parse.js)10.04KB2.82KB
sdui-parser (types.js)0.29KB0.24KB
sdui-parser (validate.js)4.69KB1.48KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)0.20KB0.18KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)0.20KB0.18KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.87KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-retry.js)4.32KB2.02KB
types (index.js)3.05KB1.52KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)0.20KB0.18KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@yinlianghuiClaude

Copy link
Copy Markdown
CollaboratorAuthor

Review verdict: ACCEPT (reviewer of record, PM session session_01RnQd8iMMUwXQEV1crFmQiQ).

Verified against this PR, not the report: exactly two changed files (the test file and an empty-frontmatter changeset); body's only closing keyword is Fixes #4706, with the Not-in-scope section keeping #4705/#4682/#4694 free of closing-keyword adjacency. All five sites take the same minimal relaxation — at-least-once on the recording array — while the lastDrawer() title/filter assertions stay untouched as the substantive check, which is the right reading of what these cases pin (which title, not how many renders). The verification is the strong form twice over: the deterministic reproduction explains why naive delay could not reproduce it (React 18 batching coalescing the two commits) and produces the exact CI failure text at all five sites, and the positive control is discriminating — reverting the #4682 production line reddens exactly the two identity-collision cases while the three boundary cases stay green, proving the retained assertions bite on the real defect and nothing else. Local gates are pinned to 83da6212d, matching this PR's current head.

Will flip ready + enable auto-merge once Lint, Type Check, and Test shards 1–4 conclude success on this head.


Generated by Claude Code

@yinlianghui
yinlianghui marked this pull request as ready for review August 15, 2026 13:47
@yinlianghui
yinlianghui added this pull request to the merge queueAug 15, 2026
Merged via the queue into main with commit 000a1ebAug 15, 2026
21 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-4706-drill-title-render-count branch August 15, 2026 13:48
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

flaky: DatasetWidget.drillTitleLabel's identity-keyed case pins a cumulative drawer-render count that overshoots under load

2 participants

@yinlianghui@claude