Add Playwright/bootRun/test.sh tasks (#6244)

## Description

Consolidates Playwright running under cohesive Task namespaces, isolates
Playwright state from the developer's local working tree, and swaps CI's
frontend webserver from `vite` dev to `vite preview` against a pre-built
`dist/`.

### `e2e:*` namespace

Renames `.taskfiles/testing.yml` to `.taskfiles/e2e.yml` and
consolidates everything Playwright-related under one `e2e:` namespace:

- `e2e:stubbed` / `e2e:live` / `e2e:enterprise` / `e2e:cross-browser`:
project-specific runners
- `e2e:check` (no-Docker subset) and `e2e:check:all` (full)
- `e2e:oauth:up` / `:down`, `e2e:saml:up` / `:down`: symmetric lifecycle
for the keycloak compose stacks
- `e2e:install`: Playwright browser install
- `docker:test`: full Docker integration suite

The redundant `frontend:test:e2e:*` project shortcuts are removed. CI
workflows (`e2e-stubbed.yml`, `e2e-live.yml`, `build-enterprise.yml`,
`nightly.yml`) are updated to call the new task names.

### Isolated Playwright state

New `STIRLING_BASE_PATH` (and `-Dstirling.base-path=`) override in
`InstallationPathConfig` redirects the entire state tree (configs,
backups, customFiles, pipeline, logs) at startup. `task e2e:live` points
it at `.test-state/playwright/` (purged on every invocation) so the
suite never touches the developer's local DB, settings.yml or backups.

`task e2e:live` auto-spawns gradle, waits for `/api/v1/info/status` to
come up, runs Playwright, then tears down the whole backend process
tree.

### CI runs Playwright against `vite preview`

Builds the frontend up-front with `VITE_BUILD_FOR_PREVIEW=1` (forces
absolute base so deep SPA routes resolve `/assets/...`) and the
playwright `webServer` now uses `vite preview --port 5173 --strictPort`
in CI. Avoids the per-page on-demand transform cost that was blowing the
30s navigation timeout under `--workers=3` on
`all-tool-pages-load.spec.ts`. Local dev keeps `vite` dev for HMR.

### OAuth/SAML compose helpers

`start-oauth-test.sh` and `start-saml-test.sh` gain a `--license-key
<KEY>` (`-k`) flag so CI and scripted runs can skip the interactive
license prompt. `start-oauth-test.sh` also moves from `for arg in "$@"`
to a `while`-with-`shift` arg loop to support multi-arg flags
consistently with the SAML script.

### Backend gradlew unification

Drops the per-platform `cmd /c gradlew.bat` branches from `backend.yml`
and routes every gradle invocation through `bash gradlew`. Works
uniformly on Linux/macOS and Windows-with-Git-Bash.

### Compare.tsx flake fix (re-land of
[#6316](https://github.com/Stirling-Tools/Stirling-PDF/pull/6316))

Piggybacks Anthony's never-merged fix from #6316. Without it,
`e2e:stubbed` continues to flake under `--workers=3` on
`compare.spec.ts`'s second-upload case via a React "Maximum update depth
exceeded" infinite loop in the Compare auto-fill effect. CI traces from
recent failed runs match exactly; 10 local runs of `compare.spec.ts`
with `CI=1 --workers=3` pass cleanly with the fix applied.

---------

Co-authored-by: James Brunton <[email protected]>
This commit is contained in:
Anthony Stirling
2026-05-11 14:50:07 +00:00
committed by GitHub
co-authored by James Brunton
parent c059e13423
commit f60a075443
16 changed files with 399 additions and 152 deletions
+12 -4
View File
@@ -63,7 +63,15 @@ jobs:
- name: Install Task - name: Install Task
uses: go-task/setup-task@3be4020d41929789a01026e0e427a4321ce0ad44 # v2.0.0 uses: go-task/setup-task@3be4020d41929789a01026e0e427a4321ce0ad44 # v2.0.0
- name: Install Playwright (chromium only) - name: Install Playwright (chromium only)
run: task frontend:test:e2e:install -- chromium run: task e2e:install -- chromium
- name: Build frontend (needed for playwright's vite preview webServer)
# Enterprise tests target :8080 (Spring Boot's bundled frontend), but
# playwright's webServer config still launches `vite preview --port 5173`
# before any test run, which needs dist/ to exist. VITE_BUILD_FOR_PREVIEW
# forces absolute asset paths so vite preview can serve deep SPA routes.
env:
VITE_BUILD_FOR_PREVIEW: "1"
run: task frontend:build
- name: Resolve kubernetes.docker.internal to localhost - name: Resolve kubernetes.docker.internal to localhost
# The compose stacks set KC_HOSTNAME=kubernetes.docker.internal so # The compose stacks set KC_HOSTNAME=kubernetes.docker.internal so
@@ -140,7 +148,7 @@ jobs:
wait_for_backend wait_for_backend
- name: Run enterprise OAuth Playwright tests - name: Run enterprise OAuth Playwright tests
id: oauth-tests id: oauth-tests
run: task frontend:test:e2e -- --project=enterprise --grep "OAuth" run: task e2e:enterprise -- --grep "OAuth"
- name: Stop backend + tear down OAuth Keycloak - name: Stop backend + tear down OAuth Keycloak
if: always() if: always()
run: | run: |
@@ -213,7 +221,7 @@ jobs:
wait_for_backend wait_for_backend
- name: Run enterprise SAML Playwright tests - name: Run enterprise SAML Playwright tests
id: saml-tests id: saml-tests
run: task frontend:test:e2e -- --project=enterprise --grep "SAML" run: task e2e:enterprise -- --grep "SAML"
- name: Stop backend + tear down SAML Keycloak - name: Stop backend + tear down SAML Keycloak
if: always() if: always()
run: | run: |
@@ -243,7 +251,7 @@ jobs:
wait_for_backend wait_for_backend
- name: Run enterprise feature Playwright tests - name: Run enterprise feature Playwright tests
id: feature-tests id: feature-tests
run: task frontend:test:e2e -- --project=enterprise --grep "Enterprise license" run: task e2e:enterprise -- --grep "Enterprise license"
- name: Print backend log on failure - name: Print backend log on failure
if: failure() if: failure()
run: | run: |
+7 -39
View File
@@ -34,58 +34,26 @@ jobs:
- name: Install Task - name: Install Task
uses: go-task/setup-task@3be4020d41929789a01026e0e427a4321ce0ad44 # v2.0.0 uses: go-task/setup-task@3be4020d41929789a01026e0e427a4321ce0ad44 # v2.0.0
- name: Install Playwright (chromium only) - name: Install Playwright (chromium only)
run: task frontend:test:e2e:install -- chromium run: task e2e:install -- chromium
- name: Start Spring Boot backend (background) - name: Build frontend (production bundle for vite preview)
env: env:
# Suppress the analytics opt-in modal that fires on first admin login when VITE_BUILD_FOR_PREVIEW: "1"
# enableAnalytics is null (see Onboarding.tsx). The modal renders a Mantine run: task frontend:build
# overlay that intercepts pointer events on every tool page until dismissed,
# which causes every "click run button" assertion in the live suite to fail.
SYSTEM_ENABLEANALYTICS: "false"
# NOTE: SECURITY_INITIALLOGIN_USERNAME/PASSWORD are intentionally NOT set.
# The live-setup project's bootstrap spec performs the real first-login
# flow against the backend's default admin/stirling user, exercising the
# forced-password-change UI and leaving the DB at admin/adminadmin for
# the rest of the live suite. This is both real coverage of the first-
# login flow and a stronger seed than env-var-driven user creation.
run: |
nohup ./gradlew :stirling-pdf:bootRun > /tmp/backend.log 2>&1 &
echo $! > /tmp/backend.pid
- name: Wait for backend to become ready
run: |
start=$SECONDS
# 300 iterations × 2s = 10 minute ceiling
for i in $(seq 1 300); do
if curl -fsS http://localhost:8080/api/v1/info/status >/dev/null 2>&1; then
echo "Backend up after $((SECONDS - start))s"
exit 0
fi
sleep 2
done
echo "Backend did not become ready in $((SECONDS - start))s"
tail -200 /tmp/backend.log || true
exit 1
- name: Run live E2E tests (chromium) - name: Run live E2E tests (chromium)
id: live-tests id: live-tests
run: task frontend:test:e2e -- --project=live run: task e2e:live
- name: Print backend log on failure - name: Print backend log on failure
if: failure() && steps.live-tests.conclusion == 'failure' if: failure() && steps.live-tests.conclusion == 'failure'
run: | run: |
echo "::group::Spring Boot backend log (last 500 lines)" echo "::group::Spring Boot backend log (last 500 lines)"
tail -500 /tmp/backend.log || echo "no backend log found" tail -500 .test-state/playwright/backend.log || echo "no backend log found"
echo "::endgroup::" echo "::endgroup::"
- name: Stop backend
if: always()
run: |
if [ -f /tmp/backend.pid ]; then
kill "$(cat /tmp/backend.pid)" 2>/dev/null || true
fi
- name: Upload backend log - name: Upload backend log
if: always() if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with: with:
name: backend-log-live-${{ github.run_id }} name: backend-log-live-${{ github.run_id }}
path: /tmp/backend.log path: .test-state/playwright/backend.log
retention-days: 7 retention-days: 7
- name: Upload Playwright report - name: Upload Playwright report
if: always() if: always()
+6 -2
View File
@@ -28,9 +28,13 @@ jobs:
- name: Install Task - name: Install Task
uses: go-task/setup-task@3be4020d41929789a01026e0e427a4321ce0ad44 # v2.0.0 uses: go-task/setup-task@3be4020d41929789a01026e0e427a4321ce0ad44 # v2.0.0
- name: Install Playwright (chromium only) - name: Install Playwright (chromium only)
run: task frontend:test:e2e:install -- chromium run: task e2e:install -- chromium
- name: Build frontend (production bundle for vite preview)
env:
VITE_BUILD_FOR_PREVIEW: "1"
run: task frontend:build
- name: Run stubbed E2E tests (chromium) - name: Run stubbed E2E tests (chromium)
run: task frontend:test:e2e -- --project=stubbed --workers=3 run: task e2e:stubbed -- --workers=3
- name: Upload Playwright report - name: Upload Playwright report
if: always() if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
+2 -2
View File
@@ -35,10 +35,10 @@ jobs:
- name: Install Task - name: Install Task
uses: go-task/setup-task@3be4020d41929789a01026e0e427a4321ce0ad44 # v2.0.0 uses: go-task/setup-task@3be4020d41929789a01026e0e427a4321ce0ad44 # v2.0.0
- name: Install all Playwright browsers - name: Install all Playwright browsers
run: task frontend:test:e2e:install run: task e2e:install
- name: Run E2E tests (all browsers) - name: Run E2E tests (all browsers)
run: task frontend:test:e2e run: task e2e:cross-browser
- name: Upload Playwright report - name: Upload Playwright report
if: always() if: always()
+1
View File
@@ -32,6 +32,7 @@ exampleYmlFiles/stirling/
/testing/cucumber/junit/ /testing/cucumber/junit/
/testing/cucumber/report.html /testing/cucumber/report.html
/testing/.failed_tests /testing/.failed_tests
/.test-state/
SwaggerDoc.json SwaggerDoc.json
# Runtime storage for uploaded files and user data (not Java source code) # Runtime storage for uploaded files and user data (not Java source code)
+21 -48
View File
@@ -1,5 +1,8 @@
version: '3' version: '3'
# All gradle invocations go through `bash gradlew` (the sh wrapper) so they work
# uniformly on Linux/macOS (native bash) and Windows-with-Git-Bash.
tasks: tasks:
dev: dev:
desc: "Start backend dev server" desc: "Start backend dev server"
@@ -10,58 +13,43 @@ tasks:
env: env:
SERVER_PORT: '{{.PORT}}' SERVER_PORT: '{{.PORT}}'
cmds: cmds:
- cmd: '{{if .AIENGINE_URL}}set "AIENGINE_URL={{.AIENGINE_URL}}" && set "AIENGINE_ENABLED=true" && {{end}}cmd /c gradlew.bat :stirling-pdf:bootRun' - '{{if .AIENGINE_URL}}AIENGINE_URL={{.AIENGINE_URL}} AIENGINE_ENABLED=true {{end}}bash gradlew :stirling-pdf:bootRun'
platforms: [windows]
- cmd: '{{if .AIENGINE_URL}}AIENGINE_URL={{.AIENGINE_URL}} AIENGINE_ENABLED=true {{end}}./gradlew :stirling-pdf:bootRun' dev:bundled:
platforms: [linux, darwin] desc: "Clean + bootRun with frontend bundled into the backend (single :8080 server)"
ignore_error: true
cmds:
- bash gradlew clean bootRun -PbuildWithFrontend=true
build: build:
desc: "Full backend build" desc: "Full backend build"
cmds: cmds:
- cmd: cmd /c gradlew.bat clean build - bash gradlew clean build
platforms: [windows]
- cmd: ./gradlew clean build
platforms: [linux, darwin]
build:fast: build:fast:
desc: "Build without tests" desc: "Build without tests"
cmds: cmds:
- cmd: cmd /c gradlew.bat clean build -x test - bash gradlew clean build -x test
platforms: [windows]
- cmd: ./gradlew clean build -x test
platforms: [linux, darwin]
build:ci: build:ci:
desc: "Build for CI (formatting checked separately)" desc: "Build for CI (formatting checked separately)"
cmds: cmds:
- cmd: cmd /c gradlew.bat build -PnoSpotless - bash gradlew build -PnoSpotless
platforms: [windows]
- cmd: ./gradlew build -PnoSpotless
platforms: [linux, darwin]
test: test:
desc: "Run backend tests" desc: "Run backend tests"
cmds: cmds:
- cmd: cmd /c gradlew.bat test - bash gradlew test
platforms: [windows]
- cmd: ./gradlew test
platforms: [linux, darwin]
format: format:
desc: "Auto-fix code formatting" desc: "Auto-fix code formatting"
cmds: cmds:
- cmd: cmd /c gradlew.bat spotlessApply - bash gradlew spotlessApply
platforms: [windows]
- cmd: ./gradlew spotlessApply
platforms: [linux, darwin]
format:check: format:check:
desc: "Check code formatting" desc: "Check code formatting"
cmds: cmds:
- cmd: cmd /c gradlew.bat spotlessCheck - bash gradlew spotlessCheck
platforms: [windows]
- cmd: ./gradlew spotlessCheck
platforms: [linux, darwin]
fix: fix:
desc: "Auto-fix backend" desc: "Auto-fix backend"
@@ -71,10 +59,7 @@ tasks:
swagger: swagger:
desc: "Generate OpenAPI docs" desc: "Generate OpenAPI docs"
cmds: cmds:
- cmd: cmd /c gradlew.bat :stirling-pdf:copySwaggerDoc - bash gradlew :stirling-pdf:copySwaggerDoc
platforms: [windows]
- cmd: ./gradlew :stirling-pdf:copySwaggerDoc
platforms: [linux, darwin]
sources: sources:
- app/core/src/main/java/**/*.java - app/core/src/main/java/**/*.java
- app/proprietary/src/main/java/**/*.java - app/proprietary/src/main/java/**/*.java
@@ -92,31 +77,19 @@ tasks:
desc: "Print project version" desc: "Print project version"
silent: true silent: true
cmds: cmds:
- cmd: cmd /c gradlew.bat printVersion --quiet | tail -1 - bash gradlew printVersion --quiet | tail -1
platforms: [windows]
- cmd: ./gradlew printVersion --quiet | tail -1
platforms: [linux, darwin]
licenses:check: licenses:check:
desc: "Check dependency licenses" desc: "Check dependency licenses"
cmds: cmds:
- cmd: cmd /c gradlew.bat checkLicense --no-parallel - bash gradlew checkLicense --no-parallel
platforms: [windows]
- cmd: ./gradlew checkLicense --no-parallel
platforms: [linux, darwin]
licenses:generate: licenses:generate:
desc: "Check and generate dependency license report" desc: "Check and generate dependency license report"
cmds: cmds:
- cmd: cmd /c gradlew.bat checkLicense generateLicenseReport --no-parallel - bash gradlew checkLicense generateLicenseReport --no-parallel
platforms: [windows]
- cmd: ./gradlew checkLicense generateLicenseReport --no-parallel
platforms: [linux, darwin]
clean: clean:
desc: "Clean build artifacts" desc: "Clean build artifacts"
cmds: cmds:
- cmd: cmd /c gradlew.bat clean - bash gradlew clean
platforms: [windows]
- cmd: ./gradlew clean
platforms: [linux, darwin]
+6
View File
@@ -55,3 +55,9 @@ tasks:
desc: "Tail docker compose logs" desc: "Tail docker compose logs"
cmds: cmds:
- docker compose -f {{.COMPOSE_DIR}}/docker-compose.yml logs -f - docker compose -f {{.COMPOSE_DIR}}/docker-compose.yml logs -f
test:
desc: "Run full Docker integration test suite (builds all variants and tests them)"
ignore_error: true
cmds:
- bash testing/test.sh {{.CLI_ARGS}}
+204
View File
@@ -0,0 +1,204 @@
version: '3'
tasks:
install:
desc: "Install Playwright browsers"
dir: frontend
deps: [ ':frontend:install' ]
cmds:
- npx playwright install {{.CLI_ARGS}} --with-deps
stubbed:
desc: "Run stubbed E2E tests"
dir: frontend
deps: [ ':frontend:prepare' ]
cmds:
- npx playwright test --project=stubbed {{.CLI_ARGS}}
live:
desc: "Run live E2E tests"
summary: |
Auto-spawns a Spring Boot backend with isolated state under
.test-state/playwright/ (purged on every run) so the suite doesn't
touch your local DB, settings.yml, backups or customFiles. The
backend is stopped automatically when the runner exits.
Pass extra Playwright flags via -- :
task e2e:live -- --headed --grep merge
deps:
- live:backend
- live:runner
live:backend:
internal: true
ignore_error: true
vars:
BASE_DIR: '{{.ROOT_DIR}}/.test-state/playwright'
env:
STIRLING_BASE_PATH: '{{.BASE_DIR}}'
# Suppress the analytics opt-in modal that fires on first admin login.
# It renders a Mantine overlay that intercepts pointer events and blocks
# FirstLoginSlide from rendering, so the bootstrap spec times out waiting
# for the password-change prompt.
SYSTEM_ENABLEANALYTICS: "false"
# NOTE: SECURITY_INITIALLOGIN_USERNAME/PASSWORD are intentionally NOT set.
# The live-setup project's bootstrap spec performs the real first-login
# flow against the backend's default admin/stirling user, exercising the
# forced-password-change UI and leaving the DB at admin/adminadmin for
# the rest of the live suite. This is both real coverage of the first-
# login flow and a stronger seed than env-var-driven user creation.
cmds:
# Wrapped in `bash -c` because Task's embedded shell (mvdan/sh) doesn't
# support `&` + `$!` + `wait` reliably (gradle was never actually
# backgrounded, so `$!` recorded a stale PID and the runner saw an
# immediate "backend died"). Real bash backgrounds gradle properly and
# gives us a stable PID to clean up.
- |
bash -c '
set -e
rm -rf "{{.BASE_DIR}}"
mkdir -p "{{.BASE_DIR}}"
# Background gradle and record its PID so the runner can clean up
# the exact process tree (wrapper + forked Spring Boot JVM) without
# resorting to fuzzy `pkill -f` patterns. `wait` keeps this script
# alive for the lifetime of gradle so Task'"'"'s parallel deps stay
# synchronised.
bash gradlew :stirling-pdf:bootRun > "{{.BASE_DIR}}/backend.log" 2>&1 &
GRADLE_PID=$!
echo $GRADLE_PID > "{{.BASE_DIR}}/backend.pid"
wait $GRADLE_PID
'
live:runner:
internal: true
deps: [ ':frontend:prepare' ]
dir: frontend
vars:
BASE_DIR: '{{.ROOT_DIR}}/.test-state/playwright'
cmds:
# Wrapped in `bash -c` because Task's embedded shell (mvdan/sh) only
# supports `EXIT`/`ERR` in `trap`, not `INT`/`TERM`. CLI_ARGS are
# passed positionally so quoted args like --grep "foo bar" survive.
- |
bash -c '
set +e
# bootRun forks a separate Spring Boot JVM as a child of the gradle
# wrapper; killing only the wrapper leaves that JVM orphaned holding
# :8080. Walk the recorded PID'"'"'s process tree via pgrep so we kill
# every descendant, then lsof as a last-resort backstop.
kill_tree() {
local pid=$1
for child in $(pgrep -P "$pid" 2>/dev/null); do
kill_tree "$child"
done
kill "$pid" 2>/dev/null || true
}
cleanup() {
if [ -f "{{.BASE_DIR}}/backend.pid" ]; then
echo "Stopping backend..."
kill_tree "$(cat "{{.BASE_DIR}}/backend.pid")"
sleep 1
lsof -ti:8080 2>/dev/null | xargs kill 2>/dev/null || true
echo "Backend stopped"
fi
}
# Trap so cleanup runs on every exit path: tests pass, tests fail,
# backend never starts, user Ctrl-Cs. Without this the gradle JVM
# is left orphaned holding :8080 whenever the wait loop times out.
trap cleanup EXIT INT TERM
echo "Waiting for backend on :8080 (up to 10min; first compile can be slow)..."
WAITED=0
while [ $WAITED -lt 600 ]; do
if curl -sf http://localhost:8080/api/v1/info/status 2>/dev/null | grep -q UP; then
echo "Backend ready, starting Playwright"
break
fi
# Bail early if the backend already died: no point waiting 10min.
if [ -f "{{.BASE_DIR}}/backend.pid" ]; then
BACKEND_PID=$(cat "{{.BASE_DIR}}/backend.pid")
if ! kill -0 "$BACKEND_PID" 2>/dev/null; then
echo "Backend process exited before becoming ready; last 100 log lines:"
tail -100 "{{.BASE_DIR}}/backend.log" 2>/dev/null || true
exit 1
fi
fi
sleep 2
WAITED=$((WAITED + 2))
done
if [ $WAITED -ge 600 ]; then
echo "Backend did not become ready within 10min, aborting"
exit 1
fi
npx playwright test --project=live "$@"
' bash {{.CLI_ARGS}}
enterprise:
desc: "Run enterprise E2E tests"
summary: |
Requires an already-running keycloak compose stack on :8080. Bring one
up first with:
task e2e:oauth:up
task e2e:saml:up
dir: frontend
deps: [ ':frontend:prepare' ]
cmds:
- npx playwright test --project=enterprise {{.CLI_ARGS}}
cross-browser:
desc: "Run stubbed E2E tests on Chromium, Firefox, and WebKit"
dir: frontend
deps: [ ':frontend:prepare' ]
cmds:
- npx playwright test --project=stubbed --project=stubbed-firefox --project=stubbed-webkit {{.CLI_ARGS}}
check:
desc: "Run all E2E tests not requiring Docker"
cmds:
- task: stubbed
- task: live
check:all:
desc: "Run all E2E tests"
summary: |
Includes the enterprise project, which requires a keycloak compose
stack on :8080. Bring one up first with:
task e2e:oauth:up
task e2e:saml:up
cmds:
- task: stubbed
- task: live
- task: enterprise
oauth:up:
desc: "Start the OAuth keycloak test environment"
summary: |
Set LICENSE_KEY=<KEY> to skip the interactive license prompt:
task e2e:oauth:up LICENSE_KEY=abc123
Pass extra flags via -- :
task e2e:oauth:up -- --auto --nobuild
ignore_error: true
cmds:
- bash testing/compose/start-oauth-test.sh {{if .LICENSE_KEY}}--license-key "{{.LICENSE_KEY}}"{{end}} {{.CLI_ARGS}}
oauth:down:
desc: "Stop the OAuth keycloak test environment"
cmds:
- docker compose -f testing/compose/docker-compose-keycloak-oauth.yml down -v
saml:up:
desc: "Start the SAML keycloak test environment"
summary: |
Set LICENSE_KEY=<KEY> to skip the interactive license prompt:
task e2e:saml:up LICENSE_KEY=abc123
Pass extra flags via -- :
task e2e:saml:up -- --auto --with-storage --nobuild --language sv-SE
ignore_error: true
cmds:
- bash testing/compose/start-saml-test.sh {{if .LICENSE_KEY}}--license-key "{{.LICENSE_KEY}}"{{end}} {{.CLI_ARGS}}
saml:down:
desc: "Stop the SAML keycloak test environment"
cmds:
- docker compose -f testing/compose/docker-compose-keycloak-saml.yml down -v
-12
View File
@@ -284,18 +284,6 @@ tasks:
cmds: cmds:
- npx vitest --coverage - npx vitest --coverage
test:e2e:
desc: "Run E2E tests"
deps: [prepare]
cmds:
- npx playwright test {{.CLI_ARGS}}
test:e2e:install:
desc: "Install E2E test browsers"
deps: [install]
cmds:
- npx playwright install {{.CLI_ARGS}} --with-deps
# ============================================================ # ============================================================
# Code Generation # Code Generation
# ============================================================ # ============================================================
+3
View File
@@ -22,6 +22,9 @@ includes:
desktop: desktop:
taskfile: .taskfiles/desktop.yml taskfile: .taskfiles/desktop.yml
dir: frontend dir: frontend
e2e:
taskfile: .taskfiles/e2e.yml
dir: .
tasks: tasks:
# ============================================================ # ============================================================
@@ -59,6 +59,18 @@ public class InstallationPathConfig {
} }
private static String initializeBasePath() { private static String initializeBasePath() {
// Allow tests / harnesses to redirect the entire state tree (configs,
// backups, customFiles, pipeline, logs) to an isolated location via
// -Dstirling.base-path=... or STIRLING_BASE_PATH=... so a Playwright
// run never touches a developer's working state.
String override = System.getProperty("stirling.base-path");
if (override == null || override.isBlank()) {
override = System.getenv("STIRLING_BASE_PATH");
}
if (override != null && !override.isBlank()) {
boolean hasTrailingSeparator = override.endsWith("/") || override.endsWith("\\");
return hasTrailingSeparator ? override : override + File.separator;
}
return "." + File.separator; return "." + File.separator;
} }
+7 -1
View File
@@ -96,7 +96,13 @@ export default defineConfig({
], ],
webServer: { webServer: {
command: "npx vite", // In CI, serve a pre-built `dist/` via `vite preview` so the heavy tool
// pages don't pay vite's on-demand transform cost on first hit (which
// blew the 30s navigationTimeout under --workers=3 — see
// all-tool-pages-load.spec.ts). Locally, keep `vite` dev for HMR.
command: process.env.CI
? "npx vite preview --port 5173 --strictPort"
: "npx vite",
url: "http://localhost:5173", url: "http://localhost:5173",
reuseExistingServer: !process.env.CI, reuseExistingServer: !process.env.CI,
timeout: 120_000, timeout: 120_000,
+24 -18
View File
@@ -157,33 +157,39 @@ const Compare = (props: BaseToolProps) => {
} }
}, []); }, []);
// Track previous file count to detect the transition to exactly 2 files. // Keep slot params in sync with the current workbench/selection state.
const prevAllIdsLengthRef = useRef<number | null>(null); //
// The effect must be idempotent: it can fire repeatedly during an upload
// Auto-fill slots when the file count first reaches 2; respect manual picker changes after that. // burst (ADD_FILES, SET_SELECTED_FILES, and downstream dispatches all land
// in quick succession), and any "did the count transition?" tracking via
// a ref races with React's batched re-renders under load. With workers=3
// in CI, that race surfaced as a "Maximum update depth exceeded" crash
// when the second slot was filled. So the implementation derives the
// target params purely from current state and bails when there's nothing
// to change.
useEffect(() => { useEffect(() => {
const selectedIds = fileState.ui.selectedFileIds as FileId[]; const selectedIds = fileState.ui.selectedFileIds as FileId[];
const allIds = fileState.files.ids as FileId[]; const allIds = fileState.files.ids as FileId[];
const prevLength = prevAllIdsLengthRef.current;
prevAllIdsLengthRef.current = allIds.length;
if (allIds.length === 2 && prevLength !== 2) {
// Transitioned to exactly 2 files — auto-fill both slots.
const [firstId, secondId] = allIds as [FileId, FileId];
fileActions.setSelectedFiles([firstId, secondId]);
base.params.setParameters((prev) => {
if (prev.baseFileId === firstId && prev.comparisonFileId === secondId)
return prev;
return { ...prev, baseFileId: firstId, comparisonFileId: secondId };
});
return;
}
if (selectedIds.length > 2) { if (selectedIds.length > 2) {
fileActions.setSelectedFiles(selectedIds.slice(0, 2) as FileId[]); fileActions.setSelectedFiles(selectedIds.slice(0, 2) as FileId[]);
return; return;
} }
// When exactly 2 files are loaded but the selection doesn't cover both,
// promote the selection so Compare can run. Skips when the selection is
// already correct so we don't dispatch a fresh array reference and
// retrigger this effect. Covers the upload window where ADD_FILES has
// landed but SET_SELECTED_FILES is still in flight.
if (allIds.length === 2) {
const selectionMatchesAllIds =
selectedIds.length === 2 &&
allIds.every((id) => selectedIds.includes(id));
if (!selectionMatchesAllIds) {
fileActions.setSelectedFiles([...allIds] as FileId[]);
}
}
// Sticky slot mapping: preserve the current slot if its file is still selected, // Sticky slot mapping: preserve the current slot if its file is still selected,
// otherwise fill empty slots from the selection in order. This lets users add/remove // otherwise fill empty slots from the selection in order. This lets users add/remove
// files from the selection without the other slot being clobbered. // files from the selection without the other slot being clobbered.
+33 -12
View File
@@ -49,6 +49,21 @@ export default defineConfig(async ({ mode }) => {
xfwd: true, xfwd: true,
}; };
// Shared between `vite` (dev) and `vite preview` (production-build serve, used
// in CI/E2E) so the live test suite still resolves /api → :8080.
const backendProxyConfig =
effectiveMode === "desktop"
? undefined
: {
"/api": backendProxy,
"/oauth2": backendProxy,
"/saml2": backendProxy,
"/login/oauth2": backendProxy,
"/login/saml2": backendProxy,
"/swagger-ui": backendProxy,
"/v1/api-docs": backendProxy,
};
return { return {
plugins: [ plugins: [
react(), react(),
@@ -107,19 +122,25 @@ export default defineConfig(async ({ mode }) => {
ignored: ["**/src-tauri/**"], ignored: ["**/src-tauri/**"],
}, },
// Only use proxy in web mode - Tauri handles backend connections directly // Only use proxy in web mode - Tauri handles backend connections directly
proxy: proxy: backendProxyConfig,
effectiveMode === "desktop"
? undefined
: {
"/api": backendProxy,
"/oauth2": backendProxy,
"/saml2": backendProxy,
"/login/oauth2": backendProxy,
"/login/saml2": backendProxy,
"/swagger-ui": backendProxy,
"/v1/api-docs": backendProxy,
}, },
preview: {
host: true,
port: 5173,
strictPort: true,
proxy: backendProxyConfig,
}, },
base: env.RUN_SUBPATH ? `/${env.RUN_SUBPATH}` : "./", // base: "./" produces relative asset URLs which work when dist/ is served
// at any path (e.g. Spring Boot bundling the frontend at /). But under
// `vite preview` for deep SPA routes (e.g. /workflow/sign/<token>), the
// browser resolves ./assets/X.js relative to the current path → 404, then
// SPA fallback returns index.html as text/html and React never mounts.
// VITE_BUILD_FOR_PREVIEW=1 (set by the CI playwright steps) overrides to
// an absolute base so deep-route asset paths resolve to /assets/...
base: env.RUN_SUBPATH
? `/${env.RUN_SUBPATH}`
: process.env.VITE_BUILD_FOR_PREVIEW === "1"
? "/"
: "./",
}; };
}); });
+29 -4
View File
@@ -15,27 +15,52 @@ echo ""
AUTO_LOGIN=false AUTO_LOGIN=false
FORCE_ALL_LOGIN=false FORCE_ALL_LOGIN=false
COMPOSE_UP_ARGS=(-d --build) COMPOSE_UP_ARGS=(-d --build)
for arg in "$@"; do while [[ $# -gt 0 ]]; do
case "$arg" in case "$1" in
--auto) --auto)
AUTO_LOGIN=true AUTO_LOGIN=true
shift
;; ;;
--all) --all)
FORCE_ALL_LOGIN=true FORCE_ALL_LOGIN=true
shift
;; ;;
--nobuild) --nobuild)
COMPOSE_UP_ARGS=(-d) COMPOSE_UP_ARGS=(-d)
shift
;;
--license-key)
if [[ -z "${2:-}" ]]; then
echo -e "${RED}Missing value for --license-key${NC}"
exit 1
fi
export PREMIUM_KEY="$2"
shift 2
;;
--license-key=*)
export PREMIUM_KEY="${1#*=}"
shift
;;
-k)
if [[ -z "${2:-}" ]]; then
echo -e "${RED}Missing value for -k${NC}"
exit 1
fi
export PREMIUM_KEY="$2"
shift 2
;; ;;
-h|--help) -h|--help)
echo "Usage: $0 [--auto] [--nobuild]" echo "Usage: $0 [--auto] [--all] [--nobuild] [--license-key <KEY>]"
echo "" echo ""
echo " --auto Enable SSO auto-login and force OAuth-only login method" echo " --auto Enable SSO auto-login and force OAuth-only login method"
echo " --all Force login method to allow all providers (overrides --auto)" echo " --all Force login method to allow all providers (overrides --auto)"
echo " --nobuild Skip building images (use existing images)" echo " --nobuild Skip building images (use existing images)"
echo " --license-key <KEY> Premium license key (skips the interactive prompt)"
echo " Equivalent to setting PREMIUM_KEY in the environment."
exit 0 exit 0
;; ;;
*) *)
echo -e "${RED}Unknown option: $arg${NC}" echo -e "${RED}Unknown option: $1${NC}"
exit 1 exit 1
;; ;;
esac esac
+23 -1
View File
@@ -55,14 +55,36 @@ while [[ $# -gt 0 ]]; do
DEFAULT_LANGUAGE="$2" DEFAULT_LANGUAGE="$2"
shift 2 shift 2
;; ;;
--license-key)
if [[ -z "${2:-}" ]]; then
echo -e "${RED}Missing value for --license-key${NC}"
exit 1
fi
export PREMIUM_KEY="$2"
shift 2
;;
--license-key=*)
export PREMIUM_KEY="${1#*=}"
shift
;;
-k)
if [[ -z "${2:-}" ]]; then
echo -e "${RED}Missing value for -k${NC}"
exit 1
fi
export PREMIUM_KEY="$2"
shift 2
;;
-h|--help) -h|--help)
echo "Usage: $0 [--auto] [--with-storage] [--nobuild] [--language <locale>]" echo "Usage: $0 [--auto] [--with-storage] [--nobuild] [--language <locale>] [--license-key <KEY>]"
echo "" echo ""
echo " --auto Enable SSO auto-login and force SAML-only login method" echo " --auto Enable SSO auto-login and force SAML-only login method"
echo " --with-storage Enable the file storage + link-sharing feature" echo " --with-storage Enable the file storage + link-sharing feature"
echo " (required to test /share/<token> flows)" echo " (required to test /share/<token> flows)"
echo " --nobuild Skip building images (use existing images)" echo " --nobuild Skip building images (use existing images)"
echo " --language Set system default locale (e.g. de-DE, sv-SE)" echo " --language Set system default locale (e.g. de-DE, sv-SE)"
echo " --license-key <KEY> Premium license key (skips the interactive prompt)"
echo " Equivalent to setting PREMIUM_KEY in the environment."
exit 0 exit 0
;; ;;
*) *)