Automation Testing: How are Cucumber.JS hooks useful for running tests with Playwright

Hooks are used when you need to setup a test environment before a test is started and also, to teardown test environment post completion of the test.

Akarsh Seggemu, M.Sc.
7 min readJun 15, 2024

When you require each test to be setup before it is executed and also it needs to be teardown post completion of the test. Hooks is a feature in Cucumber.JS which provides this feature which can be used with before and after each scenario following cucumber execution style.

Test report

Declare hooks

For example, In the file hooks.ts we define the BeforeAll, Before, After and AfterAll hooks that are executed at different scopes of the cucumber execution style.

import { BeforeAll, AfterAll, After, Before, Status } from '@cucumber/cucumber';
import { chromium, Browser, Page, BrowserContext } from '@playwright/test';

import { pageFixture } from './pageFixture';

let page: Page;
let browser: Browser;
let context: BrowserContext;

BeforeAll(async function () {
browser = await chromium.launch({ headless: false });
});

Before(async function () {
context = await browser.newContext();
page = await context.newPage();
pageFixture.page = page;
});

After(async function ({ pickle, result }) {
if (result?.status === Status.FAILED) {
// Screenshots are saved in the directory after the test is completed for each scenario
const img = await pageFixture.page.screenshot({ path: `./test-result/screenshots/${pickle.name}.png` });
await this.attach(img, 'image/png');
}
await pageFixture.page.close();
await context.close();
});

AfterAll(async function () {
await pageFixture.page.close();
await browser.close();
});

In the above TypeScript code we import Cucumber and Playwright. We defined hooks that are executed at different stages of the testing lifecycle:

  1. before all tests start
  2. before each test
  3. after each test
  4. after all tests have completed

These hooks are crucial for setting up and tearing down the environment needed for each test scenario.

At the beginning of the file, necessary modules and objects are imported, including Cucumber’s lifecycle hooks (BeforeAll, AfterAll, After, Before, Status) and Playwright's browser automation tools (chromium, Browser, Page, BrowserContext). Additionally, a custom fixture named pageFixture is imported, which is likely used to manage page-specific setup or state.

The BeforeAll hook is set to run once before all test scenarios start. Inside this hook, the Chromium browser is launched in non-headless mode (meaning the browser UI will be visible during tests) by calling chromium.launch({ headless: false }). This browser instance is stored in the browser variable for use in later hooks.

The Before hook runs before each test scenario. It creates a new browser context (context = await browser.newContext()) and a new page within that context (page = await context.newPage()). The page object is then stored in the pageFixture for use during the test. This setup ensures each test scenario runs in a fresh browser environment.

The After hook is executed after each test scenario. It checks if the test failed by examining the result.status. If the test did fail, it captures a screenshot of the current page state and saves it to a specified directory, naming the file after the test scenario's name. This is useful for debugging failed tests by visually inspecting the state at the time of failure. Regardless of the test outcome, the hook closes the current page and browser context to clean up resources.

Finally, the AfterAll hook runs once after all test scenarios have completed. It ensures that the browser is properly closed, releasing all resources it consumed during the test run.

Overall, the code in hooks.tsefficiently manages browser resources for automated tests, ensuring each test runs in a clean environment and providing useful debugging information in case of failures.

Declare Fixtures

File pageFixture.ts ,

import { Page } from '@playwright/test';

export const pageFixture = {
// @ts-ignore
page: undefined as Page
}

The above code in TypeScript, defines an object named pageFixture. This object has a single property named page. The page property is annotated with the type Page but is initially set to undefined. The syntax undefined as Page is a type assertion. It tells TypeScript that the page property will eventually hold a value of type Page, but it is undefined for now. This is useful in scenarios where you might not have the value ready at the time of object creation but you know what type it will be eventually.

