@percy/core 1.0.0-beta.7 → 1.0.0-beta.73

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 CHANGED
@@ -5,32 +5,67 @@ assets, uploading snapshots, and finalizing builds. Uses `@percy/client` for API
5
5
  Puppeteer browser for asset discovery, and starts a local API server for posting snapshots from
6
6
  other processes.
7
7
 
8
+ - [Usage](#usage)
9
+ - [`#start()`](#start)
10
+ - [`#stop()`](#stopforce)
11
+ - [`#idle()`](#idle)
12
+ - [`#snapshot()`](#snapshotoptions)
13
+ - [Advanced](#advanced)
14
+ - [Download discovery browser on install](#download-discovery-browser-on-install)
15
+ - [Skipping discovery browser download](#skipping-discovery-browser-download)
16
+
8
17
  ## Usage
9
18
 
10
- The `Percy` class will manage a Percy build and perform asset discovery on snapshots before
11
- uploading them to Percy. It also hosts a local API server for Percy SDKs to communicate with.
19
+ A `Percy` class instance can manage a Percy build, take page snapshots, and perform snapshot asset
20
+ discovery. It also hosts a local API server for Percy SDKs to communicate with.
12
21
 
13
22
  ``` js
14
23
  import Percy from '@percy/core'
15
24
 
16
- const percy = new Percy({
17
- token: PERCY_TOKEN, // defaults to PERCY_TOKEN environment variable
18
- loglevel: 'info', // what level logs to write to console
19
- server: true, // start a local API server
20
- port: 5338, // port to start the API server at
21
- concurrency: 5, // concurrency of the #capture() method
22
- snapshot: {}, // global snapshot options (see snapshots section)
23
- discovery: { // asset discovery options
24
- allowedHostnames: [], // list of hostnames allowed to capture from
25
- networkIdleTimeout: 100, // how long before network is considered idle
26
- disableAssetCache: false, // disable discovered asset caching
27
- concurrency: 5, // asset discovery concurrency
28
- launchOptions: {} // browser launch options
29
- },
30
- ...config // additional config options accessible by SDKs
31
- })
25
+ // create a new instance
26
+ const percy = new Percy(percyOptions)
27
+
28
+ // create a new instance and start it
29
+ const percy = await Percy.start(percyOptions)
32
30
  ```
33
31
 
32
+ #### Options
33
+
34
+ - `token` — Your project's `PERCY_TOKEN` (**default** `process.env.PERCY_TOKEN`)
35
+ - `loglevel` — Logger level, one of `"info"`, `"warn"`, `"error"`, `"debug"` (**default** `"info"`)
36
+ - `server` — Controls whether an API server is created (**default** `true`)
37
+ - `port` — API server port (**default** `5338`)
38
+ - `clientInfo` — Client info sent to Percy via a user-agent string
39
+ - `environmentInfo` — Environment info also sent with the user-agent string
40
+ - `deferUploads` — Defer creating a build and uploading snapshots until later
41
+ - `skipUploads` — Skip creating a build and uploading snapshots altogether
42
+
43
+ The following options can also be defined within a Percy config file
44
+
45
+ - `snapshot` — Snapshot options applied to each snapshot
46
+ - `widths` — Widths to take screenshots at (**default** `[375, 1280]`)
47
+ - `minHeight` — Minimum screenshot height (**default** `1024`)
48
+ - `percyCSS` — Percy specific CSS to inject into the snapshot
49
+ - `enableJavaScript` — Enable JavaScript for screenshots (**default** `false`)
50
+ - `discovery` — Asset discovery options
51
+ - `allowedHostnames` — Array of allowed hostnames to capture assets from
52
+ - `requestHeaders` — Request headers used when discovering snapshot assets
53
+ - `authorization` — Basic auth `username` and `password` for protected snapshot assets
54
+ - `disableCache` — Disable asset caching (**default** `false`)
55
+ - `userAgent` — Custom user-agent string used when requesting assets
56
+ - `cookies` — Browser cookies to use when requesting assets
57
+ - `networkIdleTimeout` — Milliseconds to wait for the network to idle (**default** `100`)
58
+ - `concurrency` — Asset discovery concerrency (**default** `5`)
59
+ - `launchOptions` — Asset discovery browser launch options
60
+ - `executable` — Browser executable path (**default** `process.env.PERCY_BROWSER_EXECUTABLE`)
61
+ - `timeout` — Discovery launch timeout, in milliseconds (**default** `30000`)
62
+ - `args` — Additional browser process arguments
63
+ - `headless` — Runs the browser headlessy (**default** `true`)
64
+
65
+ Additional Percy config file options are also allowed and will override any options defined by a
66
+ local config file. These config file options are also made available to SDKs via the local API
67
+ health check endpoint.
68
+
34
69
  ### `#start()`
35
70
 
36
71
  Starting a `Percy` instance will start a local API server, start the asset discovery browser, and
@@ -39,81 +74,137 @@ create a new Percy build. If an asset discovery browser is not found, one will b
39
74
  ``` js
40
75
  await percy.start()
41
76
  // [percy] Percy has started!
42
- // [percy] Created build #1: https://percy.io/org/project/123
43
77
  ```
44
78
 
45
79
  #### API Server
46
80
 
47
81
  Starting a `Percy` instance will start a local API server unless `server` is `false`. The server can
48
- be found at `http://localhost:5338/` or at the provided `port` number. All POST requests accept a
49
- JSON body with the `application/json` content-type.
82
+ be found at `http://localhost:5338/` or at the provided `port` number.
50
83
 
51
84
  - GET `/percy/healthcheck` – Responds with information about the running instance
52
85
  - GET `/percy/dom.js` – Responds with the [`@percy/dom`](./packages/dom) library
86
+ - GET `/percy/idle` - Responds when the running instance is [idle](#idle)
53
87
  - POST `/percy/snapshot` – Calls [`#snapshot()`](#snapshotoptions) with provided snapshot options
54
- - POST `/percy/stop` - Remotely [stops](#stop) the running `Percy` instance
88
+ - POST `/percy/stop` - Remotely [stops](#stopforce) the running `Percy` instance
55
89
 
56
- ### `#snapshot(options)`
90
+ ### `#stop([force])`
57
91
 
58
- Performs asset discovery for the provided DOM snapshot at widths specified here or in the instance's
59
- provided `snapshot` option. This is the primary method used by Percy SDKs to upload snapshots.
92
+ Stopping a `Percy` instance will wait for any pending snapshots, close the asset discovery browser,
93
+ close the local API server, and finalize the current Percy build. When uploads are deferred,
94
+ stopping the instance will also trigger processing of the upload queue. When `force` is `true`,
95
+ queues are cleared and closed to prevent queued snapshots from running.
60
96
 
61
97
  ``` js
62
- // snapshots can be handled concurrently, no need to await
63
- percy.snapshot({
64
- name: 'My Snapshot', // required name
65
- url: 'http://localhost:3000', // required url
66
- domSnapshot: domSnapshot, // required DOM string
67
- widths: [500, 1280], // widths to discover resources
68
- minimumHeight: 1024, // minimum height used when screenshotting
69
- percyCSS: '', // percy specific css to inject
70
- requestHeaders: {}, // asset request headers such as authorization
71
- clientInfo: '', // user-agent client info for the SDK
72
- environmentInfo: '' // user-agent environment info for the SDK
73
- })
98
+ await percy.stop()
99
+ // [percy] Processing 3 snapshots...
100
+ // [percy] Snapshot taken: Snapshot one
101
+ // [percy] Snapshot taken: Snapshot two
102
+ // [percy] Snapshot taken: Snapshot three
103
+ // [percy] Uploading 3 snapshots...
104
+ // [percy] Finalized build #1: https://percy.io/org/project/123
105
+
106
+ await percy.stop(true)
107
+ // [percy] Stopping percy...
108
+ // [percy] Finalized build #1: https://percy.io/org/project/123
74
109
  ```
75
110
 
76
- ### `#capture(options)`
111
+ ### `#idle()`
77
112
 
78
- Navigates to a URL and captures a snapshot or multiple snapshots of a page after optionally
79
- interacting with the page. Any [`#snapshot()`](#snapshotoptions) options can also be provided, with
80
- the exception of `domSnapshot`.
113
+ This method will resolve shortly after pending snapshots and uploads have completed and no more have
114
+ started. Queued tasks are not considered pending unless they are actively running, so deferred
115
+ uploads will not be awaited on with this method.
81
116
 
82
117
  ``` js
83
- // pages can be captured concurrently, no need to await
84
- percy.capture({
85
- name: 'My Snapshot', // snapshot name
86
- url: 'http://localhost:3000/', // required page URL
87
- waitFor: selectorOrTimeout, // selector or timeout to wait for before snapshotting
88
- execute: async (page) => {}, // async page function to execute before snapshotting
89
- snapshots: [{ // additional snapshots to take on this page
90
- name: 'Second Snapshot', // additional snapshot name
91
- execute: async (page) => {}, // additional snapshot page function
92
- ...options // ...additional snapshot options
93
- }],
94
- ...options // ...other snapshot options
95
- })
118
+ percy.snapshot(...);
119
+ percy.snapshot(...);
120
+ percy.snapshot(...);
121
+
122
+ await percy.idle()
123
+ // [percy] Snapshot taken: ...
124
+ // [percy] Snapshot taken: ...
125
+ // [percy] Snapshot taken: ...
96
126
  ```
97
127
 
98
- ### `#stop()`
128
+ ### `#snapshot(options)`
99
129
 
100
- Stopping a `Percy` instance will wait for any pending snapshots, close the asset discovery browser,
101
- close the local API server, and finalize the current Percy build.
130
+ Performs asset discovery for a snapshot and queues uploading the snapshot to the associated Percy
131
+ build. When an existing DOM snapshot is provided, it is served as the root resource during asset
132
+ discovery. When no existing DOM snapshot is provided, a new one will be captured using any provided
133
+ snapshot capture options.
102
134
 
103
135
  ``` js
104
- await percy.stop()
105
- // [percy] Stopping percy...
106
- // [percy] Waiting for 1 snapshot(s) to complete
107
- // [percy] Snapshot taken: My Snapshot
108
- // [percy] Finalized build #1: https://percy.io/org/project/123
109
- // [percy] Done
136
+ // snapshots can be handled concurrently, no need to await
137
+ percy.snapshot({
138
+ name: 'Snapshot 1',
139
+ url: 'http://localhost:3000',
140
+ domSnapshot: domSnapshot,
141
+ clientInfo: 'my-sdk',
142
+ environmentInfo: 'my-lib'
143
+ ...snapshotOptions
144
+ })
145
+
146
+ percy.snapshot({
147
+ name: 'Snapshot 2',
148
+ url: 'http://localhost:3000/',
149
+ ...snapshotOptions,
150
+
151
+ // without a domSnapshot, capture options will be used to take one
152
+ waitForTimeout: 1000,
153
+ waitForSelector: '.done-loading',
154
+ execute: async () => {},
155
+ additionalSnapshots: [{
156
+ name: 'Snapshot 2.1',
157
+ execute: () => {},
158
+ ...snapshotOptions
159
+ }]
160
+ })
110
161
  ```
111
162
 
163
+ #### Options
164
+
165
+ - `url` — Snapshot URL (**required**)
166
+ - `name` — Snapshot name
167
+ - `domSnapshot` — Snapshot DOM string
168
+ - `clientInfo` — Additional client info
169
+ - `environmentInfo` — Additional environment info
170
+ - `discovery` - Limited snapshot specific discovery options
171
+ - `allowedHostnames`, `requestHeaders`, `authorization`, `disableCache`, `userAgent`
172
+
173
+ **Capture options** can only be provided when `domSnapshot` is missing.
174
+
175
+ - `waitForTimeout` — Milliseconds to wait before taking a snapshot
176
+ - `waitForSelector` — CSS selector to wait for before taking a snapshot
177
+ - `execute` — Function or function body to execute within the page before taking a snapshot
178
+ - `additionalSnapshots` — Array of additional sequential snapshots to take of the page
179
+ - `name` — Snapshot name (**required** if no `prefix` or `suffix`)
180
+ - `prefix` — Snapshot name prefix (**required** if no `name` or `suffix`)
181
+ - `suffix` — Snapshot name suffix (**required** if no `name` or `prefix`)
182
+ - `waitForTimeout`, `waitForSelector`, `execute` — See above
183
+
184
+ Common snapshot options are also accepted and will override instance snapshot options. [See instance
185
+ options](#options) for common snapshot and discovery options.
186
+
112
187
  ## Advanced
113
188
 
114
- ### Puppeteer Executable Path
189
+ ### Download discovery browser on install
190
+
191
+ By default, the browser is only downloaded when asset discovery is started for the first time. This
192
+ is because many features of the CLI do not require a browser at all, and automatically downloading a
193
+ browser creates a much heavier footprint than needed for those features. However, if your CI caches
194
+ dependencies after the install step, the browser will not be cached and will be downloaded every
195
+ time Percy runs without it.
196
+
197
+ If the environment variable `PERCY_POSTINSTALL_BROWSER` is present and truthy, then the browser will
198
+ be downloaded after the package is installed to allow it to be cached. You can also require
199
+ `@percy/core/post-install` within another node module to trigger the browser download manually.
200
+
201
+ ### Skipping discovery browser download
115
202
 
116
- To avoid downloading the browser used for asset discovery, the local browser executable can be
117
- defined with an `executablePath` option provided within `discovery.launchOptions`. This options
118
- falls back to the `PUPPETEER_EXECUTABLE_PATH` environment variable if defined.
203
+ If your CI comes with a Chromium binary pre-installed and you wish to skip Percy's own browser
204
+ installation, you can set the respective `discovery.launchOptions.executable` config option. When
205
+ the executable at the provided path exists, the default download will be skipped and the provided
206
+ binary will be used instead. This option can also be set using the `PERCY_BROWSER_EXECUTABLE`
207
+ environment variable.
119
208
 
209
+ > **Warning!** Percy is only tested against the browser it downloads automatically. When providing a
210
+ > custom browser executable, you may experience unexpected issues.
@@ -0,0 +1,363 @@
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;