Chrome headless (#5676)

* always disable xvfb

* add --headless

* Revert "always disable xvfb" - it is still needed for Electron

This reverts commit 058679f4ce.

* updates

* update 3_plugins_spec

* update wording now that chrome can be headless

* fix video recording when headless

* Don't assume that chrome is headed

* update electron video recording message

* Add 2_headless_spec for Cypress.browser values

* update headless language

* still use headless by default for electron and headed for chrome

* fix up cli

* fix e2e tests

* update npm api types

* fix 2_headless_spec

* keep alphabetical ordering

* increase binary size limit

* add a comment on the cli error impl

* use _.defaults for default for headed

* fix

* _.defaults mutates
This commit is contained in:
Zach Bloomquist
2019-12-12 12:26:32 -05:00
committed by GitHub
parent 3fadac0909
commit 35109fb08c
20 changed files with 342 additions and 32 deletions

View File

@@ -63,7 +63,8 @@ exports['shows help for run --foo 1'] = `
-e, --env <env> sets environment variables. separate multiple values with a comma. overrides any value in cypress.json or cypress.env.json
--group <name> a named group for recorded runs in the Cypress Dashboard
-k, --key <record-key> your secret Record Key. you can omit this if you set a CYPRESS_RECORD_KEY environment variable.
--headed displays the Electron browser instead of running headlessly
--headed displays the browser instead of running headlessly (defaults to true for Chrome-family browsers)
--headless hide the browser instead of running headed (defaults to true for Electron)
--no-exit keep the browser open after tests finish
--parallel enables concurrent runs and automatic load balancing of specs across multiple machines or processes
-p, --port <port> runs Cypress on a specific port. overrides any value in cypress.json.

View File

@@ -32,6 +32,7 @@ exports['errors individual has the following errors 1'] = [
"childProcessKilled",
"failedDownload",
"failedUnzip",
"incompatibleHeadlessFlags",
"invalidCacheDirectory",
"invalidCypressEnv",
"invalidSmokeTestDisplayError",

View File

@@ -2,7 +2,7 @@ const _ = require('lodash')
const commander = require('commander')
const { stripIndent } = require('common-tags')
const logSymbols = require('log-symbols')
const debug = require('debug')('cypress:cli')
const debug = require('debug')('cypress:cli:cli')
const util = require('./util')
const logger = require('./logger')
const errors = require('./errors')
@@ -82,6 +82,8 @@ const parseVariableOpts = (fnArgs, args) => {
})
}
debug('variable-length opts parsed %o', { args, opts })
return util.parseOpts(opts)
}
@@ -101,7 +103,8 @@ const descriptions = {
forceInstall: 'force install the Cypress binary',
global: 'force Cypress into global mode as if its globally installed',
group: 'a named group for recorded runs in the Cypress Dashboard',
headed: 'displays the Electron browser instead of running headlessly',
headed: 'displays the browser instead of running headlessly (defaults to true for Chrome-family browsers)',
headless: 'hide the browser instead of running headed (defaults to true for Electron)',
key: 'your secret Record Key. you can omit this if you set a CYPRESS_RECORD_KEY environment variable.',
parallel: 'enables concurrent runs and automatic load balancing of specs across multiple machines or processes',
port: 'runs Cypress on a specific port. overrides any value in cypress.json.',
@@ -204,6 +207,7 @@ module.exports = {
.option('--group <name>', text('group'))
.option('-k, --key <record-key>', text('key'))
.option('--headed', text('headed'))
.option('--headless', text('headless'))
.option('--no-exit', text('exit'))
.option('--parallel', text('parallel'))
.option('-p, --port <port>', text('port'))
@@ -215,7 +219,7 @@ module.exports = {
.option('-t, --tag <tag>', text('tag'))
.option('--dev', text('dev'), coerceFalse)
.action((...fnArgs) => {
debug('running Cypress')
debug('running Cypress with args %o', fnArgs)
require('./exec/run')
.start(parseVariableOpts(fnArgs, args))
.then(util.exit)

View File

@@ -168,6 +168,11 @@ const versionMismatch = {
solution: 'Install Cypress and verify app again',
}
const incompatibleHeadlessFlags = {
description: '`--headed` and `--headless` cannot both be passed.',
solution: 'Either pass `--headed` or `--headless`, but not both.',
}
const solutionUnknown = stripIndent`
Please search Cypress documentation for possible solutions:
@@ -412,5 +417,6 @@ module.exports = {
CYPRESS_RUN_BINARY,
smokeTestFailure,
childProcessKilled,
incompatibleHeadlessFlags,
},
}

View File

@@ -1,14 +1,15 @@
const _ = require('lodash')
const debug = require('debug')('cypress:cli')
const debug = require('debug')('cypress:cli:run')
const util = require('../util')
const spawn = require('./spawn')
const verify = require('../tasks/verify')
const { exitWithError, errors } = require('../errors')
// maps options collected by the CLI
// and forms list of CLI arguments to the server
const processRunOptions = (options = {}) => {
debug('processing run options')
debug('processing run options %o', options)
const args = ['--run-project', options.project]
@@ -52,6 +53,19 @@ const processRunOptions = (options = {}) => {
args.push('--headed', options.headed)
}
if (options.headless) {
if (options.headed) {
// throw this error synchronously, it will be caught later on and
// the details will be propagated to the promise chain
const err = new Error()
err.details = errors.incompatibleHeadlessFlags
throw err
}
args.push('--headed', !options.headless)
}
// if key is set use that - else attempt to find it by environment variable
if (options.key == null) {
debug('--key is not set, looking up environment variable CYPRESS_RECORD_KEY')
@@ -116,7 +130,17 @@ module.exports = {
})
function run () {
const args = processRunOptions(options)
let args
try {
args = processRunOptions(options)
} catch (err) {
if (err.details) {
return exitWithError(err.details)()
}
throw err
}
debug('run to spawn.start args %j', args)

View File

@@ -196,6 +196,7 @@ const parseOpts = (opts) => {
'global',
'group',
'headed',
'headless',
'key',
'path',
'parallel',

View File

@@ -1,5 +1,6 @@
require('../../spec_helper')
const os = require('os')
const snapshot = require('../../support/snapshot')
const util = require(`${lib}/util`)
@@ -38,6 +39,19 @@ describe('exec run', function () {
snapshot(args)
})
it('does not allow setting paradoxical --headed and --headless flags', () => {
os.platform.returns('linux')
process.exit.returns()
expect(() => run.processRunOptions({ headed: true, headless: true })).to.throw()
})
it('passes --headed according to --headless', () => {
expect(run.processRunOptions({ headless: true })).to.deep.eq([
'--run-project', undefined, '--headed', false,
])
})
it('does not remove --record option when using --browser', () => {
const args = run.processRunOptions({
record: 'foo',

View File

@@ -238,9 +238,13 @@ declare module 'cypress' {
*/
group: string
/**
* Display the Electron browser instead of running headlessly
* Display the browser instead of running headlessly
*/
headed: boolean
/**
* Hide the browser instead of running headed
*/
headless: boolean
/**
* Specify your secret record key
*/

View File

@@ -406,7 +406,7 @@ describe('Project Nav', function () {
describe('browser with info', function () {
beforeEach(function () {
this.info = 'The Electron browser is the version of Chrome that is bundled with Electron. Cypress uses this browser when running headlessly, so it may be useful for debugging issues that occur only in headless mode.'
this.info = 'foo info bar'
this.config.browsers = [{
name: 'electron',
family: 'electron',

View File

@@ -0,0 +1,182 @@
exports['e2e headless / tests in headless mode pass'] = `
====================================================================================================
(Run Starting)
┌────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Cypress: 1.2.3 │
│ Browser: FooBrowser 88 │
│ Specs: 1 found (headless_spec.js) │
│ Searched: cypress/integration/headless_spec.js │
└────────────────────────────────────────────────────────────────────────────────────────────────┘
────────────────────────────────────────────────────────────────────────────────────────────────────
Running: headless_spec.js (1 of 1)
e2e headless spec
✓ has the expected values for Cypress.browser
1 passing
(Results)
┌────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Tests: 1 │
│ Passing: 1 │
│ Failing: 0 │
│ Pending: 0 │
│ Skipped: 0 │
│ Screenshots: 0 │
│ Video: true │
│ Duration: X seconds │
│ Spec Ran: headless_spec.js │
└────────────────────────────────────────────────────────────────────────────────────────────────┘
(Video)
- Started processing: Compressing to 32 CRF
- Finished processing: /XXX/XXX/XXX/cypress/videos/headless_spec.js.mp4 (X second)
====================================================================================================
(Run Finished)
Spec Tests Passing Failing Pending Skipped
┌────────────────────────────────────────────────────────────────────────────────────────────────┐
│ ✔ headless_spec.js XX:XX 1 1 - - - │
└────────────────────────────────────────────────────────────────────────────────────────────────┘
✔ All specs passed! XX:XX 1 1 - - -
`
exports['e2e headless / tests in headed mode pass [chrome]'] = `
====================================================================================================
(Run Starting)
┌────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Cypress: 1.2.3 │
│ Browser: FooBrowser 88 │
│ Specs: 1 found (headless_spec.js) │
│ Searched: cypress/integration/headless_spec.js │
└────────────────────────────────────────────────────────────────────────────────────────────────┘
────────────────────────────────────────────────────────────────────────────────────────────────────
Running: headless_spec.js (1 of 1)
e2e headless spec
✓ has the expected values for Cypress.browser
1 passing
(Results)
┌────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Tests: 1 │
│ Passing: 1 │
│ Failing: 0 │
│ Pending: 0 │
│ Skipped: 0 │
│ Screenshots: 0 │
│ Video: true │
│ Duration: X seconds │
│ Spec Ran: headless_spec.js │
└────────────────────────────────────────────────────────────────────────────────────────────────┘
(Video)
- Started processing: Compressing to 32 CRF
- Finished processing: /XXX/XXX/XXX/cypress/videos/headless_spec.js.mp4 (X second)
====================================================================================================
(Run Finished)
Spec Tests Passing Failing Pending Skipped
┌────────────────────────────────────────────────────────────────────────────────────────────────┐
│ ✔ headless_spec.js XX:XX 1 1 - - - │
└────────────────────────────────────────────────────────────────────────────────────────────────┘
✔ All specs passed! XX:XX 1 1 - - -
`
exports['e2e headless / tests in headed mode pass [electron]'] = `
====================================================================================================
(Run Starting)
┌────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Cypress: 1.2.3 │
│ Browser: FooBrowser 88 │
│ Specs: 1 found (headless_spec.js) │
│ Searched: cypress/integration/headless_spec.js │
└────────────────────────────────────────────────────────────────────────────────────────────────┘
────────────────────────────────────────────────────────────────────────────────────────────────────
Running: headless_spec.js (1 of 1)
Warning: Cypress can only record videos of Electron when running headlessly.
You have set the Electron browser to run headed.
A video will not be recorded when using this mode.
e2e headless spec
✓ has the expected values for Cypress.browser
1 passing
(Results)
┌────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Tests: 1 │
│ Passing: 1 │
│ Failing: 0 │
│ Pending: 0 │
│ Skipped: 0 │
│ Screenshots: 0 │
│ Video: false │
│ Duration: X seconds │
│ Spec Ran: headless_spec.js │
└────────────────────────────────────────────────────────────────────────────────────────────────┘
====================================================================================================
(Run Finished)
Spec Tests Passing Failing Pending Skipped
┌────────────────────────────────────────────────────────────────────────────────────────────────┐
│ ✔ headless_spec.js XX:XX 1 1 - - - │
└────────────────────────────────────────────────────────────────────────────────────────────────┘
✔ All specs passed! XX:XX 1 1 - - -
`

View File

@@ -275,9 +275,9 @@ exports['e2e stdout logs that electron cannot be recorded in headed mode 1'] = `
Running: simple_spec.coffee (1 of 1)
Warning: Cypress can only record videos when running headlessly.
Warning: Cypress can only record videos of Electron when running headlessly.
You have set the 'electron' browser to run headed.
You have set the Electron browser to run headed.
A video will not be recorded when using this mode.

View File

@@ -247,16 +247,22 @@ module.exports = {
_setAutomation,
_writeExtension (browser, isTextTerminal, proxyUrl, socketIoRoute) {
_writeExtension (browser, options) {
if (browser.isHeadless) {
debug('chrome is running headlessly, not installing extension')
return
}
// get the string bytes for the final extension file
return extension.setHostAndPath(proxyUrl, socketIoRoute)
.then((str) => {
const extensionDest = utils.getExtensionDir(browser, isTextTerminal)
const extensionBg = path.join(extensionDest, 'background.js')
return extension.setHostAndPath(options.proxyUrl, options.socketIoRoute).then(function (str) {
let extensionBg; let extensionDest
extensionDest = utils.getExtensionDir(browser, options.isTextTerminal)
extensionBg = path.join(extensionDest, 'background.js')
// copy the extension src to the extension dist
return utils.copyExtension(pathToExtension, extensionDest)
.then(() => {
return utils.copyExtension(pathToExtension, extensionDest).then(function () {
// and overwrite background.js with the final string bytes
return fs.writeFileAsync(extensionBg, str)
}).return(extensionDest)
@@ -310,6 +316,10 @@ module.exports = {
args.push('--proxy-bypass-list=<-loopback>')
}
if (options.isHeadless) {
args.push('--headless')
}
return args
},
@@ -338,9 +348,7 @@ module.exports = {
return Promise.all([
this._writeExtension(
browser,
isTextTerminal,
options.proxyUrl,
options.socketIoRoute
options
),
_removeRootExtension(),
_disableRestorePagesPrompt(userDir),

View File

@@ -113,7 +113,7 @@ module.exports = {
version,
path: '',
majorVersion,
info: 'Electron is the default browser that comes with Cypress. This is the browser that runs in headless mode. Selecting this browser is useful when debugging. The version number indicates the underlying Chromium version that Electron uses.',
info: 'Electron is the default browser that comes with Cypress. This is the default browser that runs in headless mode. Selecting this browser is useful when debugging. The version number indicates the underlying Chromium version that Electron uses.',
}
// the internal version of Electron, which won't be detected by `launcher`

View File

@@ -110,15 +110,15 @@ getMsgByType = (type, arg1 = {}, arg2) ->
return {msg: msg, details: arg2}
when "CANNOT_RECORD_VIDEO_HEADED"
"""
Warning: Cypress can only record videos when running headlessly.
Warning: Cypress can only record videos of Electron when running headlessly.
You have set the 'electron' browser to run headed.
You have set the Electron browser to run headed.
A video will not be recorded when using this mode.
"""
when "CANNOT_RECORD_VIDEO_FOR_THIS_BROWSER"
"""
Warning: Cypress can only record videos when using the built in 'electron' browser.
Warning: Cypress can only record videos when using an Electron or Chrome-family browser.
You have set the browser to: '#{arg1}'

View File

@@ -472,7 +472,7 @@ const getChromeProps = (isHeaded, project, writeVideoFrame) => {
return _
.chain({})
.tap((props) => {
if (isHeaded && writeVideoFrame) {
if (writeVideoFrame) {
props.screencastFrame = (e) => {
// https://chromedevtools.github.io/devtools-protocol/tot/Page#event-screencastFrame
writeVideoFrame(Buffer.from(e.data, 'base64'))
@@ -620,11 +620,12 @@ const trashAssets = Promise.method((config = {}) => {
// if we've been told to record and we're not spawning a headed browser
const browserCanBeRecorded = (browser) => {
// TODO: enable recording Electron in headed mode too
if (browser.family === 'electron' && browser.isHeadless) {
return true
}
if (browser.family === 'chrome' && browser.isHeaded) {
if (browser.family === 'chrome') {
return true
}
@@ -1116,9 +1117,14 @@ module.exports = {
},
runSpecs (options = {}) {
_.defaults(options, {
// only non-Electron browsers run headed by default
headed: options.browser.family !== 'electron',
})
const { config, browser, sys, headed, outputPath, specs, specPattern, beforeSpecRun, afterSpecRun, runUrl, parallel, group, tag } = options
const isHeadless = browser.family === 'electron' && !headed
const isHeadless = !headed
browser.isHeadless = isHeadless
browser.isHeaded = !isHeadless

View File

@@ -0,0 +1,29 @@
const e2e = require('../support/helpers/e2e')
describe('e2e headless', function () {
e2e.setup()
// cypress run
e2e.it('tests in headless mode pass', {
spec: 'headless_spec.js',
config: {
env: {
'EXPECT_HEADLESS': '1',
},
},
expectedExitCode: 0,
headed: false,
snapshot: true,
})
// cypress run --headed
e2e.it('tests in headed mode pass', {
spec: 'headless_spec.js',
expectedExitCode: 0,
headed: true,
snapshot: true,
// currently, Electron differs because it displays a
// "can not record video in headed mode" error
useSeparateBrowserSnapshots: true,
})
})

View File

@@ -86,6 +86,7 @@ describe "e2e plugins", ->
e2e.it "works with user extensions", {
browser: "chrome"
spec: "app_spec.coffee"
headed: true
project: pluginExtension
sanitizeScreenshotDimensions: true
snapshot: true

View File

@@ -0,0 +1,8 @@
const expectedHeadless = !!Cypress.env('EXPECT_HEADLESS')
describe('e2e headless spec', function () {
it('has the expected values for Cypress.browser', function () {
expect(Cypress.browser.isHeadless).to.eq(expectedHeadless)
expect(Cypress.browser.isHeaded).to.eq(!expectedHeadless)
})
})

View File

@@ -258,6 +258,7 @@ const localItFn = function (title, options = {}) {
skip: false,
browser: process.env.BROWSER,
generateTestsForDefaultBrowsers: true,
useSeparateBrowserSnapshots: false,
onRun (execFn, browser, ctx) {
return execFn()
},
@@ -289,6 +290,10 @@ const localItFn = function (title, options = {}) {
const testTitle = `${title} [${browser}]`
return mochaItFn(testTitle, function () {
if (options.useSeparateBrowserSnapshots) {
title = testTitle
}
const originalTitle = this.test.parent.titlePath().concat(title).join(' / ')
const ctx = this
@@ -472,8 +477,8 @@ module.exports = (e2e = {
args.push(`--port=${options.port}`)
}
if (options.headed) {
args.push('--headed')
if (!_.isUndefined(options.headed)) {
args.push('--headed', options.headed)
}
if (options.record) {
@@ -594,9 +599,9 @@ module.exports = (e2e = {
expect(parseFloat(version)).to.be.a.number
// if we are in headed mode or in a browser other
// if we are in headed mode or headed is undefined in a browser other
// than electron
if (options.headed || (browser && (browser !== 'electron'))) {
if (options.headed || (_.isUndefined(options.headed) && browser && browser !== 'electron')) {
expect(headless).not.to.exist
} else {
expect(headless).to.include('(headless)')

View File

@@ -68,6 +68,22 @@ describe "lib/browsers/chrome", ->
# we load the browser with blank page first
expect(utils.launch).to.be.calledWith("chrome", "about:blank", @args)
it "does not load extension in headless mode", ->
plugins.has.returns(false)
pathToTheme = extension.getPathToTheme()
chrome.open("chrome", "http://", { isHeadless: true, isHeaded: false }, @automation)
.then =>
args = utils.launch.firstCall.args[2]
expect(args).to.deep.eq([
"--remote-debugging-port=50505"
"--load-extension=/path/to/ext,#{pathToTheme}"
"--user-data-dir=/profile/dir"
"--disk-cache-dir=/profile/dir/CypressCache"
])
it "normalizes --load-extension if provided in plugin", ->
plugins.has.returns(true)
plugins.execute.resolves([