@commonpub/layer 0.3.36 → 0.3.37

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@commonpub/layer",
3
- "version": "0.3.36",
3
+ "version": "0.3.37",
4
4
  "type": "module",
5
5
  "main": "./nuxt.config.ts",
6
6
  "files": [
@@ -15,7 +15,12 @@
15
15
  "plugins",
16
16
  "server",
17
17
  "theme",
18
- "types"
18
+ "types",
19
+ "!**/__tests__/",
20
+ "!**/*.test.ts",
21
+ "!**/*.spec.ts",
22
+ "!vitest.config.ts",
23
+ "!test-setup.ts"
19
24
  ],
20
25
  "publishConfig": {
21
26
  "access": "public"
@@ -44,11 +49,11 @@
44
49
  "vue": "^3.4.0",
45
50
  "vue-router": "^4.3.0",
46
51
  "zod": "^4.3.6",
47
- "@commonpub/config": "0.7.1",
48
52
  "@commonpub/auth": "0.5.0",
53
+ "@commonpub/config": "0.7.1",
49
54
  "@commonpub/editor": "0.5.0",
50
- "@commonpub/docs": "0.5.2",
51
55
  "@commonpub/learning": "0.5.0",
56
+ "@commonpub/docs": "0.5.2",
52
57
  "@commonpub/protocol": "0.9.5",
53
58
  "@commonpub/schema": "0.8.13",
54
59
  "@commonpub/server": "2.20.1",
package/pages/search.vue CHANGED
@@ -1,5 +1,5 @@
1
1
  <script setup lang="ts">
2
- import type { Serialized, ContentListItem, PaginatedResponse } from '@commonpub/server';
2
+ import type { PaginatedResponse } from '@commonpub/server';
3
3
 
4
4
  useSeoMeta({
5
5
  title: `Search — ${useSiteName()}`,
@@ -67,7 +67,8 @@ const searchQuery = computed(() => ({
67
67
  community: communityFilter.value || undefined,
68
68
  }));
69
69
 
70
- const { data: results, status } = await useFetch<PaginatedResponse<Serialized<ContentListItem>>>('/api/search', {
70
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
71
+ const { data: results, status } = await useFetch<PaginatedResponse<any>>('/api/search', {
71
72
  query: searchQuery,
72
73
  watch: [searchQuery],
73
74
  lazy: true,
@@ -1,5 +1,5 @@
1
1
  import { searchContent, listHubs, escapeLike } from '@commonpub/server';
2
- import type { ContentSearchOptions } from '@commonpub/server';
2
+ import type { ContentSearchOptions, MeiliClient } from '@commonpub/server';
3
3
  import { users, follows, hubs } from '@commonpub/schema';
4
4
  import { sql, desc, ilike, or, and, isNull, eq } from 'drizzle-orm';
5
5
  import { z } from 'zod';
@@ -45,7 +45,7 @@ export default defineEventHandler(async (event): Promise<{ items: unknown[]; tot
45
45
  bannerUrl: hub.bannerUrl,
46
46
  memberCount: hub.memberCount,
47
47
  postCount: hub.postCount,
48
- source: (hub as Record<string, unknown>).source ?? 'local',
48
+ source: (hub as unknown as Record<string, unknown>).source ?? 'local',
49
49
  })),
50
50
  total: result.total,
51
51
  };
@@ -108,7 +108,7 @@ export default defineEventHandler(async (event): Promise<{ items: unknown[]; tot
108
108
  const meiliKey = process.env.MEILI_MASTER_KEY;
109
109
  if (meiliUrl) {
110
110
  const { MeiliSearch } = await import('meilisearch');
111
- meiliClient = new MeiliSearch({ host: meiliUrl, apiKey: meiliKey });
111
+ meiliClient = new MeiliSearch({ host: meiliUrl, apiKey: meiliKey }) as unknown as MeiliClient;
112
112
  }
113
113
  } catch { /* Meilisearch not available */ }
114
114
 
@@ -0,0 +1,5 @@
1
+ declare module 'meilisearch' {
2
+ export class MeiliSearch {
3
+ constructor(options: { host: string; apiKey?: string });
4
+ }
5
+ }
@@ -1,340 +0,0 @@
1
- /**
2
- * Component tests for FederatedContentCard.
3
- *
4
- * Tests rendering of federated content from CommonPub and non-CommonPub sources,
5
- * computed properties (typeLabel, actorHandle, timeAgo), event emission, and
6
- * conditional rendering (avatar, cover image, tags, title link).
7
- */
8
- import { describe, it, expect } from 'vitest';
9
- import { render, screen, fireEvent } from '@testing-library/vue';
10
- import { defineComponent, h } from 'vue';
11
- import FederatedContentCard from '../FederatedContentCard.vue';
12
-
13
- // Stub NuxtLink as a plain <a> tag
14
- const NuxtLink = defineComponent({
15
- name: 'NuxtLink',
16
- props: { to: String },
17
- setup(props, { slots }) {
18
- return () => h('a', { href: props.to }, slots.default?.());
19
- },
20
- });
21
-
22
- const stubs = { NuxtLink };
23
-
24
- function makeContent(overrides: Record<string, unknown> = {}) {
25
- return {
26
- id: 'fed-1',
27
- objectUri: 'https://remote.example.com/content/test',
28
- apType: 'Article',
29
- title: 'LED Cube Build',
30
- content: '<p>Build a 4x4x4 LED cube</p>',
31
- summary: '<p>A <strong>complete</strong> LED cube tutorial</p>',
32
- url: 'https://remote.example.com/project/led-cube',
33
- coverImageUrl: null,
34
- tags: [],
35
- attachments: [],
36
- inReplyTo: null,
37
- cpubType: 'project',
38
- cpubMetadata: null,
39
- cpubBlocks: null,
40
- localLikeCount: 5,
41
- localCommentCount: 2,
42
- localViewCount: 100,
43
- publishedAt: new Date(Date.now() - 3600000).toISOString(), // 1 hour ago
44
- receivedAt: new Date().toISOString(),
45
- originDomain: 'remote.example.com',
46
- actor: {
47
- actorUri: 'https://remote.example.com/users/alice',
48
- preferredUsername: 'alice',
49
- displayName: 'Alice Builder',
50
- avatarUrl: 'https://remote.example.com/avatars/alice.png',
51
- instanceDomain: 'remote.example.com',
52
- },
53
- ...overrides,
54
- };
55
- }
56
-
57
- describe('FederatedContentCard', () => {
58
- // --- Basic rendering ---
59
-
60
- it('renders title', () => {
61
- render(FederatedContentCard, {
62
- props: { content: makeContent() },
63
- global: { stubs },
64
- });
65
- expect(screen.getByText('LED Cube Build')).toBeInTheDocument();
66
- });
67
-
68
- it('renders actor name and handle', () => {
69
- render(FederatedContentCard, {
70
- props: { content: makeContent() },
71
- global: { stubs },
72
- });
73
- expect(screen.getByText('Alice Builder')).toBeInTheDocument();
74
- expect(screen.getByText('@alice@remote.example.com')).toBeInTheDocument();
75
- });
76
-
77
- it('renders origin domain badge', () => {
78
- render(FederatedContentCard, {
79
- props: { content: makeContent() },
80
- global: { stubs },
81
- });
82
- expect(screen.getByText('remote.example.com')).toBeInTheDocument();
83
- });
84
-
85
- it('strips HTML from summary', () => {
86
- render(FederatedContentCard, {
87
- props: { content: makeContent() },
88
- global: { stubs },
89
- });
90
- const summary = screen.getByText('A complete LED cube tutorial');
91
- expect(summary).toBeInTheDocument();
92
- // Should NOT contain HTML tags
93
- expect(summary.innerHTML).not.toContain('<strong>');
94
- expect(summary.innerHTML).not.toContain('<p>');
95
- });
96
-
97
- // --- Type label computed ---
98
-
99
- it('shows cpubType as type badge when present', () => {
100
- render(FederatedContentCard, {
101
- props: { content: makeContent({ cpubType: 'project' }) },
102
- global: { stubs },
103
- });
104
- expect(screen.getByText('project')).toBeInTheDocument();
105
- });
106
-
107
- it('shows "article" for AP Article without cpubType', () => {
108
- render(FederatedContentCard, {
109
- props: { content: makeContent({ cpubType: null, apType: 'Article' }) },
110
- global: { stubs },
111
- });
112
- expect(screen.getByText('article')).toBeInTheDocument();
113
- });
114
-
115
- it('shows "post" for AP Note without cpubType', () => {
116
- render(FederatedContentCard, {
117
- props: { content: makeContent({ cpubType: null, apType: 'Note' }) },
118
- global: { stubs },
119
- });
120
- expect(screen.getByText('post')).toBeInTheDocument();
121
- });
122
-
123
- // --- Avatar rendering ---
124
-
125
- it('renders avatar image when actor has avatarUrl', () => {
126
- render(FederatedContentCard, {
127
- props: { content: makeContent() },
128
- global: { stubs },
129
- });
130
- const img = screen.getByAltText('Alice Builder avatar');
131
- expect(img).toBeInTheDocument();
132
- expect(img).toHaveAttribute('src', 'https://remote.example.com/avatars/alice.png');
133
- });
134
-
135
- it('renders placeholder when actor has no avatarUrl', () => {
136
- const { container } = render(FederatedContentCard, {
137
- props: {
138
- content: makeContent({
139
- actor: {
140
- actorUri: 'https://remote.example.com/users/bob',
141
- preferredUsername: 'bob',
142
- displayName: 'Bob',
143
- avatarUrl: null,
144
- instanceDomain: 'remote.example.com',
145
- },
146
- }),
147
- },
148
- global: { stubs },
149
- });
150
- const placeholder = container.querySelector('.cpub-fed-card__avatar--placeholder');
151
- expect(placeholder).toBeInTheDocument();
152
- expect(placeholder?.textContent).toBe('B');
153
- });
154
-
155
- // --- Cover image ---
156
-
157
- it('renders cover image through proxy when coverImageUrl present', () => {
158
- const { container } = render(FederatedContentCard, {
159
- props: {
160
- content: makeContent({
161
- coverImageUrl: 'https://remote.example.com/img/cover.jpg',
162
- }),
163
- },
164
- global: { stubs },
165
- });
166
- const cover = container.querySelector('.cpub-fed-card__cover img');
167
- expect(cover).toBeInTheDocument();
168
- expect(cover?.getAttribute('src')).toContain('/api/image-proxy');
169
- expect(cover?.getAttribute('src')).toContain(encodeURIComponent('https://remote.example.com/img/cover.jpg'));
170
- });
171
-
172
- it('does not render cover image when coverImageUrl is null', () => {
173
- const { container } = render(FederatedContentCard, {
174
- props: { content: makeContent({ coverImageUrl: null }) },
175
- global: { stubs },
176
- });
177
- expect(container.querySelector('.cpub-fed-card__cover')).not.toBeInTheDocument();
178
- });
179
-
180
- // --- Tags ---
181
-
182
- it('renders tags when present', () => {
183
- render(FederatedContentCard, {
184
- props: {
185
- content: makeContent({
186
- tags: [
187
- { type: 'Hashtag', name: '#electronics' },
188
- { type: 'Hashtag', name: '#led' },
189
- ],
190
- }),
191
- },
192
- global: { stubs },
193
- });
194
- expect(screen.getByText('#electronics')).toBeInTheDocument();
195
- expect(screen.getByText('#led')).toBeInTheDocument();
196
- });
197
-
198
- it('limits tags to 5', () => {
199
- const tags = Array.from({ length: 8 }, (_, i) => ({
200
- type: 'Hashtag',
201
- name: `#tag${i}`,
202
- }));
203
- const { container } = render(FederatedContentCard, {
204
- props: { content: makeContent({ tags }) },
205
- global: { stubs },
206
- });
207
- const tagElements = container.querySelectorAll('.cpub-fed-card__tag');
208
- expect(tagElements.length).toBe(5);
209
- });
210
-
211
- it('hides tags section when empty', () => {
212
- const { container } = render(FederatedContentCard, {
213
- props: { content: makeContent({ tags: [] }) },
214
- global: { stubs },
215
- });
216
- expect(container.querySelector('.cpub-fed-card__tags')).not.toBeInTheDocument();
217
- });
218
-
219
- // --- Like count ---
220
-
221
- it('shows like count when > 0', () => {
222
- render(FederatedContentCard, {
223
- props: { content: makeContent({ localLikeCount: 5 }) },
224
- global: { stubs },
225
- });
226
- expect(screen.getByLabelText('Like this project')).toHaveTextContent('5 Like');
227
- });
228
-
229
- it('hides like count when 0', () => {
230
- render(FederatedContentCard, {
231
- props: { content: makeContent({ localLikeCount: 0 }) },
232
- global: { stubs },
233
- });
234
- expect(screen.getByLabelText('Like this project')).toHaveTextContent('Like');
235
- expect(screen.getByLabelText('Like this project').textContent?.trim()).toBe('Like');
236
- });
237
-
238
- // --- Events ---
239
-
240
- it('emits like event with content id', async () => {
241
- const { emitted } = render(FederatedContentCard, {
242
- props: { content: makeContent() },
243
- global: { stubs },
244
- });
245
- await fireEvent.click(screen.getByLabelText('Like this project'));
246
- expect(emitted().like).toBeTruthy();
247
- expect(emitted().like[0]).toEqual(['fed-1']);
248
- });
249
-
250
- it('emits boost event with content id', async () => {
251
- const { emitted } = render(FederatedContentCard, {
252
- props: { content: makeContent() },
253
- global: { stubs },
254
- });
255
- await fireEvent.click(screen.getByLabelText('Boost this project'));
256
- expect(emitted().boost).toBeTruthy();
257
- expect(emitted().boost[0]).toEqual(['fed-1']);
258
- });
259
-
260
- // --- Title link ---
261
-
262
- it('renders title as link when url is present', () => {
263
- render(FederatedContentCard, {
264
- props: { content: makeContent() },
265
- global: { stubs },
266
- });
267
- const link = screen.getByText('LED Cube Build').closest('a');
268
- expect(link).toHaveAttribute('href', 'https://remote.example.com/project/led-cube');
269
- expect(link).toHaveAttribute('target', '_blank');
270
- });
271
-
272
- it('renders title as plain text when url is null', () => {
273
- render(FederatedContentCard, {
274
- props: { content: makeContent({ url: null }) },
275
- global: { stubs },
276
- });
277
- const title = screen.getByText('LED Cube Build');
278
- expect(title.tagName).toBe('SPAN');
279
- });
280
-
281
- // --- View Original link ---
282
-
283
- it('shows View Original link when url present', () => {
284
- render(FederatedContentCard, {
285
- props: { content: makeContent() },
286
- global: { stubs },
287
- });
288
- const link = screen.getByText('View Original');
289
- expect(link).toHaveAttribute('href', 'https://remote.example.com/project/led-cube');
290
- expect(link).toHaveAttribute('rel', 'noopener');
291
- });
292
-
293
- it('hides View Original when no url', () => {
294
- render(FederatedContentCard, {
295
- props: { content: makeContent({ url: null }) },
296
- global: { stubs },
297
- });
298
- expect(screen.queryByText('View Original')).not.toBeInTheDocument();
299
- });
300
-
301
- // --- Time ago ---
302
-
303
- it('shows relative time for recent content', () => {
304
- const { container } = render(FederatedContentCard, {
305
- props: {
306
- content: makeContent({
307
- publishedAt: new Date(Date.now() - 30 * 60000).toISOString(),
308
- }),
309
- },
310
- global: { stubs },
311
- });
312
- const time = container.querySelector('.cpub-fed-card__time');
313
- expect(time?.textContent).toBe('30m');
314
- });
315
-
316
- it('shows hours for content from today', () => {
317
- const { container } = render(FederatedContentCard, {
318
- props: {
319
- content: makeContent({
320
- publishedAt: new Date(Date.now() - 5 * 3600000).toISOString(),
321
- }),
322
- },
323
- global: { stubs },
324
- });
325
- const time = container.querySelector('.cpub-fed-card__time');
326
- expect(time?.textContent).toBe('5h');
327
- });
328
-
329
- // --- Fallback values ---
330
-
331
- it('shows Unknown when actor is null', () => {
332
- render(FederatedContentCard, {
333
- props: { content: makeContent({ actor: null }) },
334
- global: { stubs },
335
- });
336
- // Both actorName and actorHandle render "Unknown" fallback
337
- const unknowns = screen.getAllByText('Unknown');
338
- expect(unknowns.length).toBeGreaterThanOrEqual(1);
339
- });
340
- });
@@ -1,208 +0,0 @@
1
- /**
2
- * Unit tests for useMirrorContent composable.
3
- *
4
- * Tests the contentType resolution logic which determines how federated
5
- * content is displayed — critical for distinguishing CommonPub vs non-CommonPub
6
- * content types.
7
- */
8
- import { describe, it, expect } from 'vitest';
9
- import { ref, nextTick } from 'vue';
10
- import { useMirrorContent } from '../useMirrorContent';
11
-
12
- function makeFedContent(overrides: Record<string, unknown> = {}) {
13
- return {
14
- id: 'fed-1',
15
- objectUri: 'https://remote.example.com/content/test',
16
- apType: 'Article',
17
- cpubType: null,
18
- title: 'Test Content',
19
- content: '<p>Hello world</p>',
20
- summary: 'A test',
21
- url: 'https://remote.example.com/article/test',
22
- coverImageUrl: null,
23
- tags: [],
24
- attachments: [],
25
- cpubMetadata: null,
26
- cpubBlocks: null,
27
- localLikeCount: 0,
28
- localCommentCount: 0,
29
- localViewCount: 0,
30
- publishedAt: '2026-03-20T10:00:00Z',
31
- receivedAt: '2026-03-20T11:00:00Z',
32
- originDomain: 'remote.example.com',
33
- actor: {
34
- actorUri: 'https://remote.example.com/users/alice',
35
- preferredUsername: 'alice',
36
- displayName: 'Alice',
37
- avatarUrl: null,
38
- instanceDomain: 'remote.example.com',
39
- },
40
- ...overrides,
41
- };
42
- }
43
-
44
- describe('useMirrorContent', () => {
45
- // --- contentType resolution ---
46
-
47
- describe('contentType', () => {
48
- it('returns cpubType when present (CommonPub project)', () => {
49
- const fedContent = ref(makeFedContent({ cpubType: 'project' }));
50
- const { contentType } = useMirrorContent(fedContent);
51
- expect(contentType.value).toBe('project');
52
- });
53
-
54
- it('returns cpubType when present (CommonPub article)', () => {
55
- const fedContent = ref(makeFedContent({ cpubType: 'article' }));
56
- const { contentType } = useMirrorContent(fedContent);
57
- expect(contentType.value).toBe('article');
58
- });
59
-
60
- it('returns cpubType when present (CommonPub blog)', () => {
61
- const fedContent = ref(makeFedContent({ cpubType: 'blog' }));
62
- const { contentType } = useMirrorContent(fedContent);
63
- expect(contentType.value).toBe('blog');
64
- });
65
-
66
- it('returns cpubType when present (CommonPub explainer)', () => {
67
- const fedContent = ref(makeFedContent({ cpubType: 'explainer' }));
68
- const { contentType } = useMirrorContent(fedContent);
69
- expect(contentType.value).toBe('explainer');
70
- });
71
-
72
- it('falls back to apType lowercase for non-CommonPub Article', () => {
73
- const fedContent = ref(makeFedContent({ cpubType: null, apType: 'Article' }));
74
- const { contentType } = useMirrorContent(fedContent);
75
- expect(contentType.value).toBe('article');
76
- });
77
-
78
- it('falls back to apType lowercase for Note', () => {
79
- const fedContent = ref(makeFedContent({ cpubType: null, apType: 'Note' }));
80
- const { contentType } = useMirrorContent(fedContent);
81
- expect(contentType.value).toBe('note');
82
- });
83
-
84
- it('falls back to "article" when both cpubType and apType are null', () => {
85
- const fedContent = ref(makeFedContent({ cpubType: null, apType: null }));
86
- const { contentType } = useMirrorContent(fedContent);
87
- expect(contentType.value).toBe('article');
88
- });
89
-
90
- it('prefers cpubType over apType', () => {
91
- const fedContent = ref(makeFedContent({ cpubType: 'project', apType: 'Article' }));
92
- const { contentType } = useMirrorContent(fedContent);
93
- expect(contentType.value).toBe('project');
94
- });
95
- });
96
-
97
- // --- transformedContent ---
98
-
99
- describe('transformedContent', () => {
100
- it('returns null when fedContent is null', () => {
101
- const fedContent = ref(null);
102
- const { transformedContent } = useMirrorContent(fedContent);
103
- expect(transformedContent.value).toBeNull();
104
- });
105
-
106
- it('maps title correctly', () => {
107
- const fedContent = ref(makeFedContent({ title: 'LED Cube Build' }));
108
- const { transformedContent } = useMirrorContent(fedContent);
109
- expect(transformedContent.value?.title).toBe('LED Cube Build');
110
- });
111
-
112
- it('uses "Untitled" when title is null', () => {
113
- const fedContent = ref(makeFedContent({ title: null }));
114
- const { transformedContent } = useMirrorContent(fedContent);
115
- expect(transformedContent.value?.title).toBe('Untitled');
116
- });
117
-
118
- it('preserves cpubBlocks when present (CommonPub-to-CommonPub)', () => {
119
- const blocks = [['paragraph', { text: 'Hello' }], ['heading', { level: 2, text: 'World' }]];
120
- const fedContent = ref(makeFedContent({ cpubBlocks: blocks }));
121
- const { transformedContent } = useMirrorContent(fedContent);
122
- expect(transformedContent.value?.content).toEqual(blocks);
123
- });
124
-
125
- it('wraps HTML content as paragraph block (non-CommonPub)', () => {
126
- const fedContent = ref(makeFedContent({
127
- cpubBlocks: null,
128
- content: '<p>Hello from Mastodon</p>',
129
- }));
130
- const { transformedContent } = useMirrorContent(fedContent);
131
- expect(transformedContent.value?.content).toEqual([
132
- ['paragraph', { html: '<p>Hello from Mastodon</p>' }],
133
- ]);
134
- });
135
-
136
- it('extracts metadata from cpubMetadata', () => {
137
- const fedContent = ref(makeFedContent({
138
- cpubType: 'project',
139
- cpubMetadata: { difficulty: 'intermediate', buildTime: '4h', estimatedCost: '$50' },
140
- }));
141
- const { transformedContent } = useMirrorContent(fedContent);
142
- expect(transformedContent.value?.difficulty).toBe('intermediate');
143
- expect(transformedContent.value?.buildTime).toBe('4h');
144
- expect(transformedContent.value?.estimatedCost).toBe('$50');
145
- });
146
-
147
- it('maps tags to expected format', () => {
148
- const fedContent = ref(makeFedContent({
149
- tags: [
150
- { type: 'Hashtag', name: '#electronics' },
151
- { type: 'Hashtag', name: '#led' },
152
- ],
153
- }));
154
- const { transformedContent } = useMirrorContent(fedContent);
155
- expect(transformedContent.value?.tags).toHaveLength(2);
156
- expect(transformedContent.value?.tags[0]?.name).toBe('#electronics');
157
- });
158
-
159
- it('maps actor to author format', () => {
160
- const fedContent = ref(makeFedContent({
161
- actor: {
162
- actorUri: 'https://remote.example.com/users/bob',
163
- preferredUsername: 'bob',
164
- displayName: 'Bob Builder',
165
- avatarUrl: 'https://remote.example.com/avatar.png',
166
- instanceDomain: 'remote.example.com',
167
- followerCount: 42,
168
- },
169
- }));
170
- const { transformedContent } = useMirrorContent(fedContent);
171
- expect(transformedContent.value?.author.username).toBe('bob');
172
- expect(transformedContent.value?.author.displayName).toBe('Bob Builder');
173
- expect(transformedContent.value?.author.avatarUrl).toBe('https://remote.example.com/avatar.png');
174
- });
175
- });
176
-
177
- // --- originDomain ---
178
-
179
- describe('originDomain', () => {
180
- it('extracts origin domain', () => {
181
- const fedContent = ref(makeFedContent({ originDomain: 'mastodon.social' }));
182
- const { originDomain } = useMirrorContent(fedContent);
183
- expect(originDomain.value).toBe('mastodon.social');
184
- });
185
-
186
- it('falls back to "unknown" when null', () => {
187
- const fedContent = ref(makeFedContent({ originDomain: null }));
188
- const { originDomain } = useMirrorContent(fedContent);
189
- expect(originDomain.value).toBe('unknown');
190
- });
191
- });
192
-
193
- // --- authorHandle ---
194
-
195
- describe('authorHandle', () => {
196
- it('formats as @user@domain', () => {
197
- const fedContent = ref(makeFedContent());
198
- const { authorHandle } = useMirrorContent(fedContent);
199
- expect(authorHandle.value).toBe('@alice@remote.example.com');
200
- });
201
-
202
- it('returns empty string when no actor', () => {
203
- const fedContent = ref(makeFedContent({ actor: null }));
204
- const { authorHandle } = useMirrorContent(fedContent);
205
- expect(authorHandle.value).toBe('');
206
- });
207
- });
208
- });