MediaAttachment.test.ts raw
1 import { describe, it, expect } from 'vitest'
2 import { MediaAttachment } from './MediaAttachment'
3
4 describe('MediaAttachment', () => {
5 describe('fromUrl', () => {
6 it('detects image from URL extension', () => {
7 const attachment = MediaAttachment.fromUrl('https://example.com/photo.jpg')
8
9 expect(attachment.type).toBe('image')
10 expect(attachment.url).toBe('https://example.com/photo.jpg')
11 expect(attachment.status).toBe('completed')
12 expect(attachment.isImage).toBe(true)
13 })
14
15 it('detects video from URL extension', () => {
16 const attachment = MediaAttachment.fromUrl('https://example.com/video.mp4')
17
18 expect(attachment.type).toBe('video')
19 expect(attachment.isVideo).toBe(true)
20 })
21
22 it('detects audio from URL extension', () => {
23 const attachment = MediaAttachment.fromUrl('https://example.com/audio.mp3')
24
25 expect(attachment.type).toBe('audio')
26 expect(attachment.isAudio).toBe(true)
27 })
28
29 it('defaults to file for unknown extensions', () => {
30 const attachment = MediaAttachment.fromUrl('https://example.com/document.pdf')
31
32 expect(attachment.type).toBe('file')
33 })
34
35 it('uses mime type over URL extension', () => {
36 const attachment = MediaAttachment.fromUrl(
37 'https://example.com/media',
38 'video/mp4'
39 )
40
41 expect(attachment.type).toBe('video')
42 })
43
44 it('handles URLs with query parameters', () => {
45 const attachment = MediaAttachment.fromUrl(
46 'https://example.com/photo.png?size=large'
47 )
48
49 expect(attachment.type).toBe('image')
50 })
51
52 it('detects various image formats', () => {
53 const formats = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'heic', 'avif', 'svg']
54
55 for (const format of formats) {
56 const attachment = MediaAttachment.fromUrl(`https://example.com/img.${format}`)
57 expect(attachment.type).toBe('image')
58 }
59 })
60
61 it('detects various video formats', () => {
62 const formats = ['mp4', 'webm', 'mov', 'avi', 'mkv']
63
64 for (const format of formats) {
65 const attachment = MediaAttachment.fromUrl(`https://example.com/vid.${format}`)
66 expect(attachment.type).toBe('video')
67 }
68 })
69
70 it('detects various audio formats', () => {
71 const formats = ['mp3', 'wav', 'ogg', 'flac', 'm4a']
72
73 for (const format of formats) {
74 const attachment = MediaAttachment.fromUrl(`https://example.com/aud.${format}`)
75 expect(attachment.type).toBe('audio')
76 }
77 })
78 })
79
80 describe('fromMetadata', () => {
81 it('creates attachment with full metadata', () => {
82 const metadata = {
83 url: 'https://example.com/photo.jpg',
84 mimeType: 'image/jpeg',
85 width: 1920,
86 height: 1080,
87 size: 102400,
88 blurhash: 'LEHV6nWB2yk8pyo0adR*.7kCMdnj',
89 sha256: 'd'.repeat(64),
90 alt: 'A beautiful sunset'
91 }
92
93 const attachment = MediaAttachment.fromMetadata(metadata)
94
95 expect(attachment.url).toBe(metadata.url)
96 expect(attachment.mimeType).toBe('image/jpeg')
97 expect(attachment.metadata?.width).toBe(1920)
98 expect(attachment.metadata?.height).toBe(1080)
99 expect(attachment.alt).toBe('A beautiful sunset')
100 })
101 })
102
103 describe('pending', () => {
104 it('creates pending attachment', () => {
105 const attachment = MediaAttachment.pending('photo.jpg', 'image')
106
107 expect(attachment.url).toBe('')
108 expect(attachment.type).toBe('image')
109 expect(attachment.status).toBe('pending')
110 expect(attachment.isUploaded).toBe(false)
111 })
112 })
113
114 describe('toImetaTag', () => {
115 it('generates imeta tag for images', () => {
116 const attachment = MediaAttachment.fromMetadata({
117 url: 'https://example.com/photo.jpg',
118 mimeType: 'image/jpeg',
119 width: 800,
120 height: 600,
121 blurhash: 'LGF5?xYk^6#M@-5c,1J5@[or[Q6.'
122 })
123
124 const tag = attachment.toImetaTag()
125
126 expect(tag).not.toBeNull()
127 expect(tag![0]).toBe('imeta')
128 expect(tag).toContain('url https://example.com/photo.jpg')
129 expect(tag).toContain('m image/jpeg')
130 expect(tag).toContain('dim 800x600')
131 expect(tag).toContain('blurhash LGF5?xYk^6#M@-5c,1J5@[or[Q6.')
132 })
133
134 it('returns null for non-images', () => {
135 const attachment = MediaAttachment.fromUrl('https://example.com/video.mp4')
136
137 const tag = attachment.toImetaTag()
138
139 expect(tag).toBeNull()
140 })
141
142 it('includes alt text in tag', () => {
143 const attachment = MediaAttachment.fromUrl('https://example.com/photo.jpg').withAlt(
144 'Description'
145 )
146
147 const tag = attachment.toImetaTag()
148
149 expect(tag).toContain('alt Description')
150 })
151 })
152
153 describe('immutable modifications', () => {
154 it('withAlt returns new instance', () => {
155 const original = MediaAttachment.fromUrl('https://example.com/photo.jpg')
156 const modified = original.withAlt('New alt text')
157
158 expect(original.alt).toBeNull()
159 expect(modified.alt).toBe('New alt text')
160 })
161
162 it('withStatus returns new instance', () => {
163 const original = MediaAttachment.pending('file.jpg', 'image')
164 const modified = original.withStatus('uploading')
165
166 expect(original.status).toBe('pending')
167 expect(modified.status).toBe('uploading')
168 })
169
170 it('withUrl returns new instance with completed status', () => {
171 const original = MediaAttachment.pending('file.jpg', 'image')
172 const modified = original.withUrl('https://example.com/uploaded.jpg')
173
174 expect(original.url).toBe('')
175 expect(original.status).toBe('pending')
176 expect(modified.url).toBe('https://example.com/uploaded.jpg')
177 expect(modified.status).toBe('completed')
178 expect(modified.isUploaded).toBe(true)
179 })
180 })
181
182 describe('equals', () => {
183 it('returns true for same URL', () => {
184 const a = MediaAttachment.fromUrl('https://example.com/photo.jpg')
185 const b = MediaAttachment.fromUrl('https://example.com/photo.jpg')
186
187 expect(a.equals(b)).toBe(true)
188 })
189
190 it('returns false for different URLs', () => {
191 const a = MediaAttachment.fromUrl('https://example.com/photo1.jpg')
192 const b = MediaAttachment.fromUrl('https://example.com/photo2.jpg')
193
194 expect(a.equals(b)).toBe(false)
195 })
196 })
197 })
198