@wise/dynamic-flow-client 5.11.0 → 5.12.1

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.
@@ -1,5 +1,6 @@
1
+ import { normaliseBehavior } from '../domain/mappers/utils/behavior-utils';
1
2
  import { getResponseTypeFromHeader } from './getResponseType';
2
- import { assertActionResponseBody, assertStepResponseBody, isActionResponseBody, parseResponseBodyAsJsonElement, } from './response-utils';
3
+ import { assertActionResponseBody, assertModalResponseBody, assertStepResponseBody, assertSubflowResponseBody, isActionResponseBody, parseResponseBodyAsJsonElement, } from './response-utils';
3
4
  export const executePoll = async (props) => {
4
5
  const { errorBehavior, signal, url, httpClient, trackEvent } = props;
5
6
  try {
@@ -27,7 +28,21 @@ export const executePoll = async (props) => {
27
28
  assertActionResponseBody(body);
28
29
  return { type: 'behavior', behavior: actionResponseBodyToBehavior(body) };
29
30
  }
30
- default: {
31
+ case 'modal': {
32
+ assertModalResponseBody(body);
33
+ return { type: 'behavior', behavior: Object.assign({ type: 'modal' }, body) };
34
+ }
35
+ case 'no-op': {
36
+ return { type: 'behavior', behavior: { type: 'none' } };
37
+ }
38
+ case 'subflow': {
39
+ assertSubflowResponseBody(body);
40
+ return {
41
+ type: 'behavior',
42
+ behavior: normaliseBehavior(Object.assign({ type: 'subflow' }, body), []),
43
+ };
44
+ }
45
+ case undefined: {
31
46
  if (isActionResponseBody(body)) {
32
47
  return { type: 'behavior', behavior: actionResponseBodyToBehavior(body) };
33
48
  }
package/build/main.js CHANGED
@@ -6474,7 +6474,21 @@ var executePoll = async (props) => {
6474
6474
  assertActionResponseBody(body);
6475
6475
  return { type: "behavior", behavior: actionResponseBodyToBehavior(body) };
6476
6476
  }
6477
- default: {
6477
+ case "modal": {
6478
+ assertModalResponseBody(body);
6479
+ return { type: "behavior", behavior: __spreadValues({ type: "modal" }, body) };
6480
+ }
6481
+ case "no-op": {
6482
+ return { type: "behavior", behavior: { type: "none" } };
6483
+ }
6484
+ case "subflow": {
6485
+ assertSubflowResponseBody(body);
6486
+ return {
6487
+ type: "behavior",
6488
+ behavior: normaliseBehavior(__spreadValues({ type: "subflow" }, body), [])
6489
+ };
6490
+ }
6491
+ case void 0: {
6478
6492
  if (isActionResponseBody(body)) {
6479
6493
  return { type: "behavior", behavior: actionResponseBodyToBehavior(body) };
6480
6494
  }
package/build/main.mjs CHANGED
@@ -6444,7 +6444,21 @@ var executePoll = async (props) => {
6444
6444
  assertActionResponseBody(body);
6445
6445
  return { type: "behavior", behavior: actionResponseBodyToBehavior(body) };
6446
6446
  }
6447
- default: {
6447
+ case "modal": {
6448
+ assertModalResponseBody(body);
6449
+ return { type: "behavior", behavior: __spreadValues({ type: "modal" }, body) };
6450
+ }
6451
+ case "no-op": {
6452
+ return { type: "behavior", behavior: { type: "none" } };
6453
+ }
6454
+ case "subflow": {
6455
+ assertSubflowResponseBody(body);
6456
+ return {
6457
+ type: "behavior",
6458
+ behavior: normaliseBehavior(__spreadValues({ type: "subflow" }, body), [])
6459
+ };
6460
+ }
6461
+ case void 0: {
6448
6462
  if (isActionResponseBody(body)) {
6449
6463
  return { type: "behavior", behavior: actionResponseBodyToBehavior(body) };
6450
6464
  }
@@ -53,6 +53,20 @@ describe('ListLayout component', () => {
53
53
  accessibilityDescription: 'Accessibility description for additional info 2',
54
54
  },
55
55
  },
56
+ {
57
+ analyticsId: 'item-3-aid',
58
+ title: 'Item 3',
59
+ tag: 'tag3',
60
+ supportingValues: { value: 'value3', subvalue: 'subvalue3' },
61
+ callToAction: {
62
+ title: 'Go to FooBar',
63
+ behavior: {
64
+ type: 'link',
65
+ url: 'https://foo.bar',
66
+ },
67
+ accessibilityDescription: 'Accessibility description for item 3 link',
68
+ },
69
+ },
56
70
  ],
57
71
  },
58
72
  ],
@@ -66,6 +80,15 @@ describe('ListLayout component', () => {
66
80
  await user.click(screen.getByRole('button', { name: 'Click me' }));
67
81
  expect(props.httpClient).toHaveBeenCalledWith('/item-cta', expect.anything());
68
82
  });
83
+ it('should render a link for the item with the correct href and accessibility description', () => {
84
+ renderWithProviders(_jsx(DynamicFlowWise, Object.assign({}, props)));
85
+ const link = screen.getByRole('link', { name: 'Go to FooBar' });
86
+ expect(link).toBeInTheDocument();
87
+ expect(link).toHaveAttribute('href', 'https://foo.bar');
88
+ expect(link).toHaveAttribute('target', '_blank');
89
+ expect(link).toHaveAttribute('rel', 'noopener noreferrer');
90
+ expect(link).toHaveAttribute('aria-description', 'Accessibility description for item 3 link');
91
+ });
69
92
  });
70
93
  describe('Extra props', () => {
71
94
  const props = Object.assign(Object.assign({}, getDefaultProps()), { initialStep: getInitialStep() });
@@ -8,7 +8,7 @@ import { abortableDelay, respondWith, respondWithDelay } from '../test-utils/fet
8
8
  const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime });
9
9
  const cases = [
10
10
  [
11
- 'a config with onError.behavior of type "action" (spec next)',
11
+ 'a config with onError.behavior of type "action" (current behavior spec)',
12
12
  {
13
13
  url: '/polling',
14
14
  interval: 2,
@@ -35,7 +35,7 @@ const cases = [
35
35
  },
36
36
  ],
37
37
  [
38
- 'a config with onError.action',
38
+ 'a config with onError.action (deprecated polling spec)',
39
39
  {
40
40
  url: '/polling',
41
41
  interval: 2,
@@ -614,4 +614,89 @@ describe.each(cases)('Given %s', (_, pollingConfig) => {
614
614
  expect(mockHttpClient).toHaveBeenCalledTimes(1);
615
615
  });
616
616
  });
617
+ describe('when the response is a modal', () => {
618
+ it('renders the modal content and stops polling', async () => {
619
+ const mockHttpClient = vi.fn().mockImplementation(async (input) => {
620
+ switch (input) {
621
+ case '/polling':
622
+ return respondWith({ title: 'Modal Title', content: [{ type: 'paragraph', text: 'Modal content' }] }, { headers: { 'X-DF-Response-Type': 'modal' } });
623
+ }
624
+ });
625
+ renderWithProviders(_jsx(DynamicFlowWise, { flowId: "polling-flow", httpClient: mockHttpClient, initialStep: initialStep(), onError: () => { }, onCompletion: () => { } }));
626
+ await waitFor(() => {
627
+ expect(screen.getByText('polling title')).toBeInTheDocument();
628
+ });
629
+ expect(mockHttpClient).toHaveBeenCalledTimes(1);
630
+ expect(mockHttpClient).toHaveBeenCalledWith('/polling', expect.anything());
631
+ await waitFor(() => {
632
+ expect(screen.getByText('Modal Title')).toBeInTheDocument();
633
+ });
634
+ expect(screen.getByText('Modal content')).toBeInTheDocument();
635
+ // original step remains visible behind the modal
636
+ expect(screen.getByText('polling title')).toBeInTheDocument();
637
+ // polling stops after the modal is shown
638
+ await vi.advanceTimersByTimeAsync(5000);
639
+ expect(mockHttpClient).toHaveBeenCalledTimes(1);
640
+ });
641
+ });
642
+ describe('when the response is a no-op', () => {
643
+ it('stops polling without changing the UI', async () => {
644
+ const mockHttpClient = vi.fn().mockImplementation(async (input) => {
645
+ switch (input) {
646
+ case '/polling':
647
+ return respondWith(null, { headers: { 'X-DF-Response-Type': 'no-op' } });
648
+ }
649
+ });
650
+ const onError = vi.fn();
651
+ renderWithProviders(_jsx(DynamicFlowWise, { flowId: "polling-flow", httpClient: mockHttpClient, initialStep: initialStep(), onError: onError, onCompletion: () => { } }));
652
+ await waitFor(() => {
653
+ expect(screen.getByText('polling title')).toBeInTheDocument();
654
+ });
655
+ expect(mockHttpClient).toHaveBeenCalledTimes(1);
656
+ expect(mockHttpClient).toHaveBeenCalledWith('/polling', expect.anything());
657
+ // polling stops, but nothing changes in the UI
658
+ await vi.advanceTimersByTimeAsync(5000);
659
+ expect(mockHttpClient).toHaveBeenCalledTimes(1);
660
+ expect(screen.getByText('polling title')).toBeInTheDocument();
661
+ // the flow doesn't fail
662
+ expect(onError).not.toHaveBeenCalled();
663
+ });
664
+ });
665
+ describe('when the response is a subflow', () => {
666
+ it('launches the subflow modally and stops polling', async () => {
667
+ const mockHttpClient = vi.fn().mockImplementation(async (input) => {
668
+ switch (input) {
669
+ case '/polling':
670
+ return respondWith({
671
+ referrerId: 'polling-flow',
672
+ launchConfig: {
673
+ type: 'dynamic',
674
+ request: { url: '/subflow-start', method: 'GET' },
675
+ presentation: { type: 'modal' },
676
+ },
677
+ }, { headers: { 'X-DF-Response-Type': 'subflow' } });
678
+ case '/subflow-start':
679
+ return respondWith({
680
+ id: 'subflow-step',
681
+ title: 'Subflow Step',
682
+ schemas: [],
683
+ layout: [],
684
+ });
685
+ }
686
+ });
687
+ renderWithProviders(_jsx(DynamicFlowWise, { flowId: "polling-flow", httpClient: mockHttpClient, initialStep: initialStep(), onError: () => { }, onCompletion: () => { } }));
688
+ await waitFor(() => {
689
+ expect(screen.getByText('polling title')).toBeInTheDocument();
690
+ });
691
+ expect(mockHttpClient).toHaveBeenCalledWith('/polling', expect.anything());
692
+ await waitFor(() => {
693
+ expect(screen.getByText('Subflow Step')).toBeInTheDocument();
694
+ });
695
+ // original step still visible behind the subflow modal
696
+ expect(screen.getByText('polling title')).toBeInTheDocument();
697
+ // polling stops after the subflow is launched
698
+ await vi.advanceTimersByTimeAsync(5000);
699
+ expect(mockHttpClient).toHaveBeenCalledTimes(2); // one poll + one subflow start
700
+ });
701
+ });
617
702
  });