Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Some links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.

Playwright does not bind your Electron button’s handler. Your renderer code must register the handler; the test launches Electron, obtains a Playwright Page for the renderer window, locates the button, and clicks it. A minimal test is:

const { _electron: electron } = require('playwright');

const electronApp = await electron.launch({ args: ['main.js'] });
const window = await electronApp.firstWindow();
await window.getByRole('button', { name: 'Save' }).click();
await electronApp.close();

This article shows the application-side binding, reliable locators, child-window handling, direct event dispatch, assertions, and fixes for common Electron automation failures.

Understand what is being bound

An Electron application normally has a main process and one or more renderer processes. The renderer owns the HTML button and its behavior. Bind the event with the mechanism your renderer uses; Playwright only drives the already-running page.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Plain DOM renderer

const saveButton = document.querySelector('#save');
saveButton.addEventListener('click', async () => {
  await saveDocument();
  document.querySelector('#status').textContent = 'Saved';
});

Register this code after the button exists, or use a module/deferred script so querySelector does not return null.

Framework renderer

React, Vue, Angular and similar frameworks should use their normal callback syntax (for example, React’s onClick). Do not add a second Playwright-side handler. The test should verify the user-visible result or another intentional side effect.

Launch Electron and get the renderer page

Install Playwright in the project and launch the same entry point you use locally:

npm install -D playwright
npx playwright install

Playwright’s Electron API is documented as experimental. The documentation lists Electron 12.2.0+, 13.4.0+ and 14+ support notes; check the current page against your exact Playwright and Electron versions before pinning a combination.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
const { test, expect } = require('@playwright/test');
const { _electron: electron } = require('playwright');

test('saves a document', async () => {
  const electronApp = await electron.launch({ args: ['main.js'] });
  const page = await electronApp.firstWindow();

  await page.getByRole('button', { name: 'Save' }).click();
  await expect(page.getByText('Saved')).toBeVisible();

  await electronApp.close();
});

firstWindow() waits for the first application window and returns a Playwright Page. Keep the close call in a finally block in production tests so a failed assertion does not leave an Electron process running:

const electronApp = await electron.launch({ args: ['main.js'] });
try {
  const page = await electronApp.firstWindow();
  await page.getByRole('button', { name: 'Save' }).click();
} finally {
  await electronApp.close();
}

Choose a locator that identifies the button

Prefer the accessible role and name

await page.getByRole('button', { name: 'Save' }).click();

This follows the control’s accessibility contract and survives many CSS refactors. The name can come from visible text, an associated label, or an ARIA label.

Use a stable test ID when the name is dynamic

await page.getByTestId('save-button').click();

Configure your application to expose a stable data-testid (or the test-ID attribute configured in your Playwright setup). Avoid selectors tied to generated class names.

Use CSS or text only when necessary

await page.locator('#save').click();
await page.locator('button').filter({ hasText: 'Save' }).click();

These can be appropriate for legacy markup, but ensure the locator resolves to exactly one intended button. A strict-mode error is useful evidence that the selector is ambiguous, not a reason to force a random match.

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Trigger the event like a user

locator.click() is the normal API. Playwright waits for the element to be actionable, including attachment, visibility and an unobstructed position, before performing the click. It therefore catches problems a real user would encounter.

const save = page.getByRole('button', { name: 'Save' });
await save.click();
await expect(page.getByRole('status')).toHaveText('Saved');

If an intentional animation or overlay makes the click temporarily impossible, wait for the relevant state rather than immediately forcing the action:

await expect(page.getByRole('button', { name: 'Save' })).toBeEnabled();
await page.getByRole('button', { name: 'Save' }).click();

click({ force: true }) bypasses actionability checks. Use it only when the test specifically needs to click through a known UI condition; otherwise it can hide an accessibility or layout defect.

Dispatch a DOM click deliberately

Use dispatchEvent('click') when the test is about the DOM handler itself rather than a user-realistic interaction:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
await page.getByTestId('save-button').dispatchEvent('click');

Playwright documents this as direct event dispatch, equivalent to element.click(). It can dispatch on a hidden element and skips normal actionability checks. It does not register a persistent listener and it does not prove that a user could reach the control. Keep the distinction explicit in the test name.

Wait for windows opened by a click

When the button creates another BrowserWindow, begin waiting before clicking so the event cannot be missed. The Electron application emits a window event for each new loaded window; windows() reports currently open windows.

