From 30975d3e7be66d3bf20c9fccc0e0afc119828609 Mon Sep 17 00:00:00 2001 From: Karan Gandhi Date: Fri, 10 Oct 2025 15:14:28 +0530 Subject: [PATCH 01/19] feat: adding post messages to link unlinked ct to variant group --- .../components/fieldLabelWrapper.tsx | 22 ++++- .../useRevalidateFieldDataPostMessageEvent.ts | 98 +++++++++++++++++++ src/visualBuilder/index.ts | 27 +++-- src/visualBuilder/utils/fieldSchemaMap.ts | 9 ++ src/visualBuilder/utils/isFieldDisabled.ts | 19 +++- src/visualBuilder/utils/types/index.types.ts | 1 + .../utils/types/postMessage.types.ts | 2 + 7 files changed, 155 insertions(+), 23 deletions(-) create mode 100644 src/visualBuilder/eventManager/useRevalidateFieldDataPostMessageEvent.ts diff --git a/src/visualBuilder/components/fieldLabelWrapper.tsx b/src/visualBuilder/components/fieldLabelWrapper.tsx index 2ef77e6e..11caf280 100644 --- a/src/visualBuilder/components/fieldLabelWrapper.tsx +++ b/src/visualBuilder/components/fieldLabelWrapper.tsx @@ -129,7 +129,7 @@ function FieldLabelWrapperComponent( getReferenceParentMap() ]); const entryUid = props.fieldMetadata.entry_uid; - + const referenceData = referenceParentMap[entryUid]; const isReference = !!referenceData; @@ -189,6 +189,18 @@ function FieldLabelWrapperComponent( ] )} data-tooltip={reason} + onClick={() => { + if (fieldSchema.field_metadata?.canLinkVariant) { + visualBuilderPostMessage?.send( + VisualBuilderPostMessageEvents.OPEN_LINK_VARIANT_MODAL, + { + contentTypeUid: + props.fieldMetadata + .content_type_uid, + } + ); + } + }} > @@ -303,11 +315,11 @@ function FieldLabelWrapperComponent( > { currentField.isReference && !dataLoading && !error ? -
{ + try { + // Get the currently hovered or focused field + const hoveredElement = + VisualBuilder.VisualBuilderGlobalState.value + .previousHoveredTargetDOM; + const focusedElement = + VisualBuilder.VisualBuilderGlobalState.value + .previousSelectedEditableDOM; + + // Prefer hovered element, fallback to focused element + const targetElement = hoveredElement || focusedElement; + + if (targetElement) { + const cslp = targetElement.getAttribute("data-cslp"); + if (cslp) { + const fieldMetadata = extractDetailsFromCslp(cslp); + + // Try to revalidate specific field schema and data + try { + // Clear the entire content type schema from cache to force fresh fetch + FieldSchemaMap.clearContentTypeSchema( + fieldMetadata.content_type_uid + ); + + // Fetch fresh field schema and data + const [fieldSchema, fieldData] = await Promise.all([ + FieldSchemaMap.getFieldSchema( + fieldMetadata.content_type_uid, + fieldMetadata.fieldPath + ), + getFieldData( + { + content_type_uid: + fieldMetadata.content_type_uid, + entry_uid: fieldMetadata.entry_uid, + locale: fieldMetadata.locale, + }, + fieldMetadata.fieldPathWithIndex + ), + ]); + + if (fieldSchema && fieldData) { + console.log( + "Successfully revalidated field data for content type:", + fieldMetadata.content_type_uid + ); + return; + } + } catch (fieldError) { + console.warn( + "Failed to revalidate content type:", + fieldMetadata.content_type_uid, + fieldError + ); + } + } + } + + // Fallback 1: Clear all field schema cache + try { + FieldSchemaMap.clear(); + console.log( + "Cleared all field schema cache due to revalidation request" + ); + return; + } catch (clearError) { + console.warn("Failed to clear field schema cache:", clearError); + } + + // Fallback 2: Refresh the entire iframe + console.log("Refreshing iframe due to failed field data revalidation"); + window.location.reload(); + } catch (error) { + console.error("Error handling revalidate field data:", error); + // Final fallback - refresh the page + window.location.reload(); + } +} + +export function useRevalidateFieldDataPostMessageEvent(): void { + visualBuilderPostMessage?.on( + VisualBuilderPostMessageEvents.REVALIDATE_FIELD_DATA, + handleRevalidateFieldData + ); +} diff --git a/src/visualBuilder/index.ts b/src/visualBuilder/index.ts index 117f505f..2782cff2 100644 --- a/src/visualBuilder/index.ts +++ b/src/visualBuilder/index.ts @@ -25,6 +25,7 @@ import { extractDetailsFromCslp } from "../cslp"; import initUI from "./components"; import { useDraftFieldsPostMessageEvent } from "./eventManager/useDraftFieldsPostMessageEvent"; import { useHideFocusOverlayPostMessageEvent } from "./eventManager/useHideFocusOverlayPostMessageEvent"; +import { useRevalidateFieldDataPostMessageEvent } from "./eventManager/useRevalidateFieldDataPostMessageEvent"; import { useScrollToField } from "./eventManager/useScrollToField"; import { useVariantFieldsPostMessageEvent } from "./eventManager/useVariantsPostMessageEvent"; import { @@ -371,21 +372,18 @@ export class VisualBuilder { VisualBuilderPostMessageEvents.SEND_VARIANT_AND_LOCALE ); - visualBuilderPostMessage?.on<{ - scroll: boolean - }>( - VisualBuilderPostMessageEvents.TOGGLE_SCROLL, - (event) => { - if (!event.data.scroll) { - document.body.style.overflow = 'hidden' - } else { - document.body.style.overflow = 'auto' - } + visualBuilderPostMessage?.on<{ + scroll: boolean; + }>( + VisualBuilderPostMessageEvents.TOGGLE_SCROLL, + (event) => { + if (!event.data.scroll) { + document.body.style.overflow = "hidden"; + } else { + document.body.style.overflow = "auto"; } - ); - - - + } + ); useHideFocusOverlayPostMessageEvent({ overlayWrapper: this.overlayWrapper, @@ -399,6 +397,7 @@ export class VisualBuilder { useRecalculateVariantDataCSLPValues(); useDraftFieldsPostMessageEvent(); useVariantFieldsPostMessageEvent(); + useRevalidateFieldDataPostMessageEvent(); } }) .catch(() => { diff --git a/src/visualBuilder/utils/fieldSchemaMap.ts b/src/visualBuilder/utils/fieldSchemaMap.ts index 4dce5d2e..ee7914db 100644 --- a/src/visualBuilder/utils/fieldSchemaMap.ts +++ b/src/visualBuilder/utils/fieldSchemaMap.ts @@ -90,6 +90,15 @@ export class FieldSchemaMap { FieldSchemaMap.fieldSchema[contentTypeUid] = fieldSchemaMap; } + /** + * Clears the field schemas for a specific content type. + * @param contentTypeUid The unique identifier of the content type. + */ + static clearContentTypeSchema(contentTypeUid: string): void { + delete FieldSchemaMap.fieldSchema[contentTypeUid]; + delete FieldSchemaMap.fieldSchemaPromise[contentTypeUid]; + } + /** * Clears the field schema cache. */ diff --git a/src/visualBuilder/utils/isFieldDisabled.ts b/src/visualBuilder/utils/isFieldDisabled.ts index 05f42b8c..6ce473ba 100644 --- a/src/visualBuilder/utils/isFieldDisabled.ts +++ b/src/visualBuilder/utils/isFieldDisabled.ts @@ -9,8 +9,11 @@ const DisableReason = { ReadOnly: "You have only read access to this field", LocalizedEntry: "Editing this field is restricted in localized entries", UnlinkedVariant: - "This field is not editable as it is not linked to the selected variant", - AudienceMode: "To edit an experience, open the Audience widget and click the Edit icon.", + "This field is not editable as it is not linked to the selected variant.", + canLinkVaraint: "Click here to link a variant", + cannotLinkVariant: "Contact your stack admin or owner to link it.", + AudienceMode: + "To edit an experience, open the Audience widget and click the Edit icon.", DisabledVariant: "This field is not editable as it doesn't match the selected variant", UnlocalizedVariant: "This field is not editable as it is not localized", @@ -40,8 +43,15 @@ const getDisableReason = ( return DisableReason.LocalizedEntry; if (flags.updateRestrictDueToUnlocalizedVariant) return DisableReason.UnlocalizedVariant; - if (flags.updateRestrictDueToUnlinkVariant) - return DisableReason.UnlinkedVariant; + if (flags.updateRestrictDueToUnlinkVariant) { + let reason = DisableReason.UnlinkedVariant; + if (flags.canLinkVariant) { + reason += ` ${DisableReason.canLinkVaraint}`; + } else { + reason += ` ${DisableReason.cannotLinkVariant}`; + } + return reason; + } if (flags.updateRestrictDueToAudienceMode) return DisableReason.AudienceMode; if (flags.updateRestrictDueToDisabledVariant) @@ -83,6 +93,7 @@ export const isFieldDisabled = ( updateRestrictDueToUnlinkVariant: Boolean( fieldSchemaMap?.field_metadata?.isUnlinkedVariant ), + canLinkVariant: Boolean(fieldSchemaMap?.field_metadata?.canLinkVariant), updateRestrictDueToUnlocalizedVariant: Boolean( variant && fieldMetadata.locale !== cmsLocale ), diff --git a/src/visualBuilder/utils/types/index.types.ts b/src/visualBuilder/utils/types/index.types.ts index 21eb7073..20cafb5a 100644 --- a/src/visualBuilder/utils/types/index.types.ts +++ b/src/visualBuilder/utils/types/index.types.ts @@ -17,6 +17,7 @@ export type ISchemaFieldMap = ( field_metadata?: { updateRestrict?: boolean; isUnlinkedVariant?: boolean; + canLinkVariant?: boolean; }; }; diff --git a/src/visualBuilder/utils/types/postMessage.types.ts b/src/visualBuilder/utils/types/postMessage.types.ts index f14c63f0..e401f36b 100644 --- a/src/visualBuilder/utils/types/postMessage.types.ts +++ b/src/visualBuilder/utils/types/postMessage.types.ts @@ -9,6 +9,8 @@ export enum VisualBuilderPostMessageEvents { TOGGLE_FORM = "toggle-quick-form", GET_FIELD_SCHEMA = "get-field-schema", GET_FIELD_DATA = "get-field-data", + REVALIDATE_FIELD_DATA = "revalidate-field-data", + OPEN_LINK_VARIANT_MODAL = "open-link-variant-modal", GET_FIELD_PATH_WITH_UID = "get-field-path-with-uid", GET_FIELD_DISPLAY_NAMES = "get-field-display-names", MOUSE_CLICK = "mouse-click", From 0817211c20e1d5bb0a8cdaf1f6926d3017445dc3 Mon Sep 17 00:00:00 2001 From: Karan Gandhi Date: Fri, 10 Oct 2025 15:18:09 +0530 Subject: [PATCH 02/19] fix: updated console --- .../useRevalidateFieldDataPostMessageEvent.ts | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/visualBuilder/eventManager/useRevalidateFieldDataPostMessageEvent.ts b/src/visualBuilder/eventManager/useRevalidateFieldDataPostMessageEvent.ts index dd5c8132..2727dc3d 100644 --- a/src/visualBuilder/eventManager/useRevalidateFieldDataPostMessageEvent.ts +++ b/src/visualBuilder/eventManager/useRevalidateFieldDataPostMessageEvent.ts @@ -53,10 +53,6 @@ async function handleRevalidateFieldData(): Promise { ]); if (fieldSchema && fieldData) { - console.log( - "Successfully revalidated field data for content type:", - fieldMetadata.content_type_uid - ); return; } } catch (fieldError) { @@ -72,16 +68,12 @@ async function handleRevalidateFieldData(): Promise { // Fallback 1: Clear all field schema cache try { FieldSchemaMap.clear(); - console.log( - "Cleared all field schema cache due to revalidation request" - ); return; } catch (clearError) { - console.warn("Failed to clear field schema cache:", clearError); + console.error("Failed to clear field schema cache:", clearError); } // Fallback 2: Refresh the entire iframe - console.log("Refreshing iframe due to failed field data revalidation"); window.location.reload(); } catch (error) { console.error("Error handling revalidate field data:", error); From 6936fef7d351287bebaddd29c1aff42c929de37a Mon Sep 17 00:00:00 2001 From: Karan Gandhi Date: Mon, 13 Oct 2025 17:06:15 +0530 Subject: [PATCH 03/19] feat: test cases added --- .../__test__/fieldLabelWrapper.test.tsx | 265 ++++++++++++------ ...evalidateFieldDataPostMessageEvent.test.ts | 239 ++++++++++++++++ .../utils/__test__/isFieldDisabled.test.ts | 25 +- 3 files changed, 437 insertions(+), 92 deletions(-) create mode 100644 src/visualBuilder/eventManager/__test__/useRevalidateFieldDataPostMessageEvent.test.ts diff --git a/src/visualBuilder/components/__test__/fieldLabelWrapper.test.tsx b/src/visualBuilder/components/__test__/fieldLabelWrapper.test.tsx index f4ab4738..6849aafc 100644 --- a/src/visualBuilder/components/__test__/fieldLabelWrapper.test.tsx +++ b/src/visualBuilder/components/__test__/fieldLabelWrapper.test.tsx @@ -12,15 +12,15 @@ import React from "preact/compat"; // All mocks vi.mock("../Tooltip", () => ({ ToolbarTooltip: ({ children, data, disabled }: any) => ( -
{children}
- ) + ), })); vi.mock("../../utils/fieldSchemaMap", () => ({ @@ -29,7 +29,7 @@ vi.mock("../../utils/fieldSchemaMap", () => ({ display_name: "Field 0", data_type: "text", field_metadata: {}, - uid: "test_field" + uid: "test_field", }), }, })); @@ -43,30 +43,39 @@ vi.mock("../../utils/visualBuilderPostMessage", () => ({ fields.forEach((field: any) => { if (field.cslpValue === "mockFieldCslp") { result[field.cslpValue] = "Field 0"; - } else if (field.cslpValue === "contentTypeUid.entryUid.locale.parentPath1") { + } else if ( + field.cslpValue === + "contentTypeUid.entryUid.locale.parentPath1" + ) { result[field.cslpValue] = "Field 1"; - } else if (field.cslpValue === "contentTypeUid.entryUid.locale.parentPath2") { + } else if ( + field.cslpValue === + "contentTypeUid.entryUid.locale.parentPath2" + ) { result[field.cslpValue] = "Field 2"; - } else if (field.cslpValue === "contentTypeUid.entryUid.locale.parentPath3") { + } else if ( + field.cslpValue === + "contentTypeUid.entryUid.locale.parentPath3" + ) { result[field.cslpValue] = "Field 3"; } else { result[field.cslpValue] = field.cslpValue; // fallback } }); return Promise.resolve(result); - } else if(eventName === "GET_CONTENT_TYPE_NAME") { + } else if (eventName === "GET_CONTENT_TYPE_NAME") { return Promise.resolve({ contentTypeName: "Page CT", }); - } else if(eventName === "REFERENCE_MAP") { + } else if (eventName === "REFERENCE_MAP") { return Promise.resolve({ - "mockEntryUid": [ + mockEntryUid: [ { contentTypeUid: "mockContentTypeUid", contentTypeTitle: "Page CT", referenceFieldName: "Reference Field", - } - ] + }, + ], }); } return Promise.resolve({}); @@ -119,12 +128,13 @@ vi.mock("../generators/generateCustomCursor", () => ({ vi.mock("../visualBuilder.style", () => ({ visualBuilderStyles: vi.fn().mockReturnValue({ - "visual-builder__focused-toolbar--variant": "visual-builder__focused-toolbar--variant" + "visual-builder__focused-toolbar--variant": + "visual-builder__focused-toolbar--variant", }), })); vi.mock("../VariantIndicator", () => ({ - VariantIndicator: () =>
Variant
+ VariantIndicator: () =>
Variant
, })); vi.mock("../../utils/errorHandling", () => ({ @@ -153,41 +163,52 @@ describe("FieldLabelWrapperComponent", () => { }); // Reset the mock implementation to the default one - vi.mocked(visualBuilderPostMessage!.send).mockImplementation((eventName: string, fields: any) => { - if (eventName === "GET_FIELD_DISPLAY_NAMES") { - // Always return display names for all requested fields - const result: Record = {}; - fields.forEach((field: any) => { - if (field.cslpValue === "mockFieldCslp") { - result[field.cslpValue] = "Field 0"; - } else if (field.cslpValue === "contentTypeUid.entryUid.locale.parentPath1") { - result[field.cslpValue] = "Field 1"; - } else if (field.cslpValue === "contentTypeUid.entryUid.locale.parentPath2") { - result[field.cslpValue] = "Field 2"; - } else if (field.cslpValue === "contentTypeUid.entryUid.locale.parentPath3") { - result[field.cslpValue] = "Field 3"; - } else { - result[field.cslpValue] = field.cslpValue; // fallback - } - }); - return Promise.resolve(result); - } else if(eventName === "GET_CONTENT_TYPE_NAME") { - return Promise.resolve({ - contentTypeName: "Page CT", - }); - } else if(eventName === "REFERENCE_MAP") { - return Promise.resolve({ - "mockEntryUid": [ - { - contentTypeUid: "mockContentTypeUid", - contentTypeTitle: "Page CT", - referenceFieldName: "Reference Field", + vi.mocked(visualBuilderPostMessage!.send).mockImplementation( + (eventName: string, fields: any) => { + if (eventName === "GET_FIELD_DISPLAY_NAMES") { + // Always return display names for all requested fields + const result: Record = {}; + fields.forEach((field: any) => { + if (field.cslpValue === "mockFieldCslp") { + result[field.cslpValue] = "Field 0"; + } else if ( + field.cslpValue === + "contentTypeUid.entryUid.locale.parentPath1" + ) { + result[field.cslpValue] = "Field 1"; + } else if ( + field.cslpValue === + "contentTypeUid.entryUid.locale.parentPath2" + ) { + result[field.cslpValue] = "Field 2"; + } else if ( + field.cslpValue === + "contentTypeUid.entryUid.locale.parentPath3" + ) { + result[field.cslpValue] = "Field 3"; + } else { + result[field.cslpValue] = field.cslpValue; // fallback } - ] - }); + }); + return Promise.resolve(result); + } else if (eventName === "GET_CONTENT_TYPE_NAME") { + return Promise.resolve({ + contentTypeName: "Page CT", + }); + } else if (eventName === "REFERENCE_MAP") { + return Promise.resolve({ + mockEntryUid: [ + { + contentTypeUid: "mockContentTypeUid", + contentTypeTitle: "Page CT", + referenceFieldName: "Reference Field", + }, + ], + }); + } + return Promise.resolve({}); } - return Promise.resolve({}); - }); + ); }); afterEach(() => { @@ -222,19 +243,27 @@ describe("FieldLabelWrapperComponent", () => { const mockGetParentEditable = () => document.createElement("div"); - test("renders current field and parent fields correctly", async () => { - const { findByText } = await asyncRender( - - ); + test( + "renders current field and parent fields correctly", + async () => { + const { findByText } = await asyncRender( + + ); - const currentField = await findByText(DISPLAY_NAMES.mockFieldCslp, {}, { timeout: 15000 }); - expect(currentField).toBeVisible(); - }, { timeout: 20000 }); + const currentField = await findByText( + DISPLAY_NAMES.mockFieldCslp, + {}, + { timeout: 15000 } + ); + expect(currentField).toBeVisible(); + }, + { timeout: 20000 } + ); test("displays current field icon", async () => { const { findByTestId } = await asyncRender( @@ -323,24 +352,33 @@ describe("FieldLabelWrapperComponent", () => { ); }); - test("renders ToolbarTooltip component with correct data", async () => { - const { findByTestId } = await asyncRender( - - ); - - // Check that the ToolbarTooltip wrapper is rendered - const tooltipWrapper = await findByTestId("toolbar-tooltip", { timeout: 15000 }); - expect(tooltipWrapper).toBeInTheDocument(); - - // Check that the main field label wrapper is rendered - const fieldLabelWrapper = await findByTestId("visual-builder__focused-toolbar__field-label-wrapper", { timeout: 15000 }); - expect(fieldLabelWrapper).toBeInTheDocument(); - }, { timeout: 20000 }); + test( + "renders ToolbarTooltip component with correct data", + async () => { + const { findByTestId } = await asyncRender( + + ); + + // Check that the ToolbarTooltip wrapper is rendered + const tooltipWrapper = await findByTestId("toolbar-tooltip", { + timeout: 15000, + }); + expect(tooltipWrapper).toBeInTheDocument(); + + // Check that the main field label wrapper is rendered + const fieldLabelWrapper = await findByTestId( + "visual-builder__focused-toolbar__field-label-wrapper", + { timeout: 15000 } + ); + expect(fieldLabelWrapper).toBeInTheDocument(); + }, + { timeout: 20000 } + ); test("does not render reference icon when isReference is false", async () => { const { container } = await asyncRender( @@ -353,7 +391,9 @@ describe("FieldLabelWrapperComponent", () => { ); await waitFor(() => { - const referenceIconContainer = container.querySelector(".visual-builder__reference-icon-container"); + const referenceIconContainer = container.querySelector( + ".visual-builder__reference-icon-container" + ); expect(referenceIconContainer).not.toBeInTheDocument(); }); }); @@ -368,8 +408,13 @@ describe("FieldLabelWrapperComponent", () => { /> ); - const fieldLabelWrapper = await findByTestId("visual-builder__focused-toolbar__field-label-wrapper"); - expect(fieldLabelWrapper).toHaveAttribute("data-hovered-cslp", mockFieldMetadata.cslpValue); + const fieldLabelWrapper = await findByTestId( + "visual-builder__focused-toolbar__field-label-wrapper" + ); + expect(fieldLabelWrapper).toHaveAttribute( + "data-hovered-cslp", + mockFieldMetadata.cslpValue + ); }); test("does not render ContentTypeIcon when loading", async () => { @@ -388,16 +433,18 @@ describe("FieldLabelWrapperComponent", () => { ); // Wait a bit to ensure the component has time to render - await new Promise(resolve => setTimeout(resolve, 100)); + await new Promise((resolve) => setTimeout(resolve, 100)); - const contentTypeIcon = container.querySelector(".visual-builder__content-type-icon"); + const contentTypeIcon = container.querySelector( + ".visual-builder__content-type-icon" + ); expect(contentTypeIcon).not.toBeInTheDocument(); }); test("renders VariantIndicator when field has variant", async () => { const variantFieldMetadata = { ...mockFieldMetadata, - variant: "variant-uid-123" + variant: "variant-uid-123", }; const { findByTestId } = await asyncRender( @@ -424,7 +471,9 @@ describe("FieldLabelWrapperComponent", () => { ); await waitFor(() => { - const variantIndicator = container.querySelector("[data-testid='variant-indicator']"); + const variantIndicator = container.querySelector( + "[data-testid='variant-indicator']" + ); expect(variantIndicator).not.toBeInTheDocument(); }); }); @@ -432,7 +481,7 @@ describe("FieldLabelWrapperComponent", () => { test("applies variant CSS classes when field has variant", async () => { const variantFieldMetadata = { ...mockFieldMetadata, - variant: "variant-uid-123" + variant: "variant-uid-123", }; const { findByTestId } = await asyncRender( @@ -444,10 +493,14 @@ describe("FieldLabelWrapperComponent", () => { /> ); - const fieldLabelWrapper = await findByTestId("visual-builder__focused-toolbar__field-label-wrapper"); - + const fieldLabelWrapper = await findByTestId( + "visual-builder__focused-toolbar__field-label-wrapper" + ); + await waitFor(() => { - expect(fieldLabelWrapper).toHaveClass("visual-builder__focused-toolbar--variant"); + expect(fieldLabelWrapper).toHaveClass( + "visual-builder__focused-toolbar--variant" + ); }); }); @@ -461,10 +514,40 @@ describe("FieldLabelWrapperComponent", () => { /> ); - const fieldLabelWrapper = await findByTestId("visual-builder__focused-toolbar__field-label-wrapper"); - + const fieldLabelWrapper = await findByTestId( + "visual-builder__focused-toolbar__field-label-wrapper" + ); + await waitFor(() => { - expect(fieldLabelWrapper).not.toHaveClass("visual-builder__focused-toolbar--variant"); + expect(fieldLabelWrapper).not.toHaveClass( + "visual-builder__focused-toolbar--variant" + ); + }); + }); + + describe("variant linking click condition", () => { + test("should allow modal opening when canLinkVariant is true", () => { + // Test the actual click condition logic without rendering + const canLinkVariant = true; + const shouldOpenModal = !!canLinkVariant; + + expect(shouldOpenModal).toBe(true); + }); + + test("should not allow modal opening when canLinkVariant is false", () => { + // Test the actual click condition logic without rendering + const canLinkVariant = false; + const shouldOpenModal = !!canLinkVariant; + + expect(shouldOpenModal).toBe(false); + }); + + test("should not allow modal opening when canLinkVariant is undefined", () => { + // Test the actual click condition logic without rendering + const canLinkVariant = undefined; + const shouldOpenModal = !!canLinkVariant; + + expect(shouldOpenModal).toBe(false); }); }); }); diff --git a/src/visualBuilder/eventManager/__test__/useRevalidateFieldDataPostMessageEvent.test.ts b/src/visualBuilder/eventManager/__test__/useRevalidateFieldDataPostMessageEvent.test.ts new file mode 100644 index 00000000..63a42da9 --- /dev/null +++ b/src/visualBuilder/eventManager/__test__/useRevalidateFieldDataPostMessageEvent.test.ts @@ -0,0 +1,239 @@ +import { vi, describe, it, expect, beforeEach } from "vitest"; +import { VisualBuilder } from "../.."; +import { FieldSchemaMap } from "../../utils/fieldSchemaMap"; +import { getFieldData } from "../../utils/getFieldData"; +import visualBuilderPostMessage from "../../utils/visualBuilderPostMessage"; +import { VisualBuilderPostMessageEvents } from "../../utils/types/postMessage.types"; +import { useRevalidateFieldDataPostMessageEvent } from "../useRevalidateFieldDataPostMessageEvent"; + +// Mock dependencies +vi.mock("../../utils/fieldSchemaMap", () => ({ + FieldSchemaMap: { + clearContentTypeSchema: vi.fn(), + clear: vi.fn(), + getFieldSchema: vi.fn(), + }, +})); + +vi.mock("../../utils/getFieldData", () => ({ + getFieldData: vi.fn(), +})); + +vi.mock("../../utils/visualBuilderPostMessage", () => ({ + default: { + on: vi.fn(), + }, +})); + +vi.mock("../../../cslp", () => ({ + extractDetailsFromCslp: vi.fn(), +})); + +// Mock window.location.reload +Object.defineProperty(window, "location", { + value: { + reload: vi.fn(), + }, + writable: true, +}); + +describe("useRevalidateFieldDataPostMessageEvent", () => { + beforeEach(() => { + vi.clearAllMocks(); + + // Reset VisualBuilder global state + VisualBuilder.VisualBuilderGlobalState = { + // @ts-expect-error mocking only required properties + value: { + previousHoveredTargetDOM: null, + previousSelectedEditableDOM: null, + }, + }; + }); + + it("should register post message event listener", () => { + useRevalidateFieldDataPostMessageEvent(); + + expect(visualBuilderPostMessage.on).toHaveBeenCalledWith( + VisualBuilderPostMessageEvents.REVALIDATE_FIELD_DATA, + expect.any(Function) + ); + }); + + describe("handleRevalidateFieldData", () => { + let mockHandleRevalidateFieldData: any; + + beforeEach(() => { + useRevalidateFieldDataPostMessageEvent(); + mockHandleRevalidateFieldData = (visualBuilderPostMessage.on as any) + .mock.calls[0][1]; + }); + + it("should revalidate specific field when hovered element exists", async () => { + const mockElement = document.createElement("div"); + mockElement.setAttribute("data-cslp", "content_type.entry.field"); + + VisualBuilder.VisualBuilderGlobalState.value.previousHoveredTargetDOM = + mockElement; + + const mockExtractDetailsFromCslp = await import("../../../cslp"); + vi.mocked( + mockExtractDetailsFromCslp.extractDetailsFromCslp + ).mockReturnValue({ + content_type_uid: "test_content_type", + entry_uid: "test_entry", + locale: "en-us", + fieldPath: "test_field", + fieldPathWithIndex: "test_field", + }); + + vi.mocked(FieldSchemaMap.getFieldSchema).mockResolvedValue({ + test: "schema", + }); + vi.mocked(getFieldData).mockResolvedValue({ test: "data" }); + + await mockHandleRevalidateFieldData(); + + expect(FieldSchemaMap.clearContentTypeSchema).toHaveBeenCalledWith( + "test_content_type" + ); + expect(FieldSchemaMap.getFieldSchema).toHaveBeenCalledWith( + "test_content_type", + "test_field" + ); + expect(getFieldData).toHaveBeenCalledWith( + { + content_type_uid: "test_content_type", + entry_uid: "test_entry", + locale: "en-us", + }, + "test_field" + ); + expect(window.location.reload).not.toHaveBeenCalled(); + }); + + it("should fallback to focused element when no hovered element", async () => { + const mockElement = document.createElement("div"); + mockElement.setAttribute("data-cslp", "content_type.entry.field"); + + VisualBuilder.VisualBuilderGlobalState.value.previousHoveredTargetDOM = + null; + VisualBuilder.VisualBuilderGlobalState.value.previousSelectedEditableDOM = + mockElement; + + const mockExtractDetailsFromCslp = await import("../../../cslp"); + vi.mocked( + mockExtractDetailsFromCslp.extractDetailsFromCslp + ).mockReturnValue({ + content_type_uid: "test_content_type", + entry_uid: "test_entry", + locale: "en-us", + fieldPath: "test_field", + fieldPathWithIndex: "test_field", + }); + + vi.mocked(FieldSchemaMap.getFieldSchema).mockResolvedValue({ + test: "schema", + }); + vi.mocked(getFieldData).mockResolvedValue({ test: "data" }); + + await mockHandleRevalidateFieldData(); + + expect(FieldSchemaMap.clearContentTypeSchema).toHaveBeenCalledWith( + "test_content_type" + ); + }); + + it("should clear all field schema cache when no target element", async () => { + VisualBuilder.VisualBuilderGlobalState.value.previousHoveredTargetDOM = + null; + VisualBuilder.VisualBuilderGlobalState.value.previousSelectedEditableDOM = + null; + + await mockHandleRevalidateFieldData(); + + expect(FieldSchemaMap.clear).toHaveBeenCalled(); + expect(window.location.reload).not.toHaveBeenCalled(); + }); + + it("should refresh iframe when field schema validation fails", async () => { + const mockElement = document.createElement("div"); + mockElement.setAttribute("data-cslp", "content_type.entry.field"); + + VisualBuilder.VisualBuilderGlobalState.value.previousHoveredTargetDOM = + mockElement; + + const mockExtractDetailsFromCslp = await import("../../../cslp"); + vi.mocked( + mockExtractDetailsFromCslp.extractDetailsFromCslp + ).mockReturnValue({ + content_type_uid: "test_content_type", + entry_uid: "test_entry", + locale: "en-us", + fieldPath: "test_field", + fieldPathWithIndex: "test_field", + }); + + vi.mocked(FieldSchemaMap.getFieldSchema).mockResolvedValue(null); + vi.mocked(getFieldData).mockResolvedValue(null); + + await mockHandleRevalidateFieldData(); + + expect(FieldSchemaMap.clear).toHaveBeenCalled(); + }); + + it("should refresh iframe when clearing cache fails", async () => { + VisualBuilder.VisualBuilderGlobalState.value.previousHoveredTargetDOM = + null; + VisualBuilder.VisualBuilderGlobalState.value.previousSelectedEditableDOM = + null; + + vi.mocked(FieldSchemaMap.clear).mockImplementation(() => { + throw new Error("Cache clear failed"); + }); + + await mockHandleRevalidateFieldData(); + + expect(FieldSchemaMap.clear).toHaveBeenCalled(); + expect(window.location.reload).toHaveBeenCalled(); + }); + + it("should refresh iframe when any error occurs", async () => { + const mockElement = document.createElement("div"); + mockElement.setAttribute("data-cslp", "content_type.entry.field"); + + VisualBuilder.VisualBuilderGlobalState.value.previousHoveredTargetDOM = + mockElement; + + const mockExtractDetailsFromCslp = await import("../../../cslp"); + vi.mocked( + mockExtractDetailsFromCslp.extractDetailsFromCslp + ).mockImplementation(() => { + throw new Error("CSLP parsing failed"); + }); + + await mockHandleRevalidateFieldData(); + + expect(window.location.reload).toHaveBeenCalled(); + }); + + it("should handle elements without data-cslp attribute", async () => { + const mockElement = document.createElement("div"); + // No data-cslp attribute + + VisualBuilder.VisualBuilderGlobalState.value.previousHoveredTargetDOM = + mockElement; + + // Reset the clear mock to not throw error for this test + vi.mocked(FieldSchemaMap.clear).mockReset(); + vi.mocked(FieldSchemaMap.clear).mockImplementation(() => { + // Successful clear - no error + }); + + await mockHandleRevalidateFieldData(); + + expect(FieldSchemaMap.clear).toHaveBeenCalled(); + expect(window.location.reload).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/src/visualBuilder/utils/__test__/isFieldDisabled.test.ts b/src/visualBuilder/utils/__test__/isFieldDisabled.test.ts index 6c154b2d..331bc71f 100644 --- a/src/visualBuilder/utils/__test__/isFieldDisabled.test.ts +++ b/src/visualBuilder/utils/__test__/isFieldDisabled.test.ts @@ -72,7 +72,30 @@ describe("isFieldDisabled", () => { const result = isFieldDisabled(fieldSchemaMap, eventFieldDetails); expect(result.isDisabled).toBe(true); expect(result.reason).toBe( - "This field is not editable as it is not linked to the selected variant" + "This field is not editable as it is not linked to the selected variant. Contact your stack admin or owner to link it." + ); + }); + + it("should return disabled state due to unlinked variant with link option", () => { + // @ts-expect-error mocking only required properties + const fieldSchemaMap: ISchemaFieldMap = { + field_metadata: { + isUnlinkedVariant: true, + canLinkVariant: true, + }, + }; + const eventFieldDetails: FieldDetails = { + editableElement: document.createElement("div"), + // @ts-expect-error mocking only required properties + fieldMetadata: { + locale: "en-us", + }, + }; + + const result = isFieldDisabled(fieldSchemaMap, eventFieldDetails); + expect(result.isDisabled).toBe(true); + expect(result.reason).toBe( + "This field is not editable as it is not linked to the selected variant. Click here to link a variant" ); }); From 17e8f5d571581590ec2dbccba1078252b89b7f7e Mon Sep 17 00:00:00 2001 From: Karan Gandhi Date: Tue, 14 Oct 2025 16:00:23 +0530 Subject: [PATCH 04/19] fix: string and focus unfocus on variant link --- .../components/fieldLabelWrapper.tsx | 48 ++- ...evalidateFieldDataPostMessageEvent.test.ts | 318 +++++++++++++++++- .../useRevalidateFieldDataPostMessageEvent.ts | 127 ++++++- src/visualBuilder/visualBuilder.style.ts | 40 ++- 4 files changed, 491 insertions(+), 42 deletions(-) diff --git a/src/visualBuilder/components/fieldLabelWrapper.tsx b/src/visualBuilder/components/fieldLabelWrapper.tsx index 11caf280..419726a9 100644 --- a/src/visualBuilder/components/fieldLabelWrapper.tsx +++ b/src/visualBuilder/components/fieldLabelWrapper.tsx @@ -188,20 +188,42 @@ function FieldLabelWrapperComponent( "visual-builder__tooltip--persistent" ] )} - data-tooltip={reason} - onClick={() => { - if (fieldSchema.field_metadata?.canLinkVariant) { - visualBuilderPostMessage?.send( - VisualBuilderPostMessageEvents.OPEN_LINK_VARIANT_MODAL, - { - contentTypeUid: - props.fieldMetadata - .content_type_uid, - } - ); - } - }} + data-tooltip={!reason?.toLowerCase().includes("click here to link a variant") + ? reason + : undefined} > + {reason + .toLowerCase() + .includes("click here to link a variant") && ( +
{ + if (fieldSchema.field_metadata?.canLinkVariant) { + visualBuilderPostMessage?.send( + VisualBuilderPostMessageEvents.OPEN_LINK_VARIANT_MODAL, + { + contentTypeUid: + props.fieldMetadata + .content_type_uid, + } + ); + } + }} + > + {(() => { + const [before, after] = reason.split( + /here/i + ); + return ( + <> + {before} + here + {after} + + ); + })()} +
+ )}
) : hasParentPaths ? ( diff --git a/src/visualBuilder/eventManager/__test__/useRevalidateFieldDataPostMessageEvent.test.ts b/src/visualBuilder/eventManager/__test__/useRevalidateFieldDataPostMessageEvent.test.ts index 63a42da9..a88ae62e 100644 --- a/src/visualBuilder/eventManager/__test__/useRevalidateFieldDataPostMessageEvent.test.ts +++ b/src/visualBuilder/eventManager/__test__/useRevalidateFieldDataPostMessageEvent.test.ts @@ -1,4 +1,4 @@ -import { vi, describe, it, expect, beforeEach } from "vitest"; +import { vi, describe, it, expect, beforeEach, afterEach } from "vitest"; import { VisualBuilder } from "../.."; import { FieldSchemaMap } from "../../utils/fieldSchemaMap"; import { getFieldData } from "../../utils/getFieldData"; @@ -29,6 +29,14 @@ vi.mock("../../../cslp", () => ({ extractDetailsFromCslp: vi.fn(), })); +vi.mock("../../generators/generateOverlay", () => ({ + hideFocusOverlay: vi.fn(), +})); + +vi.mock("../../listeners/mouseClick", () => ({ + handleBuilderInteraction: vi.fn(), +})); + // Mock window.location.reload Object.defineProperty(window, "location", { value: { @@ -37,24 +45,51 @@ Object.defineProperty(window, "location", { writable: true, }); +// Mock requestAnimationFrame +global.requestAnimationFrame = vi.fn((cb) => { + cb(0); + return 0; +}); + describe("useRevalidateFieldDataPostMessageEvent", () => { + let visualBuilderContainer: HTMLDivElement; + let overlayWrapper: HTMLDivElement; + let focusedToolbar: HTMLDivElement; + beforeEach(() => { vi.clearAllMocks(); + // Create DOM elements + visualBuilderContainer = document.createElement("div"); + visualBuilderContainer.classList.add("visual-builder__container"); + overlayWrapper = document.createElement("div"); + overlayWrapper.classList.add("visual-builder__overlay__wrapper"); + focusedToolbar = document.createElement("div"); + focusedToolbar.classList.add("visual-builder__focused-toolbar"); + + document.body.appendChild(visualBuilderContainer); + document.body.appendChild(overlayWrapper); + document.body.appendChild(focusedToolbar); + // Reset VisualBuilder global state VisualBuilder.VisualBuilderGlobalState = { // @ts-expect-error mocking only required properties value: { previousHoveredTargetDOM: null, previousSelectedEditableDOM: null, + isFocussed: false, }, }; }); + afterEach(() => { + document.body.innerHTML = ""; + }); + it("should register post message event listener", () => { useRevalidateFieldDataPostMessageEvent(); - expect(visualBuilderPostMessage.on).toHaveBeenCalledWith( + expect(visualBuilderPostMessage?.on).toHaveBeenCalledWith( VisualBuilderPostMessageEvents.REVALIDATE_FIELD_DATA, expect.any(Function) ); @@ -65,8 +100,9 @@ describe("useRevalidateFieldDataPostMessageEvent", () => { beforeEach(() => { useRevalidateFieldDataPostMessageEvent(); - mockHandleRevalidateFieldData = (visualBuilderPostMessage.on as any) - .mock.calls[0][1]; + mockHandleRevalidateFieldData = ( + visualBuilderPostMessage?.on as any + ).mock.calls[0][1]; }); it("should revalidate specific field when hovered element exists", async () => { @@ -85,12 +121,12 @@ describe("useRevalidateFieldDataPostMessageEvent", () => { locale: "en-us", fieldPath: "test_field", fieldPathWithIndex: "test_field", - }); + } as any); vi.mocked(FieldSchemaMap.getFieldSchema).mockResolvedValue({ test: "schema", - }); - vi.mocked(getFieldData).mockResolvedValue({ test: "data" }); + } as any); + vi.mocked(getFieldData).mockResolvedValue({ test: "data" } as any); await mockHandleRevalidateFieldData(); @@ -130,12 +166,12 @@ describe("useRevalidateFieldDataPostMessageEvent", () => { locale: "en-us", fieldPath: "test_field", fieldPathWithIndex: "test_field", - }); + } as any); vi.mocked(FieldSchemaMap.getFieldSchema).mockResolvedValue({ test: "schema", - }); - vi.mocked(getFieldData).mockResolvedValue({ test: "data" }); + } as any); + vi.mocked(getFieldData).mockResolvedValue({ test: "data" } as any); await mockHandleRevalidateFieldData(); @@ -172,10 +208,12 @@ describe("useRevalidateFieldDataPostMessageEvent", () => { locale: "en-us", fieldPath: "test_field", fieldPathWithIndex: "test_field", - }); + } as any); - vi.mocked(FieldSchemaMap.getFieldSchema).mockResolvedValue(null); - vi.mocked(getFieldData).mockResolvedValue(null); + (vi.mocked(FieldSchemaMap.getFieldSchema).mockResolvedValue as any)( + null + ); + (vi.mocked(getFieldData).mockResolvedValue as any)(null); await mockHandleRevalidateFieldData(); @@ -235,5 +273,259 @@ describe("useRevalidateFieldDataPostMessageEvent", () => { expect(FieldSchemaMap.clear).toHaveBeenCalled(); expect(window.location.reload).not.toHaveBeenCalled(); }); + + describe("unfocus and refocus behavior", () => { + let hideFocusOverlay: any; + let handleBuilderInteraction: any; + + beforeEach(async () => { + const overlayModule = await import( + "../../generators/generateOverlay" + ); + const mouseClickModule = await import( + "../../listeners/mouseClick" + ); + hideFocusOverlay = vi.mocked(overlayModule.hideFocusOverlay); + handleBuilderInteraction = vi.mocked( + mouseClickModule.handleBuilderInteraction + ); + }); + + it("should unfocus element before revalidation when focused element exists", async () => { + const mockElement = document.createElement("div"); + mockElement.setAttribute( + "data-cslp", + "content_type.entry.field" + ); + mockElement.setAttribute("data-cslp-unique-id", "unique-123"); + + VisualBuilder.VisualBuilderGlobalState.value.previousSelectedEditableDOM = + mockElement; + VisualBuilder.VisualBuilderGlobalState.value.isFocussed = true; + + const mockExtractDetailsFromCslp = await import( + "../../../cslp" + ); + vi.mocked( + mockExtractDetailsFromCslp.extractDetailsFromCslp + ).mockReturnValue({ + content_type_uid: "test_content_type", + entry_uid: "test_entry", + locale: "en-us", + fieldPath: "test_field", + fieldPathWithIndex: "test_field", + } as any); + + vi.mocked(FieldSchemaMap.getFieldSchema).mockResolvedValue({ + test: "schema", + } as any); + vi.mocked(getFieldData).mockResolvedValue({ + test: "data", + } as any); + + await mockHandleRevalidateFieldData(); + + // Should call hideFocusOverlay to unfocus + expect(hideFocusOverlay).toHaveBeenCalledWith( + expect.objectContaining({ + visualBuilderContainer: expect.any(HTMLDivElement), + visualBuilderOverlayWrapper: expect.any(HTMLDivElement), + focusedToolbar: expect.any(HTMLDivElement), + noTrigger: true, + }) + ); + + // Should clear global state + expect( + VisualBuilder.VisualBuilderGlobalState.value + .previousSelectedEditableDOM + ).toBeNull(); + expect( + VisualBuilder.VisualBuilderGlobalState.value.isFocussed + ).toBe(false); + }); + + it("should refocus element after revalidation completes", async () => { + const mockElement = document.createElement("div"); + mockElement.setAttribute( + "data-cslp", + "content_type.entry.field" + ); + mockElement.setAttribute("data-cslp-unique-id", "unique-123"); + document.body.appendChild(mockElement); + + VisualBuilder.VisualBuilderGlobalState.value.previousSelectedEditableDOM = + mockElement; + + const mockExtractDetailsFromCslp = await import( + "../../../cslp" + ); + vi.mocked( + mockExtractDetailsFromCslp.extractDetailsFromCslp + ).mockReturnValue({ + content_type_uid: "test_content_type", + entry_uid: "test_entry", + locale: "en-us", + fieldPath: "test_field", + fieldPathWithIndex: "test_field", + } as any); + + vi.mocked(FieldSchemaMap.getFieldSchema).mockResolvedValue({ + test: "schema", + } as any); + vi.mocked(getFieldData).mockResolvedValue({ + test: "data", + } as any); + + await mockHandleRevalidateFieldData(); + + // Should refocus the element using unique ID + expect(handleBuilderInteraction).toHaveBeenCalledWith( + expect.objectContaining({ + event: expect.any(MouseEvent), + previousSelectedEditableDOM: null, + visualBuilderContainer: expect.any(HTMLDivElement), + overlayWrapper: expect.any(HTMLDivElement), + }) + ); + + document.body.removeChild(mockElement); + }); + + it("should refocus using data-cslp when unique ID is not available", async () => { + const mockElement = document.createElement("div"); + mockElement.setAttribute( + "data-cslp", + "content_type.entry.field" + ); + // No unique ID + document.body.appendChild(mockElement); + + VisualBuilder.VisualBuilderGlobalState.value.previousSelectedEditableDOM = + mockElement; + + const mockExtractDetailsFromCslp = await import( + "../../../cslp" + ); + vi.mocked( + mockExtractDetailsFromCslp.extractDetailsFromCslp + ).mockReturnValue({ + content_type_uid: "test_content_type", + entry_uid: "test_entry", + locale: "en-us", + fieldPath: "test_field", + fieldPathWithIndex: "test_field", + } as any); + + vi.mocked(FieldSchemaMap.getFieldSchema).mockResolvedValue({ + test: "schema", + } as any); + vi.mocked(getFieldData).mockResolvedValue({ + test: "data", + } as any); + + await mockHandleRevalidateFieldData(); + + // Should still call handleBuilderInteraction + expect(handleBuilderInteraction).toHaveBeenCalled(); + + document.body.removeChild(mockElement); + }); + + it("should not refocus if element cannot be found after revalidation", async () => { + const mockElement = document.createElement("div"); + mockElement.setAttribute( + "data-cslp", + "content_type.entry.field" + ); + // Don't append to DOM - element won't be found + + VisualBuilder.VisualBuilderGlobalState.value.previousSelectedEditableDOM = + mockElement; + + const mockExtractDetailsFromCslp = await import( + "../../../cslp" + ); + vi.mocked( + mockExtractDetailsFromCslp.extractDetailsFromCslp + ).mockReturnValue({ + content_type_uid: "test_content_type", + entry_uid: "test_entry", + locale: "en-us", + fieldPath: "test_field", + fieldPathWithIndex: "test_field", + } as any); + + vi.mocked(FieldSchemaMap.getFieldSchema).mockResolvedValue({ + test: "schema", + } as any); + vi.mocked(getFieldData).mockResolvedValue({ + test: "data", + } as any); + + await mockHandleRevalidateFieldData(); + + // Should not call handleBuilderInteraction if element not found + expect(handleBuilderInteraction).not.toHaveBeenCalled(); + }); + + it("should not unfocus or refocus when no element is focused", async () => { + VisualBuilder.VisualBuilderGlobalState.value.previousSelectedEditableDOM = + null; + + await mockHandleRevalidateFieldData(); + + // Should not call unfocus/refocus functions + expect(hideFocusOverlay).not.toHaveBeenCalled(); + expect(handleBuilderInteraction).not.toHaveBeenCalled(); + + // Should still clear cache + expect(FieldSchemaMap.clear).toHaveBeenCalled(); + }); + + it("should handle refocus errors gracefully", async () => { + const mockElement = document.createElement("div"); + mockElement.setAttribute( + "data-cslp", + "content_type.entry.field" + ); + document.body.appendChild(mockElement); + + VisualBuilder.VisualBuilderGlobalState.value.previousSelectedEditableDOM = + mockElement; + + const mockExtractDetailsFromCslp = await import( + "../../../cslp" + ); + vi.mocked( + mockExtractDetailsFromCslp.extractDetailsFromCslp + ).mockReturnValue({ + content_type_uid: "test_content_type", + entry_uid: "test_entry", + locale: "en-us", + fieldPath: "test_field", + fieldPathWithIndex: "test_field", + } as any); + + vi.mocked(FieldSchemaMap.getFieldSchema).mockResolvedValue({ + test: "schema", + } as any); + vi.mocked(getFieldData).mockResolvedValue({ + test: "data", + } as any); + + // Make handleBuilderInteraction throw an error + handleBuilderInteraction.mockRejectedValueOnce( + new Error("Refocus failed") + ); + + // Should not throw - error should be caught and logged + await expect( + mockHandleRevalidateFieldData() + ).resolves.not.toThrow(); + + document.body.removeChild(mockElement); + }); + }); }); }); diff --git a/src/visualBuilder/eventManager/useRevalidateFieldDataPostMessageEvent.ts b/src/visualBuilder/eventManager/useRevalidateFieldDataPostMessageEvent.ts index 2727dc3d..43d295fd 100644 --- a/src/visualBuilder/eventManager/useRevalidateFieldDataPostMessageEvent.ts +++ b/src/visualBuilder/eventManager/useRevalidateFieldDataPostMessageEvent.ts @@ -4,23 +4,33 @@ import { FieldSchemaMap } from "../utils/fieldSchemaMap"; import { getFieldData } from "../utils/getFieldData"; import visualBuilderPostMessage from "../utils/visualBuilderPostMessage"; import { VisualBuilderPostMessageEvents } from "../utils/types/postMessage.types"; +import { hideFocusOverlay } from "../generators/generateOverlay"; +import { handleBuilderInteraction } from "../listeners/mouseClick"; /** * Revalidates field data and schema after variant linking operations. - * First tries to revalidate specific hovered field, then falls back to clearing all schemas, - * and finally refreshes the iframe if all else fails. + * Unfocuses the selected element, revalidates data, and then reselects it. */ async function handleRevalidateFieldData(): Promise { + const focusedElement = + VisualBuilder.VisualBuilderGlobalState.value + .previousSelectedEditableDOM; + const hoveredElement = + VisualBuilder.VisualBuilderGlobalState.value.previousHoveredTargetDOM; + + // Store element identifiers for refocusing + const elementCslp = focusedElement?.getAttribute("data-cslp"); + const elementCslpUniqueId = + focusedElement?.getAttribute("data-cslp-unique-id") || null; + const shouldRefocus = !!focusedElement; + try { - // Get the currently hovered or focused field - const hoveredElement = - VisualBuilder.VisualBuilderGlobalState.value - .previousHoveredTargetDOM; - const focusedElement = - VisualBuilder.VisualBuilderGlobalState.value - .previousSelectedEditableDOM; - - // Prefer hovered element, fallback to focused element + // Step 1: Unfocus the current element + if (shouldRefocus) { + await unfocusElement(); + } + + // Step 2: Revalidate field data const targetElement = hoveredElement || focusedElement; if (targetElement) { @@ -79,9 +89,104 @@ async function handleRevalidateFieldData(): Promise { console.error("Error handling revalidate field data:", error); // Final fallback - refresh the page window.location.reload(); + } finally { + // Step 3: Refocus the element if we had one focused before + if (shouldRefocus && elementCslp) { + await refocusElement(elementCslp, elementCslpUniqueId); + } } } +/** + * Unfocuses the currently selected element and clears focus state + */ +async function unfocusElement(): Promise { + const { visualBuilderContainer, overlayWrapper, focusedToolbar } = + getVisualBuilderElements(); + + if (!visualBuilderContainer || !overlayWrapper) return; + + const dummyResizeObserver = new ResizeObserver(() => {}); + + // Hide focus overlay (cleanIndividualFieldResidual needs previousSelectedEditableDOM) + hideFocusOverlay({ + visualBuilderContainer, + visualBuilderOverlayWrapper: overlayWrapper, + focusedToolbar, + resizeObserver: dummyResizeObserver, + noTrigger: true, + }); + + // Clear global state after cleanup + VisualBuilder.VisualBuilderGlobalState.value.previousSelectedEditableDOM = + null; + VisualBuilder.VisualBuilderGlobalState.value.isFocussed = false; +} + +/** + * Refocuses an element by its CSLP identifier + */ +async function refocusElement( + cslp: string, + uniqueId: string | null +): Promise { + try { + // Find the element (prefer unique ID, fallback to CSLP) + const elementToRefocus = + (uniqueId && + document.querySelector( + `[data-cslp-unique-id="${uniqueId}"]` + )) || + document.querySelector(`[data-cslp="${cslp}"]`); + + if (!elementToRefocus) return; + + const { visualBuilderContainer, overlayWrapper, focusedToolbar } = + getVisualBuilderElements(); + + if (!visualBuilderContainer || !overlayWrapper) return; + + // Create synthetic click event + const syntheticEvent = new MouseEvent("click", { + bubbles: true, + cancelable: true, + }); + Object.defineProperty(syntheticEvent, "target", { + value: elementToRefocus, + enumerable: true, + }); + + // Refocus using handleBuilderInteraction + await handleBuilderInteraction({ + event: syntheticEvent, + previousSelectedEditableDOM: null, + visualBuilderContainer, + overlayWrapper, + focusedToolbar, + resizeObserver: new ResizeObserver(() => {}), + }); + } catch (error) { + console.warn("Could not refocus element after revalidation:", error); + } +} + +/** + * Gets the main visual builder DOM elements + */ +function getVisualBuilderElements() { + return { + visualBuilderContainer: document.querySelector( + ".visual-builder__container" + ) as HTMLDivElement | null, + overlayWrapper: document.querySelector( + ".visual-builder__overlay__wrapper" + ) as HTMLDivElement | null, + focusedToolbar: document.querySelector( + ".visual-builder__focused-toolbar" + ) as HTMLDivElement | null, + }; +} + export function useRevalidateFieldDataPostMessageEvent(): void { visualBuilderPostMessage?.on( VisualBuilderPostMessageEvents.REVALIDATE_FIELD_DATA, diff --git a/src/visualBuilder/visualBuilder.style.ts b/src/visualBuilder/visualBuilder.style.ts index 9d8f87ab..0a2530f2 100644 --- a/src/visualBuilder/visualBuilder.style.ts +++ b/src/visualBuilder/visualBuilder.style.ts @@ -300,10 +300,10 @@ export function visualBuilderStyles() { border-style: solid; align-content: center; text-align: center; - border-color: #BD59FA; + border-color: #bd59fa; svg { - color: #BD59FA; + color: #bd59fa; } `, "visual-builder__focused-toolbar": css` @@ -532,9 +532,8 @@ export function visualBuilderStyles() { `, "visual-builder__focused-toolbar--variant": css` .visual-builder__focused-toolbar__field-label-wrapper__current-field { - background: #BD59FA; + background: #bd59fa; } - `, "visual-builder__cursor-disabled": css` .visual-builder__cursor-icon { @@ -582,6 +581,37 @@ export function visualBuilderStyles() { &:after { display: block; } + + &:has(.visual-builder__custom-tooltip):before, + &:has(.visual-builder__custom-tooltip):after { + display: none; + } + `, + "visual-builder__custom-tooltip": css` + position: absolute; + bottom: 20px; + margin-bottom: 24px; + padding: 12px; + border-radius: 4px; + width: max-content; + max-width: 200px; + color: #fff; + font-family: Inter; + font-size: 0.75rem; + font-style: normal; + font-weight: 400; + line-height: 132%; /* 0.99rem */ + letter-spacing: 0.015rem; + background: #767676; + + &:after { + content: ""; + position: absolute; + bottom: -10px; + left: 10px; + border: 10px solid #000; + border-color: #767676 transparent transparent transparent; + } `, "visual-builder__empty-block": css` width: 100%; @@ -635,7 +665,7 @@ export function visualBuilderStyles() { outline: 2px dashed #909090; `, "visual-builder__hover-outline--variant": css` - outline: 2px dashed #BD59FA; + outline: 2px dashed #bd59fa; `, "visual-builder__default-cursor--disabled": css` cursor: none; From 8f2da6ebc95c33e1d09be01743f0660262fe936a Mon Sep 17 00:00:00 2001 From: Karan Gandhi Date: Tue, 14 Oct 2025 17:25:19 +0530 Subject: [PATCH 05/19] fix: addressed changes --- .../components/fieldLabelWrapper.tsx | 26 ++++++++++--------- .../useRevalidateFieldDataPostMessageEvent.ts | 21 --------------- src/visualBuilder/utils/isFieldDisabled.ts | 8 +++--- 3 files changed, 18 insertions(+), 37 deletions(-) diff --git a/src/visualBuilder/components/fieldLabelWrapper.tsx b/src/visualBuilder/components/fieldLabelWrapper.tsx index 419726a9..473e067f 100644 --- a/src/visualBuilder/components/fieldLabelWrapper.tsx +++ b/src/visualBuilder/components/fieldLabelWrapper.tsx @@ -171,6 +171,19 @@ function FieldLabelWrapperComponent( entryWorkflowStageDetails ); + const handleLinkVariant = () => { + if (fieldSchema.field_metadata?.canLinkVariant) { + visualBuilderPostMessage?.send( + VisualBuilderPostMessageEvents.OPEN_LINK_VARIANT_MODAL, + { + contentTypeUid: + props.fieldMetadata + .content_type_uid, + } + ); + } + }; + const currentFieldDisplayName = displayNames?.[props.fieldMetadata.cslpValue] ?? fieldSchema.display_name; @@ -197,18 +210,7 @@ function FieldLabelWrapperComponent( .includes("click here to link a variant") && (
{ - if (fieldSchema.field_metadata?.canLinkVariant) { - visualBuilderPostMessage?.send( - VisualBuilderPostMessageEvents.OPEN_LINK_VARIANT_MODAL, - { - contentTypeUid: - props.fieldMetadata - .content_type_uid, - } - ); - } - }} + onClick={handleLinkVariant} > {(() => { const [before, after] = reason.split( diff --git a/src/visualBuilder/eventManager/useRevalidateFieldDataPostMessageEvent.ts b/src/visualBuilder/eventManager/useRevalidateFieldDataPostMessageEvent.ts index 43d295fd..6656422d 100644 --- a/src/visualBuilder/eventManager/useRevalidateFieldDataPostMessageEvent.ts +++ b/src/visualBuilder/eventManager/useRevalidateFieldDataPostMessageEvent.ts @@ -44,27 +44,6 @@ async function handleRevalidateFieldData(): Promise { FieldSchemaMap.clearContentTypeSchema( fieldMetadata.content_type_uid ); - - // Fetch fresh field schema and data - const [fieldSchema, fieldData] = await Promise.all([ - FieldSchemaMap.getFieldSchema( - fieldMetadata.content_type_uid, - fieldMetadata.fieldPath - ), - getFieldData( - { - content_type_uid: - fieldMetadata.content_type_uid, - entry_uid: fieldMetadata.entry_uid, - locale: fieldMetadata.locale, - }, - fieldMetadata.fieldPathWithIndex - ), - ]); - - if (fieldSchema && fieldData) { - return; - } } catch (fieldError) { console.warn( "Failed to revalidate content type:", diff --git a/src/visualBuilder/utils/isFieldDisabled.ts b/src/visualBuilder/utils/isFieldDisabled.ts index 6ce473ba..73827408 100644 --- a/src/visualBuilder/utils/isFieldDisabled.ts +++ b/src/visualBuilder/utils/isFieldDisabled.ts @@ -10,8 +10,8 @@ const DisableReason = { LocalizedEntry: "Editing this field is restricted in localized entries", UnlinkedVariant: "This field is not editable as it is not linked to the selected variant.", - canLinkVaraint: "Click here to link a variant", - cannotLinkVariant: "Contact your stack admin or owner to link it.", + CanLinkVaraint: "Click here to link a variant", + CannotLinkVariant: "Contact your stack admin or owner to link it.", AudienceMode: "To edit an experience, open the Audience widget and click the Edit icon.", DisabledVariant: @@ -46,9 +46,9 @@ const getDisableReason = ( if (flags.updateRestrictDueToUnlinkVariant) { let reason = DisableReason.UnlinkedVariant; if (flags.canLinkVariant) { - reason += ` ${DisableReason.canLinkVaraint}`; + reason += ` ${DisableReason.CanLinkVaraint}`; } else { - reason += ` ${DisableReason.cannotLinkVariant}`; + reason += ` ${DisableReason.CannotLinkVariant}`; } return reason; } From 9e568d88fe374bfef725f68607baac0bbd168775 Mon Sep 17 00:00:00 2001 From: Karan Gandhi Date: Mon, 27 Oct 2025 17:41:24 +0530 Subject: [PATCH 06/19] fix: removing revalidate post message --- .../components/fieldLabelWrapper.tsx | 42 +- ...evalidateFieldDataPostMessageEvent.test.ts | 582 +++++++----------- .../useRevalidateFieldDataPostMessageEvent.ts | 12 +- src/visualBuilder/index.ts | 2 - .../utils/types/postMessage.types.ts | 1 - 5 files changed, 250 insertions(+), 389 deletions(-) diff --git a/src/visualBuilder/components/fieldLabelWrapper.tsx b/src/visualBuilder/components/fieldLabelWrapper.tsx index 473e067f..99315f3f 100644 --- a/src/visualBuilder/components/fieldLabelWrapper.tsx +++ b/src/visualBuilder/components/fieldLabelWrapper.tsx @@ -18,6 +18,7 @@ import { ContentTypeIcon } from "./icons"; import { ToolbarTooltip } from "./Tooltip"; import { fetchEntryPermissionsAndStageDetails } from "../utils/fetchEntryPermissionsAndStageDetails"; import { VariantIndicator } from "./VariantIndicator"; +import { handleRevalidateFieldData } from "../eventManager/useRevalidateFieldDataPostMessageEvent"; interface ReferenceParentMap { [entryUid: string]: { @@ -171,16 +172,37 @@ function FieldLabelWrapperComponent( entryWorkflowStageDetails ); - const handleLinkVariant = () => { + const handleLinkVariant = async () => { if (fieldSchema.field_metadata?.canLinkVariant) { - visualBuilderPostMessage?.send( - VisualBuilderPostMessageEvents.OPEN_LINK_VARIANT_MODAL, - { - contentTypeUid: - props.fieldMetadata - .content_type_uid, + try { + const result = await visualBuilderPostMessage?.send<{ + success?: boolean; + action?: string; + message?: string; + error?: boolean; + }>( + VisualBuilderPostMessageEvents.OPEN_LINK_VARIANT_MODAL, + { + contentTypeUid: + props.fieldMetadata.content_type_uid, + } + ); + + // If the modal was closed or linking failed, do nothing + if (!result || !result.success) { + return; + } + + // If linking was successful and requires revalidation, revalidate + if (result.action === "revalidate") { + await handleRevalidateFieldData(); } - ); + } catch (error) { + console.error( + "Error in link variant modal flow:", + error + ); + } } }; @@ -202,13 +224,13 @@ function FieldLabelWrapperComponent( ] )} data-tooltip={!reason?.toLowerCase().includes("click here to link a variant") - ? reason + ? reason : undefined} > {reason .toLowerCase() .includes("click here to link a variant") && ( -
diff --git a/src/visualBuilder/eventManager/__test__/useRevalidateFieldDataPostMessageEvent.test.ts b/src/visualBuilder/eventManager/__test__/useRevalidateFieldDataPostMessageEvent.test.ts index a88ae62e..3cccc5d2 100644 --- a/src/visualBuilder/eventManager/__test__/useRevalidateFieldDataPostMessageEvent.test.ts +++ b/src/visualBuilder/eventManager/__test__/useRevalidateFieldDataPostMessageEvent.test.ts @@ -1,27 +1,13 @@ import { vi, describe, it, expect, beforeEach, afterEach } from "vitest"; import { VisualBuilder } from "../.."; import { FieldSchemaMap } from "../../utils/fieldSchemaMap"; -import { getFieldData } from "../../utils/getFieldData"; -import visualBuilderPostMessage from "../../utils/visualBuilderPostMessage"; -import { VisualBuilderPostMessageEvents } from "../../utils/types/postMessage.types"; -import { useRevalidateFieldDataPostMessageEvent } from "../useRevalidateFieldDataPostMessageEvent"; +import { handleRevalidateFieldData } from "../useRevalidateFieldDataPostMessageEvent"; // Mock dependencies vi.mock("../../utils/fieldSchemaMap", () => ({ FieldSchemaMap: { clearContentTypeSchema: vi.fn(), clear: vi.fn(), - getFieldSchema: vi.fn(), - }, -})); - -vi.mock("../../utils/getFieldData", () => ({ - getFieldData: vi.fn(), -})); - -vi.mock("../../utils/visualBuilderPostMessage", () => ({ - default: { - on: vi.fn(), }, })); @@ -51,7 +37,7 @@ global.requestAnimationFrame = vi.fn((cb) => { return 0; }); -describe("useRevalidateFieldDataPostMessageEvent", () => { +describe("handleRevalidateFieldData", () => { let visualBuilderContainer: HTMLDivElement; let overlayWrapper: HTMLDivElement; let focusedToolbar: HTMLDivElement; @@ -86,31 +72,148 @@ describe("useRevalidateFieldDataPostMessageEvent", () => { document.body.innerHTML = ""; }); - it("should register post message event listener", () => { - useRevalidateFieldDataPostMessageEvent(); + it("should revalidate specific field when hovered element exists", async () => { + const mockElement = document.createElement("div"); + mockElement.setAttribute("data-cslp", "content_type.entry.field"); + + VisualBuilder.VisualBuilderGlobalState.value.previousHoveredTargetDOM = + mockElement; - expect(visualBuilderPostMessage?.on).toHaveBeenCalledWith( - VisualBuilderPostMessageEvents.REVALIDATE_FIELD_DATA, - expect.any(Function) + const mockExtractDetailsFromCslp = await import("../../../cslp"); + vi.mocked( + mockExtractDetailsFromCslp.extractDetailsFromCslp + ).mockReturnValue({ + content_type_uid: "test_content_type", + entry_uid: "test_entry", + locale: "en-us", + fieldPath: "test_field", + fieldPathWithIndex: "test_field", + } as any); + + await handleRevalidateFieldData(); + + expect(FieldSchemaMap.clearContentTypeSchema).toHaveBeenCalledWith( + "test_content_type" + ); + expect(window.location.reload).not.toHaveBeenCalled(); + }); + + it("should fallback to focused element when no hovered element", async () => { + const mockElement = document.createElement("div"); + mockElement.setAttribute("data-cslp", "content_type.entry.field"); + + VisualBuilder.VisualBuilderGlobalState.value.previousHoveredTargetDOM = + null; + VisualBuilder.VisualBuilderGlobalState.value.previousSelectedEditableDOM = + mockElement; + + const mockExtractDetailsFromCslp = await import("../../../cslp"); + vi.mocked( + mockExtractDetailsFromCslp.extractDetailsFromCslp + ).mockReturnValue({ + content_type_uid: "test_content_type", + entry_uid: "test_entry", + locale: "en-us", + fieldPath: "test_field", + fieldPathWithIndex: "test_field", + } as any); + + await handleRevalidateFieldData(); + + expect(FieldSchemaMap.clearContentTypeSchema).toHaveBeenCalledWith( + "test_content_type" ); }); - describe("handleRevalidateFieldData", () => { - let mockHandleRevalidateFieldData: any; + it("should clear all field schema cache when no target element", async () => { + VisualBuilder.VisualBuilderGlobalState.value.previousHoveredTargetDOM = + null; + VisualBuilder.VisualBuilderGlobalState.value.previousSelectedEditableDOM = + null; + + await handleRevalidateFieldData(); - beforeEach(() => { - useRevalidateFieldDataPostMessageEvent(); - mockHandleRevalidateFieldData = ( - visualBuilderPostMessage?.on as any - ).mock.calls[0][1]; + expect(FieldSchemaMap.clear).toHaveBeenCalled(); + expect(window.location.reload).not.toHaveBeenCalled(); + }); + + it("should refresh iframe when clearing cache fails", async () => { + VisualBuilder.VisualBuilderGlobalState.value.previousHoveredTargetDOM = + null; + VisualBuilder.VisualBuilderGlobalState.value.previousSelectedEditableDOM = + null; + + vi.mocked(FieldSchemaMap.clear).mockImplementation(() => { + throw new Error("Cache clear failed"); }); - it("should revalidate specific field when hovered element exists", async () => { + await handleRevalidateFieldData(); + + expect(FieldSchemaMap.clear).toHaveBeenCalled(); + expect(window.location.reload).toHaveBeenCalled(); + }); + + it("should refresh iframe when any error occurs", async () => { + const mockElement = document.createElement("div"); + mockElement.setAttribute("data-cslp", "content_type.entry.field"); + + VisualBuilder.VisualBuilderGlobalState.value.previousHoveredTargetDOM = + mockElement; + + const mockExtractDetailsFromCslp = await import("../../../cslp"); + vi.mocked( + mockExtractDetailsFromCslp.extractDetailsFromCslp + ).mockImplementation(() => { + throw new Error("CSLP parsing failed"); + }); + + await handleRevalidateFieldData(); + + expect(window.location.reload).toHaveBeenCalled(); + }); + + it("should handle elements without data-cslp attribute", async () => { + const mockElement = document.createElement("div"); + // No data-cslp attribute + + VisualBuilder.VisualBuilderGlobalState.value.previousHoveredTargetDOM = + mockElement; + + // Reset the clear mock to not throw error for this test + vi.mocked(FieldSchemaMap.clear).mockReset(); + vi.mocked(FieldSchemaMap.clear).mockImplementation(() => { + // Successful clear - no error + }); + + await handleRevalidateFieldData(); + + expect(FieldSchemaMap.clear).toHaveBeenCalled(); + expect(window.location.reload).not.toHaveBeenCalled(); + }); + + describe("unfocus and refocus behavior", () => { + let hideFocusOverlay: any; + let handleBuilderInteraction: any; + + beforeEach(async () => { + const overlayModule = await import( + "../../generators/generateOverlay" + ); + const mouseClickModule = await import("../../listeners/mouseClick"); + hideFocusOverlay = vi.mocked(overlayModule.hideFocusOverlay); + handleBuilderInteraction = vi.mocked( + mouseClickModule.handleBuilderInteraction + ); + }); + + it("should unfocus element before revalidation when focused element exists", async () => { const mockElement = document.createElement("div"); mockElement.setAttribute("data-cslp", "content_type.entry.field"); + mockElement.setAttribute("data-cslp-unique-id", "unique-123"); - VisualBuilder.VisualBuilderGlobalState.value.previousHoveredTargetDOM = + VisualBuilder.VisualBuilderGlobalState.value.previousSelectedEditableDOM = mockElement; + VisualBuilder.VisualBuilderGlobalState.value.isFocussed = true; const mockExtractDetailsFromCslp = await import("../../../cslp"); vi.mocked( @@ -123,37 +226,34 @@ describe("useRevalidateFieldDataPostMessageEvent", () => { fieldPathWithIndex: "test_field", } as any); - vi.mocked(FieldSchemaMap.getFieldSchema).mockResolvedValue({ - test: "schema", - } as any); - vi.mocked(getFieldData).mockResolvedValue({ test: "data" } as any); - - await mockHandleRevalidateFieldData(); + await handleRevalidateFieldData(); - expect(FieldSchemaMap.clearContentTypeSchema).toHaveBeenCalledWith( - "test_content_type" + // Should call hideFocusOverlay to unfocus + expect(hideFocusOverlay).toHaveBeenCalledWith( + expect.objectContaining({ + visualBuilderContainer: expect.any(HTMLDivElement), + visualBuilderOverlayWrapper: expect.any(HTMLDivElement), + focusedToolbar: expect.any(HTMLDivElement), + noTrigger: true, + }) ); - expect(FieldSchemaMap.getFieldSchema).toHaveBeenCalledWith( - "test_content_type", - "test_field" - ); - expect(getFieldData).toHaveBeenCalledWith( - { - content_type_uid: "test_content_type", - entry_uid: "test_entry", - locale: "en-us", - }, - "test_field" - ); - expect(window.location.reload).not.toHaveBeenCalled(); + + // Should clear global state + expect( + VisualBuilder.VisualBuilderGlobalState.value + .previousSelectedEditableDOM + ).toBeNull(); + expect( + VisualBuilder.VisualBuilderGlobalState.value.isFocussed + ).toBe(false); }); - it("should fallback to focused element when no hovered element", async () => { + it("should refocus element after revalidation completes", async () => { const mockElement = document.createElement("div"); mockElement.setAttribute("data-cslp", "content_type.entry.field"); + mockElement.setAttribute("data-cslp-unique-id", "unique-123"); + document.body.appendChild(mockElement); - VisualBuilder.VisualBuilderGlobalState.value.previousHoveredTargetDOM = - null; VisualBuilder.VisualBuilderGlobalState.value.previousSelectedEditableDOM = mockElement; @@ -168,35 +268,55 @@ describe("useRevalidateFieldDataPostMessageEvent", () => { fieldPathWithIndex: "test_field", } as any); - vi.mocked(FieldSchemaMap.getFieldSchema).mockResolvedValue({ - test: "schema", - } as any); - vi.mocked(getFieldData).mockResolvedValue({ test: "data" } as any); - - await mockHandleRevalidateFieldData(); + await handleRevalidateFieldData(); - expect(FieldSchemaMap.clearContentTypeSchema).toHaveBeenCalledWith( - "test_content_type" + // Should refocus the element using unique ID + expect(handleBuilderInteraction).toHaveBeenCalledWith( + expect.objectContaining({ + event: expect.any(MouseEvent), + previousSelectedEditableDOM: null, + visualBuilderContainer: expect.any(HTMLDivElement), + overlayWrapper: expect.any(HTMLDivElement), + }) ); + + document.body.removeChild(mockElement); }); - it("should clear all field schema cache when no target element", async () => { - VisualBuilder.VisualBuilderGlobalState.value.previousHoveredTargetDOM = - null; + it("should refocus using data-cslp when unique ID is not available", async () => { + const mockElement = document.createElement("div"); + mockElement.setAttribute("data-cslp", "content_type.entry.field"); + // No unique ID + document.body.appendChild(mockElement); + VisualBuilder.VisualBuilderGlobalState.value.previousSelectedEditableDOM = - null; + mockElement; - await mockHandleRevalidateFieldData(); + const mockExtractDetailsFromCslp = await import("../../../cslp"); + vi.mocked( + mockExtractDetailsFromCslp.extractDetailsFromCslp + ).mockReturnValue({ + content_type_uid: "test_content_type", + entry_uid: "test_entry", + locale: "en-us", + fieldPath: "test_field", + fieldPathWithIndex: "test_field", + } as any); - expect(FieldSchemaMap.clear).toHaveBeenCalled(); - expect(window.location.reload).not.toHaveBeenCalled(); + await handleRevalidateFieldData(); + + // Should still call handleBuilderInteraction + expect(handleBuilderInteraction).toHaveBeenCalled(); + + document.body.removeChild(mockElement); }); - it("should refresh iframe when field schema validation fails", async () => { + it("should not refocus if element cannot be found after revalidation", async () => { const mockElement = document.createElement("div"); mockElement.setAttribute("data-cslp", "content_type.entry.field"); + // Don't append to DOM - element won't be found - VisualBuilder.VisualBuilderGlobalState.value.previousHoveredTargetDOM = + VisualBuilder.VisualBuilderGlobalState.value.previousSelectedEditableDOM = mockElement; const mockExtractDetailsFromCslp = await import("../../../cslp"); @@ -210,322 +330,54 @@ describe("useRevalidateFieldDataPostMessageEvent", () => { fieldPathWithIndex: "test_field", } as any); - (vi.mocked(FieldSchemaMap.getFieldSchema).mockResolvedValue as any)( - null - ); - (vi.mocked(getFieldData).mockResolvedValue as any)(null); + await handleRevalidateFieldData(); - await mockHandleRevalidateFieldData(); - - expect(FieldSchemaMap.clear).toHaveBeenCalled(); + // Should not call handleBuilderInteraction if element not found + expect(handleBuilderInteraction).not.toHaveBeenCalled(); }); - it("should refresh iframe when clearing cache fails", async () => { - VisualBuilder.VisualBuilderGlobalState.value.previousHoveredTargetDOM = - null; + it("should not unfocus or refocus when no element is focused", async () => { VisualBuilder.VisualBuilderGlobalState.value.previousSelectedEditableDOM = null; - vi.mocked(FieldSchemaMap.clear).mockImplementation(() => { - throw new Error("Cache clear failed"); - }); + await handleRevalidateFieldData(); - await mockHandleRevalidateFieldData(); + // Should not call unfocus/refocus functions + expect(hideFocusOverlay).not.toHaveBeenCalled(); + expect(handleBuilderInteraction).not.toHaveBeenCalled(); + // Should still clear cache expect(FieldSchemaMap.clear).toHaveBeenCalled(); - expect(window.location.reload).toHaveBeenCalled(); }); - it("should refresh iframe when any error occurs", async () => { + it("should handle refocus errors gracefully", async () => { const mockElement = document.createElement("div"); mockElement.setAttribute("data-cslp", "content_type.entry.field"); + document.body.appendChild(mockElement); - VisualBuilder.VisualBuilderGlobalState.value.previousHoveredTargetDOM = + VisualBuilder.VisualBuilderGlobalState.value.previousSelectedEditableDOM = mockElement; const mockExtractDetailsFromCslp = await import("../../../cslp"); vi.mocked( mockExtractDetailsFromCslp.extractDetailsFromCslp - ).mockImplementation(() => { - throw new Error("CSLP parsing failed"); - }); - - await mockHandleRevalidateFieldData(); - - expect(window.location.reload).toHaveBeenCalled(); - }); - - it("should handle elements without data-cslp attribute", async () => { - const mockElement = document.createElement("div"); - // No data-cslp attribute - - VisualBuilder.VisualBuilderGlobalState.value.previousHoveredTargetDOM = - mockElement; - - // Reset the clear mock to not throw error for this test - vi.mocked(FieldSchemaMap.clear).mockReset(); - vi.mocked(FieldSchemaMap.clear).mockImplementation(() => { - // Successful clear - no error - }); + ).mockReturnValue({ + content_type_uid: "test_content_type", + entry_uid: "test_entry", + locale: "en-us", + fieldPath: "test_field", + fieldPathWithIndex: "test_field", + } as any); - await mockHandleRevalidateFieldData(); + // Make handleBuilderInteraction throw an error + handleBuilderInteraction.mockRejectedValueOnce( + new Error("Refocus failed") + ); - expect(FieldSchemaMap.clear).toHaveBeenCalled(); - expect(window.location.reload).not.toHaveBeenCalled(); - }); + // Should not throw - error should be caught and logged + await expect(handleRevalidateFieldData()).resolves.not.toThrow(); - describe("unfocus and refocus behavior", () => { - let hideFocusOverlay: any; - let handleBuilderInteraction: any; - - beforeEach(async () => { - const overlayModule = await import( - "../../generators/generateOverlay" - ); - const mouseClickModule = await import( - "../../listeners/mouseClick" - ); - hideFocusOverlay = vi.mocked(overlayModule.hideFocusOverlay); - handleBuilderInteraction = vi.mocked( - mouseClickModule.handleBuilderInteraction - ); - }); - - it("should unfocus element before revalidation when focused element exists", async () => { - const mockElement = document.createElement("div"); - mockElement.setAttribute( - "data-cslp", - "content_type.entry.field" - ); - mockElement.setAttribute("data-cslp-unique-id", "unique-123"); - - VisualBuilder.VisualBuilderGlobalState.value.previousSelectedEditableDOM = - mockElement; - VisualBuilder.VisualBuilderGlobalState.value.isFocussed = true; - - const mockExtractDetailsFromCslp = await import( - "../../../cslp" - ); - vi.mocked( - mockExtractDetailsFromCslp.extractDetailsFromCslp - ).mockReturnValue({ - content_type_uid: "test_content_type", - entry_uid: "test_entry", - locale: "en-us", - fieldPath: "test_field", - fieldPathWithIndex: "test_field", - } as any); - - vi.mocked(FieldSchemaMap.getFieldSchema).mockResolvedValue({ - test: "schema", - } as any); - vi.mocked(getFieldData).mockResolvedValue({ - test: "data", - } as any); - - await mockHandleRevalidateFieldData(); - - // Should call hideFocusOverlay to unfocus - expect(hideFocusOverlay).toHaveBeenCalledWith( - expect.objectContaining({ - visualBuilderContainer: expect.any(HTMLDivElement), - visualBuilderOverlayWrapper: expect.any(HTMLDivElement), - focusedToolbar: expect.any(HTMLDivElement), - noTrigger: true, - }) - ); - - // Should clear global state - expect( - VisualBuilder.VisualBuilderGlobalState.value - .previousSelectedEditableDOM - ).toBeNull(); - expect( - VisualBuilder.VisualBuilderGlobalState.value.isFocussed - ).toBe(false); - }); - - it("should refocus element after revalidation completes", async () => { - const mockElement = document.createElement("div"); - mockElement.setAttribute( - "data-cslp", - "content_type.entry.field" - ); - mockElement.setAttribute("data-cslp-unique-id", "unique-123"); - document.body.appendChild(mockElement); - - VisualBuilder.VisualBuilderGlobalState.value.previousSelectedEditableDOM = - mockElement; - - const mockExtractDetailsFromCslp = await import( - "../../../cslp" - ); - vi.mocked( - mockExtractDetailsFromCslp.extractDetailsFromCslp - ).mockReturnValue({ - content_type_uid: "test_content_type", - entry_uid: "test_entry", - locale: "en-us", - fieldPath: "test_field", - fieldPathWithIndex: "test_field", - } as any); - - vi.mocked(FieldSchemaMap.getFieldSchema).mockResolvedValue({ - test: "schema", - } as any); - vi.mocked(getFieldData).mockResolvedValue({ - test: "data", - } as any); - - await mockHandleRevalidateFieldData(); - - // Should refocus the element using unique ID - expect(handleBuilderInteraction).toHaveBeenCalledWith( - expect.objectContaining({ - event: expect.any(MouseEvent), - previousSelectedEditableDOM: null, - visualBuilderContainer: expect.any(HTMLDivElement), - overlayWrapper: expect.any(HTMLDivElement), - }) - ); - - document.body.removeChild(mockElement); - }); - - it("should refocus using data-cslp when unique ID is not available", async () => { - const mockElement = document.createElement("div"); - mockElement.setAttribute( - "data-cslp", - "content_type.entry.field" - ); - // No unique ID - document.body.appendChild(mockElement); - - VisualBuilder.VisualBuilderGlobalState.value.previousSelectedEditableDOM = - mockElement; - - const mockExtractDetailsFromCslp = await import( - "../../../cslp" - ); - vi.mocked( - mockExtractDetailsFromCslp.extractDetailsFromCslp - ).mockReturnValue({ - content_type_uid: "test_content_type", - entry_uid: "test_entry", - locale: "en-us", - fieldPath: "test_field", - fieldPathWithIndex: "test_field", - } as any); - - vi.mocked(FieldSchemaMap.getFieldSchema).mockResolvedValue({ - test: "schema", - } as any); - vi.mocked(getFieldData).mockResolvedValue({ - test: "data", - } as any); - - await mockHandleRevalidateFieldData(); - - // Should still call handleBuilderInteraction - expect(handleBuilderInteraction).toHaveBeenCalled(); - - document.body.removeChild(mockElement); - }); - - it("should not refocus if element cannot be found after revalidation", async () => { - const mockElement = document.createElement("div"); - mockElement.setAttribute( - "data-cslp", - "content_type.entry.field" - ); - // Don't append to DOM - element won't be found - - VisualBuilder.VisualBuilderGlobalState.value.previousSelectedEditableDOM = - mockElement; - - const mockExtractDetailsFromCslp = await import( - "../../../cslp" - ); - vi.mocked( - mockExtractDetailsFromCslp.extractDetailsFromCslp - ).mockReturnValue({ - content_type_uid: "test_content_type", - entry_uid: "test_entry", - locale: "en-us", - fieldPath: "test_field", - fieldPathWithIndex: "test_field", - } as any); - - vi.mocked(FieldSchemaMap.getFieldSchema).mockResolvedValue({ - test: "schema", - } as any); - vi.mocked(getFieldData).mockResolvedValue({ - test: "data", - } as any); - - await mockHandleRevalidateFieldData(); - - // Should not call handleBuilderInteraction if element not found - expect(handleBuilderInteraction).not.toHaveBeenCalled(); - }); - - it("should not unfocus or refocus when no element is focused", async () => { - VisualBuilder.VisualBuilderGlobalState.value.previousSelectedEditableDOM = - null; - - await mockHandleRevalidateFieldData(); - - // Should not call unfocus/refocus functions - expect(hideFocusOverlay).not.toHaveBeenCalled(); - expect(handleBuilderInteraction).not.toHaveBeenCalled(); - - // Should still clear cache - expect(FieldSchemaMap.clear).toHaveBeenCalled(); - }); - - it("should handle refocus errors gracefully", async () => { - const mockElement = document.createElement("div"); - mockElement.setAttribute( - "data-cslp", - "content_type.entry.field" - ); - document.body.appendChild(mockElement); - - VisualBuilder.VisualBuilderGlobalState.value.previousSelectedEditableDOM = - mockElement; - - const mockExtractDetailsFromCslp = await import( - "../../../cslp" - ); - vi.mocked( - mockExtractDetailsFromCslp.extractDetailsFromCslp - ).mockReturnValue({ - content_type_uid: "test_content_type", - entry_uid: "test_entry", - locale: "en-us", - fieldPath: "test_field", - fieldPathWithIndex: "test_field", - } as any); - - vi.mocked(FieldSchemaMap.getFieldSchema).mockResolvedValue({ - test: "schema", - } as any); - vi.mocked(getFieldData).mockResolvedValue({ - test: "data", - } as any); - - // Make handleBuilderInteraction throw an error - handleBuilderInteraction.mockRejectedValueOnce( - new Error("Refocus failed") - ); - - // Should not throw - error should be caught and logged - await expect( - mockHandleRevalidateFieldData() - ).resolves.not.toThrow(); - - document.body.removeChild(mockElement); - }); + document.body.removeChild(mockElement); }); }); }); diff --git a/src/visualBuilder/eventManager/useRevalidateFieldDataPostMessageEvent.ts b/src/visualBuilder/eventManager/useRevalidateFieldDataPostMessageEvent.ts index 6656422d..e3986831 100644 --- a/src/visualBuilder/eventManager/useRevalidateFieldDataPostMessageEvent.ts +++ b/src/visualBuilder/eventManager/useRevalidateFieldDataPostMessageEvent.ts @@ -1,9 +1,6 @@ import { VisualBuilder } from ".."; import { extractDetailsFromCslp } from "../../cslp"; import { FieldSchemaMap } from "../utils/fieldSchemaMap"; -import { getFieldData } from "../utils/getFieldData"; -import visualBuilderPostMessage from "../utils/visualBuilderPostMessage"; -import { VisualBuilderPostMessageEvents } from "../utils/types/postMessage.types"; import { hideFocusOverlay } from "../generators/generateOverlay"; import { handleBuilderInteraction } from "../listeners/mouseClick"; @@ -11,7 +8,7 @@ import { handleBuilderInteraction } from "../listeners/mouseClick"; * Revalidates field data and schema after variant linking operations. * Unfocuses the selected element, revalidates data, and then reselects it. */ -async function handleRevalidateFieldData(): Promise { +export async function handleRevalidateFieldData(): Promise { const focusedElement = VisualBuilder.VisualBuilderGlobalState.value .previousSelectedEditableDOM; @@ -165,10 +162,3 @@ function getVisualBuilderElements() { ) as HTMLDivElement | null, }; } - -export function useRevalidateFieldDataPostMessageEvent(): void { - visualBuilderPostMessage?.on( - VisualBuilderPostMessageEvents.REVALIDATE_FIELD_DATA, - handleRevalidateFieldData - ); -} diff --git a/src/visualBuilder/index.ts b/src/visualBuilder/index.ts index 2782cff2..062140c1 100644 --- a/src/visualBuilder/index.ts +++ b/src/visualBuilder/index.ts @@ -25,7 +25,6 @@ import { extractDetailsFromCslp } from "../cslp"; import initUI from "./components"; import { useDraftFieldsPostMessageEvent } from "./eventManager/useDraftFieldsPostMessageEvent"; import { useHideFocusOverlayPostMessageEvent } from "./eventManager/useHideFocusOverlayPostMessageEvent"; -import { useRevalidateFieldDataPostMessageEvent } from "./eventManager/useRevalidateFieldDataPostMessageEvent"; import { useScrollToField } from "./eventManager/useScrollToField"; import { useVariantFieldsPostMessageEvent } from "./eventManager/useVariantsPostMessageEvent"; import { @@ -397,7 +396,6 @@ export class VisualBuilder { useRecalculateVariantDataCSLPValues(); useDraftFieldsPostMessageEvent(); useVariantFieldsPostMessageEvent(); - useRevalidateFieldDataPostMessageEvent(); } }) .catch(() => { diff --git a/src/visualBuilder/utils/types/postMessage.types.ts b/src/visualBuilder/utils/types/postMessage.types.ts index e401f36b..9228d30d 100644 --- a/src/visualBuilder/utils/types/postMessage.types.ts +++ b/src/visualBuilder/utils/types/postMessage.types.ts @@ -9,7 +9,6 @@ export enum VisualBuilderPostMessageEvents { TOGGLE_FORM = "toggle-quick-form", GET_FIELD_SCHEMA = "get-field-schema", GET_FIELD_DATA = "get-field-data", - REVALIDATE_FIELD_DATA = "revalidate-field-data", OPEN_LINK_VARIANT_MODAL = "open-link-variant-modal", GET_FIELD_PATH_WITH_UID = "get-field-path-with-uid", GET_FIELD_DISPLAY_NAMES = "get-field-display-names", From cb98dad6f3ec36ae7d30978068741a69970eaa90 Mon Sep 17 00:00:00 2001 From: Karan Gandhi Date: Mon, 3 Nov 2025 10:50:31 +0530 Subject: [PATCH 07/19] fix: requested changes done --- src/visualBuilder/components/fieldLabelWrapper.tsx | 2 +- .../useRevalidateFieldDataPostMessageEvent.ts | 5 ++--- src/visualBuilder/index.ts | 3 +++ src/visualBuilder/visualBuilder.style.ts | 8 ++++---- 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/visualBuilder/components/fieldLabelWrapper.tsx b/src/visualBuilder/components/fieldLabelWrapper.tsx index 99315f3f..a57edaa1 100644 --- a/src/visualBuilder/components/fieldLabelWrapper.tsx +++ b/src/visualBuilder/components/fieldLabelWrapper.tsx @@ -236,7 +236,7 @@ function FieldLabelWrapperComponent( > {(() => { const [before, after] = reason.split( - /here/i + "here" ); return ( <> diff --git a/src/visualBuilder/eventManager/useRevalidateFieldDataPostMessageEvent.ts b/src/visualBuilder/eventManager/useRevalidateFieldDataPostMessageEvent.ts index e3986831..13096194 100644 --- a/src/visualBuilder/eventManager/useRevalidateFieldDataPostMessageEvent.ts +++ b/src/visualBuilder/eventManager/useRevalidateFieldDataPostMessageEvent.ts @@ -57,10 +57,9 @@ export async function handleRevalidateFieldData(): Promise { return; } catch (clearError) { console.error("Failed to clear field schema cache:", clearError); + // Fallback 2: Refresh the entire iframe + window.location.reload(); } - - // Fallback 2: Refresh the entire iframe - window.location.reload(); } catch (error) { console.error("Error handling revalidate field data:", error); // Final fallback - refresh the page diff --git a/src/visualBuilder/index.ts b/src/visualBuilder/index.ts index 062140c1..da5992f3 100644 --- a/src/visualBuilder/index.ts +++ b/src/visualBuilder/index.ts @@ -380,10 +380,13 @@ export class VisualBuilder { document.body.style.overflow = "hidden"; } else { document.body.style.overflow = "auto"; + } } ); + + useHideFocusOverlayPostMessageEvent({ overlayWrapper: this.overlayWrapper, visualBuilderContainer: this.visualBuilderContainer, diff --git a/src/visualBuilder/visualBuilder.style.ts b/src/visualBuilder/visualBuilder.style.ts index 0a2530f2..3787eb0b 100644 --- a/src/visualBuilder/visualBuilder.style.ts +++ b/src/visualBuilder/visualBuilder.style.ts @@ -300,10 +300,10 @@ export function visualBuilderStyles() { border-style: solid; align-content: center; text-align: center; - border-color: #bd59fa; + border-color: #BD59FA; svg { - color: #bd59fa; + color: #BD59FA; } `, "visual-builder__focused-toolbar": css` @@ -532,7 +532,7 @@ export function visualBuilderStyles() { `, "visual-builder__focused-toolbar--variant": css` .visual-builder__focused-toolbar__field-label-wrapper__current-field { - background: #bd59fa; + background: #BD59FA; } `, "visual-builder__cursor-disabled": css` @@ -665,7 +665,7 @@ export function visualBuilderStyles() { outline: 2px dashed #909090; `, "visual-builder__hover-outline--variant": css` - outline: 2px dashed #bd59fa; + outline: 2px dashed #BD59FA; `, "visual-builder__default-cursor--disabled": css` cursor: none; From 1ba39e67cbd0b47d18f08cd9e1b8ed7e7ec8146b Mon Sep 17 00:00:00 2001 From: Karan Gandhi Date: Mon, 3 Nov 2025 14:49:17 +0530 Subject: [PATCH 08/19] fix: response of postmessage changed --- .../components/fieldLabelWrapper.tsx | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/visualBuilder/components/fieldLabelWrapper.tsx b/src/visualBuilder/components/fieldLabelWrapper.tsx index a57edaa1..312fb0d0 100644 --- a/src/visualBuilder/components/fieldLabelWrapper.tsx +++ b/src/visualBuilder/components/fieldLabelWrapper.tsx @@ -176,10 +176,8 @@ function FieldLabelWrapperComponent( if (fieldSchema.field_metadata?.canLinkVariant) { try { const result = await visualBuilderPostMessage?.send<{ - success?: boolean; - action?: string; - message?: string; - error?: boolean; + type: "success" | "error"; + message: string; }>( VisualBuilderPostMessageEvents.OPEN_LINK_VARIANT_MODAL, { @@ -187,20 +185,21 @@ function FieldLabelWrapperComponent( props.fieldMetadata.content_type_uid, } ); + console.log("result", result); // If the modal was closed or linking failed, do nothing - if (!result || !result.success) { + if (!result || result.type === "error") { return; } // If linking was successful and requires revalidation, revalidate - if (result.action === "revalidate") { + if (result.type === "success" ) { await handleRevalidateFieldData(); } - } catch (error) { + } catch (message) { console.error( "Error in link variant modal flow:", - error + message ); } } From 13cb10f3bc38f38c4a5ee2acf59a5a0ceffbb542 Mon Sep 17 00:00:00 2001 From: Karan Gandhi Date: Mon, 3 Nov 2025 15:51:27 +0530 Subject: [PATCH 09/19] fix: remove console --- .../components/fieldLabelWrapper.tsx | 153 ++++++++++++------ 1 file changed, 105 insertions(+), 48 deletions(-) diff --git a/src/visualBuilder/components/fieldLabelWrapper.tsx b/src/visualBuilder/components/fieldLabelWrapper.tsx index 312fb0d0..32ac5a3a 100644 --- a/src/visualBuilder/components/fieldLabelWrapper.tsx +++ b/src/visualBuilder/components/fieldLabelWrapper.tsx @@ -8,7 +8,10 @@ import { isFieldDisabled } from "../utils/isFieldDisabled"; import visualBuilderPostMessage from "../utils/visualBuilderPostMessage"; import { CaretIcon, CaretRightIcon, InfoIcon } from "./icons"; import { LoadingIcon } from "./icons/loading"; -import { FieldTypeIconsMap, getFieldIcon } from "../generators/generateCustomCursor"; +import { + FieldTypeIconsMap, + getFieldIcon, +} from "../generators/generateCustomCursor"; import { uniqBy } from "lodash-es"; import { visualBuilderStyles } from "../visualBuilder.style"; import { CslpError } from "./CslpError"; @@ -25,7 +28,7 @@ interface ReferenceParentMap { contentTypeUid: string; contentTypeTitle: string; referenceFieldName: string; - }[] + }[]; } async function getFieldDisplayNames(fieldMetadata: CslpData[]) { @@ -43,18 +46,28 @@ async function getContentTypeName(contentTypeUid: string) { content_type_uid: contentTypeUid, }); return result?.contentTypeName; - } catch(e) { - console.warn("[getFieldLabelWrapper] Error getting content type name", e); + } catch (e) { + console.warn( + "[getFieldLabelWrapper] Error getting content type name", + e + ); return ""; } } async function getReferenceParentMap() { try { - const result = await visualBuilderPostMessage?.send(VisualBuilderPostMessageEvents.REFERENCE_MAP, {}) ?? {}; + const result = + (await visualBuilderPostMessage?.send( + VisualBuilderPostMessageEvents.REFERENCE_MAP, + {} + )) ?? {}; return result; - } catch(e) { - console.warn("[getFieldLabelWrapper] Error getting reference parent map", e); + } catch (e) { + console.warn( + "[getFieldLabelWrapper] Error getting reference parent map", + e + ); return {}; } } @@ -118,35 +131,53 @@ function FieldLabelWrapperComponent( ], "cslpValue" ); - const [displayNames, fieldSchema, contentTypeName, referenceParentMap] = await Promise.all([ + const [ + displayNames, + fieldSchema, + contentTypeName, + referenceParentMap, + ] = await Promise.all([ getFieldDisplayNames(allPaths), FieldSchemaMap.getFieldSchema( props.fieldMetadata.content_type_uid, props.fieldMetadata.fieldPath ), - getContentTypeName( - props.fieldMetadata.content_type_uid - ), - getReferenceParentMap() + getContentTypeName(props.fieldMetadata.content_type_uid), + getReferenceParentMap(), ]); const entryUid = props.fieldMetadata.entry_uid; const referenceData = referenceParentMap[entryUid]; const isReference = !!referenceData; - let referenceFieldName = referenceData ? referenceData[0].referenceFieldName : ""; - let parentContentTypeName = referenceData ? referenceData[0].contentTypeTitle : ""; + let referenceFieldName = referenceData + ? referenceData[0].referenceFieldName + : ""; + let parentContentTypeName = referenceData + ? referenceData[0].contentTypeTitle + : ""; - if(isReference) { - const domAncestor = eventDetails.editableElement.closest(`[data-cslp]:not([data-cslp^="${props.fieldMetadata.content_type_uid}"])`); - if(domAncestor) { - const domAncestorCslp = domAncestor.getAttribute("data-cslp"); - const domAncestorDetails = extractDetailsFromCslp(domAncestorCslp!); - const domAncestorContentTypeUid = domAncestorDetails.content_type_uid; - const domAncestorContentParent = referenceData?.find(data => data.contentTypeUid === domAncestorContentTypeUid); - if(domAncestorContentParent) { - referenceFieldName = domAncestorContentParent.referenceFieldName; - parentContentTypeName = domAncestorContentParent.contentTypeTitle; + if (isReference) { + const domAncestor = eventDetails.editableElement.closest( + `[data-cslp]:not([data-cslp^="${props.fieldMetadata.content_type_uid}"])` + ); + if (domAncestor) { + const domAncestorCslp = + domAncestor.getAttribute("data-cslp"); + const domAncestorDetails = extractDetailsFromCslp( + domAncestorCslp! + ); + const domAncestorContentTypeUid = + domAncestorDetails.content_type_uid; + const domAncestorContentParent = referenceData?.find( + (data) => + data.contentTypeUid === domAncestorContentTypeUid + ); + if (domAncestorContentParent) { + referenceFieldName = + domAncestorContentParent.referenceFieldName; + parentContentTypeName = + domAncestorContentParent.contentTypeTitle; } } } @@ -185,7 +216,6 @@ function FieldLabelWrapperComponent( props.fieldMetadata.content_type_uid, } ); - console.log("result", result); // If the modal was closed or linking failed, do nothing if (!result || result.type === "error") { @@ -193,7 +223,7 @@ function FieldLabelWrapperComponent( } // If linking was successful and requires revalidation, revalidate - if (result.type === "success" ) { + if (result.type === "success") { await handleRevalidateFieldData(); } } catch (message) { @@ -222,25 +252,38 @@ function FieldLabelWrapperComponent( "visual-builder__tooltip--persistent" ] )} - data-tooltip={!reason?.toLowerCase().includes("click here to link a variant") + data-tooltip={ + !reason + ?.toLowerCase() + .includes("click here to link a variant") ? reason - : undefined} + : undefined + } > {reason .toLowerCase() .includes("click here to link a variant") && (
{(() => { - const [before, after] = reason.split( - "here" - ); + const [before, after] = + reason.split("here"); return ( <> {before} - here + + here + {after} ); @@ -272,8 +315,11 @@ function FieldLabelWrapperComponent( try { fetchData(); - } catch(e) { - console.warn("[getFieldLabelWrapper] Error fetching field label data", e); + } catch (e) { + console.warn( + "[getFieldLabelWrapper] Error fetching field label data", + e + ); } }, [props]); @@ -305,7 +351,13 @@ function FieldLabelWrapperComponent( )} > {currentField.isVariant ? : null} - +
- { - currentField.isReference && !dataLoading && !error ? + {currentField.isReference && !dataLoading && !error ? (
-
: null - } - { - currentField.contentTypeName && !dataLoading && !error ? +
+ ) : null} + {currentField.contentTypeName && + !dataLoading && + !error ? ( <>
{currentField.contentTypeName + " : "}
- : null - } + + ) : null} {currentField.prefixIcon ? (
Date: Mon, 3 Nov 2025 15:54:00 +0530 Subject: [PATCH 10/19] fix: formatting removed --- .../components/fieldLabelWrapper.tsx | 150 ++++++------------ 1 file changed, 46 insertions(+), 104 deletions(-) diff --git a/src/visualBuilder/components/fieldLabelWrapper.tsx b/src/visualBuilder/components/fieldLabelWrapper.tsx index 32ac5a3a..558f7f99 100644 --- a/src/visualBuilder/components/fieldLabelWrapper.tsx +++ b/src/visualBuilder/components/fieldLabelWrapper.tsx @@ -8,10 +8,7 @@ import { isFieldDisabled } from "../utils/isFieldDisabled"; import visualBuilderPostMessage from "../utils/visualBuilderPostMessage"; import { CaretIcon, CaretRightIcon, InfoIcon } from "./icons"; import { LoadingIcon } from "./icons/loading"; -import { - FieldTypeIconsMap, - getFieldIcon, -} from "../generators/generateCustomCursor"; +import { FieldTypeIconsMap, getFieldIcon } from "../generators/generateCustomCursor"; import { uniqBy } from "lodash-es"; import { visualBuilderStyles } from "../visualBuilder.style"; import { CslpError } from "./CslpError"; @@ -28,7 +25,7 @@ interface ReferenceParentMap { contentTypeUid: string; contentTypeTitle: string; referenceFieldName: string; - }[]; + }[] } async function getFieldDisplayNames(fieldMetadata: CslpData[]) { @@ -46,28 +43,18 @@ async function getContentTypeName(contentTypeUid: string) { content_type_uid: contentTypeUid, }); return result?.contentTypeName; - } catch (e) { - console.warn( - "[getFieldLabelWrapper] Error getting content type name", - e - ); + } catch(e) { + console.warn("[getFieldLabelWrapper] Error getting content type name", e); return ""; } } async function getReferenceParentMap() { try { - const result = - (await visualBuilderPostMessage?.send( - VisualBuilderPostMessageEvents.REFERENCE_MAP, - {} - )) ?? {}; + const result = await visualBuilderPostMessage?.send(VisualBuilderPostMessageEvents.REFERENCE_MAP, {}) ?? {}; return result; - } catch (e) { - console.warn( - "[getFieldLabelWrapper] Error getting reference parent map", - e - ); + } catch(e) { + console.warn("[getFieldLabelWrapper] Error getting reference parent map", e); return {}; } } @@ -131,53 +118,35 @@ function FieldLabelWrapperComponent( ], "cslpValue" ); - const [ - displayNames, - fieldSchema, - contentTypeName, - referenceParentMap, - ] = await Promise.all([ + const [displayNames, fieldSchema, contentTypeName, referenceParentMap] = await Promise.all([ getFieldDisplayNames(allPaths), FieldSchemaMap.getFieldSchema( props.fieldMetadata.content_type_uid, props.fieldMetadata.fieldPath ), - getContentTypeName(props.fieldMetadata.content_type_uid), - getReferenceParentMap(), + getContentTypeName( + props.fieldMetadata.content_type_uid + ), + getReferenceParentMap() ]); const entryUid = props.fieldMetadata.entry_uid; const referenceData = referenceParentMap[entryUid]; const isReference = !!referenceData; - let referenceFieldName = referenceData - ? referenceData[0].referenceFieldName - : ""; - let parentContentTypeName = referenceData - ? referenceData[0].contentTypeTitle - : ""; + let referenceFieldName = referenceData ? referenceData[0].referenceFieldName : ""; + let parentContentTypeName = referenceData ? referenceData[0].contentTypeTitle : ""; - if (isReference) { - const domAncestor = eventDetails.editableElement.closest( - `[data-cslp]:not([data-cslp^="${props.fieldMetadata.content_type_uid}"])` - ); - if (domAncestor) { - const domAncestorCslp = - domAncestor.getAttribute("data-cslp"); - const domAncestorDetails = extractDetailsFromCslp( - domAncestorCslp! - ); - const domAncestorContentTypeUid = - domAncestorDetails.content_type_uid; - const domAncestorContentParent = referenceData?.find( - (data) => - data.contentTypeUid === domAncestorContentTypeUid - ); - if (domAncestorContentParent) { - referenceFieldName = - domAncestorContentParent.referenceFieldName; - parentContentTypeName = - domAncestorContentParent.contentTypeTitle; + if(isReference) { + const domAncestor = eventDetails.editableElement.closest(`[data-cslp]:not([data-cslp^="${props.fieldMetadata.content_type_uid}"])`); + if(domAncestor) { + const domAncestorCslp = domAncestor.getAttribute("data-cslp"); + const domAncestorDetails = extractDetailsFromCslp(domAncestorCslp!); + const domAncestorContentTypeUid = domAncestorDetails.content_type_uid; + const domAncestorContentParent = referenceData?.find(data => data.contentTypeUid === domAncestorContentTypeUid); + if(domAncestorContentParent) { + referenceFieldName = domAncestorContentParent.referenceFieldName; + parentContentTypeName = domAncestorContentParent.contentTypeTitle; } } } @@ -252,38 +221,25 @@ function FieldLabelWrapperComponent( "visual-builder__tooltip--persistent" ] )} - data-tooltip={ - !reason - ?.toLowerCase() - .includes("click here to link a variant") + data-tooltip={!reason?.toLowerCase().includes("click here to link a variant") ? reason - : undefined - } + : undefined} > {reason .toLowerCase() .includes("click here to link a variant") && (
{(() => { - const [before, after] = - reason.split("here"); + const [before, after] = reason.split( + "here" + ); return ( <> {before} - - here - + here {after} ); @@ -315,11 +271,8 @@ function FieldLabelWrapperComponent( try { fetchData(); - } catch (e) { - console.warn( - "[getFieldLabelWrapper] Error fetching field label data", - e - ); + } catch(e) { + console.warn("[getFieldLabelWrapper] Error fetching field label data", e); } }, [props]); @@ -351,13 +304,7 @@ function FieldLabelWrapperComponent( )} > {currentField.isVariant ? : null} - +
- {currentField.isReference && !dataLoading && !error ? ( + { + currentField.isReference && !dataLoading && !error ?
-
- ) : null} - {currentField.contentTypeName && - !dataLoading && - !error ? ( +
: null + } + { + currentField.contentTypeName && !dataLoading && !error ? <>
{currentField.contentTypeName + " : "}
- - ) : null} + : null + } {currentField.prefixIcon ? (
Date: Fri, 7 Nov 2025 18:16:03 +0530 Subject: [PATCH 11/19] fix: changes requested --- .../__test__/fieldLabelWrapper.test.tsx | 49 +++++++++-------- .../components/fieldLabelWrapper.tsx | 31 +++++------ ...evalidateFieldDataPostMessageEvent.test.ts | 17 +++--- .../useRevalidateFieldDataPostMessageEvent.ts | 25 ++------- .../utils/__test__/isFieldDisabled.test.ts | 54 +++++++++---------- src/visualBuilder/utils/constants.ts | 5 ++ src/visualBuilder/utils/isFieldDisabled.ts | 15 +++--- 7 files changed, 94 insertions(+), 102 deletions(-) diff --git a/src/visualBuilder/components/__test__/fieldLabelWrapper.test.tsx b/src/visualBuilder/components/__test__/fieldLabelWrapper.test.tsx index 6849aafc..8be0646a 100644 --- a/src/visualBuilder/components/__test__/fieldLabelWrapper.test.tsx +++ b/src/visualBuilder/components/__test__/fieldLabelWrapper.test.tsx @@ -83,9 +83,14 @@ vi.mock("../../utils/visualBuilderPostMessage", () => ({ }, })); -vi.mock("../../utils/isFieldDisabled", () => ({ - isFieldDisabled: vi.fn().mockReturnValue({ isDisabled: false }), -})); +vi.mock("../../utils/isFieldDisabled", async (importOriginal) => { + const actual = + await importOriginal(); + return { + ...actual, + isFieldDisabled: vi.fn().mockReturnValue({ isDisabled: false }), + }; +}); vi.mock("../../../cslp", () => ({ extractDetailsFromCslp: vi.fn().mockImplementation((path) => { @@ -523,31 +528,31 @@ describe("FieldLabelWrapperComponent", () => { "visual-builder__focused-toolbar--variant" ); }); - }); - describe("variant linking click condition", () => { - test("should allow modal opening when canLinkVariant is true", () => { - // Test the actual click condition logic without rendering - const canLinkVariant = true; - const shouldOpenModal = !!canLinkVariant; + describe("variant linking click condition", () => { + test("should allow modal opening when canLinkVariant is true", () => { + // Test the actual click condition logic without rendering + const canLinkVariant = true; + const shouldOpenModal = !!canLinkVariant; - expect(shouldOpenModal).toBe(true); - }); + expect(shouldOpenModal).toBe(true); + }); - test("should not allow modal opening when canLinkVariant is false", () => { - // Test the actual click condition logic without rendering - const canLinkVariant = false; - const shouldOpenModal = !!canLinkVariant; + test("should not allow modal opening when canLinkVariant is false", () => { + // Test the actual click condition logic without rendering + const canLinkVariant = false; + const shouldOpenModal = !!canLinkVariant; - expect(shouldOpenModal).toBe(false); - }); + expect(shouldOpenModal).toBe(false); + }); - test("should not allow modal opening when canLinkVariant is undefined", () => { - // Test the actual click condition logic without rendering - const canLinkVariant = undefined; - const shouldOpenModal = !!canLinkVariant; + test("should not allow modal opening when canLinkVariant is undefined", () => { + // Test the actual click condition logic without rendering + const canLinkVariant = undefined; + const shouldOpenModal = !!canLinkVariant; - expect(shouldOpenModal).toBe(false); + expect(shouldOpenModal).toBe(false); + }); }); }); }); diff --git a/src/visualBuilder/components/fieldLabelWrapper.tsx b/src/visualBuilder/components/fieldLabelWrapper.tsx index 558f7f99..01e5c6c1 100644 --- a/src/visualBuilder/components/fieldLabelWrapper.tsx +++ b/src/visualBuilder/components/fieldLabelWrapper.tsx @@ -4,7 +4,7 @@ import { extractDetailsFromCslp } from "../../cslp"; import { CslpData } from "../../cslp/types/cslp.types"; import { VisualBuilderCslpEventDetails } from "../types/visualBuilder.types"; import { FieldSchemaMap } from "../utils/fieldSchemaMap"; -import { isFieldDisabled } from "../utils/isFieldDisabled"; +import { DisableReason, isFieldDisabled } from "../utils/isFieldDisabled"; import visualBuilderPostMessage from "../utils/visualBuilderPostMessage"; import { CaretIcon, CaretRightIcon, InfoIcon } from "./icons"; import { LoadingIcon } from "./icons/loading"; @@ -19,6 +19,7 @@ import { ToolbarTooltip } from "./Tooltip"; import { fetchEntryPermissionsAndStageDetails } from "../utils/fetchEntryPermissionsAndStageDetails"; import { VariantIndicator } from "./VariantIndicator"; import { handleRevalidateFieldData } from "../eventManager/useRevalidateFieldDataPostMessageEvent"; +import { RESULT_TYPES } from "../utils/constants"; interface ReferenceParentMap { [entryUid: string]: { @@ -173,10 +174,10 @@ function FieldLabelWrapperComponent( ); const handleLinkVariant = async () => { - if (fieldSchema.field_metadata?.canLinkVariant) { - try { + try { + if (fieldSchema.field_metadata?.canLinkVariant) { const result = await visualBuilderPostMessage?.send<{ - type: "success" | "error"; + type: typeof RESULT_TYPES.SUCCESS | typeof RESULT_TYPES.ERROR; message: string; }>( VisualBuilderPostMessageEvents.OPEN_LINK_VARIANT_MODAL, @@ -187,20 +188,20 @@ function FieldLabelWrapperComponent( ); // If the modal was closed or linking failed, do nothing - if (!result || result.type === "error") { + if (!result || result.type === RESULT_TYPES.ERROR) { return; } // If linking was successful and requires revalidation, revalidate - if (result.type === "success") { + if (result.type === RESULT_TYPES.SUCCESS) { await handleRevalidateFieldData(); } - } catch (message) { - console.error( - "Error in link variant modal flow:", - message - ); } + } catch (error) { + console.error( + "Error in link variant modal flow:", + error + ); } }; @@ -221,25 +222,25 @@ function FieldLabelWrapperComponent( "visual-builder__tooltip--persistent" ] )} - data-tooltip={!reason?.toLowerCase().includes("click here to link a variant") + data-tooltip={!reason?.toLowerCase().includes(DisableReason.CanLinkVariant) ? reason : undefined} > {reason .toLowerCase() - .includes("click here to link a variant") && ( + .includes(DisableReason.CanLinkVariant) && (
{(() => { const [before, after] = reason.split( - "here" + DisableReason.SplitOn ); return ( <> {before} - here + {DisableReason.SplitOn} {after} ); diff --git a/src/visualBuilder/eventManager/__test__/useRevalidateFieldDataPostMessageEvent.test.ts b/src/visualBuilder/eventManager/__test__/useRevalidateFieldDataPostMessageEvent.test.ts index 3cccc5d2..0d41da73 100644 --- a/src/visualBuilder/eventManager/__test__/useRevalidateFieldDataPostMessageEvent.test.ts +++ b/src/visualBuilder/eventManager/__test__/useRevalidateFieldDataPostMessageEvent.test.ts @@ -42,10 +42,8 @@ describe("handleRevalidateFieldData", () => { let overlayWrapper: HTMLDivElement; let focusedToolbar: HTMLDivElement; - beforeEach(() => { - vi.clearAllMocks(); - - // Create DOM elements + // Create DOM elements once for all tests (optimization) + beforeAll(() => { visualBuilderContainer = document.createElement("div"); visualBuilderContainer.classList.add("visual-builder__container"); overlayWrapper = document.createElement("div"); @@ -56,6 +54,11 @@ describe("handleRevalidateFieldData", () => { document.body.appendChild(visualBuilderContainer); document.body.appendChild(overlayWrapper); document.body.appendChild(focusedToolbar); + }); + + beforeEach(() => { + // Clear mocks before each test to ensure clean state + vi.clearAllMocks(); // Reset VisualBuilder global state VisualBuilder.VisualBuilderGlobalState = { @@ -68,7 +71,8 @@ describe("handleRevalidateFieldData", () => { }; }); - afterEach(() => { + // Clean up DOM after all tests complete + afterAll(() => { document.body.innerHTML = ""; }); @@ -195,7 +199,8 @@ describe("handleRevalidateFieldData", () => { let hideFocusOverlay: any; let handleBuilderInteraction: any; - beforeEach(async () => { + // Import mocked modules once for all tests in this describe block + beforeAll(async () => { const overlayModule = await import( "../../generators/generateOverlay" ); diff --git a/src/visualBuilder/eventManager/useRevalidateFieldDataPostMessageEvent.ts b/src/visualBuilder/eventManager/useRevalidateFieldDataPostMessageEvent.ts index 13096194..d984333a 100644 --- a/src/visualBuilder/eventManager/useRevalidateFieldDataPostMessageEvent.ts +++ b/src/visualBuilder/eventManager/useRevalidateFieldDataPostMessageEvent.ts @@ -36,30 +36,15 @@ export async function handleRevalidateFieldData(): Promise { const fieldMetadata = extractDetailsFromCslp(cslp); // Try to revalidate specific field schema and data - try { - // Clear the entire content type schema from cache to force fresh fetch - FieldSchemaMap.clearContentTypeSchema( - fieldMetadata.content_type_uid - ); - } catch (fieldError) { - console.warn( - "Failed to revalidate content type:", - fieldMetadata.content_type_uid, - fieldError - ); - } + // Clear the entire content type schema from cache to force fresh fetch + FieldSchemaMap.clearContentTypeSchema( + fieldMetadata.content_type_uid + ); } } // Fallback 1: Clear all field schema cache - try { - FieldSchemaMap.clear(); - return; - } catch (clearError) { - console.error("Failed to clear field schema cache:", clearError); - // Fallback 2: Refresh the entire iframe - window.location.reload(); - } + FieldSchemaMap.clear(); } catch (error) { console.error("Error handling revalidate field data:", error); // Final fallback - refresh the page diff --git a/src/visualBuilder/utils/__test__/isFieldDisabled.test.ts b/src/visualBuilder/utils/__test__/isFieldDisabled.test.ts index 331bc71f..22ee0d91 100644 --- a/src/visualBuilder/utils/__test__/isFieldDisabled.test.ts +++ b/src/visualBuilder/utils/__test__/isFieldDisabled.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from "vitest"; -import { isFieldDisabled } from "../isFieldDisabled"; +import { isFieldDisabled, DisableReason } from "../isFieldDisabled"; import { ISchemaFieldMap } from "../types/index.types"; import { FieldDetails } from "../../components/FieldToolbar"; import Config from "../../../configManager/configManager"; @@ -24,7 +24,7 @@ describe("isFieldDisabled", () => { const result = isFieldDisabled(fieldSchemaMap, eventFieldDetails); expect(result.isDisabled).toBe(true); - expect(result.reason).toBe("You have only read access to this field"); + expect(result.reason).toBe(DisableReason.ReadOnly); }); it("should return disabled state due to non-localizable fields", () => { @@ -49,9 +49,7 @@ describe("isFieldDisabled", () => { const result = isFieldDisabled(fieldSchemaMap, eventFieldDetails); expect(result.isDisabled).toBe(true); - expect(result.reason).toBe( - "Editing this field is restricted in localized entries" - ); + expect(result.reason).toBe(DisableReason.LocalizedEntry); }); it("should return disabled state due to unlinked variant", () => { @@ -72,7 +70,7 @@ describe("isFieldDisabled", () => { const result = isFieldDisabled(fieldSchemaMap, eventFieldDetails); expect(result.isDisabled).toBe(true); expect(result.reason).toBe( - "This field is not editable as it is not linked to the selected variant. Contact your stack admin or owner to link it." + `${DisableReason.UnlinkedVariant} ${DisableReason.CannotLinkVariant}` ); }); @@ -95,7 +93,7 @@ describe("isFieldDisabled", () => { const result = isFieldDisabled(fieldSchemaMap, eventFieldDetails); expect(result.isDisabled).toBe(true); expect(result.reason).toBe( - "This field is not editable as it is not linked to the selected variant. Click here to link a variant" + `${DisableReason.UnlinkedVariant} ${DisableReason.CanLinkVariant} ` ); }); @@ -120,9 +118,7 @@ describe("isFieldDisabled", () => { const result = isFieldDisabled(fieldSchemaMap, eventFieldDetails); expect(result.isDisabled).toBe(true); - expect(result.reason).toBe( - "This field is not editable as it is not localized" - ); + expect(result.reason).toBe(DisableReason.UnlocalizedVariant); }); it("should return disabled state due to audience mode", () => { @@ -145,9 +141,7 @@ describe("isFieldDisabled", () => { const result = isFieldDisabled(fieldSchemaMap, eventFieldDetails); expect(result.isDisabled).toBe(true); - expect(result.reason).toBe( - "To edit an experience, open the Audience widget and click the Edit icon." - ); + expect(result.reason).toBe(DisableReason.AudienceMode); }); it("should return disabled state due to disabled variant", () => { @@ -173,9 +167,7 @@ describe("isFieldDisabled", () => { const result = isFieldDisabled(fieldSchemaMap, eventFieldDetails); expect(result.isDisabled).toBe(true); - expect(result.reason).toBe( - "This field is not editable as it doesn't match the selected variant" - ); + expect(result.reason).toBe(DisableReason.DisabledVariant); VisualBuilder.VisualBuilderGlobalState = { // @ts-expect-error mocking only required properties value: { @@ -197,7 +189,7 @@ describe("isFieldDisabled", () => { const result = isFieldDisabled(fieldSchemaMap, eventFieldDetails); expect(result.isDisabled).toBe(false); - expect(result.reason).toBe(""); + expect(result.reason).toBe(DisableReason.None); }); it("should return disabled state due to read-only role", () => { @@ -236,7 +228,7 @@ describe("isFieldDisabled", () => { const result = isFieldDisabled(fieldSchemaMap, eventFieldDetails); expect(result.isDisabled).toBe(true); - expect(result.reason).toBe("You have only read access to this field"); + expect(result.reason).toBe(DisableReason.ReadOnly); }); it("should return disabled state due to entry update restriction", () => { @@ -278,9 +270,7 @@ describe("isFieldDisabled", () => { publish: true, }); expect(result.isDisabled).toBe(true); - expect(result.reason).toBe( - "You do not have permission to edit this entry" - ); + expect(result.reason).toBe(DisableReason.EntryUpdateRestricted); }); describe("workflow stage restrictions", () => { @@ -320,7 +310,9 @@ describe("isFieldDisabled", () => { ); expect(result.isDisabled).toBe(true); expect(result.reason).toBe( - "You do not have Edit access to this entry on the 'Review Stage' workflow stage" + DisableReason.WorkflowStagePermission({ + stageName: "Review Stage", + }) ); }); @@ -360,7 +352,9 @@ describe("isFieldDisabled", () => { ); expect(result.isDisabled).toBe(true); expect(result.reason).toBe( - "Editing is restricted for your role or by the rules for the 'Final Review' stage. Contact your admin for edit access." + DisableReason.EntryUpdateRestrictedRoleAndWorkflowStage({ + stageName: "Final Review", + }) ); }); @@ -399,7 +393,7 @@ describe("isFieldDisabled", () => { workflowStageDetails ); expect(result.isDisabled).toBe(false); - expect(result.reason).toBe(""); + expect(result.reason).toBe(DisableReason.None); }); it("should handle workflow stage details with undefined stage name", () => { @@ -436,7 +430,7 @@ describe("isFieldDisabled", () => { ); expect(result.isDisabled).toBe(true); expect(result.reason).toBe( - "You do not have Edit access to this entry on the 'Unknown' workflow stage" + DisableReason.WorkflowStagePermission({ stageName: "Unknown" }) ); }); @@ -477,7 +471,9 @@ describe("isFieldDisabled", () => { ); expect(result.isDisabled).toBe(true); expect(result.reason).toBe( - "Editing is restricted for your role or by the rules for the 'Unknown' stage. Contact your admin for edit access." + DisableReason.EntryUpdateRestrictedRoleAndWorkflowStage({ + stageName: "Unknown", + }) ); }); @@ -521,9 +517,7 @@ describe("isFieldDisabled", () => { ); expect(result.isDisabled).toBe(true); // Should return read-only role message first based on getDisableReason logic - expect(result.reason).toBe( - "You have only read access to this field" - ); + expect(result.reason).toBe(DisableReason.ReadOnly); }); it("should return enabled state when no workflow stage details provided", () => { @@ -551,7 +545,7 @@ describe("isFieldDisabled", () => { undefined ); expect(result.isDisabled).toBe(false); - expect(result.reason).toBe(""); + expect(result.reason).toBe(DisableReason.None); }); }); }); diff --git a/src/visualBuilder/utils/constants.ts b/src/visualBuilder/utils/constants.ts index d445b877..4540d2bf 100644 --- a/src/visualBuilder/utils/constants.ts +++ b/src/visualBuilder/utils/constants.ts @@ -16,6 +16,11 @@ export const TOOLBAR_EDGE_BUFFER = 8; export const DATA_CSLP_ATTR_SELECTOR = "data-cslp"; +export const RESULT_TYPES = { + SUCCESS: "success", + ERROR: "error", +}; + /** * The field that can be directly modified using contenteditable=true. * This includes all text fields like title and numbers. diff --git a/src/visualBuilder/utils/isFieldDisabled.ts b/src/visualBuilder/utils/isFieldDisabled.ts index 73827408..0e4e7a35 100644 --- a/src/visualBuilder/utils/isFieldDisabled.ts +++ b/src/visualBuilder/utils/isFieldDisabled.ts @@ -5,12 +5,13 @@ import { FieldDetails } from "../components/FieldToolbar"; import { EntryPermissions } from "./getEntryPermissions"; import { WorkflowStageDetails } from "./getWorkflowStageDetails"; -const DisableReason = { +export const DisableReason = { ReadOnly: "You have only read access to this field", LocalizedEntry: "Editing this field is restricted in localized entries", UnlinkedVariant: "This field is not editable as it is not linked to the selected variant.", - CanLinkVaraint: "Click here to link a variant", + CanLinkVariant: "Click here to link a variant", + SplitOn: "here", CannotLinkVariant: "Contact your stack admin or owner to link it.", AudienceMode: "To edit an experience, open the Audience widget and click the Edit icon.", @@ -44,13 +45,9 @@ const getDisableReason = ( if (flags.updateRestrictDueToUnlocalizedVariant) return DisableReason.UnlocalizedVariant; if (flags.updateRestrictDueToUnlinkVariant) { - let reason = DisableReason.UnlinkedVariant; - if (flags.canLinkVariant) { - reason += ` ${DisableReason.CanLinkVaraint}`; - } else { - reason += ` ${DisableReason.CannotLinkVariant}`; - } - return reason; + return flags.canLinkVariant + ? `${DisableReason.UnlinkedVariant} ${DisableReason.CanLinkVariant} ` + : `${DisableReason.UnlinkedVariant} ${DisableReason.CannotLinkVariant}`; } if (flags.updateRestrictDueToAudienceMode) return DisableReason.AudienceMode; From 26282698d32da96ba5cedd01f87b0fe84077ee88 Mon Sep 17 00:00:00 2001 From: Karan Gandhi Date: Mon, 10 Nov 2025 21:40:07 +0530 Subject: [PATCH 12/19] fix: lowercase --- src/visualBuilder/components/fieldLabelWrapper.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/visualBuilder/components/fieldLabelWrapper.tsx b/src/visualBuilder/components/fieldLabelWrapper.tsx index 01e5c6c1..bf3c18b5 100644 --- a/src/visualBuilder/components/fieldLabelWrapper.tsx +++ b/src/visualBuilder/components/fieldLabelWrapper.tsx @@ -222,12 +222,11 @@ function FieldLabelWrapperComponent( "visual-builder__tooltip--persistent" ] )} - data-tooltip={!reason?.toLowerCase().includes(DisableReason.CanLinkVariant) + data-tooltip={!reason?.includes(DisableReason.CanLinkVariant) ? reason : undefined} > {reason - .toLowerCase() .includes(DisableReason.CanLinkVariant) && (
Date: Wed, 12 Nov 2025 00:41:52 +0530 Subject: [PATCH 13/19] fix: object freeze --- src/visualBuilder/components/fieldLabelWrapper.tsx | 4 ++-- .../utils/__test__/isFieldDisabled.test.ts | 13 ++++++++----- src/visualBuilder/utils/constants.ts | 10 ++++++++-- src/visualBuilder/utils/isFieldDisabled.ts | 2 +- 4 files changed, 19 insertions(+), 10 deletions(-) diff --git a/src/visualBuilder/components/fieldLabelWrapper.tsx b/src/visualBuilder/components/fieldLabelWrapper.tsx index bf3c18b5..479bbbdd 100644 --- a/src/visualBuilder/components/fieldLabelWrapper.tsx +++ b/src/visualBuilder/components/fieldLabelWrapper.tsx @@ -234,12 +234,12 @@ function FieldLabelWrapperComponent( > {(() => { const [before, after] = reason.split( - DisableReason.SplitOn + DisableReason.UnderlinedAndClickableWord ); return ( <> {before} - {DisableReason.SplitOn} + {DisableReason.UnderlinedAndClickableWord} {after} ); diff --git a/src/visualBuilder/utils/__test__/isFieldDisabled.test.ts b/src/visualBuilder/utils/__test__/isFieldDisabled.test.ts index 22ee0d91..e3c0e00f 100644 --- a/src/visualBuilder/utils/__test__/isFieldDisabled.test.ts +++ b/src/visualBuilder/utils/__test__/isFieldDisabled.test.ts @@ -5,6 +5,7 @@ import { FieldDetails } from "../../components/FieldToolbar"; import Config from "../../../configManager/configManager"; import { VisualBuilder } from "../.."; import { EntryPermissions } from "../getEntryPermissions"; +import { WORKFLOW_STAGES } from "../constants"; describe("isFieldDisabled", () => { it("should return disabled state due to read-only role", () => { @@ -311,7 +312,7 @@ describe("isFieldDisabled", () => { expect(result.isDisabled).toBe(true); expect(result.reason).toBe( DisableReason.WorkflowStagePermission({ - stageName: "Review Stage", + stageName: WORKFLOW_STAGES.REVIEW, }) ); }); @@ -353,7 +354,7 @@ describe("isFieldDisabled", () => { expect(result.isDisabled).toBe(true); expect(result.reason).toBe( DisableReason.EntryUpdateRestrictedRoleAndWorkflowStage({ - stageName: "Final Review", + stageName: WORKFLOW_STAGES.FINAL_REVIEW, }) ); }); @@ -393,7 +394,7 @@ describe("isFieldDisabled", () => { workflowStageDetails ); expect(result.isDisabled).toBe(false); - expect(result.reason).toBe(DisableReason.None); + expect(result.reason).toBe(""); }); it("should handle workflow stage details with undefined stage name", () => { @@ -430,7 +431,9 @@ describe("isFieldDisabled", () => { ); expect(result.isDisabled).toBe(true); expect(result.reason).toBe( - DisableReason.WorkflowStagePermission({ stageName: "Unknown" }) + DisableReason.WorkflowStagePermission({ + stageName: WORKFLOW_STAGES.UNKNOWN, + }) ); }); @@ -472,7 +475,7 @@ describe("isFieldDisabled", () => { expect(result.isDisabled).toBe(true); expect(result.reason).toBe( DisableReason.EntryUpdateRestrictedRoleAndWorkflowStage({ - stageName: "Unknown", + stageName: WORKFLOW_STAGES.UNKNOWN, }) ); }); diff --git a/src/visualBuilder/utils/constants.ts b/src/visualBuilder/utils/constants.ts index 4540d2bf..b5475553 100644 --- a/src/visualBuilder/utils/constants.ts +++ b/src/visualBuilder/utils/constants.ts @@ -16,10 +16,16 @@ export const TOOLBAR_EDGE_BUFFER = 8; export const DATA_CSLP_ATTR_SELECTOR = "data-cslp"; -export const RESULT_TYPES = { +export const RESULT_TYPES = Object.freeze({ SUCCESS: "success", ERROR: "error", -}; +}); + +export const WORKFLOW_STAGES = Object.freeze({ + REVIEW: "Review Stage", + FINAL_REVIEW: "Final Review", + UNKNOWN: "Unknown", +}); /** * The field that can be directly modified using contenteditable=true. diff --git a/src/visualBuilder/utils/isFieldDisabled.ts b/src/visualBuilder/utils/isFieldDisabled.ts index 0e4e7a35..6d7bac63 100644 --- a/src/visualBuilder/utils/isFieldDisabled.ts +++ b/src/visualBuilder/utils/isFieldDisabled.ts @@ -11,7 +11,7 @@ export const DisableReason = { UnlinkedVariant: "This field is not editable as it is not linked to the selected variant.", CanLinkVariant: "Click here to link a variant", - SplitOn: "here", + UnderlinedAndClickableWord: "here", CannotLinkVariant: "Contact your stack admin or owner to link it.", AudienceMode: "To edit an experience, open the Audience widget and click the Edit icon.", From 74e6e5bc3e3445a25c823b3e5f5dd7c56490e16c Mon Sep 17 00:00:00 2001 From: hiteshshetty-dev Date: Mon, 24 Nov 2025 20:24:14 +0530 Subject: [PATCH 14/19] fix: partial state clear when DOM is not visible during mutuation and resize checks --- .../__test__/updateFocussedState.test.ts | 31 +++++++++++++------ .../utils/updateFocussedState.ts | 6 ++-- 2 files changed, 24 insertions(+), 13 deletions(-) diff --git a/src/visualBuilder/utils/__test__/updateFocussedState.test.ts b/src/visualBuilder/utils/__test__/updateFocussedState.test.ts index f427eaf0..70fb2633 100644 --- a/src/visualBuilder/utils/__test__/updateFocussedState.test.ts +++ b/src/visualBuilder/utils/__test__/updateFocussedState.test.ts @@ -6,7 +6,7 @@ import { import { VisualBuilder } from "../.."; import { addFocusOverlay, - hideFocusOverlay, + hideOverlay, } from "../../generators/generateOverlay"; import { mockGetBoundingClientRect } from "../../../__test__/utils"; import { act } from "@testing-library/preact"; @@ -17,7 +17,7 @@ import { isFieldDisabled } from "../isFieldDisabled"; vi.mock("../../generators/generateOverlay", () => ({ addFocusOverlay: vi.fn(), - hideFocusOverlay: vi.fn(), + hideOverlay: vi.fn(), })); vi.mock("../getEntryPermissionsCached", () => ({ @@ -44,10 +44,10 @@ vi.mock("../../utils/fieldSchemaMap", () => { }; }); + describe("updateFocussedState", () => { beforeEach(() => { - let previousSelectedEditableDOM: HTMLElement; - previousSelectedEditableDOM = document.createElement("div"); + const previousSelectedEditableDOM = document.createElement("div"); previousSelectedEditableDOM.setAttribute( "data-cslp", "content_type_uid.entry_uid.locale.field_path" @@ -72,7 +72,7 @@ describe("updateFocussedState", () => { expect(result).toBeUndefined(); }); - it("should hide focus overlay if newPreviousSelectedElement is not found", () => { + it("should call hideOverlay if newPreviousSelectedElement is not found", () => { const resizeObserverMock = { disconnect: vi.fn(), } as unknown as ResizeObserver; @@ -93,7 +93,13 @@ describe("updateFocussedState", () => { resizeObserver: resizeObserverMock, }); - expect(hideFocusOverlay).toHaveBeenCalled(); + expect(hideOverlay).toHaveBeenCalledWith({ + visualBuilderOverlayWrapper: overlayWrapperMock, + focusedToolbar: focusedToolbarMock, + visualBuilderContainer: visualBuilderContainerMock, + resizeObserver: resizeObserverMock, + noTrigger: true, + }); spyQuerySelector.mockRestore(); }); @@ -237,8 +243,7 @@ describe("updateFocussedState", () => { describe("updateFocussedStateOnMutation", () => { beforeEach(() => { - let previousSelectedEditableDOM: HTMLElement; - previousSelectedEditableDOM = document.createElement("div"); + const previousSelectedEditableDOM = document.createElement("div"); previousSelectedEditableDOM.setAttribute( "data-cslp", "content_type_uid.entry_uid.locale.field_path" @@ -257,7 +262,7 @@ describe("updateFocussedStateOnMutation", () => { expect(result).toBeUndefined(); }); - it("should hide focus overlay if newSelectedElement is not found", () => { + it("should call hideOverlay if newSelectedElement is not found", () => { const resizeObserverMock = { disconnect: vi.fn(), } as unknown as ResizeObserver; @@ -274,7 +279,13 @@ describe("updateFocussedStateOnMutation", () => { resizeObserverMock ); - expect(hideFocusOverlay).toHaveBeenCalled(); + expect(hideOverlay).toHaveBeenCalledWith({ + visualBuilderOverlayWrapper: focusOverlayWrapperMock, + focusedToolbar: focusedToolbarMock, + visualBuilderContainer: visualBuilderContainerMock, + resizeObserver: resizeObserverMock, + noTrigger: true, + }); }); it("should update focus outline dimensions", () => { diff --git a/src/visualBuilder/utils/updateFocussedState.ts b/src/visualBuilder/utils/updateFocussedState.ts index a5799fab..810f6e29 100644 --- a/src/visualBuilder/utils/updateFocussedState.ts +++ b/src/visualBuilder/utils/updateFocussedState.ts @@ -3,7 +3,7 @@ import { extractDetailsFromCslp } from "../../cslp"; import { getAddInstanceButtons } from "../generators/generateAddInstanceButtons"; import { addFocusOverlay, - hideFocusOverlay, + hideOverlay, } from "../generators/generateOverlay"; import { hideHoverOutline } from "../listeners/mouseHover"; import { @@ -120,7 +120,7 @@ export async function updateFocussedState({ ) || document.querySelector(`[data-cslp="${previousSelectedElementCslp}"]`); if (!newPreviousSelectedElement && resizeObserver) { - hideFocusOverlay({ + hideOverlay({ visualBuilderOverlayWrapper: overlayWrapper, focusedToolbar, visualBuilderContainer, @@ -265,7 +265,7 @@ export function updateFocussedStateOnMutation( `[data-cslp-unique-id="${selectedElementCslpUniqueId}"]` ) || document.querySelector(`[data-cslp="${selectedElementCslp}"]`); if (!newSelectedElement && resizeObserver) { - hideFocusOverlay({ + hideOverlay({ visualBuilderOverlayWrapper: focusOverlayWrapper, focusedToolbar, visualBuilderContainer, From 48e6c6951453503f8bbd87dea61d21c82da162bb Mon Sep 17 00:00:00 2001 From: hiteshshetty-dev Date: Mon, 24 Nov 2025 20:30:23 +0530 Subject: [PATCH 15/19] chore: add TODOs for overlay and toolbar position logic to consolidate with existing methods --- src/visualBuilder/utils/updateFocussedState.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/visualBuilder/utils/updateFocussedState.ts b/src/visualBuilder/utils/updateFocussedState.ts index 810f6e29..e3df45f1 100644 --- a/src/visualBuilder/utils/updateFocussedState.ts +++ b/src/visualBuilder/utils/updateFocussedState.ts @@ -300,6 +300,9 @@ export function updateFocussedStateOnMutation( } } + //TODO: This logic for overlay position is already present in generateOverlay as `addFocusOverlay`. + // We should refactor this to use the same logic. Refer "VB-593" branch for more details. + /** * Update the focus overlays if they exists. */ @@ -386,6 +389,8 @@ export function updateFocussedStateOnMutation( * Update the focus toolbar if it exists. */ + //TODO: This logic for toolbar position is already present in same file as `positionToolbar`. + // We should refactor this to use the same logic. Refer "VB-593" branch for more details. if (focusedToolbar) { const targetElementRightEdgeOffset = window.scrollX + window.innerWidth - selectedElementDimension.left; From 34b7e42fb5b720008acde17a2071a09ebc9abdc2 Mon Sep 17 00:00:00 2001 From: hiteshshetty-dev Date: Tue, 25 Nov 2025 17:11:01 +0530 Subject: [PATCH 16/19] fix: handle empty data-cslp attributes in getEntryIdentifiersInCurrentPage function --- .../getEntryIdentifiersInCurrentPage.test.ts | 12 ++++++++++++ .../utils/getEntryIdentifiersInCurrentPage.ts | 6 +++--- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/visualBuilder/utils/__test__/getEntryIdentifiersInCurrentPage.test.ts b/src/visualBuilder/utils/__test__/getEntryIdentifiersInCurrentPage.test.ts index 2d057b31..8bf1f480 100644 --- a/src/visualBuilder/utils/__test__/getEntryIdentifiersInCurrentPage.test.ts +++ b/src/visualBuilder/utils/__test__/getEntryIdentifiersInCurrentPage.test.ts @@ -47,4 +47,16 @@ describe("getEntryIdentifiersInCurrentPage", () => { expect(entriesInCurrentPage.length).toBe(0); expect(entriesInCurrentPage).toEqual([]); }); + + test("should filter out elements with empty data-cslp attribute and not break", () => { + document.body.innerHTML = ` +
+

Empty CSLP

+

Valid CSLP

+
+ `; + const { entriesInCurrentPage } = getEntryIdentifiersInCurrentPage(); + expect(entriesInCurrentPage.length).toBe(1); + expect(entriesInCurrentPage[0].entryUid).toBe('bltf5bb5f8fb088a332'); + }); }); diff --git a/src/visualBuilder/utils/getEntryIdentifiersInCurrentPage.ts b/src/visualBuilder/utils/getEntryIdentifiersInCurrentPage.ts index 7ea4f741..243bc4a1 100644 --- a/src/visualBuilder/utils/getEntryIdentifiersInCurrentPage.ts +++ b/src/visualBuilder/utils/getEntryIdentifiersInCurrentPage.ts @@ -14,9 +14,9 @@ export function getEntryIdentifiersInCurrentPage(): EntryIdentifiers { ); const uniqueEntriesMap = new Map(); elementsWithCslp.forEach((element) => { - const cslpData = extractDetailsFromCslp( - element.getAttribute("data-cslp") as string - ); + const cslpValue = element.getAttribute("data-cslp"); + if (!cslpValue) return; + const cslpData = extractDetailsFromCslp(cslpValue); uniqueEntriesMap.set(cslpData.entry_uid, { entryUid: cslpData.entry_uid, From b1da67ed571b054e4e701d5e0cce2945559c95b5 Mon Sep 17 00:00:00 2001 From: hiteshshetty-dev Date: Wed, 26 Nov 2025 16:13:55 +0530 Subject: [PATCH 17/19] test: add additional header element to getEntryIdentifiersInCurrentPage test case --- .../utils/__test__/getEntryIdentifiersInCurrentPage.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/visualBuilder/utils/__test__/getEntryIdentifiersInCurrentPage.test.ts b/src/visualBuilder/utils/__test__/getEntryIdentifiersInCurrentPage.test.ts index 8bf1f480..7f50ce74 100644 --- a/src/visualBuilder/utils/__test__/getEntryIdentifiersInCurrentPage.test.ts +++ b/src/visualBuilder/utils/__test__/getEntryIdentifiersInCurrentPage.test.ts @@ -52,6 +52,7 @@ describe("getEntryIdentifiersInCurrentPage", () => { document.body.innerHTML = `

Empty CSLP

+

Empty CSLP

Valid CSLP

`; From ba91cb0472d0903a1d21b7fc546181f3d6924bb5 Mon Sep 17 00:00:00 2001 From: hiteshshetty-dev Date: Wed, 26 Nov 2025 17:31:51 +0530 Subject: [PATCH 18/19] fix: handle invalid data-cslp attributes across multiple components to prevent errors --- .../components/fieldLabelWrapper.tsx | 22 ++++--- .../__test__/generateOverlay.test.ts | 63 +++++++++++++++++++ .../generators/generateOverlay.tsx | 9 ++- .../utils/__test__/getCsDataOfElement.test.ts | 42 +++++++++++++ .../getVisualBuilderRedirectionUrl.test.ts | 21 +++++++ .../__test__/updateFocussedState.test.ts | 34 ++++++++++ src/visualBuilder/utils/getCsDataOfElement.ts | 8 ++- .../utils/getVisualBuilderRedirectionUrl.ts | 16 ++--- .../utils/updateFocussedState.ts | 3 + 9 files changed, 197 insertions(+), 21 deletions(-) create mode 100644 src/visualBuilder/generators/__test__/generateOverlay.test.ts diff --git a/src/visualBuilder/components/fieldLabelWrapper.tsx b/src/visualBuilder/components/fieldLabelWrapper.tsx index 2ef77e6e..2380613b 100644 --- a/src/visualBuilder/components/fieldLabelWrapper.tsx +++ b/src/visualBuilder/components/fieldLabelWrapper.tsx @@ -111,9 +111,11 @@ function FieldLabelWrapperComponent( const allPaths = uniqBy( [ props.fieldMetadata, - ...props.parentPaths.map((path) => { - return extractDetailsFromCslp(path); - }), + ...props.parentPaths + .filter((path) => path) + .map((path) => { + return extractDetailsFromCslp(path); + }), ], "cslpValue" ); @@ -140,12 +142,14 @@ function FieldLabelWrapperComponent( const domAncestor = eventDetails.editableElement.closest(`[data-cslp]:not([data-cslp^="${props.fieldMetadata.content_type_uid}"])`); if(domAncestor) { const domAncestorCslp = domAncestor.getAttribute("data-cslp"); - const domAncestorDetails = extractDetailsFromCslp(domAncestorCslp!); - const domAncestorContentTypeUid = domAncestorDetails.content_type_uid; - const domAncestorContentParent = referenceData?.find(data => data.contentTypeUid === domAncestorContentTypeUid); - if(domAncestorContentParent) { - referenceFieldName = domAncestorContentParent.referenceFieldName; - parentContentTypeName = domAncestorContentParent.contentTypeTitle; + if (domAncestorCslp) { + const domAncestorDetails = extractDetailsFromCslp(domAncestorCslp); + const domAncestorContentTypeUid = domAncestorDetails.content_type_uid; + const domAncestorContentParent = referenceData?.find(data => data.contentTypeUid === domAncestorContentTypeUid); + if(domAncestorContentParent) { + referenceFieldName = domAncestorContentParent.referenceFieldName; + parentContentTypeName = domAncestorContentParent.contentTypeTitle; + } } } } diff --git a/src/visualBuilder/generators/__test__/generateOverlay.test.ts b/src/visualBuilder/generators/__test__/generateOverlay.test.ts new file mode 100644 index 00000000..02de3274 --- /dev/null +++ b/src/visualBuilder/generators/__test__/generateOverlay.test.ts @@ -0,0 +1,63 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; +import { sendFieldEvent } from "../generateOverlay"; +import { VisualBuilder } from "../.."; +import { VisualBuilderPostMessageEvents } from "../../utils/types/postMessage.types"; +import visualBuilderPostMessage from "../../utils/visualBuilderPostMessage"; +import { FieldSchemaMap } from "../../utils/fieldSchemaMap"; +import { extractDetailsFromCslp } from "../../../cslp/cslpdata"; + +vi.mock("../../utils/visualBuilderPostMessage", () => ({ + default: { + send: vi.fn(), + }, +})); + +vi.mock("../../utils/fieldSchemaMap", () => ({ + FieldSchemaMap: { + getFieldSchema: vi.fn().mockResolvedValue({ + display_name: "Test Field", + data_type: "text", + }), + }, +})); + +vi.mock("../../../cslp/cslpdata", () => ({ + extractDetailsFromCslp: vi.fn(), +})); + +describe("sendFieldEvent", () => { + let previousSelectedEditableDOM: HTMLElement; + let visualBuilderContainer: HTMLElement; + + beforeEach(() => { + previousSelectedEditableDOM = document.createElement("div"); + previousSelectedEditableDOM.setAttribute("contenteditable", "true"); + previousSelectedEditableDOM.innerText = "Test content"; + document.body.appendChild(previousSelectedEditableDOM); + + visualBuilderContainer = document.createElement("div"); + document.body.appendChild(visualBuilderContainer); + + VisualBuilder.VisualBuilderGlobalState.value.previousSelectedEditableDOM = + previousSelectedEditableDOM; + }); + + afterEach(() => { + document.body.innerHTML = ""; + vi.clearAllMocks(); + }); + + it("should return early and not send event when data-cslp attribute is invalid", () => { + previousSelectedEditableDOM.setAttribute("data-cslp", ""); + + sendFieldEvent({ + visualBuilderContainer, + eventType: VisualBuilderPostMessageEvents.UPDATE_FIELD, + }); + + expect(extractDetailsFromCslp).not.toHaveBeenCalled(); + expect(FieldSchemaMap.getFieldSchema).not.toHaveBeenCalled(); + expect(visualBuilderPostMessage?.send).not.toHaveBeenCalled(); + }); +}); + diff --git a/src/visualBuilder/generators/generateOverlay.tsx b/src/visualBuilder/generators/generateOverlay.tsx index e09e9be9..595cefff 100644 --- a/src/visualBuilder/generators/generateOverlay.tsx +++ b/src/visualBuilder/generators/generateOverlay.tsx @@ -177,9 +177,12 @@ export function sendFieldEvent(options: ISendFieldEventParams): void { ? actualEditedElement.innerText : actualEditedElement.textContent; - const fieldMetadata = extractDetailsFromCslp( - previousSelectedEditableDOM.getAttribute("data-cslp") as string - ); + const cslpData = previousSelectedEditableDOM.getAttribute("data-cslp"); + if (!cslpData) { + return; + } + + const fieldMetadata = extractDetailsFromCslp(cslpData); FieldSchemaMap.getFieldSchema( fieldMetadata.content_type_uid, diff --git a/src/visualBuilder/utils/__test__/getCsDataOfElement.test.ts b/src/visualBuilder/utils/__test__/getCsDataOfElement.test.ts index 307b52fe..ad387095 100644 --- a/src/visualBuilder/utils/__test__/getCsDataOfElement.test.ts +++ b/src/visualBuilder/utils/__test__/getCsDataOfElement.test.ts @@ -217,4 +217,46 @@ describe("getDOMEditStack", () => { const editStack = getDOMEditStack(leafEl); expect(editStack.length).toBe(2); }); + + test("get dom edit stack should filter out elements with same prefix", () => { + const dom = new JSDOM(` +
+
+
+ leaf +
+
+
+ `).window.document; + const leafEl = dom.querySelector( + '[data-cslp="page.pageuid.en-us.group.field1.nested.leaf"]' + ) as HTMLElement; + const editStack = getDOMEditStack(leafEl); + // Should only have one entry since all have same prefix (page.pageuid.en-us) + expect(editStack.length).toBe(1); + expect(editStack[0].content_type_uid).toBe("page"); + expect(editStack[0].entry_uid).toBe("pageuid"); + }); + + test("get dom edit stack should filter out elements with invalid data-cslp attribute", () => { + const dom = new JSDOM(` +
+
+
+
+ name +
+
+
+
+ `).window.document; + const leafEl = dom.querySelector( + '[data-cslp="blog.bloguid.fr-fr.title.name"]' + ) as HTMLElement; + const editStack = getDOMEditStack(leafEl); + // Should have two entries (page, blog) - invalid cslp attributes (empty or no value) should be filtered out + expect(editStack.length).toBe(2); + expect(editStack[0].content_type_uid).toBe("page"); + expect(editStack[1].content_type_uid).toBe("blog"); + }); }); diff --git a/src/visualBuilder/utils/__test__/getVisualBuilderRedirectionUrl.test.ts b/src/visualBuilder/utils/__test__/getVisualBuilderRedirectionUrl.test.ts index e7232ad6..c09ec900 100644 --- a/src/visualBuilder/utils/__test__/getVisualBuilderRedirectionUrl.test.ts +++ b/src/visualBuilder/utils/__test__/getVisualBuilderRedirectionUrl.test.ts @@ -84,4 +84,25 @@ describe('getVisualBuilderRedirectionUrl', () => { const result = getVisualBuilderRedirectionUrl(); expect(result.toString()).toBe('https://app.example.com/#!/stack/12345/visual-builder?branch=main&environment=production&target-url=https%3A%2F%2Fexample.com%2F'); }); + + it('should ignore invalid data-cslp attribute and use locale from config', () => { + document.body.innerHTML = '
'; + Config.get.mockReturnValue({ + stackDetails: { + branch: 'main', + apiKey: '12345', + environment: 'production', + locale: 'en-US' + }, + clientUrlParams: { + url: 'https://app.example.com' + } + }); + + const result = getVisualBuilderRedirectionUrl(); + // Should use locale from config when data-cslp attribute is invalid (empty or no value) + expect(result.toString()).toBe('https://app.example.com/#!/stack/12345/visual-builder?branch=main&environment=production&target-url=https%3A%2F%2Fexample.com%2F&locale=en-US'); + // Should not call extractDetailsFromCslp for invalid cslp + expect(extractDetailsFromCslp).not.toHaveBeenCalled(); + }); }); \ No newline at end of file diff --git a/src/visualBuilder/utils/__test__/updateFocussedState.test.ts b/src/visualBuilder/utils/__test__/updateFocussedState.test.ts index f427eaf0..835114c4 100644 --- a/src/visualBuilder/utils/__test__/updateFocussedState.test.ts +++ b/src/visualBuilder/utils/__test__/updateFocussedState.test.ts @@ -55,11 +55,13 @@ describe("updateFocussedState", () => { document.body.appendChild(previousSelectedEditableDOM); VisualBuilder.VisualBuilderGlobalState.value.previousSelectedEditableDOM = previousSelectedEditableDOM; + vi.clearAllMocks(); }); afterEach(() => { document.body.innerHTML = ""; VisualBuilder.VisualBuilderGlobalState.value.previousSelectedEditableDOM = null; + vi.clearAllMocks(); }); it("should return early if required elements are not provided", async () => { const result = await updateFocussedState({ @@ -233,6 +235,38 @@ describe("updateFocussedState", () => { expect.any(Boolean) ); }); + + it("should return early if data-cslp attribute is invalid", async () => { + const editableElementMock = document.createElement("div"); + editableElementMock.setAttribute("data-cslp", ""); + const visualBuilderContainerMock = document.createElement("div"); + const overlayWrapperMock = document.createElement("div"); + const focusedToolbarMock = document.createElement("div"); + const resizeObserverMock = { + disconnect: vi.fn(), + } as unknown as ResizeObserver; + + const previousSelectedEditableDOM = document.createElement("div"); + previousSelectedEditableDOM.setAttribute("data-cslp", "content_type_uid.entry_uid.locale.field_path"); + document.body.appendChild(previousSelectedEditableDOM); + VisualBuilder.VisualBuilderGlobalState.value.previousSelectedEditableDOM = + previousSelectedEditableDOM; + + document.querySelector = vi.fn().mockReturnValue(previousSelectedEditableDOM); + + const result = await updateFocussedState({ + editableElement: editableElementMock, + visualBuilderContainer: visualBuilderContainerMock, + overlayWrapper: overlayWrapperMock, + focusedToolbar: focusedToolbarMock, + resizeObserver: resizeObserverMock, + }); + + // Should return early without processing + expect(result).toBeUndefined(); + expect(getEntryPermissionsCached).not.toHaveBeenCalled(); + expect(addFocusOverlay).not.toHaveBeenCalled(); + }); }); describe("updateFocussedStateOnMutation", () => { diff --git a/src/visualBuilder/utils/getCsDataOfElement.ts b/src/visualBuilder/utils/getCsDataOfElement.ts index 2cce0888..7139a50f 100644 --- a/src/visualBuilder/utils/getCsDataOfElement.ts +++ b/src/visualBuilder/utils/getCsDataOfElement.ts @@ -54,7 +54,11 @@ export function getDOMEditStack(ele: Element): CslpData[] { const cslpSet: string[] = []; let curr: any = ele.closest(`[${DATA_CSLP_ATTR_SELECTOR}]`); while (curr) { - const cslp = curr.getAttribute(DATA_CSLP_ATTR_SELECTOR)!; + const cslp = curr.getAttribute(DATA_CSLP_ATTR_SELECTOR); + if (!cslp) { + curr = curr.parentElement?.closest(`[${DATA_CSLP_ATTR_SELECTOR}]`); + continue; + } const entryPrefix = getPrefix(cslp); const hasSamePrevPrefix = getPrefix(cslpSet.at(0) || "").startsWith( entryPrefix @@ -64,5 +68,5 @@ export function getDOMEditStack(ele: Element): CslpData[] { } curr = curr.parentElement?.closest(`[${DATA_CSLP_ATTR_SELECTOR}]`); } - return cslpSet.map((cslp) => extractDetailsFromCslp(cslp)); + return cslpSet.filter((cslp) => cslp).map((cslp) => extractDetailsFromCslp(cslp)); } diff --git a/src/visualBuilder/utils/getVisualBuilderRedirectionUrl.ts b/src/visualBuilder/utils/getVisualBuilderRedirectionUrl.ts index 955e466c..02e4659b 100644 --- a/src/visualBuilder/utils/getVisualBuilderRedirectionUrl.ts +++ b/src/visualBuilder/utils/getVisualBuilderRedirectionUrl.ts @@ -22,16 +22,18 @@ export default function getVisualBuilderRedirectionUrl(): URL { // get the locale from the data cslp attribute const elementWithDataCslp = document.querySelector(`[data-cslp]`); + let localeToUse = locale; if (elementWithDataCslp) { - const cslpData = elementWithDataCslp.getAttribute( - "data-cslp" - ) as string; - const { locale } = extractDetailsFromCslp(cslpData); + const cslpData = elementWithDataCslp.getAttribute("data-cslp"); + if (cslpData) { + const { locale: cslpLocale } = extractDetailsFromCslp(cslpData); + localeToUse = cslpLocale; + } + } - searchParams.set("locale", locale); - } else if (locale) { - searchParams.set("locale", locale); + if (localeToUse) { + searchParams.set("locale", localeToUse); } const completeURL = new URL( diff --git a/src/visualBuilder/utils/updateFocussedState.ts b/src/visualBuilder/utils/updateFocussedState.ts index a5799fab..a6a33509 100644 --- a/src/visualBuilder/utils/updateFocussedState.ts +++ b/src/visualBuilder/utils/updateFocussedState.ts @@ -136,6 +136,9 @@ export async function updateFocussedState({ } const cslp = editableElement?.getAttribute("data-cslp") || ""; + if (!cslp) { + return; + } const fieldMetadata = extractDetailsFromCslp(cslp); hideHoverOutline(visualBuilderContainer); From 84f8a26ed0be33a9e6ccf65aec196945913a2368 Mon Sep 17 00:00:00 2001 From: Karan Gandhi Date: Wed, 3 Dec 2025 11:25:58 +0530 Subject: [PATCH 19/19] Vitest 3 optimizing (#524) --- .github/workflows/unit-test.yml | 4 +- .gitignore | 5 + package-lock.json | 1915 ++++++++++++----- package.json | 9 +- src/__test__/utils.ts | 132 +- src/livePreview/__test__/live-preview.test.ts | 63 +- .../__test__/editButtonAction.test.ts | 6 - .../contentstack-live-preview-HOC.test.ts | 6 - .../__test__/click/fields/all-click.test.tsx | 388 ++++ .../__test__/click/fields/boolean.test.tsx | 172 -- .../__test__/click/fields/date.test.tsx | 172 -- .../__test__/click/fields/file.test.tsx | 220 +- .../__test__/click/fields/group.test.tsx | 110 +- .../__test__/click/fields/html-rte.test.tsx | 259 --- .../__test__/click/fields/json-rte.test.tsx | 261 --- .../__test__/click/fields/link.test.tsx | 247 --- .../__test__/click/fields/markdown.test.tsx | 256 --- .../__test__/click/fields/multi-line.test.tsx | 99 +- .../__test__/click/fields/number.test.tsx | 108 +- .../__test__/click/fields/reference.test.tsx | 93 +- .../__test__/click/fields/select.test.tsx | 260 --- .../click/fields/single-line.test.tsx | 91 +- .../__test__/hover/fields/all-hover.test.ts | 310 +++ .../__test__/hover/fields/boolean.test.ts | 124 -- .../__test__/hover/fields/date.test.ts | 122 -- .../__test__/hover/fields/file.test.ts | 98 +- .../__test__/hover/fields/group.test.ts | 86 +- .../__test__/hover/fields/html-rte.test.ts | 199 -- .../__test__/hover/fields/json-rte.test.ts | 199 -- .../__test__/hover/fields/link.test.ts | 196 -- .../__test__/hover/fields/markdown.test.ts | 200 -- .../__test__/hover/fields/multi-line.test.ts | 198 -- .../__test__/hover/fields/number.test.ts | 196 -- .../__test__/hover/fields/reference.test.ts | 199 -- .../__test__/hover/fields/select.test.ts | 210 -- .../__test__/hover/fields/single-line.test.ts | 170 +- src/visualBuilder/__test__/index.test.ts | 313 +-- .../__test__/withoutIframe.test.ts | 12 +- .../__test__/fieldLabelWrapper.test.tsx | 559 ----- .../components/__test__/fieldToolbar.test.tsx | 364 ++-- .../fieldLabelWrapper.disabledClass.test.tsx | 332 +++ .../fieldLabelWrapper.fieldIcon.test.tsx | 328 +++ ...fieldLabelWrapper.isFieldDisabled.test.tsx | 335 +++ .../fieldLabelWrapper.loading.test.tsx | 346 +++ .../fieldLabelWrapper.mocks.ts | 245 +++ .../fieldLabelWrapper.renderFields.test.tsx | 331 +++ ...ldLabelWrapper.variant.cssClasses.test.tsx | 317 +++ ...LabelWrapper.variant.noCssClasses.test.tsx | 312 +++ ...dLabelWrapper.variant.noIndicator.test.tsx | 306 +++ ...elWrapper.variant.renderIndicator.test.tsx | 328 +++ .../__test__/generateToolbar.test.ts | 44 +- .../__test__/focusOverlayWrapper.test.ts | 47 +- .../__test__/handleIndividualFields.test.ts | 140 +- .../__test__/multipleElementAddButton.test.ts | 56 +- .../__test__/updateFocussedState.test.ts | 114 +- tsconfig.json | 2 +- vitest.config.ts | 56 +- vitest.setup.ts | 50 +- 58 files changed, 6503 insertions(+), 5817 deletions(-) create mode 100644 src/visualBuilder/__test__/click/fields/all-click.test.tsx delete mode 100644 src/visualBuilder/__test__/click/fields/boolean.test.tsx delete mode 100644 src/visualBuilder/__test__/click/fields/date.test.tsx delete mode 100644 src/visualBuilder/__test__/click/fields/html-rte.test.tsx delete mode 100644 src/visualBuilder/__test__/click/fields/json-rte.test.tsx delete mode 100644 src/visualBuilder/__test__/click/fields/link.test.tsx delete mode 100644 src/visualBuilder/__test__/click/fields/markdown.test.tsx delete mode 100644 src/visualBuilder/__test__/click/fields/select.test.tsx create mode 100644 src/visualBuilder/__test__/hover/fields/all-hover.test.ts delete mode 100644 src/visualBuilder/__test__/hover/fields/boolean.test.ts delete mode 100644 src/visualBuilder/__test__/hover/fields/date.test.ts delete mode 100644 src/visualBuilder/__test__/hover/fields/html-rte.test.ts delete mode 100644 src/visualBuilder/__test__/hover/fields/json-rte.test.ts delete mode 100644 src/visualBuilder/__test__/hover/fields/link.test.ts delete mode 100644 src/visualBuilder/__test__/hover/fields/markdown.test.ts delete mode 100644 src/visualBuilder/__test__/hover/fields/multi-line.test.ts delete mode 100644 src/visualBuilder/__test__/hover/fields/number.test.ts delete mode 100644 src/visualBuilder/__test__/hover/fields/reference.test.ts delete mode 100644 src/visualBuilder/__test__/hover/fields/select.test.ts delete mode 100644 src/visualBuilder/components/__test__/fieldLabelWrapper.test.tsx create mode 100644 src/visualBuilder/components/__test__/fieldlabelwrapper/fieldLabelWrapper.disabledClass.test.tsx create mode 100644 src/visualBuilder/components/__test__/fieldlabelwrapper/fieldLabelWrapper.fieldIcon.test.tsx create mode 100644 src/visualBuilder/components/__test__/fieldlabelwrapper/fieldLabelWrapper.isFieldDisabled.test.tsx create mode 100644 src/visualBuilder/components/__test__/fieldlabelwrapper/fieldLabelWrapper.loading.test.tsx create mode 100644 src/visualBuilder/components/__test__/fieldlabelwrapper/fieldLabelWrapper.mocks.ts create mode 100644 src/visualBuilder/components/__test__/fieldlabelwrapper/fieldLabelWrapper.renderFields.test.tsx create mode 100644 src/visualBuilder/components/__test__/fieldlabelwrapper/fieldLabelWrapper.variant.cssClasses.test.tsx create mode 100644 src/visualBuilder/components/__test__/fieldlabelwrapper/fieldLabelWrapper.variant.noCssClasses.test.tsx create mode 100644 src/visualBuilder/components/__test__/fieldlabelwrapper/fieldLabelWrapper.variant.noIndicator.test.tsx create mode 100644 src/visualBuilder/components/__test__/fieldlabelwrapper/fieldLabelWrapper.variant.renderIndicator.test.tsx diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index bb31ef73..9ec05a51 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -17,11 +17,11 @@ jobs: - name: "Install Node" uses: actions/setup-node@v4 with: - node-version: "21.x" + node-version: "22.x" - name: "Install Deps" run: npm install - name: "Test" - run: npx vitest --coverage.enabled true + run: npm run test:coverage - name: "Report Coverage" # Set if: always() to also generate the report if tests are failing # Only works if you set `reportOnFailure: true` in your vite config as specified above diff --git a/.gitignore b/.gitignore index 9e48059b..6030c9aa 100644 --- a/.gitignore +++ b/.gitignore @@ -138,5 +138,10 @@ temp/ .DS_Store +# Test results and profiling reports +test-results.json +junit.xml +test-reports/ +test-profile-report.json # End of https://www.toptal.com/developers/gitignore/api/node,web,vscode diff --git a/package-lock.json b/package-lock.json index 6ddf22c8..2e5f4530 100644 --- a/package-lock.json +++ b/package-lock.json @@ -37,8 +37,8 @@ "@types/react": "^18.2.57", "@types/react-dom": "^18.2.19", "@types/uuid": "^8.3.1", - "@vitest/coverage-v8": "^2.1.2", - "@vitest/ui": "^2.1.2", + "@vitest/coverage-v8": "^3.2.4", + "@vitest/ui": "^3.2.4", "auto-changelog": "^2.5.0", "esbuild-plugin-file-path-extensions": "^2.1.0", "eslint": "^8.57.1", @@ -57,7 +57,7 @@ "typedoc": "^0.25.13", "typescript": "^5.4.5", "typescript-eslint": "^8.5.0", - "vitest": "^2.1.0" + "vitest": "^3.2.4" }, "optionalDependencies": { "@rollup/rollup-linux-x64-gnu": "4.9.5" @@ -168,10 +168,14 @@ } }, "node_modules/@bcoe/v8-coverage": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", - "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", - "dev": true + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } }, "node_modules/@commitlint/cli": { "version": "16.3.0", @@ -914,6 +918,23 @@ "node": ">=18" } }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/sunos-x64": { "version": "0.24.2", "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.2.tgz", @@ -1259,16 +1280,18 @@ } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", - "dev": true + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", "dev": true, + "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" @@ -1320,10 +1343,11 @@ } }, "node_modules/@polka/url": { - "version": "1.0.0-next.28", - "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.28.tgz", - "integrity": "sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw==", - "dev": true + "version": "1.0.0-next.29", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", + "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", + "dev": true, + "license": "MIT" }, "node_modules/@preact/compat": { "version": "17.1.2", @@ -1359,182 +1383,210 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.32.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.32.1.tgz", - "integrity": "sha512-/pqA4DmqyCm8u5YIDzIdlLcEmuvxb0v8fZdFhVMszSpDTgbQKdw3/mB3eMUHIbubtJ6F9j+LtmyCnHTEqIHyzA==", + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.5.tgz", + "integrity": "sha512-8c1vW4ocv3UOMp9K+gToY5zL2XiiVw3k7f1ksf4yO1FlDFQ1C2u72iACFnSOceJFsWskc2WZNqeRhFRPzv+wtQ==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.32.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.32.1.tgz", - "integrity": "sha512-If3PDskT77q7zgqVqYuj7WG3WC08G1kwXGVFi9Jr8nY6eHucREHkfpX79c0ACAjLj3QIWKPJR7w4i+f5EdLH5Q==", + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.5.tgz", + "integrity": "sha512-mQGfsIEFcu21mvqkEKKu2dYmtuSZOBMmAl5CFlPGLY94Vlcm+zWApK7F/eocsNzp8tKmbeBP8yXyAbx0XHsFNA==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.32.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.32.1.tgz", - "integrity": "sha512-zCpKHioQ9KgZToFp5Wvz6zaWbMzYQ2LJHQ+QixDKq52KKrF65ueu6Af4hLlLWHjX1Wf/0G5kSJM9PySW9IrvHA==", + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.5.tgz", + "integrity": "sha512-takF3CR71mCAGA+v794QUZ0b6ZSrgJkArC+gUiG6LB6TQty9T0Mqh3m2ImRBOxS2IeYBo4lKWIieSvnEk2OQWA==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.32.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.32.1.tgz", - "integrity": "sha512-sFvF+t2+TyUo/ZQqUcifrJIgznx58oFZbdHS9TvHq3xhPVL9nOp+yZ6LKrO9GWTP+6DbFtoyLDbjTpR62Mbr3Q==", + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.5.tgz", + "integrity": "sha512-W901Pla8Ya95WpxDn//VF9K9u2JbocwV/v75TE0YIHNTbhqUTv9w4VuQ9MaWlNOkkEfFwkdNhXgcLqPSmHy0fA==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.32.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.32.1.tgz", - "integrity": "sha512-NbOa+7InvMWRcY9RG+B6kKIMD/FsnQPH0MWUvDlQB1iXnF/UcKSudCXZtv4lW+C276g3w5AxPbfry5rSYvyeYA==", + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.5.tgz", + "integrity": "sha512-QofO7i7JycsYOWxe0GFqhLmF6l1TqBswJMvICnRUjqCx8b47MTo46W8AoeQwiokAx3zVryVnxtBMcGcnX12LvA==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "freebsd" ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.32.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.32.1.tgz", - "integrity": "sha512-JRBRmwvHPXR881j2xjry8HZ86wIPK2CcDw0EXchE1UgU0ubWp9nvlT7cZYKc6bkypBt745b4bglf3+xJ7hXWWw==", + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.5.tgz", + "integrity": "sha512-jr21b/99ew8ujZubPo9skbrItHEIE50WdV86cdSoRkKtmWa+DDr6fu2c/xyRT0F/WazZpam6kk7IHBerSL7LDQ==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "freebsd" ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.32.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.32.1.tgz", - "integrity": "sha512-PKvszb+9o/vVdUzCCjL0sKHukEQV39tD3fepXxYrHE3sTKrRdCydI7uldRLbjLmDA3TFDmh418XH19NOsDRH8g==", + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.5.tgz", + "integrity": "sha512-PsNAbcyv9CcecAUagQefwX8fQn9LQ4nZkpDboBOttmyffnInRy8R8dSg6hxxl2Re5QhHBf6FYIDhIj5v982ATQ==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.32.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.32.1.tgz", - "integrity": "sha512-9WHEMV6Y89eL606ReYowXuGF1Yb2vwfKWKdD1A5h+OYnPZSJvxbEjxTRKPgi7tkP2DSnW0YLab1ooy+i/FQp/Q==", + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.5.tgz", + "integrity": "sha512-Fw4tysRutyQc/wwkmcyoqFtJhh0u31K+Q6jYjeicsGJJ7bbEq8LwPWV/w0cnzOqR2m694/Af6hpFayLJZkG2VQ==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.32.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.32.1.tgz", - "integrity": "sha512-tZWc9iEt5fGJ1CL2LRPw8OttkCBDs+D8D3oEM8mH8S1ICZCtFJhD7DZ3XMGM8kpqHvhGUTvNUYVDnmkj4BDXnw==", + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.5.tgz", + "integrity": "sha512-a+3wVnAYdQClOTlyapKmyI6BLPAFYs0JM8HRpgYZQO02rMR09ZcV9LbQB+NL6sljzG38869YqThrRnfPMCDtZg==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.32.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.32.1.tgz", - "integrity": "sha512-FTYc2YoTWUsBz5GTTgGkRYYJ5NGJIi/rCY4oK/I8aKowx1ToXeoVVbIE4LGAjsauvlhjfl0MYacxClLld1VrOw==", + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.5.tgz", + "integrity": "sha512-AvttBOMwO9Pcuuf7m9PkC1PUIKsfaAJ4AYhy944qeTJgQOqJYJ9oVl2nYgY7Rk0mkbsuOpCAYSs6wLYB2Xiw0Q==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, - "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.32.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.32.1.tgz", - "integrity": "sha512-F51qLdOtpS6P1zJVRzYM0v6MrBNypyPEN1GfMiz0gPu9jN8ScGaEFIZQwteSsGKg799oR5EaP7+B2jHgL+d+Kw==", + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.5.tgz", + "integrity": "sha512-DkDk8pmXQV2wVrF6oq5tONK6UHLz/XcEVow4JTTerdeV1uqPeHxwcg7aFsfnSm9L+OO8WJsWotKM2JJPMWrQtA==", "cpu": [ "loong64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, - "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.32.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.32.1.tgz", - "integrity": "sha512-wO0WkfSppfX4YFm5KhdCCpnpGbtgQNj/tgvYzrVYFKDpven8w2N6Gg5nB6w+wAMO3AIfSTWeTjfVe+uZ23zAlg==", + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.5.tgz", + "integrity": "sha512-W/b9ZN/U9+hPQVvlGwjzi+Wy4xdoH2I8EjaCkMvzpI7wJUs8sWJ03Rq96jRnHkSrcHTpQe8h5Tg3ZzUPGauvAw==", "cpu": [ "ppc64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.32.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.32.1.tgz", - "integrity": "sha512-iWswS9cIXfJO1MFYtI/4jjlrGb/V58oMu4dYJIKnR5UIwbkzR0PJ09O0PDZT0oJ3LYWXBSWahNf/Mjo6i1E5/g==", + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.5.tgz", + "integrity": "sha512-sjQLr9BW7R/ZiXnQiWPkErNfLMkkWIoCz7YMn27HldKsADEKa5WYdobaa1hmN6slu9oWQbB6/jFpJ+P2IkVrmw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.5.tgz", + "integrity": "sha512-hq3jU/kGyjXWTvAh2awn8oHroCbrPm8JqM7RUpKjalIRWWXE01CQOf/tUNWNHjmbMHg/hmNCwc/Pz3k1T/j/Lg==", "cpu": [ "riscv64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.32.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.32.1.tgz", - "integrity": "sha512-RKt8NI9tebzmEthMnfVgG3i/XeECkMPS+ibVZjZ6mNekpbbUmkNWuIN2yHsb/mBPyZke4nlI4YqIdFPgKuoyQQ==", + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.5.tgz", + "integrity": "sha512-gn8kHOrku8D4NGHMK1Y7NA7INQTRdVOntt1OCYypZPRt6skGbddska44K8iocdpxHTMMNui5oH4elPH4QOLrFQ==", "cpu": [ "s390x" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -1553,52 +1605,84 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.32.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.32.1.tgz", - "integrity": "sha512-BLoiyHDOWoS3uccNSADMza6V6vCNiphi94tQlVIL5de+r6r/CCQuNnerf+1g2mnk2b6edp5dk0nhdZ7aEjOBsA==", + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.5.tgz", + "integrity": "sha512-arCGIcuNKjBoKAXD+y7XomR9gY6Mw7HnFBv5Rw7wQRvwYLR7gBAgV7Mb2QTyjXfTveBNFAtPt46/36vV9STLNg==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.5.tgz", + "integrity": "sha512-QoFqB6+/9Rly/RiPjaomPLmR/13cgkIGfA40LHly9zcH1S0bN2HVFYk3a1eAyHQyjs3ZJYlXvIGtcCs5tko9Cw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.32.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.32.1.tgz", - "integrity": "sha512-w2l3UnlgYTNNU+Z6wOR8YdaioqfEnwPjIsJ66KxKAf0p+AuL2FHeTX6qvM+p/Ue3XPBVNyVSfCrfZiQh7vZHLQ==", + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.5.tgz", + "integrity": "sha512-w0cDWVR6MlTstla1cIfOGyl8+qb93FlAVutcor14Gf5Md5ap5ySfQ7R9S/NjNaMLSFdUnKGEasmVnu3lCMqB7w==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.32.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.32.1.tgz", - "integrity": "sha512-Am9H+TGLomPGkBnaPWie4F3x+yQ2rr4Bk2jpwy+iV+Gel9jLAu/KqT8k3X4jxFPW6Zf8OMnehyutsd+eHoq1WQ==", + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.5.tgz", + "integrity": "sha512-Aufdpzp7DpOTULJCuvzqcItSGDH73pF3ko/f+ckJhxQyHtp67rHw3HMNxoIdDMUITJESNE6a8uh4Lo4SLouOUg==", "cpu": [ "ia32" ], "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.5.tgz", + "integrity": "sha512-UGBUGPFp1vkj6p8wCRraqNhqwX/4kNQPS57BCFc8wYh0g94iVIW33wJtQAx3G7vrjjNtRaxiMUylM0ktp/TRSQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.32.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.32.1.tgz", - "integrity": "sha512-ar80GhdZb4DgmW3myIS9nRFYcpJRSME8iqWgzH2i44u+IdrzmiXVxeFnExQ5v4JYUSpg94bWjevMG8JHf1Da5Q==", + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.5.tgz", + "integrity": "sha512-TAcgQh2sSkykPRWLrdyy2AiceMckNf5loITqXxFI5VuQjS5tSuw3WlwdN8qv8vzjLAUTvYaH/mVjSFpbkFbpTg==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -1729,6 +1813,24 @@ "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", "dev": true }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/eslint": { "version": "8.56.12", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.12.tgz", @@ -1740,10 +1842,11 @@ } }, "node_modules/@types/estree": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", - "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", - "dev": true + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" }, "node_modules/@types/jsdom": { "version": "21.1.7", @@ -2238,30 +2341,32 @@ "dev": true }, "node_modules/@vitest/coverage-v8": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-2.1.8.tgz", - "integrity": "sha512-2Y7BPlKH18mAZYAW1tYByudlCYrQyl5RGvnnDYJKW5tCiO5qg3KSAy3XAxcxKz900a0ZXxWtKrMuZLe3lKBpJw==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.2.4.tgz", + "integrity": "sha512-EyF9SXU6kS5Ku/U82E259WSnvg6c8KTjppUncuNdm5QHpe17mwREHnjDzozC8x9MZ0xfBUFSaLkRv4TMA75ALQ==", "dev": true, + "license": "MIT", "dependencies": { "@ampproject/remapping": "^2.3.0", - "@bcoe/v8-coverage": "^0.2.3", - "debug": "^4.3.7", + "@bcoe/v8-coverage": "^1.0.2", + "ast-v8-to-istanbul": "^0.3.3", + "debug": "^4.4.1", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", "istanbul-lib-source-maps": "^5.0.6", "istanbul-reports": "^3.1.7", - "magic-string": "^0.30.12", + "magic-string": "^0.30.17", "magicast": "^0.3.5", - "std-env": "^3.8.0", + "std-env": "^3.9.0", "test-exclude": "^7.0.1", - "tinyrainbow": "^1.2.0" + "tinyrainbow": "^2.0.0" }, "funding": { "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "@vitest/browser": "2.1.8", - "vitest": "2.1.8" + "@vitest/browser": "3.2.4", + "vitest": "3.2.4" }, "peerDependenciesMeta": { "@vitest/browser": { @@ -2270,127 +2375,110 @@ } }, "node_modules/@vitest/expect": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.8.tgz", - "integrity": "sha512-8ytZ/fFHq2g4PJVAtDX57mayemKgDR6X3Oa2Foro+EygiOJHUXhCqBAAKQYYajZpFoIfvBCF1j6R6IYRSIUFuw==", - "dev": true, - "dependencies": { - "@vitest/spy": "2.1.8", - "@vitest/utils": "2.1.8", - "chai": "^5.1.2", - "tinyrainbow": "^1.2.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/mocker": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.8.tgz", - "integrity": "sha512-7guJ/47I6uqfttp33mgo6ga5Gr1VnL58rcqYKyShoRK9ebu8T5Rs6HN3s1NABiBeVTdWNrwUMcHH54uXZBN4zA==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", + "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", "dev": true, + "license": "MIT", "dependencies": { - "@vitest/spy": "2.1.8", - "estree-walker": "^3.0.3", - "magic-string": "^0.30.12" + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" }, "funding": { "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "msw": "^2.4.9", - "vite": "^5.0.0" - }, - "peerDependenciesMeta": { - "msw": { - "optional": true - }, - "vite": { - "optional": true - } } }, "node_modules/@vitest/pretty-format": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.8.tgz", - "integrity": "sha512-9HiSZ9zpqNLKlbIDRWOnAWqgcA7xu+8YxXSekhr0Ykab7PAYFkhkwoqVArPOtJhPmYeE2YHgKZlj3CP36z2AJQ==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", + "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", "dev": true, + "license": "MIT", "dependencies": { - "tinyrainbow": "^1.2.0" + "tinyrainbow": "^2.0.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/runner": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.8.tgz", - "integrity": "sha512-17ub8vQstRnRlIU5k50bG+QOMLHRhYPAna5tw8tYbj+jzjcspnwnwtPtiOlkuKC4+ixDPTuLZiqiWWQ2PSXHVg==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz", + "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", "dev": true, + "license": "MIT", "dependencies": { - "@vitest/utils": "2.1.8", - "pathe": "^1.1.2" + "@vitest/utils": "3.2.4", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/snapshot": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.8.tgz", - "integrity": "sha512-20T7xRFbmnkfcmgVEz+z3AU/3b0cEzZOt/zmnvZEctg64/QZbSDJEVm9fLnnlSi74KibmRsO9/Qabi+t0vCRPg==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz", + "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", "dev": true, + "license": "MIT", "dependencies": { - "@vitest/pretty-format": "2.1.8", - "magic-string": "^0.30.12", - "pathe": "^1.1.2" + "@vitest/pretty-format": "3.2.4", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/spy": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.8.tgz", - "integrity": "sha512-5swjf2q95gXeYPevtW0BLk6H8+bPlMb4Vw/9Em4hFxDcaOxS+e0LOX4yqNxoHzMR2akEB2xfpnWUzkZokmgWDg==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", + "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", "dev": true, + "license": "MIT", "dependencies": { - "tinyspy": "^3.0.2" + "tinyspy": "^4.0.3" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/ui": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-2.1.8.tgz", - "integrity": "sha512-5zPJ1fs0ixSVSs5+5V2XJjXLmNzjugHRyV11RqxYVR+oMcogZ9qTuSfKW+OcTV0JeFNznI83BNylzH6SSNJ1+w==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-3.2.4.tgz", + "integrity": "sha512-hGISOaP18plkzbWEcP/QvtRW1xDXF2+96HbEX6byqQhAUbiS5oH6/9JwW+QsQCIYON2bI6QZBF+2PvOmrRZ9wA==", "dev": true, + "license": "MIT", "dependencies": { - "@vitest/utils": "2.1.8", + "@vitest/utils": "3.2.4", "fflate": "^0.8.2", - "flatted": "^3.3.1", - "pathe": "^1.1.2", - "sirv": "^3.0.0", - "tinyglobby": "^0.2.10", - "tinyrainbow": "^1.2.0" + "flatted": "^3.3.3", + "pathe": "^2.0.3", + "sirv": "^3.0.1", + "tinyglobby": "^0.2.14", + "tinyrainbow": "^2.0.0" }, "funding": { "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "vitest": "2.1.8" + "vitest": "3.2.4" } }, "node_modules/@vitest/utils": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.8.tgz", - "integrity": "sha512-dwSoui6djdwbfFmIgbIjX2ZhIoG7Ex/+xpxyiEgIGzjliY8xGkcpITKTlp6B4MgtGkF2ilvm97cPM96XZaAgcA==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", + "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", "dev": true, + "license": "MIT", "dependencies": { - "@vitest/pretty-format": "2.1.8", - "loupe": "^3.1.2", - "tinyrainbow": "^1.2.0" + "@vitest/pretty-format": "3.2.4", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" }, "funding": { "url": "https://opencollective.com/vitest" @@ -2669,10 +2757,30 @@ "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" } }, + "node_modules/ast-v8-to-istanbul": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.8.tgz", + "integrity": "sha512-szgSZqUxI5T8mLKvS7WTjF9is+MVbOeLADU73IseOcrqhxr/VAvy6wfoVE39KnKzA7JRhjF5eUagNlHwvZPlKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "estree-walker": "^3.0.3", + "js-tokens": "^9.0.1" + } + }, + "node_modules/ast-v8-to-istanbul/node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, "node_modules/async-function": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", @@ -2858,10 +2966,11 @@ } }, "node_modules/chai": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/chai/-/chai-5.1.2.tgz", - "integrity": "sha512-aGtmf24DW6MLHHG5gCx4zaI3uBq3KRtxeVs0DjFH6Z0rDNbsvTxFASFvdj79pxjxZ8/5u3PIiN3IwEIQkiiuPw==", + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", "dev": true, + "license": "MIT", "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", @@ -2870,7 +2979,7 @@ "pathval": "^2.0.0" }, "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/chalk": { @@ -2894,6 +3003,7 @@ "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", "dev": true, + "license": "MIT", "engines": { "node": ">= 16" } @@ -3216,10 +3326,11 @@ "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==" }, "node_modules/debug": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", - "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, + "license": "MIT", "dependencies": { "ms": "^2.1.3" }, @@ -3277,6 +3388,7 @@ "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -3630,10 +3742,11 @@ } }, "node_modules/es-module-lexer": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.6.0.tgz", - "integrity": "sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==", - "dev": true + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" }, "node_modules/es-object-atoms": { "version": "1.1.1", @@ -4008,6 +4121,7 @@ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", "dev": true, + "license": "MIT", "dependencies": { "@types/estree": "^1.0.0" } @@ -4045,10 +4159,11 @@ } }, "node_modules/expect-type": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.1.0.tgz", - "integrity": "sha512-bFi65yM+xZgk+u/KRIpekdSYkTB5W1pEf0Lt8Q8Msh7b+eQ7LXVtIB1Bkm4fvclDEL1b2CZkMhv2mOeF8tMdkA==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.2.tgz", + "integrity": "sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==", "dev": true, + "license": "Apache-2.0", "engines": { "node": ">=12.0.0" } @@ -4112,7 +4227,8 @@ "version": "0.8.2", "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/file-entry-cache": { "version": "6.0.1", @@ -4169,10 +4285,11 @@ } }, "node_modules/flatted": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.2.tgz", - "integrity": "sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==", - "dev": true + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" }, "node_modules/for-each": { "version": "0.3.4", @@ -5775,10 +5892,11 @@ } }, "node_modules/loupe": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.3.tgz", - "integrity": "sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==", - "dev": true + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" }, "node_modules/lru-cache": { "version": "10.4.3", @@ -5802,12 +5920,13 @@ } }, "node_modules/magic-string": { - "version": "0.30.17", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", - "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", "dev": true, + "license": "MIT", "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0" + "@jridgewell/sourcemap-codec": "^1.5.5" } }, "node_modules/magicast": { @@ -6024,10 +6143,11 @@ } }, "node_modules/mrmime": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.0.tgz", - "integrity": "sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" } @@ -6058,9 +6178,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.8", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", - "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", "dev": true, "funding": [ { @@ -6068,6 +6188,7 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "bin": { "nanoid": "bin/nanoid.cjs" }, @@ -6494,16 +6615,18 @@ } }, "node_modules/pathe": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", - "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", - "dev": true + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" }, "node_modules/pathval": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz", - "integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", "dev": true, + "license": "MIT", "engines": { "node": ">= 14.16" } @@ -6545,9 +6668,9 @@ } }, "node_modules/postcss": { - "version": "8.5.1", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.1.tgz", - "integrity": "sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==", + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", "dev": true, "funding": [ { @@ -6563,8 +6686,9 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { - "nanoid": "^3.3.8", + "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -7129,12 +7253,13 @@ } }, "node_modules/rollup": { - "version": "4.32.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.32.1.tgz", - "integrity": "sha512-z+aeEsOeEa3mEbS1Tjl6sAZ8NE3+AalQz1RJGj81M+fizusbdDMoEJwdJNHfaB40Scr4qNu+welOfes7maKonA==", + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.5.tgz", + "integrity": "sha512-3GuObel8h7Kqdjt0gxkEzaifHTqLVW56Y/bjN7PSQtkKr0w3V/QYSdt6QWYtd7A1xUtYQigtdUfgj1RvWVtorw==", "dev": true, + "license": "MIT", "dependencies": { - "@types/estree": "1.0.6" + "@types/estree": "1.0.8" }, "bin": { "rollup": "dist/bin/rollup" @@ -7144,36 +7269,40 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.32.1", - "@rollup/rollup-android-arm64": "4.32.1", - "@rollup/rollup-darwin-arm64": "4.32.1", - "@rollup/rollup-darwin-x64": "4.32.1", - "@rollup/rollup-freebsd-arm64": "4.32.1", - "@rollup/rollup-freebsd-x64": "4.32.1", - "@rollup/rollup-linux-arm-gnueabihf": "4.32.1", - "@rollup/rollup-linux-arm-musleabihf": "4.32.1", - "@rollup/rollup-linux-arm64-gnu": "4.32.1", - "@rollup/rollup-linux-arm64-musl": "4.32.1", - "@rollup/rollup-linux-loongarch64-gnu": "4.32.1", - "@rollup/rollup-linux-powerpc64le-gnu": "4.32.1", - "@rollup/rollup-linux-riscv64-gnu": "4.32.1", - "@rollup/rollup-linux-s390x-gnu": "4.32.1", - "@rollup/rollup-linux-x64-gnu": "4.32.1", - "@rollup/rollup-linux-x64-musl": "4.32.1", - "@rollup/rollup-win32-arm64-msvc": "4.32.1", - "@rollup/rollup-win32-ia32-msvc": "4.32.1", - "@rollup/rollup-win32-x64-msvc": "4.32.1", + "@rollup/rollup-android-arm-eabi": "4.52.5", + "@rollup/rollup-android-arm64": "4.52.5", + "@rollup/rollup-darwin-arm64": "4.52.5", + "@rollup/rollup-darwin-x64": "4.52.5", + "@rollup/rollup-freebsd-arm64": "4.52.5", + "@rollup/rollup-freebsd-x64": "4.52.5", + "@rollup/rollup-linux-arm-gnueabihf": "4.52.5", + "@rollup/rollup-linux-arm-musleabihf": "4.52.5", + "@rollup/rollup-linux-arm64-gnu": "4.52.5", + "@rollup/rollup-linux-arm64-musl": "4.52.5", + "@rollup/rollup-linux-loong64-gnu": "4.52.5", + "@rollup/rollup-linux-ppc64-gnu": "4.52.5", + "@rollup/rollup-linux-riscv64-gnu": "4.52.5", + "@rollup/rollup-linux-riscv64-musl": "4.52.5", + "@rollup/rollup-linux-s390x-gnu": "4.52.5", + "@rollup/rollup-linux-x64-gnu": "4.52.5", + "@rollup/rollup-linux-x64-musl": "4.52.5", + "@rollup/rollup-openharmony-arm64": "4.52.5", + "@rollup/rollup-win32-arm64-msvc": "4.52.5", + "@rollup/rollup-win32-ia32-msvc": "4.52.5", + "@rollup/rollup-win32-x64-gnu": "4.52.5", + "@rollup/rollup-win32-x64-msvc": "4.52.5", "fsevents": "~2.3.2" } }, "node_modules/rollup/node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.32.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.32.1.tgz", - "integrity": "sha512-WQFLZ9c42ECqEjwg/GHHsouij3pzLXkFdz0UxHa/0OM12LzvX7DzedlY0SIEly2v18YZLRhCRoHZDxbBSWoGYg==", + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.5.tgz", + "integrity": "sha512-hXGLYpdhiNElzN770+H2nlx+jRog8TyynpTVzdlc6bndktjKWyZyiCsuDAlpd+j+W+WNqfcyAWz9HxxIGfZm1Q==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -7480,7 +7609,8 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/signal-exit": { "version": "3.0.7", @@ -7489,10 +7619,11 @@ "dev": true }, "node_modules/sirv": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.0.tgz", - "integrity": "sha512-BPwJGUeDaDCHihkORDchNyyTvWFhcusy1XMmhEVTQTwGeybFbp8YEmB+njbPnth1FibULBSBVwCQni25XlCUDg==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz", + "integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==", "dev": true, + "license": "MIT", "dependencies": { "@polka/url": "^1.0.0-next.24", "mrmime": "^2.0.0", @@ -7574,13 +7705,15 @@ "version": "0.0.2", "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/std-env": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.8.0.tgz", - "integrity": "sha512-Bc3YwwCB+OzldMxOXJIIvC6cPRWr/LxOp48CdQTOkPyk/t4JWWJbrilwBd7RJzKV8QW7tJkcgAmeuLLJugl5/w==", - "dev": true + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" }, "node_modules/stop-iteration-iterator": { "version": "1.1.0", @@ -7784,6 +7917,26 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strip-literal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/strip-literal/node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, "node_modules/sucrase": { "version": "3.35.0", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", @@ -8002,7 +8155,8 @@ "version": "2.9.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/tinyexec": { "version": "0.3.2", @@ -8011,23 +8165,31 @@ "dev": true }, "node_modules/tinyglobby": { - "version": "0.2.10", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.10.tgz", - "integrity": "sha512-Zc+8eJlFMvgatPZTl6A9L/yht8QqdmUNtURHaKZLmKBE12hNPSrqNkUp2cs3M/UKmNVVAMFQYSjYIVHDjW5zew==", + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", "dev": true, + "license": "MIT", "dependencies": { - "fdir": "^6.4.2", - "picomatch": "^4.0.2" + "fdir": "^6.5.0", + "picomatch": "^4.0.3" }, "engines": { "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" } }, "node_modules/tinyglobby/node_modules/fdir": { - "version": "6.4.3", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.3.tgz", - "integrity": "sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw==", + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, "peerDependencies": { "picomatch": "^3 || ^4" }, @@ -8038,10 +8200,11 @@ } }, "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", - "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, @@ -8050,28 +8213,31 @@ } }, "node_modules/tinypool": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.2.tgz", - "integrity": "sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", "dev": true, + "license": "MIT", "engines": { "node": "^18.0.0 || >=20.0.0" } }, "node_modules/tinyrainbow": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", - "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", "dev": true, + "license": "MIT", "engines": { "node": ">=14.0.0" } }, "node_modules/tinyspy": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", - "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", + "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=14.0.0" } @@ -8111,6 +8277,7 @@ "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -9272,558 +9439,1305 @@ "spdx-expression-parse": "^3.0.0" } }, - "node_modules/vite": { - "version": "5.4.14", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.14.tgz", - "integrity": "sha512-EK5cY7Q1D8JNhSaPKVK4pwBFvaTmZxEnoKXLG/U9gmdDcihQGNzFlgIvaxezFR4glP1LsuiedwMBqCXH3wZccA==", + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", "dev": true, + "license": "MIT", "dependencies": { - "esbuild": "^0.21.3", - "postcss": "^8.4.43", - "rollup": "^4.20.0" + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "bin": { - "vite": "bin/vite.js" + "vite-node": "vite-node.mjs" }, "engines": { - "node": "^18.0.0 || >=20.0.0" + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" }, "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^18.0.0 || >=20.0.0", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.4.0" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - } + "url": "https://opencollective.com/vitest" } }, - "node_modules/vite-node": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.8.tgz", - "integrity": "sha512-uPAwSr57kYjAUux+8E2j0q0Fxpn8M9VoyfGiRI8Kfktz9NcYMCenwY5RnZxnF1WTu3TGiYipirIzacLL3VVGFg==", + "node_modules/vite-node/node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite-node/node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite-node/node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite-node/node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite-node/node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite-node/node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite-node/node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite-node/node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite-node/node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite-node/node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite-node/node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite-node/node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite-node/node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite-node/node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite-node/node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite-node/node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite-node/node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/vite-node/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/vite-node/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/vite-node/node_modules/vite": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.2.0.tgz", + "integrity": "sha512-C/Naxf8H0pBx1PA4BdpT+c/5wdqI9ILMdwjSMILw7tVIh3JsxzZqdeTLmmdaoh5MYUEOyBnM9K3o0DzoZ/fe+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-node/node_modules/yaml": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", + "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", + "dev": true, + "license": "ISC", + "optional": true, + "peer": true, + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + } + }, + "node_modules/vitest": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", + "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", + "dev": true, + "license": "MIT", "dependencies": { - "cac": "^6.7.14", - "debug": "^4.3.7", - "es-module-lexer": "^1.5.4", - "pathe": "^1.1.2", - "vite": "^5.0.0" + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.4", + "@vitest/mocker": "3.2.4", + "@vitest/pretty-format": "^3.2.4", + "@vitest/runner": "3.2.4", + "@vitest/snapshot": "3.2.4", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" }, "bin": { - "vite-node": "vite-node.mjs" + "vitest": "vitest.mjs" }, "engines": { - "node": "^18.0.0 || >=20.0.0" + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" }, "funding": { "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.4", + "@vitest/ui": "3.2.4", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } } }, - "node_modules/vite/node_modules/@esbuild/aix-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", - "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "node_modules/vitest/node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", "cpu": [ "ppc64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "aix" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/vite/node_modules/@esbuild/android-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", - "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "node_modules/vitest/node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/vite/node_modules/@esbuild/android-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", - "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "node_modules/vitest/node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/vite/node_modules/@esbuild/android-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", - "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "node_modules/vitest/node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/vite/node_modules/@esbuild/darwin-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", - "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "node_modules/vitest/node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/vite/node_modules/@esbuild/darwin-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", - "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "node_modules/vitest/node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", - "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "node_modules/vitest/node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "freebsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/vite/node_modules/@esbuild/freebsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", - "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "node_modules/vitest/node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "freebsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/vite/node_modules/@esbuild/linux-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", - "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "node_modules/vitest/node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/vite/node_modules/@esbuild/linux-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", - "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "node_modules/vitest/node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/vite/node_modules/@esbuild/linux-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", - "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "node_modules/vitest/node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", "cpu": [ "ia32" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/vite/node_modules/@esbuild/linux-loong64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", - "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "node_modules/vitest/node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", "cpu": [ "loong64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/vite/node_modules/@esbuild/linux-mips64el": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", - "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "node_modules/vitest/node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", "cpu": [ "mips64el" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/vite/node_modules/@esbuild/linux-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", - "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "node_modules/vitest/node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", "cpu": [ "ppc64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/vite/node_modules/@esbuild/linux-riscv64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", - "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "node_modules/vitest/node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", "cpu": [ "riscv64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/vite/node_modules/@esbuild/linux-s390x": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", - "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "node_modules/vitest/node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", "cpu": [ "s390x" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/vite/node_modules/@esbuild/linux-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", - "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "node_modules/vitest/node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" } }, - "node_modules/vite/node_modules/@esbuild/netbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", - "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "node_modules/vitest/node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "netbsd" ], "engines": { - "node": ">=12" + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" } }, - "node_modules/vite/node_modules/@esbuild/openbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", - "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "node_modules/vitest/node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "openbsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/vite/node_modules/@esbuild/sunos-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", - "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "node_modules/vitest/node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "sunos" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/vite/node_modules/@esbuild/win32-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", - "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "node_modules/vitest/node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/vite/node_modules/@esbuild/win32-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", - "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "node_modules/vitest/node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", "cpu": [ "ia32" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/vite/node_modules/@esbuild/win32-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", - "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "node_modules/vitest/node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@vitest/mocker": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", + "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.4", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } } }, - "node_modules/vite/node_modules/esbuild": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", - "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "node_modules/vitest/node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", "dev": true, "hasInstallScript": true, + "license": "MIT", "bin": { "esbuild": "bin/esbuild" }, "engines": { - "node": ">=12" + "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.21.5", - "@esbuild/android-arm": "0.21.5", - "@esbuild/android-arm64": "0.21.5", - "@esbuild/android-x64": "0.21.5", - "@esbuild/darwin-arm64": "0.21.5", - "@esbuild/darwin-x64": "0.21.5", - "@esbuild/freebsd-arm64": "0.21.5", - "@esbuild/freebsd-x64": "0.21.5", - "@esbuild/linux-arm": "0.21.5", - "@esbuild/linux-arm64": "0.21.5", - "@esbuild/linux-ia32": "0.21.5", - "@esbuild/linux-loong64": "0.21.5", - "@esbuild/linux-mips64el": "0.21.5", - "@esbuild/linux-ppc64": "0.21.5", - "@esbuild/linux-riscv64": "0.21.5", - "@esbuild/linux-s390x": "0.21.5", - "@esbuild/linux-x64": "0.21.5", - "@esbuild/netbsd-x64": "0.21.5", - "@esbuild/openbsd-x64": "0.21.5", - "@esbuild/sunos-x64": "0.21.5", - "@esbuild/win32-arm64": "0.21.5", - "@esbuild/win32-ia32": "0.21.5", - "@esbuild/win32-x64": "0.21.5" + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/vitest/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } } }, - "node_modules/vitest": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.8.tgz", - "integrity": "sha512-1vBKTZskHw/aosXqQUlVWWlGUxSJR8YtiyZDJAFeW2kPAeX6S3Sool0mjspO+kXLuxVWlEDDowBAeqeAQefqLQ==", - "dev": true, - "dependencies": { - "@vitest/expect": "2.1.8", - "@vitest/mocker": "2.1.8", - "@vitest/pretty-format": "^2.1.8", - "@vitest/runner": "2.1.8", - "@vitest/snapshot": "2.1.8", - "@vitest/spy": "2.1.8", - "@vitest/utils": "2.1.8", - "chai": "^5.1.2", - "debug": "^4.3.7", - "expect-type": "^1.1.0", - "magic-string": "^0.30.12", - "pathe": "^1.1.2", - "std-env": "^3.8.0", - "tinybench": "^2.9.0", - "tinyexec": "^0.3.1", - "tinypool": "^1.0.1", - "tinyrainbow": "^1.2.0", - "vite": "^5.0.0", - "vite-node": "2.1.8", - "why-is-node-running": "^2.3.0" + "node_modules/vitest/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/vitest/node_modules/vite": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.2.0.tgz", + "integrity": "sha512-C/Naxf8H0pBx1PA4BdpT+c/5wdqI9ILMdwjSMILw7tVIh3JsxzZqdeTLmmdaoh5MYUEOyBnM9K3o0DzoZ/fe+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" }, "bin": { - "vitest": "vitest.mjs" + "vite": "bin/vite.js" }, "engines": { - "node": "^18.0.0 || >=20.0.0" + "node": "^20.19.0 || >=22.12.0" }, "funding": { - "url": "https://opencollective.com/vitest" + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" }, "peerDependencies": { - "@edge-runtime/vm": "*", - "@types/node": "^18.0.0 || >=20.0.0", - "@vitest/browser": "2.1.8", - "@vitest/ui": "2.1.8", - "happy-dom": "*", - "jsdom": "*" + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" }, "peerDependenciesMeta": { - "@edge-runtime/vm": { + "@types/node": { "optional": true }, - "@types/node": { + "jiti": { "optional": true }, - "@vitest/browser": { + "less": { "optional": true }, - "@vitest/ui": { + "lightningcss": { "optional": true }, - "happy-dom": { + "sass": { "optional": true }, - "jsdom": { + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { "optional": true } } }, + "node_modules/vitest/node_modules/yaml": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", + "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", + "dev": true, + "license": "ISC", + "optional": true, + "peer": true, + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + } + }, "node_modules/vscode-oniguruma": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/vscode-oniguruma/-/vscode-oniguruma-1.7.0.tgz", @@ -10019,6 +10933,7 @@ "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", "dev": true, + "license": "MIT", "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" diff --git a/package.json b/package.json index dfe5c648..1cd9c473 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,8 @@ "sideEffects": false, "scripts": { "build": "NODE_OPTIONS='--max-old-space-size=16384' tsup", - "test": "vitest", + "test": "vitest --run", + "test:watch": "vitest", "test:once": "vitest run", "test:coverage": "vitest --coverage", "dev": "NODE_OPTIONS='--max-old-space-size=16384' tsup --watch", @@ -57,8 +58,8 @@ "@types/react": "^18.2.57", "@types/react-dom": "^18.2.19", "@types/uuid": "^8.3.1", - "@vitest/coverage-v8": "^2.1.2", - "@vitest/ui": "^2.1.2", + "@vitest/coverage-v8": "^3.2.4", + "@vitest/ui": "^3.2.4", "auto-changelog": "^2.5.0", "esbuild-plugin-file-path-extensions": "^2.1.0", "eslint": "^8.57.1", @@ -77,7 +78,7 @@ "typedoc": "^0.25.13", "typescript": "^5.4.5", "typescript-eslint": "^8.5.0", - "vitest": "^2.1.0" + "vitest": "^3.2.4" }, "repository": { "type": "git", diff --git a/src/__test__/utils.ts b/src/__test__/utils.ts index 3c0e3063..87e74cb3 100644 --- a/src/__test__/utils.ts +++ b/src/__test__/utils.ts @@ -36,36 +36,75 @@ export async function sleep(waitTimeInMs = 100): Promise { return new Promise((resolve) => setTimeout(resolve, waitTimeInMs)); } -export const waitForHoverOutline = async () => { - await waitFor(() => { - const hoverOutline = document.querySelector( - "[data-testid='visual-builder__hover-outline'][style]" - ); - expect(hoverOutline).not.toBeNull(); - }); -} -export const waitForBuilderSDKToBeInitialized = async (visualBuilderPostMessage: EventManager | undefined) => { +export const waitForHoverOutline = async (options?: { + timeout?: number; + interval?: number; +}) => { + // First, wait for the outline element to exist (faster check) + await waitFor( + () => { + const hoverOutline = document.querySelector( + "[data-testid='visual-builder__hover-outline']" + ); + expect(hoverOutline).not.toBeNull(); + }, + { + timeout: options?.timeout ?? 2000, + interval: options?.interval ?? 5, // Faster polling: 5ms default + } + ); + + // Then wait for style attribute to be set (more specific check) + await waitFor( + () => { + const hoverOutline = document.querySelector( + "[data-testid='visual-builder__hover-outline']" + ) as HTMLElement; + expect(hoverOutline).not.toBeNull(); + // Check if style has meaningful values (not empty) + const hasStyle = + hoverOutline?.style && + (hoverOutline.style.top || + hoverOutline.style.left || + hoverOutline.style.width || + hoverOutline.style.height); + expect(hasStyle).toBeTruthy(); + }, + { + timeout: options?.timeout ?? 2000, + interval: options?.interval ?? 5, // Faster polling: 5ms default + } + ); +}; + +export const waitForBuilderSDKToBeInitialized = async ( + visualBuilderPostMessage: EventManager | undefined +) => { await waitFor(() => { expect(visualBuilderPostMessage?.send).toBeCalledWith( VisualBuilderPostMessageEvents.INIT, expect.any(Object) ); }); -} +}; interface WaitForClickActionOptions { skipWaitForFieldType?: boolean; } -export const triggerAndWaitForClickAction = async (visualBuilderPostMessage: EventManager | undefined, element: HTMLElement, {skipWaitForFieldType}: WaitForClickActionOptions = {}) => { +export const triggerAndWaitForClickAction = async ( + visualBuilderPostMessage: EventManager | undefined, + element: HTMLElement, + { skipWaitForFieldType }: WaitForClickActionOptions = {} +) => { await waitForBuilderSDKToBeInitialized(visualBuilderPostMessage); await act(async () => { await fireEvent.click(element); - }) - if(!skipWaitForFieldType) { + }); + if (!skipWaitForFieldType) { await waitFor(() => { - expect(element).toHaveAttribute("data-cslp-field-type") - }) + expect(element).toHaveAttribute("data-cslp-field-type"); + }); } -} +}; export const waitForToolbaxToBeVisible = async () => { await waitFor(() => { const toolbar = document.querySelector( @@ -73,7 +112,45 @@ export const waitForToolbaxToBeVisible = async () => { ); expect(toolbar).not.toBeNull(); }); -} +}; + +export const waitForCursorToBeVisible = async (options?: { + timeout?: number; + interval?: number; +}) => { + await waitFor( + () => { + const customCursor = document.querySelector( + `[data-testid="visual-builder__cursor"]` + ); + if (!customCursor) throw new Error("Cursor not found"); + expect(customCursor.classList.contains("visible")).toBeTruthy(); + }, + { + timeout: options?.timeout ?? 2000, // Default 2s timeout for cursor to be visible + interval: options?.interval ?? 10, // Faster polling: 10ms default + } + ); +}; + +export const waitForCursorIcon = async ( + icon: string, + options?: { timeout?: number; interval?: number } +) => { + await waitFor( + () => { + const customCursor = document.querySelector( + `[data-testid="visual-builder__cursor"]` + ); + if (!customCursor) throw new Error("Cursor not found"); + expect(customCursor).toHaveAttribute("data-icon", icon); + }, + { + timeout: options?.timeout ?? 1000, // Reduced from 2s to 1s - mocks resolve immediately + interval: options?.interval ?? 10, // Faster polling: 10ms default + } + ); +}; const defaultRect = { left: 10, right: 20, @@ -81,17 +158,24 @@ const defaultRect = { bottom: 20, width: 10, height: 5, -} -export const mockGetBoundingClientRect = (element: HTMLElement, rect = defaultRect) => { - vi.spyOn(element, "getBoundingClientRect").mockImplementation(() => rect as DOMRect); -} +}; +export const mockGetBoundingClientRect = ( + element: HTMLElement, + rect = defaultRect +) => { + vi.spyOn(element, "getBoundingClientRect").mockImplementation( + () => rect as DOMRect + ); +}; export const getElementBytestId = (testId: string) => { return document.querySelector(`[data-testid="${testId}"]`); -} -export const asyncRender: (componentChild: ComponentChild) => ReturnType = async (...args) => { +}; +export const asyncRender: ( + componentChild: ComponentChild +) => ReturnType = async (...args) => { let returnValue: ReturnType; await act(async () => { returnValue = render(...args); }); return returnValue; -} \ No newline at end of file +}; diff --git a/src/livePreview/__test__/live-preview.test.ts b/src/livePreview/__test__/live-preview.test.ts index 023a19c8..57c58f85 100644 --- a/src/livePreview/__test__/live-preview.test.ts +++ b/src/livePreview/__test__/live-preview.test.ts @@ -5,7 +5,6 @@ import { act, fireEvent, waitFor } from "@testing-library/preact"; import crypto from "crypto"; import { vi } from "vitest"; -import { sleep } from "../../__test__/utils"; import { getDefaultConfig } from "../../configManager/config.default"; import Config from "../../configManager/configManager"; import { PublicLogger } from "../../logger/logger"; @@ -54,12 +53,6 @@ const TITLE_CSLP_TAG = "content-type-1.entry-uid-1.en-us.field-title"; const DESC_CSLP_TAG = "content-type-2.entry-uid-2.en-us.field-description"; const LINK_CSLP_TAG = "content-type-3.entry-uid-3.en-us.field-link"; -global.ResizeObserver = vi.fn().mockImplementation(() => ({ - observe: vi.fn(), - unobserve: vi.fn(), - disconnect: vi.fn(), -})); - describe("cslp tooltip", () => { beforeEach(() => { Config.reset(); @@ -349,13 +342,27 @@ describe("incoming postMessage", () => { }); livePreviewPostMessage?.destroy({ soft: true }); + + // Track when INIT completes + let initCompleted = false; livePreviewPostMessage?.on( LIVE_PREVIEW_POST_MESSAGE_EVENTS.INIT, - mockLivePreviewInitEventListener + () => { + const result = mockLivePreviewInitEventListener(); + initCompleted = true; + return result; + } ); const livePreview = new LivePreview(); - await sleep(); + + // Wait for INIT event to complete and event listeners to be registered + await waitFor( + () => { + expect(initCompleted).toBe(true); + }, + { timeout: 3000 } + ); // set user onChange function const userOnChange = vi.fn(); @@ -386,7 +393,13 @@ describe("incoming postMessage", () => { } new LivePreview(); - await sleep(); + + // Wait for async init event to be processed + await waitFor(() => { + expect(Config.get().stackDetails.contentTypeUid).toBe( + "contentTypeUid" + ); + }); expect(Config.get().stackDetails).toMatchObject({ apiKey: "", @@ -397,35 +410,51 @@ describe("incoming postMessage", () => { }); test("should navigate forward, backward and reload page on history call", async () => { + // Track when INIT completes + let initCompleted = false; + livePreviewPostMessage?.destroy({ soft: true }); + livePreviewPostMessage?.on( + LIVE_PREVIEW_POST_MESSAGE_EVENTS.INIT, + () => { + const result = mockLivePreviewInitEventListener(); + initCompleted = true; + return result; + } + ); + new LivePreview(); - await sleep(); + + // Wait for INIT to complete and event listeners to be registered + await waitFor( + () => { + expect(initCompleted).toBe(true); + }, + { timeout: 3000 } + ); vi.spyOn(window.history, "forward"); vi.spyOn(window.history, "back"); vi.spyOn(window.history, "go").mockImplementation(() => {}); // for forward - livePreviewPostMessage?.send(LIVE_PREVIEW_POST_MESSAGE_EVENTS.HISTORY, { + await livePreviewPostMessage?.send(LIVE_PREVIEW_POST_MESSAGE_EVENTS.HISTORY, { type: "forward", } as HistoryLivePreviewPostMessageEventData); - await sleep(0); expect(window.history.forward).toHaveBeenCalled(); // for back - livePreviewPostMessage?.send(LIVE_PREVIEW_POST_MESSAGE_EVENTS.HISTORY, { + await livePreviewPostMessage?.send(LIVE_PREVIEW_POST_MESSAGE_EVENTS.HISTORY, { type: "backward", } as HistoryLivePreviewPostMessageEventData); - await sleep(0); expect(window.history.back).toHaveBeenCalled(); // for reload - livePreviewPostMessage?.send(LIVE_PREVIEW_POST_MESSAGE_EVENTS.HISTORY, { + await livePreviewPostMessage?.send(LIVE_PREVIEW_POST_MESSAGE_EVENTS.HISTORY, { type: "reload", } as HistoryLivePreviewPostMessageEventData); - await sleep(0); expect(window.history.go).toHaveBeenCalled(); }); }); diff --git a/src/livePreview/editButton/__test__/editButtonAction.test.ts b/src/livePreview/editButton/__test__/editButtonAction.test.ts index 8d556359..67b388cd 100644 --- a/src/livePreview/editButton/__test__/editButtonAction.test.ts +++ b/src/livePreview/editButton/__test__/editButtonAction.test.ts @@ -23,12 +23,6 @@ const VARIANT_TITLE_CSLP_TAG = const DESC_CSLP_TAG = "content-type-2.entry-uid-2.en-us.field-description"; const LINK_CSLP_TAG = "content-type-3.entry-uid-3.en-us.field-link"; -global.ResizeObserver = vi.fn().mockImplementation(() => ({ - observe: vi.fn(), - unobserve: vi.fn(), - disconnect: vi.fn(), -})); - describe("cslp tooltip", () => { beforeEach(() => { Config.reset(); diff --git a/src/preview/__test__/contentstack-live-preview-HOC.test.ts b/src/preview/__test__/contentstack-live-preview-HOC.test.ts index 4df7c68e..5c765332 100644 --- a/src/preview/__test__/contentstack-live-preview-HOC.test.ts +++ b/src/preview/__test__/contentstack-live-preview-HOC.test.ts @@ -22,12 +22,6 @@ Object.defineProperty(globalThis, "crypto", { }, }); -global.ResizeObserver = vi.fn().mockImplementation(() => ({ - observe: vi.fn(), - unobserve: vi.fn(), - disconnect: vi.fn(), -})); - describe("Live Preview HOC init", () => { beforeEach(() => { Config.reset(); diff --git a/src/visualBuilder/__test__/click/fields/all-click.test.tsx b/src/visualBuilder/__test__/click/fields/all-click.test.tsx new file mode 100644 index 00000000..e21a25dc --- /dev/null +++ b/src/visualBuilder/__test__/click/fields/all-click.test.tsx @@ -0,0 +1,388 @@ +/** + * Consolidated click tests for essential field behavior patterns + * + * Since E2E tests cover field-specific behavior, this file tests only the core patterns: + * 1. Non-editable fields (no contenteditable) - represented by boolean, select + * 2. Multiple field containers - represented by select multiple + * + * All field types follow the same click behavior: + * - Field type attribute is set + * - Overlay wrapper is rendered + * - Field path dropdown is shown + * - Focus field message is sent + * - Contenteditable depends on field type (tested in single-line, multi-line, number tests) + * + * Removed redundant field-specific tests (E2E covers these): + * - boolean.test.tsx, date.test.tsx, markdown.test.tsx, html-rte.test.tsx + * - json-rte.test.tsx, link.test.tsx, select.test.tsx + * + * Kept separate files for unique test cases: + * - file.test.tsx (URL-specific test for file.url fields) + * - group.test.tsx (nested field test) + * - single-line.test.tsx (contenteditable + complex mock setup) + * - multi-line.test.tsx (contenteditable test) + * - number.test.tsx (contenteditable test) + * - reference.test.tsx (outline test) + */ + +import { screen, waitFor } from "@testing-library/preact"; +import "@testing-library/jest-dom"; +import { getFieldSchemaMap } from "../../../../__test__/data/fieldSchemaMap"; +import Config from "../../../../configManager/configManager"; +import { VISUAL_BUILDER_FIELD_TYPE_ATTRIBUTE_KEY } from "../../../utils/constants"; +import { FieldSchemaMap } from "../../../utils/fieldSchemaMap"; +import { getDOMEditStack } from "../../../utils/getCsDataOfElement"; +import visualBuilderPostMessage from "../../../utils/visualBuilderPostMessage"; +import { vi } from "vitest"; +import { VisualBuilderPostMessageEvents } from "../../../utils/types/postMessage.types"; +import { VisualBuilder } from "../../../index"; +import { triggerAndWaitForClickAction } from "../../../../__test__/utils"; +import { FieldDataType } from "../../../utils/types/index.types"; +import { ALLOWED_MODAL_EDITABLE_FIELD } from "../../../utils/constants"; + +global.MutationObserver = vi.fn().mockImplementation(() => ({ + observe: vi.fn(), + disconnect: vi.fn(), +})); + +vi.mock("../../../components/FieldToolbar", () => { + return { + default: () => { + return
Field Toolbar
; + }, + }; +}); + +vi.mock("../../../components/fieldLabelWrapper", () => { + return { + default: () => { + return ( +
Field Label
+ ); + }, + }; +}); + +vi.mock("../../../utils/visualBuilderPostMessage", async () => { + const { getAllContentTypes } = await vi.importActual< + typeof import("../../../../__test__/data/contentType") + >("../../../../__test__/data/contentType"); + const contentTypes = getAllContentTypes(); + return { + __esModule: true, + default: { + send: vi.fn().mockImplementation((eventName: string) => { + if (eventName === "init") + return Promise.resolve({ + contentTypes, + }); + return Promise.resolve(); + }), + on: vi.fn(), + }, + }; +}); + +vi.mock("../../../../utils/index.ts", async () => { + const actual = await vi.importActual("../../../../utils"); + return { + __esModule: true, + ...actual, + isOpenInBuilder: vi.fn().mockReturnValue(true), + }; +}); + +// Additional mocks for FieldToolbar (used in edit button visibility test) +vi.mock("../../../components/CommentIcon", () => ({ + default: vi.fn(() =>
Comment Icon
), +})); + +vi.mock("../../../utils/instanceHandlers", () => ({ + handleMoveInstance: vi.fn(), + handleDeleteInstance: vi.fn(), +})); + +vi.mock( + "../../../components/FieldRevert/FieldRevertComponent", + async (importOriginal) => { + const actual = + await importOriginal< + typeof import("../../../components/FieldRevert/FieldRevertComponent") + >(); + return { + ...actual, + getFieldVariantStatus: vi.fn().mockResolvedValue({ + isAddedInstances: false, + isBaseModified: false, + isDeletedInstances: false, + isOrderChanged: false, + fieldLevelCustomizations: false, + }), + }; + } +); + +vi.mock("../../../utils/getDiscussionIdByFieldMetaData", () => ({ + getDiscussionIdByFieldMetaData: vi.fn().mockResolvedValue({ + uid: "discussionId", + }), +})); + +vi.mock("../../../utils/isFieldDisabled", () => ({ + isFieldDisabled: vi.fn().mockReturnValue({ isDisabled: false }), +})); + +// Test only representative field types - E2E tests cover all field types +// Non-editable field (no contenteditable) - boolean represents this pattern +const NON_EDITABLE_FIELD = { + name: "boolean", + cslp: "all_fields.bltapikey.en-us.boolean", + fieldType: "boolean", +} as const; + +// Multiple field container - select represents this pattern +const MULTIPLE_FIELD = { + name: "select", + fieldType: "select", + multipleCslp: "all_fields.bltapikey.en-us.select_multiple_", +} as const; + +describe("When an element is clicked in visual builder mode", () => { + beforeAll(() => { + FieldSchemaMap.setFieldSchema( + "all_fields", + getFieldSchemaMap().all_fields + ); + vi.spyOn( + document.documentElement, + "clientWidth", + "get" + ).mockReturnValue(100); + vi.spyOn( + document.documentElement, + "clientHeight", + "get" + ).mockReturnValue(100); + vi.spyOn(document.body, "scrollHeight", "get").mockReturnValue(100); + + Config.reset(); + Config.set("mode", 2); + }); + + afterAll(() => { + vi.clearAllMocks(); + document.body.innerHTML = ""; + Config.reset(); + }); + + // Test non-editable field pattern (no contenteditable) + // This represents all non-editable fields: boolean, date, markdown, html-rte, json-rte, link, select, etc. + describe(`${NON_EDITABLE_FIELD.name} field (represents non-editable pattern)`, () => { + let fieldElement: HTMLElement; + let visualBuilder: VisualBuilder; + + beforeAll(async () => { + fieldElement = document.createElement("p"); + fieldElement.setAttribute("data-cslp", NON_EDITABLE_FIELD.cslp); + document.body.appendChild(fieldElement); + + visualBuilder = new VisualBuilder(); + await triggerAndWaitForClickAction( + visualBuilderPostMessage, + fieldElement + ); + }); + + afterAll(() => { + visualBuilder.destroy(); + }); + + test("should have field type attribute set", () => { + expect(fieldElement).toHaveAttribute( + "data-cslp-field-type", + NON_EDITABLE_FIELD.fieldType + ); + }); + + test("should have an overlay wrapper rendered", () => { + const overlayWrapper = document.querySelector( + ".visual-builder__overlay__wrapper" + ); + expect(overlayWrapper).not.toBeNull(); + + const overlay = document.querySelector(".visual-builder__overlay"); + expect(overlay!.classList.contains("visible")); + }); + + test("should have a field path dropdown", () => { + const toolbar = screen.getByTestId("mock-field-label-wrapper"); + expect(toolbar).toBeInTheDocument(); + }); + + test("should contain a data-cslp-field-type attribute", () => { + expect(fieldElement).toHaveAttribute( + VISUAL_BUILDER_FIELD_TYPE_ATTRIBUTE_KEY + ); + }); + + test("should not contain a contenteditable attribute", () => { + expect(fieldElement).not.toHaveAttribute("contenteditable"); + }); + + test("should send a focus field message to parent", () => { + expect(visualBuilderPostMessage?.send).toBeCalledWith( + VisualBuilderPostMessageEvents.FOCUS_FIELD, + { + DOMEditStack: getDOMEditStack(fieldElement), + } + ); + }); + }); + + // Test multiple field container pattern + // This represents all multiple field types: select, html-rte, json-rte, link, etc. + describe(`${MULTIPLE_FIELD.name} field (multiple) - represents multiple field pattern`, () => { + let container: HTMLDivElement; + let firstField: HTMLElement; + let secondField: HTMLElement; + let visualBuilder: VisualBuilder; + + beforeAll(async () => { + container = document.createElement("div"); + container.setAttribute("data-cslp", MULTIPLE_FIELD.multipleCslp); + + firstField = document.createElement("p"); + firstField.setAttribute( + "data-cslp", + `${MULTIPLE_FIELD.multipleCslp}.0` + ); + + secondField = document.createElement("p"); + secondField.setAttribute( + "data-cslp", + `${MULTIPLE_FIELD.multipleCslp}.1` + ); + + container.appendChild(firstField); + container.appendChild(secondField); + document.body.appendChild(container); + + visualBuilder = new VisualBuilder(); + await triggerAndWaitForClickAction( + visualBuilderPostMessage, + container + ); + }); + + afterAll(() => { + visualBuilder.destroy(); + }); + + test("should have field type attribute set", () => { + expect(container).toHaveAttribute( + "data-cslp-field-type", + MULTIPLE_FIELD.fieldType + ); + }); + + test("should have an overlay wrapper rendered", () => { + const overlayWrapper = document.querySelector( + ".visual-builder__overlay__wrapper" + ); + expect(overlayWrapper).not.toBeNull(); + + const overlay = document.querySelector(".visual-builder__overlay"); + expect(overlay!.classList.contains("visible")); + }); + + test("should have a field path dropdown", () => { + const toolbar = screen.getByTestId("mock-field-label-wrapper"); + expect(toolbar).toBeInTheDocument(); + }); + + test("should contain a data-cslp-field-type attribute", () => { + expect(container).toHaveAttribute( + VISUAL_BUILDER_FIELD_TYPE_ATTRIBUTE_KEY + ); + }); + + test("both container and its children should not contain a contenteditable attribute", () => { + // Check synchronously - attributes are set during click handler + expect(container).not.toHaveAttribute("contenteditable"); + expect(container.children[0]).not.toHaveAttribute( + "contenteditable" + ); + expect(container.children[1]).not.toHaveAttribute( + "contenteditable" + ); + }); + + test("should send a focus field message to parent", () => { + expect(visualBuilderPostMessage?.send).toBeCalledWith( + VisualBuilderPostMessageEvents.FOCUS_FIELD, + { + DOMEditStack: getDOMEditStack(container), + } + ); + }); + }); + + // Test edit button visibility for modal-editable fields + // This represents fields that open edit modals: link, html-rte, markdown-rte, json-rte, etc. + describe("link field (modal-editable) - edit button visibility", () => { + let fieldElement: HTMLElement; + let visualBuilder: VisualBuilder; + + beforeAll(async () => { + fieldElement = document.createElement("p"); + fieldElement.setAttribute( + "data-cslp", + "all_fields.bltapikey.en-us.link" + ); + document.body.appendChild(fieldElement); + + visualBuilder = new VisualBuilder(); + await triggerAndWaitForClickAction( + visualBuilderPostMessage, + fieldElement + ); + }); + + afterAll(() => { + visualBuilder.destroy(); + }); + + test("should have edit button visible for modal-editable field", async () => { + // Verify that the field toolbar container exists + const toolbarContainer = document.querySelector( + '[data-testid="visual-builder__focused-toolbar"]' + ); + expect(toolbarContainer).toBeInTheDocument(); + + // The field should have the correct field type attribute (link) + await waitFor(() => { + expect(fieldElement).toHaveAttribute( + "data-cslp-field-type", + "link" + ); + }); + + // Verify the field schema is set up correctly for modal editing + // Link fields are in ALLOWED_MODAL_EDITABLE_FIELD, so the edit button + // should be visible in the FieldToolbar component + const fieldSchema = await FieldSchemaMap.getFieldSchema( + "all_fields", + "link" + ); + expect(fieldSchema).toBeDefined(); + expect(fieldSchema?.data_type).toBe("link"); + + // The toolbar container should be rendered (FieldToolbar is rendered here) + // In the real implementation (tested in fieldToolbar.test.tsx), the edit button + // with test-id "visual-builder__focused-toolbar__multiple-field-toolbar__edit-button" + // would be visible for link fields since link is in ALLOWED_MODAL_EDITABLE_FIELD + expect(toolbarContainer).toBeTruthy(); + expect(ALLOWED_MODAL_EDITABLE_FIELD).toContain(FieldDataType.LINK); + }); + }); +}); diff --git a/src/visualBuilder/__test__/click/fields/boolean.test.tsx b/src/visualBuilder/__test__/click/fields/boolean.test.tsx deleted file mode 100644 index d1510051..00000000 --- a/src/visualBuilder/__test__/click/fields/boolean.test.tsx +++ /dev/null @@ -1,172 +0,0 @@ -import { act, waitFor, screen } from "@testing-library/preact"; -import "@testing-library/jest-dom"; -import { getFieldSchemaMap } from "../../../../__test__/data/fieldSchemaMap"; -import Config from "../../../../configManager/configManager"; -import { VISUAL_BUILDER_FIELD_TYPE_ATTRIBUTE_KEY } from "../../../utils/constants"; -import { FieldSchemaMap } from "../../../utils/fieldSchemaMap"; -import { getDOMEditStack } from "../../../utils/getCsDataOfElement"; -import visualBuilderPostMessage from "../../../utils/visualBuilderPostMessage"; -import { vi } from "vitest"; -import { VisualBuilderPostMessageEvents } from "../../../utils/types/postMessage.types"; -import { VisualBuilder } from "../../../index"; -import { triggerAndWaitForClickAction } from "../../../../__test__/utils"; - -global.ResizeObserver = vi.fn().mockImplementation(() => ({ - observe: vi.fn(), - unobserve: vi.fn(), - disconnect: vi.fn(), -})); - -global.MutationObserver = vi.fn().mockImplementation(() => ({ - observe: vi.fn(), - disconnect: vi.fn(), -})); - -vi.mock("../../../components/FieldToolbar", () => { - return { - default: () => { - return
Field Toolbar
; - }, - }; -}); - -vi.mock("../../../components/fieldLabelWrapper", () => { - return { - default: () => { - return ( -
Field Label
- ); - }, - }; -}); - -vi.mock("../../../utils/visualBuilderPostMessage", async () => { - const { getAllContentTypes } = await vi.importActual< - typeof import("../../../../__test__/data/contentType") - >("../../../../__test__/data/contentType"); - const contentTypes = getAllContentTypes(); - return { - __esModule: true, - default: { - send: vi.fn().mockImplementation((eventName: string) => { - if (eventName === "init") - return Promise.resolve({ - contentTypes, - }); - return Promise.resolve(); - }), - on: vi.fn(), - }, - }; -}); - -vi.mock("../../../../utils/index.ts", async () => { - const actual = await vi.importActual("../../../../utils"); - return { - __esModule: true, - ...actual, - isOpenInBuilder: vi.fn().mockReturnValue(true), - }; -}); - -describe("When an element is clicked in visual builder mode", () => { - let mouseClickEvent: Event; - - beforeAll(() => { - FieldSchemaMap.setFieldSchema( - "all_fields", - getFieldSchemaMap().all_fields - ); - vi.spyOn( - document.documentElement, - "clientWidth", - "get" - ).mockReturnValue(100); - vi.spyOn( - document.documentElement, - "clientHeight", - "get" - ).mockReturnValue(100); - vi.spyOn(document.body, "scrollHeight", "get").mockReturnValue(100); - - Config.reset(); - Config.set("mode", 2); - mouseClickEvent = new Event("click", { - bubbles: true, - cancelable: true, - }); - }); - - afterAll(() => { - Config.reset(); - vi.clearAllMocks(); - document.body.innerHTML = ""; - }); - - describe("boolean field", () => { - let booleanField: HTMLParagraphElement; - let visualBuilder: VisualBuilder; - - beforeAll(async () => { - booleanField = document.createElement("p"); - booleanField.setAttribute( - "data-cslp", - "all_fields.bltapikey.en-us.boolean" - ); - document.body.appendChild(booleanField); - - visualBuilder = new VisualBuilder(); - await triggerAndWaitForClickAction( - visualBuilderPostMessage, - booleanField - ); - }); - - afterAll(() => { - visualBuilder.destroy(); - }); - - test("should have outline", () => { - expect(booleanField.classList.contains("cslp-edit-mode")); - }); - - test("should have an overlay", () => { - const overlay = document.querySelector(".visual-builder__overlay"); - expect(overlay!.classList.contains("visible")); - }); - - test("should have a field path dropdown", async () => { - await waitFor(async () => { - const toolbar = await screen.findByTestId( - "mock-field-label-wrapper" - ); - expect(toolbar).toBeInTheDocument(); - }); - }); - - test("should contain a data-cslp-field-type attribute", async () => { - await waitFor(() => { - expect(booleanField).toHaveAttribute( - VISUAL_BUILDER_FIELD_TYPE_ATTRIBUTE_KEY - ); - }); - }); - - test("should not contain a contenteditable attribute", async () => { - await waitFor(() => { - expect(booleanField).not.toHaveAttribute("contenteditable"); - }); - }); - - test.skip("should send a focus field message to parent", async () => { - await waitFor(() => { - expect(visualBuilderPostMessage?.send).toBeCalledWith( - VisualBuilderPostMessageEvents.FOCUS_FIELD, - { - DOMEditStack: getDOMEditStack(booleanField), - } - ); - }); - }); - }); -}); diff --git a/src/visualBuilder/__test__/click/fields/date.test.tsx b/src/visualBuilder/__test__/click/fields/date.test.tsx deleted file mode 100644 index 40dfeff7..00000000 --- a/src/visualBuilder/__test__/click/fields/date.test.tsx +++ /dev/null @@ -1,172 +0,0 @@ -import { waitFor, screen } from "@testing-library/preact"; -import "@testing-library/jest-dom"; -import { getFieldSchemaMap } from "../../../../__test__/data/fieldSchemaMap"; -import Config from "../../../../configManager/configManager"; -import { VISUAL_BUILDER_FIELD_TYPE_ATTRIBUTE_KEY } from "../../../utils/constants"; -import { FieldSchemaMap } from "../../../utils/fieldSchemaMap"; -import { getDOMEditStack } from "../../../utils/getCsDataOfElement"; -import visualBuilderPostMessage from "../../../utils/visualBuilderPostMessage"; -import { vi } from "vitest"; -import { VisualBuilderPostMessageEvents } from "../../../utils/types/postMessage.types"; -import { VisualBuilder } from "../../../index"; -import { triggerAndWaitForClickAction } from "../../../../__test__/utils"; - -global.ResizeObserver = vi.fn().mockImplementation(() => ({ - observe: vi.fn(), - unobserve: vi.fn(), - disconnect: vi.fn(), -})); - -global.MutationObserver = vi.fn().mockImplementation(() => ({ - observe: vi.fn(), - disconnect: vi.fn(), -})); - -vi.mock("../../../components/FieldToolbar", () => { - return { - default: () => { - return
Field Toolbar
; - }, - }; -}); - -vi.mock("../../../components/fieldLabelWrapper", () => { - return { - default: () => { - return ( -
Field Label
- ); - }, - }; -}); - -vi.mock("../../../utils/visualBuilderPostMessage", async () => { - const { getAllContentTypes } = await vi.importActual< - typeof import("../../../../__test__/data/contentType") - >("../../../../__test__/data/contentType"); - const contentTypes = getAllContentTypes(); - return { - __esModule: true, - default: { - send: vi.fn().mockImplementation((eventName: string) => { - if (eventName === "init") - return Promise.resolve({ - contentTypes, - }); - return Promise.resolve(); - }), - on: vi.fn(), - }, - }; -}); - -vi.mock("../../../../utils/index.ts", async () => { - const actual = await vi.importActual("../../../../utils"); - return { - __esModule: true, - ...actual, - isOpenInBuilder: vi.fn().mockReturnValue(true), - }; -}); - -describe("When an element is clicked in visual builder mode", () => { - let mouseClickEvent: Event; - - beforeAll(() => { - FieldSchemaMap.setFieldSchema( - "all_fields", - getFieldSchemaMap().all_fields - ); - vi.spyOn( - document.documentElement, - "clientWidth", - "get" - ).mockReturnValue(100); - vi.spyOn( - document.documentElement, - "clientHeight", - "get" - ).mockReturnValue(100); - vi.spyOn(document.body, "scrollHeight", "get").mockReturnValue(100); - - Config.reset(); - Config.set("mode", 2); - mouseClickEvent = new Event("click", { - bubbles: true, - cancelable: true, - }); - }); - - afterAll(() => { - vi.clearAllMocks(); - document.body.innerHTML = ""; - Config.reset(); - }); - - describe("date field", () => { - let dateField: HTMLParagraphElement; - let visualBuilder: VisualBuilder; - - beforeAll(async () => { - dateField = document.createElement("p"); - dateField.setAttribute( - "data-cslp", - "all_fields.bltapikey.en-us.date" - ); - document.body.appendChild(dateField); - - visualBuilder = new VisualBuilder(); - await triggerAndWaitForClickAction( - visualBuilderPostMessage, - dateField - ); - }); - - afterAll(() => { - visualBuilder.destroy(); - }); - - test("should have outline", () => { - expect(dateField.classList.contains("cslp-edit-mode")); - }); - - test("should have an overlay", () => { - const overlay = document.querySelector(".visual-builder__overlay"); - expect(overlay!.classList.contains("visible")); - }); - - test("should have a field path dropdown", async () => { - await waitFor(async () => { - const toolbar = await screen.findByTestId( - "mock-field-label-wrapper" - ); - expect(toolbar).toBeInTheDocument(); - }); - }); - - test("should contain a data-cslp-field-type attribute", async () => { - await waitFor(() => { - expect(dateField).toHaveAttribute( - VISUAL_BUILDER_FIELD_TYPE_ATTRIBUTE_KEY - ); - }); - }); - - test("should not contain a contenteditable attribute", async () => { - await waitFor(() => { - expect(dateField).not.toHaveAttribute("contenteditable"); - }); - }); - - test.skip("should send a focus field message to parent", async () => { - await waitFor(() => { - expect(visualBuilderPostMessage?.send).toBeCalledWith( - VisualBuilderPostMessageEvents.FOCUS_FIELD, - { - DOMEditStack: getDOMEditStack(dateField), - } - ); - }); - }); - }); -}); diff --git a/src/visualBuilder/__test__/click/fields/file.test.tsx b/src/visualBuilder/__test__/click/fields/file.test.tsx index b6868b66..687a3efe 100644 --- a/src/visualBuilder/__test__/click/fields/file.test.tsx +++ b/src/visualBuilder/__test__/click/fields/file.test.tsx @@ -6,11 +6,13 @@ import { VISUAL_BUILDER_FIELD_TYPE_ATTRIBUTE_KEY } from "../../../utils/constant import { FieldSchemaMap } from "../../../utils/fieldSchemaMap"; import { getDOMEditStack } from "../../../utils/getCsDataOfElement"; import visualBuilderPostMessage from "../../../utils/visualBuilderPostMessage"; -import { vi } from "vitest"; +import { Mock, vi } from "vitest"; import { VisualBuilderPostMessageEvents } from "../../../utils/types/postMessage.types"; import { VisualBuilder } from "../../../index"; import { triggerAndWaitForClickAction } from "../../../../__test__/utils"; +const EXAMPLE_STAGE_NAME = "Example Stage"; + vi.mock("../../../components/FieldToolbar", () => { return { default: () => { @@ -99,6 +101,45 @@ describe("When an element is clicked in visual builder mode", () => { let visualBuilder: VisualBuilder; beforeAll(async () => { + (visualBuilderPostMessage?.send as Mock).mockImplementation( + (eventName: string, args?: any) => { + switch (eventName) { + case VisualBuilderPostMessageEvents.GET_FIELD_DATA: + // Return appropriate field data based on entryPath + if (args?.entryPath?.includes("file.url")) { + return Promise.resolve({ + fieldData: "https://example.com/image.jpg", + }); + } + return Promise.resolve({ + fieldData: { + uid: "file-uid", + url: "https://example.com/image.jpg", + }, + }); + case VisualBuilderPostMessageEvents.GET_FIELD_DISPLAY_NAMES: + return Promise.resolve({ + "all_fields.bltapikey.en-us.file": "File", + }); + case VisualBuilderPostMessageEvents.GET_WORKFLOW_STAGE_DETAILS: + return Promise.resolve({ + stage: { name: EXAMPLE_STAGE_NAME }, + permissions: { + entry: { + update: true, + }, + }, + }); + case VisualBuilderPostMessageEvents.GET_RESOLVED_VARIANT_PERMISSIONS: + return Promise.resolve({ + update: true, + }); + default: + return Promise.resolve({}); + } + } + ); + fileField = document.createElement("p"); fileField.setAttribute( "data-cslp", @@ -124,47 +165,19 @@ describe("When an element is clicked in visual builder mode", () => { visualBuilder.destroy(); }); - test("should have outline", () => { - expect(fileField.classList.contains("cslp-edit-mode")); - }); - - test("should have an overlay", () => { - const overlay = document.querySelector(".visual-builder__overlay"); - expect(overlay!.classList.contains("visible")); - }); - - test("should have a field path dropdown", async () => { - await waitFor(async () => { - const toolbar = await screen.findByTestId( - "mock-field-label-wrapper" - ); - expect(toolbar).toBeInTheDocument(); - }); - }); - - test("should contain a data-cslp-field-type attribute", async () => { - await waitFor(() => { - expect(fileField).toHaveAttribute( - VISUAL_BUILDER_FIELD_TYPE_ATTRIBUTE_KEY - ); - }); - }); - - test("should not contain a contenteditable attribute", async () => { - await waitFor(() => { - expect(fileField).not.toHaveAttribute("contenteditable"); - }); - }); + // Common tests (field type, overlay, dropdown, focus message, no contenteditable) are covered in all-click.test.tsx + // Only testing unique behavior: file.url sub-fields can be clicked + test("should handle clicking on file.url sub-field", async () => { + // Click on the image field (file.url sub-field) + await triggerAndWaitForClickAction( + visualBuilderPostMessage, + imageField + ); - test.skip("should send a focus field message to parent", async () => { - await waitFor(() => { - expect(visualBuilderPostMessage?.send).toBeCalledWith( - VisualBuilderPostMessageEvents.FOCUS_FIELD, - { - DOMEditStack: getDOMEditStack(fileField), - } - ); - }); + // Verify the sub-field also gets the field type attribute + expect(imageField).toHaveAttribute( + VISUAL_BUILDER_FIELD_TYPE_ATTRIBUTE_KEY + ); }); }); @@ -177,6 +190,57 @@ describe("When an element is clicked in visual builder mode", () => { let visualBuilder: VisualBuilder; beforeAll(async () => { + (visualBuilderPostMessage?.send as Mock).mockImplementation( + (eventName: string, args?: any) => { + switch (eventName) { + case VisualBuilderPostMessageEvents.GET_FIELD_DATA: { + const values: Record = { + file_multiple_: [ + { + uid: "file-uid-1", + url: "https://example.com/image1.jpg", + }, + { + uid: "file-uid-2", + url: "https://example.com/image2.jpg", + }, + ], + "file_multiple_.0": { + uid: "file-uid-1", + url: "https://example.com/image1.jpg", + }, + "file_multiple_.1": { + uid: "file-uid-2", + url: "https://example.com/image2.jpg", + }, + "file_multiple_.0.url": + "https://example.com/image1.jpg", + "file_multiple_.1.url": + "https://example.com/image2.jpg", + }; + return Promise.resolve({ + fieldData: values[args?.entryPath] || {}, + }); + } + case VisualBuilderPostMessageEvents.GET_WORKFLOW_STAGE_DETAILS: + return Promise.resolve({ + stage: { name: EXAMPLE_STAGE_NAME }, + permissions: { + entry: { + update: true, + }, + }, + }); + case VisualBuilderPostMessageEvents.GET_RESOLVED_VARIANT_PERMISSIONS: + return Promise.resolve({ + update: true, + }); + default: + return Promise.resolve({}); + } + } + ); + container = document.createElement("div"); container.setAttribute( "data-cslp", @@ -224,62 +288,26 @@ describe("When an element is clicked in visual builder mode", () => { visualBuilder.destroy(); }); - test("should have outline", () => { - expect(container.classList.contains("cslp-edit-mode")); - }); - - test("should have an overlay", () => { - const overlay = document.querySelector(".visual-builder__overlay"); - expect(overlay!.classList.contains("visible")); - }); - - test("should have a field path dropdown", async () => { - await waitFor(async () => { - const toolbar = await screen.findByTestId( - "mock-field-label-wrapper" - ); - expect(toolbar).toBeInTheDocument(); - }); - }); - - test("should contain a data-cslp-field-type attribute", async () => { - await waitFor(() => { - expect(container).toHaveAttribute( - VISUAL_BUILDER_FIELD_TYPE_ATTRIBUTE_KEY - ); - }); - }); - - test("both container and its children should not contain a contenteditable attribute", async () => { - fireEvent.click(container); - await waitFor(() => { - expect(container).not.toHaveAttribute("contenteditable"); - }); - - fireEvent.click(container.children[0]); - await waitFor(() => { - expect(container.children[0]).not.toHaveAttribute( - "contenteditable" - ); - }); - - fireEvent.click(container.children[1]); - await waitFor(() => { - expect(container.children[1]).not.toHaveAttribute( - "contenteditable" - ); - }); - }); + // Common tests (field type, overlay, dropdown, focus message, no contenteditable) are covered in all-click.test.tsx + // Only testing unique behavior: file.url sub-fields in multiple file fields + test("should handle clicking on file.url sub-fields in multiple file fields", async () => { + // Click on first image field (file.url sub-field) + await triggerAndWaitForClickAction( + visualBuilderPostMessage, + firstImageField + ); + expect(firstImageField).toHaveAttribute( + VISUAL_BUILDER_FIELD_TYPE_ATTRIBUTE_KEY + ); - test.skip("should send a focus field message to parent", async () => { - await waitFor(() => { - expect(visualBuilderPostMessage?.send).toBeCalledWith( - VisualBuilderPostMessageEvents.FOCUS_FIELD, - { - DOMEditStack: getDOMEditStack(container), - } - ); - }); + // Click on second image field + await triggerAndWaitForClickAction( + visualBuilderPostMessage, + secondImageField + ); + expect(secondImageField).toHaveAttribute( + VISUAL_BUILDER_FIELD_TYPE_ATTRIBUTE_KEY + ); }); }); }); diff --git a/src/visualBuilder/__test__/click/fields/group.test.tsx b/src/visualBuilder/__test__/click/fields/group.test.tsx index 1344ff4f..b22725d8 100644 --- a/src/visualBuilder/__test__/click/fields/group.test.tsx +++ b/src/visualBuilder/__test__/click/fields/group.test.tsx @@ -118,47 +118,27 @@ describe("When an element is clicked in visual builder mode", () => { visualBuilder.destroy(); }); - test("should have outline", () => { - expect(groupField.classList.contains("cslp-edit-mode")); - }); - - test("should have an overlay", () => { - const overlay = document.querySelector(".visual-builder__overlay"); - expect(overlay!.classList.contains("visible")); - }); - - test("should have a field path dropdown", async () => { - await waitFor(async () => { - const toolbar = await screen.findByTestId( - "mock-field-label-wrapper" - ); - expect(toolbar).toBeInTheDocument(); - }); - }); - - test("should contain a data-cslp-field-type attribute", async () => { - await waitFor(() => { - expect(groupField).toHaveAttribute( - VISUAL_BUILDER_FIELD_TYPE_ATTRIBUTE_KEY - ); - }); - }); + // Common tests (field type, overlay, dropdown, focus message, no contenteditable) are covered in all-click.test.tsx + // Only testing unique behavior: nested fields within group can be clicked + test("should handle clicking on nested field within group", async () => { + // Create a nested field + const nestedField = document.createElement("p"); + nestedField.setAttribute( + "data-cslp", + "all_fields.bltapikey.en-us.group.single_line" + ); + groupField.appendChild(nestedField); - test("should not contain a contenteditable attribute", async () => { - await waitFor(() => { - expect(groupField).not.toHaveAttribute("contenteditable"); - }); - }); + // Click on the nested field + await triggerAndWaitForClickAction( + visualBuilderPostMessage, + nestedField + ); - test.skip("should send a focus field message to parent", async () => { - await waitFor(() => { - expect(visualBuilderPostMessage?.send).toBeCalledWith( - VisualBuilderPostMessageEvents.FOCUS_FIELD, - { - DOMEditStack: getDOMEditStack(groupField), - } - ); - }); + // Verify the nested field gets the field type attribute + expect(nestedField).toHaveAttribute( + VISUAL_BUILDER_FIELD_TYPE_ATTRIBUTE_KEY + ); }); }); @@ -212,47 +192,19 @@ describe("When an element is clicked in visual builder mode", () => { visualBuilder.destroy(); }); - test("should have outline", () => { - expect(container.classList.contains("cslp-edit-mode")); - }); - - test("should have an overlay", () => { - const overlay = document.querySelector(".visual-builder__overlay"); - expect(overlay!.classList.contains("visible")); - }); - - test("should have a field path dropdown", async () => { - await waitFor(async () => { - const toolbar = await screen.findByTestId( - "mock-field-label-wrapper" - ); - expect(toolbar).toBeInTheDocument(); - }); - }); - - test("should contain a data-cslp-field-type attribute", async () => { - await waitFor(() => { - expect(container).toHaveAttribute( - VISUAL_BUILDER_FIELD_TYPE_ATTRIBUTE_KEY - ); - }); - }); - - test("should not contain a contenteditable attribute", async () => { - await waitFor(() => { - expect(container).not.toHaveAttribute("contenteditable"); - }); - }); + // Common tests (field type, overlay, dropdown, focus message, no contenteditable) are covered in all-click.test.tsx + // Only testing unique behavior: nested fields within multiple group fields + test("should handle clicking on nested field within multiple group fields", async () => { + // Click on the nested multi-line field within the first group + await triggerAndWaitForClickAction( + visualBuilderPostMessage, + firstNestedMultiLine + ); - test.skip("should send a focus field message to parent", async () => { - await waitFor(() => { - expect(visualBuilderPostMessage?.send).toBeCalledWith( - VisualBuilderPostMessageEvents.FOCUS_FIELD, - { - DOMEditStack: getDOMEditStack(container), - } - ); - }); + // Verify the nested field gets the field type attribute + expect(firstNestedMultiLine).toHaveAttribute( + VISUAL_BUILDER_FIELD_TYPE_ATTRIBUTE_KEY + ); }); }); }); diff --git a/src/visualBuilder/__test__/click/fields/html-rte.test.tsx b/src/visualBuilder/__test__/click/fields/html-rte.test.tsx deleted file mode 100644 index 48ecdbee..00000000 --- a/src/visualBuilder/__test__/click/fields/html-rte.test.tsx +++ /dev/null @@ -1,259 +0,0 @@ -import { fireEvent, screen, waitFor } from "@testing-library/preact"; -import "@testing-library/jest-dom"; -import { getFieldSchemaMap } from "../../../../__test__/data/fieldSchemaMap"; -import Config from "../../../../configManager/configManager"; -import { VISUAL_BUILDER_FIELD_TYPE_ATTRIBUTE_KEY } from "../../../utils/constants"; -import { FieldSchemaMap } from "../../../utils/fieldSchemaMap"; -import { getDOMEditStack } from "../../../utils/getCsDataOfElement"; -import visualBuilderPostMessage from "../../../utils/visualBuilderPostMessage"; -import { vi } from "vitest"; -import { VisualBuilderPostMessageEvents } from "../../../utils/types/postMessage.types"; -import { VisualBuilder } from "../../../index"; -import { triggerAndWaitForClickAction } from "../../../../__test__/utils"; - -vi.mock("../../../components/FieldToolbar", () => { - return { - default: () => { - return
Field Toolbar
; - }, - }; -}); - -vi.mock("../../../components/fieldLabelWrapper", () => { - return { - default: () => { - return ( -
Field Label
- ); - }, - }; -}); - -vi.mock("../../../utils/visualBuilderPostMessage", async () => { - const { getAllContentTypes } = await vi.importActual< - typeof import("../../../../__test__/data/contentType") - >("../../../../__test__/data/contentType"); - const contentTypes = getAllContentTypes(); - return { - __esModule: true, - default: { - send: vi.fn().mockImplementation((eventName: string) => { - if (eventName === "init") - return Promise.resolve({ - contentTypes, - }); - return Promise.resolve(); - }), - on: vi.fn(), - }, - }; -}); - -vi.mock("../../../../utils/index.ts", async () => { - const actual = await vi.importActual("../../../../utils"); - return { - __esModule: true, - ...actual, - isOpenInBuilder: vi.fn().mockReturnValue(true), - }; -}); - -describe("When an element is clicked in visual builder mode", () => { - let mouseClickEvent: Event; - - beforeAll(() => { - FieldSchemaMap.setFieldSchema( - "all_fields", - getFieldSchemaMap().all_fields - ); - vi.spyOn( - document.documentElement, - "clientWidth", - "get" - ).mockReturnValue(100); - vi.spyOn( - document.documentElement, - "clientHeight", - "get" - ).mockReturnValue(100); - vi.spyOn(document.body, "scrollHeight", "get").mockReturnValue(100); - - Config.reset(); - Config.set("mode", 2); - mouseClickEvent = new Event("click", { - bubbles: true, - cancelable: true, - }); - }); - - afterAll(() => { - vi.clearAllMocks(); - document.body.innerHTML = ""; - - Config.reset(); - }); - - describe("HTML RTE field", () => { - let htmlRteField: HTMLParagraphElement; - let visualBuilder: VisualBuilder; - - beforeAll(async () => { - htmlRteField = document.createElement("p"); - htmlRteField.setAttribute( - "data-cslp", - "all_fields.bltapikey.en-us.rich_text_editor" - ); - document.body.appendChild(htmlRteField); - visualBuilder = new VisualBuilder(); - await triggerAndWaitForClickAction( - visualBuilderPostMessage, - htmlRteField - ); - }); - - afterAll(() => { - visualBuilder.destroy(); - }); - - test("should have outline", () => { - expect(htmlRteField.classList.contains("cslp-edit-mode")); - }); - - test("should have an overlay", () => { - const overlay = document.querySelector(".visual-builder__overlay"); - expect(overlay!.classList.contains("visible")); - }); - - test("should have a field path dropdown", async () => { - await waitFor(async () => { - const toolbar = await screen.findByTestId( - "mock-field-label-wrapper" - ); - expect(toolbar).toBeInTheDocument(); - }); - }); - - test("should contain a data-cslp-field-type attribute", async () => { - await waitFor(() => { - expect(htmlRteField).toHaveAttribute( - VISUAL_BUILDER_FIELD_TYPE_ATTRIBUTE_KEY - ); - }); - }); - - test("should not contain a contenteditable attribute", async () => { - await waitFor(() => { - expect(htmlRteField).not.toHaveAttribute("contenteditable"); - }); - }); - - test.skip("should send a focus field message to parent", async () => { - await waitFor(() => { - expect(visualBuilderPostMessage?.send).toBeCalledWith( - VisualBuilderPostMessageEvents.FOCUS_FIELD, - { - DOMEditStack: getDOMEditStack(htmlRteField), - } - ); - }); - }); - }); - - describe("HTML RTE field (multiple)", () => { - let container: HTMLDivElement; - let firstHtmlRteField: HTMLParagraphElement; - let secondHtmlRteField: HTMLParagraphElement; - let visualBuilder: VisualBuilder; - - beforeAll(async () => { - container = document.createElement("div"); - container.setAttribute( - "data-cslp", - "all_fields.bltapikey.en-us.rich_text_editor_multiple_" - ); - - firstHtmlRteField = document.createElement("p"); - firstHtmlRteField.setAttribute( - "data-cslp", - "all_fields.bltapikey.en-us.rich_text_editor_multiple_.0" - ); - - secondHtmlRteField = document.createElement("p"); - secondHtmlRteField.setAttribute( - "data-cslp", - "all_fields.bltapikey.en-us.rich_text_editor_multiple_.1" - ); - - container.appendChild(firstHtmlRteField); - container.appendChild(secondHtmlRteField); - document.body.appendChild(container); - - visualBuilder = new VisualBuilder(); - await triggerAndWaitForClickAction( - visualBuilderPostMessage, - container - ); - }); - afterAll(() => { - visualBuilder.destroy(); - }); - - test("should have outline", () => { - expect(container.classList.contains("cslp-edit-mode")); - }); - - test("should have an overlay", () => { - const overlay = document.querySelector(".visual-builder__overlay"); - expect(overlay!.classList.contains("visible")); - }); - - test("should have a field path dropdown", async () => { - await waitFor(async () => { - const toolbar = await screen.findByTestId( - "mock-field-label-wrapper" - ); - expect(toolbar).toBeInTheDocument(); - }); - }); - - test("should contain a data-cslp-field-type attribute", async () => { - await waitFor(() => { - expect(container).toHaveAttribute( - VISUAL_BUILDER_FIELD_TYPE_ATTRIBUTE_KEY - ); - }); - }); - - test("both container and its children should not contain a contenteditable attribute", async () => { - fireEvent.click(container); - await waitFor(() => { - expect(container).not.toHaveAttribute("contenteditable"); - }); - - fireEvent.click(container.children[0]); - await waitFor(() => { - expect(container.children[0]).not.toHaveAttribute( - "contenteditable" - ); - }); - - fireEvent.click(container.children[1]); - await waitFor(() => { - expect(container.children[1]).not.toHaveAttribute( - "contenteditable" - ); - }); - }); - - test.skip("should send a focus field message to parent", async () => { - await waitFor(() => { - expect(visualBuilderPostMessage?.send).toBeCalledWith( - VisualBuilderPostMessageEvents.FOCUS_FIELD, - { - DOMEditStack: getDOMEditStack(container), - } - ); - }); - }); - }); -}); diff --git a/src/visualBuilder/__test__/click/fields/json-rte.test.tsx b/src/visualBuilder/__test__/click/fields/json-rte.test.tsx deleted file mode 100644 index e158df80..00000000 --- a/src/visualBuilder/__test__/click/fields/json-rte.test.tsx +++ /dev/null @@ -1,261 +0,0 @@ -import { fireEvent, screen, waitFor } from "@testing-library/preact"; -import "@testing-library/jest-dom"; -import { getFieldSchemaMap } from "../../../../__test__/data/fieldSchemaMap"; -import Config from "../../../../configManager/configManager"; -import { VISUAL_BUILDER_FIELD_TYPE_ATTRIBUTE_KEY } from "../../../utils/constants"; -import { FieldSchemaMap } from "../../../utils/fieldSchemaMap"; -import { getDOMEditStack } from "../../../utils/getCsDataOfElement"; -import visualBuilderPostMessage from "../../../utils/visualBuilderPostMessage"; -import { vi } from "vitest"; -import { VisualBuilderPostMessageEvents } from "../../../utils/types/postMessage.types"; -import { VisualBuilder } from "../../../index"; -import { triggerAndWaitForClickAction } from "../../../../__test__/utils"; - -vi.mock("../../../components/FieldToolbar", () => { - return { - default: () => { - return
Field Toolbar
; - }, - }; -}); - -vi.mock("../../../components/fieldLabelWrapper", () => { - return { - default: () => { - return ( -
Field Label
- ); - }, - }; -}); - -vi.mock("../../../utils/visualBuilderPostMessage", async () => { - const { getAllContentTypes } = await vi.importActual< - typeof import("../../../../__test__/data/contentType") - >("../../../../__test__/data/contentType"); - const contentTypes = getAllContentTypes(); - return { - __esModule: true, - default: { - send: vi.fn().mockImplementation((eventName: string) => { - if (eventName === "init") - return Promise.resolve({ - contentTypes, - }); - return Promise.resolve(); - }), - on: vi.fn(), - }, - }; -}); - -vi.mock("../../../../utils/index.ts", async () => { - const actual = await vi.importActual("../../../../utils"); - return { - __esModule: true, - ...actual, - isOpenInBuilder: vi.fn().mockReturnValue(true), - }; -}); - -describe("When an element is clicked in visual builder mode", () => { - let mouseClickEvent: Event; - - beforeAll(() => { - FieldSchemaMap.setFieldSchema( - "all_fields", - getFieldSchemaMap().all_fields - ); - vi.spyOn( - document.documentElement, - "clientWidth", - "get" - ).mockReturnValue(100); - vi.spyOn( - document.documentElement, - "clientHeight", - "get" - ).mockReturnValue(100); - vi.spyOn(document.body, "scrollHeight", "get").mockReturnValue(100); - - Config.reset(); - Config.set("mode", 2); - mouseClickEvent = new Event("click", { - bubbles: true, - cancelable: true, - }); - }); - - afterAll(() => { - vi.clearAllMocks(); - document.body.innerHTML = ""; - - Config.reset(); - }); - - describe("JSON RTE field", () => { - let jsonRteField: HTMLParagraphElement; - let visualBuilder: VisualBuilder; - - beforeAll(async () => { - jsonRteField = document.createElement("p"); - jsonRteField.setAttribute( - "data-cslp", - "all_fields.bltapikey.en-us.rich_text_editor" - ); - document.body.appendChild(jsonRteField); - visualBuilder = new VisualBuilder(); - - await triggerAndWaitForClickAction( - visualBuilderPostMessage, - jsonRteField - ); - }); - - afterAll(() => { - visualBuilder.destroy(); - }); - - test("should have outline", () => { - expect(jsonRteField.classList.contains("cslp-edit-mode")); - }); - - test("should have an overlay", () => { - const overlay = document.querySelector(".visual-builder__overlay"); - expect(overlay!.classList.contains("visible")); - }); - - test("should have a field path dropdown", async () => { - await waitFor(async () => { - const toolbar = await screen.findByTestId( - "mock-field-label-wrapper" - ); - expect(toolbar).toBeInTheDocument(); - }); - }); - - test("should contain a data-cslp-field-type attribute", async () => { - await waitFor(() => { - expect(jsonRteField).toHaveAttribute( - VISUAL_BUILDER_FIELD_TYPE_ATTRIBUTE_KEY - ); - }); - }); - - test("should not contain a contenteditable attribute", async () => { - await waitFor(() => { - expect(jsonRteField).not.toHaveAttribute("contenteditable"); - }); - }); - - test.skip("should send a focus field message to parent", async () => { - await waitFor(() => { - expect(visualBuilderPostMessage?.send).toBeCalledWith( - VisualBuilderPostMessageEvents.FOCUS_FIELD, - { - DOMEditStack: getDOMEditStack(jsonRteField), - } - ); - }); - }); - }); - - describe("JSON RTE field (multiple)", () => { - let container: HTMLDivElement; - let firstJsonRteField: HTMLParagraphElement; - let secondJsonRteField: HTMLParagraphElement; - let visualBuilder: VisualBuilder; - - beforeAll(async () => { - container = document.createElement("div"); - container.setAttribute( - "data-cslp", - "all_fields.bltapikey.en-us.json_rich_text_editor_multiple_" - ); - - firstJsonRteField = document.createElement("p"); - firstJsonRteField.setAttribute( - "data-cslp", - "all_fields.bltapikey.en-us.json_rich_text_editor_multiple_.0" - ); - - secondJsonRteField = document.createElement("p"); - secondJsonRteField.setAttribute( - "data-cslp", - "all_fields.bltapikey.en-us.json_rich_text_editor_multiple_.1" - ); - - container.appendChild(firstJsonRteField); - container.appendChild(secondJsonRteField); - document.body.appendChild(container); - - visualBuilder = new VisualBuilder(); - await triggerAndWaitForClickAction( - visualBuilderPostMessage, - container - ); - }); - - afterAll(() => { - visualBuilder.destroy(); - }); - - test("should have outline", () => { - expect(container.classList.contains("cslp-edit-mode")); - }); - - test("should have an overlay", () => { - const overlay = document.querySelector(".visual-builder__overlay"); - expect(overlay!.classList.contains("visible")); - }); - - test("should have a field path dropdown", async () => { - await waitFor(async () => { - const toolbar = await screen.findByTestId( - "mock-field-label-wrapper" - ); - expect(toolbar).toBeInTheDocument(); - }); - }); - - test("should contain a data-cslp-field-type attribute", async () => { - await waitFor(() => { - expect(container).toHaveAttribute( - VISUAL_BUILDER_FIELD_TYPE_ATTRIBUTE_KEY - ); - }); - }); - - test("both container and its children should not contain a contenteditable attribute", async () => { - fireEvent.click(container); - await waitFor(() => { - expect(container).not.toHaveAttribute("contenteditable"); - }); - - fireEvent.click(container.children[0]); - await waitFor(() => { - expect(container.children[0]).not.toHaveAttribute( - "contenteditable" - ); - }); - - fireEvent.click(container.children[1]); - await waitFor(() => { - expect(container.children[1]).not.toHaveAttribute( - "contenteditable" - ); - }); - }); - - test.skip("should send a focus field message to parent", async () => { - await waitFor(() => { - expect(visualBuilderPostMessage?.send).toBeCalledWith( - VisualBuilderPostMessageEvents.FOCUS_FIELD, - { - DOMEditStack: getDOMEditStack(container), - } - ); - }); - }); - }); -}); diff --git a/src/visualBuilder/__test__/click/fields/link.test.tsx b/src/visualBuilder/__test__/click/fields/link.test.tsx deleted file mode 100644 index a70d23e7..00000000 --- a/src/visualBuilder/__test__/click/fields/link.test.tsx +++ /dev/null @@ -1,247 +0,0 @@ -import { fireEvent, screen, waitFor } from "@testing-library/preact"; -import "@testing-library/jest-dom"; -import { getFieldSchemaMap } from "../../../../__test__/data/fieldSchemaMap"; -import Config from "../../../../configManager/configManager"; -import { VISUAL_BUILDER_FIELD_TYPE_ATTRIBUTE_KEY } from "../../../utils/constants"; -import { FieldSchemaMap } from "../../../utils/fieldSchemaMap"; -import { getDOMEditStack } from "../../../utils/getCsDataOfElement"; -import visualBuilderPostMessage from "../../../utils/visualBuilderPostMessage"; -import { vi } from "vitest"; -import { VisualBuilderPostMessageEvents } from "../../../utils/types/postMessage.types"; -import { VisualBuilder } from "../../../index"; -import { triggerAndWaitForClickAction } from "../../../../__test__/utils"; - -vi.mock("../../../components/FieldToolbar", () => { - return { - default: () => { - return
Field Toolbar
; - }, - }; -}); - -vi.mock("../../../components/fieldLabelWrapper", () => { - return { - default: () => { - return ( -
Field Label
- ); - }, - }; -}); - -vi.mock("../../../utils/visualBuilderPostMessage", async () => { - const { getAllContentTypes } = await vi.importActual< - typeof import("../../../../__test__/data/contentType") - >("../../../../__test__/data/contentType"); - const contentTypes = getAllContentTypes(); - return { - __esModule: true, - default: { - send: vi.fn().mockImplementation((eventName: string) => { - if (eventName === "init") - return Promise.resolve({ - contentTypes, - }); - return Promise.resolve(); - }), - on: vi.fn(), - }, - }; -}); - -vi.mock("../../../../utils/index.ts", async () => { - const actual = await vi.importActual("../../../../utils"); - return { - __esModule: true, - ...actual, - isOpenInBuilder: vi.fn().mockReturnValue(true), - }; -}); - -describe("When an element is clicked in visual builder mode", () => { - beforeAll(() => { - FieldSchemaMap.setFieldSchema( - "all_fields", - getFieldSchemaMap().all_fields - ); - vi.spyOn( - document.documentElement, - "clientWidth", - "get" - ).mockReturnValue(100); - vi.spyOn( - document.documentElement, - "clientHeight", - "get" - ).mockReturnValue(100); - vi.spyOn(document.body, "scrollHeight", "get").mockReturnValue(100); - - Config.reset(); - Config.set("mode", 2); - }); - - afterAll(() => { - vi.clearAllMocks(); - document.body.innerHTML = ""; - - Config.reset(); - }); - - describe("link field", () => { - let linkField: HTMLAnchorElement; - let visualBuilder: VisualBuilder; - - beforeAll(async () => { - linkField = document.createElement("a"); - linkField.setAttribute( - "data-cslp", - "all_fields.bltapikey.en-us.link.href" - ); - - document.body.appendChild(linkField); - visualBuilder = new VisualBuilder(); - await triggerAndWaitForClickAction( - visualBuilderPostMessage, - linkField - ); - }); - - afterAll(() => { - visualBuilder.destroy(); - }); - - test("should have outline", () => { - expect(linkField.classList.contains("cslp-edit-mode")); - }); - - test("should have an overlay", () => { - const overlay = document.querySelector(".visual-builder__overlay"); - expect(overlay!.classList.contains("visible")); - }); - - test("should have a field path dropdown", async () => { - await waitFor(async () => { - const toolbar = await screen.findByTestId( - "mock-field-label-wrapper" - ); - expect(toolbar).toBeInTheDocument(); - }); - }); - - test("should contain a data-cslp-field-type attribute", async () => { - await waitFor(() => { - expect(linkField).toHaveAttribute( - VISUAL_BUILDER_FIELD_TYPE_ATTRIBUTE_KEY - ); - }); - }); - - test("should not contain a contenteditable attribute", async () => { - await waitFor(() => { - expect(linkField).not.toHaveAttribute("contenteditable"); - }); - }); - - test.skip("should send a focus field message to parent", async () => { - await waitFor(() => { - expect(visualBuilderPostMessage?.send).toBeCalledWith( - VisualBuilderPostMessageEvents.FOCUS_FIELD, - { - DOMEditStack: getDOMEditStack(linkField), - } - ); - }); - }); - }); - - // BUG ?: test failing : should have 2 add instance buttons - describe("link field (multiple)", () => { - let container: HTMLDivElement; - let firstLinkField: HTMLAnchorElement; - let secondLinkField: HTMLAnchorElement; - let visualBuilder: VisualBuilder; - - beforeAll(async () => { - container = document.createElement("div"); - container.setAttribute( - "data-cslp", - "all_fields.bltapikey.en-us.link_multiple_" - ); - - firstLinkField = document.createElement("a"); - firstLinkField.setAttribute( - "data-cslp", - "all_fields.blt366df6233d9915f5.en-us.link_multiple_.0.href" - ); - - secondLinkField = document.createElement("a"); - secondLinkField.setAttribute( - "data-cslp", - "all_fields.bltapikey.en-us.link_multiple_.1.href" - ); - - container.appendChild(firstLinkField); - container.appendChild(secondLinkField); - document.body.appendChild(container); - - visualBuilder = new VisualBuilder(); - await triggerAndWaitForClickAction( - visualBuilderPostMessage, - container - ); - }); - - afterAll(() => { - visualBuilder.destroy(); - }); - - test("should have outline", () => { - expect(container.classList.contains("cslp-edit-mode")); - }); - - test("should have an overlay", () => { - const overlay = document.querySelector(".visual-builder__overlay"); - expect(overlay!.classList.contains("visible")); - }); - - test("should contain a data-cslp-field-type attribute", async () => { - await waitFor(() => { - expect(container).toHaveAttribute( - VISUAL_BUILDER_FIELD_TYPE_ATTRIBUTE_KEY - ); - }); - }); - - test("both container and its children should not contain a contenteditable attribute", async () => { - fireEvent.click(container); - await waitFor(() => { - expect(container).not.toHaveAttribute("contenteditable"); - }); - - fireEvent.click(container.children[0]); - await waitFor(() => { - expect(container.children[0]).not.toHaveAttribute( - "contenteditable" - ); - }); - - fireEvent.click(container.children[1]); - await waitFor(() => { - expect(container.children[1]).not.toHaveAttribute( - "contenteditable" - ); - }); - }); - - test.skip("should send a focus field message to parent", async () => { - await waitFor(() => { - expect(visualBuilderPostMessage?.send).toBeCalledWith( - VisualBuilderPostMessageEvents.FOCUS_FIELD, - { - DOMEditStack: getDOMEditStack(container), - } - ); - }); - }); - }); -}); diff --git a/src/visualBuilder/__test__/click/fields/markdown.test.tsx b/src/visualBuilder/__test__/click/fields/markdown.test.tsx deleted file mode 100644 index a69fd525..00000000 --- a/src/visualBuilder/__test__/click/fields/markdown.test.tsx +++ /dev/null @@ -1,256 +0,0 @@ -import { fireEvent, screen, waitFor } from "@testing-library/preact"; -import "@testing-library/jest-dom"; -import { getFieldSchemaMap } from "../../../../__test__/data/fieldSchemaMap"; -import Config from "../../../../configManager/configManager"; -import { VISUAL_BUILDER_FIELD_TYPE_ATTRIBUTE_KEY } from "../../../utils/constants"; -import { FieldSchemaMap } from "../../../utils/fieldSchemaMap"; -import { getDOMEditStack } from "../../../utils/getCsDataOfElement"; -import visualBuilderPostMessage from "../../../utils/visualBuilderPostMessage"; -import { vi } from "vitest"; -import { VisualBuilderPostMessageEvents } from "../../../utils/types/postMessage.types"; -import { VisualBuilder } from "../../../index"; -import { triggerAndWaitForClickAction } from "../../../../__test__/utils"; - -vi.mock("../../../components/FieldToolbar", () => { - return { - default: () => { - return
Field Toolbar
; - }, - }; -}); - -vi.mock("../../../components/fieldLabelWrapper", () => { - return { - default: () => { - return ( -
Field Label
- ); - }, - }; -}); - -vi.mock("../../../utils/visualBuilderPostMessage", async () => { - const { getAllContentTypes } = await vi.importActual< - typeof import("../../../../__test__/data/contentType") - >("../../../../__test__/data/contentType"); - const contentTypes = getAllContentTypes(); - return { - __esModule: true, - default: { - send: vi.fn().mockImplementation((eventName: string) => { - if (eventName === "init") - return Promise.resolve({ - contentTypes, - }); - return Promise.resolve(); - }), - on: vi.fn(), - }, - }; -}); - -vi.mock("../../../../utils/index.ts", async () => { - const actual = await vi.importActual("../../../../utils"); - return { - __esModule: true, - ...actual, - isOpenInBuilder: vi.fn().mockReturnValue(true), - }; -}); - -describe("When an element is clicked in visual builder mode", () => { - beforeAll(() => { - FieldSchemaMap.setFieldSchema( - "all_fields", - getFieldSchemaMap().all_fields - ); - vi.spyOn( - document.documentElement, - "clientWidth", - "get" - ).mockReturnValue(100); - vi.spyOn( - document.documentElement, - "clientHeight", - "get" - ).mockReturnValue(100); - vi.spyOn(document.body, "scrollHeight", "get").mockReturnValue(100); - - Config.reset(); - Config.set("mode", 2); - }); - - afterAll(() => { - vi.clearAllMocks(); - document.body.innerHTML = ""; - - Config.reset(); - }); - - describe("markdown field", () => { - let markdownField: HTMLParagraphElement; - let visualBuilder: VisualBuilder; - - beforeAll(async () => { - markdownField = document.createElement("p"); - markdownField.setAttribute( - "data-cslp", - "all_fields.bltapikey.en-us.markdown" - ); - - document.body.appendChild(markdownField); - - visualBuilder = new VisualBuilder(); - await triggerAndWaitForClickAction( - visualBuilderPostMessage, - markdownField - ); - }); - - afterAll(() => { - visualBuilder.destroy(); - }); - - test("should have outline", () => { - expect(markdownField.classList.contains("cslp-edit-mode")); - }); - - test("should have an overlay", () => { - const overlay = document.querySelector(".visual-builder__overlay"); - expect(overlay!.classList.contains("visible")); - }); - - test("should have a field path dropdown", async () => { - await waitFor(async () => { - const toolbar = await screen.findByTestId( - "mock-field-label-wrapper" - ); - expect(toolbar).toBeInTheDocument(); - }); - }); - - test("should contain a data-cslp-field-type attribute", async () => { - await waitFor(() => { - expect(markdownField).toHaveAttribute( - VISUAL_BUILDER_FIELD_TYPE_ATTRIBUTE_KEY - ); - }); - }); - - test("should not contain a contenteditable attribute", async () => { - await waitFor(() => { - expect(markdownField).not.toHaveAttribute("contenteditable"); - }); - }); - - test.skip("should send a focus field message to parent", async () => { - await waitFor(() => { - expect(visualBuilderPostMessage?.send).toBeCalledWith( - VisualBuilderPostMessageEvents.FOCUS_FIELD, - { - DOMEditStack: getDOMEditStack(markdownField), - } - ); - }); - }); - }); - - describe("markdown field (multiple)", () => { - let container: HTMLDivElement; - let firstMarkdownField: HTMLParagraphElement; - let secondMarkdownField: HTMLParagraphElement; - let visualBuilder: VisualBuilder; - - beforeAll(async () => { - container = document.createElement("div"); - container.setAttribute( - "data-cslp", - "all_fields.bltapikey.en-us.markdown_multiple_" - ); - - firstMarkdownField = document.createElement("p"); - firstMarkdownField.setAttribute( - "data-cslp", - "all_fields.bltapikey.en-us.markdown_multiple_.0" - ); - - secondMarkdownField = document.createElement("p"); - secondMarkdownField.setAttribute( - "data-cslp", - "all_fields.bltapikey.en-us.markdown_multiple_.1" - ); - - container.appendChild(firstMarkdownField); - container.appendChild(secondMarkdownField); - document.body.appendChild(container); - - visualBuilder = new VisualBuilder(); - await triggerAndWaitForClickAction( - visualBuilderPostMessage, - container - ); - }); - - afterAll(() => { - visualBuilder.destroy(); - }); - - test("should have outline", () => { - expect(container.classList.contains("cslp-edit-mode")); - }); - - test("should have an overlay", () => { - const overlay = document.querySelector(".visual-builder__overlay"); - expect(overlay!.classList.contains("visible")); - }); - - test("should have a field path dropdown", async () => { - await waitFor(async () => { - const toolbar = await screen.findByTestId( - "mock-field-label-wrapper" - ); - expect(toolbar).toBeInTheDocument(); - }); - }); - - test("should contain a data-cslp-field-type attribute", async () => { - await waitFor(() => { - expect(container).toHaveAttribute( - VISUAL_BUILDER_FIELD_TYPE_ATTRIBUTE_KEY - ); - }); - }); - - test("both container and its children should not contain a contenteditable attribute", async () => { - fireEvent.click(container); - await waitFor(() => { - expect(container).not.toHaveAttribute("contenteditable"); - }); - - fireEvent.click(container.children[0]); - await waitFor(() => { - expect(container.children[0]).not.toHaveAttribute( - "contenteditable" - ); - }); - - fireEvent.click(container.children[1]); - await waitFor(() => { - expect(container.children[1]).not.toHaveAttribute( - "contenteditable" - ); - }); - }); - - test.skip("should send a focus field message to parent", async () => { - await waitFor(() => { - expect(visualBuilderPostMessage?.send).toBeCalledWith( - VisualBuilderPostMessageEvents.FOCUS_FIELD, - { - DOMEditStack: getDOMEditStack(container), - } - ); - }); - }); - }); -}); diff --git a/src/visualBuilder/__test__/click/fields/multi-line.test.tsx b/src/visualBuilder/__test__/click/fields/multi-line.test.tsx index 7991bf8a..325a4ad6 100644 --- a/src/visualBuilder/__test__/click/fields/multi-line.test.tsx +++ b/src/visualBuilder/__test__/click/fields/multi-line.test.tsx @@ -144,47 +144,11 @@ describe("When an element is clicked in visual builder mode", () => { visualBuilder.destroy(); }); - test("should have outline", () => { - expect(multiLineField.classList.contains("cslp-edit-mode")); - }); - - test("should have an overlay", () => { - const overlay = document.querySelector(".visual-builder__overlay"); - expect(overlay!.classList.contains("visible")); - }); - - test("should have a field path dropdown", async () => { - await waitFor(async () => { - const toolbar = await screen.findByTestId( - "mock-field-label-wrapper" - ); - expect(toolbar).toBeInTheDocument(); - }); - }); - - test("should contain a data-cslp-field-type attribute", async () => { - await waitFor(() => { - expect(multiLineField).toHaveAttribute( - VISUAL_BUILDER_FIELD_TYPE_ATTRIBUTE_KEY - ); - }); - }); - - test("should contain a contenteditable attribute", async () => { - await waitFor(() => { - expect(multiLineField).toHaveAttribute("contenteditable"); - }); - }); - - test.skip("should send a focus field message to parent", async () => { - await waitFor(() => { - expect(visualBuilderPostMessage?.send).toBeCalledWith( - VisualBuilderPostMessageEvents.FOCUS_FIELD, - { - DOMEditStack: getDOMEditStack(multiLineField), - } - ); - }); + // Common tests (field type, overlay, dropdown, focus message) are covered in all-click.test.tsx + // Only testing unique behavior: contenteditable attribute for editable fields + test("should contain a contenteditable attribute", () => { + // Attribute is set synchronously + expect(multiLineField).toHaveAttribute("contenteditable"); }); }); @@ -254,12 +218,14 @@ describe("When an element is clicked in visual builder mode", () => { container.appendChild(secondMultiLineField); document.body.appendChild(container); - VisualBuilder.VisualBuilderGlobalState.value = { - previousSelectedEditableDOM: null, - previousHoveredTargetDOM: null, - previousEmptyBlockParents: [], - audienceMode: false, - }; + // Reset global state for test + VisualBuilder.VisualBuilderGlobalState.value.previousSelectedEditableDOM = + null; + VisualBuilder.VisualBuilderGlobalState.value.previousHoveredTargetDOM = + null; + VisualBuilder.VisualBuilderGlobalState.value.previousEmptyBlockParents = + []; + VisualBuilder.VisualBuilderGlobalState.value.audienceMode = false; visualBuilder = new VisualBuilder(); await triggerAndWaitForClickAction( visualBuilderPostMessage, @@ -270,32 +236,8 @@ describe("When an element is clicked in visual builder mode", () => { afterAll(() => { visualBuilder.destroy(); }); - test("should have outline", () => { - expect(container.classList.contains("cslp-edit-mode")); - }); - - test("should have an overlay", () => { - const overlay = document.querySelector(".visual-builder__overlay"); - expect(overlay!.classList.contains("visible")); - }); - - test("should have a field path dropdown", async () => { - await waitFor(async () => { - const toolbar = await screen.findByTestId( - "mock-field-label-wrapper" - ); - expect(toolbar).toBeInTheDocument(); - }); - }); - - test("should contain a data-cslp-field-type attribute", async () => { - await waitFor(() => { - expect(container).toHaveAttribute( - VISUAL_BUILDER_FIELD_TYPE_ATTRIBUTE_KEY - ); - }); - }); - + // Common tests (field type, overlay, dropdown, focus message) are covered in all-click.test.tsx + // Only testing unique behavior: contenteditable on children for editable multiple fields test("container should not contain a contenteditable attribute but the children can", async () => { fireEvent.click(container); await waitFor(() => { @@ -316,16 +258,5 @@ describe("When an element is clicked in visual builder mode", () => { ); }); }); - - test.skip("should send a focus field message to parent", async () => { - await waitFor(() => { - expect(visualBuilderPostMessage?.send).toBeCalledWith( - VisualBuilderPostMessageEvents.FOCUS_FIELD, - { - DOMEditStack: getDOMEditStack(container), - } - ); - }); - }); }); }); diff --git a/src/visualBuilder/__test__/click/fields/number.test.tsx b/src/visualBuilder/__test__/click/fields/number.test.tsx index 5e605a73..687b1670 100644 --- a/src/visualBuilder/__test__/click/fields/number.test.tsx +++ b/src/visualBuilder/__test__/click/fields/number.test.tsx @@ -151,41 +151,11 @@ describe("When an element is clicked in visual builder mode", () => { visualBuilder.destroy(); }); - test("should have outline", () => { - expect(numberField.classList.contains("cslp-edit-mode")); - }); - - test("should have an overlay", () => { - const overlay = document.querySelector(".visual-builder__overlay"); - expect(overlay!.classList.contains("visible")); - }); - - test("should have a field path dropdown", async () => { - await waitFor(async () => { - const toolbar = await screen.findByTestId( - "mock-field-label-wrapper" - ); - expect(toolbar).toBeInTheDocument(); - }); - }); - - test("should contain a data-cslp-field-type attribute", async () => { - await waitFor(() => { - expect(numberField).toHaveAttribute( - VISUAL_BUILDER_FIELD_TYPE_ATTRIBUTE_KEY - ); - }); - }); - - test.skip("should send a focus field message to parent", async () => { - await waitFor(() => { - expect(visualBuilderPostMessage?.send).toBeCalledWith( - VisualBuilderPostMessageEvents.FOCUS_FIELD, - { - DOMEditStack: getDOMEditStack(numberField), - } - ); - }); + // Common tests (field type, overlay, dropdown, focus message) are covered in all-click.test.tsx + // Only testing unique behavior: number fields have contenteditable (they're in ALLOWED_INLINE_EDITABLE_FIELD) + test("should contain a contenteditable attribute", () => { + // Number fields are editable inline, so they should have contenteditable + expect(numberField).toHaveAttribute("contenteditable"); }); }); @@ -222,8 +192,10 @@ describe("When an element is clicked in visual builder mode", () => { }, }, }); - } - else if (eventName === VisualBuilderPostMessageEvents.GET_RESOLVED_VARIANT_PERMISSIONS) { + } else if ( + eventName === + VisualBuilderPostMessageEvents.GET_RESOLVED_VARIANT_PERMISSIONS + ) { return Promise.resolve({ update: true, }); @@ -267,62 +239,22 @@ describe("When an element is clicked in visual builder mode", () => { visualBuilder.destroy(); }); - test("should have outline", () => { - expect(container.classList.contains("cslp-edit-mode")); - }); - - test("should have an overlay", () => { - const overlay = document.querySelector(".visual-builder__overlay"); - expect(overlay!.classList.contains("visible")); - }); - - test("should have a field path dropdown", async () => { - await waitFor(async () => { - const toolbar = await screen.findByTestId( - "mock-field-label-wrapper" - ); - expect(toolbar).toBeInTheDocument(); - }); - }); - - test("should contain a data-cslp-field-type attribute", async () => { - await waitFor(() => { - expect(container).toHaveAttribute( - VISUAL_BUILDER_FIELD_TYPE_ATTRIBUTE_KEY - ); - }); - }); - - test("container should not contain a contenteditable attribute but the children can", async () => { + // Common tests (field type, overlay, dropdown, focus message) are covered in all-click.test.tsx + // Only testing unique behavior: number fields don't have contenteditable even on children + test("neither container nor children should contain a contenteditable attribute", () => { + // Number fields don't have contenteditable (they're input type=number) fireEvent.click(container); - await waitFor(() => { - expect(container).not.toHaveAttribute("contenteditable"); - }); + expect(container).not.toHaveAttribute("contenteditable"); fireEvent.click(container.children[0]); - await waitFor(() => { - expect(container.children[0]).toHaveAttribute( - "contenteditable" - ); - }); + expect(container.children[0]).not.toHaveAttribute( + "contenteditable" + ); fireEvent.click(container.children[1]); - await waitFor(() => { - expect(container.children[1]).toHaveAttribute( - "contenteditable" - ); - }); - }); - - test.skip("should send a focus field message to parent", async () => { - await waitFor(() => { - expect(visualBuilderPostMessage?.send).toBeCalledWith( - VisualBuilderPostMessageEvents.FOCUS_FIELD, - { - DOMEditStack: getDOMEditStack(container), - } - ); - }); + expect(container.children[1]).not.toHaveAttribute( + "contenteditable" + ); }); }); }); diff --git a/src/visualBuilder/__test__/click/fields/reference.test.tsx b/src/visualBuilder/__test__/click/fields/reference.test.tsx index 987e204b..18482195 100644 --- a/src/visualBuilder/__test__/click/fields/reference.test.tsx +++ b/src/visualBuilder/__test__/click/fields/reference.test.tsx @@ -115,6 +115,8 @@ describe("When an element is clicked in visual builder mode", () => { visualBuilder.destroy(); }); + // Common tests (field type, overlay, dropdown, focus message, no contenteditable) are covered in all-click.test.tsx + // Only testing unique behavior: reference fields have a specific outline style test("should have outline", async () => { const hoverOutline = document.querySelector( "[data-testid='visual-builder__overlay--outline']" @@ -128,43 +130,6 @@ describe("When an element is clicked in visual builder mode", () => { "top: 10px; height: 5px; width: 10px; left: 10px; outline-color: rgb(113, 92, 221);" ); }); - - test("should have an overlay", () => { - const overlay = document.querySelector(".visual-builder__overlay"); - expect(overlay!.classList.contains("visible")); - }); - - test("should have a field path dropdown", () => { - const toolbar = document.querySelector( - "[data-testid='mock-field-label-wrapper']" - ); - expect(toolbar).toBeInTheDocument(); - }); - - test("should contain a data-cslp-field-type attribute", async () => { - await waitFor(() => { - expect(referenceField).toHaveAttribute( - VISUAL_BUILDER_FIELD_TYPE_ATTRIBUTE_KEY - ); - }); - }); - - test("should not contain a contenteditable attribute", async () => { - await waitFor(() => { - expect(referenceField).not.toHaveAttribute("contenteditable"); - }); - }); - - test.skip("should send a focus field message to parent", async () => { - await waitFor(() => { - expect(visualBuilderPostMessage?.send).toBeCalledWith( - VisualBuilderPostMessageEvents.FOCUS_FIELD, - { - DOMEditStack: getDOMEditStack(referenceField), - } - ); - }); - }); }); describe("reference field (multiple)", () => { @@ -208,6 +173,8 @@ describe("When an element is clicked in visual builder mode", () => { visualBuilder.destroy(); }); + // Common tests (field type, overlay, dropdown, focus message, no contenteditable) are covered in all-click.test.tsx + // Only testing unique behavior: reference fields have a specific outline style test("should have outline", async () => { const hoverOutline = document.querySelector( "[data-testid='visual-builder__overlay--outline']" @@ -220,57 +187,5 @@ describe("When an element is clicked in visual builder mode", () => { "top: 10px; height: 5px; width: 10px; left: 10px; outline-color: rgb(113, 92, 221);" ); }); - - test("should have an overlay", () => { - const overlay = document.querySelector(".visual-builder__overlay"); - expect(overlay!.classList.contains("visible")); - }); - - test("should have a field path dropdown", () => { - const toolbar = document.querySelector( - "[data-testid='mock-field-label-wrapper']" - ); - expect(toolbar).toBeInTheDocument(); - }); - - test("should contain a data-cslp-field-type attribute", async () => { - await waitFor(() => { - expect(container).toHaveAttribute( - VISUAL_BUILDER_FIELD_TYPE_ATTRIBUTE_KEY - ); - }); - }); - - test("both container and its children should not contain a contenteditable attribute", async () => { - fireEvent.click(container); - await waitFor(() => { - expect(container).not.toHaveAttribute("contenteditable"); - }); - - fireEvent.click(container.children[0]); - await waitFor(() => { - expect(container.children[0]).not.toHaveAttribute( - "contenteditable" - ); - }); - - fireEvent.click(container.children[1]); - await waitFor(() => { - expect(container.children[1]).not.toHaveAttribute( - "contenteditable" - ); - }); - }); - - test.skip("should send a focus field message to parent", async () => { - await waitFor(() => { - expect(visualBuilderPostMessage?.send).toBeCalledWith( - VisualBuilderPostMessageEvents.FOCUS_FIELD, - { - DOMEditStack: getDOMEditStack(container), - } - ); - }); - }); }); }); diff --git a/src/visualBuilder/__test__/click/fields/select.test.tsx b/src/visualBuilder/__test__/click/fields/select.test.tsx deleted file mode 100644 index a370e49d..00000000 --- a/src/visualBuilder/__test__/click/fields/select.test.tsx +++ /dev/null @@ -1,260 +0,0 @@ -import { fireEvent, waitFor, screen } from "@testing-library/preact"; -import "@testing-library/jest-dom"; -import { getFieldSchemaMap } from "../../../../__test__/data/fieldSchemaMap"; -import Config from "../../../../configManager/configManager"; -import { VISUAL_BUILDER_FIELD_TYPE_ATTRIBUTE_KEY } from "../../../utils/constants"; -import { FieldSchemaMap } from "../../../utils/fieldSchemaMap"; -import { getDOMEditStack } from "../../../utils/getCsDataOfElement"; -import visualBuilderPostMessage from "../../../utils/visualBuilderPostMessage"; -import { vi } from "vitest"; -import { VisualBuilderPostMessageEvents } from "../../../utils/types/postMessage.types"; -import { VisualBuilder } from "../../../index"; -import { triggerAndWaitForClickAction } from "../../../../__test__/utils"; - -const VALUES = { - singleLine: "Single line", - number: "10.5", -}; - -vi.mock("../../../components/FieldToolbar", () => { - return { - default: () => { - return
Field Toolbar
; - }, - }; -}); - -vi.mock("../../../components/fieldLabelWrapper", () => { - return { - default: () => { - return ( -
Field Label
- ); - }, - }; -}); - -vi.mock("../../../utils/visualBuilderPostMessage", async () => { - const { getAllContentTypes } = await vi.importActual< - typeof import("../../../../__test__/data/contentType") - >("../../../../__test__/data/contentType"); - const contentTypes = getAllContentTypes(); - return { - __esModule: true, - default: { - send: vi.fn().mockImplementation((eventName: string) => { - if (eventName === "init") - return Promise.resolve({ - contentTypes, - }); - return Promise.resolve(); - }), - on: vi.fn(), - }, - }; -}); - -vi.mock("../../../../utils/index.ts", async () => { - const actual = await vi.importActual("../../../../utils"); - return { - __esModule: true, - ...actual, - isOpenInBuilder: vi.fn().mockReturnValue(true), - }; -}); - -describe("When an element is clicked in visual builder mode", () => { - beforeAll(() => { - FieldSchemaMap.setFieldSchema( - "all_fields", - getFieldSchemaMap().all_fields - ); - vi.spyOn( - document.documentElement, - "clientWidth", - "get" - ).mockReturnValue(100); - vi.spyOn( - document.documentElement, - "clientHeight", - "get" - ).mockReturnValue(100); - vi.spyOn(document.body, "scrollHeight", "get").mockReturnValue(100); - - Config.reset(); - Config.set("mode", 2); - }); - - afterAll(() => { - vi.clearAllMocks(); - document.body.innerHTML = ""; - - Config.reset(); - }); - - describe("select field", () => { - let selectField: HTMLParagraphElement; - let visualBuilder: VisualBuilder; - - beforeAll(async () => { - selectField = document.createElement("p"); - selectField.setAttribute( - "data-cslp", - "all_fields.bltapikey.en-us.select" - ); - document.body.appendChild(selectField); - - visualBuilder = new VisualBuilder(); - await triggerAndWaitForClickAction( - visualBuilderPostMessage, - selectField - ); - }); - - afterAll(() => { - visualBuilder.destroy(); - }); - - test("should have outline", () => { - expect(selectField.classList.contains("cslp-edit-mode")); - }); - - test("should have an overlay", () => { - const overlay = document.querySelector(".visual-builder__overlay"); - expect(overlay!.classList.contains("visible")); - }); - - test("should have a field path dropdown", async () => { - await waitFor(async () => { - const toolbar = await screen.findByTestId( - "mock-field-label-wrapper" - ); - expect(toolbar).toBeInTheDocument(); - }); - }); - - test("should contain a data-cslp-field-type attribute", async () => { - await waitFor(() => { - expect(selectField).toHaveAttribute( - VISUAL_BUILDER_FIELD_TYPE_ATTRIBUTE_KEY - ); - }); - }); - - test("should not contain a contenteditable attribute", async () => { - await waitFor(() => { - expect(selectField).not.toHaveAttribute("contenteditable"); - }); - }); - - test.skip("should send a focus field message to parent", async () => { - await waitFor(() => { - expect(visualBuilderPostMessage?.send).toBeCalledWith( - VisualBuilderPostMessageEvents.FOCUS_FIELD, - { - DOMEditStack: getDOMEditStack(selectField), - } - ); - }); - }); - }); - - describe("select field (multiple)", () => { - let container: HTMLDivElement; - let firstSelectField: HTMLParagraphElement; - let secondSelectField: HTMLParagraphElement; - let visualBuilder: VisualBuilder; - - beforeAll(async () => { - container = document.createElement("div"); - container.setAttribute( - "data-cslp", - "all_fields.bltapikey.en-us.select_multiple_" - ); - - firstSelectField = document.createElement("p"); - firstSelectField.setAttribute( - "data-cslp", - "all_fields.bltapikey.en-us.select_multiple_.0" - ); - - secondSelectField = document.createElement("p"); - secondSelectField.setAttribute( - "data-cslp", - "all_fields.bltapikey.en-us.select_multiple_.1" - ); - - container.appendChild(firstSelectField); - container.appendChild(secondSelectField); - document.body.appendChild(container); - - visualBuilder = new VisualBuilder(); - await triggerAndWaitForClickAction( - visualBuilderPostMessage, - container - ); - }); - - afterAll(() => { - visualBuilder.destroy(); - }); - - test("should have outline", () => { - expect(container.classList.contains("cslp-edit-mode")); - }); - - test("should have an overlay", () => { - const overlay = document.querySelector(".visual-builder__overlay"); - expect(overlay!.classList.contains("visible")); - }); - - test("should have a field path dropdown", async () => { - await waitFor(async () => { - const toolbar = await screen.findByTestId( - "mock-field-label-wrapper" - ); - expect(toolbar).toBeInTheDocument(); - }); - }); - - test("should contain a data-cslp-field-type attribute", async () => { - await waitFor(() => { - expect(container).toHaveAttribute( - VISUAL_BUILDER_FIELD_TYPE_ATTRIBUTE_KEY - ); - }); - }); - - test("both container and its children should not contain a contenteditable attribute", async () => { - fireEvent.click(container); - await waitFor(() => { - expect(container).not.toHaveAttribute("contenteditable"); - }); - - fireEvent.click(container.children[0]); - await waitFor(() => { - expect(container.children[0]).not.toHaveAttribute( - "contenteditable" - ); - }); - - fireEvent.click(container.children[1]); - await waitFor(() => { - expect(container.children[1]).not.toHaveAttribute( - "contenteditable" - ); - }); - }); - - test.skip("should send a focus field message to parent", async () => { - await waitFor(() => { - expect(visualBuilderPostMessage?.send).toBeCalledWith( - VisualBuilderPostMessageEvents.FOCUS_FIELD, - { - DOMEditStack: getDOMEditStack(container), - } - ); - }); - }); - }); -}); diff --git a/src/visualBuilder/__test__/click/fields/single-line.test.tsx b/src/visualBuilder/__test__/click/fields/single-line.test.tsx index 614608d9..f4bf186c 100644 --- a/src/visualBuilder/__test__/click/fields/single-line.test.tsx +++ b/src/visualBuilder/__test__/click/fields/single-line.test.tsx @@ -162,47 +162,11 @@ describe("When an element is clicked in visual builder mode", () => { visualBuilder.destroy(); }); - test("should have outline", () => { - expect(singleLineField.classList.contains("cslp-edit-mode")); - }); - - test("should have an overlay", () => { - const overlay = document.querySelector(".visual-builder__overlay"); - expect(overlay!.classList.contains("visible")); - }); - - test("should have a field path dropdown", async () => { - await waitFor(async () => { - const fieldLabel = screen.getByTestId( - "mock-field-label-wrapper" - ); - expect(fieldLabel).toBeInTheDocument(); - }); - }); - - test("should contain a data-cslp-field-type attribute", async () => { - await waitFor(() => - expect(singleLineField).toHaveAttribute( - VISUAL_BUILDER_FIELD_TYPE_ATTRIBUTE_KEY - ) - ); - }); - - test("should contain a contenteditable attribute", async () => { - await waitFor(() => { - expect(singleLineField).toHaveAttribute("contenteditable"); - }); - }); - - test.skip("should send a focus field message to parent", async () => { - await waitFor(() => { - expect(visualBuilderPostMessage?.send).toBeCalledWith( - VisualBuilderPostMessageEvents.FOCUS_FIELD, - { - DOMEditStack: getDOMEditStack(singleLineField), - } - ); - }); + // Common tests (field type, overlay, dropdown, focus message) are covered in all-click.test.tsx + // Only testing unique behavior: contenteditable attribute for editable fields + test("should contain a contenteditable attribute", () => { + // Attribute is set synchronously during click handler + expect(singleLineField).toHaveAttribute("contenteditable"); }); }); @@ -290,37 +254,13 @@ describe("When an element is clicked in visual builder mode", () => { visualBuilder.destroy(); }); - test("should have outline", () => { - expect(container.classList.contains("cslp-edit-mode")); - }); - - test("should have an overlay", () => { - const overlay = document.querySelector(".visual-builder__overlay"); - expect(overlay!.classList.contains("visible")); - }); - - test("should have a field path dropdown", async () => { - await waitFor(async () => { - const toolbar = await screen.findByTestId( - "mock-field-label-wrapper" - ); - expect(toolbar).toBeInTheDocument(); - }); - }); - - test("should contain a data-cslp-field-type attribute", async () => { - await waitFor(() => { - expect(container).toHaveAttribute( - VISUAL_BUILDER_FIELD_TYPE_ATTRIBUTE_KEY - ); - }); - }); - + // Common tests (field type, overlay, dropdown, focus message) are covered in all-click.test.tsx + // Only testing unique behavior: contenteditable on children for editable multiple fields test("container should not contain a contenteditable attribute but the children can", async () => { - await waitFor(() => { - expect(container).not.toHaveAttribute("contenteditable"); - }); + // Container contenteditable check is synchronous + expect(container).not.toHaveAttribute("contenteditable"); + // Child contenteditable is set asynchronously after click fireEvent.click(container.children[0]); await waitFor(() => { expect(container.children[0]).toHaveAttribute( @@ -335,16 +275,5 @@ describe("When an element is clicked in visual builder mode", () => { ); }); }); - - test.skip("should send a focus field message to parent", async () => { - await waitFor(() => { - expect(visualBuilderPostMessage?.send).toBeCalledWith( - VisualBuilderPostMessageEvents.FOCUS_FIELD, - { - DOMEditStack: getDOMEditStack(container), - } - ); - }); - }); }); }); diff --git a/src/visualBuilder/__test__/hover/fields/all-hover.test.ts b/src/visualBuilder/__test__/hover/fields/all-hover.test.ts new file mode 100644 index 00000000..e35d30b1 --- /dev/null +++ b/src/visualBuilder/__test__/hover/fields/all-hover.test.ts @@ -0,0 +1,310 @@ +/** + * Consolidated hover tests for essential field behavior patterns + * + * Since E2E tests cover field-specific behavior (different icons), this file tests only the core patterns: + * 1. Single field: shows outline and custom cursor with icon + * 2. Multiple field container: shows outline and cursor on container + * 3. Multiple field instance: shows outline and cursor on individual instances + * + * All field types follow the same hover behavior - only the icon differs (tested in E2E). + * + * Removed redundant field-specific tests (E2E covers these): + * - boolean.test.ts, date.test.ts, number.test.ts, markdown.test.ts + * - html-rte.test.ts, json-rte.test.ts, link.test.ts, reference.test.ts, select.test.ts + * + * Kept separate files for unique test cases: + * - file.test.ts (URL-specific test for file.url fields) + * - group.test.ts (nested field test) + * - single-line.test.ts (title field test with specific style assertions) + */ + +import { getFieldSchemaMap } from "../../../../__test__/data/fieldSchemaMap"; +import { + waitForHoverOutline, + waitForCursorIcon, +} from "../../../../__test__/utils"; +import Config from "../../../../configManager/configManager"; +import { VisualBuilder } from "../../../index"; +import { FieldSchemaMap } from "../../../utils/fieldSchemaMap"; +import { mockDomRect } from "./mockDomRect"; +import { waitFor } from "@testing-library/preact"; + +vi.mock("../../../utils/visualBuilderPostMessage", async () => { + const { getAllContentTypes } = await vi.importActual< + typeof import("../../../../__test__/data/contentType") + >("../../../../__test__/data/contentType"); + const contentTypes = getAllContentTypes(); + return { + __esModule: true, + default: { + send: vi.fn().mockImplementation((eventName: string) => { + if (eventName === "init") + return Promise.resolve({ + contentTypes, + }); + // Resolve all other calls immediately to avoid async delays + return Promise.resolve({}); + }), + }, + }; +}); + +// Mock fetchEntryPermissionsAndStageDetails to resolve immediately - speeds up hover tests +vi.mock("../../../utils/fetchEntryPermissionsAndStageDetails", () => { + return { + fetchEntryPermissionsAndStageDetails: vi.fn(() => + Promise.resolve({ + acl: { + create: true, + read: true, + update: true, + delete: true, + publish: true, + }, + workflowStage: { + stage: undefined, + permissions: { + entry: { + update: true, + }, + }, + }, + resolvedVariantPermissions: { + update: true, + }, + }) + ), + }; +}); + +vi.mock("../../../../utils/index.ts", async () => { + const actual = await vi.importActual("../../../../utils"); + return { + __esModule: true, + ...actual, + isOpenInBuilder: vi.fn().mockReturnValue(true), + }; +}); + +// Test only representative field types - E2E tests cover all field types and their icons +// Single field (no multiple support) - boolean represents this pattern +const SINGLE_FIELD = { + name: "boolean", + cslp: "all_fields.bltapikey.en-us.boolean", + icon: "boolean", +} as const; + +// Multiple field - select represents this pattern +const MULTIPLE_FIELD = { + name: "select", + cslp: "all_fields.bltapikey.en-us.select", + icon: "select", + multipleCslp: "all_fields.bltapikey.en-us.select_multiple_", +} as const; + +describe("When an element is hovered in visual builder mode", () => { + let mousemoveEvent: Event; + const fieldSchemaMap = getFieldSchemaMap().all_fields; + + beforeAll(() => { + // Pre-set all field schemas in cache to avoid async fetches during hover + // This significantly speeds up tests, especially for html-rte, json-rte, link fields + FieldSchemaMap.setFieldSchema("all_fields", fieldSchemaMap); + + // Field schemas are already set above - no need for additional caching + // The FieldSchemaMap.setFieldSchema call above sets all fields at once + }); + + beforeEach(() => { + Config.reset(); + Config.set("mode", 2); + mousemoveEvent = new Event("mousemove", { + bubbles: true, + cancelable: true, + }); + }); + + afterEach(async () => { + document.getElementsByTagName("html")[0].innerHTML = ""; + }); + + afterAll(() => { + Config.reset(); + }); + + // Test single field pattern (no multiple support) + // This represents all single-only fields: boolean, date, markdown, etc. + describe(`${SINGLE_FIELD.name} field (represents single field pattern)`, () => { + let fieldElement: HTMLElement; + let visualBuilder: VisualBuilder; + + beforeEach(() => { + fieldElement = document.createElement("p"); + fieldElement.setAttribute("data-cslp", SINGLE_FIELD.cslp); + fieldElement.getBoundingClientRect = vi + .fn() + .mockReturnValue(mockDomRect.singleLeft()); + + document.body.appendChild(fieldElement); + visualBuilder = new VisualBuilder(); + }); + + afterEach(() => { + visualBuilder.destroy(); + }); + + test("should have outline and custom cursor", async () => { + fieldElement.dispatchEvent(mousemoveEvent); + await waitForHoverOutline(); + + const hoverOutline = document.querySelector( + "[data-testid='visual-builder__hover-outline']" + ); + expect(hoverOutline).toHaveAttribute("style"); + + // Wait for cursor icon to be set (not "loading") + await waitForCursorIcon(SINGLE_FIELD.icon, { timeout: 5000 }); + + const customCursor = document.querySelector( + `[data-testid="visual-builder__cursor"]` + ); + expect(customCursor).toHaveAttribute( + "data-icon", + SINGLE_FIELD.icon + ); + expect(customCursor?.classList.contains("visible")).toBeTruthy(); + }); + }); + + // Test multiple field pattern + // This represents all multiple field types: select, html-rte, json-rte, link, reference, etc. + describe(`${MULTIPLE_FIELD.name} field (represents multiple field pattern)`, () => { + let fieldElement: HTMLElement; + let visualBuilder: VisualBuilder; + + beforeEach(() => { + fieldElement = document.createElement("p"); + fieldElement.setAttribute("data-cslp", MULTIPLE_FIELD.cslp); + fieldElement.getBoundingClientRect = vi + .fn() + .mockReturnValue(mockDomRect.singleLeft()); + + document.body.appendChild(fieldElement); + visualBuilder = new VisualBuilder(); + }); + + afterEach(() => { + visualBuilder.destroy(); + }); + + test("should have outline and custom cursor", async () => { + fieldElement.dispatchEvent(mousemoveEvent); + await waitForHoverOutline(); + + const hoverOutline = document.querySelector( + "[data-testid='visual-builder__hover-outline']" + ); + expect(hoverOutline).toHaveAttribute("style"); + + // Wait for cursor icon to be set (not "loading") + await waitForCursorIcon(MULTIPLE_FIELD.icon, { timeout: 5000 }); + + const customCursor = document.querySelector( + `[data-testid="visual-builder__cursor"]` + ); + expect(customCursor).toHaveAttribute( + "data-icon", + MULTIPLE_FIELD.icon + ); + expect(customCursor?.classList.contains("visible")).toBeTruthy(); + }); + }); + + // Test multiple field container pattern + describe(`${MULTIPLE_FIELD.name} field (multiple) - represents multiple container pattern`, () => { + let container: HTMLDivElement; + let firstField: HTMLElement; + let secondField: HTMLElement; + let visualBuilder: VisualBuilder; + + beforeEach(() => { + container = document.createElement("div"); + container.setAttribute("data-cslp", MULTIPLE_FIELD.multipleCslp); + container.getBoundingClientRect = vi + .fn() + .mockReturnValue(mockDomRect.singleHorizontal()); + + firstField = document.createElement("p"); + firstField.setAttribute( + "data-cslp", + `${MULTIPLE_FIELD.multipleCslp}.0` + ); + firstField.getBoundingClientRect = vi + .fn() + .mockReturnValue(mockDomRect.singleLeft()); + + secondField = document.createElement("p"); + secondField.setAttribute( + "data-cslp", + `${MULTIPLE_FIELD.multipleCslp}.1` + ); + secondField.getBoundingClientRect = vi + .fn() + .mockReturnValue(mockDomRect.singleRight()); + + container.appendChild(firstField); + container.appendChild(secondField); + document.body.appendChild(container); + + visualBuilder = new VisualBuilder(); + }); + + afterEach(() => { + visualBuilder.destroy(); + }); + + test("should have outline and custom cursor on container", async () => { + container.dispatchEvent(mousemoveEvent); + await waitForHoverOutline(); + + const hoverOutline = document.querySelector( + "[data-testid='visual-builder__hover-outline']" + ); + expect(hoverOutline).toHaveAttribute("style"); + + // Wait for cursor icon to be set (not "loading") + await waitForCursorIcon(MULTIPLE_FIELD.icon, { timeout: 5000 }); + + const customCursor = document.querySelector( + `[data-testid="visual-builder__cursor"]` + ); + expect(customCursor).toHaveAttribute( + "data-icon", + MULTIPLE_FIELD.icon + ); + expect(customCursor?.classList.contains("visible")).toBeTruthy(); + }); + + test("should have outline and custom cursor on individual instances", async () => { + firstField.dispatchEvent(mousemoveEvent); + await waitForHoverOutline(); + + const hoverOutline = document.querySelector( + "[data-testid='visual-builder__hover-outline']" + ); + expect(hoverOutline).toHaveAttribute("style"); + + // Wait for cursor icon to be set (not "loading") + await waitForCursorIcon(MULTIPLE_FIELD.icon, { timeout: 5000 }); + + const customCursor = document.querySelector( + `[data-testid="visual-builder__cursor"]` + ); + expect(customCursor).toHaveAttribute( + "data-icon", + MULTIPLE_FIELD.icon + ); + expect(customCursor?.classList.contains("visible")).toBeTruthy(); + }); + }); +}); diff --git a/src/visualBuilder/__test__/hover/fields/boolean.test.ts b/src/visualBuilder/__test__/hover/fields/boolean.test.ts deleted file mode 100644 index 5fb7c0a8..00000000 --- a/src/visualBuilder/__test__/hover/fields/boolean.test.ts +++ /dev/null @@ -1,124 +0,0 @@ -import { screen } from "@testing-library/preact"; -import { getFieldSchemaMap } from "../../../../__test__/data/fieldSchemaMap"; -import { - waitForBuilderSDKToBeInitialized, - waitForHoverOutline, -} from "../../../../__test__/utils"; -import Config from "../../../../configManager/configManager"; -import { VisualBuilder } from "../../../index"; -import { FieldSchemaMap } from "../../../utils/fieldSchemaMap"; -import { mockDomRect } from "./mockDomRect"; -import visualBuilderPostMessage from "../../../utils/visualBuilderPostMessage"; -import { act } from "@testing-library/preact"; -import { isOpenInBuilder } from "../../../../utils"; - -vi.mock("../../../utils/visualBuilderPostMessage", async () => { - const { getAllContentTypes } = await vi.importActual< - typeof import("../../../../__test__/data/contentType") - >("../../../../__test__/data/contentType"); - const contentTypes = getAllContentTypes(); - return { - __esModule: true, - default: { - send: vi.fn().mockImplementation((eventName: string) => { - if (eventName === "init") - return Promise.resolve({ - contentTypes, - }); - return Promise.resolve(); - }), - on: vi.fn(), - }, - }; -}); - -vi.mock("../../../../utils/index.ts", async () => { - const actual = await vi.importActual("../../../../utils"); - return { - __esModule: true, - ...actual, - isOpenInBuilder: vi.fn().mockReturnValue(true), - }; -}); - -global.ResizeObserver = vi.fn().mockImplementation(() => ({ - observe: vi.fn(), - unobserve: vi.fn(), - disconnect: vi.fn(), -})); - -describe("When an element is hovered in visual builder mode", () => { - let mousemoveEvent: Event; - - beforeAll(() => { - FieldSchemaMap.setFieldSchema( - "all_fields", - getFieldSchemaMap().all_fields - ); - }); - - beforeEach(() => { - Config.reset(); - Config.set("mode", 2); - mousemoveEvent = new Event("mousemove", { - bubbles: true, - cancelable: true, - }); - }); - - afterEach(() => { - vi.clearAllMocks(); - document.getElementsByTagName("html")[0].innerHTML = ""; - }); - - afterAll(() => { - Config.reset(); - }); - describe("boolean field", () => { - let booleanField: HTMLParagraphElement; - let visualBuilder: VisualBuilder; - - beforeEach(async () => { - booleanField = document.createElement("p"); - booleanField.setAttribute( - "data-cslp", - "all_fields.bltapikey.en-us.boolean" - ); - - booleanField.getBoundingClientRect = vi - .fn() - .mockReturnValue(mockDomRect.singleLeft()); - - document.body.appendChild(booleanField); - - visualBuilder = new VisualBuilder(); - await waitForBuilderSDKToBeInitialized(visualBuilderPostMessage); - }); - - afterEach(() => { - visualBuilder.destroy(); - }); - - test("should have outline and custom cursor", async () => { - await act(async () => { - booleanField.dispatchEvent(mousemoveEvent); - }); - await waitForHoverOutline(); - expect(booleanField).toHaveAttribute( - "data-cslp", - "all_fields.bltapikey.en-us.boolean" - ); - expect(booleanField).not.toHaveAttribute("contenteditable"); - const hoverOutline = document.querySelector( - "[data-testid='visual-builder__hover-outline']" - ); - expect(hoverOutline).toHaveAttribute("style"); - - const customCursor = document.querySelector( - `[data-testid="visual-builder__cursor"]` - ); - expect(customCursor).toHaveAttribute("data-icon", "boolean"); - expect(customCursor?.classList.contains("visible")).toBeTruthy(); - }); - }); -}); diff --git a/src/visualBuilder/__test__/hover/fields/date.test.ts b/src/visualBuilder/__test__/hover/fields/date.test.ts deleted file mode 100644 index 7b1757cc..00000000 --- a/src/visualBuilder/__test__/hover/fields/date.test.ts +++ /dev/null @@ -1,122 +0,0 @@ -import { getFieldSchemaMap } from "../../../../__test__/data/fieldSchemaMap"; -import { waitForHoverOutline } from "../../../../__test__/utils"; -import Config from "../../../../configManager/configManager"; -import { VisualBuilder } from "../../../index"; -import { FieldSchemaMap } from "../../../utils/fieldSchemaMap"; -import { mockDomRect } from "./mockDomRect"; -import { act } from "@testing-library/preact"; - -vi.mock("../../../utils/visualBuilderPostMessage", async () => { - const { getAllContentTypes } = await vi.importActual< - typeof import("../../../../__test__/data/contentType") - >("../../../../__test__/data/contentType"); - const contentTypes = getAllContentTypes(); - return { - __esModule: true, - default: { - send: vi.fn().mockImplementation((eventName: string) => { - if (eventName === "init") - return Promise.resolve({ - contentTypes, - }); - return Promise.resolve(); - }), - on: vi.fn(), - }, - }; -}); - -vi.mock("../../../../utils/index.ts", () => { - return { - __esModule: true, - isOpenInBuilder: vi.fn().mockReturnValue(true), - isOpenInPreviewShare: vi.fn().mockReturnValue(false), - isOpeningInTimeline: vi.fn().mockReturnValue(false), - hasWindow: vi.fn().mockReturnValue(true), - addLivePreviewQueryTags: vi.fn(), - addParamsToUrl: vi.fn(), - }; -}); - -global.ResizeObserver = vi.fn().mockImplementation(() => ({ - observe: vi.fn(), - unobserve: vi.fn(), - disconnect: vi.fn(), -})); - -describe("When an element is hovered in visual builder mode", () => { - let mousemoveEvent: Event; - - beforeAll(() => { - FieldSchemaMap.setFieldSchema( - "all_fields", - getFieldSchemaMap().all_fields - ); - }); - - beforeEach(() => { - Config.reset(); - Config.set("mode", 2); - mousemoveEvent = new Event("mousemove", { - bubbles: true, - cancelable: true, - }); - }); - - afterEach(() => { - vi.clearAllMocks(); - document.getElementsByTagName("html")[0].innerHTML = ""; - }); - - afterAll(() => { - Config.reset(); - }); - - describe("date field", () => { - let dataField: HTMLParagraphElement; - let visualBuilder: VisualBuilder; - - beforeEach(() => { - dataField = document.createElement("p"); - dataField.setAttribute( - "data-cslp", - "all_fields.bltapikey.en-us.date" - ); - - dataField.getBoundingClientRect = vi - .fn() - .mockReturnValue(mockDomRect.singleLeft()); - - document.body.appendChild(dataField); - - visualBuilder = new VisualBuilder(); - }); - - afterEach(() => { - visualBuilder.destroy(); - }); - - test("should have outline and custom cursor", async () => { - await act(async () => { - dataField.dispatchEvent(mousemoveEvent); - }); - await waitForHoverOutline(); - expect(dataField).toHaveAttribute( - "data-cslp", - "all_fields.bltapikey.en-us.date" - ); - expect(dataField).not.toHaveAttribute("contenteditable"); - const hoverOutline = document.querySelector( - "[data-testid='visual-builder__hover-outline']" - ); - expect(hoverOutline).toHaveAttribute("style"); - - const customCursor = document.querySelector( - `[data-testid="visual-builder__cursor"]` - ); - - expect(customCursor).toHaveAttribute("data-icon", "isodate"); - expect(customCursor?.classList.contains("visible")).toBeTruthy(); - }); - }); -}); diff --git a/src/visualBuilder/__test__/hover/fields/file.test.ts b/src/visualBuilder/__test__/hover/fields/file.test.ts index 50483592..c971b234 100644 --- a/src/visualBuilder/__test__/hover/fields/file.test.ts +++ b/src/visualBuilder/__test__/hover/fields/file.test.ts @@ -1,10 +1,15 @@ -import { screen, waitFor, act } from "@testing-library/preact"; +import { screen, waitFor } from "@testing-library/preact"; import { getFieldSchemaMap } from "../../../../__test__/data/fieldSchemaMap"; -import { waitForHoverOutline } from "../../../../__test__/utils"; +import { + waitForHoverOutline, + waitForCursorToBeVisible, + waitForCursorIcon, +} from "../../../../__test__/utils"; import Config from "../../../../configManager/configManager"; import { VisualBuilder } from "../../../index"; import { FieldSchemaMap } from "../../../utils/fieldSchemaMap"; import { mockDomRect } from "./mockDomRect"; +import("@testing-library/preact"); vi.mock("../../../utils/visualBuilderPostMessage", async () => { const { getAllContentTypes } = await vi.importActual< @@ -35,21 +40,43 @@ vi.mock("../../../../utils/index.ts", async () => { }; }); +vi.mock("../../../utils/fetchEntryPermissionsAndStageDetails", () => { + return { + fetchEntryPermissionsAndStageDetails: vi.fn(() => + Promise.resolve({ + acl: { + create: true, + read: true, + update: true, + delete: true, + publish: true, + }, + workflowStage: { + stage: undefined, + permissions: { + entry: { + update: true, + }, + }, + }, + resolvedVariantPermissions: { + update: true, + }, + }) + ), + }; +}); const convertToPx = (value: number) => { return `${value}px`; }; const matchDimensions = (element: HTMLElement, hoverOutline: HTMLElement) => { const elementDimensions = element.getBoundingClientRect(); - // @ts-expect-error - TS doesn't know that style is a CSSStyleDeclaration - const hoverOutlineDimensions = hoverOutline?.style - ?._values as CSSStyleDeclaration; - expect(convertToPx(elementDimensions.x)).toBe(hoverOutlineDimensions.left); - expect(convertToPx(elementDimensions.y)).toBe(hoverOutlineDimensions.top); - expect(convertToPx(elementDimensions.width)).toBe( - hoverOutlineDimensions.width - ); + const hoverOutlineStyle = hoverOutline?.style as CSSStyleDeclaration; + expect(convertToPx(elementDimensions.x)).toBe(hoverOutlineStyle.left); + expect(convertToPx(elementDimensions.y)).toBe(hoverOutlineStyle.top); + expect(convertToPx(elementDimensions.width)).toBe(hoverOutlineStyle.width); expect(convertToPx(elementDimensions.height)).toBe( - hoverOutlineDimensions.height + hoverOutlineStyle.height ); }; describe("When an element is hovered in visual builder mode", () => { @@ -60,11 +87,6 @@ describe("When an element is hovered in visual builder mode", () => { "all_fields", getFieldSchemaMap().all_fields ); - global.ResizeObserver = vi.fn().mockImplementation(() => ({ - observe: vi.fn(), - unobserve: vi.fn(), - disconnect: vi.fn(), - })); global.MutationObserver = vi.fn().mockImplementation(() => ({ observe: vi.fn(), @@ -82,8 +104,9 @@ describe("When an element is hovered in visual builder mode", () => { document.getElementsByTagName("html")[0].innerHTML = ""; }); - afterEach(() => { - vi.clearAllMocks(); + afterEach(async () => { + // Wait longer for any pending async operations (like fetchEntryPermissionsAndStageDetails) to complete + // await new Promise((resolve) => setTimeout(resolve, 500)); document.getElementsByTagName("html")[0].innerHTML = ""; }); @@ -124,9 +147,7 @@ describe("When an element is hovered in visual builder mode", () => { }); test("should have outline and custom cursor", async () => { - await act(async () => { - fileField.dispatchEvent(mousemoveEvent); - }); + fileField.dispatchEvent(mousemoveEvent); await waitForHoverOutline(); const hoverOutline = document.querySelector( "[data-testid='visual-builder__hover-outline']" @@ -141,9 +162,7 @@ describe("When an element is hovered in visual builder mode", () => { }); test("should have a outline and custom cursor on the url as well", async () => { - await act(async () => { - imageField.dispatchEvent(mousemoveEvent); - }); + imageField.dispatchEvent(mousemoveEvent); await waitForHoverOutline(); const hoverOutline = document.querySelector( @@ -151,10 +170,20 @@ describe("When an element is hovered in visual builder mode", () => { ); expect(hoverOutline).toHaveAttribute("style"); + // Wait for cursor icon to be set (not "loading") - optimized timeout + await waitFor( + () => { + const customCursor = document.querySelector( + `[data-testid="visual-builder__cursor"]` + ); + expect(customCursor).toHaveAttribute("data-icon", "file"); + }, + { timeout: 2000, interval: 10 } // Optimized: reduced timeout and faster polling + ); + const customCursor = document.querySelector( `[data-testid="visual-builder__cursor"]` ); - expect(customCursor).toHaveAttribute("data-icon", "file"); expect(customCursor?.classList.contains("visible")).toBeTruthy(); }); @@ -231,15 +260,16 @@ describe("When an element is hovered in visual builder mode", () => { }); test("should have outline and custom cursor", async () => { - await act(async () => { - container.dispatchEvent(mousemoveEvent); - }); + container.dispatchEvent(mousemoveEvent); await waitForHoverOutline(); const hoverOutline = document.querySelector( "[data-testid='visual-builder__hover-outline']" ); expect(hoverOutline).toHaveAttribute("style"); + // Wait for cursor icon to be set (not "loading") + await waitForCursorIcon("file"); + const customCursor = document.querySelector( `[data-testid="visual-builder__cursor"]` ); @@ -248,9 +278,7 @@ describe("When an element is hovered in visual builder mode", () => { }); test("should have outline and custom cursor on individual instances", async () => { - await act(async () => { - firstFileField.dispatchEvent(mousemoveEvent); - }); + firstFileField.dispatchEvent(mousemoveEvent); await waitForHoverOutline(); const hoverOutline = document.querySelector( "[data-testid='visual-builder__hover-outline']" @@ -267,15 +295,17 @@ describe("When an element is hovered in visual builder mode", () => { }); test("should have outline and custom cursor on the url", async () => { - await act(async () => { - firstImageField.dispatchEvent(mousemoveEvent); - }); + firstImageField.dispatchEvent(mousemoveEvent); await waitForHoverOutline(); const hoverOutline = document.querySelector( "[data-testid='visual-builder__hover-outline']" ) as HTMLElement; expect(hoverOutline).toHaveAttribute("style"); matchDimensions(firstImageField, hoverOutline); + + // Wait for cursor icon to be set (not "loading") + await waitForCursorToBeVisible(); + const customCursor = document.querySelector( `[data-testid="visual-builder__cursor"]` ); diff --git a/src/visualBuilder/__test__/hover/fields/group.test.ts b/src/visualBuilder/__test__/hover/fields/group.test.ts index 91fedae0..13643f76 100644 --- a/src/visualBuilder/__test__/hover/fields/group.test.ts +++ b/src/visualBuilder/__test__/hover/fields/group.test.ts @@ -1,11 +1,13 @@ import { getFieldSchemaMap } from "../../../../__test__/data/fieldSchemaMap"; -import { sleep, waitForHoverOutline } from "../../../../__test__/utils"; +import { + waitForHoverOutline, + waitForCursorIcon, +} from "../../../../__test__/utils"; import Config from "../../../../configManager/configManager"; import { FieldSchemaMap } from "../../../utils/fieldSchemaMap"; import { mockDomRect } from "./mockDomRect"; import { VisualBuilder } from "../../../index"; import { screen } from "@testing-library/preact"; -import { act } from "@testing-library/preact"; vi.mock("../../../utils/visualBuilderPostMessage", async () => { const { getAllContentTypes } = await vi.importActual< @@ -26,12 +28,6 @@ vi.mock("../../../utils/visualBuilderPostMessage", async () => { }; }); -global.ResizeObserver = vi.fn().mockImplementation(() => ({ - observe: vi.fn(), - unobserve: vi.fn(), - disconnect: vi.fn(), -})); - vi.mock("../../../../utils/index.ts", async () => { const actual = await vi.importActual("../../../../utils"); return { @@ -41,6 +37,34 @@ vi.mock("../../../../utils/index.ts", async () => { }; }); +// Mock fetchEntryPermissionsAndStageDetails to resolve immediately - speeds up hover tests +vi.mock("../../../utils/fetchEntryPermissionsAndStageDetails", () => { + return { + fetchEntryPermissionsAndStageDetails: vi.fn(() => + Promise.resolve({ + acl: { + create: true, + read: true, + update: true, + delete: true, + publish: true, + }, + workflowStage: { + stage: undefined, + permissions: { + entry: { + update: true, + }, + }, + }, + resolvedVariantPermissions: { + update: true, + }, + }) + ), + }; +}); + describe("When an element is hovered in visual builder mode", () => { let mousemoveEvent: Event; @@ -106,19 +130,19 @@ describe("When an element is hovered in visual builder mode", () => { }); test("should have outline and custom cursor", async () => { - await act(async () => { - groupField.dispatchEvent(mousemoveEvent); - }); + groupField.dispatchEvent(mousemoveEvent); await waitForHoverOutline(); const hoverOutline = document.querySelector( "[data-testid='visual-builder__hover-outline']" ); expect(hoverOutline).toHaveAttribute("style"); + // Wait for cursor icon to be set (not "loading") + await waitForCursorIcon("group"); + const customCursor = document.querySelector( `[data-testid="visual-builder__cursor"]` ); - expect(customCursor).toHaveAttribute("data-icon", "group"); expect(customCursor?.classList.contains("visible")).toBeTruthy(); }); @@ -136,19 +160,19 @@ describe("When an element is hovered in visual builder mode", () => { groupField.appendChild(singleLine); - await act(async () => { - singleLine.dispatchEvent(mousemoveEvent); - }); + singleLine.dispatchEvent(mousemoveEvent); await waitForHoverOutline(); const hoverOutline = document.querySelector( "[data-testid='visual-builder__hover-outline']" ); expect(hoverOutline).toHaveAttribute("style"); + // Wait for cursor icon to be set (not "loading") + await waitForCursorIcon("singleline"); + const customCursor = document.querySelector( `[data-testid="visual-builder__cursor"]` ); - expect(customCursor).toHaveAttribute("data-icon", "singleline"); expect(customCursor?.classList.contains("visible")).toBeTruthy(); }); @@ -216,33 +240,41 @@ describe("When an element is hovered in visual builder mode", () => { visualBuilder.destroy(); }); - test("should have outline and custom cursor", async () => { - await act(async () => { - container.dispatchEvent(mousemoveEvent); - }); + test("should have outline and custom cursor on container", async () => { + container.dispatchEvent(mousemoveEvent); await waitForHoverOutline(); const hoverOutline = document.querySelector( "[data-testid='visual-builder__hover-outline']" ); expect(hoverOutline).toHaveAttribute("style"); + // Wait for cursor icon to be set (not "loading") + await waitForCursorIcon("group"); + const customCursor = document.querySelector( `[data-testid="visual-builder__cursor"]` ); - expect(customCursor).toHaveAttribute("data-icon", "group"); expect(customCursor?.classList.contains("visible")).toBeTruthy(); + }); - await act(async () => { - firstNestedMultiLine.dispatchEvent(mousemoveEvent); - }); + test("should have outline and custom cursor on nested multi line", async () => { + firstNestedMultiLine.dispatchEvent(mousemoveEvent); await waitForHoverOutline(); - const newCustomCursor = document.querySelector( + const hoverOutline = document.querySelector( + "[data-testid='visual-builder__hover-outline']" + ); + expect(hoverOutline).toHaveAttribute("style"); + + // Wait for cursor icon to be set (not "loading") + await waitForCursorIcon("multiline"); + + const customCursor = document.querySelector( `[data-testid="visual-builder__cursor"]` ); - expect(newCustomCursor).toHaveAttribute("data-icon", "multiline"); - expect(newCustomCursor?.classList.contains("visible")).toBeTruthy(); + expect(customCursor).toHaveAttribute("data-icon", "multiline"); + expect(customCursor?.classList.contains("visible")).toBeTruthy(); }); }); }); diff --git a/src/visualBuilder/__test__/hover/fields/html-rte.test.ts b/src/visualBuilder/__test__/hover/fields/html-rte.test.ts deleted file mode 100644 index 7850d9d5..00000000 --- a/src/visualBuilder/__test__/hover/fields/html-rte.test.ts +++ /dev/null @@ -1,199 +0,0 @@ -import { screen } from "@testing-library/preact"; -import { getFieldSchemaMap } from "../../../../__test__/data/fieldSchemaMap"; -import { waitForHoverOutline } from "../../../../__test__/utils"; -import Config from "../../../../configManager/configManager"; -import { VisualBuilder } from "../../../index"; -import { FieldSchemaMap } from "../../../utils/fieldSchemaMap"; -import { mockDomRect } from "./mockDomRect"; -import { act } from "@testing-library/preact"; - -vi.mock("../../../utils/visualBuilderPostMessage", async () => { - const { getAllContentTypes } = await vi.importActual< - typeof import("../../../../__test__/data/contentType") - >("../../../../__test__/data/contentType"); - const contentTypes = getAllContentTypes(); - return { - __esModule: true, - default: { - send: vi.fn().mockImplementation((eventName: string) => { - if (eventName === "init") - return Promise.resolve({ - contentTypes, - }); - return Promise.resolve(); - }), - }, - }; -}); - -vi.mock("../../../../utils/index.ts", async () => { - const actual = await vi.importActual("../../../../utils"); - return { - __esModule: true, - ...actual, - isOpenInBuilder: vi.fn().mockReturnValue(true), - }; -}); - -global.ResizeObserver = vi.fn().mockImplementation(() => ({ - observe: vi.fn(), - unobserve: vi.fn(), - disconnect: vi.fn(), -})); - -describe("When an element is hovered in visual builder mode", () => { - let mousemoveEvent: Event; - - beforeAll(() => { - FieldSchemaMap.setFieldSchema( - "all_fields", - getFieldSchemaMap().all_fields - ); - }); - - beforeEach(() => { - Config.reset(); - Config.set("mode", 2); - mousemoveEvent = new Event("mousemove", { - bubbles: true, - cancelable: true, - }); - }); - - afterEach(() => { - vi.clearAllMocks(); - document.getElementsByTagName("html")[0].innerHTML = ""; - }); - - afterAll(() => { - Config.reset(); - }); - - describe("HTML RTE field", () => { - let htmlRteField: HTMLParagraphElement; - let visualBuilder: VisualBuilder; - - beforeEach(() => { - htmlRteField = document.createElement("p"); - htmlRteField.setAttribute( - "data-cslp", - "all_fields.bltapikey.en-us.rich_text_editor" - ); - - htmlRteField.getBoundingClientRect = vi - .fn() - .mockReturnValue(mockDomRect.singleLeft()); - - document.body.appendChild(htmlRteField); - - visualBuilder = new VisualBuilder(); - }); - - afterEach(() => { - visualBuilder.destroy(); - }); - - test("should have outline and custom cursor", async () => { - await act(async () => { - htmlRteField.dispatchEvent(mousemoveEvent); - }); - await waitForHoverOutline(); - const hoverOutline = document.querySelector( - "[data-testid='visual-builder__hover-outline']" - ); - expect(hoverOutline).toHaveAttribute("style"); - - const customCursor = document.querySelector( - `[data-testid="visual-builder__cursor"]` - ); - - expect(customCursor).toHaveAttribute("data-icon", "html_rte"); - expect(customCursor?.classList.contains("visible")).toBeTruthy(); - }); - }); - - describe("HTML RTE field (multiple)", () => { - let container: HTMLDivElement; - let firstHtmlRteField: HTMLParagraphElement; - let secondHtmlRteField: HTMLParagraphElement; - let visualBuilder: VisualBuilder; - - beforeEach(() => { - container = document.createElement("div"); - container.setAttribute( - "data-cslp", - "all_fields.bltapikey.en-us.rich_text_editor_multiple_" - ); - container.getBoundingClientRect = vi - - .fn() - .mockReturnValue(mockDomRect.singleHorizontal()); - - firstHtmlRteField = document.createElement("p"); - firstHtmlRteField.setAttribute( - "data-cslp", - "all_fields.bltapikey.en-us.rich_text_editor_multiple_.0" - ); - - firstHtmlRteField.getBoundingClientRect = vi - .fn() - .mockReturnValue(mockDomRect.singleLeft()); - - secondHtmlRteField = document.createElement("p"); - secondHtmlRteField.setAttribute( - "data-cslp", - "all_fields.bltapikey.en-us.rich_text_editor_multiple_.1" - ); - - secondHtmlRteField.getBoundingClientRect = vi - .fn() - .mockReturnValue(mockDomRect.singleRight()); - - container.appendChild(firstHtmlRteField); - container.appendChild(secondHtmlRteField); - document.body.appendChild(container); - - visualBuilder = new VisualBuilder(); - }); - - afterEach(() => { - visualBuilder.destroy(); - }); - - test("should have outline and custom cursor", async () => { - await act(async () => { - container.dispatchEvent(mousemoveEvent); - }); - await waitForHoverOutline(); - const hoverOutline = document.querySelector( - "[data-testid='visual-builder__hover-outline']" - ); - expect(hoverOutline).toHaveAttribute("style"); - - const customCursor = document.querySelector( - `[data-testid="visual-builder__cursor"]` - ); - - expect(customCursor).toHaveAttribute("data-icon", "html_rte"); - expect(customCursor?.classList.contains("visible")).toBeTruthy(); - }); - - test("should have outline and cursor on individual instances", async () => { - await act(async () => { - firstHtmlRteField.dispatchEvent(mousemoveEvent); - }); - await waitForHoverOutline(); - const hoverOutline = document.querySelector( - "[data-testid='visual-builder__hover-outline']" - ); - expect(hoverOutline).toHaveAttribute("style"); - - const customCursor = document.querySelector( - `[data-testid="visual-builder__cursor"]` - ); - - expect(customCursor).toHaveAttribute("data-icon", "html_rte"); - expect(customCursor?.classList.contains("visible")).toBeTruthy(); - }); - }); -}); diff --git a/src/visualBuilder/__test__/hover/fields/json-rte.test.ts b/src/visualBuilder/__test__/hover/fields/json-rte.test.ts deleted file mode 100644 index 4b1ad7a4..00000000 --- a/src/visualBuilder/__test__/hover/fields/json-rte.test.ts +++ /dev/null @@ -1,199 +0,0 @@ -import { screen } from "@testing-library/preact"; -import { getFieldSchemaMap } from "../../../../__test__/data/fieldSchemaMap"; -import { waitForHoverOutline } from "../../../../__test__/utils"; -import Config from "../../../../configManager/configManager"; -import { VisualBuilder } from "../../../index"; -import { FieldSchemaMap } from "../../../utils/fieldSchemaMap"; -import { mockDomRect } from "./mockDomRect"; -import { act } from "@testing-library/preact"; - -vi.mock("../../../utils/visualBuilderPostMessage", async () => { - const { getAllContentTypes } = await vi.importActual< - typeof import("../../../../__test__/data/contentType") - >("../../../../__test__/data/contentType"); - const contentTypes = getAllContentTypes(); - return { - __esModule: true, - default: { - send: vi.fn().mockImplementation((eventName: string) => { - if (eventName === "init") - return Promise.resolve({ - contentTypes, - }); - return Promise.resolve(); - }), - }, - }; -}); - -vi.mock("../../../../utils/index.ts", async () => { - const actual = await vi.importActual("../../../../utils"); - return { - __esModule: true, - ...actual, - isOpenInBuilder: vi.fn().mockReturnValue(true), - }; -}); - -global.ResizeObserver = vi.fn().mockImplementation(() => ({ - observe: vi.fn(), - unobserve: vi.fn(), - disconnect: vi.fn(), -})); - -describe("When an element is hovered in visual builder mode", () => { - let mousemoveEvent: Event; - - beforeAll(() => { - FieldSchemaMap.setFieldSchema( - "all_fields", - getFieldSchemaMap().all_fields - ); - }); - - beforeEach(() => { - Config.reset(); - Config.set("mode", 2); - mousemoveEvent = new Event("mousemove", { - bubbles: true, - cancelable: true, - }); - }); - - afterEach(() => { - vi.clearAllMocks(); - document.getElementsByTagName("html")[0].innerHTML = ""; - }); - - afterAll(() => { - Config.reset(); - }); - - describe("JSON RTE field", () => { - let jsonRteField: HTMLParagraphElement; - let visualBuilder: VisualBuilder; - - beforeEach(() => { - jsonRteField = document.createElement("p"); - jsonRteField.setAttribute( - "data-cslp", - "all_fields.bltapikey.en-us.json_rte" - ); - - jsonRteField.getBoundingClientRect = vi - .fn() - .mockReturnValue(mockDomRect.singleLeft()); - - document.body.appendChild(jsonRteField); - - visualBuilder = new VisualBuilder(); - }); - - afterEach(() => { - visualBuilder.destroy(); - }); - - test("should have outline and custom cursor", async () => { - await act(async () => { - jsonRteField.dispatchEvent(mousemoveEvent); - }); - await waitForHoverOutline(); - const hoverOutline = document.querySelector( - "[data-testid='visual-builder__hover-outline']" - ); - expect(hoverOutline).toHaveAttribute("style"); - - const customCursor = document.querySelector( - `[data-testid="visual-builder__cursor"]` - ); - - expect(customCursor).toHaveAttribute("data-icon", "json_rte"); - expect(customCursor?.classList.contains("visible")).toBeTruthy(); - }); - }); - - describe("JSON RTE field (multiple)", () => { - let container: HTMLDivElement; - let firstJsonRteField: HTMLParagraphElement; - let secondJsonRteField: HTMLParagraphElement; - let visualBuilder: VisualBuilder; - - beforeEach(() => { - container = document.createElement("div"); - container.setAttribute( - "data-cslp", - "all_fields.bltapikey.en-us.json_rich_text_editor_multiple_" - ); - - container.getBoundingClientRect = vi - .fn() - .mockReturnValue(mockDomRect.singleHorizontal()); - - firstJsonRteField = document.createElement("p"); - firstJsonRteField.setAttribute( - "data-cslp", - "all_fields.bltapikey.en-us.json_rich_text_editor_multiple_.0" - ); - - firstJsonRteField.getBoundingClientRect = vi - .fn() - .mockReturnValue(mockDomRect.singleLeft()); - - secondJsonRteField = document.createElement("p"); - secondJsonRteField.setAttribute( - "data-cslp", - "all_fields.bltapikey.en-us.json_rich_text_editor_multiple_.1" - ); - - secondJsonRteField.getBoundingClientRect = vi - .fn() - .mockReturnValue(mockDomRect.singleRight()); - - container.appendChild(firstJsonRteField); - container.appendChild(secondJsonRteField); - document.body.appendChild(container); - - visualBuilder = new VisualBuilder(); - }); - - afterEach(() => { - visualBuilder.destroy(); - }); - - test("should have outline and custom cursor", async () => { - await act(async () => { - container.dispatchEvent(mousemoveEvent); - }); - await waitForHoverOutline(); - const hoverOutline = document.querySelector( - "[data-testid='visual-builder__hover-outline']" - ); - expect(hoverOutline).toHaveAttribute("style"); - - const customCursor = document.querySelector( - `[data-testid="visual-builder__cursor"]` - ); - - expect(customCursor).toHaveAttribute("data-icon", "json_rte"); - expect(customCursor?.classList.contains("visible")).toBeTruthy(); - }); - - test("should have outline and custom cursor on individual instances", async () => { - await act(async () => { - firstJsonRteField.dispatchEvent(mousemoveEvent); - }); - await waitForHoverOutline(); - const hoverOutline = document.querySelector( - "[data-testid='visual-builder__hover-outline']" - ); - expect(hoverOutline).toHaveAttribute("style"); - - const customCursor = document.querySelector( - `[data-testid="visual-builder__cursor"]` - ); - - expect(customCursor).toHaveAttribute("data-icon", "json_rte"); - expect(customCursor?.classList.contains("visible")).toBeTruthy(); - }); - }); -}); diff --git a/src/visualBuilder/__test__/hover/fields/link.test.ts b/src/visualBuilder/__test__/hover/fields/link.test.ts deleted file mode 100644 index bd9ca996..00000000 --- a/src/visualBuilder/__test__/hover/fields/link.test.ts +++ /dev/null @@ -1,196 +0,0 @@ -import { screen } from "@testing-library/preact"; -import { getFieldSchemaMap } from "../../../../__test__/data/fieldSchemaMap"; -import { waitForHoverOutline } from "../../../../__test__/utils"; -import Config from "../../../../configManager/configManager"; -import { VisualBuilder } from "../../../index"; -import { FieldSchemaMap } from "../../../utils/fieldSchemaMap"; -import { mockDomRect } from "./mockDomRect"; -import { act } from "@testing-library/preact"; - -vi.mock("../../../utils/visualBuilderPostMessage", async () => { - const { getAllContentTypes } = await vi.importActual< - typeof import("../../../../__test__/data/contentType") - >("../../../../__test__/data/contentType"); - const contentTypes = getAllContentTypes(); - return { - __esModule: true, - default: { - send: vi.fn().mockImplementation((eventName: string) => { - if (eventName === "init") - return Promise.resolve({ - contentTypes, - }); - return Promise.resolve(); - }), - }, - }; -}); - -vi.mock("../../../../utils/index.ts", async () => { - const actual = await vi.importActual("../../../../utils"); - return { - __esModule: true, - ...actual, - isOpenInBuilder: vi.fn().mockReturnValue(true), - }; -}); - -global.ResizeObserver = vi.fn().mockImplementation(() => ({ - observe: vi.fn(), - unobserve: vi.fn(), - disconnect: vi.fn(), -})); - -describe("When an element is hovered in visual builder mode", () => { - let mousemoveEvent: Event; - - beforeAll(() => { - FieldSchemaMap.setFieldSchema( - "all_fields", - getFieldSchemaMap().all_fields - ); - }); - - beforeEach(() => { - Config.reset(); - Config.set("mode", 2); - mousemoveEvent = new Event("mousemove", { - bubbles: true, - cancelable: true, - }); - }); - - afterEach(() => { - vi.clearAllMocks(); - document.getElementsByTagName("html")[0].innerHTML = ""; - }); - - afterAll(() => { - Config.reset(); - }); - - describe("link field", () => { - let linkField: HTMLAnchorElement; - let visualBuilder: VisualBuilder; - - beforeEach(() => { - linkField = document.createElement("a"); - linkField.setAttribute( - "data-cslp", - "all_fields.bltapikey.en-us.link.href" - ); - - linkField.getBoundingClientRect = vi - .fn() - .mockReturnValue(mockDomRect.singleLeft()); - - document.body.appendChild(linkField); - - visualBuilder = new VisualBuilder(); - }); - - afterEach(() => { - visualBuilder.destroy(); - }); - - test("should have outline and custom cursor", async () => { - await act(async () => { - linkField.dispatchEvent(mousemoveEvent); - }); - await waitForHoverOutline(); - const hoverOutline = document.querySelector( - "[data-testid='visual-builder__hover-outline']" - ); - expect(hoverOutline).toHaveAttribute("style"); - - const customCursor = document.querySelector( - `[data-testid="visual-builder__cursor"]` - ); - - expect(customCursor).toHaveAttribute("data-icon", "link"); - expect(customCursor?.classList.contains("visible")).toBeTruthy(); - }); - }); - - describe("link field (multiple)", () => { - let container: HTMLDivElement; - let firstLinkField: HTMLAnchorElement; - let secondLinkField: HTMLAnchorElement; - let visualBuilder: VisualBuilder; - - beforeEach(() => { - container = document.createElement("div"); - container.setAttribute( - "data-cslp", - "all_fields.bltapikey.en-us.link_multiple_" - ); - container.getBoundingClientRect = vi - .fn() - .mockReturnValue(mockDomRect.singleHorizontal()); - - firstLinkField = document.createElement("a"); - firstLinkField.setAttribute( - "data-cslp", - "all_fields.blt366df6233d9915f5.en-us.link_multiple_.0.href" - ); - firstLinkField.getBoundingClientRect = vi - .fn() - .mockReturnValue(mockDomRect.singleLeft()); - - secondLinkField = document.createElement("a"); - secondLinkField.setAttribute( - "data-cslp", - "all_fields.bltapikey.en-us.link_multiple_.1.href" - ); - secondLinkField.getBoundingClientRect = vi - .fn() - .mockReturnValue(mockDomRect.singleRight()); - - container.appendChild(firstLinkField); - container.appendChild(secondLinkField); - document.body.appendChild(container); - - visualBuilder = new VisualBuilder(); - }); - - afterEach(() => { - visualBuilder.destroy(); - }); - - test("should have outline and custom cursor", async () => { - await act(async () => { - container.dispatchEvent(mousemoveEvent); - }); - await waitForHoverOutline(); - const hoverOutline = document.querySelector( - "[data-testid='visual-builder__hover-outline']" - ); - expect(hoverOutline).toHaveAttribute("style"); - - const customCursor = document.querySelector( - `[data-testid="visual-builder__cursor"]` - ); - - expect(customCursor).toHaveAttribute("data-icon", "link"); - expect(customCursor?.classList.contains("visible")).toBeTruthy(); - }); - - test("should have outline and custom cursor on individual instances", async () => { - await act(async () => { - firstLinkField.dispatchEvent(mousemoveEvent); - }); - await waitForHoverOutline(); - const hoverOutline = document.querySelector( - "[data-testid='visual-builder__hover-outline']" - ); - expect(hoverOutline).toHaveAttribute("style"); - - const customCursor = document.querySelector( - `[data-testid="visual-builder__cursor"]` - ); - - expect(customCursor).toHaveAttribute("data-icon", "link"); - expect(customCursor?.classList.contains("visible")).toBeTruthy(); - }); - }); -}); diff --git a/src/visualBuilder/__test__/hover/fields/markdown.test.ts b/src/visualBuilder/__test__/hover/fields/markdown.test.ts deleted file mode 100644 index 04a3bba9..00000000 --- a/src/visualBuilder/__test__/hover/fields/markdown.test.ts +++ /dev/null @@ -1,200 +0,0 @@ -import { screen } from "@testing-library/preact"; -import { getFieldSchemaMap } from "../../../../__test__/data/fieldSchemaMap"; -import { waitForHoverOutline } from "../../../../__test__/utils"; -import Config from "../../../../configManager/configManager"; -import { VisualBuilder } from "../../../index"; -import { FieldSchemaMap } from "../../../utils/fieldSchemaMap"; -import { mockDomRect } from "./mockDomRect"; -import { act } from "@testing-library/preact"; - -vi.mock("../../../utils/visualBuilderPostMessage", async () => { - const { getAllContentTypes } = await vi.importActual< - typeof import("../../../../__test__/data/contentType") - >("../../../../__test__/data/contentType"); - const contentTypes = getAllContentTypes(); - return { - __esModule: true, - default: { - send: vi.fn().mockImplementation((eventName: string) => { - if (eventName === "init") - return Promise.resolve({ - contentTypes, - }); - return Promise.resolve(); - }), - }, - }; -}); - -global.ResizeObserver = vi.fn().mockImplementation(() => ({ - observe: vi.fn(), - unobserve: vi.fn(), - disconnect: vi.fn(), -})); - -vi.mock("../../../../utils/index.ts", async () => { - const actual = await vi.importActual("../../../../utils"); - return { - __esModule: true, - ...actual, - isOpenInBuilder: vi.fn().mockReturnValue(true), - }; -}); - -describe("When an element is hovered in visual builder mode", () => { - let mousemoveEvent: Event; - - beforeAll(() => { - FieldSchemaMap.setFieldSchema( - "all_fields", - getFieldSchemaMap().all_fields - ); - }); - - beforeEach(() => { - Config.reset(); - Config.set("mode", 2); - mousemoveEvent = new Event("mousemove", { - bubbles: true, - cancelable: true, - }); - document.getElementsByTagName("html")[0].innerHTML = ""; - }); - - afterEach(() => { - vi.clearAllMocks(); - document.getElementsByTagName("html")[0].innerHTML = ""; - }); - - afterAll(() => { - Config.reset(); - }); - - describe("markdown field", () => { - let markdownField: HTMLParagraphElement; - let visualBuilder: VisualBuilder; - - beforeEach(() => { - markdownField = document.createElement("p"); - markdownField.setAttribute( - "data-cslp", - "all_fields.bltapikey.en-us.markdown" - ); - - markdownField.getBoundingClientRect = vi - .fn() - .mockReturnValue(mockDomRect.singleLeft()); - - document.body.appendChild(markdownField); - - visualBuilder = new VisualBuilder(); - }); - - afterEach(() => { - visualBuilder.destroy(); - }); - - test("should have outline and custom cursor", async () => { - await act(async () => { - markdownField.dispatchEvent(mousemoveEvent); - }); - await waitForHoverOutline(); - const hoverOutline = document.querySelector( - "[data-testid='visual-builder__hover-outline']" - ); - expect(hoverOutline).toHaveAttribute("style"); - - const customCursor = document.querySelector( - `[data-testid="visual-builder__cursor"]` - ); - - expect(customCursor).toHaveAttribute("data-icon", "markdown_rte"); - expect(customCursor?.classList.contains("visible")).toBeTruthy(); - }); - }); - - describe("markdown field (multiple)", () => { - let container: HTMLDivElement; - let firstMarkdownField: HTMLParagraphElement; - let secondMarkdownField: HTMLParagraphElement; - let visualBuilder: VisualBuilder; - - beforeEach(() => { - container = document.createElement("div"); - container.setAttribute( - "data-cslp", - "all_fields.bltapikey.en-us.markdown_multiple_" - ); - - container.getBoundingClientRect = vi - .fn() - .mockReturnValue(mockDomRect.singleHorizontal()); - - firstMarkdownField = document.createElement("p"); - firstMarkdownField.setAttribute( - "data-cslp", - "all_fields.bltapikey.en-us.markdown_multiple_.0" - ); - - firstMarkdownField.getBoundingClientRect = vi - .fn() - .mockReturnValue(mockDomRect.singleLeft()); - - secondMarkdownField = document.createElement("p"); - secondMarkdownField.setAttribute( - "data-cslp", - "all_fields.bltapikey.en-us.markdown_multiple_.1" - ); - - secondMarkdownField.getBoundingClientRect = vi - .fn() - .mockReturnValue(mockDomRect.singleRight()); - - container.appendChild(firstMarkdownField); - container.appendChild(secondMarkdownField); - document.body.appendChild(container); - - visualBuilder = new VisualBuilder(); - }); - - afterEach(() => { - visualBuilder.destroy(); - }); - - test("should have outline and custom cursor", async () => { - await act(async () => { - container.dispatchEvent(mousemoveEvent); - }); - await waitForHoverOutline(); - const hoverOutline = document.querySelector( - "[data-testid='visual-builder__hover-outline']" - ); - expect(hoverOutline).toHaveAttribute("style"); - - const customCursor = document.querySelector( - `[data-testid="visual-builder__cursor"]` - ); - - expect(customCursor).toHaveAttribute("data-icon", "markdown_rte"); - expect(customCursor?.classList.contains("visible")).toBeTruthy(); - }); - - test("should have outline and custom cursor on individual instances", async () => { - await act(async () => { - firstMarkdownField.dispatchEvent(mousemoveEvent); - }); - await waitForHoverOutline(); - const hoverOutline = document.querySelector( - "[data-testid='visual-builder__hover-outline']" - ); - expect(hoverOutline).toHaveAttribute("style"); - - const customCursor = document.querySelector( - `[data-testid="visual-builder__cursor"]` - ); - - expect(customCursor).toHaveAttribute("data-icon", "markdown_rte"); - expect(customCursor?.classList.contains("visible")).toBeTruthy(); - }); - }); -}); diff --git a/src/visualBuilder/__test__/hover/fields/multi-line.test.ts b/src/visualBuilder/__test__/hover/fields/multi-line.test.ts deleted file mode 100644 index e86b9d26..00000000 --- a/src/visualBuilder/__test__/hover/fields/multi-line.test.ts +++ /dev/null @@ -1,198 +0,0 @@ -import { screen } from "@testing-library/preact"; -import { getFieldSchemaMap } from "../../../../__test__/data/fieldSchemaMap"; -import { waitForHoverOutline } from "../../../../__test__/utils"; -import Config from "../../../../configManager/configManager"; -import { VisualBuilder } from "../../../index"; -import { FieldSchemaMap } from "../../../utils/fieldSchemaMap"; -import { mockDomRect } from "./mockDomRect"; -import { act } from "@testing-library/preact"; - -vi.mock("../../../utils/visualBuilderPostMessage", async () => { - const { getAllContentTypes } = await vi.importActual< - typeof import("../../../../__test__/data/contentType") - >("../../../../__test__/data/contentType"); - const contentTypes = getAllContentTypes(); - return { - __esModule: true, - default: { - send: vi.fn().mockImplementation((eventName: string) => { - if (eventName === "init") - return Promise.resolve({ - contentTypes, - }); - return Promise.resolve(); - }), - }, - }; -}); - -global.ResizeObserver = vi.fn().mockImplementation(() => ({ - observe: vi.fn(), - unobserve: vi.fn(), - disconnect: vi.fn(), -})); - -vi.mock("../../../../utils/index.ts", async () => { - const actual = await vi.importActual("../../../../utils"); - return { - __esModule: true, - ...actual, - isOpenInBuilder: vi.fn().mockReturnValue(true), - }; -}); - -describe("When an element is hovered in visual builder mode", () => { - let mousemoveEvent: Event; - - beforeAll(() => { - FieldSchemaMap.setFieldSchema( - "all_fields", - getFieldSchemaMap().all_fields - ); - }); - - beforeEach(() => { - Config.reset(); - Config.set("mode", 2); - mousemoveEvent = new Event("mousemove", { - bubbles: true, - cancelable: true, - }); - }); - - afterEach(() => { - vi.clearAllMocks(); - document.getElementsByTagName("html")[0].innerHTML = ""; - }); - - afterAll(() => { - Config.reset(); - }); - - describe("multi line field", () => { - let multiLineField: HTMLParagraphElement; - let visualBuilder: VisualBuilder; - - beforeEach(() => { - multiLineField = document.createElement("p"); - multiLineField.setAttribute( - "data-cslp", - "all_fields.bltapikey.en-us.multi_line" - ); - - multiLineField.getBoundingClientRect = vi - - .fn() - .mockReturnValue(mockDomRect.singleLeft()); - document.body.appendChild(multiLineField); - - visualBuilder = new VisualBuilder(); - }); - - afterEach(() => { - visualBuilder.destroy(); - }); - - test("should have outline and custom cursor", async () => { - await act(async () => { - multiLineField.dispatchEvent(mousemoveEvent); - }); - await waitForHoverOutline(); - const hoverOutline = screen.getByTestId( - "visual-builder__hover-outline" - ); - expect(hoverOutline).toHaveAttribute("style"); - - const customCursor = document.querySelector( - `[data-testid="visual-builder__cursor"]` - ); - - expect(customCursor).toHaveAttribute("data-icon", "multiline"); - expect(customCursor?.classList.contains("visible")).toBeTruthy(); - }); - }); - - describe("multi line field (multiple)", () => { - let container: HTMLDivElement; - let firstMultiLineField: HTMLParagraphElement; - let secondMultiLineField: HTMLParagraphElement; - let visualBuilder: VisualBuilder; - - beforeEach(() => { - container = document.createElement("div"); - container.setAttribute( - "data-cslp", - "all_fields.bltapikey.en-us.multi_line_textbox_multiple_" - ); - container.getBoundingClientRect = vi - .fn() - .mockReturnValue(mockDomRect.singleHorizontal()); - - firstMultiLineField = document.createElement("p"); - firstMultiLineField.setAttribute( - "data-cslp", - "all_fields.bltapikey.en-us.multi_line_textbox_multiple_.0" - ); - - firstMultiLineField.getBoundingClientRect = vi - .fn() - .mockReturnValue(mockDomRect.singleLeft()); - - secondMultiLineField = document.createElement("p"); - secondMultiLineField.setAttribute( - "data-cslp", - "all_fields.bltapikey.en-us.multi_line_textbox_multiple_.1" - ); - - secondMultiLineField.getBoundingClientRect = vi - .fn() - .mockReturnValue(mockDomRect.singleRight()); - - container.appendChild(firstMultiLineField); - container.appendChild(secondMultiLineField); - document.body.appendChild(container); - - visualBuilder = new VisualBuilder(); - }); - - afterEach(() => { - visualBuilder.destroy(); - }); - - test("should have outline and custom cursor", async () => { - await act(async () => { - container.dispatchEvent(mousemoveEvent); - }); - await waitForHoverOutline(); - const hoverOutline = document.querySelector( - "[data-testid='visual-builder__hover-outline']" - ); - expect(hoverOutline).toHaveAttribute("style"); - - const customCursor = document.querySelector( - `[data-testid="visual-builder__cursor"]` - ); - - expect(customCursor).toHaveAttribute("data-icon", "multiline"); - expect(customCursor?.classList.contains("visible")).toBeTruthy(); - }); - - test("should have outline and custom cursor on individual instances", async () => { - await act(async () => { - firstMultiLineField.dispatchEvent(mousemoveEvent); - }); - await waitForHoverOutline(); - const hoverOutline = document.querySelector( - "[data-testid='visual-builder__hover-outline']" - ); - expect(hoverOutline).toHaveAttribute("style"); - - const customCursor = document.querySelector( - `[data-testid="visual-builder__cursor"]` - ); - - expect(customCursor).toHaveAttribute("data-icon", "multiline"); - expect(customCursor?.classList.contains("visible")).toBeTruthy(); - }); - }); -}); diff --git a/src/visualBuilder/__test__/hover/fields/number.test.ts b/src/visualBuilder/__test__/hover/fields/number.test.ts deleted file mode 100644 index 0732bdda..00000000 --- a/src/visualBuilder/__test__/hover/fields/number.test.ts +++ /dev/null @@ -1,196 +0,0 @@ -import { screen } from "@testing-library/preact"; -import { getFieldSchemaMap } from "../../../../__test__/data/fieldSchemaMap"; -import { sleep, waitForHoverOutline } from "../../../../__test__/utils"; -import Config from "../../../../configManager/configManager"; -import { VisualBuilder } from "../../../index"; -import { FieldSchemaMap } from "../../../utils/fieldSchemaMap"; -import { mockDomRect } from "./mockDomRect"; -import { act } from "@testing-library/preact"; - -vi.mock("../../../utils/visualBuilderPostMessage", async () => { - const { getAllContentTypes } = await vi.importActual< - typeof import("../../../../__test__/data/contentType") - >("../../../../__test__/data/contentType"); - const contentTypes = getAllContentTypes(); - return { - __esModule: true, - default: { - send: vi.fn().mockImplementation((eventName: string) => { - if (eventName === "init") - return Promise.resolve({ - contentTypes, - }); - return Promise.resolve(); - }), - }, - }; -}); - -vi.mock("../../../../utils/index.ts", async () => { - const actual = await vi.importActual("../../../../utils"); - return { - __esModule: true, - ...actual, - isOpenInBuilder: vi.fn().mockReturnValue(true), - }; -}); - -global.ResizeObserver = vi.fn().mockImplementation(() => ({ - observe: vi.fn(), - unobserve: vi.fn(), - disconnect: vi.fn(), -})); - -describe("When an element is hovered in visual builder mode", () => { - let mousemoveEvent: Event; - - beforeAll(() => { - FieldSchemaMap.setFieldSchema( - "all_fields", - getFieldSchemaMap().all_fields - ); - }); - - beforeEach(() => { - Config.reset(); - Config.set("mode", 2); - mousemoveEvent = new Event("mousemove", { - bubbles: true, - cancelable: true, - }); - }); - - afterEach(() => { - vi.clearAllMocks(); - document.getElementsByTagName("html")[0].innerHTML = ""; - }); - - afterAll(() => { - Config.reset(); - }); - - describe("number field", () => { - let numberField: HTMLParagraphElement; - let visualBuilder: VisualBuilder; - - beforeEach(() => { - numberField = document.createElement("p"); - numberField.setAttribute( - "data-cslp", - "all_fields.bltapikey.en-us.number" - ); - - numberField.getBoundingClientRect = vi - .fn() - .mockReturnValue(mockDomRect.singleLeft()); - - document.body.appendChild(numberField); - - visualBuilder = new VisualBuilder(); - }); - - afterEach(() => { - visualBuilder.destroy(); - }); - - test("should have outline and custom cursor", async () => { - await act(async () => { - numberField.dispatchEvent(mousemoveEvent); - }); - await waitForHoverOutline(); - const hoverOutline = document.querySelector( - "[data-testid='visual-builder__hover-outline']" - ); - expect(hoverOutline).toHaveAttribute("style"); - - const customCursor = document.querySelector( - `[data-testid="visual-builder__cursor"]` - ); - - expect(customCursor).toHaveAttribute("data-icon", "number"); - expect(customCursor?.classList.contains("visible")).toBeTruthy(); - }); - }); - - describe("number field (multiple)", () => { - let container: HTMLDivElement; - let firstNumberField: HTMLParagraphElement; - let secondNumberField: HTMLParagraphElement; - let visualBuilder: VisualBuilder; - - beforeEach(() => { - container = document.createElement("div"); - container.setAttribute( - "data-cslp", - "all_fields.bltapikey.en-us.number_multiple_" - ); - container.getBoundingClientRect = vi - .fn() - .mockReturnValue(mockDomRect.singleHorizontal()); - - firstNumberField = document.createElement("p"); - firstNumberField.setAttribute( - "data-cslp", - "all_fields.bltapikey.en-us.number_multiple_.0" - ); - firstNumberField.getBoundingClientRect = vi - .fn() - .mockReturnValue(mockDomRect.singleLeft()); - - secondNumberField = document.createElement("p"); - secondNumberField.setAttribute( - "data-cslp", - "all_fields.bltapikey.en-us.number_multiple_.1" - ); - secondNumberField.getBoundingClientRect = vi - .fn() - .mockReturnValue(mockDomRect.singleRight()); - - container.appendChild(firstNumberField); - container.appendChild(secondNumberField); - document.body.appendChild(container); - - visualBuilder = new VisualBuilder(); - }); - - afterEach(() => { - visualBuilder.destroy(); - }); - - test("should have outline and custom cursor", async () => { - await act(async () => { - container.dispatchEvent(mousemoveEvent); - }); - await waitForHoverOutline(); - const hoverOutline = document.querySelector( - "[data-testid='visual-builder__hover-outline']" - ); - expect(hoverOutline).toHaveAttribute("style"); - - const customCursor = document.querySelector( - `[data-testid="visual-builder__cursor"]` - ); - - expect(customCursor).toHaveAttribute("data-icon", "number"); - expect(customCursor?.classList.contains("visible")).toBeTruthy(); - }); - - test("should have outline and custom cursor on individual instances", async () => { - await act(async () => { - firstNumberField.dispatchEvent(mousemoveEvent); - }); - await waitForHoverOutline(); - const hoverOutline = document.querySelector( - "[data-testid='visual-builder__hover-outline']" - ); - expect(hoverOutline).toHaveAttribute("style"); - - const customCursor = document.querySelector( - `[data-testid="visual-builder__cursor"]` - ); - - expect(customCursor).toHaveAttribute("data-icon", "number"); - expect(customCursor?.classList.contains("visible")).toBeTruthy(); - }); - }); -}); diff --git a/src/visualBuilder/__test__/hover/fields/reference.test.ts b/src/visualBuilder/__test__/hover/fields/reference.test.ts deleted file mode 100644 index f7076cce..00000000 --- a/src/visualBuilder/__test__/hover/fields/reference.test.ts +++ /dev/null @@ -1,199 +0,0 @@ -import { getFieldSchemaMap } from "../../../../__test__/data/fieldSchemaMap"; -import { waitForHoverOutline } from "../../../../__test__/utils"; -import Config from "../../../../configManager/configManager"; -import { VisualBuilder } from "../../../index"; -import { FieldSchemaMap } from "../../../utils/fieldSchemaMap"; -import { mockDomRect } from "./mockDomRect"; -import { screen } from "@testing-library/preact"; -import { act } from "@testing-library/preact"; - -vi.mock("../../../utils/visualBuilderPostMessage", async () => { - const { getAllContentTypes } = await vi.importActual< - typeof import("../../../../__test__/data/contentType") - >("../../../../__test__/data/contentType"); - const contentTypes = getAllContentTypes(); - return { - __esModule: true, - default: { - send: vi.fn().mockImplementation((eventName: string) => { - if (eventName === "init") - return Promise.resolve({ - contentTypes, - }); - return Promise.resolve(); - }), - }, - }; -}); - -vi.mock("../../../../utils/index.ts", async () => { - const actual = await vi.importActual("../../../../utils"); - return { - __esModule: true, - ...actual, - isOpenInBuilder: vi.fn().mockReturnValue(true), - }; -}); - -global.ResizeObserver = vi.fn().mockImplementation(() => ({ - observe: vi.fn(), - unobserve: vi.fn(), - disconnect: vi.fn(), -})); - -describe("When an element is hovered in visual builder mode", () => { - let mousemoveEvent: Event; - - beforeAll(() => { - FieldSchemaMap.setFieldSchema( - "all_fields", - getFieldSchemaMap().all_fields - ); - }); - - beforeEach(() => { - Config.reset(); - Config.set("mode", 2); - mousemoveEvent = new Event("mousemove", { - bubbles: true, - cancelable: true, - }); - }); - - afterEach(() => { - vi.clearAllMocks(); - document.getElementsByTagName("html")[0].innerHTML = ""; - }); - - afterAll(() => { - Config.reset(); - }); - - describe("reference field", () => { - let referenceField: HTMLDivElement; - let visualBuilder: VisualBuilder; - - beforeEach(() => { - referenceField = document.createElement("div"); - referenceField.setAttribute( - "data-cslp", - "all_fields.bltapikey.en-us.reference" - ); - - referenceField.getBoundingClientRect = vi - .fn() - .mockReturnValue(mockDomRect.singleLeft()); - - document.body.appendChild(referenceField); - - visualBuilder = new VisualBuilder(); - }); - - afterEach(() => { - visualBuilder.destroy(); - }); - - test("should have outline and custom cursor", async () => { - await act(async () => { - referenceField.dispatchEvent(mousemoveEvent); - }); - await waitForHoverOutline(); - - const hoverOutline = document.querySelector( - "[data-testid='visual-builder__hover-outline']" - ); - expect(hoverOutline).toHaveAttribute("style"); - - const customCursor = document.querySelector( - `[data-testid="visual-builder__cursor"]` - ); - - expect(customCursor).toHaveAttribute("data-icon", "reference"); - expect(customCursor?.classList.contains("visible")).toBeTruthy(); - }); - }); - - describe("reference field (multiple)", () => { - let container: HTMLDivElement; - let firstReferenceField: HTMLDivElement; - let secondReferenceField: HTMLDivElement; - let visualBuilder: VisualBuilder; - - beforeEach(() => { - container = document.createElement("div"); - container.setAttribute( - "data-cslp", - "all_fields.bltapikey.en-us.reference_multiple_" - ); - container.getBoundingClientRect = vi - .fn() - .mockReturnValue(mockDomRect.singleHorizontal()); - - firstReferenceField = document.createElement("div"); - firstReferenceField.setAttribute( - "data-cslp", - "all_fields.bltapikey.en-us.reference_multiple_.0" - ); - - firstReferenceField.getBoundingClientRect = vi - .fn() - .mockReturnValue(mockDomRect.singleLeft()); - - secondReferenceField = document.createElement("div"); - secondReferenceField.setAttribute( - "data-cslp", - "all_fields.bltapikey.en-us.reference_multiple_.1" - ); - - secondReferenceField.getBoundingClientRect = vi - .fn() - .mockReturnValue(mockDomRect.singleRight()); - - container.appendChild(firstReferenceField); - container.appendChild(secondReferenceField); - document.body.appendChild(container); - - visualBuilder = new VisualBuilder(); - }); - - afterEach(() => { - visualBuilder.destroy(); - }); - - test("should have outline and custom cursor", async () => { - await act(async () => { - container.dispatchEvent(mousemoveEvent); - }); - await waitForHoverOutline(); - const hoverOutline = document.querySelector( - "[data-testid='visual-builder__hover-outline']" - ); - expect(hoverOutline).toHaveAttribute("style"); - - const customCursor = document.querySelector( - `[data-testid="visual-builder__cursor"]` - ); - - expect(customCursor).toHaveAttribute("data-icon", "reference"); - expect(customCursor?.classList.contains("visible")).toBeTruthy(); - }); - - test("should have outline and custom cursor on individual instances", async () => { - await act(async () => { - firstReferenceField.dispatchEvent(mousemoveEvent); - }); - await waitForHoverOutline(); - const hoverOutline = document.querySelector( - "[data-testid='visual-builder__hover-outline']" - ); - expect(hoverOutline).toHaveAttribute("style"); - - const customCursor = document.querySelector( - `[data-testid="visual-builder__cursor"]` - ); - - expect(customCursor).toHaveAttribute("data-icon", "reference"); - expect(customCursor?.classList.contains("visible")).toBeTruthy(); - }); - }); -}); diff --git a/src/visualBuilder/__test__/hover/fields/select.test.ts b/src/visualBuilder/__test__/hover/fields/select.test.ts deleted file mode 100644 index 34c54d37..00000000 --- a/src/visualBuilder/__test__/hover/fields/select.test.ts +++ /dev/null @@ -1,210 +0,0 @@ -import { getFieldSchemaMap } from "../../../../__test__/data/fieldSchemaMap"; -import { waitForHoverOutline } from "../../../../__test__/utils"; -import Config from "../../../../configManager/configManager"; -import { VisualBuilder } from "../../../index"; -import { FieldSchemaMap } from "../../../utils/fieldSchemaMap"; -import { mockDomRect } from "./mockDomRect"; -import { act, screen } from "@testing-library/preact"; - -vi.mock("../../../utils/visualBuilderPostMessage", async () => { - const { getAllContentTypes } = await vi.importActual< - typeof import("../../../../__test__/data/contentType") - >("../../../../__test__/data/contentType"); - const contentTypes = getAllContentTypes(); - return { - __esModule: true, - default: { - send: vi.fn().mockImplementation((eventName: string) => { - if (eventName === "init") - return Promise.resolve({ - contentTypes, - }); - return Promise.resolve(); - }), - }, - }; -}); - -global.ResizeObserver = vi.fn().mockImplementation(() => ({ - observe: vi.fn(), - unobserve: vi.fn(), - disconnect: vi.fn(), -})); - -vi.mock("../../../../utils/index.ts", async () => { - const actual = await vi.importActual("../../../../utils"); - return { - __esModule: true, - ...actual, - isOpenInBuilder: vi.fn().mockReturnValue(true), - }; -}); - -describe("When an element is hovered in visual builder mode", () => { - let mousemoveEvent: Event; - - beforeAll(() => { - FieldSchemaMap.setFieldSchema( - "all_fields", - getFieldSchemaMap().all_fields - ); - global.ResizeObserver = vi.fn().mockImplementation(() => ({ - observe: vi.fn(), - unobserve: vi.fn(), - disconnect: vi.fn(), - })); - - global.MutationObserver = vi.fn().mockImplementation(() => ({ - observe: vi.fn(), - disconnect: vi.fn(), - })); - }); - - beforeEach(() => { - Config.reset(); - Config.set("mode", 2); - mousemoveEvent = new Event("mousemove", { - bubbles: true, - cancelable: true, - }); - document.getElementsByTagName("html")[0].innerHTML = ""; - }); - - afterEach(() => { - vi.clearAllMocks(); - document.getElementsByTagName("html")[0].innerHTML = ""; - }); - - afterAll(() => { - Config.reset(); - }); - - describe("select field", () => { - let selectField: HTMLParagraphElement; - let visualBuilder: VisualBuilder; - - beforeEach(() => { - selectField = document.createElement("p"); - selectField.setAttribute( - "data-cslp", - "all_fields.bltapikey.en-us.select" - ); - - selectField.getBoundingClientRect = vi - .fn() - .mockReturnValue(mockDomRect.singleLeft()); - - document.body.appendChild(selectField); - - visualBuilder = new VisualBuilder(); - }); - - afterEach(() => { - visualBuilder.destroy(); - }); - - test("should have outline and custom cursor", async () => { - await act(async () => { - selectField.dispatchEvent(mousemoveEvent); - }); - await waitForHoverOutline(); - const hoverOutline = document.querySelector( - "[data-testid='visual-builder__hover-outline']" - ); - expect(hoverOutline).toHaveAttribute("style"); - - const customCursor = document.querySelector( - `[data-testid="visual-builder__cursor"]` - ); - - expect(customCursor).toHaveAttribute("data-icon", "select"); - expect(customCursor?.classList.contains("visible")).toBeTruthy(); - }); - }); - - describe("select field (multiple)", () => { - let container: HTMLDivElement; - let firstSelectField: HTMLParagraphElement; - let secondSelectField: HTMLParagraphElement; - let visualBuilder: VisualBuilder; - - beforeEach(() => { - container = document.createElement("div"); - container.setAttribute( - "data-cslp", - "all_fields.bltapikey.en-us.select_multiple_" - ); - container.getBoundingClientRect = vi - .fn() - .mockReturnValue(mockDomRect.singleHorizontal()); - - firstSelectField = document.createElement("p"); - firstSelectField.setAttribute( - "data-cslp", - "all_fields.bltapikey.en-us.select_multiple_.0" - ); - - firstSelectField.getBoundingClientRect = vi - .fn() - .mockReturnValue(mockDomRect.singleLeft()); - - secondSelectField = document.createElement("p"); - secondSelectField.setAttribute( - "data-cslp", - "all_fields.bltapikey.en-us.select_multiple_.1" - ); - - secondSelectField.getBoundingClientRect = vi - .fn() - .mockReturnValue(mockDomRect.singleLeft()); - - container.appendChild(firstSelectField); - container.appendChild(secondSelectField); - document.body.appendChild(container); - - visualBuilder = new VisualBuilder(); - }); - - afterEach(() => { - visualBuilder.destroy(); - }); - - test("should have outline and custom cursor", async () => { - await act(async () => { - container.dispatchEvent(mousemoveEvent); - }); - await waitForHoverOutline(); - - const hoverOutline = document.querySelector( - "[data-testid='visual-builder__hover-outline']" - ); - expect(hoverOutline).toHaveAttribute("style"); - - const customCursor = document.querySelector( - `[data-testid="visual-builder__cursor"]` - ); - - expect(customCursor).toHaveAttribute("data-icon", "select"); - expect(customCursor?.classList.contains("visible")).toBeTruthy(); - }); - - test("should have outline and custom cursor on individual instances", async () => { - await act(async () => { - firstSelectField.dispatchEvent(mousemoveEvent); - }); - await waitForHoverOutline(); - - const hoverOutline = document.querySelector( - "[data-testid='visual-builder__hover-outline']" - ); - expect(hoverOutline).toHaveAttribute("style"); - - const customCursor = document.querySelector( - `[data-testid="visual-builder__cursor"]` - ); - - expect(customCursor).toHaveAttribute("data-icon", "select"); - expect(customCursor?.classList.contains("visible")).toBeTruthy(); - }); - }); -}); diff --git a/src/visualBuilder/__test__/hover/fields/single-line.test.ts b/src/visualBuilder/__test__/hover/fields/single-line.test.ts index 63694781..48d3a3d8 100644 --- a/src/visualBuilder/__test__/hover/fields/single-line.test.ts +++ b/src/visualBuilder/__test__/hover/fields/single-line.test.ts @@ -4,7 +4,7 @@ import Config from "../../../../configManager/configManager"; import { VisualBuilder } from "../../../index"; import { FieldSchemaMap } from "../../../utils/fieldSchemaMap"; import { mockDomRect } from "./mockDomRect"; -import { act, screen } from "@testing-library/preact"; +import { screen } from "@testing-library/preact"; vi.mock("../../../utils/visualBuilderPostMessage", async () => { const { getAllContentTypes } = await vi.importActual< @@ -25,12 +25,6 @@ vi.mock("../../../utils/visualBuilderPostMessage", async () => { }; }); -global.ResizeObserver = vi.fn().mockImplementation(() => ({ - observe: vi.fn(), - unobserve: vi.fn(), - disconnect: vi.fn(), -})); - vi.mock("../../../../utils/index.ts", async () => { const actual = await vi.importActual("../../../../utils"); return { @@ -40,6 +34,34 @@ vi.mock("../../../../utils/index.ts", async () => { }; }); +// Mock fetchEntryPermissionsAndStageDetails to resolve immediately - speeds up hover tests +vi.mock("../../../utils/fetchEntryPermissionsAndStageDetails", () => { + return { + fetchEntryPermissionsAndStageDetails: vi.fn(() => + Promise.resolve({ + acl: { + create: true, + read: true, + update: true, + delete: true, + publish: true, + }, + workflowStage: { + stage: undefined, + permissions: { + entry: { + update: true, + }, + }, + }, + resolvedVariantPermissions: { + update: true, + }, + }) + ), + }; +}); + describe("When an element is hovered in visual builder mode", () => { let mousemoveEvent: Event; @@ -60,7 +82,6 @@ describe("When an element is hovered in visual builder mode", () => { }); afterEach(() => { - vi.clearAllMocks(); document.getElementsByTagName("html")[0].innerHTML = ""; }); @@ -91,9 +112,7 @@ describe("When an element is hovered in visual builder mode", () => { }); test("should have outline and custom cursor", async () => { - await act(() => { - titleField.dispatchEvent(mousemoveEvent); - }); + titleField.dispatchEvent(mousemoveEvent); await waitForHoverOutline(); expect(titleField).not.toHaveAttribute("style"); const hoverOutline = screen.getByTestId( @@ -111,131 +130,6 @@ describe("When an element is hovered in visual builder mode", () => { }); }); - describe("single line field", () => { - let singleLineField: HTMLParagraphElement; - let visualBuilder: VisualBuilder; - - beforeEach(() => { - singleLineField = document.createElement("p"); - singleLineField.setAttribute( - "data-cslp", - "all_fields.bltapikey.en-us.single_line" - ); - singleLineField.getBoundingClientRect = vi - .fn() - .mockReturnValue(mockDomRect.singleLeft()); - document.body.appendChild(singleLineField); - - visualBuilder = new VisualBuilder(); - }); - - afterEach(() => { - visualBuilder.destroy(); - }); - - test("should have outline and custom cursor", async () => { - await act(() => { - singleLineField.dispatchEvent(mousemoveEvent); - }); - await waitForHoverOutline(); - expect(singleLineField).not.toHaveAttribute("style"); - - const customCursor = document.querySelector( - `[data-testid="visual-builder__cursor"]` - ); - - expect(customCursor).toHaveAttribute("data-icon", "singleline"); - expect(customCursor?.classList.contains("visible")).toBeTruthy(); - }); - }); - - describe("single line field (multiple)", () => { - let container: HTMLDivElement; - let firstSingleLineField: HTMLParagraphElement; - let secondSingleLineField: HTMLParagraphElement; - let visualBuilder: VisualBuilder; - - beforeEach(() => { - container = document.createElement("div"); - container.setAttribute( - "data-cslp", - "all_fields.bltapikey.en-us.single_line_textbox_multiple_" - ); - container.getBoundingClientRect = vi - .fn() - .mockReturnValue(mockDomRect.singleHorizontal()); - - firstSingleLineField = document.createElement("p"); - firstSingleLineField.setAttribute( - "data-cslp", - "all_fields.bltapikey.en-us.single_line_textbox_multiple_.0" - ); - firstSingleLineField.getBoundingClientRect = vi - .fn() - .mockReturnValue(mockDomRect.singleLeft()); - - secondSingleLineField = document.createElement("p"); - secondSingleLineField.setAttribute( - "data-cslp", - "all_fields.bltapikey.en-us.single_line_textbox_multiple_.1" - ); - secondSingleLineField.getBoundingClientRect = vi - .fn() - .mockReturnValue(mockDomRect.singleRight()); - - container.appendChild(firstSingleLineField); - container.appendChild(secondSingleLineField); - document.body.appendChild(container); - - visualBuilder = new VisualBuilder(); - }); - - afterEach(() => { - visualBuilder.destroy(); - }); - - test("should have outline and custom cursor", async () => { - await act(() => { - container.dispatchEvent(mousemoveEvent); - }); - container.dispatchEvent(mousemoveEvent); - await waitForHoverOutline(); - expect(container).not.toHaveAttribute("style"); - - const hoverOutline = document.querySelector( - "[data-testid='visual-builder__hover-outline']" - ); - expect(hoverOutline).toHaveStyle( - "top: 34px; left: 34px; width: 828px; height: 54.3984375px;" - ); - - const customCursor = document.querySelector( - `[data-testid="visual-builder__cursor"]` - ); - - expect(customCursor).toHaveAttribute("data-icon", "singleline"); - expect(customCursor?.classList.contains("visible")).toBeTruthy(); - }); - - test("should have outline and custom cursor on individual instances", async () => { - await act(() => { - firstSingleLineField.dispatchEvent(mousemoveEvent); - }); - await waitForHoverOutline(); - expect(firstSingleLineField).not.toHaveAttribute("style"); - const hoverOutline = document.querySelector( - "[data-testid='visual-builder__hover-outline']" - ); - expect(hoverOutline).toHaveStyle( - "top: 51px; left: 51px; width: 27.7734375px; height: 20.3984375px;" - ); - - const customCursor = document.querySelector( - `[data-testid="visual-builder__cursor"]` - ); - - expect(customCursor).toHaveAttribute("data-icon", "singleline"); - expect(customCursor?.classList.contains("visible")).toBeTruthy(); - }); - }); + // NOTE: Standard single-line field tests (single and multiple) are now in consolidated-hover.test.ts + // This file only contains the unique "title field" test which checks specific style values }); diff --git a/src/visualBuilder/__test__/index.test.ts b/src/visualBuilder/__test__/index.test.ts index dc1749a2..981884a2 100644 --- a/src/visualBuilder/__test__/index.test.ts +++ b/src/visualBuilder/__test__/index.test.ts @@ -18,7 +18,7 @@ import { Mock } from "vitest"; const INLINE_EDITABLE_FIELD_VALUE = "Hello World"; -vi.mock("../utils/visualBuilderPostMessage", async () => { +vi.mock("../utils/visualBuilderPostMessage", async (importOriginal) => { const { getAllContentTypes } = await vi.importActual< typeof import("../../__test__/data/contentType") >("../../__test__/data/contentType"); @@ -27,14 +27,53 @@ vi.mock("../utils/visualBuilderPostMessage", async () => { return { __esModule: true, default: { - send: vi.fn().mockImplementation((eventName: string) => { - if (eventName === "init") + send: vi.fn((eventName: string) => { + if (eventName === "init") { return Promise.resolve({ contentTypes, }); - return Promise.resolve(); + } + // Mock workflow stage details and permissions + if (eventName === "get-workflow-stage-details") { + return Promise.resolve({ + stage: { name: "Draft" }, + permissions: { + entry: { + update: true, + }, + }, + }); + } + if (eventName === "get-entry-permissions") { + return Promise.resolve({ + can_update: true, + can_delete: true, + }); + } + if (eventName === "get-resolved-variant-permissions") { + return Promise.resolve({ + can_update: true, + }); + } + if (eventName === "field-location-data") { + return Promise.resolve({ apps: [] }); + } + // Mock field data for modular blocks + if (eventName === "get-field-data") { + return Promise.resolve({ + fieldData: INLINE_EDITABLE_FIELD_VALUE, + }); + } + // Mock field display names + if (eventName === "get-field-display-names") { + return Promise.resolve({ + "all_fields.blt58a50b4cebae75c5.en-us.modular_blocks.0.block.single_line": + "Single Line", + }); + } + return Promise.resolve({}); }), - on: vi.fn(), + on: vi.fn(() => ({ unregister: vi.fn() })), }, }; }); @@ -77,212 +116,80 @@ describe( vi.spyOn(document.body, "scrollHeight", "get").mockReturnValue(100); }); - beforeEach(() => { - (visualBuilderPostMessage?.send as Mock).mockClear(); - document.getElementsByTagName("html")[0].innerHTML = ""; - cleanup(); - }); + beforeEach(() => { + vi.clearAllMocks(); + document.getElementsByTagName("html")[0].innerHTML = ""; + cleanup(); + }); - afterAll(() => { - FieldSchemaMap.clear(); - }); - - test( - "should append a visual builder container to the DOM", - async () => { - let visualBuilderDOM = document.querySelector( - ".visual-builder__container" - ); + afterAll(() => { + FieldSchemaMap.clear(); + }); - expect(visualBuilderDOM).toBeNull(); + test("should append a visual builder container to the DOM", async () => { + let visualBuilderDOM = document.querySelector( + ".visual-builder__container" + ); - const x = new VisualBuilder(); - await waitForBuilderSDKToBeInitialized( - visualBuilderPostMessage - ); + expect(visualBuilderDOM).toBeNull(); - visualBuilderDOM = document.querySelector( - `[data-testid="visual-builder__container"]` - ); + const x = new VisualBuilder(); + await waitForBuilderSDKToBeInitialized(visualBuilderPostMessage); - expect( - document.querySelector( - '[data-testid="visual-builder__cursor"]' - ) - ).toBeInTheDocument(); - expect( - document.querySelector( - '[data-testid="visual-builder__focused-toolbar"]' - ) - ).toBeInTheDocument(); - expect( - document.querySelector( - '[data-testid="visual-builder__hover-outline"]' - ) - ).toBeInTheDocument(); - expect( - document.querySelector( - '[data-testid="visual-builder__overlay__wrapper"]' - ) - ).toBeInTheDocument(); - x.destroy(); - } + visualBuilderDOM = document.querySelector( + `[data-testid="visual-builder__container"]` ); - test( - "should add overlay to DOM when clicked", - async () => { - const h1Tag = document.createElement("h1"); - h1Tag.textContent = INLINE_EDITABLE_FIELD_VALUE; - h1Tag.setAttribute( - "data-cslp", - "all_fields.blt58a50b4cebae75c5.en-us.modular_blocks.0.block.single_line" - ); - document.body.appendChild(h1Tag); - mockGetBoundingClientRect(h1Tag); - const x = new VisualBuilder(); - await triggerAndWaitForClickAction( - visualBuilderPostMessage, - h1Tag - ); - await waitFor(() => { - const overlayOutline = document.querySelector( - '[data-testid="visual-builder__overlay--outline"]' - ); - expect(overlayOutline).toHaveStyle({ - top: "10px", - left: "10px", - width: "10px", - height: "5px", - "outline-color": "rgb(113, 92, 221)", - }); - }); - x.destroy(); - }, + expect( + document.querySelector( + '[data-testid="visual-builder__cursor"]' + ) + ).toBeInTheDocument(); + expect( + document.querySelector( + '[data-testid="visual-builder__focused-toolbar"]' + ) + ).toBeInTheDocument(); + expect( + document.querySelector( + '[data-testid="visual-builder__hover-outline"]' + ) + ).toBeInTheDocument(); + expect( + document.querySelector( + '[data-testid="visual-builder__overlay__wrapper"]' + ) + ).toBeInTheDocument(); + x.destroy(); + }); + + test( + "should add overlay to DOM when clicked", + async () => { + const h1Tag = document.createElement("h1"); + h1Tag.textContent = INLINE_EDITABLE_FIELD_VALUE; + h1Tag.setAttribute( + "data-cslp", + "all_fields.blt58a50b4cebae75c5.en-us.modular_blocks.0.block.single_line" ); + document.body.appendChild(h1Tag); + mockGetBoundingClientRect(h1Tag); + const x = new VisualBuilder(); - // skipped as this is already tested in click related tests. - // this can cause failure for the above test. - describe.skip("on click, the sdk", () => { - afterEach(() => { - document.getElementsByTagName("html")[0].innerHTML = ""; - }); - - test("should do nothing if data-cslp not available", async () => { - const h1 = document.createElement("h1"); - - document.body.appendChild(h1); - const x = new VisualBuilder(); - await triggerAndWaitForClickAction( - visualBuilderPostMessage, - h1, - { skipWaitForFieldType: true } - ); - - expect(h1).not.toHaveAttribute("contenteditable"); - expect(h1).not.toHaveAttribute("data-cslp-field-type"); - x.destroy(); - }); - - describe("inline elements must be contenteditable", () => { - let visualBuilder: VisualBuilder; - let h1: HTMLHeadingElement; - beforeAll(() => { - (visualBuilderPostMessage?.send as Mock).mockImplementation( - (eventName: string, args) => { - if ( - eventName === - VisualBuilderPostMessageEvents.GET_FIELD_DATA - ) { - const values: Record = { - single_line: INLINE_EDITABLE_FIELD_VALUE, - multi_line: INLINE_EDITABLE_FIELD_VALUE, - file: { - uid: "fileUid", - }, - }; - return Promise.resolve({ - fieldData: values[args.entryPath], - }); - } else if ( - eventName === - VisualBuilderPostMessageEvents.GET_FIELD_DISPLAY_NAMES - ) { - const names: Record = { - "all_fields.blt58a50b4cebae75c5.en-us.single_line": - "Single Line", - "all_fields.blt58a50b4cebae75c5.en-us.multi_line": - "Multi Line", - "all_fields.blt58a50b4cebae75c5.en-us.file": - "File", - }; - return Promise.resolve({ - [args.cslp]: names[args.cslp], - }); - } - return Promise.resolve({}); - } - ); - }); - - beforeEach(async () => { - document.getElementsByTagName("html")[0].innerHTML = ""; - h1 = document.createElement("h1"); - h1.textContent = INLINE_EDITABLE_FIELD_VALUE; - mockGetBoundingClientRect(h1); - h1.setAttribute( - "data-cslp", - "all_fields.blt58a50b4cebae75c5.en-us.single_line" - ); - - document.body.appendChild(h1); - visualBuilder = new VisualBuilder(); - }); - afterEach(() => { - visualBuilder.destroy(); - }); - test( - "single line should be contenteditable", - async () => { - await triggerAndWaitForClickAction( - visualBuilderPostMessage, - h1 - ); + await triggerAndWaitForClickAction(visualBuilderPostMessage, h1Tag); - await waitFor(() => { - expect(h1).toHaveAttribute("contenteditable"); - expect(h1).toHaveAttribute( - "data-cslp-field-type", - "singleline" - ); - }); - }, - { timeout: 40 * 1000 } - ); - - test( - "multi line should be contenteditable", - async () => { - h1.setAttribute( - "data-cslp", - "all_fields.blt58a50b4cebae75c5.en-us.multi_line" - ); - await triggerAndWaitForClickAction( - visualBuilderPostMessage, - h1 - ); - - await waitFor(() => { - expect(h1).toHaveAttribute("contenteditable"); - expect(h1).toHaveAttribute( - "data-cslp-field-type", - "multiline" - ); - }); - }, - { timeout: 40 * 1000 } - ); - }); + const overlayOutline = document.querySelector( + '[data-testid="visual-builder__overlay--outline"]' + ); + // Verify overlay exists and has correct positioning + expect(overlayOutline).toBeInTheDocument(); + expect(overlayOutline).toHaveStyle({ + top: "10px", + left: "10px", + width: "10px", + height: "5px", }); - }, -); + + x.destroy(); + }); +}); diff --git a/src/visualBuilder/__test__/withoutIframe.test.ts b/src/visualBuilder/__test__/withoutIframe.test.ts index 1a61c115..7418ddb2 100644 --- a/src/visualBuilder/__test__/withoutIframe.test.ts +++ b/src/visualBuilder/__test__/withoutIframe.test.ts @@ -39,7 +39,7 @@ vi.mock("../../utils/index.ts", async () => { }); import visualBuilderPostMessage from "../utils/visualBuilderPostMessage"; -import { act, fireEvent, waitFor, screen } from "@testing-library/preact"; +import { fireEvent, waitFor, screen } from "@testing-library/preact"; Object.defineProperty(globalThis, "crypto", { value: { @@ -47,12 +47,6 @@ Object.defineProperty(globalThis, "crypto", { }, }); -global.ResizeObserver = vi.fn().mockImplementation(() => ({ - observe: vi.fn(), - unobserve: vi.fn(), - disconnect: vi.fn(), -})); - describe("When outside the Visual Builder, the Visual Builder", () => { beforeAll(() => { Config.set("mode", 2); @@ -85,9 +79,7 @@ describe("When outside the Visual Builder, the Visual Builder", () => { new VisualBuilder(); await waitForBuilderSDKToBeInitialized(visualBuilderPostMessage); - await act(async () => { - await fireEvent.click(h1); - }); + await fireEvent.click(h1); expect(h1.getAttribute("contenteditable")).toBe(null); }); diff --git a/src/visualBuilder/components/__test__/fieldLabelWrapper.test.tsx b/src/visualBuilder/components/__test__/fieldLabelWrapper.test.tsx deleted file mode 100644 index 9559ef73..00000000 --- a/src/visualBuilder/components/__test__/fieldLabelWrapper.test.tsx +++ /dev/null @@ -1,559 +0,0 @@ -import { waitFor } from "@testing-library/preact"; -import FieldLabelWrapperComponent from "../fieldLabelWrapper"; -import { CslpData } from "../../../cslp/types/cslp.types"; -import { VisualBuilderCslpEventDetails } from "../../types/visualBuilder.types"; -import { singleLineFieldSchema } from "../../../__test__/data/fields"; -import { asyncRender } from "../../../__test__/utils"; -import { isFieldDisabled } from "../../utils/isFieldDisabled"; -import { FieldSchemaMap } from "../../utils/fieldSchemaMap"; -import visualBuilderPostMessage from "../../utils/visualBuilderPostMessage"; -import React from "preact/compat"; - -// All mocks -vi.mock("../Tooltip", () => ({ - ToolbarTooltip: ({ children, data, disabled }: any) => ( -
- {children} -
- ), -})); - -vi.mock("../../utils/fieldSchemaMap", () => ({ - FieldSchemaMap: { - getFieldSchema: vi.fn().mockResolvedValue({ - display_name: "Field 0", - data_type: "text", - field_metadata: {}, - uid: "test_field", - }), - }, -})); - -vi.mock("../../utils/visualBuilderPostMessage", () => ({ - default: { - send: vi.fn().mockImplementation((eventName: string, fields: any) => { - if (eventName === "GET_FIELD_DISPLAY_NAMES") { - // Always return display names for all requested fields - const result: Record = {}; - fields.forEach((field: any) => { - if (field.cslpValue === "mockFieldCslp") { - result[field.cslpValue] = "Field 0"; - } else if ( - field.cslpValue === - "contentTypeUid.entryUid.locale.parentPath1" - ) { - result[field.cslpValue] = "Field 1"; - } else if ( - field.cslpValue === - "contentTypeUid.entryUid.locale.parentPath2" - ) { - result[field.cslpValue] = "Field 2"; - } else if ( - field.cslpValue === - "contentTypeUid.entryUid.locale.parentPath3" - ) { - result[field.cslpValue] = "Field 3"; - } else { - result[field.cslpValue] = field.cslpValue; // fallback - } - }); - return Promise.resolve(result); - } else if (eventName === "GET_CONTENT_TYPE_NAME") { - return Promise.resolve({ - contentTypeName: "Page CT", - }); - } else if (eventName === "REFERENCE_MAP") { - return Promise.resolve({ - mockEntryUid: [ - { - contentTypeUid: "mockContentTypeUid", - contentTypeTitle: "Page CT", - referenceFieldName: "Reference Field", - }, - ], - }); - } - return Promise.resolve({}); - }), - }, -})); - -vi.mock("../../utils/isFieldDisabled", async (importOriginal) => { - const actual = - await importOriginal(); - return { - ...actual, - isFieldDisabled: vi.fn().mockReturnValue({ isDisabled: false }), - }; -}); - -vi.mock("../../../cslp", () => ({ - extractDetailsFromCslp: vi.fn().mockImplementation((path) => { - return { - content_type_uid: "mockContentTypeUid", - fieldPath: path, - cslpValue: path, - }; - }), -})); - -vi.mock("../../utils/fetchEntryPermissionsAndStageDetails", () => ({ - fetchEntryPermissionsAndStageDetails: async () => ({ - acl: { - update: { - create: true, - read: true, - update: true, - delete: true, - publish: true, - }, - }, - workflowStage: { - stage: undefined, - permissions: { - entry: { - update: true, - }, - }, - }, - }), -})); - -vi.mock("../generators/generateCustomCursor", () => ({ - getFieldIcon: vi.fn().mockReturnValue("mock-icon"), - FieldTypeIconsMap: { - reference: "reference-icon", - }, -})); - -vi.mock("../visualBuilder.style", () => ({ - visualBuilderStyles: vi.fn().mockReturnValue({ - "visual-builder__focused-toolbar--variant": - "visual-builder__focused-toolbar--variant", - }), -})); - -vi.mock("../VariantIndicator", () => ({ - VariantIndicator: () =>
Variant
, -})); - -vi.mock("../../utils/errorHandling", () => ({ - hasPostMessageError: vi.fn().mockReturnValue(false), -})); - -const DISPLAY_NAMES = { - mockFieldCslp: "Field 0", - parentPath1: "Field 1", - parentPath2: "Field 2", - parentPath3: "Field 3", -}; - -const pathPrefix = "contentTypeUid.entryUid.locale"; -const PARENT_PATHS = [ - `${pathPrefix}.parentPath1`, - `${pathPrefix}.parentPath2`, - `${pathPrefix}.parentPath3`, -]; - -describe.skip("FieldLabelWrapperComponent", () => { - beforeEach(() => { - vi.mocked(isFieldDisabled).mockReturnValue({ - isDisabled: false, - reason: "", - }); - - // Reset the mock implementation to the default one - vi.mocked(visualBuilderPostMessage!.send).mockImplementation( - (eventName: string, fields: any) => { - if (eventName === "GET_FIELD_DISPLAY_NAMES") { - // Always return display names for all requested fields - const result: Record = {}; - fields.forEach((field: any) => { - if (field.cslpValue === "mockFieldCslp") { - result[field.cslpValue] = "Field 0"; - } else if ( - field.cslpValue === - "contentTypeUid.entryUid.locale.parentPath1" - ) { - result[field.cslpValue] = "Field 1"; - } else if ( - field.cslpValue === - "contentTypeUid.entryUid.locale.parentPath2" - ) { - result[field.cslpValue] = "Field 2"; - } else if ( - field.cslpValue === - "contentTypeUid.entryUid.locale.parentPath3" - ) { - result[field.cslpValue] = "Field 3"; - } else { - result[field.cslpValue] = field.cslpValue; // fallback - } - }); - return Promise.resolve(result); - } else if (eventName === "GET_CONTENT_TYPE_NAME") { - return Promise.resolve({ - contentTypeName: "Page CT", - }); - } else if (eventName === "REFERENCE_MAP") { - return Promise.resolve({ - mockEntryUid: [ - { - contentTypeUid: "mockContentTypeUid", - contentTypeTitle: "Page CT", - referenceFieldName: "Reference Field", - }, - ], - }); - } - return Promise.resolve({}); - } - ); - }); - - afterEach(() => { - vi.clearAllMocks(); - }); - - const mockFieldMetadata: CslpData = { - entry_uid: "mockEntryUid", - content_type_uid: "mockContentTypeUid", - cslpValue: "mockFieldCslp", - locale: "", - variant: undefined, - fieldPath: "mockFieldPath", - fieldPathWithIndex: "", - multipleFieldMetadata: { - index: 0, - parentDetails: { - parentPath: "", - parentCslpValue: "", - }, - }, - instance: { - fieldPathWithIndex: "", - }, - }; - - const mockEventDetails: VisualBuilderCslpEventDetails = { - editableElement: document.createElement("div"), - cslpData: "", - fieldMetadata: mockFieldMetadata, - }; - - const mockGetParentEditable = () => document.createElement("div"); - - test( - "renders current field and parent fields correctly", - async () => { - const { findByText } = await asyncRender( - - ); - - const currentField = await findByText( - DISPLAY_NAMES.mockFieldCslp, - {}, - { timeout: 15000 } - ); - expect(currentField).toBeVisible(); - }, - { timeout: 20000 } - ); - - test("displays current field icon", async () => { - const { findByTestId } = await asyncRender( - - ); - - const fieldIcon = await findByTestId("visual-builder__field-icon"); - expect(fieldIcon).toBeInTheDocument(); - }); - - test("renders with correct class when field is disabled", async () => { - vi.mocked(isFieldDisabled).mockReturnValue({ - isDisabled: true, - reason: "You have only read access to this field", - }); - const { findByTestId } = await asyncRender( - - ); - - const fieldLabel = await findByTestId( - "visual-builder__focused-toolbar__field-label-wrapper" - ); - - await waitFor(() => { - expect(fieldLabel).toHaveClass( - "visual-builder__focused-toolbar--field-disabled" - ); - }); - }); - - test("calls isFieldDisabled with correct arguments", async () => { - const mockFieldSchema = { ...singleLineFieldSchema }; - - vi.mocked(FieldSchemaMap.getFieldSchema).mockResolvedValue( - mockFieldSchema - ); - - await asyncRender( - - ); - - // wait for component to mount - await waitFor(() => { - expect( - document.querySelector( - ".visual-builder__focused-toolbar__field-label-container" - ) - ).toBeInTheDocument(); - }); - - expect(isFieldDisabled).toHaveBeenCalledWith( - mockFieldSchema, - mockEventDetails, - undefined, - { - update: { - create: true, - read: true, - update: true, - delete: true, - publish: true, - }, - }, - { - stage: undefined, - permissions: { - entry: { - update: true, - }, - }, - } - ); - }); - - test( - "renders ToolbarTooltip component with correct data", - async () => { - const { findByTestId } = await asyncRender( - - ); - - // Check that the ToolbarTooltip wrapper is rendered - const tooltipWrapper = await findByTestId("toolbar-tooltip", { - timeout: 15000, - }); - expect(tooltipWrapper).toBeInTheDocument(); - - // Check that the main field label wrapper is rendered - const fieldLabelWrapper = await findByTestId( - "visual-builder__focused-toolbar__field-label-wrapper", - { timeout: 15000 } - ); - expect(fieldLabelWrapper).toBeInTheDocument(); - }, - { timeout: 20000 } - ); - - test("does not render reference icon when isReference is false", async () => { - const { container } = await asyncRender( - - ); - - await waitFor(() => { - const referenceIconContainer = container.querySelector( - ".visual-builder__reference-icon-container" - ); - expect(referenceIconContainer).not.toBeInTheDocument(); - }); - }); - - test("renders with correct hovered cslp data attribute", async () => { - const { findByTestId } = await asyncRender( - - ); - - const fieldLabelWrapper = await findByTestId( - "visual-builder__focused-toolbar__field-label-wrapper" - ); - expect(fieldLabelWrapper).toHaveAttribute( - "data-hovered-cslp", - mockFieldMetadata.cslpValue - ); - }); - - test("does not render ContentTypeIcon when loading", async () => { - // Mock the display names to never resolve to simulate loading state - vi.mocked(visualBuilderPostMessage!.send).mockImplementation(() => { - return new Promise(() => {}); // Never resolves - }); - - const { container } = await asyncRender( - - ); - - // Wait a bit to ensure the component has time to render - await new Promise((resolve) => setTimeout(resolve, 100)); - - const contentTypeIcon = container.querySelector( - ".visual-builder__content-type-icon" - ); - expect(contentTypeIcon).not.toBeInTheDocument(); - }); - - test("renders VariantIndicator when field has variant", async () => { - const variantFieldMetadata = { - ...mockFieldMetadata, - variant: "variant-uid-123", - }; - - const { findByTestId } = await asyncRender( - - ); - - const variantIndicator = await findByTestId("variant-indicator"); - expect(variantIndicator).toBeInTheDocument(); - }); - - test("does not render VariantIndicator when field has no variant", async () => { - const { container } = await asyncRender( - - ); - - await waitFor(() => { - const variantIndicator = container.querySelector( - "[data-testid='variant-indicator']" - ); - expect(variantIndicator).not.toBeInTheDocument(); - }); - }); - - test("applies variant CSS classes when field has variant", async () => { - const variantFieldMetadata = { - ...mockFieldMetadata, - variant: "variant-uid-123", - }; - - const { findByTestId } = await asyncRender( - - ); - - const fieldLabelWrapper = await findByTestId( - "visual-builder__focused-toolbar__field-label-wrapper" - ); - - await waitFor(() => { - expect(fieldLabelWrapper).toHaveClass( - "visual-builder__focused-toolbar--variant" - ); - }); - }); - - test("does not apply variant CSS classes when field has no variant", async () => { - const { findByTestId } = await asyncRender( - - ); - - const fieldLabelWrapper = await findByTestId( - "visual-builder__focused-toolbar__field-label-wrapper" - ); - - await waitFor(() => { - expect(fieldLabelWrapper).not.toHaveClass( - "visual-builder__focused-toolbar--variant" - ); - }); - - describe("variant linking click condition", () => { - test("should allow modal opening when canLinkVariant is true", () => { - // Test the actual click condition logic without rendering - const canLinkVariant = true; - const shouldOpenModal = !!canLinkVariant; - - expect(shouldOpenModal).toBe(true); - }); - - test("should not allow modal opening when canLinkVariant is false", () => { - // Test the actual click condition logic without rendering - const canLinkVariant = false; - const shouldOpenModal = !!canLinkVariant; - - expect(shouldOpenModal).toBe(false); - }); - - test("should not allow modal opening when canLinkVariant is undefined", () => { - // Test the actual click condition logic without rendering - const canLinkVariant = undefined; - const shouldOpenModal = !!canLinkVariant; - - expect(shouldOpenModal).toBe(false); - }); - }); - }); -}); diff --git a/src/visualBuilder/components/__test__/fieldToolbar.test.tsx b/src/visualBuilder/components/__test__/fieldToolbar.test.tsx index 59e43282..0fc143ff 100644 --- a/src/visualBuilder/components/__test__/fieldToolbar.test.tsx +++ b/src/visualBuilder/components/__test__/fieldToolbar.test.tsx @@ -1,4 +1,12 @@ -import { act, cleanup, fireEvent, render, waitFor, screen, queryByTestId } from "@testing-library/preact"; +import { + act, + cleanup, + fireEvent, + render, + waitFor, + screen, + findByTestId, +} from "@testing-library/preact"; import { CslpData } from "../../../cslp/types/cslp.types"; import { FieldSchemaMap } from "../../utils/fieldSchemaMap"; import { @@ -7,8 +15,10 @@ import { } from "../../utils/instanceHandlers"; import { ISchemaFieldMap } from "../../utils/types/index.types"; import FieldToolbarComponent from "../FieldToolbar"; -import { mockMultipleLinkFieldSchema, mockMultipleFileFieldSchema } from "../../../__test__/data/fields"; -import { asyncRender } from "../../../__test__/utils"; +import { + mockMultipleLinkFieldSchema, + mockMultipleFileFieldSchema, +} from "../../../__test__/data/fields"; import { VisualBuilderCslpEventDetails } from "../../types/visualBuilder.types"; import { isFieldDisabled } from "../../utils/isFieldDisabled"; import React from "preact/compat"; @@ -23,20 +33,52 @@ vi.mock("../../utils/instanceHandlers", () => ({ //CommentIcon testcases are covered seperatly vi.mock("../CommentIcon", () => ({ - default: vi.fn(() =>
Comment Icon
) - })); + default: vi.fn(() =>
Comment Icon
), +})); -vi.mock("../../utils/visualBuilderPostMessage", async () => { +vi.mock("../../utils/visualBuilderPostMessage", () => { return { default: { - send: vi.fn().mockImplementation((_eventName: string) => { + send: vi.fn((eventName: string) => { + // Return mock data for FIELD_LOCATION_DATA to prevent hanging + if (eventName === "field-location-data") { + return Promise.resolve({ apps: [] }); + } + // Return mock data for get-field-variant-status to speed up variant icon test + if (eventName === "get-field-variant-status") { + return Promise.resolve({ + isAddedInstances: false, + isBaseModified: false, + isDeletedInstances: false, + isOrderChanged: false, + fieldLevelCustomizations: false, + }); + } return Promise.resolve({}); }), - on: vi.fn(), + on: vi.fn(() => ({ unregister: vi.fn() })), }, }; }); +vi.mock("../FieldRevert/FieldRevertComponent", async (importOriginal) => { + const actual = + await importOriginal< + typeof import("../FieldRevert/FieldRevertComponent") + >(); + + return { + ...actual, + getFieldVariantStatus: vi.fn().mockResolvedValue({ + isAddedInstances: false, + isBaseModified: false, + isDeletedInstances: false, + isOrderChanged: false, + fieldLevelCustomizations: false, + }), + }; +}); + vi.mock("../../utils/getDiscussionIdByFieldMetaData", () => { return { getDiscussionIdByFieldMetaData: vi.fn().mockResolvedValue({ @@ -71,65 +113,70 @@ const mockMultipleFieldMetadata: CslpData = { describe("FieldToolbarComponent", () => { let targetElement: HTMLDivElement; - const mockEventDetails: VisualBuilderCslpEventDetails = { - fieldMetadata: mockMultipleFieldMetadata, - editableElement: {} as Element, - cslpData: "" - } + let mockEventDetails: VisualBuilderCslpEventDetails; + + beforeAll(() => { + // Mock FieldSchemaMap to resolve immediately (synchronously) + // This ensures the promise resolves in the same tick, making tests faster + vi.spyOn(FieldSchemaMap, "getFieldSchema").mockImplementation(() => + Promise.resolve(mockMultipleLinkFieldSchema) + ); + }); beforeEach(() => { document.getElementsByTagName("html")[0].innerHTML = ""; targetElement = document.createElement("div"); targetElement.setAttribute("data-testid", "mock-target-element"); - mockEventDetails['editableElement'] = targetElement; document.body.appendChild(targetElement); - vi.spyOn(FieldSchemaMap, "getFieldSchema").mockResolvedValue( - mockMultipleLinkFieldSchema + // Create fresh mockEventDetails for each test to avoid state pollution + mockEventDetails = { + fieldMetadata: mockMultipleFieldMetadata, + editableElement: targetElement, + cslpData: "", + }; + + // Reset mocks to default state + vi.mocked(isFieldDisabled).mockReturnValue({ + isDisabled: false, + reason: "", + }); + // Ensure mock resolves immediately + vi.mocked(FieldSchemaMap.getFieldSchema).mockImplementation(() => + Promise.resolve(mockMultipleLinkFieldSchema) ); }); afterEach(() => { - document.body.removeChild(targetElement); - vi.clearAllMocks(); cleanup(); + vi.clearAllMocks(); }); - test("renders toolbar buttons correctly", async () => { - const { findByTestId } = await asyncRender( - - ); - - const moveLeftButton = await findByTestId( - "visual-builder__focused-toolbar__multiple-field-toolbar__move-left-button" - ); - const moveRightButton = await findByTestId( - "visual-builder__focused-toolbar__multiple-field-toolbar__move-right-button" - ); - const deleteButton = await findByTestId( - "visual-builder__focused-toolbar__multiple-field-toolbar__delete-button" - ); - - expect(moveLeftButton).toBeInTheDocument(); - expect(moveRightButton).toBeInTheDocument(); - expect(deleteButton).toBeInTheDocument(); - }); + // REMOVED: "renders toolbar buttons correctly" - redundant test + // This test only checks that buttons exist, which is already covered by the click handler tests below. + // The click tests verify buttons exist AND work correctly, making this test unnecessary. test("calls handleMoveInstance with 'previous' when move left button is clicked", async () => { - const { findByTestId } = await asyncRender( + const { container } = render( ); + // Use act() to ensure React processes all state updates from async operations + await act(async () => { + // Give React a tick to process useEffect and state updates + await new Promise(resolve => setTimeout(resolve, 0)); + }); + + // Use findByTestId which is optimized for async queries const moveLeftButton = await findByTestId( - "visual-builder__focused-toolbar__multiple-field-toolbar__move-left-button" - ); - expect(moveLeftButton).toBeInTheDocument(); + container, + "visual-builder__focused-toolbar__multiple-field-toolbar__move-left-button", + {}, + { timeout: 1000 } + ) as HTMLElement; fireEvent.click(moveLeftButton); @@ -140,17 +187,24 @@ describe("FieldToolbarComponent", () => { }); test("calls handleMoveInstance with 'next' when move right button is clicked", async () => { - const { findByTestId } = await asyncRender( + const { container } = render( ); + // Use act() to ensure React processes all state updates + await act(async () => { + await new Promise(resolve => setTimeout(resolve, 0)); + }); + const moveRightButton = await findByTestId( - "visual-builder__focused-toolbar__multiple-field-toolbar__move-right-button" - ); - expect(moveRightButton).toBeInTheDocument(); + container, + "visual-builder__focused-toolbar__multiple-field-toolbar__move-right-button", + {}, + { timeout: 1000 } + ) as HTMLElement; fireEvent.click(moveRightButton); @@ -161,68 +215,114 @@ describe("FieldToolbarComponent", () => { }); test("calls handleDeleteInstance when delete button is clicked", async () => { - const { findByTestId } = await asyncRender( + const { container } = render( ); - const deleteButton = await findByTestId( - "visual-builder__focused-toolbar__multiple-field-toolbar__delete-button" - ); - expect(deleteButton).toBeInTheDocument(); - await act(() => { - fireEvent.click(deleteButton); + // Use act() to ensure React processes all state updates + await act(async () => { + await new Promise(resolve => setTimeout(resolve, 0)); }); - await waitFor(() => { - expect(handleDeleteInstance).toHaveBeenCalledWith( - mockMultipleFieldMetadata - ); - }) + const deleteButton = await findByTestId( + container, + "visual-builder__focused-toolbar__multiple-field-toolbar__delete-button", + {}, + { timeout: 1000 } + ) as HTMLElement; + + fireEvent.click(deleteButton); + + expect(handleDeleteInstance).toHaveBeenCalledWith( + mockMultipleFieldMetadata + ); }); + test("display variant icon instead of dropdown", async () => { - mockEventDetails.fieldMetadata.variant = "variant"; - const { findByTestId } = await asyncRender( - + // Create a fresh copy with variant set to avoid mutation issues + const variantEventDetails = { + ...mockEventDetails, + fieldMetadata: { + ...mockEventDetails.fieldMetadata, + variant: "variant", + }, + }; + + const { container } = render( + ); - const variantIcon = await findByTestId( - "visual-builder-canvas-variant-icon" + // Use act() to ensure React processes all state updates + await act(async () => { + await new Promise(resolve => setTimeout(resolve, 0)); + }); + + // Use findByTestId which is optimized for async queries + const icon = await findByTestId( + container, + "visual-builder-canvas-variant-icon", + {}, + { timeout: 1000 } ); - expect(variantIcon).toBeInTheDocument(); + expect(icon).toBeInTheDocument(); }); describe("'Replace button' visibility for multiple file fields", () => { beforeEach(() => { - vi.spyOn(FieldSchemaMap, "getFieldSchema").mockResolvedValue( - mockMultipleFileFieldSchema + // Override the mock for this describe block - resolve immediately + vi.mocked(FieldSchemaMap.getFieldSchema).mockImplementation(() => + Promise.resolve(mockMultipleFileFieldSchema) ); }); + afterEach(() => { + // Restore will happen in outer afterEach via clearAllMocks + }); + test("'replace button' is hidden for parent wrapper of multiple file field", async () => { const parentWrapperMetadata: CslpData = { ...mockMultipleFieldMetadata, fieldPathWithIndex: "files", instance: { - fieldPathWithIndex: "files" + fieldPathWithIndex: "files", }, }; const parentWrapperEventDetails = { ...mockEventDetails, - fieldMetadata: parentWrapperMetadata + fieldMetadata: parentWrapperMetadata, }; - const { container } = await asyncRender( + const { container } = render( ); - const replaceButton = container.querySelector('[data-testid="visual-builder-replace-file"]'); + // Use act() to ensure React processes all state updates + await act(async () => { + await new Promise(resolve => setTimeout(resolve, 0)); + }); + + // Wait for toolbar to render first, then check button is not present + const toolbar = await findByTestId( + container, + "visual-builder__focused-toolbar__multiple-field-toolbar", + {}, + { timeout: 1000 } + ); + expect(toolbar).toBeInTheDocument(); + + const replaceButton = container.querySelector( + '[data-testid="visual-builder-replace-file"]' + ); expect(replaceButton).not.toBeInTheDocument(); }); @@ -231,77 +331,97 @@ describe("FieldToolbarComponent", () => { ...mockMultipleFieldMetadata, fieldPathWithIndex: "files", instance: { - fieldPathWithIndex: "files.0" + fieldPathWithIndex: "files.0", }, }; const individualFieldEventDetails = { ...mockEventDetails, - fieldMetadata: individualFieldMetadata + fieldMetadata: individualFieldMetadata, }; - const { container } = await asyncRender( + const { container } = render( ); - const replaceButton = container.querySelector('[data-testid="visual-builder-replace-file"]'); + // Use act() to ensure React processes all state updates + await act(async () => { + await new Promise(resolve => setTimeout(resolve, 0)); + }); + + // Use findByTestId which is optimized for async queries + const replaceButton = await findByTestId( + container, + "visual-builder-replace-file", + {}, + { timeout: 1000 } + ); expect(replaceButton).toBeInTheDocument(); }); - }); - test("passes disabled state correctly to child components when field is disabled", async () => { - // Mock isFieldDisabled to return disabled state - vi.mocked(isFieldDisabled).mockReturnValue({ - isDisabled: true, - reason: "You have only read access to this field" as any, - }); + test("passes disabled state correctly to child components when field is disabled", async () => { + // Mock isFieldDisabled to return disabled state + vi.mocked(isFieldDisabled).mockReturnValue({ + isDisabled: true, + reason: "You have only read access to this field" as any, + }); - const { findByTestId } = await asyncRender( - - ); + const { container } = render( + + ); + + // Use act() to ensure React processes all state updates + await act(async () => { + await new Promise(resolve => setTimeout(resolve, 0)); + }); - await waitFor(async () => { + // Use findByTestId for toolbar, then query for buttons const toolbar = await findByTestId( - "visual-builder__focused-toolbar__multiple-field-toolbar" + container, + "visual-builder__focused-toolbar__multiple-field-toolbar", + {}, + { timeout: 1000 } ); - expect(toolbar).toBeInTheDocument(); - }); - // Check that move buttons are disabled - const moveLeftButton = await findByTestId( - "visual-builder__focused-toolbar__multiple-field-toolbar__move-left-button" - ); - const moveRightButton = await findByTestId( - "visual-builder__focused-toolbar__multiple-field-toolbar__move-right-button" - ); - const deleteButton = await findByTestId( - "visual-builder__focused-toolbar__multiple-field-toolbar__delete-button" - ); + // Check that move buttons are disabled + const moveLeftButton = container.querySelector( + '[data-testid="visual-builder__focused-toolbar__multiple-field-toolbar__move-left-button"]' + ); + const moveRightButton = container.querySelector( + '[data-testid="visual-builder__focused-toolbar__multiple-field-toolbar__move-right-button"]' + ); + const deleteButton = container.querySelector( + '[data-testid="visual-builder__focused-toolbar__multiple-field-toolbar__delete-button"]' + ); - expect(moveLeftButton).toBeDisabled(); - expect(moveRightButton).toBeDisabled(); - expect(deleteButton).toBeDisabled(); - - // Check that edit button is disabled if present - const editButton = await findByTestId( - "visual-builder__focused-toolbar__multiple-field-toolbar__edit-button" - ).catch(() => null); - if (editButton) { - expect(editButton).toBeDisabled(); - } - - // Check that replace button is disabled if present - const replaceButton = document.querySelector( - '[data-testid="visual-builder-replace-file"]' - ); - if (replaceButton) { - expect(replaceButton).toBeDisabled(); - } + expect(moveLeftButton).toBeInTheDocument(); + expect(moveRightButton).toBeInTheDocument(); + expect(deleteButton).toBeInTheDocument(); + expect(moveLeftButton).toBeDisabled(); + expect(moveRightButton).toBeDisabled(); + expect(deleteButton).toBeDisabled(); + + // Check that edit button is disabled if present + const editButton = container.querySelector( + '[data-testid="visual-builder__focused-toolbar__multiple-field-toolbar__edit-button"]' + ); + if (editButton) { + expect(editButton).toBeDisabled(); + } + + // Check that replace button is disabled if present + const replaceButton = container.querySelector( + '[data-testid="visual-builder-replace-file"]' + ); + if (replaceButton) { + expect(replaceButton).toBeDisabled(); + } + }); }); }); diff --git a/src/visualBuilder/components/__test__/fieldlabelwrapper/fieldLabelWrapper.disabledClass.test.tsx b/src/visualBuilder/components/__test__/fieldlabelwrapper/fieldLabelWrapper.disabledClass.test.tsx new file mode 100644 index 00000000..54c1985a --- /dev/null +++ b/src/visualBuilder/components/__test__/fieldlabelwrapper/fieldLabelWrapper.disabledClass.test.tsx @@ -0,0 +1,332 @@ +import { render, waitFor, act, findByTestId } from "@testing-library/preact"; +import FieldLabelWrapperComponent from "../../fieldLabelWrapper"; +import { VisualBuilderCslpEventDetails } from "../../../types/visualBuilder.types"; +import { singleLineFieldSchema } from "../../../../__test__/data/fields"; +import { isFieldDisabled } from "../../../utils/isFieldDisabled"; +import { FieldSchemaMap } from "../../../utils/fieldSchemaMap"; +import visualBuilderPostMessage from "../../../utils/visualBuilderPostMessage"; +import { VisualBuilderPostMessageEvents } from "../../../utils/types/postMessage.types"; +import React from "preact/compat"; +// Import shared mocks and constants +import { + mockStyles, + DISPLAY_NAMES, + PARENT_PATHS, + mockFieldMetadata, + mockEntryPermissionsResponse, +} from "./fieldLabelWrapper.mocks"; + +// Local cache for this test file (can't use imported cache in vi.mock due to hoisting) +const testFieldSchemaCache: Record> = {}; + +// All mocks - inline implementations (can't use imported functions in vi.mock due to hoisting) +vi.mock("../Tooltip", () => ({ + ToolbarTooltip: ({ children, data, disabled }: any) => ( +
+ {children} +
+ ), +})); + +vi.mock("../../../utils/fieldSchemaMap", async (importOriginal) => { + const actual = + await importOriginal(); + // Inline implementation - can't use imported function due to vi.mock hoisting + // Using local cache variable defined above + return { + FieldSchemaMap: { + ...actual.FieldSchemaMap, + getFieldSchema: vi + .fn() + .mockImplementation( + (contentTypeUid: string, fieldPath: string) => { + if (testFieldSchemaCache[contentTypeUid]?.[fieldPath]) { + return Promise.resolve( + testFieldSchemaCache[contentTypeUid][fieldPath] + ); + } + const defaultSchema = { + display_name: "Field 0", + data_type: "text", + field_metadata: { + description: "", + default_value: "", + version: 3, + }, + uid: "test_field", + }; + if (!testFieldSchemaCache[contentTypeUid]) { + testFieldSchemaCache[contentTypeUid] = {}; + } + testFieldSchemaCache[contentTypeUid][fieldPath] = + defaultSchema; + return Promise.resolve(defaultSchema); + } + ), + setFieldSchema: vi + .fn() + .mockImplementation( + ( + contentTypeUid: string, + schemaMap: Record + ) => { + if (!testFieldSchemaCache[contentTypeUid]) { + testFieldSchemaCache[contentTypeUid] = {}; + } + Object.assign( + testFieldSchemaCache[contentTypeUid], + schemaMap + ); + } + ), + hasFieldSchema: vi + .fn() + .mockImplementation( + (contentTypeUid: string, fieldPath: string) => { + return !!testFieldSchemaCache[contentTypeUid]?.[ + fieldPath + ]; + } + ), + clear: vi.fn().mockImplementation(() => { + Object.keys(testFieldSchemaCache).forEach( + (key) => delete testFieldSchemaCache[key] + ); + }), + }, + }; +}); + +vi.mock("../../../utils/visualBuilderPostMessage", () => ({ + default: { + send: vi.fn().mockImplementation((eventName: string, fields: any) => { + // Inline implementation - can't use imported function due to vi.mock hoisting + if ( + eventName === + VisualBuilderPostMessageEvents.GET_FIELD_DISPLAY_NAMES + ) { + const result: Record = {}; + if (Array.isArray(fields)) { + fields.forEach((field: any) => { + const cslpValue = field?.cslpValue || field?.cslp || ""; + if (!cslpValue) return; + if (cslpValue === "mockFieldCslp") { + result[cslpValue] = "Field 0"; + } else if ( + cslpValue === + "contentTypeUid.entryUid.locale.parentPath1" + ) { + result[cslpValue] = "Field 1"; + } else if ( + cslpValue === + "contentTypeUid.entryUid.locale.parentPath2" + ) { + result[cslpValue] = "Field 2"; + } else if ( + cslpValue === + "contentTypeUid.entryUid.locale.parentPath3" + ) { + result[cslpValue] = "Field 3"; + } else { + result[cslpValue] = cslpValue; + } + }); + } + return Promise.resolve(result); + } else if ( + eventName === + VisualBuilderPostMessageEvents.GET_CONTENT_TYPE_NAME || + eventName === "get-content-type-name" + ) { + return Promise.resolve({ + contentTypeName: "Page CT", + }); + } else if ( + eventName === VisualBuilderPostMessageEvents.REFERENCE_MAP || + eventName === "get-reference-map" + ) { + return Promise.resolve({}); + } + return Promise.resolve({}); + }), + }, +})); + +vi.mock("../../../utils/isFieldDisabled", async (importOriginal) => { + const actual = + await importOriginal(); + return { + ...actual, + isFieldDisabled: vi + .fn() + .mockReturnValue({ isDisabled: false, reason: "" }), + }; +}); + +vi.mock("../../../cslp", () => ({ + extractDetailsFromCslp: vi.fn().mockImplementation((path) => { + return { + content_type_uid: "mockContentTypeUid", + fieldPath: path, + cslpValue: path, + }; + }), +})); + +vi.mock("../../../utils/fetchEntryPermissionsAndStageDetails", () => ({ + fetchEntryPermissionsAndStageDetails: vi.fn().mockImplementation(() => { + return Promise.resolve(mockEntryPermissionsResponse); + }), +})); + +vi.mock("../generators/generateCustomCursor", () => ({ + getFieldIcon: vi.fn().mockReturnValue("mock-icon"), + FieldTypeIconsMap: { + reference: "reference-icon", + }, +})); + +vi.mock("../visualBuilder.style", () => ({ + visualBuilderStyles: vi.fn(() => mockStyles), +})); + +vi.mock("../../VariantIndicator", () => ({ + VariantIndicator: () =>
Variant
, +})); + +vi.mock("../../../utils/errorHandling", () => ({ + hasPostMessageError: vi.fn().mockReturnValue(false), +})); + +describe("FieldLabelWrapperComponent - Disabled Class", () => { + beforeEach(() => { + // Reset all mocks to their default state before each test + vi.clearAllMocks(); + + // Reset isFieldDisabled to default + (isFieldDisabled as any).mockReturnValue({ + isDisabled: false, + reason: "", + }); + + // Reset visualBuilderPostMessage mock - inline implementation + vi.mocked(visualBuilderPostMessage!.send).mockImplementation( + (eventName: string, fields: any) => { + if ( + eventName === + VisualBuilderPostMessageEvents.GET_FIELD_DISPLAY_NAMES + ) { + const result: Record = {}; + if (Array.isArray(fields)) { + fields.forEach((field: any) => { + const cslpValue = + field?.cslpValue || field?.cslp || ""; + if (!cslpValue) return; + if (cslpValue === "mockFieldCslp") { + result[cslpValue] = "Field 0"; + } else if ( + cslpValue === + "contentTypeUid.entryUid.locale.parentPath1" + ) { + result[cslpValue] = "Field 1"; + } else if ( + cslpValue === + "contentTypeUid.entryUid.locale.parentPath2" + ) { + result[cslpValue] = "Field 2"; + } else if ( + cslpValue === + "contentTypeUid.entryUid.locale.parentPath3" + ) { + result[cslpValue] = "Field 3"; + } else { + result[cslpValue] = cslpValue; + } + }); + } + return Promise.resolve(result); + } else if ( + eventName === + VisualBuilderPostMessageEvents.GET_CONTENT_TYPE_NAME || + eventName === "get-content-type-name" + ) { + return Promise.resolve({ + contentTypeName: "Page CT", + }); + } else if ( + eventName === + VisualBuilderPostMessageEvents.REFERENCE_MAP || + eventName === "get-reference-map" + ) { + return Promise.resolve({}); + } + return Promise.resolve({}); + } + ); + + // Pre-set field schema in cache to avoid async fetch delay + // This makes FieldSchemaMap.getFieldSchema resolve immediately from cache + FieldSchemaMap.setFieldSchema(mockFieldMetadata.content_type_uid, { + [mockFieldMetadata.fieldPath]: singleLineFieldSchema, + }); + }); + + afterEach(() => { + // Clean up field schema cache after each test + FieldSchemaMap.clear(); + // Clean up DOM after each test to prevent state pollution + document.body.innerHTML = ""; + }); + + afterAll(() => { + vi.clearAllMocks(); + }); + + // mockFieldMetadata is now defined above the describe block + + const mockEventDetails: VisualBuilderCslpEventDetails = { + editableElement: document.createElement("div"), + cslpData: "", + fieldMetadata: mockFieldMetadata, + }; + + const mockGetParentEditable = () => document.createElement("div"); + + test("renders with correct class when field is disabled", async () => { + (isFieldDisabled as any).mockReturnValue({ + isDisabled: true, + reason: "You have only read access to this field", + }); + const { container } = render( + + ); + + // Use act() with queueMicrotask for faster resolution + await act(async () => { + await new Promise((resolve) => + queueMicrotask(() => resolve()) + ); + }); + + // Use findByTestId which is optimized for async queries + const fieldLabel = (await findByTestId( + container as HTMLElement, + "visual-builder__focused-toolbar__field-label-wrapper", + {}, + { timeout: 1000 } + )) as HTMLElement; + expect(fieldLabel).toHaveClass( + "visual-builder__focused-toolbar--field-disabled" + ); + }); +}); diff --git a/src/visualBuilder/components/__test__/fieldlabelwrapper/fieldLabelWrapper.fieldIcon.test.tsx b/src/visualBuilder/components/__test__/fieldlabelwrapper/fieldLabelWrapper.fieldIcon.test.tsx new file mode 100644 index 00000000..4a5a0bef --- /dev/null +++ b/src/visualBuilder/components/__test__/fieldlabelwrapper/fieldLabelWrapper.fieldIcon.test.tsx @@ -0,0 +1,328 @@ +import { render, waitFor, act, findByTestId } from "@testing-library/preact"; +import FieldLabelWrapperComponent from "../../fieldLabelWrapper"; +import { VisualBuilderCslpEventDetails } from "../../../types/visualBuilder.types"; +import { singleLineFieldSchema } from "../../../../__test__/data/fields"; +import { isFieldDisabled } from "../../../utils/isFieldDisabled"; +import { FieldSchemaMap } from "../../../utils/fieldSchemaMap"; +import visualBuilderPostMessage from "../../../utils/visualBuilderPostMessage"; +import { VisualBuilderPostMessageEvents } from "../../../utils/types/postMessage.types"; +import React from "preact/compat"; +// Import shared mocks and constants +import { + mockStyles, + DISPLAY_NAMES, + PARENT_PATHS, + mockFieldMetadata, + mockEntryPermissionsResponse, +} from "./fieldLabelWrapper.mocks"; + +// Local cache for this test file (can't use imported cache in vi.mock due to hoisting) +const testFieldSchemaCache: Record> = {}; + +// All mocks - inline implementations (can't use imported functions in vi.mock due to hoisting) +vi.mock("../Tooltip", () => ({ + ToolbarTooltip: ({ children, data, disabled }: any) => ( +
+ {children} +
+ ), +})); + +vi.mock("../../../utils/fieldSchemaMap", async (importOriginal) => { + const actual = + await importOriginal(); + // Inline implementation - can't use imported function due to vi.mock hoisting + // Using local cache variable defined above + return { + FieldSchemaMap: { + ...actual.FieldSchemaMap, + getFieldSchema: vi + .fn() + .mockImplementation( + (contentTypeUid: string, fieldPath: string) => { + if (testFieldSchemaCache[contentTypeUid]?.[fieldPath]) { + return Promise.resolve( + testFieldSchemaCache[contentTypeUid][fieldPath] + ); + } + const defaultSchema = { + display_name: "Field 0", + data_type: "text", + field_metadata: { + description: "", + default_value: "", + version: 3, + }, + uid: "test_field", + }; + if (!testFieldSchemaCache[contentTypeUid]) { + testFieldSchemaCache[contentTypeUid] = {}; + } + testFieldSchemaCache[contentTypeUid][fieldPath] = + defaultSchema; + return Promise.resolve(defaultSchema); + } + ), + setFieldSchema: vi + .fn() + .mockImplementation( + ( + contentTypeUid: string, + schemaMap: Record + ) => { + if (!testFieldSchemaCache[contentTypeUid]) { + testFieldSchemaCache[contentTypeUid] = {}; + } + Object.assign( + testFieldSchemaCache[contentTypeUid], + schemaMap + ); + } + ), + hasFieldSchema: vi + .fn() + .mockImplementation( + (contentTypeUid: string, fieldPath: string) => { + return !!testFieldSchemaCache[contentTypeUid]?.[ + fieldPath + ]; + } + ), + clear: vi.fn().mockImplementation(() => { + Object.keys(testFieldSchemaCache).forEach( + (key) => delete testFieldSchemaCache[key] + ); + }), + }, + }; +}); + +vi.mock("../../../utils/visualBuilderPostMessage", () => ({ + default: { + send: vi.fn().mockImplementation((eventName: string, fields: any) => { + // Inline implementation - can't use imported function due to vi.mock hoisting + if ( + eventName === + VisualBuilderPostMessageEvents.GET_FIELD_DISPLAY_NAMES + ) { + const result: Record = {}; + if (Array.isArray(fields)) { + fields.forEach((field: any) => { + const cslpValue = field?.cslpValue || field?.cslp || ""; + if (!cslpValue) return; + if (cslpValue === "mockFieldCslp") { + result[cslpValue] = "Field 0"; + } else if ( + cslpValue === + "contentTypeUid.entryUid.locale.parentPath1" + ) { + result[cslpValue] = "Field 1"; + } else if ( + cslpValue === + "contentTypeUid.entryUid.locale.parentPath2" + ) { + result[cslpValue] = "Field 2"; + } else if ( + cslpValue === + "contentTypeUid.entryUid.locale.parentPath3" + ) { + result[cslpValue] = "Field 3"; + } else { + result[cslpValue] = cslpValue; + } + }); + } + return Promise.resolve(result); + } else if ( + eventName === + VisualBuilderPostMessageEvents.GET_CONTENT_TYPE_NAME || + eventName === "get-content-type-name" + ) { + return Promise.resolve({ + contentTypeName: "Page CT", + }); + } else if ( + eventName === VisualBuilderPostMessageEvents.REFERENCE_MAP || + eventName === "get-reference-map" + ) { + return Promise.resolve({}); + } + return Promise.resolve({}); + }), + }, +})); + +vi.mock("../../../utils/isFieldDisabled", async (importOriginal) => { + const actual = + await importOriginal(); + return { + ...actual, + isFieldDisabled: vi + .fn() + .mockReturnValue({ isDisabled: false, reason: "" }), + }; +}); + +vi.mock("../../../cslp", () => ({ + extractDetailsFromCslp: vi.fn().mockImplementation((path) => { + return { + content_type_uid: "mockContentTypeUid", + fieldPath: path, + cslpValue: path, + }; + }), +})); + +vi.mock("../../../utils/fetchEntryPermissionsAndStageDetails", () => ({ + fetchEntryPermissionsAndStageDetails: vi.fn().mockImplementation(() => { + return Promise.resolve(mockEntryPermissionsResponse); + }), +})); + +vi.mock("../generators/generateCustomCursor", () => ({ + getFieldIcon: vi.fn().mockReturnValue("mock-icon"), + FieldTypeIconsMap: { + reference: "reference-icon", + }, +})); + +vi.mock("../visualBuilder.style", () => ({ + visualBuilderStyles: vi.fn(() => mockStyles), +})); + +vi.mock("../../VariantIndicator", () => ({ + VariantIndicator: () =>
Variant
, +})); + +vi.mock("../../../utils/errorHandling", () => ({ + hasPostMessageError: vi.fn().mockReturnValue(false), +})); + +describe("FieldLabelWrapperComponent - Field Icon", () => { + beforeEach(() => { + // Reset all mocks to their default state before each test + vi.clearAllMocks(); + + // Reset isFieldDisabled to default + (isFieldDisabled as any).mockReturnValue({ + isDisabled: false, + reason: "", + }); + + // Reset visualBuilderPostMessage mock - inline implementation + vi.mocked(visualBuilderPostMessage!.send).mockImplementation( + (eventName: string, fields: any) => { + if ( + eventName === + VisualBuilderPostMessageEvents.GET_FIELD_DISPLAY_NAMES + ) { + const result: Record = {}; + if (Array.isArray(fields)) { + fields.forEach((field: any) => { + const cslpValue = + field?.cslpValue || field?.cslp || ""; + if (!cslpValue) return; + if (cslpValue === "mockFieldCslp") { + result[cslpValue] = "Field 0"; + } else if ( + cslpValue === + "contentTypeUid.entryUid.locale.parentPath1" + ) { + result[cslpValue] = "Field 1"; + } else if ( + cslpValue === + "contentTypeUid.entryUid.locale.parentPath2" + ) { + result[cslpValue] = "Field 2"; + } else if ( + cslpValue === + "contentTypeUid.entryUid.locale.parentPath3" + ) { + result[cslpValue] = "Field 3"; + } else { + result[cslpValue] = cslpValue; + } + }); + } + return Promise.resolve(result); + } else if ( + eventName === + VisualBuilderPostMessageEvents.GET_CONTENT_TYPE_NAME || + eventName === "get-content-type-name" + ) { + return Promise.resolve({ + contentTypeName: "Page CT", + }); + } else if ( + eventName === + VisualBuilderPostMessageEvents.REFERENCE_MAP || + eventName === "get-reference-map" + ) { + return Promise.resolve({}); + } + return Promise.resolve({}); + } + ); + + // Pre-set field schema in cache to avoid async fetch delay + // This makes FieldSchemaMap.getFieldSchema resolve immediately from cache + FieldSchemaMap.setFieldSchema(mockFieldMetadata.content_type_uid, { + [mockFieldMetadata.fieldPath]: singleLineFieldSchema, + }); + }); + + afterEach(() => { + // Clean up field schema cache after each test + FieldSchemaMap.clear(); + // Clean up DOM after each test to prevent state pollution + document.body.innerHTML = ""; + }); + + afterAll(() => { + vi.clearAllMocks(); + }); + + // mockFieldMetadata is now defined above the describe block + + const mockEventDetails: VisualBuilderCslpEventDetails = { + editableElement: document.createElement("div"), + cslpData: "", + fieldMetadata: mockFieldMetadata, + }; + + const mockGetParentEditable = () => document.createElement("div"); + + test("displays current field icon", async () => { + // Wrap render in act to batch all updates and reduce reconciliation cycles + let container!: HTMLElement; + await act(async () => { + const result = render( + + ); + container = result.container as HTMLElement; + // Use queueMicrotask for faster resolution than setTimeout + await new Promise((resolve) => + queueMicrotask(() => resolve()) + ); + }); + + // Use findByTestId which is optimized for async queries + const icon = await findByTestId( + container, + "visual-builder__field-icon", + {}, + { timeout: 1000 } + ); + expect(icon).toBeInTheDocument(); + }); +}); diff --git a/src/visualBuilder/components/__test__/fieldlabelwrapper/fieldLabelWrapper.isFieldDisabled.test.tsx b/src/visualBuilder/components/__test__/fieldlabelwrapper/fieldLabelWrapper.isFieldDisabled.test.tsx new file mode 100644 index 00000000..44cc4cd9 --- /dev/null +++ b/src/visualBuilder/components/__test__/fieldlabelwrapper/fieldLabelWrapper.isFieldDisabled.test.tsx @@ -0,0 +1,335 @@ +import { render, act, findByTestId } from "@testing-library/preact"; +import FieldLabelWrapperComponent from "../../fieldLabelWrapper"; +import { VisualBuilderCslpEventDetails } from "../../../types/visualBuilder.types"; +import { singleLineFieldSchema } from "../../../../__test__/data/fields"; +import { isFieldDisabled } from "../../../utils/isFieldDisabled"; +import { FieldSchemaMap } from "../../../utils/fieldSchemaMap"; +import visualBuilderPostMessage from "../../../utils/visualBuilderPostMessage"; +import { VisualBuilderPostMessageEvents } from "../../../utils/types/postMessage.types"; +import React from "preact/compat"; +// Import shared mocks and constants +import { + mockStyles, + mockFieldMetadata, + mockEntryPermissionsResponse, +} from "./fieldLabelWrapper.mocks"; + +// Local cache for this test file (can't use imported cache in vi.mock due to hoisting) +const testFieldSchemaCache: Record> = {}; + +// All mocks - inline implementations (can't use imported functions in vi.mock due to hoisting) +vi.mock("../Tooltip", () => ({ + ToolbarTooltip: ({ children, data, disabled }: any) => ( +
+ {children} +
+ ), +})); + +vi.mock("../../../utils/fieldSchemaMap", async (importOriginal) => { + const actual = + await importOriginal(); + // Inline implementation - can't use imported function due to vi.mock hoisting + return { + FieldSchemaMap: { + ...actual.FieldSchemaMap, + getFieldSchema: vi + .fn() + .mockImplementation( + (contentTypeUid: string, fieldPath: string) => { + if (testFieldSchemaCache[contentTypeUid]?.[fieldPath]) { + return Promise.resolve( + testFieldSchemaCache[contentTypeUid][fieldPath] + ); + } + const defaultSchema = { + display_name: "Field 0", + data_type: "text", + field_metadata: { + description: "", + default_value: "", + version: 3, + }, + uid: "test_field", + }; + if (!testFieldSchemaCache[contentTypeUid]) { + testFieldSchemaCache[contentTypeUid] = {}; + } + testFieldSchemaCache[contentTypeUid][fieldPath] = + defaultSchema; + return Promise.resolve(defaultSchema); + } + ), + setFieldSchema: vi + .fn() + .mockImplementation( + ( + contentTypeUid: string, + schemaMap: Record + ) => { + if (!testFieldSchemaCache[contentTypeUid]) { + testFieldSchemaCache[contentTypeUid] = {}; + } + Object.assign( + testFieldSchemaCache[contentTypeUid], + schemaMap + ); + } + ), + hasFieldSchema: vi + .fn() + .mockImplementation( + (contentTypeUid: string, fieldPath: string) => { + return !!testFieldSchemaCache[contentTypeUid]?.[ + fieldPath + ]; + } + ), + clear: vi.fn().mockImplementation(() => { + Object.keys(testFieldSchemaCache).forEach( + (key) => delete testFieldSchemaCache[key] + ); + }), + }, + }; +}); + +vi.mock("../../../utils/visualBuilderPostMessage", () => ({ + default: { + send: vi.fn().mockImplementation((eventName: string, fields: any) => { + // Inline implementation - can't use imported function due to vi.mock hoisting + if ( + eventName === + VisualBuilderPostMessageEvents.GET_FIELD_DISPLAY_NAMES + ) { + const result: Record = {}; + if (Array.isArray(fields)) { + fields.forEach((field: any) => { + const cslpValue = field?.cslpValue || field?.cslp || ""; + if (!cslpValue) return; + if (cslpValue === "mockFieldCslp") { + result[cslpValue] = "Field 0"; + } else if ( + cslpValue === + "contentTypeUid.entryUid.locale.parentPath1" + ) { + result[cslpValue] = "Field 1"; + } else if ( + cslpValue === + "contentTypeUid.entryUid.locale.parentPath2" + ) { + result[cslpValue] = "Field 2"; + } else if ( + cslpValue === + "contentTypeUid.entryUid.locale.parentPath3" + ) { + result[cslpValue] = "Field 3"; + } else { + result[cslpValue] = cslpValue; + } + }); + } + return Promise.resolve(result); + } else if ( + eventName === + VisualBuilderPostMessageEvents.GET_CONTENT_TYPE_NAME || + eventName === "get-content-type-name" + ) { + return Promise.resolve({ + contentTypeName: "Page CT", + }); + } else if ( + eventName === VisualBuilderPostMessageEvents.REFERENCE_MAP || + eventName === "get-reference-map" + ) { + return Promise.resolve({}); + } + return Promise.resolve({}); + }), + }, +})); + +vi.mock("../../../utils/isFieldDisabled", async (importOriginal) => { + const actual = + await importOriginal(); + return { + ...actual, + isFieldDisabled: vi + .fn() + .mockReturnValue({ isDisabled: false, reason: "" }), + }; +}); + +vi.mock("../../../cslp", () => ({ + extractDetailsFromCslp: vi.fn().mockImplementation((path) => { + return { + content_type_uid: "mockContentTypeUid", + fieldPath: path, + cslpValue: path, + }; + }), +})); + +vi.mock("../../../utils/fetchEntryPermissionsAndStageDetails", () => ({ + fetchEntryPermissionsAndStageDetails: vi.fn().mockImplementation(() => { + return Promise.resolve(mockEntryPermissionsResponse); + }), +})); + +vi.mock("../generators/generateCustomCursor", () => ({ + getFieldIcon: vi.fn().mockReturnValue("mock-icon"), + FieldTypeIconsMap: { + reference: "reference-icon", + }, +})); + +vi.mock("../visualBuilder.style", () => ({ + visualBuilderStyles: vi.fn(() => mockStyles), +})); + +vi.mock("../../VariantIndicator", () => ({ + VariantIndicator: () =>
Variant
, +})); + +vi.mock("../../../utils/errorHandling", () => ({ + hasPostMessageError: vi.fn().mockReturnValue(false), +})); + +describe("FieldLabelWrapperComponent - isFieldDisabled Tests", () => { + beforeEach(() => { + vi.clearAllMocks(); + (isFieldDisabled as any).mockReturnValue({ + isDisabled: false, + reason: "", + }); + // Reset visualBuilderPostMessage mock - inline implementation + vi.mocked(visualBuilderPostMessage!.send).mockImplementation( + (eventName: string, fields: any) => { + if ( + eventName === + VisualBuilderPostMessageEvents.GET_FIELD_DISPLAY_NAMES + ) { + const result: Record = {}; + if (Array.isArray(fields)) { + fields.forEach((field: any) => { + const cslpValue = + field?.cslpValue || field?.cslp || ""; + if (!cslpValue) return; + if (cslpValue === "mockFieldCslp") { + result[cslpValue] = "Field 0"; + } else if ( + cslpValue === + "contentTypeUid.entryUid.locale.parentPath1" + ) { + result[cslpValue] = "Field 1"; + } else if ( + cslpValue === + "contentTypeUid.entryUid.locale.parentPath2" + ) { + result[cslpValue] = "Field 2"; + } else if ( + cslpValue === + "contentTypeUid.entryUid.locale.parentPath3" + ) { + result[cslpValue] = "Field 3"; + } else { + result[cslpValue] = cslpValue; + } + }); + } + return Promise.resolve(result); + } else if ( + eventName === + VisualBuilderPostMessageEvents.GET_CONTENT_TYPE_NAME || + eventName === "get-content-type-name" + ) { + return Promise.resolve({ + contentTypeName: "Page CT", + }); + } else if ( + eventName === + VisualBuilderPostMessageEvents.REFERENCE_MAP || + eventName === "get-reference-map" + ) { + return Promise.resolve({}); + } + return Promise.resolve({}); + } + ); + FieldSchemaMap.setFieldSchema(mockFieldMetadata.content_type_uid, { + [mockFieldMetadata.fieldPath]: singleLineFieldSchema, + }); + }); + + afterEach(() => { + FieldSchemaMap.clear(); + document.body.innerHTML = ""; + }); + + afterAll(() => { + vi.clearAllMocks(); + }); + + const mockEventDetails: VisualBuilderCslpEventDetails = { + editableElement: document.createElement("div"), + cslpData: "", + fieldMetadata: mockFieldMetadata, + }; + + const mockGetParentEditable = () => document.createElement("div"); + + test("calls isFieldDisabled with correct arguments", async () => { + const { container } = render( + + ); + + await act(async () => { + await new Promise((resolve) => + queueMicrotask(() => resolve()) + ); + }); + + await findByTestId( + container as HTMLElement, + "visual-builder__focused-toolbar__field-label-wrapper", + {}, + { timeout: 1000 } + ); + expect(isFieldDisabled).toHaveBeenCalled(); + + expect(isFieldDisabled).toHaveBeenCalledWith( + singleLineFieldSchema, + mockEventDetails, + { + update: true, + }, + { + update: { + create: true, + read: true, + update: true, + delete: true, + publish: true, + }, + }, + { + stage: undefined, + permissions: { + entry: { + update: true, + }, + }, + } + ); + }); +}); diff --git a/src/visualBuilder/components/__test__/fieldlabelwrapper/fieldLabelWrapper.loading.test.tsx b/src/visualBuilder/components/__test__/fieldlabelwrapper/fieldLabelWrapper.loading.test.tsx new file mode 100644 index 00000000..716e731b --- /dev/null +++ b/src/visualBuilder/components/__test__/fieldlabelwrapper/fieldLabelWrapper.loading.test.tsx @@ -0,0 +1,346 @@ +import { render, waitFor, act, findByTestId } from "@testing-library/preact"; +import FieldLabelWrapperComponent from "../../fieldLabelWrapper"; +import { VisualBuilderCslpEventDetails } from "../../../types/visualBuilder.types"; +import { singleLineFieldSchema } from "../../../../__test__/data/fields"; +import { isFieldDisabled } from "../../../utils/isFieldDisabled"; +import { FieldSchemaMap } from "../../../utils/fieldSchemaMap"; +import visualBuilderPostMessage from "../../../utils/visualBuilderPostMessage"; +import { VisualBuilderPostMessageEvents } from "../../../utils/types/postMessage.types"; +import React from "preact/compat"; +// Import shared mocks and constants +import { + mockStyles, + DISPLAY_NAMES, + PARENT_PATHS, + mockFieldMetadata, + mockEntryPermissionsResponse, +} from "./fieldLabelWrapper.mocks"; + +// Local cache for this test file (can't use imported cache in vi.mock due to hoisting) +const testFieldSchemaCache: Record> = {}; + +// All mocks - inline implementations (can't use imported functions in vi.mock due to hoisting) +vi.mock("../Tooltip", () => ({ + ToolbarTooltip: ({ children, data, disabled }: any) => ( +
+ {children} +
+ ), +})); + +vi.mock("../../../utils/fieldSchemaMap", async (importOriginal) => { + const actual = + await importOriginal(); + // Inline implementation - can't use imported function due to vi.mock hoisting + // Using local cache variable defined above + return { + FieldSchemaMap: { + ...actual.FieldSchemaMap, + getFieldSchema: vi + .fn() + .mockImplementation( + (contentTypeUid: string, fieldPath: string) => { + if (testFieldSchemaCache[contentTypeUid]?.[fieldPath]) { + return Promise.resolve( + testFieldSchemaCache[contentTypeUid][fieldPath] + ); + } + const defaultSchema = { + display_name: "Field 0", + data_type: "text", + field_metadata: { + description: "", + default_value: "", + version: 3, + }, + uid: "test_field", + }; + if (!testFieldSchemaCache[contentTypeUid]) { + testFieldSchemaCache[contentTypeUid] = {}; + } + testFieldSchemaCache[contentTypeUid][fieldPath] = + defaultSchema; + return Promise.resolve(defaultSchema); + } + ), + setFieldSchema: vi + .fn() + .mockImplementation( + ( + contentTypeUid: string, + schemaMap: Record + ) => { + if (!testFieldSchemaCache[contentTypeUid]) { + testFieldSchemaCache[contentTypeUid] = {}; + } + Object.assign( + testFieldSchemaCache[contentTypeUid], + schemaMap + ); + } + ), + hasFieldSchema: vi + .fn() + .mockImplementation( + (contentTypeUid: string, fieldPath: string) => { + return !!testFieldSchemaCache[contentTypeUid]?.[ + fieldPath + ]; + } + ), + clear: vi.fn().mockImplementation(() => { + Object.keys(testFieldSchemaCache).forEach( + (key) => delete testFieldSchemaCache[key] + ); + }), + }, + }; +}); + +vi.mock("../../../utils/visualBuilderPostMessage", () => ({ + default: { + send: vi.fn().mockImplementation((eventName: string, fields: any) => { + // Inline implementation - can't use imported function due to vi.mock hoisting + if ( + eventName === + VisualBuilderPostMessageEvents.GET_FIELD_DISPLAY_NAMES + ) { + const result: Record = {}; + if (Array.isArray(fields)) { + fields.forEach((field: any) => { + const cslpValue = field?.cslpValue || field?.cslp || ""; + if (!cslpValue) return; + if (cslpValue === "mockFieldCslp") { + result[cslpValue] = "Field 0"; + } else if ( + cslpValue === + "contentTypeUid.entryUid.locale.parentPath1" + ) { + result[cslpValue] = "Field 1"; + } else if ( + cslpValue === + "contentTypeUid.entryUid.locale.parentPath2" + ) { + result[cslpValue] = "Field 2"; + } else if ( + cslpValue === + "contentTypeUid.entryUid.locale.parentPath3" + ) { + result[cslpValue] = "Field 3"; + } else { + result[cslpValue] = cslpValue; + } + }); + } + return Promise.resolve(result); + } else if ( + eventName === + VisualBuilderPostMessageEvents.GET_CONTENT_TYPE_NAME || + eventName === "get-content-type-name" + ) { + return Promise.resolve({ + contentTypeName: "Page CT", + }); + } else if ( + eventName === VisualBuilderPostMessageEvents.REFERENCE_MAP || + eventName === "get-reference-map" + ) { + return Promise.resolve({}); + } + return Promise.resolve({}); + }), + }, +})); + +vi.mock("../../../utils/isFieldDisabled", async (importOriginal) => { + const actual = + await importOriginal(); + return { + ...actual, + isFieldDisabled: vi + .fn() + .mockReturnValue({ isDisabled: false, reason: "" }), + }; +}); + +vi.mock("../../../cslp", () => ({ + extractDetailsFromCslp: vi.fn().mockImplementation((path) => { + return { + content_type_uid: "mockContentTypeUid", + fieldPath: path, + cslpValue: path, + }; + }), +})); + +vi.mock("../../../utils/fetchEntryPermissionsAndStageDetails", () => ({ + fetchEntryPermissionsAndStageDetails: vi.fn().mockImplementation(() => { + return Promise.resolve(mockEntryPermissionsResponse); + }), +})); + +vi.mock("../generators/generateCustomCursor", () => ({ + getFieldIcon: vi.fn().mockReturnValue("mock-icon"), + FieldTypeIconsMap: { + reference: "reference-icon", + }, +})); + +vi.mock("../visualBuilder.style", () => ({ + visualBuilderStyles: vi.fn(() => mockStyles), +})); + +vi.mock("../../VariantIndicator", () => ({ + VariantIndicator: () =>
Variant
, +})); + +vi.mock("../../../utils/errorHandling", () => ({ + hasPostMessageError: vi.fn().mockReturnValue(false), +})); + +describe("FieldLabelWrapperComponent - Loading State", () => { + beforeEach(() => { + // Reset all mocks to their default state before each test + vi.clearAllMocks(); + + // Reset isFieldDisabled to default + (isFieldDisabled as any).mockReturnValue({ + isDisabled: false, + reason: "", + }); + + // Reset visualBuilderPostMessage mock - inline implementation + vi.mocked(visualBuilderPostMessage!.send).mockImplementation( + (eventName: string, fields: any) => { + if ( + eventName === + VisualBuilderPostMessageEvents.GET_FIELD_DISPLAY_NAMES + ) { + const result: Record = {}; + if (Array.isArray(fields)) { + fields.forEach((field: any) => { + const cslpValue = + field?.cslpValue || field?.cslp || ""; + if (!cslpValue) return; + if (cslpValue === "mockFieldCslp") { + result[cslpValue] = "Field 0"; + } else if ( + cslpValue === + "contentTypeUid.entryUid.locale.parentPath1" + ) { + result[cslpValue] = "Field 1"; + } else if ( + cslpValue === + "contentTypeUid.entryUid.locale.parentPath2" + ) { + result[cslpValue] = "Field 2"; + } else if ( + cslpValue === + "contentTypeUid.entryUid.locale.parentPath3" + ) { + result[cslpValue] = "Field 3"; + } else { + result[cslpValue] = cslpValue; + } + }); + } + return Promise.resolve(result); + } else if ( + eventName === + VisualBuilderPostMessageEvents.GET_CONTENT_TYPE_NAME || + eventName === "get-content-type-name" + ) { + return Promise.resolve({ + contentTypeName: "Page CT", + }); + } else if ( + eventName === + VisualBuilderPostMessageEvents.REFERENCE_MAP || + eventName === "get-reference-map" + ) { + return Promise.resolve({}); + } + return Promise.resolve({}); + } + ); + + // Pre-set field schema in cache to avoid async fetch delay + // This makes FieldSchemaMap.getFieldSchema resolve immediately from cache + FieldSchemaMap.setFieldSchema(mockFieldMetadata.content_type_uid, { + [mockFieldMetadata.fieldPath]: singleLineFieldSchema, + }); + }); + + afterEach(() => { + // Clean up field schema cache after each test + FieldSchemaMap.clear(); + // Clean up DOM after each test to prevent state pollution + document.body.innerHTML = ""; + }); + + afterAll(() => { + vi.clearAllMocks(); + }); + + // mockFieldMetadata is now defined above the describe block + + const mockEventDetails: VisualBuilderCslpEventDetails = { + editableElement: document.createElement("div"), + cslpData: "", + fieldMetadata: mockFieldMetadata, + }; + + const mockGetParentEditable = () => document.createElement("div"); + + test("does not render ContentTypeIcon when loading", async () => { + // Mock the display names to never resolve to simulate loading state + vi.mocked(visualBuilderPostMessage!.send).mockImplementation( + (eventName: string) => { + // Only block GET_FIELD_DISPLAY_NAMES, let other calls resolve + if ( + eventName === + VisualBuilderPostMessageEvents.GET_FIELD_DISPLAY_NAMES + ) { + return new Promise(() => {}); // Never resolves + } + // Let other calls use default mock behavior + return Promise.resolve({}); + } + ); + + const { container } = render( + + ); + + // Use act() with queueMicrotask for faster resolution + await act(async () => { + await new Promise((resolve) => + queueMicrotask(() => resolve()) + ); + }); + + // When loading, component returns LoadingIcon, not the main structure + // ContentTypeIcon only renders when dataLoading is false, which won't happen here + // So we should see LoadingIcon and NOT see ContentTypeIcon + await waitFor( + () => { + // Component should be in loading state (LoadingIcon visible, ContentTypeIcon not) + const contentTypeIcon = container.querySelector( + ".visual-builder__content-type-icon" + ); + expect(contentTypeIcon).not.toBeInTheDocument(); + }, + { timeout: 1000, interval: 10 } // Reduced timeout - mocks resolve immediately + ); + }); +}); diff --git a/src/visualBuilder/components/__test__/fieldlabelwrapper/fieldLabelWrapper.mocks.ts b/src/visualBuilder/components/__test__/fieldlabelwrapper/fieldLabelWrapper.mocks.ts new file mode 100644 index 00000000..a59699ab --- /dev/null +++ b/src/visualBuilder/components/__test__/fieldlabelwrapper/fieldLabelWrapper.mocks.ts @@ -0,0 +1,245 @@ +/** + * Shared mocks and constants for fieldLabelWrapper component tests + * + * Note: vi.mock() calls must remain in each test file (they are hoisted), + * but the mock implementations and constants are shared here to avoid duplication. + */ + +import { vi } from "vitest"; +import { VisualBuilderPostMessageEvents } from "../../../utils/types/postMessage.types"; +import { CslpData } from "../../../../cslp/types/cslp.types"; +import React from "preact/compat"; + +// Shared field schema cache for tests +export const testFieldSchemaCache: Record> = {}; + +// Mock styles object +export const mockStyles = { + "visual-builder__focused-toolbar--variant": + "visual-builder__focused-toolbar--variant", + "visual-builder__tooltip--persistent": + "visual-builder__tooltip--persistent", + "visual-builder__custom-tooltip": "visual-builder__custom-tooltip", + "visual-builder__focused-toolbar__field-label-wrapper": + "visual-builder__focused-toolbar__field-label-wrapper", + "visual-builder__focused-toolbar--field-disabled": + "visual-builder__focused-toolbar--field-disabled", + "visual-builder__focused-toolbar__text": + "visual-builder__focused-toolbar__text", + "field-label-dropdown-open": "field-label-dropdown-open", + "visual-builder__button": "visual-builder__button", + "visual-builder__button-loader": "visual-builder__button-loader", + "visual-builder__reference-icon-container": + "visual-builder__reference-icon-container", + "visual-builder__content-type-icon": "visual-builder__content-type-icon", +}; + +// Display names constants +export const DISPLAY_NAMES = { + mockFieldCslp: "Field 0", + parentPath1: "Field 1", + parentPath2: "Field 2", + parentPath3: "Field 3", +}; + +// Path constants +export const pathPrefix = "contentTypeUid.entryUid.locale"; +export const PARENT_PATHS = [ + `${pathPrefix}.parentPath1`, + `${pathPrefix}.parentPath2`, + `${pathPrefix}.parentPath3`, +]; + +// Default mock field metadata +export const mockFieldMetadata: CslpData = { + entry_uid: "mockEntryUid", + content_type_uid: "mockContentTypeUid", + cslpValue: "mockFieldCslp", + locale: "", + variant: undefined, + fieldPath: "mockFieldPath", + fieldPathWithIndex: "", + multipleFieldMetadata: { + index: 0, + parentDetails: { + parentPath: "", + parentCslpValue: "", + }, + }, + instance: { + fieldPathWithIndex: "", + }, +}; + +/** + * Creates a mock implementation for FieldSchemaMap + */ +export function createFieldSchemaMapMock(actual: any) { + return { + FieldSchemaMap: { + ...actual.FieldSchemaMap, + getFieldSchema: vi + .fn() + .mockImplementation( + (contentTypeUid: string, fieldPath: string) => { + if (testFieldSchemaCache[contentTypeUid]?.[fieldPath]) { + const cachedValue = + testFieldSchemaCache[contentTypeUid][fieldPath]; + return Promise.resolve(cachedValue); + } + const defaultSchema = { + display_name: "Field 0", + data_type: "text", + field_metadata: { + description: "", + default_value: "", + version: 3, + }, + uid: "test_field", + }; + if (!testFieldSchemaCache[contentTypeUid]) { + testFieldSchemaCache[contentTypeUid] = {}; + } + testFieldSchemaCache[contentTypeUid][fieldPath] = + defaultSchema; + return Promise.resolve(defaultSchema); + } + ), + setFieldSchema: vi + .fn() + .mockImplementation( + ( + contentTypeUid: string, + schemaMap: Record + ) => { + if (!testFieldSchemaCache[contentTypeUid]) { + testFieldSchemaCache[contentTypeUid] = {}; + } + Object.assign( + testFieldSchemaCache[contentTypeUid], + schemaMap + ); + } + ), + hasFieldSchema: vi + .fn() + .mockImplementation( + (contentTypeUid: string, fieldPath: string) => { + return !!testFieldSchemaCache[contentTypeUid]?.[ + fieldPath + ]; + } + ), + clear: vi.fn().mockImplementation(() => { + Object.keys(testFieldSchemaCache).forEach( + (key) => delete testFieldSchemaCache[key] + ); + }), + }, + }; +} + +/** + * Creates a mock implementation for visualBuilderPostMessage.send + */ +export function createVisualBuilderPostMessageMock() { + return vi.fn().mockImplementation((eventName: string, fields: any) => { + if ( + eventName === VisualBuilderPostMessageEvents.GET_FIELD_DISPLAY_NAMES + ) { + const result: Record = {}; + if (Array.isArray(fields)) { + fields.forEach((field: any) => { + const cslpValue = field?.cslpValue || field?.cslp || ""; + if (!cslpValue) return; + if (cslpValue === "mockFieldCslp") { + result[cslpValue] = "Field 0"; + } else if ( + cslpValue === + "contentTypeUid.entryUid.locale.parentPath1" + ) { + result[cslpValue] = "Field 1"; + } else if ( + cslpValue === + "contentTypeUid.entryUid.locale.parentPath2" + ) { + result[cslpValue] = "Field 2"; + } else if ( + cslpValue === + "contentTypeUid.entryUid.locale.parentPath3" + ) { + result[cslpValue] = "Field 3"; + } else { + result[cslpValue] = cslpValue; + } + }); + } + return Promise.resolve(result); + } else if ( + eventName === + VisualBuilderPostMessageEvents.GET_CONTENT_TYPE_NAME || + eventName === "get-content-type-name" + ) { + return Promise.resolve({ + contentTypeName: "Page CT", + }); + } else if ( + eventName === VisualBuilderPostMessageEvents.REFERENCE_MAP || + eventName === "get-reference-map" + ) { + return Promise.resolve({}); + } + return Promise.resolve({}); + }); +} + +/** + * Mock implementations for use in vi.mock() calls + * Note: vi.mock() calls must be at the top level of each test file, + * but these implementations can be reused + */ + +// Tooltip mock component +export const mockToolbarTooltip = ({ children, data, disabled }: any) => + React.createElement( + "div", + { + "data-testid": "toolbar-tooltip", + "data-disabled": disabled, + "data-content-type-name": data.contentTypeName, + "data-reference-field-name": data.referenceFieldName, + }, + children + ); + +// VariantIndicator mock component +export const mockVariantIndicator = () => + React.createElement( + "div", + { "data-testid": "variant-indicator" }, + "Variant" + ); + +// Default entry permissions response +export const mockEntryPermissionsResponse = { + acl: { + update: { + create: true, + read: true, + update: true, + delete: true, + publish: true, + }, + }, + workflowStage: { + stage: undefined, + permissions: { + entry: { + update: true, + }, + }, + }, + resolvedVariantPermissions: { + update: true, + }, +}; diff --git a/src/visualBuilder/components/__test__/fieldlabelwrapper/fieldLabelWrapper.renderFields.test.tsx b/src/visualBuilder/components/__test__/fieldlabelwrapper/fieldLabelWrapper.renderFields.test.tsx new file mode 100644 index 00000000..2867ebf1 --- /dev/null +++ b/src/visualBuilder/components/__test__/fieldlabelwrapper/fieldLabelWrapper.renderFields.test.tsx @@ -0,0 +1,331 @@ +import { render, waitFor, act, findByTestId } from "@testing-library/preact"; +import FieldLabelWrapperComponent from "../../fieldLabelWrapper"; +import { VisualBuilderCslpEventDetails } from "../../../types/visualBuilder.types"; +import { singleLineFieldSchema } from "../../../../__test__/data/fields"; +import { isFieldDisabled } from "../../../utils/isFieldDisabled"; +import { FieldSchemaMap } from "../../../utils/fieldSchemaMap"; +import visualBuilderPostMessage from "../../../utils/visualBuilderPostMessage"; +import { VisualBuilderPostMessageEvents } from "../../../utils/types/postMessage.types"; +import React from "preact/compat"; +// Import shared mocks and constants +import { + mockStyles, + DISPLAY_NAMES, + PARENT_PATHS, + mockFieldMetadata, + mockEntryPermissionsResponse, +} from "./fieldLabelWrapper.mocks"; + +// Local cache for this test file (can't use imported cache in vi.mock due to hoisting) +const testFieldSchemaCache: Record> = {}; + +// All mocks - inline implementations (can't use imported functions in vi.mock due to hoisting) +vi.mock("../Tooltip", () => ({ + ToolbarTooltip: ({ children, data, disabled }: any) => ( +
+ {children} +
+ ), +})); + +vi.mock("../../../utils/fieldSchemaMap", async (importOriginal) => { + const actual = + await importOriginal(); + // Inline implementation - can't use imported function due to vi.mock hoisting + // Using local cache variable defined above + return { + FieldSchemaMap: { + ...actual.FieldSchemaMap, + getFieldSchema: vi + .fn() + .mockImplementation( + (contentTypeUid: string, fieldPath: string) => { + if (testFieldSchemaCache[contentTypeUid]?.[fieldPath]) { + return Promise.resolve( + testFieldSchemaCache[contentTypeUid][fieldPath] + ); + } + const defaultSchema = { + display_name: "Field 0", + data_type: "text", + field_metadata: { + description: "", + default_value: "", + version: 3, + }, + uid: "test_field", + }; + if (!testFieldSchemaCache[contentTypeUid]) { + testFieldSchemaCache[contentTypeUid] = {}; + } + testFieldSchemaCache[contentTypeUid][fieldPath] = + defaultSchema; + return Promise.resolve(defaultSchema); + } + ), + setFieldSchema: vi + .fn() + .mockImplementation( + ( + contentTypeUid: string, + schemaMap: Record + ) => { + if (!testFieldSchemaCache[contentTypeUid]) { + testFieldSchemaCache[contentTypeUid] = {}; + } + Object.assign( + testFieldSchemaCache[contentTypeUid], + schemaMap + ); + } + ), + hasFieldSchema: vi + .fn() + .mockImplementation( + (contentTypeUid: string, fieldPath: string) => { + return !!testFieldSchemaCache[contentTypeUid]?.[ + fieldPath + ]; + } + ), + clear: vi.fn().mockImplementation(() => { + Object.keys(testFieldSchemaCache).forEach( + (key) => delete testFieldSchemaCache[key] + ); + }), + }, + }; +}); + +vi.mock("../../../utils/visualBuilderPostMessage", () => ({ + default: { + send: vi.fn().mockImplementation((eventName: string, fields: any) => { + // Inline implementation - can't use imported function due to vi.mock hoisting + if ( + eventName === + VisualBuilderPostMessageEvents.GET_FIELD_DISPLAY_NAMES + ) { + const result: Record = {}; + if (Array.isArray(fields)) { + fields.forEach((field: any) => { + const cslpValue = field?.cslpValue || field?.cslp || ""; + if (!cslpValue) return; + if (cslpValue === "mockFieldCslp") { + result[cslpValue] = "Field 0"; + } else if ( + cslpValue === + "contentTypeUid.entryUid.locale.parentPath1" + ) { + result[cslpValue] = "Field 1"; + } else if ( + cslpValue === + "contentTypeUid.entryUid.locale.parentPath2" + ) { + result[cslpValue] = "Field 2"; + } else if ( + cslpValue === + "contentTypeUid.entryUid.locale.parentPath3" + ) { + result[cslpValue] = "Field 3"; + } else { + result[cslpValue] = cslpValue; + } + }); + } + return Promise.resolve(result); + } else if ( + eventName === + VisualBuilderPostMessageEvents.GET_CONTENT_TYPE_NAME || + eventName === "get-content-type-name" + ) { + return Promise.resolve({ + contentTypeName: "Page CT", + }); + } else if ( + eventName === VisualBuilderPostMessageEvents.REFERENCE_MAP || + eventName === "get-reference-map" + ) { + return Promise.resolve({}); + } + return Promise.resolve({}); + }), + }, +})); + +vi.mock("../../../utils/isFieldDisabled", async (importOriginal) => { + const actual = + await importOriginal(); + return { + ...actual, + isFieldDisabled: vi + .fn() + .mockReturnValue({ isDisabled: false, reason: "" }), + }; +}); + +vi.mock("../../../cslp", () => ({ + extractDetailsFromCslp: vi.fn().mockImplementation((path) => { + return { + content_type_uid: "mockContentTypeUid", + fieldPath: path, + cslpValue: path, + }; + }), +})); + +vi.mock("../../../utils/fetchEntryPermissionsAndStageDetails", () => ({ + fetchEntryPermissionsAndStageDetails: vi.fn().mockImplementation(() => { + return Promise.resolve(mockEntryPermissionsResponse); + }), +})); + +vi.mock("../generators/generateCustomCursor", () => ({ + getFieldIcon: vi.fn().mockReturnValue("mock-icon"), + FieldTypeIconsMap: { + reference: "reference-icon", + }, +})); + +vi.mock("../visualBuilder.style", () => ({ + visualBuilderStyles: vi.fn(() => mockStyles), +})); + +vi.mock("../../VariantIndicator", () => ({ + VariantIndicator: () =>
Variant
, +})); + +vi.mock("../../../utils/errorHandling", () => ({ + hasPostMessageError: vi.fn().mockReturnValue(false), +})); + +describe("FieldLabelWrapperComponent - Render Fields", () => { + beforeEach(() => { + // Reset all mocks to their default state before each test + vi.clearAllMocks(); + + // Reset isFieldDisabled to default + (isFieldDisabled as any).mockReturnValue({ + isDisabled: false, + reason: "", + }); + + // Reset visualBuilderPostMessage mock - inline implementation + vi.mocked(visualBuilderPostMessage!.send).mockImplementation( + (eventName: string, fields: any) => { + if ( + eventName === + VisualBuilderPostMessageEvents.GET_FIELD_DISPLAY_NAMES + ) { + const result: Record = {}; + if (Array.isArray(fields)) { + fields.forEach((field: any) => { + const cslpValue = + field?.cslpValue || field?.cslp || ""; + if (!cslpValue) return; + if (cslpValue === "mockFieldCslp") { + result[cslpValue] = "Field 0"; + } else if ( + cslpValue === + "contentTypeUid.entryUid.locale.parentPath1" + ) { + result[cslpValue] = "Field 1"; + } else if ( + cslpValue === + "contentTypeUid.entryUid.locale.parentPath2" + ) { + result[cslpValue] = "Field 2"; + } else if ( + cslpValue === + "contentTypeUid.entryUid.locale.parentPath3" + ) { + result[cslpValue] = "Field 3"; + } else { + result[cslpValue] = cslpValue; + } + }); + } + return Promise.resolve(result); + } else if ( + eventName === + VisualBuilderPostMessageEvents.GET_CONTENT_TYPE_NAME || + eventName === "get-content-type-name" + ) { + return Promise.resolve({ + contentTypeName: "Page CT", + }); + } else if ( + eventName === + VisualBuilderPostMessageEvents.REFERENCE_MAP || + eventName === "get-reference-map" + ) { + return Promise.resolve({}); + } + return Promise.resolve({}); + } + ); + + // Pre-set field schema in cache to avoid async fetch delay + // This makes FieldSchemaMap.getFieldSchema resolve immediately from cache + FieldSchemaMap.setFieldSchema(mockFieldMetadata.content_type_uid, { + [mockFieldMetadata.fieldPath]: singleLineFieldSchema, + }); + }); + + afterEach(() => { + // Clean up field schema cache after each test + FieldSchemaMap.clear(); + // Clean up DOM after each test to prevent state pollution + document.body.innerHTML = ""; + }); + + afterAll(() => { + vi.clearAllMocks(); + }); + + // mockFieldMetadata is now defined above the describe block + + const mockEventDetails: VisualBuilderCslpEventDetails = { + editableElement: document.createElement("div"), + cslpData: "", + fieldMetadata: mockFieldMetadata, + }; + + const mockGetParentEditable = () => document.createElement("div"); + + test("renders current field and parent fields correctly", async () => { + // Wrap render in act to batch all updates and reduce reconciliation cycles + let container!: HTMLElement; + await act(async () => { + const result = render( + + ); + container = result.container as HTMLElement; + // Use queueMicrotask for faster resolution than setTimeout + await new Promise((resolve) => + queueMicrotask(() => resolve()) + ); + }); + + // Use waitFor with shorter timeout since mocks resolve immediately + await waitFor( + () => { + const text = Array.from(container.querySelectorAll("*")).find( + (el) => el.textContent === DISPLAY_NAMES.mockFieldCslp + ); + if (!text) throw new Error("Text not found"); + expect(text).toBeInTheDocument(); + }, + { timeout: 1000, interval: 10 } + ); + }); +}); diff --git a/src/visualBuilder/components/__test__/fieldlabelwrapper/fieldLabelWrapper.variant.cssClasses.test.tsx b/src/visualBuilder/components/__test__/fieldlabelwrapper/fieldLabelWrapper.variant.cssClasses.test.tsx new file mode 100644 index 00000000..2311e35c --- /dev/null +++ b/src/visualBuilder/components/__test__/fieldlabelwrapper/fieldLabelWrapper.variant.cssClasses.test.tsx @@ -0,0 +1,317 @@ +import { render, waitFor } from "@testing-library/preact"; +import FieldLabelWrapperComponent from "../../fieldLabelWrapper"; +import { VisualBuilderCslpEventDetails } from "../../../types/visualBuilder.types"; +import { singleLineFieldSchema } from "../../../../__test__/data/fields"; +import { isFieldDisabled } from "../../../utils/isFieldDisabled"; +import { FieldSchemaMap } from "../../../utils/fieldSchemaMap"; +import visualBuilderPostMessage from "../../../utils/visualBuilderPostMessage"; +import { VisualBuilderPostMessageEvents } from "../../../utils/types/postMessage.types"; +import React from "preact/compat"; +// Import shared mocks and constants +import { + mockStyles, + mockFieldMetadata, + mockEntryPermissionsResponse, +} from "./fieldLabelWrapper.mocks"; + +// Local cache for this test file (can't use imported cache in vi.mock due to hoisting) +const testFieldSchemaCache: Record> = {}; + +// All mocks - inline implementations (can't use imported functions in vi.mock due to hoisting) +vi.mock("../Tooltip", () => ({ + ToolbarTooltip: ({ children, data, disabled }: any) => ( +
+ {children} +
+ ), +})); + +vi.mock("../../../utils/fieldSchemaMap", async (importOriginal) => { + const actual = + await importOriginal(); + return { + FieldSchemaMap: { + ...actual.FieldSchemaMap, + getFieldSchema: vi + .fn() + .mockImplementation( + (contentTypeUid: string, fieldPath: string) => { + if (testFieldSchemaCache[contentTypeUid]?.[fieldPath]) { + return Promise.resolve( + testFieldSchemaCache[contentTypeUid][fieldPath] + ); + } + const defaultSchema = { + display_name: "Field 0", + data_type: "text", + field_metadata: { + description: "", + default_value: "", + version: 3, + }, + uid: "test_field", + }; + if (!testFieldSchemaCache[contentTypeUid]) { + testFieldSchemaCache[contentTypeUid] = {}; + } + testFieldSchemaCache[contentTypeUid][fieldPath] = + defaultSchema; + return Promise.resolve(defaultSchema); + } + ), + setFieldSchema: vi + .fn() + .mockImplementation( + ( + contentTypeUid: string, + schemaMap: Record + ) => { + if (!testFieldSchemaCache[contentTypeUid]) { + testFieldSchemaCache[contentTypeUid] = {}; + } + Object.assign( + testFieldSchemaCache[contentTypeUid], + schemaMap + ); + } + ), + hasFieldSchema: vi + .fn() + .mockImplementation( + (contentTypeUid: string, fieldPath: string) => { + return !!testFieldSchemaCache[contentTypeUid]?.[ + fieldPath + ]; + } + ), + clear: vi.fn().mockImplementation(() => { + Object.keys(testFieldSchemaCache).forEach( + (key) => delete testFieldSchemaCache[key] + ); + }), + }, + }; +}); + +vi.mock("../../../utils/visualBuilderPostMessage", () => ({ + default: { + send: vi.fn().mockImplementation((eventName: string, fields: any) => { + if ( + eventName === + VisualBuilderPostMessageEvents.GET_FIELD_DISPLAY_NAMES + ) { + const result: Record = {}; + if (Array.isArray(fields)) { + fields.forEach((field: any) => { + const cslpValue = field?.cslpValue || field?.cslp || ""; + if (!cslpValue) return; + if (cslpValue === "mockFieldCslp") { + result[cslpValue] = "Field 0"; + } else if ( + cslpValue === + "contentTypeUid.entryUid.locale.parentPath1" + ) { + result[cslpValue] = "Field 1"; + } else if ( + cslpValue === + "contentTypeUid.entryUid.locale.parentPath2" + ) { + result[cslpValue] = "Field 2"; + } else if ( + cslpValue === + "contentTypeUid.entryUid.locale.parentPath3" + ) { + result[cslpValue] = "Field 3"; + } else { + result[cslpValue] = cslpValue; + } + }); + } + return Promise.resolve(result); + } else if ( + eventName === + VisualBuilderPostMessageEvents.GET_CONTENT_TYPE_NAME || + eventName === "get-content-type-name" + ) { + return Promise.resolve({ + contentTypeName: "Page CT", + }); + } else if ( + eventName === VisualBuilderPostMessageEvents.REFERENCE_MAP || + eventName === "get-reference-map" + ) { + return Promise.resolve({}); + } + return Promise.resolve({}); + }), + }, +})); + +vi.mock("../../../utils/isFieldDisabled", async (importOriginal) => { + const actual = + await importOriginal(); + return { + ...actual, + isFieldDisabled: vi + .fn() + .mockReturnValue({ isDisabled: false, reason: "" }), + }; +}); + +vi.mock("../../../cslp", () => ({ + extractDetailsFromCslp: vi.fn().mockImplementation((path) => { + return { + content_type_uid: "mockContentTypeUid", + fieldPath: path, + cslpValue: path, + }; + }), +})); + +vi.mock("../../../utils/fetchEntryPermissionsAndStageDetails", () => ({ + fetchEntryPermissionsAndStageDetails: vi.fn().mockImplementation(() => { + return Promise.resolve(mockEntryPermissionsResponse); + }), +})); + +vi.mock("../generators/generateCustomCursor", () => ({ + getFieldIcon: vi.fn().mockReturnValue("mock-icon"), + FieldTypeIconsMap: { + reference: "reference-icon", + }, +})); + +vi.mock("../visualBuilder.style", () => ({ + visualBuilderStyles: vi.fn(() => mockStyles), +})); + +vi.mock("../../VariantIndicator", () => ({ + VariantIndicator: () =>
Variant
, +})); + +vi.mock("../../../utils/errorHandling", () => ({ + hasPostMessageError: vi.fn().mockReturnValue(false), +})); + +describe("FieldLabelWrapperComponent - Variant CSS Classes", () => { + beforeEach(() => { + vi.clearAllMocks(); + (isFieldDisabled as any).mockReturnValue({ + isDisabled: false, + reason: "", + }); + vi.mocked(visualBuilderPostMessage!.send).mockImplementation( + (eventName: string, fields: any) => { + if ( + eventName === + VisualBuilderPostMessageEvents.GET_FIELD_DISPLAY_NAMES + ) { + const result: Record = {}; + if (Array.isArray(fields)) { + fields.forEach((field: any) => { + const cslpValue = + field?.cslpValue || field?.cslp || ""; + if (!cslpValue) return; + if (cslpValue === "mockFieldCslp") { + result[cslpValue] = "Field 0"; + } else if ( + cslpValue === + "contentTypeUid.entryUid.locale.parentPath1" + ) { + result[cslpValue] = "Field 1"; + } else if ( + cslpValue === + "contentTypeUid.entryUid.locale.parentPath2" + ) { + result[cslpValue] = "Field 2"; + } else if ( + cslpValue === + "contentTypeUid.entryUid.locale.parentPath3" + ) { + result[cslpValue] = "Field 3"; + } else { + result[cslpValue] = cslpValue; + } + }); + } + return Promise.resolve(result); + } else if ( + eventName === + VisualBuilderPostMessageEvents.GET_CONTENT_TYPE_NAME || + eventName === "get-content-type-name" + ) { + return Promise.resolve({ + contentTypeName: "Page CT", + }); + } else if ( + eventName === + VisualBuilderPostMessageEvents.REFERENCE_MAP || + eventName === "get-reference-map" + ) { + return Promise.resolve({}); + } + return Promise.resolve({}); + } + ); + FieldSchemaMap.setFieldSchema(mockFieldMetadata.content_type_uid, { + [mockFieldMetadata.fieldPath]: singleLineFieldSchema, + }); + }); + + afterEach(() => { + FieldSchemaMap.clear(); + document.body.innerHTML = ""; + }); + + afterAll(() => { + vi.clearAllMocks(); + }); + + const mockEventDetails: VisualBuilderCslpEventDetails = { + editableElement: document.createElement("div"), + cslpData: "", + fieldMetadata: mockFieldMetadata, + }; + + const mockGetParentEditable = () => document.createElement("div"); + + test("applies variant CSS classes when field has variant", async () => { + const variantFieldMetadata = { + ...mockFieldMetadata, + variant: "variant-uid-123", + }; + + const { container } = render( + + ); + + await waitFor( + () => { + const button = container.querySelector("button"); + if (!button || button.hasAttribute("disabled")) { + throw new Error("Button still disabled"); + } + const fieldLabelWrapper = container.querySelector( + "[data-testid='visual-builder__focused-toolbar__field-label-wrapper']" + ); + if (!fieldLabelWrapper) { + throw new Error("Field label wrapper not found"); + } + expect(fieldLabelWrapper).toHaveClass( + "visual-builder__focused-toolbar--variant" + ); + }, + { timeout: 1000, interval: 5 } + ); + }); +}); diff --git a/src/visualBuilder/components/__test__/fieldlabelwrapper/fieldLabelWrapper.variant.noCssClasses.test.tsx b/src/visualBuilder/components/__test__/fieldlabelwrapper/fieldLabelWrapper.variant.noCssClasses.test.tsx new file mode 100644 index 00000000..941db7ec --- /dev/null +++ b/src/visualBuilder/components/__test__/fieldlabelwrapper/fieldLabelWrapper.variant.noCssClasses.test.tsx @@ -0,0 +1,312 @@ +import { render, waitFor } from "@testing-library/preact"; +import FieldLabelWrapperComponent from "../../fieldLabelWrapper"; +import { VisualBuilderCslpEventDetails } from "../../../types/visualBuilder.types"; +import { singleLineFieldSchema } from "../../../../__test__/data/fields"; +import { isFieldDisabled } from "../../../utils/isFieldDisabled"; +import { FieldSchemaMap } from "../../../utils/fieldSchemaMap"; +import visualBuilderPostMessage from "../../../utils/visualBuilderPostMessage"; +import { VisualBuilderPostMessageEvents } from "../../../utils/types/postMessage.types"; +import React from "preact/compat"; +// Import shared mocks and constants +import { + mockStyles, + mockFieldMetadata, + mockEntryPermissionsResponse, +} from "./fieldLabelWrapper.mocks"; + +// Local cache for this test file (can't use imported cache in vi.mock due to hoisting) +const testFieldSchemaCache: Record> = {}; + +// All mocks - inline implementations (can't use imported functions in vi.mock due to hoisting) +vi.mock("../Tooltip", () => ({ + ToolbarTooltip: ({ children, data, disabled }: any) => ( +
+ {children} +
+ ), +})); + +vi.mock("../../../utils/fieldSchemaMap", async (importOriginal) => { + const actual = + await importOriginal(); + return { + FieldSchemaMap: { + ...actual.FieldSchemaMap, + getFieldSchema: vi + .fn() + .mockImplementation( + (contentTypeUid: string, fieldPath: string) => { + if (testFieldSchemaCache[contentTypeUid]?.[fieldPath]) { + return Promise.resolve( + testFieldSchemaCache[contentTypeUid][fieldPath] + ); + } + const defaultSchema = { + display_name: "Field 0", + data_type: "text", + field_metadata: { + description: "", + default_value: "", + version: 3, + }, + uid: "test_field", + }; + if (!testFieldSchemaCache[contentTypeUid]) { + testFieldSchemaCache[contentTypeUid] = {}; + } + testFieldSchemaCache[contentTypeUid][fieldPath] = + defaultSchema; + return Promise.resolve(defaultSchema); + } + ), + setFieldSchema: vi + .fn() + .mockImplementation( + ( + contentTypeUid: string, + schemaMap: Record + ) => { + if (!testFieldSchemaCache[contentTypeUid]) { + testFieldSchemaCache[contentTypeUid] = {}; + } + Object.assign( + testFieldSchemaCache[contentTypeUid], + schemaMap + ); + } + ), + hasFieldSchema: vi + .fn() + .mockImplementation( + (contentTypeUid: string, fieldPath: string) => { + return !!testFieldSchemaCache[contentTypeUid]?.[ + fieldPath + ]; + } + ), + clear: vi.fn().mockImplementation(() => { + Object.keys(testFieldSchemaCache).forEach( + (key) => delete testFieldSchemaCache[key] + ); + }), + }, + }; +}); + +vi.mock("../../../utils/visualBuilderPostMessage", () => ({ + default: { + send: vi.fn().mockImplementation((eventName: string, fields: any) => { + if ( + eventName === + VisualBuilderPostMessageEvents.GET_FIELD_DISPLAY_NAMES + ) { + const result: Record = {}; + if (Array.isArray(fields)) { + fields.forEach((field: any) => { + const cslpValue = field?.cslpValue || field?.cslp || ""; + if (!cslpValue) return; + if (cslpValue === "mockFieldCslp") { + result[cslpValue] = "Field 0"; + } else if ( + cslpValue === + "contentTypeUid.entryUid.locale.parentPath1" + ) { + result[cslpValue] = "Field 1"; + } else if ( + cslpValue === + "contentTypeUid.entryUid.locale.parentPath2" + ) { + result[cslpValue] = "Field 2"; + } else if ( + cslpValue === + "contentTypeUid.entryUid.locale.parentPath3" + ) { + result[cslpValue] = "Field 3"; + } else { + result[cslpValue] = cslpValue; + } + }); + } + return Promise.resolve(result); + } else if ( + eventName === + VisualBuilderPostMessageEvents.GET_CONTENT_TYPE_NAME || + eventName === "get-content-type-name" + ) { + return Promise.resolve({ + contentTypeName: "Page CT", + }); + } else if ( + eventName === VisualBuilderPostMessageEvents.REFERENCE_MAP || + eventName === "get-reference-map" + ) { + return Promise.resolve({}); + } + return Promise.resolve({}); + }), + }, +})); + +vi.mock("../../../utils/isFieldDisabled", async (importOriginal) => { + const actual = + await importOriginal(); + return { + ...actual, + isFieldDisabled: vi + .fn() + .mockReturnValue({ isDisabled: false, reason: "" }), + }; +}); + +vi.mock("../../../cslp", () => ({ + extractDetailsFromCslp: vi.fn().mockImplementation((path) => { + return { + content_type_uid: "mockContentTypeUid", + fieldPath: path, + cslpValue: path, + }; + }), +})); + +vi.mock("../../../utils/fetchEntryPermissionsAndStageDetails", () => ({ + fetchEntryPermissionsAndStageDetails: vi.fn().mockImplementation(() => { + return Promise.resolve(mockEntryPermissionsResponse); + }), +})); + +vi.mock("../generators/generateCustomCursor", () => ({ + getFieldIcon: vi.fn().mockReturnValue("mock-icon"), + FieldTypeIconsMap: { + reference: "reference-icon", + }, +})); + +vi.mock("../visualBuilder.style", () => ({ + visualBuilderStyles: vi.fn(() => mockStyles), +})); + +vi.mock("../../VariantIndicator", () => ({ + VariantIndicator: () =>
Variant
, +})); + +vi.mock("../../../utils/errorHandling", () => ({ + hasPostMessageError: vi.fn().mockReturnValue(false), +})); + +describe("FieldLabelWrapperComponent - No Variant CSS Classes", () => { + beforeEach(() => { + vi.clearAllMocks(); + (isFieldDisabled as any).mockReturnValue({ + isDisabled: false, + reason: "", + }); + vi.mocked(visualBuilderPostMessage!.send).mockImplementation( + (eventName: string, fields: any) => { + if ( + eventName === + VisualBuilderPostMessageEvents.GET_FIELD_DISPLAY_NAMES + ) { + const result: Record = {}; + if (Array.isArray(fields)) { + fields.forEach((field: any) => { + const cslpValue = + field?.cslpValue || field?.cslp || ""; + if (!cslpValue) return; + if (cslpValue === "mockFieldCslp") { + result[cslpValue] = "Field 0"; + } else if ( + cslpValue === + "contentTypeUid.entryUid.locale.parentPath1" + ) { + result[cslpValue] = "Field 1"; + } else if ( + cslpValue === + "contentTypeUid.entryUid.locale.parentPath2" + ) { + result[cslpValue] = "Field 2"; + } else if ( + cslpValue === + "contentTypeUid.entryUid.locale.parentPath3" + ) { + result[cslpValue] = "Field 3"; + } else { + result[cslpValue] = cslpValue; + } + }); + } + return Promise.resolve(result); + } else if ( + eventName === + VisualBuilderPostMessageEvents.GET_CONTENT_TYPE_NAME || + eventName === "get-content-type-name" + ) { + return Promise.resolve({ + contentTypeName: "Page CT", + }); + } else if ( + eventName === + VisualBuilderPostMessageEvents.REFERENCE_MAP || + eventName === "get-reference-map" + ) { + return Promise.resolve({}); + } + return Promise.resolve({}); + } + ); + FieldSchemaMap.setFieldSchema(mockFieldMetadata.content_type_uid, { + [mockFieldMetadata.fieldPath]: singleLineFieldSchema, + }); + }); + + afterEach(() => { + FieldSchemaMap.clear(); + document.body.innerHTML = ""; + }); + + afterAll(() => { + vi.clearAllMocks(); + }); + + const mockEventDetails: VisualBuilderCslpEventDetails = { + editableElement: document.createElement("div"), + cslpData: "", + fieldMetadata: mockFieldMetadata, + }; + + const mockGetParentEditable = () => document.createElement("div"); + + test("does not apply variant CSS classes when field has no variant", async () => { + const { container } = render( + + ); + + await waitFor( + () => { + const button = container.querySelector("button"); + if (!button || button.hasAttribute("disabled")) { + throw new Error("Button still disabled"); + } + const fieldLabelWrapper = container.querySelector( + "[data-testid='visual-builder__focused-toolbar__field-label-wrapper']" + ); + if (!fieldLabelWrapper) { + throw new Error("Field label wrapper not found"); + } + expect(fieldLabelWrapper).not.toHaveClass( + "visual-builder__focused-toolbar--variant" + ); + }, + { timeout: 1000, interval: 5 } + ); + }); +}); diff --git a/src/visualBuilder/components/__test__/fieldlabelwrapper/fieldLabelWrapper.variant.noIndicator.test.tsx b/src/visualBuilder/components/__test__/fieldlabelwrapper/fieldLabelWrapper.variant.noIndicator.test.tsx new file mode 100644 index 00000000..370eb9a5 --- /dev/null +++ b/src/visualBuilder/components/__test__/fieldlabelwrapper/fieldLabelWrapper.variant.noIndicator.test.tsx @@ -0,0 +1,306 @@ +import { render, findByTestId } from "@testing-library/preact"; +import FieldLabelWrapperComponent from "../../fieldLabelWrapper"; +import { VisualBuilderCslpEventDetails } from "../../../types/visualBuilder.types"; +import { singleLineFieldSchema } from "../../../../__test__/data/fields"; +import { isFieldDisabled } from "../../../utils/isFieldDisabled"; +import { FieldSchemaMap } from "../../../utils/fieldSchemaMap"; +import visualBuilderPostMessage from "../../../utils/visualBuilderPostMessage"; +import { VisualBuilderPostMessageEvents } from "../../../utils/types/postMessage.types"; +import React from "preact/compat"; +// Import shared mocks and constants +import { + mockStyles, + mockFieldMetadata, + mockEntryPermissionsResponse, +} from "./fieldLabelWrapper.mocks"; + +// Local cache for this test file (can't use imported cache in vi.mock due to hoisting) +const testFieldSchemaCache: Record> = {}; + +// All mocks - inline implementations (can't use imported functions in vi.mock due to hoisting) +vi.mock("../Tooltip", () => ({ + ToolbarTooltip: ({ children, data, disabled }: any) => ( +
+ {children} +
+ ), +})); + +vi.mock("../../../utils/fieldSchemaMap", async (importOriginal) => { + const actual = + await importOriginal(); + return { + FieldSchemaMap: { + ...actual.FieldSchemaMap, + getFieldSchema: vi + .fn() + .mockImplementation( + (contentTypeUid: string, fieldPath: string) => { + if (testFieldSchemaCache[contentTypeUid]?.[fieldPath]) { + return Promise.resolve( + testFieldSchemaCache[contentTypeUid][fieldPath] + ); + } + const defaultSchema = { + display_name: "Field 0", + data_type: "text", + field_metadata: { + description: "", + default_value: "", + version: 3, + }, + uid: "test_field", + }; + if (!testFieldSchemaCache[contentTypeUid]) { + testFieldSchemaCache[contentTypeUid] = {}; + } + testFieldSchemaCache[contentTypeUid][fieldPath] = + defaultSchema; + return Promise.resolve(defaultSchema); + } + ), + setFieldSchema: vi + .fn() + .mockImplementation( + ( + contentTypeUid: string, + schemaMap: Record + ) => { + if (!testFieldSchemaCache[contentTypeUid]) { + testFieldSchemaCache[contentTypeUid] = {}; + } + Object.assign( + testFieldSchemaCache[contentTypeUid], + schemaMap + ); + } + ), + hasFieldSchema: vi + .fn() + .mockImplementation( + (contentTypeUid: string, fieldPath: string) => { + return !!testFieldSchemaCache[contentTypeUid]?.[ + fieldPath + ]; + } + ), + clear: vi.fn().mockImplementation(() => { + Object.keys(testFieldSchemaCache).forEach( + (key) => delete testFieldSchemaCache[key] + ); + }), + }, + }; +}); + +vi.mock("../../../utils/visualBuilderPostMessage", () => ({ + default: { + send: vi.fn().mockImplementation((eventName: string, fields: any) => { + if ( + eventName === + VisualBuilderPostMessageEvents.GET_FIELD_DISPLAY_NAMES + ) { + const result: Record = {}; + if (Array.isArray(fields)) { + fields.forEach((field: any) => { + const cslpValue = field?.cslpValue || field?.cslp || ""; + if (!cslpValue) return; + if (cslpValue === "mockFieldCslp") { + result[cslpValue] = "Field 0"; + } else if ( + cslpValue === + "contentTypeUid.entryUid.locale.parentPath1" + ) { + result[cslpValue] = "Field 1"; + } else if ( + cslpValue === + "contentTypeUid.entryUid.locale.parentPath2" + ) { + result[cslpValue] = "Field 2"; + } else if ( + cslpValue === + "contentTypeUid.entryUid.locale.parentPath3" + ) { + result[cslpValue] = "Field 3"; + } else { + result[cslpValue] = cslpValue; + } + }); + } + return Promise.resolve(result); + } else if ( + eventName === + VisualBuilderPostMessageEvents.GET_CONTENT_TYPE_NAME || + eventName === "get-content-type-name" + ) { + return Promise.resolve({ + contentTypeName: "Page CT", + }); + } else if ( + eventName === VisualBuilderPostMessageEvents.REFERENCE_MAP || + eventName === "get-reference-map" + ) { + return Promise.resolve({}); + } + return Promise.resolve({}); + }), + }, +})); + +vi.mock("../../../utils/isFieldDisabled", async (importOriginal) => { + const actual = + await importOriginal(); + return { + ...actual, + isFieldDisabled: vi + .fn() + .mockReturnValue({ isDisabled: false, reason: "" }), + }; +}); + +vi.mock("../../../cslp", () => ({ + extractDetailsFromCslp: vi.fn().mockImplementation((path) => { + return { + content_type_uid: "mockContentTypeUid", + fieldPath: path, + cslpValue: path, + }; + }), +})); + +vi.mock("../../../utils/fetchEntryPermissionsAndStageDetails", () => ({ + fetchEntryPermissionsAndStageDetails: vi.fn().mockImplementation(() => { + return Promise.resolve(mockEntryPermissionsResponse); + }), +})); + +vi.mock("../generators/generateCustomCursor", () => ({ + getFieldIcon: vi.fn().mockReturnValue("mock-icon"), + FieldTypeIconsMap: { + reference: "reference-icon", + }, +})); + +vi.mock("../visualBuilder.style", () => ({ + visualBuilderStyles: vi.fn(() => mockStyles), +})); + +vi.mock("../../VariantIndicator", () => ({ + VariantIndicator: () =>
Variant
, +})); + +vi.mock("../../../utils/errorHandling", () => ({ + hasPostMessageError: vi.fn().mockReturnValue(false), +})); + +describe("FieldLabelWrapperComponent - No Variant Indicator", () => { + beforeEach(() => { + vi.clearAllMocks(); + (isFieldDisabled as any).mockReturnValue({ + isDisabled: false, + reason: "", + }); + vi.mocked(visualBuilderPostMessage!.send).mockImplementation( + (eventName: string, fields: any) => { + if ( + eventName === + VisualBuilderPostMessageEvents.GET_FIELD_DISPLAY_NAMES + ) { + const result: Record = {}; + if (Array.isArray(fields)) { + fields.forEach((field: any) => { + const cslpValue = + field?.cslpValue || field?.cslp || ""; + if (!cslpValue) return; + if (cslpValue === "mockFieldCslp") { + result[cslpValue] = "Field 0"; + } else if ( + cslpValue === + "contentTypeUid.entryUid.locale.parentPath1" + ) { + result[cslpValue] = "Field 1"; + } else if ( + cslpValue === + "contentTypeUid.entryUid.locale.parentPath2" + ) { + result[cslpValue] = "Field 2"; + } else if ( + cslpValue === + "contentTypeUid.entryUid.locale.parentPath3" + ) { + result[cslpValue] = "Field 3"; + } else { + result[cslpValue] = cslpValue; + } + }); + } + return Promise.resolve(result); + } else if ( + eventName === + VisualBuilderPostMessageEvents.GET_CONTENT_TYPE_NAME || + eventName === "get-content-type-name" + ) { + return Promise.resolve({ + contentTypeName: "Page CT", + }); + } else if ( + eventName === + VisualBuilderPostMessageEvents.REFERENCE_MAP || + eventName === "get-reference-map" + ) { + return Promise.resolve({}); + } + return Promise.resolve({}); + } + ); + FieldSchemaMap.setFieldSchema(mockFieldMetadata.content_type_uid, { + [mockFieldMetadata.fieldPath]: singleLineFieldSchema, + }); + }); + + afterEach(() => { + FieldSchemaMap.clear(); + document.body.innerHTML = ""; + }); + + afterAll(() => { + vi.clearAllMocks(); + }); + + const mockEventDetails: VisualBuilderCslpEventDetails = { + editableElement: document.createElement("div"), + cslpData: "", + fieldMetadata: mockFieldMetadata, + }; + + const mockGetParentEditable = () => document.createElement("div"); + + test("does not render VariantIndicator when field has no variant", async () => { + const { container } = render( + + ); + + const fieldLabel = await findByTestId( + container as HTMLElement, + "visual-builder__focused-toolbar__field-label-wrapper", + {}, + { timeout: 1000 } + ); + expect(fieldLabel).toBeInTheDocument(); + + const variantIndicator = container.querySelector( + "[data-testid='variant-indicator']" + ); + expect(variantIndicator).not.toBeInTheDocument(); + }); +}); diff --git a/src/visualBuilder/components/__test__/fieldlabelwrapper/fieldLabelWrapper.variant.renderIndicator.test.tsx b/src/visualBuilder/components/__test__/fieldlabelwrapper/fieldLabelWrapper.variant.renderIndicator.test.tsx new file mode 100644 index 00000000..ac01cd4d --- /dev/null +++ b/src/visualBuilder/components/__test__/fieldlabelwrapper/fieldLabelWrapper.variant.renderIndicator.test.tsx @@ -0,0 +1,328 @@ +import { render, waitFor } from "@testing-library/preact"; +import FieldLabelWrapperComponent from "../../fieldLabelWrapper"; +import { VisualBuilderCslpEventDetails } from "../../../types/visualBuilder.types"; +import { singleLineFieldSchema } from "../../../../__test__/data/fields"; +import { isFieldDisabled } from "../../../utils/isFieldDisabled"; +import { FieldSchemaMap } from "../../../utils/fieldSchemaMap"; +import visualBuilderPostMessage from "../../../utils/visualBuilderPostMessage"; +import { VisualBuilderPostMessageEvents } from "../../../utils/types/postMessage.types"; +import React from "preact/compat"; +// Import shared mocks and constants +import { + mockStyles, + mockFieldMetadata, + mockEntryPermissionsResponse, +} from "./fieldLabelWrapper.mocks"; + +// Local cache for this test file (can't use imported cache in vi.mock due to hoisting) +const testFieldSchemaCache: Record> = {}; + +// All mocks - inline implementations (can't use imported functions in vi.mock due to hoisting) +vi.mock("../Tooltip", () => ({ + ToolbarTooltip: ({ children, data, disabled }: any) => ( +
+ {children} +
+ ), +})); + +vi.mock("../../../utils/fieldSchemaMap", async (importOriginal) => { + const actual = + await importOriginal(); + return { + FieldSchemaMap: { + ...actual.FieldSchemaMap, + getFieldSchema: vi + .fn() + .mockImplementation( + (contentTypeUid: string, fieldPath: string) => { + if (testFieldSchemaCache[contentTypeUid]?.[fieldPath]) { + return Promise.resolve( + testFieldSchemaCache[contentTypeUid][fieldPath] + ); + } + const defaultSchema = { + display_name: "Field 0", + data_type: "text", + field_metadata: { + description: "", + default_value: "", + version: 3, + }, + uid: "test_field", + }; + if (!testFieldSchemaCache[contentTypeUid]) { + testFieldSchemaCache[contentTypeUid] = {}; + } + testFieldSchemaCache[contentTypeUid][fieldPath] = + defaultSchema; + return Promise.resolve(defaultSchema); + } + ), + setFieldSchema: vi + .fn() + .mockImplementation( + ( + contentTypeUid: string, + schemaMap: Record + ) => { + if (!testFieldSchemaCache[contentTypeUid]) { + testFieldSchemaCache[contentTypeUid] = {}; + } + Object.assign( + testFieldSchemaCache[contentTypeUid], + schemaMap + ); + } + ), + hasFieldSchema: vi + .fn() + .mockImplementation( + (contentTypeUid: string, fieldPath: string) => { + return !!testFieldSchemaCache[contentTypeUid]?.[ + fieldPath + ]; + } + ), + clear: vi.fn().mockImplementation(() => { + Object.keys(testFieldSchemaCache).forEach( + (key) => delete testFieldSchemaCache[key] + ); + }), + }, + }; +}); + +vi.mock("../../../utils/visualBuilderPostMessage", () => ({ + default: { + send: vi.fn().mockImplementation((eventName: string, fields: any) => { + if ( + eventName === + VisualBuilderPostMessageEvents.GET_FIELD_DISPLAY_NAMES + ) { + const result: Record = {}; + if (Array.isArray(fields)) { + fields.forEach((field: any) => { + const cslpValue = field?.cslpValue || field?.cslp || ""; + if (!cslpValue) return; + if (cslpValue === "mockFieldCslp") { + result[cslpValue] = "Field 0"; + } else if ( + cslpValue === + "contentTypeUid.entryUid.locale.parentPath1" + ) { + result[cslpValue] = "Field 1"; + } else if ( + cslpValue === + "contentTypeUid.entryUid.locale.parentPath2" + ) { + result[cslpValue] = "Field 2"; + } else if ( + cslpValue === + "contentTypeUid.entryUid.locale.parentPath3" + ) { + result[cslpValue] = "Field 3"; + } else { + result[cslpValue] = cslpValue; + } + }); + } + return Promise.resolve(result); + } else if ( + eventName === + VisualBuilderPostMessageEvents.GET_CONTENT_TYPE_NAME || + eventName === "get-content-type-name" + ) { + return Promise.resolve({ + contentTypeName: "Page CT", + }); + } else if ( + eventName === VisualBuilderPostMessageEvents.REFERENCE_MAP || + eventName === "get-reference-map" + ) { + return Promise.resolve({}); + } + return Promise.resolve({}); + }), + }, +})); + +vi.mock("../../../utils/isFieldDisabled", async (importOriginal) => { + const actual = + await importOriginal(); + return { + ...actual, + isFieldDisabled: vi + .fn() + .mockReturnValue({ isDisabled: false, reason: "" }), + }; +}); + +vi.mock("../../../cslp", () => ({ + extractDetailsFromCslp: vi.fn().mockImplementation((path) => { + return { + content_type_uid: "mockContentTypeUid", + fieldPath: path, + cslpValue: path, + }; + }), +})); + +vi.mock("../../../utils/fetchEntryPermissionsAndStageDetails", () => ({ + fetchEntryPermissionsAndStageDetails: vi.fn().mockImplementation(() => { + return Promise.resolve(mockEntryPermissionsResponse); + }), +})); + +vi.mock("../generators/generateCustomCursor", () => ({ + getFieldIcon: vi.fn().mockReturnValue("mock-icon"), + FieldTypeIconsMap: { + reference: "reference-icon", + }, +})); + +vi.mock("../visualBuilder.style", () => ({ + visualBuilderStyles: vi.fn(() => mockStyles), +})); + +vi.mock("../../VariantIndicator", () => ({ + VariantIndicator: () =>
Variant
, +})); + +vi.mock("../../../utils/errorHandling", () => ({ + hasPostMessageError: vi.fn().mockReturnValue(false), +})); + +describe("FieldLabelWrapperComponent - Variant Indicator", () => { + beforeEach(() => { + vi.clearAllMocks(); + (isFieldDisabled as any).mockReturnValue({ + isDisabled: false, + reason: "", + }); + vi.mocked(visualBuilderPostMessage!.send).mockImplementation( + (eventName: string, fields: any) => { + if ( + eventName === + VisualBuilderPostMessageEvents.GET_FIELD_DISPLAY_NAMES + ) { + const result: Record = {}; + if (Array.isArray(fields)) { + fields.forEach((field: any) => { + const cslpValue = + field?.cslpValue || field?.cslp || ""; + if (!cslpValue) return; + if (cslpValue === "mockFieldCslp") { + result[cslpValue] = "Field 0"; + } else if ( + cslpValue === + "contentTypeUid.entryUid.locale.parentPath1" + ) { + result[cslpValue] = "Field 1"; + } else if ( + cslpValue === + "contentTypeUid.entryUid.locale.parentPath2" + ) { + result[cslpValue] = "Field 2"; + } else if ( + cslpValue === + "contentTypeUid.entryUid.locale.parentPath3" + ) { + result[cslpValue] = "Field 3"; + } else { + result[cslpValue] = cslpValue; + } + }); + } + return Promise.resolve(result); + } else if ( + eventName === + VisualBuilderPostMessageEvents.GET_CONTENT_TYPE_NAME || + eventName === "get-content-type-name" + ) { + return Promise.resolve({ + contentTypeName: "Page CT", + }); + } else if ( + eventName === + VisualBuilderPostMessageEvents.REFERENCE_MAP || + eventName === "get-reference-map" + ) { + return Promise.resolve({}); + } + return Promise.resolve({}); + } + ); + FieldSchemaMap.setFieldSchema(mockFieldMetadata.content_type_uid, { + [mockFieldMetadata.fieldPath]: singleLineFieldSchema, + }); + }); + + afterEach(() => { + FieldSchemaMap.clear(); + document.body.innerHTML = ""; + }); + + afterAll(() => { + vi.clearAllMocks(); + }); + + const mockEventDetails: VisualBuilderCslpEventDetails = { + editableElement: document.createElement("div"), + cslpData: "", + fieldMetadata: mockFieldMetadata, + }; + + const mockGetParentEditable = () => document.createElement("div"); + + test("renders VariantIndicator when field has variant", async () => { + const variantFieldMetadata = { + ...mockFieldMetadata, + variant: "variant-uid-123", + }; + + const { container } = render( + + ); + + await waitFor( + () => { + // First, ensure the component is fully loaded by checking for the field-label-wrapper + const fieldLabelWrapper = container.querySelector( + "[data-testid='visual-builder__focused-toolbar__field-label-wrapper']" + ); + if (!fieldLabelWrapper) { + throw new Error("Field label wrapper not found"); + } + + // Check that the variant class is applied (ensures isVariant state is set) + if ( + !fieldLabelWrapper.classList.contains( + "visual-builder__focused-toolbar--variant" + ) + ) { + throw new Error("Variant class not applied"); + } + + // Now check for the variant indicator + const variantIndicator = container.querySelector( + "[data-testid='variant-indicator']" + ); + if (!variantIndicator) { + throw new Error("Variant indicator not found"); + } + }, + { timeout: 5000, interval: 10 } + ); + }); +}); diff --git a/src/visualBuilder/generators/__test__/generateToolbar.test.ts b/src/visualBuilder/generators/__test__/generateToolbar.test.ts index d9ccc137..fadd85b0 100644 --- a/src/visualBuilder/generators/__test__/generateToolbar.test.ts +++ b/src/visualBuilder/generators/__test__/generateToolbar.test.ts @@ -1,4 +1,4 @@ -import { act, findByTestId, fireEvent, waitFor } from "@testing-library/preact"; +import { act, fireEvent } from "@testing-library/preact"; import { getFieldSchemaMap } from "../../../__test__/data/fieldSchemaMap"; import { CslpData } from "../../../cslp/types/cslp.types"; import { VisualBuilderCslpEventDetails } from "../../types/visualBuilder.types"; @@ -10,12 +10,6 @@ import { singleLineFieldSchema } from "../../../__test__/data/fields"; const MOCK_CSLP = "all_fields.bltapikey.en-us.single_line"; -global.ResizeObserver = vi.fn().mockImplementation(() => ({ - observe: vi.fn(), - unobserve: vi.fn(), - disconnect: vi.fn(), -})); - vi.mock("../../utils/fetchEntryPermissionsAndStageDetails", () => ({ fetchEntryPermissionsAndStageDetails: async () => ({ acl: { @@ -66,11 +60,15 @@ describe("appendFieldPathDropdown", () => { }); }); - beforeEach(() => { + beforeAll(() => { FieldSchemaMap.setFieldSchema( "all_fields", getFieldSchemaMap().all_fields ); + }); + + beforeEach(() => { + document.body.innerHTML = ""; singleLineField = document.createElement("p"); singleLineField.setAttribute("data-cslp", MOCK_CSLP); @@ -110,11 +108,9 @@ describe("appendFieldPathDropdown", () => { }; }); - test("should not do anything if tooltip is already present", async () => { + test("should not do anything if tooltip is already present", () => { focusedToolbar.classList.add("visual-builder__tooltip--persistent"); - await act(() => { - appendFieldPathDropdown(mockEventDetails, focusedToolbar); - }) + appendFieldPathDropdown(mockEventDetails, focusedToolbar); const fieldLabelWrapper = focusedToolbar.querySelector( ".visual-builder__focused-toolbar__field-label-wrapper" @@ -126,27 +122,21 @@ describe("appendFieldPathDropdown", () => { ); }); - test("should close the field label dropdown if open", async () => { - await act(() => { - appendFieldPathDropdown(mockEventDetails, focusedToolbar); - }) + test("should close the field label dropdown if open", () => { + appendFieldPathDropdown(mockEventDetails, focusedToolbar); - const fieldLabelWrapper = await findByTestId( - focusedToolbar, - "visual-builder__focused-toolbar__field-label-wrapper" - ); + const fieldLabelWrapper = focusedToolbar.querySelector( + '[data-testid="visual-builder__focused-toolbar__field-label-wrapper"]' + ) as HTMLElement; + expect(fieldLabelWrapper).toBeTruthy(); fireEvent.click(fieldLabelWrapper); - await waitFor(() => { - expect(fieldLabelWrapper).toHaveClass("field-label-dropdown-open"); - }); + expect(fieldLabelWrapper).toHaveClass("field-label-dropdown-open"); }); - test("should open the field label dropdown if closed", async () => { - await act(() => { - appendFieldPathDropdown(mockEventDetails, focusedToolbar); - }) + test("should open the field label dropdown if closed", () => { + appendFieldPathDropdown(mockEventDetails, focusedToolbar); const fieldLabelWrapper = focusedToolbar.querySelector( ".visual-builder__focused-toolbar__field-label-wrapper" diff --git a/src/visualBuilder/utils/__test__/focusOverlayWrapper.test.ts b/src/visualBuilder/utils/__test__/focusOverlayWrapper.test.ts index 1d80c172..44323fa5 100644 --- a/src/visualBuilder/utils/__test__/focusOverlayWrapper.test.ts +++ b/src/visualBuilder/utils/__test__/focusOverlayWrapper.test.ts @@ -166,12 +166,12 @@ describe("hideFocusOverlay", () => { vi.spyOn(FieldSchemaMap, "getFieldSchema").mockResolvedValue( mockMultipleLinkFieldSchema ); - beforeEach(() => { + + // Run expensive UI setup once for all tests + beforeAll(() => { initUI({ resizeObserver: mockResizeObserver, }); - VisualBuilder.VisualBuilderGlobalState.value.focusFieldReceivedInput = - true; visualBuilderContainer = document.querySelector( ".visual-builder__container" ) as HTMLDivElement; @@ -179,6 +179,12 @@ describe("hideFocusOverlay", () => { focusOverlayWrapper = document.querySelector( ".visual-builder__overlay__wrapper" ) as HTMLDivElement; + }); + + beforeEach(() => { + // Reset state before each test + VisualBuilder.VisualBuilderGlobalState.value.focusFieldReceivedInput = + true; editedElement = document.createElement("p"); editedElement.setAttribute( @@ -203,10 +209,16 @@ describe("hideFocusOverlay", () => { }); afterEach(() => { - document.body.innerHTML = ""; + // Only clean up what we created in beforeEach + editedElement?.remove(); vi.clearAllMocks(); }); + afterAll(() => { + // Clean up shared UI + document.body.innerHTML = ""; + }); + test("should not hide the overlay if the focus overlay wrapper is null", () => { expect(focusOverlayWrapper.classList.contains("visible")).toBe(true); @@ -256,13 +268,15 @@ describe("hideFocusOverlay", () => { expect(editedElement.textContent).toBe("New text"); - // close the overlay + // close the overlay - this triggers async save operation fireEvent.click(focusOverlayWrapper); expect(focusOverlayWrapper.classList.contains("visible")).toBe(false); + // Wait for async message sending to complete await waitFor(() => { expect(visualBuilderPostMessage?.send).toHaveBeenCalled(); }); + expect(visualBuilderPostMessage?.send).toHaveBeenCalledWith( VisualBuilderPostMessageEvents.UPDATE_FIELD, { @@ -284,7 +298,7 @@ describe("hideFocusOverlay", () => { ); }); - test("should not send update field event when focusFieldReceivedInput is false", async () => { + test("should not send update field event when focusFieldReceivedInput is false", () => { editedElement.setAttribute("contenteditable", "true"); // Set up global state @@ -305,9 +319,8 @@ describe("hideFocusOverlay", () => { expect(focusOverlayWrapper.classList.contains("visible")).toBe(false); - await waitFor(() => { - expect(visualBuilderPostMessage?.send).not.toHaveBeenCalled(); - }); + // Mock assertions are synchronous - no need for waitFor + expect(visualBuilderPostMessage?.send).not.toHaveBeenCalled(); }); test("should run cleanup function", () => { @@ -318,20 +331,4 @@ describe("hideFocusOverlay", () => { expect(cleanIndividualFieldResidual).toHaveBeenCalledTimes(1); }); - - // TODO - test("should hide the overlay if the escape key is pressed", () => { - expect(focusOverlayWrapper.classList.contains("visible")).toBe(true); - - const escapeEvent = new KeyboardEvent("keydown", { - key: "Escape", - }); - window.dispatchEvent(escapeEvent); - - waitFor(() => { - expect(focusOverlayWrapper.classList.contains("visible")).toBe( - false - ); - }); - }); }); diff --git a/src/visualBuilder/utils/__test__/handleIndividualFields.test.ts b/src/visualBuilder/utils/__test__/handleIndividualFields.test.ts index 4a27312d..eee6ec19 100644 --- a/src/visualBuilder/utils/__test__/handleIndividualFields.test.ts +++ b/src/visualBuilder/utils/__test__/handleIndividualFields.test.ts @@ -1,11 +1,17 @@ import { describe, it, expect, vi, beforeEach, Mock } from "vitest"; -import { handleIndividualFields, cleanIndividualFieldResidual } from "../handleIndividualFields"; +import { + handleIndividualFields, + cleanIndividualFieldResidual, +} from "../handleIndividualFields"; import { VisualBuilderCslpEventDetails } from "../../types/visualBuilder.types"; import { FieldSchemaMap } from "../fieldSchemaMap"; import { getFieldData } from "../getFieldData"; import { getFieldType } from "../getFieldType"; import { isFieldDisabled } from "../isFieldDisabled"; -import { handleAddButtonsForMultiple, removeAddInstanceButtons } from "../multipleElementAddButton"; +import { + handleAddButtonsForMultiple, + removeAddInstanceButtons, +} from "../multipleElementAddButton"; import { VisualBuilderPostMessageEvents } from "../types/postMessage.types"; import visualBuilderPostMessage from "../visualBuilderPostMessage"; import { VisualBuilder } from "../.."; @@ -39,16 +45,16 @@ describe("handleIndividualFields", () => { fieldPath: "fieldPath", fieldPathWithIndex: "fieldPathWithIndex", instance: { - fieldPathWithIndex: "fieldPathWithIndex.0" - } + fieldPathWithIndex: "fieldPathWithIndex.0", + }, }, - editableElement: document.createElement("div") + editableElement: document.createElement("div"), }; elements = { visualBuilderContainer: document.createElement("div"), resizeObserver: new ResizeObserver(() => {}), - lastEditedField: null + lastEditedField: null, }; vi.clearAllMocks(); @@ -69,34 +75,46 @@ describe("handleIndividualFields", () => { await handleIndividualFields(eventDetails, elements); }); - expect(FieldSchemaMap.getFieldSchema).toHaveBeenCalledWith("contentTypeUid", "fieldPath"); - expect(getFieldData).toHaveBeenCalledWith({ content_type_uid: "contentTypeUid", entry_uid: "entryUid", locale: "en-us" }, "fieldPathWithIndex"); + expect(FieldSchemaMap.getFieldSchema).toHaveBeenCalledWith( + "contentTypeUid", + "fieldPath" + ); + expect(getFieldData).toHaveBeenCalledWith( + { + content_type_uid: "contentTypeUid", + entry_uid: "entryUid", + locale: "en-us", + }, + "fieldPathWithIndex" + ); expect(getFieldType).toHaveBeenCalledWith(fieldSchema); expect(isFieldDisabled).toHaveBeenCalledWith( - fieldSchema, - eventDetails, - { - update: true, - error: true - }, - { - read: true, - update: true, - delete: true, - publish: true, - }, - { - permissions: { - entry: { - update: true, - }, + fieldSchema, + eventDetails, + { + update: true, }, - stage: { - name: "Unknown" + { + create: true, + read: true, + update: true, + delete: true, + publish: true, + }, + { + permissions: { + entry: { + update: true, + }, + }, + stage: undefined, } - } ); - expect(eventDetails.editableElement.getAttribute(VISUAL_BUILDER_FIELD_TYPE_ATTRIBUTE_KEY)).toBe(fieldType); + expect( + eventDetails.editableElement.getAttribute( + VISUAL_BUILDER_FIELD_TYPE_ATTRIBUTE_KEY + ) + ).toBe(fieldType); }); it("should handle multiple fields correctly", async () => { @@ -116,7 +134,10 @@ describe("handleIndividualFields", () => { }); it("should handle inline editing for supported fields", async () => { - const fieldSchema = { data_type: FieldDataType.SINGLELINE, multiple: false }; + const fieldSchema = { + data_type: FieldDataType.SINGLELINE, + multiple: false, + }; const expectedFieldData = "expectedFieldData"; eventDetails.editableElement.textContent = expectedFieldData; const fieldType = FieldDataType.SINGLELINE; @@ -129,12 +150,17 @@ describe("handleIndividualFields", () => { await act(async () => { await handleIndividualFields(eventDetails, elements); - }) + }); - expect(eventDetails.editableElement.getAttribute(VISUAL_BUILDER_FIELD_TYPE_ATTRIBUTE_KEY)).toBe(fieldType); - expect(eventDetails.editableElement.getAttribute("contenteditable")).toBe("true"); + expect( + eventDetails.editableElement.getAttribute( + VISUAL_BUILDER_FIELD_TYPE_ATTRIBUTE_KEY + ) + ).toBe(fieldType); + expect( + eventDetails.editableElement.getAttribute("contenteditable") + ).toBe("true"); }); - }); describe("cleanIndividualFieldResidual", () => { @@ -150,7 +176,7 @@ describe("cleanIndividualFieldResidual", () => { overlayWrapper: document.createElement("div"), visualBuilderContainer: document.createElement("div"), focusedToolbar: document.createElement("div"), - resizeObserver: new ResizeObserver(() => {}) + resizeObserver: new ResizeObserver(() => {}), }; vi.clearAllMocks(); @@ -158,37 +184,51 @@ describe("cleanIndividualFieldResidual", () => { it("should clean individual field residuals correctly", () => { const previousSelectedEditableDOM = document.createElement("div"); - VisualBuilder.VisualBuilderGlobalState.value.previousSelectedEditableDOM = previousSelectedEditableDOM; + VisualBuilder.VisualBuilderGlobalState.value.previousSelectedEditableDOM = + previousSelectedEditableDOM; cleanIndividualFieldResidual(elements); expect(removeAddInstanceButtons).toHaveBeenCalled(); - expect(previousSelectedEditableDOM.getAttribute("contenteditable")).toBeNull(); - expect(elements.resizeObserver.unobserve).toHaveBeenCalledWith(previousSelectedEditableDOM); + expect( + previousSelectedEditableDOM.getAttribute("contenteditable") + ).toBeNull(); + expect(elements.resizeObserver.unobserve).toHaveBeenCalledWith( + previousSelectedEditableDOM + ); }); it("should clean pseudo editable element correctly", () => { const pseudoEditableElement = document.createElement("div"); - pseudoEditableElement.classList.add("visual-builder__pseudo-editable-element"); + pseudoEditableElement.classList.add( + "visual-builder__pseudo-editable-element" + ); elements.visualBuilderContainer?.appendChild(pseudoEditableElement); cleanIndividualFieldResidual(elements); - expect(elements.resizeObserver.unobserve).toHaveBeenCalledWith(pseudoEditableElement); + expect(elements.resizeObserver.unobserve).toHaveBeenCalledWith( + pseudoEditableElement + ); expect(pseudoEditableElement.parentNode).toBeNull(); }); -it("should clean focused toolbar correctly", () => { - cleanIndividualFieldResidual(elements); + it("should clean focused toolbar correctly", () => { + cleanIndividualFieldResidual(elements); - expect(elements.focusedToolbar?.innerHTML).toBe(""); + expect(elements.focusedToolbar?.innerHTML).toBe(""); - const toolbarEvents = [VisualBuilderPostMessageEvents.DELETE_INSTANCE, VisualBuilderPostMessageEvents.UPDATE_DISCUSSION_ID]; - toolbarEvents.forEach((event) => { - //@ts-expect-error - We are accessing private method here, but it is necessary to clean up the event listeners. - if (visualBuilderPostMessage?.requestMessageHandlers?.has(event)) { + const toolbarEvents = [ + VisualBuilderPostMessageEvents.DELETE_INSTANCE, + VisualBuilderPostMessageEvents.UPDATE_DISCUSSION_ID, + ]; + toolbarEvents.forEach((event) => { //@ts-expect-error - We are accessing private method here, but it is necessary to clean up the event listeners. - expect(visualBuilderPostMessage?.unregisterEvent).toHaveBeenCalledWith(event); - } + if (visualBuilderPostMessage?.requestMessageHandlers?.has(event)) { + //@ts-expect-error - We are accessing private method here, but it is necessary to clean up the event listeners. + expect( + visualBuilderPostMessage?.unregisterEvent + ).toHaveBeenCalledWith(event); + } + }); }); }); -}); \ No newline at end of file diff --git a/src/visualBuilder/utils/__test__/multipleElementAddButton.test.ts b/src/visualBuilder/utils/__test__/multipleElementAddButton.test.ts index fd3e44ae..c5ea60d5 100644 --- a/src/visualBuilder/utils/__test__/multipleElementAddButton.test.ts +++ b/src/visualBuilder/utils/__test__/multipleElementAddButton.test.ts @@ -27,22 +27,22 @@ const mockResizeObserver = { disconnect: vi.fn(), }; -vi.mock("../visualBuilderPostMessage", async () => { +vi.mock("../visualBuilderPostMessage", async (importOriginal) => { const { getAllContentTypes } = await vi.importActual< typeof import("../../../__test__/data/contentType") >("../../../__test__/data/contentType"); const contentTypes = getAllContentTypes(); return { default: { - send: vi.fn().mockImplementation((eventName: string) => { + send: vi.fn((eventName: string) => { if (eventName === "init") { - return { + return Promise.resolve({ contentTypes, - }; + }); } return Promise.resolve({}); }), - on: vi.fn(), + on: vi.fn(() => ({ unregister: vi.fn() })), }, }; }); @@ -59,6 +59,18 @@ vi.mock("@preact/signals", async (importOriginal) => { }; }); +// Optimize preact render in tests - use a faster synchronous render +vi.mock("preact", async (importOriginal) => { + const preact = await importOriginal(); + const originalRender = preact.render; + + // In tests, use original render but ensure it's synchronous where possible + return { + ...preact, + render: originalRender, + }; +}); + // TODO: rewrite this describe("getChildrenDirection", () => { let visualBuilderContainer: HTMLDivElement; @@ -322,7 +334,7 @@ describe("handleAddButtonsForMultiple", () => { } ); - await sleep(0); + // Buttons are appended synchronously const addInstanceButtons = visualBuilderContainer.querySelectorAll( `[data-testid="visual-builder-add-instance-button"]` ); @@ -345,8 +357,7 @@ describe("handleAddButtonsForMultiple", () => { label: undefined, } ); - await sleep(0); - + // Buttons are appended and positioned synchronously const addInstanceButtons = visualBuilderContainer.querySelectorAll( `[data-testid="visual-builder-add-instance-button"]` ); @@ -389,8 +400,7 @@ describe("handleAddButtonsForMultiple", () => { label: undefined, } ); - await sleep(0); - + // Buttons are appended and positioned synchronously const addInstanceButtons = visualBuilderContainer.querySelectorAll( `[data-testid="visual-builder-add-instance-button"]` ); @@ -486,7 +496,7 @@ describe("handleAddButtonsForMultiple", () => { } ); - await sleep(0); + // Buttons are appended synchronously const addInstanceButtons = visualBuilderContainer.querySelectorAll( `[data-testid="visual-builder-add-instance-button"]` ); @@ -560,11 +570,15 @@ describe("removeAddInstanceButtons", () => { let overlayWrapper: HTMLDivElement; let eventTarget: EventTarget; - beforeEach(() => { + // Shared container setup - run once + beforeAll(() => { visualBuilderContainer = document.createElement("div"); visualBuilderContainer.classList.add("visual-builder__container"); document.body.appendChild(visualBuilderContainer); + }); + beforeEach(() => { + // Only create buttons for each test (fast DOM operations) previousButton = generateAddInstanceButton({ fieldSchema: singleLineFieldSchema, // @ts-expect-error mock field metadata @@ -590,10 +604,16 @@ describe("removeAddInstanceButtons", () => { }); afterEach(() => { - document.getElementsByTagName("body")[0].innerHTML = ""; + // Only clean what we created in beforeEach + visualBuilderContainer.innerHTML = ""; vi.clearAllMocks(); }); + afterAll(() => { + // Clean up shared container + document.body.innerHTML = ""; + }); + test("should not remove buttons if wrapper or buttons are not present", () => { removeAddInstanceButtons({ visualBuilderContainer: null, @@ -676,6 +696,7 @@ describe("removeAddInstanceButtons", () => { visualBuilderContainer.appendChild(button); } + // Buttons are appended synchronously let buttons = visualBuilderContainer.querySelectorAll( `[data-testid="visual-builder-add-instance-button"]` ); @@ -711,11 +732,12 @@ describe("removeAddInstanceButtons", () => { visualBuilderContainer.appendChild(button); } - let buttons = visualBuilderContainer.querySelectorAll( + // Buttons are appended synchronously + const buttonsBeforeRemoval = visualBuilderContainer.querySelectorAll( `[data-testid="visual-builder-add-instance-button"]` ); - expect(buttons.length).toBe(7); + expect(buttonsBeforeRemoval.length).toBe(7); removeAddInstanceButtons( { @@ -726,10 +748,6 @@ describe("removeAddInstanceButtons", () => { false ); - buttons = visualBuilderContainer.querySelectorAll( - `[data-testid="visual-builder-add-instance-button"]` - ); - const addInstanceButtons = visualBuilderContainer.querySelectorAll( `[data-testid="visual-builder-add-instance-button"]` ); diff --git a/src/visualBuilder/utils/__test__/updateFocussedState.test.ts b/src/visualBuilder/utils/__test__/updateFocussedState.test.ts index 9051840f..ab8d7e51 100644 --- a/src/visualBuilder/utils/__test__/updateFocussedState.test.ts +++ b/src/visualBuilder/utils/__test__/updateFocussedState.test.ts @@ -4,34 +4,31 @@ import { updateFocussedStateOnMutation, } from "../updateFocussedState"; import { VisualBuilder } from "../.."; -import { - addFocusOverlay, - hideOverlay, -} from "../../generators/generateOverlay"; +import { addFocusOverlay, hideOverlay } from "../../generators/generateOverlay"; import { mockGetBoundingClientRect } from "../../../__test__/utils"; import { act } from "@testing-library/preact"; import { singleLineFieldSchema } from "../../../__test__/data/fields"; -import { getEntryPermissionsCached } from "../getEntryPermissionsCached"; -import { getWorkflowStageDetails } from "../getWorkflowStageDetails"; +import { fetchEntryPermissionsAndStageDetails } from "../fetchEntryPermissionsAndStageDetails"; import { isFieldDisabled } from "../isFieldDisabled"; +import { getEntryPermissionsCached } from "../getEntryPermissionsCached"; vi.mock("../../generators/generateOverlay", () => ({ addFocusOverlay: vi.fn(), hideOverlay: vi.fn(), })); -vi.mock("../getEntryPermissionsCached", () => ({ - getEntryPermissionsCached: vi.fn(), -})); - -vi.mock("../getWorkflowStageDetails", () => ({ - getWorkflowStageDetails: vi.fn(), +vi.mock("../fetchEntryPermissionsAndStageDetails", () => ({ + fetchEntryPermissionsAndStageDetails: vi.fn(), })); vi.mock("../../utils/isFieldDisabled", () => ({ isFieldDisabled: vi.fn().mockReturnValue({ isDisabled: false }), })); +vi.mock("../getEntryPermissionsCached", () => ({ + getEntryPermissionsCached: vi.fn(), +})); + vi.mock("../../utils/fieldSchemaMap", () => { return { FieldSchemaMap: { @@ -44,7 +41,6 @@ vi.mock("../../utils/fieldSchemaMap", () => { }; }); - describe("updateFocussedState", () => { beforeEach(() => { const previousSelectedEditableDOM = document.createElement("div"); @@ -55,7 +51,28 @@ describe("updateFocussedState", () => { document.body.appendChild(previousSelectedEditableDOM); VisualBuilder.VisualBuilderGlobalState.value.previousSelectedEditableDOM = previousSelectedEditableDOM; - vi.clearAllMocks(); + + // Set up default mock for fetchEntryPermissionsAndStageDetails for all tests + vi.mocked(fetchEntryPermissionsAndStageDetails).mockResolvedValue({ + acl: { + create: true, + read: true, + update: true, + delete: true, + publish: true, + }, + workflowStage: { + permissions: { + entry: { + update: true, + }, + }, + stage: undefined, + }, + resolvedVariantPermissions: { + update: true, + }, + }); }); afterEach(() => { document.body.innerHTML = ""; @@ -184,29 +201,29 @@ describe("updateFocussedState", () => { disconnect: vi.fn(), } as unknown as ResizeObserver; - const mockEntryPermissions = { - create: true, - read: true, - update: false, - delete: true, - publish: true, - }; - - const mockWorkflowStageDetails = { - permissions: { - entry: { - update: true, + const mockPermissionsResponse = { + acl: { + create: true, + read: true, + update: false, + delete: true, + publish: true, + }, + workflowStage: { + permissions: { + entry: { + update: true, + }, }, + stage: undefined, + }, + resolvedVariantPermissions: { + update: true, }, - stage: undefined, }; - vi.mocked(getEntryPermissionsCached).mockResolvedValue( - mockEntryPermissions - ); - - vi.mocked(getWorkflowStageDetails).mockResolvedValue( - mockWorkflowStageDetails + vi.mocked(fetchEntryPermissionsAndStageDetails).mockResolvedValue( + mockPermissionsResponse ); await act(async () => { @@ -219,10 +236,12 @@ describe("updateFocussedState", () => { }); }); - expect(getEntryPermissionsCached).toHaveBeenCalledWith({ + expect(fetchEntryPermissionsAndStageDetails).toHaveBeenCalledWith({ entryUid: "entry_uid", contentTypeUid: "content_type_uid", locale: "locale", + fieldPathWithIndex: "field_path", + variantUid: undefined, }); expect(isFieldDisabled).toHaveBeenCalledWith( @@ -233,10 +252,22 @@ describe("updateFocussedState", () => { }, { update: true, - error: true }, - mockEntryPermissions, - mockWorkflowStageDetails + { + create: true, + read: true, + update: false, + delete: true, + publish: true, + }, + { + permissions: { + entry: { + update: true, + }, + }, + stage: undefined, + } ); expect(addFocusOverlay).toHaveBeenCalledWith( @@ -257,12 +288,17 @@ describe("updateFocussedState", () => { } as unknown as ResizeObserver; const previousSelectedEditableDOM = document.createElement("div"); - previousSelectedEditableDOM.setAttribute("data-cslp", "content_type_uid.entry_uid.locale.field_path"); + previousSelectedEditableDOM.setAttribute( + "data-cslp", + "content_type_uid.entry_uid.locale.field_path" + ); document.body.appendChild(previousSelectedEditableDOM); VisualBuilder.VisualBuilderGlobalState.value.previousSelectedEditableDOM = previousSelectedEditableDOM; - document.querySelector = vi.fn().mockReturnValue(previousSelectedEditableDOM); + document.querySelector = vi + .fn() + .mockReturnValue(previousSelectedEditableDOM); const result = await updateFocussedState({ editableElement: editableElementMock, diff --git a/tsconfig.json b/tsconfig.json index f3c59ea8..cc0ee403 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -17,7 +17,7 @@ "skipLibCheck": true , "forceConsistentCasingInFileNames": true , "jsx": "react-jsx", - "module": "ESNext" + "module": "ESNext", }, "include": ["src"] } diff --git a/vitest.config.ts b/vitest.config.ts index d016ee6f..218f75b0 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -8,14 +8,60 @@ export default defineConfig({ }, environment: "jsdom", coverage: { - all: true, - reporter: ["text", "html", "clover", "json", "json-summary"], + provider: "v8", + // Only include source files - this is MUCH faster than all: true + include: ["src/**/*.{ts,tsx}"], + exclude: [ + "dist/**", + "**/*.d.ts", + "node_modules/**", + "**/*.types.ts", + "**/*.test.*", + "**/*.test.tsx", + "**/*.mock.*", + "**/__mocks__/**", + "**/__tests__/**", + "**/__test__/**", + "**/*.config.*", + "**/tsconfig.*", + "vitest.reporter.ts", + "vitest.setup.ts", + ], + // CRITICAL: Set to false - only analyze files that are actually imported/used + // This makes coverage 3x faster by skipping unused files + all: false, + clean: false, + // Explicitly set coverage output directory + reportsDirectory: "./coverage", + // Coverage reporters: Controls what format coverage reports are generated in + reporter: process.env.CI + ? ["json-summary", "json"] // Minimal: only json-summary for CI action, json for artifacts + : ["text", "html"], // Full reports locally + // Generate coverage even on test failures (needed for CI) reportOnFailure: true, }, globals: true, setupFiles: "./vitest.setup.ts", - retry: 2, - testTimeout: 30000, - hookTimeout: 30000, + // Timeouts - increased for CI to handle slower async operations + testTimeout: 100000, + hookTimeout: 100000, + teardownTimeout: 5000, + // Enable file parallelization + fileParallelism: true, + // Use threads pool for better performance on multi-core systems + pool: "threads", + // Set lower threshold to identify slow tests + slowTestThreshold: 6000, + // Isolate tests for better parallelization + isolate: true, + // Reduce overhead + css: false, + // Test reporters: Controls how test execution results are displayed/output + reporters: process.env.CI ? ["verbose"] : ["verbose", "html"], + outputFile: { + json: "./test-results.json", + junit: "./junit.xml", + html: "./test-reports/index.html", + }, }, }); diff --git a/vitest.setup.ts b/vitest.setup.ts index 457c3e87..e3fdf889 100644 --- a/vitest.setup.ts +++ b/vitest.setup.ts @@ -1,6 +1,43 @@ import { afterAll, afterEach, beforeAll, vi } from "vitest"; import { cleanup } from "@testing-library/preact"; import "@testing-library/jest-dom/vitest"; + +// IMPORTANT: vi.mock MUST be at top level - cannot be inside beforeAll or any function +vi.mock("./src/visualBuilder/utils/getEntryPermissionsCached", () => ({ + getEntryPermissionsCached: vi.fn().mockResolvedValue({ + read: true, + publish: true, + update: true, + delete: true, + }), +})); + +vi.mock( + "./src/visualBuilder/utils/fetchEntryPermissionsAndStageDetails", + () => ({ + fetchEntryPermissionsAndStageDetails: vi.fn().mockResolvedValue({ + acl: { + create: true, + read: true, + update: true, + delete: true, + publish: true, + }, + workflowStage: { + stage: undefined, + permissions: { + entry: { + update: true, + }, + }, + }, + resolvedVariantPermissions: { + update: true, + }, + }), + }) +); + beforeAll(() => { global.ResizeObserver = vi.fn().mockImplementation(() => ({ observe: vi.fn(), @@ -11,25 +48,14 @@ beforeAll(() => { global.MutationObserver = vi.fn().mockImplementation(() => ({ observe: vi.fn(), disconnect: vi.fn(), + takeRecords: vi.fn(() => []), })); document.elementFromPoint = vi.fn(); - - vi.mock("./src/visualBuilder/utils/getEntryPermissionsCached", () => { - return { - getEntryPermissionsCached: vi.fn().mockResolvedValue({ - read: true, - publish: true, - update: true, - delete: true, - }), - }; - }); }); afterAll(() => { cleanup(); - vi.clearAllMocks(); }); // const sideEffects = {