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 319 lines 12 kB view raw
1import { fireEvent, render, screen, waitFor } from "@solidjs/testing-library"; 2import { beforeEach, describe, expect, it, vi } from "vitest"; 3import { ExplorerPanel } from "../ExplorerPanel"; 4 5const describeRepoMock = vi.hoisted(() => vi.fn()); 6const describeServerMock = vi.hoisted(() => vi.fn()); 7const exportRepoCarMock = vi.hoisted(() => vi.fn()); 8const clearLexiconFaviconCacheMock = vi.hoisted(() => vi.fn()); 9const getLexiconFaviconsMock = vi.hoisted(() => vi.fn()); 10const getRecordMock = vi.hoisted(() => vi.fn()); 11const getRecordBacklinksMock = vi.hoisted(() => vi.fn()); 12const getProfileMock = vi.hoisted(() => vi.fn()); 13const listRecordsMock = vi.hoisted(() => vi.fn()); 14const queryLabelsMock = vi.hoisted(() => vi.fn()); 15const resolveInputMock = vi.hoisted(() => vi.fn()); 16const listenMock = vi.hoisted(() => vi.fn()); 17 18vi.mock( 19 "$/lib/api/explorer", 20 () => ({ 21 ExplorerController: { 22 describeRepo: describeRepoMock, 23 describeServer: describeServerMock, 24 exportRepoCar: exportRepoCarMock, 25 clearLexiconFaviconCache: clearLexiconFaviconCacheMock, 26 getLexiconFavicons: getLexiconFaviconsMock, 27 getRecord: getRecordMock, 28 listRecords: listRecordsMock, 29 queryLabels: queryLabelsMock, 30 resolveInput: resolveInputMock, 31 }, 32 }), 33); 34 35vi.mock("$/lib/api/profile", () => ({ ProfileController: { getProfile: getProfileMock } })); 36vi.mock("$/lib/api/diagnostics", () => ({ DiagnosticsController: { getRecordBacklinks: getRecordBacklinksMock } })); 37vi.mock("@tauri-apps/api/event", () => ({ listen: listenMock })); 38 39function renderPanel() { 40 return render(() => <ExplorerPanel />); 41} 42 43describe("ExplorerPanel", () => { 44 beforeEach(() => { 45 describeRepoMock.mockReset(); 46 describeServerMock.mockReset(); 47 exportRepoCarMock.mockReset(); 48 clearLexiconFaviconCacheMock.mockReset(); 49 getLexiconFaviconsMock.mockReset(); 50 getRecordMock.mockReset(); 51 getRecordBacklinksMock.mockReset(); 52 getProfileMock.mockReset(); 53 listRecordsMock.mockReset(); 54 queryLabelsMock.mockReset(); 55 resolveInputMock.mockReset(); 56 listenMock.mockReset(); 57 58 exportRepoCarMock.mockResolvedValue({ did: "did:plc:alice", path: "/tmp/alice.car", bytesWritten: 64 }); 59 clearLexiconFaviconCacheMock.mockResolvedValue(void 0); 60 getLexiconFaviconsMock.mockResolvedValue({}); 61 getProfileMock.mockResolvedValue({ 62 status: "available", 63 profile: { did: "did:plc:alice", handle: "alice.test", followersCount: 28, followsCount: 14 }, 64 }); 65 getRecordBacklinksMock.mockResolvedValue({ 66 likes: { cursor: null, records: [], total: 3 }, 67 quotes: { cursor: null, records: [], total: 1 }, 68 replies: { cursor: null, records: [], total: 2 }, 69 reposts: { cursor: null, records: [], total: 4 }, 70 }); 71 listenMock.mockResolvedValue(() => {}); 72 queryLabelsMock.mockResolvedValue({ labels: [] }); 73 }); 74 75 it("accepts raw handle input and renders repo collections from describeRepo", async () => { 76 resolveInputMock.mockResolvedValue({ 77 input: "@alice.test", 78 inputKind: "handle", 79 targetKind: "repo", 80 normalizedInput: "did:plc:alice", 81 uri: "at://did:plc:alice", 82 did: "did:plc:alice", 83 handle: "alice.test", 84 pdsUrl: "https://pds.example.com", 85 collection: null, 86 rkey: null, 87 }); 88 describeRepoMock.mockResolvedValue({ collections: ["app.bsky.feed.like", "app.bsky.feed.post"] }); 89 90 renderPanel(); 91 92 const input = screen.getByPlaceholderText(/at:\/\/did:\.\.\. or @handle or https:\/\/pds/u); 93 fireEvent.input(input, { target: { value: "@alice.test" } }); 94 fireEvent.submit(input.closest("form")!); 95 96 expect(resolveInputMock).toHaveBeenCalledWith("@alice.test"); 97 expect(await screen.findByRole("button", { name: /app\.bsky\.feed\.like/u })).toBeInTheDocument(); 98 expect(screen.getByRole("button", { name: /app\.bsky\.feed\.post/u })).toBeInTheDocument(); 99 expect(await screen.findByText("Followers")).toBeInTheDocument(); 100 expect(screen.getByText("28")).toBeInTheDocument(); 101 expect(screen.getByText("14")).toBeInTheDocument(); 102 expect(screen.queryByText("0 records")).not.toBeInTheDocument(); 103 expect(screen.queryByText("Count unavailable")).not.toBeInTheDocument(); 104 }); 105 106 it("renders the initial empty state and submits example chips", async () => { 107 resolveInputMock.mockRejectedValueOnce(new Error("network unavailable")); 108 109 renderPanel(); 110 111 expect(screen.getByText("Start from a handle, DID, URI, or PDS.")).toBeInTheDocument(); 112 fireEvent.click(screen.getByRole("button", { name: /@alice\.bsky\.social/u })); 113 114 await waitFor(() => expect(resolveInputMock).toHaveBeenCalledWith("@alice.bsky.social")); 115 }); 116 117 it("loads additional collection pages", async () => { 118 resolveInputMock.mockResolvedValue({ 119 input: "at://did:plc:alice/app.bsky.feed.post", 120 inputKind: "atUri", 121 targetKind: "collection", 122 normalizedInput: "at://did:plc:alice/app.bsky.feed.post", 123 uri: "at://did:plc:alice/app.bsky.feed.post", 124 did: "did:plc:alice", 125 handle: "alice.test", 126 pdsUrl: "https://pds.example.com", 127 collection: "app.bsky.feed.post", 128 rkey: null, 129 }); 130 listRecordsMock.mockResolvedValueOnce({ 131 cursor: "cursor-2", 132 records: [{ 133 uri: "at://did:plc:alice/app.bsky.feed.post/first", 134 cid: "cid-first", 135 value: { text: "First page" }, 136 }], 137 }).mockResolvedValueOnce({ 138 cursor: null, 139 records: [{ 140 uri: "at://did:plc:alice/app.bsky.feed.post/second", 141 cid: "cid-second", 142 value: { text: "Second page" }, 143 }], 144 }); 145 146 renderPanel(); 147 148 const input = screen.getByPlaceholderText(/at:\/\/did:\.\.\. or @handle or https:\/\/pds/u); 149 fireEvent.input(input, { target: { value: "at://did:plc:alice/app.bsky.feed.post" } }); 150 fireEvent.submit(input.closest("form")!); 151 152 expect(await screen.findByRole("button", { name: /first/u })).toBeInTheDocument(); 153 154 fireEvent.click(screen.getByRole("button", { name: /load more\.\.\./iu })); 155 156 await screen.findByRole("button", { name: /second/u }); 157 expect(listRecordsMock).toHaveBeenNthCalledWith(2, "did:plc:alice", "app.bsky.feed.post", "cursor-2"); 158 }); 159 160 it("handles deep-link navigation events for PDS targets", async () => { 161 let navigationHandler: ((event: { payload: { target: Record<string, unknown> } }) => void) | undefined; 162 163 listenMock.mockImplementation((_event: string, callback: typeof navigationHandler) => { 164 navigationHandler = callback; 165 return Promise.resolve(() => {}); 166 }); 167 resolveInputMock.mockResolvedValue({ 168 input: "https://pds.example.com", 169 inputKind: "pdsUrl", 170 targetKind: "pds", 171 normalizedInput: "https://pds.example.com", 172 uri: null, 173 did: null, 174 handle: null, 175 pdsUrl: "https://pds.example.com", 176 collection: null, 177 rkey: null, 178 }); 179 describeServerMock.mockResolvedValue({ 180 pdsUrl: "https://pds.example.com", 181 server: { inviteCodeRequired: true, version: "0.4.0" }, 182 repos: [{ did: "did:plc:hosted", head: "head", rev: "rev-1", active: true, status: null }], 183 cursor: null, 184 }); 185 186 renderPanel(); 187 188 await waitFor(() => expect(listenMock).toHaveBeenCalledOnce()); 189 190 navigationHandler?.({ 191 payload: { 192 target: { 193 input: "https://pds.example.com", 194 inputKind: "pdsUrl", 195 targetKind: "pds", 196 normalizedInput: "https://pds.example.com", 197 uri: null, 198 did: null, 199 handle: null, 200 pdsUrl: "https://pds.example.com", 201 collection: null, 202 rkey: null, 203 }, 204 }, 205 }); 206 207 expect(resolveInputMock).toHaveBeenCalledWith("https://pds.example.com"); 208 expect(await screen.findByText("Hosted Repositories")).toBeInTheDocument(); 209 expect(screen.getByRole("button", { name: /did:plc:hosted/u })).toBeInTheDocument(); 210 }); 211 212 it("shows record backlinks as a supplementary explorer panel", async () => { 213 resolveInputMock.mockResolvedValue({ 214 input: "at://did:plc:alice/app.bsky.feed.post/123", 215 inputKind: "atUri", 216 targetKind: "record", 217 normalizedInput: "at://did:plc:alice/app.bsky.feed.post/123", 218 uri: "at://did:plc:alice/app.bsky.feed.post/123", 219 did: "did:plc:alice", 220 handle: "alice.test", 221 pdsUrl: "https://pds.example.com", 222 collection: "app.bsky.feed.post", 223 rkey: "123", 224 }); 225 getRecordMock.mockResolvedValue({ 226 cid: "cid-123", 227 value: { $type: "app.bsky.feed.post", text: "Explorer record" }, 228 }); 229 getRecordBacklinksMock.mockResolvedValue({ 230 likes: { cursor: null, records: [], total: 3 }, 231 quotes: { cursor: null, records: [], total: 1 }, 232 replies: { cursor: null, records: [], total: 2 }, 233 reposts: { cursor: null, records: [], total: 4 }, 234 }); 235 236 renderPanel(); 237 238 const input = screen.getByPlaceholderText(/at:\/\/did:\.\.\. or @handle or https:\/\/pds/u); 239 fireEvent.input(input, { target: { value: "at://did:plc:alice/app.bsky.feed.post/123" } }); 240 fireEvent.submit(input.closest("form")!); 241 242 expect(await screen.findByText("Backlinks")).toBeInTheDocument(); 243 expect(await screen.findByText("3 records")).toBeInTheDocument(); 244 expect(screen.getByText("4 records")).toBeInTheDocument(); 245 }); 246 247 it("renders lexicon favicons in repo and collection views when available", async () => { 248 resolveInputMock.mockResolvedValueOnce({ 249 input: "@alice.test", 250 inputKind: "handle", 251 targetKind: "repo", 252 normalizedInput: "did:plc:alice", 253 uri: "at://did:plc:alice", 254 did: "did:plc:alice", 255 handle: "alice.test", 256 pdsUrl: "https://pds.example.com", 257 collection: null, 258 rkey: null, 259 }).mockResolvedValueOnce({ 260 input: "at://did:plc:alice/app.bsky.feed.post", 261 inputKind: "atUri", 262 targetKind: "collection", 263 normalizedInput: "at://did:plc:alice/app.bsky.feed.post", 264 uri: "at://did:plc:alice/app.bsky.feed.post", 265 did: "did:plc:alice", 266 handle: "alice.test", 267 pdsUrl: "https://pds.example.com", 268 collection: "app.bsky.feed.post", 269 rkey: null, 270 }); 271 describeRepoMock.mockResolvedValue({ collections: ["app.bsky.feed.post"] }); 272 listRecordsMock.mockResolvedValue({ cursor: null, records: [] }); 273 getLexiconFaviconsMock.mockResolvedValue({ "app.bsky.feed.post": "data:image/png;base64,Zm9v" }); 274 275 renderPanel(); 276 277 const input = screen.getByPlaceholderText(/at:\/\/did:\.\.\. or @handle or https:\/\/pds/u); 278 fireEvent.input(input, { target: { value: "@alice.test" } }); 279 fireEvent.submit(input.closest("form")!); 280 281 expect(await screen.findByAltText("app.bsky.feed.post favicon")).toBeInTheDocument(); 282 283 fireEvent.click(screen.getByRole("button", { name: /app\.bsky\.feed\.post/u })); 284 285 expect(await screen.findAllByAltText("app.bsky.feed.post favicon")).not.toHaveLength(0); 286 }); 287 288 it("clears and rehydrates the explorer icon cache for the current repo view", async () => { 289 resolveInputMock.mockResolvedValue({ 290 input: "@alice.test", 291 inputKind: "handle", 292 targetKind: "repo", 293 normalizedInput: "did:plc:alice", 294 uri: "at://did:plc:alice", 295 did: "did:plc:alice", 296 handle: "alice.test", 297 pdsUrl: "https://pds.example.com", 298 collection: null, 299 rkey: null, 300 }); 301 describeRepoMock.mockResolvedValue({ collections: ["sh.tangled.feed.star"] }); 302 getLexiconFaviconsMock.mockResolvedValue({ "sh.tangled.feed.star": null }); 303 304 renderPanel(); 305 306 const input = screen.getByPlaceholderText(/at:\/\/did:\.\.\. or @handle or https:\/\/pds/u); 307 fireEvent.input(input, { target: { value: "@alice.test" } }); 308 fireEvent.submit(input.closest("form")!); 309 310 expect(await screen.findByRole("button", { name: /sh\.tangled\.feed\.star/u })).toBeInTheDocument(); 311 await waitFor(() => expect(getLexiconFaviconsMock).toHaveBeenCalledWith(["sh.tangled.feed.star"])); 312 313 fireEvent.click(screen.getByRole("button", { name: /clear icon cache/i })); 314 315 await waitFor(() => expect(clearLexiconFaviconCacheMock).toHaveBeenCalledOnce()); 316 await waitFor(() => expect(getLexiconFaviconsMock).toHaveBeenCalledTimes(2)); 317 expect(await screen.findByText("Cleared explorer icon cache.")).toBeInTheDocument(); 318 }); 319});