├── .github ├── dependabot.yml └── workflows │ └── dart.yml ├── .gitignore ├── AUTHORS ├── CHANGELOG.md ├── LICENSE ├── README.md ├── analysis_options.yaml ├── example └── main.dart ├── lib ├── browser_launcher.dart └── src │ └── chrome.dart ├── pubspec.yaml └── test └── chrome_test.dart /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Set update schedule for GitHub Actions 2 | 3 | version: 2 4 | updates: 5 | 6 | - package-ecosystem: github-actions 7 | directory: / 8 | schedule: 9 | interval: monthly 10 | labels: 11 | - autosubmit 12 | groups: 13 | github-actions: 14 | patterns: 15 | - "*" 16 | -------------------------------------------------------------------------------- /.github/workflows/dart.yml: -------------------------------------------------------------------------------- 1 | name: Dart 2 | 3 | on: 4 | # Run on PRs and pushes to the default branch. 5 | push: 6 | branches: [ master ] 7 | pull_request: 8 | branches: [ master ] 9 | schedule: 10 | - cron: "0 0 * * 0" 11 | 12 | env: 13 | PUB_ENVIRONMENT: bot.github 14 | DISPLAY: ':99' 15 | 16 | jobs: 17 | test: 18 | runs-on: ubuntu-latest 19 | strategy: 20 | matrix: 21 | sdk: [3.4, dev] 22 | steps: 23 | - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 24 | - uses: dart-lang/setup-dart@0a8a0fc875eb934c15d08629302413c671d3f672 25 | with: 26 | sdk: ${{ matrix.sdk }} 27 | 28 | - run: dart pub get 29 | id: install 30 | 31 | - run: dart format --output=none --set-exit-if-changed . 32 | - run: dart analyze --fatal-infos 33 | 34 | - name: Run Xvfb 35 | run: Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 & 36 | 37 | - run: dart test 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Don’t commit the following directories created by pub. 2 | .dart_tool/ 3 | .packages 4 | pubspec.lock 5 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | # Below is a list of people and organizations that have contributed 2 | # to the project. Names should be added to the list like so: 3 | # 4 | # Name/Organization 5 | 6 | Google Inc. 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.1.2 2 | 3 | - Require Dart 3.4 4 | - Log errors from chrome 5 | - Allow tests to detect headless-only environment (for CI). 6 | - Add extra flags that may help disable additional throttling in background tabs 7 | - Add `--use-mock-keychain` flag to avoid blocking dialog on MacOS. 8 | 9 | ## 1.1.1 10 | 11 | - Populate the pubspec `repository` field. 12 | 13 | ## 1.1.0 14 | 15 | - Add optional `signIn` argument to `startWithDebugPort`. 16 | To be used together with `user-data-dir` to start a Chrome 17 | window signed in to the default profile with extensions enabled. 18 | - Enable the `avoid_dynamic_calls` lint. 19 | 20 | ## 1.0.0 21 | 22 | - Migrate to null-safety. 23 | 24 | ## 0.1.10 25 | 26 | - Support `webkit_inspection_protocol` version `^1.0.0`. 27 | 28 | ## 0.1.9 29 | 30 | - Add support for Chrome executables in `CHROME_PATH`. 31 | 32 | ## 0.1.8 33 | 34 | - Log `STDERR` on Chrome launch failure. 35 | 36 | ## 0.1.7 37 | 38 | - Widen the dependency range on `package:webkit_inspection_protocol`. 39 | 40 | ## 0.1.6 41 | 42 | - Update lower Dart SDK requirement to `2.2.0`. 43 | - Update the dependency range on `package:webkit_inspection_protocol`. 44 | 45 | ## 0.1.5 46 | 47 | - Add a parameter to use a specified user-data-dir instead of a system temp. 48 | 49 | ## 0.1.4 50 | 51 | - Start Chrome maximized. 52 | 53 | ## 0.1.3 54 | 55 | - widen the version constraint on `package:webkit_inspection_protocol` 56 | 57 | ## 0.1.2 58 | 59 | - lower min sdk version to match Flutter stable 60 | 61 | ## 0.1.1 62 | 63 | - added example 64 | 65 | ## 0.1.0 66 | 67 | - initial release 68 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2019, the Dart project authors. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above 10 | copyright notice, this list of conditions and the following 11 | disclaimer in the documentation and/or other materials provided 12 | with the distribution. 13 | * Neither the name of Google LLC nor the names of its 14 | contributors may be used to endorse or promote products derived 15 | from this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > [!IMPORTANT] 2 | > This repo has moved to https://github.com/dart-lang/tools/tree/main/pkgs/browser_launcher 3 | 4 | [![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) 5 | [![pub package](https://img.shields.io/pub/v/browser_launcher.svg)](https://pub.dev/packages/browser_launcher) 6 | [![package publisher](https://img.shields.io/pub/publisher/browser_launcher.svg)](https://pub.dev/packages/browser_launcher/publisher) 7 | 8 | Provides a standardized way to launch web browsers. 9 | 10 | Currently, Chrome is the only supported browser; support for other browsers may 11 | be added in the future. 12 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # https://dart.dev/guides/language/analysis-options 2 | include: package:dart_flutter_team_lints/analysis_options.yaml 3 | 4 | analyzer: 5 | language: 6 | strict-casts: true 7 | strict-inference: true 8 | strict-raw-types: true 9 | 10 | linter: 11 | rules: 12 | - avoid_bool_literals_in_conditional_expressions 13 | - avoid_classes_with_only_static_members 14 | - avoid_private_typedef_functions 15 | - avoid_redundant_argument_values 16 | - avoid_returning_this 17 | - avoid_unused_constructor_parameters 18 | - avoid_void_async 19 | - cancel_subscriptions 20 | - join_return_with_assignment 21 | - literal_only_boolean_expressions 22 | - missing_whitespace_between_adjacent_strings 23 | - no_adjacent_strings_in_list 24 | - no_runtimeType_toString 25 | - package_api_docs 26 | - prefer_const_declarations 27 | - prefer_expression_function_bodies 28 | - prefer_final_locals 29 | - require_trailing_commas 30 | - unnecessary_raw_strings 31 | - use_if_null_to_convert_nulls_to_bools 32 | - use_raw_strings 33 | - use_string_buffers 34 | -------------------------------------------------------------------------------- /example/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:browser_launcher/browser_launcher.dart'; 2 | 3 | const _googleUrl = 'https://www.google.com/'; 4 | const _googleImagesUrl = 'https://www.google.com/imghp?hl=en'; 5 | 6 | Future main() async { 7 | // Launches a chrome browser with two tabs open to [_googleUrl] and 8 | // [_googleImagesUrl]. 9 | await Chrome.start([_googleUrl, _googleImagesUrl]); 10 | print('launched Chrome'); 11 | 12 | // Pause briefly before opening Chrome with a debug port. 13 | await Future.delayed(const Duration(seconds: 3)); 14 | 15 | // Launches a chrome browser open to [_googleUrl]. Since we are launching with 16 | // a debug port, we will use a variety of different launch configurations, 17 | // such as launching in a new browser. 18 | final chrome = await Chrome.startWithDebugPort([_googleUrl], debugPort: 8888); 19 | print('launched Chrome with a debug port'); 20 | 21 | // When running this dart code, observe that the browser stays open for 3 22 | // seconds before we close it. 23 | await Future.delayed(const Duration(seconds: 3)); 24 | 25 | await chrome.close(); 26 | print('closed Chrome'); 27 | } 28 | -------------------------------------------------------------------------------- /lib/browser_launcher.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | export 'src/chrome.dart'; 6 | -------------------------------------------------------------------------------- /lib/src/chrome.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'dart:async'; 6 | import 'dart:convert'; 7 | import 'dart:io'; 8 | 9 | import 'package:logging/logging.dart'; 10 | import 'package:path/path.dart' as p; 11 | import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart'; 12 | 13 | const _chromeEnvironments = ['CHROME_EXECUTABLE', 'CHROME_PATH']; 14 | const _linuxExecutable = 'google-chrome'; 15 | const _macOSExecutable = 16 | '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'; 17 | const _windowsExecutable = r'Google\Chrome\Application\chrome.exe'; 18 | 19 | String get _executable { 20 | for (var chromeEnv in _chromeEnvironments) { 21 | if (Platform.environment.containsKey(chromeEnv)) { 22 | return Platform.environment[chromeEnv]!; 23 | } 24 | } 25 | if (Platform.isLinux) return _linuxExecutable; 26 | if (Platform.isMacOS) return _macOSExecutable; 27 | if (Platform.isWindows) { 28 | final windowsPrefixes = [ 29 | Platform.environment['LOCALAPPDATA'], 30 | Platform.environment['PROGRAMFILES'], 31 | Platform.environment['PROGRAMFILES(X86)'], 32 | ]; 33 | return p.join( 34 | windowsPrefixes.firstWhere( 35 | (prefix) { 36 | if (prefix == null) return false; 37 | final path = p.join(prefix, _windowsExecutable); 38 | return File(path).existsSync(); 39 | }, 40 | orElse: () => '.', 41 | )!, 42 | _windowsExecutable, 43 | ); 44 | } 45 | throw StateError('Unexpected platform type.'); 46 | } 47 | 48 | /// Manager for an instance of Chrome. 49 | class Chrome { 50 | static final _logger = Logger('BROWSER_LAUNCHER.CHROME'); 51 | 52 | Chrome._( 53 | this.debugPort, 54 | this.chromeConnection, { 55 | Process? process, 56 | Directory? dataDir, 57 | this.deleteDataDir = false, 58 | }) : _process = process, 59 | _dataDir = dataDir; 60 | 61 | final int debugPort; 62 | final ChromeConnection chromeConnection; 63 | final Process? _process; 64 | final Directory? _dataDir; 65 | final bool deleteDataDir; 66 | 67 | /// Connects to an instance of Chrome with an open debug port. 68 | static Future fromExisting(int port) async => 69 | _connect(Chrome._(port, ChromeConnection('localhost', port))); 70 | 71 | /// Starts Chrome with the given arguments and a specific port. 72 | /// 73 | /// Each url in [urls] will be loaded in a separate tab. 74 | /// 75 | /// If [userDataDir] is `null`, a new temp directory will be 76 | /// passed to chrome as a user data directory. Chrome will 77 | /// start without sign in and with extensions disabled. 78 | /// 79 | /// If [userDataDir] is not `null`, it will be passed to chrome 80 | /// as a user data directory. Chrome will start signed into 81 | /// the default profile with extensions enabled if [signIn] 82 | /// is also true. 83 | static Future startWithDebugPort( 84 | List urls, { 85 | int debugPort = 0, 86 | bool headless = false, 87 | String? userDataDir, 88 | bool signIn = false, 89 | }) async { 90 | Directory dataDir; 91 | if (userDataDir == null) { 92 | signIn = false; 93 | dataDir = Directory.systemTemp.createTempSync(); 94 | } else { 95 | dataDir = Directory(userDataDir); 96 | } 97 | final port = debugPort == 0 ? await findUnusedPort() : debugPort; 98 | final args = [ 99 | // Using a tmp directory ensures that a new instance of chrome launches 100 | // allowing for the remote debug port to be enabled. 101 | '--user-data-dir=${dataDir.path}', 102 | '--remote-debugging-port=$port', 103 | // When the DevTools has focus we don't want to slow down the application. 104 | '--disable-background-timer-throttling', 105 | '--disable-blink-features=TimerThrottlingForBackgroundTabs', 106 | '--disable-features=IntensiveWakeUpThrottling', 107 | // Since we are using a temp profile, disable features that slow the 108 | // Chrome launch. 109 | if (!signIn) '--disable-extensions', 110 | '--disable-popup-blocking', 111 | if (!signIn) '--bwsi', 112 | '--no-first-run', 113 | '--no-default-browser-check', 114 | '--disable-default-apps', 115 | '--disable-translate', 116 | '--start-maximized', 117 | // When running on MacOS, Chrome may open system dialogs requesting 118 | // credentials. This uses a mock keychain to avoid that dialog from 119 | // blocking. 120 | '--use-mock-keychain', 121 | ]; 122 | if (headless) { 123 | args.add('--headless'); 124 | } 125 | 126 | final process = await _startProcess(urls, args: args); 127 | 128 | // Wait until the DevTools are listening before trying to connect. 129 | final errorLines = []; 130 | try { 131 | final stderr = process.stderr.asBroadcastStream(); 132 | stderr 133 | .transform(utf8.decoder) 134 | .transform(const LineSplitter()) 135 | .listen(_logger.fine); 136 | 137 | await stderr 138 | .transform(utf8.decoder) 139 | .transform(const LineSplitter()) 140 | .firstWhere((line) { 141 | errorLines.add(line); 142 | return line.startsWith('DevTools listening'); 143 | }).timeout(const Duration(seconds: 60)); 144 | } on TimeoutException catch (e, s) { 145 | _logger.severe('Unable to connect to Chrome DevTools', e, s); 146 | throw Exception( 147 | 'Unable to connect to Chrome DevTools: $e.\n\n' 148 | 'Chrome STDERR:\n${errorLines.join('\n')}', 149 | ); 150 | } 151 | 152 | return _connect( 153 | Chrome._( 154 | port, 155 | ChromeConnection('localhost', port), 156 | process: process, 157 | dataDir: dataDir, 158 | deleteDataDir: userDataDir == null, 159 | ), 160 | ); 161 | } 162 | 163 | /// Starts Chrome with the given arguments. 164 | /// 165 | /// Each url in [urls] will be loaded in a separate tab. 166 | static Future start( 167 | List urls, { 168 | List args = const [], 169 | }) async => 170 | await _startProcess(urls, args: args); 171 | 172 | static Future _startProcess( 173 | List urls, { 174 | List args = const [], 175 | }) async { 176 | final processArgs = args.toList()..addAll(urls); 177 | return await Process.start(_executable, processArgs); 178 | } 179 | 180 | static Future _connect(Chrome chrome) async { 181 | // The connection is lazy. Try a simple call to make sure the provided 182 | // connection is valid. 183 | try { 184 | await chrome.chromeConnection.getTabs(); 185 | } catch (e) { 186 | await chrome.close(); 187 | throw ChromeError( 188 | 'Unable to connect to Chrome debug port: ${chrome.debugPort}\n $e', 189 | ); 190 | } 191 | return chrome; 192 | } 193 | 194 | Future close() async { 195 | chromeConnection.close(); 196 | _process?.kill(ProcessSignal.sigkill); 197 | await _process?.exitCode; 198 | try { 199 | // Chrome starts another process as soon as it dies that modifies the 200 | // profile information. Give it some time before attempting to delete 201 | // the directory. 202 | if (deleteDataDir) { 203 | await Future.delayed(const Duration(milliseconds: 500)); 204 | await _dataDir?.delete(recursive: true); 205 | } 206 | } catch (_) { 207 | // Silently fail if we can't clean up the profile information. 208 | // It is a system tmp directory so it should get cleaned up eventually. 209 | } 210 | } 211 | } 212 | 213 | class ChromeError extends Error { 214 | final String details; 215 | ChromeError(this.details); 216 | 217 | @override 218 | String toString() => 'ChromeError: $details'; 219 | } 220 | 221 | /// Returns a port that is probably, but not definitely, not in use. 222 | /// 223 | /// This has a built-in race condition: another process may bind this port at 224 | /// any time after this call has returned. 225 | Future findUnusedPort() async { 226 | int port; 227 | ServerSocket socket; 228 | try { 229 | socket = 230 | await ServerSocket.bind(InternetAddress.loopbackIPv6, 0, v6Only: true); 231 | } on SocketException { 232 | socket = await ServerSocket.bind(InternetAddress.loopbackIPv4, 0); 233 | } 234 | port = socket.port; 235 | await socket.close(); 236 | return port; 237 | } 238 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: browser_launcher 2 | version: 1.1.2 3 | description: Provides a standardized way to launch web browsers for testing and tools. 4 | repository: https://github.com/dart-lang/browser_launcher 5 | 6 | environment: 7 | sdk: ^3.4.0 8 | 9 | dependencies: 10 | logging: ^1.0.0 11 | path: ^1.8.0 12 | webkit_inspection_protocol: ^1.0.0 13 | 14 | dev_dependencies: 15 | dart_flutter_team_lints: ^3.0.0 16 | test: ^1.17.3 17 | -------------------------------------------------------------------------------- /test/chrome_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | @OnPlatform({'windows': Skip('appveyor is not setup to install Chrome')}) 6 | library; 7 | 8 | import 'dart:async'; 9 | import 'dart:io'; 10 | 11 | import 'package:browser_launcher/src/chrome.dart'; 12 | import 'package:logging/logging.dart'; 13 | import 'package:test/test.dart'; 14 | import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart'; 15 | 16 | const _headlessOnlyEnvironment = 'HEADLESS_ONLY'; 17 | 18 | bool get headlessOnlyEnvironment => 19 | Platform.environment[_headlessOnlyEnvironment] == 'true'; 20 | 21 | void _configureLogging(bool verbose) { 22 | Logger.root.level = verbose ? Level.ALL : Level.INFO; 23 | Logger.root.onRecord.listen((record) { 24 | print('${record.level.name}: ${record.time}: ${record.message}'); 25 | }); 26 | } 27 | 28 | void main() { 29 | Chrome? chrome; 30 | 31 | // Pass 'true' for debugging. 32 | _configureLogging(false); 33 | 34 | Future getTab(String url) => chrome!.chromeConnection.getTab( 35 | (t) => t.url.contains(url), 36 | retryFor: const Duration(seconds: 5), 37 | ); 38 | 39 | Future?> getTabs() => chrome!.chromeConnection.getTabs( 40 | retryFor: const Duration(seconds: 5), 41 | ); 42 | 43 | Future connectToTab(String url) async { 44 | final tab = await getTab(url); 45 | expect(tab, isNotNull); 46 | return tab!.connect(); 47 | } 48 | 49 | Future openTab(String url) => 50 | chrome!.chromeConnection.getUrl(_openTabUrl(url)); 51 | 52 | Future launchChromeWithDebugPort({ 53 | int port = 0, 54 | String? userDataDir, 55 | bool signIn = false, 56 | bool headless = false, 57 | }) async { 58 | chrome = await Chrome.startWithDebugPort( 59 | [_googleUrl], 60 | debugPort: port, 61 | userDataDir: userDataDir, 62 | signIn: signIn, 63 | headless: headless, 64 | ); 65 | } 66 | 67 | Future launchChrome({bool headless = false}) async { 68 | await Chrome.start([_googleUrl], args: [if (headless) '--headless']); 69 | } 70 | 71 | final headlessModes = [ 72 | true, 73 | if (!headlessOnlyEnvironment) false, 74 | ]; 75 | 76 | for (var headless in headlessModes) { 77 | group('(headless: $headless)', () { 78 | group('chrome with temp data dir', () { 79 | tearDown(() async { 80 | await chrome?.close(); 81 | chrome = null; 82 | }); 83 | 84 | test('can launch chrome', () async { 85 | await launchChrome(headless: headless); 86 | expect(chrome, isNull); 87 | }); 88 | 89 | test('can launch chrome with debug port', () async { 90 | await launchChromeWithDebugPort(headless: headless); 91 | expect(chrome, isNotNull); 92 | }); 93 | 94 | test('has a working debugger', () async { 95 | await launchChromeWithDebugPort(headless: headless); 96 | final tabs = await getTabs(); 97 | expect( 98 | tabs, 99 | contains( 100 | const TypeMatcher() 101 | .having((t) => t.url, 'url', _googleUrl), 102 | ), 103 | ); 104 | }); 105 | 106 | test('uses open debug port if provided port is 0', () async { 107 | await launchChromeWithDebugPort(headless: headless); 108 | expect(chrome!.debugPort, isNot(equals(0))); 109 | }); 110 | 111 | test('can provide a specific debug port', () async { 112 | final port = await findUnusedPort(); 113 | await launchChromeWithDebugPort(port: port, headless: headless); 114 | expect(chrome!.debugPort, port); 115 | }); 116 | }); 117 | 118 | group('chrome with user data dir', () { 119 | late Directory dataDir; 120 | const waitMilliseconds = Duration(milliseconds: 100); 121 | 122 | for (var signIn in [false, true]) { 123 | group('and signIn = $signIn', () { 124 | setUp(() { 125 | dataDir = Directory.systemTemp.createTempSync(_userDataDirName); 126 | }); 127 | 128 | tearDown(() async { 129 | await chrome?.close(); 130 | chrome = null; 131 | 132 | var attempts = 0; 133 | while (true) { 134 | try { 135 | attempts++; 136 | await Future.delayed(waitMilliseconds); 137 | dataDir.deleteSync(recursive: true); 138 | break; 139 | } catch (_) { 140 | if (attempts > 3) rethrow; 141 | } 142 | } 143 | }); 144 | 145 | test('can launch with debug port', () async { 146 | await launchChromeWithDebugPort( 147 | userDataDir: dataDir.path, 148 | signIn: signIn, 149 | headless: headless, 150 | ); 151 | expect(chrome, isNotNull); 152 | }); 153 | 154 | test('has a working debugger', () async { 155 | await launchChromeWithDebugPort( 156 | userDataDir: dataDir.path, 157 | signIn: signIn, 158 | headless: headless, 159 | ); 160 | final tabs = await getTabs(); 161 | expect( 162 | tabs, 163 | contains( 164 | const TypeMatcher() 165 | .having((t) => t.url, 'url', _googleUrl), 166 | ), 167 | ); 168 | }); 169 | 170 | test( 171 | 'has correct profile path', 172 | () async { 173 | await launchChromeWithDebugPort( 174 | userDataDir: dataDir.path, 175 | signIn: signIn, 176 | headless: headless, 177 | ); 178 | await openTab(_chromeVersionUrl); 179 | final wipConnection = await connectToTab(_chromeVersionUrl); 180 | await wipConnection.debugger.enable(); 181 | await wipConnection.runtime.enable(); 182 | final result = await _evaluate( 183 | wipConnection.page, 184 | "document.getElementById('profile_path').textContent", 185 | ); 186 | expect(result, contains(_userDataDirName)); 187 | }, 188 | // Note: When re-enabling, skip for headless mode because headless 189 | // mode does not allow chrome: urls. 190 | skip: 'https://github.com/dart-lang/sdk/issues/52357', 191 | ); 192 | }); 193 | } 194 | }); 195 | }); 196 | } 197 | } 198 | 199 | String _openTabUrl(String url) => '/json/new?$url'; 200 | 201 | Future _evaluate(WipPage page, String expression) async { 202 | String? result; 203 | const stopInSeconds = Duration(seconds: 5); 204 | const waitMilliseconds = Duration(milliseconds: 100); 205 | final stopTime = DateTime.now().add(stopInSeconds); 206 | 207 | while (result == null && DateTime.now().isBefore(stopTime)) { 208 | await Future.delayed(waitMilliseconds); 209 | try { 210 | final wipResponse = await page.sendCommand( 211 | 'Runtime.evaluate', 212 | params: {'expression': expression}, 213 | ); 214 | final response = wipResponse.json['result'] as Map; 215 | final value = (response['result'] as Map)['value']; 216 | result = value?.toString(); 217 | } catch (_) { 218 | return null; 219 | } 220 | } 221 | return result; 222 | } 223 | 224 | const _googleUrl = 'https://www.google.com/'; 225 | const _chromeVersionUrl = 'chrome://version/'; 226 | const _userDataDirName = 'data dir'; 227 | --------------------------------------------------------------------------------