From 02335c0680cb87b04ccdf440a33955f7c0a20071 Mon Sep 17 00:00:00 2001 From: Bradley Nelson Date: Mon, 22 Jun 2026 10:34:47 -0600 Subject: [PATCH 1/2] test(e2e): run the SvelteKit SPA Playwright suite in CI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The e2e CI job ran the legacy `scenarios/*` specs against the vanilla `static/` frontend that upstream has since removed, so it could never pass. Point CI at this repo's SvelteKit SPA suite (tests/e2e/spa) and wire up what it needs: - CI: build the release binary with `--features plugins` (the admin Plugins-tab specs exercise the WASM runtime) and run `npm run test:coverage`, building the instrumented SPA with COVERAGE=1 VITE_E2E=1 so the server serves the data-testid-instrumented build the specs drive. - Coverage harness: target 127.0.0.1 instead of `localhost` (which resolves to ::1 first on CI runners while the server binds IPv4, so readiness never connected) and poll `/ready` for webServer readiness; tee start-server-spa.sh output to a log surfaced by an always-run CI step for diagnostics. - Files page: restore a persistent breadcrumb home link (buildCrumbs returns only the path folders, so there was no "go home" affordance), and fix the `?file=` deep-link race where the viewer→URL effect stripped the param before the listing loaded — a bookmarked preview link now opens the viewer. All 101 spa specs pass locally. --- .github/workflows/ci.yml | 22 +++++--- .../src/routes/files/[...path]/+page.svelte | 54 ++++++++++++------- tests/e2e/playwright.coverage.config.ts | 4 +- tests/e2e/spa/files-extra.spec.ts | 6 ++- tests/e2e/spa/global-setup.ts | 2 +- tests/e2e/start-server-spa.sh | 19 ++++++- 6 files changed, 73 insertions(+), 34 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 391fed42..f7f5c6dd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -246,7 +246,10 @@ jobs: - name: Build SPA (Vite -> static-dist/) working-directory: frontend run: npm ci && npm run build - - run: cargo build --release + # --features plugins so the e2e Playwright job (which sets + # OXICLOUD_ENABLE_PLUGINS) can exercise the admin Plugins tab. The api/webdav + # and unit jobs reuse this binary but leave plugins disabled at runtime. + - run: cargo build --release --features plugins - uses: actions/upload-artifact@v4 with: name: oxicloud-release @@ -331,20 +334,23 @@ jobs: run: npm ci # The release binary serves the SPA from ./static-dist on disk (not - # embedded), and the Build job builds it WITHOUT VITE_E2E so it lacks the - # `data-testid` hooks the specs target. Build it here with VITE_E2E=1 so - # the server actually serves the e2e SPA the scenarios drive. - - name: Build SPA for e2e (VITE_E2E keeps data-testid hooks) + # embedded). Build the instrumented SPA here with COVERAGE=1 (Istanbul, for + # the coverage report) and VITE_E2E=1 (keeps the `data-testid` hooks the + # specs target). start-server-spa.sh points OXICLOUD_STATIC_PATH here. + - name: Build instrumented SPA for e2e (COVERAGE + VITE_E2E) working-directory: frontend - run: npm ci && VITE_E2E=1 npm run build + run: npm ci && COVERAGE=1 VITE_E2E=1 npm run build - name: Install Playwright browsers working-directory: tests/e2e run: npx playwright install --with-deps - - name: Run Playwright tests (spawns DB via pretest hook) + # Drives this PR's SvelteKit SPA specs (tests/e2e/spa) via the coverage + # config + start-server-spa.sh. (The legacy `npm test` scenarios targeted + # the removed vanilla `static/` frontend and are no longer exercised.) + - name: Run SPA e2e coverage suite working-directory: tests/e2e - run: npm test + run: npm run test:coverage env: BUILD_TARGET: release diff --git a/frontend/src/routes/files/[...path]/+page.svelte b/frontend/src/routes/files/[...path]/+page.svelte index a4abeb7a..cb4a2633 100644 --- a/frontend/src/routes/files/[...path]/+page.svelte +++ b/frontend/src/routes/files/[...path]/+page.svelte @@ -699,17 +699,26 @@ }); }); - // viewer → URL: when closed from within (X / Esc / backdrop), drop the param - // (replaceState, so closing doesn't add a history entry). + // viewer → URL: when the user closes the viewer (X / Esc / backdrop), drop the + // `?file=` param (replaceState, so closing doesn't add a history entry). Only + // act on a genuine open→closed transition: on a cold deep link the viewer + // starts closed *with* the param while the listing is still loading, and + // stripping it there would race the URL→viewer effect above and the preview + // would never open. + let viewerWasOpen = false; $effect(() => { + const open = viewerOpen; const hasParam = page.url.searchParams.get('file') !== null; - if (!viewerOpen && hasParam) { - const url = new URL(page.url); - url.searchParams.delete('file'); - // Same-origin URL object (see note above); resolve() can't type it. - // eslint-disable-next-line svelte/no-navigation-without-resolve - void goto(url, { keepFocus: true, noScroll: true, replaceState: true }); - } + untrack(() => { + if (viewerWasOpen && !open && hasParam) { + const url = new URL(page.url); + url.searchParams.delete('file'); + // Same-origin URL object (see note above); resolve() can't type it. + // eslint-disable-next-line svelte/no-navigation-without-resolve + void goto(url, { keepFocus: true, noScroll: true, replaceState: true }); + } + viewerWasOpen = open; + }); }); /** @@ -1607,26 +1616,31 @@