Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 2 additions & 10 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -86,17 +86,9 @@ jobs:
xvfb

- name: Install Tauri WebDriver
if: runner.os != 'macOS'
if: runner.os == 'Linux'
run: cargo install tauri-driver --locked

- name: Install Microsoft Edge WebDriver
if: runner.os == 'Windows'
shell: pwsh
run: |
cargo install --git https://github.com/chippers/msedgedriver-tool
msedgedriver-tool
$PWD.Path | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append

- name: Install backend (with packaging extras)
run: |
python -m pip install --upgrade pip
Expand Down Expand Up @@ -163,7 +155,7 @@ jobs:
DESKTOP_APP_PATH: ${{ steps.desktop-install.outputs.app_path }}
ZVEC_PORT: "17862"
ZVEC_STUDIO_DATA_DIR: ${{ github.workspace }}/artifacts/desktop-smoke/wdio-data
run: pnpm --filter desktop e2e:installed
run: pnpm --filter desktop e2e:installed:windows

- name: Upload desktop smoke diagnostics
if: failure() || always()
Expand Down
1 change: 1 addition & 0 deletions apps/desktop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"tauri:dev": "tauri dev",
"tauri:build": "tauri build && bash scripts/post-build-sign.sh",
"e2e:installed": "wdio run wdio.installed.conf.mjs",
"e2e:installed:windows": "node tests/installed-app.windows-smoke.mjs",
"lint": "echo '(desktop shell uses cargo for lint -- run `make desktop.check`)' && exit 0",
"typecheck": "echo '(desktop shell typecheck happens via cargo check)' && exit 0",
"test": "echo '(desktop tests run via cargo test -- see `make desktop.test`)' && exit 0",
Expand Down
24 changes: 23 additions & 1 deletion apps/desktop/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,14 +90,36 @@ pub fn run() {
config: config.clone(),
};

let context = tauri::generate_context!();

#[cfg(target_os = "windows")]
let context = {
let mut context = context;
if let Ok(additional_args) = env::var("WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS") {
let additional_args = additional_args.trim();
if !additional_args.is_empty() {
for window in &mut context.config_mut().app.windows {
window.additional_browser_args =
Some(match window.additional_browser_args.take() {
Some(configured_args) => format!("{configured_args} {additional_args}"),
None => format!(
"--disable-features=msWebOOUI,msPdfOOUI,msSmartScreenProtection {additional_args}"
),
});
}
}
}
context
};

