@akinon/pz-theme 2.0.95-rc.0 → 2.0.95
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 +8 -33
- package/package.json +4 -4
- package/src/__tests__/auth-captcha.test.tsx +261 -0
- package/src/__tests__/auth-commerce-events.test.tsx +5 -1
- package/src/__tests__/auth-destination.test.tsx +348 -0
- package/src/__tests__/auth-error-reading.test.ts +232 -0
- package/src/__tests__/auth-flow-scope.test.tsx +162 -0
- package/src/__tests__/auth-form-error-block.test.tsx +2 -0
- package/src/__tests__/auth-form-success-block.test.tsx +429 -0
- package/src/__tests__/auth-form-validation.test.tsx +2 -0
- package/src/__tests__/auth-slot-coverage.test.ts +9 -1
- package/src/__tests__/auth-step-title.test.tsx +2 -0
- package/src/__tests__/block-action-audience.test.ts +154 -0
- package/src/__tests__/detached-submit-state.test.tsx +80 -2
- package/src/__tests__/forgot-password-composed.test.tsx +2 -0
- package/src/__tests__/form-state-composition.test.tsx +484 -0
- package/src/__tests__/modal-login-close.test.tsx +366 -0
- package/src/__tests__/otp-form-composed.test.tsx +2 -0
- package/src/__tests__/password-reset-step.test.tsx +2 -0
- package/src/__tests__/register-form-composed.test.tsx +253 -4
- package/src/__tests__/registry-parity.test.ts +12 -0
- package/src/__tests__/reset-password-composed.test.tsx +385 -0
- package/src/auth/auth-captcha.tsx +125 -0
- package/src/auth/auth-destination.ts +168 -0
- package/src/auth/auth-field-errors.tsx +31 -0
- package/src/auth/form-state-context.tsx +170 -0
- package/src/auth/form-success-context.tsx +56 -0
- package/src/auth/read-auth-error.ts +332 -0
- package/src/blocks/__tests__/slider-block.test.tsx +265 -1
- package/src/blocks/account-button-block.tsx +25 -0
- package/src/blocks/add-to-cart-block.tsx +15 -0
- package/src/blocks/auth-field-block.tsx +13 -3
- package/src/blocks/auth-flow-block.tsx +45 -3
- package/src/blocks/auth-form-block.tsx +337 -54
- package/src/blocks/auth-success-block.tsx +85 -0
- package/src/blocks/block-renderer-registry.tsx +8 -0
- package/src/blocks/button-block.tsx +43 -13
- package/src/blocks/captcha-block.tsx +61 -0
- package/src/blocks/change-password-form-block.tsx +15 -0
- package/src/blocks/forgot-password-block.tsx +132 -9
- package/src/blocks/form-state-block.tsx +84 -0
- package/src/blocks/newsletter-form-block.tsx +16 -0
- package/src/blocks/otp-form-block.tsx +161 -67
- package/src/blocks/profile-form-block.tsx +13 -0
- package/src/blocks/register-form-block.tsx +362 -110
- package/src/blocks/reset-password-block.tsx +717 -0
- package/src/blocks/slider-block.tsx +170 -4
- package/src/chrome/__tests__/theme-config-source.test.ts +2 -1
- package/src/chrome/placeholder-slugs.ts +3 -0
- package/src/chrome/route-slug.ts +11 -0
- package/src/index.ts +10 -0
- package/src/overlays/__tests__/modal-loading.test.tsx +235 -0
- package/src/overlays/__tests__/modal-registry.test.ts +5 -1
- package/src/overlays/modal-chrome-css.ts +8 -1
- package/src/overlays/modal-host-context.tsx +66 -0
- package/src/overlays/modal-preview-host.tsx +7 -2
- package/src/overlays/modal-registry.ts +38 -1
- package/src/overlays/overlay-loading.tsx +144 -0
- package/src/overlays/overlay-surface.tsx +23 -0
- package/src/overlays/overlay-tokens.ts +12 -0
- package/src/overlays/theme-modal-mount.tsx +85 -6
- package/src/utils/visibility-rules.ts +114 -0
|
@@ -59,6 +59,8 @@ jest.mock('@akinon/next/data/client/user', () => {
|
|
|
59
59
|
});
|
|
60
60
|
|
|
61
61
|
jest.mock('next/navigation', () => ({
|
|
62
|
+
usePathname: () => '/',
|
|
63
|
+
useSearchParams: () => new URLSearchParams(),
|
|
62
64
|
useRouter: () => ({ push: jest.fn(), refresh: jest.fn(), replace: jest.fn() })
|
|
63
65
|
}));
|
|
64
66
|
|
|
@@ -72,10 +74,15 @@ const blockOf = (type: string, properties: Record<string, unknown>): Block => ({
|
|
|
72
74
|
hidden: false
|
|
73
75
|
});
|
|
74
76
|
|
|
75
|
-
const renderCta = (
|
|
77
|
+
const renderCta = (properties: string | Record<string, unknown>) =>
|
|
76
78
|
render(
|
|
77
79
|
<ButtonBlock
|
|
78
|
-
block={blockOf(
|
|
80
|
+
block={blockOf(
|
|
81
|
+
'button',
|
|
82
|
+
typeof properties === 'string'
|
|
83
|
+
? { text: 'Continue', submitFormId: properties }
|
|
84
|
+
: { text: 'Continue', ...properties }
|
|
85
|
+
)}
|
|
79
86
|
placeholderId="placeholder-1"
|
|
80
87
|
sectionId="section-1"
|
|
81
88
|
currentBreakpoint="desktop"
|
|
@@ -214,3 +221,74 @@ describe('a detached submit CTA', () => {
|
|
|
214
221
|
expect(forgotPassword).not.toHaveBeenCalled();
|
|
215
222
|
});
|
|
216
223
|
});
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* What the shopper SEES while the request is in flight.
|
|
227
|
+
*
|
|
228
|
+
* `disabled` alone changed nothing visible: a Button carries an authored
|
|
229
|
+
* background and colour, and those override what a browser paints on a
|
|
230
|
+
* disabled button — so a drawer's bottom-bar CTA stayed bright and
|
|
231
|
+
* clickable-looking for the whole POST, which is what makes someone press it
|
|
232
|
+
* twice. The state now travels as part state (so a designer can dress it) and
|
|
233
|
+
* carries a default dimming (so it reads as busy with nothing authored).
|
|
234
|
+
*/
|
|
235
|
+
describe('a detached CTA while its form is working', () => {
|
|
236
|
+
it('says so in its part state and dims itself', () => {
|
|
237
|
+
const { container } = renderCta({ submitFormId: 'login-form' });
|
|
238
|
+
|
|
239
|
+
const button = container.querySelector('button') as HTMLButtonElement;
|
|
240
|
+
|
|
241
|
+
expect(button.getAttribute('data-state')).toBeNull();
|
|
242
|
+
|
|
243
|
+
act(() => {
|
|
244
|
+
publishFormSubmitState('login-form', true);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
expect(button.getAttribute('data-state')).toContain('loading');
|
|
248
|
+
expect(button.getAttribute('data-state')).toContain('disabled');
|
|
249
|
+
expect(button.style.opacity).toBe('0.7');
|
|
250
|
+
expect(button.style.cursor).toBe('not-allowed');
|
|
251
|
+
expect(button).toBeDisabled();
|
|
252
|
+
|
|
253
|
+
act(() => {
|
|
254
|
+
publishFormSubmitState('login-form', false);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
expect(button.getAttribute('data-state')).toBeNull();
|
|
258
|
+
expect(button.style.opacity).not.toBe('0.7');
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it('swaps its caption only when the designer authored one', () => {
|
|
262
|
+
const { container } = renderCta({
|
|
263
|
+
submitFormId: 'login-form',
|
|
264
|
+
text: 'Devam Et',
|
|
265
|
+
busyText: 'Gönderiliyor…'
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
act(() => {
|
|
269
|
+
publishFormSubmitState('login-form', true);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
expect(container.textContent).toContain('Gönderiliyor…');
|
|
273
|
+
|
|
274
|
+
act(() => {
|
|
275
|
+
publishFormSubmitState('login-form', false);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
expect(container.textContent).toContain('Devam Et');
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it('keeps the caption put when no busy text was authored', () => {
|
|
282
|
+
const { container } = renderCta({
|
|
283
|
+
submitFormId: 'login-form',
|
|
284
|
+
text: 'Devam Et'
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
act(() => {
|
|
288
|
+
publishFormSubmitState('login-form', true);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
// Absence-preserving, exactly like the forms' own `busyLabel`.
|
|
292
|
+
expect(container.textContent).toContain('Devam Et');
|
|
293
|
+
});
|
|
294
|
+
});
|
|
@@ -67,6 +67,8 @@ jest.mock('@akinon/next/data/client/user', () => {
|
|
|
67
67
|
});
|
|
68
68
|
|
|
69
69
|
jest.mock('next/navigation', () => ({
|
|
70
|
+
usePathname: () => '/',
|
|
71
|
+
useSearchParams: () => new URLSearchParams(),
|
|
70
72
|
useRouter: () => ({ push: jest.fn(), refresh: jest.fn(), replace: jest.fn() })
|
|
71
73
|
}));
|
|
72
74
|
|
|
@@ -0,0 +1,484 @@
|
|
|
1
|
+
import { fireEvent, render, screen, waitFor } 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 ResetPasswordBlock from '../blocks/reset-password-block';
|
|
7
|
+
import {
|
|
8
|
+
containsFormStateBlock,
|
|
9
|
+
matchesFormState
|
|
10
|
+
} from '../auth/form-state-context';
|
|
11
|
+
import { __resetAuthFlowBusForTests } from '../auth/auth-flow-bus';
|
|
12
|
+
import { Block } from '../theme-block';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* A form's OTHER screens, composed the way its normal one already was.
|
|
16
|
+
*
|
|
17
|
+
* The fields, the message and the CTA had been blocks for a while. Every other
|
|
18
|
+
* state was still the form's own markup with one authored string in it: one
|
|
19
|
+
* sentence for the confirmation, one for a dead link, one for "checking", and
|
|
20
|
+
* a busy moment nobody could dress at all. So a brand could build its login
|
|
21
|
+
* screen and could not build the screen it turns into — which is what the
|
|
22
|
+
* complaint "I want to build these screens one by one, from the success
|
|
23
|
+
* message to the error; the success message should be a text if I want and a
|
|
24
|
+
* component if I want" is about.
|
|
25
|
+
*
|
|
26
|
+
* A Form State container is that: a named column the form draws when it is on
|
|
27
|
+
* that screen, holding whatever the designer puts in it.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
let forgotState = { isLoading: false, isSuccess: false };
|
|
31
|
+
const forgotPassword = jest.fn(() => ({ unwrap: () => Promise.resolve({}) }));
|
|
32
|
+
|
|
33
|
+
const passwordReset = jest.fn();
|
|
34
|
+
const unwrap = jest.fn();
|
|
35
|
+
let resetState = { isLoading: false, isSuccess: false };
|
|
36
|
+
let validation: {
|
|
37
|
+
data?: { validlink: boolean };
|
|
38
|
+
isLoading: boolean;
|
|
39
|
+
isError: boolean;
|
|
40
|
+
} = { data: { validlink: true }, isLoading: false, isError: false };
|
|
41
|
+
|
|
42
|
+
/** One mock for both forms; see the note in auth-form-success-block.test.tsx. */
|
|
43
|
+
jest.mock('@akinon/next/data/client/user', () => {
|
|
44
|
+
const handler = {
|
|
45
|
+
get(_target: unknown, prop: string) {
|
|
46
|
+
if (prop === '__esModule') return true;
|
|
47
|
+
if (typeof prop !== 'string') return undefined;
|
|
48
|
+
if (prop === 'useForgotPasswordMutation') {
|
|
49
|
+
return () => [forgotPassword, forgotState];
|
|
50
|
+
}
|
|
51
|
+
if (prop === 'usePasswordResetMutation') {
|
|
52
|
+
return () => [
|
|
53
|
+
(...args: unknown[]) => {
|
|
54
|
+
passwordReset(...args);
|
|
55
|
+
|
|
56
|
+
return { unwrap };
|
|
57
|
+
},
|
|
58
|
+
resetState
|
|
59
|
+
];
|
|
60
|
+
}
|
|
61
|
+
if (prop === 'useGetValidatePasswordResetTokenQuery') {
|
|
62
|
+
return () => validation;
|
|
63
|
+
}
|
|
64
|
+
if (prop.endsWith('Mutation')) {
|
|
65
|
+
return () => [
|
|
66
|
+
jest.fn(() => ({ unwrap: () => Promise.resolve({}) })),
|
|
67
|
+
{ isLoading: false, isSuccess: false, isError: false }
|
|
68
|
+
];
|
|
69
|
+
}
|
|
70
|
+
if (prop.endsWith('Query')) {
|
|
71
|
+
return () => ({
|
|
72
|
+
data: undefined,
|
|
73
|
+
isLoading: false,
|
|
74
|
+
isSuccess: true,
|
|
75
|
+
refetch: jest.fn()
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
return () => undefined;
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
return new Proxy({}, handler);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const signIn = jest.fn(() => Promise.resolve({ error: null, url: '/' }));
|
|
86
|
+
|
|
87
|
+
jest.mock('next-auth/react', () => ({
|
|
88
|
+
signIn: (...args: unknown[]) => signIn(...args),
|
|
89
|
+
useSession: () => ({ data: null, status: 'unauthenticated' })
|
|
90
|
+
}));
|
|
91
|
+
|
|
92
|
+
jest.mock('next/navigation', () => ({
|
|
93
|
+
useParams: () => ({ id: ['tok', 'abc123'] }),
|
|
94
|
+
usePathname: () => '/users/reset/tok/abc123',
|
|
95
|
+
useSearchParams: () => new URLSearchParams(),
|
|
96
|
+
useRouter: () => ({ push: jest.fn(), refresh: jest.fn(), replace: jest.fn() })
|
|
97
|
+
}));
|
|
98
|
+
|
|
99
|
+
const block = (overrides: Partial<Block>): Block =>
|
|
100
|
+
({
|
|
101
|
+
id: 'blk',
|
|
102
|
+
type: 'text',
|
|
103
|
+
label: 'Block',
|
|
104
|
+
value: '',
|
|
105
|
+
order: 0,
|
|
106
|
+
hidden: false,
|
|
107
|
+
properties: {},
|
|
108
|
+
styles: {},
|
|
109
|
+
...overrides
|
|
110
|
+
}) as Block;
|
|
111
|
+
|
|
112
|
+
const text = (id: string, value: string, order = 0) =>
|
|
113
|
+
block({ id, type: 'text', label: 'Text', value, order });
|
|
114
|
+
|
|
115
|
+
/** A screen the designer composed: a container plus whatever is inside it. */
|
|
116
|
+
const stateBox = (
|
|
117
|
+
formState: string,
|
|
118
|
+
children: Block[],
|
|
119
|
+
order = 0,
|
|
120
|
+
overrides: Partial<Block> = {}
|
|
121
|
+
) =>
|
|
122
|
+
block({
|
|
123
|
+
id: `box-${formState}`,
|
|
124
|
+
type: 'form-state',
|
|
125
|
+
label: 'Form State',
|
|
126
|
+
properties: { formState },
|
|
127
|
+
blocks: children,
|
|
128
|
+
order,
|
|
129
|
+
...overrides
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
const emailField = block({
|
|
133
|
+
id: 'field-email',
|
|
134
|
+
type: 'auth-field',
|
|
135
|
+
label: 'E-mail Field',
|
|
136
|
+
properties: { fieldType: 'email', label: 'E-mail' },
|
|
137
|
+
order: 0
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
const renderForgot = (
|
|
141
|
+
children: Block[],
|
|
142
|
+
properties: Record<string, unknown> = {},
|
|
143
|
+
options: { isDesigner?: boolean; selectedBlockId?: string } = {}
|
|
144
|
+
) =>
|
|
145
|
+
render(
|
|
146
|
+
<ForgotPasswordBlock
|
|
147
|
+
block={
|
|
148
|
+
block({
|
|
149
|
+
id: 'forgot-1',
|
|
150
|
+
type: 'forgot-password',
|
|
151
|
+
label: 'Forgot Password',
|
|
152
|
+
properties: { showSubmit: false, ...properties },
|
|
153
|
+
blocks: children
|
|
154
|
+
}) as Block
|
|
155
|
+
}
|
|
156
|
+
placeholderId="placeholder-1"
|
|
157
|
+
sectionId="section-1"
|
|
158
|
+
currentBreakpoint="desktop"
|
|
159
|
+
isDesigner={options.isDesigner ?? false}
|
|
160
|
+
selectedBlockId={options.selectedBlockId}
|
|
161
|
+
/>
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
const renderReset = (
|
|
165
|
+
children: Block[],
|
|
166
|
+
properties: Record<string, unknown> = {}
|
|
167
|
+
) =>
|
|
168
|
+
render(
|
|
169
|
+
<ResetPasswordBlock
|
|
170
|
+
block={
|
|
171
|
+
block({
|
|
172
|
+
id: 'reset-1',
|
|
173
|
+
type: 'reset-password',
|
|
174
|
+
label: 'Reset Password',
|
|
175
|
+
properties: { showSubmit: false, ...properties },
|
|
176
|
+
blocks: children
|
|
177
|
+
}) as Block
|
|
178
|
+
}
|
|
179
|
+
placeholderId="placeholder-1"
|
|
180
|
+
sectionId="section-1"
|
|
181
|
+
currentBreakpoint="desktop"
|
|
182
|
+
/>
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
beforeEach(() => {
|
|
186
|
+
__resetAuthFlowBusForTests();
|
|
187
|
+
forgotState = { isLoading: false, isSuccess: false };
|
|
188
|
+
resetState = { isLoading: false, isSuccess: false };
|
|
189
|
+
validation = { data: { validlink: true }, isLoading: false, isError: false };
|
|
190
|
+
forgotPassword.mockClear();
|
|
191
|
+
forgotPassword.mockImplementation(() => ({
|
|
192
|
+
unwrap: () => Promise.resolve({})
|
|
193
|
+
}));
|
|
194
|
+
passwordReset.mockClear();
|
|
195
|
+
unwrap.mockReset();
|
|
196
|
+
unwrap.mockResolvedValue(undefined);
|
|
197
|
+
signIn.mockClear();
|
|
198
|
+
signIn.mockImplementation(() => Promise.resolve({ error: null, url: '/' }));
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
describe('which screen a container belongs to', () => {
|
|
202
|
+
const filling = { screen: 'form' as const, busy: false, hasError: false };
|
|
203
|
+
|
|
204
|
+
it('keeps the form on screen while it is busy and while it was refused', () => {
|
|
205
|
+
// The rule the whole shape exists for: a single exclusive state would take
|
|
206
|
+
// the fields away the moment commerce answered with a message.
|
|
207
|
+
expect(matchesFormState('form', { ...filling, busy: true })).toBe(true);
|
|
208
|
+
expect(matchesFormState('form', { ...filling, hasError: true })).toBe(true);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('shows the busy and refused screens only when they are true', () => {
|
|
212
|
+
expect(matchesFormState('busy', filling)).toBe(false);
|
|
213
|
+
expect(matchesFormState('busy', { ...filling, busy: true })).toBe(true);
|
|
214
|
+
expect(matchesFormState('error', filling)).toBe(false);
|
|
215
|
+
expect(matchesFormState('error', { ...filling, hasError: true })).toBe(
|
|
216
|
+
true
|
|
217
|
+
);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it('does not carry them onto a finished form', () => {
|
|
221
|
+
const done = { screen: 'success' as const, busy: true, hasError: true };
|
|
222
|
+
|
|
223
|
+
// "Busy" and "refused" happen WHILE filling. On the confirmation they are
|
|
224
|
+
// leftovers, and a spinner over a success screen is a bug.
|
|
225
|
+
expect(matchesFormState('busy', done)).toBe(false);
|
|
226
|
+
expect(matchesFormState('error', done)).toBe(false);
|
|
227
|
+
expect(matchesFormState('form', done)).toBe(false);
|
|
228
|
+
expect(matchesFormState('success', done)).toBe(true);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it('treats the four screens as exclusive', () => {
|
|
232
|
+
const expired = { screen: 'expired' as const, busy: false, hasError: false };
|
|
233
|
+
|
|
234
|
+
expect(matchesFormState('expired', expired)).toBe(true);
|
|
235
|
+
expect(matchesFormState('success', expired)).toBe(false);
|
|
236
|
+
expect(matchesFormState('checking', expired)).toBe(false);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('draws nothing for a state it does not recognise', () => {
|
|
240
|
+
// A container written by a newer editor was scoped deliberately; drawing it
|
|
241
|
+
// on every screen would be worse than drawing it nowhere.
|
|
242
|
+
expect(matchesFormState('whenever', filling)).toBe(false);
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
describe('finding a composed screen', () => {
|
|
247
|
+
it('finds one at any depth, and only for the state asked about', () => {
|
|
248
|
+
const children = [
|
|
249
|
+
block({
|
|
250
|
+
id: 'group',
|
|
251
|
+
type: 'group',
|
|
252
|
+
blocks: [stateBox('success', [text('t', 'Done')])]
|
|
253
|
+
})
|
|
254
|
+
];
|
|
255
|
+
|
|
256
|
+
expect(containsFormStateBlock(children, 'success')).toBe(true);
|
|
257
|
+
expect(containsFormStateBlock(children, 'expired')).toBe(false);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it('ignores a hidden container, which draws nothing wherever it sits', () => {
|
|
261
|
+
const children = [
|
|
262
|
+
stateBox('success', [text('t', 'Done')], 0, { hidden: true })
|
|
263
|
+
];
|
|
264
|
+
|
|
265
|
+
// Standing the built-in confirmation down for a container that draws
|
|
266
|
+
// nothing would leave the shopper with no answer at all.
|
|
267
|
+
expect(containsFormStateBlock(children, 'success')).toBe(false);
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
describe('a password-reset request whose screens are composed', () => {
|
|
272
|
+
const composition = [
|
|
273
|
+
stateBox('form', [emailField], 0),
|
|
274
|
+
stateBox('success', [text('done', 'Kutunuzu kontrol edin.')], 1)
|
|
275
|
+
];
|
|
276
|
+
|
|
277
|
+
it('shows the form screen and nothing else before anything happens', () => {
|
|
278
|
+
const { container } = renderForgot(composition);
|
|
279
|
+
|
|
280
|
+
expect(container.querySelector('input[name="email"]')).not.toBeNull();
|
|
281
|
+
expect(screen.queryByText('Kutunuzu kontrol edin.')).toBeNull();
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it('swaps to the composed confirmation, fields and all', () => {
|
|
285
|
+
forgotState = { isLoading: false, isSuccess: true };
|
|
286
|
+
|
|
287
|
+
const { container } = renderForgot(composition, {
|
|
288
|
+
successLabel: 'Bir bağlantı yolda.'
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
// The composition's own screen — a Text block here, but it could as well be
|
|
292
|
+
// an icon, a heading and a Button, which is the whole point.
|
|
293
|
+
expect(screen.getByText('Kutunuzu kontrol edin.')).toBeInTheDocument();
|
|
294
|
+
// The fields went with the screen they were scoped to.
|
|
295
|
+
expect(container.querySelector('input[name="email"]')).toBeNull();
|
|
296
|
+
// And the form's own sentence is not drawn on top of it.
|
|
297
|
+
expect(screen.queryByText('Bir bağlantı yolda.')).toBeNull();
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it('drops the built-in submit, which would be an orphan there', () => {
|
|
301
|
+
forgotState = { isLoading: false, isSuccess: true };
|
|
302
|
+
|
|
303
|
+
const { container } = renderForgot(composition, { showSubmit: true });
|
|
304
|
+
|
|
305
|
+
expect(
|
|
306
|
+
container.querySelector('[data-forgot-password-submit]')
|
|
307
|
+
).toBeNull();
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
it('keeps the fields when a refusal comes back', async () => {
|
|
311
|
+
forgotPassword.mockImplementation(() => ({
|
|
312
|
+
unwrap: () =>
|
|
313
|
+
Promise.reject({ data: { non_field_errors: ['Bilinmeyen adres.'] } })
|
|
314
|
+
}));
|
|
315
|
+
|
|
316
|
+
const { container } = renderForgot([
|
|
317
|
+
...composition,
|
|
318
|
+
stateBox('error', [text('oops', 'Bir şeyler ters gitti.')], 2)
|
|
319
|
+
]);
|
|
320
|
+
|
|
321
|
+
fireEvent.change(container.querySelector('input[name="email"]')!, {
|
|
322
|
+
target: { value: 'ada@example.com' }
|
|
323
|
+
});
|
|
324
|
+
fireEvent.submit(container.querySelector('form')!);
|
|
325
|
+
|
|
326
|
+
await waitFor(() =>
|
|
327
|
+
expect(screen.getByText('Bir şeyler ters gitti.')).toBeInTheDocument()
|
|
328
|
+
);
|
|
329
|
+
// The one thing a refusal must never do: take away what has to be corrected.
|
|
330
|
+
expect(container.querySelector('input[name="email"]')).not.toBeNull();
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
it('shows the busy screen while the request is in flight', () => {
|
|
334
|
+
forgotState = { isLoading: true, isSuccess: false };
|
|
335
|
+
|
|
336
|
+
renderForgot([
|
|
337
|
+
...composition,
|
|
338
|
+
stateBox('busy', [text('wait', 'Gönderiliyor…')], 2)
|
|
339
|
+
]);
|
|
340
|
+
|
|
341
|
+
expect(screen.getByText('Gönderiliyor…')).toBeInTheDocument();
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
it('draws a child that is not a container on every screen', () => {
|
|
345
|
+
forgotState = { isLoading: false, isSuccess: true };
|
|
346
|
+
|
|
347
|
+
renderForgot([...composition, text('logo', 'GANT', 2)]);
|
|
348
|
+
|
|
349
|
+
// Shared chrome: a logo, a title, a support line — the same rule the OTP
|
|
350
|
+
// form applies to a child that is not a phase.
|
|
351
|
+
expect(screen.getByText('GANT')).toBeInTheDocument();
|
|
352
|
+
expect(screen.getByText('Kutunuzu kontrol edin.')).toBeInTheDocument();
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
it('shows a screen being edited in the canvas, previewed or not', () => {
|
|
356
|
+
renderForgot(composition, {}, {
|
|
357
|
+
isDesigner: true,
|
|
358
|
+
selectedBlockId: 'done'
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
// The form is in its normal state, but the designer is inside the success
|
|
362
|
+
// screen — so it is drawn, or it could not be dressed at all.
|
|
363
|
+
expect(screen.getByText('Kutunuzu kontrol edin.')).toBeInTheDocument();
|
|
364
|
+
});
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
describe('the screen the reset mail lands on', () => {
|
|
368
|
+
const passwordBox = block({
|
|
369
|
+
id: 'field-password',
|
|
370
|
+
type: 'auth-field',
|
|
371
|
+
label: 'Password',
|
|
372
|
+
properties: { fieldType: 'password', label: 'New password' },
|
|
373
|
+
order: 0
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
it('composes the dead-link screen instead of the built-in sentence', () => {
|
|
377
|
+
validation = {
|
|
378
|
+
data: { validlink: false },
|
|
379
|
+
isLoading: false,
|
|
380
|
+
isError: false
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
const { container } = renderReset(
|
|
384
|
+
[
|
|
385
|
+
stateBox('form', [passwordBox], 0),
|
|
386
|
+
stateBox('expired', [text('dead', 'Bağlantının süresi geçmiş.')], 1)
|
|
387
|
+
],
|
|
388
|
+
{ expiredLabel: 'This link has expired.' }
|
|
389
|
+
);
|
|
390
|
+
|
|
391
|
+
expect(screen.getByText('Bağlantının süresi geçmiş.')).toBeInTheDocument();
|
|
392
|
+
expect(screen.queryByText('This link has expired.')).toBeNull();
|
|
393
|
+
// Nothing to submit on a dead link.
|
|
394
|
+
expect(container.querySelector('input[name="password"]')).toBeNull();
|
|
395
|
+
expect(container.querySelector('[data-reset-password-submit]')).toBeNull();
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
it('composes the checking screen too', () => {
|
|
399
|
+
validation = { data: undefined, isLoading: true, isError: false };
|
|
400
|
+
|
|
401
|
+
renderReset([
|
|
402
|
+
stateBox('form', [passwordBox], 0),
|
|
403
|
+
stateBox('checking', [text('wait', 'Bağlantı kontrol ediliyor…')], 1)
|
|
404
|
+
]);
|
|
405
|
+
|
|
406
|
+
expect(
|
|
407
|
+
screen.getByText('Bağlantı kontrol ediliyor…')
|
|
408
|
+
).toBeInTheDocument();
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
it('still falls back to the built-in screens where none was composed', () => {
|
|
412
|
+
validation = {
|
|
413
|
+
data: { validlink: false },
|
|
414
|
+
isLoading: false,
|
|
415
|
+
isError: false
|
|
416
|
+
};
|
|
417
|
+
|
|
418
|
+
renderReset([stateBox('form', [passwordBox], 0)], {
|
|
419
|
+
expiredLabel: 'This link has expired.'
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
// The fence: a composition that scoped only its form keeps every screen it
|
|
423
|
+
// had before.
|
|
424
|
+
expect(screen.getByText('This link has expired.')).toBeInTheDocument();
|
|
425
|
+
});
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
describe('a login form, which has no confirmation to compose', () => {
|
|
429
|
+
it('scopes the refusal without taking the fields away', async () => {
|
|
430
|
+
signIn.mockImplementation(() =>
|
|
431
|
+
Promise.resolve({
|
|
432
|
+
error: 'CredentialsSignin',
|
|
433
|
+
code: JSON.stringify([
|
|
434
|
+
{ type: 'non_field_errors', data: ['E-posta veya şifre hatalı.'] }
|
|
435
|
+
])
|
|
436
|
+
})
|
|
437
|
+
);
|
|
438
|
+
|
|
439
|
+
const { container } = render(
|
|
440
|
+
<AuthFormBlock
|
|
441
|
+
block={
|
|
442
|
+
block({
|
|
443
|
+
id: 'login-1',
|
|
444
|
+
type: 'auth-form',
|
|
445
|
+
label: 'Auth Form',
|
|
446
|
+
properties: { showSubmit: true },
|
|
447
|
+
blocks: [
|
|
448
|
+
stateBox(
|
|
449
|
+
'form',
|
|
450
|
+
[
|
|
451
|
+
emailField,
|
|
452
|
+
block({
|
|
453
|
+
id: 'field-password',
|
|
454
|
+
type: 'auth-field',
|
|
455
|
+
properties: { fieldType: 'password', label: 'Password' },
|
|
456
|
+
order: 1
|
|
457
|
+
})
|
|
458
|
+
],
|
|
459
|
+
0
|
|
460
|
+
),
|
|
461
|
+
stateBox('error', [text('box', 'Tekrar deneyin.')], 1)
|
|
462
|
+
]
|
|
463
|
+
}) as Block
|
|
464
|
+
}
|
|
465
|
+
placeholderId="placeholder-1"
|
|
466
|
+
sectionId="section-1"
|
|
467
|
+
currentBreakpoint="desktop"
|
|
468
|
+
/>
|
|
469
|
+
);
|
|
470
|
+
|
|
471
|
+
fireEvent.change(container.querySelector('input[name="email"]')!, {
|
|
472
|
+
target: { value: 'ada@example.com' }
|
|
473
|
+
});
|
|
474
|
+
fireEvent.change(container.querySelector('input[name="password"]')!, {
|
|
475
|
+
target: { value: 'hunter2' }
|
|
476
|
+
});
|
|
477
|
+
fireEvent.submit(container.querySelector('form')!);
|
|
478
|
+
|
|
479
|
+
await waitFor(() =>
|
|
480
|
+
expect(screen.getByText('Tekrar deneyin.')).toBeInTheDocument()
|
|
481
|
+
);
|
|
482
|
+
expect(container.querySelector('input[name="email"]')).not.toBeNull();
|
|
483
|
+
});
|
|
484
|
+
});
|