Tutorials

Custom Test Matchers

nut.js relies on tests to avoid regressions and ensure that the framework works as expected. To do so, we use Jest as our test runner.

Jest provides a lot of built-in matchers to make assertions in your tests readable and easy to write.

test('two plus two is four', () => {
    const expectedResult = 4;
    expect(2 + 2).toBe(expectedResult);
});

In our codebase we make use of these built-in matchers because they are easy to use and well documented. But as time passed, we found ourselves repeating custom test helpers to assert certain conditions, especially so when writing end-to-end tests. This sparked the idea to take a closer look on how to write custom matchers for Jest.

What are Custom Jest Matchers?

Custom Jest matchers are user-defined matchers that extend Jest's default set. They allow developers to create specific test conditions that are not covered by the built-in matchers. This customization leads to more readable and maintainable tests, especially when dealing with complex data structures or specific business logic.

Advantages of Using Custom Matchers

  1. Improved Readability: Custom matchers can make tests more readable and expressive. For example, expect(user).toBeActive() is more understandable than a series of checks on user properties.

  2. Reusable Logic: They promote DRY (Don't Repeat Yourself) principles by encapsulating repeated test logic into a single, reusable matcher.

  3. Enhanced Testing Logic: Custom matchers can handle complex testing logic that built-in matchers cannot, like deep object comparisons or asynchronous validations.

  4. Better Error Messages: They can provide clearer, more context-specific error messages, aiding in quicker debugging.

Implementing Custom Matchers in Jest

Creating a custom matcher in Jest involves extending the expect object. Here's a basic structure:

expect.extend({
    toBeDivisibleBy(received, argument) {
        const pass = received % argument === 0;
        if (pass) {
            return {
                message: () => `expected ${received} not to be divisible by ${argument}`,
                pass: true,
            };
        } else {
            return {
                message: () => `expected ${received} to be divisible by ${argument}`,
                pass: false,
            };
        }
    },
});

In this example, toBeDivisibleBy is a custom matcher to check if a number is divisible by another number.

Custom nut.js Matchers

nut.js ships a set of custom matchers, available in the @nut-tree/nut-js package.

import {jestMatchers} from "@nut-tree/nut-js";

To use them, you need to extend the expect object in your test file.

expect.extend(jestMatchers);

This will make the custom matchers available in your tests. Currently, the following matchers are available:

toBeAt

toBeAt is a matcher which works with the nut.js mouse instance and verifies mouse cursor position.

It receives a Point which specifies the expected mouse cursor position on screen.

const {jestMatchers, mouse, Point} = require("@nut-tree/nut-js");

expect.extend(jestMatchers);

describe("Basic test with custom toBeAt Jest matchers", () => {
    it("should verify that cursor is at a certain position", async () => {
        // GIVEN
        const targetPoint = new Point(10, 10);

        // WHEN
        await mouse.setPosition(targetPoint);

        // THEN
        await expect(mouse).toBeAt(targetPoint);
    });
});

It also supports negation as known from other matchers:

const {jestMatchers, mouse, Point} = require("@nut-tree/nut-js");

expect.extend(jestMatchers);

describe("Basic test with custom toBeAt Jest matchers", () => {
    it("should verify that cursor is not at a certain position", async () => {
        // GIVEN
        const targetPoint = new Point(10, 10);
        const wrongPoint = new Point(10, 10);

        // WHEN
        await mouse.setPosition(targetPoint);

        // THEN
        await expect(mouse).not.toBeAt(wrongPoint);
    });
});

toBeIn

toBeIn allows us to verify whether our mouse cursor is located within a certain Region or not.

const {jestMatchers, mouse, Point} = require("@nut-tree/nut-js");

expect.extend(jestMatchers);

describe("Basic test with custom toBeIn Jest matchers", () => {
    it("should verify that cursor is within a certain region", async () => {
        // GIVEN
        const targetPoint = new Point(10, 10);
        const targetRegion = new Region(5, 5, 10, 10);

        // WHEN
        await mouse.setPosition(targetPoint);

        // THEN
        await expect(mouse).toBeIn(targetRegion);
    });
});

Just like toBeAt, it supports negation:

const {jestMatchers, mouse, Point} = require("@nut-tree/nut-js");

expect.extend(jestMatchers);

describe("Basic test with custom toBeIn Jest matchers", () => {
    it("should verify that cursor is not within a certain region", async () => {
        // GIVEN
        const targetPoint = new Point(10, 10);
        const targetRegion = new Region(100, 100, 10, 10);

        // WHEN
        await mouse.setPosition(targetPoint);

        // THEN
        await expect(mouse).not.toBeIn(targetRegion);
    });
});

toShow

The most interesting custom matcher is the toShow one. It works with the screen instance of nut.js and thus is able to verify a whole lot of things...

It supports everything that is locatable using find, thus it is possible to write assertions for:

Image Assertions

const {jestMatchers, screen, imageResource} = require("@nut-tree/nut-js");
require("@nut-tree/nl-matcher");

expect.extend(jestMatchers);

describe("Basic test with custom toShow Jest matchers", () => {
    it("should verify that the screen shows a certain image", async () => {
        // GIVEN
        screen.config.resourceDirectory = "../../e2e/assets";

        // WHEN

        // THEN
        await expect(screen).toShow(imageResource("an_image.png"));
    });
});

Once again, it is also possible to negate an expectation:

const {jestMatchers, screen, imageResource} = require("@nut-tree/nut-js");
require("@nut-tree/nl-matcher");

expect.extend(jestMatchers);

describe("Basic test with custom toShow Jest matchers", () => {
    it("should verify that the screen does not show a certain image", async () => {
        // GIVEN
        screen.config.resourceDirectory = "../../e2e/assets";

        // WHEN

        // THEN
        await expect(screen).not.toShow(imageResource("different_image.png"));
    });
});

Window Assertions

const {jestMatchers, screen, windowWithTitle} = require("@nut-tree/nut-js");
const {useBoltWindowFinder} = require("@nut-tree/bolt");

useBoltWindowFinder();

expect.extend(jestMatchers);

describe("Basic test with custom toShow Jest matchers", () => {
    it("should verify that the screen shows a window with matching title", async () => {
        // GIVEN
        const windowTitleRegex = /.*custom-test-matchers.*/;

        // WHEN

        // THEN
        await expect(screen).toShow(windowWithTitle(windowWithTitleRegex));
    });
});

Text Assertions

const {jestMatchers, screen, singleWord} = require("@nut-tree/nut-js");
const {
    LanguageModelType,
    configure,
} = require("@nut-tree/plugin-ocr");

configure({languageModelType: LanguageModelType.BEST});

expect.extend(jestMatchers);

describe("Basic test with custom toShow Jest matchers", () => {
    it("should verify that the screen shows a certain word", async () => {
        // GIVEN
        const wordToFind = "nut.js";

        // WHEN

        // THEN
        await expect(screen).toShow(singleWord(wordToFind));
    });
});

Color Assertions

const {jestMatchers, screen, RGBA, pixelWithColor} = require("@nut-tree/nut-js");

expect.extend(jestMatchers);

describe("Basic test with custom toShow Jest matchers", () => {
    it("should verify that the screen shows a pixel with a certain color", async () => {
        // GIVEN
        const colorToFind = new RGBA(255, 0, 0, 255);

        // WHEN

        // THEN
        await expect(screen).toShow(pixelWithColor(colorToFind));
    });
});

What About Vitest?

Vitest is the next generation testing framework (as they like to call themselves) and it also supports custom matchers. And the cool thing is: It is compatible with Jest matchers!

So if you want to use Vitest, you can still use the custom matchers from nut.js.

import {expect, test} from "vitest";
import {screen, jestMatchers, windowWithTitle} from "@nut-tree/nut-js";
import {useBoltWindowFinder} from "@nut-tree/bolt";

useBoltWindowFinder();

expect.extend(jestMatchers);

test("toShow", async () => {
    // GIVEN
    const windowTitle = /.*SMBP2021.*/;

    // WHEN

    // THEN
    await expect(screen).toShow(windowWithTitle(windowTitle));
});

Conclusion

Custom Jest matchers are a powerful tool to enhance the testing experience. They improve test readability, promote code reuse, handle complex testing scenarios, and provide better error messages. By creating custom matchers tailored to specific needs, nut.js ensures more efficient and effective testing.

All the best

Simon

Previous
Text Search with @nut-tree/plugin-azure