xstate 3.3.2 → 3.3.3

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/scxml.ts CHANGED
@@ -62,26 +62,54 @@ function stateNodeToSCXML(stateNode: StateNode) {
62
62
  return stateNodeToSCXML(subStateNode);
63
63
  }),
64
64
  ...Object.keys(stateNode.on)
65
- .map((event): XMLElement[] => {
66
- const transition = stateNode.on![event];
65
+ .map(
66
+ (event): XMLElement[] => {
67
+ const transition = stateNode.on![event];
67
68
 
68
- if (!transition) {
69
- return [];
70
- }
69
+ if (!transition) {
70
+ return [];
71
+ }
72
+
73
+ if (Array.isArray(transition)) {
74
+ return transition.map(targetTransition => {
75
+ return {
76
+ type: 'element',
77
+ name: 'transition',
78
+ attributes: {
79
+ ...(event ? { event } : undefined),
80
+ target: stateNode.parent!.getRelativeStateNodes(
81
+ targetTransition.target
82
+ )[0]!.id, // TODO: fixme
83
+ ...(targetTransition.cond
84
+ ? { cond: targetTransition.cond.toString() }
85
+ : undefined)
86
+ },
87
+ elements: targetTransition.actions
88
+ ? targetTransition.actions.map(action => ({
89
+ type: 'element',
90
+ name: 'send',
91
+ attributes: {
92
+ event: getActionType(action)
93
+ }
94
+ }))
95
+ : undefined
96
+ };
97
+ });
98
+ }
99
+
100
+ return Object.keys(transition).map(target => {
101
+ const targetTransition = transition[target];
71
102
 
72
- if (Array.isArray(transition)) {
73
- return transition.map(targetTransition => {
74
103
  return {
75
104
  type: 'element',
76
105
  name: 'transition',
77
106
  attributes: {
78
- ...event ? { event } : undefined,
79
- target: stateNode.parent!.getRelativeStateNodes(
80
- targetTransition.target
81
- )[0]!.id, // TODO: fixme
82
- ...targetTransition.cond
107
+ ...(event ? { event } : undefined),
108
+ target: stateNode.parent!.getRelativeStateNodes(target)![0]
109
+ .id, // TODO: fixme
110
+ ...(targetTransition.cond
83
111
  ? { cond: targetTransition.cond.toString() }
84
- : undefined
112
+ : undefined)
85
113
  },
86
114
  elements: targetTransition.actions
87
115
  ? targetTransition.actions.map(action => ({
@@ -95,32 +123,7 @@ function stateNodeToSCXML(stateNode: StateNode) {
95
123
  };
96
124
  });
97
125
  }
98
-
99
- return Object.keys(transition).map(target => {
100
- const targetTransition = transition[target];
101
-
102
- return {
103
- type: 'element',
104
- name: 'transition',
105
- attributes: {
106
- ...event ? { event } : undefined,
107
- target: stateNode.parent!.getRelativeStateNodes(target)![0].id, // TODO: fixme
108
- ...targetTransition.cond
109
- ? { cond: targetTransition.cond.toString() }
110
- : undefined
111
- },
112
- elements: targetTransition.actions
113
- ? targetTransition.actions.map(action => ({
114
- type: 'element',
115
- name: 'send',
116
- attributes: {
117
- event: getActionType(action)
118
- }
119
- }))
120
- : undefined
121
- };
122
- });
123
- })
126
+ )
124
127
  .reduce((a, b) => a.concat(b))
125
128
  ].filter(Boolean) as XMLElement[]
126
129
  };
@@ -276,12 +279,12 @@ function toConfig(
276
279
  (values: XMLElement[]) => {
277
280
  return values.map(value => ({
278
281
  target: `#${value.attributes!.target}`,
279
- ...value.elements ? executableContent(value.elements) : undefined,
280
- ...value.attributes!.cond
282
+ ...(value.elements ? executableContent(value.elements) : undefined),
283
+ ...(value.attributes!.cond
281
284
  ? {
282
285
  cond: evalCond(value.attributes!.cond as string)
283
286
  }
284
- : undefined
287
+ : undefined)
285
288
  }));
286
289
  }
287
290
  );
@@ -311,18 +314,18 @@ function toConfig(
311
314
  return {
312
315
  id,
313
316
  delimiter: options.delimiter,
314
- ...initial ? { initial } : undefined,
315
- ...parallel ? { parallel } : undefined,
316
- ...stateElements.length
317
+ ...(initial ? { initial } : undefined),
318
+ ...(parallel ? { parallel } : undefined),
319
+ ...(stateElements.length
317
320
  ? {
318
321
  states: mapValues(states, (state, key) =>
319
322
  toConfig(state, key, options)
320
323
  )
321
324
  }
322
- : undefined,
323
- ...transitionElements.length ? { on } : undefined,
324
- ...onEntry ? { onEntry } : undefined,
325
- ...onExit ? { onExit } : undefined
325
+ : undefined),
326
+ ...(transitionElements.length ? { on } : undefined),
327
+ ...(onEntry ? { onEntry } : undefined),
328
+ ...(onExit ? { onExit } : undefined)
326
329
  };
327
330
  }
328
331
 
package/src/types.ts CHANGED
@@ -25,6 +25,11 @@ export interface StateValueMap {
25
25
 
26
26
  export type StateValue = string | StateValueMap;
27
27
 
28
+ export interface HistoryValue {
29
+ states: Record<string, HistoryValue | undefined>;
30
+ current: StateValue | undefined;
31
+ }
32
+
28
33
  export type ConditionPredicate = (
29
34
  extendedState: any,
30
35
  event: EventObject,
@@ -112,7 +117,6 @@ export interface StandardMachineConfig extends MachineConfig {
112
117
  export interface ParallelMachineConfig extends MachineConfig {
113
118
  initial?: undefined;
114
119
  parallel: true;
115
- states: Record<string, CompoundStateNodeConfig>;
116
120
  }
117
121
 
118
122
  export interface EntryExitEffectMap {
@@ -190,15 +194,8 @@ export type MaybeStateValueActionsTuple = [
190
194
  ActivityMap | undefined
191
195
  ];
192
196
 
193
- export interface StateTransition {
194
- statePaths: string[][];
195
- actions: ActionMap;
196
- activities: ActivityMap | undefined;
197
- events: EventObject[];
198
- }
199
-
200
197
  // tslint:disable-next-line:class-name
201
- export interface _StateTransition {
198
+ export interface StateTransition {
202
199
  value: StateValue | undefined;
203
200
  entryExitStates: EntryExitStates | undefined;
204
201
  actions: Action[];
package/src/utils.ts CHANGED
@@ -16,7 +16,9 @@ export function getActionType(action: Action): ActionType {
16
16
  try {
17
17
  return typeof action === 'string' || typeof action === 'number'
18
18
  ? `${action}`
19
- : typeof action === 'function' ? action.name : action.type;
19
+ : typeof action === 'function'
20
+ ? action.name
21
+ : action.type;
20
22
  } catch (e) {
21
23
  throw new Error(
22
24
  'Events must be strings or objects with a string event.type property.'
@@ -113,18 +115,37 @@ export function mapFilterValues<T, P>(
113
115
  * Retrieves a value at the given path.
114
116
  * @param props The deep path to the prop of the desired value
115
117
  */
116
- export const path = (props: string[]): any => <T extends Record<string, any>>(
118
+ export const path = <T extends Record<string, any>>(props: string[]): any => (
117
119
  object: T
118
120
  ): any => {
119
- let result: Record<string, any> = object;
121
+ let result: T = object;
120
122
 
121
123
  for (const prop of props) {
122
- result = result[prop];
124
+ result = result[prop as keyof typeof result];
123
125
  }
124
126
 
125
127
  return result;
126
128
  };
127
129
 
130
+ /**
131
+ * Retrieves a value at the given path via the nested accessor prop.
132
+ * @param props The deep path to the prop of the desired value
133
+ */
134
+ export function nestedPath<T extends Record<string, any>>(
135
+ props: string[],
136
+ accessorProp: keyof T
137
+ ): (object: T) => T {
138
+ return object => {
139
+ let result: T = object;
140
+
141
+ for (const prop of props) {
142
+ result = result[accessorProp][prop];
143
+ }
144
+
145
+ return result;
146
+ };
147
+ }
148
+
128
149
  export const toStatePaths = (stateValue: StateValue): string[][] => {
129
150
  if (typeof stateValue === 'string') {
130
151
  return [[stateValue]];
@@ -203,6 +203,21 @@ describe('transient activities', () => {
203
203
  B1: '.B1',
204
204
  B2: '.B2'
205
205
  }
206
+ },
207
+ C: {
208
+ initial: 'C1',
209
+ states: {
210
+ C1: {
211
+ activities: ['C1'],
212
+ on: {
213
+ C: 'C1',
214
+ C_SIMILAR: 'C2'
215
+ }
216
+ },
217
+ C2: {
218
+ activities: ['C1'],
219
+ },
220
+ }
206
221
  }
207
222
  }
208
223
  });
@@ -223,10 +238,22 @@ describe('transient activities', () => {
223
238
  assert.deepEqual(state.activities.A, true);
224
239
  });
225
240
 
241
+ it('should have kept same activities', () => {
242
+ let state = machine.initialState;
243
+ state = machine.transition(state, 'C_SIMILAR');
244
+ assert.deepEqual(state.activities.C1, true);
245
+ });
246
+
247
+ it('should have kept same activities after self transition', () => {
248
+ let state = machine.initialState;
249
+ state = machine.transition(state, 'C');
250
+ assert.deepEqual(state.activities.C1, true);
251
+ });
252
+
226
253
  it('should have stopped after automatic transitions', () => {
227
254
  let state = machine.initialState;
228
255
  state = machine.transition(state, 'A');
229
- assert.deepEqual(state.value, { A: 'A2', B: 'B2' });
256
+ assert.deepEqual(state.value, { A: 'A2', B: 'B2', C: 'C1' });
230
257
  assert.deepEqual(state.activities.B2, true);
231
258
  });
232
259
  });
@@ -1,5 +1,5 @@
1
1
  import { assert } from 'chai';
2
- import { Machine } from '../src/index';
2
+ import { Machine } from '../src/StateNode';
3
3
 
4
4
  describe('deterministic machine', () => {
5
5
  const pedestrianStates = {
@@ -301,3 +301,142 @@ describe('parallel history states', () => {
301
301
  });
302
302
  });
303
303
  });
304
+
305
+ describe('transient history', () => {
306
+ const transientMachine = Machine({
307
+ initial: 'A',
308
+ parallel: false,
309
+ states: {
310
+ A: {
311
+ on: { EVENT: 'B' }
312
+ },
313
+ B: {
314
+ on: {
315
+ // eventless transition
316
+ '': 'C'
317
+ }
318
+ },
319
+ C: {}
320
+ }
321
+ });
322
+ it('should have history on transient transitions', () => {
323
+ const nextState = transientMachine.transition('A', 'EVENT');
324
+ assert.equal(nextState.value, 'C');
325
+ assert.isDefined(nextState.history);
326
+ });
327
+ });
328
+
329
+ describe('internal transition with history', () => {
330
+ const machine = Machine({
331
+ key: 'test',
332
+ initial: 'first',
333
+ states: {
334
+ first: {
335
+ initial: 'foo',
336
+ states: {
337
+ foo: {}
338
+ },
339
+ on: {
340
+ NEXT: 'second.other'
341
+ }
342
+ },
343
+ second: {
344
+ initial: 'nested',
345
+ states: {
346
+ nested: {},
347
+ other: {},
348
+ hist: {
349
+ history: true
350
+ }
351
+ },
352
+ on: {
353
+ NEXT: [
354
+ {
355
+ target: '.hist'
356
+ }
357
+ ]
358
+ }
359
+ }
360
+ }
361
+ });
362
+
363
+ it('should transition internally to the most recently visited state', () => {
364
+ // {
365
+ // $current: 'first',
366
+ // first: undefined,
367
+ // second: {
368
+ // $current: 'nested',
369
+ // nested: undefined,
370
+ // other: undefined
371
+ // }
372
+ // }
373
+ const state2 = machine.transition(machine.initialState, 'NEXT');
374
+ // {
375
+ // $current: 'second',
376
+ // first: undefined,
377
+ // second: {
378
+ // $current: 'other',
379
+ // nested: undefined,
380
+ // other: undefined
381
+ // }
382
+ // }
383
+ const state3 = machine.transition(state2, 'NEXT');
384
+ // {
385
+ // $current: 'second',
386
+ // first: undefined,
387
+ // second: {
388
+ // $current: 'other',
389
+ // nested: undefined,
390
+ // other: undefined
391
+ // }
392
+ // }
393
+
394
+ assert.deepEqual(state3.value, { second: 'other' });
395
+ });
396
+ });
397
+
398
+ describe('multistage history states', () => {
399
+ const pcWithTurboButtonMachine = Machine({
400
+ key: 'pc-with-turbo-button',
401
+ initial: 'off',
402
+ states: {
403
+ off: {
404
+ on: { POWER: 'starting' }
405
+ },
406
+ starting: {
407
+ on: { STARTED: 'running.H' }
408
+ },
409
+ running: {
410
+ initial: 'normal',
411
+ states: {
412
+ normal: {
413
+ on: { SWITCH_TURBO: 'turbo' }
414
+ },
415
+ turbo: {
416
+ on: { SWITCH_TURBO: 'normal' }
417
+ },
418
+ H: {
419
+ history: true
420
+ }
421
+ },
422
+ on: {
423
+ POWER: 'off'
424
+ }
425
+ }
426
+ }
427
+ });
428
+
429
+ it('should go to the most recently visited state', () => {
430
+ const onTurboState = pcWithTurboButtonMachine.transition(
431
+ 'running',
432
+ 'SWITCH_TURBO'
433
+ );
434
+ const offState = pcWithTurboButtonMachine.transition(onTurboState, 'POWER');
435
+ const loadingState = pcWithTurboButtonMachine.transition(offState, 'POWER');
436
+
437
+ assert.equal(
438
+ pcWithTurboButtonMachine.transition(loadingState, 'STARTED').toString(),
439
+ 'running.turbo'
440
+ );
441
+ });
442
+ });
@@ -0,0 +1,48 @@
1
+ import { assert } from 'chai';
2
+ import { Machine } from '../src/index';
3
+
4
+ const machine = Machine({
5
+ parallel: true,
6
+ states: {
7
+ A: {
8
+ initial: 'A1',
9
+ states: {
10
+ A1: {},
11
+ A2: {}
12
+ }
13
+ },
14
+ B: {
15
+ initial: 'B1',
16
+ states: {
17
+ B1: {},
18
+ B2: {}
19
+ }
20
+ }
21
+ }
22
+ });
23
+
24
+ describe('invalid states', () => {
25
+ xit('should reject transitioning from a String state', () => {
26
+ assert.throws(() => machine.transition('A', 'E'));
27
+ });
28
+
29
+ xit('should reject transitioning from empty states', () => {
30
+ assert.throws(() => machine.transition({ A: {}, B: {} }, 'E'));
31
+ });
32
+
33
+ it('should allow transitioning from valid states', () => {
34
+ machine.transition({ A: 'A1', B: 'B1' }, 'E');
35
+ });
36
+
37
+ it('should reject transitioning from bad state configs', () => {
38
+ assert.throws(() => machine.transition({ A: 'A3', B: 'B3' }, 'E'));
39
+ });
40
+
41
+ xit('should reject transitioning from partially valid states', () => {
42
+ assert.throws(() => machine.transition({ A: 'A1' }, 'E'));
43
+ });
44
+
45
+ xit("should reject transitioning from regions that don't exist", () => {
46
+ assert.throws(() => machine.transition({ A: 'A1', B: 'B1', Z: 'Z1' }, 'E'));
47
+ });
48
+ });