BlueSky & more on desktop
lazurite.stormlightlabs.org/
tauri
rust
typescript
bluesky
appview
atproto
solid
1import { HashRouter, Route } from "@solidjs/router";
2import { fireEvent, render, screen, waitFor } from "@solidjs/testing-library";
3import { beforeEach, describe, expect, it, vi } from "vitest";
4import { PostEngagementPanel } from "../PostEngagementPanel";
5
6const getRecordBacklinksMock = vi.hoisted(() => vi.fn());
7const moderateContentMock = vi.hoisted(() => vi.fn());
8const postNavigationMock = vi.hoisted(() => ({
9 backFromPost: vi.fn(),
10 buildPostHref: vi.fn(),
11 openPost: vi.fn(),
12 openPostEngagement: vi.fn(),
13 openPostScreen: vi.fn(),
14}));
15
16vi.mock("$/lib/api/diagnostics", () => ({ DiagnosticsController: { getRecordBacklinks: getRecordBacklinksMock } }));
17vi.mock("$/components/posts/hooks/usePostNavigation", () => ({ usePostNavigation: () => postNavigationMock }));
18vi.mock("$/lib/api/moderation", () => ({ ModerationController: { moderateContent: moderateContentMock } }));
19
20const POST_URI = "at://did:plc:alice/app.bsky.feed.post/123";
21
22function renderPanel(hash = `#/post/${encodeURIComponent(POST_URI)}/engagement`) {
23 globalThis.location.hash = hash;
24 return render(() => (
25 <HashRouter>
26 <Route path="/post/:encodedUri/engagement" component={() => <PostEngagementPanel uri={POST_URI} />} />
27 </HashRouter>
28 ));
29}
30
31describe("PostEngagementPanel", () => {
32 beforeEach(() => {
33 vi.resetAllMocks();
34 moderateContentMock.mockResolvedValue({
35 alert: false,
36 blur: "none",
37 filter: false,
38 inform: false,
39 noOverride: false,
40 });
41 getRecordBacklinksMock.mockResolvedValue({
42 likes: {
43 cursor: null,
44 records: [{
45 did: "did:plc:bob",
46 profile: { handle: "bob.test", displayName: "Bob" },
47 uri: "at://did:plc:bob/app.bsky.feed.like/1",
48 }],
49 total: 1,
50 },
51 quotes: {
52 cursor: null,
53 records: [{
54 did: "did:plc:carol",
55 profile: { handle: "carol.test", displayName: "Carol" },
56 uri: "at://did:plc:carol/app.bsky.feed.post/9",
57 value: { text: "This is a quoted post body." },
58 }],
59 total: 1,
60 },
61 replies: { cursor: null, records: [], total: 0 },
62 reposts: {
63 cursor: null,
64 records: [{
65 did: "did:plc:dana",
66 profile: { handle: "dana.test", displayName: "Dana" },
67 uri: "at://did:plc:dana/app.bsky.feed.repost/3",
68 }],
69 total: 1,
70 },
71 });
72 });
73
74 it("loads engagement and defaults to likes tab", async () => {
75 renderPanel();
76
77 expect(await screen.findByText("Post Engagement")).toBeInTheDocument();
78 expect(await screen.findByText("Bob")).toBeInTheDocument();
79 expect(getRecordBacklinksMock).toHaveBeenCalledWith(POST_URI);
80 });
81
82 it("opens quote posts from the quotes tab", async () => {
83 renderPanel(`#/post/${encodeURIComponent(POST_URI)}/engagement?tab=quotes`);
84
85 expect(await screen.findByText("This is a quoted post body.")).toBeInTheDocument();
86
87 fireEvent.click(screen.getByRole("button", { name: /carol/i }));
88
89 expect(postNavigationMock.openPostScreen).toHaveBeenCalledWith("at://did:plc:carol/app.bsky.feed.post/9");
90 });
91
92 it("switches engagement tabs via query-state routing", async () => {
93 renderPanel();
94
95 await screen.findByText("Bob");
96 fireEvent.click(screen.getByRole("button", { name: /Reposts/i }));
97
98 await waitFor(() => expect(globalThis.location.hash).toContain("tab=reposts"));
99 expect(await screen.findByText("Dana")).toBeInTheDocument();
100 });
101
102 it("renders profile moderation badges for labeled engagement actors", async () => {
103 getRecordBacklinksMock.mockResolvedValueOnce({
104 likes: {
105 cursor: null,
106 records: [{
107 did: "did:plc:bob",
108 profile: { handle: "bob.test", displayName: "Bob", labels: [{ src: "did:plc:labeler", val: "sexual" }] },
109 uri: "at://did:plc:bob/app.bsky.feed.like/1",
110 }],
111 total: 1,
112 },
113 quotes: { cursor: null, records: [], total: 0 },
114 replies: { cursor: null, records: [], total: 0 },
115 reposts: { cursor: null, records: [], total: 0 },
116 });
117 moderateContentMock.mockImplementation(async (_labels, context: string) => {
118 if (context === "profileList") {
119 return { alert: true, blur: "none", filter: false, inform: false, noOverride: false };
120 }
121
122 return { alert: false, blur: "none", filter: false, inform: false, noOverride: false };
123 });
124
125 renderPanel();
126 expect(await screen.findByText("Bob")).toBeInTheDocument();
127 expect(await screen.findByText("Alert")).toBeInTheDocument();
128 });
129});