BlueSky & more on desktop lazurite.stormlightlabs.org/
tauri rust typescript bluesky appview atproto solid
2
fork

Configure Feed

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

at main 242 lines 8.5 kB view raw
1import { AppTestProviders } from "$/test/providers"; 2import { render, screen } from "@solidjs/testing-library"; 3import type { Component, ParentProps } from "solid-js"; 4import { beforeEach, describe, expect, it, vi } from "vitest"; 5import { buildMessagesRoute } from "./lib/conversations"; 6import { buildProfileRoute } from "./lib/profile"; 7import { AppRouter } from "./router"; 8 9const listenMock = vi.hoisted(() => vi.fn()); 10 11vi.mock("@tauri-apps/api/event", () => ({ listen: listenMock })); 12vi.mock( 13 "$/components/saved/SavedPostsPanel", 14 () => ({ SavedPostsPanel: () => <div data-testid="saved-posts-view">saved</div> }), 15); 16vi.mock( 17 "$/components/search/HashtagPanel", 18 () => ({ HashtagPanel: () => <div data-testid="hashtag-view">hashtag</div> }), 19); 20vi.mock("$/components/search/SearchPanel", () => ({ SearchPanel: () => <div data-testid="search-view">search</div> })); 21vi.mock( 22 "$/components/search/SearchPreflightPanel", 23 () => ({ SearchPreflightPanel: () => <div data-testid="search-preflight-view">preflight</div> }), 24); 25 26const Shell: Component<ParentProps<{ fullWidth?: boolean }>> = (props) => ( 27 <div data-testid="shell" data-full-width={props.fullWidth ? "true" : "false"}>{props.children}</div> 28); 29 30function renderRouter(hash: string, options: { preferences?: Record<string, unknown> } = {}) { 31 globalThis.location.hash = hash; 32 const renderComposer = vi.fn(() => <div data-testid="composer-view">composer</div>); 33 const renderNotifications = vi.fn(() => <div data-testid="notifications-view">notifications</div>); 34 const renderPostEngagement = vi.fn((props: { uri: string | null }) => ( 35 <div data-testid="post-engagement-view">{props.uri ?? "none"}</div> 36 )); 37 const renderPost = vi.fn((props: { uri: string | null }) => <div data-testid="post-view">{props.uri ?? "none"}</div>); 38 const renderProfile = vi.fn((props: { actor: string | null }) => ( 39 <div data-testid="profile-view"> 40 <span>{props.actor ?? "self-profile"}</span> 41 </div> 42 )); 43 const renderTimeline = vi.fn(() => <div data-testid="timeline-view">timeline</div>); 44 45 const renderMessages = vi.fn((props: { memberDid: string | null }) => ( 46 <div data-testid="messages-view">{props.memberDid ?? "messages"}</div> 47 )); 48 49 render(() => ( 50 <AppTestProviders 51 preferences={options.preferences} 52 session={{ 53 activeDid: "did:plc:alice", 54 activeHandle: "alice.test", 55 activeSession: { did: "did:plc:alice", handle: "alice.test" }, 56 }}> 57 <AppRouter 58 renderAuth={() => <div>Auth</div>} 59 renderComposer={renderComposer} 60 renderMessages={renderMessages} 61 renderNotifications={renderNotifications} 62 renderPostEngagement={renderPostEngagement} 63 renderPost={renderPost} 64 renderProfile={renderProfile} 65 renderShell={Shell} 66 renderTimeline={renderTimeline} /> 67 </AppTestProviders> 68 )); 69 70 return { 71 renderComposer, 72 renderMessages, 73 renderNotifications, 74 renderPost, 75 renderPostEngagement, 76 renderProfile, 77 renderTimeline, 78 }; 79} 80 81describe("AppRouter", () => { 82 beforeEach(() => { 83 listenMock.mockReset(); 84 listenMock.mockResolvedValue(() => {}); 85 }); 86 87 it("renders the timeline route", async () => { 88 const { renderTimeline } = renderRouter("#/timeline"); 89 90 await screen.findByTestId("timeline-view"); 91 92 expect(renderTimeline).toHaveBeenCalledOnce(); 93 expect(screen.getByText("timeline")).toBeInTheDocument(); 94 }); 95 96 it("renders the timeline route with query params intact", async () => { 97 const { renderTimeline } = renderRouter( 98 "#/timeline?thread=at%3A%2F%2Fdid%3Aplc%3Aalice%2Fapp.bsky.feed.post%2Fxyz", 99 ); 100 101 await screen.findByTestId("timeline-view"); 102 103 expect(renderTimeline).toHaveBeenCalledOnce(); 104 }); 105 106 it("renders the standalone composer route", async () => { 107 const { renderComposer } = renderRouter("#/composer"); 108 109 await screen.findByTestId("composer-view"); 110 111 expect(renderComposer).toHaveBeenCalledOnce(); 112 expect(screen.getByText("composer")).toBeInTheDocument(); 113 }); 114 115 it("renders the notifications route inside the protected shell", async () => { 116 const { renderNotifications } = renderRouter("#/notifications"); 117 118 await screen.findByTestId("notifications-view"); 119 120 expect(renderNotifications).toHaveBeenCalledOnce(); 121 expect(screen.getByText("notifications")).toBeInTheDocument(); 122 expect(screen.getByTestId("shell")).toHaveAttribute("data-full-width", "false"); 123 }); 124 125 it("renders decoded post routes inside the protected shell", async () => { 126 const uri = "at://did:plc:alice/app.bsky.feed.post/123"; 127 const { renderPost } = renderRouter(`#/post/${encodeURIComponent(uri)}`); 128 129 await screen.findByTestId("post-view"); 130 131 expect(renderPost.mock.lastCall?.[0].uri).toBe(uri); 132 expect(screen.getByTestId("shell")).toHaveAttribute("data-full-width", "false"); 133 }); 134 135 it("renders post engagement routes with the decoded post uri", async () => { 136 const uri = "at://did:plc:alice/app.bsky.feed.post/123"; 137 const { renderPostEngagement } = renderRouter(`#/post/${encodeURIComponent(uri)}/engagement?tab=quotes`); 138 139 await screen.findByTestId("post-engagement-view"); 140 141 expect(renderPostEngagement.mock.lastCall?.[0].uri).toBe(uri); 142 expect(screen.getByTestId("shell")).toHaveAttribute("data-full-width", "false"); 143 }); 144 145 it("renders the saved posts route inside the protected shell", async () => { 146 renderRouter("#/saved"); 147 148 await screen.findByTestId("saved-posts-view"); 149 150 expect(screen.getByText("saved")).toBeInTheDocument(); 151 expect(screen.getByTestId("shell")).toHaveAttribute("data-full-width", "false"); 152 }); 153 154 it("passes the decoded member did on targeted message routes", async () => { 155 const memberDid = "did:plc:bob"; 156 const { renderMessages } = renderRouter(`#${buildMessagesRoute(memberDid)}`); 157 158 await screen.findByTestId("messages-view"); 159 160 expect(renderMessages.mock.lastCall?.[0].memberDid).toBe(memberDid); 161 expect(screen.getByText(memberDid)).toBeInTheDocument(); 162 }); 163 164 it("renders the explorer route inside the full-width shell", async () => { 165 renderRouter("#/explorer"); 166 167 await screen.findByTestId("shell"); 168 169 expect(screen.getByTestId("shell")).toHaveAttribute("data-full-width", "true"); 170 }); 171 172 it("renders the logged-in profile route", async () => { 173 const { renderProfile } = renderRouter("#/profile"); 174 175 await screen.findByTestId("profile-view"); 176 177 expect(renderProfile.mock.lastCall?.[0].actor).toBeNull(); 178 expect(screen.getByText("self-profile")).toBeInTheDocument(); 179 }); 180 181 it("passes the decoded actor on other profile routes", async () => { 182 const actor = "alice.bsky.social"; 183 const { renderProfile } = renderRouter(`#${buildProfileRoute(actor)}`); 184 185 await screen.findByTestId("profile-view"); 186 187 expect(renderProfile.mock.lastCall?.[0].actor).toBe(actor); 188 expect(screen.getByText(actor)).toBeInTheDocument(); 189 }); 190 191 it("renders hashtag routes inside the protected shell", async () => { 192 renderRouter("#/hashtag/solid"); 193 194 await screen.findByTestId("hashtag-view"); 195 196 expect(screen.getByText("hashtag")).toBeInTheDocument(); 197 expect(screen.getByTestId("shell")).toHaveAttribute("data-full-width", "false"); 198 }); 199 200 it("renders encoded hashtag routes", async () => { 201 renderRouter("#/hashtag/%23solid"); 202 203 await screen.findByTestId("hashtag-view"); 204 205 expect(screen.getByText("hashtag")).toBeInTheDocument(); 206 }); 207 208 it("redirects first search visits to the embeddings preflight", async () => { 209 renderRouter("#/search"); 210 211 await screen.findByTestId("search-preflight-view"); 212 213 expect(screen.getByText("preflight")).toBeInTheDocument(); 214 }); 215 216 it("renders the search route once the preflight was already seen", async () => { 217 renderRouter("#/search", { 218 preferences: { 219 embeddingsConfig: { 220 enabled: false, 221 preflightSeen: true, 222 modelName: "nomic-embed-text-v1.5", 223 dimensions: 768, 224 downloaded: false, 225 downloadActive: false, 226 }, 227 }, 228 }); 229 230 await screen.findByTestId("search-view"); 231 232 expect(screen.getByText("search")).toBeInTheDocument(); 233 }); 234 235 it("does not redirect profile tab search visits to the embeddings preflight", async () => { 236 renderRouter("#/search?tab=profiles"); 237 238 await screen.findByTestId("search-view"); 239 240 expect(screen.getByText("search")).toBeInTheDocument(); 241 }); 242});