diff --git a/packages/launcher/package.json b/packages/launcher/package.json index 967f3db674ca..4ea0dc07f3b8 100644 --- a/packages/launcher/package.json +++ b/packages/launcher/package.json @@ -38,5 +38,5 @@ "files": [ "lib" ], - "types": "../ts/index.d.ts" + "types": "./lib/types.ts" } diff --git a/packages/server/__snapshots__/2_headless_spec.ts.js b/packages/server/__snapshots__/2_headless_spec.ts.js index 9b6842b06c35..8366dbbfcd5b 100644 --- a/packages/server/__snapshots__/2_headless_spec.ts.js +++ b/packages/server/__snapshots__/2_headless_spec.ts.js @@ -19,16 +19,18 @@ exports['e2e headless / tests in headless mode pass'] = ` e2e headless spec ✓ has the expected values for Cypress.browser + ✓ has expected HeadlessChrome useragent + ✓ has expected launch args - 1 passing + 3 passing (Results) ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ - │ Tests: 1 │ - │ Passing: 1 │ + │ Tests: 3 │ + │ Passing: 3 │ │ Failing: 0 │ │ Pending: 0 │ │ Skipped: 0 │ @@ -52,9 +54,9 @@ exports['e2e headless / tests in headless mode pass'] = ` Spec Tests Passing Failing Pending Skipped ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ - │ ✔ headless_spec.js XX:XX 1 1 - - - │ + │ ✔ headless_spec.js XX:XX 3 3 - - - │ └────────────────────────────────────────────────────────────────────────────────────────────────┘ - ✔ All specs passed! XX:XX 1 1 - - - + ✔ All specs passed! XX:XX 3 3 - - - ` @@ -80,16 +82,18 @@ exports['e2e headless / tests in headed mode pass [chrome]'] = ` e2e headless spec ✓ has the expected values for Cypress.browser + ✓ has expected HeadlessChrome useragent + ✓ has expected launch args - 1 passing + 3 passing (Results) ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ - │ Tests: 1 │ - │ Passing: 1 │ + │ Tests: 3 │ + │ Passing: 3 │ │ Failing: 0 │ │ Pending: 0 │ │ Skipped: 0 │ @@ -113,9 +117,9 @@ exports['e2e headless / tests in headed mode pass [chrome]'] = ` Spec Tests Passing Failing Pending Skipped ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ - │ ✔ headless_spec.js XX:XX 1 1 - - - │ + │ ✔ headless_spec.js XX:XX 3 3 - - - │ └────────────────────────────────────────────────────────────────────────────────────────────────┘ - ✔ All specs passed! XX:XX 1 1 - - - + ✔ All specs passed! XX:XX 3 3 - - - ` @@ -147,16 +151,18 @@ A video will not be recorded when using this mode. e2e headless spec ✓ has the expected values for Cypress.browser + ✓ has expected HeadlessChrome useragent + ✓ has expected launch args - 1 passing + 3 passing (Results) ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ - │ Tests: 1 │ - │ Passing: 1 │ + │ Tests: 3 │ + │ Passing: 3 │ │ Failing: 0 │ │ Pending: 0 │ │ Skipped: 0 │ @@ -174,9 +180,9 @@ A video will not be recorded when using this mode. Spec Tests Passing Failing Pending Skipped ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ - │ ✔ headless_spec.js XX:XX 1 1 - - - │ + │ ✔ headless_spec.js XX:XX 3 3 - - - │ └────────────────────────────────────────────────────────────────────────────────────────────────┘ - ✔ All specs passed! XX:XX 1 1 - - - + ✔ All specs passed! XX:XX 3 3 - - - ` diff --git a/packages/server/lib/browsers/chrome.js b/packages/server/lib/browsers/chrome.ts similarity index 87% rename from packages/server/lib/browsers/chrome.js rename to packages/server/lib/browsers/chrome.ts index 0713ad3feaf3..94243ffe7f56 100644 --- a/packages/server/lib/browsers/chrome.js +++ b/packages/server/lib/browsers/chrome.ts @@ -1,18 +1,30 @@ -const _ = require('lodash') -const os = require('os') -const path = require('path') -const Promise = require('bluebird') -const la = require('lazy-ass') -const check = require('check-more-types') -const extension = require('@packages/extension') -const debug = require('debug')('cypress:server:browsers:chrome') +import _ from 'lodash' +import os from 'os' +import path from 'path' +import Promise from 'bluebird' +import la from 'lazy-ass' +import check from 'check-more-types' +import extension from '@packages/extension' +import { FoundBrowser } from '@packages/launcher' +import debugModule from 'debug' +import fs from '../util/fs' +import appData from '../util/app_data' +import utils from './utils' +import protocol from './protocol' +import { CdpAutomation } from './cdp_automation' +import * as CriClient from './cri-client' + +// TODO: this is defined in `cypress-npm-api` but there is currently no way to get there +type CypressConfiguration = any + +type Browser = FoundBrowser & { + isHeadless: boolean + isHeaded: boolean +} + const plugins = require('../plugins') -const fs = require('../util/fs') -const appData = require('../util/app_data') -const utils = require('./utils') -const protocol = require('./protocol') -const { CdpAutomation } = require('./cdp_automation') -const CriClient = require('./cri-client') + +const debug = debugModule('cypress:server:browsers:chrome') const LOAD_EXTENSION = '--load-extension=' const CHROME_VERSIONS_WITH_BUGGY_ROOT_LAYER_SCROLLING = '66 67'.split(' ') @@ -118,7 +130,19 @@ const pluginsBeforeBrowserLaunch = function (browser, args) { }) } -const _normalizeArgExtensions = function (dest, args) { +/** + * Merge the different `--load-extension` arguments into one. + * + * @param extPath path to Cypress extension + * @param args all browser args + * @param browser the current browser being launched + * @returns the modified list of arguments + */ +const _normalizeArgExtensions = function (extPath, args, browser: Browser): string[] { + if (browser.isHeadless) { + return args + } + let userExtensions const loadExtension = _.find(args, (arg) => { return arg.includes(LOAD_EXTENSION) @@ -131,7 +155,7 @@ const _normalizeArgExtensions = function (dest, args) { userExtensions = loadExtension.replace(LOAD_EXTENSION, '').split(',') } - const extensions = [].concat(userExtensions, dest, pathToTheme) + const extensions = [].concat(userExtensions, extPath, pathToTheme) args.push(LOAD_EXTENSION + _.compact(extensions).join(',')) @@ -171,6 +195,7 @@ const _disableRestorePagesPrompt = function (userDir) { // After the browser has been opened, we can connect to // its remote interface via a websocket. const _connectToChromeRemoteInterface = function (port) { + // @ts-ignore la(check.userPort(port), 'expected port number to connect CRI to', port) debug('connecting to Chrome remote interface at random port %d', port) @@ -206,6 +231,7 @@ const _maybeRecordVideo = (options) => { // a utility function that navigates to the given URL // once Chrome remote interface client is passed to it. const _navigateUsingCRI = function (url) { + // @ts-ignore la(check.url(url), 'missing url to navigate to', url) return function (client) { @@ -247,7 +273,7 @@ module.exports = { _setAutomation, - _writeExtension (browser, options) { + _writeExtension (browser: Browser, options) { if (browser.isHeadless) { debug('chrome is running headlessly, not installing extension') @@ -269,14 +295,14 @@ module.exports = { }) }, - _getArgs (options = {}) { + _getArgs (options: CypressConfiguration = {}) { let ps; let ua _.defaults(options, { browser: {}, }) - const args = [].concat(defaultArgs) + const args = ([] as string[]).concat(defaultArgs) if (os.platform() === 'linux') { args.push('--disable-gpu') @@ -316,14 +342,10 @@ module.exports = { args.push('--proxy-bypass-list=<-loopback>') } - if (options.isHeadless) { - args.push('--headless') - } - return args }, - open (browser, url, options = {}, automation) { + open (browser: Browser, url, options: CypressConfiguration = {}, automation) { const { isTextTerminal } = options const userDir = utils.getProfileDir(browser, isTextTerminal) @@ -332,6 +354,10 @@ module.exports = { .try(() => { const args = this._getArgs(options) + if (browser.isHeadless) { + args.push('--headless') + } + return getRemoteDebuggingPort() .then((port) => { args.push(`--remote-debugging-port=${port}`) @@ -344,7 +370,7 @@ module.exports = { port, ]) }) - }).spread((cacheDir, args, port) => { + }).spread((cacheDir, args: string[], port) => { return Promise.all([ this._writeExtension( browser, @@ -356,7 +382,7 @@ module.exports = { .spread((extDest) => { // normalize the --load-extensions argument by // massaging what the user passed into our own - args = _normalizeArgExtensions(extDest, args) + args = _normalizeArgExtensions(extDest, args, browser) // this overrides any previous user-data-dir args // by being the last one diff --git a/packages/server/lib/browsers/electron.coffee b/packages/server/lib/browsers/electron.coffee index e575ead73db6..b54ffc8a01cf 100644 --- a/packages/server/lib/browsers/electron.coffee +++ b/packages/server/lib/browsers/electron.coffee @@ -157,7 +157,7 @@ module.exports = { debug('debugger: received response to %s: %o', message, res) res .catch (err) -> - debug('debugger: received error on %s: %o', messsage, err) + debug('debugger: received error on %s: %o', message, err) throw err webContents.debugger.sendCommand('Browser.getVersion') diff --git a/packages/server/test/e2e/2_headless_spec.ts b/packages/server/test/e2e/2_headless_spec.ts index 1331147c2ded..48283a55f9d2 100644 --- a/packages/server/test/e2e/2_headless_spec.ts +++ b/packages/server/test/e2e/2_headless_spec.ts @@ -8,6 +8,7 @@ describe('e2e headless', function () { spec: 'headless_spec.js', config: { env: { + 'CI': process.env.CI, 'EXPECT_HEADLESS': '1', }, }, @@ -19,6 +20,11 @@ describe('e2e headless', function () { // cypress run --headed e2e.it('tests in headed mode pass', { spec: 'headless_spec.js', + config: { + env: { + 'CI': process.env.CI, + }, + }, expectedExitCode: 0, headed: true, snapshot: true, diff --git a/packages/server/test/support/fixtures/projects/e2e/cypress/integration/headless_spec.js b/packages/server/test/support/fixtures/projects/e2e/cypress/integration/headless_spec.js index 493955ef5a8a..9b29adb091d2 100644 --- a/packages/server/test/support/fixtures/projects/e2e/cypress/integration/headless_spec.js +++ b/packages/server/test/support/fixtures/projects/e2e/cypress/integration/headless_spec.js @@ -5,4 +5,22 @@ describe('e2e headless spec', function () { expect(Cypress.browser.isHeadless).to.eq(expectedHeadless) expect(Cypress.browser.isHeaded).to.eq(!expectedHeadless) }) + + it('has expected HeadlessChrome useragent', function () { + if (Cypress.browser.family !== 'chrome') { + return + } + + cy.wrap(navigator.userAgent) + .should(expectedHeadless ? 'contain' : 'not.contain', 'HeadlessChrome') + }) + + it('has expected launch args', function () { + if (Cypress.browser.family !== 'chrome') { + return + } + + cy.task('get:browser:args') + .should(expectedHeadless ? 'contain' : 'not.contain', '--headless') + }) }) diff --git a/packages/server/test/support/fixtures/projects/e2e/cypress/plugins/index.js b/packages/server/test/support/fixtures/projects/e2e/cypress/plugins/index.js index ea2e74099853..3a4d774fe2f4 100644 --- a/packages/server/test/support/fixtures/projects/e2e/cypress/plugins/index.js +++ b/packages/server/test/support/fixtures/projects/e2e/cypress/plugins/index.js @@ -10,6 +10,9 @@ module.exports = (on) => { // save some time by only reading the originals once let cache = {} + const screenshotsTaken = [] + let browserArgs = null + function getCachedImage (name) { const cachedImage = cache[name] @@ -24,6 +27,16 @@ module.exports = (on) => { }) } + on('after:screenshot', (details) => { + screenshotsTaken.push(details) + }) + + on('before:browser:launch', (browser, args) => { + browserArgs = args + + return args + }) + on('task', { 'returns:undefined' () {}, @@ -122,5 +135,13 @@ module.exports = (on) => { return performance.track('fast_visit_spec percentiles', data) .return(null) }, + + 'get:screenshots:taken' () { + return screenshotsTaken + }, + + 'get:browser:args' () { + return browserArgs + }, }) } diff --git a/packages/server/test/unit/browsers/chrome_spec.coffee b/packages/server/test/unit/browsers/chrome_spec.coffee index 7e000f4b54d7..7e23ac012934 100644 --- a/packages/server/test/unit/browsers/chrome_spec.coffee +++ b/packages/server/test/unit/browsers/chrome_spec.coffee @@ -70,16 +70,17 @@ describe "lib/browsers/chrome", -> it "does not load extension in headless mode", -> plugins.has.returns(false) + chrome._writeExtension.restore() pathToTheme = extension.getPathToTheme() - chrome.open("chrome", "http://", { isHeadless: true, isHeaded: false }, @automation) + chrome.open({ isHeadless: true, isHeaded: false }, "http://", {}, @automation) .then => args = utils.launch.firstCall.args[2] expect(args).to.deep.eq([ + "--headless" "--remote-debugging-port=50505" - "--load-extension=/path/to/ext,#{pathToTheme}" "--user-data-dir=/profile/dir" "--disk-cache-dir=/profile/dir/CypressCache" ])