mirror of
https://github.com/hanzoai/desktop.git
synced 2026-10-30 14:58:42 +00:00
* chore(deps): bump Tauri to 2.11 + all deps to latest within-major
Forwards-only, within-major dependency refresh across the JS and Rust
trees. No major-version crossings.
Tauri (Rust + JS kept mutually consistent):
- tauri 2.4 -> 2.11.3, tauri-build 2.4 -> 2.6, plugins -> latest 2.x
(dialog 2.7, fs 2.5, log 2.8, updater 2.10, deep-link 2.4, ...).
- @tauri-apps/api 2.11.1, cli 2.11.3, plugin-* aligned to the same 2.x.
- gen/schemas regenerated for the new plugin ACL permissions.
JS: raised all caret ranges to highest-within-major (radix-ui, tanstack,
framer-motion 12.41, zod 3.25, vite 6.4.3, vitest 4.1.9, typescript 5.9.3,
typescript-eslint 8.62, eslint 9.39, @types/node 20.19.43, ...). pnpm
install + dedupe; package-lock.json regenerated to match (npm-ci CI path).
React pin preserved: react & react-dom stay exactly 19.0.6 (kept equal to
avoid the React reconciler mismatch / blank-app failure mode).
pnpm.overrides added (pnpm ignored the npm-style top-level `overrides`):
- @radix-ui/react-primitive pinned 2.1.4 — 2.1.5+ adds a global
`declare module 'react'` CSSProperties augmentation (`--radix-${string}`)
that breaks rjsf StyleHTMLAttributes typing; the pin keeps every radix
component bump while dropping just the breaking transitive augmentation.
Held at safe boundary (would increase the typecheck error count):
- react-error-boundary held at ~6.0.3 (6.1.x retypes FallbackProps.error
Error -> unknown, breaking the app's typed fallbacks).
Verification (baseline = origin/main):
- frontend `tsc -b`: 34 -> 33 errors (0 new; 1 pre-existing fixed by
@types/node bump). Constraint "must not increase" satisfied.
- `cargo check` (src-tauri): 0 errors / 71 warnings, unchanged from baseline.
* test(e2e): real Tauri E2E harness (tauri-driver + WebdriverIO), decomplected from frontend smoke
The existing e2e-tests/*.spec.ts (Playwright `page.goto(:1420)`) drive the Vite
frontend in Chromium with NO Tauri IPC. That's frontend smoke, not E2E. This
adds the canonical Tauri 2.x E2E: tauri-driver launches the real hanzo-desktop
binary and WebdriverIO drives the native WebKit webview over WebDriver — real
window, real `window.__TAURI_INTERNALS__`, real commands.
Decomplected — one script per intent, no duplication:
- `pnpm test:e2e` -> wdio, real native app (apps/hanzo-desktop/e2e/)
- `pnpm test:smoke` -> playwright frontend smoke (apps/hanzo-desktop/e2e-tests/)
Playwright config + e2e-tests/README relabelled "frontend smoke — NOT Tauri E2E".
Harness (DRY):
- wdio.conf.ts (app root): capability is `tauri:options.application` ->
src-tauri/target/debug/hanzo-desktop, NO browserName (tauri-driver forwards an
unknown browserName to WebKitWebDriver otherwise). Spawns/kills tauri-driver in
beforeSession/afterSession/onComplete, frees stale ports, kills the driver's
native-driver child via its process group. ESM-safe (__dirname from
import.meta.url; the repo is "type":"module"). Headless on DISPLAY=:1.
- tsconfig.e2e.json: own TS project (node + DOM + @wdio/globals) so the specs
typecheck and stay out of the app's tsconfig.app/spec.
- e2e/helpers.ts: shared waitForAppMounted / navigateTo (client-side history,
the app uses BrowserRouter) / restoreSignedInSession (seeds the REAL persisted
zustand stores for a returning cloud user — unlocks ProtectedRoutes with no
IPC/behaviour mocks).
- Specs (minimal but real, no mocks): app-load (window + #root mount +
__TAURI_INTERNALS__ + onboarding), login (Login button gates on terms
checkbox), models (local "Zen Engine" surface + chat composer), chat (real
composer round-trip), mining (mining-page, on-chain/pending balances,
4-tier privacy selector, recent-proofs).
- e2e/README.md: how to run (needs webkit2gtk-driver + tauri-driver + a debug
build), macOS-unsupported caveat.
Deps: @wdio/{cli,local-runner,mocha-framework,spec-reporter,globals,types}
^9.29.0 (devDeps, within-major). pnpm + package-lock both updated.
Verified: config + specs typecheck (tsc -p tsconfig.e2e.json = 0 errors); app
typecheck unchanged at 33 (no e2e leakage); eslint ignores the e2e suites (their
own tsconfigs) — also clears the pre-existing e2e-tests parse error; tauri-driver
+ WebKitWebDriver present, debug binary builds & launches, wdio reaches session
creation. NOT faked green: on this box's bleeding-edge WebKitGTK 2.52.3 the
tauri-driver automation handshake hangs (IncompleteMessage / HEADERS_TIMEOUT) —
an upstream WebKitGTK>=2.46 issue, documented in e2e/README.md with mitigations.
187 lines
6.8 KiB
TypeScript
187 lines
6.8 KiB
TypeScript
// Real Tauri end-to-end harness — drives the BUILT native app over WebDriver.
|
|
//
|
|
// This is the canonical Tauri 2.x E2E setup: `tauri-driver` (a thin WebDriver
|
|
// proxy) launches the real `hanzo-desktop` binary and bridges to the platform
|
|
// webview driver. On Linux that driver is `WebKitWebDriver` (apt package
|
|
// `webkit2gtk-driver`); macOS has no equivalent driver, so this suite is
|
|
// Linux/Windows only (see e2e/README.md).
|
|
//
|
|
// Contrast with e2e-tests/ (Playwright): those hit the Vite dev frontend in
|
|
// Chromium with NO Tauri IPC — pure frontend smoke. THIS suite is the real
|
|
// thing: native window, real `window.__TAURI_INTERNALS__`, real commands.
|
|
import { spawn, spawnSync, type ChildProcess } from 'node:child_process';
|
|
import { existsSync } from 'node:fs';
|
|
import path from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
import type { Options } from '@wdio/types';
|
|
|
|
// The repo is ESM ("type": "module"), so this config is loaded as ESM — derive
|
|
// __dirname from import.meta.url instead of the CJS global.
|
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
|
|
// WDIO v9 auto-compiles this config + the specs with ts-node when present; point
|
|
// ts-node at the e2e tsconfig (node + DOM + wdio globals, transpile-only).
|
|
process.env.TS_NODE_PROJECT = path.resolve(__dirname, 'tsconfig.e2e.json');
|
|
process.env.TS_NODE_TRANSPILE_ONLY = 'true';
|
|
|
|
// The built debug binary tauri-driver launches. `cargo build` /
|
|
// `tauri build --debug` (run from src-tauri / the app) produce it here. The
|
|
// binary name is the Cargo package name (`hanzo-desktop`).
|
|
const APP_BINARY = path.resolve(
|
|
__dirname,
|
|
'src-tauri',
|
|
'target',
|
|
'debug',
|
|
process.platform === 'win32' ? 'hanzo-desktop.exe' : 'hanzo-desktop',
|
|
);
|
|
|
|
// `tauri-driver` resolved from PATH (cargo install tauri-driver) unless the
|
|
// caller overrides it. It speaks WebDriver on `tauriDriverPort` and spawns the
|
|
// native webview driver on `nativePort`.
|
|
const TAURI_DRIVER = process.env.TAURI_DRIVER_PATH ?? 'tauri-driver';
|
|
|
|
// tauri-driver resolves the native driver itself only by absolute path, so
|
|
// resolve the bare name on PATH up-front (env may not carry the same PATH into
|
|
// the spawned proxy).
|
|
function resolveOnPath(bin: string): string {
|
|
const r = spawnSync(process.platform === 'win32' ? 'where' : 'which', [bin], {
|
|
encoding: 'utf8',
|
|
});
|
|
const found = r.stdout?.split(/\r?\n/).find((l) => l.trim().length > 0);
|
|
return found?.trim() ?? bin;
|
|
}
|
|
const NATIVE_DRIVER =
|
|
process.env.TAURI_WEBDRIVER_PATH ??
|
|
resolveOnPath(process.platform === 'win32' ? 'msedgedriver' : 'WebKitWebDriver');
|
|
|
|
const TAURI_DRIVER_PORT = Number(process.env.TAURI_DRIVER_PORT ?? 4444);
|
|
const NATIVE_DRIVER_PORT = Number(process.env.TAURI_NATIVE_PORT ?? 4445);
|
|
|
|
let tauriDriver: ChildProcess | undefined;
|
|
|
|
export const config: WebdriverIO.Config = {
|
|
runner: 'local',
|
|
|
|
// tauri-driver is the WebDriver host; we manage its lifecycle in the hooks
|
|
// below rather than as a managed service (Tauri ships no wdio service).
|
|
hostname: '127.0.0.1',
|
|
port: TAURI_DRIVER_PORT,
|
|
path: '/',
|
|
|
|
specs: [path.resolve(__dirname, 'e2e', '**', '*.e2e.ts')],
|
|
maxInstances: 1, // one native window at a time
|
|
|
|
capabilities: [
|
|
{
|
|
// NO `browserName`: tauri-driver keys solely on `tauri:options` and
|
|
// would otherwise forward an unknown browserName to WebKitWebDriver
|
|
// ("Failed to match capabilities"). `maxInstances: 1` — one app at a time.
|
|
maxInstances: 1,
|
|
'tauri:options': {
|
|
application: APP_BINARY,
|
|
},
|
|
} as WebdriverIO.Capabilities,
|
|
],
|
|
|
|
logLevel: (process.env.WDIO_LOG_LEVEL as Options.Testrunner['logLevel']) ?? 'warn',
|
|
bail: 0,
|
|
waitforTimeout: 15000,
|
|
connectionRetryTimeout: 120000,
|
|
connectionRetryCount: 3,
|
|
|
|
framework: 'mocha',
|
|
reporters: ['spec'],
|
|
mochaOpts: {
|
|
ui: 'bdd',
|
|
timeout: 120000, // a cold native launch + first paint is slow
|
|
},
|
|
|
|
// --- tauri-driver lifecycle ------------------------------------------------
|
|
onPrepare: () => {
|
|
if (!existsSync(APP_BINARY)) {
|
|
throw new Error(
|
|
`Tauri debug binary not found at ${APP_BINARY}.\n` +
|
|
`Build it first: (cd apps/hanzo-desktop && pnpm tauri build --debug)\n` +
|
|
`or: cargo build --manifest-path apps/hanzo-desktop/src-tauri/Cargo.toml`,
|
|
);
|
|
}
|
|
// Fail fast with an actionable message when the native driver is absent
|
|
// (the most common Linux gap) rather than letting tauri-driver hang.
|
|
const probe = spawnSync(NATIVE_DRIVER, ['--version'], { stdio: 'ignore' });
|
|
if (probe.error) {
|
|
throw new Error(
|
|
`Native WebDriver '${NATIVE_DRIVER}' not found on PATH.\n` +
|
|
`On Linux install it with: sudo apt-get install -y webkit2gtk-driver\n` +
|
|
`(provides WebKitWebDriver). See apps/hanzo-desktop/e2e/README.md.`,
|
|
);
|
|
}
|
|
},
|
|
|
|
beforeSession: () =>
|
|
new Promise<void>((resolve, reject) => {
|
|
// Free the proxy + native ports in case a previous run leaked a driver
|
|
// (a failed session can leave tauri-driver's WebKitWebDriver child bound).
|
|
killByPort(TAURI_DRIVER_PORT);
|
|
killByPort(NATIVE_DRIVER_PORT);
|
|
|
|
// `detached` so we can signal the whole process group on cleanup —
|
|
// tauri-driver spawns the native driver as a child that must die too.
|
|
tauriDriver = spawn(
|
|
TAURI_DRIVER,
|
|
[
|
|
'--port',
|
|
String(TAURI_DRIVER_PORT),
|
|
'--native-port',
|
|
String(NATIVE_DRIVER_PORT),
|
|
'--native-driver',
|
|
NATIVE_DRIVER,
|
|
],
|
|
{ stdio: [null, 'pipe', 'pipe'], detached: true },
|
|
);
|
|
tauriDriver.on('error', (error) => reject(error));
|
|
tauriDriver.stderr?.on('data', (d: Buffer) =>
|
|
process.stderr.write(`[tauri-driver] ${d}`),
|
|
);
|
|
// Give tauri-driver a moment to bind its port before the session opens.
|
|
setTimeout(resolve, 2000);
|
|
}),
|
|
|
|
afterSession: () => {
|
|
stopTauriDriver();
|
|
},
|
|
|
|
// Belt-and-braces: guarantee the driver (and its native child) are gone even
|
|
// if a session never opened, so ports don't leak between runs.
|
|
onComplete: () => {
|
|
stopTauriDriver();
|
|
killByPort(TAURI_DRIVER_PORT);
|
|
killByPort(NATIVE_DRIVER_PORT);
|
|
},
|
|
};
|
|
|
|
function stopTauriDriver(): void {
|
|
if (!tauriDriver?.pid) return;
|
|
try {
|
|
// Negative pid = kill the whole detached process group (driver + child).
|
|
process.kill(-tauriDriver.pid, 'SIGTERM');
|
|
} catch {
|
|
tauriDriver.kill('SIGTERM');
|
|
}
|
|
tauriDriver = undefined;
|
|
}
|
|
|
|
/** Best-effort: free a TCP port held by a stale driver (Linux/macOS). */
|
|
function killByPort(port: number): void {
|
|
const r = spawnSync('sh', ['-c', `lsof -ti tcp:${port} 2>/dev/null || true`], {
|
|
encoding: 'utf8',
|
|
});
|
|
for (const pid of (r.stdout ?? '').split(/\s+/).filter(Boolean)) {
|
|
try {
|
|
process.kill(Number(pid), 'SIGTERM');
|
|
} catch {
|
|
/* already gone */
|
|
}
|
|
}
|
|
}
|