সরাসরি পর্বে যান
পর্ব ৭৬ · ৯৩-এর মধ্যে · মডিউল ১১

End-to-end test — Playwright আর নির্ভরযোগ্য selector

End-to-End Testing with Playwright — Locators, Auto-Waiting, Auth State, Traces and Accessibility

Taught in Bangla, this lesson places end-to-end tests in the strategy, starts Playwright Test with config and webServer, uses user-facing locators such as getByRole and strict mode, replaces sleeps with auto-waiting and web-first assertions, writes a full Bazarghar purchase journey with API-created test data, logs in once with storageState and a setup project, changes the network with page.route, reads failures with traces and UI mode, and adds screenshots.

এই পর্বে যা শিখবেন

  • Playwright Test শুরু করা: npm init playwright, config, browser-এর project, Edge channel আর webServer
  • নির্ভরযোগ্য locator বাছাই, chaining আর filter, আর strict mode-এর আসল error পড়া
  • Auto-waiting আর web-first assertion দিয়ে একটি সত্যিকারের flaky test স্থির করা
  • বাজারঘরের পুরো কেনাকাটার পথ, API দিয়ে test data, storageState-এ একবার লগইন, আর page.route দিয়ে ব্যর্থতার পথ
  • Trace viewer, retry, parallel, visual comparison, axe দিয়ে accessibility আর ৩৬০ px মোবাইল, আর কতগুলো E2E রাখা উচিত

১ · পুরো দোকান একসাথে পরীক্ষা — E2E কোথায় দাঁড়ায়

পর্ব ৬৫-এ একটি ছোট script Microsoft Edge খুলে বাজারঘরে দুই কেজি ইলিশ কিনেছিল, নকল gateway-তে “Pay (test)” চেপেছিল, আর ফিরে আসা পাতার লেখা পড়েছিল। সেটি ছিল সাধারণ Playwright library দিয়ে লেখা একটি script: browser চালু, ক্লিক, লেখা ছাপা। কাজ করেছিল, কিন্তু সেটি test নয়। কোনটি পাস, কোনটি ফেল, ব্যর্থ হলে কী দেখতে হবে, server কে চালু করবে, দশটি test একসাথে কীভাবে চলবে, এসবের উত্তর সেখানে ছিল না। এই পর্বে সেই উত্তর দেয় Playwright Test: একই দলের বানানো test runner, যা browser চালানোর library-র ওপর বসে।

একটি বড় মুদি দোকানের মালিক প্রতি মাসে একজন পরিচিত মানুষকে সাধারণ ক্রেতা সাজিয়ে পাঠান। তিনি দরজা দিয়ে ঢোকেন, তাক থেকে ডাল খোঁজেন, ঝুড়িতে রাখেন, কাউন্টারে দাম দেন, রসিদ নেন। রান্নাঘরের দাঁড়িপাল্লা ঠিক আছে কি না, সেটি তিনি আলাদা করে মাপেন না; সেটি অন্য পরীক্ষা। তাঁর কাজ একটাই প্রশ্নের উত্তর দেওয়া: “একজন ক্রেতা কি শুরু থেকে শেষ পর্যন্ত কিনতে পারলেন?” End-to-end test ঠিক এই নকল ক্রেতা। তিনি ধীর, তাঁকে পাঠাতে খরচ আছে, তাই প্রতিটি তাক পরীক্ষার জন্য তাঁকে পাঠানো হয় না। কিন্তু যে কাজগুলো না হলে দোকানই চলে না, সেগুলোর জন্য তিনিই সবচেয়ে বিশ্বাসযোগ্য সাক্ষী।

এই মডিউলের অন্য পর্বগুলোর সাথে ভাগটা এরকম। পর্ব ৭৩-এ (Unit test — Vitest) একটি function আলাদা করে, পর্ব ৭৪-এ (Component test) একটি component jsdom-এ, পর্ব ৭৫-এ (API integration test) HTTP আর test database একসাথে। এই পর্বে সব স্তর একসাথে: আসল browser, আসল HTML আর CSS, আসল JavaScript, আসল server, আসল cookie. কোন স্তরে কতগুলো test রাখবেন, সেই কৌশল পর্ব ৭২-এর (ফুল-স্ট্যাক test কৌশল) বিষয়; pyramid-এর তত্ত্ব আছে Software Engineering-এর test pyramid পর্বে। এখানে আমরা শুধু শেষ স্তরটি ভালোভাবে লিখতে শিখব।

Playwright Test একবার চললে কে কী করে

Test runner, web server, workers and browser projects

Playwright Test-এর অংশগুলো Test runner npx playwright test ১ webServer node server.mjs /health → 200 ২ setup project একবার লগইন → user.json ৩ worker-রা একসাথে edge 1280 × 720 journey edge user.json সহ account mobile-360 360 × 800 touch বাজারঘর · 127.0.0.1:47761 ৪ reporter: ok / x / flaky trace.zip, screenshot, তারপর server বন্ধ
ছবি ৭৬.১ — Runner নিজেই server চালু করে (১), একবার লগইন করে state রাখে (২), তারপর কয়েকটি worker আলাদা browser context-এ test চালায় (৩)। শেষে ফল আর ব্যর্থতার প্রমাণ (৪)।

এই পর্বের প্রতিটি আউটপুট একটি সত্যিকারের ল্যাব থেকে। লেখার সময়ের version: Node 24.15.0, @playwright/test 1.64.1 (npm-এর latest), @axe-core/playwright 4.14.0, Express 5.3.0. ল্যাবের বাজারঘর ইচ্ছা করে ছোট: একটি Express ফাইল, ডেটা memory-তে, পাঁচটি পণ্য, cookie-র session, লগইন, cart, checkout আর পর্ব ৬৫-এর BazarPay-এর একটি stub, যা একই server-এ “TEST MODE” পাতা দেখায় আর webhook-এর বদলে সরাসরি order-এর অবস্থা বদলায়। PostgreSQL আর আসল webhook-সহ পূর্ণ পথ পর্ব ৬৫-এ আছে; এখানে লক্ষ্য browser-এর দিক। Server চলেছে শুধু 127.0.0.1-এ, port 47761 (demo-র জন্য 47762).

Browser automation-এর ইতিহাস ছোট নয়। ThoughtWorks-এ Jason Huggins ২০০৪ সালে যে টুল বানান, তা পরে Selenium নামে পরিচিত হয়। Selenium-এর WebDriver পদ্ধতি পরে W3C-র মান হয়; WebDriver Recommendation প্রকাশ হয় ২০১৮ সালে। Google-এর Chrome দল ২০১৭ সালে Puppeteer আনে, যা Chrome DevTools Protocol দিয়ে সরাসরি browser চালায়। সেই দলের কয়েকজন পরে Microsoft-এ গিয়ে ২০২০ সালে Playwright প্রকাশ করেন, শুরু থেকেই Chromium, Firefox আর WebKit, তিনটিকেই লক্ষ্য করে। Cypress-এর মতো অন্য টুলও জনপ্রিয়, আর কোনটি ভালো, তা নিয়ে দলগুলোর মত ভিন্ন; এই কোর্স Playwright ব্যবহার করে কারণ পর্ব ৬৫-এ সেটিই চলেছে আর একাধিক browser, একাধিক tab ও network নিয়ন্ত্রণ এতে সহজ।

২ · শুরু — npm init playwright, config, Edge আর webServer

নতুন project-এ শুরু করার সরকারি পথ একটি কমান্ড। এটি সাধারণত কয়েকটি প্রশ্ন করে: TypeScript না JavaScript, test কোন folder-এ, GitHub Actions-এর ফাইল লাগবে কি না, browser নামাবে কি না। ল্যাবে প্রশ্ন এড়াতে create-playwright-এর flag দেওয়া হয়েছে, আর --no-browsers, কারণ আমরা Playwright-এর নিজের Chromium নামাব না, কম্পিউটারে থাকা Microsoft Edge ব্যবহার করব। সত্যিকারের run, রং-এর code আর project-এর পূর্ণ path বাদ দিয়ে:

$ npm init playwright@latest -- --quiet --lang=ts --browser=chromium --no-browsers
npm warn exec The following package was not found and will be installed: create-playwright@1.17.139
Getting started with writing end-to-end tests with Playwright:
Initializing project in '.'
Installing Playwright Test (npm install --save-dev @playwright/test)…
added 3 packages, and audited 4 packages in 807ms
Installing Types (npm install --save-dev @types/node)…
Writing playwright.config.ts.
Writing tests\example.spec.ts.
Writing package.json.
✔ Success! Created a Playwright Test project at ~/bazarghar
…
  npx playwright test --ui
    Starts the interactive UI mode.
  npx playwright codegen
    Auto generate tests with Codegen.

তিনটি জিনিস তৈরি হলো: package.json-এ "@playwright/test": "^1.64.1", একটি উদাহরণ test, আর playwright.config.ts. লক্ষ করুন, init-এর বানানো config-এ retries: process.env.CI ? 2 : 0 লেখা থাকে: CI-তে নিজে থেকেই দুবার আবার চেষ্টা। এই পর্বের ৮ নম্বর অংশে দেখবেন কেন এটি নিয়ে সাবধান থাকা দরকার। বাজারঘরের জন্য config এরকম দাঁড়াল:

import { defineConfig, devices } from '@playwright/test';
const PORT = 47761;

export default defineConfig({
  testDir: './tests',
  fullyParallel: true,
  forbidOnly: !!process.env.CI,       // CI-তে ভুলে থাকা test.only ধরা
  retries: 0,
  reporter: 'list',
  use: {
    baseURL: `http://127.0.0.1:${PORT}`,  // page.goto('/cart') চলে
    trace: 'on-first-retry',
    screenshot: 'only-on-failure',
  },
  webServer: {
    command: 'node server.mjs',
    url: `http://127.0.0.1:${PORT}/health`,   // 2xx না আসা পর্যন্ত অপেক্ষা
    reuseExistingServer: !process.env.CI,
    env: { PORT: String(PORT), E2E_TEST_API: '1' },
    timeout: 20_000,
  },
  projects: [
    { name: 'setup', testMatch: /.*\.setup\.ts/, use: { channel: 'msedge' } },
    { name: 'edge', dependencies: ['setup'], testIgnore: /mobile/,
      use: { ...devices['Desktop Edge'], channel: 'msedge', storageState: 'playwright/.auth/user.json' } },
    { name: 'mobile-360', testMatch: /mobile\.spec\.ts/,
      use: { channel: 'msedge', viewport: { width: 360, height: 800 }, isMobile: true, hasTouch: true, deviceScaleFactor: 2 } },
  ],
});

অংশগুলো একে একে। webServer: runner নিজেই node server.mjs চালায়, url-এ সফল উত্তর না আসা পর্যন্ত অপেক্ষা করে, সব test শেষে process বন্ধ করে। reuseExistingServer সত্য হলে আর ওই port-এ আগে থেকেই কিছু চললে নতুন server চালায় না; নিজের কম্পিউটারে এটি সুবিধা, কিন্তু ভুল server-এ test চালানোর ঝুঁকিও, তাই CI-তে বন্ধ। projects: একই test একাধিক পরিবেশে চালানোর উপায়, প্রতিটিকে বলে একটি project. সাধারণত এখানে chromium, firefox, webkit থাকে। ল্যাবে Playwright-এর নিজস্ব browser নামানো হয়নি; channel: 'msedge' বলে দেয় কম্পিউটারে ইনস্টল করা Microsoft Edge চালাতে। devices['Desktop Edge'] শুধু user agent, 1280 × 720 viewport ইত্যাদি দেয়, channel নয়; ল্যাবে সেটি print করে দেখা হয়েছে, তাই channel আলাদা করে লেখা। Firefox আর WebKit-এর project যোগ করলে আগে npx playwright install firefox webkit লাগবে; ল্যাবে সেটি চালানো হয়নি।

Test-only API-র নিয়ম: ল্যাবের server-এ /api/test/reset আর /api/test/users আছে, যা দিয়ে test নিজের ডেটা বানায়। এগুলো চালু হয় শুধু E2E_TEST_API=1 থাকলে, আর সেটি দেয় কেবল webServer.env. Production-এর build বা environment-এ এই route থাকা মানে যে কেউ সব ডেটা মুছে দিতে পারে। তাই দুই স্তরের পাহারা রাখুন: route-টি কোডে শর্তসাপেক্ষ, আর production config-এ variable-টি কখনো নয় (পর্ব ৭৮-এ environment আর secret).

এখন পুরো suite প্রথমবার, সত্যিকারের run, Windows 10-এর একটি কম্পিউটারে। সময় একটি স্থানীয় run-এর, সাধারণ ফল নয়।

$ npx playwright test
Running 8 tests using 6 workers

  ok 1 [setup] › tests\auth.setup.ts:5:1 › একবার লগইন করে state রাখা (565ms)
  ok 2 [mobile-360] › tests\mobile.spec.ts:3:1 › ৩৬০ px: মেনু থেকে কার্টে যাওয়া, পাশে scroll নেই (626ms)
  ok 3 [edge] › tests\account.spec.ts:4:1 › লগইন করা ক্রেতা সরাসরি চেকআউটে যেতে পারেন (612ms)
  ok 6 [edge] › tests\search.spec.ts:11:1 › খোঁজার API 500 দিলে ক্রেতা স্পষ্ট বার্তা পান (755ms)
  ok 5 [edge] › tests\visual.spec.ts:5:1 › পণ্যের প্রথম কার্ড দেখতে আগের মতো (757ms)
  ok 8 [edge] › tests\a11y.spec.ts:6:1 › কার্ট পাতায় axe কোনো WCAG A/AA লঙ্ঘন পায় না (1.2s)
  ok 4 [edge] › tests\search.spec.ts:3:1 › খোঁজার ফল: web-first assertion অপেক্ষা করে (1.9s)
  ok 7 [edge] › tests\journey.spec.ts:6:1 › ক্রেতা খুঁজে, কার্টে রেখে, লগইন করে অর্ডার দেন (2.3s)

  8 passed (5.0s)

এটি অবশ্য তৃতীয় run-এর ছবি। প্রথম run-এ দুটি test ফেল করেছিল, আর সেই দুটি ব্যর্থতাই এই পর্বের সবচেয়ে শিক্ষণীয় অংশ: একটি accessibility-র আসল বাগ, অন্যটি screenshot-এর প্রথম baseline. ৯ নম্বর অংশে দুটিই দেখবেন। আগে দেখা যাক একটি test শুরু থেকে শেষ পর্যন্ত কীভাবে চলে।

  1. Configprojectsপড়া
  2. webServer/health200 পর্যন্ত
  3. setupuser.jsonএকবার
  4. Contextmsedgeনতুন, খালি
  5. ধাপগুলোfill, clickauto-wait
  6. AssertiontoHaveTextতারপর বন্ধ
Runner যা ঠিক করল৮টি test
setup       auth.setup.ts            1
edge        dependencies: setup      6
mobile-360  mobile.spec.ts           1
workers: 6  (fullyParallel)
webServerrunner-এর চালানো process
$ node server.mjs   (PORT=47761, E2E_TEST_API=1)
bazarghar on http://127.0.0.1:47761 test-api=true
GET /health → 200 {"ok":true}
এখন test শুরু হতে পারে
setup project565ms
POST /api/test/reset            → 204
POST /api/test/users  rina@…    → 201
UI-তে লগইন, তারপর
storageState → playwright/.auth/user.json
journey test-এর contextedge
browser: Microsoft Edge (channel msedge)
viewport 1280 × 720
storageState: { cookies: [], origins: [] }
অর্থাৎ লগইন ছাড়া নতুন ক্রেতা
ধাপপ্রতিটি নিজে অপেক্ষা করে
goto('/')
getByLabel('পণ্য খুঁজুন').fill('ডাল')
getByRole('button', { name: 'খুঁজুন' }).click()
… cart, login, checkout, Pay (test)
শেষ assertionok 2.3s
toHaveURL(/\/orders\/BG-\d+$/)          ✓
heading level 1 = 'অর্ডার নিশ্চিত হয়েছে' ✓
getByTestId('order-ref') = BG-…          ✓
context বন্ধ; সব শেষে server বন্ধ
  1. Config পড়া। Runner তিনটি project দেখে: setup-এ ১টি, edge-এ ৬টি, mobile-360-এ ১টি, মোট ৮টি test, ৬টি worker-এ।
  2. Server চালু। Runner নিজে node server.mjs চালায় আর /health-এ 200 না আসা পর্যন্ত কোনো test শুরু করে না। আপনাকে আলাদা terminal-এ server চালাতে হয় না।
  3. Setup একবার। edge project setup-এর ওপর নির্ভর করে, তাই আগে setup চলে: ডেটা reset, একজন ক্রেতা তৈরি, UI-তে লগইন, আর cookie user.json-এ লেখা।
  4. নতুন context. প্রতিটি test একটি নতুন browser context পায়, যেন নতুন incognito জানালা। Journey test ইচ্ছা করে খালি storageState নেয়, কারণ এটি নিজেই লগইন পরীক্ষা করবে।
  5. ধাপগুলো। প্রতিটি action locator ধরে, element তৈরি আর ক্লিকযোগ্য হওয়ার অপেক্ষা করে, তারপর কাজ করে। মাঝে কোথাও ঘুম নেই।
  6. Assertion আর পরিষ্কার। URL, heading আর order নম্বর মিলল, test পাস, ২.৩ সেকেন্ডে। Context বন্ধ হয়; সব test শেষে runner server-টিও বন্ধ করে।

৩ · Locator — ব্যবহারকারী যা দেখেন, test-ও তা-ই খোঁজে

পুরোনো E2E test-এর সবচেয়ে বড় শত্রু ছিল ভঙ্গুর CSS selector: div.container > ul li:nth-child(2) button.btn-primary. Designer একটি class বদলালেন বা তালিকার ক্রম বদলাল, test ভেঙে গেল, অথচ ক্রেতার কাছে কিছুই বদলায়নি। উল্টোটাও ঘটে: বোতামের লেখা মুছে শুধু একটি icon রাখা হলো, স্ক্রিন রিডারের ব্যবহারকারী আর বোতামটি চিনতে পারেন না, কিন্তু class একই থাকায় test সবুজ।

Playwright-এর locator এই সমস্যার উত্তর। Locator একটি “কীভাবে খুঁজব”-এর বর্ণনা, এখনই খুঁজে পাওয়া element নয়। প্রতিবার ব্যবহারের সময় এটি নতুন করে DOM-এ খোঁজে, তাই React পুরো তালিকা নতুন করে আঁকলেও পুরোনো element-এ আটকে থাকে না। সরকারি docs যে ক্রমে পছন্দ করতে বলে, মোটামুটি সেটিই এখানে:

Locatorকী দিয়ে খোঁজেবাজারঘরে উদাহরণ
getByRoleARIA role আর accessible name, যেমন স্ক্রিন রিডার দেখেgetByRole('button', { name: 'খুঁজুন' })
getByLabelForm-এর <label> লেখাgetByLabel('পাসওয়ার্ড')
getByTextদৃশ্যমান লেখা; বোতাম বা link নয় এমন অংশেgetByText('কার্ট খালি।')
getByPlaceholder, getByAltText, getByTitlePlaceholder, ছবির alt, titleলেবেল না থাকলে, কম পছন্দের
getByTestIddata-testid attributegetByTestId('order-ref')
locator('css=…')CSS বা XPathশেষ উপায়; ল্যাবে শুধু #total আর #list li

getByRole কেন প্রথম? কারণ এটি একসাথে দুটি জিনিস যাচাই করে: element আছে, আর সহায়ক প্রযুক্তি সেটিকে সঠিক নামে চেনে। নামহীন icon-বোতাম getByRole('button', { name: 'মুছুন' }) দিয়ে খুঁজে পাওয়া যায় না, ফলে test-ই আপনাকে accessibility-র ভুল দেখিয়ে দেয়। Accessible name কীভাবে হিসাব হয়, তার বিস্তারিত আছে HTML ও CSS কোর্সের accessibility পর্বে। getByTestId তখন, যখন ব্যবহারকারীর দেখা কোনো স্থির পরিচয় নেই, যেমন একটি order নম্বর, যা প্রতি run-এ বদলায়। বাংলা লেখা নিয়ে কোনো সমস্যা হয়নি: ল্যাবের সব locator বাংলা নামে, আর সবগুলো ঠিক মিলেছে।

Strict mode: একাধিক মিললে থেমে যাওয়া

বাজারঘরের প্রথম পাতায় পাঁচটি পণ্য, প্রতিটিতে একই লেখার বোতাম “কার্টে যোগ করুন”। এখন যদি লেখেন page.getByRole('button', { name: 'কার্টে যোগ করুন' }).click(), Playwright কোনটি চাপবে? পুরোনো অনেক টুল নীরবে প্রথমটি চাপত, আর test পাস করত, যদিও আপনি হয়তো ডাল চেয়েছিলেন। Playwright-এর action-গুলো strict: locator ঠিক একটি element-এ না মিললে ভুল ধরা হয়। সত্যিকারের run-এর বার্তা, path ছোট করে:

$ npx playwright test -c demo.config.ts strict
  x  1 [edge] › demo\strict.spec.ts:3:1 › strict: কোন বোতাম? (202ms)

    Error: locator.click: Error: strict mode violation: getByRole('button', { name: 'কার্টে যোগ করুন' }) resolved to 5 elements:
        1) <button type="button" data-sku="ilish-1kg">কার্টে যোগ করুন</button> aka getByRole('listitem').filter({ hasText: 'ইলিশ (১ কেজি)৳1250.00' }).getByRole('button')
        2) <button type="button" data-sku="masur-1kg">কার্টে যোগ করুন</button> aka getByRole('listitem').filter({ hasText: 'মসুর ডাল (১ কেজি)৳134.90' }).getByRole('button')
        3) <button type="button" data-sku="chinigura-1kg">কার্টে যোগ করুন</button> aka getByRole('listitem').filter({ hasText: 'চিনিগুঁড়া চাল (১ কেজি)৳160.' }).getByRole('button')
        4) <button type="button" data-sku="soap">কার্টে যোগ করুন</button> aka getByRole('listitem').filter({ hasText: 'সাবান৳45.50 কার্টে যোগ করুন' }).getByRole('button')
        5) <button type="button" data-sku="morich-250g">কার্টে যোগ করুন</button> aka getByRole('listitem').filter({ hasText: 'কাঁচা মরিচ (২৫০ গ্রাম)৳30.00' }).getByRole('button')
  1 failed

বার্তাটি শুধু অভিযোগ করে না, সমাধানও বলে: প্রতিটি element-এর পাশে aka-র পরে একটি অনন্য locator. সেই পরামর্শই হলো chaining আর filter: আগে সঠিক সারি, তারপর সেই সারির ভেতরে বোতাম। Journey test-এ এভাবে লেখা হয়েছে:

const dal = page.getByRole('listitem').filter({ hasText: 'মসুর ডাল' });
await dal.getByRole('button', { name: 'কার্টে যোগ করুন' }).click();

filter-এ hasText ছাড়াও has (ভেতরে আরেকটি locator আছে এমন), hasNot আর hasNotText দেওয়া যায়। .first(), .nth(2) আছে, কিন্তু সেগুলো ক্রমের ওপর নির্ভর করে, তাই সাবধানে। Strict-এর নিয়ম action-এ (click, fill) আর একক element-এর assertion-এ খাটে; toHaveCount বা .count()-এর মতো যে কাজ নিজেই অনেক element নিয়ে, সেখানে নয়। নিচের ব্লকে নিয়মটি একটি নকল DOM-এর ওপর ছোট মডেলে। এটি Playwright নয়, শুধু ধারণাটি দেখানোর জন্য।

// শিক্ষণ মডেল: locator-এর strictness, একটি নকল DOM-এর ওপর
const btn = (sku) => ({ role: "button", name: "কার্টে যোগ করুন", sku });
const dom = [
  { role: "listitem", text: "ইলিশ (১ কেজি) ৳1250.00", kids: [btn("ilish-1kg")] },
  { role: "listitem", text: "মসুর ডাল (১ কেজি) ৳134.90", kids: [btn("masur-1kg")] },
  { role: "listitem", text: "চিনিগুঁড়া চাল (১ কেজি) ৳160.00", kids: [btn("chinigura-1kg")] },
  { role: "button", name: "খুঁজুন", testid: "search-btn" },
];
const below = (n) => (n.kids ?? []).flatMap((k) => [k, ...below(k)]);

function locator(nodes, desc) {
  return {
    count: () => nodes.length,
    getByRole: (role, { name } = {}) => locator(
      nodes.flatMap(below).filter((x) => x.role === role && (name === undefined || x.name === name)),
      `${desc}.getByRole('${role}'${name ? `, '${name}'` : ""})`),
    getByTestId: (id) => locator(nodes.flatMap(below).filter((x) => x.testid === id), `${desc}.getByTestId('${id}')`),
    filter: ({ hasText }) => locator(nodes.filter((x) => (x.text ?? "").includes(hasText)),
      `${desc}.filter({ hasText: '${hasText}' })`),
    click() {                                // action-এর জন্য ঠিক একটি element চাই
      if (nodes.length === 0) return `waiting: ${desc} -> 0 elements (times out later)`;
      if (nodes.length > 1) return `strict mode violation: ${desc} -> ${nodes.length} elements`;
      return `clicked ${nodes[0].sku ?? nodes[0].testid}`;
    },
  };
}
const page = locator([{ kids: dom }], "page");
const add = { name: "কার্টে যোগ করুন" };

console.log(page.getByRole("button", add).click());
console.log(page.getByRole("button", add).count(), "buttons share that name");
console.log(page.getByRole("listitem").filter({ hasText: "মসুর" }).getByRole("button", add).click());
console.log(page.getByRole("listitem").filter({ hasText: "চাল" }).count(), "row(s) contain 'চাল'");
console.log(page.getByRole("listitem").filter({ hasText: "সাবান" }).getByRole("button").click());
console.log(page.getByTestId("search-btn").click());

