@link-assistant/web-capture 2.0.0 → 2.0.1

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 (117) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/bin/web-capture.js +2 -5
  3. package/package.json +22 -11
  4. package/src/archive.js +0 -1
  5. package/src/binary-download.js +0 -1
  6. package/src/docx.js +13 -10
  7. package/src/fetch.js +0 -1
  8. package/src/gdocs-images.js +0 -1
  9. package/src/gdocs.js +0 -1
  10. package/src/github.js +0 -1
  11. package/src/lib.js +0 -1
  12. package/src/markdown.js +0 -1
  13. package/src/shared-dialog.js +0 -1
  14. package/src/stream.js +0 -1
  15. package/src/transport.js +23 -5
  16. package/src/txt.js +0 -1
  17. package/.changeset/README.md +0 -8
  18. package/.changeset/config.json +0 -11
  19. package/.dockerignore +0 -6
  20. package/.jscpd.json +0 -19
  21. package/.lenv +0 -8
  22. package/.prettierignore +0 -8
  23. package/.prettierrc +0 -10
  24. package/Dockerfile +0 -52
  25. package/docker-compose.yml +0 -11
  26. package/eslint.config.js +0 -112
  27. package/examples/js/capture_cache.js +0 -12
  28. package/examples/js/engine_comparison.js +0 -39
  29. package/examples/js/html_download.js +0 -23
  30. package/examples/js/image_download.js +0 -36
  31. package/examples/js/markdown_download.js +0 -23
  32. package/examples/js/playwright_example.js +0 -23
  33. package/examples/python/capture_client.py +0 -20
  34. package/examples/python/html_download.py +0 -3
  35. package/examples/python/image_download.py +0 -16
  36. package/examples/python/markdown_download.py +0 -3
  37. package/examples/python/playwright_example.py +0 -22
  38. package/jest.config.mjs +0 -17
  39. package/tests/ci/dependency-freshness.test.js +0 -74
  40. package/tests/ci/detect-code-changes.test.js +0 -187
  41. package/tests/ci/docker-install.test.js +0 -31
  42. package/tests/ci/rust-dockerfile.test.js +0 -54
  43. package/tests/ci/workflow-policy.test.js +0 -79
  44. package/tests/e2e/docker.test.js +0 -186
  45. package/tests/e2e/process.test.js +0 -215
  46. package/tests/fixtures/bold-coalesce-and-leakage.html +0 -27
  47. package/tests/fixtures/deepwiki/deepwiki-full-page.png +0 -0
  48. package/tests/fixtures/deepwiki/index.md +0 -1500
  49. package/tests/fixtures/deepwiki/part-1.md +0 -776
  50. package/tests/fixtures/habr-articles.js +0 -45
  51. package/tests/fixtures/heading-numbering.html +0 -19
  52. package/tests/fixtures/list-item-with-br.html +0 -8
  53. package/tests/fixtures/ol-continuous-numbering.html +0 -12
  54. package/tests/fixtures/paragraph-vs-line-break.html +0 -11
  55. package/tests/helpers/nock.js +0 -10
  56. package/tests/helpers/screenshot.js +0 -10
  57. package/tests/helpers/search-fixture.cjs +0 -17
  58. package/tests/helpers/temporary-directory.js +0 -12
  59. package/tests/integration/api-endpoints.test.js +0 -418
  60. package/tests/integration/archive-default-layout.test.js +0 -37
  61. package/tests/integration/browser-engines.test.js +0 -80
  62. package/tests/integration/browser-packaged-path.test.js +0 -45
  63. package/tests/integration/deepwiki-download.test.js +0 -144
  64. package/tests/integration/deepwiki-rendered-page.mock.test.js +0 -121
  65. package/tests/integration/formalai-contract.test.js +0 -214
  66. package/tests/integration/gdocs-public-doc.test.js +0 -315
  67. package/tests/integration/github-readme.test.js +0 -133
  68. package/tests/integration/google-drive-image.test.js +0 -246
  69. package/tests/integration/habr-article.test.js +0 -337
  70. package/tests/integration/search-endpoint.test.js +0 -88
  71. package/tests/integration/stackoverflow-download.test.js +0 -173
  72. package/tests/integration/wikipedia-download.test.js +0 -134
  73. package/tests/jest.setup.mjs +0 -7
  74. package/tests/mock/index.test.js +0 -225
  75. package/tests/unit/animation.test.js +0 -33
  76. package/tests/unit/batch.test.js +0 -114
  77. package/tests/unit/bold-coalesce-and-leakage.test.js +0 -54
  78. package/tests/unit/browser-launch-options.test.js +0 -42
  79. package/tests/unit/browser.test.js +0 -172
  80. package/tests/unit/cache.test.js +0 -182
  81. package/tests/unit/cli.test.js +0 -399
  82. package/tests/unit/convertGoogleDriveUrl.test.js +0 -89
  83. package/tests/unit/convertRelativeUrls.test.js +0 -34
  84. package/tests/unit/extract-images.test.js +0 -259
  85. package/tests/unit/fetch-html.test.js +0 -54
  86. package/tests/unit/figures.test.js +0 -74
  87. package/tests/unit/gdocs-export-headers.test.js +0 -28
  88. package/tests/unit/gdocs-preprocess.test.js +0 -36
  89. package/tests/unit/gdocs.test.js +0 -1490
  90. package/tests/unit/github.test.js +0 -156
  91. package/tests/unit/heading-image.test.js +0 -49
  92. package/tests/unit/heading-numbering.test.js +0 -28
  93. package/tests/unit/html2md-br-in-list-item.test.js +0 -23
  94. package/tests/unit/html2md-ol-numbering.test.js +0 -20
  95. package/tests/unit/html2md.test.js +0 -695
  96. package/tests/unit/image-mode-defaults.test.js +0 -54
  97. package/tests/unit/kreuzberg.test.js +0 -249
  98. package/tests/unit/latex.test.js +0 -123
  99. package/tests/unit/localize-images.test.js +0 -114
  100. package/tests/unit/markdown.test.js +0 -36
  101. package/tests/unit/metadata.test.js +0 -140
  102. package/tests/unit/package-runtime-dependencies.test.js +0 -74
  103. package/tests/unit/paragraph-vs-line-break.test.js +0 -56
  104. package/tests/unit/postprocess.test.js +0 -110
  105. package/tests/unit/proxy-headers.test.js +0 -100
  106. package/tests/unit/publish-verification.test.js +0 -54
  107. package/tests/unit/retry.test.js +0 -111
  108. package/tests/unit/search.test.js +0 -339
  109. package/tests/unit/shared-dialog.test.js +0 -173
  110. package/tests/unit/stackoverflow.test.js +0 -32
  111. package/tests/unit/stream.test.js +0 -83
  112. package/tests/unit/verify.test.js +0 -182
  113. package/tests/xpaste/xpaste-screenshot-markdown.mock.test.js +0 -375
  114. package/tests/xpaste/xpaste.e2e.test.js +0 -36
  115. package/tests/xpaste/xpaste.integration.test.js +0 -41
  116. package/tests/xpaste/xpaste.mock.test.js +0 -261
  117. 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
- );