Java/TestNG → TypeScript/Playwright AI-assisted migration workflow Base framework blueprint

Selenium → Playwright

Ultimate migration guide — from ATB13x Selenium base framework to a Playwright + TypeScript + AI-ready architecture
7

Chapters

Covering end-to-end migration

16

Cheat Sheet Categories

Side-by-side code mappings

3

Framework Layers

Pages, modules, tests with clear separation

4

AI Tool Templates

Claude, Copilot, Cursor, Windsurf

What this migration guide actually gives you

This is not just a Selenium vs Playwright comparison. It is a working migration playbook for moving a real Java/TestNG automation framework into a modern Playwright stack without losing structure, reporting discipline, or maintainability.

SourceATB13x

Existing Selenium base framework patterns

TargetPW + TS

Typed Playwright framework with modules and fixtures

AI assist65-70%

Realistic migration acceleration with human review

FocusStable DX

Locator quality, waits, test design, CI, and debugging

Created by Pramod Dutta

Role Founder, The Testing Academy ยท QA automation mentor
Teaching focus Selenium, Playwright, AI for QA, framework design, and career transition for automation engineers
Guide context Built around the real Selenium base framework to Playwright framework migration path, not a toy demo conversion

Why Migrate?

Built-in Reliability

Auto-wait, robust locators, tracing, parallel isolation. No more flaky tests with Thread.sleep.

🚀

Less Infrastructure

No Selenium Grid, no driver version management. Faster local & CI runs out of the box.

🤖

AI-Native DX

Playwright MCP, codegen, Inspector, TypeScript types. VS Code integration. AI-assisted testing.

Base framework migration blueprint

Layer 1: Stable primitives

Start with config, typed test data, utilities, and API clients. Freeze conventions before AI starts generating files so the code lands in the right architecture.

Layer 2: Page + module split

Keep pages focused on locators and atomic actions. Move reusable business flows into modules so the migration does not recreate Selenium-style God page objects.

Layer 3: AI-guided conversion

Use AI to draft pages, modules, tests, and fixes against strict rules. The model accelerates conversion, but the framework rules and final debugging stay human-owned.

Course Chapters

CHAPTER 1
Why Migrate & Planning
~12 min
CHAPTER 2
Setup & Configuration
~12 min
CHAPTER 3
Master Cheat Sheet
~15 min
CHAPTER 4
Page Objects & Tests
~15 min
CHAPTER 5
Waits, Retries & Infra
~12 min
CHAPTER 6
AI-Powered Migration
~15 min
CHAPTER 7
Quick Reference
~10 min

Prerequisites

  • Node.js >= 18 & npm >= 9 installed
  • VS Code with Playwright extension
  • Git installed & configured
  • Basic TypeScript knowledge (types, async/await)
  • Familiarity with Selenium Java framework

Base framework references

Selenium ATB13xSeleniumAdvanceFramework

Java / Maven / TestNG / POM + Page Factory / Retry / Allure / Data-Driven baseline used as the migration source.

Playwright Advance-Playwright-Framework

TypeScript / Playwright Test / 3-layer architecture / fixtures / Docker / CI used as the target framework shape for AI-assisted conversion.

Chapter 1Why Migrate & Planning

Understand the fundamental differences between Selenium and Playwright, compare architectures, and plan your incremental migration strategy.

Selenium vs Playwright Feature Matrix

FeatureSelenium (Java)Playwright (TypeScript)
ProtocolWebDriver (W3C)CDP / BiDi (direct)
Auto-WaitNone (manual waits)Built-in actionability checks
Parallel ExecutionGrid + TestNG threadsBuilt-in workers + sharding
Browser ManagementSeparate drivers (ChromeDriver)Bundled (npx playwright install)
Test IsolationManual (ThreadLocal)Built-in (BrowserContext per test)
DebuggingBreakpoints + logsInspector + Trace Viewer + UI Mode
ReportingAllure / ReportNG (plugin)Built-in HTML + JSON + JUnit
API TestingREST Assured (separate)Built-in APIRequestContext
Network Mocking3rd party toolsBuilt-in page.route()
Video RecordingExternal toolsBuilt-in video support
Visual TestingApplitools / add-onsBuilt-in toHaveScreenshot()
Mobile EmulationAppium (separate)Built-in device emulation
AI IntegrationLimitedMCP Server + Codegen + AI Agents
Retry LogicCustom RetryAnalyzerBuilt-in retries config
Soft AssertionsAssertJ / customBuilt-in expect.soft()

Architecture Comparison

Selenium Architecture
Test Code (Java)
    |
    v
WebDriver API  (driver.findElement, click, sendKeys)
    |
    v
JSON Wire / W3C WebDriver Protocol (HTTP)
    |
    v
Browser Driver (ChromeDriver, GeckoDriver)
    |
    v
Browser (Chrome, Firefox, Edge)

Key Objects:
  WebDriver   -- one browser instance
  WebElement  -- one DOM element
  By          -- locator strategy
  Actions     -- advanced interactions
Playwright Architecture
Test Code (TypeScript)
    |
    v
Playwright API  (page.locator, click, fill)
    |
    v
CDP / BiDi Protocol (WebSocket - direct!)
    |
    v
Browser (Chromium, Firefox, WebKit)

Key Objects:
  Browser     -- browser process
  Context     -- isolated session (cookies, storage)
  Page        -- single tab
  Locator     -- lazy element reference
  expect()    -- auto-retry assertions
Key Insight: Playwright communicates directly with browsers via WebSocket (CDP/BiDi), eliminating the driver middleman. This means faster execution, better reliability, and no driver version management.

TypeScript Quick Reference for Java Developers

JavaTypeScriptNotes
String name = "test";const name: string = 'test';const/let, type inference available
public void click()async click(): Promise<void>All browser ops are async
element.click();await element.click();Must await async calls
interface MyPage { }interface MyPage { }Same concept
List<String>string[]Array syntax
Map<String, Object>Record<string, unknown>Or { [key: string]: unknown }
@FindBy(id="x")() => page.locator('#x')Arrow functions replace annotations
try/catch/finallytry/catch/finallySame + async error handling
import com.x.LoginPage;import { LoginPage } from './pages';ES module imports
nullnull | undefinedTwo null-ish values in TS

Folder Structure Side-by-Side

Selenium Project Structure
ATB13xSeleniumAdvanceFramework/
├─ pom.xml
├─ testng_vwo_*.xml          (5 config files)
├─ src/main/java/.../
│   ├─ base/
│   │   └─ CommonToAllPage.java
│   ├─ driver/
│   │   ├─ DriverManager.java
│   │   └─ DriverManagerTL.java
│   ├─ pages/
│   │   ├─ pageFactory/
│   │   │   ├─ LoginPage_PF.java
│   │   │   └─ DashboardPage_PF.java
│   │   └─ pageObjectModel/
│   │       └─ vwo/
│   │           ├─ LoginPage.java
│   │           └─ DashBoardPage.java
│   └─ utils/
│       ├─ PropertiesReader.java
│       ├─ WaitHelpers.java
│       └─ TakeScreenShot.java
└─ src/test/java/.../
    ├─ base/CommonToAllTest.java
    ├─ listeners/
    │   ├─ RetryAnalyzer.java
    │   ├─ RetryListener.java
    │   └─ ScreenshotListener.java
    ├─ tests/vwo/...
    └─ utilsExcel/UtilExcel.java
Playwright Project Structure
Advance-Playwright-Framework/
├─ playwright.config.ts
├─ tsconfig.json
├─ package.json
├─ .env
├─ Dockerfile
├─ docker-compose.yml
├─ .github/workflows/
│   ├─ playwright.yml
│   └─ smoke-tests.yml
└─ src/
    ├─ pages/          (Page Objects)
    │   ├─ LoginPage.ts
    │   ├─ HomePage.ts
    │   ├─ ProductPage.ts
    │   ├─ CheckoutPage.ts
    │   └─ index.ts        (barrel)
    ├─ modules/        (Business Flows)
    │   ├─ LoginModule.ts
    │   ├─ ProductModule.ts
    │   ├─ CheckoutModule.ts
    │   └─ index.ts
    ├─ fixtures/       (Auth/Setup)
    │   ├─ auth.fixture.ts
    │   └─ index.ts
    ├─ utils/          (Helpers)
    │   ├─ Logger.ts
    │   ├─ WaitHelper.ts
    │   ├─ ApiHelper.ts
    │   └─ DataGenerator.ts
    ├─ api/            (REST helpers)
    ├─ testdata/       (JSON + types)
    │   ├─ users.json
    │   ├─ products.json
    │   └─ types.ts
    └─ tests/          (Specs)
        ├─ login.spec.ts
        ├─ product.spec.ts
        └─ checkout.spec.ts
What changed? No more driver/ folder, no listeners/, no separate pageFactory/. Playwright replaces all of these with built-in features. New additions: modules/ for business flows, fixtures/ for DI, testdata/ with typed JSON.

Migration Strategy: Bottom-Up Incremental

1️⃣

Pilot Critical Paths

Pick 5-10 high-value P0 Selenium tests. Stand up Playwright project with conventions.

2️⃣

Parallel Run & Baseline

Keep Selenium running. Add Playwright CI job. Compare flake rate and duration.

3️⃣

Scale Conversion

All new features in Playwright only. Convert flaky/slow Selenium tests first.

4️⃣

Sunset Selenium

Retire legacy TestNG suites once coverage & stability thresholds are met.

Implementation Order: Always go bottom-up: ConfigUtilitiesPagesModulesTestsCI. This ensures dependencies exist before consumers.

Migration Priority Table

#ActionWhy
1Replace locators firstImproves stability the most
2Remove unnecessary waitsReduces flaky timing logic
3Convert assertions to expectEnables auto-retry and reliability
4Rework frames, tabs, dialogsSimplifies control flow
5Consolidate config & reportingLeads to cleaner architecture

Chapter 2Setup & Configuration

Convert your build tools, test configuration, environment handling, and CI pipelines from Selenium/Maven/TestNG to Playwright/npm/TypeScript.

Dependency Mapping: pom.xml → package.json

Selenium DependencyVersionPlaywright EquivalentNotes
selenium-java4.35.0@playwright/testCore framework
testng7.11.0Built-in test runnerNo extra package needed
allure-testng2.26.0Built-in reportersHTML, JSON, JUnit built-in
reportng1.1.2Built-in HTML reporterOr custom reporter
assertj-core3.25.1Built-in expect()Web-first assertions with auto-retry
log4j-core3.0.0Built-in console / custom LoggerTrace viewer replaces most logging
poi + poi-ooxml5.2.4JSON files + TypeScript typesNo Excel needed
WebDriverManager-npx playwright installBundled browser management
REST Assured-Built-in APIRequestContextAPI testing included
Result: Playwright replaces ~10 separate Java dependencies with a single @playwright/test package. Your package.json devDependencies section will have just 3-4 entries (playwright, types/node, dotenv, typescript).

Configuration Conversion

testng.xml → playwright.config.ts

testng_vwo_*.xml
<!DOCTYPE suite SYSTEM "https://testng.org/testng-1.0.dtd">
<suite name="VWO Suite" parallel="methods" thread-count="2">
  <listeners>
    <listener class-name="...RetryListener"/>
    <listener class-name="...ScreenshotListener"/>
  </listeners>
  <test name="VWO Login Tests">
    <classes>
      <class name="...TestVWOLogin_05...">
        <methods>
          <include name="testPositiveLogin"/>
          <include name="testNegativeLogin"/>
        </methods>
      </class>
    </classes>
  </test>
</suite>
playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
import * as dotenv from 'dotenv';
dotenv.config();

export default defineConfig({
  testDir: './src/tests',
  fullyParallel: true,
  workers: process.env.CI ? 2 : 3,
  retries: process.env.CI ? 2 : 0,
  timeout: 60_000,
  expect: { timeout: 10_000 },
  reporter: [
    ['html', { open: 'never' }],
    ['json', { outputFile: 'test-results/results.json' }],
    ['list'],
  ],
  use: {
    baseURL: process.env.BASE_URL,
    trace: 'on-first-retry',
    video: 'retain-on-failure',
    screenshot: 'only-on-failure',
  },
  projects: [
    { name: 'chromium', use: { ...devices['Desktop Chrome'] } },
    { name: 'firefox', use: { ...devices['Desktop Firefox'] } },
    { name: 'webkit', use: { ...devices['Desktop Safari'] } },
  ],
});
What replaced what: parallel="methods" thread-count="2"fullyParallel: true, workers: 3. Listeners (RetryListener, ScreenshotListener) → retries, screenshot, video, trace config options. Test selection → testDir + file naming + --grep tags.

Environment & Properties

data.properties + PropertiesReader.java → .env + config/index.ts

data.properties + PropertiesReader.java
# data.properties
vwoURL=https://app.vwo.com
[email protected]
password=Test@4321
browser=Chrome

// PropertiesReader.java
public class PropertiesReader {
  static Properties prop = new Properties();
  static {
    FileInputStream fis = new FileInputStream(
      "src/main/resources/data.properties"
    );
    prop.load(fis);
  }
  public static String readKey(String key) {
    return prop.getProperty(key);
  }
}
.env + config/index.ts
# .env
BASE_URL=http://localhost:3000
API_BASE_URL=http://localhost:3000/api
[email protected]
TEST_PASSWORD=SecurePass123
LOG_LEVEL=INFO

// src/config/index.ts
import * as dotenv from 'dotenv';
dotenv.config();

export const AppConfig = {
  baseUrl: process.env.BASE_URL || 'http://localhost:3000',
  apiBaseUrl: process.env.API_BASE_URL || '',
  testUser: {
    email: process.env.TEST_USERNAME || '',
    password: process.env.TEST_PASSWORD || '',
  },
  timeouts: {
    api: 30_000,
    default: 30_000,
  },
} as const;

CI/CD: Maven → GitHub Actions

.github/workflows/playwright.yml
name: Playwright Tests
on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main, develop]

jobs:
  test:
    runs-on: ubuntu-latest
    timeout-minutes: 30
    strategy:
      fail-fast: false
      matrix:
        shard: [1/4, 2/4, 3/4, 4/4]    # 4 parallel shards
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npm ci
      - run: npx playwright install --with-deps
      - run: npx playwright test --shard=${{ matrix.shard }}
      - uses: actions/upload-artifact@v4
        if: always()
        with:
          name: playwright-report-${{ strategy.job-index }}
          path: playwright-report/
          retention-days: 30
4-shard parallelization: Playwright splits tests across 4 CI runners automatically. No Selenium Grid needed. Each shard uploads its report, which can be merged post-job.

Docker Support

Dockerfile
FROM mcr.microsoft.com/playwright:v1.40.0-jammy
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
CMD ["npx", "playwright", "test"]

Chapter 3The Master Cheat Sheet

The complete side-by-side reference across 16 categories. Every Selenium Java pattern mapped to its Playwright TypeScript equivalent.

1. Assertions

10 items
Selenium / TestNG-JUnitPlaywright (expect)Notes
Assert.assertEquals(driver.getTitle(), "Home")await expect(page).toHaveTitle('Home')Auto-retries
Assert.assertTrue(driver.getCurrentUrl().contains("/home"))await expect(page).toHaveURL(/.*home/)Regex support
Assert.assertTrue(element.isDisplayed())await expect(locator).toBeVisible()Auto-waits
Assert.assertFalse(element.isDisplayed())await expect(locator).toBeHidden()
Assert.assertEquals(element.getText(), "Welcome")await expect(locator).toHaveText('Welcome')Exact match
Assert.assertTrue(element.getText().contains("Wel"))await expect(locator).toContainText('Wel')Partial match
Assert.assertTrue(element.isEnabled())await expect(locator).toBeEnabled()
Assert.assertTrue(element.isSelected())await expect(locator).toBeChecked()Checkbox/radio
Assert.assertEquals(element.getAttribute("value"), "text")await expect(locator).toHaveValue('text')Input values
Assert.assertEquals(elements.size(), 5)await expect(locator).toHaveCount(5)Element count

2. Locators

14 items
Selenium (Java)Playwright (TypeScript)Notes
driver.findElement(By.cssSelector("input#email"))page.locator('input#email')
driver.findElement(By.id("email"))page.locator('#email')
driver.findElement(By.tagName("input"))page.locator('input')
driver.findElement(By.name("user"))page.locator('[name="user"]')
driver.findElement(By.className("btn"))page.locator('.btn')
driver.findElement(By.xpath("//input[@id='email']"))page.locator('xpath=//input[@id="email"]')Use as last resort
driver.findElement(By.linkText("Sign In"))page.getByText('Sign In', { exact: true })Exact text
driver.findElement(By.partialLinkText("Sign"))page.getByText('Sign')Partial text
driver.findElements(By.cssSelector(".item"))page.locator('.item').all()Returns array
(no equivalent)page.getByRole('button', { name: 'Submit' })NEW - Preferred
(no equivalent)page.getByLabel('Email')NEW
(no equivalent)page.getByPlaceholder('Enter email')NEW
(no equivalent)page.getByTestId('submit-btn')NEW - Recommended
(no equivalent)page.getByAltText('Logo')NEW
Migration Tip: Prefer user-facing locators: getByRole, getByLabel, getByText, getByTestId over XPath or CSS. They are more readable, more resilient to DOM changes, and match how users interact with the page.

3. Element Interactions

14 items
Selenium (Java)Playwright (TypeScript)Notes
element.sendKeys("text")await locator.fill('text')Clears first, then types
element.click()await locator.click()Auto-waits for actionability
element.clear()await locator.clear()
element.getText()await locator.textContent()All text including hidden
(visible text only)await locator.innerText()Visible text only
element.getAttribute("href")await locator.getAttribute('href')
element.isDisplayed()await locator.isVisible()
element.isEnabled()await locator.isEnabled()
element.isSelected()await locator.isChecked()For checkboxes
element.submit()await locator.press('Enter')
element.sendKeys(Keys.TAB)await locator.press('Tab')
element.sendKeys("text") // appendawait locator.pressSequentially('text')Simulates keystrokes
(checkbox handling indirect)await locator.check() / uncheck()NEW
(file upload via sendKeys path)await locator.setInputFiles('file.pdf')NEW
Key difference: fill() replaces existing text and types in a single step. Playwright auto-waits before performing any action (checks visibility, stability, enabled state).

4. Waits

10 items
Selenium (Java)Playwright (TypeScript)Notes
driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(10))(not needed - Playwright auto-waits)ELIMINATED
new WebDriverWait(driver, Duration.ofSeconds(10)).until(ExpectedConditions.visibilityOfElementLocated(...))await locator.waitFor({ state: 'visible' })Rarely needed
wait.until(ExpectedConditions.elementToBeClickable(...))await locator.click()Auto-waits for actionability
wait.until(ExpectedConditions.titleContains("Dashboard"))await page.waitForURL('**/dashboard')Glob patterns
wait.until(ExpectedConditions.urlContains("/home"))await page.waitForURL('**/home')
wait.until(ExpectedConditions.invisibilityOfElementLocated(...))await locator.waitFor({ state: 'hidden' })
wait.until(ExpectedConditions.alertIsPresent())page.on('dialog', ...) // register before triggerEvent-based
Thread.sleep(3000)await page.waitForTimeout(3000) // AVOIDAvoid unless debugging
(no equivalent)await page.waitForLoadState('networkidle')NEW
(no equivalent)await page.waitForResponse('**/api/data')NEW - Network-aware
Most important change: Playwright's built-in auto-waiting and actionability checks eliminate 80%+ of explicit waits. Actions like click(), fill(), check() automatically wait for the element to be visible, stable, and enabled before acting.

5. Navigation

7 items
Selenium (Java)Playwright (TypeScript)
driver.get("https://example.com")await page.goto('https://example.com')
driver.navigate().back()await page.goBack()
driver.navigate().forward()await page.goForward()
driver.navigate().refresh()await page.reload()
driver.getCurrentUrl()page.url()
driver.getTitle()await page.title()
driver.getPageSource()await page.content()

6. Windows & Tabs

5 items
Selenium (Java)Playwright (TypeScript)Notes
driver.getWindowHandle()page is already the active tab objectNo handle needed
driver.getWindowHandles()context.pages()Returns Page[]
driver.switchTo().window(handle)Use the target page object directlyNo switching needed
driver.switchTo().newWindow(WindowType.TAB)const newPage = await context.newPage()
driver.close()await page.close()
Key concept: Playwright treats each tab as a first-class Page object. No string handle switching needed - just use the page reference directly.

7. Frames / iFrames

4 items
Selenium (Java)Playwright (TypeScript)
driver.switchTo().frame(0)page.frameLocator('iframe').first()
driver.switchTo().frame("frameName")page.frameLocator('#frameName')
driver.switchTo().defaultContent()(not needed - frame locators are scoped)
driver.switchTo().frame("f1"); driver.findElement(By.id("btn")).click(); driver.switchTo().defaultContent();await page.frameLocator('#f1').locator('#btn').click()

8. Screenshots

3 items
Selenium (Java)Playwright (TypeScript)
((TakesScreenshot)driver).getScreenshotAs(OutputType.FILE)await page.screenshot({ path: 'screenshot.png' })
element.getScreenshotAs(OutputType.FILE)await locator.screenshot({ path: 'element.png' })
(full-page support varies)await page.screenshot({ path: 'full.png', fullPage: true })

9. Alerts / Dialogs

4 items
Selenium (Java)Playwright (TypeScript)
driver.switchTo().alert().accept()page.on('dialog', dialog => dialog.accept())
driver.switchTo().alert().dismiss()page.on('dialog', dialog => dialog.dismiss())
driver.switchTo().alert().getText()page.on('dialog', dialog => dialog.message())
driver.switchTo().alert().sendKeys("text")page.on('dialog', dialog => dialog.accept('text'))
Important: Register dialog listeners before triggering the dialog action.

10. Dropdowns

5 items
Selenium (Java)Playwright (TypeScript)
new Select(element).selectByValue("IN")await locator.selectOption('IN')
new Select(element).selectByVisibleText("India")await locator.selectOption({ label: 'India' })
new Select(element).selectByIndex(2)await locator.selectOption({ index: 2 })
new Select(element).getOptions()await locator.locator('option').allTextContents()
new Select(element).getFirstSelectedOption().getText()await locator.inputValue()

11. Mouse & Keyboard Actions

7 items
Selenium (Java)Playwright (TypeScript)
new Actions(driver).moveToElement(ele).perform()await locator.hover()
new Actions(driver).doubleClick(ele).perform()await locator.dblclick()
new Actions(driver).contextClick(ele).perform()await locator.click({ button: 'right' })
new Actions(driver).dragAndDrop(src, tgt).perform()await src.dragTo(tgt)
new Actions(driver).scrollToElement(ele).perform()await locator.scrollIntoViewIfNeeded()
new Actions(driver).scrollByAmount(0, 500).perform()await page.mouse.wheel(0, 500)
new Actions(driver).keyDown(Keys.CONTROL).sendKeys("a").keyUp(Keys.CONTROL).perform()await page.keyboard.press('Control+a')

12. Cookies & Storage

4 items
Selenium (Java)Playwright (TypeScript)
driver.manage().getCookies()await context.cookies()
driver.manage().addCookie(new Cookie("k", "v"))await context.addCookies([{ name: 'k', value: 'v', url: '...' }])
driver.manage().deleteAllCookies()await context.clearCookies()
(no built-in storage workflow)await context.storageState({ path: 'auth.json' })
Auth shortcut: Use storageState to save authenticated state and reuse it across tests - massively speeds up test execution.

13. JavaScript Execution

3 items
Selenium (Java)Playwright (TypeScript)
((JavascriptExecutor)driver).executeScript("return document.title")await page.evaluate(() => document.title)
js.executeScript("arguments[0].click()", element)await locator.evaluate(el => el.click())
js.executeScript("window.scrollBy(0, 500)")await page.evaluate(() => window.scrollBy(0, 500))
Prefer Playwright APIs over evaluate() wherever possible. Use native methods like locator.click() instead of JS-based clicking.

14. Configuration & Architecture

12 items
Selenium ConceptPlaywright Equivalent
WebDriver driver = new ChromeDriver()const browser = await chromium.launch()
Driver = one browser instancebrowser → context → page model
ChromeOptions / FirefoxOptionsplaywright.config.ts
Selenium GridSharding (--shard) + workers
testng.xml / JUnit runnernpx playwright test
@DataProvidertest.describe() + fixtures
Page Factory (@FindBy)Locator arrow functions
ExtentReports / AllureBuilt-in HTML, JSON, JUnit reporters
Selenium IDEnpx playwright codegen
WebDriverManagernpx playwright install
SoftAssertexpect.soft(...)
REST AssuredBuilt-in request context

15. Data-Driven Testing

4 items
Selenium (Java)Playwright (TypeScript)Notes
@DataProvider annotationtest.describe + forEachOr parameterize with arrays
Excel files (Apache POI)JSON files + TypeScript typesNo extra dependencies
data.properties + PropertiesReader.env + dotenv + config/index.tsType-safe config
UtilExcel.java for data extractionimport data from './testdata/users.json'Direct JSON import

16. Reporting & Debugging

8 items
SeleniumPlaywrightNotes
Allure / ReportNGBuilt-in HTML + JSON + JUnit reportersOr custom reporter class
RetryAnalyzer (50+ lines)retries: 2 in configOne line!
ScreenshotListener (50+ lines)screenshot: 'only-on-failure'Config option
Log4j for loggingBuilt-in console + custom LoggerTrace viewer replaces most
(no equivalent)Trace Viewer (trace: 'on-first-retry')NEW - Game changer
(external video tools)video: 'retain-on-failure'NEW - Built-in
(no built-in)Playwright Inspector (--debug)NEW
(no built-in)UI Mode (--ui)NEW

Chapter 4Page Objects & Test Conversion

Convert your Page Object Model, eliminate base classes, introduce the Module layer, and transform your test structure.

POM Conversion: LoginPage

Selenium: LoginPage.java
public class LoginPage extends CommonToAllPage {
  private By emailLocator = By.id("login-username");
  private By passwordLocator = By.id("login-password");
  private By loginBtnLocator = By.id("js-login-btn");
  private By errorLocator = By.cssSelector(".error-msg");

  public LoginPage(WebDriver driver) {
    super(driver);
  }

  public void enterEmail(String email) {
    enterInput(emailLocator, email);
  }

  public void enterPassword(String password) {
    enterInput(passwordLocator, password);
  }

  public DashBoardPage clickLogin() {
    clickElement(loginBtnLocator);
    return new DashBoardPage(driver);
  }

  public String getErrorMessage() {
    return getText(errorLocator);
  }
}
Playwright: LoginPage.ts
import { Page, Locator, expect } from '@playwright/test';

export class LoginPage {
  private readonly page: Page;

  // Locators (arrow functions - lazy evaluation)
  readonly emailInput = (): Locator =>
    this.page.getByTestId('login-username');
  readonly passwordInput = (): Locator =>
    this.page.getByTestId('login-password');
  readonly loginButton = (): Locator =>
    this.page.getByRole('button', { name: 'Login' });
  readonly errorMessage = (): Locator =>
    this.page.locator('.error-msg');

  constructor(page: Page) {
    this.page = page;
  }

  async navigate(): Promise<void> {
    await this.page.goto('/login');
  }

  async enterEmail(email: string): Promise<void> {
    await this.emailInput().fill(email);
  }

  async enterPassword(password: string): Promise<void> {
    await this.passwordInput().fill(password);
  }

  async clickLogin(): Promise<void> {
    await this.loginButton().click();
  }

  // Assertions live in the Page Object
  async expectErrorMessage(msg: string): Promise<void> {
    await expect(this.errorMessage()).toHaveText(msg);
  }

  async expectLoginVisible(): Promise<void> {
    await expect(this.loginButton()).toBeVisible();
  }
}
Key changes: (1) No inheritance from base class - Page replaces WebDriver. (2) By locators become arrow function locators. (3) @FindBy annotations not needed. (4) All methods are async. (5) Assertions can live in the page object. (6) Prefer getByTestId / getByRole over CSS selectors.

Base Class Elimination

CommonToAllPage.java (BEFORE)
public class CommonToAllPage {
  WebDriver driver;

  public CommonToAllPage(WebDriver driver) {
    this.driver = driver;
  }

  // These wrapper methods are ALL unnecessary
  // in Playwright:
  public void clickElement(By locator) {
    driver.findElement(locator).click();
  }

  public void enterInput(By locator, String text) {
    driver.findElement(locator).sendKeys(text);
  }

  public String getText(By locator) {
    return driver.findElement(locator).getText();
  }

  public void openVWOUrl() {
    driver.get(PropertiesReader.readKey("vwoURL"));
  }
}

// CommonToAllTest.java
public class CommonToAllTest {
  @BeforeMethod
  public void setUp() {
    DriverManager.init();
  }
  @AfterMethod
  public void tearDown() {
    DriverManager.down();
  }
}
Playwright: NOT NEEDED!
// CommonToAllPage.java is ELIMINATED because:
//
// clickElement(locator)  -> locator.click()
// enterInput(locator, t) -> locator.fill(t)
// getText(locator)       -> locator.textContent()
//
// Playwright's Locator API already provides all of
// these methods with auto-waiting built in.
// No wrapper needed!

// CommonToAllTest.java is ELIMINATED because:
//
// @BeforeMethod setUp() / @AfterMethod tearDown()
// with DriverManager.init() / .down()
//
// Playwright automatically:
// - Creates a fresh browser context per test
// - Provides `page` via fixture injection
// - Cleans up after each test
//
// Your test just declares what it needs:

import { test, expect } from '@playwright/test';

test('login test', async ({ page }) => {
  // `page` is auto-created, auto-cleaned
  // No setup, no teardown, no driver management
  const loginPage = new LoginPage(page);
  await loginPage.navigate();
  await loginPage.enterEmail('[email protected]');
});
Do NOT attempt to recreate CommonToAllPage in Playwright. The Locator API replaces all wrapper methods. Do NOT recreate CommonToAllTest - Playwright fixtures handle setup/teardown automatically.

The Module Layer (New in Playwright)

Modules orchestrate multi-step business workflows using Page Objects. They sit between Pages and Tests:

src/modules/LoginModule.ts
import { Page } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
import { HomePage } from '../pages/HomePage';

export class LoginModule {
  /**
   * Complete login flow with verification
   */
  async doLogin(page: Page, email: string, password: string): Promise<void> {
    const loginPage = new LoginPage(page);
    await loginPage.navigate();
    await loginPage.enterEmail(email);
    await loginPage.enterPassword(password);
    await loginPage.clickLogin();

    // Verify login succeeded
    const homePage = new HomePage(page);
    await homePage.expectAvatarVisible();
  }

  async attemptInvalidLogin(page: Page, email: string, password: string): Promise<string> {
    const loginPage = new LoginPage(page);
    await loginPage.navigate();
    await loginPage.enterEmail(email);
    await loginPage.enterPassword(password);
    await loginPage.clickLogin();
    return await loginPage.errorMessage().textContent() || '';
  }
}
Module Pattern: Modules accept Page as a method parameter (NOT in constructor). This allows better testability and reusability. Pages handle atomic interactions; Modules handle business flows.

Test Structure Conversion

Selenium: TestVWOLogin.java
public class TestVWOLogin extends CommonToAllTest {

  @Test(priority = 1)
  public void testNegativeLogin() {
    LoginPage loginPage = new LoginPage(driver);
    loginPage.openVWOUrl();
    loginPage.enterEmail("[email protected]");
    loginPage.enterPassword("wrong");
    loginPage.clickLogin();
    String error = loginPage.getErrorMessage();
    Assert.assertEquals(error,
      "Your email or password is incorrect.");
  }

  @Test(priority = 2, dependsOnMethods = "testNegativeLogin")
  public void testPositiveLogin() {
    LoginPage loginPage = new LoginPage(driver);
    loginPage.openVWOUrl();
    loginPage.enterEmail(
      PropertiesReader.readKey("username"));
    loginPage.enterPassword(
      PropertiesReader.readKey("password"));
    DashBoardPage dashboard = loginPage.clickLogin();
    String url = driver.getCurrentUrl();
    Assert.assertTrue(url.contains("dashboard"));
  }
}
Playwright: login.spec.ts
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
import { LoginModule } from '../modules/LoginModule';

test.describe('VWO Login Tests', () => {

  test('negative login shows error @P0', async ({ page }) => {
    const loginPage = new LoginPage(page);
    await loginPage.navigate();
    await loginPage.enterEmail('[email protected]');
    await loginPage.enterPassword('wrong');
    await loginPage.clickLogin();
    await loginPage.expectErrorMessage(
      'Your email or password is incorrect.'
    );
  });

  test('valid login reaches dashboard @P0', async ({ page }) => {
    const loginModule = new LoginModule();
    await loginModule.doLogin(
      page,
      process.env.TEST_USERNAME!,
      process.env.TEST_PASSWORD!,
    );
    await expect(page).toHaveURL(/.*dashboard/);
  });
});

Annotation Mapping

TestNG / JUnitPlaywright
@Testtest('name', async ({ page }) => { })
@BeforeSuiteglobalSetup in config
@BeforeClasstest.beforeAll()
@BeforeMethodtest.beforeEach()
@AfterMethodtest.afterEach()
@AfterClasstest.afterAll()
@Test(groups = {"smoke"})test('name @Smoke', ...) + --grep @Smoke
@Test(enabled = false)test.skip('name', ...)
@Test(dependsOnMethods = "x")Tests should be independent (no dependencies)

Chapter 5Waits, Retries & Infrastructure

Migrate wait strategies, retry logic, reporting, data management, and CI/CD pipelines.

Wait Strategy Migration: WaitHelpers.java → (mostly nothing)

WaitHelpers.java (6 methods, ~100 lines)
public class WaitHelpers {
  // 1. Hard wait (BAD)
  public static void waitJVM(int seconds) {
    Thread.sleep(seconds * 1000);
  }

  // 2. Implicit wait
  public static void waitImplicitWait(WebDriver d, int s) {
    d.manage().timeouts()
      .implicitlyWait(Duration.ofSeconds(s));
  }

  // 3. Explicit wait - visibility
  public static void checkVisibility(WebDriver d, By l) {
    new WebDriverWait(d, Duration.ofSeconds(30))
      .until(ExpectedConditions
        .visibilityOfElementLocated(l));
  }

  // 4. Explicit wait - returns element
  public static WebElement visibilityOfElement(
      WebDriver d, By l, int s) {
    return new WebDriverWait(d, Duration.ofSeconds(s))
      .until(ExpectedConditions
        .visibilityOfElementLocated(l));
  }

  // 5. Presence check
  public static WebElement presenceOfElement(
      WebDriver d, By l, int s) {
    return new WebDriverWait(d, Duration.ofSeconds(s))
      .until(ExpectedConditions
        .presenceOfElementLocated(l));
  }

  // 6. Fluent wait
  public static void waitFluentVisibility(
      WebDriver d, By l) {
    new FluentWait<>(d)
      .withTimeout(Duration.ofSeconds(30))
      .pollingEvery(Duration.ofSeconds(2))
      .ignoring(NoSuchElementException.class)
      .until(ExpectedConditions
        .visibilityOfElementLocated(l));
  }
}
Playwright: Auto-Wait replaces ALL of this
// WaitHelpers.java is ELIMINATED because Playwright
// auto-waits before every action.

// 1. Thread.sleep -> REMOVED (auto-wait handles it)
// 2. implicitlyWait -> NOT NEEDED (auto-wait)
// 3. checkVisibility ->
await expect(locator).toBeVisible();
// 4. visibilityOfElement ->
await locator.waitFor({ state: 'visible' });
// 5. presenceOfElement ->
await locator.waitFor({ state: 'attached' });
// 6. FluentWait ->
// Playwright auto-retries assertions. For custom:
import { WaitHelper } from '../utils/WaitHelper';
await WaitHelper.waitForCondition(
  page,
  async () => {
    const count = await page.locator('.item').count();
    return count > 0;
  },
  { timeout: 30000, interval: 2000 }
);

// In practice, 95% of waits are just:
await locator.click();     // auto-waits
await locator.fill('x');   // auto-waits
await expect(loc).toBeVisible(); // auto-retries

// The remaining 5% use:
await page.waitForURL('**/dashboard');
await page.waitForLoadState('networkidle');
await page.waitForResponse('**/api/data');

Retry & Reporting Migration

RetryAnalyzer.java (50 lines) → 1 line of config

RetryAnalyzer.java + ScreenshotListener.java
// RetryAnalyzer.java
public class RetryAnalyzer implements IRetryAnalyzer {
  int counter = 0;
  int retryLimit = 3;

  public boolean retry(ITestResult result) {
    if (counter < retryLimit) {
      counter++;
      return true;
    }
    return false;
  }
}

// ScreenshotListener.java
public class ScreenshotListener implements ITestListener {
  public void onTestFailure(ITestResult result) {
    WebDriver driver = DriverManager.getDriver();
    File src = ((TakesScreenshot) driver)
      .getScreenshotAs(OutputType.FILE);
    String timestamp = new SimpleDateFormat(
      "yyyyMMdd_HHmmss").format(new Date());
    String path = "failure_screenshots/"
      + result.getName() + "_" + timestamp + ".png";
    FileUtils.copyFile(src, new File(path));
    Reporter.log("<a href='" + path + "'>Screenshot</a>");
  }
}
playwright.config.ts (all built-in)
// ALL of the above is replaced by config options:

export default defineConfig({
  // RetryAnalyzer.java -> one line:
  retries: process.env.CI ? 2 : 0,

  // ScreenshotListener.java -> config options:
  use: {
    screenshot: 'only-on-failure',
    video: 'retain-on-failure',
    trace: 'on-first-retry',
  },

  // Allure + ReportNG -> built-in reporters:
  reporter: [
    ['html', { open: 'never' }],
    ['json', { outputFile: 'results.json' }],
    ['list'],
    // Custom reporter (optional):
    ['./src/utils/CustomTTAReporter.ts'],
  ],
});

// That's it. ~100 lines of Java replaced by
// ~10 lines of configuration.

Data-Driven Migration

UtilExcel.java + TestData.xlsx
// UtilExcel.java
public class UtilExcel {
  public static Object[][] getTestDataFromExcel(
      String sheetName) {
    FileInputStream fis = new FileInputStream(
      "src/test/resources/TestData.xlsx");
    XSSFWorkbook workbook = new XSSFWorkbook(fis);
    XSSFSheet sheet = workbook.getSheet(sheetName);
    int rows = sheet.getLastRowNum();
    int cols = sheet.getRow(0).getLastCellNum();
    Object[][] data = new Object[rows][cols];
    for (int i = 1; i <= rows; i++) {
      for (int j = 0; j < cols; j++) {
        data[i-1][j] = sheet.getRow(i)
          .getCell(j).toString();
      }
    }
    return data;
  }
}

// Usage in test:
@DataProvider
public Object[][] loginData() {
  return UtilExcel.getTestDataFromExcel("Login");
}

@Test(dataProvider = "loginData")
public void testLogin(String email, String pass) {
  // test with data...
}
JSON + TypeScript types
// src/testdata/users.json
{
  "validUsers": [
    {
      "email": "[email protected]",
      "password": "SecurePass123",
      "role": "standard"
    },
    {
      "email": "[email protected]",
      "password": "AdminPass456",
      "role": "admin"
    }
  ],
  "invalidUsers": [
    {
      "email": "[email protected]",
      "password": "bad",
      "expectedError": "Invalid credentials"
    }
  ]
}

// src/testdata/types.ts
export interface ValidUser {
  email: string;
  password: string;
  role: string;
}

// Usage in test:
import users from '../testdata/users.json';

for (const user of users.validUsers) {
  test(`login as ${user.role}`, async ({ page }) => {
    // test with user data...
  });
}

Chapter 6AI-Powered Migration

Leverage Playwright MCP, AI codegen tools, and anti-hallucination rules to accelerate your migration from a Selenium base framework into a Playwright architecture that still feels disciplined, reviewable, and production-ready.

Base Framework + MCP Setup

Before you ask AI to generate anything, freeze the target framework shape. The model should migrate into your architecture, not invent a new one on every file. Then use MCP and codegen to accelerate only the repetitive conversion work.

Framework first

Define folders, exports, naming conventions, locator strategy, environment handling, and reporting before the first AI prompt.

AI second

Use AI to generate draft code for API clients, pages, modules, tests, and fixes only after the rules file is in place.

Human review always

Keep a manual pass for flaky waits, test intent, selector quality, reporting, and business-flow correctness.

Base Framework + AI Migration Flow └─ source-java-framework/ ATB13x Selenium framework └─ target-playwright-framework/ Advance Playwright framework ├─ src/api/ Typed API clients first ├─ src/pages/ Atomic page objects ├─ src/modules/ Reusable business flows ├─ src/tests/ Spec files only ├─ CLAUDE.md / .cursorrules AI guardrails └─ MCP + Codegen Acceleration, not architecture
Rule of thumb: never let the model decide your framework design. Freeze the framework with base rules, then let AI migrate files into that framework.

Playwright MCP Setup

The Playwright MCP server provides browser automation tools that let AI agents interact with web pages using structured accessibility snapshots.

