├── .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 | 
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------