diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ddeff53..1c727e41 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +# 1.12.0 (2019-08-28) +- Fix bug in `puppeteer.connect()` +- Add the same capabilities that pupeeteer Node.JS to `puppeteer.launch` for the management of the flags passed to Chromium. +- Add `userDataDir` to `puppeteer.launch` to allow managing the user data directory. + By default, we now use a temporary data directory in the system temp folder. +- Add more tests for launching and connecting to chromium + # 1.11.0 (2019-08-15) - Update Chromium version to 686378 diff --git a/README.md b/README.md index 4ea73dfe..e7ceb83c 100644 --- a/README.md +++ b/README.md @@ -176,7 +176,7 @@ import 'package:puppeteer/puppeteer.dart'; main() async { var browser = await puppeteer.launch(); var page = await browser.newPage(); - await page.goto('https://www.w3.org'); + await page.goto('https://w3c.github.io/'); // Either use the helper to get the content var pageContent = await page.content; @@ -347,8 +347,8 @@ main() { } ``` -Note: In a future version, we can image to compile the dart code to javascript on the fly before -sending it to the browser (with ddc or dart2js). +Note: In a future version, we can imagine writing the code in Dart and it would be compiled to javascript transparently + (with ddc or dart2js). ## Related work * [chrome-remote-interface](https://github.com/cyrus-and/chrome-remote-interface) diff --git a/README.template.md b/README.template.md index 5c3b9ff0..59bcc834 100644 --- a/README.template.md +++ b/README.template.md @@ -129,8 +129,8 @@ main() { } ``` -Note: In a future version, we can image to compile the dart code to javascript on the fly before -sending it to the browser (with ddc or dart2js). +Note: In a future version, we can imagine writing the code in Dart and it would be compiled to javascript transparently + (with ddc or dart2js). ## Related work * [chrome-remote-interface](https://github.com/cyrus-and/chrome-remote-interface) diff --git a/doc/api.md b/doc/api.md index 3f5e2521..c67691a1 100644 --- a/doc/api.md +++ b/doc/api.md @@ -272,7 +272,7 @@ main() async { ``` Parameters: - - `ignoreHTTPSErrors`: Whether to ignore HTTPS errors during navigation. + - `ignoreHttpsErrors`: Whether to ignore HTTPS errors during navigation. Defaults to `false`. - `headless`: Whether to run browser in [headless mode](https://developers.google.com/web/updates/2017/04/headless-chrome). Defaults to `true` unless the `devtools` option is `true`. @@ -290,9 +290,14 @@ Parameters: Defaults to `Platform.environment`. - `devtools` Whether to auto-open a DevTools panel for each tab. If this option is `true`, the `headless` option will be set `false`. + - `ignoreDefaultArgs` <[boolean]|[List]<[string]>> If `true`, then do not + use [`puppeteer.defaultArgs()`]. If a list is given, then filter out + the given default arguments. Dangerous option; use with care. Defaults to `false`. + - `userDataDir` <[string]> Path to a [User Data Directory](https://chromium.googlesource.com/chromium/src/+/master/docs/user_data_dir.md). + - `timeout` Maximum time to wait for the browser instance to start. Defaults to 30 seconds. ```dart -puppeteer.launch({String executablePath, bool headless, bool devTools, bool useTemporaryUserData, bool noSandboxFlag, DeviceViewport defaultViewport = LaunchOptions.viewportNotSpecified, bool ignoreHttpsErrors, Duration slowMo, List args, Map environment, List plugins}) → Future +puppeteer.launch({String executablePath, bool headless, bool devTools, String userDataDir, bool noSandboxFlag, DeviceViewport defaultViewport = LaunchOptions.viewportNotSpecified, bool ignoreHttpsErrors, Duration slowMo, List args, dynamic ignoreDefaultArgs, Map environment, List plugins, Duration timeout}) → Future ``` ### class: Browser diff --git a/example/capture_spa.dart b/example/capture_spa.dart index 20838b8c..7a5ef30b 100644 --- a/example/capture_spa.dart +++ b/example/capture_spa.dart @@ -3,7 +3,7 @@ import 'package:puppeteer/puppeteer.dart'; main() async { var browser = await puppeteer.launch(); var page = await browser.newPage(); - await page.goto('https://www.w3.org'); + await page.goto('https://w3c.github.io/'); // Either use the helper to get the content var pageContent = await page.content; diff --git a/lib/src/browser.dart b/lib/src/browser.dart index de08c1eb..e2f22e9e 100644 --- a/lib/src/browser.dart +++ b/lib/src/browser.dart @@ -138,7 +138,9 @@ class Browser { target.initialized.then((initialized) { if (initialized) { - _onTargetCreatedController.add(target); + if (!_onTargetCreatedController.isClosed) { + _onTargetCreatedController.add(target); + } } }); } @@ -251,12 +253,13 @@ class Browser { await Future.delayed(Duration.zero); await _closeCallback(); - _dispose(); await connection.dispose('Browser.close'); + _dispose(); } void disconnect() { connection.dispose('Browser.disconnect'); + _dispose(); } bool get isConnected { diff --git a/lib/src/page/page.dart b/lib/src/page/page.dart index 74608bf6..ce84d762 100644 --- a/lib/src/page/page.dart +++ b/lib/src/page/page.dart @@ -176,6 +176,9 @@ class Page { onClose.then((_) { _dispose('Page.onClose completed'); }); + browser.disconnected.then((_) { + _dispose('Browser.disconnected completed'); + }); } static Future create(Target target, Session session, diff --git a/lib/src/puppeteer.dart b/lib/src/puppeteer.dart index 4156bbfa..a49bf1df 100644 --- a/lib/src/puppeteer.dart +++ b/lib/src/puppeteer.dart @@ -22,6 +22,7 @@ final List _defaultArgs = [ '--disable-backgrounding-occluded-windows', '--disable-breakpad', '--disable-client-side-phishing-detection', + '--disable-component-extensions-with-background-pages', '--disable-default-apps', '--disable-dev-shm-usage', '--disable-extensions', @@ -39,7 +40,6 @@ final List _defaultArgs = [ '--enable-automation', '--password-store=basic', '--use-mock-keychain', - '--remote-debugging-port=0', ]; final List _headlessArgs = [ @@ -70,7 +70,7 @@ class Puppeteer { /// ``` /// /// Parameters: - /// - `ignoreHTTPSErrors`: Whether to ignore HTTPS errors during navigation. + /// - `ignoreHttpsErrors`: Whether to ignore HTTPS errors during navigation. /// Defaults to `false`. /// - `headless`: Whether to run browser in [headless mode](https://developers.google.com/web/updates/2017/04/headless-chrome). /// Defaults to `true` unless the `devtools` option is `true`. @@ -88,50 +88,58 @@ class Puppeteer { /// Defaults to `Platform.environment`. /// - `devtools` Whether to auto-open a DevTools panel for each tab. If this /// option is `true`, the `headless` option will be set `false`. + /// - `ignoreDefaultArgs` <[boolean]|[List]<[string]>> If `true`, then do not + /// use [`puppeteer.defaultArgs()`]. If a list is given, then filter out + /// the given default arguments. Dangerous option; use with care. Defaults to `false`. + /// - `userDataDir` <[string]> Path to a [User Data Directory](https://chromium.googlesource.com/chromium/src/+/master/docs/user_data_dir.md). + /// - `timeout` Maximum time to wait for the browser instance to start. Defaults to 30 seconds. Future launch( {String executablePath, bool headless, bool devTools, - bool useTemporaryUserData, + String userDataDir, bool noSandboxFlag, DeviceViewport defaultViewport = LaunchOptions.viewportNotSpecified, bool ignoreHttpsErrors, Duration slowMo, List args, + /* bool | List */ dynamic ignoreDefaultArgs, Map environment, - List plugins}) async { - useTemporaryUserData ??= true; + List plugins, + Duration timeout}) async { devTools ??= false; headless ??= !devTools; - // In docker environment we want to force the '--no-sandbox' flag automatically - noSandboxFlag ??= Platform.environment['CHROME_FORCE_NO_SANDBOX'] == 'true'; - - executablePath = await _inferExecutablePath(); - - Directory userDataDir; - if (useTemporaryUserData) { - userDataDir = await Directory.systemTemp.createTemp('chrome_'); - } - - var chromeArgs = _defaultArgs.toList(); - if (args != null) { + timeout ??= Duration(seconds: 30); + + var chromeArgs = []; + var defaultArguments = defaultArgs( + args: args, + userDataDir: userDataDir, + devTools: devTools, + headless: headless, + noSandboxFlag: noSandboxFlag); + if (ignoreDefaultArgs == null) { + chromeArgs.addAll(defaultArguments); + } else if (ignoreDefaultArgs is List) { + chromeArgs.addAll( + defaultArguments.where((arg) => !ignoreDefaultArgs.contains(arg))); + } else if (args != null) { chromeArgs.addAll(args); } - if (userDataDir != null) { - chromeArgs.add('--user-data-dir=${userDataDir.path}'); + if (!chromeArgs.any((a) => a.startsWith('--remote-debugging-'))) { + chromeArgs.add('--remote-debugging-port=0'); } - if (headless) { - chromeArgs.addAll(_headlessArgs); - } - if (noSandboxFlag) { - chromeArgs.add('--no-sandbox'); - } - if (devTools) { - chromeArgs.add('--auto-open-devtools-for-tabs'); + Directory temporaryUserDataDir; + if (!chromeArgs.any((a) => a.startsWith('--user-data-dir'))) { + temporaryUserDataDir = + await Directory.systemTemp.createTemp('puppeteer_dev_profile-'); + chromeArgs.add('--user-data-dir=${temporaryUserDataDir.path}'); } + executablePath ??= await _inferExecutablePath(); + var launchOptions = LaunchOptions(args: chromeArgs, defaultViewport: defaultViewport); @@ -148,23 +156,39 @@ class Puppeteer { environment: environment); // ignore: unawaited_futures - chromeProcess.exitCode.then((exitCode) { + var chromeProcessExit = chromeProcess.exitCode.then((exitCode) { _logger.info('Chrome exit with $exitCode.'); - if (userDataDir != null) { - _logger.info('Clean ${userDataDir.path}'); - userDataDir.deleteSync(recursive: true); + if (temporaryUserDataDir != null) { + try { + _logger.info('Clean ${temporaryUserDataDir.path}'); + temporaryUserDataDir.deleteSync(recursive: true); + } catch (error) { + _logger.info('Delete temporary file failed', error); + } } }); - var webSocketUrl = await _waitForWebSocketUrl(chromeProcess); + var webSocketUrl = await _waitForWebSocketUrl(chromeProcess) + .timeout(timeout, onTimeout: () => null); if (webSocketUrl != null) { var connection = await Connection.create(webSocketUrl, delay: slowMo); var browser = createBrowser(chromeProcess, connection, defaultViewport: launchOptions.computedDefaultViewport, - closeCallback: () => _killChrome(chromeProcess), - ignoreHttpsErrors: ignoreHttpsErrors, - plugins: allPlugins); + closeCallback: () async { + if (temporaryUserDataDir != null) { + await _killChrome(chromeProcess); + } else { + // If there is a custom data-directory we need to give chrome a chance + // to save the last data + // Attempt to close chrome gracefully + await connection.send('Browser.close').catchError((error) async { + await _killChrome(chromeProcess); + }); + } + + return chromeProcessExit; + }, ignoreHttpsErrors: ignoreHttpsErrors, plugins: allPlugins); var targetFuture = browser.waitForTarget((target) => target.type == 'page'); await browser.targetApi.setDiscoverTargets(true); @@ -218,16 +242,40 @@ class Puppeteer { } var browserContextIds = await connection.targetApi.getBrowserContexts(); - return createBrowser(null, connection, + var browser = createBrowser(null, connection, browserContextIds: browserContextIds, ignoreHttpsErrors: ignoreHttpsErrors, defaultViewport: connectOptions.computedDefaultViewport, plugins: allPlugins, closeCallback: () => connection.send('Browser.close').catchError((e) => null)); + await browser.targetApi.setDiscoverTargets(true); + return browser; } Devices get devices => devices_lib.devices; + + List defaultArgs( + {bool devTools, + bool headless, + List args, + String userDataDir, + bool noSandboxFlag}) { + devTools ??= false; + headless ??= !devTools; + // In docker environment we want to force the '--no-sandbox' flag automatically + noSandboxFlag ??= Platform.environment['CHROME_FORCE_NO_SANDBOX'] == 'true'; + + return [ + ..._defaultArgs, + if (userDataDir != null) '--user-data-dir=$userDataDir', + if (noSandboxFlag) '--no-sandbox', + if (devTools) '--auto-open-devtools-for-tabs', + if (headless) ..._headlessArgs, + if (args == null || args.every((a) => a.startsWith('-'))) 'about:blank', + ...?args + ]; + } } Future _wsEndpoint(String browserURL) async { diff --git a/pubspec.yaml b/pubspec.yaml index 703392de..9149fd67 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: puppeteer description: A high-level API to control headless Chrome over the DevTools Protocol. This is a port of Puppeteer in Dart. -version: 1.11.0 +version: 1.12.0 homepage: https://github.com/xavierhainaux/puppeteer-dart author: Xavier Hainaux diff --git a/test/downloader_test.dart b/test/downloader_test.dart index c4dca113..30b1a446 100644 --- a/test/downloader_test.dart +++ b/test/downloader_test.dart @@ -9,8 +9,8 @@ main() { expect(revision.executablePath, contains('.local-chromium-test')); expect(File(revision.executablePath).existsSync(), isTrue); - var browser = await puppeteer.launch( - executablePath: revision.executablePath, useTemporaryUserData: true); + var browser = + await puppeteer.launch(executablePath: revision.executablePath); var page = await browser.newPage(); await page.close(); await browser.close(); diff --git a/test/launcher_test.dart b/test/launcher_test.dart new file mode 100644 index 00000000..3184aae6 --- /dev/null +++ b/test/launcher_test.dart @@ -0,0 +1,363 @@ +import 'dart:async'; +import 'dart:io'; +import 'package:puppeteer/puppeteer.dart'; +import 'package:shelf/shelf.dart' as shelf; +import 'package:test/test.dart'; +import 'utils/utils.dart'; + +main() { + Server server; + setUpAll(() async { + server = await Server.create(); + }); + + tearDownAll(() async { + await server.close(); + }); + + tearDown(() async { + server.clearRoutes(); + }); + group('Puppeteer', () { + group('Browser.disconnect', () { + test('should reject navigation when browser closes', () async { + server.setRoute('/one-style.css', (request) { + return Completer().future; + }); + var browser = await puppeteer.launch(); + try { + var remote = + await puppeteer.connect(browserWsEndpoint: browser.wsEndpoint); + var page = await remote.newPage(); + var navigationPromise = page + .goto('${server.prefix}/one-style.html', + timeout: const Duration(seconds: 60000)) + .then((e) => e) + .catchError((e) => e); + await server.waitForRequest('/one-style.css'); + remote.disconnect(); + var error = await navigationPromise; + expect( + error.toString(), + equals( + 'Exception: Navigation failed because browser has disconnected!')); + } finally { + await browser.close(); + } + }); + test('should reject waitForSelector when browser closes', () async { + server.setRoute( + '/empty.html', (request) => Completer().future); + var browser = await puppeteer.launch(); + var remote = + await puppeteer.connect(browserWsEndpoint: browser.wsEndpoint); + var page = await remote.newPage(); + var watchdog = page + .waitForSelector('div', + timeout: const Duration(milliseconds: 60000)) + .then((e) => e) + .catchError((e) => e); + remote.disconnect(); + var error = await watchdog; + expect(error.toString(), contains('Protocol error')); + await browser.close(); + }); + }); + group('Browser.close', () { + test('should terminate network waiters', () async { + var browser = await puppeteer.launch(); + var remote = + await puppeteer.connect(browserWsEndpoint: browser.wsEndpoint); + var newPage = await remote.newPage(); + var results = await Future.wait([ + newPage + .waitForRequest(server.emptyPage) + .then((e) => e) + .catchError((e) => e), + newPage + .waitForResponse(server.emptyPage) + .then((e) => e) + .catchError((e) => e), + browser.close() + ]); + for (var i = 0; i < 2; i++) { + var message = results[i].message; + expect(message, contains('No element')); + expect(message, isNot(contains('Timeout'))); + } + }); + }); + group('Puppeteer.launch', () { + test('should reject all promises when browser is closed', () async { + var browser = await puppeteer.launch(); + var page = await browser.newPage(); + var neverResolves = + page.evaluate('() => new Promise(r => {})').catchError((e) => e); + await browser.close(); + var error = await neverResolves; + expect(error, isA()); + }); + test('should reject if executable path is invalid', () { + expect(() => puppeteer.launch(executablePath: 'random-invalid-path'), + throwsA(predicate((e) => '$e'.contains('ProcessException: ')))); + }); + test('userDataDir option', () async { + var userDataDir = Directory.systemTemp.createTempSync('chrome'); + var browser = await puppeteer.launch(userDataDir: userDataDir.path); + // Open a page to make sure its functional. + await browser.newPage(); + expect( + userDataDir.listSync(recursive: true), hasLength(greaterThan(0))); + await browser.close(); + expect( + userDataDir.listSync(recursive: true), hasLength(greaterThan(0))); + _tryDeleteDirectory(userDataDir); + }); + test('userDataDir argument', () async { + var userDataDir = Directory.systemTemp.createTempSync('chrome'); + var args = ['--user-data-dir=${userDataDir.path}']; + + var browser = await puppeteer.launch(args: args); + await browser.newPage(); + expect( + userDataDir.listSync(recursive: true), hasLength(greaterThan(0))); + await browser.close(); + expect( + userDataDir.listSync(recursive: true), hasLength(greaterThan(0))); + _tryDeleteDirectory(userDataDir); + }); + test('userDataDir option should restore state', () async { + var userDataDir = Directory.systemTemp.createTempSync('chrome'); + try { + var browser = + await puppeteer.launch(userDataDir: userDataDir.absolute.path); + var page = await browser.newPage(); + await page.goto(server.emptyPage); + await page.evaluate("() => localStorage.hey = 'hello'"); + await browser.close(); + + var browser2 = + await puppeteer.launch(userDataDir: userDataDir.absolute.path); + var page2 = await browser2.newPage(); + await page2.goto(server.emptyPage); + expect( + await page2.evaluate('() => localStorage.hey'), equals('hello')); + await browser2.close(); + } finally { + _tryDeleteDirectory(userDataDir); + } + }); + test('userDataDir option should restore cookies', () async { + var userDataDir = Directory.systemTemp.createTempSync('chrome'); + try { + var browser = await puppeteer.launch(userDataDir: userDataDir.path); + var page = await browser.newPage(); + await page.goto(server.emptyPage); + await page.evaluate( + "() => document.cookie = 'doSomethingOnlyOnce=true; expires=Fri, 31 Dec 9999 23:59:59 GMT'"); + await browser.close(); + + var browser2 = await puppeteer.launch(userDataDir: userDataDir.path); + var page2 = await browser2.newPage(); + await page2.goto(server.emptyPage); + expect(await page2.evaluate('() => document.cookie'), + equals('doSomethingOnlyOnce=true')); + await browser2.close(); + } finally { + _tryDeleteDirectory(userDataDir); + } + }, onPlatform: { + "windows": Skip( + 'This mysteriously fails on Windows. See https://github.com/GoogleChrome/puppeteer/issues/4111') + }); + test('should return the default arguments', () { + expect(puppeteer.defaultArgs(), contains('--no-first-run')); + expect(puppeteer.defaultArgs(), contains('--headless')); + expect(puppeteer.defaultArgs(headless: false), + isNot(contains('--headless'))); + expect(puppeteer.defaultArgs(userDataDir: 'foo'), + contains('--user-data-dir=foo')); + }); + test('should work with no default arguments', () async { + var browser = await puppeteer.launch(ignoreDefaultArgs: true); + var page = await browser.newPage(); + expect(await page.evaluate('11 * 11'), equals(121)); + await page.close(); + await browser.close(); + }, skip: true); + test('should filter out ignored default arguments', () async { + //TODO(xha): implement the feature and find a way to test it; + }); + test('should have default url when launching browser', () async { + var browser = await puppeteer.launch(); + var pages = (await browser.pages).map((page) => page.url); + expect(pages, equals(['about:blank'])); + await browser.close(); + }); + test('should have custom url when launching browser', () async { + var browser = await puppeteer.launch(args: [server.emptyPage]); + var pages = await browser.pages; + expect(pages.length, equals(1)); + if (pages[0].url != server.emptyPage) { + await pages[0].waitForNavigation(); + } + expect(pages[0].url, equals(server.emptyPage)); + await browser.close(); + }); + test('should set the default viewport', () async { + var browser = await puppeteer.launch( + defaultViewport: DeviceViewport(width: 456, height: 789)); + var page = await browser.newPage(); + expect(await page.evaluate('window.innerWidth'), equals(456)); + expect(await page.evaluate('window.innerHeight'), equals(789)); + await browser.close(); + }); + test('should disable the default viewport', () async { + var browser = await puppeteer.launch(defaultViewport: null); + var page = await browser.newPage(); + expect(page.viewport, isNull); + await browser.close(); + }); + test('should take fullPage screenshots when defaultViewport is null', + () async { + var browser = await puppeteer.launch(defaultViewport: null); + var page = await browser.newPage(); + await page.goto('${server.prefix}/grid.html'); + var screenshot = await page.screenshot(fullPage: true); + expect(screenshot, isNotNull); + await browser.close(); + }); + }); + group('Puppeteer.connect', () { + test('should be able to connect multiple times to the same browser', + () async { + var originalBrowser = await puppeteer.launch(); + var browser = await puppeteer.connect( + browserWsEndpoint: originalBrowser.wsEndpoint); + var page = await browser.newPage(); + expect(await page.evaluate('() => 7 * 8'), equals(56)); + browser.disconnect(); + + var secondPage = await originalBrowser.newPage(); + expect(await secondPage.evaluate('() => 7 * 6'), equals(42), + reason: 'original browser should still work'); + await originalBrowser.close(); + }); + test('should be able to close remote browser', () async { + var originalBrowser = await puppeteer.launch(); + var remoteBrowser = await puppeteer.connect( + browserWsEndpoint: originalBrowser.wsEndpoint); + await Future.wait([ + originalBrowser.disconnected, + remoteBrowser.close(), + ]); + }); + + test('should support ignoreHTTPSErrors option', () async { + //TODO(xha): enable once we support https server + }); + test('should be able to reconnect to a disconnected browser', () async { + var originalBrowser = await puppeteer.launch(); + var browserWsEndpoint = originalBrowser.wsEndpoint; + var page = await originalBrowser.newPage(); + await page.goto('${server.prefix}/frames/nested-frames.html'); + originalBrowser.disconnect(); + + var browser = + await puppeteer.connect(browserWsEndpoint: browserWsEndpoint); + var pages = await browser.pages; + var restoredPage = pages.firstWhere( + (page) => page.url == '${server.prefix}/frames/nested-frames.html'); + expect( + dumpFrames(restoredPage.mainFrame), + equals([ + 'http:///frames/nested-frames.html', + ' http:///frames/two-frames.html (2frames)', + ' http:///frames/frame.html (uno)', + ' http:///frames/frame.html (dos)', + ' http:///frames/frame.html (aframe)', + ])); + expect(await restoredPage.evaluate('() => 7 * 8'), equals(56)); + await browser.close(); + }); + }); + // @see https://github.com/GoogleChrome/puppeteer/issues/4197#issuecomment-481793410 + test('should be able to connect to the same page simultaneously', () async { + var browserOne = await puppeteer.launch(); + var browserTwo = + await puppeteer.connect(browserWsEndpoint: browserOne.wsEndpoint); + var pages = await Future.wait([ + browserOne.onTargetCreated.first.then((target) => target.page), + browserTwo.newPage(), + ]); + expect(await pages[0].evaluate('() => 7 * 8'), equals(56)); + expect(await pages[1].evaluate('() => 7 * 6'), equals(42)); + await browserOne.close(); + }); + }); + + group('Browser target events', () { + test('should work', () async { + var browser = await puppeteer.launch(); + var events = []; + browser.onTargetCreated.listen((_) => events.add('CREATED')); + browser.onTargetChanged.listen((_) => events.add('CHANGED')); + browser.onTargetDestroyed.listen((_) => events.add('DESTROYED')); + var page = await browser.newPage(); + await page.goto(server.emptyPage); + await page.close(); + expect(events, equals(['CREATED', 'CHANGED', 'DESTROYED'])); + await browser.close(); + }); + }); + + group('Browser.Events.disconnected', () { + test( + 'should be emitted when: browser gets closed, disconnected or underlying websocket gets closed', + () async { + var originalBrowser = await puppeteer.launch(); + var browserWSEndpoint = originalBrowser.wsEndpoint; + var remoteBrowser1 = + await puppeteer.connect(browserWsEndpoint: browserWSEndpoint); + var remoteBrowser2 = + await puppeteer.connect(browserWsEndpoint: browserWSEndpoint); + + var disconnectedOriginal = 0; + var disconnectedRemote1 = 0; + var disconnectedRemote2 = 0; + originalBrowser.disconnected + .asStream() + .listen((_) => ++disconnectedOriginal); + remoteBrowser1.disconnected + .asStream() + .listen((_) => ++disconnectedRemote1); + remoteBrowser2.disconnected + .asStream() + .listen((_) => ++disconnectedRemote2); + + var disconnectedFuture = remoteBrowser2.disconnected; + remoteBrowser2.disconnect(); + await disconnectedFuture; + + expect(disconnectedOriginal, equals(0)); + expect(disconnectedRemote1, equals(0)); + expect(disconnectedRemote2, equals(1)); + + await Future.wait([ + remoteBrowser1.disconnected, + originalBrowser.disconnected, + originalBrowser.close(), + ]); + + expect(disconnectedOriginal, equals(1)); + expect(disconnectedRemote1, equals(1)); + expect(disconnectedRemote2, equals(1)); + }); + }); +} + +void _tryDeleteDirectory(Directory directory) { + try { + directory.deleteSync(recursive: true); + } catch (_) {} +} diff --git a/test/page_test.dart b/test/page_test.dart index c4e819c5..5ea2ce7a 100644 --- a/test/page_test.dart +++ b/test/page_test.dart @@ -80,6 +80,23 @@ main() { await newPage.close(); expect(newPage.isClosed, isTrue); }); + test('should terminate network waiters', () async { + var newPage = await context.newPage(); + var results = await Future.wait([ + newPage + .waitForRequest(server.emptyPage) + .then((e) => e) + .catchError((e) => e), + newPage + .waitForResponse(server.emptyPage) + .then((e) => e) + .catchError((e) => e), + newPage.close(), + ]); + for (var i = 0; i < 2; i++) { + expect(results[i], isA()); + } + }); }); group('Page.Events.Load', () { test('should fire when expected', () async { diff --git a/test/test_all.dart b/test/test_all.dart index 22b27545..72f8af7e 100644 --- a/test/test_all.dart +++ b/test/test_all.dart @@ -19,6 +19,7 @@ import 'input_file_test.dart' as input_file_test; import 'javascript_parser_test.dart' as javascript_parser_test; import 'js_handle_test.dart' as js_handle_test; import 'keyboard_test.dart' as keyboard_test; +import 'launcher_test.dart' as launcher_test; import 'mouse_test.dart' as mouse_test; import 'navigation_test.dart' as navigation_test; import 'network_test.dart' as network_test; @@ -57,6 +58,7 @@ main() { group('javascript_parser_test', javascript_parser_test.main); group('js_handle_test', js_handle_test.main); group('keyboard_test', keyboard_test.main); + group('launcher_test', launcher_test.main); group('mouse_test', mouse_test.main); group('navigation_test', navigation_test.main); group('network_test', network_test.main); diff --git a/tool/replace_for_test.dart b/tool/replace_for_test.dart index e9b80833..4a8d361d 100644 --- a/tool/replace_for_test.dart +++ b/tool/replace_for_test.dart @@ -40,186 +40,394 @@ main() { } final _input = r''' - describe_fails_ffox('Page.waitForFileChooser', function() { - it('should work when file input is attached to DOM', async({page, server}) => { - await page.setContent(``); - const [chooser] = await Promise.all([ - page.waitForFileChooser(), - page.click('input'), - ]); - expect(chooser).toBeTruthy(); - }); - it('should work when file input is not attached to DOM', async({page, server}) => { - const [chooser] = await Promise.all([ - page.waitForFileChooser(), - page.evaluate(() => { - const el = document.createElement('input'); - el.type = 'file'; - el.click(); - }), - ]); - expect(chooser).toBeTruthy(); + describe('Puppeteer', function() { + describe('BrowserFetcher', function() { + it('should download and extract linux binary', async({server}) => { + const downloadsFolder = await mkdtempAsync(TMP_FOLDER); + const browserFetcher = puppeteer.createBrowserFetcher({ + platform: 'linux', + path: downloadsFolder, + host: server.PREFIX + }); + let revisionInfo = browserFetcher.revisionInfo('123456'); + server.setRoute(revisionInfo.url.substring(server.PREFIX.length), (req, res) => { + server.serveFile(req, res, '/chromium-linux.zip'); + }); + + expect(revisionInfo.local).toBe(false); + expect(browserFetcher.platform()).toBe('linux'); + expect(await browserFetcher.canDownload('100000')).toBe(false); + expect(await browserFetcher.canDownload('123456')).toBe(true); + + revisionInfo = await browserFetcher.download('123456'); + expect(revisionInfo.local).toBe(true); + expect(await readFileAsync(revisionInfo.executablePath, 'utf8')).toBe('LINUX BINARY\n'); + const expectedPermissions = os.platform() === 'win32' ? 0666 : 0755; + expect((await statAsync(revisionInfo.executablePath)).mode & 0777).toBe(expectedPermissions); + expect(await browserFetcher.localRevisions()).toEqual(['123456']); + await browserFetcher.remove('123456'); + expect(await browserFetcher.localRevisions()).toEqual([]); + await rmAsync(downloadsFolder); + }); }); - it('should respect timeout', async({page, server}) => { - let error = null; - await page.waitForFileChooser({timeout: 1}).catch(e => error = e); - expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError); + describe('Browser.disconnect', function() { + it('should reject navigation when browser closes', async({server}) => { + server.setRoute('/one-style.css', () => {}); + const browser = await puppeteer.launch(defaultBrowserOptions); + const remote = await puppeteer.connect({browserWSEndpoint: browser.wsEndpoint()}); + const page = await remote.newPage(); + const navigationPromise = page.goto(server.PREFIX + '/one-style.html', {timeout: 60000}).catch(e => e); + await server.waitForRequest('/one-style.css'); + remote.disconnect(); + const error = await navigationPromise; + expect(error.message).toBe('Navigation failed because browser has disconnected!'); + await browser.close(); + }); + it('should reject waitForSelector when browser closes', async({server}) => { + server.setRoute('/empty.html', () => {}); + const browser = await puppeteer.launch(defaultBrowserOptions); + const remote = await puppeteer.connect({browserWSEndpoint: browser.wsEndpoint()}); + const page = await remote.newPage(); + const watchdog = page.waitForSelector('div', {timeout: 60000}).catch(e => e); + remote.disconnect(); + const error = await watchdog; + expect(error.message).toContain('Protocol error'); + await browser.close(); + }); }); - it('should respect default timeout when there is no custom timeout', async({page, server}) => { - page.setDefaultTimeout(1); - let error = null; - await page.waitForFileChooser().catch(e => error = e); - expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError); + describe('Browser.close', function() { + it('should terminate network waiters', async({context, server}) => { + const browser = await puppeteer.launch(defaultBrowserOptions); + const remote = await puppeteer.connect({browserWSEndpoint: browser.wsEndpoint()}); + const newPage = await remote.newPage(); + const results = await Promise.all([ + newPage.waitForRequest(server.EMPTY_PAGE).catch(e => e), + newPage.waitForResponse(server.EMPTY_PAGE).catch(e => e), + browser.close() + ]); + for (let i = 0; i < 2; i++) { + const message = results[i].message; + expect(message).toContain('Target closed'); + expect(message).not.toContain('Timeout'); + } + }); }); - it('should prioritize exact timeout over default timeout', async({page, server}) => { - page.setDefaultTimeout(0); - let error = null; - await page.waitForFileChooser({timeout: 1}).catch(e => error = e); - expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError); + describe('Puppeteer.launch', function() { + it('should reject all promises when browser is closed', async() => { + const browser = await puppeteer.launch(defaultBrowserOptions); + const page = await browser.newPage(); + let error = null; + const neverResolves = page.evaluate(() => new Promise(r => {})).catch(e => error = e); + await browser.close(); + await neverResolves; + expect(error.message).toContain('Protocol error'); + }); + it('should reject if executable path is invalid', async({server}) => { + let waitError = null; + const options = Object.assign({}, defaultBrowserOptions, {executablePath: 'random-invalid-path'}); + await puppeteer.launch(options).catch(e => waitError = e); + expect(waitError.message).toContain('Failed to launch'); + }); + it('userDataDir option', async({server}) => { + const userDataDir = await mkdtempAsync(TMP_FOLDER); + const options = Object.assign({userDataDir}, defaultBrowserOptions); + const browser = await puppeteer.launch(options); + // Open a page to make sure its functional. + await browser.newPage(); + expect(fs.readdirSync(userDataDir).length).toBeGreaterThan(0); + await browser.close(); + expect(fs.readdirSync(userDataDir).length).toBeGreaterThan(0); + // This might throw. See https://github.com/GoogleChrome/puppeteer/issues/2778 + await rmAsync(userDataDir).catch(e => {}); + }); + it('userDataDir argument', async({server}) => { + const userDataDir = await mkdtempAsync(TMP_FOLDER); + const options = Object.assign({}, defaultBrowserOptions); + if (CHROME) { + options.args = [ + ...(defaultBrowserOptions.args || []), + `--user-data-dir=${userDataDir}` + ]; + } else { + options.args = [ + ...(defaultBrowserOptions.args || []), + `-profile`, + userDataDir, + ]; + } + const browser = await puppeteer.launch(options); + expect(fs.readdirSync(userDataDir).length).toBeGreaterThan(0); + await browser.close(); + expect(fs.readdirSync(userDataDir).length).toBeGreaterThan(0); + // This might throw. See https://github.com/GoogleChrome/puppeteer/issues/2778 + await rmAsync(userDataDir).catch(e => {}); + }); + it('userDataDir option should restore state', async({server}) => { + const userDataDir = await mkdtempAsync(TMP_FOLDER); + const options = Object.assign({userDataDir}, defaultBrowserOptions); + const browser = await puppeteer.launch(options); + const page = await browser.newPage(); + await page.goto(server.EMPTY_PAGE); + await page.evaluate(() => localStorage.hey = 'hello'); + await browser.close(); + + const browser2 = await puppeteer.launch(options); + const page2 = await browser2.newPage(); + await page2.goto(server.EMPTY_PAGE); + expect(await page2.evaluate(() => localStorage.hey)).toBe('hello'); + await browser2.close(); + // This might throw. See https://github.com/GoogleChrome/puppeteer/issues/2778 + await rmAsync(userDataDir).catch(e => {}); + }); + // This mysteriously fails on Windows on AppVeyor. See https://github.com/GoogleChrome/puppeteer/issues/4111 + xit('userDataDir option should restore cookies', async({server}) => { + const userDataDir = await mkdtempAsync(TMP_FOLDER); + const options = Object.assign({userDataDir}, defaultBrowserOptions); + const browser = await puppeteer.launch(options); + const page = await browser.newPage(); + await page.goto(server.EMPTY_PAGE); + await page.evaluate(() => document.cookie = 'doSomethingOnlyOnce=true; expires=Fri, 31 Dec 9999 23:59:59 GMT'); + await browser.close(); + + const browser2 = await puppeteer.launch(options); + const page2 = await browser2.newPage(); + await page2.goto(server.EMPTY_PAGE); + expect(await page2.evaluate(() => document.cookie)).toBe('doSomethingOnlyOnce=true'); + await browser2.close(); + // This might throw. See https://github.com/GoogleChrome/puppeteer/issues/2778 + await rmAsync(userDataDir).catch(e => {}); + }); + it('should return the default arguments', async() => { + if (CHROME) { + expect(puppeteer.defaultArgs()).toContain('--no-first-run'); + expect(puppeteer.defaultArgs()).toContain('--headless'); + expect(puppeteer.defaultArgs({headless: false})).not.toContain('--headless'); + expect(puppeteer.defaultArgs({userDataDir: 'foo'})).toContain('--user-data-dir=foo'); + } else { + expect(puppeteer.defaultArgs()).toContain('-headless'); + expect(puppeteer.defaultArgs({headless: false})).not.toContain('-headless'); + expect(puppeteer.defaultArgs({userDataDir: 'foo'})).toContain('-profile'); + expect(puppeteer.defaultArgs({userDataDir: 'foo'})).toContain('foo'); + } + }); + it('should work with no default arguments', async() => { + const options = Object.assign({}, defaultBrowserOptions); + options.ignoreDefaultArgs = true; + const browser = await puppeteer.launch(options); + const page = await browser.newPage(); + expect(await page.evaluate('11 * 11')).toBe(121); + await page.close(); + await browser.close(); + }); + it('should filter out ignored default arguments', async() => { + // Make sure we launch with `--enable-automation` by default. + const defaultArgs = puppeteer.defaultArgs(); + const browser = await puppeteer.launch(Object.assign({}, defaultBrowserOptions, { + // Ignore first and third default argument. + ignoreDefaultArgs: [ defaultArgs[0], defaultArgs[2] ], + })); + const spawnargs = browser.process().spawnargs; + expect(spawnargs.indexOf(defaultArgs[0])).toBe(-1); + expect(spawnargs.indexOf(defaultArgs[1])).not.toBe(-1); + expect(spawnargs.indexOf(defaultArgs[2])).toBe(-1); + await browser.close(); + }); + it_fails_ffox('should have default url when launching browser', async function() { + const browser = await puppeteer.launch(defaultBrowserOptions); + const pages = (await browser.pages()).map(page => page.url()); + expect(pages).toEqual(['about:blank']); + await browser.close(); + }); + it_fails_ffox('should have custom url when launching browser', async function({server}) { + const options = Object.assign({}, defaultBrowserOptions); + options.args = [server.EMPTY_PAGE].concat(options.args || []); + const browser = await puppeteer.launch(options); + const pages = await browser.pages(); + expect(pages.length).toBe(1); + if (pages[0].url() !== server.EMPTY_PAGE) + await pages[0].waitForNavigation(); + expect(pages[0].url()).toBe(server.EMPTY_PAGE); + await browser.close(); + }); + it('should set the default viewport', async() => { + const options = Object.assign({}, defaultBrowserOptions, { + defaultViewport: { + width: 456, + height: 789 + } + }); + const browser = await puppeteer.launch(options); + const page = await browser.newPage(); + expect(await page.evaluate('window.innerWidth')).toBe(456); + expect(await page.evaluate('window.innerHeight')).toBe(789); + await browser.close(); + }); + it('should disable the default viewport', async() => { + const options = Object.assign({}, defaultBrowserOptions, { + defaultViewport: null + }); + const browser = await puppeteer.launch(options); + const page = await browser.newPage(); + expect(page.viewport()).toBe(null); + await browser.close(); + }); + it('should take fullPage screenshots when defaultViewport is null', async({server}) => { + const options = Object.assign({}, defaultBrowserOptions, { + defaultViewport: null + }); + const browser = await puppeteer.launch(options); + const page = await browser.newPage(); + await page.goto(server.PREFIX + '/grid.html'); + const screenshot = await page.screenshot({ + fullPage: true + }); + expect(screenshot).toBeInstanceOf(Buffer); + await browser.close(); + }); }); - it('should work with no timeout', async({page, server}) => { - const [chooser] = await Promise.all([ - page.waitForFileChooser({timeout: 0}), - page.evaluate(() => setTimeout(() => { - const el = document.createElement('input'); - el.type = 'file'; - el.click(); - }, 50)) - ]); - expect(chooser).toBeTruthy(); + describe('Puppeteer.connect', function() { + it('should be able to connect multiple times to the same browser', async({server}) => { + const originalBrowser = await puppeteer.launch(defaultBrowserOptions); + const browser = await puppeteer.connect({ + browserWSEndpoint: originalBrowser.wsEndpoint() + }); + const page = await browser.newPage(); + expect(await page.evaluate(() => 7 * 8)).toBe(56); + browser.disconnect(); + + const secondPage = await originalBrowser.newPage(); + expect(await secondPage.evaluate(() => 7 * 6)).toBe(42, 'original browser should still work'); + await originalBrowser.close(); + }); + it('should be able to close remote browser', async({server}) => { + const originalBrowser = await puppeteer.launch(defaultBrowserOptions); + const remoteBrowser = await puppeteer.connect({ + browserWSEndpoint: originalBrowser.wsEndpoint() + }); + await Promise.all([ + utils.waitEvent(originalBrowser, 'disconnected'), + remoteBrowser.close(), + ]); + }); + it('should support ignoreHTTPSErrors option', async({httpsServer}) => { + const originalBrowser = await puppeteer.launch(defaultBrowserOptions); + const browserWSEndpoint = originalBrowser.wsEndpoint(); + + const browser = await puppeteer.connect({browserWSEndpoint, ignoreHTTPSErrors: true}); + const page = await browser.newPage(); + let error = null; + const [serverRequest, response] = await Promise.all([ + httpsServer.waitForRequest('/empty.html'), + page.goto(httpsServer.EMPTY_PAGE).catch(e => error = e) + ]); + expect(error).toBe(null); + expect(response.ok()).toBe(true); + expect(response.securityDetails()).toBeTruthy(); + const protocol = serverRequest.socket.getProtocol().replace('v', ' '); + expect(response.securityDetails().protocol()).toBe(protocol); + await page.close(); + await browser.close(); + }); + it('should be able to reconnect to a disconnected browser', async({server}) => { + const originalBrowser = await puppeteer.launch(defaultBrowserOptions); + const browserWSEndpoint = originalBrowser.wsEndpoint(); + const page = await originalBrowser.newPage(); + await page.goto(server.PREFIX + '/frames/nested-frames.html'); + originalBrowser.disconnect(); + + const browser = await puppeteer.connect({browserWSEndpoint}); + const pages = await browser.pages(); + const restoredPage = pages.find(page => page.url() === server.PREFIX + '/frames/nested-frames.html'); + expect(utils.dumpFrames(restoredPage.mainFrame())).toEqual([ + 'http://localhost:/frames/nested-frames.html', + ' http://localhost:/frames/two-frames.html (2frames)', + ' http://localhost:/frames/frame.html (uno)', + ' http://localhost:/frames/frame.html (dos)', + ' http://localhost:/frames/frame.html (aframe)', + ]); + expect(await restoredPage.evaluate(() => 7 * 8)).toBe(56); + await browser.close(); + }); + // @see https://github.com/GoogleChrome/puppeteer/issues/4197#issuecomment-481793410 + it('should be able to connect to the same page simultaneously', async({server}) => { + const browserOne = await puppeteer.launch(); + const browserTwo = await puppeteer.connect({ browserWSEndpoint: browserOne.wsEndpoint() }); + const [page1, page2] = await Promise.all([ + new Promise(x => browserOne.once('targetcreated', target => x(target.page()))), + browserTwo.newPage(), + ]); + expect(await page1.evaluate(() => 7 * 8)).toBe(56); + expect(await page2.evaluate(() => 7 * 6)).toBe(42); + await browserOne.close(); + }); + }); - it('should return the same file chooser when there are many watchdogs simultaneously', async({page, server}) => { - await page.setContent(``); - const [fileChooser1, fileChooser2] = await Promise.all([ - page.waitForFileChooser(), - page.waitForFileChooser(), - page.$eval('input', input => input.click()), - ]); - expect(fileChooser1 === fileChooser2).toBe(true); + describe('Puppeteer.executablePath', function() { + it('should work', async({server}) => { + const executablePath = puppeteer.executablePath(); + expect(fs.existsSync(executablePath)).toBe(true); + expect(fs.realpathSync(executablePath)).toBe(executablePath); + }); }); }); - describe_fails_ffox('FileChooser.accept', function() { - it('should accept single file', async({page, server}) => { - await page.setContent(``); - const [chooser] = await Promise.all([ - page.waitForFileChooser(), - page.click('input'), - ]); - await Promise.all([ - chooser.accept([FILE_TO_UPLOAD]), - new Promise(x => page.once('metrics', x)), - ]); - expect(await page.$eval('input', input => input.files.length)).toBe(1); - expect(await page.$eval('input', input => input.files[0].name)).toBe('file-to-upload.txt'); - }); - it('should be able to read selected file', async({page, server}) => { - await page.setContent(``); - page.waitForFileChooser().then(chooser => chooser.accept([FILE_TO_UPLOAD])); - expect(await page.$eval('input', async picker => { - picker.click(); - await new Promise(x => picker.oninput = x); - const reader = new FileReader(); - const promise = new Promise(fulfill => reader.onload = fulfill); - reader.readAsText(picker.files[0]); - return promise.then(() => reader.result); - })).toBe('contents of the file'); + describe('Top-level requires', function() { + it('should require top-level Errors', async() => { + const Errors = require(path.join(puppeteerPath, '/Errors')); + expect(Errors.TimeoutError).toBe(puppeteer.errors.TimeoutError); }); - it('should be able to reset selected files with empty file list', async({page, server}) => { - await page.setContent(``); - page.waitForFileChooser().then(chooser => chooser.accept([FILE_TO_UPLOAD])); - expect(await page.$eval('input', async picker => { - picker.click(); - await new Promise(x => picker.oninput = x); - return picker.files.length; - })).toBe(1); - page.waitForFileChooser().then(chooser => chooser.accept([])); - expect(await page.$eval('input', async picker => { - picker.click(); - await new Promise(x => picker.oninput = x); - return picker.files.length; - })).toBe(0); - }); - it('should not accept multiple files for single-file input', async({page, server}) => { - await page.setContent(``); - const [chooser] = await Promise.all([ - page.waitForFileChooser(), - page.click('input'), - ]); - let error = null; - await chooser.accept([ - path.relative(process.cwd(), __dirname + '/assets/file-to-upload.txt'), - path.relative(process.cwd(), __dirname + '/assets/pptr.png'), - ]).catch(e => error = e); - expect(error).not.toBe(null); - }); - it('should fail when accepting file chooser twice', async({page, server}) => { - await page.setContent(``); - const [fileChooser] = await Promise.all([ - page.waitForFileChooser(), - page.$eval('input', input => input.click()), - ]); - await fileChooser.accept([]); - let error = null; - await fileChooser.accept([]).catch(e => error = e); - expect(error.message).toBe('Cannot accept FileChooser which is already handled!'); + it('should require top-level DeviceDescriptors', async() => { + const Devices = require(path.join(puppeteerPath, '/DeviceDescriptors')); + expect(Devices['iPhone 6']).toBe(puppeteer.devices['iPhone 6']); }); }); - describe_fails_ffox('FileChooser.cancel', function() { - it('should cancel dialog', async({page, server}) => { - // Consider file chooser canceled if we can summon another one. - // There's no reliable way in WebPlatform to see that FileChooser was - // canceled. - await page.setContent(``); - const [fileChooser1] = await Promise.all([ - page.waitForFileChooser(), - page.$eval('input', input => input.click()), - ]); - await fileChooser1.cancel(); - // If this resolves, than we successfully canceled file chooser. - await Promise.all([ - page.waitForFileChooser(), - page.$eval('input', input => input.click()), - ]); - }); - it('should fail when canceling file chooser twice', async({page, server}) => { - await page.setContent(``); - const [fileChooser] = await Promise.all([ - page.waitForFileChooser(), - page.$eval('input', input => input.click()), - ]); - await fileChooser.cancel(); - let error = null; - await fileChooser.cancel().catch(e => error = e); - expect(error.message).toBe('Cannot cancel FileChooser which is already handled!'); + describe('Browser target events', function() { + it('should work', async({server}) => { + const browser = await puppeteer.launch(defaultBrowserOptions); + const events = []; + browser.on('targetcreated', () => events.push('CREATED')); + browser.on('targetchanged', () => events.push('CHANGED')); + browser.on('targetdestroyed', () => events.push('DESTROYED')); + const page = await browser.newPage(); + await page.goto(server.EMPTY_PAGE); + await page.close(); + expect(events).toEqual(['CREATED', 'CHANGED', 'DESTROYED']); + await browser.close(); }); }); - describe_fails_ffox('FileChooser.isMultiple', () => { - it('should work for single file pick', async({page, server}) => { - await page.setContent(``); - const [chooser] = await Promise.all([ - page.waitForFileChooser(), - page.click('input'), - ]); - expect(chooser.isMultiple()).toBe(false); - }); - it('should work for "multiple"', async({page, server}) => { - await page.setContent(``); - const [chooser] = await Promise.all([ - page.waitForFileChooser(), - page.click('input'), + describe('Browser.Events.disconnected', function() { + it('should be emitted when: browser gets closed, disconnected or underlying websocket gets closed', async() => { + const originalBrowser = await puppeteer.launch(defaultBrowserOptions); + const browserWSEndpoint = originalBrowser.wsEndpoint(); + const remoteBrowser1 = await puppeteer.connect({browserWSEndpoint}); + const remoteBrowser2 = await puppeteer.connect({browserWSEndpoint}); + + let disconnectedOriginal = 0; + let disconnectedRemote1 = 0; + let disconnectedRemote2 = 0; + originalBrowser.on('disconnected', () => ++disconnectedOriginal); + remoteBrowser1.on('disconnected', () => ++disconnectedRemote1); + remoteBrowser2.on('disconnected', () => ++disconnectedRemote2); + + await Promise.all([ + utils.waitEvent(remoteBrowser2, 'disconnected'), + remoteBrowser2.disconnect(), ]); - expect(chooser.isMultiple()).toBe(true); - }); - it('should work for "webkitdirectory"', async({page, server}) => { - await page.setContent(``); - const [chooser] = await Promise.all([ - page.waitForFileChooser(), - page.click('input'), + + expect(disconnectedOriginal).toBe(0); + expect(disconnectedRemote1).toBe(0); + expect(disconnectedRemote2).toBe(1); + + await Promise.all([ + utils.waitEvent(remoteBrowser1, 'disconnected'), + utils.waitEvent(originalBrowser, 'disconnected'), + originalBrowser.close(), ]); - expect(chooser.isMultiple()).toBe(true); + + expect(disconnectedOriginal).toBe(1); + expect(disconnectedRemote1).toBe(1); + expect(disconnectedRemote2).toBe(1); }); - }); -'''; + });''';