How to Migrate Protractor Tests to Angular
Move an existing Protractor end-to-end suite for an Angular app to Playwright or Cypress with a practical migration plan, code examples, and troubleshooting.
Direct answer: “Migrate Protractor tests to Angular” means moving the end-to-end tests for an Angular application from Protractor to a maintained runner such as Playwright or Cypress. It usually changes test code, configuration, and CI setup; it does not require changing the Angular application itself. There is no universal replacement, so choose based on your browser, CI, team, and test requirements.
Protractor’s repository was archived on July 29, 2024. The Angular team’s 2021 RFC had proposed ending development around Angular 15 at the end of 2022 and named August 2023 as an end-of-life milestone; those were proposed dates, distinct from the later archive notice. The RFC and repository notice also reported close to 1,000 responses to a January 2021 survey, with fewer than 20% of respondents reporting Protractor use. Those figures are historical, not current market shares.
1. Choose a destination runner
The Angular team wrote that “there is no one-size-fits-all solution for all Angular projects out there.” Consider the following questions before converting tests:
- Which browser engines and browser versions must run in CI?
- Does your team need standards-based WebDriver compatibility?
- How much existing code uses custom Protractor helpers, page objects, or browser-specific behavior?
- Which runner fits your Angular CLI setup, parallel execution needs, reporting, and debugging workflow?
- Does the suite cover non-Angular pages, multiple origins, or other browsing contexts?
- How does the candidate runner handle retries and asynchronous UI updates?
Angular’s current end-to-end testing documentation provides setup paths for Cypress and Playwright. At the time represented by that documentation, the commands are ng add @cypress/schematic and ng add playwright-ng-schematics. Confirm the current integration instructions before using either command because CLI packages can change.
The Angular RFC also listed Selenium WebDriver, Puppeteer, TestCafe, and WebdriverIO as alternatives. Selenium’s API is relatively close to Protractor because Protractor uses WebDriver underneath, but it is not a drop-in replacement; remove any remaining Control Flow assumptions. Do not choose a runner based on a presumed universal ranking: the right choice depends on your project.
2. Inventory the suite before changing it
Treat your current tests as a record of user-visible behavior to preserve. Before editing, record:
- Test scenarios and the important outcomes each assertion protects.
- Page objects, custom locators, shared helpers, setup and teardown hooks.
- Browser settings, base URLs, environment variables, test data, authentication, and test ordering assumptions.
- Package scripts, browser or driver provisioning, CI jobs, reports, and artifacts used for debugging failures.
- Selectors that depend on Angular-specific behavior or fragile markup.
This inventory is a practical risk-control step, not an official mandatory checklist. It makes hidden dependencies visible and helps you select a representative first test.
3. Migrate a representative test to Playwright
Playwright’s official Protractor migration guide maps CSS and other locators to page.locator(...), navigation to await page.goto(...), and URL reads to page.url(). Playwright test functions are asynchronous, and actions must be awaited. The following is a runnable-style example for a project with Playwright Test configured and an Angular app reachable at the given URL:
import { test, expect } from '@playwright/test';
test('search results show the submitted query', async ({ page }) => {
await page.goto('http://localhost:4200');
await page.locator('[data-testid="search-input"]').fill('invoices');
await page.locator('[data-testid="search-submit"]').click();
await expect(page.locator('[data-testid="search-results"]'))
.toContainText('invoices');
});
Replace the example URL and selectors with the ones in your application. A locator that worked against old markup is not automatically resilient: if the application supports stable test IDs or accessible roles and names, consider using them and verify that each selector identifies the intended rendered control.
A mechanical translation table helps with common APIs, but it is not a guarantee that each old call has an exact equivalent:
| Protractor pattern | Playwright direction | Migration note |
|---|---|---|
browser.get(url) |
await page.goto(url) |
Await navigation; decide whether to wait for a particular page state. |
element(by.css(selector)) |
page.locator(selector) |
Revalidate the selector against current rendered markup. |
browser.getCurrentUrl() |
page.url() |
Read the current page URL after the action that changes it. |
| Protractor action | Playwright action such as fill, click, or press |
Use the target API’s locator and action semantics. |
| Protractor assertion | Playwright Test’s expect |
Assert the user-visible result, not merely that translated code runs. |
4. Migrate a representative test to Cypress
Cypress uses its own command and query model. Translate to that model instead of wrapping Protractor calls mechanically. The official Cypress migration guide maps browser.get to cy.visit and browser back or forward actions to cy.go.
describe('search', () => {
it('shows results for the submitted query', () => {
cy.visit('http://localhost:4200');
cy.get('[data-testid="search-input"]').type('invoices');
cy.get('[data-testid="search-submit"]').click();
cy.get('[data-testid="search-results"]')
.should('contain.text', 'invoices');
});
});
Use the Cypress configuration and test file conventions installed for your project. Cypress retries DOM queries and assertions according to its command model and defaultCommandTimeout. Protractor assumed Angular unless configured otherwise; Cypress does not require disabling Angular behavior just to visit a non-Angular page.
5. What replaces waitForAngular()?
Usually, nothing needs to replace every call one for one. Remove the blanket wait and rely on the destination runner’s documented synchronization model, then add a targeted wait only when the test has a specific condition to observe.
Playwright: auto-waiting, with Testability for an edge case
Playwright’s built-in auto-waiting makes Protractor’s waitForAngular unnecessary in the general case. Prefer an assertion about the expected UI state. For an exceptional case where Angular’s Testability signal is specifically required, Playwright documents this Angular 2+ workaround:
await page.evaluate(() => {
return new Promise<void>(resolve => {
const testabilities = (window as any).getAllAngularTestabilities?.();
if (!testabilities?.length) {
resolve();
return;
}
let remaining = testabilities.length;
for (const testability of testabilities) {
testability.whenStable(() => {
remaining -= 1;
if (remaining === 0) resolve();
});
}
});
});
This assumes the Angular Testability API is exposed in the page. Consult the Playwright migration guide for its documented polyfill option, which relies on Protractor client-side scripts. Treat both techniques as exceptional compatibility tools, not default wrappers for every test.
Cypress: retry queries and assertions
Cypress retries DOM queries and chained assertions until they pass or the configured defaultCommandTimeout is reached. Express the expected condition directly, such as cy.get(selector).should('be.visible'). Avoid arbitrary sleeps when a retrying query or assertion can observe the real condition. These runner mechanisms are different; do not assume they have identical semantics.
The Protractor RFC explains the migration pressure: Protractor’s stability detection relied on Angular Testability, coupling the test runner to framework internals. The RFC noted that other teams can use retry strategies without requiring the test platform to understand Angular internals. A retry strategy still needs an assertion that represents the state your application should reach.
6. Convert incrementally and verify behavior
- Choose one flow that exercises navigation, a form interaction, asynchronous rendering, and a meaningful assertion.
- Convert it manually with the destination runner’s official guide and remove Control Flow assumptions.
- Run it in the target local and CI environments; verify that the assertion protects the same user outcome as before.
- Move related tests in small groups. Where practical, keep old and new coverage running until important scenarios are represented in the new suite.
- Update dependencies, scripts, runner configuration, browser provisioning, and CI jobs after the new runner works in the target environment.
- Remove Protractor and its dependencies once the replacement suite covers required scenarios and CI output has been reviewed.
The archived Protractor tutorial’s Selenium Server and WebDriver Manager instructions are historical context, not current installation guidance. Follow the selected runner’s current documentation for setup.
7. Can Protractor tests be converted automatically?
Do not plan on a safe one-click conversion of an arbitrary suite. Playwright’s migration page offers API mappings and a line-by-line example. Cypress’s August 2023 article described its migrator as an educational playground for converting pasted snippets and learning the corresponding Cypress APIs; it said at publication that the tool was not intended to transform whole folders or suites. Since that capability statement is dated, check the current tool before relying on it.
Even a snippet converter cannot infer whether a custom helper encodes important setup, whether a selector still points at the right control, or whether an assertion protects the original behavior. The effort depends on wrappers, page objects, browser-specific behavior, AngularJS locators versus modern Angular markup, non-Angular pages, test-data setup, and CI infrastructure. A small pilot reveals these differences before you commit to a full conversion.
8. Troubleshooting common migration failures
| Symptom | Likely cause | Fix |
|---|---|---|
| Actions run out of order or return before completion in Playwright | Missing await or leftover Control Flow assumptions. |
Make the test function asynchronous and await navigation, actions, and evaluations. Remove Control Flow dependencies. |
| Element lookup fails after conversion | Old locator syntax was copied without validating the selector, or the UI renders a different element. | Inspect the rendered page and choose a selector that identifies the intended control; prefer stable accessible names or test IDs where available. |
| Test times out waiting for Angular stability | A blanket waitForAngular call or Testability integration is waiting on work that does not settle or is unavailable on that page. |
Remove the general wait. Assert the specific UI condition; use Angular Testability only for a verified Angular 2+ edge case. |
| Cypress command times out | The queried state never appears, the selector is wrong, or the command timeout is too short for the actual operation. | Check the selector and application outcome first, then configure defaultCommandTimeout for a justified slower condition rather than adding arbitrary delays. |
| Tests pass locally but fail in CI | Different base URL, browser provisioning, environment variables, timing, test data, or parallel execution assumptions. | Compare local and CI configuration, make setup explicit, and reproduce with the same browser and environment settings. |
| Converted test passes but no longer protects the same behavior | The assertion checks an implementation detail or merely confirms the page loaded. | Restate the user outcome the old test intended to protect and assert that visible result. |
| Non-Angular page fails under Angular-specific waiting | The old suite assumed Angular everywhere. | Use the destination runner’s normal navigation and retry behavior for that page; scope any framework-specific synchronization narrowly. |
9. Performance, reliability, and maintenance
There is no benchmark in the sources that establishes one runner as faster for every Angular project. Measure the suite in your own CI environment. Track total runtime, parallel execution behavior, retry frequency, failure diagnosis time, and the time needed to maintain selectors and helpers. A faster suite that produces ambiguous failures may cost more engineering time than it saves.
For reliability, prefer waiting on a meaningful state over fixed delays, keep test data and environment setup explicit, and avoid assuming that Angular becoming “stable” is the same as the user-visible task being complete. Validate browser and CI behavior during the pilot. Keep failure artifacts and reporting that help the team diagnose regressions, according to the chosen runner’s supported setup.
10. FAQ
Does migrating tests require rewriting my Angular app?
Usually no. The primary work is in test code, runner configuration, dependencies, and CI. Application changes may help provide more stable selectors, but they are not a universal prerequisite.
Should I choose Cypress or Playwright?
Compare them against your browser coverage, team experience, CI workflow, existing helpers, and synchronization needs. Angular documents setup paths for both; neither is established as best for every project.
Can I keep some Protractor tests while migrating?
A staged migration can keep old and new coverage running in parallel where your project and CI make that practical. Retire the old runner after the required scenarios are represented and verified in the replacement suite.
Or skip the browser setup
If your immediate task is capturing a rendered page for a report or workflow, ScreenshotNeo is a website screenshot API and MCP server; it does not replace an E2E test runner or migrate test assertions. One GET request can return an image or PDF. See the ScreenshotNeo API documentation for configuration.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
timeout=90,
)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
const fs = await import('node:fs/promises');
await fs.writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));
- Cookie banners are accepted and removed before the shot; newsletter popups and chat widgets are removed too.
- Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed; response headers report the page verdict and billing status.
- An MCP server lets Claude, Cursor, and other MCP clients use screenshot tools.
- 1,000 screenshots per month are free with no card; paid plans start at $5 for 3,000 screenshots.
Sign up for ScreenshotNeo and get 1,000 free screenshots a month, with no card required.


