@link-assistant/web-capture 1.12.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 +17 -0
- package/README.md +2 -1
- package/bin/web-capture.js +5 -7
- package/package.json +32 -18
- package/src/archive.js +1 -2
- package/src/binary-download.js +13 -0
- package/src/docx.js +14 -11
- package/src/fetch.js +1 -2
- package/src/figures.js +2 -12
- package/src/gdocs-images.js +0 -1
- package/src/gdocs.js +0 -1
- package/src/github.js +0 -1
- package/src/lib.js +1 -2
- package/src/localize-images.js +2 -12
- 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/babel.config.cjs +0 -13
- 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/html_download.py +0 -15
- package/examples/python/image_download.py +0 -20
- package/examples/python/markdown_download.py +0 -15
- package/examples/python/playwright_example.py +0 -22
- package/jest.config.mjs +0 -20
- package/tests/ci/dependency-freshness.test.js +0 -74
- package/tests/ci/detect-code-changes.test.js +0 -174
- 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/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 -128
- 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 -137
- 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 -142
- 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 -57
- 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 -85
- 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,54 +0,0 @@
|
|
|
1
|
-
import { readFileSync } from 'fs';
|
|
2
|
-
import { dirname, resolve } from 'path';
|
|
3
|
-
import { fileURLToPath } from 'url';
|
|
4
|
-
|
|
5
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
6
|
-
const __dirname = dirname(__filename);
|
|
7
|
-
const repoRoot = resolve(__dirname, '../../..');
|
|
8
|
-
|
|
9
|
-
function parseVersion(version) {
|
|
10
|
-
return version.split('.').map((part) => Number(part));
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
function compareVersions(left, right) {
|
|
14
|
-
const leftParts = parseVersion(left);
|
|
15
|
-
const rightParts = parseVersion(right);
|
|
16
|
-
const maxLength = Math.max(leftParts.length, rightParts.length);
|
|
17
|
-
|
|
18
|
-
for (let index = 0; index < maxLength; index += 1) {
|
|
19
|
-
const leftPart = leftParts[index] || 0;
|
|
20
|
-
const rightPart = rightParts[index] || 0;
|
|
21
|
-
|
|
22
|
-
if (leftPart !== rightPart) {
|
|
23
|
-
return leftPart - rightPart;
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
return 0;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
describe('Rust Dockerfile', () => {
|
|
31
|
-
test('uses a Rust builder image that satisfies Cargo.toml rust-version', () => {
|
|
32
|
-
const cargoToml = readFileSync(
|
|
33
|
-
resolve(repoRoot, 'rust/Cargo.toml'),
|
|
34
|
-
'utf8'
|
|
35
|
-
);
|
|
36
|
-
const dockerfile = readFileSync(
|
|
37
|
-
resolve(repoRoot, 'rust/Dockerfile'),
|
|
38
|
-
'utf8'
|
|
39
|
-
);
|
|
40
|
-
|
|
41
|
-
const requiredRustVersion = cargoToml.match(
|
|
42
|
-
/^rust-version = "([^"]+)"/m
|
|
43
|
-
)?.[1];
|
|
44
|
-
const dockerRustVersion = dockerfile.match(
|
|
45
|
-
/^FROM rust:(\d+(?:\.\d+){1,2})[-\s]/m
|
|
46
|
-
)?.[1];
|
|
47
|
-
|
|
48
|
-
expect(requiredRustVersion).toBeDefined();
|
|
49
|
-
expect(dockerRustVersion).toBeDefined();
|
|
50
|
-
expect(
|
|
51
|
-
compareVersions(dockerRustVersion, requiredRustVersion)
|
|
52
|
-
).toBeGreaterThanOrEqual(0);
|
|
53
|
-
});
|
|
54
|
-
});
|
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
import { readFileSync } from 'fs';
|
|
2
|
-
import { dirname, resolve } from 'path';
|
|
3
|
-
import { fileURLToPath } from 'url';
|
|
4
|
-
|
|
5
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
6
|
-
const __dirname = dirname(__filename);
|
|
7
|
-
const repoRoot = resolve(__dirname, '../../..');
|
|
8
|
-
|
|
9
|
-
const workflowFiles = [
|
|
10
|
-
'.github/workflows/js.yml',
|
|
11
|
-
'.github/workflows/rust.yml',
|
|
12
|
-
'.github/workflows/parity.yml',
|
|
13
|
-
];
|
|
14
|
-
|
|
15
|
-
function readRootFile(relativePath) {
|
|
16
|
-
return readFileSync(resolve(repoRoot, relativePath), 'utf8');
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function readWorkflow(relativePath) {
|
|
20
|
-
return readRootFile(relativePath);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
describe('workflow policy', () => {
|
|
24
|
-
test.each(workflowFiles)(
|
|
25
|
-
'%s uses release-safe concurrency cancellation',
|
|
26
|
-
(workflowFile) => {
|
|
27
|
-
const workflow = readWorkflow(workflowFile);
|
|
28
|
-
|
|
29
|
-
expect(workflow).toContain(
|
|
30
|
-
"cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}"
|
|
31
|
-
);
|
|
32
|
-
expect(workflow).not.toContain(
|
|
33
|
-
"cancel-in-progress: ${{ github.ref == 'refs/heads/main' }}"
|
|
34
|
-
);
|
|
35
|
-
}
|
|
36
|
-
);
|
|
37
|
-
|
|
38
|
-
test.each(workflowFiles)(
|
|
39
|
-
'%s uses the current checkout action',
|
|
40
|
-
(workflowFile) => {
|
|
41
|
-
const workflow = readWorkflow(workflowFile);
|
|
42
|
-
|
|
43
|
-
expect(workflow).toContain('uses: actions/checkout@v6');
|
|
44
|
-
expect(workflow).not.toContain('uses: actions/checkout@v4');
|
|
45
|
-
}
|
|
46
|
-
);
|
|
47
|
-
|
|
48
|
-
test('Rust workflow uses the current cache action', () => {
|
|
49
|
-
const workflow = readWorkflow('.github/workflows/rust.yml');
|
|
50
|
-
|
|
51
|
-
expect(workflow).toContain('uses: actions/cache@v5');
|
|
52
|
-
expect(workflow).not.toContain('uses: actions/cache@v4');
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
test('parity check ignores repository-level CI tests', () => {
|
|
56
|
-
const parityScript = readRootFile('scripts/check-js-rust-parity.mjs');
|
|
57
|
-
|
|
58
|
-
expect(parityScript).toContain('"js/tests/ci/"');
|
|
59
|
-
expect(parityScript).toContain(
|
|
60
|
-
'matchingFiles(files, JS_PARITY_PATHS, JS_PARITY_IGNORE_PATHS)'
|
|
61
|
-
);
|
|
62
|
-
});
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
test('release workflows require dependency health gates', () => {
|
|
66
|
-
const js = readWorkflow('.github/workflows/js.yml');
|
|
67
|
-
expect(js).toContain('npm audit --package-lock-only --audit-level=moderate');
|
|
68
|
-
expect(js).toContain('check-dependency-freshness.mjs npm');
|
|
69
|
-
expect(js).toContain('needs: [lint, test, dependency-health]');
|
|
70
|
-
const rust = readWorkflow('.github/workflows/rust.yml');
|
|
71
|
-
expect(rust).toContain('check-dependency-freshness.mjs cargo');
|
|
72
|
-
expect(rust).toContain('cargo update --dry-run');
|
|
73
|
-
expect(rust).toContain('needs: [lint, test, build, dependency-health]');
|
|
74
|
-
for (const workflow of [js, rust]) {
|
|
75
|
-
expect(
|
|
76
|
-
workflow.match(/needs\.dependency-health\.result == 'success'/g)
|
|
77
|
-
).toHaveLength(2);
|
|
78
|
-
}
|
|
79
|
-
});
|
package/tests/e2e/docker.test.js
DELETED
|
@@ -1,186 +0,0 @@
|
|
|
1
|
-
import fetch from 'node-fetch';
|
|
2
|
-
import http from 'http';
|
|
3
|
-
import getPort from 'get-port';
|
|
4
|
-
import { exec } from 'child_process';
|
|
5
|
-
import { promisify } from 'util';
|
|
6
|
-
|
|
7
|
-
const execAsync = promisify(exec);
|
|
8
|
-
const WAIT_FOR_READY = 10000; // ms
|
|
9
|
-
const PORT = 3000; // Use the same port as in docker-compose.yml
|
|
10
|
-
const baseUrl = `http://localhost:${PORT}`;
|
|
11
|
-
|
|
12
|
-
const timings = {};
|
|
13
|
-
let mockServer;
|
|
14
|
-
let mockUrl;
|
|
15
|
-
|
|
16
|
-
const MOCK_HTML = `<!DOCTYPE html>
|
|
17
|
-
<html lang="en">
|
|
18
|
-
<head><meta charset="utf-8"><title>Example Domain</title></head>
|
|
19
|
-
<body><h1>Example Domain</h1><p>This domain is for use in illustrative examples.</p></body>
|
|
20
|
-
</html>`;
|
|
21
|
-
|
|
22
|
-
async function isServiceRunning() {
|
|
23
|
-
try {
|
|
24
|
-
const res = await fetch(`${baseUrl}/health`);
|
|
25
|
-
return res.status === 200;
|
|
26
|
-
} catch {
|
|
27
|
-
return false;
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
beforeAll(async () => {
|
|
32
|
-
timings.start = Date.now();
|
|
33
|
-
|
|
34
|
-
// Start a local mock server to avoid depending on external network
|
|
35
|
-
const mockPort = await getPort();
|
|
36
|
-
mockServer = http.createServer((req, res) => {
|
|
37
|
-
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
38
|
-
res.end(MOCK_HTML);
|
|
39
|
-
});
|
|
40
|
-
await new Promise((resolve) =>
|
|
41
|
-
mockServer.listen(mockPort, '0.0.0.0', resolve)
|
|
42
|
-
);
|
|
43
|
-
// docker-compose.yml maps this hostname to the host gateway on Linux CI.
|
|
44
|
-
mockUrl = `http://host.docker.internal:${mockPort}`;
|
|
45
|
-
|
|
46
|
-
console.log('Checking if Docker service is already running...');
|
|
47
|
-
const alreadyRunning = await isServiceRunning();
|
|
48
|
-
if (!alreadyRunning) {
|
|
49
|
-
console.log('Service not running, starting Docker container...');
|
|
50
|
-
try {
|
|
51
|
-
const dockerStart = Date.now();
|
|
52
|
-
// Start the Docker container using docker compose (v2 command)
|
|
53
|
-
const { stdout, stderr } = await execAsync('docker compose up -d');
|
|
54
|
-
timings.dockerStartup = Date.now() - dockerStart;
|
|
55
|
-
console.log('Docker compose output:', stdout);
|
|
56
|
-
if (stderr) {
|
|
57
|
-
console.error('Docker compose errors:', stderr);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// Wait for the service to be ready
|
|
61
|
-
const readyStart = Date.now();
|
|
62
|
-
await new Promise((resolve, reject) => {
|
|
63
|
-
const timeout = setTimeout(() => {
|
|
64
|
-
reject(new Error('Docker service did not start in time'));
|
|
65
|
-
}, WAIT_FOR_READY);
|
|
66
|
-
|
|
67
|
-
const checkReady = async () => {
|
|
68
|
-
try {
|
|
69
|
-
console.log(`Checking if service is ready at ${baseUrl}...`);
|
|
70
|
-
const res = await fetch(`${baseUrl}/health`);
|
|
71
|
-
if (res.status === 200) {
|
|
72
|
-
console.log('Service is ready!');
|
|
73
|
-
clearTimeout(timeout);
|
|
74
|
-
resolve();
|
|
75
|
-
} else {
|
|
76
|
-
console.log(`Service not ready yet, status: ${res.status}`);
|
|
77
|
-
setTimeout(checkReady, 500);
|
|
78
|
-
}
|
|
79
|
-
} catch (err) {
|
|
80
|
-
console.log('Service not ready yet, error:', err.message);
|
|
81
|
-
setTimeout(checkReady, 500);
|
|
82
|
-
}
|
|
83
|
-
};
|
|
84
|
-
checkReady();
|
|
85
|
-
});
|
|
86
|
-
timings.serviceReady = Date.now() - readyStart;
|
|
87
|
-
} catch (error) {
|
|
88
|
-
console.error('Failed to start Docker container:', error);
|
|
89
|
-
throw error;
|
|
90
|
-
}
|
|
91
|
-
timings.beforeAll = Date.now() - timings.start;
|
|
92
|
-
console.log('Timing: Docker startup:', `${timings.dockerStartup}ms`);
|
|
93
|
-
console.log('Timing: Service readiness:', `${timings.serviceReady}ms`);
|
|
94
|
-
console.log('Timing: beforeAll total:', `${timings.beforeAll}ms`);
|
|
95
|
-
} else {
|
|
96
|
-
console.log('Docker service is already running. Skipping startup.');
|
|
97
|
-
timings.beforeAll = Date.now() - timings.start;
|
|
98
|
-
console.log(
|
|
99
|
-
'Timing: beforeAll (already running):',
|
|
100
|
-
`${timings.beforeAll}ms`
|
|
101
|
-
);
|
|
102
|
-
}
|
|
103
|
-
}, 30000); // Reduced timeout for beforeAll
|
|
104
|
-
|
|
105
|
-
afterAll(async () => {
|
|
106
|
-
if (mockServer) {
|
|
107
|
-
mockServer.close();
|
|
108
|
-
}
|
|
109
|
-
// Stop Docker containers
|
|
110
|
-
try {
|
|
111
|
-
await execAsync('docker compose down');
|
|
112
|
-
} catch {
|
|
113
|
-
// Ignore errors during cleanup
|
|
114
|
-
}
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
describe('E2E (Docker): Web Capture Microservice', () => {
|
|
118
|
-
it('should return HTML from /html endpoint', async () => {
|
|
119
|
-
const htmlStart = Date.now();
|
|
120
|
-
const res = await fetch(
|
|
121
|
-
`${baseUrl}/html?url=${encodeURIComponent(mockUrl)}`
|
|
122
|
-
);
|
|
123
|
-
expect(res.status).toBe(200);
|
|
124
|
-
const text = await res.text();
|
|
125
|
-
expect(text).toMatch(/<html/i);
|
|
126
|
-
timings.html = Date.now() - htmlStart;
|
|
127
|
-
console.log('Timing: /html endpoint:', `${timings.html}ms`);
|
|
128
|
-
}, 20000);
|
|
129
|
-
|
|
130
|
-
it('should return Markdown from /markdown endpoint', async () => {
|
|
131
|
-
const mdStart = Date.now();
|
|
132
|
-
const res = await fetch(
|
|
133
|
-
`${baseUrl}/markdown?url=${encodeURIComponent(mockUrl)}`
|
|
134
|
-
);
|
|
135
|
-
expect(res.status).toBe(200);
|
|
136
|
-
const text = await res.text();
|
|
137
|
-
expect(text).toMatch(/example/i);
|
|
138
|
-
timings.markdown = Date.now() - mdStart;
|
|
139
|
-
console.log('Timing: /markdown endpoint:', `${timings.markdown}ms`);
|
|
140
|
-
}, 20000);
|
|
141
|
-
|
|
142
|
-
it('should return PNG from /image endpoint', async () => {
|
|
143
|
-
const pngStart = Date.now();
|
|
144
|
-
const res = await fetch(
|
|
145
|
-
`${baseUrl}/image?url=${encodeURIComponent(mockUrl)}`
|
|
146
|
-
);
|
|
147
|
-
expect(res.status).toBe(200);
|
|
148
|
-
expect(res.headers.get('content-type')).toMatch(/^image\/png/);
|
|
149
|
-
const buf = Buffer.from(await res.arrayBuffer());
|
|
150
|
-
// PNG signature check
|
|
151
|
-
const pngSignature = Buffer.from([
|
|
152
|
-
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a,
|
|
153
|
-
]);
|
|
154
|
-
expect(buf.slice(0, 8)).toEqual(pngSignature);
|
|
155
|
-
expect(buf.length).toBeGreaterThan(100); // Should be a non-trivial PNG
|
|
156
|
-
timings.png = Date.now() - pngStart;
|
|
157
|
-
console.log('Timing: /image endpoint:', `${timings.png}ms`);
|
|
158
|
-
}, 60000);
|
|
159
|
-
|
|
160
|
-
it('should stream content from /stream endpoint', async () => {
|
|
161
|
-
const startTime = Date.now();
|
|
162
|
-
const res = await fetch(
|
|
163
|
-
`${baseUrl}/stream?url=${encodeURIComponent(mockUrl)}`
|
|
164
|
-
);
|
|
165
|
-
expect(res.status).toBe(200);
|
|
166
|
-
// Get the response as text
|
|
167
|
-
const text = await res.text();
|
|
168
|
-
expect(text).toMatch(/<html/i);
|
|
169
|
-
expect(text).toMatch(/Example Domain/i);
|
|
170
|
-
const endTime = Date.now();
|
|
171
|
-
console.log(`Timing: /stream endpoint: ${endTime - startTime}ms`);
|
|
172
|
-
}, 20000);
|
|
173
|
-
|
|
174
|
-
it('should return content from /fetch endpoint', async () => {
|
|
175
|
-
const startTime = Date.now();
|
|
176
|
-
const res = await fetch(
|
|
177
|
-
`${baseUrl}/fetch?url=${encodeURIComponent(mockUrl)}`
|
|
178
|
-
);
|
|
179
|
-
expect(res.status).toBe(200);
|
|
180
|
-
const text = await res.text();
|
|
181
|
-
expect(text).toMatch(/<html/i);
|
|
182
|
-
expect(text).toMatch(/Example Domain/i);
|
|
183
|
-
const endTime = Date.now();
|
|
184
|
-
console.log(`Timing: /fetch endpoint: ${endTime - startTime}ms`);
|
|
185
|
-
}, 20000);
|
|
186
|
-
});
|
|
@@ -1,215 +0,0 @@
|
|
|
1
|
-
import { spawn } from 'child_process';
|
|
2
|
-
import fetch from 'node-fetch';
|
|
3
|
-
import getPort from 'get-port';
|
|
4
|
-
import path from 'path';
|
|
5
|
-
|
|
6
|
-
const WAIT_FOR_READY = 5000; // ms
|
|
7
|
-
let serverProcess;
|
|
8
|
-
let serverOutput = '';
|
|
9
|
-
let baseUrl;
|
|
10
|
-
let mockProcess;
|
|
11
|
-
let mockUrl;
|
|
12
|
-
|
|
13
|
-
const MOCK_HTML = `<!DOCTYPE html>
|
|
14
|
-
<html lang="en">
|
|
15
|
-
<head><meta charset="utf-8"><title>Example Domain</title></head>
|
|
16
|
-
<body><h1>Example Domain</h1><p>This domain is for use in illustrative examples.</p></body>
|
|
17
|
-
</html>`;
|
|
18
|
-
|
|
19
|
-
beforeAll(async () => {
|
|
20
|
-
const mockPort = await getPort();
|
|
21
|
-
mockProcess = startMockServer(mockPort);
|
|
22
|
-
await waitForProcessOutput(mockProcess, 'mock fixture server listening');
|
|
23
|
-
mockUrl = `http://127.0.0.1:${mockPort}`;
|
|
24
|
-
|
|
25
|
-
const port = await getPort();
|
|
26
|
-
baseUrl = `http://127.0.0.1:${port}`;
|
|
27
|
-
|
|
28
|
-
serverProcess = spawn(
|
|
29
|
-
'node',
|
|
30
|
-
[path.resolve('bin/web-capture.js'), '--serve', '--port', port.toString()],
|
|
31
|
-
{
|
|
32
|
-
env: process.env,
|
|
33
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
34
|
-
}
|
|
35
|
-
);
|
|
36
|
-
collectProcessOutput(serverProcess, (chunk) => {
|
|
37
|
-
serverOutput += chunk;
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
// Wait for the server to be ready (simple delay or poll)
|
|
41
|
-
await new Promise((resolve, reject) => {
|
|
42
|
-
const timeout = setTimeout(() => {
|
|
43
|
-
reject(new Error('Server did not start in time'));
|
|
44
|
-
}, WAIT_FOR_READY);
|
|
45
|
-
|
|
46
|
-
let serverStarted = false;
|
|
47
|
-
serverProcess.stdout.on('data', (data) => {
|
|
48
|
-
if (
|
|
49
|
-
data.toString().includes('listening') ||
|
|
50
|
-
data.toString().includes('Server running')
|
|
51
|
-
) {
|
|
52
|
-
serverStarted = true;
|
|
53
|
-
clearTimeout(timeout);
|
|
54
|
-
resolve();
|
|
55
|
-
}
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
serverProcess.on('error', (error) => {
|
|
59
|
-
clearTimeout(timeout);
|
|
60
|
-
reject(error);
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
serverProcess.on('exit', (code, signal) => {
|
|
64
|
-
if (!serverStarted) {
|
|
65
|
-
clearTimeout(timeout);
|
|
66
|
-
reject(
|
|
67
|
-
new Error(`Server process exited with code ${code}, signal ${signal}`)
|
|
68
|
-
);
|
|
69
|
-
}
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
// Fallback: resolve after WAIT_FOR_READY
|
|
73
|
-
setTimeout(resolve, WAIT_FOR_READY);
|
|
74
|
-
});
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
afterAll(() => {
|
|
78
|
-
if (serverProcess) {
|
|
79
|
-
serverProcess.kill();
|
|
80
|
-
}
|
|
81
|
-
if (mockProcess) {
|
|
82
|
-
mockProcess.kill();
|
|
83
|
-
}
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
describe('E2E: Web Capture Microservice', () => {
|
|
87
|
-
it('should return HTML from /html endpoint', async () => {
|
|
88
|
-
const res = await fetch(
|
|
89
|
-
`${baseUrl}/html?url=${encodeURIComponent(mockUrl)}`
|
|
90
|
-
);
|
|
91
|
-
const text = await expectTextResponse(res);
|
|
92
|
-
expect(text).toMatch(/<html/i);
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
it('should return Markdown from /markdown endpoint', async () => {
|
|
96
|
-
const res = await fetch(
|
|
97
|
-
`${baseUrl}/markdown?url=${encodeURIComponent(mockUrl)}`
|
|
98
|
-
);
|
|
99
|
-
const text = await expectTextResponse(res);
|
|
100
|
-
expect(text).toMatch(/example/i);
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
it('should return PNG from /image endpoint', async () => {
|
|
104
|
-
const res = await fetch(
|
|
105
|
-
`${baseUrl}/image?url=${encodeURIComponent(mockUrl)}`
|
|
106
|
-
);
|
|
107
|
-
await expectStatusOk(res);
|
|
108
|
-
expect(res.headers.get('content-type')).toMatch(/^image\/png/);
|
|
109
|
-
const buf = Buffer.from(await res.arrayBuffer());
|
|
110
|
-
// PNG signature check
|
|
111
|
-
const pngSignature = Buffer.from([
|
|
112
|
-
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a,
|
|
113
|
-
]);
|
|
114
|
-
expect(buf.slice(0, 8)).toEqual(pngSignature);
|
|
115
|
-
expect(buf.length).toBeGreaterThan(100); // Should be a non-trivial PNG
|
|
116
|
-
}, 60000);
|
|
117
|
-
|
|
118
|
-
it('should stream content from /stream endpoint', async () => {
|
|
119
|
-
const res = await fetch(
|
|
120
|
-
`${baseUrl}/stream?url=${encodeURIComponent(mockUrl)}`
|
|
121
|
-
);
|
|
122
|
-
const text = await expectTextResponse(res);
|
|
123
|
-
expect(text).toMatch(/<html/i);
|
|
124
|
-
expect(text).toMatch(/Example Domain/i);
|
|
125
|
-
}, 20000);
|
|
126
|
-
|
|
127
|
-
it('should return content from /fetch endpoint', async () => {
|
|
128
|
-
const res = await fetch(
|
|
129
|
-
`${baseUrl}/fetch?url=${encodeURIComponent(mockUrl)}`
|
|
130
|
-
);
|
|
131
|
-
const text = await expectTextResponse(res);
|
|
132
|
-
expect(text).toMatch(/<html/i);
|
|
133
|
-
expect(text).toMatch(/Example Domain/i);
|
|
134
|
-
}, 20000);
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
function startMockServer(port) {
|
|
138
|
-
const script = `
|
|
139
|
-
const http = require('http');
|
|
140
|
-
const html = ${JSON.stringify(MOCK_HTML)};
|
|
141
|
-
const server = http.createServer((req, res) => {
|
|
142
|
-
res.writeHead(200, {
|
|
143
|
-
'Content-Type': 'text/html; charset=utf-8',
|
|
144
|
-
'Content-Length': Buffer.byteLength(html),
|
|
145
|
-
});
|
|
146
|
-
res.end(html);
|
|
147
|
-
});
|
|
148
|
-
server.listen(${port}, '127.0.0.1', () => {
|
|
149
|
-
console.log('mock fixture server listening');
|
|
150
|
-
});
|
|
151
|
-
process.on('SIGTERM', () => server.close(() => process.exit(0)));
|
|
152
|
-
process.on('SIGINT', () => server.close(() => process.exit(0)));
|
|
153
|
-
`;
|
|
154
|
-
return spawn('node', ['-e', script], {
|
|
155
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
156
|
-
});
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
function collectProcessOutput(child, onChunk) {
|
|
160
|
-
child.stdout.on('data', (data) => onChunk(data.toString()));
|
|
161
|
-
child.stderr.on('data', (data) => onChunk(data.toString()));
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
async function waitForProcessOutput(child, expected) {
|
|
165
|
-
let output = '';
|
|
166
|
-
collectProcessOutput(child, (chunk) => {
|
|
167
|
-
output += chunk;
|
|
168
|
-
});
|
|
169
|
-
await new Promise((resolve, reject) => {
|
|
170
|
-
const timeout = setTimeout(() => {
|
|
171
|
-
reject(
|
|
172
|
-
new Error(`Timed out waiting for "${expected}". Output:\n${output}`)
|
|
173
|
-
);
|
|
174
|
-
}, WAIT_FOR_READY);
|
|
175
|
-
child.stdout.on('data', (data) => {
|
|
176
|
-
if (data.toString().includes(expected)) {
|
|
177
|
-
clearTimeout(timeout);
|
|
178
|
-
resolve();
|
|
179
|
-
}
|
|
180
|
-
});
|
|
181
|
-
child.on('exit', (code, signal) => {
|
|
182
|
-
clearTimeout(timeout);
|
|
183
|
-
reject(new Error(`Process exited with code ${code}, signal ${signal}`));
|
|
184
|
-
});
|
|
185
|
-
});
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
async function expectTextResponse(res) {
|
|
189
|
-
let text;
|
|
190
|
-
try {
|
|
191
|
-
text = await res.text();
|
|
192
|
-
} catch (error) {
|
|
193
|
-
throw new Error(
|
|
194
|
-
`${error.message}\nServer output:\n${serverOutput || '(empty)'}`,
|
|
195
|
-
{ cause: error }
|
|
196
|
-
);
|
|
197
|
-
}
|
|
198
|
-
expectResponseStatus(res.status, text);
|
|
199
|
-
return text;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
async function expectStatusOk(res) {
|
|
203
|
-
if (res.status === 200) {
|
|
204
|
-
return;
|
|
205
|
-
}
|
|
206
|
-
expectResponseStatus(res.status, await res.text());
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
function expectResponseStatus(status, body) {
|
|
210
|
-
if (status !== 200) {
|
|
211
|
-
throw new Error(
|
|
212
|
-
`Expected HTTP 200, received ${status}\nBody:\n${body}\nServer output:\n${serverOutput}`
|
|
213
|
-
);
|
|
214
|
-
}
|
|
215
|
-
}
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
<!doctype html>
|
|
2
|
-
<html>
|
|
3
|
-
<body>
|
|
4
|
-
<!-- Symptom A: two adjacent bold spans -->
|
|
5
|
-
<p>
|
|
6
|
-
<span style="font-weight: 700">13.1 </span
|
|
7
|
-
><span style="font-weight: 700">First subsection</span>
|
|
8
|
-
</p>
|
|
9
|
-
|
|
10
|
-
<!-- Symptom B: outer bold ends, inner bold begins, no separator -->
|
|
11
|
-
<ol>
|
|
12
|
-
<li><span style="font-weight: 700">Item header.</span></li>
|
|
13
|
-
</ol>
|
|
14
|
-
<p><span style="font-weight: 700">Note:</span> body text.</p>
|
|
15
|
-
|
|
16
|
-
<!-- Symptom C: bold range spans across <br> and <img> -->
|
|
17
|
-
<p>
|
|
18
|
-
<span style="font-weight: 700"
|
|
19
|
-
>Caption A:<img
|
|
20
|
-
alt=""
|
|
21
|
-
src="data:image/png;base64,iVBORw0KGgo=" /><br />Caption B:<img
|
|
22
|
-
alt=""
|
|
23
|
-
src="data:image/png;base64,iVBORw0KGgo="
|
|
24
|
-
/></span>
|
|
25
|
-
</p>
|
|
26
|
-
</body>
|
|
27
|
-
</html>
|
|
Binary file
|