forked from
npmx.dev/npmx.dev
[READ-ONLY]
a fast, modern browser for the npm registry
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})