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
Related Resources
- Playwright Best Practices Skill
- Code Organization Skill
- Migration Log Template
- Reusability Check Prompts
Converted and distributed by TomeVault — claim your Tome and manage your conversions.