From workflows
This skill should be used when the user asks to "test Electron app", "automate Electron desktop app", "debug Electron renderer", "test VS Code extension", "E2E test Electron", or needs Chrome DevTools Protocol automation for Electron applications. Use for renderer process debugging, main process control, native menu automation, and file dialog testing.
npx claudepluginhub edwinhu/workflows --plugin workflowsThis skill uses the workspace's default tool permissions.
**Announce:** "I'm using dev-test-electron for Electron app automation via Chrome DevTools Protocol."
Implements Playwright E2E testing patterns: Page Object Model, test organization, configuration, reporters, artifacts, and CI/CD integration for stable suites.
Guides Next.js 16+ Turbopack for faster dev via incremental bundling, FS caching, and HMR; covers webpack comparison, bundle analysis, and production builds.
Discovers and evaluates Laravel packages via LaraPlugins.io MCP. Searches by keyword/feature, filters by health score, Laravel/PHP compatibility; fetches details, metrics, and version history.
Announce: "I'm using dev-test-electron for Electron app automation via Chrome DevTools Protocol."
## REAL Test Requirements for Electron AppsA REAL Electron test must replicate what the user does. FAKE tests test something else.
Before writing ANY test, verify from SPEC.md/PLAN.md:
| REAL Test Criteria | Your Test Must |
|---|---|
| User workflow | Replicate exact steps (click → type → see result) |
| Protocol | Use SAME protocol as production (WebSocket, IPC, etc.) |
| UI interaction | Interact with ACTUAL UI elements user sees |
| Verification | Check what USER sees, not internal state |
Electron apps often use WebSocket/IPC internally. Testing HTTP is a FAKE test.
| FAKE Electron Test | Why It's Fake | REAL Test |
|---|---|---|
| HTTP endpoint test | App uses WebSocket | Test WebSocket connection |
| Direct function call | User clicks button | CDP Input.dispatchMouseEvent or Runtime.evaluate click |
| Check internal state | User sees panel/status | CDP screenshot or DOM query |
| Mock IPC layer | Production uses real IPC | Test actual IPC messages |
| Skip main process | Main process has logic | Test BOTH renderer AND main |
If any answer is "I don't know" → Go back to SPEC.md. Don't guess.
| Thought | Reality |
|---|---|
| "HTTP is easier to test" | But app uses WebSocket. Test WebSocket. |
| "I can call the function directly" | User clicks a button. Use CDP Input events. |
| "CDP is complex, let me mock" | Mocking hides bugs. Use real CDP. |
| "Main process is hard to test" | Main process crashes break app. Test it. |
| "Panel state is internal" | User SEES the panel. Test what user sees. |
| "IPC is just plumbing" | IPC bugs cause silent failures. Test it. |
Before taking screenshots or running E2E tests, you MUST complete all 6 gates from dev-tdd:
GATE 1: BUILD
GATE 2: LAUNCH (with file-based logging)
GATE 3: WAIT
GATE 4: CHECK PROCESS
GATE 5: READ LOGS ← MANDATORY, CANNOT SKIP
GATE 6: VERIFY LOGS
THEN: E2E tests/screenshots
You loaded dev-tdd earlier. Follow the gates now.
Verify CDP tooling is available before proceeding.
Check for these tools:
# Check for curl (CDP communication)
which curl || echo "MISSING: curl"
# Check for jq (JSON parsing)
which jq || echo "MISSING: jq"
# Check for websocat or wscat (WebSocket CLI)
which websocat || which wscat || echo "MISSING: WebSocket CLI"
If missing tools:
STOP: Cannot proceed with Electron CDP automation.
Missing tools needed for CDP:
- curl (for HTTP requests)
- jq (for JSON parsing)
- websocat or wscat (for WebSocket communication)
Install with:
# macOS: Install via nix-darwin (see ~/nix/). Do NOT use brew.
# Linux: sudo apt install curl jq websocat
Reply when installed and I'll continue testing.
This gate is non-negotiable. Missing tools = full stop.
## When to Use Electron CDPUSE Electron CDP when you need:
DO NOT use Electron CDP when:
For web apps or native desktop apps, discover and read the relevant skill: Related skills:
${CLAUDE_SKILL_DIR}/../../skills/dev-test-chrome/SKILL.md and follow its instructions.${CLAUDE_SKILL_DIR}/../../skills/dev-test-playwright/SKILL.md and follow its instructions.${CLAUDE_SKILL_DIR}/../../skills/dev-test-hammerspoon/SKILL.md and follow its instructions.${CLAUDE_SKILL_DIR}/../../skills/dev-test-linux/SKILL.md and follow its instructions.| Thought | Reality |
|---|---|
| "Chrome MCP works for Electron" | NO. Electron has main process + renderer. Need Electron-specific CDP. |
| "Playwright can test Electron apps" | NO. Playwright is for web browsers, not Electron main process. |
| "I'll just test the renderer" | Main process matters. File dialogs, native menus need testing too. |
| "CDP is too complex" | It's the ONLY way to properly test Electron apps. Learn it. |
| "I can skip the main process" | NO. Main process crashes break the app. Test both. |
| Capability | Electron CDP | Chrome MCP | Playwright MCP | Hammerspoon |
|---|---|---|---|---|
| Electron renderer | ✅ | ❌ | ❌ | ❌ |
| Electron main process | ✅ | ❌ | ❌ | ❌ |
| Native menus/dialogs | ✅ | ❌ | ❌ | ✅ (macOS only) |
| Multi-window Electron | ✅ | ❌ | ❌ | ✅ (macOS only) |
| Console/network debugging | ✅ | ✅ (web only) | ❌ | ❌ |
| Headless mode | ✅ | ❌ | ✅ (web only) | ❌ |
| WebSocket IPC | ✅ | ❌ | ❌ | ❌ |
EVERY Electron E2E test MUST establish CDP connection BEFORE any automation.
You CANNOT automate without:
| Action | Why It Fails Without Connection |
|---|---|
| Send CDP command | No connection = command never sent |
| Read console logs | Can't receive events without WebSocket |
| Navigate to page | CDP Page.navigate requires connection |
| Take screenshot | CDP Page.captureScreenshot requires connection |
"App is running" ≠ "CDP is connected". Verify connection first.
Launch Electron with remote debugging port:
# Option 1: Fixed port
/path/to/electron-app --remote-debugging-port=9222
# Option 2: Random port (app outputs port number)
/path/to/electron-app --remote-debugging-port=0
# Option 3: With logging
/path/to/electron-app --remote-debugging-port=9222 --enable-logging --log-file=/tmp/electron.log 2>&1 &
CRITICAL: For GATE 2 (LAUNCH), always use --log-file flag for file-based logging.
# Get list of inspectable targets
curl -s http://localhost:9222/json/list | jq '.'
# Extract WebSocket URL for main target
WS_URL=$(curl -s http://localhost:9222/json/list | jq -r '.[0].webSocketDebuggerUrl')
echo "WebSocket URL: $WS_URL"
Example response:
[
{
"description": "",
"devtoolsFrontendUrl": "/devtools/inspector.html?ws=localhost:9222/devtools/page/...",
"id": "page-id",
"title": "My Electron App",
"type": "page",
"url": "file:///app/index.html",
"webSocketDebuggerUrl": "ws://localhost:9222/devtools/page/..."
}
]
# Interactive WebSocket session
websocat "$WS_URL"
# Send CDP commands (one per line, JSON format)
{"id":1,"method":"Runtime.enable"}
{"id":2,"method":"Page.enable"}
{"id":3,"method":"Runtime.evaluate","params":{"expression":"document.title"}}
Helper script: See scripts/connect-electron-cdp.sh for automated connection.
| Domain | Purpose | Example |
|---|---|---|
| Runtime | Execute JavaScript, console logs | Runtime.evaluate, Runtime.consoleAPICalled |
| Page | Navigation, screenshots, DOM events | Page.navigate, Page.captureScreenshot |
| DOM | Query and manipulate DOM | DOM.getDocument, DOM.querySelector |
| Debugger | Breakpoints, step debugging | Debugger.setBreakpoint |
| Network | Network requests/responses | Network.enable, Network.responseReceived |
| Input | Keyboard/mouse events | Input.dispatchKeyEvent, Input.dispatchMouseEvent |
Enable domains before use:
{"id":1,"method":"Runtime.enable"}
{"id":2,"method":"Page.enable"}
{"id":3,"method":"DOM.enable"}
{"id":4,"method":"Network.enable"}
# Evaluate JavaScript expression
echo '{"id":1,"method":"Runtime.evaluate","params":{"expression":"document.title"}}' | websocat "$WS_URL"
# Execute with return value
echo '{"id":2,"method":"Runtime.evaluate","params":{"expression":"2 + 2","returnByValue":true}}' | websocat "$WS_URL"
# Execute complex script
SCRIPT='document.querySelector("#username").value = "testuser"'
echo "{\"id\":3,\"method\":\"Runtime.evaluate\",\"params\":{\"expression\":\"$SCRIPT\"}}" | websocat "$WS_URL"
# Navigate to URL (file:// or http://)
echo '{"id":10,"method":"Page.navigate","params":{"url":"file:///app/index.html"}}' | websocat "$WS_URL"
# Wait for load event
echo '{"id":11,"method":"Page.enable"}' | websocat "$WS_URL"
# Listen for Page.loadEventFired event
# Enable Runtime domain to receive console events
echo '{"id":20,"method":"Runtime.enable"}' | websocat "$WS_URL"
# Console events arrive as:
# {"method":"Runtime.consoleAPICalled","params":{"type":"log","args":[...]}}
For complete console reading, see references/cdp-api.md
# Capture viewport screenshot (PNG base64)
echo '{"id":30,"method":"Page.captureScreenshot"}' | websocat "$WS_URL" > response.json
# Extract base64 and decode
jq -r '.result.data' response.json | base64 -d > screenshot.png
# Get document root
echo '{"id":40,"method":"DOM.getDocument"}' | websocat "$WS_URL"
# Query selector
ROOT_ID=$(jq -r '.result.root.nodeId' response.json)
echo "{\"id\":41,\"method\":\"DOM.querySelector\",\"params\":{\"nodeId\":$ROOT_ID,\"selector\":\"#submit-btn\"}}" | websocat "$WS_URL"
Electron has TWO processes:
| Process | What It Does | How to Test |
|---|---|---|
| Main | Node.js runtime, native APIs, file system, menus, dialogs | CDP Runtime.evaluate in main context OR IPC |
| Renderer | Browser/Chromium runtime, web content, DOM | CDP commands (Page, DOM, Runtime) |
Both processes MUST be tested. Renderer-only testing is incomplete.
Some Electron apps expose main process debugging:
# Check for main process target
curl -s http://localhost:9222/json/list | jq '.[] | select(.type == "node")'
If main process is available:
# Get main process WebSocket URL
MAIN_WS=$(curl -s http://localhost:9222/json/list | jq -r '.[] | select(.type == "node") | .webSocketDebuggerUrl')
# Execute Node.js code in main process
echo '{"id":1,"method":"Runtime.evaluate","params":{"expression":"process.version"}}' | websocat "$MAIN_WS"
# From renderer, send IPC to main process
SCRIPT='require("electron").ipcRenderer.send("open-file-dialog")'
echo "{\"id\":50,\"method\":\"Runtime.evaluate\",\"params\":{\"expression\":\"$SCRIPT\"}}" | websocat "$WS_URL"
For advanced main process patterns, see references/electron-specific.md
EVERY CDP command must be VERIFIED. Sending the command is not enough.
After sending a CDP command, you MUST:
error field in response)| Command | Verification |
|---|---|
Runtime.evaluate | Check result.value or result.exceptionDetails |
Page.navigate | Wait for Page.loadEventFired event |
Page.captureScreenshot | Verify result.data exists and decode base64 |
DOM.querySelector | Check result.nodeId exists (not 0) |
"I sent the command" is not verification. Read the response and verify success.
# Send command and capture response
RESPONSE=$(echo '{"id":100,"method":"Runtime.evaluate","params":{"expression":"2 + 2"}}' | websocat --one-message "$WS_URL")
# Check for error
if echo "$RESPONSE" | jq -e '.error' > /dev/null; then
echo "ERROR: CDP command failed"
echo "$RESPONSE" | jq '.error'
exit 1
fi
# Verify result
RESULT=$(echo "$RESPONSE" | jq -r '.result.result.value')
if [ "$RESULT" != "4" ]; then
echo "ERROR: Expected 4, got $RESULT"
exit 1
fi
echo "✓ VERIFIED: 2 + 2 = $RESULT"
# Enable Page domain
echo '{"id":1,"method":"Page.enable"}' | websocat "$WS_URL" &
# Navigate and wait for load event
echo '{"id":2,"method":"Page.navigate","params":{"url":"file:///app/index.html"}}' | websocat "$WS_URL"
# Wait for Page.loadEventFired event (listen to WebSocket)
# Event format: {"method":"Page.loadEventFired","params":{...}}
#!/bin/bash
set -e
# ============ GATE 1: BUILD ============
echo "GATE 1: Building Electron app..."
cd /path/to/electron-app
npm run build
echo "✓ GATE 1 PASSED"
# ============ GATE 2: LAUNCH ============
echo "GATE 2: Launching with CDP and logging..."
npm start -- --remote-debugging-port=9222 --enable-logging --log-file=/tmp/electron.log 2>&1 &
APP_PID=$!
echo "✓ GATE 2 PASSED (PID: $APP_PID)"
# ============ GATE 3: WAIT ============
echo "GATE 3: Waiting for Electron initialization..."
sleep 3
echo "✓ GATE 3 PASSED"
# ============ GATE 4: CHECK PROCESS ============
echo "GATE 4: Checking Electron process..."
if ! ps -p $APP_PID > /dev/null; then
echo "✗ GATE 4 FAILED: Electron process crashed"
echo "Reading logs from GATE 5..."
cat /tmp/electron.log
exit 1
fi
# Verify CDP port is open
if ! curl -s http://localhost:9222/json/list > /dev/null; then
echo "✗ GATE 4 FAILED: CDP port not accessible"
cat /tmp/electron.log
exit 1
fi
echo "✓ GATE 4 PASSED"
# ============ GATE 5: READ LOGS ============
echo "GATE 5: Reading full runtime logs..."
echo "=== ELECTRON RUNTIME LOGS ==="
cat /tmp/electron.log
echo "=== END LOGS ==="
echo "✓ GATE 5 PASSED (logs read)"
# ============ GATE 6: VERIFY LOGS ============
echo "GATE 6: Verifying no errors in logs..."
if grep -qE "(ERROR|FATAL|CRITICAL|Segmentation|core dumped|Uncaught Exception)" /tmp/electron.log; then
echo "✗ GATE 6 FAILED: Errors found in logs"
exit 1
fi
echo "✓ GATE 6 PASSED"
# ============ NOW: E2E TESTING ============
echo "All gates passed. Proceeding to E2E tests..."
# Get WebSocket URL
WS_URL=$(curl -s http://localhost:9222/json/list | jq -r '.[0].webSocketDebuggerUrl')
echo "CDP WebSocket: $WS_URL"
# Enable Runtime domain
echo '{"id":1,"method":"Runtime.enable"}' | websocat --one-message "$WS_URL"
# Execute test: Get document title
RESPONSE=$(echo '{"id":2,"method":"Runtime.evaluate","params":{"expression":"document.title","returnByValue":true}}' | websocat --one-message "$WS_URL")
# Verify response
if echo "$RESPONSE" | jq -e '.error' > /dev/null; then
echo "✗ E2E FAILED: CDP command error"
echo "$RESPONSE" | jq '.error'
exit 1
fi
TITLE=$(echo "$RESPONSE" | jq -r '.result.result.value')
echo "✓ E2E VERIFIED: Document title = '$TITLE'"
# Take screenshot
SCREENSHOT_RESPONSE=$(echo '{"id":3,"method":"Page.captureScreenshot"}' | websocat --one-message "$WS_URL")
echo "$SCREENSHOT_RESPONSE" | jq -r '.result.data' | base64 -d > /tmp/electron_screenshot.png
echo "✓ Screenshot saved: /tmp/electron_screenshot.png"
# Cleanup
kill $APP_PID
echo "✓ E2E TEST PASSED"
Tool description: Execute all 6 gates, then run Electron E2E test with CDP
#!/bin/bash
# Assumes gates 1-6 already passed and WS_URL is set
# Enable domains
echo '{"id":1,"method":"Runtime.enable"}' | websocat --one-message "$WS_URL"
echo '{"id":2,"method":"Page.enable"}' | websocat --one-message "$WS_URL"
# Fill form field
FILL_USERNAME='document.querySelector("#username").value = "testuser"'
RESPONSE=$(echo "{\"id\":10,\"method\":\"Runtime.evaluate\",\"params\":{\"expression\":\"$FILL_USERNAME\"}}" | websocat --one-message "$WS_URL")
if echo "$RESPONSE" | jq -e '.error' > /dev/null; then
echo "✗ FAILED: Could not fill username"
exit 1
fi
FILL_PASSWORD='document.querySelector("#password").value = "testpass"'
echo "{\"id\":11,\"method\":\"Runtime.evaluate\",\"params\":{\"expression\":\"$FILL_PASSWORD\"}}" | websocat --one-message "$WS_URL"
# Click submit button
CLICK_SUBMIT='document.querySelector("#submit-btn").click()'
echo "{\"id\":12,\"method\":\"Runtime.evaluate\",\"params\":{\"expression\":\"$CLICK_SUBMIT\"}}" | websocat --one-message "$WS_URL"
# Wait for navigation
sleep 1
# Verify success message appears
CHECK_SUCCESS='document.querySelector(".success-message") !== null'
VERIFY_RESPONSE=$(echo "{\"id\":13,\"method\":\"Runtime.evaluate\",\"params\":{\"expression\":\"$CHECK_SUCCESS\",\"returnByValue\":true}}" | websocat --one-message "$WS_URL")
SUCCESS=$(echo "$VERIFY_RESPONSE" | jq -r '.result.result.value')
if [ "$SUCCESS" != "true" ]; then
echo "✗ VERIFICATION FAILED: Success message not found"
exit 1
fi
echo "✓ VERIFIED: Form submission successful"
# Screenshot for evidence
echo '{"id":14,"method":"Page.captureScreenshot"}' | websocat --one-message "$WS_URL" | jq -r '.result.data' | base64 -d > /tmp/form_success.png
echo "✓ Screenshot: /tmp/form_success.png"
# Get all inspectable targets
curl -s http://localhost:9222/json/list | jq '.[] | {title: .title, url: .url, wsUrl: .webSocketDebuggerUrl}'
# Connect to specific window by title
WINDOW_WS=$(curl -s http://localhost:9222/json/list | jq -r '.[] | select(.title == "Settings Window") | .webSocketDebuggerUrl')
# Automate the settings window
echo '{"id":1,"method":"Runtime.evaluate","params":{"expression":"document.querySelector(\"#theme\").value = \"dark\""}}' | websocat --one-message "$WINDOW_WS"
For more advanced patterns, see references/advanced-patterns.md
| Error | Cause | Solution |
|---|---|---|
| Connection refused | Electron not started with --remote-debugging-port | Restart with flag |
| WebSocket timeout | App crashed or port blocked | Check GATE 4 (process) and GATE 5 (logs) |
"error":{"code":-32601} | Method not found | Enable domain first (e.g., Runtime.enable) |
exceptionDetails in result | JavaScript error in evaluated code | Check expression syntax |
| Empty response | WebSocket closed | Reconnect to WebSocket |
# Retry CDP command up to 3 times
for i in {1..3}; do
RESPONSE=$(echo "$CDP_COMMAND" | websocat --one-message "$WS_URL")
if echo "$RESPONSE" | jq -e '.result' > /dev/null; then
echo "✓ Command succeeded on attempt $i"
break
fi
if [ $i -eq 3 ]; then
echo "✗ Command failed after 3 attempts"
echo "$RESPONSE"
exit 1
fi
echo "Retry $i failed, waiting 1s..."
sleep 1
done
| Need | Why Electron CDP Fails | Use Instead |
|---|---|---|
| Native macOS window management | CDP doesn't control OS | Hammerspoon (macOS) |
| Cross-platform native automation | CDP is Chromium-only | Platform-specific tools |
| Test non-Electron apps | CDP requires Electron/Chromium | Hammerspoon, dev-test-linux |
| Headless CI/CD for web apps | Electron is for desktop apps | Playwright MCP |
For web apps, use Playwright or Chrome MCP. For native desktop, use platform tools.
For detailed CDP API documentation and Electron-specific features:
references/cdp-api.md - Complete CDP domains reference (Runtime, Page, DOM, Network, Input, Debugger)references/electron-specific.md - Electron main process, IPC, native APIs, file dialogsreferences/advanced-patterns.md - Multi-window, devtools, event listeners, WebSocket streamingWorking examples in examples/:
basic-test.sh - Complete E2E test with all 6 gatescdp-commands.json - Common CDP command referenceUtility scripts in scripts/:
connect-electron-cdp.sh - Automated CDP connection discoverylaunch-electron-with-logging.sh - Launch template with proper loggingverify-electron-process.sh - Health check for main + renderer| User Action | FAKE Test | REAL Test |
|---|---|---|
| Highlight text in editor | editor.setSelection() programmatically | CDP simulate actual text selection |
| Click Claude panel | Call panel function directly | CDP click on actual panel element |
| See status in panel | Check internal state variable | CDP query panel DOM for displayed text |
| Extension uses WebSocket | Test HTTP endpoint | Test WebSocket connection |
Before testing, discover what protocol the extension uses:
# Search for WebSocket usage
rg "WebSocket|ws://" --type ts
# Search for HTTP usage
rg "fetch|axios|http" --type ts
# Search for IPC usage
rg "ipcRenderer|ipcMain" --type ts
If extension uses WebSocket → Your test MUST use WebSocket, not HTTP.
FAKE test (DON'T DO THIS):
// FAKE: Calls function directly, checks internal state
const selection = await vscode.window.activeTextEditor.selection;
await extensionApi.updateSelection(selection); // Direct call!
expect(internalState.selectionCount).toBe(5); // Internal state!
REAL test (DO THIS):
# REAL: Simulates user, checks what user sees
# 1. Use CDP to simulate text selection in editor
SCRIPT='
const editor = document.querySelector(".monaco-editor");
// Simulate actual selection via CDP Input events
'
echo "{\"id\":1,\"method\":\"Runtime.evaluate\",\"params\":{\"expression\":\"$SCRIPT\"}}" | websocat "$WS_URL"
# 2. Use CDP to query Claude panel for displayed status
VERIFY='document.querySelector(".claude-panel .status-text").textContent'
RESULT=$(echo "{\"id\":2,\"method\":\"Runtime.evaluate\",\"params\":{\"expression\":\"$VERIFY\",\"returnByValue\":true}}" | websocat --one-message "$WS_URL")
# 3. Verify user-visible output
STATUS=$(echo "$RESULT" | jq -r '.result.result.value')
if [[ "$STATUS" != *"5 lines selected"* ]]; then
echo "✗ FAKE TEST: Panel doesn't show expected status"
exit 1
fi
echo "✓ REAL TEST: Panel shows '$STATUS'"
Before writing VS Code extension test, verify:
[ ] Protocol discovered (WebSocket/HTTP/IPC)
[ ] User workflow documented (what user clicks/sees)
[ ] Test uses SAME protocol as extension
[ ] Test simulates ACTUAL user actions (not API calls)
[ ] Test verifies PANEL DISPLAY (not internal state)
[ ] Test covers BOTH main and renderer processes
If any box is unchecked → Your test is probably FAKE.
This skill is referenced by dev-test for Electron desktop application testing.
Related skills:
${CLAUDE_SKILL_DIR}/../../skills/dev-test-chrome/SKILL.md and follow its instructions.${CLAUDE_SKILL_DIR}/../../skills/dev-test-playwright/SKILL.md and follow its instructions.${CLAUDE_SKILL_DIR}/../../skills/dev-test-hammerspoon/SKILL.md and follow its instructions.${CLAUDE_SKILL_DIR}/../../skills/dev-tdd/SKILL.md and follow its instructions.