134 lines
5.0 KiB
Plaintext
134 lines
5.0 KiB
Plaintext
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
|
|
|
const URLDescriptor = Object.getOwnPropertyDescriptor(globalThis, "URL")!;
|
|
const NativeURL = globalThis.URL;
|
|
const canParseDescriptor = Object.getOwnPropertyDescriptor(NativeURL, "canParse")!;
|
|
const parseDescriptor = Object.getOwnPropertyDescriptor(NativeURL, "parse")!;
|
|
|
|
function restoreURL() {
|
|
Object.defineProperty(globalThis, "URL", URLDescriptor);
|
|
Object.defineProperty(NativeURL, "canParse", canParseDescriptor);
|
|
Object.defineProperty(NativeURL, "parse", parseDescriptor);
|
|
}
|
|
|
|
describe("URL parser fallbacks", () => {
|
|
beforeEach(() => {
|
|
restoreURL();
|
|
vi.resetModules();
|
|
});
|
|
|
|
afterEach(restoreURL);
|
|
|
|
test("uses the boolean and object APIs on modern runtimes", async () => {
|
|
const canParse = vi.spyOn(NativeURL, "canParse");
|
|
const URLStatic = NativeURL as typeof URL & { parse: (input: string) => URL | null };
|
|
const parse = vi.spyOn(URLStatic, "parse");
|
|
const z = await import("../../index.js");
|
|
|
|
expect(z.validate(z.url(), "https://example.com")).toBe(true);
|
|
expect(canParse).toHaveBeenCalledOnce();
|
|
expect(parse).not.toHaveBeenCalled();
|
|
|
|
canParse.mockClear();
|
|
expect(z.validate(z.url({ normalize: true }), "https://example.com")).toBe(true);
|
|
expect(parse).toHaveBeenCalledOnce();
|
|
expect(canParse).not.toHaveBeenCalled();
|
|
|
|
parse.mockClear();
|
|
expect(z.validate(z.ipv6(), "2001:db8::1")).toBe(true);
|
|
expect(canParse).toHaveBeenCalledOnce();
|
|
expect(parse).not.toHaveBeenCalled();
|
|
});
|
|
|
|
test("URL fast-path selection preserves live option getter reads", async () => {
|
|
const z = await import("../../index.js");
|
|
const postProcessor = z.core.globalConfig.postProcessor;
|
|
z.core.globalConfig.postProcessor = undefined;
|
|
try {
|
|
for (const inherited of [false, true]) {
|
|
const schema = z.url();
|
|
const target = inherited ? Object.create(Object.getPrototypeOf(schema.def)) : schema.def;
|
|
let reads = 0;
|
|
Object.defineProperty(target, "hostname", {
|
|
get: () => (++reads === 1 ? /^example\.com$/ : undefined),
|
|
});
|
|
if (inherited) Object.setPrototypeOf(schema.def, target);
|
|
expect(z.validate(schema, "https://example.com")).toBe(false);
|
|
expect(reads).toBe(2);
|
|
reads = 0;
|
|
expect(schema.safeParse("https://example.com").success).toBe(false);
|
|
expect(reads).toBe(2);
|
|
}
|
|
} finally {
|
|
z.core.globalConfig.postProcessor = postProcessor;
|
|
}
|
|
});
|
|
|
|
test("the exported object parser still returns a URL", async () => {
|
|
const { parseURLObject } = await import("../schemas.js");
|
|
const result = parseURLObject("https://example.com", {});
|
|
expect(result).toBeInstanceOf(NativeURL);
|
|
if (typeof result === "number") expect.unreachable();
|
|
expect(result.hostname).toBe("example.com");
|
|
expect(parseURLObject("invalid", {})).toBe(2);
|
|
});
|
|
|
|
test("falls back when URL.canParse is absent", async () => {
|
|
Reflect.deleteProperty(NativeURL, "canParse");
|
|
const z = await import("../../index.js");
|
|
const cases = [
|
|
[z.url(), "https://example.com", "not a url"],
|
|
[z.ipv6(), "2001:db8::1", "not-an-ip"],
|
|
[z.cidrv6(), "2001:db8::/32", "not-an-ip/32"],
|
|
] as const;
|
|
|
|
for (const [schema, valid, invalid] of cases) {
|
|
for (const subject of [schema, z.compile(schema)]) {
|
|
expect(z.validate(subject, valid)).toBe(true);
|
|
expect(z.validate(subject, invalid)).toBe(false);
|
|
expect(subject.safeParse(valid).success).toBe(true);
|
|
expect(subject.safeParse(invalid).success).toBe(false);
|
|
}
|
|
}
|
|
});
|
|
|
|
test("falls back when URL.parse is absent", async () => {
|
|
Reflect.deleteProperty(NativeURL, "parse");
|
|
const z = await import("../../index.js");
|
|
const cases = [
|
|
[z.url({ normalize: true }), "https://example.com", "not a url"],
|
|
[z.url({ hostname: /^example\.com$/ }), "https://example.com", "https://other.com"],
|
|
[z.httpUrl(), "https://example.com", "mailto:a@example.com"],
|
|
] as const;
|
|
|
|
for (const [schema, valid, invalid] of cases) {
|
|
for (const subject of [schema, z.compile(schema)]) {
|
|
expect(z.validate(subject, valid)).toBe(true);
|
|
expect(z.validate(subject, invalid)).toBe(false);
|
|
expect(subject.safeParse(valid).success).toBe(true);
|
|
expect(subject.safeParse(invalid).success).toBe(false);
|
|
}
|
|
}
|
|
});
|
|
|
|
test("rejects without a global URL constructor", async () => {
|
|
const z = await import("../../index.js");
|
|
const cases = [
|
|
[z.url(), "https://example.com"],
|
|
[z.url({ normalize: true }), "https://example.com"],
|
|
[z.ipv6(), "2001:db8::1"],
|
|
[z.cidrv6(), "2001:db8::/32"],
|
|
] as const;
|
|
const subjects = cases.flatMap(([schema, input]) => [
|
|
[schema, input] as const,
|
|
[z.compile(schema), input] as const,
|
|
]);
|
|
Object.defineProperty(globalThis, "URL", { configurable: true, value: undefined });
|
|
|
|
for (const [schema, input] of subjects) {
|
|
expect(z.validate(schema, input)).toBe(false);
|
|
expect(schema.safeParse(input).success).toBe(false);
|
|
}
|
|
});
|
|
});
|