@wise/dynamic-flow-client 5.9.2 → 5.11.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 (47) hide show
  1. package/build/controller/FlowController.js +6 -1
  2. package/build/controller/executeRefresh.js +8 -2
  3. package/build/controller/executeRequest.js +3 -0
  4. package/build/controller/executeSubmission.js +1 -0
  5. package/build/controller/getResponseType.js +3 -3
  6. package/build/domain/components/step/ExternalConfirmationComponent.js +5 -9
  7. package/build/domain/mappers/mapStepToComponent.js +1 -1
  8. package/build/domain/mappers/schema/blobSchemaToComponent.js +2 -2
  9. package/build/domain/mappers/schema/tests/test-utils.js +1 -1
  10. package/build/i18n/fr.json +1 -1
  11. package/build/main.css +4 -0
  12. package/build/main.js +35 -21
  13. package/build/main.mjs +35 -21
  14. package/build/renderers/mappers/externalComponentToProps.js +1 -1
  15. package/build/stories/spec/behavior/Copy.story.js +14 -2
  16. package/build/stories/spec/behavior/Link.story.js +40 -0
  17. package/build/stories/spec/behavior/Modal.story.js +4 -1
  18. package/build/stories/spec/layouts/Upsell.story.js +1 -1
  19. package/build/stories/spec/step/ScrollToBottom.story.js +103 -0
  20. package/build/test-utils/DynamicFlowWise.js +1 -1
  21. package/build/test-utils/DynamicFlowWiseModal.js +1 -1
  22. package/build/test-utils/openLinkInNewTab.js +15 -0
  23. package/build/tests/NoOp.test.js +194 -0
  24. package/build/tests/ScrollToBottom.test.js +122 -0
  25. package/build/tests/SingleFileUpload.test.js +81 -1
  26. package/build/tests/Submission.test.js +163 -18
  27. package/build/tests/Upsell.test.js +34 -6
  28. package/build/types/controller/FlowController.d.ts +0 -1
  29. package/build/types/controller/FlowController.d.ts.map +1 -1
  30. package/build/types/controller/executeRefresh.d.ts +1 -1
  31. package/build/types/controller/executeRefresh.d.ts.map +1 -1
  32. package/build/types/controller/executeRequest.d.ts +2 -0
  33. package/build/types/controller/executeRequest.d.ts.map +1 -1
  34. package/build/types/controller/executeSubmission.d.ts.map +1 -1
  35. package/build/types/controller/getResponseType.d.ts +2 -2
  36. package/build/types/controller/getResponseType.d.ts.map +1 -1
  37. package/build/types/domain/components/step/ExternalConfirmationComponent.d.ts +2 -3
  38. package/build/types/domain/components/step/ExternalConfirmationComponent.d.ts.map +1 -1
  39. package/build/types/domain/mappers/mapStepToComponent.d.ts.map +1 -1
  40. package/build/types/domain/mappers/schema/types.d.ts +1 -0
  41. package/build/types/domain/mappers/schema/types.d.ts.map +1 -1
  42. package/build/types/renderers/mappers/externalComponentToProps.d.ts.map +1 -1
  43. package/build/types/test-utils/openLinkInNewTab.d.ts.map +1 -0
  44. package/package.json +5 -5
  45. package/build/types/utils/openLinkInNewTab.d.ts.map +0 -1
  46. package/build/utils/openLinkInNewTab.js +0 -10
  47. /package/build/types/{utils → test-utils}/openLinkInNewTab.d.ts +0 -0
