clone

a deno framework for local-first web apps that open instantly, work offline and sync when users choose

feat/184-task-notice-migration
lofi / package / recipes / web-share_test.ts
web-share_test.ts 3.7K
1import { parseTextShareTarget, shareOrFallback, type WebShareClient } from "./web-share.ts";23function assert(condition: unknown, message: string): asserts condition {4 if (!condition) throw new Error(message);5}67Deno.test("share recipe uses native share only for a supported user-triggered payload", async () => {8 const calls: string[] = [];9 const client: WebShareClient = {10 canShare: (data) => {11 calls.push(`can:${data.title}`);12 return true;13 },14 share: (data) => {15 calls.push(`share:${data.title}`);16 return Promise.resolve();17 },18 };19 const outcome = await shareOrFallback({ title: "One task" }, {20 client,21 fallback: () => {22 calls.push("fallback");23 },24 });25 assert(outcome === "shared", `native share ended in ${outcome}`);26 assert(calls.join(",") === "can:One task,share:One task", "native share order drifted");27});2829Deno.test("share recipe falls back only when native sharing is unavailable", async () => {30 let fallbackCount = 0;31 const unavailable = await shareOrFallback({ text: "Task" }, {32 client: {},33 fallback: () => {34 fallbackCount++;35 },36 });37 const rejected = await shareOrFallback({ text: "Task" }, {38 client: { canShare: () => false, share: () => Promise.resolve() },39 fallback: () => {40 fallbackCount++;41 },42 });43 assert(unavailable === "fallback" && rejected === "fallback", "fallback state was not stable");44 assert(fallbackCount === 2, `expected two fallback calls, received ${fallbackCount}`);45});4647Deno.test("share recipe distinguishes cancellation from runtime failure", async () => {48 let fallbackCalled = false;49 const cancelled = await shareOrFallback({ text: "Task" }, {50 client: { share: () => Promise.reject(new DOMException("closed", "AbortError")) },51 fallback: () => {52 fallbackCalled = true;53 },54 });55 const failed = await shareOrFallback({ text: "Task" }, {56 client: { share: () => Promise.reject(new Error("browser failure")) },57 fallback: () => {58 fallbackCalled = true;59 },60 });61 assert(cancelled === "cancelled", `cancellation ended in ${cancelled}`);62 assert(failed === "failed", `runtime failure ended in ${failed}`);63 assert(!fallbackCalled, "native sheet failure unexpectedly copied shared data");64});6566Deno.test("text share target accepts bounded title, text, and HTTP URLs", () => {67 const result = parseTextShareTarget(68 "?title=One+task&text=Review+it&url=https%3A%2F%2Fexample.com%2Fnotes&ignored=1",69 );70 assert(result.ok, `valid share was rejected: ${JSON.stringify(result)}`);71 assert(result.draft.title === "One task", "share title was not decoded");72 assert(result.draft.text === "Review it", "share text was not decoded");73 assert(result.draft.url === "https://example.com/notes", "share URL was not normalized");74 assert(!("ignored" in result.draft), "unknown input reached the draft");75});7677Deno.test("text share target rejects duplicate, oversized, malformed, and dangerous input", () => {78 const duplicate = parseTextShareTarget("?title=one&title=two");79 assert(!duplicate.ok && duplicate.issues.includes("duplicate-title"), "duplicate title passed");8081 const oversized = parseTextShareTarget(`?text=${"x".repeat(2_001)}`);82 assert(!oversized.ok && oversized.issues.includes("text-too-long"), "oversized text passed");8384 const malformed = parseTextShareTarget("?url=not-a-url");85 assert(!malformed.ok && malformed.issues.includes("invalid-url"), "malformed URL passed");8687 const dangerous = parseTextShareTarget("?url=javascript%3Aalert(1)");88 assert(89 !dangerous.ok && dangerous.issues.includes("unsupported-url-protocol"),90 "dangerous URL protocol passed",91 );9293 const empty = parseTextShareTarget("?unknown=value");94 assert(!empty.ok && empty.issues.includes("empty-share"), "empty share passed");95});