Skip to content
Open
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
225 changes: 225 additions & 0 deletions src/livePreview/__test__/live-preview.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,19 @@ import Config from "../../configManager/configManager";
import { PublicLogger } from "../../logger/logger";
import { ILivePreviewWindowType } from "../../types/types";
import { addLivePreviewQueryTags } from '../../utils/addLivePreviewQueryTags';
import * as utils from "../../utils";
import livePreviewPostMessage from "../eventManager/livePreviewEventManager";
import { LIVE_PREVIEW_POST_MESSAGE_EVENTS } from "../eventManager/livePreviewEventManager.constant";
import {
HistoryLivePreviewPostMessageEventData,
OnChangeLivePreviewPostMessageEventData,
} from "../eventManager/types/livePreviewPostMessageEvent.type";
import LivePreview from "../live-preview";
import * as postMessageHooks from "../eventManager/postMessageEvent.hooks";
import { LivePreviewEditButton } from "../editButton/editButton";
import { mockLivePreviewInitEventListener } from "./mock";


vi.mock("../../utils/addLivePreviewQueryTags", () => ({
addLivePreviewQueryTags: vi.fn(),
}));
Expand Down Expand Up @@ -543,3 +547,224 @@ describe("testing window event listeners", () => {
expect(addLivePreviewQueryTags).toBeCalled();
});
});

describe("enable=false early return", () => {
beforeEach(() => {
Config.reset();
livePreviewPostMessage?.destroy({ soft: true });
livePreviewPostMessage?.on(
LIVE_PREVIEW_POST_MESSAGE_EVENTS.INIT,
mockLivePreviewInitEventListener
);
});

afterEach(() => {
document.getElementsByTagName("html")[0].innerHTML = "";
vi.restoreAllMocks();
});

afterAll(() => {
Config.reset();
livePreviewPostMessage?.destroy({ soft: true });
});

test("should not call sendInitializeLivePreviewPostMessageEvent when enable is false", () => {
const sendInitSpy = vi.spyOn(
postMessageHooks,
"sendInitializeLivePreviewPostMessageEvent"
);
Config.replace({ enable: false, cleanCslpOnProduction: false });

new LivePreview();

expect(sendInitSpy).not.toHaveBeenCalled();
});

test("should call sendInitializeLivePreviewPostMessageEvent when enable is true", () => {
const sendInitSpy = vi.spyOn(
postMessageHooks,
"sendInitializeLivePreviewPostMessageEvent"
);
Config.replace({ enable: true });

new LivePreview();

expect(sendInitSpy).toHaveBeenCalled();
});

test("should not create LivePreviewEditButton when enable is false", () => {
Config.replace({
enable: false,
editButton: { enable: true },
});

new LivePreview();

expect(document.getElementById("cslp-tooltip")).toBeNull();
});

test("should not create LivePreviewEditButton when enable is false even if mode is builder", () => {
Config.replace({
enable: false,
mode: "builder",
stackDetails: { environment: "preview", apiKey: "test-key" },
});

new LivePreview();

expect(document.getElementById("cslp-tooltip")).toBeNull();
});
});

describe("LivePreview edit button condition", () => {
beforeEach(() => {
Config.reset();
LivePreviewEditButton.livePreviewEditButton = undefined as any;
livePreviewPostMessage?.destroy({ soft: true });
livePreviewPostMessage?.on(
LIVE_PREVIEW_POST_MESSAGE_EVENTS.INIT,
mockLivePreviewInitEventListener
);
vi.spyOn(utils, "isOpeningInTimeline").mockReturnValue(false);
});

afterEach(() => {
document.getElementsByTagName("html")[0].innerHTML = "";
vi.restoreAllMocks();
});

afterAll(() => {
Config.reset();
livePreviewPostMessage?.destroy({ soft: true });
});

test("should not create LivePreviewEditButton when editButton.enable is false and mode is preview", () => {
Config.replace({
enable: true,
editButton: { enable: false },
mode: "preview",
});

new LivePreview();

expect(LivePreviewEditButton.livePreviewEditButton).toBeUndefined();
});

test("should instantiate LivePreviewEditButton when mode is builder even if editButton.enable is false", () => {
LivePreviewEditButton.livePreviewEditButton = undefined as any;
Config.replace({
enable: true,
editButton: { enable: false },
mode: "builder",
stackDetails: { environment: "preview", apiKey: "test-key" },
});

new LivePreview();

expect(LivePreviewEditButton.livePreviewEditButton).toBeDefined();
});

test("should not create LivePreviewEditButton when isOpeningInTimeline returns true", () => {
vi.spyOn(utils, "isOpeningInTimeline").mockReturnValue(true);
Config.replace({
enable: true,
editButton: { enable: true },
});

new LivePreview();

expect(document.getElementById("cslp-tooltip")).toBeNull();
});
});

describe("multiple LivePreview inits", () => {
beforeEach(() => {
Config.reset();
livePreviewPostMessage?.destroy({ soft: true });
livePreviewPostMessage?.on(
LIVE_PREVIEW_POST_MESSAGE_EVENTS.INIT,
mockLivePreviewInitEventListener
);
});

afterEach(() => {
document.getElementsByTagName("html")[0].innerHTML = "";
vi.restoreAllMocks();
});

afterAll(() => {
Config.reset();
livePreviewPostMessage?.destroy({ soft: true });
});

test("should not throw when constructed multiple times", () => {
Config.replace({ enable: true });

expect(() => {
new LivePreview();
new LivePreview();
}).not.toThrow();
});

test("should have at most one edit button in the DOM after multiple inits", () => {
vi.spyOn(utils, "isOpeningInTimeline").mockReturnValue(false);
Config.replace({
enable: true,
editButton: { enable: true },
});

new LivePreview();
new LivePreview();

const buttons = document.querySelectorAll("#cslp-tooltip");
expect(buttons.length).toBe(1);
});
});