// @ts-ignore Comment: Right above the page property definition, there is a comment // @ts-ignore. This is a special TypeScript directive that tells the TypeScript compiler to ignore the next line of code during type checking. This is used here because, under normal circumstances, TypeScript might raise an error or warning assigning undefined to a property explicitly typed as Page. By using @ts-ignore, you're instructing TypeScript to bypass its usual checks for this line, which can be useful in situations where you're dealing with dynamic or unpredictable types, or when interacting with code that TypeScript doesn't understand well. However, it's generally recommended to use @ts-ignore sparingly, as it bypasses the type safety mechanisms of TypeScript.

Overall, the code in pageFixture.ts is defining an object pageFixture with a property page that is intended to hold a value of type Page but is initially undefined. The use of @ts-ignore is a way to bypass TypeScript's static type checking for this particular line, allowing the assignment of undefined to a property that is expected to be of a specific type.

For example, in file BingSearch.feature

Feature: Bing Search home page

Scenario: Check if Bing search bar is present
Given A web browser is at the Bing home page
Then The Bing search bar is present

the feature is “Bing Search home page”. The scenario described is “Check if Bing search bar is present”. This is what you will be testing in this scenario. Scenarios are made up of steps that describe the given situation, the action to take, and the expected outcome. Given is the keyword, which is used to describe the initial context of the system before the user starts interacting with it. In your scenario, “A web browser is at the Bing home page” sets the stage for the test, indicating that the first step is to navigate to the Bing home page. Then is the keyword, which is used to describe an expected outcome or state. In your case, “The Bing search bar is present” specifies what you expect to find as a result of the given conditions. This step will likely involve checking the page to ensure that the search bar is indeed present.

How to use fixtures in step definitions?

For example, in the file bing_search_steps.ts

import { Given, Then } from "@cucumber/cucumber";
import { pageFixture } from "../../hooks/pageFixture";
import { expect } from "playwright/test";

Given('A web browser is at the Bing home page', async function () {
await pageFixture.page.goto('https://www.bing.com');
await pageFixture.page.waitForLoadState('domcontentloaded');
expect(pageFixture.page.url()).toBe('https://www.bing.com/');
});

Then('The Bing search bar is present', async function () {
const searchBar = await pageFixture.page.locator('input[name="q"]');
expect(searchBar).toBeTruthy();
});
  1. Import Statements: At the beginning, the code imports necessary modules and functions. The Given and Then functions are imported from the @cucumber/cucumber package, which are used to define the steps in the test scenario. The pageFixture is imported from a local file within the project, presumably a setup file that initializes or provides a Playwright page object for interacting with web pages. The expect function is imported from playwright/test, which is used for assertions in tests.
  2. Given Step: The first step, defined with the Given function, describes the initial context of the scenario: "A web browser is at the Bing home page". It's an asynchronous function, indicating that it performs operations that return promises, such as navigating to a web page or waiting for elements to load. Inside this function, pageFixture.page.goto('https://www.bing.com') navigates the browser to the Bing home page. Then, pageFixture.page.waitForLoadState('domcontentloaded') waits until the DOM content of the page is fully loaded. Finally, an assertion checks if the current URL is exactly 'https://www.bing.com/' to ensure the navigation was successful.
  3. Then Step: The second step, defined with the Then function, specifies an expected outcome based on the initial context: "The Bing search bar is present". Similar to the first step, it's an asynchronous function. It attempts to locate the search bar on the page by using pageFixture.page.locator('input[name="q"]'), where input[name="q"] is the CSS selector for the search input field (search bars on search engines typically have a name attribute set to "q"). The locator function returns a Playwright Locator object representing the search bar. The assertion expect(searchBar).toBeTruthy(); checks if the search bar is successfully located, implying its presence on the page.

Overall, the code in file bing_search_steps.ts is a part of a test that automates a web browser to visit the Bing home page and verify that the search bar is present, using Playwright for browser automation and assertions, and Cucumber for organizing the test into readable steps.

Test report in playwright

Hope this article helped you understand how to use fixtures in playwright.

If you like my articles, please follow me on Medium, you can also watch my videos on YouTube and you can also support me by buying me a coffee.

--

--

Akarsh Seggemu, M.Sc.

IT Team Lead | Post graduate Computer Science from TU Berlin | Telugu writings are found in this account https://medium.com/@akarshseggemu_telugu