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
.
-
Before you start, make sure you have a clean git working tree (by committing or stashing any in progress changes)
-
Create a throw away app named
delete-this-app
usingCypress
for the e2e setting.
nx g @nrwl/angular:application --name=delete-this-app --e2eTestRunner=cypress
- Rename
apps/my-awesome-app-e2e/src
toapps/my-awesome-app-e2e/src-protractor
.
mv apps/my-awesome-app-e2e/src apps/my-awesome-app-e2e/src-protractor
- Move the contents of
apps/delete-this-app-e2e
toapps/my-awesome-app-e2e
.
mv apps/delete-this-app-e2e/* apps/my-awesome-app-e2e
-
In the
angular.json
(orworkspace.json
) file copy thee2e
target configuration fordelete-this-app-e2e
and use that to replace thee2e
target configuration formy-awesome-app-e2e
. In the new configuration section, replace any instance ofdelete-this-app
withmy-awesome-app
. -
Delete
delete-this-app
anddelete-this-app-e2e
.
nx g rm delete-this-app-e2e
nx g rm delete-this-app
-
Edit
apps/my-awesome-app-e2e/cypress.json
and replace any instance ofdelete-this-app
withmy-awesome-app
. -
Delete
apps/my-awesome-app-e2e/protractor.conf.js
rm apps/my-awesome-app-e2e/protractor.conf.js
-
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.
-
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.
Navigating Websites
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.