agentleFS
MarketSquareMarketSquare/robotframework-browser/.claude/skills/node-unit-test/SKILL.md

write-node-unit-tests

Skill663 starsChanged 6 days agoApache-2.0View on GitHub

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.

The file

Show its rules and text

We split this skill into the rules it gives an agent

15 rules. About 0 are common, 3 are distinctive, and 12 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈3

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 12

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Run tests with invoke:

    line 12
  2. Very specific:

    Every test file imports all Jest globals it uses from @jest/globals.

    line 25
  3. Very specific:

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

    line 31
  4. Very specific:

    Keep using the global jest object for jest.mock(), jest.fn(), and jest.mocked(...).

    line 33
  5. Very specific:

    Avoid importing jest from @jest/globals in these tests, because it can lead to worse mock typing in this repo.

    line 34
  6. Very specific:

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

    line 38
  7. Very specific:

    Use the real errors export from playwright — no mock needed.

    line 66
  8. Very specific:

    After the ts-proto migration, generated protobuf types are plain interfaces with direct property access — no getter methods.

    line 97
  9. Very specific:

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

    line 107
  10. Very specific:

    Use toBe for primitives and reference identity checks.

    line 114
  11. Very specific:

    When testing rejected promises, call expect.assertions(n) at the top of the test.

    line 118
  12. Very specific:

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

    line 146
  13. Distinctive:

    Do not add comments or docstrings explaining what the test does — the test name and code should be self-explanatory.

    line 179
  14. Distinctive:

    Do not assert on log messages — they are implementation details.

    line 181
  15. Very specific:

    Do not share mutable mock state between tests.

    line 182

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

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.ts → getters.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.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.