-
Notifications
You must be signed in to change notification settings - Fork 0
07 Writing your own modules
Once you start writing some more challenging tests, you'll find yourself looking for a way to organize your code and remove repetition.
Custom modules help with that. Judging from my experience, you'll want to write them even for relatively simple tasks for the sake of readibility.
So let's get started!
Below is the source code of the container
module. We'll use it as an example to learn how to write our own modules. Have a quick look. It'll be explained in more details later.
module.exports = {
name: "container",
methods: {
set: async function (containerElement, name) {
this._containers = {
...(this._containers ?? {}),
[name]: containerElement,
};
},
get: function (name) {
const container = this._containers?.[name];
if (!container)
throw new Error(
`No container with the name "${name}" has been declared.`
);
return container;
},
},
};
That's how we install it in one of the Jest's setupFilesAfterEnv
files:
// First we require it:
const containerModule = require("./modules/container");
// And then we call the swapModule method of the global testCtx object:
testCtx.swapModule(containerModule);
And now it's ready to be used in test files!
testCtx[0].containerSet(await testCtx[0].tlFindByTestId("myForm"), "form");
So what exactly happened?
A module is defined by an object with two properties:
- name
- methods
The name
is unique among all modules and may be seen at the beginning of every method call. So if a module is called container
, then all of the methods it exposes will start with container
, like containerSet
, containerGet
, containerDoSomething
etc.
Methods are defined as an object under the methods
key. If we wanted to use a containerDoSomething
method in our tests, we'd define it this way:
module.exports = {
name: "container",
methods: {
doSomething: function () {
console.log("containerDoSomething");
},
},
};
There are two things worth paying attention to:
- The "d" letter in "doSomething" is lower-case and it's automatically converted to upper-case when it's conjugated with the module name, giving us "containerDoSomething".
- The method is defined as a tranditional function.
The reason why a method isn't an arrow function is because it's bound to the whole context.
It means that you can access a shared this
variable across all your methods. This is how the "browser" module sets something:
module.exports = {
name: "browser",
methods: {
open: async function () {
// ...
this.driver = driver;
//...
And here you can see how the "screenshot" module reads it:
module.exports = {
name: "screenshot",
methods: {
take: async function (id) {
// ...
const screenshot = await this.driver.takeScreenshot();
// ...
What's great is that you can even call other modules from within your module. For instance, the "browser" module uses the "teardown" module:
module.exports = {
name: "browser",
methods: {
open: async function () {
// ...
this.teardownRegister(() => this.browserClose());
},
// ...
},
};
If your methods accepts multiple arguments, then you may want to consider ordering these arguments so that the users of your module may benefit from chaining.
The idea is:
If some of the arguments accepted by your method may come from calling other methods, consider placing these arguments at the beginning.
Let's say you're writing a method that clicks some element some number of times.
It could look something like this:
{
name: 'click',
methods: {
times: async function (times, element) {
for (let i = 0; i < times; i++) {
await element.click();
}
}
}
}
Now imagine you want to call it with a WebElement obtained using Testing Library. It'd look something like this:
const toClick = await testCtx[0].tlFindByText("Add");
await testCtx[0].clickTimes(3, toClick);
It's fine. It works. But it ain't pretty!
It'd be much better if the order of arguments accepted by the clickTimes
method was like this:
times: async function (element, times) {
Then you could benefit from the chaining capabilities provided by the context:
testCtx[0]
.tlFindByText("Add")
.clickTimes(3)