Unverified Commit 31209d04 authored by Jackson Gardner's avatar Jackson Gardner Committed by GitHub

`flutter test --wasm` support (#145347)

* Adds support for `flutter test --wasm`.
  * The test compilation flow is a bit different now, so that it supports compilers other than DDC. Specifically, when we run a set of unit tests, we generate a "switchboard" main function that imports each unit test and runs the main function for a specific one based off of a value set by the JS bootstrapping code. This way, there is one compile step and the same compile output is invoked for each unit test file.
* Also, removes all references to `dart:html` from flutter/flutter.
* Adds CI steps for running the framework unit tests with dart2wasm+skwasm
  * These steps are marked as `bringup: true`, so we don't know what kind of failures they will result in. Any failures they have will not block the tree at all yet while we're still in `bringup: true`. Once this PR is merged, I plan on looking at any failures and either fixing them or disabling them so we can get these CI steps running on presubmit.

This fixes https://github.com/flutter/flutter/issues/126692
parent 98d10b62
...@@ -1725,6 +1725,190 @@ targets: ...@@ -1725,6 +1725,190 @@ targets:
- bin/** - bin/**
- .ci.yaml - .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 - name: Linux web_tool_tests
recipe: flutter/flutter_drone recipe: flutter/flutter_drone
timeout: 60 timeout: 60
......
...@@ -286,7 +286,7 @@ ...@@ -286,7 +286,7 @@
/dev/devicelab/bin/tasks/technical_debt__cost.dart @HansMuller @flutter/framework /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_canvaskit.dart @yjbanov @flutter/web
/dev/devicelab/bin/tasks/web_benchmarks_html.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_home_scroll_perf__timeline_summary.dart @jonahwilliams @flutter/engine
/dev/devicelab/bin/tasks/windows_startup_test.dart @loic-sharma @flutter/desktop /dev/devicelab/bin/tasks/windows_startup_test.dart @loic-sharma @flutter/desktop
...@@ -325,7 +325,7 @@ ...@@ -325,7 +325,7 @@
# flutter_plugins @stuartmorgan @flutter/plugin # flutter_plugins @stuartmorgan @flutter/plugin
# framework_tests @HansMuller @flutter/framework # framework_tests @HansMuller @flutter/framework
# fuchsia_precache @christopherfujino @flutter/tool # fuchsia_precache @christopherfujino @flutter/tool
# realm_checker @jacksongardner @flutter/tool # realm_checker @eyebrowsoffire @flutter/tool
# skp_generator @Hixie # skp_generator @Hixie
# test_ownership @keyonghan # test_ownership @keyonghan
# tool_host_cross_arch_tests @andrewkolos @flutter/tool # tool_host_cross_arch_tests @andrewkolos @flutter/tool
...@@ -336,4 +336,5 @@ ...@@ -336,4 +336,5 @@
# web_integration_tests @yjbanov @flutter/web # web_integration_tests @yjbanov @flutter/web
# web_long_running_tests @yjbanov @flutter/web # web_long_running_tests @yjbanov @flutter/web
# web_tests @yjbanov @flutter/web # web_tests @yjbanov @flutter/web
# web_skwasm_tests @eyebrowsoffire @flutter/web
# web_tool_tests @eliasyishak @flutter/tool # web_tool_tests @eliasyishak @flutter/tool
...@@ -242,6 +242,8 @@ Future<void> main(List<String> args) async { ...@@ -242,6 +242,8 @@ Future<void> main(List<String> args) async {
'web_tests': _runWebHtmlUnitTests, 'web_tests': _runWebHtmlUnitTests,
// All the unit/widget tests run using `flutter test --platform=chrome --web-renderer=canvaskit` // All the unit/widget tests run using `flutter test --platform=chrome --web-renderer=canvaskit`
'web_canvaskit_tests': _runWebCanvasKitUnitTests, '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 // All web integration tests
'web_long_running_tests': _runWebLongRunningTests, 'web_long_running_tests': _runWebLongRunningTests,
'flutter_plugins': _runFlutterPackagesTests, 'flutter_plugins': _runFlutterPackagesTests,
...@@ -1117,14 +1119,18 @@ Future<void> _runFrameworkCoverage() async { ...@@ -1117,14 +1119,18 @@ Future<void> _runFrameworkCoverage() async {
} }
Future<void> _runWebHtmlUnitTests() { Future<void> _runWebHtmlUnitTests() {
return _runWebUnitTests('html'); return _runWebUnitTests('html', false);
} }
Future<void> _runWebCanvasKitUnitTests() { Future<void> _runWebCanvasKitUnitTests() {
return _runWebUnitTests('canvaskit'); return _runWebUnitTests('canvaskit', false);
} }
Future<void> _runWebUnitTests(String webRenderer) async { Future<void> _runWebSkwasmUnitTests() {
return _runWebUnitTests('skwasm', true);
}
Future<void> _runWebUnitTests(String webRenderer, bool useWasm) async {
final Map<String, ShardRunner> subshards = <String, ShardRunner>{}; final Map<String, ShardRunner> subshards = <String, ShardRunner>{};
final Directory flutterPackageDirectory = Directory(path.join(flutterRoot, 'packages', 'flutter')); final Directory flutterPackageDirectory = Directory(path.join(flutterRoot, 'packages', 'flutter'));
...@@ -1160,6 +1166,7 @@ Future<void> _runWebUnitTests(String webRenderer) async { ...@@ -1160,6 +1166,7 @@ Future<void> _runWebUnitTests(String webRenderer) async {
index * testsPerShard, index * testsPerShard,
(index + 1) * testsPerShard, (index + 1) * testsPerShard,
), ),
useWasm,
); );
} }
...@@ -1175,16 +1182,19 @@ Future<void> _runWebUnitTests(String webRenderer) async { ...@@ -1175,16 +1182,19 @@ Future<void> _runWebUnitTests(String webRenderer) async {
(webShardCount - 1) * testsPerShard, (webShardCount - 1) * testsPerShard,
allTests.length, allTests.length,
), ),
useWasm,
); );
await _runFlutterWebTest( await _runFlutterWebTest(
webRenderer, webRenderer,
path.join(flutterRoot, 'packages', 'flutter_web_plugins'), path.join(flutterRoot, 'packages', 'flutter_web_plugins'),
<String>['test'], <String>['test'],
useWasm,
); );
await _runFlutterWebTest( await _runFlutterWebTest(
webRenderer, webRenderer,
path.join(flutterRoot, 'packages', 'flutter_driver'), path.join(flutterRoot, 'packages', 'flutter_driver'),
<String>[path.join('test', 'src', 'web_tests', 'web_extension_test.dart')], <String>[path.join('test', 'src', 'web_tests', 'web_extension_test.dart')],
useWasm,
); );
}; };
...@@ -1333,11 +1343,19 @@ Future<void> _runWebLongRunningTests() async { ...@@ -1333,11 +1343,19 @@ Future<void> _runWebLongRunningTests() async {
'html', 'html',
path.join(flutterRoot, 'packages', 'integration_test'), path.join(flutterRoot, 'packages', 'integration_test'),
<String>['test/web_extension_test.dart'], <String>['test/web_extension_test.dart'],
false,
), ),
() => _runFlutterWebTest( () => _runFlutterWebTest(
'canvaskit', 'canvaskit',
path.join(flutterRoot, 'packages', 'integration_test'), path.join(flutterRoot, 'packages', 'integration_test'),
<String>['test/web_extension_test.dart'], <String>['test/web_extension_test.dart'],
false,
),
() => _runFlutterWebTest(
'skwasm',
path.join(flutterRoot, 'packages', 'integration_test'),
<String>['test/web_extension_test.dart'],
true,
), ),
]; ];
...@@ -2302,13 +2320,19 @@ Future<void> _runWebDebugTest(String target, { ...@@ -2302,13 +2320,19 @@ Future<void> _runWebDebugTest(String target, {
} }
} }
Future<void> _runFlutterWebTest(String webRenderer, String workingDirectory, List<String> tests) async { Future<void> _runFlutterWebTest(
String webRenderer,
String workingDirectory,
List<String> tests,
bool useWasm,
) async {
await runCommand( await runCommand(
flutter, flutter,
<String>[ <String>[
'test', 'test',
'-v', '-v',
'--platform=chrome', '--platform=chrome',
if (useWasm) '--wasm',
'--web-renderer=$webRenderer', '--web-renderer=$webRenderer',
'--dart-define=DART_HHH_BOT=$_runningInDartHHHBot', '--dart-define=DART_HHH_BOT=$_runningInDartHHHBot',
...flutterTestArgs, ...flutterTestArgs,
......
...@@ -2,11 +2,12 @@ ...@@ -2,11 +2,12 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:html' as html; import 'dart:js_interop';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:meta/dart2js.dart'; import 'package:meta/dart2js.dart';
import 'package:web/web.dart' as web;
// Tests that the framework prints stack traces in all build modes. // Tests that the framework prints stack traces in all build modes.
// //
...@@ -35,10 +36,12 @@ Future<void> main() async { ...@@ -35,10 +36,12 @@ Future<void> main() async {
} }
print(output); print(output);
html.HttpRequest.request( web.window.fetch(
'/test-result', '/test-result'.toJS,
method: 'POST', web.RequestInit(
sendData: '$output', method: 'POST',
body: '$output'.toJS,
)
); );
} }
......
...@@ -2,10 +2,13 @@ ...@@ -2,10 +2,13 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:html' as html; import 'dart:js_interop';
import 'package:web/web.dart' as web;
Future<void> main() async { Future<void> main() async {
await html.window.navigator.serviceWorker?.ready; await web.window.navigator.serviceWorker.ready.toDart;
const String response = 'CLOSE?version=1'; final JSString response = 'CLOSE?version=1'.toJS;
await html.HttpRequest.getString(response); await web.window.fetch(response).toDart;
html.document.body?.appendHtml(response); web.document.body?.append(response);
} }
...@@ -2,9 +2,12 @@ ...@@ -2,9 +2,12 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:html' as html; import 'dart:js_interop';
import 'package:web/web.dart' as web;
Future<void> main() async { Future<void> main() async {
const String response = 'CLOSE?version=1'; final JSString response = 'CLOSE?version=1'.toJS;
await html.HttpRequest.getString(response); await web.window.fetch(response).toDart;
html.document.body?.appendHtml(response); web.document.body?.append(response);
} }
...@@ -4,7 +4,9 @@ ...@@ -4,7 +4,9 @@
// @dart = 2.12 // @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. // Verify that web applications can be run in sound mode.
void main() { void main() {
...@@ -16,9 +18,11 @@ void main() { ...@@ -16,9 +18,11 @@ void main() {
output = '--- TEST SUCCEEDED ---'; output = '--- TEST SUCCEEDED ---';
} }
print(output); print(output);
html.HttpRequest.request( web.window.fetch(
'/test-result', '/test-result'.toJS,
method: 'POST', web.RequestInit(
sendData: output, method: 'POST',
body: output.toJS,
)
); );
} }
...@@ -2,11 +2,12 @@ ...@@ -2,11 +2,12 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:html' as html; import 'dart:js_interop';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:meta/dart2js.dart'; import 'package:meta/dart2js.dart';
import 'package:web/web.dart' as web;
/// Expected sequence of method calls. /// Expected sequence of method calls.
const List<String> callChain = <String>['baz', 'bar', 'foo']; const List<String> callChain = <String>['baz', 'bar', 'foo'];
...@@ -32,7 +33,7 @@ const List<StackFrame> expectedDebugStackFrames = <StackFrame>[ ...@@ -32,7 +33,7 @@ const List<StackFrame> expectedDebugStackFrames = <StackFrame>[
packageScheme: 'package', packageScheme: 'package',
package: 'packages', package: 'packages',
packagePath: 'web_integration/stack_trace.dart', packagePath: 'web_integration/stack_trace.dart',
line: 119, line: 122,
column: 3, column: 3,
className: '<unknown>', className: '<unknown>',
method: 'baz', method: 'baz',
...@@ -43,7 +44,7 @@ const List<StackFrame> expectedDebugStackFrames = <StackFrame>[ ...@@ -43,7 +44,7 @@ const List<StackFrame> expectedDebugStackFrames = <StackFrame>[
packageScheme: 'package', packageScheme: 'package',
package: 'packages', package: 'packages',
packagePath: 'web_integration/stack_trace.dart', packagePath: 'web_integration/stack_trace.dart',
line: 114, line: 117,
column: 3, column: 3,
className: '<unknown>', className: '<unknown>',
method: 'bar', method: 'bar',
...@@ -54,7 +55,7 @@ const List<StackFrame> expectedDebugStackFrames = <StackFrame>[ ...@@ -54,7 +55,7 @@ const List<StackFrame> expectedDebugStackFrames = <StackFrame>[
packageScheme: 'package', packageScheme: 'package',
package: 'packages', package: 'packages',
packagePath: 'web_integration/stack_trace.dart', packagePath: 'web_integration/stack_trace.dart',
line: 109, line: 112,
column: 3, column: 3,
className: '<unknown>', className: '<unknown>',
method: 'foo', method: 'foo',
...@@ -97,10 +98,12 @@ void main() { ...@@ -97,10 +98,12 @@ void main() {
output.writeln('--- TEST FAILED ---'); output.writeln('--- TEST FAILED ---');
} }
print(output); print(output);
html.HttpRequest.request( web.window.fetch(
'/test-result', '/test-result'.toJS,
method: 'POST', web.RequestInit(
sendData: '$output', method: 'POST',
body: '$output'.toJS,
)
); );
} }
......
...@@ -2,7 +2,9 @@ ...@@ -2,7 +2,9 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:html' as html; import 'dart:js_interop';
import 'package:web/web.dart' as web;
Future<void> main() async { Future<void> main() async {
final StringBuffer output = StringBuffer(); final StringBuffer output = StringBuffer();
...@@ -16,9 +18,11 @@ Future<void> main() async { ...@@ -16,9 +18,11 @@ Future<void> main() async {
print('--- TEST FAILED ---'); print('--- TEST FAILED ---');
} }
html.HttpRequest.request( web.window.fetch(
'/test-result', '/test-result'.toJS,
method: 'POST', web.RequestInit(
sendData: '$output', method: 'POST',
body: '$output'.toJS,
)
); );
} }
...@@ -2,16 +2,20 @@ ...@@ -2,16 +2,20 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // 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. // Attempt to load a file that is hosted in the applications's `web/` directory.
Future<void> main() async { Future<void> main() async {
try { try {
final html.HttpRequest request = await html.HttpRequest.request( final web.Response response = await web.window.fetch(
'/example', '/example'.toJS,
method: 'GET', web.RequestInit(
); method: 'GET',
final String? body = request.responseText; ),
).toDart;
final String body = (await response.text().toDart).toDart;
if (body == 'This is an Example') { if (body == 'This is an Example') {
print('--- TEST SUCCEEDED ---'); print('--- TEST SUCCEEDED ---');
} else { } else {
......
...@@ -2,7 +2,9 @@ ...@@ -2,7 +2,9 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // 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. // Attempt to load CanvasKit resources hosted on gstatic.
Future<void> main() async { Future<void> main() async {
...@@ -12,12 +14,13 @@ Future<void> main() async { ...@@ -12,12 +14,13 @@ Future<void> main() async {
return; return;
} }
try { try {
final html.HttpRequest request = await html.HttpRequest.request( final web.Response response = await web.window.fetch(
'https://www.gstatic.com/flutter-canvaskit/$engineVersion/canvaskit.js', 'https://www.gstatic.com/flutter-canvaskit/$engineVersion/canvaskit.js'.toJS,
method: 'GET', web.RequestInit(
); method: 'GET',
final dynamic response = request.response; ),
if (response != null) { ).toDart;
if (response.ok) {
print('--- TEST SUCCEEDED ---'); print('--- TEST SUCCEEDED ---');
} else { } else {
print('--- TEST FAILED ---'); print('--- TEST FAILED ---');
...@@ -27,12 +30,13 @@ Future<void> main() async { ...@@ -27,12 +30,13 @@ Future<void> main() async {
print('--- TEST FAILED ---'); print('--- TEST FAILED ---');
} }
try { try {
final html.HttpRequest request = await html.HttpRequest.request( final web.Response response = await web.window.fetch(
'https://www.gstatic.com/flutter-canvaskit/$engineVersion/canvaskit.wasm', 'https://www.gstatic.com/flutter-canvaskit/$engineVersion/canvaskit.wasm'.toJS,
method: 'GET', web.RequestInit(
); method: 'GET',
final dynamic response = request.response; )
if (response != null) { ).toDart;
if (response.ok) {
print('--- TEST SUCCEEDED ---'); print('--- TEST SUCCEEDED ---');
} else { } else {
print('--- TEST FAILED ---'); print('--- TEST FAILED ---');
......
...@@ -14,10 +14,12 @@ dependencies: ...@@ -14,10 +14,12 @@ dependencies:
flutter: flutter:
sdk: flutter sdk: flutter
web: 0.5.1
characters: 1.3.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" 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" 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" 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" 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" vector_math: 2.1.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
# PUBSPEC CHECKSUM: 6f95 # PUBSPEC CHECKSUM: 8d22
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
import 'package:web_integration/a.dart' import 'package:web_integration/a.dart'
if (dart.library.io) 'package:web_integration/b.dart' as message1; if (dart.library.io) 'package:web_integration/b.dart' as message1;
import 'package:web_integration/c.dart' 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() { void main() {
if (message1.message == 'a' && message2.message == 'd') { if (message1.message == 'a' && message2.message == 'd') {
......
...@@ -2,12 +2,11 @@ ...@@ -2,12 +2,11 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:html';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:web/web.dart' as web;
/// Whether the current browser is Firefox. /// 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. /// 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 ...@@ -15,8 +14,8 @@ bool get isFirefox => window.navigator.userAgent.toLowerCase().contains('firefox
/// `<flt-glass-pane>` element. Otherwise, looks under `<flt-glass-pane>` /// `<flt-glass-pane>` element. Otherwise, looks under `<flt-glass-pane>`
/// without penetrating the shadow DOM. In the latter case, if the application /// without penetrating the shadow DOM. In the latter case, if the application
/// creates platform views, this will also find platform view elements. /// creates platform views, this will also find platform view elements.
List<Node> findElements(String selector) { web.NodeList findElements(String selector) {
final Element? flutterView = document.querySelector('flutter-view'); final web.Element? flutterView = web.document.querySelector('flutter-view');
if (flutterView == null) { if (flutterView == null) {
fail( fail(
......
...@@ -2,10 +2,10 @@ ...@@ -2,10 +2,10 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:html' as html;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:web/web.dart' as web;
void main() => runApp(const MyApp()); void main() => runApp(const MyApp());
class MyApp extends StatelessWidget { class MyApp extends StatelessWidget {
...@@ -79,29 +79,25 @@ abstract class DeltaMode { ...@@ -79,29 +79,25 @@ abstract class DeltaMode {
void dispatchMouseWheelEvent(int mouseX, int mouseY, void dispatchMouseWheelEvent(int mouseX, int mouseY,
int deltaMode, double deltaX, double deltaY) { 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, screenX: mouseX,
screenY: mouseY, screenY: mouseY,
clientX: mouseX, clientX: mouseX,
clientY: mouseY, clientY: mouseY,
)); )));
target.dispatchEvent(html.MouseEvent('mousemove', target.dispatchEvent(web.MouseEvent('mousemove', web.MouseEventInit(
screenX: mouseX, screenX: mouseX,
screenY: mouseY, screenY: mouseY,
clientX: mouseX, clientX: mouseX,
clientY: mouseY, clientY: mouseY,
)); )));
target.dispatchEvent(html.WheelEvent('wheel', target.dispatchEvent(web.WheelEvent('wheel', web.WheelEventInit(
screenX: mouseX,
screenY: mouseY,
clientX: mouseX,
clientY: mouseY,
deltaMode: deltaMode, deltaMode: deltaMode,
deltaX : deltaX, deltaX : deltaX,
deltaY : deltaY, deltaY : deltaY,
)); )));
} }
...@@ -21,6 +21,7 @@ dependencies: ...@@ -21,6 +21,7 @@ dependencies:
integration_test: integration_test:
sdk: flutter sdk: flutter
flutter_gallery_assets: 1.0.2 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" 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" boolean_selector: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
...@@ -82,7 +83,6 @@ dev_dependencies: ...@@ -82,7 +83,6 @@ dev_dependencies:
test_core: 0.6.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" 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" 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" 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" 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" 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" yaml: 3.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
......
...@@ -2,13 +2,13 @@ ...@@ -2,13 +2,13 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:html' as html;
import 'dart:ui_web' as ui_web; import 'dart:ui_web' as ui_web;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_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; import 'package:web_e2e_tests/platform_messages_main.dart' as app;
void main() { void main() {
...@@ -27,7 +27,7 @@ void main() { ...@@ -27,7 +27,7 @@ void main() {
await tester.tap(find.byKey(const Key('input'))); await tester.tap(find.byKey(const Key('input')));
// Focus in input, otherwise clipboard will fail with // Focus in input, otherwise clipboard will fail with
// 'document is not focused' platform exception. // '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')); await Clipboard.setData(const ClipboardData(text: 'sample text'));
}, skip: true); // https://github.com/flutter/flutter/issues/54296 }, skip: true); // https://github.com/flutter/flutter/issues/54296
...@@ -38,7 +38,7 @@ void main() { ...@@ -38,7 +38,7 @@ void main() {
platformViewsRegistry.getNextPlatformViewId(); platformViewsRegistry.getNextPlatformViewId();
ui_web.platformViewRegistry.registerViewFactory('MyView', (int viewId) { ui_web.platformViewRegistry.registerViewFactory('MyView', (int viewId) {
viewInstanceCount += 1; viewInstanceCount += 1;
return html.DivElement(); return web.HTMLDivElement();
}); });
app.main(); app.main();
......
...@@ -2,13 +2,13 @@ ...@@ -2,13 +2,13 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:html';
import 'dart:js_util' as js_util; import 'dart:js_util' as js_util;
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_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/common.dart';
import 'package:web_e2e_tests/text_editing_main.dart' as app; import 'package:web_e2e_tests/text_editing_main.dart' as app;
...@@ -26,9 +26,9 @@ void main() { ...@@ -26,9 +26,9 @@ void main() {
await tester.tap(find.byKey(const Key('input'))); await tester.tap(find.byKey(const Key('input')));
// A native input element will be appended to the DOM. // A native input element will be appended to the DOM.
final List<Node> nodeList = findElements('input'); final web.NodeList nodeList = findElements('input');
expect(nodeList.length, equals(1)); 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. // The element's value will be the same as the textFormField's value.
expect(input.value, 'Text1'); expect(input.value, 'Text1');
...@@ -50,9 +50,9 @@ void main() { ...@@ -50,9 +50,9 @@ void main() {
await tester.tap(find.byKey(const Key('empty-input'))); await tester.tap(find.byKey(const Key('empty-input')));
// A native input element will be appended to the DOM. // A native input element will be appended to the DOM.
final List<Node> nodeList = findElements('input'); final web.NodeList nodeList = findElements('input');
expect(nodeList.length, equals(1)); 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. // The element's value will be empty.
expect(input.value, ''); expect(input.value, '');
...@@ -81,7 +81,7 @@ void main() { ...@@ -81,7 +81,7 @@ void main() {
await tester.tap(find.byKey(const Key('input2'))); await tester.tap(find.byKey(const Key('input2')));
// Press Tab. This should trigger `onFieldSubmitted` of TextField. // 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', <String, dynamic>{ dispatchKeyboardEvent(input, 'keydown', <String, dynamic>{
'keyCode': 13, // Enter. 'keyCode': 13, // Enter.
'cancelable': true, 'cancelable': true,
...@@ -111,9 +111,9 @@ void main() { ...@@ -111,9 +111,9 @@ void main() {
await tester.tap(find.byKey(const Key('input'))); await tester.tap(find.byKey(const Key('input')));
// A native input element will be appended to the DOM. // A native input element will be appended to the DOM.
final List<Node> nodeList = findElements('input'); final web.NodeList nodeList = findElements('input');
expect(nodeList.length, equals(1)); 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. // Press Tab. The focus should move to the next TextFormField.
dispatchKeyboardEvent(input, 'keydown', <String, dynamic>{ dispatchKeyboardEvent(input, 'keydown', <String, dynamic>{
...@@ -136,7 +136,7 @@ void main() { ...@@ -136,7 +136,7 @@ void main() {
// A native input element for the next TextField should be attached to the // A native input element for the next TextField should be attached to the
// DOM. // DOM.
final InputElement input2 = findElements('input')[0] as InputElement; final web.HTMLInputElement input2 = findElements('input').item(0)! as web.HTMLInputElement;
expect(input2.value, 'Text2'); expect(input2.value, 'Text2');
}, semanticsEnabled: false); }, semanticsEnabled: false);
...@@ -150,9 +150,9 @@ void main() { ...@@ -150,9 +150,9 @@ void main() {
await tester.tap(find.byKey(const Key('input'))); await tester.tap(find.byKey(const Key('input')));
// A native input element will be appended to the DOM. // A native input element will be appended to the DOM.
final List<Node> nodeList = findElements('input'); final web.NodeList nodeList = findElements('input');
expect(nodeList.length, equals(1)); 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. // Press and release CapsLock.
dispatchKeyboardEvent(input, 'keydown', <String, dynamic>{ dispatchKeyboardEvent(input, 'keydown', <String, dynamic>{
...@@ -191,7 +191,7 @@ void main() { ...@@ -191,7 +191,7 @@ void main() {
// A native input element for the next TextField should be attached to the // A native input element for the next TextField should be attached to the
// DOM. // DOM.
final InputElement input2 = findElements('input')[0] as InputElement; final web.HTMLInputElement input2 = findElements('input').item(0)! as web.HTMLInputElement;
expect(input2.value, 'Text2'); expect(input2.value, 'Text2');
}, semanticsEnabled: false); }, semanticsEnabled: false);
...@@ -215,16 +215,16 @@ void main() { ...@@ -215,16 +215,16 @@ void main() {
await gesture.up(); await gesture.up();
// A native input element will be appended to the DOM. // A native input element will be appended to the DOM.
final List<Node> nodeList = findElements('textarea'); final web.NodeList nodeList = findElements('textarea');
expect(nodeList.length, equals(1)); 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. // The element's value should contain the selectable text.
expect(input.value, text); expect(input.value, text);
expect(input.hasAttribute('readonly'), isTrue); expect(input.hasAttribute('readonly'), isTrue);
// Make sure the entire text is selected. // Make sure the entire text is selected.
TextRange? range = TextRange? range =
TextRange(start: input.selectionStart!, end: input.selectionEnd!); TextRange(start: input.selectionStart, end: input.selectionEnd);
expect(range.textInside(text), text); expect(range.textInside(text), text);
// Double tap to select the first word. // Double tap to select the first word.
...@@ -236,7 +236,7 @@ void main() { ...@@ -236,7 +236,7 @@ void main() {
await gesture.up(); await gesture.up();
await gesture.down(firstWordOffset); await gesture.down(firstWordOffset);
await gesture.up(); await gesture.up();
range = TextRange(start: input.selectionStart!, end: input.selectionEnd!); range = TextRange(start: input.selectionStart, end: input.selectionEnd);
expect(range.textInside(text), 'Lorem'); expect(range.textInside(text), 'Lorem');
// Double tap to select the last word. // Double tap to select the last word.
...@@ -248,21 +248,21 @@ void main() { ...@@ -248,21 +248,21 @@ void main() {
await gesture.up(); await gesture.up();
await gesture.down(lastWordOffset); await gesture.down(lastWordOffset);
await gesture.up(); await gesture.up();
range = TextRange(start: input.selectionStart!, end: input.selectionEnd!); range = TextRange(start: input.selectionStart, end: input.selectionEnd);
expect(range.textInside(text), 'amet'); expect(range.textInside(text), 'amet');
}, semanticsEnabled: false); }, semanticsEnabled: false);
} }
KeyboardEvent dispatchKeyboardEvent( web.KeyboardEvent dispatchKeyboardEvent(
EventTarget target, String type, Map<String, dynamic> args) { web.EventTarget target, String type, Map<String, dynamic> args) {
final Object jsKeyboardEvent = js_util.getProperty(window, 'KeyboardEvent') as Object; final Object jsKeyboardEvent = js_util.getProperty(web.window, 'KeyboardEvent') as Object;
final List<dynamic> eventArgs = <dynamic>[ final List<dynamic> eventArgs = <dynamic>[
type, type,
args, args,
]; ];
final KeyboardEvent event = js_util.callConstructor( final web.KeyboardEvent event = js_util.callConstructor(
jsKeyboardEvent, js_util.jsify(eventArgs) as List<dynamic>) jsKeyboardEvent, js_util.jsify(eventArgs) as List<dynamic>)
as KeyboardEvent; as web.KeyboardEvent;
target.dispatchEvent(event); target.dispatchEvent(event);
return event; return event;
......
...@@ -3,13 +3,16 @@ ...@@ -3,13 +3,16 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:async'; import 'dart:async';
import 'dart:html' as html; import 'dart:js_interop';
import 'dart:ui' as ui; import 'dart:ui' as ui;
import 'dart:ui_web';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_web_plugins/flutter_web_plugins.dart'; import 'package:flutter_web_plugins/flutter_web_plugins.dart';
import 'package:integration_test/integration_test.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; import 'package:web_e2e_tests/url_strategy_main.dart' as app;
void main() { void main() {
...@@ -56,7 +59,7 @@ class TestUrlStrategy extends UrlStrategy { ...@@ -56,7 +59,7 @@ class TestUrlStrategy extends UrlStrategy {
String getPath() => currentEntry.url; String getPath() => currentEntry.url;
@override @override
dynamic getState() => currentEntry.state; Object? getState() => currentEntry.state;
int _currentEntryIndex; int _currentEntryIndex;
final List<TestHistoryEntry> history; final List<TestHistoryEntry> history;
...@@ -111,10 +114,10 @@ class TestUrlStrategy extends UrlStrategy { ...@@ -111,10 +114,10 @@ class TestUrlStrategy extends UrlStrategy {
}); });
} }
final List<html.EventListener> listeners = <html.EventListener>[]; final List<PopStateListener> listeners = <PopStateListener>[];
@override @override
ui.VoidCallback addPopStateListener(html.EventListener fn) { ui.VoidCallback addPopStateListener(PopStateListener fn) {
listeners.add(fn); listeners.add(fn);
return () { return () {
// Schedule a micro task here to avoid removing the listener during // Schedule a micro task here to avoid removing the listener during
...@@ -134,9 +137,9 @@ class TestUrlStrategy extends UrlStrategy { ...@@ -134,9 +137,9 @@ class TestUrlStrategy extends UrlStrategy {
/// like a real browser. /// like a real browser.
void _firePopStateEvent() { void _firePopStateEvent() {
assert(withinAppHistory); assert(withinAppHistory);
final html.PopStateEvent event = html.PopStateEvent( final web.PopStateEvent event = web.PopStateEvent(
'popstate', 'popstate',
<String, dynamic>{'state': currentEntry.state}, PopStateEventInit(state: currentEntry.state?.toJSBox),
); );
for (int i = 0; i < listeners.length; i++) { for (int i = 0; i < listeners.length; i++) {
listeners[i](event); listeners[i](event);
...@@ -160,7 +163,7 @@ class TestUrlStrategy extends UrlStrategy { ...@@ -160,7 +163,7 @@ class TestUrlStrategy extends UrlStrategy {
class TestHistoryEntry { class TestHistoryEntry {
const TestHistoryEntry(this.state, this.title, this.url); const TestHistoryEntry(this.state, this.title, this.url);
final dynamic state; final Object? state;
final String? title; final String? title;
final String url; final String url;
......
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
import 'dart:async'; 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<void> testExecutable(FutureOr<void> Function() testMain) { Future<void> testExecutable(FutureOr<void> Function() testMain) {
// Enable golden file testing using Skia Gold. // Enable golden file testing using Skia Gold.
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
import 'bitfield.dart' as bitfield; 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 /// 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 /// the SMI range and avoid heap allocations. Because number encoding is
...@@ -13,14 +13,14 @@ import 'bitfield.dart' as bitfield; ...@@ -13,14 +13,14 @@ import 'bitfield.dart' as bitfield;
/// does not have to guarantee efficiency. /// does not have to guarantee efficiency.
const int kMaxUnsignedSMI = -1; const int kMaxUnsignedSMI = -1;
/// The dart:html implementation of [bitfield.Bitfield]. /// The web implementation of [bitfield.Bitfield].
class BitField<T extends dynamic> implements bitfield.BitField<T> { class BitField<T extends dynamic> implements bitfield.BitField<T> {
/// The dart:html implementation of [bitfield.Bitfield]. /// The web implementation of [bitfield.Bitfield].
// Can remove when we have metaclasses. // Can remove when we have metaclasses.
// ignore: avoid_unused_constructor_parameters // ignore: avoid_unused_constructor_parameters
BitField(int length); BitField(int length);
/// The dart:html implementation of [bitfield.Bitfield.filled]. /// The web implementation of [bitfield.Bitfield.filled].
// Can remove when we have metaclasses. // Can remove when we have metaclasses.
// ignore: avoid_unused_constructor_parameters // ignore: avoid_unused_constructor_parameters
BitField.filled(int length, bool value); BitField.filled(int length, bool value);
......
...@@ -6,7 +6,7 @@ import 'isolates.dart' as isolates; ...@@ -6,7 +6,7 @@ import 'isolates.dart' as isolates;
export 'isolates.dart' show ComputeCallback; export 'isolates.dart' show ComputeCallback;
/// The dart:html implementation of [isolate.compute]. /// The web implementation of [isolate.compute].
@pragma('dart2js:tryInline') @pragma('dart2js:tryInline')
Future<R> compute<M, R>(isolates.ComputeCallback<M, R> callback, M message, { String? debugLabel }) async { Future<R> compute<M, R>(isolates.ComputeCallback<M, R> callback, M message, { String? debugLabel }) async {
// To avoid blocking the UI immediately for an expensive function call, we // To avoid blocking the UI immediately for an expensive function call, we
......
...@@ -10,7 +10,7 @@ import 'package:flutter/rendering.dart'; ...@@ -10,7 +10,7 @@ import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.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; as flutter_goldens;
/// If true, leak tracking is enabled for all `testWidgets`. /// If true, leak tracking is enabled for all `testWidgets`.
......
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
/// ///
/// See also: /// See also:
/// ///
/// * [_extension_web.dart], which has the dart:html implementation /// * [_extension_web.dart], which has the web implementation
void registerWebServiceExtension(Future<Map<String, dynamic>> Function(Map<String, String>) call) { void registerWebServiceExtension(Future<Map<String, dynamic>> Function(Map<String, String>) call) {
throw UnsupportedError('Use registerServiceExtension instead'); throw UnsupportedError('Use registerServiceExtension instead');
} }
...@@ -3,11 +3,13 @@ ...@@ -3,11 +3,13 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:convert'; import 'dart:convert';
import 'dart:html' as html; import 'dart:js_interop';
import 'dart:js'; import 'dart:js_interop_unsafe';
import 'dart:js_util' as js_util;
/// 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. /// Registers Web Service Extension for Flutter Web application.
/// ///
...@@ -21,13 +23,13 @@ void registerWebServiceExtension(Future<Map<String, dynamic>> Function(Map<Strin ...@@ -21,13 +23,13 @@ void registerWebServiceExtension(Future<Map<String, dynamic>> Function(Map<Strin
// Define the result variable because packages/flutter_driver/lib/src/driver/web_driver.dart // Define the result variable because packages/flutter_driver/lib/src/driver/web_driver.dart
// checks for this value to become non-null when waiting for the result. If this value is // checks for this value to become non-null when waiting for the result. If this value is
// undefined at the time of the check, WebDriver throws an exception. // undefined at the time of the check, WebDriver throws an exception.
context[r'$flutterDriverResult'] = null; _window.setProperty(r'$flutterDriverResult'.toJS, null);
js_util.setProperty(html.window, r'$flutterDriver', allowInterop((dynamic message) async { _window.setProperty(r'$flutterDriver'.toJS, (JSAny message) {
final Map<String, String> params = Map<String, String>.from( final Map<String, String> params = Map<String, String>.from(
jsonDecode(message as String) as Map<String, dynamic>); jsonDecode((message as JSString).toDart) as Map<String, dynamic>);
final Map<String, dynamic> result = Map<String, dynamic>.from( call(params).then((Map<String, dynamic> result) {
await call(params)); _window.setProperty(r'$flutterDriverResult'.toJS, json.encode(result).toJS);
context[r'$flutterDriverResult'] = json.encode(result); });
})); }.toJS);
} }
...@@ -20,7 +20,7 @@ import '../common/error.dart'; ...@@ -20,7 +20,7 @@ import '../common/error.dart';
import '../common/find.dart'; import '../common/find.dart';
import '../common/handler_factory.dart'; import '../common/handler_factory.dart';
import '../common/message.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'; const String _extensionMethodName = 'driver';
......
...@@ -2,11 +2,15 @@ ...@@ -2,11 +2,15 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // 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_driver/src/extension/_extension_web.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
@JS('window')
external JSObject get _window;
void main() { void main() {
group('test web_extension', () { group('test web_extension', () {
late Future<Map<String, dynamic>> Function(Map<String, String>) call; late Future<Map<String, dynamic>> Function(Map<String, String>) call;
...@@ -21,11 +25,11 @@ void main() { ...@@ -21,11 +25,11 @@ void main() {
expect(() => registerWebServiceExtension(call), expect(() => registerWebServiceExtension(call),
returnsNormally); returnsNormally);
expect(js.context.hasProperty(r'$flutterDriver'), true); expect(_window.hasProperty(r'$flutterDriver'.toJS).toDart, true);
expect(js.context[r'$flutterDriver'], isNotNull); expect(_window.getProperty(r'$flutterDriver'.toJS), isNotNull);
expect(js.context.hasProperty(r'$flutterDriverResult'), true); expect(_window.hasProperty(r'$flutterDriverResult'.toJS).toDart, true);
expect(js.context[r'$flutterDriverResult'], isNull); expect(_window.getProperty(r'$flutterDriverResult'.toJS), isNull);
}); });
}); });
} }
...@@ -55,8 +55,9 @@ library flutter_test; ...@@ -55,8 +55,9 @@ library flutter_test;
export 'dart:async' show Future; export 'dart:async' show Future;
export 'src/_goldens_io.dart' if (dart.library.html) 'src/_goldens_web.dart'; export 'src/_goldens_io.dart' if (dart.library.js_interop) 'src/_goldens_web.dart';
export 'src/_matchers_io.dart' if (dart.library.html) 'src/_matchers_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/accessibility.dart';
export 'src/animation_sheet.dart'; export 'src/animation_sheet.dart';
export 'src/binding.dart'; export 'src/binding.dart';
......
...@@ -292,6 +292,11 @@ ByteData _invert(ByteData imageBytes) { ...@@ -292,6 +292,11 @@ ByteData _invert(ByteData imageBytes) {
/// An unsupported [WebGoldenComparator] that exists for API compatibility. /// An unsupported [WebGoldenComparator] that exists for API compatibility.
class DefaultWebGoldenComparator extends WebGoldenComparator { 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 @override
Future<bool> compare(double width, double height, Uri golden) { Future<bool> compare(double width, double height, Uri golden) {
throw UnsupportedError('DefaultWebGoldenComparator is only supported on the web.'); throw UnsupportedError('DefaultWebGoldenComparator is only supported on the web.');
......
...@@ -3,12 +3,13 @@ ...@@ -3,12 +3,13 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:convert'; import 'dart:convert';
import 'dart:html' as html; import 'dart:js_interop';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:matcher/expect.dart' show fail; import 'package:matcher/expect.dart' show fail;
import 'goldens.dart'; import 'goldens.dart';
import 'web.dart' as web;
/// An unsupported [GoldenFileComparator] that exists for API compatibility. /// An unsupported [GoldenFileComparator] that exists for API compatibility.
class LocalFileComparator extends GoldenFileComparator { class LocalFileComparator extends GoldenFileComparator {
...@@ -58,21 +59,23 @@ class DefaultWebGoldenComparator extends WebGoldenComparator { ...@@ -58,21 +59,23 @@ class DefaultWebGoldenComparator extends WebGoldenComparator {
@override @override
Future<bool> compare(double width, double height, Uri golden) async { Future<bool> compare(double width, double height, Uri golden) async {
final String key = golden.toString(); final String key = golden.toString();
final html.HttpRequest request = await html.HttpRequest.request( final web.Response response = await web.window.fetch(
'flutter_goldens', 'flutter_goldens'.toJS,
method: 'POST', web.RequestInit(
sendData: json.encode(<String, Object>{ method: 'POST',
'testUri': testUri.toString(), body: json.encode(<String, Object>{
'key': key, 'testUri': testUri.toString(),
'width': width.round(), 'key': key,
'height': height.round(), 'width': width.round(),
}), 'height': height.round(),
); }).toJS,
final String response = request.response as String; )
if (response == 'true') { ).toDart;
final String responseText = (await response.text().toDart).toDart;
if (responseText == 'true') {
return true; return true;
} }
fail(response); fail(responseText);
} }
@override @override
...@@ -85,20 +88,22 @@ class DefaultWebGoldenComparator extends WebGoldenComparator { ...@@ -85,20 +88,22 @@ class DefaultWebGoldenComparator extends WebGoldenComparator {
Future<bool> compareBytes(Uint8List bytes, Uri golden) async { Future<bool> compareBytes(Uint8List bytes, Uri golden) async {
final String key = golden.toString(); final String key = golden.toString();
final String bytesEncoded = base64.encode(bytes); final String bytesEncoded = base64.encode(bytes);
final html.HttpRequest request = await html.HttpRequest.request( final web.Response response = await web.window.fetch(
'flutter_goldens', 'flutter_goldens'.toJS,
method: 'POST', web.RequestInit(
sendData: json.encode(<String, Object>{ method: 'POST',
'testUri': testUri.toString(), body: json.encode(<String, Object>{
'key': key, 'testUri': testUri.toString(),
'bytes': bytesEncoded, 'key': key,
}), 'bytes': bytesEncoded,
); }).toJS,
final String response = request.response as String; )
if (response == 'true') { ).toDart;
final String responseText = (await response.text().toDart).toDart;
if (responseText == 'true') {
return true; return true;
} }
fail(response); fail(responseText);
} }
@override @override
......
// 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.
// 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<void> Function();
/// An entry point runner provided by a test config file
typedef EntryPointRunner = Future<void> 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<void> runWebTest(WebTest test) async {
ui_web.debugEmulateFlutterTesterEnvironment = true;
final Completer<void> completer = Completer<void>();
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<Object?> channel = _serializeSuite(getMain, hidePrints: false);
_postMessageChannel().pipe(channel);
}
StreamChannel<Object?> _serializeSuite(EntryPoint Function() getMain, {bool hidePrints = true}) => RemoteListener.start(getMain, hidePrints: hidePrints);
StreamChannel<Object?> _postMessageChannel() {
final StreamChannelController<Object?> controller = StreamChannelController<Object?>(sync: true);
final web.MessageChannel channel = web.MessageChannel();
web.window.parent!.postMessage('port'.toJS, web.window.location.origin, <JSObject>[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;
}
...@@ -18,7 +18,7 @@ import 'package:stack_trace/stack_trace.dart' as stack_trace; ...@@ -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:test_api/scaffolding.dart' as test_package show Timeout;
import 'package:vector_math/vector_math_64.dart'; 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 'goldens.dart';
import 'platform.dart'; import 'platform.dart';
import 'restoration.dart'; import 'restoration.dart';
......
...@@ -8,7 +8,7 @@ import 'dart:ui'; ...@@ -8,7 +8,7 @@ import 'dart:ui';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'package:path/path.dart' as path; 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. /// Compares image pixels against a golden image file.
/// ///
......
...@@ -14,7 +14,7 @@ import 'package:matcher/expect.dart'; ...@@ -14,7 +14,7 @@ import 'package:matcher/expect.dart';
import 'package:matcher/src/expect/async_matcher.dart'; // ignore: implementation_imports import 'package:matcher/src/expect/async_matcher.dart'; // ignore: implementation_imports
import 'package:vector_math/vector_math_64.dart' show Matrix3; 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 'accessibility.dart';
import 'binding.dart'; import 'binding.dart';
import 'controller.dart'; import 'controller.dart';
......
// 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<Response> fetch(
JSAny input, [
RequestInit init,
]);
external Location get location;
external Window? get parent;
external void postMessage(JSAny message, JSString targetOrigin, JSArray<JSAny> transfers);
external JSString? get testSelector;
}
extension type Response._(JSObject _) implements JSObject {
external JSPromise<JSString> 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;
}
name: flutter_test name: flutter_test
environment: environment:
sdk: '>=3.2.0-0 <4.0.0' sdk: '>=3.3.0-0 <4.0.0'
dependencies: dependencies:
# To update these, use "flutter update-packages --force-upgrade". # To update these, use "flutter update-packages --force-upgrade".
......
...@@ -12,6 +12,7 @@ import '../build_info.dart'; ...@@ -12,6 +12,7 @@ import '../build_info.dart';
import '../bundle_builder.dart'; import '../bundle_builder.dart';
import '../devfs.dart'; import '../devfs.dart';
import '../device.dart'; import '../device.dart';
import '../features.dart';
import '../globals.dart' as globals; import '../globals.dart' as globals;
import '../native_assets.dart'; import '../native_assets.dart';
import '../project.dart'; import '../project.dart';
...@@ -23,6 +24,7 @@ import '../test/test_time_recorder.dart'; ...@@ -23,6 +24,7 @@ import '../test/test_time_recorder.dart';
import '../test/test_wrapper.dart'; import '../test/test_wrapper.dart';
import '../test/watcher.dart'; import '../test/watcher.dart';
import '../web/compile.dart'; import '../web/compile.dart';
import '../web/web_constants.dart';
/// The name of the directory where Integration Tests are placed. /// The name of the directory where Integration Tests are placed.
/// ///
...@@ -233,7 +235,14 @@ class TestCommand extends FlutterCommand with DeviceBasedDevelopmentArtifacts { ...@@ -233,7 +235,14 @@ class TestCommand extends FlutterCommand with DeviceBasedDevelopmentArtifacts {
'in seconds (e.g. "60s"), ' 'in seconds (e.g. "60s"), '
'as a multiplier of the default timeout (e.g. "2x"), ' 'as a multiplier of the default timeout (e.g. "2x"), '
'or as the string "none" to disable the timeout entirely.', '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); addDdsOptions(verboseHelp: verboseHelp);
addServeObservatoryOptions(verboseHelp: verboseHelp); addServeObservatoryOptions(verboseHelp: verboseHelp);
usesFatalWarningsOption(verboseHelp: verboseHelp); usesFatalWarningsOption(verboseHelp: verboseHelp);
...@@ -256,6 +265,7 @@ class TestCommand extends FlutterCommand with DeviceBasedDevelopmentArtifacts { ...@@ -256,6 +265,7 @@ class TestCommand extends FlutterCommand with DeviceBasedDevelopmentArtifacts {
final Set<Uri> _testFileUris = <Uri>{}; final Set<Uri> _testFileUris = <Uri>{};
bool get isWeb => stringArg('platform') == 'chrome'; bool get isWeb => stringArg('platform') == 'chrome';
bool get useWasm => boolArg(FlutterOptions.kWebWasmFlag);
@override @override
Future<Set<DevelopmentArtifact>> get requiredArtifacts async { Future<Set<DevelopmentArtifact>> get requiredArtifacts async {
...@@ -499,6 +509,10 @@ class TestCommand extends FlutterCommand with DeviceBasedDevelopmentArtifacts { ...@@ -499,6 +509,10 @@ class TestCommand extends FlutterCommand with DeviceBasedDevelopmentArtifacts {
watcher = collector; watcher = collector;
} }
if (!isWeb && useWasm) {
throwToolExit('--wasm is only supported on the web platform');
}
Device? integrationTestDevice; Device? integrationTestDevice;
if (_isIntegrationTest) { if (_isIntegrationTest) {
integrationTestDevice = await findTargetDevice(); integrationTestDevice = await findTargetDevice();
...@@ -577,6 +591,7 @@ class TestCommand extends FlutterCommand with DeviceBasedDevelopmentArtifacts { ...@@ -577,6 +591,7 @@ class TestCommand extends FlutterCommand with DeviceBasedDevelopmentArtifacts {
testAssetDirectory: testAssetDirectory, testAssetDirectory: testAssetDirectory,
flutterProject: flutterProject, flutterProject: flutterProject,
web: isWeb, web: isWeb,
useWasm: useWasm,
randomSeed: stringArg('test-randomize-ordering-seed'), randomSeed: stringArg('test-randomize-ordering-seed'),
reporter: stringArg('reporter'), reporter: stringArg('reporter'),
fileReporter: stringArg('file-reporter'), fileReporter: stringArg('file-reporter'),
......
...@@ -37,7 +37,7 @@ import 'flutter_web_goldens.dart'; ...@@ -37,7 +37,7 @@ import 'flutter_web_goldens.dart';
import 'test_compiler.dart'; import 'test_compiler.dart';
import 'test_time_recorder.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 mime.MimeTypeResolver resolver = mime.MimeTypeResolver();
final FileSystem fileSystem = directory.fileSystem; final FileSystem fileSystem = directory.fileSystem;
return (shelf.Request request) async { return (shelf.Request request) async {
...@@ -56,10 +56,16 @@ shelf.Handler createDirectoryHandler(Directory directory) { ...@@ -56,10 +56,16 @@ shelf.Handler createDirectoryHandler(Directory directory) {
return shelf.Response.notFound('Not Found'); return shelf.Response.notFound('Not Found');
} }
final String? contentType = resolver.lookup(file.path); final String? contentType = resolver.lookup(file.path);
final bool needsCrossOriginIsolated = crossOriginIsolated && uriPath.endsWith('.html');
return shelf.Response.ok( return shelf.Response.ok(
file.openRead(), file.openRead(),
headers: <String, String>{ headers: <String, String>{
if (contentType != null) 'Content-Type': contentType if (contentType != null) 'Content-Type': contentType,
if (needsCrossOriginIsolated)
...<String, String>{
'Cross-Origin-Opener-Policy': 'same-origin',
'Cross-Origin-Embedder-Policy': 'require-corp',
},
}, },
); );
}; };
...@@ -74,6 +80,7 @@ class FlutterWebPlatform extends PlatformPlugin { ...@@ -74,6 +80,7 @@ class FlutterWebPlatform extends PlatformPlugin {
required this.buildInfo, required this.buildInfo,
required this.webMemoryFS, required this.webMemoryFS,
required FileSystem fileSystem, required FileSystem fileSystem,
required Directory buildDirectory,
required File testDartJs, required File testDartJs,
required File testHostDartJs, required File testHostDartJs,
required ChromiumLauncher chromiumLauncher, required ChromiumLauncher chromiumLauncher,
...@@ -81,8 +88,10 @@ class FlutterWebPlatform extends PlatformPlugin { ...@@ -81,8 +88,10 @@ class FlutterWebPlatform extends PlatformPlugin {
required Artifacts? artifacts, required Artifacts? artifacts,
required ProcessManager processManager, required ProcessManager processManager,
required this.webRenderer, required this.webRenderer,
required this.useWasm,
TestTimeRecorder? testTimeRecorder, TestTimeRecorder? testTimeRecorder,
}) : _fileSystem = fileSystem, }) : _fileSystem = fileSystem,
_buildDirectory = buildDirectory,
_testDartJs = testDartJs, _testDartJs = testDartJs,
_testHostDartJs = testHostDartJs, _testHostDartJs = testHostDartJs,
_chromiumLauncher = chromiumLauncher, _chromiumLauncher = chromiumLauncher,
...@@ -92,6 +101,7 @@ class FlutterWebPlatform extends PlatformPlugin { ...@@ -92,6 +101,7 @@ class FlutterWebPlatform extends PlatformPlugin {
.add(_webSocketHandler.handler) .add(_webSocketHandler.handler)
.add(createDirectoryHandler( .add(createDirectoryHandler(
fileSystem.directory(fileSystem.path.join(Cache.flutterRoot!, 'packages', 'flutter_tools')), fileSystem.directory(fileSystem.path.join(Cache.flutterRoot!, 'packages', 'flutter_tools')),
crossOriginIsolated: webRenderer == WebRendererMode.skwasm,
)) ))
.add(_handleStaticArtifact) .add(_handleStaticArtifact)
.add(_localCanvasKitHandler) .add(_localCanvasKitHandler)
...@@ -99,7 +109,8 @@ class FlutterWebPlatform extends PlatformPlugin { ...@@ -99,7 +109,8 @@ class FlutterWebPlatform extends PlatformPlugin {
.add(_wrapperHandler) .add(_wrapperHandler)
.add(_handleTestRequest) .add(_handleTestRequest)
.add(createDirectoryHandler( .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); .add(_packageFilesHandler);
_server.mount(cascade.handler); _server.mount(cascade.handler);
...@@ -116,6 +127,7 @@ class FlutterWebPlatform extends PlatformPlugin { ...@@ -116,6 +127,7 @@ class FlutterWebPlatform extends PlatformPlugin {
final WebMemoryFS webMemoryFS; final WebMemoryFS webMemoryFS;
final BuildInfo buildInfo; final BuildInfo buildInfo;
final FileSystem _fileSystem; final FileSystem _fileSystem;
final Directory _buildDirectory;
final File _testDartJs; final File _testDartJs;
final File _testHostDartJs; final File _testHostDartJs;
final ChromiumLauncher _chromiumLauncher; final ChromiumLauncher _chromiumLauncher;
...@@ -127,6 +139,7 @@ class FlutterWebPlatform extends PlatformPlugin { ...@@ -127,6 +139,7 @@ class FlutterWebPlatform extends PlatformPlugin {
final AsyncMemoizer<void> _closeMemo = AsyncMemoizer<void>(); final AsyncMemoizer<void> _closeMemo = AsyncMemoizer<void>();
final String _root; final String _root;
final WebRendererMode webRenderer; final WebRendererMode webRenderer;
final bool useWasm;
/// Allows only one test suite (typically one test file) to be loaded and run /// 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 /// at any given point in time. Loading more than one file at a time is known
...@@ -149,11 +162,13 @@ class FlutterWebPlatform extends PlatformPlugin { ...@@ -149,11 +162,13 @@ class FlutterWebPlatform extends PlatformPlugin {
required BuildInfo buildInfo, required BuildInfo buildInfo,
required WebMemoryFS webMemoryFS, required WebMemoryFS webMemoryFS,
required FileSystem fileSystem, required FileSystem fileSystem,
required Directory buildDirectory,
required Logger logger, required Logger logger,
required ChromiumLauncher chromiumLauncher, required ChromiumLauncher chromiumLauncher,
required Artifacts? artifacts, required Artifacts? artifacts,
required ProcessManager processManager, required ProcessManager processManager,
required WebRendererMode webRenderer, required WebRendererMode webRenderer,
required bool useWasm,
TestTimeRecorder? testTimeRecorder, TestTimeRecorder? testTimeRecorder,
Uri? testPackageUri, Uri? testPackageUri,
Future<shelf.Server> Function() serverFactory = defaultServerFactory, Future<shelf.Server> Function() serverFactory = defaultServerFactory,
...@@ -196,12 +211,14 @@ class FlutterWebPlatform extends PlatformPlugin { ...@@ -196,12 +211,14 @@ class FlutterWebPlatform extends PlatformPlugin {
testDartJs: testDartJs, testDartJs: testDartJs,
testHostDartJs: testHostDartJs, testHostDartJs: testHostDartJs,
fileSystem: fileSystem, fileSystem: fileSystem,
buildDirectory: buildDirectory,
chromiumLauncher: chromiumLauncher, chromiumLauncher: chromiumLauncher,
artifacts: artifacts, artifacts: artifacts,
logger: logger, logger: logger,
nullAssertions: nullAssertions, nullAssertions: nullAssertions,
processManager: processManager, processManager: processManager,
webRenderer: webRenderer, webRenderer: webRenderer,
useWasm: useWasm,
testTimeRecorder: testTimeRecorder, testTimeRecorder: testTimeRecorder,
); );
} }
...@@ -254,6 +271,11 @@ class FlutterWebPlatform extends PlatformPlugin { ...@@ -254,6 +271,11 @@ class FlutterWebPlatform extends PlatformPlugin {
'dart_stack_trace_mapper.js', 'dart_stack_trace_mapper.js',
)); ));
File get _flutterJs => _fileSystem.file(_fileSystem.path.join(
_artifacts!.getHostArtifact(HostArtifact.flutterJsDirectory).path,
'flutter.js',
));
File get _dartSdk { File get _dartSdk {
final Map<WebRendererMode, Map<NullSafetyMode, HostArtifact>> dartSdkArtifactMap = buildInfo.ddcModuleFormat == DdcModuleFormat.ddc ? kDdcDartSdkJsArtifactMap : kAmdDartSdkJsArtifactMap; final Map<WebRendererMode, Map<NullSafetyMode, HostArtifact>> dartSdkArtifactMap = buildInfo.ddcModuleFormat == DdcModuleFormat.ddc ? kDdcDartSdkJsArtifactMap : kAmdDartSdkJsArtifactMap;
return _fileSystem.file(_artifacts!.getHostArtifact(dartSdkArtifactMap[webRenderer]![_nullSafetyMode]!)); return _fileSystem.file(_artifacts!.getHostArtifact(dartSdkArtifactMap[webRenderer]![_nullSafetyMode]!));
...@@ -277,21 +299,20 @@ class FlutterWebPlatform extends PlatformPlugin { ...@@ -277,21 +299,20 @@ class FlutterWebPlatform extends PlatformPlugin {
} }
Future<shelf.Response> _handleTestRequest(shelf.Request request) async { Future<shelf.Response> _handleTestRequest(shelf.Request request) async {
if (request.url.path.endsWith('.dart.browser_test.dart.js')) { if (request.url.path.endsWith('main.dart.browser_test.dart.js')) {
final String leadingPath = request.url.path.split('.browser_test.dart.js')[0]; return shelf.Response.ok(generateTestBootstrapFileContents(
final String generatedFile = '${_fileSystem.path.split(leadingPath).join('_')}.bootstrap.js'; '/main.dart.bootstrap.js', 'require.js', 'dart_stack_trace_mapper.js'),
return shelf.Response.ok(generateTestBootstrapFileContents('/$generatedFile', 'require.js', 'dart_stack_trace_mapper.js'), headers: <String, String>{ headers: <String, String>{
HttpHeaders.contentTypeHeader: 'text/javascript', HttpHeaders.contentTypeHeader: 'text/javascript',
}); }
);
} }
if (request.url.path.endsWith('.dart.bootstrap.js')) { if (request.url.path.endsWith('main.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';
return shelf.Response.ok(generateMainModule( return shelf.Response.ok(generateMainModule(
nullAssertions: nullAssertions!, nullAssertions: nullAssertions!,
nativeNullAssertions: true, nativeNullAssertions: true,
bootstrapModule: '${_fileSystem.path.basename(leadingPath)}.dart.bootstrap', bootstrapModule: 'main.dart.bootstrap',
entrypoint: '/$generatedFile' entrypoint: '/main.dart.js'
), headers: <String, String>{ ), headers: <String, String>{
HttpHeaders.contentTypeHeader: 'text/javascript', HttpHeaders.contentTypeHeader: 'text/javascript',
}); });
...@@ -349,6 +370,21 @@ class FlutterWebPlatform extends PlatformPlugin { ...@@ -349,6 +370,21 @@ class FlutterWebPlatform extends PlatformPlugin {
_testHostDartJs.openRead(), _testHostDartJs.openRead(),
headers: <String, String>{'Content-Type': 'text/javascript'}, headers: <String, String>{'Content-Type': 'text/javascript'},
); );
} else if (request.requestedUri.path.contains('flutter.js')) {
return shelf.Response.ok(
_flutterJs.openRead(),
headers: <String, String>{'Content-Type': 'text/javascript'},
);
} else if (request.requestedUri.path.contains('main.dart.mjs')) {
return shelf.Response.ok(
_buildDirectory.childFile('main.dart.mjs').openRead(),
headers: <String, String>{'Content-Type': 'text/javascript'},
);
} else if (request.requestedUri.path.contains('main.dart.wasm')) {
return shelf.Response.ok(
_buildDirectory.childFile('main.dart.wasm').openRead(),
headers: <String, String>{'Content-Type': 'application/wasm'},
);
} else { } else {
return shelf.Response.notFound('Not Found'); return shelf.Response.notFound('Not Found');
} }
...@@ -363,7 +399,10 @@ class FlutterWebPlatform extends PlatformPlugin { ...@@ -363,7 +399,10 @@ class FlutterWebPlatform extends PlatformPlugin {
if (fileUri != null) { if (fileUri != null) {
final String dirname = _fileSystem.path.dirname(fileUri.toFilePath()); final String dirname = _fileSystem.path.dirname(fileUri.toFilePath());
final String basename = _fileSystem.path.basename(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( final shelf.Request modifiedRequest = shelf.Request(
request.method, request.method,
request.requestedUri.replace(path: basename), request.requestedUri.replace(path: basename),
...@@ -452,36 +491,66 @@ class FlutterWebPlatform extends PlatformPlugin { ...@@ -452,36 +491,66 @@ class FlutterWebPlatform extends PlatformPlugin {
return shelf.Response.internalServerError(body: error); return shelf.Response.internalServerError(body: error);
} }
final File canvasKitFile = _canvasKitFile(relativePath);
return shelf.Response.ok( return shelf.Response.ok(
_canvasKitFile(relativePath).openRead(), canvasKitFile.openRead(),
headers: <String, Object>{ headers: <String, Object>{
HttpHeaders.contentTypeHeader: contentType, 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. // A handler that serves wrapper files used to bootstrap tests.
shelf.Response _wrapperHandler(shelf.Request request) { shelf.Response _wrapperHandler(shelf.Request request) {
final String path = _fileSystem.path.fromUri(request.url); final String path = _fileSystem.path.fromUri(request.url);
if (path.endsWith('.html')) { if (path.endsWith('.html')) {
final String test = '${_fileSystem.path.withoutExtension(path)}.dart'; final String test = '${_fileSystem.path.withoutExtension(path)}.dart';
final String scriptBase = htmlEscape.convert(_fileSystem.path.basename(test));
final String link = '<link rel="x-dart-test" href="$scriptBase">';
return shelf.Response.ok(''' return shelf.Response.ok('''
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<title>${htmlEscape.convert(test)} Test</title> <title>${htmlEscape.convert(test)} Test</title>
<script src="flutter.js"></script>
<script> <script>
window.flutterConfiguration = { _flutter.buildConfig = {
canvasKitBaseUrl: "/canvaskit/" builds: [
}; ${_makeBuildConfigString()}
]
}
window.testSelector = "$test";
_flutter.loader.load({
config: {
canvasKitBaseUrl: "/canvaskit/",
}
});
</script> </script>
$link
<script src="static/dart.js"></script>
</head> </head>
</html> </html>
''', headers: <String, String>{'Content-Type': 'text/html'}); ''', headers: <String, String>{
'Content-Type': 'text/html',
if (webRenderer == WebRendererMode.skwasm)
...<String, String>{
'Cross-Origin-Opener-Policy': 'same-origin',
'Cross-Origin-Embedder-Policy': 'require-corp',
}
});
} }
return shelf.Response.notFound('Not found.'); return shelf.Response.notFound('Not found.');
} }
......
...@@ -52,6 +52,7 @@ abstract class FlutterTestRunner { ...@@ -52,6 +52,7 @@ abstract class FlutterTestRunner {
String? icudtlPath, String? icudtlPath,
Directory? coverageDirectory, Directory? coverageDirectory,
bool web = false, bool web = false,
bool useWasm = false,
String? randomSeed, String? randomSeed,
String? reporter, String? reporter,
String? fileReporter, String? fileReporter,
...@@ -117,6 +118,7 @@ class _FlutterTestRunnerImpl implements FlutterTestRunner { ...@@ -117,6 +118,7 @@ class _FlutterTestRunnerImpl implements FlutterTestRunner {
String? icudtlPath, String? icudtlPath,
Directory? coverageDirectory, Directory? coverageDirectory,
bool web = false, bool web = false,
bool useWasm = false,
String? randomSeed, String? randomSeed,
String? reporter, String? reporter,
String? fileReporter, String? fileReporter,
...@@ -186,6 +188,7 @@ class _FlutterTestRunnerImpl implements FlutterTestRunner { ...@@ -186,6 +188,7 @@ class _FlutterTestRunnerImpl implements FlutterTestRunner {
testFiles: testFiles.map((Uri uri) => uri.toFilePath()).toList(), testFiles: testFiles.map((Uri uri) => uri.toFilePath()).toList(),
buildInfo: debuggingOptions.buildInfo, buildInfo: debuggingOptions.buildInfo,
webRenderer: debuggingOptions.webRenderer, webRenderer: debuggingOptions.webRenderer,
useWasm: useWasm,
); );
testArgs testArgs
..add('--platform=chrome') ..add('--platform=chrome')
...@@ -205,6 +208,7 @@ class _FlutterTestRunnerImpl implements FlutterTestRunner { ...@@ -205,6 +208,7 @@ class _FlutterTestRunnerImpl implements FlutterTestRunner {
webMemoryFS: result, webMemoryFS: result,
logger: globals.logger, logger: globals.logger,
fileSystem: globals.fs, fileSystem: globals.fs,
buildDirectory: globals.fs.directory(tempBuildDir),
artifacts: globals.artifacts, artifacts: globals.artifacts,
processManager: globals.processManager, processManager: globals.processManager,
chromiumLauncher: ChromiumLauncher( chromiumLauncher: ChromiumLauncher(
...@@ -217,6 +221,7 @@ class _FlutterTestRunnerImpl implements FlutterTestRunner { ...@@ -217,6 +221,7 @@ class _FlutterTestRunnerImpl implements FlutterTestRunner {
), ),
testTimeRecorder: testTimeRecorder, testTimeRecorder: testTimeRecorder,
webRenderer: debuggingOptions.webRenderer, webRenderer: debuggingOptions.webRenderer,
useWasm: useWasm,
); );
}, },
); );
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'package:package_config/package_config.dart'; import 'package:package_config/package_config.dart';
import 'package:package_config/package_config_types.dart';
import 'package:process/process.dart'; import 'package:process/process.dart';
import '../artifacts.dart'; import '../artifacts.dart';
...@@ -11,6 +12,7 @@ import '../base/config.dart'; ...@@ -11,6 +12,7 @@ import '../base/config.dart';
import '../base/file_system.dart'; import '../base/file_system.dart';
import '../base/logger.dart'; import '../base/logger.dart';
import '../base/platform.dart'; import '../base/platform.dart';
import '../base/process.dart';
import '../build_info.dart'; import '../build_info.dart';
import '../bundle.dart'; import '../bundle.dart';
import '../cache.dart'; import '../cache.dart';
...@@ -44,12 +46,74 @@ class WebTestCompiler { ...@@ -44,12 +46,74 @@ class WebTestCompiler {
final ProcessManager _processManager; final ProcessManager _processManager;
final Config _config; final Config _config;
Future<File> _generateTestEntrypoint({
required List<String> testFiles,
required Directory projectDirectory,
required Directory outputDirectory,
required LanguageVersion languageVersion,
}) async {
final List<WebTestInfo> testInfos = testFiles.map((String testFilePath) {
final List<String> 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<WebMemoryFS> initialize({ Future<WebMemoryFS> initialize({
required Directory projectDirectory, required Directory projectDirectory,
required String testOutputDir, required String testOutputDir,
required List<String> testFiles, required List<String> testFiles,
required BuildInfo buildInfo, required BuildInfo buildInfo,
required WebRendererMode webRenderer, 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<WebMemoryFS> _compileJS({
required Directory projectDirectory,
required String testOutputDir,
required List<String> testFiles,
required BuildInfo buildInfo,
required WebRendererMode webRenderer,
}) async { }) async {
LanguageVersion languageVersion = LanguageVersion(2, 8); LanguageVersion languageVersion = LanguageVersion(2, 8);
late final String platformDillName; late final String platformDillName;
...@@ -78,32 +142,12 @@ class WebTestCompiler { ...@@ -78,32 +142,12 @@ class WebTestCompiler {
final Directory outputDirectory = _fileSystem.directory(testOutputDir) final Directory outputDirectory = _fileSystem.directory(testOutputDir)
..createSync(recursive: true); ..createSync(recursive: true);
final List<File> generatedFiles = <File>[]; final File testFile = await _generateTestEntrypoint(
for (final String testFilePath in testFiles) { testFiles: testFiles,
final List<String> relativeTestSegments = _fileSystem.path.split( projectDirectory: projectDirectory,
_fileSystem.path.relative(testFilePath, from: projectDirectory.childDirectory('test').path)); outputDirectory: outputDirectory,
final File generatedFile = _fileSystem.file( languageVersion: languageVersion
_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 String cachedKernelPath = getDefaultCachedKernelPath( final String cachedKernelPath = getDefaultCachedKernelPath(
trackWidgetCreation: buildInfo.trackWidgetCreation, trackWidgetCreation: buildInfo.trackWidgetCreation,
...@@ -139,7 +183,7 @@ class WebTestCompiler { ...@@ -139,7 +183,7 @@ class WebTestCompiler {
); );
final CompilerOutput? output = await residentCompiler.recompile( final CompilerOutput? output = await residentCompiler.recompile(
Uri.parse('org-dartlang-app:///main.dart'), Uri.parse('org-dartlang-app:///${testFile.basename}'),
<Uri>[], <Uri>[],
outputPath: outputDirectory.childFile('out').path, outputPath: outputDirectory.childFile('out').path,
packageConfig: buildInfo.packageConfig, packageConfig: buildInfo.packageConfig,
...@@ -157,7 +201,67 @@ class WebTestCompiler { ...@@ -157,7 +201,67 @@ class WebTestCompiler {
final File manifestFile = outputDirectory.childFile('${output.outputFilename}.json'); final File manifestFile = outputDirectory.childFile('${output.outputFilename}.json');
final File sourcemapFile = outputDirectory.childFile('${output.outputFilename}.map'); final File sourcemapFile = outputDirectory.childFile('${output.outputFilename}.map');
final File metadataFile = outputDirectory.childFile('${output.outputFilename}.metadata'); final File metadataFile = outputDirectory.childFile('${output.outputFilename}.metadata');
return WebMemoryFS() return WebMemoryFS()
..write(codeFile, manifestFile, sourcemapFile, metadataFile); ..write(codeFile, manifestFile, sourcemapFile, metadataFile);
} }
Future<WebMemoryFS> _compileWasm({
required Directory projectDirectory,
required String testOutputDir,
required List<String> 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<String> dartDefines = webRenderer.updateDartDefines(buildInfo.dartDefines);
final File outputWasmFile = outputDirectory.childFile('main.dart.wasm');
final List<String> compilationArgs = <String>[
_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) ...<String>[
'--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();
}
} }
...@@ -387,59 +387,70 @@ define("$bootstrapModule", ["$entrypoint", "dart_sdk"], function(app, dart_sdk) ...@@ -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({ String generateTestEntrypoint({
required String relativeTestPath, required List<WebTestInfo> testInfos,
required String absolutePath,
required String? testConfigPath,
required LanguageVersion languageVersion, required LanguageVersion languageVersion,
}) { }) {
return ''' final List<String> importMainStatements = <String>[];
// @dart = ${languageVersion.major}.${languageVersion.minor} final List<String> importTestConfigStatements = <String>[];
import 'org-dartlang-app:///$relativeTestPath' as test; final List<String> webTestPairs = <String>[];
import 'dart:ui' as ui;
import 'dart:ui_web' as ui_web; for (int index = 0; index < testInfos.length; index++) {
import 'dart:html'; final WebTestInfo testInfo = testInfos[index];
import 'dart:js'; final String entryPointPath = testInfo.entryPoint;
${testConfigPath != null ? "import '${Uri.file(testConfigPath)}' as test_config;" : ""} importMainStatements.add("import 'org-dartlang-app:///${Uri.file(entryPointPath)}' as test_$index show main;");
import 'package:stream_channel/stream_channel.dart';
import 'package:flutter_test/flutter_test.dart'; final String? testConfigPath = testInfo.configFile;
import 'package:test_api/backend.dart'; String? testConfigFunction = 'null';
if (testConfigPath != null) {
Future<void> main() async { importTestConfigStatements.add(
ui_web.debugEmulateFlutterTesterEnvironment = true; "import 'org-dartlang-app:///${Uri.file(testConfigPath)}' as test_config_$index show testExecutable;"
await ui_web.bootstrapEngine(); );
webGoldenComparator = DefaultWebGoldenComparator(Uri.parse('${Uri.file(absolutePath)}')); testConfigFunction = 'test_config_$index.testExecutable';
ui_web.debugOverrideDevicePixelRatio(3.0); }
ui.window.debugPhysicalSizeOverride = const ui.Size(2400, 1800); webTestPairs.add('''
'$entryPointPath': (
internalBootstrapBrowserTest(() { entryPoint: test_$index.main,
return ${testConfigPath != null ? "() => test_config.testExecutable(test.main)" : "test.main"}; entryPointRunner: $testConfigFunction,
}); goldensUri: Uri.parse('${testInfo.goldensUri}'),
),
''');
} }
return '''
// @dart = ${languageVersion.major}.${languageVersion.minor}
void internalBootstrapBrowserTest(Function getMain()) { ${importMainStatements.join('\n')}
var channel = serializeSuite(getMain, hidePrints: false);
postMessageChannel().pipe(channel);
}
StreamChannel serializeSuite(Function getMain(), {bool hidePrints = true}) => RemoteListener.start(getMain, hidePrints: hidePrints); ${importTestConfigStatements.join('\n')}
StreamChannel postMessageChannel() { import 'package:flutter_test/flutter_test.dart';
var controller = StreamChannelController<Object?>(sync: true);
var channel = MessageChannel();
window.parent!.postMessage('port', window.location.origin, [channel.port2]);
var portSubscription = channel.port1.onMessage.listen((message) { Map<String, WebTest> webTestMap = <String, WebTest>{
controller.local.sink.add(message.data); ${webTestPairs.join('\n')}
}); };
controller.local.stream
.listen(channel.port1.postMessage, onDone: portSubscription.cancel);
return controller.foreign; Future<void> main() {
final WebTest? webTest = webTestMap[testSelector];
if (webTest == null) {
throw Exception('Web test for \${testSelector} not found');
} }
return runWebTest(webTest);
}
'''; ''';
} }
......
...@@ -2,9 +2,9 @@ ...@@ -2,9 +2,9 @@
// of your plugin as a separate package, instead of inlining it in the same // of your plugin as a separate package, instead of inlining it in the same
// package as the core of your plugin. // package as the core of your plugin.
// ignore: avoid_web_libraries_in_flutter // ignore: avoid_web_libraries_in_flutter
import 'dart:html' as html show window;
import 'package:flutter_web_plugins/flutter_web_plugins.dart'; import 'package:flutter_web_plugins/flutter_web_plugins.dart';
import 'package:web/web.dart' as web;
import '{{projectName}}_platform_interface.dart'; import '{{projectName}}_platform_interface.dart';
...@@ -20,7 +20,7 @@ class {{pluginDartClass}}Web extends {{pluginDartClass}}Platform { ...@@ -20,7 +20,7 @@ class {{pluginDartClass}}Web extends {{pluginDartClass}}Platform {
/// Returns a [String] containing the version of the platform. /// Returns a [String] containing the version of the platform.
@override @override
Future<String?> getPlatformVersion() async { Future<String?> getPlatformVersion() async {
final version = html.window.navigator.userAgent; final version = web.window.navigator.userAgent;
return version; return version;
} }
} }
...@@ -13,6 +13,7 @@ dependencies: ...@@ -13,6 +13,7 @@ dependencies:
{{#web}} {{#web}}
flutter_web_plugins: flutter_web_plugins:
sdk: flutter sdk: flutter
web: ^0.5.1
{{/web}} {{/web}}
plugin_platform_interface: ^2.0.2 plugin_platform_interface: ^2.0.2
......
...@@ -87,11 +87,13 @@ void main() { ...@@ -87,11 +87,13 @@ void main() {
buildInfo: const BuildInfo(BuildMode.debug, '', treeShakeIcons: false), buildInfo: const BuildInfo(BuildMode.debug, '', treeShakeIcons: false),
webMemoryFS: WebMemoryFS(), webMemoryFS: WebMemoryFS(),
fileSystem: fileSystem, fileSystem: fileSystem,
buildDirectory: fileSystem.directory('build'),
logger: logger, logger: logger,
chromiumLauncher: chromiumLauncher, chromiumLauncher: chromiumLauncher,
artifacts: artifacts, artifacts: artifacts,
processManager: processManager, processManager: processManager,
webRenderer: WebRendererMode.canvaskit, webRenderer: WebRendererMode.canvaskit,
useWasm: false,
serverFactory: () async => server, serverFactory: () async => server,
testPackageUri: Uri.parse('test'), testPackageUri: Uri.parse('test'),
); );
...@@ -134,11 +136,13 @@ void main() { ...@@ -134,11 +136,13 @@ void main() {
), ),
webMemoryFS: WebMemoryFS(), webMemoryFS: WebMemoryFS(),
fileSystem: fileSystem, fileSystem: fileSystem,
buildDirectory: fileSystem.directory('build'),
logger: logger, logger: logger,
chromiumLauncher: chromiumLauncher, chromiumLauncher: chromiumLauncher,
artifacts: artifacts, artifacts: artifacts,
processManager: processManager, processManager: processManager,
webRenderer: WebRendererMode.canvaskit, webRenderer: WebRendererMode.canvaskit,
useWasm: false,
serverFactory: () async => server, serverFactory: () async => server,
testPackageUri: Uri.parse('test'), testPackageUri: Uri.parse('test'),
); );
......
...@@ -1386,6 +1386,7 @@ class FakeFlutterTestRunner implements FlutterTestRunner { ...@@ -1386,6 +1386,7 @@ class FakeFlutterTestRunner implements FlutterTestRunner {
String? icudtlPath, String? icudtlPath,
Directory? coverageDirectory, Directory? coverageDirectory,
bool web = false, bool web = false,
bool useWasm = false,
String? randomSeed, String? randomSeed,
String? reporter, String? reporter,
String? fileReporter, String? fileReporter,
......
...@@ -91,6 +91,7 @@ void main() { ...@@ -91,6 +91,7 @@ void main() {
testFiles: <String>['project/test/fake_test.dart'], testFiles: <String>['project/test/fake_test.dart'],
buildInfo: buildInfo, buildInfo: buildInfo,
webRenderer: WebRendererMode.canvaskit, webRenderer: WebRendererMode.canvaskit,
useWasm: false,
); );
expect(processManager.hasRemainingExpectations, isFalse); expect(processManager.hasRemainingExpectations, isFalse);
......
...@@ -138,37 +138,18 @@ void main() { ...@@ -138,37 +138,18 @@ void main() {
expect(result, contains('el.setAttribute("data-main", \'foo.dart.js\');')); 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( final String result = generateTestEntrypoint(
relativeTestPath: 'relative_path.dart', testInfos: <WebTestInfo>[
absolutePath: 'absolute_path.dart', (entryPoint: 'foo.dart', goldensUri: Uri.parse('foo.dart'), configFile: null),
testConfigPath: null, (entryPoint: 'bar.dart', goldensUri: Uri.parse('bar.dart'), configFile: 'bar_config.dart'),
],
languageVersion: LanguageVersion(2, 8), languageVersion: LanguageVersion(2, 8),
); );
expect(result, isNot(contains('test_config.testExecutable'))); 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'"));
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')"));
}); });
group('Using the DDC module system', () { group('Using the DDC module system', () {
...@@ -299,38 +280,5 @@ void main() { ...@@ -299,38 +280,5 @@ void main() {
expect(result, contains('el.setAttribute("data-main", \'foo.dart.js\');')); 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')"));
});
}); });
} }
...@@ -9,12 +9,11 @@ ...@@ -9,12 +9,11 @@
// gestures. You can also use WidgetTester to find child widgets in the widget // 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. // 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/material.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart'; import 'package:integration_test/integration_test.dart';
import 'package:integration_test_example/main.dart' as app; import 'package:integration_test_example/main.dart' as app;
import 'package:web/web.dart' as web;
void main() { void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized(); IntegrationTestWidgetsFlutterBinding.ensureInitialized();
...@@ -31,7 +30,7 @@ void main() { ...@@ -31,7 +30,7 @@ void main() {
(Widget widget) => (Widget widget) =>
widget is Text && widget is Text &&
widget.data! widget.data!
.startsWith('Platform: ${html.window.navigator.platform}\n'), .startsWith('Platform: ${web.window.navigator.platform}\n'),
), ),
findsOneWidget, findsOneWidget,
); );
......
...@@ -9,12 +9,12 @@ ...@@ -9,12 +9,12 @@
// gestures. You can also use WidgetTester to find child widgets in the widget // 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. // 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/material.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart'; import 'package:integration_test/integration_test.dart';
import 'package:integration_test_example/main.dart' as app; import 'package:integration_test_example/main.dart' as app;
import 'package:web/web.dart' as web;
void main() { void main() {
final IntegrationTestWidgetsFlutterBinding binding = final IntegrationTestWidgetsFlutterBinding binding =
...@@ -47,7 +47,7 @@ void main() { ...@@ -47,7 +47,7 @@ void main() {
(Widget widget) => (Widget widget) =>
widget is Text && widget is Text &&
widget.data! widget.data!
.startsWith('Platform: ${html.window.navigator.platform}\n'), .startsWith('Platform: ${web.window.navigator.platform}\n'),
), ),
findsOneWidget, findsOneWidget,
); );
......
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
import 'package:integration_test/integration_test.dart'; 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() { void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized(); IntegrationTestWidgetsFlutterBinding.ensureInitialized();
......
...@@ -13,7 +13,7 @@ ...@@ -13,7 +13,7 @@
import 'package:integration_test/integration_test.dart'; 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() { void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized(); IntegrationTestWidgetsFlutterBinding.ensureInitialized();
......
...@@ -2,6 +2,6 @@ ...@@ -2,6 +2,6 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // 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(); void main() => startApp();
...@@ -2,9 +2,10 @@ ...@@ -2,9 +2,10 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:html' as html;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:web/web.dart' as web;
// ignore_for_file: public_member_api_docs // ignore_for_file: public_member_api_docs
void startApp() => runApp(const MyWebApp()); void startApp() => runApp(const MyWebApp());
...@@ -26,7 +27,7 @@ class _MyWebAppState extends State<MyWebApp> { ...@@ -26,7 +27,7 @@ class _MyWebAppState extends State<MyWebApp> {
), ),
body: Center( body: Center(
key: const Key('mainapp'), key: const Key('mainapp'),
child: Text('Platform: ${html.window.navigator.platform}\n'), child: Text('Platform: ${web.window.navigator.platform}\n'),
), ),
), ),
); );
......
...@@ -11,6 +11,7 @@ dependencies: ...@@ -11,6 +11,7 @@ dependencies:
sdk: flutter sdk: flutter
cupertino_icons: 1.0.6 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" 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" collection: 1.18.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
...@@ -78,7 +79,6 @@ dev_dependencies: ...@@ -78,7 +79,6 @@ dev_dependencies:
typed_data: 1.3.2 # 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"
vm_service: 14.2.0 # 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" 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" 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" 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" webkit_inspection_protocol: 1.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
......
...@@ -15,7 +15,7 @@ import 'channel.dart'; ...@@ -15,7 +15,7 @@ import 'channel.dart';
/// ///
/// See also: /// See also:
/// ///
/// * `_callback_web.dart`, which has the dart:html implementation /// * `_callback_web.dart`, which has the web implementation
CallbackManager get callbackManager => _singletonCallbackManager; CallbackManager get callbackManager => _singletonCallbackManager;
/// IOCallbackManager singleton. /// IOCallbackManager singleton.
......
...@@ -6,7 +6,7 @@ import 'dart:async'; ...@@ -6,7 +6,7 @@ import 'dart:async';
import '../common.dart'; import '../common.dart';
/// The dart:html implementation of [CallbackManager]. /// The web implementation of [CallbackManager].
/// ///
/// See also: /// See also:
/// ///
......
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
/// ///
/// See also: /// See also:
/// ///
/// * `_extension_web.dart`, which has the dart:html implementation /// * `_extension_web.dart`, which has the web implementation
void registerWebServiceExtension( void registerWebServiceExtension(
Future<Map<String, dynamic>> Function(Map<String, String>) call) { Future<Map<String, dynamic>> Function(Map<String, String>) call) {
throw UnsupportedError('Use registerServiceExtension instead'); throw UnsupportedError('Use registerServiceExtension instead');
......
...@@ -4,9 +4,11 @@ ...@@ -4,9 +4,11 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:html' as html; import 'dart:js_interop';
import 'dart:js'; import 'dart:js_interop_unsafe';
import 'dart:js_util' as js_util;
@JS('window')
external JSObject get _window;
/// The web implementation of [registerWebServiceExtension]. /// The web implementation of [registerWebServiceExtension].
/// ///
...@@ -20,21 +22,25 @@ void registerWebServiceExtension(Future<Map<String, dynamic>> Function(Map<Strin ...@@ -20,21 +22,25 @@ void registerWebServiceExtension(Future<Map<String, dynamic>> Function(Map<Strin
// Define the result variable because packages/flutter_driver/lib/src/driver/web_driver.dart // Define the result variable because packages/flutter_driver/lib/src/driver/web_driver.dart
// checks for this value to become non-null when waiting for the result. If this value is // checks for this value to become non-null when waiting for the result. If this value is
// undefined at the time of the check, WebDriver throws an exception. // undefined at the time of the check, WebDriver throws an exception.
context[r'$flutterDriverResult'] = null; _window.setProperty(r'$flutterDriverResult'.toJS, null);
js_util.setProperty(html.window, r'$flutterDriver', allowInterop((dynamic message) async { _window.setProperty(r'$flutterDriver'.toJS, (JSAny message) {
try { (() async {
final Map<String, dynamic> messageJson = jsonDecode(message as String) as Map<String, dynamic>; try {
final Map<String, String> params = messageJson.cast<String, String>(); final Map<String, dynamic> messageJson = jsonDecode((message as JSString).toDart) as Map<String, dynamic>;
final Map<String, dynamic> result = await callback(params); final Map<String, String> params = messageJson.cast<String, String>();
context[r'$flutterDriverResult'] = json.encode(result); final Map<String, dynamic> result = await callback(params);
} catch (error, stackTrace) { _window.setProperty(r'$flutterDriverResult'.toJS, json.encode(result).toJS);
// Encode the error in the same format the FlutterDriver extension uses. } catch (error, stackTrace) {
// See //packages/flutter_driver/lib/src/extension/extension.dart // Encode the error in the same format the FlutterDriver extension uses.
context[r'$flutterDriverResult'] = json.encode(<String, dynamic>{ // See //packages/flutter_driver/lib/src/extension/extension.dart
'isError': true, _window.setProperty(r'$flutterDriverResult'.toJS,
'response': '$error\n$stackTrace', json.encode(<String, dynamic>{
}); 'isError': true,
} 'response': '$error\n$stackTrace',
})); }).toJS
);
}
})();
}.toJS);
} }
...@@ -2,4 +2,4 @@ ...@@ -2,4 +2,4 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // 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';
...@@ -2,4 +2,4 @@ ...@@ -2,4 +2,4 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // 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';
...@@ -5,19 +5,23 @@ ...@@ -5,19 +5,23 @@
@Tags(<String>['web']) @Tags(<String>['web'])
library; library;
import 'dart:js' as js; import 'dart:js_interop';
import 'dart:js_interop_unsafe';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart'; import 'package:integration_test/integration_test.dart';
@JS('window')
external JSObject get _window;
void main() { void main() {
IntegrationTestWidgetsFlutterBinding(); IntegrationTestWidgetsFlutterBinding();
test('IntegrationTestWidgetsFlutterBinding on the web should register certain global properties', () { test('IntegrationTestWidgetsFlutterBinding on the web should register certain global properties', () {
expect(js.context.hasProperty(r'$flutterDriver'), true); expect(_window.hasProperty(r'$flutterDriver'.toJS).toDart, true);
expect(js.context[r'$flutterDriver'], isNotNull); expect(_window.getProperty(r'$flutterDriver'.toJS), isNotNull);
expect(js.context.hasProperty(r'$flutterDriverResult'), true); expect(_window.hasProperty(r'$flutterDriverResult'.toJS).toDart, true);
expect(js.context[r'$flutterDriverResult'], isNull); expect(_window.getProperty(r'$flutterDriverResult'.toJS), isNull);
}); });
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment