Setup Vitest in a Tauri Project

How to write Unit Tests for Tauri Frontend with Vitest?

Starting a project for me usually starts with setting up the testing infrastructure. The only exception is when one already exists. In this article we will learn two things. We will start from setting up vitest in the Tauri project. We will then learn how to write a test in JavaScript for a part that’s connecting to the Tauri Rust backend.

I already wrote about our Tauri experience in how to write unit tests in rust for Tauri. This time, we’re going to setup the tests for the frontend using Vitest. We’re also going to write a first meaningful test for a Tauri frontend and see how to work with the Rust part during JavaScript tests.

How to Setup a Tauri App?

That’s relatively easy. Here are the steps:

  1. Make sure you have the prerequisite on your machine.
  2. cd into your projects folder
  3. Create a Tauri app using: npm create tauri-app@latest

You will be asked a few questions during the app’s setup process.

For this tutorial, I’m going to select the following:

Need to install the following packages – ‘y’

✔ Project name · tauri-demo
✔ Choose which language to use for your frontend · TypeScript / JavaScript
✔ Choose your package manager · npm
✔ Choose your UI template · Vanilla
✔ Choose your UI flavor · TypeScript (yes, Tauri calls its frontend “UI” at times)

cd into the folder (in my case, tauri-demo) and run npm install && npm run tauri dev

This will install the JavaScript dependencies and start the desktop app in dev mode.

If all worked correctly, you should see the app running like this:

The Initial Tauri App

That’s great! We now have a working greeter app!

How to Add Vitest to a Project?

The first step would be to install vitest. That’s easy: npm i -D vitest.

Once we have that, we can add the following script to our package.json:

"test:frontend": "vitest"

so our package.json file now looks like this:

{
"name": "tauri-demo",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
"tauri": "tauri",
"test:frontend": "vitest"
},
"dependencies": {
"@tauri-apps/api": "^1.4.0"
},
"devDependencies": {
"@tauri-apps/cli": "^1.4.0",
"typescript": "^5.0.2",
"vite": "^4.4.4",
"vitest": "^0.34.3"
}
}
view raw package.json hosted with ❤ by GitHub
The project’s package.json after setting up vitest

Running our test code won’t do anything because the project has no tests. Let’s write the first one.

Writing Our First Test

We currently have one file in our project – main.ts.

Let’s create a simple test for it. We will create a test file: main.spec.ts in the same folder and fill it with this content:

import './main';
describe('main', () => {
it('should be defined', () => {
expect(true).toBeTruthy();
});
});
view raw main.spec.ts hosted with ❤ by GitHub
The first Tauri-demo spec file

In order to run the tests, we will use our npm script:

npm run test:frontend.

Try it out…

If you did everything right so far, the test should fail like this:

A test is running! A test is failing! Hooray!

How to Configure Vitest for Frontend Testing?

The reason the test fails is that we are trying to use window. Let’s fix that.

How to solve window is undefined in Vitest?

vitest‘s default environment is Nodejs, and there’s no window there. In order for it to work, we’ll need to setup jsdom, which is a browser emulator for nodejs. Here’s how you setup jsdom in vitest:

  1. Install jsdom: npm i -D jsdom
  2. Set jsdom as the environment in vite.config.ts:
vite.config.ts with the test configured for jsdom environment

Great! Let’s run the tests again: npm run test:frontend.

Did the tests fail again? Great! Because they should. Let’s talk about the next error.

How to fix describe is not defined in Vitest?

Here’s the error:

The error we get after setting up fixing windows is not defined: describe is not defined

So now we need a way to define describe and probably all of the other test functions. We could import them one by one from vitest (or any other test framework for that matter). Another easy option would be to tell vitest to expose the test functions as globals.

All we need to do is add globals: true to the test configuration. The final file should look like this:

import { defineConfig } from "vite";
export default defineConfig(async () => ({
clearScreen: false,
server: {
port: 1420,
strictPort: true,
},
envPrefix: ["VITE_", "TAURI_"],
test: {
globals: true,
environment: "jsdom",
},
}));
view raw vite.config.ts hosted with ❤ by GitHub
Final vite config to enable tests

If we run npm run test:frontend now we will get the beloved green screen:

The test is passing! Our testing infrastructure is working!

Now that we have a working testing infrastructure, we can move on to the next step: Start building our app! Let’s write a meaningful test just to see it is working.

Writing the First Meaningful Test

Understanding the Tauri Code

Our test doesn’t say much. It just checks if true is truth. Because our main module is already written, let’s see what it does. The first thing that’s going to run is this piece of code:

  1. On DOMContentLoaded
  2. Get the input and message elements
  3. Set a submit eventListener on the form element, prevent its default and fire the greet function.

