Skip to content

Commit 739ad25

Browse files
Merge pull request #624 from contentstack/stage_v4
Release v4.4.5
2 parents 43d4c0a + 3fb931c commit 739ad25

11 files changed

Lines changed: 327 additions & 110 deletions

File tree

CHANGELOG.md

Lines changed: 127 additions & 59 deletions
Large diffs are not rendered by default.

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,11 @@ npm install @contentstack/live-preview-utils
2929

3030
### Load from a CDN (advanced)
3131

32-
Pin the version to match your app (update `4.4.4` when you upgrade):
32+
Pin the version to match your app (update `4.4.5` when you upgrade):
3333

3434
```html
3535
<script type="module" crossorigin="anonymous">
36-
import ContentstackLivePreview from "https://esm.sh/@contentstack/live-preview-utils@4.4.4";
36+
import ContentstackLivePreview from "https://esm.sh/@contentstack/live-preview-utils@4.4.5";
3737
3838
ContentstackLivePreview.init({
3939
stackDetails: {

package-lock.json

Lines changed: 3 additions & 25 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": "4.4.4",
3+
"version": "4.4.5",
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/visualBuilder/components/__test__/startEditingButton.test.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,8 @@ describe("StartEditingButtonComponent", () => {
110110
test("should update href with current URL when mouse enters button", async () => {
111111
Object.defineProperty(window, "location", {
112112
value: new URL("http://localhost:3000"),
113+
configurable: true,
114+
writable: true,
113115
});
114116

115117
const { getByTestId } = await asyncRender(
@@ -120,6 +122,7 @@ describe("StartEditingButtonComponent", () => {
120122

121123
Object.defineProperty(window, "location", {
122124
value: new URL("http://localhost:3000/about"),
125+
configurable: true,
123126
writable: true,
124127
});
125128

@@ -135,6 +138,8 @@ describe("StartEditingButtonComponent", () => {
135138
test("should update href with current URL when button is focused", async () => {
136139
Object.defineProperty(window, "location", {
137140
value: new URL("http://localhost:3000"),
141+
configurable: true,
142+
writable: true,
138143
});
139144

140145
const { getByTestId } = await asyncRender(
@@ -145,10 +150,11 @@ describe("StartEditingButtonComponent", () => {
145150

146151
Object.defineProperty(window, "location", {
147152
value: new URL("http://localhost:3000/contact"),
153+
configurable: true,
148154
writable: true,
149155
});
150156

151-
fireEvent.focus(button);
157+
button.focus();
152158

153159
const updatedHref = button.getAttribute("href");
154160
expect(updatedHref).not.toBe(initialHref);

src/visualBuilder/listeners/__test__/mouseClick.test.ts

Lines changed: 120 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { describe, it, expect, vi, beforeEach } from "vitest";
1+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
22
import handleBuilderInteraction from "../mouseClick";
33
import { VisualBuilder } from "../../index";
44
import { FieldSchemaMap } from "../../utils/fieldSchemaMap";
@@ -250,3 +250,122 @@ describe("handleBuilderInteraction — pauseFeedback guard", () => {
250250
expect(generateThread).toHaveBeenCalled();
251251
});
252252
});
253+
254+
describe("handleBuilderInteraction — alt+click on anchor", () => {
255+
const originalLocation = window.location;
256+
257+
beforeEach(() => {
258+
Object.defineProperty(window, "location", {
259+
value: { href: "" },
260+
writable: true,
261+
});
262+
});
263+
264+
afterEach(() => {
265+
Object.defineProperty(window, "location", {
266+
value: originalLocation,
267+
writable: true,
268+
});
269+
});
270+
271+
it("prevents default/propagation and drives navigation itself instead of relying on the native click", async () => {
272+
document.body.innerHTML = "";
273+
const anchor = document.createElement("a");
274+
anchor.href = "https://example.com/blog#anchor";
275+
document.body.appendChild(anchor);
276+
277+
const params = makeParams(anchor);
278+
Object.defineProperty(params.event, "altKey", { value: true });
279+
const preventDefaultSpy = vi.spyOn(params.event, "preventDefault");
280+
const stopPropagationSpy = vi.spyOn(params.event, "stopPropagation");
281+
282+
await handleBuilderInteraction(params);
283+
284+
expect(preventDefaultSpy).toHaveBeenCalled();
285+
expect(stopPropagationSpy).toHaveBeenCalled();
286+
expect(window.location.href).toBe("https://example.com/blog#anchor");
287+
expect(getCsDataOfElement).not.toHaveBeenCalled();
288+
});
289+
290+
it("does nothing on alt+click when the target isn't an anchor", async () => {
291+
const editableElement = makeEditableElement();
292+
const params = makeParams(editableElement);
293+
Object.defineProperty(params.event, "altKey", { value: true });
294+
const preventDefaultSpy = vi.spyOn(params.event, "preventDefault");
295+
296+
await handleBuilderInteraction(params);
297+
298+
expect(preventDefaultSpy).not.toHaveBeenCalled();
299+
expect(window.location.href).toBe("");
300+
expect(getCsDataOfElement).not.toHaveBeenCalled();
301+
});
302+
303+
it("opens target=_blank links (RTE 'open in new tab') in a new tab instead of hijacking the iframe", async () => {
304+
document.body.innerHTML = "";
305+
const anchor = document.createElement("a");
306+
anchor.href = "https://example.com/other-entry";
307+
anchor.target = "_blank";
308+
document.body.appendChild(anchor);
309+
310+
const openSpy = vi.spyOn(window, "open").mockImplementation(() => null);
311+
const params = makeParams(anchor);
312+
Object.defineProperty(params.event, "altKey", { value: true });
313+
314+
await handleBuilderInteraction(params);
315+
316+
expect(openSpy).toHaveBeenCalledWith(
317+
"https://example.com/other-entry",
318+
"_blank",
319+
"noopener,noreferrer"
320+
);
321+
expect(window.location.href).toBe("");
322+
openSpy.mockRestore();
323+
});
324+
325+
it("resolves the anchor ancestor when the click lands on nested formatted text inside the link", async () => {
326+
document.body.innerHTML = "";
327+
const anchor = document.createElement("a");
328+
anchor.href = "https://example.com/bold-link";
329+
const bold = document.createElement("strong");
330+
bold.textContent = "click me";
331+
anchor.appendChild(bold);
332+
document.body.appendChild(anchor);
333+
334+
const params = makeParams(bold);
335+
Object.defineProperty(params.event, "altKey", { value: true });
336+
const preventDefaultSpy = vi.spyOn(params.event, "preventDefault");
337+
338+
await handleBuilderInteraction(params);
339+
340+
expect(preventDefaultSpy).toHaveBeenCalled();
341+
expect(window.location.href).toBe("https://example.com/bold-link");
342+
});
343+
344+
it("blocks unsafe url schemes like javascript: on alt+click", async () => {
345+
document.body.innerHTML = "";
346+
const anchor = document.createElement("a");
347+
anchor.href = "javascript:alert(1)";
348+
document.body.appendChild(anchor);
349+
350+
const params = makeParams(anchor);
351+
Object.defineProperty(params.event, "altKey", { value: true });
352+
353+
await handleBuilderInteraction(params);
354+
355+
expect(window.location.href).toBe("");
356+
});
357+
358+
it("allows relative hrefs (resolve to the page's own http/https scheme)", async () => {
359+
document.body.innerHTML = "";
360+
const anchor = document.createElement("a");
361+
anchor.href = "/other-entry";
362+
document.body.appendChild(anchor);
363+
364+
const params = makeParams(anchor);
365+
Object.defineProperty(params.event, "altKey", { value: true });
366+
367+
await handleBuilderInteraction(params);
368+
369+
expect(window.location.href).toBe(anchor.href);
370+
});
371+
});

0 commit comments

Comments
 (0)