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