Toen we een verzoek kregen om Cypress-tests naar Playwright te migreren, verwachtten we een lang, vervelend herschrijvingsproces.
Verrassend genoeg lukte het ons met de hulp van GitHub Copilot om de migratie van Cypress naar Playwright in slechts een paar dagen te starten — en om onze CI-pijplijn de hele tijd draaiende te houden zonder de testprocessen te onderbreken.
In dit artikel deel ik hoe we deze migratie hebben aangepakt, de lessen die we onderweg hebben geleerd en hoe AI-ondersteunde code-migratie de handmatige belasting van framework-overgangen drastisch kan verminderen.
Projectcontext
Het project was een complex gezondheidszorgplatform met veel dat zich onder de motorkap afspeelt. Onze testopstelling omvatte:
- Eind-tot-eind (E2E) en integratietests
- Een aangepaste TAF-architectuur met opstellingen, afbrekingen en entiteitscreatie
- Multi-omgeving configuraties
- Dynamische gegevens en diepe navigatielogica
In totaal hadden we 148 E2E-tests die op Cypress draaiden — allemaal werkten ze prima, maar de organisatie wilde de QA-tooling standaardiseren over teams heen, wat één ding betekende: migratie naar Playwright.
Waarom GitHub Copilot gebruiken voor migratie?
Toen het verzoek voor migratie binnenkwam, zag ik een kans om te experimenteren met testautomatisering van GitHub Copilot. In plaats van handmatig honderden testbestanden te herschrijven, wilde ik zien hoe ver we AI-ondersteuning konden benutten om syntaxisconversie van Cypress naar Playwright te automatiseren.
En eerlijk gezegd, het werkte veel beter dan verwacht.
GitHub Copilot handelde het meeste van de repetitieve vertaling (selectoren, commando's, async-handling, etc.), waardoor we ons konden concentreren op infrastructuuraanpassingen en het verfijnen.
Binnen 1–2 dagen hadden we al een werkend prototype van het Playwright-testautomatiseringsframework.
Twee Belangrijke Disclaimer
Controleer Altijd AI-gegenereerde Code
Ook al versnelt AI-ondersteunde code-migratie dingen, je moet elke regel controleren. Infrastructuurmigraties kunnen gemakkelijk subtiele bugs of prestatie-regressies introduceren. Beschouw Copilot als je parenprogrammeur, niet als een autopiloot.
Houd het Oude Framework Draaien Tot het Einde
Blokkeer nooit releases of regressies tijdens de migratie. Houd je Cypress-tests draaiende op CI totdat de bijbehorende suites volledig stabiel zijn in Playwright. Zodra een suite is gemigreerd en gevalideerd, verwijder deze dan uit Cypress en wijzig CI om deze in Playwright te laten draaien.
Deze geleidelijke uitrol zorgt voor geen downtime en voortdurende testdekking gedurende het proces.
Workflow om te Migreren van Cypress naar Playwright
Toen de basis duidelijk was, gingen we over op de hands-on migratie.
Hieronder staat de workflow die ons hielp beide frameworks parallel te draaien, de CI-stabiliteit te handhaven en tests geleidelijk te migreren — zonder lopende releases te blokkeren.
1. Initialiseer Playwright en Maak de Mapstructuur aan
De eerste stap was om Playwright te initialiseren en een schone, geïsoleerde mapstructuur te creëren. We wilden dat beide frameworks naast elkaar in dezelfde repository konden leven, zodat we ze gelijktijdig 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 beide draaiende te houden
Initiëlexample:

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 waarmee we geconfronteerd werden, waren TypeScript-conflicten — beide frameworks definiëren vergelijkbare methodenamen (expect, request, enz.), en het delen van een enkele tsconfig.json leidde tot compilatiefouten.
De oplossing was eenvoudig: splits de TypeScript-configuraties. Deze structuur zorgt ervoor dat elk framework zijn eigen type-definities initialiseert en type-collisions voorkomt:
├── tsconfig.json
├── tsconfig.cypress.json
├── tsconfig.playwright.jsonHoofd tsconfig.json
Voeg de composite optie toe ter ondersteuning van projectreferenties.
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 in één repo coëxisteren — geen compilerconflicten meer en geen risico op per ongeluk het twee keer initialiseren van gedeelde methoden.
3. Maak een prompt om migratie te automatiseren met GitHub Copilot
Nadat 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 zich moet gedragen 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 zorgt voor consistente resultaten 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 iets noemen als migrate_tests.md.
Voorbeeld van de prompt:
---
mode: agent
---
Je bent een Playwright Test Generator, een expert in browserautomatisering en end-to-end testen.
Jouw specialiteit is het creëren van robuuste, betrouwbare Playwright-tests die gebruikersinteracties nauwkeurig simuleren en het gedrag van de applicatie valideren.
Jouw taak is om bestaande Cypress-tests naar Playwright-tests te migreren.
De Cypress-tests, paginaobjecten, componenten en hulpprogramma-bestanden worden als invoer geleverd, en jij moet gelijkwaardige Playwright-testcode genereren.
Geef ALLE gemigreerde code in de chat.
Bij het genereren van de Playwright-testcode of paginaobjecten, zorg ervoor dat:
1. De teststructuur dezelfde structuur volgt als de Cypress-tests.
2. Maak een globale fixture voor setup en teardown en herbruik deze in de tests.
3. Alle gebruikersinteracties (klikken, typen, navigeren) nauwkeurig worden vertaald naar Playwright-syntaxis in paginaobjecten en testbestanden.
4. Asserties in Cypress worden omgezet naar gelijkwaardige Playwright-asserties.
5. Negeer de netwerkmethoden en stobbingsonderdelen van de Cypress-tests.
6. Commenteer alle methoden met wachttijden (bijv. cy.wait) in de Playwright-code uit.
7. Gebruik geen get-methoden in paginaobjecten; gebruik in plaats daarvan directe methoden `backbackItem = () => this.page.locator('[id="item_4_title_link"]');`Waarom dit helpt
Deze gestructureerde prompt stelt GitHub Copilot in staat om meer te functioneren als een gespecialiseerde migratie-assistent, niet als een algemene codegenerator.
Met de juiste context en regels kan Copilot automatisch:
- Grote delen van Cypress-tests herschrijven naar Playwright-syntaxis
- De consistentie van map- en teststructuur behouden
- De handmatige conversietijd met 70–80% verminderen
Uiteindelijk maak 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
Toen de prompt klaar was, was de volgende stap om de migratie daadwerkelijk uit te voeren met GitHub Copilot in agentmodus. In deze fase lieten we het AI-model elk bestand verwerken — paginaobjecten, componenten, hulpprogramma's en uiteindelijk de tests — één voor één.
Voor deze taak hebben we het GPT-4.1-model gebruikt, dat de meest consistente en contextbewuste code-transformaties levert.
Hoe te beginnen met migratie
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 paginacomponent) dat je wilt migreren
Copilot zal nu je migratieprompt toepassen en een Playwright-compatibele versie van het bestand rechtstreeks in de chat genereren.
Voorbeeld van migratie van de inventoryPage Pagina Component
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. Onze ervaring is dat de AI-agent bij de eerste poging meestal 60–70% van de code goed heeft. De sleutel is om de gesprekscontext actief te houden — blijf in dezelfde chatthread verfijnen door om fixes of aanpassingen te vragen. Gaandeweg leert Copilot de patronen van je project en produceert het nauwkeurigere, herbruikbare code.
Kortom: hoe meer iteraties je in dezelfde chat doet, des te beter wordt de kwaliteit van de migratie.
Migratievolgorde: eerst bestanden, dan tests
Volg deze volgorde om afhankelijkheden consistent te houden en ontbrekende referenties te vermijden:
1. Genereer eerst de bestanden die geen tests zijn:
- Page Objects
- Componenten
- Hulpbestanden
- Constanten
2. Zodra alle ondersteunende bestanden zijn gegenereerd en geverifieerd, voeg je de map #app toe aan de chatcontext en migreer je de testbestanden zelf.
Verificatiestap
Wanneer een Playwright-test is gegenereerd:
1. Bekijk de code — controleer of de page locators, teststappen en assertions correct zijn.
2. Voer de test uit met je Playwright CLI om te controleren of hij goed werkt.
3. Corrigeer kleine syntax- of fixture-afwijkingen als Copilot een Cypress-commando verkeerd heeft begrepen.
Door na elk bestand te verifiëren, houd je de migratie stabiel en incrementeel en voorkom je later grootschalig debuggen.
Voorbeeld van een gemigreerde inventoryPage-test die succesvol slaagt:
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("The user should add item to the card", () => {
inventoryPage.item.itemByName("Bike Light").should("have.text", itemsNames.bikeLight);
inventoryPage.item.addToCartByName("Bike Light");
inventoryPage.assertCartLogoItems(1);
});
it("The user should remove item from the card", () => {
inventoryPage.backbackItem().should("have.text", itemsNames.backpackItemName);
inventoryPage.clickBackbackAddItemButton();
inventoryPage.assertCartLogoItems(1);
inventoryPage.clickBackbackRemoveItemButton();
inventoryPage.shoppingCartLogo().should("not.have.text");
});
it("The user should add multiple items to the card", () => {
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 migrated from Cypress to Playwright
test.describe("InventoryPage tests", () => {
test.beforeEach(async ({ pageManager }) => {
await pageManager.loginPage.loginWithValidData();
});
test("The user should add item to the cart", 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("The user should remove item from the cart", 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/); // Should not have any number text
});
test("The user should add multiple items to the cart", 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. Uiteindelijke repositorystructuur na de migratie
Toen de migratie voltooid was, bereikte onze repository een stabiele status met twee frameworks — Cypress en Playwright draaiden volledig naast elkaar. Deze structuur stelde ons in staat om:
- Cypress-testsuites geleidelijk uit te faseren naarmate ze werden vervangen
- CI/CD-pipelines voor beide frameworks operationeel te houden
- Een schone en modulaire architectuur te behouden
Hieronder staat de uiteindelijke structuur na het voltooien van de migratieworkflow.
Uiteindelijke mappenstructuur
├── .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/
Opmerkingen over de structuur
.github/prompts/— bevat je Copilot-migratieprompts. Door ze onder versiebeheer te houden, kun je ze bijwerken of hergebruiken voor toekomstige frameworkmigraties of refactors..github/workflows/— bevat je CI/CD-definities. Je kunt Playwright- en Cypress-jobs parallel draaien tot het oude framework volledig is uitgefaseerd.- Map
playwright/— fungeert voortaan als de primaire automatiseringsbron. Ze weerspiegelt de structuur van de vorige Cypress-implementatie om de onboarding te vergemakkelijken en consistentie te waarborgen. tsconfig.playwright.jsonentsconfig.cypress.json— blijven gescheiden voor volledige type-isolatie en voorkomen conflicten tijdens builds.
Deze structuur ondersteunde niet alleen een soepele overgang, maar maakte het project ook klaar voor toekomstige schaalbaarheid, CI-flexibiliteit en modulair testeigenaarschap over teams heen.
Verplaats je testsuite zonder releases te verstoren of dekking te verliezen. Onze QA-engineers helpen je de migratie te plannen, je automatiseringsarchitectuur aan te passen en beide frameworks stabiel te houden tijdens de overgang.
Praktisch advies & geleerde lessen
Na de migratie van bijna 150 tests met Copilot en Playwright volgen hier een paar belangrijke lessen en praktische tips die het proces soepeler maakten (en ons wat hoofdpijn bespaarden).
Algemeen advies over AI
- AI liegt — vaak overtuigend.
Controleer altijd elke regel code die de AI genereert. Vertrouw er nooit blindelings op, zeker niet tijdens infrastructuurmigraties.
- Houd je aan consistente naamgevingsconventies.
Gebruik in beide frameworks identieke variabele- en methodenamen — dat helpt de AI-agent om de code nauwkeuriger te migreren en maakt het voor jou makkelijker om ze later te vinden en te importeren.

- Bouw imports handmatig opnieuw op.
Copilot haalt importpaden vaak door elkaar. Het is meestal sneller om ze te verwijderen en handmatig opnieuw te importeren, of om globale imports in te stellen, zoals: import x from "playwright/foo/bar";
- Gebruik dezelfde chat voor alle migraties.
Blijf bij één Copilot-chatthread — die bouwt context op en "onthoudt" de conventies van je project, wat tot betere resultaten leidt.
- Splits migratietaken op.
Genereer 1–2 bestanden tegelijk. Kleinere taken leveren nauwkeurigere resultaten op en maken handmatige validatie eenvoudiger.
Advies specifiek voor Playwright
- Maak nooit zonder noodzaak meerdere instanties van hetzelfde Page Object.
Dat kan leiden tot een inconsistente status of kapotte selectors bij het navigeren tussen pagina's. De oplossing? Implementeer een Page Manager die alle page objects toegankelijk houdt via één enkele instantie.
Hier is een minimale voorbeeldopzet:
// 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("The user should login with valid data", async ({ pageManager }) => {
await pageManager.loginPage.clickLoginButton();
});Andere technische tips
- Verwijder alle "get"-voorvoegsels uit de locators die de AI-agent genereert — gebruik in plaats daarvan directe locatormethoden.
- Vergeet niet om
dotenvte configureren voor omgevingsvariabelen en inloggegevens. - Pas je Playwright-configuratie aan zodat alle rapporten in de Playwright-map terechtkomen en de root van je project schoon blijft:
reporter: [["html", { open: "never", outputFolder: "./playwright/reports/" }]],
outputDir: "./playwright/test-results",
Nawoord
De ESLint-configuratie heb ik hier niet opgenomen, omdat die per project verschilt.
De enige algemene aanbeveling die ik kan doen, is om de Playwright-plugin voor ESLint te installeren voor betere regelhandhaving: eslint-plugin-playwright.
Wil je een compleet werkend voorbeeld van deze migratie bekijken — inclusief promptbestanden, tsconfigs en CI-configuratie? Dan vind je de volledige repository hier: Repo-voorbeeld.
Of je nu migreert van Cypress, een instabiele testsuite opnieuw opbouwt of je QA-automatisering verbetert: JetBase helpt je de overgang te maken met minder handmatig werk en een lager leveringsrisico.














