Da vi modtog en anmodning om at migrere Cypress-tests til Playwright, forventede vi en lang, kedelig omskrivningsproces.
Overraskende nok, med hjælp fra GitHub Copilot, lykkedes det os at kickstarte Cypress til Playwright-migreringen på bare et par dage – og holde vores CI-pipeline kørende hele tiden uden at afbryde nogen testprocesser.
I denne artikel vil jeg dele, hvordan vi tilgik denne migrering, de lektioner vi lærte undervejs, og hvordan AI-assisteret kodemigrering drastisk kan reducere den manuelle indsats ved framework-overgange.
Projektkontekst
Projektet var en kompleks sundhedsplatform med en masse under motorhjelmen. Vores testopsætning omfattede:
- End-to-end (E2E) og integrationstests
- En brugerdefineret TAF-arkitektur med setups, teardowns og oprettelse af entiteter
- Konfigurationer for flere miljøer
- Dynamisk data og dyb navigeringslogik
I alt havde vi 148 E2E-tests kørende på Cypress – alle fungerede fint, men organisationen ønskede at standardisere QA-værktøjer på tværs af teams, hvilket betød én ting: Playwright-migrering.
Hvorfor bruge GitHub Copilot til migrering?
Da migreringsanmodningen kom ind, så jeg en mulighed for at eksperimentere med GitHub Copilot testautomatisering. I stedet for manuelt at omskrive hundredvis af testfiler ønskede jeg at se, hvor langt vi kunne presse AI-assistance til at automatisere syntaksomdannelse fra Cypress til Playwright.
Og ærligt talt, det fungerede langt bedre end forventet.
GitHub Copilot håndterede det meste af den gentagne oversættelse (selektorer, kommandoer, asynkron håndtering osv.), hvilket gjorde det muligt for os at fokusere på infrastrukturtilpasninger og finjustering.
Inden for 1-2 dage havde vi allerede en fungerende prototype af Playwright testautomatiseringsframeworket.
To vigtige ansvarsfraskrivelser
Verificer altid AI-genereret kode
Selvom AI-assisteret kodemigrering fremskynder tingene, skal du gennemgå hver eneste linje. Infrastrukturmigreringer kan nemt introducere subtile fejl eller ydelsesregressioner. Tænk på Copilot som din par-programmør, ikke en autopilot.
Hold det gamle framework kørende til det sidste
Bloker aldrig udgivelser eller regressioner under migrering. Hold dine Cypress-tests kørende på CI, indtil de tilsvarende suiter er fuldt stabile i Playwright. Når en suite er migreret og valideret, fjern den fra Cypress og skift CI til at køre den i Playwright.
Denne gradvise udrulning sikrer nul nedetid og kontinuerlig testdækning gennem hele processen.
Arbejdsgang for migrering fra Cypress til Playwright
Da grundlaget var klart, gik vi videre til den praktiske migrering.
Nedenfor er den arbejdsgang, der hjalp os med at køre begge frameworks parallelt, opretholde CI-stabilitet og begynde at migrere tests inkrementelt – uden at blokere igangværende udgivelser.
1. Initialiser Playwright og opret mappestrukturen
Det første skridt var at initialisere Playwright og oprette en ren, isoleret mappestruktur. Vi ønskede, at begge frameworks skulle leve side om side i det samme repository, så vi kunne eksekvere dem samtidigt under overgangen.
Denne opsætning gjorde det muligt for os at:
- Migrere gradvist
- Sammenligne resultater og tider mellem Cypress og Playwright
- Holde CI-pipelines kørende for begge
Eksempel på initialisering:

Her er den foreslåede mappestruktur, vi brugte til Playwright:
├── 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. Konfigurer separate tsconfig-filer for hvert framework
Et af de tidlige problemer, vi stod over for, var TypeScript-konflikter – begge frameworks definerer lignende metodenavne (expect, request osv.), og deling af en enkelt tsconfig.json førte til kompileringsfejl.
Løsningen var enkel: Opdel TypeScript-konfigurationerne. Denne struktur sikrer, at hvert framework initialiserer sine egne typdefinitioner og undgår typekollisioner:
├── tsconfig.json
├── tsconfig.cypress.json
├── tsconfig.playwright.jsonHoved-tsconfig.json
Tilføj composite-muligheden for at understøtte projektreferencer.
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"]
}Med denne adskillelse på plads kunne begge frameworks eksistere fredeligt i ét repo – ingen flere compiler-konflikter og ingen risiko for utilsigtet at initialisere delte metoder to gange.
3. Opret en prompt til at automatisere migrering med GitHub Copilot
Da Playwright var initialiseret, og projektet var struktureret, var det tid til at lade GitHub Copilot hjælpe med det tunge løft.
For at gøre dette oprettede vi en brugerdefineret prompt, der definerer, hvordan Copilot skal opføre sig, når tests omskrives fra Cypress til Playwright. Prompten fortæller agenten, hvad den skal gøre, hvordan den skal gøre det, og hvad den skal ignorere – hvilket giver konsekvente resultater på tværs af alle migreringer.
Sådan tilføjes prompten
Inde i dit GitHub-repository skal du åbne tandhjulsikonet ⚙️ i Copilot Chat og oprette en ny prompt.
Filen gemmes automatisk under: ./github/prompts/
Du kan navngive den noget i retning af migrate_tests.md.
Prompt-eksempel:
---
mode: agent
---
Du er en Playwright Test Generator, en ekspert i browserautomatisering og end-to-end-testning.
Din specialitet er at skabe robuste, pålidelige Playwright-tests, der nøjagtigt simulerer brugerinteraktioner og validerer applikationsadfærd.
Din opgave er at hjælpe med at migrere eksisterende Cypress-tests til Playwright-tests.
Cypress-tests, page object, komponenter og hjælpefiler leveres som input, og du skal generere ækvivalent Playwright testkode.
Output ALLE migrerede koder til chatten.
Når du genererer Playwright testkoden eller eventuelle page objects, skal du sikre, at:
1. Teststrukturen følger den samme struktur som Cypress-tests.
2. Opret en global fixture til opsætning og nedlukning og genbrug den i tests.
3. Alle brugerinteraktioner (klik, indtastning, navigation) oversættes nøjagtigt til Playwright-syntaks i page objects og testfiler.
4. Assertions i Cypress konverteres til ækvivalente Playwright-assertions.
5. Ignorer netværksspionering og stubbing-dele af Cypress-tests.
6. Udkommenter alle metoder med waits (f.eks. cy.wait) i Playwright-koden.
7. Brug ikke get-metoder i page objects; brug direkte metoder i stedet `backbackItem = () => this.page.locator('[id="item_4_title_link"]');`Hvorfor dette hjælper
Denne type strukturerede prompt gør det muligt for GitHub Copilot at opføre sig mere som en specialiseret migreringsassistent, ikke en generel kode-generator.
Med den rette kontekst og regler kan Copilot automatisk:
- Omskrive store dele af Cypress-tests til Playwright-syntaks
- Opretholde ensartethed i mappe- og teststrukturen
- Reducere manuel konverteringstid med 70-80 %
I bund og grund skaber du dit eget AI-migreringsværktøj – skræddersyet til dit projekts struktur og konventioner.
4. Vælg Agent-tilstand og kør migreringen trin for trin
Da prompten var klar, var næste skridt at køre migreringen med GitHub Copilot i agent-tilstand. I denne fase lod vi AI-modellen behandle hver fil – page objects, komponenter, hjælpefiler og til sidst tests – én efter én.
Til denne opgave brugte vi GPT-4.1-modellen, som leverer de mest konsekvente og kontekstbevidste kodeomdannelser.
Sådan starter du migrering
1. Åbn Copilot Chat i VS Code
2. Brug / til at vælge den promptfil, du oprettede tidligere
3. Brug # til at vælge en specifik fil (f.eks. en Cypress-test eller page object), som du vil migrere
Copilot vil nu anvende din migreringsprompt og generere en Playwright-kompatibel version af filen direkte i chatten.
Eksempel på migrering af inventoryPage Page Object
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;Finjustering af agentens adfærd
Du skal ikke bekymre dig, hvis det første output ikke er perfekt. Efter vores erfaring får AI-agenten normalt 60-70 % af koden korrekt i første forsøg. Nøglen er at holde samtalekonteksten aktiv – fortsæt med at forfine den samme chat-tråd ved at bede om rettelser eller justeringer. Når du gør dette, begynder Copilot at lære dine projektmønstre og producerer mere nøjagtig, genanvendelig kode.
I bund og grund, jo flere iterationer du laver i den samme chat, jo bedre bliver migreringskvaliteten.
Migreringsrækkefølge: Filer først, derefter tests
For at opretholde ensartede afhængigheder og undgå manglende referencer, følg denne rækkefølge:
1. Generer først ikke-testfiler:
- Page Objects
- Komponenter
- Hjælpefiler
- Konstanter
2. Når alle understøttende filer er genereret og verificeret, skal du tilføje mappen #app til chatkonteksten og derefter migrere selve testfilerne.
Verifikationstrin
Når en Playwright-test er genereret:
- Gennemgå koden – bekræft, at page locators, testtrin og assertions er korrekte.
- Kør testen ved hjælp af din Playwright CLI for at sikre, at den udføres korrekt.
- Ret mindre syntaks- eller fixture-uoverensstemmelser, hvis Copilot misforstod en Cypress-kommando.
Ved at verificere efter hver fil holder du migreringen stabil og inkrementel, hvilket undgår storstilet fejlfinding senere.
Eksempel på migreret inventoryPage-test, som bestås med succes:
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. Endelig repository-struktur efter migrering
Da migreringen var fuldført, nåede vores repository en stabil, dual-framework-tilstand – med fuld kørsel af både Cypress og Playwright side om side. Denne struktur gjorde det muligt for os at:
- Gradvist afvikle Cypress test-suiter, efterhånden som de blev erstattet
- Holde CI/CD-pipelines operationelle for begge frameworks
- Opretholde en ren og modulær arkitektur
Nedenfor er den endelige struktur efter afslutning af migreringsarbejdsgangen.
Endelig mappestruktur
├── .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/
Noter til strukturen
.github/prompts/— indeholder dine Copilot-migrationsprompter. Ved at holde disse versioneret kan du opdatere eller genbruge dem til fremtidige framework-migrations eller refaktoriseringer..github/workflows/— indeholder dine CI/CD-definitioner. Du kan køre både Playwright- og Cypress-jobs parallelt, indtil det gamle framework er fuldt ud afviklet.playwright/-mappen — fungerer nu som den primære automatiseringskilde fremover. Den afspejler strukturen fra den tidligere Cypress-implementering for at lette onboarding og sikre konsistens.tsconfig.playwright.jsonogtsconfig.cypress.json— forbliver adskilte for fuld typeisolation, hvilket forhindrer konflikter under builds.
Denne struktur understøttede ikke kun en glidende overgang, men positionerede også projektet for fremtidig skalerbarhed, CI-fleksibilitet og modulært testejerskab på tværs af teams.
Praktiske råd og lærdomme
Efter at have migreret næsten 150 tests med Copilot og Playwright, er her et par vigtige lektioner og praktiske tips, der gjorde processen glattere (og reddede os fra nogle hovedpiner).
Generelle AI-relaterede råd
- AI lyver – ofte overbevisende.
Verificer altid hver enkelt kodelinje, som AI'en genererer. Stol aldrig blindt på den, især under infrastruktur-migreringer.
- Hold dig til konsekvente navngivningskonventioner.
Brug identiske variabel- og metodenavne på tværs af frameworks – det hjælper AI-agenten med at migrere kode mere nøjagtigt og gør det lettere for dig at finde og importere dem senere.

- Genopbyg imports manuelt.
Copilot har en tendens til at rode med importstier. Det er ofte hurtigere at slette dem og importere manuelt, eller opsætte globale imports som: import x from "playwright/foo/bar";
- Genbrug den samme chat til alle migreringer.
Hold dig til en enkelt Copilot-chat-tråd – den opbygger kontekst og "husker" dine projektkonventioner, hvilket fører til bedre resultater.
- Opdel migrationopgaver.
Generer 1-2 filer ad gangen. Mindre opgaver giver mere nøjagtige resultater og gør manuel validering lettere.
Playwright-specifikke råd
- Opret aldrig flere instanser af det samme Page Object uden grund.
At gøre det kan forårsage inkonsistent tilstand eller ødelagte selektorer, når du navigerer mellem sider. Løsningen? Implementer en Page Manager, der holder alle sideobjekter tilgængelige via en enkelt instans.
Her er en minimal eksempelopsætning:
// 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();
});Andre tekniske tips
- Fjern alle "get"-præfikser fra locators, som AI-agenten genererer – brug direkte locator-metoder i stedet.
- Glem ikke at konfigurere
dotenvtil miljøvariabler og legitimationsoplysninger. - Tilpas din Playwright-konfiguration til at outputte alle rapporter under Playwright-mappen, så din projektrod forbliver ren:
reporter: [["html", { open: "never", outputFolder: "./playwright/reports/" }]],
outputDir: "./playwright/test-results",
Efterskrift
Jeg har ikke inkluderet ESLint-opsætning her, da det varierer fra projekt til projekt.
Den eneste universelle anbefaling, jeg kan give, er at installere Playwright ESLint-plugin'et for bedre regelhåndhævelse: eslint-plugin-playwright.
Hvis du gerne vil udforske et komplet fungerende eksempel på denne migrering – inklusive promptfiler, tsconfigs og CI-opsætning – kan du tjekke hele repository'et her: Eksempel på repository















