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.
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.
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:
- before all tests start
- before each test
- after each test
- 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.ts
efficiently 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();
});
- Import Statements: At the beginning, the code imports necessary modules and functions. The
Given
andThen
functions are imported from the@cucumber/cucumber
package, which are used to define the steps in the test scenario. ThepageFixture
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. Theexpect
function is imported fromplaywright/test
, which is used for assertions in tests. - 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. - 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 usingpageFixture.page.locator('input[name="q"]')
, whereinput[name="q"]
is the CSS selector for the search input field (search bars on search engines typically have a name attribute set to "q"). Thelocator
function returns a Playwright Locator object representing the search bar. The assertionexpect(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.
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.