Barazo default frontend
barazo.forum
1/**
2 * Tests for formatting utilities.
3 */
4
5import { describe, it, expect } from 'vitest'
6import {
7 formatRelativeTime,
8 formatCompactNumber,
9 slugify,
10 getTopicUrl,
11 getReplyUrl,
12 isEdited,
13} from './format'
14
15describe('formatRelativeTime', () => {
16 it('returns "just now" for recent timestamps', () => {
17 const now = new Date()
18 expect(formatRelativeTime(now.toISOString())).toBe('just now')
19 })
20
21 it('returns minutes ago', () => {
22 const date = new Date(Date.now() - 5 * 60 * 1000)
23 expect(formatRelativeTime(date.toISOString())).toBe('5m ago')
24 })
25
26 it('returns hours ago', () => {
27 const date = new Date(Date.now() - 3 * 60 * 60 * 1000)
28 expect(formatRelativeTime(date.toISOString())).toBe('3h ago')
29 })
30})
31
32describe('formatCompactNumber', () => {
33 it('returns number as-is below 1000', () => {
34 expect(formatCompactNumber(42)).toBe('42')
35 })
36
37 it('formats thousands with k suffix', () => {
38 expect(formatCompactNumber(1200)).toBe('1.2k')
39 })
40
41 it('formats millions with M suffix', () => {
42 expect(formatCompactNumber(3400000)).toBe('3.4M')
43 })
44})
45
46describe('slugify', () => {
47 it('converts title to lowercase slug', () => {
48 expect(slugify('Hello World')).toBe('hello-world')
49 })
50
51 it('removes special characters', () => {
52 expect(slugify('Building with the AT Protocol!')).toBe('building-with-the-at-protocol')
53 })
54
55 it('collapses multiple hyphens', () => {
56 expect(slugify('Hello --- World')).toBe('hello-world')
57 })
58
59 it('trims leading/trailing hyphens', () => {
60 expect(slugify('---Hello World---')).toBe('hello-world')
61 })
62
63 it('handles empty string', () => {
64 expect(slugify('')).toBe('untitled')
65 })
66
67 it('truncates long slugs', () => {
68 const longTitle = 'A'.repeat(200)
69 const slug = slugify(longTitle)
70 expect(slug.length).toBeLessThanOrEqual(80)
71 })
72})
73
74describe('getTopicUrl', () => {
75 it('generates correct URL from topic', () => {
76 const topic = {
77 authorHandle: 'jay.bsky.team',
78 rkey: '3kf1abc',
79 }
80 expect(getTopicUrl(topic)).toBe('/jay.bsky.team/3kf1abc')
81 })
82
83 it('handles different handles', () => {
84 const topic = {
85 authorHandle: 'alex.example.com',
86 rkey: '3kf3ghi',
87 }
88 expect(getTopicUrl(topic)).toBe('/alex.example.com/3kf3ghi')
89 })
90})
91
92describe('getReplyUrl', () => {
93 it('generates correct reply permalink URL', () => {
94 expect(
95 getReplyUrl({
96 topicAuthorHandle: 'jay.bsky.team',
97 topicRkey: '3kf1abc',
98 replyAuthorHandle: 'alex.example.com',
99 replyRkey: '3kf2def',
100 })
101 ).toBe('/jay.bsky.team/3kf1abc/alex.example.com/3kf2def')
102 })
103})
104
105describe('isEdited', () => {
106 it('returns false when timestamps are equal', () => {
107 const ts = '2026-01-01T12:00:00.000Z'
108 expect(isEdited(ts, ts)).toBe(false)
109 })
110
111 it('returns false when difference is under 30 seconds', () => {
112 const createdAt = '2026-01-01T12:00:00.000Z'
113 const indexedAt = new Date(new Date(createdAt).getTime() + 15_000).toISOString()
114 expect(isEdited(createdAt, indexedAt)).toBe(false)
115 })
116
117 it('returns true when difference exceeds 30 seconds', () => {
118 const createdAt = '2026-01-01T12:00:00.000Z'
119 const indexedAt = new Date(new Date(createdAt).getTime() + 60_000).toISOString()
120 expect(isEdited(createdAt, indexedAt)).toBe(true)
121 })
122
123 it('returns false for invalid date strings', () => {
124 expect(isEdited('not-a-date', '2026-01-01T12:00:00.000Z')).toBe(false)
125 expect(isEdited('2026-01-01T12:00:00.000Z', 'not-a-date')).toBe(false)
126 expect(isEdited('invalid', 'also-invalid')).toBe(false)
127 })
128})