| name | migration-patterns |
| description | Framework migration guide for moving from Puppeteer, Selenium, or Cypress to Playwright. Use when planning or executing incremental test migration, checking code reusability, or organizing migrated tests by business domain.
|
Migration Patterns Skill
Guide for migrating test automation frameworks to Playwright with TypeScript, with special focus on incremental migration and code reusability.
Table of Contents
Migration Strategy
Core Principles
- Migrate incrementally - One test at a time, one domain at a time
- Check for reusability - Before creating new code, check if it already exists
- Organize by business domain - Group tests logically, not by file type
- Track progress - Use migration log to avoid duplicates
- Maintain business logic - Don't lose important domain knowledge
The Migration Process
┌─────────────────────────────────────────┐
│ 1. Pre-Migration Analysis │
│ - Review what's already migrated │
│ - Identify reusable components │
│ - Determine correct location │
└──────────────┬──────────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ 2. Migration │
│ - Convert selectors │
│ - Update assertions │
│ - Follow best practices │
└──────────────┬──────────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ 3. Post-Migration Review │
│ - Update migration log │
│ - Document reusable components │
│ - Verify business logic preserved │
└─────────────────────────────────────────┘
Incremental Migration
Day-by-Day Workflow
Before Starting Each Day
-
Review Migration Log
- What was migrated yesterday?
- What page objects were created?
- What utilities were added?
-
Plan Today's Migration
- Which tests are candidates?
- Which test shares most code with yesterday's work?
- What domain are we focusing on?
Example: Pharmacy Domain Migration
Day 1 - Foundation
Day 2 - Building On Day 1 ⚠️ CRITICAL: Reuse existing code!
import { PrescriptionSearchPage } from '../../page-objects/pharmacy/PrescriptionSearchPage';
import { createTestPrescription } from '../../utils/pharmacy/prescription-helpers';
test('refill prescription flow', async ({ page }) => {
const searchPage = new PrescriptionSearchPage(page);
const prescription = await createTestPrescription(page, {
medication: 'Lisinopril 10mg',
refillsRemaining: 3
});
await searchPage.navigate();
await searchPage.searchById(prescription.id);
await searchPage.clickRefill(prescription.id);
const refillPage = new RefillConfirmationPage(page);
await refillPage.confirmRefill();
await expect(page.getByText('Refill submitted')).toBeVisible();
});
❌ BAD: Creating duplicates
Pre-Migration Checklist
Before migrating ANY test, complete this checklist:
## Pre-Migration Checklist for [Test Name]
### 1. Code Reusability Analysis
- [ ] Searched /page-objects/ for existing page objects
- [ ] Searched /utils/ for existing helpers
- [ ] Reviewed yesterday's migration log
- [ ] Identified what can be REUSED vs what needs to be CREATED
### 2. Business Domain Identification
- [ ] Determined which domain this test belongs to (pharmacy, billing, etc.)
- [ ] Checked if similar tests exist in that domain folder
- [ ] Identified which user flow this test covers
### 3. Location Planning
- [ ] Determined correct folder: /tests/[domain]/[sub-category]/
- [ ] Checked for naming conflicts
- [ ] Verified folder structure matches our standards
### 4. Dependencies Identification
- [ ] Listed all pages this test touches
- [ ] Listed all API calls this test makes
- [ ] Listed all test data requirements
### 5. Business Logic Verification
- [ ] Understood the business purpose of this test
- [ ] Identified critical business rules being tested
- [ ] Noted any complex workflows or edge cases
Reusability Checks
How to Check for Existing Code
1. Search Existing Page Objects
grep -r "class.*Prescription.*Page" page-objects/
grep -r "searchByMedication\|searchById" page-objects/
2. Review Migration Log
# Check templates/migration-tracker/MIGRATION-LOG.md
## January 30, 2026
### Migrated: create-prescription.spec.ts
**Business Domain:** Pharmacy > Prescription Management
**Created Files:**
- page-objects/pharmacy/PrescriptionSearchPage.ts
- page-objects/pharmacy/PrescriptionFormPage.ts
- utils/pharmacy/prescription-helpers.ts
**Reusable Components:**
- `PrescriptionSearchPage.searchByMedication(name: string)`
- `PrescriptionSearchPage.searchById(id: string)`
- `createTestPrescription(page, options)`
3. AI-Assisted Reusability Check
Prompt Template:
Using the migration-patterns skill:
Before migrating [test name], analyze our codebase:
1. Search /page-objects/pharmacy/ for existing prescription-related page objects
2. Search /utils/pharmacy/ for existing helpers
3. Review /tests/pharmacy/ for similar test flows
4. Identify what can be REUSED vs what needs to be CREATED
Then show me:
- List of existing components that can be reused
- List of new components that need to be created
- Recommended file locations
Decision Tree: Create vs Reuse
Does similar functionality exist?
│
├─ YES
│ └─ Can I extend the existing class/function?
│ ├─ YES → EXTEND existing code
│ └─ NO → Is it truly different?
│ ├─ YES → CREATE new, document why
│ └─ NO → REFACTOR existing to be reusable
│
└─ NO
└─ CREATE new code
Example: Extending vs Creating New
export class BasePrescriptionPage {
constructor(protected page: Page) {}
protected async selectMedication(name: string) {
await this.page.getByLabel('Medication').fill(name);
}
}
export class CreatePrescriptionPage extends BasePrescriptionPage {
async create(medication: string, dosage: string) {
await this.selectMedication(medication);
await this.page.getByLabel('Dosage').fill(dosage);
await this.page.getByRole('button', { name: 'Create' }).click();
}
}
export class RefillPrescriptionPage extends BasePrescriptionPage {
async refill(prescriptionId: string) {
await this.searchById(prescriptionId);
await this.page.getByRole('button', { name: 'Refill' }).click();
}
}
Domain Organization
Folder Structure by Business Domain
tests/
├── pharmacy/
│ ├── prescription-flow/
│ │ ├── create-prescription.spec.ts (Day 1)
│ │ ├── refill-prescription.spec.ts (Day 2)
│ │ ├── cancel-prescription.spec.ts (Day 3)
│ │ └── transfer-prescription.spec.ts (Day 4)
│ ├── medication-search/
│ │ ├── search-by-name.spec.ts
│ │ └── search-by-ndc.spec.ts
│ └── inventory/
│ ├── stock-check.spec.ts
│ └── reorder-alerts.spec.ts
├── patient-portal/
│ ├── appointments/
│ │ ├── schedule-appointment.spec.ts
│ │ ├── cancel-appointment.spec.ts
│ │ └── reschedule-appointment.spec.ts
│ ├── medical-records/
│ │ ├── view-records.spec.ts
│ │ └── download-records.spec.ts
│ └── messaging/
│ ├── send-message.spec.ts
│ └── read-messages.spec.ts
└── billing/
├── payments/
│ ├── add-payment-method.spec.ts
│ ├── make-payment.spec.ts
│ └── view-payment-history.spec.ts
└── insurance/
├── add-insurance.spec.ts
└── verify-coverage.spec.ts
page-objects/
├── pharmacy/
│ ├── PrescriptionSearchPage.ts
│ ├── PrescriptionFormPage.ts
│ ├── RefillConfirmationPage.ts
│ └── MedicationDetailsPage.ts
├── patient-portal/
│ ├── AppointmentSchedulerPage.ts
│ ├── MedicalRecordsPage.ts
│ └── MessagingPage.ts
└── billing/
├── PaymentMethodsPage.ts
└── InsuranceFormPage.ts
utils/
├── pharmacy/
│ ├── prescription-helpers.ts
│ ├── medication-data.ts
│ └── pharmacy-api-helpers.ts
├── patient-portal/
│ ├── appointment-helpers.ts
│ └── patient-data.ts
└── billing/
├── payment-helpers.ts
└── insurance-helpers.ts
Determining Where a Test Belongs
Ask these questions:
- What business domain? → Top-level folder (pharmacy, billing, etc.)
- What user workflow? → Sub-folder (prescription-flow, payments, etc.)
- What specific action? → File name (refill-prescription.spec.ts)
Example Decision Process:
Test: User refills a prescription
1. Business Domain: Pharmacy
→ /tests/pharmacy/
2. User Workflow: Prescription management flow
→ /tests/pharmacy/prescription-flow/
3. Specific Action: Refilling
→ /tests/pharmacy/prescription-flow/refill-prescription.spec.ts
Framework-Specific Guides
Puppeteer → Playwright
Selector Migration
await page.click('#submit-button');
await page.waitForSelector('.success-message');
await page.getByRole('button', { name: 'Submit' }).click();
await expect(page.getByRole('alert')).toBeVisible();
Wait Migration
await page.waitFor(2000);
await page.waitForSelector('#loading', { hidden: true });
await page.getByRole('progressbar').waitFor({ state: 'hidden' });
Navigation Migration
await page.goto('https://example.com');
await page.waitForNavigation();
await page.goto('https://example.com');
Assertion Migration
const text = await page.$eval('.message', el => el.textContent);
expect(text).toBe('Success');
await expect(page.getByRole('status')).toHaveText('Success');
Selenium → Playwright
driver.findElement(By.id('email')).sendKeys('test@example.com');
driver.findElement(By.id('submit')).click();
WebDriverWait wait = new WebDriverWait(driver, 10);
wait.until(ExpectedConditions.visibilityOfElementLocated(By.className('success')));
await page.getByLabel('Email').fill('test@example.com');
await page.getByRole('button', { name: 'Submit' }).click();
await expect(page.getByRole('alert')).toBeVisible();
Common Migration Challenges
Challenge 1: Handling Dynamic Content
Old Framework (Puppeteer):
await page.waitFor(3000);
const items = await page.$$('.prescription-item');
Playwright Solution:
await page.getByRole('progressbar').waitFor({ state: 'hidden' });
await page.getByRole('article').first().waitFor();
const items = await page.getByRole('article').all();
Challenge 2: File Uploads
Old Framework:
const fileInput = await page.$('input[type="file"]');
await fileInput.uploadFile('/path/to/file.pdf');
Playwright Solution:
await page.getByLabel('Upload prescription').setInputFiles('/path/to/file.pdf');
await page.locator('input[type="file"]').setInputFiles('/path/to/file.pdf');
Challenge 3: Multiple Tabs/Windows
Old Framework (Puppeteer):
const newPagePromise = new Promise(resolve =>
browser.once('targetcreated', target => resolve(target.page()))
);
await page.click('a[target="_blank"]');
const newPage = await newPagePromise;
Playwright Solution:
const [newPage] = await Promise.all([
page.context().waitForEvent('page'),
page.getByRole('link', { name: 'Open in new tab' }).click()
]);
Challenge 4: Preserving Business Logic
When migrating, document the business purpose:
test('test prescription', async ({ page }) => {
await page.goto('/prescriptions');
await page.click('#refill-123');
});
test('should allow refill only when prescription has remaining refills', async ({ page }) => {
const prescription = await createTestPrescription(page, {
medication: 'Lisinopril 10mg',
refillsRemaining: 2
});
const searchPage = new PrescriptionSearchPage(page);
await searchPage.navigate();
await searchPage.searchById(prescription.id);
await expect(searchPage.getRefillButton(prescription.id)).toBeEnabled();
await searchPage.clickRefill(prescription.id);
await expect(page.getByText('Refill submitted successfully')).toBeVisible();
});
Migration Log Template
See templates/migration-tracker/MIGRATION-LOG-TEMPLATE.md for the complete template.
Quick Reference
Before Migrating ANY Test:
- ✅ Check migration log for similar tests
- ✅ Search for existing page objects
- ✅ Search for existing utilities
- ✅ Determine business domain and location
- ✅ Understand business logic being tested
During Migration:
- ✅ Use accessible selectors (getByRole, getByLabel)
- ✅ Remove hard-coded waits
- ✅ Add proper assertions
- ✅ Create page objects for reusability
- ✅ Document business logic with comments
After Migration:
- ✅ Update migration log
- ✅ Document reusable components
- ✅ Run tests to verify functionality
- ✅ Review with team if complex business logic involved
Related Resources