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 @@
+[](https://github.com/dart-lang/browser_launcher/actions?query=workflow%3ADart+branch%3Amaster)
+[](https://pub.dev/packages/browser_launcher)
+[](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';