প্রথম লাইন strict mode violation: page.getByRole('button', 'কার্টে যোগ করুন') -> 3 elements: নকল DOM-এ তিনটি পণ্য, তাই তিনটি একই নামের বোতাম, আর action থেমে গেল। দ্বিতীয় লাইন 3 buttons share that name: গোনা বৈধ, কারণ count নিজেই অনেক element নিয়ে কাজ করে। তৃতীয় লাইন clicked masur-1kg: সারি আগে filter, তারপর বোতাম, ঠিক একটি। চতুর্থ লাইন 1 row(s) contain 'চাল'; লক্ষ করুন “ডাল” দিয়ে filter করলেও একটি সারিই মিলত, কিন্তু “চা” দিলে বিপদ হতে পারত, কারণ hasText লেখার অংশ মেলায়। পঞ্চম লাইন waiting: … -> 0 elements (times out later): নকল DOM-এ সাবান নেই। আসল Playwright এখানে সাথে সাথে ফেল করে না, timeout পর্যন্ত অপেক্ষা করে, কারণ element হয়তো একটু পরে আসবে। শেষ লাইন clicked search-btn: test id অনন্য, তাই সোজা।

৪ · ঘুম নয়, অপেক্ষা — auto-waiting আর web-first assertion

Browser-এ সবকিছু asynchronous. ক্লিকের পরে fetch যায়, উত্তর আসে, React আবার আঁকে, CSS animation চলে। কতক্ষণ লাগবে, তা নির্ভর করে কম্পিউটার, network আর সেই মুহূর্তের চাপের ওপর। পুরোনো test-এর সহজ সমাধান ছিল “একটু ঘুমাও”: sleep(500). এতে দুটি সমস্যা একসাথে। ধীর দিনে ৫০০ ms যথেষ্ট নয়, test ফেল করে, যদিও app ঠিক। দ্রুত দিনে ৫০০ ms অপচয়, আর এক হাজার test-এ সেই অপচয় মিনিটে দাঁড়ায়। এমন test, যা কোড না বদলালেও কখনো পাস কখনো ফেল করে, তাকে বলে flaky test.

রেলস্টেশনে ট্রেনের অপেক্ষা ভাবুন। এক যাত্রী ঠিক করলেন, “দশ মিনিট পরে প্ল্যাটফর্মে গিয়ে দেখব, ট্রেন না থাকলে বাড়ি ফিরব।” ট্রেন বারো মিনিট দেরিতে এলে তিনি ভুল সিদ্ধান্ত নেবেন, আর তিন মিনিটে এলে সাত মিনিট বসে থাকবেন। আরেক যাত্রী একটু পরপর প্ল্যাটফর্মে তাকান, ট্রেন এলেই ওঠেন, কিন্তু এক ঘণ্টা পেরোলে স্টেশন মাস্টারকে জানান যে কিছু গোলমাল। দ্বিতীয়জনই Playwright-এর নিয়ম: শর্ত পূরণ পর্যন্ত বারবার দেখা, একটি সর্বোচ্চ সময়সীমাসহ।

Playwright দুই জায়গায় এই নিয়ম মানে। (১) Auto-waiting action: click() চাপার আগে docs-এ লেখা কয়েকটি actionability শর্ত যাচাই করে: element আছে আর ঠিক একটি, দৃশ্যমান, স্থির (animation চলছে না), অন্য কিছু তাকে ঢেকে রাখেনি (event পায়), আর enabled. সব পূরণ না হওয়া পর্যন্ত আবার চেষ্টা, action-এর timeout পর্যন্ত। (২) Web-first assertion: await expect(locator).toHaveText(…), toBeVisible(), toHaveURL(), toHaveCount() শর্ত মিলবে কি না, তা বারবার দেখে, docs অনুযায়ী default ৫ সেকেন্ড পর্যন্ত। বিপরীতে expect(await locator.count()).toBe(1) একবারই মাপে: await দিয়ে আগে সংখ্যাটি নেওয়া হয়, তারপর সাধারণ তুলনা, আর কোনো অপেক্ষা নেই।

নিচের ব্লকে একটি নকল ঘড়িতে দুই পদ্ধতি পাশাপাশি। Search-এর ফল আসে ৬৫০ ms পরে; তার আগে তালিকায় সব ৫টি পণ্য। Polling-এর ব্যবধান [100, 250, 500, 1000] ms নেওয়া হয়েছে, যা docs-এ expect.poll-এর default হিসেবে লেখা; web-first assertion-এর ভেতরের আসল সূচি এর থেকে আলাদা হতে পারে, এটি শুধু মডেল।

// শিক্ষণ মডেল: নির্দিষ্ট ঘুম বনাম শর্ত পূরণ পর্যন্ত বারবার দেখা (simulated clock)
let now = 0;                                  // ms, নকল ঘড়ি
const sleep = (ms) => { now += ms; };
// search-এর ফল আসে ৬৫০ ms পরে; তার আগে তালিকায় ৫টি পণ্য
const rows = () => (now >= 650 ? 1 : 5);

function fixedSleepTest(waitMs) {
  now = 0; sleep(waitMs);
  const n = rows();
  return n === 1 ? `PASS (checked once at ${now} ms)` : `FAIL expected 1, received ${n} at ${now} ms`;
}

function expectToHaveCount(get, want, { timeout = 5000, intervals = [100, 250, 500, 1000] } = {}) {
  now = 0; let polls = 0, i = 0;
  while (true) {
    polls++;
    const got = get();
    if (got === want) return `PASS after ${polls} polls, at ${now} ms`;
    const step = intervals[Math.min(i++, intervals.length - 1)];
    if (now + step > timeout) return `FAIL timeout ${timeout} ms, last value ${got}, ${polls} polls`;
    sleep(step);
  }
}

console.log("sleep 500  :", fixedSleepTest(500));
console.log("sleep 1000 :", fixedSleepTest(1000));
console.log("auto-wait  :", expectToHaveCount(rows, 1));
console.log("never comes:", expectToHaveCount(() => 5, 1));

sleep 500 : FAIL expected 1, received 5 at 500 ms: ঘুম শেষ, ফল তখনো আসেনি, পুরোনো ৫টি সারি গোনা হলো। sleep 1000 : PASS (checked once at 1000 ms): ঘুম বাড়ালে পাস, কিন্তু ৩৫০ ms অপচয়, আর কোনো দিন ফল ১০০০ ms-এর বেশি দেরি করলে আবার ফেল। auto-wait : PASS after 4 polls, at 850 ms: ০, ১০০, ৩৫০ আর ৮৫০ ms-এ দেখা হয়েছে; চতুর্থবারে মিলেছে। শেষ লাইন never comes: FAIL timeout 5000 ms, last value 5, 8 polls: বাগ থাকলে অপেক্ষা অনন্তকাল নয়; সময়সীমা পেরিয়ে স্পষ্ট ব্যর্থতা, শেষ দেখা মানসহ। Playwright-এর আসল বার্তাতেও তাই থাকে: কী আশা ছিল, শেষে কী পাওয়া গেল।

সত্যিকারের flaky test, তারপর স্থির

এবার আসল browser-এ। Demo config-এ বাজারঘরের search service-এর দেরি ইচ্ছা করে এলোমেলো রাখা হয়েছে, ৩০০ থেকে ৭০০ ms-এর মধ্যে, যেমন বাস্তবের ধীর API. দুটি test, একই কাজ:

// খারাপ: নির্দিষ্ট ঘুম, তারপর এক মুহূর্তের মাপ
await page.getByRole('button', { name: 'খুঁজুন' }).click();
await page.waitForTimeout(500);
const n = await page.locator('#list li').count();
expect(n).toBe(1);

// ভালো: ঘুম নেই, assertion নিজেই আবার দেখে
await page.getByRole('button', { name: 'খুঁজুন' }).click();
await expect(page.locator('#list li')).toHaveCount(1);

প্রতিটি দশবার, চারটি worker-এ, সত্যিকারের run (একই রকম লাইনগুলো ছাঁটা, সময় একটি স্থানীয় run-এর):

$ npx playwright test -c demo.config.ts flaky stable --repeat-each=10 --workers=4
Running 20 tests using 4 workers
  ok  2 [edge] › demo\stable.spec.ts:4:1 › stable: ফল ১টি হওয়া পর্যন্ত অপেক্ষা (908ms)
  x   1 [edge] › demo\flaky.spec.ts:4:1 › flaky: ৫০০ ms ঘুমিয়ে ফল গোনা (1.1s)
  x   3 [edge] › demo\flaky.spec.ts:4:1 › flaky: ৫০০ ms ঘুমিয়ে ফল গোনা (1.0s)
  ok  5 [edge] › demo\flaky.spec.ts:4:1 › flaky: ৫০০ ms ঘুমিয়ে ফল গোনা (1.0s)
  …
    Expected: 1
    Received: 5
  …
  6 failed
  14 passed (16.5s)

Flaky test দশবারে ছয়বার ফেল করেছে, প্রতিবার Received: 5: ঘুম শেষে পুরোনো তালিকা গোনা হয়েছে। Stable test দশে দশ। ফলাফল নির্ভর করেছে শুধু সেই মুহূর্তে search কত দ্রুত ছিল, তার ওপর। আপনার কম্পিউটারে এই অনুপাত আলাদা হবে, আর ঠিক এজন্যই flaky test এত বিরক্তিকর: নিজের ল্যাপটপে পাস, CI-র ব্যস্ত server-এ ফেল। waitForTimeout-এর ব্যাপারে Playwright-এর docs নিজেই সতর্ক করে যে এটি debug-এর জন্য, production test-এ নয়।

1await page.getByRole('button', add).click();2// ক্লিক হলো, fetch গেল3await expect(cartLink).toHaveText('কার্ট (1)');
নমুনা সময়রেখা (মডেল)
সময়
বোতাম
actionability
কার্টের লেখা
  1. ০ ms: element নেই। পণ্যের তালিকা তখনো আসছে। click() ফেল করে না; locator ০টি element পেয়ে অপেক্ষা করে আর একটু পরে আবার খোঁজে।
  2. ৪৫০ ms: আছে, কিন্তু disabled. বোতাম দৃশ্যমান, তবে দাম আসার আগে নিষ্ক্রিয়। Actionability-র “enabled” শর্ত পূরণ হয়নি, তাই এখনো ক্লিক নয়।
  3. ৭০০ ms: সব শর্ত পূরণ। দৃশ্যমান, স্থির, ঢাকা নয়, enabled. এই মুহূর্তটি আপনাকে আগে থেকে জানতে হয়নি।
  4. ৭০০ ms: ক্লিক। Playwright আসল mouse event পাঠায়; পাতার JavaScript cart API-তে fetch পাঠায়। click() এখানেই শেষ, উত্তরের অপেক্ষা করে না।
  5. ৭৬০ ms: assertion বারবার দেখে। toHaveText প্রথমবার দেখে লেখা এখনো “কার্ট (0)”. ফেল নয়, আবার চেষ্টা।
  6. ৯৫০ ms: মিলল। Server-এর উত্তর এসে লেখা “কার্ট (1)”. Assertion পাস। পুরো test-এ একটিও waitForTimeout নেই, আর ধীর বা দ্রুত দুই দিনেই একই ফল।

একই ধারণা আপনি নিচের live ব্লকে নিজের চোখে দেখতে পারেন। বোতামটি ৪৫০ ms পরে আসে। একটি নির্দিষ্ট ঘুম ২০০ ms পরে একবার দেখে; একটি ছোট waitFor প্রতি ১০০ ms-এ দেখে, পেলে নিজেই ক্লিক করে, তারপর কার্টের লেখা বদলানো পর্যন্ত আবার অপেক্ষা করে। কোড বদলে সময়গুলো নিয়ে খেলে দেখুন।

<p id="cart">কার্ট (0)</p>
<div id="slot">লোড হচ্ছে…</div>
<script>
  // ৪৫০ ms পরে বোতাম আসে, যেমন ধীর API-র পরে
  setTimeout(() => {
    document.getElementById('slot').innerHTML = '<button id="add">কার্টে যোগ করুন</button>';
    document.getElementById('add').onclick = () =>
      (document.getElementById('cart').textContent = 'কার্ট (1)');
  }, 450);

  const find = () => document.getElementById('add');
  // খারাপ: ২০০ ms ঘুমিয়ে একবার দেখা
  setTimeout(() => console.log('sleep 200 ms → button?', find() ? 'yes' : 'no'), 200);

  // ভালো: শর্ত পূরণ না হওয়া পর্যন্ত বারবার দেখা, সর্বোচ্চ ২০০০ ms
  function waitFor(cond, timeout = 2000, every = 100) {
    return new Promise((ok, fail) => {
      const start = performance.now();
      const tick = () => cond() ? ok(Math.round((performance.now() - start) / 100) * 100)
        : performance.now() - start > timeout ? fail(new Error('timeout'))
        : setTimeout(tick, every);
      tick();
    });
  }
  waitFor(find).then((ms) => {
    console.log('waitFor → button visible after about', ms, 'ms');
    find().click();
    return waitFor(() => document.getElementById('cart').textContent === 'কার্ট (1)');
  }).then(() => console.log('cart text is now:', document.getElementById('cart').textContent));
</script>

প্রথম লাইন sleep 200 ms → button? no: ঘুম ভাঙার সময় বোতাম আসেনি। একটি পুরোনো ধাঁচের test এখানেই ফেল করত। দ্বিতীয় লাইন waitFor → button visible after about 500 ms: প্রতি ১০০ ms-এ দেখা হয়েছে, বোতাম আসে ৪৫০-এ, তাই পরের দেখায়, প্রায় ৫০০ ms-এ পাওয়া গেল (ধীর কম্পিউটারে timer একটু পিছিয়ে এটি ৬০০ দেখাতে পারে)। তারপর ক্লিক, আর তৃতীয় লাইন cart text is now: কার্ট (1): দ্বিতীয় অপেক্ষা লেখা বদলানো পর্যন্ত থেমেছে। এই ২৮ লাইনই Playwright-এর মূল ধারণার একটি খুব ছোট রূপ; আসল টুল এর সাথে actionability-র সব শর্ত, স্পষ্ট error আর trace যোগ করে।

