Chapter 3.4.4: Cypress and Vitest Driver Implementation
How to implement a test driver that enables us to run tests in multiple test runners like Cypress, Playwright, and Vitest.
Disclaimer: You are reading an early version of the text! The final version of the book will be revised and may contain additional content.
Previously, we have seen what techniques we can use to make our driver layer independent of whether our test framework provides an asynchronous or pseudo-synchronous API. So let's take a closer look at how we can implement a fully functional driver.
To avoid exhausting the possibilities of a book, we will implement a minimal driver with only a few methods. However, if you are interested in a more comprehensive code example, look at the following GitHub project: https://github.com/maoberlehner/talk-vitest-cypress/tree/main/test/drivers
Assertions
Let's start with the functionality we need for Assertions
. Assertions
allow us to check if our application meets certain expectations. In the first step, we define the interface in TypeScript:
// test/drivers/types.ts
type ActionCallback = () => unknown|Promise<unknown>;
export type Assertions = {
shouldBeVisible: () => ActionCallback,
shouldHaveAttribute: (name: string, value?: string|RegExp) => ActionCallback,
}
export type AssertionsNot = {
shouldNotBeVisible: () => ActionCallback,
shouldNotExist: () => ActionCallback,
}
First, we define Assertions
for shouldBeVisible()
to determine if an element is visible and shouldHaveAttribute()
to check if an element has a particular attribute with a specific value. In addition, we also define negative assertions for shouldNotBeVisible()
and shouldNotExist()
. Later we will see that we only allow these not
Assertions
for specific selectors. To make this possible, we define a separate type for negative Assertions
: AssertionsNot
.
As described above, our test driver must be able to handle both asynchronous and pseudo-synchronous code. Consequently, we allow both unknown
and Promise<unknown>
as possible return values for the ActionCallback
type. Depending on the use case, the corresponding driver must wait for Promises if the implementation returns Promises on ActionCallbacks
.
Now it is time to think about the implementation of the interface. Let`s start with writing the code for the Cypress driver:
// test/drivers/cypress/cypress-driver.ts
// ...
type ElementResolver = () => Cypress.Chainable;
function makeAssertions(elementResolver: ElementResolver): Assertions {
return {
shouldBeVisible: () => () => elementResolver().should(`be.visible`),
shouldHaveAttribute: (attribute, value) => () => elementResolver()
.should(`have.attr`, attribute).and(`match`, value),
};
}
The makeAssertions()
function returns an Assertions
object with all the possible Assertions
we will need later to write our tests. The elementResolver()
method that we pass to the function as a parameter returns a Cypress.Chainable
in the case of the Cypress driver. The Chainable
provides all the methods we know from writing Cypress tests. In the case of our two Assertions
, we use the should()
method to check if an element meets certain conditions.
Next, let`s look at how we can implement the same functionality for our Vitest Driver.
Keep reading with a 7-day free trial
Subscribe to Good Tests for Vue Applications to keep reading this post and get 7 days of free access to the full post archives.