diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml new file mode 100644 index 000000000..fc0182bcd --- /dev/null +++ b/.github/workflows/playwright.yml @@ -0,0 +1,30 @@ +name: Playwright Tests +on: + push: + branches: [develop] + pull_request: + branches: [develop] +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 + env: + TEST_USERNAME: ${{ secrets.TEST_USERNAME }} + TEST_PASSWORD: ${{ secrets.TEST_PASSWORD }} + run: npx playwright test + - uses: actions/upload-artifact@v4 + if: ${{ !cancelled() }} + with: + name: playwright-report + path: playwright-report/ + retention-days: 30 diff --git a/.gitignore b/.gitignore index da1e5896e..60269a615 100644 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,8 @@ npm-debug.log* yarn-debug.log* yarn-error.log* /.idea/ +node_modules/ +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/package-lock.json b/package-lock.json index 1b30bc015..48cb694c0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -101,6 +101,7 @@ "@cypress/webpack-dev-server": "^3.5.1", "@cypress/webpack-preprocessor": "^6.0.1", "@dtsgenerator/replace-namespace": "^1.6.0", + "@playwright/test": "^1.48.2", "@pmmmwh/react-refresh-webpack-plugin": "^0.5.11", "@storybook/addon-essentials": "^7.0.18", "@storybook/addon-interactions": "^7.0.18", @@ -4971,6 +4972,22 @@ "node": ">=14" } }, + "node_modules/@playwright/test": { + "version": "1.48.2", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.48.2.tgz", + "integrity": "sha512-54w1xCWfXuax7dz4W2M9uw0gDyh+ti/0K/MxcCUxChFh37kkdxPdfZDw5QBbuPUJHr1CiHJ1hXgSs+GgeQc5Zw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.48.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@pmmmwh/react-refresh-webpack-plugin": { "version": "0.5.11", "dev": true, @@ -24196,6 +24213,53 @@ "node": ">=4" } }, + "node_modules/playwright": { + "version": "1.48.2", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.48.2.tgz", + "integrity": "sha512-NjYvYgp4BPmiwfe31j4gHLa3J7bD2WiBz8Lk2RoSsmX38SVIARZ18VYjxLjAcDsAhA+F4iSEXTSGgjua0rrlgQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.48.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.48.2", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.48.2.tgz", + "integrity": "sha512-sjjw+qrLFlriJo64du+EK0kJgZzoQPsabGF4lBvsid+3CNIZIYLgnMj9V6JY5VhM2Peh20DJWIVpVljLLnlawA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/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/pngjs": { "version": "5.0.0", "license": "MIT", diff --git a/package.json b/package.json index 9dae06380..52cbd2061 100644 --- a/package.json +++ b/package.json @@ -102,6 +102,7 @@ "@cypress/webpack-dev-server": "^3.5.1", "@cypress/webpack-preprocessor": "^6.0.1", "@dtsgenerator/replace-namespace": "^1.6.0", + "@playwright/test": "^1.48.2", "@pmmmwh/react-refresh-webpack-plugin": "^0.5.11", "@storybook/addon-essentials": "^7.0.18", "@storybook/addon-interactions": "^7.0.18", diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 000000000..cedc520a7 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,80 @@ +import { defineConfig, devices } from '@playwright/test'; + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +import dotenv from 'dotenv'; +import path from 'path'; +dotenv.config({ path: path.resolve(__dirname, '.env') }); + +/** + * 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', + /* 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://127.0.0.1:3000', + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'on-first-retry' + }, + + /* 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'] }, + // }, + // { + // name: 'Mobile Safari', + // use: { ...devices['iPhone 12'] }, + // }, + + /* Test against branded browsers. */ + // { + // name: 'Microsoft Edge', + // use: { ...devices['Desktop Edge'], channel: 'msedge' }, + // }, + // { + // name: 'Google Chrome', + // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, + // }, + ] + + /* Run your local dev server before starting the tests */ + // webServer: { + // command: 'npm run start', + // url: 'http://127.0.0.1:3000', + // reuseExistingServer: !process.env.CI, + // }, +}); diff --git a/src/components/app/NavigationBar.tsx b/src/components/app/NavigationBar.tsx index 5d791c6dd..c337447d3 100644 --- a/src/components/app/NavigationBar.tsx +++ b/src/components/app/NavigationBar.tsx @@ -344,6 +344,7 @@ export const NavigationBar = ({ tabIndex={-1} ref={(el) => (ref_logout.current = el)} onKeyDown={(e) => handleKeyDownMenu(e, null)} + id="logout" > { const loginButton: ButtonItem = { label: translate('login.button.label'), - type: BUTTON_TYPES.PRIMARY + type: BUTTON_TYPES.PRIMARY, + id: 'login' }; const hasTenant = tenant != null; diff --git a/tests/config.ts b/tests/config.ts new file mode 100644 index 000000000..fd3e2b3c5 --- /dev/null +++ b/tests/config.ts @@ -0,0 +1,8 @@ +export const caritasRework = { + dev: 'https://dev.caritas-rework.dev.virtual-identity.net/', + stage: 'https://beratung-rework-staging.caritas.de/', + prod: 'https://beratung.caritas.de/' +}; + +// write util functions here in config or in another file? +// util functions being general checks in all registration pages and so on diff --git a/tests/login/login.spec.ts b/tests/login/login.spec.ts new file mode 100644 index 000000000..4927b696f --- /dev/null +++ b/tests/login/login.spec.ts @@ -0,0 +1,17 @@ +import { expect, test } from '@playwright/test'; +import { caritasRework } from '../config'; +import { ensureLanguage } from '../utils'; + +test('Login as an advice seeker', async ({ page }) => { + const username = process.env.TEST_USERNAME; + const password = process.env.TEST_PASSWORD; + await page.goto(`${caritasRework.dev}`); + ensureLanguage(page); + await page.fill('input[id="username"]', username!); + await page.fill('input[id="passwordInput"]', password!); + await page.click('button[id="login"]'); + await page.locator('.sessionsList__illustration__image').click(); + await expect(page.locator('a[href="/profile"]')).toBeVisible(); + await expect(page.locator('div[id="local-switch-wrapper"]')).toBeVisible(); + await page.click('div[id="logout"]'); +}); diff --git a/tests/registration/registration.spec.ts b/tests/registration/registration.spec.ts new file mode 100644 index 000000000..ea79b60e2 --- /dev/null +++ b/tests/registration/registration.spec.ts @@ -0,0 +1,49 @@ +import { test, expect } from '@playwright/test'; +import { caritasRework } from '../config'; +import { ensureLanguage, generateRandomAlphanumeric } from '../utils'; + +// checks if the page has the home titles +test('Check registration page elements', async ({ page }) => { + await page.goto(`${caritasRework.dev}registration`); + ensureLanguage(page); + await expect(page.locator('h1.headline--1')).toBeVisible(); + await expect(page.locator('h4.headline--4')).toBeVisible(); +}); + +// registration test is skipped until a delete user account feature is implemented +test.skip('Complete registration process', async ({ page }) => { + const password = process.env.TEST_PASSWORD; + await page.goto(`${caritasRework.dev}registration`); + ensureLanguage(page); + await page.click('a[data-cy="button-register"]'); + + // registration form + await page.click('div[id="panel-Children, teenagers, adults and family"]'); + await page.click('label[data-cy="topic-selection-radio-1"]'); + await page.click('button[data-cy="button-next"]'); + await page.fill('input[data-cy="input-postal-code"]', '99999'); + await page.click('button[data-cy="button-next"]'); + await page + .locator('input[name="agency-selection-radio-group"]') + .first() + .click(); + await page.click('button[data-cy="button-next"]'); + + // username & password + const randomUsername = `testuser_${generateRandomAlphanumeric(3)}`; + await page.getByLabel(/(user\s?name|benutzername)/i).fill(randomUsername); + await page + .getByLabel(/pass\s?(word|wort)/i, { exact: true }) + .first() + .fill(password!); + await page + .getByLabel(/(passwort\s?wiederholen|repeat\s?password)/i) + .fill(password!); + await page + .getByLabel(/Ich habe die Datenschutzerklä|I have the Privacy policy/) + .check(); + await page.click('button[data-cy="button-register"]'); + await page + .getByRole('button', { name: /Nachricht verfassen|Compose message/ }) + .click(); +}); diff --git a/tests/registration/registrationAgencyLink.spec.ts b/tests/registration/registrationAgencyLink.spec.ts new file mode 100644 index 000000000..8f3fc8385 --- /dev/null +++ b/tests/registration/registrationAgencyLink.spec.ts @@ -0,0 +1,5 @@ +import { test } from '@playwright/test'; + +test.skip('registration via agency link', async ({ page }) => { + // +}); diff --git a/tests/registration/registrationConsultantLink.spec.ts b/tests/registration/registrationConsultantLink.spec.ts new file mode 100644 index 000000000..0577d2c83 --- /dev/null +++ b/tests/registration/registrationConsultantLink.spec.ts @@ -0,0 +1,5 @@ +import { test } from '@playwright/test'; + +test.skip('registration via consultant link', async ({ page }) => { + // +}); diff --git a/tests/utils.ts b/tests/utils.ts new file mode 100644 index 000000000..75235b1fa --- /dev/null +++ b/tests/utils.ts @@ -0,0 +1,25 @@ +import { expect, Page } from '@playwright/test'; + +export function generateRandomAlphanumeric(length: number): string { + const chars = + 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + let result = ''; + for (let i = 0; i < length; i++) { + const randomIndex = Math.floor(Math.random() * chars.length); + result += chars[randomIndex]; + } + return result; +} + +export async function ensureLanguage(page: Page) { + let pageLang = (await page.getAttribute('html', 'lang')) || ''; + + if (!['en', 'de'].includes(pageLang)) { + await page.evaluate(() => { + document.documentElement.lang = 'en'; + }); + pageLang = 'en'; + } + + expect(['en', 'de']).toContain(pageLang); +}