@akinon/pz-theme 2.0.165-rc.0 → 2.0.165

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.
@@ -0,0 +1,795 @@
1
+ import { fireEvent, render, waitFor } from '@testing-library/react';
2
+ import React from 'react';
3
+
4
+ import ProfileFormBlock, {
5
+ readProfilePayload
6
+ } from '../blocks/profile-form-block';
7
+ import RegisterFormBlock from '../blocks/register-form-block';
8
+ import {
9
+ daysInMonth,
10
+ joinIsoDate,
11
+ splitIsoDate
12
+ } from '../blocks/birth-date-selects';
13
+ import { AUTH_CONSENT_NAMES } from '../blocks/auth-consent-block';
14
+ import { Block } from '../theme-block';
15
+
16
+ /**
17
+ * A profile form whose rows are blocks.
18
+ *
19
+ * The fixed profile form draws name, e-mail, phone and a native date input and
20
+ * nothing else, so a brand that asks for gender, a day/month/year birth date or
21
+ * its contact consents had to hand-write HTML and wire it with its own script.
22
+ * Composed, the form draws `auth-field` and `auth-consent` children instead,
23
+ * starts each one from the stored profile and saves what the visible ones say.
24
+ *
25
+ * What these cases pin is the save: a consent record is a legal record, so a
26
+ * tick the shopper never saw must never be written, and one they did see must
27
+ * be written exactly as they left it.
28
+ */
29
+
30
+ const PROFILE = {
31
+ first_name: 'Ada',
32
+ last_name: 'Lovelace',
33
+ email: 'ada@example.com',
34
+ phone: '5551234567',
35
+ date_of_birth: '1990-04-07',
36
+ gender: 'female',
37
+ email_allowed: true,
38
+ sms_allowed: false,
39
+ call_allowed: null,
40
+ whatsapp_allowed: true
41
+ };
42
+
43
+ let profileResult: { data: unknown; isLoading: boolean; isError?: boolean } = {
44
+ data: PROFILE,
45
+ isLoading: false
46
+ };
47
+ const updateProfile = jest.fn((_body: Record<string, unknown>) => ({
48
+ unwrap: () => Promise.resolve({})
49
+ }));
50
+
51
+ jest.mock('@akinon/next/data/client/account', () => ({
52
+ useGetProfileInfoQuery: () => profileResult,
53
+ useUpdateProfileMutation: () => [
54
+ (body: Record<string, unknown>) => updateProfile(body),
55
+ { isLoading: false }
56
+ ]
57
+ }));
58
+
59
+ let sessionStatus = 'authenticated';
60
+ const signIn = jest.fn(() => Promise.resolve({ error: null, url: '/' }));
61
+
62
+ jest.mock('next-auth/react', () => ({
63
+ signIn: (...args: unknown[]) => signIn(...(args as [])),
64
+ useSession: () => ({ data: null, status: sessionStatus })
65
+ }));
66
+
67
+ jest.mock('next/navigation', () => ({
68
+ usePathname: () => '/',
69
+ useSearchParams: () => new URLSearchParams(),
70
+ useRouter: () => ({ push: jest.fn(), refresh: jest.fn(), replace: jest.fn() })
71
+ }));
72
+
73
+ const block = (overrides: Partial<Block>): Block =>
74
+ ({
75
+ id: 'blk',
76
+ type: 'text',
77
+ label: 'Block',
78
+ value: '',
79
+ order: 0,
80
+ hidden: false,
81
+ properties: {},
82
+ styles: {},
83
+ ...overrides
84
+ }) as Block;
85
+
86
+ const field = (
87
+ fieldType: string,
88
+ order: number,
89
+ properties: Record<string, unknown> = {}
90
+ ): Block =>
91
+ block({
92
+ id: `field-${fieldType}`,
93
+ type: 'auth-field',
94
+ label: fieldType,
95
+ properties: { fieldType, required: false, ...properties },
96
+ order
97
+ });
98
+
99
+ const channel = (
100
+ consentType: string,
101
+ order: number,
102
+ overrides: Partial<Block> = {}
103
+ ): Block =>
104
+ block({
105
+ id: `consent-${consentType}`,
106
+ type: 'auth-consent',
107
+ label: consentType,
108
+ properties: { consentType },
109
+ order,
110
+ ...overrides
111
+ });
112
+
113
+ const renderProfile = (
114
+ children: Block[],
115
+ { isDesigner = false, properties = {} } = {}
116
+ ) =>
117
+ render(
118
+ <ProfileFormBlock
119
+ block={
120
+ block({
121
+ id: 'profile-1',
122
+ type: 'profile-form',
123
+ label: 'Profile Form',
124
+ properties,
125
+ blocks: children
126
+ }) as Block
127
+ }
128
+ placeholderId="placeholder-1"
129
+ sectionId="section-1"
130
+ currentBreakpoint="desktop"
131
+ isDesigner={isDesigner}
132
+ />
133
+ );
134
+
135
+ const input = (container: HTMLElement, selector: string) =>
136
+ container.querySelector(selector) as HTMLInputElement;
137
+
138
+ const datePart = (container: HTMLElement, part: string) =>
139
+ container.querySelector(
140
+ `select[data-date-part="${part}"]`
141
+ ) as HTMLSelectElement;
142
+
143
+ const submit = (container: HTMLElement) =>
144
+ fireEvent.submit(container.querySelector('form')!);
145
+
146
+ const lastSaved = () =>
147
+ updateProfile.mock.calls[updateProfile.mock.calls.length - 1][0];
148
+
149
+ beforeEach(() => {
150
+ updateProfile.mockClear();
151
+ signIn.mockClear();
152
+ sessionStatus = 'authenticated';
153
+ profileResult = { data: PROFILE, isLoading: false };
154
+ });
155
+
156
+ describe('composed profile form', () => {
157
+ const COMPOSED = [
158
+ field('first_name', 0),
159
+ field('last_name', 1),
160
+ field('email', 2),
161
+ field('gender', 3, { choiceControl: 'radio' }),
162
+ field('date_of_birth', 4, { dateControl: 'selects' }),
163
+ channel('email', 5),
164
+ channel('sms', 6),
165
+ channel('call', 7)
166
+ ];
167
+
168
+ it('starts every field from the stored profile', () => {
169
+ const { container } = renderProfile(COMPOSED);
170
+
171
+ expect(input(container, 'input[name="first_name"]').value).toBe('Ada');
172
+ expect(input(container, 'input[name="last_name"]').value).toBe('Lovelace');
173
+ expect(input(container, 'input[value="female"]').checked).toBe(true);
174
+ expect(input(container, 'input[value="male"]').checked).toBe(false);
175
+ expect(datePart(container, 'day').value).toBe('7');
176
+ expect(datePart(container, 'month').value).toBe('4');
177
+ expect(datePart(container, 'year').value).toBe('1990');
178
+ expect(input(container, 'input[name="date_of_birth"]').value).toBe(
179
+ '1990-04-07'
180
+ );
181
+ expect(input(container, 'input[name="email_allowed"]').checked).toBe(true);
182
+ expect(input(container, 'input[name="sms_allowed"]').checked).toBe(false);
183
+ // `null` is "never asked", which must not be drawn as an agreement.
184
+ expect(input(container, 'input[name="call_allowed"]').checked).toBe(false);
185
+ });
186
+
187
+ it('shows the e-mail without letting it be edited or sent', async () => {
188
+ const { container } = renderProfile(COMPOSED);
189
+ const email = input(container, 'input[name="email"]');
190
+
191
+ expect(email.value).toBe('ada@example.com');
192
+ expect(email.disabled).toBe(true);
193
+
194
+ submit(container);
195
+
196
+ await waitFor(() => expect(updateProfile).toHaveBeenCalled());
197
+ expect(lastSaved()).not.toHaveProperty('email');
198
+ });
199
+
200
+ it('saves the visible answers, ticks as booleans', async () => {
201
+ const { container } = renderProfile(COMPOSED);
202
+
203
+ fireEvent.click(input(container, 'input[name="sms_allowed"]'));
204
+ fireEvent.click(input(container, 'input[name="email_allowed"]'));
205
+ fireEvent.click(input(container, 'input[value="male"]'));
206
+ fireEvent.change(datePart(container, 'year'), {
207
+ target: { value: '1991' }
208
+ });
209
+
210
+ submit(container);
211
+
212
+ await waitFor(() => expect(updateProfile).toHaveBeenCalledTimes(1));
213
+ expect(lastSaved()).toEqual({
214
+ first_name: 'Ada',
215
+ last_name: 'Lovelace',
216
+ gender: 'male',
217
+ date_of_birth: '1991-04-07',
218
+ email_allowed: false,
219
+ sms_allowed: true,
220
+ // Shown and left unticked: that is the shopper's answer now.
221
+ call_allowed: false
222
+ });
223
+ });
224
+
225
+ it('never writes a consent the composition does not show', async () => {
226
+ const { container } = renderProfile([
227
+ field('first_name', 0),
228
+ channel('email', 1),
229
+ // Switched off in the editor: not drawn, so not the shopper's answer.
230
+ channel('whatsapp', 2, { hidden: true })
231
+ ]);
232
+
233
+ expect(
234
+ container.querySelector('input[name="whatsapp_allowed"]')
235
+ ).toBeNull();
236
+
237
+ submit(container);
238
+
239
+ await waitFor(() => expect(updateProfile).toHaveBeenCalled());
240
+ const saved = lastSaved();
241
+
242
+ expect(saved).not.toHaveProperty('whatsapp_allowed');
243
+ expect(saved).not.toHaveProperty('sms_allowed');
244
+ expect(saved).not.toHaveProperty('call_allowed');
245
+ expect(saved).toEqual({ first_name: 'Ada', email_allowed: true });
246
+ });
247
+
248
+ it('re-reads the stored answer when a tick changes type on the canvas', () => {
249
+ const consentBlock = (consentType: string) =>
250
+ block({
251
+ id: 'profile-1',
252
+ type: 'profile-form',
253
+ label: 'Profile Form',
254
+ blocks: [channel(consentType, 0, { id: 'consent-1' })]
255
+ }) as Block;
256
+ const props = {
257
+ placeholderId: 'placeholder-1',
258
+ sectionId: 'section-1',
259
+ currentBreakpoint: 'desktop',
260
+ isDesigner: true
261
+ };
262
+
263
+ const { container, rerender } = render(
264
+ <ProfileFormBlock block={consentBlock('email')} {...props} />
265
+ );
266
+
267
+ expect(input(container, 'input[type="checkbox"]').checked).toBe(true);
268
+
269
+ // The author switches the same row to Arama, which was never answered.
270
+ rerender(<ProfileFormBlock block={consentBlock('call')} {...props} />);
271
+
272
+ const tick = input(container, 'input[type="checkbox"]');
273
+ expect(tick.name).toBe('call_allowed');
274
+ expect(tick.checked).toBe(false);
275
+ });
276
+
277
+ it('never makes a contact permission mandatory', async () => {
278
+ const { container } = renderProfile([
279
+ channel('sms', 0, {
280
+ // What the editor seeds on every new Consent Row.
281
+ properties: { consentType: 'sms', required: { desktop: true } }
282
+ })
283
+ ]);
284
+
285
+ expect(input(container, 'input[name="sms_allowed"]').required).toBe(false);
286
+
287
+ submit(container);
288
+
289
+ await waitFor(() => expect(updateProfile).toHaveBeenCalled());
290
+ expect(lastSaved()).toEqual({ sms_allowed: false });
291
+ });
292
+
293
+ it('leaves a question that was never answered out instead of blanking it', async () => {
294
+ // Nothing stored and nothing typed: no key is written for any of them.
295
+ profileResult = {
296
+ data: { ...PROFILE, gender: null, date_of_birth: null, phone: null },
297
+ isLoading: false
298
+ };
299
+
300
+ const { container } = renderProfile([
301
+ field('gender', 0, { choiceControl: 'radio' }),
302
+ field('date_of_birth', 1, { dateControl: 'selects' }),
303
+ field('phone', 2)
304
+ ]);
305
+
306
+ submit(container);
307
+
308
+ await waitFor(() =>
309
+ expect(
310
+ container.querySelector('[data-profile-form-success]')
311
+ ).not.toBeNull()
312
+ );
313
+ expect(updateProfile).not.toHaveBeenCalled();
314
+ });
315
+
316
+ it('asks for the rest of a half-picked birth date, under the field', async () => {
317
+ profileResult = {
318
+ data: { ...PROFILE, date_of_birth: null },
319
+ isLoading: false
320
+ };
321
+
322
+ const { container } = renderProfile(
323
+ [field('date_of_birth', 0, { dateControl: 'selects' })],
324
+ { properties: { requiredMessage: 'Zorunlu alan' } }
325
+ );
326
+
327
+ fireEvent.change(datePart(container, 'day'), { target: { value: '12' } });
328
+ submit(container);
329
+
330
+ await waitFor(() =>
331
+ expect(
332
+ container.querySelector('[data-auth-field-error="date_of_birth"]')
333
+ ?.textContent
334
+ ).toBe('Zorunlu alan')
335
+ );
336
+ expect(updateProfile).not.toHaveBeenCalled();
337
+
338
+ // Editing a part clears the field's message, like any other field.
339
+ fireEvent.change(datePart(container, 'month'), { target: { value: '3' } });
340
+
341
+ expect(
342
+ container.querySelector('[data-auth-field-error="date_of_birth"]')
343
+ ).toBeNull();
344
+ });
345
+
346
+ it('reports a required gender left unanswered', async () => {
347
+ profileResult = { data: { ...PROFILE, gender: null }, isLoading: false };
348
+
349
+ const { container } = renderProfile([
350
+ field('gender', 0, { choiceControl: 'radio', required: true })
351
+ ]);
352
+
353
+ submit(container);
354
+
355
+ await waitFor(() =>
356
+ expect(
357
+ container.querySelector('[data-auth-field-error="gender"]')
358
+ ).not.toBeNull()
359
+ );
360
+ expect(updateProfile).not.toHaveBeenCalled();
361
+ });
362
+
363
+ it('waits for the profile before drawing fields a save would write', () => {
364
+ profileResult = { data: undefined, isLoading: true };
365
+
366
+ const { container } = renderProfile(COMPOSED);
367
+
368
+ expect(container.querySelector('form')).toBeNull();
369
+ });
370
+
371
+ it('draws the fields on the canvas without a profile and never saves there', async () => {
372
+ profileResult = { data: undefined, isLoading: false };
373
+
374
+ const { container } = renderProfile(COMPOSED, { isDesigner: true });
375
+
376
+ expect(container.querySelector('input[name="first_name"]')).not.toBeNull();
377
+
378
+ submit(container);
379
+
380
+ await new Promise((resolve) => setTimeout(resolve, 0));
381
+ expect(updateProfile).not.toHaveBeenCalled();
382
+ });
383
+
384
+ it('shows the error line when commerce refuses the save', async () => {
385
+ updateProfile.mockImplementationOnce(() => ({
386
+ unwrap: () => Promise.reject(new Error('400'))
387
+ }));
388
+
389
+ const { container } = renderProfile([field('first_name', 0)], {
390
+ properties: { errorMessage: 'Kaydedilemedi' }
391
+ });
392
+
393
+ submit(container);
394
+
395
+ await waitFor(() =>
396
+ expect(
397
+ container.querySelector('[data-profile-form-error]')?.textContent
398
+ ).toBe('Kaydedilemedi')
399
+ );
400
+ });
401
+
402
+ it('sends a box the shopper emptied, so a stored value can be cleared', async () => {
403
+ const { container } = renderProfile([
404
+ field('first_name', 0),
405
+ field('phone', 1)
406
+ ]);
407
+
408
+ fireEvent.change(input(container, 'input[name="phone"]'), {
409
+ target: { value: '' }
410
+ });
411
+ submit(container);
412
+
413
+ await waitFor(() => expect(updateProfile).toHaveBeenCalled());
414
+ expect(lastSaved()).toEqual({ first_name: 'Ada', phone: '' });
415
+ });
416
+
417
+ it('saves a tick whose native box the brand hid behind its own drawing', async () => {
418
+ const { container } = renderProfile([channel('sms', 0)]);
419
+ const box = input(container, 'input[name="sms_allowed"]');
420
+
421
+ // The usual custom-checkbox technique: the native input is hidden and a
422
+ // styled sibling is what the shopper sees and clicks.
423
+ box.style.display = 'none';
424
+ fireEvent.click(box);
425
+ submit(container);
426
+
427
+ await waitFor(() => expect(updateProfile).toHaveBeenCalled());
428
+ expect(lastSaved()).toEqual({ sms_allowed: true });
429
+ });
430
+
431
+ it('does not write a field whose whole row the design hid', async () => {
432
+ const { container } = renderProfile([
433
+ field('first_name', 0),
434
+ channel('sms', 1)
435
+ ]);
436
+
437
+ (
438
+ container.querySelector('[data-auth-consent]') as HTMLElement
439
+ ).style.display = 'none';
440
+ submit(container);
441
+
442
+ await waitFor(() => expect(updateProfile).toHaveBeenCalled());
443
+ expect(lastSaved()).toEqual({ first_name: 'Ada' });
444
+ });
445
+
446
+ it('starts and saves a custom tick by its own value', async () => {
447
+ profileResult = {
448
+ data: { ...PROFILE, newsletter: 'yes' },
449
+ isLoading: false
450
+ };
451
+
452
+ const { container } = renderProfile([
453
+ block({
454
+ id: 'consent-custom',
455
+ type: 'auth-consent',
456
+ properties: {
457
+ consentType: 'custom',
458
+ name: 'newsletter',
459
+ tickedValue: 'yes'
460
+ },
461
+ order: 0
462
+ })
463
+ ]);
464
+ const tick = input(container, 'input[name="newsletter"]');
465
+
466
+ expect(tick.checked).toBe(true);
467
+
468
+ fireEvent.click(tick);
469
+ submit(container);
470
+
471
+ await waitFor(() => expect(updateProfile).toHaveBeenCalled());
472
+ expect(lastSaved()).toEqual({ newsletter: '' });
473
+ });
474
+
475
+ it('shows a Form State container only in the state it is scoped to', async () => {
476
+ const { container } = renderProfile([
477
+ field('first_name', 0),
478
+ block({
479
+ id: 'state-error',
480
+ type: 'form-state',
481
+ properties: { formState: 'error' },
482
+ order: 1,
483
+ blocks: [
484
+ block({
485
+ id: 'state-error-copy',
486
+ type: 'text',
487
+ value: 'Kaydedilemedi, tekrar deneyin',
488
+ order: 0
489
+ })
490
+ ]
491
+ })
492
+ ]);
493
+
494
+ expect(container.textContent).not.toContain(
495
+ 'Kaydedilemedi, tekrar deneyin'
496
+ );
497
+
498
+ updateProfile.mockImplementationOnce(() => ({
499
+ unwrap: () => Promise.reject(new Error('400'))
500
+ }));
501
+ submit(container);
502
+
503
+ await waitFor(() =>
504
+ expect(container.textContent).toContain('Kaydedilemedi, tekrar deneyin')
505
+ );
506
+ });
507
+
508
+ it('says the profile could not be loaded instead of drawing nothing', () => {
509
+ profileResult = { data: undefined, isLoading: false, isError: true };
510
+
511
+ const { container } = renderProfile([field('first_name', 0)], {
512
+ properties: { errorMessage: 'Bilgileriniz yüklenemedi.' }
513
+ });
514
+
515
+ expect(
516
+ container.querySelector('[data-profile-form-state="unavailable"]')
517
+ ?.textContent
518
+ ).toBe('Bilgileriniz yüklenemedi.');
519
+ expect(container.querySelector('form')).toBeNull();
520
+ });
521
+
522
+ it('keeps the fixed rows, unchanged, when the form has no children', async () => {
523
+ const { container } = renderProfile([], {
524
+ properties: { showName: false, showPhone: false, showEmail: false }
525
+ });
526
+
527
+ expect(container.querySelector('[data-profile-form-composed]')).toBeNull();
528
+ expect(container.querySelector('input[name="first_name"]')).toBeNull();
529
+ expect(input(container, 'input[name="date_of_birth"]').value).toBe(
530
+ '1990-04-07'
531
+ );
532
+
533
+ submit(container);
534
+
535
+ await waitFor(() => expect(updateProfile).toHaveBeenCalled());
536
+ // The fixed form's body is what published compositions send today.
537
+ expect(lastSaved()).toMatchObject({
538
+ first_name: 'Ada',
539
+ last_name: 'Lovelace',
540
+ date_of_birth: '1990-04-07',
541
+ gender: 'female',
542
+ sms_allowed: false,
543
+ email_allowed: true
544
+ });
545
+ });
546
+ });
547
+
548
+ describe('birth date selects', () => {
549
+ it('offers month captions when twelve are given', () => {
550
+ const months =
551
+ 'Ocak,Şubat,Mart,Nisan,Mayıs,Haziran,Temmuz,Ağustos,Eylül,Ekim,Kasım,Aralık';
552
+ const { container } = renderProfile([
553
+ field('date_of_birth', 0, {
554
+ dateControl: 'selects',
555
+ monthLabels: months,
556
+ monthPlaceholder: 'Ay'
557
+ })
558
+ ]);
559
+ const options = Array.from(datePart(container, 'month').options).map(
560
+ (option) => option.textContent
561
+ );
562
+
563
+ expect(options[0]).toBe('Ay');
564
+ expect(options[1]).toBe('Ocak');
565
+ expect(options[12]).toBe('Aralık');
566
+ });
567
+
568
+ it('numbers the months otherwise, and bounds the years by age', () => {
569
+ const { container } = renderProfile([
570
+ field('date_of_birth', 0, {
571
+ dateControl: 'selects',
572
+ minAge: '18',
573
+ maxAge: '20'
574
+ })
575
+ ]);
576
+ const thisYear = new Date().getFullYear();
577
+ const years = Array.from(datePart(container, 'year').options)
578
+ .slice(1)
579
+ .map((option) => option.value);
580
+
581
+ expect(datePart(container, 'month').options[1].textContent).toBe('01');
582
+ // The stored 1990 is kept selectable even though it is out of range.
583
+ expect(years).toEqual([
584
+ String(thisYear - 18),
585
+ String(thisYear - 19),
586
+ String(thisYear - 20),
587
+ '1990'
588
+ ]);
589
+ });
590
+
591
+ it('clears a day the newly picked month cannot hold', () => {
592
+ profileResult = {
593
+ data: { ...PROFILE, date_of_birth: '1990-01-31' },
594
+ isLoading: false
595
+ };
596
+
597
+ const { container } = renderProfile([
598
+ field('date_of_birth', 0, { dateControl: 'selects' })
599
+ ]);
600
+
601
+ fireEvent.change(datePart(container, 'month'), { target: { value: '4' } });
602
+
603
+ expect(datePart(container, 'day').value).toBe('');
604
+ expect(input(container, 'input[name="date_of_birth"]').value).toBe('');
605
+ });
606
+
607
+ it('splits, joins and sizes dates', () => {
608
+ expect(splitIsoDate('1990-04-07T00:00:00Z')).toEqual({
609
+ year: '1990',
610
+ month: '4',
611
+ day: '7'
612
+ });
613
+ expect(splitIsoDate('')).toEqual({ year: '', month: '', day: '' });
614
+ expect(joinIsoDate('7', '4', '1990')).toBe('1990-04-07');
615
+ expect(joinIsoDate('7', '', '1990')).toBe('');
616
+ expect(daysInMonth('', '2')).toBe(29);
617
+ expect(daysInMonth('2023', '2')).toBe(28);
618
+ expect(daysInMonth('', '')).toBe(31);
619
+ });
620
+ });
621
+
622
+ describe('readProfilePayload', () => {
623
+ it('reads the first answer per name and skips empty and disabled controls', () => {
624
+ const form = document.createElement('form');
625
+
626
+ form.innerHTML = `
627
+ <input name="first_name" value="Ada" />
628
+ <input name="first_name" value="Copy" />
629
+ <input name="last_name" value="" />
630
+ <input name="phone" value="555" disabled />
631
+ <input type="radio" name="gender" value="female" />
632
+ <input type="radio" name="gender" value="male" checked />
633
+ <input type="checkbox" name="sms_allowed" />
634
+ <div style="display: none"><input type="checkbox" name="call_allowed" checked /></div>
635
+ <input name="email" value="ada@example.com" />
636
+ `;
637
+ document.body.appendChild(form);
638
+
639
+ expect(readProfilePayload(form)).toEqual({
640
+ first_name: 'Ada',
641
+ gender: 'male',
642
+ sms_allowed: false
643
+ });
644
+
645
+ form.remove();
646
+ });
647
+ });
648
+
649
+ describe('contact-channel ticks in a register form', () => {
650
+ it('lets a tick the design hid fall back to the blanket answer', async () => {
651
+ sessionStatus = 'unauthenticated';
652
+
653
+ const { container } = render(
654
+ <RegisterFormBlock
655
+ block={
656
+ block({
657
+ id: 'register-2',
658
+ type: 'register-form',
659
+ label: 'Register Form',
660
+ blocks: [
661
+ block({
662
+ id: 'f-email',
663
+ type: 'auth-field',
664
+ properties: { fieldType: 'email' },
665
+ order: 0
666
+ }),
667
+ block({
668
+ id: 'f-password',
669
+ type: 'auth-field',
670
+ properties: { fieldType: 'password' },
671
+ order: 1
672
+ }),
673
+ block({
674
+ id: 'c-legal',
675
+ type: 'auth-consent',
676
+ properties: { consentType: 'legal' },
677
+ order: 2
678
+ }),
679
+ block({
680
+ id: 'c-marketing',
681
+ type: 'auth-consent',
682
+ properties: { consentType: 'marketing' },
683
+ order: 3
684
+ }),
685
+ channel('sms', 4)
686
+ ]
687
+ }) as Block
688
+ }
689
+ placeholderId="placeholder-1"
690
+ sectionId="section-1"
691
+ currentBreakpoint="mobile"
692
+ isDesigner={false}
693
+ />
694
+ );
695
+
696
+ // The per-channel row is the desktop design; on this breakpoint it is off.
697
+ (
698
+ container.querySelector('[data-auth-consent="sms"]') as HTMLElement
699
+ ).style.display = 'none';
700
+
701
+ fireEvent.change(input(container, 'input[name="email"]'), {
702
+ target: { value: 'ada@example.com' }
703
+ });
704
+ fireEvent.change(input(container, 'input[name="password"]'), {
705
+ target: { value: 'hunter2' }
706
+ });
707
+ fireEvent.click(
708
+ input(container, `input[name="${AUTH_CONSENT_NAMES.legal}"]`)
709
+ );
710
+ fireEvent.click(
711
+ input(container, `input[name="${AUTH_CONSENT_NAMES.marketing}"]`)
712
+ );
713
+ fireEvent.submit(container.querySelector('form')!);
714
+
715
+ await waitFor(() => expect(signIn).toHaveBeenCalled());
716
+
717
+ const body = (signIn.mock.calls[0] as unknown[])[1] as Record<
718
+ string,
719
+ unknown
720
+ >;
721
+
722
+ // The shopper said yes to marketing; the hidden row does not say no.
723
+ expect(body.sms_allowed).toBe(true);
724
+ });
725
+
726
+ it('answer their own channel as a boolean', async () => {
727
+ sessionStatus = 'unauthenticated';
728
+
729
+ const { container } = render(
730
+ <RegisterFormBlock
731
+ block={
732
+ block({
733
+ id: 'register-1',
734
+ type: 'register-form',
735
+ label: 'Register Form',
736
+ blocks: [
737
+ block({
738
+ id: 'f-email',
739
+ type: 'auth-field',
740
+ properties: { fieldType: 'email' },
741
+ order: 0
742
+ }),
743
+ block({
744
+ id: 'f-password',
745
+ type: 'auth-field',
746
+ properties: { fieldType: 'password' },
747
+ order: 1
748
+ }),
749
+ block({
750
+ id: 'c-legal',
751
+ type: 'auth-consent',
752
+ properties: { consentType: 'legal' },
753
+ order: 2
754
+ }),
755
+ channel('sms', 3),
756
+ channel('whatsapp', 4)
757
+ ]
758
+ }) as Block
759
+ }
760
+ placeholderId="placeholder-1"
761
+ sectionId="section-1"
762
+ currentBreakpoint="desktop"
763
+ isDesigner={false}
764
+ />
765
+ );
766
+
767
+ fireEvent.change(input(container, 'input[name="email"]'), {
768
+ target: { value: 'ada@example.com' }
769
+ });
770
+ fireEvent.change(input(container, 'input[name="password"]'), {
771
+ target: { value: 'hunter2' }
772
+ });
773
+ fireEvent.click(
774
+ input(container, `input[name="${AUTH_CONSENT_NAMES.legal}"]`)
775
+ );
776
+ fireEvent.click(input(container, 'input[name="sms_allowed"]'));
777
+
778
+ // Unticked in a sign-up, whatever any stored profile says.
779
+ expect(input(container, 'input[name="whatsapp_allowed"]').checked).toBe(
780
+ false
781
+ );
782
+
783
+ fireEvent.submit(container.querySelector('form')!);
784
+
785
+ await waitFor(() => expect(signIn).toHaveBeenCalled());
786
+
787
+ const body = (signIn.mock.calls[0] as unknown[])[1] as Record<
788
+ string,
789
+ unknown
790
+ >;
791
+
792
+ expect(body.sms_allowed).toBe(true);
793
+ expect(body.whatsapp_allowed).toBe(false);
794
+ });
795
+ });