Selenium → Playwright
Chapters
Covering end-to-end migration
Cheat Sheet Categories
Side-by-side code mappings
Framework Layers
Pages, modules, tests with clear separation
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.
Existing Selenium base framework patterns
Typed Playwright framework with modules and fixtures
Realistic migration acceleration with human review
Locator quality, waits, test design, CI, and debugging
Created by Pramod Dutta
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
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
| Feature | Selenium (Java) | Playwright (TypeScript) |
|---|---|---|
| Protocol | WebDriver (W3C) | CDP / BiDi (direct) |
| Auto-Wait | None (manual waits) | Built-in actionability checks |
| Parallel Execution | Grid + TestNG threads | Built-in workers + sharding |
| Browser Management | Separate drivers (ChromeDriver) | Bundled (npx playwright install) |
| Test Isolation | Manual (ThreadLocal) | Built-in (BrowserContext per test) |
| Debugging | Breakpoints + logs | Inspector + Trace Viewer + UI Mode |
| Reporting | Allure / ReportNG (plugin) | Built-in HTML + JSON + JUnit |
| API Testing | REST Assured (separate) | Built-in APIRequestContext |
| Network Mocking | 3rd party tools | Built-in page.route() |
| Video Recording | External tools | Built-in video support |
| Visual Testing | Applitools / add-ons | Built-in toHaveScreenshot() |
| Mobile Emulation | Appium (separate) | Built-in device emulation |
| AI Integration | Limited | MCP Server + Codegen + AI Agents |
| Retry Logic | Custom RetryAnalyzer | Built-in retries config |
| Soft Assertions | AssertJ / custom | Built-in expect.soft() |
Architecture Comparison
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
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
TypeScript Quick Reference for Java Developers
| Java | TypeScript | Notes |
|---|---|---|
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/finally | try/catch/finally | Same + async error handling |
import com.x.LoginPage; | import { LoginPage } from './pages'; | ES module imports |
null | null | undefined | Two null-ish values in TS |
Folder Structure Side-by-Side
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
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
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
Pilot Critical Paths
Pick 5-10 high-value P0 Selenium tests. Stand up Playwright project with conventions.
Parallel Run & Baseline
Keep Selenium running. Add Playwright CI job. Compare flake rate and duration.
Scale Conversion
All new features in Playwright only. Convert flaky/slow Selenium tests first.
Sunset Selenium
Retire legacy TestNG suites once coverage & stability thresholds are met.
Config → Utilities → Pages → Modules → Tests → CI. This ensures dependencies exist before consumers.Migration Priority Table
| # | Action | Why |
|---|---|---|
| 1 | Replace locators first | Improves stability the most |
| 2 | Remove unnecessary waits | Reduces flaky timing logic |
| 3 | Convert assertions to expect | Enables auto-retry and reliability |
| 4 | Rework frames, tabs, dialogs | Simplifies control flow |
| 5 | Consolidate config & reporting | Leads 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 Dependency | Version | Playwright Equivalent | Notes |
|---|---|---|---|
| selenium-java | 4.35.0 | @playwright/test | Core framework |
| testng | 7.11.0 | Built-in test runner | No extra package needed |
| allure-testng | 2.26.0 | Built-in reporters | HTML, JSON, JUnit built-in |
| reportng | 1.1.2 | Built-in HTML reporter | Or custom reporter |
| assertj-core | 3.25.1 | Built-in expect() | Web-first assertions with auto-retry |
| log4j-core | 3.0.0 | Built-in console / custom Logger | Trace viewer replaces most logging |
| poi + poi-ooxml | 5.2.4 | JSON files + TypeScript types | No Excel needed |
| WebDriverManager | - | npx playwright install | Bundled browser management |
| REST Assured | - | Built-in APIRequestContext | API testing included |
@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
<!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>
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'] } },
],
});
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 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 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
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
Docker Support
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-JUnit | Playwright (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 |
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") // append | await locator.pressSequentially('text') | Simulates keystrokes |
| (checkbox handling indirect) | await locator.check() / uncheck() | NEW |
| (file upload via sendKeys path) | await locator.setInputFiles('file.pdf') | NEW |
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 trigger | Event-based |
Thread.sleep(3000) | await page.waitForTimeout(3000) // AVOID | Avoid unless debugging |
| (no equivalent) | await page.waitForLoadState('networkidle') | NEW |
| (no equivalent) | await page.waitForResponse('**/api/data') | NEW - Network-aware |
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 object | No handle needed |
driver.getWindowHandles() | context.pages() | Returns Page[] |
driver.switchTo().window(handle) | Use the target page object directly | No switching needed |
driver.switchTo().newWindow(WindowType.TAB) | const newPage = await context.newPage() | |
driver.close() | await page.close() |
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')) |
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' }) |
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)) |
evaluate() wherever possible. Use native methods like locator.click() instead of JS-based clicking.14. Configuration & Architecture
12 items ▼| Selenium Concept | Playwright Equivalent |
|---|---|
| WebDriver driver = new ChromeDriver() | const browser = await chromium.launch() |
| Driver = one browser instance | browser → context → page model |
| ChromeOptions / FirefoxOptions | playwright.config.ts |
| Selenium Grid | Sharding (--shard) + workers |
| testng.xml / JUnit runner | npx playwright test |
| @DataProvider | test.describe() + fixtures |
| Page Factory (@FindBy) | Locator arrow functions |
| ExtentReports / Allure | Built-in HTML, JSON, JUnit reporters |
| Selenium IDE | npx playwright codegen |
| WebDriverManager | npx playwright install |
| SoftAssert | expect.soft(...) |
| REST Assured | Built-in request context |
15. Data-Driven Testing
4 items ▼| Selenium (Java) | Playwright (TypeScript) | Notes |
|---|---|---|
@DataProvider annotation | test.describe + forEach | Or parameterize with arrays |
| Excel files (Apache POI) | JSON files + TypeScript types | No extra dependencies |
| data.properties + PropertiesReader | .env + dotenv + config/index.ts | Type-safe config |
| UtilExcel.java for data extraction | import data from './testdata/users.json' | Direct JSON import |
16. Reporting & Debugging
8 items ▼| Selenium | Playwright | Notes |
|---|---|---|
| Allure / ReportNG | Built-in HTML + JSON + JUnit reporters | Or custom reporter class |
| RetryAnalyzer (50+ lines) | retries: 2 in config | One line! |
| ScreenshotListener (50+ lines) | screenshot: 'only-on-failure' | Config option |
| Log4j for logging | Built-in console + custom Logger | Trace 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
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);
}
}
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();
}
}
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
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();
}
}
// 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]');
});
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:
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() || '';
}
}
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
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"));
}
}
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 / JUnit | Playwright |
|---|---|
@Test | test('name', async ({ page }) => { }) |
@BeforeSuite | globalSetup in config |
@BeforeClass | test.beforeAll() |
@BeforeMethod | test.beforeEach() |
@AfterMethod | test.afterEach() |
@AfterClass | test.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)
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));
}
}
// 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
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>");
}
}
// 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
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...
}
// 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.
Playwright MCP Setup
The Playwright MCP server provides browser automation tools that let AI agents interact with web pages using structured accessibility snapshots.
# 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.
{
"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
# 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
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.
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)
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
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
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.
# 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
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
# 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
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
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/getByRolefor 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
fakerfor dynamic test data - ✔ Accept
Pageas method param in Modules - ✔ Accept
APIRequestContextin API clients
DON'T
- ✘ Pass
Pageto API client constructors - ✘ Store
Pagein Module constructors - ✘ Use XPath or class-based CSS selectors
- ✘ Hardcode URLs or credentials
- ✘ Use
anytype - 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
| Error | Cause | Fix |
|---|---|---|
Authentication failed: 404 | Wrong endpoint URL or HTTP method | Verify URL with cURL, ensure POST method |
400 Bad Request: "no app name in header" | Missing required headers | Add clientid, appname, content-type headers |
Locator '[data-testid="x"]' not found | Wrong selector or element in iframe | Inspect page, check iframes, update selectors |
ZodError: invalid_type | API response doesn't match schema | Log response, update Zod schema to match |
Test timeout of 30000ms exceeded | Slow page load or wrong wait | test.setTimeout(120000) or add explicit waits |
SALESCLOUD_USERNAME is undefined | .env file missing or not loaded | Create .env, add import 'dotenv/config' |
Cannot find module '@pages/...' | Missing barrel exports or tsconfig paths | Create index.ts, verify tsconfig paths |
Missing trailing comma | ESLint comma-dangle rule | Add 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 typecheckpasses
Page Objects
- All page objects converted (no base class)
- Locators use getByTestId / getByRole
- Max 50 locators per page
- Barrel exports created
-
npm run typecheckpasses
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 lintpasses
Final Verification
- Test runs in headed mode without errors
- All assertions pass
- No TypeScript errors
- Changes committed with comprehensive message
Command Reference
| Command | Purpose |
|---|---|
npx playwright test | Run all tests |
npx playwright test --headed | Run with visible browser |
npx playwright test --ui | Interactive UI mode |
npx playwright test --debug | Debug with Playwright Inspector |
npx playwright test --grep @P0 | Run only P0 tagged tests |
npx playwright test --project=chromium | Run on specific browser |
npx playwright test --shard=1/4 | Run 1st of 4 shards |
npx playwright show-report | Open HTML test report |
npx playwright codegen URL | Record and generate tests |
npx playwright install | Install browsers |
npm run typecheck | TypeScript type checking |
npm run lint | ESLint 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.