From a2d2c8d42ca5df3dff51f836f2488b075956def7 Mon Sep 17 00:00:00 2001 From: Chris Breiding Date: Thu, 18 Jun 2020 13:32:57 -0400 Subject: [PATCH 01/12] Fix issue with finding elements in shadow dom under specific conditions (#7746) Co-authored-by: Jessica Sachs --- .../driver/cypress/fixtures/shadow-dom.html | 7 +++++ .../integration/commands/querying_spec.js | 6 ++++ .../integration/commands/traversals_spec.js | 6 ++++ packages/driver/src/cy/commands/navigation.js | 17 +++++++++++ packages/driver/src/cy/commands/querying.js | 28 ++++++++----------- 5 files changed, 48 insertions(+), 16 deletions(-) diff --git a/packages/driver/cypress/fixtures/shadow-dom.html b/packages/driver/cypress/fixtures/shadow-dom.html index ae5e025f3452..672e6537fe14 100644 --- a/packages/driver/cypress/fixtures/shadow-dom.html +++ b/packages/driver/cypress/fixtures/shadow-dom.html @@ -35,6 +35,13 @@ } }) } + + if (window.location.search.includes('wrap-qsa')) { + const realQuerySelectorAll = document.querySelectorAll; + document.querySelectorAll = function (...args) { + return realQuerySelectorAll.apply(document, args); + }; + } diff --git a/packages/driver/cypress/integration/commands/querying_spec.js b/packages/driver/cypress/integration/commands/querying_spec.js index ca35a8144a51..8ed6b39fda61 100644 --- a/packages/driver/cypress/integration/commands/querying_spec.js +++ b/packages/driver/cypress/integration/commands/querying_spec.js @@ -946,6 +946,12 @@ describe('src/cy/commands/querying', () => { cy.get('.in-and-out', { includeShadowDom: true }) .should('have.length', 2) }) + + // https://github.com/cypress-io/cypress/issues/7676 + it('does not error when querySelectorAll is wrapped and snapshots are off', () => { + cy.visit('/fixtures/shadow-dom.html?wrap-qsa=true') + cy.get('.shadow-1', { includeShadowDom: true }) + }) }) describe('.log', () => { diff --git a/packages/driver/cypress/integration/commands/traversals_spec.js b/packages/driver/cypress/integration/commands/traversals_spec.js index 2cf53fa4daf5..48dbdbc4d6e9 100644 --- a/packages/driver/cypress/integration/commands/traversals_spec.js +++ b/packages/driver/cypress/integration/commands/traversals_spec.js @@ -407,6 +407,12 @@ describe('src/cy/commands/traversals', () => { expect($element[0]).to.eq(el) }) }) + + // https://github.com/cypress-io/cypress/issues/7676 + it('does not error when querySelectorAll is wrapped and snapshots are off', () => { + cy.visit('/fixtures/shadow-dom.html?wrap-qsa=true') + cy.get('#shadow-element-1').find('.shadow-1', { includeShadowDom: true }) + }) }) describe('closest', () => { diff --git a/packages/driver/src/cy/commands/navigation.js b/packages/driver/src/cy/commands/navigation.js index f1bd817148c3..7a234465a514 100644 --- a/packages/driver/src/cy/commands/navigation.js +++ b/packages/driver/src/cy/commands/navigation.js @@ -425,6 +425,23 @@ module.exports = (Commands, Cypress, cy, state, config) => { return } + // if a user-loaded script redefines document.querySelectorAll and + // numTestsKeptInMemory is 0 (no snapshotting), jQuery thinks + // that document.querySelectorAll is not available (it tests to see that + // it's the native definition for some reason) and doesn't use it, + // which can fail with a weird error if querying shadow dom. + // this ensures that jQuery determines support for document.querySelectorAll + // before user scripts are executed. + // (when snapshotting is enabled, it can achieve the same thing if an XHR + // causes it to snapshot before the user script is executed, but that's + // not guaranteed to happen.) + // https://github.com/cypress-io/cypress/issues/7676 + // this shouldn't error, but we wrap it to ignore potential errors + // out of an abundance of caution + try { + cy.$$('body', contentWindow.document) + } catch (e) {} // eslint-disable-line no-empty + const options = _.last(current.get('args')) return options?.onBeforeLoad?.call(runnable.ctx, contentWindow) diff --git a/packages/driver/src/cy/commands/querying.js b/packages/driver/src/cy/commands/querying.js index b916a4f656ab..f3bd41e1f2c7 100644 --- a/packages/driver/src/cy/commands/querying.js +++ b/packages/driver/src/cy/commands/querying.js @@ -276,25 +276,20 @@ module.exports = (Commands, Cypress, cy, state) => { } const getElements = () => { - // attempt to query for the elements by withinSubject context - // and catch any sizzle errors! let $el try { - // only support shadow traversal if we're not searching - // within a subject and have been explicitly told to ignore - // boundaries. - if (!options.includeShadowDom) { - $el = cy.$$(selector, options.withinSubject) - } else { + let scope = options.withinSubject + + if (options.includeShadowDom) { const root = options.withinSubject || cy.state('document') const elementsWithShadow = $dom.findAllShadowRoots(root) - elementsWithShadow.push(root) - - $el = cy.$$(selector, elementsWithShadow) + scope = elementsWithShadow.concat(root) } + $el = cy.$$(selector, scope) + // jQuery v3 has removed its deprecated properties like ".selector" // https://jquery.com/upgrade-guide/3.0/breaking-change-deprecated-context-and-selector-properties-removed // but our error messages use this property to actually show the missing element @@ -302,16 +297,17 @@ module.exports = (Commands, Cypress, cy, state) => { if ($el.selector == null) { $el.selector = selector } - } catch (e) { - e.onFail = () => { + } catch (err) { + // this is usually a sizzle error (invalid selector) + err.onFail = () => { if (options.log === false) { - return e + return err } - options._log.error(e) + options._log.error(err) } - throw e + throw err } // if that didnt find anything and we have a within subject From 869bcec55c3680d59ee6c592d44b10a978d4928a Mon Sep 17 00:00:00 2001 From: Jennifer Shehane Date: Fri, 19 Jun 2020 10:35:06 +0630 Subject: [PATCH 02/12] Before XHR URLs are whitelisted, strip query params and hashes (#7742) --- .../cypress/integration/commands/xhr_spec.js | 28 +++++++++++++++++++ packages/driver/src/cypress/server.js | 10 ++++++- 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/packages/driver/cypress/integration/commands/xhr_spec.js b/packages/driver/cypress/integration/commands/xhr_spec.js index b720bebad347..1c7cd99a12ca 100644 --- a/packages/driver/cypress/integration/commands/xhr_spec.js +++ b/packages/driver/cypress/integration/commands/xhr_spec.js @@ -2296,6 +2296,34 @@ describe('src/cy/commands/xhr', () => { expect(resp).to.eq('{ \'bar\' }\n') }) }) + + // https://github.com/cypress-io/cypress/issues/7280 + it('ignores query params when whitelisting routes', () => { + cy.server() + cy.route(/url-with-query-param/, { foo: 'bar' }).as('getQueryParam') + cy.window().then((win) => { + win.$.get('/url-with-query-param?resource=foo.js') + + return null + }) + + cy.wait('@getQueryParam').its('response.body') + .should('deep.equal', { foo: 'bar' }) + }) + + // https://github.com/cypress-io/cypress/issues/7280 + it('ignores hashes when whitelisting routes', () => { + cy.server() + cy.route(/url-with-hash/, { foo: 'bar' }).as('getHash') + cy.window().then((win) => { + win.$.get('/url-with-hash#foo.js') + + return null + }) + + cy.wait('@getHash').its('response.body') + .should('deep.equal', { foo: 'bar' }) + }) }) describe('route setup', () => { diff --git a/packages/driver/src/cypress/server.js b/packages/driver/src/cypress/server.js index cada34fc1359..4610d8f91acf 100644 --- a/packages/driver/src/cypress/server.js +++ b/packages/driver/src/cypress/server.js @@ -66,8 +66,16 @@ const warnOnForce404Default = (obj) => { } const whitelist = (xhr) => { + const url = new URL(xhr.url) + + // https://github.com/cypress-io/cypress/issues/7280 + // we want to strip the xhr's URL of any hash and query params before + // checking the REGEX for matching file extensions + url.search = '' + url.hash = '' + // whitelist if we're GET + looks like we're fetching regular resources - return xhr.method === 'GET' && regularResourcesRe.test(xhr.url) + return xhr.method === 'GET' && regularResourcesRe.test(url.href) } const serverDefaults = { From dc2b50d3516fbcbadac262586730ec1a152dcbd1 Mon Sep 17 00:00:00 2001 From: Zach Panzarino Date: Fri, 19 Jun 2020 16:53:01 -0400 Subject: [PATCH 03/12] Add UTM parameters to Dashboard login buttons (#7639) * Add UTM parameters to Dashboard login buttons * Revert "Add UTM parameters to Dashboard login buttons" This reverts commit 568f12e3cb3c44889b605d13ffd67a7a857307bf. * Add UTM parameters to Dashboard login buttons * utmCode camel case Co-authored-by: Zach Bloomquist * Add desktop-gui integration tests for utm code * Add server unit tests for utm code Co-authored-by: Zach Bloomquist --- .../cypress/integration/login_spec.js | 6 ++++++ .../cypress/integration/runs_list_spec.js | 14 +++++++++++++- packages/desktop-gui/src/auth/auth-api.js | 4 ++-- packages/desktop-gui/src/auth/login-form.jsx | 2 +- packages/desktop-gui/src/auth/login-modal.jsx | 2 +- packages/desktop-gui/src/runs/runs-list.jsx | 2 +- packages/server/lib/gui/auth.js | 18 +++++++++++++++--- packages/server/lib/gui/events.js | 2 +- packages/server/test/unit/gui/auth_spec.js | 8 ++++++++ 9 files changed, 48 insertions(+), 10 deletions(-) diff --git a/packages/desktop-gui/cypress/integration/login_spec.js b/packages/desktop-gui/cypress/integration/login_spec.js index 5a9ec1865502..82ce041e6c4a 100644 --- a/packages/desktop-gui/cypress/integration/login_spec.js +++ b/packages/desktop-gui/cypress/integration/login_spec.js @@ -69,6 +69,12 @@ describe('Login', function () { }) }) + it('passes utm code when it triggers ipc \'begin:auth\'', function () { + cy.then(function () { + expect(this.ipc.beginAuth).to.be.calledWith('Nav Login Button') + }) + }) + it('disables login button', () => { cy.get('@loginBtn').should('be.disabled') }) diff --git a/packages/desktop-gui/cypress/integration/runs_list_spec.js b/packages/desktop-gui/cypress/integration/runs_list_spec.js index d62ee612b685..c47ceeade608 100644 --- a/packages/desktop-gui/cypress/integration/runs_list_spec.js +++ b/packages/desktop-gui/cypress/integration/runs_list_spec.js @@ -303,6 +303,18 @@ describe('Runs List', function () { it('does not fetch runs', function () { expect(this.ipc.getRuns).not.to.be.called }) + + it('clicking Log In to Dashboard opens login', () => { + cy.contains('button', 'Log In to Dashboard').click().then(function () { + expect(this.ipc.beginAuth).to.be.calledOnce + }) + }) + + it('clicking Log In to Dashboard passes utm code', () => { + cy.contains('button', 'Log In to Dashboard').click().then(function () { + expect(this.ipc.beginAuth).to.be.calledWith('Runs Tab Login Button') + }) + }) }) context('without a project id', function () { @@ -345,7 +357,7 @@ describe('Runs List', function () { it('clicking Log In to Dashboard opens login', () => { cy.contains('button', 'Log In to Dashboard').click().then(function () { - expect(this.ipc.beginAuth).to.be.called + expect(this.ipc.beginAuth).to.be.calledOnce }) }) }) diff --git a/packages/desktop-gui/src/auth/auth-api.js b/packages/desktop-gui/src/auth/auth-api.js index 0da2a4fba17a..44e1e1f4be2e 100644 --- a/packages/desktop-gui/src/auth/auth-api.js +++ b/packages/desktop-gui/src/auth/auth-api.js @@ -21,12 +21,12 @@ class AuthApi { }) } - login () { + login (utm) { ipc.onAuthMessage((__, message) => { authStore.setMessage(message) }) - return ipc.beginAuth() + return ipc.beginAuth(utm) .then((user) => { authStore.setUser(user) authStore.setMessage(null) diff --git a/packages/desktop-gui/src/auth/login-form.jsx b/packages/desktop-gui/src/auth/login-form.jsx index d24ec89cb738..4d159f29084e 100644 --- a/packages/desktop-gui/src/auth/login-form.jsx +++ b/packages/desktop-gui/src/auth/login-form.jsx @@ -115,7 +115,7 @@ class LoginForm extends Component { this.setState({ isLoggingIn: true }) - authApi.login() + authApi.login(this.props.utm) .then(() => { this.props.onSuccess() }) diff --git a/packages/desktop-gui/src/auth/login-modal.jsx b/packages/desktop-gui/src/auth/login-modal.jsx index 058fe388cb05..949213dd8639 100644 --- a/packages/desktop-gui/src/auth/login-modal.jsx +++ b/packages/desktop-gui/src/auth/login-modal.jsx @@ -70,7 +70,7 @@ class LoginContent extends Component { x

Log In

Logging in gives you access to the Cypress Dashboard Service. You can set up projects to be recorded and see test data from your project.

- this.setState({ succeeded: true })} /> + this.setState({ succeeded: true })} /> ) } diff --git a/packages/desktop-gui/src/runs/runs-list.jsx b/packages/desktop-gui/src/runs/runs-list.jsx index 2f8a0fb44c87..28633151f917 100644 --- a/packages/desktop-gui/src/runs/runs-list.jsx +++ b/packages/desktop-gui/src/runs/runs-list.jsx @@ -284,7 +284,7 @@ class RunsList extends Component { - + ) } diff --git a/packages/server/lib/gui/auth.js b/packages/server/lib/gui/auth.js index 70aeaaca51e8..c66062b59327 100644 --- a/packages/server/lib/gui/auth.js +++ b/packages/server/lib/gui/auth.js @@ -19,6 +19,7 @@ let authState let openExternalAttempted = false let authRedirectReached = false let server +let utm const _buildLoginRedirectUrl = (server) => { const { port } = server.address() @@ -26,7 +27,7 @@ const _buildLoginRedirectUrl = (server) => { return `http://127.0.0.1:${port}/redirect-to-auth` } -const _buildFullLoginUrl = (baseLoginUrl, server) => { +const _buildFullLoginUrl = (baseLoginUrl, server, utmCode) => { const { port } = server.address() if (!authState) { @@ -45,6 +46,16 @@ const _buildFullLoginUrl = (baseLoginUrl, server) => { platform: os.platform(), } + if (utmCode) { + authUrl.query = { + utm_source: 'Test Runner', + utm_medium: 'Login Button', + utm_campaign: 'TR-Dashboard', + utm_content: utmCode, + ...authUrl.query, + } + } + return authUrl.format() }) } @@ -58,7 +69,7 @@ const _getOriginFromUrl = (originalUrl) => { /** * @returns a promise that is resolved with a user when auth is complete or rejected when it fails */ -const start = (onMessage) => { +const start = (onMessage, utmCode) => { function sendMessage (type, name, arg1) { onMessage({ type, @@ -68,6 +79,7 @@ const start = (onMessage) => { }) } + utm = utmCode authRedirectReached = false return user.getBaseLoginUrl() @@ -110,7 +122,7 @@ const _launchServer = (baseLoginUrl, sendMessage) => { app.get('/redirect-to-auth', (req, res) => { authRedirectReached = true - _buildFullLoginUrl(baseLoginUrl, server) + _buildFullLoginUrl(baseLoginUrl, server, utm) .then((fullLoginUrl) => { debug('Received GET to /redirect-to-auth, redirecting: %o', { fullLoginUrl }) diff --git a/packages/server/lib/gui/events.js b/packages/server/lib/gui/events.js index f58ff0ac9851..e120ead901b9 100644 --- a/packages/server/lib/gui/events.js +++ b/packages/server/lib/gui/events.js @@ -155,7 +155,7 @@ const handleEvent = function (options, bus, event, id, type, arg) { return bus.emit('auth:message', msg) } - return auth.start(onMessage) + return auth.start(onMessage, arg) .then(send) .catch(sendErr) diff --git a/packages/server/test/unit/gui/auth_spec.js b/packages/server/test/unit/gui/auth_spec.js index 50ea9a93dda6..646b13b31b14 100644 --- a/packages/server/test/unit/gui/auth_spec.js +++ b/packages/server/test/unit/gui/auth_spec.js @@ -13,6 +13,7 @@ const RANDOM_STRING = 'a'.repeat(32) const PORT = 9001 const REDIRECT_URL = `http://127.0.0.1:${PORT}/redirect-to-auth` const FULL_LOGIN_URL = `https://foo.invalid/login.html?port=${PORT}&state=${RANDOM_STRING}&machineId=abc123&cypressVersion=${pkg.version}&platform=linux` +const FULL_LOGIN_URL_UTM = `https://foo.invalid/login.html?utm_source=Test%20Runner&utm_medium=Login%20Button&utm_campaign=TR-Dashboard&utm_content=Login%20Button&port=${PORT}&state=${RANDOM_STRING}&machineId=abc123&cypressVersion=${pkg.version}&platform=linux` describe('lib/gui/auth', function () { beforeEach(() => { @@ -66,6 +67,13 @@ describe('lib/gui/auth', function () { expect(random.id).to.be.calledOnce }) }) + + it('uses utm code to form a trackable URL', function () { + return auth._buildFullLoginUrl(BASE_URL, this.server, 'Login Button') + .then((url) => { + expect(url).to.eq(FULL_LOGIN_URL_UTM) + }) + }) }) context('._launchNativeAuth', function () { From 22e47aa5fe3864fcbbd2dfdb27a72f3d72c5b4dd Mon Sep 17 00:00:00 2001 From: Zach Panzarino Date: Mon, 22 Jun 2020 00:33:47 -0400 Subject: [PATCH 04/12] Update license copyright year (#7758) --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index 44e14bb50afd..58fe407efeff 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2019 Cypress.io +Copyright (c) 2020 Cypress.io Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal From 1e2368d336983ccd252f7cd82b2b784e56163993 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 22 Jun 2020 15:29:07 +0630 Subject: [PATCH 05/12] =?UTF-8?q?fix(deps):=20update=20dependency=20signal?= =?UTF-8?q?-exit=20to=20version=203.0.3=20=F0=9F=8C=9F=20(#7738)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Renovate Bot --- packages/server/package.json | 2 +- yarn.lock | 7 +------ 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/packages/server/package.json b/packages/server/package.json index 7520f5988095..8c392934785d 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -100,7 +100,7 @@ "semver": "6.3.0", "send": "0.17.1", "shell-env": "3.0.0", - "signal-exit": "3.0.2", + "signal-exit": "3.0.3", "squirrelly": "7.9.2", "strip-ansi": "6.0.0", "syntax-error": "1.4.0", diff --git a/yarn.lock b/yarn.lock index a0ed2fe132cb..8e5ac945d3ef 100644 --- a/yarn.lock +++ b/yarn.lock @@ -22420,12 +22420,7 @@ sigmund@~1.0.0: resolved "https://registry.yarnpkg.com/sigmund/-/sigmund-1.0.1.tgz#3ff21f198cad2175f9f3b781853fd94d0d19b590" integrity sha1-P/IfGYytIXX587eBhT/ZTQ0ZtZA= -signal-exit@3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" - integrity sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0= - -signal-exit@^3.0.0, signal-exit@^3.0.2: +signal-exit@3.0.3, signal-exit@^3.0.0, signal-exit@^3.0.2: version "3.0.3" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA== From abe2f3d52972beb3e42e3bc7e3910c6bbc34c36c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 22 Jun 2020 15:29:51 +0630 Subject: [PATCH 06/12] =?UTF-8?q?chore(deps):=20Update=20dependency=20angu?= =?UTF-8?q?lar=20to=20version=201.8.0=20=F0=9F=8C=9F=20(#7754)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Renovate Bot --- packages/driver/package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/driver/package.json b/packages/driver/package.json index 5f5dfb1480b8..03fdc5f7beda 100644 --- a/packages/driver/package.json +++ b/packages/driver/package.json @@ -19,7 +19,7 @@ "@packages/network": "*", "@packages/runner": "*", "@packages/ts": "*", - "angular": "1.7.9", + "angular": "1.8.0", "backbone": "1.4.0", "basic-auth": "2.0.1", "blob-util": "1.3.0", diff --git a/yarn.lock b/yarn.lock index 8e5ac945d3ef..c6327991c5c9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4857,10 +4857,10 @@ amdefine@>=0.0.4: resolved "https://registry.yarnpkg.com/amdefine/-/amdefine-1.0.1.tgz#4a5282ac164729e93619bcfd3ad151f817ce91f5" integrity sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU= -angular@1.7.9: - version "1.7.9" - resolved "https://registry.yarnpkg.com/angular/-/angular-1.7.9.tgz#e52616e8701c17724c3c238cfe4f9446fd570bc4" - integrity sha512-5se7ZpcOtu0MBFlzGv5dsM1quQDoDeUTwZrWjGtTNA7O88cD8TEk5IEKCTDa3uECV9XnvKREVUr7du1ACiWGFQ== +angular@1.8.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/angular/-/angular-1.8.0.tgz#b1ec179887869215cab6dfd0df2e42caa65b1b51" + integrity sha512-VdaMx+Qk0Skla7B5gw77a8hzlcOakwF8mjlW13DpIWIDlfqwAbSSLfd8N/qZnzEmQF4jC4iofInd3gE7vL8ZZg== ansi-align@^2.0.0: version "2.0.0" From baaf24a07e36dcb79906774fac32be6a1b76373a Mon Sep 17 00:00:00 2001 From: Ben Kucera <14625260+Bkucera@users.noreply.github.com> Date: Mon, 22 Jun 2020 10:53:43 -0400 Subject: [PATCH 07/12] fix dom-highlights rendering under absolute position elements (#7763) Co-authored-by: Jennifer Shehane --- .../integration/e2e/dom_hitbox.spec.js | 35 ++++++++++++++++--- packages/driver/cypress/support/utils.js | 23 +++++++----- packages/runner/src/lib/dom.js | 2 ++ 3 files changed, 47 insertions(+), 13 deletions(-) diff --git a/packages/driver/cypress/integration/e2e/dom_hitbox.spec.js b/packages/driver/cypress/integration/e2e/dom_hitbox.spec.js index e0341a5a720f..a5d0fb2561c3 100644 --- a/packages/driver/cypress/integration/e2e/dom_hitbox.spec.js +++ b/packages/driver/cypress/integration/e2e/dom_hitbox.spec.js @@ -1,4 +1,5 @@ const { clickCommandLog } = require('../../support/utils') +const { _ } = Cypress // https://github.com/cypress-io/cypress/pull/5299/files describe('rect highlight', () => { @@ -61,6 +62,24 @@ describe('rect highlight', () => { ensureCorrectHighlightPositions('#button') ensureCorrectTargetPosition('#button') }) + + // https://github.com/cypress-io/cypress/issues/7762 + it('highlights above z-index elements', () => { + cy.$$('
').css({ + position: 'absolute', + zIndex: 1000, + top: 0, + left: 0, + height: 50, + width: 50, + padding: 20, + margin: 20, + backgroundColor: 'salmon', + }).appendTo(cy.$$('body')) + + getAndPin('#absolute-el') + ensureCorrectHighlightPositions('#absolute-el') + }) }) const ensureCorrectTargetPosition = (sel) => { @@ -82,12 +101,20 @@ const ensureCorrectTargetPosition = (sel) => { const ensureCorrectHighlightPositions = (sel) => { return cy.wrap(null, { timeout: 400 }).should(() => { - const dims = { - content: cy.$$('div[data-layer=Content]')[0].getBoundingClientRect(), - padding: cy.$$('div[data-layer=Padding]')[0].getBoundingClientRect(), - border: cy.$$('div[data-layer=Border]')[0].getBoundingClientRect(), + const els = { + content: cy.$$('div[data-layer=Content]'), + padding: cy.$$('div[data-layer=Padding]'), + border: cy.$$('div[data-layer=Border]'), } + const dims = _.mapValues(els, ($el) => $el[0].getBoundingClientRect()) + + const doc = els.content[0].ownerDocument + + const contentHighlightCenter = [dims.content.x + dims.content.width / 2, dims.content.y + dims.content.height / 2] + + expect(doc.elementFromPoint(...contentHighlightCenter)).eq(els.content[0]) + expectToBeInside(dims.content, dims.padding, 'content to be inside padding') expectToBeInside(dims.padding, dims.border, 'padding to be inside border') if (sel) { diff --git a/packages/driver/cypress/support/utils.js b/packages/driver/cypress/support/utils.js index 07a3346b6d1a..149ecfd51a88 100644 --- a/packages/driver/cypress/support/utils.js +++ b/packages/driver/cypress/support/utils.js @@ -1,15 +1,18 @@ -const { $, _ } = Cypress +const { $, _, Promise } = Cypress export const getCommandLogWithText = (text) => { + // Open current test if not already open, so we can find the command log + cy.$$('.runnable-active .runnable-wrapper:not(.is-open)', top.document).click() + return cy - .$$(`.runnable-active .command-wrapper:contains(${text}):visible`, top.document) + .$$(`.runnable-active .command-wrapper:contains(${text})`, top.document) .parentsUntil('li') .last() .parent() } export const findReactInstance = function (dom) { - let key = Object.keys(dom).find((key) => key.startsWith('__reactInternalInstance$')) + let key = _.keys(dom).find((key) => key.startsWith('__reactInternalInstance$')) let internalInstance = dom[key] if (internalInstance == null) return null @@ -22,7 +25,7 @@ export const findReactInstance = function (dom) { export const clickCommandLog = (sel) => { return cy.wait(10) .then(() => { - withMutableReporterState(() => { + return withMutableReporterState(() => { const commandLogEl = getCommandLogWithText(sel) const reactCommandInstance = findReactInstance(commandLogEl[0]) @@ -33,10 +36,12 @@ export const clickCommandLog = (sel) => { reactCommandInstance.props.appState.isRunning = false - $(commandLogEl).find('.command-wrapper').click() + $(commandLogEl).find('.command-wrapper') + .click() + .get(0).scrollIntoView() // make sure command was pinned, otherwise throw a better error message - expect(cy.$$('.command-pin:visible', top.document).length, 'command should be pinned').ok + expect(cy.$$('.runnable-active .command-pin', top.document).length, 'command should be pinned').ok }) }) } @@ -46,9 +51,9 @@ export const withMutableReporterState = (fn) => { const currentTestLog = findReactInstance(cy.$$('.runnable-active', top.document)[0]) - currentTestLog.props.model.isOpen = true + currentTestLog.props.model._isOpen = true - return Cypress.Promise.try(fn) + return Promise.try(fn) .then(() => { top.Runner.configureMobx({ enforceActions: 'always' }) }) @@ -79,7 +84,7 @@ const getAllFn = (...aliases) => { return getAllFn((_.isArray(aliases[1]) ? aliases[1] : aliases[1].split(' ')).map((alias) => `@${aliases[0]}:${alias}`).join(' ')) } - return Cypress.Promise.all( + return Promise.all( aliases[0].split(' ').map((alias) => { return cy.now('get', alias) }), diff --git a/packages/runner/src/lib/dom.js b/packages/runner/src/lib/dom.js index a5474931423a..9530228040ad 100644 --- a/packages/runner/src/lib/dom.js +++ b/packages/runner/src/lib/dom.js @@ -73,6 +73,8 @@ function addElementBoxModelLayers ($el, $body) { const $container = $('
') .css({ opacity: 0.7, + position: 'absolute', + zIndex: 2147483647, }) const layers = { From 768da16a5e8cecc61d85fd4939a17c0b93e10366 Mon Sep 17 00:00:00 2001 From: Steve Date: Tue, 23 Jun 2020 01:10:35 +1000 Subject: [PATCH 08/12] feat: add `quiet` cli arg/module option (#7714) * Option to disable Cypress the verbose results to stdout --quiet mode to disable the verbose results to stdout but still use specified formatter * Update cli/schema/cypress.schema.json Co-authored-by: Jennifer Shehane * Cypress module support for --quiet Cypress module support for --quiet * Apply suggestions from code review Co-authored-by: Zach Bloomquist * Address feedback Address feedback, thanks Co-authored-by: Jennifer Shehane Co-authored-by: Zach Bloomquist --- cli/__snapshots__/cli_spec.js | 1 + cli/lib/cli.js | 2 + cli/lib/exec/run.js | 4 + cli/lib/util.js | 1 + cli/test/lib/cypress_spec.js | 12 ++ cli/types/cypress-npm-api.d.ts | 4 + .../server/__snapshots__/5_stdout_spec.js | 23 +++ packages/server/lib/modes/run.js | 173 ++++++++++-------- packages/server/lib/util/args.js | 2 +- packages/server/test/e2e/5_stdout_spec.js | 9 + packages/server/test/support/helpers/e2e.ts | 4 + 11 files changed, 156 insertions(+), 79 deletions(-) diff --git a/cli/__snapshots__/cli_spec.js b/cli/__snapshots__/cli_spec.js index 2ff6b0b1e7ab..1a928151f95b 100644 --- a/cli/__snapshots__/cli_spec.js +++ b/cli/__snapshots__/cli_spec.js @@ -75,6 +75,7 @@ exports['shows help for run --foo 1'] = ` --parallel enables concurrent runs and automatic load balancing of specs across multiple machines or processes -p, --port runs Cypress on a specific port. overrides any value in cypress.json. -P, --project path to the project + -q, --quiet run quietly, using only the configured reporter --record [bool] records the run. sends test results, screenshots and videos to your Cypress Dashboard. -r, --reporter runs a specific mocha reporter. pass a path to use a custom reporter. defaults to "spec" -o, --reporter-options options for the mocha reporter. defaults to "null" diff --git a/cli/lib/cli.js b/cli/lib/cli.js index 2875750b712c..adb6505a448b 100644 --- a/cli/lib/cli.js +++ b/cli/lib/cli.js @@ -109,6 +109,7 @@ const descriptions = { parallel: 'enables concurrent runs and automatic load balancing of specs across multiple machines or processes', port: 'runs Cypress on a specific port. overrides any value in cypress.json.', project: 'path to the project', + quiet: 'run quietly, using only the configured reporter', record: 'records the run. sends test results, screenshots and videos to your Cypress Dashboard.', reporter: 'runs a specific mocha reporter. pass a path to use a custom reporter. defaults to "spec"', reporterOptions: 'options for the mocha reporter. defaults to "null"', @@ -231,6 +232,7 @@ module.exports = { .option('--parallel', text('parallel')) .option('-p, --port ', text('port')) .option('-P, --project ', text('project')) + .option('-q, --quiet', text('quiet')) .option('--record [bool]', text('record'), coerceFalse) .option('-r, --reporter ', text('reporter')) .option('-o, --reporter-options ', text('reporterOptions')) diff --git a/cli/lib/exec/run.js b/cli/lib/exec/run.js index 28fc4f352db7..68b5e219149f 100644 --- a/cli/lib/exec/run.js +++ b/cli/lib/exec/run.js @@ -89,6 +89,10 @@ const processRunOptions = (options = {}) => { args.push('--port', options.port) } + if (options.quiet) { + args.push('--quiet') + } + // if record is defined and we're not // already in ci mode, then send it up if (options.record != null && !options.ci) { diff --git a/cli/lib/util.js b/cli/lib/util.js index 4c4d1049b05d..7819adfaadc8 100644 --- a/cli/lib/util.js +++ b/cli/lib/util.js @@ -214,6 +214,7 @@ const parseOpts = (opts) => { 'parallel', 'port', 'project', + 'quiet', 'reporter', 'reporterOptions', 'record', diff --git a/cli/test/lib/cypress_spec.js b/cli/test/lib/cypress_spec.js index 506c5e3ab386..ac4c85daff73 100644 --- a/cli/test/lib/cypress_spec.js +++ b/cli/test/lib/cypress_spec.js @@ -147,5 +147,17 @@ describe('cypress', function () { expect(args).to.deep.eq(opts) }) }) + + it('passes quiet: true', () => { + const opts = { + quiet: true, + } + + return cypress.run(opts) + .then(getStartArgs) + .then((args) => { + expect(args).to.deep.eq(opts) + }) + }) }) }) diff --git a/cli/types/cypress-npm-api.d.ts b/cli/types/cypress-npm-api.d.ts index 0ebaea604454..116d99c4e828 100644 --- a/cli/types/cypress-npm-api.d.ts +++ b/cli/types/cypress-npm-api.d.ts @@ -68,6 +68,10 @@ declare module 'cypress' { * Override default port */ port: number + /** + * Run quietly, using only the configured reporter + */ + quiet: boolean /** * Whether to record the test run */ diff --git a/packages/server/__snapshots__/5_stdout_spec.js b/packages/server/__snapshots__/5_stdout_spec.js index 255569726d5c..bc8aa9edb69e 100644 --- a/packages/server/__snapshots__/5_stdout_spec.js +++ b/packages/server/__snapshots__/5_stdout_spec.js @@ -523,3 +523,26 @@ exports['e2e stdout / displays assertion errors'] = ` ` + +exports['e2e stdout respects quiet mode 1'] = ` + + + stdout_passing_spec + file + ✓ visits file + google + ✓ visits google + ✓ google2 + apple + ✓ apple1 + ✓ visits apple + subdomains + ✓ cypress1 + ✓ visits cypress + ✓ cypress3 + + + 8 passing + + +` diff --git a/packages/server/lib/modes/run.js b/packages/server/lib/modes/run.js index d59c89c870eb..93b1a199493e 100644 --- a/packages/server/lib/modes/run.js +++ b/packages/server/lib/modes/run.js @@ -813,7 +813,7 @@ module.exports = { console.log('') }, - async postProcessRecording (name, cname, videoCompression, shouldUploadVideo) { + async postProcessRecording (name, cname, videoCompression, shouldUploadVideo, quiet) { debug('ending the video recording %o', { name, videoCompression, shouldUploadVideo }) // once this ended promises resolves @@ -824,79 +824,83 @@ module.exports = { return } - console.log('') - - terminal.header('Video', { - color: ['cyan'], - }) - - console.log('') - - const table = terminal.table({ - colWidths: [3, 21, 76], - colAligns: ['left', 'left', 'left'], - type: 'noBorder', - style: { - 'padding-right': 0, - }, - chars: { - 'left': ' ', - 'right': '', - }, - }) + const postProcessRecordingOutput = (name, videoCompression) => { + console.log('') - table.push([ - gray('-'), - gray('Started processing:'), - chalk.cyan(`Compressing to ${videoCompression} CRF`), - ]) + terminal.header('Video', { + color: ['cyan'], + }) - console.log(table.toString()) + console.log('') - const started = Date.now() - let progress = Date.now() - const throttle = env.get('VIDEO_COMPRESSION_THROTTLE') || human('10 seconds') + const table = terminal.table({ + colWidths: [3, 21, 76], + colAligns: ['left', 'left', 'left'], + type: 'noBorder', + style: { + 'padding-right': 0, + }, + chars: { + 'left': ' ', + 'right': '', + }, + }) - const onProgress = function (float) { - if (float === 1) { - const finished = Date.now() - started - const dur = `(${humanTime.long(finished)})` + table.push([ + gray('-'), + gray('Started processing:'), + chalk.cyan(`Compressing to ${videoCompression} CRF`), + ]) - const table = terminal.table({ - colWidths: [3, 21, 61, 15], - colAligns: ['left', 'left', 'left', 'right'], - type: 'noBorder', - style: { - 'padding-right': 0, - }, - chars: { - 'left': ' ', - 'right': '', - }, - }) + console.log(table.toString()) + + const started = Date.now() + let progress = Date.now() + const throttle = env.get('VIDEO_COMPRESSION_THROTTLE') || human('10 seconds') + + return function (float) { + if (float === 1) { + const finished = Date.now() - started + const dur = `(${humanTime.long(finished)})` + + const table = terminal.table({ + colWidths: [3, 21, 61, 15], + colAligns: ['left', 'left', 'left', 'right'], + type: 'noBorder', + style: { + 'padding-right': 0, + }, + chars: { + 'left': ' ', + 'right': '', + }, + }) - table.push([ - gray('-'), - gray('Finished processing:'), - `${formatPath(name, getWidth(table, 2), 'cyan')}`, - gray(dur), - ]) + table.push([ + gray('-'), + gray('Finished processing:'), + `${formatPath(name, getWidth(table, 2), 'cyan')}`, + gray(dur), + ]) - console.log(table.toString()) + console.log(table.toString()) - console.log('') - } + console.log('') + } - if (Date.now() - progress > throttle) { - // bump up the progress so we dont - // continuously get notifications - progress += throttle - const percentage = `${Math.ceil(float * 100)}%` + if (Date.now() - progress > throttle) { + // bump up the progress so we dont + // continuously get notifications + progress += throttle + const percentage = `${Math.ceil(float * 100)}%` - console.log(' Compression progress: ', chalk.cyan(percentage)) + console.log(' Compression progress: ', chalk.cyan(percentage)) + } } } + const onProgress = quiet ? undefined : postProcessRecordingOutput(name, videoCompression) + return videoCapture.process(name, cname, videoCompression, onProgress) }, @@ -1061,7 +1065,7 @@ module.exports = { }, waitForTestsToFinishRunning (options = {}) { - const { project, screenshots, startedVideoCapture, endVideoCapture, videoName, compressedVideoName, videoCompression, videoUploadOnPasses, exit, spec, estimated } = options + const { project, screenshots, startedVideoCapture, endVideoCapture, videoName, compressedVideoName, videoCompression, videoUploadOnPasses, exit, spec, estimated, quiet } = options // https://github.com/cypress-io/cypress/issues/2370 // delay 1 second if we're recording a video to give @@ -1095,10 +1099,11 @@ module.exports = { return obj } - this.displayResults(obj, estimated) - - if (screenshots && screenshots.length) { - this.displayScreenshots(screenshots) + if (!quiet) { + this.displayResults(obj, estimated) + if (screenshots && screenshots.length) { + this.displayScreenshots(screenshots) + } } const { tests, stats } = obj @@ -1137,6 +1142,7 @@ module.exports = { compressedVideoName, videoCompression, suv, + quiet, ) .catch(warnVideoRecordingFailed) } @@ -1191,19 +1197,23 @@ module.exports = { config, } - displayRunStarting({ - config, - specs, - group, - tag, - runUrl, - browser, - parallel, - specPattern, - }) + if (!options.quiet) { + displayRunStarting({ + config, + specs, + group, + tag, + runUrl, + browser, + parallel, + specPattern, + }) + } const runEachSpec = (spec, index, length, estimated) => { - displaySpecHeader(spec.name, index + 1, length, estimated) + if (!options.quiet) { + displaySpecHeader(spec.name, index + 1, length, estimated) + } return this.runSpec(spec, options, estimated) .get('results') @@ -1282,6 +1292,7 @@ module.exports = { exit: options.exit, videoCompression: options.videoCompression, videoUploadOnPasses: options.videoUploadOnPasses, + quiet: options.quiet, }), connection: this.waitForBrowserToConnect({ @@ -1322,6 +1333,7 @@ module.exports = { _.defaults(options, { isTextTerminal: true, browser: 'electron', + quiet: false, }) const socketId = random.id() @@ -1410,9 +1422,14 @@ module.exports = { videoUploadOnPasses: config.videoUploadOnPasses, exit: options.exit, headed: options.headed, + quiet: options.quiet, outputPath: options.outputPath, }) - .tap(renderSummaryTable(runUrl)) + .tap((runSpecs) => { + if (!options.quiet) { + renderSummaryTable(runUrl)(runSpecs) + } + }) } if (record) { diff --git a/packages/server/lib/util/args.js b/packages/server/lib/util/args.js index 45393095bc39..348a43f33039 100644 --- a/packages/server/lib/util/args.js +++ b/packages/server/lib/util/args.js @@ -13,7 +13,7 @@ const nestedObjectsInCurlyBracesRe = /\{(.+?)\}/g const nestedArraysInSquareBracketsRe = /\[(.+?)\]/g const everythingAfterFirstEqualRe = /=(.*)/ -const whitelist = 'appPath apiKey browser ci ciBuildId clearLogs config configFile cwd env execPath exit exitWithCode generateKey getKey group headed inspectBrk key logs mode outputPath parallel ping port project proxySource record reporter reporterOptions returnPkg runMode runProject smokeTest spec tag updating version'.split(' ') +const whitelist = 'appPath apiKey browser ci ciBuildId clearLogs config configFile cwd env execPath exit exitWithCode generateKey getKey group headed inspectBrk key logs mode outputPath parallel ping port project proxySource quiet record reporter reporterOptions returnPkg runMode runProject smokeTest spec tag updating version'.split(' ') // returns true if the given string has double quote character " // only at the last position. const hasStrayEndQuote = (s) => { diff --git a/packages/server/test/e2e/5_stdout_spec.js b/packages/server/test/e2e/5_stdout_spec.js index 412761cde255..969521025711 100644 --- a/packages/server/test/e2e/5_stdout_spec.js +++ b/packages/server/test/e2e/5_stdout_spec.js @@ -28,6 +28,15 @@ describe('e2e stdout', () => { }) }) + it('respects quiet mode', function () { + return e2e.exec(this, { + spec: 'stdout_passing_spec.coffee', + timeout: 120000, + snapshot: true, + quiet: true, + }) + }) + it('displays fullname of nested specfile', function () { return e2e.exec(this, { port: 2020, diff --git a/packages/server/test/support/helpers/e2e.ts b/packages/server/test/support/helpers/e2e.ts index 1ed61d76b408..1f4dfbe25c62 100644 --- a/packages/server/test/support/helpers/e2e.ts +++ b/packages/server/test/support/helpers/e2e.ts @@ -509,6 +509,10 @@ const e2e = { args.push('--record') } + if (options.quiet) { + args.push('--quiet') + } + if (options.parallel) { args.push('--parallel') } From bf752f1b2ed805a22aba06388884a5405bb68a84 Mon Sep 17 00:00:00 2001 From: Zach Bloomquist Date: Mon, 22 Jun 2020 14:05:04 -0400 Subject: [PATCH 09/12] fix: correct bad propagation of exit signals (#7755) --- packages/server/lib/util/node_options.ts | 7 ++- packages/server/test/scripts/run.js | 8 ++- .../server/test/unit/node_options_spec.ts | 63 +++++++++++++++++++ 3 files changed, 74 insertions(+), 4 deletions(-) create mode 100644 packages/server/test/unit/node_options_spec.ts diff --git a/packages/server/lib/util/node_options.ts b/packages/server/lib/util/node_options.ts index a948dc04ca85..0acc0246443b 100644 --- a/packages/server/lib/util/node_options.ts +++ b/packages/server/lib/util/node_options.ts @@ -3,7 +3,7 @@ import debugModule from 'debug' const debug = debugModule('cypress:server:util:node_options') -const NODE_OPTIONS = `--max-http-header-size=${1024 ** 2} --http-parser=legacy` +export const NODE_OPTIONS = `--max-http-header-size=${1024 ** 2} --http-parser=legacy` /** * If Cypress was not launched via CLI, it may be missing certain startup @@ -63,8 +63,9 @@ export function forkWithCorrectOptions (): void { { stdio: 'inherit' }, ) .on('error', () => {}) - .on('exit', (code) => { - process.exit(code) + .on('exit', (code, signal) => { + debug('child exited %o', { code, signal }) + process.exit(code === null ? 1 : code) }) } diff --git a/packages/server/test/scripts/run.js b/packages/server/test/scripts/run.js index f30e816605ff..0a1ab38e1c52 100644 --- a/packages/server/test/scripts/run.js +++ b/packages/server/test/scripts/run.js @@ -164,4 +164,10 @@ console.log(cmd) const child = execa.shell(cmd, { env, stdio: 'inherit' }) -child.on('exit', process.exit) +child.on('exit', (code, signal) => { + if (signal) { + console.error(`tests exited with signal ${signal}`) + } + + process.exit(code === null ? 1 : code) +}) diff --git a/packages/server/test/unit/node_options_spec.ts b/packages/server/test/unit/node_options_spec.ts new file mode 100644 index 000000000000..130f372d035c --- /dev/null +++ b/packages/server/test/unit/node_options_spec.ts @@ -0,0 +1,63 @@ +import '../spec_helper' +import sinon from 'sinon' +import { expect } from 'chai' +import cp, { ChildProcess } from 'child_process' +import { EventEmitter } from 'events' +import * as nodeOptions from '../../lib/util/node_options' +import mockedEnv from 'mocked-env' + +describe('NODE_OPTIONS lib', function () { + context('.forkWithCorrectOptions', function () { + let fakeProc: EventEmitter + let restoreEnv + + beforeEach(() => { + restoreEnv = mockedEnv({ + NODE_OPTIONS: '', + ORIGINAL_NODE_OPTIONS: '', + }) + }) + + afterEach(() => { + restoreEnv() + }) + + it('modifies NODE_OPTIONS', function () { + process.env.NODE_OPTIONS = 'foo' + expect(process.env.NODE_OPTIONS).to.eq('foo') + sinon.stub(cp, 'spawn').callsFake(() => { + expect(process.env).to.include({ + NODE_OPTIONS: `${nodeOptions.NODE_OPTIONS} foo`, + ORIGINAL_NODE_OPTIONS: 'foo', + }) + + return null as ChildProcess // types + }) + }) + + context('when exiting', function () { + beforeEach(() => { + fakeProc = new EventEmitter() + + sinon.stub(cp, 'spawn') + .withArgs(process.execPath, sinon.match.any, { stdio: 'inherit' }) + .returns(fakeProc as ChildProcess) + + sinon.stub(process, 'exit') + }) + + it('propagates exit codes correctly', function () { + nodeOptions.forkWithCorrectOptions() + fakeProc.emit('exit', 123) + expect(process.exit).to.be.calledWith(123) + }) + + // @see https://github.com/cypress-io/cypress/issues/7722 + it('propagates signals via a non-zero exit code', function () { + nodeOptions.forkWithCorrectOptions() + fakeProc.emit('exit', null, 'SIGKILL') + expect(process.exit).to.be.calledWith(1) + }) + }) + }) +}) From 6423b352daf348a07af5fe98c62b518d6ddcbdb9 Mon Sep 17 00:00:00 2001 From: Zach Bloomquist Date: Mon, 22 Jun 2020 14:08:42 -0400 Subject: [PATCH 10/12] chore: clean up postProcessRecording code (#7777) --- packages/server/lib/modes/run.js | 134 ++++++++++++++++--------------- 1 file changed, 69 insertions(+), 65 deletions(-) diff --git a/packages/server/lib/modes/run.js b/packages/server/lib/modes/run.js index 93b1a199493e..cc8bb57c46f6 100644 --- a/packages/server/lib/modes/run.js +++ b/packages/server/lib/modes/run.js @@ -824,84 +824,88 @@ module.exports = { return } - const postProcessRecordingOutput = (name, videoCompression) => { - console.log('') + function continueProcessing (onProgress = undefined) { + return videoCapture.process(name, cname, videoCompression, onProgress) + } - terminal.header('Video', { - color: ['cyan'], - }) + if (quiet) { + return continueProcessing() + } - console.log('') + console.log('') - const table = terminal.table({ - colWidths: [3, 21, 76], - colAligns: ['left', 'left', 'left'], - type: 'noBorder', - style: { - 'padding-right': 0, - }, - chars: { - 'left': ' ', - 'right': '', - }, - }) + terminal.header('Video', { + color: ['cyan'], + }) - table.push([ - gray('-'), - gray('Started processing:'), - chalk.cyan(`Compressing to ${videoCompression} CRF`), - ]) + console.log('') - console.log(table.toString()) - - const started = Date.now() - let progress = Date.now() - const throttle = env.get('VIDEO_COMPRESSION_THROTTLE') || human('10 seconds') - - return function (float) { - if (float === 1) { - const finished = Date.now() - started - const dur = `(${humanTime.long(finished)})` - - const table = terminal.table({ - colWidths: [3, 21, 61, 15], - colAligns: ['left', 'left', 'left', 'right'], - type: 'noBorder', - style: { - 'padding-right': 0, - }, - chars: { - 'left': ' ', - 'right': '', - }, - }) + const table = terminal.table({ + colWidths: [3, 21, 76], + colAligns: ['left', 'left', 'left'], + type: 'noBorder', + style: { + 'padding-right': 0, + }, + chars: { + 'left': ' ', + 'right': '', + }, + }) - table.push([ - gray('-'), - gray('Finished processing:'), - `${formatPath(name, getWidth(table, 2), 'cyan')}`, - gray(dur), - ]) + table.push([ + gray('-'), + gray('Started processing:'), + chalk.cyan(`Compressing to ${videoCompression} CRF`), + ]) - console.log(table.toString()) + console.log(table.toString()) - console.log('') - } + const started = Date.now() + let progress = Date.now() + const throttle = env.get('VIDEO_COMPRESSION_THROTTLE') || human('10 seconds') - if (Date.now() - progress > throttle) { - // bump up the progress so we dont - // continuously get notifications - progress += throttle - const percentage = `${Math.ceil(float * 100)}%` + const onProgress = function (float) { + if (float === 1) { + const finished = Date.now() - started + const dur = `(${humanTime.long(finished)})` - console.log(' Compression progress: ', chalk.cyan(percentage)) - } + const table = terminal.table({ + colWidths: [3, 21, 61, 15], + colAligns: ['left', 'left', 'left', 'right'], + type: 'noBorder', + style: { + 'padding-right': 0, + }, + chars: { + 'left': ' ', + 'right': '', + }, + }) + + table.push([ + gray('-'), + gray('Finished processing:'), + `${formatPath(name, getWidth(table, 2), 'cyan')}`, + gray(dur), + ]) + + console.log(table.toString()) + + console.log('') } - } - const onProgress = quiet ? undefined : postProcessRecordingOutput(name, videoCompression) + if (Date.now() - progress > throttle) { + // bump up the progress so we dont + // continuously get notifications + progress += throttle + const percentage = `${Math.ceil(float * 100)}%` + + console.log(' Compression progress: ', chalk.cyan(percentage)) + } + } - return videoCapture.process(name, cname, videoCompression, onProgress) + return continueProcessing(onProgress) }, launchBrowser (options = {}) { From 653739bb5c0bf210402aa7837441a4a4af6dbf92 Mon Sep 17 00:00:00 2001 From: Gleb Bahmutov Date: Mon, 22 Jun 2020 14:34:59 -0400 Subject: [PATCH 11/12] Handle --project "" command line argument (#7744) Co-authored-by: Jennifer Shehane --- cli/__snapshots__/errors_spec.js | 2 + cli/lib/cypress.js | 12 +++++ cli/lib/errors.js | 31 ++++++++++--- cli/lib/exec/run.js | 61 ++++++++++++++++++++++---- cli/test/lib/cypress_spec.js | 12 +++++ cli/test/lib/exec/run_spec.js | 22 ++++++++++ packages/server/lib/util/args.js | 29 +++++++++--- packages/server/test/unit/args_spec.js | 43 ++++++++++++++++++ 8 files changed, 193 insertions(+), 19 deletions(-) diff --git a/cli/__snapshots__/errors_spec.js b/cli/__snapshots__/errors_spec.js index e04b953cee2e..f97265efbf98 100644 --- a/cli/__snapshots__/errors_spec.js +++ b/cli/__snapshots__/errors_spec.js @@ -35,6 +35,7 @@ exports['errors individual has the following errors 1'] = [ "incompatibleHeadlessFlags", "invalidCacheDirectory", "invalidCypressEnv", + "invalidRunProjectPath", "invalidSmokeTestDisplayError", "missingApp", "missingDependency", @@ -44,6 +45,7 @@ exports['errors individual has the following errors 1'] = [ "removed", "smokeTestFailure", "unexpected", + "unknownError", "versionMismatch" ] diff --git a/cli/lib/cypress.js b/cli/lib/cypress.js index ad49ce60b143..25ffa5fe9c9f 100644 --- a/cli/lib/cypress.js +++ b/cli/lib/cypress.js @@ -9,13 +9,25 @@ const run = require('./exec/run') const util = require('./util') const cypressModuleApi = { + /** + * Opens Cypress GUI + * @see https://on.cypress.io/module-api#cypress-open + */ open (options = {}) { options = util.normalizeModuleOptions(options) return open.start(options) }, + /** + * Runs Cypress tests in the current project + * @see https://on.cypress.io/module-api#cypress-run + */ run (options = {}) { + if (!run.isValidProject(options.project)) { + return Promise.reject(new Error(`Invalid project path parameter: ${options.project}`)) + } + options = util.normalizeModuleOptions(options) return tmp.fileAsync() diff --git a/cli/lib/errors.js b/cli/lib/errors.js index 233b0ac855b3..d0e5334d6eee 100644 --- a/cli/lib/errors.js +++ b/cli/lib/errors.js @@ -9,13 +9,36 @@ const state = require('./tasks/state') const docsUrl = 'https://on.cypress.io' const requiredDependenciesUrl = `${docsUrl}/required-dependencies` +const runDocumentationUrl = `${docsUrl}/cypress-run` // TODO it would be nice if all error objects could be enforced via types // to only have description + solution properties const hr = '----------' +const genericErrorSolution = stripIndent` + Search for an existing issue or open a GitHub issue at + + ${chalk.blue(util.issuesUrl)} +` + // common errors Cypress application can encounter +const unknownError = { + description: 'Unknown Cypress CLI error', + solution: genericErrorSolution, +} + +const invalidRunProjectPath = { + description: 'Invalid --project path', + solution: stripIndent` + Please provide a valid project path. + + Learn more about ${chalk.cyan('cypress run')} at: + + ${chalk.blue(runDocumentationUrl)} + `, +} + const failedDownload = { description: 'The Cypress App could not be downloaded.', solution: stripIndent` @@ -26,11 +49,7 @@ const failedDownload = { const failedUnzip = { description: 'The Cypress App could not be unzipped.', - solution: stripIndent` - Search for an existing issue or open a GitHub issue at - - ${chalk.blue(util.issuesUrl)} - `, + solution: genericErrorSolution, } const missingApp = (binaryDir) => { @@ -390,6 +409,7 @@ module.exports = { getError, hr, errors: { + unknownError, nonZeroExitCodeXvfb, missingXvfb, missingApp, @@ -408,5 +428,6 @@ module.exports = { smokeTestFailure, childProcessKilled, incompatibleHeadlessFlags, + invalidRunProjectPath, }, } diff --git a/cli/lib/exec/run.js b/cli/lib/exec/run.js index 68b5e219149f..f173465c3f63 100644 --- a/cli/lib/exec/run.js +++ b/cli/lib/exec/run.js @@ -6,11 +6,60 @@ const spawn = require('./spawn') const verify = require('../tasks/verify') const { exitWithError, errors } = require('../errors') -// maps options collected by the CLI -// and forms list of CLI arguments to the server +/** + * Throws an error with "details" property from + * "errors" object. + * @param {Object} details - Error details + */ +const throwInvalidOptionError = (details) => { + if (!details) { + details = errors.unknownError + } + + // throw this error synchronously, it will be caught later on and + // the details will be propagated to the promise chain + const err = new Error() + + err.details = details + throw err +} + +/** + * Typically a user passes a string path to the project. + * But "cypress open" allows using `false` to open in global mode, + * and the user can accidentally execute `cypress run --project false` + * which should be invalid. + */ +const isValidProject = (v) => { + if (typeof v === 'boolean') { + return false + } + + if (v === '' || v === 'false' || v === 'true') { + return false + } + + return true +} + +/** + * Maps options collected by the CLI + * and forms list of CLI arguments to the server. + * + * Note: there is lightweight validation, with errors + * thrown synchronously. + * + * @returns {string[]} list of CLI arguments + */ const processRunOptions = (options = {}) => { debug('processing run options %o', options) + if (!isValidProject(options.project)) { + debug('invalid project option %o', { project: options.project }) + + return throwInvalidOptionError(errors.invalidRunProjectPath) + } + const args = ['--run-project', options.project] if (options.browser) { @@ -55,12 +104,7 @@ const processRunOptions = (options = {}) => { if (options.headless) { if (options.headed) { - // throw this error synchronously, it will be caught later on and - // the details will be propagated to the promise chain - const err = new Error() - - err.details = errors.incompatibleHeadlessFlags - throw err + return throwInvalidOptionError(errors.incompatibleHeadlessFlags) } args.push('--headed', !options.headless) @@ -123,6 +167,7 @@ const processRunOptions = (options = {}) => { module.exports = { processRunOptions, + isValidProject, // resolves with the number of failed tests start (options = {}) { _.defaults(options, { diff --git a/cli/test/lib/cypress_spec.js b/cli/test/lib/cypress_spec.js index ac4c85daff73..000575b60dc2 100644 --- a/cli/test/lib/cypress_spec.js +++ b/cli/test/lib/cypress_spec.js @@ -148,6 +148,18 @@ describe('cypress', function () { }) }) + it('rejects if project is an empty string', () => { + return expect(cypress.run({ project: '' })).to.be.rejected + }) + + it('rejects if project is true', () => { + return expect(cypress.run({ project: true })).to.be.rejected + }) + + it('rejects if project is false', () => { + return expect(cypress.run({ project: false })).to.be.rejected + }) + it('passes quiet: true', () => { const opts = { quiet: true, diff --git a/cli/test/lib/exec/run_spec.js b/cli/test/lib/exec/run_spec.js index 69df0f1fa71a..4142654d92a1 100644 --- a/cli/test/lib/exec/run_spec.js +++ b/cli/test/lib/exec/run_spec.js @@ -15,6 +15,28 @@ describe('exec run', function () { }) context('.processRunOptions', function () { + it('allows string --project option', () => { + const args = run.processRunOptions({ + project: '/path/to/project', + }) + + expect(args).to.deep.equal(['--run-project', '/path/to/project']) + }) + + it('throws an error for empty string --project', () => { + expect(() => run.processRunOptions({ project: '' })).to.throw() + }) + + it('throws an error for boolean --project', () => { + expect(() => run.processRunOptions({ project: false })).to.throw() + expect(() => run.processRunOptions({ project: true })).to.throw() + }) + + it('throws an error for --project "false" or "true"', () => { + expect(() => run.processRunOptions({ project: 'false' })).to.throw() + expect(() => run.processRunOptions({ project: 'true' })).to.throw() + }) + it('passes --browser option', () => { const args = run.processRunOptions({ browser: 'test browser', diff --git a/packages/server/lib/util/args.js b/packages/server/lib/util/args.js index 348a43f33039..43184b043ea6 100644 --- a/packages/server/lib/util/args.js +++ b/packages/server/lib/util/args.js @@ -34,17 +34,27 @@ const normalizeBackslash = (s) => { return s } +/** + * remove stray double quote from runProject and other path properties + * due to bug in NPM passing arguments with backslash at the end + * @see https://github.com/cypress-io/cypress/issues/535 + * + */ const normalizeBackslashes = (options) => { - // remove stray double quote from runProject and other path properties - // due to bug in NPM passing arguments with - // backslash at the end - // https://github.com/cypress-io/cypress/issues/535 // these properties are paths and likely to have backslash on Windows const pathProperties = ['runProject', 'project', 'appPath', 'execPath', 'configFile'] pathProperties.forEach((property) => { - if (options[property]) { + // sometimes a string parameter might get parsed into a boolean + // for example "--project ''" will be transformed in "project: true" + // which we should treat as undefined + if (typeof options[property] === 'string') { options[property] = normalizeBackslash(options[property]) + } else { + // configFile is a special case that can be set to false + if (property !== 'configFile') { + delete options[property] + } } }) @@ -148,6 +158,8 @@ const sanitizeAndConvertNestedArgs = (str, argname) => { } module.exports = { + normalizeBackslashes, + toObject (argv) { debug('argv array: %o', argv) @@ -210,7 +222,12 @@ module.exports = { let { spec } = options const { env, config, reporterOptions, outputPath, tag } = options - const project = options.project || options.runProject + let project = options.project || options.runProject + + // only accept project if it is a string + if (typeof project !== 'string') { + project = undefined + } if (spec) { const resolvePath = (p) => { diff --git a/packages/server/test/unit/args_spec.js b/packages/server/test/unit/args_spec.js index a09082c159bf..d7be0988e085 100644 --- a/packages/server/test/unit/args_spec.js +++ b/packages/server/test/unit/args_spec.js @@ -24,6 +24,37 @@ describe('lib/util/args', () => { }) }) + context('normalizeBackslashes', () => { + it('sets non-string properties to undefined', () => { + const input = { + // string properties + project: true, + appPath: '/foo/bar', + // this option can be string or false + configFile: false, + // unknown properties will be preserved + somethingElse: 42, + } + const output = argsUtil.normalizeBackslashes(input) + + expect(output).to.deep.equal({ + appPath: '/foo/bar', + configFile: false, + somethingElse: 42, + }) + }) + + it('handles empty project path string', () => { + const input = { + project: '', + } + const output = argsUtil.normalizeBackslashes(input) + + // empty project path remains + expect(output).to.deep.equal(input) + }) + }) + context('--project', () => { it('sets projectRoot', function () { const projectRoot = path.resolve(cwd, './foo/bar') @@ -31,6 +62,18 @@ describe('lib/util/args', () => { expect(options.projectRoot).to.eq(projectRoot) }) + + it('is undefined if not specified', function () { + const options = this.setup() + + expect(options.projectRoot).to.eq(undefined) + }) + + it('handles bool project parameter', function () { + const options = this.setup('--project', true) + + expect(options.projectRoot).to.eq(undefined) + }) }) context('--run-project', () => { From fc06e52f64167761b0c767060e9655927c8f7891 Mon Sep 17 00:00:00 2001 From: Jennifer Shehane Date: Tue, 23 Jun 2020 13:52:20 +0630 Subject: [PATCH 12/12] Update blob-util, remove @types/blog-util --- cli/package.json | 1 - cli/scripts/utils.js | 1 - packages/driver/package.json | 2 +- yarn.lock | 40 ++++-------------------------------- 4 files changed, 5 insertions(+), 39 deletions(-) diff --git a/cli/package.json b/cli/package.json index bfff011e8b86..30a022e948fb 100644 --- a/cli/package.json +++ b/cli/package.json @@ -63,7 +63,6 @@ "@babel/preset-env": "7.9.5", "@cypress/sinon-chai": "1.1.0", "@packages/root": "*", - "@types/blob-util": "1.3.3", "@types/bluebird": "3.5.29", "@types/chai": "4.2.7", "@types/chai-jquery": "1.1.40", diff --git a/cli/scripts/utils.js b/cli/scripts/utils.js index 8c215081d438..87389ed9826e 100644 --- a/cli/scripts/utils.js +++ b/cli/scripts/utils.js @@ -4,7 +4,6 @@ * definition files that we will need to include with our NPM package. */ const includeTypes = [ - 'blob-util', 'bluebird', 'lodash', 'mocha', diff --git a/packages/driver/package.json b/packages/driver/package.json index 03fdc5f7beda..ea44e4a1c98f 100644 --- a/packages/driver/package.json +++ b/packages/driver/package.json @@ -22,7 +22,7 @@ "angular": "1.8.0", "backbone": "1.4.0", "basic-auth": "2.0.1", - "blob-util": "1.3.0", + "blob-util": "2.0.2", "bluebird": "3.5.3", "body-parser": "1.19.0", "bootstrap": "4.4.1", diff --git a/yarn.lock b/yarn.lock index c6327991c5c9..82794acd68ae 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3669,11 +3669,6 @@ dependencies: "@babel/types" "^7.3.0" -"@types/blob-util@1.3.3": - version "1.3.3" - resolved "https://registry.yarnpkg.com/@types/blob-util/-/blob-util-1.3.3.tgz#adba644ae34f88e1dd9a5864c66ad651caaf628a" - integrity sha512-4ahcL/QDnpjWA2Qs16ZMQif7HjGP2cw3AGjHabybjw7Vm1EKu+cfQN1D78BaZbS1WJNa1opSMF5HNMztx7lR0w== - "@types/bluebird@*": version "3.5.31" resolved "https://registry.yarnpkg.com/@types/bluebird/-/bluebird-3.5.31.tgz#d17fa0ec242b51c3db302481c557ce3813bf45cb" @@ -6846,18 +6841,10 @@ black-hole-stream@0.0.1: resolved "https://registry.yarnpkg.com/black-hole-stream/-/black-hole-stream-0.0.1.tgz#33b7a06b9f1e7453d6041b82974481d2152aea42" integrity sha1-M7ega58edFPWBBuCl0SB0hUq6kI= -blob-util@1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/blob-util/-/blob-util-1.3.0.tgz#dbb4e8caffd50b5720d347e1169b6369ba34fe95" - integrity sha512-cjmYgWj8BQwoX+95rKkWvITL6PiEhSr19sX8qLRu+O6J2qmWmgUvxqhqJn425RFAwLovdDNnsCQ64RRHXjsXSg== - dependencies: - blob "0.0.4" - native-or-lie "1.0.2" - -blob@0.0.4: - version "0.0.4" - resolved "https://registry.yarnpkg.com/blob/-/blob-0.0.4.tgz#bcf13052ca54463f30f9fc7e95b9a47630a94921" - integrity sha1-vPEwUspURj8w+fx+lbmkdjCpSSE= +blob-util@2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/blob-util/-/blob-util-2.0.2.tgz#3b4e3c281111bb7f11128518006cdc60b403a1eb" + integrity sha512-T7JQa+zsXXEa6/8ZhHcQEW1UFfVM49Ts65uBkFL6fz2QmrElqmbajIDJvuA0tEhRe5eIjpV9ZF+0RfZR9voJFQ== blob@0.0.5: version "0.0.5" @@ -13984,11 +13971,6 @@ image-size@0.8.3, image-size@^0.8.2: dependencies: queue "6.0.1" -immediate@~3.0.5: - version "3.0.6" - resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b" - integrity sha1-nbHb0Pr43m++D13V5Wu2BigN5ps= - immutable@3.7.6: version "3.7.6" resolved "https://registry.yarnpkg.com/immutable/-/immutable-3.7.6.tgz#13b4d3cb12befa15482a26fe1b2ebae640071e4b" @@ -16164,13 +16146,6 @@ levn@^0.3.0, levn@~0.3.0: prelude-ls "~1.1.2" type-check "~0.3.2" -lie@*: - version "3.3.0" - resolved "https://registry.yarnpkg.com/lie/-/lie-3.3.0.tgz#dcf82dee545f46074daf200c7c1c5a08e0f40f6a" - integrity sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ== - dependencies: - immediate "~3.0.5" - liftoff@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/liftoff/-/liftoff-3.1.0.tgz#c9ba6081f908670607ee79062d700df062c52ed3" @@ -18010,13 +17985,6 @@ napi-build-utils@^1.0.1: resolved "https://registry.yarnpkg.com/napi-build-utils/-/napi-build-utils-1.0.2.tgz#b1fddc0b2c46e380a0b7a76f984dd47c41a13806" integrity sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg== -native-or-lie@1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/native-or-lie/-/native-or-lie-1.0.2.tgz#c870ee0ba0bf0ff11350595d216cfea68a6d8086" - integrity sha1-yHDuC6C/D/ETUFldIWz+poptgIY= - dependencies: - lie "*" - native-promise-only@~0.8.1: version "0.8.1" resolved "https://registry.yarnpkg.com/native-promise-only/-/native-promise-only-0.8.1.tgz#20a318c30cb45f71fe7adfbf7b21c99c1472ef11"