@servicetitan/journey 1.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.
- package/README.md +7 -0
- package/dist/__mocks__/test-runtime.d.ts +16 -0
- package/dist/__mocks__/test-runtime.d.ts.map +1 -0
- package/dist/__tests__/ambient.test.d.ts +2 -0
- package/dist/__tests__/ambient.test.d.ts.map +1 -0
- package/dist/__tests__/axios.test.d.ts +2 -0
- package/dist/__tests__/axios.test.d.ts.map +1 -0
- package/dist/__tests__/config.test.d.ts +2 -0
- package/dist/__tests__/config.test.d.ts.map +1 -0
- package/dist/__tests__/context.test.d.ts +2 -0
- package/dist/__tests__/context.test.d.ts.map +1 -0
- package/dist/__tests__/datadog.test.d.ts +2 -0
- package/dist/__tests__/datadog.test.d.ts.map +1 -0
- package/dist/__tests__/define-journey.test.d.ts +2 -0
- package/dist/__tests__/define-journey.test.d.ts.map +1 -0
- package/dist/__tests__/expose-app-journey.test.d.ts +2 -0
- package/dist/__tests__/expose-app-journey.test.d.ts.map +1 -0
- package/dist/__tests__/fetch.test.d.ts +2 -0
- package/dist/__tests__/fetch.test.d.ts.map +1 -0
- package/dist/__tests__/jquery.test.d.ts +2 -0
- package/dist/__tests__/jquery.test.d.ts.map +1 -0
- package/dist/__tests__/registry.test.d.ts +2 -0
- package/dist/__tests__/registry.test.d.ts.map +1 -0
- package/dist/__tests__/request-key.test.d.ts +2 -0
- package/dist/__tests__/request-key.test.d.ts.map +1 -0
- package/dist/__tests__/requests.test.d.ts +2 -0
- package/dist/__tests__/requests.test.d.ts.map +1 -0
- package/dist/__tests__/runtime.test.d.ts +2 -0
- package/dist/__tests__/runtime.test.d.ts.map +1 -0
- package/dist/__tests__/scope.test.d.ts +2 -0
- package/dist/__tests__/scope.test.d.ts.map +1 -0
- package/dist/__tests__/sinks.test.d.ts +2 -0
- package/dist/__tests__/sinks.test.d.ts.map +1 -0
- package/dist/__tests__/step.test.d.ts +2 -0
- package/dist/__tests__/step.test.d.ts.map +1 -0
- package/dist/__tests__/timeouts.test.d.ts +2 -0
- package/dist/__tests__/timeouts.test.d.ts.map +1 -0
- package/dist/config.d.ts +42 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +46 -0
- package/dist/config.js.map +1 -0
- package/dist/core/index.d.ts +6 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +5 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/registry.d.ts +50 -0
- package/dist/core/registry.d.ts.map +1 -0
- package/dist/core/registry.js +102 -0
- package/dist/core/registry.js.map +1 -0
- package/dist/core/runtime.d.ts +51 -0
- package/dist/core/runtime.d.ts.map +1 -0
- package/dist/core/runtime.js +401 -0
- package/dist/core/runtime.js.map +1 -0
- package/dist/core/step-tag.d.ts +16 -0
- package/dist/core/step-tag.d.ts.map +1 -0
- package/dist/core/step-tag.js +35 -0
- package/dist/core/step-tag.js.map +1 -0
- package/dist/core/types.d.ts +143 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +3 -0
- package/dist/core/types.js.map +1 -0
- package/dist/endpoint-policy.d.ts +29 -0
- package/dist/endpoint-policy.d.ts.map +1 -0
- package/dist/endpoint-policy.js +39 -0
- package/dist/endpoint-policy.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +19 -0
- package/dist/index.js.map +1 -0
- package/dist/integrations/axios.d.ts +23 -0
- package/dist/integrations/axios.d.ts.map +1 -0
- package/dist/integrations/axios.js +51 -0
- package/dist/integrations/axios.js.map +1 -0
- package/dist/integrations/fetch.d.ts +21 -0
- package/dist/integrations/fetch.d.ts.map +1 -0
- package/dist/integrations/fetch.js +63 -0
- package/dist/integrations/fetch.js.map +1 -0
- package/dist/integrations/jquery/index.d.ts +22 -0
- package/dist/integrations/jquery/index.d.ts.map +1 -0
- package/dist/integrations/jquery/index.js +27 -0
- package/dist/integrations/jquery/index.js.map +1 -0
- package/dist/integrations/jquery/instrument.d.ts +18 -0
- package/dist/integrations/jquery/instrument.d.ts.map +1 -0
- package/dist/integrations/jquery/instrument.js +44 -0
- package/dist/integrations/jquery/instrument.js.map +1 -0
- package/dist/integrations/react/context.d.ts +6 -0
- package/dist/integrations/react/context.d.ts.map +1 -0
- package/dist/integrations/react/context.js +14 -0
- package/dist/integrations/react/context.js.map +1 -0
- package/dist/integrations/react/scope.d.ts +13 -0
- package/dist/integrations/react/scope.d.ts.map +1 -0
- package/dist/integrations/react/scope.js +27 -0
- package/dist/integrations/react/scope.js.map +1 -0
- package/dist/integrations/request-key.d.ts +17 -0
- package/dist/integrations/request-key.d.ts.map +1 -0
- package/dist/integrations/request-key.js +39 -0
- package/dist/integrations/request-key.js.map +1 -0
- package/dist/sinks/datadog.d.ts +6 -0
- package/dist/sinks/datadog.d.ts.map +1 -0
- package/dist/sinks/datadog.js +64 -0
- package/dist/sinks/datadog.js.map +1 -0
- package/package.json +44 -0
- package/src/__mocks__/test-runtime.ts +37 -0
- package/src/__tests__/ambient.test.ts +101 -0
- package/src/__tests__/axios.test.ts +365 -0
- package/src/__tests__/config.test.ts +261 -0
- package/src/__tests__/context.test.tsx +84 -0
- package/src/__tests__/datadog.test.ts +160 -0
- package/src/__tests__/define-journey.test.ts +176 -0
- package/src/__tests__/expose-app-journey.test.ts +149 -0
- package/src/__tests__/fetch.test.ts +264 -0
- package/src/__tests__/jquery.test.ts +327 -0
- package/src/__tests__/registry.test.ts +265 -0
- package/src/__tests__/request-key.test.ts +81 -0
- package/src/__tests__/requests.test.ts +337 -0
- package/src/__tests__/runtime.test.ts +273 -0
- package/src/__tests__/scope.test.tsx +128 -0
- package/src/__tests__/sinks.test.ts +99 -0
- package/src/__tests__/step.test.ts +305 -0
- package/src/__tests__/timeouts.test.ts +226 -0
- package/src/config.ts +87 -0
- package/src/core/index.ts +39 -0
- package/src/core/registry.ts +162 -0
- package/src/core/runtime.ts +489 -0
- package/src/core/step-tag.ts +24 -0
- package/src/core/types.ts +160 -0
- package/src/endpoint-policy.ts +66 -0
- package/src/index.ts +51 -0
- package/src/integrations/axios.ts +85 -0
- package/src/integrations/fetch.ts +97 -0
- package/src/integrations/jquery/index.ts +62 -0
- package/src/integrations/jquery/instrument.ts +84 -0
- package/src/integrations/react/context.ts +17 -0
- package/src/integrations/react/scope.tsx +32 -0
- package/src/integrations/request-key.ts +46 -0
- package/src/sinks/datadog.ts +71 -0
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/** @jest-environment jsdom */
|
|
2
|
+
import { render, screen } from '@testing-library/react';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
JourneyNameContext,
|
|
6
|
+
useJourneyName,
|
|
7
|
+
useOptionalJourneyName,
|
|
8
|
+
} from '../integrations/react/context';
|
|
9
|
+
|
|
10
|
+
function OptionalProbe() {
|
|
11
|
+
const name = useOptionalJourneyName();
|
|
12
|
+
return <span data-testid="optional">{name ?? 'null'}</span>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function RequiredProbe() {
|
|
16
|
+
const name = useJourneyName();
|
|
17
|
+
return <span data-testid="required">{name}</span>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe('[journey] context hooks', () => {
|
|
21
|
+
describe('useOptionalJourneyName', () => {
|
|
22
|
+
describe('without a provider', () => {
|
|
23
|
+
const subject = () => render(<OptionalProbe />);
|
|
24
|
+
|
|
25
|
+
test('returns null', () => {
|
|
26
|
+
subject();
|
|
27
|
+
|
|
28
|
+
expect(screen.getByTestId('optional').textContent).toBe('null');
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe('with a provider', () => {
|
|
33
|
+
const subject = () =>
|
|
34
|
+
render(
|
|
35
|
+
<JourneyNameContext.Provider value="scoped">
|
|
36
|
+
<OptionalProbe />
|
|
37
|
+
</JourneyNameContext.Provider>
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
test('reads the provided name', () => {
|
|
41
|
+
subject();
|
|
42
|
+
|
|
43
|
+
expect(screen.getByTestId('optional').textContent).toBe('scoped');
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe('useJourneyName', () => {
|
|
49
|
+
describe('with a provider', () => {
|
|
50
|
+
const subject = () =>
|
|
51
|
+
render(
|
|
52
|
+
<JourneyNameContext.Provider value="scoped">
|
|
53
|
+
<RequiredProbe />
|
|
54
|
+
</JourneyNameContext.Provider>
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
test('returns the provided name', () => {
|
|
58
|
+
subject();
|
|
59
|
+
|
|
60
|
+
expect(screen.getByTestId('required').textContent).toBe('scoped');
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe('without a provider', () => {
|
|
65
|
+
let spy: jest.SpyInstance;
|
|
66
|
+
|
|
67
|
+
beforeEach(() => {
|
|
68
|
+
spy = jest.spyOn(console, 'error').mockImplementation(() => {});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
afterEach(() => {
|
|
72
|
+
spy.mockRestore();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const subject = () => render(<RequiredProbe />);
|
|
76
|
+
|
|
77
|
+
test('throws', () => {
|
|
78
|
+
expect(() => subject()).toThrow(
|
|
79
|
+
'useJourneyName must be used within a <JourneyScope>'
|
|
80
|
+
);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
});
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/** @jest-environment jsdom */
|
|
2
|
+
const addAction = jest.fn();
|
|
3
|
+
const importedAddAction = jest.fn();
|
|
4
|
+
|
|
5
|
+
jest.mock('@datadog/browser-rum', () => ({
|
|
6
|
+
datadogRum: {
|
|
7
|
+
addAction: (...args: unknown[]) => importedAddAction(...args),
|
|
8
|
+
},
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
import type { JourneyEvent } from '../core/types';
|
|
12
|
+
import { sendToDatadog, setJourneyRum } from '../sinks/datadog';
|
|
13
|
+
|
|
14
|
+
const RUM_GLOBAL_KEY = '__stJourneyRum';
|
|
15
|
+
|
|
16
|
+
function sampleEvent(overrides: Partial<JourneyEvent['journey']> = {}): JourneyEvent {
|
|
17
|
+
return {
|
|
18
|
+
journey: {
|
|
19
|
+
name: 'Call Screen Book!',
|
|
20
|
+
team: 'JBCE',
|
|
21
|
+
group: 'call-booking',
|
|
22
|
+
service: 'call-screen',
|
|
23
|
+
outcome: 'good',
|
|
24
|
+
durationMs: 120,
|
|
25
|
+
steps: [],
|
|
26
|
+
...overrides,
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
describe('[journey] setJourneyRum / sendToDatadog', () => {
|
|
32
|
+
beforeEach(() => {
|
|
33
|
+
addAction.mockClear();
|
|
34
|
+
importedAddAction.mockClear();
|
|
35
|
+
delete (globalThis as Record<string, unknown>)[RUM_GLOBAL_KEY];
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe('without a shared RUM instance', () => {
|
|
39
|
+
const subject = () => sendToDatadog(sampleEvent());
|
|
40
|
+
|
|
41
|
+
test('falls back to the imported datadogRum', () => {
|
|
42
|
+
subject();
|
|
43
|
+
|
|
44
|
+
expect(importedAddAction).toHaveBeenCalledTimes(1);
|
|
45
|
+
expect(importedAddAction.mock.calls[0][0]).toBe('user_journey');
|
|
46
|
+
expect(addAction).not.toHaveBeenCalled();
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe('with setJourneyRum', () => {
|
|
51
|
+
beforeEach(() => {
|
|
52
|
+
setJourneyRum({ addAction });
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const subject = () =>
|
|
56
|
+
sendToDatadog(sampleEvent({ reason: 'request-error', outcome: 'bad' }));
|
|
57
|
+
|
|
58
|
+
test('uses the shared RUM instance', () => {
|
|
59
|
+
subject();
|
|
60
|
+
|
|
61
|
+
expect(addAction).toHaveBeenCalledTimes(1);
|
|
62
|
+
expect(importedAddAction).not.toHaveBeenCalled();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('normalizes identity fields', () => {
|
|
66
|
+
subject();
|
|
67
|
+
|
|
68
|
+
const ctx = addAction.mock.calls[0][1];
|
|
69
|
+
expect(ctx.journey.name).toBe('call_screen_book');
|
|
70
|
+
expect(ctx.journey.team).toBe('jbce');
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('normalizes string tags and passes numbers/booleans through', () => {
|
|
75
|
+
setJourneyRum({ addAction });
|
|
76
|
+
sendToDatadog(
|
|
77
|
+
sampleEvent({
|
|
78
|
+
tags: {
|
|
79
|
+
'plan': 'Pro Plan!!',
|
|
80
|
+
'job.id': 42,
|
|
81
|
+
'inbound': true,
|
|
82
|
+
},
|
|
83
|
+
expected: ['book-job'],
|
|
84
|
+
})
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
const ctx = addAction.mock.calls[0][1];
|
|
88
|
+
expect(ctx.journey.tags).toEqual({
|
|
89
|
+
'plan': 'pro_plan',
|
|
90
|
+
'job.id': 42,
|
|
91
|
+
'inbound': true,
|
|
92
|
+
});
|
|
93
|
+
expect(ctx.journey.expected).toEqual(['book-job']);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test('omits reason and tags when absent', () => {
|
|
97
|
+
setJourneyRum({ addAction });
|
|
98
|
+
sendToDatadog(sampleEvent());
|
|
99
|
+
|
|
100
|
+
const ctx = addAction.mock.calls[0][1];
|
|
101
|
+
expect(ctx.journey.reason).toBeUndefined();
|
|
102
|
+
expect(ctx.journey.tags).toBeUndefined();
|
|
103
|
+
expect(ctx.journey.expected).toBeUndefined();
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test('collapses runs of underscores and drops trailing ones', () => {
|
|
107
|
+
setJourneyRum({ addAction });
|
|
108
|
+
sendToDatadog(sampleEvent({ name: 'Foo@@@Bar___' }));
|
|
109
|
+
|
|
110
|
+
expect(addAction.mock.calls[0][1].journey.name).toBe('foo_bar');
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test('truncates long string tag values to 200 characters', () => {
|
|
114
|
+
setJourneyRum({ addAction });
|
|
115
|
+
const long = `A${'b'.repeat(250)}`;
|
|
116
|
+
sendToDatadog(sampleEvent({ tags: { note: long } }));
|
|
117
|
+
|
|
118
|
+
const note = addAction.mock.calls[0][1].journey.tags.note as string;
|
|
119
|
+
expect(note.length).toBeLessThanOrEqual(200);
|
|
120
|
+
expect(note.endsWith('_')).toBe(false);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test('normalizes the failure reason string', () => {
|
|
124
|
+
setJourneyRum({ addAction });
|
|
125
|
+
sendToDatadog(sampleEvent({ outcome: 'bad', reason: 'Step Error!!' }));
|
|
126
|
+
|
|
127
|
+
expect(addAction.mock.calls[0][1].journey.reason).toBe('step_error');
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test('maps camelCase fields to Datadog snake_case wire keys', () => {
|
|
131
|
+
setJourneyRum({ addAction });
|
|
132
|
+
sendToDatadog(
|
|
133
|
+
sampleEvent({
|
|
134
|
+
steps: [
|
|
135
|
+
{
|
|
136
|
+
name: 'load',
|
|
137
|
+
startMs: 0,
|
|
138
|
+
durationMs: 12,
|
|
139
|
+
outcome: 'good',
|
|
140
|
+
httpStatus: 502,
|
|
141
|
+
},
|
|
142
|
+
],
|
|
143
|
+
})
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
const ctx = addAction.mock.calls[0][1];
|
|
147
|
+
expect(ctx.journey.duration_ms).toBe(120);
|
|
148
|
+
/* eslint-disable @typescript-eslint/naming-convention -- assert Datadog RUM wire format */
|
|
149
|
+
expect(ctx.journey.steps).toEqual([
|
|
150
|
+
{
|
|
151
|
+
name: 'load',
|
|
152
|
+
start_ms: 0,
|
|
153
|
+
duration_ms: 12,
|
|
154
|
+
outcome: 'good',
|
|
155
|
+
http_status: 502,
|
|
156
|
+
},
|
|
157
|
+
]);
|
|
158
|
+
/* eslint-enable @typescript-eslint/naming-convention */
|
|
159
|
+
});
|
|
160
|
+
});
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/** @jest-environment jsdom */
|
|
2
|
+
import { lastJourney, useJourneyTestRuntime } from '../__mocks__/test-runtime';
|
|
3
|
+
import { defineJourney, journeyStep, startJourney } from '../core';
|
|
4
|
+
|
|
5
|
+
const emitMock = jest.fn();
|
|
6
|
+
|
|
7
|
+
const base = {
|
|
8
|
+
name: 'handle-api',
|
|
9
|
+
team: 'platform',
|
|
10
|
+
group: 'test',
|
|
11
|
+
service: 'journey',
|
|
12
|
+
timeoutMs: 0,
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
describe('[journey] defineJourney', () => {
|
|
16
|
+
let journey = defineJourney(base);
|
|
17
|
+
|
|
18
|
+
useJourneyTestRuntime(emitMock);
|
|
19
|
+
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
journey = defineJourney(base);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe('when no journey is open', () => {
|
|
25
|
+
const subject = () =>
|
|
26
|
+
journey.step('do-work', step => {
|
|
27
|
+
step.setAttribute('x', 1);
|
|
28
|
+
step.complete({ 'payment.id': 42 });
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('opens, records the step, and completes good', async () => {
|
|
32
|
+
await subject();
|
|
33
|
+
|
|
34
|
+
expect(emitMock).toHaveBeenCalledTimes(1);
|
|
35
|
+
const event = lastJourney(emitMock);
|
|
36
|
+
expect(event.outcome).toBe('good');
|
|
37
|
+
expect(event.tags?.['payment.id']).toBe(42);
|
|
38
|
+
expect(event.steps[0].attributes).toEqual({ x: 1 });
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('mountStep also opens the journey', async () => {
|
|
42
|
+
await journey.mountStep('boot', step => {
|
|
43
|
+
step.complete();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
expect(emitMock).toHaveBeenCalledTimes(1);
|
|
47
|
+
expect(lastJourney(emitMock).outcome).toBe('good');
|
|
48
|
+
expect(lastJourney(emitMock).steps[0].name).toBe('boot');
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe('when a JourneyScope already opened the journey', () => {
|
|
53
|
+
beforeEach(() => {
|
|
54
|
+
startJourney(base);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const subject = () =>
|
|
58
|
+
journey.step('do-work', step => {
|
|
59
|
+
step.complete();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('does not restart the open journey', async () => {
|
|
63
|
+
await subject();
|
|
64
|
+
|
|
65
|
+
expect(emitMock).toHaveBeenCalledTimes(1);
|
|
66
|
+
expect(lastJourney(emitMock).outcome).toBe('good');
|
|
67
|
+
expect(lastJourney(emitMock).steps).toHaveLength(1);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe('when stepping via free journeyStep with a handle', () => {
|
|
72
|
+
beforeEach(() => {
|
|
73
|
+
journey.start();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const subject = () =>
|
|
77
|
+
journeyStep(journey, 'via-free', step => {
|
|
78
|
+
step.complete();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('resolves the handle name', async () => {
|
|
82
|
+
await subject();
|
|
83
|
+
|
|
84
|
+
expect(lastJourney(emitMock).steps[0].name).toBe('via-free');
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe('when using bound lifecycle helpers', () => {
|
|
89
|
+
test('fail marks bad', () => {
|
|
90
|
+
journey.start();
|
|
91
|
+
journey.fail('no-inventory');
|
|
92
|
+
|
|
93
|
+
expect(lastJourney(emitMock).outcome).toBe('bad');
|
|
94
|
+
expect(lastJourney(emitMock).reason).toBe('no-inventory');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test('exclude marks excluded', () => {
|
|
98
|
+
journey.start();
|
|
99
|
+
journey.exclude();
|
|
100
|
+
|
|
101
|
+
expect(lastJourney(emitMock).outcome).toBe('excluded');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test('setTags merges onto the open journey', () => {
|
|
105
|
+
journey.start();
|
|
106
|
+
journey.setTags({ plan: 'pro' });
|
|
107
|
+
journey.complete();
|
|
108
|
+
|
|
109
|
+
expect(lastJourney(emitMock).tags?.plan).toBe('pro');
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test('start with tags applies them', () => {
|
|
113
|
+
journey.start({ cohort: 'a' });
|
|
114
|
+
journey.complete();
|
|
115
|
+
|
|
116
|
+
expect(lastJourney(emitMock).tags?.cohort).toBe('a');
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test('complete with tags applies them', () => {
|
|
120
|
+
journey.start();
|
|
121
|
+
journey.complete({ done: true });
|
|
122
|
+
|
|
123
|
+
expect(lastJourney(emitMock).tags?.done).toBe(true);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test('start restarts an already-open journey', () => {
|
|
127
|
+
journey.start();
|
|
128
|
+
journey.start();
|
|
129
|
+
|
|
130
|
+
expect(emitMock).toHaveBeenCalledTimes(1);
|
|
131
|
+
expect(lastJourney(emitMock).outcome).toBe('excluded');
|
|
132
|
+
|
|
133
|
+
journey.complete();
|
|
134
|
+
expect(lastJourney(emitMock).outcome).toBe('good');
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test('complete after fail does not flip the outcome', () => {
|
|
138
|
+
journey.start();
|
|
139
|
+
journey.fail('validation');
|
|
140
|
+
journey.complete({ ignored: true });
|
|
141
|
+
|
|
142
|
+
expect(emitMock).toHaveBeenCalledTimes(1);
|
|
143
|
+
expect(lastJourney(emitMock).outcome).toBe('bad');
|
|
144
|
+
expect(lastJourney(emitMock).reason).toBe('validation');
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
describe('when mountStep runs under an already-open journey', () => {
|
|
149
|
+
beforeEach(() => {
|
|
150
|
+
startJourney(base);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
const subject = () =>
|
|
154
|
+
journey.mountStep('boot', step => {
|
|
155
|
+
step.complete();
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test('does not restart the open journey', async () => {
|
|
159
|
+
await subject();
|
|
160
|
+
|
|
161
|
+
expect(emitMock).toHaveBeenCalledTimes(1);
|
|
162
|
+
expect(lastJourney(emitMock).outcome).toBe('good');
|
|
163
|
+
expect(lastJourney(emitMock).steps).toHaveLength(1);
|
|
164
|
+
expect(lastJourney(emitMock).steps[0].name).toBe('boot');
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
describe('when copying def fields onto the handle', () => {
|
|
169
|
+
test('exposes the journey config properties', () => {
|
|
170
|
+
expect(journey.name).toBe(base.name);
|
|
171
|
+
expect(journey.team).toBe(base.team);
|
|
172
|
+
expect(journey.group).toBe(base.group);
|
|
173
|
+
expect(journey.service).toBe(base.service);
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
});
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/** @jest-environment jsdom */
|
|
2
|
+
import { useJourneyTestRuntime } from '../__mocks__/test-runtime';
|
|
3
|
+
import { exposeAppJourney, type WindowWithAppJourney } from '../integrations/jquery';
|
|
4
|
+
|
|
5
|
+
const emitMock = jest.fn();
|
|
6
|
+
|
|
7
|
+
function createTarget(existingApp?: Record<string, unknown>) {
|
|
8
|
+
return {
|
|
9
|
+
App: existingApp ? { ...existingApp } : undefined,
|
|
10
|
+
} as any;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
describe('[journey] exposeAppJourney', () => {
|
|
14
|
+
useJourneyTestRuntime(emitMock);
|
|
15
|
+
|
|
16
|
+
test('creates window.App when missing and assigns Journey', () => {
|
|
17
|
+
const target = createTarget();
|
|
18
|
+
|
|
19
|
+
const api = exposeAppJourney(target);
|
|
20
|
+
|
|
21
|
+
expect(target.App).toBeDefined();
|
|
22
|
+
expect(target.App.Journey).toBe(api);
|
|
23
|
+
expect(typeof api.defineJourney).toBe('function');
|
|
24
|
+
expect(typeof api.journeyStep).toBe('function');
|
|
25
|
+
expect(typeof api.firstOpenJourney).toBe('function');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('preserves existing window.App properties', () => {
|
|
29
|
+
const target = createTarget({ CallBoard: { Instance: {} } });
|
|
30
|
+
|
|
31
|
+
exposeAppJourney(target);
|
|
32
|
+
|
|
33
|
+
expect(target.App.CallBoard).toEqual({ Instance: {} });
|
|
34
|
+
expect(target.App.Journey).toBeDefined();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe('with defineJourney', () => {
|
|
38
|
+
let j: ReturnType<typeof exposeAppJourney>;
|
|
39
|
+
let bookJob: ReturnType<typeof j.defineJourney>;
|
|
40
|
+
|
|
41
|
+
beforeEach(() => {
|
|
42
|
+
j = exposeAppJourney(createTarget());
|
|
43
|
+
bookJob = j.defineJourney({
|
|
44
|
+
name: 'ko-bridge-good',
|
|
45
|
+
team: 'test',
|
|
46
|
+
group: 'test',
|
|
47
|
+
service: 'test',
|
|
48
|
+
timeoutMs: 0,
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('emits a good outcome via handle lifecycle', async () => {
|
|
53
|
+
bookJob.start();
|
|
54
|
+
await bookJob.step('book-job', step => {
|
|
55
|
+
step.setAttribute('call.id', 42);
|
|
56
|
+
return 'ok';
|
|
57
|
+
});
|
|
58
|
+
bookJob.complete();
|
|
59
|
+
|
|
60
|
+
expect(emitMock).toHaveBeenCalledTimes(1);
|
|
61
|
+
const event = emitMock.mock.calls[0][0].journey;
|
|
62
|
+
expect(event.name).toBe('ko-bridge-good');
|
|
63
|
+
expect(event.outcome).toBe('good');
|
|
64
|
+
expect(event.steps).toHaveLength(1);
|
|
65
|
+
expect(event.steps[0].attributes).toEqual({ 'call.id': 42 });
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('emits a bad outcome via handle.fail', () => {
|
|
69
|
+
bookJob = j.defineJourney({
|
|
70
|
+
name: 'ko-bridge-fail',
|
|
71
|
+
team: 'test',
|
|
72
|
+
group: 'test',
|
|
73
|
+
service: 'test',
|
|
74
|
+
timeoutMs: 0,
|
|
75
|
+
});
|
|
76
|
+
bookJob.start();
|
|
77
|
+
bookJob.fail('book-rejected');
|
|
78
|
+
|
|
79
|
+
expect(emitMock).toHaveBeenCalledTimes(1);
|
|
80
|
+
const event = emitMock.mock.calls[0][0].journey;
|
|
81
|
+
expect(event.outcome).toBe('bad');
|
|
82
|
+
expect(event.reason).toBe('book-rejected');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('emits excluded via handle.exclude', () => {
|
|
86
|
+
bookJob = j.defineJourney({
|
|
87
|
+
name: 'ko-bridge-exclude',
|
|
88
|
+
team: 'test',
|
|
89
|
+
group: 'test',
|
|
90
|
+
service: 'test',
|
|
91
|
+
timeoutMs: 0,
|
|
92
|
+
});
|
|
93
|
+
bookJob.start();
|
|
94
|
+
bookJob.exclude();
|
|
95
|
+
|
|
96
|
+
expect(emitMock).toHaveBeenCalledTimes(1);
|
|
97
|
+
expect(emitMock.mock.calls[0][0].journey.outcome).toBe('excluded');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test('applies setTags before complete', async () => {
|
|
101
|
+
bookJob = j.defineJourney({
|
|
102
|
+
name: 'ko-bridge-tags',
|
|
103
|
+
team: 'test',
|
|
104
|
+
group: 'test',
|
|
105
|
+
service: 'test',
|
|
106
|
+
timeoutMs: 0,
|
|
107
|
+
steps: ['load'],
|
|
108
|
+
});
|
|
109
|
+
bookJob.start();
|
|
110
|
+
expect(j.firstOpenJourney(['missing', 'ko-bridge-tags'])).toBe('ko-bridge-tags');
|
|
111
|
+
|
|
112
|
+
await bookJob.step('load', step => {
|
|
113
|
+
step.setAttribute('phase', 'load');
|
|
114
|
+
});
|
|
115
|
+
bookJob.setTags({ 'job.id': 9 });
|
|
116
|
+
bookJob.complete();
|
|
117
|
+
|
|
118
|
+
expect(emitMock).toHaveBeenCalledTimes(1);
|
|
119
|
+
const event = emitMock.mock.calls[0][0].journey;
|
|
120
|
+
expect(event.tags).toEqual({ 'job.id': 9 });
|
|
121
|
+
expect(event.steps[0].attributes).toEqual({ phase: 'load' });
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test('journeyStep supports candidate lists without a handle', async () => {
|
|
126
|
+
const j = exposeAppJourney(createTarget());
|
|
127
|
+
const primary = j.defineJourney({
|
|
128
|
+
name: 'ko-candidates-a',
|
|
129
|
+
team: 'test',
|
|
130
|
+
group: 'test',
|
|
131
|
+
service: 'test',
|
|
132
|
+
timeoutMs: 0,
|
|
133
|
+
});
|
|
134
|
+
primary.start();
|
|
135
|
+
|
|
136
|
+
await j.journeyStep(['missing', 'ko-candidates-a'], 'shared-step', () => 'ok');
|
|
137
|
+
primary.complete();
|
|
138
|
+
|
|
139
|
+
expect(emitMock).toHaveBeenCalledTimes(1);
|
|
140
|
+
expect(emitMock.mock.calls[0][0].journey.steps[0].name).toBe('shared-step');
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test('is idempotent when called repeatedly on window', () => {
|
|
144
|
+
const first = exposeAppJourney(window as WindowWithAppJourney);
|
|
145
|
+
const second = exposeAppJourney(window as WindowWithAppJourney);
|
|
146
|
+
expect(second).toBe(first);
|
|
147
|
+
expect((window as WindowWithAppJourney).App?.Journey).toBe(first);
|
|
148
|
+
});
|
|
149
|
+
});
|