diff --git a/.eslintrc b/.eslintrc index b32d48eecba8..ae9c0886414d 100644 --- a/.eslintrc +++ b/.eslintrc @@ -3,6 +3,7 @@ "plugin:cypress-dev/general" ], "env": { + "es6": true, "node": true } } diff --git a/packages/server/__snapshots__/plugins_spec.coffee b/packages/server/__snapshots__/plugins_spec.coffee index 75179c368d41..30ac8df24614 100644 --- a/packages/server/__snapshots__/plugins_spec.coffee +++ b/packages/server/__snapshots__/plugins_spec.coffee @@ -76,3 +76,38 @@ Started video recording: /foo/bar/.projects/working-preprocessor/cypress/videos/ (All Done) ` + +exports['e2e plugins can modify config from plugins 1'] = ` +Started video recording: /foo/bar/.projects/plugin-config/cypress/videos/abc123.mp4 + + (Tests Starting) + + + ✓ overrides config + ✓ overrides env + + 2 passing + + + (Tests Finished) + + - Tests: 2 + - Passes: 2 + - Failures: 0 + - Pending: 0 + - Duration: 10 seconds + - Screenshots: 0 + - Video Recorded: true + - Cypress Version: 1.2.3 + + + (Video) + + - Started processing: Compressing to 20 CRF + - Finished processing: /foo/bar/.projects/plugin-config/cypress/videos/abc123.mp4 (0 seconds) + + + (All Done) + +` + diff --git a/packages/server/lib/config.coffee b/packages/server/lib/config.coffee index 58c52bb01b31..62b42b6556d8 100644 --- a/packages/server/lib/config.coffee +++ b/packages/server/lib/config.coffee @@ -2,6 +2,7 @@ _ = require("lodash") path = require("path") Promise = require("bluebird") fs = require("fs-extra") +deepDiff = require("return-deep-diff") errors = require("./errors") scaffold = require("./scaffold") errors = require("./errors") @@ -236,12 +237,31 @@ module.exports = { return obj - resolvePluginValues: (cfg, overrides = {}) -> - ## TODO: for each override go through + updateWithPluginValues: (cfg, overrides = {}) -> + ## diff the overrides with cfg + ## including nested objects (env) + diffs = deepDiff(cfg, overrides, true) + + setResolvedOn = (resolvedObj, obj) -> + _.each obj, (val, key) -> + if _.isObject(val) + ## recurse setting overrides + ## inside of this nested objected + setResolvedOn(resolvedObj[key], val) + else + ## override the resolved value + resolvedObj[key] = { + value: val + from: "plugin" + } + + ## for each override go through ## and change the resolved values of cfg ## to point to the plugin + setResolvedOn(cfg.resolved, diffs) - _.defaultsDeep(overrides, cfg) + ## merge cfg into overrides + _.defaultsDeep(diffs, cfg) resolveConfigValues: (config, defaults, resolved = {}) -> ## pick out only the keys found in configKeys diff --git a/packages/server/lib/plugins/index.coffee b/packages/server/lib/plugins/index.coffee index e4044fa59e9b..c8bba92db8b3 100644 --- a/packages/server/lib/plugins/index.coffee +++ b/packages/server/lib/plugins/index.coffee @@ -44,7 +44,7 @@ module.exports = { ipc.send("load", config) - ipc.on "loaded", (config, registrations) -> + ipc.on "loaded", (newCfg, registrations) -> _.each registrations, (registration) -> log("register plugins process event", registration.event, "with id", registration.callbackId) register registration.event, (args...) -> @@ -56,7 +56,7 @@ module.exports = { } ipc.send("execute", registration.event, ids, args) - resolve(config) + resolve(newCfg) ipc.on "load:error", (type, args...) -> reject(errors.get(type, args...)) diff --git a/packages/server/lib/project.coffee b/packages/server/lib/project.coffee index de2ec80cb688..57a6afd6fcff 100644 --- a/packages/server/lib/project.coffee +++ b/packages/server/lib/project.coffee @@ -73,19 +73,22 @@ class Project extends EE @memoryCheck = setInterval(logMemory, 1000) @getConfig(options) - .then (cfg) => + .tap (cfg) => process.chdir(@projectRoot) - ## TODO: move plugins config up here + ## TODO: we currently always scaffold the plugins file + ## even when headlessly or else it will cause an error when + ## we try to load it and it's not there. We must do this here + ## else initialing the plugins will instantly fail. + if cfg.pluginsFile + scaffold.plugins(path.dirname(cfg.pluginsFile), cfg) + .then (cfg) => @_initPlugins(cfg, options) .then (modifiedCfg) -> - ## TODO merge in modifiedCfg into cfg - cfg = config.resolvePluginValues(cfg, modifiedCfg) - # if _.isObject(modifiedCfg) - # _.extend(cfg, modifiedCfg) - debug("plugin config yielded", modifiedCfg) + cfg = config.updateWithPluginValues(cfg, modifiedCfg) + return cfg .then (cfg) => @server.open(cfg, @) @@ -118,14 +121,18 @@ class Project extends EE @watchSupportFile(cfg) @watchPluginsFile(cfg, options) ) - .then => - @_initPlugins(cfg, options) # return our project instance .return(@) - _initPlugins: (config, options) -> - plugins.init(config, { + _initPlugins: (cfg, options) -> + ## only init plugins with the + ## whitelisted config values to + ## prevent tampering with the + ## internals and breaking cypress + cfg = config.whitelist(cfg) + + plugins.init(cfg, { onError: (err) -> browsers.close() options.onError(err) @@ -161,43 +168,43 @@ class Project extends EE .then -> process.chdir(localCwd) - watchSupportFile: (config) -> - if supportFile = config.supportFile + watchSupportFile: (cfg) -> + if supportFile = cfg.supportFile fs.pathExists(supportFile) .then (found) => if not found errors.throw("SUPPORT_FILE_NOT_FOUND", supportFile) - relativePath = path.relative(config.projectRoot, config.supportFile) - if config.watchForFileChanges isnt false + relativePath = path.relative(cfg.projectRoot, cfg.supportFile) + if cfg.watchForFileChanges isnt false options = { onChange: _.bind(@server.onTestFileChange, @server, relativePath) } - preprocessor.getFile(relativePath, config, options) + preprocessor.getFile(relativePath, cfg, options) ## ignore errors b/c we're just setting up the watching. errors ## are handled by the spec controller .catch -> else Promise.resolve() - watchPluginsFile: (config, options) -> - debug("attempt watch plugins file: #{config.pluginsFile}") - if not config.pluginsFile + watchPluginsFile: (cfg, options) -> + debug("attempt watch plugins file: #{cfg.pluginsFile}") + if not cfg.pluginsFile return Promise.resolve() - fs.pathExists(config.pluginsFile) + fs.pathExists(cfg.pluginsFile) .then (found) => debug("plugins file found? #{found}") ## ignore if not found. plugins#init will throw the right error return if not found debug("watch plugins file") - @watchers.watch(config.pluginsFile, { + @watchers.watch(cfg.pluginsFile, { onChange: => ## TODO: completely re-open project instead? debug("plugins file changed") ## re-init plugins after a change - @_initPlugins(config, options) + @_initPlugins(cfg, options) .catch (err) -> options.onError(err) }) @@ -221,21 +228,21 @@ class Project extends EE @watchers.watch(settings.pathToCypressJson(@projectRoot), obj) - watchSettingsAndStartWebsockets: (options = {}, config = {}) -> + watchSettingsAndStartWebsockets: (options = {}, cfg = {}) -> @watchSettings(options.onSettingsChanged) ## if we've passed down reporter ## then record these via mocha reporter - if config.report - if not Reporter.isValidReporterName(config.reporter, config.projectRoot) - paths = Reporter.getSearchPathsForReporter(config.reporter, config.projectRoot) - errors.throw("INVALID_REPORTER_NAME", config.reporter, paths) + if cfg.report + if not Reporter.isValidReporterName(cfg.reporter, cfg.projectRoot) + paths = Reporter.getSearchPathsForReporter(cfg.reporter, cfg.projectRoot) + errors.throw("INVALID_REPORTER_NAME", cfg.reporter, paths) - reporter = Reporter.create(config.reporter, config.reporterOptions, config.projectRoot) + reporter = Reporter.create(cfg.reporter, cfg.reporterOptions, cfg.projectRoot) - @automation = Automation.create(config.namespace, config.socketIoCookie, config.screenshotsFolder) + @automation = Automation.create(cfg.namespace, cfg.socketIoCookie, cfg.screenshotsFolder) - @server.startWebsockets(@automation, config, { + @server.startWebsockets(@automation, cfg, { onReloadBrowser: options.onReloadBrowser onFocusTests: options.onFocusTests @@ -377,7 +384,7 @@ class Project extends EE [browserUrl, "#/tests", specUrl].join("/").replace(multipleForwardSlashesRe, replacer) - scaffold: (config) -> + scaffold: (cfg) -> debug("scaffolding project %s", @projectRoot) scaffolds = [] @@ -392,19 +399,13 @@ class Project extends EE ## ## ensure support dir is created ## and example support file if dir doesnt exist - push(scaffold.support(config.supportFolder, config)) - - ## TODO: we currently always scaffold the plugins file - ## even when headlessly or else it will cause an error when - ## we try to load it and it's not there - if config.pluginsFile - push(scaffold.plugins(path.dirname(config.pluginsFile), config)) + push(scaffold.support(cfg.supportFolder, cfg)) ## if we're in headed mode add these other scaffolding ## tasks - if not config.isTextTerminal - push(scaffold.integration(config.integrationFolder, config)) - push(scaffold.fixture(config.fixturesFolder, config)) + if not cfg.isTextTerminal + push(scaffold.integration(cfg.integrationFolder, cfg)) + push(scaffold.fixture(cfg.fixturesFolder, cfg)) Promise.all(scaffolds) diff --git a/packages/server/package.json b/packages/server/package.json index ebb9e46f347d..c742fc999db3 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -138,6 +138,7 @@ "randomstring": "^1.1.5", "request": "2.79.0", "request-promise": "4.1.1", + "return-deep-diff": "^0.2.9", "sanitize-filename": "^1.6.1", "semver": "^5.3.0", "send": "^0.14.1", diff --git a/packages/server/test/e2e/plugins_spec.coffee b/packages/server/test/e2e/plugins_spec.coffee index 25b328e26acd..7d48bfd72a65 100644 --- a/packages/server/test/e2e/plugins_spec.coffee +++ b/packages/server/test/e2e/plugins_spec.coffee @@ -1,6 +1,7 @@ e2e = require("../support/helpers/e2e") Fixtures = require("../support/helpers/fixtures") +pluginConfig = Fixtures.projectPath("plugin-config") workingPreprocessor = Fixtures.projectPath("working-preprocessor") pluginsAsyncError = Fixtures.projectPath("plugins-async-error") @@ -22,3 +23,13 @@ describe "e2e plugins", -> snapshot: true expectedExitCode: 1 }) + + it "can modify config from plugins", -> + e2e.exec(@, { + spec: "app_spec.coffee" + env: "foo=foo,bar=bar" + config: "pageLoadTimeout=10000" + project: pluginConfig + snapshot: true + expectedExitCode: 0 + }) diff --git a/packages/server/test/integration/cypress_spec.coffee b/packages/server/test/integration/cypress_spec.coffee index fd9560f88f3f..75d27a32006f 100644 --- a/packages/server/test/integration/cypress_spec.coffee +++ b/packages/server/test/integration/cypress_spec.coffee @@ -77,6 +77,7 @@ describe "lib/cypress", -> @todosPath = Fixtures.projectPath("todos") @pristinePath = Fixtures.projectPath("pristine") @noScaffolding = Fixtures.projectPath("no-scaffolding") + @pluginConfig = Fixtures.projectPath("plugin-config") @idsPath = Fixtures.projectPath("ids") ## force cypress to call directly into main without @@ -630,6 +631,40 @@ describe "lib/cypress", -> @expectExitWith(0) + it "can override values in plugins", -> + cypress.start([ + "--run-project=#{@pluginConfig}", "--config=requestTimeout=1234,videoCompression=false" + "--env=foo=foo,bar=bar" + ]) + .then => + cfg = openProject.getProject().cfg + + expect(cfg.videoCompression).to.eq(20) + expect(cfg.defaultCommandTimeout).to.eq(500) + expect(cfg.env).to.deep.eq({ + foo: "bar" + bar: "bar" + }) + + expect(cfg.resolved.videoCompression).to.deep.eq({ + value: 20 + from: "plugin" + }) + expect(cfg.resolved.requestTimeout).to.deep.eq({ + value: 1234 + from: "cli" + }) + expect(cfg.resolved.env.foo).to.deep.eq({ + value: "bar" + from: "plugin" + }) + expect(cfg.resolved.env.bar).to.deep.eq({ + value: "bar" + from: "cli" + }) + + @expectExitWith(0) + describe "--port", -> beforeEach -> headless.listenForProjectEnd.resolves({failures: 0}) diff --git a/packages/server/test/support/fixtures/projects/plugin-config/cypress.json b/packages/server/test/support/fixtures/projects/plugin-config/cypress.json new file mode 100644 index 000000000000..0967ef424bce --- /dev/null +++ b/packages/server/test/support/fixtures/projects/plugin-config/cypress.json @@ -0,0 +1 @@ +{} diff --git a/packages/server/test/support/fixtures/projects/plugin-config/cypress/integration/app_spec.coffee b/packages/server/test/support/fixtures/projects/plugin-config/cypress/integration/app_spec.coffee new file mode 100644 index 000000000000..8b956d3b0bce --- /dev/null +++ b/packages/server/test/support/fixtures/projects/plugin-config/cypress/integration/app_spec.coffee @@ -0,0 +1,14 @@ +it "overrides config", -> + ## overrides come from plugins + expect(Cypress.config("defaultCommandTimeout")).to.eq(500) + expect(Cypress.config("videoCompression")).to.eq(20) + + ## overrides come from CLI + expect(Cypress.config("pageLoadTimeout")).to.eq(10000) + +it "overrides env", -> + ## overrides come from plugins + expect(Cypress.env("foo")).to.eq("bar") + + ## overrides come from CLI + expect(Cypress.env("bar")).to.eq("bar") diff --git a/packages/server/test/support/fixtures/projects/plugin-config/cypress/plugins/index.js b/packages/server/test/support/fixtures/projects/plugin-config/cypress/plugins/index.js new file mode 100644 index 000000000000..95461bde6d6e --- /dev/null +++ b/packages/server/test/support/fixtures/projects/plugin-config/cypress/plugins/index.js @@ -0,0 +1,12 @@ +module.exports = (on, config) => { + return new Promise((resolve) => { + setTimeout(resolve, 100) + }) + .then(() => { + config.defaultCommandTimeout = 500 + config.videoCompression = 20 + config.env.foo = 'bar' + + return config + }) +} diff --git a/packages/server/test/support/helpers/e2e.coffee b/packages/server/test/support/helpers/e2e.coffee index bf3c4ac19460..b9ee52bcf1b2 100644 --- a/packages/server/test/support/helpers/e2e.coffee +++ b/packages/server/test/support/helpers/e2e.coffee @@ -150,7 +150,7 @@ module.exports = { args.push("--hosts=#{options.hosts}") if options.debug - args.push("--show-headless-gui") + args.push("--headed") if options.reporter args.push("--reporter=#{options.reporter}") @@ -161,6 +161,12 @@ module.exports = { if browser = (env.BROWSER or options.browser) args.push("--browser=#{browser}") + if options.config + args.push("--config", options.config) + + if options.env + args.push("--env", options.env) + return args start: (ctx, options = {}) ->