Merge package:browser_launcher into the tools monorepo
diff --git a/pkgs/browser_launcher/.github/dependabot.yml b/pkgs/browser_launcher/.github/dependabot.yml
new file mode 100644
index 0000000..5a11cf5
--- /dev/null
+++ b/pkgs/browser_launcher/.github/dependabot.yml
@@ -0,0 +1,15 @@
+# Set update schedule for GitHub Actions
+
+version: 2
+updates:
+
+- package-ecosystem: github-actions
+  directory: /
+  schedule:
+    interval: monthly
+  labels:
+    - autosubmit
+  groups:
+    github-actions:
+      patterns:
+        - "*"
diff --git a/pkgs/browser_launcher/.github/workflows/dart.yml b/pkgs/browser_launcher/.github/workflows/dart.yml
new file mode 100644
index 0000000..6e1bfe2
--- /dev/null
+++ b/pkgs/browser_launcher/.github/workflows/dart.yml
@@ -0,0 +1,37 @@
+name: Dart
+
+on:
+  # Run on PRs and pushes to the default branch.
+  push:
+    branches: [ master ]
+  pull_request:
+    branches: [ master ]
+  schedule:
+    - cron: "0 0 * * 0"
+
+env:
+  PUB_ENVIRONMENT: bot.github
+  DISPLAY: ':99'
+
+jobs:
+  test:
+    runs-on: ubuntu-latest
+    strategy:
+      matrix:
+        sdk: [3.4, dev]
+    steps:
+      - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938
+      - uses: dart-lang/setup-dart@0a8a0fc875eb934c15d08629302413c671d3f672
+        with:
+          sdk: ${{ matrix.sdk }}
+
+      - run: dart pub get
+        id: install
+
+      - run: dart format --output=none --set-exit-if-changed .
+      - run: dart analyze --fatal-infos
+
+      - name: Run Xvfb 
+        run: Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 &
+
+      - run: dart test
diff --git a/pkgs/browser_launcher/.gitignore b/pkgs/browser_launcher/.gitignore
new file mode 100644
index 0000000..ec8eae3
--- /dev/null
+++ b/pkgs/browser_launcher/.gitignore
@@ -0,0 +1,4 @@
+# Don’t commit the following directories created by pub.
+.dart_tool/
+.packages
+pubspec.lock
diff --git a/pkgs/browser_launcher/AUTHORS b/pkgs/browser_launcher/AUTHORS
new file mode 100644
index 0000000..e8063a8
--- /dev/null
+++ b/pkgs/browser_launcher/AUTHORS
@@ -0,0 +1,6 @@
+# Below is a list of people and organizations that have contributed
+# to the project. Names should be added to the list like so:
+#
+#   Name/Organization <email address>
+
+Google Inc.
diff --git a/pkgs/browser_launcher/CHANGELOG.md b/pkgs/browser_launcher/CHANGELOG.md
new file mode 100644
index 0000000..4fb8a35
--- /dev/null
+++ b/pkgs/browser_launcher/CHANGELOG.md
@@ -0,0 +1,67 @@
+## 1.1.2
+
+- Require Dart 3.4
+- Log errors from chrome
+- Allow tests to detect headless-only environment (for CI).
+- Add extra flags that may help disable additional throttling in background tabs
+- Add `--use-mock-keychain` flag to avoid blocking dialog on MacOS.
+
+## 1.1.1
+
+- Populate the pubspec `repository` field.
+
+## 1.1.0
+
+- Add optional `signIn` argument to `startWithDebugPort`.
+  To be used together with `user-data-dir` to start a Chrome
+  window signed in to the default profile with extensions enabled.
+- Enable the `avoid_dynamic_calls` lint.
+
+## 1.0.0
+
+- Migrate to null-safety.
+
+## 0.1.10
+
+- Support `webkit_inspection_protocol` version `^1.0.0`.
+
+## 0.1.9
+
+- Add support for Chrome executables in `CHROME_PATH`.
+
+## 0.1.8
+
+- Log `STDERR` on Chrome launch failure.
+
+## 0.1.7
+
+- Widen the dependency range on `package:webkit_inspection_protocol`.
+
+## 0.1.6
+
+- Update lower Dart SDK requirement to `2.2.0`.
+- Update the dependency range on `package:webkit_inspection_protocol`.
+
+## 0.1.5
+
+- Add a parameter to use a specified user-data-dir instead of a system temp.
+
+## 0.1.4
+
+- Start Chrome maximized.
+
+## 0.1.3
+
+- widen the version constraint on `package:webkit_inspection_protocol`
+
+## 0.1.2
+
+- lower min sdk version to match Flutter stable
+
+## 0.1.1
+
+- added example
+
+## 0.1.0
+
+- initial release
diff --git a/pkgs/browser_launcher/LICENSE b/pkgs/browser_launcher/LICENSE
new file mode 100644
index 0000000..7670007
--- /dev/null
+++ b/pkgs/browser_launcher/LICENSE
@@ -0,0 +1,27 @@
+Copyright 2019, the Dart project authors. 
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above
+      copyright notice, this list of conditions and the following
+      disclaimer in the documentation and/or other materials provided
+      with the distribution.
+    * Neither the name of Google LLC nor the names of its
+      contributors may be used to endorse or promote products derived
+      from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/pkgs/browser_launcher/README.md b/pkgs/browser_launcher/README.md
new file mode 100644
index 0000000..cf534ab
--- /dev/null
+++ b/pkgs/browser_launcher/README.md
@@ -0,0 +1,8 @@
+[![Dart](https://github.com/dart-lang/browser_launcher/workflows/Dart/badge.svg)](https://github.com/dart-lang/browser_launcher/actions?query=workflow%3ADart+branch%3Amaster)
+[![pub package](https://img.shields.io/pub/v/browser_launcher.svg)](https://pub.dev/packages/browser_launcher)
+[![package publisher](https://img.shields.io/pub/publisher/browser_launcher.svg)](https://pub.dev/packages/browser_launcher/publisher)
+
+Provides a standardized way to launch web browsers.
+
+Currently, Chrome is the only supported browser; support for other browsers may
+be added in the future.
diff --git a/pkgs/browser_launcher/analysis_options.yaml b/pkgs/browser_launcher/analysis_options.yaml
new file mode 100644
index 0000000..556f883
--- /dev/null
+++ b/pkgs/browser_launcher/analysis_options.yaml
@@ -0,0 +1,33 @@
+# https://dart.dev/guides/language/analysis-options
+include: package:dart_flutter_team_lints/analysis_options.yaml
+
+analyzer:
+  language:
+    strict-casts: true
+    strict-inference: true
+    strict-raw-types: true
+
+linter:
+  rules:
+    - avoid_bool_literals_in_conditional_expressions
+    - avoid_classes_with_only_static_members
+    - avoid_private_typedef_functions
+    - avoid_redundant_argument_values
+    - avoid_returning_this
+    - avoid_unused_constructor_parameters
+    - avoid_void_async
+    - cancel_subscriptions
+    - join_return_with_assignment
+    - literal_only_boolean_expressions
+    - missing_whitespace_between_adjacent_strings
+    - no_adjacent_strings_in_list
+    - no_runtimeType_toString
+    - package_api_docs
+    - prefer_const_declarations
+    - prefer_expression_function_bodies
+    - prefer_final_locals
+    - require_trailing_commas
+    - unnecessary_raw_strings
+    - use_if_null_to_convert_nulls_to_bools
+    - use_raw_strings
+    - use_string_buffers
diff --git a/pkgs/browser_launcher/example/main.dart b/pkgs/browser_launcher/example/main.dart
new file mode 100644
index 0000000..86b4eea
--- /dev/null
+++ b/pkgs/browser_launcher/example/main.dart
@@ -0,0 +1,27 @@
+import 'package:browser_launcher/browser_launcher.dart';
+
+const _googleUrl = 'https://www.google.com/';
+const _googleImagesUrl = 'https://www.google.com/imghp?hl=en';
+
+Future<void> main() async {
+  // Launches a chrome browser with two tabs open to [_googleUrl] and
+  // [_googleImagesUrl].
+  await Chrome.start([_googleUrl, _googleImagesUrl]);
+  print('launched Chrome');
+
+  // Pause briefly before opening Chrome with a debug port.
+  await Future<void>.delayed(const Duration(seconds: 3));
+
+  // Launches a chrome browser open to [_googleUrl]. Since we are launching with
+  // a debug port, we will use a variety of different launch configurations,
+  // such as launching in a new browser.
+  final chrome = await Chrome.startWithDebugPort([_googleUrl], debugPort: 8888);
+  print('launched Chrome with a debug port');
+
+  // When running this dart code, observe that the browser stays open for 3
+  // seconds before we close it.
+  await Future<void>.delayed(const Duration(seconds: 3));
+
+  await chrome.close();
+  print('closed Chrome');
+}
diff --git a/pkgs/browser_launcher/lib/browser_launcher.dart b/pkgs/browser_launcher/lib/browser_launcher.dart
new file mode 100644
index 0000000..7d85dad
--- /dev/null
+++ b/pkgs/browser_launcher/lib/browser_launcher.dart
@@ -0,0 +1,5 @@
+// Copyright (c) 2019, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+export 'src/chrome.dart';
diff --git a/pkgs/browser_launcher/lib/src/chrome.dart b/pkgs/browser_launcher/lib/src/chrome.dart
new file mode 100644
index 0000000..8ee14f0
--- /dev/null
+++ b/pkgs/browser_launcher/lib/src/chrome.dart
@@ -0,0 +1,237 @@
+// Copyright (c) 2019, the Dart project authors.  Please see the AUTHORS file
+// for details. 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 'dart:convert';
+import 'dart:io';
+
+import 'package:logging/logging.dart';
+import 'package:path/path.dart' as p;
+import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart';
+
+const _chromeEnvironments = ['CHROME_EXECUTABLE', 'CHROME_PATH'];
+const _linuxExecutable = 'google-chrome';
+const _macOSExecutable =
+    '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome';
+const _windowsExecutable = r'Google\Chrome\Application\chrome.exe';
+
+String get _executable {
+  for (var chromeEnv in _chromeEnvironments) {
+    if (Platform.environment.containsKey(chromeEnv)) {
+      return Platform.environment[chromeEnv]!;
+    }
+  }
+  if (Platform.isLinux) return _linuxExecutable;
+  if (Platform.isMacOS) return _macOSExecutable;
+  if (Platform.isWindows) {
+    final windowsPrefixes = [
+      Platform.environment['LOCALAPPDATA'],
+      Platform.environment['PROGRAMFILES'],
+      Platform.environment['PROGRAMFILES(X86)'],
+    ];
+    return p.join(
+      windowsPrefixes.firstWhere(
+        (prefix) {
+          if (prefix == null) return false;
+          final path = p.join(prefix, _windowsExecutable);
+          return File(path).existsSync();
+        },
+        orElse: () => '.',
+      )!,
+      _windowsExecutable,
+    );
+  }
+  throw StateError('Unexpected platform type.');
+}
+
+/// Manager for an instance of Chrome.
+class Chrome {
+  static final _logger = Logger('BROWSER_LAUNCHER.CHROME');
+
+  Chrome._(
+    this.debugPort,
+    this.chromeConnection, {
+    Process? process,
+    Directory? dataDir,
+    this.deleteDataDir = false,
+  })  : _process = process,
+        _dataDir = dataDir;
+
+  final int debugPort;
+  final ChromeConnection chromeConnection;
+  final Process? _process;
+  final Directory? _dataDir;
+  final bool deleteDataDir;
+
+  /// Connects to an instance of Chrome with an open debug port.
+  static Future<Chrome> fromExisting(int port) async =>
+      _connect(Chrome._(port, ChromeConnection('localhost', port)));
+
+  /// Starts Chrome with the given arguments and a specific port.
+  ///
+  /// Each url in [urls] will be loaded in a separate tab.
+  ///
+  /// If [userDataDir] is `null`, a new temp directory will be
+  /// passed to chrome as a user data directory. Chrome will
+  /// start without sign in and with extensions disabled.
+  ///
+  /// If [userDataDir] is not `null`, it will be passed to chrome
+  /// as a user data directory. Chrome will start signed into
+  /// the default profile with extensions enabled if [signIn]
+  /// is also true.
+  static Future<Chrome> startWithDebugPort(
+    List<String> urls, {
+    int debugPort = 0,
+    bool headless = false,
+    String? userDataDir,
+    bool signIn = false,
+  }) async {
+    Directory dataDir;
+    if (userDataDir == null) {
+      signIn = false;
+      dataDir = Directory.systemTemp.createTempSync();
+    } else {
+      dataDir = Directory(userDataDir);
+    }
+    final port = debugPort == 0 ? await findUnusedPort() : debugPort;
+    final args = [
+      // Using a tmp directory ensures that a new instance of chrome launches
+      // allowing for the remote debug port to be enabled.
+      '--user-data-dir=${dataDir.path}',
+      '--remote-debugging-port=$port',
+      // When the DevTools has focus we don't want to slow down the application.
+      '--disable-background-timer-throttling',
+      '--disable-blink-features=TimerThrottlingForBackgroundTabs',
+      '--disable-features=IntensiveWakeUpThrottling',
+      // Since we are using a temp profile, disable features that slow the
+      // Chrome launch.
+      if (!signIn) '--disable-extensions',
+      '--disable-popup-blocking',
+      if (!signIn) '--bwsi',
+      '--no-first-run',
+      '--no-default-browser-check',
+      '--disable-default-apps',
+      '--disable-translate',
+      '--start-maximized',
+      // When running on MacOS, Chrome may open system dialogs requesting
+      // credentials. This uses a mock keychain to avoid that dialog from
+      // blocking.
+      '--use-mock-keychain',
+    ];
+    if (headless) {
+      args.add('--headless');
+    }
+
+    final process = await _startProcess(urls, args: args);
+
+    // Wait until the DevTools are listening before trying to connect.
+    final errorLines = <String>[];
+    try {
+      final stderr = process.stderr.asBroadcastStream();
+      stderr
+          .transform(utf8.decoder)
+          .transform(const LineSplitter())
+          .listen(_logger.fine);
+
+      await stderr
+          .transform(utf8.decoder)
+          .transform(const LineSplitter())
+          .firstWhere((line) {
+        errorLines.add(line);
+        return line.startsWith('DevTools listening');
+      }).timeout(const Duration(seconds: 60));
+    } on TimeoutException catch (e, s) {
+      _logger.severe('Unable to connect to Chrome DevTools', e, s);
+      throw Exception(
+        'Unable to connect to Chrome DevTools: $e.\n\n'
+        'Chrome STDERR:\n${errorLines.join('\n')}',
+      );
+    }
+
+    return _connect(
+      Chrome._(
+        port,
+        ChromeConnection('localhost', port),
+        process: process,
+        dataDir: dataDir,
+        deleteDataDir: userDataDir == null,
+      ),
+    );
+  }
+
+  /// Starts Chrome with the given arguments.
+  ///
+  /// Each url in [urls] will be loaded in a separate tab.
+  static Future<Process> start(
+    List<String> urls, {
+    List<String> args = const [],
+  }) async =>
+      await _startProcess(urls, args: args);
+
+  static Future<Process> _startProcess(
+    List<String> urls, {
+    List<String> args = const [],
+  }) async {
+    final processArgs = args.toList()..addAll(urls);
+    return await Process.start(_executable, processArgs);
+  }
+
+  static Future<Chrome> _connect(Chrome chrome) async {
+    // The connection is lazy. Try a simple call to make sure the provided
+    // connection is valid.
+    try {
+      await chrome.chromeConnection.getTabs();
+    } catch (e) {
+      await chrome.close();
+      throw ChromeError(
+        'Unable to connect to Chrome debug port: ${chrome.debugPort}\n $e',
+      );
+    }
+    return chrome;
+  }
+
+  Future<void> close() async {
+    chromeConnection.close();
+    _process?.kill(ProcessSignal.sigkill);
+    await _process?.exitCode;
+    try {
+      // Chrome starts another process as soon as it dies that modifies the
+      // profile information. Give it some time before attempting to delete
+      // the directory.
+      if (deleteDataDir) {
+        await Future<void>.delayed(const Duration(milliseconds: 500));
+        await _dataDir?.delete(recursive: true);
+      }
+    } catch (_) {
+      // Silently fail if we can't clean up the profile information.
+      // It is a system tmp directory so it should get cleaned up eventually.
+    }
+  }
+}
+
+class ChromeError extends Error {
+  final String details;
+  ChromeError(this.details);
+
+  @override
+  String toString() => 'ChromeError: $details';
+}
+
+/// Returns a port that is probably, but not definitely, not in use.
+///
+/// This has a built-in race condition: another process may bind this port at
+/// any time after this call has returned.
+Future<int> findUnusedPort() async {
+  int port;
+  ServerSocket socket;
+  try {
+    socket =
+        await ServerSocket.bind(InternetAddress.loopbackIPv6, 0, v6Only: true);
+  } on SocketException {
+    socket = await ServerSocket.bind(InternetAddress.loopbackIPv4, 0);
+  }
+  port = socket.port;
+  await socket.close();
+  return port;
+}
diff --git a/pkgs/browser_launcher/pubspec.yaml b/pkgs/browser_launcher/pubspec.yaml
new file mode 100644
index 0000000..39da2c9
--- /dev/null
+++ b/pkgs/browser_launcher/pubspec.yaml
@@ -0,0 +1,16 @@
+name: browser_launcher
+version: 1.1.2
+description: Provides a standardized way to launch web browsers for testing and tools.
+repository: https://github.com/dart-lang/browser_launcher
+
+environment:
+  sdk: ^3.4.0
+
+dependencies:
+  logging: ^1.0.0
+  path: ^1.8.0
+  webkit_inspection_protocol: ^1.0.0
+
+dev_dependencies:
+  dart_flutter_team_lints: ^3.0.0
+  test: ^1.17.3
diff --git a/pkgs/browser_launcher/test/chrome_test.dart b/pkgs/browser_launcher/test/chrome_test.dart
new file mode 100644
index 0000000..9243768
--- /dev/null
+++ b/pkgs/browser_launcher/test/chrome_test.dart
@@ -0,0 +1,226 @@
+// Copyright (c) 2019, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+@OnPlatform({'windows': Skip('appveyor is not setup to install Chrome')})
+library;
+
+import 'dart:async';
+import 'dart:io';
+
+import 'package:browser_launcher/src/chrome.dart';
+import 'package:logging/logging.dart';
+import 'package:test/test.dart';
+import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart';
+
+const _headlessOnlyEnvironment = 'HEADLESS_ONLY';
+
+bool get headlessOnlyEnvironment =>
+    Platform.environment[_headlessOnlyEnvironment] == 'true';
+
+void _configureLogging(bool verbose) {
+  Logger.root.level = verbose ? Level.ALL : Level.INFO;
+  Logger.root.onRecord.listen((record) {
+    print('${record.level.name}: ${record.time}: ${record.message}');
+  });
+}
+
+void main() {
+  Chrome? chrome;
+
+  // Pass 'true' for debugging.
+  _configureLogging(false);
+
+  Future<ChromeTab?> getTab(String url) => chrome!.chromeConnection.getTab(
+        (t) => t.url.contains(url),
+        retryFor: const Duration(seconds: 5),
+      );
+
+  Future<List<ChromeTab>?> getTabs() => chrome!.chromeConnection.getTabs(
+        retryFor: const Duration(seconds: 5),
+      );
+
+  Future<WipConnection> connectToTab(String url) async {
+    final tab = await getTab(url);
+    expect(tab, isNotNull);
+    return tab!.connect();
+  }
+
+  Future<HttpClientResponse> openTab(String url) =>
+      chrome!.chromeConnection.getUrl(_openTabUrl(url));
+
+  Future<void> launchChromeWithDebugPort({
+    int port = 0,
+    String? userDataDir,
+    bool signIn = false,
+    bool headless = false,
+  }) async {
+    chrome = await Chrome.startWithDebugPort(
+      [_googleUrl],
+      debugPort: port,
+      userDataDir: userDataDir,
+      signIn: signIn,
+      headless: headless,
+    );
+  }
+
+  Future<void> launchChrome({bool headless = false}) async {
+    await Chrome.start([_googleUrl], args: [if (headless) '--headless']);
+  }
+
+  final headlessModes = [
+    true,
+    if (!headlessOnlyEnvironment) false,
+  ];
+
+  for (var headless in headlessModes) {
+    group('(headless: $headless)', () {
+      group('chrome with temp data dir', () {
+        tearDown(() async {
+          await chrome?.close();
+          chrome = null;
+        });
+
+        test('can launch chrome', () async {
+          await launchChrome(headless: headless);
+          expect(chrome, isNull);
+        });
+
+        test('can launch chrome with debug port', () async {
+          await launchChromeWithDebugPort(headless: headless);
+          expect(chrome, isNotNull);
+        });
+
+        test('has a working debugger', () async {
+          await launchChromeWithDebugPort(headless: headless);
+          final tabs = await getTabs();
+          expect(
+            tabs,
+            contains(
+              const TypeMatcher<ChromeTab>()
+                  .having((t) => t.url, 'url', _googleUrl),
+            ),
+          );
+        });
+
+        test('uses open debug port if provided port is 0', () async {
+          await launchChromeWithDebugPort(headless: headless);
+          expect(chrome!.debugPort, isNot(equals(0)));
+        });
+
+        test('can provide a specific debug port', () async {
+          final port = await findUnusedPort();
+          await launchChromeWithDebugPort(port: port, headless: headless);
+          expect(chrome!.debugPort, port);
+        });
+      });
+
+      group('chrome with user data dir', () {
+        late Directory dataDir;
+        const waitMilliseconds = Duration(milliseconds: 100);
+
+        for (var signIn in [false, true]) {
+          group('and signIn = $signIn', () {
+            setUp(() {
+              dataDir = Directory.systemTemp.createTempSync(_userDataDirName);
+            });
+
+            tearDown(() async {
+              await chrome?.close();
+              chrome = null;
+
+              var attempts = 0;
+              while (true) {
+                try {
+                  attempts++;
+                  await Future<dynamic>.delayed(waitMilliseconds);
+                  dataDir.deleteSync(recursive: true);
+                  break;
+                } catch (_) {
+                  if (attempts > 3) rethrow;
+                }
+              }
+            });
+
+            test('can launch with debug port', () async {
+              await launchChromeWithDebugPort(
+                userDataDir: dataDir.path,
+                signIn: signIn,
+                headless: headless,
+              );
+              expect(chrome, isNotNull);
+            });
+
+            test('has a working debugger', () async {
+              await launchChromeWithDebugPort(
+                userDataDir: dataDir.path,
+                signIn: signIn,
+                headless: headless,
+              );
+              final tabs = await getTabs();
+              expect(
+                tabs,
+                contains(
+                  const TypeMatcher<ChromeTab>()
+                      .having((t) => t.url, 'url', _googleUrl),
+                ),
+              );
+            });
+
+            test(
+              'has correct profile path',
+              () async {
+                await launchChromeWithDebugPort(
+                  userDataDir: dataDir.path,
+                  signIn: signIn,
+                  headless: headless,
+                );
+                await openTab(_chromeVersionUrl);
+                final wipConnection = await connectToTab(_chromeVersionUrl);
+                await wipConnection.debugger.enable();
+                await wipConnection.runtime.enable();
+                final result = await _evaluate(
+                  wipConnection.page,
+                  "document.getElementById('profile_path').textContent",
+                );
+                expect(result, contains(_userDataDirName));
+              },
+              // Note: When re-enabling, skip for headless mode because headless
+              // mode does not allow chrome: urls.
+              skip: 'https://github.com/dart-lang/sdk/issues/52357',
+            );
+          });
+        }
+      });
+    });
+  }
+}
+
+String _openTabUrl(String url) => '/json/new?$url';
+
+Future<String?> _evaluate(WipPage page, String expression) async {
+  String? result;
+  const stopInSeconds = Duration(seconds: 5);
+  const waitMilliseconds = Duration(milliseconds: 100);
+  final stopTime = DateTime.now().add(stopInSeconds);
+
+  while (result == null && DateTime.now().isBefore(stopTime)) {
+    await Future<dynamic>.delayed(waitMilliseconds);
+    try {
+      final wipResponse = await page.sendCommand(
+        'Runtime.evaluate',
+        params: {'expression': expression},
+      );
+      final response = wipResponse.json['result'] as Map<String, dynamic>;
+      final value = (response['result'] as Map<String, dynamic>)['value'];
+      result = value?.toString();
+    } catch (_) {
+      return null;
+    }
+  }
+  return result;
+}
+
+const _googleUrl = 'https://www.google.com/';
+const _chromeVersionUrl = 'chrome://version/';
+const _userDataDirName = 'data dir';