৫ · বাজারঘরের পুরো কেনাকাটা — একটি journey test আর API দিয়ে test data

এখন সবচেয়ে মূল্যবান test-টি: একজন নতুন ক্রেতা পণ্য দেখেন, “ডাল” খোঁজেন, দুই প্যাকেট মসুর ডাল কার্টে রাখেন, চেকআউটে গিয়ে দেখেন লগইন লাগবে, লগইন করেন, অর্ডার দেন, নকল gateway-তে টাকা দেন, আর অর্ডার নিশ্চিত হওয়ার পাতা দেখেন। এমন test-কে অনেকে বলেন “journey” বা “happy path”. দোকান চলার জন্য এটি সবচেয়ে জরুরি পথ, তাই প্রতিটি পরিবর্তনের পরে এটি চলা উচিত।

import { test, expect } from './fixtures';
test.use({ storageState: { cookies: [], origins: [] } });   // লগইন ছাড়া শুরু

test('ক্রেতা খুঁজে, কার্টে রেখে, লগইন করে অর্ডার দেন', async ({ page, shopper }) => {
  await page.goto('/');
  await expect(page.getByRole('heading', { name: 'আজকের বাজার' })).toBeVisible();

  await page.getByLabel('পণ্য খুঁজুন').fill('ডাল');
  await page.getByRole('button', { name: 'খুঁজুন' }).click();
  await expect(page.getByRole('status')).toHaveText('1টি পণ্য পাওয়া গেছে');

  const dal = page.getByRole('listitem').filter({ hasText: 'মসুর ডাল' });
  await dal.getByRole('button', { name: 'কার্টে যোগ করুন' }).click();
  await dal.getByRole('button', { name: 'কার্টে যোগ করুন' }).click();
  await expect(page.getByRole('link', { name: /কার্ট/ })).toHaveText('কার্ট (2)');

  await page.getByRole('link', { name: /কার্ট/ }).click();
  await expect(page.locator('#total')).toHaveText('৳269.80');
  await page.getByRole('link', { name: 'চেকআউট করুন' }).click();

  await expect(page).toHaveURL(/\/login\?next=/);
  await page.getByLabel('ইমেইল').fill(shopper.email);
  await page.getByLabel('পাসওয়ার্ড').fill(shopper.password);
  await page.getByRole('button', { name: 'লগইন করুন' }).click();

  await expect(page).toHaveURL('/checkout');
  await page.getByRole('button', { name: 'অর্ডার দিন ও পেমেন্ট করুন' }).click();
  await expect(page.getByRole('heading', { name: /^Pay 269\.80 BDT to BG-\d+$/ })).toBeVisible();
  await page.getByRole('button', { name: 'Pay (test)' }).click();

  await expect(page).toHaveURL(/\/orders\/BG-\d+$/);
  await expect(page.getByRole('heading', { level: 1 })).toHaveText('অর্ডার নিশ্চিত হয়েছে');
  await expect(page.getByTestId('order-ref')).toHaveText(/^BG-\d+$/);
});

কয়েকটি সিদ্ধান্ত খেয়াল করুন। দুবার ক্লিকের পরে assertion. প্রতিটি ক্লিক একটি fetch পাঠায়; toHaveText('কার্ট (2)') দুটি উত্তরই আসা পর্যন্ত অপেক্ষা করে। দামের হিসাব test-এ লেখা: ২ × ১৩৪.৯০ = ২৬৯.৮০ টাকা, পয়সায় 26980. দাম যদি কোনো দিন browser থেকে নেওয়া শুরু হয়, এই লাইন ধরবে। Order নম্বর regex-এ, কারণ parallel run-এ কোন test কোন নম্বর পাবে, তা নির্দিষ্ট নয়। Payment নকল. আসল gateway-তে E2E চালানো মানে আসল account, নিয়ম আর রেট-লিমিট; test-এ সবসময় sandbox বা নিজের stub. Stub-এর একটি সীমা আছে: আসল gateway-র পাতা বদলালে এই test তা জানবে না। সেই ফাঁক ঢাকে gateway-র sandbox-এর বিরুদ্ধে মাঝে মাঝে চালানো আলাদা ছোট test আর পর্ব ৬৫-এর reconciliation.

Fixture: প্রতিটি test নিজের ক্রেতা পায়

Journey test-এর shopper কোথা থেকে এলো? এটি একটি Playwright fixture: test শুরুর আগে কিছু তৈরি করে test-কে দেয়, আর দরকার হলে শেষে পরিষ্কার করে। page, request, context নিজেরাও fixture. নিজেরটি বানাতে test.extend:

import { test as base, expect } from '@playwright/test';
type Shopper = { email: string; password: string };

// প্রতিটি test নিজের নতুন ক্রেতা পায়, API দিয়ে তৈরি, UI দিয়ে নয়
export const test = base.extend<{ shopper: Shopper }>({
  shopper: async ({ request }, use, testInfo) => {
    const email = `buyer-w${testInfo.workerIndex}-r${testInfo.repeatEachIndex}-${testInfo.testId.slice(0, 6)}@example.test`;
    const password = 'example-pass-2';
    const res = await request.post('/api/test/users', { data: { email, password } });
    expect(res.status()).toBe(201);
    await use({ email, password });            // এখানে test চলে
  },
});
export { expect };

এখানে দুটি নীতি। প্রথমত, যা পরীক্ষা করছেন না, তা UI দিয়ে বানাবেন না। Journey-র লক্ষ্য কেনাকাটা, signup নয়; signup-এর পাতা দিয়ে প্রতিবার account বানালে test ধীর হয়, আর signup-এর একটি ছোট বাগ সব test ভেঙে দেয়। request fixture সরাসরি HTTP পাঠায়, browser ছাড়া, কয়েক মিলিসেকেন্ডে। দ্বিতীয়ত, প্রতিটি test নিজের ডেটা। Email-এ worker আর test-এর id থাকায় parallel test একে অন্যের ক্রেতা ছোঁয় না। এই API আর test database-এর নকশা পর্ব ৭৫-এর (API integration test) বিষয়, আর বড় পরিসরের test data, factory আর fixture ডেটার কৌশল পর্ব ৭৭-এর (Mock, MSW আর test data). Test double-এর তত্ত্ব আছে Software Engineering-এর test doubles পর্বে।

৬ · লগইন একবার — storageState আর setup project

বাজারঘরের বেশির ভাগ পাতা লগইনের পেছনে: চেকআউট, অর্ডারের ইতিহাস, ঠিকানা। প্রতিটি test যদি লগইনের পাতা খুলে ইমেইল আর পাসওয়ার্ড লিখে শুরু করে, তাহলে লগইনের পথ শতবার পরীক্ষা হয়, অথচ একবারই যথেষ্ট, আর সব test ধীর হয়। সমাধান: একবার লগইন করে browser-এর অবস্থা ফাইলে রাখা, পরের test সেখান থেকে শুরু। Playwright একে বলে storageState: context-এর সব cookie আর প্রতিটি origin-এর localStorage, একটি JSON ফাইলে।

import { test as setup, expect } from '@playwright/test';
const authFile = 'playwright/.auth/user.json';

setup('একবার লগইন করে state রাখা', async ({ page, request }) => {
  await request.post('/api/test/reset');
  await request.post('/api/test/users', { data: { email: 'rina@example.test', password: 'example-pass-1' } });

  await page.goto('/login');
  await page.getByLabel('ইমেইল').fill('rina@example.test');
  await page.getByLabel('পাসওয়ার্ড').fill('example-pass-1');
  await page.getByRole('button', { name: 'লগইন করুন' }).click();
  await expect(page.getByRole('navigation', { name: 'প্রধান' })).toContainText('rina@example.test');

  await page.context().storageState({ path: authFile });
});

Assertion-টি জরুরি। লগইন বোতাম চাপার পরেই state লিখলে redirect আর cookie বসার আগেই ফাইল লেখা হতে পারে, আর পরের সব test রহস্যময়ভাবে লগইন ছাড়া শুরু হবে। তাই আগে নিশ্চিত হওয়া যে পাতায় ক্রেতার নাম দেখা যাচ্ছে। Config-এ edge project-এর dependencies: ['setup'] আর storageState: 'playwright/.auth/user.json', যা আগেই দেখেছেন। Playwright-এর docs এই “setup project” পদ্ধতিই সুপারিশ করে; পুরোনো globalSetup-ও চলে, কিন্তু setup project-এ trace, report আর fixture সব পাওয়া যায়। ল্যাবের তৈরি ফাইল, cookie-র মান ছোট করে:

{
  "cookies": [
    { "name": "sid", "value": "9b88c657…bb91f3", "domain": "127.0.0.1", "path": "/",
      "expires": -1, "httpOnly": true, "secure": false, "sameSite": "Lax" }
  ],
  "origins": []
}

এটি আসলে একটি চাবি: যার কাছে এই ফাইল, তিনি ওই session-এ লগইন করা। তাই playwright/.auth folder .gitignore-এ রাখুন, আর শুধু test-এর জন্য বানানো account-এ ব্যবহার করুন। secure: false কারণ ল্যাব http://127.0.0.1-এ চলে; production-এ session cookie অবশ্যই Secure (পর্ব ৪৫, Cybersecurity-র sessions পর্ব)। আরেকটি কথা: login-এর পরে server নতুন sid দেয়, কারণ লগইনের সময় session id বদলানো session fixation ঠেকায়। Setup ঠিক সেই নতুন cookie-টি ধরেছে, কারণ assertion-এর পরে state লেখা হয়েছে।

  1. ResetAPIক্রেতা তৈরি
  2. UI লগইনsidনতুন id
  3. user.jsoncookiesফাইলে
  4. নতুন contextsid সহপ্রতি test
  5. /checkout200সরাসরি
setup projectbrowser ছাড়া HTTP
POST /api/test/reset   → 204
POST /api/test/users   → 201
{ email: "rina@example.test" }
লগইনের উত্তর303
POST /login
Set-Cookie: sid=9b88…f3; HttpOnly; SameSite=Lax
Location: /
পাতায়: rina@example.test ✓
playwright/.auth/user.json.gitignore-এ
cookies: [ { name: "sid", value: "9b88…f3",
             httpOnly: true, sameSite: "Lax" } ]
origins: []
account.spec.ts-এর contextedge project
storageState: 'playwright/.auth/user.json'
context শুরুতেই cookie পায়:
sid=9b88…f3
লগইনের পাতা খোলা হয় না
test-এর ফলok 612ms
goto('/checkout')      → URL থাকে /checkout
nav: "rina@example.test" ✓
link 'লগইন' সংখ্যা = 0 ✓
  1. ডেটা API দিয়ে। Setup project ডেটা পরিষ্কার করে একজন ক্রেতা বানায়, browser ছাড়াই, সরাসরি test-only API-তে।
  2. একবার UI দিয়ে লগইন। লগইনের পথটি অন্তত একবার আসল পাতায় পরীক্ষা হলো। Server নতুন sid cookie দেয়, আর পাতায় ক্রেতার ইমেইল দেখা যায়।
  3. State ফাইলে। Assertion পাসের পরে storageState cookie-টি JSON-এ লেখে। এই ফাইল একটি চাবি, তাই repository-তে নয়।
  4. প্রতিটি test নতুন context, একই চাবি। Context আলাদা, তাই test-গুলো একে অন্যের পাতা দেখে না; কিন্তু সবাই শুরুতেই একই cookie পায়।
  5. সরাসরি কাজের পাতায়। /checkout খুললে server sid চেনে, লগইনে পাঠায় না। Test ৬১২ ms-এ শেষ, লগইনের ধাপ ছাড়াই।

একই চাবি মানে একই server-এর session. Context আলাদা হলেও cookie একই, আর বাজারঘরের cart থাকে server-এর session-এ। দুটি parallel test যদি এই state নিয়ে একই সাথে কার্টে জিনিস রাখে আর গোনে, তারা একে অন্যের সংখ্যা বদলে দেবে। ল্যাবে তাই লগইন করা test শুধু পড়ার কাজ করে, আর কার্ট বদলানো journey নিজের নতুন ক্রেতা নেয়। যে test server-এর অবস্থা বদলায়, তার জন্য docs-এর পরামর্শ হলো প্রতি worker-এ আলাদা account (testInfo.parallelIndex দিয়ে), worker-স্তরের fixture-এ একবার লগইন করে।

৭ · ব্যর্থতার পথ — page.route দিয়ে network বদলানো

Happy path সহজে পরীক্ষা হয়। কঠিন হলো খারাপ দিন: search service 500 দিল, network কেটে গেল, gateway ধীর। আসল server-কে ইচ্ছামতো ভাঙানো ঝামেলার, আর প্রতিটি test-এর জন্য আলাদা ভাঙা সম্ভবও নয়। Playwright-এ network interception আছে: page.route(pattern, handler) browser থেকে বের হওয়া মেলানো অনুরোধ মাঝপথে ধরে, আর আপনি ঠিক করেন কী হবে: নিজের উত্তর দেওয়া (route.fulfill), অনুরোধ কেটে দেওয়া (route.abort), বা বদলে সামনে পাঠানো (route.continue).

