Skip to content

Commit ba91cb0

Browse files
fix: handle invalid data-cslp attributes across multiple components to prevent errors
1 parent b1da67e commit ba91cb0

File tree

9 files changed

+197
-21
lines changed

9 files changed

+197
-21
lines changed

src/visualBuilder/components/fieldLabelWrapper.tsx

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -111,9 +111,11 @@ function FieldLabelWrapperComponent(
111111
const allPaths = uniqBy(
112112
[
113113
props.fieldMetadata,
114-
...props.parentPaths.map((path) => {
115-
return extractDetailsFromCslp(path);
116-
}),
114+
...props.parentPaths
115+
.filter((path) => path)
116+
.map((path) => {
117+
return extractDetailsFromCslp(path);
118+
}),
117119
],
118120
"cslpValue"
119121
);
@@ -140,12 +142,14 @@ function FieldLabelWrapperComponent(
140142
const domAncestor = eventDetails.editableElement.closest(`[data-cslp]:not([data-cslp^="${props.fieldMetadata.content_type_uid}"])`);
141143
if(domAncestor) {
142144
const domAncestorCslp = domAncestor.getAttribute("data-cslp");
143-
const domAncestorDetails = extractDetailsFromCslp(domAncestorCslp!);
144-
const domAncestorContentTypeUid = domAncestorDetails.content_type_uid;
145-
const domAncestorContentParent = referenceData?.find(data => data.contentTypeUid === domAncestorContentTypeUid);
146-
if(domAncestorContentParent) {
147-
referenceFieldName = domAncestorContentParent.referenceFieldName;
148-
parentContentTypeName = domAncestorContentParent.contentTypeTitle;
145+
if (domAncestorCslp) {
146+
const domAncestorDetails = extractDetailsFromCslp(domAncestorCslp);
147+
const domAncestorContentTypeUid = domAncestorDetails.content_type_uid;
148+
const domAncestorContentParent = referenceData?.find(data => data.contentTypeUid === domAncestorContentTypeUid);
149+
if(domAncestorContentParent) {
150+
referenceFieldName = domAncestorContentParent.referenceFieldName;
151+
parentContentTypeName = domAncestorContentParent.contentTypeTitle;
152+
}
149153
}
150154
}
151155
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2+
import { sendFieldEvent } from "../generateOverlay";
3+
import { VisualBuilder } from "../..";
4+
import { VisualBuilderPostMessageEvents } from "../../utils/types/postMessage.types";
5+
import visualBuilderPostMessage from "../../utils/visualBuilderPostMessage";
6+
import { FieldSchemaMap } from "../../utils/fieldSchemaMap";
7+
import { extractDetailsFromCslp } from "../../../cslp/cslpdata";
8+
9+
vi.mock("../../utils/visualBuilderPostMessage", () => ({
10+
default: {
11+
send: vi.fn(),
12+
},
13+
}));
14+
15+
vi.mock("../../utils/fieldSchemaMap", () => ({
16+
FieldSchemaMap: {
17+
getFieldSchema: vi.fn().mockResolvedValue({
18+
display_name: "Test Field",
19+
data_type: "text",
20+
}),
21+
},
22+
}));
23+
24+
vi.mock("../../../cslp/cslpdata", () => ({
25+
extractDetailsFromCslp: vi.fn(),
26+
}));
27+
28+
describe("sendFieldEvent", () => {
29+
let previousSelectedEditableDOM: HTMLElement;
30+
let visualBuilderContainer: HTMLElement;
31+
32+
beforeEach(() => {
33+
previousSelectedEditableDOM = document.createElement("div");
34+
previousSelectedEditableDOM.setAttribute("contenteditable", "true");
35+
previousSelectedEditableDOM.innerText = "Test content";
36+
document.body.appendChild(previousSelectedEditableDOM);
37+
38+
visualBuilderContainer = document.createElement("div");
39+
document.body.appendChild(visualBuilderContainer);
40+
41+
VisualBuilder.VisualBuilderGlobalState.value.previousSelectedEditableDOM =
42+
previousSelectedEditableDOM;
43+
});
44+
45+
afterEach(() => {
46+
document.body.innerHTML = "";
47+
vi.clearAllMocks();
48+
});
49+
50+
it("should return early and not send event when data-cslp attribute is invalid", () => {
51+
previousSelectedEditableDOM.setAttribute("data-cslp", "");
52+
53+
sendFieldEvent({
54+
visualBuilderContainer,
55+
eventType: VisualBuilderPostMessageEvents.UPDATE_FIELD,
56+
});
57+
58+
expect(extractDetailsFromCslp).not.toHaveBeenCalled();
59+
expect(FieldSchemaMap.getFieldSchema).not.toHaveBeenCalled();
60+
expect(visualBuilderPostMessage?.send).not.toHaveBeenCalled();
61+
});
62+
});
63+

src/visualBuilder/generators/generateOverlay.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -177,9 +177,12 @@ export function sendFieldEvent(options: ISendFieldEventParams): void {
177177
? actualEditedElement.innerText
178178
: actualEditedElement.textContent;
179179

180-
const fieldMetadata = extractDetailsFromCslp(
181-
previousSelectedEditableDOM.getAttribute("data-cslp") as string
182-
);
180+
const cslpData = previousSelectedEditableDOM.getAttribute("data-cslp");
181+
if (!cslpData) {
182+
return;
183+
}
184+
185+
const fieldMetadata = extractDetailsFromCslp(cslpData);
183186

184187
FieldSchemaMap.getFieldSchema(
185188
fieldMetadata.content_type_uid,

src/visualBuilder/utils/__test__/getCsDataOfElement.test.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,4 +217,46 @@ describe("getDOMEditStack", () => {
217217
const editStack = getDOMEditStack(leafEl);
218218
expect(editStack.length).toBe(2);
219219
});
220+
221+
test("get dom edit stack should filter out elements with same prefix", () => {
222+
const dom = new JSDOM(`
223+
<div data-cslp="page.pageuid.en-us.group">
224+
<div data-cslp="page.pageuid.en-us.group.field1">
225+
<div data-cslp="page.pageuid.en-us.group.field1.nested">
226+
<span data-cslp="page.pageuid.en-us.group.field1.nested.leaf">leaf</span>
227+
</div>
228+
</div>
229+
</div>
230+
`).window.document;
231+
const leafEl = dom.querySelector(
232+
'[data-cslp="page.pageuid.en-us.group.field1.nested.leaf"]'
233+
) as HTMLElement;
234+
const editStack = getDOMEditStack(leafEl);
235+
// Should only have one entry since all have same prefix (page.pageuid.en-us)
236+
expect(editStack.length).toBe(1);
237+
expect(editStack[0].content_type_uid).toBe("page");
238+
expect(editStack[0].entry_uid).toBe("pageuid");
239+
});
240+
241+
test("get dom edit stack should filter out elements with invalid data-cslp attribute", () => {
242+
const dom = new JSDOM(`
243+
<div data-cslp="page.pageuid.en-us.group">
244+
<div data-cslp>
245+
<div data-cslp="">
246+
<div data-cslp="blog.bloguid.fr-fr.title">
247+
<span data-cslp="blog.bloguid.fr-fr.title.name">name</span>
248+
</div>
249+
</div>
250+
</div>
251+
</div>
252+
`).window.document;
253+
const leafEl = dom.querySelector(
254+
'[data-cslp="blog.bloguid.fr-fr.title.name"]'
255+
) as HTMLElement;
256+
const editStack = getDOMEditStack(leafEl);
257+
// Should have two entries (page, blog) - invalid cslp attributes (empty or no value) should be filtered out
258+
expect(editStack.length).toBe(2);
259+
expect(editStack[0].content_type_uid).toBe("page");
260+
expect(editStack[1].content_type_uid).toBe("blog");
261+
});
220262
});

src/visualBuilder/utils/__test__/getVisualBuilderRedirectionUrl.test.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,4 +84,25 @@ describe('getVisualBuilderRedirectionUrl', () => {
8484
const result = getVisualBuilderRedirectionUrl();
8585
expect(result.toString()).toBe('https://app.example.com/#!/stack/12345/visual-builder?branch=main&environment=production&target-url=https%3A%2F%2Fexample.com%2F');
8686
});
87+
88+
it('should ignore invalid data-cslp attribute and use locale from config', () => {
89+
document.body.innerHTML = '<div data-cslp></div>';
90+
Config.get.mockReturnValue({
91+
stackDetails: {
92+
branch: 'main',
93+
apiKey: '12345',
94+
environment: 'production',
95+
locale: 'en-US'
96+
},
97+
clientUrlParams: {
98+
url: 'https://app.example.com'
99+
}
100+
});
101+
102+
const result = getVisualBuilderRedirectionUrl();
103+
// Should use locale from config when data-cslp attribute is invalid (empty or no value)
104+
expect(result.toString()).toBe('https://app.example.com/#!/stack/12345/visual-builder?branch=main&environment=production&target-url=https%3A%2F%2Fexample.com%2F&locale=en-US');
105+
// Should not call extractDetailsFromCslp for invalid cslp
106+
expect(extractDetailsFromCslp).not.toHaveBeenCalled();
107+
});
87108
});

src/visualBuilder/utils/__test__/updateFocussedState.test.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,11 +55,13 @@ describe("updateFocussedState", () => {
5555
document.body.appendChild(previousSelectedEditableDOM);
5656
VisualBuilder.VisualBuilderGlobalState.value.previousSelectedEditableDOM =
5757
previousSelectedEditableDOM;
58+
vi.clearAllMocks();
5859
});
5960
afterEach(() => {
6061
document.body.innerHTML = "";
6162
VisualBuilder.VisualBuilderGlobalState.value.previousSelectedEditableDOM =
6263
null;
64+
vi.clearAllMocks();
6365
});
6466
it("should return early if required elements are not provided", async () => {
6567
const result = await updateFocussedState({
@@ -233,6 +235,38 @@ describe("updateFocussedState", () => {
233235
expect.any(Boolean)
234236
);
235237
});
238+
239+
it("should return early if data-cslp attribute is invalid", async () => {
240+
const editableElementMock = document.createElement("div");
241+
editableElementMock.setAttribute("data-cslp", "");
242+
const visualBuilderContainerMock = document.createElement("div");
243+
const overlayWrapperMock = document.createElement("div");
244+
const focusedToolbarMock = document.createElement("div");
245+
const resizeObserverMock = {
246+
disconnect: vi.fn(),
247+
} as unknown as ResizeObserver;
248+
249+
const previousSelectedEditableDOM = document.createElement("div");
250+
previousSelectedEditableDOM.setAttribute("data-cslp", "content_type_uid.entry_uid.locale.field_path");
251+
document.body.appendChild(previousSelectedEditableDOM);
252+
VisualBuilder.VisualBuilderGlobalState.value.previousSelectedEditableDOM =
253+
previousSelectedEditableDOM;
254+
255+
document.querySelector = vi.fn().mockReturnValue(previousSelectedEditableDOM);
256+
257+
const result = await updateFocussedState({
258+
editableElement: editableElementMock,
259+
visualBuilderContainer: visualBuilderContainerMock,
260+
overlayWrapper: overlayWrapperMock,
261+
focusedToolbar: focusedToolbarMock,
262+
resizeObserver: resizeObserverMock,
263+
});
264+
265+
// Should return early without processing
266+
expect(result).toBeUndefined();
267+
expect(getEntryPermissionsCached).not.toHaveBeenCalled();
268+
expect(addFocusOverlay).not.toHaveBeenCalled();
269+
});
236270
});
237271

238272
describe("updateFocussedStateOnMutation", () => {

src/visualBuilder/utils/getCsDataOfElement.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,11 @@ export function getDOMEditStack(ele: Element): CslpData[] {
5454
const cslpSet: string[] = [];
5555
let curr: any = ele.closest(`[${DATA_CSLP_ATTR_SELECTOR}]`);
5656
while (curr) {
57-
const cslp = curr.getAttribute(DATA_CSLP_ATTR_SELECTOR)!;
57+
const cslp = curr.getAttribute(DATA_CSLP_ATTR_SELECTOR);
58+
if (!cslp) {
59+
curr = curr.parentElement?.closest(`[${DATA_CSLP_ATTR_SELECTOR}]`);
60+
continue;
61+
}
5862
const entryPrefix = getPrefix(cslp);
5963
const hasSamePrevPrefix = getPrefix(cslpSet.at(0) || "").startsWith(
6064
entryPrefix
@@ -64,5 +68,5 @@ export function getDOMEditStack(ele: Element): CslpData[] {
6468
}
6569
curr = curr.parentElement?.closest(`[${DATA_CSLP_ATTR_SELECTOR}]`);
6670
}
67-
return cslpSet.map((cslp) => extractDetailsFromCslp(cslp));
71+
return cslpSet.filter((cslp) => cslp).map((cslp) => extractDetailsFromCslp(cslp));
6872
}

src/visualBuilder/utils/getVisualBuilderRedirectionUrl.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,16 +22,18 @@ export default function getVisualBuilderRedirectionUrl(): URL {
2222

2323
// get the locale from the data cslp attribute
2424
const elementWithDataCslp = document.querySelector(`[data-cslp]`);
25+
let localeToUse = locale;
2526

2627
if (elementWithDataCslp) {
27-
const cslpData = elementWithDataCslp.getAttribute(
28-
"data-cslp"
29-
) as string;
30-
const { locale } = extractDetailsFromCslp(cslpData);
28+
const cslpData = elementWithDataCslp.getAttribute("data-cslp");
29+
if (cslpData) {
30+
const { locale: cslpLocale } = extractDetailsFromCslp(cslpData);
31+
localeToUse = cslpLocale;
32+
}
33+
}
3134

32-
searchParams.set("locale", locale);
33-
} else if (locale) {
34-
searchParams.set("locale", locale);
35+
if (localeToUse) {
36+
searchParams.set("locale", localeToUse);
3537
}
3638

3739
const completeURL = new URL(

src/visualBuilder/utils/updateFocussedState.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,9 @@ export async function updateFocussedState({
136136
}
137137

138138
const cslp = editableElement?.getAttribute("data-cslp") || "";
139+
if (!cslp) {
140+
return;
141+
}
139142
const fieldMetadata = extractDetailsFromCslp(cslp);
140143

141144
hideHoverOutline(visualBuilderContainer);

0 commit comments

Comments
 (0)