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.
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:
- Serenity/JS + Cucumber + Protractor project template on GitHub, and its Live Serenity BDD report
- Serenity/JS + Mocha + Protractor project template on GitHub, and its Live Serenity BDD report
- Serenity/JS + Jasmine + Protractor project template on GitHub, and its Live Serenity BDD report
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:
- Enhanced Reporting - You can use Serenity/JS as a drop-in replacement of any built-in Protractor framework to produce in-depth test execution reports and living documentation of your project.
- Screenplay Pattern APIs - To make your test code portable and reusable across projects and teams, Serenity/JS gives you an optional abstraction layer on top of native Protractor APIs.
- Integration Libraries - For test suites that follow the Screenplay Pattern, Serenity/JS also provides optional integration libraries to help you write API tests, manage local servers, perform assertions, and more!
Installing Serenity/JS
To add Serenity/JS to an existing Protractor project, install the following Serenity/JS modules from NPM:
- npm
- Yarn
npm install --save-dev @serenity-js/core @serenity-js/web @serenity-js/protractor @serenity-js/assertions @serenity-js/console-reporter @serenity-js/serenity-bdd
yarn add --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:
@serenity-js/core
@serenity-js/web
@serenity-js/protractor
@serenity-js/assertions
@serenity-js/console-reporter
@serenity-js/serenity-bdd
Configuring Serenity/JS and Protractor
To enable integration with Serenity/JS, configure Protractor as follows:
- Cucumber
- Mocha
- Jasmine
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,
}
};
exports.config = {
// Disable Selenium promise manager
SELENIUM_PROMISE_MANAGER: false,
framework: 'custom',
frameworkPath: require.resolve('@serenity-js/protractor/adapter'),
specs: [
'./spec/*.spec.ts', // If you're using TypeScript
// './spec/*.spec.js', // If you're using JavaScript
],
serenity: {
runner: 'mocha',
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'
} ],
]
},
mochaOpts: {
require: [
// Optional, if you're using TypeScript
'ts-node/register',
],
timeout: 10000,
// retries: 2 // auto-retry failed tests up to n times
},
};
exports.config = {
// Disable Selenium promise manager
SELENIUM_PROMISE_MANAGER: false,
framework: 'custom',
frameworkPath: require.resolve('@serenity-js/protractor/adapter'),
specs: [
'./spec/*.spec.ts', // If you're using TypeScript
// './spec/*.spec.js', // If you're using JavaScript
],
serenity: {
runner: 'jasmine',
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'
} ],
]
},
jasmineNodeOpts: {
requires: [
// Optional, if you're using TypeScript
'ts-node/register'
],
},
};
Learn more about:
- Serenity/JS Cucumber configuration options
and complete
protractor.conf.js
for Cucumber projects - Serenity/JS Jasmine configuration options
and complete
protractor.conf.js
for Jasmine projects - Serenity/JS Mocha configuration options
and complete
protractor.conf.js
for Mocha projects - Protractor configuration file
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:
- produce intermediate Serenity BDD
.json
reports, by registeringSerenityBDDReporter
as per the configuration instructions - invoke the Serenity BDD CLI when you want to produce the report, by calling
serenity-bdd run
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
{
"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:
- installation instructions in
@serenity-js/serenity-bdd
documentation, - configuration examples in
SerenityBDDReporter
API docs, - Serenity/JS examples on GitHub.
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:
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:
- Upgrade your Node.js to the latest Long-Term Support (LTS) version, such as v22.11.0
- Install Serenity/JS dependencies for Protractor, as well as Java Runtime Environment if you'd like to use Serenity BDD reports
- Migrate your existing Protractor tests to use Serenity/JS Screenplay Pattern web APIs, which you can do gradually while keeping the remaining scenarios working
- When all your test scenarios are using Serenity/JS Screenplay Pattern web APIs and there are no Protractor API calls left, introduce Playwright or WebdriverIO to ensure your migrated scenarios work with the new integration library
- Remove Protractor dependencies and enjoy your modernised test suite!
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:
Protractor | Serenity/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.all | PageElements.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()
.
Protractor | Serenity/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:
Protractor | Serenity/JS | Notes |
---|---|---|
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:
Protractor | Serenity/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
.
Protractor | Serenity/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
- Yarn
npm install @serenity-js/webdriverio --save-dev
yarn add @serenity-js/webdriverio --dev
Update the script
section in your package.json
to use wdio
instead of protractor
:
{
"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
- Yarn
npm uninstall protractor @serenity-js/protractor --save
yarn remove protractor @serenity-js/protractor
Learn more:
Next steps
Well done, your Protractor test suite is now integrated with Serenity/JS! 🎉🎉🎉
To take things further, check out:
- Web testing with Serenity/JS
- Serenity/JS examples on GitHub
- Serenity/JS Protractor project templates
- Serenity/JS API docs
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!
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 👇👇👇