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
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
11 changes: 8 additions & 3 deletions src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]


6 changes: 4 additions & 2 deletions src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1427,8 +1427,10 @@ pub fn run_with_file(open_file: Option<String>) {
.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
Expand Down
24 changes: 13 additions & 11 deletions src/e2e/fixtures/generate-e2e-fixtures.mjs
Original file line number Diff line number Diff line change
@@ -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) {
Expand All @@ -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}`);
35 changes: 34 additions & 1 deletion src/e2e/wdio.conf.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
Loading