-
Identify the unit under test and its category. Read the target file and classify it as one of: zustand store (src/renderer/stores/*.ts), pure utility (src/renderer/utils/*.ts), or React component (src/renderer/components/*.tsx). The category determines the test scaffold below. Verify the file exists with ls <path> before proceeding.
-
Read the reference test to mirror style. Open tests/unit/api.test.ts and copy its structure: top-level describe, nested describe per logical surface, beforeEach that clears the three known localStorage keys, and it('...', () => { ... }) blocks using expect(...).toBe(...) / .toEqual(...). This step's output (the imports and beforeEach shape) is reused in step 4.
-
Confirm the test setup file and config. Open src/renderer/test-setup.ts to confirm which globals it installs (at minimum a localStorage mock). Open vitest.config.ts and confirm test.environment === 'jsdom', test.setupFiles includes the renderer setup, and test.include matches tests/**/*.test.{ts,tsx}. If any of these are missing, STOP and tell the user the project setup needs fixing — do not paper over it inside the new test.
-
Create the test file at tests/unit/<name>.test.ts (or .test.tsx for components). Use the scaffold for the matched category from step 1:
Store scaffold (zustand):
import { describe, it, expect, beforeEach } from 'vitest';
import { use<Name>Store } from '../../src/renderer/stores/<name>Store';
describe('<name>Store', () => {
beforeEach(() => {
localStorage.removeItem('auth_token');
localStorage.removeItem('session_id');
localStorage.removeItem('device_id');
use<Name>Store.setState(use<Name>Store.getInitialState());
});
it('initializes with expected defaults', () => {
const state = use<Name>Store.getState();
expect(state.<field>).toBe(<default>);
});
});
Utility scaffold:
import { describe, it, expect, beforeEach } from 'vitest';
import { <fn> } from '../../src/renderer/utils/<name>';
describe('<fn>', () => {
beforeEach(() => {
localStorage.removeItem('auth_token');
localStorage.removeItem('session_id');
localStorage.removeItem('device_id');
});
it('<expected behavior>', () => {
expect(<fn>(<input>)).toEqual(<output>);
});
});
Component scaffold (.test.tsx):
import { describe, it, expect, beforeEach } from 'vitest';
import { render, screen, cleanup } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import { <Name> } from '../../src/renderer/components/<Name>';
describe('<Name>', () => {
beforeEach(() => {
cleanup();
localStorage.removeItem('auth_token');
localStorage.removeItem('session_id');
localStorage.removeItem('device_id');
});
it('renders the expected text', () => {
render(<MemoryRouter><<Name> /></MemoryRouter>);
expect(screen.getByText(/expected/i)).toBeInTheDocument();
});
});
Only add the MemoryRouter wrapper if the component uses react-router-dom hooks/components. Only use @testing-library/react if it is already in package.json — if not, ask the user before adding it as a dev dependency.
-
Cover behavior, not implementation. Write at least one test per public method/exported function/visible UI affordance. For stores, exercise actions and assert on the resulting getState(). For utilities, use representative inputs plus one boundary case (empty string, zero, missing key). For components, assert on rendered text/roles via screen queries — do not snapshot the full DOM. This step uses the imports established in step 4.
-
For tests that touch axios, mock with vi.mock('axios') and import axios from the test file. Do NOT hit the real network. Mirror the auth-token-from-localStorage pattern used in tests/unit/api.test.ts rather than inventing a new mocking strategy.
-
Run the test in isolation with npm test -- tests/unit/<name>.test.ts and verify all it blocks pass. If any fail, debug the unit under test or the test — do not delete failing assertions. Verify the file shows up in the run output (proves the include glob matched) before proceeding.
-
Run the full suite with npm test and confirm no other tests regressed. If a previously passing test fails, the new test is leaking state — most likely an unreset localStorage key or a zustand store left in a non-default state. Add the missing reset to beforeEach.
-
ReferenceError: localStorage is not defined: The test is running under the node env. Confirm vitest.config.ts has test.environment: 'jsdom' and that you did NOT add a per-file // @vitest-environment node pragma. Also confirm setupFiles includes src/renderer/test-setup.ts.
-
Test file not picked up by npm test: The path doesn't match the include glob. Move the file under tests/ and ensure the filename ends in .test.ts or .test.tsx. Files in src/ are excluded.
-
Cannot find module '@testing-library/react': It isn't installed. Stop and ask the user — don't npm install without confirmation. The other option is to assert on store/util behavior instead of rendering the component.
-
Test passes alone, fails in full suite: State leak. Add the missing reset to beforeEach: clear all three localStorage keys, call use<Name>Store.setState(use<Name>Store.getInitialState()) for any zustand store touched, and call cleanup() in component tests.
-
TypeError: ... is not a function on a zustand store action in tests: You imported the store from a relative path that resolved to a duplicate copy via node_modules. Always import from ../../src/renderer/stores/<name>Store with that exact relative depth from tests/unit/.
-
axios call hits the network and times out: You forgot vi.mock('axios') at the top of the file (above any imports that reach axios). Place it as the first non-import statement and re-run.
-
act(...) warnings on React component tests: A state update fired outside of render. Wrap the user interaction in await with an async it and use await screen.findByText(...) for assertions that depend on async state.