Skip to main content

Extending Protractor with Serenity/JS

Serenity/JS offers excellent support for Protractor! Plus, it accommodates both classic Protractor tests and Serenity/JS Screenplay Pattern scenarios, allowing you to migrate to Screenplay gradually, and then simply switch to using Serenity/JS with a more modern web integration tool like Playwright or WebdriverIO.

Use Serenity/JS to migrate away from Protractor

As Protractor reaches its end of life, you can use Serenity/JS as an abstraction layer that allows you to gradually migrate your tests to follow the Screenplay Pattern, and then swap out Protractor for a more modern integration tool. All this while keeping your test suites working all the time!

Check out the Protractor migration guide below 👇👇👇

In this article, and in less than 5 minutes, you'll learn how to:

  • integrate Serenity/JS with your Protractor test suite,
  • enable Serenity BDD reports,
  • start using the Screenplay Pattern,
  • replace Protractor with Playwright or WebdriverIO while keeping your tests working!

Want to jump straight into the code? Check out:

About Serenity/JS

Serenity/JS is an open-source framework designed to make acceptance and regression testing of complex software systems faster, more collaborative, and easier to scale.

For Protractor test suites, Serenity/JS offers:

Serenity BDD Report Example

Installing Serenity/JS

To add Serenity/JS to an existing Protractor project, install the following Serenity/JS modules from NPM:

npm install --save-dev @serenity-js/core @serenity-js/web @serenity-js/protractor @serenity-js/assertions @serenity-js/console-reporter @serenity-js/serenity-bdd

Learn more about Serenity/JS modules:

Configuring Serenity/JS and Protractor

To enable integration with Serenity/JS, configure Protractor as follows:

protractor.conf.js
exports.config = {
// Disable Selenium promise manager
SELENIUM_PROMISE_MANAGER: false,

framework: 'custom',
frameworkPath: require.resolve('@serenity-js/protractor/adapter'),

specs: [ 'features/**/*.feature' ],

serenity: {
runner: 'cucumber',
crew: [
// Optional, print test execution results to standard output
'@serenity-js/console-reporter',

// Optional, produce Serenity BDD reports
// and living documentation (HTML)
'@serenity-js/serenity-bdd',
[ '@serenity-js/core:ArtifactArchiver', {
outputDirectory: 'target/site/serenity'
} ],

// Optional, automatically capture screenshots
// upon interaction failure
[ '@serenity-js/web:Photographer', {
strategy: 'TakePhotosOfFailures'
} ],
]
},

cucumberOpts: {
require: [
'features/step_definitions/**/*.steps.ts', // If you're using TypeScript
'features/support/*.ts',
// 'features/step_definitions/**/*.steps.js', // If you're using JavaScript
// 'features/support/*.js'
],
requireModule: [
// Optional, if you're using TypeScript
'ts-node/register'
],
tags: ['not @wip'],
strict: false,
}
};

Learn more about:

Producing Serenity BDD reports and living documentation

Serenity BDD reports and living documentation are generated by Serenity BDD CLI, a Java program provided by the @serenity-js/serenity-bdd module.

To produce Serenity BDD reports, your test suite must:

The pattern used by all the Serenity/JS Project Templates relies on using the following Node modules:

  • npm-failsafe to run the reporting process even if the test suite itself has failed (which is precisely when you need test reports the most...).
  • rimraf as a convenience method to remove any test reports left over from the previous run
package.json
{
"scripts": {
"clean": "rimraf target",
"test": "failsafe clean test:execute test:report",
"test:execute": "wdio wdio.conf.ts",
"test:report": "serenity-bdd run"
}
}

To learn more about the SerenityBDDReporter, please consult:

Using Serenity/JS Screenplay Pattern APIs

The Screenplay Pattern is an innovative, user-centred approach to writing high-quality automated acceptance tests. It steers you towards an effective use of layers of abstraction, helps your test scenarios capture the business vernacular of your domain, and encourages good testing and software engineering habits on your team.

By default, when you register @serenity-js/protractor as your Protractor framework, Serenity/JS configures a default cast of actors, where every actor can:

This should be enough to help you get started with introducing test scenarios that follow the Screenplay Pattern even to an existing test suite, for example:

specs/example.spec.ts
import 'jasmine'
import { actorCalled } from '@serenity-js/core'
import { Navigate, Page } from '@serenity-js/web'
import { Ensure, equals } from '@serenity-js/assertions'

describe('My awesome website', () => {
it('can have test scenarios that follow the Screenplay Pattern', async () => {
await actorCalled('Alice').attemptsTo(
Navigate.to(`https://www.protractortest.org/`),
Ensure.that(
Page.current().title(),
equals(`Protractor - end-to-end testing for AngularJS`)
),
)
})

it('can have non-Screenplay scenarios too', async () => {
await browser.get(`https://www.protractortest.org`)
await expect(browser.getTitle())
.toBe('Protractor - end-to-end testing for AngularJS')
})
})

To learn more about the Screenplay Pattern, check out:

Migrating from Protractor to Serenity/JS

Serenity/JS accommodates both classic Protractor tests and Serenity/JS Screenplay Pattern scenarios, allowing you to migrate to Screenplay gradually while keeping your existing test suites working and providing value to your organisation during the migration. With Serenity/JS, there's no need for a big-bang rewrite!

If you have an existing Protractor test suite and want to upgrade to a more modern web integration tool like Playwright or WebdriverIO, you'll need to:

Migrate or rewrite?

Migrating existing Protractor test scenarios to follow the Serenity/JS Screenplay Pattern can be a big undertaking, depending on the size, complexity, and stability of your current test suite. Not to mention that every team has a slightly different Protractor setup and uses the tool in different ways, which makes automating the migration process challenging.

When migrating to Serenity/JS, start with the easy scenarios so that you can focus the first steps of your migration on making sure your continuous integration and reporting infrastructure work reliably. This way you'll build a stable foundation upon which you can migrate the more complex tests.

Remember, if your current Protractor test suite is reasonably stable, still works and provides value, there's no need for a risky big bang rewrite as you can migrate it one scenario at a time while keeping the existing tests working. However, if your current Protractor test suite doesn't work and you'd rather delete it than migrate it, you might prefer to start from scratch and use Serenity/JS with Playwright or WebdriverIO straight away. The choice is yours and Serenity/JS will support you either way.

Locating elements

To identify web elements with Serenity/JS you use the Page Element Query Language.

In short:

  • PageElement represents a single HTML element,
  • PageElements represent a collection of HTML elements and lets you filter it based on your criteria.
  • By represents portable locators used to identify the elements,

For example, to identify an element <h1 id="title">My article</h1> in plain Protractor you'd say:

element(by.css('h1.title'))

Serenity/JS has a similar construct and allows you to specify a description to be used when reporting interactions with the element:

import { By, PageElement } from '@serenity-js/web'

PageElement.located(By.css('h1.title'))

However, Serenity/JS also allows you to specify a human-readable description to be used when reporting interactions with the element:

import { By, PageElement } from '@serenity-js/web'

PageElement.located(By.css('h1.title'))
.describedAs('article title')

The below table shows how you can translate common Protractor element finder expressions to Serenity/JS page element expressions:

ProtractorSerenity/JS
element(by.css('.selector'))PageElement.located(By.css('...'))
element(by.id('...'))PageElement.located(By.id('...'))
element(by.xpath('...'))PageElement.located(By.xpath('...'))
element(by.model('...'))PageElement.located(By.css('[ng-model="..."]'))
element(by.repeater('...'))PageElement.located(By.css('[ng-repeat="..."]'))
element(by.cssContainingText('.selector', 'text'))PageElement.located(By.cssContainingText('.selector', 'text'))
or for greater flexibility:
PageElements.located(By.css('.selector')).where(Text, includes('text')).first()
element(by.buttonText('Submit'))PageElements.located(By.css('button')).where(Text, equals('Submit')).first()
element.allPageElements.located(By.css('...'))

Note how with Serenity/JS you can easily express complex queries using intuitive syntax. For example, you can locate the last <button /> which text meets some expectation:

import { By, PageElements, Text } from '@serenity-js/web'
import { equals } from '@serenity-js/assertions'

PageElements.located(By.css('button'))
.where(Text, equals('Submit'))
.last()

You can create aliases for expressions you use frequently:

import { By, PageElements, Text } from '@serenity-js/web'
import { equals } from '@serenity-js/assertions'

const approveButton = () =>
PageElements.located(By.css('button'))
.where(Text, equals('Approve'))
.first()

And you can also compose page elements, making them reusable:

import { actorCalled, Expectation } from '@serenity-js/core'
import { equals } from '@serenity-js/assertions'
import { By, Click, PageElement, PageElements } from '@serenity-js/web'

const clientNameField = () =>
PageElement.located(By.css('[data-test-id="client-name"]'))
.describedAs('client name field')

const invoiceRecordForClientWhereName = (expectation: Expectation<string>) =>
PageElements.located(By.css('li.invoice'))
.where(Text.of(clientNameField()), expectation)
.first()

await actorCalled('Alice').attemptsTo(
Click.on(
approveButton().of(invoiceRecordForClientWhereName(equals('Acme')))
)
)

Learn more:

Retrieving information

To retrieve information about the current web page, use Page.current().

ProtractorSerenity/JS
browser.getCurrentUrl()Page.current().url().href
browser.getTitle()Page.current().title()

For example, to navigate to a page and then assert on its title and URL you can say:

import { actorCalled } from '@serenity-js/core'
import { Ensure, endsWith, equals } from '@serenity-js/assertions'
import { Navigate, Page } from '@serenity-js/web'

await actorCalled('Amanda').attemptsTo(
Navigate.to('https://serenity-js.org'),
Ensure.that(
Page.current().title(),
endsWith('Serenity/JS')
),
Ensure.that(
Page.current().url().href,
equals('https://serenity-js.org')
),
)

To retrieve information about a page element, use Serenity/JS web questions:

ProtractorSerenity/JSNotes
element(by.id('...')).getAttribute('class')CssClasses.of(pageElement)Serenity/JS returns an array of CSS classes to make assertions easier, instead of a single string like plain Protractor
element(by.id('...')).getAttribute('value')Value.of(pageElement)Serenity/JS has a dedicated question for retrieving the value attribute of <input /> elements
element(by.id('...')).getText()Text.of(pageElement) and Text.ofAll(pageElements)Serenity/JS makes it easier to retrieve text content of all elements in a collection

For example, to navigate to a page and assert on the CSS classes of an element, you could say:

import { actorCalled } from '@serenity-js/core'
import { By, CssClasses, Navigate, PageElement } from '@serenity-js/web'
import { Ensure, include } from '@serenity-js/assertions'

const startAutomatingButton = () =>
PageElement.located(By.cssContainingText('.button', 'Start automating'))
.describedAs('"Start automating" button')

await actorCalled('Amanda').attemptsTo(
Navigate.to('https://serenity-js.org'),
Ensure.that(
CssClasses.of(startAutomatingButton()),
include('button--primary')
),
)

Performing interactions

To interact with a web page or a page element, instruct an actor to perform a desired interaction. Note that Serenity/JS also consistently models all assertion, synchronisation logging, debugging, and control flow statements as interactions too.

For example, to navigate to a page, click on a button and perform an assertion, you could say:

import { actorCalled } from '@serenity-js/core'
import { By, Click, Navigate, Page, PageElement } from '@serenity-js/web'
import { Ensure, equals } from '@serenity-js/assertions'

const startAutomatingButton = () =>
PageElement.located(By.cssContainingText('.button', 'Start automating'))
.describedAs('"Start automating" button')

await actorCalled('Amanda').attemptsTo(
Navigate.to('https://serenity-js.org'),
Click.on(startAutomatingButton()),
Ensure.that(
Page.current().url().path,
equals('/handbook/web-testing/your-first-web-scenario/')
)
)

Note that one of Serenity/JS super-powers is the ability to compose interactions into tasks, like startTutorial() below, which makes your code easy to share and reuse:

import { actorCalled, Task } from '@serenity-js/core'
import { Click, Navigate, Page } from '@serenity-js/web'
import { Ensure, equals } from '@serenity-js/assertions'

// ...

const startTutorial = () =>
Task.where(`#actor starts the Serenity/JS tutorial`,
Click.on(startAutomatingButton()),
Ensure.that(
Page.current().url().path,
equals('/handbook/web-testing/your-first-web-scenario/')
)
)

await actorCalled('Amanda').attemptsTo(
Navigate.to('https://serenity-js.org'),
startTutorial(),
)

Below table shows how you can replace Protractor-specific API calls with Serenity/JS web interactions:

ProtractorSerenity/JS
element(locator).click()Click.on(pageElement)
element(locator).clear()Clear.theValueOf(pageElement)
element(locator).sendKeys(text)Enter.theValue(text).into(pageElement)

Waiting and synchronisation

To express a synchronisation statement, use Wait.until or Ensure.eventually.

ProtractorSerenity/JS
element(locator).isPresent()Wait.until(pageElement, isPresent())
element(locator).isEnabled()Wait.until(pageElement, isEnabled())
element(locator).isSelected()Wait.until(pageElement, isSelected())
element(locator).isDisplayed()Wait.until(pageElement, isVisible())

Replacing Protractor with WebdriverIO

In many ways, WebdriverIO is very similar to Protractor:

  • it integrates with the same test runners, such as Cucumber, Mocha, and Jasmine
  • it works well with Serenity/JS
  • it works with local browsers and remote Selenium Grids
  • it supports multi-tab and multi-window test scenarios
  • it supports mobile testing
  • its configuration file follows a similar structure

Once you've migrated your test scenarios to follow the Screenplay Pattern and there are no Protractor API calls left in your code, follow WebdriverIO Protractor migration guide to:

Next, install Serenity/JS WebdriverIO module:

npm install @serenity-js/webdriverio --save-dev

Update the script section in your package.json to use wdio instead of protractor:

package.json
{
"scripts": {
"clean": "rimraf target",
"test": "failsafe clean test:execute test:report",
- "test:execute": "protractor ./protractor.conf.js",
+ "test:execute": "wdio ./wdio.conf.ts",
"test:report": "serenity-bdd run"
}
}

Once your test suite works, remove Protractor and its related Serenity/JS module:

npm uninstall protractor @serenity-js/protractor --save

Learn more:

Next steps

Well done, your Protractor test suite is now integrated with Serenity/JS! 🎉🎉🎉

To take things further, check out:

Remember, new features, tutorials, and demos are coming soon! Follow Serenity/JS on LinkedIn, subscribe to Serenity/JS channel on YouTube and join the Serenity/JS Community Chat to stay up to date! Please also make sure to star ⭐️ Serenity/JS on GitHub to help others discover the framework!

Follow Serenity/JS on LinkedIn Watch Serenity/JS on YouTube Join Serenity/JS Community Chat GitHub stars

If you have found any errors in Serenity/JS documentation, feel free to submit a fix using the Edit this page button below, and if you'd like to see more examples of using Serenity/JS with Protractor or have questions about the migration - let us know in the comments 👇👇👇