test('খোঁজার API 500 দিলে ক্রেতা স্পষ্ট বার্তা পান', async ({ page }) => {
  await page.route('**/api/products?*', (route) =>
    route.fulfill({ status: 500, contentType: 'application/json', body: '{"error":"boom"}' }));
  await page.goto('/');
  await page.getByLabel('পণ্য খুঁজুন').fill('ইলিশ');
  await page.getByRole('button', { name: 'খুঁজুন' }).click();
  await expect(page.getByRole('alert')).toHaveText('খুঁজতে সমস্যা হয়েছে, একটু পরে আবার চেষ্টা করুন');
});

Test পাস করেছে (প্রথম run-এর তালিকায় ok 6 … (755ms)). পাতার কোড r.ok না হলে একটি role="alert" বার্তা দেখায়; route না থাকলে এই পথ test-এ কখনো আসত না। Route শুধু ওই page-এর জন্য, তাই অন্য test-এ প্রভাব নেই। একই কৌশলে আরও কিছু: route.abort('internetdisconnected') দিয়ে network কাটা, fulfill-এর আগে দেরি দিয়ে ধীর API, বা page.routeFromHAR দিয়ে আগে রেকর্ড করা উত্তর চালানো।

সীমাটাও মনে রাখুন। page.route শুধু browser থেকে যাওয়া অনুরোধ ধরে। বাজারঘরের server নিজে যদি gateway-কে ডাকে, সেই server-to-server অনুরোধ browser দেখে না, তাই এখানে ধরা যায় না। সেটি বদলাতে লাগে server-এর দিকের ব্যবস্থা: নকল gateway, বা পর্ব ৭৭-এর MSW-এর মতো টুল, যা Node-এর ভেতরে অনুরোধ ধরে। Payment ব্যর্থতার পথ ল্যাবে পরীক্ষা হয় অন্যভাবে: stub পাতার “Fail (test)” বোতাম চাপলে order-এর পাতায় “পেমেন্ট সম্পন্ন হয়নি” আসে।

৮ · ব্যর্থতা পড়া — trace, --ui, screenshot, retry আর parallel

E2E test ফেল করলে প্রশ্ন ওঠে, “ঠিক কোন মুহূর্তে পাতা কেমন ছিল?” CI-র একটি লাল লাইন থেকে তা বোঝা কঠিন। Playwright এর জন্য কয়েকটি যন্ত্র দেয়।

  • Screenshot: screenshot: 'only-on-failure' থাকলে ব্যর্থ test-এর শেষ মুহূর্তের ছবি test-results-এ থাকে। ল্যাবের প্রথম run-এর ব্যর্থতায় test-failed-1.png তৈরি হয়েছিল, সাথে error-context.md, যেখানে পাতার accessibility tree-র একটি লেখা রূপ থাকে।
  • Video: video: 'retain-on-failure' দিলে প্রতিটি test রেকর্ড হয়, পাস করলে মুছে যায়। ল্যাবে চালু করা হয়নি; ভিডিও জায়গা নেয় আর trace প্রায় সব প্রশ্নের উত্তর দেয়।
  • Trace: একটি zip ফাইল, যাতে প্রতিটি action, তার আগে-পরে DOM-এর snapshot, network, console আর source থাকে। npx playwright show-trace trace.zip এটি একটি GUI-তে খোলে।
  • UI mode: npx playwright test --ui একটি জানালায় সব test দেখায়, একটি চালাতে দেয়, প্রতিটি ধাপে পাতার ছবি দেখায়, আর ফাইল বদলালে আবার চালায় (watch). নিজের কম্পিউটারে test লেখার সময় এটি সবচেয়ে আরামের; ল্যাবে GUI খোলা হয়নি, তাই এর ছবি এখানে নেই।

Flaky test-টি --trace on দিয়ে আবার চালানো হলো, ফেল করল, আর Playwright 1.64-এ থাকা npx playwright trace কমান্ড দিয়ে GUI ছাড়াই trace পড়া হলো। সত্যিকারের run, fixture-এর কিছু লাইন ছাঁটা:

$ npx playwright trace open flaky-trace.zip
  Playwright:   1.64.1
  Title:        flaky.spec.ts:4 › flaky: ৫০০ ms ঘুমিয়ে ফল গোনা
  Duration:     1.3s
  Actions:      29
$ npx playwright trace actions
     # Time       Action                        Duration
    8. 0:00.268  Navigate                         30ms
                 /
    9. 0:00.300  Fill "চাল"                       44ms
                 getByLabel('পণ্য খুঁজুন')
   10. 0:00.345  Click                           167ms
                 getByRole('button', { name: 'খুঁজুন' })
   11. 0:00.512  Wait for timeout                518ms
   12. 0:01.031  Query count                       4ms
                 locator('#list li')
   13. 0:01.037  Expect "toBe"                    1ms  ✗
$ npx playwright trace requests
     # Start     Method   Status   Name                                     Duration
    1. 0:00.276  GET      200      127.0.0.1:47762                               2ms
    2. 0:00.508  GET      ERR      products?q=%E0%A6%9A%E0%A6%BE%E0%A6%B2          -
$ npx playwright trace errors
  ✗ Error
    at flaky.spec.ts:10:13
    Error: expect(received).toBe(expected) // Object.is equality
    Expected: 1
    Received: 5

Trace পুরো গল্পটি বলে দিচ্ছে। ক্লিকের পরে ০.৫০৮ সেকেন্ডে search-এর অনুরোধ গেছে (q=%E0%A6%9A… হলো URL-encode করা “চাল”)। ০.৫১২ থেকে ৫১৮ ms ঘুম। ১.০৩১-এ গোনা হলো, ফল ৫। Search-এর অনুরোধের অবস্থা ERR, সময় নেই: test শেষ হয়ে context বন্ধ হওয়া পর্যন্ত উত্তর আসেইনি, তাই অনুরোধটি অসমাপ্ত অবস্থায় কেটে গেছে। অর্থাৎ বাগ app-এ নয়, test-এর অপেক্ষায়। GUI-তে একই তথ্য একটি timeline-এ দেখায়, প্রতিটি action-এর ওপর চাপলে তখনকার DOM-এর snapshot খোলে, যেখানে পাঁচটি পুরোনো পণ্য স্পষ্ট দেখা যেত।

Retry: সবুজ দেখায়, কিন্তু সারায় না

Retry চালু থাকলে ফেল করা test আবার চলে; পরের চেষ্টায় পাস করলে Playwright তাকে “flaky” বলে চিহ্ন দেয়, আর পুরো run সফল ধরা হয়। একই flaky test, পাঁচবার, --retries=2 দিয়ে, সত্যিকারের run:

