From 03964afb67a548c7cc0be75a9b4e8ad14c866008 Mon Sep 17 00:00:00 2001 From: hiteshshetty-dev Date: Tue, 13 Jan 2026 15:39:10 +0530 Subject: [PATCH 1/3] feat: implement isValidCslp function for CSLP value validation and add corresponding tests --- src/cslp/__test__/cslpdata.test.ts | 122 +++++++++++++++++- src/cslp/cslpdata.ts | 72 ++++++++++- src/livePreview/editButton/editButton.ts | 4 +- src/timeline/compare/compare.ts | 8 +- .../components/fieldLabelWrapper.tsx | 4 +- .../useRecalculateVariantDataCSLPValues.ts | 5 +- .../useRevalidateFieldDataPostMessageEvent.ts | 6 +- .../useVariantsPostMessageEvent.ts | 4 +- .../generators/generateEmptyBlock.tsx | 5 +- .../generators/generateHighlightedComment.tsx | 3 +- .../generators/generateOverlay.tsx | 4 +- src/visualBuilder/index.ts | 4 +- src/visualBuilder/listeners/mouseClick.ts | 24 ++-- src/visualBuilder/utils/getCsDataOfElement.ts | 8 +- .../utils/getEntryIdentifiersInCurrentPage.ts | 4 +- .../utils/getVisualBuilderRedirectionUrl.ts | 4 +- .../utils/updateFocussedState.ts | 4 +- 17 files changed, 242 insertions(+), 43 deletions(-) diff --git a/src/cslp/__test__/cslpdata.test.ts b/src/cslp/__test__/cslpdata.test.ts index 53905805..b297bbd5 100644 --- a/src/cslp/__test__/cslpdata.test.ts +++ b/src/cslp/__test__/cslpdata.test.ts @@ -1,4 +1,124 @@ -import { extractDetailsFromCslp } from "../cslpdata"; +import { extractDetailsFromCslp, isValidCslp } from "../cslpdata"; + +describe("isValidCslp", () => { + describe("valid cases", () => { + test("should return true for valid v1 format with 3 parts", () => { + expect( + isValidCslp("content_type_uid.entry_uid.locale") + ).toBeTruthy(); + }); + + test("should return true for valid v1 format with field path", () => { + expect( + isValidCslp("content_type_uid.entry_uid.locale.field_path") + ).toBeTruthy(); + }); + + test("should return true for valid v2 format with 3 parts", () => { + expect( + isValidCslp("v2:content_type_uid.entry_uid_variant_uid.locale") + ).toBeTruthy(); + }); + + test("should return true for valid v2 format with field path", () => { + expect( + isValidCslp( + "v2:content_type_uid.entry_uid_variant_uid.locale.field_path" + ) + ).toBeTruthy(); + }); + }); + + describe("invalid cases", () => { + test("should return false for null", () => { + expect(isValidCslp(null)).toBeFalsy(); + }); + + test("should return false for undefined", () => { + expect(isValidCslp(undefined)).toBeFalsy(); + }); + + test("should return false for empty string", () => { + expect(isValidCslp("")).toBeFalsy(); + }); + + test("should return false for string with less than 3 parts", () => { + expect(isValidCslp("invalid")).toBeFalsy(); + }); + + test("should return false for string with only 2 parts", () => { + expect(isValidCslp("a.b")).toBeFalsy(); + }); + + test("should return false for v2 prefix with no data", () => { + expect(isValidCslp("v2:")).toBeFalsy(); + }); + + test("should return false for v2 prefix with only 2 parts", () => { + expect(isValidCslp("v2:a.b")).toBeFalsy(); + }); + + test("should return false for v2 format where entry_uid_variant_uid has no underscore", () => { + expect( + isValidCslp("v2:content_type_uid.entry.locale") + ).toBeFalsy(); + }); + + test("should return false for v2 format where entry_uid_variant_uid is missing variant_uid", () => { + expect( + isValidCslp("v2:content_type_uid.entry_.locale") + ).toBeFalsy(); + }); + + test("should return false for v2 format where entry_uid_variant_uid is missing entry_uid", () => { + expect( + isValidCslp("v2:content_type_uid._variant_uid.locale") + ).toBeFalsy(); + }); + + test("should return false for v2 format with empty parts", () => { + expect(isValidCslp("v2:..locale")).toBeFalsy(); + }); + + test("should return false for v2 format with empty content_type_uid", () => { + expect(isValidCslp("v2:.entry_uid_variant_uid.locale")).toBeFalsy(); + }); + + test("should return false for v2 format with empty locale", () => { + expect( + isValidCslp("v2:content_type_uid.entry_uid_variant_uid.") + ).toBeFalsy(); + }); + + test("should return false for v1 format with empty parts", () => { + expect(isValidCslp("..locale")).toBeFalsy(); + }); + + test("should return false for v1 format with empty content_type_uid", () => { + expect(isValidCslp(".entry_uid.locale")).toBeFalsy(); + }); + + test("should return false for v1 format with empty entry_uid", () => { + expect(isValidCslp("content_type_uid..locale")).toBeFalsy(); + }); + + test("should return false for whitespace-only string", () => { + expect(isValidCslp(" ")).toBeFalsy(); + }); + + test("should return false for tab and newline whitespace", () => { + expect(isValidCslp("\t\n")).toBeFalsy(); + }); + + test("should return false for string with only dots", () => { + expect(isValidCslp("...")).toBeFalsy(); + }); + + test("should return false for string with only two dots", () => { + expect(isValidCslp("..")).toBeFalsy(); + }); + }); +}); describe("extractDetailsFromCslp", () => { test("should extract details from a CSLP value string with nested multiple field", () => { diff --git a/src/cslp/cslpdata.ts b/src/cslp/cslpdata.ts index 90f3fb65..661aed61 100644 --- a/src/cslp/cslpdata.ts +++ b/src/cslp/cslpdata.ts @@ -8,6 +8,76 @@ import Config from "../configManager/configManager"; import { DeepSignal } from "deepsignal"; import { cslpTagStyles } from "../livePreview/editButton/editButton.style"; +/** + * Validates that the required CSLP parts (content_type_uid, entry_uid/entry_uid_variant_uid, locale) are non-empty. + * @param parts The array of parts from splitting the CSLP string by "." + * @returns `true` if all required parts (first 3) are non-empty, `false` otherwise. + */ +function areRequiredPartsNonEmpty(parts: string[]): boolean { + // Check that we have at least 3 parts + if (parts.length < 3) { + return false; + } + // Verify that content_type_uid (parts[0]), entry_uid/entry_uid_variant_uid (parts[1]), and locale (parts[2]) are all non-empty + return parts[0].length > 0 && parts[1].length > 0 && parts[2].length > 0; +} + +/** + * Validates if a CSLP value string is valid. + * + * Supports two formats: + * - **v1 format**: `content_type_uid.entry_uid.locale[.field_path]` (requires at least 3 parts) + * - **v2 format**: `v2:content_type_uid.entry_uid_variant_uid.locale[.field_path]` + * (requires at least 3 parts, entry_uid_variant_uid must contain underscore separating entry_uid and variant_uid) + * + * @param cslpValue The CSLP value string to validate (can be null or undefined). + * @returns Type predicate: `true` if the CSLP value is valid (narrows type to `string`), `false` otherwise. + * + * @example + * Valid v1 format + * isValidCslp("page.entry123.en-us") -> true + * isValidCslp("page.entry123.en-us.title") -> true + * + * Valid v2 format + * isValidCslp("v2:page.entry123_variant456.en-us") -> true + * isValidCslp("v2:page.entry123_variant456.en-us.title") -> true + * + * Invalid cases + * isValidCslp(null) -> false + * isValidCslp("invalid") -> false (less than 3 parts) + * isValidCslp("v2:page.entry123.en-us") -> false (missing underscore in entry_uid_variant_uid) + */ +export function isValidCslp( + cslpValue: string | null | undefined +): cslpValue is string { + // Return false for null, undefined, or empty string + if (!cslpValue) { + return false; + } + + // Check for v2 format (starts with "v2:") + if (cslpValue.startsWith("v2:")) { + const dataAfterPrefix = cslpValue.substring(3); // Remove "v2:" prefix + const parts = dataAfterPrefix.split("."); + // v2 format requires at least 3 parts: content_type_uid.entry_uid_variant_uid.locale + // Verify that content_type_uid, entry_uid_variant_uid, and locale are all non-empty + if (!areRequiredPartsNonEmpty(parts)) { + return false; + } + // Verify that entry_uid_variant_uid (second part) contains both entry_uid and variant_uid separated by at least one underscore + const entryUidVariantUid = parts[1]; + const entryVariantParts = entryUidVariantUid.split("_"); + // Check that we have at least 2 parts (entry_uid and variant_uid) and all parts are non-empty + return entryVariantParts.length >= 2 && entryVariantParts.every((part) => part.length > 0); + } + + // v1 format (default, no prefix) + const parts = cslpValue.split("."); + // v1 format requires at least 3 parts: content_type_uid.entry_uid.locale + // Verify that content_type_uid, entry_uid, and locale are all non-empty + return areRequiredPartsNonEmpty(parts); +} + /** * Extracts details from a CSLP value string. * @param cslpValue The CSLP value string to extract details from. @@ -163,7 +233,7 @@ export function addCslpOutline( const cslpTag = element.getAttribute("data-cslp"); - if (trigger && cslpTag) { + if (trigger && isValidCslp(cslpTag)) { if (elements.highlightedElement) elements.highlightedElement.classList.remove( cslpTagStyles()["cslp-edit-mode"] diff --git a/src/livePreview/editButton/editButton.ts b/src/livePreview/editButton/editButton.ts index df88fc7d..7d84fe5c 100644 --- a/src/livePreview/editButton/editButton.ts +++ b/src/livePreview/editButton/editButton.ts @@ -1,7 +1,7 @@ import { effect } from "@preact/signals"; import { inIframe, isOpeningInNewTab } from "../../common/inIframe"; import Config from "../../configManager/configManager"; -import { addCslpOutline, extractDetailsFromCslp } from "../../cslp"; +import { addCslpOutline, extractDetailsFromCslp, isValidCslp } from "../../cslp"; import { cslpTagStyles } from "./editButton.style"; import { PublicLogger } from "../../logger/logger"; import { @@ -439,7 +439,7 @@ export class LivePreviewEditButton { const cslpTag = this.tooltip.getAttribute("current-data-cslp"); - if (cslpTag) { + if (isValidCslp(cslpTag)) { const { content_type_uid, entry_uid, diff --git a/src/timeline/compare/compare.ts b/src/timeline/compare/compare.ts index abdac01d..cbd5db3d 100644 --- a/src/timeline/compare/compare.ts +++ b/src/timeline/compare/compare.ts @@ -1,6 +1,7 @@ import timelinePostMessage from "../timelinePostMessage/timelinePostMessage"; import { timelinePostMessageEvents } from "../timelinePostMessage/timelinePostMessage.constant"; import { compareGlobalStyles } from "./compare.style"; +import { isValidCslp } from "../../cslp/cslpdata"; const voidElements = new Set([ "area", @@ -64,7 +65,8 @@ export function handleWebCompare() { ); const map: Record = {}; for (const element of elements) { - const cslp = element.getAttribute("data-cslp")!; + const cslp = element.getAttribute("data-cslp"); + if (!isValidCslp(cslp)) continue; if ( element.hasAttributes() && voidElements.has(element.tagName.toLowerCase()) @@ -101,8 +103,8 @@ export function handleWebCompare() { document.querySelectorAll(LEAF_CSLP_SELECTOR) ); for (const element of elements) { - const path = element.getAttribute("data-cslp")!; - if (!diff[path]) continue; + const path = element.getAttribute("data-cslp"); + if (!isValidCslp(path) || !diff[path]) continue; if (voidElements.has(element.tagName.toLowerCase())) { element.classList.add(`cs-compare__void--${operation}`); diff --git a/src/visualBuilder/components/fieldLabelWrapper.tsx b/src/visualBuilder/components/fieldLabelWrapper.tsx index 2e3d0d6e..af2ed986 100644 --- a/src/visualBuilder/components/fieldLabelWrapper.tsx +++ b/src/visualBuilder/components/fieldLabelWrapper.tsx @@ -1,6 +1,6 @@ import classNames from "classnames"; import React, { useEffect, useState } from "preact/compat"; -import { extractDetailsFromCslp } from "../../cslp"; +import { extractDetailsFromCslp, isValidCslp } from "../../cslp"; import { CslpData } from "../../cslp/types/cslp.types"; import { VisualBuilderCslpEventDetails } from "../types/visualBuilder.types"; import { FieldSchemaMap } from "../utils/fieldSchemaMap"; @@ -144,7 +144,7 @@ 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"); - if (domAncestorCslp) { + if (isValidCslp(domAncestorCslp)) { const domAncestorDetails = extractDetailsFromCslp(domAncestorCslp); const domAncestorContentTypeUid = domAncestorDetails.content_type_uid; const domAncestorContentParent = referenceData?.find(data => data.contentTypeUid === domAncestorContentTypeUid); diff --git a/src/visualBuilder/eventManager/useRecalculateVariantDataCSLPValues.ts b/src/visualBuilder/eventManager/useRecalculateVariantDataCSLPValues.ts index a577da4a..794e7948 100644 --- a/src/visualBuilder/eventManager/useRecalculateVariantDataCSLPValues.ts +++ b/src/visualBuilder/eventManager/useRecalculateVariantDataCSLPValues.ts @@ -3,6 +3,7 @@ import livePreviewPostMessage from "../../livePreview/eventManager/livePreviewEv import { LIVE_PREVIEW_POST_MESSAGE_EVENTS } from "../../livePreview/eventManager/livePreviewEventManager.constant"; import { DATA_CSLP_ATTR_SELECTOR } from "../utils/constants"; import { visualBuilderStyles } from "../visualBuilder.style"; +import { isValidCslp } from "../../cslp/cslpdata"; const VARIANT_UPDATE_DELAY_MS: Readonly = 8000; @@ -37,7 +38,7 @@ function updateVariantClasses({ dataCslp: string, observer?: MutationObserver ) => { - if (!dataCslp) return; + if (!isValidCslp(dataCslp)) return; if ( dataCslp.startsWith("v2:") && @@ -87,7 +88,7 @@ function updateVariantClasses({ const addElementClasses = (element: HTMLElement) => { const dataCslp = element.getAttribute(DATA_CSLP_ATTR_SELECTOR); - if (!dataCslp) { + if (!isValidCslp(dataCslp)) { //recursive call for child nodes element.childNodes.forEach((child) => { if (child instanceof HTMLElement) { diff --git a/src/visualBuilder/eventManager/useRevalidateFieldDataPostMessageEvent.ts b/src/visualBuilder/eventManager/useRevalidateFieldDataPostMessageEvent.ts index d984333a..339b8007 100644 --- a/src/visualBuilder/eventManager/useRevalidateFieldDataPostMessageEvent.ts +++ b/src/visualBuilder/eventManager/useRevalidateFieldDataPostMessageEvent.ts @@ -1,5 +1,5 @@ import { VisualBuilder } from ".."; -import { extractDetailsFromCslp } from "../../cslp"; +import { extractDetailsFromCslp, isValidCslp } from "../../cslp"; import { FieldSchemaMap } from "../utils/fieldSchemaMap"; import { hideFocusOverlay } from "../generators/generateOverlay"; import { handleBuilderInteraction } from "../listeners/mouseClick"; @@ -32,7 +32,7 @@ export async function handleRevalidateFieldData(): Promise { if (targetElement) { const cslp = targetElement.getAttribute("data-cslp"); - if (cslp) { + if (isValidCslp(cslp)) { const fieldMetadata = extractDetailsFromCslp(cslp); // Try to revalidate specific field schema and data @@ -51,7 +51,7 @@ export async function handleRevalidateFieldData(): Promise { window.location.reload(); } finally { // Step 3: Refocus the element if we had one focused before - if (shouldRefocus && elementCslp) { + if (shouldRefocus && isValidCslp(elementCslp)) { await refocusElement(elementCslp, elementCslpUniqueId); } } diff --git a/src/visualBuilder/eventManager/useVariantsPostMessageEvent.ts b/src/visualBuilder/eventManager/useVariantsPostMessageEvent.ts index b1d8a229..ac577a8d 100644 --- a/src/visualBuilder/eventManager/useVariantsPostMessageEvent.ts +++ b/src/visualBuilder/eventManager/useVariantsPostMessageEvent.ts @@ -3,7 +3,7 @@ import { visualBuilderStyles } from "../visualBuilder.style"; import visualBuilderPostMessage from "../utils/visualBuilderPostMessage"; import { VisualBuilderPostMessageEvents } from "../utils/types/postMessage.types"; import { FieldSchemaMap } from "../utils/fieldSchemaMap"; -import { extractDetailsFromCslp } from "../../cslp/cslpdata"; +import { extractDetailsFromCslp, isValidCslp } from "../../cslp/cslpdata"; interface VariantFieldsEvent { data: { @@ -58,7 +58,7 @@ export function addVariantFieldClass( const elements = document.querySelectorAll(`[data-cslp]`); elements.forEach((element) => { const dataCslp = element.getAttribute("data-cslp"); - if (!dataCslp) return; + if (!isValidCslp(dataCslp)) return; if (dataCslp?.includes(variant_uid)) { highlightVariantFields && diff --git a/src/visualBuilder/generators/generateEmptyBlock.tsx b/src/visualBuilder/generators/generateEmptyBlock.tsx index 8797230a..f3ef81f8 100644 --- a/src/visualBuilder/generators/generateEmptyBlock.tsx +++ b/src/visualBuilder/generators/generateEmptyBlock.tsx @@ -1,6 +1,7 @@ import { hydrate } from "preact"; +import React from "preact/compat"; import { EmptyBlock } from "../components/emptyBlock"; -import { extractDetailsFromCslp } from "../../cslp"; +import { extractDetailsFromCslp, isValidCslp } from "../../cslp"; import { FieldSchemaMap } from "../utils/fieldSchemaMap"; export async function generateEmptyBlocks( @@ -8,7 +9,7 @@ export async function generateEmptyBlocks( ): Promise { for (const emptyBlockParent of emptyBlockParents) { const cslpData = emptyBlockParent.getAttribute("data-cslp"); - if (!cslpData) { + if (!isValidCslp(cslpData)) { return; } const fieldMetadata = extractDetailsFromCslp(cslpData); diff --git a/src/visualBuilder/generators/generateHighlightedComment.tsx b/src/visualBuilder/generators/generateHighlightedComment.tsx index 324a12e4..7fa8c7c9 100644 --- a/src/visualBuilder/generators/generateHighlightedComment.tsx +++ b/src/visualBuilder/generators/generateHighlightedComment.tsx @@ -4,6 +4,7 @@ import HighlightedCommentIcon from "../components/HighlightedCommentIcon"; import { css } from "goober"; import React from "preact/compat"; import { IHighlightCommentData } from "../eventManager/useHighlightCommentIcon"; +import { isValidCslp } from "../../cslp"; /** * Inserts highlighted comment icons based on an array of paths. @@ -24,7 +25,7 @@ export function highlightCommentIconOnCanvas( const cslpValue = data?.fieldMetadata?.cslpValue; // Check if the cslpValue is already in the Object - if (!cslpValue || uniquePaths[cslpValue]) { + if (!isValidCslp(cslpValue) || uniquePaths[cslpValue]) { return; // Skip if the value is not unique } diff --git a/src/visualBuilder/generators/generateOverlay.tsx b/src/visualBuilder/generators/generateOverlay.tsx index 595cefff..abe99821 100644 --- a/src/visualBuilder/generators/generateOverlay.tsx +++ b/src/visualBuilder/generators/generateOverlay.tsx @@ -1,4 +1,4 @@ -import { extractDetailsFromCslp } from "../../cslp/cslpdata"; +import { extractDetailsFromCslp, isValidCslp } from "../../cslp/cslpdata"; import { cleanIndividualFieldResidual } from "../utils/handleIndividualFields"; import visualBuilderPostMessage from "../utils/visualBuilderPostMessage"; import { VisualBuilderPostMessageEvents } from "../utils/types/postMessage.types"; @@ -178,7 +178,7 @@ export function sendFieldEvent(options: ISendFieldEventParams): void { : actualEditedElement.textContent; const cslpData = previousSelectedEditableDOM.getAttribute("data-cslp"); - if (!cslpData) { + if (!isValidCslp(cslpData)) { return; } diff --git a/src/visualBuilder/index.ts b/src/visualBuilder/index.ts index da5992f3..c7dafe88 100644 --- a/src/visualBuilder/index.ts +++ b/src/visualBuilder/index.ts @@ -21,7 +21,7 @@ import { VisualBuilderPostMessageEvents } from "./utils/types/postMessage.types" import { setup } from "goober"; import { debounce, isEqual } from "lodash-es"; import { h } from "preact"; -import { extractDetailsFromCslp } from "../cslp"; +import { extractDetailsFromCslp, isValidCslp } from "../cslp"; import initUI from "./components"; import { useDraftFieldsPostMessageEvent } from "./eventManager/useDraftFieldsPostMessageEvent"; import { useHideFocusOverlayPostMessageEvent } from "./eventManager/useHideFocusOverlayPostMessageEvent"; @@ -176,7 +176,7 @@ export class VisualBuilder { const cslpData = editableElement && editableElement.getAttribute("data-cslp"); - if (!editableElement || !cslpData) { + if (!editableElement || !isValidCslp(cslpData)) { return; } diff --git a/src/visualBuilder/listeners/mouseClick.ts b/src/visualBuilder/listeners/mouseClick.ts index 76b2ca9e..c78bd820 100644 --- a/src/visualBuilder/listeners/mouseClick.ts +++ b/src/visualBuilder/listeners/mouseClick.ts @@ -7,6 +7,7 @@ import { getCsDataOfElement, getDOMEditStack, } from "../utils/getCsDataOfElement"; +import { isValidCslp } from "../../cslp"; import { appendFocusedToolbar } from "../generators/generateToolbar"; @@ -91,16 +92,19 @@ export async function handleBuilderInteraction( // assign a unique ID to each element which we can use to identify // them in updateFocussedState and other places where we // would have queried the element by data-cslp - const duplicates = document.querySelectorAll( - `[data-cslp="${eventTarget?.getAttribute("data-cslp")}"]` - ); - if (duplicates.length > 1) { - duplicates.forEach((ele) => { - if (!ele.hasAttribute("data-cslp-unique-id")) { - const uniqueId = `cslp-${uuidV4()}`; - ele.setAttribute("data-cslp-unique-id", uniqueId); - } - }); + const eventTargetCslp = eventTarget?.getAttribute("data-cslp"); + if (isValidCslp(eventTargetCslp)) { + const duplicates = document.querySelectorAll( + `[data-cslp="${eventTargetCslp}"]` + ); + if (duplicates.length > 1) { + duplicates.forEach((ele) => { + if (!ele.hasAttribute("data-cslp-unique-id")) { + const uniqueId = `cslp-${uuidV4()}`; + ele.setAttribute("data-cslp-unique-id", uniqueId); + } + }); + } } // if the target element is a studio-ui element, return diff --git a/src/visualBuilder/utils/getCsDataOfElement.ts b/src/visualBuilder/utils/getCsDataOfElement.ts index 7139a50f..a4d952dc 100644 --- a/src/visualBuilder/utils/getCsDataOfElement.ts +++ b/src/visualBuilder/utils/getCsDataOfElement.ts @@ -1,6 +1,6 @@ import { CslpData } from "../../cslp/types/cslp.types"; import { VisualBuilderCslpEventDetails } from "../types/visualBuilder.types"; -import { extractDetailsFromCslp } from "../../cslp/cslpdata"; +import { extractDetailsFromCslp, isValidCslp } from "../../cslp/cslpdata"; import { DATA_CSLP_ATTR_SELECTOR } from "./constants"; /** @@ -23,7 +23,7 @@ export function getCsDataOfElement( return; } const cslpData = editableElement.getAttribute("data-cslp"); - if (!cslpData) { + if (!isValidCslp(cslpData)) { return; } const fieldMetadata = extractDetailsFromCslp(cslpData); @@ -55,7 +55,7 @@ export function getDOMEditStack(ele: Element): CslpData[] { let curr: any = ele.closest(`[${DATA_CSLP_ATTR_SELECTOR}]`); while (curr) { const cslp = curr.getAttribute(DATA_CSLP_ATTR_SELECTOR); - if (!cslp) { + if (!isValidCslp(cslp)) { curr = curr.parentElement?.closest(`[${DATA_CSLP_ATTR_SELECTOR}]`); continue; } @@ -68,5 +68,5 @@ export function getDOMEditStack(ele: Element): CslpData[] { } curr = curr.parentElement?.closest(`[${DATA_CSLP_ATTR_SELECTOR}]`); } - return cslpSet.filter((cslp) => cslp).map((cslp) => extractDetailsFromCslp(cslp)); + return cslpSet.filter(isValidCslp).map((cslp) => extractDetailsFromCslp(cslp)); } diff --git a/src/visualBuilder/utils/getEntryIdentifiersInCurrentPage.ts b/src/visualBuilder/utils/getEntryIdentifiersInCurrentPage.ts index 243bc4a1..d3e74685 100644 --- a/src/visualBuilder/utils/getEntryIdentifiersInCurrentPage.ts +++ b/src/visualBuilder/utils/getEntryIdentifiersInCurrentPage.ts @@ -1,4 +1,4 @@ -import { extractDetailsFromCslp } from "../../cslp/cslpdata"; +import { extractDetailsFromCslp, isValidCslp } from "../../cslp/cslpdata"; type EntryIdentifiers = { entriesInCurrentPage: { @@ -15,7 +15,7 @@ export function getEntryIdentifiersInCurrentPage(): EntryIdentifiers { const uniqueEntriesMap = new Map(); elementsWithCslp.forEach((element) => { const cslpValue = element.getAttribute("data-cslp"); - if (!cslpValue) return; + if (!isValidCslp(cslpValue)) return; const cslpData = extractDetailsFromCslp(cslpValue); uniqueEntriesMap.set(cslpData.entry_uid, { diff --git a/src/visualBuilder/utils/getVisualBuilderRedirectionUrl.ts b/src/visualBuilder/utils/getVisualBuilderRedirectionUrl.ts index 02e4659b..17db0289 100644 --- a/src/visualBuilder/utils/getVisualBuilderRedirectionUrl.ts +++ b/src/visualBuilder/utils/getVisualBuilderRedirectionUrl.ts @@ -1,5 +1,5 @@ import Config from "../../configManager/configManager"; -import { extractDetailsFromCslp } from "../../cslp"; +import { extractDetailsFromCslp, isValidCslp } from "../../cslp"; /** * Returns the redirection URL for the Visual builder. @@ -26,7 +26,7 @@ export default function getVisualBuilderRedirectionUrl(): URL { if (elementWithDataCslp) { const cslpData = elementWithDataCslp.getAttribute("data-cslp"); - if (cslpData) { + if (isValidCslp(cslpData)) { const { locale: cslpLocale } = extractDetailsFromCslp(cslpData); localeToUse = cslpLocale; } diff --git a/src/visualBuilder/utils/updateFocussedState.ts b/src/visualBuilder/utils/updateFocussedState.ts index 989560f2..295a5683 100644 --- a/src/visualBuilder/utils/updateFocussedState.ts +++ b/src/visualBuilder/utils/updateFocussedState.ts @@ -1,5 +1,5 @@ import { VisualBuilder } from ".."; -import { extractDetailsFromCslp } from "../../cslp"; +import { extractDetailsFromCslp, isValidCslp } from "../../cslp"; import { getAddInstanceButtons } from "../generators/generateAddInstanceButtons"; import { addFocusOverlay, @@ -136,7 +136,7 @@ export async function updateFocussedState({ } const cslp = editableElement?.getAttribute("data-cslp") || ""; - if (!cslp) { + if (!isValidCslp(cslp)) { return; } const fieldMetadata = extractDetailsFromCslp(cslp); From ad42ec1ae9268562dbcfbd4b09f7b36c83cf2768 Mon Sep 17 00:00:00 2001 From: hiteshshetty-dev Date: Tue, 13 Jan 2026 16:26:31 +0530 Subject: [PATCH 2/3] test: mock extractDetailsFromCslp function in useRevalidateFieldDataPostMessageEvent tests --- .../__test__/useRevalidateFieldDataPostMessageEvent.test.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/visualBuilder/eventManager/__test__/useRevalidateFieldDataPostMessageEvent.test.ts b/src/visualBuilder/eventManager/__test__/useRevalidateFieldDataPostMessageEvent.test.ts index 0d41da73..964be326 100644 --- a/src/visualBuilder/eventManager/__test__/useRevalidateFieldDataPostMessageEvent.test.ts +++ b/src/visualBuilder/eventManager/__test__/useRevalidateFieldDataPostMessageEvent.test.ts @@ -2,7 +2,7 @@ import { vi, describe, it, expect, beforeEach, afterEach } from "vitest"; import { VisualBuilder } from "../.."; import { FieldSchemaMap } from "../../utils/fieldSchemaMap"; import { handleRevalidateFieldData } from "../useRevalidateFieldDataPostMessageEvent"; - +import * as cslpdata from "../../../cslp"; // Mock dependencies vi.mock("../../utils/fieldSchemaMap", () => ({ FieldSchemaMap: { @@ -11,9 +11,7 @@ vi.mock("../../utils/fieldSchemaMap", () => ({ }, })); -vi.mock("../../../cslp", () => ({ - extractDetailsFromCslp: vi.fn(), -})); +vi.spyOn(cslpdata, "extractDetailsFromCslp"); vi.mock("../../generators/generateOverlay", () => ({ hideFocusOverlay: vi.fn(), From b1db0fce990b258d1cdf58ce29089b8edc49b7ad Mon Sep 17 00:00:00 2001 From: hiteshshetty-dev Date: Tue, 13 Jan 2026 17:20:26 +0530 Subject: [PATCH 3/3] test: enhance tests by mocking isValidCslp function in variant-related test cases --- .../__test__/useVariantsPostMessageEvent.spec.ts | 7 +++++++ .../generators/__test__/generateOverlay.test.ts | 8 +++----- .../utils/__test__/getVisualBuilderRedirectionUrl.test.ts | 5 ++++- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/visualBuilder/eventManager/__test__/useVariantsPostMessageEvent.spec.ts b/src/visualBuilder/eventManager/__test__/useVariantsPostMessageEvent.spec.ts index 412d1a40..962cf4fb 100644 --- a/src/visualBuilder/eventManager/__test__/useVariantsPostMessageEvent.spec.ts +++ b/src/visualBuilder/eventManager/__test__/useVariantsPostMessageEvent.spec.ts @@ -121,6 +121,9 @@ describe("useVariantFieldsPostMessageEvent", () => { // Reset mocks vi.clearAllMocks(); + + // Mock isValidCslp to return true for test data (after clearAllMocks) + vi.spyOn(cslpdata, "isValidCslp").mockReturnValue(true); }); afterEach(() => { @@ -330,6 +333,9 @@ describe("addVariantFieldClass", () => { // Reset mocks vi.clearAllMocks(); + + // Mock isValidCslp to return true for test data + vi.spyOn(cslpdata, "isValidCslp").mockReturnValue(true); }); afterEach(() => { @@ -391,6 +397,7 @@ describe("addVariantFieldClass", () => { variant: cslpValue.split(":")[1] } }); + const variantUid = "variant-456"; const highlightVariantFields = false; const variantOrder = ["variant-123", "variant-456"]; diff --git a/src/visualBuilder/generators/__test__/generateOverlay.test.ts b/src/visualBuilder/generators/__test__/generateOverlay.test.ts index 02de3274..169ef54f 100644 --- a/src/visualBuilder/generators/__test__/generateOverlay.test.ts +++ b/src/visualBuilder/generators/__test__/generateOverlay.test.ts @@ -4,7 +4,7 @@ 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"; +import * as cslpdata from "../../../cslp/cslpdata"; vi.mock("../../utils/visualBuilderPostMessage", () => ({ default: { @@ -21,9 +21,7 @@ vi.mock("../../utils/fieldSchemaMap", () => ({ }, })); -vi.mock("../../../cslp/cslpdata", () => ({ - extractDetailsFromCslp: vi.fn(), -})); +vi.spyOn(cslpdata, "extractDetailsFromCslp"); describe("sendFieldEvent", () => { let previousSelectedEditableDOM: HTMLElement; @@ -55,7 +53,7 @@ describe("sendFieldEvent", () => { eventType: VisualBuilderPostMessageEvents.UPDATE_FIELD, }); - expect(extractDetailsFromCslp).not.toHaveBeenCalled(); + expect(cslpdata.extractDetailsFromCslp).not.toHaveBeenCalled(); expect(FieldSchemaMap.getFieldSchema).not.toHaveBeenCalled(); expect(visualBuilderPostMessage?.send).not.toHaveBeenCalled(); }); diff --git a/src/visualBuilder/utils/__test__/getVisualBuilderRedirectionUrl.test.ts b/src/visualBuilder/utils/__test__/getVisualBuilderRedirectionUrl.test.ts index c09ec900..8efe0a21 100644 --- a/src/visualBuilder/utils/__test__/getVisualBuilderRedirectionUrl.test.ts +++ b/src/visualBuilder/utils/__test__/getVisualBuilderRedirectionUrl.test.ts @@ -1,7 +1,7 @@ import { describe, it, expect, vi } from 'vitest'; import getVisualBuilderRedirectionUrl from '../getVisualBuilderRedirectionUrl'; import Config from '../../../configManager/configManager'; -import { extractDetailsFromCslp } from '../../../cslp'; +import { extractDetailsFromCslp, isValidCslp } from '../../../cslp'; vi.mock('../../../configManager/configManager'); vi.mock('../../../cslp'); @@ -61,6 +61,7 @@ describe('getVisualBuilderRedirectionUrl', () => { } }); + isValidCslp.mockReturnValue(true); extractDetailsFromCslp.mockReturnValue({ locale: 'fr-FR' }); const result = getVisualBuilderRedirectionUrl(); @@ -99,6 +100,8 @@ describe('getVisualBuilderRedirectionUrl', () => { } }); + isValidCslp.mockReturnValue(false); + 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');