diff --git a/packages/playwright-core/src/client/electron.ts b/packages/playwright-core/src/client/electron.ts index 37eb77e29f..630942a5ef 100644 --- a/packages/playwright-core/src/client/electron.ts +++ b/packages/playwright-core/src/client/electron.ts @@ -51,12 +51,14 @@ export class Electron extends ChannelOwner implements extraHTTPHeaders: options.extraHTTPHeaders && headersObjectToArray(options.extraHTTPHeaders), env: envObjectToArray(options.env ? options.env : process.env), }; - return ElectronApplication.from((await this._channel.launch(params)).electronApplication); + const app = ElectronApplication.from((await this._channel.launch(params)).electronApplication); + app._context._options = params; + return app; } } export class ElectronApplication extends ChannelOwner implements api.ElectronApplication { - private _context: BrowserContext; + readonly _context: BrowserContext; private _windows = new Set(); private _timeoutSettings = new TimeoutSettings(); @@ -91,7 +93,7 @@ export class ElectronApplication extends ChannelOwner Promise; constructor(browser: Browser, options: types.BrowserContextOptions, browserContextId: string | undefined) { super(browser, 'browser-context'); @@ -275,6 +276,10 @@ export abstract class BrowserContext extends SdkObject { await Promise.all(Array.from(this._downloads).map(download => download.artifact.deleteOnContextClose())); } + setCustomCloseHandler(handler: (() => Promise) | undefined) { + this._customCloseHandler = handler; + } + async close(metadata: CallMetadata) { if (this._closedStatus === 'open') { this.emit(BrowserContext.Events.BeforeClose); @@ -291,7 +296,9 @@ export abstract class BrowserContext extends SdkObject { promises.push(artifact.finishedPromise()); } - if (this._isPersistentContext) { + if (this._customCloseHandler) { + await this._customCloseHandler(); + } else if (this._isPersistentContext) { // Close all the pages instead of the context, // because we cannot close the default context. await Promise.all(this.pages().map(page => page.close(metadata))); @@ -305,6 +312,10 @@ export abstract class BrowserContext extends SdkObject { promises.push(this._deleteAllDownloads()); await Promise.all(promises); + // Custom handler should trigger didCloseInternal itself. + if (this._customCloseHandler) + return; + // Persistent context should also close the browser. if (this._isPersistentContext) await this._browser.close(); diff --git a/packages/playwright-core/src/server/electron/electron.ts b/packages/playwright-core/src/server/electron/electron.ts index 1596d6127e..dd6ae8271c 100644 --- a/packages/playwright-core/src/server/electron/electron.ts +++ b/packages/playwright-core/src/server/electron/electron.ts @@ -26,7 +26,7 @@ import { Page } from '../page'; import { TimeoutSettings } from '../../utils/timeoutSettings'; import { WebSocketTransport } from '../transport'; import { launchProcess, envArrayToObject } from '../../utils/processLauncher'; -import { BrowserContext } from '../browserContext'; +import { BrowserContext, validateBrowserContextOptions } from '../browserContext'; import type { BrowserWindow } from 'electron'; import { Progress, ProgressController } from '../progress'; import { helper } from '../helper'; @@ -37,6 +37,7 @@ import * as readline from 'readline'; import { RecentLogsCollector } from '../../utils/debugLogger'; import { internalCallMetadata, SdkObject } from '../instrumentation'; import * as channels from '../../protocol/channels'; +import { BrowserContextOptions } from '../types'; const ARTIFACTS_FOLDER = path.join(os.tmpdir(), 'playwright-artifacts-'); @@ -69,6 +70,10 @@ export class ElectronApplication extends SdkObject { } }); }); + this._browserContext.setCustomCloseHandler(async () => { + const electronHandle = await this._nodeElectronHandlePromise; + await electronHandle.evaluate(({ app }) => app.quit()); + }); this._nodeSession.send('Runtime.enable', {}).catch(e => {}); } @@ -79,8 +84,7 @@ export class ElectronApplication extends SdkObject { async close() { const progressController = new ProgressController(internalCallMetadata(), this); const closed = progressController.run(progress => helper.waitForEvent(progress, this, ElectronApplication.Events.Close).promise, this._timeoutSettings.timeout({})); - const electronHandle = await this._nodeElectronHandlePromise; - await electronHandle.evaluate(({ app }) => app.quit()); + await this._browserContext.close(internalCallMetadata()); this._nodeConnection.close(); await closed; } @@ -168,26 +172,16 @@ export class Electron extends SdkObject { close: gracefullyClose, kill }; + const contextOptions: BrowserContextOptions = { + ...options, + noDefaultViewport: true, + }; const browserOptions: BrowserOptions = { ...this._playwrightOptions, name: 'electron', isChromium: true, headful: true, - persistent: { - noDefaultViewport: true, - acceptDownloads: options.acceptDownloads, - bypassCSP: options.bypassCSP, - colorScheme: options.colorScheme, - extraHTTPHeaders: options.extraHTTPHeaders, - geolocation: options.geolocation, - httpCredentials: options.httpCredentials, - ignoreHTTPSErrors: options.ignoreHTTPSErrors, - locale: options.locale, - offline: options.offline, - recordHar: options.recordHar, - recordVideo: options.recordVideo, - timezoneId: options.timezoneId, - }, + persistent: contextOptions, browserProcess, protocolLogger: helper.debugProtocolLogger(), browserLogsCollector, @@ -195,6 +189,7 @@ export class Electron extends SdkObject { downloadsPath: artifactsDir, tracesDir: artifactsDir, }; + validateBrowserContextOptions(contextOptions, browserOptions); const browser = await CRBrowser.connect(chromeTransport, browserOptions); app = new ElectronApplication(this, browser, nodeConnection); return app; diff --git a/tests/electron/electron-app.spec.ts b/tests/electron/electron-app.spec.ts index 1522444131..55300f5312 100644 --- a/tests/electron/electron-app.spec.ts +++ b/tests/electron/electron-app.spec.ts @@ -16,6 +16,7 @@ import type { BrowserWindow } from 'electron'; import path from 'path'; +import fs from 'fs'; import { electronTest as test, expect } from './electronTest'; test('should fire close event', async ({ playwright }) => { @@ -166,3 +167,16 @@ test('should return same browser window for browser view pages', async ({ playwr expect(firstWindowId).toEqual(secondWindowId); await app.close(); }); + +test('should record video', async ({ playwright }, testInfo) => { + const app = await playwright._electron.launch({ + args: [path.join(__dirname, 'electron-window-app.js')], + recordVideo: { dir: testInfo.outputPath('video') } + }); + const page = await app.firstWindow(); + await page.setContent(``); + await page.waitForTimeout(1000); + await app.close(); + const videoPath = await page.video().path(); + expect(fs.statSync(videoPath).size).toBeGreaterThan(0); +}); diff --git a/tests/electron/electronTest.ts b/tests/electron/electronTest.ts index c5a66cd2d1..88c95b0cd4 100644 --- a/tests/electron/electronTest.ts +++ b/tests/electron/electronTest.ts @@ -48,7 +48,9 @@ export const electronTest = baseTest.extend { const [ window ] = await Promise.all([ electronApp.waitForEvent('window'), - electronApp.evaluate(electron => { + electronApp.evaluate(async electron => { + // Avoid "Error: Cannot create BrowserWindow before app is ready". + await electron.app.whenReady(); const window = new electron.BrowserWindow({ width: 800, height: 600,