From 8638026165487d3abc70c17e04c32d67b293fd52 Mon Sep 17 00:00:00 2001 From: hiteshshetty-dev Date: Thu, 4 Dec 2025 14:45:48 +0530 Subject: [PATCH 01/12] refactor: moved outline styles to independent classname --- .../useRecalculateVariantDataCSLPValues.ts | 24 +++++++------------ .../useVariantsPostMessageEvent.ts | 13 +++++----- src/visualBuilder/visualBuilder.style.ts | 3 ++- 3 files changed, 18 insertions(+), 22 deletions(-) diff --git a/src/visualBuilder/eventManager/useRecalculateVariantDataCSLPValues.ts b/src/visualBuilder/eventManager/useRecalculateVariantDataCSLPValues.ts index a577da4a..d1feedbe 100644 --- a/src/visualBuilder/eventManager/useRecalculateVariantDataCSLPValues.ts +++ b/src/visualBuilder/eventManager/useRecalculateVariantDataCSLPValues.ts @@ -46,20 +46,17 @@ function updateVariantClasses({ if (element.classList.contains("visual-builder__base-field")) { element.classList.remove("visual-builder__base-field"); } + const variantFieldClasses = ["visual-builder__variant-field"]; if (highlightVariantFields) { - element.classList.add( - visualBuilderStyles()["visual-builder__variant-field"], - "visual-builder__variant-field" - ); - } else { - element.classList.add("visual-builder__variant-field"); + variantFieldClasses.push(visualBuilderStyles()["visual-builder__variant-field-outline"]); } + element.classList.add(...variantFieldClasses); } else if ( !dataCslp.startsWith("v2:") && element.classList.contains("visual-builder__variant-field") ) { element.classList.remove( - visualBuilderStyles()["visual-builder__variant-field"], + visualBuilderStyles()["visual-builder__variant-field-outline"], "visual-builder__variant-field" ); element.classList.add("visual-builder__base-field"); @@ -70,7 +67,7 @@ function updateVariantClasses({ element.classList.contains("visual-builder__variant-field") ) { element.classList.remove( - visualBuilderStyles()["visual-builder__variant-field"], + visualBuilderStyles()["visual-builder__variant-field-outline"], "visual-builder__variant-field" ); element.classList.add("visual-builder__disabled-variant-field"); @@ -111,18 +108,15 @@ function updateVariantClasses({ if (element.classList.contains("visual-builder__base-field")) { element.classList.remove("visual-builder__base-field"); } + const variantFieldClasses = ["visual-builder__variant-field"]; if (highlightVariantFields) { - element.classList.add( - visualBuilderStyles()["visual-builder__variant-field"], - "visual-builder__variant-field" - ); - } else { - element.classList.add("visual-builder__variant-field"); + variantFieldClasses.push(visualBuilderStyles()["visual-builder__variant-field-outline"]); } + element.classList.add(...variantFieldClasses); } else if (!dataCslp.startsWith("v2:")) { if (element.classList.contains("visual-builder__variant-field")) { element.classList.remove( - visualBuilderStyles()["visual-builder__variant-field"], + visualBuilderStyles()["visual-builder__variant-field-outline"], "visual-builder__variant-field" ); } diff --git a/src/visualBuilder/eventManager/useVariantsPostMessageEvent.ts b/src/visualBuilder/eventManager/useVariantsPostMessageEvent.ts index f3e543ac..1746dc01 100644 --- a/src/visualBuilder/eventManager/useVariantsPostMessageEvent.ts +++ b/src/visualBuilder/eventManager/useVariantsPostMessageEvent.ts @@ -44,11 +44,12 @@ export function addVariantFieldClass( if (!dataCslp) return; if (dataCslp?.includes(variant_uid)) { - highlightVariantFields && + element.classList.add("visual-builder__variant-field"); + if (highlightVariantFields) { element.classList.add( - visualBuilderStyles()["visual-builder__variant-field"] + visualBuilderStyles()["visual-builder__variant-field-outline"] ); - element.classList.add("visual-builder__variant-field"); + } } else if (!dataCslp.startsWith("v2:")) { element.classList.add("visual-builder__base-field"); } else { @@ -62,11 +63,11 @@ export function removeVariantFieldClass( ): void { if (onlyHighlighted) { const variantElements = document.querySelectorAll( - `.${visualBuilderStyles()["visual-builder__variant-field"]}` + `.${visualBuilderStyles()["visual-builder__variant-field-outline"]}` ); variantElements.forEach((element) => { element.classList.remove( - visualBuilderStyles()["visual-builder__variant-field"] + visualBuilderStyles()["visual-builder__variant-field-outline"] ); }); } else { @@ -77,7 +78,7 @@ export function removeVariantFieldClass( element.classList.remove( "visual-builder__disabled-variant-field", "visual-builder__variant-field", - visualBuilderStyles()["visual-builder__variant-field"], + visualBuilderStyles()["visual-builder__variant-field-outline"], "visual-builder__base-field" ); }); diff --git a/src/visualBuilder/visualBuilder.style.ts b/src/visualBuilder/visualBuilder.style.ts index 178fcdef..ccbcac60 100644 --- a/src/visualBuilder/visualBuilder.style.ts +++ b/src/visualBuilder/visualBuilder.style.ts @@ -643,7 +643,8 @@ export function visualBuilderStyles() { "visual-builder__draft-field": css` outline: 2px dashed #eb5646; `, - "visual-builder__variant-field": css` + "visual-builder__variant-field": css``, + "visual-builder__variant-field-outline": css` outline: 2px solid #bd59fa; outline-offset: -2px; `, From c78d7a814acb2ca7ba6ced106d4809472704945d Mon Sep 17 00:00:00 2001 From: hiteshshetty-dev Date: Thu, 4 Dec 2025 14:46:55 +0530 Subject: [PATCH 02/12] feat: add highlight variant fields functionality and update related event handling --- .../useRecalculateVariantDataCSLPValues.ts | 11 ++--- .../useVariantsPostMessageEvent.ts | 40 ++++++++++++++----- src/visualBuilder/index.ts | 8 +++- .../utils/types/postMessage.types.ts | 1 + 4 files changed, 45 insertions(+), 15 deletions(-) diff --git a/src/visualBuilder/eventManager/useRecalculateVariantDataCSLPValues.ts b/src/visualBuilder/eventManager/useRecalculateVariantDataCSLPValues.ts index d1feedbe..54142d82 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 { setHighlightVariantFields } from "./useVariantsPostMessageEvent"; const VARIANT_UPDATE_DELAY_MS: Readonly = 8000; @@ -19,15 +20,14 @@ export function useRecalculateVariantDataCSLPValues(): void { LIVE_PREVIEW_POST_MESSAGE_EVENTS.VARIANT_PATCH, (event) => { if (VisualBuilder.VisualBuilderGlobalState.value.audienceMode) { - updateVariantClasses(event.data); + setHighlightVariantFields(event.data.highlightVariantFields); + updateVariantClasses(); } } ); } -function updateVariantClasses({ - highlightVariantFields, - expectedCSLPValues, -}: OnAudienceModeVariantPatchUpdate): void { +export function updateVariantClasses(): void { + const highlightVariantFields = VisualBuilder.VisualBuilderGlobalState.value.highlightVariantFields; const variant = VisualBuilder.VisualBuilderGlobalState.value.variant; const observers: MutationObserver[] = []; @@ -160,6 +160,7 @@ function updateVariantClasses({ }); observers.push(observer); + // TODO: Check if we could add attributeFilter to the observer to only observe the attribute changes for the data-cslp attribute. observer.observe(element, { attributes: true, childList: true, // Observe direct children diff --git a/src/visualBuilder/eventManager/useVariantsPostMessageEvent.ts b/src/visualBuilder/eventManager/useVariantsPostMessageEvent.ts index 1746dc01..e0aefd63 100644 --- a/src/visualBuilder/eventManager/useVariantsPostMessageEvent.ts +++ b/src/visualBuilder/eventManager/useVariantsPostMessageEvent.ts @@ -3,6 +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 { updateVariantClasses } from "./useRecalculateVariantDataCSLPValues"; interface VariantFieldsEvent { data: { @@ -34,10 +35,8 @@ interface LocaleEvent { locale: string; }; } -export function addVariantFieldClass( - variant_uid: string, - highlightVariantFields: boolean -): void { +export function addVariantFieldClass(variant_uid: string): void { + const highlightVariantFields = VisualBuilder.VisualBuilderGlobalState.value.highlightVariantFields; const elements = document.querySelectorAll(`[data-cslp]`); elements.forEach((element) => { const dataCslp = element.getAttribute("data-cslp"); @@ -94,18 +93,43 @@ export function setVariant(uid: string | null): void { export function setLocale(locale: string): void { VisualBuilder.VisualBuilderGlobalState.value.locale = locale; } +export function setHighlightVariantFields(highlight: boolean): void { + VisualBuilder.VisualBuilderGlobalState.value.highlightVariantFields = highlight; +} + +interface GetHighlightVariantFieldsStatusResponse { + highlightVariantFields: boolean; +} +export async function getHighlightVariantFieldsStatus(): Promise { + try { + const result = await visualBuilderPostMessage?.send( + VisualBuilderPostMessageEvents.GET_HIGHLIGHT_VARIANT_FIELDS_STATUS + ); + return result ?? { + highlightVariantFields: false, + }; + } catch (error) { + console.error("Failed to get highlight variant fields status:", error); + return { + highlightVariantFields: false, + }; + } +} export function useVariantFieldsPostMessageEvent(): void { visualBuilderPostMessage?.on( VisualBuilderPostMessageEvents.GET_VARIANT_ID, (event: VariantEvent) => { - setVariant(event.data.variant); + const selectedVariant = event.data.variant; + setVariant(selectedVariant); // clear field schema when variant is changed. // this is required as we cache field schema // which contain a key isUnlinkedVariant. // This key can change when variant is changed, // so clear the field schema cache FieldSchemaMap.clear(); + // recalculate and apply classes + updateVariantClasses(); } ); visualBuilderPostMessage?.on( @@ -123,11 +147,9 @@ export function useVariantFieldsPostMessageEvent(): void { visualBuilderPostMessage?.on( VisualBuilderPostMessageEvents.SHOW_VARIANT_FIELDS, (event: VariantFieldsEvent) => { + setHighlightVariantFields(event.data.variant_data.highlightVariantFields); removeVariantFieldClass(); - addVariantFieldClass( - event.data.variant_data.variant, - event.data.variant_data.highlightVariantFields - ); + addVariantFieldClass(event.data.variant_data.variant); } ); visualBuilderPostMessage?.on( diff --git a/src/visualBuilder/index.ts b/src/visualBuilder/index.ts index 117f505f..bbbdf2eb 100644 --- a/src/visualBuilder/index.ts +++ b/src/visualBuilder/index.ts @@ -26,7 +26,7 @@ import initUI from "./components"; import { useDraftFieldsPostMessageEvent } from "./eventManager/useDraftFieldsPostMessageEvent"; import { useHideFocusOverlayPostMessageEvent } from "./eventManager/useHideFocusOverlayPostMessageEvent"; import { useScrollToField } from "./eventManager/useScrollToField"; -import { useVariantFieldsPostMessageEvent } from "./eventManager/useVariantsPostMessageEvent"; +import { getHighlightVariantFieldsStatus, setHighlightVariantFields, useVariantFieldsPostMessageEvent } from "./eventManager/useVariantsPostMessageEvent"; import { generateEmptyBlocks, removeEmptyBlocks, @@ -66,6 +66,7 @@ interface VisualBuilderGlobalStateImpl { audienceMode: boolean; locale: string; variant: string | null; + highlightVariantFields: boolean; focusElementObserver: MutationObserver | null; referenceParentMap: Record; isFocussed: boolean; @@ -89,6 +90,7 @@ export class VisualBuilder { audienceMode: false, locale: Config.get().stackDetails.masterLocale || "en-us", variant: null, + highlightVariantFields: false, focusElementObserver: null, referenceParentMap: {}, isFocussed: false, @@ -363,6 +365,9 @@ export class VisualBuilder { subtree: true, }); + getHighlightVariantFieldsStatus().then((result) => { + setHighlightVariantFields(result.highlightVariantFields); + }); visualBuilderPostMessage?.on( VisualBuilderPostMessageEvents.GET_ALL_ENTRIES_IN_CURRENT_PAGE, getEntryIdentifiersInCurrentPage @@ -441,6 +446,7 @@ export class VisualBuilder { audienceMode: false, locale: "en-us", variant: null, + highlightVariantFields: false, focusElementObserver: null, referenceParentMap: {}, isFocussed: false, diff --git a/src/visualBuilder/utils/types/postMessage.types.ts b/src/visualBuilder/utils/types/postMessage.types.ts index f14c63f0..79b05292 100644 --- a/src/visualBuilder/utils/types/postMessage.types.ts +++ b/src/visualBuilder/utils/types/postMessage.types.ts @@ -45,6 +45,7 @@ export enum VisualBuilderPostMessageEvents { REMOVE_HIGHLIGHTED_COMMENTS = "remove-highlighted-comments", GET_VARIANT_ID = "get-variant-id", GET_LOCALE = "get-locale", + GET_HIGHLIGHT_VARIANT_FIELDS_STATUS = "get-highlight-variant-fields-status", SEND_VARIANT_AND_LOCALE = "send-variant-and-locale", GET_CONTENT_TYPE_NAME = "get-content-type-name", REFERENCE_MAP = "get-reference-map", From 16d00e0d10037201c599511a98b3bf833398b157 Mon Sep 17 00:00:00 2001 From: hiteshshetty-dev Date: Thu, 4 Dec 2025 17:20:40 +0530 Subject: [PATCH 03/12] feat: implement variant classname addition in MutationObserver for synced highlights --- .../useVariantsPostMessageEvent.spec.ts | 23 +++++++++++++++---- .../useVariantsPostMessageEvent.ts | 21 ++++++++++++++--- src/visualBuilder/index.ts | 7 ++++-- 3 files changed, 42 insertions(+), 9 deletions(-) diff --git a/src/visualBuilder/eventManager/__test__/useVariantsPostMessageEvent.spec.ts b/src/visualBuilder/eventManager/__test__/useVariantsPostMessageEvent.spec.ts index eae0fc12..2f3712f1 100644 --- a/src/visualBuilder/eventManager/__test__/useVariantsPostMessageEvent.spec.ts +++ b/src/visualBuilder/eventManager/__test__/useVariantsPostMessageEvent.spec.ts @@ -14,6 +14,7 @@ import { setAudienceMode, setVariant, setLocale, + setHighlightVariantFields, } from "../../../visualBuilder/eventManager/useVariantsPostMessageEvent"; import { VisualBuilderPostMessageEvents } from "../../../visualBuilder/utils/types/postMessage.types"; import { VisualBuilder } from "../../../visualBuilder"; @@ -51,6 +52,7 @@ vi.mock("../../../visualBuilder", () => { audienceMode: false, variant: null, locale: "en-us", + highlightVariantFields: false, }, }, }, @@ -337,9 +339,9 @@ describe("addVariantFieldClass", () => { it("should add classes to elements correctly based on data-cslp attribute", () => { const variantUid = "variant-123"; - const highlightVariantFields = true; + VisualBuilder.VisualBuilderGlobalState.value.highlightVariantFields = true; - addVariantFieldClass(variantUid, highlightVariantFields); + addVariantFieldClass(variantUid); // Verify querySelectorAll was called with the correct selector expect(mockQuerySelectorAll).toHaveBeenCalledWith("[data-cslp]"); @@ -368,9 +370,9 @@ describe("addVariantFieldClass", () => { it("should not add highlight class when highlightVariantFields is false", () => { const variantUid = "variant-123"; - const highlightVariantFields = false; + VisualBuilder.VisualBuilderGlobalState.value.highlightVariantFields = false; - addVariantFieldClass(variantUid, highlightVariantFields); + addVariantFieldClass(variantUid); // First element has the variant ID but should not get highlight class expect(mockElements[0].getAttribute).toHaveBeenCalledWith("data-cslp"); @@ -454,6 +456,7 @@ describe("State Management Functions", () => { VisualBuilder.VisualBuilderGlobalState.value.audienceMode = false; VisualBuilder.VisualBuilderGlobalState.value.variant = null; VisualBuilder.VisualBuilderGlobalState.value.locale = "en-us"; + VisualBuilder.VisualBuilderGlobalState.value.highlightVariantFields = false; }); it("setAudienceMode should update global state", () => { @@ -489,4 +492,16 @@ describe("State Management Functions", () => { "en-us" ); }); + + it("setHighlightVariantFields should update global state", () => { + setHighlightVariantFields(true); + expect(VisualBuilder.VisualBuilderGlobalState.value.highlightVariantFields).toBe( + true + ); + + setHighlightVariantFields(false); + expect(VisualBuilder.VisualBuilderGlobalState.value.highlightVariantFields).toBe( + false + ); + }); }); diff --git a/src/visualBuilder/eventManager/useVariantsPostMessageEvent.ts b/src/visualBuilder/eventManager/useVariantsPostMessageEvent.ts index e0aefd63..9402d25f 100644 --- a/src/visualBuilder/eventManager/useVariantsPostMessageEvent.ts +++ b/src/visualBuilder/eventManager/useVariantsPostMessageEvent.ts @@ -4,6 +4,7 @@ import visualBuilderPostMessage from "../utils/visualBuilderPostMessage"; import { VisualBuilderPostMessageEvents } from "../utils/types/postMessage.types"; import { FieldSchemaMap } from "../utils/fieldSchemaMap"; import { updateVariantClasses } from "./useRecalculateVariantDataCSLPValues"; +import { debounce } from "lodash-es"; interface VariantFieldsEvent { data: { @@ -57,6 +58,14 @@ export function addVariantFieldClass(variant_uid: string): void { }); } +export const debounceAddVariantFieldClass = debounce( + (variant_uid: string): void => { + addVariantFieldClass(variant_uid); + }, + 1000, + { trailing: true } +) as (variant_uid: string) => void; + export function removeVariantFieldClass( onlyHighlighted: boolean = false ): void { @@ -116,7 +125,7 @@ export async function getHighlightVariantFieldsStatus(): Promise { @@ -128,8 +137,14 @@ export function useVariantFieldsPostMessageEvent(): void { // This key can change when variant is changed, // so clear the field schema cache FieldSchemaMap.clear(); - // recalculate and apply classes - updateVariantClasses(); + if(isSSR) { + if(selectedVariant) { + addVariantFieldClass(selectedVariant); + } + } else { + // recalculate and apply classes + updateVariantClasses(); + } } ); visualBuilderPostMessage?.on( diff --git a/src/visualBuilder/index.ts b/src/visualBuilder/index.ts index bbbdf2eb..111ceb81 100644 --- a/src/visualBuilder/index.ts +++ b/src/visualBuilder/index.ts @@ -26,7 +26,7 @@ import initUI from "./components"; import { useDraftFieldsPostMessageEvent } from "./eventManager/useDraftFieldsPostMessageEvent"; import { useHideFocusOverlayPostMessageEvent } from "./eventManager/useHideFocusOverlayPostMessageEvent"; import { useScrollToField } from "./eventManager/useScrollToField"; -import { getHighlightVariantFieldsStatus, setHighlightVariantFields, useVariantFieldsPostMessageEvent } from "./eventManager/useVariantsPostMessageEvent"; +import { debounceAddVariantFieldClass, getHighlightVariantFieldsStatus, setHighlightVariantFields, useVariantFieldsPostMessageEvent } from "./eventManager/useVariantsPostMessageEvent"; import { generateEmptyBlocks, removeEmptyBlocks, @@ -240,6 +240,9 @@ export class VisualBuilder { previousEmptyBlockParents: emptyBlockParents, }; } + if(VisualBuilder.VisualBuilderGlobalState.value.variant && VisualBuilder.VisualBuilderGlobalState.value.highlightVariantFields) { + debounceAddVariantFieldClass(VisualBuilder.VisualBuilderGlobalState.value.variant); + } }, 100, { trailing: true } @@ -403,7 +406,7 @@ export class VisualBuilder { useOnEntryUpdatePostMessageEvent(); useRecalculateVariantDataCSLPValues(); useDraftFieldsPostMessageEvent(); - useVariantFieldsPostMessageEvent(); + useVariantFieldsPostMessageEvent({ isSSR: config.ssr ?? false }); } }) .catch(() => { From 86850965a9630de2bab17a913991f54cc2e6f419 Mon Sep 17 00:00:00 2001 From: hiteshshetty-dev Date: Thu, 4 Dec 2025 17:41:38 +0530 Subject: [PATCH 04/12] style: format conditional statements for improved readability --- src/visualBuilder/eventManager/useVariantsPostMessageEvent.ts | 4 ++-- src/visualBuilder/index.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/visualBuilder/eventManager/useVariantsPostMessageEvent.ts b/src/visualBuilder/eventManager/useVariantsPostMessageEvent.ts index 9402d25f..92bd9402 100644 --- a/src/visualBuilder/eventManager/useVariantsPostMessageEvent.ts +++ b/src/visualBuilder/eventManager/useVariantsPostMessageEvent.ts @@ -137,8 +137,8 @@ export function useVariantFieldsPostMessageEvent({ isSSR }: { isSSR: boolean }): // This key can change when variant is changed, // so clear the field schema cache FieldSchemaMap.clear(); - if(isSSR) { - if(selectedVariant) { + if (isSSR) { + if (selectedVariant) { addVariantFieldClass(selectedVariant); } } else { diff --git a/src/visualBuilder/index.ts b/src/visualBuilder/index.ts index 111ceb81..1cc99a1f 100644 --- a/src/visualBuilder/index.ts +++ b/src/visualBuilder/index.ts @@ -240,7 +240,7 @@ export class VisualBuilder { previousEmptyBlockParents: emptyBlockParents, }; } - if(VisualBuilder.VisualBuilderGlobalState.value.variant && VisualBuilder.VisualBuilderGlobalState.value.highlightVariantFields) { + if (VisualBuilder.VisualBuilderGlobalState.value.variant && VisualBuilder.VisualBuilderGlobalState.value.highlightVariantFields) { debounceAddVariantFieldClass(VisualBuilder.VisualBuilderGlobalState.value.variant); } }, From 32d8d74af925a973105ee7f471bb09584ee58d9e Mon Sep 17 00:00:00 2001 From: hiteshshetty-dev Date: Thu, 4 Dec 2025 18:26:21 +0530 Subject: [PATCH 05/12] test: enhance useVariantsPostMessageEvent tests with SSR handling and add new utility tests --- .../useVariantsPostMessageEvent.spec.ts | 205 ++++++++++++++++-- 1 file changed, 183 insertions(+), 22 deletions(-) diff --git a/src/visualBuilder/eventManager/__test__/useVariantsPostMessageEvent.spec.ts b/src/visualBuilder/eventManager/__test__/useVariantsPostMessageEvent.spec.ts index 2f3712f1..09a67ec0 100644 --- a/src/visualBuilder/eventManager/__test__/useVariantsPostMessageEvent.spec.ts +++ b/src/visualBuilder/eventManager/__test__/useVariantsPostMessageEvent.spec.ts @@ -15,6 +15,8 @@ import { setVariant, setLocale, setHighlightVariantFields, + getHighlightVariantFieldsStatus, + debounceAddVariantFieldClass, } from "../../../visualBuilder/eventManager/useVariantsPostMessageEvent"; import { VisualBuilderPostMessageEvents } from "../../../visualBuilder/utils/types/postMessage.types"; import { VisualBuilder } from "../../../visualBuilder"; @@ -22,6 +24,7 @@ import { FieldSchemaMap } from "../../../visualBuilder/utils/fieldSchemaMap"; import { visualBuilderStyles } from "../../../visualBuilder/visualBuilder.style"; import visualBuilderPostMessage from "../../../visualBuilder/utils/visualBuilderPostMessage"; import { EventManager } from "@contentstack/advanced-post-message"; +import { updateVariantClasses } from "../../../visualBuilder/eventManager/useRecalculateVariantDataCSLPValues"; const mockVisualBuilderPostMessage = visualBuilderPostMessage as MockedObject; @@ -44,6 +47,12 @@ vi.mock("../../../visualBuilder/utils/fieldSchemaMap", () => { }; }); +vi.mock("../../../visualBuilder/eventManager/useRecalculateVariantDataCSLPValues", () => { + return { + updateVariantClasses: vi.fn(), + }; +}); + vi.mock("../../../visualBuilder", () => { return { VisualBuilder: { @@ -61,11 +70,13 @@ vi.mock("../../../visualBuilder", () => { // Create a more realistic mock of the CSS modules const cssClassMock = "go109692693"; // Match the actual generated class name +const cssOutlineClassMock = "go109692694"; -vi.mock("../../../visualBuilder.style", () => { +vi.mock("../../visualBuilder.style", () => { return { visualBuilderStyles: () => ({ "visual-builder__variant-field": cssClassMock, + "visual-builder__variant-field-outline": cssOutlineClassMock, }), }; }); @@ -99,7 +110,7 @@ const mockQuerySelectorAll = vi.fn().mockImplementation((selector) => { // Return different mocks based on selector if (selector === "[data-cslp]") { return mockElements; - } else if (selector === `.${cssClassMock}`) { + } else if (selector === `.${cssOutlineClassMock}`) { return mockElements; // For onlyHighlighted=true case } else if ( selector === @@ -131,7 +142,7 @@ describe("useVariantFieldsPostMessageEvent", () => { it("should register all event listeners", () => { // Call the function - useVariantFieldsPostMessageEvent(); + useVariantFieldsPostMessageEvent({ isSSR: false }); // Verify event listeners are registered expect(mockVisualBuilderPostMessage.on).toHaveBeenCalledWith( @@ -162,7 +173,7 @@ describe("useVariantFieldsPostMessageEvent", () => { it("should handle GET_VARIANT_ID event", () => { // Register event handlers - useVariantFieldsPostMessageEvent(); + useVariantFieldsPostMessageEvent({ isSSR: false }); // Extract the event handler function const call = mockVisualBuilderPostMessage.on.mock.calls.find( @@ -186,7 +197,7 @@ describe("useVariantFieldsPostMessageEvent", () => { it("should handle GET_LOCALE event", () => { // Register event handlers - useVariantFieldsPostMessageEvent(); + useVariantFieldsPostMessageEvent({ isSSR: false }); // Extract the event handler function const call = mockVisualBuilderPostMessage.on.mock.calls.find( @@ -207,7 +218,7 @@ describe("useVariantFieldsPostMessageEvent", () => { it("should handle SET_AUDIENCE_MODE event", () => { // Register event handlers - useVariantFieldsPostMessageEvent(); + useVariantFieldsPostMessageEvent({ isSSR: false }); // Extract the event handler function const call = mockVisualBuilderPostMessage.on.mock.calls.find( @@ -228,7 +239,7 @@ describe("useVariantFieldsPostMessageEvent", () => { it("should handle SHOW_VARIANT_FIELDS event", () => { // Register event handlers - useVariantFieldsPostMessageEvent(); + useVariantFieldsPostMessageEvent({ isSSR: false }); // Extract the event handler function const call = mockVisualBuilderPostMessage.on.mock.calls.find( @@ -253,16 +264,16 @@ describe("useVariantFieldsPostMessageEvent", () => { // Verify that classes were added to elements correctly expect(mockElements[0].classList.add).toHaveBeenCalledWith( - visualBuilderStyles()["visual-builder__variant-field"] + "visual-builder__variant-field" ); expect(mockElements[0].classList.add).toHaveBeenCalledWith( - "visual-builder__variant-field" + visualBuilderStyles()["visual-builder__variant-field-outline"] ); }); it("should handle REMOVE_VARIANT_FIELDS event with onlyHighlighted=true", () => { // Register event handlers - useVariantFieldsPostMessageEvent(); + useVariantFieldsPostMessageEvent({ isSSR: false }); // Extract the event handler function const call = mockVisualBuilderPostMessage.on.mock.calls.find( @@ -277,20 +288,20 @@ describe("useVariantFieldsPostMessageEvent", () => { // Verify querySelectorAll was called with the correct selector expect(mockQuerySelectorAll).toHaveBeenCalledWith( - `.${visualBuilderStyles()["visual-builder__variant-field"]}` + `.${visualBuilderStyles()["visual-builder__variant-field-outline"]}` ); // Verify that classes were removed from elements correctly mockElements.forEach((element) => { expect(element.classList.remove).toHaveBeenCalledWith( - visualBuilderStyles()["visual-builder__variant-field"] + visualBuilderStyles()["visual-builder__variant-field-outline"] ); }); }); it("should handle REMOVE_VARIANT_FIELDS event with onlyHighlighted=false", () => { // Register event handlers - useVariantFieldsPostMessageEvent(); + useVariantFieldsPostMessageEvent({ isSSR: false }); // Extract the event handler function const call = mockVisualBuilderPostMessage.on.mock.calls.find( @@ -313,7 +324,7 @@ describe("useVariantFieldsPostMessageEvent", () => { expect(element.classList.remove).toHaveBeenCalledWith( "visual-builder__disabled-variant-field", "visual-builder__variant-field", - visualBuilderStyles()["visual-builder__variant-field"], + visualBuilderStyles()["visual-builder__variant-field-outline"], "visual-builder__base-field" ); }); @@ -325,6 +336,13 @@ describe("addVariantFieldClass", () => { const originalQuerySelectorAll = document.querySelectorAll; beforeEach(() => { + // // Reset element mocks to track new calls + // mockElements.forEach((element) => { + // element.classList.add.mockClear(); + // element.classList.remove.mockClear(); + // element.getAttribute.mockClear(); + // }); + // Mock document.querySelectorAll document.querySelectorAll = mockQuerySelectorAll; @@ -349,10 +367,10 @@ describe("addVariantFieldClass", () => { // First element has the variant ID expect(mockElements[0].getAttribute).toHaveBeenCalledWith("data-cslp"); expect(mockElements[0].classList.add).toHaveBeenCalledWith( - visualBuilderStyles()["visual-builder__variant-field"] + "visual-builder__variant-field" ); expect(mockElements[0].classList.add).toHaveBeenCalledWith( - "visual-builder__variant-field" + visualBuilderStyles()["visual-builder__variant-field-outline"] ); // Second element does not start with 'v2:' @@ -376,12 +394,12 @@ describe("addVariantFieldClass", () => { // First element has the variant ID but should not get highlight class expect(mockElements[0].getAttribute).toHaveBeenCalledWith("data-cslp"); - expect(mockElements[0].classList.add).not.toHaveBeenCalledWith( - visualBuilderStyles()["visual-builder__variant-field"] - ); expect(mockElements[0].classList.add).toHaveBeenCalledWith( "visual-builder__variant-field" ); + expect(mockElements[0].classList.add).not.toHaveBeenCalledWith( + visualBuilderStyles()["visual-builder__variant-field-outline"] + ); }); }); @@ -407,13 +425,13 @@ describe("removeVariantFieldClass", () => { // Verify querySelectorAll was called with the correct selector expect(mockQuerySelectorAll).toHaveBeenCalledWith( - `.${visualBuilderStyles()["visual-builder__variant-field"]}` + `.${visualBuilderStyles()["visual-builder__variant-field-outline"]}` ); // Verify classes were removed mockElements.forEach((element) => { expect(element.classList.remove).toHaveBeenCalledWith( - visualBuilderStyles()["visual-builder__variant-field"] + visualBuilderStyles()["visual-builder__variant-field-outline"] ); }); }); @@ -431,7 +449,7 @@ describe("removeVariantFieldClass", () => { expect(element.classList.remove).toHaveBeenCalledWith( "visual-builder__disabled-variant-field", "visual-builder__variant-field", - visualBuilderStyles()["visual-builder__variant-field"], + visualBuilderStyles()["visual-builder__variant-field-outline"], "visual-builder__base-field" ); }); @@ -505,3 +523,146 @@ describe("State Management Functions", () => { ); }); }); + +describe("getHighlightVariantFieldsStatus", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("should return highlight status when successful", async () => { + const mockResponse = { highlightVariantFields: true }; + (mockVisualBuilderPostMessage.send as any).mockResolvedValue(mockResponse); + + const result = await getHighlightVariantFieldsStatus(); + + expect(mockVisualBuilderPostMessage.send).toHaveBeenCalledWith( + VisualBuilderPostMessageEvents.GET_HIGHLIGHT_VARIANT_FIELDS_STATUS + ); + expect(result).toEqual(mockResponse); + }); + + it("should return default false when response is null", async () => { + (mockVisualBuilderPostMessage.send as any).mockResolvedValue(null); + + const result = await getHighlightVariantFieldsStatus(); + + expect(result).toEqual({ highlightVariantFields: false }); + }); + + it("should return default false when request fails", async () => { + (mockVisualBuilderPostMessage.send as any).mockRejectedValue( + new Error("Network error") + ); + + const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); + + const result = await getHighlightVariantFieldsStatus(); + + expect(result).toEqual({ highlightVariantFields: false }); + expect(consoleErrorSpy).toHaveBeenCalledWith( + "Failed to get highlight variant fields status:", + expect.any(Error) + ); + + consoleErrorSpy.mockRestore(); + }); +}); + +describe("debounceAddVariantFieldClass", () => { + const originalQuerySelectorAll = document.querySelectorAll; + + beforeEach(() => { + document.querySelectorAll = mockQuerySelectorAll; + vi.clearAllMocks(); + vi.useFakeTimers(); + }); + + afterEach(() => { + document.querySelectorAll = originalQuerySelectorAll; + vi.useRealTimers(); + }); + + it("should debounce addVariantFieldClass calls", () => { + const variantUid = "variant-123"; + VisualBuilder.VisualBuilderGlobalState.value.highlightVariantFields = true; + + // Call multiple times rapidly + debounceAddVariantFieldClass(variantUid); + debounceAddVariantFieldClass(variantUid); + debounceAddVariantFieldClass(variantUid); + + // Should not have been called yet (debounced) + expect(mockQuerySelectorAll).not.toHaveBeenCalled(); + + // Fast-forward time + vi.advanceTimersByTime(1000); + + // Should have been called once (debounced) + expect(mockQuerySelectorAll).toHaveBeenCalledTimes(1); + expect(mockQuerySelectorAll).toHaveBeenCalledWith("[data-cslp]"); + }); +}); + +describe("useVariantFieldsPostMessageEvent SSR handling", () => { + const originalQuerySelectorAll = document.querySelectorAll; + + beforeEach(() => { + document.querySelectorAll = mockQuerySelectorAll; + vi.clearAllMocks(); + }); + + afterEach(() => { + document.querySelectorAll = originalQuerySelectorAll; + }); + + it("should call addVariantFieldClass directly when isSSR is true and variant is provided", () => { + useVariantFieldsPostMessageEvent({ isSSR: true }); + + const call = mockVisualBuilderPostMessage.on.mock.calls.find( + (call: any[]) => + call[0] === VisualBuilderPostMessageEvents.GET_VARIANT_ID + ); + const handler = call ? call[1] : null; + + vi.clearAllMocks(); + handler!({ data: { variant: "variant-123" } }); + + // Should call addVariantFieldClass directly (not updateVariantClasses) + expect(mockQuerySelectorAll).toHaveBeenCalledWith("[data-cslp]"); + expect(updateVariantClasses).not.toHaveBeenCalled(); + }); + + it("should call updateVariantClasses when isSSR is false", () => { + useVariantFieldsPostMessageEvent({ isSSR: false }); + + const call = mockVisualBuilderPostMessage.on.mock.calls.find( + (call: any[]) => + call[0] === VisualBuilderPostMessageEvents.GET_VARIANT_ID + ); + const handler = call ? call[1] : null; + + vi.clearAllMocks(); + handler!({ data: { variant: "variant-123" } }); + + // Should call updateVariantClasses (not addVariantFieldClass directly) + expect(updateVariantClasses).toHaveBeenCalled(); + expect(mockQuerySelectorAll).not.toHaveBeenCalled(); + }); + + it("should not call addVariantFieldClass when isSSR is true but variant is null", () => { + useVariantFieldsPostMessageEvent({ isSSR: true }); + + const call = mockVisualBuilderPostMessage.on.mock.calls.find( + (call: any[]) => + call[0] === VisualBuilderPostMessageEvents.GET_VARIANT_ID + ); + const handler = call ? call[1] : null; + + vi.clearAllMocks(); + handler!({ data: { variant: null } }); + + // Should not call addVariantFieldClass when variant is null + expect(mockQuerySelectorAll).not.toHaveBeenCalled(); + expect(updateVariantClasses).not.toHaveBeenCalled(); + }); +}); From 0420e0fbedcb902cf15246d55cea14e0b9eca6ff Mon Sep 17 00:00:00 2001 From: hiteshshetty-dev Date: Thu, 4 Dec 2025 19:11:19 +0530 Subject: [PATCH 06/12] fix: ensure variantOrder defaults to an empty array if not provided in useVariantsPostMessageEvent --- src/visualBuilder/eventManager/useVariantsPostMessageEvent.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/visualBuilder/eventManager/useVariantsPostMessageEvent.ts b/src/visualBuilder/eventManager/useVariantsPostMessageEvent.ts index 64c94ee2..4b79ae8f 100644 --- a/src/visualBuilder/eventManager/useVariantsPostMessageEvent.ts +++ b/src/visualBuilder/eventManager/useVariantsPostMessageEvent.ts @@ -193,7 +193,7 @@ export function useVariantFieldsPostMessageEvent({ isSSR }: { isSSR: boolean }): VisualBuilderPostMessageEvents.SHOW_VARIANT_FIELDS, (event: VariantFieldsEvent) => { setHighlightVariantFields(event.data.variant_data.highlightVariantFields); - setVariantOrder(event.data.variant_data.variantOrder); + setVariantOrder(event.data.variant_data.variantOrder || []); removeVariantFieldClass(); addVariantFieldClass( event.data.variant_data.variant, From 122dd2c23a27d45aa8410657d4e6478299c3ec9e Mon Sep 17 00:00:00 2001 From: hiteshshetty-dev Date: Thu, 4 Dec 2025 19:47:49 +0530 Subject: [PATCH 07/12] refactor: change variable declaration from let to const for tooltip in editButtonAction tests --- src/livePreview/editButton/__test__/editButtonAction.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/livePreview/editButton/__test__/editButtonAction.test.ts b/src/livePreview/editButton/__test__/editButtonAction.test.ts index 67b388cd..db2acca8 100644 --- a/src/livePreview/editButton/__test__/editButtonAction.test.ts +++ b/src/livePreview/editButton/__test__/editButtonAction.test.ts @@ -598,7 +598,7 @@ describe("cslp tooltip", () => { }); new LivePreview(); - let tooltip = document.querySelector( + const tooltip = document.querySelector( "[data-test-id='cs-cslp-tooltip']" ); const tooltipParent = tooltip?.parentNode; From ef5c9649c751fabd123c47446ca2289328fe2f85 Mon Sep 17 00:00:00 2001 From: hiteshshetty-dev Date: Fri, 5 Dec 2025 10:13:41 +0530 Subject: [PATCH 08/12] refactor: remove commented-out code in useVariantsPostMessageEvent tests for clarity --- .../__test__/useVariantsPostMessageEvent.spec.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/visualBuilder/eventManager/__test__/useVariantsPostMessageEvent.spec.ts b/src/visualBuilder/eventManager/__test__/useVariantsPostMessageEvent.spec.ts index 0066fcb5..d4b4b7e0 100644 --- a/src/visualBuilder/eventManager/__test__/useVariantsPostMessageEvent.spec.ts +++ b/src/visualBuilder/eventManager/__test__/useVariantsPostMessageEvent.spec.ts @@ -338,13 +338,6 @@ describe("addVariantFieldClass", () => { const originalQuerySelectorAll = document.querySelectorAll; beforeEach(() => { - // // Reset element mocks to track new calls - // mockElements.forEach((element) => { - // element.classList.add.mockClear(); - // element.classList.remove.mockClear(); - // element.getAttribute.mockClear(); - // }); - // Mock document.querySelectorAll document.querySelectorAll = mockQuerySelectorAll; From 76f34f6ad50d12a19a0951929ce8facae0eaaa6b Mon Sep 17 00:00:00 2001 From: hiteshshetty-dev Date: Fri, 5 Dec 2025 10:22:16 +0530 Subject: [PATCH 09/12] test: update live preview HOC tests to verify additional postMessage calls and their counts --- src/preview/__test__/contentstack-live-preview-HOC.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/preview/__test__/contentstack-live-preview-HOC.test.ts b/src/preview/__test__/contentstack-live-preview-HOC.test.ts index 5c765332..fdb314e2 100644 --- a/src/preview/__test__/contentstack-live-preview-HOC.test.ts +++ b/src/preview/__test__/contentstack-live-preview-HOC.test.ts @@ -105,7 +105,8 @@ describe("Live Preview HOC init", () => { expect(livePreviewPostMessageSpy).toHaveBeenCalledTimes(1); expect(visualBuilderPostMessageSpy).toHaveBeenCalledWith('init', { isSSR: true, href: 'http://localhost:3000/' }); expect(visualBuilderPostMessageSpy).toHaveBeenCalledWith('send-variant-and-locale'); - expect(visualBuilderPostMessageSpy).toHaveBeenCalledTimes(2); + expect(visualBuilderPostMessageSpy).toHaveBeenCalledWith('get-highlight-variant-fields-status'); + expect(visualBuilderPostMessageSpy).toHaveBeenCalledTimes(3); }); test("should return the existing live preview instance if it is already initialized", async () => { From 4a55bad9761775f0ad0b66afd22abefa9f2feda9 Mon Sep 17 00:00:00 2001 From: hiteshshetty-dev Date: Fri, 2 Jan 2026 13:21:28 +0530 Subject: [PATCH 10/12] refactor: simplify debounce function by directly using addVariantFieldClass --- .../useVariantsPostMessageEvent.spec.ts | 55 +++++++------------ .../useVariantsPostMessageEvent.ts | 4 +- 2 files changed, 21 insertions(+), 38 deletions(-) diff --git a/src/visualBuilder/eventManager/__test__/useVariantsPostMessageEvent.spec.ts b/src/visualBuilder/eventManager/__test__/useVariantsPostMessageEvent.spec.ts index 09a67ec0..7ed01eb4 100644 --- a/src/visualBuilder/eventManager/__test__/useVariantsPostMessageEvent.spec.ts +++ b/src/visualBuilder/eventManager/__test__/useVariantsPostMessageEvent.spec.ts @@ -7,6 +7,20 @@ import { afterEach, MockedObject, } from "vitest"; + +const { debounce } = vi.hoisted(() => { + return { + debounce: vi.fn((fn: any, _delay: number, _options?: any) => { + return fn; + }), + }; +}); +vi.mock("lodash-es", () => { + return { + ...vi.importActual("lodash-es"), + debounce: debounce, + }; +}); import { useVariantFieldsPostMessageEvent, addVariantFieldClass, @@ -16,7 +30,6 @@ import { setLocale, setHighlightVariantFields, getHighlightVariantFieldsStatus, - debounceAddVariantFieldClass, } from "../../../visualBuilder/eventManager/useVariantsPostMessageEvent"; import { VisualBuilderPostMessageEvents } from "../../../visualBuilder/utils/types/postMessage.types"; import { VisualBuilder } from "../../../visualBuilder"; @@ -123,6 +136,12 @@ const mockQuerySelectorAll = vi.fn().mockImplementation((selector) => { return []; }); +describe("debounceAddVariantFieldClass", () => { + // Moved to the top of the file to ensure it is mocks are not cleared before the test is run + it("should debounce addVariantFieldClass calls", () => { + expect(debounce).toHaveBeenCalledWith(addVariantFieldClass, 1000, { trailing: true }); + }); +}); describe("useVariantFieldsPostMessageEvent", () => { // Store original document.querySelectorAll const originalQuerySelectorAll = document.querySelectorAll; @@ -568,40 +587,6 @@ describe("getHighlightVariantFieldsStatus", () => { }); }); -describe("debounceAddVariantFieldClass", () => { - const originalQuerySelectorAll = document.querySelectorAll; - - beforeEach(() => { - document.querySelectorAll = mockQuerySelectorAll; - vi.clearAllMocks(); - vi.useFakeTimers(); - }); - - afterEach(() => { - document.querySelectorAll = originalQuerySelectorAll; - vi.useRealTimers(); - }); - - it("should debounce addVariantFieldClass calls", () => { - const variantUid = "variant-123"; - VisualBuilder.VisualBuilderGlobalState.value.highlightVariantFields = true; - - // Call multiple times rapidly - debounceAddVariantFieldClass(variantUid); - debounceAddVariantFieldClass(variantUid); - debounceAddVariantFieldClass(variantUid); - - // Should not have been called yet (debounced) - expect(mockQuerySelectorAll).not.toHaveBeenCalled(); - - // Fast-forward time - vi.advanceTimersByTime(1000); - - // Should have been called once (debounced) - expect(mockQuerySelectorAll).toHaveBeenCalledTimes(1); - expect(mockQuerySelectorAll).toHaveBeenCalledWith("[data-cslp]"); - }); -}); describe("useVariantFieldsPostMessageEvent SSR handling", () => { const originalQuerySelectorAll = document.querySelectorAll; diff --git a/src/visualBuilder/eventManager/useVariantsPostMessageEvent.ts b/src/visualBuilder/eventManager/useVariantsPostMessageEvent.ts index 92bd9402..f22dacd7 100644 --- a/src/visualBuilder/eventManager/useVariantsPostMessageEvent.ts +++ b/src/visualBuilder/eventManager/useVariantsPostMessageEvent.ts @@ -59,9 +59,7 @@ export function addVariantFieldClass(variant_uid: string): void { } export const debounceAddVariantFieldClass = debounce( - (variant_uid: string): void => { - addVariantFieldClass(variant_uid); - }, + addVariantFieldClass, 1000, { trailing: true } ) as (variant_uid: string) => void; From 20ff3b8d56566288a03105c2e5565910d64724ef Mon Sep 17 00:00:00 2001 From: hiteshshetty-dev Date: Fri, 2 Jan 2026 14:55:42 +0530 Subject: [PATCH 11/12] fix: ensure highlight variant fields are disabled when removing variant --- src/visualBuilder/eventManager/useVariantsPostMessageEvent.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/visualBuilder/eventManager/useVariantsPostMessageEvent.ts b/src/visualBuilder/eventManager/useVariantsPostMessageEvent.ts index 3423ba2a..382a6976 100644 --- a/src/visualBuilder/eventManager/useVariantsPostMessageEvent.ts +++ b/src/visualBuilder/eventManager/useVariantsPostMessageEvent.ts @@ -199,6 +199,7 @@ export function useVariantFieldsPostMessageEvent({ isSSR }: { isSSR: boolean }): visualBuilderPostMessage?.on( VisualBuilderPostMessageEvents.REMOVE_VARIANT_FIELDS, (event: RemoveVariantFieldsEvent) => { + setHighlightVariantFields(false); removeVariantFieldClass(event?.data?.onlyHighlighted); } ); From 05b8105d04d22c4de09836ff63ed3faac6b7bc83 Mon Sep 17 00:00:00 2001 From: hiteshshetty-dev Date: Fri, 2 Jan 2026 15:21:13 +0530 Subject: [PATCH 12/12] chore: update copyright year in README.md to 2026 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c6c35e01..0a95348b 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ ContentstackLivePreview.init({ MIT License -Copyright © 2021-2025 [Contentstack](https://www.contentstack.com/). All Rights Reserved +Copyright © 2021-2026 [Contentstack](https://www.contentstack.com/). All Rights Reserved Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: