diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f70cd45d..29f9ee39 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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: diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 1a2cfc30..54dbadb7 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -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 diff --git a/.github/workflows/playwright-update-snapshots.yml b/.github/workflows/playwright-update-snapshots.yml new file mode 100644 index 00000000..893ad201 --- /dev/null +++ b/.github/workflows/playwright-update-snapshots.yml @@ -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" diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml new file mode 100644 index 00000000..999950f5 --- /dev/null +++ b/.github/workflows/playwright.yml @@ -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 diff --git a/.gitignore b/.gitignore index b9fb6993..98d51e89 100644 --- a/.gitignore +++ b/.gitignore @@ -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/ diff --git a/justfile b/justfile index f5cee183..f4527f55 100644 --- a/justfile +++ b/justfile @@ -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 diff --git a/tests/e2e/docker-compose.test.yml b/tests/e2e/docker-compose.test.yml new file mode 100644 index 00000000..cd6e5fb3 --- /dev/null +++ b/tests/e2e/docker-compose.test.yml @@ -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 diff --git a/tests/e2e/global-setup.ts b/tests/e2e/global-setup.ts new file mode 100644 index 00000000..ec60193c --- /dev/null +++ b/tests/e2e/global-setup.ts @@ -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()}`); + } +} diff --git a/tests/e2e/package-lock.json b/tests/e2e/package-lock.json new file mode 100644 index 00000000..c83526f5 --- /dev/null +++ b/tests/e2e/package-lock.json @@ -0,0 +1,97 @@ +{ + "name": "e2e", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "e2e", + "version": "1.0.0", + "license": "ISC", + "devDependencies": { + "@playwright/test": "^1.59.1", + "@types/node": "^25.6.0" + } + }, + "node_modules/@playwright/test": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.59.1.tgz", + "integrity": "sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.59.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@types/node": { + "version": "25.6.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz", + "integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.19.0" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/playwright": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.59.1.tgz", + "integrity": "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.59.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.59.1.tgz", + "integrity": "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/undici-types": { + "version": "7.19.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz", + "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/tests/e2e/package.json b/tests/e2e/package.json new file mode 100644 index 00000000..c80e7327 --- /dev/null +++ b/tests/e2e/package.json @@ -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" + } +} diff --git a/tests/e2e/playwright.config.ts b/tests/e2e/playwright.config.ts new file mode 100644 index 00000000..ebb5974c --- /dev/null +++ b/tests/e2e/playwright.config.ts @@ -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', + }, + }, +}); diff --git a/tests/e2e/scenarios/helpers.ts b/tests/e2e/scenarios/helpers.ts new file mode 100644 index 00000000..b3e1cccf --- /dev/null +++ b/tests/e2e/scenarios/helpers.ts @@ -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(); +} diff --git a/tests/e2e/scenarios/home-and-login.spec.ts b/tests/e2e/scenarios/home-and-login.spec.ts new file mode 100644 index 00000000..34163365 --- /dev/null +++ b/tests/e2e/scenarios/home-and-login.spec.ts @@ -0,0 +1,91 @@ +import { test, expect } from '@playwright/test'; +import { goToLoginPage, loginAsAdmin, TEST_ADMIN } from './helpers'; + +test('has OxiCloud title', async ({ page }) => { + await page.goto('/'); + await expect(page).toHaveTitle(/OxiCloud/); +}); + +test('language selector > choose EN > reach login page', async ({ page }) => { + await page.goto('/'); + + await expect(page.locator('#language-panel')).toBeVisible(); + await expect(page.getByText('Select your language to continue')).toBeVisible(); + await expect(page.locator('#lang-picker-name')).toHaveText('English'); + + await expect(page.locator('#language-panel')).toHaveScreenshot('language-selector.png'); + + await page.locator('#language-continue').click(); + + await expect(page.locator('#login-panel')).toBeVisible(); + await expect(page.locator('#language-panel')).toBeHidden(); + + await expect(page.locator('#login-panel')).toHaveScreenshot('login-panel.png'); +}); + +test('login with wrong password is rejected', async ({ page }) => { + await goToLoginPage(page); + + await page.locator('#login-username').fill(TEST_ADMIN.username); + await page.locator('#login-password').fill('definitely-wrong-password'); + await page.locator('#login-panel button[type="submit"]').click(); + + const loginError = page.locator('#login-error'); + await expect(loginError).toBeVisible(); + await expect(loginError).toContainText('Authentication error (403): Forbidden'); + + await expect(page.locator('#login-panel')).toBeVisible(); + await expect(page.locator('#login-panel')).toHaveScreenshot('login-panel-error.png'); +}); + +test.describe('authenticated as admin', () => { + test.beforeEach(async ({ page }) => { + await loginAsAdmin(page); + }); + + test('home page shows files list', async ({ page }) => { + await expect(page.locator('.files-container')).toBeVisible(); + await expect(page.locator('#user-menu-wrapper')).toBeVisible(); + await expect(page).toHaveScreenshot('home-files.png', { + // ignore this div (max value may change) + // FIXME: ensure same max capacity from server during test + animations: 'disabled', + mask: [page.locator('.storage-bar'), page.locator('.storage-info') ] + }); + }); + + test('theme can be changed to dark', async ({ page }) => { + await page.locator('#user-avatar-btn').click(); + await expect(page.locator('#user-menu')).toBeVisible(); + await page.locator('#theme-toggle-pill').click(); + + // html element must carry data-theme="dark" + await expect(page.locator('html')).toHaveAttribute('data-theme', 'dark'); + + // localStorage must persist the choice + const theme = await page.evaluate(() => localStorage.getItem('oxicloud_theme')); + expect(theme).toBe('dark'); + + await expect(page).toHaveScreenshot('home-files-darktheme.png', { + // ignore this div (max value may change) + // FIXME: ensure same max capacity from server during test + animations: 'disabled', + mask: [page.locator('.storage-bar'), page.locator('.storage-info') ] + }); + + // Toggle back to light + await page.locator('#theme-toggle-pill').click(); + await expect(page.locator('html')).not.toHaveAttribute('data-theme', 'dark'); + + const themeAfter = await page.evaluate(() => localStorage.getItem('oxicloud_theme')); + expect(themeAfter).toBe('light'); + + await expect(page).toHaveScreenshot('home-files-lightheme.png', { + // ignore this div (max value may change) + // FIXME: ensure same max capacity from server during test + animations: 'disabled', + mask: [page.locator('.storage-bar'), page.locator('.storage-info') ] + }); + + }); +}); diff --git a/tests/e2e/scenarios/home-and-login.spec.ts-snapshots/home-files-chromium-darwin.png b/tests/e2e/scenarios/home-and-login.spec.ts-snapshots/home-files-chromium-darwin.png new file mode 100644 index 00000000..72adfe0a Binary files /dev/null and b/tests/e2e/scenarios/home-and-login.spec.ts-snapshots/home-files-chromium-darwin.png differ diff --git a/tests/e2e/scenarios/home-and-login.spec.ts-snapshots/home-files-chromium-linux.png b/tests/e2e/scenarios/home-and-login.spec.ts-snapshots/home-files-chromium-linux.png new file mode 100644 index 00000000..af579ee1 Binary files /dev/null and b/tests/e2e/scenarios/home-and-login.spec.ts-snapshots/home-files-chromium-linux.png differ diff --git a/tests/e2e/scenarios/home-and-login.spec.ts-snapshots/home-files-darktheme-chromium-darwin.png b/tests/e2e/scenarios/home-and-login.spec.ts-snapshots/home-files-darktheme-chromium-darwin.png new file mode 100644 index 00000000..9bbee12a Binary files /dev/null and b/tests/e2e/scenarios/home-and-login.spec.ts-snapshots/home-files-darktheme-chromium-darwin.png differ diff --git a/tests/e2e/scenarios/home-and-login.spec.ts-snapshots/home-files-darktheme-chromium-linux.png b/tests/e2e/scenarios/home-and-login.spec.ts-snapshots/home-files-darktheme-chromium-linux.png new file mode 100644 index 00000000..97f7b896 Binary files /dev/null and b/tests/e2e/scenarios/home-and-login.spec.ts-snapshots/home-files-darktheme-chromium-linux.png differ diff --git a/tests/e2e/scenarios/home-and-login.spec.ts-snapshots/home-files-darktheme-firefox-darwin.png b/tests/e2e/scenarios/home-and-login.spec.ts-snapshots/home-files-darktheme-firefox-darwin.png new file mode 100644 index 00000000..3346dcb7 Binary files /dev/null and b/tests/e2e/scenarios/home-and-login.spec.ts-snapshots/home-files-darktheme-firefox-darwin.png differ diff --git a/tests/e2e/scenarios/home-and-login.spec.ts-snapshots/home-files-darktheme-firefox-linux.png b/tests/e2e/scenarios/home-and-login.spec.ts-snapshots/home-files-darktheme-firefox-linux.png new file mode 100644 index 00000000..af14b836 Binary files /dev/null and b/tests/e2e/scenarios/home-and-login.spec.ts-snapshots/home-files-darktheme-firefox-linux.png differ diff --git a/tests/e2e/scenarios/home-and-login.spec.ts-snapshots/home-files-firefox-darwin.png b/tests/e2e/scenarios/home-and-login.spec.ts-snapshots/home-files-firefox-darwin.png new file mode 100644 index 00000000..06a6d202 Binary files /dev/null and b/tests/e2e/scenarios/home-and-login.spec.ts-snapshots/home-files-firefox-darwin.png differ diff --git a/tests/e2e/scenarios/home-and-login.spec.ts-snapshots/home-files-firefox-linux.png b/tests/e2e/scenarios/home-and-login.spec.ts-snapshots/home-files-firefox-linux.png new file mode 100644 index 00000000..368300ea Binary files /dev/null and b/tests/e2e/scenarios/home-and-login.spec.ts-snapshots/home-files-firefox-linux.png differ diff --git a/tests/e2e/scenarios/home-and-login.spec.ts-snapshots/home-files-lightheme-chromium-darwin.png b/tests/e2e/scenarios/home-and-login.spec.ts-snapshots/home-files-lightheme-chromium-darwin.png new file mode 100644 index 00000000..bfbb88b3 Binary files /dev/null and b/tests/e2e/scenarios/home-and-login.spec.ts-snapshots/home-files-lightheme-chromium-darwin.png differ diff --git a/tests/e2e/scenarios/home-and-login.spec.ts-snapshots/home-files-lightheme-chromium-linux.png b/tests/e2e/scenarios/home-and-login.spec.ts-snapshots/home-files-lightheme-chromium-linux.png new file mode 100644 index 00000000..e1db230f Binary files /dev/null and b/tests/e2e/scenarios/home-and-login.spec.ts-snapshots/home-files-lightheme-chromium-linux.png differ diff --git a/tests/e2e/scenarios/home-and-login.spec.ts-snapshots/home-files-lightheme-firefox-darwin.png b/tests/e2e/scenarios/home-and-login.spec.ts-snapshots/home-files-lightheme-firefox-darwin.png new file mode 100644 index 00000000..ee62824f Binary files /dev/null and b/tests/e2e/scenarios/home-and-login.spec.ts-snapshots/home-files-lightheme-firefox-darwin.png differ diff --git a/tests/e2e/scenarios/home-and-login.spec.ts-snapshots/home-files-lightheme-firefox-linux.png b/tests/e2e/scenarios/home-and-login.spec.ts-snapshots/home-files-lightheme-firefox-linux.png new file mode 100644 index 00000000..5a60a282 Binary files /dev/null and b/tests/e2e/scenarios/home-and-login.spec.ts-snapshots/home-files-lightheme-firefox-linux.png differ diff --git a/tests/e2e/scenarios/home-and-login.spec.ts-snapshots/language-selector-chromium-darwin.png b/tests/e2e/scenarios/home-and-login.spec.ts-snapshots/language-selector-chromium-darwin.png new file mode 100644 index 00000000..f4f15f88 Binary files /dev/null and b/tests/e2e/scenarios/home-and-login.spec.ts-snapshots/language-selector-chromium-darwin.png differ diff --git a/tests/e2e/scenarios/home-and-login.spec.ts-snapshots/language-selector-chromium-linux.png b/tests/e2e/scenarios/home-and-login.spec.ts-snapshots/language-selector-chromium-linux.png new file mode 100644 index 00000000..418bc9af Binary files /dev/null and b/tests/e2e/scenarios/home-and-login.spec.ts-snapshots/language-selector-chromium-linux.png differ diff --git a/tests/e2e/scenarios/home-and-login.spec.ts-snapshots/language-selector-firefox-darwin.png b/tests/e2e/scenarios/home-and-login.spec.ts-snapshots/language-selector-firefox-darwin.png new file mode 100644 index 00000000..f1c1ad88 Binary files /dev/null and b/tests/e2e/scenarios/home-and-login.spec.ts-snapshots/language-selector-firefox-darwin.png differ diff --git a/tests/e2e/scenarios/home-and-login.spec.ts-snapshots/language-selector-firefox-linux.png b/tests/e2e/scenarios/home-and-login.spec.ts-snapshots/language-selector-firefox-linux.png new file mode 100644 index 00000000..d7155de8 Binary files /dev/null and b/tests/e2e/scenarios/home-and-login.spec.ts-snapshots/language-selector-firefox-linux.png differ diff --git a/tests/e2e/scenarios/home-and-login.spec.ts-snapshots/login-panel-chromium-darwin.png b/tests/e2e/scenarios/home-and-login.spec.ts-snapshots/login-panel-chromium-darwin.png new file mode 100644 index 00000000..afe3a7b2 Binary files /dev/null and b/tests/e2e/scenarios/home-and-login.spec.ts-snapshots/login-panel-chromium-darwin.png differ diff --git a/tests/e2e/scenarios/home-and-login.spec.ts-snapshots/login-panel-chromium-linux.png b/tests/e2e/scenarios/home-and-login.spec.ts-snapshots/login-panel-chromium-linux.png new file mode 100644 index 00000000..c5be8d9e Binary files /dev/null and b/tests/e2e/scenarios/home-and-login.spec.ts-snapshots/login-panel-chromium-linux.png differ diff --git a/tests/e2e/scenarios/home-and-login.spec.ts-snapshots/login-panel-error-chromium-darwin.png b/tests/e2e/scenarios/home-and-login.spec.ts-snapshots/login-panel-error-chromium-darwin.png new file mode 100644 index 00000000..a4bb11e5 Binary files /dev/null and b/tests/e2e/scenarios/home-and-login.spec.ts-snapshots/login-panel-error-chromium-darwin.png differ diff --git a/tests/e2e/scenarios/home-and-login.spec.ts-snapshots/login-panel-error-chromium-linux.png b/tests/e2e/scenarios/home-and-login.spec.ts-snapshots/login-panel-error-chromium-linux.png new file mode 100644 index 00000000..165496bf Binary files /dev/null and b/tests/e2e/scenarios/home-and-login.spec.ts-snapshots/login-panel-error-chromium-linux.png differ diff --git a/tests/e2e/scenarios/home-and-login.spec.ts-snapshots/login-panel-error-firefox-darwin.png b/tests/e2e/scenarios/home-and-login.spec.ts-snapshots/login-panel-error-firefox-darwin.png new file mode 100644 index 00000000..f1eac9a3 Binary files /dev/null and b/tests/e2e/scenarios/home-and-login.spec.ts-snapshots/login-panel-error-firefox-darwin.png differ diff --git a/tests/e2e/scenarios/home-and-login.spec.ts-snapshots/login-panel-error-firefox-linux.png b/tests/e2e/scenarios/home-and-login.spec.ts-snapshots/login-panel-error-firefox-linux.png new file mode 100644 index 00000000..9550180d Binary files /dev/null and b/tests/e2e/scenarios/home-and-login.spec.ts-snapshots/login-panel-error-firefox-linux.png differ diff --git a/tests/e2e/scenarios/home-and-login.spec.ts-snapshots/login-panel-firefox-darwin.png b/tests/e2e/scenarios/home-and-login.spec.ts-snapshots/login-panel-firefox-darwin.png new file mode 100644 index 00000000..2dfc1458 Binary files /dev/null and b/tests/e2e/scenarios/home-and-login.spec.ts-snapshots/login-panel-firefox-darwin.png differ diff --git a/tests/e2e/scenarios/home-and-login.spec.ts-snapshots/login-panel-firefox-linux.png b/tests/e2e/scenarios/home-and-login.spec.ts-snapshots/login-panel-firefox-linux.png new file mode 100644 index 00000000..4983bd7c Binary files /dev/null and b/tests/e2e/scenarios/home-and-login.spec.ts-snapshots/login-panel-firefox-linux.png differ diff --git a/tests/e2e/spawn-db.js b/tests/e2e/spawn-db.js new file mode 100644 index 00000000..f43ef075 --- /dev/null +++ b/tests/e2e/spawn-db.js @@ -0,0 +1,31 @@ +const { execSync, spawnSync } = require('child_process'); +const net = require('net'); +const path = require('path'); + +const COMPOSE_FILE = path.join(__dirname, 'docker-compose.test.yml'); +const CMD = `docker compose -f ${COMPOSE_FILE}`; + +function waitForPort(host, port, timeoutMs = 30_000) { + const deadline = Date.now() + timeoutMs; + return new Promise((resolve, reject) => { + function attempt() { + const sock = net.connect(port, host); + sock.once('connect', () => { sock.destroy(); resolve(); }); + sock.once('error', () => { + sock.destroy(); + if (Date.now() >= deadline) return reject(new Error(`Timeout waiting for ${host}:${port}`)); + setTimeout(attempt, 500); + }); + } + attempt(); + }); +} + +module.exports = async function globalSetup() { + console.log('[setup] Starting test postgres (database is empty) ...'); + execSync(`${CMD} down`, { stdio: 'inherit' }); + execSync(`${CMD} up -d`, { stdio: 'inherit' }); + console.log('[setup] Waiting for postgres on port 5433...'); + await waitForPort('127.0.0.1', 5433); + console.log('[setup] Postgres is ready.'); +}; diff --git a/tests/e2e/stop-db.js b/tests/e2e/stop-db.js new file mode 100644 index 00000000..dcb91d81 --- /dev/null +++ b/tests/e2e/stop-db.js @@ -0,0 +1,9 @@ +const { execSync } = require('child_process'); +const path = require('path'); + +const COMPOSE_FILE = path.join(__dirname, 'docker-compose.test.yml'); + +module.exports = async function globalTeardown() { + console.log('[teardown] Stopping test postgres...'); + execSync(`docker compose -f ${COMPOSE_FILE} down -v`, { stdio: 'inherit' }); +}; diff --git a/tests/e2e/tsconfig.json b/tests/e2e/tsconfig.json new file mode 100644 index 00000000..97dd3bff --- /dev/null +++ b/tests/e2e/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "Node16", + "moduleResolution": "node16", + "strict": true, + "esModuleInterop": true, + "outDir": "./dist", + "types": ["node"] + }, + "include": ["./**/*.ts"], + "exclude": ["node_modules", "dist"] +}