Playwright’s Electron support lets you test desktop applications with the same API you use for web.
Here’s a Playwright Electron application in action. Imagine we’re testing a simple calculator app.
// main.js (Electron main process)
const { app, BrowserWindow } = require('electron');
const path = require('path');
function createWindow() {
const mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
// This is the key for Playwright: enableRemoteModule
enableRemoteModule: true,
},
});
mainWindow.loadURL('http://localhost:3000'); // Or load a local HTML file
}
app.whenReady().then(createWindow);
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
// preload.js (Electron preload script)
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('electronAPI', {
getPlatform: () => process.platform,
});
Now, let’s write a Playwright test for this.
// calculator.spec.js
const { test, expect } = require('@playwright/test');
test('calculator adds two numbers', async ({ page }) => {
// Navigate to the Electron app.
// Playwright automatically handles launching Electron.
await page.goto('app://./index.html'); // Or your app's URL
// Interact with the app.
await page.fill('#number1', '5');
await page.fill('#number2', '3');
await page.click('#addButton');
// Assert the result.
const result = await page.textContent('#result');
expect(result).toBe('8');
});
To run this, you’d typically have your Electron app running in development or build it first. Playwright’s electronType option in playwright.config.js is crucial.
// playwright.config.js
module.exports = {
use: {
// Set this to 'electron' to enable Electron testing
// You can also specify the path to your electron executable if needed
// electron: '/path/to/your/electron.exe',
// For typical setups, Playwright can find it automatically.
// If you're using a specific build tool like electron-builder,
// you might need to point to the built app.
electron: {
// Optional: Specify the path to your Electron app binary.
// If not provided, Playwright will try to find it.
// Example: '/path/to/your/built/app/win-unpacked/YourApp.exe'
// Or for macOS: '/path/to/your/built/app/YourApp.app/Contents/MacOS/YourApp'
},
},
// ... other configurations
};
The page.goto('app://./index.html') might look odd. This app:// protocol is how Playwright communicates with your Electron application. It essentially tells Playwright to load the specified resource (an HTML file, a URL, etc.) within the context of the launched Electron app. Playwright injects its testing harness into this context, allowing you to interact with your app’s DOM and even Node.js modules if configured correctly.
The enableRemoteModule: true in main.js is a security concern in production but often necessary for older Electron apps or specific testing scenarios where you need to access Node.js APIs directly from the renderer process. Playwright’s testing harness leverages this to bridge the gap.
The preload.js script is standard Electron practice for securely exposing Node.js APIs to the renderer. In our test, we can even call methods exposed by the preload script using the page object.
// calculator.spec.js (continued)
test('gets platform info', async ({ page }) => {
// Accessing the exposed API from preload.js
const platform = await page.evaluate(() => window.electronAPI.getPlatform());
console.log(`Running on platform: ${platform}`);
expect(platform).toBeDefined();
});
This page.evaluate() allows you to run arbitrary JavaScript within the renderer process of your Electron app, enabling interaction with your app’s frontend logic and any APIs exposed via preload scripts. Playwright then captures the return value.
The real magic happens because Playwright doesn’t just launch a browser. When configured for Electron, it launches the Electron runtime itself, and then loads your application within that runtime. The page object you get in your tests is not a browser tab in the traditional sense, but rather a view into your Electron application’s renderer process. This means you can interact with your app’s UI elements as you would with any web page, but the underlying execution environment is your desktop application.
A common pitfall is forgetting to enable enableRemoteModule or not correctly setting up the electron option in playwright.config.js. If Playwright can’t find your Electron executable, tests will fail with cryptic errors about not being able to launch the application. For packaged applications, you’ll need to point Playwright directly to the executable within your build’s output directory.
The page.goto('app://./') syntax is a convention. You can also point it to a specific file like page.goto('file:///path/to/your/index.html') if your app is structured that way, but app:// is generally preferred as it signals to Playwright that it’s interacting with an Electron app.
The most surprising thing about Playwright’s Electron support is that it treats your desktop application’s renderer process exactly like a browser tab, providing a unified API for both web and desktop testing without requiring a separate testing framework for your desktop app.
The next hurdle you’ll likely encounter is handling multiple windows or dialogs within your Electron application.