Skip to content

Add real WebDriver pointer actions to package:integration_test on the web - #195207

Merged
auto-submit[bot] merged 23 commits into
flutter:masterfrom
harryterkelsen:real-webdriver-clicks-as-a-first-class-package
Oct 29, 2026
Merged

auto-submit[bot] merged 23 commits into
flutter:masterfrom
harryterkelsen:real-webdriver-clicks-as-a-first-class-package

Conversation

@harryterkelsen

@harryterkelsen harryterkelsen commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

Adds a first-class way for Flutter web integration tests to drive the real browser mouse through WebDriver: clicks, hovers, drags, and arbitrary pointer action sequences with any mouse button.

WidgetTester.tap, WidgetTester.drag and binding.handlePointerEvent inject synthetic events straight into the framework. They skip the browser's own DOM hit testing and event dispatch, which is what decides whether a pointer event lands on Flutter content or on a platform view (<iframe>, map, video) underneath it. So features that depend on browser hit testing, such as pointer handling over platform views, couldn't be checked end to end. Until now the only workaround was to misuse binding.takeScreenshot as an RPC and drive the mouse from inside the host's onScreenshot callback.

New API

On IntegrationTestWidgetsFlutterBinding:

  • Future<void> webDriverPointerActions(List<WebDriverPointerAction> actions, {FlutterView? view}) performs actions in order as one real browser pointer sequence and completes once the browser has dispatched every event in it.
  • Future<void> webDriverClick(Offset position, {FlutterView? view}) is a convenience wrapper that moves to position and presses and releases the primary button.

In package:integration_test/common.dart (which integration_test.dart does not re-export, so tests import it directly):

  • sealed class WebDriverPointerAction, created with WebDriverPointerAction.move(x, y, {Duration duration}), .down({WebDriverMouseButton button}), .up({WebDriverMouseButton button}) and .pause(Duration duration). The concrete classes are WebDriverPointerMove, WebDriverPointerDown, WebDriverPointerUp and WebDriverPointerPause, with toJson/fromJson, value equality and readable toString.
  • enum WebDriverMouseButton { primary, middle, secondary }, whose w3cButton is the W3C button index (0, 1, 2), not a Flutter kPrimaryButton-style bit flag.
  • WebDriverCommandType.pointerActions and WebDriverCommand.pointerActions(actions) in the web command protocol.
  • CallbackManager.pointerActions(List<WebDriverPointerAction> actions), which completes with an UnsupportedError by default.

For example, a drag split across two calls so the test can assert on the intermediate state:

final Offset start = tester.getCenter(find.byKey(dragHandleKey));
await binding.webDriverPointerActions(<WebDriverPointerAction>[
  WebDriverPointerAction.move(start.dx, start.dy),
  const WebDriverPointerAction.down(),
  WebDriverPointerAction.move(start.dx + 100, start.dy, duration: const Duration(milliseconds: 300)),
]);
// ...assert mid-drag...
await binding.webDriverPointerActions(<WebDriverPointerAction>[
  const WebDriverPointerAction.up(),
]);

How it works

  • Move positions are logical positions in the target view (for example from tester.getCenter). webDriverPointerActions converts them to viewport CSS pixels by adding the view host element's getBoundingClientRect() origin, so full-page, embedded and multi-view apps all work. The command travels over the same handshake as screenshot.
  • integrationDriver (in integration_test_driver_extended.dart) sends the whole sequence as a single W3C perform-actions request for one mouse input source and replies only after WebDriver has performed it. Tests can assert right after the await with no fixed sleeps, and no onScreenshot callback is needed.
  • ChromeDriver performs a timed pointerMove as a single mousemove at the end. To make the browser see intermediate events during drags and hovers, the driver splits each timed move into straight-line steps of about 16 ms, starting from the previous move's position (remembered across calls within one integrationDriver run) and capped at one step per pixel. Positions are rounded to whole CSS pixels.
  • Pressed buttons stay pressed across calls until released with up.
  • Invalid input fails fast in the browser: an empty sequence, non-finite move coordinates, or negative durations throw ArgumentError before anything is sent, and calling without a view when there is no implicit view throws StateError. Off the web, both methods throw UnsupportedError.
  • If the driver isn't a WebFlutterDriver, the host fails the in-browser call and throws a StateError. If WebDriver throws (for example, coordinates out of bounds), the host releases pressed buttons, fails the in-browser call, and rethrows the original error, so the test fails at once instead of hanging.
  • Neither method changes binding.shouldPropagateDevicePointerEvents. Real browser events reach the test binding as device events, so a test that asserts on Flutter widget gestures has to opt in with binding.shouldPropagateDevicePointerEvents = true. Platform view DOM listeners get the native events either way.