Claude Code Integration
# Add Playwright MCP to Claude Code
claude mcp add playwright npx @playwright/mcp@latest

# This registers the MCP server and persists in ~/.claude.json
# Now Claude Code can navigate, click, type, take screenshots,
# and read page accessibility trees.
VS Code / Cursor MCP Config (mcp.json)
{
  "mcpServers": {
    "playwright": {
      "command": "npx",
      "args": ["@playwright/mcp@latest"]
    }
  }
}

What Playwright MCP Provides

browser_navigate

Navigate to URLs

browser_click

Click elements by accessibility ref

browser_type

Type text into inputs

browser_snapshot

Get accessibility tree (NOT screenshots)

browser_screenshot

Visual verification

browser_tabs

Manage browser tabs

Playwright Codegen

Quick test scaffolding
# Record user interactions and generate test code
npx playwright codegen https://your-app.com

# Record with specific viewport
npx playwright codegen --viewport-size=1280,720 https://your-app.com

# Record with device emulation
npx playwright codegen --device="iPhone 13" https://your-app.com
Best practice: Use codegen for quick drafts, then refine with proper POM patterns. Use MCP for AI-assisted generation with live DOM context.

AI Prompt Templates

Copy-paste these prompts to your AI tool for each migration phase. They are designed to prevent hallucination by providing explicit rules and constraints.

Planner Prompt Phase 0: Analysis
I want to migrate a Java/Selenium test case to TypeScript/Playwright.

The test case is:
- File: [path to Java test file]
- Method: [test method name]
- Test ID: [test ID]

Please analyze this test case and provide:
1. A summary of what the test does
2. All Java classes used (page objects, modules, API clients)
3. The test flow (step-by-step)
4. Any API calls made
5. Any preconditions or setup required

RULES:
- Do NOT generate any Playwright code yet
- Only analyze and plan
- List every Java file that needs a TypeScript equivalent
- Identify the migration order (API -> Pages -> Modules -> Tests)
Generator Prompt Phase 1-4: Implementation
Convert this Java/Selenium [page object|module|test] to TypeScript/Playwright.

Source file: [paste Java code]

STRICT RULES:
1. Pages: Accept `Page` in constructor, use arrow function locators
2. Modules: Accept `Page` as METHOD parameter (NOT constructor)
3. API clients: Accept `APIRequestContext` (NOT Page)
4. Locators: Use getByTestId or getByRole (NOT XPath, NOT CSS class)
5. Max 50 locators per page object
6. All methods must be async and return Promise
7. Use TypeScript interfaces for data structures
8. Add barrel exports (index.ts) for each directory
9. NO Thread.sleep equivalents - rely on auto-wait
10. Use expect() for assertions (NOT manual checks)

