diff --git a/docs/src/api/class-electron.md b/docs/src/electron-api/class-electron.md similarity index 57% rename from docs/src/api/class-electron.md rename to docs/src/electron-api/class-electron.md index 1df55a214a252..ef12de7368ea7 100644 --- a/docs/src/api/class-electron.md +++ b/docs/src/electron-api/class-electron.md @@ -2,16 +2,17 @@ * since: v1.9 * langs: js -Playwright has **experimental** support for Electron automation. You can access electron namespace via: +Playwright has **experimental** support for Electron automation, shipped as a +separate package: -```js -const { _electron } = require('playwright'); +```sh +npm i -D @playwright/experimental-electron ``` An example of the Electron automation script would be: ```js -const { _electron: electron } = require('playwright'); +import { electron } from '@playwright/experimental-electron'; (async () => { // Launch Electron app. @@ -89,59 +90,5 @@ Specifies environment variables that will be visible to Electron. Defaults to `p Maximum time in milliseconds to wait for the application to start. Defaults to `30000` (30 seconds). Pass `0` to disable timeout. -### option: Electron.launch.acceptdownloads = %%-context-option-acceptdownloads-%% -* since: v1.12 - -### option: Electron.launch.bypassCSP = %%-context-option-bypasscsp-%% -* since: v1.12 - -### option: Electron.launch.colorScheme = %%-context-option-colorscheme-%% -* since: v1.12 - -### option: Electron.launch.extraHTTPHeaders = %%-context-option-extrahttpheaders-%% -* since: v1.12 - -### option: Electron.launch.geolocation = %%-context-option-geolocation-%% -* since: v1.12 - -### option: Electron.launch.httpcredentials = %%-context-option-httpcredentials-%% -* since: v1.12 - -### option: Electron.launch.ignoreHTTPSErrors = %%-context-option-ignorehttpserrors-%% -* since: v1.12 - -### option: Electron.launch.locale = %%-context-option-locale-%% -* since: v1.12 - -### option: Electron.launch.offline = %%-context-option-offline-%% -* since: v1.12 - -### option: Electron.launch.recordhar = %%-context-option-recordhar-%% -* since: v1.12 - -### option: Electron.launch.recordharpath = %%-context-option-recordhar-path-%% -* since: v1.12 - -### option: Electron.launch.recordHarOmitContent = %%-context-option-recordhar-omit-content-%% -* since: v1.12 - -### option: Electron.launch.recordvideo = %%-context-option-recordvideo-%% -* since: v1.12 - -### option: Electron.launch.recordvideodir = %%-context-option-recordvideo-dir-%% -* since: v1.12 - -### option: Electron.launch.recordvideosize = %%-context-option-recordvideo-size-%% -* since: v1.12 - -### option: Electron.launch.timezoneId = %%-context-option-timezoneid-%% -* since: v1.12 - -### option: Electron.launch.tracesDir = %%-browser-option-tracesdir-%% -* since: v1.36 - -### option: Electron.launch.artifactsDir = %%-browser-option-artifactsdir-%% -* since: v1.59 - ### option: Electron.launch.chromiumSandbox = %%-browser-option-chromiumsandbox-%% * since: v1.59 diff --git a/docs/src/api/class-electronapplication.md b/docs/src/electron-api/class-electronapplication.md similarity index 100% rename from docs/src/api/class-electronapplication.md rename to docs/src/electron-api/class-electronapplication.md diff --git a/package-lock.json b/package-lock.json index 154f1bdb5dcb4..231ca4c1cf8a8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1983,6 +1983,10 @@ "resolved": "packages/playwright-ct-vue", "link": true }, + "node_modules/@playwright/experimental-electron": { + "resolved": "packages/playwright-electron", + "link": true + }, "node_modules/@playwright/test": { "resolved": "packages/playwright-test", "link": true @@ -9721,6 +9725,17 @@ "node": ">=18" } }, + "packages/playwright-electron": { + "name": "@playwright/experimental-electron", + "version": "1.60.0-next", + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.60.0-next" + }, + "engines": { + "node": ">=18" + } + }, "packages/playwright-firefox": { "version": "1.60.0-next", "hasInstallScript": true, diff --git a/packages/isomorphic/protocolMetainfo.ts b/packages/isomorphic/protocolMetainfo.ts index f12fe5e240b19..7f002c2be6db4 100644 --- a/packages/isomorphic/protocolMetainfo.ts +++ b/packages/isomorphic/protocolMetainfo.ts @@ -71,7 +71,6 @@ export const methodMetainfo = new Map([ ['Worker.waitForEventInfo', { title: 'Wait for event "{info.event}"', snapshot: true, }], ['WebSocket.waitForEventInfo', { title: 'Wait for event "{info.event}"', snapshot: true, }], ['Debugger.waitForEventInfo', { title: 'Wait for event "{info.event}"', snapshot: true, }], - ['ElectronApplication.waitForEventInfo', { title: 'Wait for event "{info.event}"', snapshot: true, }], ['AndroidDevice.waitForEventInfo', { title: 'Wait for event "{info.event}"', snapshot: true, }], ['BrowserContext.addCookies', { title: 'Add cookies', group: 'configuration', }], ['BrowserContext.addInitScript', { title: 'Add init script', group: 'configuration', }], @@ -304,11 +303,6 @@ export const methodMetainfo = new Map([ ['WritableStream.close', { internal: true, }], ['CDPSession.send', { title: 'Send CDP command', group: 'configuration', }], ['CDPSession.detach', { title: 'Detach CDP session', group: 'configuration', }], - ['Electron.launch', { title: 'Launch electron', }], - ['ElectronApplication.browserWindow', { internal: true, }], - ['ElectronApplication.evaluateExpression', { title: 'Evaluate', }], - ['ElectronApplication.evaluateExpressionHandle', { title: 'Evaluate', }], - ['ElectronApplication.updateSubscription', { internal: true, }], ['Android.devices', { internal: true, }], ['AndroidSocket.write', { internal: true, }], ['AndroidSocket.close', { internal: true, }], diff --git a/packages/playwright-client/types/types.d.ts b/packages/playwright-client/types/types.d.ts index e1e1e00b155aa..18a357063612d 100644 --- a/packages/playwright-client/types/types.d.ts +++ b/packages/playwright-client/types/types.d.ts @@ -16419,366 +16419,6 @@ class TimeoutError extends Error { export const devices: Devices; -//@ts-ignore this will be any if electron is not installed -type ElectronType = typeof import('electron'); - -/** - * Electron application representation. You can use - * [electron.launch([options])](https://playwright.dev/docs/api/class-electron#electron-launch) to obtain the - * application instance. This instance you can control main electron process as well as work with Electron windows: - * - * ```js - * const { _electron: electron } = require('playwright'); - * - * (async () => { - * // Launch Electron app. - * const electronApp = await electron.launch({ args: ['main.js'] }); - * - * // Evaluation expression in the Electron context. - * const appPath = await electronApp.evaluate(async ({ app }) => { - * // This runs in the main Electron process, parameter here is always - * // the result of the require('electron') in the main app script. - * return app.getAppPath(); - * }); - * console.log(appPath); - * - * // Get the first window that the app opens, wait if necessary. - * const window = await electronApp.firstWindow(); - * // Print the title. - * console.log(await window.title()); - * // Capture a screenshot. - * await window.screenshot({ path: 'intro.png' }); - * // Direct Electron console to Node terminal. - * window.on('console', console.log); - * // Click button. - * await window.click('text=Click me'); - * // Exit app. - * await electronApp.close(); - * })(); - * ``` - * - */ -export interface ElectronApplication { - /** - * Returns the return value of - * [`pageFunction`](https://playwright.dev/docs/api/class-electronapplication#electron-application-evaluate-option-expression). - * - * If the function passed to the - * [electronApplication.evaluate(pageFunction[, arg])](https://playwright.dev/docs/api/class-electronapplication#electron-application-evaluate) - * returns a [Promise], then - * [electronApplication.evaluate(pageFunction[, arg])](https://playwright.dev/docs/api/class-electronapplication#electron-application-evaluate) - * would wait for the promise to resolve and return its value. - * - * If the function passed to the - * [electronApplication.evaluate(pageFunction[, arg])](https://playwright.dev/docs/api/class-electronapplication#electron-application-evaluate) - * returns a non-[Serializable] value, then - * [electronApplication.evaluate(pageFunction[, arg])](https://playwright.dev/docs/api/class-electronapplication#electron-application-evaluate) - * returns `undefined`. Playwright also supports transferring some additional values that are not serializable by - * `JSON`: `-0`, `NaN`, `Infinity`, `-Infinity`. - * @param pageFunction Function to be evaluated in the main Electron process. - * @param arg Optional argument to pass to - * [`pageFunction`](https://playwright.dev/docs/api/class-electronapplication#electron-application-evaluate-option-expression). - */ - evaluate(pageFunction: PageFunctionOn, arg: Arg): Promise; - /** - * Returns the return value of - * [`pageFunction`](https://playwright.dev/docs/api/class-electronapplication#electron-application-evaluate-option-expression). - * - * If the function passed to the - * [electronApplication.evaluate(pageFunction[, arg])](https://playwright.dev/docs/api/class-electronapplication#electron-application-evaluate) - * returns a [Promise], then - * [electronApplication.evaluate(pageFunction[, arg])](https://playwright.dev/docs/api/class-electronapplication#electron-application-evaluate) - * would wait for the promise to resolve and return its value. - * - * If the function passed to the - * [electronApplication.evaluate(pageFunction[, arg])](https://playwright.dev/docs/api/class-electronapplication#electron-application-evaluate) - * returns a non-[Serializable] value, then - * [electronApplication.evaluate(pageFunction[, arg])](https://playwright.dev/docs/api/class-electronapplication#electron-application-evaluate) - * returns `undefined`. Playwright also supports transferring some additional values that are not serializable by - * `JSON`: `-0`, `NaN`, `Infinity`, `-Infinity`. - * @param pageFunction Function to be evaluated in the main Electron process. - * @param arg Optional argument to pass to - * [`pageFunction`](https://playwright.dev/docs/api/class-electronapplication#electron-application-evaluate-option-expression). - */ - evaluate(pageFunction: PageFunctionOn, arg?: any): Promise; - - /** - * Returns the return value of - * [`pageFunction`](https://playwright.dev/docs/api/class-electronapplication#electron-application-evaluate-handle-option-expression) - * as a [JSHandle](https://playwright.dev/docs/api/class-jshandle). - * - * The only difference between - * [electronApplication.evaluate(pageFunction[, arg])](https://playwright.dev/docs/api/class-electronapplication#electron-application-evaluate) - * and - * [electronApplication.evaluateHandle(pageFunction[, arg])](https://playwright.dev/docs/api/class-electronapplication#electron-application-evaluate-handle) - * is that - * [electronApplication.evaluateHandle(pageFunction[, arg])](https://playwright.dev/docs/api/class-electronapplication#electron-application-evaluate-handle) - * returns [JSHandle](https://playwright.dev/docs/api/class-jshandle). - * - * If the function passed to the - * [electronApplication.evaluateHandle(pageFunction[, arg])](https://playwright.dev/docs/api/class-electronapplication#electron-application-evaluate-handle) - * returns a [Promise], then - * [electronApplication.evaluateHandle(pageFunction[, arg])](https://playwright.dev/docs/api/class-electronapplication#electron-application-evaluate-handle) - * would wait for the promise to resolve and return its value. - * @param pageFunction Function to be evaluated in the main Electron process. - * @param arg Optional argument to pass to - * [`pageFunction`](https://playwright.dev/docs/api/class-electronapplication#electron-application-evaluate-handle-option-expression). - */ - evaluateHandle(pageFunction: PageFunctionOn, arg: Arg): Promise>; - /** - * Returns the return value of - * [`pageFunction`](https://playwright.dev/docs/api/class-electronapplication#electron-application-evaluate-handle-option-expression) - * as a [JSHandle](https://playwright.dev/docs/api/class-jshandle). - * - * The only difference between - * [electronApplication.evaluate(pageFunction[, arg])](https://playwright.dev/docs/api/class-electronapplication#electron-application-evaluate) - * and - * [electronApplication.evaluateHandle(pageFunction[, arg])](https://playwright.dev/docs/api/class-electronapplication#electron-application-evaluate-handle) - * is that - * [electronApplication.evaluateHandle(pageFunction[, arg])](https://playwright.dev/docs/api/class-electronapplication#electron-application-evaluate-handle) - * returns [JSHandle](https://playwright.dev/docs/api/class-jshandle). - * - * If the function passed to the - * [electronApplication.evaluateHandle(pageFunction[, arg])](https://playwright.dev/docs/api/class-electronapplication#electron-application-evaluate-handle) - * returns a [Promise], then - * [electronApplication.evaluateHandle(pageFunction[, arg])](https://playwright.dev/docs/api/class-electronapplication#electron-application-evaluate-handle) - * would wait for the promise to resolve and return its value. - * @param pageFunction Function to be evaluated in the main Electron process. - * @param arg Optional argument to pass to - * [`pageFunction`](https://playwright.dev/docs/api/class-electronapplication#electron-application-evaluate-handle-option-expression). - */ - evaluateHandle(pageFunction: PageFunctionOn, arg?: any): Promise>; - /** - * This event is issued when the application process has been terminated. - */ - on(event: 'close', listener: () => any): this; - - /** - * Emitted when JavaScript within the Electron main process calls one of console API methods, e.g. `console.log` or - * `console.dir`. - * - * The arguments passed into `console.log` are available on the - * [ConsoleMessage](https://playwright.dev/docs/api/class-consolemessage) event handler argument. - * - * **Usage** - * - * ```js - * electronApp.on('console', async msg => { - * const values = []; - * for (const arg of msg.args()) - * values.push(await arg.jsonValue()); - * console.log(...values); - * }); - * await electronApp.evaluate(() => console.log('hello', 5, { foo: 'bar' })); - * ``` - * - */ - on(event: 'console', listener: (consoleMessage: ConsoleMessage) => any): this; - - /** - * This event is issued for every window that is created **and loaded** in Electron. It contains a - * [Page](https://playwright.dev/docs/api/class-page) that can be used for Playwright automation. - */ - on(event: 'window', listener: (page: Page) => any): 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: 'close', listener: () => any): 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: 'console', listener: (consoleMessage: ConsoleMessage) => any): 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: 'window', listener: (page: Page) => any): this; - - /** - * This event is issued when the application process has been terminated. - */ - addListener(event: 'close', listener: () => any): this; - - /** - * Emitted when JavaScript within the Electron main process calls one of console API methods, e.g. `console.log` or - * `console.dir`. - * - * The arguments passed into `console.log` are available on the - * [ConsoleMessage](https://playwright.dev/docs/api/class-consolemessage) event handler argument. - * - * **Usage** - * - * ```js - * electronApp.on('console', async msg => { - * const values = []; - * for (const arg of msg.args()) - * values.push(await arg.jsonValue()); - * console.log(...values); - * }); - * await electronApp.evaluate(() => console.log('hello', 5, { foo: 'bar' })); - * ``` - * - */ - addListener(event: 'console', listener: (consoleMessage: ConsoleMessage) => any): this; - - /** - * This event is issued for every window that is created **and loaded** in Electron. It contains a - * [Page](https://playwright.dev/docs/api/class-page) that can be used for Playwright automation. - */ - addListener(event: 'window', listener: (page: Page) => any): this; - - /** - * Removes an event listener added by `on` or `addListener`. - */ - removeListener(event: 'close', listener: () => any): this; - - /** - * Removes an event listener added by `on` or `addListener`. - */ - removeListener(event: 'console', listener: (consoleMessage: ConsoleMessage) => any): this; - - /** - * Removes an event listener added by `on` or `addListener`. - */ - removeListener(event: 'window', listener: (page: Page) => any): this; - - /** - * Removes an event listener added by `on` or `addListener`. - */ - off(event: 'close', listener: () => any): this; - - /** - * Removes an event listener added by `on` or `addListener`. - */ - off(event: 'console', listener: (consoleMessage: ConsoleMessage) => any): this; - - /** - * Removes an event listener added by `on` or `addListener`. - */ - off(event: 'window', listener: (page: Page) => any): this; - - /** - * This event is issued when the application process has been terminated. - */ - prependListener(event: 'close', listener: () => any): this; - - /** - * Emitted when JavaScript within the Electron main process calls one of console API methods, e.g. `console.log` or - * `console.dir`. - * - * The arguments passed into `console.log` are available on the - * [ConsoleMessage](https://playwright.dev/docs/api/class-consolemessage) event handler argument. - * - * **Usage** - * - * ```js - * electronApp.on('console', async msg => { - * const values = []; - * for (const arg of msg.args()) - * values.push(await arg.jsonValue()); - * console.log(...values); - * }); - * await electronApp.evaluate(() => console.log('hello', 5, { foo: 'bar' })); - * ``` - * - */ - prependListener(event: 'console', listener: (consoleMessage: ConsoleMessage) => any): this; - - /** - * This event is issued for every window that is created **and loaded** in Electron. It contains a - * [Page](https://playwright.dev/docs/api/class-page) that can be used for Playwright automation. - */ - prependListener(event: 'window', listener: (page: Page) => any): this; - - /** - * Returns the BrowserWindow object that corresponds to the given Playwright page. - * @param page Page to retrieve the window for. - */ - browserWindow(page: Page): Promise; - - /** - * Closes Electron application. - */ - close(): Promise; - - /** - * This method returns browser context that can be used for setting up context-wide routing, etc. - */ - context(): BrowserContext; - - /** - * Convenience method that waits for the first application window to be opened. - * - * **Usage** - * - * ```js - * const electronApp = await electron.launch({ - * args: ['main.js'] - * }); - * const window = await electronApp.firstWindow(); - * // ... - * ``` - * - * @param options - */ - firstWindow(options?: { - /** - * Maximum time to wait for in milliseconds. Defaults to `30000` (30 seconds). Pass `0` to disable timeout. The - * default value can be changed by using the - * [browserContext.setDefaultTimeout(timeout)](https://playwright.dev/docs/api/class-browsercontext#browser-context-set-default-timeout). - */ - timeout?: number; - }): Promise; - - /** - * Returns the main process for this Electron Application. - */ - process(): ChildProcess; - - /** - * This event is issued when the application process has been terminated. - */ - waitForEvent(event: 'close', optionsOrPredicate?: { predicate?: () => boolean | Promise, timeout?: number } | (() => boolean | Promise)): Promise; - - /** - * Emitted when JavaScript within the Electron main process calls one of console API methods, e.g. `console.log` or - * `console.dir`. - * - * The arguments passed into `console.log` are available on the - * [ConsoleMessage](https://playwright.dev/docs/api/class-consolemessage) event handler argument. - * - * **Usage** - * - * ```js - * electronApp.on('console', async msg => { - * const values = []; - * for (const arg of msg.args()) - * values.push(await arg.jsonValue()); - * console.log(...values); - * }); - * await electronApp.evaluate(() => console.log('hello', 5, { foo: 'bar' })); - * ``` - * - */ - waitForEvent(event: 'console', optionsOrPredicate?: { predicate?: (consoleMessage: ConsoleMessage) => boolean | Promise, timeout?: number } | ((consoleMessage: ConsoleMessage) => boolean | Promise)): Promise; - - /** - * This event is issued for every window that is created **and loaded** in Electron. It contains a - * [Page](https://playwright.dev/docs/api/class-page) that can be used for Playwright automation. - */ - waitForEvent(event: 'window', optionsOrPredicate?: { predicate?: (page: Page) => boolean | Promise, timeout?: number } | ((page: Page) => boolean | Promise)): Promise; - - - /** - * Convenience method that returns all the opened windows. - */ - windows(): Array; - - [Symbol.asyncDispose](): Promise; -} - export type AndroidElementInfo = { clazz: string; desc: string; @@ -16884,7 +16524,6 @@ export type AndroidKey = 'Copy' | 'Paste'; -export const _electron: Electron; export const _android: Android; // This is required to not export everything by default. See https://github.com/Microsoft/TypeScript/issues/19545#issuecomment-340490459 @@ -19903,292 +19542,6 @@ export interface Download { url(): string; } -/** - * Playwright has **experimental** support for Electron automation. You can access electron namespace via: - * - * ```js - * const { _electron } = require('playwright'); - * ``` - * - * An example of the Electron automation script would be: - * - * ```js - * const { _electron: electron } = require('playwright'); - * - * (async () => { - * // Launch Electron app. - * const electronApp = await electron.launch({ args: ['main.js'] }); - * - * // Evaluation expression in the Electron context. - * const appPath = await electronApp.evaluate(async ({ app }) => { - * // This runs in the main Electron process, parameter here is always - * // the result of the require('electron') in the main app script. - * return app.getAppPath(); - * }); - * console.log(appPath); - * - * // Get the first window that the app opens, wait if necessary. - * const window = await electronApp.firstWindow(); - * // Print the title. - * console.log(await window.title()); - * // Capture a screenshot. - * await window.screenshot({ path: 'intro.png' }); - * // Direct Electron console to Node terminal. - * window.on('console', console.log); - * // Click button. - * await window.click('text=Click me'); - * // Exit app. - * await electronApp.close(); - * })(); - * ``` - * - * **Supported Electron versions are:** - * - v12.2.0+ - * - v13.4.0+ - * - v14+ - * - * **Known issues:** - * - * If you are not able to launch Electron and it will end up in timeouts during launch, try the following: - * - Ensure that `nodeCliInspect` - * ([FuseV1Options.EnableNodeCliInspectArguments](https://www.electronjs.org/docs/latest/tutorial/fuses#nodecliinspect)) - * fuse is **not** set to `false`. - */ -export interface Electron { - /** - * Launches electron application specified with the - * [`executablePath`](https://playwright.dev/docs/api/class-electron#electron-launch-option-executable-path). - * @param options - */ - launch(options?: { - /** - * Whether to automatically download all the attachments. Defaults to `true` where all the downloads are accepted. - */ - acceptDownloads?: boolean; - - /** - * Additional arguments to pass to the application when launching. You typically pass the main script name here. - */ - args?: Array; - - /** - * If specified, artifacts (traces, videos, downloads, HAR files, etc.) are saved into this directory. The directory - * is not cleaned up when the browser closes. If not specified, a temporary directory is used and cleaned up when the - * browser closes. - */ - artifactsDir?: string; - - /** - * Toggles bypassing page's Content-Security-Policy. Defaults to `false`. - */ - bypassCSP?: boolean; - - /** - * Enable Chromium sandboxing. Defaults to `false`. - */ - chromiumSandbox?: boolean; - - /** - * Emulates [prefers-colors-scheme](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme) - * media feature, supported values are `'light'` and `'dark'`. See - * [page.emulateMedia([options])](https://playwright.dev/docs/api/class-page#page-emulate-media) for more details. - * Passing `null` resets emulation to system defaults. Defaults to `'light'`. - */ - colorScheme?: null|"light"|"dark"|"no-preference"; - - /** - * Current working directory to launch application from. - */ - cwd?: string; - - /** - * Specifies environment variables that will be visible to Electron. Defaults to `process.env`. - */ - env?: { [key: string]: string; }; - - /** - * Launches given Electron application. If not specified, launches the default Electron executable installed in this - * package, located at `node_modules/.bin/electron`. - */ - executablePath?: string; - - /** - * An object containing additional HTTP headers to be sent with every request. Defaults to none. - */ - extraHTTPHeaders?: { [key: string]: string; }; - - geolocation?: { - /** - * Latitude between -90 and 90. - */ - latitude: number; - - /** - * Longitude between -180 and 180. - */ - longitude: number; - - /** - * Non-negative accuracy value. Defaults to `0`. - */ - accuracy?: number; - }; - - /** - * Credentials for [HTTP authentication](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication). If no - * origin is specified, the username and password are sent to any servers upon unauthorized responses. - */ - httpCredentials?: { - username: string; - - password: string; - - /** - * Restrain sending http credentials on specific origin (scheme://host:port). - */ - origin?: string; - - /** - * This option only applies to the requests sent from corresponding - * [APIRequestContext](https://playwright.dev/docs/api/class-apirequestcontext) and does not affect requests sent from - * the browser. `'always'` - `Authorization` header with basic authentication credentials will be sent with the each - * API request. `'unauthorized` - the credentials are only sent when 401 (Unauthorized) response with - * `WWW-Authenticate` header is received. Defaults to `'unauthorized'`. - */ - send?: "unauthorized"|"always"; - }; - - /** - * Whether to ignore HTTPS errors when sending network requests. Defaults to `false`. - */ - ignoreHTTPSErrors?: boolean; - - /** - * Specify user locale, for example `en-GB`, `de-DE`, etc. Locale will affect `navigator.language` value, - * `Accept-Language` request header value as well as number and date formatting rules. Defaults to the system default - * locale. Learn more about emulation in our [emulation guide](https://playwright.dev/docs/emulation#locale--timezone). - */ - locale?: string; - - /** - * Whether to emulate network being offline. Defaults to `false`. Learn more about - * [network emulation](https://playwright.dev/docs/emulation#offline). - */ - offline?: boolean; - - /** - * Enables [HAR](http://www.softwareishard.com/blog/har-12-spec) recording for all pages into `recordHar.path` file. - * If not specified, the HAR is not recorded. Make sure to await - * [browserContext.close([options])](https://playwright.dev/docs/api/class-browsercontext#browser-context-close) for - * the HAR to be saved. - */ - recordHar?: { - /** - * Optional setting to control whether to omit request content from the HAR. Defaults to `false`. Deprecated, use - * `content` policy instead. - */ - omitContent?: boolean; - - /** - * Optional setting to control resource content management. If `omit` is specified, content is not persisted. If - * `attach` is specified, resources are persisted as separate files or entries in the ZIP archive. If `embed` is - * specified, content is stored inline the HAR file as per HAR specification. Defaults to `attach` for `.zip` output - * files and to `embed` for all other file extensions. - */ - content?: "omit"|"embed"|"attach"; - - /** - * Path on the filesystem to write the HAR file to. If the file name ends with `.zip`, `content: 'attach'` is used by - * default. - */ - path: string; - - /** - * When set to `minimal`, only record information necessary for routing from HAR. This omits sizes, timing, page, - * cookies, security and other types of HAR information that are not used when replaying from HAR. Defaults to `full`. - */ - mode?: "full"|"minimal"; - - /** - * A glob or regex pattern to filter requests that are stored in the HAR. When a - * [`baseURL`](https://playwright.dev/docs/api/class-browser#browser-new-context-option-base-url) via the context - * options was provided and the passed URL is a path, it gets merged via the - * [`new URL()`](https://developer.mozilla.org/en-US/docs/Web/API/URL/URL) constructor. Defaults to none. - */ - urlFilter?: string|RegExp; - }; - - /** - * Enables video recording for all pages into `recordVideo.dir` directory. If not specified videos are not recorded. - * Make sure to await - * [browserContext.close([options])](https://playwright.dev/docs/api/class-browsercontext#browser-context-close) for - * videos to be saved. - */ - recordVideo?: { - /** - * Path to the directory to put videos into. If not specified, the videos will be stored in `artifactsDir` (see - * [browserType.launch([options])](https://playwright.dev/docs/api/class-browsertype#browser-type-launch) options). - */ - dir?: string; - - /** - * Optional dimensions of the recorded videos. If not specified the size will be equal to `viewport` scaled down to - * fit into 800x800. If `viewport` is not configured explicitly the video size defaults to 800x450. Actual picture of - * each page will be scaled down if necessary to fit the specified size. - */ - size?: { - /** - * Video frame width. - */ - width: number; - - /** - * Video frame height. - */ - height: number; - }; - - /** - * If specified, enables visual annotations on interacted elements during video recording. - */ - showActions?: { - /** - * How long each annotation is displayed in milliseconds. Defaults to `500`. - */ - duration?: number; - - /** - * Position of the action title overlay. Defaults to `"top-right"`. - */ - position?: "top-left"|"top"|"top-right"|"bottom-left"|"bottom"|"bottom-right"; - - /** - * Font size of the action title in pixels. Defaults to `24`. - */ - fontSize?: number; - }; - }; - - /** - * Maximum time in milliseconds to wait for the application to start. Defaults to `30000` (30 seconds). Pass `0` to - * disable timeout. - */ - timeout?: number; - - /** - * Changes the timezone of the context. See - * [ICU's metaZones.txt](https://cs.chromium.org/chromium/src/third_party/icu/source/data/misc/metaZones.txt?rcl=faee8bc70570192d82d2978a71e2a615788597d1) - * for a list of supported timezone IDs. Defaults to the system timezone. - */ - timezoneId?: string; - - /** - * If specified, traces are saved into this directory. - */ - tracesDir?: string; - }): Promise; -} - /** * [FileChooser](https://playwright.dev/docs/api/class-filechooser) objects are dispatched by the page in the * [page.on('filechooser')](https://playwright.dev/docs/api/class-page#page-event-file-chooser) event. diff --git a/packages/playwright-core/index.mjs b/packages/playwright-core/index.mjs index 3b3c75b0f7fa7..7df237cb644f5 100644 --- a/packages/playwright-core/index.mjs +++ b/packages/playwright-core/index.mjs @@ -23,6 +23,5 @@ export const selectors = playwright.selectors; export const devices = playwright.devices; export const errors = playwright.errors; export const request = playwright.request; -export const _electron = playwright._electron; export const _android = playwright._android; export default playwright; diff --git a/packages/playwright-core/src/client/api.ts b/packages/playwright-core/src/client/api.ts index 3a36d03285cef..53f3888a9de84 100644 --- a/packages/playwright-core/src/client/api.ts +++ b/packages/playwright-core/src/client/api.ts @@ -26,7 +26,6 @@ export { Debugger } from './debugger'; export { Dialog } from './dialog'; export type { Disposable } from './disposable'; export { Download } from './download'; -export { Electron, ElectronApplication } from './electron'; export { FrameLocator, Locator } from './locator'; export { ElementHandle } from './elementHandle'; export { FileChooser } from './fileChooser'; diff --git a/packages/playwright-core/src/client/connection.ts b/packages/playwright-core/src/client/connection.ts index 15db5e3d1c598..594acfd0fab0f 100644 --- a/packages/playwright-core/src/client/connection.ts +++ b/packages/playwright-core/src/client/connection.ts @@ -27,7 +27,6 @@ import { createInstrumentation } from './clientInstrumentation'; import { Debugger } from './debugger'; import { Dialog } from './dialog'; import { DisposableObject } from './disposable'; -import { Electron, ElectronApplication } from './electron'; import { ElementHandle } from './elementHandle'; import { TargetClosedError, parseError } from './errors'; import { APIRequestContext } from './fetch'; @@ -276,12 +275,6 @@ export class Connection extends EventEmitter { case 'Disposable': result = new DisposableObject(parent, type, guid, initializer); break; - case 'Electron': - result = new Electron(parent, type, guid, initializer); - break; - case 'ElectronApplication': - result = new ElectronApplication(parent, type, guid, initializer); - break; case 'ElementHandle': result = new ElementHandle(parent, type, guid, initializer); break; diff --git a/packages/playwright-core/src/client/consoleMessage.ts b/packages/playwright-core/src/client/consoleMessage.ts index fd6427881af68..83ae9eafafda2 100644 --- a/packages/playwright-core/src/client/consoleMessage.ts +++ b/packages/playwright-core/src/client/consoleMessage.ts @@ -28,9 +28,9 @@ export class ConsoleMessage implements api.ConsoleMessage { private _page: Page | null; private _worker: Worker | null; - private _event: channels.BrowserContextConsoleEvent | channels.WorkerConsoleEvent | channels.ElectronApplicationConsoleEvent; + private _event: channels.BrowserContextConsoleEvent | channels.WorkerConsoleEvent; - constructor(platform: Platform, event: channels.BrowserContextConsoleEvent | channels.WorkerConsoleEvent | channels.ElectronApplicationConsoleEvent, page: Page | null, worker: Worker | null) { + constructor(platform: Platform, event: channels.BrowserContextConsoleEvent | channels.WorkerConsoleEvent, page: Page | null, worker: Worker | null) { this._page = page; this._worker = worker; this._event = event; diff --git a/packages/playwright-core/src/client/electron.ts b/packages/playwright-core/src/client/electron.ts deleted file mode 100644 index 2399aa61ff1ce..0000000000000 --- a/packages/playwright-core/src/client/electron.ts +++ /dev/null @@ -1,169 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { BrowserContext, prepareBrowserContextParams } from './browserContext'; -import { ChannelOwner } from './channelOwner'; -import { envObjectToArray } from './clientHelper'; -import { ConsoleMessage } from './consoleMessage'; -import { TargetClosedError, isTargetClosedError } from './errors'; -import { Events } from './events'; -import { JSHandle, parseResult, serializeArgument } from './jsHandle'; -import { Waiter } from './waiter'; -import { TimeoutSettings } from './timeoutSettings'; - -import type { Page } from './page'; -import type { BrowserContextOptions, Headers, WaitForEventOptions } from './types'; -import type * as structs from '../../types/structs'; -import type * as api from '../../types/types'; -import type * as channels from '@protocol/channels'; -import type * as childProcess from 'child_process'; -import type { BrowserWindow } from 'electron'; -import type { Playwright } from './playwright'; - -type ElectronOptions = Omit & { - env?: NodeJS.ProcessEnv, - extraHTTPHeaders?: Headers, - recordHar?: BrowserContextOptions['recordHar'], - colorScheme?: 'dark' | 'light' | 'no-preference' | null, - acceptDownloads?: boolean, - timeout?: number, -}; - -type ElectronAppType = typeof import('electron'); - -export class Electron extends ChannelOwner implements api.Electron { - _playwright!: Playwright; - - static from(electron: channels.ElectronChannel): Electron { - return (electron as any)._object; - } - - constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.ElectronInitializer) { - super(parent, type, guid, initializer); - } - - async launch(options: ElectronOptions = {}): Promise { - options = this._playwright.selectors._withSelectorOptions(options); - const params: channels.ElectronLaunchParams = { - ...await prepareBrowserContextParams(this._platform, options), - env: envObjectToArray(options.env ? options.env : this._platform.env), - tracesDir: options.tracesDir, - artifactsDir: options.artifactsDir, - timeout: new TimeoutSettings(this._platform).launchTimeout(options), - }; - const app = ElectronApplication.from((await this._channel.launch(params)).electronApplication); - this._playwright.selectors._contextsForSelectors.add(app._context); - app.once(Events.ElectronApplication.Close, () => this._playwright.selectors._contextsForSelectors.delete(app._context)); - await app._context._initializeHarFromOptions(options.recordHar); - app._context.tracing._tracesDir = options.tracesDir; - return app; - } -} - -export class ElectronApplication extends ChannelOwner implements api.ElectronApplication { - readonly _context: BrowserContext; - private _windows = new Set(); - private _timeoutSettings: TimeoutSettings; - - static from(electronApplication: channels.ElectronApplicationChannel): ElectronApplication { - return (electronApplication as any)._object; - } - - constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.ElectronApplicationInitializer) { - super(parent, type, guid, initializer); - - this._timeoutSettings = new TimeoutSettings(this._platform); - this._context = BrowserContext.from(initializer.context); - for (const page of this._context._pages) - this._onPage(page); - this._context.on(Events.BrowserContext.Page, page => this._onPage(page)); - this._channel.on('close', () => { - this.emit(Events.ElectronApplication.Close); - }); - this._channel.on('console', event => this.emit(Events.ElectronApplication.Console, new ConsoleMessage(this._platform, event, null, null))); - this._setEventToSubscriptionMapping(new Map([ - [Events.ElectronApplication.Console, 'console'], - ])); - } - - process(): childProcess.ChildProcess { - return this._connection.toImpl?.(this)?.process(); - } - - _onPage(page: Page) { - this._windows.add(page); - this.emit(Events.ElectronApplication.Window, page); - page.once(Events.Page.Close, () => this._windows.delete(page)); - } - - windows(): Page[] { - // TODO: add ElectronPage class inheriting from Page. - return [...this._windows]; - } - - async firstWindow(options?: { timeout?: number }): Promise { - if (this._windows.size) - return this._windows.values().next().value!; - return await this.waitForEvent('window', options); - } - - context(): BrowserContext { - return this._context; - } - - async [Symbol.asyncDispose]() { - await this.close(); - } - - async close() { - try { - await this._context.close(); - } catch (e) { - if (isTargetClosedError(e)) - return; - throw e; - } - } - - async waitForEvent(event: string, optionsOrPredicate: WaitForEventOptions = {}): Promise { - return await this._wrapApiCall(async () => { - const timeout = this._timeoutSettings.timeout(typeof optionsOrPredicate === 'function' ? {} : optionsOrPredicate); - const predicate = typeof optionsOrPredicate === 'function' ? optionsOrPredicate : optionsOrPredicate.predicate; - const waiter = Waiter.createForEvent(this, event); - waiter.rejectOnTimeout(timeout, `Timeout ${timeout}ms exceeded while waiting for event "${event}"`); - if (event !== Events.ElectronApplication.Close) - waiter.rejectOnEvent(this, Events.ElectronApplication.Close, () => new TargetClosedError()); - const result = await waiter.waitForEvent(this, event, predicate as any); - waiter.dispose(); - return result; - }); - } - - async browserWindow(page: Page): Promise> { - const result = await this._channel.browserWindow({ page: page._channel }); - return JSHandle.from(result.handle); - } - - async evaluate(pageFunction: structs.PageFunctionOn, arg: Arg): Promise { - const result = await this._channel.evaluateExpression({ expression: String(pageFunction), isFunction: typeof pageFunction === 'function', arg: serializeArgument(arg) }); - return parseResult(result.value); - } - - async evaluateHandle(pageFunction: structs.PageFunctionOn, arg: Arg): Promise> { - const result = await this._channel.evaluateExpressionHandle({ expression: String(pageFunction), isFunction: typeof pageFunction === 'function', arg: serializeArgument(arg) }); - return JSHandle.from(result.handle) as any as structs.SmartHandle; - } -} diff --git a/packages/playwright-core/src/client/events.ts b/packages/playwright-core/src/client/events.ts index 5eb1cb711783d..8715fad8253d9 100644 --- a/packages/playwright-core/src/client/events.ts +++ b/packages/playwright-core/src/client/events.ts @@ -93,10 +93,4 @@ export const Events = { Close: 'close', Console: 'console', }, - - ElectronApplication: { - Close: 'close', - Console: 'console', - Window: 'window', - }, }; diff --git a/packages/playwright-core/src/client/playwright.ts b/packages/playwright-core/src/client/playwright.ts index c5f2074aee984..e777e79832a0b 100644 --- a/packages/playwright-core/src/client/playwright.ts +++ b/packages/playwright-core/src/client/playwright.ts @@ -18,7 +18,6 @@ import { Android } from './android'; import { Browser } from './browser'; import { BrowserType } from './browserType'; import { ChannelOwner } from './channelOwner'; -import { Electron } from './electron'; import { TimeoutError } from './errors'; import { APIRequest } from './fetch'; import { Selectors } from './selectors'; @@ -28,7 +27,6 @@ import type { LaunchOptions } from 'playwright-core'; export class Playwright extends ChannelOwner { readonly _android: Android; - readonly _electron: Electron; readonly chromium: BrowserType; readonly firefox: BrowserType; readonly webkit: BrowserType; @@ -53,8 +51,6 @@ export class Playwright extends ChannelOwner { this.webkit._playwright = this; this._android = Android.from(initializer.android); this._android._playwright = this; - this._electron = Electron.from(initializer.electron); - this._electron._playwright = this; this.devices = this._connection.localUtils()?.devices ?? {}; this.selectors = new Selectors(this._connection._platform); this.errors = { TimeoutError }; diff --git a/packages/playwright-core/src/protocol/validator.ts b/packages/playwright-core/src/protocol/validator.ts index cca67f6407c9d..e2602e0016397 100644 --- a/packages/playwright-core/src/protocol/validator.ts +++ b/packages/playwright-core/src/protocol/validator.ts @@ -362,7 +362,6 @@ scheme.PlaywrightInitializer = tObject({ firefox: tChannel(['BrowserType']), webkit: tChannel(['BrowserType']), android: tChannel(['Android']), - electron: tChannel(['Electron']), utils: tOptional(tChannel(['LocalUtils'])), preLaunchedBrowser: tOptional(tChannel(['Browser'])), preConnectedAndroidDevice: tOptional(tChannel(['AndroidDevice'])), @@ -867,7 +866,6 @@ scheme.PageWaitForEventInfoParams = tType('EventTargetWaitForEventInfoParams'); scheme.WorkerWaitForEventInfoParams = tType('EventTargetWaitForEventInfoParams'); scheme.WebSocketWaitForEventInfoParams = tType('EventTargetWaitForEventInfoParams'); scheme.DebuggerWaitForEventInfoParams = tType('EventTargetWaitForEventInfoParams'); -scheme.ElectronApplicationWaitForEventInfoParams = tType('EventTargetWaitForEventInfoParams'); scheme.AndroidDeviceWaitForEventInfoParams = tType('EventTargetWaitForEventInfoParams'); scheme.EventTargetWaitForEventInfoResult = tOptional(tObject({})); scheme.BrowserContextWaitForEventInfoResult = tType('EventTargetWaitForEventInfoResult'); @@ -875,7 +873,6 @@ scheme.PageWaitForEventInfoResult = tType('EventTargetWaitForEventInfoResult'); scheme.WorkerWaitForEventInfoResult = tType('EventTargetWaitForEventInfoResult'); scheme.WebSocketWaitForEventInfoResult = tType('EventTargetWaitForEventInfoResult'); scheme.DebuggerWaitForEventInfoResult = tType('EventTargetWaitForEventInfoResult'); -scheme.ElectronApplicationWaitForEventInfoResult = tType('EventTargetWaitForEventInfoResult'); scheme.AndroidDeviceWaitForEventInfoResult = tType('EventTargetWaitForEventInfoResult'); scheme.BrowserContextInitializer = tObject({ debugger: tChannel(['Debugger']), @@ -2687,95 +2684,6 @@ scheme.CDPSessionSendResult = tObject({ }); scheme.CDPSessionDetachParams = tOptional(tObject({})); scheme.CDPSessionDetachResult = tOptional(tObject({})); -scheme.ElectronInitializer = tOptional(tObject({})); -scheme.ElectronLaunchParams = tObject({ - executablePath: tOptional(tString), - args: tOptional(tArray(tString)), - chromiumSandbox: tOptional(tBoolean), - cwd: tOptional(tString), - env: tOptional(tArray(tType('NameValue'))), - timeout: tFloat, - acceptDownloads: tOptional(tEnum(['accept', 'deny', 'internal-browser-default'])), - bypassCSP: tOptional(tBoolean), - colorScheme: tOptional(tEnum(['dark', 'light', 'no-preference', 'no-override'])), - extraHTTPHeaders: tOptional(tArray(tType('NameValue'))), - geolocation: tOptional(tObject({ - longitude: tFloat, - latitude: tFloat, - accuracy: tOptional(tFloat), - })), - httpCredentials: tOptional(tObject({ - username: tString, - password: tString, - origin: tOptional(tString), - })), - ignoreHTTPSErrors: tOptional(tBoolean), - locale: tOptional(tString), - offline: tOptional(tBoolean), - recordVideo: tOptional(tObject({ - dir: tOptional(tString), - size: tOptional(tObject({ - width: tInt, - height: tInt, - })), - showActions: tOptional(tObject({ - duration: tOptional(tFloat), - position: tOptional(tEnum(['top-left', 'top', 'top-right', 'bottom-left', 'bottom', 'bottom-right'])), - fontSize: tOptional(tInt), - })), - })), - strictSelectors: tOptional(tBoolean), - timezoneId: tOptional(tString), - tracesDir: tOptional(tString), - artifactsDir: tOptional(tString), - selectorEngines: tOptional(tArray(tType('SelectorEngine'))), - testIdAttributeName: tOptional(tString), -}); -scheme.ElectronLaunchResult = tObject({ - electronApplication: tChannel(['ElectronApplication']), -}); -scheme.ElectronApplicationInitializer = tObject({ - context: tChannel(['BrowserContext']), -}); -scheme.ElectronApplicationCloseEvent = tOptional(tObject({})); -scheme.ElectronApplicationConsoleEvent = tObject({ - type: tString, - text: tString, - args: tArray(tChannel(['ElementHandle', 'JSHandle'])), - location: tObject({ - url: tString, - lineNumber: tInt, - columnNumber: tInt, - }), - timestamp: tFloat, -}); -scheme.ElectronApplicationBrowserWindowParams = tObject({ - page: tChannel(['Page']), -}); -scheme.ElectronApplicationBrowserWindowResult = tObject({ - handle: tChannel(['ElementHandle', 'JSHandle']), -}); -scheme.ElectronApplicationEvaluateExpressionParams = tObject({ - expression: tString, - isFunction: tOptional(tBoolean), - arg: tType('SerializedArgument'), -}); -scheme.ElectronApplicationEvaluateExpressionResult = tObject({ - value: tType('SerializedValue'), -}); -scheme.ElectronApplicationEvaluateExpressionHandleParams = tObject({ - expression: tString, - isFunction: tOptional(tBoolean), - arg: tType('SerializedArgument'), -}); -scheme.ElectronApplicationEvaluateExpressionHandleResult = tObject({ - handle: tChannel(['ElementHandle', 'JSHandle']), -}); -scheme.ElectronApplicationUpdateSubscriptionParams = tObject({ - event: tEnum(['console']), - enabled: tBoolean, -}); -scheme.ElectronApplicationUpdateSubscriptionResult = tOptional(tObject({})); scheme.AndroidInitializer = tOptional(tObject({})); scheme.AndroidDevicesParams = tObject({ host: tOptional(tString), diff --git a/packages/playwright-core/src/server/DEPS.list b/packages/playwright-core/src/server/DEPS.list index f9cf05a0b315c..8af50d834faac 100644 --- a/packages/playwright-core/src/server/DEPS.list +++ b/packages/playwright-core/src/server/DEPS.list @@ -25,7 +25,6 @@ node_modules/yazl ./android/ ./bidi/ ./chromium/ -./electron/ ./firefox/ ./webkit/ diff --git a/packages/playwright-core/src/server/browserContext.ts b/packages/playwright-core/src/server/browserContext.ts index 41163bd7e760b..dd210efa887c8 100644 --- a/packages/playwright-core/src/server/browserContext.ts +++ b/packages/playwright-core/src/server/browserContext.ts @@ -725,13 +725,8 @@ export function validateBrowserContextOptions(options: types.BrowserContextOptio throw new Error(`"deviceScaleFactor" option is not supported with null "viewport"`); if (options.noDefaultViewport && !!options.isMobile) throw new Error(`"isMobile" option is not supported with null "viewport"`); - if (options.acceptDownloads === undefined && browserOptions.name !== 'electron') + if (options.acceptDownloads === undefined) options.acceptDownloads = 'accept'; - // Electron requires explicit acceptDownloads: true since we wait for - // https://github.com/electron/electron/pull/41718 to be widely shipped. - // In 6-12 months, we can remove this check. - else if (options.acceptDownloads === undefined && browserOptions.name === 'electron') - options.acceptDownloads = 'internal-browser-default'; if (!options.viewport && !options.noDefaultViewport) options.viewport = { width: 1280, height: 720 }; if (options.proxy) diff --git a/packages/playwright-core/src/server/dispatchers/electronDispatcher.ts b/packages/playwright-core/src/server/dispatchers/electronDispatcher.ts deleted file mode 100644 index 4dcf68d0d1db6..0000000000000 --- a/packages/playwright-core/src/server/dispatchers/electronDispatcher.ts +++ /dev/null @@ -1,95 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { BrowserContextDispatcher } from './browserContextDispatcher'; -import { Dispatcher } from './dispatcher'; -import { JSHandleDispatcher, parseArgument, serializeResult } from './jsHandleDispatcher'; -import { ElectronApplication } from '../electron/electron'; - -import type { RootDispatcher } from './dispatcher'; -import type { PageDispatcher } from './pageDispatcher'; -import type { ConsoleMessage } from '../console'; -import type { Electron } from '../electron/electron'; -import type * as channels from '@protocol/channels'; -import type { Progress } from '@protocol/progress'; - - -export class ElectronDispatcher extends Dispatcher implements channels.ElectronChannel { - _type_Electron = true; - _denyLaunch: boolean; - - constructor(scope: RootDispatcher, electron: Electron, denyLaunch: boolean) { - super(scope, electron, 'Electron', {}); - this._denyLaunch = denyLaunch; - } - - async launch(params: channels.ElectronLaunchParams, progress: Progress): Promise { - if (this._denyLaunch) - throw new Error(`Launching more browsers is not allowed.`); - const electronApplication = await this._object.launch(progress, params); - return { electronApplication: new ElectronApplicationDispatcher(this, electronApplication) }; - } -} - -export class ElectronApplicationDispatcher extends Dispatcher implements channels.ElectronApplicationChannel { - _type_EventTarget = true; - _type_ElectronApplication = true; - private readonly _subscriptions = new Set(); - - constructor(scope: ElectronDispatcher, electronApplication: ElectronApplication) { - super(scope, electronApplication, 'ElectronApplication', { - context: BrowserContextDispatcher.from(scope, electronApplication.context()) - }); - this.addObjectListener(ElectronApplication.Events.Close, () => { - this._dispatchEvent('close'); - this._dispose(); - }); - this.addObjectListener(ElectronApplication.Events.Console, (message: ConsoleMessage) => { - if (!this._subscriptions.has('console')) - return; - this._dispatchEvent('console', { - type: message.type(), - text: message.text(), - args: message.args().map(a => JSHandleDispatcher.fromJSHandle(this, a)), - location: message.location(), - timestamp: message.timestamp(), - }); - }); - } - - async browserWindow(params: channels.ElectronApplicationBrowserWindowParams, progress: Progress): Promise { - const handle = await this._object.browserWindow(progress, (params.page as PageDispatcher).page()); - return { handle: JSHandleDispatcher.fromJSHandle(this, handle) }; - } - - async evaluateExpression(params: channels.ElectronApplicationEvaluateExpressionParams, progress: Progress): Promise { - const handle = await progress.race(this._object._nodeElectronHandlePromise); - return { value: serializeResult(await handle.evaluateExpression(progress, params.expression, { isFunction: params.isFunction }, parseArgument(params.arg))) }; - } - - async evaluateExpressionHandle(params: channels.ElectronApplicationEvaluateExpressionHandleParams, progress: Progress): Promise { - const handle = await progress.race(this._object._nodeElectronHandlePromise); - const result = await handle.evaluateExpressionHandle(progress, params.expression, { isFunction: params.isFunction }, parseArgument(params.arg)); - return { handle: JSHandleDispatcher.fromJSHandle(this, result) }; - } - - async updateSubscription(params: channels.ElectronApplicationUpdateSubscriptionParams, progress: Progress): Promise { - if (params.enabled) - this._subscriptions.add(params.event); - else - this._subscriptions.delete(params.event); - } -} diff --git a/packages/playwright-core/src/server/dispatchers/jsHandleDispatcher.ts b/packages/playwright-core/src/server/dispatchers/jsHandleDispatcher.ts index 8b494360a9d5b..b1d47c0f0bd80 100644 --- a/packages/playwright-core/src/server/dispatchers/jsHandleDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/jsHandleDispatcher.ts @@ -19,13 +19,12 @@ import { ElementHandleDispatcher } from './elementHandlerDispatcher'; import { parseSerializedValue, serializeValue } from '../../protocol/serializers'; import type * as js from '../javascript'; -import type { ElectronApplicationDispatcher } from './electronDispatcher'; import type { FrameDispatcher } from './frameDispatcher'; import type { PageDispatcher, WorkerDispatcher } from './pageDispatcher'; import type * as channels from '@protocol/channels'; import type { Progress } from '@protocol/progress'; -export type JSHandleDispatcherParentScope = PageDispatcher | FrameDispatcher | WorkerDispatcher | ElectronApplicationDispatcher; +export type JSHandleDispatcherParentScope = PageDispatcher | FrameDispatcher | WorkerDispatcher; export class JSHandleDispatcher extends Dispatcher implements channels.JSHandleChannel { _type_JSHandle = true; diff --git a/packages/playwright-core/src/server/dispatchers/playwrightDispatcher.ts b/packages/playwright-core/src/server/dispatchers/playwrightDispatcher.ts index eda7983bd75bd..09b7f0ad904dd 100644 --- a/packages/playwright-core/src/server/dispatchers/playwrightDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/playwrightDispatcher.ts @@ -22,7 +22,6 @@ import { AndroidDeviceDispatcher } from './androidDispatcher'; import { BrowserDispatcher } from './browserDispatcher'; import { BrowserTypeDispatcher } from './browserTypeDispatcher'; import { Dispatcher } from './dispatcher'; -import { ElectronDispatcher } from './electronDispatcher'; import { LocalUtilsDispatcher } from './localUtilsDispatcher'; import { APIRequestContextDispatcher } from './networkDispatchers'; import { SdkObject } from '../instrumentation'; @@ -59,7 +58,6 @@ export class PlaywrightDispatcher extends Dispatcher> = new ManualPromise(); - private _process: childProcess.ChildProcess; - - constructor(parent: SdkObject, browser: CRBrowser, nodeConnection: CRConnection, process: childProcess.ChildProcess) { - super(parent, 'electron-app'); - this._process = process; - this._browserContext = browser._defaultContext as CRBrowserContext; - this._nodeConnection = nodeConnection; - this._nodeSession = nodeConnection.rootSession; - this._nodeSession.on('Runtime.executionContextCreated', async (event: Protocol.Runtime.executionContextCreatedPayload) => { - if (!event.context.auxData || !event.context.auxData.isDefault) - return; - const crExecutionContext = new CRExecutionContext(this._nodeSession, event.context); - this._nodeExecutionContext = new js.ExecutionContext(this, crExecutionContext, 'electron'); - const { result: remoteObject } = await crExecutionContext._client.send('Runtime.evaluate', { - expression: `require('electron')`, - contextId: event.context.id, - // Needed after Electron 28 to get access to require: https://github.com/microsoft/playwright/issues/28048 - includeCommandLineAPI: true, - }); - this._nodeElectronHandlePromise.resolve(new js.JSHandle(this._nodeExecutionContext!, 'object', 'ElectronModule', remoteObject.objectId!)); - }); - this._nodeSession.on('Runtime.consoleAPICalled', event => this._onConsoleAPI(event)); - const appClosePromise = new Promise(f => this.once(ElectronApplication.Events.Close, f)); - this._browserContext.setCustomCloseHandler(async () => { - const electronHandle = await this._nodeElectronHandlePromise; - await electronHandle.evaluate(({ app }) => app.quit()).catch(() => {}); - this._nodeConnection.close(); - await appClosePromise; - }); - } - - async _onConsoleAPI(event: Protocol.Runtime.consoleAPICalledPayload) { - if (event.executionContextId === 0) { - // DevTools protocol stores the last 1000 console messages. These - // messages are always reported even for removed execution contexts. In - // this case, they are marked with executionContextId = 0 and are - // reported upon enabling Runtime agent. - // - // Ignore these messages since: - // - there's no execution context we can use to operate with message - // arguments - // - these messages are reported before Playwright clients can subscribe - // to the 'console' - // page event. - // - // @see https://github.com/GoogleChrome/puppeteer/issues/3865 - return; - } - if (!this._nodeExecutionContext) - return; - const args = event.args.map(arg => createHandle(this._nodeExecutionContext!, arg)); - const message = new ConsoleMessage(null, null, event.type, undefined, args, toConsoleMessageLocation(event.stackTrace), event.timestamp); - this.emit(ElectronApplication.Events.Console, message); - } - - async initialize() { - await this._nodeSession.send('Runtime.enable', {}); - // Delay loading the app until browser is started and the browser targets are configured to auto-attach. - await this._nodeSession.send('Runtime.evaluate', { expression: '__playwright_run()' }); - } - - process(): childProcess.ChildProcess { - return this._process; - } - - context(): BrowserContext { - return this._browserContext; - } - - async close(progress: Progress) { - // This will call BrowserContext.setCustomCloseHandler. - await this._browserContext.close(progress, { reason: 'Application exited' }); - } - - async browserWindow(progress: Progress, page: Page): Promise> { - // Assume CRPage as Electron is always Chromium. - const targetId = (page.delegate as CRPage)._targetId; - const electronHandle = await progress.race(this._nodeElectronHandlePromise); - return await progress.race(electronHandle.evaluateHandle(({ BrowserWindow, webContents }, targetId) => { - const wc = webContents.fromDevToolsTargetId(targetId); - return BrowserWindow.fromWebContents(wc!)!; - }, targetId)); - } -} - -export class Electron extends SdkObject { - constructor(playwright: Playwright) { - super(playwright, 'electron'); - this.logName = 'browser'; - } - - async launch(progress: Progress, options: Omit): Promise { - let app: ElectronApplication | undefined = undefined; - // --remote-debugging-port=0 must be the last playwright's argument, loader.ts relies on it. - let electronArguments = ['--inspect=0', '--remote-debugging-port=0', ...(options.args || [])]; - - if (os.platform() === 'linux') { - if (!options.chromiumSandbox && electronArguments.indexOf('--no-sandbox') === -1) - electronArguments.unshift('--no-sandbox'); - } - - let artifactsDir: string; - const tempDirectories: string[] = []; - if (options.artifactsDir) { - artifactsDir = options.artifactsDir; - } else { - artifactsDir = await progress.race(fs.promises.mkdtemp(ARTIFACTS_FOLDER)); - tempDirectories.push(artifactsDir); - } - const browserLogsCollector = new RecentLogsCollector(); - const env = options.env ? envArrayToObject(options.env) : process.env; - - let command: string; - if (options.executablePath) { - command = options.executablePath; - } else { - try { - // By default we fallback to the Electron App executable path. - // 'electron/index.js' resolves to the actual Electron App. - command = require('electron/index.js'); - } catch (error: any) { - if ((error as NodeJS.ErrnoException)?.code === 'MODULE_NOT_FOUND') { - throw new Error('\n' + wrapInASCIIBox([ - 'Electron executablePath not found!', - 'Please install it using `npm install -D electron` or set the executablePath to your Electron executable.', - ].join('\n'), 1)); - } - throw error; - } - // Only use our own loader for non-packaged apps. - // Packaged apps might have their own command line handling. - electronArguments.unshift('-r', libPath('server', 'electron', 'loader.js')); - } - let shell = false; - if (process.platform === 'win32') { - // On Windows in order to run .cmd files, shell: true is required. - // https://github.com/nodejs/node/issues/52554 - shell = true; - // On Windows, we need to quote the executable path and arguments due to shell: true. - // We allso pass the arguments as a single string due to DEP0190, - // see https://github.com/microsoft/playwright/issues/38278. - command = [command, ...electronArguments].map(arg => `"${escapeDoubleQuotes(arg)}"`).join(' '); - electronArguments = []; - } - - // When debugging Playwright test that runs Electron, NODE_OPTIONS - // will make the debugger attach to Electron's Node. But Playwright - // also needs to attach to drive the automation. Disable external debugging. - delete env.NODE_OPTIONS; - const { launchedProcess, gracefullyClose, kill } = await progress.race(launchProcess({ - command, - args: electronArguments, - env, - log: (message: string) => { - progress.log(message); - browserLogsCollector.log(message); - }, - shell, - stdio: 'pipe', - cwd: options.cwd, - tempDirectories, - attemptToGracefullyClose: () => app!.close(nullProgress), - handleSIGINT: true, - handleSIGTERM: true, - handleSIGHUP: true, - onExit: () => app?.emit(ElectronApplication.Events.Close), - })); - - // All waitForLines must be started immediately. - // Otherwise the lines might come before we are ready. - const waitForXserverError = waitForLine(progress, launchedProcess, /Unable to open X display/).then(() => { - throw new Error([ - 'Unable to open X display!', - `================================`, - 'Most likely this is because there is no X server available.', - "Use 'xvfb-run' on Linux to launch your tests with an emulated display server.", - "For example: 'xvfb-run npm run test:e2e'", - `================================`, - progress.metadata.log - ].join('\n')); - }); - const nodeMatchPromise = waitForLine(progress, launchedProcess, /^Debugger listening on (ws:\/\/.*)$/); - const chromeMatchPromise = waitForLine(progress, launchedProcess, /^DevTools listening on (ws:\/\/.*)$/); - const debuggerDisconnectPromise = waitForLine(progress, launchedProcess, /Waiting for the debugger to disconnect\.\.\./); - - try { - const nodeMatch = await nodeMatchPromise; - const nodeTransport = await WebSocketTransport.connect(progress, nodeMatch[1]); - const nodeConnection = new CRConnection(this, nodeTransport, helper.debugProtocolLogger(), browserLogsCollector); - // Immediately release exiting process under debug. - debuggerDisconnectPromise.then(() => { - nodeTransport.close(); - }).catch(() => {}); - - const chromeMatch = await progress.race(Promise.race([ - chromeMatchPromise, - waitForXserverError, - ])); - const chromeTransport = await WebSocketTransport.connect(progress, chromeMatch[1]); - const browserProcess: BrowserProcess = { - onclose: undefined, - process: launchedProcess, - close: gracefullyClose, - kill - }; - const contextOptions: types.BrowserContextOptions = { - ...options, - noDefaultViewport: true, - }; - const browserOptions: BrowserOptions = { - name: 'electron', - browserType: 'chromium', - headful: true, - persistent: contextOptions, - browserProcess, - protocolLogger: helper.debugProtocolLogger(), - browserLogsCollector, - artifactsDir, - downloadsPath: artifactsDir, - tracesDir: options.tracesDir || artifactsDir, - originalLaunchOptions: {}, - }; - validateBrowserContextOptions(contextOptions, browserOptions); - const browser = await progress.race(CRBrowser.connect(this.attribution.playwright, chromeTransport, browserOptions)); - app = new ElectronApplication(this, browser, nodeConnection, launchedProcess); - await progress.race(app.initialize()); - return app; - } catch (error) { - await progress.race(kill()); - throw error; - } - } -} - -async function waitForLine(progress: Progress, process: childProcess.ChildProcess, regex: RegExp) { - const promise = new ManualPromise(); - // eslint-disable-next-line no-restricted-properties - const rl = readline.createInterface({ input: process.stderr! }); - const failError = new Error('Process failed to launch!'); - const listeners = [ - eventsHelper.addEventListener(rl, 'line', onLine), - eventsHelper.addEventListener(rl, 'close', () => promise.reject(failError)), - eventsHelper.addEventListener(process, 'exit', () => promise.reject(failError)), - // It is Ok to remove error handler because we did not create process and there is another listener. - eventsHelper.addEventListener(process, 'error', () => promise.reject(failError)), - ]; - - function onLine(line: string) { - const match = line.match(regex); - if (match) - promise.resolve(match); - } - - try { - return await progress.race(promise); - } finally { - eventsHelper.removeEventListeners(listeners); - } -} - -function escapeDoubleQuotes(str: string): string { - return str.replace(/"/g, '\\"'); -} diff --git a/packages/playwright-core/src/server/electron/loader.ts b/packages/playwright-core/src/server/electron/loader.ts deleted file mode 100644 index 80c6f76fb3d37..0000000000000 --- a/packages/playwright-core/src/server/electron/loader.ts +++ /dev/null @@ -1,54 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { app } from 'electron'; -import { chromiumSwitches } from '../chromium/chromiumSwitches'; - -// Always pass user arguments first, see https://github.com/microsoft/playwright/issues/16614 and -// https://github.com/microsoft/playwright/issues/29198. -// [Electron, -r, loader.js[, --no-sandbox>], --inspect=0, --remote-debugging-port=0, ...args] -process.argv.splice(1, process.argv.indexOf('--remote-debugging-port=0')); - -for (const arg of chromiumSwitches()) { - const match = arg.match(/--([^=]*)=?(.*)/)!; - app.commandLine.appendSwitch(match[1], match[2]); -} - -// Defer ready event. -const originalWhenReady = app.whenReady(); -const originalEmit = app.emit.bind(app); -let readyEventArgs: any[]; -app.emit = (event: string | symbol, ...args: any[]): boolean => { - if (event === 'ready') { - readyEventArgs = args; - return app.listenerCount('ready') > 0; - } - return originalEmit(event, ...args); -}; - -let isReady = false; -let whenReadyCallback: (event: any) => any; -const whenReadyPromise = new Promise(f => whenReadyCallback = f); -app.isReady = () => isReady; -app.whenReady = () => whenReadyPromise; - -(globalThis as any).__playwright_run = async () => { - // Wait for app to be ready to avoid browser initialization races. - const event = await originalWhenReady; - isReady = true; - whenReadyCallback(event); - originalEmit('ready', ...readyEventArgs); -}; diff --git a/packages/playwright-core/src/server/playwright.ts b/packages/playwright-core/src/server/playwright.ts index 3430f7829f27c..26d13551c4fb6 100644 --- a/packages/playwright-core/src/server/playwright.ts +++ b/packages/playwright-core/src/server/playwright.ts @@ -20,7 +20,6 @@ import { BidiChromium } from './bidi/bidiChromium'; import { BidiFirefox } from './bidi/bidiFirefox'; import { Chromium } from './chromium/chromium'; import { DebugController } from './debugController'; -import { Electron } from './electron/electron'; import { Firefox } from './firefox/firefox'; import { SdkObject, createRootSdkObject } from './instrumentation'; import { WebKit } from './webkit/webkit'; @@ -39,7 +38,6 @@ type PlaywrightOptions = { export class Playwright extends SdkObject { readonly chromium: BrowserType; readonly android: Android; - readonly electron: Electron; readonly firefox: BrowserType; readonly webkit: BrowserType; readonly options: PlaywrightOptions; @@ -60,7 +58,6 @@ export class Playwright extends SdkObject { this.chromium = new Chromium(this, new BidiChromium(this)); this.firefox = new Firefox(this, new BidiFirefox(this)); this.webkit = new WebKit(this); - this.electron = new Electron(this); this.android = new Android(this, new AdbBackend()); this.debugController = new DebugController(this); } diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index e1e1e00b155aa..18a357063612d 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -16419,366 +16419,6 @@ class TimeoutError extends Error { export const devices: Devices; -//@ts-ignore this will be any if electron is not installed -type ElectronType = typeof import('electron'); - -/** - * Electron application representation. You can use - * [electron.launch([options])](https://playwright.dev/docs/api/class-electron#electron-launch) to obtain the - * application instance. This instance you can control main electron process as well as work with Electron windows: - * - * ```js - * const { _electron: electron } = require('playwright'); - * - * (async () => { - * // Launch Electron app. - * const electronApp = await electron.launch({ args: ['main.js'] }); - * - * // Evaluation expression in the Electron context. - * const appPath = await electronApp.evaluate(async ({ app }) => { - * // This runs in the main Electron process, parameter here is always - * // the result of the require('electron') in the main app script. - * return app.getAppPath(); - * }); - * console.log(appPath); - * - * // Get the first window that the app opens, wait if necessary. - * const window = await electronApp.firstWindow(); - * // Print the title. - * console.log(await window.title()); - * // Capture a screenshot. - * await window.screenshot({ path: 'intro.png' }); - * // Direct Electron console to Node terminal. - * window.on('console', console.log); - * // Click button. - * await window.click('text=Click me'); - * // Exit app. - * await electronApp.close(); - * })(); - * ``` - * - */ -export interface ElectronApplication { - /** - * Returns the return value of - * [`pageFunction`](https://playwright.dev/docs/api/class-electronapplication#electron-application-evaluate-option-expression). - * - * If the function passed to the - * [electronApplication.evaluate(pageFunction[, arg])](https://playwright.dev/docs/api/class-electronapplication#electron-application-evaluate) - * returns a [Promise], then - * [electronApplication.evaluate(pageFunction[, arg])](https://playwright.dev/docs/api/class-electronapplication#electron-application-evaluate) - * would wait for the promise to resolve and return its value. - * - * If the function passed to the - * [electronApplication.evaluate(pageFunction[, arg])](https://playwright.dev/docs/api/class-electronapplication#electron-application-evaluate) - * returns a non-[Serializable] value, then - * [electronApplication.evaluate(pageFunction[, arg])](https://playwright.dev/docs/api/class-electronapplication#electron-application-evaluate) - * returns `undefined`. Playwright also supports transferring some additional values that are not serializable by - * `JSON`: `-0`, `NaN`, `Infinity`, `-Infinity`. - * @param pageFunction Function to be evaluated in the main Electron process. - * @param arg Optional argument to pass to - * [`pageFunction`](https://playwright.dev/docs/api/class-electronapplication#electron-application-evaluate-option-expression). - */ - evaluate(pageFunction: PageFunctionOn, arg: Arg): Promise; - /** - * Returns the return value of - * [`pageFunction`](https://playwright.dev/docs/api/class-electronapplication#electron-application-evaluate-option-expression). - * - * If the function passed to the - * [electronApplication.evaluate(pageFunction[, arg])](https://playwright.dev/docs/api/class-electronapplication#electron-application-evaluate) - * returns a [Promise], then - * [electronApplication.evaluate(pageFunction[, arg])](https://playwright.dev/docs/api/class-electronapplication#electron-application-evaluate) - * would wait for the promise to resolve and return its value. - * - * If the function passed to the - * [electronApplication.evaluate(pageFunction[, arg])](https://playwright.dev/docs/api/class-electronapplication#electron-application-evaluate) - * returns a non-[Serializable] value, then - * [electronApplication.evaluate(pageFunction[, arg])](https://playwright.dev/docs/api/class-electronapplication#electron-application-evaluate) - * returns `undefined`. Playwright also supports transferring some additional values that are not serializable by - * `JSON`: `-0`, `NaN`, `Infinity`, `-Infinity`. - * @param pageFunction Function to be evaluated in the main Electron process. - * @param arg Optional argument to pass to - * [`pageFunction`](https://playwright.dev/docs/api/class-electronapplication#electron-application-evaluate-option-expression). - */ - evaluate(pageFunction: PageFunctionOn, arg?: any): Promise; - - /** - * Returns the return value of - * [`pageFunction`](https://playwright.dev/docs/api/class-electronapplication#electron-application-evaluate-handle-option-expression) - * as a [JSHandle](https://playwright.dev/docs/api/class-jshandle). - * - * The only difference between - * [electronApplication.evaluate(pageFunction[, arg])](https://playwright.dev/docs/api/class-electronapplication#electron-application-evaluate) - * and - * [electronApplication.evaluateHandle(pageFunction[, arg])](https://playwright.dev/docs/api/class-electronapplication#electron-application-evaluate-handle) - * is that - * [electronApplication.evaluateHandle(pageFunction[, arg])](https://playwright.dev/docs/api/class-electronapplication#electron-application-evaluate-handle) - * returns [JSHandle](https://playwright.dev/docs/api/class-jshandle). - * - * If the function passed to the - * [electronApplication.evaluateHandle(pageFunction[, arg])](https://playwright.dev/docs/api/class-electronapplication#electron-application-evaluate-handle) - * returns a [Promise], then - * [electronApplication.evaluateHandle(pageFunction[, arg])](https://playwright.dev/docs/api/class-electronapplication#electron-application-evaluate-handle) - * would wait for the promise to resolve and return its value. - * @param pageFunction Function to be evaluated in the main Electron process. - * @param arg Optional argument to pass to - * [`pageFunction`](https://playwright.dev/docs/api/class-electronapplication#electron-application-evaluate-handle-option-expression). - */ - evaluateHandle(pageFunction: PageFunctionOn, arg: Arg): Promise>; - /** - * Returns the return value of - * [`pageFunction`](https://playwright.dev/docs/api/class-electronapplication#electron-application-evaluate-handle-option-expression) - * as a [JSHandle](https://playwright.dev/docs/api/class-jshandle). - * - * The only difference between - * [electronApplication.evaluate(pageFunction[, arg])](https://playwright.dev/docs/api/class-electronapplication#electron-application-evaluate) - * and - * [electronApplication.evaluateHandle(pageFunction[, arg])](https://playwright.dev/docs/api/class-electronapplication#electron-application-evaluate-handle) - * is that - * [electronApplication.evaluateHandle(pageFunction[, arg])](https://playwright.dev/docs/api/class-electronapplication#electron-application-evaluate-handle) - * returns [JSHandle](https://playwright.dev/docs/api/class-jshandle). - * - * If the function passed to the - * [electronApplication.evaluateHandle(pageFunction[, arg])](https://playwright.dev/docs/api/class-electronapplication#electron-application-evaluate-handle) - * returns a [Promise], then - * [electronApplication.evaluateHandle(pageFunction[, arg])](https://playwright.dev/docs/api/class-electronapplication#electron-application-evaluate-handle) - * would wait for the promise to resolve and return its value. - * @param pageFunction Function to be evaluated in the main Electron process. - * @param arg Optional argument to pass to - * [`pageFunction`](https://playwright.dev/docs/api/class-electronapplication#electron-application-evaluate-handle-option-expression). - */ - evaluateHandle(pageFunction: PageFunctionOn, arg?: any): Promise>; - /** - * This event is issued when the application process has been terminated. - */ - on(event: 'close', listener: () => any): this; - - /** - * Emitted when JavaScript within the Electron main process calls one of console API methods, e.g. `console.log` or - * `console.dir`. - * - * The arguments passed into `console.log` are available on the - * [ConsoleMessage](https://playwright.dev/docs/api/class-consolemessage) event handler argument. - * - * **Usage** - * - * ```js - * electronApp.on('console', async msg => { - * const values = []; - * for (const arg of msg.args()) - * values.push(await arg.jsonValue()); - * console.log(...values); - * }); - * await electronApp.evaluate(() => console.log('hello', 5, { foo: 'bar' })); - * ``` - * - */ - on(event: 'console', listener: (consoleMessage: ConsoleMessage) => any): this; - - /** - * This event is issued for every window that is created **and loaded** in Electron. It contains a - * [Page](https://playwright.dev/docs/api/class-page) that can be used for Playwright automation. - */ - on(event: 'window', listener: (page: Page) => any): 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: 'close', listener: () => any): 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: 'console', listener: (consoleMessage: ConsoleMessage) => any): 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: 'window', listener: (page: Page) => any): this; - - /** - * This event is issued when the application process has been terminated. - */ - addListener(event: 'close', listener: () => any): this; - - /** - * Emitted when JavaScript within the Electron main process calls one of console API methods, e.g. `console.log` or - * `console.dir`. - * - * The arguments passed into `console.log` are available on the - * [ConsoleMessage](https://playwright.dev/docs/api/class-consolemessage) event handler argument. - * - * **Usage** - * - * ```js - * electronApp.on('console', async msg => { - * const values = []; - * for (const arg of msg.args()) - * values.push(await arg.jsonValue()); - * console.log(...values); - * }); - * await electronApp.evaluate(() => console.log('hello', 5, { foo: 'bar' })); - * ``` - * - */ - addListener(event: 'console', listener: (consoleMessage: ConsoleMessage) => any): this; - - /** - * This event is issued for every window that is created **and loaded** in Electron. It contains a - * [Page](https://playwright.dev/docs/api/class-page) that can be used for Playwright automation. - */ - addListener(event: 'window', listener: (page: Page) => any): this; - - /** - * Removes an event listener added by `on` or `addListener`. - */ - removeListener(event: 'close', listener: () => any): this; - - /** - * Removes an event listener added by `on` or `addListener`. - */ - removeListener(event: 'console', listener: (consoleMessage: ConsoleMessage) => any): this; - - /** - * Removes an event listener added by `on` or `addListener`. - */ - removeListener(event: 'window', listener: (page: Page) => any): this; - - /** - * Removes an event listener added by `on` or `addListener`. - */ - off(event: 'close', listener: () => any): this; - - /** - * Removes an event listener added by `on` or `addListener`. - */ - off(event: 'console', listener: (consoleMessage: ConsoleMessage) => any): this; - - /** - * Removes an event listener added by `on` or `addListener`. - */ - off(event: 'window', listener: (page: Page) => any): this; - - /** - * This event is issued when the application process has been terminated. - */ - prependListener(event: 'close', listener: () => any): this; - - /** - * Emitted when JavaScript within the Electron main process calls one of console API methods, e.g. `console.log` or - * `console.dir`. - * - * The arguments passed into `console.log` are available on the - * [ConsoleMessage](https://playwright.dev/docs/api/class-consolemessage) event handler argument. - * - * **Usage** - * - * ```js - * electronApp.on('console', async msg => { - * const values = []; - * for (const arg of msg.args()) - * values.push(await arg.jsonValue()); - * console.log(...values); - * }); - * await electronApp.evaluate(() => console.log('hello', 5, { foo: 'bar' })); - * ``` - * - */ - prependListener(event: 'console', listener: (consoleMessage: ConsoleMessage) => any): this; - - /** - * This event is issued for every window that is created **and loaded** in Electron. It contains a - * [Page](https://playwright.dev/docs/api/class-page) that can be used for Playwright automation. - */ - prependListener(event: 'window', listener: (page: Page) => any): this; - - /** - * Returns the BrowserWindow object that corresponds to the given Playwright page. - * @param page Page to retrieve the window for. - */ - browserWindow(page: Page): Promise; - - /** - * Closes Electron application. - */ - close(): Promise; - - /** - * This method returns browser context that can be used for setting up context-wide routing, etc. - */ - context(): BrowserContext; - - /** - * Convenience method that waits for the first application window to be opened. - * - * **Usage** - * - * ```js - * const electronApp = await electron.launch({ - * args: ['main.js'] - * }); - * const window = await electronApp.firstWindow(); - * // ... - * ``` - * - * @param options - */ - firstWindow(options?: { - /** - * Maximum time to wait for in milliseconds. Defaults to `30000` (30 seconds). Pass `0` to disable timeout. The - * default value can be changed by using the - * [browserContext.setDefaultTimeout(timeout)](https://playwright.dev/docs/api/class-browsercontext#browser-context-set-default-timeout). - */ - timeout?: number; - }): Promise; - - /** - * Returns the main process for this Electron Application. - */ - process(): ChildProcess; - - /** - * This event is issued when the application process has been terminated. - */ - waitForEvent(event: 'close', optionsOrPredicate?: { predicate?: () => boolean | Promise, timeout?: number } | (() => boolean | Promise)): Promise; - - /** - * Emitted when JavaScript within the Electron main process calls one of console API methods, e.g. `console.log` or - * `console.dir`. - * - * The arguments passed into `console.log` are available on the - * [ConsoleMessage](https://playwright.dev/docs/api/class-consolemessage) event handler argument. - * - * **Usage** - * - * ```js - * electronApp.on('console', async msg => { - * const values = []; - * for (const arg of msg.args()) - * values.push(await arg.jsonValue()); - * console.log(...values); - * }); - * await electronApp.evaluate(() => console.log('hello', 5, { foo: 'bar' })); - * ``` - * - */ - waitForEvent(event: 'console', optionsOrPredicate?: { predicate?: (consoleMessage: ConsoleMessage) => boolean | Promise, timeout?: number } | ((consoleMessage: ConsoleMessage) => boolean | Promise)): Promise; - - /** - * This event is issued for every window that is created **and loaded** in Electron. It contains a - * [Page](https://playwright.dev/docs/api/class-page) that can be used for Playwright automation. - */ - waitForEvent(event: 'window', optionsOrPredicate?: { predicate?: (page: Page) => boolean | Promise, timeout?: number } | ((page: Page) => boolean | Promise)): Promise; - - - /** - * Convenience method that returns all the opened windows. - */ - windows(): Array; - - [Symbol.asyncDispose](): Promise; -} - export type AndroidElementInfo = { clazz: string; desc: string; @@ -16884,7 +16524,6 @@ export type AndroidKey = 'Copy' | 'Paste'; -export const _electron: Electron; export const _android: Android; // This is required to not export everything by default. See https://github.com/Microsoft/TypeScript/issues/19545#issuecomment-340490459 @@ -19903,292 +19542,6 @@ export interface Download { url(): string; } -/** - * Playwright has **experimental** support for Electron automation. You can access electron namespace via: - * - * ```js - * const { _electron } = require('playwright'); - * ``` - * - * An example of the Electron automation script would be: - * - * ```js - * const { _electron: electron } = require('playwright'); - * - * (async () => { - * // Launch Electron app. - * const electronApp = await electron.launch({ args: ['main.js'] }); - * - * // Evaluation expression in the Electron context. - * const appPath = await electronApp.evaluate(async ({ app }) => { - * // This runs in the main Electron process, parameter here is always - * // the result of the require('electron') in the main app script. - * return app.getAppPath(); - * }); - * console.log(appPath); - * - * // Get the first window that the app opens, wait if necessary. - * const window = await electronApp.firstWindow(); - * // Print the title. - * console.log(await window.title()); - * // Capture a screenshot. - * await window.screenshot({ path: 'intro.png' }); - * // Direct Electron console to Node terminal. - * window.on('console', console.log); - * // Click button. - * await window.click('text=Click me'); - * // Exit app. - * await electronApp.close(); - * })(); - * ``` - * - * **Supported Electron versions are:** - * - v12.2.0+ - * - v13.4.0+ - * - v14+ - * - * **Known issues:** - * - * If you are not able to launch Electron and it will end up in timeouts during launch, try the following: - * - Ensure that `nodeCliInspect` - * ([FuseV1Options.EnableNodeCliInspectArguments](https://www.electronjs.org/docs/latest/tutorial/fuses#nodecliinspect)) - * fuse is **not** set to `false`. - */ -export interface Electron { - /** - * Launches electron application specified with the - * [`executablePath`](https://playwright.dev/docs/api/class-electron#electron-launch-option-executable-path). - * @param options - */ - launch(options?: { - /** - * Whether to automatically download all the attachments. Defaults to `true` where all the downloads are accepted. - */ - acceptDownloads?: boolean; - - /** - * Additional arguments to pass to the application when launching. You typically pass the main script name here. - */ - args?: Array; - - /** - * If specified, artifacts (traces, videos, downloads, HAR files, etc.) are saved into this directory. The directory - * is not cleaned up when the browser closes. If not specified, a temporary directory is used and cleaned up when the - * browser closes. - */ - artifactsDir?: string; - - /** - * Toggles bypassing page's Content-Security-Policy. Defaults to `false`. - */ - bypassCSP?: boolean; - - /** - * Enable Chromium sandboxing. Defaults to `false`. - */ - chromiumSandbox?: boolean; - - /** - * Emulates [prefers-colors-scheme](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme) - * media feature, supported values are `'light'` and `'dark'`. See - * [page.emulateMedia([options])](https://playwright.dev/docs/api/class-page#page-emulate-media) for more details. - * Passing `null` resets emulation to system defaults. Defaults to `'light'`. - */ - colorScheme?: null|"light"|"dark"|"no-preference"; - - /** - * Current working directory to launch application from. - */ - cwd?: string; - - /** - * Specifies environment variables that will be visible to Electron. Defaults to `process.env`. - */ - env?: { [key: string]: string; }; - - /** - * Launches given Electron application. If not specified, launches the default Electron executable installed in this - * package, located at `node_modules/.bin/electron`. - */ - executablePath?: string; - - /** - * An object containing additional HTTP headers to be sent with every request. Defaults to none. - */ - extraHTTPHeaders?: { [key: string]: string; }; - - geolocation?: { - /** - * Latitude between -90 and 90. - */ - latitude: number; - - /** - * Longitude between -180 and 180. - */ - longitude: number; - - /** - * Non-negative accuracy value. Defaults to `0`. - */ - accuracy?: number; - }; - - /** - * Credentials for [HTTP authentication](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication). If no - * origin is specified, the username and password are sent to any servers upon unauthorized responses. - */ - httpCredentials?: { - username: string; - - password: string; - - /** - * Restrain sending http credentials on specific origin (scheme://host:port). - */ - origin?: string; - - /** - * This option only applies to the requests sent from corresponding - * [APIRequestContext](https://playwright.dev/docs/api/class-apirequestcontext) and does not affect requests sent from - * the browser. `'always'` - `Authorization` header with basic authentication credentials will be sent with the each - * API request. `'unauthorized` - the credentials are only sent when 401 (Unauthorized) response with - * `WWW-Authenticate` header is received. Defaults to `'unauthorized'`. - */ - send?: "unauthorized"|"always"; - }; - - /** - * Whether to ignore HTTPS errors when sending network requests. Defaults to `false`. - */ - ignoreHTTPSErrors?: boolean; - - /** - * Specify user locale, for example `en-GB`, `de-DE`, etc. Locale will affect `navigator.language` value, - * `Accept-Language` request header value as well as number and date formatting rules. Defaults to the system default - * locale. Learn more about emulation in our [emulation guide](https://playwright.dev/docs/emulation#locale--timezone). - */ - locale?: string; - - /** - * Whether to emulate network being offline. Defaults to `false`. Learn more about - * [network emulation](https://playwright.dev/docs/emulation#offline). - */ - offline?: boolean; - - /** - * Enables [HAR](http://www.softwareishard.com/blog/har-12-spec) recording for all pages into `recordHar.path` file. - * If not specified, the HAR is not recorded. Make sure to await - * [browserContext.close([options])](https://playwright.dev/docs/api/class-browsercontext#browser-context-close) for - * the HAR to be saved. - */ - recordHar?: { - /** - * Optional setting to control whether to omit request content from the HAR. Defaults to `false`. Deprecated, use - * `content` policy instead. - */ - omitContent?: boolean; - - /** - * Optional setting to control resource content management. If `omit` is specified, content is not persisted. If - * `attach` is specified, resources are persisted as separate files or entries in the ZIP archive. If `embed` is - * specified, content is stored inline the HAR file as per HAR specification. Defaults to `attach` for `.zip` output - * files and to `embed` for all other file extensions. - */ - content?: "omit"|"embed"|"attach"; - - /** - * Path on the filesystem to write the HAR file to. If the file name ends with `.zip`, `content: 'attach'` is used by - * default. - */ - path: string; - - /** - * When set to `minimal`, only record information necessary for routing from HAR. This omits sizes, timing, page, - * cookies, security and other types of HAR information that are not used when replaying from HAR. Defaults to `full`. - */ - mode?: "full"|"minimal"; - - /** - * A glob or regex pattern to filter requests that are stored in the HAR. When a - * [`baseURL`](https://playwright.dev/docs/api/class-browser#browser-new-context-option-base-url) via the context - * options was provided and the passed URL is a path, it gets merged via the - * [`new URL()`](https://developer.mozilla.org/en-US/docs/Web/API/URL/URL) constructor. Defaults to none. - */ - urlFilter?: string|RegExp; - }; - - /** - * Enables video recording for all pages into `recordVideo.dir` directory. If not specified videos are not recorded. - * Make sure to await - * [browserContext.close([options])](https://playwright.dev/docs/api/class-browsercontext#browser-context-close) for - * videos to be saved. - */ - recordVideo?: { - /** - * Path to the directory to put videos into. If not specified, the videos will be stored in `artifactsDir` (see - * [browserType.launch([options])](https://playwright.dev/docs/api/class-browsertype#browser-type-launch) options). - */ - dir?: string; - - /** - * Optional dimensions of the recorded videos. If not specified the size will be equal to `viewport` scaled down to - * fit into 800x800. If `viewport` is not configured explicitly the video size defaults to 800x450. Actual picture of - * each page will be scaled down if necessary to fit the specified size. - */ - size?: { - /** - * Video frame width. - */ - width: number; - - /** - * Video frame height. - */ - height: number; - }; - - /** - * If specified, enables visual annotations on interacted elements during video recording. - */ - showActions?: { - /** - * How long each annotation is displayed in milliseconds. Defaults to `500`. - */ - duration?: number; - - /** - * Position of the action title overlay. Defaults to `"top-right"`. - */ - position?: "top-left"|"top"|"top-right"|"bottom-left"|"bottom"|"bottom-right"; - - /** - * Font size of the action title in pixels. Defaults to `24`. - */ - fontSize?: number; - }; - }; - - /** - * Maximum time in milliseconds to wait for the application to start. Defaults to `30000` (30 seconds). Pass `0` to - * disable timeout. - */ - timeout?: number; - - /** - * Changes the timezone of the context. See - * [ICU's metaZones.txt](https://cs.chromium.org/chromium/src/third_party/icu/source/data/misc/metaZones.txt?rcl=faee8bc70570192d82d2978a71e2a615788597d1) - * for a list of supported timezone IDs. Defaults to the system timezone. - */ - timezoneId?: string; - - /** - * If specified, traces are saved into this directory. - */ - tracesDir?: string; - }): Promise; -} - /** * [FileChooser](https://playwright.dev/docs/api/class-filechooser) objects are dispatched by the page in the * [page.on('filechooser')](https://playwright.dev/docs/api/class-page#page-event-file-chooser) event. diff --git a/packages/playwright-electron/.npmignore b/packages/playwright-electron/.npmignore new file mode 100644 index 0000000000000..5c6877c1a1f40 --- /dev/null +++ b/packages/playwright-electron/.npmignore @@ -0,0 +1,7 @@ +**/* + +!README.md +!LICENSE +!lib/** +!index.d.ts +!index.js diff --git a/packages/playwright-electron/README.md b/packages/playwright-electron/README.md new file mode 100644 index 0000000000000..b4f90dfc6005b --- /dev/null +++ b/packages/playwright-electron/README.md @@ -0,0 +1,156 @@ +> **BEWARE** This package is **EXPERIMENTAL** and does not respect semver. + +```js +import { electron } from '@playwright/experimental-electron'; + +const electronApp = await electron.launch({ args: ['main.js'] }); +const window = await electronApp.firstWindow(); +// ... drive `window` like any Playwright Page ... +await electronApp.close(); +``` + +Read more at https://playwright.dev/docs/api/class-electron. + +## Migrating from v1.59 + +Prior to v1.60, the Electron API shipped as `playwright._electron` from the +`playwright` package. It is now exposed as `electron` from this dedicated +package. + +A number of `electron.launch(...)` options have changed in the process. The sections below describe how to achieve +the same behavior with public Playwright APIs, or with built-in Electron APIs. + +### Use Playwright APIs after launch + +#### `recordHar` + +Use [`browserContext.tracing.startHar`](https://playwright.dev/docs/api/class-tracing#tracing-start-har) / +[`stopHar`](https://playwright.dev/docs/api/class-tracing#tracing-stop-har). + +```js +const electronApp = await electron.launch({ args: ['main.js'] }); +await electronApp.context().tracing.startHar('network.har'); +// ... drive the app ... +await electronApp.context().tracing.stopHar(); +await electronApp.close(); +``` + +#### `recordVideo` + +Use [`page.screencast.start`](https://playwright.dev/docs/api/class-page#page-screencast) / +[`stop`](https://playwright.dev/docs/api/class-page#page-screencast) on each window. + +```js +const electronApp = await electron.launch({ args: ['main.js'] }); +const window = await electronApp.firstWindow(); +await window.screencast.start({ path: 'video.webm' }); +// ... drive the window ... +await window.screencast.stop(); +await electronApp.close(); +``` + +#### `colorScheme` + +Use [`page.emulateMedia`](https://playwright.dev/docs/api/class-page#page-emulate-media) +on each window. + +```js +const window = await electronApp.firstWindow(); +await window.emulateMedia({ colorScheme: 'dark' }); +``` + +#### `extraHTTPHeaders` + +Use [`browserContext.setExtraHTTPHeaders`](https://playwright.dev/docs/api/class-browsercontext#browser-context-set-extra-http-headers). + +```js +await electronApp.context().setExtraHTTPHeaders({ 'X-My-Header': 'value' }); +``` + +#### `geolocation` + +Use [`browserContext.setGeolocation`](https://playwright.dev/docs/api/class-browsercontext#browser-context-set-geolocation). + +```js +await electronApp.context().setGeolocation({ latitude: 48.858455, longitude: 2.294474 }); +``` + +#### `httpCredentials` + +Use [`browserContext.setHTTPCredentials`](https://playwright.dev/docs/api/class-browsercontext#browser-context-set-http-credentials). + +```js +await electronApp.context().setHTTPCredentials({ username: 'user', password: 'pass' }); +``` + +#### `offline` + +Use [`browserContext.setOffline`](https://playwright.dev/docs/api/class-browsercontext#browser-context-set-offline). + +```js +await electronApp.context().setOffline(true); +``` + +### Use built-in Electron APIs + +#### `bypassCSP` + +Disable CSP at the `BrowserWindow` level via Electron's +[web preferences](https://www.electronjs.org/docs/latest/api/structures/web-preferences). +Note that `webSecurity: false` also disables CORS and the Same-Origin Policy. + +```js +const win = new BrowserWindow({ + webPreferences: { + webSecurity: false, + }, +}); +``` + +#### `ignoreHTTPSErrors` + +There are several ways to relax HTTPS checks in Electron. Pick the one that +matches the scope you need. + +Per-window, allow mixed content through [web preferences](https://www.electronjs.org/docs/latest/api/structures/web-preferences): + +```js +const win = new BrowserWindow({ + webPreferences: { + allowRunningInsecureContent: true, + }, +}); +``` + +Process-wide, ignore certificate errors via Chromium command-line switches +(must run before the `ready` event): + +```js +const { app } = require('electron'); +app.commandLine.appendSwitch('ignore-certificate-errors'); +// Optional: also ignore localhost certificate errors when testing on an IP. +app.commandLine.appendSwitch('allow-insecure-localhost', 'true'); +``` + +Per-request, accept the certificate manually via the +[`certificate-error`](https://www.electronjs.org/docs/latest/api/app#event-certificate-error) +event: + +```js +app.on('certificate-error', (event, webContents, url, error, certificate, callback) => { + event.preventDefault(); + callback(true); +}); +``` + +#### `timezoneId` + +Set an environment variable at the very top of the main file, before any other logic or Chromium windows are initialized: + +```js +// main.js +process.env.TZ = 'Europe/London'; + +const { app } = require('electron'); +// ... rest of your app logic +``` diff --git a/packages/playwright-electron/index.d.ts b/packages/playwright-electron/index.d.ts new file mode 100644 index 0000000000000..24667aeeea769 --- /dev/null +++ b/packages/playwright-electron/index.d.ts @@ -0,0 +1,17 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './types'; diff --git a/packages/playwright-electron/index.js b/packages/playwright-electron/index.js new file mode 100644 index 0000000000000..f1f1d57e07df2 --- /dev/null +++ b/packages/playwright-electron/index.js @@ -0,0 +1,19 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const { selectors } = require('playwright-core'); +const { electron } = require('./lib/electron'); +module.exports = { electron, selectors }; diff --git a/packages/playwright-electron/package.json b/packages/playwright-electron/package.json new file mode 100644 index 0000000000000..5fd6297e14e9a --- /dev/null +++ b/packages/playwright-electron/package.json @@ -0,0 +1,26 @@ +{ + "name": "@playwright/experimental-electron", + "version": "1.60.0-next", + "description": "Playwright for Electron", + "repository": { + "type": "git", + "url": "git+https://github.com/microsoft/playwright.git" + }, + "homepage": "https://playwright.dev", + "engines": { + "node": ">=18" + }, + "author": { + "name": "Microsoft Corporation" + }, + "license": "Apache-2.0", + "exports": { + ".": { + "types": "./index.d.ts", + "default": "./index.js" + } + }, + "dependencies": { + "playwright-core": "1.60.0-next" + } +} diff --git a/packages/playwright-electron/src/DEPS.list b/packages/playwright-electron/src/DEPS.list new file mode 100644 index 0000000000000..6d6d0a1a68c8c --- /dev/null +++ b/packages/playwright-electron/src/DEPS.list @@ -0,0 +1,5 @@ +[*] +@isomorphic/** +@utils/** +playwright-core +node_modules/debug diff --git a/packages/playwright-electron/src/electron.ts b/packages/playwright-electron/src/electron.ts new file mode 100644 index 0000000000000..c32b8532bc8ad --- /dev/null +++ b/packages/playwright-electron/src/electron.ts @@ -0,0 +1,349 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import os from 'os'; +import readline from 'readline'; +import { EventEmitter } from 'events'; +import { chromium } from 'playwright-core'; +import debug from 'debug'; + +import { launchProcess } from '@utils/processLauncher'; +import { wrapInASCIIBox } from '@utils/ascii'; +import { debugMode } from '@utils/debug'; +import { ManualPromise } from '@isomorphic/manualPromise'; +import { monotonicTime } from '@isomorphic/time'; + +import type { BrowserWindow } from 'electron'; +import type { Browser, BrowserContext, JSHandle, Page, Worker } from 'playwright-core'; +import type childProcess from 'child_process'; +import type * as api from '../types'; + +const debugLogger = debug('pw:electron'); + +export const Events = { + ElectronApplication: { + Close: 'close', + Console: 'console', + Window: 'window', + }, +}; + +type ElectronLaunchOptions = NonNullable[0]>; + +type ElectronAppType = typeof import('electron'); + +class Progress { + private _deadline: number; + private _timeoutError: Error; + + constructor(timeout: number, timeoutMessage: string) { + this._deadline = timeout ? monotonicTime() + timeout : 0; + this._timeoutError = new Error(timeoutMessage); + } + + async race(promise: Promise): Promise { + const timeoutPromise = new ManualPromise(); + const timeout = this.timeUntilDeadline(); + const timer = timeout ? setTimeout(() => timeoutPromise.reject(this._timeoutError), timeout) : undefined; + try { + return await Promise.race([promise, timeoutPromise]); + } finally { + clearTimeout(timer); + } + } + + timeUntilDeadline() { + return this._deadline ? this._deadline - monotonicTime() : 0; + } +} + +export class Electron implements api.Electron { + async launch(options: ElectronLaunchOptions = {}): Promise { + const timeout = options.timeout ?? (debugMode() === 'inspector' ? 0 : 3 * 60 * 1000); + const progress = new Progress(timeout, `electron.launch: Timeout ${timeout}ms exceeded`); + let app: ElectronApplication | undefined; + + // --remote-debugging-port=0 must be the last playwright argument; loader.ts relies on it. + let electronArguments = ['--inspect=0', '--remote-debugging-port=0', ...(options.args || [])]; + + if (os.platform() === 'linux') { + if (!options.chromiumSandbox && !electronArguments.includes('--no-sandbox')) + electronArguments.unshift('--no-sandbox'); + } + + let command: string; + if (options.executablePath) { + command = options.executablePath; + } else { + try { + // 'electron/index.js' resolves to the Electron App executable shim. + command = require('electron/index.js'); + } catch (error: any) { + if ((error as NodeJS.ErrnoException)?.code === 'MODULE_NOT_FOUND') { + throw new Error('\n' + wrapInASCIIBox([ + 'Electron executablePath not found!', + 'Please install it using `npm install -D electron` or set the executablePath to your Electron executable.', + ].join('\n'), 1)); + } + throw error; + } + // Only inject our loader for non-packaged apps; packaged apps may have + // their own command-line handling. + electronArguments.unshift('-r', require.resolve('./loader')); + } + + let shell = false; + if (process.platform === 'win32') { + // shell: true is required to launch .cmd files. We pass the entire + // command as a single string to dodge DEP0190 and Windows quoting bugs. + // https://github.com/nodejs/node/issues/52554 + // https://github.com/microsoft/playwright/issues/38278 + shell = true; + command = [command, ...electronArguments].map(arg => `"${arg.replace(/"/g, '\\"')}"`).join(' '); + electronArguments = []; + } + + // When debugging Playwright tests that drive Electron, NODE_OPTIONS + // would make the user's debugger latch onto Electron's Node first. + // Strip it so Playwright can attach. + const env = { ...(options.env ?? process.env) }; + delete env.NODE_OPTIONS; + + const logCollector: string[] = []; + const { launchedProcess, kill } = await launchProcess({ + command, + args: electronArguments, + env, + log: (message: string) => { + debugLogger(message); + logCollector.push(message); + }, + shell, + stdio: 'pipe', + cwd: options.cwd, + tempDirectories: [], + attemptToGracefullyClose: async () => { await app?.close(); }, + handleSIGINT: true, + handleSIGTERM: true, + handleSIGHUP: true, + onExit: () => app?._onClose(), + }); + + // Start every line listener immediately — the lines may arrive before we + // are ready to await them. + const waitForXserverError = waitForLine(progress, launchedProcess, /Unable to open X display/).then(() => { + throw new Error([ + 'Unable to open X display!', + '================================', + 'Most likely this is because there is no X server available.', + "Use 'xvfb-run' on Linux to launch your tests with an emulated display server.", + "For example: 'xvfb-run npm run test:e2e'", + '================================', + ...logCollector, + ].join('\n')); + }); + const nodeMatchPromise = waitForLine(progress, launchedProcess, /^Debugger listening on (ws:\/\/.*)$/); + const chromeMatchPromise = waitForLine(progress, launchedProcess, /^DevTools listening on (ws:\/\/.*)$/); + const debuggerDisconnectPromise = waitForLine(progress, launchedProcess, /Waiting for the debugger to disconnect\.\.\./); + + try { + const nodeMatch = await nodeMatchPromise; + const worker = await chromium.connectToWorker(nodeMatch[1], { timeout: progress.timeUntilDeadline() }); + + // Release the Electron process immediately if the user is debugging it. + debuggerDisconnectPromise.then(() => worker.disconnect()).catch(() => {}); + + const chromeMatch = await Promise.race([chromeMatchPromise, waitForXserverError]); + const browser = await chromium.connectOverCDP(chromeMatch[1], { timeout: progress.timeUntilDeadline() }); + + app = new ElectronApplication(worker, browser, launchedProcess); + await progress.race(app._initialize()); + return app; + } catch (error) { + await kill(); + throw error; + } + } +} + +export class ElectronApplication extends EventEmitter implements api.ElectronApplication { + private _worker: Worker; + private _browser: Browser; + private _process: childProcess.ChildProcess; + private _context: BrowserContext; + private _windows = new Map | undefined>(); + private _appHandlePromise = new ManualPromise>(); + private _closedPromise: Promise | undefined; + + constructor(worker: Worker, browser: Browser, process: childProcess.ChildProcess) { + super(); + + this._worker = worker; + this._worker.on('console', message => this.emit(Events.ElectronApplication.Console, message)); + + this._browser = browser; + this._context = browser.contexts()[0]; + for (const page of this._context.pages()) + this._onPage(page); + this._context.on('page', page => this._onPage(page)); + // Closing the BrowserContext should close the entire app; route both through close(). + this._context.close = () => this.close(); + + this._process = process; + } + + _onClose() { + this.emit(Events.ElectronApplication.Close); + this._closedPromise ??= Promise.resolve(); + } + + process(): childProcess.ChildProcess { + return this._process; + } + + _onPage(page: Page) { + this._windows.set(page, undefined); + this.emit(Events.ElectronApplication.Window, page); + page.once('close', () => this._windows.delete(page)); + } + + windows(): Page[] { + return [...this._windows.keys()]; + } + + async firstWindow(options?: { timeout?: number }): Promise { + if (this._windows.size) + return this._windows.keys().next().value!; + return await this.waitForEvent('window', options); + } + + context(): BrowserContext { + return this._context; + } + + async [Symbol.asyncDispose]() { + await this.close(); + } + + async close() { + if (!this._closedPromise) { + this._closedPromise = new Promise(f => this.once(Events.ElectronApplication.Close, f)); + await this._browser.close(); + const appHandle = await this._appHandlePromise; + await appHandle.evaluate(({ app }) => app.quit()).catch(() => {}); + await this._worker.disconnect(); + } + await this._closedPromise; + } + + async waitForEvent(event: string, optionsOrPredicate: Function | { timeout?: number, predicate?: Function } = {}): Promise { + const promise = new ManualPromise(); + + const onEvent = async (eventArg: any) => { + try { + const predicate = typeof optionsOrPredicate === 'function' ? optionsOrPredicate : optionsOrPredicate.predicate; + if (predicate && !(await predicate(eventArg))) + return; + promise.resolve(eventArg); + } catch (e) { + promise.reject(e); + } + }; + this.addListener(event, onEvent); + + const onClose = () => promise.reject(new Error('Electron application has been closed')); + if (event !== Events.ElectronApplication.Close) + this.addListener(Events.ElectronApplication.Close, onClose); + + try { + const timeout = typeof optionsOrPredicate === 'function' ? 30000 : (optionsOrPredicate?.timeout ?? 30000); + const progress = new Progress(timeout, `Timeout ${timeout}ms exceeded while waiting for event "${event}"`); + return await progress.race(promise); + } finally { + this.removeListener(event, onEvent); + this.removeListener(Events.ElectronApplication.Close, onClose); + } + } + + async _initialize() { + await Promise.all([ + this._worker.evaluateHandle('__playwright_electron').then(handle => { + this._appHandlePromise.resolve(handle as any); + // Best-effort: in-process clients can rename the preview to make stack traces nicer. + (handle as any)._connection?.toImpl?.(handle)?._setPreview('ElectronModule'); + }), + // Defer Electron's `ready` until the browser side is wired up for auto-attach. + this._worker.evaluate('__playwright_run()'), + ]); + } + + async browserWindow(page: Page): Promise> { + let browserWindow = this._windows.get(page); + if (!browserWindow) { + const cdpSession = await this._context.newCDPSession(page); + const { targetInfo } = await cdpSession.send('Target.getTargetInfo'); + const appHandle = await this._appHandlePromise; + browserWindow = await appHandle.evaluateHandle(({ BrowserWindow, webContents }, targetId) => { + const wc = webContents.fromDevToolsTargetId(targetId); + return BrowserWindow.fromWebContents(wc!)!; + }, targetInfo.targetId); + this._windows.set(page, browserWindow); + } + return browserWindow; + } + + async evaluate(pageFunction: any, arg: Arg): Promise { + const appHandle = await this._appHandlePromise; + return appHandle.evaluate(pageFunction, arg); + } + + async evaluateHandle(pageFunction: any, arg: Arg): Promise { + const appHandle = await this._appHandlePromise; + return await appHandle.evaluateHandle(pageFunction, arg); + } +} + +async function waitForLine(progress: Progress, process: childProcess.ChildProcess, regex: RegExp) { + const promise = new ManualPromise(); + + // eslint-disable-next-line no-restricted-properties + const rl = readline.createInterface({ input: process.stderr! }); + + const failError = new Error('Process failed to launch!'); + const onFail = () => promise.reject(failError); + const onLine = (line: string) => { + const match = line.match(regex); + if (match) + promise.resolve(match); + }; + + rl.addListener('line', onLine); + rl.addListener('close', onFail); + process.addListener('exit', onFail); + // Safe to add a listener — launchProcess attached its own error handler already. + process.addListener('error', onFail); + + try { + return await progress.race(promise); + } finally { + rl.removeListener('line', onLine); + rl.removeListener('close', onFail); + process.removeListener('exit', onFail); + process.removeListener('error', onFail); + } +} + +export const electron = new Electron(); diff --git a/packages/playwright-electron/src/loader.ts b/packages/playwright-electron/src/loader.ts new file mode 100644 index 0000000000000..0e0c25dbdc364 --- /dev/null +++ b/packages/playwright-electron/src/loader.ts @@ -0,0 +1,103 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Runs inside the Electron main process via `electron -r loader.js`. +// Must be self-contained — Electron's main process does not have access +// to playwright-core's bundled deps. Keep the chromium switches list in sync +// with packages/playwright-core/src/server/chromium/chromiumSwitches.ts. + +const electronModule = require('electron') as typeof import('electron'); + +const { app } = electronModule; + +const chromiumSwitches = [ + '--disable-field-trial-config', + '--disable-background-networking', + '--disable-background-timer-throttling', + '--disable-backgrounding-occluded-windows', + '--disable-back-forward-cache', + '--disable-breakpad', + '--disable-client-side-phishing-detection', + '--disable-component-extensions-with-background-pages', + '--disable-component-update', + '--no-default-browser-check', + '--disable-default-apps', + '--disable-dev-shm-usage', + '--disable-edgeupdater', + '--disable-extensions', + '--disable-features=AvoidUnnecessaryBeforeUnloadCheckSync,BoundaryEventDispatchTracksNodeRemoval,DestroyProfileOnBrowserClose,DialMediaRouteProvider,GlobalMediaControls,HttpsUpgrades,LensOverlay,MediaRouter,PaintHolding,ThirdPartyStoragePartitioning,Translate,AutoDeElevate,RenderDocument,OptimizationHints,msForceBrowserSignIn,msEdgeUpdateLaunchServicesPreferredVersion', + '--enable-features=CDPScreenshotNewSurface', + '--allow-pre-commit-input', + '--disable-hang-monitor', + '--disable-ipc-flooding-protection', + '--disable-popup-blocking', + '--disable-prompt-on-repost', + '--disable-renderer-backgrounding', + '--force-color-profile=srgb', + '--metrics-recording-only', + '--no-first-run', + '--password-store=basic', + '--use-mock-keychain', + '--no-service-autorun', + '--export-tagged-pdf', + '--disable-search-engine-choice-screen', + '--unsafely-disable-devtools-self-xss-warnings', + '--edge-skip-compat-layer-relaunch', + '--enable-automation', + '--disable-infobars', + '--disable-sync', +]; + +// The new `chromium.connectToWorker`-based client reads these globals via +// the Node debugger to bootstrap the Electron app. +(globalThis as any).__playwright_electron = electronModule; + +// Always pass user arguments first. +// https://github.com/microsoft/playwright/issues/16614 +// https://github.com/microsoft/playwright/issues/29198 +// argv layout: [Electron, -r, loader.js[, --no-sandbox], --inspect=0, --remote-debugging-port=0, ...userArgs] +process.argv.splice(1, process.argv.indexOf('--remote-debugging-port=0')); + +for (const arg of chromiumSwitches) { + const match = arg.match(/--([^=]*)=?(.*)/)!; + app.commandLine.appendSwitch(match[1], match[2]); +} + +// Defer the `ready` event until the Playwright client has wired up auto-attach. +const originalWhenReady = app.whenReady(); +const originalEmit = app.emit.bind(app); +let readyEventArgs: any[]; +app.emit = (event: string | symbol, ...args: any[]): boolean => { + if (event === 'ready') { + readyEventArgs = args; + return app.listenerCount('ready') > 0; + } + return originalEmit(event, ...args); +}; + +let isReady = false; +let whenReadyCallback: (event: any) => any; +const whenReadyPromise = new Promise(f => whenReadyCallback = f); +app.isReady = () => isReady; +app.whenReady = () => whenReadyPromise; + +(globalThis as any).__playwright_run = async () => { + // Wait for app to be ready to avoid browser-initialization races. + const event = await originalWhenReady; + isReady = true; + whenReadyCallback(event); + originalEmit('ready', ...readyEventArgs); +}; diff --git a/packages/playwright-electron/types.d.ts b/packages/playwright-electron/types.d.ts new file mode 100644 index 0000000000000..458194d8184db --- /dev/null +++ b/packages/playwright-electron/types.d.ts @@ -0,0 +1,432 @@ +// This file is generated by /utils/generate_types/index.js +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { BrowserContext, ConsoleMessage, JSHandle, Page } from 'playwright-core'; +import type { ChildProcess } from 'child_process'; + +export * from 'playwright-core'; + +//@ts-ignore this will be any if electron is not installed +type ElectronType = typeof import('electron'); + +/** + * Electron application representation. You can use + * [electron.launch([options])](https://playwright.dev/docs/api/class-electron#electron-launch) to obtain the + * application instance. This instance you can control main electron process as well as work with Electron windows: + * + * ```js + * const { _electron: electron } = require('playwright'); + * + * (async () => { + * // Launch Electron app. + * const electronApp = await electron.launch({ args: ['main.js'] }); + * + * // Evaluation expression in the Electron context. + * const appPath = await electronApp.evaluate(async ({ app }) => { + * // This runs in the main Electron process, parameter here is always + * // the result of the require('electron') in the main app script. + * return app.getAppPath(); + * }); + * console.log(appPath); + * + * // Get the first window that the app opens, wait if necessary. + * const window = await electronApp.firstWindow(); + * // Print the title. + * console.log(await window.title()); + * // Capture a screenshot. + * await window.screenshot({ path: 'intro.png' }); + * // Direct Electron console to Node terminal. + * window.on('console', console.log); + * // Click button. + * await window.click('text=Click me'); + * // Exit app. + * await electronApp.close(); + * })(); + * ``` + * + */ +export interface ElectronApplication { + /** + * Returns the return value of + * [`pageFunction`](https://playwright.dev/docs/api/class-electronapplication#electron-application-evaluate-option-expression). + * + * If the function passed to the + * [electronApplication.evaluate(pageFunction[, arg])](https://playwright.dev/docs/api/class-electronapplication#electron-application-evaluate) + * returns a [Promise], then + * [electronApplication.evaluate(pageFunction[, arg])](https://playwright.dev/docs/api/class-electronapplication#electron-application-evaluate) + * would wait for the promise to resolve and return its value. + * + * If the function passed to the + * [electronApplication.evaluate(pageFunction[, arg])](https://playwright.dev/docs/api/class-electronapplication#electron-application-evaluate) + * returns a non-[Serializable] value, then + * [electronApplication.evaluate(pageFunction[, arg])](https://playwright.dev/docs/api/class-electronapplication#electron-application-evaluate) + * returns `undefined`. Playwright also supports transferring some additional values that are not serializable by + * `JSON`: `-0`, `NaN`, `Infinity`, `-Infinity`. + * @param pageFunction Function to be evaluated in the main Electron process. + * @param arg Optional argument to pass to + * [`pageFunction`](https://playwright.dev/docs/api/class-electronapplication#electron-application-evaluate-option-expression). + */ + evaluate: JSHandle['evaluate']; + /** + * Returns the return value of + * [`pageFunction`](https://playwright.dev/docs/api/class-electronapplication#electron-application-evaluate-handle-option-expression) + * as a [JSHandle](https://playwright.dev/docs/api/class-jshandle). + * + * The only difference between + * [electronApplication.evaluate(pageFunction[, arg])](https://playwright.dev/docs/api/class-electronapplication#electron-application-evaluate) + * and + * [electronApplication.evaluateHandle(pageFunction[, arg])](https://playwright.dev/docs/api/class-electronapplication#electron-application-evaluate-handle) + * is that + * [electronApplication.evaluateHandle(pageFunction[, arg])](https://playwright.dev/docs/api/class-electronapplication#electron-application-evaluate-handle) + * returns [JSHandle](https://playwright.dev/docs/api/class-jshandle). + * + * If the function passed to the + * [electronApplication.evaluateHandle(pageFunction[, arg])](https://playwright.dev/docs/api/class-electronapplication#electron-application-evaluate-handle) + * returns a [Promise], then + * [electronApplication.evaluateHandle(pageFunction[, arg])](https://playwright.dev/docs/api/class-electronapplication#electron-application-evaluate-handle) + * would wait for the promise to resolve and return its value. + * @param pageFunction Function to be evaluated in the main Electron process. + * @param arg Optional argument to pass to + * [`pageFunction`](https://playwright.dev/docs/api/class-electronapplication#electron-application-evaluate-handle-option-expression). + */ + evaluateHandle: JSHandle['evaluateHandle']; + /** + * This event is issued when the application process has been terminated. + */ + on(event: 'close', listener: () => any): this; + + /** + * Emitted when JavaScript within the Electron main process calls one of console API methods, e.g. `console.log` or + * `console.dir`. + * + * The arguments passed into `console.log` are available on the + * [ConsoleMessage](https://playwright.dev/docs/api/class-consolemessage) event handler argument. + * + * **Usage** + * + * ```js + * electronApp.on('console', async msg => { + * const values = []; + * for (const arg of msg.args()) + * values.push(await arg.jsonValue()); + * console.log(...values); + * }); + * await electronApp.evaluate(() => console.log('hello', 5, { foo: 'bar' })); + * ``` + * + */ + on(event: 'console', listener: (consoleMessage: ConsoleMessage) => any): this; + + /** + * This event is issued for every window that is created **and loaded** in Electron. It contains a + * [Page](https://playwright.dev/docs/api/class-page) that can be used for Playwright automation. + */ + on(event: 'window', listener: (page: Page) => any): 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: 'close', listener: () => any): 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: 'console', listener: (consoleMessage: ConsoleMessage) => any): 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: 'window', listener: (page: Page) => any): this; + + /** + * This event is issued when the application process has been terminated. + */ + addListener(event: 'close', listener: () => any): this; + + /** + * Emitted when JavaScript within the Electron main process calls one of console API methods, e.g. `console.log` or + * `console.dir`. + * + * The arguments passed into `console.log` are available on the + * [ConsoleMessage](https://playwright.dev/docs/api/class-consolemessage) event handler argument. + * + * **Usage** + * + * ```js + * electronApp.on('console', async msg => { + * const values = []; + * for (const arg of msg.args()) + * values.push(await arg.jsonValue()); + * console.log(...values); + * }); + * await electronApp.evaluate(() => console.log('hello', 5, { foo: 'bar' })); + * ``` + * + */ + addListener(event: 'console', listener: (consoleMessage: ConsoleMessage) => any): this; + + /** + * This event is issued for every window that is created **and loaded** in Electron. It contains a + * [Page](https://playwright.dev/docs/api/class-page) that can be used for Playwright automation. + */ + addListener(event: 'window', listener: (page: Page) => any): this; + + /** + * Removes an event listener added by `on` or `addListener`. + */ + removeListener(event: 'close', listener: () => any): this; + + /** + * Removes an event listener added by `on` or `addListener`. + */ + removeListener(event: 'console', listener: (consoleMessage: ConsoleMessage) => any): this; + + /** + * Removes an event listener added by `on` or `addListener`. + */ + removeListener(event: 'window', listener: (page: Page) => any): this; + + /** + * Removes an event listener added by `on` or `addListener`. + */ + off(event: 'close', listener: () => any): this; + + /** + * Removes an event listener added by `on` or `addListener`. + */ + off(event: 'console', listener: (consoleMessage: ConsoleMessage) => any): this; + + /** + * Removes an event listener added by `on` or `addListener`. + */ + off(event: 'window', listener: (page: Page) => any): this; + + /** + * This event is issued when the application process has been terminated. + */ + prependListener(event: 'close', listener: () => any): this; + + /** + * Emitted when JavaScript within the Electron main process calls one of console API methods, e.g. `console.log` or + * `console.dir`. + * + * The arguments passed into `console.log` are available on the + * [ConsoleMessage](https://playwright.dev/docs/api/class-consolemessage) event handler argument. + * + * **Usage** + * + * ```js + * electronApp.on('console', async msg => { + * const values = []; + * for (const arg of msg.args()) + * values.push(await arg.jsonValue()); + * console.log(...values); + * }); + * await electronApp.evaluate(() => console.log('hello', 5, { foo: 'bar' })); + * ``` + * + */ + prependListener(event: 'console', listener: (consoleMessage: ConsoleMessage) => any): this; + + /** + * This event is issued for every window that is created **and loaded** in Electron. It contains a + * [Page](https://playwright.dev/docs/api/class-page) that can be used for Playwright automation. + */ + prependListener(event: 'window', listener: (page: Page) => any): this; + + /** + * Returns the BrowserWindow object that corresponds to the given Playwright page. + * @param page Page to retrieve the window for. + */ + browserWindow(page: Page): Promise; + + /** + * Closes Electron application. + */ + close(): Promise; + + /** + * This method returns browser context that can be used for setting up context-wide routing, etc. + */ + context(): BrowserContext; + + /** + * Convenience method that waits for the first application window to be opened. + * + * **Usage** + * + * ```js + * const electronApp = await electron.launch({ + * args: ['main.js'] + * }); + * const window = await electronApp.firstWindow(); + * // ... + * ``` + * + * @param options + */ + firstWindow(options?: { + /** + * Maximum time to wait for in milliseconds. Defaults to `30000` (30 seconds). Pass `0` to disable timeout. The + * default value can be changed by using the + * [browserContext.setDefaultTimeout(timeout)](https://playwright.dev/docs/api/class-browsercontext#browser-context-set-default-timeout). + */ + timeout?: number; + }): Promise; + + /** + * Returns the main process for this Electron Application. + */ + process(): ChildProcess; + + /** + * This event is issued when the application process has been terminated. + */ + waitForEvent(event: 'close', optionsOrPredicate?: { predicate?: () => boolean | Promise, timeout?: number } | (() => boolean | Promise)): Promise; + + /** + * Emitted when JavaScript within the Electron main process calls one of console API methods, e.g. `console.log` or + * `console.dir`. + * + * The arguments passed into `console.log` are available on the + * [ConsoleMessage](https://playwright.dev/docs/api/class-consolemessage) event handler argument. + * + * **Usage** + * + * ```js + * electronApp.on('console', async msg => { + * const values = []; + * for (const arg of msg.args()) + * values.push(await arg.jsonValue()); + * console.log(...values); + * }); + * await electronApp.evaluate(() => console.log('hello', 5, { foo: 'bar' })); + * ``` + * + */ + waitForEvent(event: 'console', optionsOrPredicate?: { predicate?: (consoleMessage: ConsoleMessage) => boolean | Promise, timeout?: number } | ((consoleMessage: ConsoleMessage) => boolean | Promise)): Promise; + + /** + * This event is issued for every window that is created **and loaded** in Electron. It contains a + * [Page](https://playwright.dev/docs/api/class-page) that can be used for Playwright automation. + */ + waitForEvent(event: 'window', optionsOrPredicate?: { predicate?: (page: Page) => boolean | Promise, timeout?: number } | ((page: Page) => boolean | Promise)): Promise; + + + /** + * Convenience method that returns all the opened windows. + */ + windows(): Array; + + [Symbol.asyncDispose](): Promise; +} + +export const electron: Electron; + +// This is required to not export everything by default. See https://github.com/Microsoft/TypeScript/issues/19545#issuecomment-340490459 +export {}; + + +/** + * Playwright has **experimental** support for Electron automation, shipped as a separate package: + * + * An example of the Electron automation script would be: + * + * ```js + * import { electron } from '@playwright/experimental-electron'; + * + * (async () => { + * // Launch Electron app. + * const electronApp = await electron.launch({ args: ['main.js'] }); + * + * // Evaluation expression in the Electron context. + * const appPath = await electronApp.evaluate(async ({ app }) => { + * // This runs in the main Electron process, parameter here is always + * // the result of the require('electron') in the main app script. + * return app.getAppPath(); + * }); + * console.log(appPath); + * + * // Get the first window that the app opens, wait if necessary. + * const window = await electronApp.firstWindow(); + * // Print the title. + * console.log(await window.title()); + * // Capture a screenshot. + * await window.screenshot({ path: 'intro.png' }); + * // Direct Electron console to Node terminal. + * window.on('console', console.log); + * // Click button. + * await window.click('text=Click me'); + * // Exit app. + * await electronApp.close(); + * })(); + * ``` + * + * **Supported Electron versions are:** + * - v12.2.0+ + * - v13.4.0+ + * - v14+ + * + * **Known issues:** + * + * If you are not able to launch Electron and it will end up in timeouts during launch, try the following: + * - Ensure that `nodeCliInspect` + * ([FuseV1Options.EnableNodeCliInspectArguments](https://www.electronjs.org/docs/latest/tutorial/fuses#nodecliinspect)) + * fuse is **not** set to `false`. + */ +export interface Electron { + /** + * Launches electron application specified with the + * [`executablePath`](https://playwright.dev/docs/api/class-electron#electron-launch-option-executable-path). + * @param options + */ + launch(options?: { + /** + * Additional arguments to pass to the application when launching. You typically pass the main script name here. + */ + args?: Array; + + /** + * Enable Chromium sandboxing. Defaults to `false`. + */ + chromiumSandbox?: boolean; + + /** + * Current working directory to launch application from. + */ + cwd?: string; + + /** + * Specifies environment variables that will be visible to Electron. Defaults to `process.env`. + */ + env?: { [key: string]: string; }; + + /** + * Launches given Electron application. If not specified, launches the default Electron executable installed in this + * package, located at `node_modules/.bin/electron`. + */ + executablePath?: string; + + /** + * Maximum time in milliseconds to wait for the application to start. Defaults to `30000` (30 seconds). Pass `0` to + * disable timeout. + */ + timeout?: number; + }): Promise; +} + + diff --git a/packages/protocol/src/channels.d.ts b/packages/protocol/src/channels.d.ts index 8339636c0e221..9bfd93d8b1e8b 100644 --- a/packages/protocol/src/channels.d.ts +++ b/packages/protocol/src/channels.d.ts @@ -30,8 +30,6 @@ export type InitializerTraits = T extends AndroidDeviceChannel ? AndroidDeviceInitializer : T extends AndroidSocketChannel ? AndroidSocketInitializer : T extends AndroidChannel ? AndroidInitializer : - T extends ElectronApplicationChannel ? ElectronApplicationInitializer : - T extends ElectronChannel ? ElectronInitializer : T extends CDPSessionChannel ? CDPSessionInitializer : T extends WritableStreamChannel ? WritableStreamInitializer : T extends StreamChannel ? StreamInitializer : @@ -69,8 +67,6 @@ export type EventsTraits = T extends AndroidDeviceChannel ? AndroidDeviceEvents : T extends AndroidSocketChannel ? AndroidSocketEvents : T extends AndroidChannel ? AndroidEvents : - T extends ElectronApplicationChannel ? ElectronApplicationEvents : - T extends ElectronChannel ? ElectronEvents : T extends CDPSessionChannel ? CDPSessionEvents : T extends WritableStreamChannel ? WritableStreamEvents : T extends StreamChannel ? StreamEvents : @@ -108,8 +104,6 @@ export type EventTargetTraits = T extends AndroidDeviceChannel ? AndroidDeviceEventTarget : T extends AndroidSocketChannel ? AndroidSocketEventTarget : T extends AndroidChannel ? AndroidEventTarget : - T extends ElectronApplicationChannel ? ElectronApplicationEventTarget : - T extends ElectronChannel ? ElectronEventTarget : T extends CDPSessionChannel ? CDPSessionEventTarget : T extends WritableStreamChannel ? WritableStreamEventTarget : T extends StreamChannel ? StreamEventTarget : @@ -619,7 +613,6 @@ export type PlaywrightInitializer = { firefox: BrowserTypeChannel, webkit: BrowserTypeChannel, android: AndroidChannel, - electron: ElectronChannel, utils?: LocalUtilsChannel, preLaunchedBrowser?: BrowserChannel, preConnectedAndroidDevice?: AndroidDeviceChannel, @@ -4699,178 +4692,6 @@ export interface CDPSessionEvents { 'close': CDPSessionCloseEvent; } -// ----------- Electron ----------- -export type ElectronInitializer = {}; -export interface ElectronEventTarget { -} -export interface ElectronChannel extends ElectronEventTarget, Channel { - _type_Electron: boolean; - launch(params: ElectronLaunchParams, progress?: Progress): Promise; -} -export type ElectronLaunchParams = { - executablePath?: string, - args?: string[], - chromiumSandbox?: boolean, - cwd?: string, - env?: NameValue[], - timeout: number, - acceptDownloads?: 'accept' | 'deny' | 'internal-browser-default', - bypassCSP?: boolean, - colorScheme?: 'dark' | 'light' | 'no-preference' | 'no-override', - extraHTTPHeaders?: NameValue[], - geolocation?: { - longitude: number, - latitude: number, - accuracy?: number, - }, - httpCredentials?: { - username: string, - password: string, - origin?: string, - }, - ignoreHTTPSErrors?: boolean, - locale?: string, - offline?: boolean, - recordVideo?: { - dir?: string, - size?: { - width: number, - height: number, - }, - showActions?: { - duration?: number, - position?: 'top-left' | 'top' | 'top-right' | 'bottom-left' | 'bottom' | 'bottom-right', - fontSize?: number, - }, - }, - strictSelectors?: boolean, - timezoneId?: string, - tracesDir?: string, - artifactsDir?: string, - selectorEngines?: SelectorEngine[], - testIdAttributeName?: string, -}; -export type ElectronLaunchOptions = { - executablePath?: string, - args?: string[], - chromiumSandbox?: boolean, - cwd?: string, - env?: NameValue[], - acceptDownloads?: 'accept' | 'deny' | 'internal-browser-default', - bypassCSP?: boolean, - colorScheme?: 'dark' | 'light' | 'no-preference' | 'no-override', - extraHTTPHeaders?: NameValue[], - geolocation?: { - longitude: number, - latitude: number, - accuracy?: number, - }, - httpCredentials?: { - username: string, - password: string, - origin?: string, - }, - ignoreHTTPSErrors?: boolean, - locale?: string, - offline?: boolean, - recordVideo?: { - dir?: string, - size?: { - width: number, - height: number, - }, - showActions?: { - duration?: number, - position?: 'top-left' | 'top' | 'top-right' | 'bottom-left' | 'bottom' | 'bottom-right', - fontSize?: number, - }, - }, - strictSelectors?: boolean, - timezoneId?: string, - tracesDir?: string, - artifactsDir?: string, - selectorEngines?: SelectorEngine[], - testIdAttributeName?: string, -}; -export type ElectronLaunchResult = { - electronApplication: ElectronApplicationChannel, -}; - -export interface ElectronEvents { -} - -// ----------- ElectronApplication ----------- -export type ElectronApplicationInitializer = { - context: BrowserContextChannel, -}; -export interface ElectronApplicationEventTarget { - on(event: 'close', callback: (params: ElectronApplicationCloseEvent) => void): this; - on(event: 'console', callback: (params: ElectronApplicationConsoleEvent) => void): this; -} -export interface ElectronApplicationChannel extends ElectronApplicationEventTarget, EventTargetChannel { - _type_ElectronApplication: boolean; - browserWindow(params: ElectronApplicationBrowserWindowParams, progress?: Progress): Promise; - evaluateExpression(params: ElectronApplicationEvaluateExpressionParams, progress?: Progress): Promise; - evaluateExpressionHandle(params: ElectronApplicationEvaluateExpressionHandleParams, progress?: Progress): Promise; - updateSubscription(params: ElectronApplicationUpdateSubscriptionParams, progress?: Progress): Promise; -} -export type ElectronApplicationCloseEvent = {}; -export type ElectronApplicationConsoleEvent = { - type: string, - text: string, - args: JSHandleChannel[], - location: { - url: string, - lineNumber: number, - columnNumber: number, - }, - timestamp: number, -}; -export type ElectronApplicationBrowserWindowParams = { - page: PageChannel, -}; -export type ElectronApplicationBrowserWindowOptions = { - -}; -export type ElectronApplicationBrowserWindowResult = { - handle: JSHandleChannel, -}; -export type ElectronApplicationEvaluateExpressionParams = { - expression: string, - isFunction?: boolean, - arg: SerializedArgument, -}; -export type ElectronApplicationEvaluateExpressionOptions = { - isFunction?: boolean, -}; -export type ElectronApplicationEvaluateExpressionResult = { - value: SerializedValue, -}; -export type ElectronApplicationEvaluateExpressionHandleParams = { - expression: string, - isFunction?: boolean, - arg: SerializedArgument, -}; -export type ElectronApplicationEvaluateExpressionHandleOptions = { - isFunction?: boolean, -}; -export type ElectronApplicationEvaluateExpressionHandleResult = { - handle: JSHandleChannel, -}; -export type ElectronApplicationUpdateSubscriptionParams = { - event: 'console', - enabled: boolean, -}; -export type ElectronApplicationUpdateSubscriptionOptions = { - -}; -export type ElectronApplicationUpdateSubscriptionResult = void; - -export interface ElectronApplicationEvents { - 'close': ElectronApplicationCloseEvent; - 'console': ElectronApplicationConsoleEvent; -} - // ----------- Android ----------- export type AndroidInitializer = {}; export interface AndroidEventTarget { diff --git a/packages/protocol/src/protocol.yml b/packages/protocol/src/protocol.yml index abe1d504205bb..8fdb26129465d 100644 --- a/packages/protocol/src/protocol.yml +++ b/packages/protocol/src/protocol.yml @@ -790,7 +790,6 @@ Playwright: firefox: BrowserType webkit: BrowserType android: Android - electron: Electron utils: LocalUtils? # Only present when connecting remotely via BrowserType.connect() method. preLaunchedBrowser: Browser? @@ -4142,132 +4141,6 @@ CDPSession: close: -Electron: - type: interface - - commands: - - launch: - title: Launch electron - parameters: - executablePath: string? - args: - type: array? - items: string - chromiumSandbox: boolean? - cwd: string? - env: - type: array? - items: NameValue - timeout: float - acceptDownloads: - type: enum? - literals: - - accept - - deny - - internal-browser-default - bypassCSP: boolean? - colorScheme: - type: enum? - literals: - - dark - - light - - no-preference - - no-override - extraHTTPHeaders: - type: array? - items: NameValue - geolocation: - type: object? - properties: - longitude: float - latitude: float - accuracy: float? - httpCredentials: - type: object? - properties: - username: string - password: string - origin: string? - ignoreHTTPSErrors: boolean? - locale: string? - offline: boolean? - recordVideo: - type: object? - properties: - dir: string? - size: - type: object? - properties: - width: int - height: int - showActions: - type: object? - properties: - $mixin: ShowActionsOptions - strictSelectors: boolean? - timezoneId: string? - tracesDir: string? - artifactsDir: string? - selectorEngines: - type: array? - items: SelectorEngine - testIdAttributeName: string? - - returns: - electronApplication: ElectronApplication - - -ElectronApplication: - type: interface - - extends: EventTarget - - initializer: - context: BrowserContext - - commands: - - browserWindow: - internal: true - parameters: - page: Page - returns: - handle: JSHandle - - evaluateExpression: - title: Evaluate - parameters: - expression: string - isFunction: boolean? - arg: SerializedArgument - returns: - value: SerializedValue - - evaluateExpressionHandle: - title: Evaluate - parameters: - expression: string - isFunction: boolean? - arg: SerializedArgument - returns: - handle: JSHandle - - updateSubscription: - internal: true - parameters: - event: - type: enum - literals: - - console - enabled: boolean - - events: - close: - console: - parameters: - $mixin: ConsoleMessage - Android: type: interface diff --git a/tests/electron/electron-app.spec.ts b/tests/electron/electron-app.spec.ts index 7fa8f8950af86..a2aba343b05e7 100644 --- a/tests/electron/electron-app.spec.ts +++ b/tests/electron/electron-app.spec.ts @@ -17,7 +17,7 @@ import type { BrowserWindow } from 'electron'; import path from 'path'; import fs from 'fs'; -import { electronTest as test, expect } from './electronTest'; +import { electronTest as test, expect, selectors } from './electronTest'; import type { ConsoleMessage } from 'playwright'; test('should fire close event via ElectronApplication.close();', async ({ launchElectronApp }) => { @@ -63,11 +63,10 @@ test('should fire close event when the app quits itself', async ({ launchElectro await electronApp.evaluate(({ app }) => app.quit()); await waitForAppClose; } - events.sort(); // we don't care about the order - expect(events).toEqual(['application(close)', 'context(close)', 'process(exit)']); + await expect.poll(() => [...events].sort()).toEqual(['application(close)', 'context(close)', 'process(exit)']); // Give it some time to fire more events - there should not be any. await new Promise(f => setTimeout(f, 1000)); - expect(events).toEqual(['application(close)', 'context(close)', 'process(exit)']); + expect([...events].sort()).toEqual(['application(close)', 'context(close)', 'process(exit)']); }); test('should fire console events', async ({ launchElectronApp }) => { @@ -146,6 +145,47 @@ test('should evaluate handle', async ({ electronApp }) => { expect(await electronApp.evaluate(({ app }, appHandle) => app === appHandle, appHandle)).toBeTruthy(); }); +test('should infer evaluate types', async ({ electronApp }) => { + // No-arg evaluate, returning a primitive. + const appPath: string = await electronApp.evaluate(({ app }) => app.getAppPath()); + expect(typeof appPath).toBe('string'); + + // Arg evaluate with explicit typing on both sides. + const sum: number = await electronApp.evaluate(({}, { a, b }) => a + b, { a: 1, b: 2 }); + expect(sum).toBe(3); + + // Async evaluate returning an object literal. + const info = await electronApp.evaluate(async ({ app }) => ({ name: app.getName(), version: app.getVersion() })); + const name: string = info.name; + const version: string = info.version; + expect(typeof name).toBe('string'); + expect(typeof version).toBe('string'); + + // evaluateHandle returns JSHandle; jsonValue() preserves R. + const handle = await electronApp.evaluateHandle(({ app }) => ({ path: app.getAppPath() })); + const jsonValue = await handle.jsonValue(); + const path: string = jsonValue.path; + expect(typeof path).toBe('string'); + + // Passing a JSHandle as an arg unwraps it on the worker side. + const numberHandle = await electronApp.evaluateHandle(() => 42); + const doubled: number = await electronApp.evaluate(({}, n) => n * 2, numberHandle); + expect(doubled).toBe(84); + +}); + +test('should register custom selector engine', async ({ newWindow }) => { + const tag = `electron-tag-${test.info().workerIndex}`; + await selectors.register(tag, `({ + query(root, selector) { return root.querySelector(selector); }, + queryAll(root, selector) { return Array.from(root.querySelectorAll(selector)); }, + })`); + const window = await newWindow(); + await window.setContent('
'); + expect(await window.$eval(`${tag}=DIV`, e => e.nodeName)).toBe('DIV'); + expect(await window.$$eval(`${tag}=DIV`, es => es.length)).toBe(2); +}); + test('should route network', async ({ electronApp, newWindow }) => { await electronApp.context().route('**/empty.html', async (route, request) => { await route.fulfill({ @@ -200,20 +240,19 @@ test('should return browser window', async ({ launchElectronApp }) => { expect(await bwHandle.evaluate((bw: BrowserWindow) => bw.title)).toBe('Electron'); }); -test('should bypass csp', async ({ launchElectronApp, server }) => { - const app = await launchElectronApp('electron-app.js', [], { bypassCSP: true }); - await app.evaluate(electron => { - const window = new electron.BrowserWindow({ - width: 800, - height: 600, - }); - void window.loadURL('about:blank'); - }); +test('should set timezone via process.env.TZ', async ({ launchElectronApp }) => { + // Migrated from the removed timezoneId launch option, see packages/playwright-electron/README.md. + const app = await launchElectronApp('electron-options-app.js', [], { env: { PWTEST_OPTION_TZ: 'Europe/London' } }); + const page = await app.firstWindow(); + expect(await page.evaluate(() => Intl.DateTimeFormat().resolvedOptions().timeZone)).toBe('Europe/London'); +}); + +test('should ignore https errors via --ignore-certificate-errors switch', async ({ launchElectronApp, httpsServer }) => { + // Migrated from the removed ignoreHTTPSErrors launch option, see packages/playwright-electron/README.md. + const app = await launchElectronApp('electron-options-app.js', [], { env: { PWTEST_OPTION_IGNORE_HTTPS_ERRORS: '1' } }); const page = await app.firstWindow(); - await page.goto(server.PREFIX + '/csp.html'); - await page.addScriptTag({ content: 'window["__injected"] = 42;' }); - expect(await page.evaluate('window["__injected"]')).toBe(42); - expect(await page.evaluate('window["__inlineScriptValue"]')).toBe(42); + const response = await page.goto(httpsServer.EMPTY_PAGE); + expect(response.status()).toBe(200); }); test('should create page for browser view', async ({ launchElectronApp }) => { @@ -250,28 +289,29 @@ test('should return same browser window for browser view pages', async ({ launch expect(firstWindowId).toEqual(secondWindowId); }); -test('should record video', async ({ launchElectronApp }, testInfo) => { - const app = await launchElectronApp('electron-window-app.js', [], { - recordVideo: { dir: testInfo.outputPath('video') } - }); +test('should record video via page.screencast.start', async ({ launchElectronApp }, testInfo) => { + const app = await launchElectronApp('electron-window-app.js'); const page = await app.firstWindow(); + const videoPath = testInfo.outputPath('video.webm'); + await page.screencast.start({ path: videoPath }); await page.setContent(``); await page.waitForTimeout(1000); + await page.screencast.stop(); await app.close(); - const videoPath = await page.video().path(); expect(fs.statSync(videoPath).size).toBeGreaterThan(0); }); -test('should record har', async ({ launchElectronApp, server }, testInfo) => { +test('should record har via context.tracing.startHar', async ({ launchElectronApp, server }, testInfo) => { test.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/30747' }); - const app = await launchElectronApp('electron-window-app.js', [], { - recordHar: { path: testInfo.outputPath('har.zip') } - }); + const app = await launchElectronApp('electron-window-app.js'); + const harPath = testInfo.outputPath('har.zip'); + await app.context().tracing.startHar(harPath); const page = await app.firstWindow(); await page.goto(server.EMPTY_PAGE); + await app.context().tracing.stopHar(); await app.close(); - expect(fs.existsSync(testInfo.outputPath('har.zip'))).toBeTruthy(); - expect(fs.statSync(testInfo.outputPath('har.zip')).size).toBeGreaterThan(0); + expect(fs.existsSync(harPath)).toBeTruthy(); + expect(fs.statSync(harPath).size).toBeGreaterThan(0); }); test('should be able to get the first window when with a delayed navigation', async ({ launchElectronApp }) => { @@ -325,9 +365,7 @@ test('should report downloads', async ({ launchElectronApp, electronMajorVersion res.end(`Hello world`); }); - const app = await launchElectronApp('electron-window-app.js', [], { - acceptDownloads: true, - }); + const app = await launchElectronApp('electron-window-app.js'); const window = await app.firstWindow(); await window.setContent(`download`); const [download] = await Promise.all([ @@ -339,29 +377,3 @@ test('should report downloads', async ({ launchElectronApp, electronMajorVersion expect(fs.readFileSync(path).toString()).toBe('Hello world'); await app.close(); }); - -test('should save downloads to artifactsDir', async ({ launchElectronApp, electronMajorVersion, server }, testInfo) => { - server.setRoute('/download', (req, res) => { - res.setHeader('Content-Type', 'application/octet-stream'); - res.setHeader('Content-Disposition', 'attachment'); - res.end(`Hello world`); - }); - - const artifactsDir = testInfo.outputPath('artifacts'); - const app = await launchElectronApp('electron-window-app.js', [], { - acceptDownloads: true, - artifactsDir, - }); - const window = await app.firstWindow(); - await window.setContent(`download`); - const [download] = await Promise.all([ - window.waitForEvent('download'), - window.click('a') - ]); - const downloadPath = await download.path(); - expect(downloadPath.startsWith(artifactsDir)).toBeTruthy(); - expect(fs.existsSync(downloadPath)).toBeTruthy(); - await app.close(); - // User-provided artifactsDir should not be cleaned up. - expect(fs.existsSync(artifactsDir)).toBeTruthy(); -}); diff --git a/tests/electron/electron-options-app.js b/tests/electron/electron-options-app.js new file mode 100644 index 0000000000000..445b6f1879f1c --- /dev/null +++ b/tests/electron/electron-options-app.js @@ -0,0 +1,21 @@ +// Demonstrates the migration paths from removed launch options to +// built-in Electron APIs. Behavior is configured via PWTEST_OPTION_* env vars. + +if (process.env.PWTEST_OPTION_TZ) + process.env.TZ = process.env.PWTEST_OPTION_TZ; + +const { app, BrowserWindow } = require('electron'); + +if (!process.env.PWTEST_ELECTRON_USER_DATA_DIR) + throw new Error('PWTEST_ELECTRON_USER_DATA_DIR env var is not set'); +app.setPath('appData', process.env.PWTEST_ELECTRON_USER_DATA_DIR); + +if (process.env.PWTEST_OPTION_IGNORE_HTTPS_ERRORS) + app.commandLine.appendSwitch('ignore-certificate-errors'); + +app.on('window-all-closed', e => e.preventDefault()); + +app.whenReady().then(() => { + const win = new BrowserWindow({ width: 800, height: 600 }); + win.loadURL('about:blank'); +}); diff --git a/tests/electron/electron-tracing.spec.ts b/tests/electron/electron-tracing.spec.ts index ffb650645a7d5..84c63d350cd26 100644 --- a/tests/electron/electron-tracing.spec.ts +++ b/tests/electron/electron-tracing.spec.ts @@ -15,8 +15,6 @@ */ import { electronTest as test, expect } from './electronTest'; -import fs from 'fs'; -import path from 'path'; test.skip(({ trace }) => trace === 'on'); @@ -46,17 +44,3 @@ test('should support custom protocol', async ({ electronApp, newWindow, server, await expect(frame.locator('button')).toHaveCSS('color', 'rgb(255, 0, 0)'); await expect(frame.locator('button')).toHaveCSS('font-weight', '700'); }); - -test('should respect tracesDir and name', async ({ launchElectronApp, server }, testInfo) => { - const tracesDir = testInfo.outputPath('traces'); - const electronApp = await launchElectronApp('electron-window-app.js', [], { tracesDir }); - - await electronApp.context().tracing.start({ name: 'name1', snapshots: true }); - const page = await electronApp.firstWindow(); - await page.goto(server.PREFIX + '/one-style.html'); - await electronApp.context().tracing.stopChunk({ path: testInfo.outputPath('trace1.zip') }); - expect(fs.existsSync(path.join(tracesDir, 'name1.trace'))).toBe(true); - expect(fs.existsSync(path.join(tracesDir, 'name1.network'))).toBe(true); - - await electronApp.close(); -}); diff --git a/tests/electron/electronTest.ts b/tests/electron/electronTest.ts index 6e137398a826f..d4ae4ccae1172 100644 --- a/tests/electron/electronTest.ts +++ b/tests/electron/electronTest.ts @@ -18,7 +18,9 @@ import { baseTest } from '../config/baseTest'; import path from 'path'; import fs from 'fs'; import os from 'os'; -import type { ElectronApplication, Page, Electron } from '@playwright/test'; +import type { Page } from '@playwright/test'; +import type { ElectronApplication, Electron } from '@playwright/experimental-electron'; +import { electron } from '@playwright/experimental-electron'; import type { PageTestFixtures, PageWorkerFixtures } from '../page/pageTestApi'; import type { TraceViewerFixtures } from '../config/traceViewerFixtures'; import { traceViewerFixtures } from '../config/traceViewerFixtures'; @@ -26,6 +28,7 @@ import { utils } from '../../packages/playwright-core/lib/coreBundle'; import { inheritAndCleanEnv } from '../config/utils'; export { expect } from '@playwright/test'; +export { selectors } from '@playwright/experimental-electron'; const { removeFolders } = utils; @@ -58,16 +61,16 @@ export const electronTest = baseTest.extend(traceViewerFixt await removeFolders(dirs); }, - launchElectronApp: async ({ playwright, createUserDataDir }, use) => { + launchElectronApp: async ({ createUserDataDir }, use) => { // This env prevents 'Electron Security Policy' console message. process.env['ELECTRON_DISABLE_SECURITY_WARNINGS'] = 'true'; const apps: ElectronApplication[] = []; await use(async (appFile: string, args: string[] = [], options?: Parameters[0]) => { const userDataDir = await createUserDataDir(); - const app = await playwright._electron.launch({ + const app = await electron.launch({ ...options, args: [path.join(__dirname, appFile), ...args], - env: inheritAndCleanEnv({ PWTEST_ELECTRON_USER_DATA_DIR: userDataDir }), + env: inheritAndCleanEnv({ ...options?.env, PWTEST_ELECTRON_USER_DATA_DIR: userDataDir }), }); apps.push(app); return app; diff --git a/tests/installation/fixture-scripts/sanity-electron.js b/tests/installation/fixture-scripts/sanity-electron.js index 8475f129afc62..c5d2c1d21f867 100644 --- a/tests/installation/fixture-scripts/sanity-electron.js +++ b/tests/installation/fixture-scripts/sanity-electron.js @@ -14,18 +14,18 @@ * limitations under the License. */ -const playwright = require('playwright'); +const { electron } = require('@playwright/experimental-electron'); const path = require('path'); (async () => { - const application = await playwright._electron.launch({ + const application = await electron.launch({ args: [path.join(__dirname, 'electron-app.js')], }); const appPath = await application.evaluate(async ({ app }) => app.getAppPath()); await application.close(); if (appPath !== __dirname) throw new Error(`Malformed app path: got "${appPath}", expected "${__dirname}"`); - console.log(`playwright._electron SUCCESS`); + console.log(`@playwright/experimental-electron SUCCESS`); })().catch(err => { console.error(err); process.exit(1); diff --git a/tests/installation/playwright-electron-should-work.spec.ts b/tests/installation/playwright-electron-should-work.spec.ts index 09a6a5a5a34be..d1607f425723f 100755 --- a/tests/installation/playwright-electron-should-work.spec.ts +++ b/tests/installation/playwright-electron-should-work.spec.ts @@ -19,11 +19,11 @@ import { expect } from '../../packages/playwright-test'; import path from 'path'; test('electron should work', async ({ exec, tsc, writeFiles }) => { - await exec('npm i playwright electron@19.0.11'); + await exec('npm i @playwright/experimental-electron electron@19.0.11'); await exec('node sanity-electron.js'); await writeFiles({ 'test.ts': - `import { Page, _electron, ElectronApplication, Electron } from 'playwright';` + `import { electron, ElectronApplication, Electron } from '@playwright/experimental-electron';` }); await tsc('test.ts'); }); @@ -32,7 +32,7 @@ test('electron should work with special characters in path', async ({ exec, tmpW test.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/30755' }); const folderName = path.join(tmpWorkspace, '!@#$% тест with spaces and 😊'); - await exec('npm i playwright electron@19.0.11'); + await exec('npm i @playwright/experimental-electron electron@19.0.11'); await fs.promises.mkdir(folderName); for (const file of ['electron-app.js', 'sanity-electron.js']) await fs.promises.copyFile(path.join(tmpWorkspace, file), path.join(folderName, file)); @@ -42,13 +42,14 @@ test('electron should work with special characters in path', async ({ exec, tmpW }); test('should work when wrapped inside @playwright/test and trace is enabled', async ({ exec, tmpWorkspace, writeFiles }) => { - await exec('npm i -D @playwright/test electron@31'); + await exec('npm i -D @playwright/test @playwright/experimental-electron electron@31'); await writeFiles({ 'electron-with-tracing.spec.ts': ` - import { test, expect, _electron } from '@playwright/test'; + import { test, expect } from '@playwright/test'; + import { electron } from '@playwright/experimental-electron'; test('should work', async ({ trace }) => { - const electronApp = await _electron.launch({ args: [${JSON.stringify(path.join(__dirname, '../electron/electron-window-app.js'))}] }); + const electronApp = await electron.launch({ args: [${JSON.stringify(path.join(__dirname, '../electron/electron-window-app.js'))}] }); const window = await electronApp.firstWindow(); if (trace) diff --git a/tests/page/page-leaks.spec.ts b/tests/page/page-leaks.spec.ts index d38488932f62f..7b09f33e3f875 100644 --- a/tests/page/page-leaks.spec.ts +++ b/tests/page/page-leaks.spec.ts @@ -24,7 +24,7 @@ function leakedJSHandles(): string { const map = new MultiMap(); for (const [h, e] of (globalThis as any).leakedJSHandles) { const name = `[${h.worldNameForTest()}] ${h.preview()}`; - if (name === '[main] UtilityScript' || name === '[utility] UtilityScript' || name === '[electron] UtilityScript' || name === '[main] InjectedScript' || name === '[utility] InjectedScript' || name === '[electron] ElectronModule') + if (name === '[main] UtilityScript' || name === '[utility] UtilityScript' || name === '[electron] UtilityScript' || name === '[worker] UtilityScript' || name === '[main] InjectedScript' || name === '[utility] InjectedScript' || name === '[electron] ElectronModule' || name === '[worker] ElectronModule') continue; map.set(e.stack, name); } diff --git a/utils/build/build.js b/utils/build/build.js index a4b5ae0770480..288dcd5519fdf 100644 --- a/utils/build/build.js +++ b/utils/build/build.js @@ -533,7 +533,7 @@ for (const pkg of workspace.packages()) { // playwright-client is built as a bundle. if (['@playwright/client'].includes(pkg.name)) continue; - if (pkg.name === 'playwright-core' || pkg.name === 'playwright') + if (pkg.name === 'playwright-core' || pkg.name === 'playwright' || pkg.name === '@playwright/experimental-electron') continue; steps.push(new EsbuildStep({ @@ -546,6 +546,40 @@ for (const pkg of workspace.packages()) { })); } +// playwright-electron/lib/electron.js — self-contained bundle that inlines +// @utils/* and @isomorphic/* sources (via tsconfig paths) plus the `node_modules` +// deps. playwright-core, electron, and the sibling loader.js are resolved at +// runtime. +{ + const electronPkg = filePath('packages/playwright-electron'); + steps.push(new EsbuildStep({ + bundle: true, + entryPoints: [path.join(electronPkg, 'src/electron.ts')], + outfile: path.join(electronPkg, 'lib/electron.js'), + sourcemap: withSourceMaps ? 'linked' : false, + platform: 'node', + format: 'cjs', + external: [ + 'playwright-core', + 'playwright-core/*', + 'electron', + 'electron/*', + './loader', + ], + }, [filePath('packages/utils'), filePath('packages/isomorphic')])); + + // loader.ts is preloaded inside the Electron main process via `-r` and is + // already self-contained (no @utils/@isomorphic imports). Compile it + // per-file so the output stays a thin shim. + steps.push(new EsbuildStep({ + entryPoints: [path.join(electronPkg, 'src/loader.ts')], + outdir: path.join(electronPkg, 'lib'), + sourcemap: withSourceMaps ? 'linked' : false, + platform: 'node', + format: 'cjs', + })); +} + // Build playwright-core exported entry points. steps.push(new EsbuildStep({ entryPoints: [ @@ -839,19 +873,6 @@ steps.push(new EsbuildStep({ plugins: [dynamicImportToRequirePlugin], }, [filePath('packages/playwright/src')])); -// Build the Electron preload loader as a standalone CJS file. It runs inside -// the Electron process (via `electron -r loader.js`) and must not depend on -// coreBundle. `electron` is resolved at runtime by the Electron process. -steps.push(new EsbuildStep({ - bundle: true, - entryPoints: [filePath('packages/playwright-core/src/server/electron/loader.ts')], - outfile: filePath('packages/playwright-core/lib/server/electron/loader.js'), - sourcemap: withSourceMaps ? 'linked' : false, - platform: 'node', - format: 'cjs', - external: ['electron'], -}, [playwrightCoreSrc])); - function copyXdgOpen() { const outdir = filePath('packages/playwright-core/lib'); if (!fs.existsSync(outdir)) @@ -968,9 +989,11 @@ onChanges.push({ 'docs/src/api/', 'docs/src/test-api/', 'docs/src/test-reporter-api/', + 'docs/src/electron-api/', 'utils/generate_types/overrides.d.ts', 'utils/generate_types/overrides-test.d.ts', 'utils/generate_types/overrides-testReporter.d.ts', + 'utils/generate_types/overrides-electron.d.ts', 'utils/generate_types/exported.json', 'packages/playwright-core/src/server/chromium/protocol.d.ts', ], diff --git a/utils/doclint/cli.js b/utils/doclint/cli.js index 33f2846ee0722..a7471868c9f74 100755 --- a/utils/doclint/cli.js +++ b/utils/doclint/cli.js @@ -133,6 +133,7 @@ async function run() { const apiRoot = path.join(documentationRoot, 'api'); const testApiRoot = path.join(documentationRoot, 'test-api'); const testReporterApiRoot = path.join(documentationRoot, 'test-reporter-api'); + const electronApiRoot = path.join(documentationRoot, 'electron-api'); for (const lang of langs) { try { let documentation = parseApi(apiRoot); @@ -141,6 +142,8 @@ async function run() { parseApi(testApiRoot, path.join(documentationRoot, 'api', 'params.md')) ).mergeWith( parseApi(testReporterApiRoot) + ).mergeWith( + parseApi(electronApiRoot, path.join(documentationRoot, 'api', 'params.md')) ); } documentation.filterForLanguage(lang); @@ -178,7 +181,7 @@ async function run() { // Standardise naming and remove the filter in the file name // Also, Internally (playwright.dev generator) we merge test-api and test-reporter-api into api. - const canonicalName = filePath.replace(/(-(js|python|csharp|java))+/, '').replace(/(\/|\\)(test-api|test-reporter-api)(\/|\\)/, `${path.sep}api${path.sep}`); + const canonicalName = filePath.replace(/(-(js|python|csharp|java))+/, '').replace(/(\/|\\)(test-api|test-reporter-api|electron-api)(\/|\\)/, `${path.sep}api${path.sep}`); mdSections.add(canonicalName); const data = fs.readFileSync(filePath, 'utf-8'); @@ -186,7 +189,7 @@ async function run() { // Validates code snippet groups. rootNode = docs.processCodeGroups(rootNode, lang, tabs => tabs.map(tab => tab.spec)); // Renders links. - if (!filePath.startsWith(apiRoot) && !filePath.startsWith(testApiRoot) && !filePath.startsWith(testReporterApiRoot)) + if (!filePath.startsWith(apiRoot) && !filePath.startsWith(testApiRoot) && !filePath.startsWith(testReporterApiRoot) && !filePath.startsWith(electronApiRoot)) documentation.renderLinksInNodes(rootNode); // Validate links. { diff --git a/utils/generate_types/index.js b/utils/generate_types/index.js index ad0531180884f..1fe6a24fe1e58 100644 --- a/utils/generate_types/index.js +++ b/utils/generate_types/index.js @@ -500,6 +500,7 @@ class TypesGenerator { const coreDocumentation = parseApi(path.join(PROJECT_DIR, 'docs', 'src', 'api')); const testDocumentation = parseApi(path.join(PROJECT_DIR, 'docs', 'src', 'test-api'), path.join(PROJECT_DIR, 'docs', 'src', 'api', 'params.md')); const reporterDocumentation = parseApi(path.join(PROJECT_DIR, 'docs', 'src', 'test-reporter-api')); + const electronDocumentation = parseApi(path.join(PROJECT_DIR, 'docs', 'src', 'electron-api'), path.join(PROJECT_DIR, 'docs', 'src', 'api', 'params.md')); const assertionClasses = new Set([ 'APIResponseAssertions', 'GenericAssertions', @@ -516,7 +517,10 @@ class TypesGenerator { const documentation = coreDocumentation.clone(); const generator = new TypesGenerator({ documentation, - doNotGenerate: assertionClasses, + doNotGenerate: new Set([ + ...assertionClasses, + ...electronDocumentation.classesArray.map(cls => cls.name), + ]), }); let types = await generator.generateTypes(path.join(__dirname, 'overrides.d.ts')); const namedDevices = Object.keys(devices).map(name => ` ${JSON.stringify(name)}: DeviceDescriptor;`).join('\n'); @@ -606,6 +610,20 @@ class TypesGenerator { return await generator.generateTypes(path.join(__dirname, 'overrides-testReporter.d.ts')); } + /** + * @returns {Promise} + */ + async function generateElectronTypes() { + const documentation = coreDocumentation.mergeWith(electronDocumentation); + const generator = new TypesGenerator({ + documentation, + doNotGenerate: new Set([ + ...coreDocumentation.classesArray.map(cls => cls.name), + ]), + }); + return await generator.generateTypes(path.join(__dirname, 'overrides-electron.d.ts')); + } + /** * @param {string} filePath * @param {string} content @@ -635,6 +653,8 @@ class TypesGenerator { writeFile(path.join(clientTypesDir, 'types.d.ts'), coreTypes, true); writeFile(path.join(playwrightTypesDir, 'test.d.ts'), await generateTestTypes(), true); writeFile(path.join(playwrightTypesDir, 'testReporter.d.ts'), await generateReporterTypes(), true); + const electronTypesDir = path.join(PROJECT_DIR, 'packages', 'playwright-electron'); + writeFile(path.join(electronTypesDir, 'types.d.ts'), await generateElectronTypes(), true); process.exit(0); })().catch(e => { console.error(e); diff --git a/utils/generate_types/overrides-electron.d.ts b/utils/generate_types/overrides-electron.d.ts new file mode 100644 index 0000000000000..bd01b7ae03211 --- /dev/null +++ b/utils/generate_types/overrides-electron.d.ts @@ -0,0 +1,33 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { BrowserContext, ConsoleMessage, JSHandle, Page } from 'playwright-core'; +import type { ChildProcess } from 'child_process'; + +export * from 'playwright-core'; + +//@ts-ignore this will be any if electron is not installed +type ElectronType = typeof import('electron'); + +export interface ElectronApplication { + evaluate: JSHandle['evaluate']; + evaluateHandle: JSHandle['evaluateHandle']; +} + +export const electron: Electron; + +// This is required to not export everything by default. See https://github.com/Microsoft/TypeScript/issues/19545#issuecomment-340490459 +export {}; diff --git a/utils/generate_types/overrides.d.ts b/utils/generate_types/overrides.d.ts index 7d86dd472f196..d47a105693f01 100644 --- a/utils/generate_types/overrides.d.ts +++ b/utils/generate_types/overrides.d.ts @@ -276,17 +276,6 @@ class TimeoutError extends Error {} export const devices: Devices; -//@ts-ignore this will be any if electron is not installed -type ElectronType = typeof import('electron'); - -export interface ElectronApplication { - evaluate(pageFunction: PageFunctionOn, arg: Arg): Promise; - evaluate(pageFunction: PageFunctionOn, arg?: any): Promise; - - evaluateHandle(pageFunction: PageFunctionOn, arg: Arg): Promise>; - evaluateHandle(pageFunction: PageFunctionOn, arg?: any): Promise>; -} - export type AndroidElementInfo = { clazz: string; desc: string; @@ -392,7 +381,6 @@ export type AndroidKey = 'Copy' | 'Paste'; -export const _electron: Electron; export const _android: Android; // This is required to not export everything by default. See https://github.com/Microsoft/TypeScript/issues/19545#issuecomment-340490459 diff --git a/utils/workspace.js b/utils/workspace.js index 2f98fc2f3ecf5..01ee8ceee7e2e 100755 --- a/utils/workspace.js +++ b/utils/workspace.js @@ -214,6 +214,11 @@ const workspace = new Workspace(ROOT_PATH, [ path: path.join(ROOT_PATH, 'packages', 'playwright-ct-vue'), files: ['LICENSE'], }), + new PWPackage({ + name: '@playwright/experimental-electron', + path: path.join(ROOT_PATH, 'packages', 'playwright-electron'), + files: ['LICENSE'], + }), ]); if (require.main === module) {