@servicetitan/journey 2.1.0 → 3.0.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.
Files changed (163) hide show
  1. package/README.md +1 -1
  2. package/dist/__test-utils__/test-runtime.d.ts.map +1 -1
  3. package/dist/__test-utils__/test-runtime.js +9 -2
  4. package/dist/__test-utils__/test-runtime.js.map +1 -1
  5. package/dist/__tests__/core/sanitize.test.d.ts +2 -0
  6. package/dist/__tests__/core/sanitize.test.d.ts.map +1 -0
  7. package/dist/__tests__/hardening.test.d.ts +2 -0
  8. package/dist/__tests__/hardening.test.d.ts.map +1 -0
  9. package/dist/__tests__/integrations/host-mfe.test.d.ts +2 -0
  10. package/dist/__tests__/integrations/host-mfe.test.d.ts.map +1 -0
  11. package/dist/__tests__/integrations/use-journey-step.test.d.ts +2 -0
  12. package/dist/__tests__/integrations/use-journey-step.test.d.ts.map +1 -0
  13. package/dist/config.d.ts +57 -13
  14. package/dist/config.d.ts.map +1 -1
  15. package/dist/config.js +87 -24
  16. package/dist/config.js.map +1 -1
  17. package/dist/core/abort-error.d.ts +4 -0
  18. package/dist/core/abort-error.d.ts.map +1 -0
  19. package/dist/core/abort-error.js +22 -0
  20. package/dist/core/abort-error.js.map +1 -0
  21. package/dist/core/api.d.ts +22 -9
  22. package/dist/core/api.d.ts.map +1 -1
  23. package/dist/core/api.js +77 -30
  24. package/dist/core/api.js.map +1 -1
  25. package/dist/core/endpoint-policy.d.ts +14 -4
  26. package/dist/core/endpoint-policy.d.ts.map +1 -1
  27. package/dist/core/endpoint-policy.js +32 -1
  28. package/dist/core/endpoint-policy.js.map +1 -1
  29. package/dist/core/index.d.ts +4 -3
  30. package/dist/core/index.d.ts.map +1 -1
  31. package/dist/core/index.js +3 -3
  32. package/dist/core/index.js.map +1 -1
  33. package/dist/core/limits.d.ts +29 -0
  34. package/dist/core/limits.d.ts.map +1 -0
  35. package/dist/core/limits.js +63 -0
  36. package/dist/core/limits.js.map +1 -0
  37. package/dist/core/request-policy.d.ts +39 -0
  38. package/dist/core/request-policy.d.ts.map +1 -0
  39. package/dist/core/request-policy.js +18 -0
  40. package/dist/core/request-policy.js.map +1 -0
  41. package/dist/core/runtime.d.ts +65 -15
  42. package/dist/core/runtime.d.ts.map +1 -1
  43. package/dist/core/runtime.js +403 -126
  44. package/dist/core/runtime.js.map +1 -1
  45. package/dist/core/sanitize.d.ts +23 -0
  46. package/dist/core/sanitize.d.ts.map +1 -0
  47. package/dist/core/sanitize.js +143 -0
  48. package/dist/core/sanitize.js.map +1 -0
  49. package/dist/core/step-tag.d.ts +25 -6
  50. package/dist/core/step-tag.d.ts.map +1 -1
  51. package/dist/core/step-tag.js +56 -10
  52. package/dist/core/step-tag.js.map +1 -1
  53. package/dist/core/types.d.ts +73 -25
  54. package/dist/core/types.d.ts.map +1 -1
  55. package/dist/core/types.js +4 -1
  56. package/dist/core/types.js.map +1 -1
  57. package/dist/global.d.ts +91 -13
  58. package/dist/global.d.ts.map +1 -1
  59. package/dist/global.js +235 -25
  60. package/dist/global.js.map +1 -1
  61. package/dist/index.d.ts +7 -8
  62. package/dist/index.d.ts.map +1 -1
  63. package/dist/index.js +4 -6
  64. package/dist/index.js.map +1 -1
  65. package/dist/integrations/axios.d.ts +5 -1
  66. package/dist/integrations/axios.d.ts.map +1 -1
  67. package/dist/integrations/axios.js +146 -34
  68. package/dist/integrations/axios.js.map +1 -1
  69. package/dist/integrations/fetch.d.ts +3 -2
  70. package/dist/integrations/fetch.d.ts.map +1 -1
  71. package/dist/integrations/fetch.js +105 -26
  72. package/dist/integrations/fetch.js.map +1 -1
  73. package/dist/integrations/http-report.d.ts +27 -0
  74. package/dist/integrations/http-report.d.ts.map +1 -0
  75. package/dist/integrations/http-report.js +123 -0
  76. package/dist/integrations/http-report.js.map +1 -0
  77. package/dist/integrations/jquery/index.d.ts +8 -4
  78. package/dist/integrations/jquery/index.d.ts.map +1 -1
  79. package/dist/integrations/jquery/index.js +26 -8
  80. package/dist/integrations/jquery/index.js.map +1 -1
  81. package/dist/integrations/jquery/instrument.d.ts +14 -5
  82. package/dist/integrations/jquery/instrument.d.ts.map +1 -1
  83. package/dist/integrations/jquery/instrument.js +111 -29
  84. package/dist/integrations/jquery/instrument.js.map +1 -1
  85. package/dist/integrations/react/context.d.ts +4 -1
  86. package/dist/integrations/react/context.d.ts.map +1 -1
  87. package/dist/integrations/react/context.js +4 -1
  88. package/dist/integrations/react/context.js.map +1 -1
  89. package/dist/integrations/react/scope.d.ts +14 -3
  90. package/dist/integrations/react/scope.d.ts.map +1 -1
  91. package/dist/integrations/react/scope.js +46 -8
  92. package/dist/integrations/react/scope.js.map +1 -1
  93. package/dist/integrations/react/stable-config-keys.d.ts +6 -0
  94. package/dist/integrations/react/stable-config-keys.d.ts.map +1 -0
  95. package/dist/integrations/react/stable-config-keys.js +21 -0
  96. package/dist/integrations/react/stable-config-keys.js.map +1 -0
  97. package/dist/integrations/react/use-journey-step.d.ts +11 -0
  98. package/dist/integrations/react/use-journey-step.d.ts.map +1 -0
  99. package/dist/integrations/react/use-journey-step.js +19 -0
  100. package/dist/integrations/react/use-journey-step.js.map +1 -0
  101. package/dist/integrations/request-key.d.ts +8 -3
  102. package/dist/integrations/request-key.d.ts.map +1 -1
  103. package/dist/integrations/request-key.js +10 -9
  104. package/dist/integrations/request-key.js.map +1 -1
  105. package/dist/integrations/soft-skip.d.ts +7 -0
  106. package/dist/integrations/soft-skip.d.ts.map +1 -0
  107. package/dist/integrations/soft-skip.js +30 -0
  108. package/dist/integrations/soft-skip.js.map +1 -0
  109. package/dist/sinks/console.d.ts.map +1 -1
  110. package/dist/sinks/console.js +2 -1
  111. package/dist/sinks/console.js.map +1 -1
  112. package/dist/sinks/datadog.d.ts +1 -1
  113. package/dist/sinks/datadog.d.ts.map +1 -1
  114. package/dist/sinks/datadog.js +16 -20
  115. package/dist/sinks/datadog.js.map +1 -1
  116. package/package.json +2 -2
  117. package/src/__test-utils__/test-runtime.ts +9 -2
  118. package/src/__tests__/config.test.ts +150 -31
  119. package/src/__tests__/core/ambient.test.ts +16 -10
  120. package/src/__tests__/core/api.test.ts +7 -7
  121. package/src/__tests__/core/define-journey.test.ts +28 -11
  122. package/src/__tests__/core/requests.test.ts +49 -31
  123. package/src/__tests__/core/runtime.test.ts +67 -20
  124. package/src/__tests__/core/sanitize.test.ts +69 -0
  125. package/src/__tests__/core/step.test.ts +132 -36
  126. package/src/__tests__/core/timeouts.test.ts +69 -4
  127. package/src/__tests__/global.test.ts +44 -3
  128. package/src/__tests__/hardening.test.ts +613 -0
  129. package/src/__tests__/integrations/axios.test.ts +247 -27
  130. package/src/__tests__/integrations/expose-app-journey.test.ts +9 -3
  131. package/src/__tests__/integrations/fetch.test.ts +153 -19
  132. package/src/__tests__/integrations/host-mfe.test.ts +548 -0
  133. package/src/__tests__/integrations/jquery.test.ts +209 -34
  134. package/src/__tests__/integrations/scope.test.tsx +160 -2
  135. package/src/__tests__/integrations/use-journey-step.test.tsx +43 -0
  136. package/src/__tests__/sinks/console.test.ts +9 -4
  137. package/src/__tests__/sinks/datadog.test.ts +47 -0
  138. package/src/config.ts +162 -38
  139. package/src/core/abort-error.ts +24 -0
  140. package/src/core/api.ts +83 -33
  141. package/src/core/endpoint-policy.ts +46 -4
  142. package/src/core/index.ts +12 -4
  143. package/src/core/limits.ts +73 -0
  144. package/src/core/request-policy.ts +66 -0
  145. package/src/core/runtime.ts +507 -133
  146. package/src/core/sanitize.ts +177 -0
  147. package/src/core/step-tag.ts +82 -12
  148. package/src/core/types.ts +79 -25
  149. package/src/global.ts +312 -42
  150. package/src/index.ts +17 -17
  151. package/src/integrations/axios.ts +183 -45
  152. package/src/integrations/fetch.ts +125 -39
  153. package/src/integrations/http-report.ts +184 -0
  154. package/src/integrations/jquery/index.ts +34 -10
  155. package/src/integrations/jquery/instrument.ts +126 -42
  156. package/src/integrations/react/context.ts +4 -1
  157. package/src/integrations/react/scope.tsx +59 -9
  158. package/src/integrations/react/stable-config-keys.ts +28 -0
  159. package/src/integrations/react/use-journey-step.ts +31 -0
  160. package/src/integrations/request-key.ts +18 -10
  161. package/src/integrations/soft-skip.ts +35 -0
  162. package/src/sinks/console.ts +2 -1
  163. package/src/sinks/datadog.ts +23 -29
@@ -16,11 +16,12 @@ jest.mock('../integrations/jquery', () => ({
16
16
  exposeAppJourney: jest.fn(),
17
17
  }));
18
18
 
19
- import { configureJourney, getRequestTimeouts } from '../config';
19
+ import { configureJourney, getSlowRequests } from '../config';
20
20
  import { resetJourney } from '../core';
21
21
  import { moduleRuntime } from '../core/runtime';
22
22
  import type { JourneyEvent } from '../core/types';
23
23
  import { longestEndpointMatch, sortEndpointsByLongestMatch } from '../core/endpoint-policy';
24
+ import { resetSharedPolicies, setJourneyDefaultEnabled } from '../global';
24
25
  import { instrumentAxios } from '../integrations/axios';
25
26
  import { instrumentFetch } from '../integrations/fetch';
26
27
  import {
@@ -36,11 +37,18 @@ const instrumentFetchMock = jest.mocked(instrumentFetch);
36
37
  const instrumentJqueryMock = jest.mocked(instrumentJquery);
37
38
  const exposeAppJourneyMock = jest.mocked(exposeAppJourney);
38
39
 
39
- describe('[journey] configureJourney / getRequestTimeouts', () => {
40
+ describe('[journey] configureJourney / getSlowRequests', () => {
41
+ beforeEach(() => {
42
+ setJourneyDefaultEnabled(true);
43
+ });
44
+
40
45
  afterEach(() => {
46
+ setJourneyDefaultEnabled(false);
47
+ localStorage.clear();
48
+ resetSharedPolicies();
41
49
  resetJourney({
42
50
  sinks: [sendToDatadog, sendToConsole],
43
- requestTimeouts: { defaultMs: 4_000, endpoints: [] },
51
+ slowRequests: { defaultMs: 4_000, endpoints: [] },
44
52
  });
45
53
  instrumentAxiosMock.mockClear();
46
54
  instrumentFetchMock.mockClear();
@@ -49,27 +57,36 @@ describe('[journey] configureJourney / getRequestTimeouts', () => {
49
57
  });
50
58
 
51
59
  test('defaults to 4s with empty endpoints', () => {
52
- expect(getRequestTimeouts()).toEqual({ defaultMs: 4_000, endpoints: [] });
60
+ expect(getSlowRequests()).toEqual({ defaultMs: 4_000, endpoints: [] });
53
61
  });
54
62
 
55
63
  test('overrides the app-wide request timeout tier', () => {
56
64
  configureJourney({
57
- requestTimeouts: {
65
+ slowRequests: {
58
66
  defaultMs: 2_000,
59
- endpoints: [{ match: '/slow', timeoutMs: 8_000 }],
67
+ endpoints: [{ match: '/slow', slowRequestMs: 8_000 }],
60
68
  },
61
69
  });
62
70
 
63
- expect(getRequestTimeouts()).toEqual({
71
+ expect(getSlowRequests()).toEqual({
64
72
  defaultMs: 2_000,
65
- endpoints: [{ match: '/slow', timeoutMs: 8_000 }],
73
+ endpoints: [{ match: '/slow', slowRequestMs: 8_000 }],
66
74
  });
67
75
  });
68
76
 
69
- test('ignores configureJourney when requestTimeouts is omitted', () => {
70
- configureJourney({ requestTimeouts: { defaultMs: 1_000, endpoints: [] } });
77
+ test('ignores configureJourney when slowRequests is omitted', () => {
78
+ configureJourney({ slowRequests: { defaultMs: 1_000, endpoints: [] } });
71
79
  configureJourney({});
72
- expect(getRequestTimeouts().defaultMs).toBe(1_000);
80
+ expect(getSlowRequests().defaultMs).toBe(1_000);
81
+ });
82
+
83
+ test('clamps maxSteps to 1..200', () => {
84
+ configureJourney({ maxSteps: Infinity });
85
+ expect(moduleRuntime.getMaxSteps()).toBe(50);
86
+ configureJourney({ maxSteps: 0 });
87
+ expect(moduleRuntime.getMaxSteps()).toBe(50);
88
+ configureJourney({ maxSteps: 500 });
89
+ expect(moduleRuntime.getMaxSteps()).toBe(200);
73
90
  });
74
91
 
75
92
  describe('with sinks', () => {
@@ -102,11 +119,25 @@ describe('[journey] configureJourney / getRequestTimeouts', () => {
102
119
 
103
120
  test('leaves sinks unchanged when sinks is omitted', () => {
104
121
  configureJourney({ sinks: [custom] });
105
- configureJourney({ requestTimeouts: { defaultMs: 2_000, endpoints: [] } });
122
+ configureJourney({ slowRequests: { defaultMs: 2_000, endpoints: [] } });
123
+ moduleRuntime.emit(payload);
124
+
125
+ expect(custom).toHaveBeenCalledWith(payload);
126
+ expect(sendToDatadog).not.toHaveBeenCalled();
127
+ });
128
+
129
+ test('ignores a later sinks replacement on this runtime', () => {
130
+ const warn = jest.spyOn(console, 'warn').mockImplementation(() => undefined);
131
+ configureJourney({ sinks: [custom] });
132
+ configureJourney({ sinks: [sendToDatadog] });
106
133
  moduleRuntime.emit(payload);
107
134
 
108
135
  expect(custom).toHaveBeenCalledWith(payload);
109
136
  expect(sendToDatadog).not.toHaveBeenCalled();
137
+ expect(warn).toHaveBeenCalledWith(
138
+ expect.stringContaining('sinks already set on this runtime')
139
+ );
140
+ warn.mockRestore();
110
141
  });
111
142
 
112
143
  test('disables emission when sinks is empty', () => {
@@ -125,6 +156,15 @@ describe('[journey] configureJourney / getRequestTimeouts', () => {
125
156
  };
126
157
  const jquery = { ajaxPrefilter: jest.fn(), bind: jest.fn() } as never;
127
158
  const fetchTarget = { fetch: jest.fn() };
159
+ let warnSpy: jest.SpyInstance;
160
+
161
+ beforeEach(() => {
162
+ warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
163
+ });
164
+
165
+ afterEach(() => {
166
+ warnSpy.mockRestore();
167
+ });
128
168
 
129
169
  test('instruments axios when provided', () => {
130
170
  configureJourney({ axios: axios as never });
@@ -155,6 +195,38 @@ describe('[journey] configureJourney / getRequestTimeouts', () => {
155
195
  expect(instrumentJqueryMock).toHaveBeenCalledWith(jquery);
156
196
  });
157
197
 
198
+ test('instruments global $ when jquery is true', () => {
199
+ const $ = Object.assign(jest.fn(), { ajaxPrefilter: jest.fn() });
200
+ (globalThis as { $?: unknown }).$ = $;
201
+
202
+ try {
203
+ configureJourney({ jquery: true });
204
+
205
+ expect(instrumentJqueryMock).toHaveBeenCalledWith($);
206
+ } finally {
207
+ delete (globalThis as { $?: unknown }).$;
208
+ }
209
+ });
210
+
211
+ test('falls back to global jQuery when jquery is true and $ is absent', () => {
212
+ const jQuery = Object.assign(jest.fn(), { ajaxPrefilter: jest.fn() });
213
+ (globalThis as { jQuery?: unknown }).jQuery = jQuery;
214
+
215
+ try {
216
+ configureJourney({ jquery: true });
217
+
218
+ expect(instrumentJqueryMock).toHaveBeenCalledWith(jQuery);
219
+ } finally {
220
+ delete (globalThis as { jQuery?: unknown }).jQuery;
221
+ }
222
+ });
223
+
224
+ test('skips jquery wiring when jquery is false', () => {
225
+ configureJourney({ jquery: false });
226
+
227
+ expect(instrumentJqueryMock).not.toHaveBeenCalled();
228
+ });
229
+
158
230
  test('exposes App.Journey when exposeAppJourney is true', () => {
159
231
  configureJourney({ exposeAppJourney: true });
160
232
 
@@ -169,21 +241,64 @@ describe('[journey] configureJourney / getRequestTimeouts', () => {
169
241
  });
170
242
 
171
243
  test('skips transport helpers when omitted', () => {
172
- configureJourney({ requestTimeouts: { defaultMs: 1_000, endpoints: [] } });
244
+ configureJourney({ slowRequests: { defaultMs: 1_000, endpoints: [] } });
173
245
 
174
246
  expect(instrumentAxiosMock).not.toHaveBeenCalled();
175
247
  expect(instrumentFetchMock).not.toHaveBeenCalled();
176
248
  expect(instrumentJqueryMock).not.toHaveBeenCalled();
177
249
  expect(exposeAppJourneyMock).not.toHaveBeenCalled();
178
250
  });
251
+
252
+ test('never throws on missing config', () => {
253
+ const warn = jest.spyOn(console, 'warn').mockImplementation(() => {});
254
+
255
+ expect(() => configureJourney(null as never)).not.toThrow();
256
+ expect(() => configureJourney(undefined as never)).not.toThrow();
257
+ expect(warn).toHaveBeenCalledWith(
258
+ expect.stringContaining('[journey] configureJourney: skipped — missing')
259
+ );
260
+ });
261
+
262
+ test('skips when journey is disabled', () => {
263
+ setJourneyDefaultEnabled(false);
264
+ const warn = jest.spyOn(console, 'warn').mockImplementation(() => {});
265
+
266
+ configureJourney({ axios: axios as never, fetch: true });
267
+
268
+ expect(warn).toHaveBeenCalledWith(
269
+ expect.stringContaining('[journey] configureJourney: skipped — disabled')
270
+ );
271
+ expect(instrumentAxiosMock).not.toHaveBeenCalled();
272
+ expect(instrumentFetchMock).not.toHaveBeenCalled();
273
+ });
274
+
275
+ test('never throws when a transport step fails', () => {
276
+ const error = jest.spyOn(console, 'error').mockImplementation(() => {});
277
+ instrumentAxiosMock.mockImplementationOnce(() => {
278
+ throw new Error('axios boom');
279
+ });
280
+
281
+ expect(() =>
282
+ configureJourney({
283
+ axios: axios as never,
284
+ jquery,
285
+ exposeAppJourney: true,
286
+ })
287
+ ).not.toThrow();
288
+
289
+ expect(error).toHaveBeenCalledWith(
290
+ '[journey] configureJourney failed',
291
+ expect.any(Error)
292
+ );
293
+ });
179
294
  });
180
295
  });
181
296
 
182
297
  describe('longestEndpointMatch', () => {
183
298
  // Lookup expects longest-match-first (configureJourney / startJourney sort for you).
184
299
  const endpoints = [
185
- { match: '/api/invoices', timeoutMs: 2_000 },
186
- { match: '/api', timeoutMs: 1_000 },
300
+ { match: '/api/invoices', slowRequestMs: 2_000 },
301
+ { match: '/api', slowRequestMs: 1_000 },
187
302
  { match: '/other', ignore: true },
188
303
  ];
189
304
 
@@ -193,8 +308,8 @@ describe('longestEndpointMatch', () => {
193
308
  });
194
309
 
195
310
  test('picks the longest matching prefix (first hit when sorted)', () => {
196
- expect(longestEndpointMatch(endpoints, '/api/invoices/42')?.timeoutMs).toBe(2_000);
197
- expect(longestEndpointMatch(endpoints, '/api/jobs')?.timeoutMs).toBe(1_000);
311
+ expect(longestEndpointMatch(endpoints, '/api/invoices/42')?.slowRequestMs).toBe(2_000);
312
+ expect(longestEndpointMatch(endpoints, '/api/jobs')?.slowRequestMs).toBe(1_000);
198
313
  });
199
314
 
200
315
  test('returns undefined when nothing matches', () => {
@@ -202,40 +317,44 @@ describe('longestEndpointMatch', () => {
202
317
  });
203
318
 
204
319
  test('requires a path boundary after the prefix', () => {
205
- const user = [{ match: '/api/user', timeoutMs: 1_000 }];
206
- expect(longestEndpointMatch(user, '/api/user')?.timeoutMs).toBe(1_000);
207
- expect(longestEndpointMatch(user, '/api/user/42')?.timeoutMs).toBe(1_000);
320
+ const user = [{ match: '/api/user', slowRequestMs: 1_000 }];
321
+ expect(longestEndpointMatch(user, '/api/user')?.slowRequestMs).toBe(1_000);
322
+ expect(longestEndpointMatch(user, '/api/user/42')?.slowRequestMs).toBe(1_000);
208
323
  expect(longestEndpointMatch(user, '/api/user-preferences')).toBeUndefined();
209
324
  expect(longestEndpointMatch(user, '/api/users')).toBeUndefined();
210
325
 
211
- const trailed = [{ match: '/api/user/', timeoutMs: 2_000 }];
212
- expect(longestEndpointMatch(trailed, '/api/user/42')?.timeoutMs).toBe(2_000);
326
+ const trailed = [{ match: '/api/user/', slowRequestMs: 2_000 }];
327
+ expect(longestEndpointMatch(trailed, '/api/user/42')?.slowRequestMs).toBe(2_000);
213
328
  });
214
329
  });
215
330
 
216
331
  describe('configureJourney endpoint ordering', () => {
332
+ beforeEach(() => {
333
+ setJourneyDefaultEnabled(true);
334
+ });
335
+
217
336
  afterEach(() => {
337
+ setJourneyDefaultEnabled(false);
338
+ localStorage.clear();
339
+ resetSharedPolicies();
218
340
  resetJourney({
219
341
  sinks: [sendToDatadog, sendToConsole],
220
- requestTimeouts: { defaultMs: 4_000, endpoints: [] },
342
+ slowRequests: { defaultMs: 4_000, endpoints: [] },
221
343
  });
222
344
  });
223
345
 
224
346
  test('stores endpoints longest-match-first', () => {
225
347
  configureJourney({
226
- requestTimeouts: {
348
+ slowRequests: {
227
349
  defaultMs: 4_000,
228
350
  endpoints: [
229
- { match: '/api', timeoutMs: 1_000 },
230
- { match: '/api/invoices', timeoutMs: 2_000 },
351
+ { match: '/api', slowRequestMs: 1_000 },
352
+ { match: '/api/invoices', slowRequestMs: 2_000 },
231
353
  ],
232
354
  },
233
355
  });
234
356
 
235
- expect(getRequestTimeouts().endpoints?.map(e => e.match)).toEqual([
236
- '/api/invoices',
237
- '/api',
238
- ]);
357
+ expect(getSlowRequests().endpoints?.map(e => e.match)).toEqual(['/api/invoices', '/api']);
239
358
  });
240
359
  });
241
360
 
@@ -245,7 +364,7 @@ describe('[journey] sortEndpointsByLongestMatch', () => {
245
364
  });
246
365
 
247
366
  test('returns a copy for a single endpoint', () => {
248
- const only = [{ match: '/api', timeoutMs: 1_000 }];
367
+ const only = [{ match: '/api', slowRequestMs: 1_000 }];
249
368
  const sorted = sortEndpointsByLongestMatch(only);
250
369
 
251
370
  expect(sorted).toEqual(only);
@@ -1,13 +1,18 @@
1
1
  /** @jest-environment jsdom */
2
2
  import { baseJourneyDef, useJourneyTestRuntime } from '../../__test-utils__/test-runtime';
3
- import { activeJourneyStep, completeJourney, journeyStep } from '../../core';
3
+ import {
4
+ singleInFlightJourneyStep,
5
+ completeJourney,
6
+ journeyStep,
7
+ JourneyStepStamp,
8
+ } from '../../core';
4
9
  import { moduleRuntime } from '../../core/runtime';
5
10
 
6
11
  const emitMock = jest.fn();
7
12
  const baseDef = { ...baseJourneyDef, name: 'ambient-a' };
8
13
  const otherDef = { ...baseJourneyDef, name: 'ambient-b' };
9
14
 
10
- describe('[journey] activeJourneyStep', () => {
15
+ describe('[journey] singleInFlightJourneyStep', () => {
11
16
  useJourneyTestRuntime(emitMock);
12
17
 
13
18
  describe('around a single step', () => {
@@ -16,13 +21,13 @@ describe('[journey] activeJourneyStep', () => {
16
21
  });
17
22
 
18
23
  test('is set only while the step is running', async () => {
19
- expect(activeJourneyStep()).toBeNull();
24
+ expect(singleInFlightJourneyStep()).toBeNull();
20
25
 
21
26
  await journeyStep(baseDef.name, 'do-work', () => {
22
- expect(activeJourneyStep()).not.toBeNull();
27
+ expect(singleInFlightJourneyStep()).toBeInstanceOf(JourneyStepStamp);
23
28
  });
24
29
 
25
- expect(activeJourneyStep()).toBeNull();
30
+ expect(singleInFlightJourneyStep()).toBeNull();
26
31
  completeJourney(baseDef.name);
27
32
  });
28
33
  });
@@ -50,7 +55,7 @@ describe('[journey] activeJourneyStep', () => {
50
55
  completeJourney(otherDef.name);
51
56
  });
52
57
 
53
- const subject = () => journeyStep(otherDef.name, 'book', () => activeJourneyStep());
58
+ const subject = () => journeyStep(otherDef.name, 'book', () => singleInFlightJourneyStep());
54
59
 
55
60
  test('returns null', async () => {
56
61
  await expect(subject()).resolves.toBeNull();
@@ -60,7 +65,7 @@ describe('[journey] activeJourneyStep', () => {
60
65
  describe('when the first-finishing sibling ends first', () => {
61
66
  let releaseB!: () => void;
62
67
  let stepB: Promise<unknown>;
63
- let ambientWhileBAlone: ReturnType<typeof activeJourneyStep> | 'unset';
68
+ let ambientWhileBAlone: ReturnType<typeof singleInFlightJourneyStep> | 'unset';
64
69
 
65
70
  beforeEach(() => {
66
71
  moduleRuntime.startJourney(baseDef);
@@ -72,7 +77,7 @@ describe('[journey] activeJourneyStep', () => {
72
77
  });
73
78
  stepB = journeyStep(otherDef.name, 'book', async () => {
74
79
  await gateB;
75
- ambientWhileBAlone = activeJourneyStep();
80
+ ambientWhileBAlone = singleInFlightJourneyStep();
76
81
  });
77
82
  });
78
83
 
@@ -85,7 +90,7 @@ describe('[journey] activeJourneyStep', () => {
85
90
 
86
91
  test('keeps ambient on the remaining step', async () => {
87
92
  await journeyStep(baseDef.name, 'load', () => {
88
- expect(activeJourneyStep()).toBeNull();
93
+ expect(singleInFlightJourneyStep()).toBeNull();
89
94
  });
90
95
 
91
96
  releaseB();
@@ -96,7 +101,8 @@ describe('[journey] activeJourneyStep', () => {
96
101
  if (ambientWhileBAlone === 'unset' || ambientWhileBAlone == null) {
97
102
  throw new Error('expected ambient step');
98
103
  }
99
- expect(ambientWhileBAlone.name).toBe('book');
104
+ expect(ambientWhileBAlone).toBeInstanceOf(JourneyStepStamp);
105
+ expect(ambientWhileBAlone.step.name).toBe('book');
100
106
  });
101
107
  });
102
108
  });
@@ -20,10 +20,10 @@ const otherDef = { ...baseJourneyDef, name: 'api-other' };
20
20
  describe('[journey] api', () => {
21
21
  useJourneyTestRuntime(emitMock);
22
22
 
23
- describe('firstOpenJourney', () => {
23
+ describe('firstOpenJourneyName', () => {
24
24
  const names = [baseDef.name, otherDef.name];
25
25
 
26
- const subject = () => moduleRuntime.firstOpenJourney(names);
26
+ const subject = () => moduleRuntime.firstOpenJourneyName(names);
27
27
 
28
28
  describe('when none are open', () => {
29
29
  test('returns null', () => {
@@ -49,7 +49,7 @@ describe('[journey] api', () => {
49
49
 
50
50
  test('returns the first match in order', () => {
51
51
  expect(subject()).toBe(baseDef.name);
52
- expect(moduleRuntime.firstOpenJourney([otherDef.name, baseDef.name])).toBe(
52
+ expect(moduleRuntime.firstOpenJourneyName([otherDef.name, baseDef.name])).toBe(
53
53
  otherDef.name
54
54
  );
55
55
  });
@@ -85,7 +85,7 @@ describe('[journey] api', () => {
85
85
  beforeEach(async () => {
86
86
  moduleRuntime.startJourney(baseDef);
87
87
  await journeyStep(baseDef.name, 'do-work', step => {
88
- step.complete();
88
+ step.completeJourney();
89
89
  });
90
90
  });
91
91
 
@@ -102,7 +102,7 @@ describe('[journey] api', () => {
102
102
  beforeEach(async () => {
103
103
  moduleRuntime.startJourney(baseDef);
104
104
  await journeyStep(baseDef.name, 'do-work', step => {
105
- step.fail('validation');
105
+ step.failJourney('validation');
106
106
  });
107
107
  });
108
108
 
@@ -136,7 +136,7 @@ describe('[journey] api', () => {
136
136
  beforeEach(async () => {
137
137
  moduleRuntime.startJourney(baseDef);
138
138
  await journeyStep(baseDef.name, 'do-work', step => {
139
- step.fail('validation');
139
+ step.failJourney('validation');
140
140
  });
141
141
  });
142
142
 
@@ -209,7 +209,7 @@ describe('[journey] api', () => {
209
209
  beforeEach(async () => {
210
210
  moduleRuntime.startJourney(baseDef);
211
211
  await journeyStep(baseDef.name, 'do-work', step => {
212
- step.fail('validation');
212
+ step.failJourney('validation');
213
213
  });
214
214
  emitMock.mockClear();
215
215
  });
@@ -26,7 +26,7 @@ describe('[journey] defineJourney', () => {
26
26
  const subject = () =>
27
27
  journey.step('do-work', step => {
28
28
  step.setAttribute('x', 1);
29
- step.complete({ 'payment.id': 42 });
29
+ step.completeJourney({ 'payment.id': 42 });
30
30
  });
31
31
 
32
32
  test('opens, records the step, and completes good', async () => {
@@ -41,7 +41,7 @@ describe('[journey] defineJourney', () => {
41
41
 
42
42
  test('mountStep also opens the journey', async () => {
43
43
  await journey.mountStep('boot', step => {
44
- step.complete();
44
+ step.completeJourney();
45
45
  });
46
46
 
47
47
  expect(emitMock).toHaveBeenCalledTimes(1);
@@ -57,7 +57,7 @@ describe('[journey] defineJourney', () => {
57
57
 
58
58
  const subject = () =>
59
59
  journey.step('do-work', step => {
60
- step.complete();
60
+ step.completeJourney();
61
61
  });
62
62
 
63
63
  test('does not restart the open journey', async () => {
@@ -76,7 +76,7 @@ describe('[journey] defineJourney', () => {
76
76
 
77
77
  const subject = () =>
78
78
  journeyStep(journey, 'via-free', step => {
79
- step.complete();
79
+ step.completeJourney();
80
80
  });
81
81
 
82
82
  test('resolves the handle name', async () => {
@@ -153,7 +153,7 @@ describe('[journey] defineJourney', () => {
153
153
 
154
154
  const subject = () =>
155
155
  journey.mountStep('boot', step => {
156
- step.complete();
156
+ step.completeJourney();
157
157
  });
158
158
 
159
159
  test('does not restart the open journey', async () => {
@@ -166,12 +166,29 @@ describe('[journey] defineJourney', () => {
166
166
  });
167
167
  });
168
168
 
169
- describe('when copying def fields onto the handle', () => {
170
- test('exposes the journey config properties', () => {
171
- expect(journey.name).toBe(base.name);
172
- expect(journey.team).toBe(base.team);
173
- expect(journey.group).toBe(base.group);
174
- expect(journey.service).toBe(base.service);
169
+ describe('when keeping the original config on the handle', () => {
170
+ test('exposes the same config object', () => {
171
+ expect(journey.config).toBe(base);
172
+ expect(journey.config.name).toBe(base.name);
173
+ expect(journey.config.name).toBe(base.name);
174
+ expect(journey.config.team).toBe(base.team);
175
+ expect(journey.config.group).toBe(base.group);
176
+ expect(journey.config.service).toBe(base.service);
177
+ });
178
+ });
179
+
180
+ describe('when stepping via free journeyStep with a handle and nothing open', () => {
181
+ const subject = () =>
182
+ journeyStep(journey, 'via-handle', step => {
183
+ step.completeJourney();
184
+ });
185
+
186
+ test('auto-starts from the handle config', async () => {
187
+ await subject();
188
+
189
+ expect(emitMock).toHaveBeenCalledTimes(1);
190
+ expect(lastJourney(emitMock).outcome).toBe('good');
191
+ expect(lastJourney(emitMock).steps[0].name).toBe('via-handle');
175
192
  });
176
193
  });
177
194
  });