Wire dart2js through flutter tool, add compilation test (#27668)

diff --git a/dev/bots/test.dart b/dev/bots/test.dart
index 41dd11e..947d0d02 100644
--- a/dev/bots/test.dart
+++ b/dev/bots/test.dart
@@ -167,10 +167,22 @@
     await _flutterBuildApk(path);
     await _flutterBuildIpa(path);
   }
+  await _flutterBuildDart2js(path.join('dev', 'integration_tests', 'web'));
 
   print('${bold}DONE: All build tests successful.$reset');
 }
 
+Future<void> _flutterBuildDart2js(String relativePathToApplication) async {
+  print('Running Dart2JS build tests...');
+  await runCommand(flutter,
+    <String>['build', 'web', '-v'],
+    workingDirectory: path.join(flutterRoot, relativePathToApplication),
+    expectNonZeroExit: false,
+    timeout: _kShortTimeout,
+  );
+  print('Done.');
+}
+
 Future<void> _flutterBuildAot(String relativePathToApplication) async {
   print('Running AOT build tests...');
   await runCommand(flutter,
diff --git a/dev/integration_tests/web/lib/main.dart b/dev/integration_tests/web/lib/main.dart
new file mode 100644
index 0000000..49c544c
--- /dev/null
+++ b/dev/integration_tests/web/lib/main.dart
@@ -0,0 +1,13 @@
+// Copyright 2019 The Chromium 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 'package:flutter/widgets.dart';
+
+void main() {
+  runApp(Center(
+    // Can remove when https://github.com/dart-lang/sdk/issues/35801 is fixed.
+    // ignore: prefer_const_constructors
+    child: Text('Hello, World', textDirection: TextDirection.ltr),
+  ));
+}
diff --git a/dev/integration_tests/web/pubspec.yaml b/dev/integration_tests/web/pubspec.yaml
new file mode 100644
index 0000000..8749907
--- /dev/null
+++ b/dev/integration_tests/web/pubspec.yaml
@@ -0,0 +1,17 @@
+name: web_integration
+description: Integration test for web compilation.
+
+environment:
+  # The pub client defaults to an <2.0.0 sdk constraint which we need to explicitly overwrite.
+  sdk: ">=2.0.0-dev.68.0 <3.0.0"
+
+dependencies:
+  flutter:
+    sdk: flutter
+
+  collection: 1.14.11 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+  meta: 1.1.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+  typed_data: 1.1.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+  vector_math: 2.0.8 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+
+# PUBSPEC CHECKSUM: d53c
diff --git a/packages/flutter/lib/src/foundation/basic_types.dart b/packages/flutter/lib/src/foundation/basic_types.dart
index 73637cd..2c6c875 100644
--- a/packages/flutter/lib/src/foundation/basic_types.dart
+++ b/packages/flutter/lib/src/foundation/basic_types.dart
@@ -8,6 +8,7 @@
 // COMMON SIGNATURES
 
 export 'dart:ui' show VoidCallback;
+export 'bitfield.dart' if (dart.library.html) 'bitfield_unsupported.dart';
 
 /// Signature for callbacks that report that an underlying value has changed.
 ///
@@ -66,69 +67,6 @@
 ///  * [AsyncValueSetter], the setter equivalent of this signature.
 typedef AsyncValueGetter<T> = Future<T> Function();
 
-
-// BITFIELD
-
-/// The largest SMI value.
-///
-/// See <https://www.dartlang.org/articles/numeric-computation/#smis-and-mints>
-const int kMaxUnsignedSMI = 0x3FFFFFFFFFFFFFFF;
-
-/// A BitField over an enum (or other class whose values implement "index").
-/// Only the first 62 values of the enum can be used as indices.
-class BitField<T extends dynamic> {
-  /// Creates a bit field of all zeros.
-  ///
-  /// The given length must be at most 62.
-  BitField(this._length)
-    : assert(_length <= _smiBits),
-      _bits = _allZeros;
-
-  /// Creates a bit field filled with a particular value.
-  ///
-  /// If the value argument is true, the bits are filled with ones. Otherwise,
-  /// the bits are filled with zeros.
-  ///
-  /// The given length must be at most 62.
-  BitField.filled(this._length, bool value)
-    : assert(_length <= _smiBits),
-      _bits = value ? _allOnes : _allZeros;
-
-  final int _length;
-  int _bits;
-
-  static const int _smiBits = 62; // see https://www.dartlang.org/articles/numeric-computation/#smis-and-mints
-  static const int _allZeros = 0;
-  static const int _allOnes = kMaxUnsignedSMI; // 2^(_kSMIBits+1)-1
-
-  /// Returns whether the bit with the given index is set to one.
-  bool operator [](T index) {
-    assert(index.index < _length);
-    return (_bits & 1 << index.index) > 0;
-  }
-
-  /// Sets the bit with the given index to the given value.
-  ///
-  /// If value is true, the bit with the given index is set to one. Otherwise,
-  /// the bit is set to zero.
-  void operator []=(T index, bool value) {
-    assert(index.index < _length);
-    if (value)
-      _bits = _bits | (1 << index.index);
-    else
-      _bits = _bits & ~(1 << index.index);
-  }
-
-  /// Sets all the bits to the given value.
-  ///
-  /// If the value is true, the bits are all set to one. Otherwise, the bits are
-  /// all set to zero. Defaults to setting all the bits to zero.
-  void reset([ bool value = false ]) {
-    _bits = value ? _allOnes : _allZeros;
-  }
-}
-
-
 // LAZY CACHING ITERATOR
 
 /// A lazy caching version of [Iterable].
diff --git a/packages/flutter/lib/src/foundation/bitfield.dart b/packages/flutter/lib/src/foundation/bitfield.dart
new file mode 100644
index 0000000..5240bc0
--- /dev/null
+++ b/packages/flutter/lib/src/foundation/bitfield.dart
@@ -0,0 +1,67 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/// The largest SMI value.
+///
+/// See <https://www.dartlang.org/articles/numeric-computation/#smis-and-mints>
+///
+/// When compiling to JavaScript, this value is not supported since it is
+/// larger than the maximum safe 32bit integer.
+const int kMaxUnsignedSMI = 0x3FFFFFFFFFFFFFFF;
+
+/// A BitField over an enum (or other class whose values implement "index").
+/// Only the first 62 values of the enum can be used as indices.
+///
+/// When compiling to JavaScript, this class is not supported.
+class BitField<T extends dynamic> {
+  /// Creates a bit field of all zeros.
+  ///
+  /// The given length must be at most 62.
+  BitField(this._length)
+    : assert(_length <= _smiBits),
+      _bits = _allZeros;
+
+  /// Creates a bit field filled with a particular value.
+  ///
+  /// If the value argument is true, the bits are filled with ones. Otherwise,
+  /// the bits are filled with zeros.
+  ///
+  /// The given length must be at most 62.
+  BitField.filled(this._length, bool value)
+    : assert(_length <= _smiBits),
+      _bits = value ? _allOnes : _allZeros;
+
+  final int _length;
+  int _bits;
+
+  static const int _smiBits = 62; // see https://www.dartlang.org/articles/numeric-computation/#smis-and-mints
+  static const int _allZeros = 0;
+  static const int _allOnes = kMaxUnsignedSMI; // 2^(_kSMIBits+1)-1
+
+  /// Returns whether the bit with the given index is set to one.
+  bool operator [](T index) {
+    assert(index.index < _length);
+    return (_bits & 1 << index.index) > 0;
+  }
+
+  /// Sets the bit with the given index to the given value.
+  ///
+  /// If value is true, the bit with the given index is set to one. Otherwise,
+  /// the bit is set to zero.
+  void operator []=(T index, bool value) {
+    assert(index.index < _length);
+    if (value)
+      _bits = _bits | (1 << index.index);
+    else
+      _bits = _bits & ~(1 << index.index);
+  }
+
+  /// Sets all the bits to the given value.
+  ///
+  /// If the value is true, the bits are all set to one. Otherwise, the bits are
+  /// all set to zero. Defaults to setting all the bits to zero.
+  void reset([ bool value = false ]) {
+    _bits = value ? _allOnes : _allZeros;
+  }
+}
\ No newline at end of file
diff --git a/packages/flutter/lib/src/foundation/bitfield_unsupported.dart b/packages/flutter/lib/src/foundation/bitfield_unsupported.dart
new file mode 100644
index 0000000..c7baf47
--- /dev/null
+++ b/packages/flutter/lib/src/foundation/bitfield_unsupported.dart
@@ -0,0 +1,34 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/// Unsupported.
+const int kMaxUnsignedSMI = 0;
+
+/// Unsupported.
+class BitField<T extends dynamic> {
+  /// Unsupported.
+  // Ignored so that both bitfield implementations have the same API.
+  // ignore: avoid_unused_constructor_parameters
+  BitField(int length);
+
+  /// Unsupported.
+  // Ignored so that both bitfield implementations have the same API.
+  // ignore: avoid_unused_constructor_parameters
+  BitField.filled(int length, bool value);
+
+  /// Unsupported.
+  bool operator [](T index) {
+    throw UnsupportedError('Not supported when compiling to JavaScript');
+  }
+
+  /// Unsupported.
+  void operator []=(T index, bool value) {
+    throw UnsupportedError('Not supported when compiling to JavaScript');
+  }
+
+  /// Unsupported.
+  void reset([ bool value = false ]) {
+    throw UnsupportedError('Not supported when compiling to JavaScript');
+  }
+}
diff --git a/packages/flutter/lib/src/foundation/unsupported.dart b/packages/flutter/lib/src/foundation/unsupported.dart
new file mode 100644
index 0000000..7ad4207f
--- /dev/null
+++ b/packages/flutter/lib/src/foundation/unsupported.dart
@@ -0,0 +1,7 @@
+/// The largest SMI value.
+///
+/// See <https://www.dartlang.org/articles/numeric-computation/#smis-and-mints>
+///
+/// When compiling to JavaScript, this value is not supported since it is
+/// larger than the maximum safe 32bit integer.
+const int kMaxUnsignedSMI = 0x3FFFFFFFFFFFFFFF;
\ No newline at end of file
diff --git a/packages/flutter/lib/src/foundation/unsupported_web.dart b/packages/flutter/lib/src/foundation/unsupported_web.dart
new file mode 100644
index 0000000..8a23100
--- /dev/null
+++ b/packages/flutter/lib/src/foundation/unsupported_web.dart
@@ -0,0 +1,7 @@
+/// The largest SMI value.
+///
+/// See <https://www.dartlang.org/articles/numeric-computation/#smis-and-mints>
+///
+/// When compiling to JavaScript, this value is not supported since it is
+/// larger than the maximum safe 32bit integer.
+const int kMaxUnsignedSMI = 0;
\ No newline at end of file
diff --git a/packages/flutter_tools/lib/src/application_package.dart b/packages/flutter_tools/lib/src/application_package.dart
index fe8f968..989ca6f 100644
--- a/packages/flutter_tools/lib/src/application_package.dart
+++ b/packages/flutter_tools/lib/src/application_package.dart
@@ -49,6 +49,7 @@
       case TargetPlatform.linux_x64:
       case TargetPlatform.windows_x64:
       case TargetPlatform.fuchsia:
+      case TargetPlatform.web:
         return null;
     }
     assert(platform != null);
@@ -352,6 +353,7 @@
       case TargetPlatform.windows_x64:
       case TargetPlatform.fuchsia:
       case TargetPlatform.tester:
+      case TargetPlatform.web:
         return null;
     }
     return null;
diff --git a/packages/flutter_tools/lib/src/artifacts.dart b/packages/flutter_tools/lib/src/artifacts.dart
index 6cfb8bc..9bf9280 100644
--- a/packages/flutter_tools/lib/src/artifacts.dart
+++ b/packages/flutter_tools/lib/src/artifacts.dart
@@ -26,6 +26,8 @@
   frontendServerSnapshotForEngineDartSdk,
   engineDartSdkPath,
   engineDartBinary,
+  dart2jsSnapshot,
+  kernelWorkerSnapshot,
 }
 
 String _artifactToFileName(Artifact artifact, [TargetPlatform platform, BuildMode mode]) {
@@ -70,6 +72,10 @@
       return 'frontend_server.dart.snapshot';
     case Artifact.engineDartBinary:
       return 'dart';
+    case Artifact.dart2jsSnapshot:
+      return 'flutter_dart2js.dart.snapshot';
+    case Artifact.kernelWorkerSnapshot:
+      return 'flutter_kernel_worker.dart.snapshot';
   }
   assert(false, 'Invalid artifact $artifact.');
   return null;
@@ -121,6 +127,7 @@
       case TargetPlatform.windows_x64:
       case TargetPlatform.fuchsia:
       case TargetPlatform.tester:
+      case TargetPlatform.web:
         return _getHostArtifactPath(artifact, platform, mode);
     }
     assert(false, 'Invalid platform $platform.');
