@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.
Files changed (63) hide show
  1. package/CHANGELOG.md +31 -33
  2. package/package.json +4 -4
  3. package/src/__tests__/auth-flow-block.test.tsx +211 -0
  4. package/src/__tests__/auth-flow-bus.test.ts +290 -0
  5. package/src/__tests__/auth-form-error-block.test.tsx +326 -0
  6. package/src/__tests__/auth-form-validation.test.tsx +204 -0
  7. package/src/__tests__/auth-step-title.test.tsx +202 -0
  8. package/src/__tests__/detached-submit-state.test.tsx +216 -0
  9. package/src/__tests__/forgot-password-composed.test.tsx +313 -0
  10. package/src/__tests__/otp-form-composed.test.tsx +487 -0
  11. package/src/__tests__/password-reset-step.test.tsx +269 -0
  12. package/src/__tests__/register-form-composed.test.tsx +739 -0
  13. package/src/__tests__/registry-parity.test.ts +9 -0
  14. package/src/auth/auth-field-errors.tsx +151 -0
  15. package/src/auth/auth-flow-bus.ts +415 -0
  16. package/src/auth/form-error-context.tsx +55 -0
  17. package/src/auth/form-submit-bus.ts +93 -0
  18. package/src/auth/otp-form-context.tsx +80 -0
  19. package/src/blocks/__tests__/button-block.test.tsx +55 -0
  20. package/src/blocks/__tests__/link-block.test.tsx +48 -30
  21. package/src/blocks/__tests__/overlay-sweep.test.tsx +31 -8
  22. package/src/blocks/__tests__/product-gallery-block.test.tsx +12 -4
  23. package/src/blocks/__tests__/product-recommendations-block.test.tsx +9 -2
  24. package/src/blocks/__tests__/store-locator-block.test.tsx +35 -8
  25. package/src/blocks/__tests__/text-block.test.tsx +37 -3
  26. package/src/blocks/__tests__/variant-selector-block.test.tsx +16 -4
  27. package/src/blocks/auth-consent-block.tsx +214 -0
  28. package/src/blocks/auth-error-block.tsx +82 -0
  29. package/src/blocks/auth-field-block.tsx +270 -0
  30. package/src/blocks/auth-flow-block.tsx +251 -0
  31. package/src/blocks/auth-form-block.tsx +391 -47
  32. package/src/blocks/auth-step-block.tsx +81 -0
  33. package/src/blocks/auth-step-title-block.tsx +81 -0
  34. package/src/blocks/block-renderer-registry.tsx +18 -0
  35. package/src/blocks/button-block.tsx +91 -13
  36. package/src/blocks/forgot-password-block.tsx +318 -24
  37. package/src/blocks/link-block.tsx +22 -84
  38. package/src/blocks/otp-code-input-block.tsx +180 -0
  39. package/src/blocks/otp-form-block.tsx +1092 -0
  40. package/src/blocks/otp-phase-block.tsx +71 -0
  41. package/src/blocks/register-form-block.tsx +541 -74
  42. package/src/blocks/text-block.tsx +17 -40
  43. package/src/chrome/__tests__/theme-config-source.test.ts +4 -1
  44. package/src/hooks/__tests__/use-overlay.test.tsx +97 -5
  45. package/src/hooks/use-block-action.ts +87 -4
  46. package/src/hooks/use-overlay.ts +60 -9
  47. package/src/overlays/__tests__/modal-chrome-padding.test.tsx +119 -0
  48. package/src/overlays/__tests__/modal-registry.test.ts +6 -1
  49. package/src/overlays/__tests__/panel-stretch.test.tsx +60 -0
  50. package/src/overlays/__tests__/responsive-chrome.test.tsx +446 -0
  51. package/src/overlays/__tests__/responsive-presentation.test.tsx +192 -0
  52. package/src/overlays/__tests__/theme-modal-mount.test.tsx +12 -1
  53. package/src/overlays/modal-preview-host.tsx +39 -2
  54. package/src/overlays/modal-registry.ts +344 -4
  55. package/src/overlays/overlay-surface.tsx +249 -19
  56. package/src/overlays/overlay-tokens.ts +94 -0
  57. package/src/overlays/theme-modal-mount.tsx +34 -4
  58. package/src/theme-block.tsx +6 -0
  59. package/src/utils/__tests__/inner-root-styles.test.tsx +139 -0
  60. package/src/utils/__tests__/iterator-bindings.test.tsx +9 -3
  61. package/src/utils/index.ts +107 -6
  62. package/src/utils/iterator-utils.ts +39 -2
  63. 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
+ });