secure cookie error crash (#2685)

- fixes #1264 
- fixes #1321 
- fixes #1799  
- fixes #2689
- fixes #2688
- fixes #2687 	
- fixes #2686
This commit is contained in:
Brian Mann
2018-11-01 12:34:37 -04:00
committed by GitHub
parent b6519258d2
commit 2333d04a54
245 changed files with 5045 additions and 2955 deletions
+59
View File
@@ -2,8 +2,67 @@
"extends": [
"plugin:cypress-dev/general"
],
"rules": {
"no-multiple-empty-lines": ["error", { "max": 1 } ],
"no-else-return": [ "error", { "allowElseIf": false } ],
"brace-style": ["error", "1tbs", { "allowSingleLine": false }],
"no-unneeded-ternary": ["error"],
"array-bracket-newline": ["error", "consistent"],
"arrow-body-style": ["error", "always"],
"padding-line-between-statements": [
"error",
{
"blankLine": "always",
"prev": "*",
"next": "return"
},
{
"blankLine": "always",
"prev": [
"const",
"let",
"var",
"if",
"while",
"export",
"cjs-export",
"import",
"cjs-import"
],
"next": "*"
},
{
"blankLine": "any",
"prev": [
"const",
"let",
"var",
"import",
"cjs-import"
],
"next": [
"const",
"let",
"var",
"import",
"cjs-import"
]
}
]
},
"env": {
"es6": true,
"node": true
},
"parserOptions": {
"ecmaFeatures": {
"legacyDecorators": true
}
},
"overrides": {
"files": ["**/*.jsx"],
"rules": {
"arrow-body-style": "off",
}
}
}
+1 -1
View File
@@ -8,12 +8,12 @@ dist
dist-*
build
.history
.vscode
.publish
_test-output
cypress.zip
tmp/
.nyc_output
.vscode/settings.json
# from extension
Cached Theme.pak
+32
View File
@@ -0,0 +1,32 @@
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "attach",
"name": "Attach by Process ID",
"processId": "${command:PickProcess}"
},
{
"type": "node",
"request": "launch",
"name": "test: active",
"runtimeExecutable": "npm",
"runtimeArgs": [
"run",
"test-debug-package",
],
"args": [
"${file}"
],
"port": 5566,
"console": "integratedTerminal"
},
{
"type": "node",
"request": "attach",
"name": "electron",
"port": 5567,
},
]
}
+29
View File
@@ -0,0 +1,29 @@
{
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
"version": "2.0.0",
"tasks": [
{
"label": "decaffeinate-bulk file",
"type": "shell",
"command": "npm run decaffeinate-bulk -- --file ${file} convert",
"problemMatcher": []
},
{
"label": "decaffeinate-bulk dir",
"type": "shell",
"command": "npm run decaffeinate-bulk -- --dir ${fileDirname} convert",
"problemMatcher": []
},
{
"label": "decaffeinate",
"type": "shell",
"command": "npm run decaffeinate -- ${file}"
},
{
"label": "jscodeshift",
"type": "shell",
"command": "npm run jscodeshift -- ${file}"
}
]
}
+56
View File
@@ -0,0 +1,56 @@
{
"autorun": false,
"terminals": [
{
"name": "cypress open",
"focus": true,
"onlySingle": true,
"execute": true,
"command": "npm run cypress:open"
},
{
"name": "cypress run",
"focus": true,
"onlySingle": true,
"execute": false,
"command": "npm run cypress:run -- --project ../project"
},
{
"name": "packages/server test-watch",
"focus": true,
"onlySingle": true,
"execute": false,
"cwd": "[workspaceFolder]/packages/server",
"command": "npm run test-watch -- [file]"
},
{
"name": "packages/server test-e2e",
"focus": true,
"onlySingle": true,
"execute": false,
"cwd": "[workspaceFolder]/packages/server",
"command": "npm run test-e2e -- --spec name"
},
{
"name": "packages/runner watch",
"focus": true,
"onlySingle": true,
"cwd": "[workspaceFolder]/packages/runner",
"command": "npm run watch"
},
{
"name": "packages/driver cypress open",
"focus": true,
"onlySingle": true,
"cwd": "[workspaceFolder]/packages/driver",
"command": "npm run cypress:open"
},
{
"name": "packages/desktop-gui cypress open",
"focus": true,
"onlySingle": true,
"cwd": "[workspaceFolder]/packages/desktop-gui",
"command": "npm run cypress:open"
}
]
}
+17
View File
@@ -0,0 +1,17 @@
const path = require('path')
module.exports = {
decaffeinateArgs: [
'--use-cs2',
'--loose',
],
jscodeshiftScripts: [
path.resolve('node_modules', 'js-codemod', 'transforms', 'arrow-function.js'),
path.resolve('node_modules', 'js-codemod', 'transforms', 'arrow-function-arguments.js'),
path.resolve('node_modules', 'js-codemod', 'transforms', 'no-vars.js'),
path.resolve('node_modules', 'jscodemods', 'transforms', 'fix-class-assign-construct.js'),
path.resolve('node_modules', 'jscodemods', 'decaffeinate', 'fix-multi-assign-class-export.js'),
path.resolve('node_modules', 'jscodemods', 'decaffeinate', 'fix-implicit-return-assignment.js'),
path.resolve('node_modules', 'jscodemods', 'decaffeinate', 'fix-existential-conditional-assignment.js'),
],
}
+8
View File
@@ -10,6 +10,7 @@ const cache = require('./tasks/cache')
// we want to print help for the current command and exit with an error
function unknownOption (flag, type = 'option') {
if (this._allowUnknownOption) return
logger.error()
logger.error(` error: unknown ${type}:`, flag)
logger.error()
@@ -87,6 +88,7 @@ function includesVersion (args) {
function showVersions () {
debug('printing Cypress version')
return require('./exec/versions')
.getVersions()
.then((versions = {}) => {
@@ -187,6 +189,7 @@ module.exports = {
const defaultOpts = { force: true, welcomeMessage: false }
const parsedOpts = parseOpts(opts)
const options = _.extend(parsedOpts, defaultOpts)
require('./tasks/verify')
.start(options)
.catch(util.logErrorExit1)
@@ -203,6 +206,7 @@ module.exports = {
if (opts.command || !_.includes(['list', 'path', 'clear'], opts)) {
unknownOption.call(this, `cache ${opts}`, 'sub-command')
}
cache[opts]()
})
@@ -219,10 +223,12 @@ module.exports = {
// Deprecated Catches
const firstCommand = args[2]
if (!_.includes(knownCommands, firstCommand)) {
debug('unknown command %s', firstCommand)
logger.error('Unknown command', `"${firstCommand}"`)
program.outputHelp()
return util.exit(1)
}
@@ -233,7 +239,9 @@ module.exports = {
// so we have to manually catch '-v, --version'
return showVersions()
}
debug('program parsing arguments')
return program.parse(args)
},
}
+2
View File
@@ -11,6 +11,7 @@ const util = require('./util')
const cypressModuleApi = {
open (options = {}) {
options = util.normalizeModuleOptions(options)
return open.start(options)
},
@@ -31,6 +32,7 @@ const cypressModuleApi = {
message: 'Could not find Cypress test run results',
}
}
return output
})
})
+32 -19
View File
@@ -25,16 +25,19 @@ const failedUnzip = {
`,
}
const missingApp = (binaryDir) => ({
description: `No version of Cypress is installed in: ${chalk.cyan(binaryDir)}`,
solution: stripIndent`
const missingApp = (binaryDir) => {
return {
description: `No version of Cypress is installed in: ${chalk.cyan(binaryDir)}`,
solution: stripIndent`
\nPlease reinstall Cypress by running: ${chalk.cyan('cypress install')}
`,
})
}
}
const binaryNotExecutable = (executable) => ({
description: `Cypress cannot run because the binary does not have executable permissions: ${executable}`,
solution: stripIndent`\n
const binaryNotExecutable = (executable) => {
return {
description: `Cypress cannot run because the binary does not have executable permissions: ${executable}`,
solution: stripIndent`\n
Reasons this may happen:
- node was installed as 'root' or with 'sudo'
@@ -42,12 +45,13 @@ const binaryNotExecutable = (executable) => ({
Please check that you have the appropriate user permissions.
`,
})
}
}
const notInstalledCI = (executable) => ({
description: 'The cypress npm package is installed, but the Cypress binary is missing.',
solution: stripIndent`\n
const notInstalledCI = (executable) => {
return {
description: 'The cypress npm package is installed, but the Cypress binary is missing.',
solution: stripIndent`\n
We expected the binary to be installed here: ${chalk.cyan(executable)}
Reasons it may be missing:
@@ -61,7 +65,8 @@ const notInstalledCI = (executable) => ({
${chalk.blue('https://on.cypress.io/not-installed-ci-error')}
`,
})
}
}
const nonZeroExitCodeXvfb = {
description: 'XVFB exited with a non zero exit code.',
@@ -146,6 +151,7 @@ const removed = {
const CYPRESS_RUN_BINARY = {
notValid: (value) => {
const properFormat = `**/${state.getPlatformExecutable()}`
return {
description: `Could not run binary set by environment variable CYPRESS_RUN_BINARY=${value}`,
solution: `Ensure the environment variable is a path to the Cypress binary, matching ${properFormat}`,
@@ -155,15 +161,19 @@ const CYPRESS_RUN_BINARY = {
function getPlatformInfo () {
return util.getOsVersionAsync()
.then((version) => stripIndent`
.then((version) => {
return stripIndent`
Platform: ${os.platform()} (${version})
Cypress Version: ${util.pkgVersion()}
`)
`
})
}
function addPlatformInformation (info) {
return getPlatformInfo()
.then((platform) => merge(info, { platform }))
.then((platform) => {
return merge(info, { platform })
})
}
function formErrorText (info, msg) {
@@ -216,13 +226,16 @@ function formErrorText (info, msg) {
const raise = (text) => {
const err = new Error(text)
err.known = true
throw err
}
const throwFormErrorText = (info) => (msg) => {
return formErrorText(info, msg)
.then(raise)
const throwFormErrorText = (info) => {
return (msg) => {
return formErrorText(info, msg)
.then(raise)
}
}
module.exports = {
+4 -2
View File
@@ -88,6 +88,7 @@ module.exports = {
options.env = _.extend({}, options.env, overrides)
const child = cp.spawn(executable, args, options)
child.on('close', resolve)
child.on('error', reject)
@@ -137,8 +138,9 @@ module.exports = {
return xvfb.start()
.then(userFriendlySpawn)
.finally(xvfb.stop)
} else {
return userFriendlySpawn()
}
return userFriendlySpawn()
},
}
+3
View File
@@ -11,11 +11,13 @@ const getVersions = () => {
if (util.getEnv('CYPRESS_RUN_BINARY')) {
let envBinaryPath = path.resolve(util.getEnv('CYPRESS_RUN_BINARY'))
return state.parseRealPlatformBinaryFolderAsync(envBinaryPath)
.then((envBinaryDir) => {
if (!envBinaryDir) {
return throwFormErrorText(errors.CYPRESS_RUN_BINARY.notValid(envBinaryPath))()
}
debug('CYPRESS_RUN_BINARY has binaryDir:', envBinaryDir)
return envBinaryDir
@@ -24,6 +26,7 @@ const getVersions = () => {
return throwFormErrorText(errors.CYPRESS_RUN_BINARY.notValid(envBinaryPath))(err.message)
})
}
return state.getBinaryDir()
})
.then(state.getBinaryPkgVersionAsync)
+3
View File
@@ -22,6 +22,7 @@ module.exports = {
start () {
debug('Starting XVFB')
return xvfb.startAsync()
.catch({ nonZeroExitCode: true }, throwFormErrorText(errors.nonZeroExitCodeXvfb))
.catch((err) => {
@@ -35,6 +36,7 @@ module.exports = {
stop () {
debug('Stopping XVFB')
return xvfb.stopAsync()
},
@@ -48,6 +50,7 @@ module.exports = {
.then(R.T)
.catch((err) => {
debug('Could not verify xvfb: %s', err.message)
return false
})
.finally(xvfb.stopAsync)
+6 -1
View File
@@ -3,7 +3,9 @@ const chalk = require('chalk')
let logs = []
const logLevel = () => (process.env.npm_config_loglevel || 'notice')
const logLevel = () => {
return (process.env.npm_config_loglevel || 'notice')
}
const error = (...messages) => {
logs.push(messages.join(' '))
@@ -12,12 +14,14 @@ const error = (...messages) => {
const warn = (...messages) => {
if (logLevel() === 'silent') return
logs.push(messages.join(' '))
console.log(chalk.yellow(...messages)) // eslint-disable-line no-console
}
const log = (...messages) => {
if (logLevel() === 'silent' || logLevel() === 'warn') return
logs.push(messages.join(' '))
console.log(...messages) // eslint-disable-line no-console
}
@@ -26,6 +30,7 @@ const log = (...messages) => {
// on each one to allow easy unit testing for specific message
const logLines = (text) => {
const lines = text.split('\n')
R.forEach(log, lines)
}
+1
View File
@@ -5,6 +5,7 @@ const util = require('../util')
const path = () => {
logger.log(state.getCacheDir())
return undefined
}
+13 -3
View File
@@ -32,27 +32,32 @@ const prepend = (urlPath) => {
const endpoint = url.resolve(getBaseUrl(), urlPath)
const platform = os.platform()
const arch = os.arch()
return `${endpoint}?platform=${platform}&arch=${arch}`
}
const getUrl = (version) => {
if (is.url(version)) {
debug('version is already an url', version)
return version
}
return version ? prepend(`desktop/${version}`) : prepend('desktop')
}
const statusMessage = (err) =>
(err.statusCode
const statusMessage = (err) => {
return (err.statusCode
? [err.statusCode, err.statusMessage].join(' - ')
: err.toString())
}
const prettyDownloadErr = (err, version) => {
const msg = stripIndent`
URL: ${getUrl(version)}
${statusMessage(err)}
`
debug(msg)
return throwFormErrorText(errors.failedDownload)(msg)
@@ -72,6 +77,7 @@ const downloadFromUrl = ({ url, downloadDestination, progress }) => {
url,
followRedirect (response) {
const version = response.headers['x-version']
debug('redirect version:', version)
if (version) {
// set the version in options if we have one.
@@ -136,11 +142,15 @@ const start = ({ version, downloadDestination, progress }) => {
if (!downloadDestination) {
la(is.unemptyString(downloadDestination), 'missing download dir', arguments)
}
if (!progress) {
progress = { onProgress: () => ({}) }
progress = { onProgress: () => {
return {}
} }
}
const url = getUrl(version)
progress.throttle = 100
debug('needed Cypress version: %s', version)
+39 -17
View File
@@ -81,6 +81,7 @@ const downloadAndUnzip = ({ version, installDir, downloadDir }) => {
return download.start({ version, downloadDestination, progress })
.then((redirectVersion) => {
if (redirectVersion) version = redirectVersion
debug(`finished downloading file: ${downloadDestination}`)
})
.then(() => {
@@ -105,6 +106,7 @@ const downloadAndUnzip = ({ version, installDir, downloadDir }) => {
const cleanup = () => {
debug('removing zip file %s', downloadDestination)
return fs.removeAsync(downloadDestination)
}
@@ -145,12 +147,14 @@ const start = (options = {}) => {
const pkgVersion = util.pkgVersion()
let needVersion = pkgVersion
debug('version in package.json is', needVersion)
// let this environment variable reset the binary version we need
if (util.getEnv('CYPRESS_INSTALL_BINARY')) {
const envVarVersion = util.getEnv('CYPRESS_INSTALL_BINARY')
debug('using environment variable CYPRESS_INSTALL_BINARY %s', envVarVersion)
if (envVarVersion === '0') {
@@ -159,6 +163,7 @@ const start = (options = {}) => {
stripIndent`
${chalk.yellow('Note:')} Skipping binary installation: Environment variable CYPRESS_INSTALL_BINARY = 0.`)
logger.log()
return Promise.resolve()
}
@@ -173,6 +178,7 @@ const start = (options = {}) => {
if (util.getEnv('CYPRESS_CACHE_FOLDER')) {
const envCache = util.getEnv('CYPRESS_CACHE_FOLDER')
logger.log(
stripIndent`
${chalk.yellow('Note:')} Overriding Cypress cache directory to: ${chalk.cyan(envCache)}
@@ -194,11 +200,14 @@ const start = (options = {}) => {
${err.message}
`)
})
.then(() => state.getBinaryPkgVersionAsync(binaryDir))
.then(() => {
return state.getBinaryPkgVersionAsync(binaryDir)
})
.then((binaryVersion) => {
if (!binaryVersion) {
debug('no binary installed under cli version')
return true
}
@@ -212,22 +221,24 @@ const start = (options = {}) => {
if (options.force) {
debug('performing force install over existing binary')
return true
}
if ((binaryVersion === needVersion) || !util.isSemver(needVersion)) {
// our version matches, tell the user this is a noop
alreadyInstalledMsg()
return false
}
return true
})
.then((shouldInstall) => {
// noop if we've been told not to download
if (!shouldInstall) {
debug('Not downloading or installing binary')
return
}
@@ -254,6 +265,7 @@ const start = (options = {}) => {
}
const possibleFile = util.formAbsolutePath(needVersion)
debug('checking local file', possibleFile, 'cwd', process.cwd())
return fs.pathExistsAsync(possibleFile)
@@ -263,16 +275,19 @@ const start = (options = {}) => {
if (exists && path.extname(possibleFile) === '.zip') {
return possibleFile
}
return false
})
})
.then((pathToLocalFile) => {
if (pathToLocalFile) {
const absolutePath = path.resolve(needVersion)
debug('found local file at', absolutePath)
debug('skipping download')
const rendererOptions = getRendererOptions()
return new Listr([unzipTask({
progress: {
throttle: 100,
@@ -292,10 +307,13 @@ const start = (options = {}) => {
debug('preparing to download and unzip version ', needVersion, 'to path', installDir)
const downloadDir = os.tmpdir()
return downloadAndUnzip({ version: needVersion, installDir, downloadDir })
})
// delay 1 sec for UX, unless we are testing
.then(() => Promise.delay(1000))
.then(() => {
return Promise.delay(1000)
})
.then(displayCompletionMsg)
})
}
@@ -304,22 +322,24 @@ module.exports = {
start,
}
const unzipTask = ({ zipFilePath, installDir, progress, rendererOptions }) => ({
title: util.titleize('Unzipping Cypress'),
task: (ctx, task) => {
const unzipTask = ({ zipFilePath, installDir, progress, rendererOptions }) => {
return {
title: util.titleize('Unzipping Cypress'),
task: (ctx, task) => {
// as our unzip progresses indicate the status
progress.onProgress = progessify(task, 'Unzipping Cypress')
progress.onProgress = progessify(task, 'Unzipping Cypress')
return unzip.start({ zipFilePath, installDir, progress })
.then(() => {
util.setTaskTitle(
task,
util.titleize(chalk.green('Unzipped Cypress')),
rendererOptions.renderer
)
})
},
})
return unzip.start({ zipFilePath, installDir, progress })
.then(() => {
util.setTaskTitle(
task,
util.titleize(chalk.green('Unzipped Cypress')),
rendererOptions.renderer
)
})
},
}
}
const progessify = (task, title) => {
// return higher order function
@@ -342,9 +362,11 @@ const progessify = (task, title) => {
// the default
const getRendererOptions = () => {
let renderer = util.isCi() ? verbose : 'default'
if (logger.logLevel() === 'silent') {
renderer = 'silent'
}
return {
renderer,
}
+12 -1
View File
@@ -8,6 +8,7 @@ const util = require('../util')
const getPlatformExecutable = () => {
const platform = os.platform()
switch (platform) {
case 'darwin': return 'Contents/MacOS/Cypress'
case 'linux': return 'Cypress'
@@ -19,6 +20,7 @@ const getPlatformExecutable = () => {
const getPlatFormBinaryFolder = () => {
const platform = os.platform()
switch (platform) {
case 'darwin': return 'Cypress.app'
case 'linux': return 'Cypress'
@@ -30,6 +32,7 @@ const getPlatFormBinaryFolder = () => {
const getBinaryPkgPath = (binaryDir) => {
const platform = os.platform()
switch (platform) {
case 'darwin': return path.join(binaryDir, 'Contents', 'Resources', 'app', 'package.json')
case 'linux': return path.join(binaryDir, 'resources', 'app', 'package.json')
@@ -52,11 +55,14 @@ const getVersionDir = (version = util.pkgVersion()) => {
const getCacheDir = () => {
let cache_directory = util.getCacheDir()
if (util.getEnv('CYPRESS_CACHE_FOLDER')) {
const envVarCacheDir = util.getEnv('CYPRESS_CACHE_FOLDER')
debug('using environment variable CYPRESS_CACHE_FOLDER %s', envVarCacheDir)
cache_directory = path.resolve(envVarCacheDir)
}
return cache_directory
}
@@ -67,9 +73,11 @@ const parseRealPlatformBinaryFolderAsync = (binaryPath) => {
if (!realPath.toString().endsWith(getPlatformExecutable())) {
return false
}
if (os.platform() === 'darwin') {
return path.resolve(realPath, '..', '..', '..')
}
return path.resolve(realPath, '..')
})
}
@@ -86,6 +94,7 @@ const getBinaryStateContentsAsync = (binaryDir) => {
return fs.readJsonAsync(getBinaryStatePath(binaryDir))
.catch({ code: 'ENOENT' }, SyntaxError, () => {
debug('could not read binary_state.json file')
return {}
})
}
@@ -119,18 +128,20 @@ const getPathToExecutable = (binaryDir) => {
const getBinaryPkgVersionAsync = (binaryDir) => {
const pathToPackageJson = getBinaryPkgPath(binaryDir)
debug('Reading binary package.json from:', pathToPackageJson)
return fs.pathExistsAsync(pathToPackageJson)
.then((exists) => {
if (!exists) {
return null
}
return fs.readJsonAsync(pathToPackageJson)
.get('version')
})
}
module.exports = {
getPathToExecutable,
getPlatformExecutable,
+12 -6
View File
@@ -27,6 +27,7 @@ const unzip = ({ zipFilePath, installDir, progress }) => {
return new Promise((resolve, reject) => {
return yauzl.open(zipFilePath, (err, zipFile) => {
if (err) return reject(err)
// debug('zipfile.paths:', zipFile)
// zipFile.on('entry', debug)
// debug(zipFile.readEntry())
@@ -34,7 +35,6 @@ const unzip = ({ zipFilePath, installDir, progress }) => {
debug('zipFile entries count', total)
const started = new Date()
let percent = 0
@@ -59,7 +59,9 @@ const unzip = ({ zipFilePath, installDir, progress }) => {
const unzipWithNode = () => {
const endFn = (err) => {
if (err) { return reject(err) }
if (err) {
return reject(err)
}
return resolve()
}
@@ -81,10 +83,9 @@ const unzip = ({ zipFilePath, installDir, progress }) => {
const copyingFileRe = /^copying file/
const sp = cp.spawn('ditto', ['-xkV', zipFilePath, installDir])
sp.on('error', () =>
// f-it just unzip with node
unzipWithNode()
)
sp.on('error', unzipWithNode)
sp.on('close', (code) => {
if (code === 0) {
@@ -125,12 +126,17 @@ const unzip = ({ zipFilePath, installDir, progress }) => {
const start = ({ zipFilePath, installDir, progress }) => {
la(is.unemptyString(installDir), 'missing installDir')
if (!progress) progress = { onProgress: () => ({}) }
if (!progress) {
progress = { onProgress: () => {
return {}
} }
}
return fs.pathExists(installDir)
.then((exists) => {
if (exists) {
debug('removing existing unzipped binary', installDir)
return fs.removeAsync(installDir)
}
})
+31 -11
View File
@@ -7,7 +7,6 @@ const { stripIndent } = require('common-tags')
const Promise = require('bluebird')
const logSymbols = require('log-symbols')
const { throwFormErrorText, errors } = require('../errors')
const util = require('../util')
const logger = require('../logger')
@@ -16,7 +15,9 @@ const state = require('./state')
const checkExecutable = (binaryDir) => {
const executable = state.getPathToExecutable(binaryDir)
debug('checking if executable exists', executable)
return util.isExecutableAsync(executable)
.then((isExecutable) => {
debug('Binary is executable? :', isExecutable)
@@ -28,6 +29,7 @@ const checkExecutable = (binaryDir) => {
if (util.isCi()) {
return throwFormErrorText(errors.notInstalledCI(executable))()
}
return throwFormErrorText(errors.missingApp(binaryDir))(stripIndent`
Cypress executable not found at: ${chalk.cyan(executable)}
`)
@@ -37,31 +39,37 @@ const checkExecutable = (binaryDir) => {
const runSmokeTest = (binaryDir) => {
debug('running smoke test')
const cypressExecPath = 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 = (err) => {
debug('Smoke test failed:', err)
return throwFormErrorText(errors.missingDependency)(err.stderr || err.message)
}
const needsXvfb = xvfb.isNeeded()
debug('needs XVFB?', needsXvfb)
const spawn = () => {
const random = _.random(0, 1000)
const args = ['--smoke-test', `--ping=${random}`]
const smokeTestCommand = `${cypressExecPath} ${args.join(' ')}`
debug('smoke test command:', smokeTestCommand)
return Promise.resolve(util.exec(cypressExecPath, args))
.catch(onSmokeTestError)
.then((result) => {
const smokeTestReturned = result.stdout
debug('smoke test stdout "%s"', smokeTestReturned)
if (!util.stdoutLineMatches(String(random), smokeTestReturned)) {
@@ -85,15 +93,15 @@ const runSmokeTest = (binaryDir) => {
return xvfb.stop()
.catch(onXvfbError)
})
} else {
return spawn()
}
return spawn()
}
function testBinary (version, binaryDir) {
debug('running binary verification check', version)
logger.log(stripIndent`
It looks like this is your first time using Cypress: ${chalk.cyan(version)}
`)
@@ -104,18 +112,19 @@ function testBinary (version, binaryDir) {
// the verbose renderer else use
// the default
let renderer = util.isCi() ? verbose : 'default'
if (logger.logLevel() === 'silent') renderer = 'silent'
const rendererOptions = {
renderer,
}
const tasks = new Listr([
{
title: util.titleize('Verifying Cypress can run', chalk.gray(binaryDir)),
task: (ctx, task) => {
debug('clearing out the verified version')
return state.clearBinaryStateAsync(binaryDir)
.then(() => {
return Promise.all([
@@ -125,6 +134,7 @@ function testBinary (version, binaryDir) {
})
.then(() => {
debug('write verified: true')
return state.writeBinaryVerifiedAsync(true, binaryDir)
})
.then(() => {
@@ -151,6 +161,7 @@ const maybeVerify = (installedVersion, binaryDir, options = {}) => {
debug('is Verified ?', isVerified)
let shouldVerify = !isVerified
// force verify if options.force
if (options.force) {
debug('force verify')
@@ -182,6 +193,7 @@ const start = (options = {}) => {
const parseBinaryEnvVar = () => {
const envBinaryPath = util.getEnv('CYPRESS_RUN_BINARY')
debug('CYPRESS_RUN_BINARY exists, =', envBinaryPath)
logger.log(stripIndent`
${chalk.yellow('Note:')} You have set the environment variable: ${chalk.white('CYPRESS_RUN_BINARY=')}${chalk.cyan(envBinaryPath)}:
@@ -199,11 +211,14 @@ const start = (options = {}) => {
`)
}
})
.then(() => state.parseRealPlatformBinaryFolderAsync(envBinaryPath))
.then(() => {
return state.parseRealPlatformBinaryFolderAsync(envBinaryPath)
})
.then((envBinaryDir) => {
if (!envBinaryDir) {
return throwFormErrorText(errors.CYPRESS_RUN_BINARY.notValid(envBinaryPath))()
}
debug('CYPRESS_RUN_BINARY has binaryDir:', envBinaryDir)
binaryDir = envBinaryDir
@@ -213,20 +228,26 @@ const start = (options = {}) => {
})
}
return Promise.try(() => {
debug('checking environment variables')
if (util.getEnv('CYPRESS_RUN_BINARY')) {
return parseBinaryEnvVar()
}
})
.then(() => checkExecutable(binaryDir))
.tap(() => debug('binaryDir is ', binaryDir))
.then(() => state.getBinaryPkgVersionAsync(binaryDir))
.then(() => {
return checkExecutable(binaryDir)
})
.tap(() => {
return debug('binaryDir is ', binaryDir)
})
.then(() => {
return state.getBinaryPkgVersionAsync(binaryDir)
})
.then((binaryVersion) => {
if (!binaryVersion) {
debug('no Cypress binary found for cli version ', packageVersion)
return throwFormErrorText(errors.missingApp(binaryDir))(`
Cannot read binary version from: ${chalk.cyan(state.getBinaryPkgPath(binaryDir))}
`)
@@ -247,7 +268,6 @@ const start = (options = {}) => {
These versions may not work properly together.
`)
logger.log()
}
+18 -4
View File
@@ -29,6 +29,7 @@ function normalizeModuleOptions (options = {}) {
function stdoutLineMatches (expectedLine, stdout) {
const lines = stdout.split('\n').map(R.trim)
const lineMatches = R.equals(expectedLine)
return lines.some(lineMatches)
}
@@ -179,11 +180,16 @@ const util = {
return Promise.try(() => {
if (os.platform() === 'linux') {
return getosAsync()
.then((osInfo) => [osInfo.dist, osInfo.release].join(' - '))
.catch(() => os.release())
} else {
return os.release()
.then((osInfo) => {
return [osInfo.dist, osInfo.release].join(' - ')
})
.catch(() => {
return os.release()
})
}
return os.release()
})
},
@@ -195,6 +201,7 @@ const util = {
if (path.isAbsolute(filename)) {
return filename
}
return path.join(process.cwd(), '..', '..', filename)
},
@@ -202,18 +209,25 @@ const util = {
const envVar = process.env[varName]
const configVar = process.env[`npm_config_${varName}`]
const packageConfigVar = process.env[`npm_package_config_${varName}`]
if (envVar) {
debug(`Using ${varName} from environment variable`)
return envVar
}
if (configVar) {
debug(`Using ${varName} from npm config`)
return configVar
}
if (packageConfigVar) {
debug(`Using ${varName} from package.json config`)
return packageConfigVar
}
return undefined
},
+2 -1
View File
@@ -51,7 +51,8 @@ function makeUserPackageFile () {
.then((json) => {
return fs.outputJsonAsync(packageJsonDest, json, {
spaces: 2,
}).then(() => json) // returning package json object makes it easy to test
})
.return(json) // returning package json object makes it easy to test
})
}
+6 -4
View File
@@ -7,11 +7,13 @@ const la = require('lazy-ass')
const is = require('check-more-types')
const R = require('ramda')
const hasVersion = (json) =>
la(is.semver(json.version), 'cannot find version', json)
const hasVersion = (json) => {
return la(is.semver(json.version), 'cannot find version', json)
}
const hasAuthor = (json) =>
la(json.author === 'Brian Mann', 'wrong author name', json)
const hasAuthor = (json) => {
return la(json.author === 'Brian Mann', 'wrong author name', json)
}
const changeVersion = R.assoc('version', 'x.y.z')
+23 -15
View File
@@ -19,46 +19,55 @@ describe('cli', function () {
sinon.stub(process, 'exit')
sinon.stub(util, 'exit')
sinon.stub(util, 'logErrorExit1')
this.exec = (args) => cli.init(`node test ${args}`.split(' '))
this.exec = (args) => {
return cli.init(`node test ${args}`.split(' '))
}
})
context('unknown option', () => {
// note it shows help for that specific command
it('shows help', () =>
execa('bin/cypress', ['open', '--foo']).then((result) => {
it('shows help', () => {
return execa('bin/cypress', ['open', '--foo']).then((result) => {
snapshot('shows help for open --foo', result)
})
}
)
it('shows help for run command', () =>
execa('bin/cypress', ['run', '--foo']).then((result) => {
it('shows help for run command', () => {
return execa('bin/cypress', ['run', '--foo']).then((result) => {
snapshot('shows help for run --foo', result)
})
}
)
})
context('help command', () => {
it('shows help', () =>
execa('bin/cypress', ['help']).then(snapshot)
it('shows help', () => {
return execa('bin/cypress', ['help']).then(snapshot)
}
)
it('shows help for -h', () =>
execa('bin/cypress', ['-h']).then(snapshot)
it('shows help for -h', () => {
return execa('bin/cypress', ['-h']).then(snapshot)
}
)
it('shows help for --help', () =>
execa('bin/cypress', ['--help']).then(snapshot)
it('shows help for --help', () => {
return execa('bin/cypress', ['--help']).then(snapshot)
}
)
})
context('unknown command', () => {
it('shows usage and exits', () =>
execa('bin/cypress', ['foo']).then(snapshot)
it('shows usage and exits', () => {
return execa('bin/cypress', ['foo']).then(snapshot)
}
)
})
context('cypress version', function () {
const binaryDir = '/binary/dir'
beforeEach(function () {
sinon.stub(state, 'getBinaryDir').returns(binaryDir)
})
@@ -136,6 +145,7 @@ describe('cli', function () {
it('run.start with options + catches errors', function (done) {
const err = new Error('foo')
run.start.rejects(err)
this.exec('run')
@@ -261,7 +271,6 @@ describe('cli', function () {
})
})
it('install calls install.start without forcing', function () {
sinon.stub(install, 'start').resolves()
this.exec('install')
@@ -287,7 +296,6 @@ describe('cli', function () {
})
context('cypress verify', function () {
it('verify calls verify.start with force: true', function () {
sinon.stub(verify, 'start').resolves()
this.exec('verify')
+5
View File
@@ -21,6 +21,7 @@ describe('cypress', function () {
const getCallArgs = R.path(['lastCall', 'args', 0])
const getStartArgs = () => {
expect(open.start).to.be.called
return getCallArgs(open.start)
}
@@ -48,10 +49,12 @@ describe('cypress', function () {
context('.run', function () {
let outputPath
beforeEach(function () {
outputPath = path.join(os.tmpdir(), 'cypress/monorepo/cypress_spec/output.json')
sinon.stub(tmp, 'fileAsync').resolves(outputPath)
sinon.stub(run, 'start').resolves()
return fs.outputJsonAsync(outputPath, {
code: 0,
failingTests: [],
@@ -62,10 +65,12 @@ describe('cypress', function () {
const normalizeCallArgs = (args) => {
expect(args.outputPath).to.equal(outputPath)
delete args.outputPath
return args
}
const getStartArgs = () => {
expect(run.start).to.be.called
return normalizeCallArgs(getCallArgs(run.start))
}
+6 -4
View File
@@ -14,14 +14,16 @@ describe('errors', function () {
})
describe('individual', () => {
it('has the following errors', () =>
snapshot(Object.keys(errors))
it('has the following errors', () => {
return snapshot(Object.keys(errors))
}
)
})
context('.errors.formErrorText', function () {
it('returns fully formed text message', () =>
snapshot(formErrorText(missingXvfb))
it('returns fully formed text message', () => {
return snapshot(formErrorText(missingXvfb))
}
)
})
})
+16
View File
@@ -24,24 +24,28 @@ describe('util', () => {
it('matches entire output', () => {
const line = '444'
expect(stdoutLineMatches(line, line)).to.be.true
})
it('matches a line in output', () => {
const line = '444'
const stdout = ['start', line, 'something else'].join('\n')
expect(stdoutLineMatches(line, stdout)).to.be.true
})
it('matches a trimmed line in output', () => {
const line = '444'
const stdout = ['start', ` ${line} `, 'something else'].join('\n')
expect(stdoutLineMatches(line, stdout)).to.be.true
})
it('does not find match', () => {
const line = '445'
const stdout = ['start', '444', 'something else'].join('\n')
expect(stdoutLineMatches(line, stdout)).to.be.false
})
})
@@ -53,6 +57,7 @@ describe('util', () => {
const options = {
foo: 'bar',
}
snapshot('others_unchanged', normalizeModuleOptions(options))
})
@@ -60,6 +65,7 @@ describe('util', () => {
const options = {
env: 'foo=bar',
}
snapshot('env_as_string', normalizeModuleOptions(options))
})
@@ -71,6 +77,7 @@ describe('util', () => {
host: 'kevin.dev.local',
},
}
snapshot('env_as_object', normalizeModuleOptions(options))
})
@@ -81,6 +88,7 @@ describe('util', () => {
watchForFileChanges: false,
},
}
snapshot('config_as_object', normalizeModuleOptions(options))
})
@@ -91,6 +99,7 @@ describe('util', () => {
toConsole: true,
},
}
snapshot('reporter_options_as_object', normalizeModuleOptions(options))
})
@@ -100,6 +109,7 @@ describe('util', () => {
'a', 'b', 'c',
],
}
snapshot('spec_as_array', normalizeModuleOptions(options))
})
@@ -107,6 +117,7 @@ describe('util', () => {
const options = {
spec: 'x,y,z',
}
snapshot('spec_as_string', normalizeModuleOptions(options))
})
})
@@ -250,6 +261,7 @@ describe('util', () => {
it('does nothing if debug is not enabled', () => {
const log = sinon.spy()
log.enabled = false
util.printNodeOptions(log)
expect(log).not.have.been.called
@@ -257,6 +269,7 @@ describe('util', () => {
it('prints message when debug is enabled', () => {
const log = sinon.spy()
log.enabled = true
util.printNodeOptions(log)
expect(log).to.be.calledWith('NODE_OPTIONS is not set')
@@ -270,6 +283,7 @@ describe('util', () => {
it('does nothing if debug is not enabled', () => {
const log = sinon.spy()
log.enabled = false
util.printNodeOptions(log)
expect(log).not.have.been.called
@@ -277,6 +291,7 @@ describe('util', () => {
it('prints value when debug is enabled', () => {
const log = sinon.spy()
log.enabled = true
util.printNodeOptions(log)
expect(log).to.be.calledWith('NODE_OPTIONS=%s', 'foo')
@@ -287,6 +302,7 @@ describe('util', () => {
describe('.getOsVersionAsync', () => {
let util
let getos = sinon.stub().resolves(['distro-release'])
beforeEach(() => {
util = proxyquire(`${lib}/util`, { getos })
})
+4 -1
View File
@@ -43,7 +43,9 @@ function throwIfFnNotStubbed (stub, method) {
err.stack = _
.chain(err.stack)
.split('\n')
.reject((str) => _.includes(str, 'sinon'))
.reject((str) => {
return _.includes(str, 'sinon')
})
.join('\n')
.value()
@@ -52,6 +54,7 @@ function throwIfFnNotStubbed (stub, method) {
}
const $stub = sinon.stub
sinon.stub = function (obj, method) {
/* eslint-disable prefer-rest-params */
const stub = $stub.apply(this, arguments)
+15
View File
@@ -0,0 +1,15 @@
{
"exclude": [
"**/.git/**",
"**/.cache/**",
"**/.history/**",
"**/.projects/**",
"**/.publish/**",
"**/node_modules/**",
"**/app/**",
"**/build/**",
"**/dist/**",
"**/dist-test/**",
"**/.cy/**"
]
}
+16 -3
View File
@@ -12,8 +12,15 @@
"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:open:debug": "node ./scripts/debug.js cypress:open",
"cypress:run:debug": "node ./scripts/debug.js cypress:run",
"dev": "node ./scripts/start.js",
"dev-debug": "node ./scripts/debug.js dev",
"watch": "npm run all watch",
"test-debug-package": "node ./scripts/test-debug-package.js",
"jscodeshift": "jscodeshift -t ./node_modules/js-codemod/transforms/arrow-function-arguments.js",
"decaffeinate": "decaffeinate --use-cs2 --loose",
"decaffeinate-bulk": "bulk-decaffeinate",
"check-deps": "node ./scripts/check-deps.js --verbose",
"check-deps-pre": "node ./scripts/check-deps.js --verbose --prescript",
"prebuild": "npm run check-deps-pre",
@@ -23,7 +30,7 @@
"link": "node ./scripts/link-packages.js",
"install-filtered": "npm run all install -- --package $(node ./scripts/check-deps.js --list)",
"postinstall": "echo 'root postinstall' && npm run link && npm run all install && npm run build",
"clean-deps": "npm run all clean-deps",
"clean-deps": "npm run all clean-deps && rm -rf node_modules",
"docker": "./scripts/run-docker-local.sh",
"lint-js": "eslint --fix scripts/*.js packages/ts/*.js cli/*.js cli/**/*.js",
"lint-coffee": "coffeelint scripts/**/*.coffee",
@@ -59,9 +66,10 @@
"@cypress/questions-remain": "^1.0.1",
"ansi-styles": "^3.1.0",
"ascii-table": "0.0.9",
"babel-eslint": "^7.2.3",
"babel-eslint": "^10.0.1",
"bluebird": "^3.4.5",
"bluebird-retry": "^0.11.0",
"bulk-decaffeinate": "^3.3.1",
"chai": "^4.0.2",
"chalk": "^2.0.1",
"check-dependencies": "1.1.0",
@@ -72,9 +80,10 @@
"common-tags": "^1.8.0",
"console.table": "^0.9.1",
"debug": "3.1.0",
"decaffeinate": "^4.8.8",
"del": "^3.0.0",
"electron-osx-sign": "^0.4.6",
"eslint": "4.13.1",
"eslint": "4.19.1",
"eslint-plugin-cypress": "^2.0.1",
"eslint-plugin-cypress-dev": "^1.1.1",
"eslint-plugin-mocha": "^4.11.0",
@@ -82,6 +91,7 @@
"execa": "^0.8.0",
"execa-wrap": "^1.1.0",
"filesize": "^3.5.10",
"find-package-json": "^1.1.0",
"fs-extra": "^7.0.0",
"gift": "^0.10.0",
"gulp": "^3.9.1",
@@ -93,6 +103,9 @@
"human-interval": "^0.1.6",
"husky": "^0.14.3",
"inquirer": "^3.1.1",
"js-codemod": "cpojer/js-codemod#29dafed",
"jscodemods": "cypress-io/jscodemods#01b546e",
"jscodeshift": "^0.5.1",
"konfig": "^0.2.1",
"lazy-ass": "^1.6.0",
"lint-staged": "^4.1.3",
+2 -1
View File
@@ -9,6 +9,7 @@
"register.js"
],
"scripts": {
"check-deps": "node ../../scripts/check-deps.js --verbose"
"check-deps": "node ../../scripts/check-deps.js --verbose",
"clean-deps": "rm -rf node_modules"
}
}
+4
View File
@@ -92,11 +92,13 @@ class Default extends Component {
_dragover = () => {
this._setDragging(true)
return false
}
_dragleave = () => {
this._setDragging(false)
return false
}
@@ -105,6 +107,7 @@ class Default extends Component {
this._setDragging(false)
const file = _.get(e, 'dataTransfer.files[0]')
if (!file) return false
this._addProject(file.path)
@@ -118,6 +121,7 @@ class Default extends Component {
_nope (e) {
e.preventDefault()
return false
}
+8 -7
View File
@@ -122,14 +122,15 @@ export default class Nav extends Component {
{' '}{authStore.user.displayName}
</span>
)
} else {
return (
<span>
<i className='fa fa-sign-out'></i>{' '}
Log Out
</span>
)
}
return (
<span>
<i className='fa fa-sign-out'></i>{' '}
Log Out
</span>
)
}
_select = (item) => {
@@ -7,6 +7,7 @@ class AuthApi {
ipc.getCurrentUser()
.then((user) => {
authStore.setUser(user)
// mobx can trigger a synchronous re-render, which executes
// componentDidMount, etc in other components, making bluebird
// think another promise was created but not returned
@@ -35,6 +36,7 @@ class AuthApi {
})
.then((user) => {
authStore.setUser(user)
return null
})
.catch({ alreadyOpen: true }, () => {})
@@ -20,6 +20,7 @@ class AuthStore {
@action setUser (user) {
const isValid = user && user.authToken
this.user = isValid ? new User(user) : null
}
}
+9 -7
View File
@@ -40,18 +40,20 @@ class LoginForm extends Component {
Logging in...
</span>
)
} else {
return (
<span>
<i className='fa fa-github'></i>{' '}
Log In with GitHub
</span>
)
}
return (
<span>
<i className='fa fa-github'></i>{' '}
Log In with GitHub
</span>
)
}
_error () {
const error = this.state.error
if (!error) return null
return (
@@ -52,13 +52,14 @@ class Dropdown extends Component {
{this._buttonContent()}
</a>
)
} else {
return (
<span>
{this._buttonContent()}
</span>
)
}
return (
<span>
{this._buttonContent()}
</span>
)
}
_buttonContent () {
@@ -22,11 +22,14 @@ class DurationTimer {
this.timer.milliseconds = moment().diff(this.startTime)
this.timerId = setTimeout(() => this.measure(), 10)
this.timerId = setTimeout(() => {
return this.measure()
}, 10)
}
@action startTimer () {
if (this.isRunning) return
this.isRunning = true
this.measure()
}
@@ -19,6 +19,7 @@ export default class Footer extends Component {
_openChangelog (e) {
e.preventDefault()
return ipc.externalOpen('https://on.cypress.io/changelog')
}
}
@@ -28,8 +28,11 @@ class AppStore {
@action set (props) {
if (props.cypressEnv != null) this.cypressEnv = props.cypressEnv
if (props.os != null) this.os = props.os
if (props.projectRoot != null) this.projectRoot = props.projectRoot
if (props.version != null) this.version = this.newVersion = props.version
}
@@ -19,6 +19,7 @@ const handleGlobalErrors = () => {
window.onunhandledrejection = (event) => {
const reason = event && event.reason
sendErr(reason || event)
}
}
+11 -3
View File
@@ -11,7 +11,9 @@ const addMsg = (id, event, fn) => {
}
const removeMsgsByEvent = (event) => {
msgs = _.omitBy(msgs, (msg) => msg.event === event)
msgs = _.omitBy(msgs, (msg) => {
return msg.event === event
})
}
const removeMsgById = (id) => {
@@ -20,6 +22,7 @@ const removeMsgById = (id) => {
const createIpc = () => {
console.warn('Missing "ipc". Polyfilling in development mode.') // eslint-disable-line no-console
return {
on () {},
send () {},
@@ -39,7 +42,9 @@ ipc.on('response', (event, obj = {}) => {
})
const ipcBus = (...args) => {
if (args.length === 0) { return msgs }
if (args.length === 0) {
return msgs
}
// our ipc interface can either be a standard
// node callback or a promise interface
@@ -58,12 +63,15 @@ const ipcBus = (...args) => {
const lastArg = args.pop()
let fn
// enable the last arg to be a function
// which changes this interface from being
// a promise to just calling the callback
// function directly
if (lastArg && _.isFunction(lastArg)) {
fn = () => addMsg(id, event, lastArg)
fn = () => {
return addMsg(id, event, lastArg)
}
} else {
// push it back onto the array
args.push(lastArg)
+3 -1
View File
@@ -22,7 +22,9 @@ const register = (eventName, isPromiseApi = true) => {
return ipcBus(eventName, ...args)
}
if (!isPromiseApi) {
ipc[_.camelCase(`off:${eventName}`)] = () => ipcBus.off(eventName)
ipc[_.camelCase(`off:${eventName}`)] = () => {
return ipcBus.off(eventName)
}
}
}
@@ -1,6 +1,7 @@
export default {
get (key) {
const value = localStorage[key]
return value && JSON.parse(value)
},
+1
View File
@@ -6,6 +6,7 @@ const Link = ({ children, to, onClick }) => {
const navigate = (e) => {
e.preventDefault()
if (onClick) onClick()
to.navigate()
}
+4 -1
View File
@@ -48,7 +48,9 @@ module.exports = {
gravatarUrl: (email) => {
let opts = { size: '13', default: 'mm' }
if (!email) { opts.forcedefault = 'y' }
if (!email) {
opts.forcedefault = 'y'
}
return gravatar.url(email, opts, true)
},
@@ -78,6 +80,7 @@ module.exports = {
stripLeadingCyDirs (spec) {
if (!spec) return null
// remove leading 'cypress/integration' from spec
return spec.replace(cyDirRegex, '')
},
@@ -7,11 +7,13 @@ const getOrgs = () => {
ipc.getOrgs()
.then((orgs = []) => {
orgsStore.setOrgs(orgs)
return null
})
.catch(ipc.isUnauthed, ipc.handleUnauthed)
.catch((err) => {
orgsStore.setError(err)
return null
})
@@ -10,9 +10,11 @@ export class Orgs {
@observable isLoaded = false
@action setOrgs (orgs) {
this.orgs = _.map(orgs, (org) => (
new Org(org)
))
this.orgs = _.map(orgs, (org) => {
return (
new Org(org)
)
})
this.isLoading = false
this.isLoaded = true
@@ -86,8 +86,10 @@ class OnBoarding extends Component {
files = _.sortBy(files, 'name')
const notFolders = _.every(files, (file) => !file.children)
if (notFolders && files.length > 3) {
const numHidden = files.length - 2
files = files.slice(0, 2).concat({ name: `... ${numHidden} more files ...`, more: true })
}
@@ -104,16 +106,17 @@ class OnBoarding extends Component {
</ul>
</li>
)
} else {
return (
<li className={cs(className, 'new-item', { 'is-more': file.more })} key={file.name}>
<span>
<i className='fa fa-file-code-o'></i>{' '}
{file.name}
</span>
</li>
)
}
return (
<li className={cs(className, 'new-item', { 'is-more': file.more })} key={file.name}>
<span>
<i className='fa fa-file-code-o'></i>{' '}
{file.name}
</span>
</li>
)
})
}
@@ -81,9 +81,11 @@ export default class Project {
@computed get displayPath () {
const maxPathLength = 45
if (this.path.length <= maxPathLength) return this.path
const truncatedPath = this.path.slice((this.path.length - 1) - maxPathLength, this.path.length)
return '...'.concat(truncatedPath)
}
@@ -221,6 +223,7 @@ export default class Project {
@action setChosenBrowserByName (name) {
const browser = _.find(this.browsers, { name }) || this.defaultBrowser
this.setChosenBrowser(browser)
}
@@ -35,6 +35,7 @@ const loadProjects = (shouldLoad = true) => {
.then((projectsWithStatuses) => {
projectsStore.updateProjectsWithStatuses(projectsWithStatuses)
saveToLocalStorage()
return null
})
.catch(ipc.isUnauthed, ipc.handleUnauthed)
@@ -46,6 +47,7 @@ const loadProjects = (shouldLoad = true) => {
const addProject = (path) => {
const project = projectsStore.addProject(path)
project.setLoading(true)
return ipc.addProject(path)
@@ -169,11 +171,13 @@ const openProject = (project) => {
.then((config = {}) => {
updateConfig(config)
const projectIdAndPath = { id: config.projectId, path: project.path }
specsStore.setFilter(projectIdAndPath, localData.get(specsStore.getSpecsFilterId(projectIdAndPath)))
project.setLoading(false)
getSpecs(setProjectError)
projectPollingId = setInterval(updateProjectStatus, 10000)
return updateProjectStatus()
})
.catch(setProjectError)
@@ -184,7 +188,9 @@ const reopenProject = (project) => {
project.clearWarning()
return closeProject(project)
.then(() => openProject(project))
.then(() => {
return openProject(project)
})
}
const removeProject = (project) => {
@@ -202,7 +208,9 @@ const getRecordKeys = () => {
return ipc.getRecordKeys()
.catch(ipc.isUnauthed, ipc.handleUnauthed)
// ignore error, settle for no keys
.catch(() => [])
.catch(() => {
return []
})
}
export default {
@@ -13,11 +13,15 @@ class ProjectsStore {
}
@computed get other () {
return _.filter(this.projects, (project) => !project.isChosen)
return _.filter(this.projects, (project) => {
return !project.isChosen
})
}
@computed get clientProjects () {
return _.map(this.projects, (project) => _.pick(project, ['path', 'id']))
return _.map(this.projects, (project) => {
return _.pick(project, ['path', 'id'])
})
}
@action getProjectByPath (path) {
@@ -33,6 +37,7 @@ class ProjectsStore {
// or move it to the start if it already exists
const existingIndex = _.findIndex(this.projects, { path })
let project
if (existingIndex > -1) {
project = this.projects[existingIndex]
this.projects.splice(existingIndex, 1)
@@ -50,7 +55,9 @@ class ProjectsStore {
}
@action setProjects (projects) {
this.projects = _.map(projects, (project) => new Project(project))
this.projects = _.map(projects, (project) => {
return new Project(project)
})
}
@action updateProjectsWithStatuses (projectsWithStatuses = []) {
@@ -82,7 +89,9 @@ class ProjectsStore {
}
serializeProjects () {
return _.map(this.projects, (project) => project.serialize())
return _.map(this.projects, (project) => {
return project.serialize()
})
}
membershipRequested (id) {
@@ -5,6 +5,7 @@ import errors from '../lib/errors'
const ErrorMessage = observer(({ error }) => {
let errorMessage
if (errors.isTimedOut(error)) {
errorMessage = (
<p>The request for runs timed out.</p>
@@ -38,11 +38,14 @@ class PermissionMessage extends Component {
if (this.state.result === SUCCESS || membershipRequested) {
return this._success()
} else if (this.state.result === FAILURE) {
return this._failure()
} else {
return this._noResult()
}
if (this.state.result === FAILURE) {
return this._failure()
}
return this._noResult()
}
_button () {
@@ -82,22 +85,23 @@ class PermissionMessage extends Component {
// tell them it's all good
if (errors.isDenied(error) || errors.isAlreadyRequested(error)) {
return this._success()
} else {
return (
<div className='empty'>
<h4>
<i className='fa fa-exclamation-triangle failed'></i>{' '}
Request Failed
</h4>
<p>An unexpected error occurred while requesting access:</p>
<pre className='alert alert-danger'>
{this.state.error.message}
</pre>
<p>Try again.</p>
{this._button()}
</div>
)
}
return (
<div className='empty'>
<h4>
<i className='fa fa-exclamation-triangle failed'></i>{' '}
Request Failed
</h4>
<p>An unexpected error occurred while requesting access:</p>
<pre className='alert alert-danger'>
{this.state.error.message}
</pre>
<p>Try again.</p>
{this._button()}
</div>
)
}
_noResult () {
@@ -135,6 +139,7 @@ class PermissionMessage extends Component {
_setResult (error) {
if (errors.isAlreadyMember(error)) {
this.props.onRetry()
return
}
@@ -12,11 +12,13 @@ const loadRuns = (runsStore) => {
ipc.getRuns()
.then((runs) => {
runsStore.setRuns(runs)
return null
})
.catch(ipc.isUnauthed, ipc.handleUnauthed)
.catch((err) => {
runsStore.setError(err)
return null
})
@@ -193,9 +193,10 @@ export default class RunsListItem extends Component {
_osIcon () {
if (!this._moreThanOneInstance() && this.props.run.instances.length) {
return (osIcon(this.props.run.instances[0].platform.osName))
} else {
return 'desktop'
}
return 'desktop'
}
_getUniqOs () {
+15 -6
View File
@@ -161,19 +161,27 @@ class RunsList extends Component {
return this._projectNotSetup()
// the project is invalid
} else if (errors.isNotFound(this.runsStore.error)) {
}
if (errors.isNotFound(this.runsStore.error)) {
return this._projectNotSetup(false)
// they have been logged out
} else if (errors.isUnauthenticated(this.runsStore.error)) {
}
if (errors.isUnauthenticated(this.runsStore.error)) {
return this._loginMessage()
// they are not authorized to see runs
} else if (errors.isUnauthorized(this.runsStore.error)) {
}
if (errors.isUnauthorized(this.runsStore.error)) {
return this._permissionMessage()
// other error, but only show if we don't already have runs
} else if (!this.runsStore.isLoaded) {
}
if (!this.runsStore.isLoaded) {
return <ErrorMessage error={this.runsStore.error} />
}
}
@@ -189,9 +197,10 @@ class RunsList extends Component {
return this._projectNotSetup()
// OR they have setup CI
} else {
return this._empty()
}
return this._empty()
}
//--------End Run States----------//
+3 -1
View File
@@ -17,7 +17,9 @@ export class RunsStore {
}
@action setRuns (runs) {
this.runs = _.map(runs, (run) => new Run(run))
this.runs = _.map(runs, (run) => {
return new Run(run)
})
this.lastUpdated = moment().format('h:mm:ssa')
this.error = null
@@ -334,6 +334,7 @@ class SetupProject extends Component {
_error () {
const error = this.state.error
if (!error) return null
return (
@@ -426,6 +427,7 @@ class SetupProject extends Component {
isSubmitting: false,
})
this.props.onSetup(projectDetails)
return null
})
.catch(ipc.isUnauthed, ipc.handleUnauthed)
@@ -11,6 +11,7 @@ const display = (obj) => {
return _.map(obj, (value, key) => {
const hasComma = lastKey !== key
if (value.from == null) {
return displayNestedObj(key, value, hasComma)
}
@@ -45,6 +45,7 @@ class RecordKey extends Component {
) {
this._loadKeys()
}
this.wasAuthenticated = authStore.isAuthenticated
}
@@ -74,11 +74,13 @@ class SpecsList extends Component {
_clearFilter = () => {
const { id, path } = this.props.project
specsStore.clearFilter({ id, path })
}
_updateFilter = (e) => {
const { id, path } = this.props.project
specsStore.setFilter({ id, path }, e.target.value)
}
@@ -82,6 +82,7 @@ export class SpecsStore {
getSpecsFilterId ({ id = '<no-id>', path = '' }) {
const shortenedPath = path.replace(/.*cypress/, 'cypress')
return `specsFilter-${id}-${shortenedPath}`
}
@@ -104,7 +105,9 @@ export class SpecsStore {
const isCurrentAFile = i === segments.length - 1
const props = { path: currentPath, displayName: segment }
let existing = _.find(placeholder, (file) => pathsEqual(file.path, currentPath))
let existing = _.find(placeholder, (file) => {
return pathsEqual(file.path, currentPath)
})
if (!existing) {
existing = isCurrentAFile ? new Spec(_.extend(file, props)) : new Folder(props)
@@ -76,21 +76,22 @@ class UpdateBanner extends Component {
</li>
</ol>
)
} else {
return (
<ol>
<li>
<span>Quit this app.</span>
</li>
<li>
<span>Run <code>npm install --save-dev cypress@{appStore.newVersion}</code></span>
</li>
<li>
<span>Run <a href='#' onClick={this._openCyOpenDoc}><code>node_modules/.bin/cypress open</code></a> to open the new version.</span>
</li>
</ol>
)
}
return (
<ol>
<li>
<span>Quit this app.</span>
</li>
<li>
<span>Run <code>npm install --save-dev cypress@{appStore.newVersion}</code></span>
</li>
<li>
<span>Run <a href='#' onClick={this._openCyOpenDoc}><code>node_modules/.bin/cypress open</code></a> to open the new version.</span>
</li>
</ol>
)
}
_checkForUpdate () {
@@ -10,7 +10,7 @@
<script type="text/javascript" src="/node_modules/sinon/dist/sinon.js"></script>
<script type='text/javascript'>
var Cypress = parent.Cypress;
if (!Cypress){
if (!Cypress) {
throw new Error('Cypress must exist in the parent window!');
};
Cypress.onBeforeLoad(window);
@@ -49,4 +49,4 @@
})
</script>
</body>
</html>
</html>
@@ -12,7 +12,7 @@
<script type="text/javascript" src="/node_modules/sinon/dist/sinon.js"></script>
<script type='text/javascript'>
var Cypress = parent.Cypress;
if (!Cypress){
if (!Cypress) {
throw new Error('Cypress must exist in the parent window!');
};
Cypress.onBeforeLoad(window);
@@ -23,4 +23,4 @@
</head>
<body>
</body>
</html>
</html>
+38 -20
View File
@@ -1,11 +1,12 @@
fs = require("fs-extra")
cp = require("child_process")
path = require("path")
debug = require("debug")("cypress:electron")
Promise = require("bluebird")
minimist = require("minimist")
inspector = require("inspector")
paths = require("./paths")
install = require("./install")
log = require("debug")("cypress:electron")
fs = Promise.promisifyAll(fs)
@@ -14,12 +15,14 @@ module.exports = {
install.check()
install: ->
log("installing %j", arguments)
debug("installing %j", arguments)
install.package.apply(install, arguments)
cli: (argv = []) ->
opts = minimist(argv)
log("cli options %j", opts)
debug("cli options %j", opts)
pathToApp = argv[0]
@@ -32,39 +35,54 @@ module.exports = {
throw new Error("No path to your app was provided.")
open: (appPath, argv, cb) ->
log("opening %s", appPath)
debug("opening %s", appPath)
appPath = path.resolve(appPath)
dest = paths.getPathToResources("app")
log("appPath %s", appPath)
log("dest path %s", dest)
debug("appPath %s", appPath)
debug("dest path %s", dest)
## make sure this path exists!
fs.statAsync(appPath)
.then ->
log("appPath exists %s", appPath)
debug("appPath exists %s", appPath)
## clear out the existing symlink
fs.removeAsync(dest)
.then ->
symlinkType = paths.getSymlinkType()
log("making symlink from %s to %s of type %s", appPath, dest, symlinkType)
debug("making symlink from %s to %s of type %s", appPath, dest, symlinkType)
fs.ensureSymlinkAsync(appPath, dest, symlinkType)
.then ->
execPath = paths.getPathToExec()
log("spawning %s", execPath)
debug("spawning %s", execPath)
## we have an active debugger session
if inspector.url()
dp = process.debugPort + 1
argv.unshift("--inspect-brk=#{dp}")
cp.spawn(execPath, argv, {stdio: "inherit"})
.on "close", (code) ->
log("electron closing with code", code)
if code
log("original command was")
log(execPath, argv.join(" "))
if cb
log("calling callback with code", code)
cb(code)
else
log("process.exit with code", code)
process.exit(code)
debug("electron closing with code", code)
if code
debug("original command was")
debug(execPath, argv.join(" "))
if cb
debug("calling callback with code", code)
cb(code)
else
debug("process.exit with code", code)
process.exit(code)
.catch (err) ->
console.log(err.stack)
console.debug(err.stack)
process.exit(1)
}
+1
View File
@@ -9,6 +9,7 @@ describe('Cypress Example', function () {
it('returns path to example_spec', function () {
let result = example.getPathToExample()
let expected = `${cwd}/cypress/integration/example_spec.js`
expect(normalize(result)).to.eq(normalize(expected))
})
})
+3
View File
@@ -5,7 +5,9 @@
if (process.env.CYPRESS_ENV !== 'production') {
require('@packages/ts/register')
}
const launcher = require('./lib/launcher')
module.exports = launcher
if (!module.parent) {
@@ -16,6 +18,7 @@ if (!module.parent) {
console.log('⛔️ please use it as a module, not from CLI')
const pluralize = require('pluralize')
launcher.detect().then((browsers) => {
console.log('detected %s', pluralize('browser', browsers.length, true))
console.log(browsers)
+1 -1
View File
@@ -1,3 +1,3 @@
import * as debug from 'debug'
import debug from 'debug'
export const log = debug('cypress:launcher')
+1 -1
View File
@@ -1,6 +1,6 @@
import { log } from '../log'
import { FoundBrowser, Browser, NotInstalledError } from '../types'
import * as execa from 'execa'
import execa from 'execa'
import { normalize, join } from 'path'
import { trim, tap } from 'ramda'
import { pathExists } from 'fs-extra'
+3 -1
View File
@@ -18,7 +18,9 @@ global.navigator = {
appVersion: 'node',
userAgent: 'node.js',
}
global.requestAnimationFrame = (fn) => fn()
global.requestAnimationFrame = (fn) => {
return fn()
}
global.cancelAnimationFrame = () => {}
// enzyme, and therefore chai-enzyme, needs to be required after
+1
View File
@@ -52,4 +52,5 @@ const Agents = observer(({ model }) => (
))
export { Agent, AgentsList }
export default Agents
@@ -23,27 +23,32 @@ const model = (props) => {
describe('<Agents />', () => {
it('renders without no-agents class if there are agents', () => {
const component = shallow(<Agents model={model()} />)
expect(component).not.to.have.className('no-agents')
})
it('renders with no-agents class if there are no agents', () => {
const component = shallow(<Agents model={model({ agents: [] })} />)
expect(component).to.have.className('no-agents')
})
it('renders collapsible header with number of agents', () => {
const component = shallow(<Agents model={model()} />)
expect(component.find('Collapsible')).to.have.prop('header', 'Spies / Stubs (2)')
})
context('<AgentsList />', () => {
it('is rendered', () => {
const component = shallow(<Agents model={model()} />)
expect(component.find(AgentsList).first()).to.exist
})
it('renders an <Agent /> for each agent in model', () => {
const component = shallow(<AgentsList model={model()} />)
expect(component.find(Agent).length).to.equal(2)
})
})
@@ -51,41 +56,49 @@ describe('<Agents />', () => {
context('<Agent />', () => {
it('renders without no-calls class if there is a non-zero callCount', () => {
const component = shallow(<Agent model={agentModel({ callCount: 1 })} />)
expect(component).not.to.have.className('no-calls')
})
it('renders with no-calls class if zero callCount', () => {
const component = shallow(<Agent model={agentModel()} />)
expect(component).to.have.className('no-calls')
})
it('renders the type', () => {
const component = shallow(<Agent model={agentModel()} />)
expect(component.find('td').first()).to.have.text('spy')
})
it('renders the function name', () => {
const component = shallow(<Agent model={agentModel()} />)
expect(component.find('td').at(1)).to.have.text('foo')
})
it('renders the callCount if non-zero', () => {
const component = shallow(<Agent model={agentModel({ callCount: 1 })} />)
expect(component.find('.call-count')).to.have.text('1')
})
it('renders the callCount as "-" if zero', () => {
const component = shallow(<Agent model={agentModel()} />)
expect(component.find('.call-count')).to.have.text('-')
})
it('renders alias when singular', () => {
const component = shallow(<Agent model={agentModel({ alias: 'foo' })} />)
expect(component.find('td').at(2)).to.have.text('foo')
})
it('renders aliases when multiple', () => {
const component = shallow(<Agent model={agentModel({ alias: ['foo', 'bar'] })} />)
expect(component.find('td').at(2)).to.have.text('foo, bar')
})
})
@@ -6,47 +6,56 @@ import Collapsible from './collapsible'
describe('<Collapsible />', () => {
it('renders unopen', () => {
const component = shallow(<Collapsible />)
expect(component).not.to.have.className('is-open')
})
it('renders with is-open class when isOpen is true', () => {
const component = shallow(<Collapsible isOpen={true} />)
expect(component).to.have.className('is-open')
})
it('renders with headerClass on the header when specified', () => {
const component = shallow(<Collapsible headerClass='foo' />)
expect(component.find('.collapsible-header')).to.have.className('foo')
})
it('renders with headerStyle when specified', () => {
const component = shallow(<Collapsible headerStyle={{ margin: 0 }} />)
expect(component.find('.collapsible-header')).to.have.style({ margin: 0 })
})
it('renders the header', () => {
const component = shallow(<Collapsible header={<header>The header</header>} />)
expect(component.find('.collapsible-header header')).to.have.text('The header')
})
it('renders with contentClass on the content when specified', () => {
const component = shallow(<Collapsible contentClass='bar' />)
expect(component.find('.collapsible-content')).to.have.className('bar')
})
it('renders the children', () => {
const component = shallow(<Collapsible><main>A child</main></Collapsible>)
expect(component.find('.collapsible-content main')).to.have.text('A child')
})
it('opens when clicking header', () => {
const component = shallow(<Collapsible />)
component.find('.collapsible-header').simulate('click')
expect(component).to.have.className('is-open')
})
it('closes when clicking header twice', () => {
const component = shallow(<Collapsible />)
component.find('.collapsible-header').simulate('click')
component.find('.collapsible-header').simulate('click')
expect(component).not.to.have.className('is-open')
@@ -84,7 +84,9 @@ export default class Command extends Instrument {
if (this._becameNonPending()) {
clearTimeout(this._pendingTimeout)
action('became:inactive', () => this.isLongRunning = false)()
action('became:inactive', () => {
return this.isLongRunning = false
})()
}
this._prevState = this.state
@@ -32,6 +32,7 @@ describe('Command model', () => {
describe('when model is pending on initialization and LONG_RUNNING_THRESHOLD passes', () => {
let command
beforeEach(() => {
command = new Command(model())
})
@@ -51,6 +52,7 @@ describe('Command model', () => {
describe('when model is not pending on initialization, is updated to pending, and LONG_RUNNING_THRESHOLD passes', () => {
let command
beforeEach(() => {
command = new Command(model({ state: null }))
clock.tick(300)
@@ -164,6 +164,7 @@ class Command extends Component {
_isOtherCommandPinned () {
const pinnedId = this.props.appState.pinnedSnapshotId
return pinnedId != null && pinnedId !== this.props.model.id
}
@@ -237,4 +238,5 @@ class Command extends Component {
}
export { Aliases, AliasesReferences, Message }
export default Command
@@ -40,76 +40,91 @@ describe('<Command />', () => {
context('class names', () => {
it('renders with the type class', () => {
const component = shallow(<Command model={model()} />)
expect(component).to.have.className('command-type-parent')
})
it('renders with the name class, whitespace converted to dashes', () => {
const component = shallow(<Command model={model()} />)
expect(component).to.have.className('command-name-The-name')
})
it('renders with the state class', () => {
const component = shallow(<Command model={model()} />)
expect(component).to.have.className('command-state-passed')
})
it('renders with the is-event class when an event', () => {
const component = shallow(<Command model={model({ event: true })} />)
expect(component).to.have.className('command-is-event')
})
it('renders with the is-invisible class when not visible', () => {
const component = shallow(<Command model={model({ visible: false })} />)
expect(component).to.have.className('command-is-invisible')
})
it('renders with the has-num-elements class when it has numElements', () => {
const component = shallow(<Command model={model({ numElements: 1 })} />)
expect(component).to.have.className('command-has-num-elements')
})
it('renders with the no-elements class when numElements is 0', () => {
const component = shallow(<Command model={model({ numElements: 0 })} />)
expect(component).to.have.className('no-elements')
})
it('renders with the multiple-elements class when numElements is more than 1', () => {
const component = shallow(<Command model={model({ numElements: 2 })} />)
expect(component).to.have.className('multiple-elements')
})
it('renders with the other-pinned class when another command is pinned', () => {
const component = shallow(<Command model={model()} appState={appStateStub({ pinnedSnapshotId: 'c2' })} />)
expect(component).to.have.className('command-other-pinned')
})
it('does not render with the other-pinned class when no command is pinned', () => {
const component = shallow(<Command model={model()} appState={appStateStub()} />)
expect(component).not.to.have.className('command-other-pinned')
})
it('does not render with the other-pinned class when it itself is pinned', () => {
const component = shallow(<Command model={model()} appState={appStateStub({ pinnedSnapshotId: 'c1' })} />)
expect(component).not.to.have.className('command-other-pinned')
})
it('renders with the is-pinned class when it itself is pinned', () => {
const component = shallow(<Command model={model()} appState={appStateStub({ pinnedSnapshotId: 'c1' })} />)
expect(component).to.have.className('command-is-pinned')
})
it('renders with the with-indicator class when it has a renderProps indicator', () => {
const component = shallow(<Command model={model({ renderProps: { indicator: 'bad' } })} />)
expect(component).to.have.className('command-with-indicator')
})
it('renders with the scaled class when it has a renderProps message over 100 chars long', () => {
const component = shallow(<Command model={model({ displayMessage: longText })} />)
expect(component).to.have.className('command-scaled')
})
it('does not render with the scaled class when it has a renderProps message less than 100 chars long', () => {
const component = shallow(<Command model={model({ renderProps: { message: 'is short' } })} />)
expect(component).not.to.have.className('command-scaled')
})
})
@@ -117,16 +132,19 @@ describe('<Command />', () => {
context('name', () => {
it('renders the name', () => {
const component = shallow(<Command model={model()} />)
expect(component.find('.command-method')).to.have.text('The name')
})
it('renders the display name if specified', () => {
const component = shallow(<Command model={model({ displayName: 'The displayed name' })} />)
expect(component.find('.command-method')).to.have.text('The displayed name')
})
it('renders the name or display name in parentheses if an event', () => {
const component = shallow(<Command model={model({ event: true })} />)
expect(component.find('.command-method')).to.have.text('(The name)')
})
})
@@ -134,21 +152,25 @@ describe('<Command />', () => {
context('message', () => {
it('renders the displayMessage', () => {
const component = shallow(<Command model={model()} />)
expect(component.find(Message).first().shallow().find('.command-message-text').html()).to.contain('The message')
})
it('does not truncate the message when over 100 chars', () => {
const component = shallow(<Command model={model({ displayMessage: longText })} />)
expect(component.find(Message).first().shallow().find('.command-message-text').html()).to.contain(longText)
})
it('renders markdown', () => {
const component = shallow(<Command model={model({ displayMessage: withMarkdown })} />)
expect(component.find(Message).first().shallow().find('.command-message-text').html()).to.contain(fromMarkdown)
})
it('includes the renderProps indicator as a class name when specified', () => {
const component = shallow(<Command model={model({ renderProps: { indicator: 'bad' } })} />)
expect(component.find(Message).first().shallow().find('.bad')).to.exist
})
})
@@ -180,6 +202,7 @@ describe('<Command />', () => {
it('renders the right tooltip title for each alias it references', () => {
const tooltips = aliases.find('Tooltip')
expect(tooltips.first()).to.have.prop('title', 'Found an alias for: \'barAlias\'')
expect(tooltips.last()).to.have.prop('title', 'Found an alias for: \'bazAlias\'')
})
@@ -284,11 +307,13 @@ describe('<Command />', () => {
context('invisible indicator', () => {
it('renders a tooltip for the invisible indicator', () => {
const component = shallow(<Command model={model({ visible: false })} />)
expect(component.find('Tooltip').first().find('.command-invisible')).to.exist
})
it('renders the invisible indicator tooltip with the right title', () => {
const component = shallow(<Command model={model({ visible: false })} />)
expect(component.find('Tooltip').first()).to.have.prop('title', 'This element is not visible.')
})
})
@@ -296,16 +321,19 @@ describe('<Command />', () => {
context('elements', () => {
it('renders the number of elements', () => {
const component = shallow(<Command model={model({ numElements: 3 })} />)
expect(component.find('.num-elements')).to.have.text('3')
})
it('renders a tooltip for the number of elements', () => {
const component = shallow(<Command model={model({ numElements: 3 })} />)
expect(component.find('Tooltip').at(1).find('.num-elements')).to.exist
})
it('renders the number of elements tooltip with the right title', () => {
const component = shallow(<Command model={model({ numElements: 3 })} />)
expect(component.find('Tooltip').at(1)).to.have.prop('title', '3 matched elements')
})
})
@@ -313,16 +341,19 @@ describe('<Command />', () => {
context('other contents', () => {
it('renders a <FlashOnClick /> around the contents', () => {
const component = shallow(<Command model={model()} />)
expect(component.find('FlashOnClick')).to.exist
})
it('the <FlashOnClick /> has a pinned snapshot and console print message', () => {
const component = shallow(<Command model={model()} />)
expect(component.find('FlashOnClick')).to.have.prop('message', 'Printed output to your console')
})
it('renders the number', () => {
const component = shallow(<Command model={model()} />)
expect(component.find('.command-number')).to.have.text('1')
})
})
@@ -436,31 +467,37 @@ describe('<Command />', () => {
it('renders with command-has-duplicates class if it has duplicates', () => {
const component = shallow(<Command model={model(withDuplicates)} />)
expect(component).to.have.className('command-has-duplicates')
})
it('renders without command-has-duplicates class if no duplicates', () => {
const component = shallow(<Command model={model()} />)
expect(component).not.to.have.className('command-has-duplicates')
})
it('renders with command-is-duplicate class if it is a duplicate', () => {
const component = shallow(<Command model={model({ isDuplicate: true })} />)
expect(component).to.have.className('command-is-duplicate')
})
it('renders without command-is-duplicate class if it is not a duplicate', () => {
const component = shallow(<Command model={model()} />)
expect(component).not.to.have.className('command-is-duplicate')
})
it('displays number of duplicates', () => {
const component = shallow(<Command model={model({ hasDuplicates: true, numDuplicates: 5 })} />)
expect(component.find('.num-duplicates')).to.have.text('5')
})
it('opens after clicking expander', () => {
const component = shallow(<Command model={model(withDuplicates)} />)
component.find('.command-expander').simulate('click', { stopPropagation: () => {} })
expect(component).to.have.className('command-is-open')
})
@@ -17,33 +17,39 @@ describe('<AnError />', () => {
it('renders the title', () => {
const component = shallow(<AnError error={error} />)
expect(component.text()).to.include(error.title)
})
it('renders the link if there is one', () => {
const component = shallow(<AnError error={error} />)
expect(component.find('a').prop('href')).to.equal(error.link)
})
it('does not render link if there is not one', () => {
error.link = null
const component = shallow(<AnError error={error} />)
expect(component.find('a')).to.be.empty
})
it('renders callout in pre if there is one', () => {
const component = shallow(<AnError error={error} />)
expect(component.find('pre').text()).to.equal(error.callout)
})
it('does not callout if there is not one', () => {
error.callout = null
const component = shallow(<AnError error={error} />)
expect(component.find('pre')).to.be.empty
})
it('renders message with markdown', () => {
const component = shallow(<AnError error={error} />)
expect(component.find('.error-message').prop('dangerouslySetInnerHTML').__html).to.include('<h1>Message with markdown</h1>')
})
})
@@ -21,32 +21,38 @@ describe('<Controls />', () => {
describe('when running, not paused, and/or without next command', () => {
it('renders toggle auto-scrolling button', () => {
const component = shallow(<Controls events={eventsStub()} appState={appStateStub()} />)
expect(component.find('.toggle-auto-scrolling')).to.exist
})
it('renders toggle auto-scrolling button with auto-scrolling-enabled class when auto-scrolling is enabled', () => {
const component = shallow(<Controls events={eventsStub()} appState={appStateStub()} />)
expect(component.find('.toggle-auto-scrolling')).to.have.className('auto-scrolling-enabled')
})
it('renders toggle auto-scrolling button without auto-scrolling-enabled class when auto-scrolling is disabled', () => {
const component = shallow(<Controls events={eventsStub()} appState={appStateStub({ autoScrollingEnabled: false })} />)
expect(component.find('.toggle-auto-scrolling')).not.to.have.className('auto-scrolling-enabled')
})
it('renders tooltip around toggle auto-scrolling button with right title when auto-scrolling is enabled', () => {
const component = shallow(<Controls events={eventsStub()} appState={appStateStub()} />)
expect(component.find('.toggle-auto-scrolling').parent()).to.have.prop('title', 'Disable Auto-scrolling')
})
it('renders tooltip around toggle auto-scrolling button with right title when auto-scrolling is disabled', () => {
const component = shallow(<Controls events={eventsStub()} appState={appStateStub({ autoScrollingEnabled: false })} />)
expect(component.find('.toggle-auto-scrolling').parent()).to.have.prop('title', 'Enable Auto-scrolling')
})
it('toggles appState.autoScrollingEnabled when auto-scrolling button is clicked', () => {
const appState = appStateStub()
const component = shallow(<Controls events={eventsStub()} appState={appState} />)
component.find('.toggle-auto-scrolling').simulate('click')
expect(appState.toggleAutoScrolling).to.have.been.called
})
@@ -54,124 +60,146 @@ describe('<Controls />', () => {
it('emits save:state event when auto-scrolling button is clicked', () => {
const events = eventsStub()
const component = shallow(<Controls events={events} appState={appStateStub()} />)
component.find('.toggle-auto-scrolling').simulate('click')
expect(events.emit).to.have.been.calledWith('save:state')
})
it('renders stop button', () => {
const component = shallow(<Controls events={eventsStub()} appState={appStateStub()} />)
expect(component.find('.stop')).to.exist
})
it('renders tooltip around stop button', () => {
const component = shallow(<Controls events={eventsStub()} appState={appStateStub()} />)
expect(component.find('.stop').parent()).to.have.prop('title', 'Stop Running')
})
it('emits stop event when stop button is clicked', () => {
const events = eventsStub()
const component = shallow(<Controls events={events} appState={appStateStub()} />)
component.find('.stop').simulate('click')
expect(events.emit).to.have.been.calledWith('stop')
})
it('does not render paused label', () => {
const component = shallow(<Controls events={eventsStub()} appState={appStateStub()} />)
expect(component.find('.paused-label')).not.to.exist
})
it('does not render play button', () => {
const component = shallow(<Controls events={eventsStub()} appState={appStateStub()} />)
expect(component.find('.play')).not.to.exist
})
it('does not render restart button', () => {
const component = shallow(<Controls events={eventsStub()} appState={appStateStub()} />)
expect(component.find('.restart')).not.to.exist
})
it('does not render next button', () => {
const component = shallow(<Controls events={eventsStub()} appState={appStateStub()} />)
expect(component.find('.next')).not.to.exist
})
})
describe('when paused with next command', () => {
let appState
beforeEach(() => {
appState = appStateStub({ isPaused: true, nextCommandName: 'next command' })
})
it('renders paused label', () => {
const component = shallow(<Controls events={eventsStub()} appState={appState} />)
expect(component.find('.paused-label')).to.exist
})
it('renders play button', () => {
const component = shallow(<Controls events={eventsStub()} appState={appState} />)
expect(component.find('.play')).to.exist
})
it('renders tooltip around play button', () => {
const component = shallow(<Controls events={eventsStub()} appState={appState} />)
expect(component.find('.play').parent()).to.have.prop('title', 'Resume')
})
it('emits resume event when play button is clicked', () => {
const events = eventsStub()
const component = shallow(<Controls events={events} appState={appState} />)
component.find('.play').simulate('click')
expect(events.emit).to.have.been.calledWith('resume')
})
it('renders next button', () => {
const component = shallow(<Controls events={eventsStub()} appState={appState} />)
expect(component.find('.next')).to.exist
})
it('renders tooltip around next button', () => {
const component = shallow(<Controls events={eventsStub()} appState={appState} />)
expect(component.find('.next').parent()).to.have.prop('title', 'Next: \'next command\'')
})
it('emits resume event when next button is clicked', () => {
const events = eventsStub()
const component = shallow(<Controls events={events} appState={appState} />)
component.find('.next').simulate('click')
expect(events.emit).to.have.been.calledWith('next')
})
it('does not render stop button', () => {
const component = shallow(<Controls events={eventsStub()} appState={appState} />)
expect(component.find('.stop')).not.to.exist
})
})
describe('when not running', () => {
let appState
beforeEach(() => {
appState = appStateStub({ isRunning: false })
})
it('renders restart button', () => {
const component = shallow(<Controls events={eventsStub()} appState={appState} />)
expect(component.find('.restart')).to.exist
})
it('renders tooltip around restart button', () => {
const component = shallow(<Controls events={eventsStub()} appState={appState} />)
expect(component.find('.restart').parent()).to.have.prop('title', 'Run all tests')
})
it('emits restart event when restart button is clicked', () => {
const events = eventsStub()
const component = shallow(<Controls events={events} appState={appState} />)
component.find('.restart').simulate('click')
expect(events.emit).to.have.been.calledWith('restart')
})
it('does not render stop button', () => {
const component = shallow(<Controls events={eventsStub()} appState={appState} />)
expect(component.find('.stop')).not.to.exist
})
})
@@ -9,16 +9,19 @@ const eventsStub = () => ({ emit: sinon.spy() })
describe('<Header />', () => {
it('renders the focus tests button', () => {
const component = shallow(<Header />)
expect(component.find('button')).to.exist
})
it('renders a tooltip around focus tests button', () => {
const component = shallow(<Header />)
expect(component.find('Tooltip')).to.have.prop('title', 'View All Tests')
})
it('emits the focus:tests event when the focus tests button is clicked', () => {
const events = eventsStub()
shallow(<Header events={events} />).find('button').simulate('click')
expect(events.emit).to.have.been.calledWith('focus:tests')
})
@@ -51,6 +51,7 @@ class StatsStore {
incrementCount (type) {
const countKey = `num${_.capitalize(type)}`
this[countKey] = this[countKey] + 1
}
@@ -76,4 +77,5 @@ class StatsStore {
}
export { StatsStore }
export default new StatsStore()
@@ -78,6 +78,7 @@ describe('stats store', () => {
context('#incrementCount', () => {
it('increments the count for the type specified', () => {
const instance = new StatsStore()
instance.incrementCount('passed')
expect(instance.numPassed).to.equal(1)
instance.incrementCount('pending')
@@ -90,6 +91,7 @@ describe('stats store', () => {
context('#reset', () => {
let instance
beforeEach(() => {
instance = new StatsStore()
})
@@ -17,11 +17,13 @@ describe('<Stats />', () => {
context('passed', () => {
it('renders -- when zero', () => {
const component = shallow(<Stats stats={statsStub()} />)
expect(component.find('.passed .num')).to.have.text('--')
})
it('renders number when non-zero', () => {
const component = shallow(<Stats stats={statsStub({ numPassed: 5 })} />)
expect(component.find('.passed .num')).to.have.text('5')
})
})
@@ -29,11 +31,13 @@ describe('<Stats />', () => {
context('failed', () => {
it('renders -- when zero', () => {
const component = shallow(<Stats stats={statsStub()} />)
expect(component.find('.failed .num')).to.have.text('--')
})
it('renders number when non-zero', () => {
const component = shallow(<Stats stats={statsStub({ numFailed: 7 })} />)
expect(component.find('.failed .num')).to.have.text('7')
})
})
@@ -41,11 +45,13 @@ describe('<Stats />', () => {
context('pending', () => {
it('renders -- when zero', () => {
const component = shallow(<Stats stats={statsStub()} />)
expect(component.find('.pending .num')).to.have.text('--')
})
it('renders number when non-zero', () => {
const component = shallow(<Stats stats={statsStub({ numPending: 3 })} />)
expect(component.find('.pending .num')).to.have.text('3')
})
})
@@ -53,11 +59,13 @@ describe('<Stats />', () => {
context('duration', () => {
it('renders -- when zero', () => {
const component = shallow(<Stats stats={statsStub()} />)
expect(component.find('.duration .num')).to.have.text('--')
})
it('renders number when non-zero, converted from milliseconds to seconds and fixed to 2 decimal places', () => {
const component = shallow(<Stats stats={statsStub({ duration: 10562.452323523 })} />)
expect(component.find('.duration .num')).to.have.text('10.56')
})
})
+5 -1
View File
@@ -18,7 +18,9 @@ export default class Hook {
command.number = this._currentNumber
this._currentNumber++
}
const lastCommand = _.last(this.commands)
if (lastCommand && lastCommand.isMatchingEvent(command)) {
lastCommand.addDuplicate(command)
} else {
@@ -28,7 +30,9 @@ export default class Hook {
commandMatchingErr (errToMatch) {
return _(this.commands)
.filter(({ err }) => err.displayMessage === errToMatch.displayMessage)
.filter(({ err }) => {
return err.displayMessage === errToMatch.displayMessage
})
.last()
}
}
+38 -8
View File
@@ -3,53 +3,72 @@ import Hook from './hook-model'
describe('Hook model', () => {
let hook
beforeEach(() => {
hook = new Hook({ name: 'before' })
})
it('gives hooks unique ids', () => {
const anotherHook = new Hook({ name: 'test' })
expect(hook.id).not.to.equal(anotherHook.id)
})
context('#addCommand', () => {
it('adds the command to its command collection', () => {
hook.addCommand({ isMatchingEvent: () => false })
hook.addCommand({ isMatchingEvent: () => {
return false
} })
expect(hook.commands.length).to.equal(1)
hook.addCommand({})
expect(hook.commands.length).to.equal(2)
})
it('numbers commands incrementally when not events', () => {
const command1 = { event: false, isMatchingEvent: () => false }
const command1 = { event: false, isMatchingEvent: () => {
return false
} }
hook.addCommand(command1)
expect(command1.number).to.equal(1)
const command2 = { event: false }
hook.addCommand(command2)
expect(command2.number).to.equal(2)
})
it('does not number event commands', () => {
const command1 = { event: false, isMatchingEvent: () => false }
const command1 = { event: false, isMatchingEvent: () => {
return false
} }
hook.addCommand(command1)
expect(command1.number).to.equal(1)
const command2 = { event: true, isMatchingEvent: () => false }
const command2 = { event: true, isMatchingEvent: () => {
return false
} }
hook.addCommand(command2)
expect(command2.number).to.be.undefined
const command3 = { event: false }
hook.addCommand(command3)
expect(command3.number).to.equal(2)
})
it('adds command as duplicate if it matches the last command', () => {
const addDuplicate = sinon.spy()
const command1 = { event: true, isMatchingEvent: () => true, addDuplicate }
const command1 = { event: true, isMatchingEvent: () => {
return true
}, addDuplicate }
hook.addCommand(command1)
const command2 = { event: true }
hook.addCommand(command2)
expect(addDuplicate).to.be.calledWith(command2)
@@ -58,20 +77,31 @@ describe('Hook model', () => {
context('#commandMatchingErr', () => {
it('returns last command to match the error', () => {
const matchesButIsntLast = { err: { displayMessage: 'matching error message' }, isMatchingEvent: () => false }
const matchesButIsntLast = { err: { displayMessage: 'matching error message' }, isMatchingEvent: () => {
return false
} }
hook.addCommand(matchesButIsntLast)
const doesntMatch = { err: { displayMessage: 'other error message' }, isMatchingEvent: () => false }
const doesntMatch = { err: { displayMessage: 'other error message' }, isMatchingEvent: () => {
return false
} }
hook.addCommand(doesntMatch)
const matches = { err: { displayMessage: 'matching error message' } }
hook.addCommand(matches)
expect(hook.commandMatchingErr({ displayMessage: 'matching error message' })).to.eql(matches)
})
it('returns undefined when no match', () => {
const noMatch1 = { err: { displayMessage: 'some error message' }, isMatchingEvent: () => false }
const noMatch1 = { err: { displayMessage: 'some error message' }, isMatchingEvent: () => {
return false
} }
hook.addCommand(noMatch1)
const noMatch2 = { err: { displayMessage: 'other error message' } }
hook.addCommand(noMatch2)
expect(hook.commandMatchingErr({ displayMessage: 'matching error message' })).to.be.undefined
+1
View File
@@ -32,4 +32,5 @@ const Hooks = observer(({ model }) => (
))
export { Hook, HookHeader }
export default Hooks
@@ -26,33 +26,39 @@ const model = (props) => {
describe('<Hooks />', () => {
it('renders a <Hook /> for each hook in model', () => {
const component = shallow(<Hooks model={model()} />)
expect(component.find(Hook).length).to.equal(3)
})
context('<Hook />', () => {
it('renders without hook-failed class when not failed', () => {
const component = shallow(<Hook model={hookModel()} />)
expect(component).not.to.have.className('hook-failed')
})
it('renders with hook-failed class when failed', () => {
const component = shallow(<Hook model={hookModel({ failed: true })} />)
expect(component).to.have.className('hook-failed')
})
it('renders Collapsible with hook header', () => {
const component = shallow(<Hook model={hookModel()} />)
const header = shallow(component.find('Collapsible').prop('header'))
expect(header.find('.hook-failed-message')).to.have.text('(failed)')
})
it('renders Collapsible open', () => {
const component = shallow(<Hook model={hookModel()} />)
expect(component.find('Collapsible').prop('isOpen')).to.be.true
})
it('renders command for each in model', () => {
const component = shallow(<Hook model={hookModel()} />)
expect(component.find('Command').length).to.equal(2)
})
})
@@ -60,11 +66,13 @@ describe('<Hooks />', () => {
context('<HookHeader />', () => {
it('renders the name', () => {
const component = shallow(<HookHeader name='before' />)
expect(component.text()).to.contain('before')
})
it('renders the failed message', () => {
const component = shallow(<HookHeader name='before' />)
expect(component.find('.hook-failed-message')).to.have.text('(failed)')
})
})
+1
View File
@@ -72,4 +72,5 @@ class AppState {
}
export { AppState }
export default new AppState()
@@ -8,12 +8,14 @@ describe('app state', () => {
context('#startRunning', () => {
it('sets isRunning to true', () => {
const instance = new AppState()
instance.startRunning()
expect(instance.isRunning).to.be.true
})
it('sets isStopped to false', () => {
const instance = new AppState()
instance.isStopped = true
instance.startRunning()
expect(instance.isStopped).to.be.false
@@ -23,12 +25,14 @@ describe('app state', () => {
context('#pause', () => {
it('sets isPaused to true', () => {
const instance = new AppState()
instance.pause()
expect(instance.isPaused).to.be.true
})
it('sets the next command name', () => {
const instance = new AppState()
instance.pause('next command')
expect(instance.nextCommandName).to.equal('next command')
})
@@ -37,12 +41,14 @@ describe('app state', () => {
context('#resume', () => {
it('sets isPaused to false', () => {
const instance = new AppState()
instance.resume()
expect(instance.isPaused).to.be.false
})
it('unsets the next command name', () => {
const instance = new AppState()
instance.resume()
expect(instance.nextCommandName).to.be.null
})
@@ -51,6 +57,7 @@ describe('app state', () => {
context('#stop', () => {
it('sets isStopped to true', () => {
const instance = new AppState()
instance.stop()
expect(instance.isStopped).to.be.true
})
@@ -59,12 +66,14 @@ describe('app state', () => {
context('#end', () => {
it('sets isRunning to false', () => {
const instance = new AppState()
instance.end()
expect(instance.isRunning).to.be.false
})
it('resets autoScrollingEnabled', () => {
const instance = new AppState()
instance.temporarilySetAutoScrolling(false)
instance.end()
expect(instance.autoScrollingEnabled).to.be.true
@@ -74,18 +83,21 @@ describe('app state', () => {
context('#temporarilySetAutoScrolling', () => {
it('sets autoScrollingEnabled to boolean specified', () => {
const instance = new AppState()
instance.temporarilySetAutoScrolling(false)
expect(instance.autoScrollingEnabled).to.be.false
})
it('does nothing if argument is null', () => {
const instance = new AppState()
instance.temporarilySetAutoScrolling(null)
expect(instance.autoScrollingEnabled).to.be.true
})
it('does nothing if argument is undefined', () => {
const instance = new AppState()
instance.temporarilySetAutoScrolling()
expect(instance.autoScrollingEnabled).to.be.true
})
@@ -94,6 +106,7 @@ describe('app state', () => {
context('#setAutoScrolling', () => {
it('sets autoScrollingEnabled', () => {
const instance = new AppState()
instance.setAutoScrolling(false)
expect(instance.autoScrollingEnabled).to.be.false
instance.setAutoScrolling(true)
@@ -102,6 +115,7 @@ describe('app state', () => {
it('sets reset value for autoScrollingEnabled', () => {
const instance = new AppState()
instance.setAutoScrolling(false)
instance.reset()
expect(instance.autoScrollingEnabled).to.be.false
@@ -111,6 +125,7 @@ describe('app state', () => {
context('#toggleAutoScrolling', () => {
it('toggles autoScrollingEnabled', () => {
const instance = new AppState()
instance.toggleAutoScrolling()
expect(instance.autoScrollingEnabled).to.be.false
instance.toggleAutoScrolling()
@@ -119,6 +134,7 @@ describe('app state', () => {
it('sets reset value for autoScrollingEnabled', () => {
const instance = new AppState()
instance.toggleAutoScrolling()
instance.reset()
expect(instance.autoScrollingEnabled).to.be.false
@@ -128,6 +144,7 @@ describe('app state', () => {
context('#reset', () => {
it('resets autoScrollingEnabled when it has not been toggled', () => {
const instance = new AppState()
instance.temporarilySetAutoScrolling(false)
instance.reset()
expect(instance.autoScrollingEnabled).to.be.true
@@ -135,6 +152,7 @@ describe('app state', () => {
it('does not reset autoScrollingEnabled when it has been toggled', () => {
const instance = new AppState()
instance.toggleAutoScrolling()
instance.reset()
expect(instance.autoScrollingEnabled).to.be.false
@@ -142,6 +160,7 @@ describe('app state', () => {
it('sets isPaused to false', () => {
const instance = new AppState()
instance.isPaused = true
instance.reset()
expect(instance.isPaused).to.be.false
@@ -149,6 +168,7 @@ describe('app state', () => {
it('sets isRunning to false', () => {
const instance = new AppState()
instance.isRunning = true
instance.reset()
expect(instance.isRunning).to.be.false
@@ -156,6 +176,7 @@ describe('app state', () => {
it('sets nextCommandName to null', () => {
const instance = new AppState()
instance.nextCommandName = 'next command'
instance.reset()
expect(instance.nextCommandName).to.be.null
@@ -163,6 +184,7 @@ describe('app state', () => {
it('sets pinnedSnapshotId to null', () => {
const instance = new AppState()
instance.pinnedSnapshotId = 'c4'
instance.reset()
expect(instance.pinnedSnapshotId).to.be.null
@@ -4,11 +4,13 @@ describe('Err model', () => {
context('.displayMessage', () => {
it('returns combo of name and message', () => {
const err = new Err({ name: 'BadError', message: 'Something went wrong' })
expect(err.displayMessage).to.equal('BadError: Something went wrong')
})
it('returns empty string if no name or message', () => {
const err = new Err()
expect(err.displayMessage).to.equal('')
})
})
@@ -16,22 +18,26 @@ describe('Err model', () => {
context('.isCommandErr', () => {
it('returns true if an AssertionError', () => {
const err = new Err({ name: 'AssertionError', message: 'Something went wrong' })
expect(err.isCommandErr).to.be.true
})
it('returns true if an CypressError', () => {
const err = new Err({ name: 'CypressError', message: 'Something went wrong' })
expect(err.isCommandErr).to.be.true
})
it('returns false otherwise', () => {
const err = new Err({ name: 'BadError', message: 'Something went wrong' })
expect(err.isCommandErr).to.be.false
})
})
context('#update', () => {
let err
beforeEach(() => {
err = new Err({ name: 'BadError', message: 'Something went wrong' })
})
+3
View File
@@ -106,9 +106,12 @@ export default {
localBus.on('show:error', (testId) => {
const test = runnablesStore.testById(testId)
if (test.err.isCommandErr) {
const command = test.commandMatchingErr()
if (!command) return
runner.emit('runner:console:log', command.id)
} else {
runner.emit('runner:console:error', testId)
+54 -37
View File
@@ -2,45 +2,55 @@ import sinon from 'sinon'
import events from './events'
const runnerStub = () => ({
on: sinon.stub(),
emit: sinon.spy(),
})
const runnerStub = () => {
return {
on: sinon.stub(),
emit: sinon.spy(),
}
}
const appStateStub = () => ({
startRunning: sinon.spy(),
pause: sinon.spy(),
reset: sinon.spy(),
resume: sinon.spy(),
end: sinon.spy(),
temporarilySetAutoScrolling: sinon.spy(),
stop: sinon.spy(),
})
const appStateStub = () => {
return {
startRunning: sinon.spy(),
pause: sinon.spy(),
reset: sinon.spy(),
resume: sinon.spy(),
end: sinon.spy(),
temporarilySetAutoScrolling: sinon.spy(),
stop: sinon.spy(),
}
}
const runnablesStoreStub = () => ({
addLog: sinon.spy(),
reset: sinon.spy(),
runnableStarted: sinon.spy(),
runnableFinished: sinon.spy(),
setInitialScrollTop: sinon.stub(),
setRunnables: sinon.spy(),
testById: sinon.stub(),
updateLog: sinon.spy(),
})
const runnablesStoreStub = () => {
return {
addLog: sinon.spy(),
reset: sinon.spy(),
runnableStarted: sinon.spy(),
runnableFinished: sinon.spy(),
setInitialScrollTop: sinon.stub(),
setRunnables: sinon.spy(),
testById: sinon.stub(),
updateLog: sinon.spy(),
}
}
const scrollerStub = () => ({
getScrollTop: sinon.stub(),
})
const scrollerStub = () => {
return {
getScrollTop: sinon.stub(),
}
}
const statsStoreStub = () => ({
incrementCount: sinon.spy(),
pause: sinon.spy(),
reset: sinon.spy(),
resume: sinon.spy(),
start: sinon.spy(),
startRunning: sinon.spy(),
end: sinon.spy(),
})
const statsStoreStub = () => {
return {
incrementCount: sinon.spy(),
pause: sinon.spy(),
reset: sinon.spy(),
resume: sinon.spy(),
start: sinon.spy(),
startRunning: sinon.spy(),
end: sinon.spy(),
}
}
describe('events', () => {
let appState
@@ -169,6 +179,7 @@ describe('events', () => {
it('calls callback with scrollTop and autoScrollingEnabled on reporter:collect:run:state', () => {
const callback = sinon.spy()
appState.autoScrollingEnabled = false
scroller.getScrollTop.returns(321)
runner.on.withArgs('reporter:collect:run:state').callArgWith(1, callback)
@@ -227,14 +238,20 @@ describe('events', () => {
})
it('emits runner:console:log on show:error when it is a command error and there is a matching command', () => {
const test = { err: { isCommandErr: true }, commandMatchingErr: () => ({ id: 'matching command id' }) }
const test = { err: { isCommandErr: true }, commandMatchingErr: () => {
return { id: 'matching command id' }
} }
runnablesStore.testById.returns(test)
events.emit('show:error', 'test id')
expect(runner.emit).to.have.been.calledWith('runner:console:log', 'matching command id')
})
it('does not emit anything on show:error it is a command error but there not a matching command', () => {
const test = { err: { isCommandErr: true }, commandMatchingErr: () => null }
const test = { err: { isCommandErr: true }, commandMatchingErr: () => {
return null
} }
runnablesStore.testById.returns(test)
events.emit('show:error', 'test id')
expect(runner.emit).not.to.have.been.called
@@ -15,11 +15,13 @@ const renderComponent = ({ onClick = (() => {}) }) => {
describe('<FlashOnClick />', () => {
it('renders a tooltip with the specified message', () => {
const component = renderComponent({})
expect(component.find('Tooltip')).to.have.prop('title', 'Some message')
})
it('renders a tooltip around the content', () => {
const component = renderComponent({})
expect(component.find('Tooltip').find('.content')).to.exist
})
+4
View File
@@ -30,6 +30,7 @@ class Scroller {
if (!this._userScroll) {
// programmatic scroll
this._userScroll = true
return
}
@@ -41,8 +42,10 @@ class Scroller {
clearTimeout(this._countUserScrollsTimeout)
this._countUserScrollsTimeout = null
this._userScrollCount = 0
return
}
if (this._countUserScrollsTimeout) return
this._countUserScrollsTimeout = setTimeout(() => {
@@ -64,6 +67,7 @@ class Scroller {
// aim to scroll just into view, so that the bottom of the element
// is just above the bottom of the container
let scrollTopGoal = this._aboveBottom(element)
// can't have a negative scroll, so put it to the top
if (scrollTopGoal < 0) {
scrollTopGoal = 0
+10 -1
View File
@@ -35,11 +35,14 @@ describe('scroller', () => {
})
it('throws an error if attempting to scroll an element before setting a container', () => {
expect(() => scroller.scrollIntoView({})).to.throw(/container must be set/)
expect(() => {
return scroller.scrollIntoView({})
}).to.throw(/container must be set/)
})
it('does not scroll if near top and scrolling would result in negative scroll', () => {
const container = getContainer()
scroller.setContainer(container)
scroller.scrollIntoView(getElement({ offsetTop: 0 }))
expect(container.scrollTop).to.equal(0)
@@ -47,6 +50,7 @@ describe('scroller', () => {
it('does not scroll if already full visible', () => {
const container = getContainer()
scroller.setContainer(container)
scroller.scrollIntoView(getElement({ offsetTop: 80 }))
expect(container.scrollTop).to.equal(0)
@@ -54,6 +58,7 @@ describe('scroller', () => {
it('scrolls to the goal', () => {
const container = getContainer({ scrollTop: 50 })
scroller.setContainer(container)
scroller.scrollIntoView(getElement({ offsetTop: 600 }))
expect(container.scrollTop).to.equal(320)
@@ -98,6 +103,7 @@ describe('scroller', () => {
context('scrolling', () => {
it('listens to scroll event on container', () => {
const container = getContainer()
scroller.setContainer(container)
expect(container.addEventListener).to.have.been.calledWith('scroll')
})
@@ -105,6 +111,7 @@ describe('scroller', () => {
it('calls onUserScroll callback if 3 or more user scroll events are detected within 50ms', () => {
const container = getContainer()
const onUserScroll = sinon.spy()
scroller.setContainer(container, onUserScroll)
container.addEventListener.callArg(1)
clock.tick(15)
@@ -117,6 +124,7 @@ describe('scroller', () => {
it('does nothing if 50ms passes before 3 user scroll events', () => {
const container = getContainer()
const onUserScroll = sinon.spy()
scroller.setContainer(container, onUserScroll)
container.addEventListener.callArg(1)
container.addEventListener.callArg(1)
@@ -128,6 +136,7 @@ describe('scroller', () => {
it('does nothing for programmatic scroll events', () => {
const container = getContainer()
const onUserScroll = sinon.spy()
scroller.setContainer(container, onUserScroll)
scroller.scrollIntoView(getElement({ offsetTop: 600 }))
clock.tick(16)
+9
View File
@@ -35,6 +35,7 @@ describe('<Reporter />', () => {
it('initializes the events with the app state, runnables store, scroller, and stats store', () => {
const events = eventsStub()
const props = getProps({ events })
shallow(<Reporter {...props} />)
expect(events.init).to.have.been.calledWith({
appState: props.appState,
@@ -49,6 +50,7 @@ describe('<Reporter />', () => {
appState: { setAutoScrolling: sinon.spy() },
autoScrollingEnabled: false,
})
shallow(<Reporter {...props} />)
expect(props.appState.setAutoScrolling).to.have.been.calledWith(false)
})
@@ -56,31 +58,38 @@ describe('<Reporter />', () => {
it('tells events to listen to runner', () => {
const events = eventsStub()
const props = getProps({ events })
shallow(<Reporter {...props} />)
expect(events.listen).to.have.been.calledWith(props.runner)
})
it('renders with is-running class when running', () => {
const props = getProps()
props.appState.isRunning = true
const component = shallow(<Reporter {...props} />)
expect(component).to.have.className('is-running')
})
it('renders without is-running class when not running', () => {
const props = getProps()
props.appState.isRunning = false
const component = shallow(<Reporter {...props} />)
expect(component).not.to.have.className('is-running')
})
it('renders the header with the stats store', () => {
const component = shallow(<Reporter {...getProps()} />)
expect(component.find(Header)).to.have.prop('statsStore', statsStore)
})
it('renders the runnables with the error, runnables store, and spec path', () => {
const component = shallow(<Reporter {...getProps()} />)
expect(component.find(Runnables)).to.have.prop('error', error)
expect(component.find(Runnables)).to.have.prop('runnablesStore', runnablesStore)
expect(component.find(Runnables)).to.have.prop('specPath', 'the spec path')
+1
View File
@@ -62,4 +62,5 @@ const Routes = observer(({ model }) => (
))
export { Route, RoutesList }
export default Routes
@@ -25,32 +25,38 @@ const model = (props) => {
describe('<Routes />', () => {
it('renders without no-routes class if there are routes', () => {
const component = shallow(<Routes model={model()} />)
expect(component).not.to.have.className('no-routes')
})
it('renders with no-routes class if there are no routes', () => {
const component = shallow(<Routes model={model({ routes: [] })} />)
expect(component).to.have.className('no-routes')
})
it('renders collapsible header with number of routes', () => {
const component = shallow(<Routes model={model()} />)
expect(component.find('Collapsible')).to.have.prop('header', 'Routes (2)')
})
it('renders tooltip around number of routes table head item', () => {
const component = shallow(<Routes model={model()} />)
expect(component.find('th').last().find('Tooltip')).to.have.prop('title', 'Number of responses which matched this route')
})
context('<RoutesList />', () => {
it('is rendered', () => {
const component = shallow(<Routes model={model()} />)
expect(component.find(RoutesList).first()).to.exist
})
it('renders a <Route /> for each route in model', () => {
const component = shallow(<RoutesList model={model()} />)
expect(component.find(Route).length).to.equal(2)
})
})
@@ -58,51 +64,61 @@ describe('<Routes />', () => {
context('<Route />', () => {
it('renders without no-responses class if numResponses is non-zero', () => {
const component = shallow(<Route model={routeModel({ numResponses: 1 })} />)
expect(component).not.to.have.className('no-responses')
})
it('renders with no-responses class if zero numResponses', () => {
const component = shallow(<Route model={routeModel()} />)
expect(component).to.have.className('no-responses')
})
it('renders the method', () => {
const component = shallow(<Route model={routeModel()} />)
expect(component.find('td').first()).to.have.text('GET')
})
it('renders the url', () => {
const component = shallow(<Route model={routeModel()} />)
expect(component.find('td').at(1)).to.have.text('/posts$/')
})
it('renders isStubbed as Yes if stubbed', () => {
const component = shallow(<Route model={routeModel({ isStubbed: true })} />)
expect(component.find('td').at(2)).to.have.text('Yes')
})
it('renders isStubbed as No if not stubbed', () => {
const component = shallow(<Route model={routeModel({ isStubbed: false })} />)
expect(component.find('td').at(2)).to.have.text('No')
})
it('renders the alias', () => {
const component = shallow(<Route model={routeModel()} />)
expect(component.find('.route-alias')).to.have.text('getPosts')
})
it('renders a Tooltip around the alias', () => {
const component = shallow(<Route model={routeModel()} />)
expect(component.find('.route-alias').parent()).to.have.prop('title', 'Aliased this route as: \'getPosts\'')
})
it('renders the numResponses if non-zero', () => {
const component = shallow(<Route model={routeModel({ numResponses: 1 })} />)
expect(component.find('.response-count')).to.have.text('1')
})
it('renders the numResponses as "-" if zero', () => {
const component = shallow(<Route model={routeModel()} />)
expect(component.find('.response-count')).to.have.text('-')
})
})
@@ -54,4 +54,5 @@ class Runnable extends Component {
}
export { Suite }
export default Runnable

Some files were not shown because too many files have changed in this diff Show More