@link-assistant/web-capture 2.0.0 → 2.0.2
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.
- package/CHANGELOG.md +17 -0
- package/bin/web-capture.js +2 -5
- package/package.json +36 -23
- package/src/archive.js +0 -1
- package/src/binary-download.js +0 -1
- package/src/docx.js +13 -10
- package/src/fetch.js +0 -1
- package/src/gdocs-images.js +0 -1
- package/src/gdocs.js +0 -1
- package/src/github.js +0 -1
- package/src/lib.js +0 -1
- package/src/markdown.js +0 -1
- package/src/shared-dialog.js +0 -1
- package/src/stream.js +0 -1
- package/src/transport.js +23 -5
- package/src/txt.js +0 -1
- package/.changeset/README.md +0 -8
- package/.changeset/config.json +0 -11
- package/.dockerignore +0 -6
- package/.jscpd.json +0 -19
- package/.lenv +0 -8
- package/.prettierignore +0 -8
- package/.prettierrc +0 -10
- package/Dockerfile +0 -52
- package/docker-compose.yml +0 -11
- package/eslint.config.js +0 -112
- package/examples/js/capture_cache.js +0 -12
- package/examples/js/engine_comparison.js +0 -39
- package/examples/js/html_download.js +0 -23
- package/examples/js/image_download.js +0 -36
- package/examples/js/markdown_download.js +0 -23
- package/examples/js/playwright_example.js +0 -23
- package/examples/python/capture_client.py +0 -20
- package/examples/python/html_download.py +0 -3
- package/examples/python/image_download.py +0 -16
- package/examples/python/markdown_download.py +0 -3
- package/examples/python/playwright_example.py +0 -22
- package/jest.config.mjs +0 -17
- package/tests/ci/dependency-freshness.test.js +0 -74
- package/tests/ci/detect-code-changes.test.js +0 -187
- package/tests/ci/docker-install.test.js +0 -31
- package/tests/ci/rust-dockerfile.test.js +0 -54
- package/tests/ci/workflow-policy.test.js +0 -79
- package/tests/e2e/docker.test.js +0 -186
- package/tests/e2e/process.test.js +0 -215
- package/tests/fixtures/bold-coalesce-and-leakage.html +0 -27
- package/tests/fixtures/deepwiki/deepwiki-full-page.png +0 -0
- package/tests/fixtures/deepwiki/index.md +0 -1500
- package/tests/fixtures/deepwiki/part-1.md +0 -776
- package/tests/fixtures/habr-articles.js +0 -45
- package/tests/fixtures/heading-numbering.html +0 -19
- package/tests/fixtures/list-item-with-br.html +0 -8
- package/tests/fixtures/ol-continuous-numbering.html +0 -12
- package/tests/fixtures/paragraph-vs-line-break.html +0 -11
- package/tests/helpers/nock.js +0 -10
- package/tests/helpers/screenshot.js +0 -10
- package/tests/helpers/search-fixture.cjs +0 -17
- package/tests/helpers/temporary-directory.js +0 -12
- package/tests/integration/api-endpoints.test.js +0 -418
- package/tests/integration/archive-default-layout.test.js +0 -37
- package/tests/integration/browser-engines.test.js +0 -80
- package/tests/integration/browser-packaged-path.test.js +0 -45
- package/tests/integration/deepwiki-download.test.js +0 -144
- package/tests/integration/deepwiki-rendered-page.mock.test.js +0 -121
- package/tests/integration/formalai-contract.test.js +0 -214
- package/tests/integration/gdocs-public-doc.test.js +0 -315
- package/tests/integration/github-readme.test.js +0 -133
- package/tests/integration/google-drive-image.test.js +0 -246
- package/tests/integration/habr-article.test.js +0 -337
- package/tests/integration/search-endpoint.test.js +0 -88
- package/tests/integration/stackoverflow-download.test.js +0 -173
- package/tests/integration/wikipedia-download.test.js +0 -134
- package/tests/jest.setup.mjs +0 -7
- package/tests/mock/index.test.js +0 -225
- package/tests/unit/animation.test.js +0 -33
- package/tests/unit/batch.test.js +0 -114
- package/tests/unit/bold-coalesce-and-leakage.test.js +0 -54
- package/tests/unit/browser-launch-options.test.js +0 -42
- package/tests/unit/browser.test.js +0 -172
- package/tests/unit/cache.test.js +0 -182
- package/tests/unit/cli.test.js +0 -399
- package/tests/unit/convertGoogleDriveUrl.test.js +0 -89
- package/tests/unit/convertRelativeUrls.test.js +0 -34
- package/tests/unit/extract-images.test.js +0 -259
- package/tests/unit/fetch-html.test.js +0 -54
- package/tests/unit/figures.test.js +0 -74
- package/tests/unit/gdocs-export-headers.test.js +0 -28
- package/tests/unit/gdocs-preprocess.test.js +0 -36
- package/tests/unit/gdocs.test.js +0 -1490
- package/tests/unit/github.test.js +0 -156
- package/tests/unit/heading-image.test.js +0 -49
- package/tests/unit/heading-numbering.test.js +0 -28
- package/tests/unit/html2md-br-in-list-item.test.js +0 -23
- package/tests/unit/html2md-ol-numbering.test.js +0 -20
- package/tests/unit/html2md.test.js +0 -695
- package/tests/unit/image-mode-defaults.test.js +0 -54
- package/tests/unit/kreuzberg.test.js +0 -249
- package/tests/unit/latex.test.js +0 -123
- package/tests/unit/localize-images.test.js +0 -114
- package/tests/unit/markdown.test.js +0 -36
- package/tests/unit/metadata.test.js +0 -140
- package/tests/unit/package-runtime-dependencies.test.js +0 -74
- package/tests/unit/paragraph-vs-line-break.test.js +0 -56
- package/tests/unit/postprocess.test.js +0 -110
- package/tests/unit/proxy-headers.test.js +0 -100
- package/tests/unit/publish-verification.test.js +0 -54
- package/tests/unit/retry.test.js +0 -111
- package/tests/unit/search.test.js +0 -339
- package/tests/unit/shared-dialog.test.js +0 -173
- package/tests/unit/stackoverflow.test.js +0 -32
- package/tests/unit/stream.test.js +0 -83
- package/tests/unit/verify.test.js +0 -182
- package/tests/xpaste/xpaste-screenshot-markdown.mock.test.js +0 -375
- package/tests/xpaste/xpaste.e2e.test.js +0 -36
- package/tests/xpaste/xpaste.integration.test.js +0 -41
- package/tests/xpaste/xpaste.mock.test.js +0 -261
- package/tests/xpaste/xpaste.unit.test.js +0 -119
|
@@ -1,337 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Integration tests for Habr article downloads.
|
|
3
|
-
*
|
|
4
|
-
* Tests that the web-capture service can successfully download real-world content
|
|
5
|
-
* from Habr.com using both Puppeteer and Playwright engines across all three
|
|
6
|
-
* archived articles from link-foundation/meta-theory.
|
|
7
|
-
*
|
|
8
|
-
* These tests hit live servers. Set HABR_INTEGRATION=true to run them.
|
|
9
|
-
* In CI they are enabled via the workflow environment variable.
|
|
10
|
-
*
|
|
11
|
-
* Tests use retry with exponential backoff to handle transient network failures.
|
|
12
|
-
* Browser instances are shared within describe blocks to reduce startup overhead.
|
|
13
|
-
*
|
|
14
|
-
* Covers:
|
|
15
|
-
* - Markdown conversion (fast, no browser needed)
|
|
16
|
-
* - PNG and JPEG screenshots (light and dark themes)
|
|
17
|
-
* - Full-page captures
|
|
18
|
-
* - Viewport configuration
|
|
19
|
-
* - Engine comparison
|
|
20
|
-
* - Archive endpoint with documentFormat option (html/markdown)
|
|
21
|
-
*/
|
|
22
|
-
|
|
23
|
-
import { jest } from '@jest/globals';
|
|
24
|
-
import request from 'supertest';
|
|
25
|
-
import { app } from '../../src/index.js';
|
|
26
|
-
import { createBrowser } from '../../src/browser.js';
|
|
27
|
-
import { fetchHtml, convertHtmlToMarkdown } from '../../src/lib.js';
|
|
28
|
-
import { dismissPopups, scrollToLoadContent } from '../../src/popups.js';
|
|
29
|
-
import { retry } from '../../src/retry.js';
|
|
30
|
-
import { getAllArticles, HABR_ARTICLES } from '../fixtures/habr-articles.js';
|
|
31
|
-
|
|
32
|
-
const SKIP_LIVE =
|
|
33
|
-
!process.env.HABR_INTEGRATION || process.env.HABR_INTEGRATION === 'false';
|
|
34
|
-
const describeIfLive = SKIP_LIVE ? describe.skip : describe;
|
|
35
|
-
|
|
36
|
-
// These tests hit live servers and may take a while
|
|
37
|
-
jest.setTimeout(120000);
|
|
38
|
-
|
|
39
|
-
// Helper: navigate to URL with retry and exponential backoff
|
|
40
|
-
// Uses 'domcontentloaded' instead of 'networkidle0' because Habr pages have
|
|
41
|
-
// persistent ad/tracking network activity that prevents networkidle0 from firing.
|
|
42
|
-
async function navigateWithRetry(page, url) {
|
|
43
|
-
await retry(
|
|
44
|
-
async () => {
|
|
45
|
-
await page.goto(url, {
|
|
46
|
-
waitUntil: 'domcontentloaded',
|
|
47
|
-
timeout: 60000,
|
|
48
|
-
});
|
|
49
|
-
},
|
|
50
|
-
{
|
|
51
|
-
retries: 3,
|
|
52
|
-
baseDelay: 2000,
|
|
53
|
-
onRetry: (err, attempt, delay) => {
|
|
54
|
-
console.log(
|
|
55
|
-
`Navigation retry ${attempt} for ${url} after ${delay}ms: ${err.message}`
|
|
56
|
-
);
|
|
57
|
-
},
|
|
58
|
-
}
|
|
59
|
-
);
|
|
60
|
-
// Wait for page content and images to load after DOM is ready
|
|
61
|
-
await new Promise((r) => setTimeout(r, 5000));
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
describe('Habr Article Download Tests', () => {
|
|
65
|
-
// Use article 0.0.2 as the primary test article (English, most recent)
|
|
66
|
-
const primaryArticle = HABR_ARTICLES['0.0.2'];
|
|
67
|
-
|
|
68
|
-
describeIfLive('Markdown Download (fetch only)', () => {
|
|
69
|
-
it('downloads article 0.0.2 as markdown', async () => {
|
|
70
|
-
const html = await retry(() => fetchHtml(primaryArticle.url), {
|
|
71
|
-
retries: 3,
|
|
72
|
-
baseDelay: 2000,
|
|
73
|
-
});
|
|
74
|
-
expect(html.length).toBeGreaterThan(1000);
|
|
75
|
-
|
|
76
|
-
const markdown = convertHtmlToMarkdown(html, primaryArticle.url);
|
|
77
|
-
expect(markdown.length).toBeGreaterThan(100);
|
|
78
|
-
// Should contain markdown headings
|
|
79
|
-
expect(markdown).toMatch(/^#{1,3}\s/m);
|
|
80
|
-
// Should contain links
|
|
81
|
-
expect(markdown).toMatch(/\[.*?\]\(.*?\)/);
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
it('downloads all 3 articles as markdown', async () => {
|
|
85
|
-
for (const article of getAllArticles()) {
|
|
86
|
-
const html = await retry(() => fetchHtml(article.url), {
|
|
87
|
-
retries: 3,
|
|
88
|
-
baseDelay: 2000,
|
|
89
|
-
});
|
|
90
|
-
expect(html.length).toBeGreaterThan(1000);
|
|
91
|
-
|
|
92
|
-
const markdown = convertHtmlToMarkdown(html, article.url);
|
|
93
|
-
expect(markdown.length).toBeGreaterThan(100);
|
|
94
|
-
expect(markdown).toMatch(/^#{1,3}\s/m);
|
|
95
|
-
}
|
|
96
|
-
});
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
describeIfLive('Screenshots with Puppeteer', () => {
|
|
100
|
-
let browser;
|
|
101
|
-
let page;
|
|
102
|
-
|
|
103
|
-
beforeAll(async () => {
|
|
104
|
-
browser = await createBrowser('puppeteer');
|
|
105
|
-
page = await browser.newPage();
|
|
106
|
-
await page.setViewport({ width: 1280, height: 800 });
|
|
107
|
-
await navigateWithRetry(page, primaryArticle.url);
|
|
108
|
-
await dismissPopups(page);
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
afterAll(async () => {
|
|
112
|
-
if (browser) {
|
|
113
|
-
await browser.close();
|
|
114
|
-
}
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
it('captures article as PNG screenshot', async () => {
|
|
118
|
-
const buffer = await page.screenshot({ type: 'png' });
|
|
119
|
-
expect(buffer).toBeTruthy();
|
|
120
|
-
expect(buffer.length).toBeGreaterThan(10000);
|
|
121
|
-
// PNG signature: 137 80 78 71
|
|
122
|
-
expect(buffer[0]).toBe(137);
|
|
123
|
-
expect(buffer[1]).toBe(80);
|
|
124
|
-
expect(buffer[2]).toBe(78);
|
|
125
|
-
expect(buffer[3]).toBe(71);
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
it('captures article as JPEG screenshot', async () => {
|
|
129
|
-
const buffer = await page.screenshot({ type: 'jpeg', quality: 80 });
|
|
130
|
-
expect(buffer).toBeTruthy();
|
|
131
|
-
expect(buffer.length).toBeGreaterThan(5000);
|
|
132
|
-
// JPEG signature: 0xFF 0xD8 0xFF
|
|
133
|
-
expect(buffer[0]).toBe(0xff);
|
|
134
|
-
expect(buffer[1]).toBe(0xd8);
|
|
135
|
-
expect(buffer[2]).toBe(0xff);
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
it('captures full page screenshot', async () => {
|
|
139
|
-
await scrollToLoadContent(page);
|
|
140
|
-
|
|
141
|
-
const viewportBuffer = await page.screenshot({ type: 'png' });
|
|
142
|
-
const fullPageBuffer = await page.screenshot({
|
|
143
|
-
type: 'png',
|
|
144
|
-
fullPage: true,
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
// Full page should be significantly larger than viewport-only
|
|
148
|
-
expect(fullPageBuffer.length).toBeGreaterThan(viewportBuffer.length);
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
it('captures with custom viewport width (1920px)', async () => {
|
|
152
|
-
await page.setViewport({ width: 1920, height: 1080 });
|
|
153
|
-
const buffer = await page.screenshot({ type: 'png' });
|
|
154
|
-
expect(buffer).toBeTruthy();
|
|
155
|
-
expect(buffer.length).toBeGreaterThan(10000);
|
|
156
|
-
// Restore original viewport
|
|
157
|
-
await page.setViewport({ width: 1280, height: 800 });
|
|
158
|
-
});
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
describeIfLive('Screenshots with Playwright', () => {
|
|
162
|
-
let browser;
|
|
163
|
-
let page;
|
|
164
|
-
|
|
165
|
-
beforeAll(async () => {
|
|
166
|
-
browser = await createBrowser('playwright');
|
|
167
|
-
page = await browser.newPage();
|
|
168
|
-
await page.setViewport({ width: 1280, height: 800 });
|
|
169
|
-
await navigateWithRetry(page, primaryArticle.url);
|
|
170
|
-
await dismissPopups(page);
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
afterAll(async () => {
|
|
174
|
-
if (browser) {
|
|
175
|
-
await browser.close();
|
|
176
|
-
}
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
it('captures article as PNG screenshot', async () => {
|
|
180
|
-
const buffer = await page.screenshot({ type: 'png' });
|
|
181
|
-
expect(buffer).toBeTruthy();
|
|
182
|
-
expect(buffer.length).toBeGreaterThan(10000);
|
|
183
|
-
expect(buffer[0]).toBe(137);
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
it('captures article as JPEG screenshot', async () => {
|
|
187
|
-
const buffer = await page.screenshot({ type: 'jpeg', quality: 80 });
|
|
188
|
-
expect(buffer).toBeTruthy();
|
|
189
|
-
expect(buffer.length).toBeGreaterThan(5000);
|
|
190
|
-
expect(buffer[0]).toBe(0xff);
|
|
191
|
-
expect(buffer[1]).toBe(0xd8);
|
|
192
|
-
});
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
describeIfLive('Theme Support (Playwright)', () => {
|
|
196
|
-
it.each(['light', 'dark'])(
|
|
197
|
-
'captures %s theme screenshot',
|
|
198
|
-
async (theme) => {
|
|
199
|
-
const browser = await createBrowser('playwright', {
|
|
200
|
-
colorScheme: theme,
|
|
201
|
-
});
|
|
202
|
-
try {
|
|
203
|
-
const page = await browser.newPage();
|
|
204
|
-
await page.setViewport({ width: 1280, height: 800 });
|
|
205
|
-
await navigateWithRetry(page, primaryArticle.url);
|
|
206
|
-
await scrollToLoadContent(page);
|
|
207
|
-
await dismissPopups(page);
|
|
208
|
-
|
|
209
|
-
const buffer = await page.screenshot({
|
|
210
|
-
type: 'png',
|
|
211
|
-
fullPage: true,
|
|
212
|
-
});
|
|
213
|
-
expect(buffer).toBeTruthy();
|
|
214
|
-
expect(buffer.length).toBeGreaterThan(50000);
|
|
215
|
-
} finally {
|
|
216
|
-
await browser.close();
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
);
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
describeIfLive('All Three Articles (Puppeteer)', () => {
|
|
223
|
-
let browser;
|
|
224
|
-
|
|
225
|
-
beforeAll(async () => {
|
|
226
|
-
browser = await createBrowser('puppeteer');
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
afterAll(async () => {
|
|
230
|
-
if (browser) {
|
|
231
|
-
await browser.close();
|
|
232
|
-
}
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
it.each(getAllArticles().map((a) => [a.version, a.url]))(
|
|
236
|
-
'captures article %s as PNG screenshot',
|
|
237
|
-
async (version, url) => {
|
|
238
|
-
const page = await browser.newPage();
|
|
239
|
-
try {
|
|
240
|
-
await page.setViewport({ width: 1280, height: 800 });
|
|
241
|
-
await navigateWithRetry(page, url);
|
|
242
|
-
await dismissPopups(page);
|
|
243
|
-
|
|
244
|
-
const buffer = await page.screenshot({ type: 'png' });
|
|
245
|
-
expect(buffer.length).toBeGreaterThan(10000);
|
|
246
|
-
expect(buffer[0]).toBe(137);
|
|
247
|
-
} finally {
|
|
248
|
-
await page.close();
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
);
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
describeIfLive('Popup Dismissal (Playwright)', () => {
|
|
255
|
-
it('dismisses popups before capture', async () => {
|
|
256
|
-
const browser = await createBrowser('playwright');
|
|
257
|
-
try {
|
|
258
|
-
const page = await browser.newPage();
|
|
259
|
-
await page.setViewport({ width: 1280, height: 800 });
|
|
260
|
-
await navigateWithRetry(page, primaryArticle.url);
|
|
261
|
-
|
|
262
|
-
await dismissPopups(page);
|
|
263
|
-
|
|
264
|
-
const buffer = await page.screenshot({ type: 'png' });
|
|
265
|
-
expect(buffer.length).toBeGreaterThan(10000);
|
|
266
|
-
} finally {
|
|
267
|
-
await browser.close();
|
|
268
|
-
}
|
|
269
|
-
});
|
|
270
|
-
});
|
|
271
|
-
|
|
272
|
-
describeIfLive('Archive Endpoint (live)', () => {
|
|
273
|
-
it('creates markdown archive of article', async () => {
|
|
274
|
-
const res = await retry(
|
|
275
|
-
async () => {
|
|
276
|
-
const r = await request(app)
|
|
277
|
-
.get('/archive')
|
|
278
|
-
.query({
|
|
279
|
-
url: primaryArticle.url,
|
|
280
|
-
localImages: 'false',
|
|
281
|
-
documentFormat: 'markdown',
|
|
282
|
-
})
|
|
283
|
-
.buffer(true)
|
|
284
|
-
.parse((res, callback) => {
|
|
285
|
-
const chunks = [];
|
|
286
|
-
res.on('data', (chunk) => chunks.push(chunk));
|
|
287
|
-
res.on('end', () => callback(null, Buffer.concat(chunks)));
|
|
288
|
-
})
|
|
289
|
-
.timeout({ response: 30000 });
|
|
290
|
-
if (r.status !== 200) {
|
|
291
|
-
throw new Error(`Archive returned status ${r.status}`);
|
|
292
|
-
}
|
|
293
|
-
return r;
|
|
294
|
-
},
|
|
295
|
-
{ retries: 2, baseDelay: 3000 }
|
|
296
|
-
);
|
|
297
|
-
|
|
298
|
-
expect(res.body).toBeInstanceOf(Buffer);
|
|
299
|
-
expect(res.body.length).toBeGreaterThan(50);
|
|
300
|
-
// ZIP signature: PK (0x50 0x4B)
|
|
301
|
-
expect(res.body[0]).toBe(0x50);
|
|
302
|
-
expect(res.body[1]).toBe(0x4b);
|
|
303
|
-
});
|
|
304
|
-
|
|
305
|
-
it('creates HTML archive of article', async () => {
|
|
306
|
-
const res = await retry(
|
|
307
|
-
async () => {
|
|
308
|
-
const r = await request(app)
|
|
309
|
-
.get('/archive')
|
|
310
|
-
.query({
|
|
311
|
-
url: primaryArticle.url,
|
|
312
|
-
localImages: 'false',
|
|
313
|
-
documentFormat: 'html',
|
|
314
|
-
})
|
|
315
|
-
.buffer(true)
|
|
316
|
-
.parse((res, callback) => {
|
|
317
|
-
const chunks = [];
|
|
318
|
-
res.on('data', (chunk) => chunks.push(chunk));
|
|
319
|
-
res.on('end', () => callback(null, Buffer.concat(chunks)));
|
|
320
|
-
})
|
|
321
|
-
.timeout({ response: 30000 });
|
|
322
|
-
if (r.status !== 200) {
|
|
323
|
-
throw new Error(`Archive returned status ${r.status}`);
|
|
324
|
-
}
|
|
325
|
-
return r;
|
|
326
|
-
},
|
|
327
|
-
{ retries: 2, baseDelay: 3000 }
|
|
328
|
-
);
|
|
329
|
-
|
|
330
|
-
expect(res.body).toBeInstanceOf(Buffer);
|
|
331
|
-
expect(res.body.length).toBeGreaterThan(50);
|
|
332
|
-
// ZIP signature
|
|
333
|
-
expect(res.body[0]).toBe(0x50);
|
|
334
|
-
expect(res.body[1]).toBe(0x4b);
|
|
335
|
-
});
|
|
336
|
-
});
|
|
337
|
-
});
|
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Integration tests for the GET /search endpoint (issue #130).
|
|
3
|
-
*
|
|
4
|
-
* Provider HTTP calls are mocked with nock so the test is fast and offline.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import request from 'supertest';
|
|
8
|
-
import nock from '../helpers/nock.js';
|
|
9
|
-
import { app } from '../../src/index.js';
|
|
10
|
-
|
|
11
|
-
const WIKI_JSON = {
|
|
12
|
-
pages: [
|
|
13
|
-
{
|
|
14
|
-
id: 1,
|
|
15
|
-
key: 'Formal_methods',
|
|
16
|
-
title: 'Formal methods',
|
|
17
|
-
excerpt: 'the study of <b>formal</b> methods',
|
|
18
|
-
description: 'rigorous techniques',
|
|
19
|
-
},
|
|
20
|
-
],
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
afterEach(() => {
|
|
24
|
-
nock.cleanAll();
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
describe('GET /search', () => {
|
|
28
|
-
it('returns structured JSON for the wikipedia provider', async () => {
|
|
29
|
-
nock('https://en.wikipedia.org')
|
|
30
|
-
.get('/w/rest.php/v1/search/page')
|
|
31
|
-
.query(true)
|
|
32
|
-
.reply(200, WIKI_JSON);
|
|
33
|
-
|
|
34
|
-
const res = await request(app)
|
|
35
|
-
.get('/search')
|
|
36
|
-
.query({ q: 'formal-ai', provider: 'wikipedia', limit: 5 })
|
|
37
|
-
.expect(200)
|
|
38
|
-
.expect('Content-Type', /json/);
|
|
39
|
-
|
|
40
|
-
expect(res.body.query).toBe('formal-ai');
|
|
41
|
-
expect(res.body.provider).toBe('wikipedia');
|
|
42
|
-
expect(res.body.results).toHaveLength(1);
|
|
43
|
-
expect(res.body.results[0].url).toBe(
|
|
44
|
-
'https://en.wikipedia.org/wiki/Formal_methods'
|
|
45
|
-
);
|
|
46
|
-
expect(res.body.diagnostics.status).toBe(200);
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it('returns markdown when format=markdown', async () => {
|
|
50
|
-
nock('https://en.wikipedia.org')
|
|
51
|
-
.get('/w/rest.php/v1/search/page')
|
|
52
|
-
.query(true)
|
|
53
|
-
.reply(200, WIKI_JSON);
|
|
54
|
-
|
|
55
|
-
const res = await request(app)
|
|
56
|
-
.get('/search')
|
|
57
|
-
.query({ q: 'formal-ai', provider: 'wikipedia', format: 'markdown' })
|
|
58
|
-
.expect(200)
|
|
59
|
-
.expect('Content-Type', /markdown/);
|
|
60
|
-
|
|
61
|
-
expect(res.text).toContain('# Search results for "formal-ai"');
|
|
62
|
-
expect(res.text).toContain('[Formal methods]');
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it('defaults to the wikipedia provider', async () => {
|
|
66
|
-
nock('https://en.wikipedia.org')
|
|
67
|
-
.get('/w/rest.php/v1/search/page')
|
|
68
|
-
.query(true)
|
|
69
|
-
.reply(200, WIKI_JSON);
|
|
70
|
-
|
|
71
|
-
const res = await request(app)
|
|
72
|
-
.get('/search')
|
|
73
|
-
.query({ q: 'formal-ai' })
|
|
74
|
-
.expect(200);
|
|
75
|
-
expect(res.body.provider).toBe('wikipedia');
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
it('rejects a missing query', async () => {
|
|
79
|
-
await request(app).get('/search').expect(400);
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
it('rejects an unknown provider', async () => {
|
|
83
|
-
await request(app)
|
|
84
|
-
.get('/search')
|
|
85
|
-
.query({ q: 'x', provider: 'yahoo' })
|
|
86
|
-
.expect(400);
|
|
87
|
-
});
|
|
88
|
-
});
|
|
@@ -1,173 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Integration tests for downloading the StackOverflow page from issue #11.
|
|
3
|
-
*
|
|
4
|
-
* These tests hit the live StackOverflow page:
|
|
5
|
-
* https://stackoverflow.com/questions/927358/how-do-i-undo-the-most-recent-local-commits-in-git
|
|
6
|
-
*
|
|
7
|
-
* Set STACKOVERFLOW_INTEGRATION=true to run them. The suite is skipped by
|
|
8
|
-
* default so offline/local test runs stay deterministic.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { jest } from '@jest/globals';
|
|
12
|
-
import request from 'supertest';
|
|
13
|
-
import unzipper from 'unzipper';
|
|
14
|
-
import { app } from '../../src/index.js';
|
|
15
|
-
import { retry } from '../../src/retry.js';
|
|
16
|
-
|
|
17
|
-
const STACKOVERFLOW_URL =
|
|
18
|
-
'https://stackoverflow.com/questions/927358/how-do-i-undo-the-most-recent-local-commits-in-git';
|
|
19
|
-
const STACKOVERFLOW_TITLE =
|
|
20
|
-
'How do I undo the most recent local commits in Git';
|
|
21
|
-
|
|
22
|
-
const PNG_SIGNATURE = Buffer.from([
|
|
23
|
-
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a,
|
|
24
|
-
]);
|
|
25
|
-
const ZIP_SIGNATURE = Buffer.from([0x50, 0x4b]);
|
|
26
|
-
const PDF_SIGNATURE = Buffer.from([0x25, 0x50, 0x44, 0x46]);
|
|
27
|
-
|
|
28
|
-
const SKIP_LIVE =
|
|
29
|
-
!process.env.STACKOVERFLOW_INTEGRATION ||
|
|
30
|
-
process.env.STACKOVERFLOW_INTEGRATION === 'false';
|
|
31
|
-
const describeIfLive = SKIP_LIVE ? describe.skip : describe;
|
|
32
|
-
|
|
33
|
-
jest.setTimeout(180000);
|
|
34
|
-
|
|
35
|
-
function parseBinary(res, callback) {
|
|
36
|
-
const chunks = [];
|
|
37
|
-
res.on('data', (chunk) => chunks.push(Buffer.from(chunk)));
|
|
38
|
-
res.on('end', () => callback(null, Buffer.concat(chunks)));
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
async function getText(path, query = {}) {
|
|
42
|
-
return await retry(
|
|
43
|
-
() =>
|
|
44
|
-
request(app)
|
|
45
|
-
.get(path)
|
|
46
|
-
.query({ url: STACKOVERFLOW_URL, ...query })
|
|
47
|
-
.expect(200),
|
|
48
|
-
{
|
|
49
|
-
retries: 2,
|
|
50
|
-
baseDelay: 2000,
|
|
51
|
-
onRetry: (err, attempt, delay) => {
|
|
52
|
-
console.log(
|
|
53
|
-
`Retry ${attempt} for ${path} after ${delay}ms: ${err.message}`
|
|
54
|
-
);
|
|
55
|
-
},
|
|
56
|
-
}
|
|
57
|
-
);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
async function getBinary(path, query = {}) {
|
|
61
|
-
return await retry(
|
|
62
|
-
() =>
|
|
63
|
-
request(app)
|
|
64
|
-
.get(path)
|
|
65
|
-
.query({ url: STACKOVERFLOW_URL, ...query })
|
|
66
|
-
.buffer(true)
|
|
67
|
-
.parse(parseBinary)
|
|
68
|
-
.expect(200),
|
|
69
|
-
{
|
|
70
|
-
retries: 2,
|
|
71
|
-
baseDelay: 2000,
|
|
72
|
-
onRetry: (err, attempt, delay) => {
|
|
73
|
-
console.log(
|
|
74
|
-
`Retry ${attempt} for ${path} after ${delay}ms: ${err.message}`
|
|
75
|
-
);
|
|
76
|
-
},
|
|
77
|
-
}
|
|
78
|
-
);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function expectStackOverflowContent(text) {
|
|
82
|
-
expect(text).toContain(STACKOVERFLOW_TITLE);
|
|
83
|
-
expect(text).toMatch(/Stack Overflow/i);
|
|
84
|
-
expect(text).toMatch(/\bgit\b/i);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function expectBufferWithSignature(buffer, signature) {
|
|
88
|
-
expect(Buffer.isBuffer(buffer)).toBe(true);
|
|
89
|
-
expect(buffer.length).toBeGreaterThan(1000);
|
|
90
|
-
expect(buffer.subarray(0, signature.length)).toEqual(signature);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
describeIfLive('StackOverflow page downloads (JavaScript)', () => {
|
|
94
|
-
it('downloads the page as Markdown and plain text', async () => {
|
|
95
|
-
const markdown = await getText('/markdown');
|
|
96
|
-
expect(markdown.headers['content-type']).toMatch(/text\/markdown/);
|
|
97
|
-
expectStackOverflowContent(markdown.text);
|
|
98
|
-
expect(markdown.text).not.toMatch(/<html/i);
|
|
99
|
-
|
|
100
|
-
const text = await getText('/txt');
|
|
101
|
-
expect(text.headers['content-type']).toMatch(/text\/plain/);
|
|
102
|
-
expect(text.headers['content-disposition']).toContain('stackoverflow-com');
|
|
103
|
-
expectStackOverflowContent(text.text);
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
it('downloads Markdown and HTML archives with the expected files', async () => {
|
|
107
|
-
const markdownArchive = await getBinary('/archive', {
|
|
108
|
-
documentFormat: 'markdown',
|
|
109
|
-
localImages: 'false',
|
|
110
|
-
});
|
|
111
|
-
expect(markdownArchive.headers['content-type']).toMatch(/application\/zip/);
|
|
112
|
-
expectBufferWithSignature(markdownArchive.body, ZIP_SIGNATURE);
|
|
113
|
-
|
|
114
|
-
const markdownZip = await unzipper.Open.buffer(markdownArchive.body);
|
|
115
|
-
const markdownEntries = new Map(
|
|
116
|
-
markdownZip.files.map((file) => [file.path, file])
|
|
117
|
-
);
|
|
118
|
-
expect(markdownEntries.has('document.md')).toBe(true);
|
|
119
|
-
expect(markdownEntries.has('document.html')).toBe(true);
|
|
120
|
-
expectStackOverflowContent(
|
|
121
|
-
(await markdownEntries.get('document.md').buffer()).toString('utf8')
|
|
122
|
-
);
|
|
123
|
-
|
|
124
|
-
const htmlArchive = await getBinary('/archive', {
|
|
125
|
-
documentFormat: 'html',
|
|
126
|
-
localImages: 'false',
|
|
127
|
-
});
|
|
128
|
-
expect(htmlArchive.headers['content-type']).toMatch(/application\/zip/);
|
|
129
|
-
expectBufferWithSignature(htmlArchive.body, ZIP_SIGNATURE);
|
|
130
|
-
|
|
131
|
-
const htmlZip = await unzipper.Open.buffer(htmlArchive.body);
|
|
132
|
-
const htmlEntries = new Map(htmlZip.files.map((file) => [file.path, file]));
|
|
133
|
-
expect(htmlEntries.has('document.html')).toBe(true);
|
|
134
|
-
expectStackOverflowContent(
|
|
135
|
-
(await htmlEntries.get('document.html').buffer()).toString('utf8')
|
|
136
|
-
);
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
it('downloads the page as DOCX and PDF', async () => {
|
|
140
|
-
const docx = await getBinary('/docx');
|
|
141
|
-
expect(docx.headers['content-type']).toMatch(
|
|
142
|
-
/application\/vnd\.openxmlformats-officedocument\.wordprocessingml\.document/
|
|
143
|
-
);
|
|
144
|
-
expectBufferWithSignature(docx.body, ZIP_SIGNATURE);
|
|
145
|
-
|
|
146
|
-
const pdf = await getBinary('/pdf');
|
|
147
|
-
expect(pdf.headers['content-type']).toMatch(/application\/pdf/);
|
|
148
|
-
expectBufferWithSignature(pdf.body, PDF_SIGNATURE);
|
|
149
|
-
}, 180000);
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
describe.each(['puppeteer', 'playwright'])(
|
|
153
|
-
'StackOverflow browser downloads (%s engine)',
|
|
154
|
-
(engine) => {
|
|
155
|
-
describeIfLive(`${engine}`, () => {
|
|
156
|
-
it('downloads rendered HTML for the page', async () => {
|
|
157
|
-
const html = await getText('/html', { engine });
|
|
158
|
-
expect(html.headers['content-type']).toMatch(/text\/html/);
|
|
159
|
-
expectStackOverflowContent(html.text);
|
|
160
|
-
expect(html.text).toMatch(/<html/i);
|
|
161
|
-
}, 120000);
|
|
162
|
-
|
|
163
|
-
it('downloads a PNG screenshot for the page', async () => {
|
|
164
|
-
const image = await getBinary('/image', {
|
|
165
|
-
engine,
|
|
166
|
-
dismissPopups: 'true',
|
|
167
|
-
});
|
|
168
|
-
expect(image.headers['content-type']).toMatch(/image\/png/);
|
|
169
|
-
expectBufferWithSignature(image.body, PNG_SIGNATURE);
|
|
170
|
-
}, 120000);
|
|
171
|
-
});
|
|
172
|
-
}
|
|
173
|
-
);
|