[ci] Run web tests in wasm (unit + integration). (#8111)

Adds CI configuration to run web integration tests (in the master channel) compiled to Wasm.

It also removes the `build-examples` step from web integration tests, in some isolated testing:

| platform tests shard | With build-examples | Without build-examples |
|---|-----|-----|
| 1 | 30m | 21m |
| 2 | 13m | 11m |
| 3 | 17m | 10m |

## Issues

* Fixes https://github.com/flutter/flutter/issues/151664
This commit is contained in:
David Iglesias
2025-01-07 14:12:51 -08:00
committed by GitHub
parent 34d5039681
commit 11a9fa8bf6
26 changed files with 282 additions and 72 deletions

View File

@ -245,6 +245,38 @@ targets:
"PACKAGE_SHARDING": "--shardIndex 1 --shardCount 2"
}
# Wasm unit tests in master
- name: Linux_web web_dart_unit_test_wasm_shard_1 master
bringup: true
recipe: packages/packages
timeout: 60
properties:
add_recipes_cq: "true"
target_file: web_dart_unit_tests_wasm.yaml
channel: master
version_file: flutter_master.version
package_sharding: "--shardIndex 0 --shardCount 2"
env_variables: >-
{
"CHANNEL": "master",
"PACKAGE_SHARDING": "--shardIndex 0 --shardCount 2"
}
- name: Linux_web web_dart_unit_test_wasm_shard_2 master
bringup: true
recipe: packages/packages
timeout: 60
properties:
target_file: web_dart_unit_tests_wasm.yaml
channel: master
version_file: flutter_master.version
package_sharding: "--shardIndex 1 --shardCount 2"
env_variables: >-
{
"CHANNEL": "master",
"PACKAGE_SHARDING": "--shardIndex 1 --shardCount 2"
}
- name: Linux analyze master
recipe: packages/packages
timeout: 30
@ -846,6 +878,7 @@ targets:
"CHANNEL": "stable"
}
# JS integration tests in master
- name: Linux_web web_platform_tests_shard_1 master
recipe: packages/packages
timeout: 60
@ -888,6 +921,53 @@ targets:
"PACKAGE_SHARDING": "--shardIndex 2 --shardCount 3"
}
# Wasm integration tests in master
- name: Linux_web web_platform_tests_wasm_shard_1 master
bringup: true
recipe: packages/packages
timeout: 60
properties:
target_file: web_platform_tests_wasm.yaml
version_file: flutter_master.version
channel: master
package_sharding: "--shardIndex 0 --shardCount 3"
env_variables: >-
{
"CHANNEL": "master",
"PACKAGE_SHARDING": "--shardIndex 0 --shardCount 3"
}
- name: Linux_web web_platform_tests_wasm_shard_2 master
bringup: true
recipe: packages/packages
timeout: 60
properties:
target_file: web_platform_tests_wasm.yaml
version_file: flutter_master.version
channel: master
package_sharding: "--shardIndex 1 --shardCount 3"
env_variables: >-
{
"CHANNEL": "master",
"PACKAGE_SHARDING": "--shardIndex 1 --shardCount 3"
}
- name: Linux_web web_platform_tests_wasm_shard_3 master
bringup: true
recipe: packages/packages
timeout: 60
properties:
target_file: web_platform_tests_wasm.yaml
version_file: flutter_master.version
channel: master
package_sharding: "--shardIndex 2 --shardCount 3"
env_variables: >-
{
"CHANNEL": "master",
"PACKAGE_SHARDING": "--shardIndex 2 --shardCount 3"
}
# JS integration tests in stable
- name: Linux_web web_platform_tests_shard_1 stable
recipe: packages/packages
timeout: 60

View File

@ -4,4 +4,8 @@ tasks:
infra_step: true # Note infra steps failing prevents "always" from running.
- name: Dart unit tests - web
script: .ci/scripts/tool_runner.sh
args: ["dart-test", "--exclude=script/configs/dart_unit_tests_exceptions.yaml", "--platform=chrome"]
args: [
"dart-test",
"--platform=chrome",
"--exclude=script/configs/dart_unit_tests_exceptions.yaml"
]

View File

