Enable web foundation tests (#34032)

diff --git a/.cirrus.yml b/.cirrus.yml
index 3e42c4c..d9e9332 100644
--- a/.cirrus.yml
+++ b/.cirrus.yml
@@ -79,6 +79,11 @@
     - name: web_tests-linux
       env:
         SHARD: web_tests
+      test_script:
+        - dart --enable-asserts ./dev/bots/test.dart
+      container:
+        cpu: 4
+        memory: 12G
     - name: build_tests-linux
       env:
         SHARD: build_tests
diff --git a/dev/bots/test.dart b/dev/bots/test.dart
index cca6791..e892be6 100644
--- a/dev/bots/test.dart
+++ b/dev/bots/test.dart
@@ -341,15 +341,8 @@
 }
 
 Future<void> _runWebTests() async {
-  final List<String> testfiles = <String>[];
-  final Directory foundation = Directory(path.join(flutterRoot, 'packages', 'flutter', 'test', 'foundation'));
-  for (FileSystemEntity entity in foundation.listSync(recursive: true)) {
-    if (entity is File) {
-      testfiles.add(entity.path);
-    }
-  }
-  await _runFlutterWebTest(path.join(flutterRoot, 'packages', 'flutter'), expectFailure: true, tests: <String>[
-    path.join('test', 'foundation'),
+  await _runFlutterWebTest(path.join(flutterRoot, 'packages', 'flutter'), expectFailure: false, tests: <String>[
+    'test/foundation/',
   ]);
 }
 
@@ -612,23 +605,30 @@
   Duration timeout = _kLongTimeout,
   List<String> tests,
 }) async {
-  final List<String> args = <String>['test', '--platform=chrome'];
+  final List<String> args = <String>['test', '-v', '--platform=chrome'];
   if (flutterTestArgs != null && flutterTestArgs.isNotEmpty)
     args.addAll(flutterTestArgs);
 
-  args.add('--machine');
   args.addAll(tests);
 
-  await runCommand(
-    flutter,
-    args,
-    workingDirectory: workingDirectory,
-    expectNonZeroExit: expectFailure,
-    timeout: timeout,
-    environment: <String, String>{
-      'FLUTTER_WEB': 'true',
-    },
-  );
+  // TODO(jonahwilliams): fix relative path issues to make this unecessary.
+  final Directory oldCurrent = Directory.current;
+  Directory.current = Directory(path.join(flutterRoot, 'packages', 'flutter'));
+  try {
+    await runCommand(
+      flutter,
+      args,
+      workingDirectory: workingDirectory,
+      expectNonZeroExit: expectFailure,
+      timeout: timeout,
+      environment: <String, String>{
+        'FLUTTER_WEB': 'true',
+        'FLUTTER_LOW_RESOURCE_MODE': 'true',
+      },
+    );
+  } finally {
+    Directory.current = oldCurrent;
+  }
 }
 
 Future<void> _runFlutterTest(String workingDirectory, {
diff --git a/packages/flutter/test/flutter_test_alternative.dart b/packages/flutter/test/flutter_test_alternative.dart
index 9d0c457..12711c1 100644
--- a/packages/flutter/test/flutter_test_alternative.dart
+++ b/packages/flutter/test/flutter_test_alternative.dart
@@ -12,3 +12,6 @@
 
 /// A matcher that compares the type of the actual value to the type argument T.
 Matcher isInstanceOf<T>() => test_package.TypeMatcher<T>();
+
+/// Whether we are running in a web browser.
+const bool isBrowser = identical(0, 0.0);
diff --git a/packages/flutter/test/foundation/assertions_test.dart b/packages/flutter/test/foundation/assertions_test.dart
index 4db1b82..c2d5ddc 100644
--- a/packages/flutter/test/foundation/assertions_test.dart
+++ b/packages/flutter/test/foundation/assertions_test.dart
@@ -14,7 +14,7 @@
     });
     expect(log[0], contains('Example label'));
     expect(log[1], contains('debugPrintStack'));
