[ci] Pin Chromium version for web tests (#4620)

Switches the web tests from using the version of Chrome installed by the Dockerfile, which is whatever happened to be stable when the image is generated, and thus not hermetic, to a pinned version of Chromium. This uses a slightly modified version of the script that is already used for flutter/packages.

Since Chromium doesn't support mp4 playback, this updates the `video_player` integration tests to use WebM on web instead, to avoid having all the tests fail in CI.

Part of https://github.com/flutter/flutter/issues/84712
diff --git a/.cirrus.yml b/.cirrus.yml
index 50b6cca..024ce59 100644
--- a/.cirrus.yml
+++ b/.cirrus.yml
@@ -246,13 +246,15 @@
         matrix:
           CHANNEL: "master"
           CHANNEL: "stable"
+        CHROME_NO_SANDBOX: true
+        CHROME_DIR: /tmp/web_chromium/
+        CHROME_EXECUTABLE: $CHROME_DIR/chrome-linux/chrome
       install_script:
-        - git clone https://github.com/flutter/web_installers.git
-        - cd web_installers/packages/web_drivers/
-        - dart pub get
+        # Install a pinned version of Chromium and its corresponding ChromeDriver.
+        # Setting CHROME_EXECUTABLE above causes this version to be used for tests.
+        - ./script/install_chromium.sh "$CHROME_DIR"
       chromedriver_background_script:
-        - cd web_installers/packages/web_drivers/
-        - dart lib/web_driver_installer.dart chromedriver --install-only
+        - cd "$CHROME_DIR"
         - ./chromedriver/chromedriver --port=4444
       build_script:
         - ./script/tool_runner.sh build-examples --web
diff --git a/packages/video_player/video_player/CHANGELOG.md b/packages/video_player/video_player/CHANGELOG.md
index 435484b..97b8a16 100644
--- a/packages/video_player/video_player/CHANGELOG.md
+++ b/packages/video_player/video_player/CHANGELOG.md
@@ -15,6 +15,7 @@
 * Fixes integration tests.
 * Updates Android compileSdkVersion to 31.
 * Fixes a flaky integration test.
+* Integration tests now use WebM on web, to allow running with Chromium.
 
 ## 2.2.7
 
diff --git a/packages/video_player/video_player/example/assets/Butterfly-209.webm b/packages/video_player/video_player/example/assets/Butterfly-209.webm
new file mode 100644
index 0000000..991bdc7
--- /dev/null
+++ b/packages/video_player/video_player/example/assets/Butterfly-209.webm
Binary files differ
diff --git a/packages/video_player/video_player/example/integration_test/controller_swap_test.dart b/packages/video_player/video_player/example/integration_test/controller_swap_test.dart
index cae5176..8478076 100644
--- a/packages/video_player/video_player/example/integration_test/controller_swap_test.dart
+++ b/packages/video_player/video_player/example/integration_test/controller_swap_test.dart
@@ -17,11 +17,15 @@
   testWidgets(
     'can substitute one controller by another without crashing',
     (WidgetTester tester) async {
-      VideoPlayerController controller = VideoPlayerController.network(
-        'https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4',
+      // Use WebM for web to allow CI to use Chromium.
+      final String videoAssetKey =
+          kIsWeb ? 'assets/Butterfly-209.webm' : 'assets/Butterfly-209.mp4';
+
+      VideoPlayerController controller = VideoPlayerController.asset(
+        videoAssetKey,
       );
-      VideoPlayerController another = VideoPlayerController.network(
-        'https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4',
+      VideoPlayerController another = VideoPlayerController.asset(
+        videoAssetKey,
       );
       await controller.initialize();
       await another.initialize();
diff --git a/packages/video_player/video_player/example/integration_test/video_player_test.dart b/packages/video_player/video_player/example/integration_test/video_player_test.dart
index 63a3829..d652c04 100644
--- a/packages/video_player/video_player/example/integration_test/video_player_test.dart
+++ b/packages/video_player/video_player/example/integration_test/video_player_test.dart
@@ -16,6 +16,20 @@
 
 const Duration _playDuration = Duration(seconds: 1);
 
+// Use WebM for web to allow CI to use Chromium.
+final String _videoAssetKey =
+    kIsWeb ? 'assets/Butterfly-209.webm' : 'assets/Butterfly-209.mp4';
+
+// Returns the URL to load an asset from this example app as a network source.
+String getUrlForAssetAsNetworkSource(String assetKey) {
+  return 'https://github.com/flutter/plugins/blob/'
+      // This hash can be rolled forward to pick up newly-added assets.
+      'cba393233e559c925a4daf71b06b4bb01c606762'
+      '/packages/video_player/video_player/example/'
+      '$assetKey'
+      '?raw=true';
+}
+
 void main() {
   IntegrationTestWidgetsFlutterBinding.ensureInitialized();
   late VideoPlayerController _controller;
@@ -23,7 +37,7 @@
 
   group('asset videos', () {
     setUp(() {
-      _controller = VideoPlayerController.asset('assets/Butterfly-209.mp4');
+      _controller = VideoPlayerController.asset(_videoAssetKey);
     });
 
     testWidgets('can be initialized', (WidgetTester tester) async {
@@ -32,49 +46,12 @@
       expect(_controller.value.isInitialized, true);
       expect(_controller.value.position, const Duration(seconds: 0));
       expect(_controller.value.isPlaying, false);
+      // The WebM version has a slightly different duration than the MP4.
       expect(_controller.value.duration,
-          const Duration(seconds: 7, milliseconds: 540));
+          Duration(seconds: 7, milliseconds: kIsWeb ? 544 : 540));
     });
 
     testWidgets(
-      'reports buffering status',
-      (WidgetTester tester) async {
-        VideoPlayerController networkController = VideoPlayerController.network(
-          'https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4',
-        );
-        await networkController.initialize();
-        // Mute to allow playing without DOM interaction on Web.
-        // See https://developers.google.com/web/updates/2017/09/autoplay-policy-changes
-        await networkController.setVolume(0);
-        final Completer<void> started = Completer();
-        final Completer<void> ended = Completer();
-        networkController.addListener(() {
-          if (!started.isCompleted && networkController.value.isBuffering) {
-            started.complete();
-          }
-          if (started.isCompleted &&
-              !networkController.value.isBuffering &&
-              !ended.isCompleted) {
-            ended.complete();
-          }
-        });
-
-        await networkController.play();
-        await networkController.seekTo(const Duration(seconds: 5));
-        await tester.pumpAndSettle(_playDuration);
-        await networkController.pause();
-
-        expect(networkController.value.isPlaying, false);
-        expect(networkController.value.position,
-            (Duration position) => position > const Duration(seconds: 0));
-
-        await expectLater(started.future, completes);
-        await expectLater(ended.future, completes);
-      },
-      skip: !(kIsWeb || defaultTargetPlatform == TargetPlatform.android),
-    );
-
-    testWidgets(
       'live stream duration != 0',
       (WidgetTester tester) async {
         VideoPlayerController networkController = VideoPlayerController.network(
@@ -221,23 +198,78 @@
         skip: kIsWeb || // Web does not support local assets.
             // Extremely flaky on iOS: https://github.com/flutter/flutter/issues/86915
             defaultTargetPlatform == TargetPlatform.iOS);
+  });
+
+  group('file-based videos', () {
+    setUp(() async {
+      // Load the data from the asset.
+      String tempDir = (await getTemporaryDirectory()).path;
+      ByteData bytes = await rootBundle.load(_videoAssetKey);
+
+      // Write it to a file to use as a source.
+      final String filename = _videoAssetKey.split('/').last;
+      File file = File('$tempDir/$filename');
+      await file.writeAsBytes(bytes.buffer.asInt8List());
+
+      _controller = VideoPlayerController.file(file);
+    });
 
     testWidgets('test video player using static file() method as constructor',
         (WidgetTester tester) async {
-      String tempDir = (await getTemporaryDirectory()).path;
-      ByteData bytes = await rootBundle.load('assets/Butterfly-209.mp4');
+      await _controller.initialize();
 
-      File file = File('$tempDir/Butterfly-209.mp4');
-      await file.writeAsBytes(bytes.buffer.asInt8List());
+      await _controller.play();
+      expect(_controller.value.isPlaying, true);
 
-      VideoPlayerController fileController = VideoPlayerController.file(file);
-      await fileController.initialize();
-
-      await fileController.play();
-      expect(fileController.value.isPlaying, true);
-
-      await fileController.pause();
-      expect(fileController.value.isPlaying, false);
+      await _controller.pause();
+      expect(_controller.value.isPlaying, false);
     }, skip: kIsWeb);
   });
+
+  group('network videos', () {
+    setUp(() {
+      // TODO(stuartmorgan): Remove this conditional and update the hash in
+      // getUrlForAssetAsNetworkSource as a follow-up, once the webm asset is
+      // checked in.
+      final String videoUrl = kIsWeb
+          ? 'https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.webm'
+          : getUrlForAssetAsNetworkSource(_videoAssetKey);
+      _controller = VideoPlayerController.network(videoUrl);
+    });
+
+    testWidgets(
+      'reports buffering status',
+      (WidgetTester tester) async {
+        await _controller.initialize();
+        // Mute to allow playing without DOM interaction on Web.
+        // See https://developers.google.com/web/updates/2017/09/autoplay-policy-changes
+        await _controller.setVolume(0);
+        final Completer<void> started = Completer();
+        final Completer<void> ended = Completer();
+        _controller.addListener(() {
+          if (!started.isCompleted && _controller.value.isBuffering) {
+            started.complete();
+          }
+          if (started.isCompleted &&
+              !_controller.value.isBuffering &&
+              !ended.isCompleted) {
+            ended.complete();
+          }
+        });
+
+        await _controller.play();
+        await _controller.seekTo(const Duration(seconds: 5));
+        await tester.pumpAndSettle(_playDuration);
+        await _controller.pause();
+
+        expect(_controller.value.isPlaying, false);
+        expect(_controller.value.position,
+            (Duration position) => position > const Duration(seconds: 0));
+
+        await expectLater(started.future, completes);
+        await expectLater(ended.future, completes);
+      },
+      skip: !(kIsWeb || defaultTargetPlatform == TargetPlatform.android),
+    );
+  });
 }
diff --git a/packages/video_player/video_player/example/pubspec.yaml b/packages/video_player/video_player/example/pubspec.yaml
index ce1787e..6fa02c4 100644
--- a/packages/video_player/video_player/example/pubspec.yaml
+++ b/packages/video_player/video_player/example/pubspec.yaml
@@ -33,5 +33,6 @@
   assets:
     - assets/flutter-mark-square-64.png
     - assets/Butterfly-209.mp4
+    - assets/Butterfly-209.webm
     - assets/bumble_bee_captions.srt
     - assets/bumble_bee_captions.vtt
diff --git a/script/install_chromium.sh b/script/install_chromium.sh
new file mode 100755
index 0000000..1cb38af
--- /dev/null
+++ b/script/install_chromium.sh
@@ -0,0 +1,33 @@
+#!/bin/bash
+# Copyright 2013 The Flutter Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+set -e
+set -x
+
+readonly TARGET_DIR=$1
+
+# The build of Chromium used to test web functionality.
+#
+# Chromium builds can be located here: https://commondatastorage.googleapis.com/chromium-browser-snapshots/index.html?prefix=Linux_x64/
+readonly CHROMIUM_BUILD=768968
+# The ChromeDriver version corresponding to the build above. See
+# https://chromedriver.chromium.org/downloads
+# for versions mappings when updating Chromium.
+readonly CHROME_DRIVER_VERSION=84.0.4147.30
+
+# Install Chromium.
+mkdir "$TARGET_DIR"
+wget --no-verbose "https://www.googleapis.com/download/storage/v1/b/chromium-browser-snapshots/o/Linux_x64%2F${CHROMIUM_BUILD}%2Fchrome-linux.zip?alt=media" -O "$TARGET_DIR"/chromium.zip
+unzip "$TARGET_DIR"/chromium.zip -d "$TARGET_DIR"/
+
+# Install ChromeDriver.
+readonly DRIVER_ZIP_FILE="$TARGET_DIR/chromedriver.zip"
+wget --no-verbose "https://chromedriver.storage.googleapis.com/$CHROME_DRIVER_VERSION/chromedriver_linux64.zip" -O "$DRIVER_ZIP_FILE"
+unzip "$DRIVER_ZIP_FILE" -d "$TARGET_DIR/chromedriver"
+
+# Echo info at the end for ease of debugging.
+export CHROME_EXECUTABLE="$TARGET_DIR"/chrome-linux/chrome
+echo $CHROME_EXECUTABLE
+$CHROME_EXECUTABLE --version
+echo "ChromeDriver $CHROME_DRIVER_VERSION"
diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md
index 12ccf17..101bfac 100644
--- a/script/tool/CHANGELOG.md
+++ b/script/tool/CHANGELOG.md
@@ -10,11 +10,13 @@
   `--packages=path_provide_ios` now works.
 - `--run-on-changed-packages` now includes only the changed packages in a
   federated plugin, not all packages in that plugin.
-- Fix `federation-safety-check` handling of plugin deletion, and of top-level
+- Fixes `federation-safety-check` handling of plugin deletion, and of top-level
   files in unfederated plugins whose names match federated plugin heuristics
   (e.g., `packages/foo/foo_android.iml`).
-- Add an auto-retry for failed Firebase Test Lab tests as a short-term patch
+- Adds an auto-retry for failed Firebase Test Lab tests as a short-term patch
   for flake issues.
+- Adds support for `CHROME_EXECUTABLE` in `drive-examples` to match similar
+  `flutter` behavior.
 
 ## 0.7.3
 
diff --git a/script/tool/lib/src/drive_examples_command.dart b/script/tool/lib/src/drive_examples_command.dart
index 593e557..5bf0298 100644
--- a/script/tool/lib/src/drive_examples_command.dart
+++ b/script/tool/lib/src/drive_examples_command.dart
@@ -120,7 +120,9 @@
           '-d',
           'web-server',
           '--web-port=7357',
-          '--browser-name=chrome'
+          '--browser-name=chrome',
+          if (platform.environment.containsKey('CHROME_EXECUTABLE'))
+            '--chrome-binary=${platform.environment['CHROME_EXECUTABLE']}',
         ],
       if (getBoolArg(kPlatformWindows))
         kPlatformWindows: <String>['-d', 'windows'],
diff --git a/script/tool/test/drive_examples_command_test.dart b/script/tool/test/drive_examples_command_test.dart
index a7a1652..3c93d8b 100644
--- a/script/tool/test/drive_examples_command_test.dart
+++ b/script/tool/test/drive_examples_command_test.dart
@@ -584,6 +584,58 @@
           ]));
     });
 
