Skip to content

Commit 87f86a5

Browse files
Merge pull request #492 from contentstack/live-preview-outside-iframe
Live preview outside iframe
2 parents d6b9d01 + 42ee5c1 commit 87f86a5

10 files changed

Lines changed: 762 additions & 16 deletions

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ Alternatively, if you want to include the package directly in your website HTML
1616

1717
```html
1818
<script type='module' integrity='sha384-b6G+ggU20rGxqCqsgaS6zludFgj5N11xsuXhMEIARMuQY2PtyDS04TU0H5goP+32' crossorigin="anonymous">
19-
import ContentstackLivePreview from 'https://esm.sh/@contentstack/live-preview-utils@3.4.0';
19+
import ContentstackLivePreview from 'https://esm.sh/@contentstack/live-preview-utils@4.0.0';
2020
2121
ContentstackLivePreview.init({
2222
stackDetails: {

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@contentstack/live-preview-utils",
3-
"version": "3.4.0",
3+
"version": "4.0.0",
44
"description": "Contentstack provides the Live Preview SDK to establish a communication channel between the various Contentstack SDKs and your website, transmitting live changes to the preview pane.",
55
"type": "module",
66
"types": "dist/legacy/index.d.ts",

src/common/inIframe.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,20 @@
1+
import { hasWindow } from "../utils";
2+
13
export function inIframe(): boolean {
24
try {
35
return window.self !== window.top;
46
} catch (e) {
57
return true;
68
}
79
}
10+
11+
export function isOpeningInNewTab(): boolean {
12+
try {
13+
if(hasWindow()) {
14+
return !!window.opener;
15+
}
16+
return false;
17+
} catch (e) {
18+
return false;
19+
}
20+
}

src/livePreview/editButton/editButton.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { effect } from "@preact/signals";
2-
import { inIframe } from "../../common/inIframe";
2+
import { inIframe, isOpeningInNewTab } from "../../common/inIframe";
33
import Config from "../../configManager/configManager";
44
import { addCslpOutline, extractDetailsFromCslp } from "../../cslp";
55
import { cslpTagStyles } from "./editButton.style";
@@ -448,7 +448,7 @@ export class LivePreviewEditButton {
448448
fieldPathWithIndex,
449449
} = extractDetailsFromCslp(cslpTag);
450450

451-
if (inIframe()) {
451+
if (inIframe() || isOpeningInNewTab()) {
452452
livePreviewPostMessage?.send("scroll", {
453453
field: fieldPathWithIndex,
454454
content_type_uid,
Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
/**
2+
* @vitest-environment jsdom
3+
*/
4+
5+
import { vi } from "vitest";
6+
import { EventManager } from "@contentstack/advanced-post-message";
7+
import { LIVE_PREVIEW_CHANNEL_ID } from "../livePreviewEventManager.constant";
8+
9+
// Mock dependencies
10+
vi.mock("@contentstack/advanced-post-message", () => ({
11+
EventManager: vi.fn(),
12+
}));
13+
14+
vi.mock("../../../common/inIframe", () => ({
15+
isOpeningInNewTab: vi.fn(),
16+
}));
17+
18+
// Import after mocking
19+
import { isOpeningInNewTab } from "../../../common/inIframe";
20+
21+
describe("livePreviewEventManager", () => {
22+
let mockEventManager: any;
23+
let originalWindow: any;
24+
25+
beforeEach(() => {
26+
// Reset all mocks
27+
vi.clearAllMocks();
28+
29+
// Create mock EventManager
30+
mockEventManager = {
31+
on: vi.fn(),
32+
send: vi.fn(),
33+
};
34+
(EventManager as any).mockImplementation(() => mockEventManager);
35+
36+
// Store original window
37+
originalWindow = global.window;
38+
39+
// Reset isOpeningInNewTab mock
40+
(isOpeningInNewTab as any).mockReturnValue(false);
41+
});
42+
43+
afterEach(() => {
44+
// Restore original window
45+
global.window = originalWindow;
46+
47+
// Clear module cache to reset the module state
48+
vi.resetModules();
49+
});
50+
51+
describe("when window is undefined", () => {
52+
beforeEach(() => {
53+
// Mock window as undefined
54+
Object.defineProperty(global, "window", {
55+
value: undefined,
56+
writable: true,
57+
});
58+
});
59+
60+
it("should not initialize EventManager when window is undefined", async () => {
61+
// Re-import the module to trigger initialization
62+
const module = await import("../livePreviewEventManager");
63+
64+
expect(EventManager).not.toHaveBeenCalled();
65+
expect(module.default).toBeUndefined();
66+
});
67+
});
68+
69+
describe("when window is defined", () => {
70+
let mockWindow: any;
71+
72+
beforeEach(() => {
73+
// Create mock window object
74+
mockWindow = {
75+
parent: { postMessage: vi.fn() },
76+
opener: { postMessage: vi.fn() },
77+
};
78+
79+
Object.defineProperty(global, "window", {
80+
value: mockWindow,
81+
writable: true,
82+
});
83+
});
84+
85+
it("should initialize EventManager with window.parent as target when not in new tab", async () => {
86+
(isOpeningInNewTab as any).mockReturnValue(false);
87+
88+
// Re-import the module to trigger initialization
89+
const module = await import("../livePreviewEventManager");
90+
91+
expect(EventManager).toHaveBeenCalledWith(LIVE_PREVIEW_CHANNEL_ID, {
92+
target: mockWindow.parent,
93+
debug: false,
94+
suppressErrors: true,
95+
});
96+
expect(module.default).toBe(mockEventManager);
97+
});
98+
99+
it("should initialize EventManager with window.opener as target when in new tab", async () => {
100+
(isOpeningInNewTab as any).mockReturnValue(true);
101+
102+
// Re-import the module to trigger initialization
103+
const module = await import("../livePreviewEventManager");
104+
105+
expect(EventManager).toHaveBeenCalledWith(LIVE_PREVIEW_CHANNEL_ID, {
106+
target: mockWindow.opener,
107+
debug: false,
108+
suppressErrors: true,
109+
});
110+
expect(module.default).toBe(mockEventManager);
111+
});
112+
113+
it("should call isOpeningInNewTab to determine the target", async () => {
114+
// Re-import the module to trigger initialization
115+
await import("../livePreviewEventManager");
116+
117+
expect(isOpeningInNewTab).toHaveBeenCalled();
118+
});
119+
120+
it("should use correct channel ID", async () => {
121+
// Re-import the module to trigger initialization
122+
await import("../livePreviewEventManager");
123+
124+
expect(EventManager).toHaveBeenCalledWith(
125+
LIVE_PREVIEW_CHANNEL_ID,
126+
expect.any(Object)
127+
);
128+
});
129+
130+
it("should set correct default event options", async () => {
131+
(isOpeningInNewTab as any).mockReturnValue(false);
132+
133+
// Re-import the module to trigger initialization
134+
await import("../livePreviewEventManager");
135+
136+
expect(EventManager).toHaveBeenCalledWith(
137+
expect.any(String),
138+
expect.objectContaining({
139+
debug: false,
140+
suppressErrors: true,
141+
})
142+
);
143+
});
144+
145+
describe("target selection logic", () => {
146+
it("should prioritize window.opener when isOpeningInNewTab returns true", async () => {
147+
(isOpeningInNewTab as any).mockReturnValue(true);
148+
149+
// Re-import the module to trigger initialization
150+
await import("../livePreviewEventManager");
151+
152+
const callArgs = (EventManager as any).mock.calls[0];
153+
expect(callArgs[1].target).toBe(mockWindow.opener);
154+
expect(callArgs[1].target).not.toBe(mockWindow.parent);
155+
});
156+
157+
it("should use window.parent when isOpeningInNewTab returns false", async () => {
158+
(isOpeningInNewTab as any).mockReturnValue(false);
159+
160+
// Re-import the module to trigger initialization
161+
await import("../livePreviewEventManager");
162+
163+
const callArgs = (EventManager as any).mock.calls[0];
164+
expect(callArgs[1].target).toBe(mockWindow.parent);
165+
expect(callArgs[1].target).not.toBe(mockWindow.opener);
166+
});
167+
168+
it("should throw error when isOpeningInNewTab throws an error", async () => {
169+
(isOpeningInNewTab as any).mockImplementation(() => {
170+
throw new Error("isOpeningInNewTab error");
171+
});
172+
173+
// Should throw because isOpeningInNewTab error is not caught in the implementation
174+
await expect(async () => {
175+
await import("../livePreviewEventManager");
176+
}).rejects.toThrow("isOpeningInNewTab error");
177+
});
178+
});
179+
180+
describe("edge cases", () => {
181+
it("should handle missing window.parent gracefully", async () => {
182+
mockWindow.parent = undefined;
183+
(isOpeningInNewTab as any).mockReturnValue(false);
184+
185+
// Re-import the module to trigger initialization
186+
const module = await import("../livePreviewEventManager");
187+
188+
expect(EventManager).toHaveBeenCalledWith(LIVE_PREVIEW_CHANNEL_ID, {
189+
target: undefined,
190+
debug: false,
191+
suppressErrors: true,
192+
});
193+
expect(module.default).toBe(mockEventManager);
194+
});
195+
196+
it("should handle missing window.opener gracefully", async () => {
197+
mockWindow.opener = undefined;
198+
(isOpeningInNewTab as any).mockReturnValue(true);
199+
200+
// Re-import the module to trigger initialization
201+
const module = await import("../livePreviewEventManager");
202+
203+
expect(EventManager).toHaveBeenCalledWith(LIVE_PREVIEW_CHANNEL_ID, {
204+
target: undefined,
205+
debug: false,
206+
suppressErrors: true,
207+
});
208+
expect(module.default).toBe(mockEventManager);
209+
});
210+
211+
it("should handle when EventManager constructor throws", async () => {
212+
(EventManager as any).mockImplementation(() => {
213+
throw new Error("EventManager constructor error");
214+
});
215+
216+
// Should not crash the module initialization
217+
expect(async () => {
218+
await import("../livePreviewEventManager");
219+
}).not.toThrow();
220+
});
221+
});
222+
});
223+
224+
describe("module export", () => {
225+
it("should export the EventManager instance when window is available", async () => {
226+
const mockWindow = {
227+
parent: { postMessage: vi.fn() },
228+
opener: { postMessage: vi.fn() },
229+
};
230+
231+
Object.defineProperty(global, "window", {
232+
value: mockWindow,
233+
writable: true,
234+
});
235+
236+
const module = await import("../livePreviewEventManager");
237+
238+
expect(module.default).toBe(mockEventManager);
239+
});
240+
241+
it("should export undefined when window is not available", async () => {
242+
Object.defineProperty(global, "window", {
243+
value: undefined,
244+
writable: true,
245+
});
246+
247+
const module = await import("../livePreviewEventManager");
248+
249+
expect(module.default).toBeUndefined();
250+
});
251+
});
252+
});

0 commit comments

Comments
 (0)