@ -0,0 +1,13 @@
tasks:
- name: prepare tool
script: .ci/scripts/prepare_tool.sh
infra_step: true # Note infra steps failing prevents "always" from running.
- name: Dart unit tests - web (wasm)
script: .ci/scripts/tool_runner.sh
args: [
"dart-test",
"--platform=chrome",
"--wasm",
"--exclude=script/configs/dart_unit_tests_exceptions.yaml",
"--exclude=script/configs/dart_unit_tests_wasm_exceptions.yaml"
]

View File

@ -6,9 +6,11 @@ tasks:
script: .ci/scripts/tool_runner.sh
args: ["fetch-deps", "--web", "--supporting-target-platforms-only"]
infra_step: true
- name: build examples
script: .ci/scripts/tool_runner.sh
args: ["build-examples", "--web"]
- name: drive examples
script: .ci/scripts/tool_runner.sh
args: ["drive-examples", "--web", "--run-chromedriver", "--exclude=script/configs/exclude_integration_web.yaml"]
args: [
"drive-examples",
"--web",
"--run-chromedriver",
"--exclude=script/configs/exclude_integration_web.yaml",
]

View File

@ -0,0 +1,18 @@
tasks:
- name: prepare tool
script: .ci/scripts/prepare_tool.sh
infra_step: true # Note infra steps failing prevents "always" from running.
- name: download Dart deps
script: .ci/scripts/tool_runner.sh
args: ["fetch-deps", "--web", "--supporting-target-platforms-only"]
infra_step: true
- name: drive examples
script: .ci/scripts/tool_runner.sh
args: [
"drive-examples",
"--web",
"--wasm",
"--run-chromedriver",
"--exclude=script/configs/exclude_integration_web.yaml",
"--exclude=script/configs/exclude_integration_web_wasm.yaml"
]

View File

@ -31,9 +31,6 @@
.pub/
/build/
# Web related
lib/generated_plugin_registrant.dart
# Symbolication related
app.*.symbols

View File

@ -30,8 +30,5 @@
.pub/
/build/
# Web related
lib/generated_plugin_registrant.dart
# Exceptions to above rules.
!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages

View File

@ -205,7 +205,8 @@ Future<void> pumpAdWidget(Widget adUnit, WidgetTester tester) async {
// This extra pump is needed for the platform view to actually render in the DOM.
await tester.pump();
// One more for skwasm.
await tester.pump();
// This extra pump is needed to simulate the async behavior of the adsense JS mock.
await tester.pumpAndSettle();
}

View File

