diff --git a/src/livePreview/__test__/live-preview.test.ts b/src/livePreview/__test__/live-preview.test.ts index dfdcce7d..023a19c8 100644 --- a/src/livePreview/__test__/live-preview.test.ts +++ b/src/livePreview/__test__/live-preview.test.ts @@ -432,7 +432,7 @@ describe("incoming postMessage", () => { describe("testing window event listeners", () => { let addEventListenerMock: any; - let sendInitEvent = vi.fn().mockImplementation(mockLivePreviewInitEventListener); + const sendInitEvent = vi.fn().mockImplementation(mockLivePreviewInitEventListener); let livePreviewInstance: LivePreview; beforeEach(() => { diff --git a/src/livePreview/eventManager/postMessageEvent.hooks.ts b/src/livePreview/eventManager/postMessageEvent.hooks.ts index 2cf60b01..75988942 100644 --- a/src/livePreview/eventManager/postMessageEvent.hooks.ts +++ b/src/livePreview/eventManager/postMessageEvent.hooks.ts @@ -1,6 +1,6 @@ import Config, { setConfigFromParams } from "../../configManager/configManager"; import { ILivePreviewWindowType } from "../../types/types"; -import { addParamsToUrl } from "../../utils"; +import { addParamsToUrl, isOpeningInTimeline } from "../../utils"; import livePreviewPostMessage from "./livePreviewEventManager"; import { LIVE_PREVIEW_POST_MESSAGE_EVENTS } from "./livePreviewEventManager.constant"; import { @@ -95,7 +95,7 @@ export function sendInitializeLivePreviewPostMessageEvent(): void { // "init message did not contain contentTypeUid or entryUid." // ); } - if (Config.get().ssr) { + if (Config.get().ssr || isOpeningInTimeline()) { addParamsToUrl(); } Config.set("windowType", windowType); diff --git a/src/utils/__test__/addLivePreviewQueryTags.test.ts b/src/utils/__test__/addLivePreviewQueryTags.test.ts new file mode 100644 index 00000000..c232abbb --- /dev/null +++ b/src/utils/__test__/addLivePreviewQueryTags.test.ts @@ -0,0 +1,81 @@ +import { describe, test, expect, vi, beforeEach } from "vitest"; +import { addLivePreviewQueryTags } from "../addLivePreviewQueryTags"; +import { PublicLogger } from "../../logger/logger"; + +// Mock the logger +vi.mock("../../logger/logger", () => ({ + PublicLogger: { + error: vi.fn(), + }, +})); + +describe("addLivePreviewQueryTags", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + test("should return original URL when no live preview parameters in current location", () => { + // This test works with current document.location (likely has no live preview params) + const targetUrl = "http://example.com/target-page"; + + const result = addLivePreviewQueryTags(targetUrl); + + // Should return unchanged since no live preview params in current location + expect(result).toBe(targetUrl); + }); + + test("should log error and return original link when target URL is invalid", () => { + const targetUrl = "not-a-valid-url-at-all-invalid"; + + const result = addLivePreviewQueryTags(targetUrl); + + expect(PublicLogger.error).toHaveBeenCalledWith("Error while adding live preview to URL"); + expect(result).toBe(targetUrl); + }); + + test("should handle empty string input", () => { + const targetUrl = ""; + + const result = addLivePreviewQueryTags(targetUrl); + + expect(PublicLogger.error).toHaveBeenCalledWith("Error while adding live preview to URL"); + expect(result).toBe(targetUrl); + }); + + test("should handle malformed URLs gracefully", () => { + const targetUrl = "http://"; + + const result = addLivePreviewQueryTags(targetUrl); + + expect(PublicLogger.error).toHaveBeenCalledWith("Error while adding live preview to URL"); + expect(result).toBe(targetUrl); + }); + + test("should handle valid URLs without errors", () => { + const targetUrl = "https://example.com/valid-page"; + + const result = addLivePreviewQueryTags(targetUrl); + + // Should not throw errors and return some result + expect(typeof result).toBe("string"); + expect(result.length).toBeGreaterThan(0); + }); + + test("should add live preview query tags to URL with all required query parameters", () => { + const originalUrl = + "http://example.com?live_preview=hash&content_type_uid=ctuid&entry_uid=entryuid"; + const expectedUrl = + "http://example.com/?live_preview=hash&content_type_uid=ctuid&entry_uid=entryuid"; + + global.window = Object.create(window); + Object.defineProperty(window, "location", { + value: { + href: originalUrl, + }, + writable: true, + }); + const result = addLivePreviewQueryTags(originalUrl); + + expect(result).toEqual(expectedUrl); + }); +}); diff --git a/src/utils/__test__/index.test.ts b/src/utils/__test__/index.test.ts index fbf7ff9a..afb4cf83 100644 --- a/src/utils/__test__/index.test.ts +++ b/src/utils/__test__/index.test.ts @@ -1,52 +1,254 @@ -import { PublicLogger } from "../../logger/logger"; -import { addLivePreviewQueryTags, hasWindow } from "../index"; +import { hasWindow, addParamsToUrl } from "../index"; import { vi } from "vitest"; -vi.mock("../../logger/logger", () => ({ - PublicLogger: { - error: vi.fn(), - }, +// Mock addLivePreviewQueryTags function +vi.mock("../addLivePreviewQueryTags", () => ({ + addLivePreviewQueryTags: vi.fn() })); +// Import the mocked function after setting up the mock +import { addLivePreviewQueryTags } from "../addLivePreviewQueryTags"; + describe("hasWindow() function", () => { test("must check if window is available", () => { expect(hasWindow()).toBe(typeof window !== "undefined"); }); }); -describe("addLivePreviewQueryTags", () => { - test("should add live preview query tags to URL with all required query parameters", () => { - const originalUrl = - "http://example.com?live_preview=hash&content_type_uid=ctuid&entry_uid=entryuid"; - const expectedUrl = - "http://example.com/?live_preview=hash&content_type_uid=ctuid&entry_uid=entryuid"; - global.window = Object.create(window); - Object.defineProperty(window, "location", { - value: { - href: originalUrl, - }, - writable: true, +describe("addParamsToUrl", () => { + let mockAddEventListener: any; + let mockDocument: any; + let clickHandler: (event: any) => void; + + beforeEach(() => { + // Reset all mocks + vi.clearAllMocks(); + + // Mock window.addEventListener to capture the click handler + mockAddEventListener = vi.fn((event, handler) => { + if (event === "click") { + clickHandler = handler; + } }); + + // Setup mock return value for addLivePreviewQueryTags + vi.mocked(addLivePreviewQueryTags).mockImplementation((url) => `${url}?live_preview=test&content_type_uid=test&entry_uid=test`); + + // Mock document and window + mockDocument = { + location: { + origin: "https://example.com" + } + }; - const result = addLivePreviewQueryTags(originalUrl); + global.window = { + addEventListener: mockAddEventListener, + document: mockDocument + } as any; + + global.document = mockDocument as any; + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); - expect(result).toEqual(expectedUrl); + test("should add event listener when function is called", () => { + addParamsToUrl(); + + expect(mockAddEventListener).toHaveBeenCalledWith("click", expect.any(Function)); }); - test("should log error and return original link if an error occurs while adding live preview query tags", () => { - const originalUrl = - "http://example.com?live_preview=hash&content_type_uid=ctuid&entry_uid=entryuid"; - const expectedLoggedError = "Error while adding live preview to URL"; + describe("when clicking on elements", () => { + beforeEach(() => { + addParamsToUrl(); + }); + + test("should handle click directly on anchor tag", () => { + // Create mock anchor element + const mockAnchor = { + href: "https://example.com/page", + closest: vi.fn().mockReturnValue(null), + contains: vi.fn().mockReturnValue(true) + }; + mockAnchor.closest.mockReturnValue(mockAnchor); // closest('a') returns self + + const mockEvent = { + target: mockAnchor + }; + + // Trigger the click event + clickHandler(mockEvent); - vi.spyOn(global, "URL").mockImplementation(() => { - throw new Error("Mock error"); + expect(mockAnchor.closest).toHaveBeenCalledWith('a'); + expect(mockAnchor.contains).toHaveBeenCalledWith(mockAnchor); + expect(addLivePreviewQueryTags).toHaveBeenCalledWith("https://example.com/page"); + expect(mockAnchor.href).toBe("https://example.com/page?live_preview=test&content_type_uid=test&entry_uid=test"); }); - const result = addLivePreviewQueryTags(originalUrl); + test("should handle click on child element of anchor tag", () => { + // Create mock child element and parent anchor + const mockChild = { + closest: vi.fn() + }; + + const mockAnchor = { + href: "https://example.com/child-page", + contains: vi.fn().mockReturnValue(true) + }; + + mockChild.closest.mockReturnValue(mockAnchor); // closest('a') returns parent anchor + + const mockEvent = { + target: mockChild + }; + + // Trigger the click event + clickHandler(mockEvent); + + expect(mockChild.closest).toHaveBeenCalledWith('a'); + expect(mockAnchor.contains).toHaveBeenCalledWith(mockChild); + expect(addLivePreviewQueryTags).toHaveBeenCalledWith("https://example.com/child-page"); + expect(mockAnchor.href).toBe("https://example.com/child-page?live_preview=test&content_type_uid=test&entry_uid=test"); + }); + + test("should not process click when no anchor element is found", () => { + const mockElement = { + closest: vi.fn().mockReturnValue(null) + }; + + const mockEvent = { + target: mockElement + }; + + clickHandler(mockEvent); + + expect(mockElement.closest).toHaveBeenCalledWith('a'); + expect(addLivePreviewQueryTags).not.toHaveBeenCalled(); + }); + + test("should not process click when anchor doesn't contain clicked element", () => { + const mockChild = { + closest: vi.fn() + }; + + const mockAnchor = { + href: "https://example.com/page", + contains: vi.fn().mockReturnValue(false) // Anchor doesn't contain the clicked element + }; + + mockChild.closest.mockReturnValue(mockAnchor); + + const mockEvent = { + target: mockChild + }; + + clickHandler(mockEvent); + + expect(mockChild.closest).toHaveBeenCalledWith('a'); + expect(mockAnchor.contains).toHaveBeenCalledWith(mockChild); + expect(addLivePreviewQueryTags).not.toHaveBeenCalled(); + }); - expect(PublicLogger.error).toHaveBeenCalledWith(expectedLoggedError); + test("should not process external links", () => { + const mockAnchor = { + href: "https://external-site.com/page", + closest: vi.fn().mockReturnValue(null), + contains: vi.fn().mockReturnValue(true) + }; + mockAnchor.closest.mockReturnValue(mockAnchor); + + const mockEvent = { + target: mockAnchor + }; - expect(result).toEqual(originalUrl); + clickHandler(mockEvent); + + expect(addLivePreviewQueryTags).not.toHaveBeenCalled(); + expect(mockAnchor.href).toBe("https://external-site.com/page"); // Unchanged + }); + + test("should not process links that already contain live_preview", () => { + const mockAnchor = { + href: "https://example.com/page?live_preview=existing", + closest: vi.fn().mockReturnValue(null), + contains: vi.fn().mockReturnValue(true) + }; + mockAnchor.closest.mockReturnValue(mockAnchor); + + const mockEvent = { + target: mockAnchor + }; + + clickHandler(mockEvent); + + expect(addLivePreviewQueryTags).not.toHaveBeenCalled(); + expect(mockAnchor.href).toBe("https://example.com/page?live_preview=existing"); // Unchanged + }); + + test("should not process links without href", () => { + const mockAnchor = { + href: "", + closest: vi.fn().mockReturnValue(null), + contains: vi.fn().mockReturnValue(true) + }; + mockAnchor.closest.mockReturnValue(mockAnchor); + + const mockEvent = { + target: mockAnchor + }; + + clickHandler(mockEvent); + + expect(addLivePreviewQueryTags).not.toHaveBeenCalled(); + expect(mockAnchor.href).toBe(""); // Unchanged + }); + + test("should handle case when addLivePreviewQueryTags returns empty string", () => { + vi.mocked(addLivePreviewQueryTags).mockReturnValue(""); + + const originalHref = "https://example.com/page"; + const mockAnchor = { + href: originalHref, + closest: vi.fn().mockReturnValue(null), + contains: vi.fn().mockReturnValue(true) + }; + mockAnchor.closest.mockReturnValue(mockAnchor); + + const mockEvent = { + target: mockAnchor + }; + + clickHandler(mockEvent); + + expect(addLivePreviewQueryTags).toHaveBeenCalledWith(originalHref); + expect(mockAnchor.href).toBe(originalHref); // Falls back to original href when empty string returned + }); + + test("should handle nested child elements", () => { + // Create deeply nested structure: span > button > a + const mockDeepChild = { + closest: vi.fn() + }; + + const mockAnchor = { + href: "https://example.com/nested-page", + contains: vi.fn().mockReturnValue(true) + }; + + mockDeepChild.closest.mockReturnValue(mockAnchor); + + const mockEvent = { + target: mockDeepChild + }; + + clickHandler(mockEvent); + + expect(mockDeepChild.closest).toHaveBeenCalledWith('a'); + expect(mockAnchor.contains).toHaveBeenCalledWith(mockDeepChild); + expect(addLivePreviewQueryTags).toHaveBeenCalledWith("https://example.com/nested-page"); + expect(mockAnchor.href).toBe("https://example.com/nested-page?live_preview=test&content_type_uid=test&entry_uid=test"); + }); }); }); diff --git a/src/utils/addLivePreviewQueryTags.ts b/src/utils/addLivePreviewQueryTags.ts index b99ead05..fc1014a7 100644 --- a/src/utils/addLivePreviewQueryTags.ts +++ b/src/utils/addLivePreviewQueryTags.ts @@ -8,11 +8,17 @@ export function addLivePreviewQueryTags(link: string): string { const ctUid: string | null = docUrl.searchParams.get("content_type_uid"); const entryUid: string | null = docUrl.searchParams.get("entry_uid"); - if (livePreviewHash && ctUid && entryUid) { + const previewTimestamp: string | null = docUrl.searchParams.get("preview_timestamp"); + if (livePreviewHash) { newUrl.searchParams.set("live_preview", livePreviewHash); + } + if(ctUid && entryUid){ newUrl.searchParams.set("content_type_uid", ctUid); newUrl.searchParams.set("entry_uid", entryUid); } + if (previewTimestamp) { + newUrl.searchParams.set("preview_timestamp", previewTimestamp); + } return newUrl.href; } catch (error) { PublicLogger.error("Error while adding live preview to URL"); diff --git a/src/utils/index.ts b/src/utils/index.ts index 97b91903..6e7129df 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -7,16 +7,23 @@ export { addLivePreviewQueryTags }; export function addParamsToUrl() { // Setting the query params to all the click events related to current domain window.addEventListener("click", (event: any) => { - const target: any = event.target; - const targetHref: string | any = target.href; + const clickedElement = event.target; + const anchorElement = clickedElement.closest('a'); + + // Only proceed if the clicked element is either an anchor or a direct/indirect child of an anchor + if (!anchorElement || !anchorElement.contains(clickedElement)) { + return; + } + + const targetHref: string | any = anchorElement.href; const docOrigin: string = document.location.origin; if ( targetHref && targetHref.includes(docOrigin) && !targetHref.includes("live_preview") ) { - const newUrl = addLivePreviewQueryTags(target.href); - event.target.href = newUrl || target.href; + const newUrl = addLivePreviewQueryTags(targetHref); + anchorElement.href = newUrl || targetHref; } }); }