├── .env.example ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── question-or-help-wanted.md └── workflows │ └── playwright.yml ├── .gitignore ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── playwright.config.ts ├── tests-examples ├── demo-todo-app.spec.ts └── example.spec.ts └── tests ├── data ├── product-data.ts └── user-data.ts ├── setup ├── global-setup-pom.ts └── global-setup.ts ├── ui-pom ├── pages │ ├── cart-page.ts │ ├── checkout-complete.ts │ ├── checkout-step-one.ts │ ├── checkout-step-two.ts │ ├── footer-page.ts │ ├── header-page.ts │ ├── inventory-page.ts │ └── login-page.ts └── specs │ ├── checkout-pom.spec.ts │ ├── footer-pom.spec.ts │ ├── login-pom.spec.ts │ └── sort-pom.spec.ts ├── ui └── specs │ ├── checkout.spec.ts │ ├── footer.spec.ts │ ├── login-simple.spec.ts │ ├── login.spec.ts │ └── sort.spec.ts └── utils ├── footer-links.ts ├── messages.ts └── pages.ts /.env.example: -------------------------------------------------------------------------------- 1 | PASSWORD='happy_testing' -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG]" 5 | labels: bug, pending 6 | assignees: raptatinha 7 | 8 | --- 9 | 10 | **🐞 Describe the bug:** 11 | A clear and concise description of what the bug is. 12 | 13 | **👻 To Reproduce:** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **🌵 Expected behavior:** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **📸 Screenshots/GIFs/Videos:** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **💈 Dependencies' Version (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Playwright Version [e.g. 22] 30 | - DotEnv Version [e.g. 22] 31 | - VS Code Version [e.g. 22] 32 | - npm Version [e.g. 22] 33 | 34 | **🛝 Additional context:** 35 | Add any other context about the problem here. 36 | 37 | **🎡 A picture of your pet or a toy or something really cool:** 38 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question-or-help-wanted.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question Or Help Wanted 3 | about: Send your question or describe what you need help for 4 | title: "[QUESTION]" 5 | labels: help wanted, pending, question 6 | assignees: raptatinha 7 | 8 | --- 9 | 10 | **🐞 Describe the question:** 11 | A clear and concise description of what the question is or what you need. 12 | 13 | **📸 Screenshots/GIFs/Videos:** 14 | If applicable, add screenshots to help explain your needs. 15 | 16 | **🛝 Additional context:** 17 | Add any other context here. 18 | 19 | **🎡 A picture of your pet or a toy or something really cool:** 20 | -------------------------------------------------------------------------------- /.github/workflows/playwright.yml: -------------------------------------------------------------------------------- 1 | name: Playwright Tests 2 | on: 3 | push: 4 | branches: [ main, master ] 5 | pull_request: 6 | branches: [ main, master ] 7 | 8 | env: 9 | PASSWORD: ${{ secrets.PASSWORD }} 10 | USERNAME: ${{ secrets.USERNAME }} 11 | 12 | jobs: 13 | test: 14 | timeout-minutes: 60 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v3 18 | - uses: actions/setup-node@v3 19 | with: 20 | node-version: 16 21 | cache: 'npm' 22 | - name: Install dependencies 23 | run: npm ci 24 | - name: Install Playwright Browsers 25 | run: npx playwright@1.35.0 install --with-deps 26 | - name: Run Playwright tests 27 | run: npx playwright test tests/ui/ 28 | # run: npx playwright test tests/ui/ --project=chromium 29 | # run: npm run test-ui-pom 30 | - uses: actions/upload-artifact@v3 31 | if: always() 32 | with: 33 | name: index.html 34 | path: playwright-report/index.html 35 | retention-days: 30 36 | - uses: actions/upload-artifact@v3 37 | if: always() 38 | with: 39 | name: test-results 40 | path: test-results/ 41 | retention-days: 30 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | .npm 4 | .env 5 | /test-results/ 6 | /playwright-report/ 7 | /playwright/.cache/ 8 | storageState.json -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Renata Andrade 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Playwright demo for MTC 2 | 3 | 4 | 5 | Hi 👋🏽! 6 | 7 | Tests at https://www.saucedemo.com/ using [Playwright](https://playwright.dev/) for the [MTC 2023](http://minastestingconference.com.br/). 8 | With and without pom and github actions. 9 | 10 | By [Renata Andrade](https://www.linkedin.com/in/raptatinha/) 11 | 12 | If you find it useful, consider leaving a ⭐️ for this repo. 13 | 14 | Happy Testing 🎭! 15 | 16 | ## 🪜 Dependecies 17 | 18 | - Playwright v1.35.0 19 | - dotenv v16.1.4 20 | - Node v20.3.0 21 | - npm v9.6.7 22 | - VSCode 1.78.2 (Universal) 23 | 24 | > Pre requirements: 25 | 26 | - [Node setup](https://nodejs.dev/en/learn/how-to-install-nodejs/) 27 | - [VS Code setup](https://code.visualstudio.com/learn/get-started/basics) 28 | - [iTerm setup](https://iterm2.com/documentation-one-page.html) 29 | 30 | 31 | ## 💡 Fork and clone the project 32 | 33 | 1. Copy the project URL `https://github.com/raptatinha/twr-playwright-demo-mtc.git`; 34 | 1. Fork the project following the [GitHub instructions](https://docs.github.com/en/get-started/quickstart/fork-a-repo) - (use the parameter --clone=true); 35 | 1. Access the forked project `cd twr-playwright-demo-mtc` 36 | 37 | ## 🧬 Setup and Install 38 | 39 | 1. Set up the environment variables. 40 | 41 | 1.1. Create the following file in the project root folder: 42 | 43 | - .env 44 | 45 | 1.2. Copy the content of [.env.example](.env.example) into the newly created file.
46 | 1.3. Update the PASSWORD (you can check the password here: https://www.saucedemo.com/). 47 | 48 | 2. On your terminal, type: 49 | 50 | 2.1. `npm i` 51 | 52 | ## 🚀 Run 53 | All the commands are in the [package.json](package.json). 54 | 55 | - Run tests without POM: `npm run test-ui` 56 | - Run tests without POM on chrome only: `npm run test-ui-c` 57 | - Run tests with POM: `npm run test-ui-pom` 58 | 59 | ## 📊 Report 60 | 61 | `npx playwright show-report` 62 | ## 🌀 Pipeline 63 | 64 | Using GitHub Actions. 65 | Check [playwright.yml](.github/workflows/playwright.yml) 66 | 67 | ___ 68 | 69 | 70 | 💡 Share on LinkedIn something interesting you've learned! Don't forget to tag me [Renata Andrade](https://www.linkedin.com/in/raptatinha/). 71 | 72 | 💜 If you have questions, feel free to post them on [github](https://github.com/raptatinha/twr-playwright-demo-mtc/issues). 73 | 74 | Happy Testing 🎭 75 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "twr-playwright-demo-mtc", 3 | "version": "1.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "twr-playwright-demo-mtc", 9 | "version": "1.0.0", 10 | "license": "ISC", 11 | "devDependencies": { 12 | "@playwright/test": "^1.35.0", 13 | "dotenv": "^16.1.4" 14 | } 15 | }, 16 | "node_modules/@playwright/test": { 17 | "version": "1.35.0", 18 | "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.35.0.tgz", 19 | "integrity": "sha512-6qXdd5edCBynOwsz1YcNfgX8tNWeuS9fxy5o59D0rvHXxRtjXRebB4gE4vFVfEMXl/z8zTnAzfOs7aQDEs8G4Q==", 20 | "dev": true, 21 | "dependencies": { 22 | "@types/node": "*", 23 | "playwright-core": "1.35.0" 24 | }, 25 | "bin": { 26 | "playwright": "cli.js" 27 | }, 28 | "engines": { 29 | "node": ">=16" 30 | }, 31 | "optionalDependencies": { 32 | "fsevents": "2.3.2" 33 | } 34 | }, 35 | "node_modules/@types/node": { 36 | "version": "20.3.0", 37 | "resolved": "https://registry.npmjs.org/@types/node/-/node-20.3.0.tgz", 38 | "integrity": "sha512-cumHmIAf6On83X7yP+LrsEyUOf/YlociZelmpRYaGFydoaPdxdt80MAbu6vWerQT2COCp2nPvHdsbD7tHn/YlQ==", 39 | "dev": true 40 | }, 41 | "node_modules/dotenv": { 42 | "version": "16.1.4", 43 | "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.1.4.tgz", 44 | "integrity": "sha512-m55RtE8AsPeJBpOIFKihEmqUcoVncQIwo7x9U8ZwLEZw9ZpXboz2c+rvog+jUaJvVrZ5kBOeYQBX5+8Aa/OZQw==", 45 | "dev": true, 46 | "engines": { 47 | "node": ">=12" 48 | }, 49 | "funding": { 50 | "url": "https://github.com/motdotla/dotenv?sponsor=1" 51 | } 52 | }, 53 | "node_modules/fsevents": { 54 | "version": "2.3.2", 55 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", 56 | "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", 57 | "dev": true, 58 | "hasInstallScript": true, 59 | "optional": true, 60 | "os": [ 61 | "darwin" 62 | ], 63 | "engines": { 64 | "node": "^8.16.0 || ^10.6.0 || >=11.0.0" 65 | } 66 | }, 67 | "node_modules/playwright-core": { 68 | "version": "1.35.0", 69 | "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.35.0.tgz", 70 | "integrity": "sha512-muMXyPmIx/2DPrCHOD1H1ePT01o7OdKxKj2ebmCAYvqhUy+Y1bpal7B0rdoxros7YrXI294JT/DWw2LqyiqTPA==", 71 | "dev": true, 72 | "bin": { 73 | "playwright-core": "cli.js" 74 | }, 75 | "engines": { 76 | "node": ">=16" 77 | } 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "twr-playwright-demo-mtc", 3 | "version": "1.0.0", 4 | "description": "Playwright flow with and without pom and github actions", 5 | "main": "index.js", 6 | "scripts": { 7 | "test-ui": "npx playwright test tests/ui/", 8 | "test-ui-c": "npx playwright test tests/ui/ --project=chromium", 9 | "test-ui-pom": "npx playwright test tests/ui-pom/" 10 | }, 11 | "keywords": [], 12 | "author": "", 13 | "license": "ISC", 14 | "devDependencies": { 15 | "@playwright/test": "^1.35.0", 16 | "dotenv": "^16.1.4" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /playwright.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, devices } from '@playwright/test'; 2 | 3 | require('dotenv').config(); 4 | 5 | export default defineConfig({ 6 | globalSetup: require.resolve('./tests/setup/global-setup'), 7 | fullyParallel: true, 8 | forbidOnly: !!process.env.CI, 9 | retries: 0, 10 | workers: undefined, 11 | reporter: 'html', 12 | use: { 13 | baseURL: 'https://www.saucedemo.com/', 14 | trace: 'on', 15 | storageState: 'storageState.json', 16 | testIdAttribute: 'data-test' 17 | }, 18 | 19 | projects: [ 20 | { 21 | name: 'chromium', 22 | use: { ...devices['Desktop Chrome'] }, 23 | }, 24 | { 25 | name: 'firefox', 26 | use: { ...devices['Desktop Firefox'] }, 27 | }, 28 | { 29 | name: 'webkit', 30 | use: { ...devices['Desktop Safari'] }, 31 | }, 32 | { 33 | name: 'Microsoft Edge', 34 | use: { ...devices['Desktop Edge'], channel: 'msedge' }, 35 | }, 36 | ], 37 | }); 38 | -------------------------------------------------------------------------------- /tests-examples/demo-todo-app.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect, type Page } from '@playwright/test'; 2 | 3 | test.beforeEach(async ({ page }) => { 4 | await page.goto('https://demo.playwright.dev/todomvc'); 5 | }); 6 | 7 | const TODO_ITEMS = [ 8 | 'buy some cheese', 9 | 'feed the cat', 10 | 'book a doctors appointment' 11 | ]; 12 | 13 | test.describe('New Todo', () => { 14 | test('should allow me to add todo items', async ({ page }) => { 15 | // create a new todo locator 16 | const newTodo = page.getByPlaceholder('What needs to be done?'); 17 | 18 | // Create 1st todo. 19 | await newTodo.fill(TODO_ITEMS[0]); 20 | await newTodo.press('Enter'); 21 | 22 | // Make sure the list only has one todo item. 23 | await expect(page.getByTestId('todo-title')).toHaveText([ 24 | TODO_ITEMS[0] 25 | ]); 26 | 27 | // Create 2nd todo. 28 | await newTodo.fill(TODO_ITEMS[1]); 29 | await newTodo.press('Enter'); 30 | 31 | // Make sure the list now has two todo items. 32 | await expect(page.getByTestId('todo-title')).toHaveText([ 33 | TODO_ITEMS[0], 34 | TODO_ITEMS[1] 35 | ]); 36 | 37 | await checkNumberOfTodosInLocalStorage(page, 2); 38 | }); 39 | 40 | test('should clear text input field when an item is added', async ({ page }) => { 41 | // create a new todo locator 42 | const newTodo = page.getByPlaceholder('What needs to be done?'); 43 | 44 | // Create one todo item. 45 | await newTodo.fill(TODO_ITEMS[0]); 46 | await newTodo.press('Enter'); 47 | 48 | // Check that input is empty. 49 | await expect(newTodo).toBeEmpty(); 50 | await checkNumberOfTodosInLocalStorage(page, 1); 51 | }); 52 | 53 | test('should append new items to the bottom of the list', async ({ page }) => { 54 | // Create 3 items. 55 | await createDefaultTodos(page); 56 | 57 | // create a todo count locator 58 | const todoCount = page.getByTestId('todo-count') 59 | 60 | // Check test using different methods. 61 | await expect(page.getByText('3 items left')).toBeVisible(); 62 | await expect(todoCount).toHaveText('3 items left'); 63 | await expect(todoCount).toContainText('3'); 64 | await expect(todoCount).toHaveText(/3/); 65 | 66 | // Check all items in one call. 67 | await expect(page.getByTestId('todo-title')).toHaveText(TODO_ITEMS); 68 | await checkNumberOfTodosInLocalStorage(page, 3); 69 | }); 70 | }); 71 | 72 | test.describe('Mark all as completed', () => { 73 | test.beforeEach(async ({ page }) => { 74 | await createDefaultTodos(page); 75 | await checkNumberOfTodosInLocalStorage(page, 3); 76 | }); 77 | 78 | test.afterEach(async ({ page }) => { 79 | await checkNumberOfTodosInLocalStorage(page, 3); 80 | }); 81 | 82 | test('should allow me to mark all items as completed', async ({ page }) => { 83 | // Complete all todos. 84 | await page.getByLabel('Mark all as complete').check(); 85 | 86 | // Ensure all todos have 'completed' class. 87 | await expect(page.getByTestId('todo-item')).toHaveClass(['completed', 'completed', 'completed']); 88 | await checkNumberOfCompletedTodosInLocalStorage(page, 3); 89 | }); 90 | 91 | test('should allow me to clear the complete state of all items', async ({ page }) => { 92 | const toggleAll = page.getByLabel('Mark all as complete'); 93 | // Check and then immediately uncheck. 94 | await toggleAll.check(); 95 | await toggleAll.uncheck(); 96 | 97 | // Should be no completed classes. 98 | await expect(page.getByTestId('todo-item')).toHaveClass(['', '', '']); 99 | }); 100 | 101 | test('complete all checkbox should update state when items are completed / cleared', async ({ page }) => { 102 | const toggleAll = page.getByLabel('Mark all as complete'); 103 | await toggleAll.check(); 104 | await expect(toggleAll).toBeChecked(); 105 | await checkNumberOfCompletedTodosInLocalStorage(page, 3); 106 | 107 | // Uncheck first todo. 108 | const firstTodo = page.getByTestId('todo-item').nth(0); 109 | await firstTodo.getByRole('checkbox').uncheck(); 110 | 111 | // Reuse toggleAll locator and make sure its not checked. 112 | await expect(toggleAll).not.toBeChecked(); 113 | 114 | await firstTodo.getByRole('checkbox').check(); 115 | await checkNumberOfCompletedTodosInLocalStorage(page, 3); 116 | 117 | // Assert the toggle all is checked again. 118 | await expect(toggleAll).toBeChecked(); 119 | }); 120 | }); 121 | 122 | test.describe('Item', () => { 123 | 124 | test('should allow me to mark items as complete', async ({ page }) => { 125 | // create a new todo locator 126 | const newTodo = page.getByPlaceholder('What needs to be done?'); 127 | 128 | // Create two items. 129 | for (const item of TODO_ITEMS.slice(0, 2)) { 130 | await newTodo.fill(item); 131 | await newTodo.press('Enter'); 132 | } 133 | 134 | // Check first item. 135 | const firstTodo = page.getByTestId('todo-item').nth(0); 136 | await firstTodo.getByRole('checkbox').check(); 137 | await expect(firstTodo).toHaveClass('completed'); 138 | 139 | // Check second item. 140 | const secondTodo = page.getByTestId('todo-item').nth(1); 141 | await expect(secondTodo).not.toHaveClass('completed'); 142 | await secondTodo.getByRole('checkbox').check(); 143 | 144 | // Assert completed class. 145 | await expect(firstTodo).toHaveClass('completed'); 146 | await expect(secondTodo).toHaveClass('completed'); 147 | }); 148 | 149 | test('should allow me to un-mark items as complete', async ({ page }) => { 150 | // create a new todo locator 151 | const newTodo = page.getByPlaceholder('What needs to be done?'); 152 | 153 | // Create two items. 154 | for (const item of TODO_ITEMS.slice(0, 2)) { 155 | await newTodo.fill(item); 156 | await newTodo.press('Enter'); 157 | } 158 | 159 | const firstTodo = page.getByTestId('todo-item').nth(0); 160 | const secondTodo = page.getByTestId('todo-item').nth(1); 161 | const firstTodoCheckbox = firstTodo.getByRole('checkbox'); 162 | 163 | await firstTodoCheckbox.check(); 164 | await expect(firstTodo).toHaveClass('completed'); 165 | await expect(secondTodo).not.toHaveClass('completed'); 166 | await checkNumberOfCompletedTodosInLocalStorage(page, 1); 167 | 168 | await firstTodoCheckbox.uncheck(); 169 | await expect(firstTodo).not.toHaveClass('completed'); 170 | await expect(secondTodo).not.toHaveClass('completed'); 171 | await checkNumberOfCompletedTodosInLocalStorage(page, 0); 172 | }); 173 | 174 | test('should allow me to edit an item', async ({ page }) => { 175 | await createDefaultTodos(page); 176 | 177 | const todoItems = page.getByTestId('todo-item'); 178 | const secondTodo = todoItems.nth(1); 179 | await secondTodo.dblclick(); 180 | await expect(secondTodo.getByRole('textbox', { name: 'Edit' })).toHaveValue(TODO_ITEMS[1]); 181 | await secondTodo.getByRole('textbox', { name: 'Edit' }).fill('buy some sausages'); 182 | await secondTodo.getByRole('textbox', { name: 'Edit' }).press('Enter'); 183 | 184 | // Explicitly assert the new text value. 185 | await expect(todoItems).toHaveText([ 186 | TODO_ITEMS[0], 187 | 'buy some sausages', 188 | TODO_ITEMS[2] 189 | ]); 190 | await checkTodosInLocalStorage(page, 'buy some sausages'); 191 | }); 192 | }); 193 | 194 | test.describe('Editing', () => { 195 | test.beforeEach(async ({ page }) => { 196 | await createDefaultTodos(page); 197 | await checkNumberOfTodosInLocalStorage(page, 3); 198 | }); 199 | 200 | test('should hide other controls when editing', async ({ page }) => { 201 | const todoItem = page.getByTestId('todo-item').nth(1); 202 | await todoItem.dblclick(); 203 | await expect(todoItem.getByRole('checkbox')).not.toBeVisible(); 204 | await expect(todoItem.locator('label', { 205 | hasText: TODO_ITEMS[1], 206 | })).not.toBeVisible(); 207 | await checkNumberOfTodosInLocalStorage(page, 3); 208 | }); 209 | 210 | test('should save edits on blur', async ({ page }) => { 211 | const todoItems = page.getByTestId('todo-item'); 212 | await todoItems.nth(1).dblclick(); 213 | await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill('buy some sausages'); 214 | await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).dispatchEvent('blur'); 215 | 216 | await expect(todoItems).toHaveText([ 217 | TODO_ITEMS[0], 218 | 'buy some sausages', 219 | TODO_ITEMS[2], 220 | ]); 221 | await checkTodosInLocalStorage(page, 'buy some sausages'); 222 | }); 223 | 224 | test('should trim entered text', async ({ page }) => { 225 | const todoItems = page.getByTestId('todo-item'); 226 | await todoItems.nth(1).dblclick(); 227 | await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill(' buy some sausages '); 228 | await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Enter'); 229 | 230 | await expect(todoItems).toHaveText([ 231 | TODO_ITEMS[0], 232 | 'buy some sausages', 233 | TODO_ITEMS[2], 234 | ]); 235 | await checkTodosInLocalStorage(page, 'buy some sausages'); 236 | }); 237 | 238 | test('should remove the item if an empty text string was entered', async ({ page }) => { 239 | const todoItems = page.getByTestId('todo-item'); 240 | await todoItems.nth(1).dblclick(); 241 | await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill(''); 242 | await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Enter'); 243 | 244 | await expect(todoItems).toHaveText([ 245 | TODO_ITEMS[0], 246 | TODO_ITEMS[2], 247 | ]); 248 | }); 249 | 250 | test('should cancel edits on escape', async ({ page }) => { 251 | const todoItems = page.getByTestId('todo-item'); 252 | await todoItems.nth(1).dblclick(); 253 | await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill('buy some sausages'); 254 | await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Escape'); 255 | await expect(todoItems).toHaveText(TODO_ITEMS); 256 | }); 257 | }); 258 | 259 | test.describe('Counter', () => { 260 | test('should display the current number of todo items', async ({ page }) => { 261 | // create a new todo locator 262 | const newTodo = page.getByPlaceholder('What needs to be done?'); 263 | 264 | // create a todo count locator 265 | const todoCount = page.getByTestId('todo-count') 266 | 267 | await newTodo.fill(TODO_ITEMS[0]); 268 | await newTodo.press('Enter'); 269 | 270 | await expect(todoCount).toContainText('1'); 271 | 272 | await newTodo.fill(TODO_ITEMS[1]); 273 | await newTodo.press('Enter'); 274 | await expect(todoCount).toContainText('2'); 275 | 276 | await checkNumberOfTodosInLocalStorage(page, 2); 277 | }); 278 | }); 279 | 280 | test.describe('Clear completed button', () => { 281 | test.beforeEach(async ({ page }) => { 282 | await createDefaultTodos(page); 283 | }); 284 | 285 | test('should display the correct text', async ({ page }) => { 286 | await page.locator('.todo-list li .toggle').first().check(); 287 | await expect(page.getByRole('button', { name: 'Clear completed' })).toBeVisible(); 288 | }); 289 | 290 | test('should remove completed items when clicked', async ({ page }) => { 291 | const todoItems = page.getByTestId('todo-item'); 292 | await todoItems.nth(1).getByRole('checkbox').check(); 293 | await page.getByRole('button', { name: 'Clear completed' }).click(); 294 | await expect(todoItems).toHaveCount(2); 295 | await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]); 296 | }); 297 | 298 | test('should be hidden when there are no items that are completed', async ({ page }) => { 299 | await page.locator('.todo-list li .toggle').first().check(); 300 | await page.getByRole('button', { name: 'Clear completed' }).click(); 301 | await expect(page.getByRole('button', { name: 'Clear completed' })).toBeHidden(); 302 | }); 303 | }); 304 | 305 | test.describe('Persistence', () => { 306 | test('should persist its data', async ({ page }) => { 307 | // create a new todo locator 308 | const newTodo = page.getByPlaceholder('What needs to be done?'); 309 | 310 | for (const item of TODO_ITEMS.slice(0, 2)) { 311 | await newTodo.fill(item); 312 | await newTodo.press('Enter'); 313 | } 314 | 315 | const todoItems = page.getByTestId('todo-item'); 316 | const firstTodoCheck = todoItems.nth(0).getByRole('checkbox'); 317 | await firstTodoCheck.check(); 318 | await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[1]]); 319 | await expect(firstTodoCheck).toBeChecked(); 320 | await expect(todoItems).toHaveClass(['completed', '']); 321 | 322 | // Ensure there is 1 completed item. 323 | await checkNumberOfCompletedTodosInLocalStorage(page, 1); 324 | 325 | // Now reload. 326 | await page.reload(); 327 | await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[1]]); 328 | await expect(firstTodoCheck).toBeChecked(); 329 | await expect(todoItems).toHaveClass(['completed', '']); 330 | }); 331 | }); 332 | 333 | test.describe('Routing', () => { 334 | test.beforeEach(async ({ page }) => { 335 | await createDefaultTodos(page); 336 | // make sure the app had a chance to save updated todos in storage 337 | // before navigating to a new view, otherwise the items can get lost :( 338 | // in some frameworks like Durandal 339 | await checkTodosInLocalStorage(page, TODO_ITEMS[0]); 340 | }); 341 | 342 | test('should allow me to display active items', async ({ page }) => { 343 | const todoItem = page.getByTestId('todo-item'); 344 | await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check(); 345 | 346 | await checkNumberOfCompletedTodosInLocalStorage(page, 1); 347 | await page.getByRole('link', { name: 'Active' }).click(); 348 | await expect(todoItem).toHaveCount(2); 349 | await expect(todoItem).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]); 350 | }); 351 | 352 | test('should respect the back button', async ({ page }) => { 353 | const todoItem = page.getByTestId('todo-item'); 354 | await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check(); 355 | 356 | await checkNumberOfCompletedTodosInLocalStorage(page, 1); 357 | 358 | await test.step('Showing all items', async () => { 359 | await page.getByRole('link', { name: 'All' }).click(); 360 | await expect(todoItem).toHaveCount(3); 361 | }); 362 | 363 | await test.step('Showing active items', async () => { 364 | await page.getByRole('link', { name: 'Active' }).click(); 365 | }); 366 | 367 | await test.step('Showing completed items', async () => { 368 | await page.getByRole('link', { name: 'Completed' }).click(); 369 | }); 370 | 371 | await expect(todoItem).toHaveCount(1); 372 | await page.goBack(); 373 | await expect(todoItem).toHaveCount(2); 374 | await page.goBack(); 375 | await expect(todoItem).toHaveCount(3); 376 | }); 377 | 378 | test('should allow me to display completed items', async ({ page }) => { 379 | await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check(); 380 | await checkNumberOfCompletedTodosInLocalStorage(page, 1); 381 | await page.getByRole('link', { name: 'Completed' }).click(); 382 | await expect(page.getByTestId('todo-item')).toHaveCount(1); 383 | }); 384 | 385 | test('should allow me to display all items', async ({ page }) => { 386 | await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check(); 387 | await checkNumberOfCompletedTodosInLocalStorage(page, 1); 388 | await page.getByRole('link', { name: 'Active' }).click(); 389 | await page.getByRole('link', { name: 'Completed' }).click(); 390 | await page.getByRole('link', { name: 'All' }).click(); 391 | await expect(page.getByTestId('todo-item')).toHaveCount(3); 392 | }); 393 | 394 | test('should highlight the currently applied filter', async ({ page }) => { 395 | await expect(page.getByRole('link', { name: 'All' })).toHaveClass('selected'); 396 | 397 | //create locators for active and completed links 398 | const activeLink = page.getByRole('link', { name: 'Active' }); 399 | const completedLink = page.getByRole('link', { name: 'Completed' }); 400 | await activeLink.click(); 401 | 402 | // Page change - active items. 403 | await expect(activeLink).toHaveClass('selected'); 404 | await completedLink.click(); 405 | 406 | // Page change - completed items. 407 | await expect(completedLink).toHaveClass('selected'); 408 | }); 409 | }); 410 | 411 | async function createDefaultTodos(page: Page) { 412 | // create a new todo locator 413 | const newTodo = page.getByPlaceholder('What needs to be done?'); 414 | 415 | for (const item of TODO_ITEMS) { 416 | await newTodo.fill(item); 417 | await newTodo.press('Enter'); 418 | } 419 | } 420 | 421 | async function checkNumberOfTodosInLocalStorage(page: Page, expected: number) { 422 | return await page.waitForFunction(e => { 423 | return JSON.parse(localStorage['react-todos']).length === e; 424 | }, expected); 425 | } 426 | 427 | async function checkNumberOfCompletedTodosInLocalStorage(page: Page, expected: number) { 428 | return await page.waitForFunction(e => { 429 | return JSON.parse(localStorage['react-todos']).filter((todo: any) => todo.completed).length === e; 430 | }, expected); 431 | } 432 | 433 | async function checkTodosInLocalStorage(page: Page, title: string) { 434 | return await page.waitForFunction(t => { 435 | return JSON.parse(localStorage['react-todos']).map((todo: any) => todo.title).includes(t); 436 | }, title); 437 | } 438 | -------------------------------------------------------------------------------- /tests-examples/example.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@playwright/test'; 2 | 3 | test('has title', async ({ page }) => { 4 | await page.goto('https://playwright.dev/'); 5 | 6 | // Expect a title "to contain" a substring. 7 | await expect(page).toHaveTitle(/Playwright/); 8 | }); 9 | 10 | test('get started link', async ({ page }) => { 11 | await page.goto('https://playwright.dev/'); 12 | 13 | // Click the get started link. 14 | await page.getByRole('link', { name: 'Get started' }).click(); 15 | 16 | // Expects the URL to contain intro. 17 | await expect(page).toHaveURL(/.*intro/); 18 | }); 19 | -------------------------------------------------------------------------------- /tests/data/product-data.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | items: 2, 3 | orderInfo: { 4 | itemTotal: /.*25.98/, 5 | tax: /.*2.08/, 6 | orderTotal: /.*28.06/, 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /tests/data/user-data.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | validUser: 'standard_user', 3 | problemUser: 'problem_user', 4 | performanceGlitchUser: 'performance_glitch_user', 5 | invalidUser: { 6 | invalidUsername: 'invalid_user', 7 | lockedOutdUser: 'locked_out_user', 8 | }, 9 | firstName: 'Leia', 10 | lastName: 'Organa', 11 | zip: '2187', 12 | }; 13 | -------------------------------------------------------------------------------- /tests/setup/global-setup-pom.ts: -------------------------------------------------------------------------------- 1 | import { chromium, FullConfig } from '@playwright/test'; 2 | import LoginPage from '../ui-pom/pages/login-page'; 3 | import userData from '../data/user-data'; 4 | 5 | async function globalSetup(config: FullConfig) { 6 | const userName = userData.validUser; 7 | const password = process.env.PASSWORD; 8 | const { baseURL, storageState } = config.projects[0].use; 9 | const browser = await chromium.launch({ headless: true, timeout: 10000 }); 10 | const context = await browser.newContext(); 11 | // const page = await browser.newPage({ ignoreHTTPSErrors: shouldIgnoreHTTPSErrors }); 12 | const page = await browser.newPage(); 13 | const loginPage = new LoginPage(page); 14 | 15 | try { 16 | await context.tracing.start({ screenshots: true, snapshots: true }); 17 | await page.goto(baseURL!); 18 | await loginPage.doLogin(userName, password!); 19 | await loginPage.checkLoggedIn(); 20 | await page.context().storageState({ path: storageState as string }); 21 | await context.tracing.stop({path: './test-results/setup-trace.zip'}); 22 | await browser.close(); 23 | } catch (error) { 24 | await context.tracing.stop({path: './test-results/failed-setup-trace.zip'}); 25 | await browser.close(); 26 | throw error; 27 | } 28 | } 29 | 30 | export default globalSetup; 31 | 32 | // https://playwright.dev/docs/test-global-setup-teardown#capturing-trace-of-failures-during-global-setup 33 | // https://playwright.dev/docs/trace-viewer 34 | -------------------------------------------------------------------------------- /tests/setup/global-setup.ts: -------------------------------------------------------------------------------- 1 | import { chromium, FullConfig, expect } from '@playwright/test'; 2 | import userData from '../data/user-data'; 3 | 4 | async function globalSetup(config: FullConfig) { 5 | const userName = userData.validUser; 6 | const password = process.env.PASSWORD; 7 | const { baseURL, storageState } = config.projects[0].use; 8 | const browser = await chromium.launch({ headless: true, timeout: 10000 }); 9 | const context = await browser.newContext(); 10 | // const page = await browser.newPage({ ignoreHTTPSErrors: shouldIgnoreHTTPSErrors }); 11 | const page = await browser.newPage(); 12 | 13 | try { 14 | await context.tracing.start({ screenshots: true, snapshots: true }); 15 | await page.goto(baseURL!); 16 | await page.getByPlaceholder('Username').fill(userName); 17 | await page.getByPlaceholder('Password').fill(password!); 18 | await page.getByText('Login', { exact: true }).click(); 19 | await expect(page).toHaveURL(/.*inventory.html/); 20 | await expect(page).toHaveTitle(/Swag Labs/); 21 | await page.context().storageState({ path: storageState as string }); 22 | await context.tracing.stop({path: './test-results/setup-trace.zip'}); 23 | await browser.close(); 24 | } catch (error) { 25 | await context.tracing.stop({path: './test-results/failed-setup-trace.zip'}); 26 | await browser.close(); 27 | throw error; 28 | } 29 | } 30 | 31 | export default globalSetup; 32 | 33 | // https://playwright.dev/docs/test-global-setup-teardown#capturing-trace-of-failures-during-global-setup 34 | // https://playwright.dev/docs/trace-viewer 35 | -------------------------------------------------------------------------------- /tests/ui-pom/pages/cart-page.ts: -------------------------------------------------------------------------------- 1 | import { type Page, type Locator , expect } from '@playwright/test'; 2 | 3 | class CartPage { 4 | readonly page: Page; 5 | readonly removeFromCartButton: Locator; 6 | readonly checkoutButton: Locator; 7 | 8 | constructor(page: Page) { 9 | this.page = page; 10 | this.removeFromCartButton = page.getByRole('button', { name: 'Remove' }); 11 | this.checkoutButton = page.getByRole('button', { name: 'Checkout' }); 12 | } 13 | 14 | async checkItemsInCart(items: number){ 15 | await expect(this.removeFromCartButton).toHaveCount(items); 16 | } 17 | 18 | async goToCheckout(){ 19 | await this.checkoutButton.click(); 20 | } 21 | } 22 | 23 | export default CartPage; 24 | -------------------------------------------------------------------------------- /tests/ui-pom/pages/checkout-complete.ts: -------------------------------------------------------------------------------- 1 | import { type Page, type Locator , expect } from '@playwright/test'; 2 | 3 | class CheckoutCompletePage { 4 | readonly page: Page; 5 | readonly menuButton: Locator; 6 | readonly backHomeButton: Locator; 7 | readonly orderCompletedLabel: Locator; 8 | readonly orderCompletedText: Locator; 9 | readonly orderCompletedLabelString: string = 'Thank you for your order!'; 10 | readonly orderCompletedTextString: string = 'Your order has been dispatched, and will arrive just as fast as the pony can get there!'; 11 | 12 | constructor(page: Page) { 13 | this.page = page; 14 | this.orderCompletedLabel = page.getByText(this.orderCompletedLabelString); 15 | this.orderCompletedText = page.getByText(this.orderCompletedTextString); 16 | this.backHomeButton = page.getByRole('button', { name: 'Back Home' }); 17 | } 18 | 19 | async goBackToHome(){ 20 | await this.backHomeButton.click(); 21 | } 22 | 23 | async checkCheckoutSucessfull(){ 24 | await expect(this.orderCompletedLabel).toBeVisible(); 25 | await expect(this.orderCompletedText).toBeVisible(); 26 | await expect(this.backHomeButton).toBeVisible(); 27 | } 28 | } 29 | 30 | export default CheckoutCompletePage; 31 | -------------------------------------------------------------------------------- /tests/ui-pom/pages/checkout-step-one.ts: -------------------------------------------------------------------------------- 1 | import { type Page, type Locator, expect } from '@playwright/test'; 2 | 3 | interface UserData { 4 | firstName: string; 5 | lastName: string; 6 | zip: string; 7 | } 8 | 9 | class CheckoutStepOnePage { 10 | readonly page: Page; 11 | readonly firstName: Locator; 12 | readonly lastName: Locator; 13 | readonly zip: Locator; 14 | readonly continueButton: Locator; 15 | readonly inputFields: Locator; 16 | 17 | constructor(page: Page) { 18 | this.page = page; 19 | this.firstName = page.getByPlaceholder('First Name'); 20 | this.lastName = page.getByPlaceholder('Last Name'); 21 | this.zip = page.getByPlaceholder('Zip/Postal Code'); 22 | this.continueButton = page.getByText('Continue', { exact: true }); 23 | this.inputFields = page.getByRole('textbox'); 24 | } 25 | 26 | async fillFirstName(firstName: string) { 27 | await this.firstName.fill(firstName); 28 | } 29 | 30 | async fillLastName(lastName: string) { 31 | await this.lastName.fill(lastName); 32 | } 33 | 34 | async fillZip(zip: string) { 35 | await this.zip.fill(zip); 36 | } 37 | 38 | async fillInformationAndContinue(userData: UserData) { 39 | let { firstName, lastName, zip } = userData; 40 | for (const input of await this.inputFields.all()){ 41 | await expect(input).toBeEmpty(); 42 | } 43 | await this.fillFirstName(firstName); 44 | await this.fillLastName(lastName); 45 | await this.fillZip(zip); 46 | await this.continueButton.click(); 47 | } 48 | } 49 | 50 | export default CheckoutStepOnePage; 51 | -------------------------------------------------------------------------------- /tests/ui-pom/pages/checkout-step-two.ts: -------------------------------------------------------------------------------- 1 | import { type Page, type Locator , expect } from '@playwright/test'; 2 | 3 | interface OrderInfo { 4 | readonly itemTotal: RegExp; 5 | readonly tax: RegExp; 6 | readonly orderTotal: RegExp; 7 | } 8 | 9 | class CheckoutStepTwoPage { 10 | readonly page: Page; 11 | readonly itemTotalLabel: Locator; 12 | readonly taxLabel: Locator; 13 | readonly orderTotalLabel: Locator; 14 | readonly finishButton: Locator; 15 | 16 | constructor(page: Page) { 17 | this.page = page; 18 | this.itemTotalLabel = page.getByText('Item total:'); 19 | this.taxLabel = page.getByText('Tax:'); 20 | this.orderTotalLabel = page.getByText('Total:').last(); 21 | this.finishButton = page.getByText('Finish', { exact: true }); 22 | } 23 | 24 | async checkItemTotal(itemTotal: RegExp) { 25 | await expect(this.itemTotalLabel).toHaveText(itemTotal); 26 | } 27 | 28 | async checkTax(tax: RegExp) { 29 | await expect(this.taxLabel).toHaveText(tax); 30 | } 31 | 32 | async checkOrderTotal(orderTotal: RegExp) { 33 | await expect(this.orderTotalLabel).toHaveText(orderTotal); 34 | } 35 | 36 | async checkOrderInfo(orderInfo: OrderInfo){ 37 | await this.checkItemTotal(orderInfo.itemTotal); 38 | await this.checkTax(orderInfo.tax); 39 | await this.checkOrderTotal(orderInfo.orderTotal); 40 | } 41 | 42 | async completeCheckout(){ 43 | await this.finishButton.click(); 44 | } 45 | } 46 | 47 | export default CheckoutStepTwoPage; 48 | -------------------------------------------------------------------------------- /tests/ui-pom/pages/footer-page.ts: -------------------------------------------------------------------------------- 1 | import { type Page, expect, type BrowserContext } from '@playwright/test'; 2 | 3 | class FooterPage { 4 | readonly page: Page; 5 | readonly context: BrowserContext; 6 | 7 | constructor(page: Page, context: BrowserContext) { 8 | this.page = page; 9 | this.context = context; 10 | } 11 | 12 | async openAndCheckNewPage(link: string, url: RegExp) { 13 | const pagePromise = this.context.waitForEvent('page'); 14 | await this.page.getByRole('link', { name: link }).click(); 15 | const newPage = await pagePromise; 16 | await newPage.waitForLoadState(); 17 | await expect(newPage).toHaveURL(url); 18 | } 19 | } 20 | 21 | export default FooterPage; 22 | -------------------------------------------------------------------------------- /tests/ui-pom/pages/header-page.ts: -------------------------------------------------------------------------------- 1 | import { type Page, type Locator } from '@playwright/test'; 2 | 3 | class HeaderPage { 4 | readonly page: Page; 5 | readonly menuButton: Locator; 6 | readonly cartButton: Locator; 7 | 8 | constructor(page: Page) { 9 | this.page = page; 10 | this.menuButton = page.getByRole('button', { name: 'Open Menu' }); 11 | this.cartButton = page.locator('#shopping_cart_container'); 12 | } 13 | 14 | async goToCart(items: number){ 15 | await this.cartButton.filter( { hasText: items.toString() }).click(); 16 | } 17 | } 18 | 19 | export default HeaderPage; 20 | -------------------------------------------------------------------------------- /tests/ui-pom/pages/inventory-page.ts: -------------------------------------------------------------------------------- 1 | import { type Page, type Locator, expect } from '@playwright/test'; 2 | 3 | class InventoryPage { 4 | readonly page: Page; 5 | readonly addToCartButton: Locator; 6 | readonly removeFromCartButton: Locator; 7 | readonly productSortContainer: Locator; 8 | readonly inventoryContainer: Locator; 9 | 10 | constructor(page: Page) { 11 | this.page = page; 12 | this.addToCartButton = page.getByRole('button', { name: 'Add to cart' }); 13 | this.removeFromCartButton = page.getByRole('button', { name: 'Remove' }); 14 | this.productSortContainer = page.getByTestId('product_sort_container'); 15 | this.inventoryContainer = page.locator('#inventory_container').first(); 16 | } 17 | 18 | async addToCart() { 19 | await this.addToCartButton.first().click(); 20 | } 21 | 22 | async removeFromCart(){ 23 | await this.removeFromCartButton.first().click(); 24 | } 25 | 26 | async sortProducts(option: string){ 27 | await this.productSortContainer.selectOption(option); 28 | } 29 | 30 | async checkSort(product: string){ 31 | await expect(this.inventoryContainer).toContainText(product); 32 | } 33 | } 34 | 35 | export default InventoryPage; 36 | -------------------------------------------------------------------------------- /tests/ui-pom/pages/login-page.ts: -------------------------------------------------------------------------------- 1 | import { type Page, type Locator , expect } from '@playwright/test'; 2 | import messages from '../../utils/messages'; 3 | import HeaderPage from './header-page'; 4 | 5 | class LoginPage { 6 | readonly page: Page; 7 | readonly userName: Locator; 8 | readonly loginButton: Locator; 9 | readonly password: Locator; 10 | readonly messagePanel: Locator; 11 | 12 | constructor(page: Page) { 13 | this.page = page; 14 | this.userName = page.getByPlaceholder('Username'); 15 | this.loginButton = page.getByText('Login', { exact: true }); 16 | this.password = page.getByPlaceholder('Password'); 17 | this.messagePanel = page.getByTestId('error'); 18 | } 19 | 20 | async fillEmail(email: string) { 21 | await this.userName.fill(email); 22 | } 23 | 24 | async fillPassword(password: string) { 25 | await this.password.fill(password); 26 | } 27 | 28 | async doLogin(email: string, password: string) { 29 | await this.fillEmail(email); 30 | await this.fillPassword(password); 31 | await this.loginButton.click(); 32 | } 33 | 34 | async checkLoggedIn() { 35 | const headerPage = new HeaderPage(this.page); 36 | await expect(this.page).toHaveURL(/.*inventory.html/); 37 | await expect(this.page).toHaveTitle(/Swag Labs/); 38 | await expect(headerPage.menuButton).toBeVisible(); 39 | } 40 | 41 | async checkInvalidCredentials(invalidUserType: string) { 42 | await expect(this.messagePanel).toHaveText(messages.login[invalidUserType]); 43 | } 44 | } 45 | 46 | export default LoginPage; 47 | -------------------------------------------------------------------------------- /tests/ui-pom/specs/checkout-pom.spec.ts: -------------------------------------------------------------------------------- 1 | import { test } from '@playwright/test'; 2 | import CartPage from '../pages/cart-page'; 3 | import CheckoutStepCompletePage from '../pages/checkout-complete'; 4 | import CheckoutStepOnePage from '../pages/checkout-step-one'; 5 | import CheckoutStepTwoPage from '../pages/checkout-step-two'; 6 | import HeaderPage from '../pages/header-page'; 7 | import InventoryPage from '../pages/inventory-page'; 8 | import pages from '../../utils/pages'; 9 | import productData from '../../data/product-data'; 10 | import userData from '../../data/user-data'; 11 | 12 | const orderInfo = { 13 | itemTotal: /.*39.98/, 14 | tax: /.*3.20/, 15 | orderTotal: /.*43.18/, 16 | }; 17 | 18 | let inventoryPage: InventoryPage; 19 | let headerPage: HeaderPage; 20 | let cartPage: CartPage; 21 | let checkoutStepOnePage: CheckoutStepOnePage; 22 | let checkoutStepTwoPage: CheckoutStepTwoPage; 23 | let checkoutStepCompletePage: CheckoutStepCompletePage; 24 | 25 | test.beforeEach(async ({ page }) => { 26 | await page.goto(pages.homePage); 27 | inventoryPage = new InventoryPage(page); 28 | headerPage = new HeaderPage(page); 29 | cartPage = new CartPage(page); 30 | checkoutStepOnePage = new CheckoutStepOnePage(page); 31 | checkoutStepTwoPage = new CheckoutStepTwoPage(page); 32 | checkoutStepCompletePage = new CheckoutStepCompletePage(page); 33 | }); 34 | 35 | test.describe('Checkout with Page Object Model', () => { 36 | test(`successfull checkout`, async () => { 37 | await inventoryPage.addToCart(); 38 | await inventoryPage.addToCart(); 39 | await inventoryPage.removeFromCart(); 40 | await inventoryPage.addToCart(); 41 | await headerPage.goToCart(productData.items); 42 | await cartPage.checkItemsInCart(productData.items); 43 | await cartPage.goToCheckout(); 44 | await checkoutStepOnePage.fillInformationAndContinue(userData); 45 | await checkoutStepTwoPage.checkOrderInfo(orderInfo); 46 | await checkoutStepTwoPage.completeCheckout(); 47 | await checkoutStepCompletePage.checkCheckoutSucessfull(); 48 | await checkoutStepCompletePage.goBackToHome(); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /tests/ui-pom/specs/footer-pom.spec.ts: -------------------------------------------------------------------------------- 1 | import { test } from '@playwright/test'; 2 | import FooterPage from '../pages/footer-page'; 3 | import pages from '../../utils/pages'; 4 | import footerLinks from '../../utils/footer-links'; 5 | 6 | let footerPage: FooterPage; 7 | 8 | test.beforeEach(async ({ page, context }) => { 9 | await page.goto(pages.homePage); 10 | footerPage = new FooterPage(page, context); 11 | }); 12 | 13 | test.describe('Footer with Page Object Model', () => { 14 | for(const option in footerLinks){ 15 | test(`successfully open ${footerLinks[option].link}`, async () => { 16 | await footerPage.openAndCheckNewPage(footerLinks[option].link, (footerLinks[option].url)); 17 | }); 18 | } 19 | }); 20 | -------------------------------------------------------------------------------- /tests/ui-pom/specs/login-pom.spec.ts: -------------------------------------------------------------------------------- 1 | import { test } from '@playwright/test'; 2 | import LoginPage from '../pages/login-page'; 3 | import pages from '../../utils/pages'; 4 | import userData from '../../data/user-data'; 5 | 6 | const userName = userData; 7 | const password = process.env.PASSWORD; 8 | let loginPage: LoginPage; 9 | 10 | test.use({ storageState: undefined }); // doesn't share the logged in session 11 | 12 | test.beforeEach(async ({ page }) => { 13 | await page.goto(pages.loginPage); 14 | loginPage = new LoginPage(page); 15 | }); 16 | 17 | test.describe('Login with Page Object Model', () => { 18 | test(`successfull login`, async () => { 19 | await loginPage.doLogin(userName.validUser, password!); 20 | await loginPage.checkLoggedIn(); 21 | }); 22 | 23 | for(const invalidUserType in userData.invalidUser){ 24 | test(`failing login for ${invalidUserType}`, async () => { 25 | const invalidUsername = userData.invalidUser[invalidUserType]; 26 | await loginPage.doLogin(invalidUsername, password!); 27 | await loginPage.checkInvalidCredentials(invalidUserType); 28 | }); 29 | } 30 | }); 31 | -------------------------------------------------------------------------------- /tests/ui-pom/specs/sort-pom.spec.ts: -------------------------------------------------------------------------------- 1 | import { test } from '@playwright/test'; 2 | import InventoryPage from '../pages/inventory-page'; 3 | import pages from '../../utils/pages'; 4 | 5 | let inventoryPage: InventoryPage; 6 | const sortOptions = { 7 | az: 'Sauce Labs Backpack', 8 | za: 'Test.allTheThings() T-Shirt (Red)', 9 | lohi: 'Sauce Labs Onesie', 10 | hilo: 'Sauce Labs Fleece Jacket', 11 | } 12 | 13 | test.beforeEach(async ({ page }) => { 14 | await page.goto(pages.homePage); 15 | inventoryPage = new InventoryPage(page); 16 | }); 17 | 18 | test.describe('Sort with Page Object Model', () => { 19 | for(const option in sortOptions){ 20 | test(`successfully sort by ${option}`, async ({page}) => { 21 | await inventoryPage.sortProducts(option); 22 | await inventoryPage.checkSort(sortOptions[option]); 23 | }); 24 | } 25 | }); 26 | -------------------------------------------------------------------------------- /tests/ui/specs/checkout.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@playwright/test'; 2 | 3 | const orderCompletedLabelString = 'Thank you for your order!'; 4 | const orderCompletedTextString = 'Your order has been dispatched, and will arrive just as fast as the pony can get there!'; 5 | 6 | test.beforeEach(async ({ page }) => { 7 | await page.goto('/inventory.html'); 8 | }); 9 | 10 | test.describe('Checkout without Page Object Model', () => { 11 | test(`successfull checkout`, async ({page}) => { 12 | await test.step('add/remove items to/from cart', async () => { 13 | await page.getByTestId('add-to-cart-sauce-labs-backpack').click(); 14 | await expect(page.locator('#shopping_cart_container')).toHaveText('1'); 15 | await page.getByRole('button', { name: 'Add to cart' }).first().click(); 16 | await expect(page.locator('#shopping_cart_container')).toHaveText('2'); 17 | await page.getByRole('button', { name: 'Remove' }).first().click(); 18 | await expect(page.locator('#shopping_cart_container')).toHaveText('1'); 19 | await page.locator(`[name='add-to-cart-sauce-labs-bolt-t-shirt']`).click(); 20 | }); 21 | 22 | await test.step('go to cart', async () => { 23 | await page.locator('#shopping_cart_container').filter( { hasText: '2' }).click(); 24 | await expect(page.getByRole('button', { name: 'Remove' })).toHaveCount(2); 25 | }); 26 | 27 | await test.step('go to checkout step one', async () => { 28 | await page.getByRole('button', { name: 'Checkout' }).click(); 29 | }); 30 | 31 | await test.step('fill customer info and go to step two', async () => { 32 | for (const input of await page.getByRole('textbox').all()){ 33 | await expect(input).toBeEmpty(); 34 | } 35 | await page.getByPlaceholder('First Name').fill('Renatinha'); 36 | await page.getByPlaceholder('Last Name').fill('Andrade'); 37 | await page.getByPlaceholder('Zip/Postal Code').fill('2187'); 38 | await page.getByText('Continue', { exact: true }).click(); 39 | }); 40 | 41 | await test.step('check order info and complete checkout', async () => { 42 | await expect(page.getByText('Item total')).toHaveText(/.*25.98/); 43 | await expect(page.getByText('Tax')).toHaveText(/.*2.08/); 44 | await expect(page.getByText('Total:').last()).toHaveText(/.*28.06/); 45 | await page.getByText('Finish', { exact: true }).click(); 46 | }); 47 | 48 | await test.step('check checkout successfull', async () => { 49 | await expect(page.getByText(orderCompletedLabelString)).toBeVisible(); 50 | await expect(page.getByText(orderCompletedTextString)).toBeVisible(); 51 | await expect(page.getByRole('button', { name: 'Back Home' })).toBeVisible(); 52 | }); 53 | 54 | await test.step('back to home', async () => { 55 | await page.getByRole('button', { name: 'Back Home' }).click(); 56 | await expect(page).toHaveURL(/.*inventory.html/); 57 | }); 58 | }); 59 | }); 60 | 61 | 62 | -------------------------------------------------------------------------------- /tests/ui/specs/footer.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@playwright/test'; 2 | import footerLinks from '../../utils/footer-links'; 3 | 4 | test.describe('Footer without Page Object Model', () => { 5 | for(const option in footerLinks){ 6 | test(`successfully open ${footerLinks[option].link}`, 7 | async ({page, context}) => { 8 | await page.goto('/inventory.html'); 9 | const pagePromise = context.waitForEvent('page'); 10 | await page.getByRole('link', { name: footerLinks[option].link }) 11 | .click(); 12 | const newPage = await pagePromise; 13 | await newPage.waitForLoadState(); 14 | await expect(newPage).toHaveURL(footerLinks[option].url); 15 | }); 16 | } 17 | }); 18 | 19 | 20 | -------------------------------------------------------------------------------- /tests/ui/specs/login-simple.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@playwright/test'; 2 | 3 | test.use({ storageState: { cookies: [], origins: [] } }); 4 | 5 | test.beforeEach(async ({ page }) => { 6 | await page.goto('/'); 7 | }); 8 | 9 | test.describe('Simple Login', () => { 10 | test(`successfull login`, async ({ page }) => { 11 | await page.getByPlaceholder('Username') 12 | .fill(process.env.USERNAME!); 13 | await page.getByTestId('password') 14 | .fill(process.env.PASSWORD!); 15 | await page.getByText('Login', { exact: true }) 16 | .click(); 17 | await expect(page).toHaveURL(/.*inventory.html/) 18 | await expect(page).toHaveTitle(/Swag Labs/); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /tests/ui/specs/login.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect, type Page } from '@playwright/test'; 2 | import messages from '../../utils/messages'; 3 | import userData from '../../data/user-data'; 4 | 5 | const userName = userData; 6 | const password = process.env.PASSWORD!; 7 | 8 | test.use({ storageState: { cookies: [], origins: [] } }); 9 | 10 | test.beforeEach(async ({ page }) => { 11 | await page.goto('/'); 12 | }); 13 | 14 | test.describe.skip('Login without Page Object Model', () => { 15 | test(`successfull login`, async ({ page }) => { 16 | await doLogin(page, userName.validUser, password); 17 | await expect(page).toHaveURL(/.*inventory.html/); 18 | await expect(page).toHaveTitle(/Swag Labs/); 19 | }); 20 | 21 | for(const invalidUserType in userData.invalidUser){ 22 | test(`failing login for ${invalidUserType}`, async ({ page }) => { 23 | const invalidUsername = userData.invalidUser[invalidUserType]; 24 | doLogin(page, invalidUsername, password); 25 | await expect(page.getByTestId('error')).toHaveText(messages.login[invalidUserType]); 26 | }); 27 | } 28 | }); 29 | 30 | async function doLogin(page: Page, userName: string, password: string) { 31 | await page.getByPlaceholder('Username').fill(userName); 32 | await page.getByTestId('password').fill(password); 33 | await page.getByText('Login', { exact: true }).click(); 34 | }; 35 | -------------------------------------------------------------------------------- /tests/ui/specs/sort.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@playwright/test'; 2 | 3 | const sortOptions = { 4 | az: 'Sauce Labs Backpack', 5 | za: 'Test.allTheThings() T-Shirt (Red)', 6 | lohi: 'Sauce Labs Onesie', 7 | hilo: 'Sauce Labs Fleece Jacket', 8 | } 9 | 10 | test.beforeEach(async ({ page }) => { 11 | await page.goto('/inventory.html'); 12 | }); 13 | 14 | test.describe('Sort without Page Object Model', () => { 15 | for(const option in sortOptions){ 16 | test(`successfully sort by ${option}`, async ({page}) => { 17 | await page.getByTestId('product_sort_container') 18 | .selectOption(option); 19 | await expect(page.locator('#inventory_container') 20 | .first()).toContainText(sortOptions[option]); 21 | }); 22 | } 23 | }); 24 | -------------------------------------------------------------------------------- /tests/utils/footer-links.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | linkedIn: { 3 | link: 'LinkedIn', 4 | url: /.*www.linkedin.com.*company.*sauce-labs.*/ 5 | }, 6 | facebook: { 7 | link: 'Facebook', 8 | url: /.*www.facebook.com.*saucelabs/ 9 | }, 10 | twitter: { 11 | link: 'Twitter', 12 | url: /.*twitter.com.*saucelabs.*/ 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /tests/utils/messages.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | customerInfo: { 3 | firstNameRequired: 'Error: First Name is required', 4 | lastNameRequired: 'Error: Last Name is required', 5 | zipRequired: 'Error: Postal Code is required', 6 | }, 7 | login: { 8 | invalidUsername: 'Epic sadface: Username and password do not match any user in this service', 9 | lockedOutdUser: 'Epic sadface: Sorry, this user has been locked out.', 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /tests/utils/pages.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | cartPage: '/cart.html', 3 | checkoutCompletePage: '/checkout-complete.html', 4 | checkoutStepOnePage: '/checkout-step-one.html', 5 | checkoutStepTwoPage: '/checkout-step-two.html', 6 | homePage: '/inventory.html', 7 | loginPage: '/', 8 | }; 9 | --------------------------------------------------------------------------------