From 317cdbebd379fbbae93b0b9022e46994803776bc Mon Sep 17 00:00:00 2001 From: Mikael Moilanen Date: Fri, 29 Nov 2024 08:29:31 +0200 Subject: [PATCH 1/2] Add e2e tests --- .github/workflows/pr-test.yml | 36 +++ .../src/components/admin/QuestionOptions.tsx | 3 +- client/src/components/admin/SurveyList.tsx | 7 +- .../src/components/admin/SurveyListItem.tsx | 4 +- e2e/.github/workflows/playwright.yml | 27 ++ e2e/.gitignore | 5 + e2e/Dockerfile.e2e | 69 +++++ e2e/client.env | 6 + e2e/docker-compose.yml | 40 +++ e2e/package-lock.json | 245 ++++++++++++++++++ e2e/package.json | 20 ++ e2e/pages/adminPage.ts | 32 +++ e2e/pages/publishedSurveyPage.ts | 23 ++ e2e/pages/surveyEditPage.ts | 106 ++++++++ e2e/playwright.config.ts | 15 ++ e2e/server.env | 35 +++ e2e/tests/survey.test.ts | 40 +++ e2e/utils/data.ts | 22 ++ e2e/utils/db.ts | 55 ++++ e2e/utils/fixtures.ts | 22 ++ server/src/database.ts | 24 +- 21 files changed, 820 insertions(+), 16 deletions(-) create mode 100644 e2e/.github/workflows/playwright.yml create mode 100644 e2e/.gitignore create mode 100644 e2e/Dockerfile.e2e create mode 100644 e2e/client.env create mode 100644 e2e/docker-compose.yml create mode 100644 e2e/package-lock.json create mode 100644 e2e/package.json create mode 100644 e2e/pages/adminPage.ts create mode 100644 e2e/pages/publishedSurveyPage.ts create mode 100644 e2e/pages/surveyEditPage.ts create mode 100644 e2e/playwright.config.ts create mode 100644 e2e/server.env create mode 100644 e2e/tests/survey.test.ts create mode 100644 e2e/utils/data.ts create mode 100644 e2e/utils/db.ts create mode 100644 e2e/utils/fixtures.ts diff --git a/.github/workflows/pr-test.yml b/.github/workflows/pr-test.yml index a5e42372..8cd7b315 100644 --- a/.github/workflows/pr-test.yml +++ b/.github/workflows/pr-test.yml @@ -33,3 +33,39 @@ jobs: run: npm ci - name: Build run: npm run build + test-e2e: + name: Test E2E + runs-on: ubuntu-latest + defaults: + run: + working-directory: e2e + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: '18' + cache: 'npm' + cache-dependency-path: ./e2e/package-lock.json + - run: npm i + - name: Get installed Playwright version + id: playwright-version + run: echo "version=$(npm ls @playwright/test --json | jq --raw-output '.dependencies["@playwright/test"].version')" >> $GITHUB_OUTPUT + - uses: actions/cache@v4 + id: playwright-cache + with: + path: ~/.cache/ms-playwright + key: ${{ runner.os }}-playwright-${{ steps.playwright-version.outputs.version }} + restore-keys: | + ${{ runner.os }}-playwright- + # Install Playwright dependencies unless found from the cache + - name: Install Playwright's dependencies + if: steps.playwright-cache.outputs.cache-hit != 'true' + run: npx playwright install --with-deps + # Start up the stack & wait for all services to be healthy + - run: CI=1 docker compose up -d --wait + timeout-minutes: 10 + # Output container logs if any of the previous steps failed + - run: docker compose logs + if: failure() + # Execute the E2E tests + - run: npm test diff --git a/client/src/components/admin/QuestionOptions.tsx b/client/src/components/admin/QuestionOptions.tsx index 73b42874..807be131 100644 --- a/client/src/components/admin/QuestionOptions.tsx +++ b/client/src/components/admin/QuestionOptions.tsx @@ -124,7 +124,7 @@ export default function QuestionOptions({ disabled={disabled} aria-label="add-question-option" size="small" - sx={{boxShadow: 'none'}} + sx={{ boxShadow: 'none' }} onClick={() => { onChange([...options, { text: initializeLocalizedObject('') }]); }} @@ -139,6 +139,7 @@ export default function QuestionOptions({
) : ( - <> +
    {surveys .filter((s) => filterTags.length @@ -137,7 +140,7 @@ export default function SurveyList() { .map((survey) => ( ))} - +
)}
); diff --git a/client/src/components/admin/SurveyListItem.tsx b/client/src/components/admin/SurveyListItem.tsx index 94c4f84d..c7f382f4 100644 --- a/client/src/components/admin/SurveyListItem.tsx +++ b/client/src/components/admin/SurveyListItem.tsx @@ -92,7 +92,7 @@ export default function SurveyListItem(props: Props) { }, [survey.name]); return ( - <> +
  • - +
  • ); } diff --git a/e2e/.github/workflows/playwright.yml b/e2e/.github/workflows/playwright.yml new file mode 100644 index 00000000..3eb13143 --- /dev/null +++ b/e2e/.github/workflows/playwright.yml @@ -0,0 +1,27 @@ +name: Playwright Tests +on: + push: + branches: [ main, master ] + pull_request: + branches: [ main, master ] +jobs: + test: + timeout-minutes: 60 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: lts/* + - name: Install dependencies + run: npm ci + - name: Install Playwright Browsers + run: npx playwright install --with-deps + - name: Run Playwright tests + run: npx playwright test + - uses: actions/upload-artifact@v4 + if: ${{ !cancelled() }} + with: + name: playwright-report + path: playwright-report/ + retention-days: 30 diff --git a/e2e/.gitignore b/e2e/.gitignore new file mode 100644 index 00000000..68c5d18f --- /dev/null +++ b/e2e/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/e2e/Dockerfile.e2e b/e2e/Dockerfile.e2e new file mode 100644 index 00000000..fd1acf5e --- /dev/null +++ b/e2e/Dockerfile.e2e @@ -0,0 +1,69 @@ +## +# E2E test Dockerfile +## + +### +# Base image declaration +### +FROM node:20.5-alpine AS base + +ENV APPDIR /app + +### +# Client build stage +### +FROM base AS client-build + +WORKDIR ${APPDIR}/client + +COPY client/package*.json ./ +RUN npm ci + +COPY interfaces ../interfaces +COPY client ./ +RUN npm run build + +### +# Server build stage +### +FROM base AS server-build + +WORKDIR ${APPDIR}/server + +# Tell Puppeteer to skip installing Chrome. We'll be using the installed package. +ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true + +COPY server/package*.json ./ +RUN npm ci + +COPY interfaces ../interfaces +COPY server ./ +RUN npm run build +RUN rm -r src + +### +# Main image build +### +FROM base AS main + +# Install all dependencies (GDAL & others for Puppeteer) +RUN apk update && apk add \ + gdal-tools \ + chromium \ + nss \ + freetype \ + harfbuzz \ + ca-certificates \ + ttf-freefont + +WORKDIR ${APPDIR} + +COPY --from=server-build ${APPDIR}/server ./ +COPY --from=client-build ${APPDIR}/client/dist ./static/ + +ENV TZ=Europe/Helsinki + +# Define Chromium path, as it was not installed in the previous phase +ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium-browser + +CMD npm start diff --git a/e2e/client.env b/e2e/client.env new file mode 100644 index 00000000..59aa232e --- /dev/null +++ b/e2e/client.env @@ -0,0 +1,6 @@ +# Webpack dev server port +PORT=8080 +# API server URL +API_URL=http://e2e-server:3000 +# Use polling for watching file changes (may be required in Windows unless using WSL2) +VITE_USE_POLLING=false diff --git a/e2e/docker-compose.yml b/e2e/docker-compose.yml new file mode 100644 index 00000000..e6d97042 --- /dev/null +++ b/e2e/docker-compose.yml @@ -0,0 +1,40 @@ +services: + e2e-client: + build: + context: ../client + dockerfile: Dockerfile.develop + volumes: + # Include .git for detecting changes in the workspace (e.g. only run tests on changed files) + - ../.git:/app/.git:cached + - ../client:/app/client:cached + - ./client.env:/app/client/.env + - ../interfaces:/app/interfaces:cached + - /app/client/node_modules + ports: + - '127.0.0.1:8080:8080' + e2e-server: + build: + context: ../server + dockerfile: Dockerfile.develop + volumes: + # Include .git for detecting changes in the workspace (e.g. only run tests on changed files) + - ../.git:/app/.git:cached + - ../server:/app/server:cached + - ./server.env:/app/server/.env + - ../interfaces:/app/interfaces:cached + - /app/server/node_modules + ports: + - '127.0.0.1:3000:3000' + e2e-database: + image: kartoza/postgis:12.4 + environment: + POSTGRES_USER: vuorovaikutusalusta_user + POSTGRES_PASSWORD: password + POSTGRES_DB: vuorovaikutusalusta_e2e_db + DEFAULT_COLLATION: 'fi_FI.UTF8' + DEFAULT_CTYPE: 'fi_FI.UTF8' + TZ: Europe/Helsinki + volumes: + - .db-data:/var/lib/postgresql:delegated + ports: + - '127.0.0.1:5432:5432' diff --git a/e2e/package-lock.json b/e2e/package-lock.json new file mode 100644 index 00000000..a54e67ce --- /dev/null +++ b/e2e/package-lock.json @@ -0,0 +1,245 @@ +{ + "name": "e2e", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "e2e", + "version": "1.0.0", + "license": "ISC", + "devDependencies": { + "@playwright/test": "^1.49.0", + "@types/node": "^22.10.1", + "dayjs": "^1.10.7", + "pg": "^8.13.1" + } + }, + "node_modules/@playwright/test": { + "version": "1.49.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.49.0.tgz", + "integrity": "sha512-DMulbwQURa8rNIQrf94+jPJQ4FmOVdpE5ZppRNvWVjvhC+6sOeo28r8MgIpQRYouXRtt/FCCXU7zn20jnHR4Qw==", + "dev": true, + "dependencies": { + "playwright": "1.49.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@types/node": { + "version": "22.10.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.1.tgz", + "integrity": "sha512-qKgsUwfHZV2WCWLAnVP1JqnpE6Im6h3Y0+fYgMTasNQ7V++CBX5OT1as0g0f+OyubbFqhf6XVNIsmN4IIhEgGQ==", + "dev": true, + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "node_modules/dayjs": { + "version": "1.11.13", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", + "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==", + "dev": true + }, + "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/pg": { + "version": "8.13.1", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.13.1.tgz", + "integrity": "sha512-OUir1A0rPNZlX//c7ksiu7crsGZTKSOXJPgtNiHGIlC9H0lO+NC6ZDYksSgBYY/thSWhnSRBv8w1lieNNGATNQ==", + "dev": true, + "dependencies": { + "pg-connection-string": "^2.7.0", + "pg-pool": "^3.7.0", + "pg-protocol": "^1.7.0", + "pg-types": "^2.1.0", + "pgpass": "1.x" + }, + "engines": { + "node": ">= 8.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.1.1" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.1.1.tgz", + "integrity": "sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==", + "dev": true, + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.7.0.tgz", + "integrity": "sha512-PI2W9mv53rXJQEOb8xNR8lH7Hr+EKa6oJa38zsK0S/ky2er16ios1wLKhZyxzD7jUReiWokc9WK5nxSnC7W1TA==", + "dev": true + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "dev": true, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.7.0.tgz", + "integrity": "sha512-ZOBQForurqh4zZWjrgSwwAtzJ7QiRX0ovFkZr2klsen3Nm0aoh33Ls0fzfv3imeH/nw/O27cjdz5kzYJfeGp/g==", + "dev": true, + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.7.0.tgz", + "integrity": "sha512-hTK/mE36i8fDDhgDFjy6xNOG+LCorxLG3WO17tku+ij6sVHXh1jQUJ8hYAnRhNla4QVD2H8er/FOjc/+EgC6yQ==", + "dev": true + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "dev": true, + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "dev": true, + "dependencies": { + "split2": "^4.1.0" + } + }, + "node_modules/playwright": { + "version": "1.49.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.49.0.tgz", + "integrity": "sha512-eKpmys0UFDnfNb3vfsf8Vx2LEOtflgRebl0Im2eQQnYMA4Aqd+Zw8bEOB+7ZKvN76901mRnqdsiOGKxzVTbi7A==", + "dev": true, + "dependencies": { + "playwright-core": "1.49.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.49.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.49.0.tgz", + "integrity": "sha512-R+3KKTQF3npy5GTiKH/T+kdhoJfJojjHESR1YEWhYuEKRVfVaxH3+4+GvXE5xyCngCxhxnykk0Vlah9v8fs3jA==", + "dev": true, + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", + "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "dev": true, + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "dev": true, + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true, + "engines": { + "node": ">=0.4" + } + } + } +} diff --git a/e2e/package.json b/e2e/package.json new file mode 100644 index 00000000..6755b05b --- /dev/null +++ b/e2e/package.json @@ -0,0 +1,20 @@ +{ + "name": "e2e", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "codegen": "playwright codegen http://localhost:8080/admin", + "test-ui": "playwright test --ui", + "test": "playwright test --workers=1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "devDependencies": { + "@playwright/test": "^1.49.0", + "@types/node": "^22.10.1", + "pg": "^8.13.1", + "dayjs": "^1.10.7" + } +} diff --git a/e2e/pages/adminPage.ts b/e2e/pages/adminPage.ts new file mode 100644 index 00000000..2f518119 --- /dev/null +++ b/e2e/pages/adminPage.ts @@ -0,0 +1,32 @@ +import { expect, Page } from '@playwright/test'; + +export class SurveyAdminPage { + private _page: Page; + + constructor(page: Page) { + this._page = page; + } + + get page() { + return this._page; + } + + async goto() { + await this._page.goto(`http://localhost:8080/admin/`); + } + + async getSurveyList() { + return this._page.getByTestId('survey-admin-list').all(); + } + + async publishSurvey(surveyName: string) { + await this._page + .getByRole('listitem') + .filter({ hasText: surveyName }) + .getByText('julkaise') + .click(); + + await this._page.getByRole('button', { name: 'Kyllä' }).click(); + await expect(this._page.getByText('Kysely julkaistu')).toBeVisible(); + } +} diff --git a/e2e/pages/publishedSurveyPage.ts b/e2e/pages/publishedSurveyPage.ts new file mode 100644 index 00000000..3f16a39f --- /dev/null +++ b/e2e/pages/publishedSurveyPage.ts @@ -0,0 +1,23 @@ +import { Page } from '@playwright/test'; + +export class PublishedSurveyPage { + private _page: Page; + + constructor(page: Page) { + this._page = page; + } + + get page() { + return this._page; + } + + async goto(surveyName: string) { + await this._page.goto(`http://localhost:8080/${surveyName}`); + } + + async startSurvey() { + await this._page + .getByRole('button', { name: 'Aloita kysely tästä' }) + .click(); + } +} diff --git a/e2e/pages/surveyEditPage.ts b/e2e/pages/surveyEditPage.ts new file mode 100644 index 00000000..02b54d23 --- /dev/null +++ b/e2e/pages/surveyEditPage.ts @@ -0,0 +1,106 @@ +import { Page } from '@playwright/test'; + +export interface SurveyParams { + title: string; + subtitle: string; + urlName: string; + author: string; + startDate: string; // DD.MM.YYYY hh:mm + endDate: string; // DD.MM.YYYY hh:mm +} + +export interface RadioQuestionParams { + pageName: string; + title: string; + isRequired?: boolean; + answerOptions: string[]; + allowCustom?: boolean; + additionalInfo?: string; +} + +export class SurveyEditPage { + private _page: Page; + private _surveyId: string | null; + + constructor(page: Page, surveyId?: string) { + this._page = page; + this._surveyId = surveyId ?? null; + } + + get page() { + return this._page; + } + + get surveyId() { + return this._surveyId; + } + + async goto() { + if (this._surveyId) { + await this._page.goto( + `http://localhost:8080/admin/kyselyt/${this._surveyId}`, + ); + } else { + await this._page.goto('http://localhost:8080/admin'); + await this._page.getByRole('button', { name: 'Uusi kysely' }).click(); + await this._page.waitForURL('http://localhost:8080/admin/kyselyt/**'); + const urlParts = this._page.url().split('/'); + const lastElement = urlParts[urlParts.length - 1]; + this._surveyId = lastElement; + } + } + + async fillBasicInfo(params: SurveyParams) { + await this._page.getByRole('link', { name: 'Kyselyn perustiedot' }).click(); + await this._page.getByLabel('Kyselyn otsikko *').fill(params.title); + await this._page.getByLabel('Kyselyn aliotsikko').fill(params.subtitle); + await this._page.getByLabel('Kyselyn nimi *').fill(params.urlName); + await this._page + .getByLabel('Kyselyn laatija/yhteyshenkil') + .fill(params.author); + await this._page.getByLabel('Alkamisaika').fill(params.startDate); + await this._page.getByLabel('Loppumisaika').fill(params.endDate); + await this._page.getByLabel('save-changes').click(); + } + + async goToPage(pageName: string) { + await this._page.getByRole('link', { name: pageName }).click(); + } + + async renamePage(oldName: string, newName: string) { + await this.goToPage(oldName); + await this._page.getByLabel('Sivun nimi *').fill(newName); + } + + async createRadioQuestion(radioQuestionParams: RadioQuestionParams) { + await this.goToPage(radioQuestionParams.pageName); + await this._page.getByLabel('add-radio-question').click(); + await this._page.getByLabel('Otsikko').fill(radioQuestionParams.title); + if (radioQuestionParams.isRequired) { + await this._page + .getByRole('checkbox', { name: 'Vastaus pakollinen' }) + .check(); + } + + for (const [idx, option] of radioQuestionParams.answerOptions.entries()) { + if (idx > 0) await this._page.getByLabel('add-question-option').click(); + await this._page + .getByTestId(`radio-input-option-${idx}`) + .locator('textarea') + .nth(0) + .fill(option); + } + if (radioQuestionParams.allowCustom) { + await this._page.getByLabel('Salli “Jokin muu, mikä?” -').check(); + } + if (radioQuestionParams.additionalInfo) { + await this._page.getByLabel('Anna lisätietoja kysymykseen').check(); + await this._page + .getByLabel('rdw-editor') + .locator('div') + .nth(2) + .fill(radioQuestionParams.additionalInfo); + } + await this._page.getByLabel('save-changes').click(); + } +} diff --git a/e2e/playwright.config.ts b/e2e/playwright.config.ts new file mode 100644 index 00000000..9161e7a0 --- /dev/null +++ b/e2e/playwright.config.ts @@ -0,0 +1,15 @@ +import { PlaywrightTestConfig, devices } from '@playwright/test'; + +const config: PlaywrightTestConfig = { + projects: [ + { + name: 'Chrome', + use: { + ...devices['Desktop Chrome'], + contextOptions: { ignoreHTTPSErrors: true }, + }, + }, + ], +}; + +export default config; diff --git a/e2e/server.env b/e2e/server.env new file mode 100644 index 00000000..d609aa9f --- /dev/null +++ b/e2e/server.env @@ -0,0 +1,35 @@ +# Timezone +TZ=Europe/Helsinki +# Server port +PORT=3000 +# Database URL +DATABASE_URL=postgres://vuorovaikutusalusta_user:password@e2e-database:5432/vuorovaikutusalusta_e2e_db +# How many times to retry DB connection, if it fails on startup? +DATABASE_CONNECT_RETRIES=10 +# How long to wait until next DB connection retry after a failed attempt (in milliseconds) +DATABASE_CONNECT_RETRY_TIMEOUT=2000 + +# Authentication parameters +AUTH_ENABLED=false + +# Session secret for signing the session cookie +SESSION_SECRET=top_secret + +# How many screenshot tasks can be run at the same time? +PUPPETEER_CLUSTER_MAX_CONCURRENCY=2 +# How long to wait for network to become idle before snapping each screenshot? (in milliseconds) +PUPPETEER_NETWORK_IDLE_TIMEOUT=10000 + +# Email configurations +EMAIL_ENABLED=true +EMAIL_SERVICE= +EMAIL_OAUTH_CLIENT_ID= +EMAIL_OAUTH_CLIENT_SECRET= +EMAIL_OAUTH_REFRESH_TOKEN= +EMAIL_OAUTH_ACCESS_URL= +EMAIL_SENDER_ADDRESS= +EMAIL_SENDER_NAME= +# This address will be used for creating a link to the application inside the message +EMAIL_APP_URL=http://localhost:8080 +# Is user grouping enabled +USER_GROUPING_ENABLED=true \ No newline at end of file diff --git a/e2e/tests/survey.test.ts b/e2e/tests/survey.test.ts new file mode 100644 index 00000000..b2a71540 --- /dev/null +++ b/e2e/tests/survey.test.ts @@ -0,0 +1,40 @@ +import { expect } from '@playwright/test'; +import { getRadioQuestionData, testSurveyData } from '../utils/data'; +import { test } from '../utils/fixtures'; +import { clearData } from '../utils/db'; + +test.describe('Create a survey', () => { + test.afterAll(async () => { + await clearData(); + }); + test('with a radio question', async ({ + surveyEditPage, + surveyAdminPage, + surveyPage, + }) => { + await surveyEditPage.goto(); + await surveyEditPage.fillBasicInfo(testSurveyData); + expect(surveyEditPage.surveyId).not.toBeNull(); + await surveyEditPage.renamePage('Nimetön sivu', 'Sivu 1'); + const radioQuestion = getRadioQuestionData('Sivu 1'); + await surveyEditPage.createRadioQuestion(radioQuestion); + + await surveyAdminPage.goto(); + await expect( + surveyAdminPage.page + .locator('h3') + .filter({ hasText: testSurveyData.title }), + ).toBeVisible(); + expect(await surveyAdminPage.getSurveyList()).toHaveLength(1); + await surveyAdminPage.publishSurvey(testSurveyData.title); + + await surveyPage.goto(testSurveyData.urlName); + await surveyPage.startSurvey(); + expect(await surveyPage.page.locator('h1').textContent()).toBe( + testSurveyData.title, + ); + expect(await surveyPage.page.locator('h3').textContent()).toContain( + radioQuestion.title, + ); + }); +}); diff --git a/e2e/utils/data.ts b/e2e/utils/data.ts new file mode 100644 index 00000000..ad87977d --- /dev/null +++ b/e2e/utils/data.ts @@ -0,0 +1,22 @@ +import { RadioQuestionParams, SurveyParams } from '../pages/surveyEditPage'; +import dayjs from 'dayjs'; + +export const testSurveyData: SurveyParams = { + title: 'Testikysely', + subtitle: 'Testikyselyn aliotsikko', + urlName: 'testikysely', + author: 'Testaaja', + startDate: dayjs().add(1, 'day').format('DD.MM.YYYY HH:mm'), + endDate: dayjs().add(1, 'year').format('DD.MM.YYYY HH:mm'), +}; + +export function getRadioQuestionData(pageName: string): RadioQuestionParams { + return { + pageName: pageName, + title: 'Mikä on lempivärisi?', + answerOptions: ['Punainen', 'Vihreä', 'Sininen'], + isRequired: true, + allowCustom: false, + additionalInfo: 'Valitse vain yksi vaihtoehto', + }; +} diff --git a/e2e/utils/db.ts b/e2e/utils/db.ts new file mode 100644 index 00000000..ca810363 --- /dev/null +++ b/e2e/utils/db.ts @@ -0,0 +1,55 @@ +import pg from 'pg'; + +const { Pool } = pg; + +class DatabaseConnection { + private pool: any; + + constructor() { + this.pool = new Pool({ + user: 'vuorovaikutusalusta_user', + host: '127.0.0.1', + database: 'vuorovaikutusalusta_e2e_db', + password: 'password', + port: 5432, + }); + } + + private async disconnect() { + await this.pool.end(); + } + + async query(query: string) { + try { + const res = await this.pool.query(query); + return res.rows; + } catch (err) { + console.log(err); + } + } +} + +const connection = new DatabaseConnection(); + +export async function clearData() { + return connection.query(` + CREATE OR REPLACE FUNCTION data.truncate_tables( + ) + RETURNS void + LANGUAGE 'sql' + COST 100 + VOLATILE PARALLEL UNSAFE + AS $BODY$ + DO $$ DECLARE + table_name text; + BEGIN + FOR table_name IN (SELECT tablename FROM pg_tables WHERE schemaname='data') LOOP + EXECUTE 'TRUNCATE TABLE data."' || table_name || '" CASCADE;'; + END LOOP; + END $$; + $BODY$; + + + +SELECT data.truncate_tables();`); +} diff --git a/e2e/utils/fixtures.ts b/e2e/utils/fixtures.ts new file mode 100644 index 00000000..0bdce24b --- /dev/null +++ b/e2e/utils/fixtures.ts @@ -0,0 +1,22 @@ +import { test as base } from '@playwright/test'; +import { SurveyEditPage } from '../pages/surveyEditPage'; +import { SurveyAdminPage } from '../pages/adminPage'; +import { PublishedSurveyPage } from '../pages/publishedSurveyPage'; + +interface PageFixtures { + surveyEditPage: SurveyEditPage; + surveyAdminPage: SurveyAdminPage; + surveyPage: PublishedSurveyPage; +} + +export const test = base.extend({ + surveyEditPage: async ({ page }, use) => { + await use(new SurveyEditPage(page)); + }, + surveyAdminPage: async ({ page }, use) => { + await use(new SurveyAdminPage(page)); + }, + surveyPage: async ({ page }, use) => { + await use(new PublishedSurveyPage(page)); + }, +}); diff --git a/server/src/database.ts b/server/src/database.ts index b363739b..8361fa5a 100644 --- a/server/src/database.ts +++ b/server/src/database.ts @@ -34,7 +34,9 @@ export async function initializeDatabase() { // Test connection and store it for the migration migrationConnection = await db.connect(); } catch (error) { - logger.warn(`Error connecting to database: ${error}`); + logger.warn( + `Error connecting to database: ${error} for ${process.env.DATABASE_URL}`, + ); if (retryCount < connectRetries) { logger.info( `Retrying database connection (${++retryCount}/${connectRetries})`, @@ -100,16 +102,16 @@ export function getGeoJSONColumn( return !value ? 'NULL' : value.properties?.bufferRadius != null - ? // If geometry provided with buffer radius, add ST_Buffer - pgp.as.format( - 'public.ST_Buffer(public.ST_SetSRID(public.ST_GeomFromGeoJSON($1), $2), $3)', - [value, inputSRID, value.properties.bufferRadius], - ) - : pgp.as.format( - // Transform provided geometry to default SRID - 'public.ST_SetSRID(public.ST_GeomFromGeoJSON($1), $2)', - [value, inputSRID], - ); + ? // If geometry provided with buffer radius, add ST_Buffer + pgp.as.format( + 'public.ST_Buffer(public.ST_SetSRID(public.ST_GeomFromGeoJSON($1), $2), $3)', + [value, inputSRID, value.properties.bufferRadius], + ) + : pgp.as.format( + // Transform provided geometry to default SRID + 'public.ST_SetSRID(public.ST_GeomFromGeoJSON($1), $2)', + [value, inputSRID], + ); }, }; } From 0ac5fe3dfd6fd8df83602d17a597d0195f25b8fd Mon Sep 17 00:00:00 2001 From: Mikael Moilanen Date: Fri, 29 Nov 2024 14:48:18 +0200 Subject: [PATCH 2/2] Add new github workflows and run application with custom user --- .github/release-drafter.yml | 44 ++++++++++++++++++++ .github/workflows/{production.yml => ci.yml} | 17 ++++---- .github/workflows/pr-test.yml | 4 +- .github/workflows/release-draft.yml | 26 ++++++++++++ .github/workflows/test.yml | 33 --------------- Dockerfile | 11 ++++- README.md | 18 ++++++-- e2e/docker-compose.yml | 10 ++++- e2e/pages/surveyEditPage.ts | 4 +- e2e/playwright.config.ts | 1 + server/Dockerfile.develop | 3 +- 11 files changed, 120 insertions(+), 51 deletions(-) create mode 100644 .github/release-drafter.yml rename .github/workflows/{production.yml => ci.yml} (50%) create mode 100644 .github/workflows/release-draft.yml delete mode 100644 .github/workflows/test.yml diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml new file mode 100644 index 00000000..23e2d3ed --- /dev/null +++ b/.github/release-drafter.yml @@ -0,0 +1,44 @@ +name-template: 'v$RESOLVED_VERSION' +tag-template: 'v$RESOLVED_VERSION' +categories: + - title: 'Features' + label: 'feature' + - title: 'Bug fixes' + label: 'fix' + - title: 'Infra changes' + label: 'infra' + - title: 'Maintenance' + label: 'chore' +autolabeler: + - label: 'feature' + branch: + - '/feature\/.+/' + - label: 'fix' + branch: + - '/fix\/.+/' + - label: 'infra' + branch: + - '/infra\/.+/' + - label: 'chore' + branch: + - '/chore\/.+/' + - '/dependabot\/.+/' +version-resolver: + major: + labels: + - 'major' + minor: + labels: + - 'minor' + - 'feature' + patch: + labels: + - 'patch' + - 'fix' + - 'chore' + - 'infra' + default: patch +template: | + ## Changes + + $CHANGES diff --git a/.github/workflows/production.yml b/.github/workflows/ci.yml similarity index 50% rename from .github/workflows/production.yml rename to .github/workflows/ci.yml index d8bcf413..f65abf52 100644 --- a/.github/workflows/production.yml +++ b/.github/workflows/ci.yml @@ -1,21 +1,24 @@ -name: Production deployment +name: Application deployment to test and production on: push: + # On push to main branch deploy to test branches: [main] - workflow_dispatch: + release: + # On release deploy to production + types: [released] env: - IMAGE_NAME: ${{ secrets.ACR_REGISTRY_ADDRESS }}/vuorovaikutusalusta + IMAGE_NAME_TEST: ${{ secrets.ACR_REGISTRY_ADDRESS }}/vuorovaikutusalusta-test + IMAGE_NAME_PROD: ${{ secrets.ACR_REGISTRY_ADDRESS }}/vuorovaikutusalusta jobs: deploy: - name: Deploy to production + name: Deploy to test or production runs-on: ubuntu-latest steps: - name: Check out code uses: actions/checkout@v4 - - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Login to registry @@ -28,6 +31,6 @@ jobs: uses: docker/build-push-action@v5 with: push: true - cache-from: type=registry,ref=${{ env.IMAGE_NAME }}:latest + cache-from: type=registry,ref=${{ github.event_name == 'release' && env.IMAGE_NAME_PROD || env.IMAGE_NAME_TEST }}:latest cache-to: type=inline - tags: ${{ env.IMAGE_NAME }}:latest + tags: ${{ github.event_name == 'release' && env.IMAGE_NAME_PROD || env.IMAGE_NAME_TEST }}:latest diff --git a/.github/workflows/pr-test.yml b/.github/workflows/pr-test.yml index 8cd7b315..372afaf0 100644 --- a/.github/workflows/pr-test.yml +++ b/.github/workflows/pr-test.yml @@ -1,5 +1,5 @@ ## -# Runs builds on GitHub pull requests to test that they should be working fine. +# Runs builds and e2e tests ## name: Test builds on: @@ -43,7 +43,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - node-version: '18' + node-version: '20' cache: 'npm' cache-dependency-path: ./e2e/package-lock.json - run: npm i diff --git a/.github/workflows/release-draft.yml b/.github/workflows/release-draft.yml new file mode 100644 index 00000000..f397dbaf --- /dev/null +++ b/.github/workflows/release-draft.yml @@ -0,0 +1,26 @@ +name: Release draft + +on: + # Update release draft on pushes to the main branch + push: + branches: [main] + # Run on PRs only for autolabeler + pull_request: + types: [opened, reopened, synchronize] + +permissions: + contents: read + +jobs: + update-draft: + name: Update draft + permissions: + contents: write + pull-requests: write + runs-on: ubuntu-latest + steps: + - uses: release-drafter/release-drafter@v6 + with: + commitish: main + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml deleted file mode 100644 index 573a4207..00000000 --- a/.github/workflows/test.yml +++ /dev/null @@ -1,33 +0,0 @@ -name: Test deployment - -on: - push: - branches: [develop] - workflow_dispatch: - -env: - IMAGE_NAME: ${{ secrets.ACR_REGISTRY_ADDRESS }}/vuorovaikutusalusta-test - -jobs: - deploy: - name: Deploy to test - runs-on: ubuntu-latest - steps: - - name: Check out code - uses: actions/checkout@v4 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - name: Login to registry - uses: docker/login-action@v3 - with: - registry: ${{ secrets.ACR_REGISTRY_ADDRESS }} - username: ${{ secrets.ACR_REGISTRY_USERNAME }} - password: ${{ secrets.ACR_REGISTRY_PASSWORD }} - - name: Build - uses: docker/build-push-action@v5 - with: - push: true - cache-from: type=registry,ref=${{ env.IMAGE_NAME }}:latest - cache-to: type=inline - tags: ${{ env.IMAGE_NAME }}:latest diff --git a/Dockerfile b/Dockerfile index 1c2b7c24..0e633386 100644 --- a/Dockerfile +++ b/Dockerfile @@ -56,14 +56,21 @@ RUN apk update && apk add \ ca-certificates \ ttf-freefont +# Add non-root user with explicit UID and GID +RUN addgroup --system --gid 1001 appUser && \ + adduser --system --uid 1001 appGroup + WORKDIR ${APPDIR} -COPY --from=server-build ${APPDIR}/server ./ -COPY --from=client-build ${APPDIR}/client/dist ./static/ +COPY --chown=appUser:appGroup --from=server-build ${APPDIR}/server ./ +COPY --chown=appUser:appGroup --from=client-build ${APPDIR}/client/dist ./static/ ENV TZ=Europe/Helsinki # Define Chromium path, as it was not installed in the previous phase ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium-browser +# Don't run the app as root +USER appUser + CMD npm start diff --git a/README.md b/README.md index 74a310b6..a854a717 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Vuorovaikutusalusta +# Kartalla ## Ohjelmiston taustaa @@ -17,7 +17,7 @@ Kuva 1: ohjelmiston arkkitehtuuri ajoympäristössään - Käynnistä Docker -ekosysteemi projektin juuresssa komennoilla `docker-compose build && docker-compose up -d`. Esiehto: lokaalisti tulee olla asennettuna [Docker -konttien hallintajärjestelmä](https://www.docker.com/products/docker-desktop)). - Luo ympäristömuuttujille tiedosto polkuun `/server/.env` ja täytä se tarvittavilla muuttujilla ohjeen `/server/.template.env` mukaan. -- Toteuta uudet toiminnallisuudet omaan Git -haaraansa, esim. `feature/new-feature-name`. Valmistuessaan yhdistä tämä haara `develop` -haaraan, josta sovellusta ajetaan testiympäristössä. Kun on aika tehdä tuotantopäivitys, vie `develop` -haaran muutokset `master` -haaraan, josta sovellusta ajetaan tuotantoympäristössä. +- Toteuta uudet toiminnallisuudet omaan Git -haaraansa, esim. `feature/new-feature-name`. Valmistuessaan yhdistä tämä haara `develop` -haaraan, josta sovellusta ajetaan testiympäristössä. Kun on aika tehdä tuotantopäivitys, vie `develop` -haaran muutokset `main` -haaraan, josta sovellusta ajetaan tuotantoympäristössä.
    @@ -29,6 +29,16 @@ Serveri ja tietokanta juttelevat keskenään yhteydellä, joka on määritetty y Lokaalissa kehityksessä React käyttöliittymä ohjaa rajapintapyynnöt automaattisesti omaan porttiinsa. Toisin sanoen, mikäli käyttöliittymästä (portti 8080) tehdään HTTP pyyntö serverille (portti 3000), tätä ei tarvitse erikseen määrittää, vaan käyttöliittymä osaa ohjata liikenteen suoraan omasta portistaan serverin porttiin (8080 -> 3000). -Sovelluskehitys noudattaa perinteistä [Gitflow](https://www.atlassian.com/git/tutorials/comparing-workflows/gitflow-workflow#:~:text=The%20overall%20flow%20of%20Gitflow,branch%20is%20created%20from%20main&text=When%20a%20feature%20is%20complete%20it%20is%20merged%20into%20the,branch%20is%20created%20from%20main) -mallia, jossa uudet toiminnallisuudet toteutetaan omaan Git -haaraansa, esim. `feature/new-feature-name`. Valmistuessaan tämä haara yhdistetään `develop` -haaraan. Kun `develop` -haaraan kohdistuu muutoksia Githubissa, automaattinen integraatio käynnistyy, joka julkistaa haaraan viedyn uuden lähdekoodin Azureen testiympäristöön. Kun tulee aika tehdä tuotantopäivitys, yhdistetään `develop` -haaran muutokset `master` -haaraan. Githubissa `master` -haaran muutokset käynnistävät automaattisen integraation, joka julkistaa lähdekoodin Azuren DevOps -palveluun. Täältä lähdekoodi taas julkistetaan automaattisesti Azureen tuotantoympäristöön. +Sovelluskehitys noudattaa perinteistä [Gitflow](https://www.atlassian.com/git/tutorials/comparing-workflows/gitflow-workflow#:~:text=The%20overall%20flow%20of%20Gitflow,branch%20is%20created%20from%20main&text=When%20a%20feature%20is%20complete%20it%20is%20merged%20into%20the,branch%20is%20created%20from%20main) -mallia, jossa uudet toiminnallisuudet toteutetaan omaan Git -haaraansa, esim. `feature/new-feature-name`. Valmistuessaan tämä haara yhdistetään `main` -haaraan. Kun `main` -haaraan kohdistuu muutoksia Githubissa, automaattinen integraatio käynnistyy, joka julkistaa haaraan viedyn uuden lähdekoodin Azureen testiympäristöön. `main`-haaraan yhdistäminen täydentää automaattisesti `release`-luonnoksen, jonka julkaisun yhteydessä `main` haaran sisältö viedään automaattisen integraation kautta Azuren tuotantoympäristöön. -## TODO \ No newline at end of file +## E2E-testaus + +E2E testit ajetaan automaattisesti jokaisen pull requestin yhteydessä. + +E2E-testiympäristö on toteutettu vastaavalla tavalla, kuin paikallinen kehitysympäristö sillä erolla, että E2E-testiympäristö käynnistetään `e2e`-kansiosta käsin. Testit ajetaan `Playwright`-kirjastoa käyttäen. Tietokannan sisältö tallennetaan erilliseen `db-data`-volumeen, joten E2E-testien ajaminen ei vaikuta kehitystietokannan sisältöön. + +Testiympäristön käynnistämisen jälkeen seuraavat komennot ovat käytettävissä `./e2e`-polusta: + +- `npm run codegen`: Avaa selainnäkymän, josta käsin pystyy luomaan testikomentoja interaktiivisesti +- `npm run test-ui`: Ajaa testit selainnäkymässä +- `npm test`: Ajaa testit headless-tilassa näyttäen vain tulosteen komentorivillä diff --git a/e2e/docker-compose.yml b/e2e/docker-compose.yml index e6d97042..51250f8f 100644 --- a/e2e/docker-compose.yml +++ b/e2e/docker-compose.yml @@ -16,6 +16,11 @@ services: build: context: ../server dockerfile: Dockerfile.develop + healthcheck: + interval: 15s + timeout: 30s + retries: 15 + test: curl --fail http://localhost:3000/api/health || exit 1 volumes: # Include .git for detecting changes in the workspace (e.g. only run tests on changed files) - ../.git:/app/.git:cached @@ -35,6 +40,9 @@ services: DEFAULT_CTYPE: 'fi_FI.UTF8' TZ: Europe/Helsinki volumes: - - .db-data:/var/lib/postgresql:delegated + - db-data:/var/lib/postgresql:delegated ports: - '127.0.0.1:5432:5432' + +volumes: + db-data: diff --git a/e2e/pages/surveyEditPage.ts b/e2e/pages/surveyEditPage.ts index 02b54d23..126b4b53 100644 --- a/e2e/pages/surveyEditPage.ts +++ b/e2e/pages/surveyEditPage.ts @@ -43,7 +43,9 @@ export class SurveyEditPage { } else { await this._page.goto('http://localhost:8080/admin'); await this._page.getByRole('button', { name: 'Uusi kysely' }).click(); - await this._page.waitForURL('http://localhost:8080/admin/kyselyt/**'); + await this._page.waitForURL( + 'http://localhost:8080/admin/kyselyt/*/perustiedot', + ); const urlParts = this._page.url().split('/'); const lastElement = urlParts[urlParts.length - 1]; this._surveyId = lastElement; diff --git a/e2e/playwright.config.ts b/e2e/playwright.config.ts index 9161e7a0..75931f23 100644 --- a/e2e/playwright.config.ts +++ b/e2e/playwright.config.ts @@ -1,6 +1,7 @@ import { PlaywrightTestConfig, devices } from '@playwright/test'; const config: PlaywrightTestConfig = { + retries: 1, projects: [ { name: 'Chrome', diff --git a/server/Dockerfile.develop b/server/Dockerfile.develop index 49198dc5..0d4407e3 100644 --- a/server/Dockerfile.develop +++ b/server/Dockerfile.develop @@ -20,7 +20,8 @@ RUN apk update && apk add \ freetype \ harfbuzz \ ca-certificates \ - ttf-freefont + ttf-freefont \ + curl WORKDIR /app/server COPY package*.json ./