Files
AgentHub/client/src/lib/__tests__/openInPreview.test.ts
T
nntrivi2001 5d62003fe5 feat(workspace): Preview tab in the session panel; local links open there
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.
2026-11-02 15:06:04 +07:00

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);
});
});