1import { describe, expect, it } from "vite-plus/test";
2
3import {
4 isLoopbackHost,
5 newPreviewTabId,
6 normalizePreviewUrl,
7 PreviewUrlNormalizationError,
8 resolveAddressBarInput,
9} from "./preview.ts";
10
11describe("resolveAddressBarInput", () => {
12 it("opens what users type as addresses", () => {
13 expect(resolveAddressBarInput("localhost:5173")).toBe("http://localhost:5173/");
14 expect(resolveAddressBarInput("127.0.0.1:3000/path")).toBe("http://127.0.0.1:3000/path");
15 expect(resolveAddressBarInput("my-box.tailnet.ts.net")).toBe("https://my-box.tailnet.ts.net/");
16 expect(resolveAddressBarInput("cnn.com")).toBe("https://cnn.com/");
17 expect(resolveAddressBarInput("devbox:8080")).toBe("https://devbox:8080/");
18 expect(resolveAddressBarInput(" https://example.com/a b ")).toBe("https://example.com/a%20b");
19 expect(resolveAddressBarInput("example.com:8080")).toBe("https://example.com:8080/");
20 expect(resolveAddressBarInput("[::1]:3000")).toBe("http://[::1]:3000/");
21 expect(resolveAddressBarInput("192.168.1.5:3000")).toBe("https://192.168.1.5:3000/");
22 });
23
24 it("searches for text that is not an address", () => {
25 expect(resolveAddressBarInput("weather")).toBe("https://duckduckgo.com/?q=weather");
26 expect(resolveAddressBarInput("how to center a div")).toBe(
27 "https://duckduckgo.com/?q=how%20to%20center%20a%20div",
28 );
29 expect(resolveAddressBarInput("what is cnn.com")).toBe(
30 "https://duckduckgo.com/?q=what%20is%20cnn.com",
31 );
32 expect(resolveAddressBarInput("what is https://example.com")).toBe(
33 "https://duckduckgo.com/?q=what%20is%20https%3A%2F%2Fexample.com",
34 );
35 expect(resolveAddressBarInput("what is 10:30")).toBe(
36 "https://duckduckgo.com/?q=what%20is%2010%3A30",
37 );
38 expect(resolveAddressBarInput("note: buy milk")).toBe(
39 "https://duckduckgo.com/?q=note%3A%20buy%20milk",
40 );
41 });
42
43 it("still rejects empty input and unsupported schemes", () => {
44 expect(() => resolveAddressBarInput(" ")).toThrow(PreviewUrlNormalizationError);
45 expect(() => resolveAddressBarInput("ftp://example.com")).toThrow(PreviewUrlNormalizationError);
46 expect(() => resolveAddressBarInput("mailto:alice@example.com")).toThrow(
47 PreviewUrlNormalizationError,
48 );
49 expect(() => resolveAddressBarInput("data:text/plain,hello")).toThrow(
50 PreviewUrlNormalizationError,
51 );
52 // A known scheme with a numeric payload is not a host and port.
53 for (const input of ["ftp:21", "tel:5551234"]) {
54 expect(() => resolveAddressBarInput(input)).toThrow(PreviewUrlNormalizationError);
55 }
56 expect(resolveAddressBarInput("devbox:8080")).toBe("https://devbox:8080/");
57 });
58});
59
60describe("newPreviewTabId", () => {
61 it("returns a unique tab id every call", () => {
62 const a = newPreviewTabId();
63 const b = newPreviewTabId();
64 expect(a).not.toBe(b);
65 expect(a.startsWith("tab_")).toBe(true);
66 });
67});
68
69describe("isLoopbackHost", () => {
70 it.each(["localhost", "127.0.0.1", "0.0.0.0", "::1", "[::1]"])("%s is loopback", (host) => {
71 expect(isLoopbackHost(host)).toBe(true);
72 });
73
74 it.each(["example.com", "192.168.1.10", "10.0.0.1", ""])("%s is not loopback", (host) => {
75 expect(isLoopbackHost(host)).toBe(false);
76 });
77});
78
79describe("normalizePreviewUrl", () => {
80 it("treats bare loopback hosts as http", () => {
81 expect(normalizePreviewUrl("localhost:5173")).toBe("http://localhost:5173/");
82 expect(normalizePreviewUrl("127.0.0.1:3000")).toBe("http://127.0.0.1:3000/");
83 });
84
85 it("treats bare public hosts as https", () => {
86 expect(normalizePreviewUrl("example.com")).toBe("https://example.com/");
87 });
88
89 it("respects explicit schemes", () => {
90 expect(normalizePreviewUrl("https://localhost:5173")).toBe("https://localhost:5173/");
91 expect(normalizePreviewUrl("http://example.com/path?q=1")).toBe("http://example.com/path?q=1");
92 });
93
94 it("rejects empty input", () => {
95 try {
96 normalizePreviewUrl(" ");
97 expect.unreachable("expected URL normalization to fail");
98 } catch (error) {
99 expect(error).toBeInstanceOf(PreviewUrlNormalizationError);
100 expect(error).toMatchObject({ inputLength: 3, reason: "empty" });
101 expect(error).not.toHaveProperty("rawUrl");
102 expect("cause" in (error as object)).toBe(false);
103 }
104 });
105
106 it("rejects unsupported protocols", () => {
107 try {
108 normalizePreviewUrl("ftp://example.com");
109 expect.unreachable("expected URL normalization to fail");
110 } catch (error) {
111 expect(error).toBeInstanceOf(PreviewUrlNormalizationError);
112 expect(error).toMatchObject({
113 inputLength: "ftp://example.com".length,
114 reason: "unsupported-protocol",
115 protocol: "ftp:",
116 });
117 }
118 });
119
120 it("rejects unparseable input without retaining credentials or tokens", () => {
121 const rawUrl = "https://user:password@example.com:bad/path?access_token=secret#fragment";
122 try {
123 normalizePreviewUrl(rawUrl);
124 expect.unreachable("expected URL normalization to fail");
125 } catch (error) {
126 expect(error).toBeInstanceOf(PreviewUrlNormalizationError);
127 expect(error).toMatchObject({
128 inputLength: rawUrl.length,
129 reason: "parse",
130 protocol: "https:",
131 });
132 expect(error).not.toHaveProperty("rawUrl");
133 expect((error as PreviewUrlNormalizationError).cause).toBeInstanceOf(Error);
134 expect((error as PreviewUrlNormalizationError).message).not.toContain(
135 ((error as PreviewUrlNormalizationError).cause as Error).message,
136 );
137 expect((error as PreviewUrlNormalizationError).message).not.toMatch(
138 /user|password|access_token|secret|fragment/,
139 );
140 }
141 });
142});