@capillarytech/creatives-library 9.0.57-alpha.1 → 9.0.57-alpha.3
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/package.json +1 -1
- package/v2Components/CommonTestAndPreview/index.js +12 -2
- package/v2Components/FormBuilder/Functional/FormBuilderShell.js +10 -11
- package/v2Components/FormBuilder/Functional/renderers/mpushRenderers.js +9 -6
- package/v2Components/FormBuilder/Functional/tests/mpush.libraryValidity.parity.test.js +24 -11
- package/v2Components/FormBuilder/Functional/tests/mpush.shellEvents.test.js +36 -0
- package/v2Components/FormBuilder/Functional/tests/mpushRenderers.test.js +9 -1
- package/v2Components/HtmlEditor/HTMLEditor.js +12 -1
- package/v2Components/HtmlEditor/__tests__/HTMLEditor.test.js +93 -0
- package/v2Components/HtmlEditor/hooks/useValidation.js +5 -0
package/package.json
CHANGED
|
@@ -1575,8 +1575,18 @@ const CommonTestAndPreview = (props) => {
|
|
|
1575
1575
|
if (formDataObj?.whatsappDocSource) {
|
|
1576
1576
|
whatsappMedia.url = formDataObj.whatsappDocSource;
|
|
1577
1577
|
}
|
|
1578
|
-
|
|
1579
|
-
|
|
1578
|
+
// Prefer whatsappDocParams for document data; docPreview may be a React preview element.
|
|
1579
|
+
// Fall back to docPreview only when it contains plain document data.
|
|
1580
|
+
const isPlainDocParamsObject = (value) =>
|
|
1581
|
+
!!value && typeof value === 'object' && !React.isValidElement(value);
|
|
1582
|
+
const getDocParams = (formDataObj) => {
|
|
1583
|
+
const { whatsappDocParams, docPreview } = formDataObj || {};
|
|
1584
|
+
if (isPlainDocParamsObject(whatsappDocParams)) return whatsappDocParams;
|
|
1585
|
+
if (isPlainDocParamsObject(docPreview)) return docPreview;
|
|
1586
|
+
return null;
|
|
1587
|
+
};
|
|
1588
|
+
const docParams = getDocParams(formDataObj);
|
|
1589
|
+
if (docParams) {
|
|
1580
1590
|
whatsappMedia.previewUrl = docParams?.whatsappDocImg || '';
|
|
1581
1591
|
whatsappMedia.docParams = {
|
|
1582
1592
|
whatsappDocName: docParams?.whatsappDocName || '',
|
|
@@ -208,10 +208,8 @@ const FormBuilderShell = (props) => {
|
|
|
208
208
|
);
|
|
209
209
|
useEffect(() => () => debouncedEmit.cancel(), [debouncedEmit]);
|
|
210
210
|
|
|
211
|
-
// Classic pushes formData up
|
|
212
|
-
//
|
|
213
|
-
// 740-748) — the MPUSH containers read that copy (copy-content handlers) before
|
|
214
|
-
// any user input. MPUSH-only; SMS keeps its shipped behavior.
|
|
211
|
+
// Classic pushes formData up on init and on every schema re-init (Classic.js:1906-1911 ->
|
|
212
|
+
// 740-748); the MPUSH copy-content handlers read that copy. MPUSH-only, SMS unchanged.
|
|
215
213
|
const emitFormDataOnSchemaSync = Boolean(adapter.config?.features?.emitFormDataOnSchemaSync);
|
|
216
214
|
|
|
217
215
|
// ---- initialize once the (possibly async) schema is ready, then validate+emit ----
|
|
@@ -275,8 +273,11 @@ const FormBuilderShell = (props) => {
|
|
|
275
273
|
const isEcho = isEqual(parentFormData, lastEmittedRef.current) || isEqual(parentFormData, current);
|
|
276
274
|
if (!isEcho) {
|
|
277
275
|
dispatch(hydrate(fromLegacy(parentFormData, { channel })));
|
|
278
|
-
// Classic mirrors the tabCount prop only
|
|
279
|
-
|
|
276
|
+
// Classic mirrors the tabCount prop on a data sync only in EDIT mode (Classic.js:446-447);
|
|
277
|
+
// adopting the create flow's hardcoded 2 makes the emptiness check scan the empty iOS tab.
|
|
278
|
+
if (propsRef.current.isEdit) {
|
|
279
|
+
emittedTabCountRef.current = propsRef.current.tabCount || emittedTabCountRef.current;
|
|
280
|
+
}
|
|
280
281
|
// Classic validates on every deep-unequal parent push (Classic.js:516-537) —
|
|
281
282
|
// this is what turns the container's isFormValid true before Create/Done.
|
|
282
283
|
if (validateOnExternalChange) {
|
|
@@ -439,11 +440,8 @@ const FormBuilderShell = (props) => {
|
|
|
439
440
|
// MPUSH opts out of live validation via features.liveValidation:false, SMS keeps it.
|
|
440
441
|
const validateLive = liquidEnabled && !isFullMode
|
|
441
442
|
&& adapter.config?.features?.liveValidation !== false;
|
|
442
|
-
// MPUSH: Classic
|
|
443
|
-
//
|
|
444
|
-
// Classic.js:516-537) — the container's isFormValid must track typing or the
|
|
445
|
-
// library-mode Done gate (Create/index.js:117) stays stuck. Display remains
|
|
446
|
-
// gated on checkValidation, so this emission is invisible pre-save.
|
|
443
|
+
// MPUSH: Classic re-validates every change via the container round-trip (Classic.js:516-537)
|
|
444
|
+
// — keeps the container's isFormValid current for the library Done gate; display stays gated.
|
|
447
445
|
if (validationActive || validateLive || validateOnExternalChange) {
|
|
448
446
|
emitValidity(runValidate(legacy), legacy);
|
|
449
447
|
}
|
|
@@ -626,6 +624,7 @@ FormBuilderShell.propTypes = {
|
|
|
626
624
|
tagModule: PropTypes.string,
|
|
627
625
|
tags: PropTypes.array,
|
|
628
626
|
checkValidation: PropTypes.bool,
|
|
627
|
+
isEdit: PropTypes.bool,
|
|
629
628
|
injectedTags: PropTypes.object,
|
|
630
629
|
onContextChange: PropTypes.func,
|
|
631
630
|
// Shapes match TagList's own contract (array of offer objects / keyed-by-blockId map).
|
|
@@ -271,17 +271,21 @@ export const UploadField = ({
|
|
|
271
271
|
// Classic fires the wrong-file event and CONTINUES (no early return) — preserved.
|
|
272
272
|
onEvent(field.submitAction, { file, type: 'wrong file' });
|
|
273
273
|
}
|
|
274
|
-
const
|
|
274
|
+
const urlApi = window.URL || window.webkitURL;
|
|
275
|
+
const objectUrl = urlApi.createObjectURL(file);
|
|
275
276
|
const img = new Image();
|
|
276
277
|
img.src = objectUrl;
|
|
277
278
|
img.onload = () => {
|
|
278
279
|
const fileParams = {
|
|
279
|
-
width: img
|
|
280
|
-
height: img
|
|
280
|
+
width: img?.width,
|
|
281
|
+
height: img?.height,
|
|
281
282
|
error: Boolean(file && (file.size / 1e6 > 5)), // > 5 MB (Classic.js:2753)
|
|
282
283
|
};
|
|
284
|
+
urlApi.revokeObjectURL(objectUrl); // dimension read done — free the blob URL
|
|
283
285
|
onEvent(field.submitAction, { file, type: 'image', fileParams });
|
|
284
286
|
};
|
|
287
|
+
// Classic dispatches nothing on a failed image load — only release the URL.
|
|
288
|
+
img.onerror = () => urlApi.revokeObjectURL(objectUrl);
|
|
285
289
|
e.target.value = null;
|
|
286
290
|
};
|
|
287
291
|
|
|
@@ -312,9 +316,8 @@ export const UploadField = ({
|
|
|
312
316
|
/>
|
|
313
317
|
)}
|
|
314
318
|
<form encType="multipart/form-data" id={field.id}>
|
|
315
|
-
{/* Native
|
|
316
|
-
|
|
317
|
-
host type="file". Hidden: the visible control is the CapButton below. */}
|
|
319
|
+
{/* Native input on purpose: openFileDialog clicks it via #fileName, and CapInput
|
|
320
|
+
(antd text Input + affix wrapper) can't host type="file". */}
|
|
318
321
|
<input
|
|
319
322
|
key={field.id}
|
|
320
323
|
className="fb-upload-file-input"
|
|
@@ -1,15 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Library/embedded-mode validity parity — the campaigns-host Done/Preview
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* formData push -> validateForm -> onFormValidityChange, Classic.js:516-537), and the
|
|
7
|
-
* library-mode Done click early-returns on !state.isFormValid (Create/index.js:117).
|
|
8
|
-
* The shell mirrors this with a background validity emission on every field change
|
|
9
|
-
* under features.validateOnExternalChange (display stays gated on checkValidation).
|
|
10
|
-
*
|
|
11
|
-
* Also pins onContentValidityChange ({isContentEmpty}) — the footer's live
|
|
12
|
-
* Done/Preview-and-Test disable signal (CreativesContainer handleContentValidityChange).
|
|
2
|
+
* Library/embedded-mode validity parity — the campaigns-host Done/Preview gates.
|
|
3
|
+
* Pins that the container's isFormValid tracks typing (Classic re-validates each change
|
|
4
|
+
* via the container round-trip, Classic.js:516-537; the Done click early-returns on
|
|
5
|
+
* !isFormValid) and the onContentValidityChange({isContentEmpty}) footer signal.
|
|
13
6
|
*/
|
|
14
7
|
import React from 'react';
|
|
15
8
|
import '@testing-library/jest-dom';
|
|
@@ -122,6 +115,26 @@ describe.each([
|
|
|
122
115
|
const contentCalls = props.onContentValidityChange.mock.calls.map((c) => c[0]);
|
|
123
116
|
expect(contentCalls[contentCalls.length - 1]).toEqual({ isContentEmpty: false });
|
|
124
117
|
|
|
118
|
+
// user repro: image write-back (genuine parent push) then CTA check/uncheck — the create
|
|
119
|
+
// flow must not adopt the hardcoded tabCount={2} or the check scans the empty iOS tab.
|
|
120
|
+
act(() => {
|
|
121
|
+
containerRef.setState((prev) => ({
|
|
122
|
+
formData: { ...prev.formData, 0: { ...prev.formData[0], image: 'blob:uploaded' } },
|
|
123
|
+
}));
|
|
124
|
+
});
|
|
125
|
+
act(() => { jest.advanceTimersByTime(1000); });
|
|
126
|
+
const pane = document.querySelectorAll('.ant-tabs-tabpane')[0] || document;
|
|
127
|
+
const label = Array.from(pane.querySelectorAll('label')).find((l) => /action link/i.test(l.textContent || ''));
|
|
128
|
+
const cta = (label && label.querySelector('input[type="checkbox"]')) || pane.querySelector('input[type="checkbox"]');
|
|
129
|
+
act(() => { fireEvent.click(cta); });
|
|
130
|
+
act(() => { jest.advanceTimersByTime(1000); });
|
|
131
|
+
act(() => { fireEvent.click(cta); });
|
|
132
|
+
act(() => { jest.advanceTimersByTime(1000); });
|
|
133
|
+
|
|
134
|
+
expect(containerRef.state.tabCount).toBe(1); // create flow never adopts the prop
|
|
135
|
+
const finalCalls = props.onContentValidityChange.mock.calls.map((c) => c[0]);
|
|
136
|
+
expect(finalCalls[finalCalls.length - 1]).toEqual({ isContentEmpty: false }); // buttons stay enabled
|
|
137
|
+
|
|
125
138
|
utils.unmount();
|
|
126
139
|
});
|
|
127
140
|
});
|
|
@@ -161,4 +161,40 @@ describe('MPUSH shell event bridge', () => {
|
|
|
161
161
|
expect(legacy[0]).toEqual(expect.objectContaining({ 'message-title': '' }));
|
|
162
162
|
expect(legacy[1]).toEqual(expect.objectContaining({ 'message-title2': '' }));
|
|
163
163
|
});
|
|
164
|
+
|
|
165
|
+
// Classic mirrors the tabCount prop on a data push only in EDIT mode (Classic.js:446-447);
|
|
166
|
+
// adopting the create flow's hardcoded 2 made the emptiness check scan the empty iOS tab.
|
|
167
|
+
it('create flow keeps emitting tabCount 1 after a genuine parent push (image write-back); edit adopts the prop', () => {
|
|
168
|
+
const writeBack = (base) => ({
|
|
169
|
+
...base,
|
|
170
|
+
0: { ...base[0], image: 'blob:uploaded' }, // container's in-place upload write-back
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
const typeTitle = (value) => {
|
|
174
|
+
const el = document.getElementById('message-title');
|
|
175
|
+
fireEvent.change(el, { target: { value } });
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
// CREATE (no isEdit): tabCount must stay 1
|
|
179
|
+
const createProps = buildProps(buildSchema(), { formData: {} });
|
|
180
|
+
const createMount = mount(createProps);
|
|
181
|
+
act(() => {
|
|
182
|
+
createMount.rerender(createMount.ui({ ...createProps, formData: writeBack(buildFormData()) }));
|
|
183
|
+
});
|
|
184
|
+
act(() => { typeTitle('after push'); }); // next emission carries the tabCount
|
|
185
|
+
const createEmit = createProps.onChange.mock.calls[createProps.onChange.mock.calls.length - 1];
|
|
186
|
+
expect(createEmit[1]).toBe(1);
|
|
187
|
+
createMount.unmount();
|
|
188
|
+
|
|
189
|
+
// EDIT: Classic adopts the prop on the data sync (Classic.js:447)
|
|
190
|
+
const editProps = buildProps(buildSchema(), { formData: {}, isEdit: true });
|
|
191
|
+
const editMount = mount(editProps);
|
|
192
|
+
act(() => {
|
|
193
|
+
editMount.rerender(editMount.ui({ ...editProps, formData: writeBack(buildFormData()) }));
|
|
194
|
+
});
|
|
195
|
+
act(() => { typeTitle('after push'); });
|
|
196
|
+
const editEmit = editProps.onChange.mock.calls[editProps.onChange.mock.calls.length - 1];
|
|
197
|
+
expect(editEmit[1]).toBe(2);
|
|
198
|
+
editMount.unmount();
|
|
199
|
+
});
|
|
164
200
|
});
|
|
@@ -187,6 +187,7 @@ describe('UploadField', () => {
|
|
|
187
187
|
it('file change: no file => no event; wrong extension fires the wrong-file event and CONTINUES (Classic parity)', () => {
|
|
188
188
|
const createObjectURL = jest.fn(() => 'blob:x');
|
|
189
189
|
window.URL.createObjectURL = createObjectURL;
|
|
190
|
+
window.URL.revokeObjectURL = jest.fn();
|
|
190
191
|
const onEvent = jest.fn();
|
|
191
192
|
const { input } = parts(UploadField({
|
|
192
193
|
field, value: '', error: false, onEvent, renderContext: {},
|
|
@@ -201,8 +202,10 @@ describe('UploadField', () => {
|
|
|
201
202
|
expect(createObjectURL).toHaveBeenCalledWith(file); // no early return
|
|
202
203
|
});
|
|
203
204
|
|
|
204
|
-
it('image load reports dimensions and the >5MB error flag', () => {
|
|
205
|
+
it('image load reports dimensions and the >5MB error flag, then releases the blob URL', () => {
|
|
205
206
|
window.URL.createObjectURL = jest.fn(() => 'blob:x');
|
|
207
|
+
const revokeObjectURL = jest.fn();
|
|
208
|
+
window.URL.revokeObjectURL = revokeObjectURL;
|
|
206
209
|
const images = [];
|
|
207
210
|
const RealImage = global.Image;
|
|
208
211
|
global.Image = class { constructor() { images.push(this); } };
|
|
@@ -220,6 +223,11 @@ describe('UploadField', () => {
|
|
|
220
223
|
expect(onEvent).toHaveBeenCalledWith('onUpload', {
|
|
221
224
|
file, type: 'image', fileParams: { width: 300, height: 200, error: true },
|
|
222
225
|
});
|
|
226
|
+
expect(revokeObjectURL).toHaveBeenCalledWith('blob:x');
|
|
227
|
+
// failed image load: URL released, no event (Classic dispatches nothing)
|
|
228
|
+
input.props.onChange({ target: { files: [file], value: null } });
|
|
229
|
+
images[images.length - 1].onerror();
|
|
230
|
+
expect(revokeObjectURL).toHaveBeenCalledTimes(2);
|
|
223
231
|
} finally {
|
|
224
232
|
global.Image = RealImage;
|
|
225
233
|
}
|
|
@@ -336,6 +336,7 @@ const HTMLEditor = forwardRef(({
|
|
|
336
336
|
const validationTotalErrors = validation?.summary?.totalErrors || 0;
|
|
337
337
|
const validationTotalWarnings = validation?.summary?.totalWarnings || 0;
|
|
338
338
|
const validationLastValidated = validation?.lastValidated?.getTime?.() || null;
|
|
339
|
+
const validationValidatedContent = validation?.validatedContent;
|
|
339
340
|
// Only Rule Group #1 (Input & Sanitization) blocks; use for UI gating (Done/Update/Preview/Test)
|
|
340
341
|
const validationHasBlockingErrors = validation?.hasBlockingErrors || false;
|
|
341
342
|
|
|
@@ -350,6 +351,11 @@ const HTMLEditor = forwardRef(({
|
|
|
350
351
|
return;
|
|
351
352
|
}
|
|
352
353
|
|
|
354
|
+
// isValidating is false during the debounce, so results can still be for older content.
|
|
355
|
+
if (validationValidatedContent !== undefined && validationValidatedContent !== currentContent) {
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
|
|
353
359
|
// Calculate issue counts: Errors (blocking) and Warnings (non-blocking)
|
|
354
360
|
const calculateIssueCounts = () => {
|
|
355
361
|
const currentValidation = validationRef.current;
|
|
@@ -384,7 +390,7 @@ const HTMLEditor = forwardRef(({
|
|
|
384
390
|
lastSentValidationStateRef.current = newState;
|
|
385
391
|
onValidationChangeRef.current(newState);
|
|
386
392
|
}
|
|
387
|
-
}, [isValidating, validationTotalErrors, validationTotalWarnings, validationLastValidated, validationHasBlockingErrors, currentContent, apiValidationErrors]);
|
|
393
|
+
}, [isValidating, validationTotalErrors, validationTotalWarnings, validationLastValidated, validationHasBlockingErrors, validationValidatedContent, currentContent, apiValidationErrors]);
|
|
388
394
|
|
|
389
395
|
// Send initial state on mount to ensure parent has correct initial button state
|
|
390
396
|
const hasInitializedRef = useRef(false);
|
|
@@ -416,6 +422,11 @@ const HTMLEditor = forwardRef(({
|
|
|
416
422
|
return;
|
|
417
423
|
}
|
|
418
424
|
|
|
425
|
+
// Same staleness guard as the effect above.
|
|
426
|
+
if (validation?.validatedContent !== undefined && validation?.validatedContent !== currentContent) {
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
|
|
419
430
|
// Recalculate issue counts (Errors + Warnings) including API errors
|
|
420
431
|
const issueCounts = !validation || typeof validation.getAllIssues !== 'function'
|
|
421
432
|
? { errors: 0, warnings: 0, total: 0 }
|
|
@@ -4462,5 +4462,98 @@ describe('HTMLEditor', () => {
|
|
|
4462
4462
|
const lastCall = onValidationChange.mock.calls[onValidationChange.mock.calls.length - 1][0];
|
|
4463
4463
|
expect(lastCall.isContentEmpty).toBe(false);
|
|
4464
4464
|
});
|
|
4465
|
+
|
|
4466
|
+
const mockContentHook = (content) => {
|
|
4467
|
+
require('../hooks/useEditorContent').useEditorContent = () => ({
|
|
4468
|
+
content,
|
|
4469
|
+
updateContent: jest.fn(),
|
|
4470
|
+
saveContent: jest.fn(),
|
|
4471
|
+
markAsSaved: jest.fn(),
|
|
4472
|
+
isLoading: false,
|
|
4473
|
+
isDirty: false,
|
|
4474
|
+
hasContent: !!content,
|
|
4475
|
+
getContentSize: jest.fn(() => content.length),
|
|
4476
|
+
});
|
|
4477
|
+
};
|
|
4478
|
+
|
|
4479
|
+
const mockValidationFor = (validatedContent) => {
|
|
4480
|
+
mockUseValidationImpl.mockImplementation(() => ({
|
|
4481
|
+
isValidating: false,
|
|
4482
|
+
validatedContent,
|
|
4483
|
+
getAllIssues: () => [],
|
|
4484
|
+
isClean: () => true,
|
|
4485
|
+
summary: { totalErrors: 0, totalWarnings: 0 },
|
|
4486
|
+
}));
|
|
4487
|
+
};
|
|
4488
|
+
|
|
4489
|
+
it('does not report validation as complete while results are for older content', () => {
|
|
4490
|
+
const onValidationChange = jest.fn();
|
|
4491
|
+
mockContentHook('<p>uploaded zip content</p>');
|
|
4492
|
+
mockValidationFor('<p>previous content</p>');
|
|
4493
|
+
|
|
4494
|
+
render(
|
|
4495
|
+
<TestWrapper>
|
|
4496
|
+
<HTMLEditor {...defaultProps} onValidationChange={onValidationChange} />
|
|
4497
|
+
</TestWrapper>
|
|
4498
|
+
);
|
|
4499
|
+
|
|
4500
|
+
act(() => {
|
|
4501
|
+
jest.runAllTimers();
|
|
4502
|
+
});
|
|
4503
|
+
|
|
4504
|
+
const completed = onValidationChange.mock.calls.filter((call) => call[0].validationComplete);
|
|
4505
|
+
expect(completed).toHaveLength(0);
|
|
4506
|
+
});
|
|
4507
|
+
|
|
4508
|
+
it('reports validation as complete once results match the current content', () => {
|
|
4509
|
+
const onValidationChange = jest.fn();
|
|
4510
|
+
const content = '<p>uploaded zip content</p>';
|
|
4511
|
+
mockContentHook(content);
|
|
4512
|
+
mockValidationFor(content);
|
|
4513
|
+
|
|
4514
|
+
render(
|
|
4515
|
+
<TestWrapper>
|
|
4516
|
+
<HTMLEditor {...defaultProps} onValidationChange={onValidationChange} />
|
|
4517
|
+
</TestWrapper>
|
|
4518
|
+
);
|
|
4519
|
+
|
|
4520
|
+
act(() => {
|
|
4521
|
+
jest.runAllTimers();
|
|
4522
|
+
});
|
|
4523
|
+
|
|
4524
|
+
const completed = onValidationChange.mock.calls.filter((call) => call[0].validationComplete);
|
|
4525
|
+
expect(completed.length).toBeGreaterThan(0);
|
|
4526
|
+
});
|
|
4527
|
+
|
|
4528
|
+
it('does not re-notify parent when the validation state is unchanged', () => {
|
|
4529
|
+
const onValidationChange = jest.fn();
|
|
4530
|
+
const content = '<p>uploaded zip content</p>';
|
|
4531
|
+
mockContentHook(content);
|
|
4532
|
+
mockValidationFor(content);
|
|
4533
|
+
|
|
4534
|
+
const { rerender } = render(
|
|
4535
|
+
<TestWrapper>
|
|
4536
|
+
<HTMLEditor {...defaultProps} onValidationChange={onValidationChange} />
|
|
4537
|
+
</TestWrapper>
|
|
4538
|
+
);
|
|
4539
|
+
|
|
4540
|
+
act(() => {
|
|
4541
|
+
jest.runAllTimers();
|
|
4542
|
+
});
|
|
4543
|
+
|
|
4544
|
+
onValidationChange.mockClear();
|
|
4545
|
+
|
|
4546
|
+
rerender(
|
|
4547
|
+
<TestWrapper>
|
|
4548
|
+
<HTMLEditor {...defaultProps} onValidationChange={onValidationChange} />
|
|
4549
|
+
</TestWrapper>
|
|
4550
|
+
);
|
|
4551
|
+
|
|
4552
|
+
act(() => {
|
|
4553
|
+
jest.runAllTimers();
|
|
4554
|
+
});
|
|
4555
|
+
|
|
4556
|
+
expect(onValidationChange).not.toHaveBeenCalled();
|
|
4557
|
+
});
|
|
4465
4558
|
});
|
|
4466
4559
|
});
|
|
@@ -84,6 +84,7 @@ export const useValidation = (content, variant = 'email', options = {}, formatSa
|
|
|
84
84
|
const [validationState, setValidationState] = useState({
|
|
85
85
|
isValidating: false,
|
|
86
86
|
lastValidated: null,
|
|
87
|
+
validatedContent: null, // content these results came from
|
|
87
88
|
htmlErrors: [],
|
|
88
89
|
htmlWarnings: [],
|
|
89
90
|
htmlInfo: [],
|
|
@@ -114,6 +115,7 @@ export const useValidation = (content, variant = 'email', options = {}, formatSa
|
|
|
114
115
|
setValidationState((prev) => ({
|
|
115
116
|
...prev,
|
|
116
117
|
isValidating: false,
|
|
118
|
+
validatedContent: htmlContent,
|
|
117
119
|
htmlErrors: [],
|
|
118
120
|
htmlWarnings: [],
|
|
119
121
|
htmlInfo: [],
|
|
@@ -169,6 +171,7 @@ export const useValidation = (content, variant = 'email', options = {}, formatSa
|
|
|
169
171
|
setValidationState({
|
|
170
172
|
isValidating: false,
|
|
171
173
|
lastValidated: new Date(),
|
|
174
|
+
validatedContent: htmlContent,
|
|
172
175
|
htmlErrors: htmlValidation.errors,
|
|
173
176
|
htmlWarnings: htmlValidation.warnings,
|
|
174
177
|
htmlInfo: htmlValidation.info,
|
|
@@ -191,6 +194,7 @@ export const useValidation = (content, variant = 'email', options = {}, formatSa
|
|
|
191
194
|
setValidationState((prev) => ({
|
|
192
195
|
...prev,
|
|
193
196
|
isValidating: false,
|
|
197
|
+
validatedContent: htmlContent,
|
|
194
198
|
htmlErrors: [{
|
|
195
199
|
type: 'error',
|
|
196
200
|
message: `Validation failed: ${error.message}`,
|
|
@@ -255,6 +259,7 @@ export const useValidation = (content, variant = 'email', options = {}, formatSa
|
|
|
255
259
|
setValidationState({
|
|
256
260
|
isValidating: false,
|
|
257
261
|
lastValidated: null,
|
|
262
|
+
validatedContent: null,
|
|
258
263
|
htmlErrors: [],
|
|
259
264
|
htmlWarnings: [],
|
|
260
265
|
htmlInfo: [],
|