-  });
+  }, skip: isBrowser);
 
   test('debugPrintStack', () {
     final List<String> log = captureOutput(() {
@@ -39,7 +39,7 @@
 
     expect(joined, contains('captureOutput'));
     expect(joined, contains('\nExample information\n'));
-  });
+  }, skip: isBrowser);
 
   test('FlutterErrorDetails.toString', () {
     expect(
diff --git a/packages/flutter/test/foundation/change_notifier_test.dart b/packages/flutter/test/foundation/change_notifier_test.dart
index 3c09faa..020ecdc 100644
--- a/packages/flutter/test/foundation/change_notifier_test.dart
+++ b/packages/flutter/test/foundation/change_notifier_test.dart
@@ -102,7 +102,7 @@
     expect(log, <String>['badListener', 'listener1', 'listener2']);
     expect(tester.takeException(), isNullThrownError);
     log.clear();
-  });
+  }, skip: isBrowser);
 
   test('ChangeNotifier with mutating listener', () {
     final TestNotifier test = TestNotifier();
diff --git a/packages/flutter/test/foundation/diagnostics_test.dart b/packages/flutter/test/foundation/diagnostics_test.dart
index 27278f2..ec25445 100644
--- a/packages/flutter/test/foundation/diagnostics_test.dart
+++ b/packages/flutter/test/foundation/diagnostics_test.dart
@@ -1187,7 +1187,7 @@
     expect(missing.isFiltered(DiagnosticLevel.info), isFalse);
     validateObjectFlagPropertyJsonSerialization(present);
     validateObjectFlagPropertyJsonSerialization(missing);
-  });
+  }, skip: isBrowser);
 
   test('describe bool property', () {
     final FlagProperty yes = FlagProperty(
diff --git a/packages/flutter/test/foundation/reassemble_test.dart b/packages/flutter/test/foundation/reassemble_test.dart
index 1808b6c..06183bf 100644
--- a/packages/flutter/test/foundation/reassemble_test.dart
+++ b/packages/flutter/test/foundation/reassemble_test.dart
@@ -2,6 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+@TestOn('!chrome')
 import 'dart:async';
 
 import 'package:flutter/foundation.dart';
diff --git a/packages/flutter_tools/lib/src/build_runner/web_compilation_delegate.dart b/packages/flutter_tools/lib/src/build_runner/web_compilation_delegate.dart
index d829dea..9adc08e 100644
--- a/packages/flutter_tools/lib/src/build_runner/web_compilation_delegate.dart
+++ b/packages/flutter_tools/lib/src/build_runner/web_compilation_delegate.dart
@@ -161,7 +161,20 @@
     defaultGenerateFor: const InputSet(
       include: <String>[
         'lib/**',
-        'web/**',
+      ],
+    ),
+  ),
+  core.apply(
+    'flutter_tools|test_entrypoint',
+    <BuilderFactory>[
+      (BuilderOptions options) => FlutterWebTestEntrypointBuilder(
+        options.config['targets'] ?? const <String>[]
+      ),
+    ],
+    core.toRoot(),
+    hideOutput: true,
+    defaultGenerateFor: const InputSet(
+      include: <String>[
         'test/**_test.dart.browser_test.dart',
       ],
     ),
@@ -213,6 +226,7 @@
       skipBuildScriptCheck: true,
       trackPerformance: false,
       deleteFilesByDefault: true,
+      enableLowResourcesMode: platform.environment['FLUTTER_LOW_RESOURCE_MODE']?.toLowerCase() == 'true',
     );
     final Set<core.BuildDirectory> buildDirs = <core.BuildDirectory>{
       if (testOutputDir != null)
@@ -290,6 +304,10 @@
           'targets': targets,
           'release': release,
         },
+        'flutter_tools|test_entrypoint': <String, dynamic>{
+          'targets': targets,
+          'release': release,
+        },
         'flutter_tools|shell': <String, dynamic>{
           'targets': targets,
         }
@@ -348,6 +366,40 @@
 }
 
 /// A ddc-only entrypoint builder that respects the Flutter target flag.
+class FlutterWebTestEntrypointBuilder implements Builder {
+  const FlutterWebTestEntrypointBuilder(this.targets);
+
+  final List<String> targets;
+
+  @override
+  Map<String, List<String>> get buildExtensions => const <String, List<String>>{
+        '.dart': <String>[
+          ddcBootstrapExtension,
+          jsEntrypointExtension,
+          jsEntrypointSourceMapExtension,
+          jsEntrypointArchiveExtension,
+          digestsEntrypointExtension,
+        ],
+      };
+
+  @override
+  Future<void> build(BuildStep buildStep) async {
+    bool matches = false;
+    for (String target in targets) {
+      if (buildStep.inputId.path.contains(target)) {
+        matches = true;
+        break;
+      }
+    }
+    if (!matches) {
+      return;
+    }
+    log.info('building for target ${buildStep.inputId.path}');
+    await bootstrapDdc(buildStep, platform: flutterWebPlatform);
+  }
+}
+
+/// A ddc-only entrypoint builder that respects the Flutter target flag.
 class FlutterWebEntrypointBuilder implements Builder {
   const FlutterWebEntrypointBuilder(this.targets, this.release);
 
diff --git a/packages/flutter_tools/lib/src/commands/test.dart b/packages/flutter_tools/lib/src/commands/test.dart
index d30266d..cce7e75 100644
--- a/packages/flutter_tools/lib/src/commands/test.dart
+++ b/packages/flutter_tools/lib/src/commands/test.dart
@@ -110,6 +110,7 @@
   @override
   Future<Set<DevelopmentArtifact>> get requiredArtifacts async => <DevelopmentArtifact>{
     DevelopmentArtifact.universal,
+    DevelopmentArtifact.web,
   };
 
   @override
diff --git a/packages/flutter_tools/lib/src/test/runner.dart b/packages/flutter_tools/lib/src/test/runner.dart
index 796a03d..f9df159 100644
--- a/packages/flutter_tools/lib/src/test/runner.dart
+++ b/packages/flutter_tools/lib/src/test/runner.dart
@@ -73,13 +73,16 @@
       .absolute
       .uri
       .toFilePath();
-    await webCompilationProxy.initialize(
+    final bool result = await webCompilationProxy.initialize(
       projectDirectory: flutterProject.directory,
       testOutputDir: tempBuildDir,
       targets: testFiles.map((String testFile) {
         return fs.path.relative(testFile, from: flutterProject.directory.path);
       }).toList(),
     );
+    if (!result) {
+      throwToolExit('Failed to compile tests');
+    }
     testArgs.add('--platform=chrome');
     testArgs.add('--precompiled=$tempBuildDir');
     testArgs.add('--');
diff --git a/packages/flutter_tools/lib/src/web/chrome.dart b/packages/flutter_tools/lib/src/web/chrome.dart
index 3d91107..77961a2 100644
--- a/packages/flutter_tools/lib/src/web/chrome.dart
+++ b/packages/flutter_tools/lib/src/web/chrome.dart
@@ -98,16 +98,19 @@
       '--disable-default-apps',
       '--disable-translate',
       if (headless)
-        ...<String>['--headless', '--disable-gpu'],
+        ...<String>['--headless', '--disable-gpu', '--no-sandbox'],
       url,
     ];
-    final Process process = await processManager.start(args);
+
+    final Process process = await processManager.start(args, runInShell: true);
 
     // Wait until the DevTools are listening before trying to connect.
     await process.stderr
         .transform(utf8.decoder)
         .transform(const LineSplitter())
-        .firstWhere((String line) => line.startsWith('DevTools listening'))
+        .firstWhere((String line) => line.startsWith('DevTools listening'), orElse: () {
+          return 'Failed to spawn stderr';
+        })
         .timeout(const Duration(seconds: 60), onTimeout: () {
           throwToolExit('Unable to connect to Chrome DevTools.');
           return null;