@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,366 +0,0 @@
1
- import { act, fireEvent, render, waitFor } from '@testing-library/react';
2
- import React from 'react';
3
-
4
- import AuthFormBlock from '../blocks/auth-form-block';
5
- import NewsletterFormBlock from '../blocks/newsletter-form-block';
6
- import ThemeModalMount from '../overlays/theme-modal-mount';
7
- import { readThemeModals, ThemeModal } from '../overlays/modal-registry';
8
- import {
9
- __resetOverlayBusForTests,
10
- getOpenModalIds,
11
- isModalOpen,
12
- openModal,
13
- registerModalHost
14
- } from '../overlays/overlay-bus';
15
- import { clearAutoOpenRecord } from '../overlays/modal-frequency';
16
- import { __resetModalPreviewForTests } from '../overlays/use-modal-preview';
17
- import { Block } from '../theme-block';
18
-
19
- /**
20
- * A shopper who signs in inside the drawer is done with the drawer.
21
- *
22
- * It used to stay open over the page, and the reason was a default in the
23
- * wrong place: the only close a form could perform was `closeAllModals()`
24
- * behind an authored toggle, and that toggle is off by default — correctly,
25
- * since "close every modal on the page" is not something a form on a plain
26
- * page should do unasked. The live GANT drawer shows how that ends: its OTP
27
- * form has the toggle on and its login and register forms do not, so signing
28
- * in with an e-mail left the drawer covering the homepage.
29
- *
30
- * With the host's id in hand the two defaults stop fighting: a form closes THE
31
- * MODAL IT IS IN, and a form that is not in one still closes nothing.
32
- */
33
-
34
- const signIn = jest.fn(() => Promise.resolve({ error: null, url: '/account' }));
35
-
36
- jest.mock('next-auth/react', () => ({
37
- signIn: (...args: unknown[]) => signIn(...args),
38
- useSession: () => ({ data: null, status: 'unauthenticated' })
39
- }));
40
-
41
- const push = jest.fn();
42
- const refresh = jest.fn();
43
-
44
- jest.mock('next/navigation', () => ({
45
- useRouter: () => ({ push, refresh, replace: jest.fn() }),
46
- useSearchParams: () => new URLSearchParams(),
47
- usePathname: () => '/'
48
- }));
49
-
50
- jest.mock('@akinon/next/hooks/use-localization', () => ({
51
- useLocalization: () => ({ locale: 'en', currency: 'USD' })
52
- }));
53
-
54
- const subscribe = jest.fn(() => ({ unwrap: () => Promise.resolve({}) }));
55
-
56
- jest.mock('@akinon/next/data/client/misc', () => ({
57
- useEmailSubscriptionMutation: () => [subscribe, { isLoading: false }]
58
- }));
59
-
60
- const block = (overrides: Partial<Block>): Block =>
61
- ({
62
- id: 'blk',
63
- type: 'text',
64
- label: 'Block',
65
- value: '',
66
- order: 0,
67
- hidden: false,
68
- properties: {},
69
- styles: {},
70
- ...overrides
71
- }) as Block;
72
-
73
- const loginForm = (properties: Record<string, unknown> = {}) => (
74
- <AuthFormBlock
75
- block={
76
- block({
77
- id: 'login-1',
78
- type: 'auth-form',
79
- label: 'Auth Form',
80
- properties
81
- }) as Block
82
- }
83
- placeholderId="placeholder-1"
84
- sectionId="section-1"
85
- currentBreakpoint="desktop"
86
- />
87
- );
88
-
89
- const mkModal = (id: string): ThemeModal =>
90
- readThemeModals([{ id, name: 'Auth', presentation: 'drawer' }])[0];
91
-
92
- /** The drawer, with the login form composed inside it — the GANT shape. */
93
- const openDrawerWithLogin = (
94
- properties: Record<string, unknown> = {},
95
- modalId = 'auth-drawer'
96
- ) => {
97
- const view = render(
98
- <ThemeModalMount modal={mkModal(modalId)}>
99
- {loginForm(properties)}
100
- </ThemeModalMount>
101
- );
102
-
103
- act(() => {
104
- openModal(modalId);
105
- });
106
-
107
- return view;
108
- };
109
-
110
- const submit = (container: HTMLElement) => {
111
- fireEvent.change(container.querySelector('input[name="email"]')!, {
112
- target: { value: 'ada@example.com' }
113
- });
114
- fireEvent.change(container.querySelector('input[name="password"]')!, {
115
- target: { value: 'hunter2' }
116
- });
117
- fireEvent.submit(container.querySelector('form')!);
118
- };
119
-
120
- beforeEach(() => {
121
- signIn.mockClear();
122
- push.mockClear();
123
- refresh.mockClear();
124
- __resetOverlayBusForTests();
125
- __resetModalPreviewForTests();
126
- registerModalHost();
127
- clearAutoOpenRecord('auth-drawer');
128
- clearAutoOpenRecord('other');
129
- });
130
-
131
- describe('a login inside a drawer', () => {
132
- it('closes the drawer it was in, with nothing authored', async () => {
133
- const { container } = openDrawerWithLogin();
134
-
135
- expect(isModalOpen('auth-drawer')).toBe(true);
136
-
137
- submit(container);
138
-
139
- await waitFor(() => expect(isModalOpen('auth-drawer')).toBe(false));
140
- // And the surface is gone with it, not merely marked closed.
141
- expect(document.querySelector('[data-theme-modal]')).toBeNull();
142
- });
143
-
144
- it('stays on the page instead of pushing the shopper to the homepage', async () => {
145
- const { container } = openDrawerWithLogin();
146
-
147
- submit(container);
148
-
149
- await waitFor(() => expect(refresh).toHaveBeenCalled());
150
- // A sign-in inside a drawer is a detour from the page the shopper is on,
151
- // so `stay` is the default THERE — the refresh is the whole navigation.
152
- expect(push).not.toHaveBeenCalled();
153
- });
154
-
155
- it('still obeys a composition that asked to be sent somewhere', async () => {
156
- const { container } = openDrawerWithLogin({
157
- afterSignIn: 'fixed',
158
- callbackUrl: '/account'
159
- });
160
-
161
- submit(container);
162
-
163
- await waitFor(() => expect(push).toHaveBeenCalledWith('/account'));
164
- // Authored destination or not, the drawer it finished in still closes.
165
- expect(isModalOpen('auth-drawer')).toBe(false);
166
- });
167
-
168
- it('leaves other modals alone unless asked to close everything', async () => {
169
- const { container } = openDrawerWithLogin();
170
-
171
- act(() => {
172
- openModal('other');
173
- });
174
-
175
- submit(container);
176
-
177
- await waitFor(() => expect(isModalOpen('auth-drawer')).toBe(false));
178
- // Closing the surface a form finished in is never a surprise; closing an
179
- // unrelated overlay would be.
180
- expect(isModalOpen('other')).toBe(true);
181
- });
182
-
183
- it('closes every open modal when the composition authored that', async () => {
184
- const { container } = openDrawerWithLogin({ closeModalsOnSuccess: true });
185
-
186
- act(() => {
187
- openModal('other');
188
- });
189
-
190
- submit(container);
191
-
192
- // The wider meaning the toggle has always had, for a form nested two
193
- // overlays deep.
194
- await waitFor(() => expect(getOpenModalIds()).toHaveLength(0));
195
- });
196
- });
197
-
198
- describe('a login on a plain page', () => {
199
- it('closes nothing, and keeps its historical destination', async () => {
200
- const { container } = render(loginForm());
201
-
202
- act(() => {
203
- openModal('other');
204
- });
205
-
206
- submit(container);
207
-
208
- await waitFor(() => expect(push).toHaveBeenCalledWith('/'));
209
- // No host to close, so a form on a page cannot reach an overlay it knows
210
- // nothing about — which is why the toggle defaults to off.
211
- expect(isModalOpen('other')).toBe(true);
212
- });
213
- });
214
-
215
- /**
216
- * The destination, decided on the VALUE rather than on a default.
217
- *
218
- * A host-aware DEFAULT would have been dead on arrival: the editor writes every
219
- * select's default into the block it inserts, so `afterSignIn` arrives as
220
- * `'fixed'` on every newly authored form and a host-aware default would only
221
- * ever apply to compositions that predate the property. Reading the resolved
222
- * PATH instead survives that, and it keeps an authored destination working.
223
- */
224
- describe('where a drawer sign-in leaves the shopper', () => {
225
- it('goes nowhere when the destination names nowhere in particular', async () => {
226
- // `/` is the shipped default that every inserted form carries.
227
- const { container } = openDrawerWithLogin({ callbackUrl: '/' });
228
-
229
- submit(container);
230
-
231
- await waitFor(() => expect(refresh).toHaveBeenCalled());
232
- expect(push).not.toHaveBeenCalled();
233
- });
234
-
235
- it('still honours a destination the brand actually chose', async () => {
236
- const { container } = openDrawerWithLogin({ callbackUrl: '/account' });
237
-
238
- submit(container);
239
-
240
- await waitFor(() => expect(push).toHaveBeenCalledWith('/account'));
241
- });
242
-
243
- it('keeps pushing on a plain page, where `/` is a real destination', async () => {
244
- const { container } = render(loginForm({ callbackUrl: '/' }));
245
-
246
- submit(container);
247
-
248
- await waitFor(() => expect(push).toHaveBeenCalledWith('/'));
249
- });
250
- });
251
-
252
- /**
253
- * The same rule, for everything else that finishes inside a modal.
254
- *
255
- * "Only login/register/OTP" was the wrong scope: a newsletter popup, a
256
- * quick-view's add-to-cart and a change-password form all finish inside a
257
- * modal too. What differs is not the mechanism but the SUCCESS STATE — a
258
- * sign-in leaves nothing to read, while "thanks for subscribing" is the point
259
- * — so every one of them carries the same switch and only the auth forms
260
- * default it on.
261
- */
262
- describe('any block that finishes inside a modal', () => {
263
- const newsletterInDrawer = (properties: Record<string, unknown>) => {
264
- const view = render(
265
- <ThemeModalMount modal={mkModal('newsletter-modal')}>
266
- <NewsletterFormBlock
267
- block={
268
- block({
269
- id: 'news-1',
270
- type: 'newsletter-form',
271
- label: 'Newsletter',
272
- properties
273
- }) as Block
274
- }
275
- placeholderId="placeholder-1"
276
- sectionId="section-1"
277
- currentBreakpoint="desktop"
278
- />
279
- </ThemeModalMount>
280
- );
281
-
282
- act(() => {
283
- openModal('newsletter-modal');
284
- });
285
-
286
- return view;
287
- };
288
-
289
- const subscribeWith = (container: HTMLElement) => {
290
- fireEvent.change(container.querySelector('input[type="email"]')!, {
291
- target: { value: 'ada@example.com' }
292
- });
293
- fireEvent.submit(container.querySelector('form')!);
294
- };
295
-
296
- beforeEach(() => {
297
- subscribe.mockClear();
298
- clearAutoOpenRecord('newsletter-modal');
299
- });
300
-
301
- it('keeps the popup open by default, so the thank-you can be read', async () => {
302
- const { container } = newsletterInDrawer({});
303
-
304
- subscribeWith(container);
305
-
306
- await waitFor(() => expect(subscribe).toHaveBeenCalled());
307
- expect(isModalOpen('newsletter-modal')).toBe(true);
308
- });
309
-
310
- it('closes it when the designer asked for that', async () => {
311
- const { container } = newsletterInDrawer({ closeHostModalOnSuccess: true });
312
-
313
- subscribeWith(container);
314
-
315
- await waitFor(() => expect(isModalOpen('newsletter-modal')).toBe(false));
316
- });
317
- });
318
-
319
- describe('the sign-in trio, with the switch turned off', () => {
320
- it('leaves the drawer open — the default is a default, not a rule', async () => {
321
- const { container } = openDrawerWithLogin({
322
- closeHostModalOnSuccess: false
323
- });
324
-
325
- submit(container);
326
-
327
- await waitFor(() => expect(refresh).toHaveBeenCalled());
328
- expect(isModalOpen('auth-drawer')).toBe(true);
329
- });
330
- });
331
-
332
- describe('what must NOT close the surface', () => {
333
- it('a refused sign-in leaves the drawer up, with the message in it', async () => {
334
- signIn.mockImplementationOnce(() =>
335
- Promise.resolve({
336
- error: 'CredentialsSignin',
337
- code: JSON.stringify([
338
- { type: 'non_field_errors', data: ['E-mail or password is wrong.'] }
339
- ])
340
- })
341
- );
342
-
343
- const { container } = openDrawerWithLogin();
344
-
345
- submit(container);
346
-
347
- await waitFor(() =>
348
- expect(
349
- container.textContent?.includes('E-mail or password is wrong.')
350
- ).toBe(true)
351
- );
352
- // Closing here would take the error away with the form that has to be
353
- // corrected — the one case where an auto-close is plainly wrong.
354
- expect(isModalOpen('auth-drawer')).toBe(true);
355
- });
356
-
357
- it('closes the host even when a legacy composition asked for all modals', async () => {
358
- const { container } = openDrawerWithLogin({ closeModalsOnSuccess: true });
359
-
360
- submit(container);
361
-
362
- // Both run: the wide close is a superset, so an already-published drawer
363
- // is not stuck on it. (The narrow one then finds nothing to do.)
364
- await waitFor(() => expect(getOpenModalIds()).toHaveLength(0));
365
- });
366
- });