Skill · Testing
Electron drive skill
Launch the project's Electron app on a scratch profile and drive it: click, type, screenshot, run renderer or main-process code, read logs. Use to verify UI changes end to end.
How to use it
- Start your plan and connect your AI once
- Ask for the task in your own words, or say it directly:
Use the Electron drive skill skill to help me with this.Without a connection: copy the SKILL.md below into your AI's project instructions.
Driving the Electron app
Overview
scripts/drive.mjs launches the project's Electron app under Playwright in a background daemon and keeps it running between commands, so each action (click, type, snapshot, screenshot, eval) is one fast shell call. Every launch uses a scratch profile, so the agent can check a UI change or reproduce a bug in the real app, not only in unit tests, without touching the user's data.
When to Use This Skill
- Use when you need to verify a UI change in the running Electron app.
- Use when reproducing a renderer or startup bug, or a failure that only
- Use when checking first-run, onboarding or settings screens.
- Use when exercising a feature end to end, including IPC through the preload
- Do not use it for web apps without Electron, or for multi-window flows (see
shows up in the production build (for example a stricter CSP).
bridge or code in the main process.
Limitations).
How It Works
Step 1: Check prerequisites
scripts/drive.mjs, in this skill's directory, finds the project by the nearest package.json, so run it from anywhere inside the project. Set DR to its absolute path.
It needs electron and playwright-core (1.49 or later) installed in the project. If start says either is missing, tell the user rather than installing it yourself. Launch settings come from drive.config.json at the project root (see Step 4); with no file, it runs electron ..
Step 2: Start, look, act, stop
DR=<this skill's directory>/scripts/drive.mjs
$DR start # last build, profile 'default'; prints status
$DR snapshot # accessibility tree: find what to click, by role and name
$DR click 'role=button[name="Get started"]'
$DR fill 'role=textbox[name="Email"]' 'test@example.com'
$DR wait 'text=Welcome'
$DR screenshot # prints a PNG path; Read it to see the window
$DR logs --lines 80 # main and renderer console, plus the config's logFile
$DR stop # ALWAYS, before finishing
- Start options:
--build: run the config'sbuildcommand first. **Needed after any--fresh: wipe the scratch profile, which gives you the app's first-run--profile <name>: a separate scratch profile.--dev: see Step 5.- Targets are Playwright selectors. Prefer
role=button[name="…"]from --ends the flags. Put it before text that starts with dashes:- Other commands:
select <target> <value|label>,press <key>,
source change** if the app runs from a build, because the build is otherwise stale; start prints its age.
state.
snapshot output, then text=…, then CSS.
$DR fill 'role=textbox[name="Args"]' -- --verbose.
status, screenshot --selector <css>. $DR help lists everything.
Step 3: Run code in the app
eval <js> runs in the renderer, main <js> in the main process (electron and process are in scope, require is not). Use a bare expression, or a body with return. - reads the code from stdin, which avoids quoting entirely. The result comes back as JSON, so return plain data: a DOM node or a function comes back as undefined or {}.
To exercise IPC, call whatever the app's preload exposes through eval. That goes through the real preload bridge, as the app's own renderer code does.
Step 4: Configure the launch (optional)
drive.config.json at the project root; every field is optional:
{
"build": "npm run build",
"args": ["."],
"env": { "APP_DATA_DIR": "{profile}/data" },
"logFile": "logs/main.log",
"dev": {
"args": ["."],
"env": { "ELECTRON_RENDERER_URL": "http://localhost:5173" },
"url": "http://localhost:5173"
}
}
args: what Electron is launched with: an app directory whoseenv: added to the app's environment.{profile}becomes the scratchlogFile: a log file relative to the profile directory, shown bylogs.
package.json main is the built entry point, or the entry file itself.
profile directory (in env values only, not in args). This is how data kept outside userData is redirected: APP_DATA_DIR is only an example name, and it has an effect only if the app reads it. Check the app's source for the variable it actually uses.
If a project has no config and start fails or launches the wrong thing, work out these values from the project's package.json and build setup, then suggest a drive.config.json to the user.
Step 5: Dev mode (optional)
For a fast loop on renderer code, with hot reload and source maps:
- The user (or a background Bash call) starts the renderer dev server
$DR start --devlaunches Electron withdev.argsanddev.env, and
without its own Electron. Many templates' start/dev scripts launch Electron too, and two instances would share state. The config's dev.url is checked before launch.
NODE_ENV=development.
Examples
Example 1: Verify a change to the first-run screen
$DR start --build --fresh
$DR snapshot
$DR click 'role=button[name=/Next/]'
$DR screenshot
$DR logs --lines 40
$DR stop
Example 2: Inspect renderer state and call IPC
echo "return document.querySelectorAll('button').length" | $DR eval -
$DR eval "window.api.getSettings()"
Example 3: Query the main process
$DR main "electron.app.getVersion()"
$DR main "electron.BrowserWindow.getAllWindows().length"
Best Practices
- ✅ Look before acting. Take a
snapshotorscreenshotfirst. The same - ✅ Check
logsfor[renderer:error]and[renderer:pageerror]after - ✅ Prefer the production build. It catches failures that only show up
- ✅ Always
stop, including after a failure.stopreports how the app - ❌ Don't use the npm/yarn script wrappers (e.g.
yarn drive). Run the - ❌ Don't install
electronorplaywright-coreyourself; ask the user.
profile can open differently on a second launch (past the first-run screens), and a click that times out usually means the screen is not what you assumed.
each step.
there, such as a stricter Content-Security-Policy, which the dev server would miss.
went down; anything but "quit via app.quit()" (a hung quit, a SIGKILL) is worth a line in your report.
script directly. yarn 1 re-splits arguments and drops inner quotes, which silently breaks most JavaScript passed to eval and main.
Limitations
- One window. Commands act on the first window that is not DevTools, and
- One app at a time per project.
startrefuses while one is running. - Windows: run it as
node scripts/drive.mjs. The process-tree kill that - Requires
electronandplaywright-core1.49+ in the project, and Node.js - It does not replace the project's own test suite or a human check of
there is no way to pick another. status shows which window that is; a splash screen is current until it closes.
stop falls back to is POSIX only.
18+.
visual design. Stop and ask if the launch settings or data locations are unclear.
Security & Safety Notes
- Local-only. The daemon listens on
127.0.0.1with a random per-session - Never point it at the user's real data. Every launch gets
evalandmainrun arbitrary code with the app's privileges; the- **
start --buildruns thebuildcommand fromdrive.config.jsonin a --freshdeletes the scratch profile directory only, never the app's
token. Nothing is sent to third-party services.
--user-data-dir set to a scratch profile under $TMPDIR/electron-drive/<project>/profiles/<name>, which moves app.getPath('userData'). Data the app keeps elsewhere (a documents folder, a path from an env var) is only redirected if drive.config.json sets it through env. Check that before doing anything that writes data. If the app would touch real user files, stop and tell the user.
main process has full Node.js access. Only run code needed for the task, and never code that deletes files or makes network calls outside the app's normal behavior without the user's confirmation.
shell.** Read the config before the first --build in a project you did not set up.
real userData.
Common Pitfalls
- Problem:
startfails. - Problem: "The app is not running".
- Problem: The app shows old behavior after a source change.
- Problem: A click can't find a button whose name includes icon-font text
- Problem: A stuck state after an interrupted session.
Solution: Read the daemon log it prints. Common causes are a missing build (--build), electron or playwright-core not installed, or wrong args.
Solution: It crashed or quit. Run logs, then stop, then start.
Solution: The build is stale. Restart with start --build.
(such as arrow_forward). Solution: Use a regex name: role=button[name=/Next/].
Solution: $DR stop cleans up the daemon, the app and the state file, even when the daemon is already gone.
Related Skills
@electron-development- For building and packaging the Electron app@systematic-debugging- Pair with this skill to reproduce and narrow down
itself; use this skill to verify the result in the running app.
a bug in the real app.