Skip to content

fix: defer Spreadsheet.selectCellRange until sheet widget layout is ready#9381

Open
DiegoCardoso wants to merge 5 commits into
mainfrom
fix/spreadsheet-selection-obf-safe
Open

fix: defer Spreadsheet.selectCellRange until sheet widget layout is ready#9381
DiegoCardoso wants to merge 5 commits into
mainfrom
fix/spreadsheet-selection-obf-safe

Conversation

@DiegoCardoso
Copy link
Copy Markdown
Contributor

@DiegoCardoso DiegoCardoso commented May 28, 2026

Follow-up to #9333. When a Spreadsheet inside a closed Dialog is opened, Flow
flushes the property writes and the setSelectedCellAndRange RPC in the same
task. The api receives the call before the deferred init scheduled in
SheetWidget.resetFromModel has run — so SheetWidget.getRowHeight reads an
uninitialized definedRowHeights and throws

Uncaught TypeError: Cannot read properties of undefined (reading 'length')

That throw drops the selection and trips the *_noErrors/*_noConsoleErrors
console-log assertions.

#9333's connector-side guard masked this by probing definedRowHeights
through the api's private GWT fields — names that only survive
-Dgwt.style=pretty (local/dev). CI compiles the GWT client with
-Dgwt.style=OBF, where those private names are renamed and api.spreadsheetWidget
is undefined. The guard itself then threw, producing the same SEVERE log and
the same ~20 spreadsheet IT failures on the cherry-pick PRs.

Move the layout-readiness handling into the widget that owns the operation:

  • SheetWidget.isLoaded() (package-private) — exposes the already-existing
    loaded flag that flips at the end of resetFromModel's deferred command.
  • SpreadsheetWidget.selectCellRange — if !sheetWidget.isLoaded(), store the
    latest args and Scheduler.scheduleDeferred(this::applyPendingSelectCellRange).
    The deferred re-checks, applies, or re-schedules. Latest-wins (rapid successive
    calls collapse to the final selection). Bounded at 100 hops as a safety net;
    in the typical case the layout init has already been scheduled before us, so
    GWT's FIFO scheduler resolves us in 0–1 hops.
  • vaadin-spreadsheet.jssetSelectedCellAndRange goes back to a one-liner
    (this._flush(); this.api.setSelectedCellAndRange(...args)). No try/catch,
    no pending state, no constants.

Only selectCellRange is deferred — it's the only RPC that touches row/column
metrics. Other RPCs (cellsUpdated, popup ops, etc.) don't need this and
didn't fail in the original CI runs.

Reproduce locally by compiling the client with -Dgwt.style=OBF (matching CI)
and running the merged ITs in production mode:

mvn clean install -DskipTests -Dgwt.style=OBF \
  -pl vaadin-spreadsheet-flow-parent/vaadin-spreadsheet-flow-client,vaadin-spreadsheet-flow-parent/vaadin-spreadsheet-flow
node scripts/mergeITs.js spreadsheet
mvn verify -Drun-it -pl integration-tests -Dvaadin.productionMode=true \
  -Dit.test='NavigationIT,SpreadsheetDialogIT' -DskipUnitTests
  • With this PR → NavigationIT 26/0, SpreadsheetDialogIT 1/0.
  • Reverting only the GWT-side change → SpreadsheetDialogIT fails with the
    underlying Cannot read properties of undefined (reading 'length') in
    FlowClient-…js — the actual definedRowHeights NPE.

Follow-up to #9333.


🤖 Generated with Claude Code

… internals

#9333 guarded setSelectedCellAndRange by reading
`this.api.spreadsheetWidget.sheetWidget.definedRowHeights`. Those are private
GWT fields: their names survive only in `pretty` GWT output (local/dev), so the
check worked there. CI compiles the client with `-Dgwt.style=OBF`, which
obfuscates the field names, so `this.api.spreadsheetWidget` is undefined and the
guard threw `Cannot read properties of undefined (reading 'sheetWidget')`. That
single throw dropped the selection and tripped the `*_noErrors`/
`*_noConsoleErrors` console-log assertions, producing the ~20 spreadsheet IT
failures seen only on CI for the 24.10/25.0/25.1 cherry-picks (and untouched by
#9373, which threw on the same line).

Drop the private-field probe. The api's `setSelectedCellAndRange` is a public,
@JsType-exported method whose name is stable under obfuscation; call it and, if
the deferred initial relayout hasn't run yet (it throws inside
`$scrollAreaIntoView`), keep the latest selection and retry until it applies.

Reproduced locally by compiling the client with -Dgwt.style=OBF: the old probe
fails NavigationIT, this fix passes.

Follow-up to #9333, #9373.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@DiegoCardoso DiegoCardoso force-pushed the fix/spreadsheet-selection-obf-safe branch from 0a4ca6e to bb82001 Compare May 28, 2026 16:08
Move the layout-readiness handling from the connector into the widget that
owns the operation: SpreadsheetWidget.selectCellRange now checks
SheetWidget.isLoaded() and, if false, stores the latest args and schedules
itself via Scheduler.scheduleDeferred until layout state is populated
(bounded at 100 hops as a safety net). The connector's setSelectedCellAndRange
goes back to a one-liner — no try/catch, no pending state, no constants.

Why this is the right layer: the bug is "scrollAreaIntoView dereferences a
null definedRowHeights when called before the deferred init in
resetFromModel completes". The fix belongs next to scrollAreaIntoView's
caller, not in the JS connector reaching across an obfuscated boundary.
GWT's own scheduler is the natural deferral primitive (the layout init uses
it too); FIFO ordering means typical cases resolve in zero or one hops.

Only selectCellRange is deferred — it's the only RPC that touches row/column
metrics. Other RPCs (cellsUpdated, refreshCellStyles, popup ops, etc.) don't
need this and didn't fail in the original CI runs. If another method ever
surfaces the same race, the same pattern can be replicated.

Locally validated with -Dgwt.style=OBF (matching CI):
- Positive (new GWT + simple JS): NavigationIT 26/0, SpreadsheetDialogIT 1/0.
- Control (no GWT fix + simple JS): SpreadsheetDialogIT fails with the
  underlying NPE — Uncaught TypeError: Cannot read properties of undefined
  (reading 'length') in FlowClient bundle, the actual definedRowHeights
  dereference. Proves the GWT-side deferral is what's load-bearing.

Follow-up to #9333.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@DiegoCardoso DiegoCardoso changed the title fix: apply Spreadsheet selection via try/catch instead of probing GWT internals fix: defer Spreadsheet.selectCellRange until sheet widget layout is ready May 28, 2026
@vaadin vaadin deleted a comment from github-actions Bot May 28, 2026
- Move static constant + 3 instance fields to top-of-class field block
  (java:S1213 — field declaration order convention).
- Replace lambdas with method references (java:S1612).
@sonarqubecloud
Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant