├── .husky └── pre-commit ├── .github ├── CODEOWNERS ├── dependabot.yml └── workflows │ ├── dependabot-auto-merge.yml │ ├── check-sanity.yml │ ├── check-changes.yml │ └── playwright.yml ├── .lintstagedrc.json ├── reports ├── playwright-logo.png └── index.html ├── .prettierignore ├── .prettierrc.json ├── docs ├── imgs │ ├── windows-disable-hyperv.png │ └── windows-enable-hyperv.png ├── minikube-setup-mac.md └── minikube-setup-windows.md ├── apis ├── BaseApi.ts ├── AuthApi.ts ├── BookingApi.ts └── RoomApi.ts ├── .vscode └── settings.json ├── pages ├── BasePage.ts ├── AdminPage.ts ├── components │ └── Header.ts ├── RoomsPage.ts └── FrontPage.ts ├── .gitignore ├── Dockerfile ├── merge-monocart-reports.ts ├── utils └── test-data-util.ts ├── docker-compose-restful-booker.yml ├── tests ├── admin-panel │ ├── login.spec.ts │ └── room-management.spec.ts └── front-page │ ├── contact-hotel.spec.ts │ └── room-booking.spec.ts ├── eslint.config.mjs ├── package.json ├── playwright.config.ts ├── tsconfig.json ├── .kube └── restful-booker-platform.yml └── README.md /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | npx lint-staged -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @m-pujic-levi9-com 2 | -------------------------------------------------------------------------------- /.lintstagedrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "*.js": ["prettier --write", "eslint"], 3 | "*.ts": ["prettier --write", "eslint"] 4 | } -------------------------------------------------------------------------------- /reports/playwright-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m-pujic-levi9-com/playwright-e2e-tests/HEAD/reports/playwright-logo.png -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Skip formatting for folders/files: 2 | test-results 3 | playwright-report 4 | playwright/.cache 5 | .history 6 | README.md -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "none", 3 | "tabWidth": 2, 4 | "printWidth": 150, 5 | "semi": true, 6 | "singleQuote": true 7 | } -------------------------------------------------------------------------------- /docs/imgs/windows-disable-hyperv.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m-pujic-levi9-com/playwright-e2e-tests/HEAD/docs/imgs/windows-disable-hyperv.png -------------------------------------------------------------------------------- /docs/imgs/windows-enable-hyperv.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m-pujic-levi9-com/playwright-e2e-tests/HEAD/docs/imgs/windows-enable-hyperv.png -------------------------------------------------------------------------------- /apis/BaseApi.ts: -------------------------------------------------------------------------------- 1 | import { APIRequestContext } from '@playwright/test'; 2 | 3 | export class BaseApi { 4 | readonly request: APIRequestContext; 5 | 6 | constructor(request: APIRequestContext) { 7 | this.request = request; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.quickSuggestions": { 3 | "comments": false, 4 | "strings": true, 5 | "other": true 6 | }, 7 | "editor.rulers": [150], 8 | "editor.detectIndentation": false, 9 | "editor.tabSize": 2, 10 | "editor.insertSpaces": true, 11 | "cSpell.words": ["combobox", "monocart", "msedge", "testid", "viewports"] 12 | } 13 | -------------------------------------------------------------------------------- /pages/BasePage.ts: -------------------------------------------------------------------------------- 1 | import { test, Page } from '@playwright/test'; 2 | 3 | export class BasePage { 4 | readonly page: Page; 5 | 6 | constructor(page: Page) { 7 | this.page = page; 8 | } 9 | 10 | async hideBanner(baseUrl: string | undefined) { 11 | await test.step('Hide Welcome to Restful Booker Platform banner', async () => { 12 | await this.page.context().addCookies([{ name: 'banner', value: 'true', url: baseUrl ? baseUrl : '/', sameSite: 'Strict' }]); 13 | }); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Playwright 2 | /test-results/ 3 | /playwright-monocart-report/ 4 | /playwright-report/ 5 | /playwright/.cache/ 6 | /playwright-blob-report/ 7 | /playwright-all-blob-reports/ 8 | /playwright-allure-results/ 9 | /playwright-allure-report/ 10 | # Compiled output 11 | /dist 12 | /node_modules 13 | 14 | # Logs 15 | logs 16 | *.log 17 | npm-debug.log* 18 | yarn-debug.log* 19 | yarn-error.log* 20 | lerna-debug.log* 21 | 22 | # OS 23 | .DS_Store 24 | 25 | # IDEs and Editors 26 | /.idea 27 | .project 28 | .classpath 29 | .c9/ 30 | *.launch 31 | .settings/ 32 | *.sublime-workspace 33 | 34 | # IDE - VSCode 35 | .vscode/* 36 | .history/* 37 | !.vscode/settings.json 38 | !.vscode/tasks.json 39 | !.vscode/extensions.json -------------------------------------------------------------------------------- /apis/AuthApi.ts: -------------------------------------------------------------------------------- 1 | import { test, expect, APIRequestContext } from '@playwright/test'; 2 | import { BaseApi } from './BaseApi'; 3 | 4 | const path = '/auth'; 5 | 6 | export class AuthApi extends BaseApi { 7 | constructor(request: APIRequestContext) { 8 | super(request); 9 | } 10 | 11 | async login(username: string, password: string) { 12 | await test.step(`Log in using API with username: ${username} and password: ${password}`, async () => { 13 | const response = await this.request.post(`${path}/login`, { 14 | data: { 15 | username: username, 16 | password: password 17 | } 18 | }); 19 | expect(response.status(), `User '${username}' is logged in`).toBe(200); 20 | }); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Get the latest version of Playwright 2 | FROM mcr.microsoft.com/playwright:v1.34.3-jammy 3 | 4 | # Set the work directory for the application 5 | WORKDIR /app 6 | 7 | # Set the environment path to node_modules/.bin 8 | ENV PATH /app/node_modules/.bin:$PATH 9 | 10 | # Update and Upgrade OS 11 | RUN apt-get update && apt-get upgrade -y 12 | 13 | # Copy Project files to the app folder in Docker image 14 | COPY package.json package-lock.json playwright.config.ts /app/ 15 | COPY apis/ /app/apis/ 16 | COPY pages/ /app/pages/ 17 | COPY utils/ /app/utils/ 18 | COPY tests/ /app/tests/ 19 | 20 | # Set CI environment variable 21 | ENV CI=1 22 | 23 | # Install dependencies 24 | RUN npm ci 25 | 26 | # Install Playwright Browsers 27 | RUN npx playwright install --with-deps -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | # For npm packages 9 | - package-ecosystem: 'npm' 10 | directory: '/' 11 | schedule: 12 | interval: 'weekly' 13 | day: 'monday' 14 | time: '08:30' 15 | timezone: 'Europe/Belgrade' 16 | # For github actions 17 | - package-ecosystem: 'github-actions' 18 | directory: '/.github/workflows' 19 | schedule: 20 | interval: 'weekly' 21 | day: 'monday' 22 | time: '09:00' 23 | timezone: 'Europe/Belgrade' -------------------------------------------------------------------------------- /merge-monocart-reports.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unused-vars */ 2 | import { merge } from 'monocart-reporter'; 3 | 4 | const reportDataList = [ 5 | process.cwd() + '/monocart-report-chromium-1_2/index.json', 6 | process.cwd() + '/monocart-report-chromium-2_2/index.json', 7 | process.cwd() + '/monocart-report-firefox-1_2/index.json', 8 | process.cwd() + '/monocart-report-firefox-2_2/index.json', 9 | process.cwd() + '/monocart-report-webkit-1_2/index.json', 10 | process.cwd() + '/monocart-report-webkit-2_2/index.json' 11 | ]; 12 | 13 | merge(reportDataList, { 14 | name: 'Playwright E2E Tests', 15 | outputFile: 'merged-monocart-report/index.html', 16 | trend: 'previous-trend.json', 17 | attachmentPath: (currentPath) => { 18 | const searchStr = '../test-results/'; 19 | const replaceStr = './data/'; 20 | 21 | if (currentPath.startsWith(searchStr)) { 22 | return replaceStr + currentPath.slice(searchStr.length); 23 | } 24 | 25 | return currentPath; 26 | } 27 | }); 28 | -------------------------------------------------------------------------------- /apis/BookingApi.ts: -------------------------------------------------------------------------------- 1 | import { test, expect, APIRequestContext } from '@playwright/test'; 2 | import { BaseApi } from './BaseApi'; 3 | 4 | const path = '/booking'; 5 | 6 | export class BookingApi extends BaseApi { 7 | constructor(request: APIRequestContext) { 8 | super(request); 9 | } 10 | 11 | async deleteBooking(bookingId: number) { 12 | await test.step(`Delete Booking with id: ${bookingId}`, async () => { 13 | const response = await this.request.delete(`${path}/${bookingId}`); 14 | expect([202, 404], `Booking with id: ${bookingId} is deleted`).toContain(response.status()); 15 | }); 16 | } 17 | 18 | async deleteAllBookings(roomId: number) { 19 | await test.step(`Delete all Bookings for room id: ${roomId}`, async () => { 20 | const response = await this.request.get(`${path}/?roomid=${roomId}`); 21 | expect(response.status(), `All Bookings for room id: ${roomId} are fetched`).toBe(200); 22 | const data = JSON.parse(await response.text()); 23 | for (const booking of data.bookings) await this.deleteBooking(booking.bookingid); 24 | }); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /utils/test-data-util.ts: -------------------------------------------------------------------------------- 1 | import { RoomType } from '../pages/RoomsPage'; 2 | 3 | export function invalidEmails() { 4 | return [ 5 | 'plainaddress', 6 | 'email@example', 7 | '#@%^%#$@#$@#.com', 8 | '@example.com Joe Smith', 9 | '', 10 | 'email.example.com', 11 | 'email@example@example.com', 12 | '.email@example.com', 13 | 'email..email@example.com', 14 | 'email@example.com (Joe Smith)', 15 | 'email@-example.com', 16 | 'email@example..com', 17 | 'Abc..123@example.com' 18 | ]; 19 | } 20 | 21 | export function getImageUrl(roomType: RoomType) { 22 | if (roomType == RoomType.SINGLE) return 'https://images.pexels.com/photos/271618/pexels-photo-271618.jpeg'; 23 | else if (roomType == RoomType.TWIN) return 'https://images.pexels.com/photos/14021932/pexels-photo-14021932.jpeg'; 24 | else if (roomType == RoomType.DOUBLE) return 'https://images.pexels.com/photos/11857305/pexels-photo-11857305.jpeg'; 25 | else if (roomType == RoomType.FAMILY) return 'https://images.pexels.com/photos/237371/pexels-photo-237371.jpeg'; 26 | else return 'https://images.pexels.com/photos/6585757/pexels-photo-6585757.jpeg'; 27 | } 28 | -------------------------------------------------------------------------------- /pages/AdminPage.ts: -------------------------------------------------------------------------------- 1 | import { test, expect, Page, Locator } from '@playwright/test'; 2 | import { BasePage } from './BasePage'; 3 | 4 | export class AdminPage extends BasePage { 5 | readonly pageHeader: Locator; 6 | readonly usernameField: Locator; 7 | readonly passwordField: Locator; 8 | readonly loginButton: Locator; 9 | 10 | constructor(page: Page) { 11 | super(page); 12 | this.pageHeader = page.getByTestId('login-header'); 13 | this.usernameField = page.getByTestId('username'); 14 | this.passwordField = page.getByTestId('password'); 15 | this.loginButton = page.getByTestId('submit'); 16 | } 17 | 18 | async goto() { 19 | await test.step('Go to Admin Page', async () => { 20 | await this.page.goto('/#/admin'); 21 | await expect(this.pageHeader, 'Admin page loaded').toBeVisible(); 22 | }); 23 | } 24 | 25 | async login(username: string, password: string) { 26 | await test.step(`Log in using with username: ${username} and password: ${password}`, async () => { 27 | await this.usernameField.fill(username); 28 | await this.passwordField.fill(password); 29 | await this.loginButton.click(); 30 | }); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /docker-compose-restful-booker.yml: -------------------------------------------------------------------------------- 1 | version: '3.7' 2 | services: 3 | # Restful Booker Platform Docker Compose from https://github.com/mwinteringham/restful-booker-platform repository 4 | rbp-booking: 5 | image: mwinteringham/restfulbookerplatform_booking:1.6.24c7b22 6 | ports: 7 | - 3000:3000 8 | restart: always 9 | 10 | rbp-room: 11 | image: mwinteringham/restfulbookerplatform_room:1.6.24c7b22 12 | ports: 13 | - 3001:3001 14 | restart: always 15 | 16 | rbp-branding: 17 | image: mwinteringham/restfulbookerplatform_branding:1.6.24c7b22 18 | ports: 19 | - 3002:3002 20 | restart: always 21 | 22 | rbp-assets: 23 | image: mwinteringham/restfulbookerplatform_assets:1.6.24c7b22 24 | ports: 25 | - 3003:3003 26 | restart: always 27 | 28 | rbp-auth: 29 | image: mwinteringham/restfulbookerplatform_auth:1.6.24c7b22 30 | ports: 31 | - 3004:3004 32 | restart: always 33 | 34 | rbp-report: 35 | image: mwinteringham/restfulbookerplatform_report:1.6.24c7b22 36 | ports: 37 | - 3005:3005 38 | restart: always 39 | 40 | rbp-message: 41 | image: mwinteringham/restfulbookerplatform_message:1.6.24c7b22 42 | ports: 43 | - 3006:3006 44 | restart: always 45 | 46 | rbp-proxy: 47 | image: mwinteringham/restfulbookerplatform_proxy:latest 48 | ports: 49 | - 80:80 50 | restart: always -------------------------------------------------------------------------------- /.github/workflows/dependabot-auto-merge.yml: -------------------------------------------------------------------------------- 1 | name: Dependabot Pull Request Approve and Merge 2 | 3 | on: pull_request_target 4 | 5 | permissions: 6 | pull-requests: write 7 | contents: write 8 | 9 | jobs: 10 | dependabot: 11 | runs-on: ubuntu-latest 12 | # Checking the actor will prevent your Action run failing on non-Dependabot 13 | # PRs but also ensures that it only does work for Dependabot PRs. 14 | if: ${{ github.actor == 'dependabot[bot]' }} 15 | steps: 16 | # This first step will fail if there's no metadata and so the approval 17 | # will not occur. 18 | - name: Dependabot Metadata 19 | id: dependabot-metadata 20 | uses: dependabot/fetch-metadata@v2.4.0 21 | with: 22 | github-token: "${{ secrets.GITHUB_TOKEN }}" 23 | # Here the PR gets approved. 24 | - name: Approve a PR 25 | run: gh pr review --approve "$PR_URL" 26 | env: 27 | PR_URL: ${{ github.event.pull_request.html_url }} 28 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 29 | # Finally, this sets the PR to allow auto-merging for patch and minor 30 | # updates if all checks pass 31 | - name: Enable Auto-Merge for Dependabot PRs 32 | if: ${{ steps.dependabot-metadata.outputs.update-type != 'version-update:semver-major' }} 33 | run: gh pr merge --auto --squash "$PR_URL" 34 | env: 35 | PR_URL: ${{ github.event.pull_request.html_url }} 36 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /pages/components/Header.ts: -------------------------------------------------------------------------------- 1 | import { Page, Locator } from '@playwright/test'; 2 | import { BasePage } from '../BasePage'; 3 | 4 | export class Header extends BasePage { 5 | readonly roomsLink: Locator; 6 | readonly reportLink: Locator; 7 | readonly brandingLink: Locator; 8 | readonly messagesLink: Locator; 9 | readonly unreadMessagesNumber: Locator; 10 | readonly frontPageLink: Locator; 11 | readonly logoutLink: Locator; 12 | 13 | constructor(page: Page) { 14 | super(page); 15 | this.roomsLink = page.getByRole('link', { name: 'Rooms' }); 16 | this.reportLink = page.getByRole('link', { name: 'Report' }); 17 | this.brandingLink = page.getByRole('link', { name: 'Branding' }); 18 | this.messagesLink = page.locator('[href*="#/admin/messages"]'); 19 | this.unreadMessagesNumber = page.locator('a[href*="#/admin/messages"] .notification'); 20 | this.frontPageLink = page.getByRole('link', { name: 'Front Page' }); 21 | this.logoutLink = page.getByRole('link', { name: 'Logout' }); 22 | } 23 | 24 | async clickOnRooms() { 25 | await this.roomsLink.click(); 26 | } 27 | 28 | async clickOnReport() { 29 | await this.reportLink.click(); 30 | } 31 | 32 | async clickOnBranding() { 33 | await this.brandingLink.click(); 34 | } 35 | 36 | async clickOnMessages() { 37 | await this.messagesLink.click(); 38 | } 39 | 40 | async getUnreadMessagesCount() { 41 | return await this.unreadMessagesNumber.textContent(); 42 | } 43 | 44 | async clickOnFrontPage() { 45 | await this.frontPageLink.click(); 46 | } 47 | 48 | async clickOnLogout() { 49 | await this.logoutLink.click(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /tests/admin-panel/login.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@playwright/test'; 2 | import { AdminPage } from '../../pages/AdminPage'; 3 | import { Header } from '../../pages/components/Header'; 4 | 5 | test.describe('Login Tests', () => { 6 | let adminPage: AdminPage; 7 | let header: Header; 8 | 9 | const style = 'style'; 10 | const redBorder = 'border: 1px solid red;'; 11 | 12 | test.beforeEach(async ({ page, baseURL }) => { 13 | adminPage = new AdminPage(page); 14 | header = new Header(page); 15 | await adminPage.hideBanner(baseURL); 16 | await adminPage.goto(); 17 | }); 18 | 19 | test('Administrator is able to login with correct username and password @sanity @login', async () => { 20 | await adminPage.login('admin', 'password'); 21 | await expect(header.logoutLink, 'Administrator logged in!').toBeVisible(); 22 | }); 23 | 24 | test('User is not able to login with empty username @login', async () => { 25 | await adminPage.login('', 'password'); 26 | await expect(header.logoutLink, 'User is not logged in').toBeHidden(); 27 | await expect(adminPage.usernameField, 'Username field has red border!').toHaveAttribute(style, redBorder); 28 | }); 29 | 30 | test('User is not able to login with empty password @login', async () => { 31 | await adminPage.login('admin', ''); 32 | await expect(header.logoutLink, 'User is not logged in').toBeHidden(); 33 | await expect(adminPage.passwordField, 'Password field has red border!').toHaveAttribute(style, redBorder); 34 | }); 35 | 36 | test('User is not able to login with wrong password @login', async () => { 37 | await adminPage.login('admin', 'wrong_password'); 38 | await expect(header.logoutLink, 'User is not logged in').toBeHidden(); 39 | await expect(adminPage.usernameField, 'Username field has red border!').toHaveAttribute(style, redBorder); 40 | await expect(adminPage.passwordField, 'Password field has red border!').toHaveAttribute(style, redBorder); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import typescriptEslint from '@typescript-eslint/eslint-plugin'; 2 | import prettier from 'eslint-plugin-prettier'; 3 | import globals from 'globals'; 4 | import tsParser from '@typescript-eslint/parser'; 5 | import path from 'node:path'; 6 | import { fileURLToPath } from 'node:url'; 7 | import js from '@eslint/js'; 8 | import { FlatCompat } from '@eslint/eslintrc'; 9 | 10 | const __filename = fileURLToPath(import.meta.url); 11 | const __dirname = path.dirname(__filename); 12 | const compat = new FlatCompat({ 13 | baseDirectory: __dirname, 14 | recommendedConfig: js.configs.recommended, 15 | allConfig: js.configs.all 16 | }); 17 | 18 | export default [ 19 | { 20 | ignores: ['**/*.json', '**/pnpm-lock.yaml', '**/*.md', '**/.eslintignore', '**/test-results', '**/playwright-report', 'playwright/.cache'] 21 | }, 22 | ...compat.extends('eslint:recommended', 'plugin:@typescript-eslint/recommended', 'plugin:playwright/playwright-test'), 23 | { 24 | plugins: { 25 | '@typescript-eslint': typescriptEslint, 26 | prettier 27 | }, 28 | 29 | languageOptions: { 30 | globals: { 31 | ...globals.browser 32 | }, 33 | 34 | parser: tsParser, 35 | ecmaVersion: 'latest', 36 | sourceType: 'script' 37 | }, 38 | 39 | rules: { 40 | 'no-unused-expressions': 'error', 41 | 'sort-keys': 'off', 42 | 43 | indent: [ 44 | 'error', 45 | 2, 46 | { 47 | SwitchCase: 1 48 | } 49 | ], 50 | 51 | 'no-duplicate-imports': [ 52 | 'error', 53 | { 54 | includeExports: true 55 | } 56 | ], 57 | 58 | 'comma-dangle': ['error', 'never'], 59 | quotes: ['error', 'single'], 60 | semi: ['error', 'always'], 61 | '@typescript-eslint/no-unused-vars': 'error', 62 | 63 | 'max-len': [ 64 | 'warn', 65 | { 66 | code: 150, 67 | tabWidth: 2 68 | } 69 | ], 70 | 71 | 'prettier/prettier': ['error'] 72 | } 73 | } 74 | ]; 75 | -------------------------------------------------------------------------------- /reports/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Playwright E2E Test Reports 7 | 66 | 67 | 68 | 71 | 72 |
73 | 78 |
79 | 80 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "playwright-e2e-tests", 3 | "version": "1.0.0", 4 | "description": "Playwright E2E Test Framework", 5 | "main": "index.js", 6 | "scripts": { 7 | "prepare": "husky", 8 | "clean": "npm run clean:results && npm run clean:modules", 9 | "clean:modules": "rimraf ./node_modules", 10 | "clean:results": "rimraf ./playwright-report ./playwright-blob-report ./playwright-monocart-report ./playwright-allure-results ./playwright-allure-reports ./test-results", 11 | "types-check": "tsc -p tsconfig.json --noEmit", 12 | "types-check:watch": "tsc -p tsconfig.json --noEmit -w", 13 | "pw:install": "playwright install", 14 | "pw:ui": "playwright test --ui", 15 | "test:sanity": "playwright test --grep @sanity", 16 | "test:changed": "playwright test --only-changed=main", 17 | "test:failed": "playwright test --last-failed", 18 | "docker:build": "docker build . -t e2e-playwright", 19 | "allure:generate": "allure generate playwright-allure-results -o playwright-allure-report --clean", 20 | "allure:open": "allure open playwright-allure-report", 21 | "merge:report:playwright": "tsx merge-playwright-reports.ts", 22 | "merge:report:monocart": "tsx merge-monocart-reports.ts", 23 | "update:playwright": "playwright install --with-deps" 24 | }, 25 | "keywords": [], 26 | "author": "", 27 | "license": "ISC", 28 | "devDependencies": { 29 | "@eslint/eslintrc": "^3.3.3", 30 | "@eslint/js": "^9.39.1", 31 | "@faker-js/faker": "^9.9.0", 32 | "@playwright/test": "^1.57.0", 33 | "@types/node": "^24.10.1", 34 | "@typescript-eslint/eslint-plugin": "^8.49.0", 35 | "@typescript-eslint/parser": "^8.49.0", 36 | "allure-commandline": "^2.34.1", 37 | "allure-playwright": "^3.4.3", 38 | "eslint": "^9.39.2", 39 | "eslint-config-prettier": "^10.1.8", 40 | "eslint-plugin-playwright": "^2.4.0", 41 | "eslint-plugin-prettier": "^5.5.4", 42 | "eslint-plugin-promise": "^7.2.1", 43 | "globals": "^16.5.0", 44 | "husky": "^9.1.7", 45 | "lint-staged": "^16.2.7", 46 | "monocart-reporter": "^2.9.23", 47 | "prettier": "^3.7.4", 48 | "rimraf": "^6.1.2", 49 | "tsx": "^4.21.0", 50 | "typescript": "^5.9.3" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /apis/RoomApi.ts: -------------------------------------------------------------------------------- 1 | import { test, expect, APIRequestContext } from '@playwright/test'; 2 | import { BaseApi } from './BaseApi'; 3 | import { BookingApi } from './BookingApi'; 4 | import { RoomType, RoomAmenities, getAmenitiesAsList } from '../pages/RoomsPage'; 5 | import { getImageUrl } from '../utils/test-data-util'; 6 | 7 | const path = '/room'; 8 | 9 | export class RoomApi extends BaseApi { 10 | readonly bookingApi: BookingApi; 11 | 12 | constructor(request: APIRequestContext) { 13 | super(request); 14 | this.bookingApi = new BookingApi(request); 15 | } 16 | 17 | async createRoom(roomName: string, roomType: RoomType, roomIsAccessible: boolean, roomPrice: number, roomAmenities: RoomAmenities) { 18 | await this.deleteAllRooms(roomName); 19 | await test.step(`Create ${roomType} Room with name '${roomName}'`, async () => { 20 | const response = await this.request.post(`${path}/`, { 21 | data: { 22 | roomName: roomName, 23 | type: roomType, 24 | accessible: roomIsAccessible.toString(), 25 | roomPrice: roomPrice.toString(), 26 | features: getAmenitiesAsList(roomAmenities), 27 | image: getImageUrl(roomType), 28 | description: 'Room Created with Automated Test' 29 | } 30 | }); 31 | expect(response.status(), `${roomType} Room with name '${roomName}' is created`).toBe(201); 32 | }); 33 | } 34 | 35 | async deleteRoom(roomId: number) { 36 | await this.bookingApi.deleteAllBookings(roomId); 37 | await test.step(`Delete room with id: ${roomId}`, async () => { 38 | const response = await this.request.delete(`${path}/${roomId}`); 39 | expect([202, 404], `Room with id: ${roomId} is deleted`).toContain(response.status()); 40 | }); 41 | } 42 | 43 | async deleteAllRooms(roomName: string) { 44 | await test.step(`Delete all rooms with name: '${roomName}'`, async () => { 45 | const getRoomsResponse = await this.request.get(`${path}/`); 46 | expect(getRoomsResponse.status(), 'All rooms are fetched').toBe(200); 47 | const getRoomsData = JSON.parse(await getRoomsResponse.text()); 48 | const allRooms = getRoomsData.rooms; 49 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 50 | const filteredRoomsByName = allRooms.filter((room: any) => room.roomName == roomName); 51 | for (const room of filteredRoomsByName) await this.deleteRoom(room.roomid); 52 | }); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /pages/RoomsPage.ts: -------------------------------------------------------------------------------- 1 | import { test, Page, Locator } from '@playwright/test'; 2 | import { BasePage } from './BasePage'; 3 | 4 | export class RoomsPage extends BasePage { 5 | readonly roomNameField: Locator; 6 | readonly roomTypeSelect: Locator; 7 | readonly roomAccessibleSelect: Locator; 8 | readonly roomPriceField: Locator; 9 | readonly wifiCheckbox: Locator; 10 | readonly tvCheckbox: Locator; 11 | readonly radioCheckbox: Locator; 12 | readonly refreshmentsCheckbox: Locator; 13 | readonly safeCheckbox: Locator; 14 | readonly viewsCheckbox: Locator; 15 | readonly createRoomButton: Locator; 16 | readonly errorMessages: Locator; 17 | 18 | constructor(page: Page) { 19 | super(page); 20 | this.roomNameField = page.getByTestId('roomName'); 21 | this.roomTypeSelect = page.locator('#type'); 22 | this.roomAccessibleSelect = page.locator('#accessible'); 23 | this.roomPriceField = page.locator('#roomPrice'); 24 | this.wifiCheckbox = page.getByLabel('WiFi'); 25 | this.tvCheckbox = page.getByLabel('TV'); 26 | this.radioCheckbox = page.getByLabel('Radio'); 27 | this.refreshmentsCheckbox = page.getByLabel('Refreshments'); 28 | this.safeCheckbox = page.getByLabel('Safe'); 29 | this.viewsCheckbox = page.getByLabel('Views'); 30 | this.createRoomButton = page.getByRole('button', { name: 'Create' }); 31 | this.errorMessages = page.locator('.alert.alert-danger'); 32 | } 33 | 34 | async goto() { 35 | await test.step('Go to Rooms Page', async () => { 36 | await this.page.goto('/#/admin'); 37 | }); 38 | } 39 | 40 | async selectRoomType(type: RoomType | null) { 41 | if (type != null) await this.roomTypeSelect.selectOption(type); 42 | } 43 | 44 | async enterPrice(price: number | null) { 45 | if (price != null) await this.roomPriceField.fill(price.toString()); 46 | } 47 | 48 | async selectAmenities(amenities: RoomAmenities) { 49 | if (amenities.wifi) await this.wifiCheckbox.check(); 50 | else await this.wifiCheckbox.uncheck(); 51 | if (amenities.tv) await this.tvCheckbox.check(); 52 | else await this.tvCheckbox.uncheck(); 53 | if (amenities.radio) await this.radioCheckbox.check(); 54 | else await this.radioCheckbox.uncheck(); 55 | if (amenities.refreshments) await this.refreshmentsCheckbox.check(); 56 | else await this.refreshmentsCheckbox.uncheck(); 57 | if (amenities.safe) await this.safeCheckbox.check(); 58 | else await this.safeCheckbox.uncheck(); 59 | if (amenities.views) await this.viewsCheckbox.check(); 60 | else await this.viewsCheckbox.uncheck(); 61 | } 62 | 63 | async createRoom(roomName: string, roomType: RoomType | null, roomIsAccessible: boolean, roomPrice: number | null, roomAmenities: RoomAmenities) { 64 | await test.step(`Create ${roomType} Room with name '${roomName}'`, async () => { 65 | await this.roomNameField.fill(roomName); 66 | await this.selectRoomType(roomType); 67 | await this.roomAccessibleSelect.selectOption(roomIsAccessible ? 'true' : 'false'); 68 | await this.enterPrice(roomPrice); 69 | await this.selectAmenities(roomAmenities); 70 | await this.createRoomButton.click(); 71 | }); 72 | } 73 | } 74 | 75 | export enum RoomType { 76 | SINGLE = 'Single', 77 | TWIN = 'Twin', 78 | DOUBLE = 'Double', 79 | FAMILY = 'Family', 80 | SUITE = 'Suite' 81 | } 82 | 83 | export type RoomAmenities = { 84 | wifi: boolean; 85 | tv: boolean; 86 | radio: boolean; 87 | refreshments: boolean; 88 | safe: boolean; 89 | views: boolean; 90 | }; 91 | 92 | export function getAmenitiesAsList(roomAmenities: RoomAmenities) { 93 | const amenities: string[] = []; 94 | if (roomAmenities.wifi) amenities.push('WiFi'); 95 | if (roomAmenities.tv) amenities.push('TV'); 96 | if (roomAmenities.radio) amenities.push('Radio'); 97 | if (roomAmenities.refreshments) amenities.push('Refreshments'); 98 | if (roomAmenities.safe) amenities.push('Safe'); 99 | if (roomAmenities.views) amenities.push('Views'); 100 | return amenities; 101 | } 102 | 103 | export function getRoomDetailsFromAmenities(roomAmenities: RoomAmenities) { 104 | const amenities: string[] = getAmenitiesAsList(roomAmenities); 105 | return amenities.length == 0 ? 'No features added to the room' : amenities.join(', '); 106 | } 107 | -------------------------------------------------------------------------------- /.github/workflows/check-sanity.yml: -------------------------------------------------------------------------------- 1 | name: Check - Sanity Tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - 'apis/**' 9 | - 'pages/**' 10 | - 'tests/**' 11 | - 'utils/**' 12 | - '.github/workflows/sanity-check.yml' 13 | - 'playwright.config.ts' 14 | - 'package-lock.json' 15 | - 'package.json' 16 | pull_request: 17 | branches: 18 | - main 19 | types: 20 | - opened 21 | - reopened 22 | - synchronize 23 | - labeled 24 | 25 | jobs: 26 | install: 27 | name: Install 28 | runs-on: ubuntu-latest 29 | timeout-minutes: 30 30 | steps: 31 | - name: Checkout Repository 32 | id: checkout-repository 33 | uses: actions/checkout@v4 34 | - name: Setup Node 35 | id: setup-node 36 | uses: actions/setup-node@v4 37 | with: 38 | node-version: 20 39 | - name: Cache Node Modules 40 | id: cache-node-modules 41 | uses: actions/cache@v4 42 | with: 43 | path: | 44 | node_modules 45 | key: modules-${{ hashFiles('package-lock.json') }} 46 | - name: Cache Playwright Binaries 47 | id: cache-playwright 48 | uses: actions/cache@v4 49 | with: 50 | path: | 51 | ~/.cache/ms-playwright 52 | key: playwright-${{ hashFiles('package-lock.json') }} 53 | - name: Install dependencies 54 | id: install-dependencies 55 | if: steps.cache-node-modules.outputs.cache-hit != 'true' 56 | run: npm ci 57 | - name: Install Playwright Browsers 58 | id: install-playwright-browsers 59 | if: steps.cache-playwright.outputs.cache-hit != 'true' 60 | run: npx playwright install --with-deps 61 | - name: Run Type Checks 62 | id: run-type-checks 63 | run: npx tsc -p tsconfig.json --noEmit 64 | 65 | test: 66 | name: Sanity Tests 67 | runs-on: ubuntu-latest 68 | needs: [install] 69 | timeout-minutes: 60 70 | services: 71 | rbp-booking: 72 | image: mwinteringham/restfulbookerplatform_booking:1.6.24c7b22 73 | ports: 74 | - 3000:3000 75 | rbp-room: 76 | image: mwinteringham/restfulbookerplatform_room:1.6.24c7b22 77 | ports: 78 | - 3001:3001 79 | rbp-branding: 80 | image: mwinteringham/restfulbookerplatform_branding:1.6.24c7b22 81 | ports: 82 | - 3002:3002 83 | rbp-assets: 84 | image: mwinteringham/restfulbookerplatform_assets:1.6.24c7b22 85 | ports: 86 | - 3003:3003 87 | rbp-auth: 88 | image: mwinteringham/restfulbookerplatform_auth:1.6.24c7b22 89 | ports: 90 | - 3004:3004 91 | rbp-report: 92 | image: mwinteringham/restfulbookerplatform_report:1.6.24c7b22 93 | ports: 94 | - 3005:3005 95 | rbp-message: 96 | image: mwinteringham/restfulbookerplatform_message:1.6.24c7b22 97 | ports: 98 | - 3006:3006 99 | rbp-proxy: 100 | image: mwinteringham/restfulbookerplatform_proxy:latest 101 | ports: 102 | - 80:80 103 | steps: 104 | - name: Checkout Repository 105 | id: checkout-repository 106 | uses: actions/checkout@v4 107 | - name: Setup Node 108 | id: setup-node 109 | uses: actions/setup-node@v4 110 | with: 111 | node-version: 20 112 | - name: Cache Node Modules 113 | id: cache-node-modules 114 | uses: actions/cache@v4 115 | with: 116 | path: | 117 | node_modules 118 | key: modules-${{ hashFiles('package-lock.json') }} 119 | - name: Cache Playwright Binaries 120 | id: cache-playwright 121 | uses: actions/cache@v4 122 | with: 123 | path: | 124 | ~/.cache/ms-playwright 125 | key: playwright-${{ hashFiles('package-lock.json') }} 126 | # Playwright caches the browser binaries, but not their dependencies. 127 | # Those extra browser dependencies must be installed separately when the cached browsers are restored. 128 | - name: Install Playwright System Dependencies 129 | id: install-playwright-system-dependencies 130 | run: npx playwright install-deps 131 | - name: Run Playwright Tests 132 | id: run-playwright-tests 133 | run: npx playwright test --grep @sanity 134 | env: 135 | ENV: local 136 | - name: Upload Playwright HTML Report 137 | id: upload-playwright-html-report 138 | uses: actions/upload-artifact@v5 139 | if: always() 140 | with: 141 | name: playwright-report 142 | path: playwright-report/ 143 | if-no-files-found: ignore 144 | retention-days: 1 145 | -------------------------------------------------------------------------------- /.github/workflows/check-changes.yml: -------------------------------------------------------------------------------- 1 | name: Check - Changed Tests 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | types: 8 | - opened 9 | - reopened 10 | - synchronize 11 | - labeled 12 | 13 | jobs: 14 | install: 15 | name: Install 16 | runs-on: ubuntu-latest 17 | timeout-minutes: 30 18 | steps: 19 | - name: Checkout Repository 20 | id: checkout-repository 21 | uses: actions/checkout@v4 22 | with: 23 | # Force a non-shallow checkout, so that we can reference $GITHUB_BASE_REF (needed for the --only-changed option to work) 24 | # See https://github.com/actions/checkout for more details. 25 | fetch-depth: 0 26 | - name: Setup Node 27 | id: setup-node 28 | uses: actions/setup-node@v4 29 | with: 30 | node-version: 20 31 | - name: Cache Node Modules 32 | id: cache-node-modules 33 | uses: actions/cache@v4 34 | with: 35 | path: | 36 | node_modules 37 | key: modules-${{ hashFiles('package-lock.json') }} 38 | - name: Cache Playwright Binaries 39 | id: cache-playwright 40 | uses: actions/cache@v4 41 | with: 42 | path: | 43 | ~/.cache/ms-playwright 44 | key: playwright-${{ hashFiles('package-lock.json') }} 45 | - name: Install dependencies 46 | id: install-dependencies 47 | if: steps.cache-node-modules.outputs.cache-hit != 'true' 48 | run: npm ci 49 | - name: Install Playwright Browsers 50 | id: install-playwright-browsers 51 | if: steps.cache-playwright.outputs.cache-hit != 'true' 52 | run: npx playwright install --with-deps 53 | - name: Run Type Checks 54 | id: run-type-checks 55 | run: npx tsc -p tsconfig.json --noEmit 56 | 57 | test: 58 | name: Changed Tests 59 | runs-on: ubuntu-latest 60 | needs: [install] 61 | timeout-minutes: 60 62 | services: 63 | rbp-booking: 64 | image: mwinteringham/restfulbookerplatform_booking:1.6.24c7b22 65 | ports: 66 | - 3000:3000 67 | rbp-room: 68 | image: mwinteringham/restfulbookerplatform_room:1.6.24c7b22 69 | ports: 70 | - 3001:3001 71 | rbp-branding: 72 | image: mwinteringham/restfulbookerplatform_branding:1.6.24c7b22 73 | ports: 74 | - 3002:3002 75 | rbp-assets: 76 | image: mwinteringham/restfulbookerplatform_assets:1.6.24c7b22 77 | ports: 78 | - 3003:3003 79 | rbp-auth: 80 | image: mwinteringham/restfulbookerplatform_auth:1.6.24c7b22 81 | ports: 82 | - 3004:3004 83 | rbp-report: 84 | image: mwinteringham/restfulbookerplatform_report:1.6.24c7b22 85 | ports: 86 | - 3005:3005 87 | rbp-message: 88 | image: mwinteringham/restfulbookerplatform_message:1.6.24c7b22 89 | ports: 90 | - 3006:3006 91 | rbp-proxy: 92 | image: mwinteringham/restfulbookerplatform_proxy:latest 93 | ports: 94 | - 80:80 95 | steps: 96 | - name: Checkout Repository 97 | id: checkout-repository 98 | uses: actions/checkout@v4 99 | with: 100 | # Force a non-shallow checkout, so that we can reference $GITHUB_BASE_REF (needed for the --only-changed option to work) 101 | # See https://github.com/actions/checkout for more details. 102 | fetch-depth: 0 103 | - name: Setup Node 104 | id: setup-node 105 | uses: actions/setup-node@v4 106 | with: 107 | node-version: 20 108 | - name: Cache Node Modules 109 | id: cache-node-modules 110 | uses: actions/cache@v4 111 | with: 112 | path: | 113 | node_modules 114 | key: modules-${{ hashFiles('package-lock.json') }} 115 | - name: Cache Playwright Binaries 116 | id: cache-playwright 117 | uses: actions/cache@v4 118 | with: 119 | path: | 120 | ~/.cache/ms-playwright 121 | key: playwright-${{ hashFiles('package-lock.json') }} 122 | # Playwright caches the browser binaries, but not their dependencies. 123 | # Those extra browser dependencies must be installed separately when the cached browsers are restored. 124 | - name: Install Playwright System Dependencies 125 | id: install-playwright-system-dependencies 126 | run: npx playwright install-deps 127 | - name: Run Playwright Tests 128 | id: run-playwright-tests 129 | run: npx playwright test --only-changed=origin/$GITHUB_BASE_REF 130 | env: 131 | ENV: local 132 | - name: Upload Playwright HTML Report 133 | id: upload-playwright-html-report 134 | uses: actions/upload-artifact@v5 135 | if: always() 136 | with: 137 | name: playwright-report 138 | path: playwright-report/ 139 | if-no-files-found: ignore 140 | retention-days: 1 141 | -------------------------------------------------------------------------------- /pages/FrontPage.ts: -------------------------------------------------------------------------------- 1 | import { test, expect, Page, Locator } from '@playwright/test'; 2 | import { BasePage } from './BasePage'; 3 | 4 | export class FrontPage extends BasePage { 5 | readonly pageLocator: Locator; 6 | 7 | readonly bookingFirstNameField: Locator; 8 | readonly bookingLastNameField: Locator; 9 | readonly bookingEmailField: Locator; 10 | readonly bookingPhoneNumberField: Locator; 11 | readonly bookingBookButton: Locator; 12 | readonly bookingCalendarNextButton: Locator; 13 | readonly bookingConfirmationModal: Locator; 14 | readonly bookingErrorMessages: Locator; 15 | 16 | readonly contactNameField: Locator; 17 | readonly contactEmailField: Locator; 18 | readonly contactPhoneField: Locator; 19 | readonly contactSubjectField: Locator; 20 | readonly contactDescriptionField: Locator; 21 | readonly contactSubmitButton: Locator; 22 | readonly contactSuccessMessage: Locator; 23 | readonly contactErrorMessages: Locator; 24 | 25 | constructor(page: Page) { 26 | super(page); 27 | this.pageLocator = page.locator('.hotel-description'); 28 | 29 | this.bookingFirstNameField = page.locator('input.room-firstname').last(); 30 | this.bookingLastNameField = page.locator('input.room-lastname').last(); 31 | this.bookingEmailField = page.locator('input.room-email').last(); 32 | this.bookingPhoneNumberField = page.locator('input.room-phone').last(); 33 | this.bookingBookButton = page.getByRole('button', { name: 'Book', exact: true }).last(); 34 | this.bookingCalendarNextButton = page.getByRole('button', { name: 'Next' }).last(); 35 | this.bookingConfirmationModal = page.locator('.confirmation-modal'); 36 | this.bookingErrorMessages = page.locator('div.hotel-room-info .alert.alert-danger').last(); 37 | 38 | this.contactNameField = page.getByTestId('ContactName'); 39 | this.contactEmailField = page.getByTestId('ContactEmail'); 40 | this.contactPhoneField = page.getByTestId('ContactPhone'); 41 | this.contactSubjectField = page.getByTestId('ContactSubject'); 42 | this.contactDescriptionField = page.getByTestId('ContactDescription'); 43 | this.contactSubmitButton = page.getByRole('button', { name: 'Submit' }); 44 | this.contactSuccessMessage = page.locator('div.contact h2'); 45 | this.contactErrorMessages = page.locator('div.contact .alert.alert-danger'); 46 | } 47 | 48 | async goto() { 49 | await test.step('Go to Front Page', async () => { 50 | await this.page.goto('/'); 51 | await expect(this.pageLocator, 'Front Page loaded').toBeVisible(); 52 | }); 53 | } 54 | 55 | async sendMessage(name: string, email: string, phone: string, subject: string, description: string) { 56 | await test.step('Submit Message to Hotel', async () => { 57 | await this.contactNameField.fill(name); 58 | await this.contactEmailField.fill(email); 59 | await this.contactPhoneField.fill(phone); 60 | await this.contactSubjectField.fill(subject); 61 | await this.contactDescriptionField.fill(description); 62 | await this.contactSubmitButton.click(); 63 | }); 64 | } 65 | 66 | async clickBookThsRoomButton(roomName: string) { 67 | await test.step(`Click on Book this room button for Room named '${roomName}'`, async () => { 68 | await this.page.locator(`//div[./div/img[contains(@alt,'${roomName}')]]//button`).last().click(); 69 | }); 70 | } 71 | 72 | async fillBookingFields(firstName: string, lastName: string, email: string, phoneNumber: string) { 73 | await test.step('Fill in booking information', async () => { 74 | await this.bookingFirstNameField.fill(firstName); 75 | await this.bookingLastNameField.fill(lastName); 76 | await this.bookingEmailField.fill(email); 77 | await this.bookingPhoneNumberField.fill(phoneNumber); 78 | }); 79 | } 80 | 81 | async selectBookingDates() { 82 | await test.step('Select Booking dates', async () => { 83 | await this.bookingCalendarNextButton.click(); 84 | const bookingCalendarStart = this.page.locator('.rbc-day-bg:not(.rbc-off-range-bg)').first(); 85 | const bookingCalendarEnd = this.page.locator('.rbc-day-bg:not(.rbc-off-range-bg)').last(); 86 | await bookingCalendarStart.hover(); 87 | await this.page.mouse.down(); 88 | await bookingCalendarEnd.hover(); 89 | await this.page.mouse.up(); 90 | }); 91 | } 92 | 93 | async clickOnBookButton() { 94 | await test.step('Click on Book button', async () => { 95 | await this.bookingBookButton.click(); 96 | }); 97 | } 98 | 99 | async bookRoom(roomName: string, firstName: string, lastName: string, email: string, phoneNumber: string) { 100 | await test.step(`Book a Room '${roomName}'`, async () => { 101 | await this.clickBookThsRoomButton(roomName); 102 | await this.fillBookingFields(firstName, lastName, email, phoneNumber); 103 | await this.selectBookingDates(); 104 | await this.clickOnBookButton(); 105 | }); 106 | } 107 | 108 | async bookRoomWithoutDates(roomName: string, firstName: string, lastName: string, email: string, phoneNumber: string) { 109 | await test.step(`Book a Room '${roomName}' without selecting booking dates`, async () => { 110 | await this.clickBookThsRoomButton(roomName); 111 | await this.fillBookingFields(firstName, lastName, email, phoneNumber); 112 | await this.clickOnBookButton(); 113 | }); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /tests/admin-panel/room-management.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@playwright/test'; 2 | import { RoomsPage, RoomAmenities, getRoomDetailsFromAmenities, RoomType } from '../../pages/RoomsPage'; 3 | import { AuthApi } from '../../apis/AuthApi'; 4 | import { AdminPage } from '../../pages/AdminPage'; 5 | import { Header } from '../../pages/components/Header'; 6 | import { RoomApi } from '../../apis/RoomApi'; 7 | 8 | test.describe('Room Management Tests', () => { 9 | let adminPage: AdminPage; 10 | let header: Header; 11 | let roomsPage: RoomsPage; 12 | 13 | let authApi: AuthApi; 14 | let roomApi: RoomApi; 15 | 16 | test.beforeEach(async ({ page, request, baseURL }) => { 17 | adminPage = new AdminPage(page); 18 | header = new Header(page); 19 | roomsPage = new RoomsPage(page); 20 | 21 | authApi = new AuthApi(request); 22 | roomApi = new RoomApi(request); 23 | 24 | await adminPage.hideBanner(baseURL); 25 | await adminPage.goto(); 26 | await adminPage.login('admin', 'password'); 27 | await expect(header.logoutLink, 'Administrator is logged in').toBeVisible(); 28 | 29 | await authApi.login('admin', 'password'); 30 | }); 31 | 32 | const rooms: [string, RoomType, boolean, number, RoomAmenities][] = [ 33 | ['114', RoomType.SINGLE, false, 80, { wifi: false, tv: false, radio: false, refreshments: false, safe: false, views: false }], 34 | ['115', RoomType.TWIN, false, 150, { wifi: true, tv: true, radio: false, refreshments: false, safe: true, views: false }], 35 | ['116', RoomType.DOUBLE, true, 200, { wifi: true, tv: true, radio: false, refreshments: true, safe: true, views: false }], 36 | ['117', RoomType.FAMILY, true, 250, { wifi: true, tv: true, radio: true, refreshments: true, safe: true, views: true }], 37 | ['118', RoomType.SUITE, true, 300, { wifi: true, tv: true, radio: true, refreshments: true, safe: true, views: true }] 38 | ]; 39 | for (const room of rooms) { 40 | test(`User must be able to create new ${room[1]} room named ${room[0]} by filling up all mandatory fields @sanity @management @room-management`, async ({ 41 | page 42 | }) => { 43 | const name = room[0]; 44 | const type = room[1]; 45 | const accessible = room[2]; 46 | const price = room[3]; 47 | const amenities = room[4]; 48 | await roomsPage.createRoom(name, type, accessible, price, amenities); 49 | 50 | const accessibleString = accessible.toString(); 51 | const priceString = price.toString(); 52 | const amenitiesString = getRoomDetailsFromAmenities(amenities); 53 | const roomRecord = page.locator(`//div[@data-testid='roomlisting'][.//p[contains(@id,'${name}')]]`).last(); 54 | await expect(roomRecord, `Room ${name} is not created!`).toBeVisible(); 55 | await expect(roomRecord.locator('p[id*=roomName]'), `Room ${name} has correct name: ${name}`).toContainText(name); 56 | await expect(roomRecord.locator('p[id*=type]'), `Room ${name} has correct type: ${type}`).toContainText(type); 57 | await expect(roomRecord.locator('p[id*=accessible]'), `Room ${name} has correct accessibility: ${accessibleString}`).toContainText( 58 | accessibleString 59 | ); 60 | await expect(roomRecord.locator('p[id*=roomPrice]'), `Room ${name} has correct price: ${priceString}`).toContainText(priceString); 61 | await expect(roomRecord.locator('p[id*=details]'), `Room ${name} has correct details: ${amenitiesString}`).toContainText(amenitiesString); 62 | await roomApi.deleteAllRooms(name); 63 | }); 64 | } 65 | 66 | test('User must NOT be able to create new room without filling up room name field @management @room-management', async () => { 67 | await roomsPage.createRoom('', RoomType.TWIN, false, 55, { wifi: true, tv: true, radio: false, refreshments: false, safe: false, views: false }); 68 | await expect(roomsPage.errorMessages, 'Error messages are displayed').toBeVisible(); 69 | const errorMessage = 'Room name must be set'; 70 | await expect(roomsPage.errorMessages, `Error message '${errorMessage}' is displayed`).toContainText(errorMessage); 71 | }); 72 | 73 | test('User must NOT be able to create new room without filling up room price field @management @room-management', async () => { 74 | await roomsPage.createRoom('314', RoomType.TWIN, false, null, { 75 | wifi: true, 76 | tv: true, 77 | radio: false, 78 | refreshments: false, 79 | safe: true, 80 | views: false 81 | }); 82 | await expect(roomsPage.errorMessages, 'Error messages are displayed').toBeVisible(); 83 | const errorMessage = 'must be greater than or equal to 1'; 84 | await expect(roomsPage.errorMessages, `Error message '${errorMessage}' is displayed`).toContainText(errorMessage); 85 | }); 86 | 87 | test('User must NOT be able to create new room with price 0 @management @room-management', async () => { 88 | await roomsPage.createRoom('314', RoomType.TWIN, false, 0, { 89 | wifi: false, 90 | tv: false, 91 | radio: false, 92 | refreshments: false, 93 | safe: false, 94 | views: false 95 | }); 96 | await expect(roomsPage.errorMessages, 'Error messages are displayed').toBeVisible(); 97 | const errorMessage = 'must be greater than or equal to 1'; 98 | await expect(roomsPage.errorMessages, `Error message '${errorMessage}' is displayed`).toContainText(errorMessage); 99 | }); 100 | }); 101 | -------------------------------------------------------------------------------- /playwright.config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable indent */ 2 | import { defineConfig, devices } from '@playwright/test'; 3 | 4 | /** 5 | * Read environment variables from file. 6 | * https://github.com/motdotla/dotenv 7 | */ 8 | // require('dotenv').config(); 9 | 10 | function getBaseUrl() { 11 | const environment = process.env.ENV; 12 | if (environment == undefined || environment == null) return 'https://automationintesting.online/'; 13 | else if (environment == 'prod') return 'https://automationintesting.online/'; 14 | else if (environment == 'local') return 'http://localhost'; 15 | else if (environment == 'kubeLocal') return 'http://kube.local'; 16 | else if (environment == 'docker') return 'http://rbp-proxy'; 17 | else return 'https://automationintesting.online/'; 18 | } 19 | 20 | /** 21 | * See https://playwright.dev/docs/test-configuration. 22 | */ 23 | export default defineConfig({ 24 | testDir: './tests', 25 | /* Run tests in files in parallel */ 26 | fullyParallel: true, 27 | /* Fail the build on CI if you accidentally left test.only in the source code. */ 28 | forbidOnly: !!process.env.CI, 29 | /* Retry on CI only */ 30 | retries: process.env.CI ? 1 : 0, 31 | /* Opt out of parallel tests on CI. */ 32 | workers: process.env.CI ? 1 : undefined, 33 | /* Reporter to use. See https://playwright.dev/docs/test-reporters */ 34 | reporter: process.env.CI 35 | ? [ 36 | ['list'], 37 | ['line'], 38 | ['html', { open: 'never' }], 39 | [ 40 | 'monocart-reporter', 41 | { 42 | name: 'Playwright E2E Tests', 43 | outputFile: './playwright-monocart-report/index.html' 44 | } 45 | ], 46 | ['blob', { outputDir: 'playwright-blob-report', fileName: `report-${process.env.BLOB_NAME}.zip` }], 47 | [ 48 | 'allure-playwright', 49 | { 50 | detail: true, 51 | resultsDir: 'playwright-allure-results', 52 | suiteTitle: false 53 | } 54 | ] 55 | ] 56 | : [ 57 | ['list'], 58 | ['line'], 59 | ['html', { open: 'on-failure' }], 60 | [ 61 | 'monocart-reporter', 62 | { 63 | name: 'Playwright E2E Tests', 64 | outputFile: './playwright-monocart-report/index.html' 65 | } 66 | ], 67 | [ 68 | 'allure-playwright', 69 | { 70 | detail: true, 71 | resultsDir: 'playwright-allure-results', 72 | suiteTitle: false 73 | } 74 | ] 75 | ], 76 | /* Limit the number of failures on CI to save resources */ 77 | maxFailures: process.env.CI ? 10 : undefined, 78 | // Folder for test artifacts such as screenshots, videos, traces, etc. 79 | outputDir: 'test-results', 80 | /* path to the global setup files. */ 81 | // globalSetup: require.resolve('./setup/global.setup.ts'), 82 | /* path to the global teardown files. */ 83 | // globalTeardown: require.resolve('./setup/global.teardown.ts'), 84 | /* Each test is given 60 seconds. */ 85 | timeout: 60000, 86 | 87 | /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ 88 | use: { 89 | /* Base URL to use in actions like `await page.goto('/')`. */ 90 | baseURL: getBaseUrl(), 91 | /* Populates context with given storage state. */ 92 | // storageState: './state.json', 93 | /* Viewport used for all pages in the context. */ 94 | // viewport: { width: 1280, height: 720 }, 95 | /* Capture screenshot. */ 96 | screenshot: 'only-on-failure', 97 | /* Record video. */ 98 | video: 'on-first-retry', 99 | /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ 100 | trace: process.env.CI ? 'off' : 'on-first-retry', 101 | /* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */ 102 | actionTimeout: 0, 103 | /* Whether to ignore HTTPS errors when sending network requests. Defaults to `false`. */ 104 | ignoreHTTPSErrors: true, 105 | /* Run browser in headless mode. */ 106 | headless: true, 107 | /* Change the default data-testid attribute. */ 108 | testIdAttribute: 'data-testid' 109 | }, 110 | 111 | /* Configure projects for major browsers */ 112 | projects: [ 113 | { 114 | name: 'chromium', 115 | use: { ...devices['Desktop Chrome'], trace: process.env.CI ? 'on-first-retry' : 'on-first-retry' } 116 | }, 117 | 118 | { 119 | name: 'firefox', 120 | use: { ...devices['Desktop Firefox'] } 121 | }, 122 | 123 | { 124 | name: 'webkit', 125 | use: { ...devices['Desktop Safari'] } 126 | } 127 | 128 | /* Test against mobile viewports. */ 129 | // { 130 | // name: 'Mobile Chrome', 131 | // use: { ...devices['Pixel 5'] }, 132 | // }, 133 | // { 134 | // name: 'Mobile Safari', 135 | // use: { ...devices['iPhone 12'] }, 136 | // }, 137 | 138 | /* Test against branded browsers. */ 139 | // { 140 | // name: 'Microsoft Edge', 141 | // use: { ...devices['Desktop Edge'], channel: 'msedge' }, 142 | // }, 143 | // { 144 | // name: 'Google Chrome', 145 | // use: { ..devices['Desktop Chrome'], channel: 'chrome' }, 146 | // }, 147 | ] 148 | 149 | /* Run your local dev server before starting the tests */ 150 | // webServer: { 151 | // command: 'npm run start', 152 | // url: 'http://127.0.0.1:3000', 153 | // reuseExistingServer: !process.env.CI, 154 | // }, 155 | }); 156 | -------------------------------------------------------------------------------- /.github/workflows/playwright.yml: -------------------------------------------------------------------------------- 1 | name: Playwright Tests 2 | 3 | on: 4 | workflow_dispatch: 5 | schedule: 6 | - cron: '0 9 * * 1' 7 | 8 | jobs: 9 | install: 10 | name: Install 11 | runs-on: ubuntu-latest 12 | timeout-minutes: 30 13 | steps: 14 | - name: Checkout Repository 15 | id: checkout-repository 16 | uses: actions/checkout@v4 17 | - name: Setup Node 18 | id: setup-node 19 | uses: actions/setup-node@v4 20 | with: 21 | node-version: 20 22 | - name: Cache Node Modules 23 | id: cache-node-modules 24 | uses: actions/cache@v4 25 | with: 26 | path: | 27 | node_modules 28 | key: modules-${{ hashFiles('package-lock.json') }} 29 | - name: Cache Playwright Binaries 30 | id: cache-playwright 31 | uses: actions/cache@v4 32 | with: 33 | path: | 34 | ~/.cache/ms-playwright 35 | key: playwright-${{ hashFiles('package-lock.json') }} 36 | - name: Install dependencies 37 | id: install-dependencies 38 | if: steps.cache-node-modules.outputs.cache-hit != 'true' 39 | run: npm ci 40 | - name: Install Playwright Browsers 41 | id: install-playwright-browsers 42 | if: steps.cache-playwright.outputs.cache-hit != 'true' 43 | run: npx playwright install --with-deps 44 | - name: Run Type Checks 45 | id: run-type-checks 46 | run: npx tsc -p tsconfig.json --noEmit 47 | 48 | test: 49 | name: Tests - ${{ matrix.project }} - Shard ${{ matrix.shardIndex }} of ${{ matrix.shardTotal }} 50 | runs-on: ubuntu-latest 51 | needs: [install] 52 | timeout-minutes: 60 53 | strategy: 54 | fail-fast: false 55 | matrix: 56 | project: [chromium, firefox, webkit] 57 | shardIndex: [1, 2] 58 | shardTotal: [2] 59 | services: 60 | rbp-booking: 61 | image: mwinteringham/restfulbookerplatform_booking:1.6.24c7b22 62 | ports: 63 | - 3000:3000 64 | rbp-room: 65 | image: mwinteringham/restfulbookerplatform_room:1.6.24c7b22 66 | ports: 67 | - 3001:3001 68 | rbp-branding: 69 | image: mwinteringham/restfulbookerplatform_branding:1.6.24c7b22 70 | ports: 71 | - 3002:3002 72 | rbp-assets: 73 | image: mwinteringham/restfulbookerplatform_assets:1.6.24c7b22 74 | ports: 75 | - 3003:3003 76 | rbp-auth: 77 | image: mwinteringham/restfulbookerplatform_auth:1.6.24c7b22 78 | ports: 79 | - 3004:3004 80 | rbp-report: 81 | image: mwinteringham/restfulbookerplatform_report:1.6.24c7b22 82 | ports: 83 | - 3005:3005 84 | rbp-message: 85 | image: mwinteringham/restfulbookerplatform_message:1.6.24c7b22 86 | ports: 87 | - 3006:3006 88 | rbp-proxy: 89 | image: mwinteringham/restfulbookerplatform_proxy:latest 90 | ports: 91 | - 80:80 92 | steps: 93 | - name: Checkout Repository 94 | id: checkout-repository 95 | uses: actions/checkout@v4 96 | - name: Setup Node 97 | id: setup-node 98 | uses: actions/setup-node@v4 99 | with: 100 | node-version: 20 101 | - name: Cache Node Modules 102 | id: cache-node-modules 103 | uses: actions/cache@v4 104 | with: 105 | path: | 106 | node_modules 107 | key: modules-${{ hashFiles('package-lock.json') }} 108 | - name: Cache Playwright Binaries 109 | id: cache-playwright 110 | uses: actions/cache@v4 111 | with: 112 | path: | 113 | ~/.cache/ms-playwright 114 | key: playwright-${{ hashFiles('package-lock.json') }} 115 | # Playwright caches the browser binaries, but not their dependencies. 116 | # Those extra browser dependencies must be installed separately when the cached browsers are restored. 117 | - name: Install Playwright System Dependencies 118 | id: install-playwright-system-dependencies 119 | run: npx playwright install-deps ${{ matrix.project }} 120 | - name: Run Playwright Tests 121 | id: run-playwright-tests 122 | run: npx playwright test --project=${{ matrix.project }} --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} 123 | env: 124 | ENV: local 125 | BLOB_NAME: ${{ matrix.project }}-${{ matrix.shardIndex }} 126 | - name: Upload Playwright Blob Report 127 | id: upload-playwright-blob-report 128 | uses: actions/upload-artifact@v5 129 | if: always() 130 | with: 131 | name: playwright-blob-report-${{ matrix.project }}-${{ matrix.shardIndex }}_${{ matrix.shardTotal }} 132 | path: playwright-blob-report 133 | if-no-files-found: ignore 134 | retention-days: 1 135 | - name: Upload Playwright Test Results 136 | id: upload-playwright-test-results 137 | uses: actions/upload-artifact@v5 138 | if: always() 139 | with: 140 | name: test-results-${{ matrix.project }}-${{ matrix.shardIndex }}_${{ matrix.shardTotal }} 141 | path: test-results/ 142 | if-no-files-found: ignore 143 | retention-days: 1 144 | - name: Upload Playwright Monocart Report 145 | id: upload-playwright-monocart-report 146 | uses: actions/upload-artifact@v5 147 | if: always() 148 | with: 149 | name: monocart-report-${{ matrix.project }}-${{ matrix.shardIndex }}_${{ matrix.shardTotal }} 150 | path: playwright-monocart-report/ 151 | if-no-files-found: ignore 152 | retention-days: 1 153 | - name: Upload Playwright Allure Results 154 | id: upload-playwright-allure-results 155 | uses: actions/upload-artifact@v5 156 | if: always() 157 | with: 158 | name: playwright-allure-results-${{ matrix.project }}-${{ matrix.shardIndex }}_${{ matrix.shardTotal }} 159 | path: playwright-allure-results/ 160 | if-no-files-found: ignore 161 | retention-days: 1 162 | 163 | report: 164 | name: Merge and Publish Reports 165 | if: ${{ always() }} 166 | runs-on: ubuntu-latest 167 | needs: [test] 168 | timeout-minutes: 30 169 | permissions: 170 | actions: write 171 | contents: read 172 | pages: write 173 | id-token: write 174 | concurrency: 175 | group: 'pages' 176 | cancel-in-progress: true 177 | environment: 178 | name: github-pages 179 | url: ${{ steps.deployment.outputs.page_url }} 180 | steps: 181 | - name: Checkout Repository 182 | id: checkout-repository 183 | uses: actions/checkout@v4 184 | - name: Setup Node 185 | id: setup-node 186 | uses: actions/setup-node@v4 187 | with: 188 | node-version: 20 189 | - name: Cache Node Modules 190 | id: cache-node-modules 191 | uses: actions/cache@v4 192 | with: 193 | path: | 194 | node_modules 195 | key: modules-${{ hashFiles('package-lock.json') }} 196 | - name: Cache Playwright Binaries 197 | id: cache-playwright 198 | uses: actions/cache@v4 199 | with: 200 | path: | 201 | ~/.cache/ms-playwright 202 | key: playwright-${{ hashFiles('package-lock.json') }} 203 | - name: Download Playwright Blob Reports 204 | id: download-blob-reports 205 | uses: actions/download-artifact@v4 206 | with: 207 | path: playwright-all-blob-reports 208 | pattern: playwright-blob-report-* 209 | merge-multiple: true 210 | - name: Merge Playwright HTML Reports 211 | id: merge-playwright-html-reports 212 | run: npx playwright merge-reports --reporter html ./playwright-all-blob-reports 213 | - name: Download Monocart Reports 214 | id: download-monocart-reports 215 | uses: actions/download-artifact@v4 216 | with: 217 | pattern: monocart-report-* 218 | - name: Download Test Results 219 | id: download-test-results 220 | uses: actions/download-artifact@v4 221 | with: 222 | path: merged-monocart-report/data 223 | pattern: test-results-* 224 | merge-multiple: true 225 | - name: Merge Playwright Monocart Reports 226 | id: merge-playwright-monocart-reports 227 | run: | 228 | curl -o previous-trend.json https://m-pujic-levi9-com.github.io/playwright-e2e-tests/monocart/index.json 229 | npm run merge:report:monocart 230 | - name: Download Allure Results 231 | id: download-allure-results 232 | uses: actions/download-artifact@v4 233 | with: 234 | path: playwright-allure-results 235 | pattern: playwright-allure-results-* 236 | merge-multiple: true 237 | - name: Generate Allure Report 238 | id: generate-allure-report 239 | run: | 240 | curl --create-dirs -o ./playwright-allure-results/history/categories-trend.json https://m-pujic-levi9-com.github.io/playwright-e2e-tests/allure/history/categories-trend.json 241 | curl --create-dirs -o ./playwright-allure-results/history/duration-trend.json https://m-pujic-levi9-com.github.io/playwright-e2e-tests/allure/history/duration-trend.json 242 | curl --create-dirs -o ./playwright-allure-results/history/history-trend.json https://m-pujic-levi9-com.github.io/playwright-e2e-tests/allure/history/history-trend.json 243 | curl --create-dirs -o ./playwright-allure-results/history/history.json https://m-pujic-levi9-com.github.io/playwright-e2e-tests/allure/history/history.json 244 | curl --create-dirs -o ./playwright-allure-results/history/retry-trend.json https://m-pujic-levi9-com.github.io/playwright-e2e-tests/allure/history/retry-trend.json 245 | npx allure generate playwright-allure-results -o playwright-allure-report --clean 246 | - name: Move to Reports folder 247 | id: move-to-reports-folder 248 | run: | 249 | mv playwright-report reports/playwright 250 | mv merged-monocart-report reports/monocart 251 | mv playwright-allure-report reports/allure 252 | shell: bash 253 | - name: Setup Pages 254 | id: setup-pages 255 | uses: actions/configure-pages@v5 256 | - name: Upload Pages Artifact 257 | id: upload-pages-artifact 258 | uses: actions/upload-pages-artifact@v3 259 | with: 260 | path: reports/ 261 | - name: Deploy to GitHub Pages 262 | id: deployment 263 | uses: actions/deploy-pages@v4 264 | - name: Delete Unnecessary Artifacts 265 | id: delete-unnecessary-artifacts 266 | uses: geekyeggo/delete-artifact@v5 267 | with: 268 | name: | 269 | playwright-blob-report-* 270 | test-results-* 271 | monocart-report-* 272 | playwright-allure-results-* 273 | failOnError: false 274 | -------------------------------------------------------------------------------- /docs/minikube-setup-mac.md: -------------------------------------------------------------------------------- 1 | # Minikube Setup for Mac 2 | 3 | Minikube is one of free alternatives to Docker for Desktop. Minikube and Docker for Desktop are both tools that allow developers to run a local Kubernetes cluster on their own machines. However, there are some key differences between the two tools: 4 | 5 | - Docker for Desktop is a full-featured desktop application that includes Docker Engine, Kubernetes, and other tools, while Minikube is a command-line tool that provides a lightweight, single-node Kubernetes cluster. 6 | - Docker for Desktop provides a graphical user interface (GUI) that makes it easy to manage your containers and Kubernetes cluster, while Minikube requires you to use the command line to manage your cluster. 7 | - Docker for Desktop supports both Docker Compose and Kubernetes, while Minikube is focused solely on Kubernetes. 8 | - Docker for Desktop can be used for both local development and production deployments, while Minikube is primarily used for local development and testing. 9 | 10 | More information on minikube can be found [here](https://github.com/kubernetes/minikube). 11 | 12 | ## Clean up Docker for Desktop 13 | 14 | This section is only applicable if Docker for Desktop is / was installed on machine. If it isn't / wasn't installed on machine this section can be skipped. 15 | 16 | If you have Docker for Desktop installed on your machine, uninstall it before installing minikube. 17 | 18 | Uninstall guides for Windows/Mac are available [here](https://docs.docker.com/desktop/uninstall/). 19 | 20 | After that, clear the leftover Docker for Desktop data and system components. 21 | 22 | Open terminal and execute following commands, one by one, to remove all Docker Desktop dependencies on local file system: 23 | 24 | sudo rm -f /usr/local/bin/docker 25 | sudo rm -f /usr/local/bin/docker-machine 26 | sudo rm -f /usr/local/bin/docker-compose 27 | sudo rm -f /usr/local/bin/docker-credential-desktop 28 | sudo rm -f /usr/local/bin/docker-credential-ecr-login 29 | sudo rm -f /usr/local/bin/docker-credential-osxkeychain 30 | sudo rm -Rf ~/.docker 31 | sudo rm -Rf ~/Library/Containers/com.docker.docker 32 | sudo rm -Rf ~/Library/Application\ Support/Docker\ Desktop 33 | sudo rm -Rf ~/Library/Group\ Containers/group.com.docker 34 | sudo rm -f ~/Library/HTTPStorages/com.docker.docker.binarycookies 35 | sudo rm -f /Library/PrivilegedHelperTools/com.docker.vmnetd 36 | sudo rm -f /Library/LaunchDaemons/com.docker.vmnetd.plist 37 | sudo rm -Rf ~/Library/Logs/Docker\ Desktop 38 | sudo rm -Rf /usr/local/lib/docker 39 | sudo rm -f ~/Library/Preferences/com.docker.docker.plist 40 | sudo rm -Rf ~/Library/Saved\ Application\ State/com.electron.docker-frontend.savedState 41 | sudo rm -f ~/Library/Preferences/com.electron.docker-frontend.plist 42 | 43 | ## Installation Guide 44 | 45 | Official installation guides are available ono minikube's [Get Started!](https://minikube.sigs.k8s.io/docs/start/) page. This guide recommends and shows installation via package managers, for Mac [Homebrew](https://brew.sh/). You can choose any other method mentioned on minikube's [Get Started!](https://minikube.sigs.k8s.io/docs/start/) page, but than you must install other components and configuration on your own. 46 | 47 | This guide will cover setup for: 48 | 49 | - Package Manager: [Homebrew](https://brew.sh/) 50 | - Virtualization Engine: `HyperKit` 51 | - Additionally, tools and command line interfaces: `Docker CLI`, `Docker Compose` and `Docker Buildx` 52 | - Host File and Terminal Configuration 53 | 54 | ### Install minikube 55 | 56 | 1. Open Terminal 57 | 2. Install Homebrew by running the following command: 58 | 59 | /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" 60 | 61 | 3. Install HyperKit by running the following command: 62 | 63 | brew install hyperkit 64 | 65 | 4. Install Minikube by running the following command: 66 | 67 | brew install minikube 68 | 69 | 5. Verify the installation by running the following command: 70 | 71 | minikube version 72 | 73 | 6. Install Docker CLI by running the following command: 74 | 75 | brew install docker 76 | 77 | 7. Install Docker Buildx by running the following command: 78 | 79 | brew install docker-buildx 80 | 81 | - docker-buildx is a Docker plugin. For Docker to find this plugin, symlink it: 82 | 83 | mkdir -p ~/.docker/cli-plugins 84 | ln -sfn /usr/local/opt/docker-buildx/bin/docker-buildx ~/.docker/cli-plugins/docker-buildx 85 | 86 | 8. Install Docker Compose by running the following command: 87 | 88 | brew install docker-compose 89 | 90 | - Compose is now a Docker plugin. For Docker to find this plugin, symlink it: 91 | 92 | mkdir -p ~/.docker/cli-plugins 93 | ln -sfn /usr/local/opt/docker-compose/bin/docker-compose ~/.docker/cli-plugins/docker-compose 94 | 95 | ## Start minikube 96 | 97 | To start minikube, it is important that on first initial start configuration is passed with configuration flags. 98 | 99 | Recommendation is to give minikube half of machines resources, if you have 16GB or RAM, give minikube 8GB of RAM, if you have 8 core CPU, give minikube 4 cores. 100 | 101 | To be able to use ports like 80 and 8080 it is needed to extend NodePort range from default range 30000-32767 to 1-65535. 102 | 103 | ### Initial start of minikube 104 | 105 | 1. Open Terminal 106 | 2. Start minikube by running the following command (you will be asked for sudo rights): 107 | 108 | minikube start --addons=dashboard --addons=metrics-server --memory 8192 --cpus 4 --extra-config=apiserver.service-node-port-range=1-65535 109 | 110 | - sometime error can occur during initial start, in that case stop minikube, purge it and start again with same command: 111 | 112 | minikube stop 113 | minikube delete --all --purge 114 | minikube start --addons=dashboard --addons=metrics-server --memory 8192 --cpus 4 --extra-config=apiserver.service-node-port-range=1-65535 115 | 116 | --- 117 | 118 | After minikube is initially started like this, every next start can be just with the command: 119 | 120 | minikube start 121 | 122 | When you finish testing / using minikube for the day, do not forget to stop it to conserve machine resources, with command: 123 | 124 | minikube stop 125 | 126 | Next time when you start it will be in same state as when you stopped it. 127 | 128 | Minikube configuration can always be checked in `~/.minikube/machines/minikube/config.json` file. 129 | 130 | ## Configure machine to use minikube 131 | 132 | To use minikube with ease there are a couple of tips and tricks which can help you. 133 | 134 | ### (optional) Minikube Dashboard 135 | 136 | Minikube Dashboard is a web-based Kubernetes user interface. To access the dashboard use following command in Terminal: 137 | 138 | minikube dashboard 139 | 140 | This will enable the dashboard add-on, and open the proxy in the default web browser. 141 | 142 | ### (optional) Visual Studio Code (VS Code) plugins 143 | 144 | On both Windows and Mac there are plugins available for VS Code which provide user interface to Minikube's Kubernetes. Plugins which can help control Minikube's Kubernetes are: 145 | 146 | - [Docker VS Code Plugin](https://marketplace.visualstudio.com/items?itemName=ms-azuretools.vscode-docker) 147 | - [Kubernetes VS Code Plugin](https://marketplace.visualstudio.com/items?itemName=ms-kubernetes-tools.vscode-kubernetes-tools) 148 | 149 | Docker VS Code Plugin requires to configure it properly to use minikube's docker. To configure this plugin open its configuration inside of VS Code, and navigate to `Docker: Environment` section. Run following command in Terminal: 150 | 151 | minikube docker-env 152 | 153 | That command will output `DOCKER_TLS_VERIFY`, `DOCKER_HOST`, `DOCKER_CERT_PATH` and `MINIKUBE_ACTIVE_DOCKERD` items. Add all 4 items with their values in `Docker: Environment` section. 154 | 155 | Kubernetes VS Code Plugin does not require any additional configuration. 156 | 157 | After that you can use both of those plugins to control your Kubernetes cluster and docker inside VS Code. 158 | 159 | ### Configure Mac host file and terminal to use minikube 160 | 161 | #### Configure Host File 162 | 163 | Add minikube IP address in host file for easier access. Bellow command will add host record pointing to minikube IP and with domain name `kube.local`. 164 | 165 | echo "`minikube ip` kube.local" | sudo tee -a /etc/hosts > /dev/null 166 | 167 | #### Configure Current Terminal Session 168 | 169 | If you just want to use docker commands inside current session use this guide, but if you want to use it in all terminal sessions, skip this one and use next [Configure All Terminal Sessions](#configure-all-terminal-sessions) guide. 170 | 171 | To be able to run docker commands with minikube inside **CURRENT** terminal session we need to configure docker-cli to use minikube. 172 | 173 | Execute following command: 174 | 175 | minikube docker-env 176 | 177 | It will output list of commands which you need to execute, but also, at the end, commented out, there is command which you can execute, and it will do it all for you. 178 | 179 | For Mac that is following command: 180 | 181 | eval $(minikube -p minikube docker-env) 182 | 183 | IMPORTANT: If you close and/or open new terminal session you will need again to execute above command(s) before you can use docker commands. 184 | 185 | #### Configure All Terminal Sessions 186 | 187 | If you just want to use docker commands inside all sessions use this guide, but if you want to use it in current terminal session, skip this one and use previous [Configure Current Terminal Session](#configure-current-terminal-session) guide. 188 | 189 | To be able to run docker commands with minikube inside **ALL** terminal sessions we need to configure docker-cli to use minikube. Add following entry to `~/.bashrc` or `~/.zshrc`: 190 | 191 | - `eval $(minikube docker-env)` 192 | 193 | ## Uninstall minikube and all its dependencies 194 | 195 | 1. Open Terminal 196 | 2. Stop minikube by running the following command: 197 | 198 | minikube stop 199 | 200 | 3. Delete and purge minikube by running the following command: 201 | 202 | minikube delete --all --purge 203 | 204 | 4. Uninstall Docker Buildx by running the following command: 205 | 206 | brew uninstall docker-buildx 207 | 208 | 5. Uninstall Docker Compose by running the following command: 209 | 210 | brew uninstall docker-compose 211 | 212 | 6. Uninstall Docker CLI by running the following command: 213 | 214 | brew uninstall docker 215 | 216 | 7. Uninstall Minikube by running the following command: 217 | 218 | brew uninstall minikube 219 | 220 | 8. Uninstall HyperKit by running the following command: 221 | 222 | brew uninstall hyperkit 223 | 224 | 9. Remove all unused dependencies by running the following command: 225 | 226 | brew autoremove 227 | 228 | 10. Manually remove Host Entry added [this step](#configure-host-file), easiest way to do that is via nano text editor by using following command: 229 | 230 | sudo nano /etc/hosts 231 | 232 | - Then scroll to the end of the file and remove added host record 233 | - Save and Close nano text editor 234 | 235 | Control + O 236 | Control + X 237 | 238 | 11. If you have used this guide [Configure All Terminal Sessions](#configure-all-terminal-sessions), then manually remove entry from `~/.bashrc` or `~/.zshrc` files 239 | 12. (optional) Uninstall Homebrew by running the following command: 240 | 241 | /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/uninstall.sh)" 242 | -------------------------------------------------------------------------------- /docs/minikube-setup-windows.md: -------------------------------------------------------------------------------- 1 | # Minikube Setup for Windows 2 | 3 | Minikube is one of free alternatives to Docker for Desktop. Minikube and Docker for Desktop are both tools that allow developers to run a local Kubernetes cluster on their own machines. However, there are some key differences between the two tools: 4 | 5 | - Docker for Desktop is a full-featured desktop application that includes Docker Engine, Kubernetes, and other tools, while Minikube is a command-line tool that provides a lightweight, single-node Kubernetes cluster. 6 | - Docker for Desktop provides a graphical user interface (GUI) that makes it easy to manage your containers and Kubernetes cluster, while Minikube requires you to use the command line to manage your cluster. 7 | - Docker for Desktop supports both Docker Compose and Kubernetes, while Minikube is focused solely on Kubernetes. 8 | - Docker for Desktop can be used for both local development and production deployments, while Minikube is primarily used for local development and testing. 9 | 10 | More information on minikube can be found [here](https://github.com/kubernetes/minikube). 11 | 12 | ## Clean up Docker for Desktop 13 | 14 | This section is only applicable if Docker for Desktop is / was installed on machine. If it isn't / wasn't installed on machine this section can be skipped. 15 | 16 | If you have Docker for Desktop installed on your machine, uninstall it before installing minikube. 17 | 18 | Uninstall guides for Windows/Mac are available [here](https://docs.docker.com/desktop/uninstall/). 19 | 20 | After that, clear the leftover Docker for Desktop data and system components. 21 | 22 | 1. Open the elevated (as Admin) PowerShell window, type the following command and hit Enter to remove the default networks of Docker. 23 | 24 | Get-HNSNetwork | Remove-HNSNetwork 25 | 26 | 2. Run the following command to clear the program date of Docker from Windows. 27 | 28 | Remove-Item "C:\ProgramData\Docker" -Recurse 29 | 30 | 3. Run the following command to reboot your system to execute the uninstallation and cleanup. 31 | 32 | Restart-Computer -Force 33 | 34 | ## Installation Guide 35 | 36 | Official installation guides are available ono minikube's [Get Started!](https://minikube.sigs.k8s.io/docs/start/) page. This guide recommends and shows installation via package managers, for Windows [Chocolatey](https://chocolatey.org/). You can choose any other method mentioned on minikube's [Get Started!](https://minikube.sigs.k8s.io/docs/start/) page, but than you must install other components and configuration on your own. 37 | 38 | This guide will cover setup for: 39 | 40 | - Package Manager: [Chocolatey](https://chocolatey.org/) 41 | - Virtualization Engine: `Hyper-V` 42 | - Additionally, tools and command line interfaces: `Docker CLI` and `Docker Compose` 43 | - Host File and PowerShell Configuration 44 | 45 | ### Install minikube 46 | 47 | 1. Enable Hyper-V on Windows with bellow guide: 48 | 49 | 1. Press the Windows key + R to open the Run dialog box. 50 | 2. Type appwiz.cpl and press Enter. 51 | 3. In the Programs and Features window, select Turn Windows features on or off in the left-hand pane. 52 | 4. In the Windows Features window, scroll down to Hyper-V and check the box next to it. 53 | 54 | ![Windows Enable HyperV](/docs/imgs/windows-enable-hyperv.png) 55 | 56 | 5. Click on OK and wait for the installation process to complete. 57 | 6. Once the installation is complete, click on Restart Now to restart your computer. 58 | 2. Open PowerShell with Administrator privileges 59 | 3. Install Chocolatey by running the following command: 60 | 61 | Set-ExecutionPolicy Bypass -Scope Process -Force; iex ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1')) 62 | 63 | 4. Install Minikube by running the following command: 64 | 65 | choco install minikube -y 66 | 67 | 5. Install Docker CLI by running the following command: 68 | 69 | choco install docker-cli -y 70 | 71 | 6. Install Docker Compose by running the following command: 72 | 73 | choco install docker-compose -y 74 | 75 | 7. Close and Open Again PowerShell with Administrator privileges 76 | 77 | 8. Verify the installation by running the following command: 78 | 79 | minikube version 80 | 81 | ## Start minikube 82 | 83 | To start minikube, it is important that on first initial start configuration is passed with configuration flags. 84 | 85 | Recommendation is to give minikube half of machines resources, if you have 16GB or RAM, give minikube 8GB of RAM, if you have 8 core CPU, give minikube 4 cores. 86 | 87 | To be able to use ports like 80 and 8080 it is needed to extend NodePort range from default range 30000-32767 to 1-65535. 88 | 89 | ### Initial start of minikube 90 | 91 | 1. Open PowerShell with administrator privileges 92 | 2. Start minikube by running the following command: 93 | 94 | minikube start --addons=dashboard --addons=metrics-server --memory 8192 --cpus 4 --extra-config=apiserver.service-node-port-range=1-65535 95 | 96 | - sometime error can occur during initial start, in that case stop minikube, purge it and start again with same command: 97 | 98 | minikube stop 99 | minikube delete --all --purge 100 | minikube start --addons=dashboard --addons=metrics-server --memory 8192 --cpus 4 --extra-config=apiserver.service-node-port-range=1-65535 101 | 102 | --- 103 | 104 | After minikube is initially started like this, every next start can be just with the command: 105 | 106 | minikube start 107 | 108 | When you finish testing / using minikube for the day, do not forget to stop it to conserve machine resources, with command: 109 | 110 | minikube stop 111 | 112 | Next time when you start it will be in same state as when you stopped it. 113 | 114 | Minikube configuration can always be checked in `%USERPROFILE%\.minikube\machines\minikube\config.json` file. 115 | 116 | ## Configure machine to use minikube 117 | 118 | To use minikube with ease there are a couple of tips and tricks which can help you. 119 | 120 | ### (optional) Minikube Dashboard 121 | 122 | Minikube Dashboard is a web-based Kubernetes user interface. To access the dashboard use following command in Powershell: 123 | 124 | minikube dashboard 125 | 126 | This will enable the dashboard add-on, and open the proxy in the default web browser. 127 | 128 | ### (optional) Visual Studio Code (VS Code) plugins 129 | 130 | On both Windows and Mac there are plugins available for VS Code which provide user interface to Minikube's Kubernetes. Plugins which can help control Minikube's Kubernetes are: 131 | 132 | - [Docker VS Code Plugin](https://marketplace.visualstudio.com/items?itemName=ms-azuretools.vscode-docker) 133 | - [Kubernetes VS Code Plugin](https://marketplace.visualstudio.com/items?itemName=ms-kubernetes-tools.vscode-kubernetes-tools) 134 | 135 | Docker VS Code Plugin requires to configure it properly to use minikube's docker. To configure this plugin open its configuration inside of VS Code, and navigate to `Docker: Environment` section. Run following command in Powershell: 136 | 137 | minikube docker-env 138 | 139 | That command will output `DOCKER_TLS_VERIFY`, `DOCKER_HOST`, `DOCKER_CERT_PATH` and `MINIKUBE_ACTIVE_DOCKERD` items. Add all 4 items with their values in `Docker: Environment` section. 140 | 141 | Kubernetes VS Code Plugin does not require any additional configuration. 142 | 143 | After that you can use both of those plugins to control your Kubernetes cluster and docker inside VS Code. 144 | 145 | ### Configure Windows host file and terminal to use minikube 146 | 147 | #### Configure Host File 148 | 149 | Add minikube IP address in host file for easier access. Bellow guide will add host record pointing to minikube IP and with domain name `kube.local`. 150 | 151 | First, fetch minikube IP address by using bellow command: 152 | 153 | minikube ip 154 | 155 | This will output IP address of minikube. After that you can add new host record in Window by following bellow steps: 156 | 157 | 1. Open Notepad as Administrator 158 | 159 | 1. Click on the Windows icon in the taskbar or press the Windows key on your keyboard to open the Start Menu. 160 | 2. Type "Notepad" into the search bar. 161 | 3. Right-click on "Notepad" in the search results. 162 | 4. Select "Run as administrator" from the context menu. This will open Notepad with administrative privileges, which is necessary to edit the host file. 163 | 164 | 2. Open the Host File 165 | 166 | 1. In Notepad, click on "File" in the top-left corner of the window. 167 | 2. Choose "Open" from the menu. 168 | 3. In the "File Name" field of the "Open" dialog, type the following path and press Enter: C:\Windows\System32\drivers\etc\hosts 169 | This will open the host file located in the specified directory. 170 | 171 | 3. Add the new Host Entry 172 | 173 | 1. The host file should now be open in Notepad. Scroll to the end of the file. 174 | 2. Add the following line to the end of the host file: 175 | 176 | [MINIKUBE_IP_ADDRESS] kube.local 177 | 178 | IMPORTANT: Replace [MINIKUBE_IP_ADDRESS] with IP address returned by 'minikube ip. command. Also, make sure there are no leading spaces or tabs in this line. 179 | 180 | 4. Save the Changes 181 | 182 | 1. In Notepad, click on "File" in the top-left corner again. 183 | 2. Choose "Save" from the menu. This will save the changes you made to the host file. 184 | 185 | 5. Close Notepad 186 | 6. Flush DNS Cache 187 | 188 | 1. To ensure the changes take effect immediately, open PowerShell with administrative privileges. You can do this by searching for "PowerShell" in the Start Menu, right-clicking on "Windows PowerShell," and selecting "Run as administrator." 189 | 2. In the PowerShell window, run the following command to clear the DNS cache: 190 | 191 | Clear-DnsClientCache 192 | 193 | #### Configure Current PowerShell Session 194 | 195 | To be able to run docker commands with minikube inside **CURRENT** PowerShell session we need to configure docker-cli to use minikube. 196 | 197 | Execute following command: 198 | 199 | minikube docker-env 200 | 201 | It will output list of commands which you need to execute, but also, at the end, commented out, there is command which you can execute, and it will do it all for you. 202 | 203 | For Windows that is following command: 204 | 205 | & minikube -p minikube docker-env --shell powershell | Invoke-Expression 206 | 207 | IMPORTANT: If you close and/or open new PowerShell session you will need again to execute above command(s) before you can use docker commands. 208 | 209 | ## Uninstall minikube and all its dependencies 210 | 211 | 1. Open PowerShell with administrator privileges 212 | 2. Stop minikube by running the following command: 213 | 214 | minikube stop 215 | 216 | 3. Delete and purge minikube by running the following command: 217 | 218 | minikube delete --all --purge 219 | 220 | 4. Uninstall Docker Compose by running the following command: 221 | 222 | choco uninstall docker-compose -y --remove-dependencies 223 | 224 | 5. Uninstall Docker CLI by running the following command: 225 | 226 | choco uninstall docker-cli -y --remove-dependencies 227 | 228 | 6. Uninstall Minikube by running the following command: 229 | 230 | choco uninstall minikube -y --remove-dependencies 231 | 232 | 7. Disable Hyper-V on Windows with bellow guide: 233 | 234 | 1. Press the Windows key + R to open the Run dialog box. 235 | 2. Type appwiz.cpl and press Enter. 236 | 3. In the Programs and Features window, select Turn Windows features on or off in the left-hand pane. 237 | 4. In the Windows Features window, scroll down to Hyper-V and uncheck the box next to it. 238 | 239 | ![Windows Disabled HyperV](/docs/imgs/windows-disable-hyperv.png) 240 | 241 | 5. Click on OK and wait for the uninstallation process to complete. 242 | 6. Once the uninstallation is complete, click on Restart Now to restart your computer. 243 | 244 | 8. Manually remove Host Entry added in [this step](#configure-host-file). 245 | 9. (optional) Uninstall Chocolatey by following guide available [here](https://docs.chocolatey.org/en-us/choco/uninstallation). 246 | -------------------------------------------------------------------------------- /tests/front-page/contact-hotel.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@playwright/test'; 2 | import { faker } from '@faker-js/faker'; 3 | import { FrontPage } from '../../pages/FrontPage'; 4 | import { invalidEmails } from '../../utils/test-data-util'; 5 | 6 | test.describe('Contact Hotel Tests', () => { 7 | let frontPage: FrontPage; 8 | 9 | test.beforeEach(async ({ page, baseURL }) => { 10 | frontPage = new FrontPage(page); 11 | await frontPage.hideBanner(baseURL); 12 | await frontPage.goto(); 13 | }); 14 | 15 | test('Visitor must be able to contact the property by filling up all mandatory fields @sanity @contact', async () => { 16 | const name = `${faker.person.firstName()} ${faker.person.lastName()}`; 17 | const email = faker.internet.email(); 18 | const phoneNumber = faker.phone.number(); 19 | const subject = faker.lorem.sentence(3); 20 | const message = faker.lorem.lines(5); 21 | await frontPage.sendMessage(name, email, phoneNumber, subject, message); 22 | 23 | const successMessage = `Thanks for getting in touch ${name}`; 24 | await expect(frontPage.contactSuccessMessage, 'Messages Sent Successful').toBeVisible(); 25 | await expect(frontPage.contactSuccessMessage, `Success Message '${successMessage}' is displayed`).toContainText(successMessage); 26 | }); 27 | 28 | test('Visitor must NOT be able to contact the property without filling up name field @contact', async () => { 29 | const email = faker.internet.email(); 30 | const phoneNumber = faker.phone.number(); 31 | const subject = faker.lorem.sentence(3); 32 | const message = faker.lorem.lines(5); 33 | await frontPage.sendMessage('', email, phoneNumber, subject, message); 34 | 35 | const mandatoryMessage = 'Name may not be blank'; 36 | await expect(frontPage.contactErrorMessages, 'Error Messages are displayed').toBeVisible(); 37 | await expect(frontPage.contactErrorMessages, `Mandatory Message '${mandatoryMessage}' is displayed`).toContainText(mandatoryMessage); 38 | }); 39 | 40 | test('Visitor must NOT be able to contact the property without filling up email field @contact', async () => { 41 | const name = `${faker.person.firstName()} ${faker.person.lastName()}`; 42 | const phoneNumber = faker.phone.number(); 43 | const subject = faker.lorem.sentence(3); 44 | const message = faker.lorem.lines(5); 45 | await frontPage.sendMessage(name, '', phoneNumber, subject, message); 46 | 47 | const mandatoryMessage = 'Email may not be blank'; 48 | await expect(frontPage.contactErrorMessages, 'Error Messages are displayed').toBeVisible(); 49 | await expect(frontPage.contactErrorMessages, `Mandatory Message '${mandatoryMessage}' is displayed`).toContainText(mandatoryMessage); 50 | }); 51 | 52 | test('Visitor must NOT be able to contact the property without filling up phone field @contact', async () => { 53 | const name = `${faker.person.firstName()} ${faker.person.lastName()}`; 54 | const email = faker.internet.email(); 55 | const subject = faker.lorem.sentence(3); 56 | const message = faker.lorem.lines(5); 57 | await frontPage.sendMessage(name, email, '', subject, message); 58 | 59 | const mandatoryMessage = 'Phone may not be blank'; 60 | const validationMessage = 'Phone must be between 11 and 21 characters'; 61 | await expect(frontPage.contactErrorMessages, 'Error Messages are displayed').toBeVisible(); 62 | await expect(frontPage.contactErrorMessages, `Mandatory Message '${mandatoryMessage}' is displayed`).toContainText(mandatoryMessage); 63 | await expect(frontPage.contactErrorMessages, `Validation Message '${validationMessage}' is displayed`).toContainText(validationMessage); 64 | }); 65 | 66 | test('Visitor must NOT be able to contact the property without filling up subject field @contact', async () => { 67 | const name = `${faker.person.firstName()} ${faker.person.lastName()}`; 68 | const email = faker.internet.email(); 69 | const phoneNumber = faker.phone.number(); 70 | const message = faker.lorem.lines(5); 71 | await frontPage.sendMessage(name, email, phoneNumber, '', message); 72 | 73 | const mandatoryMessage = 'Subject may not be blank'; 74 | const validationMessage = 'Subject must be between 5 and 100 characters'; 75 | await expect(frontPage.contactErrorMessages, 'Error Messages are displayed').toBeVisible(); 76 | await expect(frontPage.contactErrorMessages, `Mandatory Message '${mandatoryMessage}' is displayed`).toContainText(mandatoryMessage); 77 | await expect(frontPage.contactErrorMessages, `Validation Message '${validationMessage}' is displayed`).toContainText(validationMessage); 78 | }); 79 | 80 | test('Visitor must NOT be able to contact the property without filling up message field @contact', async () => { 81 | const name = `${faker.person.firstName()} ${faker.person.lastName()}`; 82 | const email = faker.internet.email(); 83 | const phoneNumber = faker.phone.number(); 84 | const subject = faker.lorem.sentence(3); 85 | await frontPage.sendMessage(name, email, phoneNumber, subject, ''); 86 | 87 | const mandatoryMessage = 'Message may not be blank'; 88 | const validationMessage = 'Message must be between 20 and 2000 characters'; 89 | await expect(frontPage.contactErrorMessages, 'Error Messages are displayed').toBeVisible(); 90 | await expect(frontPage.contactErrorMessages, `Mandatory Message '${mandatoryMessage}' is displayed`).toContainText(mandatoryMessage); 91 | await expect(frontPage.contactErrorMessages, `Validation Message '${validationMessage}' is displayed`).toContainText(validationMessage); 92 | }); 93 | 94 | for (const invalidEmail of invalidEmails()) { 95 | test(`Visitor must NOT be able to contact the property by filling up email with invalid value: ${invalidEmail} @contact`, async () => { 96 | // eslint-disable-next-line playwright/no-skipped-test 97 | test.skip(invalidEmail == 'email@example', 'Know issue'); 98 | const name = `${faker.person.firstName()} ${faker.person.lastName()}`; 99 | const phoneNumber = faker.phone.number(); 100 | const subject = faker.lorem.sentence(3); 101 | const message = faker.lorem.lines(5); 102 | await frontPage.sendMessage(name, invalidEmail, phoneNumber, subject, message); 103 | 104 | const validationMessage = 'must be a well-formed email address'; 105 | await expect(frontPage.contactErrorMessages, 'Error Messages are displayed').toBeVisible(); 106 | await expect(frontPage.contactErrorMessages, `Validation Message '${validationMessage}' is displayed`).toContainText(validationMessage); 107 | }); 108 | } 109 | 110 | for (const invalidPhone of ['1234567890', '1234567890123456789012']) { 111 | test(`Visitor must NOT be able to contact the property by filling up the phone with invalid length, less than 11 and more than 21 characters: ${invalidPhone} @contact`, async () => { 112 | const name = `${faker.person.firstName()} ${faker.person.lastName()}`; 113 | const email = faker.internet.email(); 114 | const subject = faker.lorem.sentence(3); 115 | const message = faker.lorem.lines(5); 116 | await frontPage.sendMessage(name, email, invalidPhone, subject, message); 117 | 118 | const validationMessage = 'Phone must be between 11 and 21 characters'; 119 | await expect(frontPage.contactErrorMessages, 'Error Messages are displayed').toBeVisible(); 120 | await expect(frontPage.contactErrorMessages, `Validation Message '${validationMessage}' is displayed`).toContainText(validationMessage); 121 | }); 122 | } 123 | 124 | for (const validPhone of ['12345678901', '123456789012345678901']) { 125 | test(`Visitor must be able to contact the property by filling up phone with valid length, between 11 and 21 characters: ${validPhone} @contact`, async () => { 126 | const name = `${faker.person.firstName()} ${faker.person.lastName()}`; 127 | const email = faker.internet.email(); 128 | const subject = faker.lorem.sentence(3); 129 | const message = faker.lorem.lines(5); 130 | await frontPage.sendMessage(name, email, validPhone, subject, message); 131 | 132 | const successMessage = `Thanks for getting in touch ${name}`; 133 | await expect(frontPage.contactSuccessMessage, 'Messages Sent Successful').toBeVisible(); 134 | await expect(frontPage.contactSuccessMessage, `Success Message '${successMessage}' is displayed`).toContainText(successMessage); 135 | }); 136 | } 137 | 138 | for (const invalidSubjectLength of [4, 101]) { 139 | test(`Visitor must NOT be able to contact the property by filling up the subject with invalid length value of ${invalidSubjectLength}, less than 5 and more than 100 characters @contact`, async () => { 140 | const name = `${faker.person.firstName()} ${faker.person.lastName()}`; 141 | const email = faker.internet.email(); 142 | const phoneNumber = faker.phone.number(); 143 | const subject = faker.string.alphanumeric(invalidSubjectLength); 144 | const message = faker.lorem.lines(5); 145 | await frontPage.sendMessage(name, email, phoneNumber, subject, message); 146 | 147 | const validationMessage = 'Subject must be between 5 and 100 characters'; 148 | await expect(frontPage.contactErrorMessages, 'Error Messages are displayed').toBeVisible(); 149 | await expect(frontPage.contactErrorMessages, `Validation Message '${validationMessage}' is displayed`).toContainText(validationMessage); 150 | }); 151 | } 152 | 153 | for (const validSubjectLength of [5, 100]) { 154 | test(`Visitor must be able to contact the property by filling up the subject with valid length value of ${validSubjectLength}, between 5 and 100 characters @contact`, async () => { 155 | const name = `${faker.person.firstName()} ${faker.person.lastName()}`; 156 | const email = faker.internet.email(); 157 | const phoneNumber = faker.phone.number(); 158 | const subject = faker.string.alphanumeric(validSubjectLength); 159 | const message = faker.lorem.lines(5); 160 | await frontPage.sendMessage(name, email, phoneNumber, subject, message); 161 | 162 | const successMessage = `Thanks for getting in touch ${name}`; 163 | await expect(frontPage.contactSuccessMessage, 'Messages Sent Successful').toBeVisible(); 164 | await expect(frontPage.contactSuccessMessage, `Success Message '${successMessage}' is displayed`).toContainText(successMessage); 165 | }); 166 | } 167 | 168 | for (const invalidMessageLength of [19, 2001]) { 169 | test(`Visitor must NOT be able to contact the property by filling up the message with invalid length value of ${invalidMessageLength}, less than 20 and more than 2000 characters @contact`, async () => { 170 | const name = `${faker.person.firstName()} ${faker.person.lastName()}`; 171 | const email = faker.internet.email(); 172 | const phoneNumber = faker.phone.number(); 173 | const subject = faker.lorem.sentence(3); 174 | const message = faker.string.alphanumeric(invalidMessageLength); 175 | await frontPage.sendMessage(name, email, phoneNumber, subject, message); 176 | 177 | const validationMessage = 'Message must be between 20 and 2000 characters'; 178 | await expect(frontPage.contactErrorMessages, 'Error Messages are displayed').toBeVisible(); 179 | await expect(frontPage.contactErrorMessages, `Validation Message '${validationMessage}' is displayed`).toContainText(validationMessage); 180 | }); 181 | } 182 | 183 | for (const validMessageLength of [20, 2000]) { 184 | test(`Visitor must be able to contact the property by filling up the message with valid length value of ${validMessageLength}, between 20 and 2000 characters @contact`, async () => { 185 | const name = `${faker.person.firstName()} ${faker.person.lastName()}`; 186 | const email = faker.internet.email(); 187 | const phoneNumber = faker.phone.number(); 188 | const subject = faker.lorem.sentence(3); 189 | const message = faker.string.alphanumeric(validMessageLength); 190 | await frontPage.sendMessage(name, email, phoneNumber, subject, message); 191 | 192 | const successMessage = `Thanks for getting in touch ${name}`; 193 | await expect(frontPage.contactSuccessMessage, 'Messages Sent Successful').toBeVisible(); 194 | await expect(frontPage.contactSuccessMessage, `Success Message '${successMessage}' is displayed`).toContainText(successMessage); 195 | }); 196 | } 197 | }); 198 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig to read more about this file */ 4 | 5 | /* Projects */ 6 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ 7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ 9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ 10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | /* Language and Environment */ 14 | "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ 15 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 16 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 17 | // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ 18 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ 20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ 22 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ 23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 25 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ 26 | 27 | /* Modules */ 28 | "module": "commonjs", /* Specify what module code is generated. */ 29 | // "rootDir": "./", /* Specify the root folder within your source files. */ 30 | // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ 31 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 32 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 33 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 34 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ 35 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 36 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 37 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ 38 | // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ 39 | // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ 40 | // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ 41 | // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ 42 | // "resolveJsonModule": true, /* Enable importing .json files. */ 43 | // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ 44 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ 45 | 46 | /* JavaScript Support */ 47 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ 48 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 49 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ 50 | 51 | /* Emit */ 52 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 53 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 54 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 55 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 56 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 57 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ 58 | // "outDir": "./", /* Specify an output folder for all emitted files. */ 59 | // "removeComments": true, /* Disable emitting comments. */ 60 | // "noEmit": true, /* Disable emitting files from a compilation. */ 61 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 62 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ 63 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 64 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 65 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 66 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 67 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 68 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 69 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ 70 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ 71 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 72 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ 73 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 74 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 75 | 76 | /* Interop Constraints */ 77 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 78 | // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ 79 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 80 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ 81 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 82 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ 83 | 84 | /* Type Checking */ 85 | "strict": true, /* Enable all strict type-checking options. */ 86 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ 87 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ 88 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 89 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ 90 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 91 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ 92 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ 93 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 94 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ 95 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ 96 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 97 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 98 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 99 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ 100 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 101 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ 102 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 103 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 104 | 105 | /* Completeness */ 106 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 107 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /.kube/restful-booker-platform.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Namespace 4 | metadata: 5 | labels: 6 | kubernetes.io/metadata.name: restful-booker-platform 7 | name: restful-booker-platform 8 | 9 | --- 10 | apiVersion: v1 11 | kind: Service 12 | metadata: 13 | labels: 14 | app.kubernetes.io/name: rbp-assets 15 | name: rbp-assets 16 | namespace: restful-booker-platform 17 | spec: 18 | ports: 19 | - name: http 20 | port: 3003 21 | protocol: TCP 22 | targetPort: 3003 23 | selector: 24 | app.kubernetes.io/name: rbp-assets 25 | status: 26 | loadBalancer: {} 27 | 28 | --- 29 | apiVersion: v1 30 | kind: Service 31 | metadata: 32 | labels: 33 | app.kubernetes.io/name: rbp-auth 34 | name: rbp-auth 35 | namespace: restful-booker-platform 36 | spec: 37 | ports: 38 | - name: http 39 | port: 3004 40 | protocol: TCP 41 | targetPort: 3004 42 | selector: 43 | app.kubernetes.io/name: rbp-auth 44 | status: 45 | loadBalancer: {} 46 | 47 | --- 48 | apiVersion: v1 49 | kind: Service 50 | metadata: 51 | labels: 52 | app.kubernetes.io/name: rbp-booking 53 | name: rbp-booking 54 | namespace: restful-booker-platform 55 | spec: 56 | ports: 57 | - name: http 58 | port: 3000 59 | protocol: TCP 60 | targetPort: 3000 61 | selector: 62 | app.kubernetes.io/name: rbp-booking 63 | status: 64 | loadBalancer: {} 65 | 66 | --- 67 | apiVersion: v1 68 | kind: Service 69 | metadata: 70 | labels: 71 | app.kubernetes.io/name: rbp-branding 72 | name: rbp-branding 73 | namespace: restful-booker-platform 74 | spec: 75 | ports: 76 | - name: http 77 | port: 3002 78 | protocol: TCP 79 | targetPort: 3002 80 | selector: 81 | app.kubernetes.io/name: rbp-branding 82 | status: 83 | loadBalancer: {} 84 | 85 | --- 86 | apiVersion: v1 87 | kind: Service 88 | metadata: 89 | labels: 90 | app.kubernetes.io/name: rbp-message 91 | name: rbp-message 92 | namespace: restful-booker-platform 93 | spec: 94 | ports: 95 | - name: http 96 | port: 3006 97 | protocol: TCP 98 | targetPort: 3006 99 | selector: 100 | app.kubernetes.io/name: rbp-message 101 | status: 102 | loadBalancer: {} 103 | 104 | --- 105 | apiVersion: v1 106 | kind: Service 107 | metadata: 108 | labels: 109 | app.kubernetes.io/name: rbp-proxy 110 | name: rbp-proxy 111 | namespace: restful-booker-platform 112 | spec: 113 | type: NodePort 114 | ports: 115 | - name: http 116 | port: 80 117 | protocol: TCP 118 | nodePort: 80 119 | targetPort: 80 120 | selector: 121 | app.kubernetes.io/name: rbp-proxy 122 | status: 123 | loadBalancer: {} 124 | 125 | --- 126 | apiVersion: v1 127 | kind: Service 128 | metadata: 129 | labels: 130 | app.kubernetes.io/name: rbp-report 131 | name: rbp-report 132 | namespace: restful-booker-platform 133 | spec: 134 | ports: 135 | - name: http 136 | port: 3005 137 | protocol: TCP 138 | targetPort: 3005 139 | selector: 140 | app.kubernetes.io/name: rbp-report 141 | status: 142 | loadBalancer: {} 143 | 144 | --- 145 | apiVersion: v1 146 | kind: Service 147 | metadata: 148 | labels: 149 | app.kubernetes.io/name: rbp-room 150 | name: rbp-room 151 | namespace: restful-booker-platform 152 | spec: 153 | ports: 154 | - name: http 155 | port: 3001 156 | protocol: TCP 157 | targetPort: 3001 158 | selector: 159 | app.kubernetes.io/name: rbp-room 160 | status: 161 | loadBalancer: {} 162 | 163 | --- 164 | apiVersion: apps/v1 165 | kind: Deployment 166 | metadata: 167 | labels: 168 | app.kubernetes.io/name: rbp-assets 169 | name: rbp-assets 170 | namespace: restful-booker-platform 171 | spec: 172 | replicas: 1 173 | selector: 174 | matchLabels: 175 | app.kubernetes.io/name: rbp-assets 176 | strategy: {} 177 | template: 178 | metadata: 179 | labels: 180 | network.kubernetes.io/restful-booker-platform: "true" 181 | app.kubernetes.io/name: rbp-assets 182 | spec: 183 | containers: 184 | - name: rbp-assets 185 | image: mwinteringham/restfulbookerplatform_assets:1.6.24c7b22 186 | imagePullPolicy: Always 187 | ports: 188 | - containerPort: 3003 189 | resources: {} 190 | readinessProbe: 191 | httpGet: 192 | port: 3003 193 | path: /actuator/health 194 | initialDelaySeconds: 1 195 | periodSeconds: 5 196 | timeoutSeconds: 4 197 | successThreshold: 2 198 | failureThreshold: 3 199 | restartPolicy: Always 200 | status: {} 201 | 202 | --- 203 | apiVersion: networking.k8s.io/v1 204 | kind: NetworkPolicy 205 | metadata: 206 | name: restful-booker-platform 207 | namespace: restful-booker-platform 208 | spec: 209 | ingress: 210 | - from: 211 | - podSelector: 212 | matchLabels: 213 | network.kubernetes.io/restful-booker-platform: "true" 214 | podSelector: 215 | matchLabels: 216 | network.kubernetes.io/restful-booker-platform: "true" 217 | 218 | --- 219 | apiVersion: apps/v1 220 | kind: Deployment 221 | metadata: 222 | labels: 223 | app.kubernetes.io/name: rbp-auth 224 | name: rbp-auth 225 | namespace: restful-booker-platform 226 | spec: 227 | replicas: 1 228 | selector: 229 | matchLabels: 230 | app.kubernetes.io/name: rbp-auth 231 | strategy: {} 232 | template: 233 | metadata: 234 | labels: 235 | network.kubernetes.io/restful-booker-platform: "true" 236 | app.kubernetes.io/name: rbp-auth 237 | spec: 238 | containers: 239 | - name: rbp-auth 240 | image: mwinteringham/restfulbookerplatform_auth:1.6.24c7b22 241 | imagePullPolicy: Always 242 | ports: 243 | - containerPort: 3004 244 | resources: {} 245 | readinessProbe: 246 | httpGet: 247 | port: 3004 248 | path: /auth/actuator/health 249 | initialDelaySeconds: 1 250 | periodSeconds: 5 251 | timeoutSeconds: 4 252 | successThreshold: 2 253 | failureThreshold: 3 254 | restartPolicy: Always 255 | status: {} 256 | 257 | --- 258 | apiVersion: apps/v1 259 | kind: Deployment 260 | metadata: 261 | labels: 262 | app.kubernetes.io/name: rbp-booking 263 | name: rbp-booking 264 | namespace: restful-booker-platform 265 | spec: 266 | replicas: 1 267 | selector: 268 | matchLabels: 269 | app.kubernetes.io/name: rbp-booking 270 | strategy: {} 271 | template: 272 | metadata: 273 | labels: 274 | network.kubernetes.io/restful-booker-platform: "true" 275 | app.kubernetes.io/name: rbp-booking 276 | spec: 277 | containers: 278 | - name: rbp-booking 279 | image: mwinteringham/restfulbookerplatform_booking:1.6.24c7b22 280 | imagePullPolicy: Always 281 | ports: 282 | - containerPort: 3000 283 | resources: {} 284 | readinessProbe: 285 | httpGet: 286 | port: 3000 287 | path: /booking/actuator/health 288 | initialDelaySeconds: 1 289 | periodSeconds: 5 290 | timeoutSeconds: 4 291 | successThreshold: 2 292 | failureThreshold: 3 293 | restartPolicy: Always 294 | status: {} 295 | 296 | --- 297 | apiVersion: apps/v1 298 | kind: Deployment 299 | metadata: 300 | labels: 301 | app.kubernetes.io/name: rbp-branding 302 | name: rbp-branding 303 | namespace: restful-booker-platform 304 | spec: 305 | replicas: 1 306 | selector: 307 | matchLabels: 308 | app.kubernetes.io/name: rbp-branding 309 | strategy: {} 310 | template: 311 | metadata: 312 | labels: 313 | network.kubernetes.io/restful-booker-platform: "true" 314 | app.kubernetes.io/name: rbp-branding 315 | spec: 316 | containers: 317 | - name: rbp-branding 318 | image: mwinteringham/restfulbookerplatform_branding:1.6.24c7b22 319 | imagePullPolicy: Always 320 | ports: 321 | - containerPort: 3002 322 | resources: {} 323 | readinessProbe: 324 | httpGet: 325 | port: 3002 326 | path: /branding/actuator/health 327 | initialDelaySeconds: 1 328 | periodSeconds: 5 329 | timeoutSeconds: 4 330 | successThreshold: 2 331 | failureThreshold: 3 332 | restartPolicy: Always 333 | status: {} 334 | 335 | --- 336 | apiVersion: apps/v1 337 | kind: Deployment 338 | metadata: 339 | labels: 340 | app.kubernetes.io/name: rbp-message 341 | name: rbp-message 342 | namespace: restful-booker-platform 343 | spec: 344 | replicas: 1 345 | selector: 346 | matchLabels: 347 | app.kubernetes.io/name: rbp-message 348 | strategy: {} 349 | template: 350 | metadata: 351 | labels: 352 | network.kubernetes.io/restful-booker-platform: "true" 353 | app.kubernetes.io/name: rbp-message 354 | spec: 355 | containers: 356 | - name: rbp-message 357 | image: mwinteringham/restfulbookerplatform_message:1.6.24c7b22 358 | imagePullPolicy: Always 359 | ports: 360 | - containerPort: 3006 361 | resources: {} 362 | readinessProbe: 363 | httpGet: 364 | port: 3006 365 | path: /message/actuator/health 366 | initialDelaySeconds: 1 367 | periodSeconds: 5 368 | timeoutSeconds: 4 369 | successThreshold: 2 370 | failureThreshold: 3 371 | restartPolicy: Always 372 | status: {} 373 | 374 | --- 375 | apiVersion: apps/v1 376 | kind: Deployment 377 | metadata: 378 | labels: 379 | app.kubernetes.io/name: rbp-proxy 380 | name: rbp-proxy 381 | namespace: restful-booker-platform 382 | spec: 383 | replicas: 1 384 | selector: 385 | matchLabels: 386 | app.kubernetes.io/name: rbp-proxy 387 | strategy: {} 388 | template: 389 | metadata: 390 | labels: 391 | network.kubernetes.io/restful-booker-platform: "true" 392 | app.kubernetes.io/name: rbp-proxy 393 | spec: 394 | containers: 395 | - name: rbp-proxy 396 | image: mwinteringham/restfulbookerplatform_proxy:latest 397 | imagePullPolicy: Always 398 | ports: 399 | - containerPort: 80 400 | resources: {} 401 | restartPolicy: Always 402 | status: {} 403 | 404 | --- 405 | apiVersion: apps/v1 406 | kind: Deployment 407 | metadata: 408 | labels: 409 | app.kubernetes.io/name: rbp-report 410 | name: rbp-report 411 | namespace: restful-booker-platform 412 | spec: 413 | replicas: 1 414 | selector: 415 | matchLabels: 416 | app.kubernetes.io/name: rbp-report 417 | strategy: {} 418 | template: 419 | metadata: 420 | labels: 421 | network.kubernetes.io/restful-booker-platform: "true" 422 | app.kubernetes.io/name: rbp-report 423 | spec: 424 | containers: 425 | - name: rbp-report 426 | image: mwinteringham/restfulbookerplatform_report:1.6.24c7b22 427 | imagePullPolicy: Always 428 | ports: 429 | - containerPort: 3005 430 | resources: {} 431 | readinessProbe: 432 | httpGet: 433 | port: 3005 434 | path: /report/actuator/health 435 | initialDelaySeconds: 1 436 | periodSeconds: 5 437 | timeoutSeconds: 4 438 | successThreshold: 2 439 | failureThreshold: 3 440 | restartPolicy: Always 441 | status: {} 442 | 443 | --- 444 | apiVersion: apps/v1 445 | kind: Deployment 446 | metadata: 447 | labels: 448 | app.kubernetes.io/name: rbp-room 449 | name: rbp-room 450 | namespace: restful-booker-platform 451 | spec: 452 | replicas: 1 453 | selector: 454 | matchLabels: 455 | app.kubernetes.io/name: rbp-room 456 | strategy: {} 457 | template: 458 | metadata: 459 | labels: 460 | network.kubernetes.io/restful-booker-platform: "true" 461 | app.kubernetes.io/name: rbp-room 462 | spec: 463 | containers: 464 | - name: rbp-room 465 | image: mwinteringham/restfulbookerplatform_room:1.6.24c7b22 466 | imagePullPolicy: Always 467 | ports: 468 | - containerPort: 3001 469 | resources: {} 470 | readinessProbe: 471 | httpGet: 472 | port: 3001 473 | path: /room/actuator/health 474 | initialDelaySeconds: 1 475 | periodSeconds: 5 476 | timeoutSeconds: 4 477 | successThreshold: 2 478 | failureThreshold: 3 479 | restartPolicy: Always 480 | status: {} 481 | 482 | -------------------------------------------------------------------------------- /tests/front-page/room-booking.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@playwright/test'; 2 | import { faker } from '@faker-js/faker'; 3 | import { AuthApi } from '../../apis/AuthApi'; 4 | import { FrontPage } from '../../pages/FrontPage'; 5 | import { RoomApi } from '../../apis/RoomApi'; 6 | import { RoomAmenities, RoomType } from '../../pages/RoomsPage'; 7 | import { invalidEmails } from '../../utils/test-data-util'; 8 | 9 | test.describe('Room Booking Tests', () => { 10 | let frontPage: FrontPage; 11 | 12 | let authApi: AuthApi; 13 | let roomApi: RoomApi; 14 | 15 | const roomName: string = faker.number.int({ min: 100, max: 999 }).toString(); 16 | const roomType: RoomType = faker.helpers.arrayElement([RoomType.SINGLE, RoomType.TWIN, RoomType.DOUBLE, RoomType.FAMILY, RoomType.SUITE]); 17 | const roomIsAccessible: boolean = faker.datatype.boolean(); 18 | const roomPrice: number = faker.number.int({ min: 100, max: 999 }); 19 | const roomAmenities: RoomAmenities = { 20 | wifi: faker.datatype.boolean(), 21 | tv: faker.datatype.boolean(), 22 | radio: faker.datatype.boolean(), 23 | refreshments: faker.datatype.boolean(), 24 | safe: faker.datatype.boolean(), 25 | views: faker.datatype.boolean() 26 | }; 27 | 28 | test.beforeEach(async ({ page, request, baseURL }) => { 29 | frontPage = new FrontPage(page); 30 | 31 | authApi = new AuthApi(request); 32 | roomApi = new RoomApi(request); 33 | 34 | await frontPage.hideBanner(baseURL); 35 | await authApi.login('admin', 'password'); 36 | await roomApi.createRoom(roomName, roomType, roomIsAccessible, roomPrice, roomAmenities); 37 | 38 | await frontPage.goto(); 39 | }); 40 | 41 | test('Visitor must be able to book a room for available dates by filling up all mandatory fields @sanity @booking', async () => { 42 | const firstName = faker.person.firstName(); 43 | const lastName = faker.person.lastName(); 44 | const email = faker.internet.email(); 45 | const phoneNumber = faker.phone.number(); 46 | await frontPage.bookRoom(roomName, firstName, lastName, email, phoneNumber); 47 | 48 | const bookingSuccessMessage = 'Booking Successful!'; 49 | const bookingConfirmedMessage = 'Congratulations! Your booking has been confirmed'; 50 | await expect(frontPage.bookingConfirmationModal, 'Booking Confirmation modal is displayed').toBeVisible(); 51 | await expect(frontPage.bookingConfirmationModal, `Booking Success Message '${bookingSuccessMessage}' is displayed`).toContainText( 52 | bookingSuccessMessage 53 | ); 54 | await expect(frontPage.bookingConfirmationModal, `Booking Confirmed Message '${bookingConfirmedMessage}' is displayed`).toContainText( 55 | bookingConfirmedMessage 56 | ); 57 | }); 58 | 59 | test('Visitor must NOT be able to book a room without filling up first name field @booking', async () => { 60 | const lastName = faker.person.lastName(); 61 | const email = faker.internet.email(); 62 | const phoneNumber = faker.phone.number(); 63 | await frontPage.bookRoom(roomName, '', lastName, email, phoneNumber); 64 | 65 | const mandatoryMessage = 'Firstname should not be blank'; 66 | const validationMessage = 'size must be between 3 and 18'; 67 | await expect(frontPage.bookingErrorMessages, 'Error Messages are displayed').toBeVisible(); 68 | await expect(frontPage.bookingErrorMessages, `Mandatory Message '${mandatoryMessage}' is displayed`).toContainText(mandatoryMessage); 69 | await expect(frontPage.bookingErrorMessages, `Validation Message '${validationMessage}' is displayed`).toContainText(validationMessage); 70 | }); 71 | 72 | for (const firstNameLength of [2, 19]) { 73 | test(`Visitor must NOT be able to book a room by filling up the first name with invalid length value of ${firstNameLength}, less than 3 and more than 18 characters @booking`, async () => { 74 | const firstName = faker.string.alphanumeric(firstNameLength); 75 | const lastName = faker.person.lastName(); 76 | const email = faker.internet.email(); 77 | const phoneNumber = faker.phone.number(); 78 | await frontPage.bookRoom(roomName, firstName, lastName, email, phoneNumber); 79 | 80 | const validationMessage = 'size must be between 3 and 18'; 81 | await expect(frontPage.bookingErrorMessages, 'Error Messages are displayed').toBeVisible(); 82 | await expect(frontPage.bookingErrorMessages, `Validation Message '${validationMessage}' is displayed`).toContainText(validationMessage); 83 | }); 84 | } 85 | 86 | for (const firstNameLength of [3, 18]) { 87 | test(`Visitor must be able to book a room by filling up the first name with valid length value of ${firstNameLength}, more than 3 and less than 18 characters @booking`, async () => { 88 | const firstName = faker.string.alphanumeric(firstNameLength); 89 | const lastName = faker.person.lastName(); 90 | const email = faker.internet.email(); 91 | const phoneNumber = faker.phone.number(); 92 | await frontPage.bookRoom(roomName, firstName, lastName, email, phoneNumber); 93 | 94 | const bookingSuccessMessage = 'Booking Successful!'; 95 | const bookingConfirmedMessage = 'Congratulations! Your booking has been confirmed'; 96 | await expect(frontPage.bookingConfirmationModal, 'Booking Confirmation modal is displayed').toBeVisible(); 97 | await expect(frontPage.bookingConfirmationModal, `Booking Success Message '${bookingSuccessMessage}' is displayed`).toContainText( 98 | bookingSuccessMessage 99 | ); 100 | await expect(frontPage.bookingConfirmationModal, `Booking Confirmed Message '${bookingConfirmedMessage}' is displayed`).toContainText( 101 | bookingConfirmedMessage 102 | ); 103 | }); 104 | } 105 | 106 | test('Visitor must NOT be able to book a room without filling up last name field @booking', async () => { 107 | const firstName = faker.person.firstName(); 108 | const email = faker.internet.email(); 109 | const phoneNumber = faker.phone.number(); 110 | await frontPage.bookRoom(roomName, firstName, '', email, phoneNumber); 111 | 112 | const mandatoryMessage = 'Lastname should not be blank'; 113 | const validationMessage = 'size must be between 3 and 30'; 114 | await expect(frontPage.bookingErrorMessages, 'Error Messages are displayed').toBeVisible(); 115 | await expect(frontPage.bookingErrorMessages, `Mandatory Message '${mandatoryMessage}' is displayed`).toContainText(mandatoryMessage); 116 | await expect(frontPage.bookingErrorMessages, `Validation Message '${validationMessage}' is displayed`).toContainText(validationMessage); 117 | }); 118 | 119 | for (const lastNameLength of [2, 31]) { 120 | test(`Visitor must NOT be able to book a room by filling up the last name with invalid length value of ${lastNameLength}, less than 3 and more than 30 characters @booking`, async () => { 121 | const firstName = faker.person.firstName(); 122 | const lastName = faker.string.alphanumeric(lastNameLength); 123 | const email = faker.internet.email(); 124 | const phoneNumber = faker.phone.number(); 125 | await frontPage.bookRoom(roomName, firstName, lastName, email, phoneNumber); 126 | 127 | const validationMessage = 'size must be between 3 and 30'; 128 | await expect(frontPage.bookingErrorMessages, 'Error Messages are displayed').toBeVisible(); 129 | await expect(frontPage.bookingErrorMessages, `Validation Message '${validationMessage}' is displayed`).toContainText(validationMessage); 130 | }); 131 | } 132 | 133 | for (const lastNameLength of [3, 30]) { 134 | test(`Visitor must be able to book a room by filling up the last name with valid length value of ${lastNameLength}, more than 3 and less than 30 characters @booking`, async () => { 135 | const firstName = faker.person.firstName(); 136 | const lastName = faker.string.alphanumeric(lastNameLength); 137 | const email = faker.internet.email(); 138 | const phoneNumber = faker.phone.number(); 139 | await frontPage.bookRoom(roomName, firstName, lastName, email, phoneNumber); 140 | 141 | const bookingSuccessMessage = 'Booking Successful!'; 142 | const bookingConfirmedMessage = 'Congratulations! Your booking has been confirmed'; 143 | await expect(frontPage.bookingConfirmationModal, 'Booking Confirmation modal is displayed').toBeVisible(); 144 | await expect(frontPage.bookingConfirmationModal, `Booking Success Message '${bookingSuccessMessage}' is displayed`).toContainText( 145 | bookingSuccessMessage 146 | ); 147 | await expect(frontPage.bookingConfirmationModal, `Booking Confirmed Message '${bookingConfirmedMessage}' is displayed`).toContainText( 148 | bookingConfirmedMessage 149 | ); 150 | }); 151 | } 152 | 153 | test('Visitor must NOT be able to book a room without filling up email field @booking', async () => { 154 | const firstName = faker.person.firstName(); 155 | const lastName = faker.person.lastName(); 156 | const phoneNumber = faker.phone.number(); 157 | await frontPage.bookRoom(roomName, firstName, lastName, '', phoneNumber); 158 | 159 | const mandatoryMessage = 'must not be empty'; 160 | await expect(frontPage.bookingErrorMessages, 'Error Messages are displayed').toBeVisible(); 161 | await expect(frontPage.bookingErrorMessages, `Mandatory Message '${mandatoryMessage}' is displayed`).toContainText(mandatoryMessage); 162 | }); 163 | 164 | for (const invalidEmail of invalidEmails()) { 165 | test(`Visitor must NOT be able to book a room by filling up email with invalid value: ${invalidEmail} @booking`, async () => { 166 | // eslint-disable-next-line playwright/no-skipped-test 167 | test.skip(invalidEmail == 'email@example', 'Know issue'); 168 | const firstName = faker.person.firstName(); 169 | const lastName = faker.person.lastName(); 170 | const phoneNumber = faker.phone.number(); 171 | const email = invalidEmail; 172 | await frontPage.bookRoom(roomName, firstName, lastName, email, phoneNumber); 173 | 174 | const validationMessage = 'must be a well-formed email address'; 175 | await expect(frontPage.bookingErrorMessages, 'Error Messages are displayed').toBeVisible(); 176 | await expect(frontPage.bookingErrorMessages, `Validation Message '${validationMessage}' is displayed`).toContainText(validationMessage); 177 | }); 178 | } 179 | 180 | test('Visitor must NOT be able to book a room without filling up phone field @booking', async () => { 181 | const firstName = faker.person.firstName(); 182 | const lastName = faker.person.lastName(); 183 | const email = faker.internet.email(); 184 | await frontPage.bookRoom(roomName, firstName, lastName, email, ''); 185 | 186 | const mandatoryMessage = 'must not be empty'; 187 | const validationMessage = 'size must be between 11 and 21'; 188 | await expect(frontPage.bookingErrorMessages, 'Error Messages are displayed').toBeVisible(); 189 | await expect(frontPage.bookingErrorMessages, `Mandatory Message '${mandatoryMessage}' is displayed`).toContainText(mandatoryMessage); 190 | await expect(frontPage.bookingErrorMessages, `Validation Message '${validationMessage}' is displayed`).toContainText(validationMessage); 191 | }); 192 | 193 | for (const phoneLength of [10, 22]) { 194 | test(`Visitor must NOT be able to book a room by filling up the phone with invalid length value of ${phoneLength}, less than 11 and more than 21 characters @booking`, async () => { 195 | const firstName = faker.person.firstName(); 196 | const lastName = faker.person.lastName(); 197 | const email = faker.internet.email(); 198 | const phoneNumber = faker.string.numeric(phoneLength); 199 | await frontPage.bookRoom(roomName, firstName, lastName, email, phoneNumber); 200 | 201 | const validationMessage = 'size must be between 11 and 21'; 202 | await expect(frontPage.bookingErrorMessages, 'Error Messages are displayed').toBeVisible(); 203 | await expect(frontPage.bookingErrorMessages, `Validation Message '${validationMessage}' is displayed`).toContainText(validationMessage); 204 | }); 205 | } 206 | 207 | for (const phoneLength of [11, 21]) { 208 | test(`Visitor must be able to book a room by filling up the phone with valid length value of ${phoneLength}, more than 11 and less than 21 characters @booking`, async () => { 209 | const firstName = faker.person.firstName(); 210 | const lastName = faker.person.lastName(); 211 | const email = faker.internet.email(); 212 | const phoneNumber = faker.string.numeric(phoneLength); 213 | await frontPage.bookRoom(roomName, firstName, lastName, email, phoneNumber); 214 | 215 | const bookingSuccessMessage = 'Booking Successful!'; 216 | const bookingConfirmedMessage = 'Congratulations! Your booking has been confirmed'; 217 | await expect(frontPage.bookingConfirmationModal, 'Booking Confirmation modal is displayed').toBeVisible(); 218 | await expect(frontPage.bookingConfirmationModal, `Booking Success Message '${bookingSuccessMessage}' is displayed`).toContainText( 219 | bookingSuccessMessage 220 | ); 221 | await expect(frontPage.bookingConfirmationModal, `Booking Confirmed Message '${bookingConfirmedMessage}' is displayed`).toContainText( 222 | bookingConfirmedMessage 223 | ); 224 | }); 225 | } 226 | 227 | test.afterEach(async () => { 228 | await roomApi.deleteAllRooms(roomName); 229 | }); 230 | }); 231 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Playwright E2E Testing Framework 2 | 3 | Playwright E2E Testing Framework project represents a starting point for writing tests in Playwright. 4 | 5 | Provided tests are based on examples how to define and use utility functions, explicit wait for some element, usage of **faker** for generating random data and possible solutions for organizing tests using Page Object pattern. 6 | 7 | ## IDE Setup 8 | 9 | - Install [Visual Studio Code](https://code.visualstudio.com/download) 10 | - _Recommended extensions in Visual Studio Code:_ 11 | - [Playwright Test for VSCode](https://marketplace.visualstudio.com/items?itemName=ms-playwright.playwright) 12 | - [ESLint]() 13 | - [Prettier - Code formatter](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) 14 | - [Local History](https://marketplace.visualstudio.com/items?itemName=xyz.local-history) 15 | - [GitLens](https://marketplace.visualstudio.com/items?itemName=eamodio.gitlens) 16 | - [Markdownlint](https://marketplace.visualstudio.com/items?itemName=DavidAnson.vscode-markdownlint) 17 | - Install [Node JS](https://nodejs.org/en/download/) 18 | - Clone the repository to your local system 19 | - Open the project in Visual Studio Code and open the terminal 20 | - Make sure the path to the project is correct `\playwright-e2e-tests` 21 | - In the terminal, execute the following command: ```npm install``` 22 | - The command will install all found in the package.json 23 | 24 | ## Used Libraries 25 | 26 | - [Playwright](https://github.com/microsoft/playwright) 27 | - [Monocart Reporter](https://github.com/cenfun/monocart-reporter) 28 | - [Allure Reporter](https://github.com/allure-framework/allure-js) 29 | - [Faker JS](https://github.com/faker-js/faker) 30 | - [Prettier](https://prettier.io/) 31 | - [ESLint](https://github.com/eslint/eslint) 32 | - [Husky](https://typicode.github.io/husky/#/) 33 | - [Lint Staged](https://github.com/okonet/lint-staged) 34 | 35 | ## Launch Playwright and Execute Test Cases 36 | 37 | Open the terminal inside `\playwright-e2e-tests` and use the following commands to: 38 | 39 | - Open the Playwright UI to execute test cases against default environment: `npx playwright test --ui` 40 | - Execute all test cases without opening the Playwright UI against default environment: `npx playwright test` 41 | - Environment variables: 42 | - `ENV`, which can have value `prod` / `local` / `docker` / `kube` / `kubeLocal` , depending on which environment you would like to execute your tests (if not defined, `prod` will be used by default) 43 | - `prod` uses `https://automationintesting.online` as app URL 44 | - `local` uses `http://localhost` as app URL 45 | - `kubeLocal` uses `http://kube.local` as app URL 46 | - `docker` uses `http://rbp-proxy` as app URL 47 | - `kube` uses `http://rbp-proxy.restful-booker-platform` as app URL 48 | - Test filtering using Tags: 49 | - If not set all tests will be executed. Filtering tests using Tags is done with `--grep` and `--grep-invert` command line flags 50 | - `npx playwright test --grep "@sanity"` - Tests tagged with `@sanity` will be filtered 51 | - `npx playwright test --grep-invert "@room-management"` - Tests that are not tagged with `@room-management` will be filtered 52 | - `npx playwright test --grep "(?=.*@management)(?=.*@room-management)"` - Tests tagged with both `@management` and `@room-management` will be filtered 53 | - `npx playwright test --grep "@booking|@contact"` - Tests tagged with either `@booking` or `@contact` will be filtered 54 | 55 | Example of above commands with possible variables: 56 | 57 | - `ENV=local npx playwright test --ui` - Open Playwright UI to execute tests against Local environment 58 | - `ENV=prod npx playwright test` - Execute All tests without opening the Playwright UI against Production environment in all setup projects (browsers) 59 | - `ENV=local npx playwright test tests/admin-panel/login.spec.ts` - Execute Login tests without opening the Playwright UI on Local environment in all setup projects (browsers) 60 | - `ENV=prod npx playwright test --grep "@booking|@contact"` - Execute tests tagged with `@booking` or `@contact`, without opening the Playwright UI on Production environment in all setup projects (browsers) 61 | - `ENV=prod npx playwright test --grep-invert "@room-management" --project chromium` - Execute tests not tagged with `@room-management`, without opening the Playwright UI on Production environment only on `chromium` project (browser) 62 | 63 | ## Local Docker Environment with Docker for Desktop 64 | 65 | > Before you proceed, you should install Docker Desktop depending on your OS and start it: 66 | > 67 | >- [Docker Desktop for Windows](https://docs.docker.com/desktop/install/windows-install/) 68 | >- [Docker Desktop for Mac](https://docs.docker.com/desktop/install/mac-install/) 69 | > 70 | > As Docker for Desktop is **paid** software now, instead of it you can set up and start minikube using bellow guides: 71 | > 72 | >- [Minikube Setup for Windows](/docs/minikube-setup-windows.md) 73 | >- [Minikube Setup for Mac](/docs/minikube-setup-mac.md) 74 | 75 | After Docker has been installed on your machine, open the terminal inside `\playwright-e2e-tests` and use the following command: 76 | 77 | docker compose -f ./docker-compose-restful-booker.yml up -d 78 | 79 | That will start Restful Booker Platform locally. 80 | 81 | After everything is up and running you will have Restful Booker Platform available at: 82 | 83 | - Docker for Desktop: `http://localhost` 84 | - minikube: `http://kube.local` 85 | 86 | ## Local Kubernetes Environment with Minikube's Kubernetes 87 | 88 | > Before you proceed, you should set up and start minikube using bellow guides: 89 | > 90 | >- [Minikube Setup for Windows](/docs/minikube-setup-windows.md) 91 | >- [Minikube Setup for Mac](/docs/minikube-setup-mac.md) 92 | 93 | After minikube has been properly installed and started on your machine, open the terminal inside `\playwright-e2e-tests` and use the following command: 94 | 95 | kubectl apply -f .kube/restful-booker-platform.yml 96 | 97 | That will start Restful Booker Platform locally. 98 | 99 | After everything is up and running you will have Restful Booker Platform available at `http://kube.local`. 100 | 101 | ## Execute Playwright E2E Tests using GitHub Actions Workflows on GitHub 102 | 103 | All GitHub Actions Workflows are configured in [**GitHub Folder**](/.github/workflows) yml files. 104 | 105 | They all can be found by navigating to [GitHub Repository > Actions](https://github.com/m-pujic-levi9-com/playwright-e2e-tests/actions). 106 | 107 | There are 3 GitHub Actions Workflows setup for Playwright E2E Tests repository: 108 | 109 | - [Playwright Tests](https://github.com/m-pujic-levi9-com/playwright-e2e-tests/actions/workflows/playwright.yml) 110 | - [Check - Sanity Test](https://github.com/m-pujic-levi9-com/playwright-e2e-tests/actions/workflows/check-sanity.yml) 111 | - [Check - Changed Test](https://github.com/m-pujic-levi9-com/playwright-e2e-tests/actions/workflows/check-changed.yml) 112 | 113 | --- 114 | 115 | ### Playwright Tests 116 | 117 | This workflow is designed to run comprehensive automated tests using Playwright across multiple browsers. It Executes All Playwright E2E Tests on `local` environment using `chromium`, `firefox` and `webkit` browsers from defined branch (by default it is `main`). It includes parallel test execution, detailed reporting, and scheduled runs to maintain the reliability. 118 | 119 | Status of all ongoing and previously executed `Playwright Tests` Workflow runs can be found [here](https://github.com/m-pujic-levi9-com/playwright-e2e-tests/actions/workflows/playwright.yml). 120 | 121 | GitHub Action Workflow configuration file of this workflow is [playwright.yml](/.github/workflows/playwright.yml). 122 | 123 | #### Playwright Tests: Key Features 124 | 125 | - **Trigger Events**: 126 | - Manually triggered via `workflow_dispatch`. 127 | - Automatically scheduled to run every Monday at 9:00 AM (UTC). 128 | 129 | - **Setup and Dependencies**: 130 | - Installs Node.js and caches Node modules for faster execution. 131 | - Prepares Playwright binaries and their system dependencies. 132 | - Validates the codebase with TypeScript type checks. 133 | 134 | - **Test Execution**: 135 | - Executes tests for `chromium`, `firefox`, and `webkit` browsers. 136 | - Implements test sharding to run tests concurrently, improving efficiency. 137 | 138 | - **Test Environment**: 139 | - Deploys multiple Docker-based microservices to emulate the testing environment, ensuring tests run against a realistic backend setup. 140 | 141 | - **Reporting**: 142 | - Generates multiple types of test reports, including HTML, Monocart, and Allure. 143 | - Merges test results and publishes them to GitHub Pages for easy access. 144 | 145 | #### Playwright Tests: Workflow Overview 146 | 147 | - **Install Job** 148 | - Sets up the environment, installs dependencies, and validates the codebase with type checks. 149 | - **Test Job** 150 | - Executes Playwright tests in parallel across browsers and shards. 151 | - Deploys Docker services to replicate the application environment. 152 | - Uploads reports and test results as artifacts for post-test analysis. 153 | - **Report Job** 154 | - Consolidates and merges test reports from parallel runs. 155 | - Generates Playwright HTML, Monocart, and Allure reports. 156 | - Publishes the reports to GitHub Pages for easy sharing and review. 157 | - Cleans up unnecessary artifacts to optimize storage usage. 158 | 159 | #### Playwright Tests: Usage 160 | 161 | To trigger the workflow: 162 | 163 | - **Manual Run:** Go to the [Actions](https://github.com/m-pujic-levi9-com/playwright-e2e-tests/actions) tab in your GitHub repository, select the [Playwright Tests](https://github.com/m-pujic-levi9-com/playwright-e2e-tests/actions/workflows/playwright.yml) workflow, and click `Run workflow`. 164 | - :warning: This GitHub Actions publishes report on GitHub pages and because of that it can **ONLY** be executed on **main** branch. :warning: 165 | - **Scheduled Run:** The workflow automatically runs every Monday at 9:00 AM UTC. 166 | 167 | #### Playwright Tests: Reports 168 | 169 | - **Playwright HTML Report:** Test report without execution history and trends. 170 | - **Monocart Report:** Test report with execution history and trends.. 171 | - **Allure Report:** Test report with execution history and trends. 172 | 173 | Access the reports via the GitHub Pages link provided in the workflow logs after execution, or click [here](https://m-pujic-levi9-com.github.io/playwright-e2e-tests/). 174 | 175 | --- 176 | 177 | ### Check - Sanity Tests 178 | 179 | This GitHub Action ensures that essential functionality remains intact. It runs `@sanity` tagged tests on `local` environment using `chromium`, `firefox` and `webkit` browsers. It is triggered automatically on changes to specified parts of the repository or during pull requests targeting the `main` branch. 180 | 181 | Status of all ongoing and previously executed `Check - Sanity Tests` Workflow runs can be found [here](https://github.com/m-pujic-levi9-com/playwright-e2e-tests/actions/workflows/check-sanity.yml). 182 | 183 | GitHub Action Workflow configuration file of this workflow is [check-sanity.yml](/.github/workflows/check-sanity.yml). 184 | 185 | #### Check - Sanity Tests: Key Features 186 | 187 | - **Trigger Events**: 188 | The workflow executes on: 189 | - Push events to the `main` branch, specifically when changes occur in: 190 | - Directories: `apis/`, `pages/`, `tests/`, `utils/` 191 | - Workflow file: `.github/workflows/sanity-check.yml` 192 | - Configuration files: `playwright.config.ts`, `package-lock.json`, `package.json` 193 | - Pull request events targeting the `main` branch: 194 | - Opened 195 | - Reopened 196 | - Synchronized 197 | - Labeled 198 | 199 | - **Setup and Dependencies**: 200 | - Installs Node.js and caches Node modules for faster subsequent runs. 201 | - Installs and caches Playwright binaries, along with their system dependencies. 202 | - Runs TypeScript type checks to validate code integrity. 203 | 204 | - **Test Environment**: 205 | - Deploys multiple Docker-based microservices to emulate the testing environment, ensuring tests run against a realistic backend setup. 206 | 207 | - **Test Execution**: 208 | - Leverages Playwright to execute tests marked with the `@sanity` tag. 209 | - Ensures essential application flows are thoroughly verified. 210 | 211 | - **Reports**: 212 | - Automatically generates and uploads an HTML report for the sanity test results, accessible via workflow artifacts. 213 | 214 | #### Check - Sanity Tests: Workflow Overview 215 | 216 | - **Install Job** 217 | - Sets up the environment, installs dependencies, and ensures readiness for testing. 218 | - Verifies code correctness with TypeScript type checks. 219 | - **Test Job** 220 | - Deploys test environment by initializing a set of microservices using Docker. 221 | - Executes sanity tests using Playwright. 222 | - Uploads a detailed HTML test report. 223 | 224 | --- 225 | 226 | ### Check - Changed Tests 227 | 228 | This GitHub Action is designed to streamline the testing process for pull requests. It identifies only the tests affected by the introduced changes and executes them on `local` environment using `chromium`, `firefox` and `webkit` browsers. It is triggered automatically during pull requests targeting the `main` branch. 229 | 230 | Status of all ongoing and previously executed `Check - Changed Tests` Workflow runs can be found [here](https://github.com/m-pujic-levi9-com/playwright-e2e-tests/actions/workflows/check-changes.yml). 231 | 232 | GitHub Action Workflow configuration file of this workflow is [check-changes.yml](/.github/workflows/check-changes.yml). 233 | 234 | #### Check - Changed Tests: Key Features 235 | 236 | - **Trigger Events**: 237 | Automatically runs when a pull request targeting the `main` branch is: 238 | - Opened 239 | - Reopened 240 | - Synchronized 241 | - Labeled 242 | 243 | - **Setup and Dependencies**: 244 | - Performs a full repository checkout for accurate diff comparisons. 245 | - Sets up Node.js and caches Node modules to speed up subsequent runs. 246 | - Installs and caches Playwright binaries, along with their system dependencies. 247 | - Runs TypeScript type checks to validate code integrity. 248 | 249 | - **Test Environment**: 250 | - Deploys multiple Docker-based microservices to emulate the testing environment, ensuring tests run against a realistic backend setup. 251 | 252 | - **Test Execution**: 253 | - Leverages Playwright's `--only-changed` option to execute only the tests impacted by code changes, improving feedback time and reducing resource usage. 254 | 255 | - **Reports**: 256 | - Automatically generates and uploads an HTML report for Playwright test results, which can be accessed and reviewed directly from the workflow artifacts. 257 | 258 | #### Check - Changed Tests: Workflow Overview 259 | 260 | - **Install Job** 261 | - Sets up the environment, installs dependencies, and ensures everything is ready for test execution. 262 | - Runs TypeScript type checks to ensure code validity. 263 | 264 | - **Test Job** 265 | - Deploys test environment by initializing a set of microservices using Docker. 266 | - Executes Playwright Tests for the affected files. 267 | - Uploads a detailed HTML test report. 268 | --------------------------------------------------------------------------------