@praxisjs/fsm 0.2.1 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/dist/__tests__/decorators.test.d.ts +2 -0
- package/dist/__tests__/decorators.test.d.ts.map +1 -0
- package/dist/__tests__/decorators.test.js +84 -0
- package/dist/__tests__/decorators.test.js.map +1 -0
- package/dist/__tests__/machine.test.d.ts +2 -0
- package/dist/__tests__/machine.test.d.ts.map +1 -0
- package/dist/__tests__/machine.test.js +104 -0
- package/dist/__tests__/machine.test.js.map +1 -0
- package/package.json +2 -2
- package/src/__tests__/decorators.test.ts +103 -0
- package/src/__tests__/machine.test.ts +121 -0
package/CHANGELOG.md
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"decorators.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/decorators.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import { Transition, StateMachine } from "../decorators";
|
|
3
|
+
import { createMachine } from "../machine";
|
|
4
|
+
const TOGGLE_DEF = {
|
|
5
|
+
initial: "off",
|
|
6
|
+
states: {
|
|
7
|
+
off: { on: { toggle: "on" } },
|
|
8
|
+
on: { on: { toggle: "off" } },
|
|
9
|
+
},
|
|
10
|
+
};
|
|
11
|
+
// ── StateMachine ──────────────────────────────────────────────────────────────
|
|
12
|
+
describe("StateMachine decorator", () => {
|
|
13
|
+
it("adds a machine property via prototype", () => {
|
|
14
|
+
class Light {
|
|
15
|
+
}
|
|
16
|
+
StateMachine(TOGGLE_DEF)(Light, {});
|
|
17
|
+
const instance = new Light();
|
|
18
|
+
expect(instance.machine).toBeDefined();
|
|
19
|
+
expect(instance.machine.state()).toBe("off");
|
|
20
|
+
});
|
|
21
|
+
it("returns a separate machine per instance", () => {
|
|
22
|
+
class Bulb {
|
|
23
|
+
}
|
|
24
|
+
StateMachine(TOGGLE_DEF)(Bulb, {});
|
|
25
|
+
const a = new Bulb();
|
|
26
|
+
const b = new Bulb();
|
|
27
|
+
a.machine.send("toggle");
|
|
28
|
+
expect(a.machine.state()).toBe("on");
|
|
29
|
+
expect(b.machine.state()).toBe("off");
|
|
30
|
+
});
|
|
31
|
+
it("uses a custom property key", () => {
|
|
32
|
+
class Widget {
|
|
33
|
+
}
|
|
34
|
+
StateMachine(TOGGLE_DEF, "fsm")(Widget, {});
|
|
35
|
+
const instance = new Widget();
|
|
36
|
+
expect(instance.fsm).toBeDefined();
|
|
37
|
+
});
|
|
38
|
+
it("returns the same instance on repeated access", () => {
|
|
39
|
+
class Btn {
|
|
40
|
+
}
|
|
41
|
+
StateMachine(TOGGLE_DEF)(Btn, {});
|
|
42
|
+
const instance = new Btn();
|
|
43
|
+
expect(instance.machine).toBe(instance.machine);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
// ── Transition ────────────────────────────────────────────────────────────────
|
|
47
|
+
describe("Transition decorator", () => {
|
|
48
|
+
it("calls the original method when send succeeds", () => {
|
|
49
|
+
const original = vi.fn(() => "ran");
|
|
50
|
+
const wrapped = Transition("machine", "toggle")(original, {});
|
|
51
|
+
const instance = {
|
|
52
|
+
machine: createMachine(TOGGLE_DEF),
|
|
53
|
+
};
|
|
54
|
+
wrapped.call(instance);
|
|
55
|
+
expect(original).toHaveBeenCalled();
|
|
56
|
+
});
|
|
57
|
+
it("returns without calling method when machine is missing", () => {
|
|
58
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => { });
|
|
59
|
+
const original = vi.fn();
|
|
60
|
+
const wrapped = Transition("machine", "toggle")(original, {});
|
|
61
|
+
wrapped.call({});
|
|
62
|
+
expect(original).not.toHaveBeenCalled();
|
|
63
|
+
expect(warn).toHaveBeenCalledWith(expect.stringContaining("[Transition]"));
|
|
64
|
+
warn.mockRestore();
|
|
65
|
+
});
|
|
66
|
+
it("does not call method when transition is invalid", () => {
|
|
67
|
+
const original = vi.fn();
|
|
68
|
+
const wrapped = Transition("machine", "toggle")(original, {});
|
|
69
|
+
const instance = {
|
|
70
|
+
machine: createMachine({ initial: "on", states: { on: {} } }),
|
|
71
|
+
};
|
|
72
|
+
// "toggle" is not a valid event from "on" state
|
|
73
|
+
wrapped.call(instance);
|
|
74
|
+
expect(original).not.toHaveBeenCalled();
|
|
75
|
+
});
|
|
76
|
+
it("passes through arguments to the original method", () => {
|
|
77
|
+
const original = vi.fn((_x) => undefined);
|
|
78
|
+
const wrapped = Transition("machine", "toggle")(original, {});
|
|
79
|
+
const instance = { machine: createMachine(TOGGLE_DEF) };
|
|
80
|
+
wrapped.call(instance, 42);
|
|
81
|
+
expect(original).toHaveBeenCalledWith(42);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
//# sourceMappingURL=decorators.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"decorators.test.js","sourceRoot":"","sources":["../../src/__tests__/decorators.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"machine.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/machine.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import { createMachine } from "../machine";
|
|
3
|
+
function trafficLight() {
|
|
4
|
+
return createMachine({
|
|
5
|
+
initial: "red",
|
|
6
|
+
states: {
|
|
7
|
+
red: { on: { GO: "green" } },
|
|
8
|
+
green: { on: { SLOW: "yellow" } },
|
|
9
|
+
yellow: { on: { STOP: "red" } },
|
|
10
|
+
},
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
describe("createMachine", () => {
|
|
14
|
+
it("initializes in the given state", () => {
|
|
15
|
+
const m = trafficLight();
|
|
16
|
+
expect(m.state()).toBe("red");
|
|
17
|
+
});
|
|
18
|
+
it("transitions on a valid event", () => {
|
|
19
|
+
const m = trafficLight();
|
|
20
|
+
m.send("GO");
|
|
21
|
+
expect(m.state()).toBe("green");
|
|
22
|
+
});
|
|
23
|
+
it("returns true from send() on valid transition", () => {
|
|
24
|
+
const m = trafficLight();
|
|
25
|
+
expect(m.send("GO")).toBe(true);
|
|
26
|
+
});
|
|
27
|
+
it("returns false from send() on invalid event", () => {
|
|
28
|
+
const m = trafficLight();
|
|
29
|
+
expect(m.send("SLOW")).toBe(false); // SLOW is not valid in red
|
|
30
|
+
});
|
|
31
|
+
it("does not change state on invalid event", () => {
|
|
32
|
+
const m = trafficLight();
|
|
33
|
+
m.send("STOP"); // invalid in red
|
|
34
|
+
expect(m.state()).toBe("red");
|
|
35
|
+
});
|
|
36
|
+
it("can() returns true for valid events", () => {
|
|
37
|
+
const m = trafficLight();
|
|
38
|
+
expect(m.can("GO")).toBe(true);
|
|
39
|
+
});
|
|
40
|
+
it("can() returns false for invalid events", () => {
|
|
41
|
+
const m = trafficLight();
|
|
42
|
+
expect(m.can("SLOW")).toBe(false);
|
|
43
|
+
});
|
|
44
|
+
it("is() checks current state", () => {
|
|
45
|
+
const m = trafficLight();
|
|
46
|
+
expect(m.is("red")).toBe(true);
|
|
47
|
+
expect(m.is("green")).toBe(false);
|
|
48
|
+
});
|
|
49
|
+
it("tracks history of transitions", () => {
|
|
50
|
+
const m = trafficLight();
|
|
51
|
+
m.send("GO");
|
|
52
|
+
m.send("SLOW");
|
|
53
|
+
const h = m.history();
|
|
54
|
+
expect(h).toHaveLength(2);
|
|
55
|
+
expect(h[0]).toEqual({ from: "red", event: "GO", to: "green" });
|
|
56
|
+
expect(h[1]).toEqual({ from: "green", event: "SLOW", to: "yellow" });
|
|
57
|
+
});
|
|
58
|
+
it("resets to initial state and clears history", () => {
|
|
59
|
+
const m = trafficLight();
|
|
60
|
+
m.send("GO");
|
|
61
|
+
m.send("SLOW");
|
|
62
|
+
m.reset();
|
|
63
|
+
expect(m.state()).toBe("red");
|
|
64
|
+
expect(m.history()).toHaveLength(0);
|
|
65
|
+
});
|
|
66
|
+
it("calls onEnter on entering a state", () => {
|
|
67
|
+
const onEnter = vi.fn();
|
|
68
|
+
const m = createMachine({
|
|
69
|
+
initial: "idle",
|
|
70
|
+
states: {
|
|
71
|
+
idle: { on: { START: "active" } },
|
|
72
|
+
active: { onEnter },
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
m.send("START");
|
|
76
|
+
expect(onEnter).toHaveBeenCalledOnce();
|
|
77
|
+
});
|
|
78
|
+
it("calls onExit on leaving a state", () => {
|
|
79
|
+
const onExit = vi.fn();
|
|
80
|
+
const m = createMachine({
|
|
81
|
+
initial: "idle",
|
|
82
|
+
states: {
|
|
83
|
+
idle: { on: { START: "active" }, onExit },
|
|
84
|
+
active: {},
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
m.send("START");
|
|
88
|
+
expect(onExit).toHaveBeenCalledOnce();
|
|
89
|
+
});
|
|
90
|
+
it("calls onTransition callback", () => {
|
|
91
|
+
const onTransition = vi.fn();
|
|
92
|
+
const m = createMachine({
|
|
93
|
+
initial: "a",
|
|
94
|
+
states: {
|
|
95
|
+
a: { on: { NEXT: "b" } },
|
|
96
|
+
b: {},
|
|
97
|
+
},
|
|
98
|
+
onTransition,
|
|
99
|
+
});
|
|
100
|
+
m.send("NEXT");
|
|
101
|
+
expect(onTransition).toHaveBeenCalledWith("a", "NEXT", "b");
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
//# sourceMappingURL=machine.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"machine.test.js","sourceRoot":"","sources":["../../src/__tests__/machine.test.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@praxisjs/fsm",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"typescript": "^5.9.3"
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"@praxisjs/core": "0.4.
|
|
17
|
+
"@praxisjs/core": "0.4.1",
|
|
18
18
|
"@praxisjs/shared": "0.2.0"
|
|
19
19
|
},
|
|
20
20
|
"scripts": {
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { Transition, StateMachine } from "../decorators";
|
|
4
|
+
import { createMachine } from "../machine";
|
|
5
|
+
|
|
6
|
+
const TOGGLE_DEF = {
|
|
7
|
+
initial: "off" as const,
|
|
8
|
+
states: {
|
|
9
|
+
off: { on: { toggle: "on" as const } },
|
|
10
|
+
on: { on: { toggle: "off" as const } },
|
|
11
|
+
},
|
|
12
|
+
} as const;
|
|
13
|
+
|
|
14
|
+
// ── StateMachine ──────────────────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
describe("StateMachine decorator", () => {
|
|
17
|
+
it("adds a machine property via prototype", () => {
|
|
18
|
+
class Light {}
|
|
19
|
+
StateMachine(TOGGLE_DEF)(Light, {} as ClassDecoratorContext);
|
|
20
|
+
const instance = new Light() as Record<string, unknown>;
|
|
21
|
+
expect(instance.machine).toBeDefined();
|
|
22
|
+
expect((instance.machine as ReturnType<typeof createMachine>).state()).toBe("off");
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("returns a separate machine per instance", () => {
|
|
26
|
+
class Bulb {}
|
|
27
|
+
StateMachine(TOGGLE_DEF)(Bulb, {} as ClassDecoratorContext);
|
|
28
|
+
const a = new Bulb() as Record<string, unknown>;
|
|
29
|
+
const b = new Bulb() as Record<string, unknown>;
|
|
30
|
+
(a.machine as ReturnType<typeof createMachine>).send("toggle");
|
|
31
|
+
expect((a.machine as ReturnType<typeof createMachine>).state()).toBe("on");
|
|
32
|
+
expect((b.machine as ReturnType<typeof createMachine>).state()).toBe("off");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("uses a custom property key", () => {
|
|
36
|
+
class Widget {}
|
|
37
|
+
StateMachine(TOGGLE_DEF, "fsm")(Widget, {} as ClassDecoratorContext);
|
|
38
|
+
const instance = new Widget() as Record<string, unknown>;
|
|
39
|
+
expect(instance.fsm).toBeDefined();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("returns the same instance on repeated access", () => {
|
|
43
|
+
class Btn {}
|
|
44
|
+
StateMachine(TOGGLE_DEF)(Btn, {} as ClassDecoratorContext);
|
|
45
|
+
const instance = new Btn() as Record<string, unknown>;
|
|
46
|
+
expect(instance.machine).toBe(instance.machine);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// ── Transition ────────────────────────────────────────────────────────────────
|
|
51
|
+
|
|
52
|
+
describe("Transition decorator", () => {
|
|
53
|
+
it("calls the original method when send succeeds", () => {
|
|
54
|
+
const original = vi.fn(() => "ran");
|
|
55
|
+
const wrapped = Transition("machine", "toggle")(
|
|
56
|
+
original,
|
|
57
|
+
{} as ClassMethodDecoratorContext,
|
|
58
|
+
);
|
|
59
|
+
const instance = {
|
|
60
|
+
machine: createMachine(TOGGLE_DEF),
|
|
61
|
+
} as Record<string, unknown>;
|
|
62
|
+
wrapped.call(instance);
|
|
63
|
+
expect(original).toHaveBeenCalled();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("returns without calling method when machine is missing", () => {
|
|
67
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
68
|
+
const original = vi.fn();
|
|
69
|
+
const wrapped = Transition("machine", "toggle")(
|
|
70
|
+
original,
|
|
71
|
+
{} as ClassMethodDecoratorContext,
|
|
72
|
+
);
|
|
73
|
+
wrapped.call({} as Record<string, unknown>);
|
|
74
|
+
expect(original).not.toHaveBeenCalled();
|
|
75
|
+
expect(warn).toHaveBeenCalledWith(expect.stringContaining("[Transition]"));
|
|
76
|
+
warn.mockRestore();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("does not call method when transition is invalid", () => {
|
|
80
|
+
const original = vi.fn();
|
|
81
|
+
const wrapped = Transition("machine", "toggle")(
|
|
82
|
+
original,
|
|
83
|
+
{} as ClassMethodDecoratorContext,
|
|
84
|
+
);
|
|
85
|
+
const instance = {
|
|
86
|
+
machine: createMachine({ initial: "on" as const, states: { on: {} } }),
|
|
87
|
+
} as Record<string, unknown>;
|
|
88
|
+
// "toggle" is not a valid event from "on" state
|
|
89
|
+
wrapped.call(instance);
|
|
90
|
+
expect(original).not.toHaveBeenCalled();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("passes through arguments to the original method", () => {
|
|
94
|
+
const original = vi.fn((_x: unknown) => undefined);
|
|
95
|
+
const wrapped = Transition("machine", "toggle")(
|
|
96
|
+
original,
|
|
97
|
+
{} as ClassMethodDecoratorContext,
|
|
98
|
+
);
|
|
99
|
+
const instance = { machine: createMachine(TOGGLE_DEF) } as Record<string, unknown>;
|
|
100
|
+
wrapped.call(instance, 42);
|
|
101
|
+
expect(original).toHaveBeenCalledWith(42);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { createMachine } from "../machine";
|
|
4
|
+
|
|
5
|
+
type TrafficState = "red" | "green" | "yellow";
|
|
6
|
+
type TrafficEvent = "GO" | "SLOW" | "STOP";
|
|
7
|
+
|
|
8
|
+
function trafficLight() {
|
|
9
|
+
return createMachine<TrafficState, TrafficEvent>({
|
|
10
|
+
initial: "red",
|
|
11
|
+
states: {
|
|
12
|
+
red: { on: { GO: "green" } },
|
|
13
|
+
green: { on: { SLOW: "yellow" } },
|
|
14
|
+
yellow: { on: { STOP: "red" } },
|
|
15
|
+
},
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe("createMachine", () => {
|
|
20
|
+
it("initializes in the given state", () => {
|
|
21
|
+
const m = trafficLight();
|
|
22
|
+
expect(m.state()).toBe("red");
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("transitions on a valid event", () => {
|
|
26
|
+
const m = trafficLight();
|
|
27
|
+
m.send("GO");
|
|
28
|
+
expect(m.state()).toBe("green");
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("returns true from send() on valid transition", () => {
|
|
32
|
+
const m = trafficLight();
|
|
33
|
+
expect(m.send("GO")).toBe(true);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("returns false from send() on invalid event", () => {
|
|
37
|
+
const m = trafficLight();
|
|
38
|
+
expect(m.send("SLOW")).toBe(false); // SLOW is not valid in red
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("does not change state on invalid event", () => {
|
|
42
|
+
const m = trafficLight();
|
|
43
|
+
m.send("STOP"); // invalid in red
|
|
44
|
+
expect(m.state()).toBe("red");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("can() returns true for valid events", () => {
|
|
48
|
+
const m = trafficLight();
|
|
49
|
+
expect(m.can("GO")).toBe(true);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("can() returns false for invalid events", () => {
|
|
53
|
+
const m = trafficLight();
|
|
54
|
+
expect(m.can("SLOW")).toBe(false);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("is() checks current state", () => {
|
|
58
|
+
const m = trafficLight();
|
|
59
|
+
expect(m.is("red")).toBe(true);
|
|
60
|
+
expect(m.is("green")).toBe(false);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("tracks history of transitions", () => {
|
|
64
|
+
const m = trafficLight();
|
|
65
|
+
m.send("GO");
|
|
66
|
+
m.send("SLOW");
|
|
67
|
+
const h = m.history();
|
|
68
|
+
expect(h).toHaveLength(2);
|
|
69
|
+
expect(h[0]).toEqual({ from: "red", event: "GO", to: "green" });
|
|
70
|
+
expect(h[1]).toEqual({ from: "green", event: "SLOW", to: "yellow" });
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("resets to initial state and clears history", () => {
|
|
74
|
+
const m = trafficLight();
|
|
75
|
+
m.send("GO");
|
|
76
|
+
m.send("SLOW");
|
|
77
|
+
m.reset();
|
|
78
|
+
expect(m.state()).toBe("red");
|
|
79
|
+
expect(m.history()).toHaveLength(0);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("calls onEnter on entering a state", () => {
|
|
83
|
+
const onEnter = vi.fn();
|
|
84
|
+
const m = createMachine<"idle" | "active", "START">({
|
|
85
|
+
initial: "idle",
|
|
86
|
+
states: {
|
|
87
|
+
idle: { on: { START: "active" } },
|
|
88
|
+
active: { onEnter },
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
m.send("START");
|
|
92
|
+
expect(onEnter).toHaveBeenCalledOnce();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("calls onExit on leaving a state", () => {
|
|
96
|
+
const onExit = vi.fn();
|
|
97
|
+
const m = createMachine<"idle" | "active", "START">({
|
|
98
|
+
initial: "idle",
|
|
99
|
+
states: {
|
|
100
|
+
idle: { on: { START: "active" }, onExit },
|
|
101
|
+
active: {},
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
m.send("START");
|
|
105
|
+
expect(onExit).toHaveBeenCalledOnce();
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("calls onTransition callback", () => {
|
|
109
|
+
const onTransition = vi.fn();
|
|
110
|
+
const m = createMachine<"a" | "b", "NEXT">({
|
|
111
|
+
initial: "a",
|
|
112
|
+
states: {
|
|
113
|
+
a: { on: { NEXT: "b" } },
|
|
114
|
+
b: {},
|
|
115
|
+
},
|
|
116
|
+
onTransition,
|
|
117
|
+
});
|
|
118
|
+
m.send("NEXT");
|
|
119
|
+
expect(onTransition).toHaveBeenCalledWith("a", "NEXT", "b");
|
|
120
|
+
});
|
|
121
|
+
});
|