@capillarytech/creatives-library 8.0.281 → 8.0.282
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
|
@@ -49,10 +49,16 @@ const mockGetValidationState = jest.fn(() => ({
|
|
|
49
49
|
issueCounts: { errors: 0, warnings: 0, total: 0 },
|
|
50
50
|
}));
|
|
51
51
|
|
|
52
|
+
// Ref to capture apiValidationErrors passed to HTMLEditor (for mergedApiValidationErrors tests)
|
|
53
|
+
const capturedApiValidationErrorsRef = { current: null };
|
|
54
|
+
|
|
52
55
|
// Mock HtmlEditor - it exports a lazy-loaded component by default
|
|
53
56
|
jest.mock('../../../../v2Components/HtmlEditor/index.lazy', () => {
|
|
54
57
|
const React = require('react');
|
|
55
58
|
const MockHTMLEditor = React.forwardRef((props, ref) => {
|
|
59
|
+
if (global.__captureApiValidationErrorsRef && props.apiValidationErrors) {
|
|
60
|
+
global.__captureApiValidationErrorsRef.current = props.apiValidationErrors;
|
|
61
|
+
}
|
|
56
62
|
React.useImperativeHandle(ref, () => ({
|
|
57
63
|
getAllIssues: () => mockGetAllIssues(),
|
|
58
64
|
getValidationState: () => mockGetValidationState(),
|
|
@@ -109,6 +115,9 @@ jest.mock('../../../../v2Components/HtmlEditor/index.lazy', () => {
|
|
|
109
115
|
jest.mock('../../../../v2Components/HtmlEditor', () => {
|
|
110
116
|
const React = require('react');
|
|
111
117
|
const MockHTMLEditor = React.forwardRef((props, ref) => {
|
|
118
|
+
if (global.__captureApiValidationErrorsRef && props.apiValidationErrors) {
|
|
119
|
+
global.__captureApiValidationErrorsRef.current = props.apiValidationErrors;
|
|
120
|
+
}
|
|
112
121
|
React.useImperativeHandle(ref, () => ({
|
|
113
122
|
getAllIssues: () => mockGetAllIssues(),
|
|
114
123
|
getValidationState: () => mockGetValidationState(),
|
|
@@ -421,6 +430,86 @@ describe('EmailHTMLEditor', () => {
|
|
|
421
430
|
});
|
|
422
431
|
// Reset hasLiquidSupportFeature mock to return true by default
|
|
423
432
|
mockHasLiquidSupportFeature.mockReturnValue(true);
|
|
433
|
+
capturedApiValidationErrorsRef.current = null;
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
describe('mergedApiValidationErrors (lines 124-125)', () => {
|
|
437
|
+
beforeEach(() => {
|
|
438
|
+
global.__captureApiValidationErrorsRef = capturedApiValidationErrorsRef;
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
afterEach(() => {
|
|
442
|
+
delete global.__captureApiValidationErrorsRef;
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
it('merges tag unsupported errors into standardErrors when tagValidationError has unsupportedTags', async () => {
|
|
446
|
+
validateTags.mockReturnValue({
|
|
447
|
+
valid: false,
|
|
448
|
+
unsupportedTags: ['tagA', 'tagB'],
|
|
449
|
+
});
|
|
450
|
+
renderWithIntl({
|
|
451
|
+
metaEntities: { tags: { standard: [{ name: 'customer.name' }] } },
|
|
452
|
+
tags: [{ name: 'customer.name' }],
|
|
453
|
+
supportedTags: [],
|
|
454
|
+
});
|
|
455
|
+
const changeButton = screen.getByTestId('trigger-content-change');
|
|
456
|
+
await act(async () => {
|
|
457
|
+
fireEvent.click(changeButton);
|
|
458
|
+
});
|
|
459
|
+
await waitFor(() => {
|
|
460
|
+
expect(capturedApiValidationErrorsRef.current).not.toBeNull();
|
|
461
|
+
expect(capturedApiValidationErrorsRef.current.liquidErrors).toEqual([]);
|
|
462
|
+
expect(capturedApiValidationErrorsRef.current.standardErrors).toContain(
|
|
463
|
+
'Unsupported tags are: tagA, tagB',
|
|
464
|
+
);
|
|
465
|
+
});
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
it('merges tag missing errors into standardErrors when tagValidationError has missingTags and unsubscribe not mandatory', async () => {
|
|
469
|
+
isEmailUnsubscribeTagMandatory.mockReturnValue(false);
|
|
470
|
+
validateTags.mockReturnValue({
|
|
471
|
+
valid: false,
|
|
472
|
+
missingTags: ['unsubscribe'],
|
|
473
|
+
});
|
|
474
|
+
renderWithIntl({
|
|
475
|
+
metaEntities: { tags: { standard: [{ name: 'customer.name' }] } },
|
|
476
|
+
tags: [{ name: 'customer.name' }],
|
|
477
|
+
supportedTags: [],
|
|
478
|
+
});
|
|
479
|
+
const changeButton = screen.getByTestId('trigger-content-change');
|
|
480
|
+
await act(async () => {
|
|
481
|
+
fireEvent.click(changeButton);
|
|
482
|
+
});
|
|
483
|
+
await waitFor(() => {
|
|
484
|
+
expect(capturedApiValidationErrorsRef.current).not.toBeNull();
|
|
485
|
+
expect(capturedApiValidationErrorsRef.current.standardErrors).toContain(
|
|
486
|
+
'Missing tags are: unsubscribe',
|
|
487
|
+
);
|
|
488
|
+
});
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
it('uses apiValidationErrors.liquidErrors and concatenates apiValidationErrors.standardErrors with tag messages (merge shape)', async () => {
|
|
492
|
+
// When tag messages exist, mergedApiValidationErrors returns liquidErrors from apiValidationErrors
|
|
493
|
+
// and standardErrors = [...(apiValidationErrors?.standardErrors || []), ...tagMessages] (lines 124-125)
|
|
494
|
+
validateTags.mockReturnValue({
|
|
495
|
+
valid: false,
|
|
496
|
+
unsupportedTags: ['customTag'],
|
|
497
|
+
});
|
|
498
|
+
renderWithIntl({
|
|
499
|
+
metaEntities: { tags: { standard: [{ name: 'customer.name' }] } },
|
|
500
|
+
tags: [{ name: 'customer.name' }],
|
|
501
|
+
});
|
|
502
|
+
const changeButton = screen.getByTestId('trigger-content-change');
|
|
503
|
+
await act(async () => {
|
|
504
|
+
fireEvent.click(changeButton);
|
|
505
|
+
});
|
|
506
|
+
await waitFor(() => {
|
|
507
|
+
expect(capturedApiValidationErrorsRef.current).not.toBeNull();
|
|
508
|
+
const { liquidErrors, standardErrors } = capturedApiValidationErrorsRef.current;
|
|
509
|
+
expect(liquidErrors).toEqual([]);
|
|
510
|
+
expect(standardErrors).toContain('Unsupported tags are: customTag');
|
|
511
|
+
});
|
|
512
|
+
});
|
|
424
513
|
});
|
|
425
514
|
|
|
426
515
|
describe('Default Parameter Values (lines 60-63)', () => {
|
|
@@ -768,6 +768,130 @@ describe('useEmailWrapper', () => {
|
|
|
768
768
|
});
|
|
769
769
|
});
|
|
770
770
|
|
|
771
|
+
describe('templateId resolution (lines 209-213)', () => {
|
|
772
|
+
it('should call getTemplateDetails with templateId from params.id', async () => {
|
|
773
|
+
const templateId = 'from-params-id';
|
|
774
|
+
const editProps = {
|
|
775
|
+
...newFlowMockProps,
|
|
776
|
+
params: { id: templateId },
|
|
777
|
+
location: { pathname: '/email/edit/other', query: {} },
|
|
778
|
+
Email: {
|
|
779
|
+
...newFlowMockProps.Email,
|
|
780
|
+
templateDetails: null,
|
|
781
|
+
getTemplateDetailsInProgress: false,
|
|
782
|
+
},
|
|
783
|
+
};
|
|
784
|
+
|
|
785
|
+
renderHook((props) => useEmailWrapper(props), {
|
|
786
|
+
initialProps: editProps,
|
|
787
|
+
});
|
|
788
|
+
|
|
789
|
+
await waitFor(() => {
|
|
790
|
+
expect(mockEmailActions.getTemplateDetails).toHaveBeenCalledWith(templateId, 'email');
|
|
791
|
+
});
|
|
792
|
+
});
|
|
793
|
+
|
|
794
|
+
it('should call getTemplateDetails with templateId from location.query.id when params.id is missing', async () => {
|
|
795
|
+
const templateId = 'from-query-id';
|
|
796
|
+
const editProps = {
|
|
797
|
+
...newFlowMockProps,
|
|
798
|
+
params: {},
|
|
799
|
+
location: {
|
|
800
|
+
pathname: '/email/edit/something',
|
|
801
|
+
query: { id: templateId },
|
|
802
|
+
},
|
|
803
|
+
Email: {
|
|
804
|
+
...newFlowMockProps.Email,
|
|
805
|
+
templateDetails: null,
|
|
806
|
+
getTemplateDetailsInProgress: false,
|
|
807
|
+
},
|
|
808
|
+
};
|
|
809
|
+
|
|
810
|
+
renderHook((props) => useEmailWrapper(props), {
|
|
811
|
+
initialProps: editProps,
|
|
812
|
+
});
|
|
813
|
+
|
|
814
|
+
await waitFor(() => {
|
|
815
|
+
expect(mockEmailActions.getTemplateDetails).toHaveBeenCalledWith(templateId, 'email');
|
|
816
|
+
});
|
|
817
|
+
});
|
|
818
|
+
|
|
819
|
+
it('should call getTemplateDetails with templateId from location.params.id when params and query id are missing', async () => {
|
|
820
|
+
const templateId = 'from-location-params-id';
|
|
821
|
+
const editProps = {
|
|
822
|
+
...newFlowMockProps,
|
|
823
|
+
params: {},
|
|
824
|
+
location: {
|
|
825
|
+
pathname: '/email/edit/fallback',
|
|
826
|
+
query: {},
|
|
827
|
+
params: { id: templateId },
|
|
828
|
+
},
|
|
829
|
+
Email: {
|
|
830
|
+
...newFlowMockProps.Email,
|
|
831
|
+
templateDetails: null,
|
|
832
|
+
getTemplateDetailsInProgress: false,
|
|
833
|
+
},
|
|
834
|
+
};
|
|
835
|
+
|
|
836
|
+
renderHook((props) => useEmailWrapper(props), {
|
|
837
|
+
initialProps: editProps,
|
|
838
|
+
});
|
|
839
|
+
|
|
840
|
+
await waitFor(() => {
|
|
841
|
+
expect(mockEmailActions.getTemplateDetails).toHaveBeenCalledWith(templateId, 'email');
|
|
842
|
+
});
|
|
843
|
+
});
|
|
844
|
+
|
|
845
|
+
it('should call getTemplateDetails with templateId extracted from pathname /edit/ID when no params or query id', async () => {
|
|
846
|
+
const templateId = 'extracted-from-pathname';
|
|
847
|
+
const editProps = {
|
|
848
|
+
...newFlowMockProps,
|
|
849
|
+
params: {},
|
|
850
|
+
location: {
|
|
851
|
+
pathname: `/email/edit/${templateId}`,
|
|
852
|
+
query: {},
|
|
853
|
+
},
|
|
854
|
+
Email: {
|
|
855
|
+
...newFlowMockProps.Email,
|
|
856
|
+
templateDetails: null,
|
|
857
|
+
getTemplateDetailsInProgress: false,
|
|
858
|
+
},
|
|
859
|
+
};
|
|
860
|
+
|
|
861
|
+
renderHook((props) => useEmailWrapper(props), {
|
|
862
|
+
initialProps: editProps,
|
|
863
|
+
});
|
|
864
|
+
|
|
865
|
+
await waitFor(() => {
|
|
866
|
+
expect(mockEmailActions.getTemplateDetails).toHaveBeenCalledWith(templateId, 'email');
|
|
867
|
+
});
|
|
868
|
+
});
|
|
869
|
+
|
|
870
|
+
it('should not call getTemplateDetails when pathname includes /edit/ but has no id segment', async () => {
|
|
871
|
+
const editProps = {
|
|
872
|
+
...newFlowMockProps,
|
|
873
|
+
params: {},
|
|
874
|
+
location: {
|
|
875
|
+
pathname: '/email/edit/',
|
|
876
|
+
query: {},
|
|
877
|
+
},
|
|
878
|
+
Email: {
|
|
879
|
+
...newFlowMockProps.Email,
|
|
880
|
+
templateDetails: null,
|
|
881
|
+
getTemplateDetailsInProgress: false,
|
|
882
|
+
},
|
|
883
|
+
};
|
|
884
|
+
|
|
885
|
+
renderHook((props) => useEmailWrapper(props), {
|
|
886
|
+
initialProps: editProps,
|
|
887
|
+
});
|
|
888
|
+
|
|
889
|
+
await waitFor(() => {
|
|
890
|
+
expect(mockEmailActions.getTemplateDetails).not.toHaveBeenCalled();
|
|
891
|
+
}, { timeout: 500 });
|
|
892
|
+
});
|
|
893
|
+
});
|
|
894
|
+
|
|
771
895
|
describe('Edit Flow - BEE Editor Template', () => {
|
|
772
896
|
it('should call getTemplateDetails and set BEE editor for BEE template', async () => {
|
|
773
897
|
const templateId = 'bee-template-123';
|
|
@@ -177,9 +177,11 @@ export class Edit extends React.Component { // eslint-disable-line react/prefer-
|
|
|
177
177
|
}
|
|
178
178
|
if (nextProps.metaEntities && nextProps.metaEntities.layouts && nextProps.metaEntities.layouts.length > 0 && _.isEmpty(this.state.fullSchema)) {
|
|
179
179
|
this.setState({fullSchema: nextProps.metaEntities.layouts[0].definition, schema: (nextProps.location.query.module === 'loyalty') ? nextProps.metaEntities.layouts[0].definition.textSchema : {}}, () => {
|
|
180
|
-
this.
|
|
180
|
+
// Use this.props (latest) in callback to avoid race: templateDetails may have arrived by now
|
|
181
|
+
const latestSelectedAccount = this.getSelectedWeChatAccountFromProps(this.props);
|
|
182
|
+
this.handleEditSchemaOnPropsChange(this.props, latestSelectedAccount);
|
|
181
183
|
const templateId = get(this, "props.params.id");
|
|
182
|
-
if (
|
|
184
|
+
if (this.props.location.query.module !== 'loyalty' && templateId && templateId !== 'temp') {
|
|
183
185
|
this.props.actions.getTemplateDetails(templateId);
|
|
184
186
|
}
|
|
185
187
|
if (queryType === EMBEDDED && templateId === 'temp' && _.isEmpty(this.state.formData)) { // when his.props.params.id is temp that means mobile push template content will be passed from post message from parent with startTemplateCreation action
|
|
@@ -727,6 +729,27 @@ export class Edit extends React.Component { // eslint-disable-line react/prefer-
|
|
|
727
729
|
parent.postMessage(JSON.stringify(response), '*');
|
|
728
730
|
};
|
|
729
731
|
|
|
732
|
+
/**
|
|
733
|
+
* Compute selectedWeChatAccount from props (used so we can call with latest props
|
|
734
|
+
* in setState callback to avoid stale closure and intermittent empty form).
|
|
735
|
+
*/
|
|
736
|
+
getSelectedWeChatAccountFromProps = (props) => {
|
|
737
|
+
const queryType = String(get(props, 'location.query.type', ''))?.toLowerCase();
|
|
738
|
+
const creativesMode = String(get(props, 'creativesMode', ''))?.toLowerCase();
|
|
739
|
+
const { Edit: EditProps, Templates } = props || {};
|
|
740
|
+
const { selectedWeChatAccount: editSelectedWeChatAccount } = EditProps || {};
|
|
741
|
+
const { Templates: nextTemplates } = props || {};
|
|
742
|
+
if (isEmbeddedEditOrPreview(queryType, creativesMode)) {
|
|
743
|
+
return !_.isEmpty(editSelectedWeChatAccount)
|
|
744
|
+
? editSelectedWeChatAccount
|
|
745
|
+
: nextTemplates?.selectedWeChatAccount;
|
|
746
|
+
}
|
|
747
|
+
if (!_.isEmpty(Templates?.selectedWeChatAccount)) {
|
|
748
|
+
return Templates?.selectedWeChatAccount;
|
|
749
|
+
}
|
|
750
|
+
return undefined;
|
|
751
|
+
};
|
|
752
|
+
|
|
730
753
|
getFormData = (e) => {
|
|
731
754
|
const response = {
|
|
732
755
|
action: "getFormData",
|
|
@@ -749,6 +772,9 @@ export class Edit extends React.Component { // eslint-disable-line react/prefer-
|
|
|
749
772
|
formData["mobilepush-accounts"] = this.state.formData["mobilepush-accounts"];
|
|
750
773
|
formData['mobilepush-template'] = this.state.formData['mobilepush-template'];
|
|
751
774
|
}
|
|
775
|
+
if (data.definition?.accountId) {
|
|
776
|
+
formData['mobilepush-accounts'] = data.definition.accountId;
|
|
777
|
+
}
|
|
752
778
|
formData['template-name'] = data.name;
|
|
753
779
|
const androidData = data.versions.base.ANDROID;
|
|
754
780
|
const iosData = data.versions.base.IOS;
|
|
@@ -2004,8 +2030,23 @@ export class Edit extends React.Component { // eslint-disable-line react/prefer-
|
|
|
2004
2030
|
}
|
|
2005
2031
|
|
|
2006
2032
|
handleEditSchemaOnPropsChange = (nextProps, selectedWeChatAccount) => {
|
|
2007
|
-
|
|
2033
|
+
const queryType = String(get(this.props, 'location.query.type', ''))?.toLowerCase();
|
|
2034
|
+
const isEmbeddedLibrary = queryType === EMBEDDED && !nextProps.isFullMode;
|
|
2035
|
+
const canSetAccountFromTemplate =
|
|
2036
|
+
!selectedWeChatAccount &&
|
|
2037
|
+
nextProps.templateDetails?.definition?.accountId &&
|
|
2038
|
+
nextProps.Edit?.weCrmAccounts?.length > 0;
|
|
2039
|
+
const canPopulateForm =
|
|
2040
|
+
!_.isEmpty(nextProps.templateDetails) &&
|
|
2041
|
+
_.isEmpty(this.state.editData) &&
|
|
2042
|
+
!_.isEmpty(this.state.fullSchema) &&
|
|
2043
|
+
(this.props.location.query.type !== 'embedded' || this.props.isFullMode === false) &&
|
|
2044
|
+
(selectedWeChatAccount || isEmbeddedLibrary || canSetAccountFromTemplate);
|
|
2045
|
+
if (canPopulateForm) {
|
|
2008
2046
|
this.props = nextProps;
|
|
2047
|
+
if (canSetAccountFromTemplate) {
|
|
2048
|
+
this.setMobilePushAccountOptions(nextProps.Edit.weCrmAccounts, nextProps.templateDetails.definition.accountId);
|
|
2049
|
+
}
|
|
2009
2050
|
const mode = nextProps.templateDetails.definition ? nextProps.templateDetails.definition.mode : nextProps.templateDetails.mode;
|
|
2010
2051
|
const schema = mode === "text" ? this.state.fullSchema?.textSchema : this.state.fullSchema?.imageSchema;
|
|
2011
2052
|
const isAndroidSupported = get(this, "props.Templates.selectedWeChatAccount.configs.android") === '1';
|
|
@@ -2040,7 +2081,7 @@ export class Edit extends React.Component { // eslint-disable-line react/prefer-
|
|
|
2040
2081
|
<CapSpin spinning={spinning}>
|
|
2041
2082
|
<CapRow>
|
|
2042
2083
|
<CapColumn>
|
|
2043
|
-
<FormBuilder
|
|
2084
|
+
{!this.props.isLoadingMetaEntities && <FormBuilder
|
|
2044
2085
|
channel={MOBILE_PUSH}
|
|
2045
2086
|
schema={schema}
|
|
2046
2087
|
showLiquidErrorInFooter={this.props.showLiquidErrorInFooter}
|
|
@@ -2075,7 +2116,7 @@ export class Edit extends React.Component { // eslint-disable-line react/prefer-
|
|
|
2075
2116
|
hideTestAndPreviewBtn={this.props.hideTestAndPreviewBtn}
|
|
2076
2117
|
isFullMode={this.props.isFullMode}
|
|
2077
2118
|
eventContextTags={this.props?.eventContextTags}
|
|
2078
|
-
/>
|
|
2119
|
+
/>}
|
|
2079
2120
|
</CapColumn>
|
|
2080
2121
|
{this.props.iosCtasData && this.state.showIosCtaTable &&
|
|
2081
2122
|
<CapSlideBox
|