diff --git a/packages/pointer_interceptor/CHANGELOG.md b/packages/pointer_interceptor/CHANGELOG.md index dbf6a0187cf..74dc5e664e4 100644 --- a/packages/pointer_interceptor/CHANGELOG.md +++ b/packages/pointer_interceptor/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.9.3+1 + +* Updates example code and integration tests to accomodate hit-testing changes in the Flutter web engine. + ## 0.9.3 * Require minimal version of flutter SDK to be `2.10` diff --git a/packages/pointer_interceptor/example/integration_test/widget_test.dart b/packages/pointer_interceptor/example/integration_test/widget_test.dart index c1c054a985d..45d3850e0b5 100644 --- a/packages/pointer_interceptor/example/integration_test/widget_test.dart +++ b/packages/pointer_interceptor/example/integration_test/widget_test.dart @@ -11,37 +11,29 @@ import 'package:integration_test/integration_test.dart'; import 'package:pointer_interceptor_example/main.dart' as app; +final Finder nonClickableButtonFinder = + find.byKey(const Key('transparent-button')); +final Finder clickableWrappedButtonFinder = + find.byKey(const Key('wrapped-transparent-button')); +final Finder clickableButtonFinder = find.byKey(const Key('clickable-button')); +final Finder backgroundFinder = + find.byKey(const ValueKey('background-widget')); + void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - group('Widget', () { - final Finder nonClickableButtonFinder = - find.byKey(const Key('transparent-button')); - final Finder clickableWrappedButtonFinder = - find.byKey(const Key('wrapped-transparent-button')); - final Finder clickableButtonFinder = - find.byKey(const Key('clickable-button')); - + group('Without semantics', () { testWidgets( 'on wrapped elements, the browser does not hit the background-html-view', (WidgetTester tester) async { app.main(); await tester.pumpAndSettle(); - final html.Element? element = - _getHtmlElementFromFinder(clickableButtonFinder, tester); - - if (html.document.querySelector('flt-glass-pane')?.shadowRoot != null) { - // In flutter master... - expect(element?.id, isNot('background-html-view')); - } else { - // In previous versions (--web-renderer=html only)... - expect(element?.tagName.toLowerCase(), 'flt-platform-view'); - final html.Element? platformViewRoot = - element?.shadowRoot?.getElementById('background-html-view'); - expect(platformViewRoot, isNull); - } - }); + final html.Element element = + _getHtmlElementAtCenter(clickableButtonFinder, tester); + + expect(element.id, isNot('background-html-view')); + }, semanticsEnabled: false); testWidgets( 'on wrapped elements with intercepting set to false, the browser hits the background-html-view', @@ -49,20 +41,11 @@ void main() { app.main(); await tester.pumpAndSettle(); - final html.Element? element = - _getHtmlElementFromFinder(clickableWrappedButtonFinder, tester); - - if (html.document.querySelector('flt-glass-pane')?.shadowRoot != null) { - // In flutter master... - expect(element?.id, 'background-html-view'); - } else { - // In previous versions (--web-renderer=html only)... - expect(element?.tagName.toLowerCase(), 'flt-platform-view'); - final html.Element? platformViewRoot = - element?.shadowRoot?.getElementById('background-html-view'); - expect(platformViewRoot, isNotNull); - } - }); + final html.Element element = + _getHtmlElementAtCenter(clickableWrappedButtonFinder, tester); + + expect(element.id, 'background-html-view'); + }, semanticsEnabled: false); testWidgets( 'on unwrapped elements, the browser hits the background-html-view', @@ -70,27 +53,99 @@ void main() { app.main(); await tester.pumpAndSettle(); - final html.Element? element = - _getHtmlElementFromFinder(nonClickableButtonFinder, tester); - - if (html.document.querySelector('flt-glass-pane')?.shadowRoot != null) { - // In flutter master... - expect(element?.id, 'background-html-view'); - } else { - // In previous versions (--web-renderer=html only)... - expect(element?.tagName.toLowerCase(), 'flt-platform-view'); - final html.Element? platformViewRoot = - element?.shadowRoot?.getElementById('background-html-view'); - expect(platformViewRoot, isNotNull); - } + final html.Element element = + _getHtmlElementAtCenter(nonClickableButtonFinder, tester); + + expect(element.id, 'background-html-view'); + }, semanticsEnabled: false); + + testWidgets('on background directly', (WidgetTester tester) async { + app.main(); + await tester.pumpAndSettle(); + + final html.Element element = + _getHtmlElementAt(tester.getTopLeft(backgroundFinder)); + + expect(element.id, 'background-html-view'); + }, semanticsEnabled: false); + }); + + group('With semantics', () { + testWidgets('finds semantics of wrapped widgets', + (WidgetTester tester) async { + app.main(); + await tester.pumpAndSettle(); + + final html.Element element = + _getHtmlElementAtCenter(clickableButtonFinder, tester); + + expect(element.tagName.toLowerCase(), 'flt-semantics'); + expect(element.getAttribute('aria-label'), 'Works As Expected'); + }); + + testWidgets( + 'finds semantics of wrapped widgets with intercepting set to false', + (WidgetTester tester) async { + app.main(); + await tester.pumpAndSettle(); + + final html.Element element = + _getHtmlElementAtCenter(clickableWrappedButtonFinder, tester); + + expect(element.tagName.toLowerCase(), 'flt-semantics'); + expect(element.getAttribute('aria-label'), + 'Never calls onPressed transparent'); + }); + + testWidgets('finds semantics of unwrapped elements', + (WidgetTester tester) async { + app.main(); + await tester.pumpAndSettle(); + + final html.Element element = + _getHtmlElementAtCenter(nonClickableButtonFinder, tester); + + expect(element.tagName.toLowerCase(), 'flt-semantics'); + expect(element.getAttribute('aria-label'), 'Never calls onPressed'); + }); + + // Notice that, when hit-testing the background platform view, instead of + // finding a semantics node, the platform view itself is found. This is + // because the platform view does not add interactive semantics nodes into + // the framework's semantics tree. Instead, its semantics is determined by + // the HTML content of the platform view itself. Flutter's semantics tree + // simply allows the hit test to land on the platform view by making itself + // hit test transparent. + testWidgets('on background directly', (WidgetTester tester) async { + app.main(); + await tester.pumpAndSettle(); + + final html.Element element = + _getHtmlElementAt(tester.getTopLeft(backgroundFinder)); + + expect(element.id, 'background-html-view'); }); }); } -// This functions locates a widget from a Finder, and asks the browser what's the -// DOM element in the center of the coordinates of the widget. (Returns *which* -// DOM element will handle Mouse interactions first at those coordinates.) -html.Element? _getHtmlElementFromFinder(Finder finder, WidgetTester tester) { +// Calls [_getHtmlElementAt] passing it the center of the widget identified by +// the `finder`. +html.Element _getHtmlElementAtCenter(Finder finder, WidgetTester tester) { final Offset point = tester.getCenter(finder); - return html.document.elementFromPoint(point.dx.toInt(), point.dy.toInt()); + return _getHtmlElementAt(point); +} + +// Locates the DOM element at the given `point` using `elementFromPoint`. +// +// `elementFromPoint` is an approximate proxy for a hit test, although it's +// sensitive to the presence of shadow roots and browser quirks (not all +// browsers agree on what it should return in all situations). Since this test +// runs only in Chromium, it relies on Chromium's behavior. +html.Element _getHtmlElementAt(Offset point) { + // Probe at the shadow so the browser reports semantics nodes in addition to + // platform view elements. If probed from `html.document` the browser hides + // the contents of as an implementation detail. + final html.ShadowRoot glassPaneShadow = + html.document.querySelector('flt-glass-pane')!.shadowRoot!; + return glassPaneShadow.elementFromPoint(point.dx.toInt(), point.dy.toInt())!; } diff --git a/packages/pointer_interceptor/example/lib/main.dart b/packages/pointer_interceptor/example/lib/main.dart index 234d31df61d..b30ea010bf6 100644 --- a/packages/pointer_interceptor/example/lib/main.dart +++ b/packages/pointer_interceptor/example/lib/main.dart @@ -116,6 +116,7 @@ class _MyHomePageState extends State { alignment: Alignment.center, children: [ HtmlElement( + key: const ValueKey('background-widget'), onClick: () { _clickedOn('html-element'); }, @@ -134,7 +135,8 @@ class _MyHomePageState extends State { intercepting: false, child: ElevatedButton( key: const Key('wrapped-transparent-button'), - child: const Text('Never calls onPressed'), + child: + const Text('Never calls onPressed transparent'), onPressed: () { _clickedOn('wrapped-transparent-button'); }, diff --git a/packages/pointer_interceptor/pubspec.yaml b/packages/pointer_interceptor/pubspec.yaml index 753dfd14c56..2f5d76290b1 100644 --- a/packages/pointer_interceptor/pubspec.yaml +++ b/packages/pointer_interceptor/pubspec.yaml @@ -2,7 +2,7 @@ name: pointer_interceptor description: A widget to prevent clicks from being swallowed by underlying HtmlElementViews on the web. repository: https://github.com/flutter/packages/tree/main/packages/pointer_interceptor issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+pointer_interceptor%22 -version: 0.9.3 +version: 0.9.3+1 environment: sdk: ">=2.12.0 <3.0.0"