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
- •Incremental Migration
- •Reusability Checks
- •Domain Organization
- •Framework-Specific Guides
- •Common Migration Challenges
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
// Migrated: create-prescription.spec.ts // Created: // - PrescriptionSearchPage (page-objects/pharmacy/) // - PrescriptionFormPage (page-objects/pharmacy/) // - createTestPrescription() (utils/pharmacy/)
Day 2 - Building On Day 1 ⚠️ CRITICAL: Reuse existing code!
// Migrating: refill-prescription.spec.ts
// ✅ BEFORE migrating, check:
// 1. Does PrescriptionSearchPage already exist? YES - REUSE IT!
// 2. Is there a prescription helper? YES - createTestPrescription() exists!
// 3. Where should this test live? /tests/pharmacy/prescription-flow/
// ✅ GOOD: Reuse existing code
import { PrescriptionSearchPage } from '../../page-objects/pharmacy/PrescriptionSearchPage';
import { createTestPrescription } from '../../utils/pharmacy/prescription-helpers';
test('refill prescription flow', async ({ page }) => {
// Reuse existing page object
const searchPage = new PrescriptionSearchPage(page);
// Reuse existing helper
const prescription = await createTestPrescription(page, {
medication: 'Lisinopril 10mg',
refillsRemaining: 3
});
await searchPage.navigate();
await searchPage.searchById(prescription.id);
await searchPage.clickRefill(prescription.id);
// Only create NEW page object if needed
const refillPage = new RefillConfirmationPage(page); // This is NEW
await refillPage.confirmRefill();
await expect(page.getByText('Refill submitted')).toBeVisible();
});
❌ BAD: Creating duplicates
// Don't create PrescriptionSearchPageV2 just because you didn't check! // Don't create createPrescription() when createTestPrescription() exists!
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
# Search for similar page objects grep -r "class.*Prescription.*Page" page-objects/ # Search for specific methods 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
// ✅ EXTEND existing page object when pages are similar
// page-objects/pharmacy/BasePrescriptionPage.ts
export class BasePrescriptionPage {
constructor(protected page: Page) {}
protected async selectMedication(name: string) {
await this.page.getByLabel('Medication').fill(name);
}
}
// page-objects/pharmacy/CreatePrescriptionPage.ts
export class CreatePrescriptionPage extends BasePrescriptionPage {
async create(medication: string, dosage: string) {
await this.selectMedication(medication); // Reused!
await this.page.getByLabel('Dosage').fill(dosage);
await this.page.getByRole('button', { name: 'Create' }).click();
}
}
// page-objects/pharmacy/RefillPrescriptionPage.ts
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
// ❌ Puppeteer
await page.click('#submit-button');
await page.waitForSelector('.success-message');
// ✅ Playwright
await page.getByRole('button', { name: 'Submit' }).click();
await expect(page.getByRole('alert')).toBeVisible();
Wait Migration
// ❌ Puppeteer
await page.waitFor(2000); // Hard-coded wait
await page.waitForSelector('#loading', { hidden: true });
// ✅ Playwright
// Auto-waiting - no explicit wait needed
await page.getByRole('progressbar').waitFor({ state: 'hidden' });
Navigation Migration
// ❌ Puppeteer
await page.goto('https://example.com');
await page.waitForNavigation();
// ✅ Playwright
await page.goto('https://example.com');
// No need for waitForNavigation - goto waits automatically
Assertion Migration
// ❌ Puppeteer
const text = await page.$eval('.message', el => el.textContent);
expect(text).toBe('Success');
// ✅ Playwright
await expect(page.getByRole('status')).toHaveText('Success');
Selenium → Playwright
// ❌ Selenium
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')));
// ✅ Playwright
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); // Wait for data to load
const items = await page.$$('.prescription-item');
Playwright Solution:
// Wait for specific condition
await page.getByRole('progressbar').waitFor({ state: 'hidden' });
// Or wait for first item to appear
await page.getByRole('article').first().waitFor();
// Then get all items
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');
// Or for hidden inputs
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:
// ❌ Bad - Lost business context
test('test prescription', async ({ page }) => {
await page.goto('/prescriptions');
await page.click('#refill-123');
// What business rule are we testing?
});
// ✅ Good - Business logic preserved
test('should allow refill only when prescription has remaining refills', async ({ page }) => {
// Business Rule: Users can refill prescriptions if refills remaining > 0
const prescription = await createTestPrescription(page, {
medication: 'Lisinopril 10mg',
refillsRemaining: 2 // Critical business data
});
const searchPage = new PrescriptionSearchPage(page);
await searchPage.navigate();
await searchPage.searchById(prescription.id);
// Verify refill button is enabled (business logic)
await expect(searchPage.getRefillButton(prescription.id)).toBeEnabled();
await searchPage.clickRefill(prescription.id);
// Verify business outcome
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