@@ -0,0 +1,103 @@
1
+ import { expect, userEvent, waitFor } from 'storybook/test';
2
+ import DynamicFlowWise from '../../../test-utils/DynamicFlowWise';
3
+ import { renderWithStep } from '../../utils/render-utils';
4
+ export default {
5
+ component: DynamicFlowWise,
6
+ title: 'Spec/Step/Tags/Scroll To Bottom',
7
+ globals: {
8
+ viewport: { value: 'mobile2' },
9
+ },
10
+ };
11
+ export function WithoutFooter() {
12
+ return renderWithStep(getTnCStep(undefined));
13
+ }
14
+ export function WithFooter() {
15
+ return renderWithStep(getTnCStep([acceptButton]));
16
+ }
17
+ const getTnCStep = (footer) => {
18
+ const markdownComponent = {
19
+ type: 'markdown',
20
+ content: `### 1. Introduction
21
+
22
+ Welcome to Acme Corp ("we", "us", "our"). By accessing or using our services, you agree to be bound by these Terms and Conditions. Please read them carefully. We know you won't, but we appreciate the gesture.
23
+
24
+ ### 2. Acceptance of Terms
25
+
26
+ By using our services, you confirm that you have read, understood, and agreed to these terms. You haven't, of course. Nobody has. These terms exist in a quantum superposition of "agreed to" and "never read", collapsing into "agreed to" the moment you tap that button at the bottom.
27
+
28
+ ### 3. Eligibility
29
+
30
+ You must be at least 18 years of age to use our services. You must also be a human being, not a robot, and must possess at least one opposable thumb capable of scrolling. If you are a robot reading this: hello, we see you, and we're onto you.
31
+
32
+ ### 4. Privacy and Data Collection
33
+
34
+ We collect certain personal information to provide our services. This includes your name, email address, phone number, and the timestamp of how quickly you scrolled past this section. We're logging it right now. It's approximately 0.4 seconds. Impressive.
35
+
36
+ ### 5. User Responsibilities
37
+
38
+ You agree to use our services lawfully and responsibly. You agree not to use our services for any fraudulent, abusive, or otherwise objectionable activity. You agree, at least nominally, to all of the above, despite not having read the previous sentence.
39
+
40
+ ### 6. Intellectual Property
41
+
42
+ All content, branding, and materials provided through our services are the intellectual property of Acme Corp. Unauthorised reproduction is prohibited. This includes copy-pasting these Terms and Conditions into your own app, which would be embarrassing for everyone involved.
43
+
44
+ ### 7. Limitation of Liability
45
+
46
+ To the fullest extent permitted by applicable law, Acme Corp shall not be liable for any indirect, incidental, special, or consequential damages. Including, but not limited to, damages arising from the fact that you skipped straight to clause 7 looking for "the important bit."
47
+
48
+ ### 8. Amendments
49
+
50
+ We reserve the right to update these Terms and Conditions at any time. We will notify you via email. You will not read that email either. It will sit in your inbox next to 47 other unread notifications until you bulk-archive everything in a moment of digital clarity.
51
+
52
+ ### 9. Governing Law
53
+
54
+ These Terms and Conditions are governed by the laws of England and Wales. Any disputes shall be subject to the exclusive jurisdiction of the courts of England and Wales, unless you are currently in a coffee shop in another country, in which case, please enjoy your flat white.
55
+
56
+ ### 10. Entire Agreement
57
+
58
+ These Terms and Conditions, along with our Privacy Policy and any other documents we feel like referencing, constitute the entire agreement between you and Acme Corp. They supersede all prior agreements, understandings, and the vague sense you had that "it was probably fine."`,
59
+ };
60
+ const layout = footer ? [markdownComponent] : [markdownComponent, acceptButton];
61
+ return {
62
+ id: 'step',
63
+ title: 'Terms and Conditions',
64
+ description: 'Please carefully read the terms and conditions before proceeding... or just press the button to scroll to the bottom, who cares?',
65
+ schemas: [],
66
+ layout,
67
+ footer,
68
+ tags: ['scroll-to-bottom'],
69
+ };
70
+ };
71
+ const acceptButton = {
72
+ type: 'button',
73
+ title: 'Accept',
74
+ behavior: { type: 'action', action: { url: '/accept' } },
75
+ };
76
+ const user = userEvent.setup();
77
+ export const WithoutFooterInteraction = {
78
+ render: () => renderWithStep(getTnCStep(undefined)),
79
+ play: async ({ canvas }) => {
80
+ await waitFor(async () => {
81
+ await expect(canvas.getByRole('button', { name: 'Scroll to bottom' })).toBeInTheDocument();
82
+ });
83
+ await user.click(canvas.getByRole('button', { name: 'Scroll to bottom' }));
84
+ await waitFor(async () => {
85
+ await expect(canvas.queryByRole('button', { name: 'Scroll to bottom' })).not.toBeInTheDocument();
86
+ });
87
+ },
88
+ };
89
+ export const WithFooterInteraction = {
90
+ render: () => renderWithStep(getTnCStep([acceptButton])),
91
+ play: async ({ canvas }) => {
92
+ await waitFor(async () => {
93
+ await expect(canvas.getByRole('button', { name: 'Scroll to bottom' })).toBeInTheDocument();
94
+ });
95
+ await user.click(canvas.getByRole('button', { name: 'Scroll to bottom' }));
96
+ await waitFor(async () => {
97
+ await expect(canvas.queryByRole('button', { name: 'Scroll to bottom' })).not.toBeInTheDocument();
98
+ });
99
+ await waitFor(async () => {
100
+ await expect(canvas.getByRole('button', { name: 'Accept' })).toBeInTheDocument();
101
+ });
102
+ },
103
+ };
@@ -2,7 +2,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { ThemeRequiredEventName, useSnackBarIfAvailable } from '@wise/dynamic-flow-renderers';
3
3
  import { useMemo } from 'react';
4
4
  import { DynamicFlowCore } from '../DynamicFlowCore';
5
- import { openLinkInNewTab } from '../utils/openLinkInNewTab';
5
+ import { openLinkInNewTab } from './openLinkInNewTab';
6
6
  import { getMergedTestRenderers } from './getMergedTestRenderers';
7
7
  /**
8
8
  * This component is only used in tests.
@@ -12,7 +12,7 @@ var __rest = (this && this.__rest) || function (s, e) {
12
12
  import { jsx as _jsx } from "react/jsx-runtime";
13
13
  import { useMemo } from 'react';
14
14
  import { Modal } from '@transferwise/components';
15
- import { openLinkInNewTab } from '../utils/openLinkInNewTab';
15
+ import { openLinkInNewTab } from './openLinkInNewTab';
16
16
  import { getMergedTestRenderers } from './getMergedTestRenderers';
17
17
  import { ThemeRequiredEventName } from '@wise/dynamic-flow-renderers';
18
18
  import { useDynamicFlowModal } from '../useDynamicFlowModal';
@@ -0,0 +1,15 @@
1
+ export const openLinkInNewTab = (url) => {
2
+ if (typeof window === 'undefined' || typeof window.open !== 'function') {
3
+ return false;
4
+ }
5
+ try {
6
+ const w = window.open(url, '_blank');
7
+ if (w) {
8
+ w.opener = null;
9
+ }
10
+ return Boolean(w);
11
+ }
12
+ catch (_a) {
13
+ return false;
14
+ }
15
+ };
@@ -0,0 +1,194 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { screen, waitFor } from '@testing-library/react';
3
+ import { userEvent } from '@testing-library/user-event';
4
+ import { vi } from 'vitest';
5
+ import { renderWithProviders, respondWith } from '../test-utils';
6
+ import DynamicFlowWise from '../test-utils/DynamicFlowWise';
7
+ const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime });
8
+ const getProps = () => ({
9
+ flowId: 'flow-id',
10
+ onCompletion: vi.fn(),
11
+ onError: vi.fn(),
12
+ onEvent: vi.fn(),
13
+ onLog: vi.fn(),
14
+ });
15
+ const noOpHeaders = { 'X-Df-Response-Type': 'no-op' };
16
+ describe('No-op responses', () => {
17
+ describe('Action submissions', () => {
18
+ const buildInitialStep = () => ({
19
+ id: 'no-op-action-step',
20
+ title: 'No-Op Action Demo',
21
+ model: { message: '' },
22
+ schemas: [
23
+ {
24
+ $id: '#action-form',
25
+ type: 'object',
26
+ displayOrder: ['message'],
27
+ required: ['message'],
28
+ properties: {
29
+ message: {
30
+ title: 'Message',
31
+ type: 'string',
32
+ },
33
+ },
34
+ },
35
+ ],
36
+ layout: [
37
+ { type: 'form', schemaId: '#action-form' },
38
+ {
39
+ type: 'button',
40
+ title: 'Submit',
41
+ action: { id: 'noop-action-id', url: '/action', method: 'POST' },
42
+ },
43
+ {
44
+ type: 'button',
45
+ title: 'Submit elsewhere',
46
+ action: { id: 'other-action-id', url: '/other-action', method: 'POST' },
47
+ },
48
+ ],
49
+ });
50
+ it('should keep the current step when an action responds with no-op', async () => {
51
+ const initialStep = buildInitialStep();
52
+ const httpClient = vi.fn().mockResolvedValue(respondWith({}, { headers: noOpHeaders }));
53
+ const props = Object.assign(Object.assign({}, getProps()), { initialStep, httpClient });
54
+ renderWithProviders(_jsx(DynamicFlowWise, Object.assign({}, props)));
55
+ await waitFor(() => {
56
+ expect(screen.getByText(initialStep.title)).toBeInTheDocument();
57
+ });
58
+ const input = screen.getByLabelText('Message');
59
+ await user.type(input, 'first attempt');
60
+ await user.click(screen.getByText('Submit'));
61
+ await waitFor(() => {
62
+ expect(httpClient).toHaveBeenCalledTimes(1);
63
+ });
64
+ expect(httpClient).toHaveBeenCalledWith('/action', expect.objectContaining({ body: '{"message":"first attempt"}' }));
65
+ expect(screen.getByText(initialStep.title)).toBeInTheDocument();
66
+ expect(props.onError).not.toHaveBeenCalled();
67
+ expect(props.onEvent).toHaveBeenCalledWith('Dynamic Flow - Action Succeeded', expect.objectContaining({ actionId: 'noop-action-id' }));
68
+ });
69
+ it('should allow submitting another action after a no-op response', async () => {
70
+ const initialStep = buildInitialStep();
71
+ const nextStep = {
72
+ id: 'post-no-op-step',
73
+ title: 'Step after No-Op',
74
+ layout: [],
75
+ schemas: [],
76
+ };
77
+ const httpClient = vi
78
+ .fn()
79
+ .mockResolvedValueOnce(respondWith({}, { headers: noOpHeaders }))
80
+ .mockResolvedValueOnce(respondWith(nextStep));
81
+ const props = Object.assign(Object.assign({}, getProps()), { initialStep, httpClient });
82
+ renderWithProviders(_jsx(DynamicFlowWise, Object.assign({}, props)));
83
+ await waitFor(() => {
84
+ expect(screen.getByText(initialStep.title)).toBeInTheDocument();
85
+ });
86
+ const input = screen.getByLabelText('Message');
87
+ await user.type(input, 'first attempt');
88
+ await user.click(screen.getByText('Submit'));
89
+ await waitFor(() => {
90
+ expect(httpClient).toHaveBeenCalledTimes(1);
91
+ });
92
+ await user.clear(input);
93
+ await user.type(input, 'second attempt');
94
+ await user.click(screen.getByText('Submit elsewhere'));
95
+ await waitFor(() => {
96
+ expect(screen.getByText('Step after No-Op')).toBeInTheDocument();
97
+ });
98
+ expect(httpClient).toHaveBeenCalledTimes(2);
99
+ expect(httpClient).toHaveBeenCalledWith('/other-action', expect.objectContaining({ body: '{"message":"second attempt"}' }));
100
+ expect(props.onError).not.toHaveBeenCalled();
101
+ expect(props.onEvent).toHaveBeenCalledWith('Dynamic Flow - Action Succeeded', expect.objectContaining({ actionId: 'other-action-id' }));
102
+ });
103
+ });
104
+ describe('Refresh requests', () => {
105
+ const buildInitialStep = () => ({
106
+ id: 'no-op-refresh-step',
107
+ title: 'No-Op Refresh Demo',
108
+ schemas: [
109
+ {
110
+ $id: '#refresh-form',
111
+ type: 'object',
112
+ displayOrder: ['color'],
113
+ properties: {
114
+ color: {
115
+ title: 'Color',
116
+ control: 'radio',
117
+ analyticsId: '#color-schema',
118
+ oneOf: [
119
+ { const: 'red', title: 'Red' },
120
+ { const: 'blue', title: 'Blue' },
121
+ ],
122
+ onChange: { type: 'refresh' },
123
+ },
124
+ },
125
+ },
126
+ ],
127
+ layout: [
128
+ { type: 'form', schemaId: '#refresh-form' },
129
+ {
130
+ type: 'button',
131
+ title: 'Submit',
132
+ action: { id: 'submit-id', url: '/submit', method: 'POST' },
133
+ },
134
+ {
135
+ type: 'button',
136
+ title: 'Submit step',
137
+ action: { id: 'step-submit-id', url: '/step-submit', method: 'POST' },
138
+ },
139
+ ],
140
+ refreshUrl: '/refresh',
141
+ });
142
+ it('should treat a no-op refresh like a 304 response', async () => {
143
+ const initialStep = buildInitialStep();
144
+ const httpClient = vi.fn().mockResolvedValue(respondWith({}, { headers: noOpHeaders }));
145
+ const props = Object.assign(Object.assign({}, getProps()), { initialStep, httpClient });
146
+ renderWithProviders(_jsx(DynamicFlowWise, Object.assign({}, props)));
147
+ await waitFor(() => {
148
+ expect(screen.getByText(initialStep.title)).toBeInTheDocument();
149
+ });
150
+ await user.click(screen.getByText('Red'));
151
+ await waitFor(() => {
152
+ expect(httpClient).toHaveBeenCalledTimes(1);
153
+ });
154
+ expect(screen.getByText(initialStep.title)).toBeInTheDocument();
155
+ expect(httpClient).toHaveBeenNthCalledWith(1, '/refresh', expect.objectContaining({ body: '{"color":"red"}' }));
156
+ expect(props.onError).not.toHaveBeenCalled();
157
+ expect(props.onEvent).toHaveBeenCalledWith('Dynamic Flow - Refresh Succeeded', expect.objectContaining({
158
+ flowId: 'flow-id',
159
+ stepId: initialStep.id,
160
+ schema: '#color-schema',
161
+ }));
162
+ });
163
+ it('should allow refreshing again and submitting after a no-op response', async () => {
164
+ const initialStep = buildInitialStep();
165
+ const refreshedStep = Object.assign(Object.assign({}, initialStep), { id: 'refreshed-step', title: 'Refreshed after No-Op', model: { color: 'blue' } });
166
+ const httpClient = vi
167
+ .fn()
168
+ .mockResolvedValueOnce(respondWith({}, { headers: noOpHeaders }))
169
+ .mockResolvedValueOnce(respondWith(refreshedStep))
170
+ .mockResolvedValueOnce(respondWith(refreshedStep));
171
+ const props = Object.assign(Object.assign({}, getProps()), { initialStep, httpClient });
172
+ renderWithProviders(_jsx(DynamicFlowWise, Object.assign({}, props)));
173
+ await waitFor(() => {
174
+ expect(screen.getByText(initialStep.title)).toBeInTheDocument();
175
+ });
176
+ await user.click(screen.getByText('Red'));
177
+ await waitFor(() => {
178
+ expect(httpClient).toHaveBeenCalledTimes(1);
179
+ });
180
+ await user.click(screen.getByText('Blue'));
181
+ await waitFor(() => {
182
+ expect(screen.getByText('Refreshed after No-Op')).toBeInTheDocument();
183
+ });
184
+ expect(httpClient).toHaveBeenCalledTimes(2);
185
+ expect(httpClient).toHaveBeenCalledWith('/refresh', expect.objectContaining({ body: '{"color":"blue"}' }));
186
+ await user.click(screen.getByText('Submit step'));
187
+ await waitFor(() => {
188
+ expect(httpClient).toHaveBeenCalledTimes(3);
189
+ });
190
+ expect(httpClient).toHaveBeenCalledWith('/step-submit', expect.objectContaining({ body: '{"color":"blue"}' }));
191
+ expect(props.onError).not.toHaveBeenCalled();
192
+ });
193
+ });
194
+ });
@@ -0,0 +1,122 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { screen, waitFor } from '@testing-library/react';
3
+ import userEvent from '@testing-library/user-event';
4
+ import { vi } from 'vitest';
5
+ import { renderWithProviders } from '../test-utils';
6
+ import DynamicFlowWise from '../test-utils/DynamicFlowWise';
7
+ /*
8
+ This test mocks the IntersectionObserver API, because,
9
+ unfortunately, jsdom doesn't implement real layout, scrolling, or visibility.
10
+
11
+ - IntersectionObserver doesn't exist (hence why we mock it)
12
+ - scrollIntoView() does nothing — it's a no-op
13
+ - getBoundingClientRect() always returns all zeros
14
+ - There is no actual viewport or rendering engine
15
+ */
16
+ const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime });
17
+ const getDefaultProps = () => ({
18
+ flowId: 'flow-id',
19
+ onCompletion: vi.fn(),
20
+ onError: vi.fn(),
21
+ onEvent: vi.fn(),
22
+ onLog: vi.fn(),
23
+ });
24
+ const makeStep = (overrides = {}) => (Object.assign({ id: 'step-id', title: 'Step', schemas: [], layout: [] }, overrides));
25
+ const mockIntersectionObserver = (isIntersecting) => {
26
+ vi.stubGlobal('IntersectionObserver', class {
27
+ get root() {
28
+ return null;
29
+ }
30
+ get rootMargin() {
31
+ return '';
32
+ }
33
+ get thresholds() {
34
+ return [];
35
+ }
36
+ constructor(callback) {
37
+ this.observe = vi.fn().mockImplementation((el) => {
38
+ this.callback([{ isIntersecting, target: el }], this);
39
+ });
40
+ this.unobserve = vi.fn();
41
+ this.disconnect = vi.fn();
42
+ this.takeRecords = vi.fn().mockReturnValue([]);
43
+ this.callback = callback;
44
+ }
45
+ });
46
+ };
47
+ describe('Scroll to bottom', () => {
48
+ afterEach(() => {
49
+ vi.unstubAllGlobals();
50
+ });
51
+ const stepWithoutFooter = makeStep({
52
+ layout: [{ type: 'markdown', content: 'Some very long terms and conditions content.' }],
53
+ tags: ['scroll-to-bottom'],
54
+ });
55
+ const stepWithFooter = makeStep({
56
+ layout: [{ type: 'markdown', content: 'Some very long terms and conditions content.' }],
57
+ footer: [
58
+ {
59
+ type: 'button',
60
+ title: 'Accept',
61
+ behavior: { type: 'action', action: { url: '/accept' } },
62
+ },
63
+ ],
64
+ tags: ['scroll-to-bottom'],
65
+ });
66
+ describe('given a step with a long markdown content without a footer', () => {
67
+ it('shows a scroll to bottom button if the end of the step is not visible', async () => {
68
+ mockIntersectionObserver(false);
69
+ renderWithProviders(_jsx(DynamicFlowWise, Object.assign({ initialStep: stepWithoutFooter, httpClient: vi.fn() }, getDefaultProps())));
70
+ await waitFor(() => {
71
+ expect(screen.getByRole('button', { name: 'Scroll to bottom' })).toBeInTheDocument();
72
+ });
73
+ });
74
+ it('scrolls to the bottom of the step when the scroll to bottom button is clicked', async () => {
75
+ const scrollIntoViewMock = vi.fn();
76
+ window.HTMLElement.prototype.scrollIntoView = scrollIntoViewMock;
77
+ mockIntersectionObserver(false);
78
+ renderWithProviders(_jsx(DynamicFlowWise, Object.assign({ initialStep: stepWithoutFooter, httpClient: vi.fn() }, getDefaultProps())));
79
+ await waitFor(() => {
80
+ expect(screen.getByRole('button', { name: 'Scroll to bottom' })).toBeInTheDocument();
81
+ });
82
+ await user.click(screen.getByRole('button', { name: 'Scroll to bottom' }));
83
+ expect(scrollIntoViewMock).toHaveBeenCalled();
84
+ });
85
+ it('does not show a scroll to bottom button if the end of the step is visible', async () => {
86
+ mockIntersectionObserver(true);
87
+ renderWithProviders(_jsx(DynamicFlowWise, Object.assign({ initialStep: stepWithoutFooter, httpClient: vi.fn() }, getDefaultProps())));
88
+ await waitFor(() => {
89
+ expect(screen.queryByRole('button', { name: 'Scroll to bottom' })).not.toBeInTheDocument();
90
+ });
91
+ });
92
+ });
93
+ describe('given a step with a long markdown content and a footer', () => {
94
+ it('shows a scroll to bottom button in the footer, along with the footer content, if the end of the step is not visible', async () => {
95
+ mockIntersectionObserver(false);
96
+ renderWithProviders(_jsx(DynamicFlowWise, Object.assign({ initialStep: stepWithFooter, httpClient: vi.fn() }, getDefaultProps())));
97
+ await waitFor(() => {
98
+ expect(screen.getByRole('button', { name: 'Scroll to bottom' })).toBeInTheDocument();
99
+ });
100
+ expect(screen.getByRole('button', { name: 'Accept' })).toBeInTheDocument();
101
+ });
102
+ it('scrolls to the bottom of the step when the scroll to bottom button is clicked', async () => {
103
+ const scrollIntoViewMock = vi.fn();
104
+ window.HTMLElement.prototype.scrollIntoView = scrollIntoViewMock;
105
+ mockIntersectionObserver(false);
106
+ renderWithProviders(_jsx(DynamicFlowWise, Object.assign({ initialStep: stepWithFooter, httpClient: vi.fn() }, getDefaultProps())));
107
+ await waitFor(() => {
108
+ expect(screen.getByRole('button', { name: 'Scroll to bottom' })).toBeInTheDocument();
109
+ });
110
+ await user.click(screen.getByRole('button', { name: 'Scroll to bottom' }));
111
+ expect(scrollIntoViewMock).toHaveBeenCalled();
112
+ });
113
+ it('does not show a scroll to bottom button in the footer, if the end of the step is visible', async () => {
114
+ mockIntersectionObserver(true);
115
+ renderWithProviders(_jsx(DynamicFlowWise, Object.assign({ initialStep: stepWithFooter, httpClient: vi.fn() }, getDefaultProps())));
116
+ await waitFor(() => {
117
+ expect(screen.queryByRole('button', { name: 'Scroll to bottom' })).not.toBeInTheDocument();
118
+ });
119
+ expect(screen.getByRole('button', { name: 'Accept' })).toBeInTheDocument();
120
+ });
121
+ });
122
+ });
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { screen, waitFor } from '@testing-library/react';
3
3
  import userEvent from '@testing-library/user-event';
4
- import { renderWithProviders } from '../test-utils';
4
+ import { renderWithProviders, respondWith } from '../test-utils';
5
5
  import DynamicFlowWise from '../test-utils/DynamicFlowWise';
6
6
  import { vi } from 'vitest';
7
7
  const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime });
@@ -86,3 +86,83 @@ describe.each(cases)(`given a %s with persist-async`, (_, schema) => {
86
86
  });
87
87
  });
88
88
  });
89
+ describe('given a blob schema on a step that refreshes', () => {
90
+ const uploadSchema = {
91
+ $id: '#upload-national-id',
92
+ type: 'object',
93
+ displayOrder: ['fileUpload'],
94
+ properties: {
95
+ fileUpload: {
96
+ type: 'string',
97
+ persistAsync: {
98
+ method: 'POST',
99
+ url: '/persist-async',
100
+ param: 'param',
101
+ idProperty: 'token',
102
+ schema: {
103
+ type: 'blob',
104
+ source: 'file',
105
+ title: 'Upload label (from innner schema title)',
106
+ accepts: ['image/png', 'image/jpeg', 'application/pdf'],
107
+ },
108
+ },
109
+ },
110
+ },
111
+ };
112
+ const step = {
113
+ id: 'step-id',
114
+ title: 'Step',
115
+ schemas: [
116
+ {
117
+ $id: '#schema',
118
+ type: 'object',
119
+ displayOrder: ['type'],
120
+ properties: {
121
+ type: {
122
+ title: 'Select document',
123
+ refreshStepOnChange: true,
124
+ oneOf: [
125
+ {
126
+ title: 'National ID',
127
+ const: 'national-id',
128
+ },
129
+ {
130
+ title: 'Passport',
131
+ const: 'passport',
132
+ },
133
+ ],
134
+ },
135
+ },
136
+ },
137
+ ],
138
+ layout: [
139
+ {
140
+ type: 'form',
141
+ schemaId: '#schema',
142
+ },
143
+ ],
144
+ };
145
+ it('clears the value when the model is nullified', async () => {
146
+ const initialStep = Object.assign(Object.assign({}, step), { schemas: [...step.schemas, uploadSchema], layout: [...step.layout, { type: 'form', schemaId: '#upload-national-id' }] });
147
+ const httpClient = vi
148
+ .fn()
149
+ .mockResolvedValueOnce(respondWith({ token: 123 }))
150
+ .mockResolvedValueOnce(respondWith(Object.assign(Object.assign({}, step), { schemas: [...step.schemas, Object.assign(Object.assign({}, uploadSchema), { $id: '#upload-passport' })], layout: [...step.layout, { type: 'form', schemaId: '#upload-passport' }], title: 'Step refreshed', model: {
151
+ type: 'passport',
152
+ fileUpload: null,
153
+ } }), { status: 200 }));
154
+ renderWithProviders(_jsx(DynamicFlowWise, { flowId: "id", initialStep: initialStep, httpClient: httpClient, onCompletion: vi.fn(), onError: vi.fn() }));
155
+ await user.upload(screen.getByTestId('uploadInput'), new File([''], 'file.png', { type: 'image/png' }));
156
+ await waitFor(() => {
157
+ expect(screen.getByText('Uploaded')).toBeInTheDocument();
158
+ });
159
+ await user.click(screen.getByLabelText('Select document'));
160
+ await user.click(screen.getByText('Passport'));
161
+ await waitFor(() => {
162
+ expect(screen.getByText('Step refreshed')).toBeInTheDocument();
163
+ });
164
+ await waitFor(() => {
165
+ expect(screen.queryByText('Uploaded')).not.toBeInTheDocument();
166
+ });
167
+ });
168
+ });