npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

@ng-spark/signal-store-testing

v1.0.2

Published

Type-safe testing utilities for NgRx Signal Store

Readme

@ng-spark/signal-store-testing

A type-safe testing utility library for NgRx Signal Store. Test your Signal Stores with ease using intuitive APIs for state management, computed signals, and method calls.

npm version License: MIT

Features

State Management - Read and manipulate Signal Store state with ease ✅ Computed Signals - Test computed signal values ✅ Method Calls - Invoke store methods in tests ✅ Async Waiting - Wait for state or computed signal conditions ✅ State History - Record and navigate state changes (time travel debugging) ✅ Type-Safe - Full TypeScript support with automatic type inference ✅ Jest & Vitest Support - Works with both Jest and Vitest testing frameworks

Installation

npm install @ng-spark/signal-store-testing --save-dev

Requirements

  • Angular 19+ or 20+
  • NgRx Signals 19+ or 20+
  • Testing Framework: Jest 29+ OR Vitest 1.0+
  • TypeScript 5.9+

Quick Start

import { TestBed } from '@angular/core/testing';
import { createSignalStoreTester } from '@ng-spark/signal-store-testing';
import { MyStore } from './my.store';

describe('MyStore', () => {
  let tester: ReturnType<typeof createSignalStoreTester<InstanceType<typeof MyStore>>>;

  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [MyStore],
    });

    const store = TestBed.inject(MyStore);
    tester = createSignalStoreTester(store);
  });

  it('should manage state', () => {
    // Read state
    expect(tester.state.count).toBe(0);

    // Update state
    tester.patchState({ count: 5 });
    expect(tester.state.count).toBe(5);

    // Assert state
    tester.expectState({ count: 5 });
  });

  it('should test computed signals', () => {
    tester.patchState({ count: 10 });
    tester.expectComputed('doubleCount', 20);
  });

  it('should call methods', () => {
    tester.callMethod('increment');
    expect(tester.state.count).toBe(1);
  });

  it('should wait for async conditions', async () => {
    setTimeout(() => tester.patchState({ count: 42 }), 100);

    await tester.waitForState({ count: 42 });
    expect(tester.state.count).toBe(42);
  });
});

API Reference

Creating a Tester

createSignalStoreTester(store, options?)

Creates a tester instance for a Signal Store.

Parameters:

  • store: TStore - The Signal Store instance to test
  • options?: StoreTesterOptions - Optional configuration
    • autoDetectChanges?: boolean - Auto-detect state changes (default: true)
    • recordHistory?: boolean - Enable state history recording (default: false)
    • errorMessages?: object - Custom error messages

Returns: SignalStoreTester<TStore>

Example:

const store = TestBed.inject(CounterStore);
const tester = createSignalStoreTester(store, {
  recordHistory: true
});

State Management

tester.state

Read-only property that returns the current state of all signals.

Returns: Object containing all signal values (both state and computed)

Example:

expect(tester.state.count).toBe(0);
expect(tester.state.incrementBy).toBe(1);

tester.setState(state)

Replace the entire state with new values.

Parameters:

  • state: Partial<StoreState<TStore>> - New state values

Example:

tester.setState({ count: 10, incrementBy: 2 });

tester.patchState(partial)

Merge partial state update with current state.

Parameters:

  • partial: Partial<StoreState<TStore>> - Partial state to merge

Example:

tester.patchState({ count: 5 }); // Only updates count

tester.expectState(expected)

Assert that the state matches the expected values using Jest's toMatchObject.

Parameters:

  • expected: Partial<StoreState<TStore>> - Expected state values

Example:

tester.expectState({ count: 42, label: 'Test' });

tester.expectStateToContain(expected)

Assert that the state contains the expected fields using Jest's objectContaining.

Parameters:

  • expected: Partial<StoreState<TStore>> - Expected state values

Example:

// Only checks count, ignores other fields
tester.expectStateToContain({ count: 42 });

Computed Signals

tester.getComputed(name)

Get the current value of a computed signal.

Parameters:

  • name: ComputedSignalNames<TStore> - Name of the computed signal

Returns: The unwrapped signal value

Example:

const doubleCount = tester.getComputed('doubleCount');
expect(doubleCount).toBe(20);

tester.expectComputed(name, expected)

Assert that a computed signal has the expected value.

Parameters:

  • name: ComputedSignalNames<TStore> - Name of the computed signal
  • expected: T - Expected value

Example:

tester.expectComputed('doubleCount', 20);
tester.expectComputed('isPositive', true);

Method Calls

tester.callMethod(name, ...args)

Call a store method with optional arguments.

Parameters:

  • name: MethodNames<TStore> - Name of the method
  • ...args - Method arguments (type-safe)

Returns: The method's return value

Example:

tester.callMethod('increment');
tester.callMethod('setCount', 42);
const result = tester.callMethod('addValue', 10);

Async Operations

tester.waitForState(condition, options?)

Wait for the state to match a condition.

Parameters:

  • condition: Partial<StoreState<TStore>> | WaitCondition<StoreState<TStore>> - State to wait for or condition function
  • options?: WaitOptions - Wait options
    • timeout?: number - Timeout in ms (default: 5000)
    • interval?: number - Polling interval in ms (default: 50)
    • timeoutMessage?: string - Custom timeout error message

Returns: Promise<void>

Examples:

// Wait for specific state
await tester.waitForState({ count: 42 });

// Wait using condition function
await tester.waitForState(state => state.count > 100);

// With custom timeout
await tester.waitForState({ loading: false }, { timeout: 10000 });

