[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 406 lines 15 kB view raw
1import { describe, expect, it } from 'vitest' 2import { mountSuspended } from '@nuxt/test-utils/runtime' 3import type { NpmSearchResult } from '#shared/types' 4 5// Helper to create mock package results 6function createPackage(overrides: { 7 name: string 8 description?: string 9 keywords?: string[] 10 downloads?: number 11 updated?: string 12 insecure?: number 13}): NpmSearchResult { 14 return { 15 package: { 16 name: overrides.name, 17 version: '1.0.0', 18 description: overrides.description ?? '', 19 keywords: overrides.keywords ?? [], 20 date: overrides.updated ?? '2024-01-01T00:00:00.000Z', 21 links: { npm: '' }, 22 publisher: { username: 'test', email: '' }, 23 maintainers: [], 24 }, 25 downloads: { weekly: overrides.downloads ?? 0 }, 26 updated: overrides.updated ?? '2024-01-01T00:00:00.000Z', 27 flags: { insecure: overrides.insecure ?? 0 }, 28 score: { final: 0.5, detail: { quality: 0.5, popularity: 0.5, maintenance: 0.5 } }, 29 searchScore: 1000, 30 } 31} 32 33/** 34 * Helper to test useStructuredFilters by wrapping it in a component. 35 * This is required because the composable uses useI18n which must be 36 * called inside a Vue component's setup function. 37 */ 38async function useStructuredFiltersInComponent(packages: Ref<NpmSearchResult[]>) { 39 let capturedResult: ReturnType<typeof useStructuredFilters> 40 41 const WrapperComponent = defineComponent({ 42 setup() { 43 capturedResult = useStructuredFilters({ packages }) 44 return () => h('div') 45 }, 46 }) 47 48 await mountSuspended(WrapperComponent) 49 50 return capturedResult! 51} 52 53describe('useStructuredFilters', () => { 54 describe('keyword filtering (AND logic)', () => { 55 it('returns all packages when no keywords selected', async () => { 56 const packages = ref([ 57 createPackage({ name: 'pkg-a', keywords: ['react'] }), 58 createPackage({ name: 'pkg-b', keywords: ['vue'] }), 59 ]) 60 61 const { sortedPackages } = await useStructuredFiltersInComponent(packages) 62 63 expect(sortedPackages.value).toHaveLength(2) 64 }) 65 66 it('filters to packages with single keyword', async () => { 67 const packages = ref([ 68 createPackage({ name: 'pkg-a', keywords: ['react', 'hooks'] }), 69 createPackage({ name: 'pkg-b', keywords: ['vue'] }), 70 ]) 71 72 const { sortedPackages, addKeyword } = await useStructuredFiltersInComponent(packages) 73 addKeyword('react') 74 75 expect(sortedPackages.value).toHaveLength(1) 76 expect(sortedPackages.value[0]!.package.name).toBe('pkg-a') 77 }) 78 79 it('uses AND logic for multiple keywords', async () => { 80 const packages = ref([ 81 createPackage({ name: 'pkg-a', keywords: ['react', 'hooks'] }), 82 createPackage({ name: 'pkg-b', keywords: ['react', 'state'] }), 83 createPackage({ name: 'pkg-c', keywords: ['react', 'hooks', 'state'] }), 84 ]) 85 86 const { sortedPackages, addKeyword } = await useStructuredFiltersInComponent(packages) 87 addKeyword('react') 88 addKeyword('hooks') 89 90 // Only packages with BOTH 'react' AND 'hooks' 91 expect(sortedPackages.value).toHaveLength(2) 92 expect(sortedPackages.value.map(p => p.package.name)).toContain('pkg-a') 93 expect(sortedPackages.value.map(p => p.package.name)).toContain('pkg-c') 94 }) 95 96 it('returns empty when no package has all keywords', async () => { 97 const packages = ref([ 98 createPackage({ name: 'pkg-a', keywords: ['react'] }), 99 createPackage({ name: 'pkg-b', keywords: ['hooks'] }), 100 ]) 101 102 const { sortedPackages, addKeyword } = await useStructuredFiltersInComponent(packages) 103 addKeyword('react') 104 addKeyword('hooks') 105 106 expect(sortedPackages.value).toHaveLength(0) 107 }) 108 }) 109 110 describe('text filtering', () => { 111 it('filters by name when scope is name', async () => { 112 const packages = ref([ 113 createPackage({ name: 'react-query', description: 'Data fetching' }), 114 createPackage({ name: 'vue-query', description: 'Data fetching for Vue' }), 115 ]) 116 117 const { sortedPackages, setTextFilter, setSearchScope } = 118 await useStructuredFiltersInComponent(packages) 119 setSearchScope('name') 120 setTextFilter('react') 121 122 expect(sortedPackages.value).toHaveLength(1) 123 expect(sortedPackages.value[0]!.package.name).toBe('react-query') 124 }) 125 126 it('filters by description when scope is description', async () => { 127 const packages = ref([ 128 createPackage({ name: 'pkg-a', description: 'A React component library' }), 129 createPackage({ name: 'pkg-b', description: 'A Vue component library' }), 130 ]) 131 132 const { sortedPackages, setTextFilter, setSearchScope } = 133 await useStructuredFiltersInComponent(packages) 134 setSearchScope('description') 135 setTextFilter('React') 136 137 expect(sortedPackages.value).toHaveLength(1) 138 expect(sortedPackages.value[0]!.package.name).toBe('pkg-a') 139 }) 140 141 it('filters by keywords when scope is keywords', async () => { 142 const packages = ref([ 143 createPackage({ name: 'pkg-a', keywords: ['typescript', 'react'] }), 144 createPackage({ name: 'pkg-b', keywords: ['javascript', 'vue'] }), 145 ]) 146 147 const { sortedPackages, setTextFilter, setSearchScope } = 148 await useStructuredFiltersInComponent(packages) 149 setSearchScope('keywords') 150 setTextFilter('type') 151 152 expect(sortedPackages.value).toHaveLength(1) 153 expect(sortedPackages.value[0]!.package.name).toBe('pkg-a') 154 }) 155 156 it('filters by all fields when scope is all', async () => { 157 const packages = ref([ 158 createPackage({ name: 'pkg-a', description: 'foo', keywords: ['bar'] }), 159 createPackage({ name: 'pkg-b', description: 'baz', keywords: ['qux'] }), 160 createPackage({ name: 'search-term', description: 'other', keywords: [] }), 161 ]) 162 163 const { sortedPackages, setTextFilter, setSearchScope } = 164 await useStructuredFiltersInComponent(packages) 165 setSearchScope('all') 166 setTextFilter('search-term') 167 168 expect(sortedPackages.value).toHaveLength(1) 169 expect(sortedPackages.value[0]!.package.name).toBe('search-term') 170 }) 171 }) 172 173 describe('text filtering with operators', () => { 174 it('parses name: operator in all scope', async () => { 175 const packages = ref([ 176 createPackage({ name: 'react-query', description: 'vue stuff' }), 177 createPackage({ name: 'vue-query', description: 'react stuff' }), 178 ]) 179 180 const { sortedPackages, setTextFilter, setSearchScope } = 181 await useStructuredFiltersInComponent(packages) 182 setSearchScope('all') 183 setTextFilter('name:react') 184 185 expect(sortedPackages.value).toHaveLength(1) 186 expect(sortedPackages.value[0]!.package.name).toBe('react-query') 187 }) 188 189 it('parses desc: operator in all scope', async () => { 190 const packages = ref([ 191 createPackage({ name: 'pkg-a', description: 'A fantastic library' }), 192 createPackage({ name: 'pkg-b', description: 'A mediocre library' }), 193 ]) 194 195 const { sortedPackages, setTextFilter, setSearchScope } = 196 await useStructuredFiltersInComponent(packages) 197 setSearchScope('all') 198 setTextFilter('desc:fantastic') 199 200 expect(sortedPackages.value).toHaveLength(1) 201 expect(sortedPackages.value[0]!.package.name).toBe('pkg-a') 202 }) 203 204 it('parses kw: operator in all scope', async () => { 205 const packages = ref([ 206 createPackage({ name: 'pkg-a', keywords: ['typescript'] }), 207 createPackage({ name: 'pkg-b', keywords: ['javascript'] }), 208 ]) 209 210 const { sortedPackages, setTextFilter, setSearchScope } = 211 await useStructuredFiltersInComponent(packages) 212 setSearchScope('all') 213 setTextFilter('kw:typescript') 214 215 expect(sortedPackages.value).toHaveLength(1) 216 expect(sortedPackages.value[0]!.package.name).toBe('pkg-a') 217 }) 218 219 it('combines multiple operators with AND logic', async () => { 220 const packages = ref([ 221 createPackage({ name: 'react-lib', description: 'hooks library', keywords: ['react'] }), 222 createPackage({ name: 'react-other', description: 'other stuff', keywords: ['react'] }), 223 createPackage({ name: 'vue-lib', description: 'hooks library', keywords: ['vue'] }), 224 ]) 225 226 const { sortedPackages, setTextFilter, setSearchScope } = 227 await useStructuredFiltersInComponent(packages) 228 setSearchScope('all') 229 setTextFilter('name:react desc:hooks') 230 231 expect(sortedPackages.value).toHaveLength(1) 232 expect(sortedPackages.value[0]!.package.name).toBe('react-lib') 233 }) 234 235 it('handles comma-separated keyword values with OR logic', async () => { 236 const packages = ref([ 237 createPackage({ name: 'pkg-a', keywords: ['react'] }), 238 createPackage({ name: 'pkg-b', keywords: ['vue'] }), 239 createPackage({ name: 'pkg-c', keywords: ['angular'] }), 240 ]) 241 242 const { sortedPackages, setTextFilter, setSearchScope } = 243 await useStructuredFiltersInComponent(packages) 244 setSearchScope('all') 245 setTextFilter('kw:react,vue') 246 247 expect(sortedPackages.value).toHaveLength(2) 248 expect(sortedPackages.value.map(p => p.package.name)).toContain('pkg-a') 249 expect(sortedPackages.value.map(p => p.package.name)).toContain('pkg-b') 250 }) 251 252 it('combines operators with remaining text', async () => { 253 const packages = ref([ 254 createPackage({ 255 name: 'react-query', 256 description: 'Data fetching library', 257 keywords: ['react'], 258 }), 259 createPackage({ name: 'react-form', description: 'Form library', keywords: ['react'] }), 260 createPackage({ 261 name: 'vue-query', 262 description: 'Data fetching library', 263 keywords: ['vue'], 264 }), 265 ]) 266 267 const { sortedPackages, setTextFilter, setSearchScope } = 268 await useStructuredFiltersInComponent(packages) 269 setSearchScope('all') 270 setTextFilter('name:react fetching') 271 272 expect(sortedPackages.value).toHaveLength(1) 273 expect(sortedPackages.value[0]!.package.name).toBe('react-query') 274 }) 275 }) 276 277 describe('download range filtering', () => { 278 it('filters packages below threshold', async () => { 279 const packages = ref([ 280 createPackage({ name: 'popular', downloads: 50000 }), 281 createPackage({ name: 'unpopular', downloads: 50 }), 282 ]) 283 284 const { sortedPackages, setDownloadRange } = await useStructuredFiltersInComponent(packages) 285 setDownloadRange('gt100k') 286 287 expect(sortedPackages.value).toHaveLength(0) 288 }) 289 290 it('filters packages within range', async () => { 291 const packages = ref([ 292 createPackage({ name: 'pkg-a', downloads: 500 }), 293 createPackage({ name: 'pkg-b', downloads: 5000 }), 294 createPackage({ name: 'pkg-c', downloads: 50000 }), 295 ]) 296 297 const { sortedPackages, setDownloadRange } = await useStructuredFiltersInComponent(packages) 298 setDownloadRange('1k-10k') 299 300 expect(sortedPackages.value).toHaveLength(1) 301 expect(sortedPackages.value[0]!.package.name).toBe('pkg-b') 302 }) 303 }) 304 305 describe('sorting', () => { 306 it('sorts by downloads descending by default with downloads sort', async () => { 307 const packages = ref([ 308 createPackage({ name: 'pkg-a', downloads: 100 }), 309 createPackage({ name: 'pkg-b', downloads: 1000 }), 310 createPackage({ name: 'pkg-c', downloads: 500 }), 311 ]) 312 313 const { sortedPackages, setSort } = await useStructuredFiltersInComponent(packages) 314 setSort('downloads-week-desc') 315 316 expect(sortedPackages.value[0]!.package.name).toBe('pkg-b') 317 expect(sortedPackages.value[1]!.package.name).toBe('pkg-c') 318 expect(sortedPackages.value[2]!.package.name).toBe('pkg-a') 319 }) 320 321 it('sorts by name ascending', async () => { 322 const packages = ref([ 323 createPackage({ name: 'zlib' }), 324 createPackage({ name: 'axios' }), 325 createPackage({ name: 'lodash' }), 326 ]) 327 328 const { sortedPackages, setSort } = await useStructuredFiltersInComponent(packages) 329 setSort('name-asc') 330 331 expect(sortedPackages.value[0]!.package.name).toBe('axios') 332 expect(sortedPackages.value[1]!.package.name).toBe('lodash') 333 expect(sortedPackages.value[2]!.package.name).toBe('zlib') 334 }) 335 336 it('sorts by updated date descending', async () => { 337 const packages = ref([ 338 createPackage({ name: 'old', updated: '2023-01-01T00:00:00.000Z' }), 339 createPackage({ name: 'new', updated: '2024-06-01T00:00:00.000Z' }), 340 createPackage({ name: 'mid', updated: '2024-01-01T00:00:00.000Z' }), 341 ]) 342 343 const { sortedPackages, setSort } = await useStructuredFiltersInComponent(packages) 344 setSort('updated-desc') 345 346 expect(sortedPackages.value[0]!.package.name).toBe('new') 347 expect(sortedPackages.value[1]!.package.name).toBe('mid') 348 expect(sortedPackages.value[2]!.package.name).toBe('old') 349 }) 350 351 it('relevance sort preserves original order', async () => { 352 const packages = ref([ 353 createPackage({ name: 'first', downloads: 100 }), 354 createPackage({ name: 'second', downloads: 1000 }), 355 createPackage({ name: 'third', downloads: 500 }), 356 ]) 357 358 const { sortedPackages, setSort } = await useStructuredFiltersInComponent(packages) 359 setSort('relevance-desc') 360 361 // Relevance should preserve the original order from the server 362 expect(sortedPackages.value[0]!.package.name).toBe('first') 363 expect(sortedPackages.value[1]!.package.name).toBe('second') 364 expect(sortedPackages.value[2]!.package.name).toBe('third') 365 }) 366 }) 367 368 describe('clearing filters', () => { 369 it('clearAllFilters resets all filters', async () => { 370 const packages = ref([ 371 createPackage({ name: 'pkg-a', keywords: ['react'], downloads: 1000 }), 372 createPackage({ name: 'pkg-b', keywords: ['vue'], downloads: 500 }), 373 ]) 374 375 const { sortedPackages, setTextFilter, addKeyword, setDownloadRange, clearAllFilters } = 376 await useStructuredFiltersInComponent(packages) 377 378 setTextFilter('pkg-a') 379 addKeyword('react') 380 setDownloadRange('1k-10k') 381 382 expect(sortedPackages.value).toHaveLength(1) 383 384 clearAllFilters() 385 386 expect(sortedPackages.value).toHaveLength(2) 387 }) 388 389 it('removeKeyword removes specific keyword', async () => { 390 const packages = ref([ 391 createPackage({ name: 'pkg-a', keywords: ['react', 'hooks'] }), 392 createPackage({ name: 'pkg-b', keywords: ['react'] }), 393 ]) 394 395 const { sortedPackages, addKeyword, removeKeyword } = 396 await useStructuredFiltersInComponent(packages) 397 398 addKeyword('react') 399 addKeyword('hooks') 400 expect(sortedPackages.value).toHaveLength(1) 401 402 removeKeyword('hooks') 403 expect(sortedPackages.value).toHaveLength(2) 404 }) 405 }) 406})