@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.
- package/CHANGELOG.md +33 -15
- package/package.json +4 -4
- package/src/__tests__/auth-commerce-events.test.tsx +1 -5
- package/src/__tests__/auth-form-error-block.test.tsx +0 -2
- package/src/__tests__/auth-form-validation.test.tsx +0 -2
- package/src/__tests__/auth-slot-coverage.test.ts +1 -9
- package/src/__tests__/auth-step-title.test.tsx +0 -2
- package/src/__tests__/detached-submit-state.test.tsx +2 -80
- package/src/__tests__/forgot-password-composed.test.tsx +0 -2
- package/src/__tests__/otp-form-composed.test.tsx +0 -2
- package/src/__tests__/password-reset-step.test.tsx +0 -2
- package/src/__tests__/register-form-composed.test.tsx +4 -253
- package/src/__tests__/registry-parity.test.ts +0 -12
- package/src/auth/auth-field-errors.tsx +0 -31
- package/src/blocks/account-button-block.tsx +0 -25
- package/src/blocks/add-to-cart-block.tsx +0 -15
- package/src/blocks/auth-field-block.tsx +3 -13
- package/src/blocks/auth-flow-block.tsx +3 -45
- package/src/blocks/auth-form-block.tsx +54 -337
- package/src/blocks/block-renderer-registry.tsx +0 -8
- package/src/blocks/button-block.tsx +13 -43
- package/src/blocks/change-password-form-block.tsx +0 -15
- package/src/blocks/forgot-password-block.tsx +9 -132
- package/src/blocks/newsletter-form-block.tsx +0 -16
- package/src/blocks/otp-form-block.tsx +67 -161
- package/src/blocks/profile-form-block.tsx +0 -13
- package/src/blocks/register-form-block.tsx +110 -362
- package/src/chrome/__tests__/theme-config-source.test.ts +1 -2
- package/src/chrome/placeholder-slugs.ts +0 -3
- package/src/chrome/route-slug.ts +0 -11
- package/src/index.ts +0 -10
- package/src/overlays/__tests__/modal-registry.test.ts +1 -5
- package/src/overlays/modal-chrome-css.ts +1 -8
- package/src/overlays/modal-preview-host.tsx +2 -7
- package/src/overlays/modal-registry.ts +1 -38
- package/src/overlays/overlay-surface.tsx +0 -23
- package/src/overlays/overlay-tokens.ts +0 -12
- package/src/overlays/theme-modal-mount.tsx +6 -85
- package/src/utils/visibility-rules.ts +0 -114
- package/src/__tests__/auth-captcha.test.tsx +0 -261
- package/src/__tests__/auth-destination.test.tsx +0 -348
- package/src/__tests__/auth-error-reading.test.ts +0 -232
- package/src/__tests__/auth-flow-scope.test.tsx +0 -162
- package/src/__tests__/auth-form-success-block.test.tsx +0 -429
- package/src/__tests__/block-action-audience.test.ts +0 -154
- package/src/__tests__/form-state-composition.test.tsx +0 -484
- package/src/__tests__/modal-login-close.test.tsx +0 -366
- package/src/__tests__/reset-password-composed.test.tsx +0 -385
- package/src/auth/auth-captcha.tsx +0 -125
- package/src/auth/auth-destination.ts +0 -168
- package/src/auth/form-state-context.tsx +0 -170
- package/src/auth/form-success-context.tsx +0 -56
- package/src/auth/read-auth-error.ts +0 -332
- package/src/blocks/auth-success-block.tsx +0 -85
- package/src/blocks/captcha-block.tsx +0 -61
- package/src/blocks/form-state-block.tsx +0 -84
- package/src/blocks/reset-password-block.tsx +0 -717
- package/src/overlays/__tests__/modal-loading.test.tsx +0 -235
- package/src/overlays/modal-host-context.tsx +0 -66
- 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
|
-
});
|