tester.waitForComputed(name, condition, options?)

Wait for a computed signal to match a condition.

Parameters:

  • name: ComputedSignalNames<TStore> - Name of the computed signal
  • condition: T | WaitCondition<T> - Value to wait for or condition function
  • options?: WaitOptions - Wait options

Returns: Promise<void>

Examples:

// Wait for specific value
await tester.waitForComputed('doubleCount', 20);

// Wait using condition function
await tester.waitForComputed('doubleCount', value => value > 25);

State History (Time Travel)

tester.startRecording()

Start recording state history for time travel debugging.

Returns: StateHistory<StoreState<TStore>>

Example:

const history = tester.startRecording();

tester.stopRecording()

Stop recording state history.

Example:

tester.stopRecording();

tester.getHistory()

Get the current state history (if recording).

Returns: StateHistory<StoreState<TStore>> | null

Example:

const history = tester.getHistory();
if (history) {
  console.log(history.states.length);
}

StateHistory API

When recording is enabled, you get a StateHistory object with these methods:

history.states

Read-only array of all recorded state entries.

Type: ReadonlyArray<StateHistoryEntry<TState>>

history.currentIndex

Current position in the history.

Type: number

history.currentState

The state at the current history index.

Type: TState

history.goBack()

Navigate to the previous state in history.

Example:

history.goBack();
expect(history.currentState.count).toBe(5);

history.goForward()

Navigate to the next state in history.

Example:

history.goForward();
expect(history.currentState.count).toBe(10);

history.goToIndex(index)

Jump to a specific state by index.

Parameters:

  • index: number - Target index

Example:

history.goToIndex(0); // Go to initial state

history.reset()

Reset to the initial state (index 0).

Example:

history.reset();
expect(history.currentIndex).toBe(0);

history.clear()

Clear all history except the initial state.

Example:

history.clear();
expect(history.states.length).toBe(1);

Complete Example

import { TestBed } from '@angular/core/testing';
import { createSignalStoreTester } from '@ng-spark/signal-store-testing';
import { signalStore, withState, withComputed, withMethods, patchState } from '@ngrx/signals';
import { computed } from '@angular/core';

// Define your store
const CounterStore = signalStore(
  { providedIn: 'root' },
  withState({ count: 0, incrementBy: 1 }),
  withComputed(({ count }) => ({
    doubleCount: computed(() => count() * 2),
    isEven: computed(() => count() % 2 === 0),
  })),
  withMethods((store) => ({
    increment(): void {
      patchState(store, (state) => ({ count: state.count + state.incrementBy }));
    },
    setCount(value: number): void {
      patchState(store, { count: value });
    },
  }))
);

// Test it
describe('CounterStore', () => {
  let tester: ReturnType<typeof createSignalStoreTester<InstanceType<typeof CounterStore>>>;

  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [CounterStore],
    });

    const store = TestBed.inject(CounterStore);
    tester = createSignalStoreTester(store);
  });

  it('should test complete workflow', async () => {
    // Initial state
    expect(tester.state.count).toBe(0);
    tester.expectComputed('doubleCount', 0);
    tester.expectComputed('isEven', true);

    // Update state
    tester.patchState({ count: 5 });
    tester.expectState({ count: 5 });
    tester.expectComputed('isEven', false);

    // Call methods
    tester.callMethod('increment');
    expect(tester.state.count).toBe(6);

    // Wait for async
    setTimeout(() => tester.callMethod('setCount', 100), 100);
    await tester.waitForState({ count: 100 });

    // Time travel
    const history = tester.startRecording();
    tester.patchState({ count: 10 });
    tester.patchState({ count: 20 });

    history.goBack();
    expect(history.currentState.count).toBe(10);
  });
});

TypeScript Support

The library is fully typed and provides excellent type inference:

// Automatic type inference for store
const tester = createSignalStoreTester(store);

// State is typed
tester.state.count; // ✅ Type: number
tester.state.unknown; // ❌ TypeScript error

// Method calls are type-safe
tester.callMethod('setCount', 42); // ✅ Correct
tester.callMethod('setCount', 'invalid'); // ❌ TypeScript error
tester.callMethod('unknownMethod'); // ❌ TypeScript error

// Computed signals are type-safe
tester.getComputed('doubleCount'); // ✅ Returns: number
tester.expectComputed('isEven', true); // ✅ Type-safe

Testing Framework Support

This library works with both Jest and Vitest. The API is identical for both frameworks.

Jest Setup

// jest.config.ts
export default {
  preset: 'jest-preset-angular',
  setupFilesAfterEnv: ['<rootDir>/setup-jest.ts'],
};

Vitest Setup

// vitest.config.ts
import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    globals: true,
    environment: 'jsdom',
    setupFiles: ['./setup-vitest.ts'],
  },
});

Both frameworks provide the expect API that this library uses for assertions.

Best Practices

  1. Type Your Tester Variable

    let tester: ReturnType<typeof createSignalStoreTester<InstanceType<typeof MyStore>>>;
  2. Use Specific Assertions

    • Use expectState for partial state matching
    • Use expectStateToContain for loose matching
    • Use direct property access for single values
  3. Enable History Recording for Complex Tests

    const tester = createSignalStoreTester(store, { recordHistory: true });
  4. Use Async Waiting for Side Effects

    await tester.waitForState(state => state.loading === false);

License

MIT

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

Support

If you encounter any issues or have questions, please file an issue on the GitHub repository.


Made with ❤️ by @ng-spark