Skip to content

feat: add cheetah-grid-playwright package - #474

Merged
ota-meshi merged 31 commits into
masterfrom
feat/playwright-helper-package
Aug 20, 2026
Merged

ota-meshi merged 31 commits into
masterfrom
feat/playwright-helper-package

Conversation

@ota-meshi

@ota-meshi ota-meshi commented Aug 19, 2026 •

Copy link
Copy Markdown
Member

Follow-up to #472. Introduces cheetah-grid-playwright, a new ESM-only workspace package providing official Playwright helpers for driving the grid. Cells are addressed by field name and record index (or raw col/row indices) and operated with real mouse and keyboard events at coordinates computed through the grid API, so interactions go through the same code path as actual user input.

import { gridLocator } from "cheetah-grid-playwright";

const grid = gridLocator(page.locator(".cheetah-grid"));
await grid.cell("email", 3).click();
await grid.cell("email", 3).fill("cat@example.com");
await expect.poll(() => grid.cell("email", 3).value()).toBe("cat@example.com");

The helpers absorb the pitfalls that make hand-written recipes wrong or flaky, found through adversarial review and verified by reproduction:

  • Waits for the grid's internal scroll state after makeVisibleCell() (which only moves the DOM scroll position), forcing instant scrolling under scroll-behavior: smooth.
  • Scrolls the window (and ancestor frames) to the cell before clicking, clicks the center of the visible part of the cell (cells larger than the grid viewport), and supports grids inside same-origin iframes by converting to top-level viewport coordinates.
  • fill() loads lazy records first, opens the editor with F2, waits out the editors' Enter guard, and verifies the commit — failing with the validation message when an inputValidator rejects the value instead of reporting a false success.
  • Fails clearly instead of silently misbehaving: out-of-range cells, cells without a text editor (closing an opened menu), multiple grids under one locator, grids scaled by ancestor transforms, cross-origin iframes, and unbuilt fixtures.
  • Returns the whole rectangle for merged (colSpan/rowSpan) cells, and never leaks uncaught errors into the page (asserted for every test).

Tests (23) run against local fixture pages with vitest + Playwright (channel: "chrome"), including a smoke test of the built artifact (the page function is serialized at runtime, so the shipped file must be exercised). The lint CI job gains a build phase so type checking can resolve the built cheetah-grid type definitions.

Known limitation, documented in code: the page must expose window.cheetahGrid (the same module instance that created the grid).

This PR also carries two core fixes found through the helper's tests: makeVisibleCell scrolled the start edge of cells larger than the grid viewport out of view on every click, and the resulting scroll closed cell editors right after opening (always in WebKit, in Chrome when horizontally scrolled), making such cells impossible to edit in place; and getCellsRect clamped the row direction of ranges starting in frozen rows by column widths instead of row heights.

Possible follow-ups (not in this PR): menu editors via a selectOption-like API, selection ranges, and event waiting helpers.

🤖 Generated with Claude Code

@changeset-bot

changeset-bot Bot commented Aug 19, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 4928b7b

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
cheetah-grid Patch
cheetah-grid-playwright Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

ota-meshi and others added 2 commits August 19, 2026 12:25
New workspace package providing Playwright helpers for driving the
grid: cells addressed by field/record index, operated with real mouse
and keyboard events at coordinates computed via the grid API.

Absorbs two timing pitfalls: makeVisibleCell only updates the DOM
scroll position (grid state follows on the async scroll event), and
cell editors ignore Enter for one macrotask after opening. The FAQ
editing example gains the same wait.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replace the locally declared structural types with a type-only import
of the cheetah-grid namespace, and add a build phase to the lint CI
job so type checking can resolve the built type definitions.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@ota-meshi
ota-meshi force-pushed the feat/playwright-helper-package branch from 78bb167 to 838423d Compare August 19, 2026 03:25
ota-meshi and others added 4 commits August 19, 2026 12:52
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
No concrete use case: the namespace is looked up as window.cheetahGrid,
which the UMD bundle defines and ESM apps expose under that name anyway.
An option can be reintroduced without breakage if a need appears.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@ota-meshi
ota-meshi force-pushed the feat/playwright-helper-package branch 3 times, most recently from de4b3e8 to 94df503 Compare August 19, 2026 04:26
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@ota-meshi
ota-meshi force-pushed the feat/playwright-helper-package branch from 94df503 to 22c4b2d Compare August 19, 2026 04:27
ota-meshi and others added 17 commits August 19, 2026 13:39
The functions passed to evaluate() are serialized per call site, so the
namespace lookup, instance resolution, and cell spec resolution were
duplicated between rect() and value(). Consolidate them into a single
page function dispatching on an op parameter.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Both the scroll listener and the rAF fallback called grid.unlisten(id),
and the core unlisten is not idempotent, so every scroll-inducing
operation raised an uncaught TypeError in the page. Tests now assert
that no page errors occur during any operation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
rect() coordinates were relative to the grid's own frame while
page.mouse operates in the top-level viewport, so every click on a grid
inside an iframe landed offset. Add the ancestor frame elements'
offsets (throwing clearly for cross-origin frames), run fill()'s
macrotask wait in the grid's frame, and scope the editor lookup to the
grid root. The iframe test is served over HTTP because file: documents
are opaque origins in Chrome.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… editor

