@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,202 @@
|
|
|
1
|
+
import { act, render, screen } from '@testing-library/react';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
|
|
4
|
+
import AuthFlowBlock from '../blocks/auth-flow-block';
|
|
5
|
+
import AuthStepTitleBlock from '../blocks/auth-step-title-block';
|
|
6
|
+
import { __resetAuthFlowBusForTests, setAuthStep } from '../auth/auth-flow-bus';
|
|
7
|
+
import { Block } from '../theme-block';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* One title row for a flow of several steps.
|
|
11
|
+
*
|
|
12
|
+
* A drawer that walks a shopper through steps has ONE close button and ONE
|
|
13
|
+
* title row: the row is a SIBLING of the steps, so it renders once and cannot
|
|
14
|
+
* see which step is up. Authoring the title inside each step gives no such row,
|
|
15
|
+
* and repeating the close button per step is not a shared button. So the step
|
|
16
|
+
* carries its title, the flow — the thing that decided — publishes it, and this
|
|
17
|
+
* block renders it wherever the design puts the row.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
jest.mock('next/navigation', () => ({
|
|
21
|
+
useRouter: () => ({ push: jest.fn(), refresh: jest.fn() })
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
const block = (overrides: Partial<Block>): Block =>
|
|
25
|
+
({
|
|
26
|
+
id: 'blk',
|
|
27
|
+
type: 'text',
|
|
28
|
+
label: 'Block',
|
|
29
|
+
value: '',
|
|
30
|
+
order: 0,
|
|
31
|
+
hidden: false,
|
|
32
|
+
properties: {},
|
|
33
|
+
styles: {},
|
|
34
|
+
blocks: [],
|
|
35
|
+
...overrides
|
|
36
|
+
}) as Block;
|
|
37
|
+
|
|
38
|
+
const step = (key: string, title: string, children: Block[] = []): Block =>
|
|
39
|
+
block({
|
|
40
|
+
id: `step-${key}`,
|
|
41
|
+
type: 'auth-step',
|
|
42
|
+
label: key,
|
|
43
|
+
properties: { stepKey: key, title },
|
|
44
|
+
blocks: children.length
|
|
45
|
+
? children
|
|
46
|
+
: [block({ id: `${key}-body`, type: 'text', value: `${key} body` })]
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const titleRow = block({
|
|
50
|
+
id: 'title-row',
|
|
51
|
+
type: 'auth-step-title',
|
|
52
|
+
properties: { tag: 'h2' }
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const renderFlow = (
|
|
56
|
+
children: Block[],
|
|
57
|
+
properties: Record<string, unknown> = {}
|
|
58
|
+
) =>
|
|
59
|
+
render(
|
|
60
|
+
<AuthFlowBlock
|
|
61
|
+
block={block({
|
|
62
|
+
id: 'flow',
|
|
63
|
+
type: 'auth-flow',
|
|
64
|
+
properties: { flowId: 'auth', initialStep: 'phone', ...properties },
|
|
65
|
+
blocks: children
|
|
66
|
+
})}
|
|
67
|
+
placeholderId="placeholder-1"
|
|
68
|
+
sectionId="section-1"
|
|
69
|
+
currentBreakpoint="desktop"
|
|
70
|
+
/>
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
beforeEach(() => {
|
|
74
|
+
__resetAuthFlowBusForTests();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe('the shared title row', () => {
|
|
78
|
+
it('names the step the flow is showing', () => {
|
|
79
|
+
renderFlow([
|
|
80
|
+
titleRow,
|
|
81
|
+
step('phone', 'ÜYE OLUN VEYA GİRİŞ YAPIN'),
|
|
82
|
+
step('join', 'MYGANT')
|
|
83
|
+
]);
|
|
84
|
+
|
|
85
|
+
expect(screen.getByText('ÜYE OLUN VEYA GİRİŞ YAPIN')).toBeInTheDocument();
|
|
86
|
+
// Only the active step's title: the row is one element, not one per step.
|
|
87
|
+
expect(screen.queryByText('MYGANT')).not.toBeInTheDocument();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('follows the flow to the next step', () => {
|
|
91
|
+
renderFlow([
|
|
92
|
+
titleRow,
|
|
93
|
+
step('phone', 'ÜYE OLUN VEYA GİRİŞ YAPIN'),
|
|
94
|
+
step('join', 'MYGANT')
|
|
95
|
+
]);
|
|
96
|
+
|
|
97
|
+
// Module state, so the re-render it triggers has to be inside act().
|
|
98
|
+
act(() => {
|
|
99
|
+
setAuthStep('auth', 'join');
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
expect(screen.getByText('MYGANT')).toBeInTheDocument();
|
|
103
|
+
expect(
|
|
104
|
+
screen.queryByText('ÜYE OLUN VEYA GİRİŞ YAPIN')
|
|
105
|
+
).not.toBeInTheDocument();
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('renders once however many steps the flow has', () => {
|
|
109
|
+
renderFlow([
|
|
110
|
+
titleRow,
|
|
111
|
+
step('phone', 'Phone'),
|
|
112
|
+
step('join', 'Join'),
|
|
113
|
+
step('code', 'Code')
|
|
114
|
+
]);
|
|
115
|
+
|
|
116
|
+
expect(document.querySelectorAll('[data-auth-step-title]')).toHaveLength(1);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('is a heading by default, for a shopper navigating by them', () => {
|
|
120
|
+
renderFlow([titleRow, step('phone', 'Phone step')]);
|
|
121
|
+
|
|
122
|
+
expect(screen.getByRole('heading', { level: 2 })).toHaveTextContent(
|
|
123
|
+
'Phone step'
|
|
124
|
+
);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('honours the tag a design asks for', () => {
|
|
128
|
+
renderFlow([
|
|
129
|
+
block({
|
|
130
|
+
id: 'title-row',
|
|
131
|
+
type: 'auth-step-title',
|
|
132
|
+
properties: { tag: 'p' }
|
|
133
|
+
}),
|
|
134
|
+
step('phone', 'Phone step')
|
|
135
|
+
]);
|
|
136
|
+
|
|
137
|
+
expect(document.querySelector('p[data-auth-step-title]')).not.toBeNull();
|
|
138
|
+
expect(screen.queryByRole('heading')).toBeNull();
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('refuses a tag that is not one, rather than emitting it', () => {
|
|
142
|
+
renderFlow([
|
|
143
|
+
block({
|
|
144
|
+
id: 'title-row',
|
|
145
|
+
type: 'auth-step-title',
|
|
146
|
+
properties: { tag: 'script' }
|
|
147
|
+
}),
|
|
148
|
+
step('phone', 'Phone step')
|
|
149
|
+
]);
|
|
150
|
+
|
|
151
|
+
expect(document.querySelector('script')).toBeNull();
|
|
152
|
+
expect(screen.getByRole('heading', { level: 2 })).toBeInTheDocument();
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('falls back to its own copy where a step carries no title', () => {
|
|
156
|
+
renderFlow([
|
|
157
|
+
block({
|
|
158
|
+
id: 'title-row',
|
|
159
|
+
type: 'auth-step-title',
|
|
160
|
+
properties: { fallbackTitle: 'Log in' }
|
|
161
|
+
}),
|
|
162
|
+
step('phone', '')
|
|
163
|
+
]);
|
|
164
|
+
|
|
165
|
+
expect(screen.getByText('Log in')).toBeInTheDocument();
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('draws nothing at all for a shopper when there is no title', () => {
|
|
169
|
+
renderFlow([titleRow, step('phone', '')]);
|
|
170
|
+
|
|
171
|
+
// An empty heading is worse than no heading: a screen reader announces it.
|
|
172
|
+
expect(document.querySelector('[data-auth-step-title]')).toBeNull();
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('draws nothing outside a flow either', () => {
|
|
176
|
+
render(
|
|
177
|
+
<AuthStepTitleBlock
|
|
178
|
+
block={titleRow}
|
|
179
|
+
placeholderId="placeholder-1"
|
|
180
|
+
sectionId="section-1"
|
|
181
|
+
currentBreakpoint="desktop"
|
|
182
|
+
/>
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
expect(document.querySelector('[data-auth-step-title]')).toBeNull();
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('says what it is waiting for on the canvas', () => {
|
|
189
|
+
const { container } = render(
|
|
190
|
+
<AuthStepTitleBlock
|
|
191
|
+
block={titleRow}
|
|
192
|
+
placeholderId="placeholder-1"
|
|
193
|
+
sectionId="section-1"
|
|
194
|
+
currentBreakpoint="desktop"
|
|
195
|
+
isDesigner
|
|
196
|
+
/>
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
// Dropping a block that renders nothing looks like nothing happened.
|
|
200
|
+
expect(container.textContent).toContain('Auth Flow');
|
|
201
|
+
});
|
|
202
|
+
});
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { act, fireEvent, render, screen } from '@testing-library/react';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
|
|
4
|
+
import ButtonBlock from '../blocks/button-block';
|
|
5
|
+
import ForgotPasswordBlock from '../blocks/forgot-password-block';
|
|
6
|
+
import {
|
|
7
|
+
__resetFormSubmitBusForTests,
|
|
8
|
+
isFormSubmitting,
|
|
9
|
+
publishFormSubmitState
|
|
10
|
+
} from '../auth/form-submit-bus';
|
|
11
|
+
import { Block } from '../theme-block';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The bottom-bar CTA: a Button that submits a form living somewhere else on the
|
|
15
|
+
* page, through the native `form` attribute.
|
|
16
|
+
*
|
|
17
|
+
* Its own submit button knows when to grey itself out — the state is right
|
|
18
|
+
* there. A detached CTA is outside that form, often outside its React root, so
|
|
19
|
+
* it stayed bright and clickable for the whole request. Pressing it again did
|
|
20
|
+
* nothing (every submit handler guards), and "nothing" is what reads as broken.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
let mutationState = { isLoading: false, isSuccess: false };
|
|
24
|
+
const forgotPassword = jest.fn(() => ({ unwrap: () => Promise.resolve({}) }));
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Every `@akinon/next/data/client/*` path resolves to ONE mock module, so
|
|
28
|
+
* replacing the user path replaces it for the button block's own hook too.
|
|
29
|
+
* The factory therefore keeps the shared proxy's behaviour and only pins the
|
|
30
|
+
* one hook these tests drive.
|
|
31
|
+
*/
|
|
32
|
+
jest.mock('@akinon/next/data/client/user', () => {
|
|
33
|
+
const queryResult = () => ({
|
|
34
|
+
data: undefined,
|
|
35
|
+
isLoading: false,
|
|
36
|
+
isSuccess: true,
|
|
37
|
+
refetch: jest.fn()
|
|
38
|
+
});
|
|
39
|
+
const mutationTuple = () => [
|
|
40
|
+
jest.fn(() => ({ unwrap: () => Promise.resolve({}) })),
|
|
41
|
+
{ isLoading: false, isSuccess: false, reset: jest.fn() }
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
return new Proxy(
|
|
45
|
+
{},
|
|
46
|
+
{
|
|
47
|
+
get(_target, prop) {
|
|
48
|
+
if (prop === '__esModule') return true;
|
|
49
|
+
if (typeof prop !== 'string') return undefined;
|
|
50
|
+
if (prop === 'useForgotPasswordMutation') {
|
|
51
|
+
return () => [forgotPassword, mutationState];
|
|
52
|
+
}
|
|
53
|
+
if (prop.endsWith('Mutation')) return () => mutationTuple();
|
|
54
|
+
if (prop.endsWith('Query')) return () => queryResult();
|
|
55
|
+
return () => undefined;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
jest.mock('next/navigation', () => ({
|
|
62
|
+
useRouter: () => ({ push: jest.fn(), refresh: jest.fn(), replace: jest.fn() })
|
|
63
|
+
}));
|
|
64
|
+
|
|
65
|
+
const blockOf = (type: string, properties: Record<string, unknown>): Block => ({
|
|
66
|
+
id: `${type}-1`,
|
|
67
|
+
type,
|
|
68
|
+
label: type,
|
|
69
|
+
properties,
|
|
70
|
+
styles: {},
|
|
71
|
+
order: 0,
|
|
72
|
+
hidden: false
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const renderCta = (submitFormId: string) =>
|
|
76
|
+
render(
|
|
77
|
+
<ButtonBlock
|
|
78
|
+
block={blockOf('button', { text: 'Continue', submitFormId })}
|
|
79
|
+
placeholderId="placeholder-1"
|
|
80
|
+
sectionId="section-1"
|
|
81
|
+
currentBreakpoint="desktop"
|
|
82
|
+
/>
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
const cta = () => document.querySelector('button') as HTMLButtonElement;
|
|
86
|
+
|
|
87
|
+
beforeEach(() => {
|
|
88
|
+
__resetFormSubmitBusForTests();
|
|
89
|
+
mutationState = { isLoading: false, isSuccess: false };
|
|
90
|
+
forgotPassword.mockClear();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe('a detached submit CTA', () => {
|
|
94
|
+
it('submits the named form natively', () => {
|
|
95
|
+
renderCta('reset-form');
|
|
96
|
+
|
|
97
|
+
// The native `form` attribute is what lets it live outside the form.
|
|
98
|
+
expect(cta().getAttribute('form')).toBe('reset-form');
|
|
99
|
+
expect(cta().getAttribute('type')).toBe('submit');
|
|
100
|
+
expect(cta().disabled).toBe(false);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('greys out while that form is working', () => {
|
|
104
|
+
renderCta('reset-form');
|
|
105
|
+
|
|
106
|
+
act(() => {
|
|
107
|
+
publishFormSubmitState('reset-form', true);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
expect(cta().disabled).toBe(true);
|
|
111
|
+
expect(cta().getAttribute('aria-busy')).toBe('true');
|
|
112
|
+
|
|
113
|
+
act(() => {
|
|
114
|
+
publishFormSubmitState('reset-form', false);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
expect(cta().disabled).toBe(false);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('ignores a different form going busy', () => {
|
|
121
|
+
renderCta('reset-form');
|
|
122
|
+
|
|
123
|
+
act(() => {
|
|
124
|
+
publishFormSubmitState('login-form', true);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
expect(cta().disabled).toBe(false);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('is published by the form that owns the id', () => {
|
|
131
|
+
mutationState = { isLoading: true, isSuccess: false };
|
|
132
|
+
|
|
133
|
+
render(
|
|
134
|
+
<ForgotPasswordBlock
|
|
135
|
+
block={blockOf('forgot-password', { formHtmlId: 'reset-form' })}
|
|
136
|
+
placeholderId="placeholder-1"
|
|
137
|
+
sectionId="section-1"
|
|
138
|
+
currentBreakpoint="desktop"
|
|
139
|
+
/>
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
expect(isFormSubmitting('reset-form')).toBe(true);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('stops claiming to be busy once the form leaves the page', () => {
|
|
146
|
+
mutationState = { isLoading: true, isSuccess: false };
|
|
147
|
+
|
|
148
|
+
const view = render(
|
|
149
|
+
<ForgotPasswordBlock
|
|
150
|
+
block={blockOf('forgot-password', { formHtmlId: 'reset-form' })}
|
|
151
|
+
placeholderId="placeholder-1"
|
|
152
|
+
sectionId="section-1"
|
|
153
|
+
currentBreakpoint="desktop"
|
|
154
|
+
/>
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
expect(isFormSubmitting('reset-form')).toBe(true);
|
|
158
|
+
|
|
159
|
+
view.unmount();
|
|
160
|
+
|
|
161
|
+
// A closed drawer must not leave every CTA on the page disabled.
|
|
162
|
+
expect(isFormSubmitting('reset-form')).toBe(false);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('leaves an ordinary button alone', () => {
|
|
166
|
+
render(
|
|
167
|
+
<ButtonBlock
|
|
168
|
+
block={blockOf('button', { text: 'Shop', tag: 'button' })}
|
|
169
|
+
placeholderId="placeholder-1"
|
|
170
|
+
sectionId="section-1"
|
|
171
|
+
currentBreakpoint="desktop"
|
|
172
|
+
/>
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
act(() => {
|
|
176
|
+
publishFormSubmitState('reset-form', true);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
expect(cta().disabled).toBe(false);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('does not fire the form twice when the CTA is pressed again', () => {
|
|
183
|
+
mutationState = { isLoading: true, isSuccess: false };
|
|
184
|
+
|
|
185
|
+
render(
|
|
186
|
+
<>
|
|
187
|
+
<ForgotPasswordBlock
|
|
188
|
+
block={blockOf('forgot-password', {
|
|
189
|
+
formHtmlId: 'reset-form',
|
|
190
|
+
showSubmit: false
|
|
191
|
+
})}
|
|
192
|
+
placeholderId="placeholder-1"
|
|
193
|
+
sectionId="section-1"
|
|
194
|
+
currentBreakpoint="desktop"
|
|
195
|
+
/>
|
|
196
|
+
<ButtonBlock
|
|
197
|
+
block={blockOf('button', {
|
|
198
|
+
text: 'Continue',
|
|
199
|
+
submitFormId: 'reset-form'
|
|
200
|
+
})}
|
|
201
|
+
placeholderId="placeholder-1"
|
|
202
|
+
sectionId="section-1"
|
|
203
|
+
currentBreakpoint="desktop"
|
|
204
|
+
/>
|
|
205
|
+
</>
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
const button = screen.getByText('Continue').closest('button');
|
|
209
|
+
|
|
210
|
+
expect(button?.disabled).toBe(true);
|
|
211
|
+
|
|
212
|
+
fireEvent.click(button as HTMLButtonElement);
|
|
213
|
+
|
|
214
|
+
expect(forgotPassword).not.toHaveBeenCalled();
|
|
215
|
+
});
|
|
216
|
+
});
|