Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,8 @@ describe("Live Preview HOC init", () => {
expect(visualBuilderPostMessageSpy).toHaveBeenCalledWith('init', { isSSR: true, href: 'http://localhost:3000/' });
expect(visualBuilderPostMessageSpy).toHaveBeenCalledWith('send-variant-and-locale');
expect(visualBuilderPostMessageSpy).toHaveBeenCalledWith('get-highlight-variant-fields-status');
expect(visualBuilderPostMessageSpy).toHaveBeenCalledTimes(3);
expect(visualBuilderPostMessageSpy).toHaveBeenCalledWith('entries-in-current-page-changed', { entriesInCurrentPage: [] });
expect(visualBuilderPostMessageSpy).toHaveBeenCalledTimes(4);
});

test("should return the existing live preview instance if it is already initialized", async () => {
Expand Down
129 changes: 129 additions & 0 deletions src/visualBuilder/__test__/entriesInPage.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import { getAllContentTypes } from "../../__test__/data/contentType";
import { waitForBuilderSDKToBeInitialized } from "../../__test__/utils";
import Config from "../../configManager/configManager";
import { VisualBuilder } from "../index";
import { VisualBuilderPostMessageEvents } from "../utils/types/postMessage.types";
import visualBuilderPostMessage from "../utils/visualBuilderPostMessage";
import { Mock } from "vitest";

vi.mock("../utils/visualBuilderPostMessage", () => ({
__esModule: true,
default: {
send: vi.fn((eventName: string) =>
Promise.resolve(
eventName === "init"
? { contentTypes: getAllContentTypes() }
: {}
)
),
on: vi.fn(() => ({ unregister: vi.fn() })),
},
}));

vi.mock("../../utils/index.ts", async () => ({
__esModule: true,
...(await vi.importActual("../../utils")),
isOpenInBuilder: vi.fn().mockReturnValue(true),
}));

// Lives apart from index.test.ts: its click test leaves the document in a
// state where appending any `data-cslp` element stalls for 15 s.
describe("entries-in-current-page-changed", () => {
const CSLP = "page.blt0f6a4e1c9d2b7a53.fr-fr.title";
const ENTRY = {
entryUid: "blt0f6a4e1c9d2b7a53",
contentTypeUid: "page",
locale: "fr-fr",
};
const EVENT =
VisualBuilderPostMessageEvents.ENTRIES_IN_CURRENT_PAGE_CHANGED;
const send = visualBuilderPostMessage.send as Mock;
const entrySends = () =>
send.mock.calls.filter(([event]) => event === EVENT);
// The body observer is debounced at 100 ms.
const afterDebounce = () => new Promise((r) => setTimeout(r, 300));
Comment thread
sairaj-cs marked this conversation as resolved.

// The global MutationObserver is a stub, so the body observer's callback
// is captured while the builder constructs and fired by hand.
let bodyMutation: () => void = () => {};
const createBuilder = () => {
const Stub = global.MutationObserver;
global.MutationObserver = class {
constructor(private callback: MutationCallback) {}
observe = vi.fn((_: Node, options?: MutationObserverInit) => {
if (options?.attributeFilter?.includes("data-cslp")) {
bodyMutation = () =>
this.callback([], this as unknown as MutationObserver);
}
});
disconnect = vi.fn();
takeRecords = vi.fn(() => []);
} as unknown as typeof MutationObserver;
try {
return new VisualBuilder();
} finally {
global.MutationObserver = Stub;
}
};

beforeAll(() => Config.set("mode", 2));
beforeEach(() => {
Comment thread
sairaj-cs marked this conversation as resolved.
vi.clearAllMocks();
document.body.innerHTML = "";
});

test("sends again when an entry joins the page, not on unrelated mutations", async () => {
const x = createBuilder();
await waitForBuilderSDKToBeInitialized(visualBuilderPostMessage);
expect(entrySends()).toHaveLength(1);

const h1 = document.createElement("h1");
h1.setAttribute("data-cslp", CSLP);
document.body.appendChild(h1);
bodyMutation();
await afterDebounce();
expect(entrySends()).toHaveLength(2);
expect(entrySends()[1][1]).toEqual({ entriesInCurrentPage: [ENTRY] });

document.body.appendChild(document.createElement("div"));
bodyMutation();
await afterDebounce();
expect(entrySends()).toHaveLength(2);

x.destroy();
});

test("resends a set the editor did not receive", async () => {
const original = send.getMockImplementation()!;
send.mockImplementation((event: string, ...rest: unknown[]) =>
event === EVENT
? Promise.reject({ code: "NO_REQUEST_LISTENER_FOUND" })
: original(event, ...rest)
);
const x = createBuilder();
await waitForBuilderSDKToBeInitialized(visualBuilderPostMessage);
expect(entrySends()).toHaveLength(1);
send.mockImplementation(original);

bodyMutation();
await afterDebounce();
expect(entrySends()).toHaveLength(2);
expect(entrySends()[1][1]).toEqual({ entriesInCurrentPage: [] });

x.destroy();
});

test("destroy() drops a pending observer pass", async () => {
const x = createBuilder();
await waitForBuilderSDKToBeInitialized(visualBuilderPostMessage);
expect(entrySends()).toHaveLength(1);

const h1 = document.createElement("h1");
h1.setAttribute("data-cslp", CSLP);
document.body.appendChild(h1);
bodyMutation();
x.destroy();
await afterDebounce();
expect(entrySends()).toHaveLength(1);
});
});
11 changes: 11 additions & 0 deletions src/visualBuilder/__test__/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,17 @@ describe(
x.destroy();
});

test("should report an empty entry set once the builder is ready", async () => {
const x = new VisualBuilder();
await waitForBuilderSDKToBeInitialized(visualBuilderPostMessage);

expect(visualBuilderPostMessage.send).toHaveBeenCalledWith(
VisualBuilderPostMessageEvents.ENTRIES_IN_CURRENT_PAGE_CHANGED,
{ entriesInCurrentPage: [] }
);
x.destroy();
});
Comment thread
sairaj-cs marked this conversation as resolved.

describe("VisualBuilder init — early return conditions", () => {
afterEach(() => {
Config.set("mode", 2);
Expand Down
116 changes: 77 additions & 39 deletions src/visualBuilder/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,13 @@ import {
import { generateStartEditingButton } from "./generators/generateStartEditingButton";

import { addFocusOverlay } from "./generators/generateOverlay";
import { getEntryIdentifiersInCurrentPage } from "./utils/getEntryIdentifiersInCurrentPage";
import {
getEntryIdentifiersInCurrentPage,
getEntryIdentifiersSignature,
} from "./utils/getEntryIdentifiersInCurrentPage";
import { resolvePageContext } from "./utils/resolvePageContext";
import visualBuilderPostMessage from "./utils/visualBuilderPostMessage";
import { ignoreMissingListener } from "./utils/postMessageErrors";
import { VisualBuilderPostMessageEvents } from "./utils/types/postMessage.types";

import { debounce, isEqual } from "lodash-es";
Expand Down Expand Up @@ -206,50 +210,77 @@ export class VisualBuilder {
});
});

private mutationObserver = new MutationObserver(
debounce(
async () => {
updateFocussedStateOnMutation(
this.overlayWrapper,
this.focusedToolbar,
this.visualBuilderContainer,
this.resizeObserver
);
// null, not "": an empty page has signature "" and must still send once.
private lastEntriesSignature: string | null = null;

const emptyBlockParents = Array.from(
document.querySelectorAll(`.${VB_EmptyBlockParentClass}`)
);
/** Tell the editor which entries are on the page, only when the set changed. */
private notifyEntriesInPageIfChanged = (): void => {
const entries = getEntryIdentifiersInCurrentPage();
const signature = getEntryIdentifiersSignature(
entries.entriesInCurrentPage
);
if (signature === this.lastEntriesSignature) return;
this.lastEntriesSignature = signature;
visualBuilderPostMessage
?.send(
VisualBuilderPostMessageEvents.ENTRIES_IN_CURRENT_PAGE_CHANGED,
entries
)
.catch((error: unknown) => {
// Let the next observer pass resend a set the editor never got.
Comment thread
sairaj-cs marked this conversation as resolved.
if (this.lastEntriesSignature === signature) {
Comment thread
sairaj-cs marked this conversation as resolved.
this.lastEntriesSignature = null;
}
ignoreMissingListener(
VisualBuilderPostMessageEvents.ENTRIES_IN_CURRENT_PAGE_CHANGED
)(error);
});
};

const previousEmptyBlockParents = VisualBuilder
.VisualBuilderGlobalState.value
.previousEmptyBlockParents as Element[];

if (!isEqual(emptyBlockParents, previousEmptyBlockParents)) {
const noMoreEmptyBlockParent =
previousEmptyBlockParents.filter(
(x) => !emptyBlockParents.includes(x)
);
const newEmptyBlockParent = emptyBlockParents.filter(
(x) => !previousEmptyBlockParents.includes(x)
);
private onBodyMutation = debounce(
async () => {
updateFocussedStateOnMutation(
this.overlayWrapper,
this.focusedToolbar,
this.visualBuilderContainer,
this.resizeObserver
);
this.notifyEntriesInPageIfChanged();

removeEmptyBlocks(noMoreEmptyBlockParent);
await generateEmptyBlocks(newEmptyBlockParent);
const emptyBlockParents = Array.from(
document.querySelectorAll(`.${VB_EmptyBlockParentClass}`)
);

VisualBuilder.VisualBuilderGlobalState.value = {
...VisualBuilder.VisualBuilderGlobalState.value,
previousEmptyBlockParents: emptyBlockParents,
};
}
if (VisualBuilder.VisualBuilderGlobalState.value.variant && VisualBuilder.VisualBuilderGlobalState.value.highlightVariantFields) {
debounceAddVariantFieldClass(VisualBuilder.VisualBuilderGlobalState.value.variant);
}
},
100,
{ trailing: true }
)
const previousEmptyBlockParents = VisualBuilder
.VisualBuilderGlobalState.value
.previousEmptyBlockParents as Element[];

if (!isEqual(emptyBlockParents, previousEmptyBlockParents)) {
const noMoreEmptyBlockParent = previousEmptyBlockParents.filter(
(x) => !emptyBlockParents.includes(x)
);
const newEmptyBlockParent = emptyBlockParents.filter(
(x) => !previousEmptyBlockParents.includes(x)
);

removeEmptyBlocks(noMoreEmptyBlockParent);
await generateEmptyBlocks(newEmptyBlockParent);

VisualBuilder.VisualBuilderGlobalState.value = {
...VisualBuilder.VisualBuilderGlobalState.value,
previousEmptyBlockParents: emptyBlockParents,
};
}
if (VisualBuilder.VisualBuilderGlobalState.value.variant && VisualBuilder.VisualBuilderGlobalState.value.highlightVariantFields) {
debounceAddVariantFieldClass(VisualBuilder.VisualBuilderGlobalState.value.variant);
}
},
100,
{ trailing: true }
);

private mutationObserver = new MutationObserver(this.onBodyMutation);
Comment thread
sairaj-cs marked this conversation as resolved.

private threadMutationObserver = new MutationObserver(
debounce(() => {
const container = document.querySelector(
Expand Down Expand Up @@ -366,9 +397,13 @@ export class VisualBuilder {
useScrollToField();
useHighlightCommentIcon();

// Frameworks reuse nodes and rewrite data-cslp in place (variant
// switch, re-keyed lists), which childList alone never reports.
this.mutationObserver.observe(document.body, {
childList: true,
subtree: true,
attributes: true,
Comment thread
sairaj-cs marked this conversation as resolved.
attributeFilter: ["data-cslp"],
});

getHighlightVariantFieldsStatus().then((result) => {
Expand All @@ -378,6 +413,7 @@ export class VisualBuilder {
VisualBuilderPostMessageEvents.GET_ALL_ENTRIES_IN_CURRENT_PAGE,
getEntryIdentifiersInCurrentPage
);
this.notifyEntriesInPageIfChanged();
visualBuilderPostMessage?.send(
VisualBuilderPostMessageEvents.SEND_VARIANT_AND_LOCALE
);
Expand Down Expand Up @@ -440,6 +476,8 @@ export class VisualBuilder {
// Disconnect observers
this.resizeObserver.disconnect();
this.mutationObserver.disconnect();
// disconnect() leaves a pending debounced pass that would still post to the editor.
this.onBodyMutation.cancel();
this.threadMutationObserver.disconnect();

// Clear global state
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { JSDOM } from "jsdom";
import { getEntryIdentifiersInCurrentPage } from "../getEntryIdentifiersInCurrentPage";
import {
getEntryIdentifiersInCurrentPage,
getEntryIdentifiersSignature,
} from "../getEntryIdentifiersInCurrentPage";

const dom = new JSDOM(`
<div>
Expand All @@ -23,8 +26,8 @@ const domWithNoCslp = new JSDOM(`
`);

describe("getEntryIdentifiersInCurrentPage", () => {
test('should return an empty array if no elements with data-cslp attribute are found', () => {
document.body.innerHTML = '';
test("should return an empty array if no elements with data-cslp attribute are found", () => {
document.body.innerHTML = "";
const result = getEntryIdentifiersInCurrentPage();
expect(result.entriesInCurrentPage).toEqual([]);
});
Expand Down Expand Up @@ -58,6 +61,24 @@ describe("getEntryIdentifiersInCurrentPage", () => {
`;
const { entriesInCurrentPage } = getEntryIdentifiersInCurrentPage();
expect(entriesInCurrentPage.length).toBe(1);
expect(entriesInCurrentPage[0].entryUid).toBe('bltf5bb5f8fb088a332');
expect(entriesInCurrentPage[0].entryUid).toBe("bltf5bb5f8fb088a332");
});
});

describe("getEntryIdentifiersSignature", () => {
test("should be order independent and change with the set", () => {
const a = { entryUid: "blt1", contentTypeUid: "page", locale: "en-us" };
const b = {
entryUid: "blt2",
contentTypeUid: "header",
locale: "en-us",
};
expect(getEntryIdentifiersSignature([a, b])).toBe(
getEntryIdentifiersSignature([b, a])
);
expect(getEntryIdentifiersSignature([a])).not.toBe(
getEntryIdentifiersSignature([a, b])
);
expect(getEntryIdentifiersSignature([])).toBe("");
});
});
Loading
Loading