@percy/core 1.0.0-beta.76 → 1.0.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/README.md +86 -18
- package/package.json +24 -14
- package/dist/api.js +0 -76
- package/dist/browser.js +0 -363
- package/dist/config.js +0 -358
- package/dist/discovery.js +0 -130
- package/dist/index.js +0 -15
- package/dist/install.js +0 -173
- package/dist/network.js +0 -365
- package/dist/page.js +0 -293
- package/dist/percy.js +0 -427
- package/dist/queue.js +0 -196
- package/dist/server.js +0 -471
- package/dist/session.js +0 -140
- package/dist/snapshot.js +0 -279
- package/dist/utils.js +0 -170
- package/post-install.js +0 -23
- package/test/helpers/server.js +0 -35
- package/types/index.d.ts +0 -101
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
The core component of Percy's CLI and SDKs that handles creating builds, discovering snapshot
|
|
4
4
|
assets, uploading snapshots, and finalizing builds. Uses `@percy/client` for API communication, a
|
|
5
|
-
|
|
5
|
+
Chromium browser for asset discovery, and starts a local API server for posting snapshots from
|
|
6
6
|
other processes.
|
|
7
7
|
|
|
8
8
|
- [Usage](#usage)
|
|
@@ -49,6 +49,7 @@ The following options can also be defined within a Percy config file
|
|
|
49
49
|
- `enableJavaScript` — Enable JavaScript for screenshots (**default** `false`)
|
|
50
50
|
- `discovery` — Asset discovery options
|
|
51
51
|
- `allowedHostnames` — Array of allowed hostnames to capture assets from
|
|
52
|
+
- `disallowedHostnames` — Array of hostnames where requests will be aborted
|
|
52
53
|
- `requestHeaders` — Request headers used when discovering snapshot assets
|
|
53
54
|
- `authorization` — Basic auth `username` and `password` for protected snapshot assets
|
|
54
55
|
- `disableCache` — Disable asset caching (**default** `false`)
|
|
@@ -127,10 +128,13 @@ await percy.idle()
|
|
|
127
128
|
|
|
128
129
|
### `#snapshot(options)`
|
|
129
130
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
131
|
+
Takes one or more snapshots of a page while discovering resources to upload with the snapshot. Once
|
|
132
|
+
asset discovery has completed, the queued snapshot will resolve and an upload task will be queued
|
|
133
|
+
separately. Accepts several different syntaxes for taking snapshots in various ways.
|
|
134
|
+
|
|
135
|
+
All available syntaxes will push snapshots into the snapshot queue without the need to await on the
|
|
136
|
+
method directly. This method resolves after the snapshot upload is queued, but does not await on the
|
|
137
|
+
upload to complete.
|
|
134
138
|
|
|
135
139
|
``` js
|
|
136
140
|
// snapshots can be handled concurrently, no need to await
|
|
@@ -140,37 +144,61 @@ percy.snapshot({
|
|
|
140
144
|
domSnapshot: domSnapshot,
|
|
141
145
|
clientInfo: 'my-sdk',
|
|
142
146
|
environmentInfo: 'my-lib'
|
|
143
|
-
...snapshotOptions
|
|
144
147
|
})
|
|
145
148
|
|
|
149
|
+
// without a domSnapshot, capture options will be used to take one
|
|
146
150
|
percy.snapshot({
|
|
147
151
|
name: 'Snapshot 2',
|
|
148
|
-
url: 'http://localhost:3000
|
|
149
|
-
...snapshotOptions,
|
|
150
|
-
|
|
151
|
-
// without a domSnapshot, capture options will be used to take one
|
|
152
|
+
url: 'http://localhost:3000',
|
|
152
153
|
waitForTimeout: 1000,
|
|
153
154
|
waitForSelector: '.done-loading',
|
|
154
155
|
execute: async () => {},
|
|
155
156
|
additionalSnapshots: [{
|
|
156
157
|
name: 'Snapshot 2.1',
|
|
157
|
-
execute: () => {}
|
|
158
|
-
...snapshotOptions
|
|
158
|
+
execute: () => {}
|
|
159
159
|
}]
|
|
160
160
|
})
|
|
161
|
+
|
|
162
|
+
// alternate shorthand syntax
|
|
163
|
+
percy.snapshot({
|
|
164
|
+
baseUrl: 'http://localhost:3000',
|
|
165
|
+
snapshots: ['/', '/about', '/contact'],
|
|
166
|
+
options: {
|
|
167
|
+
widths: [600, 1200]
|
|
168
|
+
}
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
// gather snapshots from an external sitemap
|
|
172
|
+
percy.snapshot({
|
|
173
|
+
sitemap: 'https://example.com/sitemap.xml',
|
|
174
|
+
exclude: ['/blog/*']
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
// start a server and take static snapshots
|
|
178
|
+
percy.snapshot({
|
|
179
|
+
serve: './public',
|
|
180
|
+
cleanUrls: true,
|
|
181
|
+
})
|
|
161
182
|
```
|
|
162
183
|
|
|
163
184
|
#### Options
|
|
164
185
|
|
|
186
|
+
When capturing a single snapshot, the snapshot URL may be provided as the only argument rather than
|
|
187
|
+
a snapshot options object. When providing an options object, a few alternate syntaxes are available
|
|
188
|
+
depending on the provided properties ([see alternate syntaxes below](#alternate-syntaxes)).
|
|
189
|
+
|
|
190
|
+
**Common options** accepted for each snapshot:
|
|
191
|
+
|
|
165
192
|
- `url` — Snapshot URL (**required**)
|
|
166
193
|
- `name` — Snapshot name
|
|
167
194
|
- `domSnapshot` — Snapshot DOM string
|
|
168
|
-
- `clientInfo` — Additional client info
|
|
169
|
-
- `environmentInfo` — Additional environment info
|
|
170
195
|
- `discovery` - Limited snapshot specific discovery options
|
|
171
|
-
- `allowedHostnames`, `requestHeaders`, `authorization`, `disableCache`, `userAgent`
|
|
196
|
+
- `allowedHostnames`, `disallowedHostnames`, `requestHeaders`, `authorization`, `disableCache`, `userAgent`
|
|
172
197
|
|
|
173
|
-
|
|
198
|
+
Common snapshot options are also accepted and will override instance snapshot options. [See instance
|
|
199
|
+
options](#options) for common snapshot and discovery options.
|
|
200
|
+
|
|
201
|
+
**Capture options** can only be provided when `domSnapshot` is missing:
|
|
174
202
|
|
|
175
203
|
- `waitForTimeout` — Milliseconds to wait before taking a snapshot
|
|
176
204
|
- `waitForSelector` — CSS selector to wait for before taking a snapshot
|
|
@@ -180,9 +208,49 @@ percy.snapshot({
|
|
|
180
208
|
- `prefix` — Snapshot name prefix (**required** if no `name` or `suffix`)
|
|
181
209
|
- `suffix` — Snapshot name suffix (**required** if no `name` or `prefix`)
|
|
182
210
|
- `waitForTimeout`, `waitForSelector`, `execute` — See above
|
|
211
|
+
|
|
212
|
+
#### Alternate syntaxes
|
|
183
213
|
|
|
184
|
-
|
|
185
|
-
|
|
214
|
+
All snapshot syntaxes can be provided as items within an array. For example, a single method call
|
|
215
|
+
can upload multiple DOM snapshots, capture multiple external snapshots, crawl a sitemap for
|
|
216
|
+
snapshots, and host a local static server for snapshots.
|
|
217
|
+
|
|
218
|
+
**Shared options** accepted by all syntaxes:
|
|
219
|
+
|
|
220
|
+
- `clientInfo` — Client info to include with the build
|
|
221
|
+
- `environmentInfo` — Environment info to include with the build
|
|
222
|
+
|
|
223
|
+
The following alternate syntaxes may **not** be combined with snapshot options, but rather offer
|
|
224
|
+
alternate methods for taking multiple snapshots.
|
|
225
|
+
|
|
226
|
+
**List options** can only be provided when a top-level `snapshots` is present:
|
|
227
|
+
|
|
228
|
+
- `snapshots` — An array of snapshot URLs or snapshot options (**required**)
|
|
229
|
+
- `baseUrl` — The full base URL (including protocol) used when snapshot URLs only include a pathname
|
|
230
|
+
- `include`/`exclude` — Include and exclude matching snapshot names
|
|
231
|
+
- `options` — Additional options to apply to snapshots
|
|
232
|
+
- `include`/`exclude` — Include and exclude snapshots to apply these options to
|
|
233
|
+
- [Common snapshot and capture options](#options) (**excluding** `url`, `domSnapshot`)
|
|
234
|
+
|
|
235
|
+
**Sitemap options** can only be provided when a top-level `sitemap` is present:
|
|
236
|
+
|
|
237
|
+
- `sitemap` — The URL where an XML sitemap can be located (**required**)
|
|
238
|
+
- `include`/`exclude` — Include and exclude matching snapshot names
|
|
239
|
+
- `options` — Additional options to apply to snapshots
|
|
240
|
+
- `include`/`exclude` — Include and exclude snapshots to apply these options to
|
|
241
|
+
- [Common snapshot and capture options](#options) (**excluding** `url`, `domSnapshot`)
|
|
242
|
+
|
|
243
|
+
**Server options** can only be provided when a top-level `serve` is present:
|
|
244
|
+
|
|
245
|
+
- `serve` — The static directory to serve relative to the current working directory (**required**)
|
|
246
|
+
- `baseUrl` — The base URL to serve the directory at, starting with a forward slash (/)
|
|
247
|
+
- `cleanUrls` — Set to `true` to strip `.html` and `index.html` from served URLs
|
|
248
|
+
- `rewrites` — A source-destination map for rewriting source URLs into destination pathnames
|
|
249
|
+
- `snapshots` — An array of specific snapshots to take while serving the static directory
|
|
250
|
+
- `include`/`exclude` — Include and exclude matching snapshot names
|
|
251
|
+
- `options` — Additional options to apply to snapshots
|
|
252
|
+
- `include`/`exclude` — Include and exclude snapshots to apply these options to
|
|
253
|
+
- [Common snapshot and capture options](#options) (**excluding** `url`, `domSnapshot`)
|
|
186
254
|
|
|
187
255
|
## Advanced
|
|
188
256
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@percy/core",
|
|
3
|
-
"version": "1.0.0
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -10,16 +10,24 @@
|
|
|
10
10
|
"publishConfig": {
|
|
11
11
|
"access": "public"
|
|
12
12
|
},
|
|
13
|
-
"
|
|
14
|
-
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=14"
|
|
15
|
+
},
|
|
15
16
|
"files": [
|
|
16
|
-
"dist",
|
|
17
|
-
"post-install.js",
|
|
18
|
-
"types/index.d.ts",
|
|
19
|
-
"test/helpers/server.js"
|
|
17
|
+
"./dist",
|
|
18
|
+
"./post-install.js",
|
|
19
|
+
"./types/index.d.ts",
|
|
20
|
+
"./test/helpers/server.js"
|
|
20
21
|
],
|
|
21
|
-
"
|
|
22
|
-
|
|
22
|
+
"main": "./dist/index.js",
|
|
23
|
+
"types": "types/index.d.ts",
|
|
24
|
+
"type": "module",
|
|
25
|
+
"exports": {
|
|
26
|
+
".": "./dist/index.js",
|
|
27
|
+
"./utils": "./dist/utils.js",
|
|
28
|
+
"./config": "./dist/config.js",
|
|
29
|
+
"./install": "./dist/install.js",
|
|
30
|
+
"./test/helpers": "./test/helpers/index.js"
|
|
23
31
|
},
|
|
24
32
|
"scripts": {
|
|
25
33
|
"build": "node ../../scripts/build",
|
|
@@ -30,17 +38,19 @@
|
|
|
30
38
|
"test:types": "tsd"
|
|
31
39
|
},
|
|
32
40
|
"dependencies": {
|
|
33
|
-
"@percy/client": "1.0.0
|
|
34
|
-
"@percy/config": "1.0.0
|
|
35
|
-
"@percy/dom": "1.0.0
|
|
36
|
-
"@percy/logger": "1.0.0
|
|
41
|
+
"@percy/client": "1.0.0",
|
|
42
|
+
"@percy/config": "1.0.0",
|
|
43
|
+
"@percy/dom": "1.0.0",
|
|
44
|
+
"@percy/logger": "1.0.0",
|
|
37
45
|
"content-disposition": "^0.5.4",
|
|
38
46
|
"cross-spawn": "^7.0.3",
|
|
39
47
|
"extract-zip": "^2.0.1",
|
|
48
|
+
"fast-glob": "^3.2.11",
|
|
49
|
+
"micromatch": "^4.0.4",
|
|
40
50
|
"mime-types": "^2.1.34",
|
|
41
51
|
"path-to-regexp": "^6.2.0",
|
|
42
52
|
"rimraf": "^3.0.2",
|
|
43
53
|
"ws": "^8.0.0"
|
|
44
54
|
},
|
|
45
|
-
"gitHead": "
|
|
55
|
+
"gitHead": "6df509421a60144e4f9f5d59dc57a5675372a0b2"
|
|
46
56
|
}
|
package/dist/api.js
DELETED
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
Object.defineProperty(exports, "__esModule", {
|
|
4
|
-
value: true
|
|
5
|
-
});
|
|
6
|
-
exports.createPercyServer = createPercyServer;
|
|
7
|
-
|
|
8
|
-
var _fs = _interopRequireDefault(require("fs"));
|
|
9
|
-
|
|
10
|
-
var _logger = _interopRequireDefault(require("@percy/logger"));
|
|
11
|
-
|
|
12
|
-
var _server = _interopRequireDefault(require("./server"));
|
|
13
|
-
|
|
14
|
-
var _package = _interopRequireDefault(require("../package.json"));
|
|
15
|
-
|
|
16
|
-
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
|
17
|
-
|
|
18
|
-
function createPercyServer(percy, port) {
|
|
19
|
-
return new _server.default({
|
|
20
|
-
port
|
|
21
|
-
}) // facilitate logger websocket connections
|
|
22
|
-
.websocket(ws => _logger.default.connect(ws)) // general middleware
|
|
23
|
-
.route((req, res, next) => {
|
|
24
|
-
// treat all request bodies as json
|
|
25
|
-
if (req.body) try {
|
|
26
|
-
req.body = JSON.parse(req.body);
|
|
27
|
-
} catch {} // add version header
|
|
28
|
-
|
|
29
|
-
res.setHeader('Access-Control-Expose-Headers', '*, X-Percy-Core-Version');
|
|
30
|
-
res.setHeader('X-Percy-Core-Version', _package.default.version); // return json errors
|
|
31
|
-
|
|
32
|
-
return next().catch(e => {
|
|
33
|
-
var _e$status;
|
|
34
|
-
|
|
35
|
-
return res.json((_e$status = e.status) !== null && _e$status !== void 0 ? _e$status : 500, {
|
|
36
|
-
error: e.message,
|
|
37
|
-
success: false
|
|
38
|
-
});
|
|
39
|
-
});
|
|
40
|
-
}) // healthcheck returns basic information
|
|
41
|
-
.route('get', '/percy/healthcheck', (req, res) => res.json(200, {
|
|
42
|
-
loglevel: percy.loglevel(),
|
|
43
|
-
config: percy.config,
|
|
44
|
-
build: percy.build,
|
|
45
|
-
success: true
|
|
46
|
-
})) // get or set config options
|
|
47
|
-
.route(['get', 'post'], '/percy/config', async (req, res) => res.json(200, {
|
|
48
|
-
config: req.body ? await percy.setConfig(req.body) : percy.config,
|
|
49
|
-
success: true
|
|
50
|
-
})) // responds once idle (may take a long time)
|
|
51
|
-
.route('get', '/percy/idle', async (req, res) => res.json(200, {
|
|
52
|
-
success: await percy.idle().then(() => true)
|
|
53
|
-
})) // convenient @percy/dom bundle
|
|
54
|
-
.route('get', '/percy/dom.js', (req, res) => {
|
|
55
|
-
return res.file(200, require.resolve('@percy/dom'));
|
|
56
|
-
}) // legacy agent wrapper for @percy/dom
|
|
57
|
-
.route('get', '/percy-agent.js', async (req, res) => {
|
|
58
|
-
(0, _logger.default)('core:server').deprecated(['It looks like you’re using @percy/cli with an older SDK.', 'Please upgrade to the latest version to fix this warning.', 'See these docs for more info: https:docs.percy.io/docs/migrating-to-percy-cli'].join(' '));
|
|
59
|
-
let content = await _fs.default.promises.readFile(require.resolve('@percy/dom'), 'utf-8');
|
|
60
|
-
let wrapper = '(window.PercyAgent = class { snapshot(n, o) { return PercyDOM.serialize(o); } });';
|
|
61
|
-
return res.send(200, 'applicaton/javascript', content.concat(wrapper));
|
|
62
|
-
}) // post one or more snapshots
|
|
63
|
-
.route('post', '/percy/snapshot', async (req, res) => {
|
|
64
|
-
let snapshot = percy.snapshot(req.body);
|
|
65
|
-
if (!req.url.searchParams.has('async')) await snapshot;
|
|
66
|
-
return res.json(200, {
|
|
67
|
-
success: true
|
|
68
|
-
});
|
|
69
|
-
}) // stops percy at the end of the current event loop
|
|
70
|
-
.route('/percy/stop', (req, res) => {
|
|
71
|
-
setImmediate(() => percy.stop());
|
|
72
|
-
return res.json(200, {
|
|
73
|
-
success: true
|
|
74
|
-
});
|
|
75
|
-
});
|
|
76
|
-
}
|
package/dist/browser.js
DELETED
|
@@ -1,363 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
Object.defineProperty(exports, "__esModule", {
|
|
4
|
-
value: true
|
|
5
|
-
});
|
|
6
|
-
exports.default = exports.Browser = void 0;
|
|
7
|
-
|
|
8
|
-
var _os = _interopRequireDefault(require("os"));
|
|
9
|
-
|
|
10
|
-
var _path = _interopRequireDefault(require("path"));
|
|
11
|
-
|
|
12
|
-
var _fs = require("fs");
|
|
13
|
-
|
|
14
|
-
var _crossSpawn = _interopRequireDefault(require("cross-spawn"));
|
|
15
|
-
|
|
16
|
-
var _events = _interopRequireDefault(require("events"));
|
|
17
|
-
|
|
18
|
-
var _ws = _interopRequireDefault(require("ws"));
|
|
19
|
-
|
|
20
|
-
var _rimraf = _interopRequireDefault(require("rimraf"));
|
|
21
|
-
|
|
22
|
-
var _logger = _interopRequireDefault(require("@percy/logger"));
|
|
23
|
-
|
|
24
|
-
var _install = _interopRequireDefault(require("./install"));
|
|
25
|
-
|
|
26
|
-
var _session = _interopRequireDefault(require("./session"));
|
|
27
|
-
|
|
28
|
-
var _page = _interopRequireDefault(require("./page"));
|
|
29
|
-
|
|
30
|
-
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
|
31
|
-
|
|
32
|
-
function _classPrivateFieldInitSpec(obj, privateMap, value) { _checkPrivateRedeclaration(obj, privateMap); privateMap.set(obj, value); }
|
|
33
|
-
|
|
34
|
-
function _checkPrivateRedeclaration(obj, privateCollection) { if (privateCollection.has(obj)) { throw new TypeError("Cannot initialize the same private elements twice on an object"); } }
|
|
35
|
-
|
|
36
|
-
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
|
|
37
|
-
|
|
38
|
-
function _classPrivateFieldSet(receiver, privateMap, value) { var descriptor = _classExtractFieldDescriptor(receiver, privateMap, "set"); _classApplyDescriptorSet(receiver, descriptor, value); return value; }
|
|
39
|
-
|
|
40
|
-
function _classApplyDescriptorSet(receiver, descriptor, value) { if (descriptor.set) { descriptor.set.call(receiver, value); } else { if (!descriptor.writable) { throw new TypeError("attempted to set read only private field"); } descriptor.value = value; } }
|
|
41
|
-
|
|
42
|
-
function _classPrivateFieldGet(receiver, privateMap) { var descriptor = _classExtractFieldDescriptor(receiver, privateMap, "get"); return _classApplyDescriptorGet(receiver, descriptor); }
|
|
43
|
-
|
|
44
|
-
function _classExtractFieldDescriptor(receiver, privateMap, action) { if (!privateMap.has(receiver)) { throw new TypeError("attempted to " + action + " private field on non-instance"); } return privateMap.get(receiver); }
|
|
45
|
-
|
|
46
|
-
function _classApplyDescriptorGet(receiver, descriptor) { if (descriptor.get) { return descriptor.get.call(receiver); } return descriptor.value; }
|
|
47
|
-
|
|
48
|
-
var _callbacks = /*#__PURE__*/new WeakMap();
|
|
49
|
-
|
|
50
|
-
var _lastid = /*#__PURE__*/new WeakMap();
|
|
51
|
-
|
|
52
|
-
class Browser extends _events.default {
|
|
53
|
-
constructor({
|
|
54
|
-
executable = process.env.PERCY_BROWSER_EXECUTABLE,
|
|
55
|
-
headless = true,
|
|
56
|
-
cookies = [],
|
|
57
|
-
args = [],
|
|
58
|
-
timeout
|
|
59
|
-
}) {
|
|
60
|
-
super();
|
|
61
|
-
|
|
62
|
-
_defineProperty(this, "log", (0, _logger.default)('core:browser'));
|
|
63
|
-
|
|
64
|
-
_defineProperty(this, "sessions", new Map());
|
|
65
|
-
|
|
66
|
-
_defineProperty(this, "readyState", null);
|
|
67
|
-
|
|
68
|
-
_defineProperty(this, "closed", false);
|
|
69
|
-
|
|
70
|
-
_classPrivateFieldInitSpec(this, _callbacks, {
|
|
71
|
-
writable: true,
|
|
72
|
-
value: new Map()
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
_classPrivateFieldInitSpec(this, _lastid, {
|
|
76
|
-
writable: true,
|
|
77
|
-
value: 0
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
_defineProperty(this, "args", [// disable the translate popup
|
|
81
|
-
'--disable-features=Translate', // disable several subsystems which run network requests in the background
|
|
82
|
-
'--disable-background-networking', // disable task throttling of timer tasks from background pages
|
|
83
|
-
'--disable-background-timer-throttling', // disable backgrounding renderer processes
|
|
84
|
-
'--disable-renderer-backgrounding', // disable backgrounding renderers for occluded windows (reduce nondeterminism)
|
|
85
|
-
'--disable-backgrounding-occluded-windows', // disable crash reporting
|
|
86
|
-
'--disable-breakpad', // disable client side phishing detection
|
|
87
|
-
'--disable-client-side-phishing-detection', // disable default component extensions with background pages for performance
|
|
88
|
-
'--disable-component-extensions-with-background-pages', // disable installation of default apps on first run
|
|
89
|
-
'--disable-default-apps', // work-around for environments where a small /dev/shm partition causes crashes
|
|
90
|
-
'--disable-dev-shm-usage', // disable extensions
|
|
91
|
-
'--disable-extensions', // disable hang monitor dialogs in renderer processes
|
|
92
|
-
'--disable-hang-monitor', // disable inter-process communication flooding protection for javascript
|
|
93
|
-
'--disable-ipc-flooding-protection', // disable web notifications and the push API
|
|
94
|
-
'--disable-notifications', // disable the prompt when a POST request causes page navigation
|
|
95
|
-
'--disable-prompt-on-repost', // disable syncing browser data with google accounts
|
|
96
|
-
'--disable-sync', // disable site-isolation to make network requests easier to intercept
|
|
97
|
-
'--disable-site-isolation-trials', // disable the first run tasks, whether or not it's actually the first run
|
|
98
|
-
'--no-first-run', // disable the sandbox for all process types that are normally sandboxed
|
|
99
|
-
'--no-sandbox', // enable indication that browser is controlled by automation
|
|
100
|
-
'--enable-automation', // specify a consistent encryption backend across platforms
|
|
101
|
-
'--password-store=basic', // use a mock keychain on Mac to prevent blocking permissions dialogs
|
|
102
|
-
'--use-mock-keychain', // enable remote debugging on the first available port
|
|
103
|
-
'--remote-debugging-port=0']);
|
|
104
|
-
|
|
105
|
-
this.launchTimeout = timeout;
|
|
106
|
-
this.executable = executable;
|
|
107
|
-
this.headless = headless;
|
|
108
|
-
/* istanbul ignore next: only false for debugging */
|
|
109
|
-
|
|
110
|
-
if (this.headless) this.args.push('--headless', '--hide-scrollbars', '--mute-audio');
|
|
111
|
-
|
|
112
|
-
for (let a of args) if (!this.args.includes(a)) this.args.push(a); // transform cookies object to an array of cookie params
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
this.cookies = Array.isArray(cookies) ? cookies : Object.entries(cookies).map(([name, value]) => ({
|
|
116
|
-
name,
|
|
117
|
-
value
|
|
118
|
-
}));
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
async launch() {
|
|
122
|
-
// already launching or launched
|
|
123
|
-
if (this.readyState != null) return;
|
|
124
|
-
this.readyState = 0; // check if any provided executable exists
|
|
125
|
-
|
|
126
|
-
if (this.executable && !(0, _fs.existsSync)(this.executable)) {
|
|
127
|
-
this.log.error(`Browser executable not found: ${this.executable}`);
|
|
128
|
-
this.executable = null;
|
|
129
|
-
} // download and install the browser if not already present
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
this.executable || (this.executable = await _install.default.chromium()); // create a temporary profile directory
|
|
133
|
-
|
|
134
|
-
this.profile = await _fs.promises.mkdtemp(_path.default.join(_os.default.tmpdir(), 'percy-browser-')); // spawn the browser process detached in its own group and session
|
|
135
|
-
|
|
136
|
-
let args = this.args.concat(`--user-data-dir=${this.profile}`);
|
|
137
|
-
this.log.debug('Launching browser');
|
|
138
|
-
this.process = (0, _crossSpawn.default)(this.executable, args, {
|
|
139
|
-
detached: process.platform !== 'win32'
|
|
140
|
-
}); // connect a websocket to the devtools address
|
|
141
|
-
|
|
142
|
-
let addr = await this.address(this.launchTimeout);
|
|
143
|
-
this.ws = new _ws.default(addr, {
|
|
144
|
-
perMessageDeflate: false
|
|
145
|
-
}); // wait until the websocket has connected
|
|
146
|
-
|
|
147
|
-
await new Promise(resolve => this.ws.once('open', resolve));
|
|
148
|
-
this.ws.on('message', data => this._handleMessage(data)); // get version information
|
|
149
|
-
|
|
150
|
-
this.version = await this.send('Browser.getVersion');
|
|
151
|
-
this.log.debug(`Browser connected [${this.process.pid}]: ${this.version.product}`);
|
|
152
|
-
this.readyState = 1;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
isConnected() {
|
|
156
|
-
var _this$ws;
|
|
157
|
-
|
|
158
|
-
return ((_this$ws = this.ws) === null || _this$ws === void 0 ? void 0 : _this$ws.readyState) === _ws.default.OPEN;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
async close() {
|
|
162
|
-
var _this$process4, _this$ws2;
|
|
163
|
-
|
|
164
|
-
// not running, already closed, or closing
|
|
165
|
-
if (this._closed) return this._closed;
|
|
166
|
-
this.readyState = 2;
|
|
167
|
-
this.log.debug('Closing browser'); // resolves when the browser has closed
|
|
168
|
-
|
|
169
|
-
this._closed = Promise.all([new Promise(resolve => {
|
|
170
|
-
/* istanbul ignore next: race condition paranoia */
|
|
171
|
-
if (!this.process || this.process.exitCode) resolve();else this.process.on('exit', resolve);
|
|
172
|
-
}), new Promise(resolve => {
|
|
173
|
-
/* istanbul ignore next: race condition paranoia */
|
|
174
|
-
if (!this.isConnected()) resolve();else this.ws.on('close', resolve);
|
|
175
|
-
})]).then(() => {
|
|
176
|
-
var _this$process, _this$process2, _this$process3;
|
|
177
|
-
|
|
178
|
-
// needed due to a bug in Node 12 - https://github.com/nodejs/node/issues/27097
|
|
179
|
-
(_this$process = this.process) === null || _this$process === void 0 ? void 0 : _this$process.stdin.end();
|
|
180
|
-
(_this$process2 = this.process) === null || _this$process2 === void 0 ? void 0 : _this$process2.stdout.end();
|
|
181
|
-
(_this$process3 = this.process) === null || _this$process3 === void 0 ? void 0 : _this$process3.stderr.end();
|
|
182
|
-
/* istanbul ignore next:
|
|
183
|
-
* this might fail on some systems but ultimately it is just a temp file */
|
|
184
|
-
|
|
185
|
-
if (this.profile) {
|
|
186
|
-
// attempt to clean up the profile directory
|
|
187
|
-
return new Promise((resolve, reject) => {
|
|
188
|
-
(0, _rimraf.default)(this.profile, e => e ? reject(e) : resolve());
|
|
189
|
-
}).catch(error => {
|
|
190
|
-
this.log.debug('Could not clean up temporary browser profile directory.');
|
|
191
|
-
this.log.debug(error);
|
|
192
|
-
});
|
|
193
|
-
}
|
|
194
|
-
}).then(() => {
|
|
195
|
-
this.log.debug('Browser closed');
|
|
196
|
-
this.readyState = 3;
|
|
197
|
-
}); // reject any pending callbacks
|
|
198
|
-
|
|
199
|
-
for (let callback of _classPrivateFieldGet(this, _callbacks).values()) {
|
|
200
|
-
callback.reject(Object.assign(callback.error, {
|
|
201
|
-
message: `Protocol error (${callback.method}): Browser closed.`
|
|
202
|
-
}));
|
|
203
|
-
} // trigger rejecting pending session callbacks
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
for (let session of this.sessions.values()) {
|
|
207
|
-
session._handleClose();
|
|
208
|
-
} // clear own callbacks and sessions
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
_classPrivateFieldGet(this, _callbacks).clear();
|
|
212
|
-
|
|
213
|
-
this.sessions.clear();
|
|
214
|
-
/* istanbul ignore next:
|
|
215
|
-
* difficult to test failure here without mocking private properties */
|
|
216
|
-
|
|
217
|
-
if ((_this$process4 = this.process) !== null && _this$process4 !== void 0 && _this$process4.pid && !this.process.killed) {
|
|
218
|
-
// always force close the browser process
|
|
219
|
-
try {
|
|
220
|
-
this.process.kill('SIGKILL');
|
|
221
|
-
} catch (error) {
|
|
222
|
-
throw new Error(`Unable to close the browser: ${error.stack}`);
|
|
223
|
-
}
|
|
224
|
-
} // close the socket connection
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
(_this$ws2 = this.ws) === null || _this$ws2 === void 0 ? void 0 : _this$ws2.close(); // wait for the browser to close
|
|
228
|
-
|
|
229
|
-
return this._closed;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
async page(options = {}) {
|
|
233
|
-
let {
|
|
234
|
-
targetId
|
|
235
|
-
} = await this.send('Target.createTarget', {
|
|
236
|
-
url: ''
|
|
237
|
-
});
|
|
238
|
-
let {
|
|
239
|
-
sessionId
|
|
240
|
-
} = await this.send('Target.attachToTarget', {
|
|
241
|
-
targetId,
|
|
242
|
-
flatten: true
|
|
243
|
-
});
|
|
244
|
-
let page = new _page.default(this.sessions.get(sessionId), options);
|
|
245
|
-
await page._handleAttachedToTarget();
|
|
246
|
-
return page;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
async send(method, params) {
|
|
250
|
-
/* istanbul ignore next:
|
|
251
|
-
* difficult to test failure here without mocking private properties */
|
|
252
|
-
if (!this.isConnected()) throw new Error('Browser not connected'); // every command needs a unique id
|
|
253
|
-
|
|
254
|
-
let id = _classPrivateFieldSet(this, _lastid, +_classPrivateFieldGet(this, _lastid) + 1);
|
|
255
|
-
|
|
256
|
-
if (!params && typeof method === 'object') {
|
|
257
|
-
// allow providing a raw message as the only argument and return the id
|
|
258
|
-
this.ws.send(JSON.stringify({ ...method,
|
|
259
|
-
id
|
|
260
|
-
}));
|
|
261
|
-
return id;
|
|
262
|
-
} else {
|
|
263
|
-
// send the message payload
|
|
264
|
-
this.ws.send(JSON.stringify({
|
|
265
|
-
id,
|
|
266
|
-
method,
|
|
267
|
-
params
|
|
268
|
-
})); // will resolve or reject when a matching response is received
|
|
269
|
-
|
|
270
|
-
return new Promise((resolve, reject) => {
|
|
271
|
-
_classPrivateFieldGet(this, _callbacks).set(id, {
|
|
272
|
-
error: new Error(),
|
|
273
|
-
resolve,
|
|
274
|
-
reject,
|
|
275
|
-
method
|
|
276
|
-
});
|
|
277
|
-
});
|
|
278
|
-
}
|
|
279
|
-
} // Returns the devtools websocket address. If not already known, will watch the browser's
|
|
280
|
-
// stderr and resolves when it emits the devtools protocol address or rejects if the process
|
|
281
|
-
// exits for any reason or if the address does not appear after the timeout.
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
async address(timeout = 30000) {
|
|
285
|
-
this._address || (this._address = await new Promise((resolve, reject) => {
|
|
286
|
-
let stderr = '';
|
|
287
|
-
|
|
288
|
-
let handleData = chunk => {
|
|
289
|
-
stderr += chunk = chunk.toString();
|
|
290
|
-
let match = chunk.match(/^DevTools listening on (ws:\/\/.*)$/m);
|
|
291
|
-
if (match) cleanup(() => resolve(match[1]));
|
|
292
|
-
};
|
|
293
|
-
|
|
294
|
-
let handleExitClose = () => handleError();
|
|
295
|
-
|
|
296
|
-
let handleError = error => cleanup(() => {
|
|
297
|
-
var _error$message;
|
|
298
|
-
|
|
299
|
-
return reject(new Error(`Failed to launch browser. ${(_error$message = error === null || error === void 0 ? void 0 : error.message) !== null && _error$message !== void 0 ? _error$message : ''}\n${stderr}'\n\n`));
|
|
300
|
-
});
|
|
301
|
-
|
|
302
|
-
let cleanup = callback => {
|
|
303
|
-
clearTimeout(timeoutId);
|
|
304
|
-
this.process.stderr.off('data', handleData);
|
|
305
|
-
this.process.stderr.off('close', handleExitClose);
|
|
306
|
-
this.process.off('exit', handleExitClose);
|
|
307
|
-
this.process.off('error', handleError);
|
|
308
|
-
callback();
|
|
309
|
-
};
|
|
310
|
-
|
|
311
|
-
let timeoutId = setTimeout(() => handleError(new Error(`Timed out after ${timeout}ms`)), timeout);
|
|
312
|
-
this.process.stderr.on('data', handleData);
|
|
313
|
-
this.process.stderr.on('close', handleExitClose);
|
|
314
|
-
this.process.on('exit', handleExitClose);
|
|
315
|
-
this.process.on('error', handleError);
|
|
316
|
-
}));
|
|
317
|
-
return this._address;
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
_handleMessage(data) {
|
|
321
|
-
data = JSON.parse(data);
|
|
322
|
-
|
|
323
|
-
if (data.method === 'Target.attachedToTarget') {
|
|
324
|
-
// create a new session reference when attached to a target
|
|
325
|
-
let session = new _session.default(this, data);
|
|
326
|
-
this.sessions.set(session.sessionId, session);
|
|
327
|
-
} else if (data.method === 'Target.detachedFromTarget') {
|
|
328
|
-
// remove the old session reference when detached from a target
|
|
329
|
-
let session = this.sessions.get(data.params.sessionId);
|
|
330
|
-
this.sessions.delete(data.params.sessionId);
|
|
331
|
-
session === null || session === void 0 ? void 0 : session._handleClose();
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
if (data.sessionId) {
|
|
335
|
-
// message was for a specific session that sent it
|
|
336
|
-
let session = this.sessions.get(data.sessionId);
|
|
337
|
-
session === null || session === void 0 ? void 0 : session._handleMessage(data);
|
|
338
|
-
} else if (data.id && _classPrivateFieldGet(this, _callbacks).has(data.id)) {
|
|
339
|
-
// resolve or reject a pending promise created with #send()
|
|
340
|
-
let callback = _classPrivateFieldGet(this, _callbacks).get(data.id);
|
|
341
|
-
|
|
342
|
-
_classPrivateFieldGet(this, _callbacks).delete(data.id);
|
|
343
|
-
/* istanbul ignore next: races with page._handleMessage() */
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
if (data.error) {
|
|
347
|
-
callback.reject(Object.assign(callback.error, {
|
|
348
|
-
message: `Protocol error (${callback.method}): ${data.error.message}` + ('data' in data.error ? `: ${data.error.data}` : '')
|
|
349
|
-
}));
|
|
350
|
-
} else {
|
|
351
|
-
callback.resolve(data.result);
|
|
352
|
-
}
|
|
353
|
-
} else {
|
|
354
|
-
// emit the message as an event
|
|
355
|
-
this.emit(data.method, data.params);
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
exports.Browser = Browser;
|
|
362
|
-
var _default = Browser;
|
|
363
|
-
exports.default = _default;
|