Da vi fikk en forespørsel om å migrere Cypress-tester til Playwright, forventet vi en lang, kjedelig omskrivingsprosess.
Overraskende nok, med hjelp fra GitHub Copilot, klarte vi å starte Cypress til Playwright-migreringen på bare et par dager – og holde CI-pipeline vår i gang hele tiden uten å avbryte noen testprosesser.
I denne artikkelen vil jeg dele hvordan vi tilnærmet oss denne migreringen, lærdommene vi tok med oss, og hvordan AI-assistert kodemigrering drastisk kan redusere det manuelle arbeidet ved rammeverksendringer.
Prosjektkontekst
Prosjektet var en kompleks helseplattform med mye som skjedde under panseret. Vårt testoppsett inkluderte:
- Ende-til-ende (E2E) og integrasjonstester
- En tilpasset TAF-arkitektur med oppsett, nedrivninger og enhetsopprettelse
- Flere miljøkonfigurasjoner
- Dynamiske data og dyp navigasjonslogikk
Totalt hadde vi 148 E2E-tester som kjørte på Cypress – alle fungerte fint, men organisasjonen ønsket å standardisere QA-verktøy på tvers av teamene, noe som betydde én ting: Playwright-migrering.
Hvorfor bruke GitHub Copilot for migrering?
Da migreringsforespørselen kom inn, så jeg en mulighet til å eksperimentere med GitHub Copilot for testautomatisering. I stedet for å manuelt omskrive hundrevis av testfiler, ønsket jeg å se hvor langt vi kunne presse AI-assistanse for å automatisere syntaksomformingen fra Cypress til Playwright.
Og ærlig talt, det fungerte langt bedre enn forventet.
GitHub Copilot håndterte det meste av den gjentakende oversettelsen (selektorer, kommandoer, asynkron håndtering osv.), slik at vi kunne fokusere på infrastrukturjusteringer og finjustering.
Innen 1–2 dager hadde vi allerede en fungerende prototype av Playwright-testautomatiseringsrammeverket.
To viktige forbehold
Verifiser alltid AI-generert kode
Selv om AI-assistert kodemigrering fremskynder ting, må du gjennomgå hver eneste linje. Infrastrukturmigreringer kan lett introdusere subtile feil eller ytelsesforringelser. Tenk på Copilot som din parprogrammerer, ikke en autopilot.
Hold det gamle rammeverket i gang til slutten
Blokker aldri utgivelser eller regresjoner under migrering. Hold Cypress-testene dine kjørende på CI til de tilsvarende suitene er helt stabile i Playwright. Når en suite er migrert og validert, fjern den fra Cypress og bytt CI til å kjøre den i Playwright.
Denne gradvise utrullingen sikrer null nedetid og kontinuerlig testdekning gjennom hele prosessen.
Arbeidsflyt for å migrere fra Cypress til Playwright
Når grunnarbeidet var klart, gikk vi over til den praktiske migreringen.
Nedenfor er arbeidsflyten som hjalp oss med å kjøre begge rammeverk parallelt, opprettholde CI-stabilitet og starte migrering av tester trinnvis – uten å blokkere pågående utgivelser.
1. Initialiser Playwright og opprett mappestrukturen
Det første trinnet var å initialisere Playwright og opprette en ren, isolert mappestruktur. Vi ønsket at begge rammeverkene skulle ligge side om side i samme repository, slik at vi kunne utføre dem samtidig under overgangen.
Dette oppsettet tillot oss å:
- Migrere gradvis
- Sammenligne resultater og tider mellom Cypress og Playwright
- Holde CI-pipelines kjørende for begge
Initialiseringseksempel:

Her er den foreslåtte mappestrukturen vi brukte for 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 rammeverk
Et av de tidlige problemene vi møtte var TypeScript-konflikter – begge rammeverkene definerer lignende metodenavn (expect, request, osv.), og å dele én enkelt tsconfig.json førte til kompileringsfeil.
Løsningen var enkel: dele opp TypeScript-konfigurasjonene. Denne strukturen sikrer at hvert rammeverk initialiserer sine egne typedefinisjoner og unngår typekollisjoner:
├── tsconfig.json
├── tsconfig.cypress.json
├── tsconfig.playwright.jsonHoved-tsconfig.json
Legg til composite-alternativet for å støtte prosjektreferanser.
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 separasjonen på plass kunne begge rammeverkene eksistere fredelig i ett repository – ingen flere kompileringskonflikter, og ingen risiko for ved et uhell å initialisere delte metoder to ganger.
3. Opprett en ledetekst for å automatisere migrering med GitHub Copilot
Når Playwright var initialisert og prosjektet strukturert, var det på tide å la GitHub Copilot hjelpe til med det tunge arbeidet.
For å gjøre dette opprettet vi en tilpasset ledetekst som definerer hvordan Copilot skal oppføre seg når tester skrives om fra Cypress til Playwright. Ledeteksten forteller agenten hva den skal gjøre, hvordan den skal gjøre det, og hva den skal ignorere – noe som gir konsistente resultater på tvers av alle migreringer.
Slik legger du til ledeteksten
Inne i GitHub-repositoryet ditt åpner du tannhjulikonet ⚙️ i Copilot Chat og oppretter en ny ledetekst.
Filen vil bli lagret automatisk under: ./github/prompts/
Du kan gi den et navn som migrate_tests.md.
Eksempel på ledetekst:
---
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"]');`Hvorfor dette hjelper
Denne typen strukturert ledetekst gjør at GitHub Copilot kan oppføre seg mer som en spesialisert migreringsassistent, ikke en generell kodegenerator.
Med riktig kontekst og regler kan Copilot automatisk:
- Omskrive store deler av Cypress-tester til Playwright-syntaks
- Opprettholde konsistens i mappe- og teststrukturen
- Redusere manuell konverteringstid med 70–80 %
I hovedsak skaper du din egen AI-migreringsverktøy – skreddersydd til prosjektets struktur og konvensjoner.
4. Velg agentmodus og kjør migreringen trinn for trinn
Når ledeteksten var klar, var neste trinn å faktisk kjøre migreringen med GitHub Copilot i agentmodus. I denne fasen lot vi AI-modellen behandle hver fil – sideobjekter, komponenter, hjelpere og til slutt testene – én etter én.
For denne oppgaven brukte vi GPT-4.1-modellen, som leverer de mest konsistente og kontekstbevisste kodetransformasjonene.
Slik starter du migreringen
1. Åpne Copilot Chat i VS Code
2. Bruk / for å velge ledetekstfilen du opprettet tidligere
3. Bruk # for å velge en spesifikk fil (for eksempel en Cypress-test eller et sideobjekt) du vil migrere
Copilot vil nå anvende migreringsledeteksten din og generere en Playwright-kompatibel versjon av filen direkte i chatten.
Eksempel på migrering av inventoryPage sideobjekt
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;Justering av agentens oppførsel
Ikke bekymre deg om det første utdataet ikke er perfekt. Erfaringsmessig treffer AI-agenten vanligvis 60–70 % av koden riktig på første forsøk. Nøkkelen er å holde samtalekonteksten aktiv – fortsett å videreutvikle den samme chat-tråden ved å be om fikser eller justeringer. Etter hvert som du gjør dette, begynner Copilot å lære prosjektmønstrene dine og produserer mer nøyaktig, gjenbrukbar kode.
I hovedsak, jo flere iterasjoner du gjør i den samme chatten, desto bedre blir migreringskvaliteten.
Migreringsrekkefølge: Filer først, deretter tester
For å holde avhengigheter konsistente og unngå manglende referanser, følg denne rekkefølgen:
1. Generer ikke-testfiler først:
- Sideobjekter
- Komponenter
- Hjelpefiler
- Konstanter
2. Når alle støttefiler er generert og verifisert, legg til #app-mappen i chat-konteksten, og migrer deretter testfilene selv.
Verifiseringstrinn
Når en Playwright-test er generert:
1. Gjennomgå koden – bekreft at sidens lokatorer, testtrinn og påstander er korrekte.
2. Kjør testen ved hjelp av Playwright CLI for å sikre at den utføres riktig.
3. Fiks mindre syntaks- eller fixture-uoverensstemmelser hvis Copilot misforsto en Cypress-kommando.
Ved å verifisere etter hver fil holder du migreringen stabil og trinnvis, og unngår storskala feilsøking senere.
Eksempel på migrert inventoryPage-test som bestås med hell:
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 etter migrering
Når migreringen var fullført, nådde vårt repository en stabil tilstand med to rammeverk – med både Cypress og Playwright som kjørte side om side. Denne strukturen tillot oss å:
- Gradvis avvikle Cypress test-suiter etter hvert som de ble erstattet
- Holde CI/CD-pipelines operative for begge rammeverkene
- Opprettholde en ren og modulær arkitektur
Nedenfor er den endelige strukturen etter fullføring av migreringsarbeidsflyten.
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/
Merknader om strukturen
.github/prompts/– inneholder dine Copilot-migreringsledetekster. Å holde disse versjonskontrollert gjør at du kan oppdatere eller gjenbruke dem for fremtidige rammeverksmigreringer eller refaktoreringer..github/workflows/– inneholder dine CI/CD-definisjoner. Du kan kjøre både Playwright- og Cypress-jobber parallelt til full avvikling av det gamle rammeverket.playwright/-mappen – fungerer nå som den primære automatiseringskilden fremover. Den gjenspeiler strukturen til den tidligere Cypress-implementeringen for å lette onboarding og sikre konsistens.tsconfig.playwright.jsonogtsconfig.cypress.json– forblir separate for full type-isolasjon, noe som forhindrer konflikter under bygg.
Denne strukturen støttet ikke bare en jevn overgang, men posisjonerte også prosjektet for fremtidig skalerbarhet, CI-fleksibilitet og modulær testansvar på tvers av team.
Praktiske råd og lærdom
Etter å ha migrert nesten 150 tester med Copilot og Playwright, er her noen viktige lærdommer og praktiske tips som gjorde prosessen jevnere (og sparte oss for en del hodebry).
Generelle AI-relaterte råd
- AI lyver – ofte overbevisende.
Verifiser alltid hver eneste kodelinje AI-en genererer. Stol aldri blindt på den, spesielt under infrastrukturmigreringer.
- Hold deg til konsistente navnekonvensjoner.
Behold identiske variabel- og metodenavn på tvers av rammeverk – det hjelper AI-agenten med å migrere kode mer nøyaktig og gjør det enklere for deg å finne og importere dem senere.

- Gjenoppbygg importer manuelt.
Copilot har en tendens til å rote til importstier. Det er ofte raskere å slette dem og importere på nytt manuelt, eller sette opp globale importer som: import x from "playwright/foo/bar";
- Gjenbruk den samme chatten for alle migreringer.
Hold deg til en enkelt Copilot-chat-tråd – den bygger kontekst og «husker» prosjektkonvensjonene dine, noe som fører til bedre resultater.
- Dekomponer migrerings oppgaver.
Generer 1–2 filer om gangen. Mindre oppgaver gir mer nøyaktige utdata og gjør manuell validering enklere.
Playwright-spesifikke råd
- Opprett aldri flere instanser av samme sideobjekt uten grunn.
Dette kan forårsake inkonsekvent tilstand eller ødelagte selektorer når du navigerer mellom sider. Løsningen? Implementer en Page Manager som holder alle sideobjekter tilgjengelige gjennom en enkelt instans.
Her er et minimalt eksempeloppsett:
// 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»-prefiks fra lokatorer som AI-agenten genererer – bruk direkte lokatormetoder i stedet.
- Ikke glem å konfigurere
dotenvfor miljøvariabler og legitimasjon. - Tilpass Playwright-konfigurasjonen din for å skrive ut alle rapporter under Playwright-mappen, slik at prosjektroten forblir ren:
reporter: [["html", { open: "never", outputFolder: "./playwright/reports/" }]],
outputDir: "./playwright/test-results",
Etterord
Jeg har ikke tatt med ESLint-oppsett her siden det varierer fra prosjekt til prosjekt.
Den eneste universelle anbefalingen jeg kan gi er å installere Playwright ESLint-pluginet for bedre håndhevelse av regler: eslint-plugin-playwright.
Hvis du ønsker å utforske et komplett fungerende eksempel på denne migreringen – inkludert ledetekstfiler, tsconfigs og CI-oppsett – kan du sjekke ut hele repositoryet her: Repo-eksempel














