mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2026-09-03 05:10:16 +03:00
Add Tauri desktop testing to CI
This commit is contained in:
@@ -94,11 +94,14 @@ tauri: &tauri
|
||||
- *ci
|
||||
- frontend/editor/src-tauri/**
|
||||
- frontend/editor/src/desktop/**
|
||||
- frontend/desktop-e2e/**
|
||||
- frontend/editor/tsconfig.desktop.vite.json
|
||||
- frontend/package.json
|
||||
- frontend/package-lock.json
|
||||
- frontend/editor/vite.config.ts
|
||||
- .github/workflows/tauri-build.yml
|
||||
- .github/workflows/desktop-rust.yml
|
||||
- .github/workflows/desktop-e2e.yml
|
||||
- Taskfile.yml
|
||||
- .taskfiles/desktop.yml
|
||||
|
||||
|
||||
@@ -201,6 +201,30 @@ jobs:
|
||||
docker-base-changed: ${{ needs.files-changed.outputs.docker-base }}
|
||||
dockerfiles-changed: ${{ needs.files-changed.outputs.dockerfiles }}
|
||||
|
||||
# Fast Rust gate: clippy + cargo test on all three desktop OSes in a couple
|
||||
# of minutes, because it skips the jlink/bootJar prep that tauri-build needs.
|
||||
desktop-rust:
|
||||
if: needs.files-changed.outputs.tauri == 'true'
|
||||
needs: [files-changed]
|
||||
permissions:
|
||||
contents: read
|
||||
uses: ./.github/workflows/desktop-rust.yml
|
||||
secrets: inherit
|
||||
|
||||
# Runs the real app: window opens, frontend mounts, bundled JRE boots the
|
||||
# bundled JAR. Linux only on PRs (cheapest runner, and the platform where the
|
||||
# webview and jlink regressions have actually bitten); nightly covers Windows
|
||||
# too via its own dispatch.
|
||||
desktop-e2e:
|
||||
if: needs.files-changed.outputs.tauri == 'true'
|
||||
needs: [files-changed, gradle-cache-prime]
|
||||
permissions:
|
||||
contents: read
|
||||
uses: ./.github/workflows/desktop-e2e.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
platform: linux
|
||||
|
||||
tauri-build:
|
||||
if: needs.files-changed.outputs.tauri == 'true'
|
||||
needs: [files-changed]
|
||||
@@ -296,6 +320,8 @@ jobs:
|
||||
- check-licence
|
||||
- docker-compose-tests
|
||||
- test-build-docker-images
|
||||
- desktop-rust
|
||||
- desktop-e2e
|
||||
- tauri-build
|
||||
- ai-engine
|
||||
- generated-models
|
||||
@@ -322,6 +348,8 @@ jobs:
|
||||
check-licence=${{ needs.check-licence.result }}
|
||||
docker-compose-tests=${{ needs.docker-compose-tests.result }}
|
||||
test-build-docker-images=${{ needs.test-build-docker-images.result }}
|
||||
desktop-rust=${{ needs.desktop-rust.result }}
|
||||
desktop-e2e=${{ needs.desktop-e2e.result }}
|
||||
tauri-build=${{ needs.tauri-build.result }}
|
||||
ai-engine=${{ needs.ai-engine.result }}
|
||||
generated-models=${{ needs.generated-models.result }}
|
||||
|
||||
@@ -0,0 +1,230 @@
|
||||
name: Desktop E2E
|
||||
|
||||
# Launches the real desktop app and exercises it. tauri-build.yml proves the
|
||||
# bundle *compiles and packages*; nothing there ever runs it. This workflow
|
||||
# covers what only shows up at runtime.
|
||||
#
|
||||
# Two suites, because tauri-driver has no macOS backend (Apple ships no
|
||||
# WebDriver for WKWebView):
|
||||
#
|
||||
# smoke - launches the binary, reads the backend port off its stdout, and
|
||||
# runs real tools over HTTP. No webview driver, so it runs on all
|
||||
# three platforms. This is where per-platform runtime bugs live:
|
||||
# a jlink runtime missing a module, a JRE older than the JAR, or
|
||||
# JPDFium natives not published for this OS/arch.
|
||||
# ui - drives the real webview through tauri-driver: window opens, the
|
||||
# frontend mounts, and a tool runs end to end from the UI.
|
||||
# Linux + Windows only.
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
platform:
|
||||
description: "Platform to test (linux, windows, macos, or all)."
|
||||
required: false
|
||||
type: string
|
||||
default: "linux"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
platform:
|
||||
description: "Platform to test (linux, windows, macos, or all)"
|
||||
required: true
|
||||
default: "all"
|
||||
type: choice
|
||||
options:
|
||||
- all
|
||||
- linux
|
||||
- windows
|
||||
- macos
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
determine-matrix:
|
||||
if: ${{ vars.CI_PROFILE != 'lite' }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 3
|
||||
outputs:
|
||||
matrix: ${{ steps.set-matrix.outputs.matrix }}
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Determine test matrix
|
||||
id: set-matrix
|
||||
env:
|
||||
PLATFORM: ${{ inputs.platform }}
|
||||
run: |
|
||||
# `webdriver` gates the UI suite: macOS runs the headless smoke only,
|
||||
# because tauri-driver cannot drive WKWebView. macos-15 runners are
|
||||
# Apple silicon, hence darwin-arm64 natives.
|
||||
LINUX='{"os":"ubuntu-22.04","name":"linux","jpdfium_platforms":"linux-x64","webdriver":true}'
|
||||
WINDOWS='{"os":"windows-latest","name":"windows","jpdfium_platforms":"windows-x64","webdriver":true}'
|
||||
MACOS='{"os":"macos-15","name":"macos","jpdfium_platforms":"darwin-arm64","webdriver":false}'
|
||||
|
||||
case "$PLATFORM" in
|
||||
windows) ENTRIES=("$WINDOWS") ;;
|
||||
macos) ENTRIES=("$MACOS") ;;
|
||||
all) ENTRIES=("$LINUX" "$WINDOWS" "$MACOS") ;;
|
||||
*) ENTRIES=("$LINUX") ;;
|
||||
esac
|
||||
|
||||
JOINED=$(IFS=','; echo "${ENTRIES[*]}")
|
||||
echo "matrix={\"include\":[$JOINED]}" >> $GITHUB_OUTPUT
|
||||
|
||||
e2e:
|
||||
name: desktop-e2e (${{ matrix.name }})
|
||||
needs: determine-matrix
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix: ${{ fromJson(needs.determine-matrix.outputs.matrix) }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 75
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
# webkit2gtk-driver supplies WebKitWebDriver, which tauri-driver proxies
|
||||
# to; xvfb gives the app a display to open its window on.
|
||||
- name: Install Linux dependencies
|
||||
if: matrix.name == 'linux'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf libjavascriptcoregtk-4.0-dev libsoup2.4-dev libjavascriptcoregtk-4.1-dev libsoup-3.0-dev webkit2gtk-driver xvfb
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version: 22
|
||||
cache: "npm"
|
||||
cache-dependency-path: frontend/package-lock.json
|
||||
|
||||
- name: Setup Rust
|
||||
uses: dtolnay/rust-toolchain@4be9e76fd7c4901c61fb841f559994984270fce7 # stable
|
||||
with:
|
||||
toolchain: stable
|
||||
|
||||
- name: Cache Rust build
|
||||
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
|
||||
with:
|
||||
workspaces: frontend/editor/src-tauri
|
||||
# Shares tauri-build's cache: both build the app crate with real
|
||||
# resources, so the dependency crates are byte-identical. macOS has no
|
||||
# shared entry - tauri-build's macOS leg targets universal-apple-darwin
|
||||
# while this one builds host-native - so it gets its own key.
|
||||
shared-key: ${{ matrix.name == 'linux' && 'tauri-linux-x86_64' || matrix.name == 'windows' && 'tauri-windows-x86_64' || 'desktop-e2e-macos' }}
|
||||
save-if: ${{ matrix.name == 'macos' && github.ref == 'refs/heads/main' }}
|
||||
cache-on-failure: true
|
||||
|
||||
- name: Set up JDK 25
|
||||
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
|
||||
with:
|
||||
java-version: "25"
|
||||
distribution: "temurin"
|
||||
|
||||
- name: Cache Gradle User Home
|
||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
~/.gradle/wrapper
|
||||
key: gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }}
|
||||
restore-keys: |
|
||||
gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25-
|
||||
gradle-${{ runner.os }}-${{ runner.arch }}-
|
||||
|
||||
- name: Setup Task
|
||||
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
|
||||
|
||||
# --no-bundle: a launchable binary with the JRE and JAR staged beside it,
|
||||
# without paying for deb/rpm/AppImage/MSI packaging that tauri-build
|
||||
# already covers.
|
||||
- name: Build desktop app (no bundle)
|
||||
env:
|
||||
MAVEN_USER: ${{ secrets.MAVEN_USER }}
|
||||
MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
|
||||
MAVEN_PUBLIC_URL: ${{ secrets.MAVEN_PUBLIC_URL }}
|
||||
DISABLE_ADDITIONAL_FEATURES: true
|
||||
JPDFIUM_PLATFORMS: ${{ matrix.jpdfium_platforms }}
|
||||
VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY: ${{ secrets.VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY || 'sb_publishable_UHz2SVRF5mvdrPHWkRteyA_yNlZTkYb' }} # gitleaks:allow
|
||||
VITE_SAAS_SERVER_URL: ${{ secrets.VITE_SAAS_SERVER_URL || 'https://app.stirlingpdf.com' }}
|
||||
VITE_SAAS_BACKEND_API_URL: ${{ secrets.VITE_SAAS_BACKEND_API_URL || 'https://api.stirlingpdf.com' }}
|
||||
CI: true
|
||||
run: task desktop:build:dev
|
||||
|
||||
# Runs everywhere, including macOS. Needs no npm install - the runner is
|
||||
# plain Node - so this is the cheapest real coverage in the workflow.
|
||||
- name: Run desktop smoke (Linux)
|
||||
if: matrix.name == 'linux'
|
||||
env:
|
||||
WEBKIT_DISABLE_COMPOSITING_MODE: "1"
|
||||
run: xvfb-run --auto-servernum --server-args="-screen 0 1280x1024x24" task desktop:smoke
|
||||
|
||||
- name: Run desktop smoke (Windows / macOS)
|
||||
if: matrix.name != 'linux'
|
||||
run: task desktop:smoke
|
||||
|
||||
- name: Install E2E harness
|
||||
if: matrix.webdriver
|
||||
env:
|
||||
CI: true
|
||||
run: task desktop:e2e:install
|
||||
|
||||
# msedgedriver must match the installed WebView2/Edge major version. The
|
||||
# runner image ships a driver that usually already matches; fall back to
|
||||
# the exact build from Microsoft's CDN when it has drifted.
|
||||
- name: Resolve Edge WebDriver (Windows)
|
||||
if: matrix.name == 'windows'
|
||||
shell: pwsh
|
||||
run: |
|
||||
$edgeExe = "C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe"
|
||||
if (-not (Test-Path $edgeExe)) { Write-Host "[ERROR] Edge not found"; exit 1 }
|
||||
$edgeVersion = (Get-Item $edgeExe).VersionInfo.ProductVersion
|
||||
Write-Host "Edge version: $edgeVersion"
|
||||
|
||||
$preinstalled = "C:\SeleniumWebDrivers\EdgeDriver\msedgedriver.exe"
|
||||
if (Test-Path $preinstalled) {
|
||||
$driverVersion = (Get-Item $preinstalled).VersionInfo.ProductVersion
|
||||
Write-Host "Preinstalled driver version: $driverVersion"
|
||||
if ($driverVersion.Split('.')[0] -eq $edgeVersion.Split('.')[0]) {
|
||||
Write-Host "Using preinstalled msedgedriver"
|
||||
echo "TAURI_DRIVER_NATIVE=$preinstalled" >> $env:GITHUB_ENV
|
||||
exit 0
|
||||
}
|
||||
Write-Host "Major version mismatch - downloading a matching driver"
|
||||
}
|
||||
|
||||
$dest = Join-Path $env:RUNNER_TEMP "edgedriver"
|
||||
New-Item -ItemType Directory -Force -Path $dest | Out-Null
|
||||
$zip = Join-Path $env:RUNNER_TEMP "edgedriver.zip"
|
||||
Invoke-WebRequest -Uri "https://msedgedriver.microsoft.com/$edgeVersion/edgedriver_win64.zip" -OutFile $zip
|
||||
Expand-Archive -Path $zip -DestinationPath $dest -Force
|
||||
echo "TAURI_DRIVER_NATIVE=$dest\msedgedriver.exe" >> $env:GITHUB_ENV
|
||||
|
||||
- name: Run desktop UI E2E (Linux)
|
||||
if: matrix.name == 'linux'
|
||||
env:
|
||||
# WebKitGTK's compositing path is unreliable under Xvfb's software
|
||||
# GL; the app already disables the DMA-BUF renderer itself.
|
||||
WEBKIT_DISABLE_COMPOSITING_MODE: "1"
|
||||
run: xvfb-run --auto-servernum --server-args="-screen 0 1280x1024x24" task desktop:e2e
|
||||
|
||||
- name: Run desktop UI E2E (Windows)
|
||||
if: matrix.name == 'windows'
|
||||
run: task desktop:e2e
|
||||
|
||||
- name: Upload E2E logs
|
||||
if: always()
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: desktop-e2e-${{ matrix.name }}-${{ github.run_id }}
|
||||
path: frontend/desktop-e2e/logs/
|
||||
retention-days: 7
|
||||
if-no-files-found: ignore
|
||||
@@ -0,0 +1,82 @@
|
||||
name: Desktop Rust checks
|
||||
|
||||
# Fast Rust gate for the Tauri desktop shell: clippy + cargo test, no bundling.
|
||||
#
|
||||
# tauri-build.yml also runs the Rust tests, but only after `task desktop:prepare`
|
||||
# has built the backend JAR and jlink runtime - 20+ minutes before a broken Rust
|
||||
# change surfaces. Emptying `bundle.resources` via TAURI_CONFIG lets the crate
|
||||
# compile on its own, so this job reports in a couple of minutes instead.
|
||||
#
|
||||
# Runs on all three desktop OSes deliberately: large parts of src-tauri sit
|
||||
# behind #[cfg(target_os = ...)] (native printing, default-handler registration,
|
||||
# keyring backends) and are simply not compiled - let alone linted - anywhere
|
||||
# else.
|
||||
on:
|
||||
workflow_call:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
rust-checks:
|
||||
name: ${{ matrix.name }}
|
||||
# Matches tauri-build.yml: the lite profile skips desktop work entirely.
|
||||
if: ${{ vars.CI_PROFILE != 'lite' }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-22.04
|
||||
name: linux
|
||||
- os: windows-latest
|
||||
name: windows
|
||||
- os: macos-15
|
||||
name: macos
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 30
|
||||
env:
|
||||
# tauri-build hard-fails when a `resources` glob matches nothing, and
|
||||
# those globs point at the JAR and jlink runtime this job deliberately
|
||||
# skips building. Keep in sync with RUST_ONLY_CONFIG in .taskfiles/desktop.yml.
|
||||
TAURI_CONFIG: '{"bundle":{"resources":[]}}'
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
# webkit2gtk + friends are link-time dependencies of the tauri crate, so
|
||||
# even `cargo clippy` needs them present.
|
||||
- name: Install Linux build dependencies
|
||||
if: matrix.os == 'ubuntu-22.04'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf libjavascriptcoregtk-4.0-dev libsoup2.4-dev libjavascriptcoregtk-4.1-dev libsoup-3.0-dev
|
||||
|
||||
- name: Setup Rust
|
||||
uses: dtolnay/rust-toolchain@4be9e76fd7c4901c61fb841f559994984270fce7 # stable
|
||||
with:
|
||||
toolchain: stable
|
||||
components: clippy
|
||||
|
||||
- name: Cache Rust build
|
||||
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
|
||||
with:
|
||||
workspaces: frontend/editor/src-tauri
|
||||
# Separate from tauri-build's cache: that one is built without the
|
||||
# TAURI_CONFIG override, and mixing the two just thrashes both.
|
||||
shared-key: desktop-rust-${{ matrix.name }}
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
cache-on-failure: true
|
||||
|
||||
- name: Setup Task
|
||||
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
|
||||
|
||||
- name: Clippy
|
||||
run: task desktop:lint
|
||||
|
||||
- name: Cargo test
|
||||
run: task desktop:test:rust
|
||||
@@ -108,3 +108,16 @@ jobs:
|
||||
platform: all
|
||||
sign: false
|
||||
secrets: inherit
|
||||
|
||||
# PRs only run the Linux leg to keep desktop-touching PRs affordable. The
|
||||
# platform-specific runtime bugs - Windows WebView2, the macOS jlink runtime
|
||||
# and darwin JPDFium natives - get their coverage here instead. macOS runs the
|
||||
# headless smoke only; there is no WebDriver for WKWebView.
|
||||
desktop-e2e:
|
||||
name: Desktop E2E (linux + windows + macos)
|
||||
permissions:
|
||||
contents: read
|
||||
uses: ./.github/workflows/desktop-e2e.yml
|
||||
with:
|
||||
platform: all
|
||||
secrets: inherit
|
||||
|
||||
@@ -10,6 +10,17 @@ vars:
|
||||
# fails at launch with UnsupportedClassVersionError. Enforced by jlink:verify.
|
||||
REQUIRED_JAVA: "25"
|
||||
|
||||
# tauri.conf.json's `resources` globs point at the bundled JAR and the jlink
|
||||
# runtime, and tauri-build hard-fails when a glob matches nothing ("glob
|
||||
# pattern libs/*.jar path not found"). Emptying `resources` via TAURI_CONFIG
|
||||
# lets the Rust crate compile on its own, turning the ~20min jlink + bootJar
|
||||
# prepare into a ~2min check. Only bundling actually needs those files.
|
||||
RUST_ONLY_CONFIG: '{"bundle":{"resources":[]}}'
|
||||
|
||||
# Pinned so CI and local runs drive the app through the same WebDriver
|
||||
# bridge. Bump deliberately alongside the tauri crate.
|
||||
TAURI_DRIVER_VERSION: "2.0.6"
|
||||
|
||||
# Override via JPDFIUM_PLATFORMS env (csv of platform keys, or 'all').
|
||||
JPDFIUM_PLATFORMS:
|
||||
sh: |
|
||||
@@ -93,6 +104,89 @@ tasks:
|
||||
cmds:
|
||||
- cargo test
|
||||
|
||||
# ============================================================
|
||||
# Rust checks — no JRE/JAR needed, so they run in ~2min
|
||||
# ============================================================
|
||||
|
||||
test:rust:
|
||||
desc: "Run Tauri/Cargo tests without building the JRE/JAR (fast)"
|
||||
dir: editor/src-tauri
|
||||
env:
|
||||
TAURI_CONFIG: '{{.RUST_ONLY_CONFIG}}'
|
||||
cmds:
|
||||
- cargo test --locked {{.CLI_ARGS}}
|
||||
|
||||
lint:
|
||||
desc: "Clippy the Tauri Rust sources"
|
||||
dir: editor/src-tauri
|
||||
env:
|
||||
TAURI_CONFIG: '{{.RUST_ONLY_CONFIG}}'
|
||||
cmds:
|
||||
- cargo clippy --locked --all-targets -- -D warnings
|
||||
|
||||
check:
|
||||
desc: "Fast desktop Rust gate (clippy + tests, no bundle)"
|
||||
cmds:
|
||||
- task: lint
|
||||
- task: test:rust
|
||||
|
||||
# ============================================================
|
||||
# E2E — drives the real built app through tauri-driver
|
||||
# ============================================================
|
||||
|
||||
e2e:install:
|
||||
desc: "Install the desktop E2E harness (WebdriverIO + tauri-driver)"
|
||||
deps: [e2e:install:npm, e2e:install:driver]
|
||||
|
||||
e2e:install:npm:
|
||||
internal: true
|
||||
dir: desktop-e2e
|
||||
cmds:
|
||||
- '{{ if eq .CI "true" }}npm ci{{ else }}npm install{{ end }}'
|
||||
env:
|
||||
CI: '{{ .CI | default "false" }}'
|
||||
status:
|
||||
- test -d node_modules
|
||||
|
||||
e2e:install:driver:
|
||||
internal: true
|
||||
cmds:
|
||||
- cargo install tauri-driver --version {{.TAURI_DRIVER_VERSION}} --locked
|
||||
# Version-aware so a bumped TAURI_DRIVER_VERSION actually reinstalls,
|
||||
# while a matching install skips the recompile.
|
||||
status:
|
||||
- cargo install --list | grep -q "tauri-driver v{{.TAURI_DRIVER_VERSION}}"
|
||||
|
||||
smoke:
|
||||
desc: "Headless desktop smoke against the built app (all platforms)"
|
||||
summary: |
|
||||
Launches the built binary, waits for the bundled backend, and runs real
|
||||
tools through it. Needs no webview driver, so unlike desktop:e2e this
|
||||
also covers macOS.
|
||||
|
||||
task desktop:build:dev
|
||||
task desktop:smoke
|
||||
dir: desktop-e2e
|
||||
cmds:
|
||||
- node smoke/run-smoke.mjs
|
||||
|
||||
e2e:
|
||||
desc: "Run desktop UI E2E against the built app (Linux/Windows only)"
|
||||
summary: |
|
||||
Requires a non-bundled release build to exist:
|
||||
task desktop:build:dev
|
||||
task desktop:e2e
|
||||
|
||||
macOS is unsupported: there is no WebDriver for WKWebView, so
|
||||
tauri-driver has no macOS backend.
|
||||
|
||||
Pass extra WebdriverIO flags via -- :
|
||||
task desktop:e2e -- --spec specs/app-boot.e2e.js
|
||||
deps: [e2e:install]
|
||||
dir: desktop-e2e
|
||||
cmds:
|
||||
- npx wdio run wdio.conf.mjs {{.CLI_ARGS}}
|
||||
|
||||
clean:
|
||||
desc: "Clean Tauri/Cargo build artifacts"
|
||||
dir: editor
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
node_modules/
|
||||
logs/
|
||||
@@ -0,0 +1,95 @@
|
||||
# Desktop E2E
|
||||
|
||||
Tests that run the **real built desktop app** - the Tauri shell, its webview,
|
||||
and the bundled JRE running the Stirling JAR.
|
||||
|
||||
Two suites, because tauri-driver has no macOS backend:
|
||||
|
||||
| Suite | Command | Runs on | What it drives |
|
||||
| --------- | -------------------- | -------------------- | ---------------------------------------------------------- |
|
||||
| **smoke** | `task desktop:smoke` | Linux, Windows, macOS | Launches the binary, reads the backend port off its stdout, runs real tools over HTTP |
|
||||
| **ui** | `task desktop:e2e` | Linux, Windows | Drives the real webview through `tauri-driver` |
|
||||
|
||||
## What each one is for
|
||||
|
||||
**smoke** is where per-platform *runtime* bugs surface, and it is the only
|
||||
desktop coverage macOS can have. Everything it calls goes through the bundled
|
||||
jlink JRE running the bundled JAR, so it catches a runtime missing a module (the
|
||||
`jdk.dynalink` regression), a JRE older than the JAR's class-file version, and
|
||||
JPDFium natives that were never published for this OS/arch. It needs no npm
|
||||
install - the runner is plain Node.
|
||||
|
||||
- `rotate-pdf` - a pure-PDFBox tool: proves an ordinary server tool works end to end.
|
||||
- `merge-pdfs` - goes through `stirling.software.jpdfium.PdfMerge`, which
|
||||
rethrows any native failure rather than falling back to PDFBox, so a 200 here
|
||||
means the platform's JPDFium native loaded and ran.
|
||||
|
||||
**ui** covers what only breaks in the packaged webview: the window opens, the
|
||||
frontend actually mounts (rather than the blank window of #6878), the navigation
|
||||
guard holds, Tauri IPC answers, and a tool runs end to end from the UI -
|
||||
workbench → IndexedDB → the bundled backend → review panel.
|
||||
|
||||
## How this fits the rest of the suite
|
||||
|
||||
| Layer | Where | What it proves |
|
||||
| --------------------- | --------------------------------- | -------------------------------------- |
|
||||
| Rust unit/integration | `editor/src-tauri/{src,tests}` | Rust command logic |
|
||||
| Desktop TS units | `editor/src/desktop/**/*.test.ts` | The frontend's desktop seams |
|
||||
| **Desktop E2E** | `frontend/desktop-e2e` | The bundle actually launches and works |
|
||||
| Browser E2E | `editor/src/core/tests` | Product UI in a browser (Playwright) |
|
||||
|
||||
Keep specs here focused on things that can only break in the packaged app.
|
||||
Product behaviour belongs in the Playwright suites, which run far faster.
|
||||
|
||||
## Why WebdriverIO and not Playwright
|
||||
|
||||
Playwright cannot attach to WebKitGTK or WebView2 as embedded in a Tauri app -
|
||||
it speaks CDP and its own protocols, not WebDriver classic.
|
||||
[`tauri-driver`](https://v2.tauri.app/develop/tests/webdriver/) is a WebDriver
|
||||
server that launches the app binary and proxies to the platform's native webview
|
||||
driver, so the UI suite has to use a WebDriver client.
|
||||
|
||||
| Platform | UI suite | Native driver |
|
||||
| -------- | -------- | ------------------------------------------ |
|
||||
| Linux | yes | `WebKitWebDriver` (`webkit2gtk-driver`) |
|
||||
| Windows | yes | `msedgedriver` matching the WebView2 build |
|
||||
| macOS | **no** | Apple ships no WebDriver for WKWebView |
|
||||
|
||||
## Running locally
|
||||
|
||||
```bash
|
||||
task desktop:build:dev
|
||||
task desktop:smoke
|
||||
task desktop:e2e
|
||||
```
|
||||
|
||||
`desktop:build:dev` produces a non-bundled release binary under
|
||||
`editor/src-tauri/target/release/` with the jlink runtime and JAR staged
|
||||
alongside it - enough to launch, without paying for MSI/dmg packaging.
|
||||
|
||||
On Windows, point the UI suite at a matching Edge driver if it is not on `PATH`:
|
||||
|
||||
```bash
|
||||
TAURI_DRIVER_NATIVE="C:/SeleniumWebDrivers/EdgeDriver/msedgedriver.exe" task desktop:e2e
|
||||
```
|
||||
|
||||
Other environment variables:
|
||||
|
||||
- `STIRLING_APP_BINARY` - test a specific binary instead of the discovered one.
|
||||
- `SMOKE_TIMEOUT_MS` - how long the smoke runner waits for the backend port.
|
||||
|
||||
## Adding specs
|
||||
|
||||
UI specs share a single WebDriver session, and therefore a single app process,
|
||||
because the app registers `tauri-plugin-single-instance` - a second launch hands
|
||||
off to the first and exits. Add new files to the spec group in `wdio.conf.mjs`
|
||||
rather than relying on a glob; the group order matters, since
|
||||
`backend-sidecar.e2e.js` is what blocks until the backend is reachable.
|
||||
|
||||
## Why this lives here and not under `editor/`
|
||||
|
||||
Tauri resolves its "app directory" by recursively searching under `src-tauri`'s
|
||||
parent for a `package.json`. A `package.json` anywhere below `frontend/editor`
|
||||
therefore wins that search, and `tauri build` runs `beforeBuildCommand` from it -
|
||||
so the vite build dies with `Could not resolve entry module "index.html"`.
|
||||
Keeping this package a sibling of `editor/` avoids that entirely.
|
||||
@@ -0,0 +1,69 @@
|
||||
// Locating the built desktop app + the PDFs the suites exercise it with.
|
||||
// Shared by the WebDriver suite (wdio.conf.mjs) and the headless smoke runner.
|
||||
import { existsSync } from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const e2eDir = join(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
|
||||
// Sibling of editor/, deliberately NOT inside it: Tauri resolves its app dir by
|
||||
// recursively searching under src-tauri's parent for a package.json, so a
|
||||
// package.json anywhere below frontend/editor makes `tauri build` run
|
||||
// beforeBuildCommand in the wrong directory and the vite build fails with
|
||||
// "Could not resolve entry module index.html".
|
||||
export const tauriDir = join(e2eDir, "..", "editor", "src-tauri");
|
||||
|
||||
export const isWindows = process.platform === "win32";
|
||||
const exe = isWindows ? ".exe" : "";
|
||||
|
||||
/** Reuses the Playwright suites' fixtures rather than adding more binaries. */
|
||||
export function fixture(name) {
|
||||
const path = join(
|
||||
e2eDir,
|
||||
"..",
|
||||
"editor",
|
||||
"src",
|
||||
"core",
|
||||
"tests",
|
||||
"test-fixtures",
|
||||
name,
|
||||
);
|
||||
if (!existsSync(path)) {
|
||||
throw new Error(`Missing test fixture: ${path}`);
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
* `tauri build --no-bundle` renames the Cargo artefact to `mainBinaryName`,
|
||||
* which only differs from the crate name on a case-sensitive filesystem - so
|
||||
* check both spellings, release before debug.
|
||||
*/
|
||||
export function resolveAppBinary() {
|
||||
if (process.env.STIRLING_APP_BINARY) {
|
||||
const explicit = process.env.STIRLING_APP_BINARY;
|
||||
if (!existsSync(explicit)) {
|
||||
throw new Error(
|
||||
`STIRLING_APP_BINARY points at a missing file: ${explicit}`,
|
||||
);
|
||||
}
|
||||
return explicit;
|
||||
}
|
||||
|
||||
const candidates = [];
|
||||
for (const profile of ["release", "debug"]) {
|
||||
for (const name of ["Stirling-PDF", "stirling-pdf"]) {
|
||||
candidates.push(join(tauriDir, "target", profile, `${name}${exe}`));
|
||||
}
|
||||
}
|
||||
|
||||
const found = candidates.find((candidate) => existsSync(candidate));
|
||||
if (!found) {
|
||||
throw new Error(
|
||||
"No built desktop binary found. Build one first:\n" +
|
||||
" task desktop:build:dev\n" +
|
||||
`Looked in:\n${candidates.map((c) => ` ${c}`).join("\n")}`,
|
||||
);
|
||||
}
|
||||
return found;
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
// Exercises real tools against whichever backend the desktop app started.
|
||||
//
|
||||
// Every call here goes through the *bundled* jlink JRE running the *bundled*
|
||||
// JAR, so a failure means the shipped runtime is wrong for this platform -
|
||||
// a missing jlink module, a JRE older than the JAR's class-file version, or
|
||||
// native libraries that were not published for this OS/arch.
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { basename } from "node:path";
|
||||
|
||||
const PDF_MAGIC = "%PDF-";
|
||||
|
||||
async function pdfPart(path) {
|
||||
const bytes = await readFile(path);
|
||||
return new File([bytes], basename(path), { type: "application/pdf" });
|
||||
}
|
||||
|
||||
async function postForm(baseUrl, path, form) {
|
||||
const response = await fetch(`${baseUrl}${path}`, {
|
||||
method: "POST",
|
||||
body: form,
|
||||
});
|
||||
if (!response.ok) {
|
||||
const detail = await response.text().catch(() => "<unreadable body>");
|
||||
throw new Error(
|
||||
`POST ${path} returned ${response.status}: ${detail.slice(0, 800)}`,
|
||||
);
|
||||
}
|
||||
return Buffer.from(await response.arrayBuffer());
|
||||
}
|
||||
|
||||
export function baseUrl(port) {
|
||||
return `http://127.0.0.1:${port}`;
|
||||
}
|
||||
|
||||
export async function fetchStatus(port) {
|
||||
const response = await fetch(`${baseUrl(port)}/api/v1/info/status`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`status endpoint returned ${response.status}`);
|
||||
}
|
||||
return response.text();
|
||||
}
|
||||
|
||||
/**
|
||||
* Pure-PDFBox tool: proves the bundled JRE can run an ordinary server tool
|
||||
* end to end and hand back a real PDF.
|
||||
*/
|
||||
export async function rotatePdf(port, filePath, angle = 90) {
|
||||
const form = new FormData();
|
||||
form.set("fileInput", await pdfPart(filePath));
|
||||
form.set("angle", String(angle));
|
||||
return postForm(baseUrl(port), "/api/v1/general/rotate-pdf", form);
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge goes through `stirling.software.jpdfium.PdfMerge`, so it loads the
|
||||
* JPDFium native for this platform. Those natives are selected at build time
|
||||
* (-PjpdfiumPlatforms) and a wrong or missing one only fails at runtime -
|
||||
* exactly what a build-only CI job cannot catch.
|
||||
*/
|
||||
export async function mergePdfs(port, filePaths) {
|
||||
const form = new FormData();
|
||||
for (const path of filePaths) {
|
||||
form.append("fileInput", await pdfPart(path));
|
||||
}
|
||||
return postForm(baseUrl(port), "/api/v1/general/merge-pdfs", form);
|
||||
}
|
||||
|
||||
export function assertIsPdf(bytes, what) {
|
||||
if (bytes.length < 1024) {
|
||||
throw new Error(`${what}: suspiciously small response (${bytes.length}B)`);
|
||||
}
|
||||
const header = bytes.subarray(0, PDF_MAGIC.length).toString("latin1");
|
||||
if (header !== PDF_MAGIC) {
|
||||
throw new Error(`${what}: response is not a PDF (starts with "${header}")`);
|
||||
}
|
||||
}
|
||||
|
||||
/** Cheap page count - good enough to prove a merge actually combined inputs. */
|
||||
export function countPages(bytes) {
|
||||
const matches = bytes.toString("latin1").match(/\/Type\s*\/Page[^s]/g);
|
||||
return matches ? matches.length : 0;
|
||||
}
|
||||
Generated
+6001
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "stirling-desktop-e2e",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"description": "Desktop E2E suite - drives the built Tauri app through tauri-driver. Deliberately kept out of the frontend npm workspace so WebdriverIO never lands in the shared install.",
|
||||
"scripts": {
|
||||
"test": "wdio run wdio.conf.mjs"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@wdio/cli": "^9.20.0",
|
||||
"@wdio/jasmine-framework": "^9.20.0",
|
||||
"@wdio/local-runner": "^9.20.0",
|
||||
"@wdio/spec-reporter": "^9.20.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
#!/usr/bin/env node
|
||||
// Headless desktop smoke: launch the built app, wait for its bundled backend,
|
||||
// then run real tools through it.
|
||||
//
|
||||
// Why this exists alongside the WebDriver suite: tauri-driver has no macOS
|
||||
// backend (Apple ships no WebDriver for WKWebView), so the wdio specs can only
|
||||
// run on Linux and Windows. This runner never touches the webview - it reads
|
||||
// the backend port off the app's stdout and talks HTTP - so it is the only
|
||||
// desktop coverage that works on all three platforms. On Linux the app still
|
||||
// needs a display; CI wraps this in xvfb-run.
|
||||
import { spawn, spawnSync } from "node:child_process";
|
||||
|
||||
import { isWindows, resolveAppBinary, fixture } from "../lib/app-binary.mjs";
|
||||
import {
|
||||
assertIsPdf,
|
||||
countPages,
|
||||
fetchStatus,
|
||||
mergePdfs,
|
||||
rotatePdf,
|
||||
} from "../lib/backend-tools.mjs";
|
||||
|
||||
// Spring Boot cold start inside a jlink runtime on a cold CI runner.
|
||||
const BACKEND_TIMEOUT_MS = Number(process.env.SMOKE_TIMEOUT_MS || 240_000);
|
||||
// utils/logging.rs mirrors every add_log() to stdout; commands/backend.rs logs
|
||||
// this line once the Java process reports the port the OS handed it.
|
||||
const PORT_PATTERNS = [
|
||||
/backend started on port:\s*(\d+)/i,
|
||||
/running on port:\s*(\d+)/i,
|
||||
];
|
||||
|
||||
const checks = [];
|
||||
|
||||
async function check(name, fn) {
|
||||
const started = Date.now();
|
||||
try {
|
||||
await fn();
|
||||
checks.push({ name, ok: true, ms: Date.now() - started });
|
||||
console.log(` ok ${name}`);
|
||||
} catch (error) {
|
||||
checks.push({ name, ok: false, ms: Date.now() - started, error });
|
||||
console.log(
|
||||
` FAIL ${name}\n ${error.message.replace(/\n/g, "\n ")}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function killTree(child) {
|
||||
if (!child?.pid || child.exitCode !== null) return;
|
||||
if (isWindows) {
|
||||
// The app forks the bundled java process; killing only the parent leaves
|
||||
// it holding its port.
|
||||
spawnSync("taskkill", ["/pid", String(child.pid), "/T", "/F"], {
|
||||
stdio: "ignore",
|
||||
});
|
||||
} else {
|
||||
child.kill("SIGTERM");
|
||||
}
|
||||
}
|
||||
|
||||
function launchApp(binary) {
|
||||
const child = spawn(binary, [], { stdio: ["ignore", "pipe", "pipe"] });
|
||||
const output = [];
|
||||
|
||||
let resolvePort;
|
||||
let rejectPort;
|
||||
const port = new Promise((resolve, reject) => {
|
||||
resolvePort = resolve;
|
||||
rejectPort = reject;
|
||||
});
|
||||
|
||||
const scan = (chunk) => {
|
||||
const text = chunk.toString();
|
||||
output.push(text);
|
||||
for (const pattern of PORT_PATTERNS) {
|
||||
const match = text.match(pattern);
|
||||
if (match) resolvePort(Number(match[1]));
|
||||
}
|
||||
};
|
||||
|
||||
child.stdout.on("data", scan);
|
||||
child.stderr.on("data", scan);
|
||||
child.on("error", rejectPort);
|
||||
child.on("exit", (code) =>
|
||||
rejectPort(new Error(`app exited early with code ${code}`)),
|
||||
);
|
||||
|
||||
const timer = setTimeout(
|
||||
() =>
|
||||
rejectPort(new Error(`no backend port within ${BACKEND_TIMEOUT_MS}ms`)),
|
||||
BACKEND_TIMEOUT_MS,
|
||||
);
|
||||
port.finally(() => clearTimeout(timer)).catch(() => {});
|
||||
|
||||
return { child, port, output };
|
||||
}
|
||||
|
||||
const binary = resolveAppBinary();
|
||||
console.log(`Desktop smoke on ${process.platform}`);
|
||||
console.log(`Application under test: ${binary}\n`);
|
||||
|
||||
const { child, port: portPromise, output } = launchApp(binary);
|
||||
|
||||
let port;
|
||||
try {
|
||||
port = await portPromise;
|
||||
console.log(`Bundled backend came up on port ${port}\n`);
|
||||
} catch (error) {
|
||||
console.error(`\nApp never reported a backend port: ${error.message}`);
|
||||
console.error("--- app output ---");
|
||||
console.error(output.join("") || "(nothing captured)");
|
||||
console.error("--- end app output ---");
|
||||
killTree(child);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
try {
|
||||
await check("bundled JAR serves a healthy status endpoint", async () => {
|
||||
const body = await fetchStatus(port);
|
||||
if (!body.includes("UP"))
|
||||
throw new Error(`status body was: ${body.slice(0, 200)}`);
|
||||
});
|
||||
|
||||
await check("server tool: rotate-pdf returns a rotated PDF", async () => {
|
||||
const result = await rotatePdf(port, fixture("sample.pdf"), 90);
|
||||
assertIsPdf(result, "rotate-pdf");
|
||||
});
|
||||
|
||||
await check("JPDFium: merge-pdfs combines documents natively", async () => {
|
||||
const inputs = [
|
||||
fixture("compare_sample_a.pdf"),
|
||||
fixture("compare_sample_b.pdf"),
|
||||
];
|
||||
const merged = await mergePdfs(port, inputs);
|
||||
assertIsPdf(merged, "merge-pdfs");
|
||||
|
||||
// MergeController merges via stirling.software.jpdfium.PdfMerge and rethrows
|
||||
// any native failure as "JPDFium merge failed", so a 200 here means the
|
||||
// platform's JPDFium native loaded and ran.
|
||||
const pages = countPages(merged);
|
||||
if (pages < 2) {
|
||||
throw new Error(`merged PDF has ${pages} page(s); expected at least 2`);
|
||||
}
|
||||
});
|
||||
} finally {
|
||||
killTree(child);
|
||||
}
|
||||
|
||||
const failed = checks.filter((c) => !c.ok);
|
||||
console.log(
|
||||
`\n${checks.length - failed.length}/${checks.length} checks passed` +
|
||||
(failed.length
|
||||
? ` - failing: ${failed.map((c) => c.name).join(", ")}`
|
||||
: ""),
|
||||
);
|
||||
|
||||
if (failed.length) {
|
||||
console.error("\n--- app output ---");
|
||||
console.error(output.join("") || "(nothing captured)");
|
||||
console.error("--- end app output ---");
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
// Proves the packaged app actually opens and renders. A `tauri build` that
|
||||
// succeeds still ships a blank window when the webview fails to start (#6878
|
||||
// Wayland/EGL) or when the frontend bundle never mounts - neither shows up in
|
||||
// a build-only CI job.
|
||||
|
||||
import { waitForAppMount } from "./helpers/ui.js";
|
||||
|
||||
describe("desktop app boot", () => {
|
||||
it("opens the app window with the bundled frontend title", async () => {
|
||||
await expect(browser).toHaveTitle("Stirling PDF");
|
||||
});
|
||||
|
||||
it("mounts the React app instead of showing a blank window", async () => {
|
||||
const root = await $("#root");
|
||||
await expect(root).toExist();
|
||||
|
||||
// A blank window still has #root - the tell is that nothing rendered into
|
||||
// it. Wait for real children rather than asserting immediately, since the
|
||||
// webview is driveable before React has finished its first paint.
|
||||
await waitForAppMount();
|
||||
});
|
||||
|
||||
it("stays on an app URL so the navigation guard has not been bypassed", async () => {
|
||||
// lib.rs::is_app_url only permits the bundled app and the dev server.
|
||||
// Anything else here means the webview navigated away from the app, which
|
||||
// is what left the window unclosable in #6872.
|
||||
const url = await browser.getUrl();
|
||||
expect(url).toMatch(
|
||||
/^(tauri:\/\/localhost|https?:\/\/(tauri\.localhost|localhost|127\.0\.0\.1))/,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,40 @@
|
||||
// Proves the bundled JRE can actually run the bundled JAR.
|
||||
//
|
||||
// This is the regression net for the whole jlink chain: a runtime missing a
|
||||
// module (jdk.dynalink was missing and broke PDF/A validation), a JRE older
|
||||
// than the JAR's class-file version, a JAR that never got copied into
|
||||
// resources, or natives absent for the platform. All of those bundle
|
||||
// perfectly happily and only fail when the app is run.
|
||||
|
||||
import { readAppLogs, waitForBackendPort } from "./helpers/tauri.js";
|
||||
|
||||
describe("bundled backend sidecar", () => {
|
||||
let port;
|
||||
|
||||
it("starts the bundled JRE and reports the backend port", async () => {
|
||||
try {
|
||||
port = await waitForBackendPort();
|
||||
} catch (error) {
|
||||
// The Rust side logs every step of JRE/JAR discovery, so dump it before
|
||||
// failing - otherwise a CI failure here is undebuggable.
|
||||
const logs = await readAppLogs();
|
||||
console.error("--- app logs ---");
|
||||
console.error(logs.join("\n") || "(no logs captured)");
|
||||
console.error("--- end app logs ---");
|
||||
throw error;
|
||||
}
|
||||
|
||||
expect(port).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("serves a healthy status endpoint from the bundled JAR", async () => {
|
||||
// Checked from Node rather than the webview: the backend's CORS policy is
|
||||
// written for the app's tauri:// origin, and the desktop app itself goes
|
||||
// through tauri-plugin-http, so a raw in-page fetch is not representative.
|
||||
const response = await fetch(`http://127.0.0.1:${port}/api/v1/info/status`);
|
||||
expect(response.status).toBe(200);
|
||||
|
||||
const body = await response.text();
|
||||
expect(body).toContain("UP");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,49 @@
|
||||
// Drives a real tool from the UI, in the packaged app, end to end.
|
||||
//
|
||||
// This is the only test that covers the whole desktop chain in one go: the
|
||||
// webview renders the workbench, a file lands in IndexedDB inside the packaged
|
||||
// app, the tool posts to /api/v1/general/rotate-pdf on the *bundled* backend,
|
||||
// and the result comes back into the review panel. Any link in that chain can
|
||||
// break in the desktop build alone - the Playwright suites run against a dev
|
||||
// server with stubbed APIs and would not notice.
|
||||
|
||||
import { fixture } from "../lib/app-binary.mjs";
|
||||
import {
|
||||
dismissStartupModals,
|
||||
uploadFile,
|
||||
waitForAppMount,
|
||||
} from "./helpers/ui.js";
|
||||
|
||||
describe("desktop UI runs a tool against the bundled backend", () => {
|
||||
it("uploads a PDF, rotates it, and surfaces a downloadable result", async () => {
|
||||
await waitForAppMount();
|
||||
await dismissStartupModals();
|
||||
|
||||
await uploadFile(fixture("sample.pdf"));
|
||||
|
||||
const rotateTool = await $('[data-tour="tool-button-rotate"]');
|
||||
await rotateTool.click();
|
||||
|
||||
// Rotate needs no configuration beyond its default angle, so the run button
|
||||
// enabling is the signal that the tool accepted the uploaded file.
|
||||
const runButton = await $('[data-tour="run-button"]');
|
||||
await runButton.waitForEnabled({
|
||||
timeout: 30_000,
|
||||
timeoutMsg:
|
||||
"Rotate's run button never enabled - the uploaded file did not reach " +
|
||||
"the tool panel.",
|
||||
});
|
||||
await runButton.click();
|
||||
|
||||
// The review panel only renders once the backend has returned a result, so
|
||||
// this failing means the request never completed: the bundled backend was
|
||||
// unreachable from the webview, or it errored.
|
||||
await $('[data-testid="review-panel-container"]').waitForExist({
|
||||
timeout: 120_000,
|
||||
timeoutMsg:
|
||||
"No review panel after running Rotate - the UI never got a result " +
|
||||
"back from the bundled backend.",
|
||||
});
|
||||
await expect($('[data-testid="download-result-button"]')).toExist();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,77 @@
|
||||
// `browser`, `$` and `expect` are injected as globals by WebdriverIO's mocha
|
||||
// framework (injectGlobals defaults to true), so specs and helpers use them
|
||||
// without importing.
|
||||
|
||||
/**
|
||||
* Fires a Tauri command in the webview and returns the most recent answer.
|
||||
*
|
||||
* Fire-and-stash rather than awaiting inside the browser: that keeps us on
|
||||
* synchronous `browser.execute`, whose behaviour is stable across WebdriverIO
|
||||
* majors (the `executeAsync` callback form is deprecated), and it re-seeds
|
||||
* itself if the webview ever reloads and wipes the stash.
|
||||
*
|
||||
* @returns {Promise<{value?: unknown, error: string|null}>} `value` is absent
|
||||
* until the first call resolves.
|
||||
*/
|
||||
export function invokeLatest(command, key = command) {
|
||||
return browser.execute(
|
||||
(cmd, stashKey) => {
|
||||
const stash = (window.__stirlingE2E = window.__stirlingE2E || {});
|
||||
const slot = (stash[stashKey] = stash[stashKey] || { error: null });
|
||||
window.__TAURI_INTERNALS__
|
||||
.invoke(cmd)
|
||||
.then((value) => {
|
||||
slot.value = value;
|
||||
})
|
||||
.catch((error) => {
|
||||
slot.error = String(error);
|
||||
});
|
||||
return { value: slot.value, error: slot.error };
|
||||
},
|
||||
command,
|
||||
key,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for the bundled JRE to boot the Stirling JAR and resolves to the port
|
||||
* the backend picked. The app asks the OS for a free port (-Dserver.port=0),
|
||||
* so the port is only knowable at runtime via the `get_backend_port` command.
|
||||
*/
|
||||
export async function waitForBackendPort(timeout = 210_000) {
|
||||
let port = null;
|
||||
|
||||
await browser.waitUntil(
|
||||
async () => {
|
||||
const { value, error } = await invokeLatest("get_backend_port");
|
||||
if (error) {
|
||||
throw new Error(`get_backend_port rejected: ${error}`);
|
||||
}
|
||||
if (typeof value === "number") {
|
||||
port = value;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
{
|
||||
timeout,
|
||||
interval: 2_000,
|
||||
timeoutMsg:
|
||||
`Backend never reported a port within ${timeout}ms. The bundled JRE ` +
|
||||
"either failed to launch or crashed before Spring Boot bound a port - " +
|
||||
"check the app logs dumped by the spec.",
|
||||
},
|
||||
);
|
||||
|
||||
return port;
|
||||
}
|
||||
|
||||
/** Reads the in-app log buffer, which records the whole backend startup path. */
|
||||
export async function readAppLogs() {
|
||||
const { value } = await invokeLatest("get_tauri_logs", "logs");
|
||||
if (Array.isArray(value)) return value;
|
||||
// First call only fires the request; give it a beat, then read the result.
|
||||
await browser.pause(1_000);
|
||||
const { value: retried } = await invokeLatest("get_tauri_logs", "logs");
|
||||
return Array.isArray(retried) ? retried : [];
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
// `browser`, `$` and `expect` are injected as globals by WebdriverIO's jasmine
|
||||
// framework (injectGlobals defaults to true), so specs and helpers use them
|
||||
// without importing.
|
||||
|
||||
/** Resolves once React has rendered something into #root. */
|
||||
export async function waitForAppMount(timeout = 60_000) {
|
||||
await browser.waitUntil(
|
||||
async () =>
|
||||
(await browser.execute(
|
||||
() => document.querySelectorAll("#root *").length,
|
||||
)) > 0,
|
||||
{
|
||||
timeout,
|
||||
interval: 500,
|
||||
timeoutMsg:
|
||||
"#root never received any children - the app window opened but the " +
|
||||
"frontend bundle did not mount (blank-window regression).",
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function countOverlays() {
|
||||
return browser.execute(
|
||||
() => document.querySelectorAll(".mantine-Modal-overlay").length,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* A fresh profile greets you with the "Welcome to Stirling V2" modal and then
|
||||
* the sign-in modal, each behind an overlay that swallows clicks. Close them in
|
||||
* a loop rather than assuming a fixed number: the chain is version-dependent,
|
||||
* and a spec that hard-codes two dismissals breaks the moment a third appears.
|
||||
*
|
||||
* The `?bypassOnboarding=true` route is deliberately not used - it also
|
||||
* suppresses the sign-in modal, and dismissing what a real first-run user sees
|
||||
* is closer to the thing we want to know still works.
|
||||
*/
|
||||
export async function dismissStartupModals(maxModals = 6) {
|
||||
for (let attempt = 0; attempt < maxModals; attempt += 1) {
|
||||
if ((await countOverlays()) === 0) return;
|
||||
|
||||
const close = await $('.mantine-Modal-content [aria-label="Close"]');
|
||||
if (await close.isExisting()) {
|
||||
await close.click().catch(() => {});
|
||||
} else {
|
||||
await browser.keys(["Escape"]);
|
||||
}
|
||||
await browser.pause(1_000);
|
||||
}
|
||||
|
||||
await browser.waitUntil(async () => (await countOverlays()) === 0, {
|
||||
timeout: 15_000,
|
||||
interval: 500,
|
||||
timeoutMsg: `A modal overlay is still blocking the UI after ${maxModals} dismissals.`,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Puts a file into the workbench through the app's own file input.
|
||||
*
|
||||
* The input is visually hidden, and unlike Playwright's setInputFiles a
|
||||
* WebDriver send-keys needs the element to be interactable - WebKitWebDriver
|
||||
* refuses outright. Making it briefly visible is the standard workaround and
|
||||
* still exercises the real upload path.
|
||||
*/
|
||||
export async function uploadFile(path) {
|
||||
await browser.execute(() => {
|
||||
const input = document.querySelector('[data-testid="file-input"]');
|
||||
if (!input) return;
|
||||
input.style.display = "block";
|
||||
input.style.visibility = "visible";
|
||||
input.style.opacity = "1";
|
||||
input.style.width = "1px";
|
||||
input.style.height = "1px";
|
||||
});
|
||||
|
||||
await $('[data-testid="file-input"]').addValue(path);
|
||||
|
||||
// The sidebar list only renders once addFiles has resolved, which awaits the
|
||||
// IndexedDB write - so this doubles as proof that desktop file persistence
|
||||
// works in the packaged webview.
|
||||
await $(".file-sidebar-file-item").waitForExist({
|
||||
timeout: 30_000,
|
||||
timeoutMsg: `${path} never appeared in the file sidebar.`,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
// WebdriverIO config for the desktop E2E suite.
|
||||
//
|
||||
// tauri-driver is a WebDriver shim: it speaks W3C WebDriver to us, launches the
|
||||
// real Tauri binary, and proxies to the platform's native webview driver
|
||||
// (WebKitWebDriver on Linux, msedgedriver on Windows). macOS has no WebDriver
|
||||
// for WKWebView, so this suite is Linux + Windows only.
|
||||
//
|
||||
// Playwright cannot drive these webviews - it speaks CDP/its own protocol, not
|
||||
// WebDriver classic - which is why the desktop suite uses WebdriverIO while the
|
||||
// browser suites stay on Playwright.
|
||||
import { spawn, spawnSync } from "node:child_process";
|
||||
import { existsSync } from "node:fs";
|
||||
import { homedir } from "node:os";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
import { isWindows, resolveAppBinary } from "./lib/app-binary.mjs";
|
||||
|
||||
const e2eDir = dirname(fileURLToPath(import.meta.url));
|
||||
const exe = isWindows ? ".exe" : "";
|
||||
const appBinary = resolveAppBinary();
|
||||
|
||||
// Spawned without a shell so the returned pid is tauri-driver itself. Going
|
||||
// through cmd.exe on Windows would give us the shell's pid, and killing that
|
||||
// leaves tauri-driver (and the webview driver it owns) running forever - which
|
||||
// hangs the run at teardown. `cargo install` puts it in the cargo bin dir;
|
||||
// fall back to PATH for anyone who installed it elsewhere.
|
||||
function resolveTauriDriver() {
|
||||
const cargoBin = join(
|
||||
process.env.CARGO_HOME || join(homedir(), ".cargo"),
|
||||
"bin",
|
||||
`tauri-driver${exe}`,
|
||||
);
|
||||
return existsSync(cargoBin) ? cargoBin : `tauri-driver${exe}`;
|
||||
}
|
||||
|
||||
let tauriDriver;
|
||||
|
||||
export const config = {
|
||||
runner: "local",
|
||||
// tauri-driver is already running as a plain WebDriver server on this port,
|
||||
// so wdio must attach to it rather than manage a browser driver itself.
|
||||
hostname: "127.0.0.1",
|
||||
port: 4444,
|
||||
path: "/",
|
||||
|
||||
// Nested array = one session shared by both specs, run in this order. That
|
||||
// is required, not just an optimisation: the app registers
|
||||
// tauri-plugin-single-instance, so a second launch while the first is still
|
||||
// shutting down hands off to the old instance and exits immediately. Sharing
|
||||
// a session also means we pay the Spring Boot cold start only once.
|
||||
// Order matters: backend-sidecar blocks until the bundled backend reports a
|
||||
// port, so the UI spec after it can assume the backend is reachable instead
|
||||
// of racing Spring Boot's cold start.
|
||||
specs: [
|
||||
[
|
||||
join(e2eDir, "specs", "app-boot.e2e.js"),
|
||||
join(e2eDir, "specs", "backend-sidecar.e2e.js"),
|
||||
join(e2eDir, "specs", "frontend-tool-run.e2e.js"),
|
||||
],
|
||||
],
|
||||
|
||||
// The app owns a single OS window and a single bundled backend; running
|
||||
// sessions concurrently would have them fight over both.
|
||||
maxInstances: 1,
|
||||
capabilities: [
|
||||
{
|
||||
browserName: "wry",
|
||||
"tauri:options": {
|
||||
application: appBinary,
|
||||
},
|
||||
// WebdriverIO prefers WebDriver BiDi where the driver advertises it
|
||||
// (msedgedriver does). BiDi evaluates scripts in its own realm, and
|
||||
// Tauri's IPC rejects invokes from there with "Origin header is not a
|
||||
// valid URL" - so every window.__TAURI_INTERNALS__.invoke() fails. The
|
||||
// classic executeScript endpoint runs in the page's real realm and works.
|
||||
// WebKitWebDriver has no BiDi anyway, so this also keeps Linux and
|
||||
// Windows on the same code path.
|
||||
"wdio:enforceWebDriverClassic": true,
|
||||
},
|
||||
],
|
||||
|
||||
// Per-worker logs on disk so a CI failure can be inspected after the fact.
|
||||
outputDir: join(e2eDir, "logs"),
|
||||
logLevel: "info",
|
||||
bail: 0,
|
||||
waitforTimeout: 30_000,
|
||||
connectionRetryTimeout: 180_000,
|
||||
connectionRetryCount: 3,
|
||||
|
||||
// Jasmine rather than Mocha: same describe/it surface, but Mocha's tree
|
||||
// carries unpatched advisories (serialize-javascript) that dependency-review
|
||||
// fails the PR on, and its only "fix" is a major downgrade of the wdio
|
||||
// framework adapter.
|
||||
framework: "jasmine",
|
||||
reporters: ["spec"],
|
||||
jasmineOpts: {
|
||||
// The bundled JRE + Spring Boot cold start dominates: on a cold CI runner
|
||||
// the backend can take well over a minute to report its port.
|
||||
defaultTimeoutInterval: 240_000,
|
||||
},
|
||||
|
||||
onPrepare: () => {
|
||||
const args = ["--port", "4444"];
|
||||
// Windows runners ship msedgedriver at a fixed path but do not put it on
|
||||
// PATH; Linux gets WebKitWebDriver on PATH from the webkit2gtk-driver
|
||||
// package, so the override is optional there.
|
||||
if (process.env.TAURI_DRIVER_NATIVE) {
|
||||
args.push("--native-driver", process.env.TAURI_DRIVER_NATIVE);
|
||||
}
|
||||
|
||||
const bin = resolveTauriDriver();
|
||||
console.log(`Launching ${bin} ${args.join(" ")}`);
|
||||
console.log(`Application under test: ${appBinary}`);
|
||||
|
||||
tauriDriver = spawn(bin, args, {
|
||||
stdio: ["ignore", "inherit", "inherit"],
|
||||
});
|
||||
|
||||
tauriDriver.on("error", (error) => {
|
||||
console.error("tauri-driver failed to start:", error);
|
||||
process.exit(1);
|
||||
});
|
||||
},
|
||||
|
||||
onComplete: () => {
|
||||
// Without this the driver keeps the webview driver (and the app) alive and
|
||||
// the run never exits after the last spec.
|
||||
if (!tauriDriver?.pid) return;
|
||||
|
||||
if (isWindows) {
|
||||
// tauri-driver spawns msedgedriver as a child; a plain kill() leaves it
|
||||
// orphaned and holding the port, so tear down the whole tree.
|
||||
spawnSync("taskkill", ["/pid", String(tauriDriver.pid), "/T", "/F"], {
|
||||
stdio: "ignore",
|
||||
});
|
||||
} else {
|
||||
tauriDriver.kill();
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -508,8 +508,8 @@ pub async fn login(
|
||||
let username = login_response.user.user_metadata
|
||||
.as_ref()
|
||||
.and_then(|m| m.full_name.clone())
|
||||
.or_else(|| email.clone())
|
||||
.unwrap_or_else(|| username);
|
||||
.or(email.clone())
|
||||
.unwrap_or(username);
|
||||
|
||||
Ok(LoginResponse {
|
||||
token: login_response.access_token,
|
||||
@@ -568,7 +568,7 @@ pub async fn login(
|
||||
} else if error_lower.contains("timeout") {
|
||||
format!("Connection timeout: Server at {} is not responding. Check your network connection.", login_url)
|
||||
} else if error_lower.contains("dns") || error_lower.contains("resolve") {
|
||||
format!("DNS resolution failed: Cannot resolve hostname. Check if the server URL is correct.")
|
||||
"DNS resolution failed: Cannot resolve hostname. Check if the server URL is correct.".to_string()
|
||||
} else {
|
||||
format!("Network error: {}", e)
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ fn check_backend_status() -> Result<(), String> {
|
||||
}
|
||||
|
||||
// Find the bundled JRE and return the java executable path
|
||||
fn find_bundled_jre(resource_dir: &PathBuf) -> Result<PathBuf, String> {
|
||||
fn find_bundled_jre(resource_dir: &Path) -> Result<PathBuf, String> {
|
||||
let jre_dir = resource_dir.join("runtime").join("jre");
|
||||
let java_executable = if cfg!(windows) {
|
||||
jre_dir.join("bin").join("java.exe")
|
||||
@@ -74,7 +74,7 @@ fn find_bundled_jre(resource_dir: &PathBuf) -> Result<PathBuf, String> {
|
||||
}
|
||||
|
||||
// Find the Stirling-PDF JAR file
|
||||
fn find_stirling_jar(resource_dir: &PathBuf) -> Result<PathBuf, String> {
|
||||
fn find_stirling_jar(resource_dir: &Path) -> Result<PathBuf, String> {
|
||||
let libs_dir = resource_dir.join("libs");
|
||||
let mut jar_files: Vec<_> = std::fs::read_dir(&libs_dir)
|
||||
.map_err(|e| {
|
||||
@@ -113,20 +113,20 @@ fn find_stirling_jar(resource_dir: &PathBuf) -> Result<PathBuf, String> {
|
||||
}
|
||||
|
||||
// Normalize path to remove Windows UNC prefix
|
||||
fn normalize_path(path: &PathBuf) -> PathBuf {
|
||||
fn normalize_path(path: &Path) -> PathBuf {
|
||||
if cfg!(windows) {
|
||||
let path_str = path.to_string_lossy();
|
||||
if path_str.starts_with(r"\\?\") {
|
||||
PathBuf::from(&path_str[4..]) // Remove \\?\ prefix
|
||||
if let Some(stripped) = path_str.strip_prefix(r"\\?\") {
|
||||
PathBuf::from(stripped)
|
||||
} else {
|
||||
path.clone()
|
||||
path.to_path_buf()
|
||||
}
|
||||
} else {
|
||||
path.clone()
|
||||
path.to_path_buf()
|
||||
}
|
||||
}
|
||||
|
||||
fn migrate_legacy_workspace(legacy_dir: &PathBuf, target_root: &PathBuf) -> std::io::Result<()> {
|
||||
fn migrate_legacy_workspace(legacy_dir: &Path, target_root: &Path) -> std::io::Result<()> {
|
||||
for entry in std::fs::read_dir(legacy_dir)? {
|
||||
let entry = entry?;
|
||||
let file_type = entry.file_type()?;
|
||||
@@ -313,7 +313,7 @@ fn monitor_backend_output(mut rx: tauri::async_runtime::Receiver<tauri_plugin_sh
|
||||
// Format: "Stirling-PDF running on port: PORT"
|
||||
if output_str.contains("running on port:") {
|
||||
_startup_detected = true;
|
||||
if let Some(port) = extract_port_from_running_log(&output_str) {
|
||||
if let Some(port) = extract_port_from_running_log(output_str) {
|
||||
let mut port_guard = BACKEND_PORT.lock().unwrap();
|
||||
*port_guard = Some(port);
|
||||
add_log(format!("🎉 Backend started on port: {}", port));
|
||||
@@ -428,15 +428,13 @@ pub async fn start_backend(
|
||||
add_log(format!("🔍 Resource directory: {:?}", resource_dir));
|
||||
|
||||
// Find the bundled JRE
|
||||
let java_executable = find_bundled_jre(&resource_dir).map_err(|e| {
|
||||
let java_executable = find_bundled_jre(&resource_dir).inspect_err(|_| {
|
||||
reset_starting_flag();
|
||||
e
|
||||
})?;
|
||||
|
||||
// Find the Stirling-PDF JAR
|
||||
let jar_path = find_stirling_jar(&resource_dir).map_err(|e| {
|
||||
let jar_path = find_stirling_jar(&resource_dir).inspect_err(|_| {
|
||||
reset_starting_flag();
|
||||
e
|
||||
})?;
|
||||
|
||||
// Normalize the paths to remove Windows UNC prefix
|
||||
@@ -448,9 +446,8 @@ pub async fn start_backend(
|
||||
add_log(format!("📦 Normalized Java path: {:?}", normalized_java_path));
|
||||
|
||||
// Create and start the Java command
|
||||
run_stirling_pdf_jar(&app, &normalized_java_path, &normalized_jar_path).map_err(|e| {
|
||||
run_stirling_pdf_jar(&app, &normalized_java_path, &normalized_jar_path).inspect_err(|_| {
|
||||
reset_starting_flag();
|
||||
e
|
||||
})?;
|
||||
|
||||
// Reset the starting flag since startup is complete
|
||||
|
||||
@@ -27,27 +27,23 @@ const PROVISIONING_FILE_NAME: &str = "stirling-provisioning.json";
|
||||
|
||||
/// How the desktop auto-updater should behave on startup.
|
||||
///
|
||||
/// * `Prompt` – default. Show the update popup when a new version is available
|
||||
/// and let the user decide whether to install.
|
||||
/// * `Auto` – silently download and install updates on startup, then restart.
|
||||
/// Intended for managed deployments (Intune/MDM) where the user
|
||||
/// cannot (or should not) be prompted.
|
||||
/// * `Disabled` – never check for updates, never show the update UI. Administrators
|
||||
/// are expected to push updates through their normal packaging flow.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
/// * `Prompt` – default. Show the update popup when a new version is available
|
||||
/// and let the user decide whether to install.
|
||||
/// * `Auto` – silently download and install updates on startup, then restart.
|
||||
/// Intended for managed deployments (Intune/MDM) where the user cannot (or
|
||||
/// should not) be prompted.
|
||||
/// * `Disabled` – never check for updates, never show the update UI.
|
||||
/// Administrators are expected to push updates through their normal
|
||||
/// packaging flow.
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum UpdateMode {
|
||||
#[default]
|
||||
Prompt,
|
||||
Auto,
|
||||
Disabled,
|
||||
}
|
||||
|
||||
impl Default for UpdateMode {
|
||||
fn default() -> Self {
|
||||
UpdateMode::Prompt
|
||||
}
|
||||
}
|
||||
|
||||
/// Current update mode plus whether the UI is allowed to change it. Returned
|
||||
/// by [`get_update_mode`] so the settings page can show a "managed by
|
||||
/// administrator" hint instead of silently ignoring clicks.
|
||||
@@ -309,7 +305,7 @@ pub fn apply_provisioning_if_present(app_handle: &AppHandle) -> Result<(), Strin
|
||||
if let Some(mode) = parsed.update_mode {
|
||||
store.set(
|
||||
UPDATE_MODE_KEY,
|
||||
serde_json::to_value(&mode)
|
||||
serde_json::to_value(mode)
|
||||
.map_err(|e| format!("Failed to serialize update mode: {}", e))?,
|
||||
);
|
||||
// Only lock the UI when the provisioning file came from a path that
|
||||
@@ -445,7 +441,7 @@ pub async fn set_update_mode(
|
||||
|
||||
store.set(
|
||||
UPDATE_MODE_KEY,
|
||||
serde_json::to_value(&mode)
|
||||
serde_json::to_value(mode)
|
||||
.map_err(|e| format!("Failed to serialize update mode: {}", e))?,
|
||||
);
|
||||
store
|
||||
|
||||
@@ -177,7 +177,7 @@ pub fn run() {
|
||||
});
|
||||
}
|
||||
|
||||
if let Err(err) = apply_provisioning_if_present(&app.handle()) {
|
||||
if let Err(err) = apply_provisioning_if_present(app.handle()) {
|
||||
add_log(format!("⚠️ Failed to apply provisioning file: {}", err));
|
||||
}
|
||||
|
||||
|
||||
@@ -269,6 +269,29 @@ export default defineConfig({
|
||||
node: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
// Desktop E2E harness: a standalone npm package (its own package.json and
|
||||
// lockfile), deliberately outside editor/ so its package.json cannot
|
||||
// hijack Tauri's app-dir resolution. Nothing resolves @app/* there - no
|
||||
// bundler, no tsconfig paths - so relative imports are the only option.
|
||||
// `browser`, `$` and `expect` are injected as globals by WebdriverIO.
|
||||
files: ["desktop-e2e/**/*.{js,mjs}"],
|
||||
globals: {
|
||||
...modernGlobals,
|
||||
browser: "readonly",
|
||||
$: "readonly",
|
||||
$$: "readonly",
|
||||
expect: "readonly",
|
||||
describe: "readonly",
|
||||
it: "readonly",
|
||||
},
|
||||
env: {
|
||||
node: true,
|
||||
},
|
||||
rules: {
|
||||
"no-restricted-imports": "off",
|
||||
},
|
||||
},
|
||||
{
|
||||
// Editor app source (excluding desktop): ban relative/src imports, ban
|
||||
// Tauri (desktop-only), and the shared-DS Mantine import ban.
|
||||
|
||||
Reference in New Issue
Block a user