From 8638026165487d3abc70c17e04c32d67b293fd52 Mon Sep 17 00:00:00 2001 From: hiteshshetty-dev Date: Thu, 4 Dec 2025 14:45:48 +0530 Subject: [PATCH 1/9] 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 2/9] 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 3/9] 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 4/9] 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 5/9] 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 6/9] 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 7/9] 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 8/9] 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 9/9] 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 () => {