write-node-unit-tests

star 651

Write Node.js/TypeScript unit tests for the playwright-wrapper layer. Use when: creating new Jest tests, mocking Playwright API calls, testing getters/interaction/browser-control functions in isolation, improving Node.js test coverage.

MarketSquare By MarketSquare schedule Updated 5/1/2026

name: write-node-unit-tests description: 'Write Node.js/TypeScript unit tests for the playwright-wrapper layer. Use when: creating new Jest tests, mocking Playwright API calls, testing getters/interaction/browser-control functions in isolation, improving Node.js test coverage.' applyTo: 'node/playwright-wrapper/tests/**'

Write Node.js Unit Tests

Node.js unit tests live in node/playwright-wrapper/__tests__/ and use Jest + ts-jest.

Tests must be readable and test one thing at a time. Use descriptive it() names that read as plain English sentences.

Run tests with invoke:

inv utest-node
inv utest-node --coverage   # generates HTML report at node/coverage/index.html

File naming

Name test files after the source module they test: getters.tsgetters.test.ts.

IDE typing for Jest

Every test file imports all Jest globals it uses from @jest/globals. If a Jest API such as beforeEach or afterEach is used without being imported, add it to the existing import line:

import { beforeEach, describe, expect, it } from '@jest/globals';

Do not add a /// <reference types="jest" /> directive — it is redundant when importing from @jest/globals and inconsistent with the rest of the test suite.

Keep using the global jest object for jest.mock(), jest.fn(), and jest.mocked(...). Avoid importing jest from @jest/globals in these tests, because it can lead to worse mock typing in this repo.

Mocking external module dependencies

Use jest.mock() at the top of the file (before imports) to replace modules with controlled fakes.

Mocking playwright-invoke

findLocator is the primary dependency in most getter/interaction functions. Replace it with a jest.fn() and keep exists real:

jest.mock('../playwright-invoke', () => ({
    findLocator: jest.fn(),
    exists: jest.requireActual('../playwright-invoke').exists,
}));

import { findLocator } from '../playwright-invoke';
const mockFindLocator = jest.mocked(findLocator);

Mocking the logger

Suppress log output to keep test output clean:

jest.mock('../browser_logger', () => ({
    logger: { info: jest.fn(), error: jest.fn() },
}));

Mocking Playwright errors

Use the real errors export from playwright — no mock needed. Throw new errors.TimeoutError(...) directly in test stubs.

Creating mock locators

Build a helper that returns an object with all Locator methods stubbed. Pass overrides to change just one method per test:

function makeMockLocator(overrides: Partial<{
    waitFor: jest.Mock;
    isVisible: jest.Mock;
    isEnabled: jest.Mock;
    getAttribute: jest.Mock;
    isEditable: jest.Mock;
    isChecked: jest.Mock;
    elementHandle: jest.Mock;
}> = {}) {
    return {
        waitFor: jest.fn().mockResolvedValue(undefined),
        isVisible: jest.fn().mockResolvedValue(true),
        isEnabled: jest.fn().mockResolvedValue(true),
        getAttribute: jest.fn().mockResolvedValue(null),
        isEditable: jest.fn().mockResolvedValue(true),
        isChecked: jest.fn().mockResolvedValue(false),
        elementHandle: jest.fn().mockResolvedValue(makeElementHandle()),
        ...overrides,
    } as unknown as Locator;
}

Creating mock gRPC request objects

After the ts-proto migration, generated protobuf types are plain interfaces with direct property access — no getter methods. Mock them with a plain object cast to any:

function makeRequest(selector = '#el', strict = false) {
    return { selector, strict } as any;
}

Asserting on mock interactions

Use toHaveBeenCalledWith and toHaveBeenCalledTimes to verify how mocks were called — this is often the primary assertion in the wrapper layer:

expect(mockFindLocator).toHaveBeenCalledWith(callOptions, '#el', false);
expect(mockFindLocator).toHaveBeenCalledTimes(1);

Use toBe for primitives and reference identity checks; use toEqual for deep object comparison.

Async error tests

When testing rejected promises, call expect.assertions(n) at the top of the test. This ensures the test fails if the error path is never reached:

it('throws when element is not found', async () => {
    expect.assertions(1);
    mockFindLocator.mockRejectedValue(new Error('not found'));
    await expect(getElementStates(makeRequest(), {} as any)).rejects.toThrow('not found');
});

Organising tests with nested describe

Group related cases under inner describe blocks to make large test files navigable:

describe('getElementStates', () => {
    describe('when element is visible', () => {
        it('returns visible state', async () => { ... });
    });

    describe('when element times out', () => {
        it('returns detached state', async () => { ... });
    });
});

Sequencing multiple evaluate calls

When a function calls elementHandle.evaluate() multiple times, use mockResolvedValueOnce chaining:

const elementHandle = {
    evaluate: jest.fn()
        .mockResolvedValueOnce(false)   // first call: 'selected' in e
        .mockResolvedValueOnce(true),   // second call: document.activeElement === e
};

Test structure

describe('functionName', () => {
    beforeEach(() => {
        jest.clearAllMocks();
    });

    it('returns detached state when element times out', async () => {
        const locator = makeMockLocator({
            waitFor: jest.fn().mockRejectedValue(new errors.TimeoutError('Timeout')),
        });
        mockFindLocator.mockResolvedValue(locator);

        const result = await getElementStates(makeRequest(), {} as any);

        expect(JSON.parse(result.json)).toBe(2); // detached
    });
});

Things to avoid

  • Do not add comments or docstrings explaining what the test does — the test name and code should be self-explanatory.
  • Mock at the boundary closest to your unit. Prefer spying over full module replacement when only one method needs controlling; don't mock modules that aren't exercised by the test.
  • Do not assert on log messages — they are implementation details.
  • Do not share mutable mock state between tests; always call jest.clearAllMocks() in beforeEach.
Install via CLI
npx skills add https://github.com/MarketSquare/robotframework-browser --skill write-node-unit-tests
Repository Details
star Stars 651
call_split Forks 143
navigation Branch main
article Path SKILL.md
More from Creator
MarketSquare
MarketSquare Explore all skills →