cypress 15.21.0 → 16.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/angular/README.md +4 -1
- package/angular/angular/README.md +4 -1
- package/angular/angular/dist/index.d.ts +3 -19
- package/angular/angular/dist/index.js +70 -84
- package/angular/angular/package.json +11 -12
- package/angular/dist/index.d.ts +3 -19
- package/angular/dist/index.js +70 -84
- package/angular/package.json +11 -12
- package/dist/bin/cypress +2 -2
- package/dist/{cli-_xePNEPj.js → cli-Dl3GL9Ov.js} +1897 -1930
- package/dist/cli.js +2 -2
- package/dist/{cypress-DCSAOi2D.js → cypress-BfVf5_hP.js} +1 -1
- package/dist/cypress.js +3 -3
- package/dist/exec/spawn.js +1 -1
- package/dist/index.js +3 -3
- package/dist/{spawn-BkpC5qQ4.js → spawn-DpvsyDKg.js} +12 -12
- package/dist/{tap-BkTD6Abc.js → tap-tSM1ve9x.js} +41 -19
- package/mount-utils/dist/index.js +1 -1
- package/mount-utils/mount-utils/dist/index.js +1 -1
- package/mount-utils/mount-utils/package.json +5 -5
- package/mount-utils/package.json +5 -5
- package/package.json +5 -11
- package/react/dist/cypress-react.cjs.js +1 -1
- package/react/dist/cypress-react.esm-bundler.js +1 -1
- package/react/dist/index.d.ts +2 -1
- package/react/package.json +2 -2
- package/react/react/dist/cypress-react.cjs.js +1 -1
- package/react/react/dist/cypress-react.esm-bundler.js +1 -1
- package/react/react/dist/index.d.ts +2 -1
- package/react/react/package.json +2 -2
- package/svelte/dist/cypress-svelte.cjs.js +1 -1
- package/svelte/dist/cypress-svelte.esm-bundler.js +1 -1
- package/svelte/dist/index.d.ts +2 -1
- package/svelte/package.json +1 -1
- package/svelte/svelte/dist/cypress-svelte.cjs.js +1 -1
- package/svelte/svelte/dist/cypress-svelte.esm-bundler.js +1 -1
- package/svelte/svelte/dist/index.d.ts +2 -1
- package/svelte/svelte/package.json +1 -1
- package/types/cy-blob-util.d.ts +1 -1
- package/types/cypress.d.ts +32 -134
- package/vue/dist/cypress-vue.cjs.js +62 -66
- package/vue/dist/cypress-vue.esm-bundler.js +50 -54
- package/vue/dist/index.d.ts +120 -82
- package/vue/package.json +4 -4
- package/vue/vue/dist/cypress-vue.cjs.js +62 -66
- package/vue/vue/dist/cypress-vue.esm-bundler.js +50 -54
- package/vue/vue/dist/index.d.ts +120 -82
- package/vue/vue/package.json +4 -4
- package/angular-zoneless/README.md +0 -15
- package/angular-zoneless/angular-zoneless/README.md +0 -15
- package/angular-zoneless/angular-zoneless/dist/index.d.ts +0 -119
- package/angular-zoneless/angular-zoneless/dist/index.js +0 -482
- package/angular-zoneless/angular-zoneless/package.json +0 -74
- package/angular-zoneless/dist/index.d.ts +0 -119
- package/angular-zoneless/dist/index.js +0 -482
- package/angular-zoneless/package.json +0 -74
|
@@ -13,7 +13,7 @@ var dayjs = require('dayjs');
|
|
|
13
13
|
var relativeTime = require('dayjs/plugin/relativeTime');
|
|
14
14
|
var chalk = require('chalk');
|
|
15
15
|
var Bluebird = require('bluebird');
|
|
16
|
-
var spawn = require('./spawn-
|
|
16
|
+
var spawn = require('./spawn-DpvsyDKg.js');
|
|
17
17
|
var os = require('os');
|
|
18
18
|
var listr2 = require('listr2');
|
|
19
19
|
var timers = require('timers/promises');
|
|
@@ -30,1211 +30,54 @@ var require$$0 = require('stream');
|
|
|
30
30
|
var readline = require('readline');
|
|
31
31
|
var prettyBytes = require('pretty-bytes');
|
|
32
32
|
|
|
33
|
-
const debug$8 = Debug('cypress:cli');
|
|
34
|
-
const defaultBaseUrl = 'https://download.cypress.io/';
|
|
35
|
-
const defaultMaxRedirects = 10;
|
|
36
|
-
const getProxyForUrlWithNpmConfig = (url) => {
|
|
37
|
-
return proxyFromEnv.getProxyForUrl(url) ||
|
|
38
|
-
process.env.npm_config_https_proxy ||
|
|
39
|
-
process.env.npm_config_proxy ||
|
|
40
|
-
null;
|
|
41
|
-
};
|
|
42
|
-
const getBaseUrl = () => {
|
|
43
|
-
if (xvfb.util.getEnv('CYPRESS_DOWNLOAD_MIRROR')) {
|
|
44
|
-
let baseUrl = xvfb.util.getEnv('CYPRESS_DOWNLOAD_MIRROR');
|
|
45
|
-
if (!(baseUrl === null || baseUrl === void 0 ? void 0 : baseUrl.endsWith('/'))) {
|
|
46
|
-
baseUrl += '/';
|
|
47
|
-
}
|
|
48
|
-
return baseUrl || defaultBaseUrl;
|
|
49
|
-
}
|
|
50
|
-
return defaultBaseUrl;
|
|
51
|
-
};
|
|
52
|
-
const getCA = () => xvfb.__awaiter(void 0, void 0, void 0, function* () {
|
|
53
|
-
if (process.env.npm_config_cafile) {
|
|
54
|
-
try {
|
|
55
|
-
const caFileContent = yield fs.readFile(process.env.npm_config_cafile, 'utf8');
|
|
56
|
-
return caFileContent;
|
|
57
|
-
}
|
|
58
|
-
catch (error) {
|
|
59
|
-
debug$8('error reading ca file', error);
|
|
60
|
-
return;
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
if (process.env.npm_config_ca) {
|
|
64
|
-
return process.env.npm_config_ca;
|
|
65
|
-
}
|
|
66
|
-
return;
|
|
67
|
-
});
|
|
68
|
-
const prepend = (arch, urlPath, version) => {
|
|
69
|
-
const endpoint = new URL(urlPath, getBaseUrl()).toString();
|
|
70
|
-
const platform = os.platform();
|
|
71
|
-
const pathTemplate = xvfb.util.getEnv('CYPRESS_DOWNLOAD_PATH_TEMPLATE', true);
|
|
72
|
-
if ((platform === 'win32') && (arch === 'arm64')) {
|
|
73
|
-
debug$8(`detected platform ${platform} architecture ${arch} combination`);
|
|
74
|
-
arch = 'x64';
|
|
75
|
-
debug$8(`overriding to download ${platform}-${arch} instead`);
|
|
76
|
-
}
|
|
77
|
-
return pathTemplate
|
|
78
|
-
? (pathTemplate
|
|
79
|
-
.replace(/\\?\$\{endpoint\}/g, endpoint)
|
|
80
|
-
.replace(/\\?\$\{platform\}/g, platform)
|
|
81
|
-
.replace(/\\?\$\{arch\}/g, arch)
|
|
82
|
-
.replace(/\\?\$\{version\}/g, version))
|
|
83
|
-
: `${endpoint}?platform=${platform}&arch=${arch}`;
|
|
84
|
-
};
|
|
85
|
-
const getUrl = (arch, version) => {
|
|
86
|
-
if (_.isString(version) && version.match(/^https?:\/\/.*$/)) {
|
|
87
|
-
debug$8('version is already an url', version);
|
|
88
|
-
return version;
|
|
89
|
-
}
|
|
90
|
-
const urlPath = version ? `desktop/${version}` : 'desktop';
|
|
91
|
-
return prepend(arch, urlPath, version || '');
|
|
92
|
-
};
|
|
93
|
-
const statusMessage = (err) => {
|
|
94
|
-
return (err.statusCode
|
|
95
|
-
? [err.statusCode, err.statusMessage].join(' - ')
|
|
96
|
-
: err.toString());
|
|
97
|
-
};
|
|
98
|
-
const prettyDownloadErr = (err, url) => {
|
|
99
|
-
const msg = commonTags.stripIndent `
|
|
100
|
-
URL: ${url}
|
|
101
|
-
${statusMessage(err)}
|
|
102
|
-
`;
|
|
103
|
-
debug$8(msg);
|
|
104
|
-
return xvfb.throwFormErrorText(xvfb.errors.failedDownload)(msg);
|
|
105
|
-
};
|
|
106
33
|
/**
|
|
107
|
-
*
|
|
108
|
-
*
|
|
34
|
+
* Get the size of a folder or a file.
|
|
35
|
+
*
|
|
36
|
+
* This function returns the actual file size of the folder (size), not the allocated space on disk (size on disk).
|
|
37
|
+
* For more details between the difference, check this link:
|
|
38
|
+
* https://www.howtogeek.com/180369/why-is-there-a-big-difference-between-size-and-size-on-disk/
|
|
39
|
+
*
|
|
40
|
+
* @param {string} path path to the file or the folder.
|
|
109
41
|
*/
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
42
|
+
function getSize(path$1) {
|
|
43
|
+
return xvfb.__awaiter(this, void 0, void 0, function* () {
|
|
44
|
+
const stat = yield fs.lstat(path$1);
|
|
45
|
+
if (stat.isDirectory()) {
|
|
46
|
+
const list = yield fs.readdir(path$1);
|
|
47
|
+
return Bluebird.resolve(list).reduce((prev, curr) => xvfb.__awaiter(this, void 0, void 0, function* () {
|
|
48
|
+
const currPath = path.join(path$1, curr);
|
|
49
|
+
const s = yield fs.lstat(currPath);
|
|
50
|
+
if (s.isDirectory()) {
|
|
51
|
+
return prev + (yield getSize(currPath));
|
|
52
|
+
}
|
|
53
|
+
return prev + s.size;
|
|
54
|
+
}), 0);
|
|
55
|
+
}
|
|
56
|
+
return stat.size;
|
|
57
|
+
});
|
|
58
|
+
}
|
|
121
59
|
|
|
122
|
-
|
|
123
|
-
Computed checksum: ${checksum}
|
|
60
|
+
var dist = {};
|
|
124
61
|
|
|
125
|
-
|
|
126
|
-
Computed size: ${filesize}
|
|
127
|
-
`;
|
|
128
|
-
debug$8(text);
|
|
129
|
-
throw new Error(text);
|
|
130
|
-
});
|
|
131
|
-
}
|
|
132
|
-
if (expectedChecksum) {
|
|
133
|
-
debug$8('only checking expected file checksum %d', expectedChecksum);
|
|
134
|
-
const checksum = yield xvfb.util.getFileChecksum(filename);
|
|
135
|
-
if (checksum === expectedChecksum) {
|
|
136
|
-
debug$8('downloaded file has the expected checksum ✅');
|
|
137
|
-
return;
|
|
138
|
-
}
|
|
139
|
-
debug$8('raising error: file checksum mismatch');
|
|
140
|
-
const text = commonTags.stripIndent `
|
|
141
|
-
Corrupted download
|
|
62
|
+
var tapContract = {};
|
|
142
63
|
|
|
143
|
-
|
|
144
|
-
Computed checksum: ${checksum}
|
|
145
|
-
`;
|
|
146
|
-
throw new Error(text);
|
|
147
|
-
}
|
|
148
|
-
if (expectedSize) {
|
|
149
|
-
// maybe we don't have a checksum, but at least CDN returns content length
|
|
150
|
-
// which we can check against the file size
|
|
151
|
-
debug$8('only checking expected file size %d', expectedSize);
|
|
152
|
-
const filesize = yield xvfb.util.getFileSize(filename);
|
|
153
|
-
if (filesize === expectedSize) {
|
|
154
|
-
debug$8('downloaded file has the expected size ✅');
|
|
155
|
-
return;
|
|
156
|
-
}
|
|
157
|
-
debug$8('raising error: file size mismatch');
|
|
158
|
-
const text = commonTags.stripIndent `
|
|
159
|
-
Corrupted download
|
|
160
|
-
|
|
161
|
-
Expected downloaded file to have size: ${expectedSize}
|
|
162
|
-
Computed size: ${filesize}
|
|
163
|
-
`;
|
|
164
|
-
throw new Error(text);
|
|
165
|
-
}
|
|
166
|
-
debug$8('downloaded file lacks checksum or size to verify');
|
|
167
|
-
return;
|
|
168
|
-
});
|
|
169
|
-
// downloads from given url
|
|
170
|
-
// return an object with
|
|
171
|
-
// {filename: ..., downloaded: true}
|
|
172
|
-
const downloadFromUrl = ({ url, downloadDestination, progress, ca, version, redirectTTL = defaultMaxRedirects }) => {
|
|
173
|
-
if (redirectTTL <= 0) {
|
|
174
|
-
return Promise.reject(new Error(commonTags.stripIndent `
|
|
175
|
-
Failed downloading the Cypress binary.
|
|
176
|
-
There were too many redirects. The default allowance is ${defaultMaxRedirects}.
|
|
177
|
-
Maybe you got stuck in a redirect loop?
|
|
178
|
-
`));
|
|
179
|
-
}
|
|
180
|
-
return new Bluebird((resolve, reject) => {
|
|
181
|
-
const proxy = getProxyForUrlWithNpmConfig(url);
|
|
182
|
-
debug$8('Downloading package', {
|
|
183
|
-
url,
|
|
184
|
-
proxy,
|
|
185
|
-
downloadDestination,
|
|
186
|
-
});
|
|
187
|
-
if (ca) {
|
|
188
|
-
debug$8('using custom CA details from npm config');
|
|
189
|
-
}
|
|
190
|
-
const reqOptions = Object.assign(Object.assign(Object.assign({ uri: url }, (proxy ? { proxy } : {})), (ca ? { agentOptions: { ca } } : {})), { method: 'GET', followRedirect: false });
|
|
191
|
-
const req = request(reqOptions);
|
|
192
|
-
// closure
|
|
193
|
-
let started = null;
|
|
194
|
-
let expectedSize;
|
|
195
|
-
let expectedChecksum;
|
|
196
|
-
requestProgress(req, {
|
|
197
|
-
throttle: progress.throttle,
|
|
198
|
-
})
|
|
199
|
-
.on('response', (response) => {
|
|
200
|
-
// we have computed checksum and filesize during test runner binary build
|
|
201
|
-
// and have set it on the S3 object as user meta data, available via
|
|
202
|
-
// these custom headers "x-amz-meta-..."
|
|
203
|
-
// see https://github.com/cypress-io/cypress/pull/4092
|
|
204
|
-
expectedSize = response.headers['x-amz-meta-size'] ||
|
|
205
|
-
response.headers['content-length'];
|
|
206
|
-
expectedChecksum = response.headers['x-amz-meta-checksum'];
|
|
207
|
-
if (expectedChecksum) {
|
|
208
|
-
debug$8('expected checksum %s', expectedChecksum);
|
|
209
|
-
}
|
|
210
|
-
if (expectedSize) {
|
|
211
|
-
// convert from string (all Amazon custom headers are strings)
|
|
212
|
-
expectedSize = Number(expectedSize);
|
|
213
|
-
debug$8('expected file size %d', expectedSize);
|
|
214
|
-
}
|
|
215
|
-
// start counting now once we've gotten
|
|
216
|
-
// response headers
|
|
217
|
-
started = new Date();
|
|
218
|
-
if (/^3/.test(response.statusCode)) {
|
|
219
|
-
const redirectVersion = response.headers['x-version'];
|
|
220
|
-
const redirectUrl = response.headers.location;
|
|
221
|
-
debug$8('redirect version:', redirectVersion);
|
|
222
|
-
debug$8('redirect url:', redirectUrl);
|
|
223
|
-
downloadFromUrl({ url: redirectUrl, progress, ca, downloadDestination, version: redirectVersion, redirectTTL: redirectTTL - 1 })
|
|
224
|
-
.then(resolve).catch(reject);
|
|
225
|
-
// if our status code does not start with 200
|
|
226
|
-
}
|
|
227
|
-
else if (!/^2/.test(response.statusCode)) {
|
|
228
|
-
debug$8('response code %d', response.statusCode);
|
|
229
|
-
const err = new Error(commonTags.stripIndent `
|
|
230
|
-
Failed downloading the Cypress binary.
|
|
231
|
-
Response code: ${response.statusCode}
|
|
232
|
-
Response message: ${response.statusMessage}
|
|
233
|
-
`);
|
|
234
|
-
reject(err);
|
|
235
|
-
// status codes here are all 2xx
|
|
236
|
-
}
|
|
237
|
-
else {
|
|
238
|
-
// We only enable this pipe connection when we know we've got a successful return
|
|
239
|
-
// and handle the completion with verify and resolve
|
|
240
|
-
// there was a possible race condition between end of request and close of writeStream
|
|
241
|
-
// that is made ordered with this Promise.all
|
|
242
|
-
Bluebird.all([new Bluebird((r) => {
|
|
243
|
-
return response.pipe(fs.createWriteStream(downloadDestination).on('close', r));
|
|
244
|
-
}), new Bluebird((r) => response.on('end', r))])
|
|
245
|
-
.then(() => {
|
|
246
|
-
debug$8('downloading finished');
|
|
247
|
-
verifyDownloadedFile(downloadDestination, expectedSize, expectedChecksum)
|
|
248
|
-
.then(() => debug$8('verified'))
|
|
249
|
-
.then(() => resolve(version))
|
|
250
|
-
.catch(reject);
|
|
251
|
-
});
|
|
252
|
-
}
|
|
253
|
-
})
|
|
254
|
-
.on('error', (e) => {
|
|
255
|
-
if (e.code === 'ECONNRESET')
|
|
256
|
-
return; // sometimes proxies give ECONNRESET but we don't care
|
|
257
|
-
reject(e);
|
|
258
|
-
})
|
|
259
|
-
.on('progress', (state) => {
|
|
260
|
-
// total time we've elapsed
|
|
261
|
-
// starting on our first progress notification
|
|
262
|
-
const elapsed = +new Date() - +started;
|
|
263
|
-
// request-progress sends a value between 0 and 1
|
|
264
|
-
const percentage = xvfb.util.convertPercentToPercentage(state.percent);
|
|
265
|
-
const eta = xvfb.util.calculateEta(percentage, elapsed);
|
|
266
|
-
// send up our percent and seconds remaining
|
|
267
|
-
progress.onProgress(percentage, xvfb.util.secsRemaining(eta));
|
|
268
|
-
});
|
|
269
|
-
});
|
|
270
|
-
};
|
|
271
|
-
/**
|
|
272
|
-
* Download Cypress.zip from external versionUrl to local file.
|
|
273
|
-
* @param [string] version Could be "3.3.0" or full URL
|
|
274
|
-
* @param [string] downloadDestination Local filename to save as
|
|
275
|
-
*/
|
|
276
|
-
const start$3 = (opts) => xvfb.__awaiter(void 0, void 0, void 0, function* () {
|
|
277
|
-
let { version, downloadDestination, progress, redirectTTL } = opts;
|
|
278
|
-
if (!downloadDestination) {
|
|
279
|
-
assert.ok(_.isString(downloadDestination) && !_.isEmpty(downloadDestination), 'missing download dir');
|
|
280
|
-
}
|
|
281
|
-
if (!progress) {
|
|
282
|
-
progress = { onProgress: () => {
|
|
283
|
-
return {};
|
|
284
|
-
} };
|
|
285
|
-
}
|
|
286
|
-
const arch = yield xvfb.util.getRealArch();
|
|
287
|
-
const versionUrl = getUrl(arch, version);
|
|
288
|
-
progress.throttle = 100;
|
|
289
|
-
debug$8('needed Cypress version: %s', version);
|
|
290
|
-
debug$8('source url %s', versionUrl);
|
|
291
|
-
debug$8(`downloading cypress.zip to "${downloadDestination}"`);
|
|
292
|
-
try {
|
|
293
|
-
// ensure download dir exists
|
|
294
|
-
yield fs.ensureDir(path.dirname(downloadDestination));
|
|
295
|
-
const ca = yield getCA();
|
|
296
|
-
return downloadFromUrl(Object.assign({ url: versionUrl, downloadDestination, progress, ca, version }, (redirectTTL ? { redirectTTL } : {})));
|
|
297
|
-
}
|
|
298
|
-
catch (err) {
|
|
299
|
-
return prettyDownloadErr(err, versionUrl);
|
|
300
|
-
}
|
|
301
|
-
});
|
|
302
|
-
const downloadModule = {
|
|
303
|
-
start: start$3,
|
|
304
|
-
getUrl,
|
|
305
|
-
getProxyForUrlWithNpmConfig,
|
|
306
|
-
getCA,
|
|
307
|
-
};
|
|
308
|
-
|
|
309
|
-
const pipelineAsync = util.promisify(require$$0.pipeline);
|
|
310
|
-
// Unix file mode masks for entries stored in zip's externalFileAttributes
|
|
311
|
-
// (the high 16 bits when the file was zipped on a Unix host).
|
|
312
|
-
const S_IFMT = 0o170000;
|
|
313
|
-
const S_IFDIR = 0o040000;
|
|
314
|
-
const S_IFLNK = 0o120000;
|
|
315
|
-
// PATH_MAX on Linux/macOS is 4096; symlink targets larger than this are not
|
|
316
|
-
// legal filesystem paths and almost certainly indicate a malformed or
|
|
317
|
-
// malicious archive. The cap also prevents reading an arbitrarily large
|
|
318
|
-
// "symlink" entry into memory.
|
|
319
|
-
const MAX_SYMLINK_TARGET_BYTES = 4096;
|
|
320
|
-
/**
|
|
321
|
-
* Extracts the contents of a zip archive into the given destination directory.
|
|
322
|
-
* Recreates directories, regular files, and symlinks while preserving Unix
|
|
323
|
-
* file modes encoded in each entry's external attributes. Calls `onEntry` once
|
|
324
|
-
* per archive entry processed. Refuses entries whose resolved path would
|
|
325
|
-
* escape the destination directory.
|
|
326
|
-
*/
|
|
327
|
-
const extractWithYauzl = (zipFilePath, destDir, onEntry) => xvfb.__awaiter(void 0, void 0, void 0, function* () {
|
|
328
|
-
const resolvedDest = path.resolve(destDir);
|
|
329
|
-
yield new Promise((resolve, reject) => {
|
|
330
|
-
// autoClose: false — `finish` below owns closing the zipfile, so we don't
|
|
331
|
-
// want yauzl's internal end-listener closing it first and tripping a
|
|
332
|
-
// double-close (EBADF) when we do.
|
|
333
|
-
yauzl.open(zipFilePath, { lazyEntries: true, autoClose: false }, (err, zipFile) => {
|
|
334
|
-
if (err) {
|
|
335
|
-
return reject(err);
|
|
336
|
-
}
|
|
337
|
-
// `settled` guards against an in-flight `handleEntry` calling
|
|
338
|
-
// `zipFile.readEntry()` on a now-closed handle when extraction has
|
|
339
|
-
// already failed (e.g. yauzl emitted 'error' while we were writing
|
|
340
|
-
// an entry to disk).
|
|
341
|
-
let settled = false;
|
|
342
|
-
const finish = _.once((err) => {
|
|
343
|
-
var _a, _b;
|
|
344
|
-
settled = true;
|
|
345
|
-
(_a = zipFile.removeAllListeners) === null || _a === void 0 ? void 0 : _a.call(zipFile);
|
|
346
|
-
(_b = zipFile.close) === null || _b === void 0 ? void 0 : _b.call(zipFile);
|
|
347
|
-
if (err) {
|
|
348
|
-
return reject(err);
|
|
349
|
-
}
|
|
350
|
-
return resolve();
|
|
351
|
-
});
|
|
352
|
-
// Normalize any thrown / emitted value into a real Error so that a
|
|
353
|
-
// falsy rejection (e.g. `Promise.reject(undefined)`) doesn't get
|
|
354
|
-
// misread by `finish` as a successful completion.
|
|
355
|
-
const fail = (err) => {
|
|
356
|
-
finish(err instanceof Error ? err : new Error(typeof err === 'string' && err ? err : 'zip extraction failed'));
|
|
357
|
-
};
|
|
358
|
-
zipFile.on('error', fail);
|
|
359
|
-
zipFile.on('end', () => finish());
|
|
360
|
-
zipFile.on('entry', (entry) => {
|
|
361
|
-
handleEntry(zipFile, entry, resolvedDest)
|
|
362
|
-
.then(() => {
|
|
363
|
-
if (settled)
|
|
364
|
-
return;
|
|
365
|
-
onEntry();
|
|
366
|
-
zipFile.readEntry();
|
|
367
|
-
})
|
|
368
|
-
.catch(fail);
|
|
369
|
-
});
|
|
370
|
-
zipFile.readEntry();
|
|
371
|
-
});
|
|
372
|
-
});
|
|
373
|
-
});
|
|
374
|
-
const handleEntry = (zipFile, entry, resolvedDest) => xvfb.__awaiter(void 0, void 0, void 0, function* () {
|
|
375
|
-
const fileDest = path.resolve(resolvedDest, entry.fileName);
|
|
376
|
-
// refuse anything that would write outside the install dir
|
|
377
|
-
if (fileDest !== resolvedDest &&
|
|
378
|
-
!fileDest.startsWith(resolvedDest + path.sep)) {
|
|
379
|
-
throw new Error(`Refusing to extract entry outside of destination: ${entry.fileName}`);
|
|
380
|
-
}
|
|
381
|
-
const unixMode = (entry.externalFileAttributes >>> 16) & 0xffff;
|
|
382
|
-
// Some archivers mark directories by Unix mode bits instead of (or in
|
|
383
|
-
// addition to) a trailing slash; honor both so we don't extract a
|
|
384
|
-
// directory entry as a zero-byte file.
|
|
385
|
-
const isDir = /\/$/.test(entry.fileName) || (unixMode & S_IFMT) === S_IFDIR;
|
|
386
|
-
const isSymlink = (unixMode & S_IFMT) === S_IFLNK;
|
|
387
|
-
if (isDir) {
|
|
388
|
-
yield fsp.mkdir(fileDest, { recursive: true });
|
|
389
|
-
return;
|
|
390
|
-
}
|
|
391
|
-
yield fsp.mkdir(path.dirname(fileDest), { recursive: true });
|
|
392
|
-
if (isSymlink) {
|
|
393
|
-
if (entry.uncompressedSize > MAX_SYMLINK_TARGET_BYTES) {
|
|
394
|
-
throw new Error(`Refusing to extract symlink with target larger than ${MAX_SYMLINK_TARGET_BYTES} bytes: ${entry.fileName}`);
|
|
395
|
-
}
|
|
396
|
-
const linkTarget = yield readEntryAsString(zipFile, entry, MAX_SYMLINK_TARGET_BYTES);
|
|
397
|
-
const resolvedTarget = path.resolve(path.dirname(fileDest), linkTarget);
|
|
398
|
-
if (resolvedTarget !== resolvedDest &&
|
|
399
|
-
!resolvedTarget.startsWith(resolvedDest + path.sep)) {
|
|
400
|
-
throw new Error(`Refusing to extract symlink pointing outside of destination: ${entry.fileName} -> ${linkTarget}`);
|
|
401
|
-
}
|
|
402
|
-
yield fsp.rm(fileDest, { recursive: true, force: true });
|
|
403
|
-
yield fsp.symlink(linkTarget, fileDest);
|
|
404
|
-
return;
|
|
405
|
-
}
|
|
406
|
-
const readStream = yield new Promise((res, rej) => {
|
|
407
|
-
zipFile.openReadStream(entry, (err, rs) => {
|
|
408
|
-
if (err) {
|
|
409
|
-
return rej(err);
|
|
410
|
-
}
|
|
411
|
-
return res(rs);
|
|
412
|
-
});
|
|
413
|
-
});
|
|
414
|
-
// Preserve the Unix mode bits when present; otherwise fall back to a sane default.
|
|
415
|
-
const fileMode = (unixMode & 0o7777) || 0o644;
|
|
416
|
-
const writeStream = fs$1.createWriteStream(fileDest, { mode: fileMode });
|
|
417
|
-
yield pipelineAsync(readStream, writeStream);
|
|
418
|
-
});
|
|
419
|
-
const readEntryAsString = (zipFile, entry, maxBytes) => {
|
|
420
|
-
return new Promise((resolve, reject) => {
|
|
421
|
-
zipFile.openReadStream(entry, (err, rs) => {
|
|
422
|
-
if (err) {
|
|
423
|
-
return reject(err);
|
|
424
|
-
}
|
|
425
|
-
const chunks = [];
|
|
426
|
-
let received = 0;
|
|
427
|
-
let bailed = false;
|
|
428
|
-
const bail = (err) => {
|
|
429
|
-
var _a;
|
|
430
|
-
if (bailed)
|
|
431
|
-
return;
|
|
432
|
-
bailed = true;
|
|
433
|
-
(_a = rs.destroy) === null || _a === void 0 ? void 0 : _a.call(rs, err);
|
|
434
|
-
reject(err);
|
|
435
|
-
};
|
|
436
|
-
rs.on('data', (chunk) => {
|
|
437
|
-
received += chunk.length;
|
|
438
|
-
if (received > maxBytes) {
|
|
439
|
-
bail(new Error(`Refusing to read entry body larger than ${maxBytes} bytes: ${entry.fileName}`));
|
|
440
|
-
return;
|
|
441
|
-
}
|
|
442
|
-
chunks.push(chunk);
|
|
443
|
-
});
|
|
444
|
-
rs.on('end', () => {
|
|
445
|
-
if (bailed)
|
|
446
|
-
return;
|
|
447
|
-
resolve(Buffer.concat(chunks).toString('utf8'));
|
|
448
|
-
});
|
|
449
|
-
rs.on('error', bail);
|
|
450
|
-
});
|
|
451
|
-
});
|
|
452
|
-
};
|
|
453
|
-
|
|
454
|
-
const debug$7 = Debug('cypress:cli:unzip');
|
|
455
|
-
const unzipTools = {
|
|
456
|
-
extractWithYauzl,
|
|
457
|
-
};
|
|
458
|
-
// expose this function for simple testing
|
|
459
|
-
const unzip = (_a) => xvfb.__awaiter(void 0, [_a], void 0, function* ({ zipFilePath, installDir, progress }) {
|
|
460
|
-
debug$7('unzipping from %s', zipFilePath);
|
|
461
|
-
debug$7('into', installDir);
|
|
462
|
-
if (!zipFilePath) {
|
|
463
|
-
throw new Error('Missing zip filename');
|
|
464
|
-
}
|
|
465
|
-
const startTime = Date.now();
|
|
466
|
-
let yauzlDoneTime = 0;
|
|
467
|
-
yield fs.ensureDir(installDir);
|
|
468
|
-
yield new Promise((resolve, reject) => {
|
|
469
|
-
// Open with lazyEntries so yauzl doesn't auto-emit entries (which would
|
|
470
|
-
// require the fd to stay open for the duration of the OS-tool extraction).
|
|
471
|
-
// We only need the entryCount here for the progress calculation.
|
|
472
|
-
return yauzl.open(zipFilePath, { lazyEntries: true }, (err, zipFile) => {
|
|
473
|
-
yauzlDoneTime = Date.now();
|
|
474
|
-
if (err) {
|
|
475
|
-
debug$7('error using yauzl %s', err.message);
|
|
476
|
-
return reject(err);
|
|
477
|
-
}
|
|
478
|
-
const total = zipFile.entryCount;
|
|
479
|
-
debug$7('zipFile entries count', total);
|
|
480
|
-
// Close the count-only handle — the Node fallback re-opens the zip for extraction.
|
|
481
|
-
zipFile.close();
|
|
482
|
-
const started = new Date();
|
|
483
|
-
let percent = 0;
|
|
484
|
-
let count = 0;
|
|
485
|
-
const notify = (percent) => {
|
|
486
|
-
const elapsed = +new Date() - +started;
|
|
487
|
-
const eta = xvfb.util.calculateEta(percent, elapsed);
|
|
488
|
-
progress.onProgress(percent, xvfb.util.secsRemaining(eta));
|
|
489
|
-
};
|
|
490
|
-
const tick = () => {
|
|
491
|
-
count += 1;
|
|
492
|
-
percent = ((count / total) * 100);
|
|
493
|
-
const displayPercent = percent.toFixed(0);
|
|
494
|
-
return notify(Number(displayPercent));
|
|
495
|
-
};
|
|
496
|
-
const unzipWithNode = () => xvfb.__awaiter(void 0, void 0, void 0, function* () {
|
|
497
|
-
debug$7('unzipping with node.js (slow)');
|
|
498
|
-
try {
|
|
499
|
-
yield unzipTools.extractWithYauzl(zipFilePath, installDir, tick);
|
|
500
|
-
debug$7('node unzip finished');
|
|
501
|
-
return resolve();
|
|
502
|
-
}
|
|
503
|
-
catch (err) {
|
|
504
|
-
const error = err || new Error('Unknown error with Node extract tool');
|
|
505
|
-
debug$7('error %s', error.message);
|
|
506
|
-
return reject(error);
|
|
507
|
-
}
|
|
508
|
-
});
|
|
509
|
-
const unzipFallback = _.once(unzipWithNode);
|
|
510
|
-
const unzipWithUnzipTool = () => {
|
|
511
|
-
debug$7('unzipping via `unzip`');
|
|
512
|
-
const inflatingRe = /inflating:/;
|
|
513
|
-
const sp = cp.spawn('unzip', ['-o', zipFilePath, '-d', installDir]);
|
|
514
|
-
sp.on('error', (err) => {
|
|
515
|
-
debug$7('unzip tool error: %s', err.message);
|
|
516
|
-
unzipFallback();
|
|
517
|
-
});
|
|
518
|
-
sp.on('close', (code) => {
|
|
519
|
-
debug$7('unzip tool close with code %d', code);
|
|
520
|
-
if (code === 0) {
|
|
521
|
-
percent = 100;
|
|
522
|
-
notify(percent);
|
|
523
|
-
return resolve();
|
|
524
|
-
}
|
|
525
|
-
debug$7('`unzip` failed %o', { code });
|
|
526
|
-
return unzipFallback();
|
|
527
|
-
});
|
|
528
|
-
sp.stdout.on('data', (data) => {
|
|
529
|
-
if (inflatingRe.test(data)) {
|
|
530
|
-
return tick();
|
|
531
|
-
}
|
|
532
|
-
});
|
|
533
|
-
sp.stderr.on('data', (data) => {
|
|
534
|
-
debug$7('`unzip` stderr %s', data);
|
|
535
|
-
});
|
|
536
|
-
};
|
|
537
|
-
// we attempt to first unzip with the native osx
|
|
538
|
-
// ditto because its less likely to have problems
|
|
539
|
-
// with corruption, symlinks, or icons causing failures
|
|
540
|
-
// and can handle resource forks
|
|
541
|
-
// http://automatica.com.au/2011/02/unzip-mac-os-x-zip-in-terminal/
|
|
542
|
-
const unzipWithOsx = () => {
|
|
543
|
-
debug$7('unzipping via `ditto`');
|
|
544
|
-
const copyingFileRe = /^copying file/;
|
|
545
|
-
const sp = cp.spawn('ditto', ['-xkV', zipFilePath, installDir]);
|
|
546
|
-
// f-it just unzip with node
|
|
547
|
-
sp.on('error', (err) => {
|
|
548
|
-
debug$7(err.message);
|
|
549
|
-
unzipFallback();
|
|
550
|
-
});
|
|
551
|
-
sp.on('close', (code) => {
|
|
552
|
-
if (code === 0) {
|
|
553
|
-
// make sure we get to 100% on the progress bar
|
|
554
|
-
// because reading in lines is not really accurate
|
|
555
|
-
percent = 100;
|
|
556
|
-
notify(percent);
|
|
557
|
-
return resolve();
|
|
558
|
-
}
|
|
559
|
-
debug$7('`ditto` failed %o', { code });
|
|
560
|
-
return unzipFallback();
|
|
561
|
-
});
|
|
562
|
-
return readline.createInterface({
|
|
563
|
-
input: sp.stderr,
|
|
564
|
-
})
|
|
565
|
-
.on('line', (line) => {
|
|
566
|
-
if (copyingFileRe.test(line)) {
|
|
567
|
-
return tick();
|
|
568
|
-
}
|
|
569
|
-
});
|
|
570
|
-
};
|
|
571
|
-
switch (os.platform()) {
|
|
572
|
-
case 'darwin':
|
|
573
|
-
return unzipWithOsx();
|
|
574
|
-
case 'linux':
|
|
575
|
-
return unzipWithUnzipTool();
|
|
576
|
-
case 'win32':
|
|
577
|
-
return unzipWithNode();
|
|
578
|
-
default:
|
|
579
|
-
return;
|
|
580
|
-
}
|
|
581
|
-
});
|
|
582
|
-
});
|
|
583
|
-
debug$7('unzip completed %o', {
|
|
584
|
-
yauzlMs: yauzlDoneTime - startTime,
|
|
585
|
-
unzipMs: Date.now() - yauzlDoneTime,
|
|
586
|
-
});
|
|
587
|
-
});
|
|
588
|
-
function isMaybeWindowsMaxPathLengthError(err) {
|
|
589
|
-
return os.platform() === 'win32' && err.code === 'ENOENT' && err.syscall === 'realpath';
|
|
590
|
-
}
|
|
591
|
-
const start$2 = (_a) => xvfb.__awaiter(void 0, [_a], void 0, function* ({ zipFilePath, installDir, progress }) {
|
|
592
|
-
assert.ok(_.isString(installDir) && !_.isEmpty(installDir), 'missing installDir');
|
|
593
|
-
if (!progress) {
|
|
594
|
-
progress = { onProgress: () => {
|
|
595
|
-
return {};
|
|
596
|
-
} };
|
|
597
|
-
}
|
|
598
|
-
try {
|
|
599
|
-
const installDirExists = yield fs.pathExists(installDir);
|
|
600
|
-
if (installDirExists) {
|
|
601
|
-
debug$7('removing existing unzipped binary', installDir);
|
|
602
|
-
yield fs.remove(installDir);
|
|
603
|
-
}
|
|
604
|
-
yield unzip({ zipFilePath, installDir, progress });
|
|
605
|
-
}
|
|
606
|
-
catch (err) {
|
|
607
|
-
const errorTemplate = isMaybeWindowsMaxPathLengthError(err) ?
|
|
608
|
-
xvfb.errors.failedUnzipWindowsMaxPathLength
|
|
609
|
-
: xvfb.errors.failedUnzip;
|
|
610
|
-
yield xvfb.throwFormErrorText(errorTemplate)(err);
|
|
611
|
-
}
|
|
612
|
-
});
|
|
613
|
-
const unzipModule = {
|
|
614
|
-
start: start$2,
|
|
615
|
-
utils: {
|
|
616
|
-
unzip,
|
|
617
|
-
unzipTools,
|
|
618
|
-
},
|
|
619
|
-
};
|
|
620
|
-
|
|
621
|
-
const debug$6 = Debug('cypress:cli:install');
|
|
622
|
-
function _getBinaryUrlFromBuildInfo(version, arch, { commitSha, commitBranch }) {
|
|
623
|
-
const platform = os.platform();
|
|
624
|
-
if ((platform === 'win32') && (arch === 'arm64')) {
|
|
625
|
-
debug$6(`detected platform ${platform} architecture ${arch} combination`);
|
|
626
|
-
arch = 'x64';
|
|
627
|
-
debug$6(`overriding to download ${platform}-${arch} pre-release binary instead`);
|
|
628
|
-
}
|
|
629
|
-
return `https://cdn.cypress.io/beta/binary/${version}/${platform}-${arch}/${commitBranch}-${commitSha}/cypress.zip`;
|
|
630
|
-
}
|
|
631
|
-
const alreadyInstalledMsg = () => {
|
|
632
|
-
if (!xvfb.util.isPostInstall()) {
|
|
633
|
-
xvfb.loggerModule.log(commonTags.stripIndent `
|
|
634
|
-
Skipping installation:
|
|
635
|
-
|
|
636
|
-
Pass the ${chalk.yellow('--force')} option if you'd like to reinstall anyway.
|
|
637
|
-
`);
|
|
638
|
-
}
|
|
639
|
-
};
|
|
640
|
-
const displayCompletionMsg = () => {
|
|
641
|
-
xvfb.loggerModule.log();
|
|
642
|
-
xvfb.loggerModule.log('You can now open Cypress by running one of the following, depending on your package manager:');
|
|
643
|
-
xvfb.loggerModule.log();
|
|
644
|
-
xvfb.loggerModule.log(chalk.cyan('- npx cypress open'));
|
|
645
|
-
xvfb.loggerModule.log(chalk.cyan('- yarn cypress open'));
|
|
646
|
-
xvfb.loggerModule.log(chalk.cyan('- pnpm cypress open'));
|
|
647
|
-
xvfb.loggerModule.log();
|
|
648
|
-
xvfb.loggerModule.log(chalk.grey('https://on.cypress.io/opening-the-app'));
|
|
649
|
-
xvfb.loggerModule.log();
|
|
650
|
-
};
|
|
651
|
-
const validateOS = () => xvfb.__awaiter(void 0, void 0, void 0, function* () {
|
|
652
|
-
const platformInfo = yield xvfb.util.getPlatformInfo();
|
|
653
|
-
return platformInfo.match(/(win32-x64|win32-arm64|linux-x64|linux-arm64|darwin-x64|darwin-arm64)/);
|
|
654
|
-
});
|
|
655
|
-
/**
|
|
656
|
-
* Returns the version to install - either a string like `1.2.3` to be fetched
|
|
657
|
-
* from the download server or a file path or HTTP URL.
|
|
658
|
-
*/
|
|
659
|
-
function getVersionOverride(version, { arch, envVarVersion, buildInfo }) {
|
|
660
|
-
// let this environment variable reset the binary version we need
|
|
661
|
-
if (envVarVersion) {
|
|
662
|
-
return envVarVersion;
|
|
663
|
-
}
|
|
664
|
-
if (buildInfo && !buildInfo.stable) {
|
|
665
|
-
xvfb.loggerModule.log(chalk.yellow(commonTags.stripIndent `
|
|
666
|
-
${logSymbols.warning} Warning: You are installing a pre-release build of Cypress.
|
|
667
|
-
|
|
668
|
-
Bugs may be present which do not exist in production builds.
|
|
669
|
-
|
|
670
|
-
This build was created from:
|
|
671
|
-
* Commit SHA: ${buildInfo.commitSha}
|
|
672
|
-
* Commit Branch: ${buildInfo.commitBranch}
|
|
673
|
-
* Commit Timestamp: ${buildInfo.commitDate}
|
|
674
|
-
`));
|
|
675
|
-
xvfb.loggerModule.log();
|
|
676
|
-
return _getBinaryUrlFromBuildInfo(version, arch, buildInfo);
|
|
677
|
-
}
|
|
678
|
-
}
|
|
679
|
-
function getEnvVarVersion() {
|
|
680
|
-
if (!xvfb.util.getEnv('CYPRESS_INSTALL_BINARY'))
|
|
681
|
-
return;
|
|
682
|
-
// because passed file paths are often double quoted
|
|
683
|
-
// and might have extra whitespace around, be robust and trim the string
|
|
684
|
-
const trimAndRemoveDoubleQuotes = true;
|
|
685
|
-
const envVarVersion = xvfb.util.getEnv('CYPRESS_INSTALL_BINARY', trimAndRemoveDoubleQuotes);
|
|
686
|
-
debug$6('using environment variable CYPRESS_INSTALL_BINARY "%s"', envVarVersion);
|
|
687
|
-
return envVarVersion;
|
|
688
|
-
}
|
|
689
|
-
const start$1 = (...args_1) => xvfb.__awaiter(void 0, [...args_1], void 0, function* (options = {}) {
|
|
690
|
-
debug$6('installing with options %j', options);
|
|
691
|
-
const envVarVersion = getEnvVarVersion();
|
|
692
|
-
if (envVarVersion === '0') {
|
|
693
|
-
debug$6('environment variable CYPRESS_INSTALL_BINARY = 0, skipping install');
|
|
694
|
-
xvfb.loggerModule.log(commonTags.stripIndent `
|
|
695
|
-
${chalk.yellow('Note:')} Skipping binary installation: Environment variable CYPRESS_INSTALL_BINARY = 0.`);
|
|
696
|
-
xvfb.loggerModule.log();
|
|
697
|
-
return;
|
|
698
|
-
}
|
|
699
|
-
const pkgPath = xvfb.relativeToRepoRoot('package.json');
|
|
700
|
-
if (!pkgPath) {
|
|
701
|
-
return xvfb.throwFormErrorText('Could not find package.json for Cypress package to determine build information')();
|
|
702
|
-
}
|
|
703
|
-
const { buildInfo, version } = JSON.parse(yield fsp.readFile(pkgPath, 'utf8'));
|
|
704
|
-
_.defaults(options, {
|
|
705
|
-
force: false,
|
|
706
|
-
buildInfo,
|
|
707
|
-
});
|
|
708
|
-
if (xvfb.util.getEnv('CYPRESS_CACHE_FOLDER')) {
|
|
709
|
-
const envCache = xvfb.util.getEnv('CYPRESS_CACHE_FOLDER');
|
|
710
|
-
xvfb.loggerModule.log(commonTags.stripIndent `
|
|
711
|
-
${chalk.yellow('Note:')} Overriding Cypress cache directory to: ${chalk.cyan(envCache)}
|
|
712
|
-
|
|
713
|
-
Previous installs of Cypress may not be found.
|
|
714
|
-
`);
|
|
715
|
-
xvfb.loggerModule.log();
|
|
716
|
-
}
|
|
717
|
-
const pkgVersion = xvfb.util.pkgVersion();
|
|
718
|
-
const arch = yield xvfb.util.getRealArch();
|
|
719
|
-
const versionOverride = getVersionOverride(version, { arch, envVarVersion, buildInfo: options.buildInfo });
|
|
720
|
-
const versionToInstall = versionOverride || pkgVersion;
|
|
721
|
-
debug$6('version in package.json is %s, version to install is %s', pkgVersion, versionToInstall);
|
|
722
|
-
const installDir = xvfb.stateModule.getVersionDir(pkgVersion, options.buildInfo);
|
|
723
|
-
const cacheDir = xvfb.stateModule.getCacheDir();
|
|
724
|
-
const binaryDir = xvfb.stateModule.getBinaryDir(pkgVersion);
|
|
725
|
-
if (!(yield validateOS())) {
|
|
726
|
-
return xvfb.throwFormErrorText(xvfb.errors.invalidOS)();
|
|
727
|
-
}
|
|
728
|
-
try {
|
|
729
|
-
yield fs.ensureDir(cacheDir);
|
|
730
|
-
}
|
|
731
|
-
catch (err) {
|
|
732
|
-
if (err instanceof Error && 'code' in err && err.code === 'EACCES') {
|
|
733
|
-
return xvfb.throwFormErrorText(xvfb.errors.invalidCacheDirectory)(commonTags.stripIndent `
|
|
734
|
-
Failed to access ${chalk.cyan(cacheDir)}:
|
|
735
|
-
|
|
736
|
-
${err.message}
|
|
737
|
-
`);
|
|
738
|
-
}
|
|
739
|
-
else {
|
|
740
|
-
throw err;
|
|
741
|
-
}
|
|
742
|
-
}
|
|
743
|
-
const binaryPkg = yield xvfb.stateModule.getBinaryPkgAsync(binaryDir);
|
|
744
|
-
const binaryVersion = yield xvfb.stateModule.getBinaryPkgVersion(binaryPkg);
|
|
745
|
-
const shouldInstall = () => {
|
|
746
|
-
if (!binaryVersion) {
|
|
747
|
-
debug$6('no binary installed under cli version');
|
|
748
|
-
return true;
|
|
749
|
-
}
|
|
750
|
-
xvfb.loggerModule.log();
|
|
751
|
-
xvfb.loggerModule.log(commonTags.stripIndent `
|
|
752
|
-
Cypress ${chalk.green(binaryVersion)} is installed in ${chalk.cyan(installDir)}
|
|
753
|
-
`);
|
|
754
|
-
xvfb.loggerModule.log();
|
|
755
|
-
if (options.force) {
|
|
756
|
-
debug$6('performing force install over existing binary');
|
|
757
|
-
return true;
|
|
758
|
-
}
|
|
759
|
-
if ((binaryVersion === versionToInstall) || !xvfb.util.isSemver(versionToInstall)) {
|
|
760
|
-
// our version matches, tell the user this is a noop
|
|
761
|
-
alreadyInstalledMsg();
|
|
762
|
-
return false;
|
|
763
|
-
}
|
|
764
|
-
return true;
|
|
765
|
-
};
|
|
766
|
-
// noop if we've been told not to download
|
|
767
|
-
if (!shouldInstall()) {
|
|
768
|
-
return debug$6('Not downloading or installing binary');
|
|
769
|
-
}
|
|
770
|
-
if (envVarVersion) {
|
|
771
|
-
xvfb.loggerModule.log(chalk.yellow(commonTags.stripIndent `
|
|
772
|
-
${logSymbols.warning} Warning: Forcing a binary version different than the default.
|
|
773
|
-
|
|
774
|
-
The CLI expected to install version: ${chalk.green(pkgVersion)}
|
|
775
|
-
|
|
776
|
-
Instead we will install version: ${chalk.green(versionToInstall)}
|
|
777
|
-
|
|
778
|
-
These versions may not work properly together.
|
|
779
|
-
`));
|
|
780
|
-
xvfb.loggerModule.log();
|
|
781
|
-
}
|
|
782
|
-
const getLocalFilePath = () => xvfb.__awaiter(void 0, void 0, void 0, function* () {
|
|
783
|
-
// see if version supplied is a path to a binary
|
|
784
|
-
if (yield fs.pathExists(versionToInstall)) {
|
|
785
|
-
return path.extname(versionToInstall) === '.zip' ? versionToInstall : false;
|
|
786
|
-
}
|
|
787
|
-
const possibleFile = xvfb.util.formAbsolutePath(versionToInstall);
|
|
788
|
-
debug$6('checking local file', possibleFile, 'cwd', process.cwd());
|
|
789
|
-
// if this exists return the path to it
|
|
790
|
-
// else false
|
|
791
|
-
if ((yield fs.pathExists(possibleFile)) && path.extname(possibleFile) === '.zip') {
|
|
792
|
-
return possibleFile;
|
|
793
|
-
}
|
|
794
|
-
return false;
|
|
795
|
-
});
|
|
796
|
-
const pathToLocalFile = yield getLocalFilePath();
|
|
797
|
-
const tasks = pathToLocalFile ?
|
|
798
|
-
installFromLocal(pathToLocalFile, installDir) :
|
|
799
|
-
installFromRemote(versionToInstall, installDir);
|
|
800
|
-
if (options.force) {
|
|
801
|
-
debug$6('Cypress already installed at', installDir);
|
|
802
|
-
debug$6('but the installation was forced');
|
|
803
|
-
}
|
|
804
|
-
// let the user know what version of cypress we're downloading!
|
|
805
|
-
xvfb.loggerModule.log(`Installing Cypress ${chalk.gray(`(version: ${versionToInstall})`)}`);
|
|
806
|
-
xvfb.loggerModule.log();
|
|
807
|
-
const taskRunner = new listr2.Listr(tasks, Object.assign(Object.assign({
|
|
808
|
-
// In CI we want timestamped, line-per-event output. Locally,
|
|
809
|
-
// the default in-place spinner is the better experience.
|
|
810
|
-
renderer: xvfb.util.isCi() ? 'verbose' : 'default' }, (xvfb.util.isCi() && { rendererOptions: { timestamp: listr2.PRESET_TIMESTAMP } })), { silentRendererCondition: () => xvfb.loggerModule.logLevel() === 'silent' }));
|
|
811
|
-
yield taskRunner.run();
|
|
812
|
-
// delay 1 sec for UX, unless we are testing
|
|
813
|
-
yield timers.setTimeout(1000);
|
|
814
|
-
displayCompletionMsg();
|
|
815
|
-
});
|
|
816
|
-
function downloadArchive(version, downloadDestination) {
|
|
817
|
-
const inProgressTitle = 'Downloading Cypress';
|
|
818
|
-
const completedTitle = chalk.green('Downloaded Cypress');
|
|
819
|
-
return {
|
|
820
|
-
title: xvfb.util.titleize(inProgressTitle),
|
|
821
|
-
task: (ctx, task) => xvfb.__awaiter(this, void 0, void 0, function* () {
|
|
822
|
-
yield downloadModule.start({
|
|
823
|
-
version,
|
|
824
|
-
downloadDestination,
|
|
825
|
-
progress: {
|
|
826
|
-
throttle: 100,
|
|
827
|
-
onProgress: (percentComplete, remaining) => {
|
|
828
|
-
task.title = progressTitle(inProgressTitle, percentComplete, remaining);
|
|
829
|
-
},
|
|
830
|
-
},
|
|
831
|
-
});
|
|
832
|
-
debug$6(`finished downloading file: ${downloadDestination}`);
|
|
833
|
-
task.title = xvfb.util.titleize(completedTitle);
|
|
834
|
-
}),
|
|
835
|
-
};
|
|
836
|
-
}
|
|
837
|
-
function installFromLocal(pathToLocalFile, installDir) {
|
|
838
|
-
const zipFilePath = path.resolve(pathToLocalFile);
|
|
839
|
-
debug$6('found local file at', zipFilePath);
|
|
840
|
-
debug$6('skipping download');
|
|
841
|
-
return [
|
|
842
|
-
unzipArchive(zipFilePath, installDir),
|
|
843
|
-
];
|
|
844
|
-
}
|
|
845
|
-
function installFromRemote(version, installDir) {
|
|
846
|
-
const downloadDestination = path.join(os.tmpdir(), `cypress-${process.pid}.zip`);
|
|
847
|
-
debug$6('preparing to download and unzip version ', version, 'to path', installDir);
|
|
848
|
-
return [
|
|
849
|
-
downloadArchive(version, downloadDestination),
|
|
850
|
-
unzipArchive(downloadDestination, installDir),
|
|
851
|
-
cleanup(downloadDestination, installDir),
|
|
852
|
-
];
|
|
853
|
-
}
|
|
854
|
-
function unzipArchive(zipFilePath, installDir) {
|
|
855
|
-
const inProgressTitle = 'Unzipping Cypress';
|
|
856
|
-
const completedTitle = chalk.green('Unzipped Cypress');
|
|
857
|
-
return {
|
|
858
|
-
title: xvfb.util.titleize(inProgressTitle),
|
|
859
|
-
task: (ctx, task) => xvfb.__awaiter(this, void 0, void 0, function* () {
|
|
860
|
-
yield unzipModule.start({
|
|
861
|
-
zipFilePath,
|
|
862
|
-
installDir,
|
|
863
|
-
progress: {
|
|
864
|
-
onProgress: (percentComplete, remaining) => {
|
|
865
|
-
task.title = progressTitle(inProgressTitle, percentComplete, remaining);
|
|
866
|
-
},
|
|
867
|
-
},
|
|
868
|
-
});
|
|
869
|
-
task.title = xvfb.util.titleize(completedTitle);
|
|
870
|
-
}),
|
|
871
|
-
};
|
|
872
|
-
}
|
|
873
|
-
function cleanup(archiveLocation, installDir) {
|
|
874
|
-
return {
|
|
875
|
-
title: xvfb.util.titleize('Finishing Installation'),
|
|
876
|
-
task: (ctx, task) => xvfb.__awaiter(this, void 0, void 0, function* () {
|
|
877
|
-
debug$6('removing zip file %s', archiveLocation);
|
|
878
|
-
yield fs.remove(archiveLocation);
|
|
879
|
-
debug$6('finished installation in', installDir);
|
|
880
|
-
task.title = xvfb.util.titleize(chalk.green('Finished Installation'), chalk.gray(installDir));
|
|
881
|
-
}),
|
|
882
|
-
};
|
|
883
|
-
}
|
|
884
|
-
function progressTitle(title, percentComplete, remaining) {
|
|
885
|
-
return xvfb.util.titleize(title, chalk.white(` ${percentComplete}%`), chalk.gray(`${remaining}s`));
|
|
886
|
-
}
|
|
887
|
-
var installModule = {
|
|
888
|
-
start: start$1,
|
|
889
|
-
_getBinaryUrlFromBuildInfo,
|
|
890
|
-
};
|
|
891
|
-
|
|
892
|
-
/**
|
|
893
|
-
* Throws an error with "details" property from
|
|
894
|
-
* "errors" object.
|
|
895
|
-
* @param {Object} details - Error details
|
|
896
|
-
*/
|
|
897
|
-
const throwInvalidOptionError = (details) => {
|
|
898
|
-
if (!details) {
|
|
899
|
-
details = xvfb.errors.unknownError;
|
|
900
|
-
}
|
|
901
|
-
// throw this error synchronously, it will be caught later on and
|
|
902
|
-
// the details will be propagated to the promise chain
|
|
903
|
-
const err = new Error();
|
|
904
|
-
err.details = details;
|
|
905
|
-
throw err;
|
|
906
|
-
};
|
|
907
|
-
/**
|
|
908
|
-
* Selects exec args based on the configured `testingType`
|
|
909
|
-
* @param {string} testingType The type of tests being executed
|
|
910
|
-
* @returns {string[]} The array of new exec arguments
|
|
911
|
-
*/
|
|
912
|
-
const processTestingType = (options) => {
|
|
913
|
-
if (options.e2e && options.component) {
|
|
914
|
-
return throwInvalidOptionError(xvfb.errors.incompatibleTestTypeFlags);
|
|
915
|
-
}
|
|
916
|
-
if (options.testingType && (options.component || options.e2e)) {
|
|
917
|
-
return throwInvalidOptionError(xvfb.errors.incompatibleTestTypeFlags);
|
|
918
|
-
}
|
|
919
|
-
if (options.testingType === 'component' || options.component || options.ct) {
|
|
920
|
-
return ['--testing-type', 'component'];
|
|
921
|
-
}
|
|
922
|
-
if (options.testingType === 'e2e' || options.e2e) {
|
|
923
|
-
return ['--testing-type', 'e2e'];
|
|
924
|
-
}
|
|
925
|
-
if (options.testingType) {
|
|
926
|
-
return throwInvalidOptionError(xvfb.errors.invalidTestingType);
|
|
927
|
-
}
|
|
928
|
-
return [];
|
|
929
|
-
};
|
|
930
|
-
/**
|
|
931
|
-
* Throws an error if configFile is string 'false' or boolean false
|
|
932
|
-
* @param {*} options
|
|
933
|
-
*/
|
|
934
|
-
const checkConfigFile = (options) => {
|
|
935
|
-
// CLI will parse as string, module API can pass in boolean
|
|
936
|
-
if (options.configFile === 'false' || options.configFile === false) {
|
|
937
|
-
throwInvalidOptionError(xvfb.errors.invalidConfigFile);
|
|
938
|
-
}
|
|
939
|
-
};
|
|
940
|
-
|
|
941
|
-
const debug$5 = Debug('cypress:cli');
|
|
942
|
-
/**
|
|
943
|
-
* Maps options collected by the CLI
|
|
944
|
-
* and forms list of CLI arguments to the server.
|
|
945
|
-
*
|
|
946
|
-
* Note: there is lightweight validation, with errors
|
|
947
|
-
* thrown synchronously.
|
|
948
|
-
*
|
|
949
|
-
* @returns {string[]} list of CLI arguments
|
|
950
|
-
*/
|
|
951
|
-
const processOpenOptions = (options = {}) => {
|
|
952
|
-
// In addition to setting the project directory, setting the project option
|
|
953
|
-
// here ultimately decides whether cypress is run in global mode or not.
|
|
954
|
-
// It's first based off whether it's installed globally by npm/yarn (-g).
|
|
955
|
-
// A global install can be overridden by the --project flag, putting Cypress
|
|
956
|
-
// in project mode. A non-global install can be overridden by the --global
|
|
957
|
-
// flag, putting it in global mode.
|
|
958
|
-
if (!xvfb.util.isInstalledGlobally() && !options.global && !options.project) {
|
|
959
|
-
options.project = process.cwd();
|
|
960
|
-
}
|
|
961
|
-
const args = [];
|
|
962
|
-
if (options.config) {
|
|
963
|
-
args.push('--config', options.config);
|
|
964
|
-
}
|
|
965
|
-
if (options.configFile !== undefined) {
|
|
966
|
-
checkConfigFile(options);
|
|
967
|
-
args.push('--config-file', options.configFile);
|
|
968
|
-
}
|
|
969
|
-
if (options.browser) {
|
|
970
|
-
args.push('--browser', options.browser);
|
|
971
|
-
}
|
|
972
|
-
if (options.env) {
|
|
973
|
-
args.push('--env', options.env);
|
|
974
|
-
}
|
|
975
|
-
if (options.expose) {
|
|
976
|
-
args.push('--expose', options.expose);
|
|
977
|
-
}
|
|
978
|
-
if (options.port) {
|
|
979
|
-
args.push('--port', options.port);
|
|
980
|
-
}
|
|
981
|
-
if (options.project) {
|
|
982
|
-
args.push('--project', options.project);
|
|
983
|
-
}
|
|
984
|
-
if (options.global) {
|
|
985
|
-
args.push('--global', options.global);
|
|
986
|
-
}
|
|
987
|
-
if (options.inspect) {
|
|
988
|
-
args.push('--inspect');
|
|
989
|
-
}
|
|
990
|
-
if (options.inspectBrk) {
|
|
991
|
-
args.push('--inspectBrk');
|
|
992
|
-
}
|
|
993
|
-
args.push(...processTestingType(options));
|
|
994
|
-
debug$5('opening from options %j', options);
|
|
995
|
-
debug$5('command line arguments %j', args);
|
|
996
|
-
return args;
|
|
997
|
-
};
|
|
998
|
-
const start = (...args_1) => xvfb.__awaiter(void 0, [...args_1], void 0, function* (options = {}) {
|
|
999
|
-
function open() {
|
|
1000
|
-
try {
|
|
1001
|
-
const args = processOpenOptions(options);
|
|
1002
|
-
return spawn.start$1(args, {
|
|
1003
|
-
dev: options.dev,
|
|
1004
|
-
detached: Boolean(options.detached),
|
|
1005
|
-
});
|
|
1006
|
-
}
|
|
1007
|
-
catch (err) {
|
|
1008
|
-
if (err.details) {
|
|
1009
|
-
return xvfb.exitWithError(err.details)();
|
|
1010
|
-
}
|
|
1011
|
-
throw err;
|
|
1012
|
-
}
|
|
1013
|
-
}
|
|
1014
|
-
if (options.dev) {
|
|
1015
|
-
return open();
|
|
1016
|
-
}
|
|
1017
|
-
yield spawn.start();
|
|
1018
|
-
return open();
|
|
1019
|
-
});
|
|
1020
|
-
var openModule = {
|
|
1021
|
-
start,
|
|
1022
|
-
processOpenOptions,
|
|
1023
|
-
};
|
|
1024
|
-
|
|
1025
|
-
const debug$4 = Debug('cypress:cli:run');
|
|
1026
|
-
/**
|
|
1027
|
-
* Typically a user passes a string path to the project.
|
|
1028
|
-
* But "cypress open" allows using `false` to open in global mode,
|
|
1029
|
-
* and the user can accidentally execute `cypress run --project false`
|
|
1030
|
-
* which should be invalid.
|
|
1031
|
-
*/
|
|
1032
|
-
const isValidProject = (v) => {
|
|
1033
|
-
if (typeof v === 'boolean') {
|
|
1034
|
-
return false;
|
|
1035
|
-
}
|
|
1036
|
-
if (v === '' || v === 'false' || v === 'true') {
|
|
1037
|
-
return false;
|
|
1038
|
-
}
|
|
1039
|
-
return true;
|
|
1040
|
-
};
|
|
1041
|
-
/**
|
|
1042
|
-
* Maps options collected by the CLI
|
|
1043
|
-
* and forms list of CLI arguments to the server.
|
|
1044
|
-
*
|
|
1045
|
-
* Note: there is lightweight validation, with errors
|
|
1046
|
-
* thrown synchronously.
|
|
1047
|
-
*
|
|
1048
|
-
* @returns {string[]} list of CLI arguments
|
|
1049
|
-
*/
|
|
1050
|
-
const processRunOptions = (options = {}) => {
|
|
1051
|
-
debug$4('processing run options %o', options);
|
|
1052
|
-
if (!isValidProject(options.project)) {
|
|
1053
|
-
debug$4('invalid project option %o', { project: options.project });
|
|
1054
|
-
return throwInvalidOptionError(xvfb.errors.invalidRunProjectPath);
|
|
1055
|
-
}
|
|
1056
|
-
const args = ['--run-project', options.project];
|
|
1057
|
-
if (options.autoCancelAfterFailures || options.autoCancelAfterFailures === 0 || options.autoCancelAfterFailures === false) {
|
|
1058
|
-
args.push('--auto-cancel-after-failures', options.autoCancelAfterFailures);
|
|
1059
|
-
}
|
|
1060
|
-
if (options.browser) {
|
|
1061
|
-
args.push('--browser', options.browser);
|
|
1062
|
-
}
|
|
1063
|
-
if (options.ciBuildId) {
|
|
1064
|
-
args.push('--ci-build-id', options.ciBuildId);
|
|
1065
|
-
}
|
|
1066
|
-
if (options.config) {
|
|
1067
|
-
args.push('--config', options.config);
|
|
1068
|
-
}
|
|
1069
|
-
if (options.configFile !== undefined) {
|
|
1070
|
-
checkConfigFile(options);
|
|
1071
|
-
args.push('--config-file', options.configFile);
|
|
1072
|
-
}
|
|
1073
|
-
if (options.env) {
|
|
1074
|
-
args.push('--env', options.env);
|
|
1075
|
-
}
|
|
1076
|
-
if (options.expose) {
|
|
1077
|
-
args.push('--expose', options.expose);
|
|
1078
|
-
}
|
|
1079
|
-
if (options.exit === false) {
|
|
1080
|
-
args.push('--no-exit');
|
|
1081
|
-
}
|
|
1082
|
-
if (options.group) {
|
|
1083
|
-
args.push('--group', options.group);
|
|
1084
|
-
}
|
|
1085
|
-
if (options.headed) {
|
|
1086
|
-
args.push('--headed', options.headed);
|
|
1087
|
-
}
|
|
1088
|
-
if (options.headless) {
|
|
1089
|
-
if (options.headed) {
|
|
1090
|
-
return throwInvalidOptionError(xvfb.errors.incompatibleHeadlessFlags);
|
|
1091
|
-
}
|
|
1092
|
-
args.push('--headed', String(!options.headless));
|
|
1093
|
-
}
|
|
1094
|
-
// if key is set use that - else attempt to find it by environment variable
|
|
1095
|
-
if (options.key == null) {
|
|
1096
|
-
debug$4('--key is not set, looking up environment variable CYPRESS_RECORD_KEY');
|
|
1097
|
-
options.key = xvfb.util.getEnv('CYPRESS_RECORD_KEY');
|
|
1098
|
-
}
|
|
1099
|
-
// if we have a key assume we're in record mode
|
|
1100
|
-
if (options.key) {
|
|
1101
|
-
args.push('--key', options.key);
|
|
1102
|
-
}
|
|
1103
|
-
if (options.outputPath) {
|
|
1104
|
-
args.push('--output-path', options.outputPath);
|
|
1105
|
-
}
|
|
1106
|
-
if (options.parallel) {
|
|
1107
|
-
args.push('--parallel');
|
|
1108
|
-
}
|
|
1109
|
-
if (options.passWithNoTests) {
|
|
1110
|
-
args.push('--pass-with-no-tests');
|
|
1111
|
-
}
|
|
1112
|
-
if (options.posixExitCodes) {
|
|
1113
|
-
args.push('--posix-exit-codes');
|
|
1114
|
-
}
|
|
1115
|
-
if (options.port) {
|
|
1116
|
-
args.push('--port', options.port);
|
|
1117
|
-
}
|
|
1118
|
-
if (options.quiet) {
|
|
1119
|
-
args.push('--quiet');
|
|
1120
|
-
}
|
|
1121
|
-
// if record is defined and we're not
|
|
1122
|
-
// already in ci mode, then send it up
|
|
1123
|
-
if (options.record != null) {
|
|
1124
|
-
args.push('--record', options.record);
|
|
1125
|
-
}
|
|
1126
|
-
// if we have a specific reporter push that into the args
|
|
1127
|
-
if (options.reporter) {
|
|
1128
|
-
args.push('--reporter', options.reporter);
|
|
1129
|
-
}
|
|
1130
|
-
// if we have a specific reporter push that into the args
|
|
1131
|
-
if (options.reporterOptions) {
|
|
1132
|
-
args.push('--reporter-options', options.reporterOptions);
|
|
1133
|
-
}
|
|
1134
|
-
if (options.runnerUi != null) {
|
|
1135
|
-
args.push('--runner-ui', options.runnerUi);
|
|
1136
|
-
}
|
|
1137
|
-
// if we have specific spec(s) push that into the args
|
|
1138
|
-
if (options.spec) {
|
|
1139
|
-
args.push('--spec', options.spec);
|
|
1140
|
-
}
|
|
1141
|
-
if (options.tag) {
|
|
1142
|
-
args.push('--tag', options.tag);
|
|
1143
|
-
}
|
|
1144
|
-
if (options.inspect) {
|
|
1145
|
-
args.push('--inspect');
|
|
1146
|
-
}
|
|
1147
|
-
if (options.inspectBrk) {
|
|
1148
|
-
args.push('--inspectBrk');
|
|
1149
|
-
}
|
|
1150
|
-
args.push(...processTestingType(options));
|
|
1151
|
-
return args;
|
|
1152
|
-
};
|
|
1153
|
-
const runModule = {
|
|
1154
|
-
processRunOptions,
|
|
1155
|
-
isValidProject,
|
|
1156
|
-
// resolves with the number of failed tests
|
|
1157
|
-
start() {
|
|
1158
|
-
return xvfb.__awaiter(this, arguments, void 0, function* (options = {}) {
|
|
1159
|
-
_.defaults(options, {
|
|
1160
|
-
key: null,
|
|
1161
|
-
spec: null,
|
|
1162
|
-
reporter: null,
|
|
1163
|
-
reporterOptions: null,
|
|
1164
|
-
project: process.cwd(),
|
|
1165
|
-
});
|
|
1166
|
-
function run() {
|
|
1167
|
-
try {
|
|
1168
|
-
const args = processRunOptions(options);
|
|
1169
|
-
debug$4('run to spawn.start args %j', args);
|
|
1170
|
-
return spawn.start$1(args, {
|
|
1171
|
-
dev: options.dev,
|
|
1172
|
-
});
|
|
1173
|
-
}
|
|
1174
|
-
catch (err) {
|
|
1175
|
-
if (err.details) {
|
|
1176
|
-
return xvfb.exitWithError(err.details)();
|
|
1177
|
-
}
|
|
1178
|
-
throw err;
|
|
1179
|
-
}
|
|
1180
|
-
}
|
|
1181
|
-
if (options.dev) {
|
|
1182
|
-
return run();
|
|
1183
|
-
}
|
|
1184
|
-
yield spawn.start();
|
|
1185
|
-
return run();
|
|
1186
|
-
});
|
|
1187
|
-
},
|
|
1188
|
-
};
|
|
1189
|
-
|
|
1190
|
-
/**
|
|
1191
|
-
* Get the size of a folder or a file.
|
|
1192
|
-
*
|
|
1193
|
-
* This function returns the actual file size of the folder (size), not the allocated space on disk (size on disk).
|
|
1194
|
-
* For more details between the difference, check this link:
|
|
1195
|
-
* https://www.howtogeek.com/180369/why-is-there-a-big-difference-between-size-and-size-on-disk/
|
|
1196
|
-
*
|
|
1197
|
-
* @param {string} path path to the file or the folder.
|
|
1198
|
-
*/
|
|
1199
|
-
function getSize(path$1) {
|
|
1200
|
-
return xvfb.__awaiter(this, void 0, void 0, function* () {
|
|
1201
|
-
const stat = yield fs.lstat(path$1);
|
|
1202
|
-
if (stat.isDirectory()) {
|
|
1203
|
-
const list = yield fs.readdir(path$1);
|
|
1204
|
-
return Bluebird.resolve(list).reduce((prev, curr) => xvfb.__awaiter(this, void 0, void 0, function* () {
|
|
1205
|
-
const currPath = path.join(path$1, curr);
|
|
1206
|
-
const s = yield fs.lstat(currPath);
|
|
1207
|
-
if (s.isDirectory()) {
|
|
1208
|
-
return prev + (yield getSize(currPath));
|
|
1209
|
-
}
|
|
1210
|
-
return prev + s.size;
|
|
1211
|
-
}), 0);
|
|
1212
|
-
}
|
|
1213
|
-
return stat.size;
|
|
1214
|
-
});
|
|
1215
|
-
}
|
|
1216
|
-
|
|
1217
|
-
var dist = {};
|
|
1218
|
-
|
|
1219
|
-
var tapContract = {};
|
|
1220
|
-
|
|
1221
|
-
var tapErrors = {};
|
|
64
|
+
var tapErrors = {};
|
|
1222
65
|
|
|
1223
66
|
var hasRequiredTapErrors;
|
|
1224
67
|
|
|
1225
68
|
function requireTapErrors () {
|
|
1226
69
|
if (hasRequiredTapErrors) return tapErrors;
|
|
1227
70
|
hasRequiredTapErrors = 1;
|
|
1228
|
-
(function (exports
|
|
71
|
+
(function (exports) {
|
|
1229
72
|
// The copy for every `cypress tap` failure, keyed by its code. Code is not usefacing and is
|
|
1230
73
|
// just used in debug logs and telemetry.
|
|
1231
|
-
Object.defineProperty(exports
|
|
1232
|
-
exports
|
|
74
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
75
|
+
exports.UnknownTapError = exports.SpecInProgressTapError = exports.VersionSkewTapError = exports.MissingOptionTapError = exports.MissingArgumentsTapError = exports.UnknownOptionTapError = exports.UnknownCommandTapError = exports.MissingCompanionOptionTapError = exports.SnapshotNotFoundTapError = exports.AttemptNotFoundTapError = exports.CommandNotFoundTapError = exports.TestNotFoundTapError = exports.SessionNotFoundTapError = exports.InvalidValueTapError = exports.isTapError = exports.TapError = exports.tapErrorCopy = exports.TAP_ERROR_COPY = exports.TAP_TARGET = void 0;
|
|
1233
76
|
const UPDATE_COMMAND = '`npm install --save-dev cypress@latest`';
|
|
1234
77
|
/**
|
|
1235
78
|
* What every failure calls the running Cypress a tap command targets.
|
|
1236
79
|
*/
|
|
1237
|
-
exports
|
|
80
|
+
exports.TAP_TARGET = 'Cypress session';
|
|
1238
81
|
/**
|
|
1239
82
|
* Several codes stand for one thing a reader has to do about them, and saying it
|
|
1240
83
|
* four slightly different ways only reads as four different problems. Each keeps
|
|
@@ -1243,14 +86,14 @@ function requireTapErrors () {
|
|
|
1243
86
|
* The jsdoc on each entry is where the condition it really stands for is written.
|
|
1244
87
|
*/
|
|
1245
88
|
const UNREACHABLE = {
|
|
1246
|
-
description: `Could not reach the ${exports
|
|
89
|
+
description: `Could not reach the ${exports.TAP_TARGET}.`,
|
|
1247
90
|
solution: 'Make sure Cypress is still open with a browser, then run the command again. If it was just started, it may still be loading.',
|
|
1248
91
|
};
|
|
1249
92
|
// No "if it keeps failing" clause here: the entries that ask for a report already
|
|
1250
93
|
// open theirs with one, and saying it twice reads as two separate escalations.
|
|
1251
94
|
const COMMAND_FAILED = {
|
|
1252
|
-
description: `The ${exports
|
|
1253
|
-
solution: `Check the ${exports
|
|
95
|
+
description: `The ${exports.TAP_TARGET} failed while running the command.`,
|
|
96
|
+
solution: `Check the ${exports.TAP_TARGET} with \`cypress tap status\`, then try again.`,
|
|
1254
97
|
};
|
|
1255
98
|
/**
|
|
1256
99
|
* Nothing worth saying, for the conditions that should not arise: a failure with no
|
|
@@ -1262,11 +105,11 @@ function requireTapErrors () {
|
|
|
1262
105
|
description: 'An error occurred.',
|
|
1263
106
|
solution: 'Try running the command again.',
|
|
1264
107
|
};
|
|
1265
|
-
exports
|
|
108
|
+
exports.TAP_ERROR_COPY = {
|
|
1266
109
|
// Finding a session to drive
|
|
1267
110
|
/** Raised when discovery found no session record at all, and none was named. */
|
|
1268
111
|
NO_SESSION: {
|
|
1269
|
-
description: `Could not find a ${exports
|
|
112
|
+
description: `Could not find a ${exports.TAP_TARGET} to tap into.`,
|
|
1270
113
|
solution: 'Start Cypress with `cypress open`, select a testing type and launch a browser, then try again.',
|
|
1271
114
|
},
|
|
1272
115
|
/**
|
|
@@ -1275,24 +118,24 @@ function requireTapErrors () {
|
|
|
1275
118
|
* @deprecated - raise it with new SessionNotFoundTapError(), which writes its detail
|
|
1276
119
|
*/
|
|
1277
120
|
SESSION_NOT_FOUND: {
|
|
1278
|
-
description: `No ${exports
|
|
1279
|
-
solution: `Run \`cypress tap sessions\` to list the ${exports
|
|
121
|
+
description: `No ${exports.TAP_TARGET} matched the provided session id.`,
|
|
122
|
+
solution: `Run \`cypress tap sessions\` to list the ${exports.TAP_TARGET}s you can tap into.`,
|
|
1280
123
|
},
|
|
1281
124
|
/** Raised when records matched, but none answered its liveness probe. */
|
|
1282
125
|
STALE_SESSION: { ...UNREACHABLE },
|
|
1283
126
|
/** Raised when the session is live, but has no browser open to drive. */
|
|
1284
127
|
NO_BROWSER_ATTACHED: {
|
|
1285
|
-
description: `The ${exports
|
|
128
|
+
description: `The ${exports.TAP_TARGET} is running, but no test browser is open.`,
|
|
1286
129
|
solution: 'Open a Chromium-based browser in Cypress, then try again.',
|
|
1287
130
|
},
|
|
1288
131
|
/** Raised when every live session has a browser open that tap cannot drive. */
|
|
1289
132
|
UNSUPPORTED_BROWSER: {
|
|
1290
|
-
description: `The ${exports
|
|
133
|
+
description: `The ${exports.TAP_TARGET} is running an unsupported browser.`,
|
|
1291
134
|
solution: '`cypress tap` drives Chromium-based browsers only. Reopen Cypress in a supported browser and try again.',
|
|
1292
135
|
},
|
|
1293
136
|
/** Raised when a CDP call went unanswered until the timeout elapsed. */
|
|
1294
137
|
RENDERER_UNRESPONSIVE: {
|
|
1295
|
-
description: `The ${exports
|
|
138
|
+
description: `The ${exports.TAP_TARGET} is reachable, but the page running it is not responding.`,
|
|
1296
139
|
solution: 'It may be paused in DevTools, stuck in a loop, or starved of memory. Pass `--timeout <ms>` to wait longer.',
|
|
1297
140
|
},
|
|
1298
141
|
// Connecting to it
|
|
@@ -1324,12 +167,12 @@ function requireTapErrors () {
|
|
|
1324
167
|
},
|
|
1325
168
|
/** Raised when the handshake reported a schema version newer than this CLI's. */
|
|
1326
169
|
CLI_OUTDATED: {
|
|
1327
|
-
description: `The targeted ${exports
|
|
170
|
+
description: `The targeted ${exports.TAP_TARGET} is newer than this CLI.`,
|
|
1328
171
|
solution: `Update the CLI with ${UPDATE_COMMAND}, then try again.`,
|
|
1329
172
|
},
|
|
1330
|
-
/** Raised when the handshake reported an older schema version, or GraphQL
|
|
173
|
+
/** Raised when the handshake reported an older schema version, or the session does not serve the tap GraphQL route. */
|
|
1331
174
|
SESSION_OUTDATED: {
|
|
1332
|
-
description: `The targeted ${exports
|
|
175
|
+
description: `The targeted ${exports.TAP_TARGET} is older than this CLI.`,
|
|
1333
176
|
solution: `Update Cypress in the running project with ${UPDATE_COMMAND}, restart it, then try again.`,
|
|
1334
177
|
},
|
|
1335
178
|
// Reading its data
|
|
@@ -1356,17 +199,17 @@ function requireTapErrors () {
|
|
|
1356
199
|
},
|
|
1357
200
|
/** Raised when the runSpec mutation failed, or answered with no result. */
|
|
1358
201
|
SPEC_START_FAILED: {
|
|
1359
|
-
description: `The ${exports
|
|
1360
|
-
solution: `Check the ${exports
|
|
202
|
+
description: `The ${exports.TAP_TARGET} could not start the spec.`,
|
|
203
|
+
solution: `Check the ${exports.TAP_TARGET} with \`cypress tap status\`, then try again.`,
|
|
1361
204
|
},
|
|
1362
205
|
/** Raised when the given path matches no spec the session can run. */
|
|
1363
206
|
SPEC_NOT_FOUND: {
|
|
1364
|
-
description: `The ${exports
|
|
1365
|
-
solution: `\`cypress tap specs\` lists the specs the ${exports
|
|
207
|
+
description: `The ${exports.TAP_TARGET} has no spec matching that path.`,
|
|
208
|
+
solution: `\`cypress tap specs\` lists the specs the ${exports.TAP_TARGET} can run. If the spec exists but is not listed, widen \`specPattern\` in the Cypress configuration.`,
|
|
1366
209
|
},
|
|
1367
210
|
/** Raised when the session is running with no project open. */
|
|
1368
211
|
NO_PROJECT: {
|
|
1369
|
-
description: `The ${exports
|
|
212
|
+
description: `The ${exports.TAP_TARGET} has no project open.`,
|
|
1370
213
|
solution: 'Open a project in Cypress, then try again.',
|
|
1371
214
|
},
|
|
1372
215
|
/** Raised when the spec's testing type is not one this project configures. */
|
|
@@ -1378,7 +221,7 @@ function requireTapErrors () {
|
|
|
1378
221
|
// Reading the app under test
|
|
1379
222
|
/** Raised when the runner page holds no app-under-test frame to read. */
|
|
1380
223
|
NO_AUT: {
|
|
1381
|
-
description: `Failed to determine the app under test in the ${exports
|
|
224
|
+
description: `Failed to determine the app under test in the ${exports.TAP_TARGET}.`,
|
|
1382
225
|
solution: 'Run the spec again with `cypress tap run <spec>`. To read the app as it was at an earlier command, pin that command with `cypress tap pin`.',
|
|
1383
226
|
},
|
|
1384
227
|
/** Raised when an injected script threw inside the AUT frame. */
|
|
@@ -1490,19 +333,19 @@ function requireTapErrors () {
|
|
|
1490
333
|
// An unknown code is a protocol mismatch by definition: the session speaks of a
|
|
1491
334
|
// failure this CLI has no copy for, which is one more thing about its answer that
|
|
1492
335
|
// cannot be read.
|
|
1493
|
-
const FALLBACK = exports
|
|
336
|
+
const FALLBACK = exports.TAP_ERROR_COPY.PROTOCOL_MISMATCH;
|
|
1494
337
|
/**
|
|
1495
338
|
* Copy for a code, which arrives from the session over the wire — so anything that
|
|
1496
339
|
* is not a code we ship falls back rather than being trusted: a non-string, an
|
|
1497
340
|
* inherited name like `constructor`, or a code only a newer Cypress knows about.
|
|
1498
341
|
*/
|
|
1499
342
|
const tapErrorCopy = (code) => {
|
|
1500
|
-
if (typeof code !== 'string' || !Object.prototype.hasOwnProperty.call(exports
|
|
343
|
+
if (typeof code !== 'string' || !Object.prototype.hasOwnProperty.call(exports.TAP_ERROR_COPY, code)) {
|
|
1501
344
|
return FALLBACK;
|
|
1502
345
|
}
|
|
1503
|
-
return exports
|
|
346
|
+
return exports.TAP_ERROR_COPY[code];
|
|
1504
347
|
};
|
|
1505
|
-
exports
|
|
348
|
+
exports.tapErrorCopy = tapErrorCopy;
|
|
1506
349
|
/**
|
|
1507
350
|
* The one error every tap failure is raised as, on both sides of the wire: the app
|
|
1508
351
|
* throws it from a command handler, the CLI throws it from discovery, transport, and
|
|
@@ -1540,11 +383,11 @@ function requireTapErrors () {
|
|
|
1540
383
|
return { code: this.code, ...(this.detail !== undefined ? { detail: this.detail } : {}) };
|
|
1541
384
|
}
|
|
1542
385
|
}
|
|
1543
|
-
exports
|
|
386
|
+
exports.TapError = TapError;
|
|
1544
387
|
const isTapError = (err) => {
|
|
1545
388
|
return err instanceof TapError;
|
|
1546
389
|
};
|
|
1547
|
-
exports
|
|
390
|
+
exports.isTapError = isTapError;
|
|
1548
391
|
// The one door through the constructor's guard, so that every code it keeps out is
|
|
1549
392
|
// still built the same way — with the copy its class below writes.
|
|
1550
393
|
class DetailedTapError extends TapError {
|
|
@@ -1562,7 +405,7 @@ function requireTapErrors () {
|
|
|
1562
405
|
super('INVALID_VALUE', `Expected \`${name}\` to be ${expected}.\n\nInstead the value was: ${JSON.stringify(value)}`);
|
|
1563
406
|
}
|
|
1564
407
|
}
|
|
1565
|
-
exports
|
|
408
|
+
exports.InvalidValueTapError = InvalidValueTapError;
|
|
1566
409
|
/**
|
|
1567
410
|
* A value that was read fine but matched nothing there is. Each entry states what
|
|
1568
411
|
* was being looked for and where the real ones are listed; this writes the one line
|
|
@@ -1581,13 +424,13 @@ function requireTapErrors () {
|
|
|
1581
424
|
super('SESSION_NOT_FOUND', '--session', value);
|
|
1582
425
|
}
|
|
1583
426
|
}
|
|
1584
|
-
exports
|
|
427
|
+
exports.SessionNotFoundTapError = SessionNotFoundTapError;
|
|
1585
428
|
class TestNotFoundTapError extends NotFoundTapError {
|
|
1586
429
|
constructor(value) {
|
|
1587
430
|
super('TEST_NOT_FOUND', '--test-id', value);
|
|
1588
431
|
}
|
|
1589
432
|
}
|
|
1590
|
-
exports
|
|
433
|
+
exports.TestNotFoundTapError = TestNotFoundTapError;
|
|
1591
434
|
/**
|
|
1592
435
|
* The one lookup whose remedy names a value the caller gave: the test whose commands
|
|
1593
436
|
* to list. Which is why the test id is required rather than optional — the table
|
|
@@ -1601,19 +444,19 @@ function requireTapErrors () {
|
|
|
1601
444
|
});
|
|
1602
445
|
}
|
|
1603
446
|
}
|
|
1604
|
-
exports
|
|
447
|
+
exports.CommandNotFoundTapError = CommandNotFoundTapError;
|
|
1605
448
|
class AttemptNotFoundTapError extends NotFoundTapError {
|
|
1606
449
|
constructor(value, context) {
|
|
1607
450
|
super('ATTEMPT_NOT_FOUND', '--attempt', value, { context });
|
|
1608
451
|
}
|
|
1609
452
|
}
|
|
1610
|
-
exports
|
|
453
|
+
exports.AttemptNotFoundTapError = AttemptNotFoundTapError;
|
|
1611
454
|
class SnapshotNotFoundTapError extends NotFoundTapError {
|
|
1612
455
|
constructor(value, context) {
|
|
1613
456
|
super('SNAPSHOT_NOT_FOUND', '--at', value, { context });
|
|
1614
457
|
}
|
|
1615
458
|
}
|
|
1616
|
-
exports
|
|
459
|
+
exports.SnapshotNotFoundTapError = SnapshotNotFoundTapError;
|
|
1617
460
|
/**
|
|
1618
461
|
* A flag that only means something alongside another one. Both are named here;
|
|
1619
462
|
* `remedy` is the throw site's, because what dropping either one leaves you with
|
|
@@ -1624,7 +467,7 @@ function requireTapErrors () {
|
|
|
1624
467
|
super('MISSING_COMPANION_OPTION', `You passed the \`${given}\` flag without also passing the \`${required}\` flag.\n\n${remedy}`);
|
|
1625
468
|
}
|
|
1626
469
|
}
|
|
1627
|
-
exports
|
|
470
|
+
exports.MissingCompanionOptionTapError = MissingCompanionOptionTapError;
|
|
1628
471
|
/**
|
|
1629
472
|
* A name no command answers to, and a flag no command declares: say which one was
|
|
1630
473
|
* given, then list the real ones. The listing is the remedy, which is why neither
|
|
@@ -1637,13 +480,13 @@ function requireTapErrors () {
|
|
|
1637
480
|
super('UNKNOWN_COMMAND', listing ? `Unknown command "${name}"\n\n${listing}` : `Unknown command "${name}"`);
|
|
1638
481
|
}
|
|
1639
482
|
}
|
|
1640
|
-
exports
|
|
483
|
+
exports.UnknownCommandTapError = UnknownCommandTapError;
|
|
1641
484
|
class UnknownOptionTapError extends DetailedTapError {
|
|
1642
485
|
constructor(flag, listing) {
|
|
1643
486
|
super('UNKNOWN_OPTION', listing ? `Unknown option "${flag}"\n\n${listing}` : `Unknown option "${flag}"`);
|
|
1644
487
|
}
|
|
1645
488
|
}
|
|
1646
|
-
exports
|
|
489
|
+
exports.UnknownOptionTapError = UnknownOptionTapError;
|
|
1647
490
|
/**
|
|
1648
491
|
* A required input the invocation left out. Both sides raise these through here —
|
|
1649
492
|
* the CLI when its own grammar catches the omission, the session when a call
|
|
@@ -1659,13 +502,13 @@ function requireTapErrors () {
|
|
|
1659
502
|
super('INVALID_ARGUMENTS', { detail: `"${command}" is missing the required ${named} ${noun}.` });
|
|
1660
503
|
}
|
|
1661
504
|
}
|
|
1662
|
-
exports
|
|
505
|
+
exports.MissingArgumentsTapError = MissingArgumentsTapError;
|
|
1663
506
|
class MissingOptionTapError extends TapError {
|
|
1664
507
|
constructor(command, option) {
|
|
1665
508
|
super('INVALID_OPTIONS', { detail: `"${command}" is missing the required --${option} option.` });
|
|
1666
509
|
}
|
|
1667
510
|
}
|
|
1668
|
-
exports
|
|
511
|
+
exports.MissingOptionTapError = MissingOptionTapError;
|
|
1669
512
|
/**
|
|
1670
513
|
* A CLI and a session that do not speak the same tap schema. Which of the two is
|
|
1671
514
|
* behind is decided here rather than at the throw site, so the code and the copy
|
|
@@ -1680,12 +523,12 @@ function requireTapErrors () {
|
|
|
1680
523
|
class VersionSkewTapError extends TapError {
|
|
1681
524
|
constructor({ sessionSchema, cliSchema, sessionCypress, cliCypress }) {
|
|
1682
525
|
super(sessionSchema > cliSchema ? 'CLI_OUTDATED' : 'SESSION_OUTDATED', {
|
|
1683
|
-
detail: `The ${exports
|
|
1684
|
-
message: `the ${exports
|
|
526
|
+
detail: `The ${exports.TAP_TARGET} is running Cypress v${sessionCypress}; this CLI is v${cliCypress}.`,
|
|
527
|
+
message: `the ${exports.TAP_TARGET} speaks tap schema v${sessionSchema}; this CLI speaks v${cliSchema}.`,
|
|
1685
528
|
});
|
|
1686
529
|
}
|
|
1687
530
|
}
|
|
1688
|
-
exports
|
|
531
|
+
exports.VersionSkewTapError = VersionSkewTapError;
|
|
1689
532
|
/**
|
|
1690
533
|
* The spec that is mid-run, which is what makes the condition actionable: it names
|
|
1691
534
|
* what to wait on. Both sides raise it through here — the CLI from its run-state
|
|
@@ -1697,7 +540,7 @@ function requireTapErrors () {
|
|
|
1697
540
|
super('SPEC_IN_PROGRESS', spec ? `The spec ${spec} is currently running.` : 'The spec is currently running.');
|
|
1698
541
|
}
|
|
1699
542
|
}
|
|
1700
|
-
exports
|
|
543
|
+
exports.SpecInProgressTapError = SpecInProgressTapError;
|
|
1701
544
|
/**
|
|
1702
545
|
* Whatever reached the renderer without having been raised as a tap failure — a
|
|
1703
546
|
* TypeError from a command handler, an ENOTDIR from a read that should not have
|
|
@@ -1711,7 +554,7 @@ function requireTapErrors () {
|
|
|
1711
554
|
super('UNKNOWN_ERROR', { message: String((_a = cause === null || cause === void 0 ? void 0 : cause.stack) !== null && _a !== void 0 ? _a : cause), cause });
|
|
1712
555
|
}
|
|
1713
556
|
}
|
|
1714
|
-
exports
|
|
557
|
+
exports.UnknownTapError = UnknownTapError;
|
|
1715
558
|
|
|
1716
559
|
} (tapErrors));
|
|
1717
560
|
return tapErrors;
|
|
@@ -1763,45 +606,392 @@ function requirePinned () {
|
|
|
1763
606
|
|
|
1764
607
|
var pin = {};
|
|
1765
608
|
|
|
1766
|
-
var hasRequiredPin;
|
|
609
|
+
var hasRequiredPin;
|
|
610
|
+
|
|
611
|
+
function requirePin () {
|
|
612
|
+
if (hasRequiredPin) return pin;
|
|
613
|
+
hasRequiredPin = 1;
|
|
614
|
+
Object.defineProperty(pin, "__esModule", { value: true });
|
|
615
|
+
|
|
616
|
+
return pin;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
var resolveSelector = {};
|
|
620
|
+
|
|
621
|
+
var hasRequiredResolveSelector;
|
|
622
|
+
|
|
623
|
+
function requireResolveSelector () {
|
|
624
|
+
if (hasRequiredResolveSelector) return resolveSelector;
|
|
625
|
+
hasRequiredResolveSelector = 1;
|
|
626
|
+
// The `resolve-selector` command's result contract. A command's result interface
|
|
627
|
+
// lives here in `contracts/`, next to the command metadata in `../tap-contract`,
|
|
628
|
+
// so the app-side command and the CLI-side rendering type against the same shape.
|
|
629
|
+
Object.defineProperty(resolveSelector, "__esModule", { value: true });
|
|
630
|
+
resolveSelector.MAX_DERIVED_SELECTORS = void 0;
|
|
631
|
+
// Deriving a unique selector walks up from the element testing each candidate
|
|
632
|
+
// against the whole document, so deriving one per match for a selector as broad
|
|
633
|
+
// as `*` would hold the app's main thread for the size of the page. Past this
|
|
634
|
+
// many matches the list is no longer one a caller picks out of anyway — so the
|
|
635
|
+
// app derives up to here, and the CLI numbers up to here.
|
|
636
|
+
resolveSelector.MAX_DERIVED_SELECTORS = 10;
|
|
637
|
+
|
|
638
|
+
return resolveSelector;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
var hasRequiredTapContract;
|
|
642
|
+
|
|
643
|
+
function requireTapContract () {
|
|
644
|
+
if (hasRequiredTapContract) return tapContract;
|
|
645
|
+
hasRequiredTapContract = 1;
|
|
646
|
+
(function (exports) {
|
|
647
|
+
var __createBinding = (tapContract && tapContract.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
648
|
+
if (k2 === undefined) k2 = k;
|
|
649
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
650
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
651
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
652
|
+
}
|
|
653
|
+
Object.defineProperty(o, k2, desc);
|
|
654
|
+
}) : (function(o, m, k, k2) {
|
|
655
|
+
if (k2 === undefined) k2 = k;
|
|
656
|
+
o[k2] = m[k];
|
|
657
|
+
}));
|
|
658
|
+
var __exportStar = (tapContract && tapContract.__exportStar) || function(m, exports) {
|
|
659
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
660
|
+
};
|
|
661
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
662
|
+
exports.KNOWN_COMMANDS = exports.TAP_NATIVE_COMMANDS = exports.buildTapSchema = exports.TAP_COMMANDS = exports.TAP_EXEC_METHOD = exports.TAP_SCHEMA_METHOD = exports.TAP_BINDING_GLOBAL = exports.TAP_SCHEMA_VERSION = void 0;
|
|
663
|
+
exports.TAP_SCHEMA_VERSION = 1;
|
|
664
|
+
exports.TAP_BINDING_GLOBAL = '__CYPRESS_TAP_BINDING__';
|
|
665
|
+
exports.TAP_SCHEMA_METHOD = 'getSchema';
|
|
666
|
+
exports.TAP_EXEC_METHOD = 'exec';
|
|
667
|
+
// Options that recur across commands, defined once so their name, type, and help
|
|
668
|
+
// text can't drift between the commands that expose them. `test-id` and
|
|
669
|
+
// `command-id` are required in some commands and optional in others, so each use
|
|
670
|
+
// spreads it and sets `required`; the rest are identical everywhere and used
|
|
671
|
+
// directly.
|
|
672
|
+
const testIdField = { name: 'test-id', alias: 't', type: 'string', description: 'test id, as listed by the reporter command' };
|
|
673
|
+
const commandIdField = { name: 'command-id', alias: 'c', type: 'string', description: 'command id, as listed by the reporter command — a row number (test body first when duplicated), an e-prefixed event id, or hook-qualified like "h1:3"' };
|
|
674
|
+
const attemptField = { name: 'attempt', alias: 'a', type: 'number', required: false, description: '1-based attempt (attempt 1 = first run); defaults to the latest' };
|
|
675
|
+
const selectorField = { name: 'selector', alias: 'e', type: 'string', required: false, description: 'a CSS selector' };
|
|
676
|
+
const commandMeta = {
|
|
677
|
+
name: 'command',
|
|
678
|
+
description: 'retrieve the reporter row, the pinnable DOM snapshots, and the console properties of a single command in a test',
|
|
679
|
+
details: `Retrieves a single command from a test's command log: its reporter row, the DOM
|
|
680
|
+
snapshots pinnable on it, and any associated console properties.`,
|
|
681
|
+
params: [],
|
|
682
|
+
options: [
|
|
683
|
+
{ ...testIdField, required: true },
|
|
684
|
+
{ ...commandIdField, required: true },
|
|
685
|
+
// `--json` is the CLI's own flag, but this command declares it because it
|
|
686
|
+
// also changes what the command returns: nothing is withheld from a payload
|
|
687
|
+
// that is not being rendered for reading room.
|
|
688
|
+
{ name: 'json', type: 'boolean', required: false, description: 'print the raw JSON result instead of the human-readable rendering. This will output every console property in full regardless of length.' },
|
|
689
|
+
attemptField,
|
|
690
|
+
{ name: 'depth', alias: 'd', type: 'string', required: false, description: 'how many levels of nested console properties to expand before summarizing the rest as "{n keys}" / "[n items]": a number or "all" (default 3, and a section over 8 rows folds at any depth unless this is passed)' },
|
|
691
|
+
],
|
|
692
|
+
};
|
|
693
|
+
const reporterMeta = {
|
|
694
|
+
name: 'reporter',
|
|
695
|
+
description: 'retrieve the test runner reporter for the spec or a specific test',
|
|
696
|
+
details: `Shows test results the way the Cypress reporter panel does.
|
|
697
|
+
|
|
698
|
+
By default, this will output the spec-level overview featuring the spec's
|
|
699
|
+
pass/fail stats and every suite's tests including their IDs.
|
|
700
|
+
|
|
701
|
+
Provide a --test-id <id> to see one test's full story: its routes, the hooks
|
|
702
|
+
that ran, the complete command log, and the failure output when something went
|
|
703
|
+
wrong. Add --attempt to view an earlier retry.`,
|
|
704
|
+
params: [],
|
|
705
|
+
options: [
|
|
706
|
+
{ ...testIdField, required: false },
|
|
707
|
+
attemptField,
|
|
708
|
+
],
|
|
709
|
+
};
|
|
710
|
+
const pinMeta = {
|
|
711
|
+
name: 'pin',
|
|
712
|
+
description: 'pin a command’s DOM snapshot into the live application-under-test frame',
|
|
713
|
+
details: `Pins a command's DOM snapshot into the live application-under-test frame so the
|
|
714
|
+
dom/aria/inspect commands can read it. Pass --clear to release.`,
|
|
715
|
+
params: [],
|
|
716
|
+
options: [
|
|
717
|
+
{ ...testIdField, required: false },
|
|
718
|
+
{ ...commandIdField, required: false },
|
|
719
|
+
attemptField,
|
|
720
|
+
{ name: 'at', type: 'string', required: false, description: 'which snapshot to pin: a name like "before"/"after" or a 1-based index; defaults to the last (the command’s final state). Re-run on the pinned command to switch snapshots without releasing the pin' },
|
|
721
|
+
{ name: 'clear', type: 'boolean', required: false, description: 'release the current pin and restore the app to its pre-pin state' },
|
|
722
|
+
],
|
|
723
|
+
};
|
|
724
|
+
const runStateMeta = {
|
|
725
|
+
name: 'run-state',
|
|
726
|
+
description: 'report where the Cypress session is in its run lifecycle',
|
|
727
|
+
params: [],
|
|
728
|
+
options: [],
|
|
729
|
+
hidden: true,
|
|
730
|
+
};
|
|
731
|
+
const resolveSelectorMeta = {
|
|
732
|
+
name: 'resolve-selector',
|
|
733
|
+
description: 'list a unique CSS selector for each element a selector matches, null for any match none could be derived for',
|
|
734
|
+
params: [
|
|
735
|
+
{ name: 'selector', type: 'string', required: true, description: 'a CSS selector' },
|
|
736
|
+
],
|
|
737
|
+
options: [],
|
|
738
|
+
hidden: true,
|
|
739
|
+
};
|
|
740
|
+
// The canonical command metadata, the single source both sides build a schema
|
|
741
|
+
// from: the running session advertises it over the binding (see the app's
|
|
742
|
+
// TapManager), and the CLI stamps it with its own version to render help with no
|
|
743
|
+
// session attached. Order here is the order commands list in help.
|
|
744
|
+
exports.TAP_COMMANDS = [
|
|
745
|
+
commandMeta,
|
|
746
|
+
reporterMeta,
|
|
747
|
+
pinMeta,
|
|
748
|
+
runStateMeta,
|
|
749
|
+
resolveSelectorMeta,
|
|
750
|
+
];
|
|
751
|
+
const buildTapSchema = (cypressVersion) => {
|
|
752
|
+
const commands = exports.TAP_COMMANDS.map((command) => {
|
|
753
|
+
return {
|
|
754
|
+
name: command.name,
|
|
755
|
+
description: command.description,
|
|
756
|
+
...('details' in command ? { details: command.details } : {}),
|
|
757
|
+
params: command.params.map((param) => ({ ...param })),
|
|
758
|
+
options: command.options.map((option) => ({ ...option })),
|
|
759
|
+
...('hidden' in command && command.hidden ? { hidden: true } : {}),
|
|
760
|
+
};
|
|
761
|
+
});
|
|
762
|
+
return {
|
|
763
|
+
schemaVersion: exports.TAP_SCHEMA_VERSION,
|
|
764
|
+
cypressVersion,
|
|
765
|
+
commands,
|
|
766
|
+
};
|
|
767
|
+
};
|
|
768
|
+
exports.buildTapSchema = buildTapSchema;
|
|
769
|
+
const runMeta = {
|
|
770
|
+
name: 'run',
|
|
771
|
+
description: 'run (or rerun) a spec by its project-relative path',
|
|
772
|
+
details: `Runs (or reruns) a spec by its project-relative path, as listed by the specs
|
|
773
|
+
command. If no browser is open it launches one, switching to the spec's testing
|
|
774
|
+
type when needed, then requests the spec and returns immediately. This requests
|
|
775
|
+
the spec to begin, but completion of this command does not guarantee the spec
|
|
776
|
+
has started or finished.
|
|
777
|
+
|
|
778
|
+
Poll the 'status' command for progress. Read status first and keep its
|
|
779
|
+
startedAt: a verdict still carrying that same startedAt describes the spec
|
|
780
|
+
before this one, so wait for a verdict whose startedAt differs.`,
|
|
781
|
+
params: [
|
|
782
|
+
{ name: 'spec', type: 'string', required: true, description: 'project-relative spec path, as listed by the specs command' },
|
|
783
|
+
],
|
|
784
|
+
};
|
|
785
|
+
const sessionsMeta = {
|
|
786
|
+
name: 'sessions',
|
|
787
|
+
description: 'list the Cypress sessions this CLI can reach',
|
|
788
|
+
details: `Lists the Cypress sessions this CLI can reach. Cypress must be running locally,
|
|
789
|
+
support the \`tap\` command, and have an active Testing Type to be listed. A
|
|
790
|
+
session can be targeted by its PID in other commands using \`--session\`.
|
|
791
|
+
|
|
792
|
+
tap only supports Chromium based browsers (Chrome, Chromium, Edge, Electron).
|
|
793
|
+
A session running any other browser is listed as unsupported, and every other
|
|
794
|
+
tap command refuses it.`,
|
|
795
|
+
};
|
|
796
|
+
const statusMeta = {
|
|
797
|
+
name: 'status',
|
|
798
|
+
description: 'report where a Cypress session is in its lifecycle',
|
|
799
|
+
details: `Reports where a Cypress session is in its lifecycle, as JSON — for polling and
|
|
800
|
+
"where am I?" checks. Always exits 0 for a determinable stage (including "not
|
|
801
|
+
connected"); a poller branches on the \`status\` field.
|
|
802
|
+
|
|
803
|
+
Stages:
|
|
804
|
+
- not connected: Cypress cannot be reached.
|
|
805
|
+
- browser not selected: Cypress does not have a supported browser open.
|
|
806
|
+
- spec not selected: Cypress has a browser open but no spec is selected.
|
|
807
|
+
- loading: Cypress is loading the spec.
|
|
808
|
+
- running: Cypress is running the spec.
|
|
809
|
+
- passed: Cypress has completed the spec and all executed tests passed.
|
|
810
|
+
- failed: Cypress failed to run the spec, or has completed the spec and at least
|
|
811
|
+
one test failed.
|
|
812
|
+
|
|
813
|
+
Statuses other than passed and failed are intermediate states, indicating that
|
|
814
|
+
Cypress requires additional instruction or is still processing. Loading stays
|
|
815
|
+
loading for as long as the spec takes to build, so a poller needs its own
|
|
816
|
+
timeout; a spec whose build fails reports failed and carries the reason as
|
|
817
|
+
error — it ran no tests, so it reports no counts either.
|
|
818
|
+
|
|
819
|
+
From loading onwards the output carries \`startedAt\`, the spec every other field
|
|
820
|
+
describes (null while loading). In the event of a rerun the previous spec's
|
|
821
|
+
verdict remains visible until the new one begins, identical on every other
|
|
822
|
+
field, so use \`startedAt\` to validate whether output describes the previous
|
|
823
|
+
spec or the new one.`,
|
|
824
|
+
};
|
|
825
|
+
const specsMeta = {
|
|
826
|
+
name: 'specs',
|
|
827
|
+
description: 'list the specs the Cypress session can run, most recently modified first',
|
|
828
|
+
details: `Lists the specs the connected Cypress session can run, in descending order by
|
|
829
|
+
last modified. To find other testing types you must connect to a Cypress
|
|
830
|
+
session with that testing type active.`,
|
|
831
|
+
};
|
|
832
|
+
// Every selector these three take must resolve to exactly one element, so a
|
|
833
|
+
// reader is never silently shown one of several matches. Their help says so in
|
|
834
|
+
// the same words, and names the remedy the ambiguity error offers.
|
|
835
|
+
const SINGLE_ELEMENT_SELECTOR = 'a CSS selector matching exactly one element';
|
|
836
|
+
const AMBIGUOUS_SELECTOR_HELP = `The selector must match exactly one element. If it matches multiple elements
|
|
837
|
+
nothing is read: the command answers with a numbered list of the matches (up to
|
|
838
|
+
10), each with a unique selector. Re-run with --at <index> to read one of them,
|
|
839
|
+
or with whichever selector you meant.`;
|
|
840
|
+
// Where `dom` and `aria` read from with no selector: the document element only
|
|
841
|
+
// adds a <head> of script and style text; `--selector html` still reads it.
|
|
842
|
+
const TAP_DEFAULT_SELECTOR = 'body';
|
|
843
|
+
// Shared by the three selector-taking reads, so the way you pick one match out
|
|
844
|
+
// of several is identical across them.
|
|
845
|
+
const atField = {
|
|
846
|
+
name: 'at',
|
|
847
|
+
type: 'number',
|
|
848
|
+
required: false,
|
|
849
|
+
description: 'When a selector matches multiple elements, the 0-based index of the match to read',
|
|
850
|
+
};
|
|
851
|
+
const domMeta = {
|
|
852
|
+
name: 'dom',
|
|
853
|
+
description: 'read the application-under-test DOM as HTML: the page body, or a single element with its children by providing a CSS Selector',
|
|
854
|
+
details: `Reads the application-under-test DOM as HTML: a single element matching a CSS
|
|
855
|
+
Selector, and all of its children. Without --selector it reads the page body;
|
|
856
|
+
pass --selector html for the entire document. Output is capped browser-side so a
|
|
857
|
+
heavy page never ships megabytes at once.
|
|
858
|
+
|
|
859
|
+
${AMBIGUOUS_SELECTOR_HELP}`,
|
|
860
|
+
params: [],
|
|
861
|
+
options: [
|
|
862
|
+
{ ...selectorField, defaultValue: TAP_DEFAULT_SELECTOR, description: SINGLE_ELEMENT_SELECTOR },
|
|
863
|
+
{ name: 'max-chars', alias: 'm', type: 'number', required: false, defaultValue: 30000, description: 'cap on returned HTML characters' },
|
|
864
|
+
atField,
|
|
865
|
+
],
|
|
866
|
+
};
|
|
867
|
+
const ariaMeta = {
|
|
868
|
+
name: 'aria',
|
|
869
|
+
description: 'read the accessibility (ARIA) tree of the application-under-test page body, or the subtree at a selector',
|
|
870
|
+
details: `Reads the accessibility (ARIA) tree of the application under test, rooted at the
|
|
871
|
+
page body or at the element a CSS Selector matches. Structural and text-only
|
|
872
|
+
roles are dropped, leaving the compact role/name/state tree DevTools shows.
|
|
873
|
+
|
|
874
|
+
${AMBIGUOUS_SELECTOR_HELP}`,
|
|
875
|
+
params: [],
|
|
876
|
+
options: [
|
|
877
|
+
{ ...selectorField, defaultValue: TAP_DEFAULT_SELECTOR, description: SINGLE_ELEMENT_SELECTOR },
|
|
878
|
+
// The accessibility tree of a real app is deep; the cap keeps the projection
|
|
879
|
+
// affordable for an LLM. A selector roots it at a subtree for finer reads.
|
|
880
|
+
{ name: 'max-nodes', alias: 'm', type: 'number', required: false, defaultValue: 200, description: 'cap on the number of accessibility nodes returned' },
|
|
881
|
+
atField,
|
|
882
|
+
],
|
|
883
|
+
};
|
|
884
|
+
const inspectMeta = {
|
|
885
|
+
name: 'inspect',
|
|
886
|
+
description: 'retrieve the tag, attributes, computed styles, box model, and accessibility node of a specific element',
|
|
887
|
+
details: `Inspects a single element matching a CSS Selector: outputs the tag, attributes,
|
|
888
|
+
curated computed styles, box model, and that element's own accessibility node
|
|
889
|
+
— where \`aria\` reads the whole tree, this reads the one node.
|
|
1767
890
|
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
891
|
+
${AMBIGUOUS_SELECTOR_HELP}`,
|
|
892
|
+
params: [],
|
|
893
|
+
options: [
|
|
894
|
+
{ ...selectorField, required: true, description: `${SINGLE_ELEMENT_SELECTOR}, identifying the element to inspect` },
|
|
895
|
+
atField,
|
|
896
|
+
],
|
|
897
|
+
};
|
|
898
|
+
// CLI-native tap commands: implemented entirely in the CLI (session discovery
|
|
899
|
+
// over the filesystem, DOM/ARIA reads over CDP), so — unlike TAP_COMMANDS — they
|
|
900
|
+
// are never advertised by getSchema or exec'd on the session. Listed here in the
|
|
901
|
+
// order they appear in help, ahead of the schema-driven commands.
|
|
902
|
+
exports.TAP_NATIVE_COMMANDS = [
|
|
903
|
+
sessionsMeta,
|
|
904
|
+
statusMeta,
|
|
905
|
+
specsMeta,
|
|
906
|
+
runMeta,
|
|
907
|
+
domMeta,
|
|
908
|
+
ariaMeta,
|
|
909
|
+
inspectMeta,
|
|
910
|
+
];
|
|
911
|
+
const allTapCommands = [...exports.TAP_NATIVE_COMMANDS, ...exports.TAP_COMMANDS];
|
|
912
|
+
/** Every command name this CLI ships, whether it dispatches to the session or handles it itself. */
|
|
913
|
+
exports.KNOWN_COMMANDS = new Set(allTapCommands.map(({ name }) => name));
|
|
914
|
+
// The failure catalogue and the error both sides raise. Re-exported here for the
|
|
915
|
+
// same reason the result contracts are: this module is dependency-free, so the
|
|
916
|
+
// browser-side app can import it where it cannot import the package barrel.
|
|
917
|
+
__exportStar(requireTapErrors(), exports);
|
|
918
|
+
// Per-command result contracts live in `./contracts/`; re-exported here so the
|
|
919
|
+
// app's deep import of this module and the package barrel both reach them.
|
|
920
|
+
__exportStar(requireReporter(), exports);
|
|
921
|
+
__exportStar(requireCommand(), exports);
|
|
922
|
+
__exportStar(requirePinned(), exports);
|
|
923
|
+
__exportStar(requirePin(), exports);
|
|
924
|
+
__exportStar(requireResolveSelector(), exports);
|
|
925
|
+
|
|
926
|
+
} (tapContract));
|
|
927
|
+
return tapContract;
|
|
1774
928
|
}
|
|
1775
929
|
|
|
1776
|
-
var
|
|
930
|
+
var tapOperations = {};
|
|
1777
931
|
|
|
1778
|
-
var
|
|
932
|
+
var hasRequiredTapOperations;
|
|
1779
933
|
|
|
1780
|
-
function
|
|
1781
|
-
if (
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
934
|
+
function requireTapOperations () {
|
|
935
|
+
if (hasRequiredTapOperations) return tapOperations;
|
|
936
|
+
hasRequiredTapOperations = 1;
|
|
937
|
+
Object.defineProperty(tapOperations, "__esModule", { value: true });
|
|
938
|
+
tapOperations.tapRunSpecOperation = tapOperations.TapSpecsOperation = void 0;
|
|
939
|
+
tapOperations.TapSpecsOperation = {
|
|
940
|
+
operationName: 'TapSpecs',
|
|
941
|
+
query: /* GraphQL */ `
|
|
942
|
+
query TapSpecs {
|
|
943
|
+
currentProject {
|
|
944
|
+
specs {
|
|
945
|
+
relative
|
|
946
|
+
absolute
|
|
947
|
+
gitInfo {
|
|
948
|
+
lastModifiedHumanReadable
|
|
949
|
+
lastModifiedTimestamp
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
`,
|
|
955
|
+
};
|
|
956
|
+
const TAP_RUN_SPEC_QUERY = /* GraphQL */ `
|
|
957
|
+
mutation TapRunSpec($specPath: String!) {
|
|
958
|
+
runSpec(specPath: $specPath) {
|
|
959
|
+
__typename
|
|
960
|
+
... on RunSpecResponse {
|
|
961
|
+
testingType
|
|
962
|
+
browser {
|
|
963
|
+
displayName
|
|
964
|
+
}
|
|
965
|
+
spec {
|
|
966
|
+
relative
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
... on RunSpecError {
|
|
970
|
+
code
|
|
971
|
+
detailMessage
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
`;
|
|
976
|
+
const tapRunSpecOperation = (specPath) => {
|
|
977
|
+
return {
|
|
978
|
+
operationName: 'TapRunSpec',
|
|
979
|
+
query: TAP_RUN_SPEC_QUERY,
|
|
980
|
+
variables: { specPath },
|
|
981
|
+
};
|
|
982
|
+
};
|
|
983
|
+
tapOperations.tapRunSpecOperation = tapRunSpecOperation;
|
|
1794
984
|
|
|
1795
|
-
return
|
|
985
|
+
return tapOperations;
|
|
1796
986
|
}
|
|
1797
987
|
|
|
1798
|
-
var
|
|
988
|
+
var hasRequiredDist;
|
|
1799
989
|
|
|
1800
|
-
function
|
|
1801
|
-
if (
|
|
1802
|
-
|
|
1803
|
-
(function (exports
|
|
1804
|
-
var __createBinding = (
|
|
990
|
+
function requireDist () {
|
|
991
|
+
if (hasRequiredDist) return dist;
|
|
992
|
+
hasRequiredDist = 1;
|
|
993
|
+
(function (exports) {
|
|
994
|
+
var __createBinding = (dist && dist.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
1805
995
|
if (k2 === undefined) k2 = k;
|
|
1806
996
|
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
1807
997
|
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
@@ -1812,685 +1002,1506 @@ function requireTapContract () {
|
|
|
1812
1002
|
if (k2 === undefined) k2 = k;
|
|
1813
1003
|
o[k2] = m[k];
|
|
1814
1004
|
}));
|
|
1815
|
-
var __exportStar = (
|
|
1816
|
-
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports
|
|
1005
|
+
var __exportStar = (dist && dist.__exportStar) || function(m, exports) {
|
|
1006
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
1817
1007
|
};
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
exports$1.TAP_SCHEMA_VERSION = 1;
|
|
1821
|
-
exports$1.TAP_BINDING_GLOBAL = '__CYPRESS_TAP_BINDING__';
|
|
1822
|
-
exports$1.TAP_SCHEMA_METHOD = 'getSchema';
|
|
1823
|
-
exports$1.TAP_EXEC_METHOD = 'exec';
|
|
1824
|
-
// Options that recur across commands, defined once so their name, type, and help
|
|
1825
|
-
// text can't drift between the commands that expose them. `test-id` and
|
|
1826
|
-
// `command-id` are required in some commands and optional in others, so each use
|
|
1827
|
-
// spreads it and sets `required`; the rest are identical everywhere and used
|
|
1828
|
-
// directly.
|
|
1829
|
-
const testIdField = { name: 'test-id', alias: 't', type: 'string', description: 'test id, as listed by the reporter command' };
|
|
1830
|
-
const commandIdField = { name: 'command-id', alias: 'c', type: 'string', description: 'command id, as listed by the reporter command — a row number (test body first when duplicated), an e-prefixed event id, or hook-qualified like "h1:3"' };
|
|
1831
|
-
const attemptField = { name: 'attempt', alias: 'a', type: 'number', required: false, description: '1-based attempt (attempt 1 = first run); defaults to the latest' };
|
|
1832
|
-
const selectorField = { name: 'selector', alias: 'e', type: 'string', required: false, description: 'a CSS selector' };
|
|
1833
|
-
const commandMeta = {
|
|
1834
|
-
name: 'command',
|
|
1835
|
-
description: 'retrieve the reporter row, the pinnable DOM snapshots, and the console properties of a single command in a test',
|
|
1836
|
-
details: `Retrieves a single command from a test's command log: its reporter row, the DOM
|
|
1837
|
-
snapshots pinnable on it, and any associated console properties.`,
|
|
1838
|
-
params: [],
|
|
1839
|
-
options: [
|
|
1840
|
-
{ ...testIdField, required: true },
|
|
1841
|
-
{ ...commandIdField, required: true },
|
|
1842
|
-
// `--json` is the CLI's own flag, but this command declares it because it
|
|
1843
|
-
// also changes what the command returns: nothing is withheld from a payload
|
|
1844
|
-
// that is not being rendered for reading room.
|
|
1845
|
-
{ name: 'json', type: 'boolean', required: false, description: 'print the raw JSON result instead of the human-readable rendering. This will output every console property in full regardless of length.' },
|
|
1846
|
-
attemptField,
|
|
1847
|
-
{ name: 'depth', alias: 'd', type: 'string', required: false, description: 'how many levels of nested console properties to expand before summarizing the rest as "{n keys}" / "[n items]": a number or "all" (default 3, and a section over 8 rows folds at any depth unless this is passed)' },
|
|
1848
|
-
],
|
|
1008
|
+
var __importDefault = (dist && dist.__importDefault) || function (mod) {
|
|
1009
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
1849
1010
|
};
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1011
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
1012
|
+
exports.isCompatibleRecord = exports.tapGraphqlPath = exports.sessionProbePath = exports.recordPath = exports.cypressSessionsDir = exports.parseRecordPid = exports.recordFileName = exports.isTapSupportedBrowser = exports.TAP_SUPPORTED_BROWSER_FAMILY = exports.LEGACY_TAP_GRAPHQL_PATH = exports.TAP_GRAPHQL_ROUTE_PREFIX = exports.SESSION_ID_HEADER = exports.SESSIONS_ROUTE_PREFIX = exports.SESSIONS_DIRNAME = exports.MIN_SCHEMA_VERSION = exports.SCHEMA_VERSION = void 0;
|
|
1013
|
+
const path_1 = __importDefault(path);
|
|
1014
|
+
__exportStar(requireTapContract(), exports);
|
|
1015
|
+
__exportStar(requireTapOperations(), exports);
|
|
1016
|
+
exports.SCHEMA_VERSION = 1;
|
|
1017
|
+
exports.MIN_SCHEMA_VERSION = 1;
|
|
1018
|
+
exports.SESSIONS_DIRNAME = 'sessions';
|
|
1019
|
+
const RECORD_EXTENSION = '.json';
|
|
1020
|
+
exports.SESSIONS_ROUTE_PREFIX = '/__cypress/sessions/';
|
|
1021
|
+
exports.SESSION_ID_HEADER = 'x-cypress-session-id';
|
|
1022
|
+
// Fixed rather than namespaced, as the sessions route is: the CLI has to find it
|
|
1023
|
+
// knowing only the port from the record.
|
|
1024
|
+
exports.TAP_GRAPHQL_ROUTE_PREFIX = '/__cypress/tap/graphql/';
|
|
1025
|
+
// Where tap GraphQL lived before the fixed route, under the default `namespace`.
|
|
1026
|
+
// The CLI falls back to it to keep tapping sessions that predate the route, and it
|
|
1027
|
+
// can go once those are no longer supported.
|
|
1028
|
+
exports.LEGACY_TAP_GRAPHQL_PATH = '/__cypress/graphql';
|
|
1029
|
+
exports.TAP_SUPPORTED_BROWSER_FAMILY = 'chromium';
|
|
1030
|
+
// tap drives the browser over CDP, which only Chromium-based browsers speak. A
|
|
1031
|
+
// null family means no browser is open yet — nothing to call unsupported.
|
|
1032
|
+
const isTapSupportedBrowser = (browserFamily) => {
|
|
1033
|
+
return browserFamily === null || browserFamily === exports.TAP_SUPPORTED_BROWSER_FAMILY;
|
|
1866
1034
|
};
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
details: `Pins a command's DOM snapshot into the live application-under-test frame so the
|
|
1871
|
-
dom/aria/inspect commands can read it. Pass --clear to release.`,
|
|
1872
|
-
params: [],
|
|
1873
|
-
options: [
|
|
1874
|
-
{ ...testIdField, required: false },
|
|
1875
|
-
{ ...commandIdField, required: false },
|
|
1876
|
-
attemptField,
|
|
1877
|
-
{ name: 'at', type: 'string', required: false, description: 'which snapshot to pin: a name like "before"/"after" or a 1-based index; defaults to the last (the command’s final state). Re-run on the pinned command to switch snapshots without releasing the pin' },
|
|
1878
|
-
{ name: 'clear', type: 'boolean', required: false, description: 'release the current pin and restore the app to its pre-pin state' },
|
|
1879
|
-
],
|
|
1035
|
+
exports.isTapSupportedBrowser = isTapSupportedBrowser;
|
|
1036
|
+
const recordFileName = (pid) => {
|
|
1037
|
+
return `${pid}${RECORD_EXTENSION}`;
|
|
1880
1038
|
};
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1039
|
+
exports.recordFileName = recordFileName;
|
|
1040
|
+
const parseRecordPid = (entry) => {
|
|
1041
|
+
if (path_1.default.extname(entry) !== RECORD_EXTENSION) {
|
|
1042
|
+
return null;
|
|
1043
|
+
}
|
|
1044
|
+
const pid = Number(path_1.default.basename(entry, RECORD_EXTENSION));
|
|
1045
|
+
return Number.isInteger(pid) ? pid : null;
|
|
1887
1046
|
};
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
params: [
|
|
1892
|
-
{ name: 'selector', type: 'string', required: true, description: 'a CSS selector' },
|
|
1893
|
-
],
|
|
1894
|
-
options: [],
|
|
1895
|
-
hidden: true,
|
|
1047
|
+
exports.parseRecordPid = parseRecordPid;
|
|
1048
|
+
const cypressSessionsDir = (cacheRoot) => {
|
|
1049
|
+
return path_1.default.join(cacheRoot, exports.SESSIONS_DIRNAME);
|
|
1896
1050
|
};
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
exports
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
};
|
|
1051
|
+
exports.cypressSessionsDir = cypressSessionsDir;
|
|
1052
|
+
const recordPath = (cacheRoot, pid) => {
|
|
1053
|
+
return path_1.default.join((0, exports.cypressSessionsDir)(cacheRoot), (0, exports.recordFileName)(pid));
|
|
1054
|
+
};
|
|
1055
|
+
exports.recordPath = recordPath;
|
|
1056
|
+
const sessionProbePath = (sessionId) => {
|
|
1057
|
+
return `${exports.SESSIONS_ROUTE_PREFIX}${sessionId}`;
|
|
1058
|
+
};
|
|
1059
|
+
exports.sessionProbePath = sessionProbePath;
|
|
1060
|
+
const tapGraphqlPath = (operationName) => {
|
|
1061
|
+
return `${exports.TAP_GRAPHQL_ROUTE_PREFIX}${operationName}`;
|
|
1062
|
+
};
|
|
1063
|
+
exports.tapGraphqlPath = tapGraphqlPath;
|
|
1064
|
+
const isValidTestingType = (value) => {
|
|
1065
|
+
return value === 'e2e' || value === 'component' || value === null;
|
|
1066
|
+
};
|
|
1067
|
+
const isCompatibleRecord = (record) => {
|
|
1068
|
+
return Boolean(record)
|
|
1069
|
+
&& typeof record.schemaVersion === 'number'
|
|
1070
|
+
&& record.schemaVersion >= exports.MIN_SCHEMA_VERSION
|
|
1071
|
+
&& typeof record.pid === 'number'
|
|
1072
|
+
&& typeof record.projectRoot === 'string'
|
|
1073
|
+
&& Number.isInteger(record.serverPort)
|
|
1074
|
+
&& typeof record.sessionId === 'string'
|
|
1075
|
+
&& record.sessionId.length > 0
|
|
1076
|
+
&& isValidTestingType(record.testingType);
|
|
1924
1077
|
};
|
|
1925
|
-
exports
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
command. If no browser is open it launches one, switching to the spec's testing
|
|
1931
|
-
type when needed, then requests the spec and returns immediately. This requests
|
|
1932
|
-
the spec to begin, but completion of this command does not guarantee the spec
|
|
1933
|
-
has started or finished.
|
|
1078
|
+
exports.isCompatibleRecord = isCompatibleRecord;
|
|
1079
|
+
|
|
1080
|
+
} (dist));
|
|
1081
|
+
return dist;
|
|
1082
|
+
}
|
|
1934
1083
|
|
|
1935
|
-
|
|
1936
|
-
startedAt: a verdict still carrying that same startedAt describes the spec
|
|
1937
|
-
before this one, so wait for a verdict whose startedAt differs.`,
|
|
1938
|
-
params: [
|
|
1939
|
-
{ name: 'spec', type: 'string', required: true, description: 'project-relative spec path, as listed by the specs command' },
|
|
1940
|
-
],
|
|
1941
|
-
};
|
|
1942
|
-
const sessionsMeta = {
|
|
1943
|
-
name: 'sessions',
|
|
1944
|
-
description: 'list the Cypress sessions this CLI can reach',
|
|
1945
|
-
details: `Lists the Cypress sessions this CLI can reach. Cypress must be running locally,
|
|
1946
|
-
support the \`tap\` command, and have an active Testing Type to be listed. A
|
|
1947
|
-
session can be targeted by its PID in other commands using \`--session\`.
|
|
1084
|
+
var distExports = requireDist();
|
|
1948
1085
|
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1086
|
+
const debug$8 = Debug('cypress:cli:cypress-sessions');
|
|
1087
|
+
const PROBE_HOST = '127.0.0.1';
|
|
1088
|
+
const DEFAULT_PROBE_TIMEOUT_MS = 2000;
|
|
1089
|
+
const isPidAlive = (pid) => {
|
|
1090
|
+
try {
|
|
1091
|
+
process.kill(pid, 0);
|
|
1092
|
+
return true;
|
|
1093
|
+
}
|
|
1094
|
+
catch (err) {
|
|
1095
|
+
return (err === null || err === void 0 ? void 0 : err.code) === 'EPERM';
|
|
1096
|
+
}
|
|
1097
|
+
};
|
|
1098
|
+
// The server reports `cdpBrowserWsUrl` from in-memory state that it only clears
|
|
1099
|
+
// when the browser *process* exits, so a closed browser window whose process
|
|
1100
|
+
// lingers leaves the url stale. Hit the browser's own DevTools HTTP endpoint
|
|
1101
|
+
// (Chromium exposes `/json/version` whenever the CDP port is open) to confirm
|
|
1102
|
+
// the browser is really reachable before trusting the url.
|
|
1103
|
+
const cdpEndpointReachable = (cdpBrowserWsUrl, timeoutMs) => xvfb.__awaiter(void 0, void 0, void 0, function* () {
|
|
1104
|
+
let versionUrl;
|
|
1105
|
+
try {
|
|
1106
|
+
const { protocol, host } = new URL(cdpBrowserWsUrl);
|
|
1107
|
+
versionUrl = `${protocol === 'wss:' ? 'https:' : 'http:'}//${host}/json/version`;
|
|
1108
|
+
}
|
|
1109
|
+
catch (err) {
|
|
1110
|
+
debug$8('could not derive a CDP version url from %s: %o', cdpBrowserWsUrl, err);
|
|
1111
|
+
return false;
|
|
1112
|
+
}
|
|
1113
|
+
try {
|
|
1114
|
+
const response = yield fetch(versionUrl, { signal: AbortSignal.timeout(timeoutMs) });
|
|
1115
|
+
return response.ok;
|
|
1116
|
+
}
|
|
1117
|
+
catch (err) {
|
|
1118
|
+
debug$8('CDP endpoint unreachable at %s: %o', versionUrl, err);
|
|
1119
|
+
return false;
|
|
1120
|
+
}
|
|
1121
|
+
});
|
|
1122
|
+
const verifySessionRecord = (record_1, ...args_1) => xvfb.__awaiter(void 0, [record_1, ...args_1], void 0, function* (record, timeoutMs = DEFAULT_PROBE_TIMEOUT_MS) {
|
|
1123
|
+
const url = `http://${PROBE_HOST}:${record.serverPort}${distExports.sessionProbePath(record.sessionId)}`;
|
|
1124
|
+
try {
|
|
1125
|
+
const response = yield fetch(url, { signal: AbortSignal.timeout(timeoutMs) });
|
|
1126
|
+
if (response.status !== 200) {
|
|
1127
|
+
return null;
|
|
1128
|
+
}
|
|
1129
|
+
const live = yield response.json();
|
|
1130
|
+
if (live.sessionId !== record.sessionId) {
|
|
1131
|
+
return null;
|
|
1132
|
+
}
|
|
1133
|
+
const cdpBrowserWsUrl = typeof live.cdpBrowserWsUrl === 'string' ? live.cdpBrowserWsUrl : null;
|
|
1134
|
+
const attached = cdpBrowserWsUrl !== null && (yield cdpEndpointReachable(cdpBrowserWsUrl, timeoutMs));
|
|
1135
|
+
// The browser identity describes what the session has open, not what is
|
|
1136
|
+
// reachable over CDP — a browser tap cannot drive still has to be nameable.
|
|
1137
|
+
return Object.assign(Object.assign({}, record), { cdpBrowserWsUrl: attached ? cdpBrowserWsUrl : null, browserName: typeof live.browserName === 'string' ? live.browserName : null, browserFamily: typeof live.browserFamily === 'string' ? live.browserFamily : null, machineId: typeof live.machineId === 'string' ? live.machineId : null, userId: typeof live.userId === 'string' ? live.userId : null });
|
|
1138
|
+
}
|
|
1139
|
+
catch (err) {
|
|
1140
|
+
debug$8('liveness probe failed for pid %d on port %d: %o', record.pid, record.serverPort, err);
|
|
1141
|
+
return null;
|
|
1142
|
+
}
|
|
1143
|
+
});
|
|
1959
1144
|
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1145
|
+
const debug$7 = Debug('cypress:cli:cypress-sessions');
|
|
1146
|
+
const getSessionsDir = () => {
|
|
1147
|
+
return distExports.cypressSessionsDir(xvfb.stateModule.getCacheDir());
|
|
1148
|
+
};
|
|
1149
|
+
const listRecordFiles = (dir) => xvfb.__awaiter(void 0, void 0, void 0, function* () {
|
|
1150
|
+
let entries;
|
|
1151
|
+
try {
|
|
1152
|
+
entries = yield fs.readdir(dir);
|
|
1153
|
+
}
|
|
1154
|
+
catch (err) {
|
|
1155
|
+
if (err.code === 'ENOENT') {
|
|
1156
|
+
return [];
|
|
1157
|
+
}
|
|
1158
|
+
throw err;
|
|
1159
|
+
}
|
|
1160
|
+
const files = [];
|
|
1161
|
+
for (const entry of entries) {
|
|
1162
|
+
const pid = distExports.parseRecordPid(entry);
|
|
1163
|
+
if (pid !== null) {
|
|
1164
|
+
files.push({ path: path.join(dir, entry), pid });
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
return files;
|
|
1168
|
+
});
|
|
1169
|
+
const readCompatibleRecord = (filePath) => xvfb.__awaiter(void 0, void 0, void 0, function* () {
|
|
1170
|
+
let record;
|
|
1171
|
+
try {
|
|
1172
|
+
record = yield fs.readJson(filePath);
|
|
1173
|
+
}
|
|
1174
|
+
catch (err) {
|
|
1175
|
+
debug$7('could not read cypress sessions record %s: %o', filePath, err);
|
|
1176
|
+
return null;
|
|
1177
|
+
}
|
|
1178
|
+
if (!distExports.isCompatibleRecord(record)) {
|
|
1179
|
+
debug$7('incompatible cypress sessions record %s', filePath);
|
|
1180
|
+
return null;
|
|
1181
|
+
}
|
|
1182
|
+
return record;
|
|
1183
|
+
});
|
|
1184
|
+
// Reaps a record whose writer process is gone, returning whether it was dead.
|
|
1185
|
+
// Removal is best-effort: a file we can't delete (permissions, a Windows lock)
|
|
1186
|
+
// must not abort discovery of the other, live sessions, so the failure is
|
|
1187
|
+
// swallowed and the record is still reported dead.
|
|
1188
|
+
const reapIfDead = (file) => xvfb.__awaiter(void 0, void 0, void 0, function* () {
|
|
1189
|
+
const { path, pid } = file;
|
|
1190
|
+
if (isPidAlive(pid)) {
|
|
1191
|
+
return false;
|
|
1192
|
+
}
|
|
1193
|
+
yield fs.remove(path).catch((err) => {
|
|
1194
|
+
debug$7('failed to reap dead cypress sessions record %s: %o', path, err);
|
|
1195
|
+
});
|
|
1196
|
+
return true;
|
|
1197
|
+
});
|
|
1198
|
+
/**
|
|
1199
|
+
* Reads all the current cypress session records that are still live (i.e. the writer process is still running).
|
|
1200
|
+
* Reaps any dead records.
|
|
1201
|
+
*/
|
|
1202
|
+
const readLiveSessions = () => xvfb.__awaiter(void 0, void 0, void 0, function* () {
|
|
1203
|
+
const files = yield listRecordFiles(getSessionsDir());
|
|
1204
|
+
const records = yield Promise.all(files.map((file) => xvfb.__awaiter(void 0, void 0, void 0, function* () {
|
|
1205
|
+
const record = yield readCompatibleRecord(file.path);
|
|
1206
|
+
if (!record) {
|
|
1207
|
+
return null;
|
|
1208
|
+
}
|
|
1209
|
+
if (yield reapIfDead(file)) {
|
|
1210
|
+
return null;
|
|
1211
|
+
}
|
|
1212
|
+
return record;
|
|
1213
|
+
})));
|
|
1214
|
+
return records.filter((record) => record !== null);
|
|
1215
|
+
});
|
|
1216
|
+
const pruneDeadSessionRecords = () => xvfb.__awaiter(void 0, void 0, void 0, function* () {
|
|
1217
|
+
const files = yield listRecordFiles(getSessionsDir());
|
|
1218
|
+
const pruned = yield Promise.all(files.map((file) => reapIfDead(file)));
|
|
1219
|
+
const removed = pruned.filter(Boolean).length;
|
|
1220
|
+
debug$7('pruned %d dead cypress sessions record(s)', removed);
|
|
1221
|
+
return removed;
|
|
1222
|
+
});
|
|
1969
1223
|
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1224
|
+
dayjs.extend(relativeTime);
|
|
1225
|
+
// Subdirs under the cache root that are not binary version dirs. SESSIONS_DIRNAME
|
|
1226
|
+
// is sourced from cypress-sessions so a rename there can't silently make prune
|
|
1227
|
+
// treat the sessions dir as a stale binary cache and delete live records.
|
|
1228
|
+
const EXTERNAL_CACHE_ENTRIES = new Set(['bundles', distExports.SESSIONS_DIRNAME]);
|
|
1229
|
+
// output colors for the table
|
|
1230
|
+
const colors = {
|
|
1231
|
+
titles: chalk.white,
|
|
1232
|
+
dates: chalk.cyan,
|
|
1233
|
+
values: chalk.green,
|
|
1234
|
+
size: chalk.gray,
|
|
1235
|
+
};
|
|
1236
|
+
const logCachePath = () => {
|
|
1237
|
+
xvfb.loggerModule.always(xvfb.stateModule.getCacheDir());
|
|
1238
|
+
return undefined;
|
|
1239
|
+
};
|
|
1240
|
+
const clear = () => {
|
|
1241
|
+
return fs.remove(xvfb.stateModule.getCacheDir());
|
|
1242
|
+
};
|
|
1243
|
+
const prune = () => xvfb.__awaiter(void 0, void 0, void 0, function* () {
|
|
1244
|
+
const cacheDir = xvfb.stateModule.getCacheDir();
|
|
1245
|
+
const checkedInBinaryVersion = xvfb.util.pkgVersion();
|
|
1246
|
+
let deletedBinary = false;
|
|
1247
|
+
try {
|
|
1248
|
+
const versions = yield fs.readdir(cacheDir);
|
|
1249
|
+
for (const version of versions) {
|
|
1250
|
+
if (EXTERNAL_CACHE_ENTRIES.has(version))
|
|
1251
|
+
continue;
|
|
1252
|
+
if (version !== checkedInBinaryVersion) {
|
|
1253
|
+
deletedBinary = true;
|
|
1254
|
+
const versionDir = path.join(cacheDir, version);
|
|
1255
|
+
yield fs.remove(versionDir);
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
if (deletedBinary) {
|
|
1259
|
+
xvfb.loggerModule.always(`Deleted all binary caches except for the ${checkedInBinaryVersion} binary cache.`);
|
|
1260
|
+
}
|
|
1261
|
+
else {
|
|
1262
|
+
xvfb.loggerModule.always(`No binary caches found to prune.`);
|
|
1263
|
+
}
|
|
1264
|
+
yield pruneDeadSessionRecords();
|
|
1265
|
+
}
|
|
1266
|
+
catch (e) {
|
|
1267
|
+
if (e.code === 'ENOENT') {
|
|
1268
|
+
xvfb.loggerModule.always(`No Cypress cache was found at ${cacheDir}. Nothing to prune.`);
|
|
1269
|
+
return;
|
|
1270
|
+
}
|
|
1271
|
+
throw e;
|
|
1272
|
+
}
|
|
1273
|
+
});
|
|
1274
|
+
const fileSizeInMB = (size) => {
|
|
1275
|
+
return `${(size / 1024 / 1024).toFixed(1)}MB`;
|
|
1276
|
+
};
|
|
1277
|
+
/**
|
|
1278
|
+
* Collects all cached versions, finds when each was used
|
|
1279
|
+
* and prints a table with results to the terminal
|
|
1280
|
+
*/
|
|
1281
|
+
const list = (...args_1) => xvfb.__awaiter(void 0, [...args_1], void 0, function* (showSize = false) {
|
|
1282
|
+
const binaries = yield getCachedVersions(showSize);
|
|
1283
|
+
const head = [colors.titles('version'), colors.titles('last used')];
|
|
1284
|
+
if (showSize) {
|
|
1285
|
+
head.push(colors.titles('size'));
|
|
1286
|
+
}
|
|
1287
|
+
const table = new Table({
|
|
1288
|
+
head,
|
|
1289
|
+
});
|
|
1290
|
+
binaries.forEach((binary) => {
|
|
1291
|
+
const versionString = colors.values(binary.version);
|
|
1292
|
+
const lastUsed = binary.accessed ? colors.dates(binary.accessed) : 'unknown';
|
|
1293
|
+
const row = [versionString, lastUsed];
|
|
1294
|
+
if (showSize) {
|
|
1295
|
+
const size = colors.size(fileSizeInMB(binary.size));
|
|
1296
|
+
row.push(size);
|
|
1297
|
+
}
|
|
1298
|
+
return table.push(row);
|
|
1299
|
+
});
|
|
1300
|
+
xvfb.loggerModule.always(table.toString());
|
|
1301
|
+
});
|
|
1302
|
+
const getCachedVersions = (showSize) => xvfb.__awaiter(void 0, void 0, void 0, function* () {
|
|
1303
|
+
const cacheDir = xvfb.stateModule.getCacheDir();
|
|
1304
|
+
const versions = yield fs.readdir(cacheDir);
|
|
1305
|
+
const filteredVersions = versions.filter(xvfb.util.isSemver).map((version) => {
|
|
1306
|
+
return {
|
|
1307
|
+
version,
|
|
1308
|
+
folderPath: path.join(cacheDir, version),
|
|
1309
|
+
};
|
|
1310
|
+
});
|
|
1311
|
+
const binaries = [];
|
|
1312
|
+
for (const binary of filteredVersions) {
|
|
1313
|
+
const binaryDir = xvfb.stateModule.getBinaryDir(binary.version);
|
|
1314
|
+
const executable = xvfb.stateModule.getPathToExecutable(binaryDir);
|
|
1315
|
+
try {
|
|
1316
|
+
const stat = yield fs.stat(executable);
|
|
1317
|
+
const lastAccessedTime = _.get(stat, 'atime');
|
|
1318
|
+
if (lastAccessedTime) {
|
|
1319
|
+
const accessed = dayjs(lastAccessedTime).fromNow();
|
|
1320
|
+
// @ts-expect-error - accessed is not defined in the type
|
|
1321
|
+
binary.accessed = accessed;
|
|
1322
|
+
}
|
|
1323
|
+
// if no lastAccessedTime
|
|
1324
|
+
// the test runner has never been opened
|
|
1325
|
+
// or could be a test simulating missing timestamp
|
|
1326
|
+
}
|
|
1327
|
+
catch (e) {
|
|
1328
|
+
// could not find the binary or gets its stats
|
|
1329
|
+
// no-op
|
|
1330
|
+
}
|
|
1331
|
+
if (showSize) {
|
|
1332
|
+
const binaryDir = xvfb.stateModule.getBinaryDir(binary.version);
|
|
1333
|
+
const size = yield getSize(binaryDir);
|
|
1334
|
+
binaries.push(Object.assign(Object.assign({}, binary), { size }));
|
|
1335
|
+
}
|
|
1336
|
+
else {
|
|
1337
|
+
binaries.push(binary);
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
return binaries;
|
|
1341
|
+
});
|
|
1342
|
+
const cacheModule = {
|
|
1343
|
+
path: logCachePath,
|
|
1344
|
+
clear,
|
|
1345
|
+
prune,
|
|
1346
|
+
list,
|
|
1347
|
+
getCachedVersions,
|
|
1348
|
+
};
|
|
1975
1349
|
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
1350
|
+
/**
|
|
1351
|
+
* Throws an error with "details" property from
|
|
1352
|
+
* "errors" object.
|
|
1353
|
+
* @param {Object} details - Error details
|
|
1354
|
+
*/
|
|
1355
|
+
const throwInvalidOptionError = (details) => {
|
|
1356
|
+
if (!details) {
|
|
1357
|
+
details = xvfb.errors.unknownError;
|
|
1358
|
+
}
|
|
1359
|
+
// throw this error synchronously, it will be caught later on and
|
|
1360
|
+
// the details will be propagated to the promise chain
|
|
1361
|
+
const err = new Error();
|
|
1362
|
+
err.details = details;
|
|
1363
|
+
throw err;
|
|
1364
|
+
};
|
|
1365
|
+
/**
|
|
1366
|
+
* Selects exec args based on the configured `testingType`
|
|
1367
|
+
* @param {string} testingType The type of tests being executed
|
|
1368
|
+
* @returns {string[]} The array of new exec arguments
|
|
1369
|
+
*/
|
|
1370
|
+
const processTestingType = (options) => {
|
|
1371
|
+
if (options.e2e && options.component) {
|
|
1372
|
+
return throwInvalidOptionError(xvfb.errors.incompatibleTestTypeFlags);
|
|
1373
|
+
}
|
|
1374
|
+
if (options.testingType && (options.component || options.e2e)) {
|
|
1375
|
+
return throwInvalidOptionError(xvfb.errors.incompatibleTestTypeFlags);
|
|
1376
|
+
}
|
|
1377
|
+
if (options.testingType === 'component' || options.component || options.ct) {
|
|
1378
|
+
return ['--testing-type', 'component'];
|
|
1379
|
+
}
|
|
1380
|
+
if (options.testingType === 'e2e' || options.e2e) {
|
|
1381
|
+
return ['--testing-type', 'e2e'];
|
|
1382
|
+
}
|
|
1383
|
+
if (options.testingType) {
|
|
1384
|
+
return throwInvalidOptionError(xvfb.errors.invalidTestingType);
|
|
1385
|
+
}
|
|
1386
|
+
return [];
|
|
1387
|
+
};
|
|
1388
|
+
/**
|
|
1389
|
+
* Throws an error if configFile is string 'false' or boolean false
|
|
1390
|
+
* @param {*} options
|
|
1391
|
+
*/
|
|
1392
|
+
const checkConfigFile = (options) => {
|
|
1393
|
+
// CLI will parse as string, module API can pass in boolean
|
|
1394
|
+
if (options.configFile === 'false' || options.configFile === false) {
|
|
1395
|
+
throwInvalidOptionError(xvfb.errors.invalidConfigFile);
|
|
1396
|
+
}
|
|
1397
|
+
};
|
|
2015
1398
|
|
|
2016
|
-
$
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
1399
|
+
const debug$6 = Debug('cypress:cli');
|
|
1400
|
+
/**
|
|
1401
|
+
* Maps options collected by the CLI
|
|
1402
|
+
* and forms list of CLI arguments to the server.
|
|
1403
|
+
*
|
|
1404
|
+
* Note: there is lightweight validation, with errors
|
|
1405
|
+
* thrown synchronously.
|
|
1406
|
+
*
|
|
1407
|
+
* @returns {string[]} list of CLI arguments
|
|
1408
|
+
*/
|
|
1409
|
+
const processOpenOptions = (options = {}) => {
|
|
1410
|
+
// In addition to setting the project directory, setting the project option
|
|
1411
|
+
// here ultimately decides whether cypress is run in global mode or not.
|
|
1412
|
+
// It's first based off whether it's installed globally by npm/yarn (-g).
|
|
1413
|
+
// A global install can be overridden by the --project flag, putting Cypress
|
|
1414
|
+
// in project mode. A non-global install can be overridden by the --global
|
|
1415
|
+
// flag, putting it in global mode.
|
|
1416
|
+
if (!xvfb.util.isInstalledGlobally() && !options.global && !options.project) {
|
|
1417
|
+
options.project = process.cwd();
|
|
1418
|
+
}
|
|
1419
|
+
const args = [];
|
|
1420
|
+
if (options.config) {
|
|
1421
|
+
args.push('--config', options.config);
|
|
1422
|
+
}
|
|
1423
|
+
if (options.configFile !== undefined) {
|
|
1424
|
+
checkConfigFile(options);
|
|
1425
|
+
args.push('--config-file', options.configFile);
|
|
1426
|
+
}
|
|
1427
|
+
if (options.browser) {
|
|
1428
|
+
args.push('--browser', options.browser);
|
|
1429
|
+
}
|
|
1430
|
+
if (options.env) {
|
|
1431
|
+
args.push('--env', options.env);
|
|
1432
|
+
}
|
|
1433
|
+
if (options.expose) {
|
|
1434
|
+
args.push('--expose', options.expose);
|
|
1435
|
+
}
|
|
1436
|
+
if (options.port) {
|
|
1437
|
+
args.push('--port', options.port);
|
|
1438
|
+
}
|
|
1439
|
+
if (options.project) {
|
|
1440
|
+
args.push('--project', options.project);
|
|
1441
|
+
}
|
|
1442
|
+
if (options.global) {
|
|
1443
|
+
args.push('--global', options.global);
|
|
1444
|
+
}
|
|
1445
|
+
if (options.inspect) {
|
|
1446
|
+
args.push('--inspect');
|
|
1447
|
+
}
|
|
1448
|
+
if (options.inspectBrk) {
|
|
1449
|
+
args.push('--inspectBrk');
|
|
1450
|
+
}
|
|
1451
|
+
args.push(...processTestingType(options));
|
|
1452
|
+
debug$6('opening from options %j', options);
|
|
1453
|
+
debug$6('command line arguments %j', args);
|
|
1454
|
+
return args;
|
|
1455
|
+
};
|
|
1456
|
+
const start$3 = (...args_1) => xvfb.__awaiter(void 0, [...args_1], void 0, function* (options = {}) {
|
|
1457
|
+
function open() {
|
|
1458
|
+
try {
|
|
1459
|
+
const args = processOpenOptions(options);
|
|
1460
|
+
return spawn.start$1(args, {
|
|
1461
|
+
dev: options.dev,
|
|
1462
|
+
detached: Boolean(options.detached),
|
|
1463
|
+
});
|
|
1464
|
+
}
|
|
1465
|
+
catch (err) {
|
|
1466
|
+
if (err.details) {
|
|
1467
|
+
return xvfb.exitWithError(err.details)();
|
|
1468
|
+
}
|
|
1469
|
+
throw err;
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
if (options.dev) {
|
|
1473
|
+
return open();
|
|
1474
|
+
}
|
|
1475
|
+
yield spawn.start();
|
|
1476
|
+
return open();
|
|
1477
|
+
});
|
|
1478
|
+
var openModule = {
|
|
1479
|
+
start: start$3,
|
|
1480
|
+
processOpenOptions,
|
|
1481
|
+
};
|
|
2030
1482
|
|
|
2031
|
-
$
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
1483
|
+
const debug$5 = Debug('cypress:cli:run');
|
|
1484
|
+
/**
|
|
1485
|
+
* Typically a user passes a string path to the project.
|
|
1486
|
+
* But "cypress open" allows using `false` to open in global mode,
|
|
1487
|
+
* and the user can accidentally execute `cypress run --project false`
|
|
1488
|
+
* which should be invalid.
|
|
1489
|
+
*/
|
|
1490
|
+
const isValidProject = (v) => {
|
|
1491
|
+
if (typeof v === 'boolean') {
|
|
1492
|
+
return false;
|
|
1493
|
+
}
|
|
1494
|
+
if (v === '' || v === 'false' || v === 'true') {
|
|
1495
|
+
return false;
|
|
1496
|
+
}
|
|
1497
|
+
return true;
|
|
1498
|
+
};
|
|
1499
|
+
/**
|
|
1500
|
+
* Maps options collected by the CLI
|
|
1501
|
+
* and forms list of CLI arguments to the server.
|
|
1502
|
+
*
|
|
1503
|
+
* Note: there is lightweight validation, with errors
|
|
1504
|
+
* thrown synchronously.
|
|
1505
|
+
*
|
|
1506
|
+
* @returns {string[]} list of CLI arguments
|
|
1507
|
+
*/
|
|
1508
|
+
const processRunOptions = (options = {}) => {
|
|
1509
|
+
debug$5('processing run options %o', options);
|
|
1510
|
+
if (!isValidProject(options.project)) {
|
|
1511
|
+
debug$5('invalid project option %o', { project: options.project });
|
|
1512
|
+
return throwInvalidOptionError(xvfb.errors.invalidRunProjectPath);
|
|
1513
|
+
}
|
|
1514
|
+
const args = ['--run-project', options.project];
|
|
1515
|
+
if (options.autoCancelAfterFailures || options.autoCancelAfterFailures === 0 || options.autoCancelAfterFailures === false) {
|
|
1516
|
+
args.push('--auto-cancel-after-failures', options.autoCancelAfterFailures);
|
|
1517
|
+
}
|
|
1518
|
+
if (options.browser) {
|
|
1519
|
+
args.push('--browser', options.browser);
|
|
1520
|
+
}
|
|
1521
|
+
if (options.ciBuildId) {
|
|
1522
|
+
args.push('--ci-build-id', options.ciBuildId);
|
|
1523
|
+
}
|
|
1524
|
+
if (options.config) {
|
|
1525
|
+
args.push('--config', options.config);
|
|
1526
|
+
}
|
|
1527
|
+
if (options.configFile !== undefined) {
|
|
1528
|
+
checkConfigFile(options);
|
|
1529
|
+
args.push('--config-file', options.configFile);
|
|
1530
|
+
}
|
|
1531
|
+
if (options.env) {
|
|
1532
|
+
args.push('--env', options.env);
|
|
1533
|
+
}
|
|
1534
|
+
if (options.expose) {
|
|
1535
|
+
args.push('--expose', options.expose);
|
|
1536
|
+
}
|
|
1537
|
+
if (options.exit === false) {
|
|
1538
|
+
args.push('--no-exit');
|
|
1539
|
+
}
|
|
1540
|
+
if (options.group) {
|
|
1541
|
+
args.push('--group', options.group);
|
|
1542
|
+
}
|
|
1543
|
+
if (options.headed) {
|
|
1544
|
+
args.push('--headed', options.headed);
|
|
1545
|
+
}
|
|
1546
|
+
if (options.headless) {
|
|
1547
|
+
if (options.headed) {
|
|
1548
|
+
return throwInvalidOptionError(xvfb.errors.incompatibleHeadlessFlags);
|
|
1549
|
+
}
|
|
1550
|
+
args.push('--headed', String(!options.headless));
|
|
1551
|
+
}
|
|
1552
|
+
// if key is set use that - else attempt to find it by environment variable
|
|
1553
|
+
if (options.key == null) {
|
|
1554
|
+
debug$5('--key is not set, looking up environment variable CYPRESS_RECORD_KEY');
|
|
1555
|
+
options.key = xvfb.util.getEnv('CYPRESS_RECORD_KEY');
|
|
1556
|
+
}
|
|
1557
|
+
// if we have a key assume we're in record mode
|
|
1558
|
+
if (options.key) {
|
|
1559
|
+
args.push('--key', options.key);
|
|
1560
|
+
}
|
|
1561
|
+
if (options.outputPath) {
|
|
1562
|
+
args.push('--output-path', options.outputPath);
|
|
1563
|
+
}
|
|
1564
|
+
if (options.parallel) {
|
|
1565
|
+
args.push('--parallel');
|
|
1566
|
+
}
|
|
1567
|
+
if (options.passWithNoTests) {
|
|
1568
|
+
args.push('--pass-with-no-tests');
|
|
1569
|
+
}
|
|
1570
|
+
if (options.posixExitCodes) {
|
|
1571
|
+
args.push('--posix-exit-codes');
|
|
1572
|
+
}
|
|
1573
|
+
if (options.port) {
|
|
1574
|
+
args.push('--port', options.port);
|
|
1575
|
+
}
|
|
1576
|
+
if (options.quiet) {
|
|
1577
|
+
args.push('--quiet');
|
|
1578
|
+
}
|
|
1579
|
+
// if record is defined and we're not
|
|
1580
|
+
// already in ci mode, then send it up
|
|
1581
|
+
if (options.record != null) {
|
|
1582
|
+
args.push('--record', options.record);
|
|
1583
|
+
}
|
|
1584
|
+
// if we have a specific reporter push that into the args
|
|
1585
|
+
if (options.reporter) {
|
|
1586
|
+
args.push('--reporter', options.reporter);
|
|
1587
|
+
}
|
|
1588
|
+
// if we have a specific reporter push that into the args
|
|
1589
|
+
if (options.reporterOptions) {
|
|
1590
|
+
args.push('--reporter-options', options.reporterOptions);
|
|
1591
|
+
}
|
|
1592
|
+
if (options.runnerUi != null) {
|
|
1593
|
+
args.push('--runner-ui', options.runnerUi);
|
|
1594
|
+
}
|
|
1595
|
+
// if we have specific spec(s) push that into the args
|
|
1596
|
+
if (options.spec) {
|
|
1597
|
+
args.push('--spec', options.spec);
|
|
1598
|
+
}
|
|
1599
|
+
if (options.tag) {
|
|
1600
|
+
args.push('--tag', options.tag);
|
|
1601
|
+
}
|
|
1602
|
+
if (options.inspect) {
|
|
1603
|
+
args.push('--inspect');
|
|
1604
|
+
}
|
|
1605
|
+
if (options.inspectBrk) {
|
|
1606
|
+
args.push('--inspectBrk');
|
|
1607
|
+
}
|
|
1608
|
+
args.push(...processTestingType(options));
|
|
1609
|
+
return args;
|
|
1610
|
+
};
|
|
1611
|
+
const runModule = {
|
|
1612
|
+
processRunOptions,
|
|
1613
|
+
isValidProject,
|
|
1614
|
+
// resolves with the number of failed tests
|
|
1615
|
+
start() {
|
|
1616
|
+
return xvfb.__awaiter(this, arguments, void 0, function* (options = {}) {
|
|
1617
|
+
_.defaults(options, {
|
|
1618
|
+
key: null,
|
|
1619
|
+
spec: null,
|
|
1620
|
+
reporter: null,
|
|
1621
|
+
reporterOptions: null,
|
|
1622
|
+
project: process.cwd(),
|
|
1623
|
+
});
|
|
1624
|
+
function run() {
|
|
1625
|
+
try {
|
|
1626
|
+
const args = processRunOptions(options);
|
|
1627
|
+
debug$5('run to spawn.start args %j', args);
|
|
1628
|
+
return spawn.start$1(args, {
|
|
1629
|
+
dev: options.dev,
|
|
1630
|
+
});
|
|
1631
|
+
}
|
|
1632
|
+
catch (err) {
|
|
1633
|
+
if (err.details) {
|
|
1634
|
+
return xvfb.exitWithError(err.details)();
|
|
1635
|
+
}
|
|
1636
|
+
throw err;
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1639
|
+
if (options.dev) {
|
|
1640
|
+
return run();
|
|
1641
|
+
}
|
|
1642
|
+
yield spawn.start();
|
|
1643
|
+
return run();
|
|
1644
|
+
});
|
|
1645
|
+
},
|
|
1646
|
+
};
|
|
2047
1647
|
|
|
2048
|
-
$
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
1648
|
+
const debug$4 = Debug('cypress:cli');
|
|
1649
|
+
const defaultBaseUrl = 'https://download.cypress.io/';
|
|
1650
|
+
const defaultMaxRedirects = 10;
|
|
1651
|
+
const getProxyForUrlWithNpmConfig = (url) => {
|
|
1652
|
+
return proxyFromEnv.getProxyForUrl(url) ||
|
|
1653
|
+
process.env.npm_config_https_proxy ||
|
|
1654
|
+
process.env.npm_config_proxy ||
|
|
1655
|
+
null;
|
|
1656
|
+
};
|
|
1657
|
+
const getBaseUrl = () => {
|
|
1658
|
+
if (xvfb.util.getEnv('CYPRESS_DOWNLOAD_MIRROR')) {
|
|
1659
|
+
let baseUrl = xvfb.util.getEnv('CYPRESS_DOWNLOAD_MIRROR');
|
|
1660
|
+
if (!(baseUrl === null || baseUrl === void 0 ? void 0 : baseUrl.endsWith('/'))) {
|
|
1661
|
+
baseUrl += '/';
|
|
1662
|
+
}
|
|
1663
|
+
return baseUrl || defaultBaseUrl;
|
|
1664
|
+
}
|
|
1665
|
+
return defaultBaseUrl;
|
|
1666
|
+
};
|
|
1667
|
+
const getCA = () => xvfb.__awaiter(void 0, void 0, void 0, function* () {
|
|
1668
|
+
if (process.env.npm_config_cafile) {
|
|
1669
|
+
try {
|
|
1670
|
+
const caFileContent = yield fs.readFile(process.env.npm_config_cafile, 'utf8');
|
|
1671
|
+
return caFileContent;
|
|
1672
|
+
}
|
|
1673
|
+
catch (error) {
|
|
1674
|
+
debug$4('error reading ca file', error);
|
|
1675
|
+
return;
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
if (process.env.npm_config_ca) {
|
|
1679
|
+
return process.env.npm_config_ca;
|
|
1680
|
+
}
|
|
1681
|
+
return;
|
|
1682
|
+
});
|
|
1683
|
+
const prepend = (arch, urlPath, version) => {
|
|
1684
|
+
const endpoint = new URL(urlPath, getBaseUrl()).toString();
|
|
1685
|
+
const platform = os.platform();
|
|
1686
|
+
const pathTemplate = xvfb.util.getEnv('CYPRESS_DOWNLOAD_PATH_TEMPLATE', true);
|
|
1687
|
+
if ((platform === 'win32') && (arch === 'arm64')) {
|
|
1688
|
+
debug$4(`detected platform ${platform} architecture ${arch} combination`);
|
|
1689
|
+
arch = 'x64';
|
|
1690
|
+
debug$4(`overriding to download ${platform}-${arch} instead`);
|
|
1691
|
+
}
|
|
1692
|
+
return pathTemplate
|
|
1693
|
+
? (pathTemplate
|
|
1694
|
+
.replace(/\\?\$\{endpoint\}/g, endpoint)
|
|
1695
|
+
.replace(/\\?\$\{platform\}/g, platform)
|
|
1696
|
+
.replace(/\\?\$\{arch\}/g, arch)
|
|
1697
|
+
.replace(/\\?\$\{version\}/g, version))
|
|
1698
|
+
: `${endpoint}?platform=${platform}&arch=${arch}`;
|
|
1699
|
+
};
|
|
1700
|
+
const getUrl = (arch, version) => {
|
|
1701
|
+
if (_.isString(version) && version.match(/^https?:\/\/.*$/)) {
|
|
1702
|
+
debug$4('version is already an url', version);
|
|
1703
|
+
return version;
|
|
1704
|
+
}
|
|
1705
|
+
const urlPath = version ? `desktop/${version}` : 'desktop';
|
|
1706
|
+
return prepend(arch, urlPath, version || '');
|
|
1707
|
+
};
|
|
1708
|
+
const statusMessage = (err) => {
|
|
1709
|
+
return (err.statusCode
|
|
1710
|
+
? [err.statusCode, err.statusMessage].join(' - ')
|
|
1711
|
+
: err.toString());
|
|
1712
|
+
};
|
|
1713
|
+
const prettyDownloadErr = (err, url) => {
|
|
1714
|
+
const msg = commonTags.stripIndent `
|
|
1715
|
+
URL: ${url}
|
|
1716
|
+
${statusMessage(err)}
|
|
1717
|
+
`;
|
|
1718
|
+
debug$4(msg);
|
|
1719
|
+
return xvfb.throwFormErrorText(xvfb.errors.failedDownload)(msg);
|
|
1720
|
+
};
|
|
1721
|
+
/**
|
|
1722
|
+
* Checks checksum and file size for the given file. Allows both
|
|
1723
|
+
* values or just one of them to be checked.
|
|
1724
|
+
*/
|
|
1725
|
+
const verifyDownloadedFile = (filename, expectedSize, expectedChecksum) => xvfb.__awaiter(void 0, void 0, void 0, function* () {
|
|
1726
|
+
if (expectedSize && expectedChecksum) {
|
|
1727
|
+
debug$4('verifying checksum and file size');
|
|
1728
|
+
return Bluebird.join(xvfb.util.getFileChecksum(filename), xvfb.util.getFileSize(filename), (checksum, filesize) => {
|
|
1729
|
+
if (checksum === expectedChecksum && filesize === expectedSize) {
|
|
1730
|
+
debug$4('downloaded file has the expected checksum and size ✅');
|
|
1731
|
+
return;
|
|
1732
|
+
}
|
|
1733
|
+
debug$4('raising error: checksum or file size mismatch');
|
|
1734
|
+
const text = commonTags.stripIndent `
|
|
1735
|
+
Corrupted download
|
|
2086
1736
|
|
|
2087
|
-
|
|
1737
|
+
Expected downloaded file to have checksum: ${expectedChecksum}
|
|
1738
|
+
Computed checksum: ${checksum}
|
|
2088
1739
|
|
|
2089
|
-
|
|
1740
|
+
Expected downloaded file to have size: ${expectedSize}
|
|
1741
|
+
Computed size: ${filesize}
|
|
1742
|
+
`;
|
|
1743
|
+
debug$4(text);
|
|
1744
|
+
throw new Error(text);
|
|
1745
|
+
});
|
|
1746
|
+
}
|
|
1747
|
+
if (expectedChecksum) {
|
|
1748
|
+
debug$4('only checking expected file checksum %d', expectedChecksum);
|
|
1749
|
+
const checksum = yield xvfb.util.getFileChecksum(filename);
|
|
1750
|
+
if (checksum === expectedChecksum) {
|
|
1751
|
+
debug$4('downloaded file has the expected checksum ✅');
|
|
1752
|
+
return;
|
|
1753
|
+
}
|
|
1754
|
+
debug$4('raising error: file checksum mismatch');
|
|
1755
|
+
const text = commonTags.stripIndent `
|
|
1756
|
+
Corrupted download
|
|
2090
1757
|
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
gitInfo {
|
|
2105
|
-
lastModifiedHumanReadable
|
|
2106
|
-
lastModifiedTimestamp
|
|
2107
|
-
}
|
|
1758
|
+
Expected downloaded file to have checksum: ${expectedChecksum}
|
|
1759
|
+
Computed checksum: ${checksum}
|
|
1760
|
+
`;
|
|
1761
|
+
throw new Error(text);
|
|
1762
|
+
}
|
|
1763
|
+
if (expectedSize) {
|
|
1764
|
+
// maybe we don't have a checksum, but at least CDN returns content length
|
|
1765
|
+
// which we can check against the file size
|
|
1766
|
+
debug$4('only checking expected file size %d', expectedSize);
|
|
1767
|
+
const filesize = yield xvfb.util.getFileSize(filename);
|
|
1768
|
+
if (filesize === expectedSize) {
|
|
1769
|
+
debug$4('downloaded file has the expected size ✅');
|
|
1770
|
+
return;
|
|
2108
1771
|
}
|
|
2109
|
-
|
|
1772
|
+
debug$4('raising error: file size mismatch');
|
|
1773
|
+
const text = commonTags.stripIndent `
|
|
1774
|
+
Corrupted download
|
|
1775
|
+
|
|
1776
|
+
Expected downloaded file to have size: ${expectedSize}
|
|
1777
|
+
Computed size: ${filesize}
|
|
1778
|
+
`;
|
|
1779
|
+
throw new Error(text);
|
|
1780
|
+
}
|
|
1781
|
+
debug$4('downloaded file lacks checksum or size to verify');
|
|
1782
|
+
return;
|
|
1783
|
+
});
|
|
1784
|
+
// downloads from given url
|
|
1785
|
+
// return an object with
|
|
1786
|
+
// {filename: ..., downloaded: true}
|
|
1787
|
+
const downloadFromUrl = ({ url, downloadDestination, progress, ca, version, redirectTTL = defaultMaxRedirects }) => {
|
|
1788
|
+
if (redirectTTL <= 0) {
|
|
1789
|
+
return Promise.reject(new Error(commonTags.stripIndent `
|
|
1790
|
+
Failed downloading the Cypress binary.
|
|
1791
|
+
There were too many redirects. The default allowance is ${defaultMaxRedirects}.
|
|
1792
|
+
Maybe you got stuck in a redirect loop?
|
|
1793
|
+
`));
|
|
1794
|
+
}
|
|
1795
|
+
return new Bluebird((resolve, reject) => {
|
|
1796
|
+
const proxy = getProxyForUrlWithNpmConfig(url);
|
|
1797
|
+
debug$4('Downloading package', {
|
|
1798
|
+
url,
|
|
1799
|
+
proxy,
|
|
1800
|
+
downloadDestination,
|
|
1801
|
+
});
|
|
1802
|
+
if (ca) {
|
|
1803
|
+
debug$4('using custom CA details from npm config');
|
|
1804
|
+
}
|
|
1805
|
+
const reqOptions = Object.assign(Object.assign(Object.assign({ uri: url }, (proxy ? { proxy } : {})), (ca ? { agentOptions: { ca } } : {})), { method: 'GET', followRedirect: false });
|
|
1806
|
+
const req = request(reqOptions);
|
|
1807
|
+
// closure
|
|
1808
|
+
let started = null;
|
|
1809
|
+
let expectedSize;
|
|
1810
|
+
let expectedChecksum;
|
|
1811
|
+
requestProgress(req, {
|
|
1812
|
+
throttle: progress.throttle,
|
|
1813
|
+
})
|
|
1814
|
+
.on('response', (response) => {
|
|
1815
|
+
// we have computed checksum and filesize during test runner binary build
|
|
1816
|
+
// and have set it on the S3 object as user meta data, available via
|
|
1817
|
+
// these custom headers "x-amz-meta-..."
|
|
1818
|
+
// see https://github.com/cypress-io/cypress/pull/4092
|
|
1819
|
+
expectedSize = response.headers['x-amz-meta-size'] ||
|
|
1820
|
+
response.headers['content-length'];
|
|
1821
|
+
expectedChecksum = response.headers['x-amz-meta-checksum'];
|
|
1822
|
+
if (expectedChecksum) {
|
|
1823
|
+
debug$4('expected checksum %s', expectedChecksum);
|
|
1824
|
+
}
|
|
1825
|
+
if (expectedSize) {
|
|
1826
|
+
// convert from string (all Amazon custom headers are strings)
|
|
1827
|
+
expectedSize = Number(expectedSize);
|
|
1828
|
+
debug$4('expected file size %d', expectedSize);
|
|
1829
|
+
}
|
|
1830
|
+
// start counting now once we've gotten
|
|
1831
|
+
// response headers
|
|
1832
|
+
started = new Date();
|
|
1833
|
+
if (/^3/.test(response.statusCode)) {
|
|
1834
|
+
const redirectVersion = response.headers['x-version'];
|
|
1835
|
+
const redirectUrl = response.headers.location;
|
|
1836
|
+
debug$4('redirect version:', redirectVersion);
|
|
1837
|
+
debug$4('redirect url:', redirectUrl);
|
|
1838
|
+
downloadFromUrl({ url: redirectUrl, progress, ca, downloadDestination, version: redirectVersion, redirectTTL: redirectTTL - 1 })
|
|
1839
|
+
.then(resolve).catch(reject);
|
|
1840
|
+
// if our status code does not start with 200
|
|
1841
|
+
}
|
|
1842
|
+
else if (!/^2/.test(response.statusCode)) {
|
|
1843
|
+
debug$4('response code %d', response.statusCode);
|
|
1844
|
+
const err = new Error(commonTags.stripIndent `
|
|
1845
|
+
Failed downloading the Cypress binary.
|
|
1846
|
+
Response code: ${response.statusCode}
|
|
1847
|
+
Response message: ${response.statusMessage}
|
|
1848
|
+
`);
|
|
1849
|
+
reject(err);
|
|
1850
|
+
// status codes here are all 2xx
|
|
1851
|
+
}
|
|
1852
|
+
else {
|
|
1853
|
+
// We only enable this pipe connection when we know we've got a successful return
|
|
1854
|
+
// and handle the completion with verify and resolve
|
|
1855
|
+
// there was a possible race condition between end of request and close of writeStream
|
|
1856
|
+
// that is made ordered with this Promise.all
|
|
1857
|
+
Bluebird.all([new Bluebird((r) => {
|
|
1858
|
+
return response.pipe(fs.createWriteStream(downloadDestination).on('close', r));
|
|
1859
|
+
}), new Bluebird((r) => response.on('end', r))])
|
|
1860
|
+
.then(() => {
|
|
1861
|
+
debug$4('downloading finished');
|
|
1862
|
+
verifyDownloadedFile(downloadDestination, expectedSize, expectedChecksum)
|
|
1863
|
+
.then(() => debug$4('verified'))
|
|
1864
|
+
.then(() => resolve(version))
|
|
1865
|
+
.catch(reject);
|
|
1866
|
+
});
|
|
1867
|
+
}
|
|
1868
|
+
})
|
|
1869
|
+
.on('error', (e) => {
|
|
1870
|
+
if (e.code === 'ECONNRESET')
|
|
1871
|
+
return; // sometimes proxies give ECONNRESET but we don't care
|
|
1872
|
+
reject(e);
|
|
1873
|
+
})
|
|
1874
|
+
.on('progress', (state) => {
|
|
1875
|
+
// total time we've elapsed
|
|
1876
|
+
// starting on our first progress notification
|
|
1877
|
+
const elapsed = +new Date() - +started;
|
|
1878
|
+
// request-progress sends a value between 0 and 1
|
|
1879
|
+
const percentage = xvfb.util.convertPercentToPercentage(state.percent);
|
|
1880
|
+
const eta = xvfb.util.calculateEta(percentage, elapsed);
|
|
1881
|
+
// send up our percent and seconds remaining
|
|
1882
|
+
progress.onProgress(percentage, xvfb.util.secsRemaining(eta));
|
|
1883
|
+
});
|
|
1884
|
+
});
|
|
1885
|
+
};
|
|
1886
|
+
/**
|
|
1887
|
+
* Download Cypress.zip from external versionUrl to local file.
|
|
1888
|
+
* @param [string] version Could be "3.3.0" or full URL
|
|
1889
|
+
* @param [string] downloadDestination Local filename to save as
|
|
1890
|
+
*/
|
|
1891
|
+
const start$2 = (opts) => xvfb.__awaiter(void 0, void 0, void 0, function* () {
|
|
1892
|
+
let { version, downloadDestination, progress, redirectTTL } = opts;
|
|
1893
|
+
if (!downloadDestination) {
|
|
1894
|
+
assert.ok(_.isString(downloadDestination) && !_.isEmpty(downloadDestination), 'missing download dir');
|
|
1895
|
+
}
|
|
1896
|
+
if (!progress) {
|
|
1897
|
+
progress = { onProgress: () => {
|
|
1898
|
+
return {};
|
|
1899
|
+
} };
|
|
1900
|
+
}
|
|
1901
|
+
const arch = yield xvfb.util.getRealArch();
|
|
1902
|
+
const versionUrl = getUrl(arch, version);
|
|
1903
|
+
progress.throttle = 100;
|
|
1904
|
+
debug$4('needed Cypress version: %s', version);
|
|
1905
|
+
debug$4('source url %s', versionUrl);
|
|
1906
|
+
debug$4(`downloading cypress.zip to "${downloadDestination}"`);
|
|
1907
|
+
try {
|
|
1908
|
+
// ensure download dir exists
|
|
1909
|
+
yield fs.ensureDir(path.dirname(downloadDestination));
|
|
1910
|
+
const ca = yield getCA();
|
|
1911
|
+
return downloadFromUrl(Object.assign({ url: versionUrl, downloadDestination, progress, ca, version }, (redirectTTL ? { redirectTTL } : {})));
|
|
1912
|
+
}
|
|
1913
|
+
catch (err) {
|
|
1914
|
+
return prettyDownloadErr(err, versionUrl);
|
|
1915
|
+
}
|
|
1916
|
+
});
|
|
1917
|
+
const downloadModule = {
|
|
1918
|
+
start: start$2,
|
|
1919
|
+
getUrl,
|
|
1920
|
+
getProxyForUrlWithNpmConfig,
|
|
1921
|
+
getCA,
|
|
1922
|
+
};
|
|
1923
|
+
|
|
1924
|
+
const pipelineAsync = util.promisify(require$$0.pipeline);
|
|
1925
|
+
// Unix file mode masks for entries stored in zip's externalFileAttributes
|
|
1926
|
+
// (the high 16 bits when the file was zipped on a Unix host).
|
|
1927
|
+
const S_IFMT = 0o170000;
|
|
1928
|
+
const S_IFDIR = 0o040000;
|
|
1929
|
+
const S_IFLNK = 0o120000;
|
|
1930
|
+
// PATH_MAX on Linux/macOS is 4096; symlink targets larger than this are not
|
|
1931
|
+
// legal filesystem paths and almost certainly indicate a malformed or
|
|
1932
|
+
// malicious archive. The cap also prevents reading an arbitrarily large
|
|
1933
|
+
// "symlink" entry into memory.
|
|
1934
|
+
const MAX_SYMLINK_TARGET_BYTES = 4096;
|
|
1935
|
+
/**
|
|
1936
|
+
* Extracts the contents of a zip archive into the given destination directory.
|
|
1937
|
+
* Recreates directories, regular files, and symlinks while preserving Unix
|
|
1938
|
+
* file modes encoded in each entry's external attributes. Calls `onEntry` once
|
|
1939
|
+
* per archive entry processed. Refuses entries whose resolved path would
|
|
1940
|
+
* escape the destination directory.
|
|
1941
|
+
*/
|
|
1942
|
+
const extractWithYauzl = (zipFilePath, destDir, onEntry) => xvfb.__awaiter(void 0, void 0, void 0, function* () {
|
|
1943
|
+
const resolvedDest = path.resolve(destDir);
|
|
1944
|
+
yield new Promise((resolve, reject) => {
|
|
1945
|
+
// autoClose: false — `finish` below owns closing the zipfile, so we don't
|
|
1946
|
+
// want yauzl's internal end-listener closing it first and tripping a
|
|
1947
|
+
// double-close (EBADF) when we do.
|
|
1948
|
+
yauzl.open(zipFilePath, { lazyEntries: true, autoClose: false }, (err, zipFile) => {
|
|
1949
|
+
if (err) {
|
|
1950
|
+
return reject(err);
|
|
1951
|
+
}
|
|
1952
|
+
// `settled` guards against an in-flight `handleEntry` calling
|
|
1953
|
+
// `zipFile.readEntry()` on a now-closed handle when extraction has
|
|
1954
|
+
// already failed (e.g. yauzl emitted 'error' while we were writing
|
|
1955
|
+
// an entry to disk).
|
|
1956
|
+
let settled = false;
|
|
1957
|
+
const finish = _.once((err) => {
|
|
1958
|
+
var _a, _b;
|
|
1959
|
+
settled = true;
|
|
1960
|
+
(_a = zipFile.removeAllListeners) === null || _a === void 0 ? void 0 : _a.call(zipFile);
|
|
1961
|
+
(_b = zipFile.close) === null || _b === void 0 ? void 0 : _b.call(zipFile);
|
|
1962
|
+
if (err) {
|
|
1963
|
+
return reject(err);
|
|
1964
|
+
}
|
|
1965
|
+
return resolve();
|
|
1966
|
+
});
|
|
1967
|
+
// Normalize any thrown / emitted value into a real Error so that a
|
|
1968
|
+
// falsy rejection (e.g. `Promise.reject(undefined)`) doesn't get
|
|
1969
|
+
// misread by `finish` as a successful completion.
|
|
1970
|
+
const fail = (err) => {
|
|
1971
|
+
finish(err instanceof Error ? err : new Error(typeof err === 'string' && err ? err : 'zip extraction failed'));
|
|
1972
|
+
};
|
|
1973
|
+
zipFile.on('error', fail);
|
|
1974
|
+
zipFile.on('end', () => finish());
|
|
1975
|
+
zipFile.on('entry', (entry) => {
|
|
1976
|
+
handleEntry(zipFile, entry, resolvedDest)
|
|
1977
|
+
.then(() => {
|
|
1978
|
+
if (settled)
|
|
1979
|
+
return;
|
|
1980
|
+
onEntry();
|
|
1981
|
+
zipFile.readEntry();
|
|
1982
|
+
})
|
|
1983
|
+
.catch(fail);
|
|
1984
|
+
});
|
|
1985
|
+
zipFile.readEntry();
|
|
1986
|
+
});
|
|
1987
|
+
});
|
|
1988
|
+
});
|
|
1989
|
+
const handleEntry = (zipFile, entry, resolvedDest) => xvfb.__awaiter(void 0, void 0, void 0, function* () {
|
|
1990
|
+
const fileDest = path.resolve(resolvedDest, entry.fileName);
|
|
1991
|
+
// refuse anything that would write outside the install dir
|
|
1992
|
+
if (fileDest !== resolvedDest &&
|
|
1993
|
+
!fileDest.startsWith(resolvedDest + path.sep)) {
|
|
1994
|
+
throw new Error(`Refusing to extract entry outside of destination: ${entry.fileName}`);
|
|
2110
1995
|
}
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
1996
|
+
const unixMode = (entry.externalFileAttributes >>> 16) & 0xffff;
|
|
1997
|
+
// Some archivers mark directories by Unix mode bits instead of (or in
|
|
1998
|
+
// addition to) a trailing slash; honor both so we don't extract a
|
|
1999
|
+
// directory entry as a zero-byte file.
|
|
2000
|
+
const isDir = /\/$/.test(entry.fileName) || (unixMode & S_IFMT) === S_IFDIR;
|
|
2001
|
+
const isSymlink = (unixMode & S_IFMT) === S_IFLNK;
|
|
2002
|
+
if (isDir) {
|
|
2003
|
+
yield fsp.mkdir(fileDest, { recursive: true });
|
|
2004
|
+
return;
|
|
2005
|
+
}
|
|
2006
|
+
yield fsp.mkdir(path.dirname(fileDest), { recursive: true });
|
|
2007
|
+
if (isSymlink) {
|
|
2008
|
+
if (entry.uncompressedSize > MAX_SYMLINK_TARGET_BYTES) {
|
|
2009
|
+
throw new Error(`Refusing to extract symlink with target larger than ${MAX_SYMLINK_TARGET_BYTES} bytes: ${entry.fileName}`);
|
|
2121
2010
|
}
|
|
2122
|
-
|
|
2123
|
-
|
|
2011
|
+
const linkTarget = yield readEntryAsString(zipFile, entry, MAX_SYMLINK_TARGET_BYTES);
|
|
2012
|
+
const resolvedTarget = path.resolve(path.dirname(fileDest), linkTarget);
|
|
2013
|
+
if (resolvedTarget !== resolvedDest &&
|
|
2014
|
+
!resolvedTarget.startsWith(resolvedDest + path.sep)) {
|
|
2015
|
+
throw new Error(`Refusing to extract symlink pointing outside of destination: ${entry.fileName} -> ${linkTarget}`);
|
|
2124
2016
|
}
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
detailMessage
|
|
2129
|
-
}
|
|
2017
|
+
yield fsp.rm(fileDest, { recursive: true, force: true });
|
|
2018
|
+
yield fsp.symlink(linkTarget, fileDest);
|
|
2019
|
+
return;
|
|
2130
2020
|
}
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
}
|
|
2144
|
-
|
|
2145
|
-
|
|
2021
|
+
const readStream = yield new Promise((res, rej) => {
|
|
2022
|
+
zipFile.openReadStream(entry, (err, rs) => {
|
|
2023
|
+
if (err) {
|
|
2024
|
+
return rej(err);
|
|
2025
|
+
}
|
|
2026
|
+
return res(rs);
|
|
2027
|
+
});
|
|
2028
|
+
});
|
|
2029
|
+
// Preserve the Unix mode bits when present; otherwise fall back to a sane default.
|
|
2030
|
+
const fileMode = (unixMode & 0o7777) || 0o644;
|
|
2031
|
+
const writeStream = fs$1.createWriteStream(fileDest, { mode: fileMode });
|
|
2032
|
+
yield pipelineAsync(readStream, writeStream);
|
|
2033
|
+
});
|
|
2034
|
+
const readEntryAsString = (zipFile, entry, maxBytes) => {
|
|
2035
|
+
return new Promise((resolve, reject) => {
|
|
2036
|
+
zipFile.openReadStream(entry, (err, rs) => {
|
|
2037
|
+
if (err) {
|
|
2038
|
+
return reject(err);
|
|
2039
|
+
}
|
|
2040
|
+
const chunks = [];
|
|
2041
|
+
let received = 0;
|
|
2042
|
+
let bailed = false;
|
|
2043
|
+
const bail = (err) => {
|
|
2044
|
+
var _a;
|
|
2045
|
+
if (bailed)
|
|
2046
|
+
return;
|
|
2047
|
+
bailed = true;
|
|
2048
|
+
(_a = rs.destroy) === null || _a === void 0 ? void 0 : _a.call(rs, err);
|
|
2049
|
+
reject(err);
|
|
2050
|
+
};
|
|
2051
|
+
rs.on('data', (chunk) => {
|
|
2052
|
+
received += chunk.length;
|
|
2053
|
+
if (received > maxBytes) {
|
|
2054
|
+
bail(new Error(`Refusing to read entry body larger than ${maxBytes} bytes: ${entry.fileName}`));
|
|
2055
|
+
return;
|
|
2056
|
+
}
|
|
2057
|
+
chunks.push(chunk);
|
|
2058
|
+
});
|
|
2059
|
+
rs.on('end', () => {
|
|
2060
|
+
if (bailed)
|
|
2061
|
+
return;
|
|
2062
|
+
resolve(Buffer.concat(chunks).toString('utf8'));
|
|
2063
|
+
});
|
|
2064
|
+
rs.on('error', bail);
|
|
2065
|
+
});
|
|
2066
|
+
});
|
|
2067
|
+
};
|
|
2146
2068
|
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2069
|
+
const debug$3 = Debug('cypress:cli:unzip');
|
|
2070
|
+
const unzipTools = {
|
|
2071
|
+
extractWithYauzl,
|
|
2072
|
+
};
|
|
2073
|
+
// expose this function for simple testing
|
|
2074
|
+
const unzip = (_a) => xvfb.__awaiter(void 0, [_a], void 0, function* ({ zipFilePath, installDir, progress }) {
|
|
2075
|
+
debug$3('unzipping from %s', zipFilePath);
|
|
2076
|
+
debug$3('into', installDir);
|
|
2077
|
+
if (!zipFilePath) {
|
|
2078
|
+
throw new Error('Missing zip filename');
|
|
2079
|
+
}
|
|
2080
|
+
const startTime = Date.now();
|
|
2081
|
+
let yauzlDoneTime = 0;
|
|
2082
|
+
yield fs.ensureDir(installDir);
|
|
2083
|
+
yield new Promise((resolve, reject) => {
|
|
2084
|
+
// Open with lazyEntries so yauzl doesn't auto-emit entries (which would
|
|
2085
|
+
// require the fd to stay open for the duration of the OS-tool extraction).
|
|
2086
|
+
// We only need the entryCount here for the progress calculation.
|
|
2087
|
+
return yauzl.open(zipFilePath, { lazyEntries: true }, (err, zipFile) => {
|
|
2088
|
+
yauzlDoneTime = Date.now();
|
|
2089
|
+
if (err) {
|
|
2090
|
+
debug$3('error using yauzl %s', err.message);
|
|
2091
|
+
return reject(err);
|
|
2092
|
+
}
|
|
2093
|
+
const total = zipFile.entryCount;
|
|
2094
|
+
debug$3('zipFile entries count', total);
|
|
2095
|
+
// Close the count-only handle — the Node fallback re-opens the zip for extraction.
|
|
2096
|
+
zipFile.close();
|
|
2097
|
+
const started = new Date();
|
|
2098
|
+
let percent = 0;
|
|
2099
|
+
let count = 0;
|
|
2100
|
+
const notify = (percent) => {
|
|
2101
|
+
const elapsed = +new Date() - +started;
|
|
2102
|
+
const eta = xvfb.util.calculateEta(percent, elapsed);
|
|
2103
|
+
progress.onProgress(percent, xvfb.util.secsRemaining(eta));
|
|
2104
|
+
};
|
|
2105
|
+
const tick = () => {
|
|
2106
|
+
count += 1;
|
|
2107
|
+
percent = ((count / total) * 100);
|
|
2108
|
+
const displayPercent = percent.toFixed(0);
|
|
2109
|
+
return notify(Number(displayPercent));
|
|
2110
|
+
};
|
|
2111
|
+
const unzipWithNode = () => xvfb.__awaiter(void 0, void 0, void 0, function* () {
|
|
2112
|
+
debug$3('unzipping with node.js (slow)');
|
|
2113
|
+
try {
|
|
2114
|
+
yield unzipTools.extractWithYauzl(zipFilePath, installDir, tick);
|
|
2115
|
+
debug$3('node unzip finished');
|
|
2116
|
+
return resolve();
|
|
2117
|
+
}
|
|
2118
|
+
catch (err) {
|
|
2119
|
+
const error = err || new Error('Unknown error with Node extract tool');
|
|
2120
|
+
debug$3('error %s', error.message);
|
|
2121
|
+
return reject(error);
|
|
2122
|
+
}
|
|
2123
|
+
});
|
|
2124
|
+
const unzipFallback = _.once(unzipWithNode);
|
|
2125
|
+
const unzipWithUnzipTool = () => {
|
|
2126
|
+
debug$3('unzipping via `unzip`');
|
|
2127
|
+
const inflatingRe = /inflating:/;
|
|
2128
|
+
const sp = cp.spawn('unzip', ['-o', zipFilePath, '-d', installDir]);
|
|
2129
|
+
sp.on('error', (err) => {
|
|
2130
|
+
debug$3('unzip tool error: %s', err.message);
|
|
2131
|
+
unzipFallback();
|
|
2132
|
+
});
|
|
2133
|
+
sp.on('close', (code) => {
|
|
2134
|
+
debug$3('unzip tool close with code %d', code);
|
|
2135
|
+
if (code === 0) {
|
|
2136
|
+
percent = 100;
|
|
2137
|
+
notify(percent);
|
|
2138
|
+
return resolve();
|
|
2139
|
+
}
|
|
2140
|
+
debug$3('`unzip` failed %o', { code });
|
|
2141
|
+
return unzipFallback();
|
|
2142
|
+
});
|
|
2143
|
+
sp.stdout.on('data', (data) => {
|
|
2144
|
+
if (inflatingRe.test(data)) {
|
|
2145
|
+
return tick();
|
|
2146
|
+
}
|
|
2147
|
+
});
|
|
2148
|
+
sp.stderr.on('data', (data) => {
|
|
2149
|
+
debug$3('`unzip` stderr %s', data);
|
|
2150
|
+
});
|
|
2151
|
+
};
|
|
2152
|
+
// we attempt to first unzip with the native osx
|
|
2153
|
+
// ditto because its less likely to have problems
|
|
2154
|
+
// with corruption, symlinks, or icons causing failures
|
|
2155
|
+
// and can handle resource forks
|
|
2156
|
+
// http://automatica.com.au/2011/02/unzip-mac-os-x-zip-in-terminal/
|
|
2157
|
+
const unzipWithOsx = () => {
|
|
2158
|
+
debug$3('unzipping via `ditto`');
|
|
2159
|
+
const copyingFileRe = /^copying file/;
|
|
2160
|
+
const sp = cp.spawn('ditto', ['-xkV', zipFilePath, installDir]);
|
|
2161
|
+
// f-it just unzip with node
|
|
2162
|
+
sp.on('error', (err) => {
|
|
2163
|
+
debug$3(err.message);
|
|
2164
|
+
unzipFallback();
|
|
2165
|
+
});
|
|
2166
|
+
sp.on('close', (code) => {
|
|
2167
|
+
if (code === 0) {
|
|
2168
|
+
// make sure we get to 100% on the progress bar
|
|
2169
|
+
// because reading in lines is not really accurate
|
|
2170
|
+
percent = 100;
|
|
2171
|
+
notify(percent);
|
|
2172
|
+
return resolve();
|
|
2173
|
+
}
|
|
2174
|
+
debug$3('`ditto` failed %o', { code });
|
|
2175
|
+
return unzipFallback();
|
|
2176
|
+
});
|
|
2177
|
+
return readline.createInterface({
|
|
2178
|
+
input: sp.stderr,
|
|
2179
|
+
})
|
|
2180
|
+
.on('line', (line) => {
|
|
2181
|
+
if (copyingFileRe.test(line)) {
|
|
2182
|
+
return tick();
|
|
2183
|
+
}
|
|
2184
|
+
});
|
|
2185
|
+
};
|
|
2186
|
+
switch (os.platform()) {
|
|
2187
|
+
case 'darwin':
|
|
2188
|
+
return unzipWithOsx();
|
|
2189
|
+
case 'linux':
|
|
2190
|
+
return unzipWithUnzipTool();
|
|
2191
|
+
case 'win32':
|
|
2192
|
+
return unzipWithNode();
|
|
2193
|
+
default:
|
|
2194
|
+
return;
|
|
2195
|
+
}
|
|
2196
|
+
});
|
|
2197
|
+
});
|
|
2198
|
+
debug$3('unzip completed %o', {
|
|
2199
|
+
yauzlMs: yauzlDoneTime - startTime,
|
|
2200
|
+
unzipMs: Date.now() - yauzlDoneTime,
|
|
2201
|
+
});
|
|
2202
|
+
});
|
|
2203
|
+
function isMaybeWindowsMaxPathLengthError(err) {
|
|
2204
|
+
return os.platform() === 'win32' && err.code === 'ENOENT' && err.syscall === 'realpath';
|
|
2228
2205
|
}
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
const isPidAlive = (pid) => {
|
|
2236
|
-
try {
|
|
2237
|
-
process.kill(pid, 0);
|
|
2238
|
-
return true;
|
|
2239
|
-
}
|
|
2240
|
-
catch (err) {
|
|
2241
|
-
return (err === null || err === void 0 ? void 0 : err.code) === 'EPERM';
|
|
2242
|
-
}
|
|
2243
|
-
};
|
|
2244
|
-
// The server reports `cdpBrowserWsUrl` from in-memory state that it only clears
|
|
2245
|
-
// when the browser *process* exits, so a closed browser window whose process
|
|
2246
|
-
// lingers leaves the url stale. Hit the browser's own DevTools HTTP endpoint
|
|
2247
|
-
// (Chromium exposes `/json/version` whenever the CDP port is open) to confirm
|
|
2248
|
-
// the browser is really reachable before trusting the url.
|
|
2249
|
-
const cdpEndpointReachable = (cdpBrowserWsUrl, timeoutMs) => xvfb.__awaiter(void 0, void 0, void 0, function* () {
|
|
2250
|
-
let versionUrl;
|
|
2251
|
-
try {
|
|
2252
|
-
const { protocol, host } = new URL(cdpBrowserWsUrl);
|
|
2253
|
-
versionUrl = `${protocol === 'wss:' ? 'https:' : 'http:'}//${host}/json/version`;
|
|
2254
|
-
}
|
|
2255
|
-
catch (err) {
|
|
2256
|
-
debug$3('could not derive a CDP version url from %s: %o', cdpBrowserWsUrl, err);
|
|
2257
|
-
return false;
|
|
2258
|
-
}
|
|
2259
|
-
try {
|
|
2260
|
-
const response = yield fetch(versionUrl, { signal: AbortSignal.timeout(timeoutMs) });
|
|
2261
|
-
return response.ok;
|
|
2262
|
-
}
|
|
2263
|
-
catch (err) {
|
|
2264
|
-
debug$3('CDP endpoint unreachable at %s: %o', versionUrl, err);
|
|
2265
|
-
return false;
|
|
2206
|
+
const start$1 = (_a) => xvfb.__awaiter(void 0, [_a], void 0, function* ({ zipFilePath, installDir, progress }) {
|
|
2207
|
+
assert.ok(_.isString(installDir) && !_.isEmpty(installDir), 'missing installDir');
|
|
2208
|
+
if (!progress) {
|
|
2209
|
+
progress = { onProgress: () => {
|
|
2210
|
+
return {};
|
|
2211
|
+
} };
|
|
2266
2212
|
}
|
|
2267
|
-
});
|
|
2268
|
-
const verifySessionRecord = (record_1, ...args_1) => xvfb.__awaiter(void 0, [record_1, ...args_1], void 0, function* (record, timeoutMs = DEFAULT_PROBE_TIMEOUT_MS) {
|
|
2269
|
-
const url = `http://${PROBE_HOST}:${record.serverPort}${distExports.sessionProbePath(record.sessionId)}`;
|
|
2270
2213
|
try {
|
|
2271
|
-
const
|
|
2272
|
-
if (
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
const live = yield response.json();
|
|
2276
|
-
if (live.sessionId !== record.sessionId) {
|
|
2277
|
-
return null;
|
|
2214
|
+
const installDirExists = yield fs.pathExists(installDir);
|
|
2215
|
+
if (installDirExists) {
|
|
2216
|
+
debug$3('removing existing unzipped binary', installDir);
|
|
2217
|
+
yield fs.remove(installDir);
|
|
2278
2218
|
}
|
|
2279
|
-
|
|
2280
|
-
const attached = cdpBrowserWsUrl !== null && (yield cdpEndpointReachable(cdpBrowserWsUrl, timeoutMs));
|
|
2281
|
-
// The browser identity describes what the session has open, not what is
|
|
2282
|
-
// reachable over CDP — a browser tap cannot drive still has to be nameable.
|
|
2283
|
-
return Object.assign(Object.assign({}, record), { cdpBrowserWsUrl: attached ? cdpBrowserWsUrl : null, browserName: typeof live.browserName === 'string' ? live.browserName : null, browserFamily: typeof live.browserFamily === 'string' ? live.browserFamily : null, machineId: typeof live.machineId === 'string' ? live.machineId : null, userId: typeof live.userId === 'string' ? live.userId : null });
|
|
2219
|
+
yield unzip({ zipFilePath, installDir, progress });
|
|
2284
2220
|
}
|
|
2285
2221
|
catch (err) {
|
|
2286
|
-
|
|
2287
|
-
|
|
2222
|
+
const errorTemplate = isMaybeWindowsMaxPathLengthError(err) ?
|
|
2223
|
+
xvfb.errors.failedUnzipWindowsMaxPathLength
|
|
2224
|
+
: xvfb.errors.failedUnzip;
|
|
2225
|
+
yield xvfb.throwFormErrorText(errorTemplate)(err);
|
|
2288
2226
|
}
|
|
2289
2227
|
});
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2228
|
+
const unzipModule = {
|
|
2229
|
+
start: start$1,
|
|
2230
|
+
utils: {
|
|
2231
|
+
unzip,
|
|
2232
|
+
unzipTools,
|
|
2233
|
+
},
|
|
2294
2234
|
};
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
}
|
|
2304
|
-
throw err;
|
|
2235
|
+
|
|
2236
|
+
const debug$2 = Debug('cypress:cli:install');
|
|
2237
|
+
function _getBinaryUrlFromBuildInfo(version, arch, { commitSha, commitBranch }) {
|
|
2238
|
+
const platform = os.platform();
|
|
2239
|
+
if ((platform === 'win32') && (arch === 'arm64')) {
|
|
2240
|
+
debug$2(`detected platform ${platform} architecture ${arch} combination`);
|
|
2241
|
+
arch = 'x64';
|
|
2242
|
+
debug$2(`overriding to download ${platform}-${arch} pre-release binary instead`);
|
|
2305
2243
|
}
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2244
|
+
return `https://cdn.cypress.io/beta/binary/${version}/${platform}-${arch}/${commitBranch}-${commitSha}/cypress.zip`;
|
|
2245
|
+
}
|
|
2246
|
+
const alreadyInstalledMsg = () => {
|
|
2247
|
+
if (!xvfb.util.isPostInstall()) {
|
|
2248
|
+
xvfb.loggerModule.log(commonTags.stripIndent `
|
|
2249
|
+
Skipping installation:
|
|
2250
|
+
|
|
2251
|
+
Pass the ${chalk.yellow('--force')} option if you'd like to reinstall anyway.
|
|
2252
|
+
`);
|
|
2312
2253
|
}
|
|
2313
|
-
|
|
2254
|
+
};
|
|
2255
|
+
const displayCompletionMsg = () => {
|
|
2256
|
+
xvfb.loggerModule.log();
|
|
2257
|
+
xvfb.loggerModule.log('You can now open Cypress by running one of the following, depending on your package manager:');
|
|
2258
|
+
xvfb.loggerModule.log();
|
|
2259
|
+
xvfb.loggerModule.log(chalk.cyan('- npx cypress open'));
|
|
2260
|
+
xvfb.loggerModule.log(chalk.cyan('- yarn cypress open'));
|
|
2261
|
+
xvfb.loggerModule.log(chalk.cyan('- pnpm cypress open'));
|
|
2262
|
+
xvfb.loggerModule.log();
|
|
2263
|
+
xvfb.loggerModule.log(chalk.grey('https://on.cypress.io/opening-the-app'));
|
|
2264
|
+
xvfb.loggerModule.log();
|
|
2265
|
+
};
|
|
2266
|
+
const validateOS = () => xvfb.__awaiter(void 0, void 0, void 0, function* () {
|
|
2267
|
+
const platformInfo = yield xvfb.util.getPlatformInfo();
|
|
2268
|
+
return platformInfo.match(/(win32-x64|win32-arm64|linux-x64|linux-arm64|darwin-x64|darwin-arm64)/);
|
|
2314
2269
|
});
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2270
|
+
/**
|
|
2271
|
+
* Returns the version to install - either a string like `1.2.3` to be fetched
|
|
2272
|
+
* from the download server or a file path or HTTP URL.
|
|
2273
|
+
*/
|
|
2274
|
+
function getVersionOverride(version, { arch, envVarVersion, buildInfo }) {
|
|
2275
|
+
// let this environment variable reset the binary version we need
|
|
2276
|
+
if (envVarVersion) {
|
|
2277
|
+
return envVarVersion;
|
|
2319
2278
|
}
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2279
|
+
if (buildInfo && !buildInfo.stable) {
|
|
2280
|
+
xvfb.loggerModule.log(chalk.yellow(commonTags.stripIndent `
|
|
2281
|
+
${logSymbols.warning} Warning: You are installing a pre-release build of Cypress.
|
|
2282
|
+
|
|
2283
|
+
Bugs may be present which do not exist in production builds.
|
|
2284
|
+
|
|
2285
|
+
This build was created from:
|
|
2286
|
+
* Commit SHA: ${buildInfo.commitSha}
|
|
2287
|
+
* Commit Branch: ${buildInfo.commitBranch}
|
|
2288
|
+
* Commit Timestamp: ${buildInfo.commitDate}
|
|
2289
|
+
`));
|
|
2290
|
+
xvfb.loggerModule.log();
|
|
2291
|
+
return _getBinaryUrlFromBuildInfo(version, arch, buildInfo);
|
|
2323
2292
|
}
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2293
|
+
}
|
|
2294
|
+
function getEnvVarVersion() {
|
|
2295
|
+
if (!xvfb.util.getEnv('CYPRESS_INSTALL_BINARY'))
|
|
2296
|
+
return;
|
|
2297
|
+
// because passed file paths are often double quoted
|
|
2298
|
+
// and might have extra whitespace around, be robust and trim the string
|
|
2299
|
+
const trimAndRemoveDoubleQuotes = true;
|
|
2300
|
+
const envVarVersion = xvfb.util.getEnv('CYPRESS_INSTALL_BINARY', trimAndRemoveDoubleQuotes);
|
|
2301
|
+
debug$2('using environment variable CYPRESS_INSTALL_BINARY "%s"', envVarVersion);
|
|
2302
|
+
return envVarVersion;
|
|
2303
|
+
}
|
|
2304
|
+
const start = (...args_1) => xvfb.__awaiter(void 0, [...args_1], void 0, function* (options = {}) {
|
|
2305
|
+
debug$2('installing with options %j', options);
|
|
2306
|
+
const envVarVersion = getEnvVarVersion();
|
|
2307
|
+
if (envVarVersion === '0') {
|
|
2308
|
+
debug$2('environment variable CYPRESS_INSTALL_BINARY = 0, skipping install');
|
|
2309
|
+
xvfb.loggerModule.log(commonTags.stripIndent `
|
|
2310
|
+
${chalk.yellow('Note:')} Skipping binary installation: Environment variable CYPRESS_INSTALL_BINARY = 0.`);
|
|
2311
|
+
xvfb.loggerModule.log();
|
|
2312
|
+
return;
|
|
2327
2313
|
}
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
// Removal is best-effort: a file we can't delete (permissions, a Windows lock)
|
|
2332
|
-
// must not abort discovery of the other, live sessions, so the failure is
|
|
2333
|
-
// swallowed and the record is still reported dead.
|
|
2334
|
-
const reapIfDead = (file) => xvfb.__awaiter(void 0, void 0, void 0, function* () {
|
|
2335
|
-
const { path, pid } = file;
|
|
2336
|
-
if (isPidAlive(pid)) {
|
|
2337
|
-
return false;
|
|
2314
|
+
const pkgPath = xvfb.relativeToRepoRoot('package.json');
|
|
2315
|
+
if (!pkgPath) {
|
|
2316
|
+
return xvfb.throwFormErrorText('Could not find package.json for Cypress package to determine build information')();
|
|
2338
2317
|
}
|
|
2339
|
-
|
|
2340
|
-
|
|
2318
|
+
const { buildInfo, version } = JSON.parse(yield fsp.readFile(pkgPath, 'utf8'));
|
|
2319
|
+
_.defaults(options, {
|
|
2320
|
+
force: false,
|
|
2321
|
+
buildInfo,
|
|
2341
2322
|
});
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
}
|
|
2358
|
-
return record;
|
|
2359
|
-
})));
|
|
2360
|
-
return records.filter((record) => record !== null);
|
|
2361
|
-
});
|
|
2362
|
-
const pruneDeadSessionRecords = () => xvfb.__awaiter(void 0, void 0, void 0, function* () {
|
|
2363
|
-
const files = yield listRecordFiles(getSessionsDir());
|
|
2364
|
-
const pruned = yield Promise.all(files.map((file) => reapIfDead(file)));
|
|
2365
|
-
const removed = pruned.filter(Boolean).length;
|
|
2366
|
-
debug$2('pruned %d dead cypress sessions record(s)', removed);
|
|
2367
|
-
return removed;
|
|
2368
|
-
});
|
|
2369
|
-
|
|
2370
|
-
dayjs.extend(relativeTime);
|
|
2371
|
-
// Subdirs under the cache root that are not binary version dirs. SESSIONS_DIRNAME
|
|
2372
|
-
// is sourced from cypress-sessions so a rename there can't silently make prune
|
|
2373
|
-
// treat the sessions dir as a stale binary cache and delete live records.
|
|
2374
|
-
const EXTERNAL_CACHE_ENTRIES = new Set(['bundles', distExports.SESSIONS_DIRNAME]);
|
|
2375
|
-
// output colors for the table
|
|
2376
|
-
const colors = {
|
|
2377
|
-
titles: chalk.white,
|
|
2378
|
-
dates: chalk.cyan,
|
|
2379
|
-
values: chalk.green,
|
|
2380
|
-
size: chalk.gray,
|
|
2381
|
-
};
|
|
2382
|
-
const logCachePath = () => {
|
|
2383
|
-
xvfb.loggerModule.always(xvfb.stateModule.getCacheDir());
|
|
2384
|
-
return undefined;
|
|
2385
|
-
};
|
|
2386
|
-
const clear = () => {
|
|
2387
|
-
return fs.remove(xvfb.stateModule.getCacheDir());
|
|
2388
|
-
};
|
|
2389
|
-
const prune = () => xvfb.__awaiter(void 0, void 0, void 0, function* () {
|
|
2323
|
+
if (xvfb.util.getEnv('CYPRESS_CACHE_FOLDER')) {
|
|
2324
|
+
const envCache = xvfb.util.getEnv('CYPRESS_CACHE_FOLDER');
|
|
2325
|
+
xvfb.loggerModule.log(commonTags.stripIndent `
|
|
2326
|
+
${chalk.yellow('Note:')} Overriding Cypress cache directory to: ${chalk.cyan(envCache)}
|
|
2327
|
+
|
|
2328
|
+
Previous installs of Cypress may not be found.
|
|
2329
|
+
`);
|
|
2330
|
+
xvfb.loggerModule.log();
|
|
2331
|
+
}
|
|
2332
|
+
const pkgVersion = xvfb.util.pkgVersion();
|
|
2333
|
+
const arch = yield xvfb.util.getRealArch();
|
|
2334
|
+
const versionOverride = getVersionOverride(version, { arch, envVarVersion, buildInfo: options.buildInfo });
|
|
2335
|
+
const versionToInstall = versionOverride || pkgVersion;
|
|
2336
|
+
debug$2('version in package.json is %s, version to install is %s', pkgVersion, versionToInstall);
|
|
2337
|
+
const installDir = xvfb.stateModule.getVersionDir(pkgVersion, options.buildInfo);
|
|
2390
2338
|
const cacheDir = xvfb.stateModule.getCacheDir();
|
|
2391
|
-
const
|
|
2392
|
-
|
|
2339
|
+
const binaryDir = xvfb.stateModule.getBinaryDir(pkgVersion);
|
|
2340
|
+
if (!(yield validateOS())) {
|
|
2341
|
+
return xvfb.throwFormErrorText(xvfb.errors.invalidOS)();
|
|
2342
|
+
}
|
|
2393
2343
|
try {
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
}
|
|
2404
|
-
if (deletedBinary) {
|
|
2405
|
-
xvfb.loggerModule.always(`Deleted all binary caches except for the ${checkedInBinaryVersion} binary cache.`);
|
|
2344
|
+
yield fs.ensureDir(cacheDir);
|
|
2345
|
+
}
|
|
2346
|
+
catch (err) {
|
|
2347
|
+
if (err instanceof Error && 'code' in err && err.code === 'EACCES') {
|
|
2348
|
+
return xvfb.throwFormErrorText(xvfb.errors.invalidCacheDirectory)(commonTags.stripIndent `
|
|
2349
|
+
Failed to access ${chalk.cyan(cacheDir)}:
|
|
2350
|
+
|
|
2351
|
+
${err.message}
|
|
2352
|
+
`);
|
|
2406
2353
|
}
|
|
2407
2354
|
else {
|
|
2408
|
-
|
|
2409
|
-
}
|
|
2410
|
-
yield pruneDeadSessionRecords();
|
|
2411
|
-
}
|
|
2412
|
-
catch (e) {
|
|
2413
|
-
if (e.code === 'ENOENT') {
|
|
2414
|
-
xvfb.loggerModule.always(`No Cypress cache was found at ${cacheDir}. Nothing to prune.`);
|
|
2415
|
-
return;
|
|
2355
|
+
throw err;
|
|
2416
2356
|
}
|
|
2417
|
-
throw e;
|
|
2418
|
-
}
|
|
2419
|
-
});
|
|
2420
|
-
const fileSizeInMB = (size) => {
|
|
2421
|
-
return `${(size / 1024 / 1024).toFixed(1)}MB`;
|
|
2422
|
-
};
|
|
2423
|
-
/**
|
|
2424
|
-
* Collects all cached versions, finds when each was used
|
|
2425
|
-
* and prints a table with results to the terminal
|
|
2426
|
-
*/
|
|
2427
|
-
const list = (...args_1) => xvfb.__awaiter(void 0, [...args_1], void 0, function* (showSize = false) {
|
|
2428
|
-
const binaries = yield getCachedVersions(showSize);
|
|
2429
|
-
const head = [colors.titles('version'), colors.titles('last used')];
|
|
2430
|
-
if (showSize) {
|
|
2431
|
-
head.push(colors.titles('size'));
|
|
2432
2357
|
}
|
|
2433
|
-
const
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
const row = [versionString, lastUsed];
|
|
2440
|
-
if (showSize) {
|
|
2441
|
-
const size = colors.size(fileSizeInMB(binary.size));
|
|
2442
|
-
row.push(size);
|
|
2358
|
+
const binaryPkg = yield xvfb.stateModule.getBinaryPkgAsync(binaryDir);
|
|
2359
|
+
const binaryVersion = yield xvfb.stateModule.getBinaryPkgVersion(binaryPkg);
|
|
2360
|
+
const shouldInstall = () => {
|
|
2361
|
+
if (!binaryVersion) {
|
|
2362
|
+
debug$2('no binary installed under cli version');
|
|
2363
|
+
return true;
|
|
2443
2364
|
}
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
return {
|
|
2453
|
-
version,
|
|
2454
|
-
folderPath: path.join(cacheDir, version),
|
|
2455
|
-
};
|
|
2456
|
-
});
|
|
2457
|
-
const binaries = [];
|
|
2458
|
-
for (const binary of filteredVersions) {
|
|
2459
|
-
const binaryDir = xvfb.stateModule.getBinaryDir(binary.version);
|
|
2460
|
-
const executable = xvfb.stateModule.getPathToExecutable(binaryDir);
|
|
2461
|
-
try {
|
|
2462
|
-
const stat = yield fs.stat(executable);
|
|
2463
|
-
const lastAccessedTime = _.get(stat, 'atime');
|
|
2464
|
-
if (lastAccessedTime) {
|
|
2465
|
-
const accessed = dayjs(lastAccessedTime).fromNow();
|
|
2466
|
-
// @ts-expect-error - accessed is not defined in the type
|
|
2467
|
-
binary.accessed = accessed;
|
|
2468
|
-
}
|
|
2469
|
-
// if no lastAccessedTime
|
|
2470
|
-
// the test runner has never been opened
|
|
2471
|
-
// or could be a test simulating missing timestamp
|
|
2365
|
+
xvfb.loggerModule.log();
|
|
2366
|
+
xvfb.loggerModule.log(commonTags.stripIndent `
|
|
2367
|
+
Cypress ${chalk.green(binaryVersion)} is installed in ${chalk.cyan(installDir)}
|
|
2368
|
+
`);
|
|
2369
|
+
xvfb.loggerModule.log();
|
|
2370
|
+
if (options.force) {
|
|
2371
|
+
debug$2('performing force install over existing binary');
|
|
2372
|
+
return true;
|
|
2472
2373
|
}
|
|
2473
|
-
|
|
2474
|
-
//
|
|
2475
|
-
|
|
2374
|
+
if ((binaryVersion === versionToInstall) || !xvfb.util.isSemver(versionToInstall)) {
|
|
2375
|
+
// our version matches, tell the user this is a noop
|
|
2376
|
+
alreadyInstalledMsg();
|
|
2377
|
+
return false;
|
|
2476
2378
|
}
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2379
|
+
return true;
|
|
2380
|
+
};
|
|
2381
|
+
// noop if we've been told not to download
|
|
2382
|
+
if (!shouldInstall()) {
|
|
2383
|
+
return debug$2('Not downloading or installing binary');
|
|
2384
|
+
}
|
|
2385
|
+
if (envVarVersion) {
|
|
2386
|
+
xvfb.loggerModule.log(chalk.yellow(commonTags.stripIndent `
|
|
2387
|
+
${logSymbols.warning} Warning: Forcing a binary version different than the default.
|
|
2388
|
+
|
|
2389
|
+
The CLI expected to install version: ${chalk.green(pkgVersion)}
|
|
2390
|
+
|
|
2391
|
+
Instead we will install version: ${chalk.green(versionToInstall)}
|
|
2392
|
+
|
|
2393
|
+
These versions may not work properly together.
|
|
2394
|
+
`));
|
|
2395
|
+
xvfb.loggerModule.log();
|
|
2396
|
+
}
|
|
2397
|
+
const getLocalFilePath = () => xvfb.__awaiter(void 0, void 0, void 0, function* () {
|
|
2398
|
+
// see if version supplied is a path to a binary
|
|
2399
|
+
if (yield fs.pathExists(versionToInstall)) {
|
|
2400
|
+
return path.extname(versionToInstall) === '.zip' ? versionToInstall : false;
|
|
2481
2401
|
}
|
|
2482
|
-
|
|
2483
|
-
|
|
2402
|
+
const possibleFile = xvfb.util.formAbsolutePath(versionToInstall);
|
|
2403
|
+
debug$2('checking local file', possibleFile, 'cwd', process.cwd());
|
|
2404
|
+
// if this exists return the path to it
|
|
2405
|
+
// else false
|
|
2406
|
+
if ((yield fs.pathExists(possibleFile)) && path.extname(possibleFile) === '.zip') {
|
|
2407
|
+
return possibleFile;
|
|
2484
2408
|
}
|
|
2409
|
+
return false;
|
|
2410
|
+
});
|
|
2411
|
+
const pathToLocalFile = yield getLocalFilePath();
|
|
2412
|
+
const tasks = pathToLocalFile ?
|
|
2413
|
+
installFromLocal(pathToLocalFile, installDir) :
|
|
2414
|
+
installFromRemote(versionToInstall, installDir);
|
|
2415
|
+
if (options.force) {
|
|
2416
|
+
debug$2('Cypress already installed at', installDir);
|
|
2417
|
+
debug$2('but the installation was forced');
|
|
2485
2418
|
}
|
|
2486
|
-
|
|
2419
|
+
// let the user know what version of cypress we're downloading!
|
|
2420
|
+
xvfb.loggerModule.log(`Installing Cypress ${chalk.gray(`(version: ${versionToInstall})`)}`);
|
|
2421
|
+
xvfb.loggerModule.log();
|
|
2422
|
+
const taskRunner = new listr2.Listr(tasks, Object.assign(Object.assign({
|
|
2423
|
+
// In CI we want timestamped, line-per-event output. Locally,
|
|
2424
|
+
// the default in-place spinner is the better experience.
|
|
2425
|
+
renderer: xvfb.util.isCi() ? 'verbose' : 'default' }, (xvfb.util.isCi() && { rendererOptions: { timestamp: listr2.PRESET_TIMESTAMP } })), { silentRendererCondition: () => xvfb.loggerModule.logLevel() === 'silent' }));
|
|
2426
|
+
yield taskRunner.run();
|
|
2427
|
+
// delay 1 sec for UX, unless we are testing
|
|
2428
|
+
yield timers.setTimeout(1000);
|
|
2429
|
+
displayCompletionMsg();
|
|
2487
2430
|
});
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2431
|
+
function downloadArchive(version, downloadDestination) {
|
|
2432
|
+
const inProgressTitle = 'Downloading Cypress';
|
|
2433
|
+
const completedTitle = chalk.green('Downloaded Cypress');
|
|
2434
|
+
return {
|
|
2435
|
+
title: xvfb.util.titleize(inProgressTitle),
|
|
2436
|
+
task: (ctx, task) => xvfb.__awaiter(this, void 0, void 0, function* () {
|
|
2437
|
+
yield downloadModule.start({
|
|
2438
|
+
version,
|
|
2439
|
+
downloadDestination,
|
|
2440
|
+
progress: {
|
|
2441
|
+
throttle: 100,
|
|
2442
|
+
onProgress: (percentComplete, remaining) => {
|
|
2443
|
+
task.title = progressTitle(inProgressTitle, percentComplete, remaining);
|
|
2444
|
+
},
|
|
2445
|
+
},
|
|
2446
|
+
});
|
|
2447
|
+
debug$2(`finished downloading file: ${downloadDestination}`);
|
|
2448
|
+
task.title = xvfb.util.titleize(completedTitle);
|
|
2449
|
+
}),
|
|
2450
|
+
};
|
|
2451
|
+
}
|
|
2452
|
+
function installFromLocal(pathToLocalFile, installDir) {
|
|
2453
|
+
const zipFilePath = path.resolve(pathToLocalFile);
|
|
2454
|
+
debug$2('found local file at', zipFilePath);
|
|
2455
|
+
debug$2('skipping download');
|
|
2456
|
+
return [
|
|
2457
|
+
unzipArchive(zipFilePath, installDir),
|
|
2458
|
+
];
|
|
2459
|
+
}
|
|
2460
|
+
function installFromRemote(version, installDir) {
|
|
2461
|
+
const downloadDestination = path.join(os.tmpdir(), `cypress-${process.pid}.zip`);
|
|
2462
|
+
debug$2('preparing to download and unzip version ', version, 'to path', installDir);
|
|
2463
|
+
return [
|
|
2464
|
+
downloadArchive(version, downloadDestination),
|
|
2465
|
+
unzipArchive(downloadDestination, installDir),
|
|
2466
|
+
cleanup(downloadDestination, installDir),
|
|
2467
|
+
];
|
|
2468
|
+
}
|
|
2469
|
+
function unzipArchive(zipFilePath, installDir) {
|
|
2470
|
+
const inProgressTitle = 'Unzipping Cypress';
|
|
2471
|
+
const completedTitle = chalk.green('Unzipped Cypress');
|
|
2472
|
+
return {
|
|
2473
|
+
title: xvfb.util.titleize(inProgressTitle),
|
|
2474
|
+
task: (ctx, task) => xvfb.__awaiter(this, void 0, void 0, function* () {
|
|
2475
|
+
yield unzipModule.start({
|
|
2476
|
+
zipFilePath,
|
|
2477
|
+
installDir,
|
|
2478
|
+
progress: {
|
|
2479
|
+
onProgress: (percentComplete, remaining) => {
|
|
2480
|
+
task.title = progressTitle(inProgressTitle, percentComplete, remaining);
|
|
2481
|
+
},
|
|
2482
|
+
},
|
|
2483
|
+
});
|
|
2484
|
+
task.title = xvfb.util.titleize(completedTitle);
|
|
2485
|
+
}),
|
|
2486
|
+
};
|
|
2487
|
+
}
|
|
2488
|
+
function cleanup(archiveLocation, installDir) {
|
|
2489
|
+
return {
|
|
2490
|
+
title: xvfb.util.titleize('Finishing Installation'),
|
|
2491
|
+
task: (ctx, task) => xvfb.__awaiter(this, void 0, void 0, function* () {
|
|
2492
|
+
debug$2('removing zip file %s', archiveLocation);
|
|
2493
|
+
yield fs.remove(archiveLocation);
|
|
2494
|
+
debug$2('finished installation in', installDir);
|
|
2495
|
+
task.title = xvfb.util.titleize(chalk.green('Finished Installation'), chalk.gray(installDir));
|
|
2496
|
+
}),
|
|
2497
|
+
};
|
|
2498
|
+
}
|
|
2499
|
+
function progressTitle(title, percentComplete, remaining) {
|
|
2500
|
+
return xvfb.util.titleize(title, chalk.white(` ${percentComplete}%`), chalk.gray(`${remaining}s`));
|
|
2501
|
+
}
|
|
2502
|
+
var installModule = {
|
|
2503
|
+
start,
|
|
2504
|
+
_getBinaryUrlFromBuildInfo,
|
|
2494
2505
|
};
|
|
2495
2506
|
|
|
2496
2507
|
const debug$1 = Debug('cypress:cli');
|
|
@@ -2546,58 +2557,14 @@ const g = chalk.green;
|
|
|
2546
2557
|
// color for paths
|
|
2547
2558
|
const p = chalk.cyan;
|
|
2548
2559
|
const red = chalk.red;
|
|
2549
|
-
// urls
|
|
2550
|
-
const link = chalk.blue.underline;
|
|
2551
2560
|
// to be exported
|
|
2552
2561
|
const methods = {};
|
|
2553
|
-
methods.findProxyEnvironmentVariables = () => {
|
|
2554
|
-
return _.pick(process.env, ['HTTP_PROXY', 'HTTPS_PROXY', 'NO_PROXY']);
|
|
2555
|
-
};
|
|
2556
|
-
const maskSensitiveVariables = (obj) => {
|
|
2557
|
-
const masked = Object.assign({}, obj);
|
|
2558
|
-
if (masked.CYPRESS_RECORD_KEY) {
|
|
2559
|
-
masked.CYPRESS_RECORD_KEY = '<redacted>';
|
|
2560
|
-
}
|
|
2561
|
-
return masked;
|
|
2562
|
-
};
|
|
2563
|
-
methods.findCypressEnvironmentVariables = () => {
|
|
2564
|
-
const isCyVariable = (val, key) => key.startsWith('CYPRESS_');
|
|
2565
|
-
return _.pickBy(process.env, isCyVariable);
|
|
2566
|
-
};
|
|
2567
|
-
const formatCypressVariables = () => {
|
|
2568
|
-
const vars = methods.findCypressEnvironmentVariables();
|
|
2569
|
-
return maskSensitiveVariables(vars);
|
|
2570
|
-
};
|
|
2571
2562
|
methods.start = (...args_1) => xvfb.__awaiter(void 0, [...args_1], void 0, function* (options = {}) {
|
|
2572
2563
|
const args = ['--mode=info'];
|
|
2573
2564
|
yield spawn.start$1(args, {
|
|
2574
2565
|
dev: options.dev,
|
|
2575
2566
|
});
|
|
2576
2567
|
console.log();
|
|
2577
|
-
const proxyVars = methods.findProxyEnvironmentVariables();
|
|
2578
|
-
if (_.isEmpty(proxyVars)) {
|
|
2579
|
-
console.log('Proxy Settings: none detected');
|
|
2580
|
-
}
|
|
2581
|
-
else {
|
|
2582
|
-
console.log('Proxy Settings:');
|
|
2583
|
-
_.forEach(proxyVars, (value, key) => {
|
|
2584
|
-
console.log('%s: %s', key, g(value));
|
|
2585
|
-
});
|
|
2586
|
-
console.log();
|
|
2587
|
-
console.log('Learn More: %s', link('https://on.cypress.io/proxy-configuration'));
|
|
2588
|
-
console.log();
|
|
2589
|
-
}
|
|
2590
|
-
const cyVars = formatCypressVariables();
|
|
2591
|
-
if (_.isEmpty(cyVars)) {
|
|
2592
|
-
console.log('Environment Variables: none detected');
|
|
2593
|
-
}
|
|
2594
|
-
else {
|
|
2595
|
-
console.log('Environment Variables:');
|
|
2596
|
-
_.forEach(cyVars, (value, key) => {
|
|
2597
|
-
console.log('%s: %s', key, g(value));
|
|
2598
|
-
});
|
|
2599
|
-
}
|
|
2600
|
-
console.log();
|
|
2601
2568
|
console.log('Application Data:', p(xvfb.util.getApplicationDataFolder()));
|
|
2602
2569
|
console.log('Browser Profiles:', p(xvfb.util.getApplicationDataFolder('browsers')));
|
|
2603
2570
|
console.log('Binary Caches: %s', p(xvfb.stateModule.getCacheDir()));
|
|
@@ -3121,7 +3088,7 @@ const cliModule = {
|
|
|
3121
3088
|
.action(function (opts, args) {
|
|
3122
3089
|
return xvfb.__awaiter(this, void 0, void 0, function* () {
|
|
3123
3090
|
try {
|
|
3124
|
-
const { default: tapModule } = yield Promise.resolve().then(function () { return require('./tap-
|
|
3091
|
+
const { default: tapModule } = yield Promise.resolve().then(function () { return require('./tap-tSM1ve9x.js'); });
|
|
3125
3092
|
const code = yield tapModule.start(args || [], _.pick(opts, ['session', 'json', 'timeout']));
|
|
3126
3093
|
process.exit(code);
|
|
3127
3094
|
}
|