Framework build sequence
| Order | Component | Purpose |
|---|---|---|
| 1 | constants/AppConstants.js | Central place for common constants such as Base URL. |
| 2 | utils/ScreenshotHelper.js | Reusable screenshot functionality. |
| 3 | hooks/hooks.js | Repeated before/after test behavior. |
| 4 | testdata/loginData.json | Separate test data from test logic. |
| 5 | pages/HomePage.js + LoginPage.js | Page-specific locators and methods. |
| 6 | utils/JsonReader.js | Reusable access to JSON test data. |
| 7 | utils/CommonMethods.js | Generic reusable actions and checks. |
| 8 | fixture/baseFixture.js | Creates POM objects and injects them into tests. |
| 9 | tests/loginTestUpdated.spec.js | Readable TC102 business flow. |
π Framework Connectivity β Who Imports What, and Why?
This is the section to revisit whenever the framework feels confusing. A framework is not just a collection of folders. The files are connected through exports β imports β object creation β method calls β returned data.
1. The Big Picture
AppConstants.js
Exports shared constantshooks.js
Imports the URL + utilitiesloginData.json
Stores test datavalidUser.password
JsonReader.js
Reads and exposes the dataCommonMethods.js
Exports generic static methodsHomePage.js / LoginPage.js
Reuse generic actionsCommanMethods.type()
HomePage.js
Exports HomePage classbaseFixture.js
Creates the POM objectLoginPage.js
Exports LoginPage classbaseFixture.js
Creates the POM objectbaseFixture.js
Exports custom test + expectloginTestUpdated.spec.js
Receives fixtures2. The βExport β Import β Useβ Rule
module.exports = HomePage;
HomePage.js makes its HomePage class available to another file.
const HomePage = require('../pages/HomePage');
baseFixture.js receives that exported class and can create an object from it.
require(), ask: βWhich file is being imported, and what did that file export?β Then ask: βWhat am I going to do with the imported class/function/data?β3. Exactly How TC102 Moves Through the Framework
loginTestUpdated.spec.js imports test and expect from baseFixture.js.baseFixture.js imports HomePage and LoginPage, then creates new HomePage(page) and new LoginPage(page).await use(homePage) and await use(loginPage) make those prepared objects available to the test as { homePage, loginPage }.The spec calls
JsonReader.login(). JsonReader obtains the values from testdata/loginData.json.homePage.verifyHomePage() and homePage.clickSignupLogin() execute methods belonging to the HomePage object.When LoginPage needs to type or click, it calls
CommanMethods.type() or CommanMethods.clickButton().The helper checks whether the locator is enabled, then performs
fill() or click().POM verification methods use Playwright assertions such as
expect(locator).toBeVisible().4. Method-Level Connection Map
5. What βStaticβ Changes
// Because this is static:
CommanMethods.clickButton(locator);
// NOT:
const methods = new CommanMethods();
methods.clickButton(locator);
6. What βInstance Methodβ Means in a POM
const homePage = new HomePage(page);
await homePage.clickSignupLogin();
β
this.signupLoginButton
β
the locator stored when HomePage(page) was created
this. new HomePage(page) creates that object and connects its locators to the current Playwright page.7. The One Mental Model to Remember
When confused about a framework file, trace the arrow backwards and forwards: Who exported this? Who imported it? Who created it? Who calls it? What does it call next?
Constants β AppConstants.js
Store shared values once instead of hard-coding them throughout tests.
const BASE_URL = 'https://www.automationexercise.com/';
module.exports = { BASE_URL };ScreenshotHelper.js
Centralizes screenshot logic so tests do not duplicate screenshot code.
class ScreenshotHelpers {
static async capture(page, name) {
await page.screenshot({ path: `screenshots/${name}.png` });
}
}
module.exports = ScreenshotHelpers;Hooks β hooks.js
Suite-start logging.
Open the application before each test.
Failure screenshot + completion logging.
Suite-end logging.
test.beforeEach(async ({ page }) => {
await page.goto(AppConstants.BASE_URL);
});
test.afterEach(async ({ page }, testInfo) => {
if (testInfo.status !== testInfo.expectedStatus) {
await ScreenshotHelpers.capture(
page,
testInfo.title.replace(/\s+/g, "_")
);
}
});Test data β loginData.json
TC102 login credentials live outside the spec.
{
"validUser": {
"email": "valid-email",
"password": "valid-password"
},
"invalidUser": {
"email": "invalid-email",
"password": "invalid-password"
}
}Page Object Model β HomePage + LoginPage
Each POM owns locators and page-specific actions/verifications.
- Signup / Login
- Home title
- Logged-in username
- Delete Account
- ACCOUNT DELETED!
- Login heading
- Email textbox
- Password textbox
- Login button
this.JsonReader.js
The spec should not contain JSON-reading implementation details.
class JsonReader {
static login() {
// read loginData.json
// return login data
}
}
module.exports = JsonReader;JsonReader.login() for login data.CommonMethods.js
Generic actions are centralized here so multiple POMs can reuse them.
class CommanMethods {
static async checkButtonEnabled(locator) {
await expect(locator).toBeEnabled();
}
static async checkTextBoxEnabled(locator) {
await expect(locator).toBeEnabled();
}
static async clickButton(locator) {
await CommanMethods.checkButtonEnabled(locator);
await locator.click();
}
static async type(locator, value) {
await CommanMethods.checkTextBoxEnabled(locator);
await locator.fill(value);
}
}
module.exports = CommanMethods;CommanMethods.clickButton(locator). No new CommanMethods() is needed for these static methods.clickButton() checks the button is enabled before clicking. type() checks the textbox is enabled before filling. POMs do not need to call those checks separately.LoginPage.userLogin()
β
CommanMethods.type(...)
β
checkTextBoxEnabled(...)
β
locator.fill(...)
LoginPage.userLogin()
β
CommanMethods.clickButton(...)
β
checkButtonEnabled(...)
β
locator.click()this.locator.Custom fixture β baseFixture.js
The fixture moves repeated POM object creation out of every spec.
const base = require('@playwright/test');
const LoginPage = require('../pages/LoginPage');
const HomePage = require('../pages/HomePage');
exports.test = base.test.extend({
loginPage: async ({ page }, use) => {
const loginPage = new LoginPage(page);
await use(loginPage);
},
homePage: async ({ page }, use) => {
const homePage = new HomePage(page);
await use(homePage);
}
});
exports.expect = base.expect;use(value) hands the prepared POM object to the test. The spec no longer needs new HomePage(page) or new LoginPage(page).Spec file β TC102
const { test, expect } = require('../fixture/basefixture');
const JsonReader = require('../utils/JsonReader');
const Hooks = require('../hooks/hooks');
test('TC102 - Login User with correct email and password @smoke @login',
async ({ homePage, loginPage }) => {
const loginData = JsonReader.login();
await homePage.verifyHomePage();
await homePage.clickSignupLogin();
await loginPage.verifyLoginPage();
await loginPage.userLogin(
loginData.validUser.email,
loginData.validUser.password
);
await homePage.verifyLoggedInUser();
await homePage.userDeletion();
await homePage.verifyAccountDeleted();
}
);Complete execution flow
TC102 Spec
β
Custom test from baseFixture
β
Fixtures create HomePage + LoginPage objects
β
Hooks provide common setup/teardown
β
JsonReader provides login data
β
Spec calls POM methods
β
POM calls CommonMethods where appropriate
β
CommonMethods performs reusable Playwright actions
β
Browser / Automation Exercise
β
Assertions verify the resultKey rules to memorize
| Concept | Rule |
|---|---|
| CommonMethods | Generic reusable actions/checks. |
| Static utility methods | Call with class name; no utility instance needed. |
| POM methods | Instance methods because they use this. |
| Fixture | Prepares reusable objects and injects them into tests. |
use() | Passes the prepared fixture value to the test. |
| JSON | Separates test data from test logic. |
| JsonReader | Provides reusable access to JSON data. |
| Hooks | Handle repeated setup/teardown. |
| Spec | Describes the test/business flow rather than implementation details. |