Playwright बेसिक्स
Playwright, Microsoft का open-source browser automation framework है। इसका test runner, Playwright Test (@playwright/test), Chromium, Firefox और WebKit चलाता है, हर test को नया browser context देता है, element पर action से पहले उसके तैयार होने का इंतज़ार करता है, और assertions को तब तक retry करता है जब तक page expected state में न पहुँचे। Test किसी manual test case जैसा पढ़ा जाता है: /checkout खोलें, "Coupon code" में SAVE10 भरें, "Apply" पर click करें, और 100.00 SAR वाले दो lamps का total 180.00 SAR होने की उम्मीद करें।
यह क्यों ज़रूरी है
UI tests किसी suite की सबसे धीमी और सबसे नाज़ुक layer हैं, और उनकी कई failures product defects से नहीं बल्कि timing और shared state से आती हैं। Playwright अपने design से इनमें से कई कारण हटा देता है: actions इंतज़ार करते हैं, assertions retry करते हैं, और हर test isolated है। यह तभी काम आता है जब आप इसका इस्तेमाल करें; fixed sleeps और एक बार पढ़ने वाले checks किसी भी tool में flaky रहते हैं।
Testers अक्सर पहले से बनी किसी Playwright suite से जुड़ते हैं; fail हुए test का trace पढ़ना product defect और test defect को जल्दी अलग करता है और bug report को सटीक evidence देता है।
Playwright Test कैसे काम करता है
npx playwright test playwright.config.ts पढ़ता है और test files को parallel worker processes में चलाता है। हर test को नाम से माँगे गए fixtures मिलते हैं: page नए browser context (एक isolated profile, जैसे नई incognito window) में एक tab है, इसलिए cookies और storage tests के बीच नहीं रिसते; request HTTP calls भेजता है, जैसे test data बनाने के लिए; और आपके अपने fixtures test.extend से आते हैं।
- Locators:
getByRole("button", { name: "Apply" }),getByLabel,getByText,getByTestId। Locator हर इस्तेमाल पर दोबारा resolve होता है, इसलिए re-render के बाद भी काम करता है, और वह strict है: दो elements से match होने वाले locator पर action किसी एक को चुनने के बजाय fail होता है। - Auto-waiting: click से पहले Playwright इंतज़ार करता है कि element visible हो, stable हो (हिल न रहा हो), events receive करे (ढका न हो) और enabled हो;
fillसे पहले वह field के visible, enabled और editable होने का इंतज़ार करता है। - Web-first assertions:
await expect(locator).toHaveText("180.00 SAR")match होने या expect timeout ख़त्म होने तक retry करता है, जबकिexpect(await locator.textContent()).toBe("180.00 SAR")एक बार पढ़ता है और page से race करता है। - Trace viewer: हर action उससे पहले और बाद के DOM snapshots के साथ, और console, network व source; बिना देखे fail हुए test के लिए सबसे पहले यही खोलें।
- 1Config पढ़ना
playwright.config.tsसेbaseURL, timeouts, projects, retries और reportersnpx playwright test - 2Setup projectएक बार sign in करके cookies और local storage उन projects के लिए save करता है जो इस पर depend करते हैं
playwright/.auth/user.json - 3नया context और fixturesWorker test के लिए नया browser context खोलता है और
pageवcheckoutजैसे custom fixtures setup करता है - 4LocatorElement का वर्णन; हर इस्तेमाल पर दोबारा ढूँढा जाता है और एक ही match की शर्त रखता है
getByRole('button', { name: 'Apply' }) - 5Auto-wait, फिर action
clickसे पहले: visible, stable, events receive करता, enabled;fillसे पहले: visible, enabled, editableदोहराता है: Element तैयार होने या test timeout तक दोबारा जाँचता है
- 6Web-first assertion
await expect(total).toHaveText('180.00 SAR')दोहराता है: Pass होने या 5 s के expect timeout तक retry करता है
शाखा: फिर भी fail: test fail होता है; CI पर retry होता है और पहले retry का trace record होता हैफ़ेल
- 7TeardownFixtures cleanup करते हैं; context, उसकी cookies और storage हटा दिए जाते हैं
- 8ReportList output और HTML report; retry पर record हुए traces trace viewer में खुलते हैं
npx playwright show-report
चरण 3–7 के दौरान हर test के लिए एक बार, parallel workers में: cookies या storage अगले test तक नहीं जाते
Install, project layout और config file
npm init playwright@latest @playwright/test जोड़ता है, playwright.config.ts और एक example test लिखता है, GitHub Actions workflow का विकल्प देता है और browsers download करता है; खाली machine पर npx playwright install --with-deps browsers और उनकी system libraries install करता है। Environments के फ़र्क config में रहते हैं, tests में कभी नहीं:
use.baseURL: testspage.goto("/checkout")बुलाते हैं, और एक environment variable local, staging और review app के बीच switch करता है।timeoutऔरexpect.timeout: default हर test के लिए 30 seconds और हर assertion के लिए 5 seconds। इन्हें मापे हुए कारण से बढ़ाएँ, किसी धीमे step को छिपाने के लिए कभी नहीं।projects: हर browser या device profile के लिए एक, साथ में एकsetupproject जिसे बाकी projectsdependenciesमें लिखते हैं, ताकि वह पहले चले।retries: local पर 0, CI पर 1–2। जो test सिर्फ़ retry में pass हो वह flaky report होता है: उसकी जाँच करें।reporterऔरtrace:listprogress दिखाता है,htmlplaywright-report/लिखता है, और"on-first-retry"failed test के पहले retry पर trace record करता है।
// shop-e2e/
// package.json "@playwright/test" in devDependencies
// playwright.config.ts this file
// tests/ *.spec.ts, auth.setup.ts, fixtures.ts, files/ (uploads)
// pages/ page objects
// playwright/.auth/ saved sessions: add to .gitignore
// playwright-report/ HTML report (generated)
// test-results/ traces and screenshots (generated)
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests',
timeout: 30_000, // whole test, in ms (the default)
expect: { timeout: 5_000 }, // each web-first assertion (the default)
fullyParallel: true, // tests inside one file also run in parallel
forbidOnly: !!process.env.CI, // a forgotten test.only fails the CI run
retries: process.env.CI ? 2 : 0,
reporter: [['list'], ['html', { open: 'never' }]],
use: {
baseURL: process.env.BASE_URL ?? 'http://localhost:3000',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
},
projects: [
{ name: 'setup', testMatch: /.*\.setup\.ts/ },
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
dependencies: ['setup'],
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
dependencies: ['setup'],
},
],
});Run, debug और एक minimal CI job
रोज़ इस्तेमाल होने वाले Playwright commands, और फिर एक minimal CI job:
npx playwright test tests/checkout.spec.ts:10सिर्फ़ वह test चलाता है जो line 10 पर declare है (test body के अंदर की line किसी से match नहीं होती);-g "expired coupon"title से filter करता है (regular expression से),--project=chromiumbrowser से।--headedbrowser दिखाता है;--uiUI mode खोलता है, जिसमें आप tests चुनते हैं, दोबारा चलाते हैं और उनके steps पर आगे-पीछे जाते हैं।--debugPlaywright Inspector खोलता है: एक worker, दिखता browser, कोई timeout नहीं, पहले action से पहले रुका हुआ।await page.pause()headed run में आपकी चुनी line पर रोकता है।npx playwright show-reportHTML report खोलता है औरnpx playwright show-traceएकtrace.zip, CI वाला भी;--trace onlocal run के हर test का trace बनाता है।- CI:
container:हर step को एक Docker image के अंदर चलाता है, जो एक तैयार Linux environment है। Official imagemcr.microsoft.com/playwrightमें Node.js, browsers और उनकी system libraries पहले से हैं, इसलिए job सिर्फ़npm ciऔर tests चलाता है। उसका tagpackage-lock.jsonमें@playwright/testके version से match होना चाहिए।secrets.*औरvars.*repository settings में रखी values पढ़ते हैं, इसलिए file में कोई password नहीं लिखा जाता। GitHub Actions का set कियाCI=trueretries औरforbidOnlyचालू करता है (भूला हुआtest.onlyrun fail कर देता है), और failures के बाद भी traces समेत report upload होती है।
# .github/workflows/e2e.yml
name: e2e
on: [pull_request]
jobs:
e2e:
runs-on: ubuntu-latest
container:
image: mcr.microsoft.com/playwright:v1.55.0-noble # = @playwright/test version
options: --user 1001
steps:
- uses: actions/checkout@v7
- run: npm ci # no "playwright install": the image has the browsers
- run: npx playwright test
env:
BASE_URL: ${{ vars.STAGING_URL }}
E2E_EMAIL: ${{ secrets.E2E_EMAIL }}
E2E_PASSWORD: ${{ secrets.E2E_PASSWORD }}
- uses: actions/upload-artifact@v7
if: ${{ !cancelled() }} # also when tests failed
with:
name: playwright-report
path: playwright-report/Signed-in sessions, network stubs, dialogs, tabs और uploads
हर test में form से sign in करना धीमा है और login टूटते ही सब कुछ fail कर देता है। इसके बजाय एक setup project एक बार sign in करके cookies और local storage storageState से save करता है; tests यह file test.use से load करते हैं। Sign-in आम तौर पर cookie या local storage में रहता है; session storage एक ही tab का होता है और save नहीं होता, इसलिए जो app sign-in वहाँ रखता है, उसके tests signed in शुरू नहीं होते। File को version control से बाहर और credentials को environment variables में रखें। एक account share करने वाले tests को ऐसा data नहीं बदलना चाहिए जिसे दूसरे पढ़ते हैं, वरना हर worker को अपना account चाहिए।
page.route page से भेजी गई matching requests intercept करता है (आपके server की अपनी calls नहीं): route.fulfill आपके status और body से जवाब देता है, route.abort network failure simulate करता है। इसे request भेजे जाने से पहले register करें। जो आपका नहीं है या जिसे आप trigger नहीं कर सकते, जैसे provider outage, उसे stub करें; सब कुछ stub करेंगे तो tests सिर्फ़ stubs को साबित करेंगे।
Browser dialogs (alert, confirm, prompt) अपने-आप dismiss हो जाते हैं जब तक आप handler register न करें, और तब handler को हर dialog accept या dismiss करना होगा; HTML modal dialog नहीं है: उसके लिए locators इस्तेमाल करें। नया tab खोलने वाला link popup event देता है: click से पहले उसका इंतज़ार शुरू करें। setInputFiles working directory के relative path से <input type="file"> भरता है।
// tests/auth.setup.ts: runs once, before the projects that depend on 'setup'
import { test as setup, expect } from '@playwright/test';
const authFile = 'playwright/.auth/user.json';
setup('sign in once', async ({ page }) => {
await page.goto('/login');
// process.env values may be undefined; "!" tells TypeScript they are set (CI secrets)
await page.getByLabel('Email').fill(process.env.E2E_EMAIL!);
await page.getByLabel('Password').fill(process.env.E2E_PASSWORD!);
await page.getByRole('button', { name: 'Sign in' }).click();
await expect(page.getByRole('heading', { name: 'My account' })).toBeVisible();
await page.context().storageState({ path: authFile }); // cookies + local storage
});
// tests/account.spec.ts: every test in this file starts signed in
import { test, expect } from '@playwright/test';
test.use({ storageState: 'playwright/.auth/user.json' });
test('saved cards: a provider outage is reported', async ({ page }) => {
await page.route('**/api/saved-cards', (route) =>
route.fulfill({ status: 503, json: { error: 'provider unavailable' } }));
await page.goto('/account/payment'); // route registered first
await expect(page.getByRole('alert')).toContainText('try again later');
});
test('terms of use open in a new tab', async ({ page }) => {
await page.goto('/account');
const popupPromise = page.waitForEvent('popup'); // wait before the click
await page.getByRole('link', { name: 'Terms of use' }).click();
const terms = await popupPromise;
await expect(terms).toHaveURL(/terms/);
});
test('profile photo upload shows a preview', async ({ page }) => {
await page.goto('/account/profile');
await page.getByLabel('Profile photo').setInputFiles('tests/files/avatar.png');
await expect(page.getByRole('img', { name: 'Photo preview' })).toBeVisible();
});आज़माएँ: Mock API रिस्पॉन्स जनरेटर आज़माएँ: Mock एरर रिस्पॉन्स जनरेटर
Worked example: page object के साथ checkout tests
Requirement: guest checkout coupons स्वीकार करता है; SAVE10 10% घटाता है; expired code error दिखाता है और total वही रखता है; shipping-quote service down हो तो page बताता है कि shipping e-mail से confirm होगी और फिर भी order करने देता है; आख़िरी item हटाने पर browser के confirm dialog में confirmation माँगा जाता है। Desk lamp की कीमत 100.00 SAR है; shipping अलग दिखाई जाती है, total में शामिल नहीं।
एक लंबे test की जगह चार छोटे tests। Screen को सिर्फ़ CheckoutPage जानता है; एक fixture हर test को तैयार page object देता है; हर test page.request से अपना guest cart बनाता है, जो page की cookies share करता है, इसलिए parallel tests एक-दूसरे के carts नहीं देखते। तब field का label बदलने पर चार tests नहीं, एक line बदलती है।
// pages/CheckoutPage.ts: the only file that knows the checkout screen
import type { Locator, Page } from '@playwright/test';
export class CheckoutPage {
readonly couponInput: Locator;
readonly applyButton: Locator;
readonly total: Locator;
readonly alert: Locator;
readonly placeOrder: Locator;
// "private readonly page" also stores the argument as this.page
constructor(private readonly page: Page) {
this.couponInput = page.getByLabel('Coupon code');
this.applyButton = page.getByRole('button', { name: 'Apply' });
this.total = page.getByTestId('order-total');
this.alert = page.getByRole('alert');
this.placeOrder = page.getByRole('button', { name: 'Place order' });
}
// A new guest cart for this test: page.request shares cookies with the page.
async openWith(sku: string, qty: number) {
await this.page.request.post('/api/cart/items', { data: { sku, qty } });
await this.page.goto('/checkout');
}
async applyCoupon(code: string) {
await this.couponInput.fill(code);
await this.applyButton.click();
}
async removeItem(name: string) {
const row = this.page.getByRole('row').filter({ hasText: name });
await row.getByRole('button', { name: 'Remove' }).click();
}
}
// tests/fixtures.ts: a test that asks for "checkout" gets a ready page object
import { test as base } from '@playwright/test';
import { CheckoutPage } from '../pages/CheckoutPage';
export const test = base.extend<{ checkout: CheckoutPage }>({
checkout: async ({ page }, use) => {
await use(new CheckoutPage(page));
},
});
export { expect } from '@playwright/test';
// tests/checkout.spec.ts: desk lamp LAMP-01 costs 100.00 SAR
import { test, expect } from './fixtures';
test('valid coupon takes 10% off', async ({ checkout }) => {
await checkout.openWith('LAMP-01', 2);
await checkout.applyCoupon('SAVE10');
await expect(checkout.total).toHaveText('180.00 SAR');
});
test('expired coupon is refused, total unchanged', async ({ checkout }) => {
await checkout.openWith('LAMP-01', 2);
await checkout.applyCoupon('OLD5');
await expect(checkout.alert).toHaveText('This coupon has expired');
await expect(checkout.total).toHaveText('200.00 SAR');
});
test('shipping quote down: order still possible', async ({ page, checkout }) => {
await page.route('**/api/shipping-quote', (route) =>
route.fulfill({ status: 503, json: { error: 'unavailable' } }));
await checkout.openWith('LAMP-01', 1);
await expect(page.getByText('Shipping will be confirmed by e-mail')).toBeVisible();
await expect(checkout.placeOrder).toBeEnabled();
});
test('last item: confirm, then empty cart', async ({ page, checkout }) => {
await checkout.openWith('LAMP-01', 1);
page.once('dialog', (dialog) => dialog.accept()); // without a handler: dismissed
await checkout.removeItem('Desk lamp');
await expect(page.getByText('Your cart is empty')).toBeVisible();
});आम गलतियाँ
- Condition का इंतज़ार करने के बजाय
page.waitForTimeout()बुलाना या timeouts बढ़ाना। - Action या assertion से पहले
awaitभूल जाना, जिससे test कुछ जाँचे बिना pass हो सकता है;@typescript-eslint/no-floating-promiseslint rule इसे पकड़ता है। - Retry करने वाले
await expect(locator).toBeVisible()के बजाय एक बार पढ़ी गई value पर assert करना, जैसेexpect(await locator.isVisible()).toBe(true)। - हर test में UI से sign in करना, या
storageStatefile या कोई password commit करना। - हर request को stub करना, जिससे असली API बदलने के बाद भी UI tests pass होते रहते हैं।
- Docker image का tag installed
@playwright/testversion से अलग होना, जिससे CI को expected browsers नहीं मिलते।
QA का नज़रिया
Testers Playwright से तब मिलते हैं जब वे नई stories के tests लिखते हैं, tests बदलने वाले pull requests review करते हैं और लाल CI runs का triage करते हैं। Review में देखें: user-facing locators, web-first assertions, कोई fixed sleep नहीं, हर test में एक behaviour, और हर test का ख़ुद बनाया data; जहाँ stable role या label न हो, वहाँ developers के साथ data-testid values तय करें।
CI fail हो तो दोबारा चलाने से पहले trace खोलें: यह दिखाता है कि गड़बड़ product की है (DOM snapshot में गलत total, network tab में 500) या test की (गलत element पकड़ने वाला locator)। Ticket में title, project, commit और trace दर्ज करें, और जल्दी पूछें कि कौन-सी services stub हो सकती हैं और CI कौन-से accounts इस्तेमाल कर सकता है।
अभ्यास
एक नाज़ुक Playwright test review करके दोबारा लिखें
एक साथी ने support page के लिए यह test लिखा: signed-in user receipt attach करता है, ticket भेजता है और help centre खोलता है। यह CI में कभी-कभी fail होता था; जब से help centre नए tab में खुलने लगा है, यह हर बार fail होता है। Problems लिखें और इसे इस lesson के अनुसार दोबारा लिखें।
import { test, expect } from '@playwright/test';
test('support', async ({ page }) => {
await page.goto('https://staging.shop.example/login');
await page.fill('#email', 'sara.qa@example.com');
await page.fill('#pwd', 'Winter2026');
await page.click('.btn-primary');
await page.waitForTimeout(3000);
await page.goto('https://staging.shop.example/support');
await page.locator('input[type=file]').setInputFiles('C:/Users/sara/receipt.pdf');
await page.fill('#message', 'Charged twice for order 1042');
await page.click('text=Send');
expect(await page.locator('.toast').isVisible()).toBe(true);
await page.click('text=Help centre');
await expect(page).toHaveURL(/help/);
});- वे lines चिह्नित करें जो एक machine, एक व्यक्ति, असली side effects या fixed delay पर निर्भर हैं।
- Selectors को user-facing locators से और एक बार पढ़ने वाले check को web-first assertion से बदलें।
- तय करें क्या config में जाएगा, क्या setup project में, और क्या
page.routestub बनेगा। - इसे ऐसे tests में बाँटें जो
SupportPagepage object इस्तेमाल करें।
मॉडल उत्तर दिखाएँ
Problems: code में पूरे staging URLs; test के अंदर sign in, code में password के साथ; 3 seconds का fixed sleep; markup से बँधे selectors जैसे #email, .btn-primary और .toast; receipt का path जो सिर्फ़ एक laptop पर है; एक बार पढ़ने वाला isVisible() check जो अक्सर toast आने से पहले चलता है, शायद पुरानी intermittent failure की वजह; हर run में बनने वाला असली ticket; toHaveURL पुराना page जाँचता है जबकि help centre नए tab में खुलता है; एक test में तीन behaviours।
दोबारा लिखा गया रूप: config से baseURL; sign in setup project में जाता है और credentials environment variables से आते हैं, और tests test.use({ storageState: "playwright/.auth/user.json" }) से signed in शुरू होते हैं; fixed sleep हटा दिया जाता है, क्योंकि actions और assertions ख़ुद इंतज़ार करते हैं। SupportPage page object में getByLabel("Receipt"), getByLabel("Message") और getByRole("button", { name: "Send" }) locators होते हैं। Test 1 **/api/tickets को 201 से stub करता है, tests/files/receipt.pdf attach करता है, send करता है और getByText("Ticket sent") के visible होने की उम्मीद करता है। Test 2 503 से stub करके requirement में दिया error message assert करता है; एक अलग API test असली endpoint जाँचता रहता है। Test 3 "Help centre" पर click से पहले popup का इंतज़ार करता है और नए page पर toHaveURL(/help/) assert करता है।
अक्सर पूछे जाने वाले सवाल
क्या TypeScript इस्तेमाल करना ज़रूरी है?
नहीं। इस lesson का runner, Playwright Test, JavaScript या TypeScript चलाता है। Playwright, Python, Java और .NET के लिए भी उपलब्ध है, उन्हीं locators, auto-waiting और web-first assertions के साथ, उस language के आम test runner (जैसे pytest) के ज़रिए। वही language चुनें जो आपकी team पहले से इस्तेमाल करती है।
क्या tests लिखने के बजाय record कर सकते हैं?
हाँ, draft के रूप में। URL के साथ npx playwright codegen browser खोलता है और आपके click करते-करते code लिखता है, role-based locators को प्राथमिकता देते हुए। फिर तय करें क्या assert करना है, steps को page objects में ले जाएँ और किसी एक data set से बँधी हर चीज़ हटाएँ।
अगर retries से CI run हरा हो जाए, तो क्या test ठीक है?
नहीं। सिर्फ़ retry में pass होने वाला test flaky report होता है। Retries एक unstable test को हर merge रोकने से बचाते हैं, पर कारण बना रहता है: failed attempt का trace खोलें, test या product ठीक करें, और ठीक होने तक track करें।
Test local पर pass और सिर्फ़ CI में fail क्यों होता है?
आम तौर पर इसलिए कि CI अलग है: धीमी machine, data share करने वाले parallel workers, दूसरा base URL या account, दूसरा browser project, या कोई missing secret। कोई timeout बदलने से पहले uploaded report से trace खोलें, और वही --project और environment variables लेकर test को local पर चलाएँ।
संदर्भ
- Playwright DocumentationPlaywright का आधिकारिक दस्तावेज़ — Chromium, WebKit और Firefox चलाने वाला end-to-end टेस्ट फ़्रेमवर्क।
- Best Practices — Playwrightयूज़र को दिखने वाला व्यवहार टेस्ट करें, टेस्ट अलग-थलग रखें, मज़बूत locator और web-first assertions अपनाएँ, और trace से डिबग करें।
- Auto-waiting — Playwrightहर एक्शन से पहले Playwright की actionability जाँचें — visible, stable, enabled, events प्राप्त करने योग्य — जो ज़्यादातर हाथ से लिखे wait की ज़रूरत हटाती हैं।
- Locators — PlaywrightPlaywright एलिमेंट कैसे ढूँढता है, locator दोबारा resolve होकर अपने-आप इंतज़ार क्यों करते हैं, और role-आधारित locator पहले क्यों सुझाए जाते हैं।
Mutqan tools से अभ्यास करें
success, list, detail, error और empty केस के लिए पूरे mock API रिस्पॉन्स (स्टेटस, हेडर, JSON बॉडी) बनाएँ।
लोकप्रिय फ़ॉर्मेट में संरचित एरर रिस्पॉन्स बनाएँ (validation, auth, not found, rate limit, सर्वर एरर)।
YAML को एकसमान इंडेंटेशन और quoting के साथ फ़ॉर्मेट और वैलिडेट करें।
रेगुलर एक्सप्रेशन को लाइव टेस्ट करें — मैच हाइलाइटिंग, कैप्चर ग्रुप और रिप्लेस प्रीव्यू के साथ।
और गहराई से
इस topic में आपकी प्रगति
प्रगति ट्रैक करने के लिए साइन इन करें
Topics पूर्ण चिह्नित करें, जहाँ छोड़ा था वहीं से जारी रखें और specialisation परीक्षाएँ खोलें।