@reallukemanning/folio 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (332) hide show
  1. package/LICENSE +21 -0
  2. package/copy-components.js +31 -0
  3. package/coverage/base.css +224 -0
  4. package/coverage/block-navigation.js +87 -0
  5. package/coverage/core/copy-components.js.html +178 -0
  6. package/coverage/core/eslint.config.js.html +184 -0
  7. package/coverage/core/index.html +146 -0
  8. package/coverage/core/src/__tests__/benchmarks/ProjectCard.bench.tsx.html +364 -0
  9. package/coverage/core/src/__tests__/benchmarks/ProjectView.bench.tsx.html +484 -0
  10. package/coverage/core/src/__tests__/benchmarks/github.bench.ts.html +244 -0
  11. package/coverage/core/src/__tests__/benchmarks/index.html +191 -0
  12. package/coverage/core/src/__tests__/benchmarks/npm.bench.ts.html +271 -0
  13. package/coverage/core/src/__tests__/benchmarks/product-hunt.bench.ts.html +259 -0
  14. package/coverage/core/src/__tests__/benchmarks/utilities.bench.ts.html +478 -0
  15. package/coverage/core/src/components/FeaturedProject.test.tsx.html +697 -0
  16. package/coverage/core/src/components/FeaturedProject.tsx.html +163 -0
  17. package/coverage/core/src/components/ProjectCard/ProjectCard.test.tsx.html +928 -0
  18. package/coverage/core/src/components/ProjectCard/ProjectCard.tsx.html +379 -0
  19. package/coverage/core/src/components/ProjectCard/index.html +146 -0
  20. package/coverage/core/src/components/ProjectCard/index.ts.html +88 -0
  21. package/coverage/core/src/components/ProjectGrid/ProjectGrid.test.tsx.html +292 -0
  22. package/coverage/core/src/components/ProjectGrid/ProjectGrid.tsx.html +103 -0
  23. package/coverage/core/src/components/ProjectGrid/index.html +146 -0
  24. package/coverage/core/src/components/ProjectGrid/index.ts.html +88 -0
  25. package/coverage/core/src/components/ProjectList/ProjectList.test.tsx.html +292 -0
  26. package/coverage/core/src/components/ProjectList/ProjectList.tsx.html +103 -0
  27. package/coverage/core/src/components/ProjectList/index.html +146 -0
  28. package/coverage/core/src/components/ProjectList/index.ts.html +88 -0
  29. package/coverage/core/src/components/ProjectView/ProjectView.test.tsx.html +1108 -0
  30. package/coverage/core/src/components/ProjectView/ProjectView.tsx.html +589 -0
  31. package/coverage/core/src/components/ProjectView/index.html +146 -0
  32. package/coverage/core/src/components/ProjectView/index.ts.html +88 -0
  33. package/coverage/core/src/components/index.html +146 -0
  34. package/coverage/core/src/components/index.ts.html +97 -0
  35. package/coverage/core/src/index.html +116 -0
  36. package/coverage/core/src/index.ts.html +94 -0
  37. package/coverage/core/src/lib/__tests__/defineProjects.test.ts.html +421 -0
  38. package/coverage/core/src/lib/__tests__/filterByFeatured.test.ts.html +523 -0
  39. package/coverage/core/src/lib/__tests__/filterByStatus.test.ts.html +664 -0
  40. package/coverage/core/src/lib/__tests__/filterByType.test.ts.html +631 -0
  41. package/coverage/core/src/lib/__tests__/github.test.ts.html +1783 -0
  42. package/coverage/core/src/lib/__tests__/hybrid-config.test.ts.html +1345 -0
  43. package/coverage/core/src/lib/__tests__/index.html +311 -0
  44. package/coverage/core/src/lib/__tests__/normalise.test.ts.html +1585 -0
  45. package/coverage/core/src/lib/__tests__/npm-config.test.ts.html +385 -0
  46. package/coverage/core/src/lib/__tests__/npm.test.ts.html +1135 -0
  47. package/coverage/core/src/lib/__tests__/product-hunt-config.test.ts.html +397 -0
  48. package/coverage/core/src/lib/__tests__/product-hunt.test.ts.html +505 -0
  49. package/coverage/core/src/lib/__tests__/sortByDate.test.ts.html +751 -0
  50. package/coverage/core/src/lib/__tests__/sortByName.test.ts.html +832 -0
  51. package/coverage/core/src/lib/__tests__/sortByStars.test.ts.html +703 -0
  52. package/coverage/core/src/lib/defineProjects.ts.html +100 -0
  53. package/coverage/core/src/lib/filterByFeatured.ts.html +133 -0
  54. package/coverage/core/src/lib/filterByStatus.ts.html +145 -0
  55. package/coverage/core/src/lib/filterByType.ts.html +133 -0
  56. package/coverage/core/src/lib/github.ts.html +517 -0
  57. package/coverage/core/src/lib/index.html +281 -0
  58. package/coverage/core/src/lib/index.ts.html +130 -0
  59. package/coverage/core/src/lib/normalise.ts.html +868 -0
  60. package/coverage/core/src/lib/npm.ts.html +199 -0
  61. package/coverage/core/src/lib/product-hunt.ts.html +256 -0
  62. package/coverage/core/src/lib/sortByDate.ts.html +175 -0
  63. package/coverage/core/src/lib/sortByName.ts.html +172 -0
  64. package/coverage/core/src/lib/sortByStars.ts.html +172 -0
  65. package/coverage/core/src/types/index.html +116 -0
  66. package/coverage/core/src/types/index.ts.html +517 -0
  67. package/coverage/core/vitest.config.ts.html +178 -0
  68. package/coverage/coverage-final.json +53 -0
  69. package/coverage/coverage-summary.json +54 -0
  70. package/coverage/favicon.png +0 -0
  71. package/coverage/index.html +266 -0
  72. package/coverage/prettify.css +1 -0
  73. package/coverage/prettify.js +2 -0
  74. package/coverage/sort-arrow-sprite.png +0 -0
  75. package/coverage/sorter.js +210 -0
  76. package/dist/cli-components/FeaturedProject/FeaturedProject.d.ts +6 -0
  77. package/dist/cli-components/FeaturedProject/FeaturedProject.d.ts.map +1 -0
  78. package/dist/cli-components/FeaturedProject/FeaturedProject.js +9 -0
  79. package/dist/cli-components/FeaturedProject/FeaturedProject.js.map +1 -0
  80. package/dist/cli-components/FeaturedProject/FeaturedProject.tsx +54 -0
  81. package/dist/cli-components/FeaturedProject/index.d.ts +3 -0
  82. package/dist/cli-components/FeaturedProject/index.d.ts.map +1 -0
  83. package/dist/cli-components/FeaturedProject/index.js +2 -0
  84. package/dist/cli-components/FeaturedProject/index.js.map +1 -0
  85. package/dist/cli-components/FeaturedProject/index.ts +2 -0
  86. package/dist/cli-components/ProjectCard/ProjectCard.d.ts +26 -0
  87. package/dist/cli-components/ProjectCard/ProjectCard.d.ts.map +1 -0
  88. package/dist/cli-components/ProjectCard/ProjectCard.js +33 -0
  89. package/dist/cli-components/ProjectCard/ProjectCard.js.map +1 -0
  90. package/dist/cli-components/ProjectCard/ProjectCard.tsx +90 -0
  91. package/dist/cli-components/ProjectCard/index.d.ts +3 -0
  92. package/dist/cli-components/ProjectCard/index.d.ts.map +1 -0
  93. package/dist/cli-components/ProjectCard/index.js +2 -0
  94. package/dist/cli-components/ProjectCard/index.js.map +1 -0
  95. package/dist/cli-components/ProjectCard/index.ts +2 -0
  96. package/dist/cli-components/ProjectGrid/ProjectGrid.d.ts +5 -0
  97. package/dist/cli-components/ProjectGrid/ProjectGrid.d.ts.map +1 -0
  98. package/dist/cli-components/ProjectGrid/ProjectGrid.js +8 -0
  99. package/dist/cli-components/ProjectGrid/ProjectGrid.js.map +1 -0
  100. package/dist/cli-components/ProjectGrid/ProjectGrid.tsx +6 -0
  101. package/dist/cli-components/ProjectGrid/index.d.ts +3 -0
  102. package/dist/cli-components/ProjectGrid/index.d.ts.map +1 -0
  103. package/dist/cli-components/ProjectGrid/index.js +2 -0
  104. package/dist/cli-components/ProjectGrid/index.js.map +1 -0
  105. package/dist/cli-components/ProjectGrid/index.ts +2 -0
  106. package/dist/cli-components/ProjectList/ProjectList.d.ts +5 -0
  107. package/dist/cli-components/ProjectList/ProjectList.d.ts.map +1 -0
  108. package/dist/cli-components/ProjectList/ProjectList.js +8 -0
  109. package/dist/cli-components/ProjectList/ProjectList.js.map +1 -0
  110. package/dist/cli-components/ProjectList/ProjectList.tsx +6 -0
  111. package/dist/cli-components/ProjectList/index.d.ts +3 -0
  112. package/dist/cli-components/ProjectList/index.d.ts.map +1 -0
  113. package/dist/cli-components/ProjectList/index.js +2 -0
  114. package/dist/cli-components/ProjectList/index.js.map +1 -0
  115. package/dist/cli-components/ProjectList/index.ts +2 -0
  116. package/dist/cli-components/ProjectView/ProjectView.d.ts +17 -0
  117. package/dist/cli-components/ProjectView/ProjectView.d.ts.map +1 -0
  118. package/dist/cli-components/ProjectView/ProjectView.js +39 -0
  119. package/dist/cli-components/ProjectView/ProjectView.js.map +1 -0
  120. package/dist/cli-components/ProjectView/ProjectView.tsx +117 -0
  121. package/dist/cli-components/ProjectView/index.d.ts +3 -0
  122. package/dist/cli-components/ProjectView/index.d.ts.map +1 -0
  123. package/dist/cli-components/ProjectView/index.js +2 -0
  124. package/dist/cli-components/ProjectView/index.js.map +1 -0
  125. package/dist/cli-components/ProjectView/index.ts +2 -0
  126. package/dist/cli-components/types.d.ts +52 -0
  127. package/dist/cli-components/types.d.ts.map +1 -0
  128. package/dist/cli-components/types.js +2 -0
  129. package/dist/cli-components/types.js.map +1 -0
  130. package/dist/cli-components/types.ts +58 -0
  131. package/dist/cli-types.d.ts +52 -0
  132. package/dist/cli-types.d.ts.map +1 -0
  133. package/dist/cli-types.js +2 -0
  134. package/dist/cli-types.js.map +1 -0
  135. package/dist/cli.d.ts +3 -0
  136. package/dist/cli.d.ts.map +1 -0
  137. package/dist/cli.js +21 -0
  138. package/dist/cli.js.map +1 -0
  139. package/dist/commands/add.d.ts +2 -0
  140. package/dist/commands/add.d.ts.map +1 -0
  141. package/dist/commands/add.js +166 -0
  142. package/dist/commands/add.js.map +1 -0
  143. package/dist/commands/init.d.ts +6 -0
  144. package/dist/commands/init.d.ts.map +1 -0
  145. package/dist/commands/init.js +231 -0
  146. package/dist/commands/init.js.map +1 -0
  147. package/dist/components/FeaturedProject.d.ts +7 -0
  148. package/dist/components/FeaturedProject.d.ts.map +1 -0
  149. package/dist/components/FeaturedProject.js +10 -0
  150. package/dist/components/FeaturedProject.js.map +1 -0
  151. package/dist/components/FeaturedProject.test.tsx +204 -0
  152. package/dist/components/FeaturedProject.tsx +26 -0
  153. package/dist/components/ProjectCard/ProjectCard.d.ts +26 -0
  154. package/dist/components/ProjectCard/ProjectCard.d.ts.map +1 -0
  155. package/dist/components/ProjectCard/ProjectCard.js +39 -0
  156. package/dist/components/ProjectCard/ProjectCard.js.map +1 -0
  157. package/dist/components/ProjectCard/ProjectCard.test.tsx +281 -0
  158. package/dist/components/ProjectCard/ProjectCard.tsx +98 -0
  159. package/dist/components/ProjectCard/index.d.ts +2 -0
  160. package/dist/components/ProjectCard/index.d.ts.map +1 -0
  161. package/dist/components/ProjectCard/index.js +2 -0
  162. package/dist/components/ProjectCard/index.js.map +1 -0
  163. package/dist/components/ProjectCard/index.ts +1 -0
  164. package/dist/components/ProjectGrid/ProjectGrid.d.ts +5 -0
  165. package/dist/components/ProjectGrid/ProjectGrid.d.ts.map +1 -0
  166. package/dist/components/ProjectGrid/ProjectGrid.js +8 -0
  167. package/dist/components/ProjectGrid/ProjectGrid.js.map +1 -0
  168. package/dist/components/ProjectGrid/ProjectGrid.test.tsx +69 -0
  169. package/dist/components/ProjectGrid/ProjectGrid.tsx +6 -0
  170. package/dist/components/ProjectGrid/index.d.ts +2 -0
  171. package/dist/components/ProjectGrid/index.d.ts.map +1 -0
  172. package/dist/components/ProjectGrid/index.js +2 -0
  173. package/dist/components/ProjectGrid/index.js.map +1 -0
  174. package/dist/components/ProjectGrid/index.ts +1 -0
  175. package/dist/components/ProjectList/ProjectList.d.ts +5 -0
  176. package/dist/components/ProjectList/ProjectList.d.ts.map +1 -0
  177. package/dist/components/ProjectList/ProjectList.js +8 -0
  178. package/dist/components/ProjectList/ProjectList.js.map +1 -0
  179. package/dist/components/ProjectList/ProjectList.test.tsx +69 -0
  180. package/dist/components/ProjectList/ProjectList.tsx +6 -0
  181. package/dist/components/ProjectList/index.d.ts +2 -0
  182. package/dist/components/ProjectList/index.d.ts.map +1 -0
  183. package/dist/components/ProjectList/index.js +2 -0
  184. package/dist/components/ProjectList/index.js.map +1 -0
  185. package/dist/components/ProjectList/index.ts +1 -0
  186. package/dist/components/ProjectView/ProjectView.d.ts +20 -0
  187. package/dist/components/ProjectView/ProjectView.d.ts.map +1 -0
  188. package/dist/components/ProjectView/ProjectView.js +64 -0
  189. package/dist/components/ProjectView/ProjectView.js.map +1 -0
  190. package/dist/components/ProjectView/ProjectView.test.tsx +341 -0
  191. package/dist/components/ProjectView/ProjectView.tsx +168 -0
  192. package/dist/components/ProjectView/index.d.ts +2 -0
  193. package/dist/components/ProjectView/index.d.ts.map +1 -0
  194. package/dist/components/ProjectView/index.js +2 -0
  195. package/dist/components/ProjectView/index.js.map +1 -0
  196. package/dist/components/ProjectView/index.ts +1 -0
  197. package/dist/components/index.d.ts +5 -0
  198. package/dist/components/index.d.ts.map +1 -0
  199. package/dist/components/index.js +5 -0
  200. package/dist/components/index.js.map +1 -0
  201. package/dist/components/index.ts +4 -0
  202. package/dist/index.d.ts +4 -0
  203. package/dist/index.d.ts.map +1 -0
  204. package/dist/index.js +4 -0
  205. package/dist/index.js.map +1 -0
  206. package/dist/lib/defineProjects.d.ts +3 -0
  207. package/dist/lib/defineProjects.d.ts.map +1 -0
  208. package/dist/lib/defineProjects.js +4 -0
  209. package/dist/lib/defineProjects.js.map +1 -0
  210. package/dist/lib/filterByFeatured.d.ts +3 -0
  211. package/dist/lib/filterByFeatured.d.ts.map +1 -0
  212. package/dist/lib/filterByFeatured.js +10 -0
  213. package/dist/lib/filterByFeatured.js.map +1 -0
  214. package/dist/lib/filterByStatus.d.ts +3 -0
  215. package/dist/lib/filterByStatus.d.ts.map +1 -0
  216. package/dist/lib/filterByStatus.js +13 -0
  217. package/dist/lib/filterByStatus.js.map +1 -0
  218. package/dist/lib/filterByType.d.ts +3 -0
  219. package/dist/lib/filterByType.d.ts.map +1 -0
  220. package/dist/lib/filterByType.js +10 -0
  221. package/dist/lib/filterByType.js.map +1 -0
  222. package/dist/lib/github.d.ts +24 -0
  223. package/dist/lib/github.d.ts.map +1 -0
  224. package/dist/lib/github.js +107 -0
  225. package/dist/lib/github.js.map +1 -0
  226. package/dist/lib/index.d.ts +16 -0
  227. package/dist/lib/index.d.ts.map +1 -0
  228. package/dist/lib/index.js +12 -0
  229. package/dist/lib/index.js.map +1 -0
  230. package/dist/lib/normalise.d.ts +4 -0
  231. package/dist/lib/normalise.d.ts.map +1 -0
  232. package/dist/lib/normalise.js +221 -0
  233. package/dist/lib/normalise.js.map +1 -0
  234. package/dist/lib/npm.d.ts +7 -0
  235. package/dist/lib/npm.d.ts.map +1 -0
  236. package/dist/lib/npm.js +28 -0
  237. package/dist/lib/npm.js.map +1 -0
  238. package/dist/lib/product-hunt.d.ts +12 -0
  239. package/dist/lib/product-hunt.d.ts.map +1 -0
  240. package/dist/lib/product-hunt.js +40 -0
  241. package/dist/lib/product-hunt.js.map +1 -0
  242. package/dist/lib/sortByDate.d.ts +4 -0
  243. package/dist/lib/sortByDate.d.ts.map +1 -0
  244. package/dist/lib/sortByDate.js +21 -0
  245. package/dist/lib/sortByDate.js.map +1 -0
  246. package/dist/lib/sortByName.d.ts +4 -0
  247. package/dist/lib/sortByName.d.ts.map +1 -0
  248. package/dist/lib/sortByName.js +21 -0
  249. package/dist/lib/sortByName.js.map +1 -0
  250. package/dist/lib/sortByStars.d.ts +4 -0
  251. package/dist/lib/sortByStars.d.ts.map +1 -0
  252. package/dist/lib/sortByStars.js +21 -0
  253. package/dist/lib/sortByStars.js.map +1 -0
  254. package/dist/test-setup.d.ts +2 -0
  255. package/dist/test-setup.d.ts.map +1 -0
  256. package/dist/test-setup.js +2 -0
  257. package/dist/test-setup.js.map +1 -0
  258. package/dist/types/index.d.ts +121 -0
  259. package/dist/types/index.d.ts.map +1 -0
  260. package/dist/types/index.js +2 -0
  261. package/dist/types/index.js.map +1 -0
  262. package/eslint.config.js +33 -0
  263. package/package.json +47 -0
  264. package/playwright-report/index.html +85 -0
  265. package/src/__tests__/benchmarks/ProjectCard.bench.tsx +93 -0
  266. package/src/__tests__/benchmarks/ProjectView.bench.tsx +133 -0
  267. package/src/__tests__/benchmarks/github.bench.ts +53 -0
  268. package/src/__tests__/benchmarks/npm.bench.ts +62 -0
  269. package/src/__tests__/benchmarks/product-hunt.bench.ts +58 -0
  270. package/src/__tests__/benchmarks/utilities.bench.ts +131 -0
  271. package/src/cli-components/FeaturedProject/FeaturedProject.tsx +54 -0
  272. package/src/cli-components/FeaturedProject/index.ts +2 -0
  273. package/src/cli-components/ProjectCard/ProjectCard.tsx +90 -0
  274. package/src/cli-components/ProjectCard/index.ts +2 -0
  275. package/src/cli-components/ProjectGrid/ProjectGrid.tsx +6 -0
  276. package/src/cli-components/ProjectGrid/index.ts +2 -0
  277. package/src/cli-components/ProjectList/ProjectList.tsx +6 -0
  278. package/src/cli-components/ProjectList/index.ts +2 -0
  279. package/src/cli-components/ProjectView/ProjectView.tsx +117 -0
  280. package/src/cli-components/ProjectView/index.ts +2 -0
  281. package/src/cli-components/types.ts +58 -0
  282. package/src/cli-types.ts +58 -0
  283. package/src/cli.ts +26 -0
  284. package/src/commands/add.ts +191 -0
  285. package/src/commands/init.ts +260 -0
  286. package/src/components/FeaturedProject.test.tsx +204 -0
  287. package/src/components/FeaturedProject.tsx +26 -0
  288. package/src/components/ProjectCard/ProjectCard.test.tsx +281 -0
  289. package/src/components/ProjectCard/ProjectCard.tsx +98 -0
  290. package/src/components/ProjectCard/index.ts +1 -0
  291. package/src/components/ProjectGrid/ProjectGrid.test.tsx +69 -0
  292. package/src/components/ProjectGrid/ProjectGrid.tsx +6 -0
  293. package/src/components/ProjectGrid/index.ts +1 -0
  294. package/src/components/ProjectList/ProjectList.test.tsx +69 -0
  295. package/src/components/ProjectList/ProjectList.tsx +6 -0
  296. package/src/components/ProjectList/index.ts +1 -0
  297. package/src/components/ProjectView/ProjectView.test.tsx +341 -0
  298. package/src/components/ProjectView/ProjectView.tsx +168 -0
  299. package/src/components/ProjectView/index.ts +1 -0
  300. package/src/components/index.ts +4 -0
  301. package/src/index.ts +3 -0
  302. package/src/lib/__tests__/defineProjects.test.ts +112 -0
  303. package/src/lib/__tests__/filterByFeatured.test.ts +146 -0
  304. package/src/lib/__tests__/filterByStatus.test.ts +193 -0
  305. package/src/lib/__tests__/filterByType.test.ts +182 -0
  306. package/src/lib/__tests__/github.test.ts +566 -0
  307. package/src/lib/__tests__/hybrid-config.test.ts +420 -0
  308. package/src/lib/__tests__/normalise.test.ts +500 -0
  309. package/src/lib/__tests__/npm-config.test.ts +100 -0
  310. package/src/lib/__tests__/npm.test.ts +350 -0
  311. package/src/lib/__tests__/product-hunt-config.test.ts +104 -0
  312. package/src/lib/__tests__/product-hunt.test.ts +140 -0
  313. package/src/lib/__tests__/sortByDate.test.ts +222 -0
  314. package/src/lib/__tests__/sortByName.test.ts +249 -0
  315. package/src/lib/__tests__/sortByStars.test.ts +206 -0
  316. package/src/lib/defineProjects.ts +5 -0
  317. package/src/lib/filterByFeatured.ts +16 -0
  318. package/src/lib/filterByStatus.ts +20 -0
  319. package/src/lib/filterByType.ts +16 -0
  320. package/src/lib/github.ts +144 -0
  321. package/src/lib/index.ts +15 -0
  322. package/src/lib/normalise.ts +261 -0
  323. package/src/lib/npm.ts +38 -0
  324. package/src/lib/product-hunt.ts +57 -0
  325. package/src/lib/sortByDate.ts +30 -0
  326. package/src/lib/sortByName.ts +29 -0
  327. package/src/lib/sortByStars.ts +29 -0
  328. package/src/test-setup.ts +1 -0
  329. package/src/types/index.ts +144 -0
  330. package/test-results/.last-run.json +62 -0
  331. package/tsconfig.json +29 -0
  332. package/vitest.config.ts +31 -0
@@ -0,0 +1,566 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
2
+ import { fetchGitHubRepo, GitHubRepoData } from '../github'
3
+
4
+ const mockFetch = vi.fn()
5
+ vi.stubGlobal('fetch', mockFetch)
6
+
7
+ const originalEnv = process.env
8
+
9
+ describe('fetchGitHubRepo', () => {
10
+ beforeEach(() => {
11
+ vi.clearAllMocks()
12
+ process.env = { ...originalEnv }
13
+ delete process.env.GITHUB_TOKEN
14
+ })
15
+
16
+ afterEach(() => {
17
+ process.env = originalEnv
18
+ })
19
+
20
+ describe('valid repo', () => {
21
+ it('should return correct repo data for valid repo', async () => {
22
+ const mockData: GitHubRepoData = {
23
+ name: 'test-repo',
24
+ description: 'A test repository',
25
+ stargazers_count: 100,
26
+ forks_count: 20,
27
+ language: 'TypeScript',
28
+ topics: ['react', 'typescript'],
29
+ html_url: 'https://github.com/user/test-repo',
30
+ homepage: 'https://example.com',
31
+ created_at: '2024-01-01T00:00:00Z',
32
+ updated_at: '2024-06-01T00:00:00Z',
33
+ }
34
+
35
+ mockFetch.mockResolvedValueOnce({
36
+ ok: true,
37
+ json: () => Promise.resolve(mockData),
38
+ })
39
+
40
+ const result = await fetchGitHubRepo('user/test-repo')
41
+
42
+ expect(result).toEqual(mockData)
43
+ expect(mockFetch).toHaveBeenCalledWith(
44
+ 'https://api.github.com/repos/user/test-repo',
45
+ expect.objectContaining({
46
+ headers: expect.objectContaining({
47
+ Accept: 'application/vnd.github.v3+json',
48
+ }),
49
+ cache: 'force-cache',
50
+ })
51
+ )
52
+ })
53
+
54
+ it('should handle repo with null description', async () => {
55
+ const mockData = {
56
+ name: 'no-desc-repo',
57
+ description: null,
58
+ stargazers_count: 0,
59
+ forks_count: 0,
60
+ language: null,
61
+ topics: [],
62
+ html_url: 'https://github.com/user/no-desc-repo',
63
+ homepage: null,
64
+ created_at: '2024-01-01T00:00:00Z',
65
+ updated_at: '2024-01-01T00:00:00Z',
66
+ }
67
+
68
+ mockFetch.mockResolvedValueOnce({
69
+ ok: true,
70
+ json: () => Promise.resolve(mockData),
71
+ })
72
+
73
+ const result = await fetchGitHubRepo('user/no-desc-repo')
74
+
75
+ expect(result).not.toBeNull()
76
+ expect(result?.description).toBeNull()
77
+ })
78
+
79
+ it('should handle repo with missing topics', async () => {
80
+ const mockData = {
81
+ name: 'no-topics-repo',
82
+ description: 'desc',
83
+ stargazers_count: 0,
84
+ forks_count: 0,
85
+ language: 'JavaScript',
86
+ topics: undefined,
87
+ html_url: 'https://github.com/user/no-topics-repo',
88
+ homepage: null,
89
+ created_at: '2024-01-01T00:00:00Z',
90
+ updated_at: '2024-01-01T00:00:00Z',
91
+ }
92
+
93
+ mockFetch.mockResolvedValueOnce({
94
+ ok: true,
95
+ json: () => Promise.resolve(mockData),
96
+ })
97
+
98
+ const result = await fetchGitHubRepo('user/no-topics-repo')
99
+
100
+ expect(result).not.toBeNull()
101
+ expect(result?.topics).toEqual([])
102
+ })
103
+ })
104
+
105
+ describe('invalid repo', () => {
106
+ it('should return null for 404 not found', async () => {
107
+ mockFetch.mockResolvedValueOnce({
108
+ ok: false,
109
+ status: 404,
110
+ })
111
+
112
+ const result = await fetchGitHubRepo('user/nonexistent-repo')
113
+
114
+ expect(result).toBeNull()
115
+ })
116
+
117
+ it('should return null for 403 forbidden', async () => {
118
+ mockFetch.mockResolvedValueOnce({
119
+ ok: false,
120
+ status: 403,
121
+ })
122
+
123
+ const result = await fetchGitHubRepo('user/forbidden-repo')
124
+
125
+ expect(result).toBeNull()
126
+ })
127
+
128
+ it('should return null for 500 server error', async () => {
129
+ mockFetch.mockResolvedValueOnce({
130
+ ok: false,
131
+ status: 500,
132
+ })
133
+
134
+ const result = await fetchGitHubRepo('user/server-error-repo')
135
+
136
+ expect(result).toBeNull()
137
+ })
138
+
139
+ it('should return null for 401 unauthorized', async () => {
140
+ mockFetch.mockResolvedValueOnce({
141
+ ok: false,
142
+ status: 401,
143
+ })
144
+
145
+ const result = await fetchGitHubRepo('user/unauthorized-repo')
146
+
147
+ expect(result).toBeNull()
148
+ })
149
+ })
150
+
151
+ describe('network error', () => {
152
+ it('should return null for network failure', async () => {
153
+ mockFetch.mockRejectedValueOnce(new Error('Network error'))
154
+
155
+ const result = await fetchGitHubRepo('user/network-error-repo')
156
+
157
+ expect(result).toBeNull()
158
+ })
159
+
160
+ it('should return null for timeout error', async () => {
161
+ mockFetch.mockRejectedValueOnce(new Error('Timeout'))
162
+
163
+ const result = await fetchGitHubRepo('user/timeout-repo')
164
+
165
+ expect(result).toBeNull()
166
+ })
167
+
168
+ it('should return null for DNS resolution failure', async () => {
169
+ mockFetch.mockRejectedValueOnce(new Error('ENOTFOUND'))
170
+
171
+ const result = await fetchGitHubRepo('user/dns-error-repo')
172
+
173
+ expect(result).toBeNull()
174
+ })
175
+
176
+ it('should return null for connection refused', async () => {
177
+ mockFetch.mockRejectedValueOnce(new Error('ECONNREFUSED'))
178
+
179
+ const result = await fetchGitHubRepo('user/conn-refused-repo')
180
+
181
+ expect(result).toBeNull()
182
+ })
183
+ })
184
+
185
+ describe('caching strategy', () => {
186
+ it('should use force-cache for build-time caching', async () => {
187
+ const mockData = {
188
+ name: 'cached-repo',
189
+ description: 'desc',
190
+ stargazers_count: 0,
191
+ forks_count: 0,
192
+ language: null,
193
+ topics: [],
194
+ html_url: 'https://github.com/user/cached-repo',
195
+ homepage: null,
196
+ created_at: '2024-01-01T00:00:00Z',
197
+ updated_at: '2024-01-01T00:00:00Z',
198
+ }
199
+
200
+ mockFetch.mockResolvedValueOnce({
201
+ ok: true,
202
+ json: () => Promise.resolve(mockData),
203
+ })
204
+
205
+ await fetchGitHubRepo('user/cached-repo')
206
+
207
+ expect(mockFetch).toHaveBeenCalledWith(
208
+ expect.any(String),
209
+ expect.objectContaining({
210
+ cache: 'force-cache',
211
+ })
212
+ )
213
+ })
214
+ })
215
+
216
+ describe('authentication', () => {
217
+ it('should include Authorization header when GITHUB_TOKEN is set', async () => {
218
+ process.env.GITHUB_TOKEN = 'ghp_test_token_12345'
219
+
220
+ const mockData = {
221
+ name: 'auth-repo',
222
+ description: 'desc',
223
+ stargazers_count: 0,
224
+ forks_count: 0,
225
+ language: null,
226
+ topics: [],
227
+ html_url: 'https://github.com/user/auth-repo',
228
+ homepage: null,
229
+ created_at: '2024-01-01T00:00:00Z',
230
+ updated_at: '2024-01-01T00:00:00Z',
231
+ }
232
+
233
+ mockFetch.mockResolvedValueOnce({
234
+ ok: true,
235
+ json: () => Promise.resolve(mockData),
236
+ })
237
+
238
+ await fetchGitHubRepo('user/auth-repo')
239
+
240
+ expect(mockFetch).toHaveBeenCalledWith(
241
+ expect.any(String),
242
+ expect.objectContaining({
243
+ headers: expect.objectContaining({
244
+ Authorization: 'Bearer ghp_test_token_12345',
245
+ }),
246
+ })
247
+ )
248
+ })
249
+
250
+ it('should not include Authorization header when GITHUB_TOKEN is not set', async () => {
251
+ delete process.env.GITHUB_TOKEN
252
+
253
+ const mockData = {
254
+ name: 'no-auth-repo',
255
+ description: 'desc',
256
+ stargazers_count: 0,
257
+ forks_count: 0,
258
+ language: null,
259
+ topics: [],
260
+ html_url: 'https://github.com/user/no-auth-repo',
261
+ homepage: null,
262
+ created_at: '2024-01-01T00:00:00Z',
263
+ updated_at: '2024-01-01T00:00:00Z',
264
+ }
265
+
266
+ mockFetch.mockResolvedValueOnce({
267
+ ok: true,
268
+ json: () => Promise.resolve(mockData),
269
+ })
270
+
271
+ await fetchGitHubRepo('user/no-auth-repo')
272
+
273
+ const callArgs = mockFetch.mock.calls[0][1]
274
+ expect(callArgs.headers.Authorization).toBeUndefined()
275
+ })
276
+
277
+ it('should warn when GITHUB_TOKEN is not set', async () => {
278
+ delete process.env.GITHUB_TOKEN
279
+
280
+ const mockData = {
281
+ name: 'warn-repo',
282
+ description: 'desc',
283
+ stargazers_count: 0,
284
+ forks_count: 0,
285
+ language: null,
286
+ topics: [],
287
+ html_url: 'https://github.com/user/warn-repo',
288
+ homepage: null,
289
+ created_at: '2024-01-01T00:00:00Z',
290
+ updated_at: '2024-01-01T00:00:00Z',
291
+ }
292
+
293
+ mockFetch.mockResolvedValueOnce({
294
+ ok: true,
295
+ json: () => Promise.resolve(mockData),
296
+ })
297
+
298
+ const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
299
+
300
+ await fetchGitHubRepo('user/warn-repo')
301
+
302
+ expect(warnSpy).toHaveBeenCalledWith(
303
+ 'GITHUB_TOKEN not set - using unauthenticated GitHub API (60/hr rate limit)'
304
+ )
305
+
306
+ warnSpy.mockRestore()
307
+ })
308
+
309
+ it('should not warn when GITHUB_TOKEN is set', async () => {
310
+ process.env.GITHUB_TOKEN = 'ghp_test_token'
311
+
312
+ const mockData = {
313
+ name: 'no-warn-repo',
314
+ description: 'desc',
315
+ stargazers_count: 0,
316
+ forks_count: 0,
317
+ language: null,
318
+ topics: [],
319
+ html_url: 'https://github.com/user/no-warn-repo',
320
+ homepage: null,
321
+ created_at: '2024-01-01T00:00:00Z',
322
+ updated_at: '2024-01-01T00:00:00Z',
323
+ }
324
+
325
+ mockFetch.mockResolvedValueOnce({
326
+ ok: true,
327
+ json: () => Promise.resolve(mockData),
328
+ })
329
+
330
+ const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
331
+
332
+ await fetchGitHubRepo('user/no-warn-repo')
333
+
334
+ expect(warnSpy).not.toHaveBeenCalled()
335
+
336
+ warnSpy.mockRestore()
337
+ })
338
+ })
339
+
340
+ describe('rate limit handling', () => {
341
+ it('should return null for 403 rate limit error', async () => {
342
+ mockFetch.mockResolvedValueOnce({
343
+ ok: false,
344
+ status: 403,
345
+ headers: new Map([['x-ratelimit-remaining', '0']]),
346
+ })
347
+
348
+ const result = await fetchGitHubRepo('user/rate-limited-repo')
349
+
350
+ expect(result).toBeNull()
351
+ })
352
+
353
+ it('should handle secondary rate limit (403 with retry-after)', async () => {
354
+ mockFetch.mockResolvedValueOnce({
355
+ ok: false,
356
+ status: 403,
357
+ headers: new Map([['retry-after', '60']]),
358
+ })
359
+
360
+ const result = await fetchGitHubRepo('user/secondary-rate-limit')
361
+
362
+ expect(result).toBeNull()
363
+ })
364
+ })
365
+
366
+ describe('url construction', () => {
367
+ it('should construct correct GitHub API url', async () => {
368
+ const mockData = {
369
+ name: 'url-test-repo',
370
+ description: 'desc',
371
+ stargazers_count: 0,
372
+ forks_count: 0,
373
+ language: null,
374
+ topics: [],
375
+ html_url: 'https://github.com/owner/repo',
376
+ homepage: null,
377
+ created_at: '2024-01-01T00:00:00Z',
378
+ updated_at: '2024-01-01T00:00:00Z',
379
+ }
380
+
381
+ mockFetch.mockResolvedValueOnce({
382
+ ok: true,
383
+ json: () => Promise.resolve(mockData),
384
+ })
385
+
386
+ await fetchGitHubRepo('owner/repo')
387
+
388
+ expect(mockFetch).toHaveBeenCalledWith(
389
+ 'https://api.github.com/repos/owner/repo',
390
+ expect.any(Object)
391
+ )
392
+ })
393
+
394
+ it('should handle repos with special characters in name', async () => {
395
+ const mockData = {
396
+ name: 'special-repo.name',
397
+ description: 'desc',
398
+ stargazers_count: 0,
399
+ forks_count: 0,
400
+ language: null,
401
+ topics: [],
402
+ html_url: 'https://github.com/user/special-repo.name',
403
+ homepage: null,
404
+ created_at: '2024-01-01T00:00:00Z',
405
+ updated_at: '2024-01-01T00:00:00Z',
406
+ }
407
+
408
+ mockFetch.mockResolvedValueOnce({
409
+ ok: true,
410
+ json: () => Promise.resolve(mockData),
411
+ })
412
+
413
+ await fetchGitHubRepo('user/special-repo.name')
414
+
415
+ expect(mockFetch).toHaveBeenCalledWith(
416
+ 'https://api.github.com/repos/user/special-repo.name',
417
+ expect.any(Object)
418
+ )
419
+ })
420
+ })
421
+
422
+ describe('response parsing', () => {
423
+ it('should parse JSON response correctly', async () => {
424
+ const mockData = {
425
+ name: 'parse-test-repo',
426
+ description: 'A repo with unicode: 🎉',
427
+ stargazers_count: 12345,
428
+ forks_count: 6789,
429
+ language: 'Python',
430
+ topics: ['ai', 'machine-learning'],
431
+ html_url: 'https://github.com/user/parse-test-repo',
432
+ homepage: 'https://parse-test.example.com',
433
+ created_at: '2024-01-15T10:30:00Z',
434
+ updated_at: '2024-06-20T15:45:00Z',
435
+ }
436
+
437
+ mockFetch.mockResolvedValueOnce({
438
+ ok: true,
439
+ json: () => Promise.resolve(mockData),
440
+ })
441
+
442
+ const result = await fetchGitHubRepo('user/parse-test-repo')
443
+
444
+ expect(result).toEqual({
445
+ name: 'parse-test-repo',
446
+ description: 'A repo with unicode: 🎉',
447
+ stargazers_count: 12345,
448
+ forks_count: 6789,
449
+ language: 'Python',
450
+ topics: ['ai', 'machine-learning'],
451
+ html_url: 'https://github.com/user/parse-test-repo',
452
+ homepage: 'https://parse-test.example.com',
453
+ created_at: '2024-01-15T10:30:00Z',
454
+ updated_at: '2024-06-20T15:45:00Z',
455
+ })
456
+ })
457
+
458
+ it('should handle invalid JSON response', async () => {
459
+ mockFetch.mockResolvedValueOnce({
460
+ ok: true,
461
+ json: () => Promise.reject(new Error('Invalid JSON')),
462
+ })
463
+
464
+ const result = await fetchGitHubRepo('user/invalid-json-repo')
465
+
466
+ expect(result).toBeNull()
467
+ })
468
+ })
469
+
470
+ describe('no real API calls', () => {
471
+ it('should use mocked fetch and not hit real GitHub API', async () => {
472
+ mockFetch.mockResolvedValueOnce({
473
+ ok: true,
474
+ json: () => Promise.resolve({
475
+ name: 'mocked-repo',
476
+ description: 'This is mocked',
477
+ stargazers_count: 0,
478
+ forks_count: 0,
479
+ language: null,
480
+ topics: [],
481
+ html_url: 'https://github.com/user/mocked-repo',
482
+ homepage: null,
483
+ created_at: '2024-01-01T00:00:00Z',
484
+ updated_at: '2024-01-01T00:00:00Z',
485
+ }),
486
+ })
487
+
488
+ await fetchGitHubRepo('user/mocked-repo')
489
+
490
+ expect(mockFetch).toHaveBeenCalledTimes(1)
491
+ expect(mockFetch).not.toHaveBeenCalledWith(
492
+ expect.stringContaining('https://api.github.com/repos/microsoft/vscode'),
493
+ expect.any(Object)
494
+ )
495
+ })
496
+ })
497
+
498
+ describe('API contract verification', () => {
499
+ it('should accept Accept header for GitHub API v3', async () => {
500
+ const mockData = {
501
+ name: 'api-version-test',
502
+ description: 'desc',
503
+ stargazers_count: 0,
504
+ forks_count: 0,
505
+ language: null,
506
+ topics: [],
507
+ html_url: 'https://github.com/user/api-version-test',
508
+ homepage: null,
509
+ created_at: '2024-01-01T00:00:00Z',
510
+ updated_at: '2024-01-01T00:00:00Z',
511
+ }
512
+
513
+ mockFetch.mockResolvedValueOnce({
514
+ ok: true,
515
+ json: () => Promise.resolve(mockData),
516
+ })
517
+
518
+ await fetchGitHubRepo('user/api-version-test')
519
+
520
+ expect(mockFetch).toHaveBeenCalledWith(
521
+ expect.any(String),
522
+ expect.objectContaining({
523
+ headers: expect.objectContaining({
524
+ Accept: 'application/vnd.github.v3+json',
525
+ }),
526
+ })
527
+ )
528
+ })
529
+
530
+ it('should map all required fields from GitHub API response', async () => {
531
+ const apiResponse = {
532
+ name: 'full-api-repo',
533
+ description: 'Full API description',
534
+ stargazers_count: 5000,
535
+ forks_count: 300,
536
+ language: 'Rust',
537
+ topics: ['cli', 'tool'],
538
+ html_url: 'https://github.com/user/full-api-repo',
539
+ homepage: 'https://full-api.example.com',
540
+ created_at: '2023-01-01T00:00:00Z',
541
+ updated_at: '2024-12-31T23:59:59Z',
542
+ extra_field: 'should be ignored',
543
+ }
544
+
545
+ mockFetch.mockResolvedValueOnce({
546
+ ok: true,
547
+ json: () => Promise.resolve(apiResponse),
548
+ })
549
+
550
+ const result = await fetchGitHubRepo('user/full-api-repo')
551
+
552
+ expect(result).toEqual({
553
+ name: 'full-api-repo',
554
+ description: 'Full API description',
555
+ stargazers_count: 5000,
556
+ forks_count: 300,
557
+ language: 'Rust',
558
+ topics: ['cli', 'tool'],
559
+ html_url: 'https://github.com/user/full-api-repo',
560
+ homepage: 'https://full-api.example.com',
561
+ created_at: '2023-01-01T00:00:00Z',
562
+ updated_at: '2024-12-31T23:59:59Z',
563
+ })
564
+ })
565
+ })
566
+ })