Skip to content

Commit 952449c

Browse files
committed
Add Tauri WebDriver E2E workflow
1 parent 9797079 commit 952449c

6 files changed

Lines changed: 4341 additions & 289 deletions

File tree

.github/workflows/ci.yml

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,3 +73,67 @@ jobs:
7373

7474
- name: Run browser E2E
7575
run: pnpm e2e
76+
77+
tauri-webdriver-e2e:
78+
name: Tauri WebDriver E2E (${{ matrix.name }})
79+
runs-on: ${{ matrix.platform }}
80+
timeout-minutes: 45
81+
strategy:
82+
fail-fast: false
83+
matrix:
84+
include:
85+
- name: Linux
86+
platform: ubuntu-22.04
87+
- name: Windows
88+
platform: windows-latest
89+
90+
steps:
91+
- name: Checkout
92+
uses: actions/checkout@v6
93+
94+
- name: Install Linux Tauri and WebDriver dependencies
95+
if: runner.os == 'Linux'
96+
run: |
97+
sudo apt-get update
98+
sudo apt-get install -y \
99+
libwebkit2gtk-4.1-dev \
100+
libayatana-appindicator3-dev \
101+
librsvg2-dev \
102+
patchelf \
103+
libxdo-dev \
104+
webkit2gtk-driver \
105+
xvfb
106+
107+
- name: Set up pnpm
108+
uses: pnpm/action-setup@v6
109+
110+
- name: Set up Node
111+
uses: actions/setup-node@v6
112+
with:
113+
node-version: lts/*
114+
cache: pnpm
115+
116+
- name: Install frontend dependencies
117+
run: pnpm install --frozen-lockfile
118+
119+
- name: Set up Rust
120+
uses: dtolnay/rust-toolchain@stable
121+
122+
- name: Install Microsoft Edge Driver
123+
if: runner.os == 'Windows'
124+
shell: pwsh
125+
run: |
126+
cargo install --git https://github.com/chippers/msedgedriver-tool
127+
& "$HOME/.cargo/bin/msedgedriver-tool.exe"
128+
$PWD.Path >> $env:GITHUB_PATH
129+
130+
- name: Install tauri-driver
131+
run: cargo install tauri-driver --locked
132+
133+
- name: Run Tauri WebDriver E2E on Linux
134+
if: runner.os == 'Linux'
135+
run: xvfb-run pnpm e2e:tauri
136+
137+
- name: Run Tauri WebDriver E2E on Windows
138+
if: runner.os == 'Windows'
139+
run: pnpm e2e:tauri

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ src-tauri/target/
77
.workflow/
88
AGENTS.md
99
.playwright-mcp/
10+
playwright-report/
11+
test-results/
1012
openpet-settings-ui*.png
1113
*.log
1214
.env

e2e-tauri/specs/pet-window.e2e.mjs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
const PET_HIT_TARGET = '[data-testid="pet-hit-target"]';
2+
3+
describe('OpenPet Tauri desktop window', () => {
4+
it('boots the native pet window and opens its context menu', async () => {
5+
await switchToPetWindow();
6+
7+
const route = await browser.execute(() => new URLSearchParams(window.location.search).get('window'));
8+
expect(route).toBe('pet');
9+
10+
const hitTarget = await $(PET_HIT_TARGET);
11+
await expect(hitTarget).toBeDisplayed();
12+
13+
await hitTarget.click();
14+
await hitTarget.click({ button: 'right' });
15+
16+
const menu = await $('[role="menu"]');
17+
await expect(menu).toBeDisplayed();
18+
19+
const menuText = await menu.getText();
20+
expect(menuText).toMatch(/Open settings|/);
21+
});
22+
});
23+
24+
async function switchToPetWindow() {
25+
await browser.waitUntil(
26+
async () => {
27+
const handles = await browser.getWindowHandles();
28+
29+
for (const handle of handles) {
30+
await browser.switchToWindow(handle);
31+
32+
const hitTarget = await $(PET_HIT_TARGET);
33+
if (await hitTarget.isExisting()) {
34+
return true;
35+
}
36+
}
37+
38+
return false;
39+
},
40+
{
41+
timeout: 20_000,
42+
interval: 500,
43+
timeoutMsg: 'Expected the OpenPet pet window to be available to WebDriver.',
44+
},
45+
);
46+
}

e2e-tauri/wdio.conf.mjs

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import os from 'node:os';
2+
import path from 'node:path';
3+
import { spawn, spawnSync } from 'node:child_process';
4+
import { fileURLToPath } from 'node:url';
5+
6+
const rootDir = path.resolve(fileURLToPath(new URL('..', import.meta.url)));
7+
const tauriTargetDir = path.join(rootDir, 'src-tauri', 'target', 'debug');
8+
const appBinaryName = process.platform === 'win32' ? 'openpet.exe' : 'openpet';
9+
const appBinaryPath = path.join(tauriTargetDir, appBinaryName);
10+
const pnpmBinary = process.platform === 'win32' ? 'pnpm.cmd' : 'pnpm';
11+
const tauriDriverBinary =
12+
process.env.TAURI_DRIVER ??
13+
path.join(os.homedir(), '.cargo', 'bin', process.platform === 'win32' ? 'tauri-driver.exe' : 'tauri-driver');
14+
const tauriDriverArgs = process.env.TAURI_NATIVE_DRIVER
15+
? ['--native-driver', process.env.TAURI_NATIVE_DRIVER]
16+
: [];
17+
18+
let tauriDriver;
19+
let tauriDriverExitExpected = false;
20+
21+
export const config = {
22+
runner: 'local',
23+
host: '127.0.0.1',
24+
port: 4444,
25+
specs: [path.join(rootDir, 'e2e-tauri', 'specs', '**', '*.mjs')],
26+
maxInstances: 1,
27+
logLevel: process.env.WDIO_LOG_LEVEL ?? 'warn',
28+
bail: 0,
29+
waitforTimeout: 10_000,
30+
connectionRetryTimeout: 120_000,
31+
connectionRetryCount: 3,
32+
capabilities: [
33+
{
34+
maxInstances: 1,
35+
'tauri:options': {
36+
application: appBinaryPath,
37+
},
38+
},
39+
],
40+
reporters: ['spec'],
41+
framework: 'mocha',
42+
mochaOpts: {
43+
ui: 'bdd',
44+
timeout: 60_000,
45+
},
46+
47+
onPrepare: () => {
48+
if (process.platform === 'darwin') {
49+
throw new Error('Tauri WebDriver desktop tests are supported on Windows and Linux only.');
50+
}
51+
52+
if (process.env.OPENPET_SKIP_TAURI_BUILD === '1') {
53+
return;
54+
}
55+
56+
const result = spawnSync(pnpmBinary, ['tauri', 'build', '--debug', '--no-bundle'], {
57+
cwd: rootDir,
58+
stdio: 'inherit',
59+
});
60+
61+
if (result.status !== 0) {
62+
throw new Error(`Tauri debug build failed with exit code ${result.status ?? 'unknown'}.`);
63+
}
64+
},
65+
66+
beforeSession: () => {
67+
tauriDriverExitExpected = false;
68+
tauriDriver = spawn(tauriDriverBinary, tauriDriverArgs, {
69+
cwd: rootDir,
70+
stdio: ['ignore', 'inherit', 'inherit'],
71+
});
72+
73+
tauriDriver.on('error', (error) => {
74+
console.error('tauri-driver error:', error);
75+
process.exit(1);
76+
});
77+
78+
tauriDriver.on('exit', (code) => {
79+
if (!tauriDriverExitExpected) {
80+
console.error('tauri-driver exited unexpectedly with code:', code);
81+
process.exit(1);
82+
}
83+
});
84+
},
85+
86+
afterSession: () => {
87+
closeTauriDriver();
88+
},
89+
90+
onComplete: () => {
91+
closeTauriDriver();
92+
},
93+
};
94+
95+
function closeTauriDriver() {
96+
tauriDriverExitExpected = true;
97+
98+
if (tauriDriver && !tauriDriver.killed) {
99+
tauriDriver.kill();
100+
}
101+
}
102+
103+
function registerShutdownCleanup() {
104+
process.once('exit', () => {
105+
closeTauriDriver();
106+
});
107+
108+
const exitAfterCleanup = (code) => {
109+
closeTauriDriver();
110+
process.exit(code);
111+
};
112+
113+
process.once('SIGINT', () => exitAfterCleanup(130));
114+
process.once('SIGTERM', () => exitAfterCleanup(143));
115+
process.once('SIGHUP', () => exitAfterCleanup(129));
116+
117+
if (process.platform === 'win32') {
118+
process.once('SIGBREAK', () => exitAfterCleanup(130));
119+
}
120+
}
121+
122+
registerShutdownCleanup();

package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
"build": "tsc --noEmit && vite build",
1212
"typecheck": "tsc --noEmit",
1313
"e2e": "playwright test",
14+
"e2e:tauri": "wdio run e2e-tauri/wdio.conf.mjs",
1415
"e2e:ui": "playwright test --ui",
1516
"release:check": "pnpm build && cargo check --manifest-path src-tauri/Cargo.toml",
1617
"release:bundle": "pnpm release:check && pnpm tauri:build",
@@ -32,6 +33,10 @@
3233
"@types/react": "^19.2.8",
3334
"@types/react-dom": "^19.2.3",
3435
"@vitejs/plugin-react": "^6.0.1",
36+
"@wdio/cli": "^9.27.1",
37+
"@wdio/local-runner": "^9.27.1",
38+
"@wdio/mocha-framework": "^9.27.1",
39+
"@wdio/spec-reporter": "^9.27.1",
3540
"typescript": "^6.0.3",
3641
"vite": "^8.0.10"
3742
},

0 commit comments

Comments
 (0)