single-file-cli 2.6.4 → 2.7.0
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/build.sh +1 -1
- package/deno.json +1 -1
- package/lib/browser.js +30 -3
- package/lib/cdp-client.js +26 -23
- package/lib/single-file-archive.js +7 -7
- package/lib/single-file-bundle.js +1 -1
- package/lib/version.js +1 -1
- package/options.js +2 -2
- package/package.json +1 -1
- package/test/e2e/lazy-loading.test.js +204 -0
- package/test/e2e/timeouts.test.js +101 -14
package/lib/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const version = "2.
|
|
1
|
+
export const version = "2.7.0";
|
package/options.js
CHANGED
|
@@ -73,7 +73,7 @@ const OPTIONS_INFO = [{
|
|
|
73
73
|
"browser-wait-end-delay": { description: "Time to wait after capturing the page in ms", type: "number" },
|
|
74
74
|
"browser-wait-until": { description: "When to consider the page is loaded (InteractiveTime, networkIdle, networkAlmostIdle, load, domContentLoaded)", type: "string", defaultValue: "networkIdle" },
|
|
75
75
|
"browser-wait-until-delay": { description: "Delay of time in ms to wait before considering the page is loaded when the value of --browser-wait-until is reached", type: "number", defaultValue: 1000 },
|
|
76
|
-
"browser-wait-until-fallback": { description: "
|
|
76
|
+
"browser-wait-until-fallback": { description: "Stop loading the page and capture it as it is when --browser-load-max-time expires before the value of --browser-wait-until is reached, provided the page has reached DOMContentLoaded; a load timeout error is thrown otherwise", type: "boolean", defaultValue: true },
|
|
77
77
|
}, {
|
|
78
78
|
"browser-mobile-emulation": { description: "Emulate a mobile device", type: "boolean" },
|
|
79
79
|
"browser-device-width": { description: "Width of the device viewport in pixels (default value is 360 when using --browser-mobile-emulation)", type: "number" },
|
|
@@ -136,7 +136,7 @@ const OPTIONS_INFO = [{
|
|
|
136
136
|
"create-root-directory": { description: "Create a root directory based on the timestamp", type: "boolean" },
|
|
137
137
|
"extract-data-from-page": { description: "Extract compressed data from the page instead of fetching the page in order to create universal self-extracting HTML files", type: "boolean", defaultValue: true },
|
|
138
138
|
"prevent-appended-data": { description: "Prevent appending data after the compressed data when creating self-extracting HTML files", type: "boolean" },
|
|
139
|
-
"declare-appended-data": { description: "Declare the data appended after the compressed data as the comment of the ZIP archive, for readers rejecting undeclared trailing bytes (e.g. java.util.zip); ZIP tools then print that data when listing the archive", type: "boolean" },
|
|
139
|
+
"declare-appended-data": { description: "Declare the data appended after the compressed data as the comment of the ZIP archive, for readers rejecting undeclared trailing bytes (e.g. java.util.zip); ZIP tools then print that data when listing the archive. Has no effect when the extra data is relocated ahead of the compressed data, because nothing is appended then", type: "boolean" },
|
|
140
140
|
"embed-screenshot": { description: "Embed a screenshot of the page as a PNG file in the compressed file (self-extracting HTML or ZIP file). When enabled, the resulting file can be read as a ZIP file or a PNG image.", type: "boolean" },
|
|
141
141
|
"embed-screenshot-options": { description: "Options passed to the CDP method `Page.captureScreenshot()` given as a JSON string (e.g. { \"captureBeyondViewport\": false })", type: "string" },
|
|
142
142
|
"embedded-image": { description: "Path to a PNG image to embed in the compressed file. Unlike --embed-screenshot it is also compatible with --crawl-save-archive, where it becomes the image of the whole archive.", type: "string" },
|
package/package.json
CHANGED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/* global URL */
|
|
2
|
+
|
|
3
|
+
// The options under "save deferred images" all work by lying to the page: the screen is reported as
|
|
4
|
+
// tall as the document, every IntersectionObserver is told its targets are visible, and a resize —
|
|
5
|
+
// optionally a scroll — is dispatched so the page reacts. Each lie is useful and each one is also a
|
|
6
|
+
// way to capture something the reader never saw, so every fixture here isolates one mechanism and
|
|
7
|
+
// asserts both directions: what the option must load, and what it must not invent.
|
|
8
|
+
//
|
|
9
|
+
// The pages are served rather than opened as file:// so the capture takes the path a real save
|
|
10
|
+
// takes, and they are deliberately trivial: a fixture that needs a real site to reproduce is a
|
|
11
|
+
// fixture that will start failing for reasons that have nothing to do with SingleFile.
|
|
12
|
+
|
|
13
|
+
import { test } from "node:test";
|
|
14
|
+
import assert from "node:assert/strict";
|
|
15
|
+
import { createServer } from "node:http";
|
|
16
|
+
import { execFile } from "node:child_process";
|
|
17
|
+
import { promisify } from "node:util";
|
|
18
|
+
import { mkdtemp, readFile, rm } from "node:fs/promises";
|
|
19
|
+
import { tmpdir } from "node:os";
|
|
20
|
+
import { join } from "node:path";
|
|
21
|
+
import process from "node:process";
|
|
22
|
+
import { Buffer } from "node:buffer";
|
|
23
|
+
import { cliDirectory } from "../target.js";
|
|
24
|
+
|
|
25
|
+
const execFileAsync = promisify(execFile);
|
|
26
|
+
|
|
27
|
+
const TIMEOUT = 180000;
|
|
28
|
+
const VIEWPORT = ["--browser-width", "1280", "--browser-height", "900"];
|
|
29
|
+
// a 1x1 gif, small enough to read as a marker in the saved page
|
|
30
|
+
const PIXEL = "data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==";
|
|
31
|
+
const BELOW_THE_FOLD = "min-height:3000px";
|
|
32
|
+
// what a lazy-loading library does, reduced to its essentials
|
|
33
|
+
const LAZY_IMAGE_SCRIPT = `new IntersectionObserver(entries => entries.forEach(entry => {
|
|
34
|
+
if (entry.isIntersecting) { entry.target.src = entry.target.dataset.src; }
|
|
35
|
+
})).observe(document.querySelector("#below"));`;
|
|
36
|
+
|
|
37
|
+
const PAGES = {
|
|
38
|
+
// the classic lazy image: the URL lives in a data attribute, so it is not in the markup and
|
|
39
|
+
// cannot be inlined unless the page itself decides the image became visible
|
|
40
|
+
"/native-lazy.html": `<html><head><title>native lazy</title></head><body>
|
|
41
|
+
<div style="${BELOW_THE_FOLD}">filler</div>
|
|
42
|
+
<img id="below" data-src="/deferred.gif" width="10" height="10">
|
|
43
|
+
<script>${LAZY_IMAGE_SCRIPT}</script>
|
|
44
|
+
</body></html>`,
|
|
45
|
+
|
|
46
|
+
// the shape every modern infinite-scroll library has: a sentinel at the end of the document,
|
|
47
|
+
// watched by an IntersectionObserver. Nothing here reacts to scroll events at all.
|
|
48
|
+
"/observer-append.html": `<html><head><title>Article One</title></head><body>
|
|
49
|
+
<article style="${BELOW_THE_FOLD}">Article One</article>
|
|
50
|
+
<div id="sentinel" style="height:1px"></div>
|
|
51
|
+
<script>
|
|
52
|
+
new IntersectionObserver(entries => {
|
|
53
|
+
if (entries.some(entry => entry.isIntersecting)) {
|
|
54
|
+
document.body.insertAdjacentHTML("beforeend", "<article>ARTICLE TWO</article>");
|
|
55
|
+
document.title = "Article Two";
|
|
56
|
+
}
|
|
57
|
+
}).observe(document.querySelector("#sentinel"));
|
|
58
|
+
</script>
|
|
59
|
+
</body></html>`,
|
|
60
|
+
|
|
61
|
+
// content revealed by a scroll listener, which is what the "dispatch scroll event" option exists
|
|
62
|
+
// for: no scroll event, no reveal, whatever the emulated screen size says
|
|
63
|
+
"/scroll-reveal.html": `<html><head><title>scroll reveal</title>
|
|
64
|
+
<style>#hidden{opacity:0}#hidden.revealed{opacity:1}</style></head><body>
|
|
65
|
+
<div style="${BELOW_THE_FOLD}">filler</div>
|
|
66
|
+
<p id="hidden">REVEALED CONTENT</p>
|
|
67
|
+
<script>addEventListener("scroll", () => document.querySelector("#hidden").classList.add("revealed"));</script>
|
|
68
|
+
</body></html>`,
|
|
69
|
+
|
|
70
|
+
// a stylesheet the page only injects once it believes the reader arrived, and not immediately:
|
|
71
|
+
// the pass has to still be open when it lands, which is what the idle time buys
|
|
72
|
+
"/late-stylesheet.html": `<html><head><title>late stylesheet</title></head><body>
|
|
73
|
+
<div style="${BELOW_THE_FOLD}">filler</div>
|
|
74
|
+
<p id="styled">styled by a stylesheet that arrives late</p>
|
|
75
|
+
<script>
|
|
76
|
+
addEventListener("scroll", () => setTimeout(() => {
|
|
77
|
+
const link = document.createElement("link");
|
|
78
|
+
link.rel = "stylesheet";
|
|
79
|
+
link.href = "/late.css";
|
|
80
|
+
document.head.appendChild(link);
|
|
81
|
+
}, 1200), { once: true });
|
|
82
|
+
</script>
|
|
83
|
+
</body></html>`,
|
|
84
|
+
|
|
85
|
+
// the #1603 shape: a "full screen" block whose height is written as an inline style from
|
|
86
|
+
// innerHeight on every resize. The pass resizes twice, so the value in the saved page must be
|
|
87
|
+
// the real viewport, never the emulated one.
|
|
88
|
+
"/viewport-height.html": `<html><head><title>viewport height</title></head><body>
|
|
89
|
+
<div id="cover">cover</div>
|
|
90
|
+
<div style="${BELOW_THE_FOLD}">filler</div>
|
|
91
|
+
<script>
|
|
92
|
+
function size() { document.querySelector("#cover").style.height = innerHeight + "px"; }
|
|
93
|
+
addEventListener("resize", size);
|
|
94
|
+
size();
|
|
95
|
+
</script>
|
|
96
|
+
</body></html>`,
|
|
97
|
+
|
|
98
|
+
// content in a nested scroller, which the emulation does not reach: the document itself does not
|
|
99
|
+
// scroll, so every faked getter is set to the value it already had
|
|
100
|
+
"/nested-scroller.html": `<html><head><title>nested scroller</title></head><body style="margin:0">
|
|
101
|
+
<div id="scroller" style="height:400px;overflow-y:auto">
|
|
102
|
+
<div style="${BELOW_THE_FOLD}">filler</div>
|
|
103
|
+
<img id="below" data-src="/deferred.gif" width="10" height="10">
|
|
104
|
+
</div>
|
|
105
|
+
<script>${LAZY_IMAGE_SCRIPT}</script>
|
|
106
|
+
</body></html>`
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
test("a lazy image below the fold is loaded only when the option is on", { timeout: TIMEOUT }, async () => {
|
|
110
|
+
await withFixture(async capture => {
|
|
111
|
+
const off = await capture("/native-lazy.html", ["--load-deferred-images", "false"]);
|
|
112
|
+
const on = await capture("/native-lazy.html");
|
|
113
|
+
assert.ok(!off.includes(PIXEL), "the deferred image was inlined with the option off");
|
|
114
|
+
assert.ok(on.includes(PIXEL), "the deferred image was not inlined with the option on");
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("an IntersectionObserver sentinel does not make the page append the next article", { timeout: TIMEOUT }, async () => {
|
|
119
|
+
await withFixture(async capture => {
|
|
120
|
+
// the pass reports every observed target as intersecting, so a sentinel at the end of the
|
|
121
|
+
// document is exactly the case where that lie captures a page the reader never opened
|
|
122
|
+
const content = await capture("/observer-append.html");
|
|
123
|
+
assert.ok(!content.includes("ARTICLE TWO"), "an article appended by the sentinel was saved");
|
|
124
|
+
assert.match(content, /<title>Article One<\/title>/, "the saved page carries the appended article's title");
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("content revealed by a scroll listener needs the dispatch-scroll-event option", { timeout: TIMEOUT }, async () => {
|
|
129
|
+
await withFixture(async capture => {
|
|
130
|
+
const off = await capture("/scroll-reveal.html");
|
|
131
|
+
const on = await capture("/scroll-reveal.html", ["--load-deferred-images-dispatch-scroll-event", "true"]);
|
|
132
|
+
assert.ok(!off.includes("class=revealed"), "the page was revealed without the option");
|
|
133
|
+
assert.ok(on.includes("class=revealed"), "the page was not revealed with the option");
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("a stylesheet injected late is captured, and is lost when the idle time is too short", { timeout: TIMEOUT }, async () => {
|
|
138
|
+
await withFixture(async capture => {
|
|
139
|
+
const scrollEvent = ["--load-deferred-images-dispatch-scroll-event", "true"];
|
|
140
|
+
const patient = await capture("/late-stylesheet.html", scrollEvent);
|
|
141
|
+
const impatient = await capture("/late-stylesheet.html", scrollEvent.concat(["--load-deferred-images-max-idle-time", "100"]));
|
|
142
|
+
assert.ok(patient.includes("LATE_RULE"), "the late stylesheet was not captured with the default idle time");
|
|
143
|
+
assert.ok(!impatient.includes("LATE_RULE"), "the late stylesheet was captured despite a 100ms idle time");
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test("a block sized from innerHeight keeps the real viewport height in the saved page", { timeout: TIMEOUT }, async () => {
|
|
148
|
+
await withFixture(async capture => {
|
|
149
|
+
const content = await capture("/viewport-height.html");
|
|
150
|
+
const height = Number((content.match(/id=cover style="?height:(\d+)px/) || [])[1]);
|
|
151
|
+
assert.ok(height, "the cover carries no inline height");
|
|
152
|
+
// the emulated screen is the whole document, so a leaked value is thousands of pixels
|
|
153
|
+
assert.ok(height < 1000, "the cover kept the emulated screen height: " + height + "px");
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test("the zoom-out leaves no transform behind in the saved page", { timeout: TIMEOUT }, async () => {
|
|
158
|
+
await withFixture(async capture => {
|
|
159
|
+
for (const options of [[], ["--load-deferred-images-keep-zoom-level", "true"]]) {
|
|
160
|
+
const content = await capture("/native-lazy.html", options);
|
|
161
|
+
assert.ok(!/scale3d/.test(content), "a zoom-out transform was left in the saved page");
|
|
162
|
+
assert.ok(!/-sf-transform/.test(content), "an internal transform property was left in the saved page");
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test("a lazy image inside a nested scroller is loaded", { timeout: TIMEOUT, todo: "the emulation only measures document.scrollingElement, so a page that scrolls in a nested container is never enlarged" }, async () => {
|
|
168
|
+
await withFixture(async capture => {
|
|
169
|
+
const content = await capture("/nested-scroller.html");
|
|
170
|
+
assert.ok(content.includes(PIXEL), "the deferred image inside the scroller was not inlined");
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
async function withFixture(run) {
|
|
175
|
+
const requestedPaths = [];
|
|
176
|
+
const server = createServer((request, response) => {
|
|
177
|
+
const { pathname } = new URL(request.url, "http://localhost");
|
|
178
|
+
requestedPaths.push(pathname);
|
|
179
|
+
if (pathname === "/deferred.gif") {
|
|
180
|
+
response.writeHead(200, { "content-type": "image/gif" })
|
|
181
|
+
.end(Buffer.from(PIXEL.slice(PIXEL.indexOf(",") + 1), "base64"));
|
|
182
|
+
} else if (pathname === "/late.css") {
|
|
183
|
+
response.writeHead(200, { "content-type": "text/css" }).end("#styled { color: rgb(1,2,3); --marker: LATE_RULE; }");
|
|
184
|
+
} else if (PAGES[pathname]) {
|
|
185
|
+
response.writeHead(200, { "content-type": "text/html; charset=utf-8" }).end(PAGES[pathname]);
|
|
186
|
+
} else {
|
|
187
|
+
response.writeHead(404).end();
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
await new Promise(resolve => server.listen(0, "localhost", resolve));
|
|
191
|
+
const directory = await mkdtemp(join(tmpdir(), "single-file-lazy-"));
|
|
192
|
+
let saved = 0;
|
|
193
|
+
try {
|
|
194
|
+
await run(async (path, options = []) => {
|
|
195
|
+
const outputPath = join(directory, "out-" + (saved++) + ".html");
|
|
196
|
+
const url = "http://localhost:" + server.address().port + path;
|
|
197
|
+
await execFileAsync(process.execPath, ["single-file-node.js", url, outputPath].concat(VIEWPORT, options), { cwd: cliDirectory });
|
|
198
|
+
return readFile(outputPath, "utf8");
|
|
199
|
+
});
|
|
200
|
+
} finally {
|
|
201
|
+
await rm(directory, { recursive: true });
|
|
202
|
+
server.close();
|
|
203
|
+
}
|
|
204
|
+
}
|
|
@@ -1,19 +1,16 @@
|
|
|
1
|
-
/* global setInterval, clearInterval */
|
|
1
|
+
/* global setInterval, clearInterval, setTimeout, clearTimeout */
|
|
2
2
|
|
|
3
3
|
import { test } from "node:test";
|
|
4
4
|
import assert from "node:assert/strict";
|
|
5
5
|
import { createServer } from "node:http";
|
|
6
6
|
import { execFile } from "node:child_process";
|
|
7
|
-
import {
|
|
8
|
-
import { mkdtemp, rm } from "node:fs/promises";
|
|
7
|
+
import { mkdtemp, rm, readFile } from "node:fs/promises";
|
|
9
8
|
import { Buffer } from "node:buffer";
|
|
10
9
|
import { tmpdir } from "node:os";
|
|
11
10
|
import { join } from "node:path";
|
|
12
11
|
import process from "node:process";
|
|
13
12
|
import { cliDirectory } from "../target.js";
|
|
14
13
|
|
|
15
|
-
const execFileAsync = promisify(execFile);
|
|
16
|
-
|
|
17
14
|
test("load timeout is reported when the page never loads", { timeout: 120000 }, async () => {
|
|
18
15
|
const server = createServer(() => { });
|
|
19
16
|
await new Promise(resolve => server.listen(0, "localhost", resolve));
|
|
@@ -33,6 +30,87 @@ test("load timeout is reported when the page never loads", { timeout: 120000 },
|
|
|
33
30
|
}
|
|
34
31
|
});
|
|
35
32
|
|
|
33
|
+
test("a page kept busy by an endless request is captured when the wait times out", { timeout: 120000 }, async () => {
|
|
34
|
+
const server = createServer((request, response) => {
|
|
35
|
+
if (request.url == "/stream") {
|
|
36
|
+
response.writeHead(200, { "content-type": "text/plain" });
|
|
37
|
+
const intervalId = setInterval(() => response.write("tick\n"), 200);
|
|
38
|
+
response.on("close", () => clearInterval(intervalId));
|
|
39
|
+
} else {
|
|
40
|
+
response.writeHead(200, { "content-type": "text/html" });
|
|
41
|
+
response.end("<!doctype html><title>busy</title><p id=marker>captured while a request never ends</p><script>fetch('/stream')</script>");
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
await new Promise(resolve => server.listen(0, "localhost", resolve));
|
|
45
|
+
const directory = await mkdtemp(join(tmpdir(), "single-file-test-"));
|
|
46
|
+
try {
|
|
47
|
+
const url = "http://localhost:" + server.address().port + "/";
|
|
48
|
+
const output = join(directory, "out.html");
|
|
49
|
+
const { stderr } = await runCli([url, output, "--browser-load-max-time", "3000"]);
|
|
50
|
+
assert.ok(stderr.includes("did not reach networkIdle"), "stderr: " + stderr);
|
|
51
|
+
assert.ok(stderr.includes("captured as it was at"), "stderr: " + stderr);
|
|
52
|
+
assert.ok(!stderr.includes("Load timeout"), "stderr: " + stderr);
|
|
53
|
+
const content = await readFile(output, "utf8");
|
|
54
|
+
assert.ok(content.includes("captured while a request never ends"), "the page content was not captured");
|
|
55
|
+
} finally {
|
|
56
|
+
await rm(directory, { recursive: true });
|
|
57
|
+
server.closeAllConnections();
|
|
58
|
+
server.close();
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("a page whose load event never fires is captured at DOMContentLoaded", { timeout: 120000 }, async () => {
|
|
63
|
+
const server = createServer((request, response) => {
|
|
64
|
+
if (request.url.startsWith("/hang")) {
|
|
65
|
+
setTimeout(() => {
|
|
66
|
+
response.writeHead(404);
|
|
67
|
+
response.end();
|
|
68
|
+
}, 10000);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
response.writeHead(200, { "content-type": "text/html" });
|
|
72
|
+
response.end("<!doctype html><title>hung</title><p id=marker>captured before load</p><img src=/hang1><img src=/hang2><img src=/hang3>");
|
|
73
|
+
});
|
|
74
|
+
await new Promise(resolve => server.listen(0, "localhost", resolve));
|
|
75
|
+
const directory = await mkdtemp(join(tmpdir(), "single-file-test-"));
|
|
76
|
+
try {
|
|
77
|
+
const url = "http://localhost:" + server.address().port + "/";
|
|
78
|
+
const output = join(directory, "out.html");
|
|
79
|
+
const { stderr } = await runCli([url, output, "--browser-load-max-time", "3000"]);
|
|
80
|
+
assert.ok(stderr.includes("captured as it was at DOMContentLoaded"), "stderr: " + stderr);
|
|
81
|
+
const content = await readFile(output, "utf8");
|
|
82
|
+
assert.ok(content.includes("captured before load"), "the page content was not captured");
|
|
83
|
+
} finally {
|
|
84
|
+
await rm(directory, { recursive: true });
|
|
85
|
+
server.closeAllConnections();
|
|
86
|
+
server.close();
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("the load timeout is reported when the fallback is disabled", { timeout: 120000 }, async () => {
|
|
91
|
+
const server = createServer((request, response) => {
|
|
92
|
+
if (request.url == "/stream") {
|
|
93
|
+
response.writeHead(200, { "content-type": "text/plain" });
|
|
94
|
+
const intervalId = setInterval(() => response.write("tick\n"), 200);
|
|
95
|
+
response.on("close", () => clearInterval(intervalId));
|
|
96
|
+
} else {
|
|
97
|
+
response.writeHead(200, { "content-type": "text/html" });
|
|
98
|
+
response.end("<!doctype html><title>busy</title><script>fetch('/stream')</script>");
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
await new Promise(resolve => server.listen(0, "localhost", resolve));
|
|
102
|
+
const directory = await mkdtemp(join(tmpdir(), "single-file-test-"));
|
|
103
|
+
try {
|
|
104
|
+
const url = "http://localhost:" + server.address().port + "/";
|
|
105
|
+
const { stderr } = await runCli([url, join(directory, "out.html"), "--browser-load-max-time", "3000", "--browser-wait-until-fallback", "false"]);
|
|
106
|
+
assert.ok(stderr.includes("Load timeout"), "stderr: " + stderr);
|
|
107
|
+
} finally {
|
|
108
|
+
await rm(directory, { recursive: true });
|
|
109
|
+
server.closeAllConnections();
|
|
110
|
+
server.close();
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
36
114
|
test("capture timeout is reported when capturing an endless download", { timeout: 120000 }, async () => {
|
|
37
115
|
const chunk = Buffer.alloc(65536);
|
|
38
116
|
const server = createServer((_, response) => {
|
|
@@ -57,13 +135,22 @@ test("capture timeout is reported when capturing an endless download", { timeout
|
|
|
57
135
|
}
|
|
58
136
|
});
|
|
59
137
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
138
|
+
function runCli(args) {
|
|
139
|
+
return new Promise((resolve, reject) => {
|
|
140
|
+
let terminated = false, killTimeoutId;
|
|
141
|
+
const child = execFile(process.execPath, ["single-file-node.js", ...args], { cwd: cliDirectory }, (error, stdout, stderr) => {
|
|
142
|
+
clearTimeout(terminateTimeoutId);
|
|
143
|
+
clearTimeout(killTimeoutId);
|
|
144
|
+
if (terminated) {
|
|
145
|
+
reject(new Error("the process did not exit before the timeout"));
|
|
146
|
+
} else {
|
|
147
|
+
resolve({ stdout, stderr });
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
const terminateTimeoutId = setTimeout(() => {
|
|
151
|
+
terminated = true;
|
|
152
|
+
child.kill("SIGTERM");
|
|
153
|
+
killTimeoutId = setTimeout(() => child.kill("SIGKILL"), 10000);
|
|
154
|
+
}, 90000);
|
|
155
|
+
});
|
|
69
156
|
}
|