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 @@