@akinon/pz-theme 2.0.95-beta.1 → 2.0.95-rc.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 (60) hide show
  1. package/CHANGELOG.md +33 -15
  2. package/package.json +4 -4
  3. package/src/__tests__/auth-commerce-events.test.tsx +1 -5
  4. package/src/__tests__/auth-form-error-block.test.tsx +0 -2
  5. package/src/__tests__/auth-form-validation.test.tsx +0 -2
  6. package/src/__tests__/auth-slot-coverage.test.ts +1 -9
  7. package/src/__tests__/auth-step-title.test.tsx +0 -2
  8. package/src/__tests__/detached-submit-state.test.tsx +2 -80
  9. package/src/__tests__/forgot-password-composed.test.tsx +0 -2
  10. package/src/__tests__/otp-form-composed.test.tsx +0 -2
  11. package/src/__tests__/password-reset-step.test.tsx +0 -2
  12. package/src/__tests__/register-form-composed.test.tsx +4 -253
  13. package/src/__tests__/registry-parity.test.ts +0 -12
  14. package/src/auth/auth-field-errors.tsx +0 -31
  15. package/src/blocks/account-button-block.tsx +0 -25
  16. package/src/blocks/add-to-cart-block.tsx +0 -15
  17. package/src/blocks/auth-field-block.tsx +3 -13
  18. package/src/blocks/auth-flow-block.tsx +3 -45
  19. package/src/blocks/auth-form-block.tsx +54 -337
  20. package/src/blocks/block-renderer-registry.tsx +0 -8
  21. package/src/blocks/button-block.tsx +13 -43
  22. package/src/blocks/change-password-form-block.tsx +0 -15
  23. package/src/blocks/forgot-password-block.tsx +9 -132
  24. package/src/blocks/newsletter-form-block.tsx +0 -16
  25. package/src/blocks/otp-form-block.tsx +67 -161
  26. package/src/blocks/profile-form-block.tsx +0 -13
  27. package/src/blocks/register-form-block.tsx +110 -362
  28. package/src/chrome/__tests__/theme-config-source.test.ts +1 -2
  29. package/src/chrome/placeholder-slugs.ts +0 -3
  30. package/src/chrome/route-slug.ts +0 -11
  31. package/src/index.ts +0 -10
  32. package/src/overlays/__tests__/modal-registry.test.ts +1 -5
  33. package/src/overlays/modal-chrome-css.ts +1 -8
  34. package/src/overlays/modal-preview-host.tsx +2 -7
  35. package/src/overlays/modal-registry.ts +1 -38
  36. package/src/overlays/overlay-surface.tsx +0 -23
  37. package/src/overlays/overlay-tokens.ts +0 -12
  38. package/src/overlays/theme-modal-mount.tsx +6 -85
  39. package/src/utils/visibility-rules.ts +0 -114
  40. package/src/__tests__/auth-captcha.test.tsx +0 -261
  41. package/src/__tests__/auth-destination.test.tsx +0 -348
  42. package/src/__tests__/auth-error-reading.test.ts +0 -232
  43. package/src/__tests__/auth-flow-scope.test.tsx +0 -162
  44. package/src/__tests__/auth-form-success-block.test.tsx +0 -429
  45. package/src/__tests__/block-action-audience.test.ts +0 -154
  46. package/src/__tests__/form-state-composition.test.tsx +0 -484
  47. package/src/__tests__/modal-login-close.test.tsx +0 -366
  48. package/src/__tests__/reset-password-composed.test.tsx +0 -385
  49. package/src/auth/auth-captcha.tsx +0 -125
  50. package/src/auth/auth-destination.ts +0 -168
  51. package/src/auth/form-state-context.tsx +0 -170
  52. package/src/auth/form-success-context.tsx +0 -56
  53. package/src/auth/read-auth-error.ts +0 -332
  54. package/src/blocks/auth-success-block.tsx +0 -85
  55. package/src/blocks/captcha-block.tsx +0 -61
  56. package/src/blocks/form-state-block.tsx +0 -84
  57. package/src/blocks/reset-password-block.tsx +0 -717
  58. package/src/overlays/__tests__/modal-loading.test.tsx +0 -235
  59. package/src/overlays/modal-host-context.tsx +0 -66
  60. package/src/overlays/overlay-loading.tsx +0 -144
@@ -1,484 +0,0 @@
1
- import { fireEvent, render, screen, waitFor } from '@testing-library/react';
2
- import React from 'react';
3
-
4
- import AuthFormBlock from '../blocks/auth-form-block';
5
- import ForgotPasswordBlock from '../blocks/forgot-password-block';
6
- import ResetPasswordBlock from '../blocks/reset-password-block';
7
- import {
8
- containsFormStateBlock,
9
- matchesFormState
10
- } from '../auth/form-state-context';
11
- import { __resetAuthFlowBusForTests } from '../auth/auth-flow-bus';
12
- import { Block } from '../theme-block';
13
-
14
- /**
15
- * A form's OTHER screens, composed the way its normal one already was.
16
- *
17
- * The fields, the message and the CTA had been blocks for a while. Every other
18
- * state was still the form's own markup with one authored string in it: one
19
- * sentence for the confirmation, one for a dead link, one for "checking", and
20
- * a busy moment nobody could dress at all. So a brand could build its login
21
- * screen and could not build the screen it turns into — which is what the
22
- * complaint "I want to build these screens one by one, from the success
23
- * message to the error; the success message should be a text if I want and a
24
- * component if I want" is about.
25
- *
26
- * A Form State container is that: a named column the form draws when it is on
27
- * that screen, holding whatever the designer puts in it.
28
- */
29
-
30
- let forgotState = { isLoading: false, isSuccess: false };
31
- const forgotPassword = jest.fn(() => ({ unwrap: () => Promise.resolve({}) }));
32
-
33
- const passwordReset = jest.fn();
34
- const unwrap = jest.fn();
35
- let resetState = { isLoading: false, isSuccess: false };
36
- let validation: {
37
- data?: { validlink: boolean };
38
- isLoading: boolean;
39
- isError: boolean;
40
- } = { data: { validlink: true }, isLoading: false, isError: false };
41
-
42
- /** One mock for both forms; see the note in auth-form-success-block.test.tsx. */
43
- jest.mock('@akinon/next/data/client/user', () => {
44
- const handler = {
45
- get(_target: unknown, prop: string) {
46
- if (prop === '__esModule') return true;
47
- if (typeof prop !== 'string') return undefined;
48
- if (prop === 'useForgotPasswordMutation') {
49
- return () => [forgotPassword, forgotState];
50
- }
51
- if (prop === 'usePasswordResetMutation') {
52
- return () => [
53
- (...args: unknown[]) => {
54
- passwordReset(...args);
55
-
56
- return { unwrap };
57
- },
58
- resetState
59
- ];
60
- }
61
- if (prop === 'useGetValidatePasswordResetTokenQuery') {
62
- return () => validation;
63
- }
64
- if (prop.endsWith('Mutation')) {
65
- return () => [
66
- jest.fn(() => ({ unwrap: () => Promise.resolve({}) })),
67
- { isLoading: false, isSuccess: false, isError: false }
68
- ];
69
- }
70
- if (prop.endsWith('Query')) {
71
- return () => ({
72
- data: undefined,
73
- isLoading: false,
74
- isSuccess: true,
75
- refetch: jest.fn()
76
- });
77
- }
78
- return () => undefined;
79
- }
80
- };
81
-
82
- return new Proxy({}, handler);
83
- });
84
-
85
- const signIn = jest.fn(() => Promise.resolve({ error: null, url: '/' }));
86
-
87
- jest.mock('next-auth/react', () => ({
88
- signIn: (...args: unknown[]) => signIn(...args),
89
- useSession: () => ({ data: null, status: 'unauthenticated' })
90
- }));
91
-
92
- jest.mock('next/navigation', () => ({
93
- useParams: () => ({ id: ['tok', 'abc123'] }),
94
- usePathname: () => '/users/reset/tok/abc123',
95
- useSearchParams: () => new URLSearchParams(),
96
- useRouter: () => ({ push: jest.fn(), refresh: jest.fn(), replace: jest.fn() })
97
- }));
98
-
99
- const block = (overrides: Partial<Block>): Block =>
100
- ({
101
- id: 'blk',
102
- type: 'text',
103
- label: 'Block',
104
- value: '',
105
- order: 0,
106
- hidden: false,
107
- properties: {},
108
- styles: {},
109
- ...overrides
110
- }) as Block;
111
-
112
- const text = (id: string, value: string, order = 0) =>
113
- block({ id, type: 'text', label: 'Text', value, order });
114
-
115
- /** A screen the designer composed: a container plus whatever is inside it. */
116
- const stateBox = (
117
- formState: string,
118
- children: Block[],
119
- order = 0,
120
- overrides: Partial<Block> = {}
121
- ) =>
122
- block({
123
- id: `box-${formState}`,
124
- type: 'form-state',
125
- label: 'Form State',
126
- properties: { formState },
127
- blocks: children,
128
- order,
129
- ...overrides
130
- });
131
-
132
- const emailField = block({
133
- id: 'field-email',
134
- type: 'auth-field',
135
- label: 'E-mail Field',
136
- properties: { fieldType: 'email', label: 'E-mail' },
137
- order: 0
138
- });
139
-
140
- const renderForgot = (
141
- children: Block[],
142
- properties: Record<string, unknown> = {},
143
- options: { isDesigner?: boolean; selectedBlockId?: string } = {}
144
- ) =>
145
- render(
146
- <ForgotPasswordBlock
147
- block={
148
- block({
149
- id: 'forgot-1',
150
- type: 'forgot-password',
151
- label: 'Forgot Password',
152
- properties: { showSubmit: false, ...properties },
153
- blocks: children
154
- }) as Block
155
- }
156
- placeholderId="placeholder-1"
157
- sectionId="section-1"
158
- currentBreakpoint="desktop"
159
- isDesigner={options.isDesigner ?? false}
160
- selectedBlockId={options.selectedBlockId}
161
- />
162
- );
163
-
164
- const renderReset = (
165
- children: Block[],
166
- properties: Record<string, unknown> = {}
167
- ) =>
168
- render(
169
- <ResetPasswordBlock
170
- block={
171
- block({
172
- id: 'reset-1',
173
- type: 'reset-password',
174
- label: 'Reset Password',
175
- properties: { showSubmit: false, ...properties },
176
- blocks: children
177
- }) as Block
178
- }
179
- placeholderId="placeholder-1"
180
- sectionId="section-1"
181
- currentBreakpoint="desktop"
182
- />
183
- );
184
-
185
- beforeEach(() => {
186
- __resetAuthFlowBusForTests();
187
- forgotState = { isLoading: false, isSuccess: false };
188
- resetState = { isLoading: false, isSuccess: false };
189
- validation = { data: { validlink: true }, isLoading: false, isError: false };
190
- forgotPassword.mockClear();
191
- forgotPassword.mockImplementation(() => ({
192
- unwrap: () => Promise.resolve({})
193
- }));
194
- passwordReset.mockClear();
195
- unwrap.mockReset();
196
- unwrap.mockResolvedValue(undefined);
197
- signIn.mockClear();
198
- signIn.mockImplementation(() => Promise.resolve({ error: null, url: '/' }));
199
- });
200
-
201
- describe('which screen a container belongs to', () => {
202
- const filling = { screen: 'form' as const, busy: false, hasError: false };
203
-
204
- it('keeps the form on screen while it is busy and while it was refused', () => {
205
- // The rule the whole shape exists for: a single exclusive state would take
206
- // the fields away the moment commerce answered with a message.
207
- expect(matchesFormState('form', { ...filling, busy: true })).toBe(true);
208
- expect(matchesFormState('form', { ...filling, hasError: true })).toBe(true);
209
- });
210
-
211
- it('shows the busy and refused screens only when they are true', () => {
212
- expect(matchesFormState('busy', filling)).toBe(false);
213
- expect(matchesFormState('busy', { ...filling, busy: true })).toBe(true);
214
- expect(matchesFormState('error', filling)).toBe(false);
215
- expect(matchesFormState('error', { ...filling, hasError: true })).toBe(
216
- true
217
- );
218
- });
219
-
220
- it('does not carry them onto a finished form', () => {
221
- const done = { screen: 'success' as const, busy: true, hasError: true };
222
-
223
- // "Busy" and "refused" happen WHILE filling. On the confirmation they are
224
- // leftovers, and a spinner over a success screen is a bug.
225
- expect(matchesFormState('busy', done)).toBe(false);
226
- expect(matchesFormState('error', done)).toBe(false);
227
- expect(matchesFormState('form', done)).toBe(false);
228
- expect(matchesFormState('success', done)).toBe(true);
229
- });
230
-
231
- it('treats the four screens as exclusive', () => {
232
- const expired = { screen: 'expired' as const, busy: false, hasError: false };
233
-
234
- expect(matchesFormState('expired', expired)).toBe(true);
235
- expect(matchesFormState('success', expired)).toBe(false);
236
- expect(matchesFormState('checking', expired)).toBe(false);
237
- });
238
-
239
- it('draws nothing for a state it does not recognise', () => {
240
- // A container written by a newer editor was scoped deliberately; drawing it
241
- // on every screen would be worse than drawing it nowhere.
242
- expect(matchesFormState('whenever', filling)).toBe(false);
243
- });
244
- });
245
-
246
- describe('finding a composed screen', () => {
247
- it('finds one at any depth, and only for the state asked about', () => {
248
- const children = [
249
- block({
250
- id: 'group',
251
- type: 'group',
252
- blocks: [stateBox('success', [text('t', 'Done')])]
253
- })
254
- ];
255
-
256
- expect(containsFormStateBlock(children, 'success')).toBe(true);
257
- expect(containsFormStateBlock(children, 'expired')).toBe(false);
258
- });
259
-
260
- it('ignores a hidden container, which draws nothing wherever it sits', () => {
261
- const children = [
262
- stateBox('success', [text('t', 'Done')], 0, { hidden: true })
263
- ];
264
-
265
- // Standing the built-in confirmation down for a container that draws
266
- // nothing would leave the shopper with no answer at all.
267
- expect(containsFormStateBlock(children, 'success')).toBe(false);
268
- });
269
- });
270
-
271
- describe('a password-reset request whose screens are composed', () => {
272
- const composition = [
273
- stateBox('form', [emailField], 0),
274
- stateBox('success', [text('done', 'Kutunuzu kontrol edin.')], 1)
275
- ];
276
-
277
- it('shows the form screen and nothing else before anything happens', () => {
278
- const { container } = renderForgot(composition);
279
-
280
- expect(container.querySelector('input[name="email"]')).not.toBeNull();
281
- expect(screen.queryByText('Kutunuzu kontrol edin.')).toBeNull();
282
- });
283
-
284
- it('swaps to the composed confirmation, fields and all', () => {
285
- forgotState = { isLoading: false, isSuccess: true };
286
-
287
- const { container } = renderForgot(composition, {
288
- successLabel: 'Bir bağlantı yolda.'
289
- });
290
-
291
- // The composition's own screen — a Text block here, but it could as well be
292
- // an icon, a heading and a Button, which is the whole point.
293
- expect(screen.getByText('Kutunuzu kontrol edin.')).toBeInTheDocument();
294
- // The fields went with the screen they were scoped to.
295
- expect(container.querySelector('input[name="email"]')).toBeNull();
296
- // And the form's own sentence is not drawn on top of it.
297
- expect(screen.queryByText('Bir bağlantı yolda.')).toBeNull();
298
- });
299
-
300
- it('drops the built-in submit, which would be an orphan there', () => {
301
- forgotState = { isLoading: false, isSuccess: true };
302
-
303
- const { container } = renderForgot(composition, { showSubmit: true });
304
-
305
- expect(
306
- container.querySelector('[data-forgot-password-submit]')
307
- ).toBeNull();
308
- });
309
-
310
- it('keeps the fields when a refusal comes back', async () => {
311
- forgotPassword.mockImplementation(() => ({
312
- unwrap: () =>
313
- Promise.reject({ data: { non_field_errors: ['Bilinmeyen adres.'] } })
314
- }));
315
-
316
- const { container } = renderForgot([
317
- ...composition,
318
- stateBox('error', [text('oops', 'Bir şeyler ters gitti.')], 2)
319
- ]);
320
-
321
- fireEvent.change(container.querySelector('input[name="email"]')!, {
322
- target: { value: 'ada@example.com' }
323
- });
324
- fireEvent.submit(container.querySelector('form')!);
325
-
326
- await waitFor(() =>
327
- expect(screen.getByText('Bir şeyler ters gitti.')).toBeInTheDocument()
328
- );
329
- // The one thing a refusal must never do: take away what has to be corrected.
330
- expect(container.querySelector('input[name="email"]')).not.toBeNull();
331
- });
332
-
333
- it('shows the busy screen while the request is in flight', () => {
334
- forgotState = { isLoading: true, isSuccess: false };
335
-
336
- renderForgot([
337
- ...composition,
338
- stateBox('busy', [text('wait', 'Gönderiliyor…')], 2)
339
- ]);
340
-
341
- expect(screen.getByText('Gönderiliyor…')).toBeInTheDocument();
342
- });
343
-
344
- it('draws a child that is not a container on every screen', () => {
345
- forgotState = { isLoading: false, isSuccess: true };
346
-
347
- renderForgot([...composition, text('logo', 'GANT', 2)]);
348
-
349
- // Shared chrome: a logo, a title, a support line — the same rule the OTP
350
- // form applies to a child that is not a phase.
351
- expect(screen.getByText('GANT')).toBeInTheDocument();
352
- expect(screen.getByText('Kutunuzu kontrol edin.')).toBeInTheDocument();
353
- });
354
-
355
- it('shows a screen being edited in the canvas, previewed or not', () => {
356
- renderForgot(composition, {}, {
357
- isDesigner: true,
358
- selectedBlockId: 'done'
359
- });
360
-
361
- // The form is in its normal state, but the designer is inside the success
362
- // screen — so it is drawn, or it could not be dressed at all.
363
- expect(screen.getByText('Kutunuzu kontrol edin.')).toBeInTheDocument();
364
- });
365
- });
366
-
367
- describe('the screen the reset mail lands on', () => {
368
- const passwordBox = block({
369
- id: 'field-password',
370
- type: 'auth-field',
371
- label: 'Password',
372
- properties: { fieldType: 'password', label: 'New password' },
373
- order: 0
374
- });
375
-
376
- it('composes the dead-link screen instead of the built-in sentence', () => {
377
- validation = {
378
- data: { validlink: false },
379
- isLoading: false,
380
- isError: false
381
- };
382
-
383
- const { container } = renderReset(
384
- [
385
- stateBox('form', [passwordBox], 0),
386
- stateBox('expired', [text('dead', 'Bağlantının süresi geçmiş.')], 1)
387
- ],
388
- { expiredLabel: 'This link has expired.' }
389
- );
390
-
391
- expect(screen.getByText('Bağlantının süresi geçmiş.')).toBeInTheDocument();
392
- expect(screen.queryByText('This link has expired.')).toBeNull();
393
- // Nothing to submit on a dead link.
394
- expect(container.querySelector('input[name="password"]')).toBeNull();
395
- expect(container.querySelector('[data-reset-password-submit]')).toBeNull();
396
- });
397
-
398
- it('composes the checking screen too', () => {
399
- validation = { data: undefined, isLoading: true, isError: false };
400
-
401
- renderReset([
402
- stateBox('form', [passwordBox], 0),
403
- stateBox('checking', [text('wait', 'Bağlantı kontrol ediliyor…')], 1)
404
- ]);
405
-
406
- expect(
407
- screen.getByText('Bağlantı kontrol ediliyor…')
408
- ).toBeInTheDocument();
409
- });
410
-
411
- it('still falls back to the built-in screens where none was composed', () => {
412
- validation = {
413
- data: { validlink: false },
414
- isLoading: false,
415
- isError: false
416
- };
417
-
418
- renderReset([stateBox('form', [passwordBox], 0)], {
419
- expiredLabel: 'This link has expired.'
420
- });
421
-
422
- // The fence: a composition that scoped only its form keeps every screen it
423
- // had before.
424
- expect(screen.getByText('This link has expired.')).toBeInTheDocument();
425
- });
426
- });
427
-
428
- describe('a login form, which has no confirmation to compose', () => {
429
- it('scopes the refusal without taking the fields away', async () => {
430
- signIn.mockImplementation(() =>
431
- Promise.resolve({
432
- error: 'CredentialsSignin',
433
- code: JSON.stringify([
434
- { type: 'non_field_errors', data: ['E-posta veya şifre hatalı.'] }
435
- ])
436
- })
437
- );
438
-
439
- const { container } = render(
440
- <AuthFormBlock
441
- block={
442
- block({
443
- id: 'login-1',
444
- type: 'auth-form',
445
- label: 'Auth Form',
446
- properties: { showSubmit: true },
447
- blocks: [
448
- stateBox(
449
- 'form',
450
- [
451
- emailField,
452
- block({
453
- id: 'field-password',
454
- type: 'auth-field',
455
- properties: { fieldType: 'password', label: 'Password' },
456
- order: 1
457
- })
458
- ],
459
- 0
460
- ),
461
- stateBox('error', [text('box', 'Tekrar deneyin.')], 1)
462
- ]
463
- }) as Block
464
- }
465
- placeholderId="placeholder-1"
466
- sectionId="section-1"
467
- currentBreakpoint="desktop"
468
- />
469
- );
470
-
471
- fireEvent.change(container.querySelector('input[name="email"]')!, {
472
- target: { value: 'ada@example.com' }
473
- });
474
- fireEvent.change(container.querySelector('input[name="password"]')!, {
475
- target: { value: 'hunter2' }
476
- });
477
- fireEvent.submit(container.querySelector('form')!);
478
-
479
- await waitFor(() =>
480
- expect(screen.getByText('Tekrar deneyin.')).toBeInTheDocument()
481
- );
482
- expect(container.querySelector('input[name="email"]')).not.toBeNull();
483
- });
484
- });