tauri::Builder::default()
.plugin(tauri_plugin_dialog::init())
.manage(app_state)
.invoke_handler(tauri::generate_handler![
commands::sidecar_status,
commands::sidecar_url,
])
.build(tauri::generate_context!())
.build(context)
.expect("failed to build the Tauri application")
.run(|app_handle, event| {
let should_shutdown = matches!(
Expand Down
171 changes: 171 additions & 0 deletions apps/desktop/tests/installed-app.windows-smoke.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
import assert from 'node:assert/strict';
import fs from 'node:fs';
import { createRequire } from 'node:module';
import os from 'node:os';
import path from 'node:path';
import { spawn, spawnSync } from 'node:child_process';
import { fileURLToPath } from 'node:url';

const currentDir = path.dirname(fileURLToPath(import.meta.url));
const repoRoot = path.resolve(currentDir, '../../..');
const frontendRequire = createRequire(path.join(repoRoot, 'apps', 'frontend', 'package.json'));
const { chromium, expect } = frontendRequire('@playwright/test');
const artifactsDir = path.join(repoRoot, 'artifacts', 'desktop-smoke');
const appPath = process.env.DESKTOP_APP_PATH;
const tauriConfig = JSON.parse(
fs.readFileSync(path.join(repoRoot, 'apps', 'desktop', 'src-tauri', 'tauri.conf.json'), 'utf8'),
);
const webviewUserDataDir = process.env.LOCALAPPDATA
? path.win32.join(process.env.LOCALAPPDATA, tauriConfig.identifier, 'EBWebView')
: undefined;
const devToolsActivePortPath = webviewUserDataDir
? path.win32.join(webviewUserDataDir, 'DevToolsActivePort')
: undefined;
let appProcess;
let browser;

function waitForCdpEndpoint(timeoutMs) {
const deadline = Date.now() + timeoutMs;
return new Promise((resolve, reject) => {
function attempt() {
try {
const [portLine] = fs.readFileSync(devToolsActivePortPath, 'utf8').split(/\r?\n/);
const port = Number(portLine);
if (Number.isInteger(port) && port > 0) {
resolve(`http://127.0.0.1:${port}`);
return;
}
} catch (error) {
if (error?.code !== 'ENOENT') {
reject(error);
return;
}
}
if (Date.now() >= deadline) {
reject(new Error(`Timed out waiting for ${devToolsActivePortPath}`));
return;
}
setTimeout(attempt, 250);
}
attempt();
});
}

function stopApp() {
if (!appProcess || appProcess.exitCode !== null) {
return;
}

if (process.platform === 'win32') {
spawnSync('taskkill', ['/PID', String(appProcess.pid), '/T', '/F'], {
stdio: 'ignore',
});
} else {
appProcess.kill('SIGTERM');
}
}

async function waitForPage() {
const deadline = Date.now() + 60000;
while (Date.now() < deadline) {
const page = browser
.contexts()
.flatMap((context) => context.pages())
.find((candidate) => candidate.url() !== 'about:blank');
if (page) {
return page;
}
await new Promise((resolve) => setTimeout(resolve, 250));
}
throw new Error('Timed out waiting for the Tauri WebView2 page');
}

async function tauriInvoke(page, command) {
return page.evaluate(async (cmd) => {
const invoke = window.__TAURI_INTERNALS__?.invoke ?? window.__TAURI__?.invoke;
if (!invoke) return null;
return invoke(cmd);
}, command);
}

async function main() {
if (process.platform !== 'win32') {
throw new Error('The WebView2 CDP smoke test only supports Windows');
}
if (!appPath || !fs.existsSync(appPath)) {
throw new Error(`DESKTOP_APP_PATH does not exist: ${appPath}`);
}
if (!devToolsActivePortPath) {
throw new Error('LOCALAPPDATA is required for the Windows WebView2 smoke test');
}

fs.mkdirSync(artifactsDir, { recursive: true });
fs.rmSync(devToolsActivePortPath, { force: true });
const appLog = fs.openSync(path.join(artifactsDir, 'windows-app.log'), 'a');
const env = {
...process.env,
ZVEC_HOST: process.env.ZVEC_HOST ?? '127.0.0.1',
ZVEC_PORT: process.env.ZVEC_PORT ?? '17862',
ZVEC_READY_TIMEOUT_SECS: process.env.ZVEC_READY_TIMEOUT_SECS ?? '60',
ZVEC_STUDIO_DATA_DIR:
process.env.ZVEC_STUDIO_DATA_DIR ?? path.join(artifactsDir, 'windows-data'),
WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS: '--remote-debugging-port=0',
};

appProcess = spawn(appPath, [], {
env,
stdio: ['ignore', appLog, appLog],
});
appProcess.once('exit', (code, signal) => {
if (code && code !== 0) {
console.error(`installed app exited with code ${code}`);
}
if (signal) {
console.error(`installed app exited via signal ${signal}`);
}
});

const cdpUrl = await waitForCdpEndpoint(60000);
browser = await chromium.connectOverCDP(cdpUrl);
const page = await waitForPage();
await expect(page.getByTestId('app-shell')).toBeVisible({ timeout: 60000 });

await expect
.poll(async () => (await tauriInvoke(page, 'sidecar_status'))?.running, {
timeout: 60000,
message: 'sidecar_status did not report a running sidecar',
})
.toBe(true);
const status = await tauriInvoke(page, 'sidecar_status');
assert.equal(status.running, true);
assert.match(status.base_url, /^http:\/\/127\.0\.0\.1:\d+$/);

const suffix = Date.now().toString(36);
const collectionName = `uitest${suffix}`;
const collectionPath = path.join(os.tmpdir(), `zvec-${collectionName}`);

await page.getByTestId('zv-welcome-create').click();
await page.getByTestId('zv-create-name').fill(collectionName);
await page.getByTestId('zv-create-path').fill(collectionPath);
await page.getByTestId('zv-create-submit').click();

await expect(page.getByTestId('zv-detail-tab-overview')).toBeVisible({
timeout: 60000,
});
assert.equal(new URL(page.url()).pathname, `/collections/${collectionName}`);

for (const tabName of ['overview', 'browse', 'query', 'write']) {
const tab = page.getByTestId(`zv-detail-tab-${tabName}`);
await tab.click();
await expect(tab).toHaveClass(/zv-detail-tab--active/, { timeout: 30000 });
}

console.log('installed Windows desktop UI smoke: ok');
}

try {
await main();
} finally {
await browser?.close().catch(() => {});
stopApp();
}
7 changes: 6 additions & 1 deletion scripts/desktop_api_smoke.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,12 @@ def _terminate(proc: subprocess.Popen[bytes]) -> None:
if proc.poll() is not None:
return
if os.name == "nt":
proc.terminate()
subprocess.run(
["taskkill", "/PID", str(proc.pid), "/T", "/F"],
check=False,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
else:
proc.send_signal(signal.SIGTERM)
try:
Expand Down
Loading