[READ-ONLY] Mirror of https://github.com/improsocial/impro An extensible Bluesky client for web impro.social
6

Configure Feed

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

Update datalayer specs + some refactors

Grace Kind (Dec 7, 2025, 12:20 PM EST) 488147fb ce86472f

+481 -229
+2 -174
src/js/dataLayer/mutations.js
··· 1 1 import { parseUri, createNotFoundPost } from "/js/dataHelpers.js"; 2 - import { wait, getCurrentTimestamp } from "/js/utils.js"; 3 - 4 - class PostCreator { 5 - constructor(api) { 6 - this.api = api; 7 - } 8 - 9 - async createPost({ 10 - postText, 11 - facets, 12 - external, 13 - replyTo, 14 - replyRoot, 15 - quotedPost, 16 - images, 17 - }) { 18 - const externalEmbed = await this.prepareExternalEmbed(external); 19 - const imagesEmbed = await this.prepareImagesEmbed(images); 20 - let reply = null; 21 - // Add reply reference if provided 22 - if (replyTo) { 23 - if (!replyRoot) { 24 - throw new Error("replyRoot is required when replyTo is provided"); 25 - } 26 - reply = { 27 - root: { 28 - uri: replyRoot.uri, 29 - cid: replyRoot.cid, 30 - $type: "com.atproto.repo.strongRef", 31 - }, 32 - parent: { uri: replyTo.uri, cid: replyTo.cid }, 33 - }; 34 - } 35 - 36 - // Build embed(s) 37 - let embed = null; 38 - 39 - let quotedPostEmbed = null; 40 - if (quotedPost) { 41 - quotedPostEmbed = { 42 - $type: "app.bsky.embed.record", 43 - record: { 44 - uri: quotedPost.uri, 45 - cid: quotedPost.cid, 46 - }, 47 - }; 48 - } 49 - 50 - // Prioritize images over external links (can't have both external and images) 51 - const mediaEmbed = imagesEmbed || externalEmbed; 52 - 53 - if (mediaEmbed && quotedPostEmbed) { 54 - embed = { 55 - $type: "app.bsky.embed.recordWithMedia", 56 - media: mediaEmbed, 57 - record: quotedPostEmbed, 58 - }; 59 - } else if (mediaEmbed) { 60 - embed = mediaEmbed; 61 - } else if (quotedPostEmbed) { 62 - embed = quotedPostEmbed; 63 - } 64 - 65 - const res = await this.api.createPost({ 66 - text: postText, 67 - facets, 68 - embed, 69 - reply, 70 - }); 71 - 72 - // Get full post from the app view 73 - let fullPost = null; 74 - let tries = 0; 75 - do { 76 - try { 77 - fullPost = await this.api.getPost(res.uri); 78 - } catch (e) {} 79 - await wait(200); 80 - } while (!fullPost && tries < 3); 81 - if (!fullPost) { 82 - throw new Error(`Failed to get post: ${res.uri}`); 83 - } 84 - 85 - return fullPost; 86 - } 87 - 88 - async prepareImagesEmbed(images) { 89 - if (!images || images.length === 0) { 90 - return null; 91 - } 92 - 93 - const uploadedImages = []; 94 - for (const img of images) { 95 - const blob = await this.api.uploadBlob(img.file); 96 - 97 - // Get image dimensions 98 - const aspectRatio = await this.getImageAspectRatio(img.dataUrl); 99 - 100 - uploadedImages.push({ 101 - $type: "app.bsky.embed.images#image", 102 - alt: img.alt || "", 103 - image: { 104 - $type: "blob", 105 - ref: { 106 - $link: blob.ref.$link, 107 - }, 108 - mimeType: blob.mimeType, 109 - size: blob.size, 110 - }, 111 - aspectRatio: { 112 - $type: "app.bsky.embed.defs#aspectRatio", 113 - width: aspectRatio.width, 114 - height: aspectRatio.height, 115 - }, 116 - }); 117 - } 118 - 119 - return { 120 - $type: "app.bsky.embed.images", 121 - images: uploadedImages, 122 - }; 123 - } 124 - 125 - async getImageAspectRatio(dataUrl) { 126 - return new Promise((resolve) => { 127 - const img = new Image(); 128 - img.onload = () => { 129 - resolve({ width: img.width, height: img.height }); 130 - }; 131 - img.onerror = () => { 132 - // Default aspect ratio if image can't be loaded 133 - resolve({ width: 1, height: 1 }); 134 - }; 135 - img.src = dataUrl; 136 - }); 137 - } 138 - 139 - async prepareExternalEmbed(external) { 140 - if (!external) { 141 - return null; 142 - } 143 - const externalImage = external.image; 144 - const externalEmbed = { 145 - $type: "app.bsky.embed.external", 146 - external: { 147 - title: external.title, 148 - description: external.description, 149 - uri: external.url, // note - renaming url to uri 150 - }, 151 - }; 152 - // If there's an external link, upload the preview image 153 - if (externalImage) { 154 - try { 155 - const imageRes = await fetch( 156 - "https://cardyb.bsky.app/v1/image?url=http://gracekind.net/img/og-image.png" 157 - ); 158 - const imageBlob = await imageRes.blob(); 159 - const blob = await this.api.uploadBlob(imageBlob); 160 - externalEmbed.external.thumb = { 161 - $type: "blob", 162 - mimeType: blob.mimeType, 163 - ref: { 164 - $link: blob.ref.$link, 165 - }, 166 - size: blob.size, 167 - }; 168 - } catch (error) { 169 - // Don't fail the post creation if the image can't be uploaded 170 - console.error("Error uploading external link image: ", error); 171 - } 172 - } 173 - return externalEmbed; 174 - } 175 - } 2 + import { getCurrentTimestamp } from "/js/utils.js"; 3 + import { PostCreator } from "/js/postCreator.js"; 176 4 177 5 // Handles mutations to the data, making optimistic updates if needed. 178 6 export class Mutations {
+20 -20
src/js/dataLayer/preferencesProvider.js
··· 26 26 return enLocale?.name || ""; 27 27 } 28 28 29 - class Preferences { 29 + export class Preferences { 30 30 constructor(obj, labelerDefs) { 31 31 this.obj = obj; 32 32 this.labelerDefs = labelerDefs; ··· 172 172 .map((labeler) => labeler.did) 173 173 .concat(["did:plc:ar7c4by46qjdydhdevvrndac"]); // default 174 174 } 175 - } 176 175 177 - function createLoggedOutPreferences() { 178 - return new Preferences( 179 - [ 180 - { 181 - $type: "app.bsky.actor.defs#savedFeedsPrefV2", 182 - items: [ 183 - { 184 - id: "3l6ovcmm2vd2j", 185 - type: "feed", 186 - value: DISCOVER_FEED_URI, 187 - pinned: true, 188 - }, 189 - ], 190 - }, 191 - ], 192 - [] 193 - ); 176 + static createLoggedOutPreferences() { 177 + return new Preferences( 178 + [ 179 + { 180 + $type: "app.bsky.actor.defs#savedFeedsPrefV2", 181 + items: [ 182 + { 183 + id: "3l6ovcmm2vd2j", 184 + type: "feed", 185 + value: DISCOVER_FEED_URI, 186 + pinned: true, 187 + }, 188 + ], 189 + }, 190 + ], 191 + [] 192 + ); 193 + } 194 194 } 195 195 196 196 export class PreferencesProvider { ··· 208 208 209 209 async fetchPreferences() { 210 210 if (!this.api.isAuthenticated) { 211 - this._preferences = createLoggedOutPreferences(); 211 + this._preferences = Preferences.createLoggedOutPreferences(); 212 212 return; 213 213 } 214 214 const preferencesObj = await this.api.getPreferences();
+174
src/js/postCreator.js
··· 1 + import { wait } from "/js/utils.js"; 2 + 3 + export class PostCreator { 4 + constructor(api) { 5 + this.api = api; 6 + } 7 + 8 + async createPost({ 9 + postText, 10 + facets, 11 + external, 12 + replyTo, 13 + replyRoot, 14 + quotedPost, 15 + images, 16 + }) { 17 + const externalEmbed = await this.prepareExternalEmbed(external); 18 + const imagesEmbed = await this.prepareImagesEmbed(images); 19 + let reply = null; 20 + // Add reply reference if provided 21 + if (replyTo) { 22 + if (!replyRoot) { 23 + throw new Error("replyRoot is required when replyTo is provided"); 24 + } 25 + reply = { 26 + root: { 27 + uri: replyRoot.uri, 28 + cid: replyRoot.cid, 29 + $type: "com.atproto.repo.strongRef", 30 + }, 31 + parent: { uri: replyTo.uri, cid: replyTo.cid }, 32 + }; 33 + } 34 + 35 + // Build embed(s) 36 + let embed = null; 37 + 38 + let quotedPostEmbed = null; 39 + if (quotedPost) { 40 + quotedPostEmbed = { 41 + $type: "app.bsky.embed.record", 42 + record: { 43 + uri: quotedPost.uri, 44 + cid: quotedPost.cid, 45 + }, 46 + }; 47 + } 48 + 49 + // Prioritize images over external links (can't have both external and images) 50 + const mediaEmbed = imagesEmbed || externalEmbed; 51 + 52 + if (mediaEmbed && quotedPostEmbed) { 53 + embed = { 54 + $type: "app.bsky.embed.recordWithMedia", 55 + media: mediaEmbed, 56 + record: quotedPostEmbed, 57 + }; 58 + } else if (mediaEmbed) { 59 + embed = mediaEmbed; 60 + } else if (quotedPostEmbed) { 61 + embed = quotedPostEmbed; 62 + } 63 + 64 + const res = await this.api.createPost({ 65 + text: postText, 66 + facets, 67 + embed, 68 + reply, 69 + }); 70 + 71 + // Get full post from the app view 72 + let fullPost = null; 73 + let tries = 0; 74 + do { 75 + try { 76 + fullPost = await this.api.getPost(res.uri); 77 + } catch (e) {} 78 + await wait(200); 79 + } while (!fullPost && tries < 3); 80 + if (!fullPost) { 81 + throw new Error(`Failed to get post: ${res.uri}`); 82 + } 83 + 84 + return fullPost; 85 + } 86 + 87 + async prepareImagesEmbed(images) { 88 + if (!images || images.length === 0) { 89 + return null; 90 + } 91 + 92 + const uploadedImages = []; 93 + for (const img of images) { 94 + const blob = await this.api.uploadBlob(img.file); 95 + 96 + // Get image dimensions 97 + const aspectRatio = await this.getImageAspectRatio(img.dataUrl); 98 + 99 + uploadedImages.push({ 100 + $type: "app.bsky.embed.images#image", 101 + alt: img.alt || "", 102 + image: { 103 + $type: "blob", 104 + ref: { 105 + $link: blob.ref.$link, 106 + }, 107 + mimeType: blob.mimeType, 108 + size: blob.size, 109 + }, 110 + aspectRatio: { 111 + $type: "app.bsky.embed.defs#aspectRatio", 112 + width: aspectRatio.width, 113 + height: aspectRatio.height, 114 + }, 115 + }); 116 + } 117 + 118 + return { 119 + $type: "app.bsky.embed.images", 120 + images: uploadedImages, 121 + }; 122 + } 123 + 124 + async getImageAspectRatio(dataUrl) { 125 + return new Promise((resolve) => { 126 + const img = new Image(); 127 + img.onload = () => { 128 + resolve({ width: img.width, height: img.height }); 129 + }; 130 + img.onerror = () => { 131 + // Default aspect ratio if image can't be loaded 132 + resolve({ width: 1, height: 1 }); 133 + }; 134 + img.src = dataUrl; 135 + }); 136 + } 137 + 138 + async prepareExternalEmbed(external) { 139 + if (!external) { 140 + return null; 141 + } 142 + const externalImage = external.image; 143 + const externalEmbed = { 144 + $type: "app.bsky.embed.external", 145 + external: { 146 + title: external.title, 147 + description: external.description, 148 + uri: external.url, // note - renaming url to uri 149 + }, 150 + }; 151 + // If there's an external link, upload the preview image 152 + if (externalImage) { 153 + try { 154 + const imageRes = await fetch( 155 + "https://cardyb.bsky.app/v1/image?url=http://gracekind.net/img/og-image.png" 156 + ); 157 + const imageBlob = await imageRes.blob(); 158 + const blob = await this.api.uploadBlob(imageBlob); 159 + externalEmbed.external.thumb = { 160 + $type: "blob", 161 + mimeType: blob.mimeType, 162 + ref: { 163 + $link: blob.ref.$link, 164 + }, 165 + size: blob.size, 166 + }; 167 + } catch (error) { 168 + // Don't fail the post creation if the image can't be uploaded 169 + console.error("Error uploading external link image: ", error); 170 + } 171 + } 172 + return externalEmbed; 173 + } 174 + }
+106 -11
tests/dataLayer/mutations.test.js
··· 3 3 import { Mutations } from "../../src/js/dataLayer/mutations.js"; 4 4 import { DataStore } from "../../src/js/dataLayer/dataStore.js"; 5 5 import { PatchStore } from "../../src/js/dataLayer/patchStore.js"; 6 + import { Preferences } from "../../src/js/dataLayer/preferencesProvider.js"; 6 7 7 8 const t = new TestSuite("Mutations"); 8 9 ··· 19 20 }; 20 21 const dataStore = new DataStore(); 21 22 const patchStore = new PatchStore(); 22 - const mutations = new Mutations(mockApi, dataStore, patchStore); 23 + const mockPreferencesProvider = { 24 + requirePreferences: () => Preferences.createLoggedOutPreferences(), 25 + }; 26 + const mutations = new Mutations( 27 + mockApi, 28 + dataStore, 29 + patchStore, 30 + mockPreferencesProvider 31 + ); 23 32 24 33 // Start the mutation 25 34 mutations.addLike(testPost); ··· 37 46 }; 38 47 const dataStore = new DataStore(); 39 48 const patchStore = new PatchStore(); 40 - const mutations = new Mutations(mockApi, dataStore, patchStore); 49 + const mockPreferencesProvider = { 50 + requirePreferences: () => Preferences.createLoggedOutPreferences(), 51 + }; 52 + const mutations = new Mutations( 53 + mockApi, 54 + dataStore, 55 + patchStore, 56 + mockPreferencesProvider 57 + ); 41 58 42 59 await mutations.addLike(testPost); 43 60 ··· 60 77 }; 61 78 const dataStore = new DataStore(); 62 79 const patchStore = new PatchStore(); 63 - const mutations = new Mutations(mockApi, dataStore, patchStore); 80 + const mockPreferencesProvider = { 81 + requirePreferences: () => Preferences.createLoggedOutPreferences(), 82 + }; 83 + const mutations = new Mutations( 84 + mockApi, 85 + dataStore, 86 + patchStore, 87 + mockPreferencesProvider 88 + ); 64 89 65 90 // Start two concurrent operations 66 91 const promise1 = mutations.addLike(testPost); ··· 90 115 }; 91 116 const dataStore = new DataStore(); 92 117 const patchStore = new PatchStore(); 93 - const mutations = new Mutations(mockApi, dataStore, patchStore); 118 + const mockPreferencesProvider = { 119 + requirePreferences: () => Preferences.createLoggedOutPreferences(), 120 + }; 121 + const mutations = new Mutations( 122 + mockApi, 123 + dataStore, 124 + patchStore, 125 + mockPreferencesProvider 126 + ); 94 127 95 128 // Start the mutation 96 129 mutations.removeLike(testPost); ··· 107 140 }; 108 141 const dataStore = new DataStore(); 109 142 const patchStore = new PatchStore(); 110 - const mutations = new Mutations(mockApi, dataStore, patchStore); 143 + const mockPreferencesProvider = { 144 + requirePreferences: () => Preferences.createLoggedOutPreferences(), 145 + }; 146 + const mutations = new Mutations( 147 + mockApi, 148 + dataStore, 149 + patchStore, 150 + mockPreferencesProvider 151 + ); 111 152 112 153 await mutations.removeLike(testPost); 113 154 ··· 127 168 uri: "did:test:profile", 128 169 did: "did:test:profile", 129 170 handle: "test.user", 171 + followersCount: 10, 130 172 viewer: { following: null }, 131 173 }; 132 174 ··· 139 181 }; 140 182 const dataStore = new DataStore(); 141 183 const patchStore = new PatchStore(); 142 - const mutations = new Mutations(mockApi, dataStore, patchStore); 184 + const mockPreferencesProvider = { 185 + requirePreferences: () => Preferences.createLoggedOutPreferences(), 186 + }; 187 + const mutations = new Mutations( 188 + mockApi, 189 + dataStore, 190 + patchStore, 191 + mockPreferencesProvider 192 + ); 143 193 144 194 // Start the mutation 145 195 mutations.followProfile(testProfile); ··· 147 197 // Check that patch was applied immediately 148 198 const patchedProfile = patchStore.applyProfilePatches(testProfile); 149 199 assertEquals(patchedProfile.viewer.following, "fake following"); 200 + assertEquals(patchedProfile.followersCount, 11); 150 201 }); 151 202 152 203 it("should update dataStore and remove patch on success", async () => { ··· 156 207 }; 157 208 const dataStore = new DataStore(); 158 209 const patchStore = new PatchStore(); 159 - const mutations = new Mutations(mockApi, dataStore, patchStore); 210 + const mockPreferencesProvider = { 211 + requirePreferences: () => Preferences.createLoggedOutPreferences(), 212 + }; 213 + const mutations = new Mutations( 214 + mockApi, 215 + dataStore, 216 + patchStore, 217 + mockPreferencesProvider 218 + ); 160 219 161 220 await mutations.followProfile(testProfile); 162 221 163 222 // Check that profile was updated in store 164 223 const storedProfile = dataStore.getProfile(testProfile.did); 165 224 assertEquals(storedProfile.viewer.following, "follow-123"); 225 + assertEquals(storedProfile.followersCount, 11); 166 226 167 227 // Check that patch was removed 168 228 const patchedProfile = patchStore.applyProfilePatches(storedProfile); ··· 175 235 uri: "did:test:profile", 176 236 did: "did:test:profile", 177 237 handle: "test.user", 238 + followersCount: 10, 178 239 viewer: { following: "existing-follow-uri" }, 179 240 }; 180 241 ··· 187 248 }; 188 249 const dataStore = new DataStore(); 189 250 const patchStore = new PatchStore(); 190 - const mutations = new Mutations(mockApi, dataStore, patchStore); 251 + const mockPreferencesProvider = { 252 + requirePreferences: () => Preferences.createLoggedOutPreferences(), 253 + }; 254 + const mutations = new Mutations( 255 + mockApi, 256 + dataStore, 257 + patchStore, 258 + mockPreferencesProvider 259 + ); 191 260 192 261 // Start the mutation 193 262 mutations.unfollowProfile(testProfile); ··· 195 264 // Check that patch was applied immediately 196 265 const patchedProfile = patchStore.applyProfilePatches(testProfile); 197 266 assertEquals(patchedProfile.viewer.following, null); 267 + assertEquals(patchedProfile.followersCount, 9); 198 268 }); 199 269 200 270 it("should update dataStore and remove patch on success", async () => { ··· 203 273 }; 204 274 const dataStore = new DataStore(); 205 275 const patchStore = new PatchStore(); 206 - const mutations = new Mutations(mockApi, dataStore, patchStore); 276 + const mockPreferencesProvider = { 277 + requirePreferences: () => Preferences.createLoggedOutPreferences(), 278 + }; 279 + const mutations = new Mutations( 280 + mockApi, 281 + dataStore, 282 + patchStore, 283 + mockPreferencesProvider 284 + ); 207 285 208 286 await mutations.unfollowProfile(testProfile); 209 287 210 288 // Check that profile was updated in store 211 289 const storedProfile = dataStore.getProfile(testProfile.did); 212 290 assertEquals(storedProfile.viewer.following, null); 291 + assertEquals(storedProfile.followersCount, 9); 213 292 214 293 // Check that patch was removed 215 294 const patchedProfile = patchStore.applyProfilePatches(storedProfile); ··· 235 314 }; 236 315 const dataStore = new DataStore(); 237 316 const patchStore = new PatchStore(); 238 - const mutations = new Mutations(mockApi, dataStore, patchStore); 317 + const mockPreferencesProvider = { 318 + requirePreferences: () => Preferences.createLoggedOutPreferences(), 319 + }; 320 + const mutations = new Mutations( 321 + mockApi, 322 + dataStore, 323 + patchStore, 324 + mockPreferencesProvider 325 + ); 239 326 240 327 // Start like, then unlike before like completes 241 328 const likePromise = mutations.addLike(post); ··· 264 351 }; 265 352 const dataStore = new DataStore(); 266 353 const patchStore = new PatchStore(); 267 - const mutations = new Mutations(mockApi, dataStore, patchStore); 354 + const mockPreferencesProvider = { 355 + requirePreferences: () => Preferences.createLoggedOutPreferences(), 356 + }; 357 + const mutations = new Mutations( 358 + mockApi, 359 + dataStore, 360 + patchStore, 361 + mockPreferencesProvider 362 + ); 268 363 269 364 await mutations.removeLike(post); 270 365
+34 -8
tests/dataLayer/requests.test.js
··· 2 2 import { assertEquals } from "../testHelpers.js"; 3 3 import { Requests } from "/js/dataLayer/requests.js"; 4 4 import { DataStore } from "/js/dataLayer/dataStore.js"; 5 + import { Preferences } from "/js/dataLayer/preferencesProvider.js"; 5 6 6 7 const t = new TestSuite("Requests"); 7 8 ··· 29 30 }; 30 31 31 32 const dataStore = new DataStore(); 32 - const requests = new Requests(mockApi, dataStore); 33 + const mockPreferencesProvider = { 34 + requirePreferences: () => Preferences.createLoggedOutPreferences(), 35 + }; 36 + const requests = new Requests(mockApi, dataStore, mockPreferencesProvider); 33 37 34 38 await requests.loadPostThread(postURI); 35 39 ··· 54 58 }; 55 59 56 60 const dataStore = new DataStore(); 57 - const requests = new Requests(mockApi, dataStore); 61 + const mockPreferencesProvider = { 62 + requirePreferences: () => Preferences.createLoggedOutPreferences(), 63 + }; 64 + const requests = new Requests(mockApi, dataStore, mockPreferencesProvider); 58 65 59 66 await requests.loadPostThread(postURI); 60 67 ··· 82 89 }; 83 90 84 91 const dataStore = new DataStore(); 85 - const requests = new Requests(mockApi, dataStore); 92 + const mockPreferencesProvider = { 93 + requirePreferences: () => Preferences.createLoggedOutPreferences(), 94 + }; 95 + const requests = new Requests(mockApi, dataStore, mockPreferencesProvider); 86 96 87 97 await requests.loadNextFeedPage(feedURI); 88 98 ··· 119 129 getFeed: async () => newPage, 120 130 }; 121 131 122 - const requests = new Requests(mockApi, dataStore); 132 + const mockPreferencesProvider = { 133 + requirePreferences: () => Preferences.createLoggedOutPreferences(), 134 + }; 135 + const requests = new Requests(mockApi, dataStore, mockPreferencesProvider); 123 136 124 137 await requests.loadNextFeedPage(feedURI); 125 138 ··· 147 160 }; 148 161 149 162 const dataStore = new DataStore(); 150 - const requests = new Requests(mockApi, dataStore); 163 + const mockPreferencesProvider = { 164 + requirePreferences: () => Preferences.createLoggedOutPreferences(), 165 + }; 166 + const requests = new Requests(mockApi, dataStore, mockPreferencesProvider); 151 167 152 168 await requests.loadNextFeedPage(feedURI); 153 169 ··· 179 195 }; 180 196 181 197 const dataStore = new DataStore(); 182 - const requests = new Requests(mockApi, dataStore); 198 + const mockPreferencesProvider = { 199 + requirePreferences: () => Preferences.createLoggedOutPreferences(), 200 + }; 201 + const requests = new Requests(mockApi, dataStore, mockPreferencesProvider); 183 202 184 203 await requests.loadNextFeedPage(feedURI); 185 204 ··· 206 225 }; 207 226 208 227 const dataStore = new DataStore(); 209 - const requests = new Requests(mockApi, dataStore); 228 + 229 + const mockPreferencesProvider = { 230 + requirePreferences: () => Preferences.createLoggedOutPreferences(), 231 + }; 232 + const requests = new Requests(mockApi, dataStore, mockPreferencesProvider); 210 233 211 234 await requests.loadProfile(profileDID); 212 235 ··· 228 251 getProfile: async () => initialProfile, 229 252 }; 230 253 231 - const requests = new Requests(mockApi, dataStore); 254 + const mockPreferencesProvider = { 255 + requirePreferences: () => Preferences.createLoggedOutPreferences(), 256 + }; 257 + const requests = new Requests(mockApi, dataStore, mockPreferencesProvider); 232 258 233 259 await requests.loadProfile(profileDID); 234 260
+145 -16
tests/dataLayer/selectors.test.js
··· 3 3 import { Selectors } from "../../src/js/dataLayer/selectors.js"; 4 4 import { DataStore } from "../../src/js/dataLayer/dataStore.js"; 5 5 import { PatchStore } from "../../src/js/dataLayer/patchStore.js"; 6 + import { Preferences } from "../../src/js/dataLayer/preferencesProvider.js"; 6 7 7 8 const t = new TestSuite("Selectors"); 8 9 ··· 12 13 it("should return null when feed does not exist", () => { 13 14 const dataStore = new DataStore(); 14 15 const patchStore = new PatchStore(); 15 - const selectors = new Selectors(dataStore, patchStore); 16 + const mockPreferencesProvider = { 17 + requirePreferences: () => Preferences.createLoggedOutPreferences(), 18 + }; 19 + const selectors = new Selectors( 20 + dataStore, 21 + patchStore, 22 + mockPreferencesProvider, 23 + false 24 + ); 16 25 17 26 const result = selectors.getFeed(feedURI); 18 27 assertEquals(result, null); ··· 21 30 it("should hydrate and return a feed with posts", () => { 22 31 const dataStore = new DataStore(); 23 32 const patchStore = new PatchStore(); 24 - const selectors = new Selectors(dataStore, patchStore); 33 + const mockPreferencesProvider = { 34 + requirePreferences: () => Preferences.createLoggedOutPreferences(), 35 + }; 36 + const selectors = new Selectors( 37 + dataStore, 38 + patchStore, 39 + mockPreferencesProvider, 40 + false 41 + ); 25 42 26 43 // Set up test data 27 44 const rawFeed = { ··· 47 64 it("should apply patches to posts in feed", () => { 48 65 const dataStore = new DataStore(); 49 66 const patchStore = new PatchStore(); 50 - const selectors = new Selectors(dataStore, patchStore); 67 + const mockPreferencesProvider = { 68 + requirePreferences: () => Preferences.createLoggedOutPreferences(), 69 + }; 70 + const selectors = new Selectors( 71 + dataStore, 72 + patchStore, 73 + mockPreferencesProvider, 74 + false 75 + ); 51 76 52 77 const rawFeed = { 53 78 feed: [{ post: { uri: "post1" } }], ··· 78 103 it("should return null when post thread does not exist", () => { 79 104 const dataStore = new DataStore(); 80 105 const patchStore = new PatchStore(); 81 - const selectors = new Selectors(dataStore, patchStore); 106 + const mockPreferencesProvider = { 107 + requirePreferences: () => Preferences.createLoggedOutPreferences(), 108 + }; 109 + const selectors = new Selectors( 110 + dataStore, 111 + patchStore, 112 + mockPreferencesProvider, 113 + false 114 + ); 82 115 83 116 const result = selectors.getPostThread(postURI); 84 117 assertEquals(result, null); ··· 87 120 it("should hydrate and return a simple post thread", () => { 88 121 const dataStore = new DataStore(); 89 122 const patchStore = new PatchStore(); 90 - const selectors = new Selectors(dataStore, patchStore); 123 + const mockPreferencesProvider = { 124 + requirePreferences: () => Preferences.createLoggedOutPreferences(), 125 + }; 126 + const selectors = new Selectors( 127 + dataStore, 128 + patchStore, 129 + mockPreferencesProvider, 130 + false 131 + ); 91 132 92 133 const rawThread = { 93 134 post: { uri: postURI }, ··· 110 151 it("should hydrate post thread with replies", () => { 111 152 const dataStore = new DataStore(); 112 153 const patchStore = new PatchStore(); 113 - const selectors = new Selectors(dataStore, patchStore); 154 + const mockPreferencesProvider = { 155 + requirePreferences: () => Preferences.createLoggedOutPreferences(), 156 + }; 157 + const selectors = new Selectors( 158 + dataStore, 159 + patchStore, 160 + mockPreferencesProvider, 161 + false 162 + ); 114 163 115 164 const rawThread = { 116 165 post: { uri: postURI }, ··· 143 192 it("should hydrate post thread with parent", () => { 144 193 const dataStore = new DataStore(); 145 194 const patchStore = new PatchStore(); 146 - const selectors = new Selectors(dataStore, patchStore); 195 + const mockPreferencesProvider = { 196 + requirePreferences: () => Preferences.createLoggedOutPreferences(), 197 + }; 198 + const selectors = new Selectors( 199 + dataStore, 200 + patchStore, 201 + mockPreferencesProvider, 202 + false 203 + ); 147 204 148 205 const rawThread = { 149 206 post: { uri: postURI }, ··· 173 230 it("should apply patches to thread posts", () => { 174 231 const dataStore = new DataStore(); 175 232 const patchStore = new PatchStore(); 176 - const selectors = new Selectors(dataStore, patchStore); 233 + const mockPreferencesProvider = { 234 + requirePreferences: () => Preferences.createLoggedOutPreferences(), 235 + }; 236 + const selectors = new Selectors( 237 + dataStore, 238 + patchStore, 239 + mockPreferencesProvider, 240 + false 241 + ); 177 242 178 243 const rawThread = { 179 244 post: { uri: postURI }, ··· 200 265 it("should preserve non-threadViewPost replies unchanged", () => { 201 266 const dataStore = new DataStore(); 202 267 const patchStore = new PatchStore(); 203 - const selectors = new Selectors(dataStore, patchStore); 268 + const mockPreferencesProvider = { 269 + requirePreferences: () => Preferences.createLoggedOutPreferences(), 270 + }; 271 + const selectors = new Selectors( 272 + dataStore, 273 + patchStore, 274 + mockPreferencesProvider, 275 + false 276 + ); 204 277 205 278 const rawThread = { 206 279 post: { uri: postURI }, ··· 247 320 it("should return post with patches applied", () => { 248 321 const dataStore = new DataStore(); 249 322 const patchStore = new PatchStore(); 250 - const selectors = new Selectors(dataStore, patchStore); 323 + const mockPreferencesProvider = { 324 + requirePreferences: () => Preferences.createLoggedOutPreferences(), 325 + }; 326 + const selectors = new Selectors( 327 + dataStore, 328 + patchStore, 329 + mockPreferencesProvider, 330 + false 331 + ); 251 332 252 333 dataStore.setPost(postURI, testPost); 253 334 patchStore.addPostPatch(postURI, { type: "addLike" }); ··· 262 343 it("should return post without patches when no patches exist", () => { 263 344 const dataStore = new DataStore(); 264 345 const patchStore = new PatchStore(); 265 - const selectors = new Selectors(dataStore, patchStore); 346 + const mockPreferencesProvider = { 347 + requirePreferences: () => Preferences.createLoggedOutPreferences(), 348 + }; 349 + const selectors = new Selectors( 350 + dataStore, 351 + patchStore, 352 + mockPreferencesProvider, 353 + false 354 + ); 266 355 267 356 dataStore.setPost(postURI, testPost); 268 357 ··· 275 364 it("should return null when optional post missing", () => { 276 365 const dataStore = new DataStore(); 277 366 const patchStore = new PatchStore(); 278 - const selectors = new Selectors(dataStore, patchStore); 367 + const mockPreferencesProvider = { 368 + requirePreferences: () => Preferences.createLoggedOutPreferences(), 369 + }; 370 + const selectors = new Selectors( 371 + dataStore, 372 + patchStore, 373 + mockPreferencesProvider, 374 + false 375 + ); 279 376 280 377 const result = selectors.getPost("nonExistentPost", { require: false }); 281 378 assertEquals(result, null); ··· 284 381 it("should return null when post missing and require not specified", () => { 285 382 const dataStore = new DataStore(); 286 383 const patchStore = new PatchStore(); 287 - const selectors = new Selectors(dataStore, patchStore); 384 + const mockPreferencesProvider = { 385 + requirePreferences: () => Preferences.createLoggedOutPreferences(), 386 + }; 387 + const selectors = new Selectors( 388 + dataStore, 389 + patchStore, 390 + mockPreferencesProvider, 391 + false 392 + ); 288 393 289 394 const result = selectors.getPost("nonExistentPost"); 290 395 assertEquals(result, null); ··· 303 408 it("should return null when profile does not exist", () => { 304 409 const dataStore = new DataStore(); 305 410 const patchStore = new PatchStore(); 306 - const selectors = new Selectors(dataStore, patchStore); 411 + const mockPreferencesProvider = { 412 + requirePreferences: () => Preferences.createLoggedOutPreferences(), 413 + }; 414 + const selectors = new Selectors( 415 + dataStore, 416 + patchStore, 417 + mockPreferencesProvider, 418 + false 419 + ); 307 420 308 421 const result = selectors.getProfile(profileDID); 309 422 assertEquals(result, null); ··· 326 439 it("should return profile without patches when no patches exist", () => { 327 440 const dataStore = new DataStore(); 328 441 const patchStore = new PatchStore(); 329 - const selectors = new Selectors(dataStore, patchStore); 442 + const mockPreferencesProvider = { 443 + requirePreferences: () => Preferences.createLoggedOutPreferences(), 444 + }; 445 + const selectors = new Selectors( 446 + dataStore, 447 + patchStore, 448 + mockPreferencesProvider, 449 + false 450 + ); 330 451 331 452 dataStore.setProfile(profileDID, testProfile); 332 453 ··· 341 462 it("should work with multiple data types and patches", () => { 342 463 const dataStore = new DataStore(); 343 464 const patchStore = new PatchStore(); 344 - const selectors = new Selectors(dataStore, patchStore); 465 + const mockPreferencesProvider = { 466 + requirePreferences: () => Preferences.createLoggedOutPreferences(), 467 + }; 468 + const selectors = new Selectors( 469 + dataStore, 470 + patchStore, 471 + mockPreferencesProvider, 472 + false 473 + ); 345 474 346 475 // Set up data 347 476 const feedURI = "feed1";