@capillarytech/creatives-library 9.0.57-alpha.0 → 9.0.57-alpha.1
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
CHANGED
|
@@ -439,7 +439,12 @@ const FormBuilderShell = (props) => {
|
|
|
439
439
|
// MPUSH opts out of live validation via features.liveValidation:false, SMS keeps it.
|
|
440
440
|
const validateLive = liquidEnabled && !isFullMode
|
|
441
441
|
&& adapter.config?.features?.liveValidation !== false;
|
|
442
|
-
|
|
442
|
+
// MPUSH: Classic still re-validates EVERY change via the container round-trip
|
|
443
|
+
// (onChange -> container setState -> CWRP deep-unequal formData push -> validateForm,
|
|
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.
|
|
447
|
+
if (validationActive || validateLive || validateOnExternalChange) {
|
|
443
448
|
emitValidity(runValidate(legacy), legacy);
|
|
444
449
|
}
|
|
445
450
|
};
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Library/embedded-mode validity parity — the campaigns-host Done/Preview gate.
|
|
3
|
+
*
|
|
4
|
+
* The container's isFormValid must track typing: Classic re-validates every change
|
|
5
|
+
* via the container round-trip (onChange -> container setState -> CWRP deep-unequal
|
|
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).
|
|
13
|
+
*/
|
|
14
|
+
import React from 'react';
|
|
15
|
+
import '@testing-library/jest-dom';
|
|
16
|
+
import _ from 'lodash';
|
|
17
|
+
import { Router } from 'react-router-dom';
|
|
18
|
+
import { render, act, fireEvent } from '../../../../utils/test-utils';
|
|
19
|
+
import history from '../../../../utils/history';
|
|
20
|
+
import { response as mpushSchemaResponse } from '../../../../v2Containers/MobilePush/initialSchema';
|
|
21
|
+
import { Create } from '../../../../v2Containers/MobilePush/Create';
|
|
22
|
+
|
|
23
|
+
jest.mock('redux-auth-wrapper/history4/redirect', () => ({
|
|
24
|
+
connectedRouterRedirect: jest.fn(() => (Component) => Component),
|
|
25
|
+
}));
|
|
26
|
+
jest.mock('../../../../services/api', () => ({
|
|
27
|
+
...jest.requireActual('../../../../services/api'),
|
|
28
|
+
getUnsubscribeUrl: () => Promise.resolve({ response: { response: '' } }),
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
const fullDefinition = () => _.cloneDeep(mpushSchemaResponse.metaEntities[0].definition);
|
|
32
|
+
|
|
33
|
+
const intl = {
|
|
34
|
+
formatMessage: (d) => (d && (d.defaultMessage || d.id)) || '',
|
|
35
|
+
locale: 'en',
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const selectedAccount = {
|
|
39
|
+
id: 'acc-1',
|
|
40
|
+
name: 'Acc',
|
|
41
|
+
sourceTypeName: 'SOME_SDK',
|
|
42
|
+
sourceAccountIdentifier: 'lic-1',
|
|
43
|
+
configs: { android: '1', ios: '1', deeplink: '[]' },
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const buildProps = () => ({
|
|
47
|
+
intl,
|
|
48
|
+
params: { mode: 'text' },
|
|
49
|
+
route: { name: 'create' },
|
|
50
|
+
location: { pathname: '/mobilepush/create/text', query: { module: 'default', type: 'embedded' } },
|
|
51
|
+
router: { push: jest.fn() },
|
|
52
|
+
isFullMode: false,
|
|
53
|
+
isGetFormData: false,
|
|
54
|
+
isLoadingMetaEntities: false,
|
|
55
|
+
metaEntities: {},
|
|
56
|
+
Create: { createTemplateInProgress: false },
|
|
57
|
+
Edit: {},
|
|
58
|
+
Templates: { selectedWeChatAccount: selectedAccount },
|
|
59
|
+
actions: new Proxy({}, { get: (t, n) => { if (!t[n]) t[n] = jest.fn(); return t[n]; } }), // eslint-disable-line no-param-reassign
|
|
60
|
+
globalActions: {
|
|
61
|
+
fetchSchemaForEntity: jest.fn(),
|
|
62
|
+
addMessageToQueue: jest.fn(),
|
|
63
|
+
setInjectedTags: jest.fn(),
|
|
64
|
+
},
|
|
65
|
+
getFormLibraryData: jest.fn(),
|
|
66
|
+
onValidationFail: jest.fn(),
|
|
67
|
+
showLiquidErrorInFooter: jest.fn(),
|
|
68
|
+
onPersonalizationTokensChange: jest.fn(),
|
|
69
|
+
onContentValidityChange: jest.fn(),
|
|
70
|
+
getLiquidTags: jest.fn(),
|
|
71
|
+
injectedTags: {},
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const setNativeValue = (el, value) => {
|
|
75
|
+
const proto = Object.getPrototypeOf(el);
|
|
76
|
+
Object.getOwnPropertyDescriptor(proto, 'value').set.call(el, value);
|
|
77
|
+
};
|
|
78
|
+
const typeInto = (id, value) => {
|
|
79
|
+
const el = document.getElementById(id);
|
|
80
|
+
if (!el) throw new Error(`no #${id} in DOM`);
|
|
81
|
+
setNativeValue(el, value);
|
|
82
|
+
fireEvent.change(el, { target: { value } });
|
|
83
|
+
fireEvent.blur(el);
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
describe.each([
|
|
87
|
+
['Classic', 'false'],
|
|
88
|
+
['Functional', 'true'],
|
|
89
|
+
])('library-mode validity parity — %s', (flow, flagValue) => {
|
|
90
|
+
beforeEach(() => {
|
|
91
|
+
window.localStorage.setItem('ENABLE_NEW_FORMBUILDER_MPUSH', flagValue);
|
|
92
|
+
jest.useFakeTimers();
|
|
93
|
+
});
|
|
94
|
+
afterEach(() => {
|
|
95
|
+
jest.useRealTimers();
|
|
96
|
+
window.localStorage.removeItem('ENABLE_NEW_FORMBUILDER_MPUSH');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('isFormValid flips true after typing valid content (Done gate) and content-emptiness reports live', () => {
|
|
100
|
+
let containerRef = null;
|
|
101
|
+
const props = buildProps();
|
|
102
|
+
const ui = (p) => (
|
|
103
|
+
<Router history={history}>
|
|
104
|
+
<Create ref={(r) => { if (r) containerRef = r; }} {...p} />
|
|
105
|
+
</Router>
|
|
106
|
+
);
|
|
107
|
+
const utils = render(ui(props));
|
|
108
|
+
const meta = { layouts: [{ definition: fullDefinition() }], tags: { standard: [] } };
|
|
109
|
+
act(() => { utils.rerender(ui({ ...props, metaEntities: meta })); });
|
|
110
|
+
act(() => { jest.runOnlyPendingTimers(); });
|
|
111
|
+
|
|
112
|
+
// mount: empty form — invalid, content empty
|
|
113
|
+
expect(containerRef.state.isFormValid).toBe(false);
|
|
114
|
+
expect(props.onContentValidityChange).toHaveBeenCalledWith({ isContentEmpty: true });
|
|
115
|
+
|
|
116
|
+
act(() => { typeInto('message-title', 'Hello title'); });
|
|
117
|
+
act(() => { typeInto('message-editor', 'Hello body'); });
|
|
118
|
+
act(() => { jest.advanceTimersByTime(1000); }); // flush debounced emissions
|
|
119
|
+
|
|
120
|
+
// the Done gate must see the typed content: valid + non-empty
|
|
121
|
+
expect(containerRef.state.isFormValid).toBe(true);
|
|
122
|
+
const contentCalls = props.onContentValidityChange.mock.calls.map((c) => c[0]);
|
|
123
|
+
expect(contentCalls[contentCalls.length - 1]).toEqual({ isContentEmpty: false });
|
|
124
|
+
|
|
125
|
+
utils.unmount();
|
|
126
|
+
});
|
|
127
|
+
});
|