Sifa professional network frontend (Next.js, React, TailwindCSS)
sifa.id/
1import { describe, it, expect, vi, beforeEach } from 'vitest';
2
3describe('embed.js', function () {
4 let initSifaEmbeds: () => Promise<void[]>;
5
6 beforeEach(async function () {
7 document.body.innerHTML = '';
8 vi.restoreAllMocks();
9 vi.resetModules();
10
11 const mod = await import('../../public/embed.js');
12 initSifaEmbeds = mod.initSifaEmbeds;
13 });
14
15 it('replaces script tag with shadow DOM container containing profile card', async function () {
16 const script = document.createElement('script');
17 script.setAttribute('src', 'https://sifa.id/embed.js');
18 script.setAttribute('data-did', 'did:plc:test123');
19 document.body.appendChild(script);
20
21 global.fetch = vi.fn().mockResolvedValue({
22 ok: true,
23 json: function () {
24 return Promise.resolve({
25 did: 'did:plc:test123',
26 handle: 'alice.bsky.social',
27 displayName: 'Alice',
28 headline: 'Engineer',
29 avatar: null,
30 location: { country: 'Amsterdam' },
31 profileUrl: 'https://sifa.id/p/alice.bsky.social',
32 trustStats: [],
33 verifiedAccounts: [],
34 openTo: [],
35 claimed: true,
36 });
37 },
38 });
39
40 await initSifaEmbeds();
41
42 const container = document.querySelector('.sifa-embed');
43 expect(container).not.toBeNull();
44 expect(container?.shadowRoot).not.toBeNull();
45 const html = container?.shadowRoot?.innerHTML ?? '';
46 expect(html).toContain('Alice');
47 expect(html).toContain('alice.bsky.social');
48 expect(html).toContain('sifa.id/p/alice.bsky.social');
49 });
50
51 it('uses data-handle when data-did is absent', async function () {
52 const script = document.createElement('script');
53 script.setAttribute('src', 'https://sifa.id/embed.js');
54 script.setAttribute('data-handle', 'bob.bsky.social');
55 document.body.appendChild(script);
56
57 global.fetch = vi.fn().mockResolvedValue({
58 ok: true,
59 json: function () {
60 return Promise.resolve({
61 did: 'did:plc:bob',
62 handle: 'bob.bsky.social',
63 displayName: 'Bob',
64 profileUrl: 'https://sifa.id/p/bob.bsky.social',
65 trustStats: [],
66 verifiedAccounts: [],
67 openTo: [],
68 claimed: true,
69 });
70 },
71 });
72
73 await initSifaEmbeds();
74
75 expect(global.fetch).toHaveBeenCalledWith(
76 expect.stringContaining('/api/embed/bob.bsky.social/data'),
77 );
78 });
79
80 it('shows error state when profile not found', async function () {
81 const script = document.createElement('script');
82 script.setAttribute('src', 'https://sifa.id/embed.js');
83 script.setAttribute('data-did', 'did:plc:invalid');
84 document.body.appendChild(script);
85
86 global.fetch = vi.fn().mockResolvedValue({ ok: false, status: 404 });
87
88 await initSifaEmbeds();
89
90 const container = document.querySelector('.sifa-embed');
91 const html = container?.shadowRoot?.innerHTML ?? '';
92 expect(html).toContain('Profile not found');
93 });
94
95 it('renders activity data (followers, PDS, app badges)', async function () {
96 const script = document.createElement('script');
97 script.setAttribute('src', 'https://sifa.id/embed.js');
98 script.setAttribute('data-did', 'did:plc:test');
99 document.body.appendChild(script);
100
101 global.fetch = vi.fn().mockResolvedValue({
102 ok: true,
103 json: function () {
104 return Promise.resolve({
105 did: 'did:plc:test',
106 handle: 'test.bsky.social',
107 displayName: 'Test',
108 profileUrl: 'https://sifa.id/p/test.bsky.social',
109 trustStats: [],
110 verifiedAccounts: [],
111 openTo: [],
112 followersCount: 1234,
113 pdsProvider: 'Bluesky',
114 activeApps: [{ id: 'bluesky', name: 'Bluesky' }],
115 claimed: true,
116 });
117 },
118 });
119
120 await initSifaEmbeds();
121
122 const html = document.querySelector('.sifa-embed')?.shadowRoot?.innerHTML ?? '';
123 expect(html).toContain('1.2K followers');
124 expect(html).toContain('Bluesky');
125 expect(html).toContain('app-badge');
126 });
127
128 it('prefers atprotoFollowersCount over followersCount', async function () {
129 const script = document.createElement('script');
130 script.setAttribute('src', 'https://sifa.id/embed.js');
131 script.setAttribute('data-did', 'did:plc:test');
132 document.body.appendChild(script);
133
134 global.fetch = vi.fn().mockResolvedValue({
135 ok: true,
136 json: function () {
137 return Promise.resolve({
138 did: 'did:plc:test',
139 handle: 'test.bsky.social',
140 displayName: 'Test',
141 profileUrl: 'https://sifa.id/p/test.bsky.social',
142 trustStats: [],
143 verifiedAccounts: [],
144 openTo: [],
145 followersCount: 5,
146 atprotoFollowersCount: 5678,
147 claimed: true,
148 });
149 },
150 });
151
152 await initSifaEmbeds();
153
154 const html = document.querySelector('.sifa-embed')?.shadowRoot?.innerHTML ?? '';
155 expect(html).toContain('5.7K followers on Bluesky');
156 expect(html).not.toContain('5 followers');
157 });
158
159 it('renders open-to pills', async function () {
160 const script = document.createElement('script');
161 script.setAttribute('src', 'https://sifa.id/embed.js');
162 script.setAttribute('data-did', 'did:plc:test');
163 document.body.appendChild(script);
164
165 global.fetch = vi.fn().mockResolvedValue({
166 ok: true,
167 json: function () {
168 return Promise.resolve({
169 did: 'did:plc:test',
170 handle: 'test',
171 displayName: 'Test',
172 profileUrl: 'https://sifa.id/p/test',
173 trustStats: [],
174 verifiedAccounts: [],
175 openTo: ['Mentoring', 'Speaking'],
176 claimed: true,
177 });
178 },
179 });
180
181 await initSifaEmbeds();
182
183 const html = document.querySelector('.sifa-embed')?.shadowRoot?.innerHTML ?? '';
184 expect(html).toContain('Mentoring');
185 expect(html).toContain('Speaking');
186 });
187
188 it('applies dark theme styles', async function () {
189 const script = document.createElement('script');
190 script.setAttribute('src', 'https://sifa.id/embed.js');
191 script.setAttribute('data-did', 'did:plc:test');
192 script.setAttribute('data-theme', 'dark');
193 document.body.appendChild(script);
194
195 global.fetch = vi.fn().mockResolvedValue({
196 ok: true,
197 json: function () {
198 return Promise.resolve({
199 did: 'did:plc:test',
200 handle: 'test',
201 displayName: 'Test',
202 profileUrl: 'https://sifa.id/p/test',
203 trustStats: [],
204 verifiedAccounts: [],
205 openTo: [],
206 claimed: true,
207 });
208 },
209 });
210
211 await initSifaEmbeds();
212
213 const style =
214 document.querySelector('.sifa-embed')?.shadowRoot?.querySelector('style')?.textContent ?? '';
215 expect(style).toContain('#1a1a2e');
216 });
217
218 it('renders avatar image when avatar URL is provided', async function () {
219 const script = document.createElement('script');
220 script.setAttribute('src', 'https://sifa.id/embed.js');
221 script.setAttribute('data-did', 'did:plc:test');
222 document.body.appendChild(script);
223
224 global.fetch = vi.fn().mockResolvedValue({
225 ok: true,
226 json: function () {
227 return Promise.resolve({
228 did: 'did:plc:test',
229 handle: 'test',
230 displayName: 'Test User',
231 avatar: 'https://cdn.example.com/avatar.jpg',
232 profileUrl: 'https://sifa.id/p/test',
233 trustStats: [],
234 verifiedAccounts: [],
235 openTo: [],
236 claimed: true,
237 });
238 },
239 });
240
241 await initSifaEmbeds();
242
243 const html = document.querySelector('.sifa-embed')?.shadowRoot?.innerHTML ?? '';
244 expect(html).toContain('avatar.jpg');
245 expect(html).toContain('<img');
246 });
247
248 it('renders letter placeholder when no avatar', async function () {
249 const script = document.createElement('script');
250 script.setAttribute('src', 'https://sifa.id/embed.js');
251 script.setAttribute('data-did', 'did:plc:test');
252 document.body.appendChild(script);
253
254 global.fetch = vi.fn().mockResolvedValue({
255 ok: true,
256 json: function () {
257 return Promise.resolve({
258 did: 'did:plc:test',
259 handle: 'test',
260 displayName: 'Alice',
261 avatar: null,
262 profileUrl: 'https://sifa.id/p/test',
263 trustStats: [],
264 verifiedAccounts: [],
265 openTo: [],
266 claimed: true,
267 });
268 },
269 });
270
271 await initSifaEmbeds();
272
273 const html = document.querySelector('.sifa-embed')?.shadowRoot?.innerHTML ?? '';
274 expect(html).toContain('avatar-placeholder');
275 expect(html).toContain('A');
276 });
277});