cypress
Version:
Cypress is a next generation front end testing tool built for the modern web
1,204 lines (1,151 loc) • 55.5 kB
JavaScript
'use strict';
var xvfb = require('./xvfb-Csr_YzMk.js');
var _ = require('lodash');
var os = require('os');
var cp = require('child_process');
var path = require('path');
var Debug = require('debug');
var chalk = require('chalk');
var listr2 = require('listr2');
var commonTags = require('common-tags');
var Bluebird = require('bluebird');
var logSymbols = require('log-symbols');
var readline = require('readline');
var process$1 = require('process');
var require$$0 = require('stream');
var require$$1 = require('string_decoder');
var require$$1$1 = require('node:string_decoder');
const debug$1 = Debug('cypress:cli');
const verifyTestRunnerTimeoutMs = () => {
const verifyTimeout = +((xvfb.util === null || xvfb.util === void 0 ? void 0 : xvfb.util.getEnv('CYPRESS_VERIFY_TIMEOUT')) || 'NaN');
if (_.isNumber(verifyTimeout) && !_.isNaN(verifyTimeout)) {
return verifyTimeout;
}
return 30000;
};
const checkExecutable = (binaryDir) => xvfb.__awaiter(void 0, void 0, void 0, function* () {
const executable = xvfb.stateModule.getPathToExecutable(binaryDir);
debug$1('checking if executable exists', executable);
try {
const isExecutable = yield xvfb.util.isExecutableAsync(executable);
debug$1('Binary is executable? :', isExecutable);
if (!isExecutable) {
return xvfb.throwFormErrorText(xvfb.errors.binaryNotExecutable(executable))();
}
}
catch (err) {
if (err.code === 'ENOENT') {
if (xvfb.util.isCi()) {
return xvfb.throwFormErrorText(xvfb.errors.notInstalledCI(executable))();
}
return xvfb.throwFormErrorText(xvfb.errors.missingApp(binaryDir))(commonTags.stripIndent `
Cypress executable not found at: ${chalk.cyan(executable)}
`);
}
throw err;
}
});
const runSmokeTest = (binaryDir, options) => {
let executable = xvfb.stateModule.getPathToExecutable(binaryDir);
const needsXvfb = xvfb.xvfb.isNeeded();
debug$1('needs Xvfb?', needsXvfb);
/**
* Spawn Cypress running smoke test to check if all operating system
* dependencies are good.
*/
const spawn = (linuxWithDisplayEnv) => xvfb.__awaiter(void 0, void 0, void 0, function* () {
const random = _.random(0, 1000);
const args = ['--smoke-test', `--ping=${random}`];
if (needsSandbox()) {
// electron requires --no-sandbox to run as root
debug$1('disabling Electron sandbox');
args.unshift('--no-sandbox');
}
if (options.dev) {
executable = 'node';
const startScriptPath = xvfb.relativeToRepoRoot('scripts/start.js');
if (!startScriptPath) {
throw new Error(`Cypress start script (scripts/start.js) not found in parent directory of ${__dirname}`);
}
args.unshift(startScriptPath);
}
const smokeTestCommand = `${executable} ${args.join(' ')}`;
debug$1('running smoke test');
debug$1('using Cypress executable %s', executable);
debug$1('smoke test command:', smokeTestCommand);
debug$1('smoke test timeout %d ms', options.smokeTestTimeout);
const stdioOptions = _.extend({}, {
env: Object.assign(Object.assign({}, process.env), { FORCE_COLOR: '0' }),
timeout: options.smokeTestTimeout,
});
try {
const result = yield xvfb.util.exec(executable, args, stdioOptions);
// TODO: when execa > 1.1 is released
// change this to `result.all` for both stderr and stdout
// use lodash to be robust during tests against null result or missing stdout
const smokeTestStdout = _.get(result, 'stdout', '');
debug$1('smoke test stdout "%s"', smokeTestStdout);
if (!xvfb.util.stdoutLineMatches(String(random), smokeTestStdout)) {
debug$1('Smoke test failed because could not find %d in:', random, result);
const smokeTestStderr = _.get(result, 'stderr', '');
const errorText = smokeTestStderr || smokeTestStdout;
return xvfb.throwFormErrorText(xvfb.errors.smokeTestFailure(smokeTestCommand, false))(errorText);
}
}
catch (err) {
debug$1('Smoke test failed:', err);
let errMessage = err.stderr || err.message;
debug$1('error message:', errMessage);
if (err.timedOut) {
debug$1('error timedOut is true');
return xvfb.throwFormErrorText(xvfb.errors.smokeTestFailure(smokeTestCommand, true))(errMessage);
}
if (linuxWithDisplayEnv && xvfb.util.isBrokenGtkDisplay(errMessage)) {
xvfb.util.logBrokenGtkDisplayWarning();
return xvfb.throwFormErrorText(xvfb.errors.invalidSmokeTestDisplayError)(errMessage);
}
return xvfb.throwFormErrorText(xvfb.errors.missingDependency)(errMessage);
}
});
const spawnInXvfb = (linuxWithDisplayEnv) => xvfb.__awaiter(void 0, void 0, void 0, function* () {
yield xvfb.xvfb.start();
return spawn(linuxWithDisplayEnv || false).finally(() => xvfb.__awaiter(void 0, void 0, void 0, function* () {
yield xvfb.xvfb.stop();
}));
});
const userFriendlySpawn = (linuxWithDisplayEnv) => xvfb.__awaiter(void 0, void 0, void 0, function* () {
debug$1('spawning, should retry on display problem?', Boolean(linuxWithDisplayEnv));
try {
yield spawn(linuxWithDisplayEnv);
}
catch (err) {
if (err.code === 'INVALID_SMOKE_TEST_DISPLAY_ERROR') {
return spawnInXvfb(linuxWithDisplayEnv);
}
throw err;
}
});
if (needsXvfb) {
return spawnInXvfb();
}
// if we are on linux and there's already a DISPLAY
// set, then we may need to rerun cypress after
// spawning our own Xvfb server
const linuxWithDisplayEnv = xvfb.util.isPossibleLinuxWithIncorrectDisplay();
return userFriendlySpawn(linuxWithDisplayEnv);
};
function logVersionMismatch(binaryVersion, binaryDir, packageVersion) {
xvfb.loggerModule.log(`Found binary version ${chalk.green(binaryVersion)} installed in: ${chalk.cyan(binaryDir)}`);
xvfb.loggerModule.log();
xvfb.loggerModule.warn(commonTags.stripIndent `
${logSymbols.warning} Warning: Binary version ${chalk.green(binaryVersion)} does not match the expected package version ${chalk.green(packageVersion)}
These versions may not work properly together.
`);
xvfb.loggerModule.log();
}
function verifyBinary(installedVersion, binaryDir, options) {
return xvfb.__awaiter(this, void 0, void 0, function* () {
debug$1('running binary verification check', installedVersion);
// if running from 'cypress verify', don't print this message
if (!options.force) {
xvfb.loggerModule.log(commonTags.stripIndent `
It looks like this is your first time using Cypress: ${chalk.cyan(installedVersion)}
`);
}
xvfb.loggerModule.log();
const verifyTaskRunner = new listr2.Listr([{
title: xvfb.util.titleize('Verifying Cypress can run', chalk.gray(binaryDir)),
task: (ctx, task) => xvfb.__awaiter(this, void 0, void 0, function* () {
debug$1('clearing out the verified version');
yield xvfb.stateModule.clearBinaryStateAsync(binaryDir);
yield Promise.all([
runSmokeTest(binaryDir, options),
Bluebird.delay(1500), // good user experience
]);
debug$1('write verified: true');
yield xvfb.stateModule.writeBinaryVerifiedAsync(true, binaryDir);
task.title = xvfb.util.titleize(chalk.green('Verified Cypress!'), chalk.gray(binaryDir));
}),
}], {
silentRendererCondition: () => xvfb.loggerModule.logLevel() === 'silent',
});
yield verifyTaskRunner.run();
if (options.welcomeMessage) {
xvfb.loggerModule.log();
xvfb.loggerModule.log('Opening Cypress...');
}
});
}
const start$1 = (...args_1) => xvfb.__awaiter(void 0, [...args_1], void 0, function* (options = {}) {
debug$1('verifying Cypress app with options %j', options);
_.defaults(options, {
dev: false,
force: false,
welcomeMessage: true,
smokeTestTimeout: verifyTestRunnerTimeoutMs(),
skipVerify: xvfb.util.getEnv('CYPRESS_SKIP_VERIFY') === 'true',
});
if (options.skipVerify) {
debug$1('skipping verification of the Cypress app');
return Promise.resolve();
}
const packageVersion = xvfb.util.pkgVersion();
let binaryDir = xvfb.stateModule.getBinaryDir(packageVersion);
if (options.dev) {
return runSmokeTest('', options);
}
const parseBinaryEnvVar = () => xvfb.__awaiter(void 0, void 0, void 0, function* () {
const envBinaryPath = xvfb.util.getEnv('CYPRESS_RUN_BINARY');
debug$1('CYPRESS_RUN_BINARY exists, =', envBinaryPath);
xvfb.loggerModule.log(commonTags.stripIndent `
${chalk.yellow('Note:')} You have set the environment variable:
${chalk.white('CYPRESS_RUN_BINARY=')}${chalk.cyan(envBinaryPath)}
This overrides the default Cypress binary path used.
`);
xvfb.loggerModule.log();
try {
const isExecutable = yield xvfb.util.isExecutableAsync(envBinaryPath);
debug$1('CYPRESS_RUN_BINARY is executable? :', isExecutable);
if (!isExecutable) {
return xvfb.throwFormErrorText(xvfb.errors.CYPRESS_RUN_BINARY.notValid(envBinaryPath))(commonTags.stripIndent `
The supplied binary path is not executable
`);
}
const envBinaryDir = yield xvfb.stateModule.parseRealPlatformBinaryFolderAsync(envBinaryPath);
if (!envBinaryDir) {
return xvfb.throwFormErrorText(xvfb.errors.CYPRESS_RUN_BINARY.notValid(envBinaryPath))();
}
debug$1('CYPRESS_RUN_BINARY has binaryDir:', envBinaryDir);
binaryDir = envBinaryDir;
}
catch (err) {
if (err.code === 'ENOENT') {
return xvfb.throwFormErrorText(xvfb.errors.CYPRESS_RUN_BINARY.notValid(envBinaryPath))(err.message);
}
throw err;
}
});
try {
debug$1('checking environment variables');
if (xvfb.util.getEnv('CYPRESS_RUN_BINARY')) {
yield parseBinaryEnvVar();
}
yield checkExecutable(binaryDir);
debug$1('binaryDir is ', binaryDir);
const pkg = yield xvfb.stateModule.getBinaryPkgAsync(binaryDir);
const binaryVersion = xvfb.stateModule.getBinaryPkgVersion(pkg);
if (!binaryVersion) {
debug$1('no Cypress binary found for cli version ', packageVersion);
return xvfb.throwFormErrorText(xvfb.errors.missingApp(binaryDir))(`
Cannot read binary version from: ${chalk.cyan(xvfb.stateModule.getBinaryPkgPath(binaryDir))}
`);
}
debug$1(`Found binary version ${chalk.green(binaryVersion)} installed in: ${chalk.cyan(binaryDir)}`);
if (binaryVersion !== packageVersion) {
// warn if we installed with CYPRESS_INSTALL_BINARY or changed version
// in the package.json
logVersionMismatch(binaryVersion, binaryDir, packageVersion);
}
const isVerified = options.force ?
false :
yield xvfb.stateModule.getBinaryVerifiedAsync(binaryDir);
debug$1('is Verified ?', isVerified);
if (!isVerified) {
yield verifyBinary(binaryVersion, binaryDir, options);
}
}
catch (err) {
if (err.known) {
throw err;
}
return xvfb.throwFormErrorText(xvfb.errors.unexpected)(err.stack);
}
});
const isLinuxLike = () => os.platform() !== 'win32';
/**
* Returns true if running on a system where Electron needs "--no-sandbox" flag.
* @see https://crbug.com/638180
*
* On Debian we had problems running in sandbox even for non-root users.
* @see https://github.com/cypress-io/cypress/issues/5434
* Seems there is a lot of discussion around this issue among Electron users
* @see https://github.com/electron/electron/issues/17972
*/
const needsSandbox = () => isLinuxLike();
var dist = {};
var FilterTaggedContent = {};
var LineDecoder = {};
var constants = {};
var hasRequiredConstants;
function requireConstants () {
if (hasRequiredConstants) return constants;
hasRequiredConstants = 1;
Object.defineProperty(constants, "__esModule", { value: true });
constants.DEBUG_PREFIX = constants.END_TAG = constants.START_TAG = void 0;
/**
* These tags are used to mark the beginning and end of error content that should
* be filtered from stderr output. The tags are designed to be unique and easily
* identifiable in log output.
*/
constants.START_TAG = '<<<CYPRESS.STDERR.START>>>';
/**
* Marks the end of error content that should be filtered from stderr output.
*/
constants.END_TAG = '<<<CYPRESS.STDERR.END>>>';
/**
* A regex that will match output from the 'debug' package
*/
// this regexp needs to match control characters
// eslint-disable-next-line no-control-regex
constants.DEBUG_PREFIX = /^\s+(?:\u001b\[[0-9;]*m)*((\S+):)+/u;
return constants;
}
var hasRequiredLineDecoder;
function requireLineDecoder () {
if (hasRequiredLineDecoder) return LineDecoder;
hasRequiredLineDecoder = 1;
/**
* Decodes incoming string chunks into complete lines, handling partial lines across chunk boundaries.
*
* This class buffers incoming string data and provides an iterator interface to yield complete
* lines. It handles the case where a line might be split across multiple chunks by maintaining
* an internal buffer. The end() method should be called to flush any remaining buffered content
* when processing is complete.
*/
var __importDefault = (LineDecoder && LineDecoder.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(LineDecoder, "__esModule", { value: true });
LineDecoder.LineDecoder = void 0;
const debug_1 = __importDefault(Debug);
const constants_1 = requireConstants();
const debugVerbose = (0, debug_1.default)(`cypress-verbose:stderr-filtering:LineDecoder:${process.pid}`);
let LineDecoder$1 = class LineDecoder {
constructor(overrideToken = constants_1.END_TAG) {
this.overrideToken = overrideToken;
this.buffer = '';
}
/**
* Adds a chunk of string data to the internal buffer.
*
* @param chunk The string chunk to add to the buffer
*/
write(chunk) {
debugVerbose('writing chunk to line decoder', { chunk });
this.buffer += chunk;
}
/**
* Iterates over complete lines in the current buffer.
*
* This generator yields complete lines from the buffer, splitting on newline characters.
* Any incomplete line at the end of the buffer is kept for the next iteration.
* Handles both Windows (\r\n) and Unix (\n) line endings.
*
* @yields Complete lines with newline characters preserved
*/
*[Symbol.iterator]() {
debugVerbose('iterating over lines in line decoder');
let nextLine = undefined;
do {
nextLine = this.nextLine();
if (nextLine) {
debugVerbose('yielding line:', nextLine);
debugVerbose('buffer size:', this.buffer.length);
yield nextLine;
}
} while (nextLine);
}
/**
* Flushes the remaining buffer content and yields all remaining lines.
*
* This method should be called when processing is complete to ensure all buffered
* content is yielded. It processes any remaining content in the buffer plus an
* optional final chunk. Handles both Windows (\r\n) and Unix (\n) line endings.
*
* @param chunk Optional final chunk to process along with the buffer
* @yields All remaining lines from the buffer and final chunk
*/
*end(chunk) {
this.buffer = `${this.buffer}${(chunk || '')}`;
let nextLine = undefined;
do {
nextLine = this.nextLine();
if (nextLine) {
yield nextLine;
}
} while (nextLine);
}
nextLine() {
const [newlineIndex, length] = [this.buffer.indexOf('\n'), 1];
const endsWithOverrideToken = newlineIndex < 0 ? this.buffer.endsWith(this.overrideToken) : false;
if (endsWithOverrideToken) {
debugVerbose('ends with override token');
const line = this.buffer;
this.buffer = '';
return line;
}
if (newlineIndex >= 0) {
debugVerbose('contains a newline');
const line = this.buffer.slice(0, newlineIndex + length);
this.buffer = this.buffer.slice(newlineIndex + length);
return line;
}
return undefined;
}
};
LineDecoder.LineDecoder = LineDecoder$1;
return LineDecoder;
}
var writeWithBackpressure = {};
var hasRequiredWriteWithBackpressure;
function requireWriteWithBackpressure () {
if (hasRequiredWriteWithBackpressure) return writeWithBackpressure;
hasRequiredWriteWithBackpressure = 1;
Object.defineProperty(writeWithBackpressure, "__esModule", { value: true });
writeWithBackpressure.writeWithBackpressure = writeWithBackpressure$1;
async function writeWithBackpressure$1(toStream, chunk) {
return new Promise((resolve, reject) => {
try {
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk));
const ret = toStream.write(buffer);
if (ret) {
resolve();
}
else {
toStream.once('drain', () => {
resolve();
});
}
}
catch (err) {
reject(err);
}
});
}
return writeWithBackpressure;
}
var hasRequiredFilterTaggedContent;
function requireFilterTaggedContent () {
if (hasRequiredFilterTaggedContent) return FilterTaggedContent;
hasRequiredFilterTaggedContent = 1;
var __importDefault = (FilterTaggedContent && FilterTaggedContent.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(FilterTaggedContent, "__esModule", { value: true });
FilterTaggedContent.FilterTaggedContent = void 0;
const stream_1 = require$$0;
const string_decoder_1 = require$$1;
const LineDecoder_1 = requireLineDecoder();
const debug_1 = __importDefault(Debug);
const writeWithBackpressure_1 = requireWriteWithBackpressure();
const debugVerbose = (0, debug_1.default)('cypress-verbose:stderr-filtering:FilterTaggedContent');
/**
* Filters content based on start and end tags, supporting multi-line tagged content.
*
* This transform stream processes incoming data and routes content between two output streams
* based on tag detection. Content between start and end tags is sent to the filtered stream,
* while content outside tags is sent to the main output stream. The class handles cases where
* tags span multiple lines by maintaining state across line boundaries.
*
* Example usage:
* ```typescript
* const filter = new FilterTaggedContent('<secret>', '</secret>', filteredStream)
* inputStream.pipe(filter).pipe(outputStream)
* ```
*/
let FilterTaggedContent$1 = class FilterTaggedContent extends stream_1.Transform {
/**
* Creates a new FilterTaggedContent instance.
*
* @param startTag The string that marks the beginning of content to filter
* @param endTag The string that marks the end of content to filter
* @param filtered The writable stream for filtered content
*/
constructor(startTag, endTag, wasteStream) {
super({
transform: (chunk, encoding, next) => this.transform(chunk, encoding, next),
flush: (callback) => this.flush(callback),
});
this.startTag = startTag;
this.endTag = endTag;
this.wasteStream = wasteStream;
this.inTaggedContent = false;
/**
* Processes incoming chunks and routes content based on tag detection.
*
* @param chunk The buffer chunk to process
* @param encoding The encoding of the chunk
* @param next Callback to call when processing is complete
*/
this.transform = async (chunk, encoding, next) => {
var _a, _b, _c;
try {
this.ensureDecoders(encoding);
const str = (_b = (_a = this.strDecoder) === null || _a === void 0 ? void 0 : _a.write(chunk)) !== null && _b !== void 0 ? _b : '';
(_c = this.lineDecoder) === null || _c === void 0 ? void 0 : _c.write(str);
debugVerbose('processing str for tags: "%s"', str);
for (const line of Array.from(this.lineDecoder || [])) {
await this.processLine(line);
}
next();
}
catch (err) {
next(err);
}
};
/**
* Flushes any remaining buffered content when the stream ends.
*
* @param callback Callback to call when flushing is complete
*/
this.flush = async (callback) => {
var _a;
debugVerbose('flushing');
this.ensureDecoders();
try {
for (const line of Array.from(((_a = this.lineDecoder) === null || _a === void 0 ? void 0 : _a.end()) || [])) {
await this.processLine(line);
}
callback();
}
catch (err) {
callback(err);
}
};
}
ensureDecoders(encoding) {
var _a;
const enc = (_a = (encoding === 'buffer' ? 'utf8' : encoding)) !== null && _a !== void 0 ? _a : 'utf8';
if (!this.lineDecoder) {
this.lineDecoder = new LineDecoder_1.LineDecoder();
}
if (!this.strDecoder) {
this.strDecoder = new string_decoder_1.StringDecoder(enc);
}
}
/**
* Processes a single line and routes content based on tag positions.
*
* This method handles the complex logic of detecting start and end tags within a line,
* maintaining state across lines, and routing content to the appropriate streams.
* It supports cases where both tags appear on the same line, only one tag appears,
* or no tags appear but the line is part of ongoing tagged content.
*
* @param line The line to process
*/
async processLine(line) {
const startPos = line.indexOf(this.startTag);
const endPos = line.lastIndexOf(this.endTag);
if (startPos >= 0 && endPos >= 0) {
// Both tags on same line
if (startPos > 0) {
await this.pass(line.slice(0, startPos));
}
await this.writeToWasteStream(line.slice(startPos + this.startTag.length, endPos));
if (endPos + this.endTag.length < line.length) {
await this.pass(line.slice(endPos + this.endTag.length));
}
}
else if (startPos >= 0) {
// Start tag found
if (startPos > 0) {
await this.pass(line.slice(0, startPos));
}
await this.writeToWasteStream(line.slice(startPos + this.startTag.length));
this.inTaggedContent = true;
}
else if (endPos >= 0) {
// End tag found
await this.writeToWasteStream(line.slice(0, endPos));
if (endPos + this.endTag.length < line.length) {
await this.pass(line.slice(endPos + this.endTag.length));
}
this.inTaggedContent = false;
}
else if (this.inTaggedContent) {
// Currently in tagged content
await this.writeToWasteStream(line);
}
else {
// Not in tagged content
await this.pass(line);
}
}
async writeToWasteStream(line, encoding) {
var _a;
debugVerbose('writing to waste stream: "%s"', line);
await (0, writeWithBackpressure_1.writeWithBackpressure)(this.wasteStream, Buffer.from(line, (_a = (encoding === 'buffer' ? 'utf8' : encoding)) !== null && _a !== void 0 ? _a : 'utf8'));
}
async pass(line, encoding) {
var _a;
debugVerbose('passing: "%s"', line);
this.push(Buffer.from(line, (_a = (encoding === 'buffer' ? 'utf8' : encoding)) !== null && _a !== void 0 ? _a : 'utf8'));
}
};
FilterTaggedContent.FilterTaggedContent = FilterTaggedContent$1;
return FilterTaggedContent;
}
var FilterPrefixedContent = {};
var hasRequiredFilterPrefixedContent;
function requireFilterPrefixedContent () {
if (hasRequiredFilterPrefixedContent) return FilterPrefixedContent;
hasRequiredFilterPrefixedContent = 1;
var __importDefault = (FilterPrefixedContent && FilterPrefixedContent.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(FilterPrefixedContent, "__esModule", { value: true });
FilterPrefixedContent.FilterPrefixedContent = void 0;
const stream_1 = require$$0;
const string_decoder_1 = require$$1;
const LineDecoder_1 = requireLineDecoder();
const debug_1 = __importDefault(Debug);
const debugStderr = (0, debug_1.default)('cypress:stderr');
const writeWithBackpressure_1 = requireWriteWithBackpressure();
/**
* Filters content based on a prefix pattern, routing matching lines to a filtered stream.
*
* This transform stream processes incoming data line by line and routes content between two
* output streams based on a regular expression prefix test. Lines that match the prefix pattern
* are sent to the filtered stream, while non-matching lines are sent to the main output stream.
*
* Example usage:
* ```typescript
* const errorStream = new Writable()
* const filter = new FilterPrefixedContent(/^ERROR:/, errorStream)
* inputStream.pipe(filter).pipe(outputStream)
* ```
*/
let FilterPrefixedContent$1 = class FilterPrefixedContent extends stream_1.Transform {
/**
* Creates a new FilterPrefixedContent instance.
*
* @param prefix The regular expression pattern to test against the beginning of each line
* @param filtered The writable stream for lines that match the prefix pattern
*/
constructor(prefix, wasteStream) {
super(({
transform: (chunk, encoding, next) => this.transform(chunk, encoding, next),
flush: (callback) => this.flush(callback),
}));
this.prefix = prefix;
this.wasteStream = wasteStream;
/**
* Processes incoming chunks and routes lines based on prefix matching.
*
* @param chunk The buffer chunk to process
* @param encoding The encoding of the chunk
* @param next Callback to call when processing is complete
*/
this.transform = async (chunk, encoding, next) => {
try {
if (!this.strDecoder) {
// @ts-expect-error type here is not correct, 'buffer' is not a valid encoding but it does get passed in
this.strDecoder = new string_decoder_1.StringDecoder(encoding === 'buffer' ? 'utf8' : encoding);
}
if (!this.lineDecoder) {
this.lineDecoder = new LineDecoder_1.LineDecoder();
}
const str = this.strDecoder.write(chunk);
this.lineDecoder.write(str);
for (const line of Array.from(this.lineDecoder || [])) {
await this.writeLine(line, encoding);
}
next();
}
catch (err) {
debugStderr('error in transform', err);
next(err);
}
};
/**
* Flushes any remaining buffered content when the stream ends.
*
* @param callback Callback to call when flushing is complete
*/
this.flush = async (callback) => {
var _a;
try {
if (!this.strDecoder) {
this.strDecoder = new string_decoder_1.StringDecoder();
}
if (!this.lineDecoder) {
this.lineDecoder = new LineDecoder_1.LineDecoder();
}
if (this.lineDecoder) {
for (const line of Array.from(((_a = this.lineDecoder) === null || _a === void 0 ? void 0 : _a.end()) || [])) {
await this.writeLine(line);
}
}
callback();
}
catch (err) {
callback(err);
}
};
}
/**
* Routes a single line to the appropriate stream based on prefix matching.
*
* Tests the line against the prefix regular expression and routes it to either
* the filtered stream (if it matches) or the main output stream (if it doesn't match).
*
* @param line The line to test and route
*/
async writeLine(line, encoding) {
var _a, _b;
if (this.prefix.test(line)) {
await (0, writeWithBackpressure_1.writeWithBackpressure)(this.wasteStream, Buffer.from(line, (_a = (encoding === 'buffer' ? 'utf8' : encoding)) !== null && _a !== void 0 ? _a : 'utf8'));
}
else {
const canWrite = this.push(Buffer.from(line, (_b = (encoding === 'buffer' ? 'utf8' : encoding)) !== null && _b !== void 0 ? _b : 'utf8'));
if (!canWrite) {
await new Promise((resolve) => this.once('drain', resolve));
}
}
}
};
FilterPrefixedContent.FilterPrefixedContent = FilterPrefixedContent$1;
return FilterPrefixedContent;
}
var TagStream = {};
var tagsDisabled = {};
var hasRequiredTagsDisabled;
function requireTagsDisabled () {
if (hasRequiredTagsDisabled) return tagsDisabled;
hasRequiredTagsDisabled = 1;
Object.defineProperty(tagsDisabled, "__esModule", { value: true });
tagsDisabled.tagsDisabled = tagsDisabled$1;
function tagsDisabled$1() {
return process.env.ELECTRON_ENABLE_LOGGING === '1' || process.env.CYPRESS_INTERNAL_ENV === 'development';
}
return tagsDisabled;
}
var hasRequiredTagStream;
function requireTagStream () {
if (hasRequiredTagStream) return TagStream;
hasRequiredTagStream = 1;
var __importDefault = (TagStream && TagStream.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(TagStream, "__esModule", { value: true });
TagStream.TagStream = void 0;
const stream_1 = require$$0;
const constants_1 = requireConstants();
const string_decoder_1 = require$$1;
const debug_1 = __importDefault(Debug);
const tagsDisabled_1 = requireTagsDisabled();
const debug = (0, debug_1.default)('cypress:stderr-filtering:TagStream');
const debugVerbose = (0, debug_1.default)('cypress-verbose:stderr-filtering:TagStream');
/**
* A Transform stream that wraps input data with start and end tags.
*
* This stream processes incoming chunks and wraps them with configurable
* start and end tags before passing them downstream. It handles both
* Buffer and string inputs, using a StringDecoder for proper encoding
* when processing Buffer chunks.
*
* By default, the start and end tags are the constants exported by this package:
* - START_TAG
* - END_TAG
*
* @example
* ```typescript
* const tagStream = new TagStream('[START]', '[END]');
* tagStream.pipe(process.stdout);
* tagStream.write('Hello World'); // Outputs: [START]Hello World[END]
* ```
*/
let TagStream$1 = class TagStream extends stream_1.Transform {
get initializedDecoder() {
debug('initializedDecoder', !!this.decoder);
if (!this.decoder) {
this.decoder = new string_decoder_1.StringDecoder();
}
return this.decoder;
}
/**
* Creates a new TagStream instance.
*
* @param startTag - The tag to prepend to each chunk. Defaults to START_TAG.
* @param endTag - The tag to append to each chunk. Defaults to END_TAG.
*/
constructor(startTag = constants_1.START_TAG, endTag = constants_1.END_TAG) {
super({
transform: (...args) => this.transform(...args),
});
this.startTag = startTag;
this.endTag = endTag;
}
/**
* Transforms incoming chunks by wrapping them with start and end tags.
*
* Processes the input chunk, handles both Buffer and string inputs,
* and wraps the result with the configured start and end tags.
* Implements backpressure handling by waiting for the 'drain' event
* when the downstream stream cannot accept more data.
*
* @param chunk - The input chunk to transform. Can be Buffer, string, or any other type.
* @param encoding - The encoding of the chunk (used by Transform stream).
* @param callback - Callback function to signal completion of transformation.
* @returns Promise that resolves when transformation is complete.
*/
async transform(chunk, encoding, callback) {
try {
const out = chunk instanceof Buffer ?
this.initializedDecoder.write(chunk) :
chunk;
const transformed = out ? this.tag(out) : Buffer.from('');
debugVerbose(`transformed: "${transformed.toString().replaceAll('\n', '\\n')}"`);
const canWrite = this.push(transformed);
if (!canWrite) {
debugVerbose('waiting for drain');
await new Promise((resolve) => this.once('drain', resolve));
}
callback();
}
catch (err) {
debug('error', err);
callback(err);
}
}
/**
* Flushes any remaining buffered data and wraps it with tags.
*
* Called when the stream is ending to process any remaining
* data in the StringDecoder buffer.
*
* @param callback - Callback function to signal completion of flush operation.
*/
flush(callback) {
debug('flushing');
const out = this.initializedDecoder.end();
callback(undefined, out ? this.tag(out) : Buffer.from(''));
}
tag(str) {
return (0, tagsDisabled_1.tagsDisabled)() ? Buffer.from(str) : Buffer.from(`${this.startTag}${str}${this.endTag}`);
}
};
TagStream.TagStream = TagStream$1;
return TagStream;
}
var WriteToDebug = {};
var hasRequiredWriteToDebug;
function requireWriteToDebug () {
if (hasRequiredWriteToDebug) return WriteToDebug;
hasRequiredWriteToDebug = 1;
Object.defineProperty(WriteToDebug, "__esModule", { value: true });
WriteToDebug.WriteToDebug = void 0;
const stream_1 = require$$0;
const node_string_decoder_1 = require$$1$1;
const LineDecoder_1 = requireLineDecoder();
/**
* A writable stream that routes incoming data to a debug logger.
*
* This class extends Writable to provide a stream interface that processes incoming
* data and forwards it to a debug logger. It handles line-by-line processing and
* automatically manages string decoding and line buffering. The stream is useful
* for debugging purposes where you want to log stream data with proper line handling.
*
* Example usage:
* ```typescript
* const debug = require('debug')('myapp:stream')
* const debugStream = new WriteToDebug(debug)
* someStream.pipe(debugStream)
* ```
*/
let WriteToDebug$1 = class WriteToDebug extends stream_1.Writable {
/**
* Creates a new WriteToDebug instance.
*
* @param debug The debug logger instance to write output to
*/
constructor(debug) {
super({
write: (chunk, encoding, next) => {
if (!this.strDecoder) {
// @ts-expect-error type here is not correct, 'buffer' is not a valid encoding but it does get passed in
this.strDecoder = new node_string_decoder_1.StringDecoder(encoding === 'buffer' ? 'utf8' : encoding);
}
if (!this.lineDecoder) {
this.lineDecoder = new LineDecoder_1.LineDecoder();
}
const str = this.strDecoder.write(chunk);
this.lineDecoder.write(str);
for (const line of this.lineDecoder) {
this.debugLine(line);
}
next();
},
final: (callback) => {
if (!this.strDecoder) {
this.strDecoder = new node_string_decoder_1.StringDecoder();
}
if (!this.lineDecoder) {
this.lineDecoder = new LineDecoder_1.LineDecoder();
}
for (const line of this.lineDecoder.end()) {
this.debugLine(line);
}
this.strDecoder = undefined;
this.lineDecoder = undefined;
callback();
},
});
this.debug = debug;
}
/**
* Processes a single line and sends it to the debug logger.
*
* This method cleans the line by removing trailing newlines while preserving
* intentional whitespace, then sends non-empty lines to the debug logger.
* Empty lines are filtered out to avoid cluttering the debug output.
*
* @param line The line to process and log
*/
debugLine(line) {
// Remove trailing newline but preserve intentional whitespace
const clean = line.endsWith('\n') ? line.slice(0, -1) : line;
if (clean) {
this.debug(clean);
}
}
};
WriteToDebug.WriteToDebug = WriteToDebug$1;
return WriteToDebug;
}
var Filter = {};
var hasRequiredFilter;
function requireFilter () {
if (hasRequiredFilter) return Filter;
hasRequiredFilter = 1;
Object.defineProperty(Filter, "__esModule", { value: true });
Filter.filter = filter;
const constants_1 = requireConstants();
const FilterPrefixedContent_1 = requireFilterPrefixedContent();
const FilterTaggedContent_1 = requireFilterTaggedContent();
const WriteToDebug_1 = requireWriteToDebug();
const tagsDisabled_1 = requireTagsDisabled();
function filter(stderr, debug, prefix) {
const prefixTx = new FilterPrefixedContent_1.FilterPrefixedContent(prefix, stderr);
const debugWriter = new WriteToDebug_1.WriteToDebug(debug);
if ((0, tagsDisabled_1.tagsDisabled)()) {
prefixTx.pipe(debugWriter);
}
else {
const tagTx = new FilterTaggedContent_1.FilterTaggedContent(constants_1.START_TAG, constants_1.END_TAG, stderr);
prefixTx.pipe(tagTx).pipe(debugWriter);
}
return prefixTx;
}
return Filter;
}
var logError = {};
var hasRequiredLogError;
function requireLogError () {
if (hasRequiredLogError) return logError;
hasRequiredLogError = 1;
Object.defineProperty(logError, "__esModule", { value: true });
logError.logError = void 0;
/**
* Standard error logging tags used for stderr filtering
*/
const constants_1 = requireConstants();
const tagsDisabled_1 = requireTagsDisabled();
/**
* Logs error messages with special tags for stderr filtering.
*
* This function wraps console.error calls with start and end tags that can be
* used by FilterTaggedContent to identify and filter error messages from stderr
* output. The tags allow for precise control over which error messages are
* filtered while preserving the original error content.
*
* @param args The arguments to log as an error message
*/
const logError$1 = (...args) => {
// When electron debug is enabled, the output will not be filtered, so
// these tags are not needed.
if ((0, tagsDisabled_1.tagsDisabled)()) {
// eslint-disable-next-line no-console
console.error(...args);
}
else {
// eslint-disable-next-line no-console
console.error(constants_1.START_TAG, ...args, constants_1.END_TAG);
}
};
logError.logError = logError$1;
return logError;
}
var hasRequiredDist;
function requireDist () {
if (hasRequiredDist) return dist;
hasRequiredDist = 1;
(function (exports$1) {
Object.defineProperty(exports$1, "__esModule", { value: true });
exports$1.DEBUG_PREFIX = exports$1.END_TAG = exports$1.START_TAG = exports$1.logError = exports$1.filter = exports$1.WriteToDebug = exports$1.TagStream = exports$1.FilterPrefixedContent = exports$1.FilterTaggedContent = void 0;
var FilterTaggedContent_1 = requireFilterTaggedContent();
Object.defineProperty(exports$1, "FilterTaggedContent", { enumerable: true, get: function () { return FilterTaggedContent_1.FilterTaggedContent; } });
var FilterPrefixedContent_1 = requireFilterPrefixedContent();
Object.defineProperty(exports$1, "FilterPrefixedContent", { enumerable: true, get: function () { return FilterPrefixedContent_1.FilterPrefixedContent; } });
var TagStream_1 = requireTagStream();
Object.defineProperty(exports$1, "TagStream", { enumerable: true, get: function () { return TagStream_1.TagStream; } });
var WriteToDebug_1 = requireWriteToDebug();
Object.defineProperty(exports$1, "WriteToDebug", { enumerable: true, get: function () { return WriteToDebug_1.WriteToDebug; } });
var Filter_1 = requireFilter();
Object.defineProperty(exports$1, "filter", { enumerable: true, get: function () { return Filter_1.filter; } });
var logError_1 = requireLogError();
Object.defineProperty(exports$1, "logError", { enumerable: true, get: function () { return logError_1.logError; } });
var constants_1 = requireConstants();
Object.defineProperty(exports$1, "START_TAG", { enumerable: true, get: function () { return constants_1.START_TAG; } });
Object.defineProperty(exports$1, "END_TAG", { enumerable: true, get: function () { return constants_1.END_TAG; } });
Object.defineProperty(exports$1, "DEBUG_PREFIX", { enumerable: true, get: function () { return constants_1.DEBUG_PREFIX; } });
} (dist));
return dist;
}
var distExports = requireDist();
const debug = Debug('cypress:cli');
const debugElectron = Debug('cypress:electron');
const debugStderr = Debug('cypress:internal-stderr');
function isPlatform(platform) {
return os.platform() === platform;
}
function needsStderrPiped(needsXvfb) {
return _.some([
isPlatform('darwin'),
(needsXvfb && isPlatform('linux')),
xvfb.util.isPossibleLinuxWithIncorrectDisplay(),
]);
}
function needsEverythingPipedDirectly() {
return isPlatform('win32');
}
function getStdioStrategy(needsXvfb) {
if (needsEverythingPipedDirectly()) {
return 'pipe';
}
// https://github.com/cypress-io/cypress/issues/921
// https://github.com/cypress-io/cypress/issues/1143
// https://github.com/cypress-io/cypress/issues/1745
if (needsStderrPiped(needsXvfb)) {
// returning pipe here so we can massage stderr
// and remove garbage from Xlib and libuv
// due to starting the Xvfb process on linux
return ['inherit', 'inherit', 'pipe'];
}
return 'inherit';
}
function createSpawnFunction(executable, args, options) {
return (overrides = {}) => {
return new Promise((resolve, reject) => xvfb.__awaiter(this, void 0, void 0, function* () {
var _a, _b;
_.defaults(overrides, {
onStderrData: false,
});
const { onStderrData } = overrides;
const envOverrides = xvfb.util.getEnvOverrides(options);
const electronArgs = [];
const node11WindowsFix = isPlatform('win32');
let startScriptPath;
if (options.dev) {
executable = 'node';
// if we're in dev then reset
// the launch cmd to be 'npm run dev'
// This path is correct in the build output, but not the source code. This file gets bundled into
// `dist/spawn-<hash>.js`, which makes this resolution appear incorrect at first glance.
startScriptPath = xvfb.relativeToRepoRoot('scripts/start.js');
if (!startScriptPath) {
throw new Error(`Cypress start script (scripts/start.js) not found in parent directory of ${__dirname}`);
}
}
if (!options.dev && needsSandbox()) {
electronArgs.push('--no-sandbox');
}
// strip dev out of child process options
/**
* @type {import('child_process').ForkOptions}
*/
let stdioOptions = _.pick(options, 'env', 'detached', 'stdio');
// figure out if we're going to be force enabling or disabling colors.
// also figure out whether we should force stdout and stderr into thinking
// it is a tty as opposed to a pipe.
stdioOptions.env = _.extend({}, stdioOptions.env, envOverrides);
if (node11WindowsFix) {
stdioOptions = _.extend({}, stdioOptions, { windowsHide: false });
}
if (xvfb.util.isPossibleLinuxWithIncorrectDisplay()) {
// make sure we use the latest DISPLAY variable if any
debug('passing DISPLAY', process.env.DISPLAY);
stdioOptions.env.DISPLAY = process.env.DISPLAY;
}
if (stdioOptions.env.ELECTRON_RUN_AS_NODE) {
// Since we are running electron as node, we need to add an entry point file.
startScriptPath = path.join(xvfb.stateModule.getBinaryPkgPath(path.dirname(executable)), '..', 'index.js');
}
else {
// Start arguments with "--" so Electron knows these are OUR
// arguments and does not try to sanitize them. Otherwise on Windows
// an url in one of the arguments crashes it :(
// https://github.com/cypress-io/cypress/issues/5466
args = [...electronArgs, '--', ...args];
}
if (startScriptPath) {
args.unshift(startScriptPath);
}
if (process.env.CYPRESS_INTERNAL_DEV_DEBUG) {
args.unshift(process.env.CYPRESS_INTERNAL_DEV_DEBUG);
}
debug('spawn args %o %o', args, _.omit(stdioOptions, 'env'));
debug('spawning Cypress with executable: %s', executable);
const platform = yield xvfb.util.getPlatformInfo().catch((e) => reject(e));
if (!platform) {
return;
}
function resolveOn(event) {
return function (code, signal) {
debug('child event fired %o', { event, code, signal });
if (code === null) {
const errorObject = xvfb.errors.childProcessKilled(event, signal);
errorObject.platform = platform;
const err = xvfb.getErrorSync(errorObject, platform);
reject(err);
return;
}
resolve(code);
};
}
const child = cp.spawn(executable, args, stdioOptions);
child.on('close', resolveOn('close'));
child.on('exit', resolveOn('exit'));
child.on('error', reject);
if (isPlatform('win32')) {
const rl = readline.createInterface({
input: process$1.stdin,
output: process$1.stdout,
});
// on windows, SIGINT does not propagate to the child process when ctrl+c is pressed
// this makes sure all nested processes are closed(ex: firefox inside the server)
rl.on('SIGINT', function () {
return xvfb.__awaiter(this, void 0, void 0, function* () {
const kill = (yield import('tree-kill')).default;
kill(child.pid, 'SIGINT');
});
});
}
// if stdio options is set to 'pipe', then
// we should set up pipes:
// process STDIN (read stream) => child STDIN (writeable)
// child STDOUT => process STDOUT
// child STDERR => process STDERR with additional filtering
if (child.stdin) {
debug('piping process STDIN into child STDIN');
process$1.stdin.pipe(child.stdin);
}
if (child.stdout) {
debug('piping child STDOUT to process STDOUT');
child.stdout.pipe(process$1.stdout);
}
// if this is defined then we are manually piping for linux
// to filter out the garbage
if (child.stderr) {
debug('piping child STDERR to process STDERR');
const sourceStream = new require$$0.PassThro