@xstate-devtools/adapter 0.1.3 → 0.1.4

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/src/core.test.ts DELETED
@@ -1,287 +0,0 @@
1
- import { describe, expect, it, vi } from 'vitest'
2
- import { createActor, createMachine } from 'xstate'
3
- import { createInspector, getSourceLocationFromStack, type Transport } from './core.js'
4
-
5
- describe('createInspector', () => {
6
- it('sanitizes outbound inspection events before sending them to the transport', () => {
7
- const sent: unknown[] = []
8
- const transport: Transport = {
9
- send(message) {
10
- sent.push(message)
11
- },
12
- subscribe() {
13
- return () => {}
14
- },
15
- }
16
-
17
- const inspector = createInspector(transport, 'srv')
18
- const snapshot = { value: 'idle', context: {}, status: 'active' }
19
- const actorRef = {
20
- sessionId: 'actor-1',
21
- getSnapshot: vi.fn(() => snapshot),
22
- }
23
-
24
- const event: Record<string, unknown> = { type: 'route.changed' }
25
- event.self = event
26
- event.handler = function routeHandler() {}
27
-
28
- inspector.inspect({
29
- type: '@xstate.event',
30
- actorRef,
31
- event,
32
- })
33
-
34
- const message = sent.find(
35
- (candidate): candidate is { type: string; event: Record<string, unknown> } =>
36
- typeof candidate === 'object' &&
37
- candidate !== null &&
38
- 'type' in candidate &&
39
- (candidate as { type?: string }).type === 'XSTATE_EVENT',
40
- )
41
-
42
- expect(message).toBeDefined()
43
- expect(message?.event.type).toBe('route.changed')
44
- expect(message?.event.handler).toBe('[Function: routeHandler]')
45
- expect(message?.event.self).not.toBe(event)
46
- })
47
-
48
- it('sets a selected state node as active for machine actors', () => {
49
- const sent: unknown[] = []
50
- let handler: ((message: Parameters<Transport['subscribe']>[0]) => void) | undefined
51
- const transport: Transport = {
52
- send(message) {
53
- sent.push(message)
54
- },
55
- subscribe(callback) {
56
- handler = callback
57
- return () => {}
58
- },
59
- }
60
-
61
- const inspector = createInspector(transport, 'srv')
62
- const actor = createActor(
63
- createMachine({
64
- id: 'traffic',
65
- initial: 'green',
66
- states: {
67
- green: {},
68
- yellow: {},
69
- red: {},
70
- },
71
- }),
72
- )
73
-
74
- actor.start()
75
- inspector.inspect({ type: '@xstate.actor', actorRef: actor })
76
-
77
- handler?.({
78
- type: 'XSTATE_SET_ACTIVE_STATE',
79
- sessionId: `srv:${actor.sessionId}`,
80
- stateNodeId: 'traffic.red',
81
- })
82
-
83
- expect(actor.getSnapshot().value).toBe('red')
84
-
85
- const snapshotMessage = sent.find(
86
- (candidate): candidate is { type: string; snapshot: { value: unknown }; sessionId: string } =>
87
- typeof candidate === 'object' &&
88
- candidate !== null &&
89
- 'type' in candidate &&
90
- (candidate as { type?: string }).type === 'XSTATE_SNAPSHOT' &&
91
- 'snapshot' in candidate,
92
- )
93
- expect(snapshotMessage).toBeDefined()
94
- expect(snapshotMessage?.sessionId).toBe(`srv:${actor.sessionId}`)
95
- expect(snapshotMessage?.snapshot.value).toBe('red')
96
- })
97
- })
98
-
99
- describe('getSourceLocationFromStack', () => {
100
- it('skips anonymous and node internal frames and returns filesystem frame', () => {
101
- const stack = [
102
- 'Error',
103
- ' at getSourceLocation (packages/adapter/src/core.ts:10:1)',
104
- ' at inspect (packages/adapter/src/core.ts:20:1)',
105
- ' at Map.forEach (<anonymous>)',
106
- ' at WebSocket.emit (node:events:508:28)',
107
- ' at createMachine (/Users/me/project/app/machine.ts:12:3)',
108
- ].join('\n')
109
-
110
- expect(getSourceLocationFromStack(stack)).toBe(
111
- 'createMachine (/Users/me/project/app/machine.ts:12:3)',
112
- )
113
- })
114
-
115
- it('accepts vite /@fs/ urls and ignores plain browser urls', () => {
116
- const stack = [
117
- 'Error',
118
- ' at getSourceLocation (packages/adapter/src/core.ts:10:1)',
119
- ' at inspect (packages/adapter/src/core.ts:20:1)',
120
- ' at createMachine (http://localhost:5173/app/machines/auth.machine.ts:12:3)',
121
- ' at createMachine (http://localhost:5173/@fs/Users/me/project/app/machine.ts:12:3)',
122
- ].join('\n')
123
-
124
- expect(getSourceLocationFromStack(stack)).toBe(
125
- 'createMachine (http://localhost:5173/@fs/Users/me/project/app/machine.ts:12:3)',
126
- )
127
- })
128
-
129
- it('maps plain browser app urls when a web source root is configured', () => {
130
- const stack = [
131
- 'Error',
132
- ' at getSourceLocation (packages/adapter/src/core.ts:10:1)',
133
- ' at inspect (packages/adapter/src/core.ts:20:1)',
134
- ' at createMachine (http://localhost:5273/app/machines/auth.machine.ts:12:3)',
135
- ].join('\n')
136
-
137
- expect(
138
- getSourceLocationFromStack(stack, 'web', {
139
- webSourceRoot: '/Users/me/project/packages/example-remix',
140
- }),
141
- ).toBe(
142
- 'createMachine (/Users/me/project/packages/example-remix/app/machines/auth.machine.ts:12:3)',
143
- )
144
- })
145
-
146
- it('returns undefined when no filesystem-backed frame exists', () => {
147
- const stack = [
148
- 'Error',
149
- ' at getSourceLocation (packages/adapter/src/core.ts:10:1)',
150
- ' at inspect (packages/adapter/src/core.ts:20:1)',
151
- ' at Map.forEach (<anonymous>)',
152
- ' at WebSocket.emit (node:events:508:28)',
153
- ].join('\n')
154
-
155
- expect(getSourceLocationFromStack(stack)).toBeUndefined()
156
- })
157
-
158
- // --- Investigation tests for the "source link not appearing" bug ---
159
- // These tests simulate the realistic Vite/React/XState browser stack to
160
- // pinpoint why sourceLocation is undefined when inspecting machines in the
161
- // example app.
162
-
163
- it('skips vite pre-bundled xstate deps and finds user component frame', () => {
164
- // In Vite dev mode, XState and @xstate/react are pre-bundled and served at
165
- // /node_modules/.vite/deps/*.js, NOT at /node_modules/xstate/ or
166
- // /node_modules/@xstate/. isLibraryStackFrame misses these, but they must
167
- // still be skipped (via hasFilesystemBackedPath returning false).
168
- const stack = [
169
- 'Error',
170
- ' at getSourceLocation (http://localhost:5273/@fs/Users/me/xstate-devtools/packages/adapter/src/core.ts:225:21)',
171
- ' at inspect (http://localhost:5273/@fs/Users/me/xstate-devtools/packages/adapter/src/core.ts:576:47)',
172
- ' at Actor._sendInspectionEvent (http://localhost:5273/node_modules/.vite/deps/xstate.js:123:45)',
173
- ' at new Actor (http://localhost:5273/node_modules/.vite/deps/xstate.js:234:12)',
174
- ' at createActor (http://localhost:5273/node_modules/.vite/deps/xstate.js:345:10)',
175
- ' at useIdleActorRef (http://localhost:5273/node_modules/.vite/deps/@xstate_react.js:67:23)',
176
- ' at useMachine (http://localhost:5273/node_modules/.vite/deps/@xstate_react.js:207:10)',
177
- ' at MediaPlayer (http://localhost:5273/app/components/MediaPlayer.tsx:6:43)',
178
- ' at renderWithHooks (http://localhost:5273/node_modules/.vite/deps/react-dom_client.js:456:22)',
179
- ].join('\n')
180
-
181
- expect(
182
- getSourceLocationFromStack(stack, 'web', {
183
- webSourceRoot: '/Users/me/xstate-devtools/packages/example-remix',
184
- }),
185
- ).toBe(
186
- 'MediaPlayer (/Users/me/xstate-devtools/packages/example-remix/app/components/MediaPlayer.tsx:6:43)',
187
- )
188
- })
189
-
190
- it('returns undefined for vite pre-bundled stack without webSourceRoot', () => {
191
- // Without webSourceRoot, /app/ URLs cannot be remapped to filesystem paths,
192
- // so sourceLocation should be undefined and the source link hidden.
193
- const stack = [
194
- 'Error',
195
- ' at getSourceLocation (http://localhost:5273/@fs/Users/me/xstate-devtools/packages/adapter/src/core.ts:225:21)',
196
- ' at inspect (http://localhost:5273/@fs/Users/me/xstate-devtools/packages/adapter/src/core.ts:576:47)',
197
- ' at new Actor (http://localhost:5273/node_modules/.vite/deps/xstate.js:234:12)',
198
- ' at useMachine (http://localhost:5273/node_modules/.vite/deps/@xstate_react.js:207:10)',
199
- ' at MediaPlayer (http://localhost:5273/app/components/MediaPlayer.tsx:6:43)',
200
- ].join('\n')
201
-
202
- // No webSourceRoot: plain /app/ browser URLs have no filesystem mapping.
203
- expect(getSourceLocationFromStack(stack, 'web')).toBeUndefined()
204
- })
205
-
206
- it('captures real Node.js stack from within a createActor inspect callback', () => {
207
- // This test runs in Node.js. It verifies:
208
- // 1. @xstate.actor fires synchronously during createActor (user code IS on stack)
209
- // 2. getSourceLocationFromStack finds a frame — but in this test environment,
210
- // the test file itself is inside packages/adapter/ so it's filtered out by
211
- // isLibraryStackFrame. The Vitest runner frame (node_modules/@vitest) is
212
- // returned instead, which is the first "non-library" filesystem-backed frame.
213
- // This is expected here; in production the first non-library frame is user
214
- // component code (e.g. app/components/MediaPlayer.tsx).
215
- const machine = createMachine({ id: 'src-test', initial: 'idle', states: { idle: {} } })
216
-
217
- let capturedStack: string | undefined
218
-
219
- // createActor synchronously fires @xstate.actor in the Actor constructor.
220
- createActor(machine, {
221
- inspect(event) {
222
- if (event.type === '@xstate.actor') {
223
- capturedStack = new Error().stack
224
- }
225
- },
226
- })
227
-
228
- expect(capturedStack).toBeDefined()
229
-
230
- // Confirm @xstate.actor fires synchronously: capturedStack is set immediately.
231
- expect(capturedStack).toMatch(/@xstate\.actor|Actor|createActor/)
232
-
233
- const location = getSourceLocationFromStack(capturedStack, 'srv')
234
-
235
- // In this test env, the test file is filtered (it's in /packages/adapter/).
236
- // The function returns the Vitest runner frame as the first "non-library" frame.
237
- // This exposes a real limitation: the /packages/adapter/ filter also catches
238
- // test files. In a real browser, user component files at /app/ are not filtered.
239
- expect(location).toBeDefined()
240
- })
241
-
242
- it('shows that the test file itself is filtered by isLibraryStackFrame', () => {
243
- // This is an explicit documentation of the limitation: any frame inside
244
- // packages/adapter/ is treated as a library frame and skipped. This is
245
- // correct in production but means test-side assertions about "user code"
246
- // in these tests will see Vitest runner frames instead.
247
- const testFilePath = '/Users/me/xstate-devtools/packages/adapter/src/core.test.ts'
248
- const fakeStack = [
249
- 'Error',
250
- ' at getSourceLocation (/Users/me/xstate-devtools/packages/adapter/src/core.ts:225:21)',
251
- ' at inspect (/Users/me/xstate-devtools/packages/adapter/src/core.ts:576:47)',
252
- ` at it (/Users/me/xstate-devtools/packages/adapter/src/core.test.ts:200:5)`,
253
- ' at runTest (file:///Users/me/xstate-devtools/node_modules/@vitest/runner/dist/index.js:146:14)',
254
- ].join('\n')
255
-
256
- // The test file frame is filtered because it contains '/packages/adapter/'
257
- // — so the Vitest runner frame is what gets returned.
258
- const location = getSourceLocationFromStack(fakeStack, 'srv')
259
- expect(location).toMatch(/vitest/)
260
- expect(location).not.toContain(testFilePath)
261
- })
262
-
263
- it('does not show source link when @xstate.actor fires inside useEffect (deferred start)', () => {
264
- // @xstate/react calls actorRef.start() inside React.useEffect, not during
265
- // createActor. If inspection fires at start() time instead of createActor
266
- // time, the React scheduler is on the stack and user code is NOT present.
267
- // This simulates what would happen if start() (not createActor) triggered
268
- // the @xstate.actor event.
269
- const stack = [
270
- 'Error',
271
- ' at getSourceLocation (http://localhost:5273/@fs/Users/me/xstate-devtools/packages/adapter/src/core.ts:225:21)',
272
- ' at inspect (http://localhost:5273/@fs/Users/me/xstate-devtools/packages/adapter/src/core.ts:576:47)',
273
- ' at Actor.start (http://localhost:5273/node_modules/.vite/deps/xstate.js:300:8)',
274
- ' at useEffect (http://localhost:5273/node_modules/.vite/deps/@xstate_react.js:99:14)',
275
- // React scheduler — no user component frame at all
276
- ' at commitHookEffectListMount (http://localhost:5273/node_modules/.vite/deps/react-dom_client.js:22728:26)',
277
- ' at commitPassiveMountOnFiber (http://localhost:5273/node_modules/.vite/deps/react-dom_client.js:24502:13)',
278
- ].join('\n')
279
-
280
- // Without any /app/ user frame, sourceLocation should be undefined.
281
- expect(
282
- getSourceLocationFromStack(stack, 'web', {
283
- webSourceRoot: '/Users/me/xstate-devtools/packages/example-remix',
284
- }),
285
- ).toBeUndefined()
286
- })
287
- })