test(e2e): run the SvelteKit SPA Playwright suite in CI
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.
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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 @@
|
||||
</ListToolbar>
|
||||
|
||||
<nav class="breadcrumb" aria-label="Breadcrumb">
|
||||
<!-- Persistent home link → the root listing (bare /files canonicalizes to
|
||||
the user's drive root). `buildCrumbs` returns only the path folders,
|
||||
so this is the single always-present "go home" affordance. -->
|
||||
<a
|
||||
href={resolve('/files')}
|
||||
class="breadcrumb-item breadcrumb-home breadcrumb-link"
|
||||
title={t('breadcrumb.home', 'Home')}
|
||||
data-testid="files-breadcrumb-home-link"
|
||||
ondragover={(e) => e.dataTransfer?.types.includes(DRAG_TYPE) && e.preventDefault()}
|
||||
ondrop={(e) => session.homeFolderId && onCrumbDrop(e, session.homeFolderId)}
|
||||
>
|
||||
<Icon name={rootIcon} />
|
||||
</a>
|
||||
{#each crumbs as c, i (c.id)}
|
||||
{#if i > 0}
|
||||
<span class="breadcrumb-separator">></span>
|
||||
{/if}
|
||||
<span class="breadcrumb-separator">></span>
|
||||
{#if i === crumbs.length - 1}
|
||||
<span class="breadcrumb-item breadcrumb-current" class:breadcrumb-home={i === 0}>
|
||||
{#if i === 0}<Icon name={rootIcon} />{/if}
|
||||
{c.name}
|
||||
</span>
|
||||
<span class="breadcrumb-item breadcrumb-current">{c.name}</span>
|
||||
{:else}
|
||||
<a
|
||||
href={resolve(`/files/${pathSegments.slice(0, i + 1).join('/')}`)}
|
||||
class="breadcrumb-item breadcrumb-link"
|
||||
class:breadcrumb-home={i === 0}
|
||||
title={i === 0 ? t('breadcrumb.home', 'Home') : undefined}
|
||||
data-testid={i === 0 ? 'files-breadcrumb-home-link' : `files-breadcrumb-${c.id}`}
|
||||
data-testid={`files-breadcrumb-${c.id}`}
|
||||
ondragover={(e) => e.dataTransfer?.types.includes(DRAG_TYPE) && e.preventDefault()}
|
||||
ondrop={(e) => onCrumbDrop(e, c.id)}
|
||||
>
|
||||
{#if i === 0}<Icon name={rootIcon} />{/if}
|
||||
{c.name}
|
||||
</a>
|
||||
{/if}
|
||||
|
||||
@@ -31,7 +31,7 @@ export default defineConfig({
|
||||
globalTeardown: require.resolve('./global-teardown'),
|
||||
|
||||
use: {
|
||||
baseURL: 'http://localhost:8088',
|
||||
baseURL: 'http://127.0.0.1:8088',
|
||||
trace: 'on-first-retry',
|
||||
headless: true,
|
||||
screenshot: 'only-on-failure',
|
||||
@@ -50,7 +50,7 @@ export default defineConfig({
|
||||
command: process.env.BUILD_TARGET
|
||||
? `bash "${startScript}" "${workspace}/target/${process.env.BUILD_TARGET}/oxicloud"`
|
||||
: `bash "${startScript}" cargo run --features plugins`,
|
||||
url: 'http://localhost:8088',
|
||||
url: 'http://127.0.0.1:8088/ready',
|
||||
timeout: 600_000,
|
||||
reuseExistingServer: false,
|
||||
cwd: '../..',
|
||||
|
||||
@@ -202,9 +202,11 @@ test('breadcrumb navigates back to home', async ({ page }) => {
|
||||
const folderName = uniq('Crumb');
|
||||
const folder = await apiCreateFolder(page, folderName);
|
||||
await page.goto(`/files/${folder.id}`);
|
||||
// Breadcrumb home link returns to the root listing.
|
||||
// Breadcrumb home link leaves the subfolder for the root listing. Bare /files
|
||||
// canonicalizes to the user's drive root, where the just-created folder lives.
|
||||
await page.getByTestId('files-breadcrumb-home-link').click();
|
||||
await expect(page).toHaveURL(/\/files\/?$/);
|
||||
await expect(page).not.toHaveURL(new RegExp(folder.id));
|
||||
await expect(page.getByTestId(folderName)).toBeVisible({ timeout: 15_000 });
|
||||
});
|
||||
|
||||
test('open an image in the viewer and use the zoom controls', async ({ page }) => {
|
||||
|
||||
@@ -12,5 +12,5 @@ export default async function globalSetup() {
|
||||
fs.rmSync(nycDir, { recursive: true, force: true });
|
||||
fs.mkdirSync(nycDir, { recursive: true });
|
||||
|
||||
await seedAdmin('http://localhost:8088');
|
||||
await seedAdmin('http://127.0.0.1:8088');
|
||||
}
|
||||
|
||||
@@ -11,13 +11,30 @@ set -euo pipefail
|
||||
REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||
SPA_STORAGE_PATH="$REPO_ROOT/tests/e2e/storage-spa"
|
||||
|
||||
# Mirror markers + the server's stdout/stderr to a log file as well as the
|
||||
# console; CI surfaces it via the "Print server startup log" step (Playwright's
|
||||
# own webServer capture isn't always shown there). The final `exec "$@"`
|
||||
# inherits these fds, so the server's output is tee'd while it still replaces
|
||||
# this shell (Playwright tracks the PID for teardown).
|
||||
SERVER_LOG="$REPO_ROOT/tests/e2e/server-startup.log"
|
||||
exec > >(tee "$SERVER_LOG") 2>&1
|
||||
|
||||
mark() { echo "[start-server-spa $(date -u +%H:%M:%S)] $*"; }
|
||||
mark "repo_root=$REPO_ROOT server args: $*"
|
||||
if [[ -n "${1:-}" && "$1" != "cargo" ]]; then
|
||||
ls -la "$1" 2>&1 || mark "WARNING: server binary '$1' not found"
|
||||
fi
|
||||
mark "DATABASE_URL=${DATABASE_URL:-<unset>} PORT=${OXICLOUD_SERVER_PORT:-<unset>} STATIC=${OXICLOUD_STATIC_PATH:-<unset>}"
|
||||
|
||||
# ensure storage is empty before starting
|
||||
echo "Wipe $SPA_STORAGE_PATH to ensure clean startup"
|
||||
mark "wiping $SPA_STORAGE_PATH to ensure clean startup"
|
||||
rm -rf "$SPA_STORAGE_PATH"
|
||||
mkdir -p "$SPA_STORAGE_PATH"
|
||||
|
||||
# Spawn database (idempotent — reuses the running test postgres if present).
|
||||
mark "spawning test database…"
|
||||
bash "$REPO_ROOT/tests/common/spawn-db.sh"
|
||||
mark "database ready; starting server…"
|
||||
|
||||
# Replace the shell with the server process so Playwright's PID tracking works.
|
||||
exec "$@"
|
||||
|
||||
Reference in New Issue
Block a user