diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml new file mode 100644 index 0000000..8752aa7 --- /dev/null +++ b/.github/workflows/playwright.yml @@ -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 diff --git a/.gitignore b/.gitignore index cc7b141..f0960ba 100644 --- a/.gitignore +++ b/.gitignore @@ -40,3 +40,8 @@ testem.log # System files .DS_Store Thumbs.db +node_modules/ +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/angular.json b/angular.json index 2cbf973..58aa354 100644 --- a/angular.json +++ b/angular.json @@ -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" diff --git a/compose.yml b/compose.yml index 9a4847c..aee66ea 100644 --- a/compose.yml +++ b/compose.yml @@ -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 diff --git a/package-lock.json b/package-lock.json index 4bfaa2f..9d30a42 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index f1859dd..e5df74a 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..a02bfb4 --- /dev/null +++ b/playwright.config.ts @@ -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, + // }, +}); diff --git a/src/app/components/login-view/login-view.component.html b/src/app/components/login-view/login-view.component.html index c1234f8..961ccfd 100644 --- a/src/app/components/login-view/login-view.component.html +++ b/src/app/components/login-view/login-view.component.html @@ -3,6 +3,6 @@ Logout Icon
- +
diff --git a/src/app/environments/environment.ci.ts b/src/app/environments/environment.ci.ts new file mode 100644 index 0000000..1fc27ab --- /dev/null +++ b/src/app/environments/environment.ci.ts @@ -0,0 +1,4 @@ + +export class Environment { + public static readonly BASE_URL = "http://employee:8089"; +} diff --git a/src/app/environments/environment.ts b/src/app/environments/environment.ts new file mode 100644 index 0000000..e27af26 --- /dev/null +++ b/src/app/environments/environment.ts @@ -0,0 +1,4 @@ + +export class Environment { + public static readonly BASE_URL = "http://127.0.0.1:8089"; +} diff --git a/src/app/service/skill.service.ts b/src/app/service/skill.service.ts index b9ac3a6..9e7f833 100644 --- a/src/app/service/skill.service.ts +++ b/src/app/service/skill.service.ts @@ -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 { diff --git a/tests/login.spec.ts b/tests/login.spec.ts new file mode 100644 index 0000000..06a5865 --- /dev/null +++ b/tests/login.spec.ts @@ -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'); +}); + diff --git a/tests/mitarbeiter.spec.ts b/tests/mitarbeiter.spec.ts new file mode 100644 index 0000000..4977ba2 --- /dev/null +++ b/tests/mitarbeiter.spec.ts @@ -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); + }); +}); + diff --git a/tests/mitarbeiterErstellen.spec.ts b/tests/mitarbeiterErstellen.spec.ts new file mode 100644 index 0000000..9b0d7dd --- /dev/null +++ b/tests/mitarbeiterErstellen.spec.ts @@ -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); + }); +}); + diff --git a/tests/mitarbeiterbearbeiten.spec.ts b/tests/mitarbeiterbearbeiten.spec.ts new file mode 100644 index 0000000..aaa8e7d --- /dev/null +++ b/tests/mitarbeiterbearbeiten.spec.ts @@ -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'); + }); +}); + diff --git a/tests/qualifikationbearbeiten.spec.ts b/tests/qualifikationbearbeiten.spec.ts new file mode 100644 index 0000000..b9cecd9 --- /dev/null +++ b/tests/qualifikationbearbeiten.spec.ts @@ -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'); + }); +}); diff --git a/tests/qualifikationsverwaltung.spec.ts b/tests/qualifikationsverwaltung.spec.ts new file mode 100644 index 0000000..42cfd46 --- /dev/null +++ b/tests/qualifikationsverwaltung.spec.ts @@ -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); + }); +});