@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.
- package/CHANGELOG.md +11 -0
- package/bin/web-capture.js +2 -5
- package/package.json +22 -11
- 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,134 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Integration tests for downloading the Wikipedia page (issue #8).
|
|
3
|
-
*
|
|
4
|
-
* Verifies that web-capture can actually download the Wikipedia article about
|
|
5
|
-
* Wikipedia itself (https://en.wikipedia.org/wiki/Wikipedia) as both Markdown
|
|
6
|
-
* and an image (PNG screenshot) using every supported browser engine
|
|
7
|
-
* (Puppeteer and Playwright).
|
|
8
|
-
*
|
|
9
|
-
* These tests hit the live Wikipedia servers. Set WIKIPEDIA_INTEGRATION=true to
|
|
10
|
-
* run them. In CI they are enabled via the workflow environment variable. When
|
|
11
|
-
* the variable is unset the suite is skipped so default/offline runs stay
|
|
12
|
-
* deterministic.
|
|
13
|
-
*
|
|
14
|
-
* Navigation uses retry with exponential backoff to absorb transient network
|
|
15
|
-
* failures, mirroring the Habr integration suite.
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
import { jest } from '@jest/globals';
|
|
19
|
-
import { createBrowser } from '../../src/browser.js';
|
|
20
|
-
import { convertHtmlToMarkdown } from '../../src/lib.js';
|
|
21
|
-
import { retry } from '../../src/retry.js';
|
|
22
|
-
import { expectPngScreenshot } from '../helpers/screenshot.js';
|
|
23
|
-
|
|
24
|
-
const WIKIPEDIA_URL = 'https://en.wikipedia.org/wiki/Wikipedia';
|
|
25
|
-
|
|
26
|
-
const SKIP_LIVE =
|
|
27
|
-
!process.env.WIKIPEDIA_INTEGRATION ||
|
|
28
|
-
process.env.WIKIPEDIA_INTEGRATION === 'false';
|
|
29
|
-
const describeIfLive = SKIP_LIVE ? describe.skip : describe;
|
|
30
|
-
|
|
31
|
-
// These tests hit live servers and may take a while.
|
|
32
|
-
jest.setTimeout(120000);
|
|
33
|
-
|
|
34
|
-
// Navigate with retry + exponential backoff, then give the page a moment to
|
|
35
|
-
// settle so late-loading content and images are present before we capture.
|
|
36
|
-
async function navigateWithRetry(page, url) {
|
|
37
|
-
await retry(
|
|
38
|
-
async () => {
|
|
39
|
-
await page.goto(url, {
|
|
40
|
-
waitUntil: 'networkidle0',
|
|
41
|
-
timeout: 60000,
|
|
42
|
-
});
|
|
43
|
-
},
|
|
44
|
-
{
|
|
45
|
-
retries: 3,
|
|
46
|
-
baseDelay: 2000,
|
|
47
|
-
onRetry: (err, attempt, delay) => {
|
|
48
|
-
console.log(
|
|
49
|
-
`Navigation retry ${attempt} for ${url} after ${delay}ms: ${err.message}`
|
|
50
|
-
);
|
|
51
|
-
},
|
|
52
|
-
}
|
|
53
|
-
);
|
|
54
|
-
await new Promise((resolve) => setTimeout(resolve, 5000));
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// Run the full matrix (Markdown + image) once per supported engine.
|
|
58
|
-
describe.each(['puppeteer', 'playwright'])(
|
|
59
|
-
'Wikipedia Page Download (%s engine)',
|
|
60
|
-
(engine) => {
|
|
61
|
-
describeIfLive(`${engine}`, () => {
|
|
62
|
-
let browser;
|
|
63
|
-
let page;
|
|
64
|
-
|
|
65
|
-
beforeAll(async () => {
|
|
66
|
-
browser = await createBrowser(engine);
|
|
67
|
-
page = await browser.newPage();
|
|
68
|
-
await page.setViewport({ width: 1280, height: 800 });
|
|
69
|
-
await navigateWithRetry(page, WIKIPEDIA_URL);
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
afterAll(async () => {
|
|
73
|
-
if (browser) {
|
|
74
|
-
await browser.close();
|
|
75
|
-
}
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
it('downloads the Wikipedia page as Markdown', async () => {
|
|
79
|
-
const html = await page.content();
|
|
80
|
-
expect(html).toMatch(/<html/i);
|
|
81
|
-
expect(html.length).toBeGreaterThan(1000);
|
|
82
|
-
|
|
83
|
-
const markdown = convertHtmlToMarkdown(html, WIKIPEDIA_URL);
|
|
84
|
-
|
|
85
|
-
// Markdown should carry the page's real content...
|
|
86
|
-
expect(markdown).toContain('Wikipedia');
|
|
87
|
-
expect(markdown.length).toBeGreaterThan(500);
|
|
88
|
-
// ...and should contain markdown structure (headings and/or links)...
|
|
89
|
-
expect(markdown).toMatch(/(^#{1,6}\s)|(\[.*?\]\(.*?\))/m);
|
|
90
|
-
// ...but no longer be raw HTML document scaffolding.
|
|
91
|
-
expect(markdown).not.toMatch(/<html/i);
|
|
92
|
-
expect(markdown).not.toMatch(/<head[\s>]/i);
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
it('downloads the Wikipedia page as an image (PNG screenshot)', async () => {
|
|
96
|
-
const screenshot = await page.screenshot({ type: 'png' });
|
|
97
|
-
|
|
98
|
-
expectPngScreenshot(screenshot);
|
|
99
|
-
});
|
|
100
|
-
});
|
|
101
|
-
}
|
|
102
|
-
);
|
|
103
|
-
|
|
104
|
-
describeIfLive('Wikipedia Page Download (engine parity)', () => {
|
|
105
|
-
it('both engines download Markdown and an image for the Wikipedia page', async () => {
|
|
106
|
-
const engines = ['puppeteer', 'playwright'];
|
|
107
|
-
const results = [];
|
|
108
|
-
|
|
109
|
-
for (const engine of engines) {
|
|
110
|
-
const browser = await createBrowser(engine);
|
|
111
|
-
try {
|
|
112
|
-
const page = await browser.newPage();
|
|
113
|
-
await page.setViewport({ width: 1280, height: 800 });
|
|
114
|
-
await navigateWithRetry(page, WIKIPEDIA_URL);
|
|
115
|
-
|
|
116
|
-
const html = await page.content();
|
|
117
|
-
const markdown = convertHtmlToMarkdown(html, WIKIPEDIA_URL);
|
|
118
|
-
const screenshot = await page.screenshot({ type: 'png' });
|
|
119
|
-
|
|
120
|
-
results.push({ engine, markdown, screenshot });
|
|
121
|
-
} finally {
|
|
122
|
-
await browser.close();
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
for (const { markdown, screenshot } of results) {
|
|
127
|
-
// Markdown captured from both engines.
|
|
128
|
-
expect(markdown).toContain('Wikipedia');
|
|
129
|
-
expect(markdown.length).toBeGreaterThan(500);
|
|
130
|
-
// Image captured from both engines.
|
|
131
|
-
expectPngScreenshot(screenshot);
|
|
132
|
-
}
|
|
133
|
-
}, 180000);
|
|
134
|
-
});
|
package/tests/jest.setup.mjs
DELETED
package/tests/mock/index.test.js
DELETED
|
@@ -1,225 +0,0 @@
|
|
|
1
|
-
import request from 'supertest';
|
|
2
|
-
import nock from '../helpers/nock.js';
|
|
3
|
-
import { jest } from '@jest/globals';
|
|
4
|
-
|
|
5
|
-
let app;
|
|
6
|
-
|
|
7
|
-
beforeAll(async () => {
|
|
8
|
-
app = (await import('../../src/index.js')).app;
|
|
9
|
-
});
|
|
10
|
-
|
|
11
|
-
describe('Web Capture Microservice', () => {
|
|
12
|
-
beforeEach(() => {
|
|
13
|
-
nock.cleanAll();
|
|
14
|
-
jest.clearAllMocks();
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
describe('GET /html', () => {
|
|
18
|
-
const testUrl = 'https://example.com';
|
|
19
|
-
const testHtml = '<html><body><h1>Test Page</h1></body></html>';
|
|
20
|
-
|
|
21
|
-
it('should return HTML content when URL is provided', async () => {
|
|
22
|
-
nock(testUrl).get('/').reply(200, testHtml);
|
|
23
|
-
|
|
24
|
-
const response = await request(app).get('/html').query({ url: testUrl });
|
|
25
|
-
|
|
26
|
-
expect(response.status).toBe(200);
|
|
27
|
-
expect(response.type).toBe('text/html');
|
|
28
|
-
expect(response.text).toBe(testHtml);
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
it('should return 400 when URL is missing', async () => {
|
|
32
|
-
const response = await request(app).get('/html');
|
|
33
|
-
|
|
34
|
-
expect(response.status).toBe(400);
|
|
35
|
-
expect(response.text).toBe('Missing `url` parameter');
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
it('should return 500 when fetch fails', async () => {
|
|
39
|
-
nock(testUrl).get('/').replyWithError('Network error');
|
|
40
|
-
|
|
41
|
-
const response = await request(app).get('/html').query({ url: testUrl });
|
|
42
|
-
|
|
43
|
-
expect(response.status).toBe(500);
|
|
44
|
-
expect(response.text).toBe('Error fetching HTML');
|
|
45
|
-
});
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
describe('GET /markdown', () => {
|
|
49
|
-
const testUrl = 'https://example.com';
|
|
50
|
-
const testHtml =
|
|
51
|
-
'<html><body><h1>Test Page</h1><p>Some text</p></body></html>';
|
|
52
|
-
const expectedMarkdown = '# Test Page\n\nSome text';
|
|
53
|
-
|
|
54
|
-
it('should convert HTML to Markdown when URL is provided', async () => {
|
|
55
|
-
nock(testUrl).get('/').reply(200, testHtml);
|
|
56
|
-
|
|
57
|
-
const response = await request(app)
|
|
58
|
-
.get('/markdown')
|
|
59
|
-
.query({ url: testUrl });
|
|
60
|
-
|
|
61
|
-
expect(response.status).toBe(200);
|
|
62
|
-
expect(response.type).toBe('text/markdown');
|
|
63
|
-
expect(response.text).toBe(expectedMarkdown);
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it('should remove CSS from the markdown output', async () => {
|
|
67
|
-
const htmlWithCss = `
|
|
68
|
-
<html>
|
|
69
|
-
<head>
|
|
70
|
-
<style>
|
|
71
|
-
body { background-color: #f0f0f2; }
|
|
72
|
-
div { width: 600px; }
|
|
73
|
-
</style>
|
|
74
|
-
</head>
|
|
75
|
-
<body>
|
|
76
|
-
<h1>Test Page</h1>
|
|
77
|
-
<p>Some text</p>
|
|
78
|
-
</body>
|
|
79
|
-
</html>
|
|
80
|
-
`;
|
|
81
|
-
|
|
82
|
-
nock(testUrl).get('/').reply(200, htmlWithCss);
|
|
83
|
-
|
|
84
|
-
const response = await request(app)
|
|
85
|
-
.get('/markdown')
|
|
86
|
-
.query({ url: testUrl });
|
|
87
|
-
|
|
88
|
-
expect(response.status).toBe(200);
|
|
89
|
-
expect(response.type).toBe('text/markdown');
|
|
90
|
-
expect(response.text).not.toContain('background-color');
|
|
91
|
-
expect(response.text).not.toContain('width: 600px');
|
|
92
|
-
expect(response.text).toContain('Test Page');
|
|
93
|
-
expect(response.text).toContain('Some text');
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
it('should return 400 when URL is missing', async () => {
|
|
97
|
-
const response = await request(app).get('/markdown');
|
|
98
|
-
|
|
99
|
-
expect(response.status).toBe(400);
|
|
100
|
-
expect(response.text).toBe('Missing `url` parameter');
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
it('should return 500 when fetch fails', async () => {
|
|
104
|
-
nock(testUrl).get('/').replyWithError('Network error');
|
|
105
|
-
|
|
106
|
-
const response = await request(app)
|
|
107
|
-
.get('/markdown')
|
|
108
|
-
.query({ url: testUrl });
|
|
109
|
-
|
|
110
|
-
expect(response.status).toBe(500);
|
|
111
|
-
expect(response.text).toBe('Error converting to Markdown');
|
|
112
|
-
});
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
describe('GET /image', () => {
|
|
116
|
-
const testUrl = 'https://example.com';
|
|
117
|
-
// Buffer for PNG image mock
|
|
118
|
-
// eslint-disable-next-line no-unused-vars
|
|
119
|
-
const mockBuffer = Buffer.from([
|
|
120
|
-
0x89,
|
|
121
|
-
0x50,
|
|
122
|
-
0x4e,
|
|
123
|
-
0x47,
|
|
124
|
-
0x0d,
|
|
125
|
-
0x0a,
|
|
126
|
-
0x1a,
|
|
127
|
-
0x0a, // PNG signature
|
|
128
|
-
0x00,
|
|
129
|
-
0x00,
|
|
130
|
-
0x00,
|
|
131
|
-
0x0d,
|
|
132
|
-
0x49,
|
|
133
|
-
0x48,
|
|
134
|
-
0x44,
|
|
135
|
-
0x52, // IHDR chunk
|
|
136
|
-
// ... (rest can be arbitrary for test)
|
|
137
|
-
0x00,
|
|
138
|
-
0x00,
|
|
139
|
-
0x00,
|
|
140
|
-
0x00,
|
|
141
|
-
0x49,
|
|
142
|
-
0x45,
|
|
143
|
-
0x4e,
|
|
144
|
-
0x44,
|
|
145
|
-
0xae,
|
|
146
|
-
0x42,
|
|
147
|
-
0x60,
|
|
148
|
-
0x82, // IEND chunk
|
|
149
|
-
]);
|
|
150
|
-
|
|
151
|
-
it('should return PNG image when URL is provided', async () => {
|
|
152
|
-
const response = await request(app).get('/image').query({ url: testUrl });
|
|
153
|
-
|
|
154
|
-
expect(response.status).toBe(200);
|
|
155
|
-
expect(response.type).toBe('image/png');
|
|
156
|
-
const pngSignature = Buffer.from([
|
|
157
|
-
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a,
|
|
158
|
-
]);
|
|
159
|
-
expect(response.body.slice(0, 8)).toEqual(pngSignature);
|
|
160
|
-
}, 60000);
|
|
161
|
-
|
|
162
|
-
it('should return 400 when URL is missing', async () => {
|
|
163
|
-
const response = await request(app).get('/image');
|
|
164
|
-
|
|
165
|
-
expect(response.status).toBe(400);
|
|
166
|
-
expect(response.text).toBe('Missing `url` parameter');
|
|
167
|
-
});
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
describe('GET /stream', () => {
|
|
171
|
-
it('should stream content from the given URL', async () => {
|
|
172
|
-
const testUrl = 'https://example.com';
|
|
173
|
-
const testHtml = '<html><body><h1>Streamed Page</h1></body></html>';
|
|
174
|
-
nock(testUrl).get('/').reply(200, testHtml, {
|
|
175
|
-
'content-type': 'text/html',
|
|
176
|
-
});
|
|
177
|
-
const response = await request(app).get(`/stream?url=${testUrl}`);
|
|
178
|
-
expect(response.status).toBe(200);
|
|
179
|
-
expect(response.text).toMatch(/<html/i);
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
it('should return 400 when URL is missing', async () => {
|
|
183
|
-
const response = await request(app).get('/stream');
|
|
184
|
-
expect(response.status).toBe(400);
|
|
185
|
-
expect(response.text).toBe('Missing `url` parameter');
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
it('should return 500 when fetch fails', async () => {
|
|
189
|
-
const testUrl = 'https://nonexistent.example.com';
|
|
190
|
-
const response = await request(app).get(`/stream?url=${testUrl}`);
|
|
191
|
-
expect(response.status).toBe(500);
|
|
192
|
-
expect(response.text).toBe('Error proxying content');
|
|
193
|
-
});
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
describe('GET /fetch', () => {
|
|
197
|
-
const testUrl = 'https://example.com';
|
|
198
|
-
const testHtml = '<html><body><h1>Test Page</h1></body></html>';
|
|
199
|
-
|
|
200
|
-
it('should return content when URL is provided', async () => {
|
|
201
|
-
nock(testUrl).get('/').reply(200, testHtml);
|
|
202
|
-
|
|
203
|
-
const response = await request(app).get('/fetch').query({ url: testUrl });
|
|
204
|
-
|
|
205
|
-
expect(response.status).toBe(200);
|
|
206
|
-
expect(response.text).toBe(testHtml);
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
it('should return 400 when URL is missing', async () => {
|
|
210
|
-
const response = await request(app).get('/fetch');
|
|
211
|
-
|
|
212
|
-
expect(response.status).toBe(400);
|
|
213
|
-
expect(response.text).toBe('Missing `url` parameter');
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
it('should return 500 when fetch fails', async () => {
|
|
217
|
-
nock(testUrl).get('/').replyWithError('Network error');
|
|
218
|
-
|
|
219
|
-
const response = await request(app).get('/fetch').query({ url: testUrl });
|
|
220
|
-
|
|
221
|
-
expect(response.status).toBe(500);
|
|
222
|
-
expect(response.text).toBe('Error fetching content');
|
|
223
|
-
});
|
|
224
|
-
});
|
|
225
|
-
});
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import { compareFrames } from '../../src/animation.js';
|
|
2
|
-
|
|
3
|
-
describe('animation module', () => {
|
|
4
|
-
describe('compareFrames', () => {
|
|
5
|
-
it('returns 1.0 for identical frames', () => {
|
|
6
|
-
const frame = Buffer.from([1, 2, 3, 4, 5]);
|
|
7
|
-
expect(compareFrames(frame, frame)).toBe(1.0);
|
|
8
|
-
});
|
|
9
|
-
|
|
10
|
-
it('returns 0 for completely different frames', () => {
|
|
11
|
-
const frame1 = Buffer.from([0, 0, 0, 0]);
|
|
12
|
-
const frame2 = Buffer.from([255, 255, 255, 255]);
|
|
13
|
-
expect(compareFrames(frame1, frame2)).toBe(0);
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
it('returns 0 for different sized frames', () => {
|
|
17
|
-
const frame1 = Buffer.from([1, 2, 3]);
|
|
18
|
-
const frame2 = Buffer.from([1, 2]);
|
|
19
|
-
expect(compareFrames(frame1, frame2)).toBe(0);
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
it('returns 0 for null frames', () => {
|
|
23
|
-
expect(compareFrames(null, Buffer.from([1]))).toBe(0);
|
|
24
|
-
expect(compareFrames(Buffer.from([1]), null)).toBe(0);
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
it('calculates partial similarity', () => {
|
|
28
|
-
const frame1 = Buffer.from([1, 2, 3, 4]);
|
|
29
|
-
const frame2 = Buffer.from([1, 2, 0, 0]);
|
|
30
|
-
expect(compareFrames(frame1, frame2)).toBe(0.5);
|
|
31
|
-
});
|
|
32
|
-
});
|
|
33
|
-
});
|
package/tests/unit/batch.test.js
DELETED
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
getArticle,
|
|
3
|
-
getAllVersions,
|
|
4
|
-
getAllArticles,
|
|
5
|
-
createConfigFromUrls,
|
|
6
|
-
validateConfig,
|
|
7
|
-
} from '../../src/batch.js';
|
|
8
|
-
|
|
9
|
-
describe('batch module', () => {
|
|
10
|
-
const sampleConfig = {
|
|
11
|
-
articles: {
|
|
12
|
-
'0.0.0': {
|
|
13
|
-
url: 'https://example.com/article-1',
|
|
14
|
-
title: 'First Article',
|
|
15
|
-
language: 'en',
|
|
16
|
-
},
|
|
17
|
-
'0.0.1': {
|
|
18
|
-
url: 'https://example.com/article-2',
|
|
19
|
-
title: 'Second Article',
|
|
20
|
-
language: 'ru',
|
|
21
|
-
},
|
|
22
|
-
},
|
|
23
|
-
defaults: {
|
|
24
|
-
archivePath: 'archive',
|
|
25
|
-
markdownFile: 'document.md',
|
|
26
|
-
imagesDir: 'images',
|
|
27
|
-
hasLocalImages: true,
|
|
28
|
-
},
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
describe('getArticle', () => {
|
|
32
|
-
it('returns article config by version', () => {
|
|
33
|
-
const article = getArticle(sampleConfig, '0.0.0');
|
|
34
|
-
expect(article.url).toBe('https://example.com/article-1');
|
|
35
|
-
expect(article.title).toBe('First Article');
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
it('merges defaults into article config', () => {
|
|
39
|
-
const article = getArticle(sampleConfig, '0.0.0');
|
|
40
|
-
expect(article.markdownFile).toBe('document.md');
|
|
41
|
-
expect(article.hasLocalImages).toBe(true);
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it('throws for unknown version', () => {
|
|
45
|
-
expect(() => getArticle(sampleConfig, '9.9.9')).toThrow(
|
|
46
|
-
/Unknown article version/
|
|
47
|
-
);
|
|
48
|
-
});
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
describe('getAllVersions', () => {
|
|
52
|
-
it('returns all version keys', () => {
|
|
53
|
-
const versions = getAllVersions(sampleConfig);
|
|
54
|
-
expect(versions).toEqual(['0.0.0', '0.0.1']);
|
|
55
|
-
});
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
describe('getAllArticles', () => {
|
|
59
|
-
it('returns all articles with defaults merged', () => {
|
|
60
|
-
const articles = getAllArticles(sampleConfig);
|
|
61
|
-
expect(articles).toHaveLength(2);
|
|
62
|
-
expect(articles[0].markdownFile).toBe('document.md');
|
|
63
|
-
expect(articles[1].language).toBe('ru');
|
|
64
|
-
});
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
describe('createConfigFromUrls', () => {
|
|
68
|
-
it('creates config from URL list', () => {
|
|
69
|
-
const config = createConfigFromUrls([
|
|
70
|
-
'https://example.com/a',
|
|
71
|
-
'https://example.com/b',
|
|
72
|
-
]);
|
|
73
|
-
expect(Object.keys(config.articles)).toHaveLength(2);
|
|
74
|
-
expect(config.articles['1'].url).toBe('https://example.com/a');
|
|
75
|
-
expect(config.articles['2'].url).toBe('https://example.com/b');
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
it('sets default archive paths', () => {
|
|
79
|
-
const config = createConfigFromUrls(['https://habr.com/article/123']);
|
|
80
|
-
expect(config.articles['1'].archivePath).toContain('habr-com');
|
|
81
|
-
expect(config.articles['1'].markdownFile).toBe('document.md');
|
|
82
|
-
});
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
describe('validateConfig', () => {
|
|
86
|
-
it('validates correct config', () => {
|
|
87
|
-
const result = validateConfig(sampleConfig);
|
|
88
|
-
expect(result.valid).toBe(true);
|
|
89
|
-
expect(result.errors).toHaveLength(0);
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
it('rejects config without articles', () => {
|
|
93
|
-
const result = validateConfig({});
|
|
94
|
-
expect(result.valid).toBe(false);
|
|
95
|
-
expect(result.errors[0]).toContain('articles');
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
it('rejects article without url', () => {
|
|
99
|
-
const result = validateConfig({
|
|
100
|
-
articles: { 1: { title: 'No URL' } },
|
|
101
|
-
});
|
|
102
|
-
expect(result.valid).toBe(false);
|
|
103
|
-
expect(result.errors[0]).toContain('missing required "url"');
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
it('rejects article with invalid url', () => {
|
|
107
|
-
const result = validateConfig({
|
|
108
|
-
articles: { 1: { url: 'not-a-url' } },
|
|
109
|
-
});
|
|
110
|
-
expect(result.valid).toBe(false);
|
|
111
|
-
expect(result.errors[0]).toContain('invalid URL');
|
|
112
|
-
});
|
|
113
|
-
});
|
|
114
|
-
});
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
import { fileURLToPath } from 'url';
|
|
4
|
-
import {
|
|
5
|
-
normalizeGoogleDocsExportMarkdown,
|
|
6
|
-
preprocessGoogleDocsExportHtml,
|
|
7
|
-
} from '../../src/gdocs.js';
|
|
8
|
-
import { convertHtmlToMarkdown } from '../../src/lib.js';
|
|
9
|
-
|
|
10
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
11
|
-
const __dirname = path.dirname(__filename);
|
|
12
|
-
const HTML = fs.readFileSync(
|
|
13
|
-
path.join(__dirname, '..', 'fixtures', 'bold-coalesce-and-leakage.html'),
|
|
14
|
-
'utf8'
|
|
15
|
-
);
|
|
16
|
-
|
|
17
|
-
describe('bold-run coalescing and block boundary handling (issue #120)', () => {
|
|
18
|
-
let md;
|
|
19
|
-
beforeAll(() => {
|
|
20
|
-
const preprocessed = preprocessGoogleDocsExportHtml(HTML);
|
|
21
|
-
md = normalizeGoogleDocsExportMarkdown(
|
|
22
|
-
convertHtmlToMarkdown(preprocessed.html)
|
|
23
|
-
);
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
it('A: coalesces adjacent bold spans into a single bold run', () => {
|
|
27
|
-
expect(md).toMatch(/\*\*13\.1 First subsection\*\*/);
|
|
28
|
-
expect(md).not.toMatch(/\*\*13\.1\*\*\s+\*\*First subsection\*\*/);
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
it('B: never emits an empty "****" pair between adjacent bold runs', () => {
|
|
32
|
-
expect(md).not.toMatch(/\*{4}/);
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
it('C: closes bold at block-level boundaries (<br>, <img>)', () => {
|
|
36
|
-
// No single bold run may contain an image. CommonMark bold cannot span
|
|
37
|
-
// a blank line (`\n\n`), so the inner content is restricted accordingly.
|
|
38
|
-
const strongRe = /\*\*((?:(?!\*\*|\n\n)[\s\S])+?)\*\*/g;
|
|
39
|
-
const imageRe = /!\[[^\]]*\]\([^)]+\)/;
|
|
40
|
-
for (const match of md.matchAll(strongRe)) {
|
|
41
|
-
expect(imageRe.test(match[1])).toBe(false);
|
|
42
|
-
}
|
|
43
|
-
expect(md).toMatch(/\*\*Caption A:\*\*/);
|
|
44
|
-
expect(md).toMatch(/\*\*Caption B:\*\*/);
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it('D: every "**" opener has a matching closer (balanced asterisks)', () => {
|
|
48
|
-
const stripped = md
|
|
49
|
-
.replace(/`[^`]*`/g, '')
|
|
50
|
-
.replace(/!\[[^\]]*\]\([^)]*\)/g, '');
|
|
51
|
-
const count = (stripped.match(/\*\*/g) || []).length;
|
|
52
|
-
expect(count % 2).toBe(0);
|
|
53
|
-
});
|
|
54
|
-
});
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import { jest } from '@jest/globals';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import { withTemporaryDirectory } from '../helpers/temporary-directory.js';
|
|
4
|
-
|
|
5
|
-
const initialPage = { close: jest.fn(), bringToFront: jest.fn() };
|
|
6
|
-
const browser = { pages: jest.fn(() => [initialPage]), close: jest.fn() };
|
|
7
|
-
const launch = jest.fn(async () => browser);
|
|
8
|
-
const launchPersistentContext = jest.fn(async () => browser);
|
|
9
|
-
jest.unstable_mockModule('puppeteer', () => ({ default: { launch } }));
|
|
10
|
-
jest.unstable_mockModule('playwright', () => ({
|
|
11
|
-
chromium: { launchPersistentContext },
|
|
12
|
-
}));
|
|
13
|
-
const { createBrowser } = await import('../../src/browser.js');
|
|
14
|
-
|
|
15
|
-
beforeEach(() => {
|
|
16
|
-
jest.clearAllMocks();
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
test.each(['puppeteer', 'playwright'])(
|
|
20
|
-
'%s forwards packaged executablePath and channel to the engine',
|
|
21
|
-
(engine) =>
|
|
22
|
-
withTemporaryDirectory('launch-options-', async (directory) => {
|
|
23
|
-
const options = {
|
|
24
|
-
executablePath: path.join(directory, 'packaged-chromium'),
|
|
25
|
-
channel: 'chrome',
|
|
26
|
-
userDataDir: directory,
|
|
27
|
-
};
|
|
28
|
-
const handle = await createBrowser(engine, options);
|
|
29
|
-
try {
|
|
30
|
-
const observed =
|
|
31
|
-
engine === 'puppeteer'
|
|
32
|
-
? launch.mock.calls[0][0]
|
|
33
|
-
: launchPersistentContext.mock.calls[0][1];
|
|
34
|
-
expect(observed).toMatchObject({
|
|
35
|
-
executablePath: options.executablePath,
|
|
36
|
-
channel: options.channel,
|
|
37
|
-
});
|
|
38
|
-
} finally {
|
|
39
|
-
await handle.close();
|
|
40
|
-
}
|
|
41
|
-
})
|
|
42
|
-
);
|