A music player that connects to your cloud/distributed storage.
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

chore: transformer components tests

+651
+131
tests/components/transformer/output/bytes/json/test.ts
··· 1 + import { describe, it } from "@std/testing/bdd"; 2 + import { expect } from "@std/expect"; 3 + 4 + import { testWeb } from "@tests/common/index.ts"; 5 + 6 + describe("components/transformer/output/bytes/json", () => { 7 + it("ready returns false when no output is connected", async () => { 8 + const result = await testWeb(async () => { 9 + const mod = await import( 10 + "~/components/transformer/output/bytes/json/element.js" 11 + ); 12 + const t = new mod.CLASS(); 13 + return t.ready(); 14 + }); 15 + 16 + expect(result).toBe(false); 17 + }); 18 + 19 + it("tracks.collection is loading when no output is connected", async () => { 20 + const result = await testWeb(async () => { 21 + const mod = await import( 22 + "~/components/transformer/output/bytes/json/element.js" 23 + ); 24 + const t = new mod.CLASS(); 25 + return t.tracks.collection().state; 26 + }); 27 + 28 + expect(result).toBe("loading"); 29 + }); 30 + 31 + it("tracks save and collection round-trip", async () => { 32 + const result = await testWeb(async () => { 33 + const idbMod = await import( 34 + "~/components/output/polymorphic/indexed-db/element.js" 35 + ); 36 + const mod = await import( 37 + "~/components/transformer/output/bytes/json/element.js" 38 + ); 39 + 40 + const output = new idbMod.CLASS(); 41 + output.id = "test-idb"; 42 + document.body.append(output); 43 + 44 + const t = new mod.CLASS(); 45 + t.setAttribute("output-selector", "#test-idb"); 46 + document.body.append(t); 47 + 48 + await t.tracks.save([ 49 + { 50 + $type: "sh.diffuse.output.track", 51 + id: "track-1", 52 + uri: "https://example.com/track1.mp3", 53 + }, 54 + { 55 + $type: "sh.diffuse.output.track", 56 + id: "track-2", 57 + uri: "https://example.com/track2.mp3", 58 + }, 59 + ]); 60 + 61 + const col = t.tracks.collection(); 62 + if (col.state !== "loaded") return null; 63 + return (col.data as Array<{ id: string }>).map((tr) => tr.id); 64 + }); 65 + 66 + expect(result).toEqual(["track-1", "track-2"]); 67 + }); 68 + 69 + it("facets save and collection round-trip", async () => { 70 + const result = await testWeb(async () => { 71 + const idbMod = await import( 72 + "~/components/output/polymorphic/indexed-db/element.js" 73 + ); 74 + const mod = await import( 75 + "~/components/transformer/output/bytes/json/element.js" 76 + ); 77 + 78 + const output = new idbMod.CLASS(); 79 + output.id = "test-idb-facets"; 80 + document.body.append(output); 81 + 82 + const t = new mod.CLASS(); 83 + t.setAttribute("output-selector", "#test-idb-facets"); 84 + document.body.append(t); 85 + 86 + await t.facets.save([ 87 + { 88 + $type: "sh.diffuse.output.facet", 89 + id: "f1", 90 + name: "Favourites", 91 + }, 92 + ]); 93 + 94 + const col = t.facets.collection(); 95 + if (col.state !== "loaded") return null; 96 + return (col.data as Array<{ id: string; name: string }>).map((f) => ({ 97 + id: f.id, 98 + name: f.name, 99 + })); 100 + }); 101 + 102 + expect(result).toEqual([{ id: "f1", name: "Favourites" }]); 103 + }); 104 + 105 + it("saving empty array results in empty loaded collection", async () => { 106 + const result = await testWeb(async () => { 107 + const idbMod = await import( 108 + "~/components/output/polymorphic/indexed-db/element.js" 109 + ); 110 + const mod = await import( 111 + "~/components/transformer/output/bytes/json/element.js" 112 + ); 113 + 114 + const output = new idbMod.CLASS(); 115 + output.id = "test-idb-empty"; 116 + document.body.append(output); 117 + 118 + const t = new mod.CLASS(); 119 + t.setAttribute("output-selector", "#test-idb-empty"); 120 + document.body.append(t); 121 + 122 + await t.tracks.save([]); 123 + 124 + const col = t.tracks.collection(); 125 + if (col.state !== "loaded") return null; 126 + return (col.data as unknown[]).length; 127 + }); 128 + 129 + expect(result).toBe(0); 130 + }); 131 + });
+55
tests/components/transformer/output/raw/atproto-sync/test.ts
··· 1 + import { describe, it } from "@std/testing/bdd"; 2 + import { expect } from "@std/expect"; 3 + 4 + import { testWeb } from "@tests/common/index.ts"; 5 + 6 + describe("components/transformer/output/raw/atproto-sync", () => { 7 + it("ready returns true", async () => { 8 + const result = await testWeb(async () => { 9 + const mod = await import( 10 + "~/components/transformer/output/raw/atproto-sync/element.js" 11 + ); 12 + const t = new mod.CLASS(); 13 + return t.ready(); 14 + }); 15 + 16 + expect(result).toBe(true); 17 + }); 18 + 19 + it("tracks.collection is loading when no local output is connected", async () => { 20 + const result = await testWeb(async () => { 21 + const mod = await import( 22 + "~/components/transformer/output/raw/atproto-sync/element.js" 23 + ); 24 + const t = new mod.CLASS() as any; 25 + // Not connected to DOM — #localOutput stays undefined 26 + return t.tracks.collection().state; 27 + }); 28 + 29 + expect(result).toBe("loading"); 30 + }); 31 + 32 + it("facets.collection is loading when no local output is connected", async () => { 33 + const result = await testWeb(async () => { 34 + const mod = await import( 35 + "~/components/transformer/output/raw/atproto-sync/element.js" 36 + ); 37 + const t = new mod.CLASS() as any; 38 + return t.facets.collection().state; 39 + }); 40 + 41 + expect(result).toBe("loading"); 42 + }); 43 + 44 + it("playlistItems.collection is loading when no local output is connected", async () => { 45 + const result = await testWeb(async () => { 46 + const mod = await import( 47 + "~/components/transformer/output/raw/atproto-sync/element.js" 48 + ); 49 + const t = new mod.CLASS() as any; 50 + return t.playlistItems.collection().state; 51 + }); 52 + 53 + expect(result).toBe("loading"); 54 + }); 55 + });
+167
tests/components/transformer/output/refiner/default/test.ts
··· 1 + import { describe, it } from "@std/testing/bdd"; 2 + import { expect } from "@std/expect"; 3 + 4 + import { testWeb } from "@tests/common/index.ts"; 5 + 6 + describe("components/transformer/output/refiner/default", () => { 7 + it("ready returns false when no output is connected", async () => { 8 + const result = await testWeb(async () => { 9 + const mod = await import( 10 + "~/components/transformer/output/refiner/default/element.js" 11 + ); 12 + const t = new mod.CLASS(); 13 + return t.ready(); 14 + }); 15 + 16 + expect(result).toBe(false); 17 + }); 18 + 19 + it("tracks.collection is loading when no output is connected", async () => { 20 + const result = await testWeb(async () => { 21 + const mod = await import( 22 + "~/components/transformer/output/refiner/default/element.js" 23 + ); 24 + const t = new mod.CLASS(); 25 + return t.tracks.collection().state; 26 + }); 27 + 28 + expect(result).toBe("loading"); 29 + }); 30 + 31 + it("non-ephemeral tracks are saved to backing output and appear in collection", async () => { 32 + const result = await testWeb(async () => { 33 + const idbMod = await import( 34 + "~/components/output/polymorphic/indexed-db/element.js" 35 + ); 36 + const mod = await import( 37 + "~/components/transformer/output/refiner/default/element.js" 38 + ); 39 + 40 + const output = new idbMod.CLASS(); 41 + output.id = "test-idb"; 42 + document.body.append(output); 43 + 44 + const t = new mod.CLASS(); 45 + t.setAttribute("output-selector", "#test-idb"); 46 + document.body.append(t); 47 + 48 + await t.tracks.save([ 49 + { 50 + $type: "sh.diffuse.output.track", 51 + id: "track-1", 52 + uri: "https://example.com/track1.mp3", 53 + }, 54 + ]); 55 + 56 + const col = t.tracks.collection(); 57 + if (col.state !== "loaded") return null; 58 + return (col.data as Array<{ id: string }>).map((tr) => tr.id); 59 + }); 60 + 61 + expect(result).toEqual(["track-1"]); 62 + }); 63 + 64 + it("ephemeral tracks appear in transformer collection but not in backing output", async () => { 65 + const result = await testWeb(async () => { 66 + const idbMod = await import( 67 + "~/components/output/polymorphic/indexed-db/element.js" 68 + ); 69 + const mod = await import( 70 + "~/components/transformer/output/refiner/default/element.js" 71 + ); 72 + 73 + const output = new idbMod.CLASS(); 74 + output.id = "test-idb-eph"; 75 + document.body.append(output); 76 + 77 + const t = new mod.CLASS(); 78 + t.setAttribute("output-selector", "#test-idb-eph"); 79 + document.body.append(t); 80 + 81 + await t.tracks.save([ 82 + { 83 + $type: "sh.diffuse.output.track", 84 + id: "regular", 85 + uri: "https://example.com/regular.mp3", 86 + }, 87 + { 88 + $type: "sh.diffuse.output.track", 89 + id: "eph", 90 + uri: "https://example.com/eph.mp3", 91 + ephemeral: true, 92 + }, 93 + ]); 94 + 95 + const transformerCol = t.tracks.collection(); 96 + const backingCol = output.tracks.collection(); 97 + 98 + if (transformerCol.state !== "loaded") return null; 99 + if (backingCol.state !== "loaded") return null; 100 + 101 + return { 102 + transformerIds: (transformerCol.data as Array<{ id: string }>) 103 + .map((tr) => tr.id) 104 + .sort(), 105 + backingIds: (backingCol.data as Array<{ id: string }>) 106 + .map((tr) => tr.id) 107 + .sort(), 108 + }; 109 + }); 110 + 111 + expect(result?.transformerIds).toEqual(["eph", "regular"]); 112 + expect(result?.backingIds).toEqual(["regular"]); 113 + }); 114 + 115 + it("ephemeral playlist items appear in transformer collection but not in backing output", async () => { 116 + const result = await testWeb(async () => { 117 + const idbMod = await import( 118 + "~/components/output/polymorphic/indexed-db/element.js" 119 + ); 120 + const mod = await import( 121 + "~/components/transformer/output/refiner/default/element.js" 122 + ); 123 + 124 + const output = new idbMod.CLASS(); 125 + output.id = "test-idb-pl"; 126 + document.body.append(output); 127 + 128 + const t = new mod.CLASS(); 129 + t.setAttribute("output-selector", "#test-idb-pl"); 130 + document.body.append(t); 131 + 132 + await t.playlistItems.save([ 133 + { 134 + $type: "sh.diffuse.output.playlistItem", 135 + id: "pl-1", 136 + playlist: "My Playlist", 137 + criteria: [], 138 + }, 139 + { 140 + $type: "sh.diffuse.output.playlistItem", 141 + id: "pl-eph", 142 + playlist: "Ephemeral Playlist", 143 + criteria: [], 144 + ephemeral: true, 145 + }, 146 + ]); 147 + 148 + const transformerCol = t.playlistItems.collection(); 149 + const backingCol = output.playlistItems.collection(); 150 + 151 + if (transformerCol.state !== "loaded") return null; 152 + if (backingCol.state !== "loaded") return null; 153 + 154 + return { 155 + transformerIds: (transformerCol.data as Array<{ id: string }>) 156 + .map((pl) => pl.id) 157 + .sort(), 158 + backingIds: (backingCol.data as Array<{ id: string }>) 159 + .map((pl) => pl.id) 160 + .sort(), 161 + }; 162 + }); 163 + 164 + expect(result?.transformerIds).toEqual(["pl-1", "pl-eph"]); 165 + expect(result?.backingIds).toEqual(["pl-1"]); 166 + }); 167 + });
+161
tests/components/transformer/output/refiner/initial-contents/test.ts
··· 1 + import { describe, it } from "@std/testing/bdd"; 2 + import { expect } from "@std/expect"; 3 + 4 + import { testWeb } from "@tests/common/index.ts"; 5 + 6 + describe("components/transformer/output/refiner/initial-contents", () => { 7 + it("ready returns false when no output is connected", async () => { 8 + const result = await testWeb(async () => { 9 + const mod = await import( 10 + "~/components/transformer/output/refiner/initial-contents/element.js" 11 + ); 12 + const t = new mod.CLASS(); 13 + return t.ready(); 14 + }); 15 + 16 + expect(result).toBe(false); 17 + }); 18 + 19 + it("facets.collection is loading when no output is connected", async () => { 20 + const result = await testWeb(async () => { 21 + const mod = await import( 22 + "~/components/transformer/output/refiner/initial-contents/element.js" 23 + ); 24 + const t = new mod.CLASS(); 25 + return t.facets.collection().state; 26 + }); 27 + 28 + expect(result).toBe("loading"); 29 + }); 30 + 31 + it("facets.collection returns default facets when backing output is empty and not yet initialized", async () => { 32 + const result = await testWeb(async () => { 33 + const idbMod = await import( 34 + "~/components/output/polymorphic/indexed-db/element.js" 35 + ); 36 + const mod = await import( 37 + "~/components/transformer/output/refiner/initial-contents/element.js" 38 + ); 39 + 40 + const output = new idbMod.CLASS(); 41 + output.id = "test-idb"; 42 + document.body.append(output); 43 + 44 + const t = new mod.CLASS(); 45 + t.setAttribute("output-selector", "#test-idb"); 46 + document.body.append(t); 47 + 48 + // Save empty to backing so it transitions to "loaded" state 49 + await output.facets.save([]); 50 + 51 + // Wait for the IDB flag check in the constructor to resolve 52 + await new Promise((r) => setTimeout(r, 50)); 53 + 54 + const col = t.facets.collection(); 55 + if (col.state !== "loaded") return null; 56 + return (col.data as unknown[]).length > 0; 57 + }); 58 + 59 + // Should return default facets (non-empty) since not yet initialized 60 + expect(result).toBe(true); 61 + }); 62 + 63 + it("facets.collection returns empty array after explicit save([]) marks as initialized", async () => { 64 + const result = await testWeb(async () => { 65 + const idbMod = await import( 66 + "~/components/output/polymorphic/indexed-db/element.js" 67 + ); 68 + const mod = await import( 69 + "~/components/transformer/output/refiner/initial-contents/element.js" 70 + ); 71 + 72 + const output = new idbMod.CLASS(); 73 + output.id = "test-idb-init"; 74 + document.body.append(output); 75 + 76 + const t = new mod.CLASS(); 77 + t.setAttribute("output-selector", "#test-idb-init"); 78 + document.body.append(t); 79 + 80 + // Wait for the IDB flag check to resolve 81 + await new Promise((r) => setTimeout(r, 50)); 82 + 83 + // Explicit save marks as initialized (even with empty array) 84 + await t.facets.save([]); 85 + 86 + const col = t.facets.collection(); 87 + if (col.state !== "loaded") return null; 88 + return (col.data as unknown[]).length; 89 + }); 90 + 91 + expect(result).toBe(0); 92 + }); 93 + 94 + it("facets.collection returns provided facets after save with data", async () => { 95 + const result = await testWeb(async () => { 96 + const idbMod = await import( 97 + "~/components/output/polymorphic/indexed-db/element.js" 98 + ); 99 + const mod = await import( 100 + "~/components/transformer/output/refiner/initial-contents/element.js" 101 + ); 102 + 103 + const output = new idbMod.CLASS(); 104 + output.id = "test-idb-data"; 105 + document.body.append(output); 106 + 107 + const t = new mod.CLASS(); 108 + t.setAttribute("output-selector", "#test-idb-data"); 109 + document.body.append(t); 110 + 111 + await new Promise((r) => setTimeout(r, 50)); 112 + 113 + await t.facets.save([ 114 + { 115 + $type: "sh.diffuse.output.facet", 116 + id: "my-facet", 117 + name: "My Playlist", 118 + }, 119 + ]); 120 + 121 + const col = t.facets.collection(); 122 + if (col.state !== "loaded") return null; 123 + return (col.data as Array<{ id: string }>).map((f) => f.id); 124 + }); 125 + 126 + expect(result).toEqual(["my-facet"]); 127 + }); 128 + 129 + it("tracks and playlistItems delegate to backing output unchanged", async () => { 130 + const result = await testWeb(async () => { 131 + const idbMod = await import( 132 + "~/components/output/polymorphic/indexed-db/element.js" 133 + ); 134 + const mod = await import( 135 + "~/components/transformer/output/refiner/initial-contents/element.js" 136 + ); 137 + 138 + const output = new idbMod.CLASS(); 139 + output.id = "test-idb-tracks"; 140 + document.body.append(output); 141 + 142 + const t = new mod.CLASS(); 143 + t.setAttribute("output-selector", "#test-idb-tracks"); 144 + document.body.append(t); 145 + 146 + await t.tracks.save([ 147 + { 148 + $type: "sh.diffuse.output.track", 149 + id: "t1", 150 + uri: "https://example.com/track.mp3", 151 + }, 152 + ]); 153 + 154 + const col = t.tracks.collection(); 155 + if (col.state !== "loaded") return null; 156 + return (col.data as Array<{ id: string }>).map((tr) => tr.id); 157 + }); 158 + 159 + expect(result).toEqual(["t1"]); 160 + }); 161 + });
+137
tests/components/transformer/output/refiner/track-uri-passkey/test.ts
··· 1 + import { describe, it } from "@std/testing/bdd"; 2 + import { expect } from "@std/expect"; 3 + 4 + import { testWeb } from "@tests/common/index.ts"; 5 + 6 + describe("components/transformer/output/refiner/track-uri-passkey", () => { 7 + // Crypto utilities from passkey.js 8 + 9 + it("isEncryptedUri returns true for encrypted:// URIs", async () => { 10 + const result = await testWeb(async () => { 11 + const mod = await import( 12 + "~/components/transformer/output/refiner/track-uri-passkey/passkey.js" 13 + ); 14 + return mod.isEncryptedUri("encrypted://abc123def456"); 15 + }); 16 + 17 + expect(result).toBe(true); 18 + }); 19 + 20 + it("isEncryptedUri returns false for plain URIs", async () => { 21 + const results = await testWeb(async () => { 22 + const mod = await import( 23 + "~/components/transformer/output/refiner/track-uri-passkey/passkey.js" 24 + ); 25 + return [ 26 + mod.isEncryptedUri("https://example.com/track.mp3"), 27 + mod.isEncryptedUri("icecast://radio.example.com/stream"), 28 + mod.isEncryptedUri("local://tid/path.mp3"), 29 + mod.isEncryptedUri(""), 30 + ]; 31 + }); 32 + 33 + expect(results).toEqual([false, false, false, false]); 34 + }); 35 + 36 + it("encryptUri and decryptUri round-trip preserves the original URI", async () => { 37 + const result = await testWeb(async () => { 38 + const mod = await import( 39 + "~/components/transformer/output/refiner/track-uri-passkey/passkey.js" 40 + ); 41 + 42 + const key = crypto.getRandomValues(new Uint8Array(32)); 43 + const original = "https://example.com/my-track.mp3"; 44 + 45 + const encrypted = mod.encryptUri(key, original); 46 + const decrypted = mod.decryptUri(key, encrypted); 47 + 48 + return { encrypted: encrypted.startsWith("encrypted://"), decrypted }; 49 + }); 50 + 51 + expect(result.encrypted).toBe(true); 52 + expect(result.decrypted).toBe("https://example.com/my-track.mp3"); 53 + }); 54 + 55 + it("encryptUri produces different ciphertext each time (managed nonce)", async () => { 56 + const result = await testWeb(async () => { 57 + const mod = await import( 58 + "~/components/transformer/output/refiner/track-uri-passkey/passkey.js" 59 + ); 60 + 61 + const key = crypto.getRandomValues(new Uint8Array(32)); 62 + const uri = "https://example.com/track.mp3"; 63 + 64 + const enc1 = mod.encryptUri(key, uri); 65 + const enc2 = mod.encryptUri(key, uri); 66 + return enc1 !== enc2; 67 + }); 68 + 69 + expect(result).toBe(true); 70 + }); 71 + 72 + it("decryptUri with wrong key throws", async () => { 73 + const result = await testWeb(async () => { 74 + const mod = await import( 75 + "~/components/transformer/output/refiner/track-uri-passkey/passkey.js" 76 + ); 77 + 78 + const key1 = crypto.getRandomValues(new Uint8Array(32)); 79 + const key2 = crypto.getRandomValues(new Uint8Array(32)); 80 + const encrypted = mod.encryptUri(key1, "https://example.com/track.mp3"); 81 + 82 + try { 83 + mod.decryptUri(key2, encrypted); 84 + return false; 85 + } catch { 86 + return true; 87 + } 88 + }); 89 + 90 + expect(result).toBe(true); 91 + }); 92 + 93 + // Element 94 + 95 + it("passkeyActive returns false when no key is loaded", async () => { 96 + const result = await testWeb(async () => { 97 + const mod = await import( 98 + "~/components/transformer/output/refiner/track-uri-passkey/element.js" 99 + ); 100 + const t = new mod.CLASS(); 101 + document.body.append(t); 102 + return t.passkeyActive(); 103 + }); 104 + 105 + expect(result).toBe(false); 106 + }); 107 + 108 + it("lockedTracks returns empty array initially", async () => { 109 + const result = await testWeb(async () => { 110 + const mod = await import( 111 + "~/components/transformer/output/refiner/track-uri-passkey/element.js" 112 + ); 113 + const t = new mod.CLASS(); 114 + document.body.append(t); 115 + return t.lockedTracks(); 116 + }); 117 + 118 + expect(result).toEqual([]); 119 + }); 120 + 121 + it("ready becomes true after connectedCallback resolves the IDB key check", async () => { 122 + const result = await testWeb(async () => { 123 + const mod = await import( 124 + "~/components/transformer/output/refiner/track-uri-passkey/element.js" 125 + ); 126 + const t = new mod.CLASS(); 127 + document.body.append(t); 128 + 129 + // ready() starts false; connectedCallback loads the key from IDB 130 + // (returning undefined = no stored key) and then sets #keyReady = true 131 + await new Promise((r) => setTimeout(r, 50)); 132 + return t.ready(); 133 + }); 134 + 135 + expect(result).toBe(true); 136 + }); 137 + });