mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-14 10:34:06 +02:00
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:
co-authored by
James Brunton
parent
c059e13423
commit
f60a075443
@@ -63,7 +63,15 @@ jobs:
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@3be4020d41929789a01026e0e427a4321ce0ad44 # v2.0.0
|
||||
- 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
|
||||
# The compose stacks set KC_HOSTNAME=kubernetes.docker.internal so
|
||||
@@ -140,7 +148,7 @@ jobs:
|
||||
wait_for_backend
|
||||
- name: Run enterprise OAuth Playwright 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
|
||||
if: always()
|
||||
run: |
|
||||
@@ -213,7 +221,7 @@ jobs:
|
||||
wait_for_backend
|
||||
- name: Run enterprise SAML Playwright 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
|
||||
if: always()
|
||||
run: |
|
||||
@@ -243,7 +251,7 @@ jobs:
|
||||
wait_for_backend
|
||||
- name: Run enterprise feature Playwright 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
|
||||
if: failure()
|
||||
run: |
|
||||
|
||||
@@ -34,58 +34,26 @@ jobs:
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@3be4020d41929789a01026e0e427a4321ce0ad44 # v2.0.0
|
||||
- name: Install Playwright (chromium only)
|
||||
run: task frontend:test:e2e:install -- chromium
|
||||
- name: Start Spring Boot backend (background)
|
||||
run: task e2e:install -- chromium
|
||||
- name: Build frontend (production bundle for vite preview)
|
||||
env:
|
||||
# Suppress the analytics opt-in modal that fires on first admin login when
|
||||
# enableAnalytics is null (see Onboarding.tsx). The modal renders a Mantine
|
||||
# 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
|
||||
VITE_BUILD_FOR_PREVIEW: "1"
|
||||
run: task frontend:build
|
||||
- name: Run live E2E tests (chromium)
|
||||
id: live-tests
|
||||
run: task frontend:test:e2e -- --project=live
|
||||
run: task e2e:live
|
||||
- name: Print backend log on failure
|
||||
if: failure() && steps.live-tests.conclusion == 'failure'
|
||||
run: |
|
||||
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::"
|
||||
- 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
|
||||
if: always()
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: backend-log-live-${{ github.run_id }}
|
||||
path: /tmp/backend.log
|
||||
path: .test-state/playwright/backend.log
|
||||
retention-days: 7
|
||||
- name: Upload Playwright report
|
||||
if: always()
|
||||
|
||||
@@ -28,9 +28,13 @@ jobs:
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@3be4020d41929789a01026e0e427a4321ce0ad44 # v2.0.0
|
||||
- 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)
|
||||
run: task frontend:test:e2e -- --project=stubbed --workers=3
|
||||
run: task e2e:stubbed -- --workers=3
|
||||
- name: Upload Playwright report
|
||||
if: always()
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
|
||||
@@ -35,10 +35,10 @@ jobs:
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@3be4020d41929789a01026e0e427a4321ce0ad44 # v2.0.0
|
||||
- name: Install all Playwright browsers
|
||||
run: task frontend:test:e2e:install
|
||||
run: task e2e:install
|
||||
|
||||
- name: Run E2E tests (all browsers)
|
||||
run: task frontend:test:e2e
|
||||
run: task e2e:cross-browser
|
||||
|
||||
- name: Upload Playwright report
|
||||
if: always()
|
||||
|
||||
@@ -32,6 +32,7 @@ exampleYmlFiles/stirling/
|
||||
/testing/cucumber/junit/
|
||||
/testing/cucumber/report.html
|
||||
/testing/.failed_tests
|
||||
/.test-state/
|
||||
SwaggerDoc.json
|
||||
|
||||
# Runtime storage for uploaded files and user data (not Java source code)
|
||||
|
||||
+21
-48
@@ -1,5 +1,8 @@
|
||||
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:
|
||||
dev:
|
||||
desc: "Start backend dev server"
|
||||
@@ -10,58 +13,43 @@ tasks:
|
||||
env:
|
||||
SERVER_PORT: '{{.PORT}}'
|
||||
cmds:
|
||||
- cmd: '{{if .AIENGINE_URL}}set "AIENGINE_URL={{.AIENGINE_URL}}" && set "AIENGINE_ENABLED=true" && {{end}}cmd /c gradlew.bat :stirling-pdf:bootRun'
|
||||
platforms: [windows]
|
||||
- cmd: '{{if .AIENGINE_URL}}AIENGINE_URL={{.AIENGINE_URL}} AIENGINE_ENABLED=true {{end}}./gradlew :stirling-pdf:bootRun'
|
||||
platforms: [linux, darwin]
|
||||
- '{{if .AIENGINE_URL}}AIENGINE_URL={{.AIENGINE_URL}} AIENGINE_ENABLED=true {{end}}bash gradlew :stirling-pdf:bootRun'
|
||||
|
||||
dev:bundled:
|
||||
desc: "Clean + bootRun with frontend bundled into the backend (single :8080 server)"
|
||||
ignore_error: true
|
||||
cmds:
|
||||
- bash gradlew clean bootRun -PbuildWithFrontend=true
|
||||
|
||||
build:
|
||||
desc: "Full backend build"
|
||||
cmds:
|
||||
- cmd: cmd /c gradlew.bat clean build
|
||||
platforms: [windows]
|
||||
- cmd: ./gradlew clean build
|
||||
platforms: [linux, darwin]
|
||||
- bash gradlew clean build
|
||||
|
||||
build:fast:
|
||||
desc: "Build without tests"
|
||||
cmds:
|
||||
- cmd: cmd /c gradlew.bat clean build -x test
|
||||
platforms: [windows]
|
||||
- cmd: ./gradlew clean build -x test
|
||||
platforms: [linux, darwin]
|
||||
- bash gradlew clean build -x test
|
||||
|
||||
build:ci:
|
||||
desc: "Build for CI (formatting checked separately)"
|
||||
cmds:
|
||||
- cmd: cmd /c gradlew.bat build -PnoSpotless
|
||||
platforms: [windows]
|
||||
- cmd: ./gradlew build -PnoSpotless
|
||||
platforms: [linux, darwin]
|
||||
- bash gradlew build -PnoSpotless
|
||||
|
||||
test:
|
||||
desc: "Run backend tests"
|
||||
cmds:
|
||||
- cmd: cmd /c gradlew.bat test
|
||||
platforms: [windows]
|
||||
- cmd: ./gradlew test
|
||||
platforms: [linux, darwin]
|
||||
- bash gradlew test
|
||||
|
||||
format:
|
||||
desc: "Auto-fix code formatting"
|
||||
cmds:
|
||||
- cmd: cmd /c gradlew.bat spotlessApply
|
||||
platforms: [windows]
|
||||
- cmd: ./gradlew spotlessApply
|
||||
platforms: [linux, darwin]
|
||||
- bash gradlew spotlessApply
|
||||
|
||||
format:check:
|
||||
desc: "Check code formatting"
|
||||
cmds:
|
||||
- cmd: cmd /c gradlew.bat spotlessCheck
|
||||
platforms: [windows]
|
||||
- cmd: ./gradlew spotlessCheck
|
||||
platforms: [linux, darwin]
|
||||
- bash gradlew spotlessCheck
|
||||
|
||||
fix:
|
||||
desc: "Auto-fix backend"
|
||||
@@ -71,10 +59,7 @@ tasks:
|
||||
swagger:
|
||||
desc: "Generate OpenAPI docs"
|
||||
cmds:
|
||||
- cmd: cmd /c gradlew.bat :stirling-pdf:copySwaggerDoc
|
||||
platforms: [windows]
|
||||
- cmd: ./gradlew :stirling-pdf:copySwaggerDoc
|
||||
platforms: [linux, darwin]
|
||||
- bash gradlew :stirling-pdf:copySwaggerDoc
|
||||
sources:
|
||||
- app/core/src/main/java/**/*.java
|
||||
- app/proprietary/src/main/java/**/*.java
|
||||
@@ -92,31 +77,19 @@ tasks:
|
||||
desc: "Print project version"
|
||||
silent: true
|
||||
cmds:
|
||||
- cmd: cmd /c gradlew.bat printVersion --quiet | tail -1
|
||||
platforms: [windows]
|
||||
- cmd: ./gradlew printVersion --quiet | tail -1
|
||||
platforms: [linux, darwin]
|
||||
- bash gradlew printVersion --quiet | tail -1
|
||||
|
||||
licenses:check:
|
||||
desc: "Check dependency licenses"
|
||||
cmds:
|
||||
- cmd: cmd /c gradlew.bat checkLicense --no-parallel
|
||||
platforms: [windows]
|
||||
- cmd: ./gradlew checkLicense --no-parallel
|
||||
platforms: [linux, darwin]
|
||||
- bash gradlew checkLicense --no-parallel
|
||||
|
||||
licenses:generate:
|
||||
desc: "Check and generate dependency license report"
|
||||
cmds:
|
||||
- cmd: cmd /c gradlew.bat checkLicense generateLicenseReport --no-parallel
|
||||
platforms: [windows]
|
||||
- cmd: ./gradlew checkLicense generateLicenseReport --no-parallel
|
||||
platforms: [linux, darwin]
|
||||
- bash gradlew checkLicense generateLicenseReport --no-parallel
|
||||
|
||||
clean:
|
||||
desc: "Clean build artifacts"
|
||||
cmds:
|
||||
- cmd: cmd /c gradlew.bat clean
|
||||
platforms: [windows]
|
||||
- cmd: ./gradlew clean
|
||||
platforms: [linux, darwin]
|
||||
- bash gradlew clean
|
||||
|
||||
@@ -55,3 +55,9 @@ tasks:
|
||||
desc: "Tail docker compose logs"
|
||||
cmds:
|
||||
- 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}}
|
||||
|
||||
@@ -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
|
||||
@@ -284,18 +284,6 @@ tasks:
|
||||
cmds:
|
||||
- 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
|
||||
# ============================================================
|
||||
|
||||
@@ -22,6 +22,9 @@ includes:
|
||||
desktop:
|
||||
taskfile: .taskfiles/desktop.yml
|
||||
dir: frontend
|
||||
e2e:
|
||||
taskfile: .taskfiles/e2e.yml
|
||||
dir: .
|
||||
|
||||
tasks:
|
||||
# ============================================================
|
||||
|
||||
+12
@@ -59,6 +59,18 @@ public class InstallationPathConfig {
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -96,7 +96,13 @@ export default defineConfig({
|
||||
],
|
||||
|
||||
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",
|
||||
reuseExistingServer: !process.env.CI,
|
||||
timeout: 120_000,
|
||||
|
||||
@@ -157,33 +157,39 @@ const Compare = (props: BaseToolProps) => {
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Track previous file count to detect the transition to exactly 2 files.
|
||||
const prevAllIdsLengthRef = useRef<number | null>(null);
|
||||
|
||||
// Auto-fill slots when the file count first reaches 2; respect manual picker changes after that.
|
||||
// Keep slot params in sync with the current workbench/selection state.
|
||||
//
|
||||
// The effect must be idempotent: it can fire repeatedly during an upload
|
||||
// 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(() => {
|
||||
const selectedIds = fileState.ui.selectedFileIds 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) {
|
||||
fileActions.setSelectedFiles(selectedIds.slice(0, 2) as FileId[]);
|
||||
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,
|
||||
// otherwise fill empty slots from the selection in order. This lets users add/remove
|
||||
// files from the selection without the other slot being clobbered.
|
||||
|
||||
+34
-13
@@ -49,6 +49,21 @@ export default defineConfig(async ({ mode }) => {
|
||||
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 {
|
||||
plugins: [
|
||||
react(),
|
||||
@@ -107,19 +122,25 @@ export default defineConfig(async ({ mode }) => {
|
||||
ignored: ["**/src-tauri/**"],
|
||||
},
|
||||
// Only use proxy in web mode - Tauri handles backend connections directly
|
||||
proxy:
|
||||
effectiveMode === "desktop"
|
||||
? undefined
|
||||
: {
|
||||
"/api": backendProxy,
|
||||
"/oauth2": backendProxy,
|
||||
"/saml2": backendProxy,
|
||||
"/login/oauth2": backendProxy,
|
||||
"/login/saml2": backendProxy,
|
||||
"/swagger-ui": backendProxy,
|
||||
"/v1/api-docs": backendProxy,
|
||||
},
|
||||
proxy: backendProxyConfig,
|
||||
},
|
||||
base: env.RUN_SUBPATH ? `/${env.RUN_SUBPATH}` : "./",
|
||||
preview: {
|
||||
host: true,
|
||||
port: 5173,
|
||||
strictPort: true,
|
||||
proxy: backendProxyConfig,
|
||||
},
|
||||
// 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"
|
||||
? "/"
|
||||
: "./",
|
||||
};
|
||||
});
|
||||
|
||||
@@ -15,27 +15,52 @@ echo ""
|
||||
AUTO_LOGIN=false
|
||||
FORCE_ALL_LOGIN=false
|
||||
COMPOSE_UP_ARGS=(-d --build)
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--auto)
|
||||
AUTO_LOGIN=true
|
||||
shift
|
||||
;;
|
||||
--all)
|
||||
FORCE_ALL_LOGIN=true
|
||||
shift
|
||||
;;
|
||||
--nobuild)
|
||||
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)
|
||||
echo "Usage: $0 [--auto] [--nobuild]"
|
||||
echo "Usage: $0 [--auto] [--all] [--nobuild] [--license-key <KEY>]"
|
||||
echo ""
|
||||
echo " --auto Enable SSO auto-login and force OAuth-only login method"
|
||||
echo " --all Force login method to allow all providers (overrides --auto)"
|
||||
echo " --nobuild Skip building images (use existing images)"
|
||||
echo " --auto Enable SSO auto-login and force OAuth-only login method"
|
||||
echo " --all Force login method to allow all providers (overrides --auto)"
|
||||
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
|
||||
;;
|
||||
*)
|
||||
echo -e "${RED}Unknown option: $arg${NC}"
|
||||
echo -e "${RED}Unknown option: $1${NC}"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
@@ -55,14 +55,36 @@ while [[ $# -gt 0 ]]; do
|
||||
DEFAULT_LANGUAGE="$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)
|
||||
echo "Usage: $0 [--auto] [--with-storage] [--nobuild] [--language <locale>]"
|
||||
echo "Usage: $0 [--auto] [--with-storage] [--nobuild] [--language <locale>] [--license-key <KEY>]"
|
||||
echo ""
|
||||
echo " --auto Enable SSO auto-login and force SAML-only login method"
|
||||
echo " --with-storage Enable the file storage + link-sharing feature"
|
||||
echo " (required to test /share/<token> flows)"
|
||||
echo " --nobuild Skip building images (use existing images)"
|
||||
echo " --language Set system default locale (e.g. de-DE, sv-SE)"
|
||||
echo " --auto Enable SSO auto-login and force SAML-only login method"
|
||||
echo " --with-storage Enable the file storage + link-sharing feature"
|
||||
echo " (required to test /share/<token> flows)"
|
||||
echo " --nobuild Skip building images (use existing images)"
|
||||
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
|
||||
;;
|
||||
*)
|
||||
|
||||
Reference in New Issue
Block a user