fill() silently reported success when the editor did not open (e.g. a
record still loading): the value went into the grid's hidden focus
control. Load the record first via the cell value, exclude the focus
control from the editor lookup, and throw a clear error when no editor
opened.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
An out-of-range record index or col/row produced fabricated
coordinates: value() returned undefined and click() silently hit
unrelated points. Throw a clear error instead.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The grid's own mouse hit-testing does not compensate for visual
scaling, so under an ancestor transform/zoom near cells only worked by
error cancellation while farther cells silently missed. Detect the
scale mismatch and throw a clear error.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
CJS TypeScript consumers under node16 resolution could not compile
against the package because only .d.mts types were emitted while the
require condition pointed at them. Ship a single ESM build instead;
the supported Node versions (engines) all handle require(esm). Also
update tsdown to ^0.22.14 and export ./package.json.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
rect() returned only the anchor cell slice for colSpan/rowSpan cells.
Resolve the cell range and return the union of the start and end cell
rectangles.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
On menu editor columns the initial click opens the menu, leaving it
open (with focus trapped on a menu item) after the clear error. Press
Escape before throwing, and mention non-text editors in the message.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Enter does not guarantee a commit: the built-in SmallDialogInputEditor
keeps the dialog open when a validator (possibly asynchronous) rejects
the value, while fill() reported success. Poll the outcome after Enter
and throw the validation message, cancelling the editing so the dialog
does not stay open.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The grid only scrolls its own internal viewport, so on pages where the
grid sits below the fold (or is taller than the window) every click
silently missed: mouse events outside the viewport hit nothing. Scroll
the grid into view, scroll the window further to the cell when needed,
and fail clearly when the point still cannot be brought into view.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Under scroll-behavior: smooth the programmatic scroll of
makeVisibleCell animates: right after the call the scroll position is
still the old one, so the scroll-state wait was skipped entirely and
coordinates were computed at the old position. Temporarily force
scroll-behavior: auto on the grid's scrollable while adjusting.

Also group the scroll adjustment into scrollCellIntoView (named apart
from the core makeVisibleCell), ordered with the other page helpers by
call order.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The descendant search silently picked the first grid in document
order, so tests targeting another grid failed confusingly downstream.
Throw a clear error instead.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The center of the whole cell rectangle falls outside the canvas when
the cell is larger than the grid viewport (e.g. a column wider than
the grid), so clicks silently missed. Compute the click point from the
intersection of the cell and the canvas instead; rect() still returns
the whole cell rectangle.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The shipped artifact depends on cellOperation surviving the build
self-contained (it is serialized into the page at runtime), but only
the sources were tested. Drive the grid through dist/index.js in the
suite, failing fast when the package has not been built.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ota-meshi and others added 7 commits August 19, 2026 19:23
A missing file: subresource does not fail navigation, so on a fresh
clone every test silently burned the 60s hook timeout waiting for a
grid that could never appear (9+ minutes with no hint). Check for the
cheetahGrid global right after navigation in a shared gotoFixture
helper and fail with a message pointing at the build.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The eslint files globs omitted .mts, so tsdown.config.mts was silently
skipped.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Aligning the end edge of a cell larger than the grid viewport shoved
its start edge off screen on every click. The resulting scroll also
closed cell editors right after opening (always in WebKit, where the
scroll event arrives after the editor opens; in Chrome when the grid
was horizontally scrolled), making such cells impossible to edit in
place. Clamp the scroll so the start edge stays in view.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The row-direction clamp for ranges starting in frozen rows used column
widths (_getColsWidth with row indices), returning a wrong height for
such ranges on vertically scrolled grids.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The click point was only checked against the window viewport, so
overflow ancestors, iframe boxes, scaled iframes, and overlaying
elements produced silent misclicks, and viewport:null contexts or
scroll-behavior:smooth broke the window-scroll retry. Resolve the
point page-side instead: walk from the grid's frame to the top window,
instantly scrolling every clipping scroll container, frame, and window
as needed, rejecting transform-scaled frame elements, and hit-testing
the final point so a covering element fails clearly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- cell()/cellAt() now reject non-integer or negative indices
  synchronously: a negative record index resolved onto header rows,
  where click() could trigger header actions such as sorting.
- The merged-cell rectangle is now taken from getCellRangeRect, which
  handles ranges spanning the frozen-column boundary; the previous
  per-cell union mixed coordinate spaces and inverted under scroll,
  falsely rejecting visible cells.
- fill()'s commit-timeout path now closes the editor like the other
  failure paths.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The fixture server bound to all interfaces and followed .. path
traversal, exposing repository files to the local network while the
suite runs. Bind to 127.0.0.1 and reject paths escaping the repository
root. Also replace the bare TypeError on pages running cheetah-grid
< 2.2 with a message naming the required version.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@ota-meshi
ota-meshi marked this pull request as ready for review August 20, 2026 05:53
@ota-meshi
ota-meshi merged commit a3ea8f9 into master Aug 20, 2026
2 checks passed
@ota-meshi
ota-meshi deleted the feat/playwright-helper-package branch August 20, 2026 06:02
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