$ npx playwright test -c demo.config.ts flaky --repeat-each=5 --retries=2 --workers=1
Running 5 tests using 1 worker
  ok 1 [edge] › demo\flaky.spec.ts:4:1 › flaky: ৫০০ ms ঘুমিয়ে ফল গোনা (915ms)
  x  2 [edge] › demo\flaky.spec.ts:4:1 › flaky: ৫০০ ms ঘুমিয়ে ফল গোনা (898ms)
  x  3 [edge] › demo\flaky.spec.ts:4:1 › flaky: ৫০০ ms ঘুমিয়ে ফল গোনা (retry #1) (904ms)
  ok 4 [edge] › demo\flaky.spec.ts:4:1 › flaky: ৫০০ ms ঘুমিয়ে ফল গোনা (retry #2) (888ms)
  …
    test-results\flaky-…-edge-retry1-repeat1\trace.zip
  1 flaky
  4 passed (11.7s)

দ্বিতীয় চেষ্টা দুবার ফেল করে তৃতীয়বারে পাস। ফল: 1 flaky, 4 passed, আর exit code শূন্য, অর্থাৎ CI সবুজ। trace: 'on-first-retry' থাকায় প্রথম retry-র trace রাখা হয়েছে, যা ঠিক কাজের জিনিস। কিন্তু বিপদটাও স্পষ্ট: retry ছাড়া এই test-এর ব্যর্থতা দলের চোখে পড়ত, retry দিয়ে তা লুকিয়ে গেল। আর flaky test সবসময় test-এর দোষ নয়; কখনো এটি app-এর আসল race condition, যা ক্রেতাও মাঝে মাঝে দেখেন। ভালো অভ্যাস: retry রাখলেও “flaky” সংখ্যাটি report-এ আলাদা করে দেখা, প্রতিটি নতুন flaky test-কে বাগ হিসেবে খোলা, আর প্রয়োজনে সাময়িক quarantine, স্থায়ী নয়। Flaky test কমানোর আরও কৌশল পর্ব ৭৭-এ।

Parallel আর sharding

Playwright Test নিজে থেকে একাধিক worker process-এ test চালায়; প্রতিটি worker-এর নিজের browser. fullyParallel: true থাকলে একই ফাইলের test-ও ভাগ হয়। ল্যাবে একই ৮টি test ৬টি worker-এ ৫.০ সেকেন্ডে, আর --workers=1 দিয়ে ৭.৩ সেকেন্ডে শেষ হয়েছে (একটি স্থানীয় run; browser চালু আর server-এর সময় দুই ক্ষেত্রেই আছে, তাই অনুপাত ছোট)। Parallel-এর শর্ত: test-গুলো স্বাধীন, কেউ কারও ডেটা বা ক্রমের ওপর নির্ভর করে না। ৫ নম্বর অংশের প্রতি-test ক্রেতা এজন্যই।

একটি কম্পিউটারে না কুলালে sharding: --shard=1/2 আর --shard=2/2 দুটি আলাদা মেশিনে চালালে প্রতিটি অর্ধেক test পায়। ল্যাবে --list দিয়ে শুধু ভাগটা দেখা হয়েছে: প্রথম ভাগে a11y, account, journey আর একটি search; দ্বিতীয় ভাগে বাকি search, visual আর mobile. লক্ষ করার মতো, setup দুই ভাগেই আছে, কারণ নির্ভরশীল project যেখানে চলে, তার setup-ও সেখানে চলতে হয়। ভাগগুলোর report জোড়া দেওয়া আর CI-তে চালানোর কাজ পর্ব ৮১-এ (CI/CD).

৯ · চোখ আর সবার জন্য — screenshot তুলনা, axe আর ৩৬০ px

Visual comparison: toHaveScreenshot

কিছু বাগ শুধু চোখে ধরা পড়ে: একটি CSS পরিবর্তনে দামের লেখা কার্ডের বাইরে গেল, বোতাম অন্য বোতামের ওপর বসল। toHaveScreenshot একটি পাতা বা element-এর ছবি তুলে আগের অনুমোদিত ছবির (baseline) সাথে pixel ধরে মেলায়; একে বলে visual regression test. ল্যাবে ইলিশের কার্ডটি:

test('পণ্যের প্রথম কার্ড দেখতে আগের মতো', async ({ page }) => {
  await page.goto('/');
  const first = page.getByRole('listitem').filter({ hasText: 'ইলিশ' });
  await expect(first).toHaveScreenshot('ilish-card.png');
});

প্রথম run-এ baseline ছিল না, তাই test ইচ্ছা করে ফেল করে আর ছবিটি লিখে রাখে। লক্ষ করুন ফাইলের নাম: ilish-card-edge-win32.png. Playwright নামে project আর operating system জুড়ে দেয়, কারণ একই পাতা Windows আর Linux-এ আলাদা pixel দেয়।

Error: A snapshot doesn't exist at ~/bazarghar/tests/visual.spec.ts-snapshots/ilish-card-edge-win32.png, writing actual.

দ্বিতীয় run-এ পাস। তৃতীয় run-এ শুধু একটি জিনিস বদলানো হলো: পাতার বাংলা font, Windows-এর “Nirmala UI” থেকে “Vrinda”-তে। লেখা, দাম, বোতাম, সব একই। সত্যিকারের run:

$ SHOP_FONT=Vrinda npx playwright test visual
  x  2 [edge] › tests\visual.spec.ts:5:1 › পণ্যের প্রথম কার্ড দেখতে আগের মতো (798ms)
    Error: expect(locator).toHaveScreenshot(expected) failed
    Locator: getByRole('listitem').filter({ hasText: 'ইলিশ' })
      Expected an image 246px by 160px, received 246px by 165px. 2114 pixels (ratio 0.06 of all image pixels) are different.
    …
    Expected: tests\visual.spec.ts-snapshots\ilish-card-edge-win32.png
    Received: test-results\visual-…-edge\ilish-card-actual.png
    Diff:     test-results\visual-…-edge\ilish-card-diff.png

শুধু font বদলে কার্ড ৫ pixel লম্বা হয়েছে, আর ২১১৪টি pixel আলাদা। ক্রেতার কাছে এটি হয়তো কোনো বাগই নয়। বাংলা লেখা এখানে বিশেষভাবে ভঙ্গুর: যুক্তাক্ষর, কার, রেফ আর হসন্ত font আর text shaping engine মিলে আঁকে, আর বিভিন্ন font-এর উচ্চতা ও রেখা অনেক আলাদা। Windows-এ Nirmala UI, Linux CI-তে হয়তো Noto Sans Bengali বা কিছুই না, তখন fallback font. তাই ক্রেতার কম্পিউটারে ঠিক থাকলেও আপনার CI-তে ছবি মিলবে না। Font আর বাংলা typography-র আলোচনা আছে পর্ব ৫৯-এ।

ব্যবহারিক নিয়ম: (১) baseline একই পরিবেশে বানান যেখানে test চলবে, সাধারণত CI-র একটি নির্দিষ্ট container image-এ; (২) web font নিজে সাইটের সাথে দিন, system font-এর ওপর ভরসা নয়; (৩) পুরো পাতা নয়, স্থির ছোট অংশের ছবি নিন, আর বদলানো অংশ (তারিখ, order নম্বর) mask দিয়ে ঢাকুন; (৪) maxDiffPixelRatio-এর মতো সহনশীলতা দিতে পারেন, কিন্তু বেশি দিলে আসল বাগও পার পাবে; (৫) ইচ্ছাকৃত পরিবর্তনের পরে npx playwright test --update-snapshots দিয়ে নতুন baseline, আর diff-এ ছবিটি মানুষ দেখে অনুমোদন করবেন।

Accessibility: axe একটি আসল বাগ ধরল

Locator-এ getByRole ব্যবহার করলে অনেক accessibility সমস্যা নিজে থেকেই ধরা পড়ে, কিন্তু সব নয়। @axe-core/playwright (লেখার সময় 4.14.0) Deque-এর open-source axe-core engine পাতায় চালিয়ে নিয়মভিত্তিক সমস্যা খোঁজে: নামহীন বোতাম, লেবেলহীন input, কম contrast, ভুল ARIA. Tag দিয়ে WCAG-এর কোন স্তর দেখবে, তা বলা যায়।

import AxeBuilder from '@axe-core/playwright';
// … ইলিশ কার্টে রেখে /cart খোলা
const results = await new AxeBuilder({ page })
  .withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'])
  .analyze();
expect(results.violations.map((v) => `${v.id}: ${v.nodes.length} node(s)`)).toEqual([]);

প্রথম run-এ এটি ফেল করেছিল। Test-এ ছাপানো বিস্তারিতসহ সত্যিকারের আউটপুট:

button-name (critical) - Buttons must have discernible text
  .rm
  Fix any of the following:
  Element does not have inner text that is visible to screen readers
  aria-label attribute does not exist or is empty
  aria-labelledby attribute does not exist, references elements that do not exist or references elements that are empty
  Element has no title attribute
  …
    - Array []
    + Array [
    +   "button-name: 1 node(s)",
    + ]

কার্টের প্রতিটি সারির পাশে একটি লাল ✕ icon-এর বোতাম, পণ্য মুছে ফেলার জন্য। চোখে দেখলে বোঝা যায়, কিন্তু বোতামের ভেতরে শুধু একটি <svg aria-hidden="true">, কোনো লেখা নেই। স্ক্রিন রিডার শুধু বলবে “button”, কোন পণ্যের, কী কাজের, জানা যাবে না। সমাধান একটি attribute:

<!-- আগে -->
<button class="rm">${ICON}</button>
<!-- পরে -->
<button class="rm" aria-label="মুছুন: ${esc(l.p.name)}">${ICON}</button>

এরপর ok 8 [edge] › tests\a11y.spec.ts … (1.2s). এখন একটি test লেখা যায় getByRole('button', { name: 'মুছুন: ইলিশ (১ কেজি)' }) দিয়ে, যা আগে সম্ভব ছিল না। সীমাও জানুন: axe-এর মতো স্বয়ংক্রিয় যাচাই সব accessibility সমস্যা ধরতে পারে না; keyboard দিয়ে পুরো পথ চলা, focus-এর ক্রম, লেখার অর্থবোধকতা মানুষকেই দেখতে হয় (HTML ও CSS-এর accessibility পর্ব)।

মোবাইল: ৩৬০ px-এর project

বাংলাদেশে অনেক ক্রেতা ছোট পর্দার Android ফোনে কেনাকাটা করেন, আর ৩৬০ CSS pixel চওড়া viewport খুব পরিচিত মাপ। mobile-360 project একই Edge-এ viewport ৩৬০ × ৮০০, isMobile: true (meta viewport মানা হয়) আর hasTouch: true দেয়। এটি আসল ফোন নয়, Android-এর browser engine-ও নয়; কিন্তু responsive layout আর touch-এর বেশির ভাগ বাগ এখানেই ধরা পড়ে। চাইলে devices['Pixel 7']-এর মতো তৈরি বর্ণনাও আছে।

test('৩৬০ px: মেনু থেকে কার্টে যাওয়া, পাশে scroll নেই', async ({ page }) => {
  await page.goto('/');
  const nav = page.getByRole('navigation', { name: 'প্রধান' });
  await expect(nav).toBeHidden();                       // ছোট পর্দায় মেনু লুকানো
  await page.getByRole('button', { name: 'মেনু' }).tap();
  await expect(page.getByRole('button', { name: 'মেনু' })).toHaveAttribute('aria-expanded', 'true');
  await nav.getByRole('link', { name: /কার্ট/ }).tap();
  await expect(page).toHaveURL('/cart');
  const sideways = await page.evaluate(() => document.documentElement.scrollWidth - window.innerWidth);
  expect(sideways).toBeLessThanOrEqual(0);              // পাশে scroll নেই
});

Test পাস করেছে (ok 2 [mobile-360] … (626ms)). শেষ লাইনটি সহজ কিন্তু কাজের: পাতা পর্দার চেয়ে চওড়া হলে ফোনে পাশে টানতে হয়, যা লম্বা URL বা চওড়া টেবিলে প্রায়ই ঘটে। Desktop-এ এই test চালালে প্রথম assertion-ই ফেল করত, কারণ সেখানে মেনু সবসময় দেখা যায়; তাই config-এ testMatch দিয়ে এটিকে শুধু মোবাইল project-এ রাখা হয়েছে। Responsive নকশার তত্ত্ব HTML ও CSS-এর responsive design পর্বে।

১০ · কতগুলো E2E, codegen আর AI-এর যুগে

প্রতিটি E2E test এক থেকে কয়েক সেকেন্ড নেয়, unit test নেয় মিলিসেকেন্ড। প্রতিটির ব্যর্থতার কারণ অনেক জায়গায় হতে পারে, তাই খুঁজতে সময় লাগে। আর এরা flaky হওয়ার ঝুঁকি বেশি। তাই পর্ব ৭২-এর কৌশল মেনে সাধারণ পরামর্শ: কম কিন্তু জরুরি। যে পথ ভাঙলে ব্যবসা থেমে যায় (খোঁজা, কার্ট, লগইন, চেকআউট, payment-এর ফল), আর যে পথ শুধু সব স্তর একসাথে জোড়া লাগলেই দেখা যায় (cookie, redirect, CSS-এ লুকানো বোতাম)। Validation-এর প্রতিটি ভুল বার্তা, দামের প্রতিটি হিসাব, প্রতিটি role-এর অনুমতি, এসব নিচের স্তরে: unit, component আর API test-এ (পর্ব ৪৮-এর role × route ছক যেমন Vitest আর supertest-এ)। কত সংখ্যা “ঠিক”, তার কোনো সর্বজনীন উত্তর নেই; দলগুলোর মতও আলাদা, কেউ trophy মডেলে integration-এ জোর দেন, কেউ pyramid-এ unit-এ। একটি ব্যবহারিক মাপকাঠি: E2E suite এত ছোট রাখুন যে প্রতিটি pull request-এ কয়েক মিনিটে চলে, আর ফেল করলে মানুষ তা বিশ্বাস করে।

Codegen: শুরু করার সহায়ক, শেষ কথা নয়

npx playwright codegen http://127.0.0.1:47761 একটি browser আর একটি জানালা খোলে; আপনি ক্লিক আর টাইপ করেন, আর Playwright সেই কাজগুলো কোডে লেখে, সাধারণত role আর label-ভিত্তিক locator বেছে নিয়ে। এটি GUI-নির্ভর, তাই ল্যাবে চালানো হয়নি, আর এখানে এর কোনো আউটপুট দেখানো হচ্ছে না। Codegen-এর কোড একটি খসড়া: এটি জানে না আপনি কী যাচাই করতে চান, তাই assertion সাধারণত কম বা শুধু দৃশ্যমানতার; test data UI দিয়েই বানায়; কখনো .nth() বা দীর্ঘ লেখা-ভিত্তিক locator দেয়। ব্যবহার করুন locator খুঁজে পেতে, তারপর নিজের হাতে fixture, assertion আর নাম ঠিক করুন।

আরও গভীরে: Playwright Chromium আর Edge চালায় Chrome DevTools Protocol দিয়ে; Firefox আর WebKit-এর জন্য দলটি নিজস্ব patch করা build দেয়, তাই npx playwright install আলাদা browser নামায়, আর channel দিয়ে ইনস্টল করা Chrome বা Edge চালানো যায়। W3C-তে WebDriver BiDi নামে একটি নতুন দুই-দিকের protocol-এর কাজ চলছে, যাতে বিভিন্ন browser একই মানে automation সমর্থন করে। Accessible name কীভাবে হিসাব হয়, তার নিয়ম W3C-র “Accessible Name and Description Computation” মানে; getByRole এই ধরনের নিয়মের ওপর দাঁড়ানো। Flaky test নিয়ে বড় প্রতিষ্ঠানের engineering blog আর গবেষণা-পত্রে অনেক আলোচনা আছে; সাধারণ শিক্ষা হলো, বড় suite-এ flakiness একটি পরিমাপযোগ্য খরচ, যা মাপা আর কমানো দরকার।

পরীক্ষা ও ইন্টারভিউতে: (১) E2E আর integration test-এর পার্থক্য? E2E আসল browser-এ পুরো system ব্যবহারকারীর মতো চালায়; integration কয়েকটি অংশ একসাথে, সাধারণত UI ছাড়া। (২) waitForTimeout কেন খারাপ? ধীর দিনে ফেল, দ্রুত দিনে অপচয়; web-first assertion শর্ত পূরণ পর্যন্ত অপেক্ষা করে। (৩) Strict mode violation কী? Action-এর locator একাধিক element পেলে Playwright থেমে যায়; filter বা chaining দিয়ে একটিতে নামান। (৪) প্রতি test-এ লগইন না করে কীভাবে? Setup project-এ একবার লগইন, storageState ফাইলে রেখে project-এ ব্যবহার। (৫) Retry কেন সাবধানে? এটি flaky test-কে সবুজ দেখায়; trace রাখুন আর flaky সংখ্যা আলাদা করে দেখুন।

AI-এর যুগে E2E test

AI দিয়ে Playwright test লেখানো খুব সহজ, আর ফলও প্রথম দেখায় ভালো লাগে। কিন্তু কয়েকটি ভুল বারবার দেখা যায়, কারণ পুরোনো tutorial আর অন্য টুলের কোড মিশে যায়।

AI-এর ভুলকীভাবে ধরবেন
page.waitForTimeout(2000) বা waitForSelector ছড়ানোDiff-এ waitForTimeout খুঁজুন; web-first assertion দিয়ে বদলান
page.$('.btn-primary'), :nth-child-এর মতো পুরোনো CSS selectorgetByRole, getByLabel-এ বদলান; না পারলে সেটিই accessibility-র ইঙ্গিত
expect(await el.isVisible()).toBe(true), যা একবার মাপেawait expect(el).toBeVisible()
Puppeteer বা Cypress-এর API মিশিয়ে ফেলা, নেই এমন optionনিজের version-এর playwright.dev docs; npx tsc --noEmit বা চালিয়ে দেখা
আসল পাসওয়ার্ড বা token কোডে, user.json repository-তেFake account, environment variable, .gitignore-এ playwright/.auth
Test একে অন্যের ওপর নির্ভরশীল: প্রথমটি account বানায়, দ্বিতীয়টি ব্যবহার করেএকটি test একা চালান (-g); ফেল করলে fixture দিয়ে আলাদা করুন
ব্যর্থতা ঢাকতে retries: 5 আর বড় timeoutTrace খুলে কারণ খুঁজুন; retry বাড়ানো সমাধান নয়

AI-কে version, locator-এর নিয়ম আর test data-র পদ্ধতি স্পষ্ট বলুন, আর ফল নিজে চালিয়ে দেখুন। আসল account বা cookie prompt-এ দেবেন না।

আমি @playwright/test 1.64 আর TypeScript ব্যবহার করছি। একটি অনলাইন দোকানের checkout-এর E2E test লিখে দাও। শর্ত: ১. Locator শুধু getByRole, getByLabel, getByText; একান্ত দরকার হলে getByTestId. কোনো CSS class, nth-child বা XPath নয়। ২. কোথাও waitForTimeout বা নির্দিষ্ট sleep নয়। অপেক্ষা হবে web-first assertion দিয়ে: toBeVisible, toHaveText, toHaveURL, toHaveCount. ৩. একাধিক একই নামের বোতাম থাকলে getByRole('listitem').filter({ hasText }) দিয়ে সারি বেছে নাও। ৪. Test user তৈরি হবে test.extend দিয়ে বানানো একটি fixture-এ, request.post('/api/test/users') দিয়ে, প্রতিটি test-এ আলাদা email. কোনো আসল পাসওয়ার্ড নয়, শুধু 'example-pass-…'। ৫. Payment-এর ধাপে শুধু আমার নকল gateway-র "Pay (test)" বোতাম; কোনো আসল gateway-র নাম বা API বানাবে না। ৬. ব্যর্থতার একটি test-এ page.route দিয়ে /api/products-কে 500 ফেরত দেওয়াও। ৭. কোড দেওয়ার পরে বলো কোন অংশ তুমি নিশ্চিত নও; "সব test পাস করবে" বলবে না, কারণ তুমি চালাওনি।

AI-এর লেখা E2E test মেলানোর চেকলিস্ট

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন, তারপর “→ উত্তর” চাপুন।

প্র ০১E2E test সবচেয়ে বাস্তবসম্মত, তাহলে সব test E2E-তে লিখলে সমস্যা কী? অনেক দল E2E কমিয়ে রাখে কেন?

প্রশ্নটি নতুন নয়। Mike Cohn ২০০৯ সালে “Succeeding with Agile” বইয়ে test pyramid-এর ছবি জনপ্রিয় করেন: নিচে অনেক unit test, মাঝে কম service test, ওপরে খুব কম UI test. যুক্তি ছিল খরচ আর গতি। ২০১৫ সালে Google Testing Blog-এ “Just Say No to More End-to-End Tests” নামের একটি লেখা একই কথা আরও জোর দিয়ে বলে: E2E test-এর ব্যর্থতা দেরিতে আসে, কারণ খুঁজে পাওয়া কঠিন, আর flaky ফলাফল দলের আস্থা নষ্ট করে। পরে Kent C. Dodds-এর “testing trophy” integration test-কে বেশি গুরুত্ব দেয়। মডেলগুলো একে অন্যের বিরোধী নয়; সবগুলোই বলে E2E সবচেয়ে দামি স্তর।

দাম কোথায়? প্রথমত সময়: বাজারঘরের journey একা ২.৩ সেকেন্ড, অথচ একটি দামের হিসাবের unit test এক মিলিসেকেন্ডের কম। হাজার test-এ পার্থক্য মিনিট আর ঘণ্টার। দ্বিতীয়ত নির্ণয়: journey ফেল করলে কারণ হতে পারে search, cart API, cookie, login, CSS, gateway stub, যেকোনো কিছু। Unit test ফেল করলে কারণ প্রায় নিশ্চিত একটি function. তৃতীয়ত flakiness: যত বেশি চলমান অংশ, তত বেশি সময়ের অনিশ্চয়তা।

আবার শূন্য E2E-ও বিপজ্জনক। কিছু বাগ শুধু সব স্তর একসাথে হলেই দেখা যায়: cookie-র SameSite ভুল, CSS-এ একটি বোতাম আরেকটির নিচে ঢাকা, redirect-এর লুপ, production build-এ একটি environment variable না থাকা। এগুলো unit test কখনো ধরবে না। তাই ভালো নকশা হলো প্রতিটি বৈশিষ্ট্য যত নিচের স্তরে সম্ভব যাচাই করা, আর E2E রাখা “এগুলো জোড়া লাগছে কি না” আর ব্যবসার জন্য জরুরি পথের জন্য।

মূল উপলব্ধি: E2E test হলো দোকানের নকল ক্রেতা: অল্প কয়েকজন দরকার, প্রতিটি তাক পরীক্ষার জন্য নয়। প্রতিটি E2E test-কে নিজের অস্তিত্বের যুক্তি দিতে হবে।

প্র ০২CSS selector-এর বদলে role আর label দিয়ে খোঁজা কেন ভালো? data-testid কি তাহলে খারাপ?

Testing Library-র লেখক Kent C. Dodds একটি নীতি জনপ্রিয় করেছিলেন: আপনার test যত বেশি সফটওয়্যারের ব্যবহারের মতো হবে, তত বেশি আস্থা দেবে। এই চিন্তা থেকেই getByRole আর getByLabelText-এর মতো query এসেছে, আর Playwright পরে একই ধাঁচের locator যোগ করে। কারণ সহজ: ক্রেতা class নাম দেখেন না। তিনি দেখেন “খুঁজুন” লেখা একটি বোতাম, “ইমেইল” লেখা একটি ঘর।

Role আর label-ভিত্তিক locator-এর তিনটি সুবিধা। (১) স্থায়িত্ব: Tailwind-এ class বদলানো, div-এর গঠন বদলানো, CSS module-এর hash, কোনোটিই test ভাঙে না, যতক্ষণ ক্রেতার দেখা জিনিস একই। (২) Accessibility-র পাহারা: ল্যাবের কার্টে নামহীন ✕ বোতামটি getByRole দিয়ে খোঁজা সম্ভবই ছিল না; test লিখতে গিয়েই বাগ ধরা পড়ত। (৩) পড়ার সুবিধা: getByRole('button', { name: 'অর্ডার দিন ও পেমেন্ট করুন' }) পড়লেই বোঝা যায় test কী করছে।

অসুবিধাও আছে। লেখা বদলালে test বদলাতে হয়; “অর্ডার দিন” থেকে “এখনই কিনুন” হলে locator ভাঙবে, আর একাধিক ভাষার সাইটে এটি বাড়তি ঝামেলা। তখন test একটি নির্দিষ্ট locale-এ চালানো, বা অনুবাদের key থেকে লেখা নেওয়া একটি সমাধান। data-testid খারাপ নয়, শুধু শেষ উপায়। যেখানে কোনো স্থির, অর্থপূর্ণ নাম নেই, যেমন প্রতি run-এ বদলানো order নম্বর, বা একটি chart-এর canvas, সেখানে এটিই পরিষ্কার পথ। সমস্যা হয় যখন সবকিছুতে test id বসানো হয়, কারণ তখন test আর accessibility-র কোনো খবর দেয় না, আর markup-এ শুধু test-এর জন্য attribute জমে।

মূল উপলব্ধি: Locator বাছাই আসলে একটি প্রশ্ন: “ক্রেতা এটিকে কীভাবে চেনেন?” যে উত্তর ক্রেতার কাছে অর্থপূর্ণ, সেটিই সবচেয়ে টেকসই locator.

প্র ০৩Flaky test কি শুধু test-এর দোষ? Retry দিয়ে সবুজ রাখলে ক্ষতি কোথায়?

Flaky test-এর কারণ মোটামুটি তিন ধরনের। প্রথমটি test-এর ভুল: নির্দিষ্ট ঘুম, একবার মাপা assertion, test-এর মধ্যে ভাগ করা ডেটা, ক্রমের ওপর নির্ভরতা। ল্যাবের waitForTimeout(500) এর উদাহরণ, দশবারে ছয়বার ফেল। দ্বিতীয়টি পরিবেশের: ধীর CI মেশিন, network, বাইরের সেবার রেট-লিমিট, system font. তৃতীয়টি সবচেয়ে গুরুত্বপূর্ণ আর সবচেয়ে উপেক্ষিত: app-এর নিজের race condition. ধরুন দুটি fetch-এর উত্তর উল্টো ক্রমে এলে পুরোনো search-এর ফল নতুনটির ওপরে বসে যায়। Test মাঝে মাঝে ফেল করে, আর ক্রেতাও মাঝে মাঝে ভুল ফল দেখেন। এই ক্ষেত্রে flaky test আসলে একটি মূল্যবান সতর্কবার্তা।

বড় প্রতিষ্ঠানের engineering blog-এ, যেমন ২০১৬ সালে Google Testing Blog-এর flaky test নিয়ে একটি লেখায়, দেখা যায় বড় test suite-এ flakiness একটি নিয়মিত খরচ, আর তারা একে মাপে ও আলাদা করে দেখে। কারণ একটি সরল হিসাব: ১২০টি test-এর প্রতিটি যদি স্বাধীনভাবে মাত্র ০.৫% সময় ভুল করে ফেল করে, তাহলে পুরো suite একবারে পাস করার সম্ভাবনা 0.995120 ≈ 0.548. অর্থাৎ প্রায় অর্ধেক run লাল, কোনো আসল বাগ ছাড়াই। দল তখন লাল দেখলেই “আবার চালাও” বলতে শেখে।

Retry এই ব্যথা কমায়: এক retry-তে একটি test দুবার ভুল ফেল করার সম্ভাবনা 0.005² = 0.000025, suite পাসের সম্ভাবনা প্রায় 0.997. এজন্যই init-এর config CI-তে retry চালু রাখে। কিন্তু ক্ষতি তিনটি। আসল race condition লুকিয়ে যায়। Flaky test জমতে থাকে, কারণ কেউ আর দেখে না। আর suite ধীর হয়, কারণ retry-ও সময় নেয়। Playwright অন্তত সৎ: ল্যাবে ফল ছিল 1 flaky, আলাদা করে লেখা, আর প্রথম retry-র trace রাখা। সেই সংখ্যা কেউ পড়ছেন কি না, তা দলের অভ্যাস।

মূল উপলব্ধি: Retry হলো জ্বর কমানোর ওষুধ, রোগের চিকিৎসা নয়। প্রতিটি flaky test একটি বাগ-রিপোর্ট: হয় test-এর, নয়তো app-এর।

প্র ০৪Screenshot তুলনা সবচেয়ে নিখুঁত চোখ, তবু অনেক দল এটি সীমিত রাখে কেন? বাংলা সাইটে বাড়তি ঝুঁকি কী?

শক্তি স্পষ্ট: CSS-এর এমন বাগ ধরে, যা DOM-এর কোনো assertion ধরে না, যেমন দুটি বোতাম একে অন্যের ওপর বা লেখা কাটা পড়া। দুর্বলতাও স্পষ্ট: এটি “ভিন্ন” আর “ভুল”-এর পার্থক্য জানে না।

ল্যাবে font বদলে কার্ড ১৬০ থেকে ১৬৫ pixel লম্বা হলো, ২১১৪টি pixel ভিন্ন। ক্রেতার কাছে এটি হয়তো কোনো সমস্যাই নয়। একই রকম পার্থক্য আসে operating system বদলালে (font rendering, anti-aliasing), browser-এর version বদলালে, GPU থাকা না থাকায়, এমনকি screen-এর scale-এ। Playwright এজন্যই snapshot-এর নামে project আর platform লেখে (-edge-win32), যাতে Windows-এর baseline Linux-এ তুলনা না হয়।

বাংলা লেখায় ঝুঁকি বেশি। ল্যাটিন অক্ষর প্রায় স্বাধীনভাবে বসে, কিন্তু বাংলায় যুক্তাক্ষর, কার-এর অবস্থান, রেফ, হসন্ত, এসব text shaping engine (যেমন HarfBuzz) font-এর নিয়ম মেনে তৈরি করে। Font ভিন্ন হলে শুধু আকার নয়, গঠনও বদলাতে পারে, যেমন একটি যুক্তাক্ষর ভেঙে হসন্তসহ দেখানো। CI-র Linux-এ বাংলা font না থাকলে browser fallback নেয়, আর baseline পুরোপুরি অর্থহীন হয়। সমাধান: web font নিজে দেওয়া (পর্ব ৫৯), একটি নির্দিষ্ট container-এ baseline, await page.evaluate(() => document.fonts.ready)-এর মতো অপেক্ষা (Playwright নিজেও screenshot-এর আগে font লোডের অপেক্ষা করে, ল্যাবের call log-এ “waiting for fonts to load” দেখা গেছে), আর ছোট, স্থির অংশ।

আরেকটি মানবিক খরচ: প্রতিটি ইচ্ছাকৃত design পরিবর্তনে baseline হালনাগাদ আর diff দেখে অনুমোদন। এটি না করলে মানুষ চোখ বন্ধ করে --update-snapshots চালান, আর test-এর অর্থ থাকে না।

মূল উপলব্ধি: Screenshot test সবচেয়ে সংবেদনশীল চোখ, তাই সবচেয়ে বেশি মিথ্যা সতর্কতাও দেয়। পরিবেশ স্থির করুন, ছোট অংশে ব্যবহার করুন, আর অনুমোদন মানুষের হাতে রাখুন।

প্র ০৫axe কোনো violation না পেলে কি সাইট accessible? স্বয়ংক্রিয় accessibility test-এর জায়গা কোথায়?

Web accessibility-র মান WCAG, W3C-র Web Accessibility Initiative-এর তৈরি। WCAG 2.0 আসে ২০০৮ সালে, 2.1 আসে ২০১৮ সালে, যেখানে মোবাইল আর কম দৃষ্টির ব্যবহারকারীদের জন্য নতুন নিয়ম যোগ হয়, আর 2.2 প্রকাশ হয় ২০২৩ সালে। Deque Systems ২০১৫ সালের দিকে axe-core open-source করে, যা পরে অনেক browser extension আর test টুলের ভেতরের engine হয়েছে।

axe যা ভালো পারে: নিয়মভিত্তিক, নির্ণয়যোগ্য সমস্যা। বোতামের নাম আছে কি না (ল্যাবের button-name), input-এর label, ছবির alt, রঙের contrast-এর অনুপাত, ARIA attribute-এর বৈধতা, পাতার ভাষা। এগুলো প্রতিটি পরিবর্তনে স্বয়ংক্রিয়ভাবে ধরা খুব মূল্যবান, কারণ একটি icon-বোতাম যোগ করতে গিয়ে নাম ভুলে যাওয়া খুব সাধারণ।

axe যা পারে না: অর্থ আর অভিজ্ঞতা। একটি ছবির alt আছে, কিন্তু লেখা “image123.jpg” হলে নিয়ম পাস, ব্যবহারকারী কিছুই পান না। Keyboard দিয়ে পুরো checkout করা যায় কি না, focus একটি modal-এ আটকে যায় কি না, স্ক্রিন রিডারে পড়ার ক্রম যুক্তিসঙ্গত কি না, ত্রুটির বার্তা বোধগম্য কি না, এসব মানুষ দেখে। বিভিন্ন সূত্রে বলা হয় স্বয়ংক্রিয় টুল WCAG-এর সমস্যার একটি অংশই ধরতে পারে; সঠিক অনুপাত নিয়ে আমরা কোনো সংখ্যা দিচ্ছি না, কারণ তা মাপার পদ্ধতির ওপর নির্ভর করে।

তাই সুষম নকশা: axe-এর test প্রতিটি জরুরি পাতায় (ল্যাবে কার্ট), getByRole-ভিত্তিক locator যা নিজেই নাম যাচাই করে, Playwright-এ keyboard দিয়ে একটি journey (page.keyboard.press('Tab')), আর মাঝে মাঝে আসল স্ক্রিন রিডার দিয়ে মানুষের পরীক্ষা।

মূল উপলব্ধি: axe-এর শূন্য violation মানে “পরিচিত ভুলগুলো নেই”, “সবাই ব্যবহার করতে পারেন” নয়। স্বয়ংক্রিয় test মেঝে বানায়, ছাদ নয়।

অনুশীলন

হিসাব, কমান্ড, সমালোচনা আর ফল অনুমান, চারটিই আছে।

  1. Suite-এর সময় আর flakiness (numerical): একটি দোকানের ১২০টি E2E test, প্রতিটির গড় সময় ৬ সেকেন্ড (অনুমান)। (ক) ১টি worker-এ মোট কত সময়? (খ) ৪টি worker-এ আদর্শ অবস্থায়? (গ) ৩টি shard, প্রতিটিতে ৪টি worker হলে? (ঘ) প্রতিটি test স্বাধীনভাবে ০.৫% সময় ভুল করে ফেল করলে retry ছাড়া পুরো suite পাস করার সম্ভাবনা কত? ১টি retry দিলে?

    (ক) 120 × 6 = 720 s = ১২ মিনিট. (খ) 720 ÷ 4 = 180 s = ৩ মিনিট; বাস্তবে একটু বেশি, কারণ browser চালু, server আর setup project-এর সময় ভাগ হয় না। (গ) প্রতিটি shard 40টি test, 40 × 6 ÷ 4 = 60 s; প্রতিটি shard-এ setup আবার চলে, তাই এর ওপরে কিছু সেকেন্ড। (ঘ) 0.995120 ≈ 0.548, অর্থাৎ প্রায় ৪৫% run-এ অন্তত একটি মিথ্যা ব্যর্থতা। ১টি retry-তে একটি test-এর দুবার ভুল ফেলের সম্ভাবনা 0.005² = 0.000025, suite পাস (1 − 0.000025)120 ≈ 0.997. সংখ্যাটি সুন্দর, কিন্তু মনে রাখুন: retry-র পরে পাস করা প্রতিটি test “flaky” তালিকায় যায়, আর সেই তালিকা কমানোই আসল কাজ।

  2. কমান্ড লিখুন: বাজারঘরের config অনুযায়ী এই চারটি কাজের কমান্ড লিখুন: (ক) শুধু journey test, শুধু edge project-এ, trace সবসময় চালু রেখে; (খ) সেই trace খোলা; (গ) ইচ্ছাকৃত design বদলের পরে visual test-এর baseline হালনাগাদ; (ঘ) একটি test দশবার চালিয়ে flaky কি না দেখা।

    (ক) npx playwright test journey --project=edge --trace on; edge setup-এর ওপর নির্ভরশীল, তাই setup-ও চলবে। (খ) npx playwright show-trace test-results/<test-এর folder>/trace.zip, বা GUI ছাড়া npx playwright trace open … আর npx playwright trace actions. (গ) npx playwright test visual --update-snapshots, তারপর নতুন PNG-টি চোখে দেখে diff-এ অনুমোদন। (ঘ) npx playwright test search --repeat-each=10; সবগুলো পাস না হলে flaky. --retries এখানে দেবেন না, তাহলে ব্যর্থতা লুকাবে।

  3. Test সমালোচনা (নকশা): অন্তত ছয়টি সমস্যা ধরুন আর প্রতিটির সমাধান বলুন: test('buy', async ({ page }) => { await page.goto('https://shop.example/login'); await page.fill('#email', 'owner@realshop.com'); await page.fill('#pw', 'MyRealPass!'); await page.click('.btn.btn-primary'); await page.waitForTimeout(3000); await page.click('ul > li:nth-child(2) button'); await page.click('text=Checkout'); await page.waitForTimeout(5000); expect(await page.isVisible('.success')).toBe(true); }); আর পরের test এই অর্ডারের ওপর refund পরীক্ষা করে।
    • আসল production URL আর আসল মালিকের ইমেইল ও পাসওয়ার্ড: test environment, baseURL, API-তে বানানো নকল ক্রেতা।
    • প্রতি test-এ UI লগইন: setup project আর storageState, যদি লগইন এই test-এর বিষয় না হয়।
    • #email, .btn.btn-primary, nth-child(2): ভঙ্গুর; getByLabel('ইমেইল'), getByRole('button', { name }), আর পণ্যের নাম দিয়ে filter.
    • waitForTimeout(3000), (5000): ধীর আর flaky; web-first assertion, যেমন toHaveURL.
    • expect(await page.isVisible(…)).toBe(true) একবার মাপে: await expect(locator).toBeVisible().
    • “.success” দেখা দুর্বল যাচাই: order নম্বর, মোট দাম, heading-এর লেখা যাচাই।
    • পরের test এই অর্ডারের ওপর নির্ভর করে: ক্রম বা parallel বদলালে ভাঙবে; refund test নিজের অর্ডার API দিয়ে বানাবে।
    • আসল gateway-তে টাকা: stub বা sandbox.
  4. ফল অনুমান: একটি পাতায় তিনটি সারি: “ইলিশ ৳1250”, “ইলিশের ডিম ৳600”, “সাবান ৳45”, প্রতিটিতে “কার্টে যোগ করুন” বোতাম। চারটি লাইন আলাদাভাবে চালানো হলো। প্রতিটির ফল কী: সফল ক্লিক, strict mode violation, না timeout পর্যন্ত অপেক্ষা? (ক) page.getByRole('button', { name: 'কার্টে যোগ করুন' }).click() (খ) page.getByRole('listitem').filter({ hasText: 'ইলিশ' }).getByRole('button').click() (গ) page.getByRole('listitem').filter({ hasText: 'সাবান' }).getByRole('button').click() (ঘ) page.getByRole('listitem').filter({ hasText: 'ডাল' }).getByRole('button').click()

    (ক) strict mode violation, ৩টি element. (খ) strict mode violation, ২টি element: hasText লেখার অংশ মেলায়, তাই “ইলিশের ডিম”-ও “ইলিশ” ধারণ করে। সমাধান: hasText: /^ইলিশ ৳/-এর মতো regex, বা পূর্ণ নাম। (গ) সফল ক্লিক, ঠিক একটি। (ঘ) ০টি element: Playwright সাথে সাথে ফেল করে না, timeout পর্যন্ত অপেক্ষা করে (default action timeout না থাকলে test-এর timeout), তারপর ফেল। এজন্যই ভুল লেখার locator কখনো কখনো ধীর ব্যর্থতা দেয়।

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রমSetup project আর webServer-সহ একটি Playwright Test run-এর ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. Runner config পড়ে project আর test-এর তালিকা বানায়
  2. webServer-এর command চালিয়ে health URL-এ সফল উত্তরের অপেক্ষা করে
  3. Setup project লগইন করে storageState ফাইল লেখে
  4. নির্ভরশীল project-এর test-গুলো worker-এ, প্রতিটি নিজের browser context-এ চলে
  5. Reporter ফল দেখায় আর ব্যর্থ test-এর trace ও screenshot রাখে
  6. Runner webServer-এর process বন্ধ করে

একটি Playwright test-এ await page.waitForTimeout(500); expect(await page.locator('#list li').count()).toBe(1); লেখা। Search-এর ফল কখনো ৩০০ ms, কখনো ৭০০ ms-এ আসে। সবচেয়ে ভালো সমাধান কোনটি?

  • ঘুম বাড়িয়ে ২০০০ ms করা
  • Config-এ retries: 3 দেওয়া
  • await expect(page.locator('#list li')).toHaveCount(1) লেখা আর ঘুম মুছে ফেলা
  • expect(await …count())-এর আগে আরেকটি await যোগ করা

একটি পাতায় পাঁচটি পণ্যের প্রতিটিতে “কার্টে যোগ করুন” বোতাম। page.getByRole('button', { name: 'কার্টে যোগ করুন' }).click() চালালে Playwright কী করে?

  • প্রথম বোতামটি চাপে
  • Strict mode violation-এর error দেয়, পাঁচটি element-এর তালিকাসহ
  • পাঁচটি বোতামই একে একে চাপে
  • একটি মাত্র element না আসা পর্যন্ত নীরবে অপেক্ষা করে

Playwright-এ storageState ফাইল ব্যবহারের মূল উদ্দেশ্য কী?

  • Visual test-এর baseline ছবি রাখা
  • ব্যর্থ test-এর তালিকা রাখা
  • Database-এর test data সংরক্ষণ
  • একবার লগইন করে browser-এর cookie ও storage রেখে পরের test-এ আবার ব্যবহার করা

একটি E2E test-এ search API-র 500 উত্তরের ক্ষেত্রে পাতা কী দেখায়, তা যাচাই করতে চান। Server না বদলে সবচেয়ে সরাসরি উপায় কোনটি?

  • page.route দিয়ে অনুরোধ ধরে route.fulfill({ status: 500 })
  • Test-এর মাঝে server process বন্ধ করে দেওয়া
  • retries বাড়িয়ে দেওয়া যাতে কোনো এক সময় 500 আসে
  • toHaveScreenshot দিয়ে error পাতার ছবি মেলানো

একটি দল Windows ল্যাপটপে toHaveScreenshot-এর baseline বানিয়েছে, কিন্তু Linux CI-তে বাংলা লেখার কার্ডের test প্রতিবার ফেল করে। সবচেয়ে সম্ভাব্য কারণ আর সমাধান কী?

  • CI ধীর, তাই timeout বাড়ানো
  • maxDiffPixelRatio: 0.5 দিয়ে সব পার্থক্য উপেক্ষা
  • Font ও rendering-এর পার্থক্য; baseline CI-র একই পরিবেশে বানানো আর web font নিজে দেওয়া
  • Visual test পুরোপুরি মুছে ফেলা, কারণ এগুলো সবসময় অকেজো

@axe-core/playwright দিয়ে একটি পাতা scan করে শূন্য violation পাওয়া গেল। এর সবচেয়ে সঠিক ব্যাখ্যা কোনটি?

  • পাতাটি WCAG-এর সব শর্ত পূরণ করে
  • axe-এর স্বয়ংক্রিয় নিয়মে ধরা পড়ে এমন সমস্যা পাওয়া যায়নি; keyboard আর স্ক্রিন রিডারের পরীক্ষা তবু দরকার
  • Test ভুল লেখা হয়েছে, কারণ সব পাতায় কিছু না কিছু violation থাকে
  • শুধু রঙের contrast ঠিক আছে, বাকি কিছু দেখা হয়নি

একটি CI run-এর শেষে Playwright দেখাল 1 flaky, 4 passed, আর pipeline সবুজ। দলের সবচেয়ে উপযুক্ত পদক্ষেপ কোনটি?

  • কিছু না করা, কারণ pipeline সবুজ
  • Retry ২ থেকে ৫ করে দেওয়া
  • Flaky test-টি মুছে ফেলা
  • প্রথম retry-র trace খুলে কারণ খোঁজা আর বাগ হিসেবে খোলা, কারণ এটি test বা app-এর সমস্যা হতে পারে

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন, অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:

মনে রাখুন

  1. Playwright Test নিজেই সব গোছায়: webServer app চালায়, project ঠিক করে কোন browser ও viewport (channel: 'msedge', ৩৬০ px), শেষে server বন্ধ।
  2. Locator ক্রেতার ভাষায়: getByRole, getByLabel, getByText, শেষে getByTestId; একাধিক মিললে strict mode থামায়, filter আর chaining দিয়ে একটিতে নামান।
  3. ঘুম নয়: action নিজে actionability-র অপেক্ষা করে, আর await expect(locator) শর্ত পূরণ পর্যন্ত বারবার দেখে; waitForTimeout flaky test বানায়।
  4. ডেটা API দিয়ে, লগইন একবার: fixture-এ প্রতি test নিজের ক্রেতা, setup project-এ storageState, আর page.route দিয়ে ব্যর্থতার পথ।
  5. ব্যর্থতা পড়ুন, লুকাবেন না: trace আর screenshot দেখুন, retry-র “flaky” গুনুন; screenshot তুলনা font-এ ভঙ্গুর, axe-এর শূন্য মানে মেঝে, আর E2E রাখুন অল্প কিন্তু জরুরি পথে।

পরের পর্বে: পর্ব ৭৭ · Mock, MSW আর test data — flaky test কমানো। Server-এর দিকের network কীভাবে নকল করবেন, test data-র factory, আর flaky test খুঁজে বের করে স্থায়ীভাবে সারানোর কৌশল।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান