Full document, spreadsheet, slideshow, and diagram tooling
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

test: add 57 tests for slash-menu module (batch 21)

New test file for the previously untested slash-menu pure logic module:
- Constants validation (categories, items, uniqueness, completeness)
- filterCommands: query matching by name, description, category
- findCommandById: lookup, null cases
- getCategoryItems: filtering, coverage
- SlashMenuState: open/close, query, navigation, grouping
- Edge cases: regex chars, unicode, round-trip navigation

Closes #442

+426
+426
tests/slash-menu.test.ts
··· 1 + import { describe, it, expect, beforeEach } from 'vitest'; 2 + import { 3 + PLACEHOLDER_EMPTY, 4 + PLACEHOLDER_BLOCK, 5 + SLASH_COMMAND_CATEGORIES, 6 + SLASH_COMMAND_ITEMS, 7 + filterCommands, 8 + findCommandById, 9 + getCategoryItems, 10 + SlashMenuState, 11 + } from '../src/docs/slash-menu.js'; 12 + 13 + // ===================================================================== 14 + // Constants 15 + // ===================================================================== 16 + 17 + describe('slash-menu constants', () => { 18 + it('exports non-empty placeholder strings', () => { 19 + expect(PLACEHOLDER_EMPTY.length).toBeGreaterThan(0); 20 + expect(PLACEHOLDER_BLOCK.length).toBeGreaterThan(0); 21 + }); 22 + 23 + it('placeholders mention slash commands', () => { 24 + expect(PLACEHOLDER_EMPTY).toContain('/'); 25 + expect(PLACEHOLDER_BLOCK).toContain('/'); 26 + }); 27 + 28 + it('has at least 5 categories', () => { 29 + expect(SLASH_COMMAND_CATEGORIES.length).toBeGreaterThanOrEqual(5); 30 + }); 31 + 32 + it('all categories have unique ids', () => { 33 + const ids = SLASH_COMMAND_CATEGORIES.map(c => c.id); 34 + expect(new Set(ids).size).toBe(ids.length); 35 + }); 36 + 37 + it('all categories have labels', () => { 38 + for (const cat of SLASH_COMMAND_CATEGORIES) { 39 + expect(cat.label.length).toBeGreaterThan(0); 40 + } 41 + }); 42 + 43 + it('has at least 15 command items', () => { 44 + expect(SLASH_COMMAND_ITEMS.length).toBeGreaterThanOrEqual(15); 45 + }); 46 + 47 + it('all items have unique ids', () => { 48 + const ids = SLASH_COMMAND_ITEMS.map(i => i.id); 49 + expect(new Set(ids).size).toBe(ids.length); 50 + }); 51 + 52 + it('all items reference a valid category', () => { 53 + const catIds = new Set(SLASH_COMMAND_CATEGORIES.map(c => c.id)); 54 + for (const item of SLASH_COMMAND_ITEMS) { 55 + expect(catIds.has(item.category)).toBe(true); 56 + } 57 + }); 58 + 59 + it('all items have name, description, icon', () => { 60 + for (const item of SLASH_COMMAND_ITEMS) { 61 + expect(item.name.length).toBeGreaterThan(0); 62 + expect(item.description.length).toBeGreaterThan(0); 63 + expect(item.icon.length).toBeGreaterThan(0); 64 + } 65 + }); 66 + 67 + it('every category has at least one item', () => { 68 + for (const cat of SLASH_COMMAND_CATEGORIES) { 69 + const items = SLASH_COMMAND_ITEMS.filter(i => i.category === cat.id); 70 + expect(items.length).toBeGreaterThan(0); 71 + } 72 + }); 73 + }); 74 + 75 + // ===================================================================== 76 + // filterCommands 77 + // ===================================================================== 78 + 79 + describe('filterCommands', () => { 80 + it('returns all items for empty query', () => { 81 + expect(filterCommands('')).toEqual(SLASH_COMMAND_ITEMS); 82 + }); 83 + 84 + it('returns all items for whitespace query', () => { 85 + expect(filterCommands(' ')).toEqual(SLASH_COMMAND_ITEMS); 86 + }); 87 + 88 + it('filters by name (case insensitive)', () => { 89 + const result = filterCommands('heading'); 90 + expect(result.length).toBeGreaterThanOrEqual(3); 91 + // All results should mention 'heading' in name OR description OR category 92 + for (const r of result) { 93 + const matches = r.name.toLowerCase().includes('heading') || 94 + r.description.toLowerCase().includes('heading'); 95 + expect(matches).toBe(true); 96 + } 97 + }); 98 + 99 + it('filters by description', () => { 100 + const result = filterCommands('checklist'); 101 + expect(result.length).toBe(1); 102 + expect(result[0].id).toBe('taskList'); 103 + }); 104 + 105 + it('filters by category label', () => { 106 + const result = filterCommands('lists'); 107 + expect(result.length).toBeGreaterThanOrEqual(3); 108 + }); 109 + 110 + it('returns empty for no match', () => { 111 + expect(filterCommands('zzzznotfound')).toEqual([]); 112 + }); 113 + 114 + it('is case insensitive', () => { 115 + const lower = filterCommands('code'); 116 + const upper = filterCommands('CODE'); 117 + expect(lower).toEqual(upper); 118 + }); 119 + 120 + it('handles partial word match', () => { 121 + const result = filterCommands('para'); 122 + expect(result.some(r => r.id === 'paragraph')).toBe(true); 123 + }); 124 + 125 + it('matches across description keywords', () => { 126 + const result = filterCommands('syntax'); 127 + expect(result.some(r => r.id === 'codeBlock')).toBe(true); 128 + }); 129 + 130 + it('preserves original order of items', () => { 131 + const result = filterCommands(''); 132 + expect(result.map(r => r.id)).toEqual(SLASH_COMMAND_ITEMS.map(i => i.id)); 133 + }); 134 + }); 135 + 136 + // ===================================================================== 137 + // findCommandById 138 + // ===================================================================== 139 + 140 + describe('findCommandById', () => { 141 + it('finds existing command', () => { 142 + const cmd = findCommandById('paragraph'); 143 + expect(cmd).not.toBeNull(); 144 + expect(cmd!.id).toBe('paragraph'); 145 + expect(cmd!.name).toBe('Paragraph'); 146 + }); 147 + 148 + it('returns null for unknown id', () => { 149 + expect(findCommandById('nonexistent')).toBeNull(); 150 + }); 151 + 152 + it('returns null for empty string', () => { 153 + expect(findCommandById('')).toBeNull(); 154 + }); 155 + 156 + it('finds all known items', () => { 157 + for (const item of SLASH_COMMAND_ITEMS) { 158 + expect(findCommandById(item.id)).not.toBeNull(); 159 + } 160 + }); 161 + 162 + it('returns full item with all properties', () => { 163 + const cmd = findCommandById('heading1'); 164 + expect(cmd).toMatchObject({ 165 + id: 'heading1', 166 + name: 'Heading 1', 167 + category: 'text', 168 + icon: 'H1', 169 + shortcut: 'Mod+Alt+1', 170 + }); 171 + }); 172 + }); 173 + 174 + // ===================================================================== 175 + // getCategoryItems 176 + // ===================================================================== 177 + 178 + describe('getCategoryItems', () => { 179 + it('returns items for text category', () => { 180 + const items = getCategoryItems('text'); 181 + expect(items.length).toBeGreaterThan(0); 182 + expect(items.every(i => i.category === 'text')).toBe(true); 183 + }); 184 + 185 + it('returns items for lists category', () => { 186 + const items = getCategoryItems('lists'); 187 + expect(items.length).toBeGreaterThan(0); 188 + expect(items.every(i => i.category === 'lists')).toBe(true); 189 + }); 190 + 191 + it('returns empty for unknown category', () => { 192 + expect(getCategoryItems('nonexistent')).toEqual([]); 193 + }); 194 + 195 + it('returns empty for empty string', () => { 196 + expect(getCategoryItems('')).toEqual([]); 197 + }); 198 + 199 + it('covers all items when iterating all categories', () => { 200 + let total = 0; 201 + for (const cat of SLASH_COMMAND_CATEGORIES) { 202 + total += getCategoryItems(cat.id).length; 203 + } 204 + expect(total).toBe(SLASH_COMMAND_ITEMS.length); 205 + }); 206 + }); 207 + 208 + // ===================================================================== 209 + // SlashMenuState 210 + // ===================================================================== 211 + 212 + describe('SlashMenuState', () => { 213 + let state: SlashMenuState; 214 + 215 + beforeEach(() => { 216 + state = new SlashMenuState(); 217 + }); 218 + 219 + it('starts closed with empty query', () => { 220 + expect(state.isOpen).toBe(false); 221 + expect(state.query).toBe(''); 222 + expect(state.selectedIndex).toBe(0); 223 + }); 224 + 225 + it('open() sets isOpen and resets state', () => { 226 + state.query = 'test'; 227 + state.selectedIndex = 5; 228 + state.open(); 229 + expect(state.isOpen).toBe(true); 230 + expect(state.query).toBe(''); 231 + expect(state.selectedIndex).toBe(0); 232 + }); 233 + 234 + it('close() resets everything', () => { 235 + state.open(); 236 + state.setQuery('heading'); 237 + state.selectedIndex = 3; 238 + state.close(); 239 + expect(state.isOpen).toBe(false); 240 + expect(state.query).toBe(''); 241 + expect(state.selectedIndex).toBe(0); 242 + }); 243 + 244 + it('setQuery updates query and resets selectedIndex', () => { 245 + state.selectedIndex = 5; 246 + state.setQuery('code'); 247 + expect(state.query).toBe('code'); 248 + expect(state.selectedIndex).toBe(0); 249 + }); 250 + 251 + it('getFilteredItems returns all items for empty query', () => { 252 + const items = state.getFilteredItems(); 253 + expect(items).toEqual(SLASH_COMMAND_ITEMS); 254 + }); 255 + 256 + it('getFilteredItems filters when query is set', () => { 257 + state.setQuery('heading'); 258 + const items = state.getFilteredItems(); 259 + expect(items.length).toBeGreaterThan(0); 260 + expect(items.length).toBeLessThan(SLASH_COMMAND_ITEMS.length); 261 + }); 262 + 263 + it('moveDown increments selectedIndex', () => { 264 + state.open(); 265 + expect(state.selectedIndex).toBe(0); 266 + state.moveDown(); 267 + expect(state.selectedIndex).toBe(1); 268 + state.moveDown(); 269 + expect(state.selectedIndex).toBe(2); 270 + }); 271 + 272 + it('moveDown wraps at end', () => { 273 + state.open(); 274 + const total = state.getFilteredItems().length; 275 + state.selectedIndex = total - 1; 276 + state.moveDown(); 277 + expect(state.selectedIndex).toBe(0); 278 + }); 279 + 280 + it('moveUp decrements selectedIndex', () => { 281 + state.open(); 282 + state.selectedIndex = 3; 283 + state.moveUp(); 284 + expect(state.selectedIndex).toBe(2); 285 + }); 286 + 287 + it('moveUp wraps at start', () => { 288 + state.open(); 289 + expect(state.selectedIndex).toBe(0); 290 + state.moveUp(); 291 + const total = state.getFilteredItems().length; 292 + expect(state.selectedIndex).toBe(total - 1); 293 + }); 294 + 295 + it('moveDown/moveUp are no-ops on empty results', () => { 296 + state.setQuery('zzzznotfound'); 297 + state.moveDown(); 298 + expect(state.selectedIndex).toBe(0); 299 + state.moveUp(); 300 + expect(state.selectedIndex).toBe(0); 301 + }); 302 + 303 + it('getSelectedItem returns null when closed', () => { 304 + expect(state.getSelectedItem()).toBeNull(); 305 + }); 306 + 307 + it('getSelectedItem returns first item when open', () => { 308 + state.open(); 309 + const item = state.getSelectedItem(); 310 + expect(item).not.toBeNull(); 311 + expect(item!.id).toBe(SLASH_COMMAND_ITEMS[0].id); 312 + }); 313 + 314 + it('getSelectedItem tracks moveDown', () => { 315 + state.open(); 316 + state.moveDown(); 317 + const item = state.getSelectedItem(); 318 + expect(item!.id).toBe(SLASH_COMMAND_ITEMS[1].id); 319 + }); 320 + 321 + it('getSelectedItem returns null for no matches', () => { 322 + state.open(); 323 + state.setQuery('zzzznotfound'); 324 + expect(state.getSelectedItem()).toBeNull(); 325 + }); 326 + 327 + it('getGroupedItems returns groups in category order', () => { 328 + state.open(); 329 + const groups = state.getGroupedItems(); 330 + expect(groups.length).toBeGreaterThan(0); 331 + // Verify groups are in SLASH_COMMAND_CATEGORIES order 332 + const catOrder = SLASH_COMMAND_CATEGORIES.map(c => c.id); 333 + const groupOrder = groups.map(g => g.id); 334 + let lastIdx = -1; 335 + for (const gid of groupOrder) { 336 + const idx = catOrder.indexOf(gid); 337 + expect(idx).toBeGreaterThan(lastIdx); 338 + lastIdx = idx; 339 + } 340 + }); 341 + 342 + it('getGroupedItems omits empty categories', () => { 343 + state.open(); 344 + state.setQuery('blockquote'); 345 + const groups = state.getGroupedItems(); 346 + // Only 'quote' category should have matches for 'blockquote' 347 + expect(groups.length).toBe(1); 348 + expect(groups[0].id).toBe('quote'); 349 + }); 350 + 351 + it('getGroupedItems returns empty for no matches', () => { 352 + state.open(); 353 + state.setQuery('zzzznotfound'); 354 + expect(state.getGroupedItems()).toEqual([]); 355 + }); 356 + 357 + it('getGroupedItems groups have correct labels', () => { 358 + state.open(); 359 + const groups = state.getGroupedItems(); 360 + for (const group of groups) { 361 + const cat = SLASH_COMMAND_CATEGORIES.find(c => c.id === group.id); 362 + expect(cat).toBeDefined(); 363 + expect(group.label).toBe(cat!.label); 364 + } 365 + }); 366 + }); 367 + 368 + // ===================================================================== 369 + // Edge cases 370 + // ===================================================================== 371 + 372 + describe('slash-menu — edge cases', () => { 373 + it('filterCommands with special regex characters', () => { 374 + const result = filterCommands('('); 375 + // Should not throw, may return empty 376 + expect(Array.isArray(result)).toBe(true); 377 + }); 378 + 379 + it('filterCommands with unicode query', () => { 380 + const result = filterCommands('¶'); 381 + expect(Array.isArray(result)).toBe(true); 382 + }); 383 + 384 + it('filterCommands returns a new array each time', () => { 385 + const a = filterCommands(''); 386 + const b = filterCommands(''); 387 + expect(a).not.toBe(b); 388 + expect(a).toEqual(b); 389 + }); 390 + 391 + it('SlashMenuState navigation round-trip', () => { 392 + const state = new SlashMenuState(); 393 + state.open(); 394 + const initial = state.selectedIndex; 395 + state.moveDown(); 396 + state.moveUp(); 397 + expect(state.selectedIndex).toBe(initial); 398 + }); 399 + 400 + it('SlashMenuState navigation across filtered results', () => { 401 + const state = new SlashMenuState(); 402 + state.open(); 403 + state.setQuery('list'); 404 + const count = state.getFilteredItems().length; 405 + // Navigate through all items and back 406 + for (let i = 0; i < count; i++) state.moveDown(); 407 + expect(state.selectedIndex).toBe(0); // wrapped 408 + }); 409 + 410 + it('shortcut is null for items without shortcuts', () => { 411 + const cmd = findCommandById('paragraph'); 412 + expect(cmd!.shortcut).toBeNull(); 413 + }); 414 + 415 + it('shortcut is a string for items with shortcuts', () => { 416 + const cmd = findCommandById('heading1'); 417 + expect(typeof cmd!.shortcut).toBe('string'); 418 + expect(cmd!.shortcut!.length).toBeGreaterThan(0); 419 + }); 420 + 421 + it('getCategoryItems preserves order from SLASH_COMMAND_ITEMS', () => { 422 + const textItems = getCategoryItems('text'); 423 + const textFromAll = SLASH_COMMAND_ITEMS.filter(i => i.category === 'text'); 424 + expect(textItems).toEqual(textFromAll); 425 + }); 426 + });