Home / Services / Protractor to Playwright migration

Protractor to Playwright migration for a suite you inherited

You end up with a Playwright suite your current team can read, and no protractor in package.json holding the Angular upgrade up.

Short answer

We move Protractor suites into Playwright: every spec converted, rewritten or dropped, the Protractor toolchain out of the repository, and a record of what happened to each test. The Angular team announced the deprecation in August 2022 and put end of life in summer 2023, so nothing about this suite improves on its own.

Who this is for

An Angular application old enough to have an end-to-end suite in Protractor, and a team that did not write it. Nobody starts a Protractor suite now, so the specs predate most of the people looking at them.

Test quality almost never forces the decision. One of these does:

  • An Angular upgrade that has stopped, with the e2e toolchain named in the blocking ticket.
  • A CI image or a Node version the suite has never been tested against. Protractor 7.0.0 declares node >=10.13.x in its engines field, and dist-tags.latest still resolves to 7.0.0.
  • Nobody left wants the suite, and nobody you interview has used the framework.

There is no language decision here: a Protractor suite is already TypeScript or JavaScript and stays that way. We work in Playwright's other three official bindings too, Python, Java and .NET.

If you could read the whole suite in an afternoon, open the free guide below and do it yourselves. If the repository also holds a half-built replacement in something else, start at migrating from any other framework.

Why this is on your roadmap at all

Protractor's own team stopped developing it and said so in public. The Angular blog post, dated 10 August 2022, records that Protractor had already been dropped from new Angular CLI applications at v12, that updates from that day were limited to security vulnerabilities and browser incompatibilities, and that the last release was scheduled for Angular v16 in the summer of 2023. npm's registry entry for the package carries the one-line version, a deprecation notice on protractor@7.0.0 reading "We have news to share - Protractor is deprecated and will reach end-of-life by Summer 2023." The README on the repository's master branch carries no such notice and still describes the project in the present tense, so a colleague who checks there will find nothing, and that gap has never been explained. The same announcement points teams who cannot move yet at a supported fork run by HeroDevs, which is the alternative to everything below.

What the official guide gives you free

Playwright publishes a Protractor migration guide. Open it before this page and before you talk to anyone: a ten-row cheat sheet mapping element(by.*) onto page.locator(), one before-and-after example converted line by line, and two polyfills for waitForAngular. There is no equivalent guide for Selenium or Cypress. If your suite is small and its specs are straightforward, that page is your migration and you are done here.

A translation table decides the easy half. It leaves these open:

  • Whether a test is worth converting. The table assumes you want every test you have, which on a suite whose authors have left is the expensive assumption.
  • What the new suite's locators look like. Every row lands on page.locator() with a CSS or XPath string, which is how four hundred tests arrive in Playwright addressed the Protractor way.
  • Where the suite was already fighting Protractor's synchronisation. The call sites where somebody switched Angular waiting off are the map of what flakes after conversion, and no cheat sheet has a row for it.
  • Everything that is not a spec: protractor.conf.js, the onPrepare hook your login lives in, the CI job, and the protractor dependency itself.

What you get

The converted suite

Specs land in your repository the way your own code does, as pull requests your team reviews. Each spec is converted, rewritten against what the screen does now, or dropped with a line saying why.

The repository with Protractor taken out of it

The protractor dependency, protractor.conf.js, whatever wires your e2e command to it, and the CI job that called it. That is a change to your build rather than to your tests, and the one your Angular upgrade is waiting on.

The migration record, deletions first

One file that sets the old suite beside the new one, test by test. The deletions are the half to read: on a suite nobody left wrote, working out what a spec was proving is the job.

How it works

  1. Inventory

    The inventory reads for things the other two migration pages do not have to look for. What each spec was meant to prove, reconstructed from the code, the spec names and the commit messages, as a sentence per test — including the tests where the sentence is that nobody can say. Every call site where the suite turned Angular waiting off: waitForAngularEnabled(false) and, on older suites, browser.ignoreSynchronization, deprecated in Protractor's changelog at 6.0.0 in favour of the first. And whether the suite still executes. You give us the repository, the pipeline, and time with whoever remembers why it looks like this.

  2. A pilot slice

    One flow, converted the whole way through and green in your pipeline before the rest is touched. If the suite has not had a green run in a year this phase gets bigger: it has to establish what the application does as well as what the spec claims it did. You give us the flow that matters most, and somewhere we can run it.

  3. Bulk conversion

    Everything else, in batches small enough to read, in the vocabulary the pilot settled, each reviewed and merged before the next begins. You give us a reviewer who knows what the product is supposed to do.

  4. Cutover

    For a while both suites run on every commit. Once the Playwright job has been green everywhere the Protractor job was, the Protractor job comes out of the pipeline and the toolchain out of the repository. You give us the decision that the overlap has run long enough.

The wait that has nowhere to go

Here is a spec that clicks a button, waits out an HTTP round trip and asserts on what comes back. There is no wait anywhere in it, because Protractor put one there for you:

import { browser, by, element } from 'protractor';

