Skip to content

Commit 4db845f

Browse files
author
Jonah Williams
authored
Add capability to flutter test --platform=chrome (#33525)
1 parent 56940b5 commit 4db845f

10 files changed

Lines changed: 613 additions & 12 deletions

File tree

packages/flutter_tools/build.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
targets:
22
$default:
3+
builders:
4+
build_web_compilers|entrypoint:
5+
enabled: false
36
sources:
47
exclude:
58
- "test/data/**"

packages/flutter_tools/lib/src/build_runner/web_compilation_delegate.dart

Lines changed: 153 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,21 @@ import 'package:build_modules/src/platform.dart';
1212
import 'package:build_runner_core/build_runner_core.dart' as core;
1313
import 'package:build_runner_core/src/generate/build_impl.dart';
1414
import 'package:build_runner_core/src/generate/options.dart';
15+
import 'package:build_test/builder.dart';
16+
import 'package:build_test/src/debug_test_builder.dart';
1517
import 'package:build_web_compilers/build_web_compilers.dart';
1618
import 'package:build_web_compilers/builders.dart';
1719
import 'package:build_web_compilers/src/dev_compiler_bootstrap.dart';
1820
import 'package:logging/logging.dart';
1921
import 'package:meta/meta.dart';
2022
import 'package:path/path.dart' as path;
23+
import 'package:test_core/backend.dart';
2124
import 'package:watcher/watcher.dart';
2225

2326
import '../artifacts.dart';
2427
import '../base/file_system.dart';
2528
import '../base/logger.dart';
29+
import '../base/platform.dart';
2630
import '../compile.dart';
2731
import '../dart/package_map.dart';
2832
import '../globals.dart';
@@ -65,6 +69,20 @@ final DartPlatform flutterWebPlatform =
6569

6670
/// The build application to compile a flutter application to the web.
6771
final List<core.BuilderApplication> builders = <core.BuilderApplication>[
72+
core.apply(
73+
'flutter_tools|test_bootstrap',
74+
<BuilderFactory>[
75+
(BuilderOptions options) => const DebugTestBuilder(),
76+
(BuilderOptions options) => const FlutterWebTestBootstrapBuilder(),
77+
],
78+
core.toRoot(),
79+
hideOutput: true,
80+
defaultGenerateFor: const InputSet(
81+
include: <String>[
82+
'test/**',
83+
],
84+
),
85+
),
6886
core.apply(
6987
'flutter_tools|module_library',
7088
<Builder Function(BuilderOptions)>[moduleLibraryBuilder],
@@ -109,14 +127,15 @@ final List<core.BuilderApplication> builders = <core.BuilderApplication>[
109127
'flutter_tools|entrypoint',
110128
<BuilderFactory>[
111129
(BuilderOptions options) => FlutterWebEntrypointBuilder(
112-
options.config['target'] ?? 'lib/main.dart'),
130+
options.config['targets'] ?? <String>['lib/main.dart']),
113131
],
114132
core.toRoot(),
115133
hideOutput: true,
116134
defaultGenerateFor: const InputSet(
117135
include: <String>[
118136
'lib/**',
119137
'web/**',
138+
'test/**_test.dart.browser_test.dart',
120139
],
121140
),
122141
),
@@ -135,13 +154,14 @@ class BuildRunnerWebCompilationProxy extends WebCompilationProxy {
135154
@override
136155
Future<void> initialize({
137156
@required Directory projectDirectory,
138-
@required String target,
157+
@required List<String> targets,
158+
String testOutputDir,
139159
}) async {
140160
// Override the generated output directory so this does not conflict with
141161
// other build_runner output.
142162
core.overrideGeneratedOutputDirectory('flutter_web');
143163
_packageUriMapper = PackageUriMapper(
144-
path.absolute(target), PackageMap.globalPackagesPath, null, null);
164+
path.absolute('lib/main.dart'), PackageMap.globalPackagesPath, null, null);
145165
_packageGraph = core.PackageGraph.forPath(projectDirectory.path);
146166
final core.BuildEnvironment buildEnvironment = core.OverrideableEnvironment(
147167
core.IOEnvironment(_packageGraph), onLog: (LogRecord record) {
@@ -163,21 +183,31 @@ class BuildRunnerWebCompilationProxy extends WebCompilationProxy {
163183
trackPerformance: false,
164184
deleteFilesByDefault: true,
165185
);
186+
final Set<core.BuildDirectory> buildDirs = <core.BuildDirectory>{
187+
if (testOutputDir != null)
188+
core.BuildDirectory(
189+
'test',
190+
outputLocation: core.OutputLocation(
191+
testOutputDir,
192+
useSymlinks: !platform.isWindows,
193+
),
194+
),
195+
};
166196
final Status status =
167-
logger.startProgress('Compiling $target for the Web...', timeout: null);
197+
logger.startProgress('Compiling ${targets.first} for the Web...', timeout: null);
168198
try {
169199
_builder = await BuildImpl.create(
170200
buildOptions,
171201
buildEnvironment,
172202
builders,
173203
<String, Map<String, dynamic>>{
174204
'flutter_tools|entrypoint': <String, dynamic>{
175-
'target': target,
205+
'targets': targets,
176206
}
177207
},
178208
isReleaseBuild: false,
179209
);
180-
await _builder.run(const <AssetId, ChangeType>{});
210+
await _builder.run(const <AssetId, ChangeType>{}, buildDirs: buildDirs);
181211
} finally {
182212
status.stop();
183213
}
@@ -205,9 +235,9 @@ class BuildRunnerWebCompilationProxy extends WebCompilationProxy {
205235

206236
/// A ddc-only entrypoint builder that respects the Flutter target flag.
207237
class FlutterWebEntrypointBuilder implements Builder {
208-
const FlutterWebEntrypointBuilder(this.target);
238+
const FlutterWebEntrypointBuilder(this.targets);
209239

210-
final String target;
240+
final List<String> targets;
211241

212242
@override
213243
Map<String, List<String>> get buildExtensions => const <String, List<String>>{
@@ -222,10 +252,124 @@ class FlutterWebEntrypointBuilder implements Builder {
222252

223253
@override
224254
Future<void> build(BuildStep buildStep) async {
225-
if (!buildStep.inputId.path.contains(target)) {
255+
bool matches = false;
256+
for (String target in targets) {
257+
if (buildStep.inputId.path.contains(target)) {
258+
matches = true;
259+
break;
260+
}
261+
}
262+
if (!matches) {
226263
return;
227264
}
228265
log.info('building for target ${buildStep.inputId.path}');
229266
await bootstrapDdc(buildStep, platform: flutterWebPlatform);
230267
}
231268
}
269+
270+
class FlutterWebTestBootstrapBuilder implements Builder {
271+
const FlutterWebTestBootstrapBuilder();
272+
273+
@override
274+
Map<String, List<String>> get buildExtensions => const <String, List<String>>{
275+
'_test.dart': <String>[
276+
'_test.dart.browser_test.dart',
277+
]
278+
};
279+
280+
@override
281+
Future<void> build(BuildStep buildStep) async {
282+
final AssetId id = buildStep.inputId;
283+
final String contents = await buildStep.readAsString(id);
284+
final String assetPath = id.pathSegments.first == 'lib'
285+
? path.url.join('packages', id.package, id.path)
286+
: id.path;
287+
final Metadata metadata = parseMetadata(
288+
assetPath, contents, Runtime.builtIn.map((Runtime runtime) => runtime.name).toSet());
289+
290+
if (metadata.testOn.evaluate(SuitePlatform(Runtime.chrome))) {
291+
await buildStep.writeAsString(id.addExtension('.browser_test.dart'), '''
292+
import 'dart:ui' as ui;
293+
import 'dart:html';
294+
import 'dart:js';
295+
296+
import 'package:stream_channel/stream_channel.dart';
297+
import 'package:test_api/src/backend/stack_trace_formatter.dart'; // ignore: implementation_imports
298+
import 'package:test_api/src/util/stack_trace_mapper.dart'; // ignore: implementation_imports
299+
import 'package:test_api/src/remote_listener.dart'; // ignore: implementation_imports
300+
import 'package:test_api/src/suite_channel_manager.dart'; // ignore: implementation_imports
301+
302+
import "${path.url.basename(id.path)}" as test;
303+
304+
Future<void> main() async {
305+
// Extra initialization for flutter_web.
306+
// The following parameters are hard-coded in Flutter's test embedder. Since
307+
// we don't have an embedder yet this is the lowest-most layer we can put
308+
// this stuff in.
309+
await ui.webOnlyTestSetup();
310+
internalBootstrapBrowserTest(() => test.main);
311+
}
312+
313+
void internalBootstrapBrowserTest(Function getMain()) {
314+
var channel =
315+
serializeSuite(getMain, hidePrints: false, beforeLoad: () async {
316+
var serialized =
317+
await suiteChannel("test.browser.mapper").stream.first as Map;
318+
if (serialized == null) return;
319+
});
320+
postMessageChannel().pipe(channel);
321+
}
322+
StreamChannel serializeSuite(Function getMain(),
323+
{bool hidePrints = true, Future beforeLoad()}) =>
324+
RemoteListener.start(getMain,
325+
hidePrints: hidePrints, beforeLoad: beforeLoad);
326+
327+
StreamChannel suiteChannel(String name) {
328+
var manager = SuiteChannelManager.current;
329+
if (manager == null) {
330+
throw StateError('suiteChannel() may only be called within a test worker.');
331+
}
332+
333+
return manager.connectOut(name);
334+
}
335+
336+
StreamChannel postMessageChannel() {
337+
var controller = StreamChannelController(sync: true);
338+
window.onMessage.firstWhere((message) {
339+
return message.origin == window.location.origin && message.data == "port";
340+
}).then((message) {
341+
var port = message.ports.first;
342+
var portSubscription = port.onMessage.listen((message) {
343+
controller.local.sink.add(message.data);
344+
});
345+
346+
controller.local.stream.listen((data) {
347+
port.postMessage({"data": data});
348+
}, onDone: () {
349+
port.postMessage({"event": "done"});
350+
portSubscription.cancel();
351+
});
352+
});
353+
354+
context['parent'].callMethod('postMessage', [
355+
JsObject.jsify({"href": window.location.href, "ready": true}),
356+
window.location.origin,
357+
]);
358+
return controller.foreign;
359+
}
360+
361+
void setStackTraceMap
362+
per(StackTraceMapper mapper) {
363+
var formatter = StackTraceFormatter.current;
364+
if (formatter == null) {
365+
throw StateError(
366+
'setStackTraceMapper() may only be called within a test worker.');
367+
}
368+
369+
formatter.configure(mapper: mapper);
370+
}
371+
''');
372+
}
373+
}
374+
}
375+

packages/flutter_tools/lib/src/commands/test.dart

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,11 @@ class TestCommand extends FastFlutterCommand {
9999
negatable: true,
100100
help: 'Whether to build the assets bundle for testing.\n'
101101
'Consider using --no-test-assets if assets are not required.',
102+
)
103+
..addOption('platform',
104+
allowed: const <String>['tester', 'chrome'],
105+
defaultsTo: 'tester',
106+
help: 'The platform to run the unit tests on. Defaults to "tester".'
102107
);
103108
}
104109

@@ -166,6 +171,16 @@ class TestCommand extends FastFlutterCommand {
166171
'Test files must be in that directory and end with the pattern "_test.dart".'
167172
);
168173
}
174+
} else {
175+
final List<String> fileCopy = <String>[];
176+
for (String file in files) {
177+
if (file.endsWith(platform.pathSeparator)) {
178+
fileCopy.addAll(_findTests(fs.directory(file)));
179+
} else {
180+
fileCopy.add(file);
181+
}
182+
}
183+
files = fileCopy;
169184
}
170185

171186
CoverageCollector collector;
@@ -222,6 +237,7 @@ class TestCommand extends FastFlutterCommand {
222237
concurrency: jobs,
223238
buildTestAssets: buildTestAssets,
224239
flutterProject: flutterProject,
240+
web: argResults['platform'] == 'chrome',
225241
);
226242

227243
if (collector != null) {

packages/flutter_tools/lib/src/resident_web_runner.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ class ResidentWebRunner extends ResidentRunner {
112112
// Start the web compiler and build the assets.
113113
await webCompilationProxy.initialize(
114114
projectDirectory: currentProject.directory,
115-
target: target,
115+
targets: <String>[target],
116116
);
117117
_lastCompiled = DateTime.now();
118118
final AssetBundle assetBundle = AssetBundleFactory.instance.createBundle();

0 commit comments

Comments
 (0)