@link-assistant/web-capture 1.1.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/.changeset/README.md +8 -0
- package/.changeset/config.json +11 -0
- package/.github/workflows/release.yml +294 -0
- package/.jscpd.json +20 -0
- package/.lenv +8 -0
- package/.prettierignore +7 -0
- package/.prettierrc +10 -0
- package/ARCHITECTURE.md +982 -0
- package/CHANGELOG.md +22 -0
- package/Dockerfile +45 -0
- package/LICENSE +24 -0
- package/README.md +268 -0
- package/apply.sh +166 -0
- package/babel.config.cjs +13 -0
- package/bin/web-capture.js +285 -0
- package/docker-compose.yml +9 -0
- package/docs/market-analysis-2025-05-13.md +64 -0
- package/eslint.config.js +112 -0
- package/examples/js/engine_comparison.js +39 -0
- package/examples/js/html_download.js +23 -0
- package/examples/js/image_download.js +36 -0
- package/examples/js/markdown_download.js +23 -0
- package/examples/js/playwright_example.js +23 -0
- package/examples/python/html_download.py +15 -0
- package/examples/python/image_download.py +20 -0
- package/examples/python/markdown_download.py +15 -0
- package/examples/python/playwright_example.py +22 -0
- package/jest.config.mjs +18 -0
- package/package.json +83 -0
- package/scripts/changeset-version.mjs +38 -0
- package/scripts/create-github-release.mjs +93 -0
- package/scripts/create-manual-changeset.mjs +88 -0
- package/scripts/format-github-release.mjs +83 -0
- package/scripts/format-release-notes.mjs +218 -0
- package/scripts/instant-version-bump.mjs +121 -0
- package/scripts/publish-to-npm.mjs +128 -0
- package/scripts/setup-npm.mjs +37 -0
- package/scripts/validate-changeset.mjs +106 -0
- package/scripts/version-and-commit.mjs +237 -0
- package/src/browser.js +207 -0
- package/src/fetch.js +38 -0
- package/src/html.js +64 -0
- package/src/image.js +41 -0
- package/src/index.js +55 -0
- package/src/lib.js +330 -0
- package/src/markdown.js +17 -0
- package/src/stream.js +50 -0
- package/tests/e2e/docker.test.js +148 -0
- package/tests/e2e/process.test.js +117 -0
- package/tests/integration/browser-engines.test.js +128 -0
- package/tests/jest.setup.mjs +7 -0
- package/tests/mock/index.test.js +223 -0
- package/tests/unit/browser.test.js +136 -0
- package/tests/unit/cli.test.js +151 -0
- package/tests/unit/convertRelativeUrls.test.js +34 -0
- package/tests/unit/html2md.test.js +610 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { createBrowser } from '../../src/browser.js';
|
|
2
|
+
|
|
3
|
+
describe('Browser Engine Integration Tests', () => {
|
|
4
|
+
describe('Puppeteer Engine', () => {
|
|
5
|
+
let browser;
|
|
6
|
+
|
|
7
|
+
beforeEach(async () => {
|
|
8
|
+
browser = await createBrowser('puppeteer');
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
afterEach(async () => {
|
|
12
|
+
if (browser) {
|
|
13
|
+
await browser.close();
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('can navigate to a page and get content', async () => {
|
|
18
|
+
const page = await browser.newPage();
|
|
19
|
+
await page.goto('https://example.com', {
|
|
20
|
+
waitUntil: 'networkidle0',
|
|
21
|
+
timeout: 30000,
|
|
22
|
+
});
|
|
23
|
+
const content = await page.content();
|
|
24
|
+
expect(content).toContain('Example Domain');
|
|
25
|
+
}, 60000);
|
|
26
|
+
|
|
27
|
+
it('can take a screenshot', async () => {
|
|
28
|
+
const page = await browser.newPage();
|
|
29
|
+
await page.setViewport({ width: 1280, height: 800 });
|
|
30
|
+
await page.goto('https://example.com', {
|
|
31
|
+
waitUntil: 'networkidle0',
|
|
32
|
+
timeout: 30000,
|
|
33
|
+
});
|
|
34
|
+
const screenshot = await page.screenshot({ type: 'png' });
|
|
35
|
+
expect(screenshot).toBeInstanceOf(Buffer);
|
|
36
|
+
expect(screenshot.length).toBeGreaterThan(100);
|
|
37
|
+
}, 60000);
|
|
38
|
+
|
|
39
|
+
it('can set custom headers and user agent', async () => {
|
|
40
|
+
const page = await browser.newPage();
|
|
41
|
+
await page.setExtraHTTPHeaders({ 'Accept-Language': 'en-US' });
|
|
42
|
+
await page.setUserAgent('Test User Agent');
|
|
43
|
+
await page.goto('https://example.com', {
|
|
44
|
+
waitUntil: 'networkidle0',
|
|
45
|
+
timeout: 30000,
|
|
46
|
+
});
|
|
47
|
+
const content = await page.content();
|
|
48
|
+
expect(content).toBeTruthy();
|
|
49
|
+
}, 60000);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe('Playwright Engine', () => {
|
|
53
|
+
let browser;
|
|
54
|
+
|
|
55
|
+
beforeEach(async () => {
|
|
56
|
+
browser = await createBrowser('playwright');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
afterEach(async () => {
|
|
60
|
+
if (browser) {
|
|
61
|
+
await browser.close();
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('can navigate to a page and get content', async () => {
|
|
66
|
+
const page = await browser.newPage();
|
|
67
|
+
await page.goto('https://example.com', {
|
|
68
|
+
waitUntil: 'networkidle0',
|
|
69
|
+
timeout: 30000,
|
|
70
|
+
});
|
|
71
|
+
const content = await page.content();
|
|
72
|
+
expect(content).toContain('Example Domain');
|
|
73
|
+
}, 60000);
|
|
74
|
+
|
|
75
|
+
it('can take a screenshot', async () => {
|
|
76
|
+
const page = await browser.newPage();
|
|
77
|
+
await page.setViewport({ width: 1280, height: 800 });
|
|
78
|
+
await page.goto('https://example.com', {
|
|
79
|
+
waitUntil: 'networkidle0',
|
|
80
|
+
timeout: 30000,
|
|
81
|
+
});
|
|
82
|
+
const screenshot = await page.screenshot({ type: 'png' });
|
|
83
|
+
expect(screenshot).toBeInstanceOf(Buffer);
|
|
84
|
+
expect(screenshot.length).toBeGreaterThan(100);
|
|
85
|
+
}, 60000);
|
|
86
|
+
|
|
87
|
+
it('can set custom headers and user agent', async () => {
|
|
88
|
+
const page = await browser.newPage();
|
|
89
|
+
await page.setExtraHTTPHeaders({ 'Accept-Language': 'en-US' });
|
|
90
|
+
await page.setUserAgent('Test User Agent');
|
|
91
|
+
await page.goto('https://example.com', {
|
|
92
|
+
waitUntil: 'networkidle0',
|
|
93
|
+
timeout: 30000,
|
|
94
|
+
});
|
|
95
|
+
const content = await page.content();
|
|
96
|
+
expect(content).toBeTruthy();
|
|
97
|
+
}, 60000);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe('Engine Parity', () => {
|
|
101
|
+
it('both engines produce similar content for the same page', async () => {
|
|
102
|
+
const puppeteerBrowser = await createBrowser('puppeteer');
|
|
103
|
+
const playwrightBrowser = await createBrowser('playwright');
|
|
104
|
+
|
|
105
|
+
const puppeteerPage = await puppeteerBrowser.newPage();
|
|
106
|
+
const playwrightPage = await playwrightBrowser.newPage();
|
|
107
|
+
|
|
108
|
+
await puppeteerPage.goto('https://example.com', {
|
|
109
|
+
waitUntil: 'networkidle0',
|
|
110
|
+
timeout: 30000,
|
|
111
|
+
});
|
|
112
|
+
await playwrightPage.goto('https://example.com', {
|
|
113
|
+
waitUntil: 'networkidle0',
|
|
114
|
+
timeout: 30000,
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const puppeteerContent = await puppeteerPage.content();
|
|
118
|
+
const playwrightContent = await playwrightPage.content();
|
|
119
|
+
|
|
120
|
+
// Both should contain the main content
|
|
121
|
+
expect(puppeteerContent).toContain('Example Domain');
|
|
122
|
+
expect(playwrightContent).toContain('Example Domain');
|
|
123
|
+
|
|
124
|
+
await puppeteerBrowser.close();
|
|
125
|
+
await playwrightBrowser.close();
|
|
126
|
+
}, 60000);
|
|
127
|
+
});
|
|
128
|
+
});
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import request from 'supertest';
|
|
2
|
+
import nock from 'nock';
|
|
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
|
+
// Skipping strict buffer match: Puppeteer output is not deterministic
|
|
161
|
+
// expect(response.body.equals(mockBuffer)).toBe(true);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('should return 400 when URL is missing', async () => {
|
|
165
|
+
const response = await request(app).get('/image');
|
|
166
|
+
|
|
167
|
+
expect(response.status).toBe(400);
|
|
168
|
+
expect(response.text).toBe('Missing `url` parameter');
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
describe('GET /stream', () => {
|
|
173
|
+
it('should stream content from the given URL', async () => {
|
|
174
|
+
const testUrl = 'https://example.com';
|
|
175
|
+
const response = await request(app).get(`/stream?url=${testUrl}`);
|
|
176
|
+
expect(response.status).toBe(200);
|
|
177
|
+
expect(response.text).toMatch(/<html/i);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('should return 400 when URL is missing', async () => {
|
|
181
|
+
const response = await request(app).get('/stream');
|
|
182
|
+
expect(response.status).toBe(400);
|
|
183
|
+
expect(response.text).toBe('Missing `url` parameter');
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('should return 500 when fetch fails', async () => {
|
|
187
|
+
const testUrl = 'https://nonexistent.example.com';
|
|
188
|
+
const response = await request(app).get(`/stream?url=${testUrl}`);
|
|
189
|
+
expect(response.status).toBe(500);
|
|
190
|
+
expect(response.text).toBe('Error proxying content');
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
describe('GET /fetch', () => {
|
|
195
|
+
const testUrl = 'https://example.com';
|
|
196
|
+
const testHtml = '<html><body><h1>Test Page</h1></body></html>';
|
|
197
|
+
|
|
198
|
+
it('should return content when URL is provided', async () => {
|
|
199
|
+
nock(testUrl).get('/').reply(200, testHtml);
|
|
200
|
+
|
|
201
|
+
const response = await request(app).get('/fetch').query({ url: testUrl });
|
|
202
|
+
|
|
203
|
+
expect(response.status).toBe(200);
|
|
204
|
+
expect(response.text).toBe(testHtml);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('should return 400 when URL is missing', async () => {
|
|
208
|
+
const response = await request(app).get('/fetch');
|
|
209
|
+
|
|
210
|
+
expect(response.status).toBe(400);
|
|
211
|
+
expect(response.text).toBe('Missing `url` parameter');
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('should return 500 when fetch fails', async () => {
|
|
215
|
+
nock(testUrl).get('/').replyWithError('Network error');
|
|
216
|
+
|
|
217
|
+
const response = await request(app).get('/fetch').query({ url: testUrl });
|
|
218
|
+
|
|
219
|
+
expect(response.status).toBe(500);
|
|
220
|
+
expect(response.text).toBe('Error fetching content');
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
});
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { createBrowser, getBrowserEngine } from '../../src/browser.js';
|
|
2
|
+
|
|
3
|
+
describe('Browser Abstraction Layer', () => {
|
|
4
|
+
describe('getBrowserEngine', () => {
|
|
5
|
+
it('returns puppeteer by default', () => {
|
|
6
|
+
const req = { query: {} };
|
|
7
|
+
expect(getBrowserEngine(req)).toBe('puppeteer');
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it('returns playwright when engine=playwright query param is provided', () => {
|
|
11
|
+
const req = { query: { engine: 'playwright' } };
|
|
12
|
+
expect(getBrowserEngine(req)).toBe('playwright');
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('returns playwright when engine=pw query param is provided', () => {
|
|
16
|
+
const req = { query: { engine: 'pw' } };
|
|
17
|
+
expect(getBrowserEngine(req)).toBe('playwright');
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('returns puppeteer when engine=puppeteer query param is provided', () => {
|
|
21
|
+
const req = { query: { engine: 'puppeteer' } };
|
|
22
|
+
expect(getBrowserEngine(req)).toBe('puppeteer');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('returns puppeteer when engine=pptr query param is provided', () => {
|
|
26
|
+
const req = { query: { engine: 'pptr' } };
|
|
27
|
+
expect(getBrowserEngine(req)).toBe('puppeteer');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('returns playwright when browser=playwright query param is provided', () => {
|
|
31
|
+
const req = { query: { browser: 'playwright' } };
|
|
32
|
+
expect(getBrowserEngine(req)).toBe('playwright');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('is case insensitive', () => {
|
|
36
|
+
const req1 = { query: { engine: 'PLAYWRIGHT' } };
|
|
37
|
+
const req2 = { query: { engine: 'PlAyWrIgHt' } };
|
|
38
|
+
expect(getBrowserEngine(req1)).toBe('playwright');
|
|
39
|
+
expect(getBrowserEngine(req2)).toBe('playwright');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('returns playwright from BROWSER_ENGINE env var', () => {
|
|
43
|
+
const originalEnv = process.env.BROWSER_ENGINE;
|
|
44
|
+
process.env.BROWSER_ENGINE = 'playwright';
|
|
45
|
+
const req = { query: {} };
|
|
46
|
+
expect(getBrowserEngine(req)).toBe('playwright');
|
|
47
|
+
if (originalEnv) {
|
|
48
|
+
process.env.BROWSER_ENGINE = originalEnv;
|
|
49
|
+
} else {
|
|
50
|
+
delete process.env.BROWSER_ENGINE;
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('prefers query param over env var', () => {
|
|
55
|
+
const originalEnv = process.env.BROWSER_ENGINE;
|
|
56
|
+
process.env.BROWSER_ENGINE = 'playwright';
|
|
57
|
+
const req = { query: { engine: 'puppeteer' } };
|
|
58
|
+
expect(getBrowserEngine(req)).toBe('puppeteer');
|
|
59
|
+
if (originalEnv) {
|
|
60
|
+
process.env.BROWSER_ENGINE = originalEnv;
|
|
61
|
+
} else {
|
|
62
|
+
delete process.env.BROWSER_ENGINE;
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe('createBrowser - Puppeteer', () => {
|
|
68
|
+
it('creates a puppeteer browser with type field', async () => {
|
|
69
|
+
const browser = await createBrowser('puppeteer');
|
|
70
|
+
expect(browser.type).toBe('puppeteer');
|
|
71
|
+
expect(browser.newPage).toBeDefined();
|
|
72
|
+
expect(browser.close).toBeDefined();
|
|
73
|
+
await browser.close();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('can create and use a page', async () => {
|
|
77
|
+
const browser = await createBrowser('puppeteer');
|
|
78
|
+
const page = await browser.newPage();
|
|
79
|
+
expect(page.setViewport).toBeDefined();
|
|
80
|
+
expect(page.goto).toBeDefined();
|
|
81
|
+
expect(page.content).toBeDefined();
|
|
82
|
+
expect(page.screenshot).toBeDefined();
|
|
83
|
+
expect(page._type).toBe('puppeteer');
|
|
84
|
+
await browser.close();
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe('createBrowser - Playwright', () => {
|
|
89
|
+
it('creates a playwright browser with type field', async () => {
|
|
90
|
+
const browser = await createBrowser('playwright');
|
|
91
|
+
expect(browser.type).toBe('playwright');
|
|
92
|
+
expect(browser.newPage).toBeDefined();
|
|
93
|
+
expect(browser.close).toBeDefined();
|
|
94
|
+
await browser.close();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('can create and use a page', async () => {
|
|
98
|
+
const browser = await createBrowser('playwright');
|
|
99
|
+
const page = await browser.newPage();
|
|
100
|
+
expect(page.setViewport).toBeDefined();
|
|
101
|
+
expect(page.goto).toBeDefined();
|
|
102
|
+
expect(page.content).toBeDefined();
|
|
103
|
+
expect(page.screenshot).toBeDefined();
|
|
104
|
+
expect(page._type).toBe('playwright');
|
|
105
|
+
await browser.close();
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
describe('Page Adapter Compatibility', () => {
|
|
110
|
+
it('both adapters support the same interface', async () => {
|
|
111
|
+
const puppeteerBrowser = await createBrowser('puppeteer');
|
|
112
|
+
const playwrightBrowser = await createBrowser('playwright');
|
|
113
|
+
|
|
114
|
+
const puppeteerPage = await puppeteerBrowser.newPage();
|
|
115
|
+
const playwrightPage = await playwrightBrowser.newPage();
|
|
116
|
+
|
|
117
|
+
// Check that both have the same methods
|
|
118
|
+
const methods = [
|
|
119
|
+
'setExtraHTTPHeaders',
|
|
120
|
+
'setUserAgent',
|
|
121
|
+
'setViewport',
|
|
122
|
+
'goto',
|
|
123
|
+
'content',
|
|
124
|
+
'screenshot',
|
|
125
|
+
'close',
|
|
126
|
+
];
|
|
127
|
+
for (const method of methods) {
|
|
128
|
+
expect(puppeteerPage[method]).toBeDefined();
|
|
129
|
+
expect(playwrightPage[method]).toBeDefined();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
await puppeteerBrowser.close();
|
|
133
|
+
await playwrightBrowser.close();
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
});
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
// Unit tests for CLI argument parsing and functionality
|
|
2
|
+
import { spawn } from 'child_process';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import { dirname, resolve } from 'path';
|
|
5
|
+
|
|
6
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
7
|
+
const __dirname = dirname(__filename);
|
|
8
|
+
const cliPath = resolve(__dirname, '../../bin/web-capture.js');
|
|
9
|
+
|
|
10
|
+
// Helper function to run CLI and capture output
|
|
11
|
+
function runCli(args, options = {}) {
|
|
12
|
+
return new Promise((resolve, reject) => {
|
|
13
|
+
const proc = spawn('node', [cliPath, ...args], {
|
|
14
|
+
cwd: options.cwd || dirname(cliPath),
|
|
15
|
+
env: { ...process.env, ...options.env },
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
let stdout = '';
|
|
19
|
+
let stderr = '';
|
|
20
|
+
|
|
21
|
+
proc.stdout.on('data', (data) => {
|
|
22
|
+
stdout += data.toString();
|
|
23
|
+
});
|
|
24
|
+
proc.stderr.on('data', (data) => {
|
|
25
|
+
stderr += data.toString();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
proc.on('close', (code) => {
|
|
29
|
+
resolve({ code, stdout, stderr });
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
proc.on('error', reject);
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
describe('CLI', () => {
|
|
37
|
+
describe('--help', () => {
|
|
38
|
+
test('shows help message with --help flag', async () => {
|
|
39
|
+
const result = await runCli(['--help']);
|
|
40
|
+
expect(result.code).toBe(0);
|
|
41
|
+
expect(result.stdout).toContain('web-capture');
|
|
42
|
+
expect(result.stdout).toContain('Usage:');
|
|
43
|
+
expect(result.stdout).toContain('--serve');
|
|
44
|
+
expect(result.stdout).toContain('--format');
|
|
45
|
+
expect(result.stdout).toContain('--output');
|
|
46
|
+
expect(result.stdout).toContain('--engine');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('shows help message with -h flag', async () => {
|
|
50
|
+
const result = await runCli(['-h']);
|
|
51
|
+
expect(result.code).toBe(0);
|
|
52
|
+
expect(result.stdout).toContain('web-capture');
|
|
53
|
+
expect(result.stdout).toContain('Usage:');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('shows error and exits with code 1 when no arguments provided', async () => {
|
|
57
|
+
const result = await runCli([]);
|
|
58
|
+
expect(result.code).toBe(1);
|
|
59
|
+
// With lino-arguments and yargs, error messages go to stderr
|
|
60
|
+
expect(result.stderr).toContain('Error');
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe('--version', () => {
|
|
65
|
+
test('shows version with --version flag', async () => {
|
|
66
|
+
const result = await runCli(['--version']);
|
|
67
|
+
expect(result.code).toBe(0);
|
|
68
|
+
// yargs outputs just the version number
|
|
69
|
+
expect(result.stdout).toMatch(/\d+\.\d+\.\d+/);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('shows version with -v flag', async () => {
|
|
73
|
+
const result = await runCli(['-v']);
|
|
74
|
+
expect(result.code).toBe(0);
|
|
75
|
+
// yargs outputs just the version number
|
|
76
|
+
expect(result.stdout).toMatch(/\d+\.\d+\.\d+/);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe('URL validation', () => {
|
|
81
|
+
test('rejects invalid URL', async () => {
|
|
82
|
+
const result = await runCli(['not-a-valid-url-without-dots']);
|
|
83
|
+
// Should fail with invalid URL or show help due to strict mode
|
|
84
|
+
expect(result.code).toBe(1);
|
|
85
|
+
// Either an error message or help output (from yargs strict mode)
|
|
86
|
+
expect(result.stdout.length + result.stderr.length).toBeGreaterThan(0);
|
|
87
|
+
}, 15000);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe('format options', () => {
|
|
91
|
+
test('accepts --format html', async () => {
|
|
92
|
+
// Just test that the argument is accepted (actual fetch would fail without network)
|
|
93
|
+
const result = await runCli(['--help']);
|
|
94
|
+
expect(result.stdout).toContain('--format');
|
|
95
|
+
expect(result.stdout).toContain('html');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test('accepts --format markdown', async () => {
|
|
99
|
+
const result = await runCli(['--help']);
|
|
100
|
+
expect(result.stdout).toContain('markdown');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('accepts --format image', async () => {
|
|
104
|
+
const result = await runCli(['--help']);
|
|
105
|
+
expect(result.stdout).toContain('image');
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
describe('engine options', () => {
|
|
110
|
+
test('help shows engine options', async () => {
|
|
111
|
+
const result = await runCli(['--help']);
|
|
112
|
+
expect(result.stdout).toContain('--engine');
|
|
113
|
+
expect(result.stdout).toContain('puppeteer');
|
|
114
|
+
expect(result.stdout).toContain('playwright');
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test('accepts --engine option', async () => {
|
|
118
|
+
const result = await runCli(['--help']);
|
|
119
|
+
expect(result.stdout).toContain('--engine');
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
describe('server mode options', () => {
|
|
124
|
+
test('help shows --serve option', async () => {
|
|
125
|
+
const result = await runCli(['--help']);
|
|
126
|
+
expect(result.stdout).toContain('--serve');
|
|
127
|
+
expect(result.stdout).toContain('-s');
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test('help shows --port option', async () => {
|
|
131
|
+
const result = await runCli(['--help']);
|
|
132
|
+
expect(result.stdout).toContain('--port');
|
|
133
|
+
expect(result.stdout).toContain('-p');
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test('help shows API endpoints', async () => {
|
|
137
|
+
const result = await runCli(['--help']);
|
|
138
|
+
expect(result.stdout).toContain('/html');
|
|
139
|
+
expect(result.stdout).toContain('/markdown');
|
|
140
|
+
expect(result.stdout).toContain('/image');
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
describe('output options', () => {
|
|
145
|
+
test('help shows --output option', async () => {
|
|
146
|
+
const result = await runCli(['--help']);
|
|
147
|
+
expect(result.stdout).toContain('--output');
|
|
148
|
+
expect(result.stdout).toContain('-o');
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { convertRelativeUrls } from '../../src/lib.js';
|
|
2
|
+
|
|
3
|
+
describe('convertRelativeUrls', () => {
|
|
4
|
+
const baseUrl = 'https://example.com/';
|
|
5
|
+
|
|
6
|
+
it('should NOT inject runtimeHook if there are no <script> tags', () => {
|
|
7
|
+
const html = `<!DOCTYPE html><html><head><title>Test</title></head><body><h1>Hello</h1><p>World</p></body></html>`;
|
|
8
|
+
const result = convertRelativeUrls(html, baseUrl);
|
|
9
|
+
// Should not inject the runtimeHook
|
|
10
|
+
expect(result).not.toMatch(/MutationObserver/);
|
|
11
|
+
expect(result).not.toMatch(/fixAllUrls/);
|
|
12
|
+
expect(result).not.toMatch(/runtime JS hook/);
|
|
13
|
+
// Should not add any <script> tag
|
|
14
|
+
expect(result.match(/<script/gi)).toBeNull();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('should inject runtimeHook if there is a <script> tag', () => {
|
|
18
|
+
const html = `<!DOCTYPE html><html><head><title>Test</title><script src="foo.js"></script></head><body><h1>Hello</h1></body></html>`;
|
|
19
|
+
const result = convertRelativeUrls(html, baseUrl);
|
|
20
|
+
// Should inject the runtimeHook
|
|
21
|
+
expect(result).toMatch(/MutationObserver/);
|
|
22
|
+
expect(result).toMatch(/fixAllUrls/);
|
|
23
|
+
// Should add a <script> tag for the hook
|
|
24
|
+
expect((result.match(/<script/gi) || []).length).toBeGreaterThan(1);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('should inject runtimeHook if there is an inline <script>', () => {
|
|
28
|
+
const html = `<!DOCTYPE html><html><head><title>Test</title><script>console.log('hi');</script></head><body></body></html>`;
|
|
29
|
+
const result = convertRelativeUrls(html, baseUrl);
|
|
30
|
+
expect(result).toMatch(/MutationObserver/);
|
|
31
|
+
expect(result).toMatch(/fixAllUrls/);
|
|
32
|
+
expect((result.match(/<script/gi) || []).length).toBeGreaterThan(1);
|
|
33
|
+
});
|
|
34
|
+
});
|