├── .gitignore ├── README.md ├── components ├── FacetComponent.ts └── SearchOutputComponent.ts ├── fixtures └── baseFixture.ts ├── modals └── AcceptCookiesModal.ts ├── package-lock.json ├── package.json ├── pages └── ParfumPage.ts ├── playwright.config.ts └── tests └── selectFacet.spec.ts /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | /test-results/ 3 | /playwright-report/ 4 | /blob-report/ 5 | /playwright/.cache/ 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Привіт, я зробив цей репозиторій щоб дати можливість Test Automation Engineer робити тестові завдання краще. 2 | Якщо ви хочете підтримати мене, і закинути трохи гривень можете зробити це тут 3 | https://donatello.to/qasenpai 4 | 5 | ------------------------------------------------------------------------------ 6 | 7 | # Exercise 8 | 9 | 1. Navigate to [Douglas](https://www.douglas.de/de) 10 | 2. Handle the cookie consent. 11 | 3. Click on "Parfum" 12 | 4. List the products based on filters. Create data-driven tests: 13 | 14 | TDD data 15 | 16 | | Criteria (Highlights) | Marke | Produktart | Geschenk fur | Fur Wen | 17 | |-----------------------|-------|------------|--------------|---------| 18 | | Sale | ? | ? | - | ? | 19 | | Neu | - | ? | - | ? | 20 | | Limitiert | ? | ? | ? | ? | 21 | 22 | ? Means any value could be plugged in 23 | 24 | -Means criteria is not applicable 25 | 26 | - Please walk through the code. Feel free to use your own IDE, tool and framework of your choice. 27 | 28 | ● Best Practices: What were the best practices that you have incorporated? What code optimizations have you done? 29 | 30 | ● Execution: Please execute on couple of browsers 31 | 32 | ------------------------------------------------------------------------------ 33 | 34 | # Options for the completed task 35 | -- Playwright+TS 36 | https://github.com/hlazkov/unitedcode-taf 37 | 38 | -- Playwright+TS (HOT Testing) 39 | https://github.com/Xotabu4/test-task-pasha-sempai 40 | 41 | -- Playwright+TST (DanteUkraine) 42 | https://github.com/DanteUkraine/test-automation-exercise/tree/solutionAsAverageSenior 43 | 44 | -- Kotlin (not finished) 45 | https://github.com/rmarinsky/test_task_DE 46 | 47 | -- Playwright+Python 48 | https://github.com/bklyuka/test_task 49 | 50 | ------------------------------------------------------------------------------ 51 | ## Installation 52 | 53 | - `npm i` to install all 54 | 55 | ## How to run tests? 56 | 57 | - `npm run all-tests` - Running all tests 58 | - `npx playwright test selectFacet.spec.ts` - Running a single test file 59 | 60 | ## Useful links 61 | 62 | - `https://playwright.dev/` - Playwright-test documentation 63 | - `https://trace.playwright.dev/` - Trace viewer 64 | 65 | ## Used tools 66 | 67 | - [microsoft/playwright](https://github.com/microsoft/playwright) - Playwright-test repo 68 | -------------------------------------------------------------------------------- /components/FacetComponent.ts: -------------------------------------------------------------------------------- 1 | import { Page } from "@playwright/test"; 2 | 3 | export class FacetComponent { 4 | private page: Page; 5 | private selectors: FacetComponentSelectors; 6 | 7 | constructor(page: Page) { 8 | this.page = page; 9 | this.selectors = new FacetComponentSelectors(); 10 | } 11 | 12 | async clickFacetPlateByTitle(title: string) { 13 | await this.page 14 | .locator(this.selectors.facetPlate(title)) 15 | .click({ timeout: 10000 }); 16 | } 17 | 18 | async clickFacetByTitle(title: string) { 19 | await this.page 20 | .locator(this.selectors.facetCheckbox(title)) 21 | .click({ timeout: 10000 }); 22 | } 23 | 24 | async clickCloseFacetList() { 25 | await this.page 26 | .locator(this.selectors.closeButton) 27 | .click({ timeout: 10000 }); 28 | } 29 | 30 | async fillFacetSearch(text: string) { 31 | await this.page.locator(this.selectors.facetSearchInput).fill(text); 32 | } 33 | 34 | async getSearchVisibility() { 35 | return this.page 36 | .locator(this.selectors.facetSearchInput) 37 | .isVisible({ timeout: 10000 }); 38 | } 39 | 40 | async getSelectedFacetButtonVisibility(facetTitle: string) { 41 | try { 42 | await this.page 43 | .locator(this.selectors.selectedFacetButton(facetTitle)) 44 | .waitFor({ timeout: 5000, state: "visible" }); 45 | } catch {} 46 | 47 | return this.page 48 | .locator(this.selectors.selectedFacetButton(facetTitle)) 49 | .isVisible(); 50 | } 51 | } 52 | 53 | class FacetComponentSelectors { 54 | private facetMenu = '//*[@class="facet__menu"]'; 55 | 56 | facetPlate = (text: string) => { 57 | return `//*[contains(@class, 'facet-wrapper')]//*[@data-testid and text()='${text}']`; 58 | }; 59 | 60 | facetCheckbox = (text: string) => { 61 | return ( 62 | this.facetMenu + 63 | `//*[contains(@class, 'facet-option')]//*[starts-with(text(), '${text}')]` 64 | ); 65 | }; 66 | 67 | facetSearchInput = this.facetMenu + `//input[name="facet-search"]`; 68 | 69 | closeButton = 70 | this.facetMenu + 71 | `//*[@type = 'button' and contains(@class, 'close-button')]`; 72 | 73 | selectedFacetButton = (text: string) => { 74 | return `//*[@class = 'selected-facets']//button[text() = '${text}']`; 75 | }; 76 | } 77 | -------------------------------------------------------------------------------- /components/SearchOutputComponent.ts: -------------------------------------------------------------------------------- 1 | import { Page } from "@playwright/test"; 2 | 3 | export class SearchOutputComponent { 4 | private page: Page; 5 | private selectors: SearchOutputComponentSelectors; 6 | 7 | constructor(page: Page) { 8 | this.page = page; 9 | this.selectors = new SearchOutputComponentSelectors(); 10 | } 11 | 12 | async getSearchOutputElementCount() { 13 | const locators = await this.page.locator(this.selectors.productTile).all(); 14 | 15 | return locators.length; 16 | } 17 | } 18 | 19 | class SearchOutputComponentSelectors { 20 | productTile = `//*[@data-testid = 'product-tile']`; 21 | } 22 | -------------------------------------------------------------------------------- /fixtures/baseFixture.ts: -------------------------------------------------------------------------------- 1 | import { test as base } from "@playwright/test"; 2 | import { ParfumPage } from "../pages/ParfumPage"; 3 | import { CookiesModal } from "../modals/AcceptCookiesModal"; 4 | 5 | type Fixture = { 6 | parfumPage: ParfumPage; 7 | }; 8 | 9 | export const test = base.extend({ 10 | page: async ({ page }, use) => { 11 | const parfumPage = new ParfumPage(page); 12 | const cookiesModal = new CookiesModal(page); 13 | 14 | await parfumPage.navigateToParfumPage(); 15 | await cookiesModal.waitForModalVisibility(); 16 | await cookiesModal.acceptAllCookies(); 17 | 18 | await use(page); 19 | }, 20 | parfumPage: async ({ page }, use) => { 21 | const parfumPage = new ParfumPage(page); 22 | 23 | await use(parfumPage); 24 | }, 25 | }); 26 | -------------------------------------------------------------------------------- /modals/AcceptCookiesModal.ts: -------------------------------------------------------------------------------- 1 | import { Locator, Page } from "@playwright/test"; 2 | 3 | export class CookiesModal { 4 | private page: Page; 5 | private modalTitle: Locator; 6 | private acceptAllButton: Locator; 7 | 8 | constructor(page: Page) { 9 | this.page = page; 10 | this.modalTitle = this.page.locator( 11 | `[role='dialog'] [class ='uc-banner-title']` 12 | ); 13 | this.acceptAllButton = this.page.locator( 14 | "//button[contains(@class, 'accept-all')]" 15 | ); 16 | } 17 | 18 | async waitForModalVisibility() { 19 | await this.modalTitle.waitFor({ state: "visible" }); 20 | } 21 | 22 | async acceptAllCookies() { 23 | await this.acceptAllButton.click(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-exercice", 3 | "version": "1.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "test-exercice", 9 | "version": "1.0.0", 10 | "license": "ISC", 11 | "devDependencies": { 12 | "@playwright/test": "^1.42.0", 13 | "@types/node": "^20.11.22" 14 | } 15 | }, 16 | "node_modules/@playwright/test": { 17 | "version": "1.42.0", 18 | "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.42.0.tgz", 19 | "integrity": "sha512-2k1HzC28Fs+HiwbJOQDUwrWMttqSLUVdjCqitBOjdCD0svWOMQUVqrXX6iFD7POps6xXAojsX/dGBpKnjZctLA==", 20 | "dev": true, 21 | "dependencies": { 22 | "playwright": "1.42.0" 23 | }, 24 | "bin": { 25 | "playwright": "cli.js" 26 | }, 27 | "engines": { 28 | "node": ">=16" 29 | } 30 | }, 31 | "node_modules/@types/node": { 32 | "version": "20.11.22", 33 | "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.22.tgz", 34 | "integrity": "sha512-/G+IxWxma6V3E+pqK1tSl2Fo1kl41pK1yeCyDsgkF9WlVAme4j5ISYM2zR11bgLFJGLN5sVK40T4RJNuiZbEjA==", 35 | "dev": true, 36 | "dependencies": { 37 | "undici-types": "~5.26.4" 38 | } 39 | }, 40 | "node_modules/fsevents": { 41 | "version": "2.3.2", 42 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", 43 | "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", 44 | "dev": true, 45 | "hasInstallScript": true, 46 | "optional": true, 47 | "os": [ 48 | "darwin" 49 | ], 50 | "engines": { 51 | "node": "^8.16.0 || ^10.6.0 || >=11.0.0" 52 | } 53 | }, 54 | "node_modules/playwright": { 55 | "version": "1.42.0", 56 | "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.42.0.tgz", 57 | "integrity": "sha512-Ko7YRUgj5xBHbntrgt4EIw/nE//XBHOKVKnBjO1KuZkmkhlbgyggTe5s9hjqQ1LpN+Xg+kHsQyt5Pa0Bw5XpvQ==", 58 | "dev": true, 59 | "dependencies": { 60 | "playwright-core": "1.42.0" 61 | }, 62 | "bin": { 63 | "playwright": "cli.js" 64 | }, 65 | "engines": { 66 | "node": ">=16" 67 | }, 68 | "optionalDependencies": { 69 | "fsevents": "2.3.2" 70 | } 71 | }, 72 | "node_modules/playwright-core": { 73 | "version": "1.42.0", 74 | "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.42.0.tgz", 75 | "integrity": "sha512-0HD9y8qEVlcbsAjdpBaFjmaTHf+1FeIddy8VJLeiqwhcNqGCBe4Wp2e8knpqiYbzxtxarxiXyNDw2cG8sCaNMQ==", 76 | "dev": true, 77 | "bin": { 78 | "playwright-core": "cli.js" 79 | }, 80 | "engines": { 81 | "node": ">=16" 82 | } 83 | }, 84 | "node_modules/undici-types": { 85 | "version": "5.26.5", 86 | "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", 87 | "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", 88 | "dev": true 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-exercice", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "all-tests": "npx playwright test --workers=2 --retries=1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "devDependencies": { 13 | "@playwright/test": "^1.42.0", 14 | "@types/node": "^20.11.22" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /pages/ParfumPage.ts: -------------------------------------------------------------------------------- 1 | import { Page } from "@playwright/test"; 2 | import { FacetComponent } from "../components/FacetComponent"; 3 | import { SearchOutputComponent } from "../components/SearchOutputComponent"; 4 | 5 | export class ParfumPage { 6 | private page: Page; 7 | private facetComponent: FacetComponent; 8 | private searchOutput: SearchOutputComponent; 9 | 10 | constructor(page: Page) { 11 | this.page = page; 12 | this.facetComponent = new FacetComponent(this.page); 13 | this.searchOutput = new SearchOutputComponent(this.page); 14 | } 15 | 16 | async navigateToParfumPage() { 17 | await this.page.goto("/c/parfum/01", { 18 | waitUntil: "domcontentloaded", 19 | timeout: 60000, 20 | }); 21 | } 22 | 23 | async selectHighlights(value: string) { 24 | await this.facetComponent.clickFacetPlateByTitle("Highlights"); 25 | 26 | await Promise.all([ 27 | this.page.waitForLoadState("domcontentloaded"), 28 | this.facetComponent.clickFacetByTitle(value), 29 | ]); 30 | 31 | await this.page.waitForTimeout(5000); //TODO: it`s a crutch, needs to be refactored 32 | } 33 | 34 | async selectFacet(facetTitle: string, value: string) { 35 | await this.facetComponent.clickFacetPlateByTitle(facetTitle); 36 | 37 | if (await this.facetComponent.getSearchVisibility()) { 38 | await this.facetComponent.fillFacetSearch(value); 39 | } 40 | 41 | await this.facetComponent.clickFacetByTitle(value); 42 | await this.facetComponent.clickCloseFacetList(); 43 | } 44 | 45 | async selectFacets(facets: Object) { 46 | for (const facet in facets) { 47 | await this.selectFacet(facet, facets[facet]); 48 | } 49 | } 50 | 51 | async getFacetButtonVisibility(title: string) { 52 | return this.facetComponent.getSelectedFacetButtonVisibility(title); 53 | } 54 | 55 | async getProductTilesCount() { 56 | return this.searchOutput.getSearchOutputElementCount(); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /playwright.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, devices } from "@playwright/test"; 2 | 3 | /** 4 | * Read environment variables from file. 5 | * https://github.com/motdotla/dotenv 6 | */ 7 | // require('dotenv').config(); 8 | 9 | /** 10 | * See https://playwright.dev/docs/test-configuration. 11 | */ 12 | export default defineConfig({ 13 | testDir: "./tests", 14 | timeout: 30 * 1000, 15 | /* Run tests in files in parallel */ 16 | fullyParallel: true, 17 | /* Fail the build on CI if you accidentally left test.only in the source code. */ 18 | forbidOnly: !!process.env.CI, 19 | /* Retry on CI only */ 20 | retries: process.env.CI ? 2 : 0, 21 | /* Opt out of parallel tests on CI. */ 22 | workers: process.env.CI ? 1 : undefined, 23 | /* Reporter to use. See https://playwright.dev/docs/test-reporters */ 24 | reporter: "html", 25 | /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ 26 | use: { 27 | /* Base URL to use in actions like `await page.goto('/')`. */ 28 | baseURL: "https://www.douglas.de/de", 29 | 30 | /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ 31 | trace: "retain-on-failure", 32 | video: "retain-on-failure", 33 | }, 34 | 35 | /* Configure projects for major browsers */ 36 | projects: [ 37 | { 38 | name: "chromium", 39 | use: { ...devices["Desktop Chrome"] }, 40 | }, 41 | { 42 | name: "firefox", 43 | use: { ...devices["Desktop Firefox"] }, 44 | }, 45 | { 46 | name: "webkit", 47 | use: { ...devices["Desktop Safari"] }, 48 | }, 49 | ], 50 | }); 51 | -------------------------------------------------------------------------------- /tests/selectFacet.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "@playwright/test"; 2 | import { test } from "../fixtures/baseFixture"; 3 | 4 | const testData = [ 5 | { 6 | Highlights: "Sale", 7 | facets: { 8 | Marke: "Betty Barclay", 9 | Produktart: "Deodorant ", 10 | "Für Wen": "Weiblich", 11 | }, 12 | }, 13 | { 14 | Highlights: "NEU", 15 | facets: { 16 | Produktart: "Parfum", 17 | "Für Wen": "Unisex", 18 | }, 19 | }, 20 | { 21 | Highlights: "Limitiert", 22 | facets: { 23 | Marke: "Alyssa Ashley", 24 | Produktart: "Duftset", 25 | "Geschenk für": "Geburtstag", 26 | "Für Wen": "Unisex", 27 | }, 28 | }, 29 | ]; 30 | 31 | for (const { Highlights, facets } of testData) { 32 | test(`select facet by category ${Highlights}`, async ({ parfumPage }) => { 33 | await parfumPage.selectHighlights(Highlights); 34 | await parfumPage.selectFacets(facets); 35 | 36 | expect(await parfumPage.getProductTilesCount()).toBeGreaterThanOrEqual(1); 37 | }); 38 | } 39 | --------------------------------------------------------------------------------