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.
In this repository7 other files it gives its agents.
- AGENTS.mdAGENTS.md
- CLAUDE.mdCLAUDE.md
- change-keywordSkill
- ci-failure-triageSkill
- parse-rf-resultsSkill
- write-python-unit-testsSkill
- write-robot-testsSkill
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
- Distinctive: line 12
Run tests with invoke:
- Very specific: line 25
Every test file imports all Jest globals it uses from @jest/globals.
- Very specific: line 31
Do not add a /// <reference types="jest" /> directive — it is redundant when importing from @jest/globals and inconsistent with the rest of …
- Very specific: line 33
Keep using the global jest object for jest.mock(), jest.fn(), and jest.mocked(...).
- Very specific: line 34
Avoid importing jest from @jest/globals in these tests, because it can lead to worse mock typing in this repo.
- Very specific: line 38
Use jest.mock() at the top of the file (before imports) to replace modules with controlled fakes.
- Very specific: line 66
Use the real errors export from playwright — no mock needed.
- Very specific: line 97
After the ts-proto migration, generated protobuf types are plain interfaces with direct property access — no getter methods.
- Very specific: line 107
Use toHaveBeenCalledWith and toHaveBeenCalledTimes to verify how mocks were called — this is often the primary assertion in the wrapper …
- Very specific: line 114
Use toBe for primitives and reference identity checks.
- Very specific: line 118
When testing rejected promises, call expect.assertions(n) at the top of the test.
- Very specific: line 146
When a function calls elementHandle.evaluate() multiple times, use mockResolvedValueOnce chaining:
- Distinctive: line 179
Do not add comments or docstrings explaining what the test does — the test name and code should be self-explanatory.
- Distinctive: line 181
Do not assert on log messages — they are implementation details.
- Very specific: line 182
Do not share mutable mock state between tests.
≈ “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.
Files
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.htmlFile 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()inbeforeEach.
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
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.

