Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
265 changes: 174 additions & 91 deletions src/visualBuilder/components/__test__/fieldLabelWrapper.test.tsx

Large diffs are not rendered by default.

22 changes: 17 additions & 5 deletions src/visualBuilder/components/fieldLabelWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ function FieldLabelWrapperComponent(
getReferenceParentMap()
]);
const entryUid = props.fieldMetadata.entry_uid;

const referenceData = referenceParentMap[entryUid];
const isReference = !!referenceData;

Expand Down Expand Up @@ -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,
}
);
}
}}
>
<InfoIcon />
</div>
Expand Down Expand Up @@ -303,11 +315,11 @@ function FieldLabelWrapperComponent(
>
{
currentField.isReference && !dataLoading && !error ?
<div
className={classNames(
"visual-builder__reference-icon-container",
<div
className={classNames(
"visual-builder__reference-icon-container",
visualBuilderStyles()["visual-builder__reference-icon-container"]
)}
)}
>
<div
className={classNames(
Expand Down
Original file line number Diff line number Diff line change
@@ -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();
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
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";

/**
* 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.
*/
async function handleRevalidateFieldData(): Promise<void> {
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) {
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();
return;
} catch (clearError) {
console.error("Failed to clear field schema cache:", clearError);
}

// Fallback 2: Refresh the entire iframe
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
);
}
Loading
Loading