phantomas 2.0.0 → 2.4.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.
Files changed (54) hide show
  1. package/Dockerfile +39 -25
  2. package/README.md +20 -8
  3. package/bin/phantomas.js +9 -164
  4. package/bin/program.js +198 -0
  5. package/bin/utils.js +16 -0
  6. package/core/modules/navigationTiming/scope.js +7 -1
  7. package/core/modules/requestsMonitor/requestsMonitor.js +30 -13
  8. package/core/scope.js +2 -1
  9. package/extensions/cookies/cookies.js +1 -2
  10. package/extensions/filmStrip/filmStrip.js +1 -1
  11. package/extensions/pageSource/pageSource.js +2 -0
  12. package/extensions/pageStorage/pageStorage.js +82 -0
  13. package/extensions/screenshot/screenshot.js +27 -9
  14. package/extensions/scroll/scroll.js +3 -1
  15. package/extensions/userAgent/userAgent.js +55 -0
  16. package/extensions/viewport/viewport.js +1 -1
  17. package/extensions/waitForSelector/waitForSelector.js +0 -1
  18. package/hooks/build +3 -0
  19. package/lib/browser.js +81 -33
  20. package/lib/index.js +18 -9
  21. package/lib/loader.js +3 -4
  22. package/lib/metadata/metadata.json +180 -29
  23. package/modules/ajaxRequests/scope.js +1 -1
  24. package/modules/analyzeCss/analyzeCss.js +79 -76
  25. package/modules/analyzeCss/scope.js +1 -1
  26. package/modules/blockDomains/blockDomains.js +21 -21
  27. package/modules/cacheHits/cacheHits.js +6 -3
  28. package/modules/cpuTasks/cpuTasks.js +22 -0
  29. package/modules/documentHeight/scope.js +1 -1
  30. package/modules/domComplexity/scope.js +1 -1
  31. package/modules/domHiddenContent/scope.js +1 -1
  32. package/modules/domMutations/scope.js +1 -1
  33. package/modules/domQueries/domQueries.js +16 -19
  34. package/modules/domQueries/scope.js +1 -1
  35. package/modules/domains/domains.js +1 -1
  36. package/modules/events/scope.js +2 -1
  37. package/modules/globalVariables/scope.js +1 -1
  38. package/modules/jQuery/scope.js +1 -1
  39. package/modules/javaScriptBottlenecks/scope.js +1 -1
  40. package/modules/lazyLoadableImages/scope.js +2 -2
  41. package/modules/localStorage/scope.js +1 -1
  42. package/modules/protocols/protocols.js +101 -0
  43. package/modules/requestsStats/requestsStats.js +1 -1
  44. package/modules/staticAssets/staticAssets.js +2 -1
  45. package/modules/windowPerformance/windowPerformance.js +1 -0
  46. package/package.json +31 -20
  47. package/lib/fast-stats.js +0 -634
  48. package/lib/metadata/generate.js +0 -283
  49. package/lib/metadata/make_docs.js +0 -185
  50. package/reporters/csv.js +0 -54
  51. package/reporters/plain.js +0 -173
  52. package/reporters/statsd.js +0 -82
  53. package/reporters/tap.js +0 -71
  54. package/reporters/teamcity.js +0 -73
@@ -24,28 +24,31 @@ function lowerCaseHeaders(headers) {
24
24
 
25
25
  // parse given URL to get protocol and domain
26
26
  function parseEntryUrl(entry) {
27
- const parseUrl = require("url").parse;
28
27
  var parsed;
29
28
 
30
29
  // asset type
31
30
  entry.type = "other";
32
31
 
33
- if (entry.url.indexOf("data:") !== 0) {
34
- // @see http://nodejs.org/api/url.html#url_url
35
- parsed = parseUrl(entry.url) || {};
32
+ if (entry.url.indexOf("data:") === 0) {
33
+ // base64 encoded data
34
+ entry.domain = false;
35
+ entry.protocol = false;
36
+ entry.isBase64 = true;
37
+ } else if (entry.url.indexOf("blob:") === 0) {
38
+ // blob image or video
39
+ entry.domain = false;
40
+ entry.protocol = false;
41
+ entry.isBlob = true;
42
+ } else {
43
+ parsed = new URL(entry.url) || {};
36
44
 
37
- entry.protocol = parsed.protocol.replace(":", "");
45
+ entry.protocol = parsed.protocol.replace(":", ""); // e.g. "http:"
38
46
  entry.domain = parsed.hostname;
39
- entry.query = parsed.query;
47
+ entry.query = parsed.search.substring(1);
40
48
 
41
49
  if (entry.protocol === "https") {
42
50
  entry.isSSL = true;
43
51
  }
44
- } else {
45
- // base64 encoded data
46
- entry.domain = false;
47
- entry.protocol = false;
48
- entry.isBase64 = true;
49
52
  }
50
53
 
51
54
  return entry;
@@ -103,6 +106,7 @@ function addContentType(headerValue, entry) {
103
106
  break;
104
107
 
105
108
  case "video/webm":
109
+ case "video/mp4":
106
110
  entry.type = "video";
107
111
  entry.isVideo = true;
108
112
  break;
@@ -189,6 +193,11 @@ module.exports = function (phantomas) {
189
193
  const resId = resp._requestId,
190
194
  request = requests[resId];
191
195
 
196
+ if (resp.fromDiskCache === true) {
197
+ phantomas.log("response from disk cache ignored: %j", resp);
198
+ return;
199
+ }
200
+
192
201
  var entry = {
193
202
  id: resId,
194
203
  url: resp.url,
@@ -226,7 +235,7 @@ module.exports = function (phantomas) {
226
235
  *
227
236
  * "Throughout this work, time is measured in milliseconds"
228
237
  */
229
- if (!entry.isBase64) {
238
+ if (!entry.isBase64 && !entry.isBlob) {
230
239
  // resp.timing is empty when handling data:image/gif;base64,R0lGODlhAQABAIABAAAAAP///yH5BAEAAAEALAAAAAABAAEAQAICTAEAOw%3D%3D
231
240
  assert(
232
241
  typeof resp.timing !== "undefined",
@@ -314,8 +323,14 @@ module.exports = function (phantomas) {
314
323
  break;
315
324
  }
316
325
 
326
+ // HTTP and TLS protocols version
327
+ entry.httpVersion = resp.protocol;
328
+ if (resp.securityDetails) {
329
+ entry.tlsVersion = resp.securityDetails.protocol;
330
+ }
331
+
317
332
  // requests stats
318
- if (!entry.isBase64) {
333
+ if (!entry.isBase64 && !entry.isBlob) {
319
334
  phantomas.incrMetric("requests");
320
335
  phantomas.addOffender("requests", {
321
336
  url: entry.url,
@@ -343,6 +358,8 @@ module.exports = function (phantomas) {
343
358
 
344
359
  if (entry.isBase64) {
345
360
  phantomas.emit("base64recv", entry, resp); // @desc base64-encoded "response" has been received
361
+ } else if (entry.isBlob) {
362
+ // Do nothing
346
363
  } else {
347
364
  phantomas.log(
348
365
  "recv: HTTP %d <%s> [%s]",
package/core/scope.js CHANGED
@@ -3,7 +3,8 @@
3
3
  *
4
4
  * Code below is executed in page's "scope" (injected by lib/browser.js)
5
5
  */
6
- (function (scope) {
6
+ /* istanbul ignore next */
7
+ (function coreScope(scope) {
7
8
  "use strict";
8
9
 
9
10
  // create a scope
@@ -54,8 +54,7 @@ module.exports = function (phantomas) {
54
54
 
55
55
  phantomas.on("init", async (page) => {
56
56
  const url = phantomas.getParam("url"),
57
- // https://nodejs.org/docs/latest/api/url.html#url_legacy_url_api
58
- domain = require("url").parse(url).hostname;
57
+ domain = new URL(url).hostname;
59
58
 
60
59
  // domain field in cookies needs to be set
61
60
  // https://github.com/miyakogi/pyppeteer/issues/94#issuecomment-403261859
@@ -28,7 +28,7 @@ module.exports = function (phantomas) {
28
28
  .replace(/\/+$/, ""),
29
29
  filmStripPrefix = phantomas
30
30
  .getParam("film-strip-prefix", "screenshot", "string")
31
- .replace(/[^a-z0-9\-]+/gi, "-");
31
+ .replace(/[^a-z0-9-]+/gi, "-");
32
32
 
33
33
  var zoomFactor = phantomas.getParam("film-strip-zoom-factor", 0.5);
34
34
  phantomas.setZoom(zoomFactor);
@@ -33,6 +33,8 @@ module.exports = (phantomas) => {
33
33
  return new Promise(async (resolve) => {
34
34
  // https://github.com/GoogleChrome/puppeteer/blob/v1.11.0/docs/api.md#pageevaluatepagefunction-args
35
35
  const bodyHandle = await page.$("body");
36
+
37
+ /* istanbul ignore next */
36
38
  const html = await page.evaluate((body) => body.innerHTML, bodyHandle);
37
39
 
38
40
  // phantomas.log(html);
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Support for session-/localStorage injection.
3
+ */
4
+ "use strict";
5
+
6
+ module.exports = function (phantomas) {
7
+ const SESSION_STORAGE = "session-storage",
8
+ LOCAL_STORAGE = "local-storage";
9
+
10
+ phantomas.on("init", async (page) => {
11
+ let sessionStorage = phantomas.getParam(SESSION_STORAGE, false);
12
+ let localStorage = phantomas.getParam(LOCAL_STORAGE, false);
13
+
14
+ // Mapping given "storage-string" to json object if string has been given
15
+ if (typeof sessionStorage == "string" || sessionStorage instanceof String) {
16
+ sessionStorage = parseStorage(sessionStorage);
17
+ }
18
+
19
+ if (typeof localStorage == "string" || localStorage instanceof String) {
20
+ localStorage = parseStorage(localStorage);
21
+ }
22
+
23
+ if (sessionStorage) {
24
+ phantomas.log(
25
+ "Injecting sessionStorage: %j",
26
+ JSON.stringify(sessionStorage)
27
+ );
28
+ await injectStorage(page, sessionStorage, SESSION_STORAGE);
29
+ }
30
+ if (localStorage) {
31
+ phantomas.log("Injecting localStorag: %j", JSON.stringify(localStorage));
32
+ await injectStorage(page, localStorage, LOCAL_STORAGE);
33
+ }
34
+ });
35
+
36
+ function parseStorage(storageString) {
37
+ // --sessionStorage='bar=foo;domain=url'
38
+ // --localStorage='bar=fooLocal;domain=urlLocal'
39
+ var storageMap = {};
40
+ storageString.split(";").forEach(function (singleEntry) {
41
+ var entryKeyValue = singleEntry.split("=");
42
+ storageMap[entryKeyValue[0]] = entryKeyValue[1];
43
+ });
44
+ return storageMap;
45
+ }
46
+
47
+ /**
48
+ * Inject the given storage into the specified page storage.
49
+ * Either localStorage or sessionStorage
50
+ *
51
+ * @param {Page} page in which page the storage should be injected
52
+ * @param {Object} storage the JSON object consisting of the storage keys and values
53
+ * @param {string} storageType either localStorage or sessionStorage
54
+ */
55
+ async function injectStorage(page, storage, storageType) {
56
+ if (!page || !storage || !storageType) {
57
+ return;
58
+ }
59
+
60
+ /* istanbul ignore next */
61
+ await page.evaluateOnNewDocument(
62
+ (storage, storageType, SESSION_STORAGE, LOCAL_STORAGE) => {
63
+ const keys = Object.keys(storage);
64
+ const values = Object.values(storage);
65
+ if (storageType === SESSION_STORAGE) {
66
+ for (let i = 0; i < keys.length; i++) {
67
+ sessionStorage.setItem(keys[i], values[i]);
68
+ }
69
+ }
70
+ if (storageType === LOCAL_STORAGE) {
71
+ for (let i = 0; i < keys.length; i++) {
72
+ localStorage.setItem(keys[i], values[i]);
73
+ }
74
+ }
75
+ },
76
+ storage,
77
+ storageType,
78
+ SESSION_STORAGE,
79
+ LOCAL_STORAGE
80
+ );
81
+ }
82
+ };
@@ -5,16 +5,24 @@
5
5
 
6
6
  module.exports = function (phantomas) {
7
7
  const workingDirectory = require("process").cwd(),
8
- param = phantomas.getParam("screenshot");
8
+ param = phantomas.getParam("screenshot"),
9
+ fullPage = phantomas.getParam("fullPageScreenshot");
9
10
  var path = "";
10
11
 
11
12
  if (typeof param === "undefined") {
12
13
  phantomas.log(
13
- "Screenshot: to enable screenshot of the fully loaded page run phantomas with --screenshot option"
14
+ "Screenshot: to enable screenshot of the page run phantomas with --screenshot option"
14
15
  );
15
16
  return;
16
17
  }
17
18
 
19
+ if (fullPage === true) {
20
+ // the full page option is now disabled by default (bug #853)
21
+ phantomas.log(
22
+ "Screenshot: --full-page-screenshot option enabled. Please note that the option can cause layout bugs and lazyloadableImagesUnderTheFold miscount"
23
+ );
24
+ }
25
+
18
26
  // --screenshot
19
27
  if (param === true) {
20
28
  // defaults to "2013-12-07T20:15:01.521Z.png"
@@ -27,7 +35,11 @@ module.exports = function (phantomas) {
27
35
  path = param;
28
36
  }
29
37
 
30
- path = workingDirectory + "/" + path;
38
+ // deal with relative paths
39
+ if (path.indexOf("/") !== 0) {
40
+ path = workingDirectory + "/" + path;
41
+ }
42
+
31
43
  phantomas.log("Screenshot will be saved in %s", path);
32
44
 
33
45
  phantomas.on("beforeClose", (page) => {
@@ -36,17 +48,23 @@ module.exports = function (phantomas) {
36
48
  const options = {
37
49
  path: path,
38
50
  type: "png",
39
- fullPage: true, // takes a screenshot of the full scrollable page
51
+ fullPage: fullPage === true,
40
52
  };
41
53
  phantomas.log("Will take screenshot, options: %j", options);
42
54
 
43
- // https://github.com/GoogleChrome/puppeteer/blob/v1.11.0/docs/api.md#pagescreenshotoptions
44
- await page.screenshot(options);
55
+ try {
56
+ // https://github.com/GoogleChrome/puppeteer/blob/v1.11.0/docs/api.md#pagescreenshotoptions
57
+ await page.screenshot(options);
58
+
59
+ phantomas.log("Screenshot stored in %s", path);
45
60
 
46
- phantomas.log("Screenshot stored in %s", path);
61
+ // let clients know that we stored the page source in a file
62
+ phantomas.emit("screenshot", path);
63
+ } catch (err) {
64
+ phantomas.log("Error while taking the screenshot");
65
+ phantomas.log(err);
66
+ }
47
67
 
48
- // let clients know that we stored the page source in a file
49
- phantomas.emit("screenshot", path);
50
68
  resolve();
51
69
  });
52
70
  });
@@ -3,7 +3,6 @@
3
3
  *
4
4
  * Pass --scroll as an option in CLI mode
5
5
  */
6
- /* global document: true, window: true */
7
6
  "use strict";
8
7
 
9
8
  module.exports = function (phantomas) {
@@ -22,7 +21,10 @@ module.exports = function (phantomas) {
22
21
  return new Promise(async (resolve) => {
23
22
  phantomas.log("Scrolling the page...");
24
23
 
24
+ /* istanbul ignore next */
25
25
  await page.evaluate(() => document.body.scrollIntoView(false));
26
+
27
+ /* istanbul ignore next */
26
28
  const scrollOffset = await page.evaluate(() => document.body.scrollTop);
27
29
 
28
30
  // wait for lazy loading to do its job
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Sets a user agent according to --user-agent or --phone or --tablet options
3
+ */
4
+ "use strict";
5
+
6
+ module.exports = function (phantomas) {
7
+ // the user-agent template we use for all emulated devices
8
+ let userAgent =
9
+ "Mozilla/5.0 (<Platform>) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/<BrowserVersion> Safari/537.36 Phantomas/<PhantomasVersion>";
10
+
11
+ // default platform for desktop
12
+ let platform = "Windows NT 10.0; Win64; x64";
13
+
14
+ // --phone option overwrites the default platform
15
+ if (
16
+ phantomas.getParam("phone") === true ||
17
+ phantomas.getParam("phone-landscape") === true
18
+ ) {
19
+ platform = "Linux; Android 10; SM-G981B";
20
+ }
21
+
22
+ // --tablet option overwrites the default platform
23
+ if (
24
+ phantomas.getParam("tablet") === true ||
25
+ phantomas.getParam("tablet-landscape") === true
26
+ ) {
27
+ platform = "Linux; Android 10; SM-T870";
28
+ }
29
+
30
+ // if --user-agent option is set, it overwrites --phone and --tablet
31
+ // it can contain <Platform>, <BrowserVersion> and <PhantomasVersion> if needed
32
+ const param = phantomas.getParam("user-agent");
33
+ if (typeof param !== "undefined") {
34
+ phantomas.log(
35
+ "userAgent: --user-agent option detected with value %s",
36
+ param
37
+ );
38
+ userAgent = param;
39
+ }
40
+
41
+ phantomas.on("init", async (page, browser) => {
42
+ const browserVersion = await browser.version();
43
+ // browserVersion will look like HeadlessChrome/88.0.4298.0
44
+ // let's keep the number only:
45
+ const versionNumber = browserVersion.split("/")[1];
46
+
47
+ userAgent = userAgent.replace("<Platform>", platform);
48
+ userAgent = userAgent.replace("<BrowserVersion>", versionNumber);
49
+ userAgent = userAgent.replace("<PhantomasVersion>", phantomas.getVersion());
50
+
51
+ // @see // https://github.com/GoogleChrome/puppeteer/blob/v1.11.0/docs/api.md#pagesetuseragentuseragent
52
+ await page.setUserAgent(userAgent);
53
+ phantomas.log("userAgent set to %s", userAgent);
54
+ });
55
+ };
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Provides the --viewport option to set any device resolution and pixel density ratio.
3
3
  * If the user sets a viewport size as well as a device option (--phone, --tablet, ...),
4
- * we assume that he/she wants to overwrite the device values.
4
+ * we assume that he or she wants to overwrite the device values.
5
5
  *
6
6
  * Two syntaxes are supported:
7
7
  * - 1200x800 will set a 1x pixel density ratio
@@ -1,7 +1,6 @@
1
1
  /**
2
2
  * Delays report generation until given CSS selector can be resolved (i.e. given element exists)
3
3
  */
4
- /* global document: true */
5
4
  "use strict";
6
5
 
7
6
  module.exports = function (phantomas) {
package/hooks/build ADDED
@@ -0,0 +1,3 @@
1
+ #!/bin/bash
2
+ # see https://docs.docker.com/docker-hub/builds/advanced/#build-hook-examples
3
+ docker build --build-arg GITHUB_SHA=$SOURCE_COMMIT -f $DOCKERFILE_PATH -t $IMAGE_NAME .
package/lib/browser.js CHANGED
@@ -2,8 +2,7 @@
2
2
  * Expose puppeteer API and events emitter object for lib/index.js
3
3
  */
4
4
  const debug = require("debug")("phantomas:browser"),
5
- puppeteer = require("puppeteer"),
6
- VERSION = require("../package.json").version;
5
+ puppeteer = require("puppeteer");
7
6
 
8
7
  function Browser() {
9
8
  this.browser = null;
@@ -17,7 +16,7 @@ function Browser() {
17
16
  Browser.prototype.bind = (events) => (this.events = events);
18
17
 
19
18
  // initialize puppeter instance
20
- Browser.prototype.init = async () => {
19
+ Browser.prototype.init = async (phantomasOptions) => {
21
20
  const networkDebug = require("debug")("phantomas:network"),
22
21
  env = require("process").env;
23
22
 
@@ -26,10 +25,23 @@ Browser.prototype.init = async () => {
26
25
  // page.evaluate throw "Protocol error (Runtime.callFunctionOn): Target closed." without the following
27
26
  // https://github.com/GoogleChrome/puppeteer/issues/1175#issuecomment-369728215
28
27
  "--disable-dev-shm-usage",
28
+
29
+ // enable http/3 support
30
+ // https://www.bram.us/2020/04/08/how-to-enable-http3-in-chrome-firefox-safari/
31
+ "--enable-quic",
32
+ "--quic-version=h3-29",
29
33
  ],
30
34
  };
31
35
 
36
+ // handle Phantomas options
37
+ //
38
+ // --ignore-ssl-errors ignores SSL errors, such as expired or self-signed certificate errors
39
+ if (phantomasOptions["ignore-ssl-errors"]) {
40
+ options["ignoreHTTPSErrors"] = true;
41
+ }
42
+
32
43
  // customize path to Chromium binary
44
+ /* istanbul ignore next */
33
45
  if (env["PHANTOMAS_CHROMIUM_EXECUTABLE"]) {
34
46
  options.executablePath = env["PHANTOMAS_CHROMIUM_EXECUTABLE"];
35
47
  }
@@ -37,6 +49,7 @@ Browser.prototype.init = async () => {
37
49
  // detect that we run inside a container
38
50
  // @see https://github.com/jessfraz/dockerfiles/issues/65
39
51
  // @see https://github.com/Zenika/alpine-chrome#-the-best-with-seccomp
52
+ /* istanbul ignore next */
40
53
  if (env["DOCKERIZED"]) {
41
54
  debug("Docker environment detected");
42
55
  debug(
@@ -44,13 +57,27 @@ Browser.prototype.init = async () => {
44
57
  );
45
58
  }
46
59
 
60
+ // detect that we run inside an Amazon Lambda machine
61
+ // (note: the LAMBDA_TASK_ROOT env variable is automatically set by AWS)
62
+ // @see https://docs.aws.amazon.com/lambda/latest/dg/configuration-envvars.html#configuration-envvars-runtime
63
+ /* istanbul ignore next */
64
+ if (env["LAMBDA_TASK_ROOT"]) {
65
+ // Chrome then requires some more flags
66
+ options.args.push(
67
+ "--no-sandbox",
68
+ "--no-zygote",
69
+ "--disable-gpu",
70
+ "--single-process"
71
+ );
72
+ }
73
+
47
74
  debug("Launching Puppeteer: %j", options);
48
75
 
49
76
  try {
50
- // https://github.com/GoogleChrome/puppeteer/blob/v1.11.0/docs/api.md#puppeteerlaunchoptions
77
+ // https://github.com/puppeteer/puppeteer/blob/main/docs/api.md#puppeteerlaunchoptions
51
78
  this.browser = await puppeteer.launch(options);
52
79
  this.page = await this.browser.newPage();
53
- } catch (ex) {
80
+ } catch (ex) /* istanbul ignore next */ {
54
81
  debug("Puppeteer failed to launch: %s", ex);
55
82
  throw ex;
56
83
  }
@@ -60,13 +87,6 @@ Browser.prototype.init = async () => {
60
87
 
61
88
  debug("Using binary from: %s", this.browser.process().spawnfile);
62
89
 
63
- // set a custom user agent, e.g. "phantomas/2.0.0-beta (HeadlessChrome/72.0.3617.0)"
64
- // https://github.com/GoogleChrome/puppeteer/blob/v1.11.0/docs/api.md#pagesetuseragentuseragent
65
- const browserVersion = await this.browser.version();
66
- await this.page.setUserAgent(
67
- "phantomas/" + VERSION + " (" + browserVersion + ")"
68
- );
69
-
70
90
  // https://github.com/GoogleChrome/puppeteer/blob/v1.11.0/docs/api.md#browserversion
71
91
  debug("Original browser: %s", await this.browser.userAgent());
72
92
  debug("Viewport: %j", await this.page.viewport());
@@ -153,8 +173,19 @@ Browser.prototype.init = async () => {
153
173
  });
154
174
 
155
175
  this.onRequestLoaded = (eventName, data) => {
156
- var meta = responses[data.requestId],
157
- response = meta.response;
176
+ var meta = responses[data.requestId];
177
+
178
+ /* istanbul ignore if */
179
+ if (typeof meta === "undefined") {
180
+ // the browser sometimes looses trace of a request, let's ignore.
181
+ networkDebug(
182
+ "Can't find request id %d in previous requests",
183
+ data.requestId
184
+ );
185
+ return;
186
+ }
187
+
188
+ var response = meta.response;
158
189
 
159
190
  // errorText: 'net::ERR_FAILED' - request is blocked (meta.response will be empty)
160
191
  // errorText: 'net::ERR_ABORTED' - HTTP 404
@@ -181,17 +212,29 @@ Browser.prototype.init = async () => {
181
212
  // https://github.com/GoogleChrome/puppeteer/blob/v1.11.0/docs/api.md#cdpsessionsendmethod-params
182
213
  response.getContent = (async () => {
183
214
  networkDebug("Getting content for #%s", data.requestId);
184
- const resp = await this.cdp.send("Network.getResponseBody", {
185
- requestId: data.requestId,
186
- });
187
- networkDebug(
188
- "Content for #%s received (%d bytes)",
189
- data.requestId,
190
- resp.body.length
191
- );
215
+ let body = null;
216
+
217
+ try {
218
+ const resp = await this.cdp.send("Network.getResponseBody", {
219
+ requestId: data.requestId,
220
+ });
221
+ networkDebug(
222
+ "Content for #%s received (%d bytes)",
223
+ data.requestId,
224
+ resp.body.length
225
+ );
226
+
227
+ body = resp.body;
228
+ } catch (err) {
229
+ // In case the resource was dumped after a redirect
230
+ // https://github.com/puppeteer/puppeteer/issues/2258
231
+ networkDebug(
232
+ "Could not read the content of #%s. It was probably removed from the browser's buffer by a redirect.",
233
+ data.requestId
234
+ );
235
+ }
192
236
 
193
- //console.log('getContent()', resp);
194
- return resp.body;
237
+ return body;
195
238
  }).bind(this);
196
239
 
197
240
  networkDebug(
@@ -229,9 +272,10 @@ Browser.prototype.init = async () => {
229
272
  // Fired when data chunk was received over the network
230
273
  this.cdp.on("Network.dataReceived", (data) => {
231
274
  networkDebug("Network.dataReceived: %j", data);
232
-
233
- responses[data.requestId]._chunks++;
234
- responses[data.requestId]._dataLength += data.dataLength;
275
+ if (responses[data.requestId]) {
276
+ responses[data.requestId]._chunks++;
277
+ responses[data.requestId]._dataLength += data.dataLength;
278
+ }
235
279
  });
236
280
 
237
281
  return this.page;
@@ -255,11 +299,12 @@ Browser.prototype.visit = (url, waitUntil, timeout) => {
255
299
  waitUntil: waitUntil,
256
300
  timeout: (timeout || 30) * 1000, // defaults to 30 seconds, provide in miliseconds!
257
301
  });
302
+
303
+ debug("URL opened: <%s>", url);
258
304
  } catch (ex) {
259
305
  debug("Opening URL failed: " + ex);
260
306
  return reject(ex);
261
307
  }
262
- debug("URL opened: <%s>", url);
263
308
 
264
309
  // https://github.com/GoogleChrome/puppeteer/issues/1325#issuecomment-382003386
265
310
  // bind to this event when getting "Protocol error (Runtime.callFunctionOn): Target closed."
@@ -271,7 +316,7 @@ Browser.prototype.visit = (url, waitUntil, timeout) => {
271
316
  debug("Metrics: %s", JSON.stringify(metrics));
272
317
 
273
318
  this.events.emit("metrics", metrics); // @desc Emitted when Chromuim's page.metrics() has been called
274
- } catch (ex) {
319
+ } catch (ex) /* istanbul ignore next */ {
275
320
  debug("Get metrics failed: " + ex);
276
321
  return reject(ex);
277
322
  }
@@ -284,17 +329,20 @@ Browser.prototype.close = async () => {
284
329
  try {
285
330
  // Allow the beforeunload event to be executed
286
331
  // https://github.com/puppeteer/puppeteer/blob/v1.11.0/docs/api.md#pagecloseoptions
287
- if (this.page) await this.page.close({ runBeforeUnload: true });
332
+ if (this.page && !this.page.isClosed())
333
+ await this.page.close({ runBeforeUnload: true });
288
334
 
289
335
  // The page is closed, let's close the browser
290
- if (this.browser) await this.browser.close();
291
- } catch (ex) {
336
+ if (this.browser && this.browser.isConnected()) await this.browser.close();
337
+ } catch (ex) /* istanbul ignore next */ {
292
338
  debug("An exception was raised in Browser.prototype.close(): " + ex);
293
- throw ex;
339
+ debug(ex);
294
340
  }
295
341
 
296
342
  this.events.emit("close"); // @desc Chromium has been closed
297
343
  debug("Browser closed");
298
344
  };
299
345
 
346
+ Browser.prototype.getPuppeteerBrowser = () => this.browser;
347
+
300
348
  module.exports = Browser;