From 875e07f1f39e22e768aca67d42df375175ae18d9 Mon Sep 17 00:00:00 2001 From: Jennifer Shehane Date: Fri, 17 May 2019 14:23:41 +0630 Subject: [PATCH 01/49] remove Node 4 support, bump packages that were previously approved but did not support Node 4 (#4208) * remove Node 4 support, bump packages that were previously approved but had removed Node 4 support * Update snapshots to reflect whitespace changes made in commander dep * bump supports-color dep (was previously not updated due to lack of Node 4 support) --- cli/__snapshots__/cli_spec.js | 42 ++++++++++------------------------- cli/package.json | 8 +++---- 2 files changed, 16 insertions(+), 34 deletions(-) diff --git a/cli/__snapshots__/cli_spec.js b/cli/__snapshots__/cli_spec.js index fca3e63943d9..6b32ae654418 100644 --- a/cli/__snapshots__/cli_spec.js +++ b/cli/__snapshots__/cli_spec.js @@ -37,12 +37,10 @@ exports['cli help command shows help 1'] = ` Usage: cypress [options] [command] Options: - -v, --version prints Cypress version -h, --help output usage information Commands: - help Shows CLI help and exits version prints Cypress version run [options] Runs Cypress tests from the CLI without the GUI @@ -72,12 +70,10 @@ exports['cli help command shows help for --help 1'] = ` Usage: cypress [options] [command] Options: - -v, --version prints Cypress version -h, --help output usage information Commands: - help Shows CLI help and exits version prints Cypress version run [options] Runs Cypress tests from the CLI without the GUI @@ -107,12 +103,10 @@ exports['cli help command shows help for -h 1'] = ` Usage: cypress [options] [command] Options: - -v, --version prints Cypress version -h, --help output usage information Commands: - help Shows CLI help and exits version prints Cypress version run [options] Runs Cypress tests from the CLI without the GUI @@ -140,23 +134,20 @@ exports['cli unknown command shows usage and exits 1'] = ` stdout: ------- Unknown command "foo" + Usage: cypress [options] [command] - Usage: cypress [options] [command] - - Options: - - -v, --version prints Cypress version - -h, --help output usage information - - Commands: + Options: + -v, --version prints Cypress version + -h, --help output usage information - help Shows CLI help and exits - version prints Cypress version - run [options] Runs Cypress tests from the CLI without the GUI - open [options] Opens Cypress in the interactive GUI. - install [options] Installs the Cypress executable matching this package's version - verify Verifies that Cypress is installed correctly and executable - cache [options] Manages the Cypress binary cache + Commands: + help Shows CLI help and exits + version prints Cypress version + run [options] Runs Cypress tests from the CLI without the GUI + open [options] Opens Cypress in the interactive GUI. + install [options] Installs the Cypress executable matching this package's version + verify Verifies that Cypress is installed correctly and executable + cache [options] Manages the Cypress binary cache ------- stderr: ------- @@ -181,7 +172,6 @@ exports['cli unknown option shows help for cache command - no sub-command 1'] = Manages the Cypress binary cache Options: - list list cached binary versions path print the path to the binary cache clear delete all cached binaries @@ -207,13 +197,11 @@ exports['cli unknown option shows help for cache command - unknown option --foo ------- error: unknown option: --foo - Usage: cache [command] Manages the Cypress binary cache Options: - list list cached binary versions path print the path to the binary cache clear delete all cached binaries @@ -239,13 +227,11 @@ exports['cli unknown option shows help for cache command - unknown sub-command f ------- error: unknown command: cache foo - Usage: cache [command] Manages the Cypress binary cache Options: - list list cached binary versions path print the path to the binary cache clear delete all cached binaries @@ -286,13 +272,11 @@ exports['shows help for open --foo 1'] = ` ------- error: unknown option: --foo - Usage: open [options] Opens Cypress in the interactive GUI. Options: - -p, --port runs Cypress on a specific port. overrides any value in cypress.json. -e, --env sets environment variables. separate multiple values with a comma. overrides any value in cypress.json or cypress.env.json -c, --config sets configuration values. separate multiple values with a comma. overrides any value in cypress.json. @@ -323,13 +307,11 @@ exports['shows help for run --foo 1'] = ` ------- error: unknown option: --foo - Usage: run [options] Runs Cypress tests from the CLI without the GUI Options: - --record [bool] records the run. sends test results, screenshots and videos to your Cypress Dashboard. --headed displays the Electron browser instead of running headlessly -k, --key your secret Record Key. you can omit this if you set a CYPRESS_RECORD_KEY environment variable. diff --git a/cli/package.json b/cli/package.json index de2fabe56dd0..024b0fa30dc7 100644 --- a/cli/package.json +++ b/cli/package.json @@ -7,7 +7,7 @@ "cypress": "bin/cypress" }, "engines": { - "node": ">=4.0.0" + "node": ">=6.0.0" }, "scripts": { "postinstall": "node ./scripts/post-install.js", @@ -43,10 +43,10 @@ "@cypress/xvfb": "1.2.4", "arch": "2.1.1", "bluebird": "3.5.0", - "cachedir": "1.3.0", + "cachedir": "2.2.0", "chalk": "2.4.2", "check-more-types": "2.24.0", - "commander": "2.15.1", + "commander": "2.20.0", "common-tags": "1.8.0", "debug": "3.2.6", "execa": "0.10.0", @@ -66,7 +66,7 @@ "ramda": "0.24.1", "request": "2.88.0", "request-progress": "0.4.0", - "supports-color": "5.5.0", + "supports-color": "6.1.0", "tmp": "0.1.0", "url": "0.11.0", "yauzl": "2.10.0" From 523e3dad73287914d62b001f9d5c0e9ef724d5c3 Mon Sep 17 00:00:00 2001 From: Chris Breiding Date: Mon, 3 Dec 2018 14:31:45 -0500 Subject: [PATCH 02/49] Upgrade mocha to latest version (5.2.0) (#2703) Closes #2528 --- packages/driver/package.json | 2 +- packages/driver/src/cypress.coffee | 4 - packages/driver/src/cypress/cy.coffee | 12 +- .../driver/src/cypress/error_messages.coffee | 11 ++ packages/driver/src/cypress/mocha.coffee | 6 +- packages/driver/src/cypress/runner.coffee | 150 +++++++----------- .../integration/cypress/cypress_spec.coffee | 4 +- .../integration/e2e/promises_spec.coffee | 4 +- .../integration/e2e/return_value_spec.coffee | 24 +++ .../test/unit_old/cypress/mocha_spec.coffee | 16 -- .../test/unit_old/cypress/runner_spec.coffee | 25 --- .../1_commands_outside_of_test_spec.coffee.js | 2 + .../__snapshots__/3_only_spec.coffee.js | 45 +++++- .../4_return_value_spec.coffee.js | 36 +++-- packages/server/test/e2e/3_only_spec.coffee | 2 +- .../test/e2e/4_return_value_spec.coffee | 2 +- .../integration/only_multiple_spec.coffee | 13 ++ .../integration/return_value_spec.coffee | 6 +- 18 files changed, 200 insertions(+), 164 deletions(-) create mode 100644 packages/server/test/support/fixtures/projects/e2e/cypress/integration/only_multiple_spec.coffee diff --git a/packages/driver/package.json b/packages/driver/package.json index 40748a9cf55f..79aa42f3fcd0 100644 --- a/packages/driver/package.json +++ b/packages/driver/package.json @@ -49,7 +49,7 @@ "method-override": "3.0.0", "minimatch": "3.0.4", "minimist": "1.2.0", - "mocha": "cypress-io/mocha#58f6eac05e664fc6b69aa9fba70f1f6b5531a900", + "mocha": "5.2.0", "moment": "2.24.0", "morgan": "1.9.1", "npm-install-version": "6.0.2", diff --git a/packages/driver/src/cypress.coffee b/packages/driver/src/cypress.coffee index 411db0939b4c..c091e046685d 100644 --- a/packages/driver/src/cypress.coffee +++ b/packages/driver/src/cypress.coffee @@ -143,10 +143,6 @@ class $Cypress @action("cypress:config", config) initialize: ($autIframe) -> - ## push down the options - ## to the runner - @mocha.options(@runner) - @cy.initialize($autIframe) run: (fn) -> diff --git a/packages/driver/src/cypress/cy.coffee b/packages/driver/src/cypress/cy.coffee index 8a80eda2abee..2f6b7ad2d7d4 100644 --- a/packages/driver/src/cypress/cy.coffee +++ b/packages/driver/src/cypress/cy.coffee @@ -1054,9 +1054,15 @@ create = (specWindow, Cypress, Cookies, state, config, log) -> ## if we're cy or we've enqueued commands if isCy(ret) or (queue.length > currentLength) - ## the run should already be kicked off - ## by now and return this promise - return state("promise") + if fn.length + ## if user has passed done callback + ## don't return anything so we don't get an + ## 'overspecified' error from mocha + return + else + ## otherwise, return the 'queue promise' + ## so mocha awaits it + return state("promise") ## else just return ret return ret diff --git a/packages/driver/src/cypress/error_messages.coffee b/packages/driver/src/cypress/error_messages.coffee index 16d26dea288c..ce58cff24b9a 100644 --- a/packages/driver/src/cypress/error_messages.coffee +++ b/packages/driver/src/cypress/error_messages.coffee @@ -547,6 +547,17 @@ module.exports = { async_timed_out: "Timed out after '{{ms}}ms'. The done() callback was never invoked!" invalid_interface: "Invalid mocha interface '{{name}}'" timed_out: "Cypress command timeout of '{{ms}}ms' exceeded." + overspecified: """ + Cypress detected that you returned a promise in a test, but also invoked a done callback. Return a promise -or- invoke a done callback, not both. + + Read more here: https://on.cypress.io/returning-promise-and-invoking-done-callback + + #{divider(60, '-')} + + Original mocha error: + + {{error}} + """ navigation: cross_origin: """ diff --git a/packages/driver/src/cypress/mocha.coffee b/packages/driver/src/cypress/mocha.coffee index 21df770d7629..aabb7a210a0e 100644 --- a/packages/driver/src/cypress/mocha.coffee +++ b/packages/driver/src/cypress/mocha.coffee @@ -99,6 +99,9 @@ patchRunnerFail = -> ## matching the current Runner.prototype.fail except ## changing the logic for determing whether this is a valid err Runner::fail = (runnable, err) -> + if err?.message?.indexOf("Resolution method is overspecified") > -1 + err.message = $utils.errMessageByPath("mocha.overspecified", { error: err.stack }) + ## if this isnt a correct error object then just bail ## and call the original function if Object.prototype.toString.call(err) isnt "[object Error]" @@ -189,9 +192,6 @@ create = (specWindow, Cypress, reporter) -> getRootSuite: -> _mocha.suite - - options: (runner) -> - runner.options(_mocha.options) } module.exports = { diff --git a/packages/driver/src/cypress/runner.coffee b/packages/driver/src/cypress/runner.coffee index 15a96379d89c..d375c9f80a9a 100644 --- a/packages/driver/src/cypress/runner.coffee +++ b/packages/driver/src/cypress/runner.coffee @@ -6,7 +6,6 @@ Pending = require("mocha/lib/pending") $Log = require("./log") $utils = require("./utils") -defaultGrepRe = /.*/ mochaCtxKeysRe = /^(_runnable|test)$/ betweenQuotesRe = /\"(.+?)\"/ @@ -193,7 +192,7 @@ getAllSiblingTests = (suite, getTestById) -> ## iterate through each of our suites tests. ## this will iterate through all nested tests ## as well. and then we add it only if its - ## in our grepp'd tests array + ## in our filtered tests array if getTestById(test.id) tests.push test @@ -212,7 +211,7 @@ getTestFromHook = (hook, suite, getTestById) -> return found if found ## returns us the very first test - ## which is in our grepped tests array + ## which is in our filtered tests array ## based on walking down the current suite ## iterating through each test until it matches found = onFirstTest suite, (test) => @@ -227,11 +226,11 @@ getTestFromHook = (hook, suite, getTestById) -> ## we have to see if this is the last suite amongst ## its siblings. but first we have to filter out -## suites which dont have a grep'd test in them +## suites which dont have a filtered test in them isLastSuite = (suite, tests) -> return false if suite.root - ## grab all of the suites from our grep'd tests + ## grab all of the suites from our filtered tests ## including all of their ancestor suites! suites = _.reduce tests, (memo, test) -> while parent = test.parent @@ -296,17 +295,17 @@ overrideRunnerHook = (Cypress, _runner, getTestById, getTest, setTest, getTests) when "afterEach" t = getTest() - ## find all of the grep'd _tests which share + ## find all of the filtered _tests which share ## the same parent suite as our current _test tests = getAllSiblingTests(t.parent, getTestById) ## make sure this test isnt the last test overall but also - ## isnt the last test in our grep'd parent suite's tests array + ## isnt the last test in our filtered parent suite's tests array if @suite.root and (t isnt _.last(allTests)) and (t isnt _.last(tests)) changeFnToRunAfterHooks() when "afterAll" - ## find all of the grep'd allTests which share + ## find all of the filtered allTests which share ## the same parent suite as our current _test if t = getTest() siblings = getAllSiblingTests(t.parent, getTestById) @@ -327,17 +326,6 @@ overrideRunnerHook = (Cypress, _runner, getTestById, getTest, setTest, getTests) _runnerHook.call(@, name, fn) -matchesGrep = (runnable, grep) -> - ## we have optimized this iteration to the maximum. - ## we memoize the existential matchesGrep property - ## so we dont regex again needlessly when going - ## through tests which have already been set earlier - if (not runnable.matchesGrep?) or (not _.isEqual(runnable.grepRe, grep)) - runnable.grepRe = grep - runnable.matchesGrep = grep.test(runnable.fullTitle()) - - runnable.matchesGrep - getTestResults = (tests) -> _.map tests, (test) -> obj = _.pick(test, "id", "duration", "state") @@ -347,7 +335,12 @@ getTestResults = (tests) -> obj.state = "skipped" obj -normalizeAll = (suite, initialTests = {}, grep, setTestsById, setTests, onRunnable, onLogsById, getTestId) -> +hasOnly = (suite) -> + suite._onlyTests.length or + suite._onlySuites.length or + _.some(suite.suites, hasOnly) + +normalizeAll = (suite, initialTests = {}, setTestsById, setTests, onRunnable, onLogsById, getTestId) -> hasTests = false ## only loop until we find the first test @@ -361,10 +354,8 @@ normalizeAll = (suite, initialTests = {}, grep, setTestsById, setTests, onRunnab ## we hand back a normalized object but also ## create optimized lookups for the tests without ## traversing through it multiple times - tests = {} - grepIsDefault = _.isEqual(grep, defaultGrepRe) - - obj = normalize(suite, tests, initialTests, grep, grepIsDefault, onRunnable, onLogsById, getTestId) + tests = {} + normalizedSuite = normalize(suite, tests, initialTests, onRunnable, onLogsById, getTestId) if setTestsById ## use callback here to hand back @@ -375,10 +366,10 @@ normalizeAll = (suite, initialTests = {}, grep, setTestsById, setTests, onRunnab ## same pattern here setTests(_.values(tests)) - return obj + return normalizedSuite -normalize = (runnable, tests, initialTests, grep, grepIsDefault, onRunnable, onLogsById, getTestId) -> - normalizer = (runnable) => +normalize = (runnable, tests, initialTests, onRunnable, onLogsById, getTestId) -> + normalizeRunnable = (runnable) => runnable.id = getTestId() ## tests have a type of 'test' whereas suites do not have a type property @@ -402,57 +393,50 @@ normalize = (runnable, tests, initialTests, grep, grepIsDefault, onRunnable, onL push = (test) => tests[test.id] ?= test - obj = normalizer(runnable) + normalizedRunnable = normalizeRunnable(runnable) - ## if we have a default grep then avoid - ## grepping altogether and just push - ## tests into the array of tests - if grepIsDefault + if runnable.type isnt "suite" or not hasOnly(runnable) if runnable.type is "test" push(runnable) - ## and recursively iterate and normalize all other _runnables - _.each {tests: runnable.tests, suites: runnable.suites}, (_runnables, key) => - if runnable[key] - obj[key] = _.map _runnables, (runnable) => - normalize(runnable, tests, initialTests, grep, grepIsDefault, onRunnable, onLogsById, getTestId) - else - ## iterate through all tests and only push them in - ## if they match the current grep - obj.tests = _.reduce runnable.tests ? [], (memo, test) => - ## only push in the test if it matches - ## our grep - if matchesGrep(test, grep) - memo.push(normalizer(test)) - push(test) - memo - , [] - - ## and go through the suites - obj.suites = _.reduce runnable.suites ? [], (memo, suite) => - ## but only add them if a single nested test - ## actually matches the grep - any = anyTestInSuite suite, (test) => - matchesGrep(test, grep) - - if any - memo.push( - normalize( - suite, - tests, - initialTests, - grep, - grepIsDefault, - onRunnable, - onLogsById, - getTestId - ) - ) - - memo - , [] - - return obj + ## recursively iterate and normalize all other _runnables + _.each {tests: runnable.tests, suites: runnable.suites}, (_runnables, type) => + if runnable[type] + normalizedRunnable[type] = _.map _runnables, (runnable) => + normalize(runnable, tests, initialTests, onRunnable, onLogsById, getTestId) + + return normalizedRunnable + + ## this follows how mocha filters onlys. its runner#filterOnly + ## is pretty much the same minus the normalization part + filterOnly = (normalizedSuite, suite) -> + if suite._onlyTests.length + suite.tests = suite._onlyTests + normalizedSuite.tests = _.map suite._onlyTests, (test) => + normalizedTest = normalizeRunnable(test, initialTests, onRunnable, onLogsById, getTestId) + push(normalizedTest) + normalizedTest + suite.suites = [] + normalizedSuite.suites = [] + else + suite.tests = [] + normalizedSuite.tests = [] + _.each suite._onlySuites, (onlySuite) -> + normalizedOnlySuite = normalizeRunnable(onlySuite, initialTests, onRunnable, onLogsById, getTestId) + if hasOnly(onlySuite) + filterOnly(normalizedOnlySuite, onlySuite) + + suite.suites = _.filter suite.suites, (childSuite) -> + normalizedChildSuite = normalizeRunnable(childSuite, initialTests, onRunnable, onLogsById, getTestId) + suite._onlySuites.indexOf(childSuite) isnt -1 or filterOnly(normalizedChildSuite, childSuite) + normalizedSuite.suites = _.map suite.suites, (childSuite) -> + normalize(childSuite, tests, initialTests, onRunnable, onLogsById, getTestId) + + return suite.tests.length or suite.suites.length + + filterOnly(normalizedRunnable, runnable) + + return normalizedRunnable afterEachFailed = (Cypress, test, err) -> test.state = "failed" @@ -717,23 +701,6 @@ create = (specWindow, mocha, Cypress, cy) -> overrideRunnerHook(Cypress, _runner, getTestById, getTest, setTest, getTests) return { - grep: (re) -> - if arguments.length - _runner._grep = re - else - ## grab grep from the mocha _runner - ## or just set it to all in case - ## there is a mocha regression - _runner._grep ?= defaultGrepRe - - options: (options = {}) -> - ## TODO - ## need to handle - ## ignoreLeaks, asyncOnly, globals - - if re = options.grep - @grep(re) - normalizeAll: (tests) -> ## if we have an uncaught error then slice out ## all of the tests and suites and just generate @@ -750,7 +717,6 @@ create = (specWindow, mocha, Cypress, cy) -> normalizeAll( _runner.suite, tests, - @grep(), setTestsById, setTests, onRunnable, diff --git a/packages/driver/test/cypress/integration/cypress/cypress_spec.coffee b/packages/driver/test/cypress/integration/cypress/cypress_spec.coffee index e69ef13af196..0ea7e34f4980 100644 --- a/packages/driver/test/cypress/integration/cypress/cypress_spec.coffee +++ b/packages/driver/test/cypress/integration/cypress/cypress_spec.coffee @@ -29,7 +29,7 @@ describe "driver/src/cypress/index", -> expect($el.get(0)).to.eq($foo.get(0)) context "#backend", -> - it "sets __stackCleaned__ on errors", (done) -> + it "sets __stackCleaned__ on errors", -> cy.stub(@Cypress, "emit") .withArgs("backend:request") .yieldsAsync({ @@ -75,4 +75,4 @@ describe "driver/src/cypress/index", -> fn = -> Cypress.log({ message: 'My Log' }) - expect(fn).to.not.throw() \ No newline at end of file + expect(fn).to.not.throw() diff --git a/packages/driver/test/cypress/integration/e2e/promises_spec.coffee b/packages/driver/test/cypress/integration/e2e/promises_spec.coffee index cd165e95aec7..cbfcd7f286c9 100644 --- a/packages/driver/test/cypress/integration/e2e/promises_spec.coffee +++ b/packages/driver/test/cypress/integration/e2e/promises_spec.coffee @@ -106,7 +106,7 @@ describe "promises", -> cy.foo() - it "can return a promise that throws on its own without warning", (done) -> + it "can return a promise that throws on its own without warning", -> Cypress.Promise .delay(10) .then -> @@ -116,7 +116,6 @@ describe "promises", -> throw new Error("foo") .catch -> - done() it "can still fail cypress commands", (done) -> cy.on "fail", (err) -> @@ -128,3 +127,4 @@ describe "promises", -> .then -> cy.wrap({}).then -> throw new Error("foo") + return diff --git a/packages/driver/test/cypress/integration/e2e/return_value_spec.coffee b/packages/driver/test/cypress/integration/e2e/return_value_spec.coffee index 0eb158f95fad..8ef43f5ea33f 100644 --- a/packages/driver/test/cypress/integration/e2e/return_value_spec.coffee +++ b/packages/driver/test/cypress/integration/e2e/return_value_spec.coffee @@ -16,6 +16,10 @@ describe "return values", -> return undefined + it "can return cy and have done callback", (done) -> + cy.wrap({}).then -> + done() + it "throws when returning a non promise and invoking cy commands", (done) -> cy.on "fail", (err) -> expect(err.message).to.include("> foo") @@ -91,3 +95,23 @@ describe "return values", -> return "bar" cy.foo() + + describe "without invoking cy", -> + it "handles returning undefined", -> + return undefined + + it "handles synchronously invoking and returning done callback", (done) -> + return done() + + it "handles synchronously invoking done callback and returning undefined", (done) -> + done() + return undefined + + it "handles synchronously invoking done callback and returning a value", (done) -> + done() + return "foo" + + it "handles asynchronously invoking done callback", (done) -> + setTimeout -> + done() + return "foo" diff --git a/packages/driver/test/unit_old/cypress/mocha_spec.coffee b/packages/driver/test/unit_old/cypress/mocha_spec.coffee index 73c02d9ccea8..77507fb9b914 100644 --- a/packages/driver/test/unit_old/cypress/mocha_spec.coffee +++ b/packages/driver/test/unit_old/cypress/mocha_spec.coffee @@ -51,12 +51,6 @@ describe "$Cypress.Mocha API", -> beforeEach -> @mocha = $Cypress.Mocha.create(@Cypress, @iframe) - describe "abort", -> - it "resets mocha grep to all", -> - @mocha.grep /\w+/ - @Cypress.trigger "abort" - expect(@mocha.mocha._grep).to.match /.*/ - describe "stop", -> it "calls stop", -> stop = @sandbox.stub @mocha, "stop" @@ -104,16 +98,6 @@ describe "$Cypress.Mocha API", -> @Cypress.trigger("stop") expect(@Cypress.mocha).to.be.null - context "#grep", -> - beforeEach -> - @mocha = $Cypress.Mocha.create(@Cypress, @iframe) - - it "proxies argument to mocha.grep", -> - grep = @sandbox.spy @mocha.mocha, "grep" - re = /\w+/ - @mocha.grep(re) - expect(grep).to.be.calledWith re - context "#getRunner", -> beforeEach -> @mocha = $Cypress.Mocha.create(@Cypress, @iframe) diff --git a/packages/driver/test/unit_old/cypress/runner_spec.coffee b/packages/driver/test/unit_old/cypress/runner_spec.coffee index 3ed987f861dc..9d60c67e4900 100644 --- a/packages/driver/test/unit_old/cypress/runner_spec.coffee +++ b/packages/driver/test/unit_old/cypress/runner_spec.coffee @@ -626,20 +626,6 @@ describe "$Cypress.Runner API", -> expect(calls).to.have.length(1) done() - context "#grep", -> - beforeEach -> - @runner = $Cypress.Runner.runner(@Cypress, {}) - - it "set /.*/ by default", -> - @runner.grep() - expect(@runner.runner._grep).to.match /.*/ - - it "can set to another RegExp", -> - re = /.+/ - @runner.grep(re) - - expect(@runner.runner._grep).to.eq re - context "#anyTestInSuite", -> beforeEach -> runner = Fixtures.createRunnables { @@ -717,17 +703,6 @@ describe "$Cypress.Runner API", -> ## 4 tests expect(@runner.tests).to.have.length(4) - it "only pushes matching grep tests", -> - ## with 4 existing tests - expect(@runner.tests).to.have.length(4) - - @runner.grep(/four/) - - @runner.normalizeAll() - - ## only 1 test should have matched the grep - expect(@runner.tests).to.have.length(1) - it "sets runnable type", -> types = _.map @runner.runnables, "type" expect(types).to.deep.eq ["suite", "test", "suite", "test", "test", "suite", "test"] diff --git a/packages/server/__snapshots__/1_commands_outside_of_test_spec.coffee.js b/packages/server/__snapshots__/1_commands_outside_of_test_spec.coffee.js index fc2b212b5de3..7297c207df95 100644 --- a/packages/server/__snapshots__/1_commands_outside_of_test_spec.coffee.js +++ b/packages/server/__snapshots__/1_commands_outside_of_test_spec.coffee.js @@ -47,6 +47,8 @@ We dynamically generated a new test to display this failure. at stack trace line at stack trace line at stack trace line + at stack trace line + at stack trace line diff --git a/packages/server/__snapshots__/3_only_spec.coffee.js b/packages/server/__snapshots__/3_only_spec.coffee.js index 07f47cef3995..ad6cc66aad3c 100644 --- a/packages/server/__snapshots__/3_only_spec.coffee.js +++ b/packages/server/__snapshots__/3_only_spec.coffee.js @@ -7,14 +7,49 @@ exports['e2e only spec failing 1'] = ` ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ │ Cypress: 1.2.3 │ │ Browser: FooBrowser 88 │ - │ Specs: 1 found (only_spec.coffee) │ - │ Searched: cypress/integration/only_spec.coffee │ + │ Specs: 2 found (only_multiple_spec.coffee, only_spec.coffee) │ + │ Searched: cypress/integration/only*.coffee │ └────────────────────────────────────────────────────────────────────────────────────────────────┘ ──────────────────────────────────────────────────────────────────────────────────────────────────── - Running: only_spec.coffee... (1 of 1) + Running: only_multiple_spec.coffee... (1 of 2) + + + s1 + ✓ t3 + - t4 + + + 1 passing + 1 pending + + + (Results) + + ┌─────────────────────────────────────────┐ + │ Tests: 2 │ + │ Passing: 1 │ + │ Failing: 0 │ + │ Pending: 1 │ + │ Skipped: 0 │ + │ Screenshots: 0 │ + │ Video: true │ + │ Duration: X seconds │ + │ Spec Ran: only_multiple_spec.coffee │ + └─────────────────────────────────────────┘ + + + (Video) + + - Started processing: Compressing to 32 CRF + - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (X seconds) + + +──────────────────────────────────────────────────────────────────────────────────────────────────── + + Running: only_spec.coffee... (2 of 2) s1 @@ -52,9 +87,11 @@ exports['e2e only spec failing 1'] = ` Spec Tests Passing Failing Pending Skipped ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ ✔ only_multiple_spec.coffee XX:XX 2 1 - 1 - │ + ├────────────────────────────────────────────────────────────────────────────────────────────────┤ │ ✔ only_spec.coffee XX:XX 1 1 - - - │ └────────────────────────────────────────────────────────────────────────────────────────────────┘ - All specs passed! XX:XX 1 1 - - - + All specs passed! XX:XX 3 2 - 1 - ` diff --git a/packages/server/__snapshots__/4_return_value_spec.coffee.js b/packages/server/__snapshots__/4_return_value_spec.coffee.js index 9db8da1481dd..aeb6fa32c8d8 100644 --- a/packages/server/__snapshots__/4_return_value_spec.coffee.js +++ b/packages/server/__snapshots__/4_return_value_spec.coffee.js @@ -18,10 +18,11 @@ exports['e2e return value failing1 1'] = ` 1) errors when invoking commands and return a different value - 2) errors when invoking commands in custom command and returning differnet value + 2) errors when invoking commands in custom command and returning different value + 3) errors when not invoking commands, invoking done callback, and returning a promise 0 passing - 2 failing + 3 failing 1) errors when invoking commands and return a different value: CypressError: Cypress detected that you invoked one or more cy commands but returned a different value. @@ -54,7 +55,7 @@ https://on.cypress.io/returning-value-and-commands-in-test at stack trace line at stack trace line - 2) errors when invoking commands in custom command and returning differnet value: + 2) errors when invoking commands in custom command and returning different value: CypressError: Cypress detected that you invoked one or more cy commands in a custom command but returned a different value. The custom command was: @@ -85,18 +86,34 @@ https://on.cypress.io/returning-value-and-commands-in-custom-command at stack trace line at stack trace line + 3) errors when not invoking commands, invoking done callback, and returning a promise: + Cypress detected that you returned a promise in a test, but also invoked a done callback. Return a promise -or- invoke a done callback, not both. + +Read more here: https://on.cypress.io/returning-promise-and-invoking-done-callback + +----------------------------------------------------------- + +Original mocha error: + +Error: Resolution method is overspecified. Specify a callback *or* return a Promise; not both. + at stack trace line + at stack trace line + Error: Resolution method is overspecified. Specify a callback *or* return a Promise; not both. + at stack trace line + at stack trace line + (Results) ┌────────────────────────────────────────┐ - │ Tests: 2 │ + │ Tests: 3 │ │ Passing: 0 │ - │ Failing: 2 │ + │ Failing: 3 │ │ Pending: 0 │ │ Skipped: 0 │ - │ Screenshots: 2 │ + │ Screenshots: 3 │ │ Video: true │ │ Duration: X seconds │ │ Spec Ran: return_value_spec.coffee │ @@ -106,7 +123,8 @@ https://on.cypress.io/returning-value-and-commands-in-custom-command (Screenshots) - /foo/bar/.projects/e2e/cypress/screenshots/return_value_spec.coffee/errors when invoking commands and return a different value (failed).png (1280x720) - - /foo/bar/.projects/e2e/cypress/screenshots/return_value_spec.coffee/errors when invoking commands in custom command and returning differnet value (failed).png (1280x720) + - /foo/bar/.projects/e2e/cypress/screenshots/return_value_spec.coffee/errors when invoking commands in custom command and returning different value (failed).png (1280x720) + - /foo/bar/.projects/e2e/cypress/screenshots/return_value_spec.coffee/errors when not invoking commands invoking done callback and returning a promise (failed).png (1280x720) (Video) @@ -122,9 +140,9 @@ https://on.cypress.io/returning-value-and-commands-in-custom-command Spec Tests Passing Failing Pending Skipped ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ - │ ✖ return_value_spec.coffee XX:XX 2 - 2 - - │ + │ ✖ return_value_spec.coffee XX:XX 3 - 3 - - │ └────────────────────────────────────────────────────────────────────────────────────────────────┘ - 1 of 1 failed (100%) XX:XX 2 - 2 - - + 1 of 1 failed (100%) XX:XX 3 - 3 - - ` diff --git a/packages/server/test/e2e/3_only_spec.coffee b/packages/server/test/e2e/3_only_spec.coffee index 3540916e29f1..42542b1326fc 100644 --- a/packages/server/test/e2e/3_only_spec.coffee +++ b/packages/server/test/e2e/3_only_spec.coffee @@ -5,7 +5,7 @@ describe "e2e only spec", -> it "failing", -> e2e.exec(@, { - spec: "only_spec.coffee" + spec: "only*.coffee" snapshot: true expectedExitCode: 0 }) diff --git a/packages/server/test/e2e/4_return_value_spec.coffee b/packages/server/test/e2e/4_return_value_spec.coffee index c5c811d47d46..4cfa52092d1a 100644 --- a/packages/server/test/e2e/4_return_value_spec.coffee +++ b/packages/server/test/e2e/4_return_value_spec.coffee @@ -7,5 +7,5 @@ describe "e2e return value", -> e2e.exec(@, { spec: "return_value_spec.coffee" snapshot: true - expectedExitCode: 2 + expectedExitCode: 3 }) diff --git a/packages/server/test/support/fixtures/projects/e2e/cypress/integration/only_multiple_spec.coffee b/packages/server/test/support/fixtures/projects/e2e/cypress/integration/only_multiple_spec.coffee new file mode 100644 index 000000000000..84271b37ff0c --- /dev/null +++ b/packages/server/test/support/fixtures/projects/e2e/cypress/integration/only_multiple_spec.coffee @@ -0,0 +1,13 @@ +it "t1", -> +it "t2", -> +it "t3", -> + +describe "s1", -> + it.only "t3", -> + + it.only "t4" + + it "t5", -> + +describe "s2", -> + it "t3", -> diff --git a/packages/server/test/support/fixtures/projects/e2e/cypress/integration/return_value_spec.coffee b/packages/server/test/support/fixtures/projects/e2e/cypress/integration/return_value_spec.coffee index 1190c7f9a8ec..ca32aaed8db8 100644 --- a/packages/server/test/support/fixtures/projects/e2e/cypress/integration/return_value_spec.coffee +++ b/packages/server/test/support/fixtures/projects/e2e/cypress/integration/return_value_spec.coffee @@ -3,10 +3,14 @@ it "errors when invoking commands and return a different value", -> return [{}, 1, 2, "foo", (->)] -it "errors when invoking commands in custom command and returning differnet value", -> +it "errors when invoking commands in custom command and returning different value", -> Cypress.Commands.add "foo", -> cy.wrap(null) return "bar" cy.foo() + +it "errors when not invoking commands, invoking done callback, and returning a promise", (done) -> + return Promise.resolve(null).then -> + done() From 59ab85b5be846939d69b00759946838a360bf9a3 Mon Sep 17 00:00:00 2001 From: Chris Breiding Date: Tue, 21 May 2019 10:34:37 -0400 Subject: [PATCH 03/49] fix issues with merging mocha upgrade --- cli/__snapshots__/cli_spec.js | 38 +++++++++---------- .../integration/cypress/cypress_spec.coffee | 2 - .../4_return_value_spec.coffee.js | 2 +- 3 files changed, 20 insertions(+), 22 deletions(-) diff --git a/cli/__snapshots__/cli_spec.js b/cli/__snapshots__/cli_spec.js index 010f3fdd84d5..2674080d5c1a 100644 --- a/cli/__snapshots__/cli_spec.js +++ b/cli/__snapshots__/cli_spec.js @@ -51,9 +51,9 @@ exports['cli help command shows help 1'] = ` ------- stderr: ------- - + ------- - + ` exports['cli help command shows help for --help 1'] = ` @@ -84,9 +84,9 @@ exports['cli help command shows help for --help 1'] = ` ------- stderr: ------- - + ------- - + ` exports['cli help command shows help for -h 1'] = ` @@ -117,9 +117,9 @@ exports['cli help command shows help for -h 1'] = ` ------- stderr: ------- - + ------- - + ` exports['cli unknown command shows usage and exits 1'] = ` @@ -146,14 +146,14 @@ exports['cli unknown command shows usage and exits 1'] = ` run [options] Runs Cypress tests from the CLI without the GUI open [options] Opens Cypress in the interactive GUI. install [options] Installs the Cypress executable matching this package's version - verify Verifies that Cypress is installed correctly and executable + verify [options] Verifies that Cypress is installed correctly and executable cache [options] Manages the Cypress binary cache ------- stderr: ------- - + ------- - + ` exports['cli unknown option shows help for cache command - no sub-command 1'] = ` @@ -179,9 +179,9 @@ exports['cli unknown option shows help for cache command - no sub-command 1'] = ------- stderr: ------- - + ------- - + ` exports['cli unknown option shows help for cache command - unknown option --foo 1'] = ` @@ -209,9 +209,9 @@ exports['cli unknown option shows help for cache command - unknown option --foo ------- stderr: ------- - + ------- - + ` exports['cli unknown option shows help for cache command - unknown sub-command foo 1'] = ` @@ -239,9 +239,9 @@ exports['cli unknown option shows help for cache command - unknown sub-command f ------- stderr: ------- - + ------- - + ` exports['cli version and binary version 1'] = ` @@ -289,9 +289,9 @@ exports['shows help for open --foo 1'] = ` ------- stderr: ------- - + ------- - + ` exports['shows help for run --foo 1'] = ` @@ -332,7 +332,7 @@ exports['shows help for run --foo 1'] = ` ------- stderr: ------- - + ------- - + ` diff --git a/packages/driver/test/cypress/integration/cypress/cypress_spec.coffee b/packages/driver/test/cypress/integration/cypress/cypress_spec.coffee index 0ea7e34f4980..e4ed80b4ff55 100644 --- a/packages/driver/test/cypress/integration/cypress/cypress_spec.coffee +++ b/packages/driver/test/cypress/integration/cypress/cypress_spec.coffee @@ -45,8 +45,6 @@ describe "driver/src/cypress/index", -> expect(err.backend).to.be.true expect(err.stack).not.to.include("From previous event") - done() - context ".isCy", -> it "returns true on cy, cy chainable", -> expect(Cypress.isCy(cy)).to.be.true diff --git a/packages/server/__snapshots__/4_return_value_spec.coffee.js b/packages/server/__snapshots__/4_return_value_spec.coffee.js index aeb6fa32c8d8..845bded7fca1 100644 --- a/packages/server/__snapshots__/4_return_value_spec.coffee.js +++ b/packages/server/__snapshots__/4_return_value_spec.coffee.js @@ -124,7 +124,7 @@ Error: Resolution method is overspecified. Specify a callback *or* return a Prom - /foo/bar/.projects/e2e/cypress/screenshots/return_value_spec.coffee/errors when invoking commands and return a different value (failed).png (1280x720) - /foo/bar/.projects/e2e/cypress/screenshots/return_value_spec.coffee/errors when invoking commands in custom command and returning different value (failed).png (1280x720) - - /foo/bar/.projects/e2e/cypress/screenshots/return_value_spec.coffee/errors when not invoking commands invoking done callback and returning a promise (failed).png (1280x720) + - /foo/bar/.projects/e2e/cypress/screenshots/return_value_spec.coffee/errors when not invoking commands, invoking done callback, and returning a promise (failed).png (1280x720) (Video) From ce8f0ee84f01c2f50fe166f642d5e7720b49d864 Mon Sep 17 00:00:00 2001 From: Lila Conlee Date: Mon, 3 Dec 2018 14:33:36 -0500 Subject: [PATCH 04/49] Yield null from cy.writeFile (#2731) - Fixes #2466 - [Docs PR](https://github.com/cypress-io/cypress-documentation/pull/1117) Should be part of 4.0.0 --- packages/driver/src/cy/commands/files.coffee | 5 +---- .../cypress/integration/commands/files_spec.coffee | 10 ++-------- 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/packages/driver/src/cy/commands/files.coffee b/packages/driver/src/cy/commands/files.coffee index 6578adbc4d88..40f7a3009f07 100644 --- a/packages/driver/src/cy/commands/files.coffee +++ b/packages/driver/src/cy/commands/files.coffee @@ -94,16 +94,13 @@ module.exports = (Commands, Cypress, cy, state, config) -> }) if _.isObject(contents) - objContents = contents contents = JSON.stringify(contents, null, 2) Cypress.backend("write:file", fileName, contents, _.pick(options, ["encoding", "flag"])) .then ({ contents, filePath }) -> consoleProps["File Path"] = filePath consoleProps["Contents"] = contents - if objContents? - return objContents - return contents + return null .catch Promise.TimeoutError, (err) -> $utils.throwErrByPath "files.timed_out", { onFail: options._log diff --git a/packages/driver/test/cypress/integration/commands/files_spec.coffee b/packages/driver/test/cypress/integration/commands/files_spec.coffee index b767fbcb5369..b23d0bf22d54 100644 --- a/packages/driver/test/cypress/integration/commands/files_spec.coffee +++ b/packages/driver/test/cypress/integration/commands/files_spec.coffee @@ -286,17 +286,11 @@ describe "src/cy/commands/files", -> } ) - it "sets the contents as the subject", -> + it "yields null", -> Cypress.backend.resolves(okResponse) cy.writeFile("foo.txt", "contents").then (subject) -> - expect(subject).to.equal("contents") - - it "sets a JSON as the subject", -> - Cypress.backend.resolves(okResponse) - - cy.writeFile("foo.json", { name: "Test" }).then (subject) -> - expect(subject.name).to.equal("Test") + expect(subject).to.not.exist it "can write a string", -> Cypress.backend.resolves(okResponse) From 3de8a844c753a2f2dec1dd751be23c41692810cf Mon Sep 17 00:00:00 2001 From: Lila Conlee Date: Tue, 4 Dec 2018 09:54:30 -0500 Subject: [PATCH 05/49] Upgrade Sinon to 7.1.1 (#2881) - Fixes #2866 --- packages/driver/package.json | 2 +- .../test/cypress/integration/commands/angular_spec.coffee | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/driver/package.json b/packages/driver/package.json index 79aa42f3fcd0..29497e7637ab 100644 --- a/packages/driver/package.json +++ b/packages/driver/package.json @@ -55,7 +55,7 @@ "npm-install-version": "6.0.2", "parse-domain": "2.0.0", "setimmediate": "1.0.5", - "sinon": "3.3.0", + "sinon": "7.1.1", "text-mask-addons": "3.8.0", "underscore": "1.9.1", "underscore.string": "3.3.5", diff --git a/packages/driver/test/cypress/integration/commands/angular_spec.coffee b/packages/driver/test/cypress/integration/commands/angular_spec.coffee index abdcb8a9e789..c42d426cfcad 100644 --- a/packages/driver/test/cypress/integration/commands/angular_spec.coffee +++ b/packages/driver/test/cypress/integration/commands/angular_spec.coffee @@ -199,7 +199,7 @@ describe "src/cy/commands/angular", -> ## to retry after the first one resolves cy.ng("model", "missing-input") .then -> - retry.reset() + retry.resetHistory() .wait(100) .then -> expect(retry.callCount).to.eq 0 From 25f2a6d1c9acb200db5fc0016064ee0693f1f6fb Mon Sep 17 00:00:00 2001 From: Lila Conlee Date: Wed, 5 Dec 2018 12:10:01 -0500 Subject: [PATCH 06/49] Upgrade to Chai 4 (#2862) * Initial upgrade changes * Some quick fixes * More fixes * Clean up exp logic and fix spread array failures * Remove caret from package.json * Add handling for proxies in isJquery * iterate on flaky test, increase default command timeout --- packages/driver/package.json | 2 +- packages/driver/src/cy/commands/asserting.coffee | 10 +++++++--- packages/driver/src/cy/commands/connectors.coffee | 5 ++++- .../cypress/integration/commands/agents_spec.coffee | 8 ++++---- .../cypress/integration/commands/aliasing_spec.coffee | 4 ++-- .../integration/commands/assertions_spec.coffee | 8 ++++---- .../integration/commands/navigation_spec.coffee | 4 ++-- .../cypress/integration/commands/querying_spec.coffee | 2 +- .../integration/commands/screenshot_spec.coffee | 2 +- .../test/cypress/integration/cypress/cy_spec.coffee | 8 ++++---- .../cypress/integration/cypress/cypress_spec.coffee | 8 ++++---- 11 files changed, 34 insertions(+), 27 deletions(-) diff --git a/packages/driver/package.json b/packages/driver/package.json index 29497e7637ab..7085f393c38d 100644 --- a/packages/driver/package.json +++ b/packages/driver/package.json @@ -29,7 +29,7 @@ "body-parser": "1.19.0", "bootstrap": "4.3.1", "bytes": "3.1.0", - "chai": "3.5.0", + "chai": "4.2.0", "chai-as-promised": "6.0.0", "chokidar-cli": "1.2.2", "clone": "2.1.2", diff --git a/packages/driver/src/cy/commands/asserting.coffee b/packages/driver/src/cy/commands/asserting.coffee index 386446597410..49dfb3ca738b 100644 --- a/packages/driver/src/cy/commands/asserting.coffee +++ b/packages/driver/src/cy/commands/asserting.coffee @@ -92,7 +92,7 @@ module.exports = (Commands, Cypress, cy, state, config) -> ## are we doing a length assertion? if reHaveLength.test(chainers) or reExistence.test(chainers) - exp.isCheckingExistence = true + isCheckingExistence = true applyChainer = (memo, value) -> if value is lastChainer @@ -108,6 +108,8 @@ module.exports = (Commands, Cypress, cy, state, config) -> throwAndLogErr(err) else throw err + else + memo[value] else memo[value] @@ -118,10 +120,10 @@ module.exports = (Commands, Cypress, cy, state, config) -> ## because its possible we're asserting about an ## element which has left the DOM and we always ## want to auto-fail on those - if not exp.isCheckingExistence and $dom.isElement(subject) + if not isCheckingExistence and $dom.isElement(subject) cy.ensureAttached(subject, "should") - _.reduce chainers, (memo, value) => + newExp = _.reduce chainers, (memo, value) => if value not of memo err = $utils.cypressErr("The chainer: '#{value}' was not found. Could not build assertion.") err.retry = false @@ -131,6 +133,8 @@ module.exports = (Commands, Cypress, cy, state, config) -> , exp + exp = newExp ? exp + Promise.try(applyChainers).then -> ## if the _obj has been mutated then we ## are chaining assertion properties and diff --git a/packages/driver/src/cy/commands/connectors.coffee b/packages/driver/src/cy/commands/connectors.coffee index c7a67c76af03..4308a102132f 100644 --- a/packages/driver/src/cy/commands/connectors.coffee +++ b/packages/driver/src/cy/commands/connectors.coffee @@ -56,7 +56,10 @@ module.exports = (Commands, Cypress, cy, state, config) -> remoteSubject = cy.getRemotejQueryInstance(subject) args = remoteSubject or subject - args = if subject?._spreadArray then args else [args] + + try + hasSpreadArray = "_spreadArray" in subject or subject?._spreadArray + args = if hasSpreadArray then args else [args] ## name could be invoke or its! name = state("current").get("name") diff --git a/packages/driver/test/cypress/integration/commands/agents_spec.coffee b/packages/driver/test/cypress/integration/commands/agents_spec.coffee index 2bd78d910c5b..288996d432bc 100644 --- a/packages/driver/test/cypress/integration/commands/agents_spec.coffee +++ b/packages/driver/test/cypress/integration/commands/agents_spec.coffee @@ -241,7 +241,7 @@ describe "src/cy/commands/agents", -> it "retries until assertions pass", -> cy.on "command:retry", _.after 2, => @myStub("foo") - + cy.get("@myStub").should("be.calledWith", "foo") describe "errors", -> @@ -255,7 +255,7 @@ describe "src/cy/commands/agents", -> _.each ["test", "runnable", "timeout", "slow", "skip", "inspect"], (blacklist) -> it "throws on a blacklisted word: #{blacklist}", -> expect(=> cy.stub().as(blacklist)).to.throw("cy.as() cannot be aliased as: '#{blacklist}'. This word is reserved.") - + context "with dots", -> beforeEach -> @logs = [] @@ -296,7 +296,7 @@ describe "src/cy/commands/agents", -> it "retries until assertions pass", -> cy.on "command:retry", _.after 2, => @["my.stub"]("foo") - + cy.get("@my.stub").should("be.calledWith", "foo") describe "errors", -> @@ -524,4 +524,4 @@ describe "src/cy/commands/agents", -> expect(@agents.spy).to.be.a("function") expect(@agents.spy().callCount).to.be.a("number") expect(@agents.stub).to.be.a("function") - expect(@agents.stub().returns).to.be.a("function") \ No newline at end of file + expect(@agents.stub().returns).to.be.a("function") diff --git a/packages/driver/test/cypress/integration/commands/aliasing_spec.coffee b/packages/driver/test/cypress/integration/commands/aliasing_spec.coffee index 8e4b2fb84c29..6da431d9da6d 100644 --- a/packages/driver/test/cypress/integration/commands/aliasing_spec.coffee +++ b/packages/driver/test/cypress/integration/commands/aliasing_spec.coffee @@ -27,7 +27,7 @@ describe "src/cy/commands/aliasing", -> it "stores the lookup as an alias", -> cy.get("body").as("b").then -> - expect(cy.state("aliases").b).to.be.defined + expect(cy.state("aliases").b).to.exist it "stores the resulting subject as the alias", -> $body = cy.$$("body") @@ -356,7 +356,7 @@ describe "src/cy/commands/aliasing", -> .get("input:first").as("firstInput") .get("div:last").as("lastDiv") .then -> - expect(cy.getAlias("@firstInput")).to.be.defined + expect(cy.getAlias("@firstInput")).to.exist describe "errors", -> it "throws when an alias cannot be found", (done) -> diff --git a/packages/driver/test/cypress/integration/commands/assertions_spec.coffee b/packages/driver/test/cypress/integration/commands/assertions_spec.coffee index 836dfe45612b..1a70429b3ced 100644 --- a/packages/driver/test/cypress/integration/commands/assertions_spec.coffee +++ b/packages/driver/test/cypress/integration/commands/assertions_spec.coffee @@ -598,7 +598,7 @@ describe "src/cy/commands/assertions", -> expect(log.invoke("consoleProps")).to.deep.eq { Command: "assert" subject: log.get("subject") - Message: "expected to have a property length" + Message: "expected to have property length" } done() @@ -705,7 +705,7 @@ describe "src/cy/commands/assertions", -> if attrs.name is "assert" cy.removeAllListeners("log:added") - expect(log.get("message")).to.eq "expected **\ `).appendTo(cy.$$('body')) + cy.get('#whitespace1').contains('White space') cy.contains('White space').then(($btn) => { expect($btn.get(0)).to.eq(btn.get(0)) }) }) + + it('finds el with new lines + spaces', () => { + const btn = $(`\ +\ +`).appendTo(cy.$$('body')) + + cy.get('#whitespace2').contains('White space') + cy.contains('White space').then(($btn) => { + expect($btn.get(0)).to.eq(btn.get(0)) + }) + }) + + it('finds el with multiple spaces', () => { + const btn = $(`\ +\ +`).appendTo(cy.$$('body')) + + cy.get('#whitespace3').contains('White space') + cy.contains('White space').then(($btn) => { + expect($btn.get(0)).to.eq(btn.get(0)) + }) + }) + + it('finds el with regex', () => { + const btn = $(`\ +\ +`).appendTo(cy.$$('body')) + + cy.get('#whitespace4').contains('White space') + cy.contains(/White space/).then(($btn) => { + expect($btn.get(0)).to.eq(btn.get(0)) + }) + }) + + it('does not normalize text in pre tag', () => { + $(`\ +
+White
+space
+
\ +`).appendTo(cy.$$('body')) + + cy.contains('White space').should('not.match', 'pre') + cy.get('#whitespace5').contains('White\nspace') + }) + + it('finds el with leading/trailing spaces', () => { + const btn = $(``).appendTo(cy.$$('body')) + + cy.get('#whitespace6').contains('White space') + cy.contains('White space').then(($btn) => { + expect($btn.get(0)).to.eq(btn.get(0)) + }) + }) + }) + + describe('case sensitivity', () => { + beforeEach(() => { + $('').appendTo(cy.$$('body')) + }) + + it('is case sensitive when matchCase is undefined', () => { + cy.get('#test-button').contains('Test') + }) + + it('is case sensitive when matchCase is true', () => { + cy.get('#test-button').contains('Test', { + matchCase: true, + }) + }) + + it('is case insensitive when matchCase is false', () => { + cy.get('#test-button').contains('test', { + matchCase: false, + }) + + cy.get('#test-button').contains(/Test/, { + matchCase: false, + }) + }) + + it('does not crash when matchCase: false is used with regex flag, i', () => { + cy.get('#test-button').contains(/Test/i, { + matchCase: false, + }) + }) + + it('throws when content has "i" flag while matchCase: true', (done) => { + cy.on('fail', (err) => { + expect(err.message).to.eq('You passed a regular expression with the case-insensitive (i) flag and { matchCase: true } to cy.contains(). Those options conflict with each other, so please choose one or the other.') + + done() + }) + + cy.get('#test-button').contains(/Test/i, { + matchCase: true, + }) + }) + + it('passes when "i" flag is used with undefined option', () => { + cy.get('#test-button').contains(/Test/i) + }) }) describe('subject contains text nodes', () => { From 1086e78e4b0c64496ac1fb9753512e6ae4acc92e Mon Sep 17 00:00:00 2001 From: Chris Breiding Date: Thu, 30 Jan 2020 13:21:41 -0500 Subject: [PATCH 36/49] v4.0 latest deps (#6274) * upgrade mocha to 7.0.1 * enableTimeouts: false -> timeout: false * upgrade sinon to 8.1.1 * sinon.sandbox.create -> sinon.createSandbox --- packages/driver/package.json | 4 ++-- packages/driver/src/cy/commands/agents.coffee | 2 +- packages/driver/src/cypress/mocha.js | 2 +- packages/driver/test/support/unit_spec_helper.coffee | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/driver/package.json b/packages/driver/package.json index f7ae810c3a5b..69328c7adcf6 100644 --- a/packages/driver/package.json +++ b/packages/driver/package.json @@ -50,13 +50,13 @@ "methods": "1.1.2", "minimatch": "3.0.4", "minimist": "1.2.0", - "mocha": "6.2.2", + "mocha": "7.0.1", "moment": "2.24.0", "morgan": "1.9.1", "npm-install-version": "6.0.2", "parse-domain": "bahmutov/parse-domain#fb60bd4", "setimmediate": "1.0.5", - "sinon": "7.5.0", + "sinon": "8.1.1", "text-mask-addons": "3.8.0", "underscore": "1.9.1", "underscore.string": "3.3.5", diff --git a/packages/driver/src/cy/commands/agents.coffee b/packages/driver/src/cy/commands/agents.coffee index 1705fba25dbc..7816f2111655 100644 --- a/packages/driver/src/cy/commands/agents.coffee +++ b/packages/driver/src/cy/commands/agents.coffee @@ -10,7 +10,7 @@ counts = null sinon.setFormatter($utils.stringifyArg.bind($utils)) createSandbox = -> - sinon.sandbox.create().usingPromise(Promise) + sinon.createSandbox().usingPromise(Promise) display = (name) -> switch name diff --git a/packages/driver/src/cypress/mocha.js b/packages/driver/src/cypress/mocha.js index 0ec6450c1325..580405f51623 100644 --- a/packages/driver/src/cypress/mocha.js +++ b/packages/driver/src/cypress/mocha.js @@ -63,7 +63,7 @@ const globals = (specWindow, reporter) => { const _mocha = new Mocha({ reporter, - enableTimeouts: false, + timeout: false, }) // set mocha props on the specWindow diff --git a/packages/driver/test/support/unit_spec_helper.coffee b/packages/driver/test/support/unit_spec_helper.coffee index 4688f699a9cc..87bbda16692e 100644 --- a/packages/driver/test/support/unit_spec_helper.coffee +++ b/packages/driver/test/support/unit_spec_helper.coffee @@ -18,7 +18,7 @@ global.window = j.window global.document = window.document beforeEach -> - @sandbox = sinon.sandbox.create() + @sandbox = sinon.createSandbox() afterEach -> @sandbox.restore() From 0257b0a05c6c374e438e6da30a2f2615ead72821 Mon Sep 17 00:00:00 2001 From: Chris Breiding Date: Thu, 30 Jan 2020 15:31:42 -0500 Subject: [PATCH 37/49] fix flaky e2e test something is wrong with coffeescript compiling, where a syntax error in the test file also gets thrown as an error in the support file, but inconsistently, causing this e2e test snapshot to be flaky. solution for now is to avoid coffeescript for that test file. --- .../__snapshots__/5_stdout_spec.coffee.js | 39 ++++++------------- packages/server/test/e2e/5_stdout_spec.coffee | 2 +- .../stdout_exit_early_failing_spec.coffee | 1 - .../stdout_exit_early_failing_spec.js | 1 + 4 files changed, 13 insertions(+), 30 deletions(-) delete mode 100644 packages/server/test/support/fixtures/projects/e2e/cypress/integration/stdout_exit_early_failing_spec.coffee create mode 100644 packages/server/test/support/fixtures/projects/e2e/cypress/integration/stdout_exit_early_failing_spec.js diff --git a/packages/server/__snapshots__/5_stdout_spec.coffee.js b/packages/server/__snapshots__/5_stdout_spec.coffee.js index d133d39959de..14f2c4f3be8b 100644 --- a/packages/server/__snapshots__/5_stdout_spec.coffee.js +++ b/packages/server/__snapshots__/5_stdout_spec.coffee.js @@ -121,42 +121,26 @@ exports['e2e stdout displays errors from exiting early due to bundle errors 1'] ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ │ Cypress: 1.2.3 │ │ Browser: FooBrowser 88 │ - │ Specs: 1 found (stdout_exit_early_failing_spec.coffee) │ - │ Searched: cypress/integration/stdout_exit_early_failing_spec.coffee │ + │ Specs: 1 found (stdout_exit_early_failing_spec.js) │ + │ Searched: cypress/integration/stdout_exit_early_failing_spec.js │ └────────────────────────────────────────────────────────────────────────────────────────────────┘ ──────────────────────────────────────────────────────────────────────────────────────────────────── - Running: stdout_exit_early_failing_spec.coffee (1 of 1) + Running: stdout_exit_early_failing_spec.js (1 of 1) Oops...we found an error preparing this test file: - /foo/bar/.projects/e2e/cypress/integration/stdout_exit_early_failing_spec.coffee + /foo/bar/.projects/e2e/cypress/integration/stdout_exit_early_failing_spec.js The error was: -/foo/bar/.projects/e2e/cypress/integration/stdout_exit_early_failing_spec.coffee:1 -+> - ^ -ParseError: unexpected > +SyntaxError: /foo/bar/.projects/e2e/cypress/integration/stdout_exit_early_failing_spec.js: Unexpected token (1:1) -This occurred while Cypress was compiling and bundling your test code. This is usually caused by: - -- A missing file or dependency -- A syntax error in the file or one of its dependencies - -Fix the error in your code and re-run your tests. - -Oops...we found an error preparing this test file: - - /foo/bar/.projects/e2e/cypress/support/index.js - -The error was: - -:1:2: error: unexpected > -+> - ^ +> 1 | +> + | ^ + 2 | while parsing file: /foo/bar/.projects/e2e/cypress/integration/stdout_exit_early_failing_spec.js This occurred while Cypress was compiling and bundling your test code. This is usually caused by: @@ -176,7 +160,7 @@ Fix the error in your code and re-run your tests. │ Screenshots: 0 │ │ Video: true │ │ Duration: X seconds │ - │ Spec Ran: stdout_exit_early_failing_spec.coffee │ + │ Spec Ran: stdout_exit_early_failing_spec.js │ └────────────────────────────────────────────────────────────────────────────────────────────────┘ @@ -184,7 +168,7 @@ Fix the error in your code and re-run your tests. - Started processing: Compressing to 32 CRF - Finished processing: /XXX/XXX/XXX/cypress/videos/stdout_exit_early_failing_spec. (X second) - coffee.mp4 + js.mp4 ==================================================================================================== @@ -194,8 +178,7 @@ Fix the error in your code and re-run your tests. Spec Tests Passing Failing Pending Skipped ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ - │ ✖ stdout_exit_early_failing_spec.coff XX:XX - - 1 - - │ - │ ee │ + │ ✖ stdout_exit_early_failing_spec.js XX:XX - - 1 - - │ └────────────────────────────────────────────────────────────────────────────────────────────────┘ ✖ 1 of 1 failed (100%) XX:XX - - 1 - - diff --git a/packages/server/test/e2e/5_stdout_spec.coffee b/packages/server/test/e2e/5_stdout_spec.coffee index 4ff8f937aee5..6e1e00a45d13 100644 --- a/packages/server/test/e2e/5_stdout_spec.coffee +++ b/packages/server/test/e2e/5_stdout_spec.coffee @@ -14,7 +14,7 @@ describe "e2e stdout", -> it "displays errors from exiting early due to bundle errors", -> e2e.exec(@, { - spec: "stdout_exit_early_failing_spec.coffee" + spec: "stdout_exit_early_failing_spec.js" snapshot: true expectedExitCode: 1 }) diff --git a/packages/server/test/support/fixtures/projects/e2e/cypress/integration/stdout_exit_early_failing_spec.coffee b/packages/server/test/support/fixtures/projects/e2e/cypress/integration/stdout_exit_early_failing_spec.coffee deleted file mode 100644 index 935dac2a48dc..000000000000 --- a/packages/server/test/support/fixtures/projects/e2e/cypress/integration/stdout_exit_early_failing_spec.coffee +++ /dev/null @@ -1 +0,0 @@ -+> \ No newline at end of file diff --git a/packages/server/test/support/fixtures/projects/e2e/cypress/integration/stdout_exit_early_failing_spec.js b/packages/server/test/support/fixtures/projects/e2e/cypress/integration/stdout_exit_early_failing_spec.js new file mode 100644 index 000000000000..c1b229d042f5 --- /dev/null +++ b/packages/server/test/support/fixtures/projects/e2e/cypress/integration/stdout_exit_early_failing_spec.js @@ -0,0 +1 @@ ++> From ab7811767b6165c80432b1d597edc52722f7936a Mon Sep 17 00:00:00 2001 From: Chris Breiding Date: Thu, 30 Jan 2020 15:58:26 -0500 Subject: [PATCH 38/49] have eslint ignore file with intentional syntax error --- .eslintignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.eslintignore b/.eslintignore index 45d0e83020f6..bd6ecdce4ebd 100644 --- a/.eslintignore +++ b/.eslintignore @@ -25,6 +25,7 @@ packages/extension/test/helpers/background.js packages/server/lib/scaffold/plugins/index.js packages/server/lib/scaffold/support/index.js packages/server/lib/scaffold/support/commands.js +packages/server/test/support/fixtures/projects/e2e/cypress/integration/stdout_exit_early_failing_spec.js packages/launcher/lib/**/*.js From 0e82cde8d94317f4f23037892877ddd2618023d6 Mon Sep 17 00:00:00 2001 From: Zach Bloomquist Date: Fri, 31 Jan 2020 12:58:12 -0500 Subject: [PATCH 39/49] Update browser types (#6254) * update browser types and list * add todo * first pass of changes * remove 'server' mode, since it is unused * use foundbrowser type for eletron definition * fix server unit tests * update --browser arg to allow name:channel * improve backwards compatibility impl * update 3_config_spec snapshot * fix 2_headless_spec * fix 5_stdout_spec * update browser icon stuff * update 5_spec_isolation_spec snapshot * Chrome Canary => Canary * update user-facing types * fix displayName type * add debug logs * fix cypress_spec * taps * update config.json --- cli/types/index.d.ts | 24 ++++++- .../cypress/fixtures/browsers.json | 8 +-- .../desktop-gui/cypress/fixtures/config.json | 38 +++++------ .../cypress/integration/project_nav_spec.js | 4 +- .../desktop-gui/src/project-nav/browsers.jsx | 2 +- .../__snapshots__/browsers_spec.ts.js | 11 ++-- packages/launcher/lib/browsers.ts | 12 ++-- packages/launcher/lib/detect.ts | 1 + packages/launcher/lib/types.ts | 30 ++++++--- packages/runner/src/errors/no-automation.jsx | 4 +- .../runner/src/errors/no-automation.spec.jsx | 16 ++--- .../__snapshots__/3_config_spec.coffee.js | 2 +- .../__snapshots__/3_plugins_spec.coffee.js | 2 +- .../5_spec_isolation_spec.coffee.js | 8 +-- .../__snapshots__/validation_spec.coffee.js | 6 +- packages/server/lib/browsers/index.js | 66 ++++++++++++++----- .../lib/browsers/{utils.js => utils.ts} | 12 ++-- packages/server/lib/cypress.js | 45 ------------- packages/server/lib/gui/events.coffee | 2 +- packages/server/lib/modes/run.js | 24 ++++--- packages/server/lib/open_project.js | 2 +- packages/server/lib/util/validation.js | 3 +- .../test/e2e/4_browser_path_spec.coffee | 4 +- .../server/test/integration/cypress_spec.js | 21 +++--- .../integration/config_passing_spec.coffee | 2 +- .../e2e/cypress/integration/headless_spec.js | 4 +- .../projects/e2e/cypress/support/index.js | 2 +- .../cypress/plugins/index.coffee | 2 +- .../cypress/plugins.js | 2 +- packages/server/test/support/helpers/e2e.js | 10 +-- .../test/unit/browsers/browsers_spec.coffee | 11 ++-- packages/server/test/unit/config_spec.coffee | 6 +- .../server/test/unit/gui/events_spec.coffee | 4 +- packages/server/test/unit/modes/run_spec.js | 10 +-- .../server/test/unit/validation_spec.coffee | 4 +- .../cypress/integration/browser-icon_spec.jsx | 5 ++ packages/ui-components/src/browser-icon.jsx | 2 + 37 files changed, 232 insertions(+), 179 deletions(-) rename packages/server/lib/browsers/{utils.js => utils.ts} (92%) diff --git a/cli/types/index.d.ts b/cli/types/index.d.ts index e3a19378fc59..569df10816be 100644 --- a/cli/types/index.d.ts +++ b/cli/types/index.d.ts @@ -62,12 +62,32 @@ declare namespace Cypress { password: string } + type BrowserName = 'electron' | 'chrome' | 'chromium' | string + + type BrowserChannel = 'stable' | 'canary' | 'beta' | 'dev' | string + + type BrowserFamily = 'chromium' + /** * Describes a browser Cypress can control */ interface Browser { - name: "electron" | "chrome" | "canary" | "chromium" | "firefox" - displayName: "Electron" | "Chrome" | "Canary" | "Chromium" | "FireFox" + /** + * Short browser name. + */ + name: BrowserName + /** + * The underlying engine for this browser. + */ + family: BrowserFamily + /** + * The release channel of the browser. + */ + channel: BrowserChannel + /** + * Human-readable browser name. + */ + displayName: string version: string majorVersion: number path: string diff --git a/packages/desktop-gui/cypress/fixtures/browsers.json b/packages/desktop-gui/cypress/fixtures/browsers.json index 66baf4b10e8c..ab23c806ec86 100644 --- a/packages/desktop-gui/cypress/fixtures/browsers.json +++ b/packages/desktop-gui/cypress/fixtures/browsers.json @@ -2,7 +2,7 @@ { "name": "chrome", "displayName": "Chrome", - "family": "chrome", + "family": "chromium", "version": "50.0.2661.86", "path": "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", "majorVersion": "50" @@ -10,15 +10,15 @@ { "name": "chromium", "displayName": "Chromium", - "family": "chrome", + "family": "chromium", "version": "49.0.2609.0", "path": "/Users/bmann/Downloads/chrome-mac/Chromium.app/Contents/MacOS/Chromium", "majorVersion": "49" }, { - "name": "canary", + "name": "chrome", "displayName": "Canary", - "family": "chrome", + "family": "chromium", "version": "48.0", "path": "/Users/bmann/Downloads/chrome-mac/Canary.app/Contents/MacOS/Canary", "majorVersion": "48" diff --git a/packages/desktop-gui/cypress/fixtures/config.json b/packages/desktop-gui/cypress/fixtures/config.json index 122ec633561e..f55c7483b7df 100644 --- a/packages/desktop-gui/cypress/fixtures/config.json +++ b/packages/desktop-gui/cypress/fixtures/config.json @@ -6,7 +6,7 @@ { "name": "chrome", "displayName": "Chrome", - "family": "chrome", + "family": "chromium", "version": "78.0.3904.108", "path": "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", "majorVersion": "78" @@ -14,15 +14,15 @@ { "name": "chromium", "displayName": "Chromium", - "family": "chrome", + "family": "chromium", "version": "74.0.3729.0", "path": "/Applications/Chromium.app/Contents/MacOS/Chromium", "majorVersion": "74" }, { - "name": "canary", + "name": "chrome", "displayName": "Canary", - "family": "chrome", + "family": "chromium", "version": "80.0.3977.4", "path": "/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary", "majorVersion": "80" @@ -30,31 +30,31 @@ { "name": "edge", "displayName": "Edge", - "family": "chrome", + "family": "chromium", "version": "79.0.309.71", "path": "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge", "majorVersion": "79" }, { - "name": "edgeBeta", + "name": "edge", "displayName": "Edge Beta", - "family": "chrome", + "family": "chromium", "version": "79.0.309.71", "path": "/Applications/Microsoft Edge Beta.app/Contents/MacOS/Microsoft Edge Beta", "majorVersion": "79" }, { - "name": "edgeCanary", + "name": "edge", "displayName": "Edge Canary", - "family": "chrome", + "family": "chromium", "version": "79.0.309.71", "path": "/Applications/Microsoft Edge Canary.app/Contents/MacOS/Microsoft Edge Canary", "majorVersion": "79" }, { - "name": "edgeDev", + "name": "edge", "displayName": "Edge Dev", - "family": "chrome", + "family": "chromium", "version": "79.0.309.71", "path": "/Applications/Microsoft Edge Dev.app/Contents/MacOS/Microsoft Edge Dev", "majorVersion": "79" @@ -62,7 +62,7 @@ { "name": "electron", "displayName": "Electron", - "family": "electron", + "family": "chromium", "version": "73.0.3683.121", "path": "", "majorVersion": "73", @@ -77,7 +77,7 @@ "majorVersion": "69" }, { - "name": "firefoxDeveloperEdition", + "name": "firefox", "displayName": "Firefox Developer Edition", "family": "firefox", "version": "69.0.1", @@ -85,7 +85,7 @@ "majorVersion": "69" }, { - "name": "firefoxNightly", + "name": "firefox", "displayName": "Firefox Nightly", "family": "firefox", "version": "69.0.1", @@ -259,7 +259,7 @@ { "name": "chrome", "displayName": "Chrome", - "family": "chrome", + "family": "chromium", "version": "50.0.2661.86", "path": "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", "majorVersion": "50" @@ -267,22 +267,22 @@ { "name": "chromium", "displayName": "Chromium", - "family": "chrome", + "family": "chromium", "version": "49.0.2609.0", "path": "/Users/bmann/Downloads/chrome-mac/Chromium.app/Contents/MacOS/Chromium", "majorVersion": "49" }, { - "name": "canary", + "name": "chrome", "displayName": "Canary", - "family": "chrome", + "family": "chromium", "version": "48.0", "path": "/Users/bmann/Downloads/chrome-mac/Canary.app/Contents/MacOS/Canary", "majorVersion": "48" }, { "name": "electron", - "family": "electron", + "family": "chromium", "displayName": "Electron", "path": "", "version": "99.101.1234", diff --git a/packages/desktop-gui/cypress/integration/project_nav_spec.js b/packages/desktop-gui/cypress/integration/project_nav_spec.js index bc6b3d2c6195..4ff13c741eb2 100644 --- a/packages/desktop-gui/cypress/integration/project_nav_spec.js +++ b/packages/desktop-gui/cypress/integration/project_nav_spec.js @@ -256,7 +256,7 @@ describe('Project Nav', function () { ]) expect(browserArg.path).to.include('/') - expect(browserArg.family).to.equal('chrome') + expect(browserArg.family).to.equal('chromium') }) describe('stop browser', function () { @@ -370,7 +370,7 @@ describe('Project Nav', function () { { 'name': 'chromium', 'displayName': 'Chromium', - 'family': 'chrome', + 'family': 'chromium', 'version': '49.0.2609.0', 'path': '/Users/bmann/Downloads/chrome-mac/Chromium.app/Contents/MacOS/Chromium', 'majorVersion': '49', diff --git a/packages/desktop-gui/src/project-nav/browsers.jsx b/packages/desktop-gui/src/project-nav/browsers.jsx index 85a414e58f40..35c4171f460d 100644 --- a/packages/desktop-gui/src/project-nav/browsers.jsx +++ b/packages/desktop-gui/src/project-nav/browsers.jsx @@ -66,7 +66,7 @@ export default class Browsers extends Component { icon = prefixText = 'Running' } else { - icon = + icon = prefixText = '' } diff --git a/packages/launcher/__snapshots__/browsers_spec.ts.js b/packages/launcher/__snapshots__/browsers_spec.ts.js index 95a309d3ef50..822f7804f2d1 100644 --- a/packages/launcher/__snapshots__/browsers_spec.ts.js +++ b/packages/launcher/__snapshots__/browsers_spec.ts.js @@ -1,7 +1,8 @@ exports['browsers returns the expected list of browsers 1'] = [ { "name": "chrome", - "family": "chrome", + "family": "chromium", + "channel": "stable", "displayName": "Chrome", "versionRegex": {}, "profile": true, @@ -13,7 +14,8 @@ exports['browsers returns the expected list of browsers 1'] = [ }, { "name": "chromium", - "family": "chrome", + "family": "chromium", + "channel": "dev", "displayName": "Chromium", "versionRegex": {}, "profile": true, @@ -23,8 +25,9 @@ exports['browsers returns the expected list of browsers 1'] = [ ] }, { - "name": "canary", - "family": "chrome", + "name": "chrome", + "family": "chromium", + "channel": "canary", "displayName": "Canary", "versionRegex": {}, "profile": true, diff --git a/packages/launcher/lib/browsers.ts b/packages/launcher/lib/browsers.ts index b9ae258bae8d..85d4aa2d5a60 100644 --- a/packages/launcher/lib/browsers.ts +++ b/packages/launcher/lib/browsers.ts @@ -6,7 +6,8 @@ import { Browser, FoundBrowser } from './types' export const browsers: Browser[] = [ { name: 'chrome', - family: 'chrome', + family: 'chromium', + channel: 'stable', displayName: 'Chrome', versionRegex: /Google Chrome (\S+)/, profile: true, @@ -14,15 +15,18 @@ export const browsers: Browser[] = [ }, { name: 'chromium', - family: 'chrome', + family: 'chromium', + // technically Chromium is always in development + channel: 'stable', displayName: 'Chromium', versionRegex: /Chromium (\S+)/, profile: true, binary: ['chromium-browser', 'chromium'], }, { - name: 'canary', - family: 'chrome', + name: 'chrome', + family: 'chromium', + channel: 'canary', displayName: 'Canary', versionRegex: /Google Chrome Canary (\S+)/, profile: true, diff --git a/packages/launcher/lib/detect.ts b/packages/launcher/lib/detect.ts index 12caecf7fe4f..5083100256b7 100644 --- a/packages/launcher/lib/detect.ts +++ b/packages/launcher/lib/detect.ts @@ -93,6 +93,7 @@ function checkOneBrowser (browser: Browser): Promise { const pickBrowserProps = pick([ 'name', 'family', + 'channel', 'displayName', 'type', 'version', diff --git a/packages/launcher/lib/types.ts b/packages/launcher/lib/types.ts index 1aca1ebec540..b89f390c3219 100644 --- a/packages/launcher/lib/types.ts +++ b/packages/launcher/lib/types.ts @@ -1,9 +1,13 @@ import { ChildProcess } from 'child_process' import * as Bluebird from 'bluebird' -export type BrowserName = 'chrome' | 'chromium' | 'canary' | string +// TODO: some of these types can be combined with cli/types/index.d.ts -export type BrowserFamily = 'chrome' | 'electron' +export type BrowserName = 'electron' | 'chrome' | 'chromium' | string + +export type BrowserChannel = 'stable' | 'canary' | 'beta' | 'dev' | string + +export type BrowserFamily = 'chromium' export type PlatformName = 'darwin' | 'linux' | 'win32' @@ -11,14 +15,25 @@ export type PlatformName = 'darwin' | 'linux' | 'win32' * Represents a typical browser to try to detect and turn into a `FoundBrowser`. */ export type Browser = { - /** short browser name */ + /** + * Short browser name. + */ name: BrowserName + /** + * The underlying engine for this browser. + */ family: BrowserFamily - /** Optional display name */ + /** + * The release channel of the browser. + */ + channel: BrowserChannel + /** + * Human-readable browser name. + */ displayName: string /** RegExp to use to extract version from something like "Google Chrome 58.0.3029.110" */ versionRegex: RegExp - profile: boolean + profile?: boolean /** A single binary name or array of binary names for this browser. Not used on Windows. */ binary: string | string[] } @@ -26,12 +41,11 @@ export type Browser = { /** * Represents a real browser that exists on the user's system. */ -export type FoundBrowser = Browser & { - name: string +export type FoundBrowser = Omit & { path: string version: string majorVersion?: string - /** user-supplied browser? */ + /** is this a user-supplied browser? */ custom?: boolean /** optional info that will be shown in the GUI */ info?: string diff --git a/packages/runner/src/errors/no-automation.jsx b/packages/runner/src/errors/no-automation.jsx index 4802996aa830..c9e0e27e2577 100644 --- a/packages/runner/src/errors/no-automation.jsx +++ b/packages/runner/src/errors/no-automation.jsx @@ -22,8 +22,8 @@ const noBrowsers = () => ( const browser = (browser) => ( - - Run {displayName(browser.name)} {browser.majorVersion} + + Run {displayName(browser.displayName)} {browser.majorVersion} ) diff --git a/packages/runner/src/errors/no-automation.spec.jsx b/packages/runner/src/errors/no-automation.spec.jsx index 280d599d52d6..572653c20ff9 100644 --- a/packages/runner/src/errors/no-automation.spec.jsx +++ b/packages/runner/src/errors/no-automation.spec.jsx @@ -7,14 +7,14 @@ import NoAutomation from './no-automation' const noBrowsers = [] const browsersWithChosen = [ - { name: 'canary', version: '52.7', majorVersion: 52 }, - { name: 'chrome', version: '52.2', majorVersion: 52, default: true }, - { name: 'chromium', version: '53.2', majorVersion: 53 }, + { name: 'canary', displayName: 'Canary', version: '52.7', majorVersion: 52 }, + { name: 'chrome', displayName: 'Chrome', version: '52.2', majorVersion: 52, default: true }, + { name: 'chromium', displayName: 'Chromium', version: '53.2', majorVersion: 53 }, ] const browsersWithoutChosen = [ - { name: 'canary', version: '52.7', majorVersion: 52 }, - { name: 'chrome', version: '52.2', majorVersion: 52 }, - { name: 'chromium', version: '53.2', majorVersion: 53 }, + { name: 'canary', displayName: 'Canary', version: '52.7', majorVersion: 52 }, + { name: 'chrome', displayName: 'Chrome', version: '52.2', majorVersion: 52 }, + { name: 'chromium', displayName: 'Chromium', version: '53.2', majorVersion: 53 }, ] describe('', () => { @@ -44,11 +44,11 @@ describe('', () => { expect(component.find('Dropdown').prop('others')[1]).to.eql(_.extend({}, browsersWithChosen[2], { key: 'chromium53.2' })) }) - it('renders browser in with icon based on browser name', () => { + it('renders browser in with icon based on browser displayName', () => { const component = shallow() const browser = shallow(component.find('Dropdown').prop('renderItem')(browsersWithChosen[0])) - expect(browser.find('BrowserIcon').prop('browserName')).to.equal('canary') + expect(browser.find('BrowserIcon').prop('browserName')).to.equal('Canary') }) it('renders browser in with browser name and version', () => { diff --git a/packages/server/__snapshots__/3_config_spec.coffee.js b/packages/server/__snapshots__/3_config_spec.coffee.js index 7fad62eb5be1..f018a48397d5 100644 --- a/packages/server/__snapshots__/3_config_spec.coffee.js +++ b/packages/server/__snapshots__/3_config_spec.coffee.js @@ -149,6 +149,6 @@ Expected \`viewportWidth\` to be a number. Instead the value was: \`"foo"\` exports['e2e config catches invalid browser in the configuration file 1'] = ` We found an invalid value in the file: \`cypress.json\` -Found an error while validating the \`browsers\` list. Expected \`family\` to be either electron, chrome or firefox. Instead the value was: \`{"name":"bad browser","family":"unknown family","displayName":"Bad browser","version":"no version","path":"/path/to","majorVersion":123}\` +Found an error while validating the \`browsers\` list. Expected \`family\` to be either electron, chromium or firefox. Instead the value was: \`{"name":"bad browser","family":"unknown family","displayName":"Bad browser","version":"no version","path":"/path/to","majorVersion":123}\` ` diff --git a/packages/server/__snapshots__/3_plugins_spec.coffee.js b/packages/server/__snapshots__/3_plugins_spec.coffee.js index 49a7aa32b6fb..c18d6f8502ec 100644 --- a/packages/server/__snapshots__/3_plugins_spec.coffee.js +++ b/packages/server/__snapshots__/3_plugins_spec.coffee.js @@ -399,6 +399,6 @@ Expected at list one browser exports['e2e plugins catches invalid browser returned from plugins 1'] = ` An invalid configuration value returned from the plugins file: \`cypress/plugins/index.coffee\` -Found an error while validating the \`browsers\` list. Expected \`displayName\` to be a non-empty string. Instead the value was: \`{"name":"browser name","family":"chrome"}\` +Found an error while validating the \`browsers\` list. Expected \`displayName\` to be a non-empty string. Instead the value was: \`{"name":"browser name","family":"chromium"}\` ` diff --git a/packages/server/__snapshots__/5_spec_isolation_spec.coffee.js b/packages/server/__snapshots__/5_spec_isolation_spec.coffee.js index 07e66fbb4653..786ac5fc12bf 100644 --- a/packages/server/__snapshots__/5_spec_isolation_spec.coffee.js +++ b/packages/server/__snapshots__/5_spec_isolation_spec.coffee.js @@ -39,7 +39,7 @@ exports['e2e spec isolation fails'] = { "title": [ "\"before all\" hook" ], - "body": "function () {\n if (Cypress.browser.family === 'chrome') {\n return Cypress.automation('remote:debugger:protocol', {\n command: 'Emulation.setDeviceMetricsOverride',\n params: {\n width: 1280,\n height: 720,\n deviceScaleFactor: 1,\n mobile: false,\n screenWidth: 1280,\n screenHeight: 720\n }\n }).then(function () {\n // can't tell expect() not to log, so manually throwing here\n if (window.devicePixelRatio !== 1) {\n throw new Error('Setting devicePixelRatio to 1 failed');\n }\n });\n }\n}" + "body": "function () {\n if (Cypress.browser.family === 'chromium' && Cypress.browser.name !== 'electron') {\n return Cypress.automation('remote:debugger:protocol', {\n command: 'Emulation.setDeviceMetricsOverride',\n params: {\n width: 1280,\n height: 720,\n deviceScaleFactor: 1,\n mobile: false,\n screenWidth: 1280,\n screenHeight: 720\n }\n }).then(function () {\n // can't tell expect() not to log, so manually throwing here\n if (window.devicePixelRatio !== 1) {\n throw new Error('Setting devicePixelRatio to 1 failed');\n }\n });\n }\n}" }, { "hookId": "h2", @@ -286,7 +286,7 @@ exports['e2e spec isolation fails'] = { "title": [ "\"before all\" hook" ], - "body": "function () {\n if (Cypress.browser.family === 'chrome') {\n return Cypress.automation('remote:debugger:protocol', {\n command: 'Emulation.setDeviceMetricsOverride',\n params: {\n width: 1280,\n height: 720,\n deviceScaleFactor: 1,\n mobile: false,\n screenWidth: 1280,\n screenHeight: 720\n }\n }).then(function () {\n // can't tell expect() not to log, so manually throwing here\n if (window.devicePixelRatio !== 1) {\n throw new Error('Setting devicePixelRatio to 1 failed');\n }\n });\n }\n}" + "body": "function () {\n if (Cypress.browser.family === 'chromium' && Cypress.browser.name !== 'electron') {\n return Cypress.automation('remote:debugger:protocol', {\n command: 'Emulation.setDeviceMetricsOverride',\n params: {\n width: 1280,\n height: 720,\n deviceScaleFactor: 1,\n mobile: false,\n screenWidth: 1280,\n screenHeight: 720\n }\n }).then(function () {\n // can't tell expect() not to log, so manually throwing here\n if (window.devicePixelRatio !== 1) {\n throw new Error('Setting devicePixelRatio to 1 failed');\n }\n });\n }\n}" } ], "tests": [ @@ -401,7 +401,7 @@ exports['e2e spec isolation fails'] = { "title": [ "\"before all\" hook" ], - "body": "function () {\n if (Cypress.browser.family === 'chrome') {\n return Cypress.automation('remote:debugger:protocol', {\n command: 'Emulation.setDeviceMetricsOverride',\n params: {\n width: 1280,\n height: 720,\n deviceScaleFactor: 1,\n mobile: false,\n screenWidth: 1280,\n screenHeight: 720\n }\n }).then(function () {\n // can't tell expect() not to log, so manually throwing here\n if (window.devicePixelRatio !== 1) {\n throw new Error('Setting devicePixelRatio to 1 failed');\n }\n });\n }\n}" + "body": "function () {\n if (Cypress.browser.family === 'chromium' && Cypress.browser.name !== 'electron') {\n return Cypress.automation('remote:debugger:protocol', {\n command: 'Emulation.setDeviceMetricsOverride',\n params: {\n width: 1280,\n height: 720,\n deviceScaleFactor: 1,\n mobile: false,\n screenWidth: 1280,\n screenHeight: 720\n }\n }).then(function () {\n // can't tell expect() not to log, so manually throwing here\n if (window.devicePixelRatio !== 1) {\n throw new Error('Setting devicePixelRatio to 1 failed');\n }\n });\n }\n}" }, { "hookId": "h2", @@ -605,7 +605,7 @@ exports['e2e spec isolation fails'] = { "title": [ "\"before all\" hook" ], - "body": "function () {\n if (Cypress.browser.family === 'chrome') {\n return Cypress.automation('remote:debugger:protocol', {\n command: 'Emulation.setDeviceMetricsOverride',\n params: {\n width: 1280,\n height: 720,\n deviceScaleFactor: 1,\n mobile: false,\n screenWidth: 1280,\n screenHeight: 720\n }\n }).then(function () {\n // can't tell expect() not to log, so manually throwing here\n if (window.devicePixelRatio !== 1) {\n throw new Error('Setting devicePixelRatio to 1 failed');\n }\n });\n }\n}" + "body": "function () {\n if (Cypress.browser.family === 'chromium' && Cypress.browser.name !== 'electron') {\n return Cypress.automation('remote:debugger:protocol', {\n command: 'Emulation.setDeviceMetricsOverride',\n params: {\n width: 1280,\n height: 720,\n deviceScaleFactor: 1,\n mobile: false,\n screenWidth: 1280,\n screenHeight: 720\n }\n }).then(function () {\n // can't tell expect() not to log, so manually throwing here\n if (window.devicePixelRatio !== 1) {\n throw new Error('Setting devicePixelRatio to 1 failed');\n }\n });\n }\n}" }, { "hookId": "h2", diff --git a/packages/server/__snapshots__/validation_spec.coffee.js b/packages/server/__snapshots__/validation_spec.coffee.js index 10ba68ea832d..3a73ad9b4076 100644 --- a/packages/server/__snapshots__/validation_spec.coffee.js +++ b/packages/server/__snapshots__/validation_spec.coffee.js @@ -29,7 +29,7 @@ exports['lib/util/validation #isValidBrowser passes valid browsers and forms err "given": { "name": "Chrome", "displayName": "Chrome Browser", - "family": "chrome", + "family": "chromium", "path": "/path/to/chrome", "version": "1.2.3", "majorVersion": 1 @@ -51,7 +51,7 @@ exports['lib/util/validation #isValidBrowser passes valid browsers and forms err "given": { "name": "Electron", "displayName": "Electron", - "family": "electron", + "family": "chromium", "path": "", "version": "99.101.3", "majorVersion": 99 @@ -71,7 +71,7 @@ exports['lib/util/validation #isValidBrowser passes valid browsers and forms err "displayName": "Bad family browser", "family": "unknown family" }, - "expect": "Expected `family` to be either electron, chrome or firefox. Instead the value was: `{\"name\":\"bad family\",\"displayName\":\"Bad family browser\",\"family\":\"unknown family\"}`" + "expect": "Expected `family` to be either electron, chromium or firefox. Instead the value was: `{\"name\":\"bad family\",\"displayName\":\"Bad family browser\",\"family\":\"unknown family\"}`" } ] } diff --git a/packages/server/lib/browsers/index.js b/packages/server/lib/browsers/index.js index 16907779eee1..4119757316d1 100644 --- a/packages/server/lib/browsers/index.js +++ b/packages/server/lib/browsers/index.js @@ -7,7 +7,7 @@ const errors = require('../errors') const check = require('check-more-types') // returns true if the passed string is a known browser family name -const isBrowserFamily = check.oneOf(['electron', 'chrome']) +const isBrowserFamily = check.oneOf(['chromium']) let instance = null @@ -41,19 +41,18 @@ const cleanup = () => { return instance = null } -const getBrowserLauncherByFamily = function (family) { - debug('getBrowserLauncherByFamily %o', { family }) - if (!isBrowserFamily(family)) { - debug('unknown browser family', family) +const getBrowserLauncher = function (browser) { + debug('getBrowserLauncher %o', { browser }) + if (!isBrowserFamily(browser.family)) { + debug('unknown browser family', browser.family) } - switch (family) { - case 'electron': - return require('./electron') - case 'chrome': - return require('./chrome') - default: - break + if (browser.name === 'electron') { + return require('./electron') + } + + if (browser.family === 'chromium') { + return require('./chrome') } } @@ -61,19 +60,42 @@ const isValidPathToBrowser = (str) => { return path.basename(str) !== str } +const parseBrowserOption = (opt) => { + // for backwards compatibility pre-4.x + if (opt === 'canary') { + opt = 'chrome:canary' + } + + // it's a name or a path + if (!_.isString(opt) || !opt.includes(':')) { + return { + name: opt, + channel: 'stable', + } + } + + // it's in name:channel format + const split = opt.indexOf(':') + + return { + name: opt.slice(0, split), + channel: opt.slice(split + 1), + } +} + const ensureAndGetByNameOrPath = function (nameOrPath, returnAll = false, browsers = null) { const findBrowsers = Array.isArray(browsers) ? Promise.resolve(browsers) : utils.getBrowsers() return findBrowsers .then((browsers = []) => { - let browser + const filter = parseBrowserOption(nameOrPath) - debug('searching for browser %o', { nameOrPath, knownBrowsers: browsers }) + debug('searching for browser %o', { nameOrPath, filter, knownBrowsers: browsers }) // try to find the browser by name with the highest version property const sortedBrowsers = _.sortBy(browsers, ['version']) - browser = _.findLast(sortedBrowsers, { name: nameOrPath }) + const browser = _.findLast(sortedBrowsers, filter) if (browser) { // short circuit if found @@ -104,8 +126,18 @@ const ensureAndGetByNameOrPath = function (nameOrPath, returnAll = false, browse }) } +const formatBrowsersToOptions = (browsers) => { + return browsers.map((browser) => { + if (browser.channel !== 'stable') { + return [browser.name, browser.channel].join(':') + } + + return browser.name + }) +} + const throwBrowserNotFound = function (browserName, browsers = []) { - const names = _.map(browsers, 'name').join(', ') + const names = formatBrowsersToOptions(browsers).join(', ') return errors.throw('BROWSER_NOT_FOUND_BY_NAME', browserName, names) } @@ -144,7 +176,7 @@ module.exports = { onBrowserClose () {}, }) - if (!(browserLauncher = getBrowserLauncherByFamily(browser.family))) { + if (!(browserLauncher = getBrowserLauncher(browser))) { return throwBrowserNotFound(browser.name, options.browsers) } diff --git a/packages/server/lib/browsers/utils.js b/packages/server/lib/browsers/utils.ts similarity index 92% rename from packages/server/lib/browsers/utils.js rename to packages/server/lib/browsers/utils.ts index b5212c4abfc6..50a9ca4482b1 100644 --- a/packages/server/lib/browsers/utils.js +++ b/packages/server/lib/browsers/utils.ts @@ -1,3 +1,5 @@ +import { FoundBrowser } from '@packages/launcher' + const path = require('path') const debug = require('debug')('cypress:server:browsers:utils') const Promise = require('bluebird') @@ -74,7 +76,7 @@ const removeOldProfiles = function () { ]) } -module.exports = { +export = { getPort, copyExtension, @@ -100,15 +102,17 @@ module.exports = { debug('found browsers %o', { browsers }) + // @ts-ignore const version = process.versions.chrome || '' if (version) { majorVersion = parseInt(version.split('.')[0]) } - const electronBrowser = { + const electronBrowser: FoundBrowser = { name: 'electron', - family: 'electron', + channel: 'stable', + family: 'chromium', displayName: 'Electron', version, path: '', @@ -117,7 +121,7 @@ module.exports = { } // the internal version of Electron, which won't be detected by `launcher` - debug('adding Electron browser with version %s', version) + debug('adding Electron browser %o', electronBrowser) return browsers.concat(electronBrowser) }) diff --git a/packages/server/lib/cypress.js b/packages/server/lib/cypress.js index bdcee462e4c4..56cf5f8fc8a3 100644 --- a/packages/server/lib/cypress.js +++ b/packages/server/lib/cypress.js @@ -102,48 +102,6 @@ module.exports = { return require('./open_project').open(options.project, options) }, - runServer (options) { - // args = {} - // - // _.defaults options, { autoOpen: true } - // - // if not options.project - // throw new Error("Missing path to project:\n\nPlease pass 'npm run server -- --project /path/to/project'\n\n") - // - // if options.debug - // args.debug = "--debug" - // - // ## just spawn our own index.js file again - // ## but put ourselves in project mode so - // ## we actually boot a project! - // _.extend(args, { - // script: "index.js" - // watch: ["--watch", "lib"] - // ignore: ["--ignore", "lib/public"] - // verbose: "--verbose" - // exts: ["-e", "coffee,js"] - // args: ["--", "--config", "port=2020", "--mode", "openProject", "--project", options.project] - // }) - // - // args = _.chain(args).values().flatten().value() - // - // cp.spawn("nodemon", args, {stdio: "inherit"}) - // - // ## auto open in dev mode directly to our - // ## default cypress web app client - // if options.autoOpen - // _.delay -> - // require("./browsers").launch("chrome", "http://localhost:2020/__", { - // proxyServer: "http://localhost:2020" - // }) - // , 2000 - // - // if options.debug - // cp.spawn("node-inspector", [], {stdio: "inherit"}) - // - // require("opn")("http://127.0.0.1:8080/debug?ws=127.0.0.1:8080&port=5858") - }, - start (argv = []) { debug('starting cypress with argv %o', argv) @@ -285,9 +243,6 @@ module.exports = { case 'interactive': return this.runElectron(mode, options) - case 'server': - return this.runServer(options) - case 'openProject': // open + start the project return this.openProject(options) diff --git a/packages/server/lib/gui/events.coffee b/packages/server/lib/gui/events.coffee index 72a5ad278393..57765f55554e 100644 --- a/packages/server/lib/gui/events.coffee +++ b/packages/server/lib/gui/events.coffee @@ -216,7 +216,7 @@ handleEvent = (options, bus, event, id, type, arg) -> .then -> chromePolicyCheck.run (err) -> options.config.browsers.forEach (browser) -> - if browser.family == 'chrome' + if browser.family == 'chromium' browser.warning = errors.getMsgByType('BAD_POLICY_WARNING_TOOLTIP') openProject.create(arg, options, { diff --git a/packages/server/lib/modes/run.js b/packages/server/lib/modes/run.js index 94cfb770acd1..c561c0c7d6dc 100644 --- a/packages/server/lib/modes/run.js +++ b/packages/server/lib/modes/run.js @@ -453,11 +453,11 @@ const getProjectId = Promise.method((project, id) => { const getDefaultBrowserOptsByFamily = (browser, project, writeVideoFrame) => { la(browserUtils.isBrowserFamily(browser.family), 'invalid browser family in', browser) - if (browser.family === 'electron') { + if (browser.name === 'electron') { return getElectronProps(browser.isHeaded, project, writeVideoFrame) } - if (browser.family === 'chrome') { + if (browser.family === 'chromium') { return getChromeProps(browser.isHeaded, project, writeVideoFrame) } @@ -621,11 +621,11 @@ 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) { + if (browser.name === 'electron' && browser.isHeadless) { return true } - if (browser.family === 'chrome') { + if (browser.name !== 'electron' && browser.family === 'chromium') { return true } @@ -672,7 +672,7 @@ const maybeStartVideoRecording = Promise.method(function (options = {}) { console.log('') // TODO update error messages and included browser name and headed mode - if (browser.family === 'electron' && browser.isHeaded) { + if (browser.name === 'electron' && browser.isHeaded) { errors.warning('CANNOT_RECORD_VIDEO_HEADED') } else { errors.warning('CANNOT_RECORD_VIDEO_FOR_THIS_BROWSER', browser.name) @@ -967,9 +967,17 @@ module.exports = { let attempts = 0 const wait = () => { + debug('waiting for socket to connect and browser to launch...') + return Promise.join( - this.waitForSocketConnection(project, socketId), + this.waitForSocketConnection(project, socketId) + .tap(() => { + debug('socket connected', { socketId }) + }), this.launchBrowser(options) + .tap(() => { + debug('browser launched') + }) ) .timeout(timeout || 30000) .catch(Promise.TimeoutError, (err) => { @@ -1119,7 +1127,7 @@ module.exports = { runSpecs (options = {}) { _.defaults(options, { // only non-Electron browsers run headed by default - headed: options.browser.family !== 'electron', + headed: options.browser.name !== 'electron', }) const { config, browser, sys, headed, outputPath, specs, specPattern, beforeSpecRun, afterSpecRun, runUrl, parallel, group, tag } = options @@ -1333,7 +1341,7 @@ module.exports = { errors.throw('NO_SPECS_FOUND', config.integrationFolder, specPattern) } - if (browser.family === 'chrome') { + if (browser.family === 'chromium') { chromePolicyCheck.run(onWarning) } diff --git a/packages/server/lib/open_project.js b/packages/server/lib/open_project.js index 520d5ad867f0..167837dd5157 100644 --- a/packages/server/lib/open_project.js +++ b/packages/server/lib/open_project.js @@ -1,6 +1,6 @@ const _ = require('lodash') const la = require('lazy-ass') -const debug = require('debug')('cypress:server:openproject') +const debug = require('debug')('cypress:server:open_project') const Promise = require('bluebird') const chokidar = require('chokidar') const Project = require('./project') diff --git a/packages/server/lib/util/validation.js b/packages/server/lib/util/validation.js index 7eda92e28546..1fcf662e0305 100644 --- a/packages/server/lib/util/validation.js +++ b/packages/server/lib/util/validation.js @@ -47,7 +47,8 @@ const isValidBrowser = (browser) => { return errMsg('name', browser, 'a non-empty string') } - const knownBrowserFamilies = ['electron', 'chrome', 'firefox'] + // TODO: this is duplicated with browsers/index + const knownBrowserFamilies = ['electron', 'chromium', 'firefox'] if (!is.oneOf(knownBrowserFamilies)(browser.family)) { return errMsg('family', browser, commaListsOr`either ${knownBrowserFamilies}`) diff --git a/packages/server/test/e2e/4_browser_path_spec.coffee b/packages/server/test/e2e/4_browser_path_spec.coffee index 84b2f7132fd2..132e11835a91 100644 --- a/packages/server/test/e2e/4_browser_path_spec.coffee +++ b/packages/server/test/e2e/4_browser_path_spec.coffee @@ -32,10 +32,10 @@ describe "e2e launching browsers by path", -> it "works with an installed browser path", -> launcher.detect().then (browsers) => browsers.find (browser) => - browser.family == "chrome" + browser.family == "chromium" .tap (browser) => if !browser - throw new Error("A 'chrome' family browser must be installed for this test") + throw new Error("A 'chromium' family browser must be installed for this test") .get("path") ## turn binary browser names ("google-chrome") into their absolute paths ## so that server recognizes them as a path, not as a browser name diff --git a/packages/server/test/integration/cypress_spec.js b/packages/server/test/integration/cypress_spec.js index 366f07c2e883..321bc16173ce 100644 --- a/packages/server/test/integration/cypress_spec.js +++ b/packages/server/test/integration/cypress_spec.js @@ -49,21 +49,24 @@ const { formStatePath } = require(`${root}lib/util/saved_state`) const TYPICAL_BROWSERS = [ { name: 'chrome', - family: 'chrome', + family: 'chromium', + channel: 'stable', displayName: 'Chrome', version: '60.0.3112.101', path: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', majorVersion: '60', }, { name: 'chromium', - family: 'chrome', + family: 'chromium', + channel: 'stable', displayName: 'Chromium', version: '49.0.2609.0', path: '/Users/bmann/Downloads/chrome-mac/Chromium.app/Contents/MacOS/Chromium', majorVersion: '49', }, { - name: 'canary', - family: 'chrome', + name: 'chrome', + family: 'chromium', + channel: 'canary', displayName: 'Canary', version: '62.0.3197.0', path: '/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary', @@ -73,7 +76,7 @@ const TYPICAL_BROWSERS = [ const ELECTRON_BROWSER = { name: 'electron', - family: 'electron', + family: 'chromium', displayName: 'Electron', path: '', version: '99.101.1234', @@ -172,7 +175,7 @@ describe('lib/cypress', () => { it('allows browser major to be a number', () => { const browser = { name: 'Edge Beta', - family: 'chrome', + family: 'chromium', displayName: 'Edge Beta', version: '80.0.328.2', path: '/some/path', @@ -334,7 +337,7 @@ describe('lib/cypress', () => { context('--run-project', () => { beforeEach(() => { sinon.stub(electron.app, 'on').withArgs('ready').yieldsAsync() - sinon.stub(runMode, 'waitForSocketConnection') + sinon.stub(runMode, 'waitForSocketConnection').resolves() sinon.stub(runMode, 'listenForProjectEnd').resolves({ stats: { failures: 0 } }) sinon.stub(browsers, 'open') sinon.stub(commitInfo, 'getRemoteOrigin').resolves('remoteOrigin') @@ -761,7 +764,7 @@ describe('lib/cypress', () => { const found2 = _.find(argsSet, (args) => { return _.find(args, (arg) => { return arg.message && arg.message.includes( - 'Available browsers found are: chrome, chromium, canary, electron' + 'Available browsers found are: chrome, chromium, chrome:canary, electron' ) }) }) @@ -1255,7 +1258,7 @@ describe('lib/cypress', () => { sinon.stub(api, 'createRun').resolves() sinon.stub(electron.app, 'on').withArgs('ready').yieldsAsync() sinon.stub(browsers, 'open') - sinon.stub(runMode, 'waitForSocketConnection') + sinon.stub(runMode, 'waitForSocketConnection').resolves() sinon.stub(runMode, 'waitForTestsToFinishRunning').resolves({ stats: { tests: 1, diff --git a/packages/server/test/support/fixtures/projects/e2e/cypress/integration/config_passing_spec.coffee b/packages/server/test/support/fixtures/projects/e2e/cypress/integration/config_passing_spec.coffee index 49f306c642de..b4ee95c39ca6 100644 --- a/packages/server/test/support/fixtures/projects/e2e/cypress/integration/config_passing_spec.coffee +++ b/packages/server/test/support/fixtures/projects/e2e/cypress/integration/config_passing_spec.coffee @@ -13,7 +13,7 @@ describe "Cypress static methods + props", -> { browser } = Cypress expect(browser).to.be.an("object") - expect(browser.name).to.be.oneOf(["electron", "chrome", "canary", "chromium"]) + expect(browser.name).to.be.oneOf(["electron", "chrome", "chromium"]) expect(browser.displayName).to.be.oneOf(["Electron", "Chrome", "Canary", "Chromium"]) expect(browser.version).to.be.a("string") # we are parsing major version, so it should be a number 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 9b29adb091d2..d12c44c557d6 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 @@ -7,7 +7,7 @@ describe('e2e headless spec', function () { }) it('has expected HeadlessChrome useragent', function () { - if (Cypress.browser.family !== 'chrome') { + if (Cypress.browser.family !== 'chromium' || Cypress.browser.name === 'electron') { return } @@ -16,7 +16,7 @@ describe('e2e headless spec', function () { }) it('has expected launch args', function () { - if (Cypress.browser.family !== 'chrome') { + if (Cypress.browser.family !== 'chromium' || Cypress.browser.name === 'electron') { return } diff --git a/packages/server/test/support/fixtures/projects/e2e/cypress/support/index.js b/packages/server/test/support/fixtures/projects/e2e/cypress/support/index.js index 69f619dc1431..27e017398261 100644 --- a/packages/server/test/support/fixtures/projects/e2e/cypress/support/index.js +++ b/packages/server/test/support/fixtures/projects/e2e/cypress/support/index.js @@ -1,5 +1,5 @@ before(function () { - if (Cypress.browser.family === 'chrome') { + if (Cypress.browser.family === 'chromium' && Cypress.browser.name !== 'electron') { return Cypress.automation('remote:debugger:protocol', { command: 'Emulation.setDeviceMetricsOverride', params: { diff --git a/packages/server/test/support/fixtures/projects/plugin-returns-invalid-browser/cypress/plugins/index.coffee b/packages/server/test/support/fixtures/projects/plugin-returns-invalid-browser/cypress/plugins/index.coffee index b1ac2d369667..51538703e76e 100644 --- a/packages/server/test/support/fixtures/projects/plugin-returns-invalid-browser/cypress/plugins/index.coffee +++ b/packages/server/test/support/fixtures/projects/plugin-returns-invalid-browser/cypress/plugins/index.coffee @@ -4,6 +4,6 @@ module.exports = (onFn, config) -> { browsers: [{ name: "browser name", - family: "chrome" + family: "chromium" }] } diff --git a/packages/server/test/support/fixtures/projects/remote-debugging-port-removed/cypress/plugins.js b/packages/server/test/support/fixtures/projects/remote-debugging-port-removed/cypress/plugins.js index 60dd08b421ac..0a72c654dadb 100644 --- a/packages/server/test/support/fixtures/projects/remote-debugging-port-removed/cypress/plugins.js +++ b/packages/server/test/support/fixtures/projects/remote-debugging-port-removed/cypress/plugins.js @@ -2,7 +2,7 @@ const la = require('lazy-ass') module.exports = (on) => { on('before:browser:launch', (browser = {}, args) => { - la(browser.family === 'chrome', 'this test can only be run with a chrome-family browser') + la(browser.family === 'chromium', 'this test can only be run with a chromium-family browser') // remove debugging port so that the browser connection fails const newArgs = args.filter((arg) => !arg.startsWith('--remote-debugging-port=')) diff --git a/packages/server/test/support/helpers/e2e.js b/packages/server/test/support/helpers/e2e.js index c161cb10983e..e6fa7c78c524 100644 --- a/packages/server/test/support/helpers/e2e.js +++ b/packages/server/test/support/helpers/e2e.js @@ -461,7 +461,8 @@ module.exports = (e2e = { }, args (options = {}) { - let browser + debug('converting options to args %o', { options }) + const args = [ // hides a user warning to go through NPM module `--cwd=${process.cwd()}`, @@ -509,10 +510,8 @@ module.exports = (e2e = { args.push(`--reporter-options=${options.reporterOptions}`) } - browser = (options.browser) - - if (browser) { - args.push(`--browser=${browser}`) + if (options.browser) { + args.push(`--browser=${options.browser}`) } if (options.config) { @@ -625,6 +624,7 @@ module.exports = (e2e = { } return new Promise((resolve, reject) => { + debug('spawning Cypress %o', { args }) const sp = cp.spawn('node', args, { env: _.chain(process.env) .omit('CYPRESS_DEBUG') diff --git a/packages/server/test/unit/browsers/browsers_spec.coffee b/packages/server/test/unit/browsers/browsers_spec.coffee index 295a2e07e84b..364b11cf4ba6 100644 --- a/packages/server/test/unit/browsers/browsers_spec.coffee +++ b/packages/server/test/unit/browsers/browsers_spec.coffee @@ -8,20 +8,21 @@ utils = require("#{root}../lib/browsers/utils") describe "lib/browsers/index", -> context ".isBrowserFamily", -> it "allows only known browsers", -> - expect(browsers.isBrowserFamily("chrome")).to.be.true - expect(browsers.isBrowserFamily("electron")).to.be.true + expect(browsers.isBrowserFamily("chromium")).to.be.true + expect(browsers.isBrowserFamily("chrome")).to.be.false + expect(browsers.isBrowserFamily("electron")).to.be.false expect(browsers.isBrowserFamily("my-favorite-browser")).to.be.false context ".ensureAndGetByNameOrPath", -> it "returns browser by name", -> sinon.stub(utils, "getBrowsers").resolves([ - { name: "foo" } - { name: "bar" } + { name: "foo", channel: "stable" } + { name: "bar", channel: "stable" } ]) browsers.ensureAndGetByNameOrPath("foo") .then (browser) -> - expect(browser).to.deep.eq({ name: "foo" }) + expect(browser).to.deep.eq({ name: "foo", channel: "stable" }) it "throws when no browser can be found", -> browsers.ensureAndGetByNameOrPath("browserNotGonnaBeFound") diff --git a/packages/server/test/unit/config_spec.coffee b/packages/server/test/unit/config_spec.coffee index b5572134253f..5dd62761a1e4 100644 --- a/packages/server/test/unit/config_spec.coffee +++ b/packages/server/test/unit/config_spec.coffee @@ -1032,7 +1032,7 @@ describe "lib/config", -> it "keeps the list of browsers if the plugins returns empty object", -> browser = { name: "fake browser name", - family: "chrome", + family: "chromium", displayName: "My browser", version: "x.y.z", path: "/path/to/browser", @@ -1064,7 +1064,7 @@ describe "lib/config", -> it "catches browsers=null returned from plugins", -> browser = { name: "fake browser name", - family: "chrome", + family: "chromium", displayName: "My browser", version: "x.y.z", path: "/path/to/browser", @@ -1092,7 +1092,7 @@ describe "lib/config", -> it "allows user to filter browsers", -> browserOne = { name: "fake browser name", - family: "chrome", + family: "chromium", displayName: "My browser", version: "x.y.z", path: "/path/to/browser", diff --git a/packages/server/test/unit/gui/events_spec.coffee b/packages/server/test/unit/gui/events_spec.coffee index aa1f6060b1e9..e6f981d9d299 100644 --- a/packages/server/test/unit/gui/events_spec.coffee +++ b/packages/server/test/unit/gui/events_spec.coffee @@ -475,7 +475,7 @@ describe "lib/gui/events", -> sinon.stub(openProject, "create").resolves() @options.browser = "/foo" - browsers.getAllBrowsersWith.withArgs("/foo").resolves([{family: 'chrome'}, {family: 'some other'}]) + browsers.getAllBrowsersWith.withArgs("/foo").resolves([{family: 'chromium'}, {family: 'some other'}]) sinon.stub(chromePolicyCheck, "run").callsArgWith(0, new Error) @@ -488,7 +488,7 @@ describe "lib/gui/events", -> config: { browsers: [ { - family: "chrome" + family: "chromium" 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" }, { diff --git a/packages/server/test/unit/modes/run_spec.js b/packages/server/test/unit/modes/run_spec.js index 0ddbcf93e80b..34dc9a9c4d68 100644 --- a/packages/server/test/unit/modes/run_spec.js +++ b/packages/server/test/unit/modes/run_spec.js @@ -171,7 +171,7 @@ describe('lib/modes/run', () => { const browser = { name: 'electron', - family: 'electron', + family: 'chromium', isHeaded: false, } @@ -204,7 +204,7 @@ describe('lib/modes/run', () => { const browser = { name: 'chrome', - family: 'chrome', + family: 'chromium', isHeaded: true, } @@ -632,7 +632,7 @@ describe('lib/modes/run', () => { }) }) - it('disables video recording on interactive mode runs', () => { + it('disables video recording on headed runs', () => { return runMode.run({ headed: true }) .then(() => { expect(errors.warning).to.be.calledWith('CANNOT_RECORD_VIDEO_HEADED') @@ -683,7 +683,7 @@ describe('lib/modes/run', () => { name: 'fooBrowser', path: 'path/to/browser', version: '777', - family: 'electron', + family: 'chromium', }) sinon.stub(runMode, 'waitForSocketConnection').resolves() @@ -748,7 +748,7 @@ describe('lib/modes/run', () => { }) it('passes headed to openProject.launch', () => { - const browser = { name: 'electron', family: 'electron' } + const browser = { name: 'electron', family: 'chromium' } browsers.ensureAndGetByNameOrPath.resolves(browser) diff --git a/packages/server/test/unit/validation_spec.coffee b/packages/server/test/unit/validation_spec.coffee index 68e6504c2c42..81521a149706 100644 --- a/packages/server/test/unit/validation_spec.coffee +++ b/packages/server/test/unit/validation_spec.coffee @@ -16,7 +16,7 @@ describe "lib/util/validation", -> { name: "Chrome", displayName: "Chrome Browser", - family: "chrome", + family: "chromium", path: "/path/to/chrome", version: "1.2.3", majorVersion: 1 @@ -34,7 +34,7 @@ describe "lib/util/validation", -> { name: "Electron", displayName: "Electron", - family: "electron", + family: "chromium", path: "", version: "99.101.3", majorVersion: 99 diff --git a/packages/ui-components/cypress/integration/browser-icon_spec.jsx b/packages/ui-components/cypress/integration/browser-icon_spec.jsx index fe1f6bef1cfb..2bbd218c4fac 100644 --- a/packages/ui-components/cypress/integration/browser-icon_spec.jsx +++ b/packages/ui-components/cypress/integration/browser-icon_spec.jsx @@ -44,6 +44,7 @@ describe('', () => { + ) cy.get('.browser-icon').eq(0) @@ -61,6 +62,10 @@ describe('', () => { cy.get('.browser-icon').eq(3) .should('have.attr', 'src') .and('include', 'firefox') + + cy.get('.browser-icon').eq(4) + .should('have.attr', 'src') + .and('include', 'chromium') }) it('displays generic logo for unsupported browsers', () => { diff --git a/packages/ui-components/src/browser-icon.jsx b/packages/ui-components/src/browser-icon.jsx index 9a0fb5b33132..994378666a78 100644 --- a/packages/ui-components/src/browser-icon.jsx +++ b/packages/ui-components/src/browser-icon.jsx @@ -3,6 +3,7 @@ import React from 'react' const families = { chrome: /^chrome/i, + chromium: /^chromium/i, edge: /^edge/i, electron: /^electron/i, firefox: /^firefox/i, @@ -36,6 +37,7 @@ const logoPath = (browserName) => { return logoPaths[browserKey] || logoPaths[familyFallback(browserKey)] } +// browserName should be the browser's display name const BrowserIcon = ({ browserName }) => { if (logoPath(browserName)) { return From 4a01cda602dfda3662654c1b749a2e0ae60e9e0c Mon Sep 17 00:00:00 2001 From: Gleb Bahmutov Date: Mon, 3 Feb 2020 10:36:01 -0500 Subject: [PATCH 40/49] =?UTF-8?q?detect=20Microsoft=20Edge=20and=20Edge=20?= =?UTF-8?q?Canary=20browsers=20and=20allow=20runnin=E2=80=A6=20(#6181)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add microsoft edge support * feat: support edge dev * feat: support windows edge canary and edge dev * test: update snapshots * fix lint errors * edge dev -> edge * trigger windows build * run kitchensink tests on Chrome and Edge on AppVeyor * update browser types * add Edge Beta + Edge Dev * move edge stable to top of list Co-authored-by: Yousaf Nabi Co-authored-by: Jennifer Shehane Co-authored-by: Chris Breiding Co-authored-by: Zach Bloomquist --- appveyor.yml | 9 ++- .../__snapshots__/browsers_spec.ts.js | 20 ++++++- packages/launcher/index.js | 2 +- packages/launcher/lib/browsers.ts | 36 +++++++++++ packages/launcher/lib/darwin/index.ts | 43 +++++++++++-- packages/launcher/lib/detect.ts | 2 + packages/launcher/lib/windows/index.ts | 60 +++++++++++++------ 7 files changed, 147 insertions(+), 25 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index e41c017becb7..daec7c8e4d0e 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -84,13 +84,20 @@ test_script: - node --version - npm --version - 7z + - echo *** Browsers installed on this system *** - cd packages/launcher && node index.js && cd ../.. + + - echo *** Kitchensink tests run on Chrome browser *** + - npm run dev -- --run-project %CD%/packages/example --browser chrome + - echo *** Kitchensink tests run on Edge browser *** + - npm run dev -- --run-project %CD%/packages/example --browser edge + # make sure mocha runs - npm run test-mocha # make sure our snapshots are compared correctly # - npm run test-mocha-snapshot # the other larger tests - - echo Building Windows NPM package %NEXT_DEV_VERSION% + - echo *** Building Windows NPM package %NEXT_DEV_VERSION% *** - npm --no-git-tag-version version %NEXT_DEV_VERSION% - cd cli - npm install diff --git a/packages/launcher/__snapshots__/browsers_spec.ts.js b/packages/launcher/__snapshots__/browsers_spec.ts.js index 822f7804f2d1..da3286255469 100644 --- a/packages/launcher/__snapshots__/browsers_spec.ts.js +++ b/packages/launcher/__snapshots__/browsers_spec.ts.js @@ -15,7 +15,7 @@ exports['browsers returns the expected list of browsers 1'] = [ { "name": "chromium", "family": "chromium", - "channel": "dev", + "channel": "stable", "displayName": "Chromium", "versionRegex": {}, "profile": true, @@ -32,5 +32,23 @@ exports['browsers returns the expected list of browsers 1'] = [ "versionRegex": {}, "profile": true, "binary": "google-chrome-canary" + }, + { + "name": "edge", + "family": "chromium", + "channel": "canary", + "displayName": "Edge Canary", + "versionRegex": {}, + "profile": true, + "binary": "edge-canary" + }, + { + "name": "edge", + "family": "chromium", + "channel": "stable", + "displayName": "Edge", + "versionRegex": {}, + "profile": true, + "binary": "edge" } ] diff --git a/packages/launcher/index.js b/packages/launcher/index.js index 461f8faf095d..6c52ab0bfd46 100644 --- a/packages/launcher/index.js +++ b/packages/launcher/index.js @@ -28,7 +28,7 @@ if (!module.parent) { console.log(` 👍 Found "${filename}":`, foundBrowser) }) .catch((err) => { - console.log(` 👎 Couldn't find "${filename}:`, err.message) + console.log(` 👎 Couldn't find "${filename}:"`, err.message) }) }) } else { diff --git a/packages/launcher/lib/browsers.ts b/packages/launcher/lib/browsers.ts index 85d4aa2d5a60..31a4bfb31075 100644 --- a/packages/launcher/lib/browsers.ts +++ b/packages/launcher/lib/browsers.ts @@ -32,6 +32,42 @@ export const browsers: Browser[] = [ profile: true, binary: 'google-chrome-canary', }, + { + name: 'edge', + family: 'chromium', + channel: 'stable', + displayName: 'Edge', + versionRegex: /Microsoft Edge (\S+)/, + profile: true, + binary: 'edge', + }, + { + name: 'edge', + family: 'chromium', + channel: 'canary', + displayName: 'Edge Canary', + versionRegex: /Microsoft Edge Canary (\S+)/, + profile: true, + binary: 'edge-canary', + }, + { + name: 'edge', + family: 'chromium', + channel: 'beta', + displayName: 'Edge Beta', + versionRegex: /Microsoft Edge Beta (\S+)/, + profile: true, + binary: 'edge-beta', + }, + { + name: 'edge', + family: 'chromium', + channel: 'dev', + displayName: 'Edge Dev', + versionRegex: /Microsoft Edge Dev (\S+)/, + profile: true, + binary: 'edge-dev', + }, ] /** starts a found browser and opens URL if given one */ diff --git a/packages/launcher/lib/darwin/index.ts b/packages/launcher/lib/darwin/index.ts index 673fcc9dc09a..481eb162f913 100644 --- a/packages/launcher/lib/darwin/index.ts +++ b/packages/launcher/lib/darwin/index.ts @@ -3,6 +3,7 @@ import { FoundBrowser, Browser } from '../types' import * as linuxHelper from '../linux' import { log } from '../log' import { merge, partial } from 'ramda' +import { get } from 'lodash' const detectCanary = partial(findApp, [ 'Contents/MacOS/Google Chrome Canary', @@ -19,15 +20,47 @@ const detectChromium = partial(findApp, [ 'org.chromium.Chromium', 'CFBundleShortVersionString', ]) +const detectEdgeCanary = partial(findApp, [ + 'Contents/MacOS/Microsoft Edge Canary', + 'com.microsoft.Edge.Canary', + 'CFBundleShortVersionString', +]) +const detectEdgeBeta = partial(findApp, [ + 'Contents/MacOS/Microsoft Edge Beta', + 'com.microsoft.Edge.Beta', + 'CFBundleShortVersionString', +]) +const detectEdgeDev = partial(findApp, [ + 'Contents/MacOS/Microsoft Edge Dev', + 'com.microsoft.Edge.Dev', + 'CFBundleShortVersionString', +]) +const detectEdge = partial(findApp, [ + 'Contents/MacOS/Microsoft Edge', + 'com.microsoft.Edge', + 'CFBundleShortVersionString', +]) type Detectors = { - [index: string]: Function + [name: string]: { + [channel: string]: Function + } } const browsers: Detectors = { - chrome: detectChrome, - canary: detectCanary, - chromium: detectChromium, + chrome: { + stable: detectChrome, + canary: detectCanary, + }, + chromium: { + stable: detectChromium, + }, + edge: { + stable: detectEdge, + canary: detectEdgeCanary, + beta: detectEdgeBeta, + dev: detectEdgeDev, + }, } export function getVersionString (path: string) { @@ -35,7 +68,7 @@ export function getVersionString (path: string) { } export function detect (browser: Browser): Promise { - let fn = browsers[browser.name] + let fn = get(browsers, [browser.name, browser.channel]) if (!fn) { // ok, maybe it is custom alias? diff --git a/packages/launcher/lib/detect.ts b/packages/launcher/lib/detect.ts index 5083100256b7..8eab270fd480 100644 --- a/packages/launcher/lib/detect.ts +++ b/packages/launcher/lib/detect.ts @@ -138,6 +138,8 @@ export const detect = (goalBrowsers?: Browser[]): Bluebird => { }) const compactFalse = (browsers: any[]) => compact(browsers) as FoundBrowser[] + log('detecting if the following browsers are present %o', goalBrowsers) + return Bluebird.mapSeries(goalBrowsers, checkBrowser) .then(flatten) .then(compactFalse) diff --git a/packages/launcher/lib/windows/index.ts b/packages/launcher/lib/windows/index.ts index cc234bdebffa..f97f72d38c84 100644 --- a/packages/launcher/lib/windows/index.ts +++ b/packages/launcher/lib/windows/index.ts @@ -3,6 +3,7 @@ import { pathExists } from 'fs-extra' import { homedir } from 'os' import { join, normalize } from 'path' import { tap, trim } from 'ramda' +import { get } from 'lodash' import { notInstalledErr } from '../errors' import { log } from '../log' import { Browser, FoundBrowser } from '../types' @@ -34,22 +35,46 @@ function formChromeCanaryAppPath () { return normalize(exe) } +function formEdgeCanaryAppPath () { + const home = homedir() + const exe = join( + home, + 'AppData', + 'Local', + 'Microsoft', + 'Edge SxS', + 'Application', + 'msedge.exe' + ) + + return normalize(exe) +} + type NameToPath = (name: string) => string -interface WindowsBrowserPaths { - [index: string]: NameToPath - chrome: NameToPath - canary: NameToPath - chromium: NameToPath +type WindowsBrowserPaths = { + [name: string]: { + [channel: string]: NameToPath + } } const formPaths: WindowsBrowserPaths = { - chrome: formFullAppPath, - canary: formChromeCanaryAppPath, - chromium: formChromiumAppPath, + chrome: { + stable: formFullAppPath, + canary: formChromeCanaryAppPath, + }, + chromium: { + stable: formChromiumAppPath, + }, + edge: { + stable: () => normalize('C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe'), + beta: () => normalize('C:/Program Files (x86)/Microsoft/Edge Beta/Application/msedge.exe'), + dev: () => normalize('C:/Program Files (x86)/Microsoft/Edge Dev/Application/msedge.exe'), + canary: formEdgeCanaryAppPath, + }, } -function getWindowsBrowser (name: string): Promise { +function getWindowsBrowser (browser: Browser): Promise { const getVersion = (stdout: string): string => { // result from wmic datafile // "Version=61.0.3163.100" @@ -61,11 +86,12 @@ function getWindowsBrowser (name: string): Promise { } log('Could not extract version from %s using regex %s', stdout, wmicVersion) - throw notInstalledErr(name) + throw notInstalledErr(browser.name) } - const formFullAppPathFn: any = formPaths[name] || formFullAppPath - const exePath = formFullAppPathFn(name) + const formFullAppPathFn: any = get(formPaths, [browser.name, browser.channel], formFullAppPath) + + const exePath = formFullAppPathFn(browser.name) log('exe path %s', exePath) @@ -74,24 +100,24 @@ function getWindowsBrowser (name: string): Promise { log('found %s ?', exePath, exists) if (!exists) { - throw notInstalledErr(`Browser ${name} file not found at ${exePath}`) + throw notInstalledErr(`Browser ${browser.name} file not found at ${exePath}`) } return getVersionString(exePath) .then(tap(log)) .then(getVersion) .then((version: string) => { - log('browser %s at \'%s\' version %s', name, exePath, version) + log('browser %s at \'%s\' version %s', browser.name, exePath, version) return { - name, + name: browser.name, version, path: exePath, } as FoundBrowser }) }) .catch(() => { - throw notInstalledErr(name) + throw notInstalledErr(browser.name) }) } @@ -116,5 +142,5 @@ export function getVersionString (path: string) { } export function detect (browser: Browser) { - return getWindowsBrowser(browser.name) + return getWindowsBrowser(browser) } From 5177fa39cb928bf0dfe551e78a3146cb2d9d75bb Mon Sep 17 00:00:00 2001 From: Zach Bloomquist Date: Mon, 3 Feb 2020 17:44:32 -0500 Subject: [PATCH 41/49] Add test for cy.spy, cy.stub.callThroughWithNew on constructors (#6310) * add test for stub.callThroughWithNew on constructor * add test for spy on constructor * use patch-package for now --- packages/driver/package.json | 1 + packages/driver/patches/sinon+8.1.1.patch | 19 +++++++++++++++++++ .../integration/commands/agents_spec.coffee | 10 ++++++++++ 3 files changed, 30 insertions(+) create mode 100644 packages/driver/patches/sinon+8.1.1.patch diff --git a/packages/driver/package.json b/packages/driver/package.json index 69328c7adcf6..3d87332d1f69 100644 --- a/packages/driver/package.json +++ b/packages/driver/package.json @@ -10,6 +10,7 @@ "clean-deps": "rm -rf node_modules", "cypress:open": "node ../../scripts/cypress open --project ./test", "cypress:run": "node ../../scripts/cypress run --project ./test", + "postinstall": "npx patch-package", "prestart": "npm run check-deps-pre", "start": "../coffee/node_modules/.bin/coffee test/support/server.coffee" }, diff --git a/packages/driver/patches/sinon+8.1.1.patch b/packages/driver/patches/sinon+8.1.1.patch new file mode 100644 index 000000000000..c6edf9482811 --- /dev/null +++ b/packages/driver/patches/sinon+8.1.1.patch @@ -0,0 +1,19 @@ +diff --git a/node_modules/sinon/lib/sinon/util/core/function-to-string.js b/node_modules/sinon/lib/sinon/util/core/function-to-string.js +index fa0265b..a50bdf6 100644 +--- a/node_modules/sinon/lib/sinon/util/core/function-to-string.js ++++ b/node_modules/sinon/lib/sinon/util/core/function-to-string.js +@@ -9,8 +9,12 @@ module.exports = function toString() { + thisValue = this.getCall(i).thisValue; + + for (prop in thisValue) { +- if (thisValue[prop] === this) { +- return prop; ++ try { ++ if (thisValue[prop] === this) { ++ return prop; ++ } ++ } catch (e) { ++ // no-op - accessing props can throw an error, nothing to do here + } + } + } diff --git a/packages/driver/test/cypress/integration/commands/agents_spec.coffee b/packages/driver/test/cypress/integration/commands/agents_spec.coffee index 220d124fcd6d..c1f3a0c102fa 100644 --- a/packages/driver/test/cypress/integration/commands/agents_spec.coffee +++ b/packages/driver/test/cypress/integration/commands/agents_spec.coffee @@ -36,6 +36,11 @@ describe "src/cy/commands/agents", -> @obj.foo() expect(@originalCalled).to.be.false + it "can callThrough on constructors", -> + cy.stub(window, 'Notification').callThroughWithNew().as('Notification') + new Notification('Hello') + cy.get('@Notification').should('have.been.calledWith', 'Hello') + describe ".stub(obj, 'method', replacerFn)", -> beforeEach -> @originalCalled = false @@ -478,6 +483,11 @@ describe "src/cy/commands/agents", -> @obj.foo() expect(@originalCalled).to.be.true + it "can spy on constructors", -> + cy.spy(window, 'Notification').as('Notification') + new Notification('Hello') + cy.get('@Notification').should('have.been.calledWith', 'Hello') + context "#as", -> ## same as cy.stub(), so just some smoke tests here beforeEach -> From dca481a0793af46a6403b440eed94de268c5489a Mon Sep 17 00:00:00 2001 From: Ben Kucera <14625260+Bkucera@users.noreply.github.com> Date: Tue, 4 Feb 2020 05:26:32 -0500 Subject: [PATCH 42/49] Prep 4.0 launcher improvements (#6311) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * async/await-ify chrome.ts, update e2e.js * remove psInclude * bump snap-shot-it, remove patch-package * fix failing tests, tighten up code * remove unused Promise.all * more code cleanup from coffeescript conversion * fix expectedExitcode * update snapshot with cross origin error message * more code cleanup from coffeescript conversion * remove global state, cleanup normalizing stdout in snapshots * fix asserting on the expected exit code * remove firefox a default browser in e2e tests * remove dupe const * make onStdout return first arg by default * make expectedExitCode: 0 the default, remove duplicate option from all e2e tests * reuse _getArgs() properly, tighten up dupe code, move perf e2e tests to use new e2e.it helpers * make e2e test pass properly, and capture snapshot - this was being overlooked because expectedExitCode wasn’t being set (until now its the default) * revert firefox specific changes to reduce PR diff * remove newline Co-authored-by: Brian Mann --- .../__snapshots__/6_visit_spec.coffee.js | 61 ++++ .../cy_visit_performance_spec.js | 88 +----- packages/server/lib/browsers/chrome.ts | 268 ++++++++---------- packages/server/package.json | 2 +- .../server/test/e2e/1_base_url_spec.coffee | 2 - .../test/e2e/1_blacklist_hosts_spec.coffee | 1 - .../e2e/1_browserify_babel_es201spec.coffee | 1 - packages/server/test/e2e/1_cache_spec.coffee | 3 - .../1_commands_outside_of_test_spec.coffee | 1 - .../test/e2e/1_interception_spec.coffee | 1 - .../server/test/e2e/1_system_node_spec.coffee | 1 - .../server/test/e2e/2_cookies_spec.coffee | 2 - packages/server/test/e2e/2_domain_spec.coffee | 1 - packages/server/test/e2e/2_go_spec.coffee | 1 - packages/server/test/e2e/2_headless_spec.ts | 2 - packages/server/test/e2e/2_iframe_spec.coffee | 1 - packages/server/test/e2e/2_images_spec.coffee | 1 - packages/server/test/e2e/3_config_spec.coffee | 1 - .../server/test/e2e/3_issue_2891_spec.coffee | 1 - .../server/test/e2e/3_new_project_spec.coffee | 1 - packages/server/test/e2e/3_only_spec.coffee | 1 - .../test/e2e/3_page_loading_spec.coffee | 1 - .../server/test/e2e/3_plugins_spec.coffee | 6 +- .../server/test/e2e/3_user_agent_spec.coffee | 1 - .../test/e2e/4_browser_path_spec.coffee | 1 - .../test/e2e/4_form_submissions_spec.coffee | 3 - .../server/test/e2e/4_request_spec.coffee | 1 - .../4_screenshot_element_capture_spec.coffee | 1 - .../4_screenshot_fullpage_capture_spec.coffee | 1 - .../e2e/4_screenshot_nested_file_spec.coffee | 1 - .../server/test/e2e/4_websockets_spec.coffee | 1 - .../server/test/e2e/4_window_open_spec.coffee | 1 - packages/server/test/e2e/4_xhr_spec.coffee | 1 - .../5_screenshot_viewport_capture_spec.coffee | 1 - .../test/e2e/5_server_sent_events_spec.coffee | 1 - packages/server/test/e2e/5_stdout_spec.coffee | 4 - .../server/test/e2e/5_subdomain_spec.coffee | 1 - packages/server/test/e2e/6_task_spec.coffee | 1 - .../test/e2e/6_video_compression_spec.coffee | 1 - .../server/test/e2e/6_viewport_spec.coffee | 1 - packages/server/test/e2e/6_visit_spec.coffee | 2 +- .../test/e2e/6_web_security_spec.coffee | 1 - packages/server/test/e2e/7_record_spec.coffee | 18 -- .../e2e/8_network_error_handling_spec.coffee | 9 +- .../server/test/e2e/8_reporters_spec.coffee | 5 - .../performance/cy_visit_performance_spec.js | 31 +- .../performance/proxy_performance_spec.js | 21 +- .../integration/issue_2196_spec.coffee | 6 +- packages/server/test/support/helpers/e2e.js | 177 +++++++----- .../test/unit/browsers/chrome_spec.coffee | 29 +- 50 files changed, 325 insertions(+), 444 deletions(-) diff --git a/packages/server/__snapshots__/6_visit_spec.coffee.js b/packages/server/__snapshots__/6_visit_spec.coffee.js index 50383bce5e55..4737dca97b24 100644 --- a/packages/server/__snapshots__/6_visit_spec.coffee.js +++ b/packages/server/__snapshots__/6_visit_spec.coffee.js @@ -681,3 +681,64 @@ Error: ESOCKETTIMEDOUT ` + +exports['e2e visit / low response timeout / calls onBeforeLoad when overwriting cy.visit'] = ` + +==================================================================================================== + + (Run Starting) + + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Cypress: 1.2.3 │ + │ Browser: FooBrowser 88 │ + │ Specs: 1 found (issue_2196_spec.coffee) │ + │ Searched: cypress/integration/issue_2196_spec.coffee │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + + +──────────────────────────────────────────────────────────────────────────────────────────────────── + + Running: issue_2196_spec.coffee (1 of 1) + + + issue #2196: overwriting visit + ✓ fires onBeforeLoad + + + 1 passing + + + (Results) + + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Tests: 1 │ + │ Passing: 1 │ + │ Failing: 0 │ + │ Pending: 0 │ + │ Skipped: 0 │ + │ Screenshots: 0 │ + │ Video: true │ + │ Duration: X seconds │ + │ Spec Ran: issue_2196_spec.coffee │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + + + (Video) + + - Started processing: Compressing to 32 CRF + - Finished processing: /XXX/XXX/XXX/cypress/videos/issue_2196_spec.coffee.mp4 (X second) + + +==================================================================================================== + + (Run Finished) + + + Spec Tests Passing Failing Pending Skipped + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ ✔ issue_2196_spec.coffee XX:XX 1 1 - - - │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + ✔ All specs passed! XX:XX 1 1 - - - + + +` diff --git a/packages/server/__snapshots__/cy_visit_performance_spec.js b/packages/server/__snapshots__/cy_visit_performance_spec.js index 85f4875ed3ec..787891e0920e 100644 --- a/packages/server/__snapshots__/cy_visit_performance_spec.js +++ b/packages/server/__snapshots__/cy_visit_performance_spec.js @@ -1,90 +1,4 @@ -exports['cy.visit performance tests pass in chrome 1'] = ` - -==================================================================================================== - - (Run Starting) - - ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ - │ Cypress: 1.2.3 │ - │ Browser: FooBrowser 88 │ - │ Specs: 1 found (fast_visit_spec.coffee) │ - │ Searched: cypress/integration/fast_visit_spec.coffee │ - └────────────────────────────────────────────────────────────────────────────────────────────────┘ - - -──────────────────────────────────────────────────────────────────────────────────────────────────── - - Running: fast_visit_spec.coffee (1 of 1) - - - on localhost 95% of visits are faster than XX:XX, 80% are faster than XX:XX -histogram line -histogram line -histogram line -histogram line -histogram line -histogram line -histogram line -histogram line -histogram line -histogram line -histogram line -histogram line -histogram line -histogram line -histogram line - ✓ with connection: close -histogram line -histogram line -histogram line -histogram line -histogram line -histogram line -histogram line -histogram line -histogram line -histogram line -histogram line -histogram line -histogram line -histogram line -histogram line - ✓ with connection: keep-alive - - - 2 passing - - - (Results) - - ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ - │ Tests: 2 │ - │ Passing: 2 │ - │ Failing: 0 │ - │ Pending: 0 │ - │ Skipped: 0 │ - │ Screenshots: 0 │ - │ Video: false │ - │ Duration: X seconds │ - │ Spec Ran: fast_visit_spec.coffee │ - └────────────────────────────────────────────────────────────────────────────────────────────────┘ - - -==================================================================================================== - - (Run Finished) - - - Spec Tests Passing Failing Pending Skipped - ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ - │ ✔ fast_visit_spec.coffee XX:XX 2 2 - - - │ - └────────────────────────────────────────────────────────────────────────────────────────────────┘ - ✔ All specs passed! XX:XX 2 2 - - - - - -` - -exports['cy.visit performance tests pass in electron 1'] = ` +exports['cy.visit performance tests / passes'] = ` ==================================================================================================== diff --git a/packages/server/lib/browsers/chrome.ts b/packages/server/lib/browsers/chrome.ts index b6dbf8013979..1f8585e8cace 100644 --- a/packages/server/lib/browsers/chrome.ts +++ b/packages/server/lib/browsers/chrome.ts @@ -1,7 +1,7 @@ import _ from 'lodash' import os from 'os' import path from 'path' -import Promise from 'bluebird' +import Bluebird from 'bluebird' import la from 'lazy-ass' import check from 'check-more-types' import extension from '@packages/extension' @@ -18,6 +18,7 @@ import * as CriClient from './cri-client' type CypressConfiguration = any type Browser = FoundBrowser & { + majorVersion: number isHeadless: boolean isHeaded: boolean } @@ -33,7 +34,7 @@ const CHROME_VERSION_INTRODUCING_PROXY_BYPASS_ON_LOOPBACK = 72 const pathToExtension = extension.getPathToExtension() const pathToTheme = extension.getPathToTheme() -const defaultArgs = [ +const DEFAULT_ARGS = [ '--test-type', '--ignore-certificate-errors', '--start-maximized', @@ -103,16 +104,10 @@ const defaultArgs = [ '--use-mock-keychain', ] -const getRemoteDebuggingPort = Promise.method(() => { - let port +const getRemoteDebuggingPort = Bluebird.method(() => { + const port = Number(process.env.CYPRESS_REMOTE_DEBUGGING_PORT) - port = Number(process.env.CYPRESS_REMOTE_DEBUGGING_PORT) - - if (port) { - return port - } - - return utils.getPort() + return port || utils.getPort() }) const pluginsBeforeBrowserLaunch = function (browser, args) { @@ -208,44 +203,36 @@ const _connectToChromeRemoteInterface = function (port) { }) } -const _maybeRecordVideo = (options) => { - return (function (client) { - if (!options.screencastFrame) { - debug('screencastFrame is false') +const _maybeRecordVideo = async function (client, options) { + if (!options.screencastFrame) { + debug('screencastFrame is false') - return client - } + return client + } - debug('starting screencast') - client.on('Page.screencastFrame', options.screencastFrame) + debug('starting screencast') + client.on('Page.screencastFrame', options.screencastFrame) - return client.send('Page.startScreencast', { - format: 'jpeg', - }) - .then(() => { - return client - }) + await client.send('Page.startScreencast', { + format: 'jpeg', }) + + return client } // a utility function that navigates to the given URL // once Chrome remote interface client is passed to it. -const _navigateUsingCRI = function (url) { +const _navigateUsingCRI = async function (client, url) { // @ts-ignore la(check.url(url), 'missing url to navigate to', url) - - return function (client) { - la(client, 'could not get CRI client') - debug('received CRI client') - debug('navigating to page %s', url) - - // when opening the blank page and trying to navigate - // the focus gets lost. Restore it and then navigate. - return client.send('Page.bringToFront') - .then(() => { - return client.send('Page.navigate', { url }) - }) - } + la(client, 'could not get CRI client') + debug('received CRI client') + debug('navigating to page %s', url) + + // when opening the blank page and trying to navigate + // the focus gets lost. Restore it and then navigate. + await client.send('Page.bringToFront') + await client.send('Page.navigate', { url }) } const _setAutomation = (client, automation) => { @@ -254,7 +241,7 @@ const _setAutomation = (client, automation) => { ) } -module.exports = { +export = { // // tip: // by adding utility functions that start with "_" @@ -273,7 +260,7 @@ module.exports = { _setAutomation, - _writeExtension (browser: Browser, options) { + async _writeExtension (browser: Browser, options) { if (browser.isHeadless) { debug('chrome is running headlessly, not installing extension') @@ -281,41 +268,35 @@ module.exports = { } // get the string bytes for the final extension file - return extension.setHostAndPath(options.proxyUrl, options.socketIoRoute).then(function (str) { - let extensionBg; let extensionDest + const str = await extension.setHostAndPath(options.proxyUrl, options.socketIoRoute) - extensionDest = utils.getExtensionDir(browser, options.isTextTerminal) - extensionBg = path.join(extensionDest, 'background.js') + const extensionDest = utils.getExtensionDir(browser, options.isTextTerminal) + const extensionBg = path.join(extensionDest, 'background.js') - // copy the extension src to the extension dist - return utils.copyExtension(pathToExtension, extensionDest).then(function () { - // and overwrite background.js with the final string bytes - return fs.writeFileAsync(extensionBg, str) - }).return(extensionDest) - }) - }, + // copy the extension src to the extension dist + await utils.copyExtension(pathToExtension, extensionDest) - _getArgs (options: CypressConfiguration = {}) { - let ps; let ua + // and overwrite background.js with the final string bytes + await fs.writeFileAsync(extensionBg, str) - _.defaults(options, { - browser: {}, - }) + return extensionDest + }, - const args = ([] as string[]).concat(defaultArgs) + _getArgs (browser: Browser, options: CypressConfiguration, port: string) { + const args = ([] as string[]).concat(DEFAULT_ARGS) if (os.platform() === 'linux') { args.push('--disable-gpu') args.push('--no-sandbox') } - ua = options.userAgent + const ua = options.userAgent if (ua) { args.push(`--user-agent=${ua}`) } - ps = options.proxyServer + const ps = options.proxyServer if (ps) { args.push(`--proxy-server=${ps}`) @@ -330,7 +311,7 @@ module.exports = { // https://github.com/cypress-io/cypress/issues/2037 // https://github.com/cypress-io/cypress/issues/2215 // https://github.com/cypress-io/cypress/issues/2223 - const { majorVersion } = options.browser + const { majorVersion, isHeadless } = browser if (CHROME_VERSIONS_WITH_BUGGY_ROOT_LAYER_SCROLLING.includes(majorVersion)) { args.push('--disable-blink-features=RootLayerScrolling') @@ -342,101 +323,92 @@ module.exports = { args.push('--proxy-bypass-list=<-loopback>') } + if (isHeadless) { + args.push('--headless') + } + + // force ipv4 + // https://github.com/cypress-io/cypress/issues/5912 + args.push(`--remote-debugging-port=${port}`) + args.push('--remote-debugging-address=127.0.0.1') + return args }, - open (browser: Browser, url, options: CypressConfiguration = {}, automation) { + async open (browser: Browser, url, options: CypressConfiguration = {}, automation) { const { isTextTerminal } = options const userDir = utils.getProfileDir(browser, isTextTerminal) - return Promise - .try(() => { - const args = this._getArgs(options) + const port = await getRemoteDebuggingPort() + + const defaultArgs = this._getArgs(browser, options, port) + + const [cacheDir, launchArgs] = await Bluebird.all([ + // ensure that we have a clean cache dir + // before launching the browser every time + utils.ensureCleanCache(browser, isTextTerminal), + pluginsBeforeBrowserLaunch(browser, defaultArgs), + ]) + + const [extDest] = await Bluebird.all([ + this._writeExtension( + browser, + options + ), + _removeRootExtension(), + _disableRestorePagesPrompt(userDir), + ]) + // normalize the --load-extensions argument by + // massaging what the user passed into our own + const args = _normalizeArgExtensions(extDest, launchArgs, browser) + + // this overrides any previous user-data-dir args + // by being the last one + args.push(`--user-data-dir=${userDir}`) + args.push(`--disk-cache-dir=${cacheDir}`) + + debug('launching in chrome with debugging port', { url, args, port }) + + // FIRST load the blank page + // first allows us to connect the remote interface, + // start video recording and then + // we will load the actual page + const launchedBrowser = await utils.launch(browser, 'about:blank', args) + + la(launchedBrowser, 'did not get launched browser instance') + + // SECOND connect to the Chrome remote interface + // and when the connection is ready + // navigate to the actual url + const criClient = await this._connectToChromeRemoteInterface(port) + + la(criClient, 'expected Chrome remote interface reference', criClient) + + await criClient.ensureMinimumProtocolVersion('1.3') + .catch((err) => { + throw new Error(`Cypress requires at least Chrome 64.\n\nDetails:\n${err.message}`) + }) - if (browser.isHeadless) { - args.push('--headless') - } + this._setAutomation(criClient, automation) - return getRemoteDebuggingPort() - .then((port) => { - // force ipv4 - // https://github.com/cypress-io/cypress/issues/5912 - args.push(`--remote-debugging-port=${port}`) - args.push('--remote-debugging-address=127.0.0.1') - - return Promise.all([ - // ensure that we have a clean cache dir - // before launching the browser every time - utils.ensureCleanCache(browser, isTextTerminal), - pluginsBeforeBrowserLaunch(options.browser, args), - port, - ]) - }) - }).spread((cacheDir, args: string[], port) => { - return Promise.all([ - this._writeExtension( - browser, - options - ), - _removeRootExtension(), - _disableRestorePagesPrompt(userDir), - ]) - .spread((extDest) => { - // normalize the --load-extensions argument by - // massaging what the user passed into our own - args = _normalizeArgExtensions(extDest, args, browser) - - // this overrides any previous user-data-dir args - // by being the last one - args.push(`--user-data-dir=${userDir}`) - args.push(`--disk-cache-dir=${cacheDir}`) - - debug('launching in chrome with debugging port', { url, args, port }) - - // FIRST load the blank page - // first allows us to connect the remote interface, - // start video recording and then - // we will load the actual page - return utils.launch(browser, 'about:blank', args) - }).then((launchedBrowser) => { - la(launchedBrowser, 'did not get launched browser instance') - - // SECOND connect to the Chrome remote interface - // and when the connection is ready - // navigate to the actual url - return this._connectToChromeRemoteInterface(port) - .then((criClient) => { - la(criClient, 'expected Chrome remote interface reference', criClient) - - return criClient.ensureMinimumProtocolVersion('1.3') - .catch((err) => { - throw new Error(`Cypress requires at least Chrome 64.\n\nDetails:\n${err.message}`) - }).then(() => { - this._setAutomation(criClient, automation) - - // monkey-patch the .kill method to that the CDP connection is closed - const originalBrowserKill = launchedBrowser.kill - - launchedBrowser.kill = (...args) => { - debug('closing remote interface client') - - return criClient.close() - .then(() => { - debug('closing chrome') - - return originalBrowserKill.apply(launchedBrowser, args) - }) - } - - return criClient - }) - }).then(this._maybeRecordVideo(options)) - .then(this._navigateUsingCRI(url)) - // return the launched browser process - // with additional method to close the remote connection - .return(launchedBrowser) - }) - }) + // monkey-patch the .kill method to that the CDP connection is closed + const originalBrowserKill = launchedBrowser.kill + + launchedBrowser.kill = async (...args) => { + debug('closing remote interface client') + + await criClient.close() + debug('closing chrome') + + await originalBrowserKill.apply(launchedBrowser, args) + } + + await this._maybeRecordVideo(criClient, options) + await this._navigateUsingCRI(criClient, url) + + // return the launched browser process + // with additional method to close the remote connection + return launchedBrowser }, } diff --git a/packages/server/package.json b/packages/server/package.json index 55685cb782d7..d9d42e87433c 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -175,7 +175,7 @@ "proxyquire": "2.1.0", "react": "15.6.2", "repl.history": "0.1.4", - "snap-shot-it": "7.9.1", + "snap-shot-it": "7.9.2", "ssestream": "1.0.1", "stream-to-promise": "1.1.1", "supertest": "4.0.2", diff --git a/packages/server/test/e2e/1_base_url_spec.coffee b/packages/server/test/e2e/1_base_url_spec.coffee index 9fb0fb68a1f3..e4c2e8394f26 100644 --- a/packages/server/test/e2e/1_base_url_spec.coffee +++ b/packages/server/test/e2e/1_base_url_spec.coffee @@ -15,7 +15,6 @@ describe "e2e baseUrl", -> e2e.it "passes", { spec: "base_url_spec.coffee" snapshot: true - expectedExitCode: 0 } context "http", -> @@ -32,5 +31,4 @@ describe "e2e baseUrl", -> e2e.it "passes", { spec: "base_url_spec.coffee" snapshot: true - expectedExitCode: 0 } diff --git a/packages/server/test/e2e/1_blacklist_hosts_spec.coffee b/packages/server/test/e2e/1_blacklist_hosts_spec.coffee index 9ed6d239eb6f..5fe7b70fa2e6 100644 --- a/packages/server/test/e2e/1_blacklist_hosts_spec.coffee +++ b/packages/server/test/e2e/1_blacklist_hosts_spec.coffee @@ -29,5 +29,4 @@ describe "e2e blacklist", -> e2e.exec(@, { spec: "blacklist_hosts_spec.coffee" snapshot: true - expectedExitCode: 0 }) diff --git a/packages/server/test/e2e/1_browserify_babel_es201spec.coffee b/packages/server/test/e2e/1_browserify_babel_es201spec.coffee index 10d27780d8d9..576afc870d48 100644 --- a/packages/server/test/e2e/1_browserify_babel_es201spec.coffee +++ b/packages/server/test/e2e/1_browserify_babel_es201spec.coffee @@ -10,7 +10,6 @@ describe "e2e browserify, babel, es2015", -> e2e.exec(@, { spec: "browserify_babel_es2015_passing_spec.coffee" snapshot: true - expectedExitCode: 0 }) it "fails", -> diff --git a/packages/server/test/e2e/1_cache_spec.coffee b/packages/server/test/e2e/1_cache_spec.coffee index fec2e88643cd..44c26866a902 100644 --- a/packages/server/test/e2e/1_cache_spec.coffee +++ b/packages/server/test/e2e/1_cache_spec.coffee @@ -44,13 +44,11 @@ describe "e2e cache", -> e2e.exec(@, { spec: "cache_spec.coffee" snapshot: true - expectedExitCode: 0 }) it "clears cache when browser is spawned", -> e2e.exec(@, { spec: "cache_clearing_spec.coffee" - expectedExitCode: 0 }) .then => ## only 1 request should have gone out @@ -58,7 +56,6 @@ describe "e2e cache", -> e2e.exec(@, { spec: "cache_clearing_spec.coffee" - expectedExitCode: 0 }) .then -> ## and after the cache is cleaned before diff --git a/packages/server/test/e2e/1_commands_outside_of_test_spec.coffee b/packages/server/test/e2e/1_commands_outside_of_test_spec.coffee index 7c9ef2b054c6..a4b43c895d42 100644 --- a/packages/server/test/e2e/1_commands_outside_of_test_spec.coffee +++ b/packages/server/test/e2e/1_commands_outside_of_test_spec.coffee @@ -18,5 +18,4 @@ describe "e2e commands outside of test", -> e2e.it "passes on passing assertions", { spec: "assertions_passing_outside_of_test_spec.coffee" snapshot: true - expectedExitCode: 0 } diff --git a/packages/server/test/e2e/1_interception_spec.coffee b/packages/server/test/e2e/1_interception_spec.coffee index f60c008d4040..49963d1e0784 100644 --- a/packages/server/test/e2e/1_interception_spec.coffee +++ b/packages/server/test/e2e/1_interception_spec.coffee @@ -59,5 +59,4 @@ describe "e2e interception spec", -> baseUrl: "http://localhost:9876" } snapshot: true - expectedExitCode: 0 }) diff --git a/packages/server/test/e2e/1_system_node_spec.coffee b/packages/server/test/e2e/1_system_node_spec.coffee index 7c8f42f64447..d9d365f8690f 100644 --- a/packages/server/test/e2e/1_system_node_spec.coffee +++ b/packages/server/test/e2e/1_system_node_spec.coffee @@ -25,7 +25,6 @@ describe "e2e system node", -> spec: "spec.js" sanitizeScreenshotDimensions: true snapshot: true - expectedExitCode: 0 }) .then ({ stderr }) -> expect(stderr).to.contain("Plugin Node version: #{expectedNodeVersion}") diff --git a/packages/server/test/e2e/2_cookies_spec.coffee b/packages/server/test/e2e/2_cookies_spec.coffee index 681e68c1b06c..0e07a43de446 100644 --- a/packages/server/test/e2e/2_cookies_spec.coffee +++ b/packages/server/test/e2e/2_cookies_spec.coffee @@ -170,7 +170,6 @@ describe "e2e cookies", -> } spec: "cookies_spec_baseurl.coffee" snapshot: true - expectedExitCode: 0 onRun: (exec) => exec({ originalTitle: "e2e cookies with baseurl" @@ -187,5 +186,4 @@ describe "e2e cookies", -> originalTitle: "e2e cookies with no baseurl" spec: "cookies_spec_no_baseurl.coffee" snapshot: true - expectedExitCode: 0 } diff --git a/packages/server/test/e2e/2_domain_spec.coffee b/packages/server/test/e2e/2_domain_spec.coffee index d40c565764e0..85af13da1e47 100644 --- a/packages/server/test/e2e/2_domain_spec.coffee +++ b/packages/server/test/e2e/2_domain_spec.coffee @@ -16,7 +16,6 @@ describe "e2e domain", -> e2e.it "passes", { spec: "domain*" snapshot: true - expectedExitCode: 0 config: { hosts } diff --git a/packages/server/test/e2e/2_go_spec.coffee b/packages/server/test/e2e/2_go_spec.coffee index ce41d44d83e3..a693d29c7a92 100644 --- a/packages/server/test/e2e/2_go_spec.coffee +++ b/packages/server/test/e2e/2_go_spec.coffee @@ -21,5 +21,4 @@ describe "e2e go", -> e2e.it "passes", { spec: "go_spec.coffee" snapshot: true - expectedExitCode: 0 } diff --git a/packages/server/test/e2e/2_headless_spec.ts b/packages/server/test/e2e/2_headless_spec.ts index 48283a55f9d2..ff98343c535d 100644 --- a/packages/server/test/e2e/2_headless_spec.ts +++ b/packages/server/test/e2e/2_headless_spec.ts @@ -12,7 +12,6 @@ describe('e2e headless', function () { 'EXPECT_HEADLESS': '1', }, }, - expectedExitCode: 0, headed: false, snapshot: true, }) @@ -25,7 +24,6 @@ describe('e2e headless', function () { 'CI': process.env.CI, }, }, - expectedExitCode: 0, headed: true, snapshot: true, // currently, Electron differs because it displays a diff --git a/packages/server/test/e2e/2_iframe_spec.coffee b/packages/server/test/e2e/2_iframe_spec.coffee index d697b5c35b94..19a8c29503d5 100644 --- a/packages/server/test/e2e/2_iframe_spec.coffee +++ b/packages/server/test/e2e/2_iframe_spec.coffee @@ -69,7 +69,6 @@ describe "e2e iframes", -> e2e.it "passes", { spec: "iframe_spec.coffee" snapshot: true - expectedExitCode: 0 config: { hosts: { "*.foo.com": "127.0.0.1" diff --git a/packages/server/test/e2e/2_images_spec.coffee b/packages/server/test/e2e/2_images_spec.coffee index a2e0c30d4b64..f5a2688f2291 100644 --- a/packages/server/test/e2e/2_images_spec.coffee +++ b/packages/server/test/e2e/2_images_spec.coffee @@ -14,5 +14,4 @@ describe "e2e images", -> e2e.it "passes", { spec: "images_spec.coffee" snapshot: true - expectedExitCode: 0 } diff --git a/packages/server/test/e2e/3_config_spec.coffee b/packages/server/test/e2e/3_config_spec.coffee index 0e239de98c26..ef79ca0a8182 100644 --- a/packages/server/test/e2e/3_config_spec.coffee +++ b/packages/server/test/e2e/3_config_spec.coffee @@ -17,7 +17,6 @@ describe "e2e config", -> e2e.exec(@, { spec: "config_passing_spec.coffee" snapshot: true - expectedExitCode: 0 config: { env: { scriptlet: "" diff --git a/packages/server/test/e2e/3_issue_2891_spec.coffee b/packages/server/test/e2e/3_issue_2891_spec.coffee index 126e57f3dd84..560eb4af49f7 100644 --- a/packages/server/test/e2e/3_issue_2891_spec.coffee +++ b/packages/server/test/e2e/3_issue_2891_spec.coffee @@ -12,5 +12,4 @@ describe "e2e issue 2891", -> spec: "default_layout_spec.js" sanitizeScreenshotDimensions: true snapshot: true - expectedExitCode: 0 }) diff --git a/packages/server/test/e2e/3_new_project_spec.coffee b/packages/server/test/e2e/3_new_project_spec.coffee index 100993b7d783..cb9639c4cf37 100644 --- a/packages/server/test/e2e/3_new_project_spec.coffee +++ b/packages/server/test/e2e/3_new_project_spec.coffee @@ -22,7 +22,6 @@ describe "e2e new project", -> project: noScaffoldingPath sanitizeScreenshotDimensions: true snapshot: true - expectedExitCode: 0 }) .then -> fs.statAsync(supportPath) diff --git a/packages/server/test/e2e/3_only_spec.coffee b/packages/server/test/e2e/3_only_spec.coffee index 42542b1326fc..8fd17d35f403 100644 --- a/packages/server/test/e2e/3_only_spec.coffee +++ b/packages/server/test/e2e/3_only_spec.coffee @@ -7,5 +7,4 @@ describe "e2e only spec", -> e2e.exec(@, { spec: "only*.coffee" snapshot: true - expectedExitCode: 0 }) diff --git a/packages/server/test/e2e/3_page_loading_spec.coffee b/packages/server/test/e2e/3_page_loading_spec.coffee index 36fb4a652a5b..9991845c7eb0 100644 --- a/packages/server/test/e2e/3_page_loading_spec.coffee +++ b/packages/server/test/e2e/3_page_loading_spec.coffee @@ -69,5 +69,4 @@ describe "e2e page_loading", -> e2e.it "passes", { spec: "page_loading_spec.coffee" snapshot: true - expectedExitCode: 0 } diff --git a/packages/server/test/e2e/3_plugins_spec.coffee b/packages/server/test/e2e/3_plugins_spec.coffee index a540469d4fb6..6612d3faade1 100644 --- a/packages/server/test/e2e/3_plugins_spec.coffee +++ b/packages/server/test/e2e/3_plugins_spec.coffee @@ -23,7 +23,6 @@ describe "e2e plugins", -> project: workingPreprocessor sanitizeScreenshotDimensions: true snapshot: true - expectedExitCode: 0 }) it "fails", -> @@ -43,7 +42,6 @@ describe "e2e plugins", -> project: pluginConfig sanitizeScreenshotDimensions: true snapshot: true - expectedExitCode: 0 }) it "catches invalid viewportWidth returned from plugins", -> @@ -80,7 +78,7 @@ describe "e2e plugins", -> snapshot: true # we are interested in the actual filtered available browser name # which should be "electron" - normalizeAvailableBrowsers: false + normalizeStdoutAvailableBrowsers: false }) e2e.it "works with user extensions", { @@ -90,7 +88,6 @@ describe "e2e plugins", -> project: pluginExtension sanitizeScreenshotDimensions: true snapshot: true - expectedExitCode: 0 } it "handles absolute path to pluginsFile", -> @@ -105,7 +102,6 @@ describe "e2e plugins", -> project: pluginsAbsolutePath sanitizeScreenshotDimensions: true snapshot: true - expectedExitCode: 0 }) it "calls after:screenshot for cy.screenshot() and failure screenshots", -> diff --git a/packages/server/test/e2e/3_user_agent_spec.coffee b/packages/server/test/e2e/3_user_agent_spec.coffee index d173c25fec9a..56fe000db863 100644 --- a/packages/server/test/e2e/3_user_agent_spec.coffee +++ b/packages/server/test/e2e/3_user_agent_spec.coffee @@ -26,5 +26,4 @@ describe "e2e user agent", -> e2e.it "passes", { spec: "user_agent_spec.coffee" snapshot: true - expectedExitCode: 0 } diff --git a/packages/server/test/e2e/4_browser_path_spec.coffee b/packages/server/test/e2e/4_browser_path_spec.coffee index 132e11835a91..a58fe08027c5 100644 --- a/packages/server/test/e2e/4_browser_path_spec.coffee +++ b/packages/server/test/e2e/4_browser_path_spec.coffee @@ -46,5 +46,4 @@ describe "e2e launching browsers by path", -> spec: "simple_spec.coffee" browser: foundPath snapshot: true - expectedExitCode: 0 }) diff --git a/packages/server/test/e2e/4_form_submissions_spec.coffee b/packages/server/test/e2e/4_form_submissions_spec.coffee index 19a8e687f1c2..a62b44f9ee29 100644 --- a/packages/server/test/e2e/4_form_submissions_spec.coffee +++ b/packages/server/test/e2e/4_form_submissions_spec.coffee @@ -86,7 +86,6 @@ describe "e2e forms", -> e2e.exec(@, { spec: "form_submission_passing_spec.coffee" snapshot: true - expectedExitCode: 0 }) it "failing", -> @@ -132,7 +131,6 @@ describe "e2e forms", -> } spec: "form_submission_multipart_spec.coffee" snapshot: true - expectedExitCode: 0 } e2e.it "passes with http on localhost", { @@ -141,5 +139,4 @@ describe "e2e forms", -> } spec: "form_submission_multipart_spec.coffee" snapshot: true - expectedExitCode: 0 } diff --git a/packages/server/test/e2e/4_request_spec.coffee b/packages/server/test/e2e/4_request_spec.coffee index 2f6a87ed00a3..21d5b4398362 100644 --- a/packages/server/test/e2e/4_request_spec.coffee +++ b/packages/server/test/e2e/4_request_spec.coffee @@ -141,7 +141,6 @@ describe "e2e requests", -> e2e.it "passes", { spec: "request_spec.coffee" snapshot: true - expectedExitCode: 0 } it "fails when network immediately fails", -> diff --git a/packages/server/test/e2e/4_screenshot_element_capture_spec.coffee b/packages/server/test/e2e/4_screenshot_element_capture_spec.coffee index 2095d2b1ee3b..b022e0968bc3 100644 --- a/packages/server/test/e2e/4_screenshot_element_capture_spec.coffee +++ b/packages/server/test/e2e/4_screenshot_element_capture_spec.coffee @@ -20,6 +20,5 @@ describe "e2e screenshot element capture", -> ## that the runner UI is hidden and that the page is scrolled properly e2e.it "passes", { spec: "screenshot_element_capture_spec.coffee" - expectedExitCode: 0 snapshot: true } diff --git a/packages/server/test/e2e/4_screenshot_fullpage_capture_spec.coffee b/packages/server/test/e2e/4_screenshot_fullpage_capture_spec.coffee index 9e5fa22983aa..2c9b1d069b12 100644 --- a/packages/server/test/e2e/4_screenshot_fullpage_capture_spec.coffee +++ b/packages/server/test/e2e/4_screenshot_fullpage_capture_spec.coffee @@ -22,6 +22,5 @@ describe "e2e screenshot fullPage capture", -> ## that the runner UI is hidden and that the page is scrolled properly e2e.it "passes", { spec: "screenshot_fullpage_capture_spec.coffee" - expectedExitCode: 0 snapshot: true } diff --git a/packages/server/test/e2e/4_screenshot_nested_file_spec.coffee b/packages/server/test/e2e/4_screenshot_nested_file_spec.coffee index 79990aec46be..97a3fafda617 100644 --- a/packages/server/test/e2e/4_screenshot_nested_file_spec.coffee +++ b/packages/server/test/e2e/4_screenshot_nested_file_spec.coffee @@ -5,6 +5,5 @@ describe "e2e screenshot in nested spec", -> e2e.it "passes", { spec: "nested-1/nested-2/screenshot_nested_file_spec.coffee" - expectedExitCode: 0 snapshot: true } diff --git a/packages/server/test/e2e/4_websockets_spec.coffee b/packages/server/test/e2e/4_websockets_spec.coffee index 386b10ccc85a..9203e763bf3b 100644 --- a/packages/server/test/e2e/4_websockets_spec.coffee +++ b/packages/server/test/e2e/4_websockets_spec.coffee @@ -33,5 +33,4 @@ describe "e2e websockets", -> e2e.it "passes", { spec: "websockets_spec.coffee" snapshot: true - expectedExitCode: 0 } diff --git a/packages/server/test/e2e/4_window_open_spec.coffee b/packages/server/test/e2e/4_window_open_spec.coffee index 04e92ca8e7be..cc225c825ce8 100644 --- a/packages/server/test/e2e/4_window_open_spec.coffee +++ b/packages/server/test/e2e/4_window_open_spec.coffee @@ -10,5 +10,4 @@ describe.skip "e2e window.open", -> # e2e.exec(@, { # spec: "window_open_spec.coffee" # snapshot: true - # expectedExitCode: 0 # }) diff --git a/packages/server/test/e2e/4_xhr_spec.coffee b/packages/server/test/e2e/4_xhr_spec.coffee index 5d2a67b43308..a1836a04e907 100644 --- a/packages/server/test/e2e/4_xhr_spec.coffee +++ b/packages/server/test/e2e/4_xhr_spec.coffee @@ -29,5 +29,4 @@ describe "e2e xhr", -> e2e.it "passes", { spec: "xhr_spec.coffee" snapshot: true - expectedExitCode: 0 } diff --git a/packages/server/test/e2e/5_screenshot_viewport_capture_spec.coffee b/packages/server/test/e2e/5_screenshot_viewport_capture_spec.coffee index 9969452a4297..493ae3e14a1e 100644 --- a/packages/server/test/e2e/5_screenshot_viewport_capture_spec.coffee +++ b/packages/server/test/e2e/5_screenshot_viewport_capture_spec.coffee @@ -18,6 +18,5 @@ describe "e2e screenshot viewport capture", -> ## captures (namely that the runner UI is hidden) e2e.it "passes", { spec: "screenshot_viewport_capture_spec.coffee" - expectedExitCode: 0 snapshot: true } diff --git a/packages/server/test/e2e/5_server_sent_events_spec.coffee b/packages/server/test/e2e/5_server_sent_events_spec.coffee index 30d797bf5f9d..256ee1004486 100644 --- a/packages/server/test/e2e/5_server_sent_events_spec.coffee +++ b/packages/server/test/e2e/5_server_sent_events_spec.coffee @@ -59,5 +59,4 @@ describe "e2e server sent events", -> e2e.it "passes", { spec: "server_sent_events_spec.coffee" snapshot: true - expectedExitCode: 0 } diff --git a/packages/server/test/e2e/5_stdout_spec.coffee b/packages/server/test/e2e/5_stdout_spec.coffee index 6e1e00a45d13..44ac813ced45 100644 --- a/packages/server/test/e2e/5_stdout_spec.coffee +++ b/packages/server/test/e2e/5_stdout_spec.coffee @@ -24,7 +24,6 @@ describe "e2e stdout", -> spec: "stdout_passing_spec.coffee" timeout: 120000 snapshot: true - expectedExitCode: 0 }) it "logs that electron cannot be recorded in headed mode", -> @@ -32,14 +31,12 @@ describe "e2e stdout", -> spec: "simple_spec.coffee" headed: true snapshot: true - expectedExitCode: 0 }) e2e.it "logs that chrome cannot be recorded", { spec: "simple_spec.coffee" browser: "chrome" snapshot: true - expectedExitCode: 0 } it "displays fullname of nested specfile", -> @@ -47,5 +44,4 @@ describe "e2e stdout", -> port: 2020 snapshot: true spec: "nested-1/nested-2/nested-3/*" - expectedExitCode: 0 }) diff --git a/packages/server/test/e2e/5_subdomain_spec.coffee b/packages/server/test/e2e/5_subdomain_spec.coffee index 7ae1a241532a..406780b89eee 100644 --- a/packages/server/test/e2e/5_subdomain_spec.coffee +++ b/packages/server/test/e2e/5_subdomain_spec.coffee @@ -105,7 +105,6 @@ describe "e2e subdomain", -> e2e.it "passes", { spec: "subdomain_spec.coffee" snapshot: true - expectedExitCode: 0 config: { hosts: { "*.foobar.com": "127.0.0.1" diff --git a/packages/server/test/e2e/6_task_spec.coffee b/packages/server/test/e2e/6_task_spec.coffee index 916750f369da..7e4a1c61fbb4 100644 --- a/packages/server/test/e2e/6_task_spec.coffee +++ b/packages/server/test/e2e/6_task_spec.coffee @@ -23,5 +23,4 @@ describe "e2e task", -> spec: "multiple_task_registrations_spec.coffee" sanitizeScreenshotDimensions: true snapshot: true - expectedExitCode: 0 }) diff --git a/packages/server/test/e2e/6_video_compression_spec.coffee b/packages/server/test/e2e/6_video_compression_spec.coffee index 7bd5cceb2429..0090372d2f19 100644 --- a/packages/server/test/e2e/6_video_compression_spec.coffee +++ b/packages/server/test/e2e/6_video_compression_spec.coffee @@ -21,7 +21,6 @@ describe "e2e video compression", -> MS_PER_TEST } } - expectedExitCode: 0 onRun: (exec) -> process.env.VIDEO_COMPRESSION_THROTTLE = 10 diff --git a/packages/server/test/e2e/6_viewport_spec.coffee b/packages/server/test/e2e/6_viewport_spec.coffee index 6f4ec612b0a6..931159d8a044 100644 --- a/packages/server/test/e2e/6_viewport_spec.coffee +++ b/packages/server/test/e2e/6_viewport_spec.coffee @@ -11,5 +11,4 @@ describe "e2e viewport", -> e2e.it "passes", { spec: "viewport_spec.coffee" snapshot: true - expectedExitCode: 0 } diff --git a/packages/server/test/e2e/6_visit_spec.coffee b/packages/server/test/e2e/6_visit_spec.coffee index a3e9433bab7a..29b7603a2660 100644 --- a/packages/server/test/e2e/6_visit_spec.coffee +++ b/packages/server/test/e2e/6_visit_spec.coffee @@ -97,7 +97,6 @@ describe "e2e visit", -> e2e.it "passes", { spec: "visit_spec.coffee" snapshot: true - expectedExitCode: 0 onRun: (exec) -> startTlsV1Server(6776) .then (serv) -> @@ -131,6 +130,7 @@ describe "e2e visit", -> } e2e.it "calls onBeforeLoad when overwriting cy.visit", { + snapshot: true spec: "issue_2196_spec.coffee" } diff --git a/packages/server/test/e2e/6_web_security_spec.coffee b/packages/server/test/e2e/6_web_security_spec.coffee index acd72b9c5548..d2f9a737c25e 100644 --- a/packages/server/test/e2e/6_web_security_spec.coffee +++ b/packages/server/test/e2e/6_web_security_spec.coffee @@ -82,5 +82,4 @@ describe "e2e web security", -> e2e.it "passes", { spec: "web_security_spec.coffee" snapshot: true - expectedExitCode: 0 } diff --git a/packages/server/test/e2e/7_record_spec.coffee b/packages/server/test/e2e/7_record_spec.coffee index 98ef7f8a397b..051ce454ad2e 100644 --- a/packages/server/test/e2e/7_record_spec.coffee +++ b/packages/server/test/e2e/7_record_spec.coffee @@ -492,7 +492,6 @@ describe "e2e record", -> snapshot: true tag: "nightly" ciBuildId: "ciBuildId123" - expectedExitCode: 0 config: { trashAssetsBeforeRuns: false } @@ -558,7 +557,6 @@ describe "e2e record", -> spec: "record_pass*" record: true snapshot: true - expectedExitCode: 0 }) .then -> console.log('GETREQUESTURLS', getRequestUrls()) @@ -577,7 +575,6 @@ describe "e2e record", -> record: true parallel: true snapshot: true - expectedExitCode: 0 }) .then -> expect(getRequestUrls()).to.be.empty @@ -593,7 +590,6 @@ describe "e2e record", -> spec: "record_pass*" record: true snapshot: true - expectedExitCode: 0 }) context "api interaction errors", -> @@ -657,7 +653,6 @@ describe "e2e record", -> spec: "record_pass*" record: true snapshot: true - expectedExitCode: 0 }) .then -> urls = getRequestUrls() @@ -676,7 +671,6 @@ describe "e2e record", -> record: true snapshot: true ciBuildId: "ciBuildId123" - expectedExitCode: 0 }) .then -> urls = getRequestUrls() @@ -996,7 +990,6 @@ describe "e2e record", -> spec: "record_pass*" record: true snapshot: true - expectedExitCode: 0 }) .then -> urls = getRequestUrls() @@ -1038,7 +1031,6 @@ describe "e2e record", -> spec: "record_pass*" record: true snapshot: true - expectedExitCode: 0 }) .then -> urls = getRequestUrls() @@ -1099,7 +1091,6 @@ describe "e2e record", -> spec: "record_pass*" record: true snapshot: true - expectedExitCode: 0 }) .then -> urls = getRequestUrls() @@ -1161,7 +1152,6 @@ describe "e2e record", -> spec: "record_pass*" record: true snapshot: true - expectedExitCode: 0 }) .then -> urls = getRequestUrls() @@ -1236,7 +1226,6 @@ describe "e2e record", -> parallel: true snapshot: true ciBuildId: "ciBuildId123" - expectedExitCode: 0 }) .then -> urls = getRequestUrls() @@ -1289,7 +1278,6 @@ describe "e2e record", -> spec: "record_pass*" record: true snapshot: true - expectedExitCode: 0 }) describe "grace period - over tests limit", -> @@ -1324,7 +1312,6 @@ describe "e2e record", -> spec: "record_pass*" record: true snapshot: true - expectedExitCode: 0 }) describe "grace period - parallel feature", -> @@ -1358,7 +1345,6 @@ describe "e2e record", -> spec: "record_pass*" record: true snapshot: true - expectedExitCode: 0 }) describe "grace period - grouping feature", -> @@ -1392,7 +1378,6 @@ describe "e2e record", -> spec: "record_pass*" record: true snapshot: true - expectedExitCode: 0 }) describe "paid plan - over private tests limit", -> @@ -1427,7 +1412,6 @@ describe "e2e record", -> spec: "record_pass*" record: true snapshot: true - expectedExitCode: 0 }) describe "paid plan - over tests limit", -> @@ -1462,7 +1446,6 @@ describe "e2e record", -> spec: "record_pass*" record: true snapshot: true - expectedExitCode: 0 }) describe "unknown warning", -> @@ -1483,5 +1466,4 @@ describe "e2e record", -> spec: "record_pass*" record: true snapshot: true - expectedExitCode: 0 }) diff --git a/packages/server/test/e2e/8_network_error_handling_spec.coffee b/packages/server/test/e2e/8_network_error_handling_spec.coffee index aa1b54cdd3c2..9612c3259a5c 100644 --- a/packages/server/test/e2e/8_network_error_handling_spec.coffee +++ b/packages/server/test/e2e/8_network_error_handling_spec.coffee @@ -41,9 +41,7 @@ launchBrowser = (url, opts = {}) -> ## headless breaks automatic retries ## "--headless" ].concat( - chrome._getArgs({ - browser: browser - }) + chrome._getArgs(browser) ).filter (arg) -> ![ ## seems to break chrome's automatic retries @@ -405,7 +403,6 @@ describe "e2e network error handling", -> e2e.exec(@, { spec: "https_passthru_spec.js" snapshot: true - expectedExitCode: 0 }) .then -> console.log("connect counts are", connectCounts) @@ -430,7 +427,6 @@ describe "e2e network error handling", -> e2e.exec(@, { spec: "https_passthru_spec.js" snapshot: true - expectedExitCode: 0 config: { baseUrl: "https://localhost:#{HTTPS_PORT}" } @@ -460,7 +456,6 @@ describe "e2e network error handling", -> baseUrl: "http://localhost:#{PORT}" pageLoadTimeout: 4000 } - expectedExitCode: 0 snapshot: true }) @@ -480,7 +475,6 @@ describe "e2e network error handling", -> baseUrl: "http://localhost:#{PORT}" pageLoadTimeout: 4000 } - expectedExitCode: 0 snapshot: true }) @@ -510,6 +504,5 @@ describe "e2e network error handling", -> baseUrl: "http://localhost:#{PORT}" pageLoadTimeout: 4000 } - expectedExitCode: 0 snapshot: true }) diff --git a/packages/server/test/e2e/8_reporters_spec.coffee b/packages/server/test/e2e/8_reporters_spec.coffee index 720d4c4e714b..f4256030a1af 100644 --- a/packages/server/test/e2e/8_reporters_spec.coffee +++ b/packages/server/test/e2e/8_reporters_spec.coffee @@ -39,7 +39,6 @@ describe "e2e reporters", -> it "supports junit reporter and reporter options", -> e2e.exec(@, { spec: "simple_passing_spec.coffee" - expectedExitCode: 0 snapshot: true reporter: "junit" reporterOptions: "mochaFile=junit-output/result.[hash].xml,testCaseSwitchClassnameAndName=true" @@ -59,14 +58,12 @@ describe "e2e reporters", -> e2e.exec(@, { spec: "simple_passing_spec.coffee" snapshot: true - expectedExitCode: 0 reporter: "reporters/custom.js" }) it "sends file to reporter", -> e2e.exec(@, { spec: "simple_passing_spec.coffee" - expectedExitCode: 0 reporter: "reporters/uses-file.js" }) .get("stdout") @@ -79,7 +76,6 @@ describe "e2e reporters", -> e2e.exec(@, { spec: "simple_passing_spec.coffee" snapshot: true - expectedExitCode: 0 reporter: ma }) .then -> @@ -119,7 +115,6 @@ describe "e2e reporters", -> it "supports teamcity reporter and reporter options", -> e2e.exec(@, { spec: "simple_passing_spec.coffee" - expectedExitCode: 0 snapshot: true reporter: "teamcity" reporterOptions: "topLevelSuite=top suite,flowId=12345,useStdError='true',useStdError='true',recordHookFailures='true',actualVsExpected='true'" diff --git a/packages/server/test/performance/cy_visit_performance_spec.js b/packages/server/test/performance/cy_visit_performance_spec.js index 40049f676a2c..ea7a63731ca4 100644 --- a/packages/server/test/performance/cy_visit_performance_spec.js +++ b/packages/server/test/performance/cy_visit_performance_spec.js @@ -27,26 +27,19 @@ context('cy.visit performance tests', function () { return stdout.replace(/^\d+%\s+of visits to [^\s]+ finished in less than.*$/gm, 'histogram line') } - context('pass', function () { - [ - 'chrome', - 'electron', - ].forEach((browser) => { - it(`in ${browser}`, function () { - return e2e.exec(this, { - spec: 'fast_visit_spec.coffee', - snapshot: true, - expectedExitCode: 0, - config: { - video: false, - env: { - currentRetry: this.test._currentRetry, - }, + e2e.it('passes', { + onStdout, + spec: 'fast_visit_spec.coffee', + snapshot: true, + onRun (exec, browser, ctx) { + return exec({ + config: { + video: false, + env: { + currentRetry: ctx.test._currentRetry, }, - browser, - onStdout, - }) + }, }) - }) + }, }) }) diff --git a/packages/server/test/performance/proxy_performance_spec.js b/packages/server/test/performance/proxy_performance_spec.js index 4e33d1219d8f..f7056629b1a5 100644 --- a/packages/server/test/performance/proxy_performance_spec.js +++ b/packages/server/test/performance/proxy_performance_spec.js @@ -104,15 +104,6 @@ const TEST_CASES = [ }) }) -let defaultArgs = _getArgs() - -// additionally... -defaultArgs = defaultArgs.concat([ - '--headless', - '--disable-background-networking', - '--no-sandbox', // allows us to run as root, for CI -]) - const average = (arr) => { return _.sum(arr) / arr.length } @@ -188,8 +179,16 @@ const getResultsFromHar = (har) => { const runBrowserTest = (urlUnderTest, testCase) => { const cdpPort = CDP_PORT + Math.round(Math.random() * 10000) - let args = defaultArgs.concat([ - `--remote-debugging-port=${cdpPort}`, + const browser = { + isHeadless: true, + } + + const options = {} + + const args = _getArgs(browser, options, cdpPort).concat([ + // additionally... + '--disable-background-networking', + '--no-sandbox', // allows us to run as root, for CI `--user-data-dir=${fse.mkdtempSync(path.join(os.tmpdir(), 'cy-perf-'))}`, ]) diff --git a/packages/server/test/support/fixtures/projects/e2e/cypress/integration/issue_2196_spec.coffee b/packages/server/test/support/fixtures/projects/e2e/cypress/integration/issue_2196_spec.coffee index 6c62904cd957..986d6ebe9998 100644 --- a/packages/server/test/support/fixtures/projects/e2e/cypress/integration/issue_2196_spec.coffee +++ b/packages/server/test/support/fixtures/projects/e2e/cypress/integration/issue_2196_spec.coffee @@ -1,7 +1,9 @@ -onBeforeLoad = cy.stub() -onLoad = cy.stub() +onLoad = onBeforeLoad = null Cypress.Commands.overwrite "visit", (originalVisit, url, options) -> + onBeforeLoad = cy.stub().as('onBeforeLoad') + onLoad = cy.stub().as('onLoad') + return originalVisit(url, { onBeforeLoad, onLoad }) context "issue #2196: overwriting visit", -> diff --git a/packages/server/test/support/helpers/e2e.js b/packages/server/test/support/helpers/e2e.js index e6fa7c78c524..363fdc771a83 100644 --- a/packages/server/test/support/helpers/e2e.js +++ b/packages/server/test/support/helpers/e2e.js @@ -1,7 +1,7 @@ -let e2e - require('../../spec_helper') +require('mocha-banner').register() +const chalk = require('chalk').default const _ = require('lodash') let cp = require('child_process') const niv = require('npm-install-version') @@ -9,6 +9,7 @@ const path = require('path') const http = require('http') const human = require('human-interval') const morgan = require('morgan') +const stream = require('stream') const express = require('express') const Promise = require('bluebird') const snapshot = require('snap-shot-it') @@ -96,7 +97,7 @@ const replaceUploadingResults = function (orig, ...rest) { } const normalizeStdout = function (str, options = {}) { - const normalizeOptions = _.defaults({}, options, { normalizeAvailableBrowsers: true }) + const { normalizeStdoutAvailableBrowsers } = options // remove all of the dynamic parts of stdout // to normalize against what we expected @@ -105,10 +106,12 @@ const normalizeStdout = function (str, options = {}) { // (Required when paths are printed outside of our own formatting) .split(pathUpToProjectName).join('/foo/bar/.projects') - if (normalizeOptions.normalizeAvailableBrowsers) { + // unless normalization is explicitly turned off then + // always normalize the stdout replacing the browser text + if (normalizeStdoutAvailableBrowsers !== false) { // usually we are not interested in the browsers detected on this particular system // but some tests might filter / change the list of browsers - // in that case the test should pass "normalizeAvailableBrowsers:false" as options + // in that case the test should pass "normalizeStdoutAvailableBrowsers: false" as options str = str.replace(availableBrowsersRe, '$1browser1, browser2, browser3') } @@ -150,24 +153,19 @@ const ensurePort = function (port) { } const startServer = function (obj) { - let s; let srv const { onServer, port, https } = obj ensurePort(port) const app = express() - if (https) { - srv = httpsProxy.httpsServer(app) - } else { - srv = http.Server(app) - } + const srv = https ? httpsProxy.httpsServer(app) : new http.Server(app) allowDestroy(srv) app.use(morgan('dev')) - s = obj.static + const s = obj.static if (s) { const opts = _.isObject(s) ? s : {} @@ -232,60 +230,71 @@ const getMochaItFn = function (only, skip, browser, specifiedBrowser) { return it } -const getBrowsers = function (generateTestsForDefaultBrowsers, browser, defaultBrowsers) { - // if we're generating tests for default browsers - if (generateTestsForDefaultBrowsers) { - // then return an array of default browsers - return defaultBrowsers +function getBrowsers (browserPattern) { + if (!browserPattern.length) { + return DEFAULT_BROWSERS + } + + let selected = [] + + const addBrowsers = _.clone(browserPattern) + const removeBrowsers = _.remove(addBrowsers, (b) => b.startsWith('!')).map((b) => b.slice(1)) + + if (removeBrowsers.length) { + selected = _.without(DEFAULT_BROWSERS, ...removeBrowsers) + } else { + selected = _.intersection(DEFAULT_BROWSERS, addBrowsers) + } + + if (!selected.length) { + throw new Error(`options.browser: "${browserPattern}" matched no browsers`) } - // but if we haven't been told to generate tests for default browsers - // and weren't provided a specified browser then throw - if (!browser) { - throw new Error('A browser must be specified when { generateTestsForDefaultBrowsers: false }.') + return selected +} + +const normalizeToArray = (value) => { + if (value && !_.isArray(value)) { + return [value] } - // otherwise return the specified browser - return [browser] + return value } -const localItFn = function (title, options = {}) { - options = _ - .chain(options) - .clone() - .defaults({ +const localItFn = function (title, opts = {}) { + opts.browser = normalizeToArray(opts.browser) + + const DEFAULT_OPTIONS = { only: false, skip: false, - browser: process.env.BROWSER, - generateTestsForDefaultBrowsers: true, - useSeparateBrowserSnapshots: false, + browser: [], + snapshot: false, + spec: 'no spec name supplied!', + onStdout: _.identity, onRun (execFn, browser, ctx) { return execFn() }, - }) - .value() + } - const { only, skip, browser, generateTestsForDefaultBrowsers, onRun } = options + const options = _.defaults({}, opts, DEFAULT_OPTIONS) if (!title) { throw new Error('e2e.it(...) must be passed a title as the first argument') } // LOGIC FOR AUTOGENERATING DYNAMIC TESTS - // - if generateTestsForDefaultBrowsers - // - create multiple tests for each default browser - // - if browser is specified in options: - // ...skip the tests for each default browser if that browser - // ...does not match the specified one (used in CI) - // - else only generate a single test with the specified browser + // - create multiple tests for each default browser + // - if browser is specified in options: + // ...skip the tests for each default browser if that browser + // ...does not match the specified one (used in CI) // run the tests for all the default browsers, or if a browser // has been specified, only run it for that - const specifiedBrowser = browser - const browsersToTest = getBrowsers(generateTestsForDefaultBrowsers, browser, DEFAULT_BROWSERS) + const specifiedBrowser = process.env.BROWSER + const browsersToTest = getBrowsers(options.browser) const browserToTest = function (browser) { - const mochaItFn = getMochaItFn(only, skip, browser, specifiedBrowser) + const mochaItFn = getMochaItFn(options.only, options.skip, browser, specifiedBrowser) const testTitle = `${title} [${browser}]` @@ -298,9 +307,11 @@ const localItFn = function (title, options = {}) { const ctx = this - const execFn = (overrides = {}) => e2e.exec(ctx, _.extend({ originalTitle }, options, overrides, { browser })) + const execFn = (overrides = {}) => { + return e2e.exec(ctx, _.extend({ originalTitle }, options, overrides, { browser })) + } - return onRun(execFn, browser, ctx) + return options.onRun(execFn, browser, ctx) }) } @@ -319,7 +330,20 @@ localItFn.skip = function (title, options) { return localItFn(title, options) } -module.exports = (e2e = { +const maybeVerifyExitCode = (expectedExitCode, fn) => { + // bail if this is explicitly null so + // devs can turn off checking the exit code + if (expectedExitCode === null) { + return + } + + return fn() +} + +const e2e = { + + replaceStackTraceLines, + normalizeStdout, it: localItFn, @@ -327,12 +351,6 @@ module.exports = (e2e = { snapshot (...args) { args = _.compact(args) - // grab the last element in index - const index = args.length - 1 - - // normalize the stdout of it - args[index] = normalizeStdout(args[index]) - return snapshot.apply(null, args) }, @@ -401,9 +419,7 @@ module.exports = (e2e = { this.servers = null }).then(() => { - let s - - s = options.settings + const s = options.settings if (s) { return settings.write(e2ePath, s) @@ -412,15 +428,13 @@ module.exports = (e2e = { }) return afterEach(function () { - let s - process.env = _.clone(env) this.timeout(human('2 minutes')) Fixtures.remove() - s = this.servers + const s = this.servers if (s) { return Promise.map(s, stopServer) @@ -429,19 +443,19 @@ module.exports = (e2e = { }, options (ctx, options = {}) { - let spec - _.defaults(options, { browser: 'electron', project: e2ePath, timeout: options.exit === false ? 3000000 : 120000, originalTitle: null, + expectedExitCode: 0, sanitizeScreenshotDimensions: false, + normalizeStdoutAvailableBrowsers: true, }) ctx.timeout(options.timeout) - spec = options.spec + const { spec } = options if (spec) { // normalize into array and then prefix @@ -547,11 +561,11 @@ module.exports = (e2e = { return cypress.start(args) .then(() => { - let code + const { expectedExitCode } = options - if ((code = options.expectedExitCode) != null) { - return expect(process.exit).to.be.calledWith(code) - } + maybeVerifyExitCode(expectedExitCode, () => { + expect(process.exit).to.be.calledWith(expectedExitCode) + }) }) }, @@ -565,18 +579,16 @@ module.exports = (e2e = { let stderr = '' const exit = function (code) { - let expected + const { expectedExitCode } = options - if ((expected = options.expectedExitCode) != null) { - expect(code).to.eq(expected, 'expected exit code') - } + maybeVerifyExitCode(expectedExitCode, () => { + expect(code).to.eq(expectedExitCode, 'expected exit code') + }) // snapshot the stdout! if (options.snapshot) { // enable callback to modify stdout - let matches; let ostd; let str - - ostd = options.onStdout + const ostd = options.onStdout if (ostd) { stdout = ostd(stdout) @@ -584,7 +596,7 @@ module.exports = (e2e = { // if we have browser in the stdout make // sure its legit - matches = browserNameVersionRe.exec(stdout) + const matches = browserNameVersionRe.exec(stdout) if (matches) { // eslint-disable-next-line no-unused-vars @@ -607,7 +619,7 @@ module.exports = (e2e = { } } - str = normalizeStdout(stdout, options) + const str = normalizeStdout(stdout, options) if (options.originalTitle) { snapshot(options.originalTitle, str, { allowSharedSnapshot: true }) @@ -647,10 +659,19 @@ module.exports = (e2e = { .value(), }) + const ColorOutput = function () { + const colorOutput = new stream.Transform() + + colorOutput._transform = (chunk, encoding, cb) => cb(null, chalk.magenta(chunk.toString())) + + return colorOutput + } + // pipe these to our current process // so we can see them in the terminal - sp.stdout.pipe(process.stdout) - sp.stderr.pipe(process.stderr) + // color it so we can tell which is test output + sp.stdout.pipe(ColorOutput()).pipe(process.stdout) + sp.stderr.pipe(ColorOutput()).pipe(process.stderr) sp.stdout.on('data', (buf) => stdout += buf.toString()) sp.stderr.on('data', (buf) => stderr += buf.toString()) @@ -675,4 +696,6 @@ module.exports = (e2e = { `) } }, -}) +} + +module.exports = e2e diff --git a/packages/server/test/unit/browsers/chrome_spec.coffee b/packages/server/test/unit/browsers/chrome_spec.coffee index 0c2b8ffbc1a8..fb767d2391dc 100644 --- a/packages/server/test/unit/browsers/chrome_spec.coffee +++ b/packages/server/test/unit/browsers/chrome_spec.coffee @@ -29,7 +29,6 @@ describe "lib/browsers/chrome", -> kill: sinon.stub().returns() } - sinon.stub(chrome, "_getArgs").returns(@args) sinon.stub(chrome, "_writeExtension").resolves("/path/to/ext") sinon.stub(chrome, "_connectToChromeRemoteInterface").resolves(@criClient) sinon.stub(plugins, "has") @@ -59,6 +58,8 @@ describe "lib/browsers/chrome", -> expect(plugins.execute).not.to.be.called it "is noop if newArgs are not returned", -> + sinon.stub(chrome, "_getArgs").returns(@args) + plugins.has.returns(true) plugins.execute.resolves(null) @@ -78,7 +79,7 @@ describe "lib/browsers/chrome", -> .then => args = utils.launch.firstCall.args[2] - expect(args).to.deep.eq([ + expect(args).to.include.members([ "--headless" "--remote-debugging-port=50505" "--remote-debugging-address=127.0.0.1" @@ -169,40 +170,40 @@ describe "lib/browsers/chrome", -> it "disables gpu when linux", -> sinon.stub(os, "platform").returns("linux") - args = chrome._getArgs() + args = chrome._getArgs({}, {}) expect(args).to.include("--disable-gpu") it "does not disable gpu when not linux", -> sinon.stub(os, "platform").returns("darwin") - args = chrome._getArgs() + args = chrome._getArgs({}, {}) expect(args).not.to.include("--disable-gpu") it "turns off sandbox when linux", -> sinon.stub(os, "platform").returns("linux") - args = chrome._getArgs() + args = chrome._getArgs({}, {}) expect(args).to.include("--no-sandbox") it "does not turn off sandbox when not linux", -> sinon.stub(os, "platform").returns("win32") - args = chrome._getArgs() + args = chrome._getArgs({}, {}) expect(args).not.to.include("--no-sandbox") it "adds user agent when options.userAgent", -> - args = chrome._getArgs({ + args = chrome._getArgs({}, { userAgent: "foo" }) expect(args).to.include("--user-agent=foo") it "does not add user agent", -> - args = chrome._getArgs() + args = chrome._getArgs({}, {}) expect(args).not.to.include("--user-agent=foo") @@ -211,10 +212,8 @@ describe "lib/browsers/chrome", -> disabledRootLayerScrolling = (version, bool) -> args = chrome._getArgs({ - browser: { - majorVersion: version - } - }) + majorVersion: version + }, {}) if bool expect(args).to.include(arg) @@ -232,10 +231,8 @@ describe "lib/browsers/chrome", -> chromeVersionHasLoopback = (version, bool) -> args = chrome._getArgs({ - browser: { - majorVersion: version - } - }) + majorVersion: version + }, {}) if bool expect(args).to.include(arg) From 94176149c45acd993962e15b90bbc90a488270c2 Mon Sep 17 00:00:00 2001 From: Ben Kucera <14625260+Bkucera@users.noreply.github.com> Date: Wed, 5 Feb 2020 13:07:59 -0500 Subject: [PATCH 43/49] deprecated before:browser:launch event (#6293) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * deprecate old API for before:browser:launch * trigger ci * allow chrome preferences to be overwritten in before:browser:launch * add e2e test for chrome preferences * add launchOptions.windowSize, refactor plugin handling into browser/utils * async/await-ify chrome.ts, update e2e.js * add padding to warning, allow more than 1 warning in run mode * remove psInclude * bump snap-shot-it, remove patch-package * fix failing tests, tighten up code * remove unused Promise.all * more code cleanup from coffeescript conversion * fix expectedExitcode * update snapshot with cross origin error message * more code cleanup from coffeescript conversion * remove global state, cleanup normalizing stdout in snapshots * fix asserting on the expected exit code * remove firefox a default browser in e2e tests * remove dupe const * make onStdout return first arg by default * remove only * make expectedExitCode: 0 the default, remove duplicate option from all e2e tests * fix test for electron * reuse _getArgs() properly, tighten up dupe code, move perf e2e tests to use new e2e.it helpers * make e2e test pass properly, and capture snapshot - this was being overlooked because expectedExitCode wasn’t being set (until now its the default) * revert firefox specific changes to reduce PR diff * remove newline * temp 02/04/20 [skip ci] * add more scenarios / return values * rename plugin deprecation project fixture * prevent warnings in run mode from being called multiple times in the same browser launch - add plugin integration tests - cleanup some promise code in browsers/chrome - refactor the e2e deprecation tests * update e2e deprecation specs, test that deprecation warning is displayed once per spec on a run * cleanup let -> const, coffescript conversion * bump snap-shot-it, remove patch-package (cherry picked from commit ba23be5349990e3c8e0c9a987cbd9de38d6f3c26) * bump cli snap-shot-it * cleanup: let -> const, parseFloat, formatting * revert cross origin normalization * fix spacing * add types for plugin events * dont pad warning message in run mode * add e2e test for adding extensions before:browser:launch * fix failing electron unit test, consolidate creating default launch options * add extensions for electron + warning if install fails * fix failing tests, yield the right before:browser:launch args signature * e2e test electron via devtools extension * remove .only * add stdout assertions to e2e/1_deprecated_spec * remove snapshot whitespace * rename deprecation event * update deprecated browser launch args warning message * throw error on unexpected bbl property * add tests for warning + error on bbl * update snapshot * revive 2 useful tests to validate how launchOption args are merged * try fix e2e fullscreen spec, update snapshots * tighten up code for throwing errors on unknown launch options properties - list out the unknown and expected properties using existing conventions - tighten up the error message a bit * rename options -> launchOptions, add e2e test for adding unknown properties to launchOptions, removed dupe snapshots * fix fullscreen e2e test * only push user gesture arg if chromium * add e2e tests for throwing + rejecting errors, run e2e tests on all browsers, avoid ps logic only in electron * remove conditional args in fullscreen test * dont automatically install the latest version of chrome in circle * add stubs for new electron properties * switch from using port 5555 to 5544 to avoid common conflicts * temporarily commenting out windowSize launchOption so release is unblocked and all tests pass - can add it back in later once the e2e tests pass in CI * make the path to chrome profile correctly dynamic to account for all operating systems * remove magic length(8) and slice out the first 4 custom args set from the plugin launch options * add back --start-maximized in chrome * deleting windowSize-related code... for now! Co-authored-by: Zach Bloomquist Co-authored-by: Brian Mann Co-authored-by: Chris Breiding --- cli/package.json | 2 +- cli/types/index.d.ts | 51 +++ .../desktop-gui/src/project-nav/browsers.jsx | 12 +- .../desktop-gui/src/project/project-model.js | 19 +- packages/desktop-gui/src/project/project.jsx | 4 +- packages/driver/test/cypress/plugins/index.js | 3 + packages/reporter/src/main-runner.scss | 20 +- packages/reporter/src/main.scss | 20 +- .../__snapshots__/1_deprecated_spec.ts.js | 340 ++++++++++++++++++ .../4_before_browser_launch_spec.ts.js | 109 ++++++ packages/server/lib/browsers/chrome.ts | 152 ++++++-- packages/server/lib/browsers/electron.coffee | 35 +- packages/server/lib/browsers/protocol.js | 6 +- packages/server/lib/browsers/utils.ts | 149 +++++++- packages/server/lib/errors.coffee | 40 ++- packages/server/lib/gui/events.coffee | 2 + packages/server/lib/gui/windows.coffee | 16 + packages/server/lib/modes/run.js | 19 +- .../server/lib/plugins/child/run_plugins.js | 45 ++- packages/server/lib/plugins/index.coffee | 6 + packages/server/lib/plugins/util.coffee | 2 +- packages/server/lib/project.js | 1 + packages/server/package.json | 2 +- packages/server/test/e2e/1_deprecated_spec.ts | 124 +++++++ .../test/e2e/4_before_browser_launch_spec.ts | 37 ++ .../server/test/integration/cypress_spec.js | 21 +- .../server/test/integration/plugins_spec.js | 42 +++ .../projects/browser-extensions/cypress.json | 1 + .../cypress/integration/spec.js | 13 + .../cypress/plugins/index.js | 19 + .../devtools-ext/devtools.html | 5 + .../devtools-ext/manifest.json | 13 + .../projects/browser-extensions/index.html | 6 + .../chrome-browser-preferences/cypress.json | 1 + .../cypress/integration/spec.js | 5 + .../cypress/plugins/index.js | 44 +++ .../projects/e2e/cypress/plugins/index.js | 6 +- .../cypress.json | 1 + .../cypress/integration/app_spec.js | 12 + .../cypress/integration/app_spec2.js | 12 + .../cypress/plugins/index.js | 123 +++++++ .../cypress/plugins/index.coffee | 8 +- .../cypress/plugins/index.coffee | 6 +- .../cypress/plugins.js | 10 +- .../test/support/helpers/electron_stub.coffee | 3 + .../test/unit/browsers/browsers_spec.coffee | 25 +- .../test/unit/browsers/chrome_spec.coffee | 158 ++++++-- .../test/unit/browsers/electron_spec.coffee | 29 +- packages/server/test/unit/modes/run_spec.js | 1 + .../plugins/child/run_plugins_spec.coffee | 26 +- 50 files changed, 1630 insertions(+), 176 deletions(-) create mode 100644 packages/server/__snapshots__/1_deprecated_spec.ts.js create mode 100644 packages/server/__snapshots__/4_before_browser_launch_spec.ts.js create mode 100644 packages/server/test/e2e/1_deprecated_spec.ts create mode 100644 packages/server/test/e2e/4_before_browser_launch_spec.ts create mode 100644 packages/server/test/integration/plugins_spec.js create mode 100644 packages/server/test/support/fixtures/projects/browser-extensions/cypress.json create mode 100644 packages/server/test/support/fixtures/projects/browser-extensions/cypress/integration/spec.js create mode 100644 packages/server/test/support/fixtures/projects/browser-extensions/cypress/plugins/index.js create mode 100644 packages/server/test/support/fixtures/projects/browser-extensions/devtools-ext/devtools.html create mode 100644 packages/server/test/support/fixtures/projects/browser-extensions/devtools-ext/manifest.json create mode 100644 packages/server/test/support/fixtures/projects/browser-extensions/index.html create mode 100644 packages/server/test/support/fixtures/projects/chrome-browser-preferences/cypress.json create mode 100644 packages/server/test/support/fixtures/projects/chrome-browser-preferences/cypress/integration/spec.js create mode 100644 packages/server/test/support/fixtures/projects/chrome-browser-preferences/cypress/plugins/index.js create mode 100644 packages/server/test/support/fixtures/projects/plugin-before-browser-launch-deprecation/cypress.json create mode 100644 packages/server/test/support/fixtures/projects/plugin-before-browser-launch-deprecation/cypress/integration/app_spec.js create mode 100644 packages/server/test/support/fixtures/projects/plugin-before-browser-launch-deprecation/cypress/integration/app_spec2.js create mode 100644 packages/server/test/support/fixtures/projects/plugin-before-browser-launch-deprecation/cypress/plugins/index.js diff --git a/cli/package.json b/cli/package.json index a12c73513060..7e13161d4479 100644 --- a/cli/package.json +++ b/cli/package.json @@ -89,7 +89,7 @@ "proxyquire": "2.1.0", "shelljs": "0.8.3", "sinon": "7.2.2", - "snap-shot-it": "7.9.1", + "snap-shot-it": "7.9.2", "spawn-mock": "1.0.0", "strip-ansi": "4.0.0" }, diff --git a/cli/types/index.d.ts b/cli/types/index.d.ts index 569df10816be..469220ca0b49 100644 --- a/cli/types/index.d.ts +++ b/cli/types/index.d.ts @@ -50,6 +50,7 @@ declare namespace Cypress { type RequestBody = string | object type ViewportOrientation = "portrait" | "landscape" type PrevSubject = "optional" | "element" | "document" | "window" + type PluginConfig = (on: PluginEvents, config: ConfigOptions) => void interface CommandOptions { prevSubject: boolean | PrevSubject | PrevSubject[] @@ -4243,6 +4244,56 @@ declare namespace Cypress { * These are the most useful events for you to listen to. * @see https://on.cypress.io/catalog-of-events#App-Events */ + + interface browserLaunchOptions { + extensions: string[], + preferences: {[key: string]: any} + args: string[], + } + + interface dimensions { + width: number + height: number + } + + interface screenshotDetails { + size: number + takenAt: string + duration: number + dimensions: dimensions + multipart: boolean + pixelRatio: number + name: string + specName: string + testFailure: boolean + path: string + scaled: boolean + blackout: string[] + } + + interface afterScreenshotReturnObject { + path?: string + size?: number + dimensions?: dimensions + } + + interface fileObject { + filePath: string + outputPath: string + shouldWatch: boolean + } + + interface tasks { + [key: string]: (value: any) => any + } + + interface PluginEvents { + (action: 'before:browser:launch', fn: (browser: Browser, browserLaunchOptions: browserLaunchOptions) => browserLaunchOptions): void + (action: 'after:screenshot', fn: (details: screenshotDetails) => afterScreenshotReturnObject | Promise): void + (action: 'file:preprocessor', fn: (file: fileObject) => string | Promise): void + (action: 'task', tasks: tasks): void + } + interface Actions { /** * Fires when an uncaught exception occurs in your application. diff --git a/packages/desktop-gui/src/project-nav/browsers.jsx b/packages/desktop-gui/src/project-nav/browsers.jsx index 35c4171f460d..64b336f2aac3 100644 --- a/packages/desktop-gui/src/project-nav/browsers.jsx +++ b/packages/desktop-gui/src/project-nav/browsers.jsx @@ -76,8 +76,16 @@ export default class Browsers extends Component { {prefixText}{' '} {browser.displayName}{' '} {browser.majorVersion} - {this._warn(browser)} + {browser.family === 'firefox' && + beta} {this._info(browser)} + {this._warn(browser)} + ) } @@ -104,7 +112,7 @@ export default class Browsers extends Component { return ( } placement='bottom' className='browser-info-tooltip cy-tooltip' > diff --git a/packages/desktop-gui/src/project/project-model.js b/packages/desktop-gui/src/project/project-model.js index 454991c08ceb..0001fbaff1dc 100644 --- a/packages/desktop-gui/src/project/project-model.js +++ b/packages/desktop-gui/src/project/project-model.js @@ -56,7 +56,8 @@ export default class Project { @observable browserState = 'closed' @observable resolvedConfig @observable error - @observable warnings = [] + /** @type {{[key: string] : {warning:Error & {dismissed: boolean}}}} */ + @observable warnings = {} @observable apiError @observable parentTestsFolderDisplay @observable integrationExampleName @@ -216,22 +217,24 @@ export default class Project { } @action addWarning (warning) { - if (!this.dismissedWarnings[this._serializeWarning(warning)]) { - this.warnings.push(warning) + const id = warning.type + + if (id && this.warnings[id] && this.warnings[id].dismissed) { + return } + + this.warnings[id] = { ...warning } } @action clearWarning (warning) { if (!warning) { // calling with no warning clears all warnings - return this.warnings.map((warning) => { + return _.each(this.warnings, ((warning) => { return this.clearWarning(warning) - }) + })) } - this.dismissedWarnings[this._serializeWarning(warning)] = true - - this.warnings = _.without(this.warnings, warning) + warning.dismissed = true } _serializeWarning (warning) { diff --git a/packages/desktop-gui/src/project/project.jsx b/packages/desktop-gui/src/project/project.jsx index 39476896ed99..521963a302a0 100644 --- a/packages/desktop-gui/src/project/project.jsx +++ b/packages/desktop-gui/src/project/project.jsx @@ -1,6 +1,7 @@ import React, { Component } from 'react' import { observer } from 'mobx-react' import Loader from 'react-loader' +import _ from 'lodash' import C from '../lib/constants' import projectsApi from '../projects/projects-api' @@ -77,8 +78,7 @@ class Project extends Component { _renderWarnings = () => { const { warnings } = this.props.project - return warnings.map((warning, i) => - ( this._removeWarning(warning)}/>)) + return _.map(warnings, (warning, i) => (!warning.dismissed && this._removeWarning(warning)}/>)) } _removeWarning = (warning) => { diff --git a/packages/driver/test/cypress/plugins/index.js b/packages/driver/test/cypress/plugins/index.js index 91a6540921b2..ee321ee0af4a 100644 --- a/packages/driver/test/cypress/plugins/index.js +++ b/packages/driver/test/cypress/plugins/index.js @@ -9,6 +9,9 @@ const webpack = require('@cypress/webpack-preprocessor') const webpackOptions = require('@packages/runner/webpack.config.ts').default +/** + * @type {Cypress.PluginConfig} + */ module.exports = (on) => { on('file:preprocessor', webpack({ webpackOptions })) diff --git a/packages/reporter/src/main-runner.scss b/packages/reporter/src/main-runner.scss index 79331ea842d8..60a47867aaa9 100644 --- a/packages/reporter/src/main-runner.scss +++ b/packages/reporter/src/main-runner.scss @@ -6,13 +6,13 @@ @import './!(lib)*/**/*'; /* Used to provide additional context for screen readers */ -.visually-hidden { - border: 0; - clip: rect(0 0 0 0); - height: 1px; - margin: -1px; - overflow: hidden; - padding: 0; - position: absolute; - width: 1px; -} \ No newline at end of file +.visually-hidden { + border: 0; + clip: rect(0 0 0 0); + height: 1px; + margin: -1px; + overflow: hidden; + padding: 0; + position: absolute; + width: 1px; +} diff --git a/packages/reporter/src/main.scss b/packages/reporter/src/main.scss index 769165813b55..bb5e78d22141 100644 --- a/packages/reporter/src/main.scss +++ b/packages/reporter/src/main.scss @@ -7,13 +7,13 @@ @import '!(lib)*/**/*'; /* Used to provide additional context for screen readers */ -.visually-hidden { - border: 0; - clip: rect(0 0 0 0); - height: 1px; - margin: -1px; - overflow: hidden; - padding: 0; - position: absolute; - width: 1px; -} \ No newline at end of file +.visually-hidden { + border: 0; + clip: rect(0 0 0 0); + height: 1px; + margin: -1px; + overflow: hidden; + padding: 0; + position: absolute; + width: 1px; +} diff --git a/packages/server/__snapshots__/1_deprecated_spec.ts.js b/packages/server/__snapshots__/1_deprecated_spec.ts.js new file mode 100644 index 000000000000..47266b393ec5 --- /dev/null +++ b/packages/server/__snapshots__/1_deprecated_spec.ts.js @@ -0,0 +1,340 @@ +exports['deprecated before:browser:launch args / push and no return - warns user exactly once'] = ` + +==================================================================================================== + + (Run Starting) + + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Cypress: 1.2.3 │ + │ Browser: FooBrowser 88 │ + │ Specs: 1 found (app_spec.js) │ + │ Searched: cypress/integration/app_spec.js │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + + +──────────────────────────────────────────────────────────────────────────────────────────────────── + + Running: app_spec.js (1 of 1) +Deprecation Warning: The \`before:browser:launch\` plugin event changed its signature in version \`4.0.0\` + +The \`before:browser:launch\` plugin event switched from yielding the second argument as an \`array\` of browser arguments to an options \`object\` with an \`args\` property. + +We've detected that your code is still using the previous, deprecated interface signature. + +This code will not work in a future version of Cypress. Please see the upgrade guide: https://on.cypress.io/deprecated-before-browser-launch-args + + + ✓ asserts on browser args + + 1 passing + + + (Results) + + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Tests: 1 │ + │ Passing: 1 │ + │ Failing: 0 │ + │ Pending: 0 │ + │ Skipped: 0 │ + │ Screenshots: 0 │ + │ Video: false │ + │ Duration: X seconds │ + │ Spec Ran: app_spec.js │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + + +==================================================================================================== + + (Run Finished) + + + Spec Tests Passing Failing Pending Skipped + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ ✔ app_spec.js XX:XX 1 1 - - - │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + ✔ All specs passed! XX:XX 1 1 - - - + + +` + +exports['deprecated before:browser:launch args / using non-deprecated API - no warning'] = ` + +==================================================================================================== + + (Run Starting) + + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Cypress: 1.2.3 │ + │ Browser: FooBrowser 88 │ + │ Specs: 1 found (app_spec.js) │ + │ Searched: cypress/integration/app_spec.js │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + + +──────────────────────────────────────────────────────────────────────────────────────────────────── + + Running: app_spec.js (1 of 1) + + + ✓ asserts on browser args + + 1 passing + + + (Results) + + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Tests: 1 │ + │ Passing: 1 │ + │ Failing: 0 │ + │ Pending: 0 │ + │ Skipped: 0 │ + │ Screenshots: 0 │ + │ Video: false │ + │ Duration: X seconds │ + │ Spec Ran: app_spec.js │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + + +==================================================================================================== + + (Run Finished) + + + Spec Tests Passing Failing Pending Skipped + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ ✔ app_spec.js XX:XX 1 1 - - - │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + ✔ All specs passed! XX:XX 1 1 - - - + + +` + +exports['deprecated before:browser:launch args / no mutate return'] = ` + +==================================================================================================== + + (Run Starting) + + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Cypress: 1.2.3 │ + │ Browser: FooBrowser 88 │ + │ Specs: 1 found (app_spec.js) │ + │ Searched: cypress/integration/app_spec.js │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + + +──────────────────────────────────────────────────────────────────────────────────────────────────── + + Running: app_spec.js (1 of 1) + + + ✓ asserts on browser args + + 1 passing + + + (Results) + + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Tests: 1 │ + │ Passing: 1 │ + │ Failing: 0 │ + │ Pending: 0 │ + │ Skipped: 0 │ + │ Screenshots: 0 │ + │ Video: false │ + │ Duration: X seconds │ + │ Spec Ran: app_spec.js │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + + +==================================================================================================== + + (Run Finished) + + + Spec Tests Passing Failing Pending Skipped + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ ✔ app_spec.js XX:XX 1 1 - - - │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + ✔ All specs passed! XX:XX 1 1 - - - + + +` + +exports['deprecated before:browser:launch args / concat return returns once per spec'] = ` + +==================================================================================================== + + (Run Starting) + + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Cypress: 1.2.3 │ + │ Browser: FooBrowser 88 │ + │ Specs: 2 found (app_spec.js, app_spec2.js) │ + │ Searched: cypress/integration/app_spec.js, cypress/integration/app_spec2.js │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + + +──────────────────────────────────────────────────────────────────────────────────────────────────── + + Running: app_spec.js (1 of 2) +Deprecation Warning: The \`before:browser:launch\` plugin event changed its signature in version \`4.0.0\` + +The \`before:browser:launch\` plugin event switched from yielding the second argument as an \`array\` of browser arguments to an options \`object\` with an \`args\` property. + +We've detected that your code is still using the previous, deprecated interface signature. + +This code will not work in a future version of Cypress. Please see the upgrade guide: https://on.cypress.io/deprecated-before-browser-launch-args + + + ✓ asserts on browser args + + 1 passing + + + (Results) + + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Tests: 1 │ + │ Passing: 1 │ + │ Failing: 0 │ + │ Pending: 0 │ + │ Skipped: 0 │ + │ Screenshots: 0 │ + │ Video: false │ + │ Duration: X seconds │ + │ Spec Ran: app_spec.js │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + + +──────────────────────────────────────────────────────────────────────────────────────────────────── + + Running: app_spec2.js (2 of 2) +Deprecation Warning: The \`before:browser:launch\` plugin event changed its signature in version \`4.0.0\` + +The \`before:browser:launch\` plugin event switched from yielding the second argument as an \`array\` of browser arguments to an options \`object\` with an \`args\` property. + +We've detected that your code is still using the previous, deprecated interface signature. + +This code will not work in a future version of Cypress. Please see the upgrade guide: https://on.cypress.io/deprecated-before-browser-launch-args + + + ✓ 2 - asserts on browser args + + 1 passing + + + (Results) + + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Tests: 1 │ + │ Passing: 1 │ + │ Failing: 0 │ + │ Pending: 0 │ + │ Skipped: 0 │ + │ Screenshots: 0 │ + │ Video: false │ + │ Duration: X seconds │ + │ Spec Ran: app_spec2.js │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + + +==================================================================================================== + + (Run Finished) + + + Spec Tests Passing Failing Pending Skipped + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ ✔ app_spec.js XX:XX 1 1 - - - │ + ├────────────────────────────────────────────────────────────────────────────────────────────────┤ + │ ✔ app_spec2.js XX:XX 1 1 - - - │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + ✔ All specs passed! XX:XX 2 2 - - - + + +` + +exports['deprecated before:browser:launch args / fails when adding unknown properties to launchOptions'] = ` + +==================================================================================================== + + (Run Starting) + + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Cypress: 1.2.3 │ + │ Browser: FooBrowser 88 │ + │ Specs: 1 found (app_spec.js) │ + │ Searched: cypress/integration/app_spec.js │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + + +──────────────────────────────────────────────────────────────────────────────────────────────────── + + Running: app_spec.js (1 of 1) +The \`launchOptions\` object returned by your plugin's \`browser:before:launch\` handler contained unexpected properties: + +- foo +- width +- height + +\`launchOptions\` may only contain the properties: + +- preferences +- extensions +- args + +https://on.cypress.io/browser-launch-api + +` + +exports['deprecated before:browser:launch args / displays errors thrown and aborts the run'] = ` + +==================================================================================================== + + (Run Starting) + + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Cypress: 1.2.3 │ + │ Browser: FooBrowser 88 │ + │ Specs: 2 found (app_spec.js, app_spec2.js) │ + │ Searched: cypress/integration/app_spec.js, cypress/integration/app_spec2.js │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + + +──────────────────────────────────────────────────────────────────────────────────────────────────── + + Running: app_spec.js (1 of 2) +Error thrown from plugins handler +Error: Error thrown from plugins handler + [stack trace lines] + + +` + +exports['deprecated before:browser:launch args / displays promises rejected and aborts the run'] = ` + +==================================================================================================== + + (Run Starting) + + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Cypress: 1.2.3 │ + │ Browser: FooBrowser 88 │ + │ Specs: 2 found (app_spec.js, app_spec2.js) │ + │ Searched: cypress/integration/app_spec.js, cypress/integration/app_spec2.js │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + + +──────────────────────────────────────────────────────────────────────────────────────────────────── + + Running: app_spec.js (1 of 2) +Promise rejected from plugins handler +Error: Promise rejected from plugins handler + [stack trace lines] + + +` diff --git a/packages/server/__snapshots__/4_before_browser_launch_spec.ts.js b/packages/server/__snapshots__/4_before_browser_launch_spec.ts.js new file mode 100644 index 000000000000..13e7874adf94 --- /dev/null +++ b/packages/server/__snapshots__/4_before_browser_launch_spec.ts.js @@ -0,0 +1,109 @@ +exports['e2e before:browser:launch / modifies preferences on disk if DNE'] = ` + +==================================================================================================== + + (Run Starting) + + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Cypress: 1.2.3 │ + │ Browser: FooBrowser 88 │ + │ Specs: 1 found (spec.js) │ + │ Searched: cypress/integration/spec.js │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + + +──────────────────────────────────────────────────────────────────────────────────────────────────── + + Running: spec.js (1 of 1) + + + chrome browser prefs e2e + ✓ has the expected prefs + + + 1 passing + + + (Results) + + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Tests: 1 │ + │ Passing: 1 │ + │ Failing: 0 │ + │ Pending: 0 │ + │ Skipped: 0 │ + │ Screenshots: 0 │ + │ Video: false │ + │ Duration: X seconds │ + │ Spec Ran: spec.js │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + + +==================================================================================================== + + (Run Finished) + + + Spec Tests Passing Failing Pending Skipped + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ ✔ spec.js XX:XX 1 1 - - - │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + ✔ All specs passed! XX:XX 1 1 - - - + + +` + +exports['e2e before:browser:launch / can add extensions'] = ` + +==================================================================================================== + + (Run Starting) + + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Cypress: 1.2.3 │ + │ Browser: FooBrowser 88 │ + │ Specs: 1 found (spec.js) │ + │ Searched: cypress/integration/spec.js │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + + +──────────────────────────────────────────────────────────────────────────────────────────────────── + + Running: spec.js (1 of 1) + + + before:browser:launch extension e2e + ✓ has the expected extension + + + 1 passing + + + (Results) + + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Tests: 1 │ + │ Passing: 1 │ + │ Failing: 0 │ + │ Pending: 0 │ + │ Skipped: 0 │ + │ Screenshots: 0 │ + │ Video: false │ + │ Duration: X seconds │ + │ Spec Ran: spec.js │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + + +==================================================================================================== + + (Run Finished) + + + Spec Tests Passing Failing Pending Skipped + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ ✔ spec.js XX:XX 1 1 - - - │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + ✔ All specs passed! XX:XX 1 1 - - - + + +` diff --git a/packages/server/lib/browsers/chrome.ts b/packages/server/lib/browsers/chrome.ts index 1f8585e8cace..d5fce3a9127c 100644 --- a/packages/server/lib/browsers/chrome.ts +++ b/packages/server/lib/browsers/chrome.ts @@ -1,18 +1,18 @@ +import Bluebird from 'bluebird' +import check from 'check-more-types' +import debugModule from 'debug' +import la from 'lazy-ass' import _ from 'lodash' import os from 'os' import path from 'path' -import Bluebird 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 fs from '../util/fs' import { CdpAutomation } from './cdp_automation' import * as CriClient from './cri-client' +import protocol from './protocol' +import utils from './utils' // TODO: this is defined in `cypress-npm-api` but there is currently no way to get there type CypressConfiguration = any @@ -23,14 +23,24 @@ type Browser = FoundBrowser & { isHeaded: boolean } -const plugins = require('../plugins') - const debug = debugModule('cypress:server:browsers:chrome') const LOAD_EXTENSION = '--load-extension=' const CHROME_VERSIONS_WITH_BUGGY_ROOT_LAYER_SCROLLING = '66 67'.split(' ') const CHROME_VERSION_INTRODUCING_PROXY_BYPASS_ON_LOOPBACK = 72 +const CHROME_PREFERENCE_PATHS = { + default: path.join('Default', 'Preferences'), + defaultSecure: path.join('Default', 'Secure Preferences'), + localState: 'Local State', +} + +type ChromePreferences = { + default: object + defaultSecure: object + localState: object +} + const pathToExtension = extension.getPathToExtension() const pathToTheme = extension.getPathToTheme() @@ -104,27 +114,71 @@ const DEFAULT_ARGS = [ '--use-mock-keychain', ] -const getRemoteDebuggingPort = Bluebird.method(() => { - const port = Number(process.env.CYPRESS_REMOTE_DEBUGGING_PORT) +/** + * Reads all known preference files (CHROME_PREFERENCE_PATHS) from disk and retur + * @param userDir + */ +const _getChromePreferences = (userDir: string): Bluebird => { + debug('reading chrome preferences... %o', { userDir, CHROME_PREFERENCE_PATHS }) - return port || utils.getPort() -}) + return Bluebird.props(_.mapValues(CHROME_PREFERENCE_PATHS, (prefPath) => { + return fs.readJson(path.join(userDir, prefPath)) + .catch((err) => { + // return empty obj if it doesn't exist + if (err.code === 'ENOENT') { + return {} + } -const pluginsBeforeBrowserLaunch = function (browser, args) { - // bail if we're not registered to this event - if (!plugins.has('before:browser:launch')) { - return args - } + throw err + }) + })) +} + +const _mergeChromePreferences = (originalPrefs: ChromePreferences, newPrefs: ChromePreferences): ChromePreferences => { + return _.mapValues(CHROME_PREFERENCE_PATHS, (_v, prefPath) => { + const original = _.cloneDeep(originalPrefs[prefPath]) - return plugins.execute('before:browser:launch', browser, args) - .then((newArgs) => { - debug('got user args for \'before:browser:launch\'', newArgs) + if (!newPrefs[prefPath]) { + return original + } - // reset args if we got 'em - return newArgs != null ? newArgs : args + let deletions: any[] = [] + + _.mergeWith(original, newPrefs[prefPath], (_objValue, newValue, key, obj) => { + if (newValue == null) { + // setting a key to null should remove it + deletions.push([obj, key]) + } + }) + + deletions.forEach(([obj, key]) => { + delete obj[key] + }) + + return original }) } +const _writeChromePreferences = (userDir: string, originalPrefs: ChromePreferences, newPrefs: ChromePreferences) => { + return Bluebird.map(_.keys(originalPrefs), (key) => { + const originalJson = originalPrefs[key] + const newJson = newPrefs[key] + + if (!newJson || _.isEqual(originalJson, newJson)) { + return + } + + return fs.outputJson(path.join(userDir, CHROME_PREFERENCE_PATHS[key]), newJson) + }) + .return() +} + +const getRemoteDebuggingPort = async () => { + const port = Number(process.env.CYPRESS_REMOTE_DEBUGGING_PORT) + + return port || utils.getPort() +} + /** * Merge the different `--load-extension` arguments into one. * @@ -133,12 +187,12 @@ const pluginsBeforeBrowserLaunch = function (browser, args) { * @param browser the current browser being launched * @returns the modified list of arguments */ -const _normalizeArgExtensions = function (extPath, args, browser: Browser): string[] { +const _normalizeArgExtensions = function (extPath, args, pluginExtensions, browser: Browser): string[] { if (browser.isHeadless) { return args } - let userExtensions + let userExtensions = [] const loadExtension = _.find(args, (arg) => { return arg.includes(LOAD_EXTENSION) }) @@ -147,7 +201,11 @@ const _normalizeArgExtensions = function (extPath, args, browser: Browser): stri args = _.without(args, loadExtension) // form into array, enabling users to pass multiple extensions - userExtensions = loadExtension.replace(LOAD_EXTENSION, '').split(',') + userExtensions = userExtensions.concat(loadExtension.replace(LOAD_EXTENSION, '').split(',')) + } + + if (pluginExtensions) { + userExtensions = userExtensions.concat(pluginExtensions) } const extensions = [].concat(userExtensions, extPath, pathToTheme) @@ -170,9 +228,7 @@ const _disableRestorePagesPrompt = function (userDir) { return fs.readJson(prefsPath) .then((preferences) => { - let profile - - profile = preferences.profile + const profile = preferences.profile if (profile) { if ((profile['exit_type'] !== 'Normal') || (profile['exited_cleanly'] !== true)) { @@ -181,10 +237,11 @@ const _disableRestorePagesPrompt = function (userDir) { profile['exit_type'] = 'Normal' profile['exited_cleanly'] = true - return fs.writeJson(prefsPath, preferences) + return fs.outputJson(prefsPath, preferences) } } - }).catch(() => {}) + }) + .catch(() => { }) } // After the browser has been opened, we can connect to @@ -204,14 +261,14 @@ const _connectToChromeRemoteInterface = function (port) { } const _maybeRecordVideo = async function (client, options) { - if (!options.screencastFrame) { - debug('screencastFrame is false') + if (!options.onScreencastFrame) { + debug('options.onScreencastFrame is false') return client } debug('starting screencast') - client.on('Page.screencastFrame', options.screencastFrame) + client.on('Page.screencastFrame', options.onScreencastFrame) await client.send('Page.startScreencast', { format: 'jpeg', @@ -260,6 +317,12 @@ export = { _setAutomation, + _getChromePreferences, + + _mergeChromePreferences, + + _writeChromePreferences, + async _writeExtension (browser: Browser, options) { if (browser.isHeadless) { debug('chrome is running headlessly, not installing extension') @@ -340,17 +403,29 @@ export = { const userDir = utils.getProfileDir(browser, isTextTerminal) - const port = await getRemoteDebuggingPort() + const [port, preferences] = await Bluebird.all([ + getRemoteDebuggingPort(), + _getChromePreferences(userDir), + ]) const defaultArgs = this._getArgs(browser, options, port) - const [cacheDir, launchArgs] = await Bluebird.all([ + const defaultLaunchOptions = utils.getDefaultLaunchOptions({ + preferences, + args: defaultArgs, + }) + + const [cacheDir, launchOptions] = await Bluebird.all([ // ensure that we have a clean cache dir // before launching the browser every time utils.ensureCleanCache(browser, isTextTerminal), - pluginsBeforeBrowserLaunch(browser, defaultArgs), + utils.executeBeforeBrowserLaunch(browser, defaultLaunchOptions, options), ]) + if (launchOptions.preferences) { + launchOptions.preferences = _mergeChromePreferences(preferences, launchOptions.preferences as ChromePreferences) + } + const [extDest] = await Bluebird.all([ this._writeExtension( browser, @@ -358,10 +433,11 @@ export = { ), _removeRootExtension(), _disableRestorePagesPrompt(userDir), + _writeChromePreferences(userDir, preferences, launchOptions.preferences as ChromePreferences), ]) // normalize the --load-extensions argument by // massaging what the user passed into our own - const args = _normalizeArgExtensions(extDest, launchArgs, browser) + const args = _normalizeArgExtensions(extDest, launchOptions.args, launchOptions.extensions, 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 6a19d46554a5..55c6a7ea581d 100644 --- a/packages/server/lib/browsers/electron.coffee +++ b/packages/server/lib/browsers/electron.coffee @@ -11,6 +11,8 @@ appData = require("../util/app_data") plugins = require("../plugins") savedState = require("../saved_state") profileCleaner = require("../util/profile_cleaner") +utils = require('./utils') +errors = require('../errors') ## additional events that are nice to know about to be logged ## https://electronjs.org/docs/api/browser-window#instance-events @@ -41,6 +43,13 @@ getAutomation = (win) -> CdpAutomation(sendCommand) +_installExtensions = (extensionPaths = [], options) -> + extensionPaths.forEach (path) -> + try + Windows.installExtension(path) + catch + options.onWarning(errors.get('EXTENSION_NOT_LOADED', 'Electron', path)) + module.exports = { _defaultOptions: (projectRoot, state, options) -> _this = @ @@ -157,7 +166,7 @@ module.exports = { originalSendCommand.call(webContents.debugger, message, data) .then (res) -> - if debug.enabled && res.data && res.data.length > 100 + if debug.enabled && _.get(res, 'data.length') > 100 res = _.clone(res) res.data = res.data.slice(0, 100) + ' [truncated]' debug('debugger: received response to %s: %o', message, res) @@ -223,6 +232,7 @@ module.exports = { debug("received saved state %o", state) ## get our electron default options + ## TODO: this is bad, don't mutate the options object options = @_defaultOptions(projectRoot, state, options) ## get the GUI window defaults now @@ -230,22 +240,19 @@ module.exports = { debug("browser window options %o", _.omitBy(options, _.isFunction)) - Bluebird - .try => - ## bail if we're not registered to this event - return options if not plugins.has("before:browser:launch") + defaultLaunchOptions = utils.getDefaultLaunchOptions({ + preferences: options, + }) - plugins.execute("before:browser:launch", options.browser, options) - .then (newOptions) -> - if newOptions - debug("received new options from plugin event %o", newOptions) - _.extend(options, newOptions) + return utils.executeBeforeBrowserLaunch(browser, defaultLaunchOptions, options) + .then (launchOptions) => + { preferences } = launchOptions - return options - .then (options) => debug("launching browser window to url: %s", url) - @_render(url, projectRoot, automation, options) + _installExtensions(launchOptions.extensions, options) + + @_render(url, projectRoot, automation, preferences) .then (win) => ## cause the webview to receive focus so that ## native browser focus + blur events fire correctly @@ -257,6 +264,8 @@ module.exports = { win.once "closed", -> debug("closed event fired") + Windows.resetExtensions() + events.emit("exit") instance = _.extend events, { diff --git a/packages/server/lib/browsers/protocol.js b/packages/server/lib/browsers/protocol.js index 5504f0298100..407dc9449964 100644 --- a/packages/server/lib/browsers/protocol.js +++ b/packages/server/lib/browsers/protocol.js @@ -31,9 +31,6 @@ function _connectAsync (opts) { // can be closed, just needed to test the connection sock.end() }) - .catch((err) => { - errors.throw('CDP_COULD_NOT_CONNECT', opts.port, err) - }) } /** @@ -94,6 +91,9 @@ const getWsTargetFor = (port) => { } return _connectAsync(connectOpts) + .catch((err) => { + errors.throw('CDP_COULD_NOT_CONNECT', port, err) + }) .then(() => { const retry = () => { debug('attempting to find CRI target... %o', { retryIndex }) diff --git a/packages/server/lib/browsers/utils.ts b/packages/server/lib/browsers/utils.ts index 50a9ca4482b1..9ed8fd02ee91 100644 --- a/packages/server/lib/browsers/utils.ts +++ b/packages/server/lib/browsers/utils.ts @@ -1,15 +1,22 @@ +import _ from 'lodash' import { FoundBrowser } from '@packages/launcher' +// @ts-ignore +import errors from '../errors' +// @ts-ignore +import plugins from '../plugins' const path = require('path') const debug = require('debug')('cypress:server:browsers:utils') -const Promise = require('bluebird') +const Bluebird = require('bluebird') const getPort = require('get-port') const launcher = require('@packages/launcher') const fs = require('../util/fs') +const extension = require('@packages/extension') const appData = require('../util/app_data') const profileCleaner = require('../util/profile_cleaner') const PATH_TO_BROWSERS = appData.path('browsers') +const pathToProfiles = path.join(PATH_TO_BROWSERS, '*') const getBrowserPath = (browser) => { return path.join( @@ -18,6 +25,22 @@ const getBrowserPath = (browser) => { ) } +const defaultLaunchOptions: { + preferences: {[key: string]: any} + extensions: string[] + args: string[] +} = { + preferences: {}, + extensions: [], + args: [], +} + +const KNOWN_LAUNCH_OPTION_PROPERTIES = _.keys(defaultLaunchOptions) + +const getDefaultLaunchOptions = (options) => { + return _.defaultsDeep(options, defaultLaunchOptions) +} + const copyExtension = (src, dest) => { return fs.copyAsync(src, dest) } @@ -44,39 +67,111 @@ const getExtensionDir = (browser, isTextTerminal) => { ) } -const ensureCleanCache = function (browser, isTextTerminal) { +const ensureCleanCache = async function (browser, isTextTerminal) { const p = path.join( getProfileDir(browser, isTextTerminal), 'CypressCache' ) - return fs - .removeAsync(p) - .then(() => { - return fs.ensureDirAsync(p) - }).return(p) + await fs.removeAsync(p) + await fs.ensureDirAsync(p) + + return p +} + +// we now store profiles inside the Cypress binary folder +// so we need to remove the legacy root profiles that existed before +function removeLegacyProfiles () { + return profileCleaner.removeRootProfile(pathToProfiles, [ + path.join(pathToProfiles, 'run-*'), + path.join(pathToProfiles, 'interactive'), + ]) } const removeOldProfiles = function () { // a profile is considered old if it was used // in a previous run for a PID that is either // no longer active, or isnt a cypress related process - const pathToProfiles = path.join(PATH_TO_BROWSERS, '*') const pathToPartitions = appData.electronPartitionsPath() - return Promise.all([ - // we now store profiles in either interactive or run-* folders - // so we need to remove the old root profiles that existed before - profileCleaner.removeRootProfile(pathToProfiles, [ - path.join(pathToProfiles, 'run-*'), - path.join(pathToProfiles, 'interactive'), - ]), + return Bluebird.all([ + removeLegacyProfiles(), profileCleaner.removeInactiveByPid(pathToProfiles, 'run-'), profileCleaner.removeInactiveByPid(pathToPartitions, 'run-'), ]) } +const pathToExtension = extension.getPathToExtension() + +async function executeBeforeBrowserLaunch (browser, launchOptions: typeof defaultLaunchOptions, options) { + if (plugins.has('before:browser:launch')) { + const pluginConfigResult = await plugins.execute('before:browser:launch', browser, launchOptions) + + if (pluginConfigResult) { + extendLaunchOptionsFromPlugins(launchOptions, pluginConfigResult, options) + } + } + + return launchOptions +} + +function extendLaunchOptionsFromPlugins (launchOptions, pluginConfigResult, options) { + // if we returned an array from the plugin + // then we know the user is using the deprecated + // interface and we need to warn them + // TODO: remove this logic in >= v5.0.0 + if (pluginConfigResult[0]) { + options.onWarning(errors.get( + 'DEPRECATED_BEFORE_BROWSER_LAUNCH_ARGS' + )) + + _.extend(pluginConfigResult, { + args: _.filter(pluginConfigResult, (_val, key) => _.isNumber(key)), + extensions: [], + }) + } else { + // either warn about the array or potentially error on invalid props, but not both + + // strip out all the known launch option properties from the resulting object + const unexpectedProperties: string[] = _ + .chain(pluginConfigResult) + .omit(KNOWN_LAUNCH_OPTION_PROPERTIES) + .keys() + .value() + + if (unexpectedProperties.length) { + errors.throw('UNEXPECTED_BEFORE_BROWSER_LAUNCH_PROPERTIES', unexpectedProperties, KNOWN_LAUNCH_OPTION_PROPERTIES) + } + } + + _.forEach(launchOptions, (val, key) => { + const pluginResultValue = pluginConfigResult[key] + + if (pluginResultValue) { + if (_.isPlainObject(val)) { + launchOptions[key] = _.extend({}, launchOptions[key], pluginResultValue) + + return + } + + launchOptions[key] = pluginResultValue + + return + } + }) + + return launchOptions +} + export = { + extendLaunchOptionsFromPlugins, + + executeBeforeBrowserLaunch, + + defaultLaunchOptions, + + getDefaultLaunchOptions, + getPort, copyExtension, @@ -93,6 +188,28 @@ export = { launch: launcher.launch, + writeExtension (browser, isTextTerminal, proxyUrl, socketIoRoute, onScreencastFrame) { + debug('writing extension') + + // debug('writing extension to chrome browser') + // get the string bytes for the final extension file + return extension.setHostAndPath(proxyUrl, socketIoRoute, onScreencastFrame) + .then((str) => { + const extensionDest = getExtensionDir(browser, isTextTerminal) + const extensionBg = path.join(extensionDest, 'background.js') + + // copy the extension src to the extension dist + return copyExtension(pathToExtension, extensionDest) + .then(() => { + debug('copied extension') + + // and overwrite background.js with the final string bytes + return fs.writeFileAsync(extensionBg, str) + }) + .return(extensionDest) + }) + }, + getBrowsers () { debug('getBrowsers') @@ -106,7 +223,7 @@ export = { const version = process.versions.chrome || '' if (version) { - majorVersion = parseInt(version.split('.')[0]) + majorVersion = parseFloat(version.split('.')[0]) } const electronBrowser: FoundBrowser = { diff --git a/packages/server/lib/errors.coffee b/packages/server/lib/errors.coffee index e3c72ddbe037..de3a99b22dd3 100644 --- a/packages/server/lib/errors.coffee +++ b/packages/server/lib/errors.coffee @@ -844,6 +844,14 @@ getMsgByType = (type, arg1 = {}, arg2, arg3) -> """ 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 """ + when "EXTENSION_NOT_LOADED" + """ + #{arg1} could not install the extension at path: + + > #{arg2} + + Please verify that this is the path to a valid, unpacked WebExtension. + """ when "COULD_NOT_FIND_SYSTEM_NODE" """ `nodeVersion` is set to `system`, but Cypress could not find a usable Node executable on your PATH. @@ -880,14 +888,36 @@ getMsgByType = (type, arg1 = {}, arg2, arg3) -> """ Failed to connect to Chrome, retrying in 1 second (attempt #{chalk.yellow(arg1)}/32) """ + when "DEPRECATED_BEFORE_BROWSER_LAUNCH_ARGS" + """ + Deprecation Warning: The `before:browser:launch` plugin event changed its signature in version `4.0.0` + + The `before:browser:launch` plugin event switched from yielding the second argument as an `array` of browser arguments to an options `object` with an `args` property. + + We've detected that your code is still using the previous, deprecated interface signature. + + This code will not work in a future version of Cypress. Please see the upgrade guide: #{chalk.yellow('https://on.cypress.io/deprecated-before-browser-launch-args')} + """ + when "UNEXPECTED_BEFORE_BROWSER_LAUNCH_PROPERTIES" + """ + The `launchOptions` object returned by your plugin's `browser:before:launch` handler contained unexpected properties: + + #{listItems(arg1)} + + `launchOptions` may only contain the properties: + + #{listItems(arg2)} + + https://on.cypress.io/browser-launch-api + """ when "COULD_NOT_PARSE_ARGUMENTS" - """ - Cypress encountered an error while parsing the argument #{chalk.gray(arg1)} + """ + Cypress encountered an error while parsing the argument #{chalk.gray(arg1)} - You passed: #{arg2} + You passed: #{arg2} - The error was: #{arg3} - """ + The error was: #{arg3} + """ get = (type, arg1, arg2, arg3) -> msg = getMsgByType(type, arg1, arg2, arg3) diff --git a/packages/server/lib/gui/events.coffee b/packages/server/lib/gui/events.coffee index 57765f55554e..6dc607aac900 100644 --- a/packages/server/lib/gui/events.coffee +++ b/packages/server/lib/gui/events.coffee @@ -3,6 +3,7 @@ ipc = require("electron").ipcMain shell = require("electron").shell debug = require('debug')('cypress:server:events') pluralize = require("pluralize") +stripAnsi = require("strip-ansi") dialog = require("./dialog") pkg = require("./package") logs = require("./logs") @@ -207,6 +208,7 @@ handleEvent = (options, bus, event, id, type, arg) -> bus.emit("project:error", errors.clone(err, {html: true})) onWarning = (warning) -> + warning.message = stripAnsi(warning.message) bus.emit("project:warning", errors.clone(warning, {html: true})) browsers.getAllBrowsersWith(options.browser) diff --git a/packages/server/lib/gui/windows.coffee b/packages/server/lib/gui/windows.coffee index 19b0f19220e5..94bafd89d01e 100644 --- a/packages/server/lib/gui/windows.coffee +++ b/packages/server/lib/gui/windows.coffee @@ -40,6 +40,22 @@ setWindowProxy = (win) -> }) module.exports = { + installExtension: (path) -> + ## extensions can only be installed for all BrowserWindows + name = BrowserWindow.addExtension(path) + + debug('electron extension installed %o', { success: !!name, name, path }) + + if !name + throw new Error('Extension could not be installed.') + + resetExtensions: -> + ## remove all extensions + extensions = _.keys(BrowserWindow.getExtensions()) + + debug('removing all electron extensions %o', extensions) + extensions.forEach(BrowserWindow.removeExtension) + reset: -> windows = {} diff --git a/packages/server/lib/modes/run.js b/packages/server/lib/modes/run.js index 6261de4bc6ae..1cb2fb587739 100644 --- a/packages/server/lib/modes/run.js +++ b/packages/server/lib/modes/run.js @@ -473,7 +473,7 @@ const getChromeProps = (isHeaded, project, writeVideoFrame) => { .chain({}) .tap((props) => { if (writeVideoFrame) { - props.screencastFrame = (e) => { + props.onScreencastFrame = (e) => { // https://chromedevtools.github.io/devtools-protocol/tot/Page#event-screencastFrame writeVideoFrame(Buffer.from(e.data, 'base64')) } @@ -915,8 +915,25 @@ module.exports = { }, } + const warnings = {} + browserOpts.projectRoot = projectRoot + browserOpts.onWarning = (err) => { + const { message } = err + + // if this warning has already been + // seen for this browser launch then + // suppress it + if (warnings[message]) { + return + } + + warnings[message] = err + + return project.onWarning + } + return openProject.launch(browser, spec, browserOpts) }, diff --git a/packages/server/lib/plugins/child/run_plugins.js b/packages/server/lib/plugins/child/run_plugins.js index 8962804a9654..b40c40b090a7 100644 --- a/packages/server/lib/plugins/child/run_plugins.js +++ b/packages/server/lib/plugins/child/run_plugins.js @@ -7,6 +7,9 @@ const Promise = require('bluebird') const preprocessor = require('./preprocessor') const task = require('./task') const util = require('../util') +const errors = require('../../errors') + +const ARRAY_METHODS = ['concat', 'push', 'unshift', 'slice', 'pop', 'shift', 'slice', 'splice', 'filter', 'map', 'forEach', 'reduce', 'reverse', 'splice', 'includes'] const registeredEvents = {} @@ -26,6 +29,10 @@ const sendError = (ipc, err) => { ipc.send('error', util.serializeError(err)) } +const sendWarning = (ipc, warningErr) => { + ipc.send('warning', util.serializeError(warningErr)) +} + let plugins const load = (ipc, config, pluginsFile) => { @@ -89,10 +96,46 @@ const execute = (ipc, event, ids, args = []) => { preprocessor.wrap(ipc, invoke, ids, args) return - case 'before:browser:launch': + case 'before:browser:launch': { + // TODO: remove in next breaking release + // This will send a warning message when a deprecated API is used + // define array-like functions on this object so we can warn about using deprecated array API + // while still fufiling desired behavior + const [, launchOptions] = args + + let hasEmittedWarning = false + + ARRAY_METHODS.forEach((name) => { + const boundFn = launchOptions.args[name].bind(launchOptions.args) + + launchOptions[name] = function () { + if (hasEmittedWarning) return + + hasEmittedWarning = true + + sendWarning(ipc, + errors.get( + 'DEPRECATED_BEFORE_BROWSER_LAUNCH_ARGS' + )) + + // eslint-disable-next-line prefer-rest-params + return boundFn.apply(this, arguments) + } + }) + + Object.defineProperty(launchOptions, 'length', { + get () { + return this.args.length + }, + }) + + launchOptions[Symbol.iterator] = launchOptions.args[Symbol.iterator].bind(launchOptions.args) + util.wrapChildPromise(ipc, invoke, ids, args) return + } + case 'task': task.wrap(ipc, registeredEvents, ids, args) diff --git a/packages/server/lib/plugins/index.coffee b/packages/server/lib/plugins/index.coffee index 0ea612935ece..8b9fce6c1037 100644 --- a/packages/server/lib/plugins/index.coffee +++ b/packages/server/lib/plugins/index.coffee @@ -95,8 +95,14 @@ module.exports = { err.title = "Error running plugin" options.onError(err) + handleWarning = (warningErr) -> + debug("plugins process warning:", warningErr.stack) + return if not pluginsProcess ## prevent repeating this in case of multiple warnings + options.onWarning(warningErr) + pluginsProcess.on("error", handleError) ipc.on("error", handleError) + ipc.on("warning", handleWarning) ## see timers/parent.js line #93 for why this is necessary process.on("exit", killPluginsProcess) diff --git a/packages/server/lib/plugins/util.coffee b/packages/server/lib/plugins/util.coffee index e628dbc07a23..20d5aa67e32c 100644 --- a/packages/server/lib/plugins/util.coffee +++ b/packages/server/lib/plugins/util.coffee @@ -6,7 +6,7 @@ Promise = require("bluebird") UNDEFINED_SERIALIZED = "__cypress_undefined__" serializeError = (err) -> - _.pick(err, "name", "message", "stack", "code", "annotated") + _.pick(err, "name", "message", "stack", "code", "annotated", "type") module.exports = { serializeError: serializeError diff --git a/packages/server/lib/project.js b/packages/server/lib/project.js index cb0d5387503e..c80206905fc2 100644 --- a/packages/server/lib/project.js +++ b/packages/server/lib/project.js @@ -158,6 +158,7 @@ class Project extends EE { options.onError(err) }, + onWarning: options.onWarning, }) } diff --git a/packages/server/package.json b/packages/server/package.json index d9d42e87433c..347dd6af0171 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -118,7 +118,7 @@ "signal-exit": "3.0.2", "sinon": "5.1.1", "squirrelly": "7.7.0", - "strip-ansi": "3.0.1", + "strip-ansi": "6.0.0", "syntax-error": "1.4.0", "systeminformation": "4.19.1", "term-size": "2.1.0", diff --git a/packages/server/test/e2e/1_deprecated_spec.ts b/packages/server/test/e2e/1_deprecated_spec.ts new file mode 100644 index 000000000000..088730355882 --- /dev/null +++ b/packages/server/test/e2e/1_deprecated_spec.ts @@ -0,0 +1,124 @@ +const e2e = require('../support/helpers/e2e') +const Fixtures = require('../support/helpers/fixtures') + +const beforeBrowserLaunchProject = Fixtures.projectPath('plugin-before-browser-launch-deprecation') + +describe('deprecated before:browser:launch args', () => { + e2e.setup() + + e2e.it('fails when adding unknown properties to launchOptions', { + config: { + video: false, + env: { + BEFORE_BROWSER_LAUNCH_HANDLER: 'return-unknown-properties', + }, + }, + project: beforeBrowserLaunchProject, + spec: 'app_spec.js', + expectedExitCode: 1, + snapshot: true, + }) + + e2e.it('push and no return - warns user exactly once', { + config: { + video: false, + env: { + BEFORE_BROWSER_LAUNCH_HANDLER: 'return-undefined-mutate-array', + }, + }, + project: beforeBrowserLaunchProject, + spec: 'app_spec.js', + snapshot: true, + stdoutInclude: 'Deprecation Warning:', + psInclude: ['--foo', '--bar'], + }) + + e2e.it('using non-deprecated API - no warning', { + // TODO: implement webPreferences.additionalArgs here + // once we decide if/what we're going to make the implemenation + // SUGGESTION: add this to Cypress.browser.args which will capture + // whatever args we use to launch the browser + config: { + video: false, + env: { + BEFORE_BROWSER_LAUNCH_HANDLER: 'return-launch-options-mutate-only-args-property', + }, + }, + project: beforeBrowserLaunchProject, + spec: 'app_spec.js', + snapshot: true, + stdoutExclude: 'Deprecation Warning:', + psInclude: ['--foo', '--bar'], + }) + + e2e.it('concat return returns once per spec', { + // TODO: implement webPreferences.additionalArgs here + // once we decide if/what we're going to make the implemenation + // SUGGESTION: add this to Cypress.browser.args which will capture + // whatever args we use to launch the browser + config: { + video: false, + env: { + BEFORE_BROWSER_LAUNCH_HANDLER: 'return-array-mutation', + }, + }, + project: beforeBrowserLaunchProject, + spec: 'app_spec.js,app_spec2.js', + snapshot: true, + stdoutInclude: 'Deprecation Warning:', + }) + + e2e.it('no mutate return', { + // TODO: implement webPreferences.additionalArgs here + // once we decide if/what we're going to make the implemenation + // SUGGESTION: add this to Cypress.browser.args which will capture + // whatever args we use to launch the browser + config: { + video: false, + env: { + BEFORE_BROWSER_LAUNCH_HANDLER: 'return-new-array-without-mutation', + }, + }, + project: beforeBrowserLaunchProject, + spec: 'app_spec.js', + snapshot: true, + stdoutInclude: 'Deprecation Warning:', + psInclude: '--foo', + }) + + // TODO: these errors could be greatly improved by the code frame + // improvements - because we "wrap" the user error with our own + // error which reads strangely - the message + stack are both + // printed. we should print that we are aborting the run because + // the before:browser:launch handler threw an error / rejected + e2e.it('displays errors thrown and aborts the run', { + config: { + video: false, + env: { + BEFORE_BROWSER_LAUNCH_HANDLER: 'throw-explicit-error', + }, + }, + project: beforeBrowserLaunchProject, + spec: 'app_spec.js,app_spec2.js', + expectedExitCode: 1, + snapshot: true, + }) + + // TODO: these errors could be greatly improved by the code frame + // improvements - because we "wrap" the user error with our own + // error which reads strangely - the message + stack are both + // printed. we should print that we are aborting the run because + // the before:browser:launch handler threw an error / rejected + e2e.it('displays promises rejected and aborts the run', { + config: { + video: false, + env: { + BEFORE_BROWSER_LAUNCH_HANDLER: 'reject-promise', + }, + }, + project: beforeBrowserLaunchProject, + spec: 'app_spec.js,app_spec2.js', + expectedExitCode: 1, + snapshot: true, + }) +}) diff --git a/packages/server/test/e2e/4_before_browser_launch_spec.ts b/packages/server/test/e2e/4_before_browser_launch_spec.ts new file mode 100644 index 000000000000..6808fd26d3b0 --- /dev/null +++ b/packages/server/test/e2e/4_before_browser_launch_spec.ts @@ -0,0 +1,37 @@ +const browserUtils = require('../../lib/browsers/utils') +const e2e = require('../support/helpers/e2e') +const Fixtures = require('../support/helpers/fixtures') + +const browser = { + name: 'chrome', +} +const isTextTerminal = true // we're always in run mode +const PATH_TO_CHROME_PROFILE = browserUtils.getProfileDir(browser, isTextTerminal) + +describe('e2e before:browser:launch', () => { + e2e.setup() + + e2e.it('modifies preferences on disk if DNE', { + browser: 'chrome', + config: { + video: false, + env: { + PATH_TO_CHROME_PROFILE, + }, + }, + project: Fixtures.projectPath('chrome-browser-preferences'), + snapshot: true, + spec: 'spec.js', + }) + + e2e.it('can add extensions', { + spec: 'spec.js', + config: { + video: false, + }, + headed: true, + project: Fixtures.projectPath('browser-extensions'), + sanitizeScreenshotDimensions: true, + snapshot: true, + }) +}) diff --git a/packages/server/test/integration/cypress_spec.js b/packages/server/test/integration/cypress_spec.js index 90e8ac1baeb8..3bcfc1feee46 100644 --- a/packages/server/test/integration/cypress_spec.js +++ b/packages/server/test/integration/cypress_spec.js @@ -1090,6 +1090,7 @@ describe('lib/cypress', () => { attach: sinon.stub(), sendCommand: sinon.stub().resolves(), }, + getOSProcessId: sinon.stub(), setUserAgent: sinon.stub(), session: { clearCache: sinon.stub().resolves(), @@ -1137,9 +1138,7 @@ describe('lib/cypress', () => { const browserArgs = args[2] - expect(browserArgs, 'two arguments to Chrome').to.have.length(7) - - expect(browserArgs.slice(0, 4), 'arguments to Chrome').to.deep.eq([ + expect(browserArgs.slice(0, 4), 'first 4 custom launch arguments to Chrome').to.deep.eq([ 'chrome', 'foo', 'bar', 'baz', ]) @@ -1180,15 +1179,15 @@ describe('lib/cypress', () => { return runMode.listenForProjectEnd.resolves({ stats: { failures: 0 } }) }) - it('can change the default port to 5555', function () { + it('can change the default port to 5544', function () { const listen = sinon.spy(http.Server.prototype, 'listen') const open = sinon.spy(Server.prototype, 'open') - return cypress.start([`--run-project=${this.todosPath}`, '--port=5555']) + return cypress.start([`--run-project=${this.todosPath}`, '--port=5544']) .then(() => { - expect(openProject.getProject().cfg.port).to.eq(5555) - expect(listen).to.be.calledWith(5555) - expect(open).to.be.calledWithMatch({ port: 5555 }) + expect(openProject.getProject().cfg.port).to.eq(5544) + expect(listen).to.be.calledWith(5544) + expect(open).to.be.calledWithMatch({ port: 5544 }) this.expectExitWith(0) }) }) @@ -1199,11 +1198,11 @@ describe('lib/cypress', () => { server = Promise.promisifyAll(server) - return server.listenAsync(5555, '127.0.0.1') + return server.listenAsync(5544, '127.0.0.1') .then(() => { - return cypress.start([`--run-project=${this.todosPath}`, '--port=5555']) + return cypress.start([`--run-project=${this.todosPath}`, '--port=5544']) }).then(() => { - this.expectExitWithErr('PORT_IN_USE_LONG', '5555') + this.expectExitWithErr('PORT_IN_USE_LONG', '5544') }) }) }) diff --git a/packages/server/test/integration/plugins_spec.js b/packages/server/test/integration/plugins_spec.js new file mode 100644 index 000000000000..6303b2318a71 --- /dev/null +++ b/packages/server/test/integration/plugins_spec.js @@ -0,0 +1,42 @@ +require('../spec_helper') + +const plugins = require('../../lib/plugins') +const Fixtures = require('../support/helpers/fixtures') + +const pluginsFile = Fixtures.projectPath('plugin-before-browser-launch-deprecation/cypress/plugins/index.js') + +describe('lib/plugins', () => { + beforeEach(() => { + Fixtures.scaffold() + }) + + afterEach(() => { + Fixtures.remove() + }) + + it('prints deprecation message if before:browser:launch argument is mutated as array', () => { + const onWarning = sinon.stub() + + const projectConfig = { + pluginsFile, + env: { + BEFORE_BROWSER_LAUNCH_HANDLER: 'return-array-mutation', + }, + } + + const options = { + onWarning, + } + + return plugins.init(projectConfig, options) + .then(() => { + return plugins.execute('before:browser:launch', {}, { + args: [], + }) + }) + .then(() => { + expect(onWarning).to.be.calledOnce + expect(onWarning.firstCall.args[0].message).to.include('Deprecation Warning: The `before:browser:launch` plugin event changed its signature in version `4.0.0`') + }) + }) +}) diff --git a/packages/server/test/support/fixtures/projects/browser-extensions/cypress.json b/packages/server/test/support/fixtures/projects/browser-extensions/cypress.json new file mode 100644 index 000000000000..0967ef424bce --- /dev/null +++ b/packages/server/test/support/fixtures/projects/browser-extensions/cypress.json @@ -0,0 +1 @@ +{} diff --git a/packages/server/test/support/fixtures/projects/browser-extensions/cypress/integration/spec.js b/packages/server/test/support/fixtures/projects/browser-extensions/cypress/integration/spec.js new file mode 100644 index 000000000000..d9cafa56b83b --- /dev/null +++ b/packages/server/test/support/fixtures/projects/browser-extensions/cypress/integration/spec.js @@ -0,0 +1,13 @@ +context('before:browser:launch extension e2e', () => { + it('has the expected extension', () => { + if (Cypress.browser.name === 'electron') { + cy.wrap(window.top).its('theExtensionLoaded').should('be.true') + + return + } + + cy.visit('/index.html') + .get('#extension') + .should('contain', 'inserted from extension!') + }) +}) diff --git a/packages/server/test/support/fixtures/projects/browser-extensions/cypress/plugins/index.js b/packages/server/test/support/fixtures/projects/browser-extensions/cypress/plugins/index.js new file mode 100644 index 000000000000..e708af7d7f9d --- /dev/null +++ b/packages/server/test/support/fixtures/projects/browser-extensions/cypress/plugins/index.js @@ -0,0 +1,19 @@ +const path = require('path') + +module.exports = (on) => { + on('before:browser:launch', (browser, options) => { + const { extensions } = options + + if (browser.name === 'electron') { + // electron doesn't support background pages yet, so load a devtools extension + // instead which will work + extensions.push(path.join(__dirname, '../../devtools-ext')) + } else { + extensions.push(path.join(__dirname, '../../../plugin-extension/ext')) + } + + options.preferences.devTools = true + + return options + }) +} diff --git a/packages/server/test/support/fixtures/projects/browser-extensions/devtools-ext/devtools.html b/packages/server/test/support/fixtures/projects/browser-extensions/devtools-ext/devtools.html new file mode 100644 index 000000000000..fd0d93d7cd64 --- /dev/null +++ b/packages/server/test/support/fixtures/projects/browser-extensions/devtools-ext/devtools.html @@ -0,0 +1,5 @@ + diff --git a/packages/server/test/support/fixtures/projects/browser-extensions/devtools-ext/manifest.json b/packages/server/test/support/fixtures/projects/browser-extensions/devtools-ext/manifest.json new file mode 100644 index 000000000000..0b9d8264c1c7 --- /dev/null +++ b/packages/server/test/support/fixtures/projects/browser-extensions/devtools-ext/manifest.json @@ -0,0 +1,13 @@ +{ + "name": "e2e devtools ext", + "version": "0", + "description": "tests adding devtools extension into Cypress", + "permissions": [ + "tabs", + "webNavigation", + "" + ], + "content_scripts": [], + "devtools_page": "devtools.html", + "manifest_version": 2 +} diff --git a/packages/server/test/support/fixtures/projects/browser-extensions/index.html b/packages/server/test/support/fixtures/projects/browser-extensions/index.html new file mode 100644 index 000000000000..07adf59497f7 --- /dev/null +++ b/packages/server/test/support/fixtures/projects/browser-extensions/index.html @@ -0,0 +1,6 @@ + + + hello +
+ + diff --git a/packages/server/test/support/fixtures/projects/chrome-browser-preferences/cypress.json b/packages/server/test/support/fixtures/projects/chrome-browser-preferences/cypress.json new file mode 100644 index 000000000000..0967ef424bce --- /dev/null +++ b/packages/server/test/support/fixtures/projects/chrome-browser-preferences/cypress.json @@ -0,0 +1 @@ +{} diff --git a/packages/server/test/support/fixtures/projects/chrome-browser-preferences/cypress/integration/spec.js b/packages/server/test/support/fixtures/projects/chrome-browser-preferences/cypress/integration/spec.js new file mode 100644 index 000000000000..e1571123a5dd --- /dev/null +++ b/packages/server/test/support/fixtures/projects/chrome-browser-preferences/cypress/integration/spec.js @@ -0,0 +1,5 @@ +context('chrome browser prefs e2e', () => { + it('has the expected prefs', () => { + cy.task('assert') + }) +}) diff --git a/packages/server/test/support/fixtures/projects/chrome-browser-preferences/cypress/plugins/index.js b/packages/server/test/support/fixtures/projects/chrome-browser-preferences/cypress/plugins/index.js new file mode 100644 index 000000000000..4a01eedb72c9 --- /dev/null +++ b/packages/server/test/support/fixtures/projects/chrome-browser-preferences/cypress/plugins/index.js @@ -0,0 +1,44 @@ +const Bluebird = require('bluebird') +const { expect } = require('chai') +const fse = require('fs-extra') +const path = require('path') + +module.exports = (on, config) => { + const parentPid = process.ppid + let { PATH_TO_CHROME_PROFILE } = config.env + + // the existing path to the chrome profile contains + // the wrong pid - so we need to swap it out with our + // parent child process's pid + // NOTE: we could yield the browser's profilePath as + // a property to make it easier to do this + PATH_TO_CHROME_PROFILE = PATH_TO_CHROME_PROFILE + .split(/run-\d+/) + .join(`run-${parentPid}`) + + on('before:browser:launch', (browser, launchOptions) => { + const { preferences } = launchOptions + + preferences.default.foo = 'bar' + preferences.defaultSecure.bar = 'baz' + preferences.localState.baz = 'quux' + + return launchOptions + }) + + on('task', { + assert: () => { + return Bluebird.join( + fse.readJson(path.join(PATH_TO_CHROME_PROFILE, 'Default/Preferences')), + fse.readJson(path.join(PATH_TO_CHROME_PROFILE, 'Default/Secure Preferences')), + fse.readJson(path.join(PATH_TO_CHROME_PROFILE, 'Local State')), + (defaultPrefs, defaultSecure, localState) => { + expect(defaultPrefs.foo).to.eq('bar') + expect(defaultSecure.bar).to.eq('baz') + expect(localState.baz).to.eq('quux') + } + ) + .thenReturn(null) + }, + }) +} 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 3a4d774fe2f4..6d1c70ca9551 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 @@ -31,10 +31,10 @@ module.exports = (on) => { screenshotsTaken.push(details) }) - on('before:browser:launch', (browser, args) => { - browserArgs = args + on('before:browser:launch', (browser, options) => { + browserArgs = options.args - return args + return options }) on('task', { diff --git a/packages/server/test/support/fixtures/projects/plugin-before-browser-launch-deprecation/cypress.json b/packages/server/test/support/fixtures/projects/plugin-before-browser-launch-deprecation/cypress.json new file mode 100644 index 000000000000..0967ef424bce --- /dev/null +++ b/packages/server/test/support/fixtures/projects/plugin-before-browser-launch-deprecation/cypress.json @@ -0,0 +1 @@ +{} diff --git a/packages/server/test/support/fixtures/projects/plugin-before-browser-launch-deprecation/cypress/integration/app_spec.js b/packages/server/test/support/fixtures/projects/plugin-before-browser-launch-deprecation/cypress/integration/app_spec.js new file mode 100644 index 000000000000..7696b3805a64 --- /dev/null +++ b/packages/server/test/support/fixtures/projects/plugin-before-browser-launch-deprecation/cypress/integration/app_spec.js @@ -0,0 +1,12 @@ +// eslint-disable-next-line +it('asserts on browser args', () => { + // we cannot assert on ps output in electron + // because the launchOptions.args does not go + // anywhere in the process that we can detect + // TODO: swap this out for Cypress.isBrowser(...) + if (Cypress.browser.name === 'electron') { + return + } + + cy.task('assertPsOutput') +}) diff --git a/packages/server/test/support/fixtures/projects/plugin-before-browser-launch-deprecation/cypress/integration/app_spec2.js b/packages/server/test/support/fixtures/projects/plugin-before-browser-launch-deprecation/cypress/integration/app_spec2.js new file mode 100644 index 000000000000..ddfbfb4f0f25 --- /dev/null +++ b/packages/server/test/support/fixtures/projects/plugin-before-browser-launch-deprecation/cypress/integration/app_spec2.js @@ -0,0 +1,12 @@ +// eslint-disable-next-line +it('2 - asserts on browser args', () => { + // we cannot assert on ps output in electron + // because the launchOptions.args does not go + // anywhere in the process that we can detect + // TODO: swap this out for Cypress.isBrowser(...) + if (Cypress.browser.name === 'electron') { + return + } + + cy.task('assertPsOutput') +}) diff --git a/packages/server/test/support/fixtures/projects/plugin-before-browser-launch-deprecation/cypress/plugins/index.js b/packages/server/test/support/fixtures/projects/plugin-before-browser-launch-deprecation/cypress/plugins/index.js new file mode 100644 index 000000000000..96ea549772b7 --- /dev/null +++ b/packages/server/test/support/fixtures/projects/plugin-before-browser-launch-deprecation/cypress/plugins/index.js @@ -0,0 +1,123 @@ +const _ = require('lodash') +const cp = require('bluebird').promisifyAll(require('child_process')) +const { expect } = require('chai') + +const assertPsOutput = (strs) => { + if (!_.isArray(strs)) { + strs = [strs] + } + + return () => { + return cp.execAsync('ps -fww') + .call('toString') + .then((psOutput) => { + _.forEach(strs, (str) => { + expect(psOutput, 'ps output').contain(str) + }) + + return null + }) + } +} + +const getHandlersByType = (type) => { + switch (type) { + case 'return-array-mutation': + return { + onBeforeBrowserLaunch (browser, launchOptions) { + // this will emit a warning but only once + launchOptions = launchOptions.concat(['--foo']) + launchOptions.push('--foo=bar') + launchOptions.unshift('--load-extension=/foo/bar/baz.js') + + return launchOptions + }, + onTask: { assertPsOutput: assertPsOutput(['--foo', '--foo=bar']) }, + } + + case 'return-new-array-without-mutation': + return { + onBeforeBrowserLaunch (browser, launchOptions) { + // this will emit a warning + launchOptions = [...launchOptions, '--foo'] + + return launchOptions + }, + onTask: { assertPsOutput: assertPsOutput('--foo') }, + + } + + case 'return-launch-options-mutate-only-args-property': + return { + onBeforeBrowserLaunch (browser, launchOptions) { + // this will NOT emit a warning + launchOptions.args.push('--foo') + launchOptions.args.unshift('--bar') + + return launchOptions + }, + onTask: { assertPsOutput: assertPsOutput(['--foo', '--bar']) }, + + } + + case 'return-undefined-mutate-array': + return { + onBeforeBrowserLaunch (browser, launchOptions) { + // this will emit a warning + launchOptions.push('--foo') + launchOptions.push('--bar') + + return + }, + onTask: { assertPsOutput: assertPsOutput([]) }, + + } + + case 'return-unknown-properties': + return { + onBeforeBrowserLaunch (browser, launchOptions) { + // this will fail with an error + launchOptions.foo = 'foo' + launchOptions.width = 800 + launchOptions.height = 600 + + return launchOptions + }, + } + + case 'throw-explicit-error': + return { + onBeforeBrowserLaunch (browser, launchOptions) { + throw new Error('Error thrown from plugins handler') + }, + } + + case 'reject-promise': + return { + onBeforeBrowserLaunch (browser, launchOptions) { + return Promise + .resolve(null) + .then(() => { + throw new Error('Promise rejected from plugins handler') + }) + }, + } + + default: () => { + throw new Error('config.env.BEFORE_BROWSER_LAUNCH_HANDLER must be set to a valid handler type') + } + } +} + +module.exports = (on, config) => { + const beforeBrowserLaunchHandler = config.env.BEFORE_BROWSER_LAUNCH_HANDLER + + if (!beforeBrowserLaunchHandler) { + throw new Error('config.env.BEFORE_BROWSER_LAUNCH_HANDLER must be set to a valid handler type') + } + + const { onBeforeBrowserLaunch, onTask } = getHandlersByType(beforeBrowserLaunchHandler) + + on('before:browser:launch', onBeforeBrowserLaunch) + on('task', onTask) +} diff --git a/packages/server/test/support/fixtures/projects/plugin-browser/cypress/plugins/index.coffee b/packages/server/test/support/fixtures/projects/plugin-browser/cypress/plugins/index.coffee index aff44633c89b..59d48beb486d 100644 --- a/packages/server/test/support/fixtures/projects/plugin-browser/cypress/plugins/index.coffee +++ b/packages/server/test/support/fixtures/projects/plugin-browser/cypress/plugins/index.coffee @@ -1,5 +1,5 @@ module.exports = (onFn, config) -> - onFn 'before:browser:launch', (browser = {}, args) -> + onFn 'before:browser:launch', (browser, options) -> { name } = browser switch name @@ -7,8 +7,10 @@ module.exports = (onFn, config) -> return [name, "foo", "bar", "baz"] when "electron" return { - browser: "electron" - foo: "bar" + preferences: { + browser: "electron" + foo: "bar" + } } else throw new Error("unrecognized browser name: '#{name}' for before:browser:launch") diff --git a/packages/server/test/support/fixtures/projects/plugin-extension/cypress/plugins/index.coffee b/packages/server/test/support/fixtures/projects/plugin-extension/cypress/plugins/index.coffee index 30f08a8a1c9b..2ccd6b8b4e29 100644 --- a/packages/server/test/support/fixtures/projects/plugin-extension/cypress/plugins/index.coffee +++ b/packages/server/test/support/fixtures/projects/plugin-extension/cypress/plugins/index.coffee @@ -1,8 +1,8 @@ path = require("path") module.exports = (onFn, config) -> - onFn "before:browser:launch", (browser = {}, args) -> + onFn "before:browser:launch", (browser = {}, options) -> pathToExt = path.resolve("ext") - args.push("--load-extension=#{pathToExt}") - args + options.args.push("--load-extension=#{pathToExt}") + options diff --git a/packages/server/test/support/fixtures/projects/remote-debugging-port-removed/cypress/plugins.js b/packages/server/test/support/fixtures/projects/remote-debugging-port-removed/cypress/plugins.js index 0a72c654dadb..bcc725e90a00 100644 --- a/packages/server/test/support/fixtures/projects/remote-debugging-port-removed/cypress/plugins.js +++ b/packages/server/test/support/fixtures/projects/remote-debugging-port-removed/cypress/plugins.js @@ -1,14 +1,16 @@ const la = require('lazy-ass') module.exports = (on) => { - on('before:browser:launch', (browser = {}, args) => { + on('before:browser:launch', (browser = {}, options) => { la(browser.family === 'chromium', 'this test can only be run with a chromium-family browser') // remove debugging port so that the browser connection fails - const newArgs = args.filter((arg) => !arg.startsWith('--remote-debugging-port=')) + const newArgs = options.args.filter((arg) => !arg.startsWith('--remote-debugging-port=')) - la(newArgs.length === args.length - 1, 'exactly one argument should have been removed') + la(newArgs.length === options.args.length - 1, 'exactly one argument should have been removed') - return newArgs + options.args = newArgs + + return options }) } diff --git a/packages/server/test/support/helpers/electron_stub.coffee b/packages/server/test/support/helpers/electron_stub.coffee index 0ddbd88ef7f3..ee304cb51764 100644 --- a/packages/server/test/support/helpers/electron_stub.coffee +++ b/packages/server/test/support/helpers/electron_stub.coffee @@ -25,6 +25,9 @@ module.exports = { } BrowserWindow: { fromWebContents: -> + getExtensions: -> + removeExtension: -> + addExtension: -> } Menu: { buildFromTemplate: -> diff --git a/packages/server/test/unit/browsers/browsers_spec.coffee b/packages/server/test/unit/browsers/browsers_spec.coffee index f539f985e660..f65e9b610712 100644 --- a/packages/server/test/unit/browsers/browsers_spec.coffee +++ b/packages/server/test/unit/browsers/browsers_spec.coffee @@ -4,6 +4,7 @@ Promise = require("bluebird") Windows = require("#{root}../lib/gui/windows") browsers = require("#{root}../lib/browsers") utils = require("#{root}../lib/browsers/utils") +snapshot = require('snap-shot-it') describe "lib/browsers/index", -> context ".getBrowserInstance", -> @@ -51,14 +52,34 @@ describe "lib/browsers/index", -> browsers: [] }) .then (e) -> - console.error(e) throw new Error("should've failed") - , (err) -> + .catch (err) -> # by being explicit with assertions, if something is unexpected # we will get good error message that includes the "err" object expect(err).to.have.property("type").to.eq("BROWSER_NOT_FOUND_BY_NAME") expect(err).to.have.property("message").to.contain("'foo-bad-bang' was not found on your system") + context ".extendLaunchOptionsFromPlugins", -> + it "throws an error if unexpected property passed", -> + fn = -> + utils.extendLaunchOptionsFromPlugins({}, { foo: 'bar' }) + + ## this error is snapshotted in an e2e test, no need to do it here + expect(fn).to.throw({ type: "UNEXPECTED_BEFORE_BROWSER_LAUNCH_PROPERTIES" }) + + it "warns if array passed and changes it to args", -> + onWarning = sinon.stub() + + result = utils.extendLaunchOptionsFromPlugins({ args: [] }, ['foo'], { onWarning }) + + expect(result).to.deep.eq({ + args: ['foo'] + }) + + ## this error is snapshotted in e2e tests, no need to do it here + expect(onWarning).to.be.calledOnce + expect(onWarning).to.be.calledWithMatch({ type: "DEPRECATED_BEFORE_BROWSER_LAUNCH_ARGS" }) + # Ooo, browser clean up tests are disabled?!! # it "calls onBrowserClose callback on close", -> diff --git a/packages/server/test/unit/browsers/chrome_spec.coffee b/packages/server/test/unit/browsers/chrome_spec.coffee index fb767d2391dc..043cfc632ba7 100644 --- a/packages/server/test/unit/browsers/chrome_spec.coffee +++ b/packages/server/test/unit/browsers/chrome_spec.coffee @@ -11,7 +11,6 @@ fs = require("#{root}../lib/util/fs") describe "lib/browsers/chrome", -> context "#open", -> beforeEach -> - @args = [] # mock CRI client during testing @criClient = { ensureMinimumProtocolVersion: sinon.stub().resolves() @@ -31,11 +30,16 @@ describe "lib/browsers/chrome", -> sinon.stub(chrome, "_writeExtension").resolves("/path/to/ext") sinon.stub(chrome, "_connectToChromeRemoteInterface").resolves(@criClient) - sinon.stub(plugins, "has") - sinon.stub(plugins, "execute") + sinon.stub(plugins, "execute").callThrough() sinon.stub(utils, "launch").resolves(@launchedBrowser) sinon.stub(utils, "getProfileDir").returns("/profile/dir") sinon.stub(utils, "ensureCleanCache").resolves("/profile/dir/CypressCache") + + @readJson = sinon.stub(fs, 'readJson') + @readJson.withArgs('/profile/dir/Default/Preferences').rejects({ code: 'ENOENT' }) + @readJson.withArgs('/profile/dir/Default/Secure Preferences').rejects({ code: 'ENOENT' }) + @readJson.withArgs('/profile/dir/Local State').rejects({ code: 'ENOENT' }) + # port for Chrome remote interface communication sinon.stub(utils, "getPort").resolves(50505) @@ -51,26 +55,25 @@ describe "lib/browsers/chrome", -> expect(@criClient.send).to.have.been.calledWith("Page.navigate") it "is noop without before:browser:launch", -> - plugins.has.returns(false) - chrome.open("chrome", "http://", {}, @automation) .then -> expect(plugins.execute).not.to.be.called it "is noop if newArgs are not returned", -> - sinon.stub(chrome, "_getArgs").returns(@args) + args = [] + + sinon.stub(chrome, "_getArgs").returns(args) + sinon.stub(plugins, 'has').returns(true) - plugins.has.returns(true) plugins.execute.resolves(null) chrome.open("chrome", "http://", {}, @automation) .then => # to initialize remote interface client and prepare for true tests # we load the browser with blank page first - expect(utils.launch).to.be.calledWith("chrome", "about:blank", @args) + expect(utils.launch).to.be.calledWith("chrome", "about:blank", args) it "does not load extension in headless mode", -> - plugins.has.returns(false) chrome._writeExtension.restore() pathToTheme = extension.getPathToTheme() @@ -88,21 +91,18 @@ describe "lib/browsers/chrome", -> ]) it "normalizes --load-extension if provided in plugin", -> - plugins.has.returns(true) - plugins.execute.resolves([ - "--foo=bar", "--load-extension=/foo/bar/baz.js" - ]) + plugins.register 'before:browser:launch', (browser, config) -> + return Promise.resolve({ + args: ["--foo=bar", "--load-extension=/foo/bar/baz.js"] + }) pathToTheme = extension.getPathToTheme() - ## this should get obliterated - @args.push("--something=else") - chrome.open("chrome", "http://", {}, @automation) .then => args = utils.launch.firstCall.args[2] - expect(args).to.deep.eq([ + expect(args).to.include.members([ "--foo=bar" "--load-extension=/foo/bar/baz.js,/path/to/ext,#{pathToTheme}" "--user-data-dir=/profile/dir" @@ -110,21 +110,18 @@ describe "lib/browsers/chrome", -> ]) it "normalizes multiple extensions from plugins", -> - plugins.has.returns(true) - plugins.execute.resolves([ - "--foo=bar", "--load-extension=/foo/bar/baz.js,/quux.js" - ]) + plugins.register 'before:browser:launch', (browser, config) -> + return Promise.resolve { + args: ["--foo=bar", "--load-extension=/foo/bar/baz.js,/quux.js"] + } pathToTheme = extension.getPathToTheme() - ## this should get obliterated - @args.push("--something=else") - chrome.open("chrome", "http://", {}, @automation) .then => args = utils.launch.firstCall.args[2] - expect(args).to.deep.eq([ + expect(args).to.include.members([ "--foo=bar" "--load-extension=/foo/bar/baz.js,/quux.js,/path/to/ext,#{pathToTheme}" "--user-data-dir=/profile/dir" @@ -132,17 +129,17 @@ describe "lib/browsers/chrome", -> ]) it "cleans up an unclean browser profile exit status", -> - sinon.stub(fs, "readJson").withArgs("/profile/dir/Default/Preferences").resolves({ + @readJson.withArgs("/profile/dir/Default/Preferences").resolves({ profile: { exit_type: "Abnormal" exited_cleanly: false } }) - sinon.stub(fs, "writeJson") + sinon.stub(fs, "outputJson").resolves() chrome.open("chrome", "http://", {}, @automation) .then -> - expect(fs.writeJson).to.be.calledWith("/profile/dir/Default/Preferences", { + expect(fs.outputJson).to.be.calledWith("/profile/dir/Default/Preferences", { profile: { exit_type: "Normal" exited_cleanly: true @@ -242,3 +239,108 @@ describe "lib/browsers/chrome", -> chromeVersionHasLoopback("71", false) chromeVersionHasLoopback("72", true) chromeVersionHasLoopback("73", true) + + context "#_getChromePreferences", -> + it "returns map of empty if the files do not exist", -> + sinon.stub(fs, 'readJson') + .withArgs('/foo/Default/Preferences').rejects({ code: 'ENOENT' }) + .withArgs('/foo/Default/Secure Preferences').rejects({ code: 'ENOENT' }) + .withArgs('/foo/Local State').rejects({ code: 'ENOENT' }) + + expect(chrome._getChromePreferences('/foo')).to.eventually.deep.eq({ + default: {}, + defaultSecure: {}, + localState: {} + }) + + it "returns map of json objects if the files do exist", -> + sinon.stub(fs, 'readJson') + .withArgs('/foo/Default/Preferences').resolves({ foo: 'bar' }) + .withArgs('/foo/Default/Secure Preferences').resolves({ bar: 'baz' }) + .withArgs('/foo/Local State').resolves({ baz: 'quux' }) + + expect(chrome._getChromePreferences('/foo')).to.eventually.deep.eq({ + default: { foo: 'bar' }, + defaultSecure: { bar: 'baz' }, + localState: { baz: 'quux' } + }) + + context "#_mergeChromePreferences", -> + it "merges as expected", -> + originalPrefs = { + default: {}, + defaultSecure: { + foo: 'bar' + deleteThis: 'nephew' + }, + localState: {} + } + + newPrefs = { + default: { + something: { + nested: 'here' + }, + }, + defaultSecure: { + deleteThis: null + }, + someGarbage: true + } + + expected = { + default: { + something: { + nested: 'here' + } + }, + defaultSecure: { + foo: 'bar' + }, + localState: {} + } + + expect(chrome._mergeChromePreferences(originalPrefs, newPrefs)).to.deep.eq(expected) + + context "#_writeChromePreferences", -> + it "writes json as expected", -> + outputJson = sinon.stub(fs, 'outputJson') + defaultPrefs = outputJson.withArgs('/foo/Default/Preferences').resolves() + securePrefs = outputJson.withArgs('/foo/Default/Secure Preferences').resolves() + statePrefs = outputJson.withArgs('/foo/Local State').resolves() + + originalPrefs = { + default: {}, + defaultSecure: { + foo: 'bar' + deleteThis: 'nephew' + }, + localState: {} + } + + newPrefs = chrome._mergeChromePreferences(originalPrefs, { + default: { + something: { + nested: 'here' + }, + }, + defaultSecure: { + deleteThis: null + }, + someGarbage: true + }) + + expect(chrome._writeChromePreferences('/foo', originalPrefs, newPrefs)).to.eventually.equal() + .then -> + expect(defaultPrefs).to.be.calledWith('/foo/Default/Preferences', { + something: { + nested: 'here' + }, + }) + + expect(securePrefs).to.be.calledWith('/foo/Default/Secure Preferences', { + foo: 'bar' + }) + + ## no changes were made + expect(statePrefs).to.not.be.called diff --git a/packages/server/test/unit/browsers/electron_spec.coffee b/packages/server/test/unit/browsers/electron_spec.coffee index 660c3d4c2ad1..a3050382b8c6 100644 --- a/packages/server/test/unit/browsers/electron_spec.coffee +++ b/packages/server/test/unit/browsers/electron_spec.coffee @@ -21,6 +21,7 @@ describe "lib/browsers/electron", -> @options = { some: "var" projectRoot: "/foo/" + onWarning: sinon.stub().returns() } @automation = Automation.create("foo", "bar", "baz") @win = _.extend(new EE(), { @@ -45,6 +46,9 @@ describe "lib/browsers/electron", -> } }) + sinon.stub(Windows, 'installExtension').returns() + sinon.stub(Windows, 'resetExtensions').returns() + @stubForOpen = -> sinon.stub(electron, "_render").resolves(@win) sinon.stub(plugins, "has") @@ -96,9 +100,13 @@ describe "lib/browsers/electron", -> expect(options).to.include.keys("onFocus", "onNewWindow", "onPaint", "onCrashed") ## https://github.com/cypress-io/cypress/issues/1992 - it "it merges in options without removing essential options", -> + it "it merges in user preferences without removing essential options", -> plugins.has.returns(true) - plugins.execute.resolves({foo: "bar"}) + plugins.execute.withArgs("before:browser:launch").resolves({ + preferences: { + foo: "bar" + } + }) electron.open("electron", @url, @options, @automation) .then => @@ -106,6 +114,23 @@ describe "lib/browsers/electron", -> expect(options).to.include.keys("foo", "onFocus", "onNewWindow", "onPaint", "onCrashed") + it "installs supplied extensions from before:browser:launch and warns on failure", -> + plugins.has.returns(true) + plugins.execute.resolves({ extensions: ['foo', 'bar'] }) + + Windows.installExtension.withArgs('bar').throws() + + electron.open("electron", @url, @options, @automation) + .then => + expect(Windows.installExtension).to.be.calledTwice + expect(Windows.installExtension).to.be.calledWith('foo') + expect(Windows.installExtension).to.be.calledWith('bar') + + expect(@options.onWarning).to.be.calledOnce + + warning = @options.onWarning.firstCall.args[0].message + expect(warning).to.contain('Electron').and.contain('bar') + context "._launch", -> beforeEach -> sinon.stub(menu, "set") diff --git a/packages/server/test/unit/modes/run_spec.js b/packages/server/test/unit/modes/run_spec.js index 34dc9a9c4d68..c643e84afa7f 100644 --- a/packages/server/test/unit/modes/run_spec.js +++ b/packages/server/test/unit/modes/run_spec.js @@ -211,6 +211,7 @@ describe('lib/modes/run', () => { runMode.launchBrowser({ spec, browser, + project: {}, }) expect(this.launch).to.be.calledWithMatch(browser, spec, {}) diff --git a/packages/server/test/unit/plugins/child/run_plugins_spec.coffee b/packages/server/test/unit/plugins/child/run_plugins_spec.coffee index a2ffdea0bed6..ffdd2339072c 100644 --- a/packages/server/test/unit/plugins/child/run_plugins_spec.coffee +++ b/packages/server/test/unit/plugins/child/run_plugins_spec.coffee @@ -8,6 +8,7 @@ preprocessor = require("#{root}../../lib/plugins/child/preprocessor") task = require("#{root}../../lib/plugins/child/task") runPlugins = require("#{root}../../lib/plugins/child/run_plugins") util = require("#{root}../../lib/plugins/util") +browserUtils = require("#{root}../../lib/browsers/utils") Fixtures = require("#{root}../../test/support/helpers/fixtures") colorCodeRe = /\[[0-9;]+m/gm @@ -104,15 +105,20 @@ describe "lib/plugins/child/run_plugins", -> describe "on 'execute' message", -> beforeEach -> sinon.stub(preprocessor, "wrap") + @onFilePreprocessor = sinon.stub().resolves() @beforeBrowserLaunch = sinon.stub().resolves() @taskRequested = sinon.stub().resolves("foo") + pluginsFn = (register) => register("file:preprocessor", @onFilePreprocessor) register("before:browser:launch", @beforeBrowserLaunch) register("task", @taskRequested) + mockery.registerMock("plugins-file", pluginsFn) + runPlugins(@ipc, "plugins-file") + @ipc.on.withArgs("load").yield() context "file:preprocessor", -> @@ -128,7 +134,7 @@ describe "lib/plugins/child/run_plugins", -> expect(preprocessor.wrap.lastCall.args[2]).to.equal(@ids) expect(preprocessor.wrap.lastCall.args[3]).to.equal(args) - it "invokes registered function when invoked by preprocessor handler", -> + it "invokes registered function when invoked by handler", -> @ipc.on.withArgs("execute").yield("file:preprocessor", @ids, []) preprocessor.wrap.lastCall.args[1](2, ["one", "two"]) expect(@onFilePreprocessor).to.be.calledWith("one", "two") @@ -136,22 +142,26 @@ describe "lib/plugins/child/run_plugins", -> context "before:browser:launch", -> beforeEach -> sinon.stub(util, "wrapChildPromise") + + browser = {} + launchOptions = browserUtils.getDefaultLaunchOptions({}) + + @args = [browser, launchOptions] @ids = { eventId: 1, invocationId: "00" } it "wraps child promise", -> args = ["arg1", "arg2"] - @ipc.on.withArgs("execute").yield("before:browser:launch", @ids, args) + @ipc.on.withArgs("execute").yield("before:browser:launch", @ids, @args) expect(util.wrapChildPromise).to.be.called expect(util.wrapChildPromise.lastCall.args[0]).to.equal(@ipc) expect(util.wrapChildPromise.lastCall.args[1]).to.be.a("function") expect(util.wrapChildPromise.lastCall.args[2]).to.equal(@ids) - expect(util.wrapChildPromise.lastCall.args[3]).to.equal(args) + expect(util.wrapChildPromise.lastCall.args[3]).to.equal(@args) - it "invokes registered function when invoked by preprocessor handler", -> - @ipc.on.withArgs("execute").yield("before:browser:launch", @ids, []) - args = ["one", "two"] - util.wrapChildPromise.lastCall.args[1](3, args) - expect(@beforeBrowserLaunch).to.be.calledWith("one", "two") + it "invokes registered function when invoked by handler", -> + @ipc.on.withArgs("execute").yield("before:browser:launch", @ids, @args) + util.wrapChildPromise.lastCall.args[1](3, @args) + expect(@beforeBrowserLaunch).to.be.calledWith(@args...) context "task", -> beforeEach -> From 291a3546da1b22c62b24ac419cd470be2b692f72 Mon Sep 17 00:00:00 2001 From: Zach Bloomquist Date: Wed, 5 Feb 2020 15:53:52 -0500 Subject: [PATCH 44/49] Remove `--browser canary` backwards-compatibility (#6333) * remove --browser canary compatibility * update warning when canary is passed * add tests for --browser canary warning * Update packages/server/test/unit/browsers/browsers_spec.coffee --- .../__snapshots__/browsers_spec.coffee.js | 19 ++++++++++++++++ packages/server/lib/browsers/index.js | 7 +----- packages/server/lib/errors.coffee | 11 +++++++++- .../test/unit/browsers/browsers_spec.coffee | 22 ++++++++++++++----- 4 files changed, 46 insertions(+), 13 deletions(-) create mode 100644 packages/server/__snapshots__/browsers_spec.coffee.js diff --git a/packages/server/__snapshots__/browsers_spec.coffee.js b/packages/server/__snapshots__/browsers_spec.coffee.js new file mode 100644 index 000000000000..ef2a380c8fb6 --- /dev/null +++ b/packages/server/__snapshots__/browsers_spec.coffee.js @@ -0,0 +1,19 @@ +exports['lib/browsers/index .ensureAndGetByNameOrPath throws when no browser can be found 1'] = ` +Can't run because you've entered an invalid browser name. + +Browser: 'browserNotGonnaBeFound' was not found on your system. + +Available browsers found are: chrome, electron +` + +exports['lib/browsers/index .ensureAndGetByNameOrPath throws a special error when canary is passed 1'] = ` +Can't run because you've entered an invalid browser name. + +Browser: 'canary' was not found on your system. + +Available browsers found are: chrome, chrome:canary, firefox + +Note: In Cypress 4.0, Canary must be launched as \`chrome:canary\`, not \`canary\`. + +See https://on.cypress.io/migration-guide for more information on breaking changes in 4.0. +` diff --git a/packages/server/lib/browsers/index.js b/packages/server/lib/browsers/index.js index 33c86b64091f..2c14d772999e 100644 --- a/packages/server/lib/browsers/index.js +++ b/packages/server/lib/browsers/index.js @@ -23,7 +23,7 @@ const kill = function (unbind) { instance.removeAllListeners() } - instance.once('exit', function (...args) { + instance.once('exit', (...args) => { debug('browser process killed') return resolve.apply(null, args) @@ -61,11 +61,6 @@ const isValidPathToBrowser = (str) => { } const parseBrowserOption = (opt) => { - // for backwards compatibility pre-4.x - if (opt === 'canary') { - opt = 'chrome:canary' - } - // it's a name or a path if (!_.isString(opt) || !opt.includes(':')) { return { diff --git a/packages/server/lib/errors.coffee b/packages/server/lib/errors.coffee index de3a99b22dd3..0912e1e191d2 100644 --- a/packages/server/lib/errors.coffee +++ b/packages/server/lib/errors.coffee @@ -97,13 +97,22 @@ getMsgByType = (type, arg1 = {}, arg2, arg3) -> #{arg1} """ when "BROWSER_NOT_FOUND_BY_NAME" - """ + str = """ Can't run because you've entered an invalid browser name. Browser: '#{arg1}' was not found on your system. Available browsers found are: #{arg2} """ + + if arg1 is 'canary' + str += """ + \n\nNote: In Cypress 4.0, Canary must be launched as `chrome:canary`, not `canary`. + + See https://on.cypress.io/migration-guide for more information on breaking changes in 4.0. + """ + + return str when "BROWSER_NOT_FOUND_BY_PATH" msg = """ We could not identify a known browser at the path you provided: `#{arg1}` diff --git a/packages/server/test/unit/browsers/browsers_spec.coffee b/packages/server/test/unit/browsers/browsers_spec.coffee index f65e9b610712..07add4864ad1 100644 --- a/packages/server/test/unit/browsers/browsers_spec.coffee +++ b/packages/server/test/unit/browsers/browsers_spec.coffee @@ -36,12 +36,22 @@ describe "lib/browsers/index", -> expect(browser).to.deep.eq({ name: "foo", channel: "stable" }) it "throws when no browser can be found", -> - browsers.ensureAndGetByNameOrPath("browserNotGonnaBeFound") - .then -> - throw new Error("should have failed") - .catch (err) -> - expect(err.type).to.eq("BROWSER_NOT_FOUND_BY_NAME") - expect(err.message).to.contain("'browserNotGonnaBeFound' was not found on your system") + expect(browsers.ensureAndGetByNameOrPath("browserNotGonnaBeFound")) + .to.be.rejectedWith({ type: 'BROWSER_NOT_FOUND_BY_NAME' }) + .then (err) -> + snapshot(err.message) + + it "throws a special error when canary is passed", -> + sinon.stub(utils, "getBrowsers").resolves([ + { name: "chrome", channel: "stable" } + { name: "chrome", channel: "canary" } + { name: "firefox", channel: "stable" } + ]) + + expect(browsers.ensureAndGetByNameOrPath("canary")) + .to.be.rejectedWith({ type: 'BROWSER_NOT_FOUND_BY_NAME' }) + .then (err) -> + snapshot(err.message) context ".open", -> it "throws an error if browser family doesn't exist", -> From 4b4842e55c406423a0ffe5445d3f94820a886b13 Mon Sep 17 00:00:00 2001 From: Chris Breiding Date: Wed, 5 Feb 2020 16:01:11 -0500 Subject: [PATCH 45/49] Define types for plugin events (#6322) * define types for plugin events * add plugins file TS test * fix plugins return type * add void returns where allowed * config can be partial * add async test * cannot return unknown keys in config * add more tests * add Task types and tests * fix style issues * update task type with compromise, remove redundant tests * fix type * bump kitchensink dep * add typescript reference and jsdoc to plugins scaffold file * update scaffold snapshot * add more tests for before:browser:launch * fix return type Co-authored-by: Gleb Bahmutov Co-authored-by: Jennifer Shehane --- cli/types/index.d.ts | 54 +++++---- cli/types/tests/plugins-config.ts | 108 ++++++++++++++++++ packages/example/package.json | 2 +- .../server/__snapshots__/scaffold_spec.js | 4 + packages/server/lib/scaffold/plugins/index.js | 4 + 5 files changed, 147 insertions(+), 25 deletions(-) create mode 100644 cli/types/tests/plugins-config.ts diff --git a/cli/types/index.d.ts b/cli/types/index.d.ts index 469220ca0b49..7a1a3d877fed 100644 --- a/cli/types/index.d.ts +++ b/cli/types/index.d.ts @@ -50,7 +50,7 @@ declare namespace Cypress { type RequestBody = string | object type ViewportOrientation = "portrait" | "landscape" type PrevSubject = "optional" | "element" | "document" | "window" - type PluginConfig = (on: PluginEvents, config: ConfigOptions) => void + type PluginConfig = (on: PluginEvents, config: ConfigOptions) => void | Partial | Promise> interface CommandOptions { prevSubject: boolean | PrevSubject | PrevSubject[] @@ -4236,31 +4236,22 @@ declare namespace Cypress { (fn: (currentSubject: Subject) => void): Chainable } - // for just a few events like "window:alert" it makes sense to allow passing cy.stub() or - // a user callback function. Others probably only need a callback function. - - /** - * These events come from the application currently under test (your application). - * These are the most useful events for you to listen to. - * @see https://on.cypress.io/catalog-of-events#App-Events - */ - - interface browserLaunchOptions { + interface BrowserLaunchOptions { extensions: string[], - preferences: {[key: string]: any} + preferences: { [key: string]: any } args: string[], } - interface dimensions { + interface Dimensions { width: number height: number } - interface screenshotDetails { + interface ScreenshotDetails { size: number takenAt: string duration: number - dimensions: dimensions + dimensions: Dimensions multipart: boolean pixelRatio: number name: string @@ -4271,29 +4262,44 @@ declare namespace Cypress { blackout: string[] } - interface afterScreenshotReturnObject { + interface AfterScreenshotReturnObject { path?: string size?: number - dimensions?: dimensions + dimensions?: Dimensions } - interface fileObject { + interface FileObject { filePath: string outputPath: string shouldWatch: boolean } - interface tasks { - [key: string]: (value: any) => any + /** + * Individual task callback. Receives a single argument and _should_ return + * anything but `undefined` or a promise that resolves anything but `undefined` + * TODO: find a way to express "anything but undefined" in TypeScript + */ + type Task = (value: any) => any + + interface Tasks { + [key: string]: Task } interface PluginEvents { - (action: 'before:browser:launch', fn: (browser: Browser, browserLaunchOptions: browserLaunchOptions) => browserLaunchOptions): void - (action: 'after:screenshot', fn: (details: screenshotDetails) => afterScreenshotReturnObject | Promise): void - (action: 'file:preprocessor', fn: (file: fileObject) => string | Promise): void - (action: 'task', tasks: tasks): void + (action: 'before:browser:launch', fn: (browser: Browser, browserLaunchOptions: BrowserLaunchOptions) => void | BrowserLaunchOptions | Promise): void + (action: 'after:screenshot', fn: (details: ScreenshotDetails) => void | AfterScreenshotReturnObject | Promise): void + (action: 'file:preprocessor', fn: (file: FileObject) => string | Promise): void + (action: 'task', tasks: Tasks): void } + // for just a few events like "window:alert" it makes sense to allow passing cy.stub() or + // a user callback function. Others probably only need a callback function. + + /** + * These events come from the application currently under test (your application). + * These are the most useful events for you to listen to. + * @see https://on.cypress.io/catalog-of-events#App-Events + */ interface Actions { /** * Fires when an uncaught exception occurs in your application. diff --git a/cli/types/tests/plugins-config.ts b/cli/types/tests/plugins-config.ts new file mode 100644 index 000000000000..f081289040de --- /dev/null +++ b/cli/types/tests/plugins-config.ts @@ -0,0 +1,108 @@ +// checking types passed to cypress/plugins/index.js file + +// does nothing +const pluginConfig: Cypress.PluginConfig = (on, config) => {} + +// allows synchronous returns +const pluginConfig2: Cypress.PluginConfig = (on, config) => { + config // $ExpectType ConfigOptions + config.baseUrl // $ExpectType: string + + on('before:browser:launch', (browser, options) => { + browser.displayName // $ExpectType string + options.extensions // $ExpectType string[] + options.args // $ExpectType string[] + + console.log('launching browser', browser.displayName) + return options + }) + + on('file:preprocessor', (file) => { + file.filePath // $ExpectType string + file.outputPath // $ExpectType string + file.shouldWatch // $ExpectType boolean + + return file.outputPath + }) + + on('after:screenshot', (details) => { + details.size // $ExpectType number + details.takenAt // $ExpectType string + details.duration // $ExpectType number + details.dimensions // $ExpectType Dimensions + details.multipart // $ExpectType boolean + details.pixelRatio // $ExpectType number + details.name // $ExpectType string + details.specName // $ExpectType string + details.testFailure // $ExpectType boolean + details.path // $ExpectType string + details.scaled // $ExpectType boolean + details.blackout // $ExpectType string[] + + return { + path: '/path/to/screenshot', + size: 1000, + // FIXME: why can't dimensions be included? + // dimensions: { + // width: 100, + // height: 100, + // } + } + }) + + on('task', { + foo() { + return true + } + }) + + return { + baseUrl: 'http://localhost:3000' + } +} + +// allows/disallows void returns +const pluginConfig3: Cypress.PluginConfig = (on, config) => { + on('before:browser:launch', (browser, options) => {}) + + on('file:preprocessor', (file) => {}) // $ExpectError + + on('after:screenshot', () => {}) + + // FIXME: this should error, but doesn't because the type isn't quite right + // on('task', { // $ExpectError + // foo() {} + // }) +} + +// allows async returns +const pluginConfig4: Cypress.PluginConfig = (on, config) => { + on('before:browser:launch', (browser, options) => { + return Promise.resolve(options) + }) + + on('file:preprocessor', (file) => { + return Promise.resolve(file.outputPath) + }) + + on('after:screenshot', () => { + return Promise.resolve({}) + }) + + on('task', { + foo() { + return Promise.resolve([]) + } + }) + + return Promise.resolve({ + baseUrl: 'http://localhost:3000' + }) +} + +// does not allow returning unknown properties +const pluginConfig5: Cypress.PluginConfig = (on, config) => { // $ExpectError + return { + unknownKey: 42 + } +} diff --git a/packages/example/package.json b/packages/example/package.json index 0dffa2342af2..0e766216e220 100644 --- a/packages/example/package.json +++ b/packages/example/package.json @@ -29,7 +29,7 @@ "bin-up": "1.2.2", "chai": "3.5.0", "cross-env": "6.0.3", - "cypress-example-kitchensink": "1.9.0", + "cypress-example-kitchensink": "1.9.1", "gulp": "4.0.2", "gulp-clean": "0.4.0", "gulp-gh-pages": "0.6.0-6", diff --git a/packages/server/__snapshots__/scaffold_spec.js b/packages/server/__snapshots__/scaffold_spec.js index 6652540b8ab6..a1831b1bb765 100644 --- a/packages/server/__snapshots__/scaffold_spec.js +++ b/packages/server/__snapshots__/scaffold_spec.js @@ -428,6 +428,7 @@ exports['lib/scaffold .fileTree leaves out plugins if configured to false 1'] = ] exports['lib/scaffold .plugins creates pluginsFile when pluginsFolder does not exist 1'] = ` +/// // *********************************************************** // This example plugins/index.js can be used to load plugins // @@ -441,6 +442,9 @@ exports['lib/scaffold .plugins creates pluginsFile when pluginsFolder does not e // This function is called when a project is opened or re-opened (e.g. due to // the project's config changing) +/** + * @type {Cypress.PluginConfig} + */ module.exports = (on, config) => { // on is used to hook into various events Cypress emits // config is the resolved Cypress config diff --git a/packages/server/lib/scaffold/plugins/index.js b/packages/server/lib/scaffold/plugins/index.js index fd170fba6912..aa9918d21530 100644 --- a/packages/server/lib/scaffold/plugins/index.js +++ b/packages/server/lib/scaffold/plugins/index.js @@ -1,3 +1,4 @@ +/// // *********************************************************** // This example plugins/index.js can be used to load plugins // @@ -11,6 +12,9 @@ // This function is called when a project is opened or re-opened (e.g. due to // the project's config changing) +/** + * @type {Cypress.PluginConfig} + */ module.exports = (on, config) => { // `on` is used to hook into various events Cypress emits // `config` is the resolved Cypress config From 7019a0bfef6c172fc1578ee6e416af5b0ec66509 Mon Sep 17 00:00:00 2001 From: Zach Bloomquist Date: Wed, 5 Feb 2020 17:39:52 -0500 Subject: [PATCH 46/49] updates to electon extensions - Windows.resetExtensions => removeAllExtensions - remove all extensions before installing too --- packages/server/lib/browsers/electron.coffee | 4 +++- packages/server/lib/gui/windows.coffee | 2 +- packages/server/test/unit/browsers/electron_spec.coffee | 9 ++++++++- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/packages/server/lib/browsers/electron.coffee b/packages/server/lib/browsers/electron.coffee index 55c6a7ea581d..669f40afbb9f 100644 --- a/packages/server/lib/browsers/electron.coffee +++ b/packages/server/lib/browsers/electron.coffee @@ -44,6 +44,8 @@ getAutomation = (win) -> CdpAutomation(sendCommand) _installExtensions = (extensionPaths = [], options) -> + Windows.removeAllExtensions() + extensionPaths.forEach (path) -> try Windows.installExtension(path) @@ -264,7 +266,7 @@ module.exports = { win.once "closed", -> debug("closed event fired") - Windows.resetExtensions() + Windows.removeAllExtensions() events.emit("exit") diff --git a/packages/server/lib/gui/windows.coffee b/packages/server/lib/gui/windows.coffee index 94bafd89d01e..d9a07d2b6b0c 100644 --- a/packages/server/lib/gui/windows.coffee +++ b/packages/server/lib/gui/windows.coffee @@ -49,7 +49,7 @@ module.exports = { if !name throw new Error('Extension could not be installed.') - resetExtensions: -> + removeAllExtensions: -> ## remove all extensions extensions = _.keys(BrowserWindow.getExtensions()) diff --git a/packages/server/test/unit/browsers/electron_spec.coffee b/packages/server/test/unit/browsers/electron_spec.coffee index a3050382b8c6..87cce4ad602b 100644 --- a/packages/server/test/unit/browsers/electron_spec.coffee +++ b/packages/server/test/unit/browsers/electron_spec.coffee @@ -47,7 +47,7 @@ describe "lib/browsers/electron", -> }) sinon.stub(Windows, 'installExtension').returns() - sinon.stub(Windows, 'resetExtensions').returns() + sinon.stub(Windows, 'removeAllExtensions').returns() @stubForOpen = -> sinon.stub(electron, "_render").resolves(@win) @@ -122,6 +122,8 @@ describe "lib/browsers/electron", -> electron.open("electron", @url, @options, @automation) .then => + expect(Windows.removeAllExtensions).to.be.calledOnce + expect(Windows.installExtension).to.be.calledTwice expect(Windows.installExtension).to.be.calledWith('foo') expect(Windows.installExtension).to.be.calledWith('bar') @@ -131,6 +133,11 @@ describe "lib/browsers/electron", -> warning = @options.onWarning.firstCall.args[0].message expect(warning).to.contain('Electron').and.contain('bar') + @win.emit('closed') + + ## called once before installing extensions, once on exit + expect(Windows.removeAllExtensions).to.be.calledTwice + context "._launch", -> beforeEach -> sinon.stub(menu, "set") From 5abec9d39d6ce3ed997b9885230ce26bbd329e3e Mon Sep 17 00:00:00 2001 From: Zach Bloomquist Date: Wed, 5 Feb 2020 17:42:01 -0500 Subject: [PATCH 47/49] remove redundant comment --- packages/server/lib/gui/windows.coffee | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/server/lib/gui/windows.coffee b/packages/server/lib/gui/windows.coffee index d9a07d2b6b0c..e5c825c3bbf8 100644 --- a/packages/server/lib/gui/windows.coffee +++ b/packages/server/lib/gui/windows.coffee @@ -50,7 +50,6 @@ module.exports = { throw new Error('Extension could not be installed.') removeAllExtensions: -> - ## remove all extensions extensions = _.keys(BrowserWindow.getExtensions()) debug('removing all electron extensions %o', extensions) From aa1cc0ddbe8f6433dbbcedac8523d3a4c7b03f61 Mon Sep 17 00:00:00 2001 From: Jennifer Shehane Date: Thu, 6 Feb 2020 11:24:56 +0630 Subject: [PATCH 48/49] Bump @cypress/browserify-preprocessor to latest release --- packages/server/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server/package.json b/packages/server/package.json index 347dd6af0171..019dcf6539a9 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -40,7 +40,7 @@ "test-watch": "./test/support/watch test" }, "dependencies": { - "@cypress/browserify-preprocessor": "2.1.1", + "@cypress/browserify-preprocessor": "2.1.3", "@cypress/commit-info": "2.2.0", "@cypress/get-windows-proxy": "1.6.0", "@cypress/icons": "0.7.0", From d76123b3d9d3e7cca38daf1b800b60777ab411db Mon Sep 17 00:00:00 2001 From: Chris Breiding Date: Thu, 6 Feb 2020 10:52:13 -0500 Subject: [PATCH 49/49] Add Firefox support (#1359) Co-authored-by: Ben Kucera <14625260+Bkucera@users.noreply.github.com> Co-authored-by: Gleb Bahmutov Co-authored-by: Brian Mann Co-authored-by: Zach Bloomquist Co-authored-by: Jennifer Shehane --- .vscode/settings.json | 3 +- circle.yml | 348 ++++++++++++-- cli/__snapshots__/cli_spec.js | 2 +- cli/lib/cli.js | 2 +- cli/types/index.d.ts | 55 ++- package.json | 6 +- .../cypress/integration/project_nav_spec.js | 35 +- .../cypress/integration/specs_list_spec.js | 8 +- packages/desktop-gui/src/app/nav.scss | 20 +- packages/desktop-gui/src/lib/browser-model.js | 2 + .../desktop-gui/src/project-nav/browsers.jsx | 12 +- packages/desktop-gui/src/specs/specs-list.jsx | 2 +- .../src/styles/components/_general.scss | 8 +- packages/driver/package.json | 2 +- packages/driver/src/cy/chai.js | 139 +++++- packages/driver/src/cy/chai/inspect.js | 431 ++++++++++++++++++ packages/driver/src/cy/commands/cookies.js | 4 + .../driver/src/cy/commands/navigation.coffee | 7 + .../driver/src/cy/commands/request.coffee | 3 +- packages/driver/src/cy/commands/task.coffee | 2 + packages/driver/src/cy/errors.coffee | 3 + packages/driver/src/cy/keyboard.ts | 29 +- packages/driver/src/cy/mouse.js | 212 +++++---- packages/driver/src/cy/snapshots.coffee | 98 ++-- packages/driver/src/cy/video-recorder.ts | 37 ++ packages/driver/src/cypress.js | 25 +- packages/driver/src/cypress/browser.coffee | 22 + packages/driver/src/cypress/cy.js | 7 +- .../driver/src/cypress/error_messages.coffee | 3 + packages/driver/src/cypress/runner.js | 17 +- packages/driver/src/cypress/utils.coffee | 13 + packages/driver/src/dom/.eslintrc.json | 5 + packages/driver/src/dom/coordinates.js | 13 +- packages/driver/src/dom/document.ts | 6 + packages/driver/src/dom/elements.ts | 19 +- packages/driver/src/dom/selection.ts | 107 ++++- packages/driver/src/util/firefox_forced_gc.ts | 58 +++ .../driver/test/cypress/fixtures/dom.html | 52 ++- .../commands/actions/click_spec.js | 239 ++++------ .../commands/actions/scroll_spec.coffee | 6 +- .../commands/actions/trigger_spec.coffee | 28 +- .../integration/commands/actions/type_spec.js | 185 ++++++-- .../integration/commands/clock_spec.coffee | 5 +- .../integration/commands/cookies_spec.coffee | 17 +- .../commands/navigation_spec.coffee | 32 +- .../integration/commands/xhr_spec.coffee | 6 +- .../cypress/integration/cy/timers_spec.js | 20 +- .../integration/cypress/browser_spec.coffee | 39 ++ .../integration/cypress/utils_spec.coffee | 11 +- .../integration/dom/visibility_spec.js | 2 +- .../integration/e2e/focus_blur_spec.js | 69 ++- .../integration/e2e/redirects_spec.coffee | 3 +- .../cypress/integration/e2e/webcam_spec.js | 7 + .../cypress/integration/issues/573_spec.js | 4 +- .../integration/issues/761_2968_3973_spec.js | 132 +++--- .../util/firefox_forced_gc_spec.ts | 217 +++++++++ packages/extension/README.md | 8 +- packages/extension/app/background.js | 144 +++--- packages/extension/app/manifest.json | 10 +- packages/extension/lib/extension.js | 7 +- packages/extension/lib/util.js | 7 + packages/extension/package.json | 5 +- .../test/integration/background_spec.coffee | 179 ++++---- packages/https-proxy/https.js | 14 +- .../__snapshots__/browsers_spec.ts.js | 60 ++- packages/launcher/lib/browsers.ts | 37 ++ packages/launcher/lib/darwin/index.ts | 27 +- packages/launcher/lib/darwin/util.ts | 9 +- packages/launcher/lib/detect.ts | 2 + packages/launcher/lib/types.ts | 14 +- packages/launcher/lib/windows/index.ts | 23 + packages/proxy/lib/http/error-middleware.ts | 10 +- packages/proxy/lib/http/index.ts | 3 +- packages/proxy/lib/http/request-middleware.ts | 6 +- packages/reporter/src/commands/commands.scss | 2 +- packages/reporter/src/header/header.scss | 2 +- .../reporter/src/instruments/instruments.scss | 2 +- packages/reporter/src/lib/app-state.spec.ts | 22 + packages/reporter/src/lib/app-state.ts | 14 + packages/reporter/src/lib/base.scss | 2 + packages/reporter/src/lib/events.spec.ts | 21 + packages/reporter/src/lib/events.ts | 12 + .../reporter/src/lib/forced-gc-warning.scss | 79 ++++ .../src/lib/forced-gc-warning.spec.tsx | 112 +++++ .../reporter/src/lib/forced-gc-warning.tsx | 159 +++++++ packages/reporter/src/lib/variables.scss | 2 + packages/reporter/src/main.spec.tsx | 10 + packages/reporter/src/main.tsx | 4 + .../reporter/src/runnables/runnables.scss | 9 +- packages/runner/src/app/app.jsx | 4 + packages/runner/src/app/app.scss | 1 + packages/runner/src/app/app.spec.jsx | 9 + .../src/errors/automation-disconnected.jsx | 2 +- .../errors/automation-disconnected.spec.jsx | 2 +- packages/runner/src/errors/errors.scss | 7 +- packages/runner/src/header/header.scss | 9 + packages/runner/src/iframe/iframe.scss | 10 +- packages/runner/src/iframe/iframes.jsx | 5 +- packages/runner/src/lib/event-manager.js | 23 +- ...> 1_browserify_babel_es201_spec.coffee.js} | 0 .../1_commands_outside_of_test_spec.coffee.js | 12 +- .../__snapshots__/1_deprecated_spec.ts.js | 2 +- .../__snapshots__/2_headless_spec.ts.js | 6 +- .../__snapshots__/3_issue_1669_spec.coffee.js | 2 +- .../__snapshots__/3_issue_173_spec.coffee.js | 2 +- .../__snapshots__/3_issue_674_spec.coffee.js | 2 +- .../4_form_submissions_spec.coffee.js | 17 +- .../__snapshots__/4_promises_spec.coffee.js | 2 +- .../__snapshots__/4_request_spec.coffee.js | 2 +- .../4_return_value_spec.coffee.js | 2 +- .../6_uncaught_spec_errors_spec.coffee.js | 4 +- .../6_uncaught_support_file_spec.coffee.js | 2 +- .../__snapshots__/6_visit_spec.coffee.js | 8 +- .../6_web_security_spec.coffee.js | 71 ++- .../__snapshots__/7_record_spec.coffee.js | 4 +- packages/server/lib/browsers/chrome.ts | 11 +- packages/server/lib/browsers/firefox-util.ts | 284 ++++++++++++ packages/server/lib/browsers/firefox.ts | 255 +++++++++++ packages/server/lib/browsers/index.js | 6 +- packages/server/lib/browsers/types.ts | 7 + packages/server/lib/browsers/utils.ts | 2 - packages/server/lib/config.coffee | 3 + packages/server/lib/controllers/runner.coffee | 1 + packages/server/lib/errors.coffee | 11 +- packages/server/lib/modes/run.js | 48 +- packages/server/lib/project.js | 13 + packages/server/lib/socket.js | 16 +- packages/server/lib/util/stream_buffer.ts | 133 +++--- packages/server/lib/util/validation.js | 21 + packages/server/lib/video_capture.js | 40 +- packages/server/package.json | 10 +- ...e => 1_browserify_babel_es201_spec.coffee} | 0 packages/server/test/e2e/1_firefox_spec.ts | 59 +++ packages/server/test/e2e/2_headless_spec.ts | 34 +- .../server/test/e2e/3_issue_173_spec.coffee | 11 +- .../server/test/e2e/3_issue_674_spec.coffee | 11 +- .../test/e2e/4_form_submissions_spec.coffee | 25 +- .../server/test/e2e/4_promises_spec.coffee | 11 +- .../test/e2e/6_video_compression_spec.coffee | 2 +- .../test/e2e/6_web_security_spec.coffee | 16 +- packages/server/test/scripts/run.js | 4 +- packages/server/test/specUtils.ts | 50 ++ packages/server/test/spec_helper.coffee | 7 +- .../cypress/integration/firefox_windowSize.js | 6 + .../cypress/integration/screenshots_spec.js | 2 +- .../server_sent_events_spec.coffee | 2 +- .../integration/websockets_spec.coffee | 12 +- .../projects/e2e/cypress/plugins/index.js | 10 +- .../projects/firefox-memory/cypress.json | 1 + .../cypress/integration/spec.js | 85 ++++ .../firefox-memory/cypress/plugins/index.js | 109 +++++ .../plugin-event-deprecated/cypress.json | 1 + .../cypress/integration/app_spec.coffee | 2 + .../cypress/plugins/index.js | 43 ++ .../projects/screen-size/cypress.json | 1 + .../cypress/integration/firefox_windowSize.js | 7 + .../screen-size/cypress/plugins/index.js | 10 + packages/server/test/support/helpers/e2e.js | 35 +- .../test/unit/browsers/browsers_spec.coffee | 1 + .../test/unit/browsers/chrome_spec.coffee | 38 ++ .../server/test/unit/browsers/firefox_spec.ts | 368 +++++++++++++++ packages/server/test/unit/config_spec.coffee | 21 + .../server/test/unit/gui/events_spec.coffee | 2 +- packages/server/test/unit/project_spec.js | 30 ++ packages/server/test/unit/socket_spec.coffee | 25 +- packages/socket/package.json | 2 +- scripts/binary/smoke.coffee | 5 +- scripts/unit/binary/util/packages-spec.js | 2 +- 168 files changed, 4919 insertions(+), 1058 deletions(-) create mode 100644 packages/driver/src/cy/chai/inspect.js create mode 100644 packages/driver/src/cy/video-recorder.ts create mode 100644 packages/driver/src/cypress/browser.coffee create mode 100644 packages/driver/src/dom/.eslintrc.json create mode 100644 packages/driver/src/util/firefox_forced_gc.ts create mode 100644 packages/driver/test/cypress/integration/cypress/browser_spec.coffee create mode 100644 packages/driver/test/cypress/integration/util/firefox_forced_gc_spec.ts create mode 100644 packages/extension/lib/util.js create mode 100644 packages/reporter/src/lib/forced-gc-warning.scss create mode 100644 packages/reporter/src/lib/forced-gc-warning.spec.tsx create mode 100644 packages/reporter/src/lib/forced-gc-warning.tsx rename packages/server/__snapshots__/{1_browserify_babel_es201spec.coffee.js => 1_browserify_babel_es201_spec.coffee.js} (100%) create mode 100644 packages/server/lib/browsers/firefox-util.ts create mode 100644 packages/server/lib/browsers/firefox.ts create mode 100644 packages/server/lib/browsers/types.ts rename packages/server/test/e2e/{1_browserify_babel_es201spec.coffee => 1_browserify_babel_es201_spec.coffee} (100%) create mode 100644 packages/server/test/e2e/1_firefox_spec.ts create mode 100644 packages/server/test/specUtils.ts create mode 100644 packages/server/test/support/fixtures/projects/e2e/cypress/integration/firefox_windowSize.js create mode 100644 packages/server/test/support/fixtures/projects/firefox-memory/cypress.json create mode 100644 packages/server/test/support/fixtures/projects/firefox-memory/cypress/integration/spec.js create mode 100644 packages/server/test/support/fixtures/projects/firefox-memory/cypress/plugins/index.js create mode 100644 packages/server/test/support/fixtures/projects/plugin-event-deprecated/cypress.json create mode 100644 packages/server/test/support/fixtures/projects/plugin-event-deprecated/cypress/integration/app_spec.coffee create mode 100644 packages/server/test/support/fixtures/projects/plugin-event-deprecated/cypress/plugins/index.js create mode 100644 packages/server/test/support/fixtures/projects/screen-size/cypress.json create mode 100644 packages/server/test/support/fixtures/projects/screen-size/cypress/integration/firefox_windowSize.js create mode 100644 packages/server/test/support/fixtures/projects/screen-size/cypress/plugins/index.js create mode 100644 packages/server/test/unit/browsers/firefox_spec.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index a08fff44ae4d..c25c1b8538c5 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -45,5 +45,6 @@ "editor.codeActionsOnSave": { "source.fixAll.eslint": false } - } + }, + "typescript.tsdk": "node_modules/typescript/lib" } diff --git a/circle.yml b/circle.yml index d64e2efc3321..2ab3b356b566 100644 --- a/circle.yml +++ b/circle.yml @@ -28,6 +28,15 @@ defaults: &defaults COLUMNS: 100 LINES: 24 +# filters and requires for testing binary with Firefox +testBinaryFirefox: &testBinaryFirefox + filters: + branches: + only: + - develop + requires: + - build-npm-package + - build-binary executors: # the Docker image with Cypress dependencies and Chrome browser @@ -247,6 +256,83 @@ commands: packageName: web-config packagePath: packages/web-config/package.json + post-install-comment: + description: Post GitHub comment with a blurb on how to install pre-release version + steps: + - run: ls -la + # make sure JSON files with uploaded urls are present + - run: ls -la binary-url.json npm-package-url.json + - run: cat binary-url.json + - run: cat npm-package-url.json + - run: + name: Post pre-release install comment + command: | + node scripts/add-install-comment.js \ + --npm npm-package-url.json \ + --binary binary-url.json + + test-binary-against-repo: + description: | + Takes the built binary and NPM package, clones given example repo + and runs the new version of Cypress against it. + parameters: + repo: + description: Name of the repo like "cypress-example-kitchensink" + type: string + browser: + description: Name of the browser to use + type: enum + enum: ["electron", "chrome", "firefox"] + default: "electron" + command: + description: Test command to run to start Cypress tests + type: string + default: "npm run e2e" + wait-on: + description: Url to wait-on before starting tests + type: string + default: "" + steps: + - attach_workspace: + at: ~/ + # make sure the binary and NPM package files are present + - run: ls -l + - run: ls -l cypress.zip cypress.tgz + - run: + name: Cloning project <> + command: git clone --depth 1 https://github.com/cypress-io/<>.git /tmp/<> + - run: + command: npm install + working_directory: /tmp/<> + - run: + name: Install Cypress + working_directory: /tmp/<> + # force installing the freshly built binary + command: CYPRESS_INSTALL_BINARY=~/cypress/cypress.zip npm i ~/cypress/cypress.tgz + - run: + working_directory: /tmp/<> + command: npm run build --if-present + - run: + working_directory: /tmp/<> + command: npm start --if-present + background: true + - when: + condition: <> + steps: + - run: + name: Wait-on <> + command: npx wait-on <> --timeout 120000 + - run: + working_directory: /tmp/<> + command: <> -- --browser <> + - store-npm-logs + - store_artifacts: + name: screenshots + path: /tmp/<>/cypress/screenshots + - store_artifacts: + name: videos + path: /tmp/<>/cypress/videos + jobs: ## code checkout and NPM installs build: @@ -508,6 +594,62 @@ jobs: browser: electron chunk: 8 + "server-e2e-tests-firefox-1": + <<: *defaults + steps: + - run-e2e-tests: + browser: firefox + chunk: 1 + + "server-e2e-tests-firefox-2": + <<: *defaults + steps: + - run-e2e-tests: + browser: firefox + chunk: 2 + + "server-e2e-tests-firefox-3": + <<: *defaults + steps: + - run-e2e-tests: + browser: firefox + chunk: 3 + + "server-e2e-tests-firefox-4": + <<: *defaults + steps: + - run-e2e-tests: + browser: firefox + chunk: 4 + + "server-e2e-tests-firefox-5": + <<: *defaults + steps: + - run-e2e-tests: + browser: firefox + chunk: 5 + + "server-e2e-tests-firefox-6": + <<: *defaults + steps: + - run-e2e-tests: + browser: firefox + chunk: 6 + + "server-e2e-tests-firefox-7": + <<: *defaults + steps: + - run-e2e-tests: + browser: firefox + chunk: 7 + + "server-e2e-tests-firefox-8": + <<: *defaults + steps: + - run-e2e-tests: + browser: firefox + chunk: 8 + "driver-integration-tests-chrome": <<: *defaults parallelism: 5 @@ -558,6 +700,30 @@ jobs: # path: /tmp/artifacts # - store-npm-logs + "driver-integration-tests-firefox": + <<: *defaults + parallelism: 5 + steps: + - attach_workspace: + at: ~/ + - run: + command: npm start + background: true + working_directory: packages/driver + - run: + command: $(npm bin)/wait-on http://localhost:3500 + working_directory: packages/driver + - run: + command: | + CYPRESS_KONFIG_ENV=production \ + CYPRESS_RECORD_KEY=$PACKAGES_RECORD_KEY \ + npm run cypress:run -- --record --parallel --group 5x-driver-firefox --browser firefox + working_directory: packages/driver + - store_test_results: + path: /tmp/cypress + - store_artifacts: + path: /tmp/artifacts + "desktop-gui-integration-tests-2x": <<: *defaults parallelism: 2 @@ -857,12 +1023,6 @@ jobs: name: Verify Cypress binary working_directory: /tmp/testing command: $(npm bin)/cypress verify - - run: - name: Post pre-release install comment - command: | - node scripts/add-install-comment.js \ - --npm npm-package-url.json \ - --binary binary-url.json - run: name: Running other test projects with new NPM package and binary command: | @@ -872,6 +1032,15 @@ jobs: --provider circle - store-npm-logs + post-pre-release-install-comment: + <<: *defaults + steps: + # needs uploaded NPM and test binary + - attach_workspace: + at: ~/ + - run: ls -la + - post-install-comment + "test-npm-module-and-verify-binary": <<: *defaults steps: @@ -924,36 +1093,76 @@ jobs: $(npm bin)/cypress run --record - store-npm-logs + "test-binary-against-recipes-firefox": + <<: *defaults + steps: + - test-binary-against-repo: + repo: cypress-example-recipes + command: npm run test:ci:firefox + "test-binary-against-kitchensink": <<: *defaults steps: - - attach_workspace: - at: ~/ - # make sure the binary and NPM package files are present - - run: ls -l - - run: ls -l cypress.zip cypress.tgz - - run: - name: Cloning kitchensink project - command: git clone --depth 1 https://github.com/cypress-io/cypress-example-kitchensink.git /tmp/kitchensink - - run: - command: npm install - working_directory: /tmp/kitchensink - - run: - name: Install Cypress - working_directory: /tmp/kitchensink - # force installing the freshly built binary - command: CYPRESS_INSTALL_BINARY=~/cypress/cypress.zip npm i ~/cypress/cypress.tgz - - run: - working_directory: /tmp/kitchensink - command: npm run build - - run: - working_directory: /tmp/kitchensink - command: npm start - background: true - - run: - working_directory: /tmp/kitchensink - command: npm run e2e - - store-npm-logs + - test-binary-against-repo: + repo: cypress-example-kitchensink + + "test-binary-against-kitchensink-firefox": + <<: *defaults + steps: + - test-binary-against-repo: + repo: cypress-example-kitchensink + browser: firefox + + "test-binary-against-kitchensink-chrome": + <<: *defaults + steps: + - test-binary-against-repo: + repo: cypress-example-kitchensink + browser: chrome + + "test-binary-against-todomvc-firefox": + <<: *defaults + steps: + - test-binary-against-repo: + repo: cypress-example-todomvc + browser: firefox + + "test-binary-against-documentation-firefox": + <<: *defaults + steps: + - test-binary-against-repo: + repo: cypress-documentation + browser: firefox + command: "npm run cypress:run" + wait-on: "http://localhost:2222" + + + "test-binary-against-realworld-firefox": + <<: *defaults + steps: + - test-binary-against-repo: + repo: cypress-example-realworld + browser: firefox + command: "npm run cypress:run" + wait-on: "http://localhost:4100" + + "test-binary-against-api-testing-firefox": + <<: *defaults + steps: + - test-binary-against-repo: + repo: cypress-example-api-testing + browser: firefox + command: "npm run cy:run" + wait-on: "http://localhost:3000" + + "test-binary-against-piechopper-firefox": + <<: *defaults + steps: + - test-binary-against-repo: + repo: cypress-example-piechopper + browser: firefox + command: "npm run cypress:run" + wait-on: "http://localhost:8080" test-binary-as-specific-user: <<: *defaults @@ -1083,9 +1292,36 @@ linux-workflow: &linux-workflow context: test-runner:performance-tracking requires: - build + - server-e2e-tests-firefox-1: + requires: + - build + - server-e2e-tests-firefox-2: + requires: + - build + - server-e2e-tests-firefox-3: + requires: + - build + - server-e2e-tests-firefox-4: + requires: + - build + - server-e2e-tests-firefox-5: + requires: + - build + - server-e2e-tests-firefox-6: + requires: + - build + - server-e2e-tests-firefox-7: + requires: + - build + - server-e2e-tests-firefox-8: + requires: + - build - driver-integration-tests-chrome: requires: - build + - driver-integration-tests-firefox: + requires: + - build ## TODO: add these back in when flaky tests are fixed # - driver-integration-tests-electron: # requires: @@ -1146,6 +1382,15 @@ linux-workflow: &linux-workflow - develop requires: - build-binary + - post-pre-release-install-comment: + context: test-runner:commit-status-checks + filters: + branches: + only: + - develop + requires: + - upload-npm-package + - upload-binary - test-binary-and-npm-against-other-projects: context: test-runner:trigger-test-jobs filters: @@ -1172,14 +1417,26 @@ linux-workflow: &linux-workflow requires: - build-npm-package - build-binary + + - test-binary-against-recipes-firefox: + <<: *testBinaryFirefox - test-binary-against-kitchensink: - filters: - branches: - only: - - develop - requires: - - build-npm-package - - build-binary + <<: *testBinaryFirefox + - test-binary-against-kitchensink-firefox: + <<: *testBinaryFirefox + - test-binary-against-kitchensink-chrome: + <<: *testBinaryFirefox + - test-binary-against-todomvc-firefox: + <<: *testBinaryFirefox + - test-binary-against-documentation-firefox: + <<: *testBinaryFirefox + - test-binary-against-api-testing-firefox: + <<: *testBinaryFirefox + - test-binary-against-realworld-firefox: + <<: *testBinaryFirefox + - test-binary-against-piechopper-firefox: + <<: *testBinaryFirefox + - test-binary-as-specific-user: name: "test binary as a non-root user" executor: non-root-docker-user @@ -1278,6 +1535,17 @@ mac-workflow: &mac-workflow - Mac NPM package - Mac binary + - post-pre-release-install-comment: + context: test-runner:commit-status-checks + name: Post Mac pre-release install comment + filters: + branches: + only: + - develop + requires: + - Mac NPM package upload + - Mac binary upload + - test-binary-and-npm-against-other-projects: context: test-runner:trigger-test-jobs name: Test Mac binary against other projects diff --git a/cli/__snapshots__/cli_spec.js b/cli/__snapshots__/cli_spec.js index 5eaf5f414e5c..997b069ac556 100644 --- a/cli/__snapshots__/cli_spec.js +++ b/cli/__snapshots__/cli_spec.js @@ -69,7 +69,7 @@ exports['shows help for run --foo 1'] = ` -e, --env sets environment variables. separate multiple values with a comma. overrides any value in cypress.json or cypress.env.json --group a named group for recorded runs in the Cypress Dashboard -k, --key your secret Record Key. you can omit this if you set a CYPRESS_RECORD_KEY environment variable. - --headed displays the browser instead of running headlessly (defaults to true for Chrome-family browsers) + --headed displays the browser instead of running headlessly (defaults to true for Firefox and Chromium-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 diff --git a/cli/lib/cli.js b/cli/lib/cli.js index ecb7f3550157..7b611a2749e8 100644 --- a/cli/lib/cli.js +++ b/cli/lib/cli.js @@ -103,7 +103,7 @@ 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 browser instead of running headlessly (defaults to true for Chrome-family browsers)', + headed: 'displays the browser instead of running headlessly (defaults to true for Firefox and Chromium-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', diff --git a/cli/types/index.d.ts b/cli/types/index.d.ts index 7a1a3d877fed..099ea5f24ffd 100644 --- a/cli/types/index.d.ts +++ b/cli/types/index.d.ts @@ -33,6 +33,8 @@ // Cypress, cy, Log inherits EventEmitter. type EventEmitter2 = import("eventemitter2").EventEmitter2 +type Nullable = T | null + interface EventEmitter extends EventEmitter2 { proxyTo: (cy: Cypress.cy) => null emitMap: (eventName: string, args: any[]) => Array<(...args: any[]) => any> @@ -63,11 +65,21 @@ declare namespace Cypress { password: string } - type BrowserName = 'electron' | 'chrome' | 'chromium' | string + interface Backend { + /** + * Firefox only: Force Cypress to run garbage collection routines. + * No-op if not running in Firefox. + * + * @see https://on.cypress.io/firefox-gc-issue + */ + (task: 'firefox:force:gc'): Promise + } + + type BrowserName = 'electron' | 'chrome' | 'chromium' | 'firefox' | string - type BrowserChannel = 'stable' | 'canary' | 'beta' | 'dev' | string + type BrowserChannel = 'stable' | 'canary' | 'beta' | 'dev' | 'nightly' | string - type BrowserFamily = 'chromium' + type BrowserFamily = 'chromium' | 'firefox' /** * Describes a browser Cypress can control @@ -224,6 +236,11 @@ declare namespace Cypress { */ LocalStorage: LocalStorage + /** + * Promise wrapper for certain internal tasks. + */ + backend: Backend + /** * Returns all configuration objects. * @see https://on.cypress.io/config @@ -298,6 +315,15 @@ declare namespace Cypress { */ env(object: ObjectLike): void + /** + * Firefox only: Get the current number of tests that will run between forced garbage collections. + * + * Returns undefined if not in Firefox, returns a null or 0 if forced GC is disabled. + * + * @see https://on.cypress.io/firefox-gc-issue + */ + getFirefoxGcInterval(): number | null | undefined + /** * Checks if a variable is a valid instance of `cy` or a `cy` chainable. * @@ -308,6 +334,13 @@ declare namespace Cypress { isCy(obj: Chainable): obj is Chainable isCy(obj: any): obj is Chainable + /** + * Checks if you're running in the supplied browser family. + * e.g. isBrowser('Chrome') will be true for the browser 'Canary' + * @param name browser family name to check + */ + isBrowser(name: string): boolean + /** * Internal options for "cy.log" used in custom commands. * @@ -1113,7 +1146,7 @@ declare namespace Cypress { parentsUntil(element: E | JQuery, filter?: string, options?: Partial): Chainable> /** - * Stop cy commands from running and allow interaction with the application under test. You can then “resume” running all commands or choose to step through the “next” commands from the Command Log. + * Stop cy commands from running and allow interaction with the application under test. You can then "resume" running all commands or choose to step through the "next" commands from the Command Log. * This does not set a `debugger` in your code, unlike `.debug()` * * @see https://on.cypress.io/pause @@ -1256,7 +1289,7 @@ declare namespace Cypress { * Get the root DOM element. * The root element yielded is `` by default. * However, when calling `.root()` from a `.within()` command, - * the root element will point to the element you are “within”. + * the root element will point to the element you are "within". * * @see https://on.cypress.io/root */ @@ -2239,6 +2272,13 @@ declare namespace Cypress { * @default true */ waitForAnimations: boolean + /** + * Firefox-only: The number of tests that will run between forced garbage collections. + * If a number is supplied, it will apply to `run` mode and `open` mode. + * Set the interval to `null` or 0 to disable forced garbage collections. + * @default { runMode: 1, openMode: null } + */ + firefoxGcInterval: Nullable, openMode: Nullable }> } interface DebugOptions { @@ -4434,6 +4474,10 @@ declare namespace Cypress { * @see https://on.cypress.io/catalog-of-events#App-Events */ (action: 'test:before:run', fn: (attributes: ObjectLike, test: Mocha.ITest) => void): void + /** + * Fires before the test and all **before** and **beforeEach** hooks run. If a `Promise` is returned, it will be awaited before proceeding. + */ + (action: 'test:before:run:async', fn: (attributes: ObjectLike, test: Mocha.ITest) => void | Promise): void /** * Fires after the test and all **afterEach** and **after** hooks run. * @see https://on.cypress.io/catalog-of-events#App-Events @@ -4446,6 +4490,7 @@ declare namespace Cypress { logs(filters: any): any add(obj: any): any get(): any + get(key: string): CommandQueue[K] toJSON(): string[] create(): CommandQueue } diff --git a/package.json b/package.json index b4d905fdba78..447e2991861b 100644 --- a/package.json +++ b/package.json @@ -92,8 +92,8 @@ "@types/react-dom": "16.9.4", "@types/request-promise": "4.1.45", "@types/sinon-chai": "3.2.3", - "@typescript-eslint/eslint-plugin": "1.13.0", - "@typescript-eslint/parser": "1.13.0", + "@typescript-eslint/eslint-plugin": "2.14.0", + "@typescript-eslint/parser": "2.14.0", "ansi-styles": "3.2.1", "arg": "4.1.2", "ascii-table": "0.0.9", @@ -167,7 +167,7 @@ "terminal-banner": "1.1.0", "through": "2.3.8", "ts-node": "8.3.0", - "typescript": "3.5.3", + "typescript": "3.7.4", "vinyl-paths": "2.1.0" }, "engines": { diff --git a/packages/desktop-gui/cypress/integration/project_nav_spec.js b/packages/desktop-gui/cypress/integration/project_nav_spec.js index 4ff13c741eb2..65f731633980 100644 --- a/packages/desktop-gui/cypress/integration/project_nav_spec.js +++ b/packages/desktop-gui/cypress/integration/project_nav_spec.js @@ -148,6 +148,11 @@ describe('Project Nav', function () { }) }) + it('shows beta text for firefox', function () { + cy.get('.browsers li').contains('Firefox') + .contains('beta') + }) + it('shows info icon with tooltip for browsder with info', function () { const browserWithInfo = _.find(this.config.browsers, (b) => !!b.info) @@ -252,7 +257,7 @@ describe('Project Nav', function () { const browserArg = this.ipc.launchBrowser.getCall(0).args[0].browser expect(browserArg).to.have.keys([ - 'family', 'name', 'path', 'version', 'majorVersion', 'displayName', 'info', 'isChosen', 'custom', 'warning', + 'family', 'name', 'path', 'version', 'majorVersion', 'displayName', 'info', 'isChosen', 'custom', 'warning', 'channel', ]) expect(browserArg.path).to.include('/') @@ -412,6 +417,34 @@ describe('Project Nav', function () { localStorage.setItem('chosenBrowser', 'Custom') cy.get('.browsers-list .dropdown-chosen') .should('contain', 'Custom') + + cy.wrap(localStorage.getItem('chosenBrowser')).should('equal', 'Custom') + }) + }) + + describe('browser with info', function () { + beforeEach(function () { + this.info = 'foo info bar [baz](http://example.com/)' + this.config.browsers = [{ + name: 'electron', + family: 'electron', + displayName: 'Electron', + version: '50.0.2661.86', + path: '', + majorVersion: '50', + info: this.info, + }] + + this.openProject.resolve(this.config) + }) + + it('shows info icon with linkified tooltip', function () { + cy.get('.browsers .fa-info-circle').trigger('mouseover') + + cy.get('.cy-tooltip').should('contain', 'foo info bar baz') + cy.get('.cy-tooltip a').should('have.text', 'baz').click().then(function () { + expect(this.ipc.externalOpen).to.be.calledWith('http://example.com/') + }) }) }) }) diff --git a/packages/desktop-gui/cypress/integration/specs_list_spec.js b/packages/desktop-gui/cypress/integration/specs_list_spec.js index 93f3dfca99e4..24a4c1e9e7cb 100644 --- a/packages/desktop-gui/cypress/integration/specs_list_spec.js +++ b/packages/desktop-gui/cypress/integration/specs_list_spec.js @@ -161,16 +161,16 @@ describe('Specs List', function () { context('run all specs', function () { it('displays run all specs button', () => { - cy.contains('.btn', 'Run all specs') + cy.contains('.all-tests', 'Run all specs') }) it('has play icon', () => { - cy.contains('.btn', 'Run all specs') + cy.contains('.all-tests', 'Run all specs') .find('i').should('have.class', 'fa-play') }) it('triggers browser launch on click of button', () => { - cy.contains('.btn', 'Run all specs').click() + cy.contains('.all-tests', 'Run all specs').click() .then(function () { const launchArgs = this.ipc.launchBrowser.lastCall.args @@ -182,7 +182,7 @@ describe('Specs List', function () { describe('all specs running in browser', function () { beforeEach(() => { - cy.contains('.btn', 'Run all specs').as('allSpecs').click() + cy.contains('.all-tests', 'Run all specs').as('allSpecs').click() }) it('updates spec icon', function () { diff --git a/packages/desktop-gui/src/app/nav.scss b/packages/desktop-gui/src/app/nav.scss index d2bdcfba85d4..04affb922055 100644 --- a/packages/desktop-gui/src/app/nav.scss +++ b/packages/desktop-gui/src/app/nav.scss @@ -182,6 +182,10 @@ } } + .fa-check-circle, .fa-sync-alt { + margin-right: 3px; + } + .browser-icon { color: #4573d9; margin-right: 3px; @@ -213,6 +217,14 @@ margin-right: 4px; } +.browser-beta { + font-size: 12px; + top: -5px; + position: relative; + margin-left: 4px; + color: #d87b0b; +} + .browser-info-tooltip { background: #ececec; border-color: #c7c7c7; @@ -230,6 +242,12 @@ .close-browser { .btn { - margin-top: 7px; + margin-top: 5px; + line-height: 28px; + padding: 0 9px 0 7px; + font-size: 13px; + i { + margin-right: 3px; + } } } diff --git a/packages/desktop-gui/src/lib/browser-model.js b/packages/desktop-gui/src/lib/browser-model.js index 44c8c667bc5b..7d61f78b5b12 100644 --- a/packages/desktop-gui/src/lib/browser-model.js +++ b/packages/desktop-gui/src/lib/browser-model.js @@ -4,6 +4,7 @@ export default class Browser { @observable displayName @observable name @observable family + @observable channel @observable version @observable path @observable majorVersion @@ -16,6 +17,7 @@ export default class Browser { this.displayName = browser.displayName this.name = browser.name this.family = browser.family + this.channel = browser.channel this.version = browser.version this.path = browser.path this.majorVersion = browser.majorVersion diff --git a/packages/desktop-gui/src/project-nav/browsers.jsx b/packages/desktop-gui/src/project-nav/browsers.jsx index 64b336f2aac3..1c6e689ea1d6 100644 --- a/packages/desktop-gui/src/project-nav/browsers.jsx +++ b/packages/desktop-gui/src/project-nav/browsers.jsx @@ -60,10 +60,10 @@ export default class Browsers extends Component { let prefixText if (project.browserState === 'opening') { - icon = + icon = prefixText = 'Opening' } else if (project.browserState === 'opened') { - icon = + icon = prefixText = 'Running' } else { icon = @@ -76,13 +76,7 @@ export default class Browsers extends Component { {prefixText}{' '} {browser.displayName}{' '} {browser.majorVersion} - {browser.family === 'firefox' && - beta} + {browser.family === 'firefox' && beta} {this._info(browser)} {this._warn(browser)} diff --git a/packages/desktop-gui/src/specs/specs-list.jsx b/packages/desktop-gui/src/specs/specs-list.jsx index 927092402140..98a654708618 100644 --- a/packages/desktop-gui/src/specs/specs-list.jsx +++ b/packages/desktop-gui/src/specs/specs-list.jsx @@ -46,7 +46,7 @@ class SpecsList extends Component {
- + {' '} {allSpecsSpec.displayName} diff --git a/packages/desktop-gui/src/styles/components/_general.scss b/packages/desktop-gui/src/styles/components/_general.scss index c15080368c9d..8e4c7d8d5f50 100644 --- a/packages/desktop-gui/src/styles/components/_general.scss +++ b/packages/desktop-gui/src/styles/components/_general.scss @@ -70,19 +70,19 @@ outline: 0 !important; } -.fa.green { +i.green { color: #028863; } -.fa.blue { +i.blue { color: #3454c1; } -.fa.red { +i.red { color: $red-primary; } -.fa.orange { +i.orange { color: #F5A327; } diff --git a/packages/driver/package.json b/packages/driver/package.json index 3d87332d1f69..a17d05caf495 100644 --- a/packages/driver/package.json +++ b/packages/driver/package.json @@ -64,7 +64,7 @@ "url-parse": "1.4.7", "vanilla-text-mask": "5.1.1", "wait-on": "3.3.0", - "webpack": "4.41.0", + "webpack": "4.41.2", "zone.js": "0.9.0" }, "files": [ diff --git a/packages/driver/src/cy/chai.js b/packages/driver/src/cy/chai.js index e5bcdfbd36c8..607d0690097d 100644 --- a/packages/driver/src/cy/chai.js +++ b/packages/driver/src/cy/chai.js @@ -1,3 +1,4 @@ +/* eslint-disable prefer-rest-params */ // tests in driver/test/cypress/integration/commands/assertions_spec.coffee const _ = require('lodash') @@ -8,6 +9,7 @@ const sinonChai = require('@cypress/sinon-chai') const $dom = require('../dom') const $utils = require('../cypress/utils') const $chaiJquery = require('../cypress/chai_jquery') +const chaiInspect = require('./chai/inspect') // all words between single quotes const allPropertyWordsBetweenSingleQuotes = /('.*?')/g @@ -35,6 +37,12 @@ let chaiUtils = null chai.use(sinonChai) +const getType = function (val) { + const match = /\[object (.*)\]/.exec(Object.prototype.toString.call(val)) + + return match && match[1] +} + chai.use((chai, u) => { chaiUtils = u @@ -76,12 +84,26 @@ chai.use((chai, u) => { matchProto = chai.Assertion.prototype.match lengthProto = chai.Assertion.prototype.__methods.length.method containProto = chai.Assertion.prototype.__methods.contain.method - existProto = Object.getOwnPropertyDescriptor(chai.Assertion.prototype, 'exist').get; - ({ getMessage } = chaiUtils) + existProto = Object.getOwnPropertyDescriptor(chai.Assertion.prototype, 'exist').get + const { objDisplay } = chai.util; + + ({ getMessage } = chai.util) + const _inspect = chai.util.inspect + + const { inspect, setFormatValueHook } = chaiInspect.create(chai) + + // prevent tunneling into Window objects (can throw cross-origin errors in firefox) + setFormatValueHook((ctx, val) => { + if (val && (getType(val) === 'Window')) { + return '[window]' + } + }) // remove any single quotes between our **, // except escaped quotes, empty strings and number strings. const removeOrKeepSingleQuotesBetweenStars = (message) => { + // remove any single quotes between our **, preserving escaped quotes + // and if an empty string, put the quotes back return message.replace(allBetweenFourStars, (match) => { if (valueHasLeadingOrTrailingWhitespaces.test(match)) { // Above we used \s+, but below we use \s*. @@ -123,12 +145,14 @@ chai.use((chai, u) => { } return memo - }, []) + } + , []) } const restoreAsserts = function () { - chaiUtils.getMessage = getMessage - + chai.util.inspect = _inspect + chai.util.getMessage = getMessage + chai.util.objDisplay = objDisplay chai.Assertion.prototype.assert = assertProto chai.Assertion.prototype.match = matchProto chai.Assertion.prototype.__methods.length.method = lengthProto @@ -137,9 +161,72 @@ chai.use((chai, u) => { return Object.defineProperty(chai.Assertion.prototype, 'exist', { get: existProto }) } + const overrideChaiInspect = () => { + return chai.util.inspect = inspect + } + + const overrideChaiObjDisplay = () => { + return chai.util.objDisplay = function (obj) { + const str = chai.util.inspect(obj) + const type = Object.prototype.toString.call(obj) + + if (chai.config.truncateThreshold && (str.length >= chai.config.truncateThreshold)) { + if (type === '[object Function]') { + if (!obj.name || (obj.name === '')) { + return '[Function]' + } + + return `[Function: ${obj.name}]` + } + + if (type === '[object Array]') { + return `[ Array(${obj.length}) ]` + } + + if (type === '[object Object]') { + const keys = Object.keys(obj) + const kstr = keys.length > 2 ? `${keys.splice(0, 2).join(', ')}, ...` : keys.join(', ') + + return `{ Object (${kstr}) }` + } + + return str + } + + return str + } + } + const overrideChaiAsserts = function (assertFn) { chai.Assertion.prototype.assert = createPatchedAssert(assertFn) + const _origGetmessage = function (obj, args) { + const negate = chaiUtils.flag(obj, 'negate') + const val = chaiUtils.flag(obj, 'object') + const expected = args[3] + const actual = chaiUtils.getActual(obj, args) + let msg = (negate ? args[2] : args[1]) + const flagMsg = chaiUtils.flag(obj, 'message') + + if (typeof msg === 'function') { + msg = msg() + } + + msg = msg || '' + msg = msg + .replace(/#\{this\}/g, () => { + return chaiUtils.objDisplay(val) + }) + .replace(/#\{act\}/g, () => { + return chaiUtils.objDisplay(actual) + }) + .replace(/#\{exp\}/g, () => { + return chaiUtils.objDisplay(expected) + }) + + return (flagMsg ? `${flagMsg}: ${msg}` : msg) + } + chaiUtils.getMessage = function (assert, args) { const obj = assert._obj @@ -149,7 +236,7 @@ chai.use((chai, u) => { assert._obj = $dom.stringify(obj, 'short') } - const msg = getMessage.call(this, assert, args) + const msg = _origGetmessage.call(this, assert, args) // restore the real obj if we changed it if (obj !== assert._obj) { @@ -160,9 +247,9 @@ chai.use((chai, u) => { } chai.Assertion.overwriteMethod('match', (_super) => { - return (function (regExp, ...args) { + return (function (regExp) { if (_.isRegExp(regExp) || $dom.isDom(this._obj)) { - return _super.apply(this, [regExp, ...args]) + return _super.apply(this, arguments) } const err = $utils.cypressErr($utils.errMessageByPath('chai.match_invalid_argument', { regExp })) @@ -173,11 +260,11 @@ chai.use((chai, u) => { }) const containFn1 = (_super) => { - return (function (text, ...args) { + return (function (text) { let obj = this._obj if (!($dom.isJquery(obj) || $dom.isElement(obj))) { - return _super.apply(this, [text, ...args]) + return _super.apply(this, arguments) } const escText = $utils.escapeQuotes(text) @@ -200,8 +287,8 @@ chai.use((chai, u) => { } const containFn2 = (_super) => { - return (function (...args) { - _super.apply(this, args) + return (function () { + return _super.apply(this, arguments) }) } @@ -209,11 +296,11 @@ chai.use((chai, u) => { chai.Assertion.overwriteChainableMethod('length', (_super) => { - return (function (length, ...args) { + return (function (length) { let obj = this._obj if (!($dom.isJquery(obj) || $dom.isElement(obj))) { - return _super.apply(this, [length, ...args]) + return _super.apply(this, arguments) } length = $utils.normalizeNumber(length) @@ -263,19 +350,20 @@ chai.use((chai, u) => { } }) }, + (_super) => { - return (function (...args) { - return _super.apply(this, args) + return (function () { + return _super.apply(this, arguments) }) }) return chai.Assertion.overwriteProperty('exist', (_super) => { - return (function (...args) { + return (function () { const obj = this._obj if (!($dom.isJquery(obj) || $dom.isElement(obj))) { try { - return _super.apply(this, args) + return _super.apply(this, arguments) } catch (e) { e.type = 'existence' throw e @@ -347,12 +435,12 @@ chai.use((chai, u) => { }) } - // only override assertions for this specific - // expect function instance so we do not affect - // the outside world const overrideExpect = () => { - // make the assertion + // only override assertions for this specific + // expect function instance so we do not affect + // the outside world return (val, message) => { + // make the assertion return new chai.Assertion(val, message) } } @@ -365,8 +453,8 @@ chai.use((chai, u) => { const fns = _.functions(chai.assert) _.each(fns, (name) => { - return fn[name] = function (...args) { - return chai.assert[name].apply(this, args) + return fn[name] = function () { + return chai.assert[name].apply(this, arguments) } }) @@ -392,7 +480,8 @@ chai.use((chai, u) => { // restoreOverrides() restoreAsserts() - // overrideChai() + overrideChaiInspect() + overrideChaiObjDisplay() overrideChaiAsserts(assertFn) return setSpecWindowGlobals(specWindow) diff --git a/packages/driver/src/cy/chai/inspect.js b/packages/driver/src/cy/chai/inspect.js new file mode 100644 index 000000000000..73fbf0aff76f --- /dev/null +++ b/packages/driver/src/cy/chai/inspect.js @@ -0,0 +1,431 @@ +// Changes made: added 'formatValueHook' to process value before being formatted. +// For example the hook can be used to turn `window` objects into the string '[window]' +// to avoid deep recursion. + +// This is (almost) directly from chai/lib/util (which is based on nodejs utils) +// https://github.com/joyent/node/blob/f8c335d0caf47f16d31413f89aa28eda3878e3aa/lib/util.js + +// let getName = require('get-func-name') +// let getProperties = require('./getProperties') +let getEnumerableProperties = require('chai/lib/chai/utils/getEnumerableProperties') +// let config = require('../config') + +module.exports = { + + create (chai) { + const { getName, getProperties } = chai.util + const { config } = chai + + /** + * ### .inspect(obj, [showHidden], [depth], [colors]) + * + * Echoes the value of a value. Tries to print the value out + * in the best way possible given the different types. + * + * @param {Object} obj The object to print out. + * @param {Boolean} showHidden Flag that shows hidden (not enumerable) + * properties of objects. Default is false. + * @param {Number} depth Depth in which to descend in object. Default is 2. + * @param {Boolean} colors Flag to turn on ANSI escape codes to color the + * output. Default is false (no coloring). + * @namespace Utils + * @name inspect + */ + function inspect (obj, showHidden, depth, colors) { + let ctx = { + showHidden, + seen: [], + stylize (str) { + return str + }, + } + + return formatValue(ctx, obj, (typeof depth === 'undefined' ? 2 : depth)) + } + + // Returns true if object is a DOM element. + let isDOMElement = function (object) { + if (typeof HTMLElement === 'object') { + return object instanceof HTMLElement + } + + return object && + typeof object === 'object' && + 'nodeType' in object && + object.nodeType === 1 && + typeof object.nodeName === 'string' + } + + let formatValueHook + + const setFormatValueHook = (fn) => formatValueHook = fn + + function formatValue (ctx, value, recurseTimes) { + // Provide a hook for user-specified inspect functions. + // Check that value is an object with an inspect function on it + + const hookRet = formatValueHook && formatValueHook(ctx, value) + + if (hookRet) { + return hookRet + } + + if (value && typeof value.inspect === 'function' && + // Filter out the util module, it's inspect function is special + value.inspect !== exports.inspect && + // Also filter out any prototype objects using the circular check. + !(value.constructor && value.constructor.prototype === value)) { + let ret = value.inspect(recurseTimes, ctx) + + if (typeof ret !== 'string') { + ret = formatValue(ctx, ret, recurseTimes) + } + + return ret + } + + // Primitive types cannot have properties + let primitive = formatPrimitive(ctx, value) + + if (primitive) { + return primitive + } + + // If this is a DOM element, try to get the outer HTML. + if (isDOMElement(value)) { + if ('outerHTML' in value) { + return value.outerHTML + // This value does not have an outerHTML attribute, + // it could still be an XML element + } + + // Attempt to serialize it + try { + if (document.xmlVersion) { + let xmlSerializer = new XMLSerializer() + + return xmlSerializer.serializeToString(value) + } + + // Firefox 11- do not support outerHTML + // It does, however, support innerHTML + // Use the following to render the element + let ns = 'http://www.w3.org/1999/xhtml' + let container = document.createElementNS(ns, '_') + + container.appendChild(value.cloneNode(false)) + let html = container.innerHTML + .replace('><', `>${value.innerHTML}<`) + + container.innerHTML = '' + + return html + } catch (err) { + // This could be a non-native DOM implementation, + // continue with the normal flow: + // printing the element as if it is an object. + } + } + + // Look up the keys of the object. + let visibleKeys = getEnumerableProperties(value) + let keys = ctx.showHidden ? getProperties(value) : visibleKeys + + let name; let nameSuffix + + // Some type of object without properties can be shortcut. + // In IE, errors have a single `stack` property, or if they are vanilla `Error`, + // a `stack` plus `description` property; ignore those for consistency. + if (keys.length === 0 || (isError(value) && ( + (keys.length === 1 && keys[0] === 'stack') || + (keys.length === 2 && keys[0] === 'description' && keys[1] === 'stack') + ))) { + if (typeof value === 'function') { + name = getName(value) + nameSuffix = name ? `: ${name}` : '' + + return ctx.stylize(`[Function${nameSuffix}]`, 'special') + } + + if (isRegExp(value)) { + return ctx.stylize(RegExp.prototype.toString.call(value), 'regexp') + } + + if (isDate(value)) { + return ctx.stylize(Date.prototype.toUTCString.call(value), 'date') + } + + if (isError(value)) { + return formatError(value) + } + } + + let base = '' + let array = false + let typedArray = false + let braces = ['{', '}'] + + if (isTypedArray(value)) { + typedArray = true + braces = ['[', ']'] + } + + // Make Array say that they are Array + if (isArray(value)) { + array = true + braces = ['[', ']'] + } + + // Make functions say that they are functions + if (typeof value === 'function') { + name = getName(value) + nameSuffix = name ? `: ${name}` : '' + base = ` [Function${nameSuffix}]` + } + + // Make RegExps say that they are RegExps + if (isRegExp(value)) { + base = ` ${RegExp.prototype.toString.call(value)}` + } + + // Make dates with properties first say the date + if (isDate(value)) { + base = ` ${Date.prototype.toUTCString.call(value)}` + } + + // Make error with message first say the error + if (isError(value)) { + return formatError(value) + } + + // eslint-disable-next-line eqeqeq + if (keys.length === 0 && (!array || value.length == 0)) { + return braces[0] + base + braces[1] + } + + if (recurseTimes < 0) { + if (isRegExp(value)) { + return ctx.stylize(RegExp.prototype.toString.call(value), 'regexp') + } + + return ctx.stylize('[Object]', 'special') + } + + ctx.seen.push(value) + + let output + + if (array) { + output = formatArray(ctx, value, recurseTimes, visibleKeys, keys) + } else if (typedArray) { + return formatTypedArray(value) + } else { + output = keys.map(function (key) { + return formatProperty(ctx, value, recurseTimes, visibleKeys, key, array) + }) + } + + ctx.seen.pop() + + return reduceToSingleString(output, base, braces) + } + + function formatPrimitive (ctx, value) { + switch (typeof value) { + case 'undefined': + return ctx.stylize('undefined', 'undefined') + + case 'string': { + const simple = `'${JSON.stringify(value).replace(/^"|"$/g, '') + .replace(/'/g, '\\\'') + .replace(/\\"/g, '"')}'` + + return ctx.stylize(simple, 'string') + } + + case 'number': + if (value === 0 && (1 / value) === -Infinity) { + return ctx.stylize('-0', 'number') + } + + return ctx.stylize(`${value}`, 'number') + + case 'boolean': + return ctx.stylize(`${value}`, 'boolean') + + case 'symbol': + return ctx.stylize(value.toString(), 'symbol') + default: + null + } + // For some reason typeof null is "object", so special case here. + if (value === null) { + return ctx.stylize('null', 'null') + } + } + + function formatError (value) { + return `[${Error.prototype.toString.call(value)}]` + } + + function formatArray (ctx, value, recurseTimes, visibleKeys, keys) { + let output = [] + + for (let i = 0, l = value.length; i < l; ++i) { + if (Object.prototype.hasOwnProperty.call(value, String(i))) { + output.push(formatProperty(ctx, value, recurseTimes, visibleKeys, + String(i), true)) + } else { + output.push('') + } + } + + keys.forEach(function (key) { + if (!key.match(/^\d+$/)) { + output.push(formatProperty(ctx, value, recurseTimes, visibleKeys, + key, true)) + } + }) + + return output + } + + function formatTypedArray (value) { + let str = '[ ' + + for (let i = 0; i < value.length; ++i) { + if (str.length >= config.truncateThreshold - 7) { + str += '...' + break + } + + str += `${value[i]}, ` + } + str += ' ]' + + // Removing trailing `, ` if the array was not truncated + if (str.indexOf(', ]') !== -1) { + str = str.replace(', ]', ' ]') + } + + return str + } + + function formatProperty (ctx, value, recurseTimes, visibleKeys, key, array) { + let name + let propDescriptor = Object.getOwnPropertyDescriptor(value, key) + let str + + if (propDescriptor) { + if (propDescriptor.get) { + if (propDescriptor.set) { + str = ctx.stylize('[Getter/Setter]', 'special') + } else { + str = ctx.stylize('[Getter]', 'special') + } + } else { + if (propDescriptor.set) { + str = ctx.stylize('[Setter]', 'special') + } + } + } + + if (visibleKeys.indexOf(key) < 0) { + name = `[${key}]` + } + + if (!str) { + if (ctx.seen.indexOf(value[key]) < 0) { + if (recurseTimes === null) { + str = formatValue(ctx, value[key], null) + } else { + str = formatValue(ctx, value[key], recurseTimes - 1) + } + + if (str.indexOf('\n') > -1) { + if (array) { + str = str.split('\n').map(function (line) { + return ` ${line}` + }).join('\n').substr(2) + } else { + str = `\n${str.split('\n').map(function (line) { + return ` ${line}` + }).join('\n')}` + } + } + } else { + str = ctx.stylize('[Circular]', 'special') + } + } + + if (typeof name === 'undefined') { + if (array && key.match(/^\d+$/)) { + return str + } + + name = JSON.stringify(`${key}`) + if (name.match(/^"([a-zA-Z_][a-zA-Z_0-9]*)"$/)) { + name = name.substr(1, name.length - 2) + name = ctx.stylize(name, 'name') + } else { + name = name.replace(/'/g, '\\\'') + .replace(/\\"/g, '"') + .replace(/(^"|"$)/g, '\'') + + name = ctx.stylize(name, 'string') + } + } + + return `${name}: ${str}` + } + + function reduceToSingleString (output, base, braces) { + let length = output.reduce(function (prev, cur) { + return prev + cur.length + 1 + }, 0) + + if (length > 60) { + return `${braces[0] + + (base === '' ? '' : `${base}\n `) + } ${ + output.join(',\n ') + } ${ + braces[1]}` + } + + return `${braces[0] + base} ${output.join(', ')} ${braces[1]}` + } + + function isTypedArray (ar) { + // Unfortunately there's no way to check if an object is a TypedArray + // We have to check if it's one of these types + return (typeof ar === 'object' && /\w+Array]$/.test(objectToString(ar))) + } + + function isArray (ar) { + return Array.isArray(ar) || + (typeof ar === 'object' && objectToString(ar) === '[object Array]') + } + + function isRegExp (re) { + return typeof re === 'object' && objectToString(re) === '[object RegExp]' + } + + function isDate (d) { + return typeof d === 'object' && objectToString(d) === '[object Date]' + } + + function isError (e) { + return typeof e === 'object' && objectToString(e) === '[object Error]' + } + + function objectToString (o) { + return Object.prototype.toString.call(o) + } + + return { + inspect, + setFormatValueHook, + } + }, +} diff --git a/packages/driver/src/cy/commands/cookies.js b/packages/driver/src/cy/commands/cookies.js index 615527225511..ae2f04946f51 100644 --- a/packages/driver/src/cy/commands/cookies.js +++ b/packages/driver/src/cy/commands/cookies.js @@ -80,6 +80,10 @@ module.exports = function (Commands, Cypress, cy, state, config) { const handleBackendError = (command, action, onFail) => { return (err) => { + if (!_.includes(err.stack, err.message)) { + err.stack = `${err.message}\n${err.stack}` + } + if (err.name === 'CypressError') { throw err } diff --git a/packages/driver/src/cy/commands/navigation.coffee b/packages/driver/src/cy/commands/navigation.coffee index cec1898e2f56..1bb587a038f1 100644 --- a/packages/driver/src/cy/commands/navigation.coffee +++ b/packages/driver/src/cy/commands/navigation.coffee @@ -8,6 +8,8 @@ $utils = require("../../cypress/utils") $Log = require("../../cypress/log") $Location = require("../../cypress/location") +debug = require('debug')('cypress:driver:navigation') + id = null previousDomainVisited = null hasVisitedAboutBlank = null @@ -42,6 +44,7 @@ isValidVisitMethod = (method) -> _.includes(VALID_VISIT_METHODS, method) timedOutWaitingForPageLoad = (ms, log) -> + debug('timedOutWaitingForPageLoad') $utils.throwErrByPath("navigation.timed_out", { args: { configFile: Cypress.config("configFile") @@ -91,6 +94,7 @@ aboutBlank = (win) -> navigationChanged = (Cypress, cy, state, source, arg) -> ## get the current url of our remote application url = cy.getRemoteLocation("href") + debug('navigation changed:', url) ## dont trigger for empty url's or about:blank return if _.isEmpty(url) or url is "about:blank" @@ -164,6 +168,7 @@ pageLoading = (bool, state) -> Cypress.action("app:page:loading", bool) stabilityChanged = (Cypress, state, config, stable, event) -> + debug('stabilityChanged:', stable) if currentlyVisitingAboutBlank if stable is false ## if we're currently visiting about blank @@ -235,6 +240,7 @@ stabilityChanged = (Cypress, state, config, stable, event) -> state("onPageLoadErr", onPageLoadErr) loading = -> + debug('waiting for window:load') new Promise (resolve, reject) -> cy.once "window:load", -> cy.state("onPageLoadErr", null) @@ -793,6 +799,7 @@ module.exports = (Commands, Cypress, cy, state, config) -> error: err stack: err.stack } + noStackTrace: true }) visit = -> diff --git a/packages/driver/src/cy/commands/request.coffee b/packages/driver/src/cy/commands/request.coffee index 11bdddfe4335..2181ae0a8043 100644 --- a/packages/driver/src/cy/commands/request.coffee +++ b/packages/driver/src/cy/commands/request.coffee @@ -269,6 +269,7 @@ module.exports = (Commands, Cypress, cy, state, config) -> stack: err.stack method: requestOpts.method url: requestOpts.url - } + }, + noStackTrace: true }) }) diff --git a/packages/driver/src/cy/commands/task.coffee b/packages/driver/src/cy/commands/task.coffee index 88fad566a035..6549fcc0dd59 100644 --- a/packages/driver/src/cy/commands/task.coffee +++ b/packages/driver/src/cy/commands/task.coffee @@ -63,6 +63,8 @@ module.exports = (Commands, Cypress, cy, state, config) -> ## re-throw if timedOut error from above throw error if error.name is "CypressError" + $utils.normalizeErrorStack(error) + if error?.isKnownError $utils.throwErrByPath("task.known_error", { onFail: options._log diff --git a/packages/driver/src/cy/errors.coffee b/packages/driver/src/cy/errors.coffee index beb2d1bcbd21..77fd16effc9f 100644 --- a/packages/driver/src/cy/errors.coffee +++ b/packages/driver/src/cy/errors.coffee @@ -55,6 +55,9 @@ create = (state, config, log) -> if l = current and current.getLastLog() l.error(err) + ## normalize error message for firefox + $utils.normalizeErrorStack(err) + return err commandRunningFailed = (err) -> diff --git a/packages/driver/src/cy/keyboard.ts b/packages/driver/src/cy/keyboard.ts index 658633b1efbf..ba8338adea8a 100644 --- a/packages/driver/src/cy/keyboard.ts +++ b/packages/driver/src/cy/keyboard.ts @@ -37,7 +37,7 @@ interface KeyDetailsPartial extends Partial { type SimulatedDefault = ( el: HTMLElement, key: KeyDetails, - options: any + options: typeOptions ) => void interface KeyDetails { @@ -236,7 +236,7 @@ const shouldIgnoreEvent = < return options[eventName] === false } -const shouldUpdateValue = (el: HTMLElement, key: KeyDetails, options) => { +const shouldUpdateValue = (el: HTMLElement, key: KeyDetails, options: typeOptions) => { if (!key.text) return false const bounds = $selection.getSelectionBounds(el) @@ -250,7 +250,7 @@ const shouldUpdateValue = (el: HTMLElement, key: KeyDetails, options) => { const isNumberInputType = $elements.isInput(el) && $elements.isInputType(el, 'number') if (isNumberInputType) { - const needsValue = options.prevVal || '' + const needsValue = options.prevValue || '' const needsValueLength = (needsValue && needsValue.length) || 0 const curVal = $elements.getNativeProp(el, 'value') const bounds = $selection.getSelectionBounds(el) @@ -269,13 +269,13 @@ const shouldUpdateValue = (el: HTMLElement, key: KeyDetails, options) => { return } - options.prevVal = needsValue + key.text + options.prevValue = needsValue + key.text return } - key.text = (options.prevVal || '') + key.text - options.prevVal = null + key.text = (options.prevValue || '') + key.text + options.prevValue = undefined } if (noneSelected) { @@ -491,7 +491,7 @@ const simulatedDefaultKeyMap: { [key: string]: SimulatedDefault } = { $selection.replaceSelectionContents(el, '\n') } - options.onEnterPressed() + options.onEnterPressed && options.onEnterPressed() }, Delete: (el, key) => { key.events.input = $selection.deleteRightOfCursor(el) @@ -600,6 +600,8 @@ export interface typeOptions { onEnterPressed?: Function onNoMatchingSpecialChars?: Function onBeforeSpecialCharAction?: Function + prevValue?: string + id?: string } export class Keyboard { @@ -776,11 +778,7 @@ export class Keyboard { el: HTMLElement, eventType: KeyEventType, keyDetails: KeyDetails, - opts: { - id: string - onEvent?: (...args) => boolean - onBeforeEvent?: (...args) => boolean - } + opts: typeOptions ) { debug('fireSimulatedEvent', eventType, keyDetails) @@ -883,8 +881,7 @@ export class Keyboard { let event: Event debug('event options:', eventType, eventOptions) - - if (eventConstructor === 'TextEvent') { + if (eventConstructor === 'TextEvent' && win[eventConstructor]) { event = document.createEvent('TextEvent') // @ts-ignore event.initTextEvent( @@ -965,7 +962,7 @@ export class Keyboard { return true } - simulatedKeydown (el: HTMLElement, _key: KeyDetails, options: any) { + simulatedKeydown (el: HTMLElement, _key: KeyDetails, options: typeOptions) { if (isModifier(_key)) { const didFlag = this.flagModifier(_key) @@ -1050,7 +1047,7 @@ export class Keyboard { this.simulatedKeyup(elToKeyup, key, options) } - simulatedKeyup (el: HTMLElement, _key: KeyDetails, options: any) { + simulatedKeyup (el: HTMLElement, _key: KeyDetails, options: typeOptions) { if (shouldIgnoreEvent('keyup', _key.events)) { debug('simulatedKeyup: ignoring event') delete _key.events.keyup diff --git a/packages/driver/src/cy/mouse.js b/packages/driver/src/cy/mouse.js index 0aac7be11b4d..03ae308c6ec9 100644 --- a/packages/driver/src/cy/mouse.js +++ b/packages/driver/src/cy/mouse.js @@ -39,45 +39,141 @@ const getMouseCoords = (state) => { return state('mouseCoords') } -const shouldFireMouseMoveEvents = (targetEl, lastHoveredEl, fromElViewport, coords) => { - // not the same element, fire mouse move events - if (lastHoveredEl !== targetEl) { - return true +const create = (state, keyboard, focused, Cypress) => { + const isFirefox = Cypress.browser.family === 'firefox' + + const sendPointerEvent = (el, evtOptions, evtName, bubbles = false, cancelable = false) => { + const constructor = el.ownerDocument.defaultView.PointerEvent + + return sendEvent(evtName, el, evtOptions, bubbles, cancelable, constructor) } + const sendMouseEvent = (el, evtOptions, evtName, bubbles = false, cancelable = false) => { + // IE doesn't have event constructors, so you should use document.createEvent('mouseevent') + // https://dom.spec.whatwg.org/#dom-document-createevent + const constructor = el.ownerDocument.defaultView.MouseEvent - const xy = (obj) => { - return _.pick(obj, 'x', 'y') + return sendEvent(evtName, el, evtOptions, bubbles, cancelable, constructor) } - // if we have the same element, but the xy coords are different - // then fire mouse move events... - return !_.isEqual(xy(fromElViewport), xy(coords)) -} + const sendPointerup = (el, evtOptions) => { + if (isFirefox && el.disabled) { + return {} + } + + return sendPointerEvent(el, evtOptions, 'pointerup', true, true) + } + const sendPointerdown = (el, evtOptions) => { + if (isFirefox && el.disabled) { + return {} + } + + return sendPointerEvent(el, evtOptions, 'pointerdown', true, true) + } + const sendPointermove = (el, evtOptions) => { + return sendPointerEvent(el, evtOptions, 'pointermove', true, true) + } + const sendPointerover = (el, evtOptions) => { + return sendPointerEvent(el, evtOptions, 'pointerover', true, true) + } + const sendPointerenter = (el, evtOptions) => { + return sendPointerEvent(el, evtOptions, 'pointerenter', false, false) + } + const sendPointerleave = (el, evtOptions) => { + return sendPointerEvent(el, evtOptions, 'pointerleave', false, false) + } + const sendPointerout = (el, evtOptions) => { + return sendPointerEvent(el, evtOptions, 'pointerout', true, true) + } + + const sendMouseup = (el, evtOptions) => { + if (isFirefox && el.disabled) { + return {} + } -const shouldMoveCursorToEndAfterMousedown = (el) => { - if (!$elements.isElement(el)) { - return false + return sendMouseEvent(el, evtOptions, 'mouseup', true, true) } + const sendMousedown = (el, evtOptions) => { + if (isFirefox && el.disabled) { + return {} + } + + return sendMouseEvent(el, evtOptions, 'mousedown', true, true) + } + const sendMousemove = (el, evtOptions) => { + return sendMouseEvent(el, evtOptions, 'mousemove', true, true) + } + const sendMouseover = (el, evtOptions) => { + return sendMouseEvent(el, evtOptions, 'mouseover', true, true) + } + const sendMouseenter = (el, evtOptions) => { + return sendMouseEvent(el, evtOptions, 'mouseenter', false, false) + } + const sendMouseleave = (el, evtOptions) => { + return sendMouseEvent(el, evtOptions, 'mouseleave', false, false) + } + const sendMouseout = (el, evtOptions) => { + return sendMouseEvent(el, evtOptions, 'mouseout', true, true) + } + const sendClick = (el, evtOptions, opts = {}) => { + // send the click event if firefox and force (needed for force check checkbox) + if (!opts.force && isFirefox && el.disabled) { + return {} + } - if (!($elements.isInput(el) || $elements.isTextarea(el) || $elements.isContentEditable(el))) { - return false + return sendMouseEvent(el, evtOptions, 'click', true, true) } + const sendDblclick = (el, evtOptions) => { + if (isFirefox && el.disabled) { + return {} + } - if (!$elements.isFocused(el)) { - return false + return sendMouseEvent(el, evtOptions, 'dblclick', true, true) } + const sendContextmenu = (el, evtOptions) => { + if (isFirefox && el.disabled) { + return {} + } - if ($elements.isNeedSingleValueChangeInputElement(el)) { - return false + return sendMouseEvent(el, evtOptions, 'contextmenu', true, true) } + const shouldFireMouseMoveEvents = (targetEl, lastHoveredEl, fromElViewport, coords) => { + // not the same element, fire mouse move events + if (lastHoveredEl !== targetEl) { + return true + } - return true -} + const xy = (obj) => { + return _.pick(obj, 'x', 'y') + } + + // if we have the same element, but the xy coords are different + // then fire mouse move events... + return !_.isEqual(xy(fromElViewport), xy(coords)) + } + + const shouldMoveCursorToEndAfterMousedown = (el) => { + if (!$elements.isElement(el)) { + return false + } + + if (!($elements.isInput(el) || $elements.isTextarea(el) || $elements.isContentEditable(el))) { + return false + } + + if (!$elements.isFocused(el)) { + return false + } + + if ($elements.isNeedSingleValueChangeInputElement(el)) { + return false + } + + return true + } -const create = (state, keyboard, focused) => { const mouse = { _getDefaultMouseOptions (x, y, win) { - const _activeModifiers = keyboard.getActiveModifiers(state) + const _activeModifiers = keyboard.getActiveModifiers() const modifiersEventOptions = $Keyboard.toModifiersEventOptions(_activeModifiers) const coordsEventOptions = toCoordsEventOptions(x, y, win) @@ -514,6 +610,8 @@ const create = (state, keyboard, focused) => { mouse.moveToCoords(fromElViewport) } + el = forceEl || el + const win = $dom.getWindowByElement(el) const defaultOptions = mouse._getDefaultMouseOptions(fromElViewport.x, fromElViewport.y, win) @@ -523,7 +621,7 @@ const create = (state, keyboard, focused) => { detail: 1, }, mouseEvtOptionsExtend) - let click = sendClick(el, clickEventOptions) + let click = sendClick(el, clickEventOptions, { force: !!forceEl }) return { click } }, @@ -626,72 +724,6 @@ const sendEvent = (evtName, el, evtOptions, bubbles = false, cancelable = false, } } -const sendPointerEvent = (el, evtOptions, evtName, bubbles = false, cancelable = false) => { - const Constructor = el.ownerDocument.defaultView.PointerEvent - - return sendEvent(evtName, el, evtOptions, bubbles, cancelable, Constructor) -} -const sendMouseEvent = (el, evtOptions, evtName, bubbles = false, cancelable = false) => { - // TODO: IE doesn't have event constructors, so you should use document.createEvent('mouseevent') - // https://dom.spec.whatwg.org/#dom-document-createevent - const Constructor = el.ownerDocument.defaultView.MouseEvent - - return sendEvent(evtName, el, evtOptions, bubbles, cancelable, Constructor) -} - -const sendPointerup = (el, evtOptions) => { - return sendPointerEvent(el, evtOptions, 'pointerup', true, true) -} -const sendPointerdown = (el, evtOptions) => { - return sendPointerEvent(el, evtOptions, 'pointerdown', true, true) -} -const sendPointermove = (el, evtOptions) => { - return sendPointerEvent(el, evtOptions, 'pointermove', true, true) -} -const sendPointerover = (el, evtOptions) => { - return sendPointerEvent(el, evtOptions, 'pointerover', true, true) -} -const sendPointerenter = (el, evtOptions) => { - return sendPointerEvent(el, evtOptions, 'pointerenter', false, false) -} -const sendPointerleave = (el, evtOptions) => { - return sendPointerEvent(el, evtOptions, 'pointerleave', false, false) -} -const sendPointerout = (el, evtOptions) => { - return sendPointerEvent(el, evtOptions, 'pointerout', true, true) -} - -const sendMouseup = (el, evtOptions) => { - return sendMouseEvent(el, evtOptions, 'mouseup', true, true) -} -const sendMousedown = (el, evtOptions) => { - return sendMouseEvent(el, evtOptions, 'mousedown', true, true) -} -const sendMousemove = (el, evtOptions) => { - return sendMouseEvent(el, evtOptions, 'mousemove', true, true) -} -const sendMouseover = (el, evtOptions) => { - return sendMouseEvent(el, evtOptions, 'mouseover', true, true) -} -const sendMouseenter = (el, evtOptions) => { - return sendMouseEvent(el, evtOptions, 'mouseenter', false, false) -} -const sendMouseleave = (el, evtOptions) => { - return sendMouseEvent(el, evtOptions, 'mouseleave', false, false) -} -const sendMouseout = (el, evtOptions) => { - return sendMouseEvent(el, evtOptions, 'mouseout', true, true) -} -const sendClick = (el, evtOptions) => { - return sendMouseEvent(el, evtOptions, 'click', true, true) -} -const sendDblclick = (el, evtOptions) => { - return sendMouseEvent(el, evtOptions, 'dblclick', true, true) -} -const sendContextmenu = (el, evtOptions) => { - return sendMouseEvent(el, evtOptions, 'contextmenu', true, true) -} - const formatReasonNotFired = (reason) => { return `⚠️ not fired (${reason})` } diff --git a/packages/driver/src/cy/snapshots.coffee b/packages/driver/src/cy/snapshots.coffee index 72a5e0e277ee..0337e9a1e998 100644 --- a/packages/driver/src/cy/snapshots.coffee +++ b/packages/driver/src/cy/snapshots.coffee @@ -120,56 +120,62 @@ create = ($$, state) -> ## which arrives here as number 1 ## jQuery v2 allowed to silently try setting 1[HIGHLIGHT_ATTR] doing nothing ## jQuery v3 runs in strict mode and throws an error if you attempt to set a property - isJqueryElement = $dom.isElement($elToHighlight) and $dom.isJquery($elToHighlight) - if isJqueryElement - $elToHighlight.attr(HIGHLIGHT_ATTR, true) - - ## TODO: throw error here if cy is undefined! - - $body = $$("body").clone() - - ## for the head and body, get an array of all CSS, - ## whether it's links or style tags - ## if it's same-origin, it will get the actual styles as a string - ## it it's cross-domain, it will get a reference to the link's href - { headStyleIds, bodyStyleIds } = snapshotsCss.getStyleIds() - - ## replaces iframes with placeholders - replaceIframes($body) - - ## remove tags we don't want in body - $body.find("script,link[rel='stylesheet'],style").remove() - - ## here we need to figure out if we're in a remote manual environment - ## if so we need to stringify the DOM: - ## 1. grab all inputs / textareas / options and set their value on the element - ## 2. convert DOM to string: body.prop("outerHTML") - ## 3. send this string via websocket to our server - ## 4. server rebroadcasts this to our client and its stored as a property - - ## its also possible for us to store the DOM string completely on the server - ## without ever sending it back to the browser (until its requests). - ## we could just store it in memory and wipe it out intelligently. - ## this would also prevent having to store the DOM structure on the client, - ## which would reduce memory, and some CPU operations - - ## now remove it after we clone - if isJqueryElement - $elToHighlight.removeAttr(HIGHLIGHT_ATTR) - - ## preserve attributes on the tag - htmlAttrs = getHtmlAttrs($$("html")[0]) + ## TODO: in firefox sometimes this throws a cross-origin access error + try + isJqueryElement = $dom.isElement($elToHighlight) and $dom.isJquery($elToHighlight) + + if isJqueryElement + $elToHighlight.attr(HIGHLIGHT_ATTR, true) + + ## TODO: throw error here if cy is undefined! + + $body = $$("body").clone() + + ## for the head and body, get an array of all CSS, + ## whether it's links or style tags + ## if it's same-origin, it will get the actual styles as a string + ## it it's cross-domain, it will get a reference to the link's href + { headStyleIds, bodyStyleIds } = snapshotsCss.getStyleIds() + + ## replaces iframes with placeholders + replaceIframes($body) + + ## remove tags we don't want in body + $body.find("script,link[rel='stylesheet'],style").remove() + + ## here we need to figure out if we're in a remote manual environment + ## if so we need to stringify the DOM: + ## 1. grab all inputs / textareas / options and set their value on the element + ## 2. convert DOM to string: body.prop("outerHTML") + ## 3. send this string via websocket to our server + ## 4. server rebroadcasts this to our client and its stored as a property + + ## its also possible for us to store the DOM string completely on the server + ## without ever sending it back to the browser (until its requests). + ## we could just store it in memory and wipe it out intelligently. + ## this would also prevent having to store the DOM structure on the client, + ## which would reduce memory, and some CPU operations + + ## now remove it after we clone + if isJqueryElement + $elToHighlight.removeAttr(HIGHLIGHT_ATTR) + + ## preserve attributes on the tag + htmlAttrs = getHtmlAttrs($$("html")[0]) + + snapshot = { + name + htmlAttrs + body: $body + } - snapshot = { - name - htmlAttrs - body: $body - } + snapshotsMap.set(snapshot, { headStyleIds, bodyStyleIds }) - snapshotsMap.set(snapshot, { headStyleIds, bodyStyleIds }) + return snapshot - return snapshot + catch e + null return { createSnapshot diff --git a/packages/driver/src/cy/video-recorder.ts b/packages/driver/src/cy/video-recorder.ts new file mode 100644 index 000000000000..c85334726f3d --- /dev/null +++ b/packages/driver/src/cy/video-recorder.ts @@ -0,0 +1,37 @@ +export function create (Cypress) { + // Only start recording with getUserMedia API if we're in firefox and video-enabled and run mode. + // TODO: this logic should be cleaned up or gotten from some video-specific config value + if ( + Cypress.isBrowser('firefox') + && Cypress.config('video') + && !Cypress.config('isInteractive') + // navigator.mediaDevices will be undefined if the browser does not support display capture + && window.navigator.mediaDevices + ) { + window.navigator.mediaDevices.getUserMedia({ + audio: false, + video: { + // mediaSource "browser" is supported by a firefox user preference + // @ts-ignore + mediaSource: 'browser', + frameRate: { + exact: 30, + }, + }, + }) + .then((stream) => { + const options = { + mimeType: 'video/webm', + } + + // @ts-ignore + const mediaRecorder = new window.MediaRecorder(stream, options) + + mediaRecorder.start(200) + + mediaRecorder.addEventListener('dataavailable', (e) => { + Cypress.action('recorder:frame', e.data) + }) + }) + } +} diff --git a/packages/driver/src/cypress.js b/packages/driver/src/cypress.js index a3e9ec3f0a00..f3ee5629acff 100644 --- a/packages/driver/src/cypress.js +++ b/packages/driver/src/cypress.js @@ -1,5 +1,6 @@ const _ = require('lodash') const $ = require('jquery') +const chai = require('chai') const blobUtil = require('blob-util') const minimatch = require('minimatch') const moment = require('moment') @@ -15,6 +16,7 @@ const $Commands = require('./cypress/commands') const $Cookies = require('./cypress/cookies') const $Cy = require('./cypress/cy') const $Events = require('./cypress/events') +const $FirefoxForcedGc = require('./util/firefox_forced_gc') const $Keyboard = require('./cy/keyboard') const $SetterGetter = require('./cypress/setter_getter') const $Log = require('./cypress/log') @@ -27,6 +29,7 @@ const $Server = require('./cypress/server') const $Screenshot = require('./cypress/screenshot') const $SelectorPlayground = require('./cypress/selector_playground') const $utils = require('./cypress/utils') +const browserInfo = require('./cypress/browser') const proxies = { runner: 'getStartTime getTestsState getEmissions setNumLogs countByTestState getDisplayPropsForLog getConsolePropsForLogById getSnapshotPropsForLogById getErrorByTestId setStartTime resumeAtTest normalizeAll'.split(' '), @@ -152,9 +155,12 @@ class $Cypress { config = _.omit(config, 'env', 'remote', 'resolved', 'scaffoldedFiles', 'javascripts', 'state') + _.extend(this, browserInfo(config)) + this.state = $SetterGetter.create({}) this.config = $SetterGetter.create(config) this.env = $SetterGetter.create(env) + this.getFirefoxGcInterval = $FirefoxForcedGc.createIntervalGetter(this.config) this.Cookies = $Cookies.create(config.namespace, d) @@ -196,6 +202,8 @@ class $Cypress { this.events.proxyTo(this.cy) + $FirefoxForcedGc.install(this) + return null } @@ -204,6 +212,9 @@ class $Cypress { // other objects communicate intent // and 'action' to Cypress switch (eventName) { + case 'recorder:frame': + return this.emit('recorder:frame', args[0]) + case 'cypress:stop': return this.emit('stop') @@ -310,13 +321,25 @@ class $Cypress { break - case 'runner:fail': + case 'runner:fail': { // mocha runner calculated a failure + + const err = args[0].err + + if (err.actual) { + err.actual = chai.util.inspect(err.actual) + } + + if (err.expected) { + err.expected = chai.util.inspect(err.expected) + } + if (this.config('isTextTerminal')) { return this.emit('mocha', 'fail', ...args) } break + } case 'mocha:runnable:run': return this.runner.onRunnableRun(...args) diff --git a/packages/driver/src/cypress/browser.coffee b/packages/driver/src/cypress/browser.coffee new file mode 100644 index 000000000000..ccdcb7b91105 --- /dev/null +++ b/packages/driver/src/cypress/browser.coffee @@ -0,0 +1,22 @@ +_ = require("lodash") +$utils = require("./utils") + +isBrowser = (config, obj='') -> + if _.isString(obj) + name = obj.toLowerCase() + currentName = config.browser.name.toLowerCase() + + return name == currentName + + if _.isObject(obj) + return _.isMatch(config.browser, obj) + + $utils.throwErrByPath("browser.invalid_arg", { + args: { method: 'isBrowser', obj: $utils.stringify(obj) } + }) + +module.exports = (config) -> + { + browser: config.browser + isBrowser: _.partial(isBrowser, config) + } diff --git a/packages/driver/src/cypress/cy.js b/packages/driver/src/cypress/cy.js index 0e970238fdba..a35352d179c3 100644 --- a/packages/driver/src/cypress/cy.js +++ b/packages/driver/src/cypress/cy.js @@ -26,6 +26,7 @@ const $Stability = require('../cy/stability') const $selection = require('../dom/selection') const $Snapshots = require('../cy/snapshots') const $CommandQueue = require('./command_queue') +const $VideoRecorder = require('../cy/video-recorder') const privateProps = { props: { name: 'state', url: true }, @@ -118,6 +119,7 @@ const create = function (specWindow, Cypress, Cookies, state, config, log) { const queue = $CommandQueue.create() + $VideoRecorder.create(Cypress) const timeouts = $Timeouts.create(state) const stability = $Stability.create(Cypress, state) const retries = $Retries.create(Cypress, state, timeouts.timeout, timeouts.clearTimeout, stability.whenStable, onFinishAssertions) @@ -127,7 +129,7 @@ const create = function (specWindow, Cypress, Cookies, state, config, log) { const location = $Location.create(state) const focused = $Focused.create(state) const keyboard = $Keyboard.create(state) - const mouse = $Mouse.create(state, keyboard, focused) + const mouse = $Mouse.create(state, keyboard, focused, Cypress) const timers = $Timers.create() const { expect } = $Chai.create(specWindow, assertions.assert) @@ -675,6 +677,8 @@ const create = function (specWindow, Cypress, Cookies, state, config, log) { stopped = true + $utils.normalizeErrorStack(err) + // store the error on state now state('error', err) @@ -712,6 +716,7 @@ const create = function (specWindow, Cypress, Cookies, state, config, log) { // collect all of the callbacks for 'fail' rets = Cypress.action('cy:fail', err, state('runnable')) } catch (err2) { + $utils.normalizeErrorStack(err2) // and if any of these throw synchronously immediately error finish(err2) } diff --git a/packages/driver/src/cypress/error_messages.coffee b/packages/driver/src/cypress/error_messages.coffee index 8b2517f20f66..0f3e6c12bcf6 100644 --- a/packages/driver/src/cypress/error_messages.coffee +++ b/packages/driver/src/cypress/error_messages.coffee @@ -79,6 +79,9 @@ module.exports = { timed_out: "#{cmd('blur')} timed out because your browser did not receive any blur events. This is a known bug in Chrome when it is not the currently focused window." wrong_focused_element: "#{cmd('blur')} can only be called on the focused element. Currently the focused element is a: {{node}}" + browser: + invalid_arg: "Cypress.{{method}}() must be passed the name of a browser or an object to filter with. You passed: {{obj}}" + chai: length_invalid_argument: "You must provide a valid number to a length assertion. You passed: '{{length}}'" match_invalid_argument: "'match' requires its argument be a 'RegExp'. You passed: '{{regExp}}'" diff --git a/packages/driver/src/cypress/runner.js b/packages/driver/src/cypress/runner.js index c852ee6fffd3..7a42738b482c 100644 --- a/packages/driver/src/cypress/runner.js +++ b/packages/driver/src/cypress/runner.js @@ -17,7 +17,7 @@ const TEST_AFTER_RUN_EVENT = 'runner:test:after:run' const ERROR_PROPS = 'message type name stack fileName lineNumber columnNumber host uncaught actual expected showDiff isPending'.split(' ') const RUNNABLE_LOGS = 'routes agents commands'.split(' ') -const RUNNABLE_PROPS = 'id title root hookName hookId err state failedFromHookId body speed type duration wallClockStartedAt wallClockDuration timings file'.split(' ') +const RUNNABLE_PROPS = 'id order title root hookName hookId err state failedFromHookId body speed type duration wallClockStartedAt wallClockDuration timings file'.split(' ') // ## initial payload // { @@ -474,8 +474,16 @@ const normalizeAll = (suite, initialTests = {}, setTestsById, setTests, onRunnab } if (setTests) { + let i = 0 + + const testsArr = _.map(tests, (test) => { + test.order = i += 1 + + return test + }) + // same pattern here - setTests(_.values(tests)) + setTests(testsArr) } return normalizedSuite @@ -736,6 +744,8 @@ const _runnerListeners = function (_runner, Cypress, _emissions, getTestById, ge let hookName const isHook = runnable.type === 'hook' + $utils.normalizeErrorStack(err) + if (isHook) { const parentTitle = runnable.parent.title @@ -817,6 +827,9 @@ const create = function (specWindow, mocha, Cypress, cy) { // else do the same thing as mocha here err = $utils.appendErrMsg(err, append()) + // remove this error's stack since it gives no valuable context + err.stack = '' + const throwErr = function () { throw err } diff --git a/packages/driver/src/cypress/utils.coffee b/packages/driver/src/cypress/utils.coffee index 3c1ba00f0239..b9d773f95185 100644 --- a/packages/driver/src/cypress/utils.coffee +++ b/packages/driver/src/cypress/utils.coffee @@ -101,7 +101,18 @@ module.exports = { ## with the new one err.stack = stack.replace(str, err.toString()) + module.exports.normalizeErrorStack(err) + return err + + normalizeErrorStack: (e) -> + ## normalize error message + stack for firefox + errString = e.toString() + errStack = e.stack || '' + + if !errStack.slice(0, errStack.indexOf('\n')).includes(errString.slice(0, errString.indexOf('\n'))) + e.stack = "#{errString}\n#{errStack}" + return e cloneErr: (obj) -> err2 = new Error(obj.message) @@ -117,6 +128,8 @@ module.exports = { throwErr: (err, options = {}) -> if _.isString(err) err = @cypressErr(err) + if options.noStackTrace + err.stack = '' onFail = options.onFail errProps = options.errProps diff --git a/packages/driver/src/dom/.eslintrc.json b/packages/driver/src/dom/.eslintrc.json new file mode 100644 index 000000000000..e27d0693f4d7 --- /dev/null +++ b/packages/driver/src/dom/.eslintrc.json @@ -0,0 +1,5 @@ +{ + "globals": { + "Cypress": true + } +} diff --git a/packages/driver/src/dom/coordinates.js b/packages/driver/src/dom/coordinates.js index 088adf4baec6..cf1d1be030f6 100644 --- a/packages/driver/src/dom/coordinates.js +++ b/packages/driver/src/dom/coordinates.js @@ -75,8 +75,9 @@ const getElementPositioning = ($el) => { // because its useful to any caller const rectCenter = getCenterCoordinates(rect) - const topCenter = rectCenter.y - const leftCenter = rectCenter.x + // rounding needed for firefox, which returns floating numbers + const topCenter = Math.ceil(rectCenter.y) + const leftCenter = Math.ceil(rectCenter.x) return { scrollTop: el.scrollTop, @@ -93,15 +94,15 @@ const getElementPositioning = ($el) => { leftCenter, }, fromElWindow: { - top: rect.top + win.scrollY, + top: Math.ceil(rect.top + win.scrollY), left: rect.left + win.scrollX, - topCenter: topCenter + win.scrollY, + topCenter: Math.ceil(topCenter + win.scrollY), leftCenter: leftCenter + win.scrollX, }, fromAutWindow: { - top: rectFromAut.top + autFrame.scrollY, + top: Math.ceil(rectFromAut.top + autFrame.scrollY), left: rectFromAut.left + autFrame.scrollX, - topCenter: rectFromAutCenter.y + autFrame.scrollY, + topCenter: Math.ceil(rectFromAutCenter.y + autFrame.scrollY), leftCenter: rectFromAutCenter.x + autFrame.scrollX, }, } diff --git a/packages/driver/src/dom/document.ts b/packages/driver/src/dom/document.ts index 083983037402..e8f738a64168 100644 --- a/packages/driver/src/dom/document.ts +++ b/packages/driver/src/dom/document.ts @@ -17,6 +17,12 @@ const isDocument = (obj: HTMLElement | Document): obj is Document => { // does this document have a currently active window (defaultView) const hasActiveWindow = (doc) => { + // in firefox, detached documents still have a reference to their window + // but document.location is null + if (Cypress.isBrowser('firefox') && !doc.location) { + return false + } + return !!doc.defaultView } diff --git a/packages/driver/src/dom/elements.ts b/packages/driver/src/dom/elements.ts index e7eb4324fc39..fc1641b6a4d1 100644 --- a/packages/driver/src/dom/elements.ts +++ b/packages/driver/src/dom/elements.ts @@ -7,6 +7,9 @@ import * as $jquery from './jquery' import * as $selection from './selection' import { parentHasDisplayNone } from './visibility' import * as $window from './window' +import Debug from 'debug' + +const debug = Debug('cypress:driver:elements') const { wrap } = $jquery @@ -331,7 +334,7 @@ const isNeedSingleValueChangeInputElement = (el: HTMLElement): el is HTMLSingleV return false } - return inputTypeNeedSingleValueChangeRe.test(el.type) + return inputTypeNeedSingleValueChangeRe.test((el.getAttribute('type') || '').toLocaleLowerCase()) } const canSetSelectionRangeElement = (el): el is HTMLElementCanSetSelectionRange => { @@ -350,7 +353,7 @@ const getTagName = (el) => { // - with [contenteditable] // - with document.designMode = 'on' const isContentEditable = (el: any): el is HTMLContentEditableElement => { - return getNativeProp(el, 'isContentEditable') + return getNativeProp(el, 'isContentEditable') || $document.getDocumentFromElement(el).designMode === 'on' } const isTextarea = (el): el is HTMLTextAreaElement => { @@ -602,10 +605,10 @@ const isAttached = function ($el) { const doc = $document.getDocumentFromElement(els[0]) // TODO: i guess its possible each element - // is technically bound to a differnet document + // is technically bound to a different document // but c'mon const isIn = (el) => { - return $.contains((doc as unknown) as Element, el) + return $.contains(doc, el) } // make sure the document is currently @@ -730,11 +733,15 @@ const isScrollable = ($el) => { const checkDocumentElement = (win, documentElement) => { // Check if body height is higher than window height if (win.innerHeight < documentElement.scrollHeight) { + debug('isScrollable: window scrollable on Y') + return true } // Check if body width is higher than window width if (win.innerWidth < documentElement.scrollWidth) { + debug('isScrollable: window scrollable on X') + return true } @@ -762,6 +769,8 @@ const isScrollable = ($el) => { if (el.clientHeight < el.scrollHeight) { // and our element has scroll or auto overflow or overflowX if (isScrollOrAuto(overflow) || isScrollOrAuto(overflowY)) { + debug('isScrollable: clientHeight < scrollHeight and scroll/auto overflow') + return true } } @@ -769,6 +778,8 @@ const isScrollable = ($el) => { // x axis if (el.clientWidth < el.scrollWidth) { if (isScrollOrAuto(overflow) || isScrollOrAuto(overflowX)) { + debug('isScrollable: clientWidth < scrollWidth and scroll/auto overflow') + return true } } diff --git a/packages/driver/src/dom/selection.ts b/packages/driver/src/dom/selection.ts index 4f1dd21448d3..37af392575ba 100644 --- a/packages/driver/src/dom/selection.ts +++ b/packages/driver/src/dom/selection.ts @@ -71,7 +71,13 @@ const _replaceSelectionContentsContentEditable = function (el, text) { const doc = $document.getDocumentFromElement(el) // NOTE: insertText will also handle '\n', and render newlines - $elements.callNativeMethod(doc, 'execCommand', 'insertText', true, text) + let nativeUI = true + + if (Cypress.browser.family === 'firefox') { + nativeUI = false + } + + $elements.callNativeMethod(doc, 'execCommand', 'insertText', nativeUI, text) } // Keeping around native implementation @@ -340,15 +346,7 @@ const moveCursorRight = function (el) { } } -const moveCursorUp = (el) => { - return _moveCursorUpOrDown(el, true) -} - -const moveCursorDown = (el) => { - return _moveCursorUpOrDown(el, false) -} - -const _moveCursorUpOrDown = function (el, up) { +const _moveCursorUpOrDown = function (up: boolean, el: HTMLElement) { if ($elements.isInput(el)) { // on an input, instead of moving the cursor // we want to perform the native browser action @@ -370,9 +368,44 @@ const _moveCursorUpOrDown = function (el, up) { const isTextarea = $elements.isTextarea(el) + if (isTextarea && Cypress.browser.family === 'firefox') { + const val = $elements.getNativeProp(el as HTMLTextAreaElement, 'value') + const bounds = _getSelectionBoundsFromTextarea(el as HTMLTextAreaElement) + let toPos + + if (up) { + const partial = val.slice(0, bounds.start) + const lastEOL = partial.lastIndexOf('\n') + const offset = partial.length - lastEOL - 1 + const SOL = partial.slice(0, lastEOL).lastIndexOf('\n') + 1 + const toLineLen = partial.slice(SOL, lastEOL).length + + toPos = SOL + Math.min(toLineLen, offset) + + // const baseLen = arr.slice(0, -2).join().length - 1 + // toPos = baseLen + arr.slice(-1)[0].length + } else { + const partial = val.slice(bounds.end) + const arr = partial.split('\n') + const baseLen = arr.slice(0, 1).join('\n').length + bounds.end + + toPos = baseLen + (bounds.end - val.slice(0, bounds.end).lastIndexOf('\n')) + } + + setSelectionRange(el, toPos, toPos) + + return + } + if (isTextarea || $elements.isContentEditable(el)) { const selection = _getSelectionByEl(el) + if (Cypress.browser.family === 'firefox' && !selection.isCollapsed) { + up ? selection.collapseToStart() : selection.collapseToEnd() + + return + } + return $elements.callNativeMethod(selection, 'modify', 'move', up ? 'backward' : 'forward', @@ -380,15 +413,10 @@ const _moveCursorUpOrDown = function (el, up) { } } -const moveCursorToLineStart = (el) => { - return _moveCursorToLineStartOrEnd(el, true) -} +const moveCursorUp = _.curry(_moveCursorUpOrDown)(true) +const moveCursorDown = _.curry(_moveCursorUpOrDown)(false) -const moveCursorToLineEnd = (el) => { - return _moveCursorToLineStartOrEnd(el, false) -} - -const _moveCursorToLineStartOrEnd = function (el: HTMLElement, toStart) { +const _moveCursorToLineStartOrEnd = function (toStart: boolean, el: HTMLElement) { const isInput = $elements.isInput(el) const isTextarea = $elements.isTextarea(el) const isInputOrTextArea = isInput || isTextarea @@ -396,12 +424,55 @@ const _moveCursorToLineStartOrEnd = function (el: HTMLElement, toStart) { if ($elements.isContentEditable(el) || isInputOrTextArea) { const selection = _getSelectionByEl(el) + if (Cypress.browser.family === 'firefox' && isInputOrTextArea) { + if (isInput) { + let toPos = 0 + + if (!toStart) { + toPos = $elements.getNativeProp(el as HTMLInputElement, 'value').length + } + + setSelectionRange(el, toPos, toPos) + + return + } + // const doc = $document.getDocumentFromElement(el) + // console.log(doc.activeElement) + // $elements.callNativeMethod(doc, 'execCommand', 'selectall', false) + // $elements.callNativeMethod(el, 'select') + // _getSelectionByEl(el).ca + // toStart ? _getSelectionByEl(el).collapseToStart : _getSelectionByEl(el).collapseToEnd() + + if (isTextarea) { + const bounds = _getSelectionBoundsFromTextarea(el) + const value = $elements.getNativeProp(el as HTMLTextAreaElement, 'value') + let toPos: number + + if (toStart) { + toPos = value.slice(0, bounds.start).lastIndexOf('\n') + 1 + } else { + const valSlice = value.slice(bounds.end) + const EOLNewline = valSlice.indexOf('\n') + const EOL = EOLNewline === -1 ? valSlice.length : EOLNewline + + toPos = bounds.end + EOL + } + + setSelectionRange(el, toPos, toPos) + + return + } + } + // the selection.modify API is non-standard, may work differently in other browsers, and is not in IE11. // https://developer.mozilla.org/en-US/docs/Web/API/Selection/modify return $elements.callNativeMethod(selection, 'modify', 'move', toStart ? 'backward' : 'forward', 'lineboundary') } } +const moveCursorToLineStart = _.curry(_moveCursorToLineStartOrEnd)(true) +const moveCursorToLineEnd = _.curry(_moveCursorToLineStartOrEnd)(false) + const isCollapsed = function (el) { if ($elements.isTextarea(el) || $elements.isInput(el)) { const { start, end } = getSelectionBounds(el) diff --git a/packages/driver/src/util/firefox_forced_gc.ts b/packages/driver/src/util/firefox_forced_gc.ts new file mode 100644 index 000000000000..8d1b76186f8d --- /dev/null +++ b/packages/driver/src/util/firefox_forced_gc.ts @@ -0,0 +1,58 @@ +import { get, isNumber } from 'lodash' + +export function createIntervalGetter (config) { + return () => { + if (get(config('browser'), 'family') !== 'firefox') { + return undefined + } + + const intervals = config('firefoxGcInterval') + + if (isNumber(intervals)) { + return intervals + } + + return intervals[config('isInteractive') ? 'openMode' : 'runMode'] + } +} + +export function install (Cypress: Cypress.Cypress & EventEmitter) { + if (!Cypress.isBrowser('firefox')) { + return + } + + let cyVisitedSinceLastGc = false + let testsSinceLastForcedGc = 0 + + Cypress.on('command:start', function (cmd) { + if (cmd.get('name') === 'visit') { + cyVisitedSinceLastGc = true + } + }) + + Cypress.on('test:before:run:async', function (testAttrs) { + const { order } = testAttrs + + testsSinceLastForcedGc++ + + // if this is the first test, or the last test didn't run a cy.visit... + if (order === 0 || !cyVisitedSinceLastGc) { + return + } + + const gcInterval = Cypress.getFirefoxGcInterval() + + cyVisitedSinceLastGc = false + + if (gcInterval && gcInterval > 0 && testsSinceLastForcedGc >= gcInterval) { + testsSinceLastForcedGc = 0 + Cypress.emit('before:firefox:force:gc', { gcInterval }) + + return Cypress.backend('firefox:force:gc').then(() => { + return Cypress.emit('after:firefox:force:gc', { gcInterval }) + }) + } + + return + }) +} diff --git a/packages/driver/test/cypress/fixtures/dom.html b/packages/driver/test/cypress/fixtures/dom.html index fec644b076d9..533cf1a78192 100644 --- a/packages/driver/test/cypress/fixtures/dom.html +++ b/packages/driver/test/cypress/fixtures/dom.html @@ -6,6 +6,46 @@