├── .gitignore ├── assets └── html-test-report.PNG ├── renovate.json ├── test-data └── booking-details.json ├── tests ├── 04_get_all_booking_details.spec.js ├── 05_get_specific_booking_details.spec.js ├── 06_get_booking_details_query_param.spec.js ├── 07_get_booking_details_query_param.spec.js ├── 02_post_static_json_data.spec.js ├── 10_delete_booking_details.spec.js ├── 01_post_static_data.spec.js ├── 03_post_dynamic_data.spec.js ├── 09_partial_update_booking_details.spec.js └── 08_update_booking_details.spec.js ├── package.json ├── README.md ├── playwright.config.js └── tests-examples └── demo-todo-app.spec.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | /test-results/ 3 | /playwright-report/ 4 | /playwright/.cache/ -------------------------------------------------------------------------------- /assets/html-test-report.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codewithmmak/playwright-api-testing/HEAD/assets/html-test-report.PNG -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:recommended" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /test-data/booking-details.json: -------------------------------------------------------------------------------- 1 | { 2 | "firstname": "Jim", 3 | "lastname": "Brown", 4 | "totalprice": 111, 5 | "depositpaid": true, 6 | "bookingdates": { 7 | "checkin": "2023-06-01", 8 | "checkout": "2023-061-15" 9 | }, 10 | "additionalneeds": "Breakfast" 11 | } -------------------------------------------------------------------------------- /tests/04_get_all_booking_details.spec.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | const { test, expect } = require('@playwright/test'); 3 | 4 | test('should be get all the booking details', async ({ request }) => { 5 | const response = await request.get(`/booking`); 6 | console.log(await response.json()); 7 | expect(response.ok()).toBeTruthy(); 8 | expect(response.status()).toBe(200); 9 | }); -------------------------------------------------------------------------------- /tests/05_get_specific_booking_details.spec.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | const { test, expect } = require('@playwright/test'); 3 | 4 | test('should be get specific booking details', async ({ request }) => { 5 | const response = await request.get(`/booking/1`); 6 | console.log(await response.json()); 7 | expect(response.ok()).toBeTruthy(); 8 | expect(response.status()).toBe(200); 9 | }); -------------------------------------------------------------------------------- /tests/06_get_booking_details_query_param.spec.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | const { test, expect } = require('@playwright/test'); 3 | 4 | test('should be able to get subset of booking details using query parameters', async ({ request }) => { 5 | const response = await request.get(`/booking`, { 6 | params: { 7 | firstname: "Susan", 8 | lastname: "Jackson" 9 | }, 10 | }); 11 | console.log(await response.json()); 12 | expect(response.ok()).toBeTruthy(); 13 | expect(response.status()).toBe(200); 14 | }); -------------------------------------------------------------------------------- /tests/07_get_booking_details_query_param.spec.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | const { test, expect } = require('@playwright/test'); 3 | 4 | test('should be able to get subset of booking details using query parameters - checkin date example', async ({ request }) => { 5 | const response = await request.get(`/booking`, { 6 | params: { 7 | checkin: "2021-01-15", 8 | checkout: "2023-03-25" 9 | }, 10 | }); 11 | console.log(await response.json()); 12 | expect(response.ok()).toBeTruthy(); 13 | expect(response.status()).toBe(200); 14 | }); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "playwright-api-testing", 3 | "version": "1.0.0", 4 | "description": "---\r # Playwright API Testing Setup Guide\r ---", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "npx playwright test" 8 | }, 9 | "keywords": [ 10 | "playwright", 11 | "javascript", 12 | "fakerjs", 13 | "playwright api testing", 14 | "playwright api testing example" 15 | ], 16 | "author": "Code with MMAK", 17 | "license": "ISC", 18 | "devDependencies": { 19 | "@faker-js/faker": "^8.0.2", 20 | "@playwright/test": "^1.37.0", 21 | "luxon": "^3.4.0", 22 | "npm-check-updates": "^16.11.1", 23 | "rimraf": "^5.0.1" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tests/02_post_static_json_data.spec.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | const { test, expect } = require('@playwright/test'); 3 | const bookingDetails = require('../test-data/booking-details.json'); 4 | //testcase 2 5 | test('should be able to create a booking', async ({ request }) => { 6 | const response = await request.post(`/booking`, { 7 | data: bookingDetails 8 | }); 9 | console.log(await response.json()); 10 | expect(response.ok()).toBeTruthy(); 11 | expect(response.status()).toBe(200); 12 | const responseBody = await response.json() 13 | expect(responseBody.booking).toHaveProperty("firstname", "Jim"); 14 | expect(responseBody.booking).toHaveProperty("lastname", "Brown"); 15 | expect(responseBody.booking).toHaveProperty("totalprice", 111); 16 | expect(responseBody.booking).toHaveProperty("depositpaid", true); 17 | }); -------------------------------------------------------------------------------- /tests/10_delete_booking_details.spec.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | const { test, expect } = require('@playwright/test'); 3 | 4 | var token 5 | 6 | test('should be able to delete the booking details', async ({ request }) => { 7 | 8 | // Create a Token which will be used in DELETE request 9 | 10 | const response = await request.post(`/auth`, { 11 | data: { 12 | "username": "admin", 13 | "password": "password123" 14 | } 15 | }); 16 | console.log(await response.json()); 17 | expect(response.ok()).toBeTruthy(); 18 | expect(response.status()).toBe(200); 19 | const responseBody = await response.json(); 20 | token = responseBody.token; 21 | console.log("New Token is: " + token); 22 | 23 | // DELETE 24 | 25 | const deleteRequest = await request.delete(`/booking/1`, { 26 | headers: { 27 | 'Content-Type': 'application/json', 28 | 'Cookie': `token=${token}` 29 | } 30 | }); 31 | expect(deleteRequest.status()).toEqual(201); 32 | expect(deleteRequest.statusText()).toBe('Created'); 33 | }); -------------------------------------------------------------------------------- /tests/01_post_static_data.spec.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | const { test, expect } = require('@playwright/test'); 3 | 4 | //test case 1 5 | test('should be able to create a booking', async ({ request }) => { 6 | const response = await request.post(`/booking`, { 7 | data: { 8 | "firstname": "Jim", 9 | "lastname": "Brown", 10 | "totalprice": 111, 11 | "depositpaid": true, 12 | "bookingdates": { 13 | "checkin": "2023-06-01", 14 | "checkout": "2023-06-15" 15 | }, 16 | "additionalneeds": "Breakfast" 17 | } 18 | }); 19 | console.log(await response.json()); 20 | expect(response.ok()).toBeTruthy(); 21 | expect(response.status()).toBe(200); 22 | const responseBody = await response.json() 23 | expect(responseBody.booking).toHaveProperty("firstname", "Jim"); 24 | expect(responseBody.booking).toHaveProperty("lastname", "Brown"); 25 | expect(responseBody.booking).toHaveProperty("totalprice", 111); 26 | expect(responseBody.booking).toHaveProperty("depositpaid", true); 27 | }); -------------------------------------------------------------------------------- /tests/03_post_dynamic_data.spec.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | const { test, expect } = require('@playwright/test'); 3 | import { faker } from '@faker-js/faker'; 4 | // const { DateTime } = require("luxon"); 5 | 6 | const randomFirstName = faker.name.firstName() 7 | const randomLastName = faker.name.lastName() 8 | const randomNumber = faker.random.numeric(4) 9 | // const currentDate = DateTime.now().toFormat('yyyy-MM-dd') 10 | // const currentDatePlusFive = DateTime.now().plus({ days: 5 }).toFormat('yyyy-MM-dd') 11 | 12 | test('should be able to create a booking', async ({ request }) => { 13 | const response = await request.post(`/booking`, { 14 | data: { 15 | "firstname": randomFirstName, 16 | "lastname": randomLastName, 17 | "totalprice": randomNumber, 18 | "depositpaid": true, 19 | // "bookingdates": { 20 | // "checkin": currentDate, 21 | // "checkout": currentDatePlusFive 22 | // }, 23 | "additionalneeds": "Breakfast" 24 | } 25 | }); 26 | console.log(await response.json()); 27 | expect(response.ok()).toBeTruthy(); 28 | expect(response.status()).toBe(200); 29 | const responseBody = await response.json() 30 | expect(responseBody.booking).toHaveProperty("firstname", randomFirstName); 31 | expect(responseBody.booking).toHaveProperty("lastname", randomLastName); 32 | }); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | --- 2 | # Playwright API Testing Setup Guide 3 | --- 4 | 5 | This is a Playwright API testing framework designed to demonstrate playwright api testing example 6 | 7 | ## Features of this framework 8 | * Playwright API Testing 9 | 10 | ## Getting started 11 | 12 | ### Pre-requisites 13 | * Download and install Node.js 14 | * Download and install any Text Editor like Visual Code/Sublime/Brackets 15 | 16 | ### Setup Scripts 17 | * Clone the repository into a folder 18 | * Go to Project root directory and install Dependency: `npm install` 19 | * All the dependencies from package.json would be installed in node_modules folder. 20 | 21 | ### Install Visual Code Extension (Optional) 22 | * Playwright Test for VSCode 23 | * GitLens — Git supercharged by GitKraken 24 | * Material Icon Theme 25 | 26 | ### Update Visual Code Settings 27 | * Go to Visual Code Preference > Setting and search `formatOnSave` and enable/ON it. 28 | 29 | ## How to Run Test Locally 30 | * Go to the Project root directory and run command: `npm test` 31 | 32 | ## How to Run Single Spec Locally 33 | * Go to the Project root directory and run command: `npx playwright test tests/01_post_static_data.spec.js` 34 | 35 | ## How to view default Playwright HTML report 36 | * Go to the Project root directory: `./playwright-report/index.html` 37 | 38 | ### Playwright HTML Test Report 39 | ![Playwright HTML Test Report](./assets/html-test-report.PNG?raw=true "Playwright HTML Test Report") -------------------------------------------------------------------------------- /tests/09_partial_update_booking_details.spec.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | const { test, expect } = require('@playwright/test'); 3 | 4 | var token 5 | 6 | test('should be able to partial update the booking details', async ({ request }) => { 7 | 8 | // Create a Token which will be used in PATCH request 9 | 10 | const response = await request.post(`/auth`, { 11 | data: { 12 | "username": "admin", 13 | "password": "password123" 14 | } 15 | }); 16 | console.log(await response.json()); 17 | expect(response.ok()).toBeTruthy(); 18 | expect(response.status()).toBe(200); 19 | const responseBody = await response.json(); 20 | token = responseBody.token; 21 | console.log("New Token is: " + token); 22 | 23 | // PATCH 24 | 25 | const partialUpdateRequest = await request.patch(`/booking/1`, { 26 | headers: { 27 | 'Content-Type': 'application/json', 28 | 'Accept': 'application/json', 29 | 'Cookie': `token=${token}` 30 | }, 31 | data: { 32 | "firstname": "Sim", 33 | "lastname": "Son", 34 | "totalprice": 333, 35 | "depositpaid": false 36 | } 37 | }); 38 | console.log(await partialUpdateRequest.json()); 39 | expect(partialUpdateRequest.ok()).toBeTruthy(); 40 | expect(partialUpdateRequest.status()).toBe(200); 41 | const partialUpdatedResponseBody = await partialUpdateRequest.json() 42 | expect(partialUpdatedResponseBody).toHaveProperty("firstname", "Sim"); 43 | expect(partialUpdatedResponseBody).toHaveProperty("lastname", "Son"); 44 | expect(partialUpdatedResponseBody).toHaveProperty("totalprice", 333); 45 | expect(partialUpdatedResponseBody).toHaveProperty("depositpaid", false); 46 | }); -------------------------------------------------------------------------------- /tests/08_update_booking_details.spec.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | const { test, expect } = require('@playwright/test'); 3 | 4 | var token 5 | 6 | test('should be able to update the booking details', async ({ request }) => { 7 | 8 | // Create a Token which will be used in PUT request 9 | 10 | const response = await request.post(`/auth`, { 11 | data: { 12 | "username": "admin", 13 | "password": "password123" 14 | } 15 | }); 16 | console.log(await response.json()); 17 | expect(response.ok()).toBeTruthy(); 18 | expect(response.status()).toBe(200); 19 | const responseBody = await response.json(); 20 | token = responseBody.token; 21 | console.log("New Token is: " + token); 22 | 23 | // PUT 24 | const updateRequest = await request.put(`/booking/1`, { 25 | headers: { 26 | 'Content-Type': 'application/json', 27 | 'Accept': 'application/json', 28 | 'Cookie': `token=${token}`, 29 | }, 30 | data: { 31 | "firstname": "Jim", 32 | "lastname": "Brown", 33 | "totalprice": 111, 34 | "depositpaid": true, 35 | "bookingdates": { 36 | "checkin": "2023-06-01", 37 | "checkout": "2023-06-15" 38 | }, 39 | "additionalneeds": "Breakfast" 40 | } 41 | }); 42 | console.log(await updateRequest.json()); 43 | expect(updateRequest.ok()).toBeTruthy(); 44 | expect(updateRequest.status()).toBe(200); 45 | const updatedResponseBody = await updateRequest.json() 46 | expect(updatedResponseBody).toHaveProperty("firstname", "Jim"); 47 | expect(updatedResponseBody).toHaveProperty("lastname", "Brown"); 48 | expect(updatedResponseBody).toHaveProperty("totalprice", 111); 49 | expect(updatedResponseBody).toHaveProperty("depositpaid", true); 50 | }); -------------------------------------------------------------------------------- /playwright.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | const { defineConfig, devices } = require('@playwright/test'); 3 | 4 | /** 5 | * Read environment variables from file. 6 | * https://github.com/motdotla/dotenv 7 | */ 8 | // require('dotenv').config(); 9 | 10 | /** 11 | * @see https://playwright.dev/docs/test-configuration 12 | */ 13 | module.exports = defineConfig({ 14 | testDir: './tests', 15 | /* Maximum time one test can run for. */ 16 | timeout: 30 * 1000, 17 | expect: { 18 | /** 19 | * Maximum time expect() should wait for the condition to be met. 20 | * For example in `await expect(locator).toHaveText();` 21 | */ 22 | timeout: 5000 23 | }, 24 | /* Run tests in files in parallel */ 25 | fullyParallel: true, 26 | /* Fail the build on CI if you accidentally left test.only in the source code. */ 27 | forbidOnly: !!process.env.CI, 28 | /* Retry on CI only */ 29 | retries: process.env.CI ? 2 : 0, 30 | /* Opt out of parallel tests on CI. */ 31 | workers: process.env.CI ? 1 : undefined, 32 | /* Reporter to use. See https://playwright.dev/docs/test-reporters */ 33 | reporter: 'html', 34 | /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ 35 | use: { 36 | /* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */ 37 | actionTimeout: 0, 38 | /* Base URL to use in actions like `await page.goto('/')`. */ 39 | baseURL: 'https://restful-booker.herokuapp.com', 40 | // baseURL: 'https://petstore.swagger.io/v2', 41 | 42 | /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ 43 | trace: 'on-first-retry', 44 | }, 45 | 46 | /* Configure projects for major browsers */ 47 | projects: [ 48 | { 49 | name: 'chromium', 50 | use: { ...devices['Desktop Chrome'] }, 51 | }, 52 | 53 | // { 54 | // name: 'firefox', 55 | // use: { ...devices['Desktop Firefox'] }, 56 | // }, 57 | 58 | // { 59 | // name: 'webkit', 60 | // use: { ...devices['Desktop Safari'] }, 61 | // }, 62 | 63 | /* Test against mobile viewports. */ 64 | // { 65 | // name: 'Mobile Chrome', 66 | // use: { ...devices['Pixel 5'] }, 67 | // }, 68 | // { 69 | // name: 'Mobile Safari', 70 | // use: { ...devices['iPhone 12'] }, 71 | // }, 72 | 73 | /* Test against branded browsers. */ 74 | // { 75 | // name: 'Microsoft Edge', 76 | // use: { channel: 'msedge' }, 77 | // }, 78 | // { 79 | // name: 'Google Chrome', 80 | // use: { channel: 'chrome' }, 81 | // }, 82 | ], 83 | 84 | /* Folder for test artifacts such as screenshots, videos, traces, etc. */ 85 | // outputDir: 'test-results/', 86 | 87 | /* Run your local dev server before starting the tests */ 88 | // webServer: { 89 | // command: 'npm run start', 90 | // port: 3000, 91 | // }, 92 | }); 93 | 94 | -------------------------------------------------------------------------------- /tests-examples/demo-todo-app.spec.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | const { test, expect } = require('@playwright/test'); 3 | 4 | test.beforeEach(async ({ page }) => { 5 | await page.goto('https://demo.playwright.dev/todomvc'); 6 | }); 7 | 8 | const TODO_ITEMS = [ 9 | 'buy some cheese', 10 | 'feed the cat', 11 | 'book a doctors appointment' 12 | ]; 13 | 14 | test.describe('New Todo', () => { 15 | test('should allow me to add todo items', async ({ page }) => { 16 | // create a new todo locator 17 | const newTodo = page.getByPlaceholder('What needs to be done?'); 18 | 19 | // Create 1st todo. 20 | await newTodo.fill(TODO_ITEMS[0]); 21 | await newTodo.press('Enter'); 22 | 23 | // Make sure the list only has one todo item. 24 | await expect(page.getByTestId('todo-title')).toHaveText([ 25 | TODO_ITEMS[0] 26 | ]); 27 | 28 | // Create 2nd todo. 29 | await newTodo.fill(TODO_ITEMS[1]); 30 | await newTodo.press('Enter'); 31 | 32 | // Make sure the list now has two todo items. 33 | await expect(page.getByTestId('todo-title')).toHaveText([ 34 | TODO_ITEMS[0], 35 | TODO_ITEMS[1] 36 | ]); 37 | 38 | await checkNumberOfTodosInLocalStorage(page, 2); 39 | }); 40 | 41 | test('should clear text input field when an item is added', async ({ page }) => { 42 | // create a new todo locator 43 | const newTodo = page.getByPlaceholder('What needs to be done?'); 44 | 45 | // Create one todo item. 46 | await newTodo.fill(TODO_ITEMS[0]); 47 | await newTodo.press('Enter'); 48 | 49 | // Check that input is empty. 50 | await expect(newTodo).toBeEmpty(); 51 | await checkNumberOfTodosInLocalStorage(page, 1); 52 | }); 53 | 54 | test('should append new items to the bottom of the list', async ({ page }) => { 55 | // Create 3 items. 56 | await createDefaultTodos(page); 57 | 58 | // create a todo count locator 59 | const todoCount = page.getByTestId('todo-count') 60 | 61 | // Check test using different methods. 62 | await expect(page.getByText('3 items left')).toBeVisible(); 63 | await expect(todoCount).toHaveText('3 items left'); 64 | await expect(todoCount).toContainText('3'); 65 | await expect(todoCount).toHaveText(/3/); 66 | 67 | // Check all items in one call. 68 | await expect(page.getByTestId('todo-title')).toHaveText(TODO_ITEMS); 69 | await checkNumberOfTodosInLocalStorage(page, 3); 70 | }); 71 | }); 72 | 73 | test.describe('Mark all as completed', () => { 74 | test.beforeEach(async ({ page }) => { 75 | await createDefaultTodos(page); 76 | await checkNumberOfTodosInLocalStorage(page, 3); 77 | }); 78 | 79 | test.afterEach(async ({ page }) => { 80 | await checkNumberOfTodosInLocalStorage(page, 3); 81 | }); 82 | 83 | test('should allow me to mark all items as completed', async ({ page }) => { 84 | // Complete all todos. 85 | await page.getByLabel('Mark all as complete').check(); 86 | 87 | // Ensure all todos have 'completed' class. 88 | await expect(page.getByTestId('todo-item')).toHaveClass(['completed', 'completed', 'completed']); 89 | await checkNumberOfCompletedTodosInLocalStorage(page, 3); 90 | }); 91 | 92 | test('should allow me to clear the complete state of all items', async ({ page }) => { 93 | const toggleAll = page.getByLabel('Mark all as complete'); 94 | // Check and then immediately uncheck. 95 | await toggleAll.check(); 96 | await toggleAll.uncheck(); 97 | 98 | // Should be no completed classes. 99 | await expect(page.getByTestId('todo-item')).toHaveClass(['', '', '']); 100 | }); 101 | 102 | test('complete all checkbox should update state when items are completed / cleared', async ({ page }) => { 103 | const toggleAll = page.getByLabel('Mark all as complete'); 104 | await toggleAll.check(); 105 | await expect(toggleAll).toBeChecked(); 106 | await checkNumberOfCompletedTodosInLocalStorage(page, 3); 107 | 108 | // Uncheck first todo. 109 | const firstTodo = page.getByTestId('todo-item').nth(0); 110 | await firstTodo.getByRole('checkbox').uncheck(); 111 | 112 | // Reuse toggleAll locator and make sure its not checked. 113 | await expect(toggleAll).not.toBeChecked(); 114 | 115 | await firstTodo.getByRole('checkbox').check(); 116 | await checkNumberOfCompletedTodosInLocalStorage(page, 3); 117 | 118 | // Assert the toggle all is checked again. 119 | await expect(toggleAll).toBeChecked(); 120 | }); 121 | }); 122 | 123 | test.describe('Item', () => { 124 | 125 | test('should allow me to mark items as complete', async ({ page }) => { 126 | // create a new todo locator 127 | const newTodo = page.getByPlaceholder('What needs to be done?'); 128 | 129 | // Create two items. 130 | for (const item of TODO_ITEMS.slice(0, 2)) { 131 | await newTodo.fill(item); 132 | await newTodo.press('Enter'); 133 | } 134 | 135 | // Check first item. 136 | const firstTodo = page.getByTestId('todo-item').nth(0); 137 | await firstTodo.getByRole('checkbox').check(); 138 | await expect(firstTodo).toHaveClass('completed'); 139 | 140 | // Check second item. 141 | const secondTodo = page.getByTestId('todo-item').nth(1); 142 | await expect(secondTodo).not.toHaveClass('completed'); 143 | await secondTodo.getByRole('checkbox').check(); 144 | 145 | // Assert completed class. 146 | await expect(firstTodo).toHaveClass('completed'); 147 | await expect(secondTodo).toHaveClass('completed'); 148 | }); 149 | 150 | test('should allow me to un-mark items as complete', async ({ page }) => { 151 | // create a new todo locator 152 | const newTodo = page.getByPlaceholder('What needs to be done?'); 153 | 154 | // Create two items. 155 | for (const item of TODO_ITEMS.slice(0, 2)) { 156 | await newTodo.fill(item); 157 | await newTodo.press('Enter'); 158 | } 159 | 160 | const firstTodo = page.getByTestId('todo-item').nth(0); 161 | const secondTodo = page.getByTestId('todo-item').nth(1); 162 | const firstTodoCheckbox = firstTodo.getByRole('checkbox'); 163 | 164 | await firstTodoCheckbox.check(); 165 | await expect(firstTodo).toHaveClass('completed'); 166 | await expect(secondTodo).not.toHaveClass('completed'); 167 | await checkNumberOfCompletedTodosInLocalStorage(page, 1); 168 | 169 | await firstTodoCheckbox.uncheck(); 170 | await expect(firstTodo).not.toHaveClass('completed'); 171 | await expect(secondTodo).not.toHaveClass('completed'); 172 | await checkNumberOfCompletedTodosInLocalStorage(page, 0); 173 | }); 174 | 175 | test('should allow me to edit an item', async ({ page }) => { 176 | await createDefaultTodos(page); 177 | 178 | const todoItems = page.getByTestId('todo-item'); 179 | const secondTodo = todoItems.nth(1); 180 | await secondTodo.dblclick(); 181 | await expect(secondTodo.getByRole('textbox', { name: 'Edit' })).toHaveValue(TODO_ITEMS[1]); 182 | await secondTodo.getByRole('textbox', { name: 'Edit' }).fill('buy some sausages'); 183 | await secondTodo.getByRole('textbox', { name: 'Edit' }).press('Enter'); 184 | 185 | // Explicitly assert the new text value. 186 | await expect(todoItems).toHaveText([ 187 | TODO_ITEMS[0], 188 | 'buy some sausages', 189 | TODO_ITEMS[2] 190 | ]); 191 | await checkTodosInLocalStorage(page, 'buy some sausages'); 192 | }); 193 | }); 194 | 195 | test.describe('Editing', () => { 196 | test.beforeEach(async ({ page }) => { 197 | await createDefaultTodos(page); 198 | await checkNumberOfTodosInLocalStorage(page, 3); 199 | }); 200 | 201 | test('should hide other controls when editing', async ({ page }) => { 202 | const todoItem = page.getByTestId('todo-item').nth(1); 203 | await todoItem.dblclick(); 204 | await expect(todoItem.getByRole('checkbox')).not.toBeVisible(); 205 | await expect(todoItem.locator('label', { 206 | hasText: TODO_ITEMS[1], 207 | })).not.toBeVisible(); 208 | await checkNumberOfTodosInLocalStorage(page, 3); 209 | }); 210 | 211 | test('should save edits on blur', async ({ page }) => { 212 | const todoItems = page.getByTestId('todo-item'); 213 | await todoItems.nth(1).dblclick(); 214 | await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill('buy some sausages'); 215 | await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).dispatchEvent('blur'); 216 | 217 | await expect(todoItems).toHaveText([ 218 | TODO_ITEMS[0], 219 | 'buy some sausages', 220 | TODO_ITEMS[2], 221 | ]); 222 | await checkTodosInLocalStorage(page, 'buy some sausages'); 223 | }); 224 | 225 | test('should trim entered text', async ({ page }) => { 226 | const todoItems = page.getByTestId('todo-item'); 227 | await todoItems.nth(1).dblclick(); 228 | await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill(' buy some sausages '); 229 | await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Enter'); 230 | 231 | await expect(todoItems).toHaveText([ 232 | TODO_ITEMS[0], 233 | 'buy some sausages', 234 | TODO_ITEMS[2], 235 | ]); 236 | await checkTodosInLocalStorage(page, 'buy some sausages'); 237 | }); 238 | 239 | test('should remove the item if an empty text string was entered', async ({ page }) => { 240 | const todoItems = page.getByTestId('todo-item'); 241 | await todoItems.nth(1).dblclick(); 242 | await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill(''); 243 | await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Enter'); 244 | 245 | await expect(todoItems).toHaveText([ 246 | TODO_ITEMS[0], 247 | TODO_ITEMS[2], 248 | ]); 249 | }); 250 | 251 | test('should cancel edits on escape', async ({ page }) => { 252 | const todoItems = page.getByTestId('todo-item'); 253 | await todoItems.nth(1).dblclick(); 254 | await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill('buy some sausages'); 255 | await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Escape'); 256 | await expect(todoItems).toHaveText(TODO_ITEMS); 257 | }); 258 | }); 259 | 260 | test.describe('Counter', () => { 261 | test('should display the current number of todo items', async ({ page }) => { 262 | // create a new todo locator 263 | const newTodo = page.getByPlaceholder('What needs to be done?'); 264 | 265 | // create a todo count locator 266 | const todoCount = page.getByTestId('todo-count') 267 | 268 | await newTodo.fill(TODO_ITEMS[0]); 269 | await newTodo.press('Enter'); 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) { 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 | /** 422 | * @param {import('@playwright/test').Page} page 423 | * @param {number} expected 424 | */ 425 | async function checkNumberOfTodosInLocalStorage(page, expected) { 426 | return await page.waitForFunction(e => { 427 | return JSON.parse(localStorage['react-todos']).length === e; 428 | }, expected); 429 | } 430 | 431 | /** 432 | * @param {import('@playwright/test').Page} page 433 | * @param {number} expected 434 | */ 435 | async function checkNumberOfCompletedTodosInLocalStorage(page, expected) { 436 | return await page.waitForFunction(e => { 437 | return JSON.parse(localStorage['react-todos']).filter(i => i.completed).length === e; 438 | }, expected); 439 | } 440 | 441 | /** 442 | * @param {import('@playwright/test').Page} page 443 | * @param {string} title 444 | */ 445 | async function checkTodosInLocalStorage(page, title) { 446 | return await page.waitForFunction(t => { 447 | return JSON.parse(localStorage['react-todos']).map(i => i.title).includes(t); 448 | }, title); 449 | } 450 | --------------------------------------------------------------------------------