Skip to content

Commit 6b2249f

Browse files
ditmanbalvinderz
andauthored
[pointer_interceptor_web] Update package APIs and tests. (#5785)
## Changes: * Updates the web implementation of the `pointer_interceptor` to `package:web`, and the latest `HtmlElementView` APIs available on Flutter. * Resolves a timing issue in integration tests by waiting a few frames before doing any assertions from the DOM of a Flutter Web app, that should fix flutter rolls. ## Issues: * Fixes flutter/flutter#140834 * Fixes flutter/flutter#139753 * Closes #5673 (Credit to @balvinderz!) ## Tests * Tested manually. All integration tests pass locally. --- Co-authored-by: balvinderz <[email protected]>
1 parent ece0d7b commit 6b2249f

File tree

10 files changed

+130
-148
lines changed

10 files changed

+130
-148
lines changed

packages/pointer_interceptor/pointer_interceptor_web/CHANGELOG.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1+
## 0.10.1
2+
3+
* Uses `HtmlElementView.fromTagName` instead of custom factories.
4+
* Migrates package and tests to `platform:web`.
5+
* Updates minimum supported SDK version to Flutter 3.16.0/Dart 3.2.0.
6+
17
## 0.10.0
28

39
* Moves web implementation to its own package.
4-

packages/pointer_interceptor/pointer_interceptor_web/example/integration_test/widget_test.dart

Lines changed: 37 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,19 @@
44

55
// ignore_for_file: avoid_print
66

7-
import 'dart:html' as html;
8-
97
// Imports the Flutter Driver API.
108
import 'package:flutter/src/widgets/framework.dart';
119
import 'package:flutter_test/flutter_test.dart';
1210
import 'package:integration_test/integration_test.dart';
13-
1411
import 'package:pointer_interceptor_web_example/main.dart' as app;
12+
import 'package:web/web.dart' as web;
1513

1614
final Finder nonClickableButtonFinder =
1715
find.byKey(const Key('transparent-button'));
1816
final Finder clickableWrappedButtonFinder =
1917
find.byKey(const Key('wrapped-transparent-button'));
2018
final Finder clickableButtonFinder = find.byKey(const Key('clickable-button'));
21-
final Finder backgroundFinder =
22-
find.byKey(const ValueKey<String>('background-widget'));
19+
final Finder backgroundFinder = find.byKey(const Key('background-widget'));
2320

2421
void main() {
2522
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
@@ -28,10 +25,9 @@ void main() {
2825
testWidgets(
2926
'on wrapped elements, the browser does not hit the background-html-view',
3027
(WidgetTester tester) async {
31-
app.main();
32-
await tester.pumpAndSettle();
28+
await _fullyRenderApp(tester);
3329

34-
final html.Element element =
30+
final web.Element element =
3531
_getHtmlElementAtCenter(clickableButtonFinder, tester);
3632

3733
expect(element.id, isNot('background-html-view'));
@@ -40,10 +36,9 @@ void main() {
4036
testWidgets(
4137
'on wrapped elements with intercepting set to false, the browser hits the background-html-view',
4238
(WidgetTester tester) async {
43-
app.main();
44-
await tester.pumpAndSettle();
39+
await _fullyRenderApp(tester);
4540

46-
final html.Element element =
41+
final web.Element element =
4742
_getHtmlElementAtCenter(clickableWrappedButtonFinder, tester);
4843

4944
expect(element.id, 'background-html-view');
@@ -52,20 +47,18 @@ void main() {
5247
testWidgets(
5348
'on unwrapped elements, the browser hits the background-html-view',
5449
(WidgetTester tester) async {
55-
app.main();
56-
await tester.pumpAndSettle();
50+
await _fullyRenderApp(tester);
5751

58-
final html.Element element =
52+
final web.Element element =
5953
_getHtmlElementAtCenter(nonClickableButtonFinder, tester);
6054

6155
expect(element.id, 'background-html-view');
6256
}, semanticsEnabled: false);
6357

6458
testWidgets('on background directly', (WidgetTester tester) async {
65-
app.main();
66-
await tester.pumpAndSettle();
59+
await _fullyRenderApp(tester);
6760

68-
final html.Element element =
61+
final web.Element element =
6962
_getHtmlElementAt(tester.getTopLeft(backgroundFinder));
7063

7164
expect(element.id, 'background-html-view');
@@ -75,15 +68,9 @@ void main() {
7568
group('With semantics', () {
7669
testWidgets('finds semantics of wrapped widgets',
7770
(WidgetTester tester) async {
78-
app.main();
79-
await tester.pumpAndSettle();
71+
await _fullyRenderApp(tester);
8072

81-
if (!_newSemanticsAvailable()) {
82-
print('Skipping test: Needs flutter > 2.10');
83-
return;
84-
}
85-
86-
final html.Element element =
73+
final web.Element element =
8774
_getHtmlElementAtCenter(clickableButtonFinder, tester);
8875

8976
expect(element.tagName.toLowerCase(), 'flt-semantics');
@@ -93,15 +80,9 @@ void main() {
9380
testWidgets(
9481
'finds semantics of wrapped widgets with intercepting set to false',
9582
(WidgetTester tester) async {
96-
app.main();
97-
await tester.pumpAndSettle();
98-
99-
if (!_newSemanticsAvailable()) {
100-
print('Skipping test: Needs flutter > 2.10');
101-
return;
102-
}
83+
await _fullyRenderApp(tester);
10384

104-
final html.Element element =
85+
final web.Element element =
10586
_getHtmlElementAtCenter(clickableWrappedButtonFinder, tester);
10687

10788
expect(element.tagName.toLowerCase(), 'flt-semantics');
@@ -111,15 +92,9 @@ void main() {
11192

11293
testWidgets('finds semantics of unwrapped elements',
11394
(WidgetTester tester) async {
114-
app.main();
115-
await tester.pumpAndSettle();
95+
await _fullyRenderApp(tester);
11696

117-
if (!_newSemanticsAvailable()) {
118-
print('Skipping test: Needs flutter > 2.10');
119-
return;
120-
}
121-
122-
final html.Element element =
97+
final web.Element element =
12398
_getHtmlElementAtCenter(nonClickableButtonFinder, tester);
12499

125100
expect(element.tagName.toLowerCase(), 'flt-semantics');
@@ -134,20 +109,26 @@ void main() {
134109
// simply allows the hit test to land on the platform view by making itself
135110
// hit test transparent.
136111
testWidgets('on background directly', (WidgetTester tester) async {
137-
app.main();
138-
await tester.pumpAndSettle();
112+
await _fullyRenderApp(tester);
139113

140-
final html.Element element =
114+
final web.Element element =
141115
_getHtmlElementAt(tester.getTopLeft(backgroundFinder));
142116

143117
expect(element.id, 'background-html-view');
144118
});
145119
});
146120
}
147121

122+
Future<void> _fullyRenderApp(WidgetTester tester) async {
123+
await tester.pumpWidget(const app.MyApp());
124+
// Pump 2 frames so the framework injects the platform view into the DOM.
125+
await tester.pump();
126+
await tester.pump();
127+
}
128+
148129
// Calls [_getHtmlElementAt] passing it the center of the widget identified by
149130
// the `finder`.
150-
html.Element _getHtmlElementAtCenter(Finder finder, WidgetTester tester) {
131+
web.Element _getHtmlElementAtCenter(Finder finder, WidgetTester tester) {
151132
final Offset point = tester.getCenter(finder);
152133
return _getHtmlElementAt(point);
153134
}
@@ -158,22 +139,20 @@ html.Element _getHtmlElementAtCenter(Finder finder, WidgetTester tester) {
158139
// sensitive to the presence of shadow roots and browser quirks (not all
159140
// browsers agree on what it should return in all situations). Since this test
160141
// runs only in Chromium, it relies on Chromium's behavior.
161-
html.Element _getHtmlElementAt(Offset point) {
142+
web.Element _getHtmlElementAt(Offset point) {
162143
// Probe at the shadow so the browser reports semantics nodes in addition to
163144
// platform view elements. If probed from `html.document` the browser hides
164145
// the contents of <flt-glass-name> as an implementation detail.
165-
final html.ShadowRoot glassPaneShadow =
166-
html.document.querySelector('flt-glass-pane')!.shadowRoot!;
167-
return glassPaneShadow.elementFromPoint(point.dx.toInt(), point.dy.toInt())!;
146+
final web.ShadowRoot glassPaneShadow =
147+
web.document.querySelector('flt-glass-pane')!.shadowRoot!;
148+
// Use `round` below to ensure clicks always fall *inside* the located
149+
// element, rather than truncating the decimals.
150+
// Truncating decimals makes some tests fail when a centered element (in high
151+
// DPI) is not exactly aligned to the pixel grid (because the browser *rounds*)
152+
return glassPaneShadow.elementFromPoint(point.dx.round(), point.dy.round());
168153
}
169154

170-
// TODO(dit): Remove this after flutter master (2.13) lands into stable.
171-
// This detects that we can do new semantics assertions by looking at the 'id'
172-
// attribute on flt-semantics elements (it is now set in 2.13 and up).
173-
bool _newSemanticsAvailable() {
174-
final html.ShadowRoot glassPaneShadow =
175-
html.document.querySelector('flt-glass-pane')!.shadowRoot!;
176-
final List<html.Element> elements =
177-
glassPaneShadow.querySelectorAll('flt-semantics[id]');
178-
return elements.isNotEmpty;
155+
/// Shady API: https://github.com/w3c/csswg-drafts/issues/556
156+
extension ElementFromPointInShadowRoot on web.ShadowRoot {
157+
external web.Element elementFromPoint(int x, int y);
179158
}

packages/pointer_interceptor/pointer_interceptor_web/example/lib/main.dart

Lines changed: 44 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -2,50 +2,51 @@
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
44

5-
// ignore: avoid_web_libraries_in_flutter
6-
import 'dart:html' as html;
5+
import 'dart:js_interop';
76
import 'dart:ui_web' as ui_web;
87

98
import 'package:flutter/material.dart';
109
import 'package:pointer_interceptor_platform_interface/pointer_interceptor_platform_interface.dart';
1110
import 'package:pointer_interceptor_web/pointer_interceptor_web.dart';
11+
import 'package:web/web.dart' as web;
1212

1313
const String _htmlElementViewType = '_htmlElementViewType';
14-
const double _videoWidth = 640;
15-
const double _videoHeight = 480;
14+
const double _containerWidth = 640;
15+
const double _containerHeight = 480;
1616

1717
/// The html.Element that will be rendered underneath the flutter UI.
18-
html.Element htmlElement = html.DivElement()
19-
..style.width = '100%'
20-
..style.height = '100%'
21-
..style.backgroundColor = '#fabada'
22-
..style.cursor = 'auto'
23-
..id = 'background-html-view';
18+
final web.Element _htmlElement =
19+
(web.document.createElement('div') as web.HTMLDivElement)
20+
..style.width = '100%'
21+
..style.height = '100%'
22+
..style.backgroundColor = '#fabada'
23+
..style.cursor = 'auto'
24+
..id = 'background-html-view';
2425

2526
// See other examples commented out below...
2627

27-
// html.Element htmlElement = html.VideoElement()
28-
// ..style.width = '100%'
29-
// ..style.height = '100%'
30-
// ..style.cursor = 'auto'
31-
// ..style.backgroundColor = 'black'
32-
// ..id = 'background-html-view'
33-
// ..src = 'https://archive.org/download/BigBuckBunny_124/Content/big_buck_bunny_720p_surround.mp4'
34-
// ..poster = 'https://peach.blender.org/wp-content/uploads/title_anouncement.jpg?x11217'
35-
// ..controls = true;
36-
37-
// html.Element htmlElement = html.IFrameElement()
28+
// final web.Element _htmlElement =
29+
// (web.document.createElement('video') as web.HTMLVideoElement)
30+
// ..style.width = '100%'
31+
// ..style.height = '100%'
32+
// ..style.cursor = 'auto'
33+
// ..style.backgroundColor = 'black'
34+
// ..id = 'background-html-view'
35+
// ..src =
36+
// 'https://archive.org/download/BigBuckBunny_124/Content/big_buck_bunny_720p_surround.mp4'
37+
// ..poster =
38+
// 'https://peach.blender.org/wp-content/uploads/title_anouncement.jpg?x11217'
39+
// ..controls = true;
40+
41+
// final web.Element _htmlElement =
42+
// (web.document.createElement('video') as web.HTMLIFrameElement)
3843
// ..width = '100%'
3944
// ..height = '100%'
4045
// ..id = 'background-html-view'
4146
// ..src = 'https://www.youtube.com/embed/IyFZznAk69U'
4247
// ..style.border = 'none';
4348

4449
void main() {
45-
ui_web.platformViewRegistry.registerViewFactory(
46-
_htmlElementViewType,
47-
(int viewId) => htmlElement,
48-
);
4950
runApp(const MyApp());
5051
}
5152

@@ -81,6 +82,15 @@ class _MyHomePageState extends State<MyHomePage> {
8182
});
8283
}
8384

85+
@override
86+
void initState() {
87+
super.initState();
88+
ui_web.platformViewRegistry.registerViewFactory(
89+
_htmlElementViewType,
90+
(int viewId) => _htmlElement,
91+
);
92+
}
93+
8494
@override
8595
Widget build(BuildContext context) {
8696
return Scaffold(
@@ -109,13 +119,13 @@ class _MyHomePageState extends State<MyHomePage> {
109119
),
110120
Container(
111121
color: Colors.black,
112-
width: _videoWidth,
113-
height: _videoHeight,
122+
width: _containerWidth,
123+
height: _containerHeight,
114124
child: Stack(
115125
alignment: Alignment.center,
116126
children: <Widget>[
117127
HtmlElement(
118-
key: const ValueKey<String>('background-widget'),
128+
key: const Key('background-widget'),
119129
onClick: () {
120130
_clickedOn('html-element');
121131
},
@@ -207,9 +217,12 @@ class HtmlElement extends StatelessWidget {
207217

208218
@override
209219
Widget build(BuildContext context) {
210-
htmlElement.onClick.listen((_) {
211-
onClick();
212-
});
220+
_htmlElement.addEventListener(
221+
'click',
222+
(JSAny? _) {
223+
onClick();
224+
}.toJS,
225+
);
213226

214227
return const HtmlElementView(
215228
viewType: _htmlElementViewType,

packages/pointer_interceptor/pointer_interceptor_web/example/pubspec.yaml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ description: "Demonstrates how to use the pointer_interceptor_web plugin."
33
publish_to: 'none'
44

55
environment:
6-
sdk: ">=3.1.0 <4.0.0"
7-
flutter: ">=3.13.0"
6+
sdk: ^3.2.0
7+
flutter: '>=3.16.0'
88

99
dependencies:
1010
cupertino_icons: ^1.0.2
@@ -13,6 +13,7 @@ dependencies:
1313
pointer_interceptor_platform_interface: ^0.10.0
1414
pointer_interceptor_web:
1515
path: ../../pointer_interceptor_web
16+
web: '>=0.3.0 <0.5.0'
1617

1718
dev_dependencies:
1819
flutter_test:
5.46 KB
Loading
20.5 KB
Loading

packages/pointer_interceptor/pointer_interceptor_web/example/web/index.html

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,13 @@
1111
The path provided below has to start and end with a slash "/" in order for
1212
it to work correctly.
1313
14-
Fore more details:
14+
For more details:
1515
* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base
16+
17+
This is a placeholder for base href that will be replaced by the value of
18+
the `--base-href` argument provided to `flutter build`.
1619
-->
17-
<base href="/">
20+
<base href="$FLUTTER_BASE_HREF">
1821

1922
<meta charset="UTF-8">
2023
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
@@ -31,18 +34,21 @@
3134

3235
<title>example</title>
3336
<link rel="manifest" href="manifest.json">
37+
38+
<!-- This script adds the flutter initialization JS code -->
39+
<script src="flutter.js" defer></script>
3440
</head>
3541
<body>
36-
<!-- This script installs service_worker.js to provide PWA functionality to
37-
application. For more information, see:
38-
https://developers.google.com/web/fundamentals/primers/service-workers -->
3942
<script>
40-
if ('serviceWorker' in navigator) {
41-
window.addEventListener('flutter-first-frame', function () {
42-
navigator.serviceWorker.register('flutter_service_worker.js');
43+
window.addEventListener('load', function(ev) {
44+
// Download main.dart.js
45+
_flutter.loader.loadEntrypoint({
46+
onEntrypointLoaded: async function(engineInitializer) {
47+
let appRunner = await engineInitializer.initializeEngine();
48+
appRunner.runApp();
49+
}
4350
});
44-
}
51+
});
4552
</script>
46-
<script src="main.dart.js" type="application/javascript"></script>
4753
</body>
4854
</html>

0 commit comments

Comments
 (0)