From 7de0ccd36e5ed796ef1cb866d07a17fea57df746 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Tue, 24 Oct 2023 12:25:53 -0700 Subject: [PATCH] chore: support await using for close() and dispose() (#27766) This change assumes that the user has Node 18 with Symbol.dispose available. Fixes https://github.com/microsoft/playwright/issues/27141 --- .../playwright-core/src/browserServerImpl.ts | 1 + .../playwright-core/src/client/android.ts | 8 ++ .../playwright-core/src/client/browser.ts | 4 + .../src/client/browserContext.ts | 4 + .../playwright-core/src/client/electron.ts | 4 + packages/playwright-core/src/client/fetch.ts | 8 ++ .../playwright-core/src/client/harRouter.ts | 4 + .../playwright-core/src/client/jsHandle.ts | 4 + packages/playwright-core/src/client/page.ts | 4 + packages/playwright-core/types/types.d.ts | 98 +++++++++++-------- packages/playwright/ThirdPartyNotices.txt | 58 ++++++++++- .../bundles/babel/package-lock.json | 47 +++++++++ .../playwright/bundles/babel/package.json | 1 + .../bundles/babel/src/babelBundleImpl.ts | 1 + tests/config/checkCoverage.js | 3 + tests/installation/npmTest.ts | 2 +- .../playwright-test-plugin.spec.ts | 6 +- tests/library/browsertype-launch.spec.ts | 64 ++++++++---- utils/doclint/documentation.js | 4 +- utils/generate_types/index.js | 22 ++++- utils/generate_types/test/tsconfig.json | 2 +- 21 files changed, 282 insertions(+), 67 deletions(-) diff --git a/packages/playwright-core/src/browserServerImpl.ts b/packages/playwright-core/src/browserServerImpl.ts index 4220e0b49f..41f9c2c2e4 100644 --- a/packages/playwright-core/src/browserServerImpl.ts +++ b/packages/playwright-core/src/browserServerImpl.ts @@ -65,6 +65,7 @@ export class BrowserServerLauncherImpl implements BrowserServerLauncher { browserServer.process = () => browser.options.browserProcess.process!; browserServer.wsEndpoint = () => wsEndpoint; browserServer.close = () => browser.options.browserProcess.close(); + browserServer[Symbol.asyncDispose] = browserServer.close; browserServer.kill = () => browser.options.browserProcess.kill(); (browserServer as any)._disconnectForTest = () => server.close(); (browserServer as any)._userDataDirForTest = (browser as any)._userDataDirForTest; diff --git a/packages/playwright-core/src/client/android.ts b/packages/playwright-core/src/client/android.ts index 395fbda6c7..6c7a9e44ae 100644 --- a/packages/playwright-core/src/client/android.ts +++ b/packages/playwright-core/src/client/android.ts @@ -234,6 +234,10 @@ export class AndroidDevice extends ChannelOwner i return binary; } + async [Symbol.asyncDispose]() { + await this.close(); + } + async close() { try { if (this._shouldCloseConnectionOnClose) @@ -307,6 +311,10 @@ export class AndroidSocket extends ChannelOwner i async close(): Promise { await this._channel.close(); } + + async [Symbol.asyncDispose]() { + await this.close(); + } } async function loadFile(file: string | Buffer): Promise { diff --git a/packages/playwright-core/src/client/browser.ts b/packages/playwright-core/src/client/browser.ts index 0df8c73efb..be47ddeb51 100644 --- a/packages/playwright-core/src/client/browser.ts +++ b/packages/playwright-core/src/client/browser.ts @@ -131,6 +131,10 @@ export class Browser extends ChannelOwner implements ap return buffer; } + async [Symbol.asyncDispose]() { + await this.close(); + } + async close(options: { reason?: string } = {}): Promise { this._closeReason = options.reason; try { diff --git a/packages/playwright-core/src/client/browserContext.ts b/packages/playwright-core/src/client/browserContext.ts index 10f9d82492..a74996bbe2 100644 --- a/packages/playwright-core/src/client/browserContext.ts +++ b/packages/playwright-core/src/client/browserContext.ts @@ -387,6 +387,10 @@ export class BrowserContext extends ChannelOwner this.emit(Events.BrowserContext.Close, this); } + async [Symbol.asyncDispose]() { + await this.close(); + } + async close(options: { reason?: string } = {}): Promise { if (this._closeWasCalled) return; diff --git a/packages/playwright-core/src/client/electron.ts b/packages/playwright-core/src/client/electron.ts index 5a05711f80..8a0b43ff1f 100644 --- a/packages/playwright-core/src/client/electron.ts +++ b/packages/playwright-core/src/client/electron.ts @@ -108,6 +108,10 @@ export class ElectronApplication extends ChannelOwner { await this._instrumentation.onWillCloseRequestContext(this); await this._channel.dispose(); @@ -302,6 +306,10 @@ export class APIResponse implements api.APIResponse { return JSON.parse(content); } + async [Symbol.asyncDispose]() { + await this.dispose(); + } + async dispose(): Promise { await this._request._channel.disposeAPIResponse({ fetchUid: this._fetchUid() }); } diff --git a/packages/playwright-core/src/client/harRouter.ts b/packages/playwright-core/src/client/harRouter.ts index 92cac024f9..0ba71d8ba8 100644 --- a/packages/playwright-core/src/client/harRouter.ts +++ b/packages/playwright-core/src/client/harRouter.ts @@ -93,6 +93,10 @@ export class HarRouter { page.once(Events.Page.Close, () => this.dispose()); } + async [Symbol.asyncDispose]() { + await this.dispose(); + } + dispose() { this._localUtils._channel.harClose({ harId: this._harId }).catch(() => {}); } diff --git a/packages/playwright-core/src/client/jsHandle.ts b/packages/playwright-core/src/client/jsHandle.ts index c51c6694f3..39afd9a52e 100644 --- a/packages/playwright-core/src/client/jsHandle.ts +++ b/packages/playwright-core/src/client/jsHandle.ts @@ -63,6 +63,10 @@ export class JSHandle extends ChannelOwner im return null as any; } + async [Symbol.asyncDispose]() { + await this.dispose(); + } + async dispose() { return await this._channel.dispose(); } diff --git a/packages/playwright-core/src/client/page.ts b/packages/playwright-core/src/client/page.ts index 61e20bb28e..0e8701f905 100644 --- a/packages/playwright-core/src/client/page.ts +++ b/packages/playwright-core/src/client/page.ts @@ -517,6 +517,10 @@ export class Page extends ChannelOwner implements api.Page await this._channel.bringToFront(); } + async [Symbol.asyncDispose]() { + await this.close(); + } + async close(options: { runBeforeUnload?: boolean, reason?: string } = {}) { this._closeReason = options.reason; try { diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index a1ff90e483..08644a856e 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -4707,6 +4707,8 @@ export interface Page { request: APIRequestContext; touchscreen: Touchscreen; + + [Symbol.asyncDispose](): Promise; } /** @@ -7632,12 +7634,6 @@ export interface BrowserContext { */ on(event: 'page', listener: (page: Page) => void): this; - /** - * Emitted when exception is unhandled in any of the pages in this context. To listen for errors from a particular - * page, use [page.on('pageerror')](https://playwright.dev/docs/api/class-page#page-event-page-error) instead. - */ - on(event: 'weberror', listener: (webError: WebError) => void): this; - /** * Emitted when a request is issued from any pages created through this context. The [request] object is read-only. To * only listen for requests from a particular page, use @@ -7683,6 +7679,12 @@ export interface BrowserContext { */ on(event: 'serviceworker', listener: (worker: Worker) => void): this; + /** + * Emitted when exception is unhandled in any of the pages in this context. To listen for errors from a particular + * page, use [page.on('pageerror')](https://playwright.dev/docs/api/class-page#page-event-page-error) instead. + */ + on(event: 'weberror', listener: (webError: WebError) => void): this; + /** * Adds an event listener that will be automatically removed after it is triggered once. See `addListener` for more information about this event. */ @@ -7708,11 +7710,6 @@ export interface BrowserContext { */ once(event: 'page', listener: (page: Page) => void): this; - /** - * Adds an event listener that will be automatically removed after it is triggered once. See `addListener` for more information about this event. - */ - once(event: 'weberror', listener: (webError: WebError) => void): this; - /** * Adds an event listener that will be automatically removed after it is triggered once. See `addListener` for more information about this event. */ @@ -7738,6 +7735,11 @@ export interface BrowserContext { */ once(event: 'serviceworker', listener: (worker: Worker) => void): this; + /** + * Adds an event listener that will be automatically removed after it is triggered once. See `addListener` for more information about this event. + */ + once(event: 'weberror', listener: (webError: WebError) => void): this; + /** * **NOTE** Only works with Chromium browser's persistent context. * @@ -7824,12 +7826,6 @@ export interface BrowserContext { */ addListener(event: 'page', listener: (page: Page) => void): this; - /** - * Emitted when exception is unhandled in any of the pages in this context. To listen for errors from a particular - * page, use [page.on('pageerror')](https://playwright.dev/docs/api/class-page#page-event-page-error) instead. - */ - addListener(event: 'weberror', listener: (webError: WebError) => void): this; - /** * Emitted when a request is issued from any pages created through this context. The [request] object is read-only. To * only listen for requests from a particular page, use @@ -7875,6 +7871,12 @@ export interface BrowserContext { */ addListener(event: 'serviceworker', listener: (worker: Worker) => void): this; + /** + * Emitted when exception is unhandled in any of the pages in this context. To listen for errors from a particular + * page, use [page.on('pageerror')](https://playwright.dev/docs/api/class-page#page-event-page-error) instead. + */ + addListener(event: 'weberror', listener: (webError: WebError) => void): this; + /** * Removes an event listener added by `on` or `addListener`. */ @@ -7900,11 +7902,6 @@ export interface BrowserContext { */ removeListener(event: 'page', listener: (page: Page) => void): this; - /** - * Removes an event listener added by `on` or `addListener`. - */ - removeListener(event: 'weberror', listener: (webError: WebError) => void): this; - /** * Removes an event listener added by `on` or `addListener`. */ @@ -7930,6 +7927,11 @@ export interface BrowserContext { */ removeListener(event: 'serviceworker', listener: (worker: Worker) => void): this; + /** + * Removes an event listener added by `on` or `addListener`. + */ + removeListener(event: 'weberror', listener: (webError: WebError) => void): this; + /** * Removes an event listener added by `on` or `addListener`. */ @@ -7955,11 +7957,6 @@ export interface BrowserContext { */ off(event: 'page', listener: (page: Page) => void): this; - /** - * Removes an event listener added by `on` or `addListener`. - */ - off(event: 'weberror', listener: (webError: WebError) => void): this; - /** * Removes an event listener added by `on` or `addListener`. */ @@ -7985,6 +7982,11 @@ export interface BrowserContext { */ off(event: 'serviceworker', listener: (worker: Worker) => void): this; + /** + * Removes an event listener added by `on` or `addListener`. + */ + off(event: 'weberror', listener: (webError: WebError) => void): this; + /** * **NOTE** Only works with Chromium browser's persistent context. * @@ -8071,12 +8073,6 @@ export interface BrowserContext { */ prependListener(event: 'page', listener: (page: Page) => void): this; - /** - * Emitted when exception is unhandled in any of the pages in this context. To listen for errors from a particular - * page, use [page.on('pageerror')](https://playwright.dev/docs/api/class-page#page-event-page-error) instead. - */ - prependListener(event: 'weberror', listener: (webError: WebError) => void): this; - /** * Emitted when a request is issued from any pages created through this context. The [request] object is read-only. To * only listen for requests from a particular page, use @@ -8122,6 +8118,12 @@ export interface BrowserContext { */ prependListener(event: 'serviceworker', listener: (worker: Worker) => void): this; + /** + * Emitted when exception is unhandled in any of the pages in this context. To listen for errors from a particular + * page, use [page.on('pageerror')](https://playwright.dev/docs/api/class-page#page-event-page-error) instead. + */ + prependListener(event: 'weberror', listener: (webError: WebError) => void): this; + /** * Adds cookies into this browser context. All pages within this context will have these cookies installed. Cookies * can be obtained via @@ -8670,12 +8672,6 @@ export interface BrowserContext { */ waitForEvent(event: 'page', optionsOrPredicate?: { predicate?: (page: Page) => boolean | Promise, timeout?: number } | ((page: Page) => boolean | Promise)): Promise; - /** - * Emitted when exception is unhandled in any of the pages in this context. To listen for errors from a particular - * page, use [page.on('pageerror')](https://playwright.dev/docs/api/class-page#page-event-page-error) instead. - */ - waitForEvent(event: 'weberror', optionsOrPredicate?: { predicate?: (webError: WebError) => boolean | Promise, timeout?: number } | ((webError: WebError) => boolean | Promise)): Promise; - /** * Emitted when a request is issued from any pages created through this context. The [request] object is read-only. To * only listen for requests from a particular page, use @@ -8721,6 +8717,12 @@ export interface BrowserContext { */ waitForEvent(event: 'serviceworker', optionsOrPredicate?: { predicate?: (worker: Worker) => boolean | Promise, timeout?: number } | ((worker: Worker) => boolean | Promise)): Promise; + /** + * Emitted when exception is unhandled in any of the pages in this context. To listen for errors from a particular + * page, use [page.on('pageerror')](https://playwright.dev/docs/api/class-page#page-event-page-error) instead. + */ + waitForEvent(event: 'weberror', optionsOrPredicate?: { predicate?: (webError: WebError) => boolean | Promise, timeout?: number } | ((webError: WebError) => boolean | Promise)): Promise; + /** * API testing helper associated with this context. Requests made with this API will use context cookies. @@ -8728,6 +8730,8 @@ export interface BrowserContext { request: APIRequestContext; tracing: Tracing; + + [Symbol.asyncDispose](): Promise; } /** @@ -8992,6 +8996,8 @@ export interface JSHandle { * @param propertyName property to get */ getProperty(propertyName: string): Promise; + + [Symbol.asyncDispose](): Promise; } /** @@ -13747,6 +13753,8 @@ export interface ElectronApplication { * Convenience method that returns all the opened windows. */ windows(): Array; + + [Symbol.asyncDispose](): Promise; } export type AndroidElementInfo = { @@ -14806,6 +14814,8 @@ export interface AndroidDevice { webViews(): Array; input: AndroidInput; + + [Symbol.asyncDispose](): Promise; } export interface AndroidInput { @@ -14941,6 +14951,8 @@ export interface AndroidSocket { * @param data Data to write. */ write(data: Buffer): Promise; + + [Symbol.asyncDispose](): Promise; } /** @@ -15880,6 +15892,8 @@ export interface APIRequestContext { }>; }>; }>; + + [Symbol.asyncDispose](): Promise; } /** @@ -15950,6 +15964,8 @@ export interface APIResponse { * Contains the URL of the response. */ url(): string; + + [Symbol.asyncDispose](): Promise; } /** @@ -16545,6 +16561,8 @@ export interface Browser extends EventEmitter { * Returns the browser version. */ version(): string; + + [Symbol.asyncDispose](): Promise; } export interface BrowserServer { @@ -16601,6 +16619,8 @@ export interface BrowserServer { * to establish connection to the browser. */ wsEndpoint(): string; + + [Symbol.asyncDispose](): Promise; } /** diff --git a/packages/playwright/ThirdPartyNotices.txt b/packages/playwright/ThirdPartyNotices.txt index 4393289d93..3adafd64c3 100644 --- a/packages/playwright/ThirdPartyNotices.txt +++ b/packages/playwright/ThirdPartyNotices.txt @@ -34,10 +34,12 @@ This project incorporates components from the projects listed below. The origina - @babel/highlight@7.22.5 (https://github.com/babel/babel) - @babel/parser@7.23.0 (https://github.com/babel/babel) - @babel/plugin-proposal-decorators@7.23.2 (https://github.com/babel/babel) +- @babel/plugin-proposal-explicit-resource-management@7.23.0 (https://github.com/babel/babel) - @babel/plugin-syntax-async-generators@7.8.4 (https://github.com/babel/babel/tree/master/packages/babel-plugin-syntax-async-generators) - @babel/plugin-syntax-class-static-block@7.14.5 (https://github.com/babel/babel) - @babel/plugin-syntax-decorators@7.22.10 (https://github.com/babel/babel) - @babel/plugin-syntax-dynamic-import@7.8.3 (https://github.com/babel/babel/tree/master/packages/babel-plugin-syntax-dynamic-import) +- @babel/plugin-syntax-explicit-resource-management@7.22.5 (https://github.com/babel/babel) - @babel/plugin-syntax-export-namespace-from@7.8.3 (https://github.com/babel/babel/tree/master/packages/babel-plugin-syntax-export-namespace-from) - @babel/plugin-syntax-import-assertions@7.22.5 (https://github.com/babel/babel) - @babel/plugin-syntax-json-strings@7.8.3 (https://github.com/babel/babel/tree/master/packages/babel-plugin-syntax-json-strings) @@ -1140,6 +1142,33 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ========================================= END OF @babel/plugin-proposal-decorators@7.23.2 AND INFORMATION +%% @babel/plugin-proposal-explicit-resource-management@7.23.0 NOTICES AND INFORMATION BEGIN HERE +========================================= +MIT License + +Copyright (c) 2014-present Sebastian McKenzie and other contributors + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF @babel/plugin-proposal-explicit-resource-management@7.23.0 AND INFORMATION + %% @babel/plugin-syntax-async-generators@7.8.4 NOTICES AND INFORMATION BEGIN HERE ========================================= MIT License @@ -1248,6 +1277,33 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ========================================= END OF @babel/plugin-syntax-dynamic-import@7.8.3 AND INFORMATION +%% @babel/plugin-syntax-explicit-resource-management@7.22.5 NOTICES AND INFORMATION BEGIN HERE +========================================= +MIT License + +Copyright (c) 2014-present Sebastian McKenzie and other contributors + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF @babel/plugin-syntax-explicit-resource-management@7.22.5 AND INFORMATION + %% @babel/plugin-syntax-export-namespace-from@7.8.3 NOTICES AND INFORMATION BEGIN HERE ========================================= MIT License @@ -4351,6 +4407,6 @@ END OF yallist@3.1.1 AND INFORMATION SUMMARY BEGIN HERE ========================================= -Total Packages: 149 +Total Packages: 151 ========================================= END OF SUMMARY \ No newline at end of file diff --git a/packages/playwright/bundles/babel/package-lock.json b/packages/playwright/bundles/babel/package-lock.json index c4cb673b5e..a55f8eff0b 100644 --- a/packages/playwright/bundles/babel/package-lock.json +++ b/packages/playwright/bundles/babel/package-lock.json @@ -13,6 +13,7 @@ "@babel/helper-plugin-utils": "^7.22.5", "@babel/parser": "^7.23.0", "@babel/plugin-proposal-decorators": "^7.23.2", + "@babel/plugin-proposal-explicit-resource-management": "^7.23.0", "@babel/plugin-syntax-async-generators": "^7.8.4", "@babel/plugin-syntax-import-assertions": "^7.22.5", "@babel/plugin-syntax-json-strings": "^7.8.3", @@ -380,6 +381,21 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-proposal-explicit-resource-management": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-explicit-resource-management/-/plugin-proposal-explicit-resource-management-7.23.0.tgz", + "integrity": "sha512-wu5/1COnSuGj78UBhTpxmESzV/xEcWIjkM54PZ8mhmwHrDBriro0o8LAkbbTXDAsQjwlqkqvIyXzOSykHrDiSg==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-explicit-resource-management": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-syntax-async-generators": { "version": "7.8.4", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", @@ -430,6 +446,20 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-syntax-explicit-resource-management": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-explicit-resource-management/-/plugin-syntax-explicit-resource-management-7.22.5.tgz", + "integrity": "sha512-vokH/rTR4m9hlcxXXL0CPnpoGHUbZ6gfI3kq/UZSwrF9qGo/LxWqEP0qWYqkt5kc6/jrCOTaBeYw+lYleEFtLA==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-syntax-export-namespace-from": { "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", @@ -1449,6 +1479,15 @@ "@babel/plugin-syntax-decorators": "^7.22.10" } }, + "@babel/plugin-proposal-explicit-resource-management": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-explicit-resource-management/-/plugin-proposal-explicit-resource-management-7.23.0.tgz", + "integrity": "sha512-wu5/1COnSuGj78UBhTpxmESzV/xEcWIjkM54PZ8mhmwHrDBriro0o8LAkbbTXDAsQjwlqkqvIyXzOSykHrDiSg==", + "requires": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-explicit-resource-management": "^7.22.5" + } + }, "@babel/plugin-syntax-async-generators": { "version": "7.8.4", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", @@ -1481,6 +1520,14 @@ "@babel/helper-plugin-utils": "^7.8.0" } }, + "@babel/plugin-syntax-explicit-resource-management": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-explicit-resource-management/-/plugin-syntax-explicit-resource-management-7.22.5.tgz", + "integrity": "sha512-vokH/rTR4m9hlcxXXL0CPnpoGHUbZ6gfI3kq/UZSwrF9qGo/LxWqEP0qWYqkt5kc6/jrCOTaBeYw+lYleEFtLA==", + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, "@babel/plugin-syntax-export-namespace-from": { "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", diff --git a/packages/playwright/bundles/babel/package.json b/packages/playwright/bundles/babel/package.json index d367d24be1..c4c2cdc853 100644 --- a/packages/playwright/bundles/babel/package.json +++ b/packages/playwright/bundles/babel/package.json @@ -14,6 +14,7 @@ "@babel/helper-plugin-utils": "^7.22.5", "@babel/parser": "^7.23.0", "@babel/plugin-proposal-decorators": "^7.23.2", + "@babel/plugin-proposal-explicit-resource-management": "^7.23.0", "@babel/plugin-syntax-async-generators": "^7.8.4", "@babel/plugin-syntax-import-assertions": "^7.22.5", "@babel/plugin-syntax-json-strings": "^7.8.3", diff --git a/packages/playwright/bundles/babel/src/babelBundleImpl.ts b/packages/playwright/bundles/babel/src/babelBundleImpl.ts index 46dd400366..b8dfa198b0 100644 --- a/packages/playwright/bundles/babel/src/babelBundleImpl.ts +++ b/packages/playwright/bundles/babel/src/babelBundleImpl.ts @@ -33,6 +33,7 @@ function babelTransformOptions(isTypeScript: boolean, isModule: boolean, plugins if (isTypeScript) { plugins.push( [require('@babel/plugin-proposal-decorators'), { version: '2023-05' }], + [require('@babel/plugin-proposal-explicit-resource-management')], [require('@babel/plugin-transform-class-properties')], [require('@babel/plugin-transform-class-static-block')], [require('@babel/plugin-transform-numeric-separator')], diff --git a/tests/config/checkCoverage.js b/tests/config/checkCoverage.js index d288d4a2b6..52236eb8d1 100644 --- a/tests/config/checkCoverage.js +++ b/tests/config/checkCoverage.js @@ -68,6 +68,9 @@ let success = true; for (const method of api) { if (coveredMethods.has(method)) continue; + // [Symbol.asyncDispose] + if (method.endsWith('.undefined')) + continue; success = false; console.log(`ERROR: Missing coverage for "${method}"`) } diff --git a/tests/installation/npmTest.ts b/tests/installation/npmTest.ts index ce6a370b9b..01110d3c38 100644 --- a/tests/installation/npmTest.ts +++ b/tests/installation/npmTest.ts @@ -198,7 +198,7 @@ export const test = _test }); }, tsc: async ({ exec }, use) => { - await exec('npm i typescript@5.2.2 @types/node@16'); + await exec('npm i typescript@5.2.2 @types/node@18'); await use((args: string) => exec('npx', 'tsc', args, { shell: process.platform === 'win32' })); }, }); diff --git a/tests/installation/playwright-test-plugin.spec.ts b/tests/installation/playwright-test-plugin.spec.ts index 3e9ddfb0e5..5762b49900 100755 --- a/tests/installation/playwright-test-plugin.spec.ts +++ b/tests/installation/playwright-test-plugin.spec.ts @@ -42,7 +42,7 @@ test('npm: @playwright/test plugin should work', async ({ exec, tmpWorkspace }) expect(output).toContain('plugin value: hello from plugin'); expect(output).toContain('1 passed'); - await exec('npm i typescript@5.2.2 @types/node@16'); + await exec('npm i typescript@5.2.2 @types/node@18'); await exec('npx tsc playwright-test-plugin-types.ts'); }); @@ -56,7 +56,7 @@ test('pnpm: @playwright/test plugin should work', async ({ exec, tmpWorkspace }) expect(output).toContain('plugin value: hello from plugin'); expect(output).toContain('1 passed'); - await exec('pnpm add typescript@5.2.2 @types/node@16'); + await exec('pnpm add typescript@5.2.2 @types/node@18'); await exec('pnpm exec tsc playwright-test-plugin-types.ts'); }); @@ -70,6 +70,6 @@ test('yarn: @playwright/test plugin should work', async ({ exec, tmpWorkspace }) expect(output).toContain('plugin value: hello from plugin'); expect(output).toContain('1 passed'); - await exec('yarn add typescript@5.2.2 @types/node@16'); + await exec('yarn add typescript@5.2.2 @types/node@18'); await exec('yarn tsc playwright-test-plugin-types.ts'); }); diff --git a/tests/library/browsertype-launch.spec.ts b/tests/library/browsertype-launch.spec.ts index d36e9d8d8d..bdd5128dd1 100644 --- a/tests/library/browsertype-launch.spec.ts +++ b/tests/library/browsertype-launch.spec.ts @@ -16,62 +16,64 @@ */ import { playwrightTest as it, expect } from '../config/browserTest'; +import type { Browser, BrowserContext, Page } from '@playwright/test'; +import { kTargetClosedErrorMessage } from '../config/errors'; it('should reject all promises when browser is closed', async ({ browserType }) => { const browser = await browserType.launch(); const page = await (await browser.newContext()).newPage(); - let error = null; + let error: Error | undefined; const neverResolves = page.evaluate(() => new Promise(r => {})).catch(e => error = e); await page.evaluate(() => new Promise(f => setTimeout(f, 0))); await browser.close(); await neverResolves; // WebKit under task-set -c 1 is giving browser, rest are giving target. - expect(error.message).toContain(' closed'); + expect(error!.message).toContain(' closed'); }); it('should throw if userDataDir option is passed', async ({ browserType }) => { - let waitError = null; + let waitError: Error | undefined; await browserType.launch({ userDataDir: 'random-path' } as any).catch(e => waitError = e); - expect(waitError.message).toContain('userDataDir option is not supported in `browserType.launch`. Use `browserType.launchPersistentContext` instead'); + expect(waitError!.message).toContain('userDataDir option is not supported in `browserType.launch`. Use `browserType.launchPersistentContext` instead'); }); it('should throw if userDataDir is passed as an argument', async ({ browserType }) => { - let waitError = null; + let waitError: Error | undefined; await browserType.launch({ args: ['--user-data-dir=random-path', '--profile=random-path'] } as any).catch(e => waitError = e); - expect(waitError.message).toContain(`Pass userDataDir parameter to 'browserType.launchPersistentContext`); + expect(waitError!.message).toContain(`Pass userDataDir parameter to 'browserType.launchPersistentContext`); }); it('should throw if port option is passed', async ({ browserType }) => { const error = await browserType.launch({ port: 1234 } as any).catch(e => e); - expect(error.message).toContain('Cannot specify a port without launching as a server.'); + expect(error!.message).toContain('Cannot specify a port without launching as a server.'); }); it('should throw if port option is passed for persistent context', async ({ browserType }) => { const error = await browserType.launchPersistentContext('foo', { port: 1234 } as any).catch(e => e); - expect(error.message).toContain('Cannot specify a port without launching as a server.'); + expect(error!.message).toContain('Cannot specify a port without launching as a server.'); }); it('should throw if page argument is passed', async ({ browserType, browserName }) => { it.skip(browserName === 'firefox'); - let waitError = null; + let waitError: Error | undefined; await browserType.launch({ args: ['http://example.com'] }).catch(e => waitError = e); - expect(waitError.message).toContain('can not specify page'); + expect(waitError!.message).toContain('can not specify page'); }); it('should reject if launched browser fails immediately', async ({ mode, browserType, asset, isWindows }) => { it.skip(mode.startsWith('service')); - let waitError = null; + let waitError: Error | undefined; await browserType.launch({ executablePath: asset('dummy_bad_browser_executable.js') }).catch(e => waitError = e); - expect(waitError.message).toContain(isWindows ? 'browserType.launch: spawn UNKNOWN' : 'Browser logs:'); + expect(waitError!.message).toContain(isWindows ? 'browserType.launch: spawn UNKNOWN' : 'Browser logs:'); }); it('should reject if executable path is invalid', async ({ browserType, mode }) => { it.skip(mode.startsWith('service'), 'on service mode we dont allow passing custom executable path'); - let waitError = null; + let waitError: Error | undefined; await browserType.launch({ executablePath: 'random-invalid-path' }).catch(e => waitError = e); - expect(waitError.message).toContain('Failed to launch'); + expect(waitError!.message).toContain('Failed to launch'); }); it('should handle timeout', async ({ browserType, mode }) => { @@ -79,9 +81,9 @@ it('should handle timeout', async ({ browserType, mode }) => { const options: any = { timeout: 5000, __testHookBeforeCreateBrowser: () => new Promise(f => setTimeout(f, 6000)) }; const error = await browserType.launch(options).catch(e => e); - expect(error.message).toContain(`browserType.launch: Timeout 5000ms exceeded.`); - expect(error.message).toContain(``); - expect(error.message).toContain(` pid=`); + expect(error!.message).toContain(`browserType.launch: Timeout 5000ms exceeded.`); + expect(error!.message).toContain(``); + expect(error!.message).toContain(` pid=`); }); it('should handle exception', async ({ browserType, mode }) => { @@ -90,7 +92,7 @@ it('should handle exception', async ({ browserType, mode }) => { const e = new Error('Dummy'); const options = { __testHookBeforeCreateBrowser: () => { throw e; }, timeout: 9000 }; const error = await browserType.launch(options).catch(e => e); - expect(error.message).toContain('Dummy'); + expect(error!.message).toContain('Dummy'); }); it('should report launch log', async ({ browserType, mode }) => { @@ -99,7 +101,7 @@ it('should report launch log', async ({ browserType, mode }) => { const e = new Error('Dummy'); const options = { __testHookBeforeCreateBrowser: () => { throw e; }, timeout: 9000 }; const error = await browserType.launch(options).catch(e => e); - expect(error.message).toContain(''); + expect(error!.message).toContain(''); }); it('should accept objects as options', async ({ mode, browserType }) => { @@ -127,3 +129,27 @@ it('should be callable twice', async ({ browserType }) => { ]); await browser.close(); }); + +it('should allow await using', async ({ browserType }) => { + const nodeVersion = +process.versions.node.split('.')[0]; + it.skip(nodeVersion < 18); + + let b: Browser; + let c: BrowserContext; + let p: Page; + { + await using browser = await browserType.launch(); + b = browser; + { + await using context = await browser.newContext(); + c = context; + { + await using page = await context.newPage(); + p = page; + } + expect(p.isClosed()).toBeTruthy(); + } + expect(await c.clearCookies().catch(e => e.message)).toContain(kTargetClosedErrorMessage); + } + expect(b.isConnected()).toBeFalsy(); +}); diff --git a/utils/doclint/documentation.js b/utils/doclint/documentation.js index 2a6bba10ed..de35df27ba 100644 --- a/utils/doclint/documentation.js +++ b/utils/doclint/documentation.js @@ -55,8 +55,8 @@ const md = require('../markdown'); * @typedef {{ * langs: Langs, * since: string, - * deprecated: string | undefined, - * discouraged: string | undefined, + * deprecated?: string | undefined, + * discouraged?: string | undefined, * experimental: boolean * }} Metainfo */ diff --git a/utils/generate_types/index.js b/utils/generate_types/index.js index d5f3736d47..434343c380 100644 --- a/utils/generate_types/index.js +++ b/utils/generate_types/index.js @@ -16,7 +16,6 @@ // @ts-check const path = require('path'); -const toKebabCase = require('lodash/kebabCase') const devices = require('../../packages/playwright-core/lib/server/deviceDescriptors'); const md = require('../markdown'); const docs = require('../doclint/documentation'); @@ -54,6 +53,27 @@ class TypesGenerator { if (!options.includeExperimental) this.documentation.filterOutExperimental(); this.documentation.copyDocsFromSuperclasses([]); + this.injectDisposeAsync(); + } + + injectDisposeAsync() { + for (const [name, clazz] of this.documentation.classes.entries()) { + /** @type {docs.Member | undefined} */ + let newMember = undefined; + for (const [memberName, member] of clazz.members) { + if (memberName !== 'close' && memberName !== 'dispose') + continue; + if (!member.async) + continue; + newMember = new docs.Member('method', { langs: {}, since: '1.0', experimental: false }, '[Symbol.asyncDispose]', null, []); + newMember.async = true; + break; + } + if (newMember) { + clazz.membersArray = [...clazz.membersArray, newMember]; + clazz.index(); + } + } } /** diff --git a/utils/generate_types/test/tsconfig.json b/utils/generate_types/test/tsconfig.json index eb64864983..557e0065fd 100644 --- a/utils/generate_types/test/tsconfig.json +++ b/utils/generate_types/test/tsconfig.json @@ -1,7 +1,7 @@ { "compilerOptions": { "strict": true, - "target": "ES2019", + "target": "ESNext", "noEmit": true, "moduleResolution": "node", },