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
This commit is contained in:
Pavel Feldman 2023-10-24 12:25:53 -07:00 committed by GitHub
parent c8134bca5d
commit 7de0ccd36e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 282 additions and 67 deletions

View File

@ -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;

View File

@ -234,6 +234,10 @@ export class AndroidDevice extends ChannelOwner<channels.AndroidDeviceChannel> i
return binary;
}
async [Symbol.asyncDispose]() {
await this.close();
}
async close() {
try {
if (this._shouldCloseConnectionOnClose)
@ -307,6 +311,10 @@ export class AndroidSocket extends ChannelOwner<channels.AndroidSocketChannel> i
async close(): Promise<void> {
await this._channel.close();
}
async [Symbol.asyncDispose]() {
await this.close();
}
}
async function loadFile(file: string | Buffer): Promise<Buffer> {

View File

@ -131,6 +131,10 @@ export class Browser extends ChannelOwner<channels.BrowserChannel> implements ap
return buffer;
}
async [Symbol.asyncDispose]() {
await this.close();
}
async close(options: { reason?: string } = {}): Promise<void> {
this._closeReason = options.reason;
try {

View File

@ -387,6 +387,10 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
this.emit(Events.BrowserContext.Close, this);
}
async [Symbol.asyncDispose]() {
await this.close();
}
async close(options: { reason?: string } = {}): Promise<void> {
if (this._closeWasCalled)
return;

View File

@ -108,6 +108,10 @@ export class ElectronApplication extends ChannelOwner<channels.ElectronApplicati
return this._context;
}
async [Symbol.asyncDispose]() {
await this.close();
}
async close() {
if (this._isClosed)
return;

View File

@ -95,6 +95,10 @@ export class APIRequestContext extends ChannelOwner<channels.APIRequestContextCh
this._tracing = Tracing.from(initializer.tracing);
}
async [Symbol.asyncDispose]() {
await this.dispose();
}
async dispose(): Promise<void> {
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<void> {
await this._request._channel.disposeAPIResponse({ fetchUid: this._fetchUid() });
}

View File

@ -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(() => {});
}

View File

@ -63,6 +63,10 @@ export class JSHandle<T = any> extends ChannelOwner<channels.JSHandleChannel> im
return null as any;
}
async [Symbol.asyncDispose]() {
await this.dispose();
}
async dispose() {
return await this._channel.dispose();
}

View File

@ -517,6 +517,10 @@ export class Page extends ChannelOwner<channels.PageChannel> 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 {

View File

@ -4707,6 +4707,8 @@ export interface Page {
request: APIRequestContext;
touchscreen: Touchscreen;
[Symbol.asyncDispose](): Promise<void>;
}
/**
@ -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<boolean>, timeout?: number } | ((page: Page) => boolean | Promise<boolean>)): Promise<Page>;
/**
* 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<boolean>, timeout?: number } | ((webError: WebError) => boolean | Promise<boolean>)): Promise<WebError>;
/**
* 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<boolean>, timeout?: number } | ((worker: Worker) => boolean | Promise<boolean>)): Promise<Worker>;
/**
* 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<boolean>, timeout?: number } | ((webError: WebError) => boolean | Promise<boolean>)): Promise<WebError>;
/**
* 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<void>;
}
/**
@ -8992,6 +8996,8 @@ export interface JSHandle<T = any> {
* @param propertyName property to get
*/
getProperty(propertyName: string): Promise<JSHandle>;
[Symbol.asyncDispose](): Promise<void>;
}
/**
@ -13747,6 +13753,8 @@ export interface ElectronApplication {
* Convenience method that returns all the opened windows.
*/
windows(): Array<Page>;
[Symbol.asyncDispose](): Promise<void>;
}
export type AndroidElementInfo = {
@ -14806,6 +14814,8 @@ export interface AndroidDevice {
webViews(): Array<AndroidWebView>;
input: AndroidInput;
[Symbol.asyncDispose](): Promise<void>;
}
export interface AndroidInput {
@ -14941,6 +14951,8 @@ export interface AndroidSocket {
* @param data Data to write.
*/
write(data: Buffer): Promise<void>;
[Symbol.asyncDispose](): Promise<void>;
}
/**
@ -15880,6 +15892,8 @@ export interface APIRequestContext {
}>;
}>;
}>;
[Symbol.asyncDispose](): Promise<void>;
}
/**
@ -15950,6 +15964,8 @@ export interface APIResponse {
* Contains the URL of the response.
*/
url(): string;
[Symbol.asyncDispose](): Promise<void>;
}
/**
@ -16545,6 +16561,8 @@ export interface Browser extends EventEmitter {
* Returns the browser version.
*/
version(): string;
[Symbol.asyncDispose](): Promise<void>;
}
export interface BrowserServer {
@ -16601,6 +16619,8 @@ export interface BrowserServer {
* to establish connection to the browser.
*/
wsEndpoint(): string;
[Symbol.asyncDispose](): Promise<void>;
}
/**

View File

@ -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

View File

@ -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",

View File

@ -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",

View File

@ -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')],

View File

@ -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}"`)
}

View File

@ -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' }));
},
});

View File

@ -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');
});

View File

@ -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(`<launching>`);
expect(error.message).toContain(`<launched> pid=`);
expect(error!.message).toContain(`browserType.launch: Timeout 5000ms exceeded.`);
expect(error!.message).toContain(`<launching>`);
expect(error!.message).toContain(`<launched> 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('<launching>');
expect(error!.message).toContain('<launching>');
});
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();
});

View File

@ -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
*/

View File

@ -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();
}
}
}
/**

View File

@ -1,7 +1,7 @@
{
"compilerOptions": {
"strict": true,
"target": "ES2019",
"target": "ESNext",
"noEmit": true,
"moduleResolution": "node",
},