@windstream/react-shared-components 0.2.54 → 0.2.56

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 (27) hide show
  1. package/dist/contentful/index.d.ts +200 -2
  2. package/dist/contentful/index.esm.js +5 -5
  3. package/dist/contentful/index.esm.js.map +1 -1
  4. package/dist/contentful/index.js +4 -4
  5. package/dist/contentful/index.js.map +1 -1
  6. package/dist/index.d.ts +2 -2
  7. package/dist/styles.css +1 -1
  8. package/package.json +1 -1
  9. package/src/contentful/blocks/dynamic-form/DynamicForm.stories.tsx +154 -0
  10. package/src/contentful/blocks/dynamic-form/conditional-fields.test.tsx +108 -0
  11. package/src/contentful/blocks/dynamic-form/conditional-fields.tsx +103 -0
  12. package/src/contentful/blocks/dynamic-form/constants.ts +28 -0
  13. package/src/contentful/blocks/dynamic-form/context.ts +35 -0
  14. package/src/contentful/blocks/dynamic-form/field-map.test.tsx +421 -0
  15. package/src/contentful/blocks/dynamic-form/field-map.tsx +486 -0
  16. package/src/contentful/blocks/dynamic-form/form-field.test.tsx +319 -0
  17. package/src/contentful/blocks/dynamic-form/form-field.tsx +224 -0
  18. package/src/contentful/blocks/dynamic-form/index.test.tsx +355 -0
  19. package/src/contentful/blocks/dynamic-form/index.tsx +552 -0
  20. package/src/contentful/blocks/dynamic-form/render.test.tsx +116 -0
  21. package/src/contentful/blocks/dynamic-form/render.tsx +172 -0
  22. package/src/contentful/blocks/dynamic-form/types.ts +211 -0
  23. package/src/contentful/blocks/dynamic-form/utils.test.ts +238 -0
  24. package/src/contentful/blocks/dynamic-form/utils.ts +255 -0
  25. package/src/contentful/blocks/primary-hero/index.test.tsx +28 -3
  26. package/src/contentful/blocks/primary-hero/index.tsx +5 -1
  27. package/src/contentful/index.ts +20 -0
@@ -0,0 +1,355 @@
1
+ import React from "react";
2
+ import { DynamicForm } from "./index";
3
+ import { DynamicFormItem } from "./types";
4
+
5
+ import { fireEvent, render, screen, waitFor } from "@testing-library/react";
6
+
7
+ const fields: DynamicFormItem[] = [
8
+ {
9
+ itemType: "formField",
10
+ fieldId: "firstName",
11
+ fieldType: "Text",
12
+ displayName: "First Name",
13
+ requiredField: true,
14
+ errorMessage: "First name is required",
15
+ },
16
+ {
17
+ itemType: "formField",
18
+ fieldId: "consent",
19
+ fieldType: "Checkbox",
20
+ displayName: "I agree",
21
+ requiredField: true,
22
+ },
23
+ ];
24
+
25
+ describe("DynamicForm", () => {
26
+ it("renders the title, fields and submit button", () => {
27
+ render(
28
+ <DynamicForm title="Contact us" fields={fields} submitLabel="Send" />
29
+ );
30
+ expect(screen.getByText("Contact us")).toBeInTheDocument();
31
+ expect(screen.getByText("First Name")).toBeInTheDocument();
32
+ expect(
33
+ screen.getByRole("button", { name: /submit form/i })
34
+ ).toBeInTheDocument();
35
+ });
36
+
37
+ it("shows a validation message and does not call handlers when required fields are empty", async () => {
38
+ const onEmailSubmit = jest.fn();
39
+ render(
40
+ <DynamicForm
41
+ fields={fields}
42
+ formSubmitOptions={["email"]}
43
+ emailTemplateName="welcome"
44
+ onEmailSubmit={onEmailSubmit}
45
+ validationErrorText="Please fill required fields"
46
+ />
47
+ );
48
+
49
+ fireEvent.click(screen.getByRole("button", { name: /submit form/i }));
50
+
51
+ await waitFor(() =>
52
+ expect(
53
+ screen.getByText("Please fill required fields")
54
+ ).toBeInTheDocument()
55
+ );
56
+ expect(onEmailSubmit).not.toHaveBeenCalled();
57
+ });
58
+
59
+ it("invokes email, smtp and sms handlers and renders the success message", async () => {
60
+ const onEmailSubmit = jest.fn().mockResolvedValue(undefined);
61
+ const onSmtpSubmit = jest.fn().mockResolvedValue(undefined);
62
+ const onSmsSubmit = jest.fn().mockResolvedValue(undefined);
63
+ const onSubmitSuccess = jest.fn();
64
+
65
+ render(
66
+ <DynamicForm
67
+ fields={fields}
68
+ formSubmitOptions={["email", "sftp", "sms"]}
69
+ emailTemplateName="welcome"
70
+ onEmailSubmit={onEmailSubmit}
71
+ onSmtpSubmit={onSmtpSubmit}
72
+ onSmsSubmit={onSmsSubmit}
73
+ onSubmitSuccess={onSubmitSuccess}
74
+ successMessage={<div>Thanks for reaching out</div>}
75
+ />
76
+ );
77
+
78
+ fireEvent.change(screen.getByLabelText("First Name"), {
79
+ target: { value: "Jane", name: "firstName" },
80
+ });
81
+ fireEvent.click(screen.getByRole("checkbox"));
82
+
83
+ fireEvent.click(screen.getByRole("button", { name: /submit form/i }));
84
+
85
+ await waitFor(() =>
86
+ expect(screen.getByText("Thanks for reaching out")).toBeInTheDocument()
87
+ );
88
+
89
+ expect(onEmailSubmit).toHaveBeenCalledTimes(1);
90
+ expect(onSmtpSubmit).toHaveBeenCalledTimes(1);
91
+ expect(onSmsSubmit).toHaveBeenCalledTimes(1);
92
+ expect(onSubmitSuccess).toHaveBeenCalledTimes(1);
93
+ });
94
+
95
+ it("renders the error node when every action fails", async () => {
96
+ const onEmailSubmit = jest.fn().mockRejectedValue(new Error("boom"));
97
+
98
+ render(
99
+ <DynamicForm
100
+ fields={fields}
101
+ formSubmitOptions={["email"]}
102
+ emailTemplateName="welcome"
103
+ onEmailSubmit={onEmailSubmit}
104
+ errorMessage={<div>Something went wrong</div>}
105
+ />
106
+ );
107
+
108
+ fireEvent.change(screen.getByLabelText("First Name"), {
109
+ target: { value: "Jane", name: "firstName" },
110
+ });
111
+ fireEvent.click(screen.getByRole("checkbox"));
112
+ fireEvent.click(screen.getByRole("button", { name: /submit form/i }));
113
+
114
+ await waitFor(() =>
115
+ expect(screen.getByText("Something went wrong")).toBeInTheDocument()
116
+ );
117
+ });
118
+
119
+ const submit = () =>
120
+ fireEvent.click(screen.getByRole("button", { name: /submit form/i }));
121
+
122
+ it("applies TN/IP preset validation before submitting", async () => {
123
+ const onEmailSubmit = jest.fn().mockResolvedValue(undefined);
124
+ const presetFields: DynamicFormItem[] = [
125
+ {
126
+ itemType: "formField",
127
+ fieldId: "contact",
128
+ fieldType: "Text",
129
+ displayName: "Contact",
130
+ applyPresetValidations: true,
131
+ selectPresetValidationsToApply: "TN-IP-Validation",
132
+ },
133
+ ];
134
+ render(
135
+ <DynamicForm
136
+ fields={presetFields}
137
+ formSubmitOptions={["email"]}
138
+ emailTemplateName="welcome"
139
+ onEmailSubmit={onEmailSubmit}
140
+ successMessage={<div>Done</div>}
141
+ />
142
+ );
143
+
144
+ fireEvent.change(screen.getByLabelText("Contact"), {
145
+ target: { value: "+1 555-123-4567", name: "contact" },
146
+ });
147
+ submit();
148
+
149
+ await waitFor(() => expect(onEmailSubmit).toHaveBeenCalledTimes(1));
150
+ const body = onEmailSubmit.mock.calls[0][0];
151
+ expect(JSON.parse(body.formData)).toMatchObject({
152
+ form1: { contact: "5551234567" },
153
+ });
154
+ });
155
+
156
+ it("shows a validation error when a required-one group is entirely empty", async () => {
157
+ const groupFields: DynamicFormItem[] = [
158
+ {
159
+ itemType: "formField",
160
+ fieldId: "opt1",
161
+ fieldType: "Text",
162
+ displayName: "Option 1",
163
+ validationGroupId: "grp",
164
+ validationType: "At least One Required",
165
+ },
166
+ {
167
+ itemType: "formField",
168
+ fieldId: "opt2",
169
+ fieldType: "Text",
170
+ displayName: "Option 2",
171
+ validationGroupId: "grp",
172
+ validationType: "At least One Required",
173
+ },
174
+ ];
175
+ render(
176
+ <DynamicForm
177
+ fields={groupFields}
178
+ formSubmitOptions={["email"]}
179
+ emailTemplateName="welcome"
180
+ onEmailSubmit={jest.fn()}
181
+ validationErrorText="Fill at least one"
182
+ />
183
+ );
184
+ submit();
185
+ await waitFor(() =>
186
+ expect(screen.getByText("Fill at least one")).toBeInTheDocument()
187
+ );
188
+ });
189
+
190
+ it("treats a partial action failure as a success", async () => {
191
+ const onEmailSubmit = jest.fn().mockResolvedValue(undefined);
192
+ const onSmsSubmit = jest.fn().mockRejectedValue(new Error("sms down"));
193
+ render(
194
+ <DynamicForm
195
+ fields={fields}
196
+ formSubmitOptions={["email", "sms"]}
197
+ emailTemplateName="welcome"
198
+ onEmailSubmit={onEmailSubmit}
199
+ onSmsSubmit={onSmsSubmit}
200
+ successMessage={<div>Partially sent</div>}
201
+ />
202
+ );
203
+ fireEvent.change(screen.getByLabelText("First Name"), {
204
+ target: { value: "Jane", name: "firstName" },
205
+ });
206
+ fireEvent.click(screen.getByRole("checkbox"));
207
+ submit();
208
+ await waitFor(() =>
209
+ expect(screen.getByText("Partially sent")).toBeInTheDocument()
210
+ );
211
+ });
212
+
213
+ it("runs the post handler, buildPayload and analytics tracking", async () => {
214
+ const postHandler = jest.fn().mockResolvedValue(undefined);
215
+ const buildPayload = jest.fn((base: any) => ({ ...base, custom: true }));
216
+ const onTrack = jest.fn();
217
+ render(
218
+ <DynamicForm
219
+ fields={[
220
+ {
221
+ itemType: "formField",
222
+ fieldId: "name",
223
+ fieldType: "Text",
224
+ displayName: "Name",
225
+ },
226
+ ]}
227
+ formSubmitOptions={["post"]}
228
+ actionHandlers={{ post: postHandler }}
229
+ buildPayload={buildPayload}
230
+ onTrack={onTrack}
231
+ segmentEventId="evt"
232
+ successMessage={<div>Posted</div>}
233
+ />
234
+ );
235
+ submit();
236
+ await waitFor(() => expect(postHandler).toHaveBeenCalledTimes(1));
237
+ expect(buildPayload).toHaveBeenCalled();
238
+ expect(onTrack).toHaveBeenCalledWith("evt", expect.any(Object));
239
+ });
240
+
241
+ it("reports an unsupported action and renders the error node", async () => {
242
+ const onError = jest.fn();
243
+ render(
244
+ <DynamicForm
245
+ fields={[
246
+ {
247
+ itemType: "formField",
248
+ fieldId: "name",
249
+ fieldType: "Text",
250
+ displayName: "Name",
251
+ },
252
+ ]}
253
+ formSubmitOptions={["voice"]}
254
+ onError={onError}
255
+ errorMessage={<div>Action unavailable</div>}
256
+ />
257
+ );
258
+ submit();
259
+ await waitFor(() =>
260
+ expect(screen.getByText("Action unavailable")).toBeInTheDocument()
261
+ );
262
+ expect(onError).toHaveBeenCalledWith(
263
+ expect.objectContaining({ type: "unsupported_action" })
264
+ );
265
+ });
266
+
267
+ it("passes shouldShowPopup=true when a popup option is selected", async () => {
268
+ const onEmailSubmit = jest.fn().mockResolvedValue(undefined);
269
+ const onSubmitSuccess = jest.fn();
270
+ render(
271
+ <DynamicForm
272
+ fields={[
273
+ {
274
+ itemType: "formField",
275
+ fieldId: "topic",
276
+ fieldType: "Radio",
277
+ displayName: "Topic",
278
+ dataSource: [{ label: "Escalate", value: "escalate", popup: true }],
279
+ },
280
+ ]}
281
+ formSubmitOptions={["email"]}
282
+ emailTemplateName="welcome"
283
+ onEmailSubmit={onEmailSubmit}
284
+ onSubmitSuccess={onSubmitSuccess}
285
+ successMessage={<div>Sent</div>}
286
+ />
287
+ );
288
+ fireEvent.click(screen.getByText("Escalate"));
289
+ submit();
290
+ await waitFor(() =>
291
+ expect(onSubmitSuccess).toHaveBeenCalledWith(
292
+ expect.objectContaining({ topic: "escalate" }),
293
+ { shouldShowPopup: true }
294
+ )
295
+ );
296
+ });
297
+
298
+ it("handles an unexpected error thrown while building the payload", async () => {
299
+ const buildPayload = jest.fn(() => {
300
+ throw new Error("boom");
301
+ });
302
+ render(
303
+ <DynamicForm
304
+ fields={[
305
+ {
306
+ itemType: "formField",
307
+ fieldId: "name",
308
+ fieldType: "Text",
309
+ displayName: "Name",
310
+ },
311
+ ]}
312
+ formSubmitOptions={["email"]}
313
+ emailTemplateName="welcome"
314
+ onEmailSubmit={jest.fn()}
315
+ buildPayload={buildPayload}
316
+ errorMessage={<div>Unexpected failure</div>}
317
+ />
318
+ );
319
+ submit();
320
+ await waitFor(() =>
321
+ expect(screen.getByText("Unexpected failure")).toBeInTheDocument()
322
+ );
323
+ });
324
+
325
+ it("sends a multipart payload when files are attached", async () => {
326
+ global.URL.createObjectURL = jest.fn(() => "blob:preview");
327
+ global.URL.revokeObjectURL = jest.fn();
328
+ const onEmailSubmit = jest.fn().mockResolvedValue(undefined);
329
+ const { container } = render(
330
+ <DynamicForm
331
+ fields={[
332
+ {
333
+ itemType: "formField",
334
+ fieldId: "attachment",
335
+ fieldType: "Imageupload",
336
+ displayName: "Attachment",
337
+ },
338
+ ]}
339
+ formSubmitOptions={["email"]}
340
+ emailTemplateName="welcome"
341
+ onEmailSubmit={onEmailSubmit}
342
+ successMessage={<div>Uploaded</div>}
343
+ />
344
+ );
345
+ const fileInput = container.querySelector(
346
+ 'input[type="file"]'
347
+ ) as HTMLInputElement;
348
+ const file = new File(["x"], "pic.png", { type: "image/png" });
349
+ Object.defineProperty(file, "size", { value: 100 });
350
+ fireEvent.change(fileInput, { target: { files: [file] } });
351
+ submit();
352
+ await waitFor(() => expect(onEmailSubmit).toHaveBeenCalledTimes(1));
353
+ expect(onEmailSubmit.mock.calls[0][0]).toBeInstanceOf(FormData);
354
+ });
355
+ });