Comprehensive Playwright-Powered End-to-End Testing, Modular Framework using Page Object Model

balaji k
5 min readMar 12, 2024

Key Features

- Modular Design: Promotes reusability and maintainability by having seperate classes for ‘Actions’ & ‘Assertions’
- Data Generation: Supports utility functions and API calls to generate test data.
- Page Object Model: Common class for maintaining the page element locators.
- Fixtures: Provide a consistent and efficient setup and teardown process.

Introduction to Core Components

Page Object Model (POM)

The Page Object Model is a design pattern that encourages better test maintenance and reduces code duplication. It abstracts page details into objects, allowing testers to write more readable and robust test cases, especially useful in web application testing.

Page Objects (pages/)

Page objects represent the page elements locators of the web application under test.

Actions (actions/)

We have seperate class for actions that perform interactions with the web application, using the page objects to interact with the UI.

Assertions (assertions/)

We have seperate class for assertions that verify the state of the application after performing actions, validating that the application behaves as expected.

Utilities (utils/)

Utility functions provide common functionality that can be reused across tests, such as generating random book data.

Test Cases (tests/e2e/)

The test cases define end-to-end testing scenarios, focusing on specific features or functionalities of the application.

UseCase1: In this test, we are invoking the ‘generateRandomBookData’ function to generate the test data and consume it in the tests by passing them as arguments.

import { test } from '../../fixtures/addBooks_fixture.js';
import generateRandomBookData from '../../utils/generateRandomBookData.js';

test.describe.serial('Adding Books functionality', () => {
let title, isbn, genre, summary;

test.beforeAll(() => {
// Generate book data once before all tests
const bookData = generateRandomBookData();
({ title, isbn, genre, summary } = bookData); // Destructure and assign to the scoped variables
});

test.beforeEach(async ({ page }) => {
await page.goto('http://localhost:3000/books');

})

test('Verify-> new book added', async ({ addBookActions, addBookAssertions }) => {
// Use the generated book data in our test
await addBookAssertions.nodataValidationisPresent()
await addBookActions.clickaddBook(title, genre, isbn, summary);
await addBookAssertions.verifyBookInTable(title)
await addBookAssertions.nodataValidationisNotPresent()
});
});

Use Case2: In this test, when we are not able to generatedatas in the UI and it requires some api calls to get the test datas available in the UI test environment, we can invoke the api calls in the before hooks.

import { test } from '../../fixtures/updateBooks_fixture.js'; 
import generateRandomBookData from '../../utils/generateRandomBookData.js';
import { addBookViaAPI } from '../../requests/addBooks.js';

test.describe.serial('viewmore, edit, delte functionality', () => {

test.beforeAll(() => {
// Generate bookdata using the requests/addBooks.js file
addBookViaAPI(generateRandomBookData());
});

test.beforeEach(async ({ page }) => {
await page.goto('http://localhost:3000/books');
});

test('Verify-> viewMore functionality', async ({ page, updateBookActions }) => {
await updateBookActions.clickviewMore();
});

test('Verify-> edit functionality', async ({ page, updateBookActions, updateBookAssertions }) => {
await updateBookActions.clickeditBook();
await updateBookAssertions.verifyUpdatedBookInTable('_editText')
})

test('verify-> delete functionality', async ({ page,updateBookActions, updateBookAssertions}) => {
await updateBookActions.clickdeleteBook() //delete the book added in this test
await updateBookAssertions.verifyBookNotIntable('_editText')
await updateBookActions.clickdeleteBook() //delete the remaining one more book
await updateBookAssertions.nodataValidationisPresent()
});

})

Enhancing Tests with Fixtures

Fixtures manage test setup and teardown processes, initializing data, configuring the testing environment, or allocating resources that tests may require.

Benefits of Using Fixtures

- Consistency: Ensures each test runs under the same conditions.
- Efficiency: Manages resources smartly, speeding up the testing process.

Implementing Fixtures

Our framework utilizes Playwright’s fixtures to enhance the testing workflow, managing BrowserContext, Page objects, actions and assertions custom fixtures for initialization.

import { test as baseTest } from '@playwright/test';
import { AddBookActions } from '../pages/addBooks/addBooks_actions';
import { AddBooksAssertions } from '../pages/addBooks/addBooks_assertions'; //
import { AddBookPageObjects } from '../pages/addBooks/addBooks_pageObjects';

export const test = baseTest.extend ({
context: async({browser}, use) => {
const context = await browser.newContext();
await use(context);
},

page: async ({context}, use) => {
const page = await context.newPage();
await use(page);
},

addBookActions: async ({ page }, use) => {
await use(new AddBookActions(new AddBookPageObjects(page)));
},
addBookAssertions: async ({ page }, use) => {
await use(new AddBooksAssertions(new AddBookPageObjects(page)));
}

});

export default test;
-------------------------------------------------------------------------------
import { test as baseTest } from '@playwright/test';
import { UpdateBookActions } from '../pages/UpdateBooks/UpdateBooks_actions';
import { UpdateBooksAssertions } from '../pages/UpdateBooks/UpdateBooks_assertions';
import { UpdateBookPageObjects } from '../pages/UpdateBooks/UpdateBooks_pageObjects';

// Extend the base test fixture with additional properties for UpdateBookActions and UpdateBooksAssertions
export const test = baseTest.extend({
context: async ({ browser }, use) => {
const context = await browser.newContext();
await use(context);
},

page: async ({ context }, use) => {
const page = await context.newPage();
await use(page);
},

updateBookActions: async ({ page }, use) => {
await use(new UpdateBookActions(new UpdateBookPageObjects(page)));
},

updateBookAssertions: async ({ page }, use) => {
await use(new UpdateBooksAssertions(new UpdateBookPageObjects(page)));
},
});

export default test;

Testdata generation:

The project generates random data for books using a utility function defined in utils/generateRandomBookData.js. This function utilizes the randomatic package to create random strings and numbers, which are then assembled to form book data, including ‘titles, genres, ISBNs, and summaries’. Here’s a brief overview of how the data is generated:

- Title: A random string prefixed with “Book “ and followed by a 5-character string composed of alphabets and numbers.
- Genre: Randomly selected from a predefined list of genres.
- ISBN: A random 13-digit number represented as a string.
- Summary: A simple string that includes the generated title.

This generated data is used in tests to add, update, or verify books in the application, ensuring a variety of data points for thorough testing.

This UI automation project repo: Please find the project at “https://github.com/balajiregt/playwright_pom_fixtures_modular_framework_template

AUT: The application is availabe in the repo ‘https://github.com/ruiyigan/books-manager-react-app’ , thanks ‘ruiyigan’

Thats it for today all. Let me know if this modular approach is really modular or any refactor needs to be done. Cheers all..

--

--

balaji k

Just sharing some snippets on the things that I face in a day of my work| Software Engineer| QA| Loves to travel| Loves to drive| Loves family time