Merge pull request 'feat: add Playwright testing framework configuration and tests' (#71) from feature/add-playwright into main
All checks were successful
Playwright Tests / test (push) Successful in 2m7s

Reviewed-on: #71
This commit is contained in:
Jan Gleytenhoover 2025-01-17 13:34:51 +00:00
commit 11da909e3f
17 changed files with 503 additions and 10 deletions

71
.github/workflows/playwright.yml vendored Normal file
View file

@ -0,0 +1,71 @@
name: Playwright Tests
on:
push:
branches: [ main, master ]
pull_request:
branches: [ main, master ]
jobs:
test:
timeout-minutes: 60
runs-on: ubuntu-latest
services:
postgres-employee:
image: postgres:13.3
env:
POSTGRES_DB: employee_db
POSTGRES_USER: employee
POSTGRES_PASSWORD: secret
employee:
image: berndheidemann/employee-management-service:1.1.3
# image: berndheidemann/employee-management-service_without_keycloak:1.1
env:
spring.datasource.url: jdbc:postgresql://postgres-employee:5432/employee_db
spring.datasource.username: employee
spring.datasource.password: secret
steps:
# Checkout the repository
- uses: actions/checkout@v4
# Set up Node.js
- uses: actions/setup-node@v4
with:
node-version: lts/*
# Install project dependencies
- name: Install dependencies
run: npm ci
# Build the Angular project
- name: Build Angular Project
run: |
npm install -g @angular/cli
ng build --configuration=pipeline
# Start Angular development server
- name: Start Angular Development Server in Background
run: ng serve --configuration=pipeline > angular.log 2>&1 &
env:
PORT: 4200 # Ensure the server runs on a predictable port
# Install Playwright and dependencies
- name: Install Playwright Browsers
run: npx playwright install --with-deps
# Run Playwright tests
- name: Run Playwright Tests
run: npx playwright test
env:
CI: true # Ensures Playwright runs in CI mode
# Upload Playwright report
- uses: actions/upload-artifact@v3
if: ${{ !cancelled() }}
with:
name: playwright-report
path: playwright-report/
retention-days: 30

5
.gitignore vendored
View file

@ -40,3 +40,8 @@ testem.log
# System files
.DS_Store
Thumbs.db
node_modules/
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/

View file

@ -52,6 +52,14 @@
"optimization": false,
"extractLicenses": false,
"sourceMap": true
},
"pipeline": {
"fileReplacements": [
{
"replace": "src/app/environments/environment.ts",
"with": "src/app/environments/environment.ci.ts"
}
]
}
},
"defaultConfiguration": "production"
@ -64,6 +72,9 @@
},
"development": {
"buildTarget": "employeeService:build:development"
},
"pipeline": {
"buildTarget": "employeeService:build:pipeline"
}
},
"defaultConfiguration": "development"

View file

@ -6,14 +6,10 @@ services:
postgres-employee:
container_name: postgres_employee
image: postgres:13.3
volumes:
- employee_postgres_data:/var/lib/postgresql/data
environment:
POSTGRES_DB: employee_db
POSTGRES_USER: employee
POSTGRES_PASSWORD: secret
ports:
- "5432:5432"
employee:
container_name: employee

68
package-lock.json generated
View file

@ -26,7 +26,9 @@
"@angular-devkit/build-angular": "^18.2.0",
"@angular/cli": "^18.2.0",
"@angular/compiler-cli": "^18.2.0",
"@playwright/test": "^1.49.1",
"@types/jasmine": "~5.1.0",
"@types/node": "^22.10.7",
"jasmine-core": "~5.2.0",
"karma": "~6.4.0",
"karma-chrome-launcher": "~3.2.0",
@ -3896,6 +3898,21 @@
"node": ">=14"
}
},
"node_modules/@playwright/test": {
"version": "1.49.1",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.49.1.tgz",
"integrity": "sha512-Ky+BVzPz8pL6PQxHqNRW1k3mIyv933LML7HktS8uik0bUXNCdPhoS/kLihiO1tMf/egaJb4IutXd7UywvXEW+g==",
"dev": true,
"dependencies": {
"playwright": "1.49.1"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@popperjs/core": {
"version": "2.11.8",
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
@ -4452,11 +4469,10 @@
}
},
"node_modules/@types/node": {
"version": "22.10.5",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.5.tgz",
"integrity": "sha512-F8Q+SeGimwOo86fiovQh8qiXfFEh2/ocYv7tU5pJ3EXMSSxk1Joj5wefpFK2fHTf/N6HKGSxIDBT9f3gCxXPkQ==",
"version": "22.10.7",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.7.tgz",
"integrity": "sha512-V09KvXxFiutGp6B7XkpaDXlNadZxrzajcY50EuoLIpQ6WWYCSvf19lVIazzfIzQvhUN2HjX12spLojTnhuKlGg==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.20.0"
}
@ -10819,6 +10835,50 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/playwright": {
"version": "1.49.1",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.49.1.tgz",
"integrity": "sha512-VYL8zLoNTBxVOrJBbDuRgDWa3i+mfQgDTrL8Ah9QXZ7ax4Dsj0MSq5bYgytRnDVVe+njoKnfsYkH3HzqVj5UZA==",
"dev": true,
"dependencies": {
"playwright-core": "1.49.1"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"fsevents": "2.3.2"
}
},
"node_modules/playwright-core": {
"version": "1.49.1",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.49.1.tgz",
"integrity": "sha512-BzmpVcs4kE2CH15rWfzpjzVGhWERJfmnXmniSyKeRZUs9Ws65m+RGIi7mjJK/euCegfn3i7jvqWeWyHe9y3Vgg==",
"dev": true,
"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,
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/postcss": {
"version": "8.4.41",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.41.tgz",

View file

@ -28,7 +28,9 @@
"@angular-devkit/build-angular": "^18.2.0",
"@angular/cli": "^18.2.0",
"@angular/compiler-cli": "^18.2.0",
"@playwright/test": "^1.49.1",
"@types/jasmine": "~5.1.0",
"@types/node": "^22.10.7",
"jasmine-core": "~5.2.0",
"karma": "~6.4.0",
"karma-chrome-launcher": "~3.2.0",

69
playwright.config.ts Normal file
View file

@ -0,0 +1,69 @@
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 ? 8 : 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'] },
},
/* 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,
// },
});

View file

@ -3,6 +3,6 @@
<img src="img.png" alt="Logout Icon" class="logo-image" />
<form>
<button (click)="login()" type="submit">Mit KeyCLoak anmelden</button>
<button (click)="login()" type="submit">Login with Keycloak</button>
</form>
</div>

View file

@ -0,0 +1,4 @@
export class Environment {
public static readonly BASE_URL = "http://employee:8089";
}

View file

@ -0,0 +1,4 @@
export class Environment {
public static readonly BASE_URL = "http://127.0.0.1:8089";
}

View file

@ -4,6 +4,7 @@ import { EmployeesForAQualificationDTO, QualificationGetDTO, QualificationPostDT
import { Observable } from "rxjs";
import { EmployeeNameDataDTO } from "../models/mitarbeiter";
import { EmployeeService } from "./employee.service";
import { Environment } from "../environments/environment";
@Injectable({
providedIn: 'root'
@ -19,7 +20,7 @@ export class SkillService {
this.http.delete(`${SkillService.BASE_URL}/qualifications/${id}`).subscribe();
}
public static readonly BASE_URL = "http://localhost:8089";
public static readonly BASE_URL = Environment.BASE_URL;
getToPutDto(skill: QualificationGetDTO): QualificationPostDTO {
return {

46
tests/login.spec.ts Normal file
View file

@ -0,0 +1,46 @@
import { test, expect } from '@playwright/test';
import { async } from 'rxjs';
test('LoginPageShouldRender', async ({ page }) => {
await page.goto('http://localhost:4200');
// Expect a title "to contain" a substring.
await expect(page).toHaveTitle(/EmployeeService/);
});
test('LoginPageShouldHaveCorrectHeading', async ({ page }) => {
await page.goto('http://localhost:4200');
const heading = page.getByRole('heading');
await expect(heading).toHaveText('Hi-Tec GmbH');
});
test('LoginPageShouldHaveLoginButton', async ({ page }) => {
await page.goto('http://localhost:4200');
const button = page.getByRole('button');
await expect(button).toHaveText('Login with Keycloak');
});
test('LoginPageButtonShouldRedirectToKeycloak', async ({ page }) => {
await page.goto('http://localhost:4200');
const button = page.getByText("Login with Keycloak");
await button.click();
await page.waitForFunction(() => window.location.href.includes('keycloak'));
expect(page.url()).toContain("keycloak.szut.dev");
});
test('AfterLoginUserShouldBeRedirectedToEmployees', async ({ page }) => {
await page.goto('http://localhost:4200');
await page.getByRole('button').click();
await page.waitForFunction(() => window.location.href.includes('keycloak'));
await page.getByLabel('Username or email').fill('user');
await page.getByLabel('Password').fill('test');
await page.click('#kc-login');
expect(page.url()).toContain('localhost');
expect(page.url()).toContain('mitarbeiter');
});

58
tests/mitarbeiter.spec.ts Normal file
View file

@ -0,0 +1,58 @@
import { test, expect } from "@playwright/test";
test.describe('mitarbeiter', () => {
test.beforeEach(async ({ page }) => {
await page.goto('http://localhost:4200');
await page.getByRole('button').click();
await page.waitForFunction(() => window.location.href.includes('keycloak'));
await page.getByLabel('Username or email').fill('user');
await page.getByLabel('Password').fill('test');
await page.click('#kc-login');
await page.goto('http://localhost:4200/mitarbeiter');
});
test('ShouldLoad', async ({ page }) => {
await expect(page.getByRole('heading')).toHaveText("Employees");
});
test('ShouldLoadEmployees', async ({ page }) => {
expect(page.getByText('Max')).toBeTruthy();
});
test('AddEmployeeShouldRedirect', async ({ page }) => {
await page.getByText('Add employee').click();
expect(page.url()).toContain('mitarbeitererstellen');
});
test('EditShouldRedirectToCorrespondingPage', async ({ page }) => {
const button = page.getByText('Edit').first();
await button.click();
expect(page.url()).toContain('mitarbeiterbearbeiten');
expect(page.url()).toContain('1');
});
test('DeleteShouldBeThere', async ({ page }) => {
const button = page.getByText('Delete').first();
const users = page.getByText('Delete');
await users.first().waitFor({ state: "visible" });
expect(await users.count()).toBeGreaterThan(1);
expect(button).toBeTruthy();
});
test('SearchShouldWork', async ({ page }) => {
const searchField = page.getByRole('textbox');
const searchButton = page.getByText('Search').first();
await searchField.fill('Max');
await searchButton.click();
const hiddenItem = page.getByText('MusterFrau');
await expect(hiddenItem).toHaveCount(0);
});
});

View file

@ -0,0 +1,61 @@
import { test, expect } from "@playwright/test";
test.describe('mitarbeiter', () => {
test.beforeEach(async ({ page }) => {
await page.goto('http://localhost:4200');
await page.getByRole('button').click();
await page.waitForFunction(() => window.location.href.includes('keycloak'));
await page.getByLabel('Username or email').fill('user');
await page.getByLabel('Password').fill('test');
await page.click('#kc-login');
await page.goto('http://localhost:4200/mitarbeitererstellen');
});
test('pageShouldLoad', async ({ page }) => {
await expect(page.getByText('Save')).toHaveCount(1);
});
test('backButtonShouldGoBack', async ({ page }) => {
await page.getByText('Back').click();
expect(page.url().includes('erstellen')).toBeFalsy();
});
test('EveryFieldShouldValidateEmptiness', async ({ page }) => {
await page.getByText('Save').click();
const errors = page.getByText('This field is required');
await expect(errors).toHaveCount(6);
});
test('PhoneNumberShouldBeValidated', async ({ page }) => {
await page.getByLabel('Phone').fill("asd");
await page.getByText('Save').click();
const error = page.getByText('This field must be a valid phone number');
await expect(error).toHaveCount(1);
});
test('PostCodeShouldValidateTooShort', async ({ page }) => {
await page.getByLabel('Postcode').fill("1");
await page.getByText('Save').click();
const error = page.getByText('The value is too short');
await expect(error).toHaveCount(1);
});
test('PostCodeShouldValidateTooLong', async ({ page }) => {
await page.getByLabel('Postcode').fill("123456");
await page.getByText('Save').click();
const error = page.getByText('The value is too long');
await expect(error).toHaveCount(1);
});
});

View file

@ -0,0 +1,24 @@
import { test, expect } from "@playwright/test";
test.describe('mitarbeiterbearbeiten', () => {
test.beforeEach(async ({ page }) => {
await page.goto('http://localhost:4200');
await page.getByRole('button').click();
await page.waitForFunction(() => window.location.href.includes('keycloak'));
await page.getByLabel('Username or email').fill('user');
await page.getByLabel('Password').fill('test');
await page.click('#kc-login');
await page.goto('http://localhost:4200/mitarbeiterbearbeiten/1');
});
test('ShouldLoad', async ({ page }) => {
expect(page.getByText("Save")).toBeTruthy();
});
test('FieldsShouldHaveValues', async ({page}) => {
await expect(page.getByLabel('First Name')).toHaveValue('Max');
});
});

View file

@ -0,0 +1,23 @@
import { test, expect } from "@playwright/test";
test.describe('qualifikationbearbeiten', () => {
test.beforeEach(async ({ page }) => {
await page.goto('http://localhost:4200');
await page.getByRole('button').click();
await page.waitForFunction(() => window.location.href.includes('keycloak'));
await page.getByLabel('Username or email').fill('user');
await page.getByLabel('Password').fill('test');
await page.click('#kc-login');
await page.goto('http://localhost:4200/qualifikationbearbeiten/1');
});
test('ShouldLoad', async ({ page }) => {
expect(page.getByText("Save")).toBeTruthy();
});
test('FieldsShouldHaveValues', async ({page}) => {
await expect(page.getByLabel('Name')).toHaveValue('Java');
});
});

View file

@ -0,0 +1,58 @@
import { test, expect } from "@playwright/test";
test.describe('qualifikationen', () => {
test.beforeEach(async ({ page }) => {
await page.goto('http://localhost:4200');
await page.getByRole('button').click();
await page.waitForFunction(() => window.location.href.includes('keycloak'));
await page.getByLabel('Username or email').fill('user');
await page.getByLabel('Password').fill('test');
await page.click('#kc-login');
await page.goto('http://localhost:4200/qualifikationsverwaltung');
});
test('ShouldLoad', async ({ page }) => {
await expect(page.getByRole('heading')).toHaveText("Qualifications");
});
test('ShouldLoadQualifications', async ({ page }) => {
expect(page.getByText('Java')).toBeTruthy();
});
// TODO
// test('AddQualificationShouldRedirect', async ({ page }) => {
// await page.getByText('Add qualification').click();
// expect(page.url()).toContain('mitarbeitererstellen');
// });
test('EditShouldRedirectToCorrespondingPage', async ({ page }) => {
const button = page.getByText('Edit').first();
await button.click();
expect(page.url()).toContain('qualifikationbearbeiten');
expect(page.url()).toContain('1');
});
test('DeleteShouldBeThere', async ({ page }) => {
const button = page.getByText('Delete').first();
const users = page.getByText('Delete');
await users.first().waitFor({ state: "visible" });
expect(await users.count()).toBe(2);
expect(button).toBeTruthy();
});
test('SearchShouldWork', async ({ page }) => {
const searchField = page.getByRole('textbox');
const searchButton = page.getByText('Search').first();
await searchField.fill('Java');
await searchButton.click();
const hiddenItem = page.getByText('Angular');
await expect(hiddenItem).toHaveCount(0);
});
});