पेज ऑब्जेक्ट मॉडल
Page Object Model (POM) UI test automation का एक design pattern है। हर page या screen के लिए एक class बनती है, जिसे page object कहते हैं। यह methods के रूप में बताती है कि user वहाँ क्या कर सकता है, जैसे search(term) या placeOrder(address), और छिपाती है कि वह कैसे होता है: locators, clicks, typing और waits। Tests ये methods call करते हैं और assertions अपने पास रखते हैं। जब "Place order" button का label बदलता है, तो order करने वाले हर test की जगह CheckoutPage की सिर्फ़ एक line बदलती है।
नाम के बावजूद, Martin Fowler बताते हैं कि page objects आम तौर पर हर पूरे page के लिए नहीं, बल्कि page के अहम हिस्सों के लिए बनते हैं; ऐसे हिस्सों की classes को अक्सर component objects कहा जाता है। Screenplay pattern इसी code को व्यवस्थित करने का एक दूसरा तरीका है।
यह क्यों ज़रूरी है
इस structure के बिना हर test वही locators और steps दोहराता है। जब कोई button rename होता है या form में एक step जुड़ता है, तो product ठीक चलने के बावजूद दर्जनों tests fail हो जाते हैं, और कोई testing की जगह पूरा दिन tests edit करने में लगाता है।
Page objects interface के बदलाव को एक class का fix बना देते हैं और test को उसी scenario की तरह पढ़ने लायक बनाते हैं जिसे वह check करता है। लेकिन ये खराब locator को अच्छा नहीं बनाते: page object के अंदर fragile locator भी fragile ही रहता है, बस अब एक जगह पर।
Page object में क्या आता है और क्या बाहर रहता है
Page object किसी page, या उसके हिस्से, को application की शब्दावली वाले API में लपेटता है। यह हर test के लिए उसी test के browser handle से बनता है, जैसे पिछले lesson का Playwright page fixture, और यही अकेला code है जो जानता है कि उसकी screen कैसे बनी है। Selenium documentation ऐसे rules देता है जो हर tool पर लागू होते हैं:
- Public methods वे services हैं जो page देता है, और उनके नाम user के इरादे पर रखे जाते हैं:
placeOrder(), न किclickBlueButton()। - अंदरूनी बातें अंदर रहती हैं: locators, frames, clicks और waits का क्रम। Test को कभी selector की ज़रूरत नहीं पड़ती।
- Assertions नहीं: method data या element लौटाता है, और क्या सही है यह test तय करता है। Fowler दोनों राय सामने रखकर ख़ुद भी assertions बाहर रखना पसंद करते हैं; जो checks कई tests में दोहराए जाते हैं, वे shared assertion helpers में जा सकते हैं। अंदर सिर्फ़ एक check आता है: कि सही page load हुआ है, जो object बनते समय होता है। JavaScript का constructor
awaitनहीं कर सकता, इसलिए Playwright के page objects यह check उस method में करते हैं जो page तक पहुँचाता है, जैसा नीचेplaceOrder()करता है। - Navigation अगला page object लौटाता है:
cart.checkout()एकCheckoutPageलौटाता है, इसलिए return types ही flow का documentation बन जाते हैं। - एक ही action के अलग नतीजे अलग methods हैं:
signIn()account page लौटाता है,signInExpectingError()sign-in page।
Component objects: दोहराए जाने वाले हिस्से के लिए एक class
Screen के कई हिस्से pages नहीं होते: search और cart count वाला header, orders table की एक row, date picker, dialog। Selenium documentation इनकी classes को page component objects कहता है; Fowler का उदाहरण एक album list है जिसमें album objects होते हैं। Page objects इन्हें जोड़कर बनते हैं: CheckoutPage के पास एक header है, और OrdersPage किसी order number के लिए एक OrderRow लौटाता है।
मुख्य technique root है: component object को उसके अपने container का locator मिलता है और वह सिर्फ़ उसी के अंदर खोजता है, इसलिए एक class हर row के लिए काम करती है, और order('ORD-2026-000184').cancel() बगल वाली row का button click नहीं कर सकता। Playwright locator को filter({ hasText }) से छोटा करता है और उसके अंदर getByRole से खोजता है; Selenium parent element पर findElement call करता है।
Calls ऊपर से नीचे जाती हैं; page object library को सीधे भी call कर सकता है
- 1TestsUser की भाषा में scenario और हर assertion:
checkout.placeOrder('Home'), फिरexpect(…)Requirement बदलने पर बदलते हैं
- 2Page objectsहर page के लिए एक class: user जो करता है उसके नाम वाले methods, जो data, locator या अगला page object लौटाते हैं; कोई assertion नहीं
Page या flow बदलने पर बदलते हैं: एक file, हर test नहीं
- 3Component objectsHeader, order row, date picker: component के अपने root के सापेक्ष locators
Shared हिस्सा बदलने पर बदलते हैं, उसे दिखाने वाले हर page के लिए
- 4Browser automation libraryElements खोजती है, click करती है, type करती है और application का इंतज़ार करती है
Tool upgrade या बदलने पर बदलती है
Tests से Browser automation library तक:अनुमति नहीं: Test के अंदर लिखा locator, click या wait
// components/order-row.ts: one class for every row of the orders table
import type { Locator } from '@playwright/test';
export class OrderRow {
constructor(private readonly root: Locator) {} // the row's own element
status(): Locator {
return this.root.getByTestId('order-status');
}
async cancel(): Promise<void> {
await this.root.getByRole('button', { name: 'Cancel order' }).click();
}
}
// pages/orders-page.ts: the page composes the component
import type { Page } from '@playwright/test';
import { OrderRow } from '../components/order-row';
export class OrdersPage {
constructor(private readonly page: Page) {}
order(orderNumber: string): OrderRow {
const row = this.page.getByRole('row').filter({ hasText: orderNumber });
return new OrderRow(row);
}
}
// in a test
const ordersPage = new OrdersPage(page);
const row = ordersPage.order('ORD-2026-000184');
await row.cancel();
await expect(row.status()).toHaveText('Cancelled');Worked example: checkout test का refactoring
एक online store में बारह tests checkout से गुज़रते हैं, और हर एक अच्छे, user-facing locators के साथ नीचे वाले पहले test के steps दोहराता है।
Refactoring के बाद steps page objects में हैं, cart count एक Header component में, और test सिर्फ़ scenario और expected नतीजा बताता है। CheckoutPage submit करने के दो नतीजों को दो methods से दिखाता है, जो एक private submit() share करते हैं।
- "Place order" का नाम "Pay now" हो गया: पहले 12 tests बदलते; अब
CheckoutPageकी एक line। - Cart count की test id बदल गई या वह header के अंदर किसी दूसरी जगह चला गया: सिर्फ़
Header.cartCount()बदलता है, कोई test नहीं।
// BEFORE: tests/checkout.spec.ts (these steps are copied into 12 tests)
test('customer pays with a saved address', async ({ page }) => {
await page.goto('/cart');
await page.getByRole('button', { name: 'Checkout' }).click();
await page.getByRole('radio', { name: 'Home' }).check();
await page.getByRole('button', { name: 'Place order' }).click();
await expect(page.getByRole('heading', { level: 1 }))
.toHaveText('Thank you for your order');
await expect(page.getByRole('banner').getByTestId('cart-count')).toHaveText('0');
});
// AFTER: pages/checkout-page.ts (CartPage, ConfirmationPage and the
// Header component are written the same way; imports omitted)
export class CheckoutPage {
readonly header: Header;
constructor(private readonly page: Page) {
this.header = new Header(page);
}
async placeOrder(address: string): Promise<ConfirmationPage> {
await this.submit(address);
await this.page.waitForURL('**/order/confirmation'); // the page-loaded check
return new ConfirmationPage(this.page);
}
async placeOrderExpectingError(address: string): Promise<CheckoutPage> {
await this.submit(address);
return this; // stays on checkout, which shows the error
}
errorMessage(): Locator {
return this.page.getByRole('alert');
}
private async submit(address: string): Promise<void> {
await this.page.getByRole('radio', { name: address }).check();
await this.page.getByRole('button', { name: 'Place order' }).click();
}
}
// AFTER: tests/checkout.spec.ts
test('customer pays with a saved address', async ({ page }) => {
const cart = await new CartPage(page).open();
const checkout = await cart.checkout();
const confirmation = await checkout.placeOrder('Home');
await expect(confirmation.heading()).toHaveText('Thank you for your order');
await expect(confirmation.header.cartCount()).toHaveText('0');
});Screenplay pattern: actors, tasks और questions
Page objects test code को screens के इर्द-गिर्द व्यवस्थित करते हैं; Screenplay pattern उसे system इस्तेमाल करने वाले लोगों और उनके लक्ष्यों के इर्द-गिर्द रखता है। Java के लिए Serenity BDD और Serenity/JS इसे implement करते हैं; Serenity/JS की शब्दावली में इसके concepts नीचे हैं।
क्योंकि एक actor के पास कई abilities हो सकती हैं, एक ही scenario API से order बना सकता है और browser में उसे check कर सकता है।
- Actors: लोग या external systems जो system under test से interact करते हैं, जैसे लैला, जो दोबारा खरीदारी करने आई customer है।
- Abilities: actor जिन libraries का इस्तेमाल करता है उनके पतले wrappers, जैसे browser के लिए Playwright या REST API के लिए HTTP client।
- Interactions: एक interface के ज़रिए low-level activities, जैसे button click करना या request भेजना।
- Tasks: activities का क्रम जो business का एक step बनाता है, जैसे cart खोलने, address चुनने और submit करने से बना
PlaceOrder। - Questions: जानकारी निकालते हैं, जैसे page पर दिखा या API response में आया order status; test उसी जवाब पर assert करता है।
Anti-patterns जो page objects को बोझ बना देते हैं
इनमें से हर एक suite को बदलना मुश्किल बनाता है।
- Page objects के अंदर assertions, जैसे
verifyOrderTotal(): test अब नहीं दिखाता कि वह क्या उम्मीद करता है, और अलग-अलग उम्मीद वाले tests वह method share नहीं कर पाते। - God page: सैकड़ों lines का
BasePageजिसे हर page inherit करता है, या पूरे single-page app के लिए एक ही class। Deep inheritance की जगह छोटी classes और composition (page के पास header है) चुनें। - Steps expose करना: tests page object के elements को एक-एक करके click और fill करते हैं, तो flow फिर से test में आ जाता है। Retry करने वाले assertion के लिए locator लौटाना ठीक है, जैसा Playwright के examples करते हैं।
- Methods में fixed sleeps:
placeOrder()के अंदरwaitForTimeout(3000)उसे call करने वाले हर test को धीमा करता है और धीमे दिन फिर भी fail होता है। - Flags से चलने वाले methods:
checkout(true, false, 'Home')कई flows को एक नाम के पीछे छिपा देता है। - Shared state: static driver या पूरे suite के लिए एक ही page object parallel runs तोड़ देता है; page objects हर test के लिए या fixture से बनाएँ।
आम गलतियाँ
- हर screen के page objects पहले से लिख देना, बजाय इसके कि classes और methods तब जोड़ें जब tests को उनकी ज़रूरत हो।
- Methods के नाम mechanics पर रखना, जैसे
clickSubmitButton(), न कि इस पर कि user क्या हासिल करता है। - यह उम्मीद करना कि pattern brittle tests ठीक कर देगा: यह positional XPath या fixed sleep को एक जगह ले आता है, उसे बेहतर नहीं बनाता।
- Page objects से click करते हुए test data बनाना, जैसे हर test में sign-up form से customer बनाना, जबकि fixture में API call ज़्यादा तेज़ है।
QA का नज़रिया
Page objects shared code हैं: इन्हें developers के साथ review करें, और उम्मीद रखें कि markup बदलने वाला pull request page या component object भी update करे। Conventions जल्दी तय करें और repository में लिखें: user की भाषा में method names, return types, assertions नहीं, और पसंदीदा locators; और जहाँ page में accessible names या test ids न हों, वहाँ developers से उन्हें जोड़ने को कहें।
जब कोई UI test fail होता है, तो stack trace में failing line, CheckoutPage.placeOrder जैसे method के अंदर, बता देती है कि journey कहाँ टूटी; bug report में इसे लिखें। अगर UI का छोटा बदलाव भी कई files में edits करवाता है, तो structure leak हो चुका है: test-दर-test patch करने की जगह इसे retrospective में उठाएँ।
अभ्यास
Search-और-cart test को page और component objects में refactor करें
नीचे वाला test एक product search करता है, उसे cart में डालता है और cart check करता है। Search box और cart count हर page पर दिखने वाले header में हैं; results में हर product का एक card है। Page और component objects design करें, फिर test को बिना किसी locator के दोबारा लिखें।
फिर बताएँ कि अगर "Add to cart" बदलकर "Add to basket" हो जाए, और अगर cart count header से बाहर चला जाए, तो कौन-सी files बदलेंगी।
test('adds a found product to the cart', async ({ page }) => {
await page.goto('/');
const header = page.getByRole('banner');
await header.getByRole('searchbox', { name: 'Search products' }).fill('kettle');
await header.getByRole('button', { name: 'Search' }).click();
const card = page.getByTestId('product-card').filter({ hasText: 'Steel Kettle' });
await expect(card.getByTestId('price')).toHaveText('89.00 SAR');
await card.getByRole('button', { name: 'Add to cart' }).click();
await expect(header.getByTestId('cart-count')).toHaveText('1');
await header.getByRole('link', { name: 'Cart' }).click();
const line = page.getByTestId('cart-line').filter({ hasText: 'Steel Kettle' });
await expect(line).toContainText('89.00 SAR');
});- हर locator और वह page या दोहराया जाने वाला हिस्सा चिह्नित करें जिसका वह है।
- Classes तय करें: हर page के लिए एक page object, हर दोहराए जाने वाले हिस्से के लिए एक component object।
- हर class के methods user के इरादे से लिखें, return types के साथ: data, assert करने के लिए locator, या अगला page object।
- इन्हीं methods से test दोबारा लिखें, हर assertion उसी में रखें, और बदलाव वाले दोनों सवालों के जवाब दें।
मॉडल उत्तर दिखाएँ
Classes: Header component (root: banner): search(term) → SearchResultsPage, cartCount() → locator, openCart() → CartPage। HomePage: open() → HomePage, और header। SearchResultsPage: header और product(name) → ProductCard। ProductCard component (root: नाम से filter किया गया card): price() → locator, और addToCart(), जो कुछ नहीं लौटाता क्योंकि page वही रहता है। CartPage: line(name) → locator।
Test इरादों की एक chain बन जाता है: const home = await new HomePage(page).open();, const results = await home.header.search('kettle');, const kettle = results.product('Steel Kettle');, await expect(kettle.price()).toHaveText('89.00 SAR');, await kettle.addToCart();, await expect(results.header.cartCount()).toHaveText('1');, const cart = await results.header.openCart(); और await expect(cart.line('Steel Kettle')).toContainText('89.00 SAR');। तीनों assertions और उनकी expected values test में ही रहती हैं, और कोई locator test में नहीं रहता।
Button का नाम बदलने से सिर्फ़ ProductCard.addToCart() का एक locator बदलता है, कोई test नहीं। Header से बाहर गया count अब Header का हिस्सा नहीं है: cartCount() को उस component में ले जाएँ जिसमें अब वह है, जैसे एक नया SideMenu, और count पढ़ने वाले tests में वह call update करें; locator फिर भी एक ही file में रहता है।
अक्सर पूछे जाने वाले सवाल
क्या Page Object Model सिर्फ़ Selenium के लिए है?
नहीं। Fowler बताते हैं कि यह किसी भी UI technology पर लागू होता है, और teams इसे Playwright, Cypress, Appium जैसे mobile tools और desktop UI tests के साथ इस्तेमाल करती हैं। कुछ teams इसकी जगह सीधे helper functions या framework के custom commands इस्तेमाल करती हैं; ज़रूरी यह है कि tests इरादा बताएँ और हर screen की जानकारी एक ही जगह हो।
Page object locators लौटाए या values?
यह tool पर निर्भर है। Playwright के expect(locator).toHaveText() जैसे retry करने वाले assertions के साथ locator लौटाएँ, ताकि assertion page का इंतज़ार कर सके; एक बार पढ़ी गई value, जैसे await locator.textContent(), page से race करती है। Selenium में methods आम तौर पर explicit wait के बाद values लौटाते हैं। दोनों हालात में checking test ही करता है।
क्या page objects की जगह Screenplay इस्तेमाल करूँ?
Page और component objects से शुरू करें: ये आसान हैं और ज़्यादातर suites के लिए काफ़ी हैं। Screenplay बड़े suites में फ़ायदेमंद है जहाँ user roles बहुत हों, या जहाँ scenarios कई interfaces मिलाते हों, जैसे setup के लिए API और check के लिए browser; इसकी कीमत है ज़्यादा और छोटी classes और कठिन शुरुआत। दोनों इरादे को interaction से अलग रखते हैं, इसलिए बाद में बदलना संभव है।
संदर्भ
- Page object models — SeleniumPage object पैटर्न: हर पेज या कंपोनेंट के लिए एक क्लास जो locator छिपाती है और टेस्ट को चाहिए वे एक्शन देती है, जिससे दोहराव घटता है।
- Page Object (Martin Fowler)Page object पैटर्न: पेज को application-specific API में लपेटना ताकि टेस्ट सीधे HTML को न छुएँ, और assertions page object में नहीं बल्कि टेस्ट में रखना।
- Locators — PlaywrightPlaywright एलिमेंट कैसे ढूँढता है, locator दोबारा resolve होकर अपने-आप इंतज़ार क्यों करते हैं, और role-आधारित locator पहले क्यों सुझाए जाते हैं।
- Best Practices — Playwrightयूज़र को दिखने वाला व्यवहार टेस्ट करें, टेस्ट अलग-थलग रखें, मज़बूत locator और web-first assertions अपनाएँ, और trace से डिबग करें।
Mutqan tools से अभ्यास करें
JavaScript और TypeScript कोड को एकसमान इंडेंटेशन और स्पेसिंग के साथ सुंदर बनाएँ।
दो टेक्स्ट की लाइन-दर-लाइन या शब्द-दर-शब्द तुलना करें और अंतर हाइलाइट करें।
नेस्टेड एलिमेंट को सही इंडेंटेशन देकर HTML सुंदर बनाएँ, pre, script और style ब्लॉक सुरक्षित रखते हुए।
और गहराई से
इस topic में आपकी प्रगति
प्रगति ट्रैक करने के लिए साइन इन करें
Topics पूर्ण चिह्नित करें, जहाँ छोड़ा था वहीं से जारी रखें और specialisation परीक्षाएँ खोलें।