Skip to content

Commit 3e0d09d

Browse files
test: initial end to end test with playwright
1 parent e4ea221 commit 3e0d09d

26 files changed

Lines changed: 450 additions & 0 deletions

.github/workflows/ci.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
name: CI
22

3+
# IMPORTANT: using Swatinem/rust-cache@v2 : reuse same cache as playwright.yml (minimize non necessary new compilation)
4+
35
on:
46
push:
57
branches:

.github/workflows/playwright.yml

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
name: Playwright Test (end-to-end)
2+
3+
# IMPORTANT: using Swatinem/rust-cache@v2 : reuse same cache as ci.yml (minimize non necessary new compilation)
4+
5+
on:
6+
push:
7+
branches: [ main, dev ]
8+
pull_request:
9+
branches: [ main, dev ]
10+
jobs:
11+
test:
12+
timeout-minutes: 60
13+
runs-on: ubuntu-latest
14+
defaults:
15+
run:
16+
working-directory: tests/e2e
17+
steps:
18+
- uses: actions/checkout@v4
19+
20+
- name: Install Rust toolchain
21+
uses: dtolnay/rust-toolchain@stable
22+
23+
- name: Cache Rust build
24+
uses: Swatinem/rust-cache@v2
25+
26+
- uses: actions/setup-node@v4
27+
with:
28+
node-version: lts/*
29+
30+
- name: Install Node dependencies
31+
run: npm ci
32+
33+
- name: Install Playwright browsers
34+
run: npx playwright install --with-deps
35+
36+
- name: Run Playwright tests (spawns DB via pretest hook)
37+
run: npm test
38+
39+
- uses: actions/upload-artifact@v4
40+
if: ${{ !cancelled() }}
41+
with:
42+
name: playwright-report
43+
path: tests/e2e/playwright-report/
44+
retention-days: 30

.gitignore

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,3 +87,11 @@ resources/gen/
8787

8888
# Helm chart dependencies
8989
charts/*/charts/*
90+
91+
# Playwright
92+
tests/e2e/node_modules/
93+
tests/e2e/test-results/
94+
tests/e2e/playwright-report/
95+
tests/e2e/blob-report/
96+
tests/e2e/playwright/.cache/
97+
tests/e2e/playwright/.auth/

justfile

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,3 +63,7 @@ front-lint:
6363
# check CSS rules
6464
front-rules:
6565
stylelint static/css/
66+
67+
# end-to-end tests
68+
front-test:
69+
cd tests/e2e && npm test

tests/e2e/docker-compose.test.yml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
services:
2+
postgres-test:
3+
image: postgres:18.2-alpine3.23
4+
environment:
5+
POSTGRES_USER: oxicloud_test
6+
POSTGRES_PASSWORD: oxicloud_test
7+
POSTGRES_DB: oxicloud_test
8+
ports:
9+
- "5433:5432"
10+
tmpfs:
11+
- /var/lib/postgresql # in-memory: always blank on start (pg18+ uses /var/lib/postgresql/18/data)
12+
healthcheck:
13+
test: ["CMD-SHELL", "pg_isready -U oxicloud_test"]
14+
interval: 2s
15+
timeout: 5s
16+
retries: 10

tests/e2e/global-setup.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { TEST_ADMIN } from './scenarios/helpers';
2+
3+
export default async function globalSetup() {
4+
const res = await fetch('http://localhost:8087/api/setup', {
5+
method: 'POST',
6+
headers: { 'Content-Type': 'application/json' },
7+
body: JSON.stringify({
8+
username: TEST_ADMIN.username,
9+
email: TEST_ADMIN.email,
10+
password: TEST_ADMIN.password,
11+
}),
12+
});
13+
14+
// 409 = admin already exists (idempotent across retries)
15+
if (!res.ok && res.status !== 409) {
16+
throw new Error(`Admin setup failed: ${res.status} ${await res.text()}`);
17+
}
18+
}

tests/e2e/package-lock.json

Lines changed: 97 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/e2e/package.json

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
{
2+
"name": "e2e",
3+
"version": "1.0.0",
4+
"description": "",
5+
"main": "index.js",
6+
"scripts": {
7+
"pretest": "node -e \"(async()=>{await require('./spawn-db.js')()})().catch(e=>{console.error(e);process.exit(1)})\"",
8+
"test": "npx playwright test",
9+
"posttest": "node -e \"(async()=>{await require('./stop-db.js')()})().catch(e=>{console.error(e);process.exit(1)})\"",
10+
11+
"pretest-ui": "node -e \"(async()=>{await require('./spawn-db.js')()})().catch(e=>{console.error(e);process.exit(1)})\"",
12+
"test-ui": "npx playwright test --ui",
13+
"posttest-ui": "node -e \"(async()=>{await require('./stop-db.js')()})().catch(e=>{console.error(e);process.exit(1)})\"",
14+
15+
"pretest-update-snapshots": "node -e \"(async()=>{await require('./spawn-db.js')()})().catch(e=>{console.error(e);process.exit(1)})\"",
16+
"update-snapshots":"npx playwright test --update-snapshots",
17+
"posttest-update-snapshots": "node -e \"(async()=>{await require('./stop-db.js')()})().catch(e=>{console.error(e);process.exit(1)})\""
18+
},
19+
"keywords": [],
20+
"author": "",
21+
"license": "ISC",
22+
"type": "commonjs",
23+
"devDependencies": {
24+
"@playwright/test": "^1.59.1",
25+
"@types/node": "^25.6.0"
26+
}
27+
}

tests/e2e/playwright.config.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { defineConfig, devices } from '@playwright/test';
2+
3+
export default defineConfig({
4+
testDir: './scenarios',
5+
fullyParallel: true,
6+
forbidOnly: !!process.env.CI,
7+
retries: process.env.CI ? 2 : 0,
8+
workers: process.env.CI ? 1 : undefined,
9+
reporter: process.env.CI ? [['github'], ['html']] : 'html',
10+
11+
globalSetup: require.resolve('./global-setup'),
12+
13+
use: {
14+
baseURL: 'http://localhost:8087',
15+
trace: 'on-first-retry',
16+
toHaveScreenshot: { maxDiffPixelRatio: 0.02 },
17+
},
18+
19+
projects: [
20+
{
21+
name: 'chromium',
22+
use: { ...devices['Desktop Chrome'] },
23+
},
24+
{
25+
name: 'firefox',
26+
use: { ...devices['Desktop Firefox'] },
27+
},
28+
],
29+
30+
webServer: {
31+
command: 'cargo run',
32+
url: 'http://localhost:8087',
33+
timeout: 600_000,
34+
reuseExistingServer: false,
35+
cwd: '../..',
36+
stdout: 'pipe',
37+
stderr: 'pipe',
38+
env: {
39+
DATABASE_URL: 'postgres://oxicloud_test:oxicloud_test@localhost:5433/oxicloud_test',
40+
OXICLOUD_SERVER_PORT: 8087,
41+
OXICLOUD_DB_CONNECTION_STRING: 'postgres://oxicloud_test:oxicloud_test@localhost:5433/oxicloud_test',
42+
OXICLOUD_STORAGE_PATH: './tests/e2e/storage',
43+
OXICLOUD_STATIC_PATH: './static',
44+
OXICLOUD_JWT_SECRET: 'test-secret-do-not-use-in-prod-minimum-32-chars',
45+
OXICLOUD_ENABLE_AUTH: 'true',
46+
OXICLOUD_ENABLE_TRASH: 'true',
47+
OXICLOUD_ENABLE_SEARCH: 'true',
48+
OXICLOUD_ENABLE_FILE_SHARING: 'true',
49+
OXICLOUD_WOPI_ENABLED: 'false',
50+
OXICLOUD_OIDC_ENABLED: 'false',
51+
RUST_LOG: 'warn',
52+
},
53+
},
54+
});

tests/e2e/scenarios/helpers.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { Page, expect } from '@playwright/test';
2+
3+
export const TEST_ADMIN = {
4+
username: 'admin',
5+
email: 'testadmin@example.com',
6+
password: 'TestPassword1!',
7+
};
8+
9+
/**
10+
* Log in as the test admin and wait until the main app is ready.
11+
*/
12+
export async function loginAsAdmin(page: Page) {
13+
await goToLoginPage(page);
14+
await page.locator('#login-username').fill(TEST_ADMIN.username);
15+
await page.locator('#login-password').fill(TEST_ADMIN.password);
16+
await page.locator('#login-panel button[type="submit"]').click();
17+
await expect(page.locator('#sidebar')).toBeVisible({ timeout: 15_000 });
18+
}
19+
20+
/**
21+
* Navigate to `/` and land on the login panel, handling the language selector
22+
* if it appears (fresh localStorage). The admin account is guaranteed to exist
23+
* because globalSetup created it before any test ran.
24+
*/
25+
export async function goToLoginPage(page: Page) {
26+
await page.goto('/');
27+
28+
// Both panels start with .hidden — wait for JS to reveal one.
29+
await page.waitForSelector('#language-panel:not(.hidden), #login-panel:not(.hidden)');
30+
31+
if (await page.locator('#language-panel').isVisible()) {
32+
await page.locator('#language-continue').click();
33+
}
34+
35+
await expect(page.locator('#login-panel')).toBeVisible();
36+
}

0 commit comments

Comments
 (0)