diff --git a/src/visualBuilder/components/fieldLabelWrapper.tsx b/src/visualBuilder/components/fieldLabelWrapper.tsx index 2ef77e6e..2380613b 100644 --- a/src/visualBuilder/components/fieldLabelWrapper.tsx +++ b/src/visualBuilder/components/fieldLabelWrapper.tsx @@ -111,9 +111,11 @@ function FieldLabelWrapperComponent( const allPaths = uniqBy( [ props.fieldMetadata, - ...props.parentPaths.map((path) => { - return extractDetailsFromCslp(path); - }), + ...props.parentPaths + .filter((path) => path) + .map((path) => { + return extractDetailsFromCslp(path); + }), ], "cslpValue" ); @@ -140,12 +142,14 @@ function FieldLabelWrapperComponent( const domAncestor = eventDetails.editableElement.closest(`[data-cslp]:not([data-cslp^="${props.fieldMetadata.content_type_uid}"])`); if(domAncestor) { const domAncestorCslp = domAncestor.getAttribute("data-cslp"); - const domAncestorDetails = extractDetailsFromCslp(domAncestorCslp!); - const domAncestorContentTypeUid = domAncestorDetails.content_type_uid; - const domAncestorContentParent = referenceData?.find(data => data.contentTypeUid === domAncestorContentTypeUid); - if(domAncestorContentParent) { - referenceFieldName = domAncestorContentParent.referenceFieldName; - parentContentTypeName = domAncestorContentParent.contentTypeTitle; + if (domAncestorCslp) { + const domAncestorDetails = extractDetailsFromCslp(domAncestorCslp); + const domAncestorContentTypeUid = domAncestorDetails.content_type_uid; + const domAncestorContentParent = referenceData?.find(data => data.contentTypeUid === domAncestorContentTypeUid); + if(domAncestorContentParent) { + referenceFieldName = domAncestorContentParent.referenceFieldName; + parentContentTypeName = domAncestorContentParent.contentTypeTitle; + } } } } diff --git a/src/visualBuilder/generators/__test__/generateOverlay.test.ts b/src/visualBuilder/generators/__test__/generateOverlay.test.ts new file mode 100644 index 00000000..02de3274 --- /dev/null +++ b/src/visualBuilder/generators/__test__/generateOverlay.test.ts @@ -0,0 +1,63 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; +import { sendFieldEvent } from "../generateOverlay"; +import { VisualBuilder } from "../.."; +import { VisualBuilderPostMessageEvents } from "../../utils/types/postMessage.types"; +import visualBuilderPostMessage from "../../utils/visualBuilderPostMessage"; +import { FieldSchemaMap } from "../../utils/fieldSchemaMap"; +import { extractDetailsFromCslp } from "../../../cslp/cslpdata"; + +vi.mock("../../utils/visualBuilderPostMessage", () => ({ + default: { + send: vi.fn(), + }, +})); + +vi.mock("../../utils/fieldSchemaMap", () => ({ + FieldSchemaMap: { + getFieldSchema: vi.fn().mockResolvedValue({ + display_name: "Test Field", + data_type: "text", + }), + }, +})); + +vi.mock("../../../cslp/cslpdata", () => ({ + extractDetailsFromCslp: vi.fn(), +})); + +describe("sendFieldEvent", () => { + let previousSelectedEditableDOM: HTMLElement; + let visualBuilderContainer: HTMLElement; + + beforeEach(() => { + previousSelectedEditableDOM = document.createElement("div"); + previousSelectedEditableDOM.setAttribute("contenteditable", "true"); + previousSelectedEditableDOM.innerText = "Test content"; + document.body.appendChild(previousSelectedEditableDOM); + + visualBuilderContainer = document.createElement("div"); + document.body.appendChild(visualBuilderContainer); + + VisualBuilder.VisualBuilderGlobalState.value.previousSelectedEditableDOM = + previousSelectedEditableDOM; + }); + + afterEach(() => { + document.body.innerHTML = ""; + vi.clearAllMocks(); + }); + + it("should return early and not send event when data-cslp attribute is invalid", () => { + previousSelectedEditableDOM.setAttribute("data-cslp", ""); + + sendFieldEvent({ + visualBuilderContainer, + eventType: VisualBuilderPostMessageEvents.UPDATE_FIELD, + }); + + expect(extractDetailsFromCslp).not.toHaveBeenCalled(); + expect(FieldSchemaMap.getFieldSchema).not.toHaveBeenCalled(); + expect(visualBuilderPostMessage?.send).not.toHaveBeenCalled(); + }); +}); + diff --git a/src/visualBuilder/generators/generateOverlay.tsx b/src/visualBuilder/generators/generateOverlay.tsx index e09e9be9..595cefff 100644 --- a/src/visualBuilder/generators/generateOverlay.tsx +++ b/src/visualBuilder/generators/generateOverlay.tsx @@ -177,9 +177,12 @@ export function sendFieldEvent(options: ISendFieldEventParams): void { ? actualEditedElement.innerText : actualEditedElement.textContent; - const fieldMetadata = extractDetailsFromCslp( - previousSelectedEditableDOM.getAttribute("data-cslp") as string - ); + const cslpData = previousSelectedEditableDOM.getAttribute("data-cslp"); + if (!cslpData) { + return; + } + + const fieldMetadata = extractDetailsFromCslp(cslpData); FieldSchemaMap.getFieldSchema( fieldMetadata.content_type_uid, diff --git a/src/visualBuilder/utils/__test__/getCsDataOfElement.test.ts b/src/visualBuilder/utils/__test__/getCsDataOfElement.test.ts index 307b52fe..ad387095 100644 --- a/src/visualBuilder/utils/__test__/getCsDataOfElement.test.ts +++ b/src/visualBuilder/utils/__test__/getCsDataOfElement.test.ts @@ -217,4 +217,46 @@ describe("getDOMEditStack", () => { const editStack = getDOMEditStack(leafEl); expect(editStack.length).toBe(2); }); + + test("get dom edit stack should filter out elements with same prefix", () => { + const dom = new JSDOM(` +
+
+
+ leaf +
+
+
+ `).window.document; + const leafEl = dom.querySelector( + '[data-cslp="page.pageuid.en-us.group.field1.nested.leaf"]' + ) as HTMLElement; + const editStack = getDOMEditStack(leafEl); + // Should only have one entry since all have same prefix (page.pageuid.en-us) + expect(editStack.length).toBe(1); + expect(editStack[0].content_type_uid).toBe("page"); + expect(editStack[0].entry_uid).toBe("pageuid"); + }); + + test("get dom edit stack should filter out elements with invalid data-cslp attribute", () => { + const dom = new JSDOM(` +
+
+
+
+ name +
+
+
+
+ `).window.document; + const leafEl = dom.querySelector( + '[data-cslp="blog.bloguid.fr-fr.title.name"]' + ) as HTMLElement; + const editStack = getDOMEditStack(leafEl); + // Should have two entries (page, blog) - invalid cslp attributes (empty or no value) should be filtered out + expect(editStack.length).toBe(2); + expect(editStack[0].content_type_uid).toBe("page"); + expect(editStack[1].content_type_uid).toBe("blog"); + }); }); diff --git a/src/visualBuilder/utils/__test__/getEntryIdentifiersInCurrentPage.test.ts b/src/visualBuilder/utils/__test__/getEntryIdentifiersInCurrentPage.test.ts index 2d057b31..7f50ce74 100644 --- a/src/visualBuilder/utils/__test__/getEntryIdentifiersInCurrentPage.test.ts +++ b/src/visualBuilder/utils/__test__/getEntryIdentifiersInCurrentPage.test.ts @@ -47,4 +47,17 @@ describe("getEntryIdentifiersInCurrentPage", () => { expect(entriesInCurrentPage.length).toBe(0); expect(entriesInCurrentPage).toEqual([]); }); + + test("should filter out elements with empty data-cslp attribute and not break", () => { + document.body.innerHTML = ` +
+

Empty CSLP

+

Empty CSLP

+

Valid CSLP

+
+ `; + const { entriesInCurrentPage } = getEntryIdentifiersInCurrentPage(); + expect(entriesInCurrentPage.length).toBe(1); + expect(entriesInCurrentPage[0].entryUid).toBe('bltf5bb5f8fb088a332'); + }); }); diff --git a/src/visualBuilder/utils/__test__/getVisualBuilderRedirectionUrl.test.ts b/src/visualBuilder/utils/__test__/getVisualBuilderRedirectionUrl.test.ts index e7232ad6..c09ec900 100644 --- a/src/visualBuilder/utils/__test__/getVisualBuilderRedirectionUrl.test.ts +++ b/src/visualBuilder/utils/__test__/getVisualBuilderRedirectionUrl.test.ts @@ -84,4 +84,25 @@ describe('getVisualBuilderRedirectionUrl', () => { const result = getVisualBuilderRedirectionUrl(); expect(result.toString()).toBe('https://app.example.com/#!/stack/12345/visual-builder?branch=main&environment=production&target-url=https%3A%2F%2Fexample.com%2F'); }); + + it('should ignore invalid data-cslp attribute and use locale from config', () => { + document.body.innerHTML = '
'; + Config.get.mockReturnValue({ + stackDetails: { + branch: 'main', + apiKey: '12345', + environment: 'production', + locale: 'en-US' + }, + clientUrlParams: { + url: 'https://app.example.com' + } + }); + + const result = getVisualBuilderRedirectionUrl(); + // Should use locale from config when data-cslp attribute is invalid (empty or no value) + 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'); + // Should not call extractDetailsFromCslp for invalid cslp + expect(extractDetailsFromCslp).not.toHaveBeenCalled(); + }); }); \ No newline at end of file diff --git a/src/visualBuilder/utils/__test__/updateFocussedState.test.ts b/src/visualBuilder/utils/__test__/updateFocussedState.test.ts index f427eaf0..835114c4 100644 --- a/src/visualBuilder/utils/__test__/updateFocussedState.test.ts +++ b/src/visualBuilder/utils/__test__/updateFocussedState.test.ts @@ -55,11 +55,13 @@ describe("updateFocussedState", () => { document.body.appendChild(previousSelectedEditableDOM); VisualBuilder.VisualBuilderGlobalState.value.previousSelectedEditableDOM = previousSelectedEditableDOM; + vi.clearAllMocks(); }); afterEach(() => { document.body.innerHTML = ""; VisualBuilder.VisualBuilderGlobalState.value.previousSelectedEditableDOM = null; + vi.clearAllMocks(); }); it("should return early if required elements are not provided", async () => { const result = await updateFocussedState({ @@ -233,6 +235,38 @@ describe("updateFocussedState", () => { expect.any(Boolean) ); }); + + it("should return early if data-cslp attribute is invalid", async () => { + const editableElementMock = document.createElement("div"); + editableElementMock.setAttribute("data-cslp", ""); + const visualBuilderContainerMock = document.createElement("div"); + const overlayWrapperMock = document.createElement("div"); + const focusedToolbarMock = document.createElement("div"); + const resizeObserverMock = { + disconnect: vi.fn(), + } as unknown as ResizeObserver; + + const previousSelectedEditableDOM = document.createElement("div"); + previousSelectedEditableDOM.setAttribute("data-cslp", "content_type_uid.entry_uid.locale.field_path"); + document.body.appendChild(previousSelectedEditableDOM); + VisualBuilder.VisualBuilderGlobalState.value.previousSelectedEditableDOM = + previousSelectedEditableDOM; + + document.querySelector = vi.fn().mockReturnValue(previousSelectedEditableDOM); + + const result = await updateFocussedState({ + editableElement: editableElementMock, + visualBuilderContainer: visualBuilderContainerMock, + overlayWrapper: overlayWrapperMock, + focusedToolbar: focusedToolbarMock, + resizeObserver: resizeObserverMock, + }); + + // Should return early without processing + expect(result).toBeUndefined(); + expect(getEntryPermissionsCached).not.toHaveBeenCalled(); + expect(addFocusOverlay).not.toHaveBeenCalled(); + }); }); describe("updateFocussedStateOnMutation", () => { diff --git a/src/visualBuilder/utils/getCsDataOfElement.ts b/src/visualBuilder/utils/getCsDataOfElement.ts index 2cce0888..7139a50f 100644 --- a/src/visualBuilder/utils/getCsDataOfElement.ts +++ b/src/visualBuilder/utils/getCsDataOfElement.ts @@ -54,7 +54,11 @@ export function getDOMEditStack(ele: Element): CslpData[] { const cslpSet: string[] = []; let curr: any = ele.closest(`[${DATA_CSLP_ATTR_SELECTOR}]`); while (curr) { - const cslp = curr.getAttribute(DATA_CSLP_ATTR_SELECTOR)!; + const cslp = curr.getAttribute(DATA_CSLP_ATTR_SELECTOR); + if (!cslp) { + curr = curr.parentElement?.closest(`[${DATA_CSLP_ATTR_SELECTOR}]`); + continue; + } const entryPrefix = getPrefix(cslp); const hasSamePrevPrefix = getPrefix(cslpSet.at(0) || "").startsWith( entryPrefix @@ -64,5 +68,5 @@ export function getDOMEditStack(ele: Element): CslpData[] { } curr = curr.parentElement?.closest(`[${DATA_CSLP_ATTR_SELECTOR}]`); } - return cslpSet.map((cslp) => extractDetailsFromCslp(cslp)); + return cslpSet.filter((cslp) => cslp).map((cslp) => extractDetailsFromCslp(cslp)); } diff --git a/src/visualBuilder/utils/getEntryIdentifiersInCurrentPage.ts b/src/visualBuilder/utils/getEntryIdentifiersInCurrentPage.ts index 7ea4f741..243bc4a1 100644 --- a/src/visualBuilder/utils/getEntryIdentifiersInCurrentPage.ts +++ b/src/visualBuilder/utils/getEntryIdentifiersInCurrentPage.ts @@ -14,9 +14,9 @@ export function getEntryIdentifiersInCurrentPage(): EntryIdentifiers { ); const uniqueEntriesMap = new Map(); elementsWithCslp.forEach((element) => { - const cslpData = extractDetailsFromCslp( - element.getAttribute("data-cslp") as string - ); + const cslpValue = element.getAttribute("data-cslp"); + if (!cslpValue) return; + const cslpData = extractDetailsFromCslp(cslpValue); uniqueEntriesMap.set(cslpData.entry_uid, { entryUid: cslpData.entry_uid, diff --git a/src/visualBuilder/utils/getVisualBuilderRedirectionUrl.ts b/src/visualBuilder/utils/getVisualBuilderRedirectionUrl.ts index 955e466c..02e4659b 100644 --- a/src/visualBuilder/utils/getVisualBuilderRedirectionUrl.ts +++ b/src/visualBuilder/utils/getVisualBuilderRedirectionUrl.ts @@ -22,16 +22,18 @@ export default function getVisualBuilderRedirectionUrl(): URL { // get the locale from the data cslp attribute const elementWithDataCslp = document.querySelector(`[data-cslp]`); + let localeToUse = locale; if (elementWithDataCslp) { - const cslpData = elementWithDataCslp.getAttribute( - "data-cslp" - ) as string; - const { locale } = extractDetailsFromCslp(cslpData); + const cslpData = elementWithDataCslp.getAttribute("data-cslp"); + if (cslpData) { + const { locale: cslpLocale } = extractDetailsFromCslp(cslpData); + localeToUse = cslpLocale; + } + } - searchParams.set("locale", locale); - } else if (locale) { - searchParams.set("locale", locale); + if (localeToUse) { + searchParams.set("locale", localeToUse); } const completeURL = new URL( diff --git a/src/visualBuilder/utils/updateFocussedState.ts b/src/visualBuilder/utils/updateFocussedState.ts index a5799fab..a6a33509 100644 --- a/src/visualBuilder/utils/updateFocussedState.ts +++ b/src/visualBuilder/utils/updateFocussedState.ts @@ -136,6 +136,9 @@ export async function updateFocussedState({ } const cslp = editableElement?.getAttribute("data-cslp") || ""; + if (!cslp) { + return; + } const fieldMetadata = extractDetailsFromCslp(cslp); hideHoverOutline(visualBuilderContainer);