Migrating from Protractor to Cypress

Nx helps configure your e2e tests for you. When running the Nx generator to create a new app, you can choose Protractor as an option, but the default is Cypress. If you have an existing set of e2e tests using Protractor and would like to switch to using Cypress, you can follow these steps.

Let's say your existing app is named my-awesome-app and the e2e Protractor tests are located in my-awesome-app-e2e.

  1. Before you start, make sure you have a clean git working tree (by committing or stashing any in progress changes)

  2. Create a throw away app named delete-this-app using Cypress for the e2e setting.

nx g @nrwl/angular:application --name=delete-this-app --e2eTestRunner=cypress
  1. Rename apps/my-awesome-app-e2e/src to apps/my-awesome-app-e2e/src-protractor.
mv apps/my-awesome-app-e2e/src apps/my-awesome-app-e2e/src-protractor
  1. Move the contents of apps/delete-this-app-e2e to apps/my-awesome-app-e2e.
mv apps/delete-this-app-e2e/* apps/my-awesome-app-e2e
  1. In the angular.json (or workspace.json) file copy the e2e target configuration for delete-this-app-e2e and use that to replace the e2e target configuration for my-awesome-app-e2e. In the new configuration section, replace any instance of delete-this-app with my-awesome-app.

  2. Delete delete-this-app and delete-this-app-e2e.

nx g rm delete-this-app-e2e
nx g rm delete-this-app
  1. Edit apps/my-awesome-app-e2e/cypress.json and replace any instance of delete-this-app with my-awesome-app.

  2. Delete apps/my-awesome-app-e2e/protractor.conf.js

rm apps/my-awesome-app-e2e/protractor.conf.js
  1. The most arduous part of this migration is replacing all Protractor API instances with the Cypress API. For more specifics, please refer to the before and after examples below.

  2. Run your Cypress tests with the same command that launched your Protractor tests.

nx e2e my-awesome-app-e2e

How to Get DOM Elements

Getting a single element on the page

When it comes to e2e tests, one of the most common things you'll need to do is get one or more HTML elements on a page. Rather than split element fetching into multiple methods that you need to memorize, everything in Cypress can be accomplished with cy.get while using CSS selectors or the preferred data attribute.

Before: Protractor

1// Get an element
2element(by.tagName('h1'))
3
4// Get an element using a CSS selector.
5element(by.css('.my-class'))
6
7// Get an element with the given id.
8element(by.id('my-id'))
9
10// Get an element using an input name selector.
11element(by.name('field-name'))
12
13// Get an element by the text it contains within a certain CSS selector
14element(by.cssContainingText('.my-class', 'text'))
15
16// Get the first element containing a specific text (only for link elements)
17element(by.linkText('text')

After: Cypress

1// Get an element
2cy.get('h1');
3
4// Get an element using a CSS selector.
5cy.get('.my-class');
6
7// Get an element with the given id.
8cy.get('#my-id');
9
10// Get an element using an input name selector.
11cy.get('input[name="field-name"]');
12
13// Get an element by the text it contains within a certain CSS selector
14cy.get('.my-class').contains('text');
15
16// Get the first element containing a specific text (available for any element)
17cy.contains('text');
18
19// Get an element by the preferred [data-cy] attribute
20cy.get('[data-cy]="my-id"]');

You can learn more about best practices for selecting DOM elements in the Cypress official documentation.

Getting multiple elements on a page

With Protractor when you want to get access to more than one element on the page, you would need to chain the .all() method. However, in Cypress, no syntax change is necessary!

Before: Protractor

1// Get all list-item elements on the page
2element.all(by.tagName('li'));
3
4// Get all elements by using a CSS selector.
5element.all(by.css('.list-item'));
6
7// Find an element using an input name selector.
8element.all(by.name('field-name'));

After: Cypress

1// Get all list-item elements on the page
2cy.get('li');
3
4// Get all elements by using a CSS selector.
5cy.get('.list-item');
6
7// Find an element using an input name selector.
8cy.get('input[name="field-name"]');

You can learn more about how to get DOM elements in the Cypress official documentation.

Assertions

Similar to Protractor, Cypress enables use of human readable assertions. Here are some common DOM element assertions with Cypress and equivalent assertions with Protractor.

Length

Before: Protractor

1const list = element.all(by.css('li.selected'));
2
3expect(list.count()).toBe(3);

After: Cypress

1// retry until we find 3 matching <li.selected>
2cy.get('li.selected').should('have.length', 3);

Class

Before: Protractor

1expect(
2  element(by.tagName('form')).element(by.tagName('input')).getAttribute('class')
3).not.toContain('disabled');

After: Cypress

1// retry until this input does not have class disabled
2cy.get('form').find('input').should('not.have.class', 'disabled');

Value

Before: Protractor

1expect(element(by.tagName('textarea'))).getAttribute('value')).toBe('foo bar baz')

After: Cypress

1// retry until this textarea has the correct value
2cy.get('textarea').should('have.value', 'foo bar baz');

Text Content

Before: Protractor

1// assert the element's text content is exactly the given text
2expect(element(by.id('user-name')).getText()).toBe('Joe Smith');
3
4// assert the element's text includes the given substring
5expect(element(by.id('address')).getText()).toContain('Atlanta');
6
7// assert the span does not contain 'click me'
8const child = element(by.tagName('a')).getWebElement();
9const parent = child.getDriver().findElement(by.css('span.help'));
10expect(parent.getText()).not.toContain('click me');
11
12// assert that the greeting starts with "Hello"
13element(by.id('greeting').getText()).toMatch(/^Hello/);

After: Cypress

1// assert the element's text content is exactly the given text
2cy.get('#user-name').should('have.text', 'Joe Smith');
3
4// assert the element's text includes the given substring
5cy.get('#address').should('include.text', 'Atlanta');
6
7// retry until this span does not contain 'click me'
8cy.get('a').parent('span.help').should('not.contain', 'click me');
9
10// the element's text should start with "Hello"
11cy.get('#greeting')
12  .invoke('text')
13  .should('match', /^Hello/);
14
15// tip: use cy.contains to find element with its text
16// matching the given regular expression
17cy.contains('#a-greeting', /^Hello/);

Visibility

Before: Protractor

1// assert button is visible
2expect(element(by.tagName('button')).isDisplayed()).toBe(true);

After: Cypress

1// retry until this button is visible
2cy.get('button').should('be.visible');

Existence

Before: Protractor

1// assert the spinner no longer exists
2expect(element(by.id('loading')).isPresent()).toBe(false);

After: Cypress

1// retry until loading spinner no longer exists
2cy.get('#loading').should('not.exist');

State

Before: Protractor

1expect(element('input[type="radio"]').isSelected()).toBeTruthy();

After: Cypress

1// retry until our radio is checked
2cy.get(':radio').should('be.checked');

CSS

Before: Protractor

1// assert .completed has css style "line-through" for "text-decoration" property
2expect(element(by.css('.completed')).getCssValue('text-decoration')).toBe(
3  'line-through'
4);
5
6// assert the accordion does not have a "display: none"
7expect(element(by.id('accordion')).getCssValue('display')).not.toBe('none');

After: Cypress

1// retry until .completed has matching css
2cy.get('.completed').should('have.css', 'text-decoration', 'line-through');
3
4// retry while .accordion css has the "display: none" property
5cy.get('#accordion').should('not.have.css', 'display', 'none');

Disabled property

1<input type="text" id="example-input" disabled />

Before: Protractor

1// assert the input is disabled
2expect(element(by.id('example-input')).isEnabled()).toBe(false);

After: Cypress

1cy.get('#example-input')
2  .should('be.disabled')
3  // let's enable this element from the test
4  .invoke('prop', 'disabled', false);
5
6cy.get('#example-input')
7  // we can use "enabled" assertion
8  .should('be.enabled')
9  // or negate the "disabled" assertion
10  .and('not.be.disabled');

Cypress has one additional feature that can make a critical difference in the reliability of your tests' assertions: retry-ability. When your test fails an assertion or command, Cypress will mimic a real user with build-in wait times and multiple attempts at asserting your tests in order to minimize the amount of false negatives / positives.

Before: Protractor

1describe('verify elements on a page', () => {
2  it('verifies that a link is visible', () => {
3    expect($('a.submit-link').isDisplayed()).toBe(true);
4  });
5});

After: Cypress

1describe('verify elements on a page', () => {
2  it('verifies that a link is visible', () => {
3    cy.get('a.submit-link').should('be.visible');
4  });
5});

In the example above, if the submit link does not appear on the page at the exact moment when Protractor runs the test (which can be due to any number of factors including API calls, slow browser rendering, etc.), your test will fail. However, Cypress factors these conditions into its assertions and will only fail if the time goes beyond a reasonable amount.

Negative assertions

There are positive and negative assertions. Negative assertions have the "not" chainer prefixed to the assertion. Examples of negative assertions in both Protractor and Cypress:

Before: Protractor

1expect(
2  element(by.css('.todo'))
3    .getAttribute('class')
4    .then((classes) => {
5      return classes.split(' ').indexOf('completed') !== -1;
6    })
7).not.toBe(true);
8expect(element(by.id('loading')).isDisplayed()).not.toBe(true);

After: Cypress

1cy.get('.todo').should('not.have.class', 'completed');
2cy.get('#loading').should('not.be.visible');

Learn more about how Cypress handles assertions.

Network Handling

Network Spying

Protractor doesn't offer a built-in solution for network spying. With Cypress, you can leverage the intercept API to spy on and manage the behavior of any network request. Cypress will automatically wait for any request to complete before continuing your test.

Network Stubbing

Cypress's intercept API also allows you to stub any network request for your app under test. You can use the intercept API to make assertions based on different simulated responses for your network requests. You can also use the intercept API to stub a custom response for specific network requests.

For more information, check out the intercept API documentation.

When you want to visit a page, you can do so with the following code:

Before: Protractor

1it('visits a page', () => {
2  browser.get('/about');
3  browser.navigate().forward();
4  browser.navigate().back();
5});

After: Cypress

1it('visits a page', () => {
2  cy.visit('/about');
3  cy.go('forward');
4  cy.go('back');
5});

For more information, check out the Cypress official documentation on navigation!

Automatic Retrying and Waiting

Web applications are almost never synchronous. With Protractor, you may be accustomed to adding arbitrary timeouts or using the waitForAngular API to wait for Angular to finish rendering before attempting to interact with an element. With Cypress, commands that query the DOM are automatically retried. Cypress will automatically wait and retry most commands until an element appears in the DOM. If an element is not actionable within the defaultCommandTimeout setting, the command will fail. This enables you to write tests without the need for arbitrary timeouts, enabling you to write more predictable tests.

For more information, check out the Cypress official documentation on timeouts!

Cypress vs WebDriver Control Flow

Cypress commands are similar to Protractor code at first glance. Cypress commands are not invoked immediately and are enqueued to run serially at a later time. Cypress commands might look like promises, but the Cypress API is not an exact implementation of Promises. The modern web is asychronous, therefore you need to interact with modern web apps in an asynchronous fashion. This is why the Cypress API is asynchronous. This allows you to write deterministic tests since all of your commands are executed serially, enabling your tests to run predictably each time. In comparison, Protractor's WebDriverJS API is based on promises, which is managed by a control flow. This Control Flow enables you to write asynchronous Protractor tests in a synchronous style.

For more information, check out the Cypress official documentation on asyncronous!

Using Page Objects

A common pattern when writing end-to-end tests, especially with Protractor, is Page Objects. Page Objects can simplify your test code by creating reusable methods if you find yourself writing the same test code across multiple test cases. You can use the same Page Object pattern within your Cypress tests: Cypress also provides a Custom Command API to enable you to add methods to the cy global directly:

For more information, check out the Cypress official documentation on custom commands!

Continuous Integration

Cypress makes it easy to run your tests in all Continuous Integration environments. Check out the Cypress in-depth guides to run your Cypress tests in GitHub Actions, CircleCI, GitLab CI, Bitbucket Pipeline, or AWS CodeBuild.

Cypress also has code samples to get Cypress up and running in many of the other popular CI environments. Even if your CI provider isn't listed, you can still run Cypress in your CI environment.

Other Resources