@ -232,11 +232,11 @@ void main() {
visualization.HeatmapLayerOptions()
..data = <gmaps.LatLng>[gmaps.LatLng(0, 0)].toJS;
expect(heatmap.data, hasLength(0));
expect(heatmap.data.array.toDart, hasLength(0));
controller.update(options);
expect(heatmap.data, hasLength(1));
expect(heatmap.data.array.toDart, hasLength(1));
});
group('remove', () {

View File

@ -435,7 +435,7 @@ void main() {
controller.addHeatmaps(heatmaps);
expect(
controller.heatmaps[const HeatmapId('1')]?.heatmap?.data,
controller.heatmaps[const HeatmapId('1')]!.heatmap!.data.array.toDart,
hasLength(0),
);
@ -450,7 +450,7 @@ void main() {
expect(controller.heatmaps.length, 1);
expect(
controller.heatmaps[const HeatmapId('1')]?.heatmap?.data,
controller.heatmaps[const HeatmapId('1')]!.heatmap!.data.array.toDart,
hasLength(1),
);
});
@ -510,7 +510,9 @@ void main() {
controller.heatmaps.values.first.heatmap!;
expect(
heatmap.get('gradient'),
(heatmap.get('gradient')! as JSArray<JSString>)
.toDart
.map((JSString? value) => value!.toDart),
<String>['rgba(250, 186, 218, 0.00)', 'rgba(250, 186, 218, 1.00)'],
);
});

View File

@ -1,6 +1,7 @@
## NEXT
## 2.10.1
* Updates minimum supported SDK version to Flutter 3.22/Dart 3.4.
* Fixes Wasm tests in internal PickedFile implementation.
## 2.10.0

View File

@ -2,7 +2,9 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// TODO(dit): Remove this, https://github.com/flutter/flutter/issues/144286
export 'lost_data.dart';
export 'unsupported.dart'
if (dart.library.html) 'html.dart'
if (dart.library.js_interop) 'html.dart'
if (dart.library.io) 'io.dart';

View File

@ -4,7 +4,7 @@ repository: https://github.com/flutter/packages/tree/main/packages/image_picker/
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+image_picker%22
# NOTE: We strongly prefer non-breaking changes, even at the expense of a
# less-clean API. See https://flutter.dev/go/platform-interface-breaking-changes
version: 2.10.0
version: 2.10.1
environment:
sdk: ^3.4.0
@ -20,6 +20,7 @@ dependencies:
dev_dependencies:
flutter_test:
sdk: flutter
web: ^1.1.0
topics:
- image-picker

View File

@ -6,15 +6,18 @@
library;
import 'dart:convert';
import 'dart:html' as html;
import 'dart:js_interop';
import 'dart:typed_data';
import 'package:flutter_test/flutter_test.dart';
import 'package:image_picker_platform_interface/image_picker_platform_interface.dart';
import 'package:web/web.dart' as web;
const String expectedStringContents = 'Hello, world!';
final List<int> bytes = utf8.encode(expectedStringContents);
final html.File textFile = html.File(<List<int>>[bytes], 'hello.txt');
final String textFileUrl = html.Url.createObjectUrl(textFile);
final Uint8List bytes = utf8.encode(expectedStringContents);
final web.File textFile =
web.File(<JSUint8Array>[bytes.toJS].toJS, 'hello.txt');
final String textFileUrl = web.URL.createObjectURL(textFile);
void main() {
group('Create with an objectUrl', () {

View File

@ -70,7 +70,8 @@ Future<void> _fullyRenderApp(WidgetTester tester) async {
await tester.pumpWidget(const app.MyApp());
// Pump 2 frames so the framework injects the platform view into the DOM.
await tester.pump();
await tester.pump();
// Give the browser some time to perform DOM operations (for Wasm code)
await tester.pump(const Duration(milliseconds: 500));
}
// Calls [_getHtmlElementAt] passing it the center of the widget identified by

View File

@ -13,6 +13,12 @@ import 'tolerant_comparator.dart'
if (dart.library.js_interop) 'tolerant_comparator_web.dart';
import 'utils.dart';
/// A const to tell apart Wasm from JS web.
///
/// This is used below to do comparisons of numbers, where in JS a whole double
/// is serialized as "2", in Wasm (and non-web platforms) it's "2.0".
const bool kIsJS = kIsWeb && !kIsWasm;
void main() {
const LibraryName coreName = LibraryName(<String>['core']);
const LibraryName materialName = LibraryName(<String>['material']);
@ -229,7 +235,7 @@ void main() {
await tester.pumpAndSettle();
expect(eventLog, contains('menu_item {args: second}'));
expect(eventLog,
contains(kIsWeb ? 'dropdown {value: 2}' : 'dropdown {value: 2.0}'));
contains(kIsJS ? 'dropdown {value: 2}' : 'dropdown {value: 2.0}'));
await tester.tapAt(const Offset(20.0, 20.0));
await tester.pump();
@ -682,15 +688,15 @@ void main() {
await _slideToValue(tester, sliderFinder, 20.0);
await tester.pumpAndSettle();
expect(eventLog,
contains(kIsWeb ? 'slider {value: 20}' : 'slider {value: 20.0}'));
contains(kIsJS ? 'slider {value: 20}' : 'slider {value: 20.0}'));
expect(
eventLog,
contains(
kIsWeb ? 'slider.start {value: 0}' : 'slider.start {value: 0.0}'));
kIsJS ? 'slider.start {value: 0}' : 'slider.start {value: 0.0}'));
expect(
eventLog,
contains(
kIsWeb ? 'slider.end {value: 20}' : 'slider.end {value: 20.0}'));
kIsJS ? 'slider.end {value: 20}' : 'slider.end {value: 20.0}'));
});
}

View File

@ -34,6 +34,7 @@ void main() {
));
// Platform view creation happens asynchronously.
await tester.pumpAndSettle();
await tester.pump();
final html.Element anchor = _findSingleAnchor();
expect(anchor.getAttribute('href'), uri.toString());
@ -51,6 +52,7 @@ void main() {
)),
));
await tester.pumpAndSettle();
await tester.pump();
// Check that the same anchor has been updated.
expect(anchor.getAttribute('href'), uri2.toString());
@ -68,6 +70,7 @@ void main() {
)),
));
await tester.pumpAndSettle();
await tester.pump();
// Check that internal route properly prepares using the default
// [UrlStrategy]
@ -102,6 +105,7 @@ void main() {
),
));
await tester.pumpAndSettle();
await tester.pump();
final Size containerSize = tester.getSize(find.byKey(containerKey));
// The Stack widget inserted by the `WebLinkDelegate` shouldn't loosen the
@ -130,6 +134,7 @@ void main() {
));
// Platform view creation happens asynchronously.
await tester.pumpAndSettle();
await tester.pump();
final html.Element anchor = _findSingleAnchor();
expect(anchor.hasAttribute('href'), false);
@ -161,6 +166,7 @@ void main() {
);
await tester.pumpAndSettle();
await tester.pump();
await tester.scrollUntilVisible(
find.text('#${itemCount - 1}'),
@ -213,6 +219,7 @@ void main() {
));
// Platform view creation happens asynchronously.
await tester.pumpAndSettle();
await tester.pump();
expect(observer.currentRouteName, '/');
expect(testPlugin.launches, isEmpty);
@ -227,10 +234,6 @@ void main() {
// should be no calls to `launchUrl`.
expect(observer.currentRouteName, '/foobar');
expect(testPlugin.launches, isEmpty);
// Needed when testing on on Chrome98 headless in CI.
// See https://github.com/flutter/flutter/issues/121161
await tester.pumpAndSettle();
});
testWidgets('keydown to navigate to internal link',
@ -258,6 +261,7 @@ void main() {
));
// Platform view creation happens asynchronously.
await tester.pumpAndSettle();
await tester.pump();
expect(observer.currentRouteName, '/');
expect(testPlugin.launches, isEmpty);
@ -272,10 +276,6 @@ void main() {
// should be no calls to `launchUrl`.
expect(observer.currentRouteName, '/foobar');
expect(testPlugin.launches, isEmpty);
// Needed when testing on on Chrome98 headless in CI.
// See https://github.com/flutter/flutter/issues/121161
await tester.pumpAndSettle();
});
testWidgets('click to navigate to external link',
@ -300,6 +300,7 @@ void main() {
));
// Platform view creation happens asynchronously.
await tester.pumpAndSettle();
await tester.pump();
expect(observer.currentRouteName, '/');
expect(testPlugin.launches, isEmpty);
@ -315,10 +316,6 @@ void main() {
// no calls to `launchUrl`.
expect(observer.currentRouteName, '/');
expect(testPlugin.launches, isEmpty);
// Needed when testing on on Chrome98 headless in CI.
// See https://github.com/flutter/flutter/issues/121161
await tester.pumpAndSettle();
});
testWidgets('keydown to navigate to external link',
@ -343,6 +340,7 @@ void main() {
));
// Platform view creation happens asynchronously.
await tester.pumpAndSettle();
await tester.pump();
expect(observer.currentRouteName, '/');
expect(testPlugin.launches, isEmpty);
@ -357,17 +355,13 @@ void main() {
// `launchUrl`, and there's no change to the app's route name.
expect(observer.currentRouteName, '/');
expect(testPlugin.launches, <String>['https://google.com']);
// Needed when testing on on Chrome98 headless in CI.
// See https://github.com/flutter/flutter/issues/121161
await tester.pumpAndSettle();
});
});
}
html.Element _findSingleAnchor() {
final List<html.Element> foundAnchors = <html.Element>[];
html.NodeList anchors = html.document.querySelectorAll('a');
final html.NodeList anchors = html.document.querySelectorAll('a');
for (int i = 0; i < anchors.length; i++) {
final html.Element anchor = anchors.item(i)! as html.Element;
if (anchor.hasProperty(linkViewIdProperty.toJS).toDart) {
@ -375,27 +369,24 @@ html.Element _findSingleAnchor() {
}
}
// Search inside the shadow DOM as well.
final html.ShadowRoot? shadowRoot =
html.document.querySelector('flt-glass-pane')?.shadowRoot;
if (shadowRoot != null) {
anchors = shadowRoot.querySelectorAll('a');
for (int i = 0; i < anchors.length; i++) {
final html.Element anchor = anchors.item(i)! as html.Element;
if (anchor.hasProperty(linkViewIdProperty.toJS).toDart) {
foundAnchors.add(anchor);
}
}
}
return foundAnchors.single;
}
void _simulateClick(html.Element target) {
// Stop the browser from navigating away from the test suite.
target.addEventListener(
'click',
(html.Event e) {
e.preventDefault();
}.toJS);
// Synthesize a click event.
target.dispatchEvent(
html.MouseEvent(
'click',
html.MouseEventInit()..bubbles = true,
html.MouseEventInit(
bubbles: true,
cancelable: true,
),
),
);
}
@ -404,7 +395,11 @@ void _simulateKeydown(html.Element target) {
target.dispatchEvent(
html.KeyboardEvent(
'keydown',
html.KeyboardEventInit()..bubbles = true,
html.KeyboardEventInit(
bubbles: true,
cancelable: true,
code: 'Space',
),
),
);
}

View File

@ -1,3 +1,7 @@
## 1.1.13
* Works around a subtle Wasm bug in `writeRadialGradient`.
## 1.1.12
* Transfers the package source from https://github.com/dnfield/vector_graphics

View File

@ -394,10 +394,10 @@ class VectorGraphicsCodec {
buffer._putFloat32(centerY);
buffer._putFloat32(radius);
if (focalX != null) {
if (focalX != null && focalY != null) {
buffer._putUint8(1);
buffer._putFloat32(focalX);
buffer._putFloat32(focalY!);
buffer._putFloat32(focalY);
} else {
buffer._putUint8(0);
}
@ -710,7 +710,7 @@ class VectorGraphicsCodec {
return id;
}
/// Write a new path to the [buffer], returing the identifier
/// Write a new path to the [buffer], returning the identifier
/// assigned to it.
///
/// The [fillType] argument is either `1` for a fill or `0` for a stroke.

View File

@ -2,7 +2,7 @@ name: vector_graphics_codec
description: An encoding library for the binary format used in `package:vector_graphics`
repository: https://github.com/flutter/packages/tree/main/packages/vector_graphics_codec
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+vector_graphics%22
version: 1.1.12
version: 1.1.13
environment:
sdk: ^3.4.0

View File

@ -199,6 +199,10 @@ void main() {
expect(controller.value.position,
lessThanOrEqualTo(controller.value.duration));
},
// Flaky on the web, headless browsers don't like to seek to non-buffered
// positions of a video (and since this isn't even injecting the video
// element on the page, the video never starts buffering with the test)
skip: kIsWeb,
);
testWidgets('test video player view with local asset',

View File

@ -0,0 +1,10 @@
# Packages that are excluded from dart unit tests (compiled to Wasm)
#
# This list should be kept as short as possible, and things should remain here
# only as long as necessary, since in general all web packages should work with
# Wasm.
#
# This is only used for the rare case where a package fails in Wasm, but works
# in JS mode.
[] # Needed so the contents of this file are an empty array, not `null`!

View File

@ -1,8 +1,8 @@
# This list should be kept as short as possible, and things should remain here
# only as long as necessary, since in general the goal is for all of the latest
# versions of packages to be mutually compatible, and compilable with Wasm.
# This is only used for wasm compilation. Once all packages in the repo have
# been migrated, remove this file and use `exclude_all_packages_app.yaml` only.
#
# This is only used for the rare case where a package fails in Wasm, but works
# in JS mode.
[] # Needed so the contents of this file are an empty array, not `null`!

View File

@ -0,0 +1,8 @@
# This list should be kept as short as possible, and things should remain here
# only as long as necessary, since in general all web packages should work with
# Wasm.
#
# This is only used for the rare case where a package fails in Wasm, but works
# in JS mode.
[] # Needed so the contents of this file are an empty array, not `null`!

View File

@ -41,6 +41,8 @@ class DartTestCommand extends PackageLoopingCommand {
help: 'Runs tests on the given platform instead of the default platform '
'("vm" in most cases, "chrome" for web plugin implementations).',
);
argParser.addFlag(kWebWasmFlag,
help: 'Compile to WebAssembly rather than JavaScript');
}
static const String _platformFlag = 'platform';
@ -108,18 +110,21 @@ class DartTestCommand extends PackageLoopingCommand {
platform = 'chrome';
}
// Whether to run web tests compiled to wasm.
final bool wasm = platform != 'vm' && getBoolArg(kWebWasmFlag);
bool passed;
if (package.requiresFlutter()) {
passed = await _runFlutterTests(package, platform: platform);
passed = await _runFlutterTests(package, platform: platform, wasm: wasm);
} else {
passed = await _runDartTests(package, platform: platform);
passed = await _runDartTests(package, platform: platform, wasm: wasm);
}
return passed ? PackageResult.success() : PackageResult.fail();
}
/// Runs the Dart tests for a Flutter package, returning true on success.
Future<bool> _runFlutterTests(RepositoryPackage package,
{String? platform}) async {
{String? platform, bool wasm = false}) async {
final String experiment = getStringArg(kEnableExperiment);
final int exitCode = await processRunner.runAndStream(
@ -131,6 +136,7 @@ class DartTestCommand extends PackageLoopingCommand {
// Flutter defaults to VM mode (under a different name) and explicitly
// setting it is deprecated, so pass nothing in that case.
if (platform != null && platform != 'vm') '--platform=$platform',
if (wasm) '--wasm',
],
workingDir: package.directory,
);
@ -139,7 +145,7 @@ class DartTestCommand extends PackageLoopingCommand {
/// Runs the Dart tests for a non-Flutter package, returning true on success.
Future<bool> _runDartTests(RepositoryPackage package,
{String? platform}) async {
{String? platform, bool wasm = false}) async {
// Unlike `flutter test`, `dart run test` does not automatically get
// packages
if (!await runPubGet(package, processRunner, super.platform)) {
@ -156,6 +162,7 @@ class DartTestCommand extends PackageLoopingCommand {
if (experiment.isNotEmpty) '--enable-experiment=$experiment',
'test',
if (platform != null) '--platform=$platform',
if (wasm) '--compiler=dart2wasm',
],
workingDir: package.directory,
);

View File

@ -337,6 +337,33 @@ test_on: vm && browser
);
});
test('runs in Chrome (wasm) when requested for Flutter package', () async {
final RepositoryPackage package = createFakePackage(
'a_package',
packagesDir,
isFlutter: true,
extraFiles: <String>['test/empty_test.dart'],
);
await runCapturingPrint(
runner, <String>['dart-test', '--platform=chrome', '--wasm']);
expect(
processRunner.recordedCalls,
orderedEquals(<ProcessCall>[
ProcessCall(
getFlutterCommand(mockPlatform),
const <String>[
'test',
'--color',
'--platform=chrome',
'--wasm',
],
package.path),
]),
);
});
test('runs in Chrome by default for Flutter plugins that implement web',
() async {
final RepositoryPackage plugin = createFakePlugin(
@ -517,6 +544,33 @@ test_on: vm && browser
);
});
test('runs in Chrome (wasm) when requested for Dart package', () async {
final RepositoryPackage package = createFakePackage(
'package',
packagesDir,
extraFiles: <String>['test/empty_test.dart'],
);
await runCapturingPrint(
runner, <String>['dart-test', '--platform=chrome', '--wasm']);
expect(
processRunner.recordedCalls,
orderedEquals(<ProcessCall>[
ProcessCall('dart', const <String>['pub', 'get'], package.path),
ProcessCall(
'dart',
const <String>[
'run',
'test',
'--platform=chrome',
'--compiler=dart2wasm',
],
package.path),
]),
);
});
test('skips running in browser mode if package opts out', () async {
final RepositoryPackage package = createFakePackage(
'a_package',