[flutter_tools] run web unit tests in sound null safety (#70799)

diff --git a/dev/bots/test.dart b/dev/bots/test.dart
index 8bd0494..2aca003 100644
--- a/dev/bots/test.dart
+++ b/dev/bots/test.dart
@@ -1126,13 +1126,13 @@
         '--concurrency=1',  // do not parallelize on Cirrus, to reduce flakiness
       '-v',
       '--platform=chrome',
+      '--sound-null-safety', // web tests do not autodetect yet.
       ...?flutterTestArgs,
       ...tests,
     ],
     workingDirectory: workingDirectory,
     environment: <String, String>{
       'FLUTTER_WEB': 'true',
-      'FLUTTER_LOW_RESOURCE_MODE': 'true',
     },
   );
 }
diff --git a/packages/flutter_driver/test/src/web_tests/web_extension_test.dart b/packages/flutter_driver/test/src/web_tests/web_extension_test.dart
index 6cdb0c8..c65ef3e 100644
--- a/packages/flutter_driver/test/src/web_tests/web_extension_test.dart
+++ b/packages/flutter_driver/test/src/web_tests/web_extension_test.dart
@@ -2,7 +2,6 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-// @dart = 2.8
 import 'dart:js' as js;
 
 import 'package:flutter_driver/src/extension/_extension_web.dart';
