Перейти к содержимому
GitHubXDiscord

@triggery/testing

Утилиты для тестирования Triggery. Без рантайм-зависимостей, framework-agnostic — одинаково работает в тестах React, Solid, Vue, в Node или воркере; совместимо с Vitest, Jest и node:test.

Тот же код триггера, который ты отправляешь в прод, выполняется в тестах без React, JSDOM или хост-фреймворка. Ты тестируешь сценарии, а не компоненты.

npm bundle

pnpm add -D @triggery/core @triggery/testing

Peer-зависимости: @triggery/core. Test-runner agnostic — никакой зависимости от vi.useFakeTimers() / jest.useFakeTimers().

ЭкспортНазначение
createTestRuntime({ triggers? })Изолированный рантайм на каждый тест. Без загрязнения глобального состояния.
mockCondition(trigger, name, value | getter)Подсунуть условие без рендера компонента.
mockAction(trigger, name, fn)Зарегистрировать обработчик действия — обычно vi.fn() / jest.fn().
flushMicrotasks()Опустошить дефолтный микротаск-шедулер перед проверкой.
createFakeScheduler()Управляемые виртуальные часы для actions.debounce / throttle / defer.

createFakeScheduler() предоставляет:

  • install() / uninstall() — подменяют globalThis.setTimeout / clearTimeout контролируемой реализацией.
  • advance(ms) — выполняет все таймеры, попадающие в окно, и опустошает микротаски.
  • flushAll() — выполняет все ожидающие таймеры независимо от запланированного времени.

Триггер, который проверяет условие user.isMod и выполняет два действия:

message.test.ts
import { createTrigger } from '@triggery/core';
import { createTestRuntime } from '@triggery/testing';
import { expect, test, vi } from 'vitest';

test('mod with notifications shows toast and plays sound', () => {
  const rt = createTestRuntime();

  const t = createTrigger<{
    events:     { 'new-message': string };
    conditions: { user: { isMod: boolean } };
    actions:    { showToast: string; playSound: 'beep' | 'mod-alert' };
  }>(
    {
      id: 'msg',
      events: ['new-message'],
      required: ['user'],
      handler: ({ event, conditions, actions, check }) => {
        if (!check.is('user', (u) => u.isMod)) return;
        actions.showToast?.(event.payload);
        actions.playSound?.('mod-alert');
      },
    },
    rt,
  );

  rt.mockCondition(t, 'user', { isMod: true });
  const showToast = vi.fn();
  const playSound = vi.fn();
  rt.mockAction(t, 'showToast', showToast);
  rt.mockAction(t, 'playSound', playSound);

  rt.fireSync('new-message', 'hi');

  expect(showToast).toHaveBeenCalledWith('hi');
  expect(playSound).toHaveBeenCalledWith('mod-alert');
});

Без DOM, без провайдера, без act(). Триггер прогоняется напрямую.

import { createTestRuntime, createFakeScheduler } from '@triggery/testing';
import { afterEach, beforeEach, expect, test, vi } from 'vitest';

let clock: ReturnType<typeof createFakeScheduler>;
beforeEach(() => { clock = createFakeScheduler(); clock.install(); });
afterEach(()  => { clock.uninstall(); });

test('actions.debounce(200) collapses bursts', () => {
  const rt = createTestRuntime();
  const search = vi.fn();
  rt.mockAction(searchTrigger, 'search', search);

  rt.fireSync('input', 'a');
  rt.fireSync('input', 'ab');
  rt.fireSync('input', 'abc');

  clock.advance(199);
  expect(search).not.toHaveBeenCalled();

  clock.advance(1);            // total = 200 ms
  expect(search).toHaveBeenCalledTimes(1);
  expect(search).toHaveBeenCalledWith('abc');
});
test('take-latest cancels stale runs', async () => {
  const rt = createTestRuntime();
  rt.fire('search', 'a');      // gets cancelled
  rt.fire('search', 'ab');     // gets cancelled
  await rt.fire('search', 'abc'); // wins

  expect(rt.inspect()?.outcome).toBe('completed');
});