138 lines
4.2 KiB
TypeScript
138 lines
4.2 KiB
TypeScript
import { describe, it, expect, vi, afterEach } from "vitest";
|
|
import {
|
|
buildMailtoShareHref,
|
|
buildSlackWebShareUrl,
|
|
DISCORD_NATIVE_DM_HUB_URL,
|
|
DISCORD_WEB_DM_HUB_URL,
|
|
NATIVE_SHARE_FALLBACK_DELAY_MS,
|
|
type NativeFallbackTimers,
|
|
scheduleNativeSchemeThenFallback,
|
|
SLACK_NATIVE_OPEN_URL,
|
|
} from "../../../lib/create/shareChannels";
|
|
|
|
describe("shareChannels", () => {
|
|
afterEach(() => {
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
it("buildSlackWebShareUrl encodes the outgoing URL query value", () => {
|
|
expect(buildSlackWebShareUrl("https://example.com/rules/r1")).toBe(
|
|
"https://slack.com/share?url=https%3A%2F%2Fexample.com%2Frules%2Fr1",
|
|
);
|
|
expect(
|
|
buildSlackWebShareUrl("https://example.com/rules/a?b=c&d=e"),
|
|
).toBe(
|
|
"https://slack.com/share?url=https%3A%2F%2Fexample.com%2Frules%2Fa%3Fb%3Dc%26d%3De",
|
|
);
|
|
});
|
|
|
|
it("buildMailtoShareHref percent-encodes subject and body including newlines", () => {
|
|
expect(
|
|
buildMailtoShareHref({
|
|
subject: "Hello & welcome",
|
|
body: "Line one\n\nhttps://x.com/y z",
|
|
}),
|
|
).toBe(
|
|
"mailto:?subject=Hello%20%26%20welcome&body=Line%20one%0A%0Ahttps%3A%2F%2Fx.com%2Fy%20z",
|
|
);
|
|
});
|
|
|
|
it("buildMailtoShareHref handles unicode", () => {
|
|
const href = buildMailtoShareHref({
|
|
subject: "日本語",
|
|
body: "café ☕",
|
|
});
|
|
expect(href.startsWith("mailto:?subject=")).toBe(true);
|
|
expect(href).toContain(encodeURIComponent("日本語"));
|
|
expect(href).toContain(encodeURIComponent("café ☕"));
|
|
});
|
|
|
|
it("exposes Discord native + web DM hub URL constants", () => {
|
|
expect(DISCORD_WEB_DM_HUB_URL).toBe("https://discord.com/channels/@me");
|
|
expect(DISCORD_NATIVE_DM_HUB_URL).toBe("discord://-/channels/@me");
|
|
});
|
|
|
|
it("scheduleNativeSchemeThenFallback skips native assign and invokes fallback synchronously when URL is not allowlisted", () => {
|
|
const assign = vi.fn();
|
|
const fb = vi.fn();
|
|
const timers: NativeFallbackTimers = {
|
|
setTimeout: (): unknown => 0,
|
|
clearTimeout: vi.fn(),
|
|
};
|
|
|
|
scheduleNativeSchemeThenFallback(
|
|
"javascript:alert(1)",
|
|
fb,
|
|
{
|
|
assignLocationHref: assign,
|
|
getVisibilityState: (): Document["visibilityState"] => "visible",
|
|
onVisibilityChange: () => {},
|
|
offVisibilityChange: () => {},
|
|
},
|
|
timers,
|
|
);
|
|
|
|
expect(assign).not.toHaveBeenCalled();
|
|
expect(fb).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("scheduleNativeSchemeThenFallback triggers fallback once after timeout when tab stays visible", () => {
|
|
vi.useFakeTimers();
|
|
|
|
const assign = vi.fn();
|
|
const fb = vi.fn();
|
|
|
|
scheduleNativeSchemeThenFallback(
|
|
SLACK_NATIVE_OPEN_URL,
|
|
fb,
|
|
{
|
|
assignLocationHref: assign,
|
|
getVisibilityState: (): Document["visibilityState"] => "visible",
|
|
onVisibilityChange: () => {},
|
|
offVisibilityChange: () => {},
|
|
},
|
|
window as unknown as NativeFallbackTimers,
|
|
NATIVE_SHARE_FALLBACK_DELAY_MS,
|
|
);
|
|
|
|
expect(assign).toHaveBeenCalledWith(SLACK_NATIVE_OPEN_URL);
|
|
|
|
vi.advanceTimersByTime(NATIVE_SHARE_FALLBACK_DELAY_MS - 1);
|
|
expect(fb).not.toHaveBeenCalled();
|
|
|
|
vi.advanceTimersByTime(10);
|
|
expect(fb).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("scheduleNativeSchemeThenFallback cancels fallback when visibility becomes hidden before timeout", () => {
|
|
vi.useFakeTimers();
|
|
let vis: Document["visibilityState"] = "visible";
|
|
const listeners: (() => void)[] = [];
|
|
const fb = vi.fn();
|
|
|
|
scheduleNativeSchemeThenFallback(
|
|
DISCORD_NATIVE_DM_HUB_URL,
|
|
fb,
|
|
{
|
|
assignLocationHref: vi.fn(),
|
|
getVisibilityState: (): Document["visibilityState"] => vis,
|
|
onVisibilityChange: (l: () => void): void => {
|
|
listeners.push(l);
|
|
},
|
|
offVisibilityChange: (l: () => void): void => {
|
|
const idx = listeners.indexOf(l);
|
|
if (idx >= 0) listeners.splice(idx, 1);
|
|
},
|
|
},
|
|
window as unknown as NativeFallbackTimers,
|
|
NATIVE_SHARE_FALLBACK_DELAY_MS,
|
|
);
|
|
|
|
vis = "hidden";
|
|
listeners.forEach((l) => l());
|
|
|
|
vi.advanceTimersByTime(NATIVE_SHARE_FALLBACK_DELAY_MS + 200);
|
|
expect(fb).not.toHaveBeenCalled();
|
|
});
|
|
});
|