|
| 1 | +import { |
| 2 | + stripMarkdown, |
| 3 | + truncateText, |
| 4 | + formatRelativeTime, |
| 5 | +} from '../../../extensions/conversations/external/components/utils' |
| 6 | + |
| 7 | +describe('conversations utils', () => { |
| 8 | + describe('stripMarkdown', () => { |
| 9 | + it('should return empty string for undefined input', () => { |
| 10 | + expect(stripMarkdown(undefined)).toBe('') |
| 11 | + }) |
| 12 | + |
| 13 | + it('should return empty string for empty string input', () => { |
| 14 | + expect(stripMarkdown('')).toBe('') |
| 15 | + }) |
| 16 | + |
| 17 | + it('should return plain text unchanged', () => { |
| 18 | + expect(stripMarkdown('Hello world')).toBe('Hello world') |
| 19 | + }) |
| 20 | + |
| 21 | + describe('headers', () => { |
| 22 | + it('should strip h1 headers', () => { |
| 23 | + expect(stripMarkdown('# Header 1')).toBe('Header 1') |
| 24 | + }) |
| 25 | + |
| 26 | + it('should strip h2 headers', () => { |
| 27 | + expect(stripMarkdown('## Header 2')).toBe('Header 2') |
| 28 | + }) |
| 29 | + |
| 30 | + it('should strip h6 headers', () => { |
| 31 | + expect(stripMarkdown('###### Header 6')).toBe('Header 6') |
| 32 | + }) |
| 33 | + |
| 34 | + it('should strip headers with text after', () => { |
| 35 | + expect(stripMarkdown('# Title\nSome content')).toBe('Title\nSome content') |
| 36 | + }) |
| 37 | + }) |
| 38 | + |
| 39 | + describe('bold and italic', () => { |
| 40 | + it('should strip bold with asterisks', () => { |
| 41 | + expect(stripMarkdown('This is **bold** text')).toBe('This is bold text') |
| 42 | + }) |
| 43 | + |
| 44 | + it('should strip bold with underscores', () => { |
| 45 | + expect(stripMarkdown('This is __bold__ text')).toBe('This is bold text') |
| 46 | + }) |
| 47 | + |
| 48 | + it('should strip italic with asterisks', () => { |
| 49 | + expect(stripMarkdown('This is *italic* text')).toBe('This is italic text') |
| 50 | + }) |
| 51 | + |
| 52 | + it('should strip italic with underscores', () => { |
| 53 | + expect(stripMarkdown('This is _italic_ text')).toBe('This is italic text') |
| 54 | + }) |
| 55 | + |
| 56 | + it('should strip nested bold and italic', () => { |
| 57 | + expect(stripMarkdown('This is ***bold and italic*** text')).toBe('This is bold and italic text') |
| 58 | + }) |
| 59 | + }) |
| 60 | + |
| 61 | + describe('strikethrough', () => { |
| 62 | + it('should strip strikethrough', () => { |
| 63 | + expect(stripMarkdown('This is ~~deleted~~ text')).toBe('This is deleted text') |
| 64 | + }) |
| 65 | + }) |
| 66 | + |
| 67 | + describe('links', () => { |
| 68 | + it('should convert links to just text', () => { |
| 69 | + expect(stripMarkdown('Check [this link](https://example.com)')).toBe('Check this link') |
| 70 | + }) |
| 71 | + |
| 72 | + it('should handle links with complex URLs', () => { |
| 73 | + expect(stripMarkdown('[Click here](https://example.com/path?query=1&foo=bar)')).toBe('Click here') |
| 74 | + }) |
| 75 | + |
| 76 | + it('should handle multiple links', () => { |
| 77 | + expect(stripMarkdown('[Link 1](url1) and [Link 2](url2)')).toBe('Link 1 and Link 2') |
| 78 | + }) |
| 79 | + }) |
| 80 | + |
| 81 | + describe('images', () => { |
| 82 | + it('should remove images completely', () => { |
| 83 | + expect(stripMarkdown('Text  more text')).toBe('Text more text') |
| 84 | + }) |
| 85 | + |
| 86 | + it('should remove images with empty alt text', () => { |
| 87 | + expect(stripMarkdown('')).toBe('') |
| 88 | + }) |
| 89 | + }) |
| 90 | + |
| 91 | + describe('code', () => { |
| 92 | + it('should strip inline code backticks', () => { |
| 93 | + expect(stripMarkdown('Use `console.log()` to debug')).toBe('Use console.log() to debug') |
| 94 | + }) |
| 95 | + |
| 96 | + it('should remove code blocks', () => { |
| 97 | + expect(stripMarkdown('```javascript\nconst x = 1;\n```')).toBe('') |
| 98 | + }) |
| 99 | + |
| 100 | + it('should remove code blocks with content around', () => { |
| 101 | + expect(stripMarkdown('Before\n```\ncode\n```\nAfter')).toBe('Before\nAfter') |
| 102 | + }) |
| 103 | + }) |
| 104 | + |
| 105 | + describe('blockquotes', () => { |
| 106 | + it('should strip blockquote markers', () => { |
| 107 | + expect(stripMarkdown('> This is a quote')).toBe('This is a quote') |
| 108 | + }) |
| 109 | + |
| 110 | + it('should handle nested blockquotes', () => { |
| 111 | + // Each > at start of line is stripped, so "> >" becomes " " (space remains from second >) |
| 112 | + expect(stripMarkdown('> First level\n> > Nested')).toBe('First level\n Nested') |
| 113 | + }) |
| 114 | + }) |
| 115 | + |
| 116 | + describe('horizontal rules', () => { |
| 117 | + it('should remove horizontal rules with dashes', () => { |
| 118 | + expect(stripMarkdown('Above\n---\nBelow')).toBe('Above\nBelow') |
| 119 | + }) |
| 120 | + |
| 121 | + it('should remove horizontal rules with asterisks', () => { |
| 122 | + expect(stripMarkdown('Above\n***\nBelow')).toBe('Above\nBelow') |
| 123 | + }) |
| 124 | + |
| 125 | + it('should remove horizontal rules with underscores', () => { |
| 126 | + expect(stripMarkdown('Above\n___\nBelow')).toBe('Above\nBelow') |
| 127 | + }) |
| 128 | + }) |
| 129 | + |
| 130 | + describe('lists', () => { |
| 131 | + it('should strip unordered list markers with dash', () => { |
| 132 | + expect(stripMarkdown('- Item 1\n- Item 2')).toBe('Item 1\nItem 2') |
| 133 | + }) |
| 134 | + |
| 135 | + it('should strip unordered list markers with asterisk', () => { |
| 136 | + expect(stripMarkdown('* Item 1\n* Item 2')).toBe('Item 1\nItem 2') |
| 137 | + }) |
| 138 | + |
| 139 | + it('should strip unordered list markers with plus', () => { |
| 140 | + expect(stripMarkdown('+ Item 1\n+ Item 2')).toBe('Item 1\nItem 2') |
| 141 | + }) |
| 142 | + |
| 143 | + it('should strip ordered list markers', () => { |
| 144 | + expect(stripMarkdown('1. First\n2. Second\n3. Third')).toBe('First\nSecond\nThird') |
| 145 | + }) |
| 146 | + |
| 147 | + it('should handle indented list items', () => { |
| 148 | + expect(stripMarkdown(' - Nested item')).toBe('Nested item') |
| 149 | + }) |
| 150 | + }) |
| 151 | + |
| 152 | + describe('HTML tags', () => { |
| 153 | + it('should remove HTML tags', () => { |
| 154 | + expect(stripMarkdown('Text with <strong>HTML</strong> tags')).toBe('Text with HTML tags') |
| 155 | + }) |
| 156 | + |
| 157 | + it('should remove self-closing tags', () => { |
| 158 | + expect(stripMarkdown('Line<br/>break')).toBe('Linebreak') |
| 159 | + }) |
| 160 | + |
| 161 | + it('should remove tags with attributes', () => { |
| 162 | + expect(stripMarkdown('<a href="url">Link</a>')).toBe('Link') |
| 163 | + }) |
| 164 | + |
| 165 | + it('should remove incomplete/partial tags for security', () => { |
| 166 | + expect(stripMarkdown('<script')).toBe('script') |
| 167 | + expect(stripMarkdown('text<script>alert(1)')).toBe('textalert(1)') |
| 168 | + }) |
| 169 | + |
| 170 | + it('should remove lone angle brackets', () => { |
| 171 | + // '< b >' is treated as a tag and removed entirely |
| 172 | + expect(stripMarkdown('a < b > c')).toBe('a c') |
| 173 | + // Lone < without matching > is stripped |
| 174 | + expect(stripMarkdown('a < b')).toBe('a b') |
| 175 | + // Lone > without matching < is stripped |
| 176 | + expect(stripMarkdown('a > b')).toBe('a b') |
| 177 | + }) |
| 178 | + }) |
| 179 | + |
| 180 | + describe('whitespace handling', () => { |
| 181 | + it('should collapse multiple newlines', () => { |
| 182 | + expect(stripMarkdown('Line 1\n\n\n\nLine 2')).toBe('Line 1\nLine 2') |
| 183 | + }) |
| 184 | + |
| 185 | + it('should trim leading and trailing whitespace', () => { |
| 186 | + expect(stripMarkdown(' text with spaces ')).toBe('text with spaces') |
| 187 | + }) |
| 188 | + }) |
| 189 | + |
| 190 | + describe('combined markdown', () => { |
| 191 | + it('should handle complex markdown', () => { |
| 192 | + const markdown = `# Welcome |
| 193 | +
|
| 194 | +This is **bold** and *italic* text with a [link](https://example.com). |
| 195 | +
|
| 196 | +- Item 1 |
| 197 | +- Item 2 |
| 198 | +
|
| 199 | +\`\`\` |
| 200 | +code block |
| 201 | +\`\`\` |
| 202 | +
|
| 203 | +> A quote |
| 204 | +
|
| 205 | +Done!` |
| 206 | + |
| 207 | + const expected = `Welcome |
| 208 | +This is bold and italic text with a link. |
| 209 | +Item 1 |
| 210 | +Item 2 |
| 211 | +A quote |
| 212 | +Done!` |
| 213 | + |
| 214 | + expect(stripMarkdown(markdown)).toBe(expected) |
| 215 | + }) |
| 216 | + |
| 217 | + it('should handle message-like content', () => { |
| 218 | + const markdown = 'Hey! Check out this **new feature** at [our docs](https://docs.example.com) 🎉' |
| 219 | + expect(stripMarkdown(markdown)).toBe('Hey! Check out this new feature at our docs 🎉') |
| 220 | + }) |
| 221 | + }) |
| 222 | + }) |
| 223 | + |
| 224 | + describe('truncateText', () => { |
| 225 | + it('should return "No messages yet" for undefined input', () => { |
| 226 | + expect(truncateText(undefined, 60)).toBe('No messages yet') |
| 227 | + }) |
| 228 | + |
| 229 | + it('should return "No messages yet" for empty string', () => { |
| 230 | + expect(truncateText('', 60)).toBe('No messages yet') |
| 231 | + }) |
| 232 | + |
| 233 | + it('should return text unchanged if shorter than max length', () => { |
| 234 | + expect(truncateText('Short text', 60)).toBe('Short text') |
| 235 | + }) |
| 236 | + |
| 237 | + it('should return text unchanged if equal to max length', () => { |
| 238 | + const text = 'a'.repeat(60) |
| 239 | + expect(truncateText(text, 60)).toBe(text) |
| 240 | + }) |
| 241 | + |
| 242 | + it('should truncate text longer than max length with ellipsis', () => { |
| 243 | + const text = 'a'.repeat(70) |
| 244 | + const result = truncateText(text, 60) |
| 245 | + expect(result.length).toBe(60) |
| 246 | + expect(result.endsWith('...')).toBe(true) |
| 247 | + }) |
| 248 | + }) |
| 249 | + |
| 250 | + describe('formatRelativeTime', () => { |
| 251 | + beforeEach(() => { |
| 252 | + jest.useFakeTimers() |
| 253 | + jest.setSystemTime(new Date('2024-01-15T12:00:00Z')) |
| 254 | + }) |
| 255 | + |
| 256 | + afterEach(() => { |
| 257 | + jest.useRealTimers() |
| 258 | + }) |
| 259 | + |
| 260 | + it('should return empty string for undefined input', () => { |
| 261 | + expect(formatRelativeTime(undefined)).toBe('') |
| 262 | + }) |
| 263 | + |
| 264 | + it('should return "Just now" for times less than a minute ago', () => { |
| 265 | + const now = new Date('2024-01-15T11:59:30Z').toISOString() |
| 266 | + expect(formatRelativeTime(now)).toBe('Just now') |
| 267 | + }) |
| 268 | + |
| 269 | + it('should return minutes ago for times less than an hour ago', () => { |
| 270 | + const thirtyMinsAgo = new Date('2024-01-15T11:30:00Z').toISOString() |
| 271 | + expect(formatRelativeTime(thirtyMinsAgo)).toBe('30m ago') |
| 272 | + }) |
| 273 | + |
| 274 | + it('should return hours ago for times less than a day ago', () => { |
| 275 | + const fiveHoursAgo = new Date('2024-01-15T07:00:00Z').toISOString() |
| 276 | + expect(formatRelativeTime(fiveHoursAgo)).toBe('5h ago') |
| 277 | + }) |
| 278 | + |
| 279 | + it('should return "Yesterday" for times one day ago', () => { |
| 280 | + const yesterday = new Date('2024-01-14T12:00:00Z').toISOString() |
| 281 | + expect(formatRelativeTime(yesterday)).toBe('Yesterday') |
| 282 | + }) |
| 283 | + |
| 284 | + it('should return days ago for times less than a week ago', () => { |
| 285 | + const threeDaysAgo = new Date('2024-01-12T12:00:00Z').toISOString() |
| 286 | + expect(formatRelativeTime(threeDaysAgo)).toBe('3d ago') |
| 287 | + }) |
| 288 | + |
| 289 | + it('should return formatted date for times a week or more ago', () => { |
| 290 | + const twoWeeksAgo = new Date('2024-01-01T12:00:00Z').toISOString() |
| 291 | + const result = formatRelativeTime(twoWeeksAgo) |
| 292 | + expect(result).toMatch(/\d{1,2}\/\d{1,2}\/\d{4}/) |
| 293 | + }) |
| 294 | + }) |
| 295 | +}) |
0 commit comments