@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.
Files changed (117) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/bin/web-capture.js +2 -5
  3. package/package.json +36 -23
  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,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
- });
@@ -1,7 +0,0 @@
1
- import { jest } from '@jest/globals';
2
-
3
- // Increase timeout for tests that might take longer
4
- jest.setTimeout(10000);
5
-
6
- // Suppress console.error during tests
7
- global.console.error = jest.fn();
@@ -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
- });
@@ -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
- );