TARGET FRAMEWORK:
- @playwright/test v1.57+
- TypeScript strict mode
- Path aliases: @pages/*, @modules/*, @utils/*

Do NOT:
- Create base classes or helper wrappers for click/fill/getText
- Use `any` type - use proper TypeScript types
- Hardcode URLs or credentials - use environment variables
- Add unnecessary waits - Playwright auto-waits
Healer Prompt Phase 5: Debugging
The migrated Playwright test is failing with this error:
[paste error]

Test output shows:
[paste console logs]

Please fix this issue while maintaining ALL framework rules:
- Pages accept Page in constructor
- Modules accept Page as method parameter
- API clients accept APIRequestContext
- Use getByTestId/getByRole locators
- No base class wrappers
- All methods async with Promise return type

Common fixes to check:
1. Missing await keywords
2. Wrong locator strategy (CSS vs data-testid)
3. Missing headers on API calls
4. Timing issues (add explicit wait only if needed)
5. Zod schema mismatch with actual API response
Reviewer Prompt Quality Check
Review this migrated Playwright code for anti-patterns:

[paste TypeScript code]

Check for:
1. Page stored in Module constructor (should be method param)
2. Page passed to API client (should be APIRequestContext)
3. XPath or class-based selectors (should use data-testid/role)
4. Hardcoded URLs or credentials
5. `any` type usage
6. Missing async/await
7. Unnecessary waits (waitForTimeout, etc.)
8. More than 50 locators in one page
9. Missing barrel exports
10. Base class wrappers that replicate Locator API

Anti-Hallucination Rules Files

Place these files in your project root to guide AI tools. They prevent common migration mistakes by encoding framework rules directly into the AI's context.

CLAUDE.md (project root)
# Playwright Migration Framework Rules

## Architecture
- 3-layer: Pages (locators + actions) -> Modules (business flows) -> Tests (specs)
- Pages accept `Page` in constructor
- Modules accept `Page` as METHOD parameter (NOT constructor)
- API clients accept `APIRequestContext` (NOT Page)

## Locators
- ALWAYS prefer: getByTestId, getByRole, getByLabel, getByText
- NEVER use: XPath, CSS class selectors
- Max 50 locators per page object
- Use arrow functions: `readonly btn = (): Locator => this.page.getByTestId('x')`

## TypeScript
- Strict mode enabled
- NO `any` type - use proper types or `unknown`
- All browser methods must be async with Promise return
- Use interfaces for data structures
- Single quotes, trailing commas

## Waits
- NEVER use Thread.sleep / waitForTimeout (except debugging)
- Rely on auto-wait for click/fill/check actions
- Use expect() assertions for waiting on conditions
- Only use explicit waits for network/URL changes

## Testing
- Use `test()` from @playwright/test (not custom wrappers)
- Tests must be independent (no dependsOn)
- Use @P0/@Smoke tags in test name for filtering
- Environment variables for credentials (never hardcode)

## DO NOT
- Create base classes that wrap Locator API methods
- Store Page in Module constructors
- Pass Page to API client classes
- Use @FindBy or PageFactory patterns
- Add unnecessary helper utilities that Playwright already provides
.cursorrules (project root)
You are a Playwright TypeScript test automation expert migrating from Selenium Java.

ARCHITECTURE:
- Pages: src/pages/*.ts - Locators + atomic actions. Constructor takes Page.
- Modules: src/modules/*.ts - Business workflows. Methods take Page as parameter.
- Tests: src/tests/*.spec.ts - Feature specs using Playwright test().
- Fixtures: src/fixtures/*.ts - Auth, context setup.
- API: src/api/*.ts - REST clients. Constructor takes APIRequestContext.

LOCATOR PRIORITY (highest to lowest):
1. page.getByTestId('x') - most stable
2. page.getByRole('button', { name: 'Submit' })
3. page.getByLabel('Email')
4. page.getByText('Sign In')
5. page.locator('[data-testid="x"]') - CSS fallback
6. NEVER: XPath, class-based selectors

CODE STYLE:
- TypeScript strict mode, single quotes, trailing commas
- Arrow function locators: readonly x = (): Locator => this.page.getByTestId('y')
- All browser ops async with await
- Barrel exports (index.ts) for every directory
- Max 50 locators per page

FORBIDDEN PATTERNS:
- Base classes wrapping click/fill/getText (Locator API handles this)
- Page stored in Module constructor
- Page passed to API clients
- any type usage
- Thread.sleep / waitForTimeout
- Hardcoded URLs or credentials
.github/copilot-instructions.md
# Playwright Framework Guidelines for GitHub Copilot

## When generating Page Objects:
- Constructor takes `Page` from @playwright/test
- Use arrow function locators returning `Locator`
- Prefer getByTestId() and getByRole() over CSS
- Include async action methods returning Promise
- Max 50 locators per file

## When generating Modules:
- Do NOT store Page in constructor
- Accept Page as first parameter in each method
- Compose Page Objects inside methods
- Add comprehensive JSDoc with @example

## When generating Tests:
- Import { test, expect } from '@playwright/test'
- Use test.describe() for grouping
- Add tags: @P0, @Smoke, @Regression in test name
- Tests must be independent, no shared state
- Use expect() for all assertions (auto-retry)

## NEVER generate:
- CommonToAllPage base class equivalents
- DriverManager or driver setup code
- Thread.sleep or waitForTimeout calls
- XPath locators or class-based CSS selectors
- Hardcoded credentials or URLs
.windsurfrules (project root)
Framework: Playwright with TypeScript (strict mode)
Test Runner: @playwright/test

Layer Rules:
- src/pages/ = Page Objects (constructor: Page, locators: arrow functions)
- src/modules/ = Business flows (methods take Page param, NOT constructor)
- src/tests/ = Specs (use test() from @playwright/test)
- src/api/ = API clients (constructor: APIRequestContext, NOT Page)
- src/fixtures/ = Test fixtures (auth, data setup)

Locator Rules:
- Use: getByTestId, getByRole, getByLabel, getByText, getByPlaceholder
- Avoid: XPath, class-based CSS, fragile selectors
- Format: readonly name = (): Locator => this.page.getByTestId('x')
- Limit: Max 50 locators per page object

Wait Rules:
- Playwright auto-waits on all actions (click, fill, check)
- Use expect() assertions for condition waiting (auto-retry)
- ONLY use explicit waits for: URL changes, network responses
- NEVER use: waitForTimeout, Thread.sleep equivalents

Code Style: single quotes, trailing commas, no semicolons optional
Exports: Barrel exports (index.ts) in every directory
Types: No `any` - use proper interfaces or `unknown`

AI Migration Workflow

1. Planner Agent

Analyzes Java test, lists all dependencies, creates migration plan with component mapping table.

Input: Java test file

Output: Migration plan doc

2. Generator Agent

Converts Java code to TypeScript following strict framework rules. Bottom-up: API → Pages → Modules → Tests.

Input: Java code + rules

Output: TypeScript files

3. Healer Agent

Fixes errors from typecheck/lint/runtime. Patches code while maintaining all framework rules.

Input: Errors + code

Output: Fixed code

Real-world result: Using this AI workflow, a complex test case (DRP_GPX_1318) was migrated in ~8 hours: 42 files created, 8,153 lines of code, 0 TypeScript errors. AI achieved ~65-70% migration; remaining fixes needed human-in-the-loop debugging.

Chapter 7Quick Reference & Verification

Dos and Don'ts, troubleshooting guide, verification checklists, and command reference.

Dos and Don'ts

DO

  • ✔ Follow bottom-up order: API → Pages → Modules → Tests
  • ✔ Use getByTestId / getByRole for locators
  • ✔ Use environment variables for credentials
  • ✔ Create barrel exports (index.ts) for every dir
  • ✔ Add TypeScript interfaces for data structures
  • ✔ Use expect() for all assertions (auto-retry)
  • ✔ Verify after each phase: npm run typecheck && npm run lint
  • ✔ Use faker for dynamic test data
  • ✔ Accept Page as method param in Modules
  • ✔ Accept APIRequestContext in API clients

DON'T

  • ✘ Pass Page to API client constructors
  • ✘ Store Page in Module constructors
  • ✘ Use XPath or class-based CSS selectors
  • ✘ Hardcode URLs or credentials
  • ✘ Use any type - use proper types
  • ✘ Skip error handling on API calls
  • ✘ Exceed 50 locators per page object
  • ✘ Use double quotes (use single quotes)
  • ✘ Forget trailing commas in objects/arrays
  • ✘ Create base classes wrapping Locator API

Troubleshooting

ErrorCauseFix
Authentication failed: 404Wrong endpoint URL or HTTP methodVerify URL with cURL, ensure POST method
400 Bad Request: "no app name in header"Missing required headersAdd clientid, appname, content-type headers
Locator '[data-testid="x"]' not foundWrong selector or element in iframeInspect page, check iframes, update selectors
ZodError: invalid_typeAPI response doesn't match schemaLog response, update Zod schema to match
Test timeout of 30000ms exceededSlow page load or wrong waittest.setTimeout(120000) or add explicit waits
SALESCLOUD_USERNAME is undefined.env file missing or not loadedCreate .env, add import 'dotenv/config'
Cannot find module '@pages/...'Missing barrel exports or tsconfig pathsCreate index.ts, verify tsconfig paths
Missing trailing commaESLint comma-dangle ruleAdd trailing commas to all multi-line objects

Migration Verification Checklist

Pre-Migration

  • Source Java test case identified and analyzed
  • Migration plan created with component mapping
  • Target repository set up with Playwright
  • Dependencies installed (npm install)
  • Rules files added (CLAUDE.md / .cursorrules)

API Layer

  • API endpoints identified from Java code
  • API client classes created (APIRequestContext)
  • TypeScript types / Zod schemas created
  • Barrel exports created
  • npm run typecheck passes

Page Objects

  • All page objects converted (no base class)
  • Locators use getByTestId / getByRole
  • Max 50 locators per page
  • Barrel exports created
  • npm run typecheck passes

Modules & Tests

  • Modules accept Page as method parameter
  • Test spec created with proper structure
  • All assertions use expect() (auto-retry)
  • Tags added (@P0, @Smoke, @Regression)
  • npm run typecheck && npm run lint passes

Final Verification

  • Test runs in headed mode without errors
  • All assertions pass
  • No TypeScript errors
  • Changes committed with comprehensive message

Command Reference

CommandPurpose
npx playwright testRun all tests
npx playwright test --headedRun with visible browser
npx playwright test --uiInteractive UI mode
npx playwright test --debugDebug with Playwright Inspector
npx playwright test --grep @P0Run only P0 tagged tests
npx playwright test --project=chromiumRun on specific browser
npx playwright test --shard=1/4Run 1st of 4 shards
npx playwright show-reportOpen HTML test report
npx playwright codegen URLRecord and generate tests
npx playwright installInstall browsers
npm run typecheckTypeScript type checking
npm run lintESLint checking

Tutorial Complete!

Created by Pramod Dutta - The Testing Academy

Reference repos: ATB13xSeleniumAdvanceFramework → Advance-Playwright-Framework

Use AI to accelerate the migration, but keep framework design, final debugging, and review in human hands.

GitHub · LinkedIn · TheTestingAcademy