search-web-api 1.0.13

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 (111) hide show
  1. package/README.md +307 -0
  2. package/demo/index.ts +38 -0
  3. package/demo/openapi.ts +402 -0
  4. package/demo/routes/autocomplete.ts +124 -0
  5. package/demo/routes/search.ts +61 -0
  6. package/docs/AUTOCOMPLETE.md +296 -0
  7. package/docs/CATEGORY_SEARCH.md +265 -0
  8. package/package.json +32 -0
  9. package/src/autocomplete/autocomplete-ai-next-word-predictor.ts +47 -0
  10. package/src/autocomplete/autocomplete-search-engine-backends.ts +231 -0
  11. package/src/category-registry.ts +6 -0
  12. package/src/config/search-engine-constants.ts +216 -0
  13. package/src/constants.ts +6 -0
  14. package/src/engine-descriptions.ts +7 -0
  15. package/src/engine-status.ts +7 -0
  16. package/src/engine.ts +6 -0
  17. package/src/registry/search-engine-category-registry.ts +200 -0
  18. package/src/registry/search-engine-descriptions.ts +121 -0
  19. package/src/registry/search-engine-status-tracker.ts +240 -0
  20. package/src/result-container.ts +8 -0
  21. package/src/search/search-engines-registry-list.ts +205 -0
  22. package/src/search/search-query-executor.ts +168 -0
  23. package/src/search/search-result-container.ts +421 -0
  24. package/src/search-web-types.ts +7 -0
  25. package/src/search.ts +10 -0
  26. package/src/sources/academic/arxiv.ts +72 -0
  27. package/src/sources/academic/core.ts +65 -0
  28. package/src/sources/academic/crossref.ts +75 -0
  29. package/src/sources/academic/doaj.ts +63 -0
  30. package/src/sources/academic/google_scholar.ts +53 -0
  31. package/src/sources/academic/openalex.ts +75 -0
  32. package/src/sources/academic/pubmed.ts +96 -0
  33. package/src/sources/academic/semantic_scholar.ts +77 -0
  34. package/src/sources/academic/wikidata.ts +44 -0
  35. package/src/sources/general/baidu.ts +59 -0
  36. package/src/sources/general/bing.ts +30 -0
  37. package/src/sources/general/brave.ts +54 -0
  38. package/src/sources/general/duckduckgo.ts +49 -0
  39. package/src/sources/general/google.ts +68 -0
  40. package/src/sources/general/mojeek.ts +56 -0
  41. package/src/sources/general/qwant.ts +37 -0
  42. package/src/sources/general/startpage.ts +44 -0
  43. package/src/sources/general/yahoo.ts +41 -0
  44. package/src/sources/general/yandex.ts +56 -0
  45. package/src/sources/images/bing_images.ts +68 -0
  46. package/src/sources/images/deviantart.ts +71 -0
  47. package/src/sources/images/flickr.ts +132 -0
  48. package/src/sources/images/google_images.ts +101 -0
  49. package/src/sources/images/imgur.ts +58 -0
  50. package/src/sources/images/openclipart.ts +52 -0
  51. package/src/sources/images/pixabay.ts +56 -0
  52. package/src/sources/images/unsplash.ts +38 -0
  53. package/src/sources/images/wallhaven.ts +50 -0
  54. package/src/sources/it/crates.ts +41 -0
  55. package/src/sources/it/dockerhub.ts +47 -0
  56. package/src/sources/it/github.ts +37 -0
  57. package/src/sources/it/gitlab.ts +55 -0
  58. package/src/sources/it/npm.ts +36 -0
  59. package/src/sources/it/packagist.ts +43 -0
  60. package/src/sources/it/pypi.ts +30 -0
  61. package/src/sources/it/rubygems.ts +43 -0
  62. package/src/sources/it/stackoverflow.ts +39 -0
  63. package/src/sources/maps/apple_maps.ts +105 -0
  64. package/src/sources/maps/openstreetmap.ts +34 -0
  65. package/src/sources/maps/photon.ts +77 -0
  66. package/src/sources/news/bing_news.ts +95 -0
  67. package/src/sources/news/google_news.ts +80 -0
  68. package/src/sources/news/hackernews.ts +94 -0
  69. package/src/sources/news/yahoo_news.ts +77 -0
  70. package/src/sources/shopping/ebay.ts +96 -0
  71. package/src/sources/social/mastodon.ts +46 -0
  72. package/src/sources/social/medium.ts +52 -0
  73. package/src/sources/social/reddit.ts +48 -0
  74. package/src/sources/social/soundcloud.ts +64 -0
  75. package/src/sources/social/twitter.ts +56 -0
  76. package/src/sources/specialized/annas_archive.ts +97 -0
  77. package/src/sources/specialized/archive.ts +48 -0
  78. package/src/sources/specialized/genius.ts +43 -0
  79. package/src/sources/specialized/goodreads.ts +62 -0
  80. package/src/sources/specialized/imdb.ts +55 -0
  81. package/src/sources/specialized/openlibrary.ts +59 -0
  82. package/src/sources/specialized/wikipedia.ts +37 -0
  83. package/src/sources/specialized/wttr.ts +98 -0
  84. package/src/sources/torrents/1337x.ts +43 -0
  85. package/src/sources/torrents/eztv.ts +47 -0
  86. package/src/sources/torrents/kickass.ts +63 -0
  87. package/src/sources/torrents/nyaa.ts +53 -0
  88. package/src/sources/torrents/solidtorrents.ts +68 -0
  89. package/src/sources/torrents/thepiratebay.ts +57 -0
  90. package/src/sources/torrents/yts.ts +54 -0
  91. package/src/sources/videos/bing_videos.ts +91 -0
  92. package/src/sources/videos/dailymotion.ts +101 -0
  93. package/src/sources/videos/invidious.ts +86 -0
  94. package/src/sources/videos/peertube.ts +76 -0
  95. package/src/sources/videos/vimeo.ts +67 -0
  96. package/src/sources/videos/youtube.ts +78 -0
  97. package/src/suggest-next-words/autocomplete-ai.ts +38 -0
  98. package/src/suggest-next-words/autocomplete-search-engines.ts +384 -0
  99. package/src/suggest-next-words/misspelled-typos-8k.json +1 -0
  100. package/src/types/search-engine-interface.ts +27 -0
  101. package/src/types/search-result-types.ts +405 -0
  102. package/test/api.test.ts +128 -0
  103. package/test/autocomplete-ai.test.ts +20 -0
  104. package/test/autocomplete-engines.test.ts +131 -0
  105. package/test/engine-health-suite.test.ts +350 -0
  106. package/test/search.test.ts +69 -0
  107. package/test/sources-unit.test.ts +1152 -0
  108. package/test/sources.test.ts +182 -0
  109. package/test/test-utils.ts +81 -0
  110. package/tsconfig.json +16 -0
  111. package/vitest.config.ts +20 -0
@@ -0,0 +1,1152 @@
1
+ /**
2
+ * Unit tests for all search-web-api engine functions using mocked fetch.
3
+ *
4
+ * Every engine in ALL_ENGINES is tested:
5
+ * - Happy path: mock returns appropriate HTML/JSON → engine returns non-empty results
6
+ * - Error path: mock returns 404 → engine returns []
7
+ *
8
+ * Special cases:
9
+ * - core: skipped unless CORE_API_KEY env var is set (requires API key)
10
+ * - pubmed: two-step fetch (esearch then efetch) handled via URL-pattern dispatch
11
+ * - apple_maps: three-step fetch handled via URL-pattern dispatch
12
+ * - youtube: tries multiple Invidious instances; first mock success wins
13
+ * - flickr: HTML with embedded modelExport JSON
14
+ * - google_images: HTML with embedded {"ischj":...} JSON
15
+ * - vimeo: HTML with embedded `var data = {...};`
16
+ * - pypi: regex-parsed HTML
17
+ * - google_news: URL decode via Buffer.from
18
+ */
19
+
20
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
21
+
22
+ // Import every engine function directly so we bypass registry imports
23
+ import { google } from "../src/sources/general/google.js";
24
+ import { bing } from "../src/sources/general/bing.js";
25
+ import { duckduckgo } from "../src/sources/general/duckduckgo.js";
26
+ import { yahoo } from "../src/sources/general/yahoo.js";
27
+ import { qwant } from "../src/sources/general/qwant.js";
28
+ import { startpage } from "../src/sources/general/startpage.js";
29
+ import { brave } from "../src/sources/general/brave.js";
30
+ import { yandex } from "../src/sources/general/yandex.js";
31
+ import { baidu } from "../src/sources/general/baidu.js";
32
+ import { mojeek } from "../src/sources/general/mojeek.js";
33
+
34
+ import { github } from "../src/sources/it/github.js";
35
+ import { gitlab } from "../src/sources/it/gitlab.js";
36
+ import { stackoverflow } from "../src/sources/it/stackoverflow.js";
37
+ import { npm } from "../src/sources/it/npm.js";
38
+ import { crates } from "../src/sources/it/crates.js";
39
+ import { dockerhub } from "../src/sources/it/dockerhub.js";
40
+ import { pypi } from "../src/sources/it/pypi.js";
41
+ import { packagist } from "../src/sources/it/packagist.js";
42
+ import { rubygems } from "../src/sources/it/rubygems.js";
43
+
44
+ import { unsplash } from "../src/sources/images/unsplash.js";
45
+ import { bing_images } from "../src/sources/images/bing_images.js";
46
+ import { google_images } from "../src/sources/images/google_images.js";
47
+ import { flickr } from "../src/sources/images/flickr.js";
48
+ import { imgur } from "../src/sources/images/imgur.js";
49
+ import { pixabay } from "../src/sources/images/pixabay.js";
50
+ import { wallhaven } from "../src/sources/images/wallhaven.js";
51
+ import { deviantart } from "../src/sources/images/deviantart.js";
52
+ import { openclipart } from "../src/sources/images/openclipart.js";
53
+
54
+ import { youtube } from "../src/sources/videos/youtube.js";
55
+ import { vimeo } from "../src/sources/videos/vimeo.js";
56
+ import { dailymotion } from "../src/sources/videos/dailymotion.js";
57
+ import { invidious } from "../src/sources/videos/invidious.js";
58
+ import { peertube } from "../src/sources/videos/peertube.js";
59
+ import { bing_videos } from "../src/sources/videos/bing_videos.js";
60
+
61
+ import { hackernews } from "../src/sources/news/hackernews.js";
62
+ import { yahoo_news } from "../src/sources/news/yahoo_news.js";
63
+ import { bing_news } from "../src/sources/news/bing_news.js";
64
+ import { google_news } from "../src/sources/news/google_news.js";
65
+
66
+ import { google_scholar } from "../src/sources/academic/google_scholar.js";
67
+ import { arxiv } from "../src/sources/academic/arxiv.js";
68
+ import { wikidata } from "../src/sources/academic/wikidata.js";
69
+ import { semantic_scholar } from "../src/sources/academic/semantic_scholar.js";
70
+ import { crossref } from "../src/sources/academic/crossref.js";
71
+ import { pubmed } from "../src/sources/academic/pubmed.js";
72
+ import { openalex } from "../src/sources/academic/openalex.js";
73
+ import { doaj } from "../src/sources/academic/doaj.js";
74
+ import { core } from "../src/sources/academic/core.js";
75
+
76
+ import { torrent_1337x } from "../src/sources/torrents/1337x.js";
77
+ import { thepiratebay } from "../src/sources/torrents/thepiratebay.js";
78
+ import { nyaa } from "../src/sources/torrents/nyaa.js";
79
+ import { yts } from "../src/sources/torrents/yts.js";
80
+ import { eztv } from "../src/sources/torrents/eztv.js";
81
+ import { solidtorrents } from "../src/sources/torrents/solidtorrents.js";
82
+ import { kickass } from "../src/sources/torrents/kickass.js";
83
+
84
+ import { twitter } from "../src/sources/social/twitter.js";
85
+ import { reddit } from "../src/sources/social/reddit.js";
86
+ import { medium } from "../src/sources/social/medium.js";
87
+ import { soundcloud } from "../src/sources/social/soundcloud.js";
88
+ import { mastodon } from "../src/sources/social/mastodon.js";
89
+
90
+ import { openstreetmap } from "../src/sources/maps/openstreetmap.js";
91
+ import { photon } from "../src/sources/maps/photon.js";
92
+ import { apple_maps } from "../src/sources/maps/apple_maps.js";
93
+
94
+ import { ebay } from "../src/sources/shopping/ebay.js";
95
+
96
+ import { wikipedia } from "../src/sources/specialized/wikipedia.js";
97
+ import { imdb } from "../src/sources/specialized/imdb.js";
98
+ import { genius } from "../src/sources/specialized/genius.js";
99
+ import { archive } from "../src/sources/specialized/archive.js";
100
+ import { openlibrary } from "../src/sources/specialized/openlibrary.js";
101
+ import { wttr } from "../src/sources/specialized/wttr.js";
102
+ import { annas_archive } from "../src/sources/specialized/annas_archive.js";
103
+ import { goodreads } from "../src/sources/specialized/goodreads.js";
104
+
105
+ // ---------------------------------------------------------------------------
106
+ // Helpers
107
+ // ---------------------------------------------------------------------------
108
+
109
+ function makeMockResponse(opts: {
110
+ ok?: boolean;
111
+ json?: any;
112
+ html?: string;
113
+ status?: number;
114
+ }): Response {
115
+ const { ok = true, json, html, status = ok ? 200 : 404 } = opts;
116
+ return {
117
+ ok,
118
+ status,
119
+ json: json !== undefined
120
+ ? () => Promise.resolve(json)
121
+ : () => Promise.reject(new Error("not json")),
122
+ text: html !== undefined
123
+ ? () => Promise.resolve(html)
124
+ : () => Promise.reject(new Error("not text")),
125
+ } as unknown as Response;
126
+ }
127
+
128
+ function assertResults(results: any, engineName: string) {
129
+ expect(Array.isArray(results), `${engineName}: should return an array`).toBe(true);
130
+ expect(results.length, `${engineName}: should have at least 1 result`).toBeGreaterThan(0);
131
+ for (const r of results) {
132
+ expect(typeof r.title, `${engineName}: title should be string`).toBe("string");
133
+ expect(r.title.length, `${engineName}: title should not be empty`).toBeGreaterThan(0);
134
+ expect(typeof r.content, `${engineName}: content should be string`).toBe("string");
135
+ }
136
+ }
137
+
138
+ // ---------------------------------------------------------------------------
139
+ // Mock responses catalogue
140
+ // ---------------------------------------------------------------------------
141
+
142
+ // ---- General HTML ----
143
+ const GOOGLE_HTML = `<html><body>
144
+ <div class="Gx5Zad fP1Qef xpd EtOod pkphOe">
145
+ <a href="/url?q=https://example.com&amp;sa=U"><span class="BNeawe vvjwJb AP7Wnd">Test Title</span><span class="BNeawe s3v9rd AP7Wnd">Test snippet content here.</span></a>
146
+ </div>
147
+ </body></html>`;
148
+
149
+ const BING_HTML = `<html><body>
150
+ <li class="b_algo">
151
+ <h2><a href="https://example.com">Bing Result Title</a></h2>
152
+ <div class="b_caption"><p>Bing snippet content here.</p></div>
153
+ </li>
154
+ </body></html>`;
155
+
156
+ const DUCKDUCKGO_HTML = `<html><body>
157
+ <div class="result">
158
+ <div class="result__title"><a href="https://example.com">DDG Title</a></div>
159
+ <div class="result__snippet">DDG snippet content here.</div>
160
+ </div>
161
+ </body></html>`;
162
+
163
+ const YAHOO_HTML = `<html><body>
164
+ <div class="algo-sr">
165
+ <a href="https://example.com">Yahoo Title</a>
166
+ <div class="compText">Yahoo snippet content here.</div>
167
+ </div>
168
+ </body></html>`;
169
+
170
+ const STARTPAGE_HTML = `<html><body>
171
+ <div class="w-gl__result">
172
+ <a class="w-gl__result-title" href="https://example.com"><h3>Startpage Title</h3></a>
173
+ <p class="w-gl__description">Startpage snippet content.</p>
174
+ </div>
175
+ </body></html>`;
176
+
177
+ const BRAVE_HTML = `<html><body>
178
+ <div class="snippet">
179
+ <a href="https://example.com"><span class="search-snippet-title">Brave Title</span></a>
180
+ <div class="content">Brave snippet content.</div>
181
+ </div>
182
+ </body></html>`;
183
+
184
+ const YANDEX_HTML = `<html><body>
185
+ <li class="serp-item">
186
+ <a class="b-serp-item__title-link" href="https://example.com"><span>Yandex Title</span></a>
187
+ <div class="b-serp-item__text">Yandex content here.</div>
188
+ </li>
189
+ </body></html>`;
190
+
191
+ const BAIDU_HTML = `<html><body>
192
+ <div id="content_left">
193
+ <div class="result c-container">
194
+ <h3><a class="c-title" href="https://example.com">Baidu Title</a></h3>
195
+ <div class="c-abstract">Baidu snippet content here.</div>
196
+ </div>
197
+ </div>
198
+ </body></html>`;
199
+
200
+ const MOJEEK_HTML = `<html><body>
201
+ <ul class="results-standard">
202
+ <li>
203
+ <a class="ob" href="https://example.com">Mojeek URL</a>
204
+ <h2><a href="https://example.com">Mojeek Title</a></h2>
205
+ <p class="s">Mojeek snippet content here.</p>
206
+ </li>
207
+ </ul>
208
+ </body></html>`;
209
+
210
+ // ---- IT JSON ----
211
+ const GITHUB_JSON = {
212
+ items: [{ html_url: "https://github.com/user/repo", full_name: "user/repo", description: "A test repo" }],
213
+ };
214
+ const GITLAB_JSON = [{ web_url: "https://gitlab.com/user/repo", name: "repo", description: "A gitlab repo", namespace: { name: "user" }, tag_list: [], star_count: 5, last_activity_at: "2024-01-01T00:00:00Z" }];
215
+ const STACKOVERFLOW_JSON = { items: [{ link: "https://stackoverflow.com/q/1", title: "How to test?", tags: ["javascript", "testing"] }] };
216
+ const NPM_JSON = { objects: [{ package: { name: "express", description: "Fast web framework", links: { npm: "https://www.npmjs.com/package/express" } } }] };
217
+ const CRATES_JSON = { crates: [{ name: "serde", max_version: "1.0.0", description: "A serialization framework", downloads: 100000, recent_downloads: 5000 }] };
218
+ const DOCKERHUB_JSON = { results: [{ name: "nginx", namespace: "library", description: "Official nginx image", star_count: 100, pull_count: 1000000, is_official: true }] };
219
+ const PYPI_HTML = `<html><body>
220
+ <a class="package-snippet" href="/project/requests/">
221
+ <span class="package-snippet__name">requests</span>
222
+ <span class="package-snippet__version">2.31.0</span>
223
+ <p class="package-snippet__description">Python HTTP for Humans.</p>
224
+ </a>
225
+ </body></html>`;
226
+ const PACKAGIST_JSON = { results: [{ name: "laravel/framework", description: "Laravel framework", url: "https://packagist.org/packages/laravel/framework", downloads: 50000, favers: 1000 }] };
227
+ const RUBYGEMS_JSON = [{ name: "rails", version: "7.0.0", info: "Full-stack web framework", downloads: 50000000, authors: "DHH" }];
228
+
229
+ // ---- Images ----
230
+ const UNSPLASH_JSON = { results: [{ links: { html: "https://unsplash.com/photos/abc" }, description: "A beautiful sunset", alt_description: "sunset over mountains", user: { name: "John Doe" }, urls: { small: "https://images.unsplash.com/photo-abc?w=400" } }] };
231
+ const BING_IMAGES_HTML = `<html><body>
232
+ <ul class="dgControl_list">
233
+ <li>
234
+ <a class="iusc" m='{"purl":"https://example.com/image.jpg","turl":"https://tbn.example.com/thumb.jpg","desc":"A nice image","t":"Nice Image"}'>
235
+ <div class="infnmpt"><a>Nice Image</a></div>
236
+ </a>
237
+ </li>
238
+ </ul>
239
+ </body></html>`;
240
+ // google_images returns HTML with embedded JSON
241
+ const GOOGLE_IMAGES_HTML = `{"ischj":{"metadata":[{"result":{"referrer_url":"https://example.com/page","page_title":"Example Image","site_title":"Example Site"},"original_image":{"width":1920,"height":1080},"thumbnail":{"url":"https://encrypted-tbn0.gstatic.com/thumb.jpg"},"text_in_grid":{"snippet":"An example image"}}]}}`;
242
+ // flickr returns HTML with embedded modelExport JSON
243
+ // Flickr's modelExport structure: main[legend[0]][parseInt(legend[1])][legend[2]]...[legend[7]]
244
+ // legend = ["a","0","b","c","d","e",0,"f"]
245
+ // access path: main["a"][0]["b"]["c"]["d"]["e"][0]["f"]
246
+ const flickrPhotoData = {
247
+ id: "12345",
248
+ title: "Flickr Photo",
249
+ description: "A nice flickr photo",
250
+ realname: "Jane Smith",
251
+ ownerNsid: "user123",
252
+ sizes: {
253
+ data: {
254
+ b: { data: { url: "https://live.staticflickr.com/photo_b.jpg", width: 1024, height: 768 } },
255
+ n: { data: { url: "https://live.staticflickr.com/photo_n.jpg", width: 320, height: 240 } },
256
+ }
257
+ }
258
+ };
259
+ const FLICKR_MODEL_EXPORT: Record<string, any> = {
260
+ legend: [["a","0","b","c","d","e",0,"f"]],
261
+ main: { a: [{ b: { c: { d: { e: [{ f: flickrPhotoData }] } } } }] },
262
+ };
263
+ const FLICKR_MODEL_EXPORT_STR = JSON.stringify(FLICKR_MODEL_EXPORT);
264
+ const FLICKR_HTML = "<html><body><script>\nvar modelExport: " + FLICKR_MODEL_EXPORT_STR + ",\n other: 1;\n</script></body></html>";
265
+ const IMGUR_HTML = `<html><body>
266
+ <div class="cards">
267
+ <div class="post">
268
+ <a href="/gallery/abcdef">
269
+ <img src="https://i.imgur.com/abcdefb.jpg" alt="Funny Cat">
270
+ </a>
271
+ </div>
272
+ </div>
273
+ </body></html>`;
274
+ const PIXABAY_JSON = { page: { results: [{ mediaType: "photo", href: "/photos/sunset-123/", name: "Beautiful Sunset", description: "A sunset photo", sources: { small: "https://cdn.pixabay.com/photo/thumb.jpg", large: "https://cdn.pixabay.com/photo/full.jpg" } }] } };
275
+ const WALLHAVEN_JSON = { data: [{ url: "https://wallhaven.cc/w/abc123", resolution: "1920x1080", category: "general", purity: "sfw", file_size: 1048576, file_type: "image/jpeg", path: "https://w.wallhaven.cc/full/abc/abc123.jpg", thumbs: { small: "https://th.wallhaven.cc/small/abc/abc123.jpg" } }] };
276
+ const DEVIANTART_HTML = `<html><body>
277
+ <div class="V_S0t_">
278
+ <div>
279
+ <div>
280
+ <a href="https://www.deviantart.com/user/art/My-Art-123" aria-label="My Artwork">
281
+ <div><img src="https://images-wixmp-ed30a86b8c4ca887773594c2.wixmp.com/f/image.jpg" srcset="https://images-wixmp-ed30a86b8c4ca887773594c2.wixmp.com/f/image.jpg/v1/abc 480w" alt="My Artwork"></div>
282
+ </a>
283
+ </div>
284
+ </div>
285
+ </div>
286
+ </body></html>`;
287
+ const OPENCLIPART_HTML = `<html><body>
288
+ <div class="gallery">
289
+ <div class="artwork">
290
+ <a href="/detail/123/arrow-clipart">
291
+ <img src="/image/123.svg" alt="Arrow Clipart">
292
+ </a>
293
+ </div>
294
+ </div>
295
+ </body></html>`;
296
+
297
+ // ---- Videos ----
298
+ // youtube/invidious return same JSON format
299
+ const INVIDIOUS_JSON = [{ type: "video", videoId: "abc123", title: "Test Video", description: "A test video", author: "TestChannel", viewCount: 10000, lengthSeconds: 300, publishedText: "2 days ago", videoThumbnails: [{ quality: "sddefault", url: "https://i.ytimg.com/vi/abc123/sddefault.jpg" }] }];
300
+ const VIMEO_HTML = `<html><body><script>var data = {"filtered":{"data":[{"type":"clip","clip":{"uri":"/videos/123456","name":"Vimeo Test Video","created_time":"2024-01-15T10:00:00+00:00","pictures":{"sizes":[{"link":"https://i.vimeocdn.com/video/123456_640.jpg"}]}}}]}};</script></body></html>`;
301
+ const DAILYMOTION_JSON = { list: [{ title: "Daily Test Video", url: "https://www.dailymotion.com/video/abc123", description: "A test video on Dailymotion", duration: 185, created_time: 1700000000, thumbnail_360_url: "https://s2.dmcdn.net/v/abc/360.jpg" }] };
302
+ const BING_VIDEOS_HTML = `<html><body>
303
+ <div class="dg_u">
304
+ <div id="mc_vtvc_video_0">
305
+ <div class="vrhdata" vrhm='{"murl":"https://www.youtube.com/watch?v=abc123","vt":"Bing Video Title","du":"3:45"}'></div>
306
+ <div class="mc_vtvc_meta_block"><span>YouTube</span><span>3:45</span></div>
307
+ <div class="mc_vtvc_th"><img src="https://tbn.example.com/thumb.jpg"></div>
308
+ </div>
309
+ </div>
310
+ </body></html>`;
311
+ const PEERTUBE_JSON = { data: [{ url: "https://peer.tube/videos/watch/abc123", name: "PeerTube Test", description: "A test video", duration: 240, views: 500, thumbnailUrl: "https://peer.tube/lazy-static/thumbnails/abc.jpg", embedUrl: "https://peer.tube/videos/embed/abc123", channel: { displayName: "TestChannel", name: "testchannel", host: "peer.tube" }, account: { displayName: "Test User" }, tags: ["test"] }] };
312
+
313
+ // ---- News ----
314
+ const HACKERNEWS_JSON = { hits: [{ objectID: "12345", title: "HN Test Story", url: "https://example.com/story", points: 100, num_comments: 50, author: "user1" }] };
315
+ const YAHOO_NEWS_HTML = `<html><body>
316
+ <ol class="searchCenterMiddle">
317
+ <li>
318
+ <h4><a href="https://news.yahoo.com/article-123.html">Yahoo News Title</a></h4>
319
+ <p class="compText">Yahoo news snippet content here.</p>
320
+ <img src="https://s.yimg.com/image.jpg">
321
+ </li>
322
+ </ol>
323
+ </body></html>`;
324
+ const BING_NEWS_HTML = `<html><body>
325
+ <div class="newsitem">
326
+ <a class="title" href="https://example.com/news-article">Bing News Title</a>
327
+ <div class="snippet">Bing news snippet content here.</div>
328
+ <div class="source"><span aria-label="Reuters · 2 hours ago">Reuters</span></div>
329
+ </div>
330
+ </body></html>`;
331
+ // google_news href requires a base64url-encoded actual URL
332
+ // Encoding "https://example.com/article" in base64url format
333
+ const GOOGLE_NEWS_ENCODED = Buffer.from("https://example.com/article").toString("base64url");
334
+ const GOOGLE_NEWS_HTML = `<html><body>
335
+ <div class="xrnccd">
336
+ <article>
337
+ <a href="/articles/${GOOGLE_NEWS_ENCODED}">
338
+ <h3>Google News Title</h3>
339
+ <time>2 hours ago</time>
340
+ <a data-n-tid="1">Example News</a>
341
+ </a>
342
+ </article>
343
+ </div>
344
+ </body></html>`;
345
+
346
+ // ---- Academic ----
347
+ const ARXIV_XML = `<?xml version="1.0" encoding="UTF-8"?>
348
+ <feed xmlns="http://www.w3.org/2005/Atom">
349
+ <entry>
350
+ <title>Test Paper Title</title>
351
+ <id>https://arxiv.org/abs/2401.00001</id>
352
+ <summary>This is the abstract of the test paper.</summary>
353
+ <author><name>John Smith</name></author>
354
+ <published>2024-01-01T00:00:00Z</published>
355
+ <category term="cs.LG"/>
356
+ </entry>
357
+ </feed>`;
358
+ const GOOGLE_SCHOLAR_HTML = `<html><body>
359
+ <div class="gs_r gs_or gs_scl">
360
+ <div class="gs_ri">
361
+ <h3 class="gs_rt"><a href="https://example.com/paper">Scholar Paper Title</a></h3>
362
+ <div class="gs_a">Smith et al. - Nature, 2024</div>
363
+ <div class="gs_rs">Abstract snippet of the paper here.</div>
364
+ </div>
365
+ </div>
366
+ </body></html>`;
367
+ const WIKIDATA_JSON = { search: [{ id: "Q42", label: "Douglas Adams", description: "English author", url: "https://www.wikidata.org/wiki/Q42" }] };
368
+ const SEMANTIC_SCHOLAR_JSON = { results: [{ id: "abc123", title: { text: "Deep Learning Paper" }, abstract: { text: "An abstract about deep learning." }, authors: [{ name: "Yann LeCun" }], year: 2024, venue: "NeurIPS", citationCount: 500, primaryPaperLink: { url: "https://www.semanticscholar.org/paper/abc123" } }] };
369
+ const CROSSREF_JSON = { message: { items: [{ title: ["Quantum Computing Review"], URL: "https://doi.org/10.1000/test", DOI: "10.1000/test", type: "journal-article", "container-title": ["Science"], author: [{ given: "Alice", family: "Smith" }], publisher: "Nature Publishing" }] } };
370
+ // pubmed is two-step: first esearch returns XML with IDs, then efetch returns article XML
371
+ const PUBMED_ESEARCH_XML = `<?xml version="1.0" encoding="UTF-8"?>
372
+ <eSearchResult>
373
+ <IdList>
374
+ <Id>38000001</Id>
375
+ </IdList>
376
+ </eSearchResult>`;
377
+ const PUBMED_EFETCH_XML = `<?xml version="1.0" encoding="UTF-8"?>
378
+ <PubmedArticleSet>
379
+ <PubmedArticle>
380
+ <MedlineCitation>
381
+ <PMID>38000001</PMID>
382
+ <Article>
383
+ <ArticleTitle>Test Cancer Research Article</ArticleTitle>
384
+ <Abstract>
385
+ <AbstractText>This is the abstract of the cancer research article.</AbstractText>
386
+ </Abstract>
387
+ <AuthorList>
388
+ <Author>
389
+ <ForeName>Jane</ForeName>
390
+ <LastName>Doe</LastName>
391
+ </Author>
392
+ </AuthorList>
393
+ <Journal>
394
+ <Title>Journal of Cancer Research</Title>
395
+ </Journal>
396
+ </Article>
397
+ </MedlineCitation>
398
+ </PubmedArticle>
399
+ </PubmedArticleSet>`;
400
+ const OPENALEX_JSON = { results: [{ title: "OpenAlex Paper", id: "https://openalex.org/W123", doi: "https://doi.org/10.1000/openalex", primary_location: { landing_page_url: "https://doi.org/10.1000/openalex" }, authorships: [{ author: { display_name: "Alice Smith" } }], abstract_inverted_index: { "This": [0], "is": [1], "abstract": [2] }, publication_year: 2024, host_venue: { display_name: "Nature" }, cited_by_count: 100 }] };
401
+ const DOAJ_JSON = { results: [{ bibjson: { title: "DOAJ Open Access Article", abstract: "An open access article abstract.", author: [{ name: "Bob Jones" }], identifier: [{ type: "doi", id: "10.1000/doaj" }], link: [{ type: "fulltext", url: "https://example.com/article.pdf" }], journal: { title: "Open Access Journal" }, year: "2024" } }] };
402
+ const CORE_JSON = { results: [{ title: "CORE Paper", downloadUrl: "https://core.ac.uk/download/123.pdf", doi: "10.1000/core", abstract: "CORE paper abstract here.", authors: [{ name: "Carol White" }], journals: [{ title: "Science" }], yearPublished: 2024, id: 123 }] };
403
+
404
+ // ---- Torrents ----
405
+ const TORRENT_1337X_HTML = `<html><body>
406
+ <table class="table-list">
407
+ <tbody>
408
+ <tr>
409
+ <td class="name">
410
+ <a href="/torrent/123/">Torrent Name</a>
411
+ <a href="/torrent/123/ubuntu-22.04.torrent">Ubuntu 22.04</a>
412
+ </td>
413
+ <td class="seeds">100</td>
414
+ <td class="leeches">20</td>
415
+ <td class="size">2.3 GB<span>2,300,000,000</span></td>
416
+ </tr>
417
+ </tbody>
418
+ </table>
419
+ </body></html>`;
420
+ const THEPIRATEBAY_HTML = `<html><body>
421
+ <table id="searchResult">
422
+ <tbody>
423
+ <tr>
424
+ <td class="vertTh"><a class="detLink" href="/torrent/1234">Ubuntu Linux ISO</a></td>
425
+ <td></td>
426
+ <td>120</td>
427
+ <td>15</td>
428
+ <td><a href="magnet:?xt=urn:btih:abc123&dn=Ubuntu">Magnet</a></td>
429
+ <font class="detDesc">Uploaded 2024-01-01, Size 2.3 GiB, ULed by ubuntu</font>
430
+ </tr>
431
+ </tbody>
432
+ </table>
433
+ </body></html>`;
434
+ const NYAA_HTML = `<html><body>
435
+ <table class="torrent-list">
436
+ <tbody>
437
+ <tr>
438
+ <td><a href="/?c=1_2">Anime</a></td>
439
+ <td>
440
+ <a href="/view/1234">One Piece 1001</a>
441
+ <a href="/view/1234">One Piece 1001</a>
442
+ </td>
443
+ <td></td>
444
+ <td>350 MiB</td>
445
+ <td></td>
446
+ <td>50</td>
447
+ <td>5</td>
448
+ <td>200</td>
449
+ <td><a href="magnet:?xt=urn:btih:nyaa123&dn=OnePiece">Magnet</a></td>
450
+ </tr>
451
+ </tbody>
452
+ </table>
453
+ </body></html>`;
454
+ const YTS_JSON = { data: { movies: [{ title: "Inception", title_long: "Inception (2010)", rating: 8.8, year: 2010, genres: ["Sci-Fi", "Action"], medium_cover_image: "https://yts.mx/assets/images/movies/inception/medium-cover.jpg", torrents: [{ quality: "1080p", size: "2.14 GB", seeds: 500, peers: 100, hash: "abc123def456" }] }] } };
455
+ const EZTV_HTML = `<html><body>
456
+ <table class="forum_header_border">
457
+ <tr class="forum_header_border">
458
+ <td></td>
459
+ <td><a class="epinfo" href="/ep/123/breaking-bad-s01e01/">Breaking Bad S01E01</a></td>
460
+ <td><a class="magnet" href="magnet:?xt=urn:btih:eztv123&dn=BreakingBad">Magnet</a></td>
461
+ <td>350 MB</td>
462
+ <td>2024-01-01</td>
463
+ <td>75</td>
464
+ </tr>
465
+ </table>
466
+ </body></html>`;
467
+ const SOLIDTORRENTS_HTML = `<html><body>
468
+ <li class="search-result">
469
+ <a class="dl-torrent" href="/torrents/abc123.torrent">Download</a>
470
+ <a class="dl-magnet" href="magnet:?xt=urn:btih:solid123&dn=Ubuntu">Magnet</a>
471
+ <h5 class="title"><a href="/torrent/abc123">Ubuntu 22.04 LTS</a></h5>
472
+ <a class="category">Linux</a>
473
+ <div class="stats">
474
+ <div>5,000</div>
475
+ <div>2.3 GB</div>
476
+ <div>15</div>
477
+ <div>120</div>
478
+ <div>2024-01-01</div>
479
+ </div>
480
+ </li>
481
+ </body></html>`;
482
+ const KICKASS_HTML = `<html><body>
483
+ <table class="data">
484
+ <tr></tr>
485
+ <tr>
486
+ <td></td>
487
+ <td>
488
+ <a class="cellMainLink" href="/usearch/ubuntu-22.04.html">Ubuntu 22.04 LTS</a>
489
+ <span class="font11px lightgrey block">2024-01-01</span>
490
+ </td>
491
+ <td></td>
492
+ <td class="nobr">2.3 GB</td>
493
+ <td></td>
494
+ <td class="green">100</td>
495
+ <td class="red">10</td>
496
+ </tr>
497
+ </table>
498
+ </body></html>`;
499
+
500
+ // ---- Social ----
501
+ const TWITTER_HTML = `<html><body>
502
+ <div class="timeline-item">
503
+ <a class="tweet-link" href="/user/status/123"></a>
504
+ <span class="fullname">Test User</span>
505
+ <span class="username">@testuser</span>
506
+ <div class="tweet-content">This is a test tweet about programming.</div>
507
+ <div class="tweet-date"><a>Jan 15, 2024</a></div>
508
+ <div class="tweet-stats">42 likes</div>
509
+ </div>
510
+ </body></html>`;
511
+ const REDDIT_HTML = `<html><body>
512
+ <div class="search-result">
513
+ <a class="search-title" href="https://www.reddit.com/r/programming/comments/abc/test_post/">Test Reddit Post</a>
514
+ <div class="search-result-body">This is the body of the reddit post.</div>
515
+ </div>
516
+ </body></html>`;
517
+ const MEDIUM_HTML = `<html><body>
518
+ <article>
519
+ <h2><a href="/programming/test-article">Medium Test Article</a></h2>
520
+ <p>This is the article description snippet.</p>
521
+ <a rel="author">Jane Doe</a>
522
+ <span aria-label="5 min read">5 min read</span>
523
+ </article>
524
+ </body></html>`;
525
+ const SOUNDCLOUD_HTML = `<html><body>
526
+ <article class="searchList__item">
527
+ <a itemprop="url" href="/artist/lofi-track">
528
+ <span itemprop="name">Lo-Fi Track</span>
529
+ <span itemprop="byArtist">ChillArtist</span>
530
+ <span class="sc-ministats-plays">50K</span>
531
+ <time itemprop="duration">3:45</time>
532
+ </article>
533
+ </body></html>`;
534
+ const MASTODON_JSON = { accounts: [{ uri: "https://mastodon.social/@testuser", username: "testuser", display_name: "Test User", followers_count: 1000, note: "<p>This is a test mastodon bio</p>", avatar: "https://files.mastodon.social/avatar.jpg" }] };
535
+
536
+ // ---- Maps ----
537
+ const OPENSTREETMAP_JSON = [{ display_name: "New York City, United States", osm_type: "relation", osm_id: "175905", type: "city", class: "place" }];
538
+ const PHOTON_JSON = { features: [{ geometry: { type: "Point", coordinates: [-74.006, 40.7128] }, properties: { name: "New York City", osm_type: "R", osm_id: 175905, city: "New York City", country: "United States", type: "city" } }] };
539
+ // apple_maps is 3-step: DDG token → Apple bootstrap → search
540
+ const APPLE_MAPS_TOKEN_TEXT = "duckduckgo_token_abc123";
541
+ const APPLE_MAPS_BOOTSTRAP_JSON = { authInfo: { access_token: "apple_mapkit_token_xyz789" } };
542
+ const APPLE_MAPS_SEARCH_JSON = { results: [{ name: "Tokyo Tower", placecardUrl: "https://maps.apple.com/?q=Tokyo+Tower", center: { lat: 35.6586, lng: 139.7454 }, locality: "Tokyo", country: "Japan", poiCategory: "Landmark" }] };
543
+
544
+ // ---- Shopping ----
545
+ const EBAY_HTML = `<html><body>
546
+ <li class="s-item">
547
+ <a class="s-item__link" href="https://www.ebay.com/itm/123456">
548
+ <h3 class="s-item__title">Test Laptop Computer</h3>
549
+ </a>
550
+ <span class="s-item__price">$499.99</span>
551
+ <span class="s-item__shipping">Free shipping</span>
552
+ </li>
553
+ </body></html>`;
554
+
555
+ // ---- Specialized ----
556
+ const WIKIPEDIA_JSON = { query: { search: [{ title: "JavaScript", snippet: "JavaScript is a programming language.", pageid: 9845 }] } };
557
+ const IMDB_HTML = `<html><body>
558
+ <li class="ipc-metadata-list-summary-item">
559
+ <a class="ipc-metadata-list-summary-item__t" href="/title/tt1375666/?ref_=fn_all_ttl_1">Inception</a>
560
+ <li class="ipc-metadata-list-summary-item__li">2010</li>
561
+ <img src="https://m.media-amazon.com/images/M/MV5BMjAxM.jpg">
562
+ </li>
563
+ </body></html>`;
564
+ const GENIUS_JSON = { response: { sections: [{ type: "song", hits: [{ result: { url: "https://genius.com/Queen-bohemian-rhapsody-lyrics", full_title: "Bohemian Rhapsody by Queen", artist_names: "Queen", song_art_image_thumbnail_url: "https://images.genius.com/queen.jpg" } }] }] } };
565
+ const ARCHIVE_JSON = { response: { docs: [{ identifier: "python-tutorial", title: "Python Tutorial", description: "A comprehensive Python tutorial", mediatype: "texts", downloads: 5000 }] } };
566
+ const OPENLIBRARY_JSON = { docs: [{ key: "/works/OL12345W", title: "The Lord of the Rings", author_name: ["J.R.R. Tolkien"], first_publish_year: 1954, isbn: ["9780261102354"], lending_identifier_s: "lordoftherings", first_sentence: ["In a hole in the ground there lived a hobbit."] }] };
567
+ const WTTR_JSON = {
568
+ current_condition: [{ weatherCode: "113", temp_C: "20", tempC: "20", FeelsLikeC: "18", humidity: "65", windspeedKmph: "15", winddirDegree: "180", pressure: "1013", cloudcover: "10" }],
569
+ nearest_area: [{ areaName: [{ value: "London" }] }],
570
+ weather: [{ date: "2024-01-15", hourly: [{ tempC: "15", weatherCode: "116", humidity: "70", windspeedKmph: "20" }] }],
571
+ };
572
+ const ANNAS_ARCHIVE_HTML = `<html><body>
573
+ <main>
574
+ <div class="js-aarecord-list-outer">
575
+ <div>
576
+ <a href="/md5/abc123def456">Python Programming Book</a>
577
+ <a href="/search?q=Guido+van+Rossum">Guido van Rossum</a>
578
+ <a href="/search?q=OReilly">O'Reilly Media</a>
579
+ <div class="relative">A comprehensive Python programming book covering all aspects.</div>
580
+ <img src="https://cover.annas-archive.gl/abc123.jpg">
581
+ </div>
582
+ </div>
583
+ </main>
584
+ </body></html>`;
585
+ const GOODREADS_HTML = `<html><body>
586
+ <table>
587
+ <tr>
588
+ <td>
589
+ <a class="bookTitle" href="/book/show/44767458-dune">Dune</a>
590
+ <img class="bookCover" src="https://i.gr-assets.com/images/S/dune.jpg">
591
+ <a class="authorName">Frank Herbert</a>
592
+ <span class="uitext">avg rating 4.25</span>
593
+ </td>
594
+ </tr>
595
+ </table>
596
+ </body></html>`;
597
+
598
+ // ---------------------------------------------------------------------------
599
+ // URL-pattern dispatcher — returns the right mock for each URL
600
+ // ---------------------------------------------------------------------------
601
+
602
+ function getMockForUrl(url: string, options?: RequestInit): Response {
603
+ const u = url.toString();
604
+
605
+ // ---- general ----
606
+ if (u.includes("google.com/search") && !u.includes("tbm=isch") && !u.includes("news.google.com")) return makeMockResponse({ html: GOOGLE_HTML });
607
+ if (u.includes("bing.com/search")) return makeMockResponse({ html: BING_HTML });
608
+ if (u.includes("duckduckgo.com/html")) return makeMockResponse({ html: DUCKDUCKGO_HTML });
609
+ if (u.includes("search.yahoo.com/search")) return makeMockResponse({ html: YAHOO_HTML });
610
+ if (u.includes("api.qwant.com")) return makeMockResponse({ json: { data: { result: { items: [{ url: "https://example.com", title: "Qwant Title", desc: "Qwant description." }] } } } });
611
+ if (u.includes("startpage.com/sp/search")) return makeMockResponse({ html: STARTPAGE_HTML });
612
+ if (u.includes("search.brave.com")) return makeMockResponse({ html: BRAVE_HTML });
613
+ if (u.includes("yandex.com/search")) return makeMockResponse({ html: YANDEX_HTML });
614
+ if (u.includes("baidu.com/s")) return makeMockResponse({ html: BAIDU_HTML });
615
+ if (u.includes("mojeek.com/search")) return makeMockResponse({ html: MOJEEK_HTML });
616
+
617
+ // ---- it ----
618
+ if (u.includes("api.github.com/search/repositories")) return makeMockResponse({ json: GITHUB_JSON });
619
+ if (u.includes("gitlab.com/api/v4/projects")) return makeMockResponse({ json: GITLAB_JSON });
620
+ if (u.includes("api.stackexchange.com")) return makeMockResponse({ json: STACKOVERFLOW_JSON });
621
+ if (u.includes("registry.npmjs.org/-/v1/search")) return makeMockResponse({ json: NPM_JSON });
622
+ if (u.includes("crates.io/api/v1/crates")) return makeMockResponse({ json: CRATES_JSON });
623
+ if (u.includes("hub.docker.com/api/search")) return makeMockResponse({ json: DOCKERHUB_JSON });
624
+ if (u.includes("pypi.org/search")) return makeMockResponse({ html: PYPI_HTML });
625
+ if (u.includes("packagist.org/search.json")) return makeMockResponse({ json: PACKAGIST_JSON });
626
+ if (u.includes("rubygems.org/api/v1/search")) return makeMockResponse({ json: RUBYGEMS_JSON });
627
+
628
+ // ---- images ----
629
+ if (u.includes("unsplash.com/napi/search/photos")) return makeMockResponse({ json: UNSPLASH_JSON });
630
+ if (u.includes("bing.com/images/async")) return makeMockResponse({ html: BING_IMAGES_HTML });
631
+ if (u.includes("google.com/search") && u.includes("tbm=isch")) return makeMockResponse({ html: GOOGLE_IMAGES_HTML });
632
+ if (u.includes("flickr.com/search")) return makeMockResponse({ html: FLICKR_HTML });
633
+ if (u.includes("imgur.com/search")) return makeMockResponse({ html: IMGUR_HTML });
634
+ if (u.includes("pixabay.com/images/search")) return makeMockResponse({ json: PIXABAY_JSON });
635
+ if (u.includes("wallhaven.cc/api/v1/search")) return makeMockResponse({ json: WALLHAVEN_JSON });
636
+ if (u.includes("deviantart.com/search")) return makeMockResponse({ html: DEVIANTART_HTML });
637
+ if (u.includes("openclipart.org/search")) return makeMockResponse({ html: OPENCLIPART_HTML });
638
+
639
+ // ---- videos ----
640
+ // youtube and invidious both use /api/v1/search on various Invidious instances
641
+ if (u.includes("/api/v1/search") && (
642
+ u.includes("invidious.nerdvpn.de") ||
643
+ u.includes("invidious.private.coffee") ||
644
+ u.includes("inv.nadeko.net") ||
645
+ u.includes("invidious.privacyredirect.com") ||
646
+ u.includes("yewtu.be") ||
647
+ u.includes("inv.riverside.rocks")
648
+ )) return makeMockResponse({ json: INVIDIOUS_JSON });
649
+ if (u.includes("vimeo.com/search")) return makeMockResponse({ html: VIMEO_HTML });
650
+ if (u.includes("api.dailymotion.com/videos")) return makeMockResponse({ json: DAILYMOTION_JSON });
651
+ if (u.includes("bing.com/videos/asyncv2")) return makeMockResponse({ html: BING_VIDEOS_HTML });
652
+ if (u.includes("peer.tube/api/v1/search/videos")) return makeMockResponse({ json: PEERTUBE_JSON });
653
+
654
+ // ---- news ----
655
+ if (u.includes("hn.algolia.com/api/v1/search")) return makeMockResponse({ json: HACKERNEWS_JSON });
656
+ if (u.includes("news.search.yahoo.com/search")) return makeMockResponse({ html: YAHOO_NEWS_HTML });
657
+ if (u.includes("bing.com/news/infinitescrollajax")) return makeMockResponse({ html: BING_NEWS_HTML });
658
+ if (u.includes("news.google.com/search")) return makeMockResponse({ html: GOOGLE_NEWS_HTML });
659
+
660
+ // ---- academic ----
661
+ if (u.includes("export.arxiv.org/api/query")) return makeMockResponse({ html: ARXIV_XML });
662
+ if (u.includes("scholar.google.com/scholar")) return makeMockResponse({ html: GOOGLE_SCHOLAR_HTML });
663
+ if (u.includes("wikidata.org/w/api.php")) return makeMockResponse({ json: WIKIDATA_JSON });
664
+ if (u.includes("semanticscholar.org/api/1/search")) return makeMockResponse({ json: SEMANTIC_SCHOLAR_JSON });
665
+ if (u.includes("api.crossref.org/works")) return makeMockResponse({ json: CROSSREF_JSON });
666
+ // pubmed two-step
667
+ if (u.includes("esearch.fcgi")) return makeMockResponse({ html: PUBMED_ESEARCH_XML });
668
+ if (u.includes("efetch.fcgi")) return makeMockResponse({ html: PUBMED_EFETCH_XML });
669
+ if (u.includes("api.openalex.org/works")) return makeMockResponse({ json: OPENALEX_JSON });
670
+ if (u.includes("doaj.org/api/v2/search/articles")) return makeMockResponse({ json: DOAJ_JSON });
671
+ if (u.includes("api.core.ac.uk/v3/search/works")) return makeMockResponse({ json: CORE_JSON });
672
+
673
+ // ---- torrents ----
674
+ if (u.includes("1337x.to/search/")) return makeMockResponse({ html: TORRENT_1337X_HTML });
675
+ if (u.includes("thepiratebay.org/search.php")) return makeMockResponse({ html: THEPIRATEBAY_HTML });
676
+ if (u.includes("nyaa.si/")) return makeMockResponse({ html: NYAA_HTML });
677
+ if (u.includes("yts.mx/api/v2/list_movies.json")) return makeMockResponse({ json: YTS_JSON });
678
+ if (u.includes("eztv.re/search/")) return makeMockResponse({ html: EZTV_HTML });
679
+ if (u.includes("solidtorrents.to/search")) return makeMockResponse({ html: SOLIDTORRENTS_HTML });
680
+ if (u.includes("kickasstorrents.to/usearch/")) return makeMockResponse({ html: KICKASS_HTML });
681
+
682
+ // ---- social ----
683
+ if (u.includes("nitter.net/search")) return makeMockResponse({ html: TWITTER_HTML });
684
+ if (u.includes("old.reddit.com/search")) return makeMockResponse({ html: REDDIT_HTML });
685
+ if (u.includes("medium.com/search")) return makeMockResponse({ html: MEDIUM_HTML });
686
+ if (u.includes("soundcloud.com/search")) return makeMockResponse({ html: SOUNDCLOUD_HTML });
687
+ if (u.includes("mastodon.social/api/v2/search")) return makeMockResponse({ json: MASTODON_JSON });
688
+
689
+ // ---- maps ----
690
+ if (u.includes("nominatim.openstreetmap.org/search")) return makeMockResponse({ json: OPENSTREETMAP_JSON });
691
+ if (u.includes("photon.komoot.io/api")) return makeMockResponse({ json: PHOTON_JSON });
692
+ // apple_maps 3-step
693
+ if (u.includes("duckduckgo.com/local.js")) return makeMockResponse({ html: APPLE_MAPS_TOKEN_TEXT });
694
+ if (u.includes("cdn.apple-mapkit.com/ma/bootstrap")) return makeMockResponse({ json: APPLE_MAPS_BOOTSTRAP_JSON });
695
+ if (u.includes("api.apple-mapkit.com/v1/search")) return makeMockResponse({ json: APPLE_MAPS_SEARCH_JSON });
696
+
697
+ // ---- shopping ----
698
+ if (u.includes("ebay.com/sch/i.html")) return makeMockResponse({ html: EBAY_HTML });
699
+
700
+ // ---- specialized ----
701
+ if (u.includes("en.wikipedia.org/w/api.php")) return makeMockResponse({ json: WIKIPEDIA_JSON });
702
+ if (u.includes("www.imdb.com/find")) return makeMockResponse({ html: IMDB_HTML });
703
+ if (u.includes("genius.com/api/search/multi")) return makeMockResponse({ json: GENIUS_JSON });
704
+ if (u.includes("archive.org/advancedsearch.php")) return makeMockResponse({ json: ARCHIVE_JSON });
705
+ if (u.includes("openlibrary.org/search.json")) return makeMockResponse({ json: OPENLIBRARY_JSON });
706
+ if (u.includes("wttr.in/")) return makeMockResponse({ json: WTTR_JSON });
707
+ if (u.includes("annas-archive")) return makeMockResponse({ html: ANNAS_ARCHIVE_HTML });
708
+ if (u.includes("goodreads.com/search")) return makeMockResponse({ html: GOODREADS_HTML });
709
+
710
+ // fallback — unknown URL
711
+ console.warn("[test mock] unmatched URL:", u);
712
+ return makeMockResponse({ ok: false, status: 404, html: "Not Found" });
713
+ }
714
+
715
+ // ---------------------------------------------------------------------------
716
+ // Test suite
717
+ // ---------------------------------------------------------------------------
718
+
719
+ describe("Engine Unit Tests (mocked fetch)", () => {
720
+ let fetchSpy: ReturnType<typeof vi.spyOn>;
721
+
722
+ beforeEach(() => {
723
+ fetchSpy = vi.spyOn(globalThis, "fetch").mockImplementation(
724
+ (input: RequestInfo | URL, init?: RequestInit) => {
725
+ const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : (input as Request).url;
726
+ return Promise.resolve(getMockForUrl(url, init));
727
+ }
728
+ );
729
+ });
730
+
731
+ afterEach(() => {
732
+ fetchSpy.mockRestore();
733
+ });
734
+
735
+ // ---- general ----
736
+ describe("general", () => {
737
+ it("google returns results", async () => {
738
+ const r = await google("typescript", 1);
739
+ assertResults(r, "google");
740
+ });
741
+ it("bing returns results", async () => {
742
+ const r = await bing("typescript", 1);
743
+ assertResults(r, "bing");
744
+ });
745
+ it("duckduckgo returns results", async () => {
746
+ const r = await duckduckgo("typescript", 1);
747
+ assertResults(r, "duckduckgo");
748
+ });
749
+ it("yahoo returns results", async () => {
750
+ const r = await yahoo("typescript", 1);
751
+ assertResults(r, "yahoo");
752
+ });
753
+ it("qwant returns results", async () => {
754
+ const r = await qwant("typescript", 1);
755
+ assertResults(r, "qwant");
756
+ });
757
+ it("startpage returns results", async () => {
758
+ const r = await startpage("typescript", 1);
759
+ assertResults(r, "startpage");
760
+ });
761
+ it("brave returns results", async () => {
762
+ const r = await brave("typescript", 1);
763
+ assertResults(r, "brave");
764
+ });
765
+ it("yandex returns results", async () => {
766
+ const r = await yandex("typescript", 1);
767
+ assertResults(r, "yandex");
768
+ });
769
+ it("baidu returns results", async () => {
770
+ const r = await baidu("typescript", 1);
771
+ assertResults(r, "baidu");
772
+ });
773
+ it("mojeek returns results", async () => {
774
+ const r = await mojeek("typescript", 1);
775
+ assertResults(r, "mojeek");
776
+ });
777
+
778
+ it("bing returns [] on 404", async () => {
779
+ fetchSpy.mockImplementation(() => Promise.resolve(makeMockResponse({ ok: false, status: 404, html: "" })));
780
+ const r = await bing("test", 1);
781
+ expect(r).toEqual([]);
782
+ });
783
+ it("google returns [] on 404", async () => {
784
+ fetchSpy.mockImplementation(() => Promise.resolve(makeMockResponse({ ok: false, status: 404, html: "" })));
785
+ const r = await google("test", 1);
786
+ expect(r).toEqual([]);
787
+ });
788
+ });
789
+
790
+ // ---- IT ----
791
+ describe("it", () => {
792
+ it("github returns results", async () => {
793
+ const r = await github("typescript", 1);
794
+ assertResults(r, "github");
795
+ });
796
+ it("gitlab returns results", async () => {
797
+ const r = await gitlab("typescript", 1);
798
+ assertResults(r, "gitlab");
799
+ });
800
+ it("stackoverflow returns results", async () => {
801
+ const r = await stackoverflow("typescript", 1);
802
+ assertResults(r, "stackoverflow");
803
+ });
804
+ it("npm returns results", async () => {
805
+ const r = await npm("express", 1);
806
+ assertResults(r, "npm");
807
+ });
808
+ it("crates returns results", async () => {
809
+ const r = await crates("serde", 1);
810
+ assertResults(r, "crates");
811
+ });
812
+ it("dockerhub returns results", async () => {
813
+ const r = await dockerhub("nginx", 1);
814
+ assertResults(r, "dockerhub");
815
+ });
816
+ it("pypi returns results", async () => {
817
+ const r = await pypi("requests", 1);
818
+ assertResults(r, "pypi");
819
+ });
820
+ it("packagist returns results", async () => {
821
+ const r = await packagist("laravel", 1);
822
+ assertResults(r, "packagist");
823
+ });
824
+ it("rubygems returns results", async () => {
825
+ const r = await rubygems("rails", 1);
826
+ assertResults(r, "rubygems");
827
+ });
828
+
829
+ it("github returns [] on 404", async () => {
830
+ fetchSpy.mockImplementation(() => Promise.resolve(makeMockResponse({ ok: false, status: 404, json: {} })));
831
+ const r = await github("test", 1);
832
+ expect(r).toEqual([]);
833
+ });
834
+ });
835
+
836
+ // ---- images ----
837
+ describe("images", () => {
838
+ it("unsplash returns results", async () => {
839
+ const r = await unsplash("sunset", 1);
840
+ assertResults(r, "unsplash");
841
+ });
842
+ it("bing_images returns results", async () => {
843
+ const r = await bing_images("sunset", 1);
844
+ assertResults(r, "bing_images");
845
+ });
846
+ it("google_images returns results", async () => {
847
+ const r = await google_images("sunset", 1);
848
+ assertResults(r, "google_images");
849
+ });
850
+ it("flickr returns results", async () => {
851
+ const r = await flickr("sunset", 1);
852
+ assertResults(r, "flickr");
853
+ });
854
+ it("imgur returns results", async () => {
855
+ const r = await imgur("cats", 1);
856
+ assertResults(r, "imgur");
857
+ });
858
+ it("pixabay returns results", async () => {
859
+ const r = await pixabay("mountains", 1);
860
+ assertResults(r, "pixabay");
861
+ });
862
+ it("wallhaven returns results", async () => {
863
+ const r = await wallhaven("landscape", 1);
864
+ assertResults(r, "wallhaven");
865
+ });
866
+ it("deviantart returns results", async () => {
867
+ const r = await deviantart("art", 1);
868
+ assertResults(r, "deviantart");
869
+ });
870
+ it("openclipart returns results", async () => {
871
+ const r = await openclipart("arrow", 1);
872
+ assertResults(r, "openclipart");
873
+ });
874
+
875
+ it("unsplash returns [] on 404", async () => {
876
+ fetchSpy.mockImplementation(() => Promise.resolve(makeMockResponse({ ok: false, status: 404, json: {} })));
877
+ const r = await unsplash("test", 1);
878
+ expect(r).toEqual([]);
879
+ });
880
+ });
881
+
882
+ // ---- videos ----
883
+ describe("videos", () => {
884
+ it("youtube returns results", async () => {
885
+ const r = await youtube("tutorial", 1);
886
+ assertResults(r, "youtube");
887
+ });
888
+ it("vimeo returns results", async () => {
889
+ const r = await vimeo("documentary", 1);
890
+ assertResults(r, "vimeo");
891
+ });
892
+ it("dailymotion returns results", async () => {
893
+ const r = await dailymotion("music", 1);
894
+ assertResults(r, "dailymotion");
895
+ });
896
+ it("invidious returns results", async () => {
897
+ const r = await invidious("linux", 1);
898
+ assertResults(r, "invidious");
899
+ });
900
+ it("peertube returns results", async () => {
901
+ const r = await peertube("linux", 1);
902
+ assertResults(r, "peertube");
903
+ });
904
+ it("bing_videos returns results", async () => {
905
+ const r = await bing_videos("tutorial", 1);
906
+ assertResults(r, "bing_videos");
907
+ });
908
+
909
+ it("dailymotion returns [] on 404", async () => {
910
+ fetchSpy.mockImplementation(() => Promise.resolve(makeMockResponse({ ok: false, status: 404, json: {} })));
911
+ const r = await dailymotion("test", 1);
912
+ expect(r).toEqual([]);
913
+ });
914
+ });
915
+
916
+ // ---- news ----
917
+ describe("news", () => {
918
+ it("hackernews returns results", async () => {
919
+ const r = await hackernews("technology", 1);
920
+ assertResults(r, "hackernews");
921
+ });
922
+ it("yahoo_news returns results", async () => {
923
+ const r = await yahoo_news("technology", 1);
924
+ assertResults(r, "yahoo_news");
925
+ });
926
+ it("bing_news returns results", async () => {
927
+ const r = await bing_news("technology", 1);
928
+ assertResults(r, "bing_news");
929
+ });
930
+ it("google_news returns results", async () => {
931
+ const r = await google_news("technology", 1);
932
+ assertResults(r, "google_news");
933
+ });
934
+
935
+ it("hackernews returns [] on 404", async () => {
936
+ fetchSpy.mockImplementation(() => Promise.resolve(makeMockResponse({ ok: false, status: 404, json: {} })));
937
+ const r = await hackernews("test", 1);
938
+ expect(r).toEqual([]);
939
+ });
940
+ });
941
+
942
+ // ---- academic ----
943
+ describe("academic", () => {
944
+ it("arxiv returns results", async () => {
945
+ const r = await arxiv("neural+networks", 1);
946
+ assertResults(r, "arxiv");
947
+ });
948
+ it("google_scholar returns results", async () => {
949
+ const r = await google_scholar("machine learning", 1);
950
+ assertResults(r, "google_scholar");
951
+ });
952
+ it("wikidata returns results", async () => {
953
+ const r = await wikidata("python", 1);
954
+ assertResults(r, "wikidata");
955
+ });
956
+ it("semantic_scholar returns results", async () => {
957
+ const r = await semantic_scholar("deep learning", 1);
958
+ assertResults(r, "semantic_scholar");
959
+ });
960
+ it("crossref returns results", async () => {
961
+ const r = await crossref("quantum computing", 1);
962
+ assertResults(r, "crossref");
963
+ });
964
+ it("pubmed returns results (two-step)", async () => {
965
+ const r = await pubmed("cancer", 1);
966
+ assertResults(r, "pubmed");
967
+ });
968
+ it("openalex returns results", async () => {
969
+ const r = await openalex("machine learning", 1);
970
+ assertResults(r, "openalex");
971
+ });
972
+ it("doaj returns results", async () => {
973
+ const r = await doaj("machine learning", 1);
974
+ assertResults(r, "doaj");
975
+ });
976
+
977
+ it("core returns [] without CORE_API_KEY", async () => {
978
+ const saved = process.env.CORE_API_KEY;
979
+ delete process.env.CORE_API_KEY;
980
+ const r = await core("machine learning", 1);
981
+ expect(r).toEqual([]);
982
+ if (saved !== undefined) process.env.CORE_API_KEY = saved;
983
+ });
984
+ it("core returns results with CORE_API_KEY", async () => {
985
+ process.env.CORE_API_KEY = "test-api-key";
986
+ const r = await core("machine learning", 1);
987
+ assertResults(r, "core");
988
+ delete process.env.CORE_API_KEY;
989
+ });
990
+
991
+ it("arxiv returns [] on 404", async () => {
992
+ fetchSpy.mockImplementation(() => Promise.resolve(makeMockResponse({ ok: false, status: 404, html: "" })));
993
+ const r = await arxiv("test", 1);
994
+ expect(r).toEqual([]);
995
+ });
996
+ it("pubmed returns [] if esearch returns 404", async () => {
997
+ fetchSpy.mockImplementation(() => Promise.resolve(makeMockResponse({ ok: false, status: 404, html: "" })));
998
+ const r = await pubmed("test", 1);
999
+ expect(r).toEqual([]);
1000
+ });
1001
+ });
1002
+
1003
+ // ---- torrents ----
1004
+ describe("torrents", () => {
1005
+ it("1337x returns results", async () => {
1006
+ const r = await torrent_1337x("ubuntu", 1);
1007
+ assertResults(r, "1337x");
1008
+ });
1009
+ it("thepiratebay returns results", async () => {
1010
+ const r = await thepiratebay("ubuntu", 1);
1011
+ assertResults(r, "thepiratebay");
1012
+ });
1013
+ it("nyaa returns results", async () => {
1014
+ const r = await nyaa("one piece", 1);
1015
+ assertResults(r, "nyaa");
1016
+ });
1017
+ it("yts returns results", async () => {
1018
+ const r = await yts("inception", 1);
1019
+ assertResults(r, "yts");
1020
+ });
1021
+ it("eztv returns results", async () => {
1022
+ const r = await eztv("breaking bad", 1);
1023
+ assertResults(r, "eztv");
1024
+ });
1025
+ it("solidtorrents returns results", async () => {
1026
+ const r = await solidtorrents("ubuntu", 1);
1027
+ assertResults(r, "solidtorrents");
1028
+ });
1029
+ it("kickass returns results", async () => {
1030
+ const r = await kickass("ubuntu", 1);
1031
+ assertResults(r, "kickass");
1032
+ });
1033
+
1034
+ it("yts returns [] on 404", async () => {
1035
+ fetchSpy.mockImplementation(() => Promise.resolve(makeMockResponse({ ok: false, status: 404, json: {} })));
1036
+ const r = await yts("test", 1);
1037
+ expect(r).toEqual([]);
1038
+ });
1039
+ });
1040
+
1041
+ // ---- social ----
1042
+ describe("social", () => {
1043
+ it("twitter returns results", async () => {
1044
+ const r = await twitter("programming", 1);
1045
+ assertResults(r, "twitter");
1046
+ });
1047
+ it("reddit returns results", async () => {
1048
+ const r = await reddit("programming", 1);
1049
+ assertResults(r, "reddit");
1050
+ });
1051
+ it("medium returns results", async () => {
1052
+ const r = await medium("javascript", 1);
1053
+ assertResults(r, "medium");
1054
+ });
1055
+ it("soundcloud returns results", async () => {
1056
+ const r = await soundcloud("lofi", 1);
1057
+ assertResults(r, "soundcloud");
1058
+ });
1059
+ it("mastodon returns results", async () => {
1060
+ const r = await mastodon("programming", 1);
1061
+ assertResults(r, "mastodon");
1062
+ });
1063
+
1064
+ it("reddit returns [] on 404", async () => {
1065
+ fetchSpy.mockImplementation(() => Promise.resolve(makeMockResponse({ ok: false, status: 404, html: "" })));
1066
+ const r = await reddit("test", 1);
1067
+ expect(r).toEqual([]);
1068
+ });
1069
+ });
1070
+
1071
+ // ---- maps ----
1072
+ describe("maps", () => {
1073
+ it("openstreetmap returns results", async () => {
1074
+ const r = await openstreetmap("New York", 1);
1075
+ assertResults(r, "openstreetmap");
1076
+ });
1077
+ it("photon returns results", async () => {
1078
+ const r = await photon("Paris", 1);
1079
+ assertResults(r, "photon");
1080
+ });
1081
+ it("apple_maps returns results (three-step)", async () => {
1082
+ const r = await apple_maps("Tokyo", 1);
1083
+ assertResults(r, "apple_maps");
1084
+ });
1085
+
1086
+ it("openstreetmap returns [] on 404", async () => {
1087
+ fetchSpy.mockImplementation(() => Promise.resolve(makeMockResponse({ ok: false, status: 404, json: [] })));
1088
+ const r = await openstreetmap("test", 1);
1089
+ expect(r).toEqual([]);
1090
+ });
1091
+ });
1092
+
1093
+ // ---- shopping ----
1094
+ describe("shopping", () => {
1095
+ it("ebay returns results", async () => {
1096
+ const r = await ebay("laptop", 1);
1097
+ assertResults(r, "ebay");
1098
+ });
1099
+ it("ebay returns [] on 404", async () => {
1100
+ fetchSpy.mockImplementation(() => Promise.resolve(makeMockResponse({ ok: false, status: 404, html: "" })));
1101
+ const r = await ebay("test", 1);
1102
+ expect(r).toEqual([]);
1103
+ });
1104
+ });
1105
+
1106
+ // ---- specialized ----
1107
+ describe("specialized", () => {
1108
+ it("wikipedia returns results", async () => {
1109
+ const r = await wikipedia("javascript", 1);
1110
+ assertResults(r, "wikipedia");
1111
+ });
1112
+ it("imdb returns results", async () => {
1113
+ const r = await imdb("inception", 1);
1114
+ assertResults(r, "imdb");
1115
+ });
1116
+ it("genius returns results", async () => {
1117
+ const r = await genius("bohemian rhapsody", 1);
1118
+ assertResults(r, "genius");
1119
+ });
1120
+ it("archive returns results", async () => {
1121
+ const r = await archive("books", 1);
1122
+ assertResults(r, "archive");
1123
+ });
1124
+ it("openlibrary returns results", async () => {
1125
+ const r = await openlibrary("tolkien", 1);
1126
+ assertResults(r, "openlibrary");
1127
+ });
1128
+ it("wttr returns results", async () => {
1129
+ const r = await wttr("London", 1);
1130
+ assertResults(r, "wttr");
1131
+ });
1132
+ it("annas_archive returns results", async () => {
1133
+ const r = await annas_archive("python", 1);
1134
+ assertResults(r, "annas_archive");
1135
+ });
1136
+ it("goodreads returns results", async () => {
1137
+ const r = await goodreads("dune", 1);
1138
+ assertResults(r, "goodreads");
1139
+ });
1140
+
1141
+ it("wikipedia returns [] on 404", async () => {
1142
+ fetchSpy.mockImplementation(() => Promise.resolve(makeMockResponse({ ok: false, status: 404, json: {} })));
1143
+ const r = await wikipedia("test", 1);
1144
+ expect(r).toEqual([]);
1145
+ });
1146
+ it("wttr returns [] on 404", async () => {
1147
+ fetchSpy.mockImplementation(() => Promise.resolve(makeMockResponse({ ok: false, status: 404, json: {} })));
1148
+ const r = await wttr("test", 1);
1149
+ expect(r).toEqual([]);
1150
+ });
1151
+ });
1152
+ });