@akinon/pz-theme 2.0.83-rc.0 → 2.0.84-beta.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 +39 -33
- package/package.json +4 -4
- package/src/__tests__/auth-flow-block.test.tsx +211 -0
- package/src/__tests__/auth-flow-bus.test.ts +290 -0
- package/src/__tests__/auth-form-error-block.test.tsx +326 -0
- package/src/__tests__/auth-form-validation.test.tsx +204 -0
- package/src/__tests__/auth-step-title.test.tsx +202 -0
- package/src/__tests__/detached-submit-state.test.tsx +216 -0
- package/src/__tests__/forgot-password-composed.test.tsx +313 -0
- package/src/__tests__/otp-form-composed.test.tsx +487 -0
- package/src/__tests__/password-reset-step.test.tsx +269 -0
- package/src/__tests__/register-form-composed.test.tsx +739 -0
- package/src/__tests__/registry-parity.test.ts +9 -0
- package/src/auth/auth-field-errors.tsx +151 -0
- package/src/auth/auth-flow-bus.ts +415 -0
- package/src/auth/form-error-context.tsx +55 -0
- package/src/auth/form-submit-bus.ts +93 -0
- package/src/auth/otp-form-context.tsx +80 -0
- package/src/blocks/__tests__/button-block.test.tsx +55 -0
- package/src/blocks/__tests__/link-block.test.tsx +48 -30
- package/src/blocks/__tests__/overlay-sweep.test.tsx +31 -8
- package/src/blocks/__tests__/product-gallery-block.test.tsx +12 -4
- package/src/blocks/__tests__/product-recommendations-block.test.tsx +9 -2
- package/src/blocks/__tests__/store-locator-block.test.tsx +35 -8
- package/src/blocks/__tests__/text-block.test.tsx +37 -3
- package/src/blocks/__tests__/variant-selector-block.test.tsx +16 -4
- package/src/blocks/auth-consent-block.tsx +214 -0
- package/src/blocks/auth-error-block.tsx +82 -0
- package/src/blocks/auth-field-block.tsx +270 -0
- package/src/blocks/auth-flow-block.tsx +251 -0
- package/src/blocks/auth-form-block.tsx +391 -47
- package/src/blocks/auth-step-block.tsx +81 -0
- package/src/blocks/auth-step-title-block.tsx +81 -0
- package/src/blocks/block-renderer-registry.tsx +18 -0
- package/src/blocks/button-block.tsx +91 -13
- package/src/blocks/forgot-password-block.tsx +318 -24
- package/src/blocks/link-block.tsx +22 -84
- package/src/blocks/otp-code-input-block.tsx +180 -0
- package/src/blocks/otp-form-block.tsx +1092 -0
- package/src/blocks/otp-phase-block.tsx +71 -0
- package/src/blocks/register-form-block.tsx +541 -74
- package/src/blocks/text-block.tsx +17 -40
- package/src/chrome/__tests__/theme-config-source.test.ts +4 -1
- package/src/hooks/__tests__/use-overlay.test.tsx +97 -5
- package/src/hooks/use-block-action.ts +87 -4
- package/src/hooks/use-overlay.ts +60 -9
- package/src/overlays/__tests__/modal-chrome-padding.test.tsx +119 -0
- package/src/overlays/__tests__/modal-registry.test.ts +6 -1
- package/src/overlays/__tests__/panel-stretch.test.tsx +60 -0
- package/src/overlays/__tests__/responsive-chrome.test.tsx +446 -0
- package/src/overlays/__tests__/responsive-presentation.test.tsx +192 -0
- package/src/overlays/__tests__/theme-modal-mount.test.tsx +12 -1
- package/src/overlays/modal-preview-host.tsx +39 -2
- package/src/overlays/modal-registry.ts +344 -4
- package/src/overlays/overlay-surface.tsx +249 -19
- package/src/overlays/overlay-tokens.ts +94 -0
- package/src/overlays/theme-modal-mount.tsx +34 -4
- package/src/theme-block.tsx +6 -0
- package/src/utils/__tests__/inner-root-styles.test.tsx +139 -0
- package/src/utils/__tests__/iterator-bindings.test.tsx +9 -3
- package/src/utils/index.ts +107 -6
- package/src/utils/iterator-utils.ts +39 -2
- package/src/utils/part-styles.ts +4 -0
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
|
|
4
|
+
import ForgotPasswordBlock from '../blocks/forgot-password-block';
|
|
5
|
+
import {
|
|
6
|
+
__resetAuthFlowBusForTests,
|
|
7
|
+
AuthFlowIdContext,
|
|
8
|
+
getAuthFlowState
|
|
9
|
+
} from '../auth/auth-flow-bus';
|
|
10
|
+
import { Block } from '../theme-block';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The password reset, element by element.
|
|
14
|
+
*
|
|
15
|
+
* Its pieces used to be style PARTS — a brand could recolour the input but not
|
|
16
|
+
* put a sentence above it explaining what the mail does, move the message out
|
|
17
|
+
* from under the button, or replace the CTA with the drawer's pinned one. Now
|
|
18
|
+
* they are blocks, the way a login form's fields already were: an `auth-field`
|
|
19
|
+
* for the address, a Form Error block for the message, an ordinary Button for
|
|
20
|
+
* the submit.
|
|
21
|
+
*
|
|
22
|
+
* What must NOT change is what reaches commerce: a composed form and a fixed
|
|
23
|
+
* one request the reset for the same address. These pin that, the two states of
|
|
24
|
+
* the journey, and the refusal that keeps the canvas from mailing a real
|
|
25
|
+
* shopper.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
let mutationState = { isLoading: false, isSuccess: false };
|
|
29
|
+
const forgotPassword = jest.fn(() => ({ unwrap: () => Promise.resolve({}) }));
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* A catch-all, not just the one hook this block calls.
|
|
33
|
+
*
|
|
34
|
+
* `jest.config`'s moduleNameMapper collapses every `@akinon/next/data/client/*`
|
|
35
|
+
* onto ONE mock file, so replacing the module for `user` replaces it for `misc`
|
|
36
|
+
* too — and a composed form draws a child Button, which reaches for a
|
|
37
|
+
* subscription hook of its own. Overriding a single export leaves every other
|
|
38
|
+
* one undefined and the child crashes.
|
|
39
|
+
*/
|
|
40
|
+
jest.mock('@akinon/next/data/client/user', () => {
|
|
41
|
+
const handler = {
|
|
42
|
+
get(_target: unknown, prop: string) {
|
|
43
|
+
if (prop === '__esModule') return true;
|
|
44
|
+
if (typeof prop !== 'string') return undefined;
|
|
45
|
+
if (prop === 'useForgotPasswordMutation') {
|
|
46
|
+
return () => [forgotPassword, mutationState];
|
|
47
|
+
}
|
|
48
|
+
if (prop.endsWith('Mutation')) {
|
|
49
|
+
return () => [
|
|
50
|
+
jest.fn(() => ({ unwrap: () => Promise.resolve({}) })),
|
|
51
|
+
{ isLoading: false, isSuccess: false, isError: false }
|
|
52
|
+
];
|
|
53
|
+
}
|
|
54
|
+
if (prop.endsWith('Query')) {
|
|
55
|
+
return () => ({
|
|
56
|
+
data: undefined,
|
|
57
|
+
isLoading: false,
|
|
58
|
+
isSuccess: true,
|
|
59
|
+
refetch: jest.fn()
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
return () => undefined;
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
return new Proxy({}, handler);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
jest.mock('next/navigation', () => ({
|
|
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
|
+
/** Exactly what `forgotPasswordDefaultChildren()` seeds in the editor. */
|
|
87
|
+
const SEEDED: Block[] = [
|
|
88
|
+
block({
|
|
89
|
+
id: 'field-email',
|
|
90
|
+
type: 'auth-field',
|
|
91
|
+
label: 'E-mail Field',
|
|
92
|
+
properties: { fieldType: 'email', label: 'E-mail' },
|
|
93
|
+
order: 0
|
|
94
|
+
}),
|
|
95
|
+
block({
|
|
96
|
+
id: 'form-error',
|
|
97
|
+
type: 'auth-error',
|
|
98
|
+
label: 'Form Error',
|
|
99
|
+
order: 1
|
|
100
|
+
}),
|
|
101
|
+
block({
|
|
102
|
+
id: 'submit',
|
|
103
|
+
type: 'button',
|
|
104
|
+
label: 'Submit Button',
|
|
105
|
+
properties: {
|
|
106
|
+
text: 'Send reset link',
|
|
107
|
+
tag: 'button',
|
|
108
|
+
type: 'submit'
|
|
109
|
+
},
|
|
110
|
+
order: 2
|
|
111
|
+
})
|
|
112
|
+
];
|
|
113
|
+
|
|
114
|
+
const renderComposed = (
|
|
115
|
+
properties: Record<string, unknown> = {},
|
|
116
|
+
children: Block[] = SEEDED,
|
|
117
|
+
options: { isDesigner?: boolean } = {}
|
|
118
|
+
) =>
|
|
119
|
+
render(
|
|
120
|
+
<AuthFlowIdContext.Provider value="auth">
|
|
121
|
+
<ForgotPasswordBlock
|
|
122
|
+
block={
|
|
123
|
+
block({
|
|
124
|
+
id: 'reset-1',
|
|
125
|
+
type: 'forgot-password',
|
|
126
|
+
label: 'Forgot Password',
|
|
127
|
+
// The seeded Button is the submit, so the built-in one is off —
|
|
128
|
+
// which is what the editor sets when it drops the form.
|
|
129
|
+
properties: { showSubmit: false, ...properties },
|
|
130
|
+
blocks: children
|
|
131
|
+
}) as Block
|
|
132
|
+
}
|
|
133
|
+
placeholderId="placeholder-1"
|
|
134
|
+
sectionId="section-1"
|
|
135
|
+
currentBreakpoint="desktop"
|
|
136
|
+
isDesigner={options.isDesigner ?? false}
|
|
137
|
+
/>
|
|
138
|
+
</AuthFlowIdContext.Provider>
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
const form = () => document.querySelector('form') as HTMLFormElement;
|
|
142
|
+
const emailInput = () =>
|
|
143
|
+
document.querySelector('input[name="email"]') as HTMLInputElement;
|
|
144
|
+
|
|
145
|
+
beforeEach(() => {
|
|
146
|
+
__resetAuthFlowBusForTests();
|
|
147
|
+
mutationState = { isLoading: false, isSuccess: false };
|
|
148
|
+
forgotPassword.mockClear();
|
|
149
|
+
forgotPassword.mockImplementation(() => ({
|
|
150
|
+
unwrap: () => Promise.resolve({})
|
|
151
|
+
}));
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
describe('a composed password reset', () => {
|
|
155
|
+
it('draws its children instead of its own row', () => {
|
|
156
|
+
const { container } = renderComposed();
|
|
157
|
+
|
|
158
|
+
expect(
|
|
159
|
+
container.querySelector('[data-forgot-password-composed]')
|
|
160
|
+
).not.toBeNull();
|
|
161
|
+
// The child Button, not the block's built-in submit.
|
|
162
|
+
expect(container.querySelector('[data-forgot-password-submit]')).toBeNull();
|
|
163
|
+
expect(screen.getByText('Send reset link')).toBeTruthy();
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('requests the reset for the address a child field collected', async () => {
|
|
167
|
+
renderComposed();
|
|
168
|
+
|
|
169
|
+
fireEvent.change(emailInput(), { target: { value: 'ada@example.com' } });
|
|
170
|
+
fireEvent.submit(form());
|
|
171
|
+
|
|
172
|
+
await waitFor(() => expect(forgotPassword).toHaveBeenCalled());
|
|
173
|
+
// The same body the fixed row sends — the address is read off the form
|
|
174
|
+
// rather than from state no child writes to.
|
|
175
|
+
expect(forgotPassword).toHaveBeenCalledWith({ email: 'ada@example.com' });
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('trims the address rather than sending what a stray space typed', async () => {
|
|
179
|
+
renderComposed();
|
|
180
|
+
|
|
181
|
+
fireEvent.change(emailInput(), { target: { value: ' ada@example.com ' } });
|
|
182
|
+
fireEvent.submit(form());
|
|
183
|
+
|
|
184
|
+
await waitFor(() => expect(forgotPassword).toHaveBeenCalled());
|
|
185
|
+
expect(forgotPassword).toHaveBeenCalledWith({ email: 'ada@example.com' });
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('says which field is missing instead of raising a browser bubble', async () => {
|
|
189
|
+
renderComposed({ requiredMessage: 'Bu alan zorunludur.' });
|
|
190
|
+
|
|
191
|
+
fireEvent.submit(form());
|
|
192
|
+
|
|
193
|
+
await waitFor(() =>
|
|
194
|
+
expect(screen.getByText('Bu alan zorunludur.')).toBeTruthy()
|
|
195
|
+
);
|
|
196
|
+
expect(forgotPassword).not.toHaveBeenCalled();
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('never mails a real shopper from the canvas', () => {
|
|
200
|
+
renderComposed({}, SEEDED, { isDesigner: true });
|
|
201
|
+
|
|
202
|
+
fireEvent.change(emailInput(), { target: { value: 'ada@example.com' } });
|
|
203
|
+
fireEvent.submit(form());
|
|
204
|
+
|
|
205
|
+
expect(forgotPassword).not.toHaveBeenCalled();
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('still renders its fixed row for a composition that has no children', () => {
|
|
209
|
+
const { container } = renderComposed({ showSubmit: true }, []);
|
|
210
|
+
|
|
211
|
+
expect(
|
|
212
|
+
container.querySelector('[data-forgot-password-composed]')
|
|
213
|
+
).toBeNull();
|
|
214
|
+
expect(
|
|
215
|
+
container.querySelector('[data-forgot-password-field="email"]')
|
|
216
|
+
).not.toBeNull();
|
|
217
|
+
expect(
|
|
218
|
+
container.querySelector('[data-forgot-password-submit]')
|
|
219
|
+
).not.toBeNull();
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
describe('where the reset form puts its message', () => {
|
|
224
|
+
const failing = () =>
|
|
225
|
+
forgotPassword.mockImplementation(() => ({
|
|
226
|
+
unwrap: () => Promise.reject(new Error('refused'))
|
|
227
|
+
}));
|
|
228
|
+
|
|
229
|
+
it('draws it in the Form Error block, and stands its own node down', async () => {
|
|
230
|
+
failing();
|
|
231
|
+
const { container } = renderComposed({ errorLabel: 'Gönderilemedi.' });
|
|
232
|
+
|
|
233
|
+
fireEvent.change(emailInput(), { target: { value: 'ada@example.com' } });
|
|
234
|
+
fireEvent.submit(form());
|
|
235
|
+
|
|
236
|
+
await waitFor(() =>
|
|
237
|
+
expect(container.querySelector('[data-auth-error]')).not.toBeNull()
|
|
238
|
+
);
|
|
239
|
+
expect(screen.getByText('Gönderilemedi.')).toBeTruthy();
|
|
240
|
+
// Exactly one message: the fallback node is what the block exists to
|
|
241
|
+
// replace, so it must not draw beside it.
|
|
242
|
+
expect(container.querySelector('[data-forgot-password-error]')).toBeNull();
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it('falls back to its own node for a composition that placed none', async () => {
|
|
246
|
+
failing();
|
|
247
|
+
const { container } = renderComposed({ errorLabel: 'Gönderilemedi.' }, [
|
|
248
|
+
SEEDED[0],
|
|
249
|
+
SEEDED[2]
|
|
250
|
+
]);
|
|
251
|
+
|
|
252
|
+
fireEvent.change(emailInput(), { target: { value: 'ada@example.com' } });
|
|
253
|
+
fireEvent.submit(form());
|
|
254
|
+
|
|
255
|
+
await waitFor(() =>
|
|
256
|
+
expect(
|
|
257
|
+
container.querySelector('[data-forgot-password-error]')
|
|
258
|
+
).not.toBeNull()
|
|
259
|
+
);
|
|
260
|
+
expect(screen.getByText('Gönderilemedi.')).toBeTruthy();
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
describe('the state after the mail is away', () => {
|
|
265
|
+
it('moves the flow to the step the design authored for it', async () => {
|
|
266
|
+
renderComposed({ successStepTarget: 'sent' });
|
|
267
|
+
|
|
268
|
+
fireEvent.change(emailInput(), { target: { value: 'ada@example.com' } });
|
|
269
|
+
fireEvent.submit(form());
|
|
270
|
+
|
|
271
|
+
// A step is more than a message and a link could be: the brand's own copy,
|
|
272
|
+
// illustration and CTA, authored out of ordinary blocks.
|
|
273
|
+
await waitFor(() => expect(getAuthFlowState('auth')?.active).toBe('sent'));
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it('names the flow outright when the target spells one', async () => {
|
|
277
|
+
renderComposed({ successStepTarget: 'drawer:sent' });
|
|
278
|
+
|
|
279
|
+
fireEvent.change(emailInput(), { target: { value: 'ada@example.com' } });
|
|
280
|
+
fireEvent.submit(form());
|
|
281
|
+
|
|
282
|
+
await waitFor(() =>
|
|
283
|
+
expect(getAuthFlowState('drawer')?.active).toBe('sent')
|
|
284
|
+
);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it('leaves the flow alone for a form that named no step', async () => {
|
|
288
|
+
renderComposed();
|
|
289
|
+
|
|
290
|
+
fireEvent.change(emailInput(), { target: { value: 'ada@example.com' } });
|
|
291
|
+
fireEvent.submit(form());
|
|
292
|
+
|
|
293
|
+
await waitFor(() => expect(forgotPassword).toHaveBeenCalled());
|
|
294
|
+
// Nothing was navigated: the block shows its own success copy instead,
|
|
295
|
+
// which is what every published reset form does.
|
|
296
|
+
expect(getAuthFlowState('auth')).toBeNull();
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it('shows its own success copy once commerce has answered', () => {
|
|
300
|
+
mutationState = { isLoading: false, isSuccess: true };
|
|
301
|
+
|
|
302
|
+
const { container } = renderComposed({
|
|
303
|
+
successLabel: 'Posta yolda.'
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
expect(
|
|
307
|
+
container.querySelector('[data-forgot-password-state="success"]')
|
|
308
|
+
).not.toBeNull();
|
|
309
|
+
expect(screen.getByText('Posta yolda.')).toBeTruthy();
|
|
310
|
+
// The form is gone, so the same address cannot be submitted twice.
|
|
311
|
+
expect(document.querySelector('form')).toBeNull();
|
|
312
|
+
});
|
|
313
|
+
});
|