Passing options to Playwright fixtures
Playwright fixtures help set up environments for each test to run in. They are great when dealing with repeated tasks like authentication. Big blocks of code that do what they need to do well.
But what if sometimes that needs to change? Perhaps a test needs to be logged in as a different type of user, or not at all. Opting a test out of using fixture will lose any benefit the others have, plus having multiple similar fixtures for situations like that would quickly become a nightmare to manage.
Thankfully, fixtures can have options passed to them. The mechanics are not so straightforward, but using them well can make fixtures even more powerful.
Adding an option
Each option is defined alongside other fixtures and wherever the base test fixture is being extended.
interface Options {
authenticated?: boolean;
role?: Role;
}
export const test = baseTest.extend<Options>({
authenticated: [true, { option: true }],
// ...
});
The option's name is the key in the object. After that is a tuple containing its default value, followed by { option: true }
object to indicate that this entry is only an option.
From here, every fixture option is then available to any other fixture that might benefit from it.
Setting an option value
Playwright has a use
option that can define configuration for the tests within that file. The value is supplied as a property on that object.
test.use({ authenticated: false });
In tests where this value has not been supplied, it will fall back to the default value defined in the test
fixture.
The use
method can also be called within a describe
block to limit its coverage to just a few tests. This can be useful where they all share a similar setup, such as using a particular user to perform their actions.
Using an option
Fixtures define their dependencies as the first argument of the function. Options are just another dependency and are declared here too.
Playwright will make sure that the option is evaluated beforehand. It will either have a defined value from the test or the default value defined in its declaration.
export const test = baseTest.extend<Options>({
context: async ({ authenticated }, use) => {
if (!authenticated) {
// User is not authed - use existing context instance
await use(unauthedContext);
return;
}
// ...
},
});
This example is borrowed from the previous post on authentication using fixtures. Here if the test decides it doesn't need to be authenticated in this scenario, then it can skip the setup altogether.
Fixture option best practice
Make the names of options as descriptive as possible. Any option that gets created is available to all other fixtures should they need it. While that can be incredibly useful, it might not be clear to the author of a test if an option will have an effect on the tests they're writing.
Similarly, keep the configuration of those options as simple as possible. Passing too much information will make the fixture less clear and put more of a requirement on the test author to craft the right value. Avoid objects where possible.
However there are some instances where an object might be preferable over setting up several boolean options - for example defining some initial test data. In these cases make sure documentation is clear showing what each option is supposed to include. If TypeScript is available, make use of the type variable when extending the base test fixture.
Summary
When used carefully, Playwright fixture options can make light work of repeated actions. Keep them small and focused to get the most benefit when authoring your end-to-end tests.