diff --git a/.knip.jsonc b/.knip.jsonc index 69009ec21c..62b8481b8f 100644 --- a/.knip.jsonc +++ b/.knip.jsonc @@ -17,8 +17,6 @@ ], "ignoreBinaries": ["svelte-check", "docs:build"], "ignoreDependencies": [ - "@xstate-repo/jest-utils", - "@xstate-repo/jest-utils/setup", "synckit", // package.json#exports aren't added as entry points, because `dist/` is .gitignored "react", diff --git a/.vscode/launch.json b/.vscode/launch.json index 087b07217d..357841a28e 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -4,32 +4,24 @@ { "type": "node", "request": "launch", - "name": "Jest Current File", - "program": "${workspaceFolder}/node_modules/.bin/jest", - "args": ["${file}", "--config", "jest.config.js", "--no-cache"], + "name": "Vitest Current Test File", + "autoAttachChildProcesses": true, + "skipFiles": ["/**", "**/node_modules/**"], + "program": "${workspaceRoot}/node_modules/vitest/vitest.mjs", + "args": ["run", "${relativeFile}"], "console": "integratedTerminal", - "internalConsoleOptions": "neverOpen", - "windows": { - "program": "${workspaceFolder}/node_modules/jest/bin/jest" - } + "internalConsoleOptions": "neverOpen" }, { "type": "node", "request": "launch", - "name": "Jest Current File (no timeout)", - "program": "${workspaceFolder}/node_modules/.bin/jest", - "args": [ - "${file}", - "--config", - "jest.config.js", - "--no-cache", - "--testTimeout=2147483647" - ], + "name": "Vitest Current Test File (no timeout)", + "autoAttachChildProcesses": true, + "skipFiles": ["/**", "**/node_modules/**"], + "program": "${workspaceRoot}/node_modules/vitest/vitest.mjs", + "args": ["run", "${relativeFile}", "--testTimeout=2147483647"], "console": "integratedTerminal", - "internalConsoleOptions": "neverOpen", - "windows": { - "program": "${workspaceFolder}/node_modules/jest/bin/jest" - } + "internalConsoleOptions": "neverOpen" } ] } diff --git a/jest.config.js b/jest.config.js deleted file mode 100644 index daef5f79a9..0000000000 --- a/jest.config.js +++ /dev/null @@ -1,33 +0,0 @@ -const { constants } = require('jest-config'); - -/** @type {import('@jest/types').Config.InitialOptions} */ -module.exports = { - prettierPath: null, - setupFilesAfterEnv: ['@xstate-repo/jest-utils/setup'], - transform: { - [constants.DEFAULT_JS_PATTERN]: 'babel-jest', - '^.+\\.vue$': '@vue/vue3-jest', - '^.+\\.svelte$': [ - 'svelte-jester', - { - preprocess: true, - rootMode: 'upward' - } - ] - }, - resolver: '/scripts/jest-resolver.js', - globals: { - 'vue-jest': { - // weird way of disabling ts-jest-based transformer - transform: { - '^typescript$': 'babel-jest', - '^tsx?$': 'babel-jest' - } - } - }, - watchPlugins: [ - 'jest-watch-typeahead/filename', - 'jest-watch-typeahead/testname' - ], - testEnvironment: 'jsdom' -}; diff --git a/package.json b/package.json index c26ca60d8c..51812fbc86 100644 --- a/package.json +++ b/package.json @@ -28,8 +28,8 @@ "build": "preconstruct build", "fix": "manypkg fix", "typecheck": "tsc", - "test": "jest", - "test:core": "jest packages/core", + "test": "vitest", + "test:core": "vitest --project xstate", "changeset": "changeset", "release": "changeset publish", "version": "changeset version && node ./scripts/bump-peer-dep-ranges.js" @@ -52,33 +52,21 @@ "@babel/preset-typescript": "^7.23.3", "@changesets/changelog-github": "^0.4.8", "@changesets/cli": "^2.26.2", - "@jest/types": "^29.6.3", "@manypkg/cli": "^0.16.1", "@manypkg/get-packages": "^1.1.3", "@preconstruct/cli": "^2.8.1", - "@testing-library/vue": "^6.6.1", - "@types/jest": "^29.5.10", "@types/node": "^20.14.13", - "@vue/compiler-sfc": "^3.0.11", - "@vue/vue3-jest": "^29.2.6", - "babel-jest": "^29.7.0", "babel-preset-solid": "^1.8.4", + "happy-dom": "^14.12.3", "husky": "^3.1.0", - "jest": "^29.7.0", - "jest-config": "^29.7.0", - "jest-environment-jsdom": "^29.7.0", - "jest-resolve": "^29.7.0", - "jest-snapshot": "^29.7.0", - "jest-watch-typeahead": "^2.2.2", "knip": "^5.27.0", "lint-staged": "^8.2.1", "patch-package": "^6.5.1", "prettier": "^3.1.0", "prettier-plugin-jsdoc": "^1.3.0", - "svelte-jester": "^2.3.2", "synckit": "^0.8.5", "typescript": "^5.5.4", - "vue": "^3.0.11" + "vitest": "^2.0.3" }, "husky": { "hooks": { diff --git a/packages/core/package.json b/packages/core/package.json index df456a4e06..80c346f2a0 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -119,7 +119,6 @@ "homepage": "https://github.com/statelyai/xstate/tree/main/packages/core#readme", "devDependencies": { "@scion-scxml/test-framework": "^2.0.15", - "@xstate-repo/jest-utils": "*", "ajv": "^8.12.0", "pkg-up": "^3.1.0", "rxjs": "^7.8.0", diff --git a/packages/core/test/actions.test.ts b/packages/core/test/actions.test.ts index 389404bfeb..186e5dcbf5 100644 --- a/packages/core/test/actions.test.ts +++ b/packages/core/test/actions.test.ts @@ -1,4 +1,4 @@ -import { sleep } from '@xstate-repo/jest-utils'; +import { setTimeout as sleep } from 'node:timers/promises'; import { cancel, emit, @@ -421,9 +421,9 @@ describe('entry/exit actions', () => { }); it('should work with function actions', () => { - const entrySpy = jest.fn(); - const exitSpy = jest.fn(); - const transitionSpy = jest.fn(); + const entrySpy = vi.fn(); + const exitSpy = vi.fn(); + const transitionSpy = vi.fn(); const machine = createMachine({ initial: 'a', @@ -552,7 +552,7 @@ describe('entry/exit actions', () => { }); it("shouldn't use a referenced custom action over a builtin one when there is a naming conflict", () => { - const spy = jest.fn(); + const spy = vi.fn(); const machine = createMachine( { context: { @@ -579,7 +579,7 @@ describe('entry/exit actions', () => { }); it("shouldn't use a referenced custom action over an inline one when there is a naming conflict", () => { - const spy = jest.fn(); + const spy = vi.fn(); let called = false; const machine = createMachine( @@ -608,8 +608,8 @@ describe('entry/exit actions', () => { }); it('root entry/exit actions should be called on root reentering transitions', () => { - let entrySpy = jest.fn(); - let exitSpy = jest.fn(); + let entrySpy = vi.fn(); + let exitSpy = vi.fn(); const machine = createMachine({ id: 'root', @@ -1386,85 +1386,87 @@ describe('entry/exit actions', () => { expect(flushTracked()).toEqual([]); }); - it("shouldn't exit (and reenter) state on targetless delayed transition", (done) => { - const machine = createMachine({ - initial: 'one', - states: { - one: { - after: { - 10: { - actions: () => { - // do smth + it("shouldn't exit (and reenter) state on targetless delayed transition", () => + new Promise((resolve) => { + const machine = createMachine({ + initial: 'one', + states: { + one: { + after: { + 10: { + actions: () => { + // do smth + } } } } } - } - }); + }); - const flushTracked = trackEntries(machine); + const flushTracked = trackEntries(machine); - createActor(machine).start(); - flushTracked(); + createActor(machine).start(); + flushTracked(); - setTimeout(() => { - expect(flushTracked()).toEqual([]); - done(); - }, 50); - }); + setTimeout(() => { + expect(flushTracked()).toEqual([]); + resolve(); + }, 50); + })); }); describe('when reaching a final state', () => { // https://github.com/statelyai/xstate/issues/1109 - it('exit actions should be called when invoked machine reaches its final state', (done) => { - let exitCalled = false; - let childExitCalled = false; - const childMachine = createMachine({ - exit: () => { - exitCalled = true; - }, - initial: 'a', - states: { - a: { - type: 'final', - exit: () => { - childExitCalled = true; + it('exit actions should be called when invoked machine reaches its final state', () => + new Promise((resolve) => { + let exitCalled = false; + let childExitCalled = false; + const childMachine = createMachine({ + exit: () => { + exitCalled = true; + }, + initial: 'a', + states: { + a: { + type: 'final', + exit: () => { + childExitCalled = true; + } } } - } - }); + }); - const parentMachine = createMachine({ - initial: 'active', - states: { - active: { - invoke: { - src: childMachine, - onDone: 'finished' + const parentMachine = createMachine({ + initial: 'active', + states: { + active: { + invoke: { + src: childMachine, + onDone: 'finished' + } + }, + finished: { + type: 'final' } - }, - finished: { - type: 'final' } - } - }); + }); - const actor = createActor(parentMachine); - actor.subscribe({ - complete: () => { - expect(exitCalled).toBeTruthy(); - expect(childExitCalled).toBeTruthy(); - done(); - } - }); - actor.start(); - }); + const actor = createActor(parentMachine); + actor.subscribe({ + complete: () => { + expect(exitCalled).toBeTruthy(); + expect(childExitCalled).toBeTruthy(); + resolve(); + } + }); + actor.start(); + })); }); describe('when stopped', () => { it('exit actions should not be called when stopping a machine', () => { - const rootSpy = jest.fn(); - const childSpy = jest.fn(); + const rootSpy = vi.fn(); + const childSpy = vi.fn(); const machine = createMachine({ exit: rootSpy, @@ -1621,7 +1623,7 @@ describe('entry/exit actions', () => { }); it('sent events from exit handlers of a stopped child should not be received by its children', () => { - const spy = jest.fn(); + const spy = vi.fn(); const grandchild = createMachine({ id: 'grandchild', @@ -1664,7 +1666,7 @@ describe('entry/exit actions', () => { }); it('sent events from exit handlers of a done child should be received by its children', () => { - const spy = jest.fn(); + const spy = vi.fn(); const grandchild = createMachine({ id: 'grandchild', @@ -1735,7 +1737,7 @@ describe('entry/exit actions', () => { }); it('should note execute referenced custom actions correctly when stopping an interpreter', () => { - const spy = jest.fn(); + const spy = vi.fn(); const parent = createMachine( { id: 'parent', @@ -1946,7 +1948,7 @@ describe('initial actions', () => { }); it('should execute actions of initial transitions only once when taking an explicit transition', () => { - const spy = jest.fn(); + const spy = vi.fn(); const machine = createMachine({ initial: 'a', states: { @@ -1994,7 +1996,7 @@ describe('initial actions', () => { }); it('should execute actions of all initial transitions resolving to the initial state value', () => { - const spy = jest.fn(); + const spy = vi.fn(); const machine = createMachine({ initial: { target: 'a', @@ -2028,7 +2030,7 @@ describe('initial actions', () => { }); it('should execute actions of the initial transition when taking a root reentering self-transition', () => { - const spy = jest.fn(); + const spy = vi.fn(); const machine = createMachine({ id: 'root', initial: { @@ -2065,7 +2067,7 @@ describe('initial actions', () => { describe('actions on invalid transition', () => { it('should not recall previous actions', () => { - const spy = jest.fn(); + const spy = vi.fn(); const machine = createMachine({ initial: 'idle', states: { @@ -2103,7 +2105,7 @@ describe('actions config', () => { const definedAction = () => {}; it('should reference actions defined in actions parameter of machine options (entry actions)', () => { - const spy = jest.fn(); + const spy = vi.fn(); const machine = createMachine({ initial: 'a', states: { @@ -2132,7 +2134,7 @@ describe('actions config', () => { }); it('should reference actions defined in actions parameter of machine options (initial state)', () => { - const spy = jest.fn(); + const spy = vi.fn(); const machine = createMachine( { entry: ['definedAction', { type: 'definedAction' }, 'undefinedAction'] @@ -2234,7 +2236,7 @@ describe('actions config', () => { describe('action meta', () => { it('should provide the original params', () => { - const spy = jest.fn(); + const spy = vi.fn(); const testMachine = createMachine( { @@ -2268,7 +2270,7 @@ describe('action meta', () => { }); it('should provide undefined params when it was configured as string', () => { - const spy = jest.fn(); + const spy = vi.fn(); const testMachine = createMachine( { @@ -2295,7 +2297,7 @@ describe('action meta', () => { }); it('should provide the action with resolved params when they are dynamic', () => { - const spy = jest.fn(); + const spy = vi.fn(); const machine = createMachine( { @@ -2321,7 +2323,7 @@ describe('action meta', () => { }); it('should resolve dynamic params using context value', () => { - const spy = jest.fn(); + const spy = vi.fn(); const machine = createMachine( { @@ -2350,7 +2352,7 @@ describe('action meta', () => { }); it('should resolve dynamic params using event value', () => { - const spy = jest.fn(); + const spy = vi.fn(); const machine = createMachine( { @@ -2383,120 +2385,122 @@ describe('action meta', () => { }); describe('forwardTo()', () => { - it('should forward an event to a service', (done) => { - const child = createMachine({ - types: {} as { - events: { - type: 'EVENT'; - value: number; - }; - }, - id: 'child', - initial: 'active', - states: { - active: { - on: { - EVENT: { - actions: sendParent({ type: 'SUCCESS' }), - guard: ({ event }) => event.value === 42 + it('should forward an event to a service', () => + new Promise((resolve) => { + const child = createMachine({ + types: {} as { + events: { + type: 'EVENT'; + value: number; + }; + }, + id: 'child', + initial: 'active', + states: { + active: { + on: { + EVENT: { + actions: sendParent({ type: 'SUCCESS' }), + guard: ({ event }) => event.value === 42 + } } } } - } - }); + }); - const parent = createMachine({ - types: {} as { - events: - | { - type: 'EVENT'; - value: number; - } - | { - type: 'SUCCESS'; - }; - }, - id: 'parent', - initial: 'first', - states: { - first: { - invoke: { src: child, id: 'myChild' }, - on: { - EVENT: { - actions: forwardTo('myChild') - }, - SUCCESS: 'last' - } + const parent = createMachine({ + types: {} as { + events: + | { + type: 'EVENT'; + value: number; + } + | { + type: 'SUCCESS'; + }; }, - last: { - type: 'final' + id: 'parent', + initial: 'first', + states: { + first: { + invoke: { src: child, id: 'myChild' }, + on: { + EVENT: { + actions: forwardTo('myChild') + }, + SUCCESS: 'last' + } + }, + last: { + type: 'final' + } } - } - }); + }); - const service = createActor(parent); - service.subscribe({ complete: () => done() }); - service.start(); + const service = createActor(parent); + service.subscribe({ complete: () => resolve() }); + service.start(); - service.send({ type: 'EVENT', value: 42 }); - }); + service.send({ type: 'EVENT', value: 42 }); + })); - it('should forward an event to a service (dynamic)', (done) => { - const child = createMachine({ - types: {} as { - events: { - type: 'EVENT'; - value: number; - }; - }, - id: 'child', - initial: 'active', - states: { - active: { - on: { - EVENT: { - actions: sendParent({ type: 'SUCCESS' }), - guard: ({ event }) => event.value === 42 + it('should forward an event to a service (dynamic)', () => + new Promise((resolve) => { + const child = createMachine({ + types: {} as { + events: { + type: 'EVENT'; + value: number; + }; + }, + id: 'child', + initial: 'active', + states: { + active: { + on: { + EVENT: { + actions: sendParent({ type: 'SUCCESS' }), + guard: ({ event }) => event.value === 42 + } } } } - } - }); + }); - const parent = createMachine({ - types: {} as { - context: { child?: AnyActorRef }; - events: { type: 'EVENT'; value: number } | { type: 'SUCCESS' }; - }, - id: 'parent', - initial: 'first', - context: { - child: undefined - }, - states: { - first: { - entry: assign({ - child: ({ spawn }) => spawn(child, { id: 'x' }) - }), - on: { - EVENT: { - actions: forwardTo(({ context }) => context.child!) - }, - SUCCESS: 'last' - } + const parent = createMachine({ + types: {} as { + context: { child?: AnyActorRef }; + events: { type: 'EVENT'; value: number } | { type: 'SUCCESS' }; }, - last: { - type: 'final' + id: 'parent', + initial: 'first', + context: { + child: undefined + }, + states: { + first: { + entry: assign({ + child: ({ spawn }) => spawn(child, { id: 'x' }) + }), + on: { + EVENT: { + actions: forwardTo(({ context }) => context.child!) + }, + SUCCESS: 'last' + } + }, + last: { + type: 'final' + } } - } - }); + }); - const service = createActor(parent); - service.subscribe({ complete: () => done() }); - service.start(); + const service = createActor(parent); + service.subscribe({ complete: () => resolve() }); + service.start(); - service.send({ type: 'EVENT', value: 42 }); - }); + service.send({ type: 'EVENT', value: 42 }); + })); it('should not cause an infinite loop when forwarding to undefined', () => { const machine = createMachine({ @@ -2505,7 +2509,7 @@ describe('forwardTo()', () => { } }); - const errorSpy = jest.fn(); + const errorSpy = vi.fn(); const actorRef = createActor(machine); actorRef.subscribe({ @@ -2514,7 +2518,7 @@ describe('forwardTo()', () => { actorRef.start(); actorRef.send({ type: 'TEST' }); - expect(errorSpy).toMatchMockCallsInlineSnapshot(` + expect(errorSpy.mock.calls).toMatchInlineSnapshot(` [ [ [Error: Attempted to forward event to undefined actor. This risks an infinite loop in the sender.], @@ -2526,7 +2530,7 @@ describe('forwardTo()', () => { describe('log()', () => { it('should log a string', () => { - const consoleSpy = jest.fn(); + const consoleSpy = vi.fn(); console.log = consoleSpy; const machine = createMachine({ entry: log('some string', 'string label') @@ -2544,7 +2548,7 @@ describe('log()', () => { }); it('should log an expression', () => { - const consoleSpy = jest.fn(); + const consoleSpy = vi.fn(); console.log = consoleSpy; const machine = createMachine({ context: { @@ -2567,7 +2571,7 @@ describe('log()', () => { describe('enqueueActions', () => { it('should execute a simple referenced action', () => { - const spy = jest.fn(); + const spy = vi.fn(); const machine = createMachine( { @@ -2588,8 +2592,8 @@ describe('enqueueActions', () => { }); it('should execute multiple different referenced actions', () => { - const spy1 = jest.fn(); - const spy2 = jest.fn(); + const spy1 = vi.fn(); + const spy2 = vi.fn(); const machine = createMachine( { @@ -2613,7 +2617,7 @@ describe('enqueueActions', () => { }); it('should execute multiple same referenced actions', () => { - const spy = jest.fn(); + const spy = vi.fn(); const machine = createMachine( { @@ -2635,7 +2639,7 @@ describe('enqueueActions', () => { }); it('should execute a parameterized action', () => { - const spy = jest.fn(); + const spy = vi.fn(); const machine = createMachine( { @@ -2655,7 +2659,7 @@ describe('enqueueActions', () => { createActor(machine).start(); - expect(spy).toMatchMockCallsInlineSnapshot(` + expect(spy.mock.calls).toMatchInlineSnapshot(` [ [ { @@ -2667,7 +2671,7 @@ describe('enqueueActions', () => { }); it('should execute a function', () => { - const spy = jest.fn(); + const spy = vi.fn(); const machine = createMachine({ entry: enqueueActions(({ enqueue }) => { @@ -2681,7 +2685,7 @@ describe('enqueueActions', () => { }); it('should execute a builtin action using its own action creator', () => { - const spy = jest.fn(); + const spy = vi.fn(); const machine = createMachine({ on: { @@ -2708,7 +2712,7 @@ describe('enqueueActions', () => { }); it('should execute a builtin action using its bound action creator', () => { - const spy = jest.fn(); + const spy = vi.fn(); const machine = createMachine({ on: { @@ -2750,7 +2754,7 @@ describe('enqueueActions', () => { }); it('should be able to check a simple referenced guard', () => { - const spy = jest.fn().mockImplementation(() => true); + const spy = vi.fn().mockImplementation(() => true); const machine = createMachine( { context: { @@ -2773,7 +2777,7 @@ describe('enqueueActions', () => { }); it('should be able to check a parameterized guard', () => { - const spy = jest.fn(); + const spy = vi.fn(); const machine = createMachine( { @@ -2801,7 +2805,7 @@ describe('enqueueActions', () => { createActor(machine); - expect(spy).toMatchMockCallsInlineSnapshot(` + expect(spy.mock.calls).toMatchInlineSnapshot(` [ [ { @@ -2859,7 +2863,7 @@ describe('enqueueActions', () => { } }); - const spy = jest.fn(); + const spy = vi.fn(); const parentMachine = setup({ types: {} as { events: ParentEvent }, @@ -2905,7 +2909,7 @@ describe('enqueueActions', () => { entry: 'sendToParent' }); - const parentSpy = jest.fn(); + const parentSpy = vi.fn(); const parentMachine = setup({ types: {} as { events: ParentEvent }, @@ -2955,76 +2959,78 @@ describe('sendParent', () => { }); describe('sendTo', () => { - it('should be able to send an event to an actor', (done) => { - const childMachine = createMachine({ - types: {} as { - events: { type: 'EVENT' }; - }, - initial: 'waiting', - states: { - waiting: { - on: { - EVENT: { - actions: () => done() + it('should be able to send an event to an actor', () => + new Promise((resolve) => { + const childMachine = createMachine({ + types: {} as { + events: { type: 'EVENT' }; + }, + initial: 'waiting', + states: { + waiting: { + on: { + EVENT: { + actions: () => resolve() + } } } } - } - }); + }); - const parentMachine = createMachine({ - types: {} as { - context: { - child: ActorRefFromLogic; - }; - }, - context: ({ spawn }) => ({ - child: spawn(childMachine) - }), - entry: sendTo(({ context }) => context.child, { type: 'EVENT' }) - }); + const parentMachine = createMachine({ + types: {} as { + context: { + child: ActorRefFromLogic; + }; + }, + context: ({ spawn }) => ({ + child: spawn(childMachine) + }), + entry: sendTo(({ context }) => context.child, { type: 'EVENT' }) + }); - createActor(parentMachine).start(); - }); + createActor(parentMachine).start(); + })); - it('should be able to send an event from expression to an actor', (done) => { - const childMachine = createMachine({ - types: {} as { - events: { type: 'EVENT'; count: number }; - }, - initial: 'waiting', - states: { - waiting: { - on: { - EVENT: { - actions: () => done() + it('should be able to send an event from expression to an actor', () => + new Promise((resolve) => { + const childMachine = createMachine({ + types: {} as { + events: { type: 'EVENT'; count: number }; + }, + initial: 'waiting', + states: { + waiting: { + on: { + EVENT: { + actions: () => resolve() + } } } } - } - }); + }); - const parentMachine = createMachine({ - types: {} as { - context: { - child: ActorRefFromLogic; - count: number; - }; - }, - context: ({ spawn }) => { - return { - child: spawn(childMachine, { id: 'child' }), - count: 42 - }; - }, - entry: sendTo( - ({ context }) => context.child, - ({ context }) => ({ type: 'EVENT', count: context.count }) - ) - }); + const parentMachine = createMachine({ + types: {} as { + context: { + child: ActorRefFromLogic; + count: number; + }; + }, + context: ({ spawn }) => { + return { + child: spawn(childMachine, { id: 'child' }), + count: 42 + }; + }, + entry: sendTo( + ({ context }) => context.child, + ({ context }) => ({ type: 'EVENT', count: context.count }) + ) + }); - createActor(parentMachine).start(); - }); + createActor(parentMachine).start(); + })); it('should report a type error for an invalid event', () => { const childMachine = createMachine({ @@ -3057,66 +3063,68 @@ describe('sendTo', () => { }); }); - it('should be able to send an event to a named actor', (done) => { - const childMachine = createMachine({ - types: {} as { - events: { type: 'EVENT' }; - }, - initial: 'waiting', - states: { - waiting: { - on: { - EVENT: { - actions: () => done() + it('should be able to send an event to a named actor', () => + new Promise((resolve) => { + const childMachine = createMachine({ + types: {} as { + events: { type: 'EVENT' }; + }, + initial: 'waiting', + states: { + waiting: { + on: { + EVENT: { + actions: () => resolve() + } } } } - } - }); + }); - const parentMachine = createMachine({ - types: {} as { - context: { child: ActorRefFromLogic }; - }, - context: ({ spawn }) => ({ - child: spawn(childMachine, { id: 'child' }) - }), - // No type-safety for the event yet - entry: sendTo('child', { type: 'EVENT' }) - }); + const parentMachine = createMachine({ + types: {} as { + context: { child: ActorRefFromLogic }; + }, + context: ({ spawn }) => ({ + child: spawn(childMachine, { id: 'child' }) + }), + // No type-safety for the event yet + entry: sendTo('child', { type: 'EVENT' }) + }); - createActor(parentMachine).start(); - }); + createActor(parentMachine).start(); + })); - it('should be able to send an event directly to an ActorRef', (done) => { - const childMachine = createMachine({ - types: {} as { - events: { type: 'EVENT' }; - }, - initial: 'waiting', - states: { - waiting: { - on: { - EVENT: { - actions: () => done() + it('should be able to send an event directly to an ActorRef', () => + new Promise((resolve) => { + const childMachine = createMachine({ + types: {} as { + events: { type: 'EVENT' }; + }, + initial: 'waiting', + states: { + waiting: { + on: { + EVENT: { + actions: () => resolve() + } } } } - } - }); + }); - const parentMachine = createMachine({ - types: {} as { - context: { child: ActorRefFromLogic }; - }, - context: ({ spawn }) => ({ - child: spawn(childMachine) - }), - entry: sendTo(({ context }) => context.child, { type: 'EVENT' }) - }); + const parentMachine = createMachine({ + types: {} as { + context: { child: ActorRefFromLogic }; + }, + context: ({ spawn }) => ({ + child: spawn(childMachine) + }), + entry: sendTo(({ context }) => context.child, { type: 'EVENT' }) + }); - createActor(parentMachine).start(); - }); + createActor(parentMachine).start(); + })); it('should be able to read from event', () => { expect.assertions(1); @@ -3162,7 +3170,7 @@ describe('sendTo', () => { entry: sendTo('child', 'a string') }); - const errorSpy = jest.fn(); + const errorSpy = vi.fn(); const actorRef = createActor(machine); actorRef.subscribe({ @@ -3170,7 +3178,7 @@ describe('sendTo', () => { }); actorRef.start(); - expect(errorSpy).toMatchMockCallsInlineSnapshot(` + expect(errorSpy.mock.calls).toMatchInlineSnapshot(` [ [ [Error: Only event objects may be used with sendTo; use sendTo({ type: "a string" }) instead], @@ -3181,70 +3189,72 @@ describe('sendTo', () => { }); describe('raise', () => { - it('should be able to send a delayed event to itself', (done) => { - const machine = createMachine({ - initial: 'a', - states: { - a: { - entry: raise( - { type: 'EVENT' }, - { - delay: 1 + it('should be able to send a delayed event to itself', () => + new Promise((resolve) => { + const machine = createMachine({ + initial: 'a', + states: { + a: { + entry: raise( + { type: 'EVENT' }, + { + delay: 1 + } + ), + on: { + TO_B: 'b' } - ), - on: { - TO_B: 'b' - } - }, - b: { - on: { - EVENT: 'c' + }, + b: { + on: { + EVENT: 'c' + } + }, + c: { + type: 'final' } - }, - c: { - type: 'final' } - } - }); + }); - const service = createActor(machine).start(); + const service = createActor(machine).start(); - service.subscribe({ complete: () => done() }); + service.subscribe({ complete: () => resolve() }); - // Ensures that the delayed self-event is sent when in the `b` state - service.send({ type: 'TO_B' }); - }); + // Ensures that the delayed self-event is sent when in the `b` state + service.send({ type: 'TO_B' }); + })); - it('should be able to send a delayed event to itself with delay = 0', (done) => { - const machine = createMachine({ - initial: 'a', - states: { - a: { - entry: raise( - { type: 'EVENT' }, - { - delay: 0 + it('should be able to send a delayed event to itself with delay = 0', () => + new Promise((resolve) => { + const machine = createMachine({ + initial: 'a', + states: { + a: { + entry: raise( + { type: 'EVENT' }, + { + delay: 0 + } + ), + on: { + EVENT: 'b' } - ), - on: { - EVENT: 'b' - } - }, - b: {} - } - }); + }, + b: {} + } + }); - const service = createActor(machine).start(); + const service = createActor(machine).start(); - // The state should not be changed yet; `delay: 0` is equivalent to `setTimeout(..., 0)` - expect(service.getSnapshot().value).toEqual('a'); + // The state should not be changed yet; `delay: 0` is equivalent to `setTimeout(..., 0)` + expect(service.getSnapshot().value).toEqual('a'); - setTimeout(() => { - // The state should be changed now - expect(service.getSnapshot().value).toEqual('b'); - done(); - }); - }); + setTimeout(() => { + // The state should be changed now + expect(service.getSnapshot().value).toEqual('b'); + resolve(); + }); + })); it('should be able to raise an event and respond to it in the same state', () => { const machine = createMachine({ @@ -3267,36 +3277,37 @@ describe('raise', () => { expect(service.getSnapshot().value).toEqual('b'); }); - it('should be able to raise a delayed event and respond to it in the same state', (done) => { - const machine = createMachine({ - initial: 'a', - states: { - a: { - entry: raise( - { type: 'TO_B' }, - { - delay: 100 + it('should be able to raise a delayed event and respond to it in the same state', () => + new Promise((resolve) => { + const machine = createMachine({ + initial: 'a', + states: { + a: { + entry: raise( + { type: 'TO_B' }, + { + delay: 100 + } + ), + on: { + TO_B: 'b' } - ), - on: { - TO_B: 'b' + }, + b: { + type: 'final' } - }, - b: { - type: 'final' } - } - }); + }); - const service = createActor(machine).start(); + const service = createActor(machine).start(); - service.subscribe({ complete: () => done() }); + service.subscribe({ complete: () => resolve() }); - setTimeout(() => { - // didn't transition yet - expect(service.getSnapshot().value).toEqual('a'); - }, 50); - }); + setTimeout(() => { + // didn't transition yet + expect(service.getSnapshot().value).toEqual('a'); + }, 50); + })); it('should accept event expression', () => { const machine = createMachine({ @@ -3368,7 +3379,7 @@ describe('raise', () => { ) }); - const errorSpy = jest.fn(); + const errorSpy = vi.fn(); const actorRef = createActor(machine); actorRef.subscribe({ @@ -3376,7 +3387,7 @@ describe('raise', () => { }); actorRef.start(); - expect(errorSpy).toMatchMockCallsInlineSnapshot(` + expect(errorSpy.mock.calls).toMatchInlineSnapshot(` [ [ [Error: Only event objects may be used with raise; use raise({ type: "a string" }) instead], @@ -3423,8 +3434,8 @@ describe('cancel', () => { }); it('should cancel only the delayed event in the machine that scheduled it when canceling the event with the same ID in the machine that sent it first', async () => { - const fooSpy = jest.fn(); - const barSpy = jest.fn(); + const fooSpy = vi.fn(); + const barSpy = vi.fn(); const machine = createMachine({ invoke: [ @@ -3471,8 +3482,8 @@ describe('cancel', () => { }); it('should cancel only the delayed event in the machine that scheduled it when canceling the event with the same ID in the machine that sent it second', async () => { - const fooSpy = jest.fn(); - const barSpy = jest.fn(); + const fooSpy = vi.fn(); + const barSpy = vi.fn(); const machine = createMachine({ invoke: [ @@ -3519,7 +3530,7 @@ describe('cancel', () => { }); it('should not try to clear an undefined timeout when canceling an unscheduled timer', async () => { - const spy = jest.fn(); + const spy = vi.fn(); const machine = createMachine({ on: { @@ -3761,7 +3772,7 @@ describe('actions', () => { }); it('should call an inline action responding to an initial raise with the raised event', () => { - const spy = jest.fn(); + const spy = vi.fn(); const machine = createMachine({ entry: raise({ type: 'HELLO' }), @@ -3780,7 +3791,7 @@ describe('actions', () => { }); it('should call a referenced action responding to an initial raise with the raised event', () => { - const spy = jest.fn(); + const spy = vi.fn(); const machine = createMachine( { @@ -3806,7 +3817,7 @@ describe('actions', () => { }); it('should call an inline action responding to an initial raise with updated (non-initial) context', () => { - const spy = jest.fn(); + const spy = vi.fn(); const machine = createMachine({ context: { count: 0 }, @@ -3826,7 +3837,7 @@ describe('actions', () => { }); it('should call a referenced action responding to an initial raise with updated (non-initial) context', () => { - const spy = jest.fn(); + const spy = vi.fn(); const machine = createMachine( { @@ -3853,7 +3864,7 @@ describe('actions', () => { }); it('should call inline entry custom action with undefined parametrized action object', () => { - const spy = jest.fn(); + const spy = vi.fn(); createActor( createMachine({ entry: (_, params) => { @@ -3866,7 +3877,7 @@ describe('actions', () => { }); it('should call inline entry builtin action with undefined parametrized action object', () => { - const spy = jest.fn(); + const spy = vi.fn(); createActor( createMachine({ entry: assign((_, params) => { @@ -3880,7 +3891,7 @@ describe('actions', () => { }); it('should call inline transition custom action with undefined parametrized action object', () => { - const spy = jest.fn(); + const spy = vi.fn(); const actorRef = createActor( createMachine({ @@ -3899,7 +3910,7 @@ describe('actions', () => { }); it('should call inline transition builtin action with undefined parameters', () => { - const spy = jest.fn(); + const spy = vi.fn(); const actorRef = createActor( createMachine({ @@ -3919,7 +3930,7 @@ describe('actions', () => { }); it('should call a referenced custom action with undefined params when it has no params and it is referenced using a string', () => { - const spy = jest.fn(); + const spy = vi.fn(); createActor( createMachine( @@ -3940,7 +3951,7 @@ describe('actions', () => { }); it('should call a referenced builtin action with undefined params when it has no params and it is referenced using a string', () => { - const spy = jest.fn(); + const spy = vi.fn(); createActor( createMachine( @@ -3962,7 +3973,7 @@ describe('actions', () => { }); it('should call a referenced custom action with the provided parametrized action object', () => { - const spy = jest.fn(); + const spy = vi.fn(); createActor( createMachine( @@ -3990,7 +4001,7 @@ describe('actions', () => { }); it('should call a referenced builtin action with the provided parametrized action object', () => { - const spy = jest.fn(); + const spy = vi.fn(); createActor( createMachine( @@ -4019,6 +4030,8 @@ describe('actions', () => { }); it('should warn if called in custom action', () => { + const warnSpy = vi.spyOn(console, 'warn'); + const machine = createMachine({ entry: () => { assign({}); @@ -4030,7 +4043,7 @@ describe('actions', () => { createActor(machine).start(); - expect(console.warn).toMatchMockCallsInlineSnapshot(` + expect(warnSpy.mock.calls).toMatchInlineSnapshot(` [ [ "Custom actions should not call \`assign()\` directly, as it is not imperative. See https://stately.ai/docs/actions#built-in-actions for more details.", diff --git a/packages/core/test/activities.test.ts b/packages/core/test/activities.test.ts index 93a56bae27..0174530d60 100644 --- a/packages/core/test/activities.test.ts +++ b/packages/core/test/activities.test.ts @@ -246,7 +246,7 @@ describe('invocations (activities)', () => { }); it('should remember the invocations even after an ignored event', () => { - let cleanupSpy = jest.fn(); + let cleanupSpy = vi.fn(); let active = false; const machine = createMachine({ initial: 'A', @@ -279,7 +279,7 @@ describe('invocations (activities)', () => { }); it('should remember the invocations when transitioning within the invoking state', () => { - let cleanupSpy = jest.fn(); + let cleanupSpy = vi.fn(); let active = false; const machine = createMachine({ initial: 'A', diff --git a/packages/core/test/actor.test.ts b/packages/core/test/actor.test.ts index d21488fc00..5134bd20a7 100644 --- a/packages/core/test/actor.test.ts +++ b/packages/core/test/actor.test.ts @@ -30,7 +30,7 @@ import { stopChild } from '../src/index.ts'; import { setup } from '../src/setup.ts'; -import { sleep } from '@xstate-repo/jest-utils'; +import { setTimeout as sleep } from 'node:timers/promises'; describe('spawning machines', () => { const context = { @@ -120,68 +120,69 @@ describe('spawning machines', () => { } }); - it('should spawn machines', (done) => { - const todoMachine = createMachine({ - id: 'todo', - initial: 'incomplete', - states: { - incomplete: { - on: { SET_COMPLETE: 'complete' } - }, - complete: { - entry: sendParent({ type: 'TODO_COMPLETED' }) + it('should spawn machines', () => + new Promise((resolve) => { + const todoMachine = createMachine({ + id: 'todo', + initial: 'incomplete', + states: { + incomplete: { + on: { SET_COMPLETE: 'complete' } + }, + complete: { + entry: sendParent({ type: 'TODO_COMPLETED' }) + } } - } - }); + }); - const todosMachine = createMachine({ - types: {} as { - context: typeof context; - events: TodoEvent; - }, - id: 'todos', - context, - initial: 'active', - states: { - active: { - on: { - TODO_COMPLETED: 'success' + const todosMachine = createMachine({ + types: {} as { + context: typeof context; + events: TodoEvent; + }, + id: 'todos', + context, + initial: 'active', + states: { + active: { + on: { + TODO_COMPLETED: 'success' + } + }, + success: { + type: 'final' } }, - success: { - type: 'final' - } - }, - on: { - ADD: { - actions: assign({ - todoRefs: ({ context, event, spawn }) => ({ - ...context.todoRefs, - [event.id]: spawn(todoMachine) + on: { + ADD: { + actions: assign({ + todoRefs: ({ context, event, spawn }) => ({ + ...context.todoRefs, + [event.id]: spawn(todoMachine) + }) }) - }) - }, - SET_COMPLETE: { - actions: sendTo( - ({ context, event }) => { - return context.todoRefs[event.id]; - }, - { type: 'SET_COMPLETE' } - ) + }, + SET_COMPLETE: { + actions: sendTo( + ({ context, event }) => { + return context.todoRefs[event.id]; + }, + { type: 'SET_COMPLETE' } + ) + } } - } - }); - const service = createActor(todosMachine); - service.subscribe({ - complete: () => { - done(); - } - }); - service.start(); + }); + const service = createActor(todosMachine); + service.subscribe({ + complete: () => { + resolve(); + } + }); + service.start(); - service.send({ type: 'ADD', id: 42 }); - service.send({ type: 'SET_COMPLETE', id: 42 }); - }); + service.send({ type: 'ADD', id: 42 }); + service.send({ type: 'SET_COMPLETE', id: 42 }); + })); it('should spawn referenced machines', () => { const childMachine = createMachine({ @@ -220,171 +221,175 @@ describe('spawning machines', () => { expect(actor.getSnapshot().value).toBe('success'); }); - it('should allow bidirectional communication between parent/child actors', (done) => { - const actor = createActor(clientMachine); - actor.subscribe({ - complete: () => { - done(); - } - }); - actor.start(); - }); + it('should allow bidirectional communication between parent/child actors', () => + new Promise((resolve) => { + const actor = createActor(clientMachine); + actor.subscribe({ + complete: () => { + resolve(); + } + }); + actor.start(); + })); }); const aaa = 'dadasda'; describe('spawning promises', () => { - it('should be able to spawn a promise', (done) => { - const promiseMachine = createMachine({ - types: {} as { - context: { promiseRef?: PromiseActorRef }; - }, - id: 'promise', - initial: 'idle', - context: { - promiseRef: undefined - }, - states: { - idle: { - entry: assign({ - promiseRef: ({ spawn }) => { - const ref = spawn( - fromPromise( - () => - new Promise((res) => { - res('response'); - }) - ), - { id: 'my-promise' } - ); + it('should be able to spawn a promise', () => + new Promise((resolve) => { + const promiseMachine = createMachine({ + types: {} as { + context: { promiseRef?: PromiseActorRef }; + }, + id: 'promise', + initial: 'idle', + context: { + promiseRef: undefined + }, + states: { + idle: { + entry: assign({ + promiseRef: ({ spawn }) => { + const ref = spawn( + fromPromise( + () => + new Promise((res) => { + res('response'); + }) + ), + { id: 'my-promise' } + ); - return ref; - } - }), - on: { - 'xstate.done.actor.my-promise': { - target: 'success', - guard: ({ event }) => event.output === 'response' + return ref; + } + }), + on: { + 'xstate.done.actor.my-promise': { + target: 'success', + guard: ({ event }) => event.output === 'response' + } } + }, + success: { + type: 'final' } - }, - success: { - type: 'final' } - } - }); + }); - const promiseService = createActor(promiseMachine); - promiseService.subscribe({ - complete: () => { - done(); - } - }); + const promiseService = createActor(promiseMachine); + promiseService.subscribe({ + complete: () => { + resolve(); + } + }); - promiseService.start(); - }); + promiseService.start(); + })); - it('should be able to spawn a referenced promise', (done) => { - const promiseMachine = setup({ - actors: { - somePromise: fromPromise(() => Promise.resolve('response')) - } - }).createMachine({ - types: {} as { - context: { promiseRef?: PromiseActorRef }; - }, - id: 'promise', - initial: 'idle', - context: { - promiseRef: undefined - }, - states: { - idle: { - entry: assign({ - promiseRef: ({ spawn }) => - spawn('somePromise', { id: 'my-promise' }) - }), - on: { - 'xstate.done.actor.my-promise': { - target: 'success', - guard: ({ event }) => event.output === 'response' + it('should be able to spawn a referenced promise', () => + new Promise((resolve) => { + const promiseMachine = setup({ + actors: { + somePromise: fromPromise(() => Promise.resolve('response')) + } + }).createMachine({ + types: {} as { + context: { promiseRef?: PromiseActorRef }; + }, + id: 'promise', + initial: 'idle', + context: { + promiseRef: undefined + }, + states: { + idle: { + entry: assign({ + promiseRef: ({ spawn }) => + spawn('somePromise', { id: 'my-promise' }) + }), + on: { + 'xstate.done.actor.my-promise': { + target: 'success', + guard: ({ event }) => event.output === 'response' + } } + }, + success: { + type: 'final' } - }, - success: { - type: 'final' } - } - }); + }); - const promiseService = createActor(promiseMachine); - promiseService.subscribe({ - complete: () => { - done(); - } - }); + const promiseService = createActor(promiseMachine); + promiseService.subscribe({ + complete: () => { + resolve(); + } + }); - promiseService.start(); - }); + promiseService.start(); + })); }); describe('spawning callbacks', () => { - it('should be able to spawn an actor from a callback', (done) => { - const callbackMachine = createMachine({ - types: {} as { + it('should be able to spawn an actor from a callback', () => + new Promise((resolve) => { + const callbackMachine = createMachine({ + types: {} as { + context: { + callbackRef?: CallbackActorRef<{ type: 'START' }>; + }; + }, + id: 'callback', + initial: 'idle', context: { - callbackRef?: CallbackActorRef<{ type: 'START' }>; - }; - }, - id: 'callback', - initial: 'idle', - context: { - callbackRef: undefined - }, - states: { - idle: { - entry: assign({ - callbackRef: ({ spawn }) => - spawn( - fromCallback<{ type: 'START' }>(({ sendBack, receive }) => { - receive((event) => { - if (event.type === 'START') { - setTimeout(() => { - sendBack({ type: 'SEND_BACK' }); - }, 10); - } - }); + callbackRef: undefined + }, + states: { + idle: { + entry: assign({ + callbackRef: ({ spawn }) => + spawn( + fromCallback<{ type: 'START' }>(({ sendBack, receive }) => { + receive((event) => { + if (event.type === 'START') { + setTimeout(() => { + sendBack({ type: 'SEND_BACK' }); + }, 10); + } + }); + }) + ) + }), + on: { + START_CB: { + actions: sendTo(({ context }) => context.callbackRef!, { + type: 'START' }) - ) - }), - on: { - START_CB: { - actions: sendTo(({ context }) => context.callbackRef!, { - type: 'START' - }) - }, - SEND_BACK: 'success' + }, + SEND_BACK: 'success' + } + }, + success: { + type: 'final' } - }, - success: { - type: 'final' } - } - }); + }); - const callbackService = createActor(callbackMachine); - callbackService.subscribe({ - complete: () => { - done(); - } - }); + const callbackService = createActor(callbackMachine); + callbackService.subscribe({ + complete: () => { + resolve(); + } + }); - callbackService.start(); - callbackService.send({ type: 'START_CB' }); - }); + callbackService.start(); + callbackService.send({ type: 'START_CB' }); + })); it('should not deliver events sent to the parent after the callback actor gets stopped', () => { - const spy = jest.fn(); + const spy = vi.fn(); let sendToParent: () => void; @@ -423,62 +428,26 @@ describe('spawning callbacks', () => { }); describe('spawning observables', () => { - it('should spawn an observable', (done) => { - const observableLogic = fromObservable(() => interval(10)); - const observableMachine = createMachine({ - id: 'observable', - initial: 'idle', - context: { - observableRef: undefined! as ActorRefFrom - }, - states: { - idle: { - entry: assign({ - observableRef: ({ spawn }) => { - const ref = spawn(observableLogic, { - id: 'int', - syncSnapshot: true - }); - - return ref; - } - }), - on: { - 'xstate.snapshot.int': { - target: 'success', - guard: ({ event }) => event.snapshot.context === 5 - } - } - }, - success: { - type: 'final' - } - } - }); - - const observableService = createActor(observableMachine); - observableService.subscribe({ - complete: () => { - done(); - } - }); - - observableService.start(); - }); - - it('should spawn a referenced observable', (done) => { - const observableMachine = createMachine( - { + it('should spawn an observable', () => + new Promise((resolve) => { + const observableLogic = fromObservable(() => interval(10)); + const observableMachine = createMachine({ id: 'observable', initial: 'idle', context: { - observableRef: undefined! as AnyActorRef + observableRef: undefined! as ActorRefFrom }, states: { idle: { entry: assign({ - observableRef: ({ spawn }) => - spawn('interval', { id: 'int', syncSnapshot: true }) + observableRef: ({ spawn }) => { + const ref = spawn(observableLogic, { + id: 'int', + syncSnapshot: true + }); + + return ref; + } }), on: { 'xstate.snapshot.int': { @@ -491,71 +460,110 @@ describe('spawning observables', () => { type: 'final' } } - }, - { - actors: { - interval: fromObservable(() => interval(10)) + }); + + const observableService = createActor(observableMachine); + observableService.subscribe({ + complete: () => { + resolve(); } - } - ); + }); - const observableService = createActor(observableMachine); - observableService.subscribe({ - complete: () => { - done(); - } - }); + observableService.start(); + })); - observableService.start(); - }); + it('should spawn a referenced observable', () => + new Promise((resolve) => { + const observableMachine = createMachine( + { + id: 'observable', + initial: 'idle', + context: { + observableRef: undefined! as AnyActorRef + }, + states: { + idle: { + entry: assign({ + observableRef: ({ spawn }) => + spawn('interval', { id: 'int', syncSnapshot: true }) + }), + on: { + 'xstate.snapshot.int': { + target: 'success', + guard: ({ event }) => event.snapshot.context === 5 + } + } + }, + success: { + type: 'final' + } + } + }, + { + actors: { + interval: fromObservable(() => interval(10)) + } + } + ); - it(`should read the latest snapshot of the event's origin while handling that event`, (done) => { - const observableLogic = fromObservable(() => interval(10)); - const observableMachine = createMachine({ - id: 'observable', - initial: 'idle', - context: { - observableRef: undefined! as ActorRefFrom - }, - states: { - idle: { - entry: assign({ - observableRef: ({ spawn }) => { - const ref = spawn(observableLogic, { - id: 'int', - syncSnapshot: true - }); + const observableService = createActor(observableMachine); + observableService.subscribe({ + complete: () => { + resolve(); + } + }); - return ref; - } - }), - on: { - 'xstate.snapshot.int': { - target: 'success', - guard: ({ context, event }) => { - return ( - event.snapshot.context === 1 && - context.observableRef.getSnapshot().context === 1 - ); + observableService.start(); + })); + + it(`should read the latest snapshot of the event's origin while handling that event`, () => + new Promise((resolve) => { + const observableLogic = fromObservable(() => interval(10)); + const observableMachine = createMachine({ + id: 'observable', + initial: 'idle', + context: { + observableRef: undefined! as ActorRefFrom + }, + states: { + idle: { + entry: assign({ + observableRef: ({ spawn }) => { + const ref = spawn(observableLogic, { + id: 'int', + syncSnapshot: true + }); + + return ref; + } + }), + on: { + 'xstate.snapshot.int': { + target: 'success', + guard: ({ context, event }) => { + return ( + event.snapshot.context === 1 && + context.observableRef.getSnapshot().context === 1 + ); + } } } + }, + success: { + type: 'final' } - }, - success: { - type: 'final' } - } - }); + }); - const observableService = createActor(observableMachine); - observableService.subscribe({ - complete: () => { - done(); - } - }); + const observableService = createActor(observableMachine); + observableService.subscribe({ + complete: () => { + resolve(); + } + }); - observableService.start(); - }); + observableService.start(); + })); it('should notify direct child listeners with final snapshot before it gets stopped', async () => { const intervalActor = fromObservable(() => interval(10)); @@ -600,7 +608,7 @@ describe('spawning observables', () => { await waitFor(actorRef, (state) => state.matches('active')); - const spy = jest.fn(); + const spy = vi.fn(); actorRef.getSnapshot().children.childActor!.subscribe((data) => { spy(data.context); @@ -650,81 +658,46 @@ describe('spawning observables', () => { ); const actorRef = createActor(parentMachine); - actorRef.start(); - - await waitFor(actorRef, (state) => state.matches('active')); - - const spy = jest.fn(); - - actorRef.getSnapshot().children.childActor!.subscribe((data) => { - spy(data); - }); - - await waitFor(actorRef, (state) => state.status !== 'active'); - spy.mockClear(); - - // wait for potential next event from the interval actor - await sleep(15); - - expect(spy).not.toHaveBeenCalled(); - }); -}); - -describe('spawning event observables', () => { - it('should spawn an event observable', (done) => { - const eventObservableLogic = fromEventObservable(() => - interval(10).pipe(map((val) => ({ type: 'COUNT', val }))) - ); - const observableMachine = createMachine({ - id: 'observable', - initial: 'idle', - context: { - observableRef: undefined! as ActorRefFrom - }, - states: { - idle: { - entry: assign({ - observableRef: ({ spawn }) => { - const ref = spawn(eventObservableLogic, { id: 'int' }); - - return ref; - } - }), - on: { - COUNT: { - target: 'success', - guard: ({ event }) => event.val === 5 - } - } - }, - success: { - type: 'final' - } - } - }); + actorRef.start(); - const observableService = createActor(observableMachine); - observableService.subscribe({ - complete: () => { - done(); - } + await waitFor(actorRef, (state) => state.matches('active')); + + const spy = vi.fn(); + + actorRef.getSnapshot().children.childActor!.subscribe((data) => { + spy(data); }); - observableService.start(); + await waitFor(actorRef, (state) => state.status !== 'active'); + spy.mockClear(); + + // wait for potential next event from the interval actor + await sleep(15); + + expect(spy).not.toHaveBeenCalled(); }); +}); - it('should spawn a referenced event observable', (done) => { - const observableMachine = createMachine( - { +describe('spawning event observables', () => { + it('should spawn an event observable', () => + new Promise((resolve) => { + const eventObservableLogic = fromEventObservable(() => + interval(10).pipe(map((val) => ({ type: 'COUNT', val }))) + ); + const observableMachine = createMachine({ id: 'observable', initial: 'idle', context: { - observableRef: undefined! as AnyActorRef + observableRef: undefined! as ActorRefFrom }, states: { idle: { entry: assign({ - observableRef: ({ spawn }) => spawn('interval', { id: 'int' }) + observableRef: ({ spawn }) => { + const ref = spawn(eventObservableLogic, { id: 'int' }); + + return ref; + } }), on: { COUNT: { @@ -737,93 +710,133 @@ describe('spawning event observables', () => { type: 'final' } } - }, - { - actors: { - interval: fromEventObservable(() => - interval(10).pipe(map((val) => ({ type: 'COUNT', val }))) - ) + }); + + const observableService = createActor(observableMachine); + observableService.subscribe({ + complete: () => { + resolve(); } - } - ); + }); - const observableService = createActor(observableMachine); - observableService.subscribe({ - complete: () => { - done(); - } - }); + observableService.start(); + })); - observableService.start(); - }); + it('should spawn a referenced event observable', () => + new Promise((resolve) => { + const observableMachine = createMachine( + { + id: 'observable', + initial: 'idle', + context: { + observableRef: undefined! as AnyActorRef + }, + states: { + idle: { + entry: assign({ + observableRef: ({ spawn }) => spawn('interval', { id: 'int' }) + }), + on: { + COUNT: { + target: 'success', + guard: ({ event }) => event.val === 5 + } + } + }, + success: { + type: 'final' + } + } + }, + { + actors: { + interval: fromEventObservable(() => + interval(10).pipe(map((val) => ({ type: 'COUNT', val }))) + ) + } + } + ); + + const observableService = createActor(observableMachine); + observableService.subscribe({ + complete: () => { + resolve(); + } + }); + + observableService.start(); + })); }); describe('communicating with spawned actors', () => { - it('should treat an interpreter as an actor', (done) => { - const existingMachine = createMachine({ - types: { - events: {} as { - type: 'ACTIVATE'; - origin: AnyActorRef; - } - }, - initial: 'inactive', - states: { - inactive: { - on: { ACTIVATE: 'active' } + it('should treat an interpreter as an actor', () => + new Promise((resolve) => { + const existingMachine = createMachine({ + types: { + events: {} as { + type: 'ACTIVATE'; + origin: AnyActorRef; + } }, - active: { - entry: sendTo(({ event }) => event.origin, { type: 'EXISTING.DONE' }) + initial: 'inactive', + states: { + inactive: { + on: { ACTIVATE: 'active' } + }, + active: { + entry: sendTo(({ event }) => event.origin, { + type: 'EXISTING.DONE' + }) + } } - } - }); + }); - const existingService = createActor(existingMachine).start(); + const existingService = createActor(existingMachine).start(); - const parentMachine = createMachine({ - types: {} as { - context: { existingRef?: typeof existingService }; - }, - initial: 'pending', - context: { - existingRef: undefined - }, - states: { - pending: { - entry: assign({ - // No need to spawn an existing service: - existingRef: existingService - }), - on: { - 'EXISTING.DONE': 'success' - }, - after: { - 100: { - actions: sendTo( - ({ context }) => context.existingRef!, - ({ self }) => ({ - type: 'ACTIVATE', - origin: self - }) - ) + const parentMachine = createMachine({ + types: {} as { + context: { existingRef?: typeof existingService }; + }, + initial: 'pending', + context: { + existingRef: undefined + }, + states: { + pending: { + entry: assign({ + // No need to spawn an existing service: + existingRef: existingService + }), + on: { + 'EXISTING.DONE': 'success' + }, + after: { + 100: { + actions: sendTo( + ({ context }) => context.existingRef!, + ({ self }) => ({ + type: 'ACTIVATE', + origin: self + }) + ) + } } + }, + success: { + type: 'final' } - }, - success: { - type: 'final' } - } - }); + }); - const parentService = createActor(parentMachine); - parentService.subscribe({ - complete: () => { - done(); - } - }); + const parentService = createActor(parentMachine); + parentService.subscribe({ + complete: () => { + resolve(); + } + }); - parentService.start(); - }); + parentService.start(); + })); }); describe('actors', () => { @@ -964,8 +977,8 @@ describe('actors', () => { }); it('should stop multiple inline spawned actors that have no explicit ids', () => { - const cleanup1 = jest.fn(); - const cleanup2 = jest.fn(); + const cleanup1 = vi.fn(); + const cleanup2 = vi.fn(); const parent = createMachine({ context: ({ spawn }) => ({ @@ -984,8 +997,8 @@ describe('actors', () => { }); it('should stop multiple referenced spawned actors that have no explicit ids', () => { - const cleanup1 = jest.fn(); - const cleanup2 = jest.fn(); + const cleanup1 = vi.fn(); + const cleanup2 = vi.fn(); const parent = createMachine( { @@ -1012,124 +1025,214 @@ describe('actors', () => { }); describe('with actor logic', () => { - it('should work with a transition function logic', (done) => { - const countLogic = fromTransition((count: number, event: any) => { - if (event.type === 'INC') { - return count + 1; - } else if (event.type === 'DEC') { - return count - 1; - } - return count; - }, 0); - - const countMachine = createMachine({ - types: {} as { - context: { count: ActorRefFrom | undefined }; - }, - context: { - count: undefined - }, - entry: assign({ - count: ({ spawn }) => spawn(countLogic) - }), - on: { - INC: { - actions: forwardTo(({ context }) => context.count!) + it('should work with a transition function logic', () => + new Promise((resolve) => { + const countLogic = fromTransition((count: number, event: any) => { + if (event.type === 'INC') { + return count + 1; + } else if (event.type === 'DEC') { + return count - 1; } - } - }); + return count; + }, 0); - const countService = createActor(countMachine); - countService.subscribe((state) => { - if (state.context.count?.getSnapshot().context === 2) { - done(); - } - }); - countService.start(); - - countService.send({ type: 'INC' }); - countService.send({ type: 'INC' }); - - expect( - countService.getSnapshot().context.count?.getSnapshot().context - ).toBe(2); - }); + const countMachine = createMachine({ + types: {} as { + context: { count: ActorRefFrom | undefined }; + }, + context: { + count: undefined + }, + entry: assign({ + count: ({ spawn }) => spawn(countLogic) + }), + on: { + INC: { + actions: forwardTo(({ context }) => context.count!) + } + } + }); - it('should work with a promise logic (fulfill)', (done) => { - const countMachine = createMachine({ - types: {} as { + const countService = createActor(countMachine); + countService.subscribe((state) => { + if (state.context.count?.getSnapshot().context === 2) { + resolve(); + } + }); + countService.start(); + + countService.send({ type: 'INC' }); + countService.send({ type: 'INC' }); + + expect( + countService.getSnapshot().context.count?.getSnapshot().context + ).toBe(2); + })); + + it('should work with a promise logic (fulfill)', () => + new Promise((resolve) => { + const countMachine = createMachine({ + types: {} as { + context: { + count: ActorRefFrom> | undefined; + }; + }, context: { - count: ActorRefFrom> | undefined; - }; - }, - context: { - count: undefined - }, - entry: assign({ - count: ({ spawn }) => - spawn( + count: undefined + }, + entry: assign({ + count: ({ spawn }) => + spawn( + fromPromise( + () => + new Promise((res) => { + setTimeout(() => res(42)); + }) + ), + { id: 'test' } + ) + }), + initial: 'pending', + states: { + pending: { + on: { + 'xstate.done.actor.test': { + target: 'success', + guard: ({ event }) => event.output === 42 + } + } + }, + success: { + type: 'final' + } + } + }); + + const countService = createActor(countMachine); + countService.subscribe({ + complete: () => { + resolve(); + } + }); + countService.start(); + })); + + it('should work with a promise logic (reject)', () => + new Promise((resolve) => { + const errorMessage = 'An error occurred'; + const countMachine = createMachine({ + types: {} as { + context: { count: ActorRefFrom> }; + }, + context: ({ spawn }) => ({ + count: spawn( fromPromise( () => - new Promise((res) => { - setTimeout(() => res(42)); + new Promise((_, rej) => { + setTimeout(() => rej(errorMessage), 1); }) ), { id: 'test' } ) - }), - initial: 'pending', - states: { - pending: { - on: { - 'xstate.done.actor.test': { - target: 'success', - guard: ({ event }) => event.output === 42 + }), + initial: 'pending', + states: { + pending: { + on: { + 'xstate.error.actor.test': { + target: 'success', + guard: ({ event }) => { + return event.error === errorMessage; + } + } } + }, + success: { + type: 'final' } + } + }); + + const countService = createActor(countMachine); + countService.subscribe({ + complete: () => { + resolve(); + } + }); + countService.start(); + })); + + it('actor logic should have reference to the parent', () => + new Promise((resolve) => { + const pongLogic: ActorLogic, EventObject> = { + transition: (state, event, { self }) => { + if (event.type === 'PING') { + self._parent?.send({ type: 'PONG' }); + } + + return state; }, - success: { - type: 'final' + getInitialSnapshot: () => ({ + status: 'active', + output: undefined, + error: undefined + }), + getPersistedSnapshot: (s) => s + }; + + const pingMachine = createMachine({ + types: {} as { + context: { ponger: ActorRefFrom | undefined }; + }, + initial: 'waiting', + context: { + ponger: undefined + }, + entry: assign({ + ponger: ({ spawn }) => spawn(pongLogic) + }), + states: { + waiting: { + entry: sendTo(({ context }) => context.ponger!, { type: 'PING' }), + invoke: { + id: 'ponger', + src: pongLogic + }, + on: { + PONG: 'success' + } + }, + success: { + type: 'final' + } } - } - }); + }); - const countService = createActor(countMachine); - countService.subscribe({ - complete: () => { - done(); - } - }); - countService.start(); - }); + const pingService = createActor(pingMachine); + pingService.subscribe({ + complete: () => { + resolve(); + } + }); + pingService.start(); + })); + }); - it('should work with a promise logic (reject)', (done) => { - const errorMessage = 'An error occurred'; - const countMachine = createMachine({ - types: {} as { - context: { count: ActorRefFrom> }; - }, + it('should be able to spawn callback actors in (lazy) initial context', () => + new Promise((resolve) => { + const machine = createMachine({ + types: {} as { context: { ref: CallbackActorRef } }, context: ({ spawn }) => ({ - count: spawn( - fromPromise( - () => - new Promise((_, rej) => { - setTimeout(() => rej(errorMessage), 1); - }) - ), - { id: 'test' } + ref: spawn( + fromCallback(({ sendBack }) => { + sendBack({ type: 'TEST' }); + }) ) }), - initial: 'pending', + initial: 'waiting', states: { - pending: { - on: { - 'xstate.error.actor.test': { - target: 'success', - guard: ({ event }) => { - return event.error === errorMessage; - } - } - } + waiting: { + on: { TEST: 'success' } }, success: { type: 'final' @@ -1137,53 +1240,30 @@ describe('actors', () => { } }); - const countService = createActor(countMachine); - countService.subscribe({ + const actor = createActor(machine); + actor.subscribe({ complete: () => { - done(); + resolve(); } }); - countService.start(); - }); + actor.start(); + })); - it('actor logic should have reference to the parent', (done) => { - const pongLogic: ActorLogic, EventObject> = { - transition: (state, event, { self }) => { - if (event.type === 'PING') { - self._parent?.send({ type: 'PONG' }); - } + it('should be able to spawn machines in (lazy) initial context', () => + new Promise((resolve) => { + const childMachine = createMachine({ + entry: sendParent({ type: 'TEST' }) + }); - return state; - }, - getInitialSnapshot: () => ({ - status: 'active', - output: undefined, - error: undefined + const machine = createMachine({ + types: {} as { context: { ref: ActorRefFrom } }, + context: ({ spawn }) => ({ + ref: spawn(childMachine) }), - getPersistedSnapshot: (s) => s - }; - - const pingMachine = createMachine({ - types: {} as { - context: { ponger: ActorRefFrom | undefined }; - }, initial: 'waiting', - context: { - ponger: undefined - }, - entry: assign({ - ponger: ({ spawn }) => spawn(pongLogic) - }), states: { waiting: { - entry: sendTo(({ context }) => context.ponger!, { type: 'PING' }), - invoke: { - id: 'ponger', - src: pongLogic - }, - on: { - PONG: 'success' - } + on: { TEST: 'success' } }, success: { type: 'final' @@ -1191,75 +1271,14 @@ describe('actors', () => { } }); - const pingService = createActor(pingMachine); - pingService.subscribe({ + const actor = createActor(machine); + actor.subscribe({ complete: () => { - done(); + resolve(); } }); - pingService.start(); - }); - }); - - it('should be able to spawn callback actors in (lazy) initial context', (done) => { - const machine = createMachine({ - types: {} as { context: { ref: CallbackActorRef } }, - context: ({ spawn }) => ({ - ref: spawn( - fromCallback(({ sendBack }) => { - sendBack({ type: 'TEST' }); - }) - ) - }), - initial: 'waiting', - states: { - waiting: { - on: { TEST: 'success' } - }, - success: { - type: 'final' - } - } - }); - - const actor = createActor(machine); - actor.subscribe({ - complete: () => { - done(); - } - }); - actor.start(); - }); - - it('should be able to spawn machines in (lazy) initial context', (done) => { - const childMachine = createMachine({ - entry: sendParent({ type: 'TEST' }) - }); - - const machine = createMachine({ - types: {} as { context: { ref: ActorRefFrom } }, - context: ({ spawn }) => ({ - ref: spawn(childMachine) - }), - initial: 'waiting', - states: { - waiting: { - on: { TEST: 'success' } - }, - success: { - type: 'final' - } - } - }); - - const actor = createActor(machine); - actor.subscribe({ - complete: () => { - done(); - } - }); - actor.start(); - }); + actor.start(); + })); // https://github.com/statelyai/xstate/issues/2507 it('should not crash on child machine sync completion during self-initialization', () => { @@ -1305,7 +1324,10 @@ describe('actors', () => { it('should not crash on child promise-like sync completion during self-initialization', () => { const promiseLogic = fromPromise( - () => ({ then: (fn: any) => fn(null) }) as any + () => + ({ + then: (fn: any) => fn(null) + }) as any ); const parentMachine = createMachine({ types: {} as { @@ -1693,7 +1715,7 @@ describe('actors', () => { }); it('should be possible to pass `self` as input to a child machine from within the context factory', () => { - const spy = jest.fn(); + const spy = vi.fn(); const child = createMachine({ types: {} as { diff --git a/packages/core/test/actorLogic.test.ts b/packages/core/test/actorLogic.test.ts index 7f37a7f145..80db2cab62 100644 --- a/packages/core/test/actorLogic.test.ts +++ b/packages/core/test/actorLogic.test.ts @@ -17,6 +17,7 @@ import { } from '../src/actors/index.ts'; import { waitFor } from '../src/waitFor.ts'; import { raise, sendTo } from '../src/actions.ts'; +import type { Mock } from 'vitest'; describe('promise logic (fromPromise)', () => { it('should interpret a promise', async () => { @@ -33,44 +34,47 @@ describe('promise logic (fromPromise)', () => { expect(snapshot.output).toBe('hello'); }); - it('should resolve', (done) => { - const actor = createActor(fromPromise(() => Promise.resolve(42))); + it('should resolve', () => + new Promise((resolve) => { + const actor = createActor(fromPromise(() => Promise.resolve(42))); - actor.subscribe((state) => { - if (state.output === 42) { - done(); - } - }); + actor.subscribe((state) => { + if (state.output === 42) { + resolve(); + } + }); - actor.start(); - }); + actor.start(); + })); - it('should resolve (observer .next)', (done) => { - const actor = createActor(fromPromise(() => Promise.resolve(42))); + it('should resolve (observer .next)', () => + new Promise((resolve) => { + const actor = createActor(fromPromise(() => Promise.resolve(42))); - actor.subscribe({ - next: (state) => { - if (state.output === 42) { - done(); + actor.subscribe({ + next: (state) => { + if (state.output === 42) { + resolve(); + } } - } - }); + }); - actor.start(); - }); + actor.start(); + })); - it('should reject (observer .error)', (done) => { - const actor = createActor(fromPromise(() => Promise.reject('Error'))); + it('should reject (observer .error)', () => + new Promise((resolve) => { + const actor = createActor(fromPromise(() => Promise.reject('Error'))); - actor.subscribe({ - error: (data) => { - expect(data).toBe('Error'); - done(); - } - }); + actor.subscribe({ + error: (data) => { + expect(data).toBe('Error'); + resolve(); + } + }); - actor.start(); - }); + actor.start(); + })); it('should complete (observer .complete)', async () => { const actor = createActor(fromPromise(() => Promise.resolve(42))); @@ -95,45 +99,47 @@ describe('promise logic (fromPromise)', () => { expect(called).toBe(false); }); - it('should persist an unresolved promise', (done) => { - const promiseLogic = fromPromise( - () => - new Promise((res) => { - setTimeout(() => res(42), 10); - }) - ); - - const actor = createActor(promiseLogic); - actor.start(); + it('should persist an unresolved promise', () => + new Promise((resolve) => { + const promiseLogic = fromPromise( + () => + new Promise((res) => { + setTimeout(() => res(42), 10); + }) + ); - const resolvedPersistedState = actor.getPersistedSnapshot(); - actor.stop(); + const actor = createActor(promiseLogic); + actor.start(); - const restoredActor = createActor(promiseLogic, { - snapshot: resolvedPersistedState - }).start(); + const resolvedPersistedState = actor.getPersistedSnapshot(); + actor.stop(); - setTimeout(() => { - expect(restoredActor.getSnapshot().output).toBe(42); - done(); - }, 20); - }); + const restoredActor = createActor(promiseLogic, { + snapshot: resolvedPersistedState + }).start(); - it('should persist a resolved promise', (done) => { - const promiseLogic = fromPromise( - () => - new Promise((res) => { - res(42); - }) - ); + setTimeout(() => { + expect(restoredActor.getSnapshot().output).toBe(42); + resolve(); + }, 20); + })); + + it('should persist a resolved promise', () => + new Promise((resolve) => { + const promiseLogic = fromPromise( + () => + new Promise((res) => { + res(42); + }) + ); - const actor = createActor(promiseLogic); - actor.start(); + const actor = createActor(promiseLogic); + actor.start(); - setTimeout(() => { - const resolvedPersistedState = actor.getPersistedSnapshot(); + setTimeout(() => { + const resolvedPersistedState = actor.getPersistedSnapshot(); - expect(resolvedPersistedState).toMatchInlineSnapshot(` + expect(resolvedPersistedState).toMatchInlineSnapshot(` { "error": undefined, "input": undefined, @@ -142,13 +148,13 @@ describe('promise logic (fromPromise)', () => { } `); - const restoredActor = createActor(promiseLogic, { - snapshot: resolvedPersistedState - }).start(); - expect(restoredActor.getSnapshot().output).toBe(42); - done(); - }, 5); - }); + const restoredActor = createActor(promiseLogic, { + snapshot: resolvedPersistedState + }).start(); + expect(restoredActor.getSnapshot().output).toBe(42); + resolve(); + }, 5); + })); it('should not invoke a resolved promise again', async () => { let createdPromises = 0; @@ -235,7 +241,7 @@ describe('promise logic (fromPromise)', () => { it('should abort when stopping', async () => { const deferred = withResolvers(); - const fn = jest.fn(); + const fn = vi.fn(); const promiseLogic = fromPromise((ctx) => { return new Promise((res) => { ctx.signal.addEventListener('abort', fn); @@ -253,14 +259,14 @@ describe('promise logic (fromPromise)', () => { it('should not abort when stopped if promise is resolved/rejected', async () => { const resolvedDeferred = withResolvers(); - const resolvedSignalListener = jest.fn(); + const resolvedSignalListener = vi.fn(); const resolvedPromiseLogic = fromPromise((ctx) => { ctx.signal.addEventListener('abort', resolvedSignalListener); return resolvedDeferred.promise; }); const rejectedDeferred = withResolvers(); - const rejectedSignalListener = jest.fn(); + const rejectedSignalListener = vi.fn(); const rejectedPromiseLogic = fromPromise((ctx) => { ctx.signal.addEventListener('abort', rejectedSignalListener); return rejectedDeferred.promise.catch(() => {}); @@ -283,10 +289,10 @@ describe('promise logic (fromPromise)', () => { it('should not reuse the same signal for different actors with same logic', async () => { let deferredMap: Map> = new Map(); - let signalListenerMap: Map = new Map(); + let signalListenerMap: Map = new Map(); const p = fromPromise(({ self, signal }) => { const deferred = withResolvers(); - const signalListener = jest.fn(); + const signalListener = vi.fn(); deferredMap.set(self.id, deferred); signalListenerMap.set(self.id, signalListener); signal.addEventListener('abort', signalListener); @@ -343,10 +349,10 @@ describe('promise logic (fromPromise)', () => { it('should not reuse the same signal for different actors with same logic and id', async () => { let deferredList: PromiseWithResolvers[] = []; - let signalListenerList: jest.Mock[] = []; + let signalListenerList: Mock[] = []; const p = fromPromise(({ signal }) => { const deferred = withResolvers(); - const fn = jest.fn(); + const fn = vi.fn(); deferredList.push(deferred); signalListenerList.push(fn); signal.addEventListener('abort', fn); @@ -407,10 +413,10 @@ describe('promise logic (fromPromise)', () => { it('should not reuse the same signal for the same actor when restarted', async () => { let deferredList: PromiseWithResolvers[] = []; - let signalListenerList: jest.Mock[] = []; + let signalListenerList: Mock[] = []; const p = fromPromise(({ signal }) => { const deferred = withResolvers(); - const fn = jest.fn(); + const fn = vi.fn(); deferredList.push(deferred); signalListenerList.push(fn); signal.addEventListener('abort', fn); @@ -561,7 +567,7 @@ describe('observable logic (fromObservable)', () => { it('should resolve', () => { const actor = createActor(fromObservable(() => of(42))); - const spy = jest.fn(); + const spy = vi.fn(); actor.subscribe((snapshot) => spy(snapshot.context)); @@ -572,7 +578,7 @@ describe('observable logic (fromObservable)', () => { it('should resolve (observer .next)', () => { const actor = createActor(fromObservable(() => of(42))); - const spy = jest.fn(); + const spy = vi.fn(); actor.subscribe({ next: (snapshot) => spy(snapshot.context) @@ -586,14 +592,14 @@ describe('observable logic (fromObservable)', () => { const actor = createActor( fromObservable(() => throwError(() => 'Observable error.')) ); - const spy = jest.fn(); + const spy = vi.fn(); actor.subscribe({ error: spy }); actor.start(); - expect(spy).toMatchMockCallsInlineSnapshot(` + expect(spy.mock.calls).toMatchInlineSnapshot(` [ [ "Observable error.", @@ -604,7 +610,7 @@ describe('observable logic (fromObservable)', () => { it('should complete (observer .complete)', () => { const actor = createActor(fromObservable(() => EMPTY)); - const spy = jest.fn(); + const spy = vi.fn(); actor.subscribe({ complete: spy @@ -705,42 +711,43 @@ describe('callback logic (fromCallback)', () => { createActor(callbackLogic).start(); }); - it('can send self reference in an event to parent', (done) => { - const machine = createMachine({ - types: {} as { - events: { type: 'PING'; ref: AnyActorRef }; - }, - invoke: { - src: fromCallback(({ self, sendBack, receive }) => { - receive((event) => { - switch (event.type) { - case 'PONG': { - done(); + it('can send self reference in an event to parent', () => + new Promise((resolve) => { + const machine = createMachine({ + types: {} as { + events: { type: 'PING'; ref: AnyActorRef }; + }, + invoke: { + src: fromCallback(({ self, sendBack, receive }) => { + receive((event) => { + switch (event.type) { + case 'PONG': { + resolve(); + } } - } - }); + }); - sendBack({ - type: 'PING', - ref: self - }); - }) - }, - on: { - PING: { - actions: sendTo( - ({ event }) => event.ref, - () => ({ type: 'PONG' }) - ) + sendBack({ + type: 'PING', + ref: self + }); + }) + }, + on: { + PING: { + actions: sendTo( + ({ event }) => event.ref, + () => ({ type: 'PONG' }) + ) + } } - } - }); + }); - createActor(machine).start(); - }); + createActor(machine).start(); + })); it('should persist the input of a callback', () => { - const spy = jest.fn(); + const spy = vi.fn(); const machine = createMachine( { types: {} as { events: { type: 'EV'; data: number } }, @@ -1080,7 +1087,7 @@ describe('machine logic', () => { expect(() => actorRef.getPersistedSnapshot() ).toThrowErrorMatchingInlineSnapshot( - `"An inline child actor cannot be persisted."` + `[Error: An inline child actor cannot be persisted.]` ); }); @@ -1186,35 +1193,36 @@ describe('composable actor logic', () => { expect(logs).toEqual([42]); }); - it('should work with observables', (done) => { - const logs: any[] = []; + it('should work with observables', () => + new Promise((resolve) => { + const logs: any[] = []; - function withLogs(actorLogic: T): T { - return { - ...actorLogic, - transition: (state: Snapshot, event, actorScope) => { - const s = actorLogic.transition(state, event, actorScope); + function withLogs(actorLogic: T): T { + return { + ...actorLogic, + transition: (state: Snapshot, event, actorScope) => { + const s = actorLogic.transition(state, event, actorScope); - if (s.status === 'active') { - logs.push(s.context); - } + if (s.status === 'active') { + logs.push(s.context); + } - return s; - } - }; - } + return s; + } + }; + } - const observableLogic = fromObservable(() => interval(10).pipe(take(4))); + const observableLogic = fromObservable(() => interval(10).pipe(take(4))); - const actor = createActor(withLogs(observableLogic)).start(); + const actor = createActor(withLogs(observableLogic)).start(); - actor.subscribe({ - complete: () => { - expect(logs).toEqual([0, 1, 2, 3]); - done(); - } - }); - }); + actor.subscribe({ + complete: () => { + expect(logs).toEqual([0, 1, 2, 3]); + resolve(); + } + }); + })); it('higher-level logic wrapping a machine should be able to persist a snapshot', () => { const logged: any[] = []; diff --git a/packages/core/test/after.test.ts b/packages/core/test/after.test.ts index 73c4357b2c..3295fc00fe 100644 --- a/packages/core/test/after.test.ts +++ b/packages/core/test/after.test.ts @@ -1,4 +1,4 @@ -import { sleep } from '@xstate-repo/jest-utils'; +import { setTimeout as sleep } from 'node:timers/promises'; import { createMachine, createActor, cancel } from '../src/index.ts'; const lightMachine = createMachine({ @@ -27,26 +27,26 @@ const lightMachine = createMachine({ }); afterEach(() => { - jest.useRealTimers(); + vi.useRealTimers(); }); describe('delayed transitions', () => { it('should transition after delay', () => { - jest.useFakeTimers(); + vi.useFakeTimers(); const actorRef = createActor(lightMachine).start(); expect(actorRef.getSnapshot().value).toBe('green'); - jest.advanceTimersByTime(500); + vi.advanceTimersByTime(500); expect(actorRef.getSnapshot().value).toBe('green'); - jest.advanceTimersByTime(510); + vi.advanceTimersByTime(510); expect(actorRef.getSnapshot().value).toBe('yellow'); }); it('should not try to clear an undefined timeout when exiting source state of a delayed transition', async () => { // https://github.com/statelyai/xstate/issues/5001 - const spy = jest.fn(); + const spy = vi.fn(); const machine = createMachine({ initial: 'green', @@ -85,77 +85,83 @@ describe('delayed transitions', () => { `); }); - it('should be able to transition with delay from nested initial state', (done) => { - const machine = createMachine({ - initial: 'nested', - states: { - nested: { - initial: 'wait', - states: { - wait: { - after: { - 10: '#end' + it('should be able to transition with delay from nested initial state', () => + new Promise((resolve) => { + const machine = createMachine({ + initial: 'nested', + states: { + nested: { + initial: 'wait', + states: { + wait: { + after: { + 10: '#end' + } } } + }, + end: { + id: 'end', + type: 'final' } - }, - end: { - id: 'end', - type: 'final' } - } - }); - - const actor = createActor(machine); - actor.subscribe({ - complete: () => { - done(); - } - }); - actor.start(); - }); + }); - it('parent state should enter child state without re-entering self (relative target)', (done) => { - const actual: string[] = []; - const machine = createMachine({ - initial: 'one', - states: { - one: { - initial: 'two', - entry: () => actual.push('entered one'), - states: { - two: { - entry: () => actual.push('entered two') + const actor = createActor(machine); + actor.subscribe({ + complete: () => { + resolve(); + } + }); + actor.start(); + })); + + it('parent state should enter child state without re-entering self (relative target)', () => + new Promise((resolve) => { + const actual: string[] = []; + const machine = createMachine({ + initial: 'one', + states: { + one: { + initial: 'two', + entry: () => actual.push('entered one'), + states: { + two: { + entry: () => actual.push('entered two') + }, + three: { + entry: () => actual.push('entered three'), + always: '#end' + } }, - three: { - entry: () => actual.push('entered three'), - always: '#end' + after: { + 10: '.three' } }, - after: { - 10: '.three' + end: { + id: 'end', + type: 'final' } - }, - end: { - id: 'end', - type: 'final' } - } - }); + }); - const actor = createActor(machine); - actor.subscribe({ - complete: () => { - expect(actual).toEqual(['entered one', 'entered two', 'entered three']); - done(); - } - }); - actor.start(); - }); + const actor = createActor(machine); + actor.subscribe({ + complete: () => { + expect(actual).toEqual([ + 'entered one', + 'entered two', + 'entered three' + ]); + resolve(); + } + }); + actor.start(); + })); it('should defer a single send event for a delayed conditional transition (#886)', () => { - jest.useFakeTimers(); - const spy = jest.fn(); + vi.useFakeTimers(); + const spy = vi.fn(); const machine = createMachine({ initial: 'X', states: { @@ -185,79 +191,83 @@ describe('delayed transitions', () => { createActor(machine).start(); - jest.advanceTimersByTime(10); + vi.advanceTimersByTime(10); expect(spy).not.toHaveBeenCalled(); }); // TODO: figure out correct behavior for restoring delayed transitions - it.skip('should execute an after transition after starting from a state resolved using `.getPersistedSnapshot`', (done) => { - const machine = createMachine({ - id: 'machine', - initial: 'a', - states: { - a: { - on: { next: 'withAfter' } - }, - - withAfter: { - after: { - 1: { target: 'done' } - } - }, - - done: { - type: 'final' - } - } - }); - - const actorRef1 = createActor(machine).start(); - actorRef1.send({ type: 'next' }); - const withAfterState = actorRef1.getPersistedSnapshot(); - - const actorRef2 = createActor(machine, { snapshot: withAfterState }); - actorRef2.subscribe({ complete: () => done() }); - actorRef2.start(); - }); - - it('should execute an after transition after starting from a persisted state', (done) => { - const createMyMachine = () => - createMachine({ - initial: 'A', + it.skip('should execute an after transition after starting from a state resolved using `.getPersistedSnapshot`', () => + new Promise((resolve) => { + const machine = createMachine({ + id: 'machine', + initial: 'a', states: { - A: { - on: { - NEXT: 'B' - } + a: { + on: { next: 'withAfter' } }, - B: { + + withAfter: { after: { - 1: 'C' + 1: { target: 'done' } } }, - C: { + + done: { type: 'final' } } }); - let service = createActor(createMyMachine()).start(); + const actorRef1 = createActor(machine).start(); + actorRef1.send({ type: 'next' }); + const withAfterState = actorRef1.getPersistedSnapshot(); - const persistedSnapshot = JSON.parse(JSON.stringify(service.getSnapshot())); + const actorRef2 = createActor(machine, { snapshot: withAfterState }); + actorRef2.subscribe({ complete: () => resolve() }); + actorRef2.start(); + })); - service = createActor(createMyMachine(), { - snapshot: persistedSnapshot - }).start(); + it('should execute an after transition after starting from a persisted state', () => + new Promise((resolve) => { + const createMyMachine = () => + createMachine({ + initial: 'A', + states: { + A: { + on: { + NEXT: 'B' + } + }, + B: { + after: { + 1: 'C' + } + }, + C: { + type: 'final' + } + } + }); - service.send({ type: 'NEXT' }); + let service = createActor(createMyMachine()).start(); - service.subscribe({ complete: () => done() }); - }); + const persistedSnapshot = JSON.parse( + JSON.stringify(service.getSnapshot()) + ); + + service = createActor(createMyMachine(), { + snapshot: persistedSnapshot + }).start(); + + service.send({ type: 'NEXT' }); + + service.subscribe({ complete: () => resolve() }); + })); describe('delay expressions', () => { it('should evaluate the expression (function) to determine the delay', () => { - jest.useFakeTimers(); - const spy = jest.fn(); + vi.useFakeTimers(); + const spy = vi.fn(); const context = { delay: 500 }; @@ -287,16 +297,16 @@ describe('delayed transitions', () => { expect(spy).toBeCalledWith(context); expect(actor.getSnapshot().value).toBe('inactive'); - jest.advanceTimersByTime(300); + vi.advanceTimersByTime(300); expect(actor.getSnapshot().value).toBe('inactive'); - jest.advanceTimersByTime(200); + vi.advanceTimersByTime(200); expect(actor.getSnapshot().value).toBe('active'); }); it('should evaluate the expression (string) to determine the delay', () => { - jest.useFakeTimers(); - const spy = jest.fn(); + vi.useFakeTimers(); + const spy = vi.fn(); const machine = createMachine( { initial: 'inactive', @@ -334,10 +344,10 @@ describe('delayed transitions', () => { expect(spy).toBeCalledWith(event); expect(actor.getSnapshot().value).toBe('active'); - jest.advanceTimersByTime(300); + vi.advanceTimersByTime(300); expect(actor.getSnapshot().value).toBe('active'); - jest.advanceTimersByTime(200); + vi.advanceTimersByTime(200); expect(actor.getSnapshot().value).toBe('inactive'); }); }); diff --git a/packages/core/test/assert.test.ts b/packages/core/test/assert.test.ts index 035f1d0d62..6cc054d600 100644 --- a/packages/core/test/assert.test.ts +++ b/packages/core/test/assert.test.ts @@ -1,100 +1,102 @@ import { createActor, createMachine, assertEvent } from '../src'; describe('assertion helpers', () => { - it('assertEvent asserts the correct event type', (done) => { - const machine = createMachine( - { - types: { - events: {} as - | { type: 'greet'; message: string } - | { type: 'count'; value: number } + it('assertEvent asserts the correct event type', () => + new Promise((resolve) => { + const machine = createMachine( + { + types: { + events: {} as + | { type: 'greet'; message: string } + | { type: 'count'; value: number } + }, + on: { + greet: { actions: 'greet' }, + count: { actions: 'greet' } + } }, - on: { - greet: { actions: 'greet' }, - count: { actions: 'greet' } - } - }, - { - actions: { - greet: ({ event }) => { - // @ts-expect-error - event.message; - - assertEvent(event, 'greet'); - event.message satisfies string; - - // @ts-expect-error - event.count; + { + actions: { + greet: ({ event }) => { + // @ts-expect-error + event.message; + + assertEvent(event, 'greet'); + event.message satisfies string; + + // @ts-expect-error + event.count; + } } } - } - ); - - const actor = createActor(machine); - - actor.subscribe({ - error(err) { - expect(err).toMatchInlineSnapshot( - `[Error: Expected event {"type":"count","value":42} to have type "greet"]` - ); - done(); - } - }); - - actor.start(); - - actor.send({ type: 'count', value: 42 }); - }); - - it('assertEvent asserts multiple event types', (done) => { - const machine = createMachine( - { - types: { - events: {} as - | { type: 'greet'; message: string } - | { type: 'notify'; message: string; level: 'info' | 'error' } - | { type: 'count'; value: number } - }, - on: { - greet: { actions: 'greet' }, - count: { actions: 'greet' } + ); + + const actor = createActor(machine); + + actor.subscribe({ + error(err) { + expect(err).toMatchInlineSnapshot( + `[Error: Expected event {"type":"count","value":42} to have type "greet"]` + ); + resolve(); } - }, - { - actions: { - greet: ({ event }) => { - // @ts-expect-error - event.message; + }); + + actor.start(); + + actor.send({ type: 'count', value: 42 }); + })); + + it('assertEvent asserts multiple event types', () => + new Promise((resolve) => { + const machine = createMachine( + { + types: { + events: {} as + | { type: 'greet'; message: string } + | { type: 'notify'; message: string; level: 'info' | 'error' } + | { type: 'count'; value: number } + }, + on: { + greet: { actions: 'greet' }, + count: { actions: 'greet' } + } + }, + { + actions: { + greet: ({ event }) => { + // @ts-expect-error + event.message; - assertEvent(event, ['greet', 'notify']); - event.message satisfies string; + assertEvent(event, ['greet', 'notify']); + event.message satisfies string; - // @ts-expect-error - event.level; + // @ts-expect-error + event.level; - assertEvent(event, ['notify']); - event.level satisfies 'info' | 'error'; + assertEvent(event, ['notify']); + event.level satisfies 'info' | 'error'; - // @ts-expect-error - event.count; + // @ts-expect-error + event.count; + } } } - } - ); + ); - const actor = createActor(machine); + const actor = createActor(machine); - actor.subscribe({ - error(err) { - expect(err).toMatchInlineSnapshot( - `[Error: Expected event {"type":"count","value":42} to have one of types "greet", "notify"]` - ); - done(); - } - }); + actor.subscribe({ + error(err) { + expect(err).toMatchInlineSnapshot( + `[Error: Expected event {"type":"count","value":42} to have one of types "greet", "notify"]` + ); + resolve(); + } + }); - actor.start(); + actor.start(); - actor.send({ type: 'count', value: 42 }); - }); + actor.send({ type: 'count', value: 42 }); + })); }); diff --git a/packages/core/test/assign.test.ts b/packages/core/test/assign.test.ts index b0e8055fb4..537f8bbc36 100644 --- a/packages/core/test/assign.test.ts +++ b/packages/core/test/assign.test.ts @@ -338,31 +338,32 @@ describe('assign meta', () => { expect(actor.getSnapshot().context.count).toEqual(11); }); - it('a parameterized action that resolves to assign() should be provided the params', (done) => { - const machine = createMachine( - { - on: { - EVENT: { - actions: { - type: 'inc', - params: { value: 5 } + it('a parameterized action that resolves to assign() should be provided the params', () => + new Promise((resolve) => { + const machine = createMachine( + { + on: { + EVENT: { + actions: { + type: 'inc', + params: { value: 5 } + } } } + }, + { + actions: { + inc: assign(({ context }, params) => { + expect(params).toEqual({ value: 5 }); + resolve(); + return context; + }) + } } - }, - { - actions: { - inc: assign(({ context }, params) => { - expect(params).toEqual({ value: 5 }); - done(); - return context; - }) - } - } - ); + ); - const service = createActor(machine).start(); + const service = createActor(machine).start(); - service.send({ type: 'EVENT' }); - }); + service.send({ type: 'EVENT' }); + })); }); diff --git a/packages/core/test/emit.test.ts b/packages/core/test/emit.test.ts index 5fb2365a6a..e83eec6797 100644 --- a/packages/core/test/emit.test.ts +++ b/packages/core/test/emit.test.ts @@ -165,7 +165,7 @@ describe('event emitter', () => { }); it('listener should be able to read the updated snapshot of the emitting actor', () => { - const spy = jest.fn(); + const spy = vi.fn(); const machine = createMachine({ initial: 'a', @@ -195,7 +195,7 @@ describe('event emitter', () => { }); it('wildcard listeners should be able to receive all emitted events', () => { - const spy = jest.fn(); + const spy = vi.fn(); const machine = setup({ types: { @@ -228,7 +228,7 @@ describe('event emitter', () => { }); it('events can be emitted from promise logic', () => { - const spy = jest.fn(); + const spy = vi.fn(); const logic = fromPromise( async ({ emit }) => { @@ -263,7 +263,7 @@ describe('event emitter', () => { }); it('events can be emitted from transition logic', () => { - const spy = jest.fn(); + const spy = vi.fn(); const logic = fromTransition< any, @@ -307,7 +307,7 @@ describe('event emitter', () => { }); it('events can be emitted from observable logic', () => { - const spy = jest.fn(); + const spy = vi.fn(); const logic = fromObservable( ({ emit }) => { @@ -350,7 +350,7 @@ describe('event emitter', () => { }); it('events can be emitted from event observable logic', () => { - const spy = jest.fn(); + const spy = vi.fn(); const logic = fromEventObservable< any, @@ -395,7 +395,7 @@ describe('event emitter', () => { }); it('events can be emitted from callback logic', () => { - const spy = jest.fn(); + const spy = vi.fn(); const logic = fromCallback( ({ emit }) => { @@ -430,7 +430,7 @@ describe('event emitter', () => { }); it('events can be emitted from callback logic (restored root)', () => { - const spy = jest.fn(); + const spy = vi.fn(); const logic = fromCallback( ({ emit }) => { diff --git a/packages/core/test/errors.test.ts b/packages/core/test/errors.test.ts index 0a6cb0d1a1..7285e23121 100644 --- a/packages/core/test/errors.test.ts +++ b/packages/core/test/errors.test.ts @@ -1,4 +1,4 @@ -import { sleep } from '@xstate-repo/jest-utils'; +import { setTimeout as sleep } from 'node:timers/promises'; import { assign, createActor, @@ -8,6 +8,19 @@ import { fromTransition } from '../src'; +// mocked reportUnhandledError due to unknown issue with vitest and global error +// handlers not catching thrown errors +// see: https://github.com/vitest-dev/vitest/issues/6292 +vi.mock('../src/reportUnhandledError.ts', () => { + return { + reportUnhandledError: (err: unknown) => { + setTimeout(() => { + dispatchEvent(new ErrorEvent('error', { error: err })); + }); + } + }; +}); + const cleanups: (() => void)[] = []; function installGlobalOnErrorHandler(handler: (ev: ErrorEvent) => void) { window.addEventListener('error', handler); @@ -21,193 +34,198 @@ afterEach(() => { describe('error handling', () => { // https://github.com/statelyai/xstate/issues/4004 - it('does not cause an infinite loop when an error is thrown in subscribe', (done) => { - const machine = createMachine({ - id: 'machine', - initial: 'initial', - context: { - count: 0 - }, - states: { - initial: { - on: { activate: 'active' } + it('does not cause an infinite loop when an error is thrown in subscribe', () => + new Promise((resolve) => { + const machine = createMachine({ + id: 'machine', + initial: 'initial', + context: { + count: 0 }, - active: {} - } - }); - - const spy = jest.fn().mockImplementation(() => { - throw new Error('no_infinite_loop_when_error_is_thrown_in_subscribe'); - }); - - const actor = createActor(machine).start(); + states: { + initial: { + on: { activate: 'active' } + }, + active: {} + } + }); - actor.subscribe(spy); - actor.send({ type: 'activate' }); + const spy = vi.fn().mockImplementation(() => { + throw new Error('no_infinite_loop_when_error_is_thrown_in_subscribe'); + }); - expect(spy).toHaveBeenCalledTimes(1); + const actor = createActor(machine).start(); - installGlobalOnErrorHandler((ev) => { - ev.preventDefault(); - expect(ev.error.message).toEqual( - 'no_infinite_loop_when_error_is_thrown_in_subscribe' - ); - done(); - }); - }); + actor.subscribe(spy); + actor.send({ type: 'activate' }); - it(`doesn't crash the actor when an error is thrown in subscribe`, (done) => { - const spy = jest.fn(); + expect(spy).toHaveBeenCalledTimes(1); - const machine = createMachine({ - id: 'machine', - initial: 'initial', - context: { - count: 0 - }, - states: { - initial: { - on: { activate: 'active' } + installGlobalOnErrorHandler((ev) => { + ev.preventDefault(); + expect(ev.error.message).toEqual( + 'no_infinite_loop_when_error_is_thrown_in_subscribe' + ); + resolve(); + }); + })); + + it(`doesn't crash the actor when an error is thrown in subscribe`, () => + new Promise((resolve) => { + const spy = vi.fn(); + + const machine = createMachine({ + id: 'machine', + initial: 'initial', + context: { + count: 0 }, - active: { - on: { - do: { - actions: spy + states: { + initial: { + on: { activate: 'active' } + }, + active: { + on: { + do: { + actions: spy + } } } } - } - }); + }); - const subscriber = jest.fn().mockImplementationOnce(() => { - throw new Error('doesnt_crash_actor_when_error_is_thrown_in_subscribe'); - }); + const subscriber = vi.fn().mockImplementationOnce(() => { + throw new Error('doesnt_crash_actor_when_error_is_thrown_in_subscribe'); + }); - const actor = createActor(machine).start(); + const actor = createActor(machine).start(); - actor.subscribe(subscriber); - actor.send({ type: 'activate' }); + actor.subscribe(subscriber); + actor.send({ type: 'activate' }); - expect(subscriber).toHaveBeenCalledTimes(1); - expect(actor.getSnapshot().status).toEqual('active'); + expect(subscriber).toHaveBeenCalledTimes(1); + expect(actor.getSnapshot().status).toEqual('active'); - installGlobalOnErrorHandler((ev) => { - ev.preventDefault(); - expect(ev.error.message).toEqual( - 'doesnt_crash_actor_when_error_is_thrown_in_subscribe' - ); + installGlobalOnErrorHandler((ev) => { + ev.preventDefault(); + expect(ev.error.message).toEqual( + 'doesnt_crash_actor_when_error_is_thrown_in_subscribe' + ); - actor.send({ type: 'do' }); - expect(spy).toHaveBeenCalledTimes(1); + actor.send({ type: 'do' }); + expect(spy).toHaveBeenCalledTimes(1); - done(); - }); - }); + resolve(); + }); + })); - it(`doesn't notify error listener when an error is thrown in subscribe`, (done) => { - const machine = createMachine({ - id: 'machine', - initial: 'initial', - context: { - count: 0 - }, - states: { - initial: { - on: { activate: 'active' } + it(`doesn't notify error listener when an error is thrown in subscribe`, () => + new Promise((resolve) => { + const machine = createMachine({ + id: 'machine', + initial: 'initial', + context: { + count: 0 }, - active: {} - } - }); - - const nextSpy = jest.fn().mockImplementation(() => { - throw new Error( - 'doesnt_notify_error_listener_when_error_is_thrown_in_subscribe' - ); - }); - const errorSpy = jest.fn(); - - const actor = createActor(machine).start(); - - actor.subscribe({ - next: nextSpy, - error: errorSpy - }); - actor.send({ type: 'activate' }); - - expect(nextSpy).toHaveBeenCalledTimes(1); - expect(errorSpy).toHaveBeenCalledTimes(0); - - installGlobalOnErrorHandler((ev) => { - ev.preventDefault(); - expect(ev.error.message).toEqual( - 'doesnt_notify_error_listener_when_error_is_thrown_in_subscribe' - ); - done(); - }); - }); - - it('unhandled sync errors thrown when starting a child actor should be reported globally', (done) => { - const machine = createMachine({ - initial: 'pending', - states: { - pending: { - invoke: { - src: fromCallback(() => { - throw new Error('unhandled_sync_error_in_actor_start'); - }), - onDone: 'success' + states: { + initial: { + on: { activate: 'active' } + }, + active: {} + } + }); + + const nextSpy = vi.fn().mockImplementation(() => { + throw new Error( + 'doesnt_notify_error_listener_when_error_is_thrown_in_subscribe' + ); + }); + const errorSpy = vi.fn(); + + const actor = createActor(machine).start(); + + actor.subscribe({ + next: nextSpy, + error: errorSpy + }); + actor.send({ type: 'activate' }); + + expect(nextSpy).toHaveBeenCalledTimes(1); + expect(errorSpy).toHaveBeenCalledTimes(0); + + installGlobalOnErrorHandler((ev) => { + ev.preventDefault(); + expect(ev.error.message).toEqual( + 'doesnt_notify_error_listener_when_error_is_thrown_in_subscribe' + ); + resolve(); + }); + })); + + it('unhandled sync errors thrown when starting a child actor should be reported globally', () => + new Promise((resolve) => { + const machine = createMachine({ + initial: 'pending', + states: { + pending: { + invoke: { + src: fromCallback(() => { + throw new Error('unhandled_sync_error_in_actor_start'); + }), + onDone: 'success' + } + }, + success: { + type: 'final' } - }, - success: { - type: 'final' } - } - }); - - createActor(machine).start(); - - installGlobalOnErrorHandler((ev) => { - ev.preventDefault(); - expect(ev.error.message).toEqual('unhandled_sync_error_in_actor_start'); - done(); - }); - }); - - it('unhandled rejection of a promise actor should be reported globally in absence of error listener', (done) => { - const machine = createMachine({ - initial: 'pending', - states: { - pending: { - invoke: { - src: fromPromise(() => - Promise.reject( - new Error( - 'unhandled_rejection_in_promise_actor_without_error_listener' + }); + + createActor(machine).start(); + + installGlobalOnErrorHandler((ev) => { + ev.preventDefault(); + expect(ev.error.message).toEqual('unhandled_sync_error_in_actor_start'); + resolve(); + }); + })); + + it('unhandled rejection of a promise actor should be reported globally in absence of error listener', () => + new Promise((resolve) => { + const machine = createMachine({ + initial: 'pending', + states: { + pending: { + invoke: { + src: fromPromise(() => + Promise.reject( + new Error( + 'unhandled_rejection_in_promise_actor_without_error_listener' + ) ) - ) - ), - onDone: 'success' + ), + onDone: 'success' + } + }, + success: { + type: 'final' } - }, - success: { - type: 'final' } - } - }); + }); - createActor(machine).start(); + createActor(machine).start(); - installGlobalOnErrorHandler((ev) => { - ev.preventDefault(); - expect(ev.error.message).toEqual( - 'unhandled_rejection_in_promise_actor_without_error_listener' - ); - done(); - }); - }); + installGlobalOnErrorHandler((ev) => { + ev.preventDefault(); + expect(ev.error.message).toEqual( + 'unhandled_rejection_in_promise_actor_without_error_listener' + ); + resolve(); + }); + })); it('unhandled rejection of a promise actor should be reported to the existing error listener of its parent', async () => { - const errorSpy = jest.fn(); + const errorSpy = vi.fn(); const machine = createMachine({ initial: 'pending', @@ -238,7 +256,7 @@ describe('error handling', () => { await sleep(0); - expect(errorSpy).toMatchMockCallsInlineSnapshot(` + expect(errorSpy.mock.calls).toMatchInlineSnapshot(` [ [ [Error: unhandled_rejection_in_promise_actor_with_parent_listener], @@ -248,7 +266,7 @@ describe('error handling', () => { }); it('unhandled rejection of a promise actor should be reported to the existing error listener of its grandparent', async () => { - const errorSpy = jest.fn(); + const errorSpy = vi.fn(); const child = createMachine({ initial: 'pending', @@ -294,7 +312,7 @@ describe('error handling', () => { await sleep(0); - expect(errorSpy).toMatchMockCallsInlineSnapshot(` + expect(errorSpy.mock.calls).toMatchInlineSnapshot(` [ [ [Error: unhandled_rejection_in_promise_actor_with_grandparent_listener], @@ -303,142 +321,146 @@ describe('error handling', () => { `); }); - it('handled sync errors thrown when starting a child actor should not be reported globally', (done) => { - const machine = createMachine({ - initial: 'pending', - states: { - pending: { - invoke: { - src: fromCallback(() => { - throw new Error('handled_sync_error_in_actor_start'); - }), - onError: 'failed' + it('handled sync errors thrown when starting a child actor should not be reported globally', () => + new Promise((resolve, reject) => { + const machine = createMachine({ + initial: 'pending', + states: { + pending: { + invoke: { + src: fromCallback(() => { + throw new Error('handled_sync_error_in_actor_start'); + }), + onError: 'failed' + } + }, + failed: { + type: 'final' } - }, - failed: { - type: 'final' } - } - }); - - createActor(machine).start(); - - installGlobalOnErrorHandler(() => { - done.fail(); - }); - - setTimeout(() => { - done(); - }, 10); - }); - - it('handled sync errors thrown when starting a child actor should be reported globally when not all of its own observers come with an error listener', (done) => { - const machine = createMachine({ - initial: 'pending', - states: { - pending: { - invoke: { - src: fromCallback(() => { - throw new Error('handled_sync_error_in_actor_start'); - }), - onError: 'failed' + }); + + createActor(machine).start(); + + installGlobalOnErrorHandler(() => { + reject(); + }); + + setTimeout(() => { + resolve(); + }, 10); + })); + + it('handled sync errors thrown when starting a child actor should be reported globally when not all of its own observers come with an error listener', () => + new Promise((resolve) => { + const machine = createMachine({ + initial: 'pending', + states: { + pending: { + invoke: { + src: fromCallback(() => { + throw new Error('handled_sync_error_in_actor_start'); + }), + onError: 'failed' + } + }, + failed: { + type: 'final' } - }, - failed: { - type: 'final' } - } - }); - - const actorRef = createActor(machine); - const childActorRef = Object.values(actorRef.getSnapshot().children)[0]; - childActorRef.subscribe({ - error: function preventUnhandledErrorListener() {} - }); - childActorRef.subscribe(() => {}); - actorRef.start(); - - installGlobalOnErrorHandler((ev) => { - ev.preventDefault(); - expect(ev.error.message).toEqual('handled_sync_error_in_actor_start'); - done(); - }); - }); - - it('handled sync errors thrown when starting a child actor should not be reported globally when all of its own observers come with an error listener', (done) => { - const machine = createMachine({ - initial: 'pending', - states: { - pending: { - invoke: { - src: fromCallback(() => { - throw new Error('handled_sync_error_in_actor_start'); - }), - onError: 'failed' + }); + + const actorRef = createActor(machine); + const childActorRef = Object.values(actorRef.getSnapshot().children)[0]; + childActorRef.subscribe({ + error: function preventUnhandledErrorListener() {} + }); + childActorRef.subscribe(() => {}); + actorRef.start(); + + installGlobalOnErrorHandler((ev) => { + ev.preventDefault(); + expect(ev.error.message).toEqual('handled_sync_error_in_actor_start'); + resolve(); + }); + })); + + it('handled sync errors thrown when starting a child actor should not be reported globally when all of its own observers come with an error listener', () => + new Promise((resolve, reject) => { + const machine = createMachine({ + initial: 'pending', + states: { + pending: { + invoke: { + src: fromCallback(() => { + throw new Error('handled_sync_error_in_actor_start'); + }), + onError: 'failed' + } + }, + failed: { + type: 'final' } - }, - failed: { - type: 'final' } - } - }); - - const actorRef = createActor(machine); - const childActorRef = Object.values(actorRef.getSnapshot().children)[0]; - childActorRef.subscribe({ - error: function preventUnhandledErrorListener() {} - }); - childActorRef.subscribe({ - error: function preventUnhandledErrorListener() {} - }); - actorRef.start(); - - installGlobalOnErrorHandler(() => { - done.fail(); - }); - - setTimeout(() => { - done(); - }, 10); - }); - - it('unhandled sync errors thrown when starting a child actor should be reported twice globally when not all of its own observers come with an error listener and when the root has no error listener of its own', (done) => { - const machine = createMachine({ - initial: 'pending', - states: { - pending: { - invoke: { - src: fromCallback(() => { - throw new Error('handled_sync_error_in_actor_start'); - }) + }); + + const actorRef = createActor(machine); + const childActorRef = Object.values(actorRef.getSnapshot().children)[0]; + childActorRef.subscribe({ + error: function preventUnhandledErrorListener() {} + }); + childActorRef.subscribe({ + error: function preventUnhandledErrorListener() {} + }); + actorRef.start(); + + installGlobalOnErrorHandler(() => { + reject(); + }); + + setTimeout(() => { + resolve(); + }, 10); + })); + + it('unhandled sync errors thrown when starting a child actor should be reported twice globally when not all of its own observers come with an error listener and when the root has no error listener of its own', () => + new Promise((resolve) => { + const machine = createMachine({ + initial: 'pending', + states: { + pending: { + invoke: { + src: fromCallback(() => { + throw new Error('handled_sync_error_in_actor_start'); + }) + } } } - } - }); - - const actorRef = createActor(machine); - const childActorRef = Object.values(actorRef.getSnapshot().children)[0]; - childActorRef.subscribe({ - error: function preventUnhandledErrorListener() {} - }); - childActorRef.subscribe({}); - actorRef.start(); - - const actual: string[] = []; - - installGlobalOnErrorHandler((ev) => { - ev.preventDefault(); - actual.push(ev.error.message); - - if (actual.length === 2) { - expect(actual).toEqual([ - 'handled_sync_error_in_actor_start', - 'handled_sync_error_in_actor_start' - ]); - done(); - } - }); - }); + }); + + const actorRef = createActor(machine); + const childActorRef = Object.values(actorRef.getSnapshot().children)[0]; + childActorRef.subscribe({ + error: function preventUnhandledErrorListener() {} + }); + childActorRef.subscribe({}); + actorRef.start(); + + const actual: string[] = []; + + installGlobalOnErrorHandler((ev) => { + ev.preventDefault(); + actual.push(ev.error.message); + + if (actual.length === 2) { + expect(actual).toEqual([ + 'handled_sync_error_in_actor_start', + 'handled_sync_error_in_actor_start' + ]); + resolve(); + } + }); + })); it(`handled sync errors shouldn't notify the error listener`, () => { const machine = createMachine({ @@ -458,7 +480,7 @@ describe('error handling', () => { } }); - const errorSpy = jest.fn(); + const errorSpy = vi.fn(); const actorRef = createActor(machine); actorRef.subscribe({ @@ -489,7 +511,7 @@ describe('error handling', () => { } }); - const errorSpy = jest.fn(); + const errorSpy = vi.fn(); const actorRef = createActor(machine); actorRef.subscribe({ @@ -497,7 +519,7 @@ describe('error handling', () => { }); actorRef.start(); - expect(errorSpy).toMatchMockCallsInlineSnapshot(` + expect(errorSpy.mock.calls).toMatchInlineSnapshot(` [ [ [Error: unhandled_sync_error_in_actor_start_with_root_error_listener], @@ -506,47 +528,48 @@ describe('error handling', () => { `); }); - it(`unhandled sync errors should not notify the global listener when the root error listener is present`, (done) => { - const machine = createMachine({ - initial: 'pending', - states: { - pending: { - invoke: { - src: fromCallback(() => { - throw new Error( - 'unhandled_sync_error_in_actor_start_with_root_error_listener' - ); - }), - onDone: 'success' + it(`unhandled sync errors should not notify the global listener when the root error listener is present`, () => + new Promise((resolve, reject) => { + const machine = createMachine({ + initial: 'pending', + states: { + pending: { + invoke: { + src: fromCallback(() => { + throw new Error( + 'unhandled_sync_error_in_actor_start_with_root_error_listener' + ); + }), + onDone: 'success' + } + }, + success: { + type: 'final' } - }, - success: { - type: 'final' } - } - }); + }); - const errorSpy = jest.fn(); + const errorSpy = vi.fn(); - const actorRef = createActor(machine); - actorRef.subscribe({ - error: errorSpy - }); - actorRef.start(); + const actorRef = createActor(machine); + actorRef.subscribe({ + error: errorSpy + }); + actorRef.start(); - expect(errorSpy).toHaveBeenCalledTimes(1); + expect(errorSpy).toHaveBeenCalledTimes(1); - installGlobalOnErrorHandler(() => { - done.fail(); - }); + installGlobalOnErrorHandler(() => { + reject(); + }); - setTimeout(() => { - done(); - }, 10); - }); + setTimeout(() => { + resolve(); + }, 10); + })); it(`handled sync errors thrown when starting an actor shouldn't crash the parent`, () => { - const spy = jest.fn(); + const spy = vi.fn(); const machine = createMachine({ initial: 'pending', @@ -578,133 +601,137 @@ describe('error handling', () => { expect(spy).toHaveBeenCalledTimes(1); }); - it(`unhandled sync errors thrown when starting an actor should crash the parent`, (done) => { - const machine = createMachine({ - initial: 'pending', - states: { - pending: { - invoke: { - src: fromCallback(() => { - throw new Error('unhandled_sync_error_in_actor_start'); - }) + it(`unhandled sync errors thrown when starting an actor should crash the parent`, () => + new Promise((resolve) => { + const machine = createMachine({ + initial: 'pending', + states: { + pending: { + invoke: { + src: fromCallback(() => { + throw new Error('unhandled_sync_error_in_actor_start'); + }) + } } } - } - }); - - const actorRef = createActor(machine); - actorRef.start(); - - expect(actorRef.getSnapshot().status).toBe('error'); - - installGlobalOnErrorHandler((ev) => { - ev.preventDefault(); - expect(ev.error.message).toEqual('unhandled_sync_error_in_actor_start'); - done(); - }); - }); - - it(`error thrown by the error listener should be reported globally`, (done) => { - const machine = createMachine({ - initial: 'pending', - states: { - pending: { - invoke: { - src: fromCallback(() => { - throw new Error('handled_sync_error_in_actor_start'); - }) + }); + + const actorRef = createActor(machine); + actorRef.start(); + + expect(actorRef.getSnapshot().status).toBe('error'); + + installGlobalOnErrorHandler((ev) => { + ev.preventDefault(); + expect(ev.error.message).toEqual('unhandled_sync_error_in_actor_start'); + resolve(); + }); + })); + + it(`error thrown by the error listener should be reported globally`, () => + new Promise((resolve) => { + const machine = createMachine({ + initial: 'pending', + states: { + pending: { + invoke: { + src: fromCallback(() => { + throw new Error('handled_sync_error_in_actor_start'); + }) + } } } - } - }); + }); - const actorRef = createActor(machine); - actorRef.subscribe({ - error: () => { - throw new Error('error_thrown_by_error_listener'); - } - }); - actorRef.start(); - - installGlobalOnErrorHandler((ev) => { - ev.preventDefault(); - expect(ev.error.message).toEqual('error_thrown_by_error_listener'); - done(); - }); - }); - - it(`error should be reported globally if not every observer comes with an error listener`, (done) => { - const machine = createMachine({ - initial: 'pending', - states: { - pending: { - invoke: { - src: fromCallback(() => { - throw new Error( - 'error_thrown_when_not_every_observer_comes_with_an_error_listener' - ); - }) + const actorRef = createActor(machine); + actorRef.subscribe({ + error: () => { + throw new Error('error_thrown_by_error_listener'); + } + }); + actorRef.start(); + + installGlobalOnErrorHandler((ev) => { + ev.preventDefault(); + expect(ev.error.message).toEqual('error_thrown_by_error_listener'); + resolve(); + }); + })); + + it(`error should be reported globally if not every observer comes with an error listener`, () => + new Promise((resolve) => { + const machine = createMachine({ + initial: 'pending', + states: { + pending: { + invoke: { + src: fromCallback(() => { + throw new Error( + 'error_thrown_when_not_every_observer_comes_with_an_error_listener' + ); + }) + } } } - } - }); - - const actorRef = createActor(machine); - actorRef.subscribe({ - error: function preventUnhandledErrorListener() {} - }); - actorRef.subscribe(() => {}); - actorRef.start(); - - installGlobalOnErrorHandler((ev) => { - ev.preventDefault(); - expect(ev.error.message).toEqual( - 'error_thrown_when_not_every_observer_comes_with_an_error_listener' - ); - done(); - }); - }); - - it(`uncaught error and an error thrown by the error listener should both be reported globally when not every observer comes with an error listener`, (done) => { - const machine = createMachine({ - initial: 'pending', - states: { - pending: { - invoke: { - src: fromCallback(() => { - throw new Error( - 'error_thrown_when_not_every_observer_comes_with_an_error_listener' - ); - }) + }); + + const actorRef = createActor(machine); + actorRef.subscribe({ + error: function preventUnhandledErrorListener() {} + }); + actorRef.subscribe(() => {}); + actorRef.start(); + + installGlobalOnErrorHandler((ev) => { + ev.preventDefault(); + expect(ev.error.message).toEqual( + 'error_thrown_when_not_every_observer_comes_with_an_error_listener' + ); + resolve(); + }); + })); + + it(`uncaught error and an error thrown by the error listener should both be reported globally when not every observer comes with an error listener`, () => + new Promise((resolve) => { + const machine = createMachine({ + initial: 'pending', + states: { + pending: { + invoke: { + src: fromCallback(() => { + throw new Error( + 'error_thrown_when_not_every_observer_comes_with_an_error_listener' + ); + }) + } } } - } - }); - - const actorRef = createActor(machine); - actorRef.subscribe({ - error: () => { - throw new Error('error_thrown_by_error_listener'); - } - }); - actorRef.subscribe(() => {}); - actorRef.start(); + }); - let actual: string[] = []; - - installGlobalOnErrorHandler((ev) => { - ev.preventDefault(); - actual.push(ev.error.message); - - if (actual.length === 2) { - expect(actual).toEqual([ - 'error_thrown_by_error_listener', - 'error_thrown_when_not_every_observer_comes_with_an_error_listener' - ]); - done(); - } - }); - }); + const actorRef = createActor(machine); + actorRef.subscribe({ + error: () => { + throw new Error('error_thrown_by_error_listener'); + } + }); + actorRef.subscribe(() => {}); + actorRef.start(); + + let actual: string[] = []; + + installGlobalOnErrorHandler((ev) => { + ev.preventDefault(); + actual.push(ev.error.message); + + if (actual.length === 2) { + expect(actual).toEqual([ + 'error_thrown_by_error_listener', + 'error_thrown_when_not_every_observer_comes_with_an_error_listener' + ]); + resolve(); + } + }); + })); it('error thrown in initial custom entry action should error the actor', () => { const machine = createMachine({ @@ -713,7 +740,7 @@ describe('error handling', () => { } }); - const errorSpy = jest.fn(); + const errorSpy = vi.fn(); const actorRef = createActor(machine); actorRef.subscribe({ @@ -726,7 +753,7 @@ describe('error handling', () => { expect(snapshot.error).toMatchInlineSnapshot( `[Error: error_thrown_in_initial_entry_action]` ); - expect(errorSpy).toMatchMockCallsInlineSnapshot(` + expect(errorSpy.mock.calls).toMatchInlineSnapshot(` [ [ [Error: error_thrown_in_initial_entry_action], @@ -742,7 +769,7 @@ describe('error handling', () => { }) }); - const errorSpy = jest.fn(); + const errorSpy = vi.fn(); const actorRef = createActor(machine); @@ -757,7 +784,7 @@ describe('error handling', () => { }); actorRef.start(); - expect(errorSpy).toMatchMockCallsInlineSnapshot(` + expect(errorSpy.mock.calls).toMatchInlineSnapshot(` [ [ [Error: error_thrown_when_resolving_initial_entry_action], @@ -785,7 +812,7 @@ describe('error handling', () => { } }); - const errorSpy = jest.fn(); + const errorSpy = vi.fn(); const actorRef = createActor(machine); actorRef.subscribe({ @@ -799,7 +826,7 @@ describe('error handling', () => { expect(snapshot.error).toMatchInlineSnapshot( `[Error: error_thrown_in_a_custom_entry_action_when_transitioning]` ); - expect(errorSpy).toMatchMockCallsInlineSnapshot(` + expect(errorSpy.mock.calls).toMatchInlineSnapshot(` [ [ [Error: error_thrown_in_a_custom_entry_action_when_transitioning], @@ -809,7 +836,7 @@ describe('error handling', () => { }); it(`shouldn't execute deferred initial actions that come after an action that errors`, () => { - const spy = jest.fn(); + const spy = vi.fn(); const machine = createMachine({ entry: [ @@ -860,7 +887,7 @@ describe('error handling', () => { }); it('should error when a guard throws when transitioning', () => { - const spy = jest.fn(); + const spy = vi.fn(); const machine = createMachine({ initial: 'a', states: { diff --git a/packages/core/test/event.test.ts b/packages/core/test/event.test.ts index 12d0964c6b..01808bfbd2 100644 --- a/packages/core/test/event.test.ts +++ b/packages/core/test/event.test.ts @@ -7,63 +7,64 @@ import { import { sendTo } from '../src/actions/send'; describe('events', () => { - it('should be able to respond to sender by sending self', (done) => { - const authServerMachine = createMachine({ - types: { - events: {} as { type: 'CODE'; sender: AnyActorRef } - }, - id: 'authServer', - initial: 'waitingForCode', - states: { - waitingForCode: { - on: { - CODE: { - actions: sendTo( - ({ event }) => { - expect(event.sender).toBeDefined(); - return event.sender; - }, - { type: 'TOKEN' }, - { delay: 10 } - ) + it('should be able to respond to sender by sending self', () => + new Promise((resolve) => { + const authServerMachine = createMachine({ + types: { + events: {} as { type: 'CODE'; sender: AnyActorRef } + }, + id: 'authServer', + initial: 'waitingForCode', + states: { + waitingForCode: { + on: { + CODE: { + actions: sendTo( + ({ event }) => { + expect(event.sender).toBeDefined(); + return event.sender; + }, + { type: 'TOKEN' }, + { delay: 10 } + ) + } } } } - } - }); + }); - const authClientMachine = createMachine({ - id: 'authClient', - initial: 'idle', - states: { - idle: { - on: { AUTH: 'authorizing' } - }, - authorizing: { - invoke: { - id: 'auth-server', - src: authServerMachine + const authClientMachine = createMachine({ + id: 'authClient', + initial: 'idle', + states: { + idle: { + on: { AUTH: 'authorizing' } }, - entry: sendTo('auth-server', ({ self }) => ({ - type: 'CODE', - sender: self - })), - on: { - TOKEN: 'authorized' + authorizing: { + invoke: { + id: 'auth-server', + src: authServerMachine + }, + entry: sendTo('auth-server', ({ self }) => ({ + type: 'CODE', + sender: self + })), + on: { + TOKEN: 'authorized' + } + }, + authorized: { + type: 'final' } - }, - authorized: { - type: 'final' } - } - }); + }); - const service = createActor(authClientMachine); - service.subscribe({ complete: () => done() }); - service.start(); + const service = createActor(authClientMachine); + service.subscribe({ complete: () => resolve() }); + service.start(); - service.send({ type: 'AUTH' }); - }); + service.send({ type: 'AUTH' }); + })); }); describe('nested transitions', () => { diff --git a/packages/core/test/eventDescriptors.test.ts b/packages/core/test/eventDescriptors.test.ts index e6a9c1c5a7..bd15793ca8 100644 --- a/packages/core/test/eventDescriptors.test.ts +++ b/packages/core/test/eventDescriptors.test.ts @@ -239,6 +239,8 @@ describe('event descriptors', () => { }); it('should not match infix wildcards', () => { + const warnSpy = vi.spyOn(console, 'warn'); + const machine = createMachine({ initial: 'start', states: { @@ -260,7 +262,7 @@ describe('event descriptors', () => { expect(actorRef1.getSnapshot().matches('success')).toBeFalsy(); - expect(console.warn).toMatchMockCallsInlineSnapshot(` + expect(warnSpy.mock.calls).toMatchInlineSnapshot(` [ [ "Wildcards can only be the last token of an event descriptor (e.g., "event.*") or the entire event descriptor ("*"). Check the "event.*.bar.*" event.", @@ -276,6 +278,7 @@ describe('event descriptors', () => { ], ] `); + warnSpy.mockClear(); const actorRef2 = createActor(machine).start(); @@ -283,7 +286,7 @@ describe('event descriptors', () => { expect(actorRef2.getSnapshot().matches('success')).toBeFalsy(); - expect(console.warn).toMatchMockCallsInlineSnapshot(` + expect(warnSpy.mock.calls).toMatchInlineSnapshot(` [ [ "Wildcards can only be the last token of an event descriptor (e.g., "event.*") or the entire event descriptor ("*"). Check the "event.*.bar.*" event.", @@ -299,6 +302,8 @@ describe('event descriptors', () => { }); it('should not match wildcards as part of tokens', () => { + const warnSpy = vi.spyOn(console, 'warn'); + const machine = createMachine({ initial: 'start', states: { @@ -320,7 +325,7 @@ describe('event descriptors', () => { expect(actorRef1.getSnapshot().matches('success')).toBeFalsy(); - expect(console.warn).toMatchMockCallsInlineSnapshot(` + expect(warnSpy.mock.calls).toMatchInlineSnapshot(` [ [ "Wildcards can only be the last token of an event descriptor (e.g., "event.*") or the entire event descriptor ("*"). Check the "event*.bar.*" event.", @@ -330,6 +335,7 @@ describe('event descriptors', () => { ], ] `); + warnSpy.mockClear(); const actorRef2 = createActor(machine).start(); @@ -337,7 +343,7 @@ describe('event descriptors', () => { expect(actorRef2.getSnapshot().matches('success')).toBeFalsy(); - expect(console.warn).toMatchMockCallsInlineSnapshot(` + expect(warnSpy.mock.calls).toMatchInlineSnapshot(` [ [ "Wildcards can only be the last token of an event descriptor (e.g., "event.*") or the entire event descriptor ("*"). Check the "event*.bar.*" event.", diff --git a/packages/core/test/final.test.ts b/packages/core/test/final.test.ts index 8f5d45fa1d..3e0e32ebd7 100644 --- a/packages/core/test/final.test.ts +++ b/packages/core/test/final.test.ts @@ -15,7 +15,7 @@ describe('final states', () => { expect(actorRef.getSnapshot().status).toBe('done'); }); it('output of a machine with a root state being final should be called with a "xstate.done.state.ROOT_ID" event', () => { - const spy = jest.fn(); + const spy = vi.fn(); const machine = createMachine({ type: 'final', output: ({ event }) => { @@ -36,7 +36,7 @@ describe('final states', () => { `); }); it('should emit the "xstate.done.state.*" event when all nested states are in their final states', () => { - const onDoneSpy = jest.fn(); + const onDoneSpy = vi.fn(); const machine = createMachine({ id: 'm', @@ -125,64 +125,65 @@ describe('final states', () => { expect(actual).toEqual(['bazAction', 'barAction', 'fooAction']); }); - it('should call output expressions on nested final nodes', (done) => { - interface Ctx { - revealedSecret?: string; - } + it('should call output expressions on nested final nodes', () => + new Promise((resolve) => { + interface Ctx { + revealedSecret?: string; + } - const machine = createMachine({ - types: {} as { context: Ctx }, - initial: 'secret', - context: { - revealedSecret: undefined - }, - states: { - secret: { - initial: 'wait', - states: { - wait: { - on: { - REQUEST_SECRET: 'reveal' + const machine = createMachine({ + types: {} as { context: Ctx }, + initial: 'secret', + context: { + revealedSecret: undefined + }, + states: { + secret: { + initial: 'wait', + states: { + wait: { + on: { + REQUEST_SECRET: 'reveal' + } + }, + reveal: { + type: 'final', + output: () => ({ + secret: 'the secret' + }) } }, - reveal: { - type: 'final', - output: () => ({ - secret: 'the secret' + onDone: { + target: 'success', + actions: assign({ + revealedSecret: ({ event }) => { + return (event.output as any).secret; + } }) } }, - onDone: { - target: 'success', - actions: assign({ - revealedSecret: ({ event }) => { - return (event.output as any).secret; - } - }) + success: { + type: 'final' } - }, - success: { - type: 'final' } - } - }); - - const service = createActor(machine); - service.subscribe({ - complete: () => { - expect(service.getSnapshot().context).toEqual({ - revealedSecret: 'the secret' - }); - done(); - } - }); - service.start(); + }); + + const service = createActor(machine); + service.subscribe({ + complete: () => { + expect(service.getSnapshot().context).toEqual({ + revealedSecret: 'the secret' + }); + resolve(); + } + }); + service.start(); - service.send({ type: 'REQUEST_SECRET' }); - }); + service.send({ type: 'REQUEST_SECRET' }); + })); it("should only call data expression once when entering root's final state", () => { - const spy = jest.fn(); + const spy = vi.fn(); const machine = createMachine({ initial: 'start', states: { @@ -224,7 +225,7 @@ describe('final states', () => { }); it('state output should be able to use context updated by the entry action of the reached final state', () => { - const spy = jest.fn(); + const spy = vi.fn(); const machine = createMachine({ context: { count: 0 @@ -577,7 +578,7 @@ describe('final states', () => { expect(actorRef.getSnapshot().status).toEqual('done'); }); it('root output should be called with a "xstate.done.state.*" event of the parallel root when a direct final child of that parallel root is reached', () => { - const spy = jest.fn(); + const spy = vi.fn(); const machine = createMachine({ type: 'parallel', states: { @@ -605,7 +606,7 @@ describe('final states', () => { }); it('root output should be called with a "xstate.done.state.*" event of the parallel root when a final child of its compound child is reached', () => { - const spy = jest.fn(); + const spy = vi.fn(); const machine = createMachine({ type: 'parallel', states: { @@ -638,7 +639,7 @@ describe('final states', () => { }); it('root output should be called with a "xstate.done.state.*" event of the parallel root when a final descendant is reached 2 parallel levels deep', () => { - const spy = jest.fn(); + const spy = vi.fn(); const machine = createMachine({ type: 'parallel', states: { @@ -676,7 +677,7 @@ describe('final states', () => { }); it('onDone of an outer parallel state should be called with its own "xstate.done.state.*" event when its direct parallel child completes', () => { - const spy = jest.fn(); + const spy = vi.fn(); const machine = createMachine({ initial: 'a', states: { @@ -720,7 +721,7 @@ describe('final states', () => { }); it('onDone should not be called when the machine reaches its final state', () => { - const spy = jest.fn(); + const spy = vi.fn(); const machine = createMachine({ type: 'parallel', states: { @@ -754,7 +755,7 @@ describe('final states', () => { }); it('machine should not complete when a parallel child of a compound state completes', () => { - const spy = jest.fn(); + const spy = vi.fn(); const machine = createMachine({ initial: 'a', states: { @@ -780,7 +781,7 @@ describe('final states', () => { }); it('root output should only be called once when multiple parallel regions complete at once', () => { - const spy = jest.fn(); + const spy = vi.fn(); const machine = createMachine({ type: 'parallel', @@ -801,7 +802,7 @@ describe('final states', () => { }); it('onDone of a parallel state should only be called once when multiple parallel regions complete at once', () => { - const spy = jest.fn(); + const spy = vi.fn(); const machine = createMachine({ initial: 'a', @@ -1083,7 +1084,7 @@ describe('final states', () => { }); it('should not resolve output of a final state if its parent is a parallel state', () => { - const spy = jest.fn(); + const spy = vi.fn(); const machine = createMachine({ initial: 'A', @@ -1112,7 +1113,7 @@ describe('final states', () => { }); it('should only call exit actions once when a child machine reaches its final state and sends an event to its parent that ends up stopping that child', () => { - const spy = jest.fn(); + const spy = vi.fn(); const child = createMachine({ initial: 'start', diff --git a/packages/core/test/getNextSnapshot.test.ts b/packages/core/test/getNextSnapshot.test.ts index 76759391d2..c30b70ac8f 100644 --- a/packages/core/test/getNextSnapshot.test.ts +++ b/packages/core/test/getNextSnapshot.test.ts @@ -52,7 +52,7 @@ describe('getNextSnapshot', () => { expect(s2.value).toEqual('c'); }); it('should not execute actions', () => { - const fn = jest.fn(); + const fn = vi.fn(); const machine = createMachine({ initial: 'a', diff --git a/packages/core/test/guards.test.ts b/packages/core/test/guards.test.ts index 1fdadf4e5f..aa29df2c1a 100644 --- a/packages/core/test/guards.test.ts +++ b/packages/core/test/guards.test.ts @@ -239,7 +239,7 @@ describe('guard conditions', () => { } }); - const errorSpy = jest.fn(); + const errorSpy = vi.fn(); const actorRef = createActor(machine); actorRef.subscribe({ @@ -249,7 +249,7 @@ describe('guard conditions', () => { actorRef.send({ type: 'BAD_COND' }); - expect(errorSpy).toMatchMockCallsInlineSnapshot(` + expect(errorSpy.mock.calls).toMatchInlineSnapshot(` [ [ [Error: Unable to evaluate guard 'doesNotExist' in transition for event 'BAD_COND' in state node '(machine).foo': @@ -472,7 +472,7 @@ describe('custom guards', () => { }); it('should provide the undefined params if a guard was configured using a string', () => { - const spy = jest.fn(); + const spy = vi.fn(); const machine = createMachine( { @@ -499,7 +499,7 @@ describe('custom guards', () => { }); it('should provide the guard with resolved params when they are dynamic', () => { - const spy = jest.fn(); + const spy = vi.fn(); const machine = createMachine( { @@ -528,7 +528,7 @@ describe('custom guards', () => { }); it('should resolve dynamic params using context value', () => { - const spy = jest.fn(); + const spy = vi.fn(); const machine = createMachine( { @@ -563,7 +563,7 @@ describe('custom guards', () => { }); it('should resolve dynamic params using event value', () => { - const spy = jest.fn(); + const spy = vi.fn(); const machine = createMachine( { @@ -596,7 +596,7 @@ describe('custom guards', () => { }); it('should call a referenced `not` guard that embeds an inline function guard with undefined params', () => { - const spy = jest.fn(); + const spy = vi.fn(); const machine = createMachine( { @@ -630,7 +630,7 @@ describe('custom guards', () => { }); it('should call a string guard referenced by referenced `not` with undefined params', () => { - const spy = jest.fn(); + const spy = vi.fn(); const machine = createMachine( { @@ -662,7 +662,7 @@ describe('custom guards', () => { }); it('should call an object guard referenced by referenced `not` with its own params', () => { - const spy = jest.fn(); + const spy = vi.fn(); const machine = createMachine( { @@ -697,7 +697,7 @@ describe('custom guards', () => { }); it('should call an inline function guard embedded in referenced `and` with undefined params', () => { - const spy = jest.fn(); + const spy = vi.fn(); const machine = createMachine( { @@ -732,7 +732,7 @@ describe('custom guards', () => { }); it('should call a string guard referenced by referenced `and` with undefined params', () => { - const spy = jest.fn(); + const spy = vi.fn(); const machine = createMachine( { @@ -764,7 +764,7 @@ describe('custom guards', () => { }); it('should call an object guard referenced by referenced `and` with its own params', () => { - const spy = jest.fn(); + const spy = vi.fn(); const machine = createMachine( { @@ -804,7 +804,7 @@ describe('custom guards', () => { describe('referencing guards', () => { it('guard should be checked when referenced by a string', () => { - const spy = jest.fn(); + const spy = vi.fn(); const machine = createMachine( { on: { @@ -832,7 +832,7 @@ describe('referencing guards', () => { }); it('guard should be checked when referenced by a parametrized guard object', () => { - const spy = jest.fn(); + const spy = vi.fn(); const machine = createMachine( { on: { @@ -875,7 +875,7 @@ describe('referencing guards', () => { } }); - const errorSpy = jest.fn(); + const errorSpy = vi.fn(); const actorRef = createActor(machine); actorRef.subscribe({ @@ -884,7 +884,7 @@ describe('referencing guards', () => { actorRef.start(); actorRef.send({ type: 'EVENT' }); - expect(errorSpy).toMatchMockCallsInlineSnapshot(` + expect(errorSpy.mock.calls).toMatchInlineSnapshot(` [ [ [Error: Unable to evaluate guard 'missing-predicate' in transition for event 'EVENT' in state node 'invalid-predicate.active': @@ -1130,7 +1130,7 @@ describe('not() guard', () => { }); it('should evaluate dynamic params of the referenced guard', () => { - const spy = jest.fn(); + const spy = vi.fn(); const machine = createMachine( { @@ -1158,7 +1158,7 @@ describe('not() guard', () => { const actorRef = createActor(machine).start(); actorRef.send({ type: 'EV', secret: 42 }); - expect(spy).toMatchMockCallsInlineSnapshot(` + expect(spy.mock.calls).toMatchInlineSnapshot(` [ [ { @@ -1297,7 +1297,7 @@ describe('and() guard', () => { }); it('should evaluate dynamic params of the referenced guard', () => { - const spy = jest.fn(); + const spy = vi.fn(); const machine = createMachine( { @@ -1328,7 +1328,7 @@ describe('and() guard', () => { const actorRef = createActor(machine).start(); actorRef.send({ type: 'EV', secret: 42 }); - expect(spy).toMatchMockCallsInlineSnapshot(` + expect(spy.mock.calls).toMatchInlineSnapshot(` [ [ { @@ -1468,7 +1468,7 @@ describe('or() guard', () => { }); it('should evaluate dynamic params of the referenced guard', () => { - const spy = jest.fn(); + const spy = vi.fn(); const machine = createMachine( { @@ -1499,7 +1499,7 @@ describe('or() guard', () => { const actorRef = createActor(machine).start(); actorRef.send({ type: 'EV', secret: 42 }); - expect(spy).toMatchMockCallsInlineSnapshot(` + expect(spy.mock.calls).toMatchInlineSnapshot(` [ [ { diff --git a/packages/core/test/history.test.ts b/packages/core/test/history.test.ts index 4a99b205d7..eaec14e43a 100644 --- a/packages/core/test/history.test.ts +++ b/packages/core/test/history.test.ts @@ -270,7 +270,7 @@ describe('history states', () => { }); it('should execute actions of the initial transition when a history state without a default target is targeted and its parent state was never visited yet', () => { - const spy = jest.fn(); + const spy = vi.fn(); const machine = createMachine({ initial: 'a', @@ -301,7 +301,7 @@ describe('history states', () => { }); it('should not execute actions of the initial transition when a history state with a default target is targeted and its parent state was never visited yet', () => { - const spy = jest.fn(); + const spy = vi.fn(); const machine = createMachine({ initial: 'a', states: { @@ -333,7 +333,7 @@ describe('history states', () => { }); it('should execute entry actions of a parent of the targeted history state when its parent state was never visited yet', () => { - const spy = jest.fn(); + const spy = vi.fn(); const machine = createMachine({ initial: 'a', states: { @@ -363,7 +363,7 @@ describe('history states', () => { }); it('should execute actions of the initial transition when it select a history state as the initial state of its parent', () => { - const spy = jest.fn(); + const spy = vi.fn(); const machine = createMachine({ initial: 'a', states: { @@ -394,7 +394,7 @@ describe('history states', () => { }); it('should execute actions of the initial transition when a history state without a default target is targeted and its parent state was already visited', () => { - const spy = jest.fn(); + const spy = vi.fn(); const machine = createMachine({ initial: 'a', @@ -432,7 +432,7 @@ describe('history states', () => { }); it('should not execute actions of the initial transition when a history state with a default target is targeted and its parent state was already visited', () => { - const spy = jest.fn(); + const spy = vi.fn(); const machine = createMachine({ initial: 'a', states: { @@ -471,7 +471,7 @@ describe('history states', () => { }); it('should execute entry actions of a parent of the targeted history state when its parent state was already visited', () => { - const spy = jest.fn(); + const spy = vi.fn(); const machine = createMachine({ initial: 'a', states: { @@ -508,7 +508,7 @@ describe('history states', () => { }); it('should invoke an actor when reentering the stored configuration through the history state', () => { - const spy = jest.fn(); + const spy = vi.fn(); const machine = createMachine({ initial: 'running', diff --git a/packages/core/test/input.test.ts b/packages/core/test/input.test.ts index d1e658beb9..0d82d6a147 100644 --- a/packages/core/test/input.test.ts +++ b/packages/core/test/input.test.ts @@ -10,7 +10,7 @@ import { describe('input', () => { it('should create a machine with input', () => { - const spy = jest.fn(); + const spy = vi.fn(); const machine = createMachine({ types: {} as { @@ -30,16 +30,17 @@ describe('input', () => { expect(spy).toHaveBeenCalledWith(42); }); - it('initial event should have input property', (done) => { - const machine = createMachine({ - entry: ({ event }) => { - expect(event.input.greeting).toBe('hello'); - done(); - } - }); + it('initial event should have input property', () => + new Promise((resolve) => { + const machine = createMachine({ + entry: ({ event }) => { + expect(event.input.greeting).toBe('hello'); + resolve(); + } + }); - createActor(machine, { input: { greeting: 'hello' } }).start(); - }); + createActor(machine, { input: { greeting: 'hello' } }).start(); + })); it('should error if input is expected but not provided', () => { const machine = createMachine({ @@ -69,56 +70,58 @@ describe('input', () => { }).not.toThrowError(); }); - it('should provide input data to invoked machines', (done) => { - const invokedMachine = createMachine({ - types: {} as { - input: { greeting: string }; - context: { greeting: string }; - }, - context: ({ input }) => input, - entry: ({ context, event }) => { - expect(context.greeting).toBe('hello'); - expect(event.input.greeting).toBe('hello'); - done(); - } - }); + it('should provide input data to invoked machines', () => + new Promise((resolve) => { + const invokedMachine = createMachine({ + types: {} as { + input: { greeting: string }; + context: { greeting: string }; + }, + context: ({ input }) => input, + entry: ({ context, event }) => { + expect(context.greeting).toBe('hello'); + expect(event.input.greeting).toBe('hello'); + resolve(); + } + }); - const machine = createMachine({ - invoke: { - src: invokedMachine, - input: { greeting: 'hello' } - } - }); + const machine = createMachine({ + invoke: { + src: invokedMachine, + input: { greeting: 'hello' } + } + }); - createActor(machine).start(); - }); + createActor(machine).start(); + })); - it('should provide input data to spawned machines', (done) => { - const spawnedMachine = createMachine({ - types: {} as { - input: { greeting: string }; - context: { greeting: string }; - }, - context({ input }) { - return input; - }, - entry: ({ context, event }) => { - expect(context.greeting).toBe('hello'); - expect(event.input.greeting).toBe('hello'); - done(); - } - }); + it('should provide input data to spawned machines', () => + new Promise((resolve) => { + const spawnedMachine = createMachine({ + types: {} as { + input: { greeting: string }; + context: { greeting: string }; + }, + context({ input }) { + return input; + }, + entry: ({ context, event }) => { + expect(context.greeting).toBe('hello'); + expect(event.input.greeting).toBe('hello'); + resolve(); + } + }); - const machine = createMachine({ - entry: assign(({ spawn }) => { - return { - ref: spawn(spawnedMachine, { input: { greeting: 'hello' } }) - }; - }) - }); + const machine = createMachine({ + entry: assign(({ spawn }) => { + return { + ref: spawn(spawnedMachine, { input: { greeting: 'hello' } }) + }; + }) + }); - createActor(machine).start(); - }); + createActor(machine).start(); + })); it('should create a promise with input', async () => { const promiseLogic = fromPromise<{ count: number }, { count: number }>( @@ -147,39 +150,41 @@ describe('input', () => { expect(transitionActor.getSnapshot().context).toEqual({ count: 42 }); }); - it('should create an observable actor with input', (done) => { - const observableLogic = fromObservable< - { count: number }, - { count: number } - >(({ input }) => of(input)); - - const observableActor = createActor(observableLogic, { - input: { count: 42 } - }); - - const sub = observableActor.subscribe((state) => { - if (state.context?.count !== 42) return; - expect(state.context).toEqual({ count: 42 }); - done(); - sub.unsubscribe(); - }); - - observableActor.start(); - }); - - it('should create a callback actor with input', (done) => { - const callbackLogic = fromCallback(({ input }) => { - expect(input).toEqual({ count: 42 }); - done(); - }); - - createActor(callbackLogic, { - input: { count: 42 } - }).start(); - }); + it('should create an observable actor with input', () => + new Promise((resolve) => { + const observableLogic = fromObservable< + { count: number }, + { count: number } + >(({ input }) => of(input)); + + const observableActor = createActor(observableLogic, { + input: { count: 42 } + }); + + const sub = observableActor.subscribe((state) => { + if (state.context?.count !== 42) return; + expect(state.context).toEqual({ count: 42 }); + resolve(); + sub.unsubscribe(); + }); + + observableActor.start(); + })); + + it('should create a callback actor with input', () => + new Promise((resolve) => { + const callbackLogic = fromCallback(({ input }) => { + expect(input).toEqual({ count: 42 }); + resolve(); + }); + + createActor(callbackLogic, { + input: { count: 42 } + }).start(); + })); it('should provide a static inline input to the referenced actor', () => { - const spy = jest.fn(); + const spy = vi.fn(); const child = createMachine({ context: ({ input }: { input: number }) => { @@ -211,7 +216,7 @@ describe('input', () => { }); it('should provide a dynamic inline input to the referenced actor', () => { - const spy = jest.fn(); + const spy = vi.fn(); const child = createMachine({ context: ({ input }: { input: number }) => { @@ -255,7 +260,7 @@ describe('input', () => { }); it('should call the input factory with self when invoking', () => { - const spy = jest.fn(); + const spy = vi.fn(); const machine = createMachine({ invoke: { @@ -270,7 +275,7 @@ describe('input', () => { }); it('should call the input factory with self when spawning', () => { - const spy = jest.fn(); + const spy = vi.fn(); const machine = createMachine( { diff --git a/packages/core/test/internalTransitions.test.ts b/packages/core/test/internalTransitions.test.ts index 4c77391172..780e3eade2 100644 --- a/packages/core/test/internalTransitions.test.ts +++ b/packages/core/test/internalTransitions.test.ts @@ -169,7 +169,7 @@ describe('internal transitions', () => { }); it('should work with targetless transitions (in conditional array)', () => { - const spy = jest.fn(); + const spy = vi.fn(); const machine = createMachine({ initial: 'foo', states: { @@ -188,7 +188,7 @@ describe('internal transitions', () => { }); it('should work with targetless transitions (in object)', () => { - const spy = jest.fn(); + const spy = vi.fn(); const machine = createMachine({ initial: 'foo', states: { @@ -207,7 +207,7 @@ describe('internal transitions', () => { }); it('should work on parent with targetless transitions (in conditional array)', () => { - const spy = jest.fn(); + const spy = vi.fn(); const machine = createMachine({ on: { TARGETLESS_ARRAY: [{ actions: [spy] }] @@ -223,7 +223,7 @@ describe('internal transitions', () => { }); it('should work on parent with targetless transitions (in object)', () => { - const spy = jest.fn(); + const spy = vi.fn(); const machine = createMachine({ on: { TARGETLESS_OBJECT: { actions: [spy] } diff --git a/packages/core/test/interpreter.test.ts b/packages/core/test/interpreter.test.ts index eacb255e31..6f20aac201 100644 --- a/packages/core/test/interpreter.test.ts +++ b/packages/core/test/interpreter.test.ts @@ -61,49 +61,50 @@ describe('interpreter', () => { expect(service.getSnapshot().value).toEqual('foo'); }); - it('initially spawned actors should not be spawned when reading initial state', (done) => { - let promiseSpawned = 0; + it('initially spawned actors should not be spawned when reading initial state', () => + new Promise((resolve) => { + let promiseSpawned = 0; - const machine = createMachine({ - initial: 'idle', - context: { - actor: undefined! as ActorRefFrom> - }, - states: { - idle: { - entry: assign({ - actor: ({ spawn }) => { - return spawn( - fromPromise( - () => - new Promise(() => { - promiseSpawned++; - }) - ) - ); - } - }) + const machine = createMachine({ + initial: 'idle', + context: { + actor: undefined! as ActorRefFrom> + }, + states: { + idle: { + entry: assign({ + actor: ({ spawn }) => { + return spawn( + fromPromise( + () => + new Promise(() => { + promiseSpawned++; + }) + ) + ); + } + }) + } } - } - }); + }); - const service = createActor(machine); + const service = createActor(machine); - expect(promiseSpawned).toEqual(0); + expect(promiseSpawned).toEqual(0); - service.getSnapshot(); - service.getSnapshot(); - service.getSnapshot(); + service.getSnapshot(); + service.getSnapshot(); + service.getSnapshot(); - expect(promiseSpawned).toEqual(0); + expect(promiseSpawned).toEqual(0); - service.start(); + service.start(); - setTimeout(() => { - expect(promiseSpawned).toEqual(1); - done(); - }, 100); - }); + setTimeout(() => { + expect(promiseSpawned).toEqual(1); + resolve(); + }, 100); + })); it('does not execute actions from a restored state', () => { let called = false; @@ -179,7 +180,7 @@ describe('interpreter', () => { }); it('should not notify subscribers of the current state upon subscription (subscribe)', () => { - const spy = jest.fn(); + const spy = vi.fn(); const service = createActor(machine).start(); service.subscribe(spy); @@ -365,83 +366,84 @@ describe('interpreter', () => { expect(stopped).toBe(true); }); - it('can send an event after a delay (delayed transitions)', (done) => { - const clock = new SimulatedClock(); - const letterMachine = createMachine( - { - types: {} as { - events: { type: 'FIRE_DELAY'; value: number }; - }, - id: 'letter', - context: { - delay: 100 - }, - initial: 'a', - states: { - a: { - after: { - delayA: 'b' - } + it('can send an event after a delay (delayed transitions)', () => + new Promise((resolve) => { + const clock = new SimulatedClock(); + const letterMachine = createMachine( + { + types: {} as { + events: { type: 'FIRE_DELAY'; value: number }; }, - b: { - after: { - someDelay: 'c' - } - }, - c: { - entry: raise({ type: 'FIRE_DELAY', value: 200 }, { delay: 20 }), - on: { - FIRE_DELAY: 'd' - } + id: 'letter', + context: { + delay: 100 }, - d: { - after: { - delayD: 'e' + initial: 'a', + states: { + a: { + after: { + delayA: 'b' + } + }, + b: { + after: { + someDelay: 'c' + } + }, + c: { + entry: raise({ type: 'FIRE_DELAY', value: 200 }, { delay: 20 }), + on: { + FIRE_DELAY: 'd' + } + }, + d: { + after: { + delayD: 'e' + } + }, + e: { + after: { someDelay: 'f' } + }, + f: { + type: 'final' } - }, - e: { - after: { someDelay: 'f' } - }, - f: { - type: 'final' + } + }, + { + delays: { + someDelay: ({ context }) => { + return context.delay + 50; + }, + delayA: ({ context }) => context.delay, + delayD: ({ context, event }) => context.delay + event.value } } - }, - { - delays: { - someDelay: ({ context }) => { - return context.delay + 50; - }, - delayA: ({ context }) => context.delay, - delayD: ({ context, event }) => context.delay + event.value - } - } - ); + ); - const actor = createActor(letterMachine, { clock }); - actor.subscribe({ - complete: () => { - done(); - } - }); - actor.start(); - - expect(actor.getSnapshot().value).toEqual('a'); - clock.increment(100); - expect(actor.getSnapshot().value).toEqual('b'); - clock.increment(100 + 50); - expect(actor.getSnapshot().value).toEqual('c'); - clock.increment(20); - expect(actor.getSnapshot().value).toEqual('d'); - clock.increment(100 + 200); - expect(actor.getSnapshot().value).toEqual('e'); - clock.increment(100 + 50); - }); + const actor = createActor(letterMachine, { clock }); + actor.subscribe({ + complete: () => { + resolve(); + } + }); + actor.start(); + + expect(actor.getSnapshot().value).toEqual('a'); + clock.increment(100); + expect(actor.getSnapshot().value).toEqual('b'); + clock.increment(100 + 50); + expect(actor.getSnapshot().value).toEqual('c'); + clock.increment(20); + expect(actor.getSnapshot().value).toEqual('d'); + clock.increment(100 + 200); + expect(actor.getSnapshot().value).toEqual('e'); + clock.increment(100 + 50); + })); }); describe('activities (deprecated)', () => { it('should start activities', () => { - const spy = jest.fn(); + const spy = vi.fn(); const activityMachine = createMachine( { @@ -473,7 +475,7 @@ describe('interpreter', () => { }); it('should stop activities', () => { - const spy = jest.fn(); + const spy = vi.fn(); const activityMachine = createMachine( { @@ -509,7 +511,7 @@ describe('interpreter', () => { }); it('should stop activities upon stopping the service', () => { - const spy = jest.fn(); + const spy = vi.fn(); const stopActivityMachine = createMachine( { @@ -604,50 +606,51 @@ describe('interpreter', () => { expect(service.getSnapshot().value).toEqual('green'); }); - it('can cancel a delayed event using expression to resolve send id', (done) => { - const machine = createMachine({ - initial: 'first', - states: { - first: { - entry: [ - raise( - { type: 'FOO' }, - { - id: 'foo', - delay: 100 - } - ), - raise( - { type: 'BAR' }, - { - delay: 200 - } - ), - cancel(() => 'foo') - ], - on: { - FOO: 'fail', - BAR: 'pass' + it('can cancel a delayed event using expression to resolve send id', () => + new Promise((resolve) => { + const machine = createMachine({ + initial: 'first', + states: { + first: { + entry: [ + raise( + { type: 'FOO' }, + { + id: 'foo', + delay: 100 + } + ), + raise( + { type: 'BAR' }, + { + delay: 200 + } + ), + cancel(() => 'foo') + ], + on: { + FOO: 'fail', + BAR: 'pass' + } + }, + fail: { + type: 'final' + }, + pass: { + type: 'final' } - }, - fail: { - type: 'final' - }, - pass: { - type: 'final' } - } - }); + }); - const service = createActor(machine).start(); + const service = createActor(machine).start(); - service.subscribe({ - complete: () => { - expect(service.getSnapshot().value).toBe('pass'); - done(); - } - }); - }); + service.subscribe({ + complete: () => { + expect(service.getSnapshot().value).toBe('pass'); + resolve(); + } + }); + })); it('should not throw an error if an event is sent to an uninitialized interpreter', () => { const actorRef = createActor(lightMachine); @@ -655,42 +658,43 @@ describe('interpreter', () => { expect(() => actorRef.send({ type: 'SOME_EVENT' })).not.toThrow(); }); - it('should defer events sent to an uninitialized service', (done) => { - const deferMachine = createMachine({ - id: 'defer', - initial: 'a', - states: { - a: { - on: { NEXT_A: 'b' } - }, - b: { - on: { NEXT_B: 'c' } - }, - c: { - type: 'final' + it('should defer events sent to an uninitialized service', () => + new Promise((resolve) => { + const deferMachine = createMachine({ + id: 'defer', + initial: 'a', + states: { + a: { + on: { NEXT_A: 'b' } + }, + b: { + on: { NEXT_B: 'c' } + }, + c: { + type: 'final' + } } - } - }); + }); - let state: any; - const deferService = createActor(deferMachine); + let state: any; + const deferService = createActor(deferMachine); - deferService.subscribe({ - next: (nextState) => { - state = nextState; - }, - complete: done - }); + deferService.subscribe({ + next: (nextState) => { + state = nextState; + }, + complete: resolve + }); - // uninitialized - deferService.send({ type: 'NEXT_A' }); - deferService.send({ type: 'NEXT_B' }); + // uninitialized + deferService.send({ type: 'NEXT_A' }); + deferService.send({ type: 'NEXT_B' }); - expect(state).not.toBeDefined(); + expect(state).not.toBeDefined(); - // initialized - deferService.start(); - }); + // initialized + deferService.start(); + })); it('should throw an error if initial state sent to interpreter is invalid', () => { const invalidMachine = { @@ -720,6 +724,8 @@ describe('interpreter', () => { }); it('should not update when stopped', () => { + const spy = vi.spyOn(console, 'warn'); + const service = createActor(lightMachine, { clock: new SimulatedClock() }); @@ -735,7 +741,7 @@ describe('interpreter', () => { expect(service.getSnapshot().value).toEqual('yellow'); } - expect(console.warn).toMatchMockCallsInlineSnapshot(` + expect(spy.mock.calls).toMatchInlineSnapshot(` [ [ "Event "TIMER" was sent to stopped actor "x:27 (x:27)". This actor has already reached its final state, and will not transition. @@ -844,76 +850,78 @@ describe('interpreter', () => { } }); - it('should resolve send event expressions', (done) => { - const actor = createActor(machine); - actor.subscribe({ complete: () => done() }); - actor.start(); - }); + it('should resolve send event expressions', () => + new Promise((resolve) => { + const actor = createActor(machine); + actor.subscribe({ complete: () => resolve() }); + actor.start(); + })); }); describe('sendParent() event expressions', () => { - it('should resolve sendParent event expressions', (done) => { - const childMachine = createMachine({ - types: {} as { - context: { password: string }; - input: { password: string }; - }, - id: 'child', - initial: 'start', - context: ({ input }) => ({ - password: input.password - }), - states: { - start: { - entry: sendParent(({ context }) => { - return { type: 'NEXT', password: context.password }; - }) + it('should resolve sendParent event expressions', () => + new Promise((resolve) => { + const childMachine = createMachine({ + types: {} as { + context: { password: string }; + input: { password: string }; + }, + id: 'child', + initial: 'start', + context: ({ input }) => ({ + password: input.password + }), + states: { + start: { + entry: sendParent(({ context }) => { + return { type: 'NEXT', password: context.password }; + }) + } } - } - }); + }); - const parentMachine = createMachine({ - types: {} as { - events: { - type: 'NEXT'; - password: string; - }; - }, - id: 'parent', - initial: 'start', - states: { - start: { - invoke: { - id: 'child', - src: childMachine, - input: { password: 'foo' } - }, - on: { - NEXT: { - target: 'finish', - guard: ({ event }) => event.password === 'foo' + const parentMachine = createMachine({ + types: {} as { + events: { + type: 'NEXT'; + password: string; + }; + }, + id: 'parent', + initial: 'start', + states: { + start: { + invoke: { + id: 'child', + src: childMachine, + input: { password: 'foo' } + }, + on: { + NEXT: { + target: 'finish', + guard: ({ event }) => event.password === 'foo' + } } + }, + finish: { + type: 'final' } - }, - finish: { - type: 'final' } - } - }); + }); - const actor = createActor(parentMachine); - actor.subscribe({ - next: (state) => { - if (state.matches('start')) { - const childActor = state.children.child; + const actor = createActor(parentMachine); + actor.subscribe({ + next: (state) => { + if (state.matches('start')) { + const childActor = state.children.child; - expect(typeof childActor!.send).toBe('function'); - } - }, - complete: () => done() - }); - actor.start(); - }); + expect(typeof childActor!.send).toBe('function'); + } + }, + complete: () => resolve() + }); + actor.start(); + })); }); describe('.send()', () => { @@ -936,70 +944,74 @@ describe('interpreter', () => { } }); - it('can send events with a string', (done) => { - const service = createActor(sendMachine); - service.subscribe({ complete: () => done() }); - service.start(); - - service.send({ type: 'ACTIVATE' }); - }); - - it('can send events with an object', (done) => { - const service = createActor(sendMachine); - service.subscribe({ complete: () => done() }); - service.start(); - - service.send({ type: 'ACTIVATE' }); - }); - - it('can send events with an object with payload', (done) => { - const service = createActor(sendMachine); - service.subscribe({ complete: () => done() }); - service.start(); - - service.send({ type: 'EVENT', id: 42 }); - }); - - it('should receive and process all events sent simultaneously', (done) => { - const toggleMachine = createMachine({ - id: 'toggle', - initial: 'inactive', - states: { - fail: {}, - inactive: { - on: { - INACTIVATE: 'fail', - ACTIVATE: 'active' - } - }, - active: { - on: { - INACTIVATE: 'success' + it('can send events with a string', () => + new Promise((resolve) => { + const service = createActor(sendMachine); + service.subscribe({ complete: () => resolve() }); + service.start(); + + service.send({ type: 'ACTIVATE' }); + })); + + it('can send events with an object', () => + new Promise((resolve) => { + const service = createActor(sendMachine); + service.subscribe({ complete: () => resolve() }); + service.start(); + + service.send({ type: 'ACTIVATE' }); + })); + + it('can send events with an object with payload', () => + new Promise((resolve) => { + const service = createActor(sendMachine); + service.subscribe({ complete: () => resolve() }); + service.start(); + + service.send({ type: 'EVENT', id: 42 }); + })); + + it('should receive and process all events sent simultaneously', () => + new Promise((resolve) => { + const toggleMachine = createMachine({ + id: 'toggle', + initial: 'inactive', + states: { + fail: {}, + inactive: { + on: { + INACTIVATE: 'fail', + ACTIVATE: 'active' + } + }, + active: { + on: { + INACTIVATE: 'success' + } + }, + success: { + type: 'final' } - }, - success: { - type: 'final' } - } - }); + }); - const toggleService = createActor(toggleMachine); - toggleService.subscribe({ - complete: () => { - done(); - } - }); - toggleService.start(); + const toggleService = createActor(toggleMachine); + toggleService.subscribe({ + complete: () => { + resolve(); + } + }); + toggleService.start(); - toggleService.send({ type: 'ACTIVATE' }); - toggleService.send({ type: 'INACTIVATE' }); - }); + toggleService.send({ type: 'ACTIVATE' }); + toggleService.send({ type: 'INACTIVATE' }); + })); }); describe('.start()', () => { it('should initialize the service', () => { - const contextSpy = jest.fn(); - const entrySpy = jest.fn(); + const contextSpy = vi.fn(); + const entrySpy = vi.fn(); const machine = createMachine({ context: contextSpy, @@ -1019,8 +1031,8 @@ describe('interpreter', () => { }); it('should not reinitialize a started service', () => { - const contextSpy = jest.fn(); - const entrySpy = jest.fn(); + const contextSpy = vi.fn(); + const entrySpy = vi.fn(); const machine = createMachine({ context: contextSpy, @@ -1093,64 +1105,68 @@ describe('interpreter', () => { }); describe('.stop()', () => { - it('should cancel delayed events', (done) => { - let called = false; - const delayedMachine = createMachine({ - id: 'delayed', - initial: 'foo', - states: { - foo: { - after: { - 50: { - target: 'bar', - actions: () => { - called = true; + it('should cancel delayed events', () => + new Promise((resolve) => { + let called = false; + const delayedMachine = createMachine({ + id: 'delayed', + initial: 'foo', + states: { + foo: { + after: { + 50: { + target: 'bar', + actions: () => { + called = true; + } } } - } - }, - bar: {} - } - }); + }, + bar: {} + } + }); - const delayedService = createActor(delayedMachine).start(); + const delayedService = createActor(delayedMachine).start(); - delayedService.stop(); + delayedService.stop(); - setTimeout(() => { - expect(called).toBe(false); - done(); - }, 60); - }); + setTimeout(() => { + expect(called).toBe(false); + resolve(); + }, 60); + })); - it('should not execute transitions after being stopped', (done) => { - let called = false; + it('should not execute transitions after being stopped', () => + new Promise((resolve) => { + const warnSpy = vi.spyOn(console, 'warn'); - const testMachine = createMachine({ - initial: 'waiting', - states: { - waiting: { - on: { - TRIGGER: 'active' - } - }, - active: { - entry: () => { - called = true; + let called = false; + + const testMachine = createMachine({ + initial: 'waiting', + states: { + waiting: { + on: { + TRIGGER: 'active' + } + }, + active: { + entry: () => { + called = true; + } } } - } - }); + }); - const service = createActor(testMachine).start(); + const service = createActor(testMachine).start(); - service.stop(); + service.stop(); - service.send({ type: 'TRIGGER' }); + service.send({ type: 'TRIGGER' }); - setTimeout(() => { - expect(called).toBeFalsy(); - expect(console.warn).toMatchMockCallsInlineSnapshot(` + setTimeout(() => { + expect(called).toBeFalsy(); + expect(warnSpy.mock.calls).toMatchInlineSnapshot(` [ [ "Event "TRIGGER" was sent to stopped actor "x:43 (x:43)". This actor has already reached its final state, and will not transition. @@ -1158,9 +1174,9 @@ describe('interpreter', () => { ], ] `); - done(); - }, 10); - }); + resolve(); + }, 10); + })); it('stopping a not-started interpreter should not crash', () => { const service = createActor( @@ -1309,88 +1325,91 @@ describe('interpreter', () => { } }); - it('should be subscribable', (done) => { - let count: number; - const intervalService = createActor(intervalMachine).start(); - - expect(typeof intervalService.subscribe === 'function').toBeTruthy(); + it('should be subscribable', () => + new Promise((resolve) => { + let count: number; + const intervalService = createActor(intervalMachine).start(); - intervalService.subscribe( - (state) => (count = state.context.count), - undefined, - () => { - expect(count).toEqual(5); - done(); - } - ); - }); + expect(typeof intervalService.subscribe === 'function').toBeTruthy(); - it('should be interoperable with RxJS, etc. via Symbol.observable', (done) => { - let count = 0; - const intervalService = createActor(intervalMachine).start(); + intervalService.subscribe( + (state) => (count = state.context.count), + undefined, + () => { + expect(count).toEqual(5); + resolve(); + } + ); + })); - const state$ = from(intervalService); + it('should be interoperable with RxJS, etc. via Symbol.observable', () => + new Promise((resolve) => { + let count = 0; + const intervalService = createActor(intervalMachine).start(); - state$.subscribe({ - next: () => { - count += 1; - }, - error: undefined, - complete: () => { - expect(count).toEqual(5); - done(); - } - }); - }); + const state$ = from(intervalService); - it('should be unsubscribable', (done) => { - const countContext = { count: 0 }; - const machine = createMachine({ - types: {} as { context: typeof countContext }, - context: countContext, - initial: 'active', - states: { - active: { - always: { - target: 'finished', - guard: ({ context }) => context.count >= 5 - }, - on: { - INC: { - actions: assign({ count: ({ context }) => context.count + 1 }) + state$.subscribe({ + next: () => { + count += 1; + }, + error: undefined, + complete: () => { + expect(count).toEqual(5); + resolve(); + } + }); + })); + + it('should be unsubscribable', () => + new Promise((resolve) => { + const countContext = { count: 0 }; + const machine = createMachine({ + types: {} as { context: typeof countContext }, + context: countContext, + initial: 'active', + states: { + active: { + always: { + target: 'finished', + guard: ({ context }) => context.count >= 5 + }, + on: { + INC: { + actions: assign({ count: ({ context }) => context.count + 1 }) + } } + }, + finished: { + type: 'final' } - }, - finished: { - type: 'final' } - } - }); + }); + + let count: number; + const service = createActor(machine); + service.subscribe({ + complete: () => { + expect(count).toEqual(2); + resolve(); + } + }); + service.start(); - let count: number; - const service = createActor(machine); - service.subscribe({ - complete: () => { - expect(count).toEqual(2); - done(); - } - }); - service.start(); + const subscription = service.subscribe( + (state) => (count = state.context.count) + ); - const subscription = service.subscribe( - (state) => (count = state.context.count) - ); - - service.send({ type: 'INC' }); - service.send({ type: 'INC' }); - subscription.unsubscribe(); - service.send({ type: 'INC' }); - service.send({ type: 'INC' }); - service.send({ type: 'INC' }); - }); + service.send({ type: 'INC' }); + service.send({ type: 'INC' }); + subscription.unsubscribe(); + service.send({ type: 'INC' }); + service.send({ type: 'INC' }); + service.send({ type: 'INC' }); + })); it('should call complete() once a final state is reached', () => { - const completeCb = jest.fn(); + const completeCb = vi.fn(); const service = createActor( createMachine({ @@ -1416,7 +1435,7 @@ describe('interpreter', () => { }); it('should call complete() once the interpreter is stopped', () => { - const completeCb = jest.fn(); + const completeCb = vi.fn(); const service = createActor(createMachine({})).start(); @@ -1506,132 +1525,134 @@ describe('interpreter', () => { expect(actor.getSnapshot().children).not.toHaveProperty('childActor'); }); - it('state.children should reference invoked child actors (promise)', (done) => { - const parentMachine = createMachine( - { - initial: 'active', - types: {} as { - actors: { - src: 'num'; - logic: PromiseActorLogic; - }; - }, - states: { - active: { - invoke: { - id: 'childActor', - src: 'num', - onDone: [ - { - target: 'success', - guard: ({ event }) => { - return event.output === 42; - } - }, - { target: 'failure' } - ] - } - }, - success: { - type: 'final' + it('state.children should reference invoked child actors (promise)', () => + new Promise((resolve) => { + const parentMachine = createMachine( + { + initial: 'active', + types: {} as { + actors: { + src: 'num'; + logic: PromiseActorLogic; + }; }, - failure: { - type: 'final' + states: { + active: { + invoke: { + id: 'childActor', + src: 'num', + onDone: [ + { + target: 'success', + guard: ({ event }) => { + return event.output === 42; + } + }, + { target: 'failure' } + ] + } + }, + success: { + type: 'final' + }, + failure: { + type: 'final' + } + } + }, + { + actors: { + num: fromPromise( + () => + new Promise((res) => { + setTimeout(() => { + res(42); + }, 100); + }) + ) } } - }, - { - actors: { - num: fromPromise( - () => - new Promise((res) => { - setTimeout(() => { - res(42); - }, 100); - }) - ) - } - } - ); - - const service = createActor(parentMachine); - - service.subscribe({ - next: (state) => { - if (state.matches('active')) { - const childActor = state.children.childActor; - - expect(childActor).toHaveProperty('send'); - } - }, - complete: () => { - expect(service.getSnapshot().matches('success')).toBeTruthy(); - expect(service.getSnapshot().children).not.toHaveProperty( - 'childActor' - ); - done(); - } - }); + ); - service.start(); - }); + const service = createActor(parentMachine); - it('state.children should reference invoked child actors (observable)', (done) => { - const interval$ = interval(10); - const intervalLogic = fromObservable(() => interval$); + service.subscribe({ + next: (state) => { + if (state.matches('active')) { + const childActor = state.children.childActor; - const parentMachine = createMachine( - { - types: {} as { - actors: { - src: 'intervalLogic'; - logic: typeof intervalLogic; - }; + expect(childActor).toHaveProperty('send'); + } }, - initial: 'active', - states: { - active: { - invoke: { - id: 'childActor', - src: 'intervalLogic', - onSnapshot: { - target: 'success', - guard: ({ event }) => { - return event.snapshot.context === 3; + complete: () => { + expect(service.getSnapshot().matches('success')).toBeTruthy(); + expect(service.getSnapshot().children).not.toHaveProperty( + 'childActor' + ); + resolve(); + } + }); + + service.start(); + })); + + it('state.children should reference invoked child actors (observable)', () => + new Promise((resolve) => { + const interval$ = interval(10); + const intervalLogic = fromObservable(() => interval$); + + const parentMachine = createMachine( + { + types: {} as { + actors: { + src: 'intervalLogic'; + logic: typeof intervalLogic; + }; + }, + initial: 'active', + states: { + active: { + invoke: { + id: 'childActor', + src: 'intervalLogic', + onSnapshot: { + target: 'success', + guard: ({ event }) => { + return event.snapshot.context === 3; + } } } + }, + success: { + type: 'final' } - }, - success: { - type: 'final' + } + }, + { + actors: { + intervalLogic } } - }, - { - actors: { - intervalLogic + ); + + const service = createActor(parentMachine); + service.subscribe({ + complete: () => { + expect(service.getSnapshot().children).not.toHaveProperty( + 'childActor' + ); + resolve(); } - } - ); + }); - const service = createActor(parentMachine); - service.subscribe({ - complete: () => { - expect(service.getSnapshot().children).not.toHaveProperty( - 'childActor' - ); - done(); - } - }); + service.subscribe((state) => { + if (state.matches('active')) { + expect(state.children['childActor']).not.toBeUndefined(); + } + }); - service.subscribe((state) => { - if (state.matches('active')) { - expect(state.children['childActor']).not.toBeUndefined(); - } - }); - - service.start(); - }); + service.start(); + })); it('state.children should reference spawned actors', () => { const childMachine = createMachine({ @@ -1726,7 +1747,7 @@ describe('interpreter', () => { }); it("shouldn't execute actions when reading a snapshot of not started actor", () => { - const spy = jest.fn(); + const spy = vi.fn(); const actorRef = createActor( createMachine({ entry: () => { @@ -1741,7 +1762,7 @@ describe('interpreter', () => { }); it(`should execute entry actions when starting the actor after reading its snapshot first`, () => { - const spy = jest.fn(); + const spy = vi.fn(); const actorRef = createActor( createMachine({ @@ -1767,26 +1788,27 @@ describe('interpreter', () => { expect(actor.getSnapshot()).toBe(initialState); }); - it('should call an onDone callback immediately if the service is already done', (done) => { - const machine = createMachine({ - initial: 'a', - states: { - a: { - type: 'final' + it('should call an onDone callback immediately if the service is already done', () => + new Promise((resolve) => { + const machine = createMachine({ + initial: 'a', + states: { + a: { + type: 'final' + } } - } - }); + }); - const service = createActor(machine).start(); + const service = createActor(machine).start(); - expect(service.getSnapshot().status).toBe('done'); + expect(service.getSnapshot().status).toBe('done'); - service.subscribe({ - complete: () => { - done(); - } - }); - }); + service.subscribe({ + complete: () => { + resolve(); + } + }); + })); }); it('should throw if an event is received', () => { @@ -1841,7 +1863,7 @@ it('should not process events sent directly to own actor ref before initial entr }); it('should not notify the completion observer for an active logic when it gets subscribed before starting', () => { - const spy = jest.fn(); + const spy = vi.fn(); const machine = createMachine({}); createActor(machine).subscribe({ complete: spy }); @@ -1850,7 +1872,10 @@ it('should not notify the completion observer for an active logic when it gets s }); it('should not notify the completion observer for an errored logic when it gets subscribed after it errors', () => { - const spy = jest.fn(); + // use fake timers to avoid error being thrown in separate microtask + vi.useFakeTimers(); + + const spy = vi.fn(); const machine = createMachine({ entry: () => { @@ -1859,6 +1884,7 @@ it('should not notify the completion observer for an errored logic when it gets }); const actorRef = createActor(machine); actorRef.subscribe({ error: () => {} }); + actorRef.start(); actorRef.subscribe({ @@ -1869,7 +1895,7 @@ it('should not notify the completion observer for an errored logic when it gets }); it('should notify the error observer for an errored logic when it gets subscribed after it errors', () => { - const spy = jest.fn(); + const spy = vi.fn(); const machine = createMachine({ entry: () => { @@ -1884,7 +1910,7 @@ it('should notify the error observer for an errored logic when it gets subscribe error: spy }); - expect(spy).toMatchMockCallsInlineSnapshot(` + expect(spy.mock.calls).toMatchInlineSnapshot(` [ [ [Error: error], diff --git a/packages/core/test/invoke.test.ts b/packages/core/test/invoke.test.ts index 344b67cfe2..26ef6b259c 100644 --- a/packages/core/test/invoke.test.ts +++ b/packages/core/test/invoke.test.ts @@ -23,7 +23,7 @@ import { ActorRef, AnyEventObject } from '../src/index.ts'; -import { sleep } from '@xstate-repo/jest-utils'; +import { setTimeout as sleep } from 'node:timers/promises'; const user = { name: 'David' }; @@ -104,303 +104,311 @@ describe('invoke', () => { expect(actorRef.getSnapshot().context).toEqual({ count: -3 }); }); - it('should start services (explicit machine, invoke = config)', (done) => { - const childMachine = createMachine({ - id: 'fetch', - types: {} as { - context: { userId: string | undefined; user?: typeof user | undefined }; - events: { - type: 'RESOLVE'; - user: typeof user; - }; - input: { userId: string }; - }, - context: ({ input }) => ({ - userId: input.userId - }), - initial: 'pending', - states: { - pending: { - entry: raise({ type: 'RESOLVE', user }), - on: { - RESOLVE: { - target: 'success', - guard: ({ context }) => { - return context.userId !== undefined; + it('should start services (explicit machine, invoke = config)', () => + new Promise((resolve) => { + const childMachine = createMachine({ + id: 'fetch', + types: {} as { + context: { + userId: string | undefined; + user?: typeof user | undefined; + }; + events: { + type: 'RESOLVE'; + user: typeof user; + }; + input: { userId: string }; + }, + context: ({ input }) => ({ + userId: input.userId + }), + initial: 'pending', + states: { + pending: { + entry: raise({ type: 'RESOLVE', user }), + on: { + RESOLVE: { + target: 'success', + guard: ({ context }) => { + return context.userId !== undefined; + } } } + }, + success: { + type: 'final', + entry: assign({ + user: ({ event }) => event.user + }) + }, + failure: { + entry: sendParent({ type: 'REJECT' }) } }, - success: { - type: 'final', - entry: assign({ - user: ({ event }) => event.user - }) - }, - failure: { - entry: sendParent({ type: 'REJECT' }) - } - }, - output: ({ context }) => ({ user: context.user }) - }); + output: ({ context }) => ({ user: context.user }) + }); - const machine = createMachine({ - types: {} as { + const machine = createMachine({ + types: {} as { + context: { + selectedUserId: string; + user?: typeof user; + }; + }, + id: 'fetcher', + initial: 'idle', context: { - selectedUserId: string; - user?: typeof user; - }; - }, - id: 'fetcher', - initial: 'idle', - context: { - selectedUserId: '42', - user: undefined - }, - states: { - idle: { - on: { - GO_TO_WAITING: 'waiting' - } + selectedUserId: '42', + user: undefined }, - waiting: { - invoke: { - src: childMachine, - input: ({ context }: any) => ({ - userId: context.selectedUserId - }), - onDone: { - target: 'received', - guard: ({ event }) => { - // Should receive { user: { name: 'David' } } as event data - return (event.output as any).user.name === 'David'; + states: { + idle: { + on: { + GO_TO_WAITING: 'waiting' + } + }, + waiting: { + invoke: { + src: childMachine, + input: ({ context }: any) => ({ + userId: context.selectedUserId + }), + onDone: { + target: 'received', + guard: ({ event }) => { + // Should receive { user: { name: 'David' } } as event data + return (event.output as any).user.name === 'David'; + } } } + }, + received: { + type: 'final' } - }, - received: { - type: 'final' } - } - }); - - const actor = createActor(machine); - actor.subscribe({ - complete: () => { - done(); - } - }); - actor.start(); - actor.send({ type: 'GO_TO_WAITING' }); - }); + }); - it('should start services (explicit machine, invoke = machine)', (done) => { - const childMachine = createMachine({ - types: {} as { - events: { type: 'RESOLVE' }; - input: { userId: string }; - }, - initial: 'pending', - states: { - pending: { - entry: raise({ type: 'RESOLVE' }), - on: { - RESOLVE: { - target: 'success' - } - } - }, - success: { - type: 'final' + const actor = createActor(machine); + actor.subscribe({ + complete: () => { + resolve(); } - } - }); + }); + actor.start(); + actor.send({ type: 'GO_TO_WAITING' }); + })); - const machine = createMachine({ - initial: 'idle', - states: { - idle: { - on: { - GO_TO_WAITING: 'waiting' - } + it('should start services (explicit machine, invoke = machine)', () => + new Promise((resolve) => { + const childMachine = createMachine({ + types: {} as { + events: { type: 'RESOLVE' }; + input: { userId: string }; }, - waiting: { - invoke: { - src: childMachine, - onDone: 'received' + initial: 'pending', + states: { + pending: { + entry: raise({ type: 'RESOLVE' }), + on: { + RESOLVE: { + target: 'success' + } + } + }, + success: { + type: 'final' } - }, - received: { - type: 'final' } - } - }); - const actor = createActor(machine); - actor.subscribe({ - complete: () => { - done(); - } - }); - actor.start(); - actor.send({ type: 'GO_TO_WAITING' }); - }); + }); - it('should start services (machine as invoke config)', (done) => { - const machineInvokeMachine = createMachine({ - types: {} as { - events: { - type: 'SUCCESS'; - data: number; - }; - }, - id: 'machine-invoke', - initial: 'pending', - states: { - pending: { - invoke: { - src: createMachine({ - id: 'child', - initial: 'sending', - states: { - sending: { - entry: sendParent({ type: 'SUCCESS', data: 42 }) - } - } - }) + const machine = createMachine({ + initial: 'idle', + states: { + idle: { + on: { + GO_TO_WAITING: 'waiting' + } }, - on: { - SUCCESS: { - target: 'success', - guard: ({ event }) => { - return event.data === 42; - } + waiting: { + invoke: { + src: childMachine, + onDone: 'received' } + }, + received: { + type: 'final' } - }, - success: { - type: 'final' } - } - }); - const actor = createActor(machineInvokeMachine); - actor.subscribe({ complete: () => done() }); - actor.start(); - }); + }); + const actor = createActor(machine); + actor.subscribe({ + complete: () => { + resolve(); + } + }); + actor.start(); + actor.send({ type: 'GO_TO_WAITING' }); + })); - it('should start deeply nested service (machine as invoke config)', (done) => { - const machineInvokeMachine = createMachine({ - types: {} as { - events: { - type: 'SUCCESS'; - data: number; - }; - }, - id: 'parent', - initial: 'a', - states: { - a: { - initial: 'b', - states: { - b: { - invoke: { - src: createMachine({ - id: 'child', - initial: 'sending', - states: { - sending: { - entry: sendParent({ type: 'SUCCESS', data: 42 }) - } + it('should start services (machine as invoke config)', () => + new Promise((resolve) => { + const machineInvokeMachine = createMachine({ + types: {} as { + events: { + type: 'SUCCESS'; + data: number; + }; + }, + id: 'machine-invoke', + initial: 'pending', + states: { + pending: { + invoke: { + src: createMachine({ + id: 'child', + initial: 'sending', + states: { + sending: { + entry: sendParent({ type: 'SUCCESS', data: 42 }) } - }) + } + }) + }, + on: { + SUCCESS: { + target: 'success', + guard: ({ event }) => { + return event.data === 42; + } } } - } - }, - success: { - id: 'success', - type: 'final' - } - }, - on: { - SUCCESS: { - target: '.success', - guard: ({ event }) => { - return event.data === 42; + }, + success: { + type: 'final' } } - } - }); - const actor = createActor(machineInvokeMachine); - actor.subscribe({ complete: () => done() }); - actor.start(); - }); - - it('should use the service overwritten by .provide(...)', (done) => { - const childMachine = createMachine({ - id: 'child', - initial: 'init', - states: { - init: {} - } - }); + }); + const actor = createActor(machineInvokeMachine); + actor.subscribe({ complete: () => resolve() }); + actor.start(); + })); - const someParentMachine = createMachine( - { - id: 'parent', + it('should start deeply nested service (machine as invoke config)', () => + new Promise((resolve) => { + const machineInvokeMachine = createMachine({ types: {} as { - context: { count: number }; - actors: { - src: 'child'; - id: 'someService'; - logic: typeof childMachine; + events: { + type: 'SUCCESS'; + data: number; }; }, - context: { count: 0 }, - initial: 'start', + id: 'parent', + initial: 'a', states: { - start: { - invoke: { - src: 'child', - id: 'someService' - }, - on: { - STOP: 'stop' + a: { + initial: 'b', + states: { + b: { + invoke: { + src: createMachine({ + id: 'child', + initial: 'sending', + states: { + sending: { + entry: sendParent({ type: 'SUCCESS', data: 42 }) + } + } + }) + } + } } }, - stop: { + success: { + id: 'success', type: 'final' } + }, + on: { + SUCCESS: { + target: '.success', + guard: ({ event }) => { + return event.data === 42; + } + } } - }, - { - actors: { - child: childMachine + }); + const actor = createActor(machineInvokeMachine); + actor.subscribe({ complete: () => resolve() }); + actor.start(); + })); + + it('should use the service overwritten by .provide(...)', () => + new Promise((resolve) => { + const childMachine = createMachine({ + id: 'child', + initial: 'init', + states: { + init: {} } - } - ); + }); - const actor = createActor( - someParentMachine.provide({ - actors: { - child: createMachine({ - id: 'child', - initial: 'init', - states: { - init: { - entry: [sendParent({ type: 'STOP' })] + const someParentMachine = createMachine( + { + id: 'parent', + types: {} as { + context: { count: number }; + actors: { + src: 'child'; + id: 'someService'; + logic: typeof childMachine; + }; + }, + context: { count: 0 }, + initial: 'start', + states: { + start: { + invoke: { + src: 'child', + id: 'someService' + }, + on: { + STOP: 'stop' } + }, + stop: { + type: 'final' } - }) + } + }, + { + actors: { + child: childMachine + } } - }) - ); - actor.subscribe({ - complete: () => { - done(); - } - }); - actor.start(); - }); + ); + + const actor = createActor( + someParentMachine.provide({ + actors: { + child: createMachine({ + id: 'child', + initial: 'init', + states: { + init: { + entry: [sendParent({ type: 'STOP' })] + } + } + }) + } + }) + ); + actor.subscribe({ + complete: () => { + resolve(); + } + }); + actor.start(); + })); describe('parent to child', () => { const subMachine = createMachine({ @@ -416,61 +424,63 @@ describe('invoke', () => { } }); - it('should communicate with the child machine (invoke on machine)', (done) => { - const mainMachine = createMachine({ - id: 'parent', - initial: 'one', - invoke: { - id: 'foo-child', - src: subMachine - }, - states: { - one: { - entry: sendTo('foo-child', { type: 'NEXT' }), - on: { NEXT: 'two' } + it('should communicate with the child machine (invoke on machine)', () => + new Promise((resolve) => { + const mainMachine = createMachine({ + id: 'parent', + initial: 'one', + invoke: { + id: 'foo-child', + src: subMachine }, - two: { - type: 'final' + states: { + one: { + entry: sendTo('foo-child', { type: 'NEXT' }), + on: { NEXT: 'two' } + }, + two: { + type: 'final' + } } - } - }); + }); - const actor = createActor(mainMachine); - actor.subscribe({ - complete: () => { - done(); - } - }); - actor.start(); - }); + const actor = createActor(mainMachine); + actor.subscribe({ + complete: () => { + resolve(); + } + }); + actor.start(); + })); - it('should communicate with the child machine (invoke on state)', (done) => { - const mainMachine = createMachine({ - id: 'parent', - initial: 'one', - states: { - one: { - invoke: { - id: 'foo-child', - src: subMachine + it('should communicate with the child machine (invoke on state)', () => + new Promise((resolve) => { + const mainMachine = createMachine({ + id: 'parent', + initial: 'one', + states: { + one: { + invoke: { + id: 'foo-child', + src: subMachine + }, + entry: sendTo('foo-child', { type: 'NEXT' }), + on: { NEXT: 'two' } }, - entry: sendTo('foo-child', { type: 'NEXT' }), - on: { NEXT: 'two' } - }, - two: { - type: 'final' + two: { + type: 'final' + } } - } - }); + }); - const actor = createActor(mainMachine); - actor.subscribe({ - complete: () => { - done(); - } - }); - actor.start(); - }); + const actor = createActor(mainMachine); + actor.subscribe({ + complete: () => { + resolve(); + } + }); + actor.start(); + })); it('should transition correctly if child invocation causes it to directly go to final state', () => { const doneSubMachine = createMachine({ @@ -512,51 +522,52 @@ describe('invoke', () => { expect(actor.getSnapshot().value).toBe('two'); }); - it('should work with invocations defined in orthogonal state nodes', (done) => { - const pongMachine = createMachine({ - id: 'pong', - initial: 'active', - states: { - active: { - type: 'final' - } - }, - output: { secret: 'pingpong' } - }); + it('should work with invocations defined in orthogonal state nodes', () => + new Promise((resolve) => { + const pongMachine = createMachine({ + id: 'pong', + initial: 'active', + states: { + active: { + type: 'final' + } + }, + output: { secret: 'pingpong' } + }); - const pingMachine = createMachine({ - id: 'ping', - type: 'parallel', - states: { - one: { - initial: 'active', - states: { - active: { - invoke: { - id: 'pong', - src: pongMachine, - onDone: { - target: 'success', - guard: ({ event }) => event.output.secret === 'pingpong' + const pingMachine = createMachine({ + id: 'ping', + type: 'parallel', + states: { + one: { + initial: 'active', + states: { + active: { + invoke: { + id: 'pong', + src: pongMachine, + onDone: { + target: 'success', + guard: ({ event }) => event.output.secret === 'pingpong' + } } + }, + success: { + type: 'final' } - }, - success: { - type: 'final' } } } - } - }); + }); - const actor = createActor(pingMachine); - actor.subscribe({ - complete: () => { - done(); - } - }); - actor.start(); - }); + const actor = createActor(pingMachine); + actor.subscribe({ + complete: () => { + resolve(); + } + }); + actor.start(); + })); it('should not reinvoke root-level invocations on root non-reentering transitions', () => { // https://github.com/statelyai/xstate/issues/2147 @@ -633,79 +644,80 @@ describe('invoke', () => { expect(actorStopped).toBe(true); }); - it('child should not invoke an actor when it transitions to an invoking state when it gets stopped by its parent', (done) => { - let invokeCount = 0; + it('child should not invoke an actor when it transitions to an invoking state when it gets stopped by its parent', () => + new Promise((resolve) => { + let invokeCount = 0; - const child = createMachine({ - id: 'child', - initial: 'idle', - states: { - idle: { - invoke: { - src: fromCallback(({ sendBack }) => { - invokeCount++; + const child = createMachine({ + id: 'child', + initial: 'idle', + states: { + idle: { + invoke: { + src: fromCallback(({ sendBack }) => { + invokeCount++; - if (invokeCount > 1) { - // prevent a potential infinite loop - throw new Error('This should be impossible.'); - } + if (invokeCount > 1) { + // prevent a potential infinite loop + throw new Error('This should be impossible.'); + } - // it's important for this test to send the event back when the parent is *not* currently processing an event - // this ensures that the parent can process the received event immediately and can stop the child immediately - setTimeout(() => sendBack({ type: 'STARTED' })); - }) - }, - on: { - STARTED: 'active' - } - }, - active: { - invoke: { - src: fromCallback(({ sendBack }) => { - sendBack({ type: 'STOPPED' }); - }) + // it's important for this test to send the event back when the parent is *not* currently processing an event + // this ensures that the parent can process the received event immediately and can stop the child immediately + setTimeout(() => sendBack({ type: 'STARTED' })); + }) + }, + on: { + STARTED: 'active' + } }, - on: { - STOPPED: { - target: 'idle', - actions: forwardTo(SpecialTargets.Parent) + active: { + invoke: { + src: fromCallback(({ sendBack }) => { + sendBack({ type: 'STOPPED' }); + }) + }, + on: { + STOPPED: { + target: 'idle', + actions: forwardTo(SpecialTargets.Parent) + } } } } - } - }); - const parent = createMachine({ - id: 'parent', - initial: 'idle', - states: { - idle: { - on: { - START: 'active' - } - }, - active: { - invoke: { src: child }, - on: { - STOPPED: 'done' + }); + const parent = createMachine({ + id: 'parent', + initial: 'idle', + states: { + idle: { + on: { + START: 'active' + } + }, + active: { + invoke: { src: child }, + on: { + STOPPED: 'done' + } + }, + done: { + type: 'final' } - }, - done: { - type: 'final' } - } - }); + }); - const service = createActor(parent); - service.subscribe({ - complete: () => { - expect(invokeCount).toBe(1); - done(); - } - }); - service.start(); + const service = createActor(parent); + service.subscribe({ + complete: () => { + expect(invokeCount).toBe(1); + resolve(); + } + }); + service.start(); - service.send({ type: 'START' }); - }); + service.send({ type: 'START' }); + })); }); type PromiseExecutor = ( @@ -782,146 +794,120 @@ describe('invoke', () => { } }); - it('should be invoked with a promise factory and resolve through onDone', (done) => { - const machine = createMachine({ - initial: 'pending', - states: { - pending: { - invoke: { - src: fromPromise(() => - createPromise((resolve) => { - resolve(); - }) - ), - onDone: 'success' + it('should be invoked with a promise factory and resolve through onDone', () => + new Promise((resolve) => { + const machine = createMachine({ + initial: 'pending', + states: { + pending: { + invoke: { + src: fromPromise(() => + createPromise((resolve) => { + resolve(); + }) + ), + onDone: 'success' + } + }, + success: { + type: 'final' } - }, - success: { - type: 'final' } - } - }); - const service = createActor(machine); - service.subscribe({ - complete: () => { - done(); - } - }); - service.start(); - }); - - it('should be invoked with a promise factory and reject with ErrorExecution', (done) => { - const actor = createActor(invokePromiseMachine, { - input: { id: 31, succeed: false } - }); - actor.subscribe({ complete: () => done() }); - actor.start(); - }); + }); + const service = createActor(machine); + service.subscribe({ + complete: () => { + resolve(); + } + }); + service.start(); + })); - it('should be invoked with a promise factory and surface any unhandled errors', (done) => { - const promiseMachine = createMachine({ - id: 'invokePromise', - initial: 'pending', - states: { - pending: { - invoke: { - src: fromPromise(() => - createPromise(() => { - throw new Error('test'); - }) - ), - onDone: 'success' + it('should be invoked with a promise factory and reject with ErrorExecution', () => + new Promise((resolve) => { + const actor = createActor(invokePromiseMachine, { + input: { id: 31, succeed: false } + }); + actor.subscribe({ complete: () => resolve() }); + actor.start(); + })); + + it('should be invoked with a promise factory and surface any unhandled errors', () => + new Promise((resolve) => { + const promiseMachine = createMachine({ + id: 'invokePromise', + initial: 'pending', + states: { + pending: { + invoke: { + src: fromPromise(() => + createPromise(() => { + throw new Error('test'); + }) + ), + onDone: 'success' + } + }, + success: { + type: 'final' } - }, - success: { - type: 'final' } - } - }); + }); - const service = createActor(promiseMachine); - service.subscribe({ - error(err) { - expect((err as any).message).toEqual(expect.stringMatching(/test/)); - done(); - } - }); + const service = createActor(promiseMachine); + service.subscribe({ + error(err) { + expect((err as any).message).toEqual( + expect.stringMatching(/test/) + ); + resolve(); + } + }); - service.start(); - }); + service.start(); + })); - it('should be invoked with a promise factory and stop on unhandled onError target', (done) => { - const completeSpy = jest.fn(); + it('should be invoked with a promise factory and stop on unhandled onError target', () => + new Promise((resolve) => { + const completeSpy = vi.fn(); - const promiseMachine = createMachine({ - id: 'invokePromise', - initial: 'pending', - states: { - pending: { - invoke: { - src: fromPromise(() => - createPromise(() => { - throw new Error('test'); - }) - ), - onDone: 'success' + const promiseMachine = createMachine({ + id: 'invokePromise', + initial: 'pending', + states: { + pending: { + invoke: { + src: fromPromise(() => + createPromise(() => { + throw new Error('test'); + }) + ), + onDone: 'success' + } + }, + success: { + type: 'final' } - }, - success: { - type: 'final' } - } - }); - - const actor = createActor(promiseMachine); + }); - actor.subscribe({ - error: (err) => { - expect(err).toBeInstanceOf(Error); - expect((err as any).message).toBe('test'); - expect(completeSpy).not.toHaveBeenCalled(); - done(); - }, - complete: completeSpy - }); - actor.start(); - }); + const actor = createActor(promiseMachine); - it('should be invoked with a promise factory and resolve through onDone for compound state nodes', (done) => { - const promiseMachine = createMachine({ - id: 'promise', - initial: 'parent', - states: { - parent: { - initial: 'pending', - states: { - pending: { - invoke: { - src: fromPromise(() => - createPromise((resolve) => resolve()) - ), - onDone: 'success' - } - }, - success: { - type: 'final' - } - }, - onDone: 'success' + actor.subscribe({ + error: (err) => { + expect(err).toBeInstanceOf(Error); + expect((err as any).message).toBe('test'); + expect(completeSpy).not.toHaveBeenCalled(); + resolve(); }, - success: { - type: 'final' - } - } - }); - const actor = createActor(promiseMachine); - actor.subscribe({ complete: () => done() }); - actor.start(); - }); + complete: completeSpy + }); + actor.start(); + })); - it('should be invoked with a promise service and resolve through onDone for compound state nodes', (done) => { - const promiseMachine = createMachine( - { + it('should be invoked with a promise factory and resolve through onDone for compound state nodes', () => + new Promise((resolve) => { + const promiseMachine = createMachine({ id: 'promise', initial: 'parent', states: { @@ -930,7 +916,9 @@ describe('invoke', () => { states: { pending: { invoke: { - src: 'somePromise', + src: fromPromise(() => + createPromise((resolve) => resolve()) + ), onDone: 'success' } }, @@ -944,151 +932,69 @@ describe('invoke', () => { type: 'final' } } - }, - { - actors: { - somePromise: fromPromise(() => - createPromise((resolve) => resolve()) - ) - } - } - ); - const actor = createActor(promiseMachine); - actor.subscribe({ complete: () => done() }); - actor.start(); - }); - it('should assign the resolved data when invoked with a promise factory', (done) => { - const promiseMachine = createMachine({ - types: {} as { context: { count: number } }, - id: 'promise', - context: { count: 0 }, - initial: 'pending', - states: { - pending: { - invoke: { - src: fromPromise(() => - createPromise((resolve) => resolve({ count: 1 })) - ), - onDone: { - target: 'success', - actions: assign({ - count: ({ event }) => event.output.count - }) + }); + const actor = createActor(promiseMachine); + actor.subscribe({ complete: () => resolve() }); + actor.start(); + })); + + it('should be invoked with a promise service and resolve through onDone for compound state nodes', () => + new Promise((resolve) => { + const promiseMachine = createMachine( + { + id: 'promise', + initial: 'parent', + states: { + parent: { + initial: 'pending', + states: { + pending: { + invoke: { + src: 'somePromise', + onDone: 'success' + } + }, + success: { + type: 'final' + } + }, + onDone: 'success' + }, + success: { + type: 'final' } } }, - success: { - type: 'final' - } - } - }); - - const actor = createActor(promiseMachine); - actor.subscribe({ - complete: () => { - expect(actor.getSnapshot().context.count).toEqual(1); - done(); - } - }); - actor.start(); - }); - - it('should assign the resolved data when invoked with a promise service', (done) => { - const promiseMachine = createMachine( - { - types: {} as { context: { count: number } }, - id: 'promise', - context: { count: 0 }, - initial: 'pending', - states: { - pending: { - invoke: { - src: 'somePromise', - onDone: { - target: 'success', - actions: assign({ - count: ({ event }) => event.output.count - }) - } - } - }, - success: { - type: 'final' - } - } - }, - { - actors: { - somePromise: fromPromise(() => - createPromise((resolve) => resolve({ count: 1 })) - ) - } - } - ); - - const actor = createActor(promiseMachine); - actor.subscribe({ - complete: () => { - expect(actor.getSnapshot().context.count).toEqual(1); - done(); - } - }); - actor.start(); - }); - - it('should provide the resolved data when invoked with a promise factory', (done) => { - let count = 0; - - const promiseMachine = createMachine({ - id: 'promise', - context: { count: 0 }, - initial: 'pending', - states: { - pending: { - invoke: { - src: fromPromise(() => - createPromise((resolve) => resolve({ count: 1 })) - ), - onDone: { - target: 'success', - actions: ({ event }) => { - count = (event.output as any).count; - } - } + { + actors: { + somePromise: fromPromise(() => + createPromise((resolve) => resolve()) + ) } - }, - success: { - type: 'final' } - } - }); - - const actor = createActor(promiseMachine); - actor.subscribe({ - complete: () => { - expect(count).toEqual(1); - done(); - } - }); - actor.start(); - }); - - it('should provide the resolved data when invoked with a promise service', (done) => { - let count = 0; - - const promiseMachine = createMachine( - { + ); + const actor = createActor(promiseMachine); + actor.subscribe({ complete: () => resolve() }); + actor.start(); + })); + it('should assign the resolved data when invoked with a promise factory', () => + new Promise((resolve) => { + const promiseMachine = createMachine({ + types: {} as { context: { count: number } }, id: 'promise', + context: { count: 0 }, initial: 'pending', states: { pending: { invoke: { - src: 'somePromise', + src: fromPromise(() => + createPromise((resolve) => resolve({ count: 1 })) + ), onDone: { target: 'success', - actions: ({ event }) => { - count = event.output.count; - } + actions: assign({ + count: ({ event }) => event.output.count + }) } } }, @@ -1096,321 +1002,449 @@ describe('invoke', () => { type: 'final' } } - }, - { - actors: { - somePromise: fromPromise(() => - createPromise((resolve) => resolve({ count: 1 })) - ) - } - } - ); + }); - const actor = createActor(promiseMachine); - actor.subscribe({ - complete: () => { - expect(count).toEqual(1); - done(); - } - }); - actor.start(); - }); + const actor = createActor(promiseMachine); + actor.subscribe({ + complete: () => { + expect(actor.getSnapshot().context.count).toEqual(1); + resolve(); + } + }); + actor.start(); + })); + + it('should assign the resolved data when invoked with a promise service', () => + new Promise((resolve) => { + const promiseMachine = createMachine( + { + types: {} as { context: { count: number } }, + id: 'promise', + context: { count: 0 }, + initial: 'pending', + states: { + pending: { + invoke: { + src: 'somePromise', + onDone: { + target: 'success', + actions: assign({ + count: ({ event }) => event.output.count + }) + } + } + }, + success: { + type: 'final' + } + } + }, + { + actors: { + somePromise: fromPromise(() => + createPromise((resolve) => resolve({ count: 1 })) + ) + } + } + ); - it('should be able to specify a Promise as a service', (done) => { - interface BeginEvent { - type: 'BEGIN'; - payload: boolean; - } + const actor = createActor(promiseMachine); + actor.subscribe({ + complete: () => { + expect(actor.getSnapshot().context.count).toEqual(1); + resolve(); + } + }); + actor.start(); + })); - const promiseActor = fromPromise( - ({ input }: { input: { foo: boolean; event: { payload: any } } }) => { - return createPromise((resolve, reject) => { - input.foo && input.event.payload ? resolve() : reject(); - }); - } - ); + it('should provide the resolved data when invoked with a promise factory', () => + new Promise((resolve) => { + let count = 0; - const promiseMachine = createMachine( - { + const promiseMachine = createMachine({ id: 'promise', - types: {} as { - context: { foo: boolean }; - events: BeginEvent; - actors: { - src: 'somePromise'; - logic: typeof promiseActor; - }; - }, + context: { count: 0 }, initial: 'pending', - context: { - foo: true - }, states: { pending: { - on: { - BEGIN: 'first' - } - }, - first: { invoke: { - src: 'somePromise', - input: ({ context, event }) => ({ - foo: context.foo, - event: event - }), - onDone: 'last' + src: fromPromise(() => + createPromise((resolve) => resolve({ count: 1 })) + ), + onDone: { + target: 'success', + actions: ({ event }) => { + count = (event.output as any).count; + } + } } }, - last: { + success: { type: 'final' } } - }, - { - actors: { - somePromise: promiseActor + }); + + const actor = createActor(promiseMachine); + actor.subscribe({ + complete: () => { + expect(count).toEqual(1); + resolve(); } - } - ); + }); + actor.start(); + })); - const actor = createActor(promiseMachine); - actor.subscribe({ complete: () => done() }); - actor.start(); - actor.send({ - type: 'BEGIN', - payload: true - }); - }); + it('should provide the resolved data when invoked with a promise service', () => + new Promise((resolve) => { + let count = 0; - it('should be able to reuse the same promise logic multiple times and create unique promise for each created actor', (done) => { - const machine = createMachine( - { - types: {} as { - context: { - result1: number | null; - result2: number | null; - }; - actors: { - src: 'getRandomNumber'; - logic: PromiseActorLogic<{ result: number }>; - }; + const promiseMachine = createMachine( + { + id: 'promise', + initial: 'pending', + states: { + pending: { + invoke: { + src: 'somePromise', + onDone: { + target: 'success', + actions: ({ event }) => { + count = event.output.count; + } + } + } + }, + success: { + type: 'final' + } + } }, - context: { - result1: null, - result2: null + { + actors: { + somePromise: fromPromise(() => + createPromise((resolve) => resolve({ count: 1 })) + ) + } + } + ); + + const actor = createActor(promiseMachine); + actor.subscribe({ + complete: () => { + expect(count).toEqual(1); + resolve(); + } + }); + actor.start(); + })); + + it('should be able to specify a Promise as a service', () => + new Promise((resolve) => { + interface BeginEvent { + type: 'BEGIN'; + payload: boolean; + } + + const promiseActor = fromPromise( + ({ + input + }: { + input: { foo: boolean; event: { payload: any } }; + }) => { + return createPromise((resolve, reject) => { + input.foo && input.event.payload ? resolve() : reject(); + }); + } + ); + + const promiseMachine = createMachine( + { + id: 'promise', + types: {} as { + context: { foo: boolean }; + events: BeginEvent; + actors: { + src: 'somePromise'; + logic: typeof promiseActor; + }; + }, + initial: 'pending', + context: { + foo: true + }, + states: { + pending: { + on: { + BEGIN: 'first' + } + }, + first: { + invoke: { + src: 'somePromise', + input: ({ context, event }) => ({ + foo: context.foo, + event: event + }), + onDone: 'last' + } + }, + last: { + type: 'final' + } + } }, - initial: 'pending', - states: { - pending: { - type: 'parallel', - states: { - state1: { - initial: 'active', - states: { - active: { - invoke: { - src: 'getRandomNumber', - onDone: { - target: 'success', - // TODO: we get DoneInvokeEvent here, this gets fixed with https://github.com/microsoft/TypeScript/pull/48838 - actions: assign(({ event }) => ({ - result1: event.output.result - })) + { + actors: { + somePromise: promiseActor + } + } + ); + + const actor = createActor(promiseMachine); + actor.subscribe({ complete: () => resolve() }); + actor.start(); + actor.send({ + type: 'BEGIN', + payload: true + }); + })); + + it('should be able to reuse the same promise logic multiple times and create unique promise for each created actor', () => + new Promise((resolve) => { + const machine = createMachine( + { + types: {} as { + context: { + result1: number | null; + result2: number | null; + }; + actors: { + src: 'getRandomNumber'; + logic: PromiseActorLogic<{ result: number }>; + }; + }, + context: { + result1: null, + result2: null + }, + initial: 'pending', + states: { + pending: { + type: 'parallel', + states: { + state1: { + initial: 'active', + states: { + active: { + invoke: { + src: 'getRandomNumber', + onDone: { + target: 'success', + // TODO: we get DoneInvokeEvent here, this gets fixed with https://github.com/microsoft/TypeScript/pull/48838 + actions: assign(({ event }) => ({ + result1: event.output.result + })) + } } + }, + success: { + type: 'final' } - }, - success: { - type: 'final' } - } - }, - state2: { - initial: 'active', - states: { - active: { - invoke: { - src: 'getRandomNumber', - onDone: { - target: 'success', - actions: assign(({ event }) => ({ - result2: event.output.result - })) + }, + state2: { + initial: 'active', + states: { + active: { + invoke: { + src: 'getRandomNumber', + onDone: { + target: 'success', + actions: assign(({ event }) => ({ + result2: event.output.result + })) + } } + }, + success: { + type: 'final' } - }, - success: { - type: 'final' } } - } + }, + onDone: 'done' }, - onDone: 'done' - }, - done: { - type: 'final' + done: { + type: 'final' + } + } + }, + { + actors: { + // it's important for this actor to be reused, this test shouldn't use a factory or anything like that + getRandomNumber: fromPromise(() => { + return createPromise((resolve) => + resolve({ result: Math.random() }) + ); + }) } } - }, - { - actors: { - // it's important for this actor to be reused, this test shouldn't use a factory or anything like that - getRandomNumber: fromPromise(() => { - return createPromise((resolve) => - resolve({ result: Math.random() }) - ); - }) - } - } - ); + ); - const service = createActor(machine); - service.subscribe({ - complete: () => { - const snapshot = service.getSnapshot(); - expect(typeof snapshot.context.result1).toBe('number'); - expect(typeof snapshot.context.result2).toBe('number'); - expect(snapshot.context.result1).not.toBe(snapshot.context.result2); - done(); - } - }); - service.start(); - }); + const service = createActor(machine); + service.subscribe({ + complete: () => { + const snapshot = service.getSnapshot(); + expect(typeof snapshot.context.result1).toBe('number'); + expect(typeof snapshot.context.result2).toBe('number'); + expect(snapshot.context.result1).not.toBe( + snapshot.context.result2 + ); + resolve(); + } + }); + service.start(); + })); - it('should not emit onSnapshot if stopped', (done) => { - const machine = createMachine({ - initial: 'active', - states: { - active: { - invoke: { - src: fromPromise(() => - createPromise((res) => { - setTimeout(() => res(42), 5); - }) - ), - onSnapshot: {} + it('should not emit onSnapshot if stopped', () => + new Promise((resolve) => { + const machine = createMachine({ + initial: 'active', + states: { + active: { + invoke: { + src: fromPromise(() => + createPromise((res) => { + setTimeout(() => res(42), 5); + }) + ), + onSnapshot: {} + }, + on: { + deactivate: 'inactive' + } }, - on: { - deactivate: 'inactive' - } - }, - inactive: { - on: { - '*': { - actions: ({ event }) => { - if (event.snapshot) { - throw new Error( - `Received unexpected event: ${event.type}` - ); + inactive: { + on: { + '*': { + actions: ({ event }) => { + if (event.snapshot) { + throw new Error( + `Received unexpected event: ${event.type}` + ); + } } } } } } - } - }); + }); - const actor = createActor(machine).start(); - actor.send({ type: 'deactivate' }); + const actor = createActor(machine).start(); + actor.send({ type: 'deactivate' }); - setTimeout(() => { - done(); - }, 10); - }); + setTimeout(() => { + resolve(); + }, 10); + })); }); }); describe('with callbacks', () => { - it('should be able to specify a callback as a service', (done) => { - interface BeginEvent { - type: 'BEGIN'; - payload: boolean; - } - interface CallbackEvent { - type: 'CALLBACK'; - data: number; - } - - const someCallback = fromCallback( - ({ - sendBack, - input - }: { - sendBack: (event: BeginEvent | CallbackEvent) => void; - input: { foo: boolean; event: BeginEvent | CallbackEvent }; - }) => { - if (input.foo && input.event.type === 'BEGIN') { - sendBack({ - type: 'CALLBACK', - data: 40 - }); - sendBack({ - type: 'CALLBACK', - data: 41 - }); - sendBack({ - type: 'CALLBACK', - data: 42 - }); - } + it('should be able to specify a callback as a service', () => + new Promise((resolve) => { + interface BeginEvent { + type: 'BEGIN'; + payload: boolean; + } + interface CallbackEvent { + type: 'CALLBACK'; + data: number; } - ); - const callbackMachine = createMachine( - { - id: 'callback', - types: {} as { - context: { foo: boolean }; - events: BeginEvent | CallbackEvent; - actors: { - src: 'someCallback'; - logic: typeof someCallback; - }; - }, - initial: 'pending', - context: { - foo: true - }, - states: { - pending: { - on: { - BEGIN: 'first' - } + const someCallback = fromCallback( + ({ + sendBack, + input + }: { + sendBack: (event: BeginEvent | CallbackEvent) => void; + input: { foo: boolean; event: BeginEvent | CallbackEvent }; + }) => { + if (input.foo && input.event.type === 'BEGIN') { + sendBack({ + type: 'CALLBACK', + data: 40 + }); + sendBack({ + type: 'CALLBACK', + data: 41 + }); + sendBack({ + type: 'CALLBACK', + data: 42 + }); + } + } + ); + + const callbackMachine = createMachine( + { + id: 'callback', + types: {} as { + context: { foo: boolean }; + events: BeginEvent | CallbackEvent; + actors: { + src: 'someCallback'; + logic: typeof someCallback; + }; }, - first: { - invoke: { - src: 'someCallback', - input: ({ context, event }) => ({ - foo: context.foo, - event: event - }) + initial: 'pending', + context: { + foo: true + }, + states: { + pending: { + on: { + BEGIN: 'first' + } }, - on: { - CALLBACK: { - target: 'last', - guard: ({ event }) => event.data === 42 + first: { + invoke: { + src: 'someCallback', + input: ({ context, event }) => ({ + foo: context.foo, + event: event + }) + }, + on: { + CALLBACK: { + target: 'last', + guard: ({ event }) => event.data === 42 + } } + }, + last: { + type: 'final' } - }, - last: { - type: 'final' + } + }, + { + actors: { + someCallback } } - }, - { - actors: { - someCallback - } - } - ); + ); - const actor = createActor(callbackMachine); - actor.subscribe({ complete: () => done() }); - actor.start(); - actor.send({ - type: 'BEGIN', - payload: true - }); - }); + const actor = createActor(callbackMachine); + actor.subscribe({ complete: () => resolve() }); + actor.start(); + actor.send({ + type: 'BEGIN', + payload: true + }); + })); it('should transition correctly if callback function sends an event', () => { const callbackMachine = createMachine( @@ -1545,48 +1579,49 @@ describe('invoke', () => { } }); - it('should treat a callback source as an event stream', (done) => { - const intervalMachine = createMachine({ - types: {} as { context: { count: number } }, - id: 'interval', - initial: 'counting', - context: { - count: 0 - }, - states: { - counting: { - invoke: { - id: 'intervalService', - src: fromCallback(({ sendBack }) => { - const ivl = setInterval(() => { - sendBack({ type: 'INC' }); - }, 10); + it('should treat a callback source as an event stream', () => + new Promise((resolve) => { + const intervalMachine = createMachine({ + types: {} as { context: { count: number } }, + id: 'interval', + initial: 'counting', + context: { + count: 0 + }, + states: { + counting: { + invoke: { + id: 'intervalService', + src: fromCallback(({ sendBack }) => { + const ivl = setInterval(() => { + sendBack({ type: 'INC' }); + }, 10); - return () => clearInterval(ivl); - }) - }, - always: { - target: 'finished', - guard: ({ context }) => context.count === 3 - }, - on: { - INC: { - actions: assign({ count: ({ context }) => context.count + 1 }) + return () => clearInterval(ivl); + }) + }, + always: { + target: 'finished', + guard: ({ context }) => context.count === 3 + }, + on: { + INC: { + actions: assign({ count: ({ context }) => context.count + 1 }) + } } + }, + finished: { + type: 'final' } - }, - finished: { - type: 'final' } - } - }); - const actor = createActor(intervalMachine); - actor.subscribe({ complete: () => done() }); - actor.start(); - }); + }); + const actor = createActor(intervalMachine); + actor.subscribe({ complete: () => resolve() }); + actor.start(); + })); it('should dispose of the callback (if disposal function provided)', () => { - const spy = jest.fn(); + const spy = vi.fn(); const intervalMachine = createMachine({ id: 'interval', initial: 'counting', @@ -1610,67 +1645,69 @@ describe('invoke', () => { expect(spy).toHaveBeenCalled(); }); - it('callback should be able to receive messages from parent', (done) => { - const pingPongMachine = createMachine({ - id: 'ping-pong', - initial: 'active', - states: { - active: { - invoke: { - id: 'child', - src: fromCallback(({ sendBack, receive }) => { - receive((e) => { - if (e.type === 'PING') { - sendBack({ type: 'PONG' }); - } - }); - }) + it('callback should be able to receive messages from parent', () => + new Promise((resolve) => { + const pingPongMachine = createMachine({ + id: 'ping-pong', + initial: 'active', + states: { + active: { + invoke: { + id: 'child', + src: fromCallback(({ sendBack, receive }) => { + receive((e) => { + if (e.type === 'PING') { + sendBack({ type: 'PONG' }); + } + }); + }) + }, + entry: sendTo('child', { type: 'PING' }), + on: { + PONG: 'done' + } }, - entry: sendTo('child', { type: 'PING' }), - on: { - PONG: 'done' + done: { + type: 'final' } - }, - done: { - type: 'final' } - } - }); - const actor = createActor(pingPongMachine); - actor.subscribe({ complete: () => done() }); - actor.start(); - }); + }); + const actor = createActor(pingPongMachine); + actor.subscribe({ complete: () => resolve() }); + actor.start(); + })); - it('should call onError upon error (sync)', (done) => { - const errorMachine = createMachine({ - id: 'error', - initial: 'safe', - states: { - safe: { - invoke: { - src: fromCallback(() => { - throw new Error('test'); - }), - onError: { - target: 'failed', - guard: ({ event }) => { - return ( - event.error instanceof Error && - event.error.message === 'test' - ); + it('should call onError upon error (sync)', () => + new Promise((resolve) => { + const errorMachine = createMachine({ + id: 'error', + initial: 'safe', + states: { + safe: { + invoke: { + src: fromCallback(() => { + throw new Error('test'); + }), + onError: { + target: 'failed', + guard: ({ event }) => { + return ( + event.error instanceof Error && + event.error.message === 'test' + ); + } } } + }, + failed: { + type: 'final' } - }, - failed: { - type: 'final' } - } - }); - const actor = createActor(errorMachine); - actor.subscribe({ complete: () => done() }); - actor.start(); - }); + }); + const actor = createActor(errorMachine); + actor.subscribe({ complete: () => resolve() }); + actor.start(); + })); it('should transition correctly upon error (sync)', () => { const errorMachine = createMachine({ @@ -1795,7 +1832,7 @@ describe('invoke', () => { } } }); - const spy = jest.fn(); + const spy = vi.fn(); const actorRef = createActor(errorMachine); actorRef.subscribe({ @@ -1811,28 +1848,29 @@ describe('invoke', () => { `); }); - it('should work with input', (done) => { - const machine = createMachine({ - types: {} as { - context: { foo: string }; - }, - initial: 'start', - context: { foo: 'bar' }, - states: { - start: { - invoke: { - src: fromCallback(({ input }) => { - expect(input).toEqual({ foo: 'bar' }); - done(); - }), - input: ({ context }: any) => context + it('should work with input', () => + new Promise((resolve) => { + const machine = createMachine({ + types: {} as { + context: { foo: string }; + }, + initial: 'start', + context: { foo: 'bar' }, + states: { + start: { + invoke: { + src: fromCallback(({ input }) => { + expect(input).toEqual({ foo: 'bar' }); + resolve(); + }), + input: ({ context }: any) => context + } } } - } - }); + }); - createActor(machine).start(); - }); + createActor(machine).start(); + })); it('sub invoke race condition ends on the completed state', () => { const anotherChildMachine = createMachine({ @@ -1853,605 +1891,628 @@ describe('invoke', () => { initial: 'begin', states: { begin: { - invoke: { - src: anotherChildMachine, - id: 'invoked.child', - onDone: 'completed' - }, - on: { - STOPCHILD: { - actions: sendTo('invoked.child', { type: 'STOP' }) - } - } - }, - completed: { - type: 'final' - } - } - }); - - const actorRef = createActor(anotherParentMachine).start(); - actorRef.send({ type: 'STOPCHILD' }); - - expect(actorRef.getSnapshot().value).toEqual('completed'); - }); - }); - - describe('with observables', () => { - it('should work with an infinite observable', (done) => { - interface Events { - type: 'COUNT'; - value: number; - } - const obsMachine = createMachine({ - types: {} as { context: { count: number | undefined }; events: Events }, - id: 'infiniteObs', - initial: 'counting', - context: { count: undefined }, - states: { - counting: { - invoke: { - src: fromObservable(() => interval(10)), - onSnapshot: { - actions: assign({ - count: ({ event }) => event.snapshot.context - }) - } - }, - always: { - target: 'counted', - guard: ({ context }) => context.count === 5 - } - }, - counted: { - type: 'final' - } - } - }); - - const service = createActor(obsMachine); - service.subscribe({ - complete: () => { - done(); - } - }); - service.start(); - }); - - it('should work with a finite observable', (done) => { - interface Ctx { - count: number | undefined; - } - interface Events { - type: 'COUNT'; - value: number; - } - const obsMachine = createMachine({ - types: {} as { context: Ctx; events: Events }, - id: 'obs', - initial: 'counting', - context: { - count: undefined - }, - states: { - counting: { - invoke: { - src: fromObservable(() => interval(10).pipe(take(5))), - onSnapshot: { - actions: assign({ - count: ({ event }) => event.snapshot.context - }) - }, - onDone: { - target: 'counted', - guard: ({ context }) => context.count === 4 - } - } - }, - counted: { - type: 'final' - } - } - }); - - const actor = createActor(obsMachine); - actor.subscribe({ - complete: () => { - done(); - } - }); - actor.start(); - }); - - it('should receive an emitted error', (done) => { - interface Ctx { - count: number | undefined; - } - interface Events { - type: 'COUNT'; - value: number; - } - const obsMachine = createMachine({ - types: {} as { context: Ctx; events: Events }, - id: 'obs', - initial: 'counting', - context: { count: undefined }, - states: { - counting: { - invoke: { - src: fromObservable(() => - interval(10).pipe( - map((value) => { - if (value === 5) { - throw new Error('some error'); - } - - return value; - }) - ) - ), - onSnapshot: { - actions: assign({ - count: ({ event }) => event.snapshot.context - }) - }, - onError: { - target: 'success', - guard: ({ context, event }) => { - expect((event.error as any).message).toEqual('some error'); - return ( - context.count === 4 && - (event.error as any).message === 'some error' - ); - } + invoke: { + src: anotherChildMachine, + id: 'invoked.child', + onDone: 'completed' + }, + on: { + STOPCHILD: { + actions: sendTo('invoked.child', { type: 'STOP' }) } } }, - success: { + completed: { type: 'final' } } }); - const actor = createActor(obsMachine); - actor.subscribe({ - complete: () => { - done(); - } - }); - actor.start(); - }); + const actorRef = createActor(anotherParentMachine).start(); + actorRef.send({ type: 'STOPCHILD' }); - it('should work with input', (done) => { - const childLogic = fromObservable(({ input }: { input: number }) => - of(input) - ); + expect(actorRef.getSnapshot().value).toEqual('completed'); + }); + }); - const machine = createMachine( - { + describe('with observables', () => { + it('should work with an infinite observable', () => + new Promise((resolve) => { + interface Events { + type: 'COUNT'; + value: number; + } + const obsMachine = createMachine({ types: {} as { - actors: { - src: 'childLogic'; - logic: typeof childLogic; - }; + context: { count: number | undefined }; + events: Events; }, - context: { received: undefined }, - invoke: { - src: 'childLogic', - input: 42, - onSnapshot: { - actions: ({ event }) => { - if ( - event.snapshot.status === 'active' && - event.snapshot.context === 42 - ) { - done(); + id: 'infiniteObs', + initial: 'counting', + context: { count: undefined }, + states: { + counting: { + invoke: { + src: fromObservable(() => interval(10)), + onSnapshot: { + actions: assign({ + count: ({ event }) => event.snapshot.context + }) } + }, + always: { + target: 'counted', + guard: ({ context }) => context.count === 5 } + }, + counted: { + type: 'final' } } - }, - { - actors: { - childLogic + }); + + const service = createActor(obsMachine); + service.subscribe({ + complete: () => { + resolve(); } + }); + service.start(); + })); + + it('should work with a finite observable', () => + new Promise((resolve) => { + interface Ctx { + count: number | undefined; } - ); + interface Events { + type: 'COUNT'; + value: number; + } + const obsMachine = createMachine({ + types: {} as { context: Ctx; events: Events }, + id: 'obs', + initial: 'counting', + context: { + count: undefined + }, + states: { + counting: { + invoke: { + src: fromObservable(() => interval(10).pipe(take(5))), + onSnapshot: { + actions: assign({ + count: ({ event }) => event.snapshot.context + }) + }, + onDone: { + target: 'counted', + guard: ({ context }) => context.count === 4 + } + } + }, + counted: { + type: 'final' + } + } + }); - createActor(machine).start(); - }); - }); + const actor = createActor(obsMachine); + actor.subscribe({ + complete: () => { + resolve(); + } + }); + actor.start(); + })); + + it('should receive an emitted error', () => + new Promise((resolve) => { + interface Ctx { + count: number | undefined; + } + interface Events { + type: 'COUNT'; + value: number; + } + const obsMachine = createMachine({ + types: {} as { context: Ctx; events: Events }, + id: 'obs', + initial: 'counting', + context: { count: undefined }, + states: { + counting: { + invoke: { + src: fromObservable(() => + interval(10).pipe( + map((value) => { + if (value === 5) { + throw new Error('some error'); + } - describe('with event observables', () => { - it('should work with an infinite event observable', (done) => { - interface Events { - type: 'COUNT'; - value: number; - } - const obsMachine = createMachine({ - types: {} as { context: { count: number | undefined }; events: Events }, - id: 'obs', - initial: 'counting', - context: { count: undefined }, - states: { - counting: { - invoke: { - src: fromEventObservable(() => - interval(10).pipe(map((value) => ({ type: 'COUNT', value }))) - ) - }, - on: { - COUNT: { - actions: assign({ count: ({ event }) => event.value }) + return value; + }) + ) + ), + onSnapshot: { + actions: assign({ + count: ({ event }) => event.snapshot.context + }) + }, + onError: { + target: 'success', + guard: ({ context, event }) => { + expect((event.error as any).message).toEqual('some error'); + return ( + context.count === 4 && + (event.error as any).message === 'some error' + ); + } + } } }, - always: { - target: 'counted', - guard: ({ context }) => context.count === 5 + success: { + type: 'final' } - }, - counted: { - type: 'final' } - } - }); + }); - const service = createActor(obsMachine); - service.subscribe({ - complete: () => { - done(); - } - }); - service.start(); - }); + const actor = createActor(obsMachine); + actor.subscribe({ + complete: () => { + resolve(); + } + }); + actor.start(); + })); - it('should work with a finite event observable', (done) => { - interface Ctx { - count: number | undefined; - } - interface Events { - type: 'COUNT'; - value: number; - } - const obsMachine = createMachine({ - types: {} as { context: Ctx; events: Events }, - id: 'obs', - initial: 'counting', - context: { - count: undefined - }, - states: { - counting: { - invoke: { - src: fromEventObservable(() => - interval(10).pipe( - take(5), - map((value) => ({ type: 'COUNT', value })) - ) - ), - onDone: { - target: 'counted', - guard: ({ context }) => context.count === 4 - } + it('should work with input', () => + new Promise((resolve) => { + const childLogic = fromObservable(({ input }: { input: number }) => + of(input) + ); + + const machine = createMachine( + { + types: {} as { + actors: { + src: 'childLogic'; + logic: typeof childLogic; + }; }, - on: { - COUNT: { - actions: assign({ - count: ({ event }) => event.value - }) + context: { received: undefined }, + invoke: { + src: 'childLogic', + input: 42, + onSnapshot: { + actions: ({ event }) => { + if ( + event.snapshot.status === 'active' && + event.snapshot.context === 42 + ) { + resolve(); + } + } } } }, - counted: { - type: 'final' + { + actors: { + childLogic + } } - } - }); + ); - const actor = createActor(obsMachine); - actor.subscribe({ - complete: () => { - done(); + createActor(machine).start(); + })); + }); + + describe('with event observables', () => { + it('should work with an infinite event observable', () => + new Promise((resolve) => { + interface Events { + type: 'COUNT'; + value: number; } - }); - actor.start(); - }); + const obsMachine = createMachine({ + types: {} as { + context: { count: number | undefined }; + events: Events; + }, + id: 'obs', + initial: 'counting', + context: { count: undefined }, + states: { + counting: { + invoke: { + src: fromEventObservable(() => + interval(10).pipe(map((value) => ({ type: 'COUNT', value }))) + ) + }, + on: { + COUNT: { + actions: assign({ count: ({ event }) => event.value }) + } + }, + always: { + target: 'counted', + guard: ({ context }) => context.count === 5 + } + }, + counted: { + type: 'final' + } + } + }); - it('should receive an emitted error', (done) => { - interface Ctx { - count: number | undefined; - } - interface Events { - type: 'COUNT'; - value: number; - } - const obsMachine = createMachine({ - types: {} as { context: Ctx; events: Events }, - id: 'obs', - initial: 'counting', - context: { count: undefined }, - states: { - counting: { - invoke: { - src: fromEventObservable(() => - interval(10).pipe( - map((value) => { - if (value === 5) { - throw new Error('some error'); - } + const service = createActor(obsMachine); + service.subscribe({ + complete: () => { + resolve(); + } + }); + service.start(); + })); - return { type: 'COUNT', value }; + it('should work with a finite event observable', () => + new Promise((resolve) => { + interface Ctx { + count: number | undefined; + } + interface Events { + type: 'COUNT'; + value: number; + } + const obsMachine = createMachine({ + types: {} as { context: Ctx; events: Events }, + id: 'obs', + initial: 'counting', + context: { + count: undefined + }, + states: { + counting: { + invoke: { + src: fromEventObservable(() => + interval(10).pipe( + take(5), + map((value) => ({ type: 'COUNT', value })) + ) + ), + onDone: { + target: 'counted', + guard: ({ context }) => context.count === 4 + } + }, + on: { + COUNT: { + actions: assign({ + count: ({ event }) => event.value }) - ) - ), - onError: { - target: 'success', - guard: ({ context, event }) => { - expect((event.error as any).message).toEqual('some error'); - return ( - context.count === 4 && - (event.error as any).message === 'some error' - ); } } }, - on: { - COUNT: { - actions: assign({ count: ({ event }) => event.value }) + counted: { + type: 'final' + } + } + }); + + const actor = createActor(obsMachine); + actor.subscribe({ + complete: () => { + resolve(); + } + }); + actor.start(); + })); + + it('should receive an emitted error', () => + new Promise((resolve) => { + interface Ctx { + count: number | undefined; + } + interface Events { + type: 'COUNT'; + value: number; + } + const obsMachine = createMachine({ + types: {} as { context: Ctx; events: Events }, + id: 'obs', + initial: 'counting', + context: { count: undefined }, + states: { + counting: { + invoke: { + src: fromEventObservable(() => + interval(10).pipe( + map((value) => { + if (value === 5) { + throw new Error('some error'); + } + + return { type: 'COUNT', value }; + }) + ) + ), + onError: { + target: 'success', + guard: ({ context, event }) => { + expect((event.error as any).message).toEqual('some error'); + return ( + context.count === 4 && + (event.error as any).message === 'some error' + ); + } + } + }, + on: { + COUNT: { + actions: assign({ count: ({ event }) => event.value }) + } } + }, + success: { + type: 'final' } - }, - success: { - type: 'final' } - } - }); + }); - const actor = createActor(obsMachine); - actor.subscribe({ - complete: () => { - done(); - } - }); - actor.start(); - }); + const actor = createActor(obsMachine); + actor.subscribe({ + complete: () => { + resolve(); + } + }); + actor.start(); + })); - it('should work with input', (done) => { - const machine = createMachine({ - invoke: { - src: fromEventObservable(({ input }) => - of({ - type: 'obs.event', - value: input - }) - ), - input: 42 - }, - on: { - 'obs.event': { - actions: ({ event }) => { - expect(event.value).toEqual(42); - done(); + it('should work with input', () => + new Promise((resolve) => { + const machine = createMachine({ + invoke: { + src: fromEventObservable(({ input }) => + of({ + type: 'obs.event', + value: input + }) + ), + input: 42 + }, + on: { + 'obs.event': { + actions: ({ event }) => { + expect(event.value).toEqual(42); + resolve(); + } } } - } - }); + }); - createActor(machine).start(); - }); + createActor(machine).start(); + })); }); describe('with logic', () => { - it('should work with actor logic', (done) => { - const countLogic: ActorLogic< - Snapshot & { context: number }, - EventObject - > = { - transition: (state, event) => { - if (event.type === 'INC') { - return { - ...state, - context: state.context + 1 - }; - } else if (event.type === 'DEC') { - return { - ...state, - context: state.context - 1 - }; - } - return state; - }, - getInitialSnapshot: () => ({ - status: 'active', - output: undefined, - error: undefined, - context: 0 - }), - getPersistedSnapshot: (s) => s - }; + it('should work with actor logic', () => + new Promise((resolve) => { + const countLogic: ActorLogic< + Snapshot & { context: number }, + EventObject + > = { + transition: (state, event) => { + if (event.type === 'INC') { + return { + ...state, + context: state.context + 1 + }; + } else if (event.type === 'DEC') { + return { + ...state, + context: state.context - 1 + }; + } + return state; + }, + getInitialSnapshot: () => ({ + status: 'active', + output: undefined, + error: undefined, + context: 0 + }), + getPersistedSnapshot: (s) => s + }; - const countMachine = createMachine({ - invoke: { - id: 'count', - src: countLogic - }, - on: { - INC: { - actions: forwardTo('count') + const countMachine = createMachine({ + invoke: { + id: 'count', + src: countLogic + }, + on: { + INC: { + actions: forwardTo('count') + } } - } - }); + }); - const countService = createActor(countMachine); - countService.subscribe((state) => { - if (state.children['count']?.getSnapshot().context === 2) { - done(); - } - }); - countService.start(); + const countService = createActor(countMachine); + countService.subscribe((state) => { + if (state.children['count']?.getSnapshot().context === 2) { + resolve(); + } + }); + countService.start(); - countService.send({ type: 'INC' }); - countService.send({ type: 'INC' }); - }); + countService.send({ type: 'INC' }); + countService.send({ type: 'INC' }); + })); - it('logic should have reference to the parent', (done) => { - const pongLogic: ActorLogic, EventObject> = { - transition: (state, event, { self }) => { - if (event.type === 'PING') { - self._parent?.send({ type: 'PONG' }); - } + it('logic should have reference to the parent', () => + new Promise((resolve) => { + const pongLogic: ActorLogic, EventObject> = { + transition: (state, event, { self }) => { + if (event.type === 'PING') { + self._parent?.send({ type: 'PONG' }); + } - return state; - }, - getInitialSnapshot: () => ({ - status: 'active', - output: undefined, - error: undefined - }), - getPersistedSnapshot: (s) => s - }; + return state; + }, + getInitialSnapshot: () => ({ + status: 'active', + output: undefined, + error: undefined + }), + getPersistedSnapshot: (s) => s + }; - const pingMachine = createMachine({ - initial: 'waiting', - states: { - waiting: { - entry: sendTo('ponger', { type: 'PING' }), - invoke: { - id: 'ponger', - src: pongLogic + const pingMachine = createMachine({ + initial: 'waiting', + states: { + waiting: { + entry: sendTo('ponger', { type: 'PING' }), + invoke: { + id: 'ponger', + src: pongLogic + }, + on: { + PONG: 'success' + } }, - on: { - PONG: 'success' + success: { + type: 'final' } - }, - success: { - type: 'final' } - } - }); + }); - const pingService = createActor(pingMachine); - pingService.subscribe({ - complete: () => { - done(); - } - }); - pingService.start(); - }); + const pingService = createActor(pingMachine); + pingService.subscribe({ + complete: () => { + resolve(); + } + }); + pingService.start(); + })); }); describe('with transition functions', () => { - it('should work with a transition function', (done) => { - const countReducer = ( - count: number, - event: { type: 'INC' } | { type: 'DEC' } - ): number => { - if (event.type === 'INC') { - return count + 1; - } else if (event.type === 'DEC') { - return count - 1; - } - return count; - }; - - const countMachine = createMachine({ - invoke: { - id: 'count', - src: fromTransition(countReducer, 0) - }, - on: { - INC: { - actions: forwardTo('count') + it('should work with a transition function', () => + new Promise((resolve) => { + const countReducer = ( + count: number, + event: { type: 'INC' } | { type: 'DEC' } + ): number => { + if (event.type === 'INC') { + return count + 1; + } else if (event.type === 'DEC') { + return count - 1; } - } - }); + return count; + }; - const countService = createActor(countMachine); - countService.subscribe((state) => { - if (state.children['count']?.getSnapshot().context === 2) { - done(); - } - }); - countService.start(); + const countMachine = createMachine({ + invoke: { + id: 'count', + src: fromTransition(countReducer, 0) + }, + on: { + INC: { + actions: forwardTo('count') + } + } + }); - countService.send({ type: 'INC' }); - countService.send({ type: 'INC' }); - }); + const countService = createActor(countMachine); + countService.subscribe((state) => { + if (state.children['count']?.getSnapshot().context === 2) { + resolve(); + } + }); + countService.start(); - it('should schedule events in a FIFO queue', (done) => { - type CountEvents = { type: 'INC' } | { type: 'DOUBLE' }; + countService.send({ type: 'INC' }); + countService.send({ type: 'INC' }); + })); - const countReducer = ( - count: number, - event: CountEvents, - { self }: ActorScope - ): number => { - if (event.type === 'INC') { - self.send({ type: 'DOUBLE' }); - return count + 1; - } - if (event.type === 'DOUBLE') { - return count * 2; - } + it('should schedule events in a FIFO queue', () => + new Promise((resolve) => { + type CountEvents = { type: 'INC' } | { type: 'DOUBLE' }; - return count; - }; + const countReducer = ( + count: number, + event: CountEvents, + { self }: ActorScope + ): number => { + if (event.type === 'INC') { + self.send({ type: 'DOUBLE' }); + return count + 1; + } + if (event.type === 'DOUBLE') { + return count * 2; + } - const countMachine = createMachine({ - invoke: { - id: 'count', - src: fromTransition(countReducer, 0) - }, - on: { - INC: { - actions: forwardTo('count') + return count; + }; + + const countMachine = createMachine({ + invoke: { + id: 'count', + src: fromTransition(countReducer, 0) + }, + on: { + INC: { + actions: forwardTo('count') + } } - } - }); + }); - const countService = createActor(countMachine); - countService.subscribe((state) => { - if (state.children['count']?.getSnapshot().context === 2) { - done(); - } - }); - countService.start(); + const countService = createActor(countMachine); + countService.subscribe((state) => { + if (state.children['count']?.getSnapshot().context === 2) { + resolve(); + } + }); + countService.start(); - countService.send({ type: 'INC' }); - }); + countService.send({ type: 'INC' }); + })); - it('should emit onSnapshot', (done) => { - const doublerLogic = fromTransition( - (_, event: { type: 'update'; value: number }) => event.value * 2, - 0 - ); - const machine = createMachine( - { - types: {} as { - actors: { src: 'doublerLogic'; logic: typeof doublerLogic }; - }, - invoke: { - id: 'doubler', - src: 'doublerLogic', - onSnapshot: { - actions: ({ event }) => { - if (event.snapshot.context === 42) { - done(); + it('should emit onSnapshot', () => + new Promise((resolve) => { + const doublerLogic = fromTransition( + (_, event: { type: 'update'; value: number }) => event.value * 2, + 0 + ); + const machine = createMachine( + { + types: {} as { + actors: { src: 'doublerLogic'; logic: typeof doublerLogic }; + }, + invoke: { + id: 'doubler', + src: 'doublerLogic', + onSnapshot: { + actions: ({ event }) => { + if (event.snapshot.context === 42) { + resolve(); + } } } - } + }, + entry: sendTo( + 'doubler', + { type: 'update', value: 21 }, + { delay: 10 } + ) }, - entry: sendTo('doubler', { type: 'update', value: 21 }, { delay: 10 }) - }, - { - actors: { - doublerLogic + { + actors: { + doublerLogic + } } - } - ); + ); - createActor(machine).start(); - }); + createActor(machine).start(); + })); }); describe('with machines', () => { @@ -2499,49 +2560,51 @@ describe('invoke', () => { } }); - it('should create invocations from machines in nested states', (done) => { - const actor = createActor(pingMachine); - actor.subscribe({ complete: () => done() }); - actor.start(); - }); + it('should create invocations from machines in nested states', () => + new Promise((resolve) => { + const actor = createActor(pingMachine); + actor.subscribe({ complete: () => resolve() }); + actor.start(); + })); - it('should emit onSnapshot', (done) => { - const childMachine = createMachine({ - initial: 'a', - states: { - a: { - after: { - 10: 'b' - } - }, - b: {} - } - }); - const machine = createMachine( - { - types: {} as { - actors: { src: 'childMachine'; logic: typeof childMachine }; - }, - invoke: { - src: 'childMachine', - onSnapshot: { - actions: ({ event }) => { - if (event.snapshot.value === 'b') { - done(); + it('should emit onSnapshot', () => + new Promise((resolve) => { + const childMachine = createMachine({ + initial: 'a', + states: { + a: { + after: { + 10: 'b' + } + }, + b: {} + } + }); + const machine = createMachine( + { + types: {} as { + actors: { src: 'childMachine'; logic: typeof childMachine }; + }, + invoke: { + src: 'childMachine', + onSnapshot: { + actions: ({ event }) => { + if (event.snapshot.value === 'b') { + resolve(); + } } } } + }, + { + actors: { + childMachine + } } - }, - { - actors: { - childMachine - } - } - ); + ); - createActor(machine).start(); - }); + createActor(machine).start(); + })); }); describe('multiple simultaneous services', () => { @@ -2591,20 +2654,21 @@ describe('invoke', () => { } }); - it('should start all services at once', (done) => { - const service = createActor(multiple); - service.subscribe({ - complete: () => { - expect(service.getSnapshot().context).toEqual({ - one: 'one', - two: 'two' - }); - done(); - } - }); + it('should start all services at once', () => + new Promise((resolve) => { + const service = createActor(multiple); + service.subscribe({ + complete: () => { + expect(service.getSnapshot().context).toEqual({ + one: 'one', + two: 'two' + }); + resolve(); + } + }); - service.start(); - }); + service.start(); + })); const parallel = createMachine({ types: {} as { context: { one?: string; two?: string } }, @@ -2666,20 +2730,21 @@ describe('invoke', () => { } }); - it('should run services in parallel', (done) => { - const service = createActor(parallel); - service.subscribe({ - complete: () => { - expect(service.getSnapshot().context).toEqual({ - one: 'one', - two: 'two' - }); - done(); - } - }); + it('should run services in parallel', () => + new Promise((resolve) => { + const service = createActor(parallel); + service.subscribe({ + complete: () => { + expect(service.getSnapshot().context).toEqual({ + one: 'one', + two: 'two' + }); + resolve(); + } + }); - service.start(); - }); + service.start(); + })); it('should not invoke an actor if it gets stopped immediately by transitioning away in immediate microstep', () => { // Since an actor will be canceled when the state machine leaves the invoking state @@ -2749,66 +2814,67 @@ describe('invoke', () => { expect(actorStarted).toBe(false); }); - it('should invoke a service if other service gets stopped in subsequent microstep (#1180)', (done) => { - const machine = createMachine({ - initial: 'running', - states: { - running: { - type: 'parallel', - states: { - one: { - initial: 'active', - on: { - STOP_ONE: '.idle' - }, - states: { - idle: {}, - active: { - invoke: { - id: 'active', - src: fromCallback(() => { - /* ... */ - }) - }, - on: { - NEXT: { - actions: raise({ type: 'STOP_ONE' }) + it('should invoke a service if other service gets stopped in subsequent microstep (#1180)', () => + new Promise((resolve) => { + const machine = createMachine({ + initial: 'running', + states: { + running: { + type: 'parallel', + states: { + one: { + initial: 'active', + on: { + STOP_ONE: '.idle' + }, + states: { + idle: {}, + active: { + invoke: { + id: 'active', + src: fromCallback(() => { + /* ... */ + }) + }, + on: { + NEXT: { + actions: raise({ type: 'STOP_ONE' }) + } } } } - } - }, - two: { - initial: 'idle', - on: { - NEXT: '.active' }, - states: { - idle: {}, - active: { - invoke: { - id: 'post', - src: fromPromise(() => Promise.resolve(42)), - onDone: '#done' + two: { + initial: 'idle', + on: { + NEXT: '.active' + }, + states: { + idle: {}, + active: { + invoke: { + id: 'post', + src: fromPromise(() => Promise.resolve(42)), + onDone: '#done' + } } } } } + }, + done: { + id: 'done', + type: 'final' } - }, - done: { - id: 'done', - type: 'final' } - } - }); + }); - const service = createActor(machine); - service.subscribe({ complete: () => done() }); - service.start(); + const service = createActor(machine); + service.subscribe({ complete: () => resolve() }); + service.start(); - service.send({ type: 'NEXT' }); - }); + service.send({ type: 'NEXT' }); + })); it('should invoke an actor when reentering invoking state within a single macrostep', () => { let actorStartedCount = 0; @@ -2846,50 +2912,51 @@ describe('invoke', () => { }); }); - it('invoke `src` can be used with invoke `input`', (done) => { - const machine = createMachine( - { - types: {} as { - actors: { - src: 'search'; - logic: PromiseActorLogic< - number, - { - endpoint: string; + it('invoke `src` can be used with invoke `input`', () => + new Promise((resolve) => { + const machine = createMachine( + { + types: {} as { + actors: { + src: 'search'; + logic: PromiseActorLogic< + number, + { + endpoint: string; + } + >; + }; + }, + initial: 'searching', + states: { + searching: { + invoke: { + src: 'search', + input: { + endpoint: 'example.com' + }, + onDone: 'success' } - >; - }; - }, - initial: 'searching', - states: { - searching: { - invoke: { - src: 'search', - input: { - endpoint: 'example.com' - }, - onDone: 'success' + }, + success: { + type: 'final' } - }, - success: { - type: 'final' } - } - }, - { - actors: { - search: fromPromise(async ({ input }) => { - expect(input.endpoint).toEqual('example.com'); + }, + { + actors: { + search: fromPromise(async ({ input }) => { + expect(input.endpoint).toEqual('example.com'); - return 42; - }) + return 42; + }) + } } - } - ); - const actor = createActor(machine); - actor.subscribe({ complete: () => done() }); - actor.start(); - }); + ); + const actor = createActor(machine); + actor.subscribe({ complete: () => resolve() }); + actor.start(); + })); it('invoke `src` can be used with dynamic invoke `input`', async () => { const machine = createMachine( @@ -2941,45 +3008,46 @@ describe('invoke', () => { }); }); - it('invoke generated ID should be predictable based on the state node where it is defined', (done) => { - const machine = createMachine( - { - initial: 'a', - states: { - a: { - invoke: { - src: 'someSrc', - onDone: { - guard: ({ event }) => { - // invoke ID should not be 'someSrc' - const expectedType = 'xstate.done.actor.0.(machine).a'; - expect(event.type).toEqual(expectedType); - return event.type === expectedType; - }, - target: 'b' + it('invoke generated ID should be predictable based on the state node where it is defined', () => + new Promise((resolve) => { + const machine = createMachine( + { + initial: 'a', + states: { + a: { + invoke: { + src: 'someSrc', + onDone: { + guard: ({ event }) => { + // invoke ID should not be 'someSrc' + const expectedType = 'xstate.done.actor.0.(machine).a'; + expect(event.type).toEqual(expectedType); + return event.type === expectedType; + }, + target: 'b' + } } + }, + b: { + type: 'final' } - }, - b: { - type: 'final' + } + }, + { + actors: { + someSrc: fromPromise(() => Promise.resolve()) } } - }, - { - actors: { - someSrc: fromPromise(() => Promise.resolve()) - } - } - ); + ); - const actor = createActor(machine); - actor.subscribe({ - complete: () => { - done(); - } - }); - actor.start(); - }); + const actor = createActor(machine); + actor.subscribe({ + complete: () => { + resolve(); + } + }); + actor.start(); + })); it.each([ ['src with string reference', { src: 'someSrc' }], @@ -3025,128 +3093,130 @@ describe('invoke', () => { ); // https://github.com/statelyai/xstate/issues/464 - it('xstate.done.actor events should only select onDone transition on the invoking state when invokee is referenced using a string', (done) => { - let counter = 0; - let invoked = false; + it('xstate.done.actor events should only select onDone transition on the invoking state when invokee is referenced using a string', () => + new Promise((resolve) => { + let counter = 0; + let invoked = false; - const createSingleState = (): any => ({ - initial: 'fetch', - states: { - fetch: { - invoke: { - src: 'fetchSmth', - onDone: { - actions: 'handleSuccess' + const createSingleState = (): any => ({ + initial: 'fetch', + states: { + fetch: { + invoke: { + src: 'fetchSmth', + onDone: { + actions: 'handleSuccess' + } } } } - } - }); + }); - const testMachine = createMachine( - { - type: 'parallel', - states: { - first: createSingleState(), - second: createSingleState() - } - }, - { - actions: { - handleSuccess: () => { - ++counter; + const testMachine = createMachine( + { + type: 'parallel', + states: { + first: createSingleState(), + second: createSingleState() } }, - actors: { - fetchSmth: fromPromise(() => { - if (invoked) { - // create a promise that won't ever resolve for the second invoking state - return new Promise(() => { - /* ... */ - }); + { + actions: { + handleSuccess: () => { + ++counter; } - invoked = true; - return Promise.resolve(42); - }) + }, + actors: { + fetchSmth: fromPromise(() => { + if (invoked) { + // create a promise that won't ever resolve for the second invoking state + return new Promise(() => { + /* ... */ + }); + } + invoked = true; + return Promise.resolve(42); + }) + } } - } - ); + ); - createActor(testMachine).start(); + createActor(testMachine).start(); - // check within a macrotask so all promise-induced microtasks have a chance to resolve first - setTimeout(() => { - expect(counter).toEqual(1); - done(); - }, 0); - }); + // check within a macrotask so all promise-induced microtasks have a chance to resolve first + setTimeout(() => { + expect(counter).toEqual(1); + resolve(); + }, 0); + })); - it('xstate.done.actor events should have unique names when invokee is a machine with an id property', (done) => { - const actual: AnyEventObject[] = []; + it('xstate.done.actor events should have unique names when invokee is a machine with an id property', () => + new Promise((resolve) => { + const actual: AnyEventObject[] = []; - const childMachine = createMachine({ - id: 'child', - initial: 'a', - states: { - a: { - invoke: { - src: fromPromise(() => { - return Promise.resolve(42); - }), - onDone: 'b' + const childMachine = createMachine({ + id: 'child', + initial: 'a', + states: { + a: { + invoke: { + src: fromPromise(() => { + return Promise.resolve(42); + }), + onDone: 'b' + } + }, + b: { + type: 'final' } - }, - b: { - type: 'final' } - } - }); + }); - const createSingleState = (): any => ({ - initial: 'fetch', - states: { - fetch: { - invoke: { - src: childMachine + const createSingleState = (): any => ({ + initial: 'fetch', + states: { + fetch: { + invoke: { + src: childMachine + } } } - } - }); + }); - const testMachine = createMachine({ - type: 'parallel', - states: { - first: createSingleState(), - second: createSingleState() - }, - on: { - '*': { - actions: ({ event }) => { - actual.push(event); + const testMachine = createMachine({ + type: 'parallel', + states: { + first: createSingleState(), + second: createSingleState() + }, + on: { + '*': { + actions: ({ event }) => { + actual.push(event); + } } } - } - }); + }); - createActor(testMachine).start(); + createActor(testMachine).start(); - // check within a macrotask so all promise-induced microtasks have a chance to resolve first - setTimeout(() => { - expect(actual).toEqual([ - { - type: 'xstate.done.actor.0.(machine).first.fetch', - output: undefined, - actorId: '0.(machine).first.fetch' - }, - { - type: 'xstate.done.actor.0.(machine).second.fetch', - output: undefined, - actorId: '0.(machine).second.fetch' - } - ]); - done(); - }, 100); - }); + // check within a macrotask so all promise-induced microtasks have a chance to resolve first + setTimeout(() => { + expect(actual).toEqual([ + { + type: 'xstate.done.actor.0.(machine).first.fetch', + output: undefined, + actorId: '0.(machine).first.fetch' + }, + { + type: 'xstate.done.actor.0.(machine).second.fetch', + output: undefined, + actorId: '0.(machine).second.fetch' + } + ]); + resolve(); + }, 100); + })); it('should get reinstantiated after reentering the invoking state in a microstep', () => { let invokeCount = 0; @@ -3364,76 +3434,78 @@ describe('invoke', () => { }); describe('invoke input', () => { - it('should provide input to an actor creator', (done) => { - const machine = createMachine( - { - types: {} as { - context: { count: number }; - actors: { - src: 'stringService'; - logic: PromiseActorLogic< - boolean, - { - staticVal: string; - newCount: number; + it('should provide input to an actor creator', () => + new Promise((resolve) => { + const machine = createMachine( + { + types: {} as { + context: { count: number }; + actors: { + src: 'stringService'; + logic: PromiseActorLogic< + boolean, + { + staticVal: string; + newCount: number; + } + >; + }; + }, + initial: 'pending', + context: { + count: 42 + }, + states: { + pending: { + invoke: { + src: 'stringService', + input: ({ context }) => ({ + staticVal: 'hello', + newCount: context.count * 2 + }), + onDone: 'success' } - >; - }; - }, - initial: 'pending', - context: { - count: 42 - }, - states: { - pending: { - invoke: { - src: 'stringService', - input: ({ context }) => ({ - staticVal: 'hello', - newCount: context.count * 2 - }), - onDone: 'success' + }, + success: { + type: 'final' } - }, - success: { - type: 'final' } - } - }, - { - actors: { - stringService: fromPromise(({ input }) => { - expect(input).toEqual({ newCount: 84, staticVal: 'hello' }); + }, + { + actors: { + stringService: fromPromise(({ input }) => { + expect(input).toEqual({ newCount: 84, staticVal: 'hello' }); - return Promise.resolve(true); - }) + return Promise.resolve(true); + }) + } } - } - ); + ); - const service = createActor(machine); - service.subscribe({ - complete: () => { - done(); - } - }); + const service = createActor(machine); + service.subscribe({ + complete: () => { + resolve(); + } + }); - service.start(); - }); + service.start(); + })); - it('should provide self to input mapper', (done) => { - const machine = createMachine({ - invoke: { - src: fromCallback(({ input }) => { - expect(input.responder.send).toBeDefined(); - done(); - }), - input: ({ self }) => ({ - responder: self - }) - } - }); + it('should provide self to input mapper', () => + new Promise((resolve) => { + const machine = createMachine({ + invoke: { + src: fromCallback(({ input }) => { + expect(input.responder.send).toBeDefined(); + resolve(); + }), + input: ({ self }) => ({ + responder: self + }) + } + }); - createActor(machine).start(); - }); + createActor(machine).start(); + })); }); diff --git a/packages/core/test/machine.test.ts b/packages/core/test/machine.test.ts index 6ed47f6243..09c0b9e265 100644 --- a/packages/core/test/machine.test.ts +++ b/packages/core/test/machine.test.ts @@ -96,8 +96,8 @@ describe('machine', () => { describe('machine.provide', () => { it('should override an action', () => { - const originalEntry = jest.fn(); - const overridenEntry = jest.fn(); + const originalEntry = vi.fn(); + const overridenEntry = vi.fn(); const machine = createMachine( { @@ -122,8 +122,8 @@ describe('machine', () => { }); it('should override a guard', () => { - const originalGuard = jest.fn().mockImplementation(() => true); - const overridenGuard = jest.fn().mockImplementation(() => true); + const originalGuard = vi.fn().mockImplementation(() => true); + const overridenGuard = vi.fn().mockImplementation(() => true); const machine = createMachine( { diff --git a/packages/core/test/parallel.test.ts b/packages/core/test/parallel.test.ts index 8a4695be3d..63df06ed1c 100644 --- a/packages/core/test/parallel.test.ts +++ b/packages/core/test/parallel.test.ts @@ -756,7 +756,7 @@ describe('parallel states', () => { }); it('should execute actions of the initial transition of a parallel region when entering the initial state nodes of a machine', () => { - const spy = jest.fn(); + const spy = vi.fn(); const machine = createMachine({ type: 'parallel', @@ -779,7 +779,7 @@ describe('parallel states', () => { }); it('should execute actions of the initial transition of a parallel region when the parallel state is targeted with an explicit transition', () => { - const spy = jest.fn(); + const spy = vi.fn(); const machine = createMachine({ initial: 'a', @@ -1051,72 +1051,73 @@ describe('parallel states', () => { }); }); - it('should raise a "xstate.done.state.*" event when all child states reach final state', (done) => { - const machine = createMachine({ - id: 'test', - initial: 'p', - states: { - p: { - type: 'parallel', - states: { - a: { - initial: 'idle', - states: { - idle: { - on: { - FINISH: 'finished' + it('should raise a "xstate.done.state.*" event when all child states reach final state', () => + new Promise((resolve) => { + const machine = createMachine({ + id: 'test', + initial: 'p', + states: { + p: { + type: 'parallel', + states: { + a: { + initial: 'idle', + states: { + idle: { + on: { + FINISH: 'finished' + } + }, + finished: { + type: 'final' } - }, - finished: { - type: 'final' } - } - }, - b: { - initial: 'idle', - states: { - idle: { - on: { - FINISH: 'finished' + }, + b: { + initial: 'idle', + states: { + idle: { + on: { + FINISH: 'finished' + } + }, + finished: { + type: 'final' } - }, - finished: { - type: 'final' } - } - }, - c: { - initial: 'idle', - states: { - idle: { - on: { - FINISH: 'finished' + }, + c: { + initial: 'idle', + states: { + idle: { + on: { + FINISH: 'finished' + } + }, + finished: { + type: 'final' } - }, - finished: { - type: 'final' } } - } + }, + onDone: 'success' }, - onDone: 'success' - }, - success: { - type: 'final' + success: { + type: 'final' + } } - } - }); + }); - const service = createActor(machine); - service.subscribe({ - complete: () => { - done(); - } - }); - service.start(); + const service = createActor(machine); + service.subscribe({ + complete: () => { + resolve(); + } + }); + service.start(); - service.send({ type: 'FINISH' }); - }); + service.send({ type: 'FINISH' }); + })); it('should raise a "xstate.done.state.*" event when a pseudostate of a history type is directly on a parallel state', () => { const machine = createMachine({ diff --git a/packages/core/test/predictableExec.test.ts b/packages/core/test/predictableExec.test.ts index 871256b139..e28ffb7fe5 100644 --- a/packages/core/test/predictableExec.test.ts +++ b/packages/core/test/predictableExec.test.ts @@ -270,67 +270,68 @@ describe('predictableExec', () => { expect(actual).toEqual([0, 1, 2]); }); - it('parent should be able to read the updated state of a child when receiving an event from it', (done) => { - const child = createMachine({ - initial: 'a', - states: { - a: { - // we need to clear the call stack before we send the event to the parent - after: { - 1: 'b' + it('parent should be able to read the updated state of a child when receiving an event from it', () => + new Promise((resolve) => { + const child = createMachine({ + initial: 'a', + states: { + a: { + // we need to clear the call stack before we send the event to the parent + after: { + 1: 'b' + } + }, + b: { + entry: sendParent({ type: 'CHILD_UPDATED' }) } - }, - b: { - entry: sendParent({ type: 'CHILD_UPDATED' }) } - } - }); + }); - let service: AnyActor; + let service: AnyActor; - const machine = createMachine({ - invoke: { - id: 'myChild', - src: child - }, - initial: 'initial', - states: { - initial: { - on: { - CHILD_UPDATED: [ - { - guard: () => { - return ( - service.getSnapshot().children.myChild.getSnapshot() - .value === 'b' - ); + const machine = createMachine({ + invoke: { + id: 'myChild', + src: child + }, + initial: 'initial', + states: { + initial: { + on: { + CHILD_UPDATED: [ + { + guard: () => { + return ( + service.getSnapshot().children.myChild.getSnapshot() + .value === 'b' + ); + }, + target: 'success' }, - target: 'success' - }, - { - target: 'fail' - } - ] + { + target: 'fail' + } + ] + } + }, + success: { + type: 'final' + }, + fail: { + type: 'final' } - }, - success: { - type: 'final' - }, - fail: { - type: 'final' } - } - }); + }); - service = createActor(machine); - service.subscribe({ - complete: () => { - expect(service.getSnapshot().value).toBe('success'); - done(); - } - }); - service.start(); - }); + service = createActor(machine); + service.subscribe({ + complete: () => { + expect(service.getSnapshot().value).toBe('success'); + resolve(); + } + }); + service.start(); + })); it('should be possible to send immediate events to initially invoked actors', () => { const child = createMachine({ @@ -365,37 +366,38 @@ describe('predictableExec', () => { expect(service.getSnapshot().value).toBe('done'); }); - it('should create invoke based on context updated by entry actions of the same state', (done) => { - const machine = createMachine({ - context: { - updated: false - }, - initial: 'a', - states: { - a: { - on: { - NEXT: 'b' - } + it('should create invoke based on context updated by entry actions of the same state', () => + new Promise((resolve) => { + const machine = createMachine({ + context: { + updated: false }, - b: { - entry: assign({ updated: true }), - invoke: { - src: fromPromise(({ input }) => { - expect(input.updated).toBe(true); - done(); - return Promise.resolve(); - }), - input: ({ context }: any) => ({ - updated: context.updated - }) + initial: 'a', + states: { + a: { + on: { + NEXT: 'b' + } + }, + b: { + entry: assign({ updated: true }), + invoke: { + src: fromPromise(({ input }) => { + expect(input.updated).toBe(true); + resolve(); + return Promise.resolve(); + }), + input: ({ context }: any) => ({ + updated: context.updated + }) + } } } - } - }); + }); - const actorRef = createActor(machine).start(); - actorRef.send({ type: 'NEXT' }); - }); + const actorRef = createActor(machine).start(); + actorRef.send({ type: 'NEXT' }); + })); it('should deliver events sent from the entry actions to a service invoked in the same state', () => { let received: any; @@ -435,64 +437,65 @@ describe('predictableExec', () => { expect(received).toEqual({ type: 'KNOCK_KNOCK' }); }); - it('parent should be able to read the updated state of a child when receiving an event from it', (done) => { - const child = createMachine({ - initial: 'a', - states: { - a: { - // we need to clear the call stack before we send the event to the parent - after: { - 1: 'b' + it('parent should be able to read the updated state of a child when receiving an event from it', () => + new Promise((resolve) => { + const child = createMachine({ + initial: 'a', + states: { + a: { + // we need to clear the call stack before we send the event to the parent + after: { + 1: 'b' + } + }, + b: { + entry: sendParent({ type: 'CHILD_UPDATED' }) } - }, - b: { - entry: sendParent({ type: 'CHILD_UPDATED' }) } - } - }); + }); - let service: AnyActor; + let service: AnyActor; - const machine = createMachine({ - invoke: { - id: 'myChild', - src: child - }, - initial: 'initial', - states: { - initial: { - on: { - CHILD_UPDATED: [ - { - guard: () => - service.getSnapshot().children.myChild.getSnapshot().value === - 'b', - target: 'success' - }, - { - target: 'fail' - } - ] - } - }, - success: { - type: 'final' + const machine = createMachine({ + invoke: { + id: 'myChild', + src: child }, - fail: { - type: 'final' + initial: 'initial', + states: { + initial: { + on: { + CHILD_UPDATED: [ + { + guard: () => + service.getSnapshot().children.myChild.getSnapshot() + .value === 'b', + target: 'success' + }, + { + target: 'fail' + } + ] + } + }, + success: { + type: 'final' + }, + fail: { + type: 'final' + } } - } - }); + }); - service = createActor(machine); - service.subscribe({ - complete: () => { - expect(service.getSnapshot().value).toBe('success'); - done(); - } - }); - service.start(); - }); + service = createActor(machine); + service.subscribe({ + complete: () => { + expect(service.getSnapshot().value).toBe('success'); + resolve(); + } + }); + service.start(); + })); it('should be possible to send immediate events to initially invoked actors', () => { const child = createMachine({ @@ -528,32 +531,33 @@ describe('predictableExec', () => { }); // https://github.com/statelyai/xstate/issues/3617 - it('should deliver events sent from the exit actions to a service invoked in the same state', (done) => { - const machine = createMachine({ - initial: 'active', - states: { - active: { - invoke: { - id: 'my-service', - src: fromCallback(({ receive }) => { - receive((event) => { - if (event.type === 'MY_EVENT') { - done(); - } - }); - }) + it('should deliver events sent from the exit actions to a service invoked in the same state', () => + new Promise((resolve) => { + const machine = createMachine({ + initial: 'active', + states: { + active: { + invoke: { + id: 'my-service', + src: fromCallback(({ receive }) => { + receive((event) => { + if (event.type === 'MY_EVENT') { + resolve(); + } + }); + }) + }, + exit: sendTo('my-service', { type: 'MY_EVENT' }), + on: { + TOGGLE: 'inactive' + } }, - exit: sendTo('my-service', { type: 'MY_EVENT' }), - on: { - TOGGLE: 'inactive' - } - }, - inactive: {} - } - }); + inactive: {} + } + }); - const actor = createActor(machine).start(); + const actor = createActor(machine).start(); - actor.send({ type: 'TOGGLE' }); - }); + actor.send({ type: 'TOGGLE' }); + })); }); diff --git a/packages/core/test/rehydration.test.ts b/packages/core/test/rehydration.test.ts index 041153cf7e..589750564a 100644 --- a/packages/core/test/rehydration.test.ts +++ b/packages/core/test/rehydration.test.ts @@ -7,7 +7,7 @@ import { assign, sendTo } from '../src/index.ts'; -import { sleep } from '@xstate-repo/jest-utils'; +import { setTimeout as sleep } from 'node:timers/promises'; describe('rehydration', () => { describe('using persisted state', () => { @@ -157,7 +157,7 @@ describe('rehydration', () => { }); it('should not replay actions when starting from a persisted state', () => { - const entrySpy = jest.fn(); + const entrySpy = vi.fn(); const machine = createMachine({ entry: entrySpy }); @@ -267,7 +267,7 @@ describe('rehydration', () => { }); it('a rehydrated done child should not re-notify the parent about its completion', () => { - const spy = jest.fn(); + const spy = vi.fn(); const machine = createMachine( { @@ -346,7 +346,7 @@ describe('rehydration', () => { actorRef.send({ type: 'NEXT' }); const persistedState = actorRef.getPersistedSnapshot(); - const spy = jest.fn(); + const spy = vi.fn(); const actorRef2 = createActor(machine, { snapshot: persistedState }); actorRef2.subscribe({ complete: spy @@ -379,7 +379,7 @@ describe('rehydration', () => { const persistedState = actorRef.getPersistedSnapshot(); - const spy = jest.fn(); + const spy = vi.fn(); const actorRef2 = createActor(machine, { snapshot: persistedState }); actorRef2.subscribe({ error: spy @@ -390,7 +390,7 @@ describe('rehydration', () => { }); it(`shouldn't re-notify the parent about the error when rehydrating`, async () => { - const spy = jest.fn(); + const spy = vi.fn(); const machine = createMachine( { @@ -427,7 +427,7 @@ describe('rehydration', () => { const subject = new BehaviorSubject(0); const subjectLogic = fromObservable(() => subject); - const spy = jest.fn(); + const spy = vi.fn(); const machine = createMachine( { diff --git a/packages/core/test/scxml.test.ts b/packages/core/test/scxml.test.ts index a939ce35b4..410b5675f5 100644 --- a/packages/core/test/scxml.test.ts +++ b/packages/core/test/scxml.test.ts @@ -1,4 +1,3 @@ -import { clearConsoleMocks } from '@xstate-repo/jest-utils'; import * as fs from 'fs'; import * as path from 'path'; import * as pkgUp from 'pkg-up'; @@ -478,8 +477,6 @@ describe('scxml', () => { } catch (e) { console.log(JSON.stringify(machine.config, null, 2)); throw e; - } finally { - clearConsoleMocks(); } }); }); diff --git a/packages/core/test/spawnChild.test.ts b/packages/core/test/spawnChild.test.ts index 8d54c79818..f1b05cf91a 100644 --- a/packages/core/test/spawnChild.test.ts +++ b/packages/core/test/spawnChild.test.ts @@ -48,45 +48,46 @@ describe('spawnChild action', () => { expect(actor.getSnapshot().children.child).toBeDefined(); }); - it('should accept `syncSnapshot` option', (done) => { - const observableLogic = fromObservable(() => interval(10)); - const observableMachine = createMachine({ - id: 'observable', - initial: 'idle', - context: { - observableRef: undefined! as ActorRefFrom - }, - states: { - idle: { - entry: spawnChild(observableLogic, { - id: 'int', - syncSnapshot: true - }), - on: { - 'xstate.snapshot.int': { - target: 'success', - guard: ({ event }) => event.snapshot.context === 5 + it('should accept `syncSnapshot` option', () => + new Promise((resolve) => { + const observableLogic = fromObservable(() => interval(10)); + const observableMachine = createMachine({ + id: 'observable', + initial: 'idle', + context: { + observableRef: undefined! as ActorRefFrom + }, + states: { + idle: { + entry: spawnChild(observableLogic, { + id: 'int', + syncSnapshot: true + }), + on: { + 'xstate.snapshot.int': { + target: 'success', + guard: ({ event }) => event.snapshot.context === 5 + } } + }, + success: { + type: 'final' } - }, - success: { - type: 'final' } - } - }); + }); - const observableService = createActor(observableMachine); - observableService.subscribe({ - complete: () => { - done(); - } - }); + const observableService = createActor(observableMachine); + observableService.subscribe({ + complete: () => { + resolve(); + } + }); - observableService.start(); - }); + observableService.start(); + })); it('should handle a dynamic id', () => { - const spy = jest.fn(); + const spy = vi.fn(); const child = createMachine({ on: { diff --git a/packages/core/test/stateIn.test.ts b/packages/core/test/stateIn.test.ts index 01e7fff12f..f8f697728a 100644 --- a/packages/core/test/stateIn.test.ts +++ b/packages/core/test/stateIn.test.ts @@ -466,7 +466,7 @@ describe('transition "in" check', () => { }); it('should be possible to check an ID with a path', () => { - const spy = jest.fn(); + const spy = vi.fn(); const machine = createMachine({ type: 'parallel', states: { diff --git a/packages/core/test/system.test.ts b/packages/core/test/system.test.ts index dd95672d37..40c0d6f4b8 100644 --- a/packages/core/test/system.test.ts +++ b/packages/core/test/system.test.ts @@ -21,102 +21,104 @@ import { import { ActorSystem } from '../src/system.ts'; describe('system', () => { - it('should register an invoked actor', (done) => { - type MySystem = ActorSystem<{ - actors: { - receiver: ActorRef, { type: 'HELLO' }>; - }; - }>; - - const machine = createMachine({ - id: 'parent', - initial: 'a', - states: { - a: { - invoke: [ - { - src: fromCallback(({ receive }) => { - receive((event) => { - expect(event.type).toBe('HELLO'); - done(); - }); - }), - systemId: 'receiver' - }, - { - src: createMachine({ - id: 'childmachine', - entry: ({ system }) => { - const receiver = (system as MySystem)?.get('receiver'); - - if (receiver) { - receiver.send({ type: 'HELLO' }); - } - } - }) - } - ] - } - } - }); - - createActor(machine).start(); - }); - - it('should register a spawned actor', (done) => { - type MySystem = ActorSystem<{ - actors: { - receiver: ActorRef, { type: 'HELLO' }>; - }; - }>; - - const machine = createMachine({ - types: {} as { - context: { - ref: CallbackActorRef; - machineRef?: ActorRefFrom; + it('should register an invoked actor', () => + new Promise((resolve) => { + type MySystem = ActorSystem<{ + actors: { + receiver: ActorRef, { type: 'HELLO' }>; }; - }, - id: 'parent', - context: ({ spawn }) => ({ - ref: spawn( - fromCallback(({ receive }) => { - receive((event) => { - expect(event.type).toBe('HELLO'); - done(); - }); - }), - { systemId: 'receiver' } - ) - }), - on: { - toggle: { - actions: assign({ - machineRef: ({ spawn }) => { - return spawn( - createMachine({ + }>; + + const machine = createMachine({ + id: 'parent', + initial: 'a', + states: { + a: { + invoke: [ + { + src: fromCallback(({ receive }) => { + receive((event) => { + expect(event.type).toBe('HELLO'); + resolve(); + }); + }), + systemId: 'receiver' + }, + { + src: createMachine({ id: 'childmachine', entry: ({ system }) => { const receiver = (system as MySystem)?.get('receiver'); if (receiver) { receiver.send({ type: 'HELLO' }); - } else { - throw new Error('no'); } } }) - ); - } - }) + } + ] + } } - } - }); + }); - const actor = createActor(machine).start(); + createActor(machine).start(); + })); - actor.send({ type: 'toggle' }); - }); + it('should register a spawned actor', () => + new Promise((resolve) => { + type MySystem = ActorSystem<{ + actors: { + receiver: ActorRef, { type: 'HELLO' }>; + }; + }>; + + const machine = createMachine({ + types: {} as { + context: { + ref: CallbackActorRef; + machineRef?: ActorRefFrom; + }; + }, + id: 'parent', + context: ({ spawn }) => ({ + ref: spawn( + fromCallback(({ receive }) => { + receive((event) => { + expect(event.type).toBe('HELLO'); + resolve(); + }); + }), + { systemId: 'receiver' } + ) + }), + on: { + toggle: { + actions: assign({ + machineRef: ({ spawn }) => { + return spawn( + createMachine({ + id: 'childmachine', + entry: ({ system }) => { + const receiver = (system as MySystem)?.get('receiver'); + + if (receiver) { + receiver.send({ type: 'HELLO' }); + } else { + throw new Error('no'); + } + } + }) + ); + } + }) + } + } + }); + + const actor = createActor(machine).start(); + + actor.send({ type: 'toggle' }); + })); it('system can be immediately accessed outside the actor', () => { const machine = createMachine({ @@ -217,7 +219,7 @@ describe('system', () => { } }); - const errorSpy = jest.fn(); + const errorSpy = vi.fn(); const actorRef = createActor(machine, { systemId: 'test' }); actorRef.subscribe({ @@ -226,7 +228,7 @@ describe('system', () => { actorRef.start(); actorRef.send({ type: 'toggle' }); - expect(errorSpy).toMatchMockCallsInlineSnapshot(` + expect(errorSpy.mock.calls).toMatchInlineSnapshot(` [ [ [Error: Actor with system ID 'test' already exists.], @@ -468,7 +470,7 @@ describe('system', () => { }); it('should gracefully handle re-registration of a `systemId` during a reentering transition', () => { - const spy = jest.fn(); + const spy = vi.fn(); let counter = 0; @@ -513,7 +515,7 @@ describe('system', () => { }); it('should be able to send an event to an ancestor with a registered `systemId` from an initial entry action', () => { - const spy = jest.fn(); + const spy = vi.fn(); const child = createMachine({ entry: sendTo(({ system }) => system.get('myRoot'), { diff --git a/packages/core/test/toPromise.test.ts b/packages/core/test/toPromise.test.ts index b7543fb6c6..23fef90f6c 100644 --- a/packages/core/test/toPromise.test.ts +++ b/packages/core/test/toPromise.test.ts @@ -121,6 +121,9 @@ describe('toPromise', () => { }); it('should immediately reject for an actor that had an error', async () => { + // use fake timers to avoid error being thrown in separate microtask + vi.useFakeTimers(); + const machine = createMachine({ entry: () => { throw new Error('oh noes'); @@ -132,6 +135,6 @@ describe('toPromise', () => { expect(actor.getSnapshot().status).toBe('error'); expect(actor.getSnapshot().error).toEqual(new Error('oh noes')); - await expect(toPromise(actor)).rejects.toEqual(new Error('oh noes')); + expect(toPromise(actor)).rejects.toEqual(new Error('oh noes')); }); }); diff --git a/packages/core/test/transient.test.ts b/packages/core/test/transient.test.ts index 1e1647b261..a7d1b57108 100644 --- a/packages/core/test/transient.test.ts +++ b/packages/core/test/transient.test.ts @@ -697,7 +697,7 @@ describe('transient states (eventless transitions)', () => { }); it("should execute an always transition after a raised transition even if that raised transition doesn't change the state", () => { - const spy = jest.fn(); + const spy = vi.fn(); let counter = 0; const machine = createMachine({ always: { diff --git a/packages/core/test/waitFor.test.ts b/packages/core/test/waitFor.test.ts index 18535aaf96..578c5063b8 100644 --- a/packages/core/test/waitFor.test.ts +++ b/packages/core/test/waitFor.test.ts @@ -115,7 +115,7 @@ describe('waitFor', () => { const machine = createMachine({}); const actorRef = createActor(machine).start(); - const spy = jest.fn(); + const spy = vi.fn(); actorRef.subscribe = spy; waitFor(actorRef, () => true).then(() => {}); diff --git a/packages/core/vitest.config.mts b/packages/core/vitest.config.mts new file mode 100644 index 0000000000..efa4087d12 --- /dev/null +++ b/packages/core/vitest.config.mts @@ -0,0 +1,8 @@ +import { defineProject } from 'vitest/config'; + +export default defineProject({ + test: { + globals: true, + environmentMatchGlobs: [['test/errors.test.ts', 'happy-dom']] + } +}); diff --git a/packages/xstate-graph/test/__snapshots__/graph.test.ts.snap b/packages/xstate-graph/test/__snapshots__/graph.test.ts.snap index 55f106b4b3..00b78a9cc7 100644 --- a/packages/xstate-graph/test/__snapshots__/graph.test.ts.snap +++ b/packages/xstate-graph/test/__snapshots__/graph.test.ts.snap @@ -1,6 +1,6 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`@xstate/graph getPathFromEvents() should return a path to the last entered state by the event sequence: path from events 1`] = ` +exports[`@xstate/graph > getPathFromEvents() > should return a path to the last entered state by the event sequence > path from events 1`] = ` { "state": { "red": "flashing", @@ -34,7 +34,7 @@ exports[`@xstate/graph getPathFromEvents() should return a path to the last ente } `; -exports[`@xstate/graph getShortestPaths() should return a mapping of shortest paths to all states (parallel): shortest paths parallel 1`] = ` +exports[`@xstate/graph > getShortestPaths() > should return a mapping of shortest paths to all states (parallel) > shortest paths parallel 1`] = ` [ { "state": { @@ -98,7 +98,7 @@ exports[`@xstate/graph getShortestPaths() should return a mapping of shortest pa ] `; -exports[`@xstate/graph getShortestPaths() should return a mapping of shortest paths to all states: shortest paths 1`] = ` +exports[`@xstate/graph > getShortestPaths() > should return a mapping of shortest paths to all states > shortest paths 1`] = ` [ { "state": "green", @@ -223,7 +223,7 @@ exports[`@xstate/graph getShortestPaths() should return a mapping of shortest pa ] `; -exports[`@xstate/graph getSimplePaths() should return a mapping of arrays of simple paths to all states 2`] = ` +exports[`@xstate/graph > getSimplePaths() > should return a mapping of arrays of simple paths to all states 2`] = ` [ { "state": "green", @@ -468,7 +468,7 @@ exports[`@xstate/graph getSimplePaths() should return a mapping of arrays of sim ] `; -exports[`@xstate/graph getSimplePaths() should return a mapping of simple paths to all states (parallel): simple paths parallel 1`] = ` +exports[`@xstate/graph > getSimplePaths() > should return a mapping of simple paths to all states (parallel) > simple paths parallel 1`] = ` [ { "state": { @@ -561,7 +561,7 @@ exports[`@xstate/graph getSimplePaths() should return a mapping of simple paths ] `; -exports[`@xstate/graph getSimplePaths() should return multiple paths for equivalent transitions: simple paths equal transitions 1`] = ` +exports[`@xstate/graph > getSimplePaths() > should return multiple paths for equivalent transitions > simple paths equal transitions 1`] = ` [ { "state": "a", @@ -601,7 +601,7 @@ exports[`@xstate/graph getSimplePaths() should return multiple paths for equival ] `; -exports[`@xstate/graph getSimplePaths() should return value-based paths: simple paths context 1`] = ` +exports[`@xstate/graph > getSimplePaths() > should return value-based paths > simple paths context 1`] = ` [ { "state": "start", @@ -666,7 +666,7 @@ exports[`@xstate/graph getSimplePaths() should return value-based paths: simple ] `; -exports[`@xstate/graph toDirectedGraph should represent a statechart as a directed graph 1`] = ` +exports[`@xstate/graph > toDirectedGraph > should represent a statechart as a directed graph 1`] = ` { "children": [ { diff --git a/packages/xstate-graph/test/graph.test.ts b/packages/xstate-graph/test/graph.test.ts index ac6e203135..51c8352318 100644 --- a/packages/xstate-graph/test/graph.test.ts +++ b/packages/xstate-graph/test/graph.test.ts @@ -221,7 +221,7 @@ describe('@xstate/graph', () => { ).toHaveLength(1); }); - xit('should not throw when a condition is present', () => { + it.skip('should not throw when a condition is present', () => { expect(() => getShortestPaths(condMachine)).not.toThrow(); }); diff --git a/packages/xstate-graph/test/index.test.ts b/packages/xstate-graph/test/index.test.ts index b7c903ffd6..e5d31f66d3 100644 --- a/packages/xstate-graph/test/index.test.ts +++ b/packages/xstate-graph/test/index.test.ts @@ -200,7 +200,7 @@ it('prevents infinite recursion based on a provided limit', () => { expect(() => { model.getShortestPaths({ limit: 100 }); - }).toThrowErrorMatchingInlineSnapshot(`"Traversal limit exceeded"`); + }).toThrowErrorMatchingInlineSnapshot(`[Error: Traversal limit exceeded]`); }); describe('test model options', () => { diff --git a/packages/xstate-graph/test/paths.test.ts b/packages/xstate-graph/test/paths.test.ts index 8dcee07325..c64aa99a65 100644 --- a/packages/xstate-graph/test/paths.test.ts +++ b/packages/xstate-graph/test/paths.test.ts @@ -47,7 +47,7 @@ describe('testModel.testPaths(...)', () => { const events = typeof options.events === 'function' ? options.events(initialState) - : options.events ?? []; + : (options.events ?? []); const nextState = getNextSnapshot(logic, initialState, events[0]); return [ diff --git a/packages/xstate-graph/vitest.config.mts b/packages/xstate-graph/vitest.config.mts new file mode 100644 index 0000000000..58fb522a9a --- /dev/null +++ b/packages/xstate-graph/vitest.config.mts @@ -0,0 +1,7 @@ +import { defineProject } from 'vitest/config'; + +export default defineProject({ + test: { + globals: true + } +}); diff --git a/packages/xstate-immer/test/immer.test.ts b/packages/xstate-immer/test/immer.test.ts index 1bfa2d3da8..d529152f60 100644 --- a/packages/xstate-immer/test/immer.test.ts +++ b/packages/xstate-immer/test/immer.test.ts @@ -158,76 +158,78 @@ describe('@xstate/immer', () => { expect(actorRef.getSnapshot().context.foo.bar.baz).toEqual([1, 2, 3, 4]); }); - it('should create updates (form example)', (done) => { - interface FormContext { - name: string; - age: number | undefined; - } - - type NameUpdateEvent = ImmerUpdateEvent<'UPDATE_NAME', string>; - type AgeUpdateEvent = ImmerUpdateEvent<'UPDATE_AGE', number>; - - type FormEvent = - | NameUpdateEvent - | AgeUpdateEvent - | { - type: 'SUBMIT'; - }; - - const nameUpdater = createUpdater( - 'UPDATE_NAME', - ({ context, event }) => { - context.name = event.input; + it('should create updates (form example)', () => + new Promise((resolve) => { + interface FormContext { + name: string; + age: number | undefined; } - ); - const ageUpdater = createUpdater( - 'UPDATE_AGE', - ({ context, event }) => { - context.age = event.input; - } - ); + type NameUpdateEvent = ImmerUpdateEvent<'UPDATE_NAME', string>; + type AgeUpdateEvent = ImmerUpdateEvent<'UPDATE_AGE', number>; + + type FormEvent = + | NameUpdateEvent + | AgeUpdateEvent + | { + type: 'SUBMIT'; + }; + + const nameUpdater = createUpdater< + FormContext, + NameUpdateEvent, + FormEvent + >('UPDATE_NAME', ({ context, event }) => { + context.name = event.input; + }); - const formMachine = createMachine({ - types: {} as { context: FormContext; events: FormEvent }, - initial: 'editing', - context: { - name: '', - age: undefined - }, - states: { - editing: { - on: { - [nameUpdater.type]: { actions: nameUpdater.action }, - [ageUpdater.type]: { actions: ageUpdater.action }, - SUBMIT: 'submitting' - } + const ageUpdater = createUpdater( + 'UPDATE_AGE', + ({ context, event }) => { + context.age = event.input; + } + ); + + const formMachine = createMachine({ + types: {} as { context: FormContext; events: FormEvent }, + initial: 'editing', + context: { + name: '', + age: undefined }, - submitting: { - always: { - target: 'success', - guard: ({ context }) => { - return context.name === 'David' && context.age === 0; + states: { + editing: { + on: { + [nameUpdater.type]: { actions: nameUpdater.action }, + [ageUpdater.type]: { actions: ageUpdater.action }, + SUBMIT: 'submitting' } + }, + submitting: { + always: { + target: 'success', + guard: ({ context }) => { + return context.name === 'David' && context.age === 0; + } + } + }, + success: { + type: 'final' } - }, - success: { - type: 'final' } - } - }); + }); - const service = createActor(formMachine); - service.subscribe({ - complete: () => { - done(); - } - }); - service.start(); + const service = createActor(formMachine); + service.subscribe({ + complete: () => { + resolve(); + } + }); + service.start(); - service.send(nameUpdater.update('David')); - service.send(ageUpdater.update(0)); + service.send(nameUpdater.update('David')); + service.send(ageUpdater.update(0)); - service.send({ type: 'SUBMIT' }); - }); + service.send({ type: 'SUBMIT' }); + })); }); diff --git a/packages/xstate-immer/vitest.config.mts b/packages/xstate-immer/vitest.config.mts new file mode 100644 index 0000000000..58fb522a9a --- /dev/null +++ b/packages/xstate-immer/vitest.config.mts @@ -0,0 +1,7 @@ +import { defineProject } from 'vitest/config'; + +export default defineProject({ + test: { + globals: true + } +}); diff --git a/packages/xstate-inspect/test/inspect.test.ts b/packages/xstate-inspect/test/inspect.test.ts index 5bebc7191f..60a09f80a2 100644 --- a/packages/xstate-inspect/test/inspect.test.ts +++ b/packages/xstate-inspect/test/inspect.test.ts @@ -1,9 +1,6 @@ import { assign, createMachine, createActor } from 'xstate'; import { createDevTools, inspect } from '../src/index.ts'; -// mute the warning about this not being implemented by jsdom -window.open = () => null; - const windowListenersUsedArguments: Array = []; const windowAddEventListener = window.addEventListener; window.addEventListener = function (...args: any) { @@ -223,46 +220,48 @@ describe('@xstate/inspect', () => { }); // TODO: the value is still available on `machine.definition.initial` and that is not handled by the serializer - it.skip('should not crash when registering machine with very deep context when serializer manages to replace it', (done) => { - type DeepObject = { nested?: DeepObject }; - - const deepObj: DeepObject = {}; + it.skip('should not crash when registering machine with very deep context when serializer manages to replace it', async () => { + await new Promise((resolve) => { + type DeepObject = { nested?: DeepObject }; - let current = deepObj; - for (let i = 0; i < 20_000; i += 1) { - current.nested = {}; - current = current.nested; - } + const deepObj: DeepObject = {}; - const machine = createMachine({ - initial: 'active', - context: deepObj, - states: { - active: {} + let current = deepObj; + for (let i = 0; i < 20_000; i += 1) { + current.nested = {}; + current = current.nested; } - }); - const devTools = createDevTools(); + const machine = createMachine({ + initial: 'active', + context: deepObj, + states: { + active: {} + } + }); - inspect({ - iframe: false, - devTools, - serialize: (key, value) => { - if (key === 'nested') { - return '[very deep]'; + const devTools = createDevTools(); + + inspect({ + iframe: false, + devTools, + serialize: (key, value) => { + if (key === 'nested') { + return '[very deep]'; + } + + return value; } + }); - return value; - } - }); + const service = createActor(machine).start(); - const service = createActor(machine).start(); + devTools.onRegister(() => { + resolve(); + }); - devTools.onRegister(() => { - done(); + expect(() => devTools.register(service)).not.toThrow(); }); - - expect(() => devTools.register(service)).not.toThrow(); }); it('should successfully serialize value with unsafe toJSON when serializer manages to replace it', () => { @@ -385,11 +384,11 @@ describe('@xstate/inspect', () => { }); it('browser inspector should use targetWindow if provided', () => { - const windowMock = jest.fn() as unknown as Window; - const windowSpy = jest.spyOn(window, 'open'); + const windowMock = vi.fn() as unknown as Window; + const windowSpy = vi.spyOn(window, 'open'); windowSpy.mockImplementation(() => windowMock); - const localWindowMock = jest.fn() as unknown as Window; + const localWindowMock = vi.fn() as unknown as Window; const devTools = createDevTools(); inspect({ diff --git a/packages/xstate-inspect/vitest.config.mts b/packages/xstate-inspect/vitest.config.mts new file mode 100644 index 0000000000..13c3f1b071 --- /dev/null +++ b/packages/xstate-inspect/vitest.config.mts @@ -0,0 +1,8 @@ +import { defineProject } from 'vitest/config'; + +export default defineProject({ + test: { + globals: true, + environment: 'happy-dom' + } +}); diff --git a/packages/xstate-react/test/createActorContext.test.tsx b/packages/xstate-react/test/createActorContext.test.tsx index cf12ae8eb4..e796cda4fb 100644 --- a/packages/xstate-react/test/createActorContext.test.tsx +++ b/packages/xstate-react/test/createActorContext.test.tsx @@ -8,25 +8,6 @@ import { import { fireEvent, screen, render, waitFor } from '@testing-library/react'; import { useSelector, createActorContext, shallowEqual } from '../src'; -const originalConsoleError = console.error; - -afterEach(() => { - console.error = originalConsoleError; -}); - -function checkConsoleErrorOutputForMissingProvider() { - expect(console.error).toHaveBeenCalledTimes(3); - expect((console.error as any).mock.calls[0][0].message.split('\n')[0]).toBe( - `Uncaught [Error: You used a hook from \"ActorProvider\" but it's not inside a component.]` - ); - expect((console.error as any).mock.calls[1][0].message.split('\n')[0]).toBe( - `Uncaught [Error: You used a hook from \"ActorProvider\" but it's not inside a component.]` - ); - expect((console.error as any).mock.calls[2][0].split('\n')[0]).toBe( - `The above error occurred in the component:` - ); -} - describe('createActorContext', () => { it('should work with useSelector', () => { const someMachine = createMachine({ @@ -243,7 +224,8 @@ describe('createActorContext', () => { }); it('useActorRef should throw when the actor was not provided', () => { - console.error = jest.fn(); + const spy = vi.spyOn(console, 'error'); + const SomeContext = createActorContext(createMachine({})); const App = () => { @@ -252,13 +234,18 @@ describe('createActorContext', () => { }; expect(() => render()).toThrowErrorMatchingInlineSnapshot( - `"You used a hook from "ActorProvider" but it's not inside a component."` + `[Error: You used a hook from "ActorProvider" but it's not inside a component.]` + ); + + expect(spy).toHaveBeenCalledTimes(1); + expect(spy.mock.calls[0][0].split('\n')[0]).toMatchInlineSnapshot( + `"The above error occurred in the component:"` ); - checkConsoleErrorOutputForMissingProvider(); }); it('useSelector should throw when the actor was not provided', () => { - console.error = jest.fn(); + const spy = vi.spyOn(console, 'error'); + const SomeContext = createActorContext(createMachine({})); const App = () => { @@ -267,9 +254,13 @@ describe('createActorContext', () => { }; expect(() => render()).toThrowErrorMatchingInlineSnapshot( - `"You used a hook from "ActorProvider" but it's not inside a component."` + `[Error: You used a hook from "ActorProvider" but it's not inside a component.]` + ); + + expect(spy).toHaveBeenCalledTimes(1); + expect(spy.mock.calls[0][0].split('\n')[0]).toMatchInlineSnapshot( + `"The above error occurred in the component:"` ); - checkConsoleErrorOutputForMissingProvider(); }); it('should be able to pass interpreter options to the provider', () => { @@ -281,7 +272,7 @@ describe('createActorContext', () => { } } }); - const stubFn = jest.fn(); + const stubFn = vi.fn(); const SomeContext = createActorContext(someMachine); const Component = () => { @@ -342,7 +333,7 @@ describe('createActorContext', () => { } } }); - const stubFn = jest.fn(); + const stubFn = vi.fn(); const SomeContext = createActorContext(someMachine); const Component = () => { diff --git a/packages/xstate-react/test/useActor.test.tsx b/packages/xstate-react/test/useActor.test.tsx index ea8b20c30b..7578f7dd80 100644 --- a/packages/xstate-react/test/useActor.test.tsx +++ b/packages/xstate-react/test/useActor.test.tsx @@ -1,5 +1,5 @@ import { act, fireEvent, screen } from '@testing-library/react'; -import { sleep } from '@xstate-repo/jest-utils'; +import { setTimeout as sleep } from 'node:timers/promises'; import * as React from 'react'; import { useState } from 'react'; import { BehaviorSubject } from 'rxjs'; @@ -20,7 +20,7 @@ import { useActor, useSelector } from '../src/index.ts'; import { describeEachReactMode } from './utils.tsx'; afterEach(() => { - jest.useRealTimers(); + vi.useRealTimers(); }); describeEachReactMode('useActor (%s)', ({ suiteKey, render }) => { @@ -249,187 +249,190 @@ describeEachReactMode('useActor (%s)', ({ suiteKey, render }) => { await screen.findByTestId('success'); }); - it('actions should not use stale data in a builtin transition action', (done) => { - const toggleMachine = createMachine({ - types: {} as { - context: { latest: number }; - events: { type: 'SET_LATEST' }; - }, - context: { - latest: 0 - }, - on: { - SET_LATEST: { - actions: 'setLatest' + it('actions should not use stale data in a builtin transition action', () => + new Promise((resolve) => { + const toggleMachine = createMachine({ + types: {} as { + context: { latest: number }; + events: { type: 'SET_LATEST' }; + }, + context: { + latest: 0 + }, + on: { + SET_LATEST: { + actions: 'setLatest' + } } - } - }); + }); - const Component = () => { - const [ext, setExt] = useState(1); - - const [, send] = useActor( - toggleMachine.provide({ - actions: { - setLatest: assign({ - latest: () => { - expect(ext).toBe(2); - done(); - return ext; - } - }) - } - }) - ); + const Component = () => { + const [ext, setExt] = useState(1); + + const [, send] = useActor( + toggleMachine.provide({ + actions: { + setLatest: assign({ + latest: () => { + expect(ext).toBe(2); + resolve(); + return ext; + } + }) + } + }) + ); - return ( - <> - - ); - }; + return ( + + ); + }; - render(); - const button = screen.getByTestId('button'); + render(); + const button = screen.getByTestId('button'); - fireEvent.click(button); - }); + fireEvent.click(button); + })); it('actions created by a layout effect should access the latest closure values', () => { const actual: number[] = []; @@ -107,7 +108,7 @@ describeEachReactMode('useActorRef (%s)', ({ suiteKey, render }) => { }); it('should rerender OK when only the provided machine implementations have changed', () => { - console.warn = jest.fn(); + console.warn = vi.fn(); const machine = createMachine({ initial: 'foo', context: { id: 1 }, @@ -269,7 +270,7 @@ describeEachReactMode('useActorRef (%s)', ({ suiteKey, render }) => { }); it('should deliver messages sent from an effect to the root actor registered in the system', () => { - const spy = jest.fn(); + const spy = vi.fn(); const m = createMachine({ on: { PING: { @@ -698,7 +699,7 @@ describeEachReactMode('useActorRef (%s)', ({ suiteKey, render }) => { }); it('should be able to rehydrate an inline actor when changing machines', () => { - const spy = jest.fn(); + const spy = vi.fn(); const createSampleMachine = (counter: number) => { const child = createMachine({ @@ -766,8 +767,8 @@ describeEachReactMode('useActorRef (%s)', ({ suiteKey, render }) => { }); it("should execute action bound to a specific machine's instance when the action is provided in render", () => { - const spy1 = jest.fn(); - const spy2 = jest.fn(); + const spy1 = vi.fn(); + const spy2 = vi.fn(); const machine = createMachine({ on: { @@ -816,7 +817,7 @@ describeEachReactMode('useActorRef (%s)', ({ suiteKey, render }) => { }); it('should execute an initial entry action once', () => { - const spy = jest.fn(); + const spy = vi.fn(); const machine = createMachine({ entry: spy diff --git a/packages/xstate-react/test/useSelector.test.tsx b/packages/xstate-react/test/useSelector.test.tsx index 3de48d3a97..fed479f1b7 100644 --- a/packages/xstate-react/test/useSelector.test.tsx +++ b/packages/xstate-react/test/useSelector.test.tsx @@ -615,7 +615,7 @@ describeEachReactMode('useSelector (%s)', ({ suiteKey, render }) => { return null; } - console.error = jest.fn(); + console.error = vi.fn(); render(); const [snapshot1] = snapshots; @@ -703,7 +703,7 @@ describeEachReactMode('useSelector (%s)', ({ suiteKey, render }) => { }); it('should not log any spurious errors when used with a not-started actor', () => { - const spy = jest.fn(); + const spy = vi.fn(); console.error = spy; const machine = createMachine({}); diff --git a/packages/xstate-react/vitest.config.mts b/packages/xstate-react/vitest.config.mts new file mode 100644 index 0000000000..13c3f1b071 --- /dev/null +++ b/packages/xstate-react/vitest.config.mts @@ -0,0 +1,8 @@ +import { defineProject } from 'vitest/config'; + +export default defineProject({ + test: { + globals: true, + environment: 'happy-dom' + } +}); diff --git a/packages/xstate-solid/package.json b/packages/xstate-solid/package.json index d0f2b83c09..69aa53b81d 100644 --- a/packages/xstate-solid/package.json +++ b/packages/xstate-solid/package.json @@ -51,8 +51,9 @@ } }, "devDependencies": { + "@solidjs/testing-library": "^0.8.9", "solid-js": "^1.7.6", - "solid-testing-library": "^0.3.0", + "vite-plugin-solid": "^2.10.2", "xstate": "5.17.4" } } diff --git a/packages/xstate-solid/test/fromActorRef.test.tsx b/packages/xstate-solid/test/fromActorRef.test.tsx index 4c4d4c01bd..17cc707a25 100644 --- a/packages/xstate-solid/test/fromActorRef.test.tsx +++ b/packages/xstate-solid/test/fromActorRef.test.tsx @@ -10,7 +10,7 @@ import { onMount } from 'solid-js'; import { createStore, reconcile } from 'solid-js/store'; -import { fireEvent, render, screen, waitFor } from 'solid-testing-library'; +import { fireEvent, render, screen, waitFor } from '@solidjs/testing-library'; import { ActorRef, ActorRefFrom, @@ -26,145 +26,148 @@ const createSimpleActor = (value: T) => createActor(fromTransition((s) => s, value)); describe('fromActorRef', () => { - it('initial invoked actor should be immediately available', (done) => { - const childMachine = createMachine({ - id: 'childMachine', - initial: 'active', - states: { - active: {} - } - }); - const machine = createMachine({ - initial: 'active', - invoke: { - id: 'child', - src: childMachine - }, - states: { - active: {} - } - }); + it('initial invoked actor should be immediately available', () => + new Promise((resolve) => { + const childMachine = createMachine({ + id: 'childMachine', + initial: 'active', + states: { + active: {} + } + }); + const machine = createMachine({ + initial: 'active', + invoke: { + id: 'child', + src: childMachine + }, + states: { + active: {} + } + }); - const ChildTest: Component<{ actor: ActorRefFrom }> = ( - props - ) => { - const state = fromActorRef(props.actor); + const ChildTest: Component<{ + actor: ActorRefFrom; + }> = (props) => { + const state = fromActorRef(props.actor); - expect(state().value).toEqual('active'); - done(); + expect(state().value).toEqual('active'); + resolve(); - return null; - }; + return null; + }; - const Test = () => { - const [state] = useActor(machine); - return ( - } - /> - ); - }; + const Test = () => { + const [state] = useActor(machine); + return ( + } + /> + ); + }; - render(() => ); - }); + render(() => ); + })); - it('invoked actor should be able to receive (deferred) events that it replays when active', (done) => { - const childMachine = createMachine({ - id: 'childMachine', - initial: 'active', - states: { - active: { - on: { - FINISH: { actions: sendParent({ type: 'FINISH' }) } + it('invoked actor should be able to receive (deferred) events that it replays when active', () => + new Promise((resolve) => { + const childMachine = createMachine({ + id: 'childMachine', + initial: 'active', + states: { + active: { + on: { + FINISH: { actions: sendParent({ type: 'FINISH' }) } + } } } - } - }); - const machine = createMachine({ - initial: 'active', - invoke: { - id: 'child', - src: childMachine - }, - states: { - active: { - on: { FINISH: 'success' } + }); + const machine = createMachine({ + initial: 'active', + invoke: { + id: 'child', + src: childMachine }, - success: {} - } - }); + states: { + active: { + on: { FINISH: 'success' } + }, + success: {} + } + }); - const ChildTest: Component<{ actor: ActorRefFrom }> = ( - props - ) => { - const state = fromActorRef(props.actor); + const ChildTest: Component<{ + actor: ActorRefFrom; + }> = (props) => { + const state = fromActorRef(props.actor); - onMount(() => { - expect(state().value).toEqual('active'); - props.actor.send({ type: 'FINISH' }); - }); + onMount(() => { + expect(state().value).toEqual('active'); + props.actor.send({ type: 'FINISH' }); + }); - return null; - }; + return null; + }; - const Test = () => { - const [state] = useActor(machine); - createEffect(() => { - if (state.matches('success')) { - done(); - } - }); + const Test = () => { + const [state] = useActor(machine); + createEffect(() => { + if (state.matches('success')) { + resolve(); + } + }); - return ( - } - /> - ); - }; + return ( + } + /> + ); + }; - render(() => ); - }); + render(() => ); + })); - it('send should update synchronously', (done) => { - const machine = createMachine({ - initial: 'start', - states: { - start: { - on: { - done: 'success' + it('send should update synchronously', () => + new Promise((resolve) => { + const machine = createMachine({ + initial: 'start', + states: { + start: { + on: { + done: 'success' + } + }, + success: { + type: 'final' } - }, - success: { - type: 'final' } - } - }); - - const Spawner = () => { - const [actorRef] = createSignal(createActor(machine).start()); - const snapshot = fromActorRef(actorRef); - - onMount(() => { - expect(snapshot().value).toBe('start'); - actorRef().send({ type: 'done' }); - expect(snapshot().value).toBe('success'); }); - return ( - - - - - - - - - ); - }; + const Spawner = () => { + const [actorRef] = createSignal(createActor(machine).start()); + const snapshot = fromActorRef(actorRef); - render(() => ); - waitFor(() => screen.getByTestId('success')).then(() => done()); - }); + onMount(() => { + expect(snapshot().value).toBe('start'); + actorRef().send({ type: 'done' }); + expect(snapshot().value).toBe('success'); + }); + + return ( + + + + + + + + + ); + }; + + render(() => ); + waitFor(() => screen.getByTestId('success')).then(() => resolve()); + })); it('should only trigger effects once for nested context values', () => { const childMachine = createMachine({ @@ -519,65 +522,68 @@ describe('fromActorRef', () => { expect(canDoSomethingEl.textContent).toEqual('true'); }); - it('spawned actor should be able to receive (deferred) events that it replays when active', (done) => { - const childMachine = createMachine({ - id: 'childMachine', - initial: 'active', - states: { - active: { - on: { - FINISH: { actions: sendParent({ type: 'FINISH' }) } + it('spawned actor should be able to receive (deferred) events that it replays when active', () => + new Promise((resolve) => { + const childMachine = createMachine({ + id: 'childMachine', + initial: 'active', + states: { + active: { + on: { + FINISH: { actions: sendParent({ type: 'FINISH' }) } + } } } - } - }); - const machine = createMachine({ - types: {} as { + }); + const machine = createMachine({ + types: {} as { + context: { + actorRef?: ActorRefFrom; + }; + }, + initial: 'active', context: { - actorRef?: ActorRefFrom; - }; - }, - initial: 'active', - context: { - actorRef: undefined - }, - states: { - active: { - entry: assign({ - actorRef: ({ spawn }) => spawn(childMachine) - }), - on: { FINISH: 'success' } + actorRef: undefined }, - success: {} - } - }); - - const ChildTest = (props: { actor: ActorRefFrom }) => { - const snapshot = fromActorRef(props.actor); - createEffect(() => { - expect(snapshot().value).toEqual('active'); + states: { + active: { + entry: assign({ + actorRef: ({ spawn }) => spawn(childMachine) + }), + on: { FINISH: 'success' } + }, + success: {} + } }); - onMount(() => { - props.actor.send({ type: 'FINISH' }); - }); + const ChildTest = (props: { + actor: ActorRefFrom; + }) => { + const snapshot = fromActorRef(props.actor); + createEffect(() => { + expect(snapshot().value).toEqual('active'); + }); - return null; - }; + onMount(() => { + props.actor.send({ type: 'FINISH' }); + }); - const Test = () => { - const [state] = useActor(machine); - createEffect(() => { - if (state.matches('success')) { - done(); - } - }); + return null; + }; - return ; - }; + const Test = () => { + const [state] = useActor(machine); + createEffect(() => { + if (state.matches('success')) { + resolve(); + } + }); - render(() => ); - }); + return ; + }; + + render(() => ); + })); it('should provide value from `actor.getSnapshot()` immediately', () => { const simpleActor = createActor(fromTransition((s) => s, 42)); @@ -1144,67 +1150,68 @@ describe('fromActorRef', () => { }); }); - it(`actor should not reevaluate a scope depending on state.matches when state.value doesn't change`, (done) => { - jest.useFakeTimers(); + it(`actor should not reevaluate a scope depending on state.matches when state.value doesn't change`, () => + new Promise((resolve) => { + vi.useFakeTimers(); - interface MachineContext { - counter: number; - } + interface MachineContext { + counter: number; + } - const machine = createMachine({ - types: {} as { - context: MachineContext; - }, - context: { - counter: 0 - }, - initial: 'idle', - states: { - idle: { - on: { - INC: { - actions: assign({ - counter: ({ context }) => context.counter + 1 - }) + const machine = createMachine({ + types: {} as { + context: MachineContext; + }, + context: { + counter: 0 + }, + initial: 'idle', + states: { + idle: { + on: { + INC: { + actions: assign({ + counter: ({ context }) => context.counter + 1 + }) + } } } } - } - }); + }); - const counterService = createActor(machine).start(); + const counterService = createActor(machine).start(); - const Comp = () => { - let calls = 0; - const snapshot = fromActorRef(counterService); + const Comp = () => { + let calls = 0; + const snapshot = fromActorRef(counterService); - createEffect(() => { - calls++; - snapshot().matches('foo'); - }); + createEffect(() => { + calls++; + snapshot().matches('foo'); + }); - onMount(() => { - counterService.send({ type: 'INC' }); - counterService.send({ type: 'INC' }); - counterService.send({ type: 'INC' }); - setTimeout(() => { + onMount(() => { + counterService.send({ type: 'INC' }); + counterService.send({ type: 'INC' }); counterService.send({ type: 'INC' }); setTimeout(() => { counterService.send({ type: 'INC' }); setTimeout(() => { - expect(calls).toBe(1); - done(); - }, 100); + counterService.send({ type: 'INC' }); + setTimeout(() => { + expect(calls).toBe(1); + resolve(); + }, 100); + }); }); }); - }); - return null; - }; + return null; + }; - render(() => ); - jest.advanceTimersByTime(110); - }); + render(() => ); + vi.advanceTimersByTime(110); + })); it('actor should be updated when it changes shallow', () => { const counterMachine = createMachine({ diff --git a/packages/xstate-solid/test/selector.test.tsx b/packages/xstate-solid/test/selector.test.tsx index 8c68f0751c..11b098c561 100644 --- a/packages/xstate-solid/test/selector.test.tsx +++ b/packages/xstate-solid/test/selector.test.tsx @@ -1,6 +1,6 @@ /* @jsxImportSource solid-js */ import { createMemo, createSignal, from } from 'solid-js'; -import { fireEvent, render, screen } from 'solid-testing-library'; +import { fireEvent, render, screen } from '@solidjs/testing-library'; import { ActorRefFrom, AnyMachineSnapshot, diff --git a/packages/xstate-solid/test/useActor.test.tsx b/packages/xstate-solid/test/useActor.test.tsx index 539d31108c..3faa183333 100644 --- a/packages/xstate-solid/test/useActor.test.tsx +++ b/packages/xstate-solid/test/useActor.test.tsx @@ -10,7 +10,7 @@ import { onCleanup, onMount } from 'solid-js'; -import { fireEvent, render, screen, waitFor } from 'solid-testing-library'; +import { fireEvent, render, screen, waitFor } from '@solidjs/testing-library'; import { Actor, ActorLogicFrom, @@ -28,7 +28,7 @@ function sleep(ms: number) { } afterEach(() => { - jest.useRealTimers(); + vi.useRealTimers(); }); describe('useActor', () => { @@ -213,148 +213,151 @@ describe('useActor', () => { render(() => ); }); - it('should not spawn actors until service is started', (done) => { - const spawnMachine = createMachine({ - types: {} as { context: any }, - id: 'spawn', - initial: 'start', - context: { ref: undefined }, - states: { - start: { - entry: assign({ - ref: ({ spawn }) => - spawn( - fromPromise(() => new Promise((res) => res(42))), - { id: 'my-promise' } - ) - }), - on: { - 'xstate.done.actor.my-promise': 'success' + it('should not spawn actors until service is started', () => + new Promise((resolve) => { + const spawnMachine = createMachine({ + types: {} as { context: any }, + id: 'spawn', + initial: 'start', + context: { ref: undefined }, + states: { + start: { + entry: assign({ + ref: ({ spawn }) => + spawn( + fromPromise(() => new Promise((res) => res(42))), + { id: 'my-promise' } + ) + }), + on: { + 'xstate.done.actor.my-promise': 'success' + } + }, + success: { + type: 'final' } - }, - success: { - type: 'final' } - } - }); + }); - const Spawner = () => { - const [current] = useActor(spawnMachine); + const Spawner = () => { + const [current] = useActor(spawnMachine); - return ( - - - - - - - - - ); - }; + return ( + + + + + + + + + ); + }; - render(() => ); - waitFor(() => screen.getByTestId('success')).then(() => done()); - }); + render(() => ); + waitFor(() => screen.getByTestId('success')).then(() => resolve()); + })); - it('send should update synchronously', (done) => { - const machine = createMachine({ - initial: 'start', - states: { - start: { - on: { - done: 'success' + it('send should update synchronously', () => + new Promise((resolve) => { + const machine = createMachine({ + initial: 'start', + states: { + start: { + on: { + done: 'success' + } + }, + success: { + type: 'final' } - }, - success: { - type: 'final' } - } - }); + }); - const Spawner = () => { - const [current, send] = useActor(machine); + const Spawner = () => { + const [current, send] = useActor(machine); - onMount(() => { - expect(current.value).toBe('start'); - send({ type: 'done' }); - expect(current.value).toBe('success'); - }); + onMount(() => { + expect(current.value).toBe('start'); + send({ type: 'done' }); + expect(current.value).toBe('success'); + }); - return ( - - - - - - - - - ); - }; + return ( + + + + + + + + + ); + }; - render(() => ); - waitFor(() => screen.getByTestId('success')).then(() => done()); - }); + render(() => ); + waitFor(() => screen.getByTestId('success')).then(() => resolve()); + })); - it('actions should not have stale data', (done) => { - const toggleMachine = createMachine({ - types: {} as { - events: { type: 'TOGGLE' }; - }, - initial: 'inactive', - states: { - inactive: { - on: { TOGGLE: 'active' } + it('actions should not have stale data', () => + new Promise((resolve) => { + const toggleMachine = createMachine({ + types: {} as { + events: { type: 'TOGGLE' }; }, - active: { - entry: 'doAction' + initial: 'inactive', + states: { + inactive: { + on: { TOGGLE: 'active' } + }, + active: { + entry: 'doAction' + } } - } - }); + }); - const Toggle = () => { - const [ext, setExt] = createSignal(false); + const Toggle = () => { + const [ext, setExt] = createSignal(false); - const doAction = () => { - expect(ext()).toBeTruthy(); - done(); - }; + const doAction = () => { + expect(ext()).toBeTruthy(); + resolve(); + }; - const [, send] = useActor( - toggleMachine.provide({ - actions: { - doAction - } - }) - ); + const [, send] = useActor( + toggleMachine.provide({ + actions: { + doAction + } + }) + ); - return ( -
-
- ); - }; + return ( +
+
+ ); + }; - render(() => ); + render(() => ); - const button = screen.getByTestId('button'); - const extButton = screen.getByTestId('extbutton'); - fireEvent.click(extButton); + const button = screen.getByTestId('button'); + const extButton = screen.getByTestId('extbutton'); + fireEvent.click(extButton); - fireEvent.click(button); - }); + fireEvent.click(button); + })); it('should capture all actions', () => { let count = 0; @@ -893,60 +896,61 @@ describe('useActor', () => { expect(canDoSomethingEl.textContent).toEqual('true'); }); - it(`should not reevaluate a scope depending on state.matches when state.value doesn't change`, (done) => { - interface MachineContext { - counter: number; - } + it(`should not reevaluate a scope depending on state.matches when state.value doesn't change`, () => + new Promise((resolve) => { + interface MachineContext { + counter: number; + } - const machine = createMachine({ - types: {} as { context: MachineContext }, - context: { - counter: 0 - }, - initial: 'idle', - states: { - idle: { - on: { - INC: { - actions: assign({ - counter: ({ context }) => context.counter + 1 - }) + const machine = createMachine({ + types: {} as { context: MachineContext }, + context: { + counter: 0 + }, + initial: 'idle', + states: { + idle: { + on: { + INC: { + actions: assign({ + counter: ({ context }) => context.counter + 1 + }) + } } } } - } - }); + }); - const Comp = () => { - let calls = 0; - const [state, send] = useActor(machine); + const Comp = () => { + let calls = 0; + const [state, send] = useActor(machine); - createEffect(() => { - calls++; - state.matches('foo'); - }); + createEffect(() => { + calls++; + state.matches('foo'); + }); - onMount(() => { - send({ type: 'INC' }); - send({ type: 'INC' }); - send({ type: 'INC' }); - setTimeout(() => { + onMount(() => { + send({ type: 'INC' }); + send({ type: 'INC' }); send({ type: 'INC' }); setTimeout(() => { send({ type: 'INC' }); setTimeout(() => { - expect(calls).toBe(1); - done(); - }, 100); + send({ type: 'INC' }); + setTimeout(() => { + expect(calls).toBe(1); + resolve(); + }, 100); + }); }); }); - }); - return null; - }; + return null; + }; - render(() => ); - }); + render(() => ); + })); it('should successfully spawn actors from the lazily declared context', () => { let childSpawned = false; @@ -1081,7 +1085,7 @@ describe('useActor', () => { }); it('should be able to use a delay provided outside of SolidJS', () => { - jest.useFakeTimers(); + vi.useFakeTimers(); const machine = createMachine( { @@ -1130,7 +1134,7 @@ describe('useActor', () => { expect(screen.getByTestId('result').textContent).toBe('b'); - jest.advanceTimersByTime(310); + vi.advanceTimersByTime(310); expect(screen.getByTestId('result').textContent).toBe('c'); }); @@ -1389,39 +1393,40 @@ describe('useActor', () => { expect(machine2Value.textContent).toEqual('101'); }); - it('Service should stop on component cleanup', (done) => { - jest.useFakeTimers(); - const machine = createMachine({ - initial: 'a', - states: { - a: { - on: { - EV: { - target: 'b' + it('Service should stop on component cleanup', () => + new Promise((resolve) => { + vi.useFakeTimers(); + const machine = createMachine({ + initial: 'a', + states: { + a: { + on: { + EV: { + target: 'b' + } } - } - }, - b: {} - } - }); - const Display = () => { - onCleanup(() => { - expect(service.getSnapshot().status).toBe('stopped'); - done(); + }, + b: {} + } }); - const [state, , service] = useActor(machine); - return
{state.toString()}
; - }; - const Counter = () => { - const [show, setShow] = createSignal(true); - setTimeout(() => setShow(false), 100); + const Display = () => { + onCleanup(() => { + expect(service.getSnapshot().status).toBe('stopped'); + resolve(); + }); + const [state, , service] = useActor(machine); + return
{state.toString()}
; + }; + const Counter = () => { + const [show, setShow] = createSignal(true); + setTimeout(() => setShow(false), 100); - return
{show() ? : null}
; - }; + return
{show() ? : null}
; + }; - render(() => ); - jest.advanceTimersByTime(200); - }); + render(() => ); + vi.advanceTimersByTime(200); + })); it('.can should trigger on context change', () => { const machine = createMachine( @@ -1541,42 +1546,43 @@ describe('useActor', () => { expect(activatedCount).toEqual(1); }); - it('child component should be able to send an event to a parent immediately in an effect', (done) => { - const machine = createMachine({ - types: {} as { - events: { type: 'FINISH' }; - }, - initial: 'active', - states: { - active: { - on: { FINISH: 'success' } + it('child component should be able to send an event to a parent immediately in an effect', () => + new Promise((resolve) => { + const machine = createMachine({ + types: {} as { + events: { type: 'FINISH' }; }, - success: {} - } - }); - - const ChildTest = (props: { send: any }) => { - // This will send an event to the parent service - // BEFORE the service is ready. - onMount(() => { - props.send({ type: 'FINISH' }); + initial: 'active', + states: { + active: { + on: { FINISH: 'success' } + }, + success: {} + } }); - return null; - }; + const ChildTest = (props: { send: any }) => { + // This will send an event to the parent service + // BEFORE the service is ready. + onMount(() => { + props.send({ type: 'FINISH' }); + }); - const Test = () => { - const [state, send] = useActor(machine); - createEffect(() => { - if (state.matches('success')) { - done(); - } - }); - return ; - }; + return null; + }; - render(() => ); - }); + const Test = () => { + const [state, send] = useActor(machine); + createEffect(() => { + if (state.matches('success')) { + resolve(); + } + }); + return ; + }; + + render(() => ); + })); it('custom data should be available right away for the invoked actor', () => { const childMachine = createMachine({ @@ -1633,7 +1639,7 @@ describe('useActor', () => { // https://github.com/davidkpiano/xstate/issues/1334 it('delayed transitions should work when initializing from a rehydrated state', () => { - jest.useFakeTimers(); + vi.useFakeTimers(); try { const testMachine = createMachine({ types: {} as { @@ -1681,11 +1687,11 @@ describe('useActor', () => { const button = screen.getByTestId('button'); fireEvent.click(button); - jest.advanceTimersByTime(110); + vi.advanceTimersByTime(110); expect(container.textContent).toBe('idle'); } finally { - jest.useRealTimers(); + vi.useRealTimers(); } }); diff --git a/packages/xstate-solid/test/useActorRef.test.tsx b/packages/xstate-solid/test/useActorRef.test.tsx index 3cb819a81e..61e2c33f44 100644 --- a/packages/xstate-solid/test/useActorRef.test.tsx +++ b/packages/xstate-solid/test/useActorRef.test.tsx @@ -1,87 +1,89 @@ /* @jsxImportSource solid-js */ import { createMachine } from 'xstate'; -import { render, fireEvent, screen } from 'solid-testing-library'; +import { render, fireEvent, screen } from '@solidjs/testing-library'; import { useActorRef } from '../src/index.ts'; import { createEffect } from 'solid-js'; describe('useActorRef', () => { - it('observer should be called with next state', (done) => { - const machine = createMachine({ - initial: 'inactive', - states: { - inactive: { - on: { - ACTIVATE: 'active' - } - }, - active: {} - } - }); + it('observer should be called with next state', () => + new Promise((resolve) => { + const machine = createMachine({ + initial: 'inactive', + states: { + inactive: { + on: { + ACTIVATE: 'active' + } + }, + active: {} + } + }); - const App = () => { - const service = useActorRef(machine); + const App = () => { + const service = useActorRef(machine); - createEffect(() => { - service.subscribe((state) => { - if (state.matches('active')) { - done(); - } + createEffect(() => { + service.subscribe((state) => { + if (state.matches('active')) { + resolve(); + } + }); }); - }); - return ( -