UI टेस्ट के लिए Locator रणनीतियाँ
Locator वह query है जिससे UI टेस्ट कोई एलिमेंट खोजता है — बटन, फ़ील्ड, टेबल की row। इसे अच्छी तरह चुनें तो टेस्ट redesign, refactoring और नए CSS फ़्रेमवर्क में भी टिका रहता है; ख़राब चुनें तो suite हर sprint ऐसे कारणों से टूटता है जिनका defects से कोई लेना-देना नहीं।
यह गाइड एक priority order देती है जो Playwright, Selenium, Cypress और Testing Library में काम करता है, CSS और XPath की तुलना करती है, dynamic ids, iframes और shadow DOM को सँभालना बताती है, और एक worked example व review checklist के साथ ख़त्म होती है। यह UI टेस्ट ऑटोमेशन गाइड का हिस्सा है, जो waits, assertions और page objects कवर करती है।
अच्छा locator क्या होता है
Locator को सबसे अच्छी तरह इससे परखा जाता है कि उसे क्या तोड़ता है। अच्छा locator तभी बदलता है जब यूज़र जिस व्यवहार पर निर्भर है वह बदले — तब नहीं जब कोई डिज़ाइनर class का नाम बदले या कोई developer wrapper div जोड़ दे।
- Unique — टेस्ट जिस state की अपेक्षा करता है उसमें ठीक एक एलिमेंट से मेल खाता है। Playwright locators strict होते हैं: कई एलिमेंट से मेल खाने वाले locator पर action error देता है, जिससे अस्पष्टता जल्दी दिखती है।
- Stable — layout, styling, DOM की गहराई, एलिमेंट के क्रम और build या render पर बनी values से स्वतंत्र।
- Readable — टेस्ट पढ़ने वाला पेज खोले बिना जान ले कि किस एलिमेंट की बात है।
- यूज़र के इरादे के क़रीब — एलिमेंट को वैसे नाम देता है जैसे यूज़र या assistive technology उसे समझती है: “Place order” बटन, “Email” फ़ील्ड।
- ठीक करने में सस्ता — टूटने पर failure message से कारण साफ़ दिखे।
Priority order: पहले यूज़र को दिखने वाला, XPath आख़िर में
Playwright का documentation पहले role-आधारित locators सुझाता है, और Testing Library अपनी queries को इसी तरह रैंक करती है: वे queries पहले आती हैं जो दिखाती हैं कि यूज़र और assistive technology पेज को कैसे समझते हैं, और उसकी queries में test ids आख़िर में आती हैं। जहाँ ये न पहुँचें वहाँ CSS और XPath जोड़ने पर क्रम यह बनता है:
- 1. Role और accessible name — getByRole("button", { name: "Place order" })। Role HTML एलिमेंट (button, link, textbox, checkbox, heading) या उसके ARIA role से आता है; name label, content या aria-label से आता है, जिसकी गणना W3C की Accessible Name specification के अनुसार होती है।
- 2. Label — <label> एलिमेंट या aria-labelledby से जुड़े form fields के लिए getByLabel("Email")।
- 3. दिखने वाला text, alt text, title और input का hint text — जिन एलिमेंट्स का role और name उपयोगी न हो उनके लिए getByText, getByAltText और getByTitle (और input के hint text के लिए उसका locator)। Text locators headings और messages जैसे non-interactive content के लिए ठीक हैं; hint text, label का कमज़ोर विकल्प है।
- 4. Test id — Playwright में getByTestId("order-total") default रूप से data-testid से मेल खाता है (attribute configurable है); Cypress data-cy या ऐसे ही data-* attribute की सलाह देता है। तब इस्तेमाल करें जब यूज़र-आधारित attributes अस्पष्ट, ग़ायब या बार-बार बदलने वाले हों, जैसे translated text।
- 5. CSS selector — structural मामलों के लिए: attributes, combinators, name से form inputs। छोटा रखें और stable attributes पर टिकाएँ।
- 6. XPath — जब parent या ancestor तक जाना हो, या ऐसे टूल में text match करना हो जिसमें text locator नहीं है। Relative और छोटा रखें।
Element: <button type="submit">Place order</button>
Playwright page.getByRole("button", { name: "Place order" })
Testing Library screen.getByRole("button", { name: "Place order" })
Selenium (Java) driver.findElement(By.xpath("//button[normalize-space()='Place order']"))
driver.findElement(By.cssSelector("[data-testid=place-order]"))
Cypress cy.contains("button", "Place order")
cy.get("[data-cy=place-order]")| Locator | Restyle | Wrapper div जुड़ना | Item जुड़ना या sorting बदलना | नया build या render | Translated UI | ग़ायब accessible name दिखाता हैसाइड फ़ायदा |
|---|---|---|---|---|---|---|
| 1 · Role और accessible name | टिकता है | टिकता है | टिकता है | टिकता है | टूटता हैयह English text से मेल खाता है | हाँ |
| 2 · Label | टिकता है | टिकता है | टिकता है | टिकता है | टूटता हैयह English text से मेल खाता है | हाँ |
| 3 · दिखने वाला text | टिकता है | टिकता है | टिकता है | टिकता है | टूटता हैयह English text से मेल खाता है | नहीं |
| 4 · Test id | टिकता है | टिकता है | टिकता है | टिकता है | टिकता है | नहीं |
| 5 · CSS selectorछोटा, stable attributes पर टिका | टिकता है | टिकता है | टिकता है | टिकता है | टिकता है | नहीं |
| 6 · XPathrelative, text से match | टिकता है | टिकता है | टिकता है | टिकता है | टूटता हैयह English text से मेल खाता है | नहीं |
| Styling class | टूटता है | टिकता है | टिकता है | टिकता है | टिकता है | नहीं |
| Index-आधारित | टिकता है | टिकता है | ग़लत एलिमेंट | टिकता है | टिकता है | नहीं |
| Generated value | लागू नहीं: यह हर render पर पहले ही टूटता है | लागू नहीं: यह हर render पर पहले ही टूटता है | लागू नहीं: यह हर render पर पहले ही टूटता है | टूटता है | लागू नहीं: यह हर render पर पहले ही टूटता है | नहीं |
| Absolute XPath | टिकता है | टूटता है | टूट सकता हैअगर किसी ancestor की position बदले | टिकता है | टिकता है | नहीं |
- टूटता है, टूट सकता है, या ग़लत एलिमेंट ढूँढता है
- Anti-pattern
- Priority order: उदाहरण
- 1 · Role और accessible name
getByRole("button", { name: "Place order" }) - 2 · Label
getByLabel("Email") - 3 · दिखने वाला text
getByText("Sign in") - 4 · Test id
getByTestId("order-total") - 5 · CSS selector
input[name=terms] - 6 · XPath
//button[normalize-space()='Place order']
- Anti-patterns: उदाहरण
- Styling class
.btn-primary - Index-आधारित
(//button)[2] - Generated value
#input-4821 - Absolute XPath
/html/body/div[2]/div/div[3]/form/button
उदाहरण पेज के checkout फ़ॉर्म और bug titles से। हर locale की अपनी अपेक्षित strings हों तो text-आधारित locators translated UI पर भी चलते हैं। Generated value हर render पर टूटती है, चाहे कुछ और बदले या नहीं (—)।
Selenium और Cypress में यह क्रम लागू करना
Selenium में built-in role locator नहीं है। इसकी strategies हैं id, name, class name, tag name, link text, partial link text, CSS selector और XPath, और Selenium 4 relative locators (above, below, left of, right of, near) जोड़ता है। व्यवहार में क्रम बनता है: test id या stable id, फिर name, फिर छोटा CSS selector, फिर XPath — दिखने वाला text links के लिए link text से या बाक़ी के लिए text-matching XPath से। कई टीमें इन्हें यूज़र को दिखने वाले एलिमेंट के नाम वाले page-object methods में लपेटती हैं, ताकि टेस्ट फिर भी “Place order पर क्लिक करें” जैसा पढ़ा जाए।
Cypress, cy.get (CSS) और cy.contains (text) से query करता है। इसकी best-practices गाइड tag, id और class selectors के बजाय dedicated data-* attributes की सलाह देती है, और उन एलिमेंट्स के लिए cy.contains की जिनका text टेस्ट हो रहे व्यवहार का हिस्सा है। जो टीमें Cypress में role-आधारित queries चाहती हैं वे अक्सर Testing Library integration जोड़ती हैं, जो findByRole और बाक़ी findBy व findAllBy queries देता है।
Test ids: developers के साथ एक contract
Test id एक attribute है जो markup में सिर्फ़ ऑटोमेशन के लिए जोड़ा जाता है, जैसे data-testid, data-test या data-cy। यह सबसे stable hook है, क्योंकि टेस्ट के सिवा कुछ भी इस पर निर्भर नहीं — बशर्ते टीम इसे सजावट नहीं, contract माने।
- पूरे प्रोडक्ट के लिए एक attribute नाम तय करें और टूल को वही इस्तेमाल करने के लिए configure करें (जैसे Playwright की testIdAttribute setting)।
- मकसद से नाम दें, जगह या style से नहीं: checkout-place-order, न कि button-3 या blue-btn।
- पेज के भीतर ids unique रखें। दोहराए जाने वाले items के लिए id container पर लगाएँ या content से scope करें, फिर child को role से खोजें — order ORD-2026-000184 की row, फिर उसका “Cancel” बटन।
- Test ids फ़ीचर वाले उसी pull request में जोड़ें, और उन्हें हटाने की review वैसे करें जैसे API बदलाव की करते हैं।
- साफ़ role और name को test id से न बदलें; यूज़र-आधारित locator यह भी दिखाता है कि एलिमेंट assistive technology को उपलब्ध है।
- Production builds से test ids हटाना टीम का फ़ैसला है। उन्हें रखने में कुछ bytes लगते हैं और production पर read-only smoke tests चलाए जा सकते हैं।
// Playwright: scope to the row for one order, then find its button by role
const row = page.getByRole("row").filter({ hasText: "ORD-2026-000184" });
await row.getByRole("button", { name: "Cancel" }).click();
// playwright.config.ts: use the attribute your team agreed on
export default defineConfig({ use: { testIdAttribute: "data-test" } });CSS बनाम XPath
दोनों भाषाएँ ज़्यादातर structural queries व्यक्त कर लेती हैं; फ़र्क़ किनारों पर मायने रखता है। कोई भी “गलत” नहीं है, लेकिन आम मामलों में CSS आमतौर पर छोटा होता है, और कुछ मामलों में XPath ही अकेला विकल्प है।
- Readability — ids, classes और attributes के लिए CSS छोटा है: [data-testid=cart] बनाम //*[@data-testid="cart"]।
- दिशा — XPath parent या ancestor तक ऊपर जा सकता है (.., ancestor::tr) और पिछले siblings तक; CSS पारंपरिक रूप से सिर्फ़ नीचे और आगे चलता है। मौजूदा प्रमुख ब्राउज़रों में सपोर्टेड :has() pseudo-class कुछ “parent of” मामले कवर करती है।
- Text — XPath text match कर सकता है (normalize-space()="Pay now", contains(., "Pay")); CSS text content match नहीं कर सकता। Playwright और Cypress CSS के ऊपर अपना text matching जोड़ते हैं।
- Shadow DOM — CSS selectors open shadow roots के अंदर इस्तेमाल हो सकते हैं, और Playwright के CSS व role locators उन्हें अपने-आप pierce करते हैं; XPath shadow boundaries पार नहीं करता।
- Version — ब्राउज़र XPath 1.0 लागू करते हैं, इसलिए matches() और ends-with() जैसे XPath 2.0 functions उपलब्ध नहीं हैं।
- Speed — UI टेस्ट के लिए दोनों काफ़ी तेज़ हैं और शायद ही कभी bottleneck बनते हैं। Readability और stability के आधार पर चुनें।
Goal CSS XPath
Element by test id [data-testid=cart] //*[@data-testid="cart"]
Input by name input[name=email] //input[@name="email"]
Id starting with a prefix [id^="order-row-"] //*[starts-with(@id,"order-row-")]
Attribute contains a value [class*="error"] //*[contains(@class,"error")]
Button with exact text (not possible in CSS) //button[normalize-space()="Pay now"]
Button in the row of an order (CSS cannot match the text) //tr[td[normalize-space()="ORD-2026-000184"]]//button
Parent of an element (only via :has()) //input[@name="email"]/..Dynamic ids और generated class names
कई फ़्रेमवर्क और component libraries attribute values build या render के समय बनाती हैं: id="input-4821" जो हर render पर बदलता है, CSS modules से class="Button_primary__3xK9a", CSS-in-JS libraries से class="css-1q2w3e"। ऐसी value कॉपी करने वाला locator आज pass होता है और अगले build या अगले page load के बाद fail।
- इन्हें पहचानें — ऐसे अंक या hash जो एक ही पेज के दो loads में अलग हों। दो sessions का markup सेव करके तुलना करें ताकि बदलते हिस्से दिखें।
- यूज़र-आधारित locator या test id को प्राथमिकता दें — टिकाऊ इलाज stable hook है, ज़्यादा चालाक selector नहीं।
- अगर match करना ही पड़े तो stable हिस्से पर टिकाएँ — [id^="order-row-"] (starts with), [class*="Button_primary"] (contains) या XPath starts-with()। Pattern इतना कसा रखें कि unique बना रहे।
- बदलते text को pattern से match करें — जैसे Playwright में getByText(/Order ORD-\d{4}-\d{6} confirmed/) — और commit करने से पहले pattern को असली samples पर जाँचें।
- टेस्ट डेटा से आने वाली values कभी hard-code न करें — locator उस डेटा से बनाएँ जो टेस्ट ने बनाया, जैसे टेस्ट सेटअप करने वाली API call से लौटा order number।
Iframes और shadow DOM
Iframe एक अलग document है, इसलिए main page का locator उसके अंदर नहीं देख सकता। Payment fields, embedded maps, chat widgets और rich-text editors अक्सर iframes होते हैं।
Shadow DOM किसी web component के अंदरूनी markup को encapsulate करता है। Open shadow roots तक ऑटोमेशन टूल पहुँच सकते हैं। Closed shadow roots जान-बूझकर छिपे होते हैं और standard locators से नहीं पहुँचते; उन्हें component के public व्यवहार से टेस्ट करें या developers से test hook माँगें।
- Selenium — driver.switchTo().frame(…) से frame में जाएँ, action करें, फिर driver.switchTo().defaultContent() से लौटें। Shadow DOM के लिए host एलिमेंट खोजें, getShadowRoot() (Java; Python में shadow_root) बुलाएँ, फिर उसके अंदर CSS selectors से खोजें।
- Playwright — page.frameLocator(…) या locator.contentFrame() frame के अंदर locators के लिए scope देता है, switching की ज़रूरत नहीं। Playwright locators default रूप से open shadow roots को pierce करते हैं, XPath को छोड़कर।
- Cypress — shadow DOM .shadow() command या includeShadowDom configuration option से सपोर्टेड है। Iframes के लिए कोई dedicated built-in command नहीं है; आम तरीक़ा iframe एलिमेंट से frame के document body तक पहुँचना है, और cross-origin frames और सीमाएँ जोड़ते हैं।
- Third-party frames — payment provider के hosted card field के लिए provider का test mode और उसका published test data इस्तेमाल करें, और frame में टाइप करने वाले टेस्ट कम रखें।
// Selenium (Java) — iframe: switch in, act, switch back
driver.switchTo().frame(driver.findElement(By.cssSelector("iframe[title='Card details']")));
driver.findElement(By.name("cardnumber")).sendKeys(testCardNumber);
driver.switchTo().defaultContent();
// Selenium (Java) — open shadow root, then CSS inside it
SearchContext shadow = driver.findElement(By.cssSelector("app-date-picker")).getShadowRoot();
shadow.findElement(By.cssSelector("button.next-month")).click();
// Playwright — frame scope, no switching; open shadow roots are pierced
const card = page.frameLocator("iframe[title='Card details']");
await card.getByLabel("Card number").fill(testCardNumber);
await page.locator("app-date-picker").getByRole("button", { name: "Next month" }).click();Anti-patterns जो suites को नाज़ुक बनाते हैं
- Absolute XPath — /html/body/div[2]/div/div[3]/form/button किसी भी ancestor के बदलने पर टूटता है। ब्राउज़र का “Copy XPath” अक्सर ठीक यही देता है।
- Index-आधारित selectors — बिना कारण :nth-child(3), (//button)[2] या .first()। ये क्रम पर निर्भर हैं और item जुड़ने, हटने या sorting बदलने पर टूटते हैं।
- Styling classes — .btn-primary, .mt-4 या .text-red-500 हर visual redesign या utility-CSS refactor के साथ बदलती हैं।
- Generated values — build या render पर बने ids और class hashes।
- लंबी chains — div.container > div.row > div.col-md-6 > form > div:nth-child(2) > input पूरे layout को encode कर देता है।
- गलत भाषा का text — “Place order” खोजने वाला टेस्ट तब fail होता है जब वही suite Arabic interface («إتمام الطلب») पर चलता है। या तो हर locale को उसकी अपेक्षित strings दें, या translated text वाले एलिमेंट्स के लिए test ids इस्तेमाल करें।
- दोहराए गए locators — वही selector बीस टेस्ट में चिपकाया गया। उसे एक page या component object में रखें।
- बहुत चौड़े text matches — getByText("Save") जो बटन, toast और help paragraph तीनों से मेल खाए। किसी region तक scope करें या role इस्तेमाल करें।
Automation defects caused by locators (written like bug titles)
- checkout.spec fails after restyle: .btn-primary no longer matches "Place order"
- orders.spec cancels the wrong order when the list is sorted by date: locator (//button[text()="Cancel"])[1]
- login.spec fails on the Arabic build: getByText("Sign in") not found
- profile.spec fails intermittently: #input-4821 is regenerated on every render
- search.spec breaks after a wrapper div is added: /html/body/div[2]/div/div[3]/form/buttonLocators और accessibility
Role और label locators वही जानकारी इस्तेमाल करते हैं जो screen reader बोलता है: एलिमेंट का role, HTML एलिमेंट या WAI-ARIA से, और उसका accessible name। जब getByRole("button", { name: "Delete" }) सिर्फ़ icon वाले delete बटन को नहीं खोज पाता, तो संभावित कारण है कि बटन का कोई accessible name नहीं — यह असली accessibility defect है, टेस्ट की समस्या नहीं। Markup ठीक करें (दिखने वाला text या aria-label), locator नहीं।
यही तब लागू होता है जब कोई component library बटन जैसा व्यवहार करने वाला div बनाती है: उसके चारों ओर CSS locators लिखने के बजाय ग़ायब semantics का defect उठाएँ।
यह एक साइड फ़ायदा है, accessibility टेस्ट नहीं। Role-आधारित locators का pass होना यह नहीं दिखाता कि पेज accessible है; keyboard, screen reader और automated accessibility जाँचें फिर भी ज़रूरी हैं।
Worked example: checkout फ़ॉर्म के locators
नीचे का markup एक काल्पनिक checkout पेज से है। हर एलिमेंट के लिए सूची सुझाया गया locator, Selenium का समकक्ष और किससे बचना है, बताती है।
- Email फ़ील्ड — getByLabel("Email")। Selenium: By.name("email")। #email-1293 (generated) और form > input:nth-of-type(1) से बचें।
- Terms checkbox — getByRole("checkbox", { name: "I accept the terms" })। Selenium: By.cssSelector("input[name=terms]")।
- Order total — getByTestId("order-total"); इसका दिखने वाला text रक़म के साथ बदलता है, इसलिए text वह है जिसे assert करते हैं, जिससे खोजते नहीं। Selenium: By.cssSelector("[data-testid=order-total]")।
- Submit बटन — getByRole("button", { name: "Place order" })। Selenium: normalized text पर XPath, या टीम जोड़े तो test id। .btn-primary और //form/button[1] से बचें।
- फ़ॉर्म ख़ुद — #checkout-7f3a या .Checkout_form__x9Kp2 इस्तेमाल न करें। Scope चाहिए तो फ़ॉर्म को accessible name दें और getByRole("form", { name: … }) इस्तेमाल करें, या test id जोड़ें।
<form id="checkout-7f3a" class="Checkout_form__x9Kp2">
<label for="email-1293">Email</label>
<input id="email-1293" name="email" type="email">
<label><input type="checkbox" name="terms"> I accept the terms</label>
<div class="summary">
<span>Total</span> <span data-testid="order-total">199.20 SAR</span>
</div>
<button type="submit" class="btn btn-primary mt-4">Place order</button>
</form>Locator review checklist
- क्या यह टेस्ट की अपेक्षित state में ठीक एक एलिमेंट से मेल खाता है?
- क्या यह restyle, items के क्रम बदलने और DOM की गहराई बदलने में टिकेगा?
- क्या यह पहले role/label/text, फिर test id, फिर CSS, और आख़िर में XPath है — और अगर नहीं, तो क्या कारण लिखा है?
- क्या यह generated ids, hashed classes, indexes और absolute paths से मुक्त है?
- क्या यह टेस्ट body के बजाय एक page या component object में रहता है?
- क्या यह suite की हर locale में काम करेगा?
- अगर यह कल fail हो, तो क्या error message बताएगा कि कौन सा एलिमेंट ग़ायब था?
अक्सर पूछे जाने वाले सवाल
UI टेस्ट के लिए सबसे अच्छी locator रणनीति क्या है?
एलिमेंट्स को वैसे खोजें जैसे यूज़र उन्हें समझते हैं — role और accessible name, label या दिखने वाले text से — फिर dedicated test id, फिर छोटा CSS selector, और XPath तभी जब कुछ और काम न करे। इसका पहला हिस्सा (यूज़र को दिखने वाले attributes, फिर test id) Playwright और Testing Library सुझाते हैं; CSS और फिर XPath जोड़ने से यह हर टूल में काम करता है।
क्या XPath, CSS से धीमा है?
UI टेस्ट में यह फ़र्क़ शायद ही अहम होता है, जहाँ page loads और network calls ज़्यादा समय लेते हैं। CSS को चुनने के असली कारण हैं कि यह छोटा और पढ़ने में आसान है; parents तक जाने और text match करने के लिए XPath उपयोगी रहता है।
data-testid इस्तेमाल करूँ या getByRole?
जब एलिमेंट का role और name साफ़ हो तब getByRole इस्तेमाल करें, क्योंकि यह वही match करता है जो यूज़र देखते हैं और ग़ायब accessible names भी पकड़ता है। Test id तब इस्तेमाल करें जब role और name अस्पष्ट, ग़ायब या बार-बार बदलने वाले हों, जैसे translated text या दोहराए items।
Dynamic IDs को कैसे सँभालें?
Stable hook (test id) माँगें या यूज़र-आधारित locator इस्तेमाल करें। अगर generated value match करनी ही हो तो starts-with या contains से उसके stable हिस्से पर टिकाएँ, और values hard-code करने के बजाय टेस्ट के अपने डेटा से locators बनाएँ।
Iframe के अंदर एलिमेंट कैसे खोजें?
Selenium में switchTo().frame से frame में जाएँ और switchTo().defaultContent से लौटें। Playwright में frame के अंदर locators का scope पाने के लिए frameLocator या contentFrame इस्तेमाल करें; switching की ज़रूरत नहीं।
Playwright क्यों कहता है कि मेरा locator कई एलिमेंट्स से मेल खाया?
Playwright locators strict हैं: action को ठीक एक match चाहिए। Locator को ज़्यादा specific बनाएँ — accessible name जोड़ें, किसी region या row तक scope करें, या filter इस्तेमाल करें — .first() जोड़ने के बजाय, जो अस्पष्टता छिपा देता है।
क्या ब्राउज़र developer tools से कॉपी किए locators इस्तेमाल करूँ?
उन्हें सिर्फ़ शुरुआती बिंदु मानें। “Copy XPath” और “Copy selector” आमतौर पर absolute paths या index-आधारित selectors देते हैं जो छोटे layout बदलावों पर टूटते हैं; commit से पहले उन्हें priority order के अनुसार दोबारा लिखें।
संदर्भ
- Locators — PlaywrightPlaywright एलिमेंट कैसे ढूँढता है, locator दोबारा resolve होकर अपने-आप इंतज़ार क्यों करते हैं, और role-आधारित locator पहले क्यों सुझाए जाते हैं।
- About Queries — Testing Libraryक्वेरी प्रकार और उनकी प्राथमिकता — role और label पहले, टेक्स्ट बाद में, test id आख़िर में — ताकि टेस्ट एलिमेंट वैसे ढूँढें जैसे यूज़र और सहायक तकनीक।
- Locator strategies — SeleniumWebDriver जिन locator प्रकारों को सपोर्ट करता है (id, name, CSS selector, XPath, link text आदि) और उनमें से कैसे चुनें।
- Best Practices — Cypressसमर्पित एट्रिब्यूट से एलिमेंट चुनने, फ़िक्स्ड wait से बचने और टेस्ट को स्वतंत्र रखने पर Cypress की गाइडेंस।
- Best Practices — Playwrightयूज़र को दिखने वाला व्यवहार टेस्ट करें, टेस्ट अलग-थलग रखें, मज़बूत locator और web-first assertions अपनाएँ, और trace से डिबग करें।
- Accessible Name and Description Computation 1.2ब्राउज़र किसी एलिमेंट का accessible name और description लेबल, ARIA एट्रिब्यूट और कंटेंट से कैसे निकालते हैं (W3C Working Draft)।
- Accessible Rich Internet Applications (WAI-ARIA) 1.2W3C Recommendation जो ARIA roles, states और properties परिभाषित करती है — कस्टम विजेट अपना अर्थ सहायक तकनीक को कैसे बताते हैं।
अभ्यास
Pull request के locators की review करें
आप एक electronics web shop (काल्पनिक) की टीम में automation reviewer हैं, जिसका UI अंग्रेज़ी और अरबी में है। एक सहकर्मी ने pull request #412 खोली है, जिसमें "My orders" पेज के पहले UI टेस्ट हैं: list को filter करना, किसी order को track और cancel करना, invoice download करना और support chat खोलना। इन टेस्ट के आठ locators नीचे के material में पेज के छोटे किए गए markup के साथ दिए हैं। आठ में से सात locators लेखक की मशीन पर English build के सामने pass होते हैं; L8 time out होता है, और लेखक उसके लिए मदद माँग रहा है।
गाइड के priority order, उसके anti-patterns और उसकी review checklist के साथ काग़ज़ पर locators की review करें। हर finding वैसे लिखें जैसे pull request में लिखते: locator को क्या तोड़ता है, उसकी जगह क्या इस्तेमाल करें, और क्या टेस्ट में नहीं बल्कि markup में बदलना होगा। इसके लिए 20–30 मिनट रखें।
Electronics web shop (काल्पनिक) - अंग्रेज़ी और अरबी UI
पेज "My orders", English build का markup, छोटा किया हुआ। Nightly UI suite:
Playwright (TypeScript), दोनों भाषाओं पर चलता है।
Build पर generated: search-5521 और row-7f21c जैसे ids हर deployment पर बदलते हैं;
Filters_bar__9kQ2z जैसी classes CSS modules से आती हैं।
हर row का icon बटन order का invoice download करता है (design में "Download invoice")।
Arabic UI: Status = «الحالة», Apply = «تطبيق», Track = «تتبّع», Cancel = «إلغاء»,
Processing = «قيد المعالجة», Download invoice = «تنزيل الفاتورة»,
Search orders = «البحث في الطلبات», Support chat = «محادثة الدعم».
<main>
<h1>My orders</h1>
<form class="Filters_bar__9kQ2z">
<input id="search-5521" name="q" type="search" placeholder="Search orders">
<label for="status-5522">Status</label>
<select id="status-5522" name="status">
<option value="all">All</option>
<option value="open">Open</option>
<option value="delivered">Delivered</option>
</select>
<button type="submit" class="btn btn-outline">Apply</button>
</form>
<table class="Table_root__x1Ab9">
<thead>
<tr><th>Order</th><th>Date</th><th>Total</th><th>Status</th><th>Actions</th></tr>
</thead>
<tbody>
<tr id="row-7f21c" class="Table_row__2kX9q">
<td>ORD-2026-004417</td><td>12 Sep 2026</td><td>249.00 SAR</td>
<td><span class="badge badge-green">Delivered</span></td>
<td>
<button type="button" class="btn btn-link">Track</button>
<button type="button" class="btn-icon"><svg aria-hidden="true"></svg></button>
</td>
</tr>
<tr id="row-7f21d" class="Table_row__2kX9q">
<td>ORD-2026-004431</td><td>28 Sep 2026</td><td>89.50 SAR</td>
<td><span class="badge badge-amber">Processing</span></td>
<td>
<button type="button" class="btn btn-link">Track</button>
<button type="button" class="btn btn-link">Cancel</button>
<button type="button" class="btn-icon"><svg aria-hidden="true"></svg></button>
</td>
</tr>
</tbody>
</table>
<iframe title="Support chat" src="https://chat.example.com/widget"></iframe>
</main>
Pull request #412 "orders.spec: My orders के पहले UI टेस्ट" - इसके आठ locators
L1 page.locator('#search-5521') // search field
L2 page.locator('.Filters_bar__9kQ2z select') // status filter
L3 page.locator('.btn-outline') // "Apply"
L4 page.locator('xpath=/html/body/main/table/tbody/tr[2]/td[5]/button[2]')
// "Cancel", ORD-2026-004431
L5 page.getByText('Track').first() // "Track", ORD-2026-004417
L6 page.locator('.badge-amber') // status, ORD-2026-004431
L7 page.locator('tr:nth-child(2) .btn-icon') // invoice बटन, ORD-2026-004431
L8 page.getByRole('button', { name: 'Start chat' }) // chat widget के अंदर
लेखक का note: L1-L7 मेरी मशीन पर pass होते हैं (English build)। L8 time out होता है - मदद?- L1–L8 में हर locator के लिए लिखें कि उसे क्या तोड़ेगा या वह क्यों fail होता है, गाइड से कारण का नाम लेते हुए (generated value, styling class, absolute XPath, index, बहुत चौड़ा text match, गलत भाषा का text, iframe)। यह भी बताएँ कि कौन-से आज फिर भी pass होते हैं।
- हर locator को priority order में Playwright में दोबारा लिखें: दोहराए जाने वाले items को उस order की row तक scope करें जिसका number टेस्ट ने ख़ुद बनाया, और नतीजा Arabic run में भी काम करे।
- वे जगहें चिह्नित करें जहाँ locator सही fix नहीं है: developers से क्या जोड़ने को कहेंगे (test id, label, accessible name), और इनमें से किसे defect के रूप में रिपोर्ट करेंगे।
- Status filter और किसी दिए गए order के "Cancel" बटन के लिए Selenium (Java) के समकक्ष लिखें।
- अपने नतीजे पर गाइड की checklist चलाएँ: uniqueness, locale, और क्या failure message ग़ायब एलिमेंट का नाम बताएगा।
मॉडल उत्तर दिखाएँ
L1–L3, filter फ़ॉर्म। लेखक के कहे अनुसार L1–L7 सभी आज pass होते हैं; review इस बारे में है कि उन्हें क्या तोड़ेगा। L1 #search-5521 एक generated id है: यह हर deployment पर बदलता है, इसलिए टेस्ट आज pass होता है और अगली release के बाद fail। Field का कोई <label> नहीं है; उसका इकलौता accessible name hint text से आता है, क्योंकि जब कोई और चीज़ text field को नाम न दे तो ब्राउज़र आख़िरी विकल्प के रूप में placeholder इस्तेमाल करते हैं, और Playwright भी यही नियम मानता है। इसलिए page.getByRole("searchbox", { name: "Search orders" }) उसे आज पा लेता है, और page.getByPlaceholder("Search orders") भी, जो गाइड के क्रम में level 3 है। दोनों हालात में टेस्ट ऐसे hint text पर टिका है जो यूज़र के टाइप करते ही ग़ायब हो जाता है, और गाइड इसे label का कमज़ोर विकल्प कहती है: role locator इस्तेमाल करें और developers से दिखने वाला label माँगें (label का text "Search orders" हो तो locator चलता रहेगा)। L2 .Filters_bar__9kQ2z select CSS modules की ऐसी class कॉपी करता है जिसका hash हर build में बदलता है; select का label है, इसलिए locator page.getByLabel("Status") है। L3 .btn-outline एक styling class है जो अगले restyle में ग़ायब हो जाएगी, भले आज वह ठीक एक एलिमेंट से मेल खाती हो; page.getByRole("button", { name: "Apply" }) इस्तेमाल करें।
L4 और L5, टेबल की rows। L4 absolute XPath है और दो बार index-आधारित: tr[2] sort order पर और account में मौजूद orders पर निर्भर है, और button[2] सिर्फ़ उन rows में "Cancel" है जिनमें Cancel बटन है — delivered row में दूसरा बटन invoice icon है, इसलिए क्रम बदलने पर यह चुपचाप गलत order cancel कर देगा या गलत बटन click करेगा। Chrome DevTools का "Copy full XPath" ठीक यही path देता है; सादा "Copy XPath" इसके बजाय generated row id पर टिकता है (//*[@id="row-7f21d"]/td[5]/button[2]), जो बेहतर नहीं है। Row को उसके content से scope करके फिर बटन को role से खोजें: const row = page.getByRole("row").filter({ hasText: orderNumber }) और row.getByRole("button", { name: "Cancel" }), जहाँ orderNumber वह value है जो टेस्ट के अपने setup ने बनाई, hard-coded ORD-2026-004431 नहीं। L5 getByText("Track").first(): getByText("Track") दो बटनों से मेल खाता है, इसलिए .first() के बिना Playwright का strict mode click रोक देता; .first() अस्पष्टता हल करने के बजाय उसे छिपा देता है और उस order पर click करता है जो sorting में पहले आ जाए। Nightly Arabic run में यह सीधे fail होता है, क्योंकि वहाँ बटन पर «تتبّع» लिखा है। वही fix: ORD-2026-004417 की row पर row.getByRole("button", { name: "Track" })।
L6 और L7, status badge और icon बटन। L6 .badge-amber status को उसकी रंग वाली class से खोजता है: अगले restyle पर टूटता है, किसी order तक scoped नहीं है (processing में दूसरा order हो तो यह अस्पष्ट हो जाता है), और रंग status के साथ बदलता है — delivered order के badge की class badge-green है — इसलिए टेस्ट के order cancel करने के बाद locator उस badge से मेल नहीं खाता जिसे जाँचना है, और failure message दिखी हुई status बताने के बजाय कहता है कि एलिमेंट नहीं मिला। Status का text वही है जिसे टेस्ट assert करता है, इसलिए — गाइड के worked example में order total की तरह — उससे खोजना नहीं चाहिए: badge पर data-testid="order-status" माँगें, उसे सिर्फ़ row scope के भीतर इस्तेमाल करें क्योंकि वह हर row में एक बार दोहराया जाता है, और row.getByTestId("order-status") को toHaveText(...) और run की भाषा की अपेक्षित status से assert करें। L7 tr:nth-child(2) .btn-icon index और styling दोनों पर टिका है, लेकिन गहरी समस्या markup में है: बटन में सिर्फ़ aria-hidden icon है और कोई accessible name नहीं, इसलिए getByRole("button", { name: "Download invoice" }) उसे नहीं पाता और screen reader उसे सिर्फ़ "button" बोलता है। यह रिपोर्ट करने लायक accessibility defect है; developers के aria-label="Download invoice" (या visually hidden text) जोड़ने के बाद locator row.getByRole("button", { name: "Download invoice" }) है। ग़ायब name के चारों ओर CSS locator न लिखें।
L8, chat widget। page.getByRole("button", { name: "Start chat" }) time out होता है क्योंकि बटन एक iframe के अंदर है, और main page का locator दूसरे document के अंदर नहीं देख सकता। Playwright को frame scope चाहिए: page.frameLocator("iframe[title='Support chat']").getByRole("button", { name: "Start chat" }); frame का title attribute ख़ुद frame के लिए stable, यूज़र को दिखने वाला hook है। Widget किसी third party का document है, इसलिए उससे interact करने वाले एक-दो टेस्ट ही रखें और उसके अंदरूनी markup पर assert न करें।
जो टेस्ट में नहीं, markup में बदलता है। तीन अनुरोध developers को जाते हैं, हो सके तो उसी pull request में: search field के लिए दिखने वाला <label>; icon बटन पर aria-label="Download invoice"; और status badge पर data-testid="order-status" (टीम का तय attribute, मकसद से नाम, row के भीतर इस्तेमाल)। पहले दो accessibility defects के रूप में tracker में जाते हैं — टेस्ट ने उन्हें सिर्फ़ उजागर किया: icon बटन का कोई accessible name ही नहीं है, और search field का नाम सिर्फ़ ऐसा hint text है जो यूज़र के टाइप करते ही ग़ायब हो जाता है। Test id एक contract है, इसलिए अब से उसे हटाने की review API बदलाव की तरह होती है। किसी और चीज़ को test id नहीं चाहिए: "Cancel", "Track", "Apply" और "Status" के role और name साफ़ हैं, और गाइड कहती है कि इन्हें test ids से न बदलें।
Arabic run। Locators में इस्तेमाल हर name translated है: "Status" «الحالة» है, "Apply" «تطبيق», "Track" «تتبّع», "Cancel" «إلغاء», "Download invoice" «تنزيل الفاتورة», और status text "Processing" «قيد المعالجة» भी। गाइड दो रास्ते देती है और तीसरे को anti-pattern बताती है: हर locale के लिए एक strings table रखें और names उससे पढ़ें, या translated text वाले एलिमेंट्स के लिए test ids इस्तेमाल करें; जो fail होता है वह अरबी में भी चलने वाले suite में hard-coded English string है। यहाँ per-locale strings के साथ role और name बेहतर default है, क्योंकि यह वह भी जाँचता है जो Arabic यूज़र देखते हैं; data-testid="order-cancel" स्वीकार्य है अगर टीम कारण दर्ज करे। iframe का title और search field का hint text भी translated हैं, इसलिए वे भी उसी table में जाते हैं।
Selenium (Java)। Status filter: new Select(driver.findElement(By.name("status"))).selectByValue("open"); name और option की value stable हैं और translated नहीं, इसलिए यही line दोनों भाषाओं में चलती है, जबकि By.id("status-5522") generated है। किसी दिए गए order का "Cancel": Selenium में role locator नहीं है, इसलिए relative XPath को row के content और बटन के text पर टिकाएँ: By.xpath("//tr[td[normalize-space()='" + orderNumber + "']]//button[normalize-space()='" + cancelLabel + "']"), जहाँ cancelLabel strings table से पढ़ा जाता है ("Cancel" या «إلغاء»); बटन पर test id हो तो यह //tr[td[normalize-space()='…']]//*[@data-testid='order-cancel'] बनता है। दोनों relative हैं, टेस्ट के अपने डेटा पर टिके हैं और readable हैं; page object का method cancelOrder(orderNumber) उन्हें एक जगह रखता है।
नतीजे पर checklist। Uniqueness: हर दोबारा लिखा locator अपेक्षित state में एक एलिमेंट से मेल खाता है; row scope ही "Cancel", "Track" और status badge को unique बनाता है, और न हो तो Playwright का strict mode बता देगा। Restyle, re-sort और wrapper div में टिकना: हाँ, क्योंकि कुछ भी classes, indexes या depth का हवाला नहीं देता। Priority order: हर जगह role या label, लिखे कारण के साथ एक test id (status का text assert होता है, उससे खोजा नहीं जाता), और XPath सिर्फ़ Selenium में जहाँ role locator नहीं है; label जुड़ने तक search field का name hint text पर टिका है, और review comment यह बात लिखता है। Locale: यूज़र को दिखने वाला हर name हर locale की strings table से आता है, इसलिए Arabic run वही locators इस्तेमाल करता है। एक page object में रहना: OrdersPage.row(orderNumber), cancel(orderNumber) वग़ैरह, टेस्ट body में नहीं। Error messages: row.getByRole("button", { name: "Cancel" }) fail हो तो Playwright row filter और ग़ायब "Cancel" बटन बताता है; /html/body/main/table/tbody/tr[2]/td[5]/button[2] fail हो तो वह एक path बताता है और किसी को पता नहीं कौन-सा एलिमेंट चाहिए था।
अगर आपके जवाब अलग हैं, तो उन्हें गाइड के दो सवालों से जाँचें: क्या locator सिर्फ़ तब बदलेगा जब यूज़र जिस व्यवहार पर निर्भर है वह बदले, और क्या वह अपेक्षित state में एक एलिमेंट से मेल खाता है? दर्ज कारण के साथ अलग चुनाव ठीक है — label जुड़ने तक getByPlaceholder("Search orders"), दो भाषाओं वाले suite के लिए "Cancel" पर test id, या getByLabel की जगह getByRole("combobox", { name: "Status" }) — लेकिन generated id, styling class, index या absolute path ठीक नहीं, और न ही ऐसा locator जो ग़ायब accessible name के चारों ओर रास्ता बनाए।
इस topic में आपकी प्रगति
प्रगति ट्रैक करने के लिए साइन इन करें
Topics पूर्ण चिह्नित करें, जहाँ छोड़ा था वहीं से जारी रखें और specialisation परीक्षाएँ खोलें।
QA रोडमैप का हिस्सा
इस topic से पहले
अगले topics
इस गाइड में बताए गए टूल्स
नेस्टेड एलिमेंट को सही इंडेंटेशन देकर HTML सुंदर बनाएँ, pre, script और style ब्लॉक सुरक्षित रखते हुए।
रेगुलर एक्सप्रेशन को लाइव टेस्ट करें — मैच हाइलाइटिंग, कैप्चर ग्रुप और रिप्लेस प्रीव्यू के साथ।
दो टेक्स्ट की लाइन-दर-लाइन या शब्द-दर-शब्द तुलना करें और अंतर हाइलाइट करें।