describe('invoices', () => {
  it('shows the new invoice after the refresh', async () => {
    await browser.get('/invoices/');
    await element(by.css('[data-testid="refresh"]')).click();

    const row = element(by.css('[data-testid="invoice-row"]'));
    expect(await row.getText()).toEqual('INV-1042');
  });
});

That is Protractor 7.0.0, with an await on every call on purpose: control flow was removed at 6.0.0, and the changelog's own line is "Control flow is removed and you should use async await to run your tests". Most Protractor code still circulating has no await anywhere, and the current version cannot run it.

The same test in Playwright, with the wait inside the assertion:

import { test, expect } from '@playwright/test';

test('shows the new invoice after the refresh', async ({ page }) => {
  await page.goto('/invoices/');
  await page.getByRole('button', { name: 'Refresh' }).click();

  await expect(page.getByTestId('invoice-row')).toHaveText('INV-1042');
});
Playwright 1.62 for every sample below; the Protractor above is 7.0.0.

Protractor's runner asked Angular whether it had settled before it did anything, which is why a Protractor suite reads as though waiting were not a problem, and why those suites fall over on the parts of a page Angular does not control.

Playwright needs no such hook. The auto-waiting documentation says it "auto-waits for all the relevant checks to pass and only then performs the requested action". For a click that means the locator matches one element and no more, and the element is visible, done animating, unobscured and enabled. Assertions retry on the same principle, and the official guide sets out the equivalence in its Migration Principles list, pairing waitForAngular with Playwright Test auto-waiting.

That guide also ships a polyfill for waitForAngular, in two forms: one that keeps protractor in your package.json so it can borrow the framework's client-side script, and a shorter one calling window.getAllAngularTestabilities(), which the guide notes works only on Angular 2 and above. Either is a fair bridge mid-conversion. Neither belongs in a finished suite: a converted test that still asks Angular whether it has settled has been translated rather than migrated. How long Playwright waits, and what it waits for is the long version.

The two cheat-sheet rows that only mean anything on AngularJS

Check those two rows before you plan anything. element(by.model('...')) becomes page.locator('[ng-model="..."]'), and element(by.repeater('...')) becomes page.locator('[ng-repeat="..."]'). Both are exact, and the right-hand side of each is an attribute selector, so it finds something only if that attribute is in your rendered HTML. Open dev tools and search for ng-model: ten seconds, and it tells you more about the shape of this job than the spec count does.

Protractor's README says why: the project works with AngularJS and is compatible with Angular applications, but "for Angular apps, the binding and model locators are not supported", and it recommends by.css instead. So a suite full of by.model was written against AngularJS, while one written for Angular 2 and later is already full of CSS strings — two different jobs, and the second carries a trap the first does not, because a CSS string that still resolves can point at a component somebody rebuilt underneath it. The cheat sheet is ten rows and by.binding is not one of them, so that translation is yours.

Either way, every row lands on page.locator() with a string, which is what a like-for-like table is for and also the one moment you get to choose differently. Playwright's locators documentation names getByRole, getByText, getByLabel, getByPlaceholder, getByAltText, getByTitle and getByTestId as the recommended built-in locators, and the best practices page gives the reason under its own heading, "Prefer user-facing attributes to XPath or CSS selectors". The same converted test, twice:

import { test, expect } from '@playwright/test';

test('adds a todo through the AngularJS attribute', async ({ page }) => {
  await page.goto('/todos/');
  await page.locator('[ng-model="todoList.todoText"]').fill('first test');
  await page.getByRole('button', { name: 'Add' }).click();

  await expect(page.getByRole('listitem')).toHaveText(['first test']);
});

test('adds a todo through a locator that outlives the attribute', async ({ page }) => {
  await page.goto('/todos/');
  await page.getByLabel('New todo').fill('first test');
  await page.getByRole('button', { name: 'Add' }).click();

  await expect(page.getByRole('listitem')).toHaveText(['first test']);
});

The second survives somebody deleting ng-model, and which of the two your suite ends up in gets decided once, in the pilot. Choosing locators for a suite you are going to keep runs that argument at length.

protractor.conf.js does not become one file

The config carries jobs that Playwright splits between a config file, a fixture and a pipeline, so mapping it key by key is the wrong exercise. Cut to the keys worth arguing about:

const { browser, by, element } = require('protractor');

/** @type {import('protractor').Config} */
exports.config = {
  directConnect: true,
  baseUrl: 'https://staging.example.com',
  specs: ['e2e/**/*.e2e-spec.ts'],
  suites: { smoke: 'e2e/smoke/*.e2e-spec.ts' },
  rootElement: 'app-root',
  allScriptsTimeout: 11000,
  getPageTimeout: 10000,
  restartBrowserBetweenTests: false,
  capabilities: { browserName: 'chrome' },
  framework: 'jasmine',
  jasmineNodeOpts: { defaultTimeoutInterval: 30000 },

  onPrepare: async () => {
    await browser.get('/login');
    await element(by.css('#username')).sendKeys('ada');
    await element(by.css('#password')).sendKeys('hunter2');
    await element(by.css('button[type="submit"]')).click();
  },
};

