[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);
   }));