Definition
- tests/ – Thin specs expressing what is being verified, separate so scenarios read clearly and volume grows without entangling interaction detail.
- pages/ – Page Objects with stable locators and interaction methods, so a UI change is a single edit that leaves specs untouched.
- fixtures/ – Dependency injection and lifecycle logic, so setup and teardown are defined once and injected automatically.
- utils/ – Cross-cutting UI-agnostic helpers like formatting, random data, and custom assertions.
- data/ – Externalized test data in JSON, CSV, or factories, so inputs are not hard-coded and can be isolated per worker.
- config/ – Environment and runner settings so the suite is portable across environments.
- api/ – Backend helpers for fast preconditions and source-of-truth verification, separate from UI concerns.
- reports/ – Generated output and artifacts in a known location for CI to publish (gitignored).
Organizing the National Bank of India automation is like organizing a well-run bank building. The tests folder is the list of daily checks. The pages folder is the directory of specialist clerks who operate each screen. The fixtures folder is the front-desk setup. The utils folder is the shared toolroom. The data folder is the file cabinet of sample customers. The config folder is the rulebook saying which branch to use. The api folder is the fast internal back-channel. And the reports folder is where end-of-day summaries are kept. Each room has one clear purpose, so anyone can find things and a change in one room does not disturb the others.
Understanding the Concepts
Why does the tests/ folder exist?
The tests folder holds the specs, and they exist as a separate layer so that what is being verified is expressed clearly and independently of how the UI is driven. Keeping specs thin means a reader sees the scenario at a glance.
// tests/loan-approval.spec.ts
import { test, expect } from '../fixtures/bankFixtures';
test('Eligible customer loan is approved', async ({ loanPage, api }) => {
await api.seedCustomer('CUST-001', { income: 60000, credit: 750, age: 25 });
await loanPage.navigate();
await loanPage.applyForLoan('CUST-001', 500000);
await expect(loanPage.statusMessage).toContainText('Approved');
});
Why does the pages/ folder exist?
Page Objects hold all locators and interaction methods for a screen in one predictable place. When the UI changes, you know exactly where to go, and specs never need editing for a locator change.
// pages/LoanPage.ts
import { Page, Locator } from '@playwright/test';
export class LoanPage {
readonly customerField: Locator;
readonly amountField: Locator;
readonly applyButton: Locator;
readonly statusMessage: Locator;
constructor(private page: Page) {
this.customerField = page.getByLabel('Customer ID');
this.amountField = page.getByLabel('Loan Amount');
this.applyButton = page.getByRole('button', { name: 'Apply' });
this.statusMessage = page.getByTestId('loan-status');
}
async navigate() { await this.page.goto('/loans/apply'); }
async applyForLoan(customerId: string, amount: number) {
await this.customerField.fill(customerId);
await this.amountField.fill(amount.toString());
await this.applyButton.click();
}
}
Why does the fixtures/ folder exist?
Fixtures hold dependency injection and lifecycle code so that setup and teardown are defined once and injected into tests automatically, rather than copy-pasted.
// fixtures/bankFixtures.ts
import { test as base } from '@playwright/test';
import { LoanPage } from '../pages/LoanPage';
import { ApiHelper } from '../api/ApiHelper';
type Fixtures = {
loanPage: LoanPage;
api: ApiHelper;
};
export const test = base.extend<Fixtures>({
loanPage: async ({ page }, use) => {
await use(new LoanPage(page));
},
api: async ({ request }, use) => {
const api = new ApiHelper(request);
await use(api);
await api.cleanup(); // teardown
},
});
export { expect } from '@playwright/test';
Why does the utils/ folder exist?
Utilities hold cross-cutting, UI-agnostic logic needed in many places. Without this folder, the same helper gets duplicated across pages and specs.
// utils/moneyFormatter.ts
export function formatINR(amount: number): string {
return new Intl.NumberFormat('en-IN', {
style: 'currency',
currency: 'INR',
}).format(amount);
}
// utils/randomData.ts
export function randomAccountId(): string {
return `ACC-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
}
// utils/customAssertions.ts
import { expect, Locator } from '@playwright/test';
export async function expectAmount(locator: Locator, amount: number) {
await expect(locator).toContainText(formatINR(amount));
}
Why does the api/ folder exist?
API helpers create preconditions far faster and more reliably than clicking through the UI. They also verify UI actions at the source of truth.
// api/ApiHelper.ts
import { APIRequestContext } from '@playwright/test';
export class ApiHelper {
private createdIds: string[] = [];
constructor(private request: APIRequestContext) {}
async seedCustomer(id: string, data: Record<string, unknown>) {
await this.request.post('/api/test/customers', {
data: { id, ...data },
});
this.createdIds.push(id);
}
async cleanup() {
for (const id of this.createdIds) {
await this.request.delete(`/api/test/customers/${id}`);
}
}
}
Why do data/ and config/ exist?
Data externalizes inputs so tests are data-driven and portable. Config externalizes environment settings so the suite runs anywhere by changing settings, not code.
// data/loan-test-cases.json
[
{ "income": 60000, "credit": 750, "age": 25, "expected": "Approved" },
{ "income": 30000, "credit": 600, "age": 18, "expected": "Rejected" },
{ "income": 60000, "credit": 750, "age": 18, "expected": "Rejected" }
]
# Environment-specific runs
BASE_URL=https://staging.nationalbank.in npx playwright test
BASE_URL=https://qa.nationalbank.in npx playwright test
Why does the reports/ folder exist?
Reports hold generated output and artifacts in a known location for the CI pipeline to publish and for engineers to diagnose failures. It is gitignored because it is generated.
# CI publishes the report as an artifact
- name: Upload test report
uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report
path: reports/
What scaling properties does this structure give?
The structure embodies separation of concerns, which gives three scaling properties:
| Property | How the Structure Enables It | |---|---| | Changes are localized | UI change -> pages/, env change -> config/, data change -> data/ | | Code is reused | Shared logic in utils/, api/, fixtures/ instead of copied | | Structure is self-documenting | A new engineer knows where a spec, locator, helper, or config belongs |
Dependency flow (sensible direction):
specs ──> pages ──> components
│ │
│ └──> utils
│
└──> fixtures ──> api
│
└──> utils
Nothing depends on reports/
What are common interview follow-up questions?
- "Do the exact folder names matter?" No. The principle is that each concern has a home and dependencies flow sensibly. Names are conventions.
- "How do you prevent pages/ from bloating?" Extract shared UI components into a components/ subfolder that pages compose.
- "How do you handle test data for parallel runs?" Generate unique data per test or per worker using factories in utils/ or API helpers in api/.
Visual Overview
Folder Structure with Responsibilities
───────────────────────────────────────
framework/
│
├── tests/ WHAT to verify (thin specs)
│ ├── transfer.spec.ts
│ ├── loan.spec.ts
│ └── auth.setup.ts
│
├── pages/ HOW to interact (POs)
│ ├── TransferPage.ts
│ └── LoanPage.ts
│
├── components/ SHARED UI parts
│ ├── HeaderComponent.ts
│ └── DatePickerComponent.ts
│
├── fixtures/ SETUP + INJECTION
│ └── bankFixtures.ts
│
├── utils/ SHARED LOGIC
│ ├── moneyFormatter.ts
│ └── randomData.ts
│
├── api/ BACKEND HELPERS
│ └── ApiHelper.ts
│
├── data/ TEST INPUTS
│ └── loan-test-cases.json
│
├── config/ ENVIRONMENT
│ └── playwright.config.ts
│
├── .auth/ STORAGE STATE (gitignored)
│ └── teller.json
│
└── reports/ OUTPUT (gitignored)
└── index.html
Why Each Folder Exists ────────────────────── Folder │ Responsibility │ Change Localized To ───────────┼─────────────────────────┼────────────────────── tests/ │ Intent + assertions │ Business logic change pages/ │ Locators + interaction │ UI redesign fixtures/ │ Setup + teardown │ Lifecycle change utils/ │ Shared helpers │ Formatting rule api/ │ Backend preconditions │ API contract change data/ │ Test inputs │ Data scenario change config/ │ Environment settings │ New environment reports/ │ Generated diagnostics │ Reporter change
Key Takeaway
I structure an automation framework so that each folder owns exactly one responsibility, and the value is the reasoning behind each boundary, not the list of names. Tests hold thin specs for readability. Pages hold Page Objects for single-point locator maintenance. Fixtures hold dependency injection for consistent, reusable setup. Utils hold shared helpers so logic lives once. API helpers create fast, reliable preconditions and verify at the source of truth. Data externalizes inputs for portability. Config externalizes environment settings so the suite runs anywhere. And reports hold generated diagnostics for CI to publish.
This structure gives three scaling properties: changes are localized because a UI change touches pages and an environment change touches config; code is reused because shared logic sits in utils, api, and fixtures instead of being copied; and the layout is self-documenting so a new engineer instantly knows where everything belongs.
At the National Bank of India this structure let the suite grow past several hundred tests without becoming unmaintainable. When we added a new environment we only edited config, when a screen was redesigned we only edited pages, and when we needed faster setup we added API seeding without touching specs.
Remember one sentence: each folder isolates one concern so changes are localized, code is reused, and the structure is self-documenting, which is what makes a framework scalable.
---