[READ-ONLY] a fast, modern browser for the npm registry
at main 190 lines 5.0 kB view raw
1import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' 2import { 3 checkPackageExists, 4 findSimilarPackages, 5 normalizePackageName, 6} from '../../../../app/utils/package-name' 7 8describe('normalizePackageName', () => { 9 it.each([ 10 ['esbuild', 'esbuild'], 11 ['@scope/package', 'package'], 12 ['ESBuild', 'esbuild'], 13 ['my.package', 'mypackage'], 14 ['my-package', 'mypackage'], 15 ['my_package', 'mypackage'], 16 ['jslint', 'lint'], 17 ['nodefoo', 'foo'], 18 ['foojs', 'foo'], 19 ['foonode', 'foo'], 20 ['foo-js', 'foo'], 21 ['foo-node', 'foo'], 22 ['@foo/bar', 'bar'], 23 ] as const)('"%s" -> "%s"', (input, expected) => { 24 expect(normalizePackageName(input)).toBe(expected) 25 }) 26}) 27 28describe('checkPackageExists', () => { 29 let fetchMock: ReturnType<typeof vi.fn> 30 31 beforeEach(() => { 32 fetchMock = vi.fn() 33 vi.stubGlobal('$fetch', fetchMock) 34 }) 35 36 afterEach(() => { 37 vi.unstubAllGlobals() 38 }) 39 40 it('returns true when package exists', async () => { 41 fetchMock.mockResolvedValue(undefined) 42 43 const result = await checkPackageExists('vue') 44 45 expect(result).toBe(true) 46 expect(fetchMock).toHaveBeenCalledWith('https://registry.npmjs.org/vue', { method: 'HEAD' }) 47 }) 48 49 it('returns false when package does not exist', async () => { 50 fetchMock.mockRejectedValue(new Error('404')) 51 52 const result = await checkPackageExists('nonexistent-package') 53 54 expect(result).toBe(false) 55 }) 56 57 it('encodes regular package names', async () => { 58 fetchMock.mockResolvedValue(undefined) 59 60 await checkPackageExists('some-package') 61 62 expect(fetchMock).toHaveBeenCalledWith('https://registry.npmjs.org/some-package', { 63 method: 'HEAD', 64 }) 65 }) 66 67 it('encodes scoped package names correctly', async () => { 68 fetchMock.mockResolvedValue(undefined) 69 70 await checkPackageExists('@vue/core') 71 72 expect(fetchMock).toHaveBeenCalledWith('https://registry.npmjs.org/@vue%2Fcore', { 73 method: 'HEAD', 74 }) 75 }) 76}) 77 78describe('findSimilarPackages', () => { 79 let fetchMock: ReturnType<typeof vi.fn> 80 81 beforeEach(() => { 82 fetchMock = vi.fn() 83 vi.stubGlobal('$fetch', fetchMock) 84 }) 85 86 afterEach(() => { 87 vi.unstubAllGlobals() 88 }) 89 90 it('returns empty array on error', async () => { 91 fetchMock.mockRejectedValue(new Error('Network error')) 92 93 const result = await findSimilarPackages('test-package') 94 95 expect(result).toEqual([]) 96 }) 97 98 it('marks exact name matches as exact-match', async () => { 99 fetchMock.mockResolvedValue({ 100 objects: [{ package: { name: 'svelte', description: 'speed.' } }], 101 }) 102 103 const result = await findSimilarPackages('svelte') 104 105 expect(result).toHaveLength(1) 106 expect(result[0]).toMatchObject({ 107 name: 'svelte', 108 similarity: 'exact-match', 109 }) 110 }) 111 112 it('returns partial matches up to the exact match', async () => { 113 fetchMock.mockResolvedValue({ 114 objects: [ 115 { package: { name: 'svel-te', description: 'spe-ed' } }, 116 { package: { name: 'svelte', description: 'speed.' } }, 117 ], 118 }) 119 120 const result = await findSimilarPackages('svelte') 121 122 expect(result).toHaveLength(2) 123 expect(result[0]).toEqual({ 124 name: 'svelte', 125 description: 'speed.', 126 similarity: 'exact-match', 127 }) 128 expect(result[1]).toEqual({ 129 name: 'svel-te', 130 description: 'spe-ed', 131 similarity: 'very-similar', 132 }) 133 }) 134 135 it('marks normalized matches as very-similar', async () => { 136 fetchMock.mockResolvedValue({ 137 objects: [{ package: { name: 'my-pkg', description: 'A package' } }], 138 }) 139 140 const result = await findSimilarPackages('my_pkg') 141 142 expect(result).toHaveLength(1) 143 expect(result[0]).toMatchObject({ 144 name: 'my-pkg', 145 similarity: 'very-similar', 146 }) 147 }) 148 149 it('excludes non-matching packages', async () => { 150 fetchMock.mockResolvedValue({ 151 objects: [{ package: { name: 'absolute-nonsense' } }], 152 }) 153 154 const result = await findSimilarPackages('esbuild') 155 156 expect(result).toEqual([]) 157 }) 158 159 it('includes packages within levenshtein distance threshold', async () => { 160 fetchMock.mockResolvedValue({ 161 objects: [{ package: { name: 'sebuild', description: 'a confused esbuild' } }], 162 }) 163 164 const result = await findSimilarPackages('esbuild') 165 166 expect(result).toHaveLength(1) 167 expect(result[0]).toMatchObject({ 168 name: 'sebuild', 169 similarity: 'similar', 170 }) 171 }) 172 173 it('sorts results by similarity: exact > very-similar > similar', async () => { 174 fetchMock.mockResolvedValue({ 175 objects: [ 176 { package: { name: 'sebuild' } }, // similar 177 { package: { name: 'esbuild' } }, // exact-match 178 { package: { name: 'es-build' } }, // very-similar 179 ], 180 }) 181 182 const result = await findSimilarPackages('esbuild') 183 184 expect(result).toEqual([ 185 { name: 'esbuild', similarity: 'exact-match' }, 186 { name: 'es-build', similarity: 'very-similar' }, 187 { name: 'sebuild', similarity: 'similar' }, 188 ]) 189 }) 190})