describe("LivePreview init with partial stackDetails", () => {
beforeEach(() => {
Config.reset();
livePreviewPostMessage?.destroy({ soft: true });
livePreviewPostMessage?.on(
LIVE_PREVIEW_POST_MESSAGE_EVENTS.INIT,
mockLivePreviewInitEventListener
);
});

afterEach(() => {
document.getElementsByTagName("html")[0].innerHTML = "";
vi.restoreAllMocks();
});

afterAll(() => {
Config.reset();
livePreviewPostMessage?.destroy({ soft: true });
});

test("should not throw when constructed with partial stackDetails", () => {
Config.replace({
enable: true,
stackDetails: {
apiKey: "partial-key",
} as any,
});

expect(() => new LivePreview()).not.toThrow();
});

test("should preserve provided stackDetails fields and retain defaults for omitted ones", () => {
Config.replace({
enable: true,
stackDetails: {
apiKey: "my-api-key",
} as any,
});

new LivePreview();

const { stackDetails } = Config.get();
expect(stackDetails.apiKey).toBe("my-api-key");
expect(stackDetails.locale).toBe("en-us");
expect(stackDetails.masterLocale).toBe("en-us");
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ import {
useHistoryPostMessageEvent,
sendInitializeLivePreviewPostMessageEvent,
} from "../postMessageEvent.hooks";
import { isOpeningInNewTab } from "../../../common/inIframe";
import { isOpeningInNewTab, inVisualEditor } from "../../../common/inIframe";
import { addParamsToUrl } from "../../../utils";
import { ILivePreviewWindowType } from "../../../types/types";

// Mock dependencies
Expand Down Expand Up @@ -678,6 +679,31 @@ describe("postMessageEvent.hooks", () => {
expect(Config.set).not.toHaveBeenCalled();
});

it("should return early and skip post-init setup when inVisualEditor is true", async () => {
mockConfig = {
ssr: true,
mode: 1,
};
(Config.get as any).mockReturnValue(mockConfig);
(livePreviewPostMessage as any).send.mockResolvedValue({
windowType: ILivePreviewWindowType.PREVIEW,
contentTypeUid: "blog",
entryUid: "entry-123",
});
(inVisualEditor as any).mockReturnValueOnce(true);

await sendInitializeLivePreviewPostMessageEvent();
await Promise.resolve();

expect(setConfigFromParams).not.toHaveBeenCalled();
expect(Config.set).not.toHaveBeenCalled();
expect(addParamsToUrl).not.toHaveBeenCalled();
expect(livePreviewPostMessage?.on).not.toHaveBeenCalledWith(
LIVE_PREVIEW_POST_MESSAGE_EVENTS.ON_CHANGE,
expect.any(Function),
);
});

it("should start CHECK_ENTRY_PAGE interval when ssr is false", async () => {
vi.useFakeTimers();
try {
Expand Down
48 changes: 48 additions & 0 deletions src/visualBuilder/eventManager/__test__/useCollab.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -410,3 +410,51 @@ describe("cleanup", () => {
});
});
});

// Integration tests using real Config to document the intentional read-only isolation design.
// Share recipients receive COLLAB_ENABLE with fromShare:true, which sets pauseFeedback and
// isFeedbackMode but intentionally does NOT set collab.enable. As a result, share recipients
// never enter the collab block in mouseClick.ts (guarded by `collab.enable === true`) and
// bypass the full collab flow by design.
describe("read-only isolation — fromShare integration", () => {
let realConfig: { get: () => any; set: (key: string, value: any) => void; reset: () => void };
let collabEnableHandler: (data: any) => void;
let collabDisableHandler: (data: any) => void;

beforeEach(async () => {
vi.clearAllMocks();
const realConfigModule = await vi.importActual<{ default: typeof realConfig }>(
"../../../configManager/configManager"
);
realConfig = realConfigModule.default;
realConfig.reset();
vi.mocked(Config.set).mockImplementation((key, value) => realConfig.set(key, value));
vi.mocked(Config.get).mockImplementation(() => realConfig.get());
useCollab();
collabEnableHandler = getHandler(0);
collabDisableHandler = getHandler(2);
});

afterEach(() => {
realConfig.reset();
vi.mocked(Config.get).mockReset();
vi.mocked(Config.set).mockReset();
});

it("should set pauseFeedback but not enable after COLLAB_ENABLE with fromShare:true", () => {
collabEnableHandler({
data: { collab: { fromShare: true, pauseFeedback: true, isFeedbackMode: true } },
});

expect(realConfig.get().collab.pauseFeedback).toBe(true);
expect(realConfig.get().collab.enable).not.toBe(true);
});

it("should set collab.pauseFeedback when COLLAB_DISABLE fires with fromShare:true", () => {
collabDisableHandler({
data: { collab: { fromShare: true, pauseFeedback: true } },
});

expect(realConfig.get().collab.pauseFeedback).toBe(true);
});
});
Loading
Loading