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
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294import { describe, it, beforeEach, afterEach } from "node:test";import assert from "node:assert/strict";import { DataLayer } from "/js/dataLayer/dataLayer.js";import { DraftMediaStore } from "/js/drafts.js";import { PreferencesProvider } from "/js/dataLayer/preferencesProvider.js";import { HiddenFeedItemsStore } from "/js/dataLayer/hiddenFeedItemsStore.js";import { Constellation } from "/js/constellation.js";import { trackDisposable } from "../../testHelpers.js";
function createMockApi(options = {}) { const isAuthenticated = options.isAuthenticated ?? false; return { getProfile: async (did) => options.profiles?.[did] ?? null, isAuthenticated, session: isAuthenticated ? { did: "did:plc:testuser" } : null, getPreferences: async () => options.preferences ?? [], getLabelers: async () => options.labelers ?? [], updatePreferences: async () => {}, };}
function createDataLayer(api) { const dataLayer = new DataLayer( api, new PreferencesProvider(api), { resolveHandle: async () => null }, new DraftMediaStore("test-media"), new HiddenFeedItemsStore(), new Constellation(), ); trackDisposable(dataLayer.derived.liveStatusScheduler); return dataLayer;}
describe("constructor", () => { it("should initialize all components", () => { const mockApi = createMockApi(); const dataLayer = createDataLayer(mockApi);
assert(dataLayer.api !== undefined); assert(dataLayer.dataStore !== undefined); assert(dataLayer.patchStore !== undefined); assert(dataLayer.preferencesProvider !== undefined); assert(dataLayer.requests !== undefined); assert(dataLayer.mutations !== undefined); assert(dataLayer.derived !== undefined); assert(dataLayer.declarative !== undefined); });
it("should set isAuthenticated from api", () => { const mockApi = createMockApi({ isAuthenticated: true }); const dataLayer = createDataLayer(mockApi);
assert.deepEqual(dataLayer.isAuthenticated, true); });
it("should initialize empty subscribers array", () => { const mockApi = createMockApi(); const dataLayer = createDataLayer(mockApi);
assert.deepEqual(dataLayer.subscribers, []); });});
describe("preferences", () => { it("should load logged out preferences on demand", async () => { const mockApi = createMockApi({ isAuthenticated: false }); const dataLayer = createDataLayer(mockApi);
const preferences = await dataLayer.preferencesProvider.requirePreferences();
assert(preferences !== null); });
it("should fetch preferences from API when authenticated", async () => { const mockPreferences = [ { $type: "app.bsky.actor.defs#savedFeedsPrefV2", items: [] }, ]; const mockApi = createMockApi({ isAuthenticated: true, preferences: mockPreferences, }); const dataLayer = createDataLayer(mockApi);
const preferences = await dataLayer.preferencesProvider.requirePreferences();
assert.deepEqual(preferences.obj, mockPreferences); });});
describe("hasCachedFeed", () => { it("should return false when feed not cached", () => { const mockApi = createMockApi(); const dataLayer = createDataLayer(mockApi);
const result = dataLayer.hasCachedFeed("at://feed/uri");
assert.deepEqual(result, false); });
it("should return true when feed is cached", () => { const mockApi = createMockApi(); const dataLayer = createDataLayer(mockApi); const feedURI = "at://feed/uri";
dataLayer.dataStore.$feeds.set(feedURI, { feed: [], cursor: null });
const result = dataLayer.hasCachedFeed(feedURI);
assert.deepEqual(result, true); });});
describe("hasCachedAuthorFeed", () => { it("should return false when author feed not cached", () => { const mockApi = createMockApi(); const dataLayer = createDataLayer(mockApi);
const result = dataLayer.hasCachedAuthorFeed("did:test:user", "posts");
assert.deepEqual(result, false); });
it("should return true when author feed is cached", () => { const mockApi = createMockApi(); const dataLayer = createDataLayer(mockApi); const profileDid = "did:test:user"; const feedType = "posts";
dataLayer.dataStore.$authorFeeds.set(`${profileDid}-${feedType}`, { feed: [], cursor: null, });
const result = dataLayer.hasCachedAuthorFeed(profileDid, feedType);
assert.deepEqual(result, true); });
it("should construct correct feed URI from profileDid and feedType", () => { const mockApi = createMockApi(); const dataLayer = createDataLayer(mockApi); const profileDid = "did:test:user"; const feedType = "replies";
// Cache with the expected URI format dataLayer.dataStore.$authorFeeds.set("did:test:user-replies", { feed: [], cursor: null, });
const result = dataLayer.hasCachedAuthorFeed(profileDid, feedType);
assert.deepEqual(result, true); });});
describe("component integration", () => { it("should pass dataStore to derived", async () => { const mockApi = createMockApi({ isAuthenticated: false }); const dataLayer = createDataLayer(mockApi); const postURI = "at://post/uri"; const post = { uri: postURI, text: "test", likeCount: 5 };
await dataLayer.preferencesProvider.requirePreferences();
// Set data through dataStore dataLayer.dataStore.$posts.set(postURI, post);
// Verify derived can access it const result = dataLayer.derived.$hydratedPosts.get(postURI); assert.deepEqual(result.uri, postURI); });
it("should pass patchStore to derived", async () => { const mockApi = createMockApi({ isAuthenticated: false }); const dataLayer = createDataLayer(mockApi); const postURI = "at://post/uri"; const post = { uri: postURI, likeCount: 5, viewer: { like: null } };
await dataLayer.preferencesProvider.requirePreferences();
dataLayer.dataStore.$posts.set(postURI, post); dataLayer.patchStore.addPostPatch(postURI, { type: "addLike" });
// Verify derived apply patches const result = dataLayer.derived.$hydratedPosts.get(postURI); assert.deepEqual(result.likeCount, 6); });
it("should pass derived and requests to declarative", async () => { const mockApi = createMockApi({ isAuthenticated: false, profiles: { "did:test:user": { did: "did:test:user", handle: "test.user" }, }, }); const dataLayer = createDataLayer(mockApi);
await dataLayer.preferencesProvider.requirePreferences();
// Verify declarative can access derived const profile = await dataLayer.declarative.ensureDetailedProfile("did:test:user"); assert(profile !== null); });});
describe("selected feed persistence", () => { // createMockApi's authenticated session did const sessionStateKey = "session-state:did:plc:testuser"; const legacyKey = "home-view-currentFeedUri";
// PersistedReactiveStore saves via an effect, which flushes on a double // requestAnimationFrame function flushEffects() { return new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)), ); }
function cleanup() { localStorage.removeItem(sessionStateKey); localStorage.removeItem(legacyKey); }
function createAuthedDataLayer() { return createDataLayer(createMockApi({ isAuthenticated: true })); }
beforeEach(cleanup); afterEach(cleanup);
it("restores the stored selection for the account", () => { localStorage.setItem( sessionStateKey, JSON.stringify({ selectedFeedUri: "following" }), ); const dataLayer = createAuthedDataLayer(); assert.deepEqual(dataLayer.dataStore.$selectedFeedUri.get(), "following"); });
it("migrates a selection stored under the legacy key", () => { localStorage.setItem(legacyKey, JSON.stringify("following")); const dataLayer = createAuthedDataLayer(); assert.deepEqual(dataLayer.dataStore.$selectedFeedUri.get(), "following"); assert.deepEqual(localStorage.getItem(legacyKey), null); });
it("prefers the session-state key over the legacy key", () => { localStorage.setItem( sessionStateKey, JSON.stringify({ selectedFeedUri: "following" }), ); localStorage.setItem(legacyKey, JSON.stringify("stale")); const dataLayer = createAuthedDataLayer(); assert.deepEqual(dataLayer.dataStore.$selectedFeedUri.get(), "following"); });
it("persists selection changes", async () => { const dataLayer = createAuthedDataLayer(); dataLayer.mutations.setSelectedFeedUri("following"); await flushEffects(); assert.deepEqual(JSON.parse(localStorage.getItem(sessionStateKey)), { selectedFeedUri: "following", }); });
it("persists a fallback applied by setPinnedItems", async () => { const dataLayer = createAuthedDataLayer(); dataLayer.mutations.setSelectedFeedUri( "at://did:test/app.bsky.feed.generator/gone", ); dataLayer.dataStore.setPinnedItems([ { type: "timeline", data: { uri: "following" } }, ]); await flushEffects(); assert.deepEqual(JSON.parse(localStorage.getItem(sessionStateKey)), { selectedFeedUri: "following", }); });
it("neither restores nor persists without a session", async () => { localStorage.setItem(legacyKey, JSON.stringify("following")); const dataLayer = createDataLayer(createMockApi()); assert.deepEqual(dataLayer.dataStore.$selectedFeedUri.get(), null); dataLayer.mutations.setSelectedFeedUri("following"); await flushEffects(); assert.deepEqual(localStorage.getItem(sessionStateKey), null); });});