Skip to content

Commit 3ba47e9

Browse files
committed
fix(e2e): probe tauri-webdriver readiness + binary-path fallback + fail CI on test
failure - setup/global.ts now polls /status, detects driver crash during startup, and writes stdout/stderr to reports/ so failures land in the existing artifact upload instead of being lost in xvfb-run output. - support/platform.ts probes both target/<triple>/debug and target/debug so macOS rows where --target matches the host triple stop failing with "binary not found". - e2e-webview.yml gets a post-test gate that re-asserts failure based on the continue-on-error test step outcomes — without it the job was green even when every spec failed.
1 parent 7d498df commit 3ba47e9

3 files changed

Lines changed: 106 additions & 27 deletions

File tree

.github/workflows/e2e-webview.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,3 +241,13 @@ jobs:
241241
path: e2e-webview/screenshots/
242242
if-no-files-found: ignore
243243
retention-days: 7
244+
245+
- name: Fail job if tests failed
246+
# The test steps use continue-on-error so the artifact-upload steps
247+
# above always run on failure. This gate restores red CI when any
248+
# spec fails — without it, a failing matrix row reports green.
249+
if: ${{ steps.gate.outputs.skip != 'true' && (steps.test-linux.outcome == 'failure' || steps.test-other.outcome == 'failure') }}
250+
shell: bash
251+
run: |
252+
echo "::error::E2E tests failed on ${{ matrix.label }} — see job logs and uploaded artifacts."
253+
exit 1

e2e-webview/setup/global.ts

Lines changed: 73 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,85 @@
11
import { spawn, type ChildProcess } from 'node:child_process';
2+
import { setTimeout as delay } from 'node:timers/promises';
3+
import fs from 'node:fs';
4+
import path from 'node:path';
5+
6+
const HOST = '127.0.0.1';
7+
const PORT = 4444;
8+
const READY_TIMEOUT_MS = 30_000;
9+
const POLL_INTERVAL_MS = 250;
10+
const STATUS_URL = `http://${HOST}:${PORT}/status`;
11+
12+
const REPORTS_DIR = path.resolve('./reports');
13+
fs.mkdirSync(REPORTS_DIR, { recursive: true });
214

315
export default async function globalSetup() {
16+
const stdoutPath = path.join(REPORTS_DIR, 'tauri-webdriver.stdout.log');
17+
const stderrPath = path.join(REPORTS_DIR, 'tauri-webdriver.stderr.log');
18+
const stdoutLog = fs.createWriteStream(stdoutPath);
19+
const stderrLog = fs.createWriteStream(stderrPath);
20+
421
const driver: ChildProcess = spawn('tauri-webdriver', [], {
5-
stdio: 'inherit',
22+
stdio: ['ignore', 'pipe', 'pipe'],
623
shell: process.platform === 'win32',
724
});
825

9-
await new Promise<void>((resolve, reject) => {
10-
driver.once('error', reject);
11-
// tauri-webdriver binds ports synchronously on startup; 2s is plenty.
12-
setTimeout(resolve, 2_000);
26+
driver.stdout?.on('data', (chunk: Buffer) => {
27+
stdoutLog.write(chunk);
28+
process.stdout.write(chunk);
29+
});
30+
driver.stderr?.on('data', (chunk: Buffer) => {
31+
stderrLog.write(chunk);
32+
process.stderr.write(chunk);
33+
});
34+
35+
type ExitInfo = { code: number | null; signal: NodeJS.Signals | null };
36+
const exitState: { value: ExitInfo | null } = { value: null };
37+
driver.once('exit', (code, signal) => {
38+
exitState.value = { code, signal };
39+
});
40+
41+
const spawnFailed = new Promise<never>((_, reject) => {
42+
driver.once('error', (err) =>
43+
reject(new Error(`tauri-webdriver failed to spawn: ${(err as Error).message}`)),
44+
);
1345
});
1446

47+
const ready = (async () => {
48+
const deadline = Date.now() + READY_TIMEOUT_MS;
49+
while (Date.now() < deadline) {
50+
const exited = exitState.value;
51+
if (exited) {
52+
throw new Error(
53+
`tauri-webdriver exited during startup (code=${exited.code}, signal=${exited.signal}). ` +
54+
`See ${path.relative(process.cwd(), stderrPath)} for driver output.`,
55+
);
56+
}
57+
try {
58+
const res = await fetch(STATUS_URL, { signal: AbortSignal.timeout(1_000) });
59+
if (res.ok) return;
60+
} catch {
61+
// not yet — keep polling
62+
}
63+
await delay(POLL_INTERVAL_MS);
64+
}
65+
throw new Error(
66+
`tauri-webdriver did not become ready on ${STATUS_URL} within ${READY_TIMEOUT_MS}ms. ` +
67+
`See ${path.relative(process.cwd(), stderrPath)} for driver output.`,
68+
);
69+
})();
70+
71+
try {
72+
await Promise.race([ready, spawnFailed]);
73+
} catch (err) {
74+
if (driver.exitCode === null && !driver.killed) driver.kill('SIGTERM');
75+
stdoutLog.end();
76+
stderrLog.end();
77+
throw err;
78+
}
79+
1580
return async () => {
16-
if (!driver.killed) driver.kill('SIGTERM');
81+
if (driver.exitCode === null && !driver.killed) driver.kill('SIGTERM');
82+
stdoutLog.end();
83+
stderrLog.end();
1784
};
1885
}

e2e-webview/support/platform.ts

Lines changed: 23 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -10,28 +10,30 @@ export function resolveAppBinary(): string {
1010

1111
const target = process.env.TAURI_TARGET;
1212
const repoRoot = path.resolve(process.cwd(), '..');
13-
const targetDir = path.join(
14-
repoRoot,
15-
'src-tauri',
16-
'target',
17-
...(target ? [target] : []),
18-
'debug',
19-
);
20-
2113
const binaryName = process.platform === 'win32' ? `${CRATE}.exe` : CRATE;
22-
const binaryPath = path.join(targetDir, binaryName);
2314

24-
if (!fs.existsSync(binaryPath)) {
25-
throw new Error(
26-
[
27-
`Tauri debug binary not found at ${binaryPath}.`,
28-
`Build it first from the repo root:`,
29-
` pnpm tauri build --debug --no-bundle --features webdriver${target ? ` --target ${target}` : ''}`,
30-
`(plain "cargo build" produces a binary with no embedded frontendDist —`,
31-
` use the tauri CLI so index.html is registered.)`,
32-
`Or set TAURI_E2E_BINARY to an explicit path.`,
33-
].join('\n'),
34-
);
15+
// Cargo writes to target/<triple>/debug when --target is passed and the
16+
// triple differs from the host. On some CI configurations (notably the
17+
// macos-15-intel runner with --target x86_64-apple-darwin matching the
18+
// host triple) the output lands at target/debug instead. Probe both.
19+
const candidates = [
20+
...(target ? [path.join(repoRoot, 'src-tauri', 'target', target, 'debug', binaryName)] : []),
21+
path.join(repoRoot, 'src-tauri', 'target', 'debug', binaryName),
22+
];
23+
24+
for (const candidate of candidates) {
25+
if (fs.existsSync(candidate)) return candidate;
3526
}
36-
return binaryPath;
27+
28+
throw new Error(
29+
[
30+
`Tauri debug binary not found. Searched:`,
31+
...candidates.map((c) => ` - ${c}`),
32+
`Build it first from the repo root:`,
33+
` pnpm tauri build --debug --no-bundle --features webdriver${target ? ` --target ${target}` : ''}`,
34+
`(plain "cargo build" produces a binary with no embedded frontendDist —`,
35+
` use the tauri CLI so index.html is registered.)`,
36+
`Or set TAURI_E2E_BINARY to an explicit path.`,
37+
].join('\n'),
38+
);
3739
}

0 commit comments

Comments
 (0)