diff --git a/cli/__snapshots__/cli_spec.js b/cli/__snapshots__/cli_spec.js index 6b32ae654418..010f3fdd84d5 100644 --- a/cli/__snapshots__/cli_spec.js +++ b/cli/__snapshots__/cli_spec.js @@ -46,14 +46,14 @@ exports['cli help command shows help 1'] = ` run [options] Runs Cypress tests from the CLI without the GUI open [options] Opens Cypress in the interactive GUI. install [options] Installs the Cypress executable matching this package's version - verify Verifies that Cypress is installed correctly and executable + verify [options] Verifies that Cypress is installed correctly and executable cache [options] Manages the Cypress binary cache ------- stderr: ------- - + ------- - + ` exports['cli help command shows help for --help 1'] = ` @@ -79,14 +79,14 @@ exports['cli help command shows help for --help 1'] = ` run [options] Runs Cypress tests from the CLI without the GUI open [options] Opens Cypress in the interactive GUI. install [options] Installs the Cypress executable matching this package's version - verify Verifies that Cypress is installed correctly and executable + verify [options] Verifies that Cypress is installed correctly and executable cache [options] Manages the Cypress binary cache ------- stderr: ------- - + ------- - + ` exports['cli help command shows help for -h 1'] = ` @@ -112,14 +112,14 @@ exports['cli help command shows help for -h 1'] = ` run [options] Runs Cypress tests from the CLI without the GUI open [options] Opens Cypress in the interactive GUI. install [options] Installs the Cypress executable matching this package's version - verify Verifies that Cypress is installed correctly and executable + verify [options] Verifies that Cypress is installed correctly and executable cache [options] Manages the Cypress binary cache ------- stderr: ------- - + ------- - + ` exports['cli unknown command shows usage and exits 1'] = ` @@ -151,9 +151,9 @@ exports['cli unknown command shows usage and exits 1'] = ` ------- stderr: ------- - + ------- - + ` exports['cli unknown option shows help for cache command - no sub-command 1'] = ` @@ -179,9 +179,9 @@ exports['cli unknown option shows help for cache command - no sub-command 1'] = ------- stderr: ------- - + ------- - + ` exports['cli unknown option shows help for cache command - unknown option --foo 1'] = ` @@ -209,9 +209,9 @@ exports['cli unknown option shows help for cache command - unknown option --foo ------- stderr: ------- - + ------- - + ` exports['cli unknown option shows help for cache command - unknown sub-command foo 1'] = ` @@ -239,9 +239,9 @@ exports['cli unknown option shows help for cache command - unknown sub-command f ------- stderr: ------- - + ------- - + ` exports['cli version and binary version 1'] = ` @@ -289,9 +289,9 @@ exports['shows help for open --foo 1'] = ` ------- stderr: ------- - + ------- - + ` exports['shows help for run --foo 1'] = ` @@ -332,7 +332,7 @@ exports['shows help for run --foo 1'] = ` ------- stderr: ------- - + ------- - + ` diff --git a/cli/__snapshots__/errors_spec.js b/cli/__snapshots__/errors_spec.js index 9eba5ebf2d44..c3891648fdb6 100644 --- a/cli/__snapshots__/errors_spec.js +++ b/cli/__snapshots__/errors_spec.js @@ -32,7 +32,7 @@ exports['errors individual has the following errors 1'] = [ "failedDownload", "failedUnzip", "invalidCacheDirectory", - "invalidDisplayError", + "invalidSmokeTestDisplayError", "missingApp", "missingDependency", "missingXvfb", @@ -45,19 +45,11 @@ exports['errors individual has the following errors 1'] = [ ] exports['invalid display error'] = ` -Cypress failed to start. +Cypress verification failed. -First, we have tried to start Cypress using your DISPLAY settings -but encountered the following problem: +Cypress failed to start after spawning a new XVFB server. ----------- - -prev message - ----------- - -Then we started our own XVFB and tried to start Cypress again, but -got the following error: +The error logs we received were: ---------- diff --git a/cli/__snapshots__/verify_spec.js b/cli/__snapshots__/verify_spec.js index fd02db1e68d1..ded2295d591c 100644 --- a/cli/__snapshots__/verify_spec.js +++ b/cli/__snapshots__/verify_spec.js @@ -159,7 +159,7 @@ Error: Cypress verification timed out. This command failed with the following output: -/cache/Cypress/1.2.3/Cypress.app/Contents/MacOS/Cypress --smoke-test --ping=222 --enable-logging +/cache/Cypress/1.2.3/Cypress.app/Contents/MacOS/Cypress --smoke-test --ping=222 ---------- @@ -181,7 +181,7 @@ Error: Cypress verification failed. This command failed with the following output: -/cache/Cypress/1.2.3/Cypress.app/Contents/MacOS/Cypress --smoke-test --ping=222 --enable-logging +/cache/Cypress/1.2.3/Cypress.app/Contents/MacOS/Cypress --smoke-test --ping=222 ---------- @@ -203,7 +203,7 @@ Error: Cypress verification failed. This command failed with the following output: -/cache/Cypress/1.2.3/Cypress.app/Contents/MacOS/Cypress --smoke-test --ping=222 --enable-logging +/cache/Cypress/1.2.3/Cypress.app/Contents/MacOS/Cypress --smoke-test --ping=222 ---------- @@ -364,19 +364,15 @@ It looks like this is your first time using Cypress: 1.2.3 ✖ Verifying Cypress can run /cache/Cypress/1.2.3/Cypress.app STRIPPED -Error: Your system is missing the dependency: XVFB +Error: XVFB exited with a non zero exit code. -Install XVFB and run Cypress again. +There was a problem spawning Xvfb. -Read our documentation on dependencies for more information: - -https://on.cypress.io/required-dependencies - -If you are using Docker, we provide containers with all required dependencies installed. +This is likely a problem with your system, permissions, or installation of Xvfb. ---------- -Caught error trying to run XVFB: "test without xvfb" +Error: test without xvfb ---------- @@ -386,23 +382,15 @@ Cypress Version: 1.2.3 ` exports['tried to verify twice, on the first try got the DISPLAY error'] = ` -Cypress failed to start. +Cypress verification failed. -First, we have tried to start Cypress using your DISPLAY settings -but encountered the following problem: +Cypress failed to start after spawning a new XVFB server. ----------- - -[some noise here] Gtk: cannot open display: 987 -and maybe a few other lines here with weird indent - ----------- - -Then we started our own XVFB and tried to start Cypress again, but -got the following error: +The error logs we received were: ---------- +[some noise here] Gtk: cannot open display: 987 some other error again with some weird indent diff --git a/cli/lib/cli.js b/cli/lib/cli.js index d18ac01f2b52..cecc0172565b 100644 --- a/cli/lib/cli.js +++ b/cli/lib/cli.js @@ -228,6 +228,7 @@ module.exports = { .command('verify') .usage('[options]') .description('Verifies that Cypress is installed correctly and executable') + .option('--dev', text('dev'), coerceFalse) .action((opts) => { const defaultOpts = { force: true, welcomeMessage: false } const parsedOpts = parseOpts(opts) diff --git a/cli/lib/errors.js b/cli/lib/errors.js index 9146ead7c14d..4e7e5a95ed69 100644 --- a/cli/lib/errors.js +++ b/cli/lib/errors.js @@ -108,21 +108,14 @@ const smokeTestFailure = (smokeTestCommand, timedOut) => { } } -const invalidDisplayError = { - description: 'Cypress failed to start.', - solution (msg, prevMessage) { +const invalidSmokeTestDisplayError = { + code: 'INVALID_SMOKE_TEST_DISPLAY_ERROR', + description: 'Cypress verification failed.', + solution (msg) { return stripIndent` - First, we have tried to start Cypress using your DISPLAY settings - but encountered the following problem: - - ${hr} + Cypress failed to start after spawning a new XVFB server. - ${prevMessage} - - ${hr} - - Then we started our own XVFB and tried to start Cypress again, but - got the following error: + The error logs we received were: ${hr} @@ -301,17 +294,23 @@ function formErrorText (info, msg, prevMessage) { }) } -const raise = (text) => { - const err = new Error(text) +const raise = (info) => { + return (text) => { + const err = new Error(text) + + if (info.code) { + err.code = info.code + } - err.known = true - throw err + err.known = true + throw err + } } const throwFormErrorText = (info) => { return (msg, prevMessage) => { return formErrorText(info, msg, prevMessage) - .then(raise) + .then(raise(info)) } } @@ -326,7 +325,7 @@ module.exports = { missingApp, notInstalledCI, missingDependency, - invalidDisplayError, + invalidSmokeTestDisplayError, versionMismatch, binaryNotExecutable, unexpected, diff --git a/cli/lib/exec/spawn.js b/cli/lib/exec/spawn.js index 08e6e04e7ee5..7451cf628507 100644 --- a/cli/lib/exec/spawn.js +++ b/cli/lib/exec/spawn.js @@ -1,29 +1,39 @@ const _ = require('lodash') -const la = require('lazy-ass') -const is = require('check-more-types') const os = require('os') const cp = require('child_process') const path = require('path') const Promise = require('bluebird') const debug = require('debug')('cypress:cli') -const { stripIndent } = require('common-tags') +const debugElectron = require('debug')('cypress:electron') const util = require('../util') const state = require('../tasks/state') const xvfb = require('./xvfb') -const logger = require('../logger') -const logSymbols = require('log-symbols') const { throwFormErrorText, errors } = require('../errors') const isXlibOrLibudevRe = /^(?:Xlib|libudev)/ const isHighSierraWarningRe = /\*\*\* WARNING/ +const GARBAGE_WARNINGS = [isXlibOrLibudevRe, isHighSierraWarningRe] + +const isGarbageLineWarning = (str) => { + return _.some(GARBAGE_WARNINGS, (re) => { + return re.test(str) + }) +} + function isPlatform (platform) { return os.platform() === platform } function needsStderrPiped (needsXvfb) { - return isPlatform('darwin') || (needsXvfb && isPlatform('linux')) + return _.some([ + isPlatform('darwin'), + + (needsXvfb && isPlatform('linux')), + + util.isPossibleLinuxWithIncorrectDisplay(), + ]) } function needsEverythingPipedDirectly () { @@ -48,18 +58,6 @@ function getStdio (needsXvfb) { return 'inherit' } -/** - * Returns true if DISPLAY is set for Linux platform - * and the application exits really quickly. - */ -const isPotentialDisplayProblem = (platform, display, code, elapsedMs) => { - la(is.unemptyString(platform), 'missing platform', platform) - la(is.number(code), 'expected exit code to be a number', code) - la(elapsedMs >= 0, 'elapsed ms should be >= 0', elapsedMs) - - return platform === 'linux' && display && code === 1 && elapsedMs < 1000 -} - module.exports = { start (args, options = {}) { const needsXvfb = xvfb.isNeeded() @@ -75,13 +73,19 @@ module.exports = { args = [].concat(args, '--cwd', process.cwd()) _.defaults(options, { + dev: false, env: process.env, detached: false, stdio: getStdio(needsXvfb), }) - const spawn = () => { + const spawn = (overrides = {}) => { return new Promise((resolve, reject) => { + _.defaults(overrides, { + onStderrData: false, + electronLogging: false, + }) + if (options.dev) { // if we're in dev then reset // the launch cmd to be 'npm run dev' @@ -91,32 +95,37 @@ module.exports = { ) } - const overrides = util.getEnvOverrides() + const { onStderrData, electronLogging } = overrides + const envOverrides = util.getEnvOverrides() + const electronArgs = _.clone(args) const node11WindowsFix = isPlatform('win32') - debug('spawning Cypress with executable: %s', executable) - debug('spawn forcing env overrides %o', overrides) - debug('spawn args %o %o', args, _.omit(options, 'env')) - // strip dev out of child process options - options = _.omit(options, 'dev') - options = _.omit(options, 'binaryFolder') + 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. - options.env = _.extend({}, options.env, overrides) + stdioOptions.env = _.extend({}, stdioOptions.env, envOverrides) + if (node11WindowsFix) { - options = _.extend({}, options, { windowsHide: false }) + stdioOptions = _.extend({}, stdioOptions, { windowsHide: false }) + } + + if (electronLogging) { + stdioOptions.env.ELECTRON_ENABLE_LOGGING = true } - if (os.platform() === 'linux' && process.env.DISPLAY) { + if (util.isPossibleLinuxWithIncorrectDisplay()) { // make sure we use the latest DISPLAY variable if any debug('passing DISPLAY', process.env.DISPLAY) - options.env.DISPLAY = process.env.DISPLAY + stdioOptions.env.DISPLAY = process.env.DISPLAY } - const child = cp.spawn(executable, args, options) + debug('spawning Cypress with executable: %s', executable) + debug('spawn args %o %o', electronArgs, _.omit(stdioOptions, 'env')) + + const child = cp.spawn(executable, electronArgs, stdioOptions) child.on('close', resolve) child.on('error', reject) @@ -131,10 +140,13 @@ module.exports = { const str = data.toString() // bail if this is warning line garbage - if ( - isXlibOrLibudevRe.test(str) || - isHighSierraWarningRe.test(str) - ) { + if (isGarbageLineWarning(str)) { + return + } + + // if we have a callback and this explictly returns + // false then bail + if (onStderrData && onStderrData(str) === false) { return } @@ -156,7 +168,7 @@ module.exports = { throw err }) - if (options.detached) { + if (stdioOptions.detached) { child.unref() } }) @@ -165,47 +177,41 @@ module.exports = { const spawnInXvfb = () => { return xvfb .start() - .then(() => { - // call userFriendlySpawn ourselves - // to prevent result of previous promise - // from becoming a parameter to userFriendlySpawn - debug('spawning Cypress after starting XVFB') - - return userFriendlySpawn() - }) + .then(userFriendlySpawn) .finally(xvfb.stop) } - const userFriendlySpawn = (shouldRetryOnDisplayProblem) => { - debug('spawning, should retry on display problem?', Boolean(shouldRetryOnDisplayProblem)) - if (os.platform() === 'linux') { - debug('DISPLAY is %s', process.env.DISPLAY) - } - - const electronStarted = Number(new Date()) + const userFriendlySpawn = (linuxWithDisplayEnv) => { + debug('spawning, should retry on display problem?', Boolean(linuxWithDisplayEnv)) - return spawn() - .then((code) => { - const electronFinished = Number(new Date()) - const elapsed = electronFinished - electronStarted - - debug('electron open returned %d after %d ms', code, elapsed) + let brokenGtkDisplay - if (shouldRetryOnDisplayProblem && - isPotentialDisplayProblem(os.platform(), process.env.DISPLAY, code, elapsed)) { - debug('Cypress thinks there is a potential display or OS problem') - debug('retrying the command with our XVFB') + const overrides = {} - // if we get this error, we are on Linux and DISPLAY is set - logger.warn(`${stripIndent` - - ${logSymbols.warning} Warning: Cypress process has finished very quickly with an error, - which might be related to a potential problem with how the DISPLAY is configured. + if (linuxWithDisplayEnv) { + _.extend(overrides, { + electronLogging: true, + onStderrData (str) { + // if we receive a broken pipe anywhere + // then we know that's why cypress exited early + if (util.isBrokenGtkDisplay(str)) { + brokenGtkDisplay = true + } - DISPLAY was set to "${process.env.DISPLAY}" + // we should attempt to always slurp up + // the stderr logs unless we've explicitly + // enabled the electron debug logging + if (!debugElectron.enabled) { + return false + } + }, + }) + } - We will attempt to spin our XVFB server and run Cypress again. - `}\n`) + return spawn(overrides) + .then((code) => { + if (code !== 0 && brokenGtkDisplay) { + util.logBrokenGtkDisplayWarning() return spawnInXvfb() } @@ -219,10 +225,11 @@ module.exports = { return spawnInXvfb() } - // if we have problems spawning Cypress, maybe user DISPLAY setting is incorrect - // in that case retry with our own XVFB - const shouldRetryOnDisplayProblem = os.platform() === 'linux' + // 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 = util.isPossibleLinuxWithIncorrectDisplay() - return userFriendlySpawn(shouldRetryOnDisplayProblem) + return userFriendlySpawn(linuxWithDisplayEnv) }, } diff --git a/cli/lib/exec/xvfb.js b/cli/lib/exec/xvfb.js index 2959718d4b55..6e750bc27cba 100644 --- a/cli/lib/exec/xvfb.js +++ b/cli/lib/exec/xvfb.js @@ -1,7 +1,6 @@ const os = require('os') const Promise = require('bluebird') const Xvfb = require('@cypress/xvfb') -const R = require('ramda') const { stripIndent } = require('common-tags') const debug = require('debug')('cypress:cli') const debugXvfb = require('debug')('cypress:xvfb') @@ -26,6 +25,7 @@ module.exports = { debug('Starting XVFB') return xvfb.startAsync() + .return(null) .catch({ nonZeroExitCode: true }, throwFormErrorText(errors.nonZeroExitCodeXvfb)) .catch((err) => { if (err.known) { @@ -40,6 +40,10 @@ module.exports = { debug('Stopping XVFB') return xvfb.stopAsync() + .return(null) + .catch(() => { + // noop + }) }, isNeeded () { @@ -75,7 +79,7 @@ module.exports = { // async method, resolved with Boolean verify () { return xvfb.startAsync() - .then(R.T) + .return(true) .catch((err) => { debug('Could not verify xvfb: %s', err.message) diff --git a/cli/lib/tasks/verify.js b/cli/lib/tasks/verify.js index b53411a1ee30..4052c22fccdc 100644 --- a/cli/lib/tasks/verify.js +++ b/cli/lib/tasks/verify.js @@ -3,9 +3,10 @@ const chalk = require('chalk') const Listr = require('listr') const debug = require('debug')('cypress:cli') const verbose = require('@cypress/listr-verbose-renderer') -const { stripIndent, stripIndents } = require('common-tags') +const { stripIndent } = require('common-tags') const Promise = require('bluebird') const logSymbols = require('log-symbols') +const path = require('path') const { throwFormErrorText, errors } = require('../errors') const util = require('../util') @@ -37,18 +38,9 @@ const checkExecutable = (binaryDir) => { } const runSmokeTest = (binaryDir, options) => { - debug('running smoke test') - const cypressExecPath = state.getPathToExecutable(binaryDir) + let executable = state.getPathToExecutable(binaryDir) - debug('using Cypress executable %s', cypressExecPath) - - const onXvfbError = (err) => { - debug('caught xvfb error %s', err.message) - - return throwFormErrorText(errors.missingXvfb)(`Caught error trying to run XVFB: "${err.message}"`) - } - - const onSmokeTestError = (smokeTestCommand, runningWithOurXvfb, prevDisplayError) => { + const onSmokeTestError = (smokeTestCommand, linuxWithDisplayEnv) => { return (err) => { debug('Smoke test failed:', err) @@ -59,43 +51,17 @@ const runSmokeTest = (binaryDir, options) => { if (err.timedOut) { debug('error timedOut is true') - return throwFormErrorText(errors.smokeTestFailure(smokeTestCommand, true))(errMessage) + return throwFormErrorText( + errors.smokeTestFailure(smokeTestCommand, true) + )(errMessage) } - if (!runningWithOurXvfb && !prevDisplayError && util.isDisplayError(errMessage)) { - // running without our XVFB - // for the very first time - // and we hit invalid display error - debug('Smoke test hit Linux display problem: %s', errMessage) - - logger.warn(`${stripIndents` - - ${logSymbols.warning} Warning: we have caught a display problem: - - ${stripIndents(errMessage)} + if (linuxWithDisplayEnv && util.isBrokenGtkDisplay(errMessage)) { + util.logBrokenGtkDisplayWarning() - We will attempt to spin our XVFB server and verify again. - `}\n`) - - const err = new Error(errMessage) - - err.displayError = true - err.platform = 'linux' - throw err - } - - if (prevDisplayError) { - debug('this was our 2nd attempt at verifying') - debug('first we tried with user-given DISPLAY') - debug('now we have tried spinning our own XVFB') - debug('and yet it still has failed with') - debug(errMessage) - - return throwFormErrorText(errors.invalidDisplayError)(errMessage, prevDisplayError.message) + return throwFormErrorText(errors.invalidSmokeTestDisplayError)(errMessage) } - debug('throwing missing dependency error') - return throwFormErrorText(errors.missingDependency)(errMessage) } } @@ -108,21 +74,32 @@ const runSmokeTest = (binaryDir, options) => { * Spawn Cypress running smoke test to check if all operating system * dependencies are good. */ - const spawn = (runningWithOurXvfb, prevDisplayError) => { + const spawn = (linuxWithDisplayEnv) => { const random = _.random(0, 1000) - const args = ['--smoke-test', `--ping=${random}`, '--enable-logging'] - const smokeTestCommand = `${cypressExecPath} ${args.join(' ')}` + const args = ['--smoke-test', `--ping=${random}`] + process.env.ELECTRON_ENABLE_LOGGING = true + + if (options.dev) { + executable = 'node' + args.unshift( + path.resolve(__dirname, '..', '..', '..', 'scripts', 'start.js') + ) + } + + const smokeTestCommand = `${executable} ${args.join(' ')}` + + debug('running smoke test') + debug('using Cypress executable %s', executable) debug('smoke test command:', smokeTestCommand) return Promise.resolve(util.exec( - cypressExecPath, + executable, args, { timeout: options.smokeTestTimeout } )) - .catch(onSmokeTestError(smokeTestCommand, runningWithOurXvfb, prevDisplayError)) + .catch(onSmokeTestError(smokeTestCommand, linuxWithDisplayEnv)) .then((result) => { - // TODO: when execa > 1.1 is released // change this to `result.all` for both stderr and stdout const smokeTestReturned = result.stdout @@ -137,27 +114,34 @@ const runSmokeTest = (binaryDir, options) => { }) } - const spinXvfbAndVerify = (prevDisplayError) => { - return xvfb.start() - .catch(onXvfbError) - .then(spawn.bind(null, true, prevDisplayError)) - .finally(() => { - return xvfb.stop() - .catch(onXvfbError) + const spawnInXvfb = (linuxWithDisplayEnv) => { + return xvfb + .start() + .then(() => { + return spawn(linuxWithDisplayEnv) + }) + .finally(xvfb.stop) + } + + const userFriendlySpawn = (linuxWithDisplayEnv) => { + debug('spawning, should retry on display problem?', Boolean(linuxWithDisplayEnv)) + + return spawn(linuxWithDisplayEnv) + .catch({ code: 'INVALID_SMOKE_TEST_DISPLAY_ERROR' }, () => { + return spawnInXvfb(linuxWithDisplayEnv) }) } if (needsXvfb) { - return spinXvfbAndVerify() + return spawnInXvfb() } - return spawn() - .catch({ displayError: true, platform: 'linux' }, (e) => { - debug('there was a display error') - debug('will try spinning our own XVFB and verify Cypress') + // 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 = util.isPossibleLinuxWithIncorrectDisplay() - return spinXvfbAndVerify(e) - }) + return userFriendlySpawn(linuxWithDisplayEnv) } function testBinary (version, binaryDir, options) { @@ -248,11 +232,16 @@ const start = (options = {}) => { let binaryDir = state.getBinaryDir(packageVersion) _.defaults(options, { + dev: false, force: false, welcomeMessage: true, smokeTestTimeout: 10000, }) + if (options.dev) { + return runSmokeTest('', options) + } + const parseBinaryEnvVar = () => { const envBinaryPath = util.getEnv('CYPRESS_RUN_BINARY') diff --git a/cli/lib/util.js b/cli/lib/util.js index 88fdf5358ec5..4296b39587c8 100644 --- a/cli/lib/util.js +++ b/cli/lib/util.js @@ -11,7 +11,9 @@ const getos = require('getos') const chalk = require('chalk') const Promise = require('bluebird') const cachedir = require('cachedir') +const logSymbols = require('log-symbols') const executable = require('executable') +const { stripIndent } = require('common-tags') const supportsColor = require('supports-color') const isInstalledGlobally = require('is-installed-globally') const pkg = require(path.join(__dirname, '..', 'package.json')) @@ -22,6 +24,8 @@ const issuesUrl = 'https://github.com/cypress-io/cypress/issues' const getosAsync = Promise.promisify(getos) +const isBrokenGtkDisplayRe = /Gtk: cannot open display/ + const stringify = (val) => { return _.isObject(val) ? JSON.stringify(val) : val } @@ -38,6 +42,38 @@ const isLinux = () => { return os.platform() === 'linux' } +/** + * If the DISPLAY variable is set incorrectly, when trying to spawn + * Cypress executable we get an error like this: + ``` + [1005:0509/184205.663837:WARNING:browser_main_loop.cc(258)] Gtk: cannot open display: 99 + ``` + */ +const isBrokenGtkDisplay = (str) => { + return isBrokenGtkDisplayRe.test(str) +} + +const isPossibleLinuxWithIncorrectDisplay = () => { + return isLinux() && process.env.DISPLAY +} + +const logBrokenGtkDisplayWarning = () => { + debug('Cypress exited due to a broken gtk display because of a potential invalid DISPLAY env... retrying after starting XVFB') + + // if we get this error, we are on Linux and DISPLAY is set + logger.warn(stripIndent` + + ${logSymbols.warning} Warning: Cypress failed to start. + + This is likely due to a misconfigured DISPLAY environment variable. + + DISPLAY was set to: "${process.env.DISPLAY}" + + Cypress will attempt to fix the problem and rerun. + `) + logger.warn() +} + function stdoutLineMatches (expectedLine, stdout) { const lines = stdout.split('\n').map(R.trim) const lineMatches = R.equals(expectedLine) @@ -260,6 +296,12 @@ const util = { issuesUrl, + isBrokenGtkDisplay, + + logBrokenGtkDisplayWarning, + + isPossibleLinuxWithIncorrectDisplay, + getGitHubIssueUrl (number) { la(is.positive(number), 'github issue should be a positive number', number) la(_.isInteger(number), 'github issue should be an integer', number) @@ -267,16 +309,6 @@ const util = { return `${issuesUrl}/${number}` }, - /** - * If the DISPLAY variable is set incorrectly, when trying to spawn - * Cypress executable we get an error like this: - ``` - [1005:0509/184205.663837:WARNING:browser_main_loop.cc(258)] Gtk: cannot open display: 99 - ``` - */ - isDisplayError (errorMessage) { - return isLinux() && errorMessage.includes('cannot open display:') - }, } module.exports = util diff --git a/cli/test/lib/errors_spec.js b/cli/test/lib/errors_spec.js index 741b5d423ac8..4310da3a2f84 100644 --- a/cli/test/lib/errors_spec.js +++ b/cli/test/lib/errors_spec.js @@ -67,7 +67,7 @@ describe('errors', function () { }) it('forms full text for invalid display error', () => { - return formErrorText(errors.invalidDisplayError, 'current message', 'prev message') + return formErrorText(errors.invalidSmokeTestDisplayError, 'current message', 'prev message') .then((text) => { snapshot('invalid display error', text) }) diff --git a/cli/test/lib/exec/spawn_spec.js b/cli/test/lib/exec/spawn_spec.js index e83e68161706..9cfeb8068945 100644 --- a/cli/test/lib/exec/spawn_spec.js +++ b/cli/test/lib/exec/spawn_spec.js @@ -57,7 +57,8 @@ describe('lib/exec/spawn', function () { '--cwd', cwd, ], { - foo: 'bar', + detached: false, + stdio: ['inherit', 'inherit', 'pipe'], }) }) }) @@ -75,7 +76,8 @@ describe('lib/exec/spawn', function () { '--cwd', cwd, ], { - foo: 'bar', + detached: false, + stdio: ['inherit', 'inherit', 'pipe'], }) }) }) @@ -138,6 +140,12 @@ describe('lib/exec/spawn', function () { this.spawnedProcess.on.withArgs('close').onFirstCall().yieldsAsync(1) this.spawnedProcess.on.withArgs('close').onSecondCall().yieldsAsync(0) + const buf1 = '[some noise here] Gtk: cannot open display: 987' + + this.spawnedProcess.stderr.on + .withArgs('data') + .yields(buf1) + os.platform.returns('linux') return spawn.start('--foo') diff --git a/cli/test/lib/tasks/verify_spec.js b/cli/test/lib/tasks/verify_spec.js index 193ee78c68d4..8df629664f74 100644 --- a/cli/test/lib/tasks/verify_spec.js +++ b/cli/test/lib/tasks/verify_spec.js @@ -8,6 +8,7 @@ const { stripIndent } = require('common-tags') const { mockSpawn } = require('spawn-mock') const mockfs = require('mock-fs') +const mockedEnv = require('mocked-env') const fs = require(`${lib}/fs`) const util = require(`${lib}/util`) @@ -56,7 +57,7 @@ context('lib/tasks/verify', () => { sinon.stub(_, 'random').returns('222') util.exec - .withArgs(executablePath, ['--smoke-test', '--ping=222', '--enable-logging']) + .withArgs(executablePath, ['--smoke-test', '--ping=222']) .resolves(spawnedProcess) }) @@ -285,7 +286,13 @@ context('lib/tasks/verify', () => { }) describe('smoke test retries on bad display with our XVFB', () => { + let restore + beforeEach(() => { + restore = mockedEnv({ + DISPLAY: 'test-display', + }) + createfs({ alreadyVerified: false, executable: mockfs.file({ mode: 0777 }), @@ -296,14 +303,16 @@ context('lib/tasks/verify', () => { sinon.spy(logger, 'warn') }) + afterEach(() => { + restore() + }) + it('successfully retries with our XVFB on Linux', () => { // initially we think the user has everything set xvfb.isNeeded.returns(false) + sinon.stub(util, 'isPossibleLinuxWithIncorrectDisplay').returns(true) sinon.stub(util, 'exec').callsFake(() => { - // using .callsFake to set platform to Linux - // to allow retry logic to work - os.platform.returns('linux') const firstSpawnError = new Error('') // this message contains typical Gtk error shown if X11 is incorrect @@ -325,7 +334,9 @@ context('lib/tasks/verify', () => { return verify.start().then(() => { expect(util.exec).to.have.been.calledTwice // user should have been warned - expect(logger.warn).to.have.been.calledOnce + expect(logger.warn).to.have.been.calledWithMatch( + 'This is likely due to a misconfigured DISPLAY environment variable.' + ) }) }) @@ -333,10 +344,12 @@ context('lib/tasks/verify', () => { // initially we think the user has everything set xvfb.isNeeded.returns(false) + sinon.stub(util, 'isPossibleLinuxWithIncorrectDisplay').returns(true) + sinon.stub(util, 'exec').callsFake(() => { + os.platform.returns('linux') expect(xvfb.start).to.not.have.been.called - os.platform.returns('linux') const firstSpawnError = new Error('') // this message contains typical Gtk error shown if X11 is incorrect @@ -349,10 +362,12 @@ context('lib/tasks/verify', () => { // the second time it runs, it fails for some other reason const secondMessage = stripIndent` + [some noise here] Gtk: cannot open display: 987 some other error again with some weird indent ` + util.exec.withArgs(executablePath).rejects(new Error(secondMessage)) return Promise.reject(firstSpawnError) @@ -360,14 +375,15 @@ context('lib/tasks/verify', () => { return verify.start().then(() => { throw new Error('Should have failed') - }, (e) => { + }) + .catch((e) => { expect(util.exec).to.have.been.calledTwice // second time around we should have called XVFB expect(xvfb.start).to.have.been.calledOnce expect(xvfb.stop).to.have.been.calledOnce // user should have been warned - expect(logger.warn).to.have.been.calledOnce + expect(logger.warn).to.have.been.calledWithMatch('DISPLAY was set to: "test-display"') snapshot('tried to verify twice, on the first try got the DISPLAY error', e.message) }) @@ -511,6 +527,7 @@ context('lib/tasks/verify', () => { describe('on linux', () => { beforeEach(() => { xvfb.isNeeded.returns(true) + createfs({ alreadyVerified: false, executable: mockfs.file({ mode: 0777 }), @@ -533,10 +550,17 @@ context('lib/tasks/verify', () => { it('logs error and exits when starting xvfb fails', () => { const err = new Error('test without xvfb') + xvfb.start.restore() + + err.nonZeroExitCode = true err.stack = 'xvfb? no dice' - xvfb.start.rejects(err) + sinon.stub(xvfb._xvfb, 'startAsync').rejects(err) - return verify.start().catch((err) => { + return verify.start() + .then(() => { + throw new Error('should have thrown') + }) + .catch((err) => { expect(xvfb.stop).to.be.calledOnce logger.error(err) @@ -590,7 +614,7 @@ context('lib/tasks/verify', () => { customDir: '/real/custom', }) util.exec - .withArgs(realEnvBinaryPath, ['--smoke-test', '--ping=222', '--enable-logging']) + .withArgs(realEnvBinaryPath, ['--smoke-test', '--ping=222']) .resolves(spawnedProcess) return verify.start().then(() => { @@ -598,7 +622,8 @@ context('lib/tasks/verify', () => { snapshot('valid CYPRESS_RUN_BINARY 1', normalize(stdout.toString())) }) }) - ;['darwin', 'linux', 'win32'].forEach((platform) => { + + _.each(['darwin', 'linux', 'win32'], (platform) => { return it('can log error to user', () => { process.env.CYPRESS_RUN_BINARY = '/custom/' os.platform.returns(platform) diff --git a/cli/test/lib/util_spec.js b/cli/test/lib/util_spec.js index 0070944265d2..210655babd2e 100644 --- a/cli/test/lib/util_spec.js +++ b/cli/test/lib/util_spec.js @@ -15,13 +15,14 @@ describe('util', () => { sinon.stub(logger, 'error') }) - context('.isDisplayError', () => { + context('.isBrokenGtkDisplay', () => { it('detects only GTK message', () => { os.platform.returns('linux') const text = '[some noise here] Gtk: cannot open display: 99' - expect(util.isDisplayError(text)).to.be.true + + expect(util.isBrokenGtkDisplay(text)).to.be.true // and not for the other messages - expect(util.isDisplayError('display was set incorrectly')).to.be.false + expect(util.isBrokenGtkDisplay('display was set incorrectly')).to.be.false }) }) diff --git a/package.json b/package.json index b9bc1a873230..2986f766565b 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "cypress", "productName": "Cypress", - "version": "3.2.0", + "version": "3.3.0", "description": "Cypress.io end to end testing tool", "private": true, "engines": { @@ -12,6 +12,7 @@ "start": "node ./cli/bin/cypress open --dev --global", "cypress:open": "node ./cli/bin/cypress open --dev --global", "cypress:run": "node ./cli/bin/cypress run --dev", + "cypress:verify": "node ./cli/bin/cypress verify --dev", "cypress:open:debug": "node ./scripts/debug.js cypress:open", "cypress:run:debug": "node ./scripts/debug.js cypress:run", "dev": "node ./scripts/start.js", @@ -122,7 +123,7 @@ "gulp-typescript": "3.2.4", "hasha": "5.0.0", "human-interval": "0.1.6", - "husky": "0.14.3", + "husky": "2.3.0", "inquirer": "3.3.0", "inquirer-confirm": "2.0.3", "js-codemod": "cpojer/js-codemod#29dafed", diff --git a/packages/driver/src/cy/commands/querying.coffee b/packages/driver/src/cy/commands/querying.coffee index 9136de24676f..d999f385244f 100644 --- a/packages/driver/src/cy/commands/querying.coffee +++ b/packages/driver/src/cy/commands/querying.coffee @@ -126,9 +126,16 @@ module.exports = (Commands, Cypress, cy, state, config) -> options._log.set(obj) - ## we always want to strip everything after the first '.' - ## since we support alias propertys like '1' or 'all' - if aliasObj = cy.getAlias(selector.split(".")[0]) + ## We want to strip everything after the last '.' + ## only when it is potentially a number or 'all' + if _.indexOf(selector, ".") == -1 || + selector.slice(1) in _.keys(cy.state("aliases")) + toSelect = selector + else + allParts = _.split(selector, '.') + toSelect = _.join(_.dropRight(allParts, 1), '.') + + if aliasObj = cy.getAlias(toSelect) {subject, alias, command} = aliasObj return do resolveAlias = -> @@ -176,7 +183,11 @@ module.exports = (Commands, Cypress, cy, state, config) -> ## if this is a route command when command.get("name") is "route" - alias = _.compact([alias, selector.split(".")[1]]).join(".") + if !(_.indexOf(selector, ".") == -1 || + selector.slice(1) in _.keys(cy.state("aliases"))) + allParts = _.split(selector, ".") + index = _.last(allParts) + alias = _.join([alias, index], ".") requests = cy.getRequestsByAlias(alias) ? null log(requests, "route") return requests @@ -206,7 +217,7 @@ module.exports = (Commands, Cypress, cy, state, config) -> try $el = cy.$$(selector, options.withinSubject) catch e - e.onFail = -> options._log.error(e) + e.onFail = -> if options.log is false then e else options._log.error(e) throw e ## if that didnt find anything and we have a within subject diff --git a/packages/driver/src/cy/commands/waiting.coffee b/packages/driver/src/cy/commands/waiting.coffee index c2eb47718b5e..bc15c6a4e176 100644 --- a/packages/driver/src/cy/commands/waiting.coffee +++ b/packages/driver/src/cy/commands/waiting.coffee @@ -66,9 +66,15 @@ module.exports = (Commands, Cypress, cy, state, config) -> , options waitForXhr = (str, options) -> - ## we always want to strip everything after the first '.' + ## we always want to strip everything after the last '.' ## since we support alias property 'request' - [str, str2] = str.split(".") + if _.indexOf(str, ".") == -1 || + str.slice(1) in _.keys(cy.state("aliases")) + [str, str2] = [str, null] + else + # potentially request, response or index + allParts = _.split(str, '.') + [str, str2] = [_.join(_.dropRight(allParts, 1), '.'), _.last(allParts)] if not aliasObj = cy.getAlias(str, "wait", log) cy.aliasNotFoundFor(str, "wait", log) @@ -79,7 +85,7 @@ module.exports = (Commands, Cypress, cy, state, config) -> {alias, command} = aliasObj str = _.compact([alias, str2]).join(".") - + type = cy.getXhrTypeByAlias(str) [ index, num ] = getNumRequests(state, alias) diff --git a/packages/driver/src/cy/xhrs.coffee b/packages/driver/src/cy/xhrs.coffee index 6b3c375bcde3..48011b87bb59 100644 --- a/packages/driver/src/cy/xhrs.coffee +++ b/packages/driver/src/cy/xhrs.coffee @@ -24,7 +24,11 @@ xhrNotWaitedOnByIndex = (state, alias, index, prop) -> create = (state) -> return { getIndexedXhrByAlias: (alias, index) -> - [str, prop] = alias.split(".") + if _.indexOf(alias, ".") == -1 + [str, prop] = [alias, null] + else + allParts = _.split(alias, '.') + [str, prop] = [_.join(_.dropRight(allParts, 1), '.'), _.last(allParts)] if prop if prop is "request" @@ -38,7 +42,12 @@ create = (state) -> xhrNotWaitedOnByIndex(state, str, index, "responses") getRequestsByAlias: (alias) -> - [alias, prop] = alias.split(".") + if _.indexOf(alias, ".") == -1 || alias in _.keys(cy.state("aliases")) + [alias, prop] = [alias, null] + else + # potentially valid prop + allParts = _.split(alias, '.') + [alias, prop] = [_.join(_.dropRight(allParts, 1), '.'), _.last(allParts)] if prop and not validAliasApiRe.test(prop) $utils.throwErrByPath "get.alias_invalid", { diff --git a/packages/driver/test/cypress/integration/commands/agents_spec.coffee b/packages/driver/test/cypress/integration/commands/agents_spec.coffee index 2f2f87b1e2bd..2bd78d910c5b 100644 --- a/packages/driver/test/cypress/integration/commands/agents_spec.coffee +++ b/packages/driver/test/cypress/integration/commands/agents_spec.coffee @@ -201,59 +201,115 @@ describe "src/cy/commands/agents", -> expect(@consoleProps[" 1.2 matching arguments"]).to.eql(["foo", "baz"]) describe ".as", -> - beforeEach -> - @logs = [] - cy.on "log:added", (attrs, log) => - @logs.push(log) + context "without dots", -> + beforeEach -> + @logs = [] + cy.on "log:added", (attrs, log) => + @logs.push(log) - @stub = cy.stub().as("myStub") + @stub = cy.stub().as("myStub") - it "returns stub", -> - expect(@stub).to.have.property("callCount") + it "returns stub", -> + expect(@stub).to.have.property("callCount") - it "updates instrument log with alias", -> - expect(@logs[0].get("alias")).to.eq("myStub") - expect(@logs[0].get("aliasType")).to.eq("agent") + it "updates instrument log with alias", -> + expect(@logs[0].get("alias")).to.eq("myStub") + expect(@logs[0].get("aliasType")).to.eq("agent") - it "includes alias in invocation log", -> - @stub() - expect(@logs[1].get("alias")).to.eql(["myStub"]) - expect(@logs[1].get("aliasType")).to.eq("agent") + it "includes alias in invocation log", -> + @stub() + expect(@logs[1].get("alias")).to.eql(["myStub"]) + expect(@logs[1].get("aliasType")).to.eq("agent") - it "includes alias in console props", -> - @stub() - consoleProps = @logs[1].get("consoleProps")() - expect(consoleProps["Alias"]).to.eql("myStub") + it "includes alias in console props", -> + @stub() + consoleProps = @logs[1].get("consoleProps")() + expect(consoleProps["Alias"]).to.eql("myStub") + + it "updates the displayName of the agent", -> + expect(@myStub.displayName).to.eq("myStub") + + it "stores the lookup as an alias", -> + expect(cy.state("aliases").myStub).to.be.defined + + it "stores the agent as the subject", -> + expect(cy.state("aliases").myStub.subject).to.eq(@stub) + + it "assigns subject to runnable ctx", -> + expect(@myStub).to.eq(@stub) + + it "retries until assertions pass", -> + cy.on "command:retry", _.after 2, => + @myStub("foo") + + cy.get("@myStub").should("be.calledWith", "foo") + + describe "errors", -> + _.each [null, undefined, {}, [], 123], (value) => + it "throws when passed: #{value}", -> + expect(=> cy.stub().as(value)).to.throw("cy.as() can only accept a string.") + + it "throws on blank string", -> + expect(=> cy.stub().as("")).to.throw("cy.as() cannot be passed an empty string.") + + _.each ["test", "runnable", "timeout", "slow", "skip", "inspect"], (blacklist) -> + it "throws on a blacklisted word: #{blacklist}", -> + expect(=> cy.stub().as(blacklist)).to.throw("cy.as() cannot be aliased as: '#{blacklist}'. This word is reserved.") + + context "with dots", -> + beforeEach -> + @logs = [] + cy.on "log:added", (attrs, log) => + @logs.push(log) + + @stub = cy.stub().as("my.stub") + + it "returns stub", -> + expect(@stub).to.have.property("callCount") + + it "updates instrument log with alias", -> + expect(@logs[0].get("alias")).to.eq("my.stub") + expect(@logs[0].get("aliasType")).to.eq("agent") + + it "includes alias in invocation log", -> + @stub() + expect(@logs[1].get("alias")).to.eql(["my.stub"]) + expect(@logs[1].get("aliasType")).to.eq("agent") + + it "includes alias in console props", -> + @stub() + consoleProps = @logs[1].get("consoleProps")() + expect(consoleProps["Alias"]).to.eql("my.stub") - it "updates the displayName of the agent", -> - expect(@myStub.displayName).to.eq("myStub") + it "updates the displayName of the agent", -> + expect(@["my.stub"].displayName).to.eq("my.stub") - it "stores the lookup as an alias", -> - expect(cy.state("aliases").myStub).to.be.defined + it "stores the lookup as an alias", -> + expect(cy.state("aliases")["my.stub"]).to.be.defined - it "stores the agent as the subject", -> - expect(cy.state("aliases").myStub.subject).to.eq(@stub) + it "stores the agent as the subject", -> + expect(cy.state("aliases")["my.stub"].subject).to.eq(@stub) - it "assigns subject to runnable ctx", -> - expect(@myStub).to.eq(@stub) + it "assigns subject to runnable ctx", -> + expect(@["my.stub"]).to.eq(@stub) - it "retries until assertions pass", -> - cy.on "command:retry", _.after 2, => - @myStub("foo") - - cy.get("@myStub").should("be.calledWith", "foo") + it "retries until assertions pass", -> + cy.on "command:retry", _.after 2, => + @["my.stub"]("foo") + + cy.get("@my.stub").should("be.calledWith", "foo") - describe "errors", -> - _.each [null, undefined, {}, [], 123], (value) => - it "throws when passed: #{value}", -> - expect(=> cy.stub().as(value)).to.throw("cy.as() can only accept a string.") + describe "errors", -> + _.each [null, undefined, {}, [], 123], (value) => + it "throws when passed: #{value}", -> + expect(=> cy.stub().as(value)).to.throw("cy.as() can only accept a string.") - it "throws on blank string", -> - expect(=> cy.stub().as("")).to.throw("cy.as() cannot be passed an empty string.") + it "throws on blank string", -> + expect(=> cy.stub().as("")).to.throw("cy.as() cannot be passed an empty string.") - _.each ["test", "runnable", "timeout", "slow", "skip", "inspect"], (blacklist) -> - it "throws on a blacklisted word: #{blacklist}", -> - expect(=> cy.stub().as(blacklist)).to.throw("cy.as() cannot be aliased as: '#{blacklist}'. This word is reserved.") + _.each ["test", "runnable", "timeout", "slow", "skip", "inspect"], (blacklist) -> + it "throws on a blacklisted word: #{blacklist}", -> + expect(=> cy.stub().as(blacklist)).to.throw("cy.as() cannot be aliased as: '#{blacklist}'. This word is reserved.") describe "logging", -> beforeEach -> @@ -468,4 +524,4 @@ describe "src/cy/commands/agents", -> expect(@agents.spy).to.be.a("function") expect(@agents.spy().callCount).to.be.a("number") expect(@agents.stub).to.be.a("function") - expect(@agents.stub().returns).to.be.a("function") + expect(@agents.stub().returns).to.be.a("function") \ No newline at end of file diff --git a/packages/driver/test/cypress/integration/commands/aliasing_spec.coffee b/packages/driver/test/cypress/integration/commands/aliasing_spec.coffee index 59a28cfe3806..8e4b2fb84c29 100644 --- a/packages/driver/test/cypress/integration/commands/aliasing_spec.coffee +++ b/packages/driver/test/cypress/integration/commands/aliasing_spec.coffee @@ -51,6 +51,20 @@ describe "src/cy/commands/aliasing", -> cy.get("@obj").should("deep.eq", { foo: "bar" }) + it "allows dot in alias names", -> + cy.get("body").as("body.foo").then -> + expect(cy.get('@body.foo')).to.be.defined + expect(cy.state("aliases")['body.foo']).to.be.defined + + it "recognizes dot and non dot with same alias names", -> + cy.get("body").as("body").then -> + expect(cy.get('@body')).to.be.defined + expect(cy.state("aliases")['body']).to.be.defined + cy.contains("foo").as("body.foo").then -> + expect(cy.get('@body.foo')).to.be.defined + expect(cy.get('@body.foo')).to.not.eq(cy.get('@body')) + expect(cy.state("aliases")['body.foo']).to.be.defined + context "DOM subjects", -> it "assigns the remote jquery instance", -> obj = {} @@ -75,6 +89,10 @@ describe "src/cy/commands/aliasing", -> .noop({}).as("baz").then (obj) -> expect(@baz).to.eq obj + it "assigns subject with dot to runnable ctx", -> + cy.noop({}).as("bar.baz").then (obj) -> + expect(@["bar.baz"]).to.eq obj + describe "nested hooks", -> afterEach -> if not @bar @@ -130,6 +148,13 @@ describe "src/cy/commands/aliasing", -> cy.get("div:first").as("@myAlias") + it "throws on alias starting with @ char and dots", (done) -> + cy.on "fail", (err) -> + expect(err.message).to.eq "'@my.alias' cannot be named starting with the '@' symbol. Try renaming the alias to 'my.alias', or something else that does not start with the '@' symbol." + done() + + cy.get("div:first").as("@my.alias") + it "does not throw on alias with @ char in non-starting position", () -> cy.get("div:first").as("my@Alias") cy.get("@my@Alias") @@ -157,7 +182,6 @@ describe "src/cy/commands/aliasing", -> lastLog = @lastLog expect(lastLog.get("aliasType")).to.eq "primitive" - it "sets aliasType to 'dom'", -> cy.get("body").find("button:first").click().as("button").then -> lastLog = @lastLog @@ -198,11 +222,6 @@ describe "src/cy/commands/aliasing", -> expect(@logs[1].get("name")).to.eq("route") expect(@logs[1].get("alias")).to.eq("getFoo") - # it "does not alias previous logs when no matching chainerId", -> - # cy - # .get("div:first") - # .noop({}).as("foo").then -> - context "#replayCommandsFrom", -> describe "subject in document", -> it "returns if subject is still in the document", -> @@ -349,7 +368,7 @@ describe "src/cy/commands/aliasing", -> .get("body").as("b") .get("input:first").as("firstInput") .get("@lastDiv") - + it "throws when alias is missing '@' but matches an available alias", (done) -> cy.on "fail", (err) -> expect(err.message).to.eq "Invalid alias: 'getAny'.\nYou forgot the '@'. It should be written as: '@getAny'." @@ -358,4 +377,4 @@ describe "src/cy/commands/aliasing", -> cy .server() .route("*", {}).as("getAny") - .wait("getAny").then -> + .wait("getAny").then -> \ No newline at end of file diff --git a/packages/driver/test/cypress/integration/commands/querying_spec.coffee b/packages/driver/test/cypress/integration/commands/querying_spec.coffee index 84f08e912745..0d782519cfdc 100644 --- a/packages/driver/test/cypress/integration/commands/querying_spec.coffee +++ b/packages/driver/test/cypress/integration/commands/querying_spec.coffee @@ -732,18 +732,17 @@ describe "src/cy/commands/querying", -> expect(@lastLog.get("$el").get(0)).not.to.be.ok it "logs route aliases", -> - cy - .visit("http://localhost:3500/fixtures/jquery.html") - .server() - .route(/users/, {}).as("getUsers") - .window().then { timeout: 2000 }, (win) -> - win.$.get("/users") - .get("@getUsers").then -> - expect(@lastLog.pick("message", "referencesAlias", "aliasType")).to.deep.eq { - message: "@getUsers" - referencesAlias: {name: "getUsers"} - aliasType: "route" - } + cy.visit("http://localhost:3500/fixtures/jquery.html") + cy.server() + cy.route(/users/, {}).as("get.users") + cy.window().then { timeout: 2000 }, (win) -> + win.$.get("/users") + cy.get("@get.users").then -> + expect(@lastLog.pick("message", "referencesAlias", "aliasType")).to.deep.eq { + message: "@get.users" + referencesAlias: {name: "get.users"} + aliasType: "route" + } it "logs primitive aliases", (done) -> cy.on "log:added", (attrs, log) -> @@ -885,6 +884,15 @@ describe "src/cy/commands/querying", -> .get("@getUsers").then (xhr) -> expect(xhr.url).to.include "/users" + it "handles dots in alias name", -> + cy.server() + cy.route(/users/, {}).as("get.users") + cy.visit("http://localhost:3500/fixtures/jquery.html") + cy.window().then { timeout: 2000 }, (win) -> + win.$.get("/users") + cy.get("@get.users").then (xhr) -> + expect(xhr.url).to.include "/users" + it "returns null if no xhr is found", -> cy .server() @@ -908,6 +916,20 @@ describe "src/cy/commands/querying", -> expect(xhrs[0].url).to.include "/users?num=1" expect(xhrs[1].url).to.include "/users?num=2" + it "returns an array of xhrs when dots in alias name", -> + cy.visit("http://localhost:3500/fixtures/jquery.html") + cy.server() + cy.route(/users/, {}).as("get.users") + cy.window().then { timeout: 2000 }, (win) -> + Promise.all([ + win.$.get("/users", {num: 1}) + win.$.get("/users", {num: 2}) + ]) + cy.get("@get.users.all").then (xhrs) -> + expect(xhrs).to.be.an("array") + expect(xhrs[0].url).to.include "/users?num=1" + expect(xhrs[1].url).to.include "/users?num=2" + it "returns the 1st xhr", -> cy .visit("http://localhost:3500/fixtures/jquery.html") @@ -934,6 +956,18 @@ describe "src/cy/commands/querying", -> .get("@getUsers.2").then (xhr2) -> expect(xhr2.url).to.include "/users?num=2" + it "returns the 2nd xhr when dots in alias", -> + cy.visit("http://localhost:3500/fixtures/jquery.html") + cy.server() + cy.route(/users/, {}).as("get.users") + cy.window().then { timeout: 2000 }, (win) -> + Promise.all([ + win.$.get("/users", {num: 1}) + win.$.get("/users", {num: 2}) + ]) + cy.get("@get.users.2").then (xhr2) -> + expect(xhr2.url).to.include "/users?num=2" + it "returns the 3rd xhr as null", -> cy .server() @@ -1627,4 +1661,4 @@ describe "src/cy/commands/querying", -> cy.on "command:retry", _.after 2, -> Cypress.stop() - cy.contains(/^does not contain asdfasdf at all$/) + cy.contains(/^does not contain asdfasdf at all$/) \ No newline at end of file diff --git a/packages/driver/test/cypress/integration/commands/waiting_spec.coffee b/packages/driver/test/cypress/integration/commands/waiting_spec.coffee index 7f14aaad37e1..5fadf4edfed6 100644 --- a/packages/driver/test/cypress/integration/commands/waiting_spec.coffee +++ b/packages/driver/test/cypress/integration/commands/waiting_spec.coffee @@ -379,7 +379,7 @@ describe "src/cy/commands/waiting", -> Cypress.config("requestTimeout", 200) cy.on "fail", (err) -> - expect(err.message).to.include "cy.wait() timed out waiting 200ms for the 3rd request to the route: 'getUsers'. No request ever occurred." + expect(err.message).to.include "cy.wait() timed out waiting 200ms for the 3rd request to the route: 'get.users'. No request ever occurred." done() cy.on "command:retry", => @@ -390,10 +390,9 @@ describe "src/cy/commands/waiting", -> win = cy.state("window") win.$.get("/users", {num: response}) - cy - .server() - .route(/users/, resp).as("getUsers") - .wait(["@getUsers", "@getUsers", "@getUsers"]) + cy.server() + cy.route(/users/, resp).as("get.users") + cy.wait(["@get.users", "@get.users", "@get.users"]) it "throws waiting for the 2nd response", (done) -> resp = {foo: "foo"} @@ -517,15 +516,14 @@ describe "src/cy/commands/waiting", -> _.defer => win.$.get("/timeout?ms=2002") cy.on "fail", (err) -> - expect(err.message).to.include "cy.wait() timed out waiting 500ms for the 1st request to the route: 'getThree'. No request ever occurred." + expect(err.message).to.include "cy.wait() timed out waiting 500ms for the 1st request to the route: 'get.three'. No request ever occurred." done() - cy - .server() - .route("/timeout?ms=2001").as("getOne") - .route("/timeout?ms=2002").as("getTwo") - .route(/three/, {}).as("getThree") - .wait(["@getOne", "@getTwo", "@getThree"]) + cy.server() + cy.route("/timeout?ms=2001").as("getOne") + cy.route("/timeout?ms=2002").as("getTwo") + cy.route(/three/, {}).as("get.three") + cy.wait(["@getOne", "@getTwo", "@get.three"]) it "throws when waiting on the 3rd response on array of aliases", (done) -> Cypress.config("requestTimeout", 200) @@ -570,17 +568,16 @@ describe "src/cy/commands/waiting", -> resp1 = {foo: "foo"} resp2 = {bar: "bar"} - cy - .server() - .route(/users/, resp1).as("getUsers") - .route(/posts/, resp2).as("getPosts") - .window().then (win) -> - win.$.get("/users") - win.$.get("/posts") - null - .wait(["@getUsers", "@getPosts"]).spread (xhr1, xhr2) -> - expect(xhr1.responseBody).to.deep.eq resp1 - expect(xhr2.responseBody).to.deep.eq resp2 + cy.server() + cy.route(/users/, resp1).as("getUsers") + cy.route(/posts/, resp2).as("get.posts") + cy.window().then (win) -> + win.$.get("/users") + win.$.get("/posts") + null + cy.wait(["@getUsers", "@get.posts"]).spread (xhr1, xhr2) -> + expect(xhr1.responseBody).to.deep.eq resp1 + expect(xhr2.responseBody).to.deep.eq resp2 describe "multiple separate alias waits", -> before -> @@ -918,4 +915,4 @@ describe "src/cy/commands/waiting", -> # Command: "wait" # "Waited For": _.str.clean(fn.toString()) # Retried: "3 times" - # } + # } \ No newline at end of file diff --git a/packages/driver/test/cypress/integration/commands/xhr_spec.coffee b/packages/driver/test/cypress/integration/commands/xhr_spec.coffee index 87501f75bdc1..497fec854fe9 100644 --- a/packages/driver/test/cypress/integration/commands/xhr_spec.coffee +++ b/packages/driver/test/cypress/integration/commands/xhr_spec.coffee @@ -188,7 +188,7 @@ describe "src/cy/commands/xhr", -> cy .server() - .route({url: /timeout/}).as("getTimeout") + .route({url: /timeout/}).as("get.timeout") .window().then (win) -> xhr = new win.XMLHttpRequest xhr.open("GET", "/timeout?ms=100") @@ -198,7 +198,7 @@ describe "src/cy/commands/xhr", -> xhr.onload = -> onloaded = true null - .wait("@getTimeout").then (xhr) -> + .wait("@get.timeout").then (xhr) -> expect(onloaded).to.be.true expect(onreadystatechanged).to.be.true expect(xhr.status).to.eq(200) @@ -2076,4 +2076,4 @@ describe "src/cy/commands/xhr", -> # .window().then (win) -> # win.$.get("/foo") # null - # .respond() + # .respond() \ No newline at end of file diff --git a/packages/driver/test/cypress/integration/issues/3847_spec.js b/packages/driver/test/cypress/integration/issues/3847_spec.js new file mode 100644 index 000000000000..2f68fda57ce8 --- /dev/null +++ b/packages/driver/test/cypress/integration/issues/3847_spec.js @@ -0,0 +1,42 @@ +// https://github.com/cypress-io/cypress/issues/3847 +describe('issue 3847', () => { + // global variable + let queryKey = '\'input\'' + + // like Sizzle throw error + let error = new Error(`Syntax error, unrecognized expression: ${queryKey}`) + + beforeEach(() => { + cy.visit('/fixtures/dom.html') + }) + + it('options default { log: true } should be work without Unhandled rejection', (done) => { + cy.on('fail', (err) => { + expect(err.message).to.eql(error.message) + expect(err.name).to.eql(error.name) + done() + + return false + }) + + // get 'input' + cy.get(queryKey) + }) + + // Unhandled rejection TypeError: Cannot read property 'error' of undefined + it('options { log: false } will not throw Unhandled rejection', (done) => { + + // error should seem like { log: true } + cy.on('fail', (err) => { + expect(err.message).to.eql(error.message) + expect(err.name).to.eql(error.name) + expect(err.message).not.to.match(/Unhandled\srejection\sTypeError/) + done() + + return false + }) + + // get 'input' + cy.get(queryKey, { log: false }) + }) +}) diff --git a/packages/electron/lib/electron.coffee b/packages/electron/lib/electron.coffee index 7fd3b2d6d7f2..c8476a29ec19 100644 --- a/packages/electron/lib/electron.coffee +++ b/packages/electron/lib/electron.coffee @@ -73,7 +73,7 @@ module.exports = { debug("spawning %s with args", execPath, argv) if debug.enabled - # let's see everything Electron spits back + ## enable the internal chromium logger argv.push("--enable-logging") cp.spawn(execPath, argv, {stdio: "inherit"}) diff --git a/packages/server/__snapshots__/7_record_spec.coffee.js b/packages/server/__snapshots__/7_record_spec.coffee.js index b0ad0f250a2c..3473b3e1c2c9 100644 --- a/packages/server/__snapshots__/7_record_spec.coffee.js +++ b/packages/server/__snapshots__/7_record_spec.coffee.js @@ -1537,7 +1537,107 @@ StatusCodeError: 402 ` -exports['e2e record api interaction warnings create run warnings grace period - over limit warns when over private test recordings 1'] = ` + +exports['e2e record api interaction errors create run 402 - free plan exceeds monthly tests errors and exits when on free plan and over recorded tests limit 1'] = ` +You've exceeded the limit of test recordings under your free plan this month. The limit is 500 test recordings. + +To continue recording tests this month you must upgrade your account. Please visit your billing to upgrade to another billing plan. + +https://on.cypress.io/dashboard/organizations/org-id-1234/billing + +` + +exports['e2e record api interaction errors create run 402 - grouping feature not available in plan errors and exits when attempting parallel run when not available in plan 1'] = ` +Grouping is not included under your current billing plan. + +To run your tests with groups, please visit your billing and upgrade to another plan with grouping. + +https://on.cypress.io/dashboard/organizations/org-id-1234/billing + +` + + +exports['e2e record api interaction warnings create run warnings grace period - grouping feature warns when using parallel feature 1'] = ` +Grouping is not included under your free plan. + +Your plan is now in a grace period, which means your tests will still run with groups until 2999-12-31. Please upgrade your plan to continue running your tests with groups in the future. + +https://on.cypress.io/dashboard/organizations/org-id-1234/billing + +==================================================================================================== + + (Run Starting) + + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Cypress: 1.2.3 │ + │ Browser: FooBrowser 88 │ + │ Specs: 1 found (record_pass_spec.coffee) │ + │ Searched: cypress/integration/record_pass* │ + │ Params: Group: false, Parallel: false │ + │ Run URL: https://dashboard.cypress.io/#/projects/cjvoj7/runs/12 │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + + +──────────────────────────────────────────────────────────────────────────────────────────────────── + + Running: record_pass_spec.coffee... (1 of 1) + Estimated: 8 seconds + + + record pass + ✓ passes + - is pending + + + 1 passing + 1 pending + + + (Results) + + ┌───────────────────────────────────────┐ + │ Tests: 2 │ + │ Passing: 1 │ + │ Failing: 0 │ + │ Pending: 1 │ + │ Skipped: 0 │ + │ Screenshots: 1 │ + │ Video: true │ + │ Duration: X seconds │ + │ Estimated: 8 seconds │ + │ Spec Ran: record_pass_spec.coffee │ + └───────────────────────────────────────┘ + + + (Screenshots) + + - /foo/bar/.projects/e2e/cypress/screenshots/record_pass_spec.coffee/yay it passes.png (202x1002) + + + (Uploading Results) + + - Done Uploading (1/1) /foo/bar/.projects/e2e/cypress/screenshots/record_pass_spec.coffee/yay it passes.png + +==================================================================================================== + + (Run Finished) + + + Spec Tests Passing Failing Pending Skipped + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ ✔ record_pass_spec.coffee XX:XX 2 1 - 1 - │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + All specs passed! XX:XX 2 1 - 1 - + + +─────────────────────────────────────────────────────────────────────────────────────────────────────── + + Recorded Run: https://dashboard.cypress.io/#/projects/cjvoj7/runs/12 + + +` + +exports['e2e record api interaction warnings create run warnings grace period - over private tests limit warns when over private test recordings 1'] = ` You've exceeded the limit of private test recordings under your free plan this month. The limit is 500 private test recordings. Your plan is now in a grace period, which means your tests will still be recorded until 2999-12-31. Please upgrade your plan to continue recording tests on the Cypress Dashboard in the future. @@ -1617,10 +1717,10 @@ https://on.cypress.io/dashboard/organizations/org-id-1234/billing ` -exports['e2e record api interaction warnings create run warnings grace period - parallel feature warns when using parallel feature 1'] = ` -Parallelization is not included under your free plan. +exports['e2e record api interaction warnings create run warnings grace period - over tests limit warns when over test recordings 1'] = ` +You've exceeded the limit of test recordings under your free plan this month. The limit is 500 test recordings. -Your plan is now in a grace period, which means your tests will still run in parallel until 2999-12-31. Please upgrade your plan to continue running your tests in parallel in the future. +Your plan is now in a grace period, which means your tests will still be recorded until 2999-12-31. Please upgrade your plan to continue recording tests on the Cypress Dashboard in the future. https://on.cypress.io/dashboard/organizations/org-id-1234/billing @@ -1697,10 +1797,10 @@ https://on.cypress.io/dashboard/organizations/org-id-1234/billing ` -exports['e2e record api interaction warnings create run warnings paid plan - over limit warns when over private test recordings 1'] = ` -You've exceeded the limit of private test recordings under your current billing plan this month. The limit is 500 private test recordings. +exports['e2e record api interaction warnings create run warnings grace period - parallel feature warns when using parallel feature 1'] = ` +Parallelization is not included under your free plan. -To upgrade your account, please visit your billing to upgrade to another billing plan. +Your plan is now in a grace period, which means your tests will still run in parallel until 2999-12-31. Please upgrade your plan to continue running your tests in parallel in the future. https://on.cypress.io/dashboard/organizations/org-id-1234/billing @@ -1777,19 +1877,90 @@ https://on.cypress.io/dashboard/organizations/org-id-1234/billing ` -exports['e2e record api interaction errors create run 402 - grouping feature not available in plan errors and exits when attempting parallel run when not available in plan 1'] = ` -Grouping is not included under your current billing plan. +exports['e2e record api interaction warnings create run warnings paid plan - over private tests limit warns when over private test recordings 1'] = ` +You've exceeded the limit of private test recordings under your current billing plan this month. The limit is 500 private test recordings. -To run your tests with groups, please visit your billing and upgrade to another plan with grouping. +To upgrade your account, please visit your billing to upgrade to another billing plan. https://on.cypress.io/dashboard/organizations/org-id-1234/billing +==================================================================================================== + + (Run Starting) + + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Cypress: 1.2.3 │ + │ Browser: FooBrowser 88 │ + │ Specs: 1 found (record_pass_spec.coffee) │ + │ Searched: cypress/integration/record_pass* │ + │ Params: Group: false, Parallel: false │ + │ Run URL: https://dashboard.cypress.io/#/projects/cjvoj7/runs/12 │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + + +──────────────────────────────────────────────────────────────────────────────────────────────────── + + Running: record_pass_spec.coffee... (1 of 1) + Estimated: 8 seconds + + + record pass + ✓ passes + - is pending + + + 1 passing + 1 pending + + + (Results) + + ┌───────────────────────────────────────┐ + │ Tests: 2 │ + │ Passing: 1 │ + │ Failing: 0 │ + │ Pending: 1 │ + │ Skipped: 0 │ + │ Screenshots: 1 │ + │ Video: true │ + │ Duration: X seconds │ + │ Estimated: 8 seconds │ + │ Spec Ran: record_pass_spec.coffee │ + └───────────────────────────────────────┘ + + + (Screenshots) + + - /foo/bar/.projects/e2e/cypress/screenshots/record_pass_spec.coffee/yay it passes.png (202x1002) + + + (Uploading Results) + + - Done Uploading (1/1) /foo/bar/.projects/e2e/cypress/screenshots/record_pass_spec.coffee/yay it passes.png + +==================================================================================================== + + (Run Finished) + + + Spec Tests Passing Failing Pending Skipped + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ ✔ record_pass_spec.coffee XX:XX 2 1 - 1 - │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + All specs passed! XX:XX 2 1 - 1 - + + +─────────────────────────────────────────────────────────────────────────────────────────────────────── + + Recorded Run: https://dashboard.cypress.io/#/projects/cjvoj7/runs/12 + + ` -exports['e2e record api interaction warnings create run warnings grace period - grouping feature warns when using parallel feature 1'] = ` -Grouping is not included under your free plan. +exports['e2e record api interaction warnings create run warnings paid plan - over tests limit warns when over test recordings 1'] = ` +You've exceeded the limit of test recordings under your current billing plan this month. The limit is 500 test recordings. -Your plan is now in a grace period, which means your tests will still run with groups until 2999-12-31. Please upgrade your plan to continue running your tests with groups in the future. +To upgrade your account, please visit your billing to upgrade to another billing plan. https://on.cypress.io/dashboard/organizations/org-id-1234/billing diff --git a/packages/server/lib/cypress.coffee b/packages/server/lib/cypress.coffee index d9a5ab7e7d9b..95f9c5d3a37a 100644 --- a/packages/server/lib/cypress.coffee +++ b/packages/server/lib/cypress.coffee @@ -60,6 +60,10 @@ module.exports = { ## juggle up the totalFailed since our outer ## promise is expecting this object structure debug("electron finished with", code) + + if mode is "smokeTest" + return resolve(code) + resolve({totalFailed: code}) args = require("./util/args").toArray(options) @@ -179,10 +183,16 @@ module.exports = { .catch(exitErr) when "smokeTest" - require("./modes/smoke_test")(options) - .then (pong) -> - console.log(pong) - .then(exit0) + @runElectron(mode, options) + .then (pong) => + if not @isCurrentlyRunningElectron() + return pong + + if pong is options.ping + return 0 + + return 1 + .then(exit) .catch(exitErr) when "returnPkg" diff --git a/packages/server/lib/errors.coffee b/packages/server/lib/errors.coffee index 78c34e16cad4..a125bfac5408 100644 --- a/packages/server/lib/errors.coffee +++ b/packages/server/lib/errors.coffee @@ -690,7 +690,7 @@ getMsgByType = (type, arg1 = {}, arg2) -> """ when "FREE_PLAN_EXCEEDS_MONTHLY_PRIVATE_TESTS" """ - You've exceeded the limit of private test recordings under your free plan this month. #{arg1.usedMessage} + You've exceeded the limit of private test recordings under your free plan this month. #{arg1.usedTestsMessage} To continue recording tests this month you must upgrade your account. Please visit your billing to upgrade to another billing plan. @@ -698,7 +698,7 @@ getMsgByType = (type, arg1 = {}, arg2) -> """ when "FREE_PLAN_IN_GRACE_PERIOD_EXCEEDS_MONTHLY_PRIVATE_TESTS" """ - You've exceeded the limit of private test recordings under your free plan this month. #{arg1.usedMessage} + You've exceeded the limit of private test recordings under your free plan this month. #{arg1.usedTestsMessage} Your plan is now in a grace period, which means your tests will still be recorded until #{arg1.gracePeriodMessage}. Please upgrade your plan to continue recording tests on the Cypress Dashboard in the future. @@ -706,7 +706,31 @@ getMsgByType = (type, arg1 = {}, arg2) -> """ when "PAID_PLAN_EXCEEDS_MONTHLY_PRIVATE_TESTS" """ - You've exceeded the limit of private test recordings under your current billing plan this month. #{arg1.usedMessage} + You've exceeded the limit of private test recordings under your current billing plan this month. #{arg1.usedTestsMessage} + + To upgrade your account, please visit your billing to upgrade to another billing plan. + + #{arg1.link} + """ + when "FREE_PLAN_EXCEEDS_MONTHLY_TESTS" + """ + You've exceeded the limit of test recordings under your free plan this month. #{arg1.usedTestsMessage} + + To continue recording tests this month you must upgrade your account. Please visit your billing to upgrade to another billing plan. + + #{arg1.link} + """ + when "FREE_PLAN_IN_GRACE_PERIOD_EXCEEDS_MONTHLY_TESTS" + """ + You've exceeded the limit of test recordings under your free plan this month. #{arg1.usedTestsMessage} + + Your plan is now in a grace period, which means your tests will still be recorded until #{arg1.gracePeriodMessage}. Please upgrade your plan to continue recording tests on the Cypress Dashboard in the future. + + #{arg1.link} + """ + when "PAID_PLAN_EXCEEDS_MONTHLY_TESTS" + """ + You've exceeded the limit of test recordings under your current billing plan this month. #{arg1.usedTestsMessage} To upgrade your account, please visit your billing to upgrade to another billing plan. diff --git a/packages/server/lib/modes/index.coffee b/packages/server/lib/modes/index.coffee index 1d03a9ab7e74..ffef482abfa3 100644 --- a/packages/server/lib/modes/index.coffee +++ b/packages/server/lib/modes/index.coffee @@ -6,3 +6,5 @@ module.exports = (mode, options) -> require("./run").run(options) when "interactive" require("./interactive").run(options) + when "smokeTest" + require("./smoke_test").run(options) diff --git a/packages/server/lib/modes/record.coffee b/packages/server/lib/modes/record.coffee index b43269d61b6a..b2f93bbf5baa 100644 --- a/packages/server/lib/modes/record.coffee +++ b/packages/server/lib/modes/record.coffee @@ -231,9 +231,9 @@ getCommitFromGitOrCi = (git) -> defaultBranch: null }) -usedMessage = (limit) -> +usedTestsMessage = (limit, phrase) -> if _.isFinite(limit) - "The limit is #{chalk.blue(limit)} private test recordings." + "The limit is #{chalk.blue(limit)} #{phrase} recordings." else "" @@ -307,7 +307,13 @@ createRun = (options = {}) -> switch warning.code when "FREE_PLAN_IN_GRACE_PERIOD_EXCEEDS_MONTHLY_PRIVATE_TESTS" errors.warning("FREE_PLAN_IN_GRACE_PERIOD_EXCEEDS_MONTHLY_PRIVATE_TESTS", { - usedMessage: usedMessage(warning.limit) + usedTestsMessage: usedTestsMessage(warning.limit, "private test") + gracePeriodMessage: gracePeriodMessage(warning.gracePeriodEnds) + link: billingLink(warning.orgId) + }) + when "FREE_PLAN_IN_GRACE_PERIOD_EXCEEDS_MONTHLY_TESTS" + errors.warning("FREE_PLAN_IN_GRACE_PERIOD_EXCEEDS_MONTHLY_TESTS", { + usedTestsMessage: usedTestsMessage(warning.limit, "test") gracePeriodMessage: gracePeriodMessage(warning.gracePeriodEnds) link: billingLink(warning.orgId) }) @@ -318,7 +324,12 @@ createRun = (options = {}) -> }) when "PAID_PLAN_EXCEEDS_MONTHLY_PRIVATE_TESTS" errors.warning("PAID_PLAN_EXCEEDS_MONTHLY_PRIVATE_TESTS", { - usedMessage: usedMessage(warning.limit) + usedTestsMessage: usedTestsMessage(warning.limit, "private test") + link: billingLink(warning.orgId) + }) + when "PAID_PLAN_EXCEEDS_MONTHLY_TESTS" + errors.warning("PAID_PLAN_EXCEEDS_MONTHLY_TESTS", { + usedTestsMessage: usedTestsMessage(warning.limit, "test") link: billingLink(warning.orgId) }) when "PLAN_IN_GRACE_PERIOD_RUN_GROUPING_FEATURE_USED" @@ -345,7 +356,12 @@ createRun = (options = {}) -> switch code when "FREE_PLAN_EXCEEDS_MONTHLY_PRIVATE_TESTS" errors.throw("FREE_PLAN_EXCEEDS_MONTHLY_PRIVATE_TESTS", { - usedMessage: usedMessage(limit) + usedTestsMessage: usedTestsMessage(limit, "private test") + link: billingLink(orgId) + }) + when "FREE_PLAN_EXCEEDS_MONTHLY_TESTS" + errors.throw("FREE_PLAN_EXCEEDS_MONTHLY_TESTS", { + usedTestsMessage: usedTestsMessage(limit, "test") link: billingLink(orgId) }) when "PARALLEL_FEATURE_NOT_AVAILABLE_IN_PLAN" diff --git a/packages/server/lib/modes/smoke_test.coffee b/packages/server/lib/modes/smoke_test.coffee index 62ff5c05a6d1..a3bf00cb0ccb 100644 --- a/packages/server/lib/modes/smoke_test.coffee +++ b/packages/server/lib/modes/smoke_test.coffee @@ -1,2 +1,6 @@ -module.exports = (options) -> - Promise.resolve(options.pong) \ No newline at end of file +module.exports = { + run: (options) -> + console.log(options.pong) + + return options.pong +} diff --git a/packages/server/lib/util/ci_provider.js b/packages/server/lib/util/ci_provider.js index 6cae50460550..ccb23a692076 100644 --- a/packages/server/lib/util/ci_provider.js +++ b/packages/server/lib/util/ci_provider.js @@ -282,7 +282,12 @@ const _providerCommitParams = function () { return { appveyor: { sha: env.APPVEYOR_REPO_COMMIT, - branch: env.APPVEYOR_REPO_BRANCH, + // since APPVEYOR_REPO_BRANCH will be the target branch on a PR + // we need to use PULL_REQUEST_HEAD_REPO_BRANCH if it exists. + // e.g. if you have a PR: develop <- my-feature-branch + // my-feature-branch is APPVEYOR_PULL_REQUEST_HEAD_REPO_BRANCH + // develop is APPVEYOR_REPO_BRANCH + branch: env.APPVEYOR_PULL_REQUEST_HEAD_REPO_BRANCH || env.APPVEYOR_REPO_BRANCH, message: join('\n', env.APPVEYOR_REPO_COMMIT_MESSAGE, env.APPVEYOR_REPO_COMMIT_MESSAGE_EXTENDED), authorName: env.APPVEYOR_REPO_COMMIT_AUTHOR, authorEmail: env.APPVEYOR_REPO_COMMIT_AUTHOR_EMAIL, diff --git a/packages/server/test/e2e/7_record_spec.coffee b/packages/server/test/e2e/7_record_spec.coffee index d07315e29296..3b3b211616d6 100644 --- a/packages/server/test/e2e/7_record_spec.coffee +++ b/packages/server/test/e2e/7_record_spec.coffee @@ -829,6 +829,30 @@ describe "e2e record", -> expectedExitCode: 1 }) + describe "create run 402 - free plan exceeds monthly tests", -> + setup([{ + method: "post" + url: "/runs" + req: "postRunRequest@2.1.0", + res: (req, res) -> res.status(402).json({ + code: "FREE_PLAN_EXCEEDS_MONTHLY_TESTS" + payload: { + used: 600 + limit: 500 + orgId: "org-id-1234" + } + }) + }]) + + it "errors and exits when on free plan and over recorded tests limit", -> + e2e.exec(@, { + key: "f858a2bc-b469-4e48-be67-0876339ee7e1" + spec: "record_pass*" + record: true + snapshot: true + expectedExitCode: 1 + }) + describe "create run 402 - parallel feature not available in plan", -> setup([{ method: "post" @@ -1164,7 +1188,7 @@ describe "e2e record", -> describe "api interaction warnings", -> describe "create run warnings", -> - describe "grace period - over limit", -> + describe "grace period - over private tests limit", -> routes = defaultRoutes.slice() routes[0] = { method: "post" @@ -1195,6 +1219,37 @@ describe "e2e record", -> expectedExitCode: 0 }) + describe "grace period - over tests limit", -> + routes = defaultRoutes.slice() + routes[0] = { + method: "post" + url: "/runs" + req: "postRunRequest@2.1.0", + res: (req, res) -> res.status(200).json({ + runId + groupId + machineId + runUrl + warnings: [{ + code: "FREE_PLAN_IN_GRACE_PERIOD_EXCEEDS_MONTHLY_TESTS" + limit: 500 + gracePeriodEnds: "2999-12-31" + orgId: "org-id-1234" + }] + }) + } + + setup(routes) + + it "warns when over test recordings", -> + e2e.exec(@, { + key: "f858a2bc-b469-4e48-be67-0876339ee7e1" + spec: "record_pass*" + record: true + snapshot: true + expectedExitCode: 0 + }) + describe "grace period - parallel feature", -> routes = defaultRoutes.slice() routes[0] = { @@ -1255,7 +1310,7 @@ describe "e2e record", -> expectedExitCode: 0 }) - describe "paid plan - over limit", -> + describe "paid plan - over private tests limit", -> routes = defaultRoutes.slice() routes[0] = { method: "post" @@ -1285,3 +1340,34 @@ describe "e2e record", -> snapshot: true expectedExitCode: 0 }) + + describe "paid plan - over tests limit", -> + routes = defaultRoutes.slice() + routes[0] = { + method: "post" + url: "/runs" + req: "postRunRequest@2.1.0", + res: (req, res) -> res.status(200).json({ + runId + groupId + machineId + runUrl + warnings: [{ + code: "PAID_PLAN_EXCEEDS_MONTHLY_TESTS" + used: 700 + limit: 500 + orgId: "org-id-1234" + }] + }) + } + + setup(routes) + + it "warns when over test recordings", -> + e2e.exec(@, { + key: "f858a2bc-b469-4e48-be67-0876339ee7e1" + spec: "record_pass*" + record: true + snapshot: true + expectedExitCode: 0 + }) diff --git a/packages/server/test/unit/ci_provider_spec.coffee b/packages/server/test/unit/ci_provider_spec.coffee index 503550910626..a7a4290fba46 100644 --- a/packages/server/test/unit/ci_provider_spec.coffee +++ b/packages/server/test/unit/ci_provider_spec.coffee @@ -83,7 +83,7 @@ describe "lib/util/ci_provider", -> }) expectsCommitParams({ sha: "repoCommit" - branch: "repoBranch" + branch: "appveyorPullRequestHeadRepoBranch" message: "repoCommitMessage" authorName: "repoCommitAuthor" authorEmail: "repoCommitAuthorEmail" diff --git a/scripts/win-appveyor-build.js b/scripts/win-appveyor-build.js index 41745c3a80e3..7ad0634fc661 100755 --- a/scripts/win-appveyor-build.js +++ b/scripts/win-appveyor-build.js @@ -19,9 +19,9 @@ shell.set('-e') // any error is fatal // https://www.appveyor.com/docs/environment-variables/ const isRightBranch = () => { - const branch = process.env.APPVEYOR_REPO_BRANCH + const branch = process.env.APPVEYOR_PULL_REQUEST_HEAD_REPO_BRANCH || process.env.APPVEYOR_REPO_BRANCH - return branch === 'develop' || branch === 'issue-716-ffmpeg-packaging' + return branch === 'develop' } const isForkedPullRequest = () => {