How To Set Up A Custom World For Cucumber/Playwright Tests In TypeScript
Cucumber
Playwright
TypeScript
24/04/2023
It's important to ensure that each test you run is independent of each other. Cucumber uses the concept of a World, which is an isolated scope for each scenario, exposed as this
to each step.
import { CustomWorld } from "./custom-world"
Given("my color is {string}", function(this: CustomWorld, color: string) { const { pageUrl } = this // You may either access a variable set in this unique World, this.color = color // Or set a variable accessible to following steps. // [...]})
GitHub user Tallyb has a good example project that implements a World setup. No need to reinvent the wheel, yet improving it wouldn't hurt. 😁
Hooks
There's a configuration file I won't focus on, as its content is quite straightforward. Rather, we should pay attention to the hooks file. This file does the grunt work of launching the test browser, storing traces (similar to log files) after a test has failed, instantiating variables, etc.
The Before
hook is of particular to interest to us since this is where you would define any variables that you wish to use throughout any scenario.
Before(async function( this: CustomWorldBeforeSetup, // used to be `ICustomWorld` { pickle }: ITestCaseHookParameter) { this.startTime = new Date() this.pageUrl = "https://localhost:3000" // New variable
// [...]})
However, TypeScript will complain 👎 if we define any new variables in this hook. That's because it hasn't been defined in the CustomWorldBeforeSetup
type. Note that I've replaced the original ICustomWorld
with the aforementioned type. Read more to find out why!
Custom World
The "World" is nothing more than a JavaScript class that is instantiated on each test run. By default, only debug
is set. I separated the typing of the "Custom World" into two:
CustomWorldBeforeSetup
CustomWorld
Before setup
I renamed ICustomWorld
to CustomWorldBeforeSetup
. For each scenario you test, the TestWorld
class will be instantiated before any of the hooks run. This means, unless you declare the variables inside the class itself, as is the case with debug
, you will mark most properties as optional.
/** * The "custom world" before the setup of a scenario. */export interface CustomWorldBeforeSetup extends World { debug: boolean
// Remaining properties are optional feature?: messages.Pickle context?: BrowserContext page?: Page pageUrl?: string // New property testName?: string startTime?: Date server?: APIRequestContext playwrightOptions?: PlaywrightTestOptions}
class TestWorld extends World implements CustomWorldBeforeSetup { constructor(options: IWorldOptions) { super(options) }
debug = false}
setWorldConstructor(TestWorld)
However, this interface doesn't accurately reflect 🙅♀️ the custom world after the entire setup has run. Furthermore, you would have to always check if the variable is defined since it's marked as optional. Hence, why there's a second interface.
After setup
In this interface, we know for sure that all the listed properties below are defined since this represents TestWorld
after the Before
hook has run.
/** * The "custom world" after the setup of a scenario, i.e. with all of its * properties set. */export default interface CustomWorld extends CustomWorldBeforeSetup { feature: messages.Pickle context: BrowserContext page: Page pageUrl: string // New property set in a hook testName: string startTime: Date server: APIRequestContext color?: string // New property set in a step}
Consequently, at any step, assign this
the CustomWorld
interface, not CustomWorldBeforeSetup
. Lastly, notice 🔔 color
is an optional property as it is set in a scenario, i.e. step, not in a hook as is the case with pageUrl
.