mirror of
https://github.com/cypress-io/cypress.git
synced 2026-05-08 07:50:24 -05:00
secure cookie error crash (#2685)
- fixes #1264 - fixes #1321 - fixes #1799 - fixes #2689 - fixes #2688 - fixes #2687 - fixes #2686
This commit is contained in:
@@ -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
@@ -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
|
||||
|
||||
Vendored
+32
@@ -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,
|
||||
},
|
||||
]
|
||||
}
|
||||
Vendored
+29
@@ -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}"
|
||||
}
|
||||
]
|
||||
}
|
||||
Vendored
+56
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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'),
|
||||
],
|
||||
}
|
||||
@@ -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)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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
@@ -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 = {
|
||||
|
||||
@@ -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()
|
||||
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ const util = require('../util')
|
||||
|
||||
const path = () => {
|
||||
logger.log(state.getCacheDir())
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
},
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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')
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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 })
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"exclude": [
|
||||
"**/.git/**",
|
||||
"**/.cache/**",
|
||||
"**/.history/**",
|
||||
"**/.projects/**",
|
||||
"**/.publish/**",
|
||||
"**/node_modules/**",
|
||||
"**/app/**",
|
||||
"**/build/**",
|
||||
"**/dist/**",
|
||||
"**/dist-test/**",
|
||||
"**/.cy/**"
|
||||
]
|
||||
}
|
||||
+16
-3
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,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)
|
||||
|
||||
@@ -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)
|
||||
},
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ const Link = ({ children, to, onClick }) => {
|
||||
const navigate = (e) => {
|
||||
e.preventDefault()
|
||||
if (onClick) onClick()
|
||||
|
||||
to.navigate()
|
||||
}
|
||||
|
||||
|
||||
@@ -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 () {
|
||||
|
||||
@@ -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----------//
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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,3 +1,3 @@
|
||||
import * as debug from 'debug'
|
||||
import debug from 'debug'
|
||||
|
||||
export const log = debug('cypress:launcher')
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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' })
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
})
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user