5d62003fe5
The session panel's header becomes two tabs, Changes and Preview. Preview shows a web page by address (Reload, Open in a browser tab, the workspace's ports as chips, last address per workspace) in an iframe on the page's own origin: http(s) only, no userinfo, never the dashboard's origin, no top navigation. One rule for every link (lib/openInPreview.ts): a chat or terminal link to localhost/127.0.0.1/0.0.0.0/[::1] opens in Preview while a session panel answers and the dashboard is on loopback; anything else, or a Ctrl/Cmd click, opens a browser tab as before.
85 lines
3.3 KiB
TypeScript
85 lines
3.3 KiB
TypeScript
/**
|
|
* @file openInPreview.test.ts
|
|
* @description The one link rule: which URLs the Preview takes (http(s),
|
|
* never the dashboard's origin, `0.0.0.0` read as `localhost`), that a local
|
|
* link goes to the session panel only when one answers, and that a
|
|
* Ctrl/Cmd click keeps the browser's own tab.
|
|
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
|
*/
|
|
|
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
import {
|
|
OPEN_IN_PREVIEW_EVENT,
|
|
isLoopbackUrl,
|
|
normalizePreviewUrl,
|
|
onLinkClick,
|
|
openInPreview,
|
|
type OpenInPreview,
|
|
} from "../openInPreview";
|
|
|
|
const SELF = "http://localhost:4820";
|
|
|
|
describe("normalizePreviewUrl", () => {
|
|
it("adds http:// to a bare host:port and reads 0.0.0.0 as localhost", () => {
|
|
expect(normalizePreviewUrl("localhost:3000", SELF)).toBe("http://localhost:3000/");
|
|
expect(normalizePreviewUrl("http://0.0.0.0:5173/a", SELF)).toBe("http://localhost:5173/a");
|
|
});
|
|
it("refuses the dashboard's own origin, other schemes and credentials", () => {
|
|
expect(normalizePreviewUrl("http://localhost:4820/settings", SELF)).toBeNull();
|
|
expect(normalizePreviewUrl("javascript:alert(1)", SELF)).toBeNull();
|
|
expect(normalizePreviewUrl("file:///etc/passwd", SELF)).toBeNull();
|
|
expect(normalizePreviewUrl("http://u:p@localhost:3000", SELF)).toBeNull();
|
|
expect(normalizePreviewUrl(" ", SELF)).toBeNull();
|
|
});
|
|
it("accepts an external https page", () => {
|
|
expect(normalizePreviewUrl("https://example.com/x", SELF)).toBe("https://example.com/x");
|
|
});
|
|
});
|
|
|
|
describe("isLoopbackUrl", () => {
|
|
it("is true only for this machine's hosts", () => {
|
|
expect(isLoopbackUrl("http://localhost:3000")).toBe(true);
|
|
expect(isLoopbackUrl("http://127.0.0.1:8080/x")).toBe(true);
|
|
expect(isLoopbackUrl("http://[::1]:3000")).toBe(true);
|
|
expect(isLoopbackUrl("https://github.com")).toBe(false);
|
|
expect(isLoopbackUrl("not a url")).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("openInPreview", () => {
|
|
let off: (() => void) | null = null;
|
|
afterEach(() => off?.());
|
|
const listen = (take: boolean) => {
|
|
const seen: string[] = [];
|
|
const fn = (e: Event) => {
|
|
const d = (e as CustomEvent<OpenInPreview>).detail;
|
|
seen.push(d.url);
|
|
if (take) d.handled = true;
|
|
};
|
|
window.addEventListener(OPEN_IN_PREVIEW_EVENT, fn);
|
|
off = () => window.removeEventListener(OPEN_IN_PREVIEW_EVENT, fn);
|
|
return seen;
|
|
};
|
|
|
|
it("hands a local page to a listening panel", () => {
|
|
const seen = listen(true);
|
|
expect(openInPreview("http://localhost:3000/login")).toBe(true);
|
|
expect(seen).toEqual(["http://localhost:3000/login"]);
|
|
});
|
|
it("falls back when no panel takes it, and never sends an external page", () => {
|
|
const seen = listen(false);
|
|
expect(openInPreview("http://localhost:3000")).toBe(false);
|
|
expect(openInPreview("https://github.com/login")).toBe(false);
|
|
expect(seen).toEqual(["http://localhost:3000/"]);
|
|
});
|
|
it("leaves a Ctrl/Cmd click to the browser", () => {
|
|
listen(true);
|
|
const preventDefault = vi.fn();
|
|
const base = { ctrlKey: false, metaKey: false, shiftKey: false, button: 0, preventDefault };
|
|
onLinkClick({ ...base, ctrlKey: true }, "http://localhost:3000");
|
|
expect(preventDefault).not.toHaveBeenCalled();
|
|
onLinkClick(base, "http://localhost:3000");
|
|
expect(preventDefault).toHaveBeenCalledTimes(1);
|
|
});
|
|
});
|