@@ -10,7 +9,7 @@
 
 void main() {
   group('test web_extension', () {
-    Future<Map<String, dynamic>> Function(Map<String, String>) call;
+    late Future<Map<String, dynamic>> Function(Map<String, String>) call;
 
     setUp(() {
       call = (Map<String, String> args) async {
diff --git a/packages/flutter_tools/lib/src/isolated/devfs_web.dart b/packages/flutter_tools/lib/src/isolated/devfs_web.dart
index c17e3b6..2c24a79 100644
--- a/packages/flutter_tools/lib/src/isolated/devfs_web.dart
+++ b/packages/flutter_tools/lib/src/isolated/devfs_web.dart
@@ -35,18 +35,9 @@
 import '../project.dart';
 import '../web/bootstrap.dart';
 import '../web/chrome.dart';
+import '../web/compile.dart';
 import '../web/memory_fs.dart';
 
-/// Web rendering backend mode.
-enum WebRendererMode {
-  /// Auto detects which rendering backend to use.
-  autoDetect,
-  /// Always uses canvaskit.
-  canvaskit,
-  /// Always uses html.
-  html,
-}
-
 typedef DwdsLauncher = Future<Dwds> Function(
     {@required AssetReader assetReader,
     @required Stream<BuildResult> buildResults,
@@ -549,46 +540,14 @@
     return webSdkFile;
   }
 
-  static const Map<WebRendererMode, Map<NullSafetyMode, Artifact>> _dartSdkJsArtifactMap =
-    <WebRendererMode, Map<NullSafetyMode, Artifact>> {
-      WebRendererMode.autoDetect: <NullSafetyMode, Artifact> {
-        NullSafetyMode.sound: Artifact.webPrecompiledCanvaskitAndHtmlSoundSdk,
-        NullSafetyMode.unsound: Artifact.webPrecompiledCanvaskitAndHtmlSdk,
-      },
-      WebRendererMode.canvaskit: <NullSafetyMode, Artifact> {
-        NullSafetyMode.sound: Artifact.webPrecompiledCanvaskitSoundSdk,
-        NullSafetyMode.unsound: Artifact.webPrecompiledCanvaskitSdk,
-      },
-      WebRendererMode.html: <NullSafetyMode, Artifact> {
-        NullSafetyMode.sound: Artifact.webPrecompiledSoundSdk,
-        NullSafetyMode.unsound: Artifact.webPrecompiledSdk,
-      },
-    };
-
-  static const Map<WebRendererMode, Map<NullSafetyMode, Artifact>> _dartSdkJsMapArtifactMap =
-    <WebRendererMode, Map<NullSafetyMode, Artifact>> {
-      WebRendererMode.autoDetect: <NullSafetyMode, Artifact> {
-        NullSafetyMode.sound: Artifact.webPrecompiledCanvaskitAndHtmlSoundSdkSourcemaps,
-        NullSafetyMode.unsound: Artifact.webPrecompiledCanvaskitAndHtmlSdkSourcemaps,
-      },
-      WebRendererMode.canvaskit: <NullSafetyMode, Artifact> {
-        NullSafetyMode.sound: Artifact.webPrecompiledCanvaskitSoundSdkSourcemaps,
-        NullSafetyMode.unsound: Artifact.webPrecompiledCanvaskitSdkSourcemaps,
-      },
-      WebRendererMode.html: <NullSafetyMode, Artifact> {
-        NullSafetyMode.sound: Artifact.webPrecompiledSoundSdkSourcemaps,
-        NullSafetyMode.unsound: Artifact.webPrecompiledSdkSourcemaps,
-      },
-    };
-
   File get _resolveDartSdkJsFile =>
       globals.fs.file(globals.artifacts.getArtifactPath(
-          _dartSdkJsArtifactMap[webRenderer][_nullSafetyMode]
+          kDartSdkJsArtifactMap[webRenderer][_nullSafetyMode]
       ));
 
   File get _resolveDartSdkJsMapFile =>
     globals.fs.file(globals.artifacts.getArtifactPath(
-        _dartSdkJsMapArtifactMap[webRenderer][_nullSafetyMode]
+        kDartSdkJsMapArtifactMap[webRenderer][_nullSafetyMode]
     ));
 
   @override
diff --git a/packages/flutter_tools/lib/src/isolated/web_compilation_delegate.dart b/packages/flutter_tools/lib/src/isolated/web_compilation_delegate.dart
index b092421..ca2d34c 100644
--- a/packages/flutter_tools/lib/src/isolated/web_compilation_delegate.dart
+++ b/packages/flutter_tools/lib/src/isolated/web_compilation_delegate.dart
@@ -3,6 +3,7 @@
 // found in the LICENSE file.
 
 import 'package:meta/meta.dart';
+import 'package:package_config/package_config.dart';
 
 import '../artifacts.dart';
 import '../base/common.dart';
@@ -10,6 +11,7 @@
 import '../build_info.dart';
 import '../bundle.dart';
 import '../compile.dart';
+import '../dart/language_version.dart';
 import '../globals.dart' as globals;
 import '../web/compile.dart';
 import '../web/memory_fs.dart';
@@ -27,13 +29,24 @@
     @required List<String> testFiles,
     @required BuildInfo buildInfo,
   }) async {
-    if (buildInfo.nullSafetyMode == NullSafetyMode.sound) {
-      throwToolExit('flutter test --platform=chrome does not currently support sound mode');
-    }
+    LanguageVersion languageVersion = LanguageVersion(2, 8);
+    Artifact platformDillArtifact;
+    // TODO(jonahwilliams): to support autodetect this would need to partition the source code into a
+    // a sound and unsound set and perform separate compilations.
     final List<String> extraFrontEndOptions = List<String>.of(buildInfo.extraFrontEndOptions ?? <String>[]);
-    if (!extraFrontEndOptions.contains('--no-sound-null-safety')) {
-      extraFrontEndOptions.add('--no-sound-null-safety');
+    if (buildInfo.nullSafetyMode == NullSafetyMode.unsound || buildInfo.nullSafetyMode == NullSafetyMode.autodetect) {
+      platformDillArtifact = Artifact.webPlatformKernelDill;
+      if (!extraFrontEndOptions.contains('--no-sound-null-safety')) {
+        extraFrontEndOptions.add('--no-sound-null-safety');
+      }
+    } else if (buildInfo.nullSafetyMode == NullSafetyMode.sound) {
+      platformDillArtifact = Artifact.webPlatformSoundKernelDill;
+      languageVersion = nullSafeVersion;
+      if (!extraFrontEndOptions.contains('--sound-null-safety')) {
+        extraFrontEndOptions.add('--sound-null-safety');
+      }
     }
+
     final Directory outputDirectory = globals.fs.directory(testOutputDir)
       ..createSync(recursive: true);
     final List<File> generatedFiles = <File>[];
@@ -44,12 +57,12 @@
         globals.fs.path.join(outputDirectory.path, '${relativeTestSegments.join('_')}.test.dart'));
       generatedFile
         ..createSync(recursive: true)
-        ..writeAsStringSync(_generateEntrypoint(relativeTestSegments.join('/'), testFilePath));
+        ..writeAsStringSync(_generateEntrypoint(relativeTestSegments.join('/'), testFilePath, 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=2.8\n');
+    final StringBuffer buffer = StringBuffer('// @dart=${languageVersion.major}.${languageVersion.minor}\n');
     for (final File generatedFile in generatedFiles) {
       buffer.writeln('import "${globals.fs.path.basename(generatedFile.path)}";');
     }
@@ -77,7 +90,7 @@
       targetModel: TargetModel.dartdevc,
       extraFrontEndOptions: extraFrontEndOptions,
       platformDill: globals.fs.file(globals.artifacts
-        .getArtifactPath(Artifact.webPlatformKernelDill, mode: buildInfo.mode))
+        .getArtifactPath(platformDillArtifact, mode: buildInfo.mode))
         .absolute.uri.toString(),
       dartDefines: buildInfo.dartDefines,
       librariesSpec: globals.fs.file(globals.artifacts
@@ -106,9 +119,9 @@
       ..write(codeFile, manifestFile, sourcemapFile, metadataFile);
   }
 
-  String _generateEntrypoint(String relativeTestPath, String absolutePath) {
+  String _generateEntrypoint(String relativeTestPath, String absolutePath, LanguageVersion languageVersion) {
     return '''
-  // @dart = 2.8
+  // @dart = ${languageVersion.major}.${languageVersion.minor}
   import 'org-dartlang-app:///$relativeTestPath' as test;
   import 'dart:ui' as ui;
   import 'dart:html';
@@ -137,7 +150,7 @@
     postMessageChannel().pipe(channel);
   }
 
-  StreamChannel serializeSuite(Function getMain(), {bool hidePrints = true, Future beforeLoad()}) => RemoteListener.start(getMain, hidePrints: hidePrints, beforeLoad: beforeLoad);
+  StreamChannel serializeSuite(Function getMain(), {bool hidePrints = true}) => RemoteListener.start(getMain, hidePrints: hidePrints);
 
   StreamChannel suiteChannel(String name) {
     var manager = SuiteChannelManager.current;
diff --git a/packages/flutter_tools/lib/src/test/flutter_web_goldens.dart b/packages/flutter_tools/lib/src/test/flutter_web_goldens.dart
index 5c2e0f8..cbc5456 100644
--- a/packages/flutter_tools/lib/src/test/flutter_web_goldens.dart
+++ b/packages/flutter_tools/lib/src/test/flutter_web_goldens.dart
@@ -149,7 +149,6 @@
     final File testConfigFile = findTestConfigFile(globals.fs.file(testUri));
     // Generate comparator process for the file.
     return '''
-// @dart=2.9
 import 'dart:convert'; // ignore: dart_convert_import
 import 'dart:io'; // ignore: dart_io_import
 
@@ -165,12 +164,12 @@
   final commands = stdin
     .transform<String>(utf8.decoder)
     .transform<String>(const LineSplitter())
-    .map<Object>(jsonDecode);
-  await for (final Object command in commands) {
+    .map<dynamic>(jsonDecode);
+  await for (final dynamic command in commands) {
     if (command is Map<String, dynamic>) {
-      File imageFile = File(command['imageFile']);
-      Uri goldenKey = Uri.parse(command['key']);
-      bool update = command['update'];
+      File imageFile = File(command['imageFile'] as String);
+      Uri goldenKey = Uri.parse(command['key'] as String);
+      bool update = command['update'] as bool;
 
       final bytes = await File(imageFile.path).readAsBytes();
       if (update) {
diff --git a/packages/flutter_tools/lib/src/test/flutter_web_platform.dart b/packages/flutter_tools/lib/src/test/flutter_web_platform.dart
index 8f7786d..375d62d 100644
--- a/packages/flutter_tools/lib/src/test/flutter_web_platform.dart
+++ b/packages/flutter_tools/lib/src/test/flutter_web_platform.dart
@@ -32,6 +32,7 @@
 import '../dart/package_map.dart';
 import '../project.dart';
 import '../web/chrome.dart';
+import '../web/compile.dart';
 import '../web/memory_fs.dart';
 import 'flutter_web_goldens.dart';
 import 'test_compiler.dart';
@@ -158,13 +159,13 @@
     'dart_stack_trace_mapper.js',
   ));
 
-  /// The precompiled dart sdk.
-  File get _dartSdk => _fileSystem.file(_fileSystem.path.join(
-    _artifacts.getArtifactPath(Artifact.flutterWebSdk),
-    'kernel',
-    'amd',
-    'dart_sdk.js',
-  ));
+  File get _dartSdk => _fileSystem.file(_artifacts.getArtifactPath(kDartSdkJsArtifactMap[WebRendererMode.html][
+    buildInfo.nullSafetyMode == NullSafetyMode.sound ? NullSafetyMode.sound : NullSafetyMode.unsound
+  ]));
+
+  File get _dartSdkSourcemaps => _fileSystem.file(_artifacts.getArtifactPath(kDartSdkJsMapArtifactMap[WebRendererMode.html][
+    buildInfo.nullSafetyMode == NullSafetyMode.sound ? NullSafetyMode.sound : NullSafetyMode.unsound
+  ]));
 
   /// The precompiled test javascript.
   File get _testDartJs => _fileSystem.file(_fileSystem.path.join(
@@ -223,6 +224,11 @@
         _dartSdk.openRead(),
         headers: <String, String>{'Content-Type': 'text/javascript'},
       );
+    } else if (request.requestedUri.path.contains('dart_sdk.js.map')) {
+      return shelf.Response.ok(
+        _dartSdkSourcemaps.openRead(),
+        headers: <String, String>{'Content-Type': 'text/javascript'},
+      );
     } else if (request.requestedUri.path
         .contains('dart_stack_trace_mapper.js')) {
       return shelf.Response.ok(
diff --git a/packages/flutter_tools/lib/src/web/compile.dart b/packages/flutter_tools/lib/src/web/compile.dart
index dcd52e1..465a094 100644
--- a/packages/flutter_tools/lib/src/web/compile.dart
+++ b/packages/flutter_tools/lib/src/web/compile.dart
@@ -4,6 +4,7 @@
 
 import 'package:meta/meta.dart';
 
+import '../artifacts.dart';
 import '../base/common.dart';
 import '../base/context.dart';
 import '../base/file_system.dart';
@@ -106,3 +107,45 @@
     throw UnimplementedError();
   }
 }
+
+/// Web rendering backend mode.
+enum WebRendererMode {
+  /// Auto detects which rendering backend to use.
+  autoDetect,
+  /// Always uses canvaskit.
+  canvaskit,
+  /// Always uses html.
+  html,
+}
+
+/// The correct precompiled artifact to use for each build and render mode.
+const Map<WebRendererMode, Map<NullSafetyMode, Artifact>> kDartSdkJsArtifactMap = <WebRendererMode, Map<NullSafetyMode, Artifact>>{
+  WebRendererMode.autoDetect: <NullSafetyMode, Artifact> {
+    NullSafetyMode.sound: Artifact.webPrecompiledCanvaskitAndHtmlSoundSdk,
+    NullSafetyMode.unsound: Artifact.webPrecompiledCanvaskitAndHtmlSdk,
+  },
+  WebRendererMode.canvaskit: <NullSafetyMode, Artifact> {
+    NullSafetyMode.sound: Artifact.webPrecompiledCanvaskitSoundSdk,
+    NullSafetyMode.unsound: Artifact.webPrecompiledCanvaskitSdk,
+  },
+  WebRendererMode.html: <NullSafetyMode, Artifact> {
+    NullSafetyMode.sound: Artifact.webPrecompiledSoundSdk,
+    NullSafetyMode.unsound: Artifact.webPrecompiledSdk,
+  },
+};
+
+/// The correct source map artifact to use for each build and render mode.
+const Map<WebRendererMode, Map<NullSafetyMode, Artifact>> kDartSdkJsMapArtifactMap = <WebRendererMode, Map<NullSafetyMode, Artifact>>{
+  WebRendererMode.autoDetect: <NullSafetyMode, Artifact> {
+    NullSafetyMode.sound: Artifact.webPrecompiledCanvaskitAndHtmlSoundSdkSourcemaps,
+    NullSafetyMode.unsound: Artifact.webPrecompiledCanvaskitAndHtmlSdkSourcemaps,
+  },
+  WebRendererMode.canvaskit: <NullSafetyMode, Artifact> {
+    NullSafetyMode.sound: Artifact.webPrecompiledCanvaskitSoundSdkSourcemaps,
+    NullSafetyMode.unsound: Artifact.webPrecompiledCanvaskitSdkSourcemaps,
+  },
+  WebRendererMode.html: <NullSafetyMode, Artifact> {
+    NullSafetyMode.sound: Artifact.webPrecompiledSoundSdkSourcemaps,
+    NullSafetyMode.unsound: Artifact.webPrecompiledSdkSourcemaps,
+  },
+};
diff --git a/packages/flutter_tools/test/general.shard/web/devfs_web_test.dart b/packages/flutter_tools/test/general.shard/web/devfs_web_test.dart
index e1e82a7..77228e4 100644
--- a/packages/flutter_tools/test/general.shard/web/devfs_web_test.dart
+++ b/packages/flutter_tools/test/general.shard/web/devfs_web_test.dart
@@ -14,6 +14,7 @@
 import 'package:flutter_tools/src/compile.dart';
 import 'package:flutter_tools/src/convert.dart';
 import 'package:flutter_tools/src/globals.dart' as globals;
+import 'package:flutter_tools/src/web/compile.dart';
 import 'package:mockito/mockito.dart';
 import 'package:package_config/package_config.dart';
 import 'package:shelf/shelf.dart';
diff --git a/packages/flutter_web_plugins/lib/src/plugin_registry.dart b/packages/flutter_web_plugins/lib/src/plugin_registry.dart
index f9a2fff..ada8aae 100644
--- a/packages/flutter_web_plugins/lib/src/plugin_registry.dart
+++ b/packages/flutter_web_plugins/lib/src/plugin_registry.dart
@@ -121,8 +121,8 @@
 
   /// Sends a platform message from the platform side back to the framework.
   @override
-  Future<ByteData> send(String channel, ByteData? message) {
-    final Completer<ByteData> completer = Completer<ByteData>();
+  Future<ByteData?> send(String channel, ByteData? message) {
+    final Completer<ByteData?> completer = Completer<ByteData?>();
     ui.window.onPlatformMessage!(channel, message, (ByteData? reply) {
       try {
         completer.complete(reply);
diff --git a/packages/flutter_web_plugins/test/plugin_registry_test.dart b/packages/flutter_web_plugins/test/plugin_registry_test.dart
index 7957100..7b950d7 100644
--- a/packages/flutter_web_plugins/test/plugin_registry_test.dart
+++ b/packages/flutter_web_plugins/test/plugin_registry_test.dart
@@ -57,7 +57,7 @@
       ServicesBinding.instance!.defaultBinaryMessenger
           .setMessageHandler('test_send', (ByteData? data) {
         loggedMessages.add(codec.decodeMessage(data) as String);
-        return null;
+        return Future<ByteData?>.value(null);
       });
 
       await pluginBinaryMessenger.send(