diff --git a/.ci.yaml b/.ci.yaml index 0f0f321c32..b44649f26a 100644 --- a/.ci.yaml +++ b/.ci.yaml @@ -1725,6 +1725,190 @@ targets: - bin/** - .ci.yaml + - name: Linux web_skwasm_tests_0 + bringup: true + recipe: flutter/flutter_drone + timeout: 60 + properties: + dependencies: >- + [ + {"dependency": "android_sdk", "version": "version:34v3"}, + {"dependency": "chrome_and_driver", "version": "version:119.0.6045.9"}, + {"dependency": "goldctl", "version": "git_revision:720a542f6fe4f92922c3b8f0fdcc4d2ac6bb83cd"} + ] + shard: web_skwasm_tests + subshard: "0" + tags: > + ["framework", "hostonly", "shard", "linux"] + # Retry for flakes caused by https://github.com/flutter/flutter/issues/132654 + presubmit_max_attempts: "2" + runIf: + - dev/** + - packages/** + - bin/** + - .ci.yaml + + - name: Linux web_skwasm_tests_1 + bringup: true + recipe: flutter/flutter_drone + timeout: 60 + properties: + dependencies: >- + [ + {"dependency": "android_sdk", "version": "version:34v3"}, + {"dependency": "chrome_and_driver", "version": "version:119.0.6045.9"}, + {"dependency": "goldctl", "version": "git_revision:720a542f6fe4f92922c3b8f0fdcc4d2ac6bb83cd"} + ] + shard: web_skwasm_tests + subshard: "1" + tags: > + ["framework", "hostonly", "shard", "linux"] + # Retry for flakes caused by https://github.com/flutter/flutter/issues/132654 + presubmit_max_attempts: "2" + runIf: + - dev/** + - packages/** + - bin/** + - .ci.yaml + + - name: Linux web_skwasm_tests_2 + bringup: true + recipe: flutter/flutter_drone + timeout: 60 + properties: + dependencies: >- + [ + {"dependency": "android_sdk", "version": "version:34v3"}, + {"dependency": "chrome_and_driver", "version": "version:119.0.6045.9"}, + {"dependency": "goldctl", "version": "git_revision:720a542f6fe4f92922c3b8f0fdcc4d2ac6bb83cd"} + ] + shard: web_skwasm_tests + subshard: "2" + tags: > + ["framework", "hostonly", "shard", "linux"] + # Retry for flakes caused by https://github.com/flutter/flutter/issues/132654 + presubmit_max_attempts: "2" + runIf: + - dev/** + - packages/** + - bin/** + - .ci.yaml + + - name: Linux web_skwasm_tests_3 + bringup: true + recipe: flutter/flutter_drone + timeout: 60 + properties: + dependencies: >- + [ + {"dependency": "android_sdk", "version": "version:34v3"}, + {"dependency": "chrome_and_driver", "version": "version:119.0.6045.9"}, + {"dependency": "goldctl", "version": "git_revision:720a542f6fe4f92922c3b8f0fdcc4d2ac6bb83cd"} + ] + shard: web_skwasm_tests + subshard: "3" + tags: > + ["framework", "hostonly", "shard", "linux"] + # Retry for flakes caused by https://github.com/flutter/flutter/issues/132654 + presubmit_max_attempts: "2" + runIf: + - dev/** + - packages/** + - bin/** + - .ci.yaml + + - name: Linux web_skwasm_tests_4 + bringup: true + recipe: flutter/flutter_drone + timeout: 60 + properties: + dependencies: >- + [ + {"dependency": "android_sdk", "version": "version:34v3"}, + {"dependency": "chrome_and_driver", "version": "version:119.0.6045.9"}, + {"dependency": "goldctl", "version": "git_revision:720a542f6fe4f92922c3b8f0fdcc4d2ac6bb83cd"} + ] + shard: web_skwasm_tests + subshard: "4" + tags: > + ["framework", "hostonly", "shard", "linux"] + # Retry for flakes caused by https://github.com/flutter/flutter/issues/132654 + presubmit_max_attempts: "2" + runIf: + - dev/** + - packages/** + - bin/** + - .ci.yaml + + - name: Linux web_skwasm_tests_5 + bringup: true + recipe: flutter/flutter_drone + timeout: 60 + properties: + dependencies: >- + [ + {"dependency": "android_sdk", "version": "version:34v3"}, + {"dependency": "chrome_and_driver", "version": "version:119.0.6045.9"}, + {"dependency": "goldctl", "version": "git_revision:720a542f6fe4f92922c3b8f0fdcc4d2ac6bb83cd"} + ] + shard: web_skwasm_tests + subshard: "5" + tags: > + ["framework", "hostonly", "shard", "linux"] + # Retry for flakes caused by https://github.com/flutter/flutter/issues/132654 + presubmit_max_attempts: "2" + runIf: + - dev/** + - packages/** + - bin/** + - .ci.yaml + + - name: Linux web_skwasm_tests_6 + bringup: true + recipe: flutter/flutter_drone + timeout: 60 + properties: + dependencies: >- + [ + {"dependency": "android_sdk", "version": "version:34v3"}, + {"dependency": "chrome_and_driver", "version": "version:119.0.6045.9"}, + {"dependency": "goldctl", "version": "git_revision:720a542f6fe4f92922c3b8f0fdcc4d2ac6bb83cd"} + ] + shard: web_skwasm_tests + subshard: "6" + tags: > + ["framework", "hostonly", "shard", "linux"] + # Retry for flakes caused by https://github.com/flutter/flutter/issues/132654 + presubmit_max_attempts: "2" + runIf: + - dev/** + - packages/** + - bin/** + - .ci.yaml + + - name: Linux web_skwasm_tests_7_last + bringup: true + recipe: flutter/flutter_drone + timeout: 60 + properties: + dependencies: >- + [ + {"dependency": "android_sdk", "version": "version:34v3"}, + {"dependency": "chrome_and_driver", "version": "version:119.0.6045.9"}, + {"dependency": "goldctl", "version": "git_revision:720a542f6fe4f92922c3b8f0fdcc4d2ac6bb83cd"} + ] + shard: web_skwasm_tests + subshard: "7_last" + tags: > + ["framework", "hostonly", "shard", "linux"] + # Retry for flakes caused by https://github.com/flutter/flutter/issues/132654 + presubmit_max_attempts: "2" + runIf: + - dev/** + - packages/** + - bin/** + - .ci.yaml + - name: Linux web_tool_tests recipe: flutter/flutter_drone timeout: 60 diff --git a/TESTOWNERS b/TESTOWNERS index fa05098720..09f667c7ca 100644 --- a/TESTOWNERS +++ b/TESTOWNERS @@ -286,7 +286,7 @@ /dev/devicelab/bin/tasks/technical_debt__cost.dart @HansMuller @flutter/framework /dev/devicelab/bin/tasks/web_benchmarks_canvaskit.dart @yjbanov @flutter/web /dev/devicelab/bin/tasks/web_benchmarks_html.dart @yjbanov @flutter/web -/dev/devicelab/bin/tasks/web_benchmarks_skwasm.dart @jacksongardner @flutter/web +/dev/devicelab/bin/tasks/web_benchmarks_skwasm.dart @eyebrowsoffire @flutter/web /dev/devicelab/bin/tasks/windows_home_scroll_perf__timeline_summary.dart @jonahwilliams @flutter/engine /dev/devicelab/bin/tasks/windows_startup_test.dart @loic-sharma @flutter/desktop @@ -325,7 +325,7 @@ # flutter_plugins @stuartmorgan @flutter/plugin # framework_tests @HansMuller @flutter/framework # fuchsia_precache @christopherfujino @flutter/tool -# realm_checker @jacksongardner @flutter/tool +# realm_checker @eyebrowsoffire @flutter/tool # skp_generator @Hixie # test_ownership @keyonghan # tool_host_cross_arch_tests @andrewkolos @flutter/tool @@ -336,4 +336,5 @@ # web_integration_tests @yjbanov @flutter/web # web_long_running_tests @yjbanov @flutter/web # web_tests @yjbanov @flutter/web +# web_skwasm_tests @eyebrowsoffire @flutter/web # web_tool_tests @eliasyishak @flutter/tool diff --git a/dev/bots/test.dart b/dev/bots/test.dart index d43fdbf352..0285460689 100644 --- a/dev/bots/test.dart +++ b/dev/bots/test.dart @@ -242,6 +242,8 @@ Future main(List args) async { 'web_tests': _runWebHtmlUnitTests, // All the unit/widget tests run using `flutter test --platform=chrome --web-renderer=canvaskit` 'web_canvaskit_tests': _runWebCanvasKitUnitTests, + // All the unit/widget tests run using `flutter test --platform=chrome --wasm --web-renderer=skwasm` + 'web_skwasm_tests': _runWebSkwasmUnitTests, // All web integration tests 'web_long_running_tests': _runWebLongRunningTests, 'flutter_plugins': _runFlutterPackagesTests, @@ -1117,14 +1119,18 @@ Future _runFrameworkCoverage() async { } Future _runWebHtmlUnitTests() { - return _runWebUnitTests('html'); + return _runWebUnitTests('html', false); } Future _runWebCanvasKitUnitTests() { - return _runWebUnitTests('canvaskit'); + return _runWebUnitTests('canvaskit', false); } -Future _runWebUnitTests(String webRenderer) async { +Future _runWebSkwasmUnitTests() { + return _runWebUnitTests('skwasm', true); +} + +Future _runWebUnitTests(String webRenderer, bool useWasm) async { final Map subshards = {}; final Directory flutterPackageDirectory = Directory(path.join(flutterRoot, 'packages', 'flutter')); @@ -1160,6 +1166,7 @@ Future _runWebUnitTests(String webRenderer) async { index * testsPerShard, (index + 1) * testsPerShard, ), + useWasm, ); } @@ -1175,16 +1182,19 @@ Future _runWebUnitTests(String webRenderer) async { (webShardCount - 1) * testsPerShard, allTests.length, ), + useWasm, ); await _runFlutterWebTest( webRenderer, path.join(flutterRoot, 'packages', 'flutter_web_plugins'), ['test'], + useWasm, ); await _runFlutterWebTest( webRenderer, path.join(flutterRoot, 'packages', 'flutter_driver'), [path.join('test', 'src', 'web_tests', 'web_extension_test.dart')], + useWasm, ); }; @@ -1333,11 +1343,19 @@ Future _runWebLongRunningTests() async { 'html', path.join(flutterRoot, 'packages', 'integration_test'), ['test/web_extension_test.dart'], + false, ), () => _runFlutterWebTest( 'canvaskit', path.join(flutterRoot, 'packages', 'integration_test'), ['test/web_extension_test.dart'], + false, + ), + () => _runFlutterWebTest( + 'skwasm', + path.join(flutterRoot, 'packages', 'integration_test'), + ['test/web_extension_test.dart'], + true, ), ]; @@ -2302,13 +2320,19 @@ Future _runWebDebugTest(String target, { } } -Future _runFlutterWebTest(String webRenderer, String workingDirectory, List tests) async { +Future _runFlutterWebTest( + String webRenderer, + String workingDirectory, + List tests, + bool useWasm, +) async { await runCommand( flutter, [ 'test', '-v', '--platform=chrome', + if (useWasm) '--wasm', '--web-renderer=$webRenderer', '--dart-define=DART_HHH_BOT=$_runningInDartHHHBot', ...flutterTestArgs, diff --git a/dev/integration_tests/web/lib/framework_stack_trace.dart b/dev/integration_tests/web/lib/framework_stack_trace.dart index bf49c6bc22..d68eb46416 100644 --- a/dev/integration_tests/web/lib/framework_stack_trace.dart +++ b/dev/integration_tests/web/lib/framework_stack_trace.dart @@ -2,11 +2,12 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:html' as html; +import 'dart:js_interop'; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:meta/dart2js.dart'; +import 'package:web/web.dart' as web; // Tests that the framework prints stack traces in all build modes. // @@ -35,10 +36,12 @@ Future main() async { } print(output); - html.HttpRequest.request( - '/test-result', - method: 'POST', - sendData: '$output', + web.window.fetch( + '/test-result'.toJS, + web.RequestInit( + method: 'POST', + body: '$output'.toJS, + ) ); } diff --git a/dev/integration_tests/web/lib/service_worker_test.dart b/dev/integration_tests/web/lib/service_worker_test.dart index c449025a96..cf4241457e 100644 --- a/dev/integration_tests/web/lib/service_worker_test.dart +++ b/dev/integration_tests/web/lib/service_worker_test.dart @@ -2,10 +2,13 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:html' as html; +import 'dart:js_interop'; + +import 'package:web/web.dart' as web; + Future main() async { - await html.window.navigator.serviceWorker?.ready; - const String response = 'CLOSE?version=1'; - await html.HttpRequest.getString(response); - html.document.body?.appendHtml(response); + await web.window.navigator.serviceWorker.ready.toDart; + final JSString response = 'CLOSE?version=1'.toJS; + await web.window.fetch(response).toDart; + web.document.body?.append(response); } diff --git a/dev/integration_tests/web/lib/service_worker_test_blocked_service_workers.dart b/dev/integration_tests/web/lib/service_worker_test_blocked_service_workers.dart index f7c7e91295..755eac620e 100644 --- a/dev/integration_tests/web/lib/service_worker_test_blocked_service_workers.dart +++ b/dev/integration_tests/web/lib/service_worker_test_blocked_service_workers.dart @@ -2,9 +2,12 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:html' as html; +import 'dart:js_interop'; + +import 'package:web/web.dart' as web; + Future main() async { - const String response = 'CLOSE?version=1'; - await html.HttpRequest.getString(response); - html.document.body?.appendHtml(response); + final JSString response = 'CLOSE?version=1'.toJS; + await web.window.fetch(response).toDart; + web.document.body?.append(response); } diff --git a/dev/integration_tests/web/lib/sound_mode.dart b/dev/integration_tests/web/lib/sound_mode.dart index 34b57f4e6b..ffd50f8558 100644 --- a/dev/integration_tests/web/lib/sound_mode.dart +++ b/dev/integration_tests/web/lib/sound_mode.dart @@ -4,7 +4,9 @@ // @dart = 2.12 -import 'dart:html' as html; +import 'dart:js_interop'; + +import 'package:web/web.dart' as web; // Verify that web applications can be run in sound mode. void main() { @@ -16,9 +18,11 @@ void main() { output = '--- TEST SUCCEEDED ---'; } print(output); - html.HttpRequest.request( - '/test-result', - method: 'POST', - sendData: output, + web.window.fetch( + '/test-result'.toJS, + web.RequestInit( + method: 'POST', + body: output.toJS, + ) ); } diff --git a/dev/integration_tests/web/lib/stack_trace.dart b/dev/integration_tests/web/lib/stack_trace.dart index 4290d5ba9d..a7eec7d4ac 100644 --- a/dev/integration_tests/web/lib/stack_trace.dart +++ b/dev/integration_tests/web/lib/stack_trace.dart @@ -2,11 +2,12 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:html' as html; +import 'dart:js_interop'; import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:meta/dart2js.dart'; +import 'package:web/web.dart' as web; /// Expected sequence of method calls. const List callChain = ['baz', 'bar', 'foo']; @@ -32,7 +33,7 @@ const List expectedDebugStackFrames = [ packageScheme: 'package', package: 'packages', packagePath: 'web_integration/stack_trace.dart', - line: 119, + line: 122, column: 3, className: '', method: 'baz', @@ -43,7 +44,7 @@ const List expectedDebugStackFrames = [ packageScheme: 'package', package: 'packages', packagePath: 'web_integration/stack_trace.dart', - line: 114, + line: 117, column: 3, className: '', method: 'bar', @@ -54,7 +55,7 @@ const List expectedDebugStackFrames = [ packageScheme: 'package', package: 'packages', packagePath: 'web_integration/stack_trace.dart', - line: 109, + line: 112, column: 3, className: '', method: 'foo', @@ -97,10 +98,12 @@ void main() { output.writeln('--- TEST FAILED ---'); } print(output); - html.HttpRequest.request( - '/test-result', - method: 'POST', - sendData: '$output', + web.window.fetch( + '/test-result'.toJS, + web.RequestInit( + method: 'POST', + body: '$output'.toJS, + ) ); } diff --git a/dev/integration_tests/web/lib/web_define_loading.dart b/dev/integration_tests/web/lib/web_define_loading.dart index 742afbb560..89c332f47f 100644 --- a/dev/integration_tests/web/lib/web_define_loading.dart +++ b/dev/integration_tests/web/lib/web_define_loading.dart @@ -2,7 +2,9 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:html' as html; +import 'dart:js_interop'; + +import 'package:web/web.dart' as web; Future main() async { final StringBuffer output = StringBuffer(); @@ -16,9 +18,11 @@ Future main() async { print('--- TEST FAILED ---'); } - html.HttpRequest.request( - '/test-result', - method: 'POST', - sendData: '$output', + web.window.fetch( + '/test-result'.toJS, + web.RequestInit( + method: 'POST', + body: '$output'.toJS, + ) ); } diff --git a/dev/integration_tests/web/lib/web_directory_loading.dart b/dev/integration_tests/web/lib/web_directory_loading.dart index 2b533009ba..1294bc5ebb 100644 --- a/dev/integration_tests/web/lib/web_directory_loading.dart +++ b/dev/integration_tests/web/lib/web_directory_loading.dart @@ -2,16 +2,20 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:html' as html; +import 'dart:js_interop'; + +import 'package:web/web.dart' as web; // Attempt to load a file that is hosted in the applications's `web/` directory. Future main() async { try { - final html.HttpRequest request = await html.HttpRequest.request( - '/example', - method: 'GET', - ); - final String? body = request.responseText; + final web.Response response = await web.window.fetch( + '/example'.toJS, + web.RequestInit( + method: 'GET', + ), + ).toDart; + final String body = (await response.text().toDart).toDart; if (body == 'This is an Example') { print('--- TEST SUCCEEDED ---'); } else { diff --git a/dev/integration_tests/web/lib/web_resources_cdn_test.dart b/dev/integration_tests/web/lib/web_resources_cdn_test.dart index cfa4537a4a..23c5de73e6 100644 --- a/dev/integration_tests/web/lib/web_resources_cdn_test.dart +++ b/dev/integration_tests/web/lib/web_resources_cdn_test.dart @@ -2,7 +2,9 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:html' as html; +import 'dart:js_interop'; + +import 'package:web/web.dart' as web; // Attempt to load CanvasKit resources hosted on gstatic. Future main() async { @@ -12,12 +14,13 @@ Future main() async { return; } try { - final html.HttpRequest request = await html.HttpRequest.request( - 'https://www.gstatic.com/flutter-canvaskit/$engineVersion/canvaskit.js', - method: 'GET', - ); - final dynamic response = request.response; - if (response != null) { + final web.Response response = await web.window.fetch( + 'https://www.gstatic.com/flutter-canvaskit/$engineVersion/canvaskit.js'.toJS, + web.RequestInit( + method: 'GET', + ), + ).toDart; + if (response.ok) { print('--- TEST SUCCEEDED ---'); } else { print('--- TEST FAILED ---'); @@ -27,12 +30,13 @@ Future main() async { print('--- TEST FAILED ---'); } try { - final html.HttpRequest request = await html.HttpRequest.request( - 'https://www.gstatic.com/flutter-canvaskit/$engineVersion/canvaskit.wasm', - method: 'GET', - ); - final dynamic response = request.response; - if (response != null) { + final web.Response response = await web.window.fetch( + 'https://www.gstatic.com/flutter-canvaskit/$engineVersion/canvaskit.wasm'.toJS, + web.RequestInit( + method: 'GET', + ) + ).toDart; + if (response.ok) { print('--- TEST SUCCEEDED ---'); } else { print('--- TEST FAILED ---'); diff --git a/dev/integration_tests/web/pubspec.yaml b/dev/integration_tests/web/pubspec.yaml index 606926df8e..f862e7888b 100644 --- a/dev/integration_tests/web/pubspec.yaml +++ b/dev/integration_tests/web/pubspec.yaml @@ -14,10 +14,12 @@ dependencies: flutter: sdk: flutter + web: 0.5.1 + characters: 1.3.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" collection: 1.18.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" material_color_utilities: 0.8.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" meta: 1.12.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" vector_math: 2.1.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" -# PUBSPEC CHECKSUM: 6f95 +# PUBSPEC CHECKSUM: 8d22 diff --git a/dev/integration_tests/web/test/test.dart b/dev/integration_tests/web/test/test.dart index e184f93fdc..709c3e77fc 100644 --- a/dev/integration_tests/web/test/test.dart +++ b/dev/integration_tests/web/test/test.dart @@ -5,7 +5,7 @@ import 'package:web_integration/a.dart' if (dart.library.io) 'package:web_integration/b.dart' as message1; import 'package:web_integration/c.dart' - if (dart.library.html) 'package:web_integration/d.dart' as message2; + if (dart.library.js_interop) 'package:web_integration/d.dart' as message2; void main() { if (message1.message == 'a' && message2.message == 'd') { diff --git a/dev/integration_tests/web_e2e_tests/lib/common.dart b/dev/integration_tests/web_e2e_tests/lib/common.dart index 7e0d3f7106..044edab078 100644 --- a/dev/integration_tests/web_e2e_tests/lib/common.dart +++ b/dev/integration_tests/web_e2e_tests/lib/common.dart @@ -2,12 +2,11 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:html'; - import 'package:flutter_test/flutter_test.dart'; +import 'package:web/web.dart' as web; /// Whether the current browser is Firefox. -bool get isFirefox => window.navigator.userAgent.toLowerCase().contains('firefox'); +bool get isFirefox => web.window.navigator.userAgent.toLowerCase().contains('firefox'); /// Finds elements in the DOM tree rendered by the Flutter Web engine. /// @@ -15,8 +14,8 @@ bool get isFirefox => window.navigator.userAgent.toLowerCase().contains('firefox /// `` element. Otherwise, looks under `` /// without penetrating the shadow DOM. In the latter case, if the application /// creates platform views, this will also find platform view elements. -List findElements(String selector) { - final Element? flutterView = document.querySelector('flutter-view'); +web.NodeList findElements(String selector) { + final web.Element? flutterView = web.document.querySelector('flutter-view'); if (flutterView == null) { fail( diff --git a/dev/integration_tests/web_e2e_tests/lib/scroll_wheel_main.dart b/dev/integration_tests/web_e2e_tests/lib/scroll_wheel_main.dart index 6c71be9643..2b6b357042 100644 --- a/dev/integration_tests/web_e2e_tests/lib/scroll_wheel_main.dart +++ b/dev/integration_tests/web_e2e_tests/lib/scroll_wheel_main.dart @@ -2,10 +2,10 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:html' as html; - import 'package:flutter/material.dart'; +import 'package:web/web.dart' as web; + void main() => runApp(const MyApp()); class MyApp extends StatelessWidget { @@ -79,29 +79,25 @@ abstract class DeltaMode { void dispatchMouseWheelEvent(int mouseX, int mouseY, int deltaMode, double deltaX, double deltaY) { - final html.EventTarget target = html.document.elementFromPoint(mouseX, mouseY)!; + final web.EventTarget target = web.document.elementFromPoint(mouseX, mouseY)!; - target.dispatchEvent(html.MouseEvent('mouseover', + target.dispatchEvent(web.MouseEvent('mouseover', web.MouseEventInit( screenX: mouseX, screenY: mouseY, clientX: mouseX, clientY: mouseY, - )); + ))); - target.dispatchEvent(html.MouseEvent('mousemove', + target.dispatchEvent(web.MouseEvent('mousemove', web.MouseEventInit( screenX: mouseX, screenY: mouseY, clientX: mouseX, clientY: mouseY, - )); + ))); - target.dispatchEvent(html.WheelEvent('wheel', - screenX: mouseX, - screenY: mouseY, - clientX: mouseX, - clientY: mouseY, + target.dispatchEvent(web.WheelEvent('wheel', web.WheelEventInit( deltaMode: deltaMode, deltaX : deltaX, deltaY : deltaY, - )); + ))); } diff --git a/dev/integration_tests/web_e2e_tests/pubspec.yaml b/dev/integration_tests/web_e2e_tests/pubspec.yaml index b8a568f24a..c17307053e 100644 --- a/dev/integration_tests/web_e2e_tests/pubspec.yaml +++ b/dev/integration_tests/web_e2e_tests/pubspec.yaml @@ -21,6 +21,7 @@ dependencies: integration_test: sdk: flutter flutter_gallery_assets: 1.0.2 + web: 0.5.1 async: 2.11.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" boolean_selector: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" @@ -82,7 +83,6 @@ dev_dependencies: test_core: 0.6.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" typed_data: 1.3.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" watcher: 1.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - web: 0.5.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" web_socket_channel: 2.4.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" webkit_inspection_protocol: 1.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" yaml: 3.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" diff --git a/dev/integration_tests/web_e2e_tests/test_driver/platform_messages_integration.dart b/dev/integration_tests/web_e2e_tests/test_driver/platform_messages_integration.dart index ff160db09c..de0afb59fd 100644 --- a/dev/integration_tests/web_e2e_tests/test_driver/platform_messages_integration.dart +++ b/dev/integration_tests/web_e2e_tests/test_driver/platform_messages_integration.dart @@ -2,13 +2,13 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:html' as html; import 'dart:ui_web' as ui_web; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; +import 'package:web/web.dart' as web; import 'package:web_e2e_tests/platform_messages_main.dart' as app; void main() { @@ -27,7 +27,7 @@ void main() { await tester.tap(find.byKey(const Key('input'))); // Focus in input, otherwise clipboard will fail with // 'document is not focused' platform exception. - html.document.querySelector('input')?.focus(); + (web.document.querySelector('input') as web.HTMLElement?)?.focus(); await Clipboard.setData(const ClipboardData(text: 'sample text')); }, skip: true); // https://github.com/flutter/flutter/issues/54296 @@ -38,7 +38,7 @@ void main() { platformViewsRegistry.getNextPlatformViewId(); ui_web.platformViewRegistry.registerViewFactory('MyView', (int viewId) { viewInstanceCount += 1; - return html.DivElement(); + return web.HTMLDivElement(); }); app.main(); diff --git a/dev/integration_tests/web_e2e_tests/test_driver/text_editing_integration.dart b/dev/integration_tests/web_e2e_tests/test_driver/text_editing_integration.dart index 906a02684a..7e064a6358 100644 --- a/dev/integration_tests/web_e2e_tests/test_driver/text_editing_integration.dart +++ b/dev/integration_tests/web_e2e_tests/test_driver/text_editing_integration.dart @@ -2,13 +2,13 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:html'; import 'dart:js_util' as js_util; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; +import 'package:web/web.dart' as web; import 'package:web_e2e_tests/common.dart'; import 'package:web_e2e_tests/text_editing_main.dart' as app; @@ -26,9 +26,9 @@ void main() { await tester.tap(find.byKey(const Key('input'))); // A native input element will be appended to the DOM. - final List nodeList = findElements('input'); + final web.NodeList nodeList = findElements('input'); expect(nodeList.length, equals(1)); - final InputElement input = nodeList[0] as InputElement; + final web.HTMLInputElement input = nodeList.item(0)! as web.HTMLInputElement; // The element's value will be the same as the textFormField's value. expect(input.value, 'Text1'); @@ -50,9 +50,9 @@ void main() { await tester.tap(find.byKey(const Key('empty-input'))); // A native input element will be appended to the DOM. - final List nodeList = findElements('input'); + final web.NodeList nodeList = findElements('input'); expect(nodeList.length, equals(1)); - final InputElement input = nodeList[0] as InputElement; + final web.HTMLInputElement input = nodeList.item(0)! as web.HTMLInputElement; // The element's value will be empty. expect(input.value, ''); @@ -81,7 +81,7 @@ void main() { await tester.tap(find.byKey(const Key('input2'))); // Press Tab. This should trigger `onFieldSubmitted` of TextField. - final InputElement input = findElements('input')[0] as InputElement; + final web.HTMLInputElement input = findElements('input').item(0)! as web.HTMLInputElement; dispatchKeyboardEvent(input, 'keydown', { 'keyCode': 13, // Enter. 'cancelable': true, @@ -111,9 +111,9 @@ void main() { await tester.tap(find.byKey(const Key('input'))); // A native input element will be appended to the DOM. - final List nodeList = findElements('input'); + final web.NodeList nodeList = findElements('input'); expect(nodeList.length, equals(1)); - final InputElement input = nodeList[0] as InputElement; + final web.HTMLInputElement input = nodeList.item(0)! as web.HTMLInputElement; // Press Tab. The focus should move to the next TextFormField. dispatchKeyboardEvent(input, 'keydown', { @@ -136,7 +136,7 @@ void main() { // A native input element for the next TextField should be attached to the // DOM. - final InputElement input2 = findElements('input')[0] as InputElement; + final web.HTMLInputElement input2 = findElements('input').item(0)! as web.HTMLInputElement; expect(input2.value, 'Text2'); }, semanticsEnabled: false); @@ -150,9 +150,9 @@ void main() { await tester.tap(find.byKey(const Key('input'))); // A native input element will be appended to the DOM. - final List nodeList = findElements('input'); + final web.NodeList nodeList = findElements('input'); expect(nodeList.length, equals(1)); - final InputElement input = nodeList[0] as InputElement; + final web.HTMLInputElement input = nodeList.item(0)! as web.HTMLInputElement; // Press and release CapsLock. dispatchKeyboardEvent(input, 'keydown', { @@ -191,7 +191,7 @@ void main() { // A native input element for the next TextField should be attached to the // DOM. - final InputElement input2 = findElements('input')[0] as InputElement; + final web.HTMLInputElement input2 = findElements('input').item(0)! as web.HTMLInputElement; expect(input2.value, 'Text2'); }, semanticsEnabled: false); @@ -215,16 +215,16 @@ void main() { await gesture.up(); // A native input element will be appended to the DOM. - final List nodeList = findElements('textarea'); + final web.NodeList nodeList = findElements('textarea'); expect(nodeList.length, equals(1)); - final TextAreaElement input = nodeList[0] as TextAreaElement; + final web.HTMLTextAreaElement input = nodeList.item(0)! as web.HTMLTextAreaElement; // The element's value should contain the selectable text. expect(input.value, text); expect(input.hasAttribute('readonly'), isTrue); // Make sure the entire text is selected. TextRange? range = - TextRange(start: input.selectionStart!, end: input.selectionEnd!); + TextRange(start: input.selectionStart, end: input.selectionEnd); expect(range.textInside(text), text); // Double tap to select the first word. @@ -236,7 +236,7 @@ void main() { await gesture.up(); await gesture.down(firstWordOffset); await gesture.up(); - range = TextRange(start: input.selectionStart!, end: input.selectionEnd!); + range = TextRange(start: input.selectionStart, end: input.selectionEnd); expect(range.textInside(text), 'Lorem'); // Double tap to select the last word. @@ -248,21 +248,21 @@ void main() { await gesture.up(); await gesture.down(lastWordOffset); await gesture.up(); - range = TextRange(start: input.selectionStart!, end: input.selectionEnd!); + range = TextRange(start: input.selectionStart, end: input.selectionEnd); expect(range.textInside(text), 'amet'); }, semanticsEnabled: false); } -KeyboardEvent dispatchKeyboardEvent( - EventTarget target, String type, Map args) { - final Object jsKeyboardEvent = js_util.getProperty(window, 'KeyboardEvent') as Object; +web.KeyboardEvent dispatchKeyboardEvent( + web.EventTarget target, String type, Map args) { + final Object jsKeyboardEvent = js_util.getProperty(web.window, 'KeyboardEvent') as Object; final List eventArgs = [ type, args, ]; - final KeyboardEvent event = js_util.callConstructor( + final web.KeyboardEvent event = js_util.callConstructor( jsKeyboardEvent, js_util.jsify(eventArgs) as List) - as KeyboardEvent; + as web.KeyboardEvent; target.dispatchEvent(event); return event; diff --git a/dev/integration_tests/web_e2e_tests/test_driver/url_strategy_integration.dart b/dev/integration_tests/web_e2e_tests/test_driver/url_strategy_integration.dart index bba6a6de1e..cd0d53542c 100644 --- a/dev/integration_tests/web_e2e_tests/test_driver/url_strategy_integration.dart +++ b/dev/integration_tests/web_e2e_tests/test_driver/url_strategy_integration.dart @@ -3,13 +3,16 @@ // found in the LICENSE file. import 'dart:async'; -import 'dart:html' as html; +import 'dart:js_interop'; import 'dart:ui' as ui; +import 'dart:ui_web'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_web_plugins/flutter_web_plugins.dart'; import 'package:integration_test/integration_test.dart'; +import 'package:web/helpers.dart'; +import 'package:web/web.dart' as web; import 'package:web_e2e_tests/url_strategy_main.dart' as app; void main() { @@ -56,7 +59,7 @@ class TestUrlStrategy extends UrlStrategy { String getPath() => currentEntry.url; @override - dynamic getState() => currentEntry.state; + Object? getState() => currentEntry.state; int _currentEntryIndex; final List history; @@ -111,10 +114,10 @@ class TestUrlStrategy extends UrlStrategy { }); } - final List listeners = []; + final List listeners = []; @override - ui.VoidCallback addPopStateListener(html.EventListener fn) { + ui.VoidCallback addPopStateListener(PopStateListener fn) { listeners.add(fn); return () { // Schedule a micro task here to avoid removing the listener during @@ -134,9 +137,9 @@ class TestUrlStrategy extends UrlStrategy { /// like a real browser. void _firePopStateEvent() { assert(withinAppHistory); - final html.PopStateEvent event = html.PopStateEvent( + final web.PopStateEvent event = web.PopStateEvent( 'popstate', - {'state': currentEntry.state}, + PopStateEventInit(state: currentEntry.state?.toJSBox), ); for (int i = 0; i < listeners.length; i++) { listeners[i](event); @@ -160,7 +163,7 @@ class TestUrlStrategy extends UrlStrategy { class TestHistoryEntry { const TestHistoryEntry(this.state, this.title, this.url); - final dynamic state; + final Object? state; final String? title; final String url; diff --git a/examples/api/test/flutter_test_config.dart b/examples/api/test/flutter_test_config.dart index 69ad13655b..e1ec331181 100644 --- a/examples/api/test/flutter_test_config.dart +++ b/examples/api/test/flutter_test_config.dart @@ -5,7 +5,7 @@ import 'dart:async'; -import 'goldens_io.dart' if (dart.library.html) 'goldens_web.dart' as flutter_goldens; +import 'goldens_io.dart' if (dart.library.js_interop) 'goldens_web.dart' as flutter_goldens; Future testExecutable(FutureOr Function() testMain) { // Enable golden file testing using Skia Gold. diff --git a/packages/flutter/lib/src/foundation/_bitfield_web.dart b/packages/flutter/lib/src/foundation/_bitfield_web.dart index e1f74cc3b5..91ad7744a4 100644 --- a/packages/flutter/lib/src/foundation/_bitfield_web.dart +++ b/packages/flutter/lib/src/foundation/_bitfield_web.dart @@ -4,7 +4,7 @@ import 'bitfield.dart' as bitfield; -/// The dart:html implementation of [bitfield.kMaxUnsignedSMI]. +/// The web implementation of [bitfield.kMaxUnsignedSMI]. /// /// This value is used as an optimization to coerce some numbers to be within /// the SMI range and avoid heap allocations. Because number encoding is @@ -13,14 +13,14 @@ import 'bitfield.dart' as bitfield; /// does not have to guarantee efficiency. const int kMaxUnsignedSMI = -1; -/// The dart:html implementation of [bitfield.Bitfield]. +/// The web implementation of [bitfield.Bitfield]. class BitField implements bitfield.BitField { - /// The dart:html implementation of [bitfield.Bitfield]. + /// The web implementation of [bitfield.Bitfield]. // Can remove when we have metaclasses. // ignore: avoid_unused_constructor_parameters BitField(int length); - /// The dart:html implementation of [bitfield.Bitfield.filled]. + /// The web implementation of [bitfield.Bitfield.filled]. // Can remove when we have metaclasses. // ignore: avoid_unused_constructor_parameters BitField.filled(int length, bool value); diff --git a/packages/flutter/lib/src/foundation/_isolates_web.dart b/packages/flutter/lib/src/foundation/_isolates_web.dart index 4350e54614..7410a5271c 100644 --- a/packages/flutter/lib/src/foundation/_isolates_web.dart +++ b/packages/flutter/lib/src/foundation/_isolates_web.dart @@ -6,7 +6,7 @@ import 'isolates.dart' as isolates; export 'isolates.dart' show ComputeCallback; -/// The dart:html implementation of [isolate.compute]. +/// The web implementation of [isolate.compute]. @pragma('dart2js:tryInline') Future compute(isolates.ComputeCallback callback, M message, { String? debugLabel }) async { // To avoid blocking the UI immediately for an expensive function call, we diff --git a/packages/flutter/test/flutter_test_config.dart b/packages/flutter/test/flutter_test_config.dart index 0009b5b1d0..8feff83b1a 100644 --- a/packages/flutter/test/flutter_test_config.dart +++ b/packages/flutter/test/flutter_test_config.dart @@ -10,7 +10,7 @@ import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; -import '_goldens_io.dart' if (dart.library.html) '_goldens_web.dart' +import '_goldens_io.dart' if (dart.library.js_interop) '_goldens_web.dart' as flutter_goldens; /// If true, leak tracking is enabled for all `testWidgets`. diff --git a/packages/flutter_driver/lib/src/extension/_extension_io.dart b/packages/flutter_driver/lib/src/extension/_extension_io.dart index 87aba4deb4..9ad5bff06f 100644 --- a/packages/flutter_driver/lib/src/extension/_extension_io.dart +++ b/packages/flutter_driver/lib/src/extension/_extension_io.dart @@ -6,7 +6,7 @@ /// /// See also: /// -/// * [_extension_web.dart], which has the dart:html implementation +/// * [_extension_web.dart], which has the web implementation void registerWebServiceExtension(Future> Function(Map) call) { throw UnsupportedError('Use registerServiceExtension instead'); } diff --git a/packages/flutter_driver/lib/src/extension/_extension_web.dart b/packages/flutter_driver/lib/src/extension/_extension_web.dart index ad33bb3db1..fc44a0a01a 100644 --- a/packages/flutter_driver/lib/src/extension/_extension_web.dart +++ b/packages/flutter_driver/lib/src/extension/_extension_web.dart @@ -3,11 +3,13 @@ // found in the LICENSE file. import 'dart:convert'; -import 'dart:html' as html; -import 'dart:js'; -import 'dart:js_util' as js_util; +import 'dart:js_interop'; +import 'dart:js_interop_unsafe'; -/// The dart:html implementation of [registerWebServiceExtension]. +@JS('window') +external JSObject get _window; + +/// The web implementation of [registerWebServiceExtension]. /// /// Registers Web Service Extension for Flutter Web application. /// @@ -21,13 +23,13 @@ void registerWebServiceExtension(Future> Function(Map params = Map.from( - jsonDecode(message as String) as Map); - final Map result = Map.from( - await call(params)); - context[r'$flutterDriverResult'] = json.encode(result); - })); + jsonDecode((message as JSString).toDart) as Map); + call(params).then((Map result) { + _window.setProperty(r'$flutterDriverResult'.toJS, json.encode(result).toJS); + }); + }.toJS); } diff --git a/packages/flutter_driver/lib/src/extension/extension.dart b/packages/flutter_driver/lib/src/extension/extension.dart index 6e29825d32..c21b3b88d5 100644 --- a/packages/flutter_driver/lib/src/extension/extension.dart +++ b/packages/flutter_driver/lib/src/extension/extension.dart @@ -20,7 +20,7 @@ import '../common/error.dart'; import '../common/find.dart'; import '../common/handler_factory.dart'; import '../common/message.dart'; -import '_extension_io.dart' if (dart.library.html) '_extension_web.dart'; +import '_extension_io.dart' if (dart.library.js_interop) '_extension_web.dart'; const String _extensionMethodName = 'driver'; diff --git a/packages/flutter_driver/test/src/web_tests/web_extension_test.dart b/packages/flutter_driver/test/src/web_tests/web_extension_test.dart index 3771c906f3..3bfe87072e 100644 --- a/packages/flutter_driver/test/src/web_tests/web_extension_test.dart +++ b/packages/flutter_driver/test/src/web_tests/web_extension_test.dart @@ -2,11 +2,15 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:js' as js; +import 'dart:js_interop'; +import 'dart:js_interop_unsafe'; import 'package:flutter_driver/src/extension/_extension_web.dart'; import 'package:flutter_test/flutter_test.dart'; +@JS('window') +external JSObject get _window; + void main() { group('test web_extension', () { late Future> Function(Map) call; @@ -21,11 +25,11 @@ void main() { expect(() => registerWebServiceExtension(call), returnsNormally); - expect(js.context.hasProperty(r'$flutterDriver'), true); - expect(js.context[r'$flutterDriver'], isNotNull); + expect(_window.hasProperty(r'$flutterDriver'.toJS).toDart, true); + expect(_window.getProperty(r'$flutterDriver'.toJS), isNotNull); - expect(js.context.hasProperty(r'$flutterDriverResult'), true); - expect(js.context[r'$flutterDriverResult'], isNull); + expect(_window.hasProperty(r'$flutterDriverResult'.toJS).toDart, true); + expect(_window.getProperty(r'$flutterDriverResult'.toJS), isNull); }); }); } diff --git a/packages/flutter_test/lib/flutter_test.dart b/packages/flutter_test/lib/flutter_test.dart index 9d76239f03..9f90b528d4 100644 --- a/packages/flutter_test/lib/flutter_test.dart +++ b/packages/flutter_test/lib/flutter_test.dart @@ -55,8 +55,9 @@ library flutter_test; export 'dart:async' show Future; -export 'src/_goldens_io.dart' if (dart.library.html) 'src/_goldens_web.dart'; -export 'src/_matchers_io.dart' if (dart.library.html) 'src/_matchers_web.dart'; +export 'src/_goldens_io.dart' if (dart.library.js_interop) 'src/_goldens_web.dart'; +export 'src/_matchers_io.dart' if (dart.library.js_interop) 'src/_matchers_web.dart'; +export 'src/_test_selector_io.dart' if (dart.library.js_interop) 'src/_test_selector_web.dart'; export 'src/accessibility.dart'; export 'src/animation_sheet.dart'; export 'src/binding.dart'; diff --git a/packages/flutter_test/lib/src/_goldens_io.dart b/packages/flutter_test/lib/src/_goldens_io.dart index e68fc06a93..1743de8d90 100644 --- a/packages/flutter_test/lib/src/_goldens_io.dart +++ b/packages/flutter_test/lib/src/_goldens_io.dart @@ -292,6 +292,11 @@ ByteData _invert(ByteData imageBytes) { /// An unsupported [WebGoldenComparator] that exists for API compatibility. class DefaultWebGoldenComparator extends WebGoldenComparator { + /// This is provided to prevent warnings from the analyzer. + DefaultWebGoldenComparator(Uri _) { + throw UnsupportedError('DefaultWebGoldenComparator is only supported on the web.'); + } + @override Future compare(double width, double height, Uri golden) { throw UnsupportedError('DefaultWebGoldenComparator is only supported on the web.'); diff --git a/packages/flutter_test/lib/src/_goldens_web.dart b/packages/flutter_test/lib/src/_goldens_web.dart index b29f9ccdca..dba4b5f344 100644 --- a/packages/flutter_test/lib/src/_goldens_web.dart +++ b/packages/flutter_test/lib/src/_goldens_web.dart @@ -3,12 +3,13 @@ // found in the LICENSE file. import 'dart:convert'; -import 'dart:html' as html; +import 'dart:js_interop'; import 'dart:typed_data'; import 'package:matcher/expect.dart' show fail; import 'goldens.dart'; +import 'web.dart' as web; /// An unsupported [GoldenFileComparator] that exists for API compatibility. class LocalFileComparator extends GoldenFileComparator { @@ -58,21 +59,23 @@ class DefaultWebGoldenComparator extends WebGoldenComparator { @override Future compare(double width, double height, Uri golden) async { final String key = golden.toString(); - final html.HttpRequest request = await html.HttpRequest.request( - 'flutter_goldens', - method: 'POST', - sendData: json.encode({ - 'testUri': testUri.toString(), - 'key': key, - 'width': width.round(), - 'height': height.round(), - }), - ); - final String response = request.response as String; - if (response == 'true') { + final web.Response response = await web.window.fetch( + 'flutter_goldens'.toJS, + web.RequestInit( + method: 'POST', + body: json.encode({ + 'testUri': testUri.toString(), + 'key': key, + 'width': width.round(), + 'height': height.round(), + }).toJS, + ) + ).toDart; + final String responseText = (await response.text().toDart).toDart; + if (responseText == 'true') { return true; } - fail(response); + fail(responseText); } @override @@ -85,20 +88,22 @@ class DefaultWebGoldenComparator extends WebGoldenComparator { Future compareBytes(Uint8List bytes, Uri golden) async { final String key = golden.toString(); final String bytesEncoded = base64.encode(bytes); - final html.HttpRequest request = await html.HttpRequest.request( - 'flutter_goldens', - method: 'POST', - sendData: json.encode({ - 'testUri': testUri.toString(), - 'key': key, - 'bytes': bytesEncoded, - }), - ); - final String response = request.response as String; - if (response == 'true') { + final web.Response response = await web.window.fetch( + 'flutter_goldens'.toJS, + web.RequestInit( + method: 'POST', + body: json.encode({ + 'testUri': testUri.toString(), + 'key': key, + 'bytes': bytesEncoded, + }).toJS, + ) + ).toDart; + final String responseText = (await response.text().toDart).toDart; + if (responseText == 'true') { return true; } - fail(response); + fail(responseText); } @override diff --git a/packages/flutter_test/lib/src/_test_selector_io.dart b/packages/flutter_test/lib/src/_test_selector_io.dart new file mode 100644 index 0000000000..1fd189c1f5 --- /dev/null +++ b/packages/flutter_test/lib/src/_test_selector_io.dart @@ -0,0 +1,6 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// This is an empty file that is imported instead of web_test_selector.dart on +// non-web platforms. diff --git a/packages/flutter_test/lib/src/_test_selector_web.dart b/packages/flutter_test/lib/src/_test_selector_web.dart new file mode 100644 index 0000000000..85862e1519 --- /dev/null +++ b/packages/flutter_test/lib/src/_test_selector_web.dart @@ -0,0 +1,85 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:js_interop'; +import 'dart:ui' as ui; +import 'dart:ui_web' as ui_web; + +import 'package:stream_channel/stream_channel.dart'; +import 'package:test_api/backend.dart'; + +import '_goldens_web.dart'; +import 'goldens.dart'; +import 'web.dart' as web; + +// This file contains APIs that are used by the generated test harness for +// running flutter unit tests. + +/// A `main` entry point for a test. +typedef EntryPoint = FutureOr Function(); + +/// An entry point runner provided by a test config file +typedef EntryPointRunner = Future Function(EntryPoint); + +/// Metadata about a web test to run +typedef WebTest = ({ + EntryPoint entryPoint, + EntryPointRunner? entryPointRunner, + Uri goldensUri, +}); + +/// Gets the test selector set by the test bootstrapping logic +String get testSelector { + final JSString? jsTestSelector = web.window.testSelector; + if (jsTestSelector == null) { + throw Exception('Test selector not set'); + } + return jsTestSelector.toDart; +} + +/// Runs a specific web test +Future runWebTest(WebTest test) async { + ui_web.debugEmulateFlutterTesterEnvironment = true; + final Completer completer = Completer(); + await ui_web.bootstrapEngine(runApp: () => completer.complete()); + await completer.future; + webGoldenComparator = DefaultWebGoldenComparator(test.goldensUri); + + /// This hard-codes the device pixel ratio to 3.0 and a 2400 x 1800 window + /// size for the purposes of testing. + ui_web.debugOverrideDevicePixelRatio(3.0); + ui.window.debugPhysicalSizeOverride = const ui.Size(2400, 1800); + + final EntryPointRunner? entryPointRunner = test.entryPointRunner; + final EntryPoint entryPoint = test.entryPoint; + _internalBootstrapBrowserTest(() { + return entryPointRunner != null ? () => entryPointRunner(entryPoint) : entryPoint; + }); +} + +void _internalBootstrapBrowserTest(EntryPoint Function() getMain) { + final StreamChannel channel = _serializeSuite(getMain, hidePrints: false); + _postMessageChannel().pipe(channel); +} + +StreamChannel _serializeSuite(EntryPoint Function() getMain, {bool hidePrints = true}) => RemoteListener.start(getMain, hidePrints: hidePrints); + +StreamChannel _postMessageChannel() { + final StreamChannelController controller = StreamChannelController(sync: true); + final web.MessageChannel channel = web.MessageChannel(); + web.window.parent!.postMessage('port'.toJS, web.window.location.origin, [channel.port2].toJS); + + final JSFunction eventCallback = (web.Event event) { + controller.local.sink.add(event.data.dartify()); + }.toJS; + channel.port1.addEventListener('message'.toJS, eventCallback); + channel.port1.start(); + controller.local.stream.listen( + (Object? message) => channel.port1.postMessage(message.jsify()), + onDone: () => channel.port1.removeEventListener('message'.toJS, eventCallback), + ); + + return controller.foreign; +} diff --git a/packages/flutter_test/lib/src/binding.dart b/packages/flutter_test/lib/src/binding.dart index 999ff5863a..95a80d2f3e 100644 --- a/packages/flutter_test/lib/src/binding.dart +++ b/packages/flutter_test/lib/src/binding.dart @@ -18,7 +18,7 @@ import 'package:stack_trace/stack_trace.dart' as stack_trace; import 'package:test_api/scaffolding.dart' as test_package show Timeout; import 'package:vector_math/vector_math_64.dart'; -import '_binding_io.dart' if (dart.library.html) '_binding_web.dart' as binding; +import '_binding_io.dart' if (dart.library.js_interop) '_binding_web.dart' as binding; import 'goldens.dart'; import 'platform.dart'; import 'restoration.dart'; diff --git a/packages/flutter_test/lib/src/goldens.dart b/packages/flutter_test/lib/src/goldens.dart index 0f83ac832e..cb73450c21 100644 --- a/packages/flutter_test/lib/src/goldens.dart +++ b/packages/flutter_test/lib/src/goldens.dart @@ -8,7 +8,7 @@ import 'dart:ui'; import 'package:meta/meta.dart'; import 'package:path/path.dart' as path; -import '_goldens_io.dart' if (dart.library.html) '_goldens_web.dart' as goldens; +import '_goldens_io.dart' if (dart.library.js_interop) '_goldens_web.dart' as goldens; /// Compares image pixels against a golden image file. /// diff --git a/packages/flutter_test/lib/src/matchers.dart b/packages/flutter_test/lib/src/matchers.dart index f5db0a6b3f..500d658915 100644 --- a/packages/flutter_test/lib/src/matchers.dart +++ b/packages/flutter_test/lib/src/matchers.dart @@ -14,7 +14,7 @@ import 'package:matcher/expect.dart'; import 'package:matcher/src/expect/async_matcher.dart'; // ignore: implementation_imports import 'package:vector_math/vector_math_64.dart' show Matrix3; -import '_matchers_io.dart' if (dart.library.html) '_matchers_web.dart' show MatchesGoldenFile, captureImage; +import '_matchers_io.dart' if (dart.library.js_interop) '_matchers_web.dart' show MatchesGoldenFile, captureImage; import 'accessibility.dart'; import 'binding.dart'; import 'controller.dart'; diff --git a/packages/flutter_test/lib/src/web.dart b/packages/flutter_test/lib/src/web.dart new file mode 100644 index 0000000000..37f97db380 --- /dev/null +++ b/packages/flutter_test/lib/src/web.dart @@ -0,0 +1,74 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// This code is copied from `package:web` which still needs its own +// documentation for public members. Since this is a shim that users should not +// use, we ignore this lint for this file. +// ignore_for_file: public_member_api_docs + +/// A stripped down version of `package:web` to avoid pinning that repo in +/// Flutter as a dependency. +/// +/// These are manually copied over from `package:web` as needed, and should stay +/// in sync with the latest package version as much as possible. +/// +/// If missing members are needed, copy them over into the corresponding +/// extension or interface. If missing interfaces/types are needed, copy them +/// over while excluding unnecessary inheritance to make the copy minimal. These +/// types are erased at runtime, so excluding supertypes is safe. If a member is +/// needed that belongs to a supertype, then add the necessary `implements` +/// clause to the subtype when you add that supertype. Keep extensions next to +/// the interface they extend. +library; + +import 'dart:js_interop'; + +@JS() +external Window get window; + +extension type Window._(JSObject _) implements JSObject { + external JSPromise fetch( + JSAny input, [ + RequestInit init, + ]); + external Location get location; + external Window? get parent; + external void postMessage(JSAny message, JSString targetOrigin, JSArray transfers); + + external JSString? get testSelector; +} + +extension type Response._(JSObject _) implements JSObject { + external JSPromise text(); +} + +extension type RequestInit._(JSObject _) implements JSObject { + external factory RequestInit({ + String method, + JSAny? body, + }); +} + +extension type Location._(JSObject _) implements JSObject { + external JSString get origin; +} + +extension type MessageChannel._(JSObject _) implements JSObject { + external factory MessageChannel(); + + external MessagePort port1; + external MessagePort port2; +} + +extension type MessagePort._(JSObject _) implements JSObject { + external void addEventListener(JSString eventName, JSFunction callback); + external void removeEventListener(JSString eventName, JSFunction callback); + external void postMessage(JSAny? message); + + external void start(); +} + +extension type Event._(JSObject _) implements JSObject { + external JSObject? get data; +} diff --git a/packages/flutter_test/pubspec.yaml b/packages/flutter_test/pubspec.yaml index 9c78fbdeb9..050488582b 100644 --- a/packages/flutter_test/pubspec.yaml +++ b/packages/flutter_test/pubspec.yaml @@ -1,7 +1,7 @@ name: flutter_test environment: - sdk: '>=3.2.0-0 <4.0.0' + sdk: '>=3.3.0-0 <4.0.0' dependencies: # To update these, use "flutter update-packages --force-upgrade". diff --git a/packages/flutter_tools/lib/src/commands/test.dart b/packages/flutter_tools/lib/src/commands/test.dart index 3a813e0049..e37350a791 100644 --- a/packages/flutter_tools/lib/src/commands/test.dart +++ b/packages/flutter_tools/lib/src/commands/test.dart @@ -12,6 +12,7 @@ import '../build_info.dart'; import '../bundle_builder.dart'; import '../devfs.dart'; import '../device.dart'; +import '../features.dart'; import '../globals.dart' as globals; import '../native_assets.dart'; import '../project.dart'; @@ -23,6 +24,7 @@ import '../test/test_time_recorder.dart'; import '../test/test_wrapper.dart'; import '../test/watcher.dart'; import '../web/compile.dart'; +import '../web/web_constants.dart'; /// The name of the directory where Integration Tests are placed. /// @@ -233,7 +235,14 @@ class TestCommand extends FlutterCommand with DeviceBasedDevelopmentArtifacts { 'in seconds (e.g. "60s"), ' 'as a multiplier of the default timeout (e.g. "2x"), ' 'or as the string "none" to disable the timeout entirely.', + ) + ..addFlag( + FlutterOptions.kWebWasmFlag, + help: 'Compile to WebAssembly rather than JavaScript.\n$kWasmMoreInfo', + negatable: false, + hide: !featureFlags.isFlutterWebWasmEnabled, ); + addDdsOptions(verboseHelp: verboseHelp); addServeObservatoryOptions(verboseHelp: verboseHelp); usesFatalWarningsOption(verboseHelp: verboseHelp); @@ -256,6 +265,7 @@ class TestCommand extends FlutterCommand with DeviceBasedDevelopmentArtifacts { final Set _testFileUris = {}; bool get isWeb => stringArg('platform') == 'chrome'; + bool get useWasm => boolArg(FlutterOptions.kWebWasmFlag); @override Future> get requiredArtifacts async { @@ -499,6 +509,10 @@ class TestCommand extends FlutterCommand with DeviceBasedDevelopmentArtifacts { watcher = collector; } + if (!isWeb && useWasm) { + throwToolExit('--wasm is only supported on the web platform'); + } + Device? integrationTestDevice; if (_isIntegrationTest) { integrationTestDevice = await findTargetDevice(); @@ -577,6 +591,7 @@ class TestCommand extends FlutterCommand with DeviceBasedDevelopmentArtifacts { testAssetDirectory: testAssetDirectory, flutterProject: flutterProject, web: isWeb, + useWasm: useWasm, randomSeed: stringArg('test-randomize-ordering-seed'), reporter: stringArg('reporter'), fileReporter: stringArg('file-reporter'), diff --git a/packages/flutter_tools/lib/src/test/flutter_web_platform.dart b/packages/flutter_tools/lib/src/test/flutter_web_platform.dart index b5a3508ef7..c77231db80 100644 --- a/packages/flutter_tools/lib/src/test/flutter_web_platform.dart +++ b/packages/flutter_tools/lib/src/test/flutter_web_platform.dart @@ -37,7 +37,7 @@ import 'flutter_web_goldens.dart'; import 'test_compiler.dart'; import 'test_time_recorder.dart'; -shelf.Handler createDirectoryHandler(Directory directory) { +shelf.Handler createDirectoryHandler(Directory directory, { required bool crossOriginIsolated} ) { final mime.MimeTypeResolver resolver = mime.MimeTypeResolver(); final FileSystem fileSystem = directory.fileSystem; return (shelf.Request request) async { @@ -56,10 +56,16 @@ shelf.Handler createDirectoryHandler(Directory directory) { return shelf.Response.notFound('Not Found'); } final String? contentType = resolver.lookup(file.path); + final bool needsCrossOriginIsolated = crossOriginIsolated && uriPath.endsWith('.html'); return shelf.Response.ok( file.openRead(), headers: { - if (contentType != null) 'Content-Type': contentType + if (contentType != null) 'Content-Type': contentType, + if (needsCrossOriginIsolated) + ...{ + 'Cross-Origin-Opener-Policy': 'same-origin', + 'Cross-Origin-Embedder-Policy': 'require-corp', + }, }, ); }; @@ -74,6 +80,7 @@ class FlutterWebPlatform extends PlatformPlugin { required this.buildInfo, required this.webMemoryFS, required FileSystem fileSystem, + required Directory buildDirectory, required File testDartJs, required File testHostDartJs, required ChromiumLauncher chromiumLauncher, @@ -81,8 +88,10 @@ class FlutterWebPlatform extends PlatformPlugin { required Artifacts? artifacts, required ProcessManager processManager, required this.webRenderer, + required this.useWasm, TestTimeRecorder? testTimeRecorder, }) : _fileSystem = fileSystem, + _buildDirectory = buildDirectory, _testDartJs = testDartJs, _testHostDartJs = testHostDartJs, _chromiumLauncher = chromiumLauncher, @@ -92,6 +101,7 @@ class FlutterWebPlatform extends PlatformPlugin { .add(_webSocketHandler.handler) .add(createDirectoryHandler( fileSystem.directory(fileSystem.path.join(Cache.flutterRoot!, 'packages', 'flutter_tools')), + crossOriginIsolated: webRenderer == WebRendererMode.skwasm, )) .add(_handleStaticArtifact) .add(_localCanvasKitHandler) @@ -99,7 +109,8 @@ class FlutterWebPlatform extends PlatformPlugin { .add(_wrapperHandler) .add(_handleTestRequest) .add(createDirectoryHandler( - fileSystem.directory(fileSystem.path.join(fileSystem.currentDirectory.path, 'test')) + fileSystem.directory(fileSystem.path.join(fileSystem.currentDirectory.path, 'test')), + crossOriginIsolated: webRenderer == WebRendererMode.skwasm, )) .add(_packageFilesHandler); _server.mount(cascade.handler); @@ -116,6 +127,7 @@ class FlutterWebPlatform extends PlatformPlugin { final WebMemoryFS webMemoryFS; final BuildInfo buildInfo; final FileSystem _fileSystem; + final Directory _buildDirectory; final File _testDartJs; final File _testHostDartJs; final ChromiumLauncher _chromiumLauncher; @@ -127,6 +139,7 @@ class FlutterWebPlatform extends PlatformPlugin { final AsyncMemoizer _closeMemo = AsyncMemoizer(); final String _root; final WebRendererMode webRenderer; + final bool useWasm; /// Allows only one test suite (typically one test file) to be loaded and run /// at any given point in time. Loading more than one file at a time is known @@ -149,11 +162,13 @@ class FlutterWebPlatform extends PlatformPlugin { required BuildInfo buildInfo, required WebMemoryFS webMemoryFS, required FileSystem fileSystem, + required Directory buildDirectory, required Logger logger, required ChromiumLauncher chromiumLauncher, required Artifacts? artifacts, required ProcessManager processManager, required WebRendererMode webRenderer, + required bool useWasm, TestTimeRecorder? testTimeRecorder, Uri? testPackageUri, Future Function() serverFactory = defaultServerFactory, @@ -196,12 +211,14 @@ class FlutterWebPlatform extends PlatformPlugin { testDartJs: testDartJs, testHostDartJs: testHostDartJs, fileSystem: fileSystem, + buildDirectory: buildDirectory, chromiumLauncher: chromiumLauncher, artifacts: artifacts, logger: logger, nullAssertions: nullAssertions, processManager: processManager, webRenderer: webRenderer, + useWasm: useWasm, testTimeRecorder: testTimeRecorder, ); } @@ -254,6 +271,11 @@ class FlutterWebPlatform extends PlatformPlugin { 'dart_stack_trace_mapper.js', )); + File get _flutterJs => _fileSystem.file(_fileSystem.path.join( + _artifacts!.getHostArtifact(HostArtifact.flutterJsDirectory).path, + 'flutter.js', + )); + File get _dartSdk { final Map> dartSdkArtifactMap = buildInfo.ddcModuleFormat == DdcModuleFormat.ddc ? kDdcDartSdkJsArtifactMap : kAmdDartSdkJsArtifactMap; return _fileSystem.file(_artifacts!.getHostArtifact(dartSdkArtifactMap[webRenderer]![_nullSafetyMode]!)); @@ -277,21 +299,20 @@ class FlutterWebPlatform extends PlatformPlugin { } Future _handleTestRequest(shelf.Request request) async { - if (request.url.path.endsWith('.dart.browser_test.dart.js')) { - final String leadingPath = request.url.path.split('.browser_test.dart.js')[0]; - final String generatedFile = '${_fileSystem.path.split(leadingPath).join('_')}.bootstrap.js'; - return shelf.Response.ok(generateTestBootstrapFileContents('/$generatedFile', 'require.js', 'dart_stack_trace_mapper.js'), headers: { - HttpHeaders.contentTypeHeader: 'text/javascript', - }); + if (request.url.path.endsWith('main.dart.browser_test.dart.js')) { + return shelf.Response.ok(generateTestBootstrapFileContents( + '/main.dart.bootstrap.js', 'require.js', 'dart_stack_trace_mapper.js'), + headers: { + HttpHeaders.contentTypeHeader: 'text/javascript', + } + ); } - if (request.url.path.endsWith('.dart.bootstrap.js')) { - final String leadingPath = request.url.path.split('.dart.bootstrap.js')[0]; - final String generatedFile = '${_fileSystem.path.split(leadingPath).join('_')}.dart.test.dart.js'; + if (request.url.path.endsWith('main.dart.bootstrap.js')) { return shelf.Response.ok(generateMainModule( nullAssertions: nullAssertions!, nativeNullAssertions: true, - bootstrapModule: '${_fileSystem.path.basename(leadingPath)}.dart.bootstrap', - entrypoint: '/$generatedFile' + bootstrapModule: 'main.dart.bootstrap', + entrypoint: '/main.dart.js' ), headers: { HttpHeaders.contentTypeHeader: 'text/javascript', }); @@ -349,6 +370,21 @@ class FlutterWebPlatform extends PlatformPlugin { _testHostDartJs.openRead(), headers: {'Content-Type': 'text/javascript'}, ); + } else if (request.requestedUri.path.contains('flutter.js')) { + return shelf.Response.ok( + _flutterJs.openRead(), + headers: {'Content-Type': 'text/javascript'}, + ); + } else if (request.requestedUri.path.contains('main.dart.mjs')) { + return shelf.Response.ok( + _buildDirectory.childFile('main.dart.mjs').openRead(), + headers: {'Content-Type': 'text/javascript'}, + ); + } else if (request.requestedUri.path.contains('main.dart.wasm')) { + return shelf.Response.ok( + _buildDirectory.childFile('main.dart.wasm').openRead(), + headers: {'Content-Type': 'application/wasm'}, + ); } else { return shelf.Response.notFound('Not Found'); } @@ -363,7 +399,10 @@ class FlutterWebPlatform extends PlatformPlugin { if (fileUri != null) { final String dirname = _fileSystem.path.dirname(fileUri.toFilePath()); final String basename = _fileSystem.path.basename(fileUri.toFilePath()); - final shelf.Handler handler = createDirectoryHandler(_fileSystem.directory(dirname)); + final shelf.Handler handler = createDirectoryHandler( + _fileSystem.directory(dirname), + crossOriginIsolated: webRenderer == WebRendererMode.skwasm, + ); final shelf.Request modifiedRequest = shelf.Request( request.method, request.requestedUri.replace(path: basename), @@ -452,36 +491,66 @@ class FlutterWebPlatform extends PlatformPlugin { return shelf.Response.internalServerError(body: error); } + final File canvasKitFile = _canvasKitFile(relativePath); return shelf.Response.ok( - _canvasKitFile(relativePath).openRead(), + canvasKitFile.openRead(), headers: { HttpHeaders.contentTypeHeader: contentType, }, ); } + String _makeBuildConfigString() { + return useWasm ? ''' + { + compileTarget: "dart2wasm", + renderer: "${webRenderer.name}", + mainWasmPath: "main.dart.wasm", + jsSupportRuntimePath: "main.dart.mjs", + } +''' : ''' + { + compileTarget: "dartdevc", + renderer: "${webRenderer.name}", + mainJsPath: "main.dart.browser_test.dart.js", + } +'''; + } + // A handler that serves wrapper files used to bootstrap tests. shelf.Response _wrapperHandler(shelf.Request request) { final String path = _fileSystem.path.fromUri(request.url); if (path.endsWith('.html')) { final String test = '${_fileSystem.path.withoutExtension(path)}.dart'; - final String scriptBase = htmlEscape.convert(_fileSystem.path.basename(test)); - final String link = ''; return shelf.Response.ok(''' ${htmlEscape.convert(test)} Test + - $link - - ''', headers: {'Content-Type': 'text/html'}); + ''', headers: { + 'Content-Type': 'text/html', + if (webRenderer == WebRendererMode.skwasm) + ...{ + 'Cross-Origin-Opener-Policy': 'same-origin', + 'Cross-Origin-Embedder-Policy': 'require-corp', + } + }); } return shelf.Response.notFound('Not found.'); } diff --git a/packages/flutter_tools/lib/src/test/runner.dart b/packages/flutter_tools/lib/src/test/runner.dart index 6164eb3d8e..9b85adbfac 100644 --- a/packages/flutter_tools/lib/src/test/runner.dart +++ b/packages/flutter_tools/lib/src/test/runner.dart @@ -52,6 +52,7 @@ abstract class FlutterTestRunner { String? icudtlPath, Directory? coverageDirectory, bool web = false, + bool useWasm = false, String? randomSeed, String? reporter, String? fileReporter, @@ -117,6 +118,7 @@ class _FlutterTestRunnerImpl implements FlutterTestRunner { String? icudtlPath, Directory? coverageDirectory, bool web = false, + bool useWasm = false, String? randomSeed, String? reporter, String? fileReporter, @@ -186,6 +188,7 @@ class _FlutterTestRunnerImpl implements FlutterTestRunner { testFiles: testFiles.map((Uri uri) => uri.toFilePath()).toList(), buildInfo: debuggingOptions.buildInfo, webRenderer: debuggingOptions.webRenderer, + useWasm: useWasm, ); testArgs ..add('--platform=chrome') @@ -205,6 +208,7 @@ class _FlutterTestRunnerImpl implements FlutterTestRunner { webMemoryFS: result, logger: globals.logger, fileSystem: globals.fs, + buildDirectory: globals.fs.directory(tempBuildDir), artifacts: globals.artifacts, processManager: globals.processManager, chromiumLauncher: ChromiumLauncher( @@ -217,6 +221,7 @@ class _FlutterTestRunnerImpl implements FlutterTestRunner { ), testTimeRecorder: testTimeRecorder, webRenderer: debuggingOptions.webRenderer, + useWasm: useWasm, ); }, ); diff --git a/packages/flutter_tools/lib/src/test/web_test_compiler.dart b/packages/flutter_tools/lib/src/test/web_test_compiler.dart index 0c82b7eaab..f8ac6d2d9b 100644 --- a/packages/flutter_tools/lib/src/test/web_test_compiler.dart +++ b/packages/flutter_tools/lib/src/test/web_test_compiler.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'package:package_config/package_config.dart'; +import 'package:package_config/package_config_types.dart'; import 'package:process/process.dart'; import '../artifacts.dart'; @@ -11,6 +12,7 @@ import '../base/config.dart'; import '../base/file_system.dart'; import '../base/logger.dart'; import '../base/platform.dart'; +import '../base/process.dart'; import '../build_info.dart'; import '../bundle.dart'; import '../cache.dart'; @@ -44,12 +46,74 @@ class WebTestCompiler { final ProcessManager _processManager; final Config _config; + Future _generateTestEntrypoint({ + required List testFiles, + required Directory projectDirectory, + required Directory outputDirectory, + required LanguageVersion languageVersion, + }) async { + final List testInfos = testFiles.map((String testFilePath) { + final List relativeTestSegments = _fileSystem.path.split( + _fileSystem.path.relative( + testFilePath, + from: projectDirectory.childDirectory('test').path + ) + ); + + final File? testConfigFile = findTestConfigFile(_fileSystem.file(testFilePath), _logger); + String? testConfigPath; + if (testConfigFile != null) { + testConfigPath = _fileSystem.path.split( + _fileSystem.path.relative( + testConfigFile.path, + from: projectDirectory.childDirectory('test').path + ) + ).join('/'); + } + return ( + entryPoint: relativeTestSegments.join('/'), + configFile: testConfigPath, + goldensUri: Uri.file(testFilePath), + ); + }).toList(); + return _fileSystem.file(_fileSystem.path.join(outputDirectory.path, 'main.dart')) + ..createSync(recursive: true) + ..writeAsStringSync(generateTestEntrypoint( + testInfos: testInfos, + languageVersion: languageVersion + ) + ); + } + Future initialize({ required Directory projectDirectory, required String testOutputDir, required List testFiles, required BuildInfo buildInfo, required WebRendererMode webRenderer, + required bool useWasm, + }) async { + return useWasm ? _compileWasm( + projectDirectory: projectDirectory, + testOutputDir: testOutputDir, + testFiles: testFiles, + buildInfo: buildInfo, + webRenderer: webRenderer, + ) : _compileJS( + projectDirectory: projectDirectory, + testOutputDir: testOutputDir, + testFiles: testFiles, + buildInfo: buildInfo, + webRenderer: webRenderer, + ); + } + + Future _compileJS({ + required Directory projectDirectory, + required String testOutputDir, + required List testFiles, + required BuildInfo buildInfo, + required WebRendererMode webRenderer, }) async { LanguageVersion languageVersion = LanguageVersion(2, 8); late final String platformDillName; @@ -78,32 +142,12 @@ class WebTestCompiler { final Directory outputDirectory = _fileSystem.directory(testOutputDir) ..createSync(recursive: true); - final List generatedFiles = []; - for (final String testFilePath in testFiles) { - final List relativeTestSegments = _fileSystem.path.split( - _fileSystem.path.relative(testFilePath, from: projectDirectory.childDirectory('test').path)); - final File generatedFile = _fileSystem.file( - _fileSystem.path.join(outputDirectory.path, '${relativeTestSegments.join('_')}.test.dart')); - generatedFile - ..createSync(recursive: true) - ..writeAsStringSync(generateTestEntrypoint( - relativeTestPath: relativeTestSegments.join('/'), - absolutePath: testFilePath, - testConfigPath: findTestConfigFile(_fileSystem.file(testFilePath), _logger)?.path, - languageVersion: languageVersion, - )); - generatedFiles.add(generatedFile); - } - // Generate a fake main file that imports all tests to be executed. This will force - // each of them to be compiled. - final StringBuffer buffer = StringBuffer('// @dart=${languageVersion.major}.${languageVersion.minor}\n'); - for (final File generatedFile in generatedFiles) { - buffer.writeln('import "${_fileSystem.path.basename(generatedFile.path)}";'); - } - buffer.writeln('void main() {}'); - _fileSystem.file(_fileSystem.path.join(outputDirectory.path, 'main.dart')) - ..createSync() - ..writeAsStringSync(buffer.toString()); + final File testFile = await _generateTestEntrypoint( + testFiles: testFiles, + projectDirectory: projectDirectory, + outputDirectory: outputDirectory, + languageVersion: languageVersion + ); final String cachedKernelPath = getDefaultCachedKernelPath( trackWidgetCreation: buildInfo.trackWidgetCreation, @@ -139,7 +183,7 @@ class WebTestCompiler { ); final CompilerOutput? output = await residentCompiler.recompile( - Uri.parse('org-dartlang-app:///main.dart'), + Uri.parse('org-dartlang-app:///${testFile.basename}'), [], outputPath: outputDirectory.childFile('out').path, packageConfig: buildInfo.packageConfig, @@ -157,7 +201,67 @@ class WebTestCompiler { final File manifestFile = outputDirectory.childFile('${output.outputFilename}.json'); final File sourcemapFile = outputDirectory.childFile('${output.outputFilename}.map'); final File metadataFile = outputDirectory.childFile('${output.outputFilename}.metadata'); + return WebMemoryFS() ..write(codeFile, manifestFile, sourcemapFile, metadataFile); } + + Future _compileWasm({ + required Directory projectDirectory, + required String testOutputDir, + required List testFiles, + required BuildInfo buildInfo, + required WebRendererMode webRenderer, + }) async { + final Directory outputDirectory = _fileSystem.directory(testOutputDir) + ..createSync(recursive: true); + final File testFile = await _generateTestEntrypoint( + testFiles: testFiles, + projectDirectory: projectDirectory, + outputDirectory: outputDirectory, + languageVersion: currentLanguageVersion(_fileSystem, Cache.flutterRoot!), + ); + + final String dartSdkPath = _artifacts.getArtifactPath(Artifact.engineDartSdkPath, platform: TargetPlatform.web_javascript); + final String platformBinariesPath = _artifacts.getHostArtifact(HostArtifact.webPlatformKernelFolder).path; + final String platformFilePath = _fileSystem.path.join(platformBinariesPath, 'dart2wasm_platform.dill'); + final List dartDefines = webRenderer.updateDartDefines(buildInfo.dartDefines); + final File outputWasmFile = outputDirectory.childFile('main.dart.wasm'); + + final List compilationArgs = [ + _artifacts.getArtifactPath(Artifact.engineDartBinary, platform: TargetPlatform.web_javascript), + 'compile', + 'wasm', + '--packages=.dart_tool/package_config.json', + '--extra-compiler-option=--dart-sdk=$dartSdkPath', + '--extra-compiler-option=--platform=$platformFilePath', + '--extra-compiler-option=--multi-root-scheme=org-dartlang-app', + '--extra-compiler-option=--multi-root=${projectDirectory.childDirectory('test').path}', + '--extra-compiler-option=--multi-root=${outputDirectory.path}', + if (webRenderer == WebRendererMode.skwasm) ...[ + '--extra-compiler-option=--import-shared-memory', + '--extra-compiler-option=--shared-memory-max-pages=32768', + ], + ...buildInfo.extraFrontEndOptions, + for (final String dartDefine in dartDefines) + '-D$dartDefine', + + '-O1', + '-o', + outputWasmFile.path, + testFile.path, // dartfile + ]; + + final ProcessUtils processUtils = ProcessUtils( + logger: _logger, + processManager: _processManager, + ); + + await processUtils.run( + throwOnError: true, + compilationArgs, + ); + + return WebMemoryFS(); + } } diff --git a/packages/flutter_tools/lib/src/web/bootstrap.dart b/packages/flutter_tools/lib/src/web/bootstrap.dart index c31c6e4e1d..0de0580a1b 100644 --- a/packages/flutter_tools/lib/src/web/bootstrap.dart +++ b/packages/flutter_tools/lib/src/web/bootstrap.dart @@ -387,59 +387,70 @@ define("$bootstrapModule", ["$entrypoint", "dart_sdk"], function(app, dart_sdk) '''; } -/// Generates the bootstrap logic required for a flutter test running in a browser. +typedef WebTestInfo = ({ + String entryPoint, + Uri goldensUri, + String? configFile, +}); + +/// Generates the bootstrap logic required for running a group of unit test +/// files in the browser. /// -/// This hard-codes the device pixel ratio to 3.0 and a 2400 x 1800 window size. +/// This creates one "switchboard" main function that imports all the main +/// functions of the unit test files that need to be run. The javascript code +/// that starts the test sets a `window.testSelector` that specifies which main +/// function to invoke. This allows us to compile all the unit test files as a +/// single web application and invoke that with a different selector for each +/// test. String generateTestEntrypoint({ - required String relativeTestPath, - required String absolutePath, - required String? testConfigPath, + required List testInfos, required LanguageVersion languageVersion, }) { + final List importMainStatements = []; + final List importTestConfigStatements = []; + final List webTestPairs = []; + + for (int index = 0; index < testInfos.length; index++) { + final WebTestInfo testInfo = testInfos[index]; + final String entryPointPath = testInfo.entryPoint; + importMainStatements.add("import 'org-dartlang-app:///${Uri.file(entryPointPath)}' as test_$index show main;"); + + final String? testConfigPath = testInfo.configFile; + String? testConfigFunction = 'null'; + if (testConfigPath != null) { + importTestConfigStatements.add( + "import 'org-dartlang-app:///${Uri.file(testConfigPath)}' as test_config_$index show testExecutable;" + ); + testConfigFunction = 'test_config_$index.testExecutable'; + } + webTestPairs.add(''' + '$entryPointPath': ( + entryPoint: test_$index.main, + entryPointRunner: $testConfigFunction, + goldensUri: Uri.parse('${testInfo.goldensUri}'), + ), +'''); + } return ''' - // @dart = ${languageVersion.major}.${languageVersion.minor} - import 'org-dartlang-app:///$relativeTestPath' as test; - import 'dart:ui' as ui; - import 'dart:ui_web' as ui_web; - import 'dart:html'; - import 'dart:js'; - ${testConfigPath != null ? "import '${Uri.file(testConfigPath)}' as test_config;" : ""} - import 'package:stream_channel/stream_channel.dart'; - import 'package:flutter_test/flutter_test.dart'; - import 'package:test_api/backend.dart'; +// @dart = ${languageVersion.major}.${languageVersion.minor} - Future main() async { - ui_web.debugEmulateFlutterTesterEnvironment = true; - await ui_web.bootstrapEngine(); - webGoldenComparator = DefaultWebGoldenComparator(Uri.parse('${Uri.file(absolutePath)}')); - ui_web.debugOverrideDevicePixelRatio(3.0); - ui.window.debugPhysicalSizeOverride = const ui.Size(2400, 1800); +${importMainStatements.join('\n')} - internalBootstrapBrowserTest(() { - return ${testConfigPath != null ? "() => test_config.testExecutable(test.main)" : "test.main"}; - }); - } - - void internalBootstrapBrowserTest(Function getMain()) { - var channel = serializeSuite(getMain, hidePrints: false); - postMessageChannel().pipe(channel); - } - - StreamChannel serializeSuite(Function getMain(), {bool hidePrints = true}) => RemoteListener.start(getMain, hidePrints: hidePrints); - - StreamChannel postMessageChannel() { - var controller = StreamChannelController(sync: true); - var channel = MessageChannel(); - window.parent!.postMessage('port', window.location.origin, [channel.port2]); - - var portSubscription = channel.port1.onMessage.listen((message) { - controller.local.sink.add(message.data); - }); - controller.local.stream - .listen(channel.port1.postMessage, onDone: portSubscription.cancel); - - return controller.foreign; +${importTestConfigStatements.join('\n')} + +import 'package:flutter_test/flutter_test.dart'; + +Map webTestMap = { + ${webTestPairs.join('\n')} +}; + +Future main() { + final WebTest? webTest = webTestMap[testSelector]; + if (webTest == null) { + throw Exception('Web test for \${testSelector} not found'); } + return runWebTest(webTest); +} '''; } diff --git a/packages/flutter_tools/templates/plugin/lib/projectName_web.dart.tmpl b/packages/flutter_tools/templates/plugin/lib/projectName_web.dart.tmpl index 5a3d1c0b48..bdbaa077c4 100644 --- a/packages/flutter_tools/templates/plugin/lib/projectName_web.dart.tmpl +++ b/packages/flutter_tools/templates/plugin/lib/projectName_web.dart.tmpl @@ -2,9 +2,9 @@ // of your plugin as a separate package, instead of inlining it in the same // package as the core of your plugin. // ignore: avoid_web_libraries_in_flutter -import 'dart:html' as html show window; import 'package:flutter_web_plugins/flutter_web_plugins.dart'; +import 'package:web/web.dart' as web; import '{{projectName}}_platform_interface.dart'; @@ -20,7 +20,7 @@ class {{pluginDartClass}}Web extends {{pluginDartClass}}Platform { /// Returns a [String] containing the version of the platform. @override Future getPlatformVersion() async { - final version = html.window.navigator.userAgent; + final version = web.window.navigator.userAgent; return version; } } diff --git a/packages/flutter_tools/templates/plugin_shared/pubspec.yaml.tmpl b/packages/flutter_tools/templates/plugin_shared/pubspec.yaml.tmpl index 98ee945644..f7e063e13c 100644 --- a/packages/flutter_tools/templates/plugin_shared/pubspec.yaml.tmpl +++ b/packages/flutter_tools/templates/plugin_shared/pubspec.yaml.tmpl @@ -13,6 +13,7 @@ dependencies: {{#web}} flutter_web_plugins: sdk: flutter + web: ^0.5.1 {{/web}} plugin_platform_interface: ^2.0.2 diff --git a/packages/flutter_tools/test/commands.shard/hermetic/flutter_web_platform_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/flutter_web_platform_test.dart index 9879ed2108..10eccc6ab6 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/flutter_web_platform_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/flutter_web_platform_test.dart @@ -87,11 +87,13 @@ void main() { buildInfo: const BuildInfo(BuildMode.debug, '', treeShakeIcons: false), webMemoryFS: WebMemoryFS(), fileSystem: fileSystem, + buildDirectory: fileSystem.directory('build'), logger: logger, chromiumLauncher: chromiumLauncher, artifacts: artifacts, processManager: processManager, webRenderer: WebRendererMode.canvaskit, + useWasm: false, serverFactory: () async => server, testPackageUri: Uri.parse('test'), ); @@ -134,11 +136,13 @@ void main() { ), webMemoryFS: WebMemoryFS(), fileSystem: fileSystem, + buildDirectory: fileSystem.directory('build'), logger: logger, chromiumLauncher: chromiumLauncher, artifacts: artifacts, processManager: processManager, webRenderer: WebRendererMode.canvaskit, + useWasm: false, serverFactory: () async => server, testPackageUri: Uri.parse('test'), ); diff --git a/packages/flutter_tools/test/commands.shard/hermetic/test_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/test_test.dart index 99aad75046..fa27e70499 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/test_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/test_test.dart @@ -1386,6 +1386,7 @@ class FakeFlutterTestRunner implements FlutterTestRunner { String? icudtlPath, Directory? coverageDirectory, bool web = false, + bool useWasm = false, String? randomSeed, String? reporter, String? fileReporter, diff --git a/packages/flutter_tools/test/general.shard/test/web_test_compiler_test.dart b/packages/flutter_tools/test/general.shard/test/web_test_compiler_test.dart index 578d3c3027..af63836241 100644 --- a/packages/flutter_tools/test/general.shard/test/web_test_compiler_test.dart +++ b/packages/flutter_tools/test/general.shard/test/web_test_compiler_test.dart @@ -91,6 +91,7 @@ void main() { testFiles: ['project/test/fake_test.dart'], buildInfo: buildInfo, webRenderer: WebRendererMode.canvaskit, + useWasm: false, ); expect(processManager.hasRemainingExpectations, isFalse); diff --git a/packages/flutter_tools/test/general.shard/web/bootstrap_test.dart b/packages/flutter_tools/test/general.shard/web/bootstrap_test.dart index 65b05ca603..23b94d4d24 100644 --- a/packages/flutter_tools/test/general.shard/web/bootstrap_test.dart +++ b/packages/flutter_tools/test/general.shard/web/bootstrap_test.dart @@ -138,37 +138,18 @@ void main() { expect(result, contains('el.setAttribute("data-main", \'foo.dart.js\');')); }); - test('generateTestEntrypoint does not generate test config wrappers when testConfigPath is not passed', () { + test('generateTestEntrypoint generates proper imports and mappings for tests', () { final String result = generateTestEntrypoint( - relativeTestPath: 'relative_path.dart', - absolutePath: 'absolute_path.dart', - testConfigPath: null, + testInfos: [ + (entryPoint: 'foo.dart', goldensUri: Uri.parse('foo.dart'), configFile: null), + (entryPoint: 'bar.dart', goldensUri: Uri.parse('bar.dart'), configFile: 'bar_config.dart'), + ], languageVersion: LanguageVersion(2, 8), ); - expect(result, isNot(contains('test_config.testExecutable'))); - }); - - test('generateTestEntrypoint generates test config wrappers when testConfigPath is passed', () { - final String result = generateTestEntrypoint( - relativeTestPath: 'relative_path.dart', - absolutePath: 'absolute_path.dart', - testConfigPath: 'test_config_path.dart', - languageVersion: LanguageVersion(2, 8), - ); - - expect(result, contains('test_config.testExecutable')); - }); - - test('generateTestEntrypoint embeds urls correctly', () { - final String result = generateTestEntrypoint( - relativeTestPath: 'relative_path.dart', - absolutePath: '/test/absolute_path.dart', - testConfigPath: null, - languageVersion: LanguageVersion(2, 8), - ); - - expect(result, contains("Uri.parse('file:///test/absolute_path.dart')")); + expect(result, contains("import 'org-dartlang-app:///foo.dart'")); + expect(result, contains("import 'org-dartlang-app:///bar.dart'")); + expect(result, contains("import 'org-dartlang-app:///bar_config.dart'")); }); group('Using the DDC module system', () { @@ -299,38 +280,5 @@ void main() { expect(result, contains('el.setAttribute("data-main", \'foo.dart.js\');')); }); - - test('generateTestEntrypoint does not generate test config wrappers when testConfigPath is not passed', () { - final String result = generateTestEntrypoint( - relativeTestPath: 'relative_path.dart', - absolutePath: 'absolute_path.dart', - testConfigPath: null, - languageVersion: LanguageVersion(2, 8), - ); - - expect(result, isNot(contains('test_config.testExecutable'))); - }); - - test('generateTestEntrypoint generates test config wrappers when testConfigPath is passed', () { - final String result = generateTestEntrypoint( - relativeTestPath: 'relative_path.dart', - absolutePath: 'absolute_path.dart', - testConfigPath: 'test_config_path.dart', - languageVersion: LanguageVersion(2, 8), - ); - - expect(result, contains('test_config.testExecutable')); - }); - - test('generateTestEntrypoint embeds urls correctly', () { - final String result = generateTestEntrypoint( - relativeTestPath: 'relative_path.dart', - absolutePath: '/test/absolute_path.dart', - testConfigPath: null, - languageVersion: LanguageVersion(2, 8), - ); - - expect(result, contains("Uri.parse('file:///test/absolute_path.dart')")); - }); }); } diff --git a/packages/integration_test/example/integration_test/_example_test_web.dart b/packages/integration_test/example/integration_test/_example_test_web.dart index 4e9cbd4de7..11359f8225 100644 --- a/packages/integration_test/example/integration_test/_example_test_web.dart +++ b/packages/integration_test/example/integration_test/_example_test_web.dart @@ -9,12 +9,11 @@ // gestures. You can also use WidgetTester to find child widgets in the widget // tree, read text, and verify that the values of widget properties are correct. -import 'dart:html' as html; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; - import 'package:integration_test_example/main.dart' as app; +import 'package:web/web.dart' as web; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); @@ -31,7 +30,7 @@ void main() { (Widget widget) => widget is Text && widget.data! - .startsWith('Platform: ${html.window.navigator.platform}\n'), + .startsWith('Platform: ${web.window.navigator.platform}\n'), ), findsOneWidget, ); diff --git a/packages/integration_test/example/integration_test/_extended_test_web.dart b/packages/integration_test/example/integration_test/_extended_test_web.dart index 5868e5c6a8..78ce0fdaad 100644 --- a/packages/integration_test/example/integration_test/_extended_test_web.dart +++ b/packages/integration_test/example/integration_test/_extended_test_web.dart @@ -9,12 +9,12 @@ // gestures. You can also use WidgetTester to find child widgets in the widget // tree, read text, and verify that the values of widget properties are correct. -import 'dart:html' as html; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; import 'package:integration_test_example/main.dart' as app; +import 'package:web/web.dart' as web; void main() { final IntegrationTestWidgetsFlutterBinding binding = @@ -47,7 +47,7 @@ void main() { (Widget widget) => widget is Text && widget.data! - .startsWith('Platform: ${html.window.navigator.platform}\n'), + .startsWith('Platform: ${web.window.navigator.platform}\n'), ), findsOneWidget, ); diff --git a/packages/integration_test/example/integration_test/example_test.dart b/packages/integration_test/example/integration_test/example_test.dart index 0e81b37d9e..3d490b901d 100644 --- a/packages/integration_test/example/integration_test/example_test.dart +++ b/packages/integration_test/example/integration_test/example_test.dart @@ -11,7 +11,7 @@ import 'package:integration_test/integration_test.dart'; -import '_example_test_io.dart' if (dart.library.html) '_example_test_web.dart' as tests; +import '_example_test_io.dart' if (dart.library.js_interop) '_example_test_web.dart' as tests; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); diff --git a/packages/integration_test/example/integration_test/extended_test.dart b/packages/integration_test/example/integration_test/extended_test.dart index 883bb08a44..9fc8825c9e 100644 --- a/packages/integration_test/example/integration_test/extended_test.dart +++ b/packages/integration_test/example/integration_test/extended_test.dart @@ -13,7 +13,7 @@ import 'package:integration_test/integration_test.dart'; -import '_extended_test_io.dart' if (dart.library.html) '_extended_test_web.dart' as tests; +import '_extended_test_io.dart' if (dart.library.js_interop) '_extended_test_web.dart' as tests; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); diff --git a/packages/integration_test/example/lib/main.dart b/packages/integration_test/example/lib/main.dart index a0950a1beb..43821039fc 100644 --- a/packages/integration_test/example/lib/main.dart +++ b/packages/integration_test/example/lib/main.dart @@ -2,6 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'my_app.dart' if (dart.library.html) 'my_web_app.dart'; +import 'my_app.dart' if (dart.library.js_interop) 'my_web_app.dart'; void main() => startApp(); diff --git a/packages/integration_test/example/lib/my_web_app.dart b/packages/integration_test/example/lib/my_web_app.dart index 905a2451ad..5a44d7bd1a 100644 --- a/packages/integration_test/example/lib/my_web_app.dart +++ b/packages/integration_test/example/lib/my_web_app.dart @@ -2,9 +2,10 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:html' as html; import 'package:flutter/material.dart'; +import 'package:web/web.dart' as web; + // ignore_for_file: public_member_api_docs void startApp() => runApp(const MyWebApp()); @@ -26,7 +27,7 @@ class _MyWebAppState extends State { ), body: Center( key: const Key('mainapp'), - child: Text('Platform: ${html.window.navigator.platform}\n'), + child: Text('Platform: ${web.window.navigator.platform}\n'), ), ), ); diff --git a/packages/integration_test/example/pubspec.yaml b/packages/integration_test/example/pubspec.yaml index 3bbd3b2ec2..eae7601a1c 100644 --- a/packages/integration_test/example/pubspec.yaml +++ b/packages/integration_test/example/pubspec.yaml @@ -11,6 +11,7 @@ dependencies: sdk: flutter cupertino_icons: 1.0.6 + web: 0.5.1 characters: 1.3.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" collection: 1.18.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" @@ -78,7 +79,6 @@ dev_dependencies: typed_data: 1.3.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" vm_service: 14.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" watcher: 1.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - web: 0.5.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" web_socket_channel: 2.4.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" webdriver: 3.0.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" webkit_inspection_protocol: 1.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" diff --git a/packages/integration_test/lib/src/_callback_io.dart b/packages/integration_test/lib/src/_callback_io.dart index e2181cee60..acd60f4768 100644 --- a/packages/integration_test/lib/src/_callback_io.dart +++ b/packages/integration_test/lib/src/_callback_io.dart @@ -15,7 +15,7 @@ import 'channel.dart'; /// /// See also: /// -/// * `_callback_web.dart`, which has the dart:html implementation +/// * `_callback_web.dart`, which has the web implementation CallbackManager get callbackManager => _singletonCallbackManager; /// IOCallbackManager singleton. diff --git a/packages/integration_test/lib/src/_callback_web.dart b/packages/integration_test/lib/src/_callback_web.dart index 88d01c6d26..bf7061b9be 100644 --- a/packages/integration_test/lib/src/_callback_web.dart +++ b/packages/integration_test/lib/src/_callback_web.dart @@ -6,7 +6,7 @@ import 'dart:async'; import '../common.dart'; -/// The dart:html implementation of [CallbackManager]. +/// The web implementation of [CallbackManager]. /// /// See also: /// diff --git a/packages/integration_test/lib/src/_extension_io.dart b/packages/integration_test/lib/src/_extension_io.dart index cf0c91cfc9..3bdfae7e27 100644 --- a/packages/integration_test/lib/src/_extension_io.dart +++ b/packages/integration_test/lib/src/_extension_io.dart @@ -6,7 +6,7 @@ /// /// See also: /// -/// * `_extension_web.dart`, which has the dart:html implementation +/// * `_extension_web.dart`, which has the web implementation void registerWebServiceExtension( Future> Function(Map) call) { throw UnsupportedError('Use registerServiceExtension instead'); diff --git a/packages/integration_test/lib/src/_extension_web.dart b/packages/integration_test/lib/src/_extension_web.dart index e422cc5aa1..b94497a570 100644 --- a/packages/integration_test/lib/src/_extension_web.dart +++ b/packages/integration_test/lib/src/_extension_web.dart @@ -4,9 +4,11 @@ import 'dart:async'; import 'dart:convert'; -import 'dart:html' as html; -import 'dart:js'; -import 'dart:js_util' as js_util; +import 'dart:js_interop'; +import 'dart:js_interop_unsafe'; + +@JS('window') +external JSObject get _window; /// The web implementation of [registerWebServiceExtension]. /// @@ -20,21 +22,25 @@ void registerWebServiceExtension(Future> Function(Map messageJson = jsonDecode(message as String) as Map; - final Map params = messageJson.cast(); - final Map result = await callback(params); - context[r'$flutterDriverResult'] = json.encode(result); - } catch (error, stackTrace) { - // Encode the error in the same format the FlutterDriver extension uses. - // See //packages/flutter_driver/lib/src/extension/extension.dart - context[r'$flutterDriverResult'] = json.encode({ - 'isError': true, - 'response': '$error\n$stackTrace', - }); - } - })); + _window.setProperty(r'$flutterDriver'.toJS, (JSAny message) { + (() async { + try { + final Map messageJson = jsonDecode((message as JSString).toDart) as Map; + final Map params = messageJson.cast(); + final Map result = await callback(params); + _window.setProperty(r'$flutterDriverResult'.toJS, json.encode(result).toJS); + } catch (error, stackTrace) { + // Encode the error in the same format the FlutterDriver extension uses. + // See //packages/flutter_driver/lib/src/extension/extension.dart + _window.setProperty(r'$flutterDriverResult'.toJS, + json.encode({ + 'isError': true, + 'response': '$error\n$stackTrace', + }).toJS + ); + } + })(); + }.toJS); } diff --git a/packages/integration_test/lib/src/callback.dart b/packages/integration_test/lib/src/callback.dart index c10e2d27c6..a83efd259f 100644 --- a/packages/integration_test/lib/src/callback.dart +++ b/packages/integration_test/lib/src/callback.dart @@ -2,4 +2,4 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -export '_callback_io.dart' if (dart.library.html) '_callback_web.dart'; +export '_callback_io.dart' if (dart.library.js_interop) '_callback_web.dart'; diff --git a/packages/integration_test/lib/src/extension.dart b/packages/integration_test/lib/src/extension.dart index a2cb861fbf..815005b80a 100644 --- a/packages/integration_test/lib/src/extension.dart +++ b/packages/integration_test/lib/src/extension.dart @@ -2,4 +2,4 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -export '_extension_io.dart' if (dart.library.html) '_extension_web.dart'; +export '_extension_io.dart' if (dart.library.js_interop) '_extension_web.dart'; diff --git a/packages/integration_test/test/web_extension_test.dart b/packages/integration_test/test/web_extension_test.dart index 8097729366..860adf7ddd 100644 --- a/packages/integration_test/test/web_extension_test.dart +++ b/packages/integration_test/test/web_extension_test.dart @@ -5,19 +5,23 @@ @Tags(['web']) library; -import 'dart:js' as js; +import 'dart:js_interop'; +import 'dart:js_interop_unsafe'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; +@JS('window') +external JSObject get _window; + void main() { IntegrationTestWidgetsFlutterBinding(); test('IntegrationTestWidgetsFlutterBinding on the web should register certain global properties', () { - expect(js.context.hasProperty(r'$flutterDriver'), true); - expect(js.context[r'$flutterDriver'], isNotNull); + expect(_window.hasProperty(r'$flutterDriver'.toJS).toDart, true); + expect(_window.getProperty(r'$flutterDriver'.toJS), isNotNull); - expect(js.context.hasProperty(r'$flutterDriverResult'), true); - expect(js.context[r'$flutterDriverResult'], isNull); + expect(_window.hasProperty(r'$flutterDriverResult'.toJS).toDart, true); + expect(_window.getProperty(r'$flutterDriverResult'.toJS), isNull); }); }