@praxisjs/fsm 0.2.0 → 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 CHANGED
@@ -1,5 +1,19 @@
1
1
  # @praxisjs/fsm
2
2
 
3
+ ## 0.2.2
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [fe39901]
8
+ - @praxisjs/core@0.4.1
9
+
10
+ ## 0.2.1
11
+
12
+ ### Patch Changes
13
+
14
+ - Updated dependencies [f52354d]
15
+ - @praxisjs/core@0.4.0
16
+
3
17
  ## 0.2.0
4
18
 
5
19
  ### Minor Changes
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=decorators.test.d.ts.map
@@ -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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=machine.test.d.ts.map
@@ -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.0",
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.3.0",
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
+ });