Da vi modtog en anmodning om at migrere Cypress-tests til Playwright, forventede vi en lang og kedelig omskrivningsproces.
Overraskende nok lykkedes det os med hjælp fra GitHub Copilot at starte Cypress til Playwright-migreringen på blot 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 greb denne migrering an, de erfaringer vi gjorde os undervejs, og hvordan AI-assisteret kodemigrering drastisk kan reducere den manuelle indsats ved framework-overgange.
Projektkontekst
Projektet var en kompleks sundhedsplatform med mange processer kørende i baggrunden. Vores testopsætning omfattede:
- End-to-end (E2E) og integrationstests
- En brugerdefineret TAF-arkitektur med opsætninger, nedrivninger og oprettelse af entiteter
- Multi-miljø konfigurationer
- Dynamisk data og kompleks navigationslogik
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 kun 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 syntakskonvertering fra Cypress til Playwright.
Og ærligt talt, det fungerede langt bedre end forventet.
GitHub Copilot håndterede det meste af den gentagne oversættelse (vælgere, 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 processen, skal du gennemgå hver eneste linje. Infrastrukturmigreringer kan let introducere subtile fejl eller ydelsesforringelser. Tænk på Copilot som din parprogrammør, ikke en autopilot.
Hold det gamle framework kørende til det sidste
Blokér aldrig releases 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 under hele processen.
Arbejdsgang for migrering fra Cypress til Playwright
Da grundlaget var på plads, gik vi i gang med den praktiske migrering.
Nedenfor er arbejdsgangen, der hjalp os med at køre begge frameworks parallelt, opretholde CI-stabilitet og begynde at migrere tests trinvist – uden at blokere igangværende releases.
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 eksistere side om side i det samme repository, så vi kunne udføre 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
Initialiseringseksempel:

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 stødte på, 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 simpel: 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-indstillingen for at understøtte projekt-referencer.
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 compilerkonflikter og ingen risiko for utilsigtet at initialisere delte metoder to gange.
3. Opret en prompt for 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øjer du 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 give den et navn som migrate_tests.md.
Eksempel på prompt:
---
mode: agent
---
You are a Playwright Test Generator, an expert in browser automation and end-to-end testing.
Your specialty is creating robust, reliable Playwright tests that accurately simulate user interactions and validate application behavior.
Your task is to help migrate existing Cypress tests to Playwright tests.
The Cypress tests, page object, components, and helper files are provided as input, and you need to generate equivalent Playwright test code.
Output ALL migrated code to the chat.
When generating the Playwright test code or any page objects, ensure that:
1. The test structure follows the same structure as the Cypress tests.
2. Create a global fixture for setup and teardown and reuse it in the tests.
3. All user interactions (clicks, typing, navigation) are accurately translated to Playwright syntax in page objects and test files.
4. Assertions in Cypress are converted to equivalent Playwright assertions.
5. Ignore the network spying and stubbing parts of the Cypress tests.
6. Comment out all the methods with waits (e.g., cy.wait) in the Playwright code.
7. Do not use get methods in page objects; use direct methods instead `backbackItem = () => this.page.locator('[id="item_4_title_link"]');`Derfor hjælper det
Denne type strukturerede prompt gør det muligt for GitHub Copilot at opføre sig mere som en specialiseret migreringsassistent og ikke en generel kode-generator.
Med den rette kontekst og regler kan Copilot automatisk:
- Omskrive store dele af Cypress-tests til Playwright-syntaks
- Opretholde konsistens i mappe- og teststrukturen
- Reducere manuel konverteringstid med 70–80%
I bund og grund skaber du din egen AI-migrationsværktøjskæde – skræddersyet til dit projekts struktur og konventioner.
4. Vælg agenttilstand og kør migreringen trin for trin
Da prompten var klar, var næste skridt at køre migreringen med GitHub Copilot i agenttilstand. I denne fase lod vi AI-modellen behandle hver fil – sideobjekter, komponenter, hjælpefiler og til sidst tests – én efter én.
Til denne opgave brugte vi GPT-4.1-modellen, som leverer de mest konsistente og kontekstbevidste kodeomformninger.
Sådan starter du migrering
1. Åbn Copilot Chat i VS Code
2. Brug / til at vælge den prompt-fil, du oprettede tidligere
3. Brug # til at vælge en specifik fil (f.eks. en Cypress-test eller et sideobjekt), som du vil migrere
Copilot vil nu anvende din migrationsprompt 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. Ifølge vores erfaring rammer AI-agenten typisk 60-70 % af koden rigtigt ved første forsøg. Nøglen er at holde samtalekonteksten aktiv – fortsæt med at finjustere 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 udfører i den samme chat, jo bedre bliver migreringskvaliteten.
Migreringsrækkefølge: Filer først, derefter tests
For at opretholde konsistente afhængigheder og undgå manglende referencer skal du følge denne rækkefølge:
1. Generer ikke-testfiler først:
- Sideobjekter
- Komponenter
- Hjælpefiler
- Konstanter
2. Når alle understøttende filer er genereret og verificeret, skal du tilføje #app-mappen til chatkonteksten og derefter migrere selve testfilerne.
Verifikationstrin
Når en Playwright-test er genereret:
- Gennemgå koden – bekræft, at side-locatorer, 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 opretholder du en stabil og trinvis migrering og undgår omfattende fejlfinding senere.
Eksempel på migreret inventoryPage-test, der kører fejlfrit:
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, opnåede vores repository en stabil, dobbelt-framework-tilstand – med både Cypress og Playwright kørende side om side. Denne struktur gjorde det muligt for os at:
- Gradvist udfase Cypress-testsuiter, 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 fuldførelse 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/
Bemærkninger om strukturen
.github/prompts/– indeholder dine Copilot-migreringsprompts. Ved at versionere disse kan du opdatere eller genbruge dem til fremtidige framework-migreringer eller refaktoreringer..github/workflows/– indeholder dine CI/CD-definitioner. Du kan køre både Playwright- og Cypress-jobs parallelt, indtil den fulde udfasning af det gamle framework.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 separate for fuld typeisolation, hvilket forhindrer konflikter under builds.
Praktiske råd og lærdom
Efter at have migreret næsten 150 tests med Copilot og Playwright, er her et par vigtige erfaringer og praktiske tips, der gjorde processen smidigere (og reddede os fra nogle hovedpiner).
Generelle AI-relaterede råd
- AI lyver – ofte overbevisende.
Verificer altid hver eneste kodelinje, AI genererer. Stol aldrig blindt på den, især under infrastrukturmigreringer.
- Overhold konsekvente navngivningskonventioner.
Bevar 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 import-stier. Det er ofte hurtigere at slette dem og importere manuelt igen, 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 migreringsopgaver.
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.
Dette kan forårsage inkonsistent tilstand eller ødelagte vælgere, 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 locatorer, 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 håndhævelse af regler: eslint-plugin-playwright.
Hvis du gerne vil udforske et komplet fungerende eksempel på denne migrering – inklusive prompt-filer, tsconfigs og CI-opsætning – kan du tjekke det fulde repository her: Repo-eksempel














