Skip to content

Commit b50c43e

Browse files
fix: resolve new tab bugs
1 parent ab113eb commit b50c43e

6 files changed

Lines changed: 106 additions & 46 deletions

File tree

src/common/inIframe.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { hasWindow } from "../utils";
2+
13
export function inIframe(): boolean {
24
try {
35
return window.self !== window.top;
@@ -6,9 +8,12 @@ export function inIframe(): boolean {
68
}
79
}
810

9-
export function inNewTab(): boolean {
11+
export function isOpeningInNewTab(): boolean {
1012
try {
11-
return !!window.opener;
13+
if(hasWindow()) {
14+
return !!window.opener;
15+
}
16+
return false;
1217
} catch (e) {
1318
return false;
1419
}

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, inNewTab } 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() || inNewTab()) {
451+
if (inIframe() || isOpeningInNewTab()) {
452452
livePreviewPostMessage?.send("scroll", {
453453
field: fieldPathWithIndex,
454454
content_type_uid,

src/livePreview/eventManager/__test__/livePreviewEventManager.test.ts

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,11 @@ vi.mock("@contentstack/advanced-post-message", () => ({
1212
}));
1313

1414
vi.mock("../../../common/inIframe", () => ({
15-
inNewTab: vi.fn(),
15+
isOpeningInNewTab: vi.fn(),
1616
}));
1717

1818
// Import after mocking
19-
import { inNewTab } from "../../../common/inIframe";
19+
import { isOpeningInNewTab } from "../../../common/inIframe";
2020

2121
describe("livePreviewEventManager", () => {
2222
let mockEventManager: any;
@@ -36,8 +36,8 @@ describe("livePreviewEventManager", () => {
3636
// Store original window
3737
originalWindow = global.window;
3838

39-
// Reset inNewTab mock
40-
(inNewTab as any).mockReturnValue(false);
39+
// Reset isOpeningInNewTab mock
40+
(isOpeningInNewTab as any).mockReturnValue(false);
4141
});
4242

4343
afterEach(() => {
@@ -83,7 +83,7 @@ describe("livePreviewEventManager", () => {
8383
});
8484

8585
it("should initialize EventManager with window.parent as target when not in new tab", async () => {
86-
(inNewTab as any).mockReturnValue(false);
86+
(isOpeningInNewTab as any).mockReturnValue(false);
8787

8888
// Re-import the module to trigger initialization
8989
const module = await import("../livePreviewEventManager");
@@ -97,7 +97,7 @@ describe("livePreviewEventManager", () => {
9797
});
9898

9999
it("should initialize EventManager with window.opener as target when in new tab", async () => {
100-
(inNewTab as any).mockReturnValue(true);
100+
(isOpeningInNewTab as any).mockReturnValue(true);
101101

102102
// Re-import the module to trigger initialization
103103
const module = await import("../livePreviewEventManager");
@@ -110,11 +110,11 @@ describe("livePreviewEventManager", () => {
110110
expect(module.default).toBe(mockEventManager);
111111
});
112112

113-
it("should call inNewTab to determine the target", async () => {
113+
it("should call isOpeningInNewTab to determine the target", async () => {
114114
// Re-import the module to trigger initialization
115115
await import("../livePreviewEventManager");
116116

117-
expect(inNewTab).toHaveBeenCalled();
117+
expect(isOpeningInNewTab).toHaveBeenCalled();
118118
});
119119

120120
it("should use correct channel ID", async () => {
@@ -128,7 +128,7 @@ describe("livePreviewEventManager", () => {
128128
});
129129

130130
it("should set correct default event options", async () => {
131-
(inNewTab as any).mockReturnValue(false);
131+
(isOpeningInNewTab as any).mockReturnValue(false);
132132

133133
// Re-import the module to trigger initialization
134134
await import("../livePreviewEventManager");
@@ -143,8 +143,8 @@ describe("livePreviewEventManager", () => {
143143
});
144144

145145
describe("target selection logic", () => {
146-
it("should prioritize window.opener when inNewTab returns true", async () => {
147-
(inNewTab as any).mockReturnValue(true);
146+
it("should prioritize window.opener when isOpeningInNewTab returns true", async () => {
147+
(isOpeningInNewTab as any).mockReturnValue(true);
148148

149149
// Re-import the module to trigger initialization
150150
await import("../livePreviewEventManager");
@@ -154,8 +154,8 @@ describe("livePreviewEventManager", () => {
154154
expect(callArgs[1].target).not.toBe(mockWindow.parent);
155155
});
156156

157-
it("should use window.parent when inNewTab returns false", async () => {
158-
(inNewTab as any).mockReturnValue(false);
157+
it("should use window.parent when isOpeningInNewTab returns false", async () => {
158+
(isOpeningInNewTab as any).mockReturnValue(false);
159159

160160
// Re-import the module to trigger initialization
161161
await import("../livePreviewEventManager");
@@ -165,22 +165,22 @@ describe("livePreviewEventManager", () => {
165165
expect(callArgs[1].target).not.toBe(mockWindow.opener);
166166
});
167167

168-
it("should throw error when inNewTab throws an error", async () => {
169-
(inNewTab as any).mockImplementation(() => {
170-
throw new Error("inNewTab error");
168+
it("should throw error when isOpeningInNewTab throws an error", async () => {
169+
(isOpeningInNewTab as any).mockImplementation(() => {
170+
throw new Error("isOpeningInNewTab error");
171171
});
172172

173-
// Should throw because inNewTab error is not caught in the implementation
173+
// Should throw because isOpeningInNewTab error is not caught in the implementation
174174
await expect(async () => {
175175
await import("../livePreviewEventManager");
176-
}).rejects.toThrow("inNewTab error");
176+
}).rejects.toThrow("isOpeningInNewTab error");
177177
});
178178
});
179179

180180
describe("edge cases", () => {
181181
it("should handle missing window.parent gracefully", async () => {
182182
mockWindow.parent = undefined;
183-
(inNewTab as any).mockReturnValue(false);
183+
(isOpeningInNewTab as any).mockReturnValue(false);
184184

185185
// Re-import the module to trigger initialization
186186
const module = await import("../livePreviewEventManager");
@@ -195,7 +195,7 @@ describe("livePreviewEventManager", () => {
195195

196196
it("should handle missing window.opener gracefully", async () => {
197197
mockWindow.opener = undefined;
198-
(inNewTab as any).mockReturnValue(true);
198+
(isOpeningInNewTab as any).mockReturnValue(true);
199199

200200
// Re-import the module to trigger initialization
201201
const module = await import("../livePreviewEventManager");

src/livePreview/eventManager/__test__/postMessageEvent.hooks.test.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
useOnEntryUpdatePostMessageEvent,
1717
useHistoryPostMessageEvent,
1818
} from "../postMessageEvent.hooks";
19+
import { isOpeningInNewTab } from "../../../common/inIframe";
1920

2021
// Mock dependencies
2122
vi.mock("../../../configManager/configManager", () => ({
@@ -37,6 +38,10 @@ vi.mock("../livePreviewEventManager", () => ({
3738
},
3839
}));
3940

41+
vi.mock("../../../common/inIframe", () => ({
42+
isOpeningInNewTab: vi.fn(),
43+
}));
44+
4045
describe("postMessageEvent.hooks", () => {
4146
let mockWindow: any;
4247
let mockConfig: any;
@@ -59,6 +64,12 @@ describe("postMessageEvent.hooks", () => {
5964
go: vi.fn(),
6065
},
6166
};
67+
68+
// Make location.href writable
69+
Object.defineProperty(mockWindow.location, 'href', {
70+
writable: true,
71+
value: "https://example.com"
72+
});
6273

6374
// Mock onChange function
6475
mockOnChange = vi.fn();
@@ -83,6 +94,9 @@ describe("postMessageEvent.hooks", () => {
8394
mockWindow._eventCallbacks[event] = callback;
8495
}
8596
);
97+
98+
// Mock isOpeningInNewTab to return true by default
99+
(isOpeningInNewTab as any).mockReturnValue(true);
86100
});
87101

88102
afterEach(() => {
@@ -146,6 +160,9 @@ describe("postMessageEvent.hooks", () => {
146160
});
147161

148162
it("should reload window when ssr is true and no event_type", () => {
163+
// Set URL to include live_preview parameter so reload path is taken
164+
mockWindow.location.href = "https://example.com?live_preview=old-hash";
165+
149166
const eventData: OnChangeLivePreviewPostMessageEventData = {
150167
hash: "test-hash",
151168
};
@@ -180,6 +197,12 @@ describe("postMessageEvent.hooks", () => {
180197
});
181198

182199
describe("HASH_CHANGE event type", () => {
200+
beforeEach(() => {
201+
// Reset config for these tests to non-SSR
202+
mockConfig.ssr = false;
203+
(Config.get as any).mockReturnValue(mockConfig);
204+
});
205+
183206
it("should update URL with new hash in query params", () => {
184207
const eventData: OnChangeLivePreviewPostMessageEventData = {
185208
hash: "new-hash-value",
@@ -214,6 +237,9 @@ describe("postMessageEvent.hooks", () => {
214237
const callback = mockWindow._eventCallbacks[LIVE_PREVIEW_POST_MESSAGE_EVENTS.ON_CHANGE];
215238
callback({ data: eventData });
216239

240+
expect(setConfigFromParams).toHaveBeenCalledWith({
241+
live_preview: "updated-hash",
242+
});
217243
expect(mockWindow.history.pushState).toHaveBeenCalledWith(
218244
{},
219245
"",
@@ -223,6 +249,12 @@ describe("postMessageEvent.hooks", () => {
223249
});
224250

225251
describe("URL_CHANGE event type", () => {
252+
beforeEach(() => {
253+
// Reset config for these tests to non-SSR
254+
mockConfig.ssr = false;
255+
(Config.get as any).mockReturnValue(mockConfig);
256+
});
257+
226258
it("should navigate to new URL when url is provided", () => {
227259
const eventData: OnChangeLivePreviewPostMessageEventData = {
228260
hash: "test-hash",
@@ -262,6 +294,9 @@ describe("postMessageEvent.hooks", () => {
262294

263295
describe("Error handling", () => {
264296
it("should log error and return when window is not defined", () => {
297+
// Mock isOpeningInNewTab to return true so we enter the if block
298+
(isOpeningInNewTab as any).mockReturnValue(true);
299+
265300
// Mock window as undefined
266301
Object.defineProperty(global, "window", {
267302
value: undefined,
@@ -276,6 +311,12 @@ describe("postMessageEvent.hooks", () => {
276311
callback({ data: eventData });
277312

278313
expect(PublicLogger.error).toHaveBeenCalledWith("window is not defined");
314+
315+
// Restore window for other tests
316+
Object.defineProperty(global, "window", {
317+
value: mockWindow,
318+
writable: true,
319+
});
279320
});
280321

281322
it("should handle errors in try-catch block", () => {

src/livePreview/eventManager/livePreviewEventManager.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { EventManager } from "@contentstack/advanced-post-message";
22
import { LIVE_PREVIEW_CHANNEL_ID } from "./livePreviewEventManager.constant";
3-
import { inNewTab } from "../../common/inIframe";
3+
import { isOpeningInNewTab } from "../../common/inIframe";
4+
import { Z } from "vitest/dist/chunks/reporters.D7Jzd9GS.js";
45

56
let livePreviewPostMessage: EventManager | undefined;
67

@@ -11,7 +12,7 @@ if (typeof window !== "undefined") {
1112
suppressErrors: true
1213
};
1314

14-
if (inNewTab()) {
15+
if (isOpeningInNewTab()) {
1516
eventOptions.target = window.opener;
1617
}
1718

src/livePreview/eventManager/postMessageEvent.hooks.ts

Lines changed: 33 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { isOpeningInNewTab } from "../../common/inIframe";
12
import Config, { setConfigFromParams } from "../../configManager/configManager";
23
import { PublicLogger } from "../../logger/logger";
34
import { ILivePreviewWindowType } from "../../types/types";
@@ -60,27 +61,39 @@ export function useOnEntryUpdatePostMessageEvent(): void {
6061
onChange();
6162
}
6263

63-
if(!window) {
64-
PublicLogger.error("window is not defined");
65-
return;
66-
};
67-
68-
// This section will run when there is a change in the entry and the website is SSR
69-
if(ssr && !event_type) {
70-
window.location.reload();
71-
}
72-
73-
// This section will run when the hash changes and the website is SSR or CSR
74-
if(event_type === OnChangeLivePreviewPostMessageEventTypes.HASH_CHANGE){
75-
const newUrl = new URL(window.location.href);
76-
newUrl.searchParams.set("live_preview", event.data.hash);
77-
window.history.pushState({}, "", newUrl.toString());
64+
if(isOpeningInNewTab()) {
65+
if(!window) {
66+
PublicLogger.error("window is not defined");
67+
return;
68+
};
69+
70+
// This section will run when there is a change in the entry and the website is SSR
71+
if(ssr && !event_type) {
72+
if(window.location.href.includes("live_preview")) {
73+
window.location.reload();
74+
} else {
75+
const url = new URL(window.location.href);
76+
url.searchParams.set("live_preview", event.data.hash);
77+
url.searchParams.set("content_type_uid", Config.get().stackDetails.contentTypeUid || "");
78+
url.searchParams.set("entry_uid", Config.get().stackDetails.entryUid || "");
79+
window.location.href = url.toString();
80+
}
81+
}
82+
83+
// This section will run when the hash changes and the website is SSR or CSR
84+
if(event_type === OnChangeLivePreviewPostMessageEventTypes.HASH_CHANGE){
85+
const newUrl = new URL(window.location.href);
86+
newUrl.searchParams.set("live_preview", event.data.hash);
87+
window.history.pushState({}, "", newUrl.toString());
88+
}
89+
90+
// This section will run when the URL of the page changes
91+
if(event_type === OnChangeLivePreviewPostMessageEventTypes.URL_CHANGE && event.data.url){
92+
window.location.href = event.data.url;
93+
}
7894
}
7995

80-
// This section will run when the URL of the page changes
81-
if(event_type === OnChangeLivePreviewPostMessageEventTypes.URL_CHANGE && event.data.url){
82-
window.location.href = event.data.url;
83-
}
96+
8497
} catch (error) {
8598
PublicLogger.error("Error handling live preview update:", error);
8699
return;
@@ -127,7 +140,7 @@ export function sendInitializeLivePreviewPostMessageEvent(): void {
127140
// "init message did not contain contentTypeUid or entryUid."
128141
// );
129142
}
130-
if (Config.get().ssr || isOpeningInTimeline()) {
143+
if (Config.get().ssr || isOpeningInTimeline() || isOpeningInNewTab()) {
131144
addParamsToUrl();
132145
}
133146
Config.set("windowType", windowType);

0 commit comments

Comments
 (0)