Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
name: CI

# IMPORTANT: using Swatinem/rust-cache@v2 : reuse same cache as playwright.yml (minimize non necessary new compilation)

on:
push:
branches:
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/docker-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ on:
pull_request:
branches: [ "main", "dev" ]

concurrency:
group: rust-build-${{ github.ref }}
cancel-in-progress: false

jobs:
build-and-test:
name: Build and Test Docker Image
Expand Down
39 changes: 39 additions & 0 deletions .github/workflows/playwright-update-snapshots.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
name: Playwright — Update snapshots

on:
workflow_dispatch:

jobs:
update-snapshots:
timeout-minutes: 60
runs-on: ubuntu-latest
defaults:
run:
working-directory: tests/e2e
steps:
- uses: actions/checkout@v4

- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable

- name: Cache Rust build
uses: Swatinem/rust-cache@v2

- uses: actions/setup-node@v4
with:
node-version: lts/*

- name: Install Node dependencies
run: npm ci

- name: Install Playwright browsers
run: npx playwright install --with-deps

- name: Update snapshots
run: npm test -- --update-snapshots

- name: Commit updated snapshots
uses: stefanzweifel/git-auto-commit-action@v5
with:
commit_message: "test(e2e): update playwright linux snapshots"
file_pattern: "tests/e2e/scenarios/**/*-linux.png"
61 changes: 61 additions & 0 deletions .github/workflows/playwright.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
name: Playwright Test (end-to-end)

# IMPORTANT: using Swatinem/rust-cache@v2 : reuse same cache as ci.yml (minimize non necessary new compilation)

on:
push:
branches: [ main, dev ]
pull_request:
branches: [ main, dev ]

concurrency:
group: rust-build-${{ github.ref }}
cancel-in-progress: false

jobs:
test:
timeout-minutes: 60
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable

- name: Cache compiled binary
id: binary-cache
uses: actions/cache@v4
with:
path: ${{ github.workspace }}/target/debug/oxicloud
key: ${{ runner.os }}-oxicloud-binary-${{ hashFiles('src/**', 'Cargo.toml', 'Cargo.lock') }}

- name: Cache Rust dependencies
if: steps.binary-cache.outputs.cache-hit != 'true'
uses: Swatinem/rust-cache@v2

- name: Build server
if: steps.binary-cache.outputs.cache-hit != 'true'
run: cargo build

- uses: actions/setup-node@v4
with:
node-version: lts/*

- name: Install Node dependencies
working-directory: tests/e2e
run: npm ci

- name: Install Playwright browsers
working-directory: tests/e2e
run: npx playwright install --with-deps

- name: Run Playwright tests (spawns DB via pretest hook)
working-directory: tests/e2e
run: npm test

- uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
with:
name: playwright-report
path: tests/e2e/playwright-report/
retention-days: 30
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,11 @@ resources/gen/

# Helm chart dependencies
charts/*/charts/*

# Playwright
tests/e2e/node_modules/
tests/e2e/test-results/
tests/e2e/playwright-report/
tests/e2e/blob-report/
tests/e2e/playwright/.cache/
tests/e2e/playwright/.auth/
8 changes: 8 additions & 0 deletions justfile
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,11 @@ front-lint:
# check CSS rules
front-rules:
stylelint static/css/

# end-to-end tests
front-test:
cd tests/e2e && npm test

# update images snapshots
front-test-update-snapshot:
cd tests/e2e && npm test -- --update-snapshots
16 changes: 16 additions & 0 deletions tests/e2e/docker-compose.test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
services:
postgres-test:
image: postgres:18.2-alpine3.23
environment:
POSTGRES_USER: oxicloud_test
POSTGRES_PASSWORD: oxicloud_test
POSTGRES_DB: oxicloud_test
ports:
- "5433:5432"
tmpfs:
- /var/lib/postgresql # in-memory: always blank on start (pg18+ uses /var/lib/postgresql/18/data)
healthcheck:
test: ["CMD-SHELL", "pg_isready -U oxicloud_test"]
interval: 2s
timeout: 5s
retries: 10
18 changes: 18 additions & 0 deletions tests/e2e/global-setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { TEST_ADMIN } from './scenarios/helpers';

export default async function globalSetup() {
const res = await fetch('http://localhost:8087/api/setup', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
username: TEST_ADMIN.username,
email: TEST_ADMIN.email,
password: TEST_ADMIN.password,
}),
});

// 409 = admin already exists (idempotent across retries)
if (!res.ok && res.status !== 409) {
throw new Error(`Admin setup failed: ${res.status} ${await res.text()}`);
}
}
97 changes: 97 additions & 0 deletions tests/e2e/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

19 changes: 19 additions & 0 deletions tests/e2e/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"name": "e2e",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"pretest": "node -e \"(async()=>{await require('./spawn-db.js')()})().catch(e=>{console.error(e);process.exit(1)})\"",
"test": "npx playwright test",
"posttest": "node -e \"(async()=>{await require('./stop-db.js')()})().catch(e=>{console.error(e);process.exit(1)})\""
},
"keywords": [],
"author": "",
"license": "ISC",
"type": "commonjs",
"devDependencies": {
"@playwright/test": "^1.59.1",
"@types/node": "^25.6.0"
}
}
57 changes: 57 additions & 0 deletions tests/e2e/playwright.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
testDir: './scenarios',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: process.env.CI ? [['github'], ['html']] : 'html',

globalSetup: require.resolve('./global-setup'),

use: {
baseURL: 'http://localhost:8087',
trace: 'on-first-retry',
},

expect: {
toHaveScreenshot: { maxDiffPixelRatio: 0.02 },
},

projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
],

webServer: {
command: process.env.CI ? `${process.env.GITHUB_WORKSPACE}/target/debug/oxicloud` : 'cargo run',
url: 'http://localhost:8087',
timeout: 600_000,
reuseExistingServer: false,
cwd: '../..',
stdout: 'pipe',
stderr: 'pipe',
env: {
DATABASE_URL: 'postgres://oxicloud_test:oxicloud_test@localhost:5433/oxicloud_test',
OXICLOUD_SERVER_PORT: 8087,
OXICLOUD_DB_CONNECTION_STRING: 'postgres://oxicloud_test:oxicloud_test@localhost:5433/oxicloud_test',
OXICLOUD_STORAGE_PATH: './tests/e2e/storage',
OXICLOUD_STATIC_PATH: './static',
OXICLOUD_JWT_SECRET: 'test-secret-do-not-use-in-prod-minimum-32-chars',
OXICLOUD_ENABLE_AUTH: 'true',
OXICLOUD_ENABLE_TRASH: 'true',
OXICLOUD_ENABLE_SEARCH: 'true',
OXICLOUD_ENABLE_FILE_SHARING: 'true',
OXICLOUD_WOPI_ENABLED: 'false',
OXICLOUD_OIDC_ENABLED: 'false',
RUST_LOG: 'warn',
},
},
});
36 changes: 36 additions & 0 deletions tests/e2e/scenarios/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { Page, expect } from '@playwright/test';

export const TEST_ADMIN = {
username: 'admin',
email: 'testadmin@example.com',
password: 'TestPassword1!',
};

/**
* Log in as the test admin and wait until the main app is ready.
*/
export async function loginAsAdmin(page: Page) {
await goToLoginPage(page);
await page.locator('#login-username').fill(TEST_ADMIN.username);
await page.locator('#login-password').fill(TEST_ADMIN.password);
await page.locator('#login-panel button[type="submit"]').click();
await expect(page.locator('#sidebar')).toBeVisible({ timeout: 15_000 });
}

/**
* Navigate to `/` and land on the login panel, handling the language selector
* if it appears (fresh localStorage). The admin account is guaranteed to exist
* because globalSetup created it before any test ran.
*/
export async function goToLoginPage(page: Page) {
await page.goto('/');

// Both panels start with .hidden — wait for JS to reveal one.
await page.waitForSelector('#language-panel:not(.hidden), #login-panel:not(.hidden)');

if (await page.locator('#language-panel').isVisible()) {
await page.locator('#language-continue').click();
}

await expect(page.locator('#login-panel')).toBeVisible();
}
Loading
Loading