Toen we een verzoek kregen om Cypress-tests naar Playwright te migreren, verwachtten we een langdradig herschrijvingsproces.
Verrassend genoeg, met de hulp van GitHub Copilot, slaagden we erin om de migratie van Cypress naar Playwright in slechts een paar dagen op te starten — en onze CI-pijplijn de hele tijd draaiende te houden zonder enige testprocessen te onderbreken.
In dit artikel deel ik hoe we deze migratie hebben aangepakt, de lessen die we onderweg hebben geleerd, en hoe AI-geassisteerde code-migratie de handmatige overhead van framework-overgangen drastisch kan verminderen.
Projectcontext
Het project was een complex gezondheidsplatform met veel activiteiten op de achtergrond. Onze testopstelling omvatte:
- Eind-tot-einde (E2E) en integratietests
- Een aangepaste TAF-architectuur met opstellingen, afbouwen en entiteit creatie
- Multi-omgeving configuraties
- Dynamische data en diepgaande navigatielogica
In totaal hadden we 148 E2E-tests die draaiden op Cypress — allemaal werkten ze prima, maar de organisatie wilde de QA-tooling over de verschillende teams standaardiseren, wat één ding betekende: migratie naar Playwright.
Waarom GitHub Copilot gebruiken voor migratie?
Toen het migratieverzoek binnenkwam, zag ik een kans om te experimenteren met testautomatisering met GitHub Copilot. In plaats van handmatig honderden testbestanden te herschrijven, wilde ik zien hoe ver we AI-assistentie konden duwen om de syntaxisconversie van Cypress naar Playwright te automatiseren.
En eerlijk gezegd, het werkte veel beter dan verwacht.
GitHub Copilot verzorgde het merendeel van de repetitieve vertaling (selectoren, commando's, async-afhandeling, enz.), waardoor we ons konden concentreren op infrastructuur aanpassingen en optimalisatie.
Binnen 1–2 dagen hadden we al een werkend prototype van het Playwright-testautomatiseringsframework.
Twee belangrijke disclaimers
Controleer altijd AI-gegenereerde code
Ook al versnelt AI-geassisteerde code-migratie de zaken, je moet elke regel bekijken. Infrastructuurmigraties kunnen gemakkelijk subtiele bugs of prestatie regressies introduceren. Beschouw Copilot als je pair programmer, niet als een autopilot.
Houd het oude framework draaiende tot het einde
Blokkeer nooit releases of regressies tijdens de migratie. Houd je Cypress-tests draaiende op CI totdat de overeenkomstige suites volledig stabiel zijn in Playwright. Zodra een suite is gemigreerd en gevalideerd, verwijder je deze uit Cypress en schakel je CI over naar een uitvoering in Playwright.
Deze geleidelijke uitrol zorgt voor nul downtime en continue testdekking gedurende het proces.
Workflow voor migratie van Cypress naar Playwright
Zodra de basis duidelijk was, gingen we over naar de praktische migratie.
Hieronder is de workflow die ons hielp om beide frameworks parallel te draaien, CI-stabiliteit te behouden en tests geleidelijk te migreren — zonder lopende releases te blokkeren.
1. Initialiseer Playwright en maak de mappenstructuur aan
De eerste stap was om Playwright te initialiseren en een schone, geïsoleerde mappenstructuur aan te maken.
We wilden beide frameworks naast elkaar in dezelfde repository laten leven, zodat we ze tegelijkertijd tijdens de overgang konden uitvoeren.Die opstelling stelde ons in staat om:
- Geleidelijk te migreren
- Resultaten en tijden tussen Cypress en Playwright te vergelijken
- CI-pijplijnen voor beiden draaiende te houden
Voorbeeld van initialisatie:

Hier is de voorgestelde mappenstructuur die we voor Playwright hebben gebruikt:
├── cypress/
│ ├── app/
│ ├── downloads/
│ ├── e2e/
│ ├── fixtures/
│ ├── screenshots/
│ ├── support/
│ └── videos/
├── playwright/
│ ├── app/
│ │ ├── components/
│ │ ├── fixtures/
│ │ └── pageobjects/
│ ├── constants/
│ ├── helpers/
│ ├── reports/
│ ├── test-results/
│ └── tests/
├── .gitignore
├── package.json
├── playwright.config.ts
├── smoke.config.ts
├── tsconfig.json2. Configureer aparte tsconfig bestanden voor elk framework
Een van de vroege problemen waar we tegenaan liepen, waren TypeScript conflicten — beide frameworks definiëren vergelijkbare methodenamen (expect, request, enz.), en het delen van één tsconfig.json leidde tot compilatiefouten.
De oplossing was eenvoudig: splits de TypeScript-configuraties. Deze structuur zorgt ervoor dat elk framework zijn eigen type-definities initializeert en typebotsingen voorkomt:
├── tsconfig.json
├── tsconfig.cypress.json
├── tsconfig.playwright.jsonHoofd tsconfig.json
Voeg de composite optie toe om projectreferenties te ondersteunen.
tsconfig.json
{
"compilerOptions": {
"composite": true
}
}tsconfig.cypress.json
{
"extends": "./tsconfig.json",
"compilerOptions": {
"types": ["cypress", "@testing-library/cypress", "node"],
"isolatedModules": false
},
"include": ["cypress/**/*.ts"]
}tsconfig.playwright.json
{
"extends": "./tsconfig.json",
"compilerOptions": {
"types": ["node", "@playwright/test"]
},
"include": ["playwright/**/*.ts", "playwright.config.ts"]
}Met deze scheiding konden beide frameworks vreedzaam naast elkaar bestaan in één repo — geen compilerconflicten meer en geen risico dat gedeelde methoden per ongeluk twee keer worden geïnitialiseerd.
3. Maak een prompt om migratie te automatiseren met GitHub Copilot
Toen Playwright was geïnitialiseerd en het project was gestructureerd, was het tijd om GitHub Copilot te laten helpen met het zware werk.
Om dit te doen, hebben we een aangepaste prompt gemaakt die definieert hoe Copilot moet handelen bij het herschrijven van tests van Cypress naar Playwright.
De prompt vertelt de agent wat te doen, hoe het te doen, en wat te negeren — wat consistente resultaten oplevert bij alle migraties.
Hoe de Prompt Toe te Voegen
Open binnen je GitHub-repository het tandwielpictogram ⚙️ in Copilot Chat en maak een nieuwe prompt aan.
Het bestand wordt automatisch opgeslagen onder: ./github/prompts/
Je kunt het een naam geven zoals migrate_tests.md.
Promptvoorbeeld:
---
mode: agent
---
Je bent een Playwright Test Generator, een expert in browserautomatisering en end-to-end testen.
Je specialiteit is het creëren van robuuste, betrouwbare Playwright-tests die gebruikersinteracties nauwkeurig simuleren en de app-gedragingen valideren.
Je taak is om te helpen bestaande Cypress-tests naar Playwright-tests te migreren.
De Cypress-tests, page object, componenten en helperbestanden worden als input gegeven, en je moet gelijkwaardige Playwright-testcode genereren.
Geef ALLE gemigreerde code in de chat terug.
Wanneer je de Playwright-testcode of enige page objects genereert, zorg ervoor dat:
1. De teststructuur dezelfde structuur volgt als de Cypress-tests.
2. Maak een globale fixture voor setup en teardown en hergebruik deze in de tests.
3. Alle gebruikersinteracties (klikken, typen, navigatie) worden nauwkeurig vertaald naar Playwright-syntax in page objects en testbestanden.
4. Asserties in Cypress worden geconverteerd naar equivalente Playwright-asserties.
5. Negeer de netwerkspionage en stubbing delen van de Cypress-tests.
6. Zet alle methoden met wacht (bijv. cy.wait) in de Playwright-code commentaar.
7. Gebruik geen get-methoden in page objects; gebruik directe methoden in plaats daarvan `backbackItem = () => this.page.locator('[id="item_4_title_link"]');`Waarom Dit Helpt
Dit soort gestructureerde prompt stelt GitHub Copilot in staat om meer te functioneren als een gespecialiseerde migratie-assistent, en niet als een algemene codegenerator.
Met de juiste context en regels kan Copilot automatisch:
- Grote stukken Cypress-tests herschrijven naar Playwright-syntax
- De consistentie van mappen en teststructuren behouden
- De handmatige conversietijd met 70–80% verminderen
In wezen creëer je je eigen AI-migratietoolchain — afgestemd op de structuur en conventies van je project.
4. Selecteer de Agentmodus en Voer de Migratie Stap voor Stap Uit
Eenmaal de prompt klaar was, was de volgende stap om daadwerkelijk de migratie uit te voeren met GitHub Copilot in de agentmodus. In deze fase laten we het AI-model elk bestand verwerken — page objects, componenten, helpers, en tenslotte de tests — één voor één.
Voor deze taak gebruikten we het GPT-4.1-model, dat de meest consistente en contextbewuste code-transformaties levert.
Hoe te Beginnen met Migreren
1. Open Copilot Chat in VS Code
2. Gebruik / om het promptbestand te selecteren dat je eerder hebt gemaakt
3.Gebruik # om een specifiek bestand te selecteren (bijvoorbeeld een Cypress-test of paginaobject) dat je wilt migreren
Copilot zal nu je migratieprompt toepassen en een Playwright-compatibele versie van het bestand direct in de chat genereren.
Voorbeeld van migratie van het inventoryPage Paginaobject
import { ItemComponent } from "../components/item.component";
import Common from "./common";
class InventoryPage extends Common {
item = new ItemComponent();
backbackItem = () => cy.get("#item_4_title_link");
backbackAddItemButton = () => cy.getByTestId("add-to-cart-sauce-labs-backpack");
bikeLightsAddItemButton = () => cy.getByTestId("add-to-cart-sauce-labs-bike-light");
backbackRemoveItemButton = () => cy.getByTestId("remove-sauce-labs-backpack");
shoppingCartLogo = () => cy.get("#shopping_cart_container");
clickBackbackAddItemButton = () => this.backbackAddItemButton().click();
clickBikeLightsAddItemButton = () => this.bikeLightsAddItemButton().click();
clickBackbackRemoveItemButton = () => this.backbackRemoveItemButton().click();
assertCartLogoItems = (itemsNum: number) => this.shoppingCartLogo().should("have.text", itemsNum);
}
export default new InventoryPage();
import { Page, Locator, expect } from "@playwright/test";
import Common from "./common";
class InventoryPage extends Common {
constructor(page: Page) {
super(page);
this.item = new ItemComponent(page);
}
// Locators
backbackItem(): Locator {
return this.page.locator("#item_4_title_link");
}
backbackAddItemButton(): Locator {
return this.page.getByTestId("add-to-cart-sauce-labs-backpack");
}
bikeLightsAddItemButton(): Locator {
return this.page.getByTestId("add-to-cart-sauce-labs-bike-light");
}
backbackRemoveItemButton(): Locator {
return this.page.getByTestId("remove-sauce-labs-backpack");
}
shoppingCartLogo(): Locator {
return this.page.locator("#shopping_cart_container");
}
// Actions
async clickBackbackAddItemButton(): Promise<void> {
await this.backbackAddItemButton().click();
}
async clickBikeLightsAddItemButton(): Promise<void> {
await this.bikeLightsAddItemButton().click();
}
async clickBackbackRemoveItemButton(): Promise<void> {
await this.backbackRemoveItemButton().click();
}
async assertCartLogoItems(itemsNum: number): Promise<void> {
await expect(this.shoppingCartLogo()).toHaveText(String(itemsNum));
}
}
export default InventoryPage;Het gedrag van de agent afstemmen
Maak je geen zorgen als de eerste output niet perfect is.Op basis van onze ervaring krijgt de AI-agent meestal 60–70 % van de code goed bij de eerste poging. De sleutel is om de gesprekscontext actief te houden — blijf dezelfde chatthread verfijnen door om fixes of aanpassingen te vragen. Terwijl je dit doet, begint Copilot te leren van jouw projectpatronen en produceert het nauwkeurigere, herbruikbare code.
In wezen geldt: hoe meer iteraties je in dezelfde chat uitvoert, hoe beter de kwaliteit van de migratie wordt.
Migratievolgorde: Eerst bestanden, dan tests
Om afhankelijkheden consistent te houden en ontbrekende verwijzingen te voorkomen, volg deze volgorde:
1. Genereer eerst de niet-testbestanden:
- Pagina-objecten
- Componenten
- Helpbestanden
- Constanten
2. Zodra alle ondersteunende bestanden zijn gegenereerd en geverifieerd, voeg je de #app map toe aan de chatcontext, en migreer je vervolgens de testbestanden zelf.
Verificatiestap
Wanneer een Playwright-test is gegenereerd:
1. Bekijk de code — bevestig dat pagina-locators, teststappen en beweringen correct zijn.
2. Voer de test uit met je Playwright CLI om ervoor te zorgen dat deze correct wordt uitgevoerd.
3. Los kleine syntax- of fixture-ongelijkheden op als Copilot een Cypress-commando verkeerd heeft begrepen.
Door na elk bestand te verifiëren, houd je de migratie stabiel en incrementeel, waardoor je later grootschalig debuggen voorkomt.
Voorbeeld van de gemigreerde inventoryPage test die succesvol doorloopt:
import inventoryPage from "cypress/app/pageobjects/inventoryPage";
import loginPage from "cypress/app/pageobjects/loginPage";
import { itemsNames } from "../../fixtures/data.json";
describe("InventoryPage tests", () => {
beforeEach(() => {
loginPage.loginWithValidData();
});
it("De gebruiker zou een item aan het winkelwagentje moeten toevoegen", () => {
inventoryPage.item.itemByName("Fietslicht").should("have.text", itemsNames.bikeLight);
inventoryPage.item.addToCartByName("Fietslicht");
inventoryPage.assertCartLogoItems(1);
});
it("De gebruiker zou een item uit het winkelwagentje moeten verwijderen", () => {
inventoryPage.backbackItem().should("have.text", itemsNames.backpackItemName);
inventoryPage.clickBackbackAddItemButton();
inventoryPage.assertCartLogoItems(1);
inventoryPage.clickBackbackRemoveItemButton();
inventoryPage.shoppingCartLogo().should("not.have.text");
});
it("De gebruiker zou meerdere items aan het winkelwagentje moeten toevoegen", () => {
inventoryPage.backbackItem().should("have.text", itemsNames.backpackItemName);
inventoryPage.clickBackbackAddItemButton();
inventoryPage.assertCartLogoItems(1);
inventoryPage.clickBikeLightsAddItemButton();
inventoryPage.assertCartLogoItems(2);
});
});
import { test, expect } from "../fixtures/test.fixture";
import { itemsNames } from "../../constants/data.json";
// InventoryPage tests gemigreerd van Cypress naar Playwright
test.```javascript
describe("InventoryPage tests", () => {
test.beforeEach(async ({ pageManager }) => {
await pageManager.loginPage.loginWithValidData();
});
test("De gebruiker zou een item aan het winkelwagentje moeten toevoegen", async ({ pageManager }) => {
await expect(
pageManager.inventoryPage.item.itemByName(itemsNames.bikeLight)).toHaveText(itemsNames.bikeLight);
await pageManager.inventoryPage.item.addToCartByName(itemsNames.bikeLight);
await pageManager.inventoryPage.assertCartLogoItems(1);
});
test("De gebruiker zou een item uit het winkelwagentje moeten verwijderen", async ({ pageManager }) => {
await expect(
pageManager.inventoryPage.backbackItem()).toHaveText(itemsNames.backpackItemName);
await pageManager.inventoryPage.clickBackbackAddItemButton();
await pageManager.inventoryPage.assertCartLogoItems(1);
await pageManager.inventoryPage.clickBackbackRemoveItemButton();
await expect(pageManager.inventoryPage.shoppingCartLogo()).not.toHaveText(/\d/); // Zou geen nummertekst moeten hebben
});
test("De gebruiker zou meerdere items aan het winkelwagentje moeten toevoegen", async ({ pageManager }) => {
await expect(pageManager.inventoryPage.backbackItem()).toHaveText(itemsNames.backpackItemName);
await pageManager.inventoryPage.clickBackbackAddItemButton();
await pageManager.inventoryPage.assertCartLogoItems(1);
await pageManager.inventoryPage.clickBikeLightsAddItemButton();
await pageManager.inventoryPage.assertCartLogoItems(2);
});
});5. Einde Repository Structuur Na Migratie
Eenmaal de migratie voltooid was, bereikte onze repository een stabiele, duale-framework staat - volledig draaiend met zowel Cypress als Playwright naast elkaar. Deze structuur stelde ons in staat om:
- Geleidelijk de Cypress test suites af te bouwen naarmate ze vervangen werden
- CI/CD pipelines operationeel te houden voor beide frameworks
- Een schone en modulaire architectuur te behouden
Hieronder is de eindstructuur na het voltooien van de migratieworkflow.
Eindmapstructuur
├── .env
├── .gitignore
├── readme.md
├── package.json
├── playwright.config.ts
├── smoke.config.ts
├── tsconfig.cypress.json
├── tsconfig.playwright.json
├── tsconfig.json
├── .github/
│ ├── prompts/
│ └── workflows/
├── cypress/
│ ├── app/
│ ├── downloads/
│ ├── e2e/
│ ├── fixtures/
│ ├── screenshots/
│ ├── support/
│ └── videos/
├── playwright/
│ ├── app/
│ ├── constants/
│ ├── helpers/
│ ├── reports/
│ ├── test-results/
│ └── tests/
Notities over de Structuur
.github/prompts/— bevat uw Copilot-migratieprompts. Het versiebeheer hiervan stelt u in staat om deze bij te werken of opnieuw te gebruiken voor toekomstige frameworkmigraties of refactors..github/workflows/— houdt uw CI/CD-definities vast. U kunt zowel Playwright als Cypress taken parallel uitvoeren totdat de oude framework volledig is afgewaardeerd.playwright/map — fungeert nu als de primaire automatiseringsbron voorwaarts. Het weerspiegelt de structuur van de vorige Cypress-implementatie om onboarding te vergemakkelijken en consistentie te waarborgen.tsconfig.playwright.jsonentsconfig.cypress.json— blijven apart voor volledige type-isolatie, waardoor conflicten tijdens builds worden voorkomen.
Deze structuur ondersteunde niet alleen een soepele overgang, maar positioneerde het project ook voor toekomstige schaalbaarheid, CI-flexibiliteit en modulair testbeheer binnen teams.
Praktische Adviezen & Leerpunten
Na het migreren van bijna 150 tests met Copilot en Playwright, hier zijn een paar belangrijke lessen en praktische tips die het proces soepeler maakten (en ons een aantal hoofdpijn bespaarden).
Algemene AI-gerelateerde Adviezen
- AI liegt — vaak overtuigend.
Verifieer altijd elke regel code die de AI genereert. Vertrouw het nooit blindelings, vooral niet tijdens infrastructuurmigraties.
- Hou je aan consistente naamgevingsconventies.
Behoud identieke variabele- en methodenamen tussen frameworks — dit helpt de AI Agent om de code nauwkeuriger te migreren en maakt het makkelijker voor jou om ze later te vinden en te importeren.

- Herbouw importen handmatig.
Copilot verprutst vaak importpaden. Het is vaak sneller om ze te verwijderen en handmatig opnieuw te importeren, of om globale imports in te stellen zoals: import x from "playwright/foo/bar";
- Herbruik dezelfde chat voor alle migraties.
Blijf bij een enkele Copilot-chatthread — dit bouwt context op en "onthoudt" je projectconventies, wat leidt tot betere resultaten.
- Decompose migratietaken.
Genereer 1–2 bestanden tegelijkertijd. Kleinere taken leveren nauwkeurigere uitvoer en maken handmatige validatie gemakkelijker.
Playwright-specifieke Adviezen
- Creëer nooit meerdere instanties van hetzelfde Page Object zonder noodzaak.
Dit kan leiden tot inconsistente toestanden of gebroken selectors bij het navigeren tussen pagina's. De oplossing? Implementeer een Page Manager die alle page objects toegankelijk houdt via een enkele instantie.
Hier is een minimaal voorbeeld van de setup:
// login.page.ts
class LoginPage extends Common {
async clickLoginButton(): Promise<void> {
await this.loginButton().click();
}
}
export default LoginPage;
//-----------------------------------------------------------
//PageManager.ts
export class PageManager {
readonly loginPage: LoginPage;
constructor(page: Page) {
this.loginPage = new LoginPage(page);
}
}
//-----------------------------------------------------------
//test.fixture.ts
export const test = base.extend<{
pageManager: PageManager;
}>({
pageManager: async ({ page }, use) => {
const manager = new PageManager(page);
await use(manager);
},
});
export { expect };
//-----------------------------------------------------------
//userLogin.spec.ts
test("De gebruiker moet inloggen met geldige gegevens", async ({ pageManager }) => {
await pageManager.loginPage.clickLoginButton();
});Andere technische tips
- Verwijder alle “get” voorvoegsels van locators die de AI-agent genereert — gebruik directe locator-methoden in plaats daarvan.
- Vergeet niet om
dotenvte configureren voor omgevingsvariabelen en inloggegevens. - Pas je Playwright-configuratie aan om alle rapporten onder de Playwright-map op te slaan, zodat de hoofdmap van je project schoon blijft:
reporter: [["html", { open: "never", outputFolder: "./playwright/reports/" }]],
outputDir: "./playwright/test-results",
Nawoord
Ik heb de ESLint-configuratie hier niet opgenomen, aangezien deze varieert van project tot project.
De enige universele aanbeveling die ik kan doen, is om de Playwright ESLint-plugin te installeren voor een betere handhaving van regels: eslint-plugin-playwright.
Als je een compleet werkend voorbeeld van deze migratie wilt verkennen — inclusief promptbestanden, tsconfig's en CI-configuratie — kun je de volledige repository hier bekijken:














