forked from
npmx.dev/npmx.dev
[READ-ONLY]
a fast, modern browser for the npm registry
1import { describe, expect, it, vi } from 'vitest'
2import type { JsDelivrFileNode, PackageFileTree } from '../../../../shared/types'
3import {
4 convertToFileTree,
5 fetchFileTree,
6 getPackageFileTree,
7} from '../../../../server/utils/file-tree'
8
9const getChildren = (node?: PackageFileTree): PackageFileTree[] => node?.children ?? []
10
11const mockFetchOk = <T>(body: T) => {
12 const fetchMock = vi.fn().mockResolvedValue({
13 ok: true,
14 json: async () => body,
15 })
16 vi.stubGlobal('fetch', fetchMock)
17 return fetchMock
18}
19
20const mockFetchError = (status: number) => {
21 const fetchMock = vi.fn().mockResolvedValue({
22 ok: false,
23 status,
24 })
25 vi.stubGlobal('fetch', fetchMock)
26 return fetchMock
27}
28
29const mockCreateError = () => {
30 const createErrorMock = vi.fn((opts: { statusCode: number; message: string }) => opts)
31 vi.stubGlobal('createError', createErrorMock)
32 return createErrorMock
33}
34
35describe('convertToFileTree', () => {
36 it('converts jsDelivr nodes to a sorted tree with directories first', () => {
37 const input: JsDelivrFileNode[] = [
38 { type: 'file', name: 'zeta.txt', size: 120 },
39 {
40 type: 'directory',
41 name: 'src',
42 files: [
43 { type: 'file', name: 'b.ts', size: 5 },
44 { type: 'file', name: 'a.ts', size: 3 },
45 ],
46 },
47 { type: 'file', name: 'alpha.txt', size: 10 },
48 {
49 type: 'directory',
50 name: 'assets',
51 files: [{ type: 'file', name: 'logo.svg', size: 42 }],
52 },
53 ]
54
55 const tree = convertToFileTree(input)
56
57 const names = tree.map(node => node.name)
58 expect(names).toEqual(['assets', 'src', 'alpha.txt', 'zeta.txt'])
59
60 const srcNode = tree.find(node => node.name === 'src')
61 expect(srcNode?.type).toBe('directory')
62 expect(getChildren(srcNode).map(child => child.name)).toEqual(['a.ts', 'b.ts'])
63 })
64
65 it('builds correct paths and preserves file sizes', () => {
66 const input: JsDelivrFileNode[] = [
67 {
68 type: 'directory',
69 name: 'src',
70 files: [
71 { type: 'file', name: 'index.ts', size: 100 },
72 {
73 type: 'directory',
74 name: 'utils',
75 files: [{ type: 'file', name: 'format.ts', size: 22 }],
76 },
77 ],
78 },
79 ]
80
81 const tree = convertToFileTree(input)
82
83 const src = tree[0]
84 expect(src?.path).toBe('src')
85
86 const indexFile = getChildren(src).find(child => child.name === 'index.ts')
87 expect(indexFile?.path).toBe('src/index.ts')
88 expect(indexFile?.size).toBe(100)
89
90 const utilsDir = getChildren(src).find(child => child.name === 'utils')
91 expect(utilsDir?.type).toBe('directory')
92
93 const formatFile = getChildren(utilsDir).find(child => child.name === 'format.ts')
94 expect(formatFile?.path).toBe('src/utils/format.ts')
95 expect(formatFile?.size).toBe(22)
96 })
97
98 it('returns an empty tree for empty input', () => {
99 const tree = convertToFileTree([])
100 const empty: PackageFileTree[] = []
101 expect(tree).toEqual(empty)
102 })
103
104 it('handles directories without a files property', () => {
105 const input: JsDelivrFileNode[] = [
106 {
107 type: 'directory',
108 name: 'src',
109 },
110 ]
111
112 const tree = convertToFileTree(input)
113
114 expect(tree[0]?.type).toBe('directory')
115 expect(tree[0]?.children).toEqual([])
116 })
117})
118
119describe('fetchFileTree', () => {
120 it('returns parsed json when response is ok', async () => {
121 const body = {
122 type: 'npm',
123 name: 'pkg',
124 version: '1.0.0',
125 default: 'index.js',
126 files: [],
127 }
128
129 mockFetchOk(body)
130
131 try {
132 const result = await fetchFileTree('pkg', '1.0.0')
133 expect(result).toEqual(body)
134 } finally {
135 vi.unstubAllGlobals()
136 }
137 })
138
139 it('throws a 404 error when package is not found', async () => {
140 mockFetchError(404)
141 mockCreateError()
142
143 try {
144 await expect(fetchFileTree('pkg', '1.0.0')).rejects.toMatchObject({ statusCode: 404 })
145 } finally {
146 vi.unstubAllGlobals()
147 }
148 })
149
150 it('throws a 502 error for non-404 failures', async () => {
151 mockFetchError(500)
152 mockCreateError()
153
154 try {
155 await expect(fetchFileTree('pkg', '1.0.0')).rejects.toMatchObject({ statusCode: 502 })
156 } finally {
157 vi.unstubAllGlobals()
158 }
159 })
160})
161
162describe('getPackageFileTree', () => {
163 it('returns metadata and a converted tree', async () => {
164 const body = {
165 type: 'npm',
166 name: 'pkg',
167 version: '1.0.0',
168 default: 'index.js',
169 files: [
170 {
171 type: 'directory',
172 name: 'src',
173 files: [{ type: 'file', name: 'index.js', size: 5 }],
174 },
175 ],
176 }
177
178 mockFetchOk(body)
179
180 try {
181 const result = await getPackageFileTree('pkg', '1.0.0')
182 expect(result.package).toBe('pkg')
183 expect(result.version).toBe('1.0.0')
184 expect(result.default).toBe('index.js')
185 expect(result.tree[0]?.path).toBe('src')
186 expect(result.tree[0]?.children?.[0]?.path).toBe('src/index.js')
187 } finally {
188 vi.unstubAllGlobals()
189 }
190 })
191
192 it('returns undefined when default is missing', async () => {
193 const body = {
194 type: 'npm',
195 name: 'pkg',
196 version: '1.0.0',
197 files: [],
198 }
199
200 mockFetchOk(body)
201
202 try {
203 const result = await getPackageFileTree('pkg', '1.0.0')
204 expect(result.default).toBeUndefined()
205 } finally {
206 vi.unstubAllGlobals()
207 }
208 })
209})