[READ-ONLY] a fast, modern browser for the npm registry
0
fork

Configure Feed

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

at main 434 lines 14 kB view raw
1import { beforeEach, describe, expect, it, vi } from 'vitest' 2import { mountSuspended } from '@nuxt/test-utils/runtime' 3import { ref } from 'vue' 4import type { ComparisonFacet } from '#shared/types/comparison' 5import { 6 DEFAULT_FACETS, 7 FACET_INFO, 8 FACETS_BY_CATEGORY, 9 hasComingSoonFacets, 10} from '#shared/types/comparison' 11import type { FacetInfoWithLabels } from '~/composables/useFacetSelection' 12 13// Mock useRouteQuery - needs to be outside of the helper to persist across calls 14const mockRouteQuery = ref('') 15vi.mock('@vueuse/router', () => ({ 16 useRouteQuery: () => mockRouteQuery, 17})) 18 19/** 20 * Helper to test useFacetSelection by wrapping it in a component. 21 * This is required because the composable uses useI18n which must be 22 * called inside a Vue component's setup function. 23 */ 24async function useFacetSelectionInComponent() { 25 // Create refs to capture the composable's return values 26 const capturedSelectedFacets = shallowRef<FacetInfoWithLabels[]>([]) 27 const capturedIsAllSelected = ref(false) 28 const capturedIsNoneSelected = ref(false) 29 let capturedIsFacetSelected: (facet: ComparisonFacet) => boolean 30 let capturedToggleFacet: (facet: ComparisonFacet) => void 31 let capturedSelectCategory: (category: string) => void 32 let capturedDeselectCategory: (category: string) => void 33 let capturedSelectAll: () => void 34 let capturedDeselectAll: () => void 35 let capturedAllFacets: ComparisonFacet[] 36 37 const WrapperComponent = defineComponent({ 38 setup() { 39 const { 40 selectedFacets, 41 isFacetSelected, 42 toggleFacet, 43 selectCategory, 44 deselectCategory, 45 selectAll, 46 deselectAll, 47 isAllSelected, 48 isNoneSelected, 49 allFacets, 50 } = useFacetSelection() 51 52 // Sync values to captured refs 53 watchEffect(() => { 54 capturedSelectedFacets.value = [...selectedFacets.value] 55 capturedIsAllSelected.value = isAllSelected.value 56 capturedIsNoneSelected.value = isNoneSelected.value 57 }) 58 59 capturedIsFacetSelected = isFacetSelected 60 capturedToggleFacet = toggleFacet 61 capturedSelectCategory = selectCategory 62 capturedDeselectCategory = deselectCategory 63 capturedSelectAll = selectAll 64 capturedDeselectAll = deselectAll 65 capturedAllFacets = allFacets 66 67 return () => h('div') 68 }, 69 }) 70 71 await mountSuspended(WrapperComponent) 72 73 return { 74 selectedFacets: capturedSelectedFacets, 75 isFacetSelected: (facet: ComparisonFacet) => capturedIsFacetSelected(facet), 76 toggleFacet: (facet: ComparisonFacet) => capturedToggleFacet(facet), 77 selectCategory: (category: string) => capturedSelectCategory(category), 78 deselectCategory: (category: string) => capturedDeselectCategory(category), 79 selectAll: () => capturedSelectAll(), 80 deselectAll: () => capturedDeselectAll(), 81 isAllSelected: capturedIsAllSelected, 82 isNoneSelected: capturedIsNoneSelected, 83 allFacets: capturedAllFacets!, 84 } 85} 86 87describe('useFacetSelection', () => { 88 beforeEach(() => { 89 mockRouteQuery.value = '' 90 }) 91 92 it('returns DEFAULT_FACETS when no query param', async () => { 93 const { isFacetSelected } = await useFacetSelectionInComponent() 94 95 // All default facets should be selected 96 for (const facet of DEFAULT_FACETS) { 97 expect(isFacetSelected(facet)).toBe(true) 98 } 99 }) 100 101 it('parses facets from query param', async () => { 102 mockRouteQuery.value = 'downloads,types,license' 103 104 const { isFacetSelected } = await useFacetSelectionInComponent() 105 106 expect(isFacetSelected('downloads')).toBe(true) 107 expect(isFacetSelected('types')).toBe(true) 108 expect(isFacetSelected('license')).toBe(true) 109 expect(isFacetSelected('packageSize')).toBe(false) 110 }) 111 112 it('filters out invalid facets from query', async () => { 113 mockRouteQuery.value = 'downloads,invalidFacet,types' 114 115 const { isFacetSelected } = await useFacetSelectionInComponent() 116 117 expect(isFacetSelected('downloads')).toBe(true) 118 expect(isFacetSelected('types')).toBe(true) 119 }) 120 121 it.runIf(hasComingSoonFacets)('filters out comingSoon facets from query', async () => { 122 mockRouteQuery.value = 'downloads,totalDependencies,types' 123 124 const { isFacetSelected } = await useFacetSelectionInComponent() 125 126 expect(isFacetSelected('downloads')).toBe(true) 127 expect(isFacetSelected('types')).toBe(true) 128 expect(isFacetSelected('totalDependencies')).toBe(false) 129 }) 130 131 it('falls back to DEFAULT_FACETS if all parsed facets are invalid', async () => { 132 mockRouteQuery.value = 'invalidFacet1,invalidFacet2' 133 134 const { isFacetSelected } = await useFacetSelectionInComponent() 135 136 // All default facets should be selected when query is invalid 137 for (const facet of DEFAULT_FACETS) { 138 expect(isFacetSelected(facet)).toBe(true) 139 } 140 }) 141 142 describe('selectedFacets enriched data', () => { 143 it('includes label and description for each facet', async () => { 144 mockRouteQuery.value = 'downloads,types' 145 146 const { selectedFacets } = await useFacetSelectionInComponent() 147 148 for (const facet of selectedFacets.value) { 149 expect(facet.id).toBeDefined() 150 expect(facet.label).toBeDefined() 151 expect(facet.description).toBeDefined() 152 expect(facet.category).toBeDefined() 153 } 154 }) 155 156 it('includes category info for each facet', async () => { 157 mockRouteQuery.value = 'downloads,packageSize' 158 159 const { selectedFacets } = await useFacetSelectionInComponent() 160 161 const downloadsFacet = selectedFacets.value.find(f => f.id === 'downloads') 162 const packageSizeFacet = selectedFacets.value.find(f => f.id === 'packageSize') 163 164 expect(downloadsFacet?.category).toBe('health') 165 expect(packageSizeFacet?.category).toBe('performance') 166 }) 167 }) 168 169 describe('isFacetSelected', () => { 170 it('returns true for selected facets', async () => { 171 mockRouteQuery.value = 'downloads,types' 172 173 const { isFacetSelected } = await useFacetSelectionInComponent() 174 175 expect(isFacetSelected('downloads')).toBe(true) 176 expect(isFacetSelected('types')).toBe(true) 177 }) 178 179 it('returns false for unselected facets', async () => { 180 mockRouteQuery.value = 'downloads,types' 181 182 const { isFacetSelected } = await useFacetSelectionInComponent() 183 184 expect(isFacetSelected('license')).toBe(false) 185 expect(isFacetSelected('engines')).toBe(false) 186 }) 187 }) 188 189 describe('toggleFacet', () => { 190 it('adds facet when not selected', async () => { 191 mockRouteQuery.value = 'downloads' 192 193 const { isFacetSelected, toggleFacet } = await useFacetSelectionInComponent() 194 195 toggleFacet('types') 196 197 expect(isFacetSelected('downloads')).toBe(true) 198 expect(isFacetSelected('types')).toBe(true) 199 }) 200 201 it('removes facet when selected', async () => { 202 mockRouteQuery.value = 'downloads,types' 203 204 const { isFacetSelected, toggleFacet } = await useFacetSelectionInComponent() 205 206 toggleFacet('types') 207 208 expect(isFacetSelected('downloads')).toBe(true) 209 expect(isFacetSelected('types')).toBe(false) 210 }) 211 212 it('does not remove last facet', async () => { 213 mockRouteQuery.value = 'downloads' 214 215 const { isFacetSelected, toggleFacet } = await useFacetSelectionInComponent() 216 217 toggleFacet('downloads') 218 219 // Should still be selected (can't remove the last one) 220 expect(isFacetSelected('downloads')).toBe(true) 221 }) 222 }) 223 224 describe('selectCategory', () => { 225 it('selects all facets in a category', async () => { 226 mockRouteQuery.value = 'downloads' 227 228 const { isFacetSelected, selectCategory } = await useFacetSelectionInComponent() 229 230 selectCategory('performance') 231 232 const performanceFacets = FACETS_BY_CATEGORY.performance.filter( 233 f => !FACET_INFO[f].comingSoon, // comingSoon facet 234 ) 235 for (const facet of performanceFacets) { 236 expect(isFacetSelected(facet)).toBe(true) 237 } 238 }) 239 240 it('preserves existing selections from other categories', async () => { 241 mockRouteQuery.value = 'downloads,license' 242 243 const { isFacetSelected, selectCategory } = await useFacetSelectionInComponent() 244 245 selectCategory('compatibility') 246 247 expect(isFacetSelected('downloads')).toBe(true) 248 expect(isFacetSelected('license')).toBe(true) 249 }) 250 }) 251 252 describe('deselectCategory', () => { 253 it('deselects all facets in a category', async () => { 254 mockRouteQuery.value = '' 255 const { isFacetSelected, deselectCategory } = await useFacetSelectionInComponent() 256 257 deselectCategory('performance') 258 259 const nonComingSoonPerformanceFacets = FACETS_BY_CATEGORY.performance.filter( 260 f => !FACET_INFO[f].comingSoon, 261 ) 262 for (const facet of nonComingSoonPerformanceFacets) { 263 expect(isFacetSelected(facet)).toBe(false) 264 } 265 }) 266 267 it('does not deselect if it would leave no facets', async () => { 268 mockRouteQuery.value = 'packageSize,installSize' 269 270 const { isFacetSelected, deselectCategory } = await useFacetSelectionInComponent() 271 272 deselectCategory('performance') 273 274 // Should still have at least one facet selected - since we can't 275 // deselect all, the original selection should remain 276 expect(isFacetSelected('packageSize') || isFacetSelected('installSize')).toBe(true) 277 }) 278 }) 279 280 describe('selectAll', () => { 281 it('selects all default facets', async () => { 282 mockRouteQuery.value = 'downloads' 283 284 const { isFacetSelected, selectAll } = await useFacetSelectionInComponent() 285 286 selectAll() 287 288 for (const facet of DEFAULT_FACETS) { 289 expect(isFacetSelected(facet)).toBe(true) 290 } 291 }) 292 }) 293 294 describe('deselectAll', () => { 295 it('keeps only the first default facet', async () => { 296 mockRouteQuery.value = '' 297 298 const { isFacetSelected, deselectAll } = await useFacetSelectionInComponent() 299 300 deselectAll() 301 302 // Only the first default facet should be selected 303 expect(isFacetSelected(DEFAULT_FACETS[0]!)).toBe(true) 304 // Second one should not be selected 305 expect(isFacetSelected(DEFAULT_FACETS[1]!)).toBe(false) 306 }) 307 }) 308 309 describe('isAllSelected', () => { 310 it('returns true when all facets selected', async () => { 311 mockRouteQuery.value = '' 312 313 const { isAllSelected } = await useFacetSelectionInComponent() 314 315 expect(isAllSelected.value).toBe(true) 316 }) 317 318 it('returns false when not all facets selected', async () => { 319 mockRouteQuery.value = 'downloads,types' 320 321 const { isAllSelected } = await useFacetSelectionInComponent() 322 323 expect(isAllSelected.value).toBe(false) 324 }) 325 }) 326 327 describe('isNoneSelected', () => { 328 it('returns true when only one facet selected', async () => { 329 mockRouteQuery.value = 'downloads' 330 331 const { isNoneSelected } = await useFacetSelectionInComponent() 332 333 expect(isNoneSelected.value).toBe(true) 334 }) 335 336 it('returns false when multiple facets selected', async () => { 337 mockRouteQuery.value = 'downloads,types' 338 339 const { isNoneSelected } = await useFacetSelectionInComponent() 340 341 expect(isNoneSelected.value).toBe(false) 342 }) 343 }) 344 345 describe('URL param behavior', () => { 346 it('clears URL param when selecting all defaults', async () => { 347 mockRouteQuery.value = 'downloads,types' 348 349 const { selectAll } = await useFacetSelectionInComponent() 350 351 selectAll() 352 353 // Should clear to empty string when matching defaults 354 expect(mockRouteQuery.value).toBe('') 355 }) 356 357 it('sets URL param when selecting subset of facets via toggleFacet', async () => { 358 mockRouteQuery.value = '' 359 360 const { toggleFacet, deselectAll } = await useFacetSelectionInComponent() 361 362 // Start with one facet, then add another 363 deselectAll() 364 toggleFacet('types') 365 366 expect(mockRouteQuery.value).toContain('types') 367 }) 368 }) 369 370 describe('allFacets export', () => { 371 it('exports allFacets array', async () => { 372 const { allFacets } = await useFacetSelectionInComponent() 373 374 expect(Array.isArray(allFacets)).toBe(true) 375 expect(allFacets.length).toBeGreaterThan(0) 376 }) 377 }) 378 379 describe('whitespace handling', () => { 380 it('trims whitespace from facet names in query', async () => { 381 mockRouteQuery.value = ' downloads , types , license ' 382 383 const { isFacetSelected } = await useFacetSelectionInComponent() 384 385 expect(isFacetSelected('downloads')).toBe(true) 386 expect(isFacetSelected('types')).toBe(true) 387 expect(isFacetSelected('license')).toBe(true) 388 }) 389 }) 390 391 describe('duplicate handling', () => { 392 it('handles duplicate facets in query by deduplication via Set', async () => { 393 // When adding facets, the code uses Set for deduplication 394 mockRouteQuery.value = 'downloads' 395 396 const { isFacetSelected, selectCategory } = await useFacetSelectionInComponent() 397 398 // downloads is in health category, selecting health should dedupe 399 selectCategory('health') 400 401 // downloads should be selected exactly once (verified by isFacetSelected working) 402 expect(isFacetSelected('downloads')).toBe(true) 403 }) 404 }) 405 406 describe('multiple category operations', () => { 407 it('can select multiple categories', async () => { 408 mockRouteQuery.value = 'downloads' 409 410 const { isFacetSelected, selectCategory } = await useFacetSelectionInComponent() 411 412 selectCategory('performance') 413 selectCategory('security') 414 415 // Should have facets from both categories plus original 416 expect(isFacetSelected('packageSize')).toBe(true) 417 expect(isFacetSelected('license')).toBe(true) 418 expect(isFacetSelected('downloads')).toBe(true) 419 }) 420 421 it('can deselect multiple categories', async () => { 422 mockRouteQuery.value = '' 423 424 const { isFacetSelected, deselectCategory } = await useFacetSelectionInComponent() 425 426 deselectCategory('performance') 427 deselectCategory('health') 428 429 // Should not have performance or health facets 430 expect(isFacetSelected('packageSize')).toBe(false) 431 expect(isFacetSelected('downloads')).toBe(false) 432 }) 433 }) 434})