@percy/core 1.12.0 → 1.13.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/dist/api.js +60 -45
- package/dist/browser.js +82 -68
- package/dist/config.js +16 -9
- package/dist/discovery.js +65 -60
- package/dist/install.js +29 -27
- package/dist/network.js +72 -79
- package/dist/page.js +47 -54
- package/dist/percy.js +85 -85
- package/dist/queue.js +103 -146
- package/dist/server.js +51 -88
- package/dist/session.js +8 -15
- package/dist/snapshot.js +105 -92
- package/dist/utils.js +60 -58
- package/package.json +6 -6
package/dist/api.js
CHANGED
|
@@ -3,112 +3,118 @@ import path from 'path';
|
|
|
3
3
|
import { createRequire } from 'module';
|
|
4
4
|
import logger from '@percy/logger';
|
|
5
5
|
import { normalize } from '@percy/config/utils';
|
|
6
|
-
import { getPackageJSON, Server } from './utils.js';
|
|
6
|
+
import { getPackageJSON, Server } from './utils.js';
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
// need require.resolve until import.meta.resolve can be transpiled
|
|
9
|
+
export const PERCY_DOM = createRequire(import.meta.url).resolve('@percy/dom');
|
|
9
10
|
|
|
11
|
+
// Returns a URL encoded string of nested query params
|
|
10
12
|
function encodeURLSearchParams(subj, prefix) {
|
|
11
13
|
return typeof subj === 'object' ? Object.entries(subj).map(([key, value]) => encodeURLSearchParams(value, prefix ? `${prefix}[${key}]` : key)).join('&') : `${prefix}=${encodeURIComponent(subj)}`;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
+
}
|
|
14
15
|
|
|
16
|
+
// Create a Percy CLI API server instance
|
|
15
17
|
export function createPercyServer(percy, port) {
|
|
16
18
|
let pkg = getPackageJSON(import.meta.url);
|
|
17
19
|
let server = Server.createServer({
|
|
18
20
|
port
|
|
19
|
-
})
|
|
21
|
+
})
|
|
22
|
+
// general middleware
|
|
20
23
|
.route((req, res, next) => {
|
|
21
24
|
var _percy$testing, _percy$testing4, _percy$testing4$api, _percy$testing5, _percy$testing5$api;
|
|
22
|
-
|
|
23
25
|
// treat all request bodies as json
|
|
24
26
|
if (req.body) try {
|
|
25
27
|
req.body = JSON.parse(req.body);
|
|
26
|
-
} catch {}
|
|
28
|
+
} catch {}
|
|
27
29
|
|
|
28
|
-
|
|
30
|
+
// add version header
|
|
31
|
+
res.setHeader('Access-Control-Expose-Headers', '*, X-Percy-Core-Version');
|
|
29
32
|
|
|
33
|
+
// skip or change api version header in testing mode
|
|
30
34
|
if (((_percy$testing = percy.testing) === null || _percy$testing === void 0 ? void 0 : _percy$testing.version) !== false) {
|
|
31
35
|
var _percy$testing2;
|
|
32
|
-
|
|
33
36
|
res.setHeader('X-Percy-Core-Version', ((_percy$testing2 = percy.testing) === null || _percy$testing2 === void 0 ? void 0 : _percy$testing2.version) ?? pkg.version);
|
|
34
|
-
}
|
|
35
|
-
|
|
37
|
+
}
|
|
36
38
|
|
|
39
|
+
// track all api reqeusts in testing mode
|
|
37
40
|
if (percy.testing && !req.url.pathname.startsWith('/test/')) {
|
|
38
41
|
var _percy$testing3;
|
|
39
|
-
|
|
40
42
|
((_percy$testing3 = percy.testing).requests || (_percy$testing3.requests = [])).push({
|
|
41
43
|
url: `${req.url.pathname}${req.url.search}`,
|
|
42
44
|
method: req.method,
|
|
43
45
|
body: req.body
|
|
44
46
|
});
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
+
}
|
|
47
48
|
|
|
49
|
+
// support sabotaging requests in testing mode
|
|
48
50
|
if (((_percy$testing4 = percy.testing) === null || _percy$testing4 === void 0 ? void 0 : (_percy$testing4$api = _percy$testing4.api) === null || _percy$testing4$api === void 0 ? void 0 : _percy$testing4$api[req.url.pathname]) === 'error') {
|
|
49
51
|
next = () => {
|
|
50
52
|
var _percy$testing$build;
|
|
51
|
-
|
|
52
53
|
return Promise.reject(new Error(((_percy$testing$build = percy.testing.build) === null || _percy$testing$build === void 0 ? void 0 : _percy$testing$build.error) || 'testing'));
|
|
53
54
|
};
|
|
54
55
|
} else if (((_percy$testing5 = percy.testing) === null || _percy$testing5 === void 0 ? void 0 : (_percy$testing5$api = _percy$testing5.api) === null || _percy$testing5$api === void 0 ? void 0 : _percy$testing5$api[req.url.pathname]) === 'disconnect') {
|
|
55
56
|
next = () => req.connection.destroy();
|
|
56
|
-
}
|
|
57
|
-
|
|
57
|
+
}
|
|
58
58
|
|
|
59
|
+
// return json errors
|
|
59
60
|
return next().catch(e => {
|
|
60
61
|
var _percy$testing6;
|
|
61
|
-
|
|
62
62
|
return res.json(e.status ?? 500, {
|
|
63
63
|
build: ((_percy$testing6 = percy.testing) === null || _percy$testing6 === void 0 ? void 0 : _percy$testing6.build) || percy.build,
|
|
64
64
|
error: e.message,
|
|
65
65
|
success: false
|
|
66
66
|
});
|
|
67
67
|
});
|
|
68
|
-
})
|
|
68
|
+
})
|
|
69
|
+
// healthcheck returns basic information
|
|
69
70
|
.route('get', '/percy/healthcheck', (req, res) => {
|
|
70
71
|
var _percy$testing7;
|
|
71
|
-
|
|
72
72
|
return res.json(200, {
|
|
73
73
|
build: ((_percy$testing7 = percy.testing) === null || _percy$testing7 === void 0 ? void 0 : _percy$testing7.build) ?? percy.build,
|
|
74
74
|
loglevel: percy.loglevel(),
|
|
75
75
|
config: percy.config,
|
|
76
76
|
success: true
|
|
77
77
|
});
|
|
78
|
-
})
|
|
78
|
+
})
|
|
79
|
+
// get or set config options
|
|
79
80
|
.route(['get', 'post'], '/percy/config', async (req, res) => res.json(200, {
|
|
80
81
|
config: req.body ? percy.set(req.body) : percy.config,
|
|
81
82
|
success: true
|
|
82
|
-
}))
|
|
83
|
+
}))
|
|
84
|
+
// responds once idle (may take a long time)
|
|
83
85
|
.route('get', '/percy/idle', async (req, res) => res.json(200, {
|
|
84
86
|
success: await percy.idle().then(() => true)
|
|
85
|
-
}))
|
|
87
|
+
}))
|
|
88
|
+
// convenient @percy/dom bundle
|
|
86
89
|
.route('get', '/percy/dom.js', (req, res) => {
|
|
87
90
|
return res.file(200, PERCY_DOM);
|
|
88
|
-
})
|
|
91
|
+
})
|
|
92
|
+
// legacy agent wrapper for @percy/dom
|
|
89
93
|
.route('get', '/percy-agent.js', async (req, res) => {
|
|
90
94
|
logger('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(' '));
|
|
91
95
|
let content = await fs.promises.readFile(PERCY_DOM, 'utf-8');
|
|
92
96
|
let wrapper = '(window.PercyAgent = class { snapshot(n, o) { return PercyDOM.serialize(o); } });';
|
|
93
97
|
return res.send(200, 'applicaton/javascript', content.concat(wrapper));
|
|
94
|
-
})
|
|
98
|
+
})
|
|
99
|
+
// post one or more snapshots, optionally async
|
|
95
100
|
.route('post', '/percy/snapshot', async (req, res) => {
|
|
96
101
|
let snapshot = percy.snapshot(req.body);
|
|
97
102
|
if (!req.url.searchParams.has('async')) await snapshot;
|
|
98
103
|
return res.json(200, {
|
|
99
104
|
success: true
|
|
100
105
|
});
|
|
101
|
-
})
|
|
106
|
+
})
|
|
107
|
+
// post one or more comparisons, optionally waiting
|
|
102
108
|
.route('post', '/percy/comparison', async (req, res) => {
|
|
103
109
|
let upload = percy.upload(req.body);
|
|
104
|
-
if (req.url.searchParams.has('await')) await upload;
|
|
110
|
+
if (req.url.searchParams.has('await')) await upload;
|
|
105
111
|
|
|
112
|
+
// generate and include one or more redirect links to comparisons
|
|
106
113
|
let link = ({
|
|
107
114
|
name,
|
|
108
115
|
tag
|
|
109
116
|
}) => {
|
|
110
117
|
var _percy$build;
|
|
111
|
-
|
|
112
118
|
return [percy.client.apiUrl, '/comparisons/redirect?', encodeURLSearchParams(normalize({
|
|
113
119
|
buildId: (_percy$build = percy.build) === null || _percy$build === void 0 ? void 0 : _percy$build.id,
|
|
114
120
|
snapshot: {
|
|
@@ -119,7 +125,6 @@ export function createPercyServer(percy, port) {
|
|
|
119
125
|
snake: true
|
|
120
126
|
}))].join('');
|
|
121
127
|
};
|
|
122
|
-
|
|
123
128
|
return res.json(200, Object.assign({
|
|
124
129
|
success: true
|
|
125
130
|
}, req.body ? Array.isArray(req.body) ? {
|
|
@@ -127,18 +132,22 @@ export function createPercyServer(percy, port) {
|
|
|
127
132
|
} : {
|
|
128
133
|
link: link(req.body)
|
|
129
134
|
} : {}));
|
|
130
|
-
})
|
|
135
|
+
})
|
|
136
|
+
// flushes one or more snapshots from the internal queue
|
|
131
137
|
.route('post', '/percy/flush', async (req, res) => res.json(200, {
|
|
132
138
|
success: await percy.flush(req.body).then(() => true)
|
|
133
|
-
}))
|
|
139
|
+
}))
|
|
140
|
+
// stops percy at the end of the current event loop
|
|
134
141
|
.route('/percy/stop', (req, res) => {
|
|
135
142
|
setImmediate(() => percy.stop());
|
|
136
143
|
return res.json(200, {
|
|
137
144
|
success: true
|
|
138
145
|
});
|
|
139
|
-
});
|
|
146
|
+
});
|
|
140
147
|
|
|
141
|
-
|
|
148
|
+
// add test endpoints only in testing mode
|
|
149
|
+
return !percy.testing ? server : server
|
|
150
|
+
// manipulates testing mode configuration to trigger specific scenarios
|
|
142
151
|
.route('/test/api/:cmd', ({
|
|
143
152
|
body,
|
|
144
153
|
params: {
|
|
@@ -146,7 +155,6 @@ export function createPercyServer(percy, port) {
|
|
|
146
155
|
}
|
|
147
156
|
}, res) => {
|
|
148
157
|
body = Buffer.isBuffer(body) ? body.toString() : body;
|
|
149
|
-
|
|
150
158
|
if (cmd === 'reset') {
|
|
151
159
|
// the reset command will reset testing mode and clear any logs
|
|
152
160
|
percy.testing = {};
|
|
@@ -156,7 +164,6 @@ export function createPercyServer(percy, port) {
|
|
|
156
164
|
percy.testing.version = body;
|
|
157
165
|
} else if (cmd === 'error' || cmd === 'disconnect') {
|
|
158
166
|
var _percy$testing8;
|
|
159
|
-
|
|
160
167
|
// the error or disconnect commands will cause specific endpoints to fail
|
|
161
168
|
((_percy$testing8 = percy.testing).api || (_percy$testing8.api = {}))[body] = cmd;
|
|
162
169
|
} else if (cmd === 'build-failure') {
|
|
@@ -169,35 +176,43 @@ export function createPercyServer(percy, port) {
|
|
|
169
176
|
// 404 for unknown commands
|
|
170
177
|
return res.send(404);
|
|
171
178
|
}
|
|
172
|
-
|
|
173
179
|
return res.json(200, {
|
|
174
180
|
success: true
|
|
175
181
|
});
|
|
176
|
-
})
|
|
182
|
+
})
|
|
183
|
+
// returns an array of raw requests made to the api
|
|
177
184
|
.route('get', '/test/requests', (req, res) => res.json(200, {
|
|
178
185
|
requests: percy.testing.requests
|
|
179
|
-
}))
|
|
186
|
+
}))
|
|
187
|
+
// returns an array of raw logs from the logger
|
|
180
188
|
.route('get', '/test/logs', (req, res) => res.json(200, {
|
|
181
189
|
logs: Array.from(logger.instance.messages)
|
|
182
|
-
}))
|
|
190
|
+
}))
|
|
191
|
+
// serves a very basic html page for testing snapshots
|
|
183
192
|
.route('get', '/test/snapshot', (req, res) => {
|
|
184
193
|
return res.send(200, 'text/html', '<p>Snapshot Me!</p>');
|
|
185
194
|
});
|
|
186
|
-
}
|
|
195
|
+
}
|
|
187
196
|
|
|
197
|
+
// Create a static server instance with an automatic sitemap
|
|
188
198
|
export function createStaticServer(options) {
|
|
189
199
|
let {
|
|
190
200
|
serve: dir,
|
|
191
201
|
baseUrl = ''
|
|
192
202
|
} = options;
|
|
193
|
-
let server = Server.createServer(options);
|
|
203
|
+
let server = Server.createServer(options);
|
|
194
204
|
|
|
195
|
-
|
|
205
|
+
// remove trailing slashes so the base snapshot name matches other snapshots
|
|
206
|
+
baseUrl = baseUrl.replace(/\/$/, '');
|
|
196
207
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
208
|
+
// used when generating an automatic sitemap
|
|
209
|
+
let toURL = Server.createRewriter(
|
|
210
|
+
// reverse rewrites' src, dest, & order
|
|
211
|
+
Object.entries((options === null || options === void 0 ? void 0 : options.rewrites) ?? {}).reduce((acc, rw) => [rw.reverse(), ...acc], []), (filename, rewrite) => new URL(path.posix.join('/', baseUrl,
|
|
212
|
+
// cleanUrls will trim trailing .html/index.html from paths
|
|
213
|
+
!options.cleanUrls ? rewrite(filename) : rewrite(filename).replace(/(\/index)?\.html$/, '')), server.address()));
|
|
200
214
|
|
|
215
|
+
// include automatic sitemap route
|
|
201
216
|
server.route('get', `${baseUrl}/sitemap.xml`, async (req, res) => {
|
|
202
217
|
let {
|
|
203
218
|
default: glob
|
package/dist/browser.js
CHANGED
|
@@ -16,35 +16,56 @@ export class Browser extends EventEmitter {
|
|
|
16
16
|
closed = false;
|
|
17
17
|
#callbacks = new Map();
|
|
18
18
|
#lastid = 0;
|
|
19
|
-
args = [
|
|
20
|
-
|
|
21
|
-
'--disable-
|
|
22
|
-
|
|
23
|
-
'--disable-
|
|
24
|
-
|
|
25
|
-
'--disable-
|
|
26
|
-
|
|
27
|
-
'--disable-
|
|
28
|
-
|
|
29
|
-
'--disable-
|
|
30
|
-
|
|
31
|
-
'--disable-
|
|
32
|
-
|
|
33
|
-
'--disable-
|
|
34
|
-
|
|
35
|
-
'--disable-
|
|
36
|
-
|
|
37
|
-
'--
|
|
38
|
-
|
|
39
|
-
'--
|
|
40
|
-
|
|
41
|
-
'--
|
|
19
|
+
args = [
|
|
20
|
+
// disable the translate popup
|
|
21
|
+
'--disable-features=Translate',
|
|
22
|
+
// disable several subsystems which run network requests in the background
|
|
23
|
+
'--disable-background-networking',
|
|
24
|
+
// disable task throttling of timer tasks from background pages
|
|
25
|
+
'--disable-background-timer-throttling',
|
|
26
|
+
// disable backgrounding renderer processes
|
|
27
|
+
'--disable-renderer-backgrounding',
|
|
28
|
+
// disable backgrounding renderers for occluded windows (reduce nondeterminism)
|
|
29
|
+
'--disable-backgrounding-occluded-windows',
|
|
30
|
+
// disable crash reporting
|
|
31
|
+
'--disable-breakpad',
|
|
32
|
+
// disable client side phishing detection
|
|
33
|
+
'--disable-client-side-phishing-detection',
|
|
34
|
+
// disable default component extensions with background pages for performance
|
|
35
|
+
'--disable-component-extensions-with-background-pages',
|
|
36
|
+
// disable installation of default apps on first run
|
|
37
|
+
'--disable-default-apps',
|
|
38
|
+
// work-around for environments where a small /dev/shm partition causes crashes
|
|
39
|
+
'--disable-dev-shm-usage',
|
|
40
|
+
// disable extensions
|
|
41
|
+
'--disable-extensions',
|
|
42
|
+
// disable hang monitor dialogs in renderer processes
|
|
43
|
+
'--disable-hang-monitor',
|
|
44
|
+
// disable inter-process communication flooding protection for javascript
|
|
45
|
+
'--disable-ipc-flooding-protection',
|
|
46
|
+
// disable web notifications and the push API
|
|
47
|
+
'--disable-notifications',
|
|
48
|
+
// disable the prompt when a POST request causes page navigation
|
|
49
|
+
'--disable-prompt-on-repost',
|
|
50
|
+
// disable syncing browser data with google accounts
|
|
51
|
+
'--disable-sync',
|
|
52
|
+
// disable site-isolation to make network requests easier to intercept
|
|
53
|
+
'--disable-site-isolation-trials',
|
|
54
|
+
// disable the first run tasks, whether or not it's actually the first run
|
|
55
|
+
'--no-first-run',
|
|
56
|
+
// disable the sandbox for all process types that are normally sandboxed
|
|
57
|
+
'--no-sandbox',
|
|
58
|
+
// enable indication that browser is controlled by automation
|
|
59
|
+
'--enable-automation',
|
|
60
|
+
// specify a consistent encryption backend across platforms
|
|
61
|
+
'--password-store=basic',
|
|
62
|
+
// use a mock keychain on Mac to prevent blocking permissions dialogs
|
|
63
|
+
'--use-mock-keychain',
|
|
64
|
+
// enable remote debugging on the first available port
|
|
42
65
|
'--remote-debugging-port=0'];
|
|
43
|
-
|
|
44
66
|
constructor(percy) {
|
|
45
67
|
super().percy = percy;
|
|
46
68
|
}
|
|
47
|
-
|
|
48
69
|
async launch() {
|
|
49
70
|
// already launching or launched
|
|
50
71
|
if (this.readyState != null) return;
|
|
@@ -59,57 +80,57 @@ export class Browser extends EventEmitter {
|
|
|
59
80
|
args = [],
|
|
60
81
|
timeout
|
|
61
82
|
} = launchOptions;
|
|
62
|
-
executable ?? (executable = process.env.PERCY_BROWSER_EXECUTABLE);
|
|
83
|
+
executable ?? (executable = process.env.PERCY_BROWSER_EXECUTABLE);
|
|
63
84
|
|
|
85
|
+
// transform cookies object to an array of cookie params
|
|
64
86
|
this.cookies = Array.isArray(cookies) ? cookies : Object.entries(cookies).map(([name, value]) => ({
|
|
65
87
|
name,
|
|
66
88
|
value
|
|
67
|
-
}));
|
|
89
|
+
}));
|
|
68
90
|
|
|
91
|
+
// check if any provided executable exists
|
|
69
92
|
if (executable && !fs.existsSync(executable)) {
|
|
70
93
|
this.log.error(`Browser executable not found: ${executable}`);
|
|
71
94
|
executable = null;
|
|
72
|
-
}
|
|
73
|
-
|
|
95
|
+
}
|
|
74
96
|
|
|
97
|
+
// download and install the browser if not already present
|
|
75
98
|
this.executable = executable || (await install.chromium());
|
|
76
|
-
this.log.debug('Launching browser');
|
|
99
|
+
this.log.debug('Launching browser');
|
|
77
100
|
|
|
101
|
+
// create a temporary profile directory and collect additional launch arguments
|
|
78
102
|
this.profile = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'percy-browser-'));
|
|
79
103
|
/* istanbul ignore next: only false for debugging */
|
|
80
|
-
|
|
81
104
|
if (headless) this.args.push('--headless', '--hide-scrollbars', '--mute-audio');
|
|
82
|
-
|
|
83
105
|
for (let a of args) if (!this.args.includes(a)) this.args.push(a);
|
|
106
|
+
this.args.push(`--user-data-dir=${this.profile}`);
|
|
84
107
|
|
|
85
|
-
|
|
86
|
-
|
|
108
|
+
// spawn the browser process and connect a websocket to the devtools address
|
|
87
109
|
this.ws = new WebSocket(await this.spawn(timeout), {
|
|
88
110
|
perMessageDeflate: false
|
|
89
|
-
});
|
|
111
|
+
});
|
|
90
112
|
|
|
113
|
+
// wait until the websocket has connected
|
|
91
114
|
await new Promise(resolve => this.ws.once('open', resolve));
|
|
92
|
-
this.ws.on('message', data => this._handleMessage(data));
|
|
115
|
+
this.ws.on('message', data => this._handleMessage(data));
|
|
93
116
|
|
|
117
|
+
// get version information
|
|
94
118
|
this.version = await this.send('Browser.getVersion');
|
|
95
119
|
this.log.debug(`Browser connected [${this.process.pid}]: ${this.version.product}`);
|
|
96
120
|
this.readyState = 1;
|
|
97
121
|
}
|
|
98
|
-
|
|
99
122
|
isConnected() {
|
|
100
123
|
var _this$ws;
|
|
101
|
-
|
|
102
124
|
return ((_this$ws = this.ws) === null || _this$ws === void 0 ? void 0 : _this$ws.readyState) === WebSocket.OPEN;
|
|
103
125
|
}
|
|
104
|
-
|
|
105
126
|
async close() {
|
|
106
127
|
var _this$process, _this$ws2;
|
|
107
|
-
|
|
108
128
|
// not running, already closed, or closing
|
|
109
129
|
if (this._closed) return this._closed;
|
|
110
130
|
this.readyState = 2;
|
|
111
|
-
this.log.debug('Closing browser');
|
|
131
|
+
this.log.debug('Closing browser');
|
|
112
132
|
|
|
133
|
+
// resolves when the browser has closed
|
|
113
134
|
this._closed = Promise.all([new Promise(resolve => {
|
|
114
135
|
/* istanbul ignore next: race condition paranoia */
|
|
115
136
|
if (!this.process || this.process.exitCode) resolve();else this.process.on('exit', resolve);
|
|
@@ -131,25 +152,26 @@ export class Browser extends EventEmitter {
|
|
|
131
152
|
}).then(() => {
|
|
132
153
|
this.log.debug('Browser closed');
|
|
133
154
|
this.readyState = 3;
|
|
134
|
-
});
|
|
155
|
+
});
|
|
135
156
|
|
|
157
|
+
// reject any pending callbacks
|
|
136
158
|
for (let callback of this.#callbacks.values()) {
|
|
137
159
|
callback.reject(Object.assign(callback.error, {
|
|
138
160
|
message: `Protocol error (${callback.method}): Browser closed.`
|
|
139
161
|
}));
|
|
140
|
-
}
|
|
141
|
-
|
|
162
|
+
}
|
|
142
163
|
|
|
164
|
+
// trigger rejecting pending session callbacks
|
|
143
165
|
for (let session of this.sessions.values()) {
|
|
144
166
|
session._handleClose();
|
|
145
|
-
}
|
|
146
|
-
|
|
167
|
+
}
|
|
147
168
|
|
|
169
|
+
// clear own callbacks and sessions
|
|
148
170
|
this.#callbacks.clear();
|
|
149
171
|
this.sessions.clear();
|
|
172
|
+
|
|
150
173
|
/* istanbul ignore next:
|
|
151
174
|
* difficult to test failure here without mocking private properties */
|
|
152
|
-
|
|
153
175
|
if ((_this$process = this.process) !== null && _this$process !== void 0 && _this$process.pid && !this.process.killed) {
|
|
154
176
|
// always force close the browser process
|
|
155
177
|
try {
|
|
@@ -157,14 +179,14 @@ export class Browser extends EventEmitter {
|
|
|
157
179
|
} catch (error) {
|
|
158
180
|
throw new Error(`Unable to close the browser: ${error.stack}`);
|
|
159
181
|
}
|
|
160
|
-
}
|
|
161
|
-
|
|
182
|
+
}
|
|
162
183
|
|
|
163
|
-
|
|
184
|
+
// close the socket connection
|
|
185
|
+
(_this$ws2 = this.ws) === null || _this$ws2 === void 0 ? void 0 : _this$ws2.close();
|
|
164
186
|
|
|
187
|
+
// wait for the browser to close
|
|
165
188
|
return this._closed;
|
|
166
189
|
}
|
|
167
|
-
|
|
168
190
|
async page(options = {}) {
|
|
169
191
|
let {
|
|
170
192
|
targetId
|
|
@@ -181,17 +203,17 @@ export class Browser extends EventEmitter {
|
|
|
181
203
|
await page._handleAttachedToTarget();
|
|
182
204
|
return page;
|
|
183
205
|
}
|
|
184
|
-
|
|
185
206
|
async send(method, params) {
|
|
186
207
|
/* istanbul ignore next:
|
|
187
208
|
* difficult to test failure here without mocking private properties */
|
|
188
|
-
if (!this.isConnected()) throw new Error('Browser not connected');
|
|
209
|
+
if (!this.isConnected()) throw new Error('Browser not connected');
|
|
189
210
|
|
|
211
|
+
// every command needs a unique id
|
|
190
212
|
let id = ++this.#lastid;
|
|
191
|
-
|
|
192
213
|
if (!params && typeof method === 'object') {
|
|
193
214
|
// allow providing a raw message as the only argument and return the id
|
|
194
|
-
this.ws.send(JSON.stringify({
|
|
215
|
+
this.ws.send(JSON.stringify({
|
|
216
|
+
...method,
|
|
195
217
|
id
|
|
196
218
|
}));
|
|
197
219
|
return id;
|
|
@@ -201,8 +223,9 @@ export class Browser extends EventEmitter {
|
|
|
201
223
|
id,
|
|
202
224
|
method,
|
|
203
225
|
params
|
|
204
|
-
}));
|
|
226
|
+
}));
|
|
205
227
|
|
|
228
|
+
// will resolve or reject when a matching response is received
|
|
206
229
|
return new Promise((resolve, reject) => {
|
|
207
230
|
this.#callbacks.set(id, {
|
|
208
231
|
error: new Error(),
|
|
@@ -213,26 +236,22 @@ export class Browser extends EventEmitter {
|
|
|
213
236
|
});
|
|
214
237
|
}
|
|
215
238
|
}
|
|
216
|
-
|
|
217
239
|
async spawn(timeout = 30000) {
|
|
218
240
|
// spawn the browser process detached in its own group and session
|
|
219
241
|
this.process = spawn(this.executable, this.args, {
|
|
220
242
|
detached: process.platform !== 'win32'
|
|
221
|
-
});
|
|
243
|
+
});
|
|
222
244
|
|
|
245
|
+
// watch the process stderr and resolve when it emits the devtools protocol address
|
|
223
246
|
this.address = await new Promise((resolve, reject) => {
|
|
224
247
|
let stderr = '';
|
|
225
|
-
|
|
226
248
|
let handleData = chunk => {
|
|
227
249
|
stderr += chunk = chunk.toString();
|
|
228
250
|
let match = chunk.match(/^DevTools listening on (ws:\/\/.*)$/m);
|
|
229
251
|
if (match) cleanup(() => resolve(match[1]));
|
|
230
252
|
};
|
|
231
|
-
|
|
232
253
|
let handleExitClose = () => handleError();
|
|
233
|
-
|
|
234
254
|
let handleError = error => cleanup(() => reject(new Error(`Failed to launch browser. ${(error === null || error === void 0 ? void 0 : error.message) ?? ''}\n${stderr}'\n\n`)));
|
|
235
|
-
|
|
236
255
|
let cleanup = callback => {
|
|
237
256
|
clearTimeout(timeoutId);
|
|
238
257
|
this.process.stderr.off('data', handleData);
|
|
@@ -241,7 +260,6 @@ export class Browser extends EventEmitter {
|
|
|
241
260
|
this.process.off('error', handleError);
|
|
242
261
|
callback();
|
|
243
262
|
};
|
|
244
|
-
|
|
245
263
|
let timeoutId = setTimeout(() => handleError(new Error(`Timed out after ${timeout}ms`)), timeout);
|
|
246
264
|
this.process.stderr.on('data', handleData);
|
|
247
265
|
this.process.stderr.on('close', handleExitClose);
|
|
@@ -250,10 +268,8 @@ export class Browser extends EventEmitter {
|
|
|
250
268
|
});
|
|
251
269
|
return this.address;
|
|
252
270
|
}
|
|
253
|
-
|
|
254
271
|
_handleMessage(data) {
|
|
255
272
|
data = JSON.parse(data);
|
|
256
|
-
|
|
257
273
|
if (data.method === 'Target.attachedToTarget') {
|
|
258
274
|
// create a new session reference when attached to a target
|
|
259
275
|
let session = new Session(this, data);
|
|
@@ -264,7 +280,6 @@ export class Browser extends EventEmitter {
|
|
|
264
280
|
this.sessions.delete(data.params.sessionId);
|
|
265
281
|
session === null || session === void 0 ? void 0 : session._handleClose();
|
|
266
282
|
}
|
|
267
|
-
|
|
268
283
|
if (data.sessionId) {
|
|
269
284
|
// message was for a specific session that sent it
|
|
270
285
|
let session = this.sessions.get(data.sessionId);
|
|
@@ -273,8 +288,8 @@ export class Browser extends EventEmitter {
|
|
|
273
288
|
// resolve or reject a pending promise created with #send()
|
|
274
289
|
let callback = this.#callbacks.get(data.id);
|
|
275
290
|
this.#callbacks.delete(data.id);
|
|
276
|
-
/* istanbul ignore next: races with page._handleMessage() */
|
|
277
291
|
|
|
292
|
+
/* istanbul ignore next: races with page._handleMessage() */
|
|
278
293
|
if (data.error) {
|
|
279
294
|
callback.reject(Object.assign(callback.error, {
|
|
280
295
|
message: `Protocol error (${callback.method}): ${data.error.message}` + ('data' in data.error ? `: ${data.error.data}` : '')
|
|
@@ -287,6 +302,5 @@ export class Browser extends EventEmitter {
|
|
|
287
302
|
this.emit(data.method, data.params);
|
|
288
303
|
}
|
|
289
304
|
}
|
|
290
|
-
|
|
291
305
|
}
|
|
292
306
|
export default Browser;
|
package/dist/config.js
CHANGED
|
@@ -165,8 +165,9 @@ export const configSchema = {
|
|
|
165
165
|
}
|
|
166
166
|
}
|
|
167
167
|
}
|
|
168
|
-
};
|
|
168
|
+
};
|
|
169
169
|
|
|
170
|
+
// Common per-snapshot capture options
|
|
170
171
|
export const snapshotSchema = {
|
|
171
172
|
$id: '/snapshot',
|
|
172
173
|
$ref: '#/$defs/snapshot',
|
|
@@ -482,8 +483,9 @@ export const snapshotSchema = {
|
|
|
482
483
|
}
|
|
483
484
|
}
|
|
484
485
|
}
|
|
485
|
-
};
|
|
486
|
+
};
|
|
486
487
|
|
|
488
|
+
// Comparison upload options
|
|
487
489
|
export const comparisonSchema = {
|
|
488
490
|
type: 'object',
|
|
489
491
|
$id: '/comparison',
|
|
@@ -560,10 +562,12 @@ export const comparisonSchema = {
|
|
|
560
562
|
}
|
|
561
563
|
}
|
|
562
564
|
}
|
|
563
|
-
};
|
|
565
|
+
};
|
|
564
566
|
|
|
565
|
-
|
|
567
|
+
// Grouped schemas for easier registration
|
|
568
|
+
export const schemas = [configSchema, snapshotSchema, comparisonSchema];
|
|
566
569
|
|
|
570
|
+
// Config migrate function
|
|
567
571
|
export function configMigration(config, util) {
|
|
568
572
|
/* eslint-disable curly */
|
|
569
573
|
if (config.version < 2) {
|
|
@@ -581,8 +585,9 @@ export function configMigration(config, util) {
|
|
|
581
585
|
until: '2.0.0'
|
|
582
586
|
});
|
|
583
587
|
}
|
|
584
|
-
}
|
|
588
|
+
}
|
|
585
589
|
|
|
590
|
+
// Snapshot option migrate function
|
|
586
591
|
export function snapshotMigration(config, util, root = '') {
|
|
587
592
|
// discovery options have moved
|
|
588
593
|
util.deprecate(`${root}.devicePixelRatio`, {
|
|
@@ -591,8 +596,9 @@ export function snapshotMigration(config, util, root = '') {
|
|
|
591
596
|
until: '2.0.0',
|
|
592
597
|
warn: true
|
|
593
598
|
});
|
|
594
|
-
}
|
|
599
|
+
}
|
|
595
600
|
|
|
601
|
+
// Snapshot list options migrate function
|
|
596
602
|
export function snapshotListMigration(config, util) {
|
|
597
603
|
if (config.snapshots) {
|
|
598
604
|
// migrate each snapshot options
|
|
@@ -601,9 +607,9 @@ export function snapshotListMigration(config, util) {
|
|
|
601
607
|
snapshotMigration(config, util, `snapshots[${i}]`);
|
|
602
608
|
}
|
|
603
609
|
}
|
|
604
|
-
}
|
|
605
|
-
|
|
610
|
+
}
|
|
606
611
|
|
|
612
|
+
// migrate options
|
|
607
613
|
if (Array.isArray(config.options)) {
|
|
608
614
|
for (let i in config.options) {
|
|
609
615
|
snapshotMigration(config, util, `options[${i}]`);
|
|
@@ -611,8 +617,9 @@ export function snapshotListMigration(config, util) {
|
|
|
611
617
|
} else {
|
|
612
618
|
snapshotMigration(config, util, 'options');
|
|
613
619
|
}
|
|
614
|
-
}
|
|
620
|
+
}
|
|
615
621
|
|
|
622
|
+
// Grouped migrations for easier registration
|
|
616
623
|
export const migrations = {
|
|
617
624
|
'/config': configMigration,
|
|
618
625
|
'/snapshot': snapshotMigration,
|