From aaff8ba2f062be9cd8a1bac0546ebf7942d8460e Mon Sep 17 00:00:00 2001 From: Milan Glisic Date: Wed, 8 Apr 2026 23:22:55 +0200 Subject: [PATCH] fix(electron): enable screenshot-on-failure for Electron apps --- .../playwright-core/src/client/electron.ts | 6 ++++- .../playwright-core/src/client/playwright.ts | 6 ++++- tests/electron/electron-app.spec.ts | 27 +++++++++++++++++++ .../playwright.artifacts.spec.ts | 27 +++++++++++++++++++ 4 files changed, 64 insertions(+), 2 deletions(-) diff --git a/packages/playwright-core/src/client/electron.ts b/packages/playwright-core/src/client/electron.ts index 2399aa61ff1ce..362d2c1de226e 100644 --- a/packages/playwright-core/src/client/electron.ts +++ b/packages/playwright-core/src/client/electron.ts @@ -66,7 +66,11 @@ export class Electron extends ChannelOwner implements }; 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)); + this._playwright._electronApps.add(app); + app.once(Events.ElectronApplication.Close, () => { + this._playwright.selectors._contextsForSelectors.delete(app._context); + this._playwright._electronApps.delete(app); + }); await app._context._initializeHarFromOptions(options.recordHar); app._context.tracing._tracesDir = options.tracesDir; return app; diff --git a/packages/playwright-core/src/client/playwright.ts b/packages/playwright-core/src/client/playwright.ts index c5f2074aee984..b561d8c9f9c59 100644 --- a/packages/playwright-core/src/client/playwright.ts +++ b/packages/playwright-core/src/client/playwright.ts @@ -23,6 +23,7 @@ import { TimeoutError } from './errors'; import { APIRequest } from './fetch'; import { Selectors } from './selectors'; +import type { ElectronApplication } from './electron'; import type * as channels from '@protocol/channels'; import type { LaunchOptions } from 'playwright-core'; @@ -41,6 +42,7 @@ export class Playwright extends ChannelOwner { _defaultLaunchOptions?: LaunchOptions; _defaultContextTimeout?: number; _defaultContextNavigationTimeout?: number; + readonly _electronApps = new Set(); constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.PlaywrightInitializer) { super(parent, type, guid, initializer); @@ -75,7 +77,9 @@ export class Playwright extends ChannelOwner { } _allContexts() { - return this._browserTypes().flatMap(type => [...type._contexts]); + const browserContexts = this._browserTypes().flatMap(type => [...type._contexts]); + const electronContexts = [...this._electronApps].map(app => app._context); + return [...browserContexts, ...electronContexts]; } _allPages() { diff --git a/tests/electron/electron-app.spec.ts b/tests/electron/electron-app.spec.ts index 7fa8f8950af86..8cdd98e38c2cd 100644 --- a/tests/electron/electron-app.spec.ts +++ b/tests/electron/electron-app.spec.ts @@ -365,3 +365,30 @@ test('should save downloads to artifactsDir', async ({ launchElectronApp, electr // User-provided artifactsDir should not be cleaned up. expect(fs.existsSync(artifactsDir)).toBeTruthy(); }); + +test('should include electron pages in playwright._allPages()', async ({ playwright, electronApp, newWindow }) => { + const window = await newWindow(); + await window.setContent('

Hello Electron

'); + const allPages = (playwright as any)._allPages(); + expect(allPages).toContain(window); +}); + +test('should include electron context in playwright._allContexts()', async ({ playwright, electronApp }) => { + const allContexts = (playwright as any)._allContexts(); + expect(allContexts).toContain(electronApp.context()); +}); + +test('should remove electron context from tracking on close', async ({ playwright, launchElectronApp }) => { + const app = await launchElectronApp('electron-app.js'); + const context = app.context(); + expect((playwright as any)._allContexts()).toContain(context); + await app.close(); + expect((playwright as any)._allContexts()).not.toContain(context); +}); + +test('should take screenshot of electron window', async ({ electronApp, newWindow }) => { + const window = await newWindow(); + await window.setContent('

Screenshot Test

'); + const screenshot = await window.screenshot(); + expect(screenshot.byteLength).toBeGreaterThan(0); +}); diff --git a/tests/playwright-test/playwright.artifacts.spec.ts b/tests/playwright-test/playwright.artifacts.spec.ts index 1ea81bdca904a..31c973e766d26 100644 --- a/tests/playwright-test/playwright.artifacts.spec.ts +++ b/tests/playwright-test/playwright.artifacts.spec.ts @@ -615,3 +615,30 @@ test('should take screenshot when page is closed in afterEach', async ({ runInli expect(result.failed).toBe(1); expect(fs.existsSync(testInfo.outputPath('test-results', 'a-fails', 'test-failed-1.png'))).toBeTruthy(); }); + +test('should work with screenshot: only-on-failure for electron', async ({ runInlineTest }, testInfo) => { + const result = await runInlineTest({ + 'electron.spec.ts': ` + import { test, expect, _electron } from '@playwright/test'; + + test('should fail and capture electron screenshot', async () => { + const electronApp = await _electron.launch({ args: [${JSON.stringify(path.join(__dirname, '..', 'electron', 'electron-window-app.js'))}] }); + const window = await electronApp.firstWindow(); + await window.setContent('

Electron Screenshot Test

'); + expect(1).toBe(2); + }); + `, + 'playwright.config.ts': ` + module.exports = { use: { screenshot: 'only-on-failure' } }; + `, + }, { workers: 1 }, { PLAYWRIGHT_NO_COPY_PROMPT: 'true' }); + + expect(result.exitCode).toBe(1); + expect(result.failed).toBe(1); + expect(listFiles(testInfo.outputPath('test-results'))).toEqual([ + '.last-run.json', + 'electron-should-fail-and-capture-electron-screenshot', + ' error-context.md', + ' test-failed-1.png', + ]); +});