Something went wrong. Try again.
[READ-ONLY] Mirror of https://github.com/improsocial/impro
An extensible Bluesky client for web impro.social
Something went wrong. Try again.
JavaScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439import { describe, it, beforeEach, afterEach } from "node:test";import assert from "node:assert/strict";import { installFakeIndexedDB } from "../testHelpers.js";import { DraftMediaStore, buildDraftFromComposerSnapshot, getDraftDeviceId,} from "/js/drafts.js";import { createGif } from "../../shared/factories.js";
describe("getDraftDeviceId", () => { it("mints a device id once and returns the same value after", () => { const first = getDraftDeviceId(); assert(first.length > 0); assert.deepEqual(getDraftDeviceId(), first); });});
describe("DraftMediaStore.parseVideoMimeType", () => { it("parses the mime type out of a video key", () => { assert.deepEqual( DraftMediaStore.parseVideoMimeType("video:video/quicktime:abc123.mov"), "video/quicktime", ); });
it("parses legacy keys without a mime segment as mp4", () => { assert.deepEqual( DraftMediaStore.parseVideoMimeType("video:abc123"), "video/mp4", ); });
it("falls back to mp4 for unknown mime segments", () => { assert.deepEqual( DraftMediaStore.parseVideoMimeType("video:video/x-unknown:abc123.bin"), "video/mp4", ); });});
describe("DraftMediaStore", () => { let records; let store; let originalCreateObjectURL; let originalRevokeObjectURL; let mintedUrls; let revokedUrls;
beforeEach(() => { ({ records } = installFakeIndexedDB()); store = new DraftMediaStore("test-media"); originalCreateObjectURL = URL.createObjectURL; originalRevokeObjectURL = URL.revokeObjectURL; let urlCounter = 0; mintedUrls = []; revokedUrls = []; URL.createObjectURL = () => { const url = `blob:fake-${urlCounter++}`; mintedUrls.push(url); return url; }; URL.revokeObjectURL = (url) => revokedUrls.push(url); });
afterEach(() => { delete globalThis.indexedDB; URL.createObjectURL = originalCreateObjectURL; URL.revokeObjectURL = originalRevokeObjectURL; });
describe("load", () => { it("records url entries for present images and null for missing paths", async () => { records.set("image:a", { blob: new Blob(["bytes"]) }); await store.load(["image:a", "image:gone"]); const media = store.$media.get(); assert(media["image:a"].url.startsWith("blob:fake-")); assert.deepEqual(media["image:gone"], null); });
it("checks video presence without minting URLs", async () => { records.set("video:video/mp4:a.mp4", { blob: new Blob(["big"]) }); await store.load(["video:video/mp4:a.mp4", "video:video/mp4:b.mp4"]); assert.deepEqual(store.$media.get(), { "video:video/mp4:a.mp4": { url: null }, "video:video/mp4:b.mp4": null, }); assert.deepEqual(mintedUrls, []); });
it("does not re-read paths that already have entries", async () => { records.set("image:a", { blob: new Blob(["bytes"]) }); await store.load(["image:a"]); const firstUrl = store.$media.get()["image:a"].url; await store.load(["image:a", "image:a"]); assert.deepEqual(store.$media.get()["image:a"].url, firstUrl); assert.deepEqual(mintedUrls.length, 1); assert.deepEqual(revokedUrls, []); });
it("does not re-check paths already recorded as missing", async () => { await store.load(["image:gone"]); assert.deepEqual(store.$media.get()["image:gone"], null); // Bytes appearing later are not picked up - the null entry stands // until a save() records them records.set("image:gone", { blob: new Blob(["late"]) }); await store.load(["image:gone"]); assert.deepEqual(store.$media.get()["image:gone"], null); }); });
it("save writes bytes with a timestamp and records an entry with a URL for images", async () => { await store.save("image:a", new Blob(["bytes"])); assert(records.get("image:a").blob instanceof Blob); assert(typeof records.get("image:a").createdAt === "string"); assert(store.$media.get()["image:a"].url.startsWith("blob:fake-")); });
it("save records video paths without minting a URL", async () => { await store.save("video:video/mp4:a.mp4", new Blob(["x"])); assert(records.has("video:video/mp4:a.mp4")); assert.deepEqual(store.$media.get()["video:video/mp4:a.mp4"], { url: null, }); assert.deepEqual(mintedUrls, []); });
it("re-saving an image replaces the entry and revokes the old URL", async () => { await store.save("image:a", new Blob(["one"])); const firstUrl = store.$media.get()["image:a"].url; await store.save("image:a", new Blob(["two"])); assert(store.$media.get()["image:a"].url !== firstUrl); assert.deepEqual(revokedUrls, [firstUrl]); });
it("readBlob returns raw bytes without minting a URL, null when missing", async () => { const blob = new Blob(["bytes"]); await store.save("video:video/mp4:a.mp4", blob); assert.deepEqual(await store.readBlob("video:video/mp4:a.mp4"), blob); assert.deepEqual(await store.readBlob("image:missing"), null); assert.deepEqual(mintedUrls, []); });
it("delete removes bytes, marks the path missing, and revokes its URL", async () => { await store.save("image:a", new Blob(["bytes"])); const url = store.$media.get()["image:a"].url; await store.delete("image:a"); assert(!records.has("image:a")); assert.deepEqual(store.$media.get()["image:a"], null); assert.deepEqual(revokedUrls, [url]); });
it("propagates write failures without recording an entry", async () => { installFakeIndexedDB({ failWrites: true }); const failingStore = new DraftMediaStore("test-media"); await assert.rejects(() => failingStore.save("image:a", new Blob(["x"]))); assert.deepEqual(failingStore.$media.get(), {}); });
it("fetches string sources (data URLs) into blobs", async () => { const originalFetch = globalThis.fetch; globalThis.fetch = async () => ({ blob: async () => new Blob(["fetched"]), }); try { await store.save("image:from-url", "blob:some-url"); assert(records.get("image:from-url").blob instanceof Blob); } finally { if (originalFetch) { globalThis.fetch = originalFetch; } else { delete globalThis.fetch; } } });});
describe("buildDraftFromComposerSnapshot", () => { function makeDraftSnapshot(overrides = {}) { const { threadgateAllow = null, postgateEmbeddingRules = null, ...postOverrides } = overrides; return { posts: [ { postText: "hello world", images: [], video: null, external: null, quotedRecord: null, labels: null, ...postOverrides, }, ], threadgateAllow, postgateEmbeddingRules, }; }
it("serializes a text-only snapshot into a single-post draft", () => { const { draft, media } = buildDraftFromComposerSnapshot(makeDraftSnapshot()); assert.deepEqual(draft.$type, "app.bsky.draft.defs#draft"); assert.deepEqual(draft.deviceId, getDraftDeviceId()); assert.deepEqual(draft.deviceName, "Web"); assert.deepEqual(draft.posts.length, 1); assert.deepEqual(draft.posts[0].text, "hello world"); assert.deepEqual(draft.posts[0].embedGallery, undefined); assert.deepEqual(media, []); });
it("reuses image localRefPath keys and mints keys for new images", () => { const fileA = {}; const fileB = {}; const { draft, media } = buildDraftFromComposerSnapshot( makeDraftSnapshot({ images: [ { file: fileA, dataUrl: "data:a", localRefPath: "image:existing" }, { file: fileB, dataUrl: "data:b", alt: "a bird" }, { file: {}, dataUrl: "data:c" }, ], }), ); const items = draft.posts[0].embedGallery.items; assert.deepEqual(items.length, 3); assert.deepEqual(items[0].localRef.path, "image:existing"); assert.deepEqual(items[0].alt, undefined); assert(items[1].localRef.path.startsWith("image:")); assert.deepEqual(items[1].alt, "a bird"); assert(items[2].localRef.path !== items[1].localRef.path); assert.deepEqual(media.length, 3); assert.deepEqual(media[0], { path: "image:existing", source: fileA }); assert.deepEqual(media[1].source, fileB); });
it("always mints a fresh video key", () => { const file = { type: "video/webm" }; const first = buildDraftFromComposerSnapshot( makeDraftSnapshot({ video: { file, alt: "clip", captions: null } }), ); const second = buildDraftFromComposerSnapshot( makeDraftSnapshot({ video: { file, alt: "clip", captions: null } }), ); const firstPath = first.draft.posts[0].embedVideos[0].localRef.path; const secondPath = second.draft.posts[0].embedVideos[0].localRef.path; assert(firstPath.startsWith("video:video/webm:")); assert(firstPath.endsWith(".webm")); assert(firstPath !== secondPath); assert.deepEqual(first.draft.posts[0].embedVideos[0].alt, "clip"); assert.deepEqual(first.media, [{ path: firstPath, source: file }]); });
it("mints mp4 video keys for unknown mime types", () => { const { draft } = buildDraftFromComposerSnapshot( makeDraftSnapshot({ video: { file: { type: "video/x-unknown" } } }), ); const path = draft.posts[0].embedVideos[0].localRef.path; assert(path.startsWith("video:video/mp4:")); assert(path.endsWith(".mp4")); });
it("carries video captions through verbatim", () => { const captions = [ { $type: "app.bsky.draft.defs#draftEmbedCaption", lang: "en", content: "hello", }, ]; const { draft } = buildDraftFromComposerSnapshot( makeDraftSnapshot({ video: { file: { type: "video/mp4" }, captions } }), ); assert.deepEqual(draft.posts[0].embedVideos[0].captions, captions); });
it("serializes a gif into a ww/hh/alt external uri with no media entries", () => { const { draft, media } = buildDraftFromComposerSnapshot( makeDraftSnapshot({ gif: { gif: createGif({ id: "dance", width: 498, height: 280 }), alt: "a cat", }, }), ); assert.deepEqual(draft.posts[0].embedExternals, [ { $type: "app.bsky.draft.defs#draftEmbedExternal", uri: "https://static.klipy.com/ii/abc/def/dance.gif?ww=498&hh=280&alt=a+cat", }, ]); assert.deepEqual(media, []); });
it("prefers the gif over a lingering external link", () => { const { draft } = buildDraftFromComposerSnapshot( makeDraftSnapshot({ gif: { gif: createGif({ id: "dance" }), alt: "" }, external: { url: "https://example.com/article", title: "t" }, }), ); assert.deepEqual(draft.posts[0].embedExternals.length, 1); assert( draft.posts[0].embedExternals[0].uri.startsWith( "https://static.klipy.com/", ), ); });
it("serializes external links and quotes without media entries", () => { const { draft, media } = buildDraftFromComposerSnapshot( makeDraftSnapshot({ external: { url: "https://example.com/article", title: "t" }, quotedRecord: { uri: "at://did:plc:a/app.bsky.feed.post/1", cid: "c1", }, }), ); assert.deepEqual(draft.posts[0].embedExternals, [ { $type: "app.bsky.draft.defs#draftEmbedExternal", uri: "https://example.com/article", }, ]); assert.deepEqual(draft.posts[0].embedRecords, [ { $type: "app.bsky.draft.defs#draftEmbedRecord", record: { uri: "at://did:plc:a/app.bsky.feed.post/1", cid: "c1" }, }, ]); assert.deepEqual(media, []); });
it("carries labels, threadgate, and postgate passthrough fields", () => { const labels = { $type: "com.atproto.label.defs#selfLabels", values: [{ val: "porn" }], }; const { draft } = buildDraftFromComposerSnapshot( makeDraftSnapshot({ labels, threadgateAllow: [{ $type: "app.bsky.feed.threadgate#followingRule" }], postgateEmbeddingRules: [ { $type: "app.bsky.feed.postgate#disableRule" }, ], }), ); assert.deepEqual(draft.posts[0].labels, labels); assert.deepEqual(draft.threadgateAllow.length, 1); assert.deepEqual(draft.postgateEmbeddingRules.length, 1); });
it("omits empty postgate rules", () => { const { draft } = buildDraftFromComposerSnapshot( makeDraftSnapshot({ postgateEmbeddingRules: [] }), ); assert.deepEqual(draft.postgateEmbeddingRules, undefined); });
it("carries unrestored media embeds through verbatim", () => { const unrestoredImages = [ { $type: "app.bsky.draft.defs#draftEmbedImage", alt: "kept", localRef: { $type: "app.bsky.draft.defs#draftEmbedLocalRef", path: "image:foreign", }, }, ]; const unrestoredVideo = { $type: "app.bsky.draft.defs#draftEmbedVideo", localRef: { $type: "app.bsky.draft.defs#draftEmbedLocalRef", path: "video:video/mp4:foreign.mp4", }, }; const { draft, media } = buildDraftFromComposerSnapshot( makeDraftSnapshot({ unrestoredImages, unrestoredVideo }), ); assert.deepEqual(draft.posts[0].embedGallery.items, unrestoredImages); assert.deepEqual(draft.posts[0].embedVideos, [unrestoredVideo]); assert.deepEqual(media, []); });
it("composer media wins over unrestored media", () => { const videoFile = { type: "video/mp4" }; const { draft, media } = buildDraftFromComposerSnapshot( makeDraftSnapshot({ images: [{ file: {}, dataUrl: "data:a" }], video: { file: videoFile, alt: "", captions: null }, unrestoredImages: [ { $type: "app.bsky.draft.defs#draftEmbedImage", localRef: { $type: "app.bsky.draft.defs#draftEmbedLocalRef", path: "image:foreign", }, }, ], unrestoredVideo: { $type: "app.bsky.draft.defs#draftEmbedVideo", localRef: { $type: "app.bsky.draft.defs#draftEmbedLocalRef", path: "video:video/mp4:foreign.mp4", }, }, }), ); const items = draft.posts[0].embedGallery.items; assert.deepEqual(items.length, 1); assert(items[0].localRef.path !== "image:foreign"); assert.deepEqual(draft.posts[0].embedVideos.length, 1); assert( draft.posts[0].embedVideos[0].localRef.path !== "video:video/mp4:foreign.mp4", ); assert.deepEqual(media.length, 2); });});
describe("DraftMediaStore.parseVideoExtension", () => { it("derives the extension from the key's mime type", () => { assert.deepEqual( DraftMediaStore.parseVideoExtension("video:video/quicktime:abc.mov"), "mov", ); });
it("defaults to mp4 for legacy keys without a mime segment", () => { assert.deepEqual( DraftMediaStore.parseVideoExtension("video:abc123"), "mp4", ); });});