const childWindowPromise = electronApp.waitForEvent('window');
await page.getByRole('button', { name: 'Open details' }).click();
const childWindow = await childWindowPromise;
await childWindow.getByRole('heading', { name: 'Details' }).waitFor();

You can also inspect existing windows when startup creates more than one:

const pages = electronApp.windows();
for (const candidate of pages) {
  console.log(await candidate.title());
}

If a click opens an external URL or a native dialog instead of an Electron renderer window, handle that behavior at the application boundary; a Page is available only for a loaded renderer window.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Assert the result, not merely the click

A click assertion alone can pass while the handler throws or silently does nothing. Assert the visible state, navigation, emitted application state, or a controlled mock:

await page.getByRole('button', { name: 'Save' }).click();
await expect(page.getByRole('status')).toHaveText('Saved');

await page.getByRole('button', { name: 'Open details' }).click();
const details = await childWindowPromise;
await expect(details).toHaveTitle(/Details/);

For asynchronous handlers, await the UI result rather than inserting an arbitrary sleep. If the handler calls a backend, make the dependency deterministic with a test fixture or mocked endpoint.

Troubleshoot failures systematically

Electron launch times out

First verify the executable arguments and entry file. Playwright’s Electron documentation specifically calls out the nodeCliInspect Electron fuse (FuseV1Options.EnableNodeCliInspectArguments): if it is disabled, launch under Playwright can time out. Check the fuse configuration for the packaged or development build and confirm version compatibility.

“Locator not found” or strict-mode errors

  • Wait for the renderer’s ready state or a unique heading before locating the button.
  • Inspect the accessible name; icon-only buttons may need an accessible label.
  • Replace generated CSS classes with a role/name or stable test ID.
  • Resolve duplicate buttons by scoping to a dialog, form or window.
const dialog = page.getByRole('dialog', { name: 'Preferences' });
await dialog.getByRole('button', { name: 'Save' }).click();

Click is intercepted or the button is not actionable

Check overlays, disabled state, animation and scroll position. Wait for the overlay to disappear and the button to become enabled. Use force only when bypassing those checks is the behavior under test.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

The click runs but no state changes

Confirm that the renderer actually bound the listener, that the script ran after the element was created, and that the handler did not throw. Capture console output and page errors:

page.on('console', message => console.log('[renderer]', message.text()));
page.on('pageerror', error => console.error('[renderer error]', error));

Then assert the handler’s intended result. A direct dispatchEvent can help isolate a DOM-binding problem, but switch back to click() for the user-path test.

The child-window wait hangs

Ensure the promise is created before the click and that the application really creates a BrowserWindow. If the action reuses an existing window, inspect electronApp.windows() after the click instead of waiting for a new event.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Reliability and maintenance checklist

  • Keep renderer event binding in application code and test actions in test code.
  • Use one clear accessible name per interactive control.
  • Prefer locator.click(); reserve dispatch for explicit event-level tests.
  • Wait for observable readiness and assert observable outcomes.
  • Register window-event waits before actions that create windows.
  • Close the Electron application in teardown, including failure paths.
  • Pin and regularly verify Playwright/Electron versions because Electron automation support is experimental.

Or skip the browser setup:

If your goal is to capture an Electron-hosted or ordinary web page rather than test its click behavior, ScreenshotNeo provides a single screenshot request. Its cleanup accepts cookie or consent banners before capture and removes more than 60 known consent platforms, newsletter popups and chat widgets; each step can be disabled. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads and cache hits are not billed, and response headers report the page verdict and billing status. It also offers an MCP server with take_screenshot, get_page_info and capture_pdf for Claude, Cursor and other MCP clients.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

cURL

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

Python

import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)

Node.js

const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

See the complete option list and authentication details in the ScreenshotNeo documentation. Features include full-page and element capture, device presets, retina scale, PDF controls, custom CSS/JavaScript, click-before-capture, waits, request blocking, headers/cookies, geolocation, resizing, TTL caching, signed links, asynchronous jobs, bulk capture, usage reporting and an OpenAPI specification. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000. Create a free ScreenshotNeo account to get started.

Frequently Asked Questions

Does Playwright register the Electron button listener?

No. The renderer registers the listener; Playwright launches the app and performs the test action.

When should I use dispatchEvent instead of click?

Use dispatchEvent for an intentional DOM-event test, including hidden elements. Use locator.click() for normal user-path automation.

How do I capture a second Electron window?

Create electronApp.waitForEvent(‘window’) before clicking, then await the returned Page.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.