[web] Migrate Flutter Web DOM usage to JS static interop - 44. (#33380)
diff --git a/lib/web_ui/lib/src/engine/dom.dart b/lib/web_ui/lib/src/engine/dom.dart
index d409f61..78dffb9 100644
--- a/lib/web_ui/lib/src/engine/dom.dart
+++ b/lib/web_ui/lib/src/engine/dom.dart
@@ -49,6 +49,13 @@
]) as DomCSSStyleDeclaration;
external DomScreen? get screen;
external int requestAnimationFrame(DomRequestAnimationFrameCallback callback);
+ void postMessage(Object message, String targetOrigin,
+ [List<DomMessagePort>? messagePorts]) =>
+ js_util.callMethod(this, 'postMessage', <Object>[
+ message,
+ targetOrigin,
+ if (messagePorts != null) js_util.jsify(messagePorts)
+ ]);
}
typedef DomRequestAnimationFrameCallback = void Function(num highResTime);
@@ -936,6 +943,8 @@
external String? get search;
// We have to change the name here because 'hash' is inherited from [Object].
String get locationHash => js_util.getProperty(this, 'hash');
+ external String get origin;
+ external String get href;
}
@JS()
@@ -1132,8 +1141,8 @@
external String? get data;
}
-DomCompositionEvent createDomCompositionEvent(String type, [Map<dynamic,
- dynamic>? options]) =>
+DomCompositionEvent createDomCompositionEvent(String type,
+ [Map<dynamic, dynamic>? options]) =>
js_util.callConstructor(domGetConstructor('CompositionEvent')!,
<Object>[type, if (options != null) js_util.jsify(options)]);
@@ -1174,6 +1183,7 @@
extension DomTokenListExtension on DomTokenList {
external void add(String value);
+ external void remove(String value);
external bool contains(String token);
}
@@ -1317,6 +1327,64 @@
DomPoint(this.x, this.y);
}
+@JS()
+@staticInterop
+class DomWebSocket extends DomEventTarget {}
+
+extension DomWebSocketExtension on DomWebSocket {
+ external void send(Object? data);
+}
+
+DomWebSocket createDomWebSocket(String url) =>
+ domCallConstructorString('WebSocket', <Object>[url])! as DomWebSocket;
+
+@JS()
+@staticInterop
+class DomMessageEvent extends DomEvent {}
+
+extension DomMessageEventExtension on DomMessageEvent {
+ dynamic get data => js_util.dartify(js_util.getProperty(this, 'data'));
+ external String get origin;
+}
+
+@JS()
+@staticInterop
+class DomHTMLIFrameElement extends DomHTMLElement {}
+
+extension DomHTMLIFrameElementExtension on DomHTMLIFrameElement {
+ external set src(String? value);
+ external String? get src;
+ external set height(String? value);
+ external set width(String? value);
+ external DomWindow get contentWindow;
+}
+
+DomHTMLIFrameElement createDomHTMLIFrameElement() =>
+ domDocument.createElement('iframe') as DomHTMLIFrameElement;
+
+@JS()
+@staticInterop
+class DomMessagePort extends DomEventTarget {}
+
+extension DomMessagePortExtension on DomMessagePort {
+ void postMessage(Object? message) => js_util.callMethod(this, 'postMessage',
+ <Object>[if (message != null) js_util.jsify(message)]);
+ external void start();
+}
+
+@JS()
+@staticInterop
+class DomMessageChannel {}
+
+extension DomMessageChannelExtension on DomMessageChannel {
+ external DomMessagePort get port1;
+ external DomMessagePort get port2;
+}
+
+DomMessageChannel createDomMessageChannel() =>
+ domCallConstructorString('MessageChannel', <Object>[])!
+ as DomMessageChannel;
+
Object? domGetConstructor(String constructorName) =>
js_util.getProperty(domWindow, constructorName);
diff --git a/sky/packages/sky_engine/BUILD.gn b/sky/packages/sky_engine/BUILD.gn
index c587ad8..28d7ea3 100644
--- a/sky/packages/sky_engine/BUILD.gn
+++ b/sky/packages/sky_engine/BUILD.gn
@@ -200,7 +200,6 @@
" \"dart:core\": \"core/core.dart\"",
" \"dart:developer\": \"developer/developer.dart\"",
" \"dart:ffi\": \"ffi/ffi.dart\"",
- " \"dart:html\": \"html/html_dart2js.dart\"",
" \"dart:io\": \"io/io.dart\"",
" \"dart:isolate\": \"isolate/isolate.dart\"",
" \"dart:js\": \"js/js.dart\"",
diff --git a/sky/packages/sky_engine/lib/_embedder.yaml b/sky/packages/sky_engine/lib/_embedder.yaml
index c59d362..0901274 100644
--- a/sky/packages/sky_engine/lib/_embedder.yaml
+++ b/sky/packages/sky_engine/lib/_embedder.yaml
@@ -8,7 +8,6 @@
"dart:core": "../../../../../third_party/dart/sdk/lib/core/core.dart"
"dart:developer": "../../../../../third_party/dart/sdk/lib/developer/developer.dart"
"dart:ffi": "../../../../../third_party/dart/sdk/lib/ffi/ffi.dart"
- "dart:html": "../../../../../third_party/dart/sdk/lib/html/html_dart2js.dart"
"dart:io": "../../../../../third_party/dart/sdk/lib/io/io.dart"
"dart:isolate": "../../../../../third_party/dart/sdk/lib/isolate/isolate.dart"
"dart:js": "../../../../../third_party/dart/sdk/lib/js/js.dart"
diff --git a/web_sdk/sdk_rewriter.dart b/web_sdk/sdk_rewriter.dart
index f7dfbf8..3ab50d5 100644
--- a/web_sdk/sdk_rewriter.dart
+++ b/web_sdk/sdk_rewriter.dart
@@ -42,7 +42,6 @@
import 'dart:collection';
import 'dart:convert' hide Codec;
import 'dart:developer' as developer;
-import 'dart:html' as html;
import 'dart:js' as js;
import 'dart:js_util' as js_util;
import 'dart:_js_annotations';
diff --git a/web_sdk/test/js_access_test.dart b/web_sdk/test/js_access_test.dart
index 81ab8c5..dc45714 100644
--- a/web_sdk/test/js_access_test.dart
+++ b/web_sdk/test/js_access_test.dart
@@ -64,8 +64,6 @@
final _CheckResult result = _checkFile(
File('lib/web_ui/lib/src/engine/alarm_clock.dart'),
'''
-import 'dart:html'
- show HtmlElement;
import 'dart:async';
import 'package:ui/ui.dart'
as ui;
@@ -73,8 +71,7 @@
);
expect(result.failed, isTrue);
expect(result.violations, <String>[
- "on line 1: import is broken up into multiple lines: import 'dart:html'",
- "on line 4: import is broken up into multiple lines: import 'package:ui/ui.dart'",
+ "on line 2: import is broken up into multiple lines: import 'package:ui/ui.dart'",
]);
}
diff --git a/web_sdk/test/sdk_rewriter_test.dart b/web_sdk/test/sdk_rewriter_test.dart
index 69c707f..383db27 100644
--- a/web_sdk/test/sdk_rewriter_test.dart
+++ b/web_sdk/test/sdk_rewriter_test.dart
@@ -30,7 +30,6 @@
import 'dart:collection';
import 'dart:convert' hide Codec;
import 'dart:developer' as developer;
-import 'dart:html' as html;
import 'dart:js' as js;
import 'dart:js_util' as js_util;
import 'dart:_js_annotations';
diff --git a/web_sdk/web_engine_tester/lib/golden_tester.dart b/web_sdk/web_engine_tester/lib/golden_tester.dart
index db7b0dc..e5c2f04 100644
--- a/web_sdk/web_engine_tester/lib/golden_tester.dart
+++ b/web_sdk/web_engine_tester/lib/golden_tester.dart
@@ -4,14 +4,16 @@
import 'dart:async';
import 'dart:convert';
-import 'dart:html' as html;
import 'package:test/test.dart';
+// ignore: implementation_imports
import 'package:ui/src/engine.dart' show operatingSystem, OperatingSystem, useCanvasKit;
+// ignore: implementation_imports
+import 'package:ui/src/engine/dom.dart';
import 'package:ui/ui.dart';
Future<dynamic> _callScreenshotServer(dynamic requestData) async {
- final html.HttpRequest request = await html.HttpRequest.request(
+ final DomXMLHttpRequest request = await domHttpRequest(
'screenshot',
method: 'POST',
sendData: json.encode(requestData),
diff --git a/web_sdk/web_engine_tester/lib/static/host.dart b/web_sdk/web_engine_tester/lib/static/host.dart
index 2af3419..e98cc23 100644
--- a/web_sdk/web_engine_tester/lib/static/host.dart
+++ b/web_sdk/web_engine_tester/lib/static/host.dart
@@ -7,11 +7,12 @@
import 'dart:async';
import 'dart:convert';
-import 'dart:html';
import 'package:js/js.dart';
import 'package:stack_trace/stack_trace.dart';
import 'package:stream_channel/stream_channel.dart';
+// ignore: implementation_imports
+import 'package:ui/src/engine/dom.dart';
/// A class defined in content shell, used to control its behavior.
@JS()
@@ -46,13 +47,14 @@
external set _jsApi(_JSApi api);
/// The iframes created for each loaded test suite, indexed by the suite id.
-final Map<int, IFrameElement> _iframes = <int, IFrameElement>{};
+final Map<int, DomHTMLIFrameElement> _iframes = <int, DomHTMLIFrameElement>{};
/// Subscriptions created for each loaded test suite, indexed by the suite id.
-final Map<int, List<StreamSubscription<dynamic>>> _subscriptions = <int, List<StreamSubscription<dynamic>>>{};
+final Map<int, List<DomSubscription>> _domSubscriptions = <int, List<DomSubscription>>{};
+final Map<int, List<StreamSubscription<dynamic>>> _streamSubscriptions =<int, List<StreamSubscription<dynamic>>>{};
/// The URL for the current page.
-final Uri _currentUrl = Uri.parse(window.location.href);
+final Uri _currentUrl = Uri.parse(domWindow.location.href);
/// Code that runs in the browser and loads test suites at the server's behest.
///
@@ -111,7 +113,7 @@
testRunner?.waitUntilDone();
if (_currentUrl.queryParameters['debug'] == 'true') {
- document.body!.classes.add('debug');
+ domDocument.body!.classList.add('debug');
}
runZonedGuarded(
@@ -126,14 +128,17 @@
final StreamChannel<dynamic> iframeChannel = _connectToIframe(url, messageId);
suiteChannel.pipe(iframeChannel);
} else if (message['command'] == 'displayPause') {
- document.body!.classes.add('paused');
+ domDocument.body!.classList.add('paused');
} else if (message['command'] == 'resume') {
- document.body!.classes.remove('paused');
+ domDocument.body!.classList.remove('paused');
} else {
assert(message['command'] == 'closeSuite');
_iframes.remove(message['id'])!.remove();
- for (final StreamSubscription<dynamic> subscription in _subscriptions.remove(message['id'])!) {
+ for (final DomSubscription subscription in _domSubscriptions.remove(message['id'])!) {
+ subscription.cancel();
+ }
+ for (final StreamSubscription<dynamic> subscription in _streamSubscriptions.remove(message['id'])!) {
subscription.cancel();
}
}
@@ -145,9 +150,10 @@
(_) => serverChannel.sink.add(<String, String>{'command': 'ping'}));
_jsApi = _JSApi(resume: allowInterop(() {
- if (!document.body!.classes.remove('paused')) {
+ if (!domDocument.body!.classList.contains('paused')) {
return;
}
+ domDocument.body!.classList.remove('paused');
serverChannel.sink.add(<String, String>{'command': 'resume'});
}), restartCurrent: allowInterop(() {
serverChannel.sink.add(<String, String>{'command': 'restart'});
@@ -164,13 +170,13 @@
MultiChannel<dynamic> _connectToServer() {
// The `managerUrl` query parameter contains the WebSocket URL of the remote
// [BrowserManager] with which this communicates.
- final WebSocket webSocket = WebSocket(_currentUrl.queryParameters['managerUrl']!);
+ final DomWebSocket webSocket = createDomWebSocket(_currentUrl.queryParameters['managerUrl']!);
final StreamChannelController<dynamic> controller = StreamChannelController<dynamic>(sync: true);
- webSocket.onMessage.listen((MessageEvent message) {
- final String data = message.data as String;
+ webSocket.addEventListener('message', allowInterop((DomEvent message) {
+ final String data = (message as DomMessageEvent).data as String;
controller.local.sink.add(jsonDecode(data));
- });
+ }));
controller.local.stream
.listen((dynamic message) => webSocket.send(jsonEncode(message)));
@@ -183,31 +189,34 @@
///
/// [id] identifies the suite loaded in this iframe.
StreamChannel<dynamic> _connectToIframe(String url, int id) {
- final IFrameElement iframe = IFrameElement();
+ final DomHTMLIFrameElement iframe = createDomHTMLIFrameElement();
_iframes[id] = iframe;
iframe
..src = url
..width = '1000'
..height = '1000';
- document.body!.children.add(iframe);
+ domDocument.body!.appendChild(iframe);
// Use this to communicate securely with the iframe.
- final MessageChannel channel = MessageChannel();
+ final DomMessageChannel channel = createDomMessageChannel();
final StreamChannelController<dynamic> controller = StreamChannelController<dynamic>(sync: true);
// Use this to avoid sending a message to the iframe before it's sent a
// message to us. This ensures that no messages get dropped on the floor.
final Completer<dynamic> readyCompleter = Completer<dynamic>();
- final List<StreamSubscription<dynamic>> subscriptions = <StreamSubscription<dynamic>>[];
- _subscriptions[id] = subscriptions;
-
- subscriptions.add(window.onMessage.listen((dynamic message) {
+ final List<DomSubscription> domSubscriptions = <DomSubscription>[];
+ final List<StreamSubscription<dynamic>> streamSubscriptions = <StreamSubscription<dynamic>>[];
+ _domSubscriptions[id] = domSubscriptions;
+ _streamSubscriptions[id] = streamSubscriptions;
+ domSubscriptions.add(DomSubscription(domWindow, 'message',
+ allowInterop((DomEvent event) {
+ final DomMessageEvent message = event as DomMessageEvent;
// A message on the Window can theoretically come from any website. It's
// very unlikely that a malicious site would care about hacking someone's
// unit tests, let alone be able to find the test server while it's
// running, but it's good practice to check the origin anyway.
- if (message.origin != window.location.origin) {
+ if (message.origin != domWindow.location.origin) {
return;
}
@@ -220,20 +229,24 @@
if (message.data['ready'] == true) {
// This message indicates that the iframe is actively listening for
// events, so the message channel's second port can now be transferred.
- iframe.contentWindow!.postMessage('port', window.location.origin, <MessagePort>[channel.port2]);
+ channel.port2.start();
+ iframe.contentWindow.postMessage('port', domWindow.location.origin,
+ <DomMessagePort>[channel.port2]);
readyCompleter.complete();
} else if (message.data['exception'] == true) {
// This message from `dart.js` indicates that an exception occurred
// loading the test.
controller.local.sink.add(message.data['data']);
}
- }));
+ })));
- subscriptions.add(channel.port1.onMessage.listen((dynamic message) {
- controller.local.sink.add(message.data['data']);
- }));
+ channel.port1.start();
+ domSubscriptions.add(DomSubscription(channel.port1, 'message',
+ allowInterop((DomEvent message) {
+ controller.local.sink.add((message as DomMessageEvent).data['data']);
+ })));
- subscriptions.add(controller.local.stream.listen((dynamic message) async {
+ streamSubscriptions.add(controller.local.stream.listen((dynamic message) async {
await readyCompleter.future;
channel.port1.postMessage(message);
}));