+    test('driving a web plugin with CHROME_EXECUTABLE', () async {
+      final Directory pluginDirectory = createFakePlugin(
+        'plugin',
+        packagesDir,
+        extraFiles: <String>[
+          'example/test_driver/plugin_test.dart',
+          'example/test_driver/plugin.dart',
+        ],
+        platformSupport: <String, PlatformDetails>{
+          kPlatformWeb: const PlatformDetails(PlatformSupport.inline),
+        },
+      );
+
+      final Directory pluginExampleDirectory =
+          pluginDirectory.childDirectory('example');
+
+      mockPlatform.environment['CHROME_EXECUTABLE'] = '/path/to/chrome';
+
+      final List<String> output = await runCapturingPrint(runner, <String>[
+        'drive-examples',
+        '--web',
+      ]);
+
+      expect(
+        output,
+        containsAllInOrder(<Matcher>[
+          contains('Running for plugin'),
+          contains('No issues found!'),
+        ]),
+      );
+
+      expect(
+          processRunner.recordedCalls,
+          orderedEquals(<ProcessCall>[
+            ProcessCall(
+                getFlutterCommand(mockPlatform),
+                const <String>[
+                  'drive',
+                  '-d',
+                  'web-server',
+                  '--web-port=7357',
+                  '--browser-name=chrome',
+                  '--chrome-binary=/path/to/chrome',
+                  '--driver',
+                  'test_driver/plugin_test.dart',
+                  '--target',
+                  'test_driver/plugin.dart'
+                ],
+                pluginExampleDirectory.path),
+          ]));
+    });
+
     test('driving when plugin does not suppport Windows is a no-op', () async {
       createFakePlugin('plugin', packagesDir, extraFiles: <String>[
         'example/test_driver/plugin_test.dart',
diff --git a/script/tool/test/mocks.dart b/script/tool/test/mocks.dart
index 3d0aef1..f6333eb 100644
--- a/script/tool/test/mocks.dart
+++ b/script/tool/test/mocks.dart
@@ -30,6 +30,9 @@
   Uri get script => isWindows
       ? Uri.file(r'C:\foo\bar', windows: true)
       : Uri.file('/foo/bar', windows: false);
+
+  @override
+  Map<String, String> environment = <String, String>{};
 }
 
 class MockProcess extends Mock implements io.Process {