From b30a40fe0718154fc1d92665fb82c2056ceade43 Mon Sep 17 00:00:00 2001 From: Jan Klattenhoff Date: Thu, 16 Jan 2025 16:46:30 +0100 Subject: [PATCH 01/48] feat: add Playwright testing framework configuration and tests --- .github/workflows/playwright.yml | 27 ++ .gitignore | 5 + package-lock.json | 68 ++++- package.json | 2 + playwright.config.ts | 79 +++++ tests-examples/demo-todo-app.spec.ts | 437 +++++++++++++++++++++++++++ tests/example.spec.ts | 18 ++ 7 files changed, 632 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/playwright.yml create mode 100644 playwright.config.ts create mode 100644 tests-examples/demo-todo-app.spec.ts create mode 100644 tests/example.spec.ts diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml new file mode 100644 index 0000000..3eb1314 --- /dev/null +++ b/.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/.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/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..a05d8b5 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,79 @@ +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/tests-examples/demo-todo-app.spec.ts b/tests-examples/demo-todo-app.spec.ts new file mode 100644 index 0000000..8641cb5 --- /dev/null +++ b/tests-examples/demo-todo-app.spec.ts @@ -0,0 +1,437 @@ +import { test, expect, type Page } from '@playwright/test'; + +test.beforeEach(async ({ page }) => { + await page.goto('https://demo.playwright.dev/todomvc'); +}); + +const TODO_ITEMS = [ + 'buy some cheese', + 'feed the cat', + 'book a doctors appointment' +] as const; + +test.describe('New Todo', () => { + test('should allow me to add todo items', async ({ page }) => { + // create a new todo locator + const newTodo = page.getByPlaceholder('What needs to be done?'); + + // Create 1st todo. + await newTodo.fill(TODO_ITEMS[0]); + await newTodo.press('Enter'); + + // Make sure the list only has one todo item. + await expect(page.getByTestId('todo-title')).toHaveText([ + TODO_ITEMS[0] + ]); + + // Create 2nd todo. + await newTodo.fill(TODO_ITEMS[1]); + await newTodo.press('Enter'); + + // Make sure the list now has two todo items. + await expect(page.getByTestId('todo-title')).toHaveText([ + TODO_ITEMS[0], + TODO_ITEMS[1] + ]); + + await checkNumberOfTodosInLocalStorage(page, 2); + }); + + test('should clear text input field when an item is added', async ({ page }) => { + // create a new todo locator + const newTodo = page.getByPlaceholder('What needs to be done?'); + + // Create one todo item. + await newTodo.fill(TODO_ITEMS[0]); + await newTodo.press('Enter'); + + // Check that input is empty. + await expect(newTodo).toBeEmpty(); + await checkNumberOfTodosInLocalStorage(page, 1); + }); + + test('should append new items to the bottom of the list', async ({ page }) => { + // Create 3 items. + await createDefaultTodos(page); + + // create a todo count locator + const todoCount = page.getByTestId('todo-count') + + // Check test using different methods. + await expect(page.getByText('3 items left')).toBeVisible(); + await expect(todoCount).toHaveText('3 items left'); + await expect(todoCount).toContainText('3'); + await expect(todoCount).toHaveText(/3/); + + // Check all items in one call. + await expect(page.getByTestId('todo-title')).toHaveText(TODO_ITEMS); + await checkNumberOfTodosInLocalStorage(page, 3); + }); +}); + +test.describe('Mark all as completed', () => { + test.beforeEach(async ({ page }) => { + await createDefaultTodos(page); + await checkNumberOfTodosInLocalStorage(page, 3); + }); + + test.afterEach(async ({ page }) => { + await checkNumberOfTodosInLocalStorage(page, 3); + }); + + test('should allow me to mark all items as completed', async ({ page }) => { + // Complete all todos. + await page.getByLabel('Mark all as complete').check(); + + // Ensure all todos have 'completed' class. + await expect(page.getByTestId('todo-item')).toHaveClass(['completed', 'completed', 'completed']); + await checkNumberOfCompletedTodosInLocalStorage(page, 3); + }); + + test('should allow me to clear the complete state of all items', async ({ page }) => { + const toggleAll = page.getByLabel('Mark all as complete'); + // Check and then immediately uncheck. + await toggleAll.check(); + await toggleAll.uncheck(); + + // Should be no completed classes. + await expect(page.getByTestId('todo-item')).toHaveClass(['', '', '']); + }); + + test('complete all checkbox should update state when items are completed / cleared', async ({ page }) => { + const toggleAll = page.getByLabel('Mark all as complete'); + await toggleAll.check(); + await expect(toggleAll).toBeChecked(); + await checkNumberOfCompletedTodosInLocalStorage(page, 3); + + // Uncheck first todo. + const firstTodo = page.getByTestId('todo-item').nth(0); + await firstTodo.getByRole('checkbox').uncheck(); + + // Reuse toggleAll locator and make sure its not checked. + await expect(toggleAll).not.toBeChecked(); + + await firstTodo.getByRole('checkbox').check(); + await checkNumberOfCompletedTodosInLocalStorage(page, 3); + + // Assert the toggle all is checked again. + await expect(toggleAll).toBeChecked(); + }); +}); + +test.describe('Item', () => { + + test('should allow me to mark items as complete', async ({ page }) => { + // create a new todo locator + const newTodo = page.getByPlaceholder('What needs to be done?'); + + // Create two items. + for (const item of TODO_ITEMS.slice(0, 2)) { + await newTodo.fill(item); + await newTodo.press('Enter'); + } + + // Check first item. + const firstTodo = page.getByTestId('todo-item').nth(0); + await firstTodo.getByRole('checkbox').check(); + await expect(firstTodo).toHaveClass('completed'); + + // Check second item. + const secondTodo = page.getByTestId('todo-item').nth(1); + await expect(secondTodo).not.toHaveClass('completed'); + await secondTodo.getByRole('checkbox').check(); + + // Assert completed class. + await expect(firstTodo).toHaveClass('completed'); + await expect(secondTodo).toHaveClass('completed'); + }); + + test('should allow me to un-mark items as complete', async ({ page }) => { + // create a new todo locator + const newTodo = page.getByPlaceholder('What needs to be done?'); + + // Create two items. + for (const item of TODO_ITEMS.slice(0, 2)) { + await newTodo.fill(item); + await newTodo.press('Enter'); + } + + const firstTodo = page.getByTestId('todo-item').nth(0); + const secondTodo = page.getByTestId('todo-item').nth(1); + const firstTodoCheckbox = firstTodo.getByRole('checkbox'); + + await firstTodoCheckbox.check(); + await expect(firstTodo).toHaveClass('completed'); + await expect(secondTodo).not.toHaveClass('completed'); + await checkNumberOfCompletedTodosInLocalStorage(page, 1); + + await firstTodoCheckbox.uncheck(); + await expect(firstTodo).not.toHaveClass('completed'); + await expect(secondTodo).not.toHaveClass('completed'); + await checkNumberOfCompletedTodosInLocalStorage(page, 0); + }); + + test('should allow me to edit an item', async ({ page }) => { + await createDefaultTodos(page); + + const todoItems = page.getByTestId('todo-item'); + const secondTodo = todoItems.nth(1); + await secondTodo.dblclick(); + await expect(secondTodo.getByRole('textbox', { name: 'Edit' })).toHaveValue(TODO_ITEMS[1]); + await secondTodo.getByRole('textbox', { name: 'Edit' }).fill('buy some sausages'); + await secondTodo.getByRole('textbox', { name: 'Edit' }).press('Enter'); + + // Explicitly assert the new text value. + await expect(todoItems).toHaveText([ + TODO_ITEMS[0], + 'buy some sausages', + TODO_ITEMS[2] + ]); + await checkTodosInLocalStorage(page, 'buy some sausages'); + }); +}); + +test.describe('Editing', () => { + test.beforeEach(async ({ page }) => { + await createDefaultTodos(page); + await checkNumberOfTodosInLocalStorage(page, 3); + }); + + test('should hide other controls when editing', async ({ page }) => { + const todoItem = page.getByTestId('todo-item').nth(1); + await todoItem.dblclick(); + await expect(todoItem.getByRole('checkbox')).not.toBeVisible(); + await expect(todoItem.locator('label', { + hasText: TODO_ITEMS[1], + })).not.toBeVisible(); + await checkNumberOfTodosInLocalStorage(page, 3); + }); + + test('should save edits on blur', async ({ page }) => { + const todoItems = page.getByTestId('todo-item'); + await todoItems.nth(1).dblclick(); + await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill('buy some sausages'); + await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).dispatchEvent('blur'); + + await expect(todoItems).toHaveText([ + TODO_ITEMS[0], + 'buy some sausages', + TODO_ITEMS[2], + ]); + await checkTodosInLocalStorage(page, 'buy some sausages'); + }); + + test('should trim entered text', async ({ page }) => { + const todoItems = page.getByTestId('todo-item'); + await todoItems.nth(1).dblclick(); + await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill(' buy some sausages '); + await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Enter'); + + await expect(todoItems).toHaveText([ + TODO_ITEMS[0], + 'buy some sausages', + TODO_ITEMS[2], + ]); + await checkTodosInLocalStorage(page, 'buy some sausages'); + }); + + test('should remove the item if an empty text string was entered', async ({ page }) => { + const todoItems = page.getByTestId('todo-item'); + await todoItems.nth(1).dblclick(); + await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill(''); + await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Enter'); + + await expect(todoItems).toHaveText([ + TODO_ITEMS[0], + TODO_ITEMS[2], + ]); + }); + + test('should cancel edits on escape', async ({ page }) => { + const todoItems = page.getByTestId('todo-item'); + await todoItems.nth(1).dblclick(); + await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill('buy some sausages'); + await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Escape'); + await expect(todoItems).toHaveText(TODO_ITEMS); + }); +}); + +test.describe('Counter', () => { + test('should display the current number of todo items', async ({ page }) => { + // create a new todo locator + const newTodo = page.getByPlaceholder('What needs to be done?'); + + // create a todo count locator + const todoCount = page.getByTestId('todo-count') + + await newTodo.fill(TODO_ITEMS[0]); + await newTodo.press('Enter'); + + await expect(todoCount).toContainText('1'); + + await newTodo.fill(TODO_ITEMS[1]); + await newTodo.press('Enter'); + await expect(todoCount).toContainText('2'); + + await checkNumberOfTodosInLocalStorage(page, 2); + }); +}); + +test.describe('Clear completed button', () => { + test.beforeEach(async ({ page }) => { + await createDefaultTodos(page); + }); + + test('should display the correct text', async ({ page }) => { + await page.locator('.todo-list li .toggle').first().check(); + await expect(page.getByRole('button', { name: 'Clear completed' })).toBeVisible(); + }); + + test('should remove completed items when clicked', async ({ page }) => { + const todoItems = page.getByTestId('todo-item'); + await todoItems.nth(1).getByRole('checkbox').check(); + await page.getByRole('button', { name: 'Clear completed' }).click(); + await expect(todoItems).toHaveCount(2); + await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]); + }); + + test('should be hidden when there are no items that are completed', async ({ page }) => { + await page.locator('.todo-list li .toggle').first().check(); + await page.getByRole('button', { name: 'Clear completed' }).click(); + await expect(page.getByRole('button', { name: 'Clear completed' })).toBeHidden(); + }); +}); + +test.describe('Persistence', () => { + test('should persist its data', async ({ page }) => { + // create a new todo locator + const newTodo = page.getByPlaceholder('What needs to be done?'); + + for (const item of TODO_ITEMS.slice(0, 2)) { + await newTodo.fill(item); + await newTodo.press('Enter'); + } + + const todoItems = page.getByTestId('todo-item'); + const firstTodoCheck = todoItems.nth(0).getByRole('checkbox'); + await firstTodoCheck.check(); + await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[1]]); + await expect(firstTodoCheck).toBeChecked(); + await expect(todoItems).toHaveClass(['completed', '']); + + // Ensure there is 1 completed item. + await checkNumberOfCompletedTodosInLocalStorage(page, 1); + + // Now reload. + await page.reload(); + await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[1]]); + await expect(firstTodoCheck).toBeChecked(); + await expect(todoItems).toHaveClass(['completed', '']); + }); +}); + +test.describe('Routing', () => { + test.beforeEach(async ({ page }) => { + await createDefaultTodos(page); + // make sure the app had a chance to save updated todos in storage + // before navigating to a new view, otherwise the items can get lost :( + // in some frameworks like Durandal + await checkTodosInLocalStorage(page, TODO_ITEMS[0]); + }); + + test('should allow me to display active items', async ({ page }) => { + const todoItem = page.getByTestId('todo-item'); + await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check(); + + await checkNumberOfCompletedTodosInLocalStorage(page, 1); + await page.getByRole('link', { name: 'Active' }).click(); + await expect(todoItem).toHaveCount(2); + await expect(todoItem).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]); + }); + + test('should respect the back button', async ({ page }) => { + const todoItem = page.getByTestId('todo-item'); + await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check(); + + await checkNumberOfCompletedTodosInLocalStorage(page, 1); + + await test.step('Showing all items', async () => { + await page.getByRole('link', { name: 'All' }).click(); + await expect(todoItem).toHaveCount(3); + }); + + await test.step('Showing active items', async () => { + await page.getByRole('link', { name: 'Active' }).click(); + }); + + await test.step('Showing completed items', async () => { + await page.getByRole('link', { name: 'Completed' }).click(); + }); + + await expect(todoItem).toHaveCount(1); + await page.goBack(); + await expect(todoItem).toHaveCount(2); + await page.goBack(); + await expect(todoItem).toHaveCount(3); + }); + + test('should allow me to display completed items', async ({ page }) => { + await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check(); + await checkNumberOfCompletedTodosInLocalStorage(page, 1); + await page.getByRole('link', { name: 'Completed' }).click(); + await expect(page.getByTestId('todo-item')).toHaveCount(1); + }); + + test('should allow me to display all items', async ({ page }) => { + await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check(); + await checkNumberOfCompletedTodosInLocalStorage(page, 1); + await page.getByRole('link', { name: 'Active' }).click(); + await page.getByRole('link', { name: 'Completed' }).click(); + await page.getByRole('link', { name: 'All' }).click(); + await expect(page.getByTestId('todo-item')).toHaveCount(3); + }); + + test('should highlight the currently applied filter', async ({ page }) => { + await expect(page.getByRole('link', { name: 'All' })).toHaveClass('selected'); + + //create locators for active and completed links + const activeLink = page.getByRole('link', { name: 'Active' }); + const completedLink = page.getByRole('link', { name: 'Completed' }); + await activeLink.click(); + + // Page change - active items. + await expect(activeLink).toHaveClass('selected'); + await completedLink.click(); + + // Page change - completed items. + await expect(completedLink).toHaveClass('selected'); + }); +}); + +async function createDefaultTodos(page: Page) { + // create a new todo locator + const newTodo = page.getByPlaceholder('What needs to be done?'); + + for (const item of TODO_ITEMS) { + await newTodo.fill(item); + await newTodo.press('Enter'); + } +} + +async function checkNumberOfTodosInLocalStorage(page: Page, expected: number) { + return await page.waitForFunction(e => { + return JSON.parse(localStorage['react-todos']).length === e; + }, expected); +} + +async function checkNumberOfCompletedTodosInLocalStorage(page: Page, expected: number) { + return await page.waitForFunction(e => { + return JSON.parse(localStorage['react-todos']).filter((todo: any) => todo.completed).length === e; + }, expected); +} + +async function checkTodosInLocalStorage(page: Page, title: string) { + return await page.waitForFunction(t => { + return JSON.parse(localStorage['react-todos']).map((todo: any) => todo.title).includes(t); + }, title); +} diff --git a/tests/example.spec.ts b/tests/example.spec.ts new file mode 100644 index 0000000..54a906a --- /dev/null +++ b/tests/example.spec.ts @@ -0,0 +1,18 @@ +import { test, expect } from '@playwright/test'; + +test('has title', async ({ page }) => { + await page.goto('https://playwright.dev/'); + + // Expect a title "to contain" a substring. + await expect(page).toHaveTitle(/Playwright/); +}); + +test('get started link', async ({ page }) => { + await page.goto('https://playwright.dev/'); + + // Click the get started link. + await page.getByRole('link', { name: 'Get started' }).click(); + + // Expects page to have a heading with the name of Installation. + await expect(page.getByRole('heading', { name: 'Installation' })).toBeVisible(); +}); -- 2.45.3 From 6c1ec524ecc25d6179406bdbc0402e41bc6b1bad Mon Sep 17 00:00:00 2001 From: Jan Klattenhoff Date: Thu, 16 Jan 2025 17:19:38 +0100 Subject: [PATCH 02/48] chore: update Playwright tests and remove examples --- .github/workflows/playwright.yml | 2 +- playwright.config.ts | 10 - .../login-view/login-view.component.html | 2 +- tests-examples/demo-todo-app.spec.ts | 437 ------------------ tests/example.spec.ts | 18 - tests/login.spec.ts | 42 ++ 6 files changed, 44 insertions(+), 467 deletions(-) delete mode 100644 tests-examples/demo-todo-app.spec.ts delete mode 100644 tests/example.spec.ts create mode 100644 tests/login.spec.ts diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index 3eb1314..cd0c6cd 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -19,7 +19,7 @@ jobs: run: npx playwright install --with-deps - name: Run Playwright tests run: npx playwright test - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v3 if: ${{ !cancelled() }} with: name: playwright-report diff --git a/playwright.config.ts b/playwright.config.ts index a05d8b5..2b34436 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -39,16 +39,6 @@ export default defineConfig({ use: { ...devices['Desktop Chrome'] }, }, - { - name: 'firefox', - use: { ...devices['Desktop Firefox'] }, - }, - - { - name: 'webkit', - use: { ...devices['Desktop Safari'] }, - }, - /* Test against mobile viewports. */ // { // name: 'Mobile Chrome', 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/tests-examples/demo-todo-app.spec.ts b/tests-examples/demo-todo-app.spec.ts deleted file mode 100644 index 8641cb5..0000000 --- a/tests-examples/demo-todo-app.spec.ts +++ /dev/null @@ -1,437 +0,0 @@ -import { test, expect, type Page } from '@playwright/test'; - -test.beforeEach(async ({ page }) => { - await page.goto('https://demo.playwright.dev/todomvc'); -}); - -const TODO_ITEMS = [ - 'buy some cheese', - 'feed the cat', - 'book a doctors appointment' -] as const; - -test.describe('New Todo', () => { - test('should allow me to add todo items', async ({ page }) => { - // create a new todo locator - const newTodo = page.getByPlaceholder('What needs to be done?'); - - // Create 1st todo. - await newTodo.fill(TODO_ITEMS[0]); - await newTodo.press('Enter'); - - // Make sure the list only has one todo item. - await expect(page.getByTestId('todo-title')).toHaveText([ - TODO_ITEMS[0] - ]); - - // Create 2nd todo. - await newTodo.fill(TODO_ITEMS[1]); - await newTodo.press('Enter'); - - // Make sure the list now has two todo items. - await expect(page.getByTestId('todo-title')).toHaveText([ - TODO_ITEMS[0], - TODO_ITEMS[1] - ]); - - await checkNumberOfTodosInLocalStorage(page, 2); - }); - - test('should clear text input field when an item is added', async ({ page }) => { - // create a new todo locator - const newTodo = page.getByPlaceholder('What needs to be done?'); - - // Create one todo item. - await newTodo.fill(TODO_ITEMS[0]); - await newTodo.press('Enter'); - - // Check that input is empty. - await expect(newTodo).toBeEmpty(); - await checkNumberOfTodosInLocalStorage(page, 1); - }); - - test('should append new items to the bottom of the list', async ({ page }) => { - // Create 3 items. - await createDefaultTodos(page); - - // create a todo count locator - const todoCount = page.getByTestId('todo-count') - - // Check test using different methods. - await expect(page.getByText('3 items left')).toBeVisible(); - await expect(todoCount).toHaveText('3 items left'); - await expect(todoCount).toContainText('3'); - await expect(todoCount).toHaveText(/3/); - - // Check all items in one call. - await expect(page.getByTestId('todo-title')).toHaveText(TODO_ITEMS); - await checkNumberOfTodosInLocalStorage(page, 3); - }); -}); - -test.describe('Mark all as completed', () => { - test.beforeEach(async ({ page }) => { - await createDefaultTodos(page); - await checkNumberOfTodosInLocalStorage(page, 3); - }); - - test.afterEach(async ({ page }) => { - await checkNumberOfTodosInLocalStorage(page, 3); - }); - - test('should allow me to mark all items as completed', async ({ page }) => { - // Complete all todos. - await page.getByLabel('Mark all as complete').check(); - - // Ensure all todos have 'completed' class. - await expect(page.getByTestId('todo-item')).toHaveClass(['completed', 'completed', 'completed']); - await checkNumberOfCompletedTodosInLocalStorage(page, 3); - }); - - test('should allow me to clear the complete state of all items', async ({ page }) => { - const toggleAll = page.getByLabel('Mark all as complete'); - // Check and then immediately uncheck. - await toggleAll.check(); - await toggleAll.uncheck(); - - // Should be no completed classes. - await expect(page.getByTestId('todo-item')).toHaveClass(['', '', '']); - }); - - test('complete all checkbox should update state when items are completed / cleared', async ({ page }) => { - const toggleAll = page.getByLabel('Mark all as complete'); - await toggleAll.check(); - await expect(toggleAll).toBeChecked(); - await checkNumberOfCompletedTodosInLocalStorage(page, 3); - - // Uncheck first todo. - const firstTodo = page.getByTestId('todo-item').nth(0); - await firstTodo.getByRole('checkbox').uncheck(); - - // Reuse toggleAll locator and make sure its not checked. - await expect(toggleAll).not.toBeChecked(); - - await firstTodo.getByRole('checkbox').check(); - await checkNumberOfCompletedTodosInLocalStorage(page, 3); - - // Assert the toggle all is checked again. - await expect(toggleAll).toBeChecked(); - }); -}); - -test.describe('Item', () => { - - test('should allow me to mark items as complete', async ({ page }) => { - // create a new todo locator - const newTodo = page.getByPlaceholder('What needs to be done?'); - - // Create two items. - for (const item of TODO_ITEMS.slice(0, 2)) { - await newTodo.fill(item); - await newTodo.press('Enter'); - } - - // Check first item. - const firstTodo = page.getByTestId('todo-item').nth(0); - await firstTodo.getByRole('checkbox').check(); - await expect(firstTodo).toHaveClass('completed'); - - // Check second item. - const secondTodo = page.getByTestId('todo-item').nth(1); - await expect(secondTodo).not.toHaveClass('completed'); - await secondTodo.getByRole('checkbox').check(); - - // Assert completed class. - await expect(firstTodo).toHaveClass('completed'); - await expect(secondTodo).toHaveClass('completed'); - }); - - test('should allow me to un-mark items as complete', async ({ page }) => { - // create a new todo locator - const newTodo = page.getByPlaceholder('What needs to be done?'); - - // Create two items. - for (const item of TODO_ITEMS.slice(0, 2)) { - await newTodo.fill(item); - await newTodo.press('Enter'); - } - - const firstTodo = page.getByTestId('todo-item').nth(0); - const secondTodo = page.getByTestId('todo-item').nth(1); - const firstTodoCheckbox = firstTodo.getByRole('checkbox'); - - await firstTodoCheckbox.check(); - await expect(firstTodo).toHaveClass('completed'); - await expect(secondTodo).not.toHaveClass('completed'); - await checkNumberOfCompletedTodosInLocalStorage(page, 1); - - await firstTodoCheckbox.uncheck(); - await expect(firstTodo).not.toHaveClass('completed'); - await expect(secondTodo).not.toHaveClass('completed'); - await checkNumberOfCompletedTodosInLocalStorage(page, 0); - }); - - test('should allow me to edit an item', async ({ page }) => { - await createDefaultTodos(page); - - const todoItems = page.getByTestId('todo-item'); - const secondTodo = todoItems.nth(1); - await secondTodo.dblclick(); - await expect(secondTodo.getByRole('textbox', { name: 'Edit' })).toHaveValue(TODO_ITEMS[1]); - await secondTodo.getByRole('textbox', { name: 'Edit' }).fill('buy some sausages'); - await secondTodo.getByRole('textbox', { name: 'Edit' }).press('Enter'); - - // Explicitly assert the new text value. - await expect(todoItems).toHaveText([ - TODO_ITEMS[0], - 'buy some sausages', - TODO_ITEMS[2] - ]); - await checkTodosInLocalStorage(page, 'buy some sausages'); - }); -}); - -test.describe('Editing', () => { - test.beforeEach(async ({ page }) => { - await createDefaultTodos(page); - await checkNumberOfTodosInLocalStorage(page, 3); - }); - - test('should hide other controls when editing', async ({ page }) => { - const todoItem = page.getByTestId('todo-item').nth(1); - await todoItem.dblclick(); - await expect(todoItem.getByRole('checkbox')).not.toBeVisible(); - await expect(todoItem.locator('label', { - hasText: TODO_ITEMS[1], - })).not.toBeVisible(); - await checkNumberOfTodosInLocalStorage(page, 3); - }); - - test('should save edits on blur', async ({ page }) => { - const todoItems = page.getByTestId('todo-item'); - await todoItems.nth(1).dblclick(); - await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill('buy some sausages'); - await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).dispatchEvent('blur'); - - await expect(todoItems).toHaveText([ - TODO_ITEMS[0], - 'buy some sausages', - TODO_ITEMS[2], - ]); - await checkTodosInLocalStorage(page, 'buy some sausages'); - }); - - test('should trim entered text', async ({ page }) => { - const todoItems = page.getByTestId('todo-item'); - await todoItems.nth(1).dblclick(); - await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill(' buy some sausages '); - await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Enter'); - - await expect(todoItems).toHaveText([ - TODO_ITEMS[0], - 'buy some sausages', - TODO_ITEMS[2], - ]); - await checkTodosInLocalStorage(page, 'buy some sausages'); - }); - - test('should remove the item if an empty text string was entered', async ({ page }) => { - const todoItems = page.getByTestId('todo-item'); - await todoItems.nth(1).dblclick(); - await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill(''); - await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Enter'); - - await expect(todoItems).toHaveText([ - TODO_ITEMS[0], - TODO_ITEMS[2], - ]); - }); - - test('should cancel edits on escape', async ({ page }) => { - const todoItems = page.getByTestId('todo-item'); - await todoItems.nth(1).dblclick(); - await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill('buy some sausages'); - await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Escape'); - await expect(todoItems).toHaveText(TODO_ITEMS); - }); -}); - -test.describe('Counter', () => { - test('should display the current number of todo items', async ({ page }) => { - // create a new todo locator - const newTodo = page.getByPlaceholder('What needs to be done?'); - - // create a todo count locator - const todoCount = page.getByTestId('todo-count') - - await newTodo.fill(TODO_ITEMS[0]); - await newTodo.press('Enter'); - - await expect(todoCount).toContainText('1'); - - await newTodo.fill(TODO_ITEMS[1]); - await newTodo.press('Enter'); - await expect(todoCount).toContainText('2'); - - await checkNumberOfTodosInLocalStorage(page, 2); - }); -}); - -test.describe('Clear completed button', () => { - test.beforeEach(async ({ page }) => { - await createDefaultTodos(page); - }); - - test('should display the correct text', async ({ page }) => { - await page.locator('.todo-list li .toggle').first().check(); - await expect(page.getByRole('button', { name: 'Clear completed' })).toBeVisible(); - }); - - test('should remove completed items when clicked', async ({ page }) => { - const todoItems = page.getByTestId('todo-item'); - await todoItems.nth(1).getByRole('checkbox').check(); - await page.getByRole('button', { name: 'Clear completed' }).click(); - await expect(todoItems).toHaveCount(2); - await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]); - }); - - test('should be hidden when there are no items that are completed', async ({ page }) => { - await page.locator('.todo-list li .toggle').first().check(); - await page.getByRole('button', { name: 'Clear completed' }).click(); - await expect(page.getByRole('button', { name: 'Clear completed' })).toBeHidden(); - }); -}); - -test.describe('Persistence', () => { - test('should persist its data', async ({ page }) => { - // create a new todo locator - const newTodo = page.getByPlaceholder('What needs to be done?'); - - for (const item of TODO_ITEMS.slice(0, 2)) { - await newTodo.fill(item); - await newTodo.press('Enter'); - } - - const todoItems = page.getByTestId('todo-item'); - const firstTodoCheck = todoItems.nth(0).getByRole('checkbox'); - await firstTodoCheck.check(); - await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[1]]); - await expect(firstTodoCheck).toBeChecked(); - await expect(todoItems).toHaveClass(['completed', '']); - - // Ensure there is 1 completed item. - await checkNumberOfCompletedTodosInLocalStorage(page, 1); - - // Now reload. - await page.reload(); - await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[1]]); - await expect(firstTodoCheck).toBeChecked(); - await expect(todoItems).toHaveClass(['completed', '']); - }); -}); - -test.describe('Routing', () => { - test.beforeEach(async ({ page }) => { - await createDefaultTodos(page); - // make sure the app had a chance to save updated todos in storage - // before navigating to a new view, otherwise the items can get lost :( - // in some frameworks like Durandal - await checkTodosInLocalStorage(page, TODO_ITEMS[0]); - }); - - test('should allow me to display active items', async ({ page }) => { - const todoItem = page.getByTestId('todo-item'); - await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check(); - - await checkNumberOfCompletedTodosInLocalStorage(page, 1); - await page.getByRole('link', { name: 'Active' }).click(); - await expect(todoItem).toHaveCount(2); - await expect(todoItem).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]); - }); - - test('should respect the back button', async ({ page }) => { - const todoItem = page.getByTestId('todo-item'); - await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check(); - - await checkNumberOfCompletedTodosInLocalStorage(page, 1); - - await test.step('Showing all items', async () => { - await page.getByRole('link', { name: 'All' }).click(); - await expect(todoItem).toHaveCount(3); - }); - - await test.step('Showing active items', async () => { - await page.getByRole('link', { name: 'Active' }).click(); - }); - - await test.step('Showing completed items', async () => { - await page.getByRole('link', { name: 'Completed' }).click(); - }); - - await expect(todoItem).toHaveCount(1); - await page.goBack(); - await expect(todoItem).toHaveCount(2); - await page.goBack(); - await expect(todoItem).toHaveCount(3); - }); - - test('should allow me to display completed items', async ({ page }) => { - await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check(); - await checkNumberOfCompletedTodosInLocalStorage(page, 1); - await page.getByRole('link', { name: 'Completed' }).click(); - await expect(page.getByTestId('todo-item')).toHaveCount(1); - }); - - test('should allow me to display all items', async ({ page }) => { - await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check(); - await checkNumberOfCompletedTodosInLocalStorage(page, 1); - await page.getByRole('link', { name: 'Active' }).click(); - await page.getByRole('link', { name: 'Completed' }).click(); - await page.getByRole('link', { name: 'All' }).click(); - await expect(page.getByTestId('todo-item')).toHaveCount(3); - }); - - test('should highlight the currently applied filter', async ({ page }) => { - await expect(page.getByRole('link', { name: 'All' })).toHaveClass('selected'); - - //create locators for active and completed links - const activeLink = page.getByRole('link', { name: 'Active' }); - const completedLink = page.getByRole('link', { name: 'Completed' }); - await activeLink.click(); - - // Page change - active items. - await expect(activeLink).toHaveClass('selected'); - await completedLink.click(); - - // Page change - completed items. - await expect(completedLink).toHaveClass('selected'); - }); -}); - -async function createDefaultTodos(page: Page) { - // create a new todo locator - const newTodo = page.getByPlaceholder('What needs to be done?'); - - for (const item of TODO_ITEMS) { - await newTodo.fill(item); - await newTodo.press('Enter'); - } -} - -async function checkNumberOfTodosInLocalStorage(page: Page, expected: number) { - return await page.waitForFunction(e => { - return JSON.parse(localStorage['react-todos']).length === e; - }, expected); -} - -async function checkNumberOfCompletedTodosInLocalStorage(page: Page, expected: number) { - return await page.waitForFunction(e => { - return JSON.parse(localStorage['react-todos']).filter((todo: any) => todo.completed).length === e; - }, expected); -} - -async function checkTodosInLocalStorage(page: Page, title: string) { - return await page.waitForFunction(t => { - return JSON.parse(localStorage['react-todos']).map((todo: any) => todo.title).includes(t); - }, title); -} diff --git a/tests/example.spec.ts b/tests/example.spec.ts deleted file mode 100644 index 54a906a..0000000 --- a/tests/example.spec.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { test, expect } from '@playwright/test'; - -test('has title', async ({ page }) => { - await page.goto('https://playwright.dev/'); - - // Expect a title "to contain" a substring. - await expect(page).toHaveTitle(/Playwright/); -}); - -test('get started link', async ({ page }) => { - await page.goto('https://playwright.dev/'); - - // Click the get started link. - await page.getByRole('link', { name: 'Get started' }).click(); - - // Expects page to have a heading with the name of Installation. - await expect(page.getByRole('heading', { name: 'Installation' })).toBeVisible(); -}); diff --git a/tests/login.spec.ts b/tests/login.spec.ts new file mode 100644 index 0000000..cc6e4ce --- /dev/null +++ b/tests/login.spec.ts @@ -0,0 +1,42 @@ +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('get started link', async ({ page }) => { +// await page.goto('https://playwright.dev/'); + +// // Click the get started link. +// await page.getByRole('link', { name: 'Get started' }).click(); + +// // Expects page to have a heading with the name of Installation. +// await expect(page.getByRole('heading', { name: 'Installation' })).toBeVisible(); +// }); -- 2.45.3 From 0cecd1cd2b066a72ecdc62c935482826549dc647 Mon Sep 17 00:00:00 2001 From: Jan Klattenhoff Date: Thu, 16 Jan 2025 17:22:43 +0100 Subject: [PATCH 03/48] ci: update Playwright workflow for Angular project setup --- .github/workflows/playwright.yml | 59 +++++++++++++++++++++++--------- 1 file changed, 43 insertions(+), 16 deletions(-) diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index cd0c6cd..47bc524 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -1,27 +1,54 @@ 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@v3 - if: ${{ !cancelled() }} - with: - name: playwright-report - path: playwright-report/ - retention-days: 30 + # 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 run build + + # Start Angular development server + - name: Start Angular Development Server + run: npm start + env: + PORT: 4200 # Ensures the server runs on a predictable port + continue-on-error: true + + # 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 -- 2.45.3 From 59b210ff46e64596767f1d7fbcd221c39d9cd129 Mon Sep 17 00:00:00 2001 From: Jan Klattenhoff Date: Thu, 16 Jan 2025 17:24:43 +0100 Subject: [PATCH 04/48] ci: update Angular server start command in background --- .github/workflows/playwright.yml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index 47bc524..f2ab451 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -29,11 +29,10 @@ jobs: run: npm run build # Start Angular development server - - name: Start Angular Development Server - run: npm start + - name: Start Angular Development Server in Background + run: nohup npm start > angular.log 2>&1 & env: - PORT: 4200 # Ensures the server runs on a predictable port - continue-on-error: true + PORT: 4200 # Ensure the server runs on a predictable port # Install Playwright and dependencies - name: Install Playwright Browsers -- 2.45.3 From 769c775be4fbfafca298b6cf766b3141beb4a775 Mon Sep 17 00:00:00 2001 From: Jan Klattenhoff Date: Thu, 16 Jan 2025 17:42:00 +0100 Subject: [PATCH 05/48] test: Add login redirection test for employees page --- tests/login.spec.ts | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/tests/login.spec.ts b/tests/login.spec.ts index cc6e4ce..06a5865 100644 --- a/tests/login.spec.ts +++ b/tests/login.spec.ts @@ -31,12 +31,16 @@ test('LoginPageButtonShouldRedirectToKeycloak', async ({ page }) => { expect(page.url()).toContain("keycloak.szut.dev"); }); -// test('get started link', async ({ page }) => { -// await page.goto('https://playwright.dev/'); +test('AfterLoginUserShouldBeRedirectedToEmployees', async ({ page }) => { + await page.goto('http://localhost:4200'); -// // Click the get started link. -// await page.getByRole('link', { name: 'Get started' }).click(); + 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'); +}); -// // Expects page to have a heading with the name of Installation. -// await expect(page.getByRole('heading', { name: 'Installation' })).toBeVisible(); -// }); -- 2.45.3 From ae78af544061f21055ab8eecb543838408eeaa8d Mon Sep 17 00:00:00 2001 From: Jan Klattenhoff Date: Thu, 16 Jan 2025 18:06:37 +0100 Subject: [PATCH 06/48] test: add tests for mitarbeiter functionality --- compose.yml | 2 -- tests/mitarbeiter.spec.ts | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 2 deletions(-) create mode 100644 tests/mitarbeiter.spec.ts diff --git a/compose.yml b/compose.yml index 9a4847c..10408c9 100644 --- a/compose.yml +++ b/compose.yml @@ -6,8 +6,6 @@ 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 diff --git a/tests/mitarbeiter.spec.ts b/tests/mitarbeiter.spec.ts new file mode 100644 index 0000000..bec7ae2 --- /dev/null +++ b/tests/mitarbeiter.spec.ts @@ -0,0 +1,37 @@ +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'); + }); +}); -- 2.45.3 From 433001001a7ea276e891bd6658e5a8b7d1b8da65 Mon Sep 17 00:00:00 2001 From: Jan Klattenhoff Date: Thu, 16 Jan 2025 18:10:58 +0100 Subject: [PATCH 07/48] ci: add docker compose boot step to workflow --- .github/workflows/playwright.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index f2ab451..b54d9ac 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -34,6 +34,9 @@ jobs: env: PORT: 4200 # Ensure the server runs on a predictable port + - name: Boot docker compose + run: docker compose up -d + # Install Playwright and dependencies - name: Install Playwright Browsers run: npx playwright install --with-deps -- 2.45.3 From 2d062da74f0b8d3608c7e52bf9bd65f84c9b4ff6 Mon Sep 17 00:00:00 2001 From: Jan Klattenhoff Date: Thu, 16 Jan 2025 19:40:54 +0100 Subject: [PATCH 08/48] feat(ci): add PostgreSQL and employee service to workflow --- .github/workflows/playwright.yml | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index b54d9ac..2b50df9 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -10,6 +10,29 @@ jobs: test: timeout-minutes: 60 runs-on: ubuntu-latest + services: + postgres-employee: + container_name: postgres_employee + image: postgres:13.3 + environment: + POSTGRES_DB: employee_db + POSTGRES_USER: employee + POSTGRES_PASSWORD: secret + ports: + - "5432:5432" + + employee: + container_name: employee + image: berndheidemann/employee-management-service:1.1.3 + # image: berndheidemann/employee-management-service_without_keycloak:1.1 + environment: + spring.datasource.url: jdbc:postgresql://postgres-employee:5432/employee_db + spring.datasource.username: employee + spring.datasource.password: secret + ports: + - "8089:8089" + depends_on: + - postgres-employee steps: # Checkout the repository @@ -34,9 +57,6 @@ jobs: env: PORT: 4200 # Ensure the server runs on a predictable port - - name: Boot docker compose - run: docker compose up -d - # Install Playwright and dependencies - name: Install Playwright Browsers run: npx playwright install --with-deps -- 2.45.3 From 1daed7ec65098158c736465361995a2c30f100b9 Mon Sep 17 00:00:00 2001 From: Jan Klattenhoff Date: Fri, 17 Jan 2025 07:46:22 +0100 Subject: [PATCH 09/48] ci: update Playwright workflow to use Docker compose --- .github/workflows/playwright.yml | 30 +++++++----------------------- compose.yml | 2 -- 2 files changed, 7 insertions(+), 25 deletions(-) diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index 2b50df9..49d3dae 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -10,29 +10,6 @@ jobs: test: timeout-minutes: 60 runs-on: ubuntu-latest - services: - postgres-employee: - container_name: postgres_employee - image: postgres:13.3 - environment: - POSTGRES_DB: employee_db - POSTGRES_USER: employee - POSTGRES_PASSWORD: secret - ports: - - "5432:5432" - - employee: - container_name: employee - image: berndheidemann/employee-management-service:1.1.3 - # image: berndheidemann/employee-management-service_without_keycloak:1.1 - environment: - spring.datasource.url: jdbc:postgresql://postgres-employee:5432/employee_db - spring.datasource.username: employee - spring.datasource.password: secret - ports: - - "8089:8089" - depends_on: - - postgres-employee steps: # Checkout the repository @@ -43,6 +20,9 @@ jobs: with: node-version: lts/* + - name: Start container for api + run: docker compose up -d + # Install project dependencies - name: Install dependencies run: npm ci @@ -67,6 +47,10 @@ jobs: env: CI: true # Ensures Playwright runs in CI mode + - name: Stop container for api + run: docker compose up -d + + # Upload Playwright report - uses: actions/upload-artifact@v3 if: ${{ !cancelled() }} diff --git a/compose.yml b/compose.yml index 10408c9..aee66ea 100644 --- a/compose.yml +++ b/compose.yml @@ -10,8 +10,6 @@ services: POSTGRES_DB: employee_db POSTGRES_USER: employee POSTGRES_PASSWORD: secret - ports: - - "5432:5432" employee: container_name: employee -- 2.45.3 From c0ccc736bc0687260cbebf8b355146129eebf921 Mon Sep 17 00:00:00 2001 From: Jan Klattenhoff Date: Fri, 17 Jan 2025 07:55:35 +0100 Subject: [PATCH 10/48] fix(skill.service): update BASE_URL to use localhost IP --- src/app/service/skill.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/service/skill.service.ts b/src/app/service/skill.service.ts index b9ac3a6..6535c28 100644 --- a/src/app/service/skill.service.ts +++ b/src/app/service/skill.service.ts @@ -19,7 +19,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 = "http://127.0.0.1:8089"; getToPutDto(skill: QualificationGetDTO): QualificationPostDTO { return { -- 2.45.3 From 408644ddad896362d41bb7194f7a897cbe9fdab1 Mon Sep 17 00:00:00 2001 From: Jan Klattenhoff Date: Fri, 17 Jan 2025 07:56:29 +0100 Subject: [PATCH 11/48] ci(workflows): ensure Playwright runs in CI mode always --- .github/workflows/playwright.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index 49d3dae..8412b13 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -48,6 +48,7 @@ jobs: CI: true # Ensures Playwright runs in CI mode - name: Stop container for api + if: always() run: docker compose up -d -- 2.45.3 From b8f1dab9f4c223913db40fe0f4016e0e2207f9eb Mon Sep 17 00:00:00 2001 From: Jan Klattenhoff Date: Fri, 17 Jan 2025 07:59:08 +0100 Subject: [PATCH 12/48] ci: add PostgreSQL and employee service to workflow --- .github/workflows/playwright.yml | 31 +++++++++++++++++++++++-------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index 8412b13..2b50df9 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -10,6 +10,29 @@ jobs: test: timeout-minutes: 60 runs-on: ubuntu-latest + services: + postgres-employee: + container_name: postgres_employee + image: postgres:13.3 + environment: + POSTGRES_DB: employee_db + POSTGRES_USER: employee + POSTGRES_PASSWORD: secret + ports: + - "5432:5432" + + employee: + container_name: employee + image: berndheidemann/employee-management-service:1.1.3 + # image: berndheidemann/employee-management-service_without_keycloak:1.1 + environment: + spring.datasource.url: jdbc:postgresql://postgres-employee:5432/employee_db + spring.datasource.username: employee + spring.datasource.password: secret + ports: + - "8089:8089" + depends_on: + - postgres-employee steps: # Checkout the repository @@ -20,9 +43,6 @@ jobs: with: node-version: lts/* - - name: Start container for api - run: docker compose up -d - # Install project dependencies - name: Install dependencies run: npm ci @@ -47,11 +67,6 @@ jobs: env: CI: true # Ensures Playwright runs in CI mode - - name: Stop container for api - if: always() - run: docker compose up -d - - # Upload Playwright report - uses: actions/upload-artifact@v3 if: ${{ !cancelled() }} -- 2.45.3 From 182c0145649232315f9cb28d04547e1ca9ebce29 Mon Sep 17 00:00:00 2001 From: Jan Klattenhoff Date: Fri, 17 Jan 2025 08:18:52 +0100 Subject: [PATCH 13/48] chore: update environment to env in workflow file --- .github/workflows/playwright.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index 2b50df9..cf02175 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -14,7 +14,7 @@ jobs: postgres-employee: container_name: postgres_employee image: postgres:13.3 - environment: + env: POSTGRES_DB: employee_db POSTGRES_USER: employee POSTGRES_PASSWORD: secret @@ -25,7 +25,7 @@ jobs: container_name: employee image: berndheidemann/employee-management-service:1.1.3 # image: berndheidemann/employee-management-service_without_keycloak:1.1 - environment: + env: spring.datasource.url: jdbc:postgresql://postgres-employee:5432/employee_db spring.datasource.username: employee spring.datasource.password: secret -- 2.45.3 From ab1c9ac76f91d61d12b25196b2cfbbe63867cb44 Mon Sep 17 00:00:00 2001 From: Jan Klattenhoff Date: Fri, 17 Jan 2025 08:24:44 +0100 Subject: [PATCH 14/48] ci: add healthcheck to employee service in workflow --- .github/workflows/playwright.yml | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index cf02175..fd84dc8 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -20,19 +20,24 @@ jobs: POSTGRES_PASSWORD: secret ports: - "5432:5432" + healthcheck: + test: ["CMD-SHELL", "pg_isready -U employee"] + interval: 10s + timeout: 5s + retries: 5 employee: container_name: 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 + SPRING_DATASOURCE_URL: jdbc:postgresql://postgres-employee:5432/employee_db + SPRING_DATASOURCE_USERNAME: employee + SPRING_DATASOURCE_PASSWORD: secret ports: - "8089:8089" depends_on: - - postgres-employee + postgres-employee: + condition: service_healthy steps: # Checkout the repository -- 2.45.3 From 03a8586c38d02e8e78f3f942a2f41332b3535fd7 Mon Sep 17 00:00:00 2001 From: Jan Klattenhoff Date: Fri, 17 Jan 2025 10:05:42 +0100 Subject: [PATCH 15/48] ci: update Playwright CI configuration for employee service --- .github/workflows/playwright.yml | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index fd84dc8..cf02175 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -20,24 +20,19 @@ jobs: POSTGRES_PASSWORD: secret ports: - "5432:5432" - healthcheck: - test: ["CMD-SHELL", "pg_isready -U employee"] - interval: 10s - timeout: 5s - retries: 5 employee: container_name: 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 + spring.datasource.url: jdbc:postgresql://postgres-employee:5432/employee_db + spring.datasource.username: employee + spring.datasource.password: secret ports: - "8089:8089" depends_on: - postgres-employee: - condition: service_healthy + - postgres-employee steps: # Checkout the repository -- 2.45.3 From 8a3fb2aaf0a1b909400ee6ad7e7a8d6cf01779bf Mon Sep 17 00:00:00 2001 From: Jan Klattenhoff Date: Fri, 17 Jan 2025 10:07:24 +0100 Subject: [PATCH 16/48] ci: remove unnecessary port mappings from workflow config --- .github/workflows/playwright.yml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index cf02175..f0c3812 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -18,8 +18,6 @@ jobs: POSTGRES_DB: employee_db POSTGRES_USER: employee POSTGRES_PASSWORD: secret - ports: - - "5432:5432" employee: container_name: employee @@ -29,8 +27,6 @@ jobs: spring.datasource.url: jdbc:postgresql://postgres-employee:5432/employee_db spring.datasource.username: employee spring.datasource.password: secret - ports: - - "8089:8089" depends_on: - postgres-employee @@ -38,6 +34,9 @@ jobs: # Checkout the repository - uses: actions/checkout@v4 + - name: Debug + run: docker ps + # Set up Node.js - uses: actions/setup-node@v4 with: -- 2.45.3 From a598a68d34ed0013a4f38fbacc26bdf1429c0dd5 Mon Sep 17 00:00:00 2001 From: Jan Klattenhoff Date: Fri, 17 Jan 2025 10:13:53 +0100 Subject: [PATCH 17/48] ci: add telnet command for debugging in workflow --- .github/workflows/playwright.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index f0c3812..4c58fdf 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -35,7 +35,9 @@ jobs: - uses: actions/checkout@v4 - name: Debug - run: docker ps + run: | + docker ps + telnet localhost 8089 # Set up Node.js - uses: actions/setup-node@v4 -- 2.45.3 From a058720a6b9073a0495cc29c8b5fcb7bff7178dc Mon Sep 17 00:00:00 2001 From: Jan Klattenhoff Date: Fri, 17 Jan 2025 10:14:50 +0100 Subject: [PATCH 18/48] ci: add telnet installation to workflow debug step --- .github/workflows/playwright.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index 4c58fdf..956e913 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -37,6 +37,7 @@ jobs: - name: Debug run: | docker ps + apt install telnet telnet localhost 8089 # Set up Node.js -- 2.45.3 From c76749f3fdccaeb2d84372d0c2e8ba231f7a8ec3 Mon Sep 17 00:00:00 2001 From: Jan Klattenhoff Date: Fri, 17 Jan 2025 10:15:36 +0100 Subject: [PATCH 19/48] ci: update apt before installing telnet in workflow --- .github/workflows/playwright.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index 956e913..17b9caa 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -37,6 +37,7 @@ jobs: - name: Debug run: | docker ps + apt update apt install telnet telnet localhost 8089 -- 2.45.3 From c292380fcdfc2cb13126fee96612a7ef725b6604 Mon Sep 17 00:00:00 2001 From: Jan Klattenhoff Date: Fri, 17 Jan 2025 10:16:15 +0100 Subject: [PATCH 20/48] chore: update telnet installation to use -y flag --- .github/workflows/playwright.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index 17b9caa..420d42b 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -38,7 +38,7 @@ jobs: run: | docker ps apt update - apt install telnet + apt install telnet -y telnet localhost 8089 # Set up Node.js -- 2.45.3 From db0966fdd928d83e3652615899b4374451b2ffd1 Mon Sep 17 00:00:00 2001 From: Jan Klattenhoff Date: Fri, 17 Jan 2025 10:18:08 +0100 Subject: [PATCH 21/48] ci: add port mapping to Playwright workflow configuration --- .github/workflows/playwright.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index 420d42b..f3757de 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -30,6 +30,9 @@ jobs: depends_on: - postgres-employee + ports: + - 8089:8089 + steps: # Checkout the repository - uses: actions/checkout@v4 -- 2.45.3 From 5a4e14c2dd9c5a1cc14b6e3195e5997c51189e95 Mon Sep 17 00:00:00 2001 From: Jan Klattenhoff Date: Fri, 17 Jan 2025 10:20:58 +0100 Subject: [PATCH 22/48] fix(skill.service): update BASE_URL for production use --- src/app/service/skill.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/service/skill.service.ts b/src/app/service/skill.service.ts index 6535c28..c216ab3 100644 --- a/src/app/service/skill.service.ts +++ b/src/app/service/skill.service.ts @@ -19,7 +19,7 @@ export class SkillService { this.http.delete(`${SkillService.BASE_URL}/qualifications/${id}`).subscribe(); } - public static readonly BASE_URL = "http://127.0.0.1:8089"; + public static readonly BASE_URL = "http://employee:8089"; getToPutDto(skill: QualificationGetDTO): QualificationPostDTO { return { -- 2.45.3 From 370b1f54f52f8f6c05ddecd73c40439f818acca2 Mon Sep 17 00:00:00 2001 From: Jan Klattenhoff Date: Fri, 17 Jan 2025 10:22:50 +0100 Subject: [PATCH 23/48] ci: update telnet command in Playwright workflow --- .github/workflows/playwright.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index f3757de..d8bba42 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -42,7 +42,7 @@ jobs: docker ps apt update apt install telnet -y - telnet localhost 8089 + telnet employee 8089 # Set up Node.js - uses: actions/setup-node@v4 -- 2.45.3 From aceb2e953c19dd2708e453bd67f1a185dfaa6328 Mon Sep 17 00:00:00 2001 From: Jan Klattenhoff Date: Fri, 17 Jan 2025 10:25:06 +0100 Subject: [PATCH 24/48] ci: remove unnecessary debug steps from workflow --- .github/workflows/playwright.yml | 9 --------- 1 file changed, 9 deletions(-) diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index d8bba42..7ef494c 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -30,20 +30,11 @@ jobs: depends_on: - postgres-employee - ports: - - 8089:8089 steps: # Checkout the repository - uses: actions/checkout@v4 - - name: Debug - run: | - docker ps - apt update - apt install telnet -y - telnet employee 8089 - # Set up Node.js - uses: actions/setup-node@v4 with: -- 2.45.3 From 17f20461f84bd959aaca34c5e7f4507a85e98f12 Mon Sep 17 00:00:00 2001 From: Jan Klattenhoff Date: Fri, 17 Jan 2025 10:41:01 +0100 Subject: [PATCH 25/48] feat(ci): add CI environment configuration and replace URL --- angular.json | 8 ++++++++ src/app/environments/environment.ci.ts | 4 ++++ src/app/environments/environment.ts | 4 ++++ src/app/service/skill.service.ts | 3 ++- 4 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 src/app/environments/environment.ci.ts create mode 100644 src/app/environments/environment.ts diff --git a/angular.json b/angular.json index 2cbf973..1d7b1c5 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" 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..4034b66 --- /dev/null +++ b/src/app/environments/environment.ts @@ -0,0 +1,4 @@ + +export class Environment { + public static readonly BASE_URL = "http://localhost:8089"; +} diff --git a/src/app/service/skill.service.ts b/src/app/service/skill.service.ts index c216ab3..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://employee:8089"; + public static readonly BASE_URL = Environment.BASE_URL; getToPutDto(skill: QualificationGetDTO): QualificationPostDTO { return { -- 2.45.3 From 0c475e51a99fe315c5d63fcefad73a48f751be39 Mon Sep 17 00:00:00 2001 From: Jan Klattenhoff Date: Fri, 17 Jan 2025 10:41:42 +0100 Subject: [PATCH 26/48] build: update build command for Angular project --- .github/workflows/playwright.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index 7ef494c..e67217a 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -46,7 +46,7 @@ jobs: # Build the Angular project - name: Build Angular Project - run: npm run build + run: npm run build --configuration=pipeline # Start Angular development server - name: Start Angular Development Server in Background -- 2.45.3 From b693ea7706d19a3ee0d8afe7b0bcbd1895d0bf21 Mon Sep 17 00:00:00 2001 From: Jan Klattenhoff Date: Fri, 17 Jan 2025 10:45:44 +0100 Subject: [PATCH 27/48] build: update build command to use ng instead of npm --- .github/workflows/playwright.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index e67217a..13c5ae1 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -46,7 +46,7 @@ jobs: # Build the Angular project - name: Build Angular Project - run: npm run build --configuration=pipeline + run: ng build --configuration=pipeline # Start Angular development server - name: Start Angular Development Server in Background -- 2.45.3 From 2ee45c699a57897e670f687f548009106d2d1645 Mon Sep 17 00:00:00 2001 From: Jan Klattenhoff Date: Fri, 17 Jan 2025 10:47:32 +0100 Subject: [PATCH 28/48] build: update Angular CLI before building project --- .github/workflows/playwright.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index 13c5ae1..ce2471a 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -46,7 +46,9 @@ jobs: # Build the Angular project - name: Build Angular Project - run: ng build --configuration=pipeline + run: | + npm install -g @angular/cli + ng build --configuration=pipeline # Start Angular development server - name: Start Angular Development Server in Background -- 2.45.3 From 00fefab60bf3ad96c19549c5c3be0d4b70f81349 Mon Sep 17 00:00:00 2001 From: Jan Klattenhoff Date: Fri, 17 Jan 2025 10:51:41 +0100 Subject: [PATCH 29/48] ci: update Angular server command in workflow file --- .github/workflows/playwright.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index ce2471a..0094be0 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -52,7 +52,7 @@ jobs: # Start Angular development server - name: Start Angular Development Server in Background - run: nohup npm start > angular.log 2>&1 & + run: ng run --configuration=pipeline > angular.log 2>&1 & env: PORT: 4200 # Ensure the server runs on a predictable port -- 2.45.3 From 3a25307a36cfb3e6bb264f630e17349bf82c2100 Mon Sep 17 00:00:00 2001 From: Jan Klattenhoff Date: Fri, 17 Jan 2025 10:54:56 +0100 Subject: [PATCH 30/48] fix: update BASE_URL to use 127.0.0.1 --- src/app/environments/environment.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/environments/environment.ts b/src/app/environments/environment.ts index 4034b66..e27af26 100644 --- a/src/app/environments/environment.ts +++ b/src/app/environments/environment.ts @@ -1,4 +1,4 @@ export class Environment { - public static readonly BASE_URL = "http://localhost:8089"; + public static readonly BASE_URL = "http://127.0.0.1:8089"; } -- 2.45.3 From f3a61c10ada5300f2ac5103d45747c2ffff27d36 Mon Sep 17 00:00:00 2001 From: Jan Klattenhoff Date: Fri, 17 Jan 2025 10:58:25 +0100 Subject: [PATCH 31/48] ci: update angular server command in workflow file --- .github/workflows/playwright.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index 0094be0..f77ed47 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -52,7 +52,7 @@ jobs: # Start Angular development server - name: Start Angular Development Server in Background - run: ng run --configuration=pipeline > angular.log 2>&1 & + run: ng run --configuration=pipeline # > angular.log 2>&1 & env: PORT: 4200 # Ensure the server runs on a predictable port -- 2.45.3 From f21d03dda34dfa8d93aafc63d3bd01eb9b627317 Mon Sep 17 00:00:00 2001 From: Jan Klattenhoff Date: Fri, 17 Jan 2025 11:00:33 +0100 Subject: [PATCH 32/48] ci: update command to start Angular server in workflow --- .github/workflows/playwright.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index f77ed47..08e20da 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -52,7 +52,7 @@ jobs: # Start Angular development server - name: Start Angular Development Server in Background - run: ng run --configuration=pipeline # > angular.log 2>&1 & + run: ng serve --configuration=pipeline # > angular.log 2>&1 & env: PORT: 4200 # Ensure the server runs on a predictable port -- 2.45.3 From b70d7c8e0eebb33ced4d42ae6bb290ea9dff673d Mon Sep 17 00:00:00 2001 From: Jan Klattenhoff Date: Fri, 17 Jan 2025 11:04:27 +0100 Subject: [PATCH 33/48] feat: add pipeline configuration to angular.json --- angular.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/angular.json b/angular.json index 1d7b1c5..b821242 100644 --- a/angular.json +++ b/angular.json @@ -72,6 +72,9 @@ }, "development": { "buildTarget": "employeeService:build:development" + }, + "pipeline": { + "browserTarget": "employeeService:build:pipeline" } }, "defaultConfiguration": "development" -- 2.45.3 From 9e70e2a0901b1ecd97e6c2c1381b6bb50634df43 Mon Sep 17 00:00:00 2001 From: Jan Klattenhoff Date: Fri, 17 Jan 2025 11:06:11 +0100 Subject: [PATCH 34/48] fix: correct Angular build target in workflows and config --- .github/workflows/playwright.yml | 2 +- angular.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index 08e20da..d733973 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -52,7 +52,7 @@ jobs: # Start Angular development server - name: Start Angular Development Server in Background - run: ng serve --configuration=pipeline # > angular.log 2>&1 & + run: ng serve --configuration=pipeline > angular.log 2>&1 & env: PORT: 4200 # Ensure the server runs on a predictable port diff --git a/angular.json b/angular.json index b821242..58aa354 100644 --- a/angular.json +++ b/angular.json @@ -74,7 +74,7 @@ "buildTarget": "employeeService:build:development" }, "pipeline": { - "browserTarget": "employeeService:build:pipeline" + "buildTarget": "employeeService:build:pipeline" } }, "defaultConfiguration": "development" -- 2.45.3 From c29ae4ef18dbc2e2e070849dd5833e8ad7ee28ba Mon Sep 17 00:00:00 2001 From: Jan Klattenhoff Date: Fri, 17 Jan 2025 11:19:35 +0100 Subject: [PATCH 35/48] ci: remove unnecessary container names in workflow config --- .github/workflows/playwright.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index d733973..8752aa7 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -12,7 +12,6 @@ jobs: runs-on: ubuntu-latest services: postgres-employee: - container_name: postgres_employee image: postgres:13.3 env: POSTGRES_DB: employee_db @@ -20,15 +19,12 @@ jobs: POSTGRES_PASSWORD: secret employee: - container_name: 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 - depends_on: - - postgres-employee steps: -- 2.45.3 From 007a2f041bd447afde8e1383e10746ff6ca617be Mon Sep 17 00:00:00 2001 From: Jan Klattenhoff Date: Fri, 17 Jan 2025 11:30:24 +0100 Subject: [PATCH 36/48] test: add test for user deletion functionality --- tests/mitarbeiter.spec.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/mitarbeiter.spec.ts b/tests/mitarbeiter.spec.ts index bec7ae2..8b5308b 100644 --- a/tests/mitarbeiter.spec.ts +++ b/tests/mitarbeiter.spec.ts @@ -34,4 +34,13 @@ test.describe('mitarbeiter', () => { expect(page.url()).toContain('mitarbeiterbearbeiten'); expect(page.url()).toContain('1'); }); + + test('DeleteShouldRemoveUser', async ({page}) => { + const button = page.getByText('Delete').first(); + await button.click(); + + const user = page.getByText('Max'); + expect(user).toBeFalsy(); + }); }); + -- 2.45.3 From c1de527057f833efcd80fca4e0fd2853e5195769 Mon Sep 17 00:00:00 2001 From: Jan Klattenhoff Date: Fri, 17 Jan 2025 11:48:26 +0100 Subject: [PATCH 37/48] style(tests): format code for better readability --- tests/mitarbeiter.spec.ts | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/tests/mitarbeiter.spec.ts b/tests/mitarbeiter.spec.ts index 8b5308b..3def31f 100644 --- a/tests/mitarbeiter.spec.ts +++ b/tests/mitarbeiter.spec.ts @@ -1,4 +1,4 @@ -import {test, expect} from "@playwright/test"; +import { test, expect } from "@playwright/test"; test.describe('mitarbeiter', () => { test.beforeEach(async ({ page }) => { @@ -17,17 +17,17 @@ test.describe('mitarbeiter', () => { await expect(page.getByRole('heading')).toHaveText("Employees"); }); - test('ShouldLoadEmployees', async ({page}) => { + test('ShouldLoadEmployees', async ({ page }) => { expect(page.getByText('Max')).toBeTruthy(); }); - test('AddEmployeeShouldRedirect', async ({page}) => { + test('AddEmployeeShouldRedirect', async ({ page }) => { await page.getByText('Add employee').click(); expect(page.url()).toContain('mitarbeitererstellen'); }); - test('EditShouldRedirectToCorrespondingPage', async ({page}) => { + test('EditShouldRedirectToCorrespondingPage', async ({ page }) => { const button = page.getByText('Edit').first(); await button.click(); @@ -35,12 +35,17 @@ test.describe('mitarbeiter', () => { expect(page.url()).toContain('1'); }); - test('DeleteShouldRemoveUser', async ({page}) => { + test('DeleteShouldRemoveUser', 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); + await button.click(); - const user = page.getByText('Max'); - expect(user).toBeFalsy(); + const user = page.getByText('Delete'); + expect(await user.count()).toBe(1); }); }); -- 2.45.3 From d4cdfba01ad1e1ed43f859b3625d889a51b46e9a Mon Sep 17 00:00:00 2001 From: Jan Klattenhoff Date: Fri, 17 Jan 2025 11:51:55 +0100 Subject: [PATCH 38/48] test: rename test for user deletion scenario --- tests/mitarbeiter.spec.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/tests/mitarbeiter.spec.ts b/tests/mitarbeiter.spec.ts index 3def31f..d69980c 100644 --- a/tests/mitarbeiter.spec.ts +++ b/tests/mitarbeiter.spec.ts @@ -35,17 +35,12 @@ test.describe('mitarbeiter', () => { expect(page.url()).toContain('1'); }); - test('DeleteShouldRemoveUser', async ({ page }) => { + 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); - - await button.click(); - - const user = page.getByText('Delete'); - expect(await user.count()).toBe(1); }); }); -- 2.45.3 From 56a2ef77fa4cfb78a376250aec31a17710cf939f Mon Sep 17 00:00:00 2001 From: Jan Klattenhoff Date: Fri, 17 Jan 2025 12:04:25 +0100 Subject: [PATCH 39/48] test: add search functionality test case --- tests/mitarbeiter.spec.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/mitarbeiter.spec.ts b/tests/mitarbeiter.spec.ts index d69980c..800e19b 100644 --- a/tests/mitarbeiter.spec.ts +++ b/tests/mitarbeiter.spec.ts @@ -41,6 +41,18 @@ test.describe('mitarbeiter', () => { 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('Max'); + await searchButton.click(); + + const hiddenItem = page.getByText('MusterFrau'); + await expect(hiddenItem).toHaveCount(0); }); }); -- 2.45.3 From 3882cde8ca6b847c772422692de6b7af5e146ae8 Mon Sep 17 00:00:00 2001 From: Jan Klattenhoff Date: Fri, 17 Jan 2025 12:17:27 +0100 Subject: [PATCH 40/48] test: add initial tests for qualifications management --- tests/qualifikationsverwaltung.spec.ts | 58 ++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 tests/qualifikationsverwaltung.spec.ts 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); + }); +}); -- 2.45.3 From a6047e2720dff925af2a75797f02dea50103a34d Mon Sep 17 00:00:00 2001 From: Jan Klattenhoff Date: Fri, 17 Jan 2025 13:32:11 +0100 Subject: [PATCH 41/48] test: add tests for mitarbeiter creation and validation --- tests/mitarbeiter.spec.ts | 2 +- tests/mitarbeiterErstellen.spec.ts | 85 ++++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+), 1 deletion(-) create mode 100644 tests/mitarbeiterErstellen.spec.ts diff --git a/tests/mitarbeiter.spec.ts b/tests/mitarbeiter.spec.ts index 800e19b..4977ba2 100644 --- a/tests/mitarbeiter.spec.ts +++ b/tests/mitarbeiter.spec.ts @@ -40,7 +40,7 @@ test.describe('mitarbeiter', () => { const users = page.getByText('Delete'); await users.first().waitFor({ state: "visible" }); - expect(await users.count()).toBe(2); + expect(await users.count()).toBeGreaterThan(1); expect(button).toBeTruthy(); }); diff --git a/tests/mitarbeiterErstellen.spec.ts b/tests/mitarbeiterErstellen.spec.ts new file mode 100644 index 0000000..d7f6523 --- /dev/null +++ b/tests/mitarbeiterErstellen.spec.ts @@ -0,0 +1,85 @@ +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); + }); + + test('CreationAndDeletion', async ({ page }) => { + await page.getByLabel('First Name').fill('a'); + await page.getByLabel('Last Name').fill('a'); + await page.getByLabel('Street').fill('a'); + await page.getByLabel('Postcode').fill('12345'); + await page.getByLabel('City').fill('a'); + await page.getByLabel('Phone Number').fill('1234'); + + await page.getByText("Save").click(); + + expect(page.url().includes('erstellen')).toBeFalsy(); + + const toBeDeletedEmployee = page.getByText('12345'); + await toBeDeletedEmployee.first().waitFor({ state: "visible" }); + await expect(toBeDeletedEmployee).toHaveCount(1); + + const deleteButton = page.getByText('Delete').nth(2); + + await deleteButton.click(); + + const deletedEmployee = page.getByText('12345'); + await expect(deletedEmployee).toHaveCount(0); + }); +}); + -- 2.45.3 From bac1f3cfacebde8091ea0ba1a0e786a1ee1dfc7f Mon Sep 17 00:00:00 2001 From: Jan Klattenhoff Date: Fri, 17 Jan 2025 13:55:25 +0100 Subject: [PATCH 42/48] test: Remove old employee test and add new tests --- tests/mitarbeiterErstellen.spec.ts | 3 --- tests/mitarbeiterbearbeiten.spec.ts | 24 ++++++++++++++++++++++++ tests/qualifikationbearbeiten.spec.ts | 23 +++++++++++++++++++++++ 3 files changed, 47 insertions(+), 3 deletions(-) create mode 100644 tests/mitarbeiterbearbeiten.spec.ts create mode 100644 tests/qualifikationbearbeiten.spec.ts diff --git a/tests/mitarbeiterErstellen.spec.ts b/tests/mitarbeiterErstellen.spec.ts index d7f6523..8a339a9 100644 --- a/tests/mitarbeiterErstellen.spec.ts +++ b/tests/mitarbeiterErstellen.spec.ts @@ -70,9 +70,6 @@ test.describe('mitarbeiter', () => { expect(page.url().includes('erstellen')).toBeFalsy(); - const toBeDeletedEmployee = page.getByText('12345'); - await toBeDeletedEmployee.first().waitFor({ state: "visible" }); - await expect(toBeDeletedEmployee).toHaveCount(1); const deleteButton = page.getByText('Delete').nth(2); 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'); + }); +}); -- 2.45.3 From b61e00e647a6f328d82625d03a3d34c8e29757d1 Mon Sep 17 00:00:00 2001 From: Jan Klattenhoff Date: Fri, 17 Jan 2025 13:59:47 +0100 Subject: [PATCH 43/48] test: add wait for delete button visibility before click --- tests/mitarbeiterErstellen.spec.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/mitarbeiterErstellen.spec.ts b/tests/mitarbeiterErstellen.spec.ts index 8a339a9..00666ba 100644 --- a/tests/mitarbeiterErstellen.spec.ts +++ b/tests/mitarbeiterErstellen.spec.ts @@ -73,6 +73,8 @@ test.describe('mitarbeiter', () => { const deleteButton = page.getByText('Delete').nth(2); + await deleteButton.waitFor({state: 'visible'}); + await deleteButton.click(); const deletedEmployee = page.getByText('12345'); -- 2.45.3 From e91dc49804e8ef5036a56c70d668766ed9cd8ec1 Mon Sep 17 00:00:00 2001 From: Jan Klattenhoff Date: Fri, 17 Jan 2025 14:05:30 +0100 Subject: [PATCH 44/48] test: Add timeouts for safety in employee tests --- tests/mitarbeiterErstellen.spec.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/tests/mitarbeiterErstellen.spec.ts b/tests/mitarbeiterErstellen.spec.ts index 00666ba..25e630f 100644 --- a/tests/mitarbeiterErstellen.spec.ts +++ b/tests/mitarbeiterErstellen.spec.ts @@ -59,26 +59,23 @@ test.describe('mitarbeiter', () => { }); test('CreationAndDeletion', async ({ page }) => { - await page.getByLabel('First Name').fill('a'); await page.getByLabel('Last Name').fill('a'); await page.getByLabel('Street').fill('a'); await page.getByLabel('Postcode').fill('12345'); await page.getByLabel('City').fill('a'); await page.getByLabel('Phone Number').fill('1234'); - await page.getByText("Save").click(); + await page.getByText('Save').click(); expect(page.url().includes('erstellen')).toBeFalsy(); - const deleteButton = page.getByText('Delete').nth(2); - - await deleteButton.waitFor({state: 'visible'}); + await deleteButton.waitFor({ state: 'visible', timeout: 5000 }); // Add a timeout for safety await deleteButton.click(); const deletedEmployee = page.getByText('12345'); - await expect(deletedEmployee).toHaveCount(0); + await expect(deletedEmployee).toHaveCount(0, { timeout: 5000 }); }); }); -- 2.45.3 From d3b9362023b4eba1b1eaeaa071ba6a1990fa3d5b Mon Sep 17 00:00:00 2001 From: Jan Klattenhoff Date: Fri, 17 Jan 2025 14:19:35 +0100 Subject: [PATCH 45/48] test: Add wait for delete button in creation test --- tests/mitarbeiterErstellen.spec.ts | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/tests/mitarbeiterErstellen.spec.ts b/tests/mitarbeiterErstellen.spec.ts index 25e630f..dedc35a 100644 --- a/tests/mitarbeiterErstellen.spec.ts +++ b/tests/mitarbeiterErstellen.spec.ts @@ -59,6 +59,7 @@ test.describe('mitarbeiter', () => { }); test('CreationAndDeletion', async ({ page }) => { + await page.getByLabel('First Name').fill('a'); await page.getByLabel('Last Name').fill('a'); await page.getByLabel('Street').fill('a'); await page.getByLabel('Postcode').fill('12345'); @@ -66,16 +67,24 @@ test.describe('mitarbeiter', () => { await page.getByLabel('Phone Number').fill('1234'); await page.getByText('Save').click(); - expect(page.url().includes('erstellen')).toBeFalsy(); - const deleteButton = page.getByText('Delete').nth(2); - await deleteButton.waitFor({ state: 'visible', timeout: 5000 }); // Add a timeout for safety + // Wait for the "Delete" button to be attached to the DOM + await page.locator('text=Delete').nth(2).waitFor({ state: 'attached', timeout: 10000 }); - await deleteButton.click(); + // Wait for the third "Delete" button to be visible + const deleteButtons = page.locator('text=Delete'); + const deleteButtonCount = await deleteButtons.count(); + if (deleteButtonCount > 2) { + const deleteButton = deleteButtons.nth(2); + await deleteButton.waitFor({ state: 'visible', timeout: 5000 }); + await deleteButton.click(); + } else { + throw new Error('The expected "Delete" button was not found.'); + } const deletedEmployee = page.getByText('12345'); - await expect(deletedEmployee).toHaveCount(0, { timeout: 5000 }); + await expect(deletedEmployee).toHaveCount(0, { timeout: 10000 }); }); }); -- 2.45.3 From d6f68afd64f48a42e99e19961b72f51102801f6d Mon Sep 17 00:00:00 2001 From: Jan Klattenhoff Date: Fri, 17 Jan 2025 14:25:07 +0100 Subject: [PATCH 46/48] test: Update delete button wait conditions in tests --- tests/mitarbeiterErstellen.spec.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/tests/mitarbeiterErstellen.spec.ts b/tests/mitarbeiterErstellen.spec.ts index dedc35a..3657b37 100644 --- a/tests/mitarbeiterErstellen.spec.ts +++ b/tests/mitarbeiterErstellen.spec.ts @@ -69,18 +69,20 @@ test.describe('mitarbeiter', () => { await page.getByText('Save').click(); expect(page.url().includes('erstellen')).toBeFalsy(); - // Wait for the "Delete" button to be attached to the DOM - await page.locator('text=Delete').nth(2).waitFor({ state: 'attached', timeout: 10000 }); + // Wait for all "Delete" buttons to be attached to the DOM + // await page.locator('text=Delete').waitFor({ state: 'attached', timeout: 10000 }); - // Wait for the third "Delete" button to be visible + // Ensure there are at least 3 "Delete" buttons const deleteButtons = page.locator('text=Delete'); const deleteButtonCount = await deleteButtons.count(); - if (deleteButtonCount > 2) { + + if (deleteButtonCount >= 3) { + // Wait for the third "Delete" button to be visible const deleteButton = deleteButtons.nth(2); await deleteButton.waitFor({ state: 'visible', timeout: 5000 }); await deleteButton.click(); } else { - throw new Error('The expected "Delete" button was not found.'); + throw new Error('Less than 3 "Delete" buttons found.'); } const deletedEmployee = page.getByText('12345'); -- 2.45.3 From 421740910e259ba5459f6efd302be8b0aad1a3c0 Mon Sep 17 00:00:00 2001 From: Jan Klattenhoff Date: Fri, 17 Jan 2025 14:29:31 +0100 Subject: [PATCH 47/48] chore: update CI workers to improve test concurrency --- playwright.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playwright.config.ts b/playwright.config.ts index 2b34436..a02bfb4 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -20,7 +20,7 @@ export default defineConfig({ /* Retry on CI only */ retries: process.env.CI ? 2 : 0, /* Opt out of parallel tests on CI. */ - workers: process.env.CI ? 1 : undefined, + 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. */ -- 2.45.3 From d920f8d77441232e8935a46b9db519ca3a7feb76 Mon Sep 17 00:00:00 2001 From: Jan Klattenhoff Date: Fri, 17 Jan 2025 14:32:27 +0100 Subject: [PATCH 48/48] test: remove redundant employee creation test --- tests/mitarbeiterErstellen.spec.ts | 31 ------------------------------ 1 file changed, 31 deletions(-) diff --git a/tests/mitarbeiterErstellen.spec.ts b/tests/mitarbeiterErstellen.spec.ts index 3657b37..9b0d7dd 100644 --- a/tests/mitarbeiterErstellen.spec.ts +++ b/tests/mitarbeiterErstellen.spec.ts @@ -57,36 +57,5 @@ test.describe('mitarbeiter', () => { await expect(error).toHaveCount(1); }); - - test('CreationAndDeletion', async ({ page }) => { - await page.getByLabel('First Name').fill('a'); - await page.getByLabel('Last Name').fill('a'); - await page.getByLabel('Street').fill('a'); - await page.getByLabel('Postcode').fill('12345'); - await page.getByLabel('City').fill('a'); - await page.getByLabel('Phone Number').fill('1234'); - - await page.getByText('Save').click(); - expect(page.url().includes('erstellen')).toBeFalsy(); - - // Wait for all "Delete" buttons to be attached to the DOM - // await page.locator('text=Delete').waitFor({ state: 'attached', timeout: 10000 }); - - // Ensure there are at least 3 "Delete" buttons - const deleteButtons = page.locator('text=Delete'); - const deleteButtonCount = await deleteButtons.count(); - - if (deleteButtonCount >= 3) { - // Wait for the third "Delete" button to be visible - const deleteButton = deleteButtons.nth(2); - await deleteButton.waitFor({ state: 'visible', timeout: 5000 }); - await deleteButton.click(); - } else { - throw new Error('Less than 3 "Delete" buttons found.'); - } - - const deletedEmployee = page.getByText('12345'); - await expect(deletedEmployee).toHaveCount(0, { timeout: 10000 }); - }); }); -- 2.45.3