diff --git a/src/configManager/__test__/configManager.test.ts b/src/configManager/__test__/configManager.test.ts index 74f6328d..dad4cbff 100644 --- a/src/configManager/__test__/configManager.test.ts +++ b/src/configManager/__test__/configManager.test.ts @@ -1,4 +1,4 @@ -import Config, { updateConfigFromUrl } from "../configManager"; +import Config, { updateConfigFromUrl, syncToStackSdk } from "../configManager"; import { getDefaultConfig, getUserInitData } from "../config.default"; import { DeepSignal } from "deepsignal"; import { IConfig } from "../../types/types"; @@ -102,6 +102,73 @@ describe("config default flags", () => { }); }); +describe("syncToStackSdk", () => { + beforeEach(() => { + Config.reset(); + }); + + afterAll(() => { + Config.reset(); + }); + + test("should set hash, stackSdkLivePreview.hash and stackSdkLivePreview.live_preview when hash is provided", () => { + syncToStackSdk({ hash: "abc123" }); + + const config = Config.get(); + expect(config.stackSdk.live_preview.hash).toBe("abc123"); + expect(config.stackSdk.live_preview.live_preview).toBe("abc123"); + expect(config.stackSdk.live_preview.content_type_uid).toBeUndefined(); + expect(config.stackSdk.live_preview.entry_uid).toBeUndefined(); + }); + + test("should set content_type_uid on stackSdk when contentTypeUid is provided", () => { + syncToStackSdk({ contentTypeUid: "blog" }); + + const config = Config.get(); + expect(config.stackSdk.live_preview.content_type_uid).toBe("blog"); + expect(config.stackSdk.live_preview.hash).toBeUndefined(); + expect(config.stackSdk.live_preview.entry_uid).toBeUndefined(); + }); + + test("should set entry_uid on stackSdk when entryUid is provided", () => { + syncToStackSdk({ entryUid: "entry-42" }); + + const config = Config.get(); + expect(config.stackSdk.live_preview.entry_uid).toBe("entry-42"); + expect(config.stackSdk.live_preview.hash).toBeUndefined(); + expect(config.stackSdk.live_preview.content_type_uid).toBeUndefined(); + }); + + test("should set all three fields when all params are provided", () => { + syncToStackSdk({ hash: "h1", contentTypeUid: "page", entryUid: "e1" }); + + const config = Config.get(); + expect(config.stackSdk.live_preview.hash).toBe("h1"); + expect(config.stackSdk.live_preview.live_preview).toBe("h1"); + expect(config.stackSdk.live_preview.content_type_uid).toBe("page"); + expect(config.stackSdk.live_preview.entry_uid).toBe("e1"); + }); + + test("should skip falsy values — null and undefined are ignored", () => { + syncToStackSdk({ hash: null, contentTypeUid: undefined, entryUid: null }); + + const config = Config.get(); + expect(config.stackSdk.live_preview.hash).toBeUndefined(); + expect(config.stackSdk.live_preview.content_type_uid).toBeUndefined(); + expect(config.stackSdk.live_preview.entry_uid).toBeUndefined(); + }); + + test("should not overwrite existing stackSdk values for keys not passed", () => { + syncToStackSdk({ hash: "first", contentTypeUid: "ct1", entryUid: "e1" }); + syncToStackSdk({ hash: "second" }); + + const config = Config.get(); + expect(config.stackSdk.live_preview.hash).toBe("second"); + expect(config.stackSdk.live_preview.content_type_uid).toBe("ct1"); + expect(config.stackSdk.live_preview.entry_uid).toBe("e1"); + }); +}); + describe("update config from url", () => { let config: DeepSignal; diff --git a/src/configManager/configManager.ts b/src/configManager/configManager.ts index 949aa6ad..6bbc9fbe 100644 --- a/src/configManager/configManager.ts +++ b/src/configManager/configManager.ts @@ -73,22 +73,50 @@ export function setConfigFromParams( const content_type_uid = urlParams.get("content_type_uid"); const entry_uid = urlParams.get("entry_uid"); - const stackSdkLivePreview = Config.get().stackSdk.live_preview; - if (live_preview) { Config.set("hash", live_preview); - stackSdkLivePreview.hash = live_preview; - stackSdkLivePreview.live_preview = live_preview; } if (content_type_uid) { Config.set("stackDetails.contentTypeUid", content_type_uid); - stackSdkLivePreview.content_type_uid = content_type_uid; } if (entry_uid) { Config.set("stackDetails.entryUid", entry_uid); - stackSdkLivePreview.entry_uid = entry_uid; + } + + syncToStackSdk({ + hash: live_preview, + contentTypeUid: content_type_uid, + entryUid: entry_uid, + }); +} + +/** + * Syncs hash, contentTypeUid, and entryUid into the user's stackSdk.live_preview object. + * Auto-effects via deepsignal were ruled out because Config.reset() replaces the deepSignal + * instance, which would blind any bound effect. Explicit sync is the safe alternative. + */ +export function syncToStackSdk({ + hash, + contentTypeUid, + entryUid, +}: { + hash?: string | null; + contentTypeUid?: string | null; + entryUid?: string | null; +}): void { + const stackSdkLivePreview = Config.get().stackSdk.live_preview; + + if (hash) { + stackSdkLivePreview.hash = hash; + stackSdkLivePreview.live_preview = hash; + } + if (contentTypeUid) { + stackSdkLivePreview.content_type_uid = contentTypeUid; + } + if (entryUid) { + stackSdkLivePreview.entry_uid = entryUid; } Config.set("stackSdk.live_preview", stackSdkLivePreview); diff --git a/src/livePreview/eventManager/__test__/postMessageEvent.hooks.test.ts b/src/livePreview/eventManager/__test__/postMessageEvent.hooks.test.ts index 80b6b4cf..72eac803 100644 --- a/src/livePreview/eventManager/__test__/postMessageEvent.hooks.test.ts +++ b/src/livePreview/eventManager/__test__/postMessageEvent.hooks.test.ts @@ -3,7 +3,7 @@ */ import { vi } from "vitest"; -import Config, { setConfigFromParams } from "../../../configManager/configManager"; +import Config, { syncToStackSdk } from "../../../configManager/configManager"; import { PublicLogger } from "../../../logger/logger"; import livePreviewPostMessage from "../livePreviewEventManager"; import { LIVE_PREVIEW_POST_MESSAGE_EVENTS } from "../livePreviewEventManager.constant"; @@ -26,7 +26,7 @@ vi.mock("../../../configManager/configManager", () => ({ get: vi.fn(), set: vi.fn(), }, - setConfigFromParams: vi.fn(), + syncToStackSdk: vi.fn(), })); vi.mock("../../../logger/logger", () => ({ @@ -139,9 +139,8 @@ describe("postMessageEvent.hooks", () => { const callback = mockWindow._eventCallbacks[LIVE_PREVIEW_POST_MESSAGE_EVENTS.ON_CHANGE]; callback({ data: eventData }); - expect(setConfigFromParams).toHaveBeenCalledWith({ - live_preview: "test-hash", - }); + expect(Config.set).toHaveBeenCalledWith("hash", "test-hash"); + expect(syncToStackSdk).toHaveBeenCalledWith({ hash: "test-hash" }); expect(mockOnChange).toHaveBeenCalled(); }); @@ -156,9 +155,8 @@ describe("postMessageEvent.hooks", () => { const callback = mockWindow._eventCallbacks[LIVE_PREVIEW_POST_MESSAGE_EVENTS.ON_CHANGE]; callback({ data: eventData }); - expect(setConfigFromParams).toHaveBeenCalledWith({ - live_preview: "test-hash", - }); + expect(Config.set).toHaveBeenCalledWith("hash", "test-hash"); + expect(syncToStackSdk).toHaveBeenCalledWith({ hash: "test-hash" }); expect(mockOnChange).not.toHaveBeenCalled(); }); }); @@ -180,9 +178,8 @@ describe("postMessageEvent.hooks", () => { const callback = mockWindow._eventCallbacks[LIVE_PREVIEW_POST_MESSAGE_EVENTS.ON_CHANGE]; callback({ data: eventData }); - expect(setConfigFromParams).toHaveBeenCalledWith({ - live_preview: "test-hash", - }); + expect(Config.set).toHaveBeenCalledWith("hash", "test-hash"); + expect(syncToStackSdk).toHaveBeenCalledWith({ hash: "test-hash" }); expect(mockWindow.location.reload).toHaveBeenCalled(); expect(mockOnChange).not.toHaveBeenCalled(); }); @@ -198,9 +195,8 @@ describe("postMessageEvent.hooks", () => { const callback = mockWindow._eventCallbacks[LIVE_PREVIEW_POST_MESSAGE_EVENTS.ON_CHANGE]; callback({ data: eventData }); - expect(setConfigFromParams).toHaveBeenCalledWith({ - live_preview: "test-hash", - }); + expect(Config.set).toHaveBeenCalledWith("hash", "test-hash"); + expect(syncToStackSdk).toHaveBeenCalledWith({ hash: "test-hash" }); expect(mockWindow.location.reload).not.toHaveBeenCalled(); expect(mockOnChange).not.toHaveBeenCalled(); }); @@ -308,9 +304,8 @@ describe("postMessageEvent.hooks", () => { const callback = mockWindow._eventCallbacks[LIVE_PREVIEW_POST_MESSAGE_EVENTS.ON_CHANGE]; callback({ data: eventData }); - expect(setConfigFromParams).toHaveBeenCalledWith({ - live_preview: "new-hash-value", - }); + expect(Config.set).toHaveBeenCalledWith("hash", "new-hash-value"); + expect(syncToStackSdk).toHaveBeenCalledWith({ hash: "new-hash-value" }); expect(mockWindow.history.pushState).toHaveBeenCalledWith( {}, "", @@ -331,9 +326,8 @@ describe("postMessageEvent.hooks", () => { const callback = mockWindow._eventCallbacks[LIVE_PREVIEW_POST_MESSAGE_EVENTS.ON_CHANGE]; callback({ data: eventData }); - expect(setConfigFromParams).toHaveBeenCalledWith({ - live_preview: "updated-hash", - }); + expect(Config.set).toHaveBeenCalledWith("hash", "updated-hash"); + expect(syncToStackSdk).toHaveBeenCalledWith({ hash: "updated-hash" }); expect(mockWindow.history.pushState).toHaveBeenCalledWith( {}, "", @@ -361,9 +355,8 @@ describe("postMessageEvent.hooks", () => { const callback = mockWindow._eventCallbacks[LIVE_PREVIEW_POST_MESSAGE_EVENTS.ON_CHANGE]; callback({ data: eventData }); - expect(setConfigFromParams).toHaveBeenCalledWith({ - live_preview: "test-hash", - }); + expect(Config.set).toHaveBeenCalledWith("hash", "test-hash"); + expect(syncToStackSdk).toHaveBeenCalledWith({ hash: "test-hash" }); expect(mockWindow.location.href).toBe("https://newdomain.com/new-page"); }); @@ -379,9 +372,8 @@ describe("postMessageEvent.hooks", () => { const callback = mockWindow._eventCallbacks[LIVE_PREVIEW_POST_MESSAGE_EVENTS.ON_CHANGE]; callback({ data: eventData }); - expect(setConfigFromParams).toHaveBeenCalledWith({ - live_preview: "test-hash", - }); + expect(Config.set).toHaveBeenCalledWith("hash", "test-hash"); + expect(syncToStackSdk).toHaveBeenCalledWith({ hash: "test-hash" }); expect(mockWindow.location.href).toBe(originalHref); }); }); @@ -432,9 +424,9 @@ describe("postMessageEvent.hooks", () => { ); }); - it("should handle errors when setConfigFromParams throws", () => { - (setConfigFromParams as any).mockImplementation(() => { - throw new Error("setConfigFromParams error"); + it("should handle errors when syncToStackSdk throws", () => { + (syncToStackSdk as any).mockImplementation(() => { + throw new Error("syncToStackSdk error"); }); const eventData: OnChangeLivePreviewPostMessageEventData = { @@ -464,7 +456,8 @@ describe("postMessageEvent.hooks", () => { const callback = mockWindow._eventCallbacks[LIVE_PREVIEW_POST_MESSAGE_EVENTS.ON_CHANGE]; callback({ data: eventData }); - expect(setConfigFromParams).toHaveBeenCalledWith({ live_preview: "test-hash" }); + expect(Config.set).toHaveBeenCalledWith("hash", "test-hash"); + expect(syncToStackSdk).toHaveBeenCalledWith({ hash: "test-hash" }); expect(mockWindow.location.reload).not.toHaveBeenCalled(); expect(mockOnChange).not.toHaveBeenCalled(); }); @@ -484,7 +477,8 @@ describe("postMessageEvent.hooks", () => { const callback = mockWindow._eventCallbacks[LIVE_PREVIEW_POST_MESSAGE_EVENTS.ON_CHANGE]; callback({ data: eventData }); - expect(setConfigFromParams).toHaveBeenCalledWith({ live_preview: "new-hash" }); + expect(Config.set).toHaveBeenCalledWith("hash", "new-hash"); + expect(syncToStackSdk).toHaveBeenCalledWith({ hash: "new-hash" }); expect(mockWindow.history.pushState).not.toHaveBeenCalled(); }); @@ -505,7 +499,8 @@ describe("postMessageEvent.hooks", () => { const callback = mockWindow._eventCallbacks[LIVE_PREVIEW_POST_MESSAGE_EVENTS.ON_CHANGE]; callback({ data: eventData }); - expect(setConfigFromParams).toHaveBeenCalledWith({ live_preview: "test-hash" }); + expect(Config.set).toHaveBeenCalledWith("hash", "test-hash"); + expect(syncToStackSdk).toHaveBeenCalledWith({ hash: "test-hash" }); expect(mockWindow.location.href).toBe(originalHref); }); }); @@ -637,7 +632,7 @@ describe("postMessageEvent.hooks", () => { ); }); - it("should call setConfigFromParams with content_type_uid and entry_uid when INIT response provides them", async () => { + it("should sync contentTypeUid and entryUid to Config and stackSdk when INIT response provides them", async () => { mockConfig = { ssr: true, mode: 1, @@ -652,10 +647,9 @@ describe("postMessageEvent.hooks", () => { await sendInitializeLivePreviewPostMessageEvent(); await Promise.resolve(); - expect(setConfigFromParams).toHaveBeenCalledWith({ - content_type_uid: "blog", - entry_uid: "entry-123", - }); + expect(Config.set).toHaveBeenCalledWith("stackDetails.contentTypeUid", "blog"); + expect(Config.set).toHaveBeenCalledWith("stackDetails.entryUid", "entry-123"); + expect(syncToStackSdk).toHaveBeenCalledWith({ contentTypeUid: "blog", entryUid: "entry-123" }); }); it("should return early and skip post-init setup when windowType is BUILDER", async () => { @@ -674,8 +668,9 @@ describe("postMessageEvent.hooks", () => { await sendInitializeLivePreviewPostMessageEvent(); await Promise.resolve(); - expect(setConfigFromParams).not.toHaveBeenCalled(); - expect(Config.set).not.toHaveBeenCalled(); + expect(syncToStackSdk).not.toHaveBeenCalled(); + expect(Config.set).not.toHaveBeenCalledWith("stackDetails.contentTypeUid", expect.anything()); + expect(Config.set).not.toHaveBeenCalledWith("stackDetails.entryUid", expect.anything()); }); it("should start CHECK_ENTRY_PAGE interval when ssr is false", async () => { diff --git a/src/livePreview/eventManager/postMessageEvent.hooks.ts b/src/livePreview/eventManager/postMessageEvent.hooks.ts index bf9b1534..b63991bf 100644 --- a/src/livePreview/eventManager/postMessageEvent.hooks.ts +++ b/src/livePreview/eventManager/postMessageEvent.hooks.ts @@ -1,5 +1,5 @@ import { inVisualEditor, isOpeningInNewTab } from "../../common/inIframe"; -import Config, { setConfigFromParams } from "../../configManager/configManager"; +import Config, { syncToStackSdk } from "../../configManager/configManager"; import { PublicLogger } from "../../logger/logger"; import { ILivePreviewWindowType } from "../../types/types"; import { addParamsToUrl, isOpeningInTimeline } from "../../utils"; @@ -52,9 +52,11 @@ export function useOnEntryUpdatePostMessageEvent(): void { try { const { ssr, onChange, stackDetails } = Config.get(); const event_type = event.data._metadata?.event_type; - setConfigFromParams({ - live_preview: event.data.hash, - }); + // hash is typed as required string, guard is a safety net + if (event.data.hash) { + Config.set("hash", event.data.hash); + syncToStackSdk({ hash: event.data.hash }); + } // This section will run when there is a change in the entry and the website is CSR if (!ssr && !event_type) { @@ -161,11 +163,10 @@ export function sendInitializeLivePreviewPostMessageEvent(): void { } if (contentTypeUid && entryUid) { - // TODO: we should not use this function. Instead we should have sideEffect run automatically when we set the config. - setConfigFromParams({ - content_type_uid: contentTypeUid, - entry_uid: entryUid, - }); + // Sync is explicit here intentionally: auto-effects via deepsignal would go blind when Config.reset() is called. + Config.set("stackDetails.contentTypeUid", contentTypeUid); + Config.set("stackDetails.entryUid", entryUid); + syncToStackSdk({ contentTypeUid, entryUid }); } else { // TODO: add debug logs that runs conditionally // PublicLogger.debug(