Tests

  • New web e2e webdriver_pointer_actions_integration, registered in run_web_tests.dart for CanvasKit and --wasm in profile mode. It checks that a real click reaches a same-origin <iframe> platform view (which tester.tap can't do), that a click reaches a Flutter button only once device pointer events are opted in, that position conversion still works when the view's host element is offset in the page, that a timed drag produces intermediate pointer moves, that buttons stay pressed across calls for a split drag, and that moving without a button hovers.
  • New unit tests in packages/integration_test/test/webdriver_command_test.dart cover action serialization, equality and fromJson validation, the pointerActions command, the CallbackManager default, input validation, and the off-web UnsupportedError.
  • New unit tests in packages/integration_test/test/integration_driver_pointer_actions_test.dart cover the W3C request integrationDriver builds (rounding, timed-move subdivision across requests, pauses and buttons) and its error handling for non-web drivers, WebDriver failures and malformed action JSON.

Compatibility

CallbackManager.pointerActions has a default body, so classes that extend CallbackManager keep compiling. A class that implements it would need to add pointerActions. The binding's callbackManager can't be swapped out, though, so outside code shouldn't have a real implementation. common.dart is not re-exported from integration_test.dart, so no new names enter the scope of existing tests.

Part of #179806

Pre-launch Checklist

If you need help, consider asking for advice on the #hackers-new channel on Discord.

If this change needs to override an active code freeze, provide a comment explaining why. The code freeze workflow can be overridden by code reviewers. See pinned issues for any active code freezes with guidance.

Note: The Flutter team is currently trialing the use of Gemini Code Assist for GitHub. Comments from the gemini-code-assist bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed.

- Rewrite the integrationDriver click tests around a fake app and a fake
  mouse whose behavior is stated in the test, so each test reads as the
  clicks the app asks for and where the mouse clicked.
- Fold the view-to-viewport origin lookup into the existing web/io
  callback split instead of separate view_utils libraries.
- Explain why the first button click in the e2e test reaches neither the
  button nor the iframe.
- Drop comments that only made sense with knowledge of earlier designs.
@github-actions github-actions Bot added a: tests "flutter test", flutter_test, or one of our tests framework flutter/packages/flutter repository. See also f: labels. f: integration_test The flutter/packages/integration_test plugin team-web Owned by Web platform team labels Oct 24, 2026
@harryterkelsen harryterkelsen added the CICD Run CI/CD label Oct 24, 2026
@harryterkelsen
harryterkelsen marked this pull request as ready for review October 24, 2026 02:05

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces the webDriverClick API to integration_test on the web, enabling real browser pointer clicks via WebDriver. It adds the click command to WebDriverCommandType, implements the click dispatching logic in the web callback manager and integration driver, and includes comprehensive integration and unit tests. The review feedback suggests replacing deprecated usages of PlatformDispatcher.implicitView with non-deprecated alternatives in both the test driver and the core integration test package.

Each kind of pointer action is now its own final subclass
(WebDriverPointerMove, WebDriverPointerDown, WebDriverPointerUp,
WebDriverPointerPause) with non-nullable fields, replacing the
WebDriverPointerActionType enum and nullable fields. The
WebDriverPointerAction.move/down/up/pause constructors redirect to the
subclasses, so call sites are unchanged, and consumers can switch
exhaustively on the action.
mdebbar
mdebbar previously approved these changes Oct 28, 2026
Drop the exhaustive switch example from the WebDriverPointerAction doc
comment, along with the snippet preamble import that only it needed.
@harryterkelsen harryterkelsen changed the title Add real WebDriver clicks to package:integration_test on the web Add real WebDriver pointer actions to package:integration_test on the web Oct 28, 2026
Add a direct dependency on package:meta to integration_test and annotate
the sealed WebDriverPointerAction with @immutable, replacing the
per-member avoid_equals_and_hash_code_on_mutable_classes ignores on its
subclasses.

Also make the base generative constructor private and drop redundant
"See [X.new]" lines from the redirecting factory constructor docs.
Drop the package:meta import and dependency so common.dart remains
completely standalone and compatible with zero-dependency target
environments.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

a: tests "flutter test", flutter_test, or one of our tests CICD Run CI/CD f: integration_test The flutter/packages/integration_test plugin framework flutter/packages/flutter repository. See also f: labels. team-web Owned by Web platform team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants