@akinon/pz-theme 2.0.83-rc.0 → 2.0.83
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 +31 -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,269 @@
|
|
|
1
|
+
import { cleanup, fireEvent, render, screen } 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 {
|
|
7
|
+
__resetAuthFlowBusForTests,
|
|
8
|
+
AuthFlowIdContext,
|
|
9
|
+
getAuthFlowState
|
|
10
|
+
} from '../auth/auth-flow-bus';
|
|
11
|
+
import { Block } from '../theme-block';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Password reset AS A STEP of the flow, rather than a page the drawer sends
|
|
15
|
+
* the shopper away to.
|
|
16
|
+
*
|
|
17
|
+
* The two ends of that journey are what these cover: the login form's "forgot
|
|
18
|
+
* password?" moving the flow instead of navigating, and the reset form offering
|
|
19
|
+
* the way back once the mail is on its way. Both are opt-in — a form with no
|
|
20
|
+
* step named behaves exactly as it did, which is what keeps published auth
|
|
21
|
+
* PAGES (where navigating is correct) working.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
let mutationState = { isLoading: false, isSuccess: false };
|
|
25
|
+
const forgotPassword = jest.fn(() => ({ unwrap: () => Promise.resolve({}) }));
|
|
26
|
+
|
|
27
|
+
jest.mock('@akinon/next/data/client/user', () => ({
|
|
28
|
+
useForgotPasswordMutation: () => [forgotPassword, mutationState]
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
jest.mock('next-auth/react', () => ({
|
|
32
|
+
signIn: jest.fn(() => Promise.resolve({ error: null, url: '/' })),
|
|
33
|
+
useSession: () => ({ data: null, status: 'unauthenticated' })
|
|
34
|
+
}));
|
|
35
|
+
|
|
36
|
+
jest.mock('next/navigation', () => ({
|
|
37
|
+
useRouter: () => ({ push: jest.fn(), refresh: jest.fn(), replace: jest.fn() })
|
|
38
|
+
}));
|
|
39
|
+
|
|
40
|
+
const blockOf = (type: string, properties: Record<string, unknown>): Block => ({
|
|
41
|
+
id: `${type}-1`,
|
|
42
|
+
type,
|
|
43
|
+
label: type,
|
|
44
|
+
properties,
|
|
45
|
+
styles: {},
|
|
46
|
+
order: 0,
|
|
47
|
+
hidden: false
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const renderForgotPassword = (
|
|
51
|
+
properties: Record<string, unknown> = {},
|
|
52
|
+
options: { isDesigner?: boolean; flowId?: string } = {}
|
|
53
|
+
) =>
|
|
54
|
+
render(
|
|
55
|
+
<AuthFlowIdContext.Provider value={options.flowId ?? 'auth'}>
|
|
56
|
+
<ForgotPasswordBlock
|
|
57
|
+
block={blockOf('forgot-password', properties)}
|
|
58
|
+
placeholderId="placeholder-1"
|
|
59
|
+
sectionId="section-1"
|
|
60
|
+
currentBreakpoint="desktop"
|
|
61
|
+
isDesigner={options.isDesigner ?? false}
|
|
62
|
+
/>
|
|
63
|
+
</AuthFlowIdContext.Provider>
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
const renderLoginForm = (
|
|
67
|
+
properties: Record<string, unknown> = {},
|
|
68
|
+
options: { flowId?: string; children?: Block[] } = {}
|
|
69
|
+
) =>
|
|
70
|
+
render(
|
|
71
|
+
<AuthFlowIdContext.Provider value={options.flowId ?? 'auth'}>
|
|
72
|
+
<AuthFormBlock
|
|
73
|
+
block={{
|
|
74
|
+
...blockOf('auth-form', { mode: 'login', ...properties }),
|
|
75
|
+
blocks: options.children
|
|
76
|
+
}}
|
|
77
|
+
placeholderId="placeholder-1"
|
|
78
|
+
sectionId="section-1"
|
|
79
|
+
currentBreakpoint="desktop"
|
|
80
|
+
/>
|
|
81
|
+
</AuthFlowIdContext.Provider>
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
/** The shape every dropped Auth Form now has: its fields are child blocks. */
|
|
85
|
+
const composedFields: Block[] = [
|
|
86
|
+
{
|
|
87
|
+
id: 'field-email',
|
|
88
|
+
type: 'auth-field',
|
|
89
|
+
label: 'E-mail Field',
|
|
90
|
+
properties: { fieldType: 'email' },
|
|
91
|
+
styles: {},
|
|
92
|
+
order: 0,
|
|
93
|
+
hidden: false
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
id: 'field-password',
|
|
97
|
+
type: 'auth-field',
|
|
98
|
+
label: 'Password Field',
|
|
99
|
+
properties: { fieldType: 'password' },
|
|
100
|
+
styles: {},
|
|
101
|
+
order: 1,
|
|
102
|
+
hidden: false
|
|
103
|
+
}
|
|
104
|
+
];
|
|
105
|
+
|
|
106
|
+
beforeEach(() => {
|
|
107
|
+
__resetAuthFlowBusForTests();
|
|
108
|
+
mutationState = { isLoading: false, isSuccess: false };
|
|
109
|
+
forgotPassword.mockClear();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe('the login form reaching the reset step', () => {
|
|
113
|
+
it('moves the flow instead of navigating when a step is named', () => {
|
|
114
|
+
renderLoginForm({
|
|
115
|
+
showForgotPassword: true,
|
|
116
|
+
forgotPasswordStepTarget: 'reset'
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const control = screen.getByText('Forgot password?');
|
|
120
|
+
|
|
121
|
+
// A button, not an anchor: nothing to navigate to.
|
|
122
|
+
expect(control.tagName).toBe('BUTTON');
|
|
123
|
+
expect(control.getAttribute('type')).toBe('button');
|
|
124
|
+
|
|
125
|
+
fireEvent.click(control);
|
|
126
|
+
|
|
127
|
+
expect(getAuthFlowState('auth')?.active).toBe('reset');
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('reads flow:step so a form outside the flow can still send it', () => {
|
|
131
|
+
renderLoginForm(
|
|
132
|
+
{
|
|
133
|
+
showForgotPassword: true,
|
|
134
|
+
forgotPasswordStepTarget: 'auth:reset'
|
|
135
|
+
},
|
|
136
|
+
{ flowId: '' }
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
fireEvent.click(screen.getByText('Forgot password?'));
|
|
140
|
+
|
|
141
|
+
expect(getAuthFlowState('auth')?.active).toBe('reset');
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('draws the control on a composed form too', () => {
|
|
145
|
+
// The arrangement's login form has auth-field children, and a control that
|
|
146
|
+
// only exists on the fixed layout would leave `Forgot Password Step` doing
|
|
147
|
+
// nothing exactly where the journey needs it.
|
|
148
|
+
renderLoginForm(
|
|
149
|
+
{
|
|
150
|
+
showForgotPassword: true,
|
|
151
|
+
forgotPasswordStepTarget: 'reset'
|
|
152
|
+
},
|
|
153
|
+
{ children: composedFields }
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
// Composed: the fields came from the children.
|
|
157
|
+
expect(document.querySelectorAll('input[name]')).toHaveLength(2);
|
|
158
|
+
|
|
159
|
+
fireEvent.click(screen.getByText('Forgot password?'));
|
|
160
|
+
|
|
161
|
+
expect(getAuthFlowState('auth')?.active).toBe('reset');
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('stays a navigating link when no step is named', () => {
|
|
165
|
+
renderLoginForm({
|
|
166
|
+
showForgotPassword: true,
|
|
167
|
+
forgotPasswordUrl: '/auth/forgot-password'
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
const control = screen.getByText('Forgot password?');
|
|
171
|
+
|
|
172
|
+
expect(control.tagName).toBe('A');
|
|
173
|
+
expect(control.getAttribute('href')).toBe('/auth/forgot-password');
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
describe('the reset form as a step', () => {
|
|
178
|
+
it('draws its own message instead of the browser bubble', () => {
|
|
179
|
+
renderForgotPassword({ requiredMessage: 'Bu alan gereklidir.' });
|
|
180
|
+
|
|
181
|
+
fireEvent.submit(document.querySelector('form') as HTMLFormElement);
|
|
182
|
+
|
|
183
|
+
expect((document.querySelector('form') as HTMLFormElement).noValidate).toBe(
|
|
184
|
+
true
|
|
185
|
+
);
|
|
186
|
+
expect(screen.getByText('Bu alan gereklidir.')).toBeTruthy();
|
|
187
|
+
// Nothing was asked of commerce.
|
|
188
|
+
expect(forgotPassword).not.toHaveBeenCalled();
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('fills the block wrapper in both of its states', () => {
|
|
192
|
+
renderForgotPassword();
|
|
193
|
+
expect(
|
|
194
|
+
(document.querySelector('form') as HTMLFormElement).classList
|
|
195
|
+
).toContain('w-full');
|
|
196
|
+
|
|
197
|
+
cleanup();
|
|
198
|
+
mutationState = { isLoading: false, isSuccess: true };
|
|
199
|
+
renderForgotPassword();
|
|
200
|
+
expect(
|
|
201
|
+
(
|
|
202
|
+
document.querySelector(
|
|
203
|
+
'[data-forgot-password-state="success"]'
|
|
204
|
+
) as HTMLElement
|
|
205
|
+
).classList
|
|
206
|
+
).toContain('w-full');
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('sends the mail once the address is there', () => {
|
|
210
|
+
renderForgotPassword();
|
|
211
|
+
|
|
212
|
+
fireEvent.change(document.querySelector('input[name="email"]')!, {
|
|
213
|
+
target: { value: 'shopper@example.com' }
|
|
214
|
+
});
|
|
215
|
+
fireEvent.submit(document.querySelector('form') as HTMLFormElement);
|
|
216
|
+
|
|
217
|
+
expect(forgotPassword).toHaveBeenCalledWith({
|
|
218
|
+
email: 'shopper@example.com'
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('offers the way back from the success state', () => {
|
|
223
|
+
mutationState = { isLoading: false, isSuccess: true };
|
|
224
|
+
renderForgotPassword({
|
|
225
|
+
backStepTarget: 'login',
|
|
226
|
+
backLabel: 'Girişe dön'
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
const back = screen.getByText('Girişe dön');
|
|
230
|
+
fireEvent.click(back);
|
|
231
|
+
|
|
232
|
+
expect(getAuthFlowState('auth')?.active).toBe('login');
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it('keeps the success state as it was when no step is named', () => {
|
|
236
|
+
mutationState = { isLoading: false, isSuccess: true };
|
|
237
|
+
renderForgotPassword();
|
|
238
|
+
|
|
239
|
+
expect(
|
|
240
|
+
document.querySelector('[data-forgot-password-state="success"]')
|
|
241
|
+
).toBeTruthy();
|
|
242
|
+
expect(document.querySelector('[data-forgot-password-back]')).toBeNull();
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it('shows the back control in the designer without moving the flow', () => {
|
|
246
|
+
renderForgotPassword(
|
|
247
|
+
{ backStepTarget: 'login', previewSuccess: true },
|
|
248
|
+
{ isDesigner: true }
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
fireEvent.click(screen.getByText('Back to log in'));
|
|
252
|
+
|
|
253
|
+
// The canvas has its own step switcher; a click here must not move the
|
|
254
|
+
// flow under the designer's feet.
|
|
255
|
+
expect(getAuthFlowState('auth')).toBeNull();
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it('hands its submit to a Button elsewhere on the page', () => {
|
|
259
|
+
renderForgotPassword({
|
|
260
|
+
formHtmlId: 'reset-form',
|
|
261
|
+
showSubmit: false
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
expect((document.querySelector('form') as HTMLFormElement).id).toBe(
|
|
265
|
+
'reset-form'
|
|
266
|
+
);
|
|
267
|
+
expect(document.querySelector('[data-forgot-password-submit]')).toBeNull();
|
|
268
|
+
});
|
|
269
|
+
});
|