From 93bf1d2ae223f941bdb3ef4dc06ffa238892faed Mon Sep 17 00:00:00 2001 From: Daniel Graf Date: Sun, 16 Nov 2025 12:48:00 +0100 Subject: [PATCH] 218 feature request end to end e2e frontend testing framework 2 (#477) --- .dockerignore | 5 + .github/workflows/ci.yml | 124 +++++++++++ .gitignore | 3 + e2e/docker-compose.ci.yml | 59 ++++++ e2e/package-lock.json | 195 ++++++++++++++++++ e2e/package.json | 27 +++ e2e/playwright.config.js | 61 ++++++ e2e/tests/login.spec.js | 37 ++++ .../static/js/date-picker-combined.js | 23 +-- 9 files changed, 517 insertions(+), 17 deletions(-) create mode 100644 .dockerignore create mode 100644 .github/workflows/ci.yml create mode 100644 e2e/docker-compose.ci.yml create mode 100644 e2e/package-lock.json create mode 100644 e2e/package.json create mode 100644 e2e/playwright.config.js create mode 100644 e2e/tests/login.spec.js diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..d44005b9 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,5 @@ +e2e/ +data/ +docs/ +scripts/ +src/ diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..469c0db1 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,124 @@ +name: CI + +on: + pull_request: + branches: + - main + +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up JDK 25 + uses: actions/setup-java@v4 + with: + java-version: '25' + distribution: 'temurin' + cache: maven + + - name: Install dependencies for acknowledgments script + run: | + sudo apt-get update + sudo apt-get install -y jq curl + + - name: Generate acknowledgments data + run: | + chmod +x scripts/generate-acknowledgments.sh + ./scripts/generate-acknowledgments.sh + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Build + run: mvn verify -DskipTests + + - name: Create bundle + run: mkdir staging && cp target/*.jar staging + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Cache Docker layers + uses: actions/cache@v4 + with: + path: /tmp/.buildx-cache + key: docker-${{ runner.os }}-${{ github.sha }} + restore-keys: | + docker-${{ runner.os }}- + + - name: Build docker image + uses: docker/build-push-action@v6 + with: + platforms: linux/amd64 + context: . + push: false + tags: dedicatedcode/reitti:latest + cache-from: type=local,src=/tmp/.buildx-cache + cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max + + - name: Move Docker cache + run: | + rm -rf /tmp/.buildx-cache + mv /tmp/.buildx-cache-new /tmp/.buildx-cache + + - name: Start docker-compose + run: docker compose -f docker-compose.ci.yml up -d + working-directory: e2e + + - name: Wait for app to be ready + run: | + timeout 60 bash -c 'until curl -f http://localhost:8080; do sleep 2; done' + working-directory: e2e + + - name: Cache npm dependencies + uses: actions/cache@v4 + with: + path: e2e/node_modules + key: npm-${{ runner.os }}-${{ hashFiles('e2e/package-lock.json') }} + restore-keys: | + npm-${{ runner.os }}- + + - name: Install dependencies + run: npm ci + working-directory: e2e + + - name: Cache Playwright browsers + uses: actions/cache@v4 + with: + path: ~/.cache/ms-playwright + key: playwright-${{ runner.os }}-${{ hashFiles('e2e/package-lock.json') }} + restore-keys: | + playwright-${{ runner.os }}- + + - name: Install Playwright browsers + run: npx playwright install --with-deps + working-directory: e2e + + - name: Run Playwright tests + run: CI=1 npx playwright test --project=chromium --project=firefox --project=webkit --project="Mobile Chrome" --project="Mobile Safari" + env: + BASE_URL: http://localhost:8080 + working-directory: e2e + + - name: Upload Playwright test results + uses: actions/upload-artifact@v4 + if: failure() + with: + name: playwright-test-results + path: e2e/test-results/ + retention-days: 30 + + - name: Upload Playwright report + uses: actions/upload-artifact@v4 + if: failure() + with: + name: playwright-report + path: e2e/playwright-report/ + retention-days: 30 + + - name: Stop docker-compose + run: docker compose -f docker-compose.ci.yml down + working-directory: e2e + if: always() diff --git a/.gitignore b/.gitignore index c11a42c9..98c332c6 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,6 @@ replay_pid* # Local docker-compose override docker-compose.override.yml +/e2e/node_modules/ +/e2e/playwright-report/ +/e2e/test-results/ diff --git a/e2e/docker-compose.ci.yml b/e2e/docker-compose.ci.yml new file mode 100644 index 00000000..70eb2613 --- /dev/null +++ b/e2e/docker-compose.ci.yml @@ -0,0 +1,59 @@ +services: + reitti: + image: dedicatedcode/reitti:latest + ports: + - 8080:8080 + depends_on: + rabbitmq: + condition: service_healthy + postgis: + condition: service_healthy + restart: true + redis: + condition: service_healthy + volumes: + - reitti-data:/data/ + environment: + POSTGIS_USER: reitti + POSTGIS_PASSWORD: reitti + POSTGIS_DB: reittidb + POSTGIS_HOST: postgis + postgis: + image: postgis/postgis:17-3.5-alpine + environment: + POSTGRES_USER: reitti + POSTGRES_PASSWORD: reitti + POSTGRES_DB: reittidb + volumes: + - postgis-data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U reitti -d reittidb"] + interval: 10s + timeout: 5s + retries: 5 + rabbitmq: + image: rabbitmq:3-management-alpine + environment: + RABBITMQ_DEFAULT_USER: reitti + RABBITMQ_DEFAULT_PASS: reitti + volumes: + - rabbitmq-data:/var/lib/rabbitmq + healthcheck: + test: ["CMD", "rabbitmq-diagnostics", "check_port_connectivity"] + interval: 30s + timeout: 10s + retries: 5 + redis: + image: redis:7-alpine + volumes: + - redis-data:/data + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 5s + retries: 5 +volumes: + postgis-data: + rabbitmq-data: + redis-data: + reitti-data: diff --git a/e2e/package-lock.json b/e2e/package-lock.json new file mode 100644 index 00000000..fe5223ab --- /dev/null +++ b/e2e/package-lock.json @@ -0,0 +1,195 @@ +{ + "name": "e2e-tests", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "e2e-tests", + "version": "1.0.0", + "license": "MIT", + "devDependencies": { + "@midleman/github-actions-reporter": "^1.10.1", + "@playwright/test": "^1.40.0" + } + }, + "node_modules/@actions/core": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.11.1.tgz", + "integrity": "sha512-hXJCSrkwfA46Vd9Z3q4cpEpHB1rL5NG04+/rbqW9d3+CSvtB1tYe8UTpAlixa1vj0m/ULglfEK2UKxMGxCxv5A==", + "dev": true, + "dependencies": { + "@actions/exec": "^1.1.1", + "@actions/http-client": "^2.0.1" + } + }, + "node_modules/@actions/exec": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@actions/exec/-/exec-1.1.1.tgz", + "integrity": "sha512-+sCcHHbVdk93a0XT19ECtO/gIXoxvdsgQLzb2fE2/5sIZmWQuluYyjPQtrtTHdU1YzTZ7bAPN4sITq2xi1679w==", + "dev": true, + "dependencies": { + "@actions/io": "^1.0.1" + } + }, + "node_modules/@actions/http-client": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.2.3.tgz", + "integrity": "sha512-mx8hyJi/hjFvbPokCg4uRd4ZX78t+YyRPtnKWwIl+RzNaVuFpQHfmlGVfsKEJN8LwTCvL+DfVgAM04XaHkm6bA==", + "dev": true, + "dependencies": { + "tunnel": "^0.0.6", + "undici": "^5.25.4" + } + }, + "node_modules/@actions/io": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@actions/io/-/io-1.1.3.tgz", + "integrity": "sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q==", + "dev": true + }, + "node_modules/@fastify/busboy": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", + "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", + "dev": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@midleman/github-actions-reporter": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@midleman/github-actions-reporter/-/github-actions-reporter-1.10.1.tgz", + "integrity": "sha512-G0kTVd8LyylE8fCvQd3CjNXbGAlCb5MO58sgKC1/PCqejz8OiVec8w5kprWFq+JRps4SkPtM4RImwO4iFHzbMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@actions/core": "^1.10.0", + "ansi-to-html": "^0.7.2", + "marked": "^12.0.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/estruyf" + }, + "peerDependencies": { + "@playwright/test": "^1.42.1" + } + }, + "node_modules/@playwright/test": { + "version": "1.56.1", + "integrity": "sha512-vSMYtL/zOcFpvJCW71Q/OEGQb7KYBPAdKh35WNSkaZA75JlAO8ED8UN6GUNTm3drWomcbcqRPFqQbLae8yBTdg==", + "dev": true, + "dependencies": { + "playwright": "1.56.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@playwright/test/node_modules/playwright": { + "version": "1.56.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.1.tgz", + "integrity": "sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw==", + "dev": true, + "dependencies": { + "playwright-core": "1.56.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/ansi-to-html": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/ansi-to-html/-/ansi-to-html-0.7.2.tgz", + "integrity": "sha512-v6MqmEpNlxF+POuyhKkidusCHWWkaLcGRURzivcU3I9tv7k4JVhFcnukrM5Rlk2rUywdZuzYAZ+kbZqWCnfN3g==", + "dev": true, + "dependencies": { + "entities": "^2.2.0" + }, + "bin": { + "ansi-to-html": "bin/ansi-to-html" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "dev": true, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "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, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/marked": { + "version": "12.0.2", + "resolved": "https://registry.npmjs.org/marked/-/marked-12.0.2.tgz", + "integrity": "sha512-qXUm7e/YKFoqFPYPa3Ukg9xlI5cyAtGmyEIzMfW//m6kXwCy2Ps9DYf5ioijFKQ8qyuscrHoY04iJGctu2Kg0Q==", + "dev": true, + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/playwright-core": { + "version": "1.56.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.56.1.tgz", + "integrity": "sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ==", + "dev": true, + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tunnel": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", + "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", + "dev": true, + "engines": { + "node": ">=0.6.11 <=0.7.0 || >=0.7.3" + } + }, + "node_modules/undici": { + "version": "5.29.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.29.0.tgz", + "integrity": "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==", + "dev": true, + "dependencies": { + "@fastify/busboy": "^2.0.0" + }, + "engines": { + "node": ">=14.0" + } + } + } +} diff --git a/e2e/package.json b/e2e/package.json new file mode 100644 index 00000000..09c29823 --- /dev/null +++ b/e2e/package.json @@ -0,0 +1,27 @@ +{ + "name": "e2e-tests", + "version": "1.0.0", + "description": "Frontend tests", + "type": "module", + "scripts": { + "test": "playwright test", + "test:headed": "playwright test --headed", + "test:debug": "playwright test --debug", + "test:ui": "playwright test --ui", + "docker:up": "docker compose up -d", + "docker:down": "docker compose down", + "test:docker": "npm run docker:up && sleep 5 && npm run test && npm run docker:down" + }, + "devDependencies": { + "@playwright/test": "^1.40.0", + "@midleman/github-actions-reporter": "^1.10.1" + }, + "keywords": [ + "playwright", + "testing", + "frontend", + "date-picker" + ], + "author": "", + "license": "MIT" +} diff --git a/e2e/playwright.config.js b/e2e/playwright.config.js new file mode 100644 index 00000000..f8431dd1 --- /dev/null +++ b/e2e/playwright.config.js @@ -0,0 +1,61 @@ +import { defineConfig, devices } from '@playwright/test'; + +/** + * @see https://playwright.dev/docs/test-configuration + */ +export default defineConfig({ + testDir: './tests', + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : undefined, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: [["html"], ["@midleman/github-actions-reporter"]], + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Base URL to use in actions like `await page.goto('/')`. */ + baseURL: 'http://localhost:8080', + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'on-first-retry', + /* Take screenshot on failure */ + screenshot: 'only-on-failure', + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + + { + name: 'firefox', + use: { ...devices['Desktop Firefox'] }, + }, + + { + name: 'webkit', + use: { ...devices['Desktop Safari'] }, + }, + + /* Test against mobile viewports. */ + { + name: 'Mobile Chrome', + use: { + ...devices['Pixel 5'], + hasTouch: true, + }, + }, + { + name: 'Mobile Safari', + use: { + ...devices['iPhone 12'], + hasTouch: true, + }, + }, + ] +}); diff --git a/e2e/tests/login.spec.js b/e2e/tests/login.spec.js new file mode 100644 index 00000000..15184852 --- /dev/null +++ b/e2e/tests/login.spec.js @@ -0,0 +1,37 @@ +import {expect, test} from '@playwright/test'; + +test.describe('Login Page Tests', () => { + test.beforeEach(async ({page}) => { + await page.goto('/'); + await page.waitForLoadState('networkidle'); + + // Wait for fonts to load to ensure consistent rendering + await page.waitForFunction(() => { + return document.fonts.ready; + }); + + if (await page.title() === 'Setup - Reitti') { + await page.locator('#password').fill('admin') + .then(() => page.keyboard.press('Enter')); + } + await page.waitForLoadState('networkidle'); + }); + + test('should load Login Page', async ({page}) => { + // Check if the page loads with the expected title + await expect(page).toHaveTitle('Reitti - Login'); + + // Check if the main container is present + await expect(page.locator('#username')).toBeVisible(); + await expect(page.locator('#password')).toBeVisible(); + + + await page.locator('#username').fill('admin'); + await page.locator('#password').fill('admin'); + + await page.locator('button:has-text("Login")').click(); + + await page.waitForLoadState('networkidle'); + await expect(page.locator('.navbar .nav-link.active')).toBeVisible(); + }); +}); diff --git a/src/main/resources/static/js/date-picker-combined.js b/src/main/resources/static/js/date-picker-combined.js index 856f0604..ddf86508 100644 --- a/src/main/resources/static/js/date-picker-combined.js +++ b/src/main/resources/static/js/date-picker-combined.js @@ -385,13 +385,13 @@ class SelectionManager { const clickedStart = getTimebandStart(itemData.date, timeband); if (this.datePicker.options.singleDateMode) { - this.#handleTimebandSingleDateMode(clickedStart, timeband, itemData); + this.#handleTimebandSingleDateMode(clickedStart, timeband); } else { - this.#handleTimebandRangeMode(clickedStart, timeband, itemData); + this.#handleTimebandRangeMode(clickedStart, timeband); } } - #handleTimebandSingleDateMode(clickedStart, timeband, itemData) { + #handleTimebandSingleDateMode(clickedStart, timeband) { if (!this.selectedStartDate) { return this.#selectSingleTimebandDate(clickedStart, timeband); } @@ -460,7 +460,7 @@ class SelectionManager { this.selectionTimeband = null; } - #handleTimebandRangeMode(clickedStart, timeband, itemData) { + #handleTimebandRangeMode(clickedStart, timeband) { if (!this.selectedStartDate || (this.selectedStartDate && this.selectedEndDate)) { // Start new timeband range this.startTimebandRangeSelection(clickedStart, timeband); @@ -816,9 +816,6 @@ class DatePicker { this.render(); this.#setupScrollListener(); this.#setupDelegatedItemEvents(); - if (this.options.startDate) { - this.setSelectedRange(this.options.startDate); - } } #createContainer() { @@ -1017,11 +1014,6 @@ class DatePicker { ); } - #attachItemEvents(element, itemData, index) { - // Backward compatibility for custom renderers that expect direct events. - this.#prepareItemElement(element, itemData, index); - } - #prepareItemElement(el, itemData, index) { if (!el) return; if (!el.classList.contains('timeband-item')) { @@ -1045,7 +1037,7 @@ class DatePicker { if (!data && el.dataset && el.dataset.time && this.itemByTime) { data = this.itemByTime.get(Number(el.dataset.time)) || null; } - if (data) this.handleItemClick(data, Number.isNaN(idx) ? undefined : idx); + if (data) this.handleItemClick(data); }; this._onMouseOver = (e) => { @@ -1073,7 +1065,7 @@ class DatePicker { this.scrollContainer.addEventListener('mouseout', this._onMouseOut, { passive: true }); } - handleItemClick(itemData, index) { + handleItemClick(itemData) { const sm = this.selectionManager; if (sm.isSelectingRange && @@ -1200,7 +1192,6 @@ class DatePicker { // Already at max zoom out (YEAR) this._wheelChainCount = 0; this._wheelChainDir = 0; - return; } else if (dir < 0) { // zoom in if (this.currentTimeband === TIMEBANDS.YEAR) { const target = new Date(date.getFullYear(), 0, 1); @@ -1215,7 +1206,6 @@ class DatePicker { // Already at max zoom in (DAY) this._wheelChainCount = 0; this._wheelChainDir = 0; - return; } } @@ -2073,7 +2063,6 @@ class DatePicker { end: TimebandUtils.getItemRangeEnd(lastItem, this.currentTimeband) }; } - } /** UMD-style export **/