Let’s test the greet functionality, because it’s the main business logic:

The Greet Function

This function checks if we have message and input elements. If they exist, it fills the message element with the result of the invoke function.

The invoke function is part of the Tauri API. It is the communication protocol between the JavaScript client and the Rust backend. It is called Inter-Process Communication, or IPC in short. In essence, we invoke the greet API with the payload:

{
  name: greetInputEl.value,
}

In the folder src-tauri/src there’s the main.rs file, where the greet API is implemented:

The Greet API in the Tauri backend

It is easy to see the string that’s supposed to return is

Hello, ${name}! You've been greeted from Rust!

Let’s summarize what our app is all about:

  1. Listen to DOMContentLoaded event
  2. Set an event listener on the greet form
  3. Set the greet message to Hello, ${name}! You've been greeted from Rust!

That’s what our test needs to check. Let’s write it down.

Writing the Test

The first thing we need to do is change the it description:

As part of the preparation phase, we need to setup the DOM elements:

Now we need to start the first piece of logic inside the DOMContentLoaded listener by dispatching the event:

window.dispatchEvent(new Event("DOMContentLoaded"));

Now let’s get a hold on our elements and set the input element with a value:

Now we come to a Tauri specific test part. We need to mock the Rust backend.

How to Mock the Tauri Backend

In order to mock the Tauri backend, we need to use Tauri test utilities. In this case, we want to mock the IPC. We will import mockIPC like this: import { mockIPC } from "@tauri-apps/api/mocks";

We then use it like this:

This is a very simple API, so the mock looks much like the “real thing”.

What it’s going to do is intercept every call to invoke and return the value returned from the callback.

Now we need two more things: dispatch the form’s submit event and wait for an event loop cycle for the API to return its value:

The first line is pretty straight forward. The second line is an old trick to move one “tick” ahead and await for async processes to settle down. Many people do not agree with this method, but sometimes they are a must and in vivid we use them a lot in tests because of the async nature of the Fast templating system.

Finaly, we get to the assertion:

We expect the greet message element’s text to be the expected value.

If we run the test now it will pass. Here’s the full code of the test:

import './main';
import { mockIPC } from "@tauri-apps/api/mocks";
describe('main', () => {
it('should set the greeting message inside the message element', async () => {
document.body.innerHTML = `
<form id="greet-form">
<input id="greet-input" />
</form>
<div id="greet-msg"></div>
`;
window.dispatchEvent(new Event("DOMContentLoaded"));
const name = 'John Doe';
const greetInputEl = document.querySelector("#greet-input") as HTMLInputElement;
const greetMsgEl = document.querySelector("#greet-msg");
const greetForm = document.querySelector("#greet-form") as HTMLFormElement;
greetInputEl.value = name;
mockIPC((cmd, args) => {
if(cmd === "greet") {
return `Hello, ${args.name}! You've been greeted from Rust!`;
}
});
greetForm.dispatchEvent(new Event("submit"));
await new Promise((resolve) => setTimeout(resolve, 0));
expect(greetMsgEl?.textContent).toBe(`Hello, ${name}! You've been greeted from Rust!`);
});
});
view raw main.spec.ts hosted with ❤ by GitHub
The full test file

We can always refactor for clarity:

it('should set the greeting message inside the message element', async () => {
setupDomElements();
window.dispatchEvent(new Event("DOMContentLoaded"));
const name = 'John Doe';
const { greetForm, greetMsgEl } = getElementsAndSetInputValue(name);
mockIPC((cmd, args) => {
if(cmd === "greet") {
return `Hello, ${args.name}! You've been greeted from Rust!`;
}
});
await dispatchFormSubmit(greetForm);
expect(greetMsgEl?.textContent).toBe(`Hello, ${name}! You've been greeted from Rust!`);
});

It’s still long, but note this code was not written with testability in mind. It’s legacy code we covered with a test.

The next step would be to create a login screen for the application, but this will wait for the next article.

Summary

Testing is a basic part of every development ecosystem. Vitest is the newest test runner in the JavaScript ecosystem. With this article, combined with the article about setting up unit tests in rust, you now know how to setup a fullstack testing infrastructure in a Tauri project.

In the next posts in this series, we are going to use the knowledge garnered in the articles to build a full application with Tauri as a desktop wrapper, Vivid for frontend components and Firebase for user management and database.

Thanks a lot to Hod Bauer for the kind and thorough review.

0 0 votes
Article Rating
Subscribe
Notify of
guest

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments