machina-test 0.2.0

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/LICENSE ADDED
@@ -0,0 +1,69 @@
1
+ Copyright (c) 2011-2023 Jim Cowart (MIT License)
2
+
3
+ Permission is hereby granted, free of charge, to any person
4
+ obtaining a copy of this software and associated documentation
5
+ files (the "Software"), to deal in the Software without
6
+ restriction, including without limitation the rights to use,
7
+ copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ copies of the Software, and to permit persons to whom the
9
+ Software is furnished to do so, subject to the following
10
+ conditions:
11
+
12
+ The above copyright notice and this permission notice shall be
13
+ included in all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
17
+ OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
19
+ HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
20
+ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22
+ OTHER DEALINGS IN THE SOFTWARE.
23
+
24
+ See http://opensource.org/licenses/MIT for more details.
25
+
26
+ --------------------------------------------------------------------
27
+ Also available under GPL 2.0
28
+ Copyright (c) 2011-2015 Jim Cowart (GPL-2.0 License)
29
+
30
+ This program is free software; you can redistribute it and/or modify
31
+ it under the terms of the GNU General Public License as published by
32
+ the Free Software Foundation; either version 2 of the License, or (at
33
+ your option) any later version.
34
+
35
+ This program is distributed in the hope that it will be useful, but
36
+ WITHOUT ANY WARRANTY; without even the implied warranty of
37
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
38
+ General Public License for more details.
39
+
40
+ See http://opensource.org/licenses/GPL-2.0 for more details
41
+
42
+ --------------------------------------------------------------------
43
+
44
+ The extend function included in machina was very heavily borrowed
45
+ from the backbone.js project, which can be found at http://backbonejs.org.
46
+ Due to its origin, the backbone license is included below as well.
47
+
48
+ Copyright (c) 2010-2012 Jeremy Ashkenas, DocumentCloud
49
+
50
+ Permission is hereby granted, free of charge, to any person
51
+ obtaining a copy of this software and associated documentation
52
+ files (the "Software"), to deal in the Software without
53
+ restriction, including without limitation the rights to use,
54
+ copy, modify, merge, publish, distribute, sublicense, and/or sell
55
+ copies of the Software, and to permit persons to whom the
56
+ Software is furnished to do so, subject to the following
57
+ conditions:
58
+
59
+ The above copyright notice and this permission notice shall be
60
+ included in all copies or substantial portions of the Software.
61
+
62
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
63
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
64
+ OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
65
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
66
+ HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
67
+ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
68
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
69
+ OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,110 @@
1
+ # machina-test
2
+
3
+ Jest/Vitest custom matchers for testing [machina](https://machina-js.org) FSM topology. Assert reachability, catch dead states, and verify structural invariants — all from your existing test suite.
4
+
5
+ ```ts
6
+ import "machina-test";
7
+
8
+ expect(fsm).toHaveNoUnreachableStates();
9
+ expect(fsm).toAlwaysReach("delivered", { from: "placed" });
10
+ expect(fsm).toNeverReach("shipped", { from: "cancelled" });
11
+ ```
12
+
13
+ Built on [machina-inspect](https://www.npmjs.com/package/machina-inspect). Zero ceremony beyond the import.
14
+
15
+ ## Install
16
+
17
+ ```bash
18
+ npm install --save-dev machina-test
19
+ # or
20
+ pnpm add -D machina-test
21
+ ```
22
+
23
+ `machina` >= 6.1.0 is a peer dependency. Either `jest` or `vitest` must be available as the host test runner.
24
+
25
+ ## Setup
26
+
27
+ Import `machina-test` in your test files (or in a setup file that runs after the framework's globals are available). The import registers the matchers via `expect.extend()` as a side effect.
28
+
29
+ ```ts
30
+ // in each test file
31
+ import "machina-test";
32
+
33
+ // or in jest.setup.ts / vitest.setup.ts
34
+ import "machina-test";
35
+ ```
36
+
37
+ That's it. TypeScript users get autocomplete for all three matchers automatically.
38
+
39
+ ## Matchers
40
+
41
+ ### `toHaveNoUnreachableStates()`
42
+
43
+ Asserts that every state in the FSM is reachable from `initialState`. Delegates to machina-inspect's `inspectGraph()`, which recurses into child FSM graphs.
44
+
45
+ ```ts
46
+ expect(fsm).toHaveNoUnreachableStates();
47
+ ```
48
+
49
+ ### `toAlwaysReach(targetState, { from })`
50
+
51
+ Asserts that a path exists from `from` to `targetState` in the FSM's top-level graph. BFS over all edges (both "definite" and "possible"). Top-level graph only — does not traverse into `_child` FSMs.
52
+
53
+ ```ts
54
+ expect(fsm).toAlwaysReach("delivered", { from: "placed" });
55
+ ```
56
+
57
+ "Always" refers to graph topology — the plumbing exists — not runtime certainty. Whether the path executes at runtime depends on which handlers fire.
58
+
59
+ ### `toNeverReach(targetState, { from })`
60
+
61
+ Asserts that no path exists from `from` to `targetState`. The logical inverse of `toAlwaysReach`.
62
+
63
+ ```ts
64
+ expect(fsm).toNeverReach("shipped", { from: "cancelled" });
65
+ ```
66
+
67
+ ### `.not` variants
68
+
69
+ Standard Jest/Vitest negation works as expected:
70
+
71
+ ```ts
72
+ expect(fsm).not.toAlwaysReach("shipped", { from: "cancelled" });
73
+ expect(fsm).not.toNeverReach("delivered", { from: "placed" });
74
+ ```
75
+
76
+ ### Invalid state names
77
+
78
+ Typos produce clean test failures (not thrown exceptions) with actionable messages:
79
+
80
+ ```
81
+ State 'shiped' does not exist in FSM 'order-workflow'. Available states: placed, validating, processing, shipped, delivered, cancelled, refunded.
82
+ ```
83
+
84
+ ## Testing hierarchical FSMs
85
+
86
+ `toAlwaysReach` and `toNeverReach` operate on the **top-level graph only**. They do not traverse into `_child` FSMs. This is intentional — it avoids ambiguity around composite state names and keeps assertions explicit about which level you're testing.
87
+
88
+ To test a child FSM, pass it directly to `expect()`:
89
+
90
+ ```ts
91
+ // Test the parent — sees browsing, checkout, confirmation
92
+ expect(checkout).toAlwaysReach("confirmation", { from: "browsing" });
93
+
94
+ // Test the child — sees entering-details, processing, authorized, declined
95
+ const payment = createPaymentFsm();
96
+ expect(payment).toAlwaysReach("authorized", { from: "entering-details" });
97
+ expect(payment).toNeverReach("entering-details", { from: "authorized" });
98
+ ```
99
+
100
+ **The one exception**: `toHaveNoUnreachableStates()` _does_ recurse into children via `inspectGraph()`. An orphaned state in a child surfaces as a failure when called on the parent.
101
+
102
+ ## See also
103
+
104
+ - [machina-inspect](https://www.npmjs.com/package/machina-inspect) — the graph analysis engine these matchers are built on
105
+ - [eslint-plugin-machina](https://www.npmjs.com/package/eslint-plugin-machina) — catch structural issues at lint time in your editor
106
+ - [testing-with-machina-test example](../../examples/testing-with-machina-test/) — full working example with flat and hierarchical FSMs
107
+
108
+ ## License
109
+
110
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,128 @@
1
+ let machina_inspect = require("machina-inspect");
2
+
3
+ //#region src/reachability.ts
4
+ /**
5
+ * Returns true if `to` is reachable from `from` via BFS over the
6
+ * top-level graph's edges. Returns false if either state is missing
7
+ * from the graph, or if no path exists.
8
+ *
9
+ * Treats `from === to` as trivially reachable (every state reaches itself).
10
+ */
11
+ const canReach = (graph, from, to) => {
12
+ if (from === to) return true;
13
+ if (!(from in graph.nodes)) return false;
14
+ const visited = /* @__PURE__ */ new Set();
15
+ const queue = [from];
16
+ visited.add(from);
17
+ while (queue.length > 0) {
18
+ const current = queue.shift();
19
+ const node = graph.nodes[current];
20
+ if (!node) continue;
21
+ for (const edge of node.edges) {
22
+ if (edge.to === to) return true;
23
+ if (!visited.has(edge.to) && edge.to in graph.nodes) {
24
+ visited.add(edge.to);
25
+ queue.push(edge.to);
26
+ }
27
+ }
28
+ }
29
+ return false;
30
+ };
31
+
32
+ //#endregion
33
+ //#region src/matchers.ts
34
+ /**
35
+ * Build the StateGraph for the received FSM instance.
36
+ * `buildStateGraph` accepts both config objects and live instances;
37
+ * here we always receive live instances (users pass `expect(fsm)`).
38
+ */
39
+ const getGraph = (received) => {
40
+ return (0, machina_inspect.buildStateGraph)(received);
41
+ };
42
+ /**
43
+ * Produce the "invalid state" failure message. Used when a state name
44
+ * passed to a matcher does not exist in the graph.
45
+ */
46
+ const invalidStateMessage = (stateName, fsmId, graph) => {
47
+ return `State '${stateName}' does not exist in FSM '${fsmId}'. Available states: ${Object.keys(graph.nodes).join(", ")}.`;
48
+ };
49
+ /**
50
+ * Passes when the FSM has no states that are unreachable from initialState.
51
+ * Uses inspectGraph() to run all checks and filters for unreachable-state
52
+ * findings, which includes recursive child graph analysis.
53
+ */
54
+ const toHaveNoUnreachableStates = (received) => {
55
+ const graph = getGraph(received);
56
+ const findings = (0, machina_inspect.inspectGraph)(graph).filter((f) => f.type === "unreachable-state");
57
+ const pass = findings.length === 0;
58
+ const message = () => {
59
+ if (pass) return `Expected FSM '${graph.fsmId}' to have unreachable states, but all states are reachable.`;
60
+ const stateList = findings.map((f) => `'${f.states.join(", ")}'`).join(", ");
61
+ return `Expected FSM '${graph.fsmId}' to have no unreachable states, but found: ${stateList}.`;
62
+ };
63
+ return {
64
+ pass,
65
+ message
66
+ };
67
+ };
68
+ /**
69
+ * Passes when a path exists from `options.from` to `targetState` in the
70
+ * FSM's top-level graph (BFS over all edges, definite and possible).
71
+ */
72
+ const toAlwaysReach = (received, targetState, options) => {
73
+ const graph = getGraph(received);
74
+ const { from } = options;
75
+ if (!(from in graph.nodes)) return {
76
+ pass: false,
77
+ message: () => invalidStateMessage(from, graph.fsmId, graph)
78
+ };
79
+ if (!(targetState in graph.nodes)) return {
80
+ pass: false,
81
+ message: () => invalidStateMessage(targetState, graph.fsmId, graph)
82
+ };
83
+ const pass = canReach(graph, from, targetState);
84
+ const message = () => {
85
+ if (pass) return `Expected FSM '${graph.fsmId}' to have no path from '${from}' to '${targetState}', but a path exists.`;
86
+ return `Expected FSM '${graph.fsmId}' to have a path from '${from}' to '${targetState}', but no path exists.`;
87
+ };
88
+ return {
89
+ pass,
90
+ message
91
+ };
92
+ };
93
+ /**
94
+ * Passes when NO path exists from `options.from` to `targetState`.
95
+ * The logical inverse of toAlwaysReach.
96
+ */
97
+ const toNeverReach = (received, targetState, options) => {
98
+ const graph = getGraph(received);
99
+ const { from } = options;
100
+ if (!(from in graph.nodes)) return {
101
+ pass: false,
102
+ message: () => invalidStateMessage(from, graph.fsmId, graph)
103
+ };
104
+ if (!(targetState in graph.nodes)) return {
105
+ pass: false,
106
+ message: () => invalidStateMessage(targetState, graph.fsmId, graph)
107
+ };
108
+ const pass = !canReach(graph, from, targetState);
109
+ const message = () => {
110
+ if (pass) return `Expected FSM '${graph.fsmId}' to have a path from '${from}' to '${targetState}', but no path exists.`;
111
+ return `Expected FSM '${graph.fsmId}' to have no path from '${from}' to '${targetState}', but a path exists.`;
112
+ };
113
+ return {
114
+ pass,
115
+ message
116
+ };
117
+ };
118
+
119
+ //#endregion
120
+ //#region src/index.ts
121
+ expect.extend({
122
+ toHaveNoUnreachableStates,
123
+ toAlwaysReach,
124
+ toNeverReach
125
+ });
126
+
127
+ //#endregion
128
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.cjs","names":[],"sources":["../src/reachability.ts","../src/matchers.ts","../src/index.ts"],"sourcesContent":["// =============================================================================\n// reachability.ts — BFS reachability check for StateGraph\n//\n// Top-level graph only. Child graphs are not traversed here — users who want\n// to assert reachability inside a child FSM pass the child instance directly\n// to expect().\n//\n// Both \"definite\" and \"possible\" edges are followed. The question is whether\n// the plumbing exists, not whether runtime conditions will always cause it to\n// execute. See design decisions in the build plan for rationale.\n// =============================================================================\n\nimport type { StateGraph } from \"machina-inspect\";\n\n/**\n * Returns true if `to` is reachable from `from` via BFS over the\n * top-level graph's edges. Returns false if either state is missing\n * from the graph, or if no path exists.\n *\n * Treats `from === to` as trivially reachable (every state reaches itself).\n */\nconst canReach = (graph: StateGraph, from: string, to: string): boolean => {\n // Trivially reachable — a state always reaches itself\n if (from === to) {\n return true;\n }\n\n // If the source state isn't in the graph, there's nothing to walk\n if (!(from in graph.nodes)) {\n return false;\n }\n\n const visited = new Set<string>();\n const queue: string[] = [from];\n visited.add(from);\n\n while (queue.length > 0) {\n const current = queue.shift()!;\n const node = graph.nodes[current];\n\n // Missing node is a safety check — shouldn't happen in a well-formed graph\n if (!node) {\n continue;\n }\n\n for (const edge of node.edges) {\n if (edge.to === to) {\n return true;\n }\n if (!visited.has(edge.to) && edge.to in graph.nodes) {\n visited.add(edge.to);\n queue.push(edge.to);\n }\n }\n }\n\n return false;\n};\n\nexport { canReach };\n","// =============================================================================\n// matchers.ts — Custom matcher implementations for machina FSMs\n//\n// Each matcher follows the expect.extend() contract:\n// - Receives the subject as `received`\n// - Returns { pass: boolean, message: () => string }\n// - `message` is always the FAILURE message — jest/vitest call it only\n// when the assertion fails (accounting for .not inversion)\n//\n// Invalid state names produce matcher failures, not throws, so they appear\n// as clean test failures in the runner rather than unhandled exceptions.\n// =============================================================================\n\nimport { buildStateGraph, inspectGraph, type StateGraph } from \"machina-inspect\";\nimport type { MachinaInstance } from \"machina\";\nimport { canReach } from \"./reachability\";\nimport type { ReachabilityOptions } from \"./types\";\n\n// ---------------------------------------------------------------------------\n// Internal helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Build the StateGraph for the received FSM instance.\n * `buildStateGraph` accepts both config objects and live instances;\n * here we always receive live instances (users pass `expect(fsm)`).\n */\nconst getGraph = (received: MachinaInstance): StateGraph => {\n return buildStateGraph(received);\n};\n\n/**\n * Produce the \"invalid state\" failure message. Used when a state name\n * passed to a matcher does not exist in the graph.\n */\nconst invalidStateMessage = (stateName: string, fsmId: string, graph: StateGraph): string => {\n const available = Object.keys(graph.nodes).join(\", \");\n return `State '${stateName}' does not exist in FSM '${fsmId}'. Available states: ${available}.`;\n};\n\n// ---------------------------------------------------------------------------\n// Matcher: toHaveNoUnreachableStates\n// ---------------------------------------------------------------------------\n\n/**\n * Passes when the FSM has no states that are unreachable from initialState.\n * Uses inspectGraph() to run all checks and filters for unreachable-state\n * findings, which includes recursive child graph analysis.\n */\nconst toHaveNoUnreachableStates = (received: MachinaInstance): jest.CustomMatcherResult => {\n const graph = getGraph(received);\n const findings = inspectGraph(graph).filter(f => f.type === \"unreachable-state\");\n\n const pass = findings.length === 0;\n\n const message = (): string => {\n if (pass) {\n // Negated assertion failed: .not.toHaveNoUnreachableStates() but all states reachable\n return `Expected FSM '${graph.fsmId}' to have unreachable states, but all states are reachable.`;\n }\n const stateList = findings.map(f => `'${f.states.join(\", \")}'`).join(\", \");\n return `Expected FSM '${graph.fsmId}' to have no unreachable states, but found: ${stateList}.`;\n };\n\n return { pass, message };\n};\n\n// ---------------------------------------------------------------------------\n// Matcher: toAlwaysReach\n// ---------------------------------------------------------------------------\n\n/**\n * Passes when a path exists from `options.from` to `targetState` in the\n * FSM's top-level graph (BFS over all edges, definite and possible).\n */\nconst toAlwaysReach = (\n received: MachinaInstance,\n targetState: string,\n options: ReachabilityOptions\n): jest.CustomMatcherResult => {\n const graph = getGraph(received);\n const { from } = options;\n\n // Invalid state names are reported as matcher failures for clean output\n if (!(from in graph.nodes)) {\n return {\n pass: false,\n message: () => invalidStateMessage(from, graph.fsmId, graph),\n };\n }\n\n if (!(targetState in graph.nodes)) {\n return {\n pass: false,\n message: () => invalidStateMessage(targetState, graph.fsmId, graph),\n };\n }\n\n const pass = canReach(graph, from, targetState);\n\n const message = (): string => {\n if (pass) {\n // Negated assertion failed: .not.toAlwaysReach but path exists\n return `Expected FSM '${graph.fsmId}' to have no path from '${from}' to '${targetState}', but a path exists.`;\n }\n return `Expected FSM '${graph.fsmId}' to have a path from '${from}' to '${targetState}', but no path exists.`;\n };\n\n return { pass, message };\n};\n\n// ---------------------------------------------------------------------------\n// Matcher: toNeverReach\n// ---------------------------------------------------------------------------\n\n/**\n * Passes when NO path exists from `options.from` to `targetState`.\n * The logical inverse of toAlwaysReach.\n */\nconst toNeverReach = (\n received: MachinaInstance,\n targetState: string,\n options: ReachabilityOptions\n): jest.CustomMatcherResult => {\n const graph = getGraph(received);\n const { from } = options;\n\n // Invalid state names are reported as matcher failures for clean output\n if (!(from in graph.nodes)) {\n return {\n pass: false,\n message: () => invalidStateMessage(from, graph.fsmId, graph),\n };\n }\n\n if (!(targetState in graph.nodes)) {\n return {\n pass: false,\n message: () => invalidStateMessage(targetState, graph.fsmId, graph),\n };\n }\n\n // pass: true means \"no path exists\" (the assertion that toNeverReach checks)\n const pass = !canReach(graph, from, targetState);\n\n const message = (): string => {\n if (pass) {\n // Negated assertion failed: .not.toNeverReach but no path exists\n return `Expected FSM '${graph.fsmId}' to have a path from '${from}' to '${targetState}', but no path exists.`;\n }\n return `Expected FSM '${graph.fsmId}' to have no path from '${from}' to '${targetState}', but a path exists.`;\n };\n\n return { pass, message };\n};\n\nexport { toHaveNoUnreachableStates, toAlwaysReach, toNeverReach };\n","// =============================================================================\n// index.ts — Side-effect entry point for machina-test\n//\n// Importing this module registers all three custom matchers with the global\n// `expect` provided by Jest or Vitest. This is the same pattern used by\n// @testing-library/jest-dom — the import IS the side effect.\n//\n// `expect` must be available as a global at the time this module is evaluated.\n// Put the import in a test file or in a setup file that runs after the test\n// framework's globals are injected.\n// =============================================================================\n\nimport { toHaveNoUnreachableStates, toAlwaysReach, toNeverReach } from \"./matchers\";\n\n// Register with the host test runner's global expect\nexpect.extend({ toHaveNoUnreachableStates, toAlwaysReach, toNeverReach });\n\n// Re-export the options type so consumers can import it alongside the package\nexport type { ReachabilityOptions } from \"./types\";\n"],"mappings":";;;;;;;;;;AAqBA,MAAM,YAAY,OAAmB,MAAc,OAAwB;AAEvE,KAAI,SAAS,GACT,QAAO;AAIX,KAAI,EAAE,QAAQ,MAAM,OAChB,QAAO;CAGX,MAAM,0BAAU,IAAI,KAAa;CACjC,MAAM,QAAkB,CAAC,KAAK;AAC9B,SAAQ,IAAI,KAAK;AAEjB,QAAO,MAAM,SAAS,GAAG;EACrB,MAAM,UAAU,MAAM,OAAO;EAC7B,MAAM,OAAO,MAAM,MAAM;AAGzB,MAAI,CAAC,KACD;AAGJ,OAAK,MAAM,QAAQ,KAAK,OAAO;AAC3B,OAAI,KAAK,OAAO,GACZ,QAAO;AAEX,OAAI,CAAC,QAAQ,IAAI,KAAK,GAAG,IAAI,KAAK,MAAM,MAAM,OAAO;AACjD,YAAQ,IAAI,KAAK,GAAG;AACpB,UAAM,KAAK,KAAK,GAAG;;;;AAK/B,QAAO;;;;;;;;;;AC7BX,MAAM,YAAY,aAA0C;AACxD,6CAAuB,SAAS;;;;;;AAOpC,MAAM,uBAAuB,WAAmB,OAAe,UAA8B;AAEzF,QAAO,UAAU,UAAU,2BAA2B,MAAM,uBAD1C,OAAO,KAAK,MAAM,MAAM,CAAC,KAAK,KAAK,CACwC;;;;;;;AAYjG,MAAM,6BAA6B,aAAwD;CACvF,MAAM,QAAQ,SAAS,SAAS;CAChC,MAAM,6CAAwB,MAAM,CAAC,QAAO,MAAK,EAAE,SAAS,oBAAoB;CAEhF,MAAM,OAAO,SAAS,WAAW;CAEjC,MAAM,gBAAwB;AAC1B,MAAI,KAEA,QAAO,iBAAiB,MAAM,MAAM;EAExC,MAAM,YAAY,SAAS,KAAI,MAAK,IAAI,EAAE,OAAO,KAAK,KAAK,CAAC,GAAG,CAAC,KAAK,KAAK;AAC1E,SAAO,iBAAiB,MAAM,MAAM,8CAA8C,UAAU;;AAGhG,QAAO;EAAE;EAAM;EAAS;;;;;;AAW5B,MAAM,iBACF,UACA,aACA,YAC2B;CAC3B,MAAM,QAAQ,SAAS,SAAS;CAChC,MAAM,EAAE,SAAS;AAGjB,KAAI,EAAE,QAAQ,MAAM,OAChB,QAAO;EACH,MAAM;EACN,eAAe,oBAAoB,MAAM,MAAM,OAAO,MAAM;EAC/D;AAGL,KAAI,EAAE,eAAe,MAAM,OACvB,QAAO;EACH,MAAM;EACN,eAAe,oBAAoB,aAAa,MAAM,OAAO,MAAM;EACtE;CAGL,MAAM,OAAO,SAAS,OAAO,MAAM,YAAY;CAE/C,MAAM,gBAAwB;AAC1B,MAAI,KAEA,QAAO,iBAAiB,MAAM,MAAM,0BAA0B,KAAK,QAAQ,YAAY;AAE3F,SAAO,iBAAiB,MAAM,MAAM,yBAAyB,KAAK,QAAQ,YAAY;;AAG1F,QAAO;EAAE;EAAM;EAAS;;;;;;AAW5B,MAAM,gBACF,UACA,aACA,YAC2B;CAC3B,MAAM,QAAQ,SAAS,SAAS;CAChC,MAAM,EAAE,SAAS;AAGjB,KAAI,EAAE,QAAQ,MAAM,OAChB,QAAO;EACH,MAAM;EACN,eAAe,oBAAoB,MAAM,MAAM,OAAO,MAAM;EAC/D;AAGL,KAAI,EAAE,eAAe,MAAM,OACvB,QAAO;EACH,MAAM;EACN,eAAe,oBAAoB,aAAa,MAAM,OAAO,MAAM;EACtE;CAIL,MAAM,OAAO,CAAC,SAAS,OAAO,MAAM,YAAY;CAEhD,MAAM,gBAAwB;AAC1B,MAAI,KAEA,QAAO,iBAAiB,MAAM,MAAM,yBAAyB,KAAK,QAAQ,YAAY;AAE1F,SAAO,iBAAiB,MAAM,MAAM,0BAA0B,KAAK,QAAQ,YAAY;;AAG3F,QAAO;EAAE;EAAM;EAAS;;;;;AC1I5B,OAAO,OAAO;CAAE;CAA2B;CAAe;CAAc,CAAC"}
@@ -0,0 +1,50 @@
1
+ //#region src/types.d.ts
2
+ interface ReachabilityOptions {
3
+ /** The state to start the BFS from */
4
+ from: string;
5
+ }
6
+ declare global {
7
+ namespace jest {
8
+ interface Matchers<R> {
9
+ /**
10
+ * Asserts that every state in the FSM is reachable from its
11
+ * initialState. Delegates to `inspectGraph` and filters for
12
+ * unreachable-state findings.
13
+ */
14
+ toHaveNoUnreachableStates(): R;
15
+ /**
16
+ * Asserts that a path exists (via BFS over graph edges) from
17
+ * `options.from` to `targetState`.
18
+ */
19
+ toAlwaysReach(targetState: string, options: ReachabilityOptions): R;
20
+ /**
21
+ * Asserts that NO path exists (via BFS over graph edges) from
22
+ * `options.from` to `targetState`.
23
+ */
24
+ toNeverReach(targetState: string, options: ReachabilityOptions): R;
25
+ }
26
+ }
27
+ }
28
+ declare module "vitest" {
29
+ interface Assertion<T = any> {
30
+ /**
31
+ * Asserts that every state in the FSM is reachable from its
32
+ * initialState. Delegates to `inspectGraph` and filters for
33
+ * unreachable-state findings.
34
+ */
35
+ toHaveNoUnreachableStates(): T;
36
+ /**
37
+ * Asserts that a path exists (via BFS over graph edges) from
38
+ * `options.from` to `targetState`.
39
+ */
40
+ toAlwaysReach(targetState: string, options: ReachabilityOptions): T;
41
+ /**
42
+ * Asserts that NO path exists (via BFS over graph edges) from
43
+ * `options.from` to `targetState`.
44
+ */
45
+ toNeverReach(targetState: string, options: ReachabilityOptions): T;
46
+ }
47
+ } //# sourceMappingURL=types.d.ts.map
48
+ //#endregion
49
+ export { type ReachabilityOptions };
50
+ //# sourceMappingURL=index.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.cts","names":[],"sources":["../src/types.ts"],"mappings":";UAWiB,mBAAA;EAAA;EAEb,IAAA;AAAA;AAAA,QAII,MAAA;EAAA,UAEM,IAAA;IAAA,UACI,QAAA;MANjB;;;;;MAYW,yBAAA,IAA6B,CAAA;MAYoC;;;;MANjE,aAAA,CAAc,WAAA,UAAqB,OAAA,EAAS,mBAAA,GAAsB,CAAA;MAZ5D;;;;MAkBN,YAAA,CAAa,WAAA,UAAqB,OAAA,EAAS,mBAAA,GAAsB,CAAA;IAAA;EAAA;AAAA;AAAA;EAAA,UAQ/D,SAAA;IARW;;;;;IAcjB,yBAAA,IAA6B,CAAA;IAdyC;AAAA;;;IAoBtE,aAAA,CAAc,WAAA,UAAqB,OAAA,EAAS,mBAAA,GAAsB,CAAA;IAAtB;;;;IAM5C,YAAA,CAAa,WAAA,UAAqB,OAAA,EAAS,mBAAA,GAAsB,CAAA;EAAA;AAAA"}
@@ -0,0 +1,50 @@
1
+ //#region src/types.d.ts
2
+ interface ReachabilityOptions {
3
+ /** The state to start the BFS from */
4
+ from: string;
5
+ }
6
+ declare global {
7
+ namespace jest {
8
+ interface Matchers<R> {
9
+ /**
10
+ * Asserts that every state in the FSM is reachable from its
11
+ * initialState. Delegates to `inspectGraph` and filters for
12
+ * unreachable-state findings.
13
+ */
14
+ toHaveNoUnreachableStates(): R;
15
+ /**
16
+ * Asserts that a path exists (via BFS over graph edges) from
17
+ * `options.from` to `targetState`.
18
+ */
19
+ toAlwaysReach(targetState: string, options: ReachabilityOptions): R;
20
+ /**
21
+ * Asserts that NO path exists (via BFS over graph edges) from
22
+ * `options.from` to `targetState`.
23
+ */
24
+ toNeverReach(targetState: string, options: ReachabilityOptions): R;
25
+ }
26
+ }
27
+ }
28
+ declare module "vitest" {
29
+ interface Assertion<T = any> {
30
+ /**
31
+ * Asserts that every state in the FSM is reachable from its
32
+ * initialState. Delegates to `inspectGraph` and filters for
33
+ * unreachable-state findings.
34
+ */
35
+ toHaveNoUnreachableStates(): T;
36
+ /**
37
+ * Asserts that a path exists (via BFS over graph edges) from
38
+ * `options.from` to `targetState`.
39
+ */
40
+ toAlwaysReach(targetState: string, options: ReachabilityOptions): T;
41
+ /**
42
+ * Asserts that NO path exists (via BFS over graph edges) from
43
+ * `options.from` to `targetState`.
44
+ */
45
+ toNeverReach(targetState: string, options: ReachabilityOptions): T;
46
+ }
47
+ } //# sourceMappingURL=types.d.ts.map
48
+ //#endregion
49
+ export { type ReachabilityOptions };
50
+ //# sourceMappingURL=index.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/types.ts"],"mappings":";UAWiB,mBAAA;EAAA;EAEb,IAAA;AAAA;AAAA,QAII,MAAA;EAAA,UAEM,IAAA;IAAA,UACI,QAAA;MANjB;;;;;MAYW,yBAAA,IAA6B,CAAA;MAYoC;;;;MANjE,aAAA,CAAc,WAAA,UAAqB,OAAA,EAAS,mBAAA,GAAsB,CAAA;MAZ5D;;;;MAkBN,YAAA,CAAa,WAAA,UAAqB,OAAA,EAAS,mBAAA,GAAsB,CAAA;IAAA;EAAA;AAAA;AAAA;EAAA,UAQ/D,SAAA;IARW;;;;;IAcjB,yBAAA,IAA6B,CAAA;IAdyC;AAAA;;;IAoBtE,aAAA,CAAc,WAAA,UAAqB,OAAA,EAAS,mBAAA,GAAsB,CAAA;IAAtB;;;;IAM5C,YAAA,CAAa,WAAA,UAAqB,OAAA,EAAS,mBAAA,GAAsB,CAAA;EAAA;AAAA"}
package/dist/index.mjs ADDED
@@ -0,0 +1,129 @@
1
+ import { buildStateGraph, inspectGraph } from "machina-inspect";
2
+
3
+ //#region src/reachability.ts
4
+ /**
5
+ * Returns true if `to` is reachable from `from` via BFS over the
6
+ * top-level graph's edges. Returns false if either state is missing
7
+ * from the graph, or if no path exists.
8
+ *
9
+ * Treats `from === to` as trivially reachable (every state reaches itself).
10
+ */
11
+ const canReach = (graph, from, to) => {
12
+ if (from === to) return true;
13
+ if (!(from in graph.nodes)) return false;
14
+ const visited = /* @__PURE__ */ new Set();
15
+ const queue = [from];
16
+ visited.add(from);
17
+ while (queue.length > 0) {
18
+ const current = queue.shift();
19
+ const node = graph.nodes[current];
20
+ if (!node) continue;
21
+ for (const edge of node.edges) {
22
+ if (edge.to === to) return true;
23
+ if (!visited.has(edge.to) && edge.to in graph.nodes) {
24
+ visited.add(edge.to);
25
+ queue.push(edge.to);
26
+ }
27
+ }
28
+ }
29
+ return false;
30
+ };
31
+
32
+ //#endregion
33
+ //#region src/matchers.ts
34
+ /**
35
+ * Build the StateGraph for the received FSM instance.
36
+ * `buildStateGraph` accepts both config objects and live instances;
37
+ * here we always receive live instances (users pass `expect(fsm)`).
38
+ */
39
+ const getGraph = (received) => {
40
+ return buildStateGraph(received);
41
+ };
42
+ /**
43
+ * Produce the "invalid state" failure message. Used when a state name
44
+ * passed to a matcher does not exist in the graph.
45
+ */
46
+ const invalidStateMessage = (stateName, fsmId, graph) => {
47
+ return `State '${stateName}' does not exist in FSM '${fsmId}'. Available states: ${Object.keys(graph.nodes).join(", ")}.`;
48
+ };
49
+ /**
50
+ * Passes when the FSM has no states that are unreachable from initialState.
51
+ * Uses inspectGraph() to run all checks and filters for unreachable-state
52
+ * findings, which includes recursive child graph analysis.
53
+ */
54
+ const toHaveNoUnreachableStates = (received) => {
55
+ const graph = getGraph(received);
56
+ const findings = inspectGraph(graph).filter((f) => f.type === "unreachable-state");
57
+ const pass = findings.length === 0;
58
+ const message = () => {
59
+ if (pass) return `Expected FSM '${graph.fsmId}' to have unreachable states, but all states are reachable.`;
60
+ const stateList = findings.map((f) => `'${f.states.join(", ")}'`).join(", ");
61
+ return `Expected FSM '${graph.fsmId}' to have no unreachable states, but found: ${stateList}.`;
62
+ };
63
+ return {
64
+ pass,
65
+ message
66
+ };
67
+ };
68
+ /**
69
+ * Passes when a path exists from `options.from` to `targetState` in the
70
+ * FSM's top-level graph (BFS over all edges, definite and possible).
71
+ */
72
+ const toAlwaysReach = (received, targetState, options) => {
73
+ const graph = getGraph(received);
74
+ const { from } = options;
75
+ if (!(from in graph.nodes)) return {
76
+ pass: false,
77
+ message: () => invalidStateMessage(from, graph.fsmId, graph)
78
+ };
79
+ if (!(targetState in graph.nodes)) return {
80
+ pass: false,
81
+ message: () => invalidStateMessage(targetState, graph.fsmId, graph)
82
+ };
83
+ const pass = canReach(graph, from, targetState);
84
+ const message = () => {
85
+ if (pass) return `Expected FSM '${graph.fsmId}' to have no path from '${from}' to '${targetState}', but a path exists.`;
86
+ return `Expected FSM '${graph.fsmId}' to have a path from '${from}' to '${targetState}', but no path exists.`;
87
+ };
88
+ return {
89
+ pass,
90
+ message
91
+ };
92
+ };
93
+ /**
94
+ * Passes when NO path exists from `options.from` to `targetState`.
95
+ * The logical inverse of toAlwaysReach.
96
+ */
97
+ const toNeverReach = (received, targetState, options) => {
98
+ const graph = getGraph(received);
99
+ const { from } = options;
100
+ if (!(from in graph.nodes)) return {
101
+ pass: false,
102
+ message: () => invalidStateMessage(from, graph.fsmId, graph)
103
+ };
104
+ if (!(targetState in graph.nodes)) return {
105
+ pass: false,
106
+ message: () => invalidStateMessage(targetState, graph.fsmId, graph)
107
+ };
108
+ const pass = !canReach(graph, from, targetState);
109
+ const message = () => {
110
+ if (pass) return `Expected FSM '${graph.fsmId}' to have a path from '${from}' to '${targetState}', but no path exists.`;
111
+ return `Expected FSM '${graph.fsmId}' to have no path from '${from}' to '${targetState}', but a path exists.`;
112
+ };
113
+ return {
114
+ pass,
115
+ message
116
+ };
117
+ };
118
+
119
+ //#endregion
120
+ //#region src/index.ts
121
+ expect.extend({
122
+ toHaveNoUnreachableStates,
123
+ toAlwaysReach,
124
+ toNeverReach
125
+ });
126
+
127
+ //#endregion
128
+ export { };
129
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/reachability.ts","../src/matchers.ts","../src/index.ts"],"sourcesContent":["// =============================================================================\n// reachability.ts — BFS reachability check for StateGraph\n//\n// Top-level graph only. Child graphs are not traversed here — users who want\n// to assert reachability inside a child FSM pass the child instance directly\n// to expect().\n//\n// Both \"definite\" and \"possible\" edges are followed. The question is whether\n// the plumbing exists, not whether runtime conditions will always cause it to\n// execute. See design decisions in the build plan for rationale.\n// =============================================================================\n\nimport type { StateGraph } from \"machina-inspect\";\n\n/**\n * Returns true if `to` is reachable from `from` via BFS over the\n * top-level graph's edges. Returns false if either state is missing\n * from the graph, or if no path exists.\n *\n * Treats `from === to` as trivially reachable (every state reaches itself).\n */\nconst canReach = (graph: StateGraph, from: string, to: string): boolean => {\n // Trivially reachable — a state always reaches itself\n if (from === to) {\n return true;\n }\n\n // If the source state isn't in the graph, there's nothing to walk\n if (!(from in graph.nodes)) {\n return false;\n }\n\n const visited = new Set<string>();\n const queue: string[] = [from];\n visited.add(from);\n\n while (queue.length > 0) {\n const current = queue.shift()!;\n const node = graph.nodes[current];\n\n // Missing node is a safety check — shouldn't happen in a well-formed graph\n if (!node) {\n continue;\n }\n\n for (const edge of node.edges) {\n if (edge.to === to) {\n return true;\n }\n if (!visited.has(edge.to) && edge.to in graph.nodes) {\n visited.add(edge.to);\n queue.push(edge.to);\n }\n }\n }\n\n return false;\n};\n\nexport { canReach };\n","// =============================================================================\n// matchers.ts — Custom matcher implementations for machina FSMs\n//\n// Each matcher follows the expect.extend() contract:\n// - Receives the subject as `received`\n// - Returns { pass: boolean, message: () => string }\n// - `message` is always the FAILURE message — jest/vitest call it only\n// when the assertion fails (accounting for .not inversion)\n//\n// Invalid state names produce matcher failures, not throws, so they appear\n// as clean test failures in the runner rather than unhandled exceptions.\n// =============================================================================\n\nimport { buildStateGraph, inspectGraph, type StateGraph } from \"machina-inspect\";\nimport type { MachinaInstance } from \"machina\";\nimport { canReach } from \"./reachability\";\nimport type { ReachabilityOptions } from \"./types\";\n\n// ---------------------------------------------------------------------------\n// Internal helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Build the StateGraph for the received FSM instance.\n * `buildStateGraph` accepts both config objects and live instances;\n * here we always receive live instances (users pass `expect(fsm)`).\n */\nconst getGraph = (received: MachinaInstance): StateGraph => {\n return buildStateGraph(received);\n};\n\n/**\n * Produce the \"invalid state\" failure message. Used when a state name\n * passed to a matcher does not exist in the graph.\n */\nconst invalidStateMessage = (stateName: string, fsmId: string, graph: StateGraph): string => {\n const available = Object.keys(graph.nodes).join(\", \");\n return `State '${stateName}' does not exist in FSM '${fsmId}'. Available states: ${available}.`;\n};\n\n// ---------------------------------------------------------------------------\n// Matcher: toHaveNoUnreachableStates\n// ---------------------------------------------------------------------------\n\n/**\n * Passes when the FSM has no states that are unreachable from initialState.\n * Uses inspectGraph() to run all checks and filters for unreachable-state\n * findings, which includes recursive child graph analysis.\n */\nconst toHaveNoUnreachableStates = (received: MachinaInstance): jest.CustomMatcherResult => {\n const graph = getGraph(received);\n const findings = inspectGraph(graph).filter(f => f.type === \"unreachable-state\");\n\n const pass = findings.length === 0;\n\n const message = (): string => {\n if (pass) {\n // Negated assertion failed: .not.toHaveNoUnreachableStates() but all states reachable\n return `Expected FSM '${graph.fsmId}' to have unreachable states, but all states are reachable.`;\n }\n const stateList = findings.map(f => `'${f.states.join(\", \")}'`).join(\", \");\n return `Expected FSM '${graph.fsmId}' to have no unreachable states, but found: ${stateList}.`;\n };\n\n return { pass, message };\n};\n\n// ---------------------------------------------------------------------------\n// Matcher: toAlwaysReach\n// ---------------------------------------------------------------------------\n\n/**\n * Passes when a path exists from `options.from` to `targetState` in the\n * FSM's top-level graph (BFS over all edges, definite and possible).\n */\nconst toAlwaysReach = (\n received: MachinaInstance,\n targetState: string,\n options: ReachabilityOptions\n): jest.CustomMatcherResult => {\n const graph = getGraph(received);\n const { from } = options;\n\n // Invalid state names are reported as matcher failures for clean output\n if (!(from in graph.nodes)) {\n return {\n pass: false,\n message: () => invalidStateMessage(from, graph.fsmId, graph),\n };\n }\n\n if (!(targetState in graph.nodes)) {\n return {\n pass: false,\n message: () => invalidStateMessage(targetState, graph.fsmId, graph),\n };\n }\n\n const pass = canReach(graph, from, targetState);\n\n const message = (): string => {\n if (pass) {\n // Negated assertion failed: .not.toAlwaysReach but path exists\n return `Expected FSM '${graph.fsmId}' to have no path from '${from}' to '${targetState}', but a path exists.`;\n }\n return `Expected FSM '${graph.fsmId}' to have a path from '${from}' to '${targetState}', but no path exists.`;\n };\n\n return { pass, message };\n};\n\n// ---------------------------------------------------------------------------\n// Matcher: toNeverReach\n// ---------------------------------------------------------------------------\n\n/**\n * Passes when NO path exists from `options.from` to `targetState`.\n * The logical inverse of toAlwaysReach.\n */\nconst toNeverReach = (\n received: MachinaInstance,\n targetState: string,\n options: ReachabilityOptions\n): jest.CustomMatcherResult => {\n const graph = getGraph(received);\n const { from } = options;\n\n // Invalid state names are reported as matcher failures for clean output\n if (!(from in graph.nodes)) {\n return {\n pass: false,\n message: () => invalidStateMessage(from, graph.fsmId, graph),\n };\n }\n\n if (!(targetState in graph.nodes)) {\n return {\n pass: false,\n message: () => invalidStateMessage(targetState, graph.fsmId, graph),\n };\n }\n\n // pass: true means \"no path exists\" (the assertion that toNeverReach checks)\n const pass = !canReach(graph, from, targetState);\n\n const message = (): string => {\n if (pass) {\n // Negated assertion failed: .not.toNeverReach but no path exists\n return `Expected FSM '${graph.fsmId}' to have a path from '${from}' to '${targetState}', but no path exists.`;\n }\n return `Expected FSM '${graph.fsmId}' to have no path from '${from}' to '${targetState}', but a path exists.`;\n };\n\n return { pass, message };\n};\n\nexport { toHaveNoUnreachableStates, toAlwaysReach, toNeverReach };\n","// =============================================================================\n// index.ts — Side-effect entry point for machina-test\n//\n// Importing this module registers all three custom matchers with the global\n// `expect` provided by Jest or Vitest. This is the same pattern used by\n// @testing-library/jest-dom — the import IS the side effect.\n//\n// `expect` must be available as a global at the time this module is evaluated.\n// Put the import in a test file or in a setup file that runs after the test\n// framework's globals are injected.\n// =============================================================================\n\nimport { toHaveNoUnreachableStates, toAlwaysReach, toNeverReach } from \"./matchers\";\n\n// Register with the host test runner's global expect\nexpect.extend({ toHaveNoUnreachableStates, toAlwaysReach, toNeverReach });\n\n// Re-export the options type so consumers can import it alongside the package\nexport type { ReachabilityOptions } from \"./types\";\n"],"mappings":";;;;;;;;;;AAqBA,MAAM,YAAY,OAAmB,MAAc,OAAwB;AAEvE,KAAI,SAAS,GACT,QAAO;AAIX,KAAI,EAAE,QAAQ,MAAM,OAChB,QAAO;CAGX,MAAM,0BAAU,IAAI,KAAa;CACjC,MAAM,QAAkB,CAAC,KAAK;AAC9B,SAAQ,IAAI,KAAK;AAEjB,QAAO,MAAM,SAAS,GAAG;EACrB,MAAM,UAAU,MAAM,OAAO;EAC7B,MAAM,OAAO,MAAM,MAAM;AAGzB,MAAI,CAAC,KACD;AAGJ,OAAK,MAAM,QAAQ,KAAK,OAAO;AAC3B,OAAI,KAAK,OAAO,GACZ,QAAO;AAEX,OAAI,CAAC,QAAQ,IAAI,KAAK,GAAG,IAAI,KAAK,MAAM,MAAM,OAAO;AACjD,YAAQ,IAAI,KAAK,GAAG;AACpB,UAAM,KAAK,KAAK,GAAG;;;;AAK/B,QAAO;;;;;;;;;;AC7BX,MAAM,YAAY,aAA0C;AACxD,QAAO,gBAAgB,SAAS;;;;;;AAOpC,MAAM,uBAAuB,WAAmB,OAAe,UAA8B;AAEzF,QAAO,UAAU,UAAU,2BAA2B,MAAM,uBAD1C,OAAO,KAAK,MAAM,MAAM,CAAC,KAAK,KAAK,CACwC;;;;;;;AAYjG,MAAM,6BAA6B,aAAwD;CACvF,MAAM,QAAQ,SAAS,SAAS;CAChC,MAAM,WAAW,aAAa,MAAM,CAAC,QAAO,MAAK,EAAE,SAAS,oBAAoB;CAEhF,MAAM,OAAO,SAAS,WAAW;CAEjC,MAAM,gBAAwB;AAC1B,MAAI,KAEA,QAAO,iBAAiB,MAAM,MAAM;EAExC,MAAM,YAAY,SAAS,KAAI,MAAK,IAAI,EAAE,OAAO,KAAK,KAAK,CAAC,GAAG,CAAC,KAAK,KAAK;AAC1E,SAAO,iBAAiB,MAAM,MAAM,8CAA8C,UAAU;;AAGhG,QAAO;EAAE;EAAM;EAAS;;;;;;AAW5B,MAAM,iBACF,UACA,aACA,YAC2B;CAC3B,MAAM,QAAQ,SAAS,SAAS;CAChC,MAAM,EAAE,SAAS;AAGjB,KAAI,EAAE,QAAQ,MAAM,OAChB,QAAO;EACH,MAAM;EACN,eAAe,oBAAoB,MAAM,MAAM,OAAO,MAAM;EAC/D;AAGL,KAAI,EAAE,eAAe,MAAM,OACvB,QAAO;EACH,MAAM;EACN,eAAe,oBAAoB,aAAa,MAAM,OAAO,MAAM;EACtE;CAGL,MAAM,OAAO,SAAS,OAAO,MAAM,YAAY;CAE/C,MAAM,gBAAwB;AAC1B,MAAI,KAEA,QAAO,iBAAiB,MAAM,MAAM,0BAA0B,KAAK,QAAQ,YAAY;AAE3F,SAAO,iBAAiB,MAAM,MAAM,yBAAyB,KAAK,QAAQ,YAAY;;AAG1F,QAAO;EAAE;EAAM;EAAS;;;;;;AAW5B,MAAM,gBACF,UACA,aACA,YAC2B;CAC3B,MAAM,QAAQ,SAAS,SAAS;CAChC,MAAM,EAAE,SAAS;AAGjB,KAAI,EAAE,QAAQ,MAAM,OAChB,QAAO;EACH,MAAM;EACN,eAAe,oBAAoB,MAAM,MAAM,OAAO,MAAM;EAC/D;AAGL,KAAI,EAAE,eAAe,MAAM,OACvB,QAAO;EACH,MAAM;EACN,eAAe,oBAAoB,aAAa,MAAM,OAAO,MAAM;EACtE;CAIL,MAAM,OAAO,CAAC,SAAS,OAAO,MAAM,YAAY;CAEhD,MAAM,gBAAwB;AAC1B,MAAI,KAEA,QAAO,iBAAiB,MAAM,MAAM,yBAAyB,KAAK,QAAQ,YAAY;AAE1F,SAAO,iBAAiB,MAAM,MAAM,0BAA0B,KAAK,QAAQ,YAAY;;AAG3F,QAAO;EAAE;EAAM;EAAS;;;;;AC1I5B,OAAO,OAAO;CAAE;CAA2B;CAAe;CAAc,CAAC"}
package/package.json ADDED
@@ -0,0 +1,78 @@
1
+ {
2
+ "name": "machina-test",
3
+ "version": "0.2.0",
4
+ "description": "Jest/Vitest custom matchers for testing machina FSMs.",
5
+ "author": "Jim Cowart (http://ifandelse.com)",
6
+ "license": "MIT",
7
+ "homepage": "https://machina-js.org/",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git://github.com/ifandelse/machina.js.git"
11
+ },
12
+ "bugs": {
13
+ "url": "http://github.com/ifandelse/machina.js/issues"
14
+ },
15
+ "publishConfig": {
16
+ "access": "public"
17
+ },
18
+ "licenses": [
19
+ {
20
+ "type": "MIT",
21
+ "url": "http://opensource.org/licenses/MIT"
22
+ },
23
+ {
24
+ "type": "GPL",
25
+ "url": "http://opensource.org/licenses/GPL-2.0"
26
+ }
27
+ ],
28
+ "main": "dist/index.cjs",
29
+ "module": "dist/index.mjs",
30
+ "types": "dist/index.d.mts",
31
+ "exports": {
32
+ ".": {
33
+ "types": "./dist/index.d.mts",
34
+ "import": "./dist/index.mjs",
35
+ "require": "./dist/index.cjs"
36
+ }
37
+ },
38
+ "files": [
39
+ "dist",
40
+ "README.md"
41
+ ],
42
+ "engines": {
43
+ "node": ">=22.22"
44
+ },
45
+ "sideEffects": true,
46
+ "dependencies": {
47
+ "machina-inspect": "1.0.0"
48
+ },
49
+ "peerDependencies": {
50
+ "machina": ">=6.1.0",
51
+ "jest": ">=29",
52
+ "vitest": ">=1"
53
+ },
54
+ "peerDependenciesMeta": {
55
+ "jest": {
56
+ "optional": true
57
+ },
58
+ "vitest": {
59
+ "optional": true
60
+ }
61
+ },
62
+ "devDependencies": {
63
+ "@types/jest": "^30.0.0",
64
+ "eslint": "^9.39.2",
65
+ "jest": "^30.2.0",
66
+ "ts-jest": "^29.4.6",
67
+ "tsdown": "^0.20.3",
68
+ "typescript": "^5.9.3",
69
+ "machina": "6.1.0"
70
+ },
71
+ "scripts": {
72
+ "dev": "tsdown --watch",
73
+ "build": "tsdown",
74
+ "test": "jest --passWithNoTests",
75
+ "test:watch": "jest --watch",
76
+ "lint": "ESLINT_USE_FLAT_CONFIG=false eslint --config ../../.eslintrc.js 'src/**/*.{ts,tsx}'"
77
+ }
78
+ }