diff --git a/packages/desktop-gui/cypress/integration/project_nav_spec.coffee b/packages/desktop-gui/cypress/integration/project_nav_spec.coffee index 790e9b969e90..52fafe589002 100644 --- a/packages/desktop-gui/cypress/integration/project_nav_spec.coffee +++ b/packages/desktop-gui/cypress/integration/project_nav_spec.coffee @@ -151,7 +151,7 @@ describe "Project Nav", -> it "sends the required parameters to launch a browser", -> browserArg = @ipc.launchBrowser.getCall(0).args[0].browser expect(browserArg).to.have.keys([ - "family", "name", "path", "version", "majorVersion", "displayName", "info", "isChosen", "custom" + "family", "name", "path", "version", "majorVersion", "displayName", "info", "isChosen", "custom", "warning" ]) expect(browserArg.path).to.include('/') expect(browserArg.family).to.equal('chrome') @@ -232,6 +232,30 @@ describe "Project Nav", -> cy.get(".browsers-list") .find(".dropdown-toggle").should("not.be.visible") + describe "browser has a warning attached", -> + beforeEach -> + @browsers = [{ + "name": "chromium", + "displayName": "Chromium", + "family": "chrome", + "version": "49.0.2609.0", + "path": "/Users/bmann/Downloads/chrome-mac/Chromium.app/Contents/MacOS/Chromium", + "majorVersion": "49", + "warning": "Cypress detected policy settings on your computer that may cause issues with using this browser. For more information, see https://on.cypress.io/bad-browser-policy" + }] + + @config.browsers = @browsers + @openProject.resolve(@config) + + it "shows warning icon with linkified tooltip", -> + cy.get(".browsers .fa-exclamation-triangle").trigger("mouseover") + cy.get(".cy-tooltip") + .should("contain", "Cypress detected policy settings on your computer that may cause issues with using this browser. For more information, see") + .get(".cy-tooltip a") + .click() + .then () -> + expect(@ipc.externalOpen).to.be.calledWith("https://on.cypress.io/bad-browser-policy") + describe "custom browser available", -> beforeEach -> @config.browsers.push({ @@ -274,7 +298,6 @@ describe "Project Nav", -> it "shows info icon with tooltip", -> cy.get(".browsers .fa-info-circle") - .then ($el) -> - $el[0].dispatchEvent(new Event("mouseover", {bubbles: true})) + .trigger("mouseover") cy.get(".cy-tooltip") .should("contain", @info) diff --git a/packages/desktop-gui/cypress/integration/warning_message_spec.coffee b/packages/desktop-gui/cypress/integration/warning_message_spec.coffee index afbc4f3f767d..9cd1d1be6a56 100644 --- a/packages/desktop-gui/cypress/integration/warning_message_spec.coffee +++ b/packages/desktop-gui/cypress/integration/warning_message_spec.coffee @@ -80,3 +80,41 @@ describe "WarningMessage", -> .click() .then -> expect(@ipc.externalOpen).not.to.be.called + + context "with multiple warnings", -> + beforeEach -> + @warningObj2 = {type: "GOOD_BUT_NOT_TOO_GOOD", name: "Fairly good warning", message: "Other message"} + + it "shows multiple warnings", -> + cy.shouldBeOnProjectSpecs().then => + @ipc.onProjectWarning.yield(null, @warningObj) + @ipc.onProjectWarning.yield(null, @warningObj2) + + cy.get(".alert-warning") + .should("have.length", 2) + .should("be.visible") + .first() + .should("contain", "Some warning") + cy.get(".alert-warning") + .its('1') + .should("contain", "Other message") + + it "can dismiss the warnings", -> + cy.shouldBeOnProjectSpecs().then => + @ipc.onProjectWarning.yield(null, @warningObj) + @ipc.onProjectWarning.yield(null, @warningObj2) + + cy.get(".alert-warning") + .should("contain", "Some warning") + .should("contain", "Other message") + .get(".alert-warning button") + .first() + .click() + cy.get(".alert-warning") + .should("not.contain", "Some warning") + .should("contain", "Other message") + .get(".alert-warning button") + .click() + cy.get(".alert-warning") + .should("not.contain", "Some warning") + .should("not.contain", "Other message") diff --git a/packages/desktop-gui/package.json b/packages/desktop-gui/package.json index a99b87cef574..6ab86b198ce4 100644 --- a/packages/desktop-gui/package.json +++ b/packages/desktop-gui/package.json @@ -26,7 +26,7 @@ "devDependencies": { "@babel/plugin-proposal-object-rest-spread": "7.4.4", "@cypress/icons": "0.7.0", - "@cypress/json-schemas": "5.32.1", + "@cypress/json-schemas": "5.31.3", "@cypress/react-tooltip": "0.5.0", "bin-up": "1.2.0", "bluebird": "3.5.3", diff --git a/packages/desktop-gui/src/app/nav.scss b/packages/desktop-gui/src/app/nav.scss index 9cb1e7f2aef0..c06b434164b5 100644 --- a/packages/desktop-gui/src/app/nav.scss +++ b/packages/desktop-gui/src/app/nav.scss @@ -135,6 +135,12 @@ margin-right: 4px; } +.nav .browser-warning { + color: $red-primary; + margin-left: 6px; + margin-right: 4px; +} + .browser-info-tooltip { background: #ececec; border-color: #c7c7c7; diff --git a/packages/desktop-gui/src/lib/browser-model.js b/packages/desktop-gui/src/lib/browser-model.js index d885d0136eb6..e830432d3444 100644 --- a/packages/desktop-gui/src/lib/browser-model.js +++ b/packages/desktop-gui/src/lib/browser-model.js @@ -9,6 +9,7 @@ export default class Browser { @observable majorVersion @observable info @observable custom + @observable warning @observable isChosen = false constructor (browser) { @@ -20,6 +21,7 @@ export default class Browser { this.majorVersion = browser.majorVersion this.info = browser.info this.custom = browser.custom + this.warning = browser.warning } @computed get icon () { diff --git a/packages/desktop-gui/src/lib/markdown-renderer.jsx b/packages/desktop-gui/src/lib/markdown-renderer.jsx new file mode 100644 index 000000000000..2cac4f4d548a --- /dev/null +++ b/packages/desktop-gui/src/lib/markdown-renderer.jsx @@ -0,0 +1,37 @@ +import React from 'react' +import Markdown from 'markdown-it' + +import ipc from '../lib/ipc' + +const md = new Markdown({ + html: true, + linkify: true, +}) + +export default class MarkdownRenderer extends React.PureComponent { + componentDidMount () { + this.node.addEventListener('click', this._clickHandler) + } + + componentWillUnmount () { + this.node.removeEventListener('click', this._clickHandler) + } + + _clickHandler (e) { + if (e.target.href) { + e.preventDefault() + + return ipc.externalOpen(e.target.href) + } + } + + render () { + return ( + this.node = node} + dangerouslySetInnerHTML={{ + __html: md.render(this.props.markdown), + }}> + + ) + } +} diff --git a/packages/desktop-gui/src/project-nav/browsers.jsx b/packages/desktop-gui/src/project-nav/browsers.jsx index 77a95c6c39bd..6c3abed08e93 100644 --- a/packages/desktop-gui/src/project-nav/browsers.jsx +++ b/packages/desktop-gui/src/project-nav/browsers.jsx @@ -2,6 +2,7 @@ import React, { Component } from 'react' import { observer } from 'mobx-react' import Tooltip from '@cypress/react-tooltip' import Dropdown from '../dropdown/dropdown' +import MarkdownRenderer from '../lib/markdown-renderer' import projectsApi from '../projects/projects-api' @@ -73,11 +74,28 @@ export default class Browsers extends Component { {prefixText}{' '} {browser.displayName}{' '} {browser.majorVersion} + {this._warn(browser)} {this._info(browser)} ) } + _warn (browser) { + if (!browser.warning) return null + + return ( + + } + placement='bottom' + className='browser-info-tooltip cy-tooltip' + > + + + + ) + } + _info (browser) { if (!browser.info) return null diff --git a/packages/desktop-gui/src/project/project-model.js b/packages/desktop-gui/src/project/project-model.js index a499b2b0ff82..aa5a5df7d7e6 100644 --- a/packages/desktop-gui/src/project/project-model.js +++ b/packages/desktop-gui/src/project/project-model.js @@ -53,7 +53,7 @@ export default class Project { @observable browserState = 'closed' @observable resolvedConfig @observable error - @observable warning + @observable warnings = [] @observable apiError @observable parentTestsFolderDisplay @observable integrationExampleName @@ -209,18 +209,23 @@ export default class Project { this.error = null } - @action setWarning (warning) { + @action addWarning (warning) { if (!this.dismissedWarnings[this._serializeWarning(warning)]) { - this.warning = warning + this.warnings.push(warning) } } - @action clearWarning () { - if (this.warning) { - this.dismissedWarnings[this._serializeWarning(this.warning)] = true + @action clearWarning (warning) { + if (!warning) { + // calling with no warning clears all warnings + return this.warnings.map((warning) => { + return this.clearWarning(warning) + }) } - this.warning = null + this.dismissedWarnings[this._serializeWarning(warning)] = true + + this.warnings = _.without(this.warnings, warning) } _serializeWarning (warning) { diff --git a/packages/desktop-gui/src/project/project.jsx b/packages/desktop-gui/src/project/project.jsx index 24b2d8a42ab3..df9a0d2ed482 100644 --- a/packages/desktop-gui/src/project/project.jsx +++ b/packages/desktop-gui/src/project/project.jsx @@ -39,15 +39,11 @@ class Project extends Component { if (this.props.project.error) return - const { warning } = this.props.project - return (
- {warning && - - } + {this._renderWarnings()} {this._currentView()}
@@ -72,8 +68,16 @@ class Project extends Component { } } - _removeWarning = () => { - this.props.project.clearWarning() + _renderWarnings = () => { + const { warnings } = this.props.project + + return warnings.map((warning, i) => + ( this._removeWarning(warning)}/>) + ) + } + + _removeWarning = (warning) => { + this.props.project.clearWarning(warning) } _reopenProject = () => { diff --git a/packages/desktop-gui/src/project/warning-message.jsx b/packages/desktop-gui/src/project/warning-message.jsx index 1e3197878176..9acc559bb5ae 100644 --- a/packages/desktop-gui/src/project/warning-message.jsx +++ b/packages/desktop-gui/src/project/warning-message.jsx @@ -1,32 +1,9 @@ import React, { Component } from 'react' import { observer } from 'mobx-react' -import Markdown from 'markdown-it' - -import ipc from '../lib/ipc' - -const md = new Markdown({ - html: true, - linkify: true, -}) +import MarkdownRenderer from '../lib/markdown-renderer' @observer class WarningMessage extends Component { - componentDidMount () { - this.warningMessageNode.addEventListener('click', this._clickHandler) - } - - componentWillUnmount () { - this.warningMessageNode.removeEventListener('click', this._clickHandler) - } - - _clickHandler (e) { - if (e.target.href) { - e.preventDefault() - - return ipc.externalOpen(e.target.href) - } - } - render () { const warningText = this.props.warning.message.split('\n').join('
') @@ -36,9 +13,9 @@ class WarningMessage extends Component { {' '} Warning

-
this.warningMessageNode = node} dangerouslySetInnerHTML={{ - __html: md.render(warningText), - }}>
+
+ +
diff --git a/packages/desktop-gui/src/projects/projects-api.js b/packages/desktop-gui/src/projects/projects-api.js index 78a4d1773af5..32c1ba1bf392 100644 --- a/packages/desktop-gui/src/projects/projects-api.js +++ b/packages/desktop-gui/src/projects/projects-api.js @@ -168,7 +168,7 @@ const openProject = (project) => { }) ipc.onProjectWarning((__, warning) => { - project.setWarning(warning) + project.addWarning(warning) }) return ipc.openProject(project.path) diff --git a/packages/launcher/lib/types.ts b/packages/launcher/lib/types.ts index 1ba6ca1f1cd4..1aca1ebec540 100644 --- a/packages/launcher/lib/types.ts +++ b/packages/launcher/lib/types.ts @@ -35,6 +35,8 @@ export type FoundBrowser = Browser & { custom?: boolean /** optional info that will be shown in the GUI */ info?: string + /** optional warning that will be shown in the GUI */ + warning?: string } // all common type definition for this module diff --git a/packages/server/lib/errors.coffee b/packages/server/lib/errors.coffee index c1677c1387ae..377e0854d47b 100644 --- a/packages/server/lib/errors.coffee +++ b/packages/server/lib/errors.coffee @@ -788,6 +788,20 @@ getMsgByType = (type, arg1 = {}, arg2) -> Provide a path to an existing fixture file. """ + when "BAD_POLICY_WARNING" + """ + Cypress detected policy settings on your computer that may cause issues. + + The following policies were detected that may prevent Cypress from automating Chrome: + + > #{arg1.join('\n > ')} + + For more information, see https://on.cypress.io/bad-browser-policy + """ + when "BAD_POLICY_WARNING_TOOLTIP" + """ + Cypress detected policy settings on your computer that may cause issues with using this browser. For more information, see https://on.cypress.io/bad-browser-policy + """ get = (type, arg1, arg2) -> msg = getMsgByType(type, arg1, arg2) diff --git a/packages/server/lib/gui/events.coffee b/packages/server/lib/gui/events.coffee index bb179e7a62ac..4d66aef92af0 100644 --- a/packages/server/lib/gui/events.coffee +++ b/packages/server/lib/gui/events.coffee @@ -14,6 +14,7 @@ Updater = require("../updater") Project = require("../project") openProject = require("../open_project") ensureUrl = require("../util/ensure-url") +chromePolicyCheck = require("../util/chrome_policy_check") browsers = require("../browsers") konfig = require("../konfig") @@ -204,6 +205,11 @@ handleEvent = (options, bus, event, id, type, arg) -> .then (browsers = []) -> options.config = _.assign(options.config, { browsers }) .then -> + chromePolicyCheck.run (err) -> + options.config.browsers.forEach (browser) -> + if browser.family == 'chrome' + browser.warning = errors.getMsgByType('BAD_POLICY_WARNING_TOOLTIP') + openProject.create(arg, options, { onFocusTests: onFocusTests onSpecChanged: onSpecChanged diff --git a/packages/server/lib/modes/run.coffee b/packages/server/lib/modes/run.coffee index 89963e011a4c..cbf894dfc172 100644 --- a/packages/server/lib/modes/run.coffee +++ b/packages/server/lib/modes/run.coffee @@ -25,6 +25,7 @@ terminal = require("../util/terminal") specsUtil = require("../util/specs") humanTime = require("../util/human_time") electronApp = require("../util/electron_app") +chromePolicyCheck = require("../util/chrome_policy_check") color = (val, c) -> chalk[c](val) @@ -341,6 +342,9 @@ writeOutput = (outputPath, results) -> fs.outputJsonAsync(outputPath, results) +onWarning = (err) -> + console.log(chalk.yellow(err.message)) + openProjectCreate = (projectRoot, socketId, options) -> ## now open the project to boot the server ## putting our web client app in headless mode @@ -351,8 +355,7 @@ openProjectCreate = (projectRoot, socketId, options) -> morgan: false report: true isTextTerminal: options.isTextTerminal - onWarning: (err) -> - console.log(err.message) + onWarning onError: (err) -> console.log("") if err.details @@ -953,6 +956,9 @@ module.exports = { if not specs.length errors.throw('NO_SPECS_FOUND', config.integrationFolder, specPattern) + if browser.family == 'chrome' + chromePolicyCheck.run(onWarning) + runAllSpecs = ({ beforeSpecRun, afterSpecRun, runUrl }, parallelOverride = parallel) => @runSpecs({ beforeSpecRun diff --git a/packages/server/lib/util/chrome_policy_check.js b/packages/server/lib/util/chrome_policy_check.js new file mode 100644 index 000000000000..8e550f7aec6d --- /dev/null +++ b/packages/server/lib/util/chrome_policy_check.js @@ -0,0 +1,111 @@ +const _ = require('lodash') +const debug = require('debug')('cypress:server:chrome_policy_check') +const errors = require('../errors') +const os = require('os') + +// https://www.chromium.org/administrators/policy-list-3#Proxy +// https://www.chromium.org/administrators/policy-list-3#ProxySettings +const BAD_PROXY_POLICY_NAMES = [ + 'ProxySettings', + 'ProxyMode', + 'ProxyServerMode', + 'ProxyServer', + 'ProxyPacUrl', + 'ProxyBypassList', +] + +// https://www.chromium.org/administrators/policy-list-3#Extensions +const BAD_EXTENSION_POLICY_NAMES = [ + 'ExtensionInstallBlacklist', + 'ExtensionInstallWhitelist', + 'ExtensionInstallForcelist', + 'ExtensionInstallSources', + 'ExtensionAllowedTypes', + 'ExtensionAllowInsecureUpdates', + 'ExtensionSettings', + 'UninstallBlacklistedExtensions', +] + +const POLICY_KEYS = [ + 'Software\\Policies\\Google\\Chrome', + 'Software\\Policies\\Google\\Chromium', +] + +const POLICY_HKEYS = [ + 'HKEY_LOCAL_MACHINE', + 'HKEY_CURRENT_USER', +] + +function warnIfPolicyMatches (policyNames, allPolicies, warningName, cb) { + const matchedPolicyPaths = _.chain(policyNames) + .map((policyName) => { + return _.chain(allPolicies) + .find({ name: policyName }) + .get('fullPath') + .value() + }) + .filter() + .value() + + if (!matchedPolicyPaths.length) { + return + } + + cb(errors.get(warningName, matchedPolicyPaths)) +} + +function getRunner ({ enumerateValues }) { + function getAllPolicies () { + return _.flattenDeep( + POLICY_KEYS.map((key) => { + return POLICY_HKEYS.map((hkey) => { + return enumerateValues(hkey, key) + .map((value) => { + value.fullPath = `${hkey}\\${key}\\${value.name}` + + return value + }) + }) + }) + ) + } + + return function run (cb) { + try { + debug('running chrome policy check') + + const policies = getAllPolicies() + const badPolicyNames = _.concat(BAD_PROXY_POLICY_NAMES, BAD_EXTENSION_POLICY_NAMES) + + debug('received policies %o', { policies, badPolicyNames }) + + warnIfPolicyMatches(badPolicyNames, policies, 'BAD_POLICY_WARNING', cb) + } catch (err) { + debug('error running policy check %o', { err }) + } + } +} + +module.exports = { + run: _.noop, + getRunner, +} + +/** + * Only check on Windows. While it is possible for macOS/Linux to have preferences set that + * override Cypress's settings, it's never been reported as an issue and would require more + * native extensions to support checking. + * https://github.com/cypress-io/cypress/issues/4391 + */ +if (os.platform() === 'win32') { + try { + const registryJs = require('@cypress/registry-js') + + module.exports = { + run: getRunner(registryJs), + getRunner, + } + } catch (err) { + debug('error initializing chrome policy check %o', { err }) + } +} diff --git a/packages/server/test/unit/chrome_policy_check.coffee b/packages/server/test/unit/chrome_policy_check.coffee new file mode 100644 index 000000000000..ee0914517e13 --- /dev/null +++ b/packages/server/test/unit/chrome_policy_check.coffee @@ -0,0 +1,64 @@ +require("../spec_helper") + +_ = require("lodash") +{ stripIndent } = require("common-tags") +chromePolicyCheck = require("#{root}lib/util/chrome_policy_check") + +describe "lib/util/chrome_policy_check", -> + context ".getRunner returns a function", -> + it "calls callback with an error if policies are found", -> + run = chromePolicyCheck.getRunner({ + enumerateValues: (hkey, key) -> + ## mock a registry with a couple of policies + _.get({ + 'HKEY_LOCAL_MACHINE': { + 'Software\\Policies\\Google\\Chrome': [ + { name: 'ProxyServer' } + ] + }, + 'HKEY_CURRENT_USER': { + 'Software\\Policies\\Google\\Chromium': [ + { name: 'ExtensionSettings' } + ] + }, + }, "#{hkey}.#{key}", []) + }) + + cb = sinon.stub() + + run(cb) + + expect(cb).to.be.calledOnce + expect(cb.getCall(0).args[0].message).to.eq(stripIndent """ + Cypress detected policy settings on your computer that may cause issues. + + The following policies were detected that may prevent Cypress from automating Chrome: + + > HKEY_LOCAL_MACHINE\\Software\\Policies\\Google\\Chrome\\ProxyServer + > HKEY_CURRENT_USER\\Software\\Policies\\Google\\Chromium\\ExtensionSettings + + For more information, see https://on.cypress.io/bad-browser-policy + """ + ) + + it "does not call callback if no policies are found", -> + run = chromePolicyCheck.getRunner({ + enumerateValues: _.constant([]) + }) + + cb = sinon.stub() + + run(cb) + + expect(cb).to.not.be.called + + it "fails silently if enumerateValues throws", -> + run = chromePolicyCheck.getRunner({ + enumerateValues: -> throw new Error('blah') + }) + + cb = sinon.stub() + + run(cb) + + expect(cb).to.not.be.called diff --git a/packages/server/test/unit/gui/events_spec.coffee b/packages/server/test/unit/gui/events_spec.coffee index eb9f1b70624b..872d4080684c 100644 --- a/packages/server/test/unit/gui/events_spec.coffee +++ b/packages/server/test/unit/gui/events_spec.coffee @@ -5,6 +5,7 @@ EE = require("events") extension = require("@packages/extension") electron = require("electron") Promise = require("bluebird") +chromePolicyCheck = require("#{root}../lib/util/chrome_policy_check") cache = require("#{root}../lib/cache") logger = require("#{root}../lib/logger") Project = require("#{root}../lib/project") @@ -493,6 +494,34 @@ describe "lib/gui/events", -> } ) + it "attaches warning to Chrome browsers when Chrome policy check fails", -> + sinon.stub(openProject, "create").resolves() + @options.browser = "/foo" + + browsers.getAllBrowsersWith.withArgs("/foo").resolves([{family: 'chrome'}, {family: 'some other'}]) + + sinon.stub(chromePolicyCheck, "run").callsArgWith(0, new Error) + + @handleEvent("open:project", "/_test-output/path/to/project").then => + expect(browsers.getAllBrowsersWith).to.be.calledWith(@options.browser) + expect(openProject.create).to.be.calledWithMatch( + "/_test-output/path/to/project", + { + browser: "/foo", + config: { + browsers: [ + { + family: "chrome" + warning: "Cypress detected policy settings on your computer that may cause issues with using this browser. For more information, see https://on.cypress.io/bad-browser-policy" + }, + { + family: "some other" + } + ] + } + } + ) + describe "close:project", -> beforeEach -> sinon.stub(Project.prototype, "close").withArgs({sync: true}).resolves() diff --git a/scripts/check-deps.js b/scripts/check-deps.js index 77b89a2bc74c..2b4d3184e867 100644 --- a/scripts/check-deps.js +++ b/scripts/check-deps.js @@ -6,6 +6,10 @@ const fs = require('fs') const path = require('path') const stripAnsi = require('strip-ansi') +if (process.env.NO_CHECK_DEPS) { + process.exit(0) +} + const args = require('minimist')(process.argv.slice(2)) const cwd = args.cwd || process.cwd()