Most of it moves without an argument. specs and suites become testDir, a match pattern and tags; baseUrl becomes use.baseURL; capabilities becomes an entry under projects; and framework: 'jasmine' with its jasmineNodeOpts disappears, because runner and assertion library arrive together in @playwright/test.

Three keys have nothing to map onto, for one reason. rootElement exists so Protractor can find your Angular application on the page. allScriptsTimeout is documented as needing to be longer than the maximum time your application takes to stabilise between tasks, and getPageTimeout is how long to wait for a page to load. All three are the synchronisation model showing through the config, and Playwright waits per action instead.

onPrepare is the key that costs time. Protractor's reference describes it as a callback that runs once Protractor is ready and before the specs execute, once per capability — in practice, where the login lives, along with the reporter registration and whatever global state every spec quietly assumes. It is a function in a config file, so nothing in the test runner knows it ran. In Playwright that work comes apart: signing in becomes a fixture the specs ask for by name, or a stored authentication state; the reporter is a config key; anything genuinely global becomes a setup project. All of it is invisible to an estimate built from a spec count.

directConnect: true means Protractor was driving chromedriver on the same machine, so there is no grid here to argue about. One Playwright key has nothing above it to map from: trace: 'on-first-retry' records a failing rerun in enough detail to replay, which is what your team reaches for the first time a converted spec fails for a reason nobody recognises.

Where this stops

An application this old usually contains something the engagement does not cover. Those specs get named during the inventory, and they are not in the quote.

  • Internet Explorer. A codebase old enough to carry a Protractor suite is old enough to have IE11 in its capabilities. Playwright does not support that browser, so those specs do not move, and no shape of this engagement moves them.
  • The Angular application itself. We convert the tests and take the Protractor toolchain out of the repository. We do not upgrade Angular; unblocking that upgrade is not the same as doing it.
  • Native mobile apps. Playwright emulates a browser at phone size — a viewport, a user-agent string, touch events. It cannot drive an iOS or Android build, so any part of the suite that does stays where it is and we sell you nothing to replace it.
  • Load and performance at scale. Playwright puts one real browser through one journey at a time. Generating concurrent load takes a different instrument, and that work is not sold here.
  • Safari on a real iOS device. Playwright ships its own WebKit build, which is not the browser on somebody's iPhone: the browser documentation says Playwright "doesn't work with the branded version of Safari since it relies on patches". Teams who need that browser hold on to a device cloud for it.

Security and penetration testing are not offered here either, in any framework.

What it costs

The work is engineer time, so engineers are what gets priced. Engineers are billed hourly, from $50 an hour, depending on where the engineer sits. The minimum engagement is one full-time engineer for one month.

What you are billed for on a Protractor suite is how many specs survive the inventory, not how many exist. A suite where a third of the specs cover a screen that has been redesigned twice since anyone last ran them is a smaller job than its spec count says, and the spec count is the number every buyer leads with. The other lever is whether the suite still runs: converting a spec you can watch pass is a different job from converting one nobody has seen work.

Read first

Questions

Is Protractor really end of life?

Yes, and you do not have to take it from us: the Angular team announced it themselves. Their blog post of 10 August 2022 says Protractor was dropped from new Angular CLI applications at v12, that updates were limited from that day to security vulnerabilities and browser incompatibilities, and that the last release was scheduled for Angular v16 in the summer of 2023. npm's registry entry carries a deprecation notice on protractor 7.0.0 putting end of life at Summer 2023; the README on master carries none.

Do our tests get converted, or rewritten?

Both, and which one a spec gets is settled in the inventory, test by test. A spec that still describes what the screen does is converted. A spec whose screen has been redesigned twice since anyone ran it is rewritten, which costs more. A spec nobody can connect to a requirement is a candidate for deletion, and you make that call.

What happens to browser.waitForAngular()?

It comes out, and nothing takes its place, because the job it was doing happens per action instead. Playwright's auto-waiting documentation says it auto-waits for all the relevant checks to pass and only then performs the requested action, and its assertions retry until they pass or the timeout runs out. The official Protractor guide ships a polyfill in two forms for the edge cases.

Can you get Protractor out of the repository so we can upgrade Angular?

Removing it is the second deliverable on this page: the protractor dependency, protractor.conf.js, whatever wires your e2e command to it, and the CI job that called it. That is a build change, so it lands in the cutover phase. We take the toolchain out, and we do not upgrade the Angular application.

Do we have to do the audit before you start work?

No. Most clients arrive knowing the job, and we scope that and start. The audit is for the team that wants to know what it has before it picks a direction, so it is a route in and never a gate. It is optional, it is billed by the hour, and how long it takes depends on the project. The minimum of one full-time engineer for one month does not apply to it.

Three answers and we can size this

The spec count, the Angular version, and whether the suite has had a green run this year. The last one moves the estimate most. We do not need an audit to start. If you would rather look before deciding, what the audit covers includes which tests flake and why, and what should happen first. An engineer by the month can run the suite once the last batch merges.