diff --git a/.env b/.env new file mode 100644 index 00000000..5a3f174a --- /dev/null +++ b/.env @@ -0,0 +1,9 @@ +BASE_URL=http://localhost:4200/ +TIMEOUT=20 +EXPECT_TIMEOUT=5 +SCREEN_WIDTH=1280, +SCREEN_HEIGHT=720, +OUTPUT_DIR=./test-output +HEADLESS=true +# TEST_TAG=[SMOKE] +# PWDEBUG=console npm run test diff --git a/.gitignore b/.gitignore index ac6d7827..5c0bdf64 100644 --- a/.gitignore +++ b/.gitignore @@ -47,7 +47,10 @@ testem.log .DS_Store Thumbs.db +.ssh .angular test-results/ playwright-report/ -.ngapimock \ No newline at end of file +.ngapimock +allure-results/ +test-output/ diff --git a/apps/golden-sample-app-e2e/.eslintrc.json b/apps/golden-sample-app-e2e/.eslintrc.json deleted file mode 100644 index cbdb2cd0..00000000 --- a/apps/golden-sample-app-e2e/.eslintrc.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "extends": ["../../.eslintrc.json"], - "ignorePatterns": ["!**/*"], - "overrides": [ - { - "files": ["*.ts"], - "extends": ["plugin:playwright/playwright-test"] - } - ] -} diff --git a/apps/golden-sample-app-e2e/data/credentials.ts b/apps/golden-sample-app-e2e/data/credentials.ts new file mode 100644 index 00000000..ca4b9712 --- /dev/null +++ b/apps/golden-sample-app-e2e/data/credentials.ts @@ -0,0 +1,11 @@ +import { User } from './data-types/user'; + +export const defaultUser: User = { + username: '', + password: '', +}; + +export const wrongUser: User = { + username: 'someone', + password: 'some_password', +}; diff --git a/apps/golden-sample-app-e2e/data/data-types/user.ts b/apps/golden-sample-app-e2e/data/data-types/user.ts new file mode 100644 index 00000000..307a12c9 --- /dev/null +++ b/apps/golden-sample-app-e2e/data/data-types/user.ts @@ -0,0 +1,4 @@ +export interface User { + username: string; + password: string; +} diff --git a/apps/golden-sample-app-e2e/example.spec.ts b/apps/golden-sample-app-e2e/example.spec.ts deleted file mode 100644 index 600622fa..00000000 --- a/apps/golden-sample-app-e2e/example.spec.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { test, expect } from '@playwright/test'; - -test('basic test', async ({ page }) => { - await page.goto('http://localhost:4200'); - const title = page.locator('#kc-page-title'); - await expect(title).toHaveText('Welcome to your online banking'); -}); diff --git a/apps/golden-sample-app-e2e/page-objects/pages/_base-page.ts b/apps/golden-sample-app-e2e/page-objects/pages/_base-page.ts new file mode 100644 index 00000000..b6e3e0b1 --- /dev/null +++ b/apps/golden-sample-app-e2e/page-objects/pages/_base-page.ts @@ -0,0 +1,42 @@ +import { expect, Locator, Page, test, TestInfo } from '@playwright/test'; +import { LocatorOptions } from '../../utils/locator-options'; + +export abstract class BasePage { + url: string; + title: string | RegExp; + + constructor( + public page: Page, + public testInfo: TestInfo, + options?: Partial<{ url: string; title: string | RegExp }>, + ) { + this.url = options?.url || ''; + this.title = options?.title || ''; + } + + async open() { + await test.step(`Open ${this.getPageName(this)} page`, async () => { + await this.page.goto(this.url); + }); + } + + $(selector: string, options?: LocatorOptions): Locator { + return this.page.locator(selector, options); + } + + async toBeOpened() { + if (this.url) { + await expect(this.page).toHaveURL(this.url); + } + if (this.title) { + await expect(this.page).toHaveTitle(this.title); + } + } + + protected getPageName(page: object): string { + const pageName = page.constructor.name; + return pageName.toLowerCase().endsWith('page') + ? pageName.substring(0, pageName.length - 4) + : pageName; + } +} diff --git a/apps/golden-sample-app-e2e/page-objects/pages/identity-page.ts b/apps/golden-sample-app-e2e/page-objects/pages/identity-page.ts new file mode 100644 index 00000000..ffa66bc9 --- /dev/null +++ b/apps/golden-sample-app-e2e/page-objects/pages/identity-page.ts @@ -0,0 +1,20 @@ +import { User } from '../../data/data-types/user'; +import { BasePage } from './_base-page'; +import { defaultUser } from '../../data/credentials'; + +export class IdentityPage extends BasePage { + userName = this.$('#username'); + userNameLabel = this.$('label[for="username"]'); + password = this.$('#password'); + passwordLabel = this.$('label[for="password"]'); + errorMessage = this.$('#input-error'); + loginButton = this.$('.btn[value="Log in"]'); + loginForm = this.$('.identity-container__panel'); + + async login(user: User = defaultUser) { + await this.open(); + await this.userName.fill(user.username); + await this.password.fill(user.password); + await this.loginButton.click(); + } +} diff --git a/apps/golden-sample-app-e2e/page-objects/test-runner.ts b/apps/golden-sample-app-e2e/page-objects/test-runner.ts new file mode 100644 index 00000000..9e95a762 --- /dev/null +++ b/apps/golden-sample-app-e2e/page-objects/test-runner.ts @@ -0,0 +1,15 @@ +import { test as baseTest } from '@playwright/test'; +import { IdentityPage } from './pages/identity-page'; +import { testConfig } from '../test-config'; +import 'dotenv/config'; + +export const test = baseTest.extend<{ + // Pages + identityPage: IdentityPage; +}>({ + identityPage: async ({ page }, use, testInfo) => { + await use( + new IdentityPage(page, testInfo, { url: testConfig.appBaseUrl() }), + ); + }, +}); diff --git a/apps/golden-sample-app-e2e/page-objects/ui-components/_base-component.ts b/apps/golden-sample-app-e2e/page-objects/ui-components/_base-component.ts new file mode 100644 index 00000000..589dbcd7 --- /dev/null +++ b/apps/golden-sample-app-e2e/page-objects/ui-components/_base-component.ts @@ -0,0 +1,22 @@ +import { Locator, Page } from '@playwright/test'; +import { LocatorOptions } from '../../utils/locator-options'; +import { isLocator } from 'apps/golden-sample-app-e2e/utils/playwright-utils'; + +export abstract class BaseComponent { + get page(): Page { + if (isLocator(this.accessor)) + throw new Error('Root locator is not defined'); + return this.accessor as Page; + } + get root(): Locator { + if (!isLocator(this.accessor)) + throw new Error('Root locator is not defined'); + return this.accessor as Locator; + } + + constructor(private accessor: Page | Locator) { } + + $(selector: string, options?: LocatorOptions): Locator { + return this.root.locator(selector, options); + } +} diff --git a/apps/golden-sample-app-e2e/project.json b/apps/golden-sample-app-e2e/project.json deleted file mode 100644 index 7acd0244..00000000 --- a/apps/golden-sample-app-e2e/project.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "name": "golden-sample-app-e2e", - "$schema": "../../node_modules/nx/schemas/project-schema.json", - "sourceRoot": "apps/golden-sample-app-e2e/src", - "projectType": "application", - "tags": ["type:app"], - "implicitDependencies": ["golden-sample-app"], - "targets": { - "lint": { - "executor": "@nx/linter:eslint", - "options": { - "lintFilePatterns": ["apps/golden-sample-app-e2e/**/*.ts"] - } - } - } -} diff --git a/apps/golden-sample-app-e2e/specs/login.spec.ts b/apps/golden-sample-app-e2e/specs/login.spec.ts new file mode 100644 index 00000000..a4a6d9d3 --- /dev/null +++ b/apps/golden-sample-app-e2e/specs/login.spec.ts @@ -0,0 +1,43 @@ +import { expect } from '@playwright/test'; +import { test } from '../page-objects/test-runner'; +import { wrongUser } from '../data/credentials'; + +const i18n = { + identity: { + username: 'Username or email', + password: 'Password', + loginButton: 'Log in', + error: + 'Incorrect username or passwordPlease check your credentials and try again.', + }, +}; + +test.describe.configure({ mode: 'parallel' }); + +test.describe('@feature @i18n Login tests', () => { + + test('Empty user name', async ({ identityPage }) => { + identityPage.open(); + await test.step('Validate Input fields labels', async () => { + await expect.soft(identityPage.userNameLabel, { message: `Expect Username label: "${i18n.identity.username}"` }) + .toHaveText(i18n.identity.username); + await expect.soft(identityPage.passwordLabel, { message: `Expect Password label: "${i18n.identity.password}"` }) + .toHaveText(i18n.identity.password); + }); + await test.step( + `Fill in credentials: "${wrongUser.username}/${wrongUser.password}"`, + async () => { + await identityPage.userName.fill(wrongUser.username); + await identityPage.password.fill(wrongUser.password); + }, + ); + await test.step('Try to login"', async () => { + await identityPage.loginButton.click(); + }); + await test.step('Validate Failed login error message', async () => { + await expect(identityPage.errorMessage, { message: `Expect error message: "${i18n.identity.error}"` }) + .toHaveText(i18n.identity.error); + }); + }); + +}); diff --git a/apps/golden-sample-app-e2e/test-config.ts b/apps/golden-sample-app-e2e/test-config.ts new file mode 100644 index 00000000..1e1dff47 --- /dev/null +++ b/apps/golden-sample-app-e2e/test-config.ts @@ -0,0 +1,23 @@ +import { User } from './data/data-types/user'; +import config from '../../playwright.config'; +import 'dotenv/config'; + +export class TestConfig { + baseUrl = config.use?.baseURL || 'BASE URL UNDEFINED'; + locale = 'en'; + + appBaseUrl() { + return this.baseUrl; + } + + init( + options: Partial<{ user: User; url: string; }> = { + url: this.baseUrl, + }, + ) { + if (options.url) { + this.baseUrl = options.url; + } + } +} +export const testConfig = new TestConfig(); diff --git a/apps/golden-sample-app-e2e/utils/locator-options.ts b/apps/golden-sample-app-e2e/utils/locator-options.ts new file mode 100644 index 00000000..f0f57602 --- /dev/null +++ b/apps/golden-sample-app-e2e/utils/locator-options.ts @@ -0,0 +1,8 @@ +import { Locator } from '@playwright/test'; + +export interface LocatorOptions { + has?: Locator; + hasNot?: Locator; + hasNotText?: string | RegExp; + hasText?: string | RegExp; +} diff --git a/apps/golden-sample-app-e2e/utils/playwright-utils.ts b/apps/golden-sample-app-e2e/utils/playwright-utils.ts new file mode 100644 index 00000000..779e08e7 --- /dev/null +++ b/apps/golden-sample-app-e2e/utils/playwright-utils.ts @@ -0,0 +1,5 @@ +import { Locator } from '@playwright/test'; + +export const isLocator = (param: any): param is Locator => + typeof param === 'object' && param.toString().split('@')[0] === 'Locator'; + \ No newline at end of file diff --git a/apps/golden-sample-app-e2e/utils/time-utils.ts b/apps/golden-sample-app-e2e/utils/time-utils.ts new file mode 100644 index 00000000..a78d7709 --- /dev/null +++ b/apps/golden-sample-app-e2e/utils/time-utils.ts @@ -0,0 +1,6 @@ +export const timeID = () => + new Date() + .toISOString() + .split('.')[0] + .replace(/T/g, '-') + .replace(/:/g, '-'); diff --git a/apps/golden-sample-app-e2e/utils/timer-utils.ts b/apps/golden-sample-app-e2e/utils/timer-utils.ts new file mode 100644 index 00000000..e5c45ef9 --- /dev/null +++ b/apps/golden-sample-app-e2e/utils/timer-utils.ts @@ -0,0 +1,9 @@ +export async function waitDelay(timeoutMSec: number) { + await new Promise(function (r) { + setTimeout(r, timeoutMSec); + }); +} + +export async function waitSeconds(timeoutSec: number) { + await waitDelay(timeoutSec * 1000); +} diff --git a/global-setup.ts b/global-setup.ts new file mode 100644 index 00000000..8944881b --- /dev/null +++ b/global-setup.ts @@ -0,0 +1,8 @@ +import type { FullConfig } from '@playwright/test'; + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +async function globalSetup(config: FullConfig) { + // Add your global setup code here +} + +export default globalSetup; diff --git a/package-lock.json b/package-lock.json index d105b684..01e7269e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -42,6 +42,7 @@ "ngx-cookie-service": "^16.0.0", "ngx-mask": "^13.0.0", "ngx-quill": "^16.2.1", + "playwright": "1.41.2", "postcss-preset-env": "^8.4.2", "quill": "^1.3.7", "rxjs": "^7.8.1", @@ -70,13 +71,15 @@ "@nx/js": "16.4.1", "@nx/linter": "16.4.1", "@nx/workspace": "16.4.1", - "@playwright/test": "^1.18.1", + "@playwright/test": "1.41.2", "@schematics/angular": "16.0.4", "@types/jest": "^29.4.4", "@types/node": "^18.17.14", "@types/quill": "^1.3.10", "@typescript-eslint/eslint-plugin": "^5.59.9", "@typescript-eslint/parser": "5.59.8", + "allure-playwright": "^2.9.2", + "dotenv": "^16.3.1", "eslint": "^8.31.0", "eslint-config-prettier": "^8.3.0", "eslint-plugin-import": "^2.27.5", @@ -587,6 +590,15 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "node_modules/@angular-eslint/builder/node_modules/dotenv": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz", + "integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==", + "dev": true, + "engines": { + "node": ">=10" + } + }, "node_modules/@angular-eslint/builder/node_modules/enquirer": { "version": "2.3.6", "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", @@ -1064,6 +1076,15 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "node_modules/@angular-eslint/schematics/node_modules/dotenv": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz", + "integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==", + "dev": true, + "engines": { + "node": ">=10" + } + }, "node_modules/@angular-eslint/schematics/node_modules/enquirer": { "version": "2.3.6", "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", @@ -7153,6 +7174,15 @@ "nx": ">= 15 <= 17" } }, + "node_modules/@nx/cypress/node_modules/dotenv": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz", + "integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==", + "dev": true, + "engines": { + "node": ">=10" + } + }, "node_modules/@nx/cypress/node_modules/lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -7488,6 +7518,15 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "node_modules/@nx/jest/node_modules/dotenv": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz", + "integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==", + "dev": true, + "engines": { + "node": ">=10" + } + }, "node_modules/@nx/jest/node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -8163,6 +8202,15 @@ "node": ">=10" } }, + "node_modules/@nx/webpack/node_modules/dotenv": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz", + "integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==", + "dev": true, + "engines": { + "node": ">=10" + } + }, "node_modules/@nx/webpack/node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -8687,6 +8735,15 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "node_modules/@nx/workspace/node_modules/dotenv": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz", + "integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==", + "dev": true, + "engines": { + "node": ">=10" + } + }, "node_modules/@nx/workspace/node_modules/enquirer": { "version": "2.3.6", "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", @@ -9490,12 +9547,12 @@ } }, "node_modules/@playwright/test": { - "version": "1.39.0", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.39.0.tgz", - "integrity": "sha512-3u1iFqgzl7zr004bGPYiN/5EZpRUSFddQBra8Rqll5N0/vfpqlP9I9EXqAoGacuAbX6c9Ulg/Cjqglp5VkK6UQ==", + "version": "1.41.2", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.41.2.tgz", + "integrity": "sha512-qQB9h7KbibJzrDpkXkYvsmiDJK14FULCCZgEcoe2AvFAS64oCirWTwzTlAYEbKaRxWs5TFesE1Na6izMv3HfGg==", "dev": true, "dependencies": { - "playwright": "1.39.0" + "playwright": "1.41.2" }, "bin": { "playwright": "cli.js" @@ -10229,9 +10286,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "18.18.8", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.18.8.tgz", - "integrity": "sha512-OLGBaaK5V3VRBS1bAkMVP2/W9B+H8meUfl866OrMNQqt7wDgdpWPp5o6gmIc9pB+lIQHSq4ZL8ypeH1vPxcPaQ==", + "version": "18.19.14", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.14.tgz", + "integrity": "sha512-EnQ4Us2rmOS64nHDWr0XqAD8DsO6f3XR6lf9UIIrZQpUzPVdN/oPuEzfDWNHSyXLvoGgjuEm/sPwFGSSs35Wtg==", "dev": true, "dependencies": { "undici-types": "~5.26.4" @@ -11252,6 +11309,46 @@ "ajv": "^8.8.2" } }, + "node_modules/allure-js-commons": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/allure-js-commons/-/allure-js-commons-2.12.0.tgz", + "integrity": "sha512-uVMKT2LBRJQ9nPTrfE61zwryF3WhaUGIaj0PrVP7AoUpAexzGx0nOUjsJxUes1BwomcTIH1ORZo6r3kmIs7N9g==", + "dev": true, + "dependencies": { + "properties": "^1.2.1", + "strip-ansi": "^5.2.0" + } + }, + "node_modules/allure-js-commons/node_modules/ansi-regex": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", + "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/allure-js-commons/node_modules/strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "dependencies": { + "ansi-regex": "^4.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/allure-playwright": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/allure-playwright/-/allure-playwright-2.12.0.tgz", + "integrity": "sha512-H2S7bEGqH/I3kR+6cnUpgRMfUOD1i73Sa8kQtPUGzavAZ6ZuF0vITf8jrlddNo1o4NKm2/HymUp0NnDBphT7uQ==", + "dev": true, + "dependencies": { + "allure-js-commons": "2.12.0" + } + }, "node_modules/angular-oauth2-oidc": { "version": "15.0.1", "resolved": "https://registry.npmjs.org/angular-oauth2-oidc/-/angular-oauth2-oidc-15.0.1.tgz", @@ -13911,12 +14008,15 @@ } }, "node_modules/dotenv": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz", - "integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==", + "version": "16.4.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.1.tgz", + "integrity": "sha512-CjA3y+Dr3FyFDOAMnxZEGtnW9KBR2M0JvvUtXNW+dYJL5ROWxP9DUHCwgFqpMk0OXCc0ljhaNTr2w/kutYIcHQ==", "dev": true, "engines": { - "node": ">=10" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/motdotla/dotenv?sponsor=1" } }, "node_modules/duplexer": { @@ -22102,6 +22202,15 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "node_modules/nx/node_modules/dotenv": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz", + "integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==", + "dev": true, + "engines": { + "node": ">=10" + } + }, "node_modules/nx/node_modules/enquirer": { "version": "2.3.6", "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", @@ -22986,12 +23095,11 @@ } }, "node_modules/playwright": { - "version": "1.39.0", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.39.0.tgz", - "integrity": "sha512-naE5QT11uC/Oiq0BwZ50gDmy8c8WLPRTEWuSSFVG2egBka/1qMoSqYQcROMT9zLwJ86oPofcTH2jBY/5wWOgIw==", - "dev": true, + "version": "1.41.2", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.41.2.tgz", + "integrity": "sha512-v0bOa6H2GJChDL8pAeLa/LZC4feoAMbSQm1/jF/ySsWWoaNItvrMP7GEkvEEFyCTUYKMxjQKaTSg5up7nR6/8A==", "dependencies": { - "playwright-core": "1.39.0" + "playwright-core": "1.41.2" }, "bin": { "playwright": "cli.js" @@ -23004,10 +23112,9 @@ } }, "node_modules/playwright-core": { - "version": "1.39.0", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.39.0.tgz", - "integrity": "sha512-+k4pdZgs1qiM+OUkSjx96YiKsXsmb59evFoqv8SKO067qBA+Z2s/dCzJij/ZhdQcs2zlTAgRKfeiiLm8PQ2qvw==", - "dev": true, + "version": "1.41.2", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.41.2.tgz", + "integrity": "sha512-VaTvwCA4Y8kxEe+kfm2+uUUw5Lubf38RxF7FpBxLPmGe5sdNkSg5e3ChEigaGrX7qdqT3pt2m/98LiyvU2x6CA==", "bin": { "playwright-core": "cli.js" }, @@ -23019,7 +23126,6 @@ "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, "hasInstallScript": true, "optional": true, "os": [ @@ -24359,6 +24465,15 @@ "node": ">= 6" } }, + "node_modules/properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/properties/-/properties-1.2.1.tgz", + "integrity": "sha512-qYNxyMj1JeW54i/EWEFsM1cVwxJbtgPp8+0Wg9XjNaK6VE/c4oRi6PNu5p7w1mNXEIQIjV5Wwn8v8Gz82/QzdQ==", + "dev": true, + "engines": { + "node": ">=0.10" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", diff --git a/package.json b/package.json index 1f8456e4..fd919311 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "affected:test": "nx affected:test", "xi18n": "ng extract-i18n golden-sample-app --output-path=apps/golden-sample-app/src/locale && xliffmerge --profile apps/golden-sample-app/src/xliffmerge.json nl-NL", "e2e": "nx e2e", + "e2e-test": "npx playwright test --update-snapshots", "lint": "nx run-many --all --target=lint", "format": "prettier --write \"{apps,libs}/**/*.{ts,md,html}\" \"!{apps,libs}/**/coverage\" ", "format:check": "prettier --list-different \"{apps,libs}/**/*.{ts,md}\" \"!{apps,libs}/**/coverage\"" @@ -59,6 +60,7 @@ "ngx-cookie-service": "^16.0.0", "ngx-mask": "^13.0.0", "ngx-quill": "^16.2.1", + "playwright": "1.41.2", "postcss-preset-env": "^8.4.2", "quill": "^1.3.7", "rxjs": "^7.8.1", @@ -87,7 +89,7 @@ "@nx/js": "16.4.1", "@nx/linter": "16.4.1", "@nx/workspace": "16.4.1", - "@playwright/test": "^1.18.1", + "@playwright/test": "1.41.2", "@schematics/angular": "16.0.4", "@types/jest": "^29.4.4", "@types/node": "^18.17.14", @@ -113,6 +115,8 @@ "ts-node": "^10.9.1", "tslib": "^2.4.0", "typescript": "5.1.6", - "webpack-bundle-analyzer": "^4.5.0" + "webpack-bundle-analyzer": "^4.5.0", + "allure-playwright": "^2.9.2", + "dotenv": "^16.3.1" } } diff --git a/playwright.config.ts b/playwright.config.ts index f1983d50..7abbd943 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -1,101 +1,57 @@ import { PlaywrightTestConfig, devices } from '@playwright/test'; +import 'dotenv/config'; -/** - * See https://playwright.dev/docs/test-configuration. - */ +// Reference: https://playwright.dev/docs/test-configuration const config: PlaywrightTestConfig = { - testDir: './apps/golden-sample-app-e2e', - - /* Maximum time one test can run for. */ - timeout: 30 * 1000, - + timeout: (Number(process.env['TIMEOUT']) || 120) * 1000, + testDir: './apps/golden-sample-app-e2e/specs/', + retries: process.env['CI'] ? 1 : 0, + grep: process.env['TEST_TAG'] ? RegExp(process.env['TEST_TAG']) : undefined, + outputDir: process.env['OUTPUT_DIR'] || './test-output', + forbidOnly: !!process.env['CI'], + workers: process.env['CI'] ? 4 : 1, expect: { - /** - * Maximum time expect() should wait for the condition to be met. - * For example in `await expect(locator).toHaveText();` - */ - timeout: 5000, + timeout: (Number(process.env['EXPECT_TIMEOUT']) || 5) * 1000, + toHaveScreenshot: { + maxDiffPixelRatio: 0.01, + }, }, - - /* Fail the build on CI if you accidentally left test.only in the source code. */ - forbidOnly: !!process.env.CI, - - /* Retry on CI only */ - retries: process.env.CI ? 2 : 0, - - /* Opt out of parallel tests on CI. */ - workers: process.env.CI ? 1 : undefined, - - /* Reporter to use. See https://playwright.dev/docs/test-reporters */ - reporter: 'html', - - /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + reporter: [['list'], ['allure-playwright'], ['html']], + globalSetup: require.resolve(__dirname + '/global-setup.ts'), use: { - /* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */ - actionTimeout: 0, - - /* Base URL to use in actions like `await page.goto('/')`. */ - // baseURL: 'http://localhost:3000', - - /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ - trace: 'on-first-retry', + trace: 'retain-on-failure', + baseURL: process.env['BASE_URL'] || 'http://localhost:4200/', + ignoreHTTPSErrors: true, + screenshot: 'only-on-failure', + video: 'on-first-retry', + viewport: { + width: Number(process.env['SCREEN_WIDTH']) || 1280, + height: Number(process.env['SCREEN_HEIGHT']) || 720, + }, + contextOptions: { + ignoreHTTPSErrors: true, + acceptDownloads: true, + }, }, - - /* Configure projects for major browsers */ projects: [ { - name: 'chromium', - - /* Project-specific settings. */ + name: 'Web Chrome', use: { ...devices['Desktop Chrome'], + headless: process.env['HEADLESS']?.toLowerCase() === 'true', + launchOptions: { + chromiumSandbox: false, + args: [ + // List of Chrome arguments: http://peter.sh/experiments/chromium-command-line-switches/ + '--disable-gpu', + '--no-sandbox', + '--disable-setuid-sandbox', + '--disable-dev-shm-usage', + ], + }, }, }, - - { - name: 'firefox', - use: { - ...devices['Desktop Firefox'], - }, - }, - - { - name: 'webkit', - use: { - ...devices['Desktop Safari'], - }, - }, - - /* Test against mobile viewports. */ - // { - // name: 'Mobile Chrome', - // use: { - // ...devices['Pixel 5'], - // }, - // }, - // { - // name: 'Mobile Safari', - // use: { - // ...devices['iPhone 12'], - // }, - // }, - - /* Test against branded browsers. */ - // { - // name: 'Microsoft Edge', - // use: { - // channel: 'msedge', - // }, - // }, - // { - // name: 'Google Chrome', - // use: { - // channel: 'chrome', - // }, - // }, ], - - /* Folder for test artifacts such as screenshots, videos, traces, etc. */ - // outputDir: 'test-results/', }; + export default config;