Deeplink Testing
The service provides the ability to test custom protocol handlers and deeplinks in your Electron application using the browser.electron.triggerDeeplink() method. This feature automatically handles platform-specific differences, particularly on Windows where deeplinks would normally launch a new instance instead of reaching the test instance.
Overview
What is Deeplink Testing?
Deeplink testing allows you to verify that your Electron application correctly handles custom protocol URLs (e.g., myapp://action?param=value). This is essential when your app registers as a protocol handler and needs to respond to URLs opened from external sources like web browsers, emails, or other applications.
Why is it Needed?
Testing protocol handlers presents unique challenges:
- Windows Issue: On Windows, triggering a deeplink normally launches a new app instance instead of routing to the running test instance. This happens because the test instance and the externally-triggered instance use different user data directories.
- Test Automation: You need a programmatic way to trigger deeplinks without manual intervention.
- Cross-Platform Testing: Different platforms use different mechanisms to trigger protocol handlers.
When Should You Use It?
Use browser.electron.triggerDeeplink() when you need to:
- Test that your app correctly handles custom protocol URLs
- Verify deeplink parameter parsing and routing logic
- Ensure single-instance behavior works correctly
- Test protocol handler registration and activation
- Validate deeplink-driven workflows in your application
Basic Usage
Simple Example
describe('Protocol Handler Tests', () => {
it('should handle custom protocol deeplinks', async () => {
// Trigger the deeplink
await browser.electron.triggerDeeplink('myapp://open?file=test.txt');
// Wait for app to process it
await browser.waitUntil(async () => {
const openedFile = await browser.electron.execute(() => {
return globalThis.lastOpenedFile;
});
return openedFile === 'test.txt';
}, {
timeout: 5000,
timeoutMsg: 'App did not handle the deeplink'
});
});
});
Complex URL Parameters
The method preserves all URL parameters, including complex query strings:
it('should preserve query parameters', async () => {
await browser.electron.triggerDeeplink(
'myapp://action?param1=value1¶m2=value2&array[]=a&array[]=b'
);
const receivedParams = await browser.electron.execute(() => {
return globalThis.lastDeeplinkParams;
});
expect(receivedParams.param1).toBe('value1');
expect(receivedParams.param2).toBe('value2');
expect(receivedParams.array).toEqual(['a', 'b']);
});
Error Handling
it('should reject invalid protocols', async () => {
await expect(
browser.electron.triggerDeeplink('https://example.com')
).rejects.toThrow('Invalid deeplink protocol');
});
it('should reject malformed URLs', async () => {
await expect(
browser.electron.triggerDeeplink('not a url')
).rejects.toThrow('Invalid deeplink URL');
});
Platform Behavior
The service handles platform-specific differences automatically:
Windows
Behavior:
- Uses
cmd /c startcommand to trigger the deeplink - Automatically appends the test instance's
userDatadirectory as a query parameter - Cannot use script-based apps (
appEntryPoint) - requires packaged binary
URL Modification:
// Input URL
'myapp://test?foo=bar'
// URL triggered on Windows (userData appended automatically)
'myapp://test?foo=bar&userData=/tmp/electron-test'
Why This is Needed:
On Windows, when a protocol URL is opened, the OS launches the registered application binary. Without the userData parameter, this creates a new instance with a different user data directory, preventing Electron's single-instance lock from working correctly. By appending the userData parameter, your app can use the same directory as the test instance, allowing the single-instance lock to route the deeplink to the test instance.
macOS
Behavior:
- Uses
opencommand to trigger the deeplink - No URL modification needed (OS handles single-instance automatically)
- No special configuration required
URL Modification:
// URL passed unchanged
'myapp://test?foo=bar'
Linux
Behavior:
- Uses
gio opencommand to trigger the deeplink - Automatically appends the test instance's
userDatadirectory as a query parameter (like Windows) - Cannot use script-based apps (
appEntryPoint) - requires packaged binary
URL Modification:
// Input URL
'myapp://test?foo=bar'
// URL triggered on Linux (userData appended automatically)
'myapp://test?foo=bar&userData=/tmp/electron-test'
Why This is Needed:
Similar to Windows, Linux requires the userData parameter to ensure the deeplink-triggered instance uses the same user data directory as the test instance, enabling Electron's single-instance lock to route the deeplink correctly.
Setup Requirements
1. Service Configuration
Windows & Linux Configuration
On Windows and Linux, you must use a packaged binary (not appEntryPoint). Script-based apps cannot register protocol handlers at the OS level.
wdio.conf.ts
export const config = {
capabilities: [
{
browserName: 'electron',
'wdio:electronServiceOptions': {
// Use packaged binary (auto-detected or explicit)
appBinaryPath: './dist/win-unpacked/MyApp.exe',
// Optional but recommended: Explicit user data directory
appArgs: ['--user-data-dir=/tmp/test-user-data']
}
}
]
};
Important Notes:
appEntryPointwill NOT work for protocol handler testing on Windows/Linux- You must use
appBinaryPathor let the service auto-detect your binary - The service will warn you if you're using
appEntryPointwith protocol handlers - See Service Configuration for help finding your app binary path
macOS Configuration
macOS works with both packaged binaries and script-based apps:
wdio.conf.ts
export const config = {
capabilities: [
{
browserName: 'electron',
'wdio:electronServiceOptions': {
// Either works on macOS
appBinaryPath: './dist/mac/MyApp.app/Contents/MacOS/MyApp',
// OR
appEntryPoint: './dist/main.js'
}
}
]
};
2. Protocol Handler Registration
Your app must register as a protocol handler. This is typically done in your main process:
import { app } from 'electron';
// Register protocol handler
if (process.defaultApp) {
// Development: Include path to main file
app.setAsDefaultProtocolClient('myapp', process.execPath, [
path.resolve(process.argv[1])
]);
} else {
// Production: No additional arguments needed
app.setAsDefaultProtocolClient('myapp');
}
Note: Replace 'myapp' with your custom protocol scheme.