@@ -183,13 +190,17 @@
       case Artifact.engineDartSdkPath:
         return dartSdkPath;
       case Artifact.engineDartBinary:
-        return fs.path.join(dartSdkPath,'bin', _artifactToFileName(artifact));
+        return fs.path.join(dartSdkPath, 'bin', _artifactToFileName(artifact));
       case Artifact.platformKernelDill:
         return fs.path.join(_getFlutterPatchedSdkPath(), _artifactToFileName(artifact));
       case Artifact.platformLibrariesJson:
         return fs.path.join(_getFlutterPatchedSdkPath(), 'lib', _artifactToFileName(artifact));
       case Artifact.flutterPatchedSdkPath:
         return _getFlutterPatchedSdkPath();
+      case Artifact.dart2jsSnapshot:
+        return fs.path.join(dartSdkPath, 'bin', 'snapshots', _artifactToFileName(artifact));
+      case Artifact.kernelWorkerSnapshot:
+        return fs.path.join(dartSdkPath, 'bin', 'snapshots', _artifactToFileName(artifact));
       default:
         assert(false, 'Artifact $artifact not available for platform $platform.');
         return null;
@@ -205,6 +216,7 @@
       case TargetPlatform.windows_x64:
       case TargetPlatform.fuchsia:
       case TargetPlatform.tester:
+      case TargetPlatform.web:
         assert(mode == null, 'Platform $platform does not support different build modes.');
         return fs.path.join(engineDir, platformName);
       case TargetPlatform.ios:
@@ -265,6 +277,10 @@
         return fs.path.join(_hostEngineOutPath, 'dart-sdk');
       case Artifact.engineDartBinary:
         return fs.path.join(_hostEngineOutPath, 'dart-sdk', 'bin', _artifactToFileName(artifact));
+      case Artifact.dart2jsSnapshot:
+        return fs.path.join(_hostEngineOutPath, 'dart-sdk', 'bin', 'snapshots', _artifactToFileName(artifact));
+      case Artifact.kernelWorkerSnapshot:
+        return fs.path.join(_hostEngineOutPath, 'dart-sdk', 'bin', 'snapshots', _artifactToFileName(artifact));
     }
     assert(false, 'Invalid artifact $artifact.');
     return null;
diff --git a/packages/flutter_tools/lib/src/build_info.dart b/packages/flutter_tools/lib/src/build_info.dart
index cd8ce05..2e3322b 100644
--- a/packages/flutter_tools/lib/src/build_info.dart
+++ b/packages/flutter_tools/lib/src/build_info.dart
@@ -266,6 +266,7 @@
   windows_x64,
   fuchsia,
   tester,
+  web,
 }
 
 /// iOS target device architecture.
@@ -325,6 +326,8 @@
       return 'fuchsia';
     case TargetPlatform.tester:
       return 'flutter-tester';
+    case TargetPlatform.web:
+      return 'web';
   }
   assert(false);
   return null;
@@ -346,6 +349,8 @@
       return TargetPlatform.darwin_x64;
     case 'linux-x64':
       return TargetPlatform.linux_x64;
+    case 'web':
+      return TargetPlatform.web;
   }
   assert(platform != null);
   return null;
@@ -400,6 +405,11 @@
   return fs.path.join(getBuildDirectory(), 'ios');
 }
 
+/// Returns the web build output directory.
+String getWebBuildDirectory() {
+  return fs.path.join(getBuildDirectory(), 'web');
+}
+
 /// Returns directory used by incremental compiler (IKG - incremental kernel
 /// generator) to store cached intermediate state.
 String getIncrementalCompilerByteStoreDirectory() {
diff --git a/packages/flutter_tools/lib/src/commands/build.dart b/packages/flutter_tools/lib/src/commands/build.dart
index c31f741..64100e2 100644
--- a/packages/flutter_tools/lib/src/commands/build.dart
+++ b/packages/flutter_tools/lib/src/commands/build.dart
@@ -11,6 +11,7 @@
 import 'build_bundle.dart';
 import 'build_flx.dart';
 import 'build_ios.dart';
+import 'build_web.dart';
 
 class BuildCommand extends FlutterCommand {
   BuildCommand({bool verboseHelp = false}) {
@@ -20,6 +21,7 @@
     addSubcommand(BuildIOSCommand());
     addSubcommand(BuildFlxCommand());
     addSubcommand(BuildBundleCommand(verboseHelp: verboseHelp));
+    addSubcommand(BuildWebCommand());
   }
 
   @override
diff --git a/packages/flutter_tools/lib/src/commands/build_web.dart b/packages/flutter_tools/lib/src/commands/build_web.dart
new file mode 100644
index 0000000..a1a4645
--- /dev/null
+++ b/packages/flutter_tools/lib/src/commands/build_web.dart
@@ -0,0 +1,38 @@
+// Copyright 2019 The Chromium 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 '../base/logger.dart';
+import '../build_info.dart';
+import '../globals.dart';
+import '../runner/flutter_command.dart' show ExitStatus, FlutterCommandResult;
+import '../web/compile.dart';
+import 'build.dart';
+
+class BuildWebCommand extends BuildSubCommand {
+  BuildWebCommand() {
+    usesTargetOption();
+    usesPubOption();
+    defaultBuildMode = BuildMode.release;
+  }
+
+  @override
+  final String name = 'web';
+
+  @override
+  bool get hidden => true;
+
+  @override
+  final String description = '(EXPERIMENTAL) build a web application bundle.';
+
+  @override
+  Future<FlutterCommandResult> runCommand() async {
+    final String target = argResults['target'];
+    final Status status = logger.startProgress('Compiling $target to JavaScript...', timeout: null);
+    final int result = await webCompiler.compile(target: target);
+    status.stop();
+    return FlutterCommandResult(result == 0 ? ExitStatus.success : ExitStatus.fail);
+  }
+}
diff --git a/packages/flutter_tools/lib/src/context_runner.dart b/packages/flutter_tools/lib/src/context_runner.dart
index ba6a2b1..7a4490b 100644
--- a/packages/flutter_tools/lib/src/context_runner.dart
+++ b/packages/flutter_tools/lib/src/context_runner.dart
@@ -39,6 +39,7 @@
 import 'run_hot.dart';
 import 'usage.dart';
 import 'version.dart';
+import 'web/compile.dart';
 import 'windows/windows_workflow.dart';
 
 Future<T> runInContext<T>(
@@ -91,6 +92,7 @@
       Usage: () => Usage(),
       UserMessages: () => UserMessages(),
       WindowsWorkflow: () => const WindowsWorkflow(),
+      WebCompiler: () => const WebCompiler(),
       Xcode: () => Xcode(),
       XcodeProjectInterpreter: () => XcodeProjectInterpreter(),
     },
diff --git a/packages/flutter_tools/lib/src/web/compile.dart b/packages/flutter_tools/lib/src/web/compile.dart
new file mode 100644
index 0000000..4dab748
--- /dev/null
+++ b/packages/flutter_tools/lib/src/web/compile.dart
@@ -0,0 +1,70 @@
+// Copyright 2019 The Chromium 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 'package:meta/meta.dart';
+
+import '../artifacts.dart';
+import '../base/common.dart';
+import '../base/context.dart';
+import '../base/file_system.dart';
+import '../base/io.dart';
+import '../base/process_manager.dart';
+import '../build_info.dart';
+import '../convert.dart';
+import '../globals.dart';
+
+/// The [WebCompiler] instance.
+WebCompiler get webCompiler => context[WebCompiler];
+
+/// A wrapper around dart2js for web compilation.
+class WebCompiler {
+  const WebCompiler();
+
+  /// Compile `target` using dart2js.
+  ///
+  /// `minify` controls whether minifaction of the source is enabled. Defaults to `true`.
+  /// `enabledAssertions` controls whether assertions are enabled. Defaults to `false`.
+  Future<int> compile({@required String target, bool minify = true, bool enabledAssertions = false}) async {
+    final String engineDartPath = artifacts.getArtifactPath(Artifact.engineDartBinary);
+    final String dart2jsPath = artifacts.getArtifactPath(Artifact.dart2jsSnapshot);
+    final String flutterPatchedSdkPath = artifacts.getArtifactPath(Artifact.flutterPatchedSdkPath);
+    final String librariesPath = fs.path.join(flutterPatchedSdkPath, 'libraries.json');
+    final Directory outputDir = fs.directory(getWebBuildDirectory());
+    if (!outputDir.existsSync()) {
+      outputDir.createSync(recursive: true);
+    }
+    final String outputPath = fs.path.join(outputDir.path, 'main.dart.js');
+    if (!processManager.canRun(engineDartPath)) {
+      throwToolExit('Unable to find Dart binary at $engineDartPath');
+    }
+    final List<String> command = <String>[
+      engineDartPath,
+      dart2jsPath,
+      target,
+      '-o',
+      '$outputPath',
+      '--libraries-spec=$librariesPath',
+      '--platform-binaries=$flutterPatchedSdkPath',
+    ];
+    if (minify) {
+      command.add('-m');
+    }
+    if (enabledAssertions) {
+      command.add('--enable-asserts');
+    }
+    printTrace(command.join(' '));
+    final Process result = await processManager.start(command);
+    result
+        .stdout
+        .transform(utf8.decoder)
+        .transform(const LineSplitter())
+        .listen(printStatus);
+    result
+        .stderr
+        .transform(utf8.decoder)
+        .transform(const LineSplitter())
+        .listen(printError);
+    return result.exitCode;
+  }
+}
diff --git a/packages/flutter_tools/test/web/compile_test.dart b/packages/flutter_tools/test/web/compile_test.dart
new file mode 100644
index 0000000..0316a33
--- /dev/null
+++ b/packages/flutter_tools/test/web/compile_test.dart
@@ -0,0 +1,54 @@
+// Copyright 2019 The Chromium 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 'package:flutter_tools/src/artifacts.dart';
+import 'package:flutter_tools/src/base/file_system.dart';
+import 'package:flutter_tools/src/base/io.dart';
+import 'package:flutter_tools/src/base/logger.dart';
+import 'package:flutter_tools/src/globals.dart';
+import 'package:flutter_tools/src/web/compile.dart';
+import 'package:mockito/mockito.dart';
+import 'package:process/process.dart';
+
+import '../src/context.dart';
+
+void main() {
+  final MockProcessManager mockProcessManager = MockProcessManager();
+  final MockProcess mockProcess = MockProcess();
+  final BufferLogger mockLogger = BufferLogger();
+
+  testUsingContext('invokes dart2js with correct arguments', () async {
+    const WebCompiler webCompiler = WebCompiler();
+    final String engineDartPath = artifacts.getArtifactPath(Artifact.engineDartBinary);
+    final String dart2jsPath = artifacts.getArtifactPath(Artifact.dart2jsSnapshot);
+    final String flutterPatchedSdkPath = artifacts.getArtifactPath(Artifact.flutterPatchedSdkPath);
+    final String librariesPath = fs.path.join(flutterPatchedSdkPath, 'libraries.json');
+
+    when(mockProcess.stdout).thenAnswer((Invocation invocation) => const Stream<List<int>>.empty());
+    when(mockProcess.stderr).thenAnswer((Invocation invocation) => const Stream<List<int>>.empty());
+    when(mockProcess.exitCode).thenAnswer((Invocation invocation) async => 0);
+    when(mockProcessManager.start(any)).thenAnswer((Invocation invocation) async => mockProcess);
+    when(mockProcessManager.canRun(engineDartPath)).thenReturn(true);
+
+    await webCompiler.compile(target: 'lib/main.dart');
+
+    final String outputPath = fs.path.join('build', 'web', 'main.dart.js');
+    verify(mockProcessManager.start(<String>[
+      engineDartPath,
+      dart2jsPath,
+      'lib/main.dart',
+      '-o',
+      outputPath,
+      '--libraries-spec=$librariesPath',
+      '--platform-binaries=$flutterPatchedSdkPath',
+      '-m',
+    ])).called(1);
+  }, overrides: <Type, Generator>{
+    ProcessManager: () => mockProcessManager,
+    Logger: () => mockLogger,
+  });
+}
+
+class MockProcessManager extends Mock implements ProcessManager {}
+class MockProcess extends Mock implements Process {}
\ No newline at end of file