From 86546eb533435875d23c5c4f54a12ec03db1b1d8 Mon Sep 17 00:00:00 2001 From: Shy Hunter Date: Wed, 29 Apr 2026 05:26:57 +0200 Subject: [PATCH] fix(e2e): explicit Cargo feature + stage fixtures into Tauri $TEMP scope MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two adjacent fixes that together unblock `npm run test:e2e` on macOS: 1. Replace `[target.'cfg(debug_assertions)'.dependencies]` (which Cargo evaluates against target spec triples, not rustc's debug_assertions flag, producing an unexpected-cfg warning and non-deterministic plugin inclusion) with an opt-in `e2e` Cargo feature. The tauri-plugin-webdriver-automation plugin now compiles in only when built with --features e2e. Release builds are unaffected. 2. Stage e2e fixtures into ${os.tmpdir()}/papercut-e2e/{real,error,output}/ so they fall inside Tauri's $TEMP fs capability scope. Without this, every test fails at handleFileSelected → getFileSizeBytes → scope denied → corrupt-file branch, and the step bar never advances past 0. Real fixtures are mirrored at the top of wdio.conf.ts; generated error-path fixtures (large-sparse, corrupt) go to the same staging area via generate-e2e-fixtures.mjs. After this change, the success-case PDF compression tests (PDF-Q-WEB / SCREEN / PRINT) all pass. A separate upstream issue in tauri-plugin-webdriver-automation 0.1.3 (lock poisoned: PoisonError on cross-test transitions) prevents the rest of the suite from completing and is tracked upstream. New build command: `npm run e2e:build` (alias for `tauri build --debug --features e2e --bundles app`). Co-Authored-By: Claude Opus 4.7 --- package.json | 1 + src-tauri/Cargo.toml | 11 +++++-- src-tauri/src/lib.rs | 6 ++-- src/e2e/fixtures/generate-e2e-fixtures.mjs | 24 ++++++++------- src/e2e/wdio.conf.ts | 35 +++++++++++++++++++++- 5 files changed, 60 insertions(+), 17 deletions(-) diff --git a/package.json b/package.json index ced2dcf..4bbfdd9 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "test": "vitest run", "test:watch": "vitest", "test:coverage": "vitest run --coverage", + "e2e:build": "tauri build --debug --features e2e --bundles app", "pretest:e2e": "node src/e2e/fixtures/generate-e2e-fixtures.mjs", "test:e2e": "wdio run src/e2e/wdio.conf.ts", "test:e2e:pdf": "wdio run src/e2e/wdio.conf.ts --suite pdf", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index d281e1e..25af0e8 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -31,8 +31,13 @@ image = "0.25" webp = "0.3" uuid = { version = "1", features = ["v4"] } -# E2E testing — debug builds only (never ships to end users) -[target.'cfg(debug_assertions)'.dependencies] -tauri-plugin-webdriver-automation = "0.1" +# E2E automation — opt-in via `--features e2e`. Never enabled in release builds. +# Previously declared under `[target.'cfg(debug_assertions)'.dependencies]`, which +# Cargo evaluates against target spec triples (not the rustc debug_assertions +# flag), so inclusion was non-deterministic and produced an unexpected-cfg warning. +tauri-plugin-webdriver-automation = { version = "0.1", optional = true } + +[features] +e2e = ["dep:tauri-plugin-webdriver-automation"] diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 053dcd3..70ebccc 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1427,8 +1427,10 @@ pub fn run_with_file(open_file: Option) { .plugin(tauri_plugin_opener::init()) .invoke_handler(tauri::generate_handler![greet, process_image, rotate_image, compress_pdf, cancel_processing, protect_pdf, unlock_pdf, convert_pdfa, repair_pdf, convert_with_libreoffice, convert_with_calibre, convert_with_textutil, convert_with_word, detect_converters, reveal_in_finder]); - // E2E automation plugin — debug builds only, never ships in release - #[cfg(debug_assertions)] + // E2E automation plugin — gated behind the `e2e` Cargo feature so it is + // deterministically included only when explicitly requested (e.g. + // `tauri build --debug --features e2e`). Never compiled into release. + #[cfg(feature = "e2e")] let builder = builder.plugin(tauri_plugin_webdriver_automation::init()); builder diff --git a/src/e2e/fixtures/generate-e2e-fixtures.mjs b/src/e2e/fixtures/generate-e2e-fixtures.mjs index a04a15a..3454119 100644 --- a/src/e2e/fixtures/generate-e2e-fixtures.mjs +++ b/src/e2e/fixtures/generate-e2e-fixtures.mjs @@ -1,25 +1,27 @@ #!/usr/bin/env node /** - * Generate E2E error-path fixtures. + * Generate E2E error-path fixtures into Tauri's $TEMP fs scope. * Run automatically via `npm run pretest:e2e` before every test run. * - * Creates in test-fixtures-e2e/: + * Creates in ${os.tmpdir()}/papercut-e2e/error/: * large-sparse.pdf -- 110 MB sparse file (triggers >100 MB guard) * large-sparse.jpg -- 110 MB sparse file (image variant) * corrupt.pdf -- 0 bytes (triggers empty-file error) * corrupt.jpg -- 0 bytes (triggers empty-image error) * - * Real PDF/image inputs are reused from test-fixtures/ -- no need to regenerate them. + * Why $TEMP: Tauri's fs capability scope (capabilities/default.json) only + * permits reads from $DOCUMENT, $DOWNLOAD, $DESKTOP, $TEMP. Fixtures placed + * outside those paths are denied by the runtime, causing handleFileSelected + * to fall into the corrupt-file branch and never advance past step 0. * - * Note: test-fixtures-e2e/large-sparse.* are in .gitignore (too large to commit). - * test-fixtures-e2e/corrupt.* ARE committed as zero-byte stubs. + * Real PDF/image inputs are staged from project test-fixtures/ → ${stage}/real/ + * by the wdio config's top-level setup (see src/e2e/wdio.conf.ts). */ import { writeFileSync, mkdirSync, openSync, ftruncateSync, closeSync } from 'fs'; -import { join, dirname } from 'path'; -import { fileURLToPath } from 'url'; +import { join } from 'path'; +import { tmpdir } from 'os'; -const __dirname = dirname(fileURLToPath(import.meta.url)); -const outDir = join(__dirname, '../../../test-fixtures-e2e'); +const outDir = join(tmpdir(), 'papercut-e2e', 'error'); mkdirSync(outDir, { recursive: true }); function createSparseFile(filePath, sizeBytes) { @@ -33,8 +35,8 @@ const MB_110 = 110 * 1024 * 1024; createSparseFile(join(outDir, 'large-sparse.pdf'), MB_110); createSparseFile(join(outDir, 'large-sparse.jpg'), MB_110); -// Zero-byte corrupt stubs (already committed; recreated here for idempotency) +// Zero-byte corrupt stubs writeFileSync(join(outDir, 'corrupt.pdf'), Buffer.alloc(0)); writeFileSync(join(outDir, 'corrupt.jpg'), Buffer.alloc(0)); -console.log('E2E error-path fixtures ready in test-fixtures-e2e/'); +console.log(`E2E error-path fixtures ready in ${outDir}`); diff --git a/src/e2e/wdio.conf.ts b/src/e2e/wdio.conf.ts index 324fd0a..a677fbd 100644 --- a/src/e2e/wdio.conf.ts +++ b/src/e2e/wdio.conf.ts @@ -2,11 +2,44 @@ import { join, dirname } from 'path'; import { fileURLToPath } from 'url'; import { spawn, spawnSync, type ChildProcess } from 'child_process'; import { createConnection } from 'net'; +import { mkdirSync, copyFileSync, readdirSync, statSync } from 'fs'; +import { tmpdir } from 'os'; const __dirname = dirname(fileURLToPath(import.meta.url)); +// Stage fixtures into Tauri's $TEMP fs scope before specs are imported. +// +// Tauri's capability scope (src-tauri/capabilities/default.json) only permits +// reads from $DOCUMENT/$DOWNLOAD/$DESKTOP/$TEMP. The project's test-fixtures/ +// directory is outside every allowed scope, so reading fixtures directly from +// there fails the runtime fs check and the UI never advances past step 0. +// +// We mirror real fixtures into ${tmpdir}/papercut-e2e/real/ here, and the +// pretest:e2e script generates error-path fixtures into ${tmpdir}/papercut-e2e/ +// error/. The driver helper picks both up via E2E_*_DIR env vars set below. +const STAGE_DIR = join(tmpdir(), 'papercut-e2e'); +const STAGE_REAL = join(STAGE_DIR, 'real'); +const STAGE_ERROR = join(STAGE_DIR, 'error'); +const STAGE_OUTPUT = join(STAGE_DIR, 'output'); +mkdirSync(STAGE_REAL, { recursive: true }); +mkdirSync(STAGE_ERROR, { recursive: true }); +mkdirSync(STAGE_OUTPUT, { recursive: true }); + +const PROJECT_REAL_FIXTURES = join(__dirname, '../../test-fixtures'); +for (const entry of readdirSync(PROJECT_REAL_FIXTURES)) { + const src = join(PROJECT_REAL_FIXTURES, entry); + if (statSync(src).isFile()) { + copyFileSync(src, join(STAGE_REAL, entry)); + } +} + +process.env.E2E_REAL_FIXTURES_DIR ??= STAGE_REAL; +process.env.E2E_FIXTURES_DIR ??= STAGE_ERROR; +process.env.E2E_OUTPUT_DIR ??= STAGE_OUTPUT; + // Resolve the Tauri binary path for the current platform. -// E2E tests run against a debug build so tauri-plugin-webdriver-automation is compiled in. +// E2E tests run against a debug build with `--features e2e` so the +// tauri-plugin-webdriver-automation plugin is registered. function getTauriBinaryPath(): string { if (process.platform === 'darwin') { return join(__dirname, '../../src-tauri/target/debug/bundle/macos/Papercut.app/Contents/MacOS/tauri-app');