Cucumber in the Typescript
Welcome to my course of Cucumber framework for beginners. In this post we will go through setting up the project in Typescript. The project will be based on Selenium and the results will be reported by Allure reporting framework.
If you need some more general information about Cucumber and Gherkin please read the introduction.
In my series, I present how to efficiently organize code in various languages. When working with BDD, it is important to remember that coding is the final step. One should never start with coding. BDD begins with a discussion about functionalities, use cases, and examples. After this discussion, a feature description in terms of Given-When-Then is created. Such a description should not focus on webpages. Please see my another post on this topic.
LANGUAGE AND IDE
JavaScript is a popular programming language used to create dynamic web pages and interactive web applications. It is a scripting language that supports object-oriented, imperative, and functional programming styles. In JavaScript, variables are declared using the var
, let
, or const
keyword. Functions can be defined using the function
keyword.
Typescript is a superset of JavaScript that adds static typing and other features to the language. It is a strongly typed language that provides compile-time type checking, making it easier to detect errors and bugs during development. Typescript supports classes, interfaces, and modules, making it easier to write scalable and maintainable code.
To edit Typescript or JavaScript code, I recommend Visual Studio Code.
WHAT DO WE WANT TO TEST?
Our test page is pretty simple (the same for the whole course). It contains several typical elements of UI forms:
- text field (input),
- dropdown,
- checkbox,
- table with dynamic content,
- text element with dynamic content.
The URL address of this page is: https://danieldelimata.github.io/sample-page/
.
BEFORE WE START
Before doing anything we have to install Node.js and NPM. Node.js is a JavaScript runtime that allows you to run JavaScript on the server-side. NPM (Node Package Manager) is a package manager for Node.js. You can download and install Node.js from the official website, and NPM will be installed automatically.
SETTING UP THE SKELETON OF THE PROJECT
In JavaScript projects do not have so strictly defined structure as Java or C#. You can create a new project using the following commands:
Selenium:
mkdir typescript-test-project
cd typescript-test-project
npm init -y
npm install --save-dev typescript
tsc --init
npm install --save-dev @cucumber/cucumber
npm install --save-dev allure-cucumberjs
npm install --save-dev @types/node
npm install --save-dev selenium-webdriver
npm install --save-dev @types/selenium-webdriver
touch .dummy.txt
"module.exports = { default: '--publish-quiet' }" >> cucumber.js
Playwright:
mkdir typescript-test-project
cd typescript-test-project
npm init -y
npm install --save-dev typescript
tsc --init
npm install --save-dev @cucumber/cucumber
npm install --save-dev allure-cucumberjs
npm install --save-dev @types/node
npm install --save-dev playwright
touch .dummy.txt
"module.exports = { default: '--publish-quiet' }" >> cucumber.js
The file cucumber.js
in the project root directory is needed to disable the message about possibility of publishing results. The empty file .dummy.txt
is needed for Allure reporter.
Open tsconfig.json
in your editor. We can edit this file and set-up options according to our needs.
For our example let us finish with the following tsconfig.json
file:
{
"compilerOptions": {
"module": "commonjs",
"outDir": "./dist",
"target": "ES2022",
"lib": ["ES2022"],
"esModuleInterop": true,
"skipLibCheck": true
},
"include": [
"./features/step_definitions/**/*",
"./features/support/**/*"
]
}
In above code we use currently the newest version of ECMA Script.
Typescript will create JavaScript files in dist
directory. In features
we will place our Gherkin files.
The package.json
file is the main configuration file for most JavaScript projects and its primary purpose is to manage project dependencies and configure external tools.
The package.json
file contains a list of the project dependencies along with their versions. This allows for easy installation of all required packages and tools with a single command npm install
when cloning the project or installing dependencies.
This file also serves to store other project settings and metadata such as project name, author, version, description, scripts for running, testing, and building the project, as well as various configurations for external tools such as ESLint, Prettier, Babel, and others.
The scripts
section in the package.json
file is a powerful feature that allows developers to define custom scripts for running, testing, and building the project. This section contains a list of key-value pairs where the key is the name of the script, and the value is the command to execute.
For example, you can define a script named start
that runs your project, or a script named test
that runs your tests using a testing framework. The scripts
section also allows for the creation of custom scripts that can execute multiple commands. This can be useful for running complex build processes or deploying your project to production. From our point of view the most important is test
script.
Just after initialization of our project we see in package.json
:
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
Let us replace it with the following code:
"scripts": {
"test": "tsc && cucumber-js --require ./dist/step_definitions/ --format ./dist/support/reporter.js:.dummy.txt"
},
It allows to run our tests by executing simple command npm test
.
The tsc
command is a TypeScript compiler command that compiles TypeScript code into JavaScript code. When you run the tsc
command, it reads your TypeScript files and produces equivalent JavaScript files. In our case they will be placed in the dist
directory.
This process involves checking the syntax and types of your TypeScript code and generating JavaScript code that is compatible with a specified ECMAScript target version.
The cucumber-js
command is our runner. It executes tests The --require option
in cucumber-js
allows developers to specify one or more files or directories that contain the step definitions for the tests. This option is used to load the step definitions and make them available to the cucumber-js
runtime. The --format
option in cucumber-js
allows developers to specify the format of the output for the test results. There are several built-in formats available in cucumber-js
, including pretty
, progress
, summary
, and json
.
In our case, we will use Allure reporting framework, let us create the filefeatures/support/reporter.ts
with the following contents.
import { AllureRuntime, CucumberJSAllureFormatter } from "allure-cucumberjs";
export default class Reporter extends CucumberJSAllureFormatter {
constructor(options: any) {
super(options, new AllureRuntime({ resultsDir: "./allure-results" }), {});
}
}
Now, we have to place files into appropriate directories. First, of course, we will place feature
files.
features/f01.feature
:
Feature: F01 Searching - Clearing of searching criteria
User story:
* As a user of Customers page
* I want to be able to clear searching criteria
* in order to quickly type new criteria
Acceptance criteria:
* After clearing, search criteria and summary should be as in the very beginning.
Scenario Outline: Clearing of searching criteria
Given the user is on the page
When the user enters the value "<search>" in the text-input
And the user selects value "<column>" in the drop-down
And the user sets case sensitivity switch to "<case>"
And the user clears filters
Then the user should see that search criteria are cleared
And the user should see that the search result summary is as in the very beginning
Examples:
| Search | Column | Case |
| Alice | Name | True |
| alice | Name | False |
At this moment we do not have the glue code yet. We can however start execution just to get some snippets for it. They are produced in logs.
Again we execute:
npm test
Running Cucumber-js will create snippets for steps. They should be placed in steps
directory in a file e.g. stepDefinitions.ts
.
Such snippets of methods may look as follows.
Then('the user should see that search criteria are cleared', function () {
// Write code here that turns the phrase above into concrete actions
return 'pending';
});
Please note that these snippets are in JavaScript, so they do not include types. We have to provide the typescript code by our own.
Our automation needs running of Selenium or Playwright. Let us prepare the file hook.ts
in the features/support
directory and place there the code responsible for Selenium WebDriver. In the same file we place the code responsible for making screenshots when failure occurs.
features/support/hook.ts
:
import { After, AfterAll, BeforeAll, Status } from '@cucumber/cucumber';
import { Builder, WebDriver } from 'selenium-webdriver';
import { Options } from 'selenium-webdriver/chrome';
export let driver: WebDriver;
BeforeAll(async () => {
driver = await new Builder()
.forBrowser('chrome')
.setChromeOptions(
new Options()
.headless()
.windowSize({ width: 640, height: 480 })
)
.build();
return driver.manage().window().maximize();
});
AfterAll(async () => driver.quit());
After(async function (scenario) {
if (scenario.result?.status === Status.FAILED) {
const attach = this.attach;
const png = await driver.takeScreenshot();
const decodedImage = Buffer.from(png, "base64");
return attach(decodedImage, "image/png");
}
});t
or analogously for Playwright
import { After, AfterAll, BeforeAll, Status, World } from '@cucumber/cucumber';
import { chromium, Browser, Page } from 'playwright';
export let browser: Browser;
export let page: Page;
BeforeAll({ timeout: 5 * 1000 }, async function (this: World) {
browser = await chromium.launch({
headless: true,
});
page = await browser.newPage();
return page;
});
AfterAll(async function (this: World) {
return browser.close();
});
After(async function (scenario) {
if (scenario.result?.status === Status.FAILED) {
const attach = this.attach;
const screenshot = await page.screenshot();
return attach(screenshot, "image/png");
}
});
To complete our setup we will need one more thing. We want to implement Page Object Pattern. To do this we create two more directories and files AbstractPageObject.ts
and customersPage.ts
. Our directory structure will look as follows.
Project Folder
├── features
│ ├── page_objects
│ │ ├── AbstractPageObject.ts
│ │ ├── customersPage.ts
│ ├── step_definitions
│ │ ├── stepDefinitions.ts
│ ├── support
│ │ ├── hook.ts
│ │ ├── reporter.ts
│ ├── f01.feature
├── .dummy.txt
├── .gitignore
├── cucumber.js
├── package.json
├── tsconfig.json
features/page_objects/AbstractPageObject.ts
:
import { WebDriver } from 'selenium-webdriver';
export abstract class AbstractPageObject {
protected readonly driver: WebDriver;
constructor(driver: WebDriver) {
this.driver = driver;
}
}
or analogously for Playwright
import { Page } from "playwright";
export abstract class AbstractPageObject {
protected readonly page: Page;
constructor(page: Page) {
this.page = page;
}
}
features/page_objects/customersPage.ts
:
import { WebDriver, By } from 'selenium-webdriver';
import { AbstractPageObject } from './AbstractPageObject';
import { driver } from '../support/hook';
export class CustomersPage extends AbstractPageObject {
private clickToClearFiltersButton = By.id('clear-button');
private searchInput = By.id('search-input');
private searchColumn = By.id('search-column');
private matchCase = By.id('match-case');
private summary = By.id('table-resume');
private searchTerm = By.id('search-slogan');
private searchResultsTable = By.xpath("//table");
constructor() {
super(driver);
}
/**
* Click on Clear Filters Button.
*
* @return the CustomersPage class instance.
*/
clickClearFiltersButton = async () => {
await this.driver.findElement(this.clickToClearFiltersButton).click();
return this;
};
/**
* Set value to searchInput field.
*
* @param input String which should be typed into the field
* @return the CustomersPage class instance.
*/
setSearchInput = async (input: string) => {
await this.driver.findElement(this.searchInput).sendKeys(input);
return this;
};
/**
* Set value to Search Column Drop Down List field.
*
* @param value String which should match with one of values visible on the
* dropdown
* @return the CustomersPage class instance.
*/
selectSearchColumnByText = async (value: string) => {
await this.driver.findElement(
By.xpath(`//select[@id='search-column']/option[text()='${value}']`))
.click();
return this;
}
/**
* Set Match Case Checkbox field to required value.
*
* @param value boolean value of the checkbox status true - checked,
* false - unchecked
* @return the CustomersPage class instance.
*/
setMatchCaseCheckboxField = async (value: boolean) => {
const checkboxState = await this.driver.findElement(this.matchCase).isSelected();
if (checkboxState !== value) {
await this.driver.findElement(this.matchCase).click();
}
return this;
};
getSummaryText = async () => {
return await this.driver.findElement(this.summary)
.getText();
};
getSearchTermText = async () => {
return await this.driver.findElement(this.searchTerm)
.getText();
};
getSearchInputText = async () => {
return await this.driver.findElement(this.searchInput)
.getText();
};
getSearchResultsTableText = async () => {
return await this.driver.findElement(this.searchResultsTable)
.getText();
};
open = async () => {
const pageUrl: string = "https://danieldelimata.github.io/sample-page/";
await this.driver.get(pageUrl);
};
}
or analogously for Playwright
import { AbstractPageObject } from "./AbstractPageObject";
import { Page } from "playwright";
import { page } from '../support/hook';
export class CustomersPage extends AbstractPageObject {
protected clickToClearFiltersButton
= "#clear-button";
protected searchInput
= "#search-input";
protected searchColumn
= "#search-column";
protected matchCase
= "#match-case";
protected summary
= "#table-resume";
protected searchTerm
= "#search-slogan";
protected searchResultsTable
= "//table";
constructor() {
super(page);
}
/**
* Click on Clear Filters Button.
*
* @return the CustomersPage class instance.
*/
clickClearFiltersButton = async () => {
await this.page.click(this.clickToClearFiltersButton);
return this;
};
/**
* Set value to searchInput field.
*
* @param input String which should be typed into the field
* @return the CustomersPage class instance.
*/
setSearchInput = async (input: string) => {
await this.page.fill(this.searchInput, input);
return this;
};
/**
* Set value to Search Column Drop Down List field.
*
* @param value String which should match with one of values visible on the
* dropdown
* @return the CustomersPage class instance.
*/
selectSearchColumnByText = async (value: string) => {
await this.page.selectOption(this.searchColumn, value)
return this;
}
/**
* Set Match Case Checkbox field to required value.
*
* @param value boolean value of the checkbox status true - checked,
* false - unchecked
* @return the CustomersPage class instance.
*/
setMatchCaseCheckboxField = async (value: boolean) => {
await this.page.waitForSelector(this.matchCase)
let element = await this.page.$(this.matchCase)
var checkboxState = await element?.isChecked();
if (checkboxState != value) {
(await this.page.click(this.matchCase));
}
return this;
};
getSummaryText = async () => {
await this.page.waitForSelector(this.summary)
let element = await this.page.$(this.summary)
return await this.page.evaluate(el => el.textContent, element)
};
getSearchTermText = async () => {
await this.page.waitForSelector(this.searchTerm)
let element = await this.page.$(this.searchTerm)
return await this.page.evaluate(el => el.textContent, element)
};
getSearchInputText = async () => {
await this.page.waitForSelector(this.searchInput)
let element = await this.page.$(this.searchInput)
return await this.page.evaluate(el => el.textContent, element)
};
getSearchResultsTableText = async () => {
await this.page.waitForSelector(this.searchResultsTable)
let [element] = await this.page.$$(this.searchResultsTable)
return await this.page.evaluate(el => el.textContent, element)
};
open = async () => {
const pageUrl: string = "https://danieldelimata.github.io/sample-page/";
return await this.page.goto(pageUrl, { waitUntil: 'domcontentloaded' });
};
}
The above code is related to UI only. It should not contain methods related to tests. These should be placed in glue-code in step_definitions.ts
. This file can look as follows. Please note that this code is common both for Selenium and for Playwright, because it operates only on Page Object methods.
step_definitions/step_definitions.ts
:
import assert from 'assert';
import { Given, Then, When } from '@cucumber/cucumber';
import { CustomersPage } from '../page_objects/customersPage';
let customersPage: CustomersPage;
let searchSummaryAtVeryBeginning: string;
Given('the user is on the page',
async function () {
customersPage = new CustomersPage();
customersPage.open();
searchSummaryAtVeryBeginning = await customersPage.getSummaryText();
});
When('the user enters the value {string} in the text-input',
async function (value: string) {
await customersPage.setSearchInput(value);
});
When('the user selects value {string} in the drop-down',
async function (value: string) {
await customersPage.selectSearchColumnByText(value);
});
When('the user sets case sensitivity switch to {string}',
async function (isCaseSensitive: string) {
let booleanValue: boolean = isCaseSensitive.toLowerCase() === 'true';
await customersPage.setMatchCaseCheckboxField(booleanValue);
});
Then('the user should see the following result summary {string}',
async function (expectedSummary: string) {
assert.strictEqual(
await customersPage.getSummaryText(),
expectedSummary);
});
Then('the user should see that the search term is {string}',
async function (expectedTerm: string) {
let searchTerm: string = await customersPage.getSearchTermText();
assert.ok(searchTerm.startsWith(expectedTerm),
"Actual should starts with expected."
+ "\nActual: " + searchTerm
+ "\nExpected: " + expectedTerm);
});
When('the user clears filters',
async function () {
await customersPage.clickClearFiltersButton();
});
Then('the user should see that search criteria are cleared',
async function () {
assert.strictEqual(await customersPage.getSearchInputText(), "");
});
Then('the user should see that the search result summary is as in the very beginning',
async function () {
assert.strictEqual(await customersPage.getSummaryText(),
searchSummaryAtVeryBeginning);
});
Then('the user should see that the search results are as follows: {string}',
async function (expectedResults: string) {
let resultText: string
= await customersPage.getSearchResultsTableText();
assert.strictEqual(
resultText
.replace(/(\s+)/gm, " ")
.trim(),
expectedResults);
});
Everything together we can run with the npm test
command.
Our results are stored in the directory allure-results
. To visualize them we can execute:
allure serve allure-results
The results should open in the default web browser.
REPORTING ISSUE AND REPOSITORIES
Currently, there is an issue which causes that Allure framework reports results in a bit strange way. Such a problem does not occur for older versions of packages.
If you are interested in details you can see the following repositories:
- https://github.com/DanielDelimata/sample-typescript-cucumberjs-selenium (newest Cucumber-JS)
- https://github.com/DanielDelimata/sample-typescript-cucumberjs-selenium-old (correctly working Allure reports)
The Playwright version can be found here:
The story was originally created by me, but it may contain parts that were created with AI assistance. My original text has been corrected and partially rephrased by Chat Generative Pre-trained Transformer to improve the language.