From 75119d91407b4c492432e8dabf54f893312afab0 Mon Sep 17 00:00:00 2001 From: Jennifer Shehane Date: Fri, 22 Jan 2021 10:46:37 +0630 Subject: [PATCH 001/134] initial commit From 803290329782324b00019faa4003ea4100448d23 Mon Sep 17 00:00:00 2001 From: Zach Bloomquist Date: Fri, 29 Jan 2021 18:19:35 +0000 Subject: [PATCH 002/134] feat: remove deprecated cy.route2 command (#14709) --- .../integration/commands/net_stubbing_spec.ts | 22 ------------------- packages/driver/src/cy/commands/querying.js | 2 +- packages/driver/src/cy/commands/waiting.js | 2 +- .../driver/src/cy/net-stubbing/add-command.ts | 12 +--------- packages/driver/src/cypress/error_messages.js | 1 - packages/net-stubbing/lib/external-types.ts | 8 ------- 6 files changed, 3 insertions(+), 44 deletions(-) diff --git a/packages/driver/cypress/integration/commands/net_stubbing_spec.ts b/packages/driver/cypress/integration/commands/net_stubbing_spec.ts index 827e2f8dff..8adeb83662 100644 --- a/packages/driver/cypress/integration/commands/net_stubbing_spec.ts +++ b/packages/driver/cypress/integration/commands/net_stubbing_spec.ts @@ -5,28 +5,6 @@ describe('network stubbing', { retries: 2 }, function () { cy.spy(Cypress.utils, 'warning') }) - context('cy.route2()', function () { - it('emits a warning', function () { - cy.route2('*') - .then(() => expect(Cypress.utils.warning).to.be.calledWith('`cy.route2()` was renamed to `cy.intercept()` and will be removed in a future release. Please update usages of `cy.route2()` to use `cy.intercept()` instead.')) - }) - - it('calls through to cy.intercept()', function (done) { - cy.route2('*', 'hello world').then(() => { - $.get('/abc123').done((responseText, _, xhr) => { - expect(responseText).to.eq('hello world') - - done() - }) - }) - }) - - it('can be used with cy.wait', function () { - cy.route2('*', 'hello world').as('foo') - .then(() => $.get('/abc123')).wait('@foo') - }) - }) - context('cy.intercept()', function () { beforeEach(function () { // we don't use cy.spy() because it causes an infinite loop with logging events diff --git a/packages/driver/src/cy/commands/querying.js b/packages/driver/src/cy/commands/querying.js index 94ceac9e9c..2f27124d92 100644 --- a/packages/driver/src/cy/commands/querying.js +++ b/packages/driver/src/cy/commands/querying.js @@ -274,7 +274,7 @@ module.exports = (Commands, Cypress, cy, state) => { return requests } - if (['route2', 'intercept'].includes(command.get('name'))) { + if (command.get('name') === 'intercept') { const requests = getAliasedRequests(alias, state) // detect alias.all and alias.index const specifier = /\.(all|[\d]+)$/.exec(selector) diff --git a/packages/driver/src/cy/commands/waiting.js b/packages/driver/src/cy/commands/waiting.js index f8d2b1fd55..96e5383e26 100644 --- a/packages/driver/src/cy/commands/waiting.js +++ b/packages/driver/src/cy/commands/waiting.js @@ -162,7 +162,7 @@ module.exports = (Commands, Cypress, cy, state) => { } const isNetworkInterceptCommand = (command) => { - const commandsThatCreateNetworkIntercepts = ['route', 'route2', 'intercept'] + const commandsThatCreateNetworkIntercepts = ['route', 'intercept'] const commandName = command.get('name') return commandsThatCreateNetworkIntercepts.includes(commandName) diff --git a/packages/driver/src/cy/net-stubbing/add-command.ts b/packages/driver/src/cy/net-stubbing/add-command.ts index b5a0d0e40f..5e636e1d4e 100644 --- a/packages/driver/src/cy/net-stubbing/add-command.ts +++ b/packages/driver/src/cy/net-stubbing/add-command.ts @@ -278,13 +278,6 @@ export function addCommand (Commands, Cypress: Cypress.Cypress, cy: Cypress.cy, return emitNetEvent('route:added', frame) } - function route2 (...args) { - $errUtils.warnByPath('net_stubbing.route2_renamed') - - // @ts-ignore - return intercept.apply(undefined, args) - } - function intercept (matcher: RouteMatcher, handler?: RouteHandler | StringMatcher, arg2?: RouteHandler) { function getMatcherOptions (): RouteMatcherOptions { if (_.isString(matcher) && $utils.isValidHttpMethod(matcher) && isStringMatcher(handler)) { @@ -322,8 +315,5 @@ export function addCommand (Commands, Cypress: Cypress.Cypress, cy: Cypress.cy, .then(() => null) } - Commands.addAll({ - intercept, - route2, - }) + Commands.addAll({ intercept }) } diff --git a/packages/driver/src/cypress/error_messages.js b/packages/driver/src/cypress/error_messages.js index 0b9082516c..175bc35485 100644 --- a/packages/driver/src/cypress/error_messages.js +++ b/packages/driver/src/cypress/error_messages.js @@ -915,7 +915,6 @@ module.exports = { }, net_stubbing: { - route2_renamed: `${cmd('route2')} was renamed to ${cmd('intercept')} and will be removed in a future release. Please update usages of ${cmd('route2')} to use ${cmd('intercept')} instead.`, invalid_static_response: ({ cmd, message, staticResponse }) => { return cyStripIndent(`\ An invalid StaticResponse was supplied to \`${cmd}()\`. ${message} diff --git a/packages/net-stubbing/lib/external-types.ts b/packages/net-stubbing/lib/external-types.ts index 358478bfa1..8d129db6d5 100644 --- a/packages/net-stubbing/lib/external-types.ts +++ b/packages/net-stubbing/lib/external-types.ts @@ -393,14 +393,6 @@ declare global { * cy.intercept('GET', 'http://foo.com/fruits', ['apple', 'banana', 'cherry']) */ intercept(method: Method, url: RouteMatcher, response?: RouteHandler): Chainable - /** - * @deprecated Use `cy.intercept()` instead. - */ - route2(url: RouteMatcher, response?: RouteHandler): Chainable - /** - * @deprecated Use `cy.intercept()` instead. - */ - route2(method: Method, url: RouteMatcher, response?: RouteHandler): Chainable /** * Wait for a specific request to complete. * From 8303457b987297aaa53c0c2549eb3bf83472bb79 Mon Sep 17 00:00:00 2001 From: Jennifer Shehane Date: Tue, 2 Feb 2021 13:24:38 +0630 Subject: [PATCH 003/134] feat(breaking): Remove Cypress.moment() (#14729) --- cli/package.json | 1 - cli/types/cy-moment.d.ts | 7 ----- cli/types/cypress.d.ts | 12 -------- cli/types/index.d.ts | 1 - cli/types/tests/cypress-tests.ts | 8 ------ packages/desktop-gui/index.d.ts | 1 - .../commands/actions/type_errors_spec.js | 23 +++++++-------- .../integration/commands/actions/type_spec.js | 2 +- .../cypress/integration/util/moment_spec.js | 28 ------------------- packages/driver/package.json | 2 +- packages/driver/src/config/moment.js | 19 ------------- packages/driver/src/cy/keyboard.ts | 9 ++++-- packages/driver/src/cypress.js | 24 ---------------- packages/driver/src/cypress/error_messages.js | 12 +++----- packages/driver/src/cypress/runner.js | 4 +-- packages/driver/src/cypress/utils.js | 4 +-- packages/driver/src/main.js | 1 - packages/reporter/index.d.ts | 1 - packages/runner/index.d.ts | 1 - .../e2e/cypress/integration/request_spec.js | 4 ++- packages/ui-components/index.d.ts | 1 - yarn.lock | 10 +++---- 22 files changed, 37 insertions(+), 138 deletions(-) delete mode 100644 cli/types/cy-moment.d.ts delete mode 100644 packages/driver/cypress/integration/util/moment_spec.js delete mode 100644 packages/driver/src/config/moment.js diff --git a/cli/package.json b/cli/package.json index 7de3045a15..0e94a58600 100644 --- a/cli/package.json +++ b/cli/package.json @@ -49,7 +49,6 @@ "lodash": "^4.17.19", "log-symbols": "^4.0.0", "minimist": "^1.2.5", - "moment": "^2.29.1", "ospath": "^1.2.2", "pretty-bytes": "^5.4.1", "ramda": "~0.26.1", diff --git a/cli/types/cy-moment.d.ts b/cli/types/cy-moment.d.ts deleted file mode 100644 index d68f10b9b5..0000000000 --- a/cli/types/cy-moment.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -import moment = require('moment') -export = Moment -export as namespace Moment - -declare namespace Moment { - type MomentStatic = typeof moment -} diff --git a/cli/types/cypress.d.ts b/cli/types/cypress.d.ts index c0aee1ac19..88ec37cc32 100644 --- a/cli/types/cypress.d.ts +++ b/cli/types/cypress.d.ts @@ -176,18 +176,6 @@ declare namespace Cypress { * @see https://on.cypress.io/minimatch */ minimatch: typeof Minimatch.minimatch - /** - * @deprecated Will be removed in a future version. - * Consider including your own datetime formatter in your tests. - * - * Cypress automatically includes moment.js and exposes it as Cypress.moment. - * - * @see https://on.cypress.io/moment - * @see http://momentjs.com/ - * @example - * const todaysDate = Cypress.moment().format("MMM DD, YYYY") - */ - moment: Moment.MomentStatic /** * Cypress automatically includes Bluebird and exposes it as Cypress.Promise. * diff --git a/cli/types/index.d.ts b/cli/types/index.d.ts index c66f78aa11..607d5df14a 100644 --- a/cli/types/index.d.ts +++ b/cli/types/index.d.ts @@ -9,7 +9,6 @@ /// /// -/// /// /// /// diff --git a/cli/types/tests/cypress-tests.ts b/cli/types/tests/cypress-tests.ts index 000cf8fb83..c2ce047ec1 100644 --- a/cli/types/tests/cypress-tests.ts +++ b/cli/types/tests/cypress-tests.ts @@ -5,14 +5,6 @@ namespace CypressLodashTests { }) } -namespace CypressMomentTests { - Cypress.moment() // $ExpectType Moment - Cypress.moment('1982-08-23') // $ExpectType Moment - Cypress.moment(Date()) // $ExpectType Moment - Cypress.moment(Date()).format() // $ExpectType string - Cypress.moment().startOf('week') // $ExpectType Moment -} - namespace CypressSinonTests { Cypress.sinon // $ExpectType SinonStatic diff --git a/packages/desktop-gui/index.d.ts b/packages/desktop-gui/index.d.ts index 821afbbe33..f310b6a354 100644 --- a/packages/desktop-gui/index.d.ts +++ b/packages/desktop-gui/index.d.ts @@ -1,6 +1,5 @@ /// /// -/// /// /// diff --git a/packages/driver/cypress/integration/commands/actions/type_errors_spec.js b/packages/driver/cypress/integration/commands/actions/type_errors_spec.js index eb0f69d4c5..9422d35b3c 100644 --- a/packages/driver/cypress/integration/commands/actions/type_errors_spec.js +++ b/packages/driver/cypress/integration/commands/actions/type_errors_spec.js @@ -324,7 +324,7 @@ If you want to skip parsing special character sequences and type the text exactl it('throws when chars is not a string', function (done) { cy.on('fail', (err) => { expect(this.logs.length).to.eq(2) - expect(err.message).to.eq('Typing into a `date` input with `cy.type()` requires a valid date with the format `yyyy-MM-dd`. You passed: `1989`') + expect(err.message).to.eq('Typing into a `date` input with `cy.type()` requires a valid date with the format `YYYY-MM-DD`. You passed: `1989`') expect(err.docsUrl).to.eq('https://on.cypress.io/type') done() }) @@ -335,7 +335,7 @@ If you want to skip parsing special character sequences and type the text exactl it('throws when chars is invalid format', function (done) { cy.on('fail', (err) => { expect(this.logs.length).to.eq(2) - expect(err.message).to.eq('Typing into a `date` input with `cy.type()` requires a valid date with the format `yyyy-MM-dd`. You passed: `01-01-1989`') + expect(err.message).to.eq('Typing into a `date` input with `cy.type()` requires a valid date with the format `YYYY-MM-DD`. You passed: `01-01-1989`') expect(err.docsUrl).to.eq('https://on.cypress.io/type') done() }) @@ -343,14 +343,15 @@ If you want to skip parsing special character sequences and type the text exactl cy.get('#date-without-value').type('01-01-1989') }) - it('throws when chars is invalid date', function (done) { + it('throws when chars is non-existent date', function (done) { cy.on('fail', (err) => { expect(this.logs.length).to.eq(2) - expect(err.message).to.eq('Typing into a `date` input with `cy.type()` requires a valid date with the format `yyyy-MM-dd`. You passed: `1989-04-31`') + expect(err.message).to.eq('Typing into a `date` input with `cy.type()` requires a valid date with the format `YYYY-MM-DD`. You passed: `1989-04-31`') expect(err.docsUrl).to.eq('https://on.cypress.io/type') done() }) + // there are not 31 days in April cy.get('#date-without-value').type('1989-04-31') }) }) @@ -359,7 +360,7 @@ If you want to skip parsing special character sequences and type the text exactl it('throws when chars is not a string', function (done) { cy.on('fail', (err) => { expect(this.logs.length).to.eq(2) - expect(err.message).to.eq('Typing into a `month` input with `cy.type()` requires a valid month with the format `yyyy-MM`. You passed: `6`') + expect(err.message).to.eq('Typing into a `month` input with `cy.type()` requires a valid month with the format `YYYY-MM`. You passed: `6`') expect(err.docsUrl).to.eq('https://on.cypress.io/type') done() }) @@ -370,7 +371,7 @@ If you want to skip parsing special character sequences and type the text exactl it('throws when chars is invalid format', function (done) { cy.on('fail', (err) => { expect(this.logs.length).to.eq(2) - expect(err.message).to.eq('Typing into a `month` input with `cy.type()` requires a valid month with the format `yyyy-MM`. You passed: `01/2000`') + expect(err.message).to.eq('Typing into a `month` input with `cy.type()` requires a valid month with the format `YYYY-MM`. You passed: `01/2000`') expect(err.docsUrl).to.eq('https://on.cypress.io/type') done() }) @@ -381,7 +382,7 @@ If you want to skip parsing special character sequences and type the text exactl it('throws when chars is invalid month', function (done) { cy.on('fail', (err) => { expect(this.logs.length).to.eq(2) - expect(err.message).to.eq('Typing into a `month` input with `cy.type()` requires a valid month with the format `yyyy-MM`. You passed: `1989-13`') + expect(err.message).to.eq('Typing into a `month` input with `cy.type()` requires a valid month with the format `YYYY-MM`. You passed: `1989-13`') expect(err.docsUrl).to.eq('https://on.cypress.io/type') done() }) @@ -401,14 +402,14 @@ If you want to skip parsing special character sequences and type the text exactl // it "throws when chars is invalid format", (done) -> // cy.on "fail", (err) => // expect(@logs.length).to.eq(2) - // expect(err.message).to.eq("Typing into a week input with cy.type() requires a valid week with the format 'yyyy-Www', where W is the literal character 'W' and ww is the week number (00-53). You passed: 2005/W18") + // expect(err.message).to.eq("Typing into a week input with cy.type() requires a valid week with the format 'YYYY-Www', where W is the literal character 'W' and ww is the week number (00-53). You passed: 2005/W18") // done() context('[type=week]', () => { it('throws when chars is not a string', function (done) { cy.on('fail', (err) => { expect(this.logs.length).to.eq(2) - expect(err.message).to.eq('Typing into a `week` input with `cy.type()` requires a valid week with the format `yyyy-Www`, where `W` is the literal character `W` and `ww` is the week number (00-53). You passed: `23`') + expect(err.message).to.eq('Typing into a `week` input with `cy.type()` requires a valid week with the format `YYYY-Www`, where `W` is the literal character `W` and `ww` is the week number (00-53). You passed: `23`') expect(err.docsUrl).to.eq('https://on.cypress.io/type') done() }) @@ -419,7 +420,7 @@ If you want to skip parsing special character sequences and type the text exactl it('throws when chars is invalid format', function (done) { cy.on('fail', (err) => { expect(this.logs.length).to.eq(2) - expect(err.message).to.eq('Typing into a `week` input with `cy.type()` requires a valid week with the format `yyyy-Www`, where `W` is the literal character `W` and `ww` is the week number (00-53). You passed: `2005/W18`') + expect(err.message).to.eq('Typing into a `week` input with `cy.type()` requires a valid week with the format `YYYY-Www`, where `W` is the literal character `W` and `ww` is the week number (00-53). You passed: `2005/W18`') expect(err.docsUrl).to.eq('https://on.cypress.io/type') done() }) @@ -430,7 +431,7 @@ If you want to skip parsing special character sequences and type the text exactl it('throws when chars is invalid week', function (done) { cy.on('fail', (err) => { expect(this.logs.length).to.eq(2) - expect(err.message).to.eq('Typing into a `week` input with `cy.type()` requires a valid week with the format `yyyy-Www`, where `W` is the literal character `W` and `ww` is the week number (00-53). You passed: `1995-W60`') + expect(err.message).to.eq('Typing into a `week` input with `cy.type()` requires a valid week with the format `YYYY-Www`, where `W` is the literal character `W` and `ww` is the week number (00-53). You passed: `1995-W60`') expect(err.docsUrl).to.eq('https://on.cypress.io/type') done() }) diff --git a/packages/driver/cypress/integration/commands/actions/type_spec.js b/packages/driver/cypress/integration/commands/actions/type_spec.js index 19d299db82..cb7a48b902 100644 --- a/packages/driver/cypress/integration/commands/actions/type_spec.js +++ b/packages/driver/cypress/integration/commands/actions/type_spec.js @@ -1608,7 +1608,7 @@ describe('src/cy/commands/actions/type - #type', () => { it('errors when invalid datetime', (done) => { cy.on('fail', (err) => { expect(err.message).contain('datetime') - expect(err.message).contain('yyyy-MM-ddThh:mm') + expect(err.message).contain('YYYY-MM-DDThh:mm') done() }) diff --git a/packages/driver/cypress/integration/util/moment_spec.js b/packages/driver/cypress/integration/util/moment_spec.js deleted file mode 100644 index fb8827ad81..0000000000 --- a/packages/driver/cypress/integration/util/moment_spec.js +++ /dev/null @@ -1,28 +0,0 @@ -context('#moment', () => { - it('logs deprecation warning', () => { - cy.stub(Cypress.utils, 'warning') - - Cypress.moment() - expect(Cypress.utils.warning).to.be.calledWith('`Cypress.moment` has been deprecated and will be removed in a future release. Consider migrating to a different datetime formatter.') - }) - - it('still has other moment properties', () => { - expect(Cypress.moment.duration).to.be.a('function') - expect(Cypress.moment.isDate()).to.be.false - expect(Cypress.moment.isDate(new Date())).to.be.true - }) - - it('logs deprecation warning when using duration', () => { - cy.stub(Cypress.utils, 'warning') - - Cypress.moment.duration() - expect(Cypress.utils.warning).to.be.calledWith('`Cypress.moment` has been deprecated and will be removed in a future release. Consider migrating to a different datetime formatter.') - }) - - it('logs deprecation warning when using isDate', () => { - cy.stub(Cypress.utils, 'warning') - - Cypress.moment.isDate() - expect(Cypress.utils.warning).to.be.calledWith('`Cypress.moment` has been deprecated and will be removed in a future release. Consider migrating to a different datetime formatter.') - }) -}) diff --git a/packages/driver/package.json b/packages/driver/package.json index d13fb31e10..1085569f4a 100644 --- a/packages/driver/package.json +++ b/packages/driver/package.json @@ -41,6 +41,7 @@ "compression": "1.7.4", "cors": "2.8.5", "cypress-multi-reporters": "1.4.0", + "dayjs": "^1.10.3", "debug": "4.3.1", "error-stack-parser": "2.0.6", "errorhandler": "1.5.1", @@ -58,7 +59,6 @@ "minimatch": "3.0.4", "minimist": "1.2.5", "mocha": "7.0.1", - "moment": "2.29.1", "morgan": "1.9.1", "ordinal": "1.0.3", "react-15.6.1": "npm:react@15.6.1", diff --git a/packages/driver/src/config/moment.js b/packages/driver/src/config/moment.js deleted file mode 100644 index 6abbae37af..0000000000 --- a/packages/driver/src/config/moment.js +++ /dev/null @@ -1,19 +0,0 @@ -const moment = require('moment') - -moment.updateLocale('en', { - relativeTime: { - future: 'in %s', - past: '%s ago', - s: 's', - m: '1m', - mm: '%dm', - h: '1h', - hh: '%dh', - d: '1d', - dd: '%dd', - M: '1mo', - MM: '%dmo', - y: '1y', - yy: '%dy', - }, -}) diff --git a/packages/driver/src/cy/keyboard.ts b/packages/driver/src/cy/keyboard.ts index 1dcdb03c2e..0ea6c34a06 100644 --- a/packages/driver/src/cy/keyboard.ts +++ b/packages/driver/src/cy/keyboard.ts @@ -1,7 +1,7 @@ import Promise from 'bluebird' import Debug from 'debug' import _ from 'lodash' -import moment from 'moment' +import dayjs from 'dayjs' import $errUtils from '../cypress/error_utils' import { USKeyboard } from '../cypress/UsKeyboardLayout' import * as $dom from '../dom' @@ -473,10 +473,15 @@ const validateTyping = ( if (isDate) { dateChars = dateRe.exec(chars) + const dateExists = (date) => { + // dayjs rounds up dates that don't exist to valid dates + return dayjs(date, 'YYYY-MM-DD').format('YYYY-MM-DD') === date + } + if ( _.isString(chars) && dateChars && - moment(dateChars[0]).isValid() + dateExists(dateChars[0]) ) { skipCheckUntilIndex = _getEndIndex(chars, dateChars[0]) diff --git a/packages/driver/src/cypress.js b/packages/driver/src/cypress.js index db85922a1f..d25f66343c 100644 --- a/packages/driver/src/cypress.js +++ b/packages/driver/src/cypress.js @@ -2,7 +2,6 @@ const _ = require('lodash') const $ = require('jquery') const blobUtil = require('blob-util') const minimatch = require('minimatch') -const moment = require('moment') const Promise = require('bluebird') const sinon = require('sinon') const lolex = require('lolex') @@ -566,28 +565,6 @@ class $Cypress { } } -function wrapMoment (moment) { - function deprecatedFunction (...args) { - $errUtils.warnByPath('moment.deprecated') - - return moment.apply(moment, args) - } - // copy all existing properties from "moment" like "moment.duration" - _.keys(moment).forEach((key) => { - const value = moment[key] - - if (_.isFunction(value)) { - // recursively wrap any property that can be called by the user - // so that Cypress.moment.duration() shows deprecated message - deprecatedFunction[key] = wrapMoment(value) - } else { - deprecatedFunction[key] = value - } - }) - - return deprecatedFunction -} - // attach to $Cypress to access // all of the constructors // to enable users to monkeypatch @@ -613,7 +590,6 @@ $Cypress.prototype.Screenshot = $Screenshot $Cypress.prototype.SelectorPlayground = $SelectorPlayground $Cypress.prototype.utils = $utils $Cypress.prototype._ = _ -$Cypress.prototype.moment = wrapMoment(moment) $Cypress.prototype.Blob = blobUtil $Cypress.prototype.Promise = Promise $Cypress.prototype.minimatch = minimatch diff --git a/packages/driver/src/cypress/error_messages.js b/packages/driver/src/cypress/error_messages.js index 175bc35485..4f43b109c0 100644 --- a/packages/driver/src/cypress/error_messages.js +++ b/packages/driver/src/cypress/error_messages.js @@ -872,10 +872,6 @@ module.exports = { }, - moment: { - deprecated: `\`Cypress.moment\` has been deprecated and will be removed in a future release. Consider migrating to a different datetime formatter.`, - }, - navigation: { cross_origin ({ message, originPolicy, configFile }) { return { @@ -1620,15 +1616,15 @@ module.exports = { docsUrl: 'https://on.cypress.io/type', }, invalid_date: { - message: `Typing into a \`date\` input with ${cmd('type')} requires a valid date with the format \`yyyy-MM-dd\`. You passed: \`{{chars}}\``, + message: `Typing into a \`date\` input with ${cmd('type')} requires a valid date with the format \`YYYY-MM-DD\`. You passed: \`{{chars}}\``, docsUrl: 'https://on.cypress.io/type', }, invalid_datetime: { - message: `Typing into a datetime input with ${cmd('type')} requires a valid datetime with the format \`yyyy-MM-ddThh:mm\`, for example \`2017-06-01T08:30\`. You passed: \`{{chars}}\``, + message: `Typing into a datetime input with ${cmd('type')} requires a valid datetime with the format \`YYYY-MM-DDThh:mm\`, for example \`2017-06-01T08:30\`. You passed: \`{{chars}}\``, docsUrl: 'https://on.cypress.io/type', }, invalid_month: { - message: `Typing into a \`month\` input with ${cmd('type')} requires a valid month with the format \`yyyy-MM\`. You passed: \`{{chars}}\``, + message: `Typing into a \`month\` input with ${cmd('type')} requires a valid month with the format \`YYYY-MM\`. You passed: \`{{chars}}\``, docsUrl: 'https://on.cypress.io/type', }, invalid_time: { @@ -1636,7 +1632,7 @@ module.exports = { docsUrl: 'https://on.cypress.io/type', }, invalid_week: { - message: `Typing into a \`week\` input with ${cmd('type')} requires a valid week with the format \`yyyy-Www\`, where \`W\` is the literal character \`W\` and \`ww\` is the week number (00-53). You passed: \`{{chars}}\``, + message: `Typing into a \`week\` input with ${cmd('type')} requires a valid week with the format \`YYYY-Www\`, where \`W\` is the literal character \`W\` and \`ww\` is the week number (00-53). You passed: \`{{chars}}\``, docsUrl: 'https://on.cypress.io/type', }, multiple_elements: { diff --git a/packages/driver/src/cypress/runner.js b/packages/driver/src/cypress/runner.js index 0d638615ad..49115efe1c 100644 --- a/packages/driver/src/cypress/runner.js +++ b/packages/driver/src/cypress/runner.js @@ -1,7 +1,7 @@ /* eslint-disable prefer-rest-params */ /* globals Cypress */ const _ = require('lodash') -const moment = require('moment') +const dayjs = require('dayjs') const Promise = require('bluebird') const Pending = require('mocha/lib/pending') @@ -1274,7 +1274,7 @@ const create = (specWindow, mocha, Cypress, cy) => { run (fn) { if (_startTime == null) { - _startTime = moment().toJSON() + _startTime = dayjs().toJSON() } _runnerListeners(_runner, Cypress, _emissions, getTestById, getTest, setTest, getTestFromHookOrFindTest) diff --git a/packages/driver/src/cypress/utils.js b/packages/driver/src/cypress/utils.js index 2b77cb8c0c..efb16e3b58 100644 --- a/packages/driver/src/cypress/utils.js +++ b/packages/driver/src/cypress/utils.js @@ -1,7 +1,7 @@ const _ = require('lodash') const capitalize = require('underscore.string/capitalize') const methods = require('methods') -const moment = require('moment') +const dayjs = require('dayjs') const $jquery = require('../dom/jquery') const $Location = require('./location') @@ -280,7 +280,7 @@ module.exports = { }, addTwentyYears () { - return moment().add(20, 'years').unix() + return dayjs().add(20, 'year').unix() }, locReload (forceReload, win) { diff --git a/packages/driver/src/main.js b/packages/driver/src/main.js index 7043fcd6f8..6c01e575e8 100644 --- a/packages/driver/src/main.js +++ b/packages/driver/src/main.js @@ -3,6 +3,5 @@ require('setimmediate') require('./config/bluebird') require('./config/jquery') require('./config/lodash') -require('./config/moment') module.exports = require('./cypress') diff --git a/packages/reporter/index.d.ts b/packages/reporter/index.d.ts index 1c75cc27df..3c6f8f3f73 100644 --- a/packages/reporter/index.d.ts +++ b/packages/reporter/index.d.ts @@ -1,6 +1,5 @@ /// /// -/// /// /// diff --git a/packages/runner/index.d.ts b/packages/runner/index.d.ts index 821afbbe33..f310b6a354 100644 --- a/packages/runner/index.d.ts +++ b/packages/runner/index.d.ts @@ -1,6 +1,5 @@ /// /// -/// /// /// diff --git a/packages/server/test/support/fixtures/projects/e2e/cypress/integration/request_spec.js b/packages/server/test/support/fixtures/projects/e2e/cypress/integration/request_spec.js index c1577c5de4..f6f8229555 100644 --- a/packages/server/test/support/fixtures/projects/e2e/cypress/integration/request_spec.js +++ b/packages/server/test/support/fixtures/projects/e2e/cypress/integration/request_spec.js @@ -1,6 +1,8 @@ +const dayjs = require('dayjs') + describe('redirects + requests', () => { it('gets and sets cookies from cy.request', () => { - const oneMinuteFromNow = Cypress.moment().add(1, 'minute').unix() + const oneMinuteFromNow = dayjs().add(1, 'minute').unix() cy .request('http://localhost:2293/') diff --git a/packages/ui-components/index.d.ts b/packages/ui-components/index.d.ts index 821afbbe33..f310b6a354 100644 --- a/packages/ui-components/index.d.ts +++ b/packages/ui-components/index.d.ts @@ -1,6 +1,5 @@ /// /// -/// /// /// diff --git a/yarn.lock b/yarn.lock index df920f173e..802a9e5461 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12379,6 +12379,11 @@ dateformat@^3.0.0, dateformat@^3.0.2: resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-3.0.3.tgz#a6e37499a4d9a9cf85ef5872044d62901c9889ae" integrity sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q== +dayjs@^1.10.3: + version "1.10.3" + resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.10.3.tgz#cf3357c8e7f508432826371672ebf376cb7d619b" + integrity sha512-/2fdLN987N8Ki7Id8BUN2nhuiRyxTLumQnSQf9CNncFCyqFsSKb9TNhzRYcC8K8eJSJOKvbvkImo/MKKhNi4iw== + dayjs@^1.9.3: version "1.9.3" resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.9.3.tgz#b7f94b22ad2a136a4ca02a01ab68ae893fe1a268" @@ -22960,11 +22965,6 @@ moment@2.17.0: resolved "https://registry.yarnpkg.com/moment/-/moment-2.17.0.tgz#a4c292e02aac5ddefb29a6eed24f51938dd3b74f" integrity sha1-pMKS4CqsXd77Kabu0k9Rk43Tt08= -moment@2.29.1, moment@^2.29.1: - version "2.29.1" - resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.1.tgz#b2be769fa31940be9eeea6469c075e35006fa3d3" - integrity sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ== - moment@^2.27.0, moment@^2.9.0: version "2.29.0" resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.0.tgz#fcbef955844d91deb55438613ddcec56e86a3425" From 70fc07e65a3cb20f4fb000ccb9d2ee7c3bb06b98 Mon Sep 17 00:00:00 2001 From: Chris Breiding Date: Fri, 12 Feb 2021 10:25:45 -0500 Subject: [PATCH 004/134] remove outdated references to moment types --- packages/server-ct/index.d.ts | 1 - packages/server/index.d.ts | 1 - yarn.lock | 40 +++++++++-------------------------- 3 files changed, 10 insertions(+), 32 deletions(-) diff --git a/packages/server-ct/index.d.ts b/packages/server-ct/index.d.ts index de42a61e4b..0682ddcedd 100644 --- a/packages/server-ct/index.d.ts +++ b/packages/server-ct/index.d.ts @@ -1,6 +1,5 @@ /// /// -/// /// /// /// diff --git a/packages/server/index.d.ts b/packages/server/index.d.ts index 71423d08b6..3eb68df85e 100644 --- a/packages/server/index.d.ts +++ b/packages/server/index.d.ts @@ -1,6 +1,5 @@ /// /// -/// /// /// /// diff --git a/yarn.lock b/yarn.lock index a7a1af626a..d8805e2639 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12885,7 +12885,7 @@ debug@^3.0.0, debug@^3.1.0, debug@^3.1.1, debug@^3.2.5, debug@^3.2.6: dependencies: ms "^2.1.1" -debuglog@*, debuglog@^1.0.1: +debuglog@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492" integrity sha1-qiT/uaw9+aI1GDfPstJ5NgzXhJI= @@ -18802,7 +18802,7 @@ import-local@2.0.0, import-local@^2.0.0: pkg-dir "^3.0.0" resolve-cwd "^2.0.0" -imurmurhash@*, imurmurhash@^0.1.4: +imurmurhash@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= @@ -21969,11 +21969,6 @@ lodash._basecreate@^3.0.0: resolved "https://registry.yarnpkg.com/lodash._basecreate/-/lodash._basecreate-3.0.3.tgz#1bc661614daa7fc311b7d03bf16806a0213cf821" integrity sha1-G8ZhYU2qf8MRt9A78WgGoCE8+CE= -lodash._baseindexof@*: - version "3.1.0" - resolved "https://registry.yarnpkg.com/lodash._baseindexof/-/lodash._baseindexof-3.1.0.tgz#fe52b53a1c6761e42618d654e4a25789ed61822c" - integrity sha1-/lK1OhxnYeQmGNZU5KJXie1hgiw= - lodash._baseuniq@~4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/lodash._baseuniq/-/lodash._baseuniq-4.6.0.tgz#0ebb44e456814af7905c6212fa2c9b2d51b841e8" @@ -21982,29 +21977,12 @@ lodash._baseuniq@~4.6.0: lodash._createset "~4.0.0" lodash._root "~3.0.0" -lodash._bindcallback@*: - version "3.0.1" - resolved "https://registry.yarnpkg.com/lodash._bindcallback/-/lodash._bindcallback-3.0.1.tgz#e531c27644cf8b57a99e17ed95b35c748789392e" - integrity sha1-5THCdkTPi1epnhftlbNcdIeJOS4= - -lodash._cacheindexof@*: - version "3.0.2" - resolved "https://registry.yarnpkg.com/lodash._cacheindexof/-/lodash._cacheindexof-3.0.2.tgz#3dc69ac82498d2ee5e3ce56091bafd2adc7bde92" - integrity sha1-PcaayCSY0u5ePOVgkbr9Ktx73pI= - -lodash._createcache@*: - version "3.1.2" - resolved "https://registry.yarnpkg.com/lodash._createcache/-/lodash._createcache-3.1.2.tgz#56d6a064017625e79ebca6b8018e17440bdcf093" - integrity sha1-VtagZAF2JeeevKa4AY4XRAvc8JM= - dependencies: - lodash._getnative "^3.0.0" - lodash._createset@~4.0.0: version "4.0.3" resolved "https://registry.yarnpkg.com/lodash._createset/-/lodash._createset-4.0.3.tgz#0f4659fbb09d75194fa9e2b88a6644d363c9fe26" integrity sha1-D0ZZ+7CddRlPqeK4imZE02PJ/iY= -lodash._getnative@*, lodash._getnative@^3.0.0: +lodash._getnative@^3.0.0: version "3.9.1" resolved "https://registry.yarnpkg.com/lodash._getnative/-/lodash._getnative-3.9.1.tgz#570bc7dede46d61cdcde687d65d3eecbaa3aaff5" integrity sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U= @@ -22202,11 +22180,6 @@ lodash.reduce@^4.6.0: resolved "https://registry.yarnpkg.com/lodash.reduce/-/lodash.reduce-4.6.0.tgz#f1ab6b839299ad48f784abbf476596f03b914d3b" integrity sha1-8atrg5KZrUj3hKu/R2WW8DuRTTs= -lodash.restparam@*: - version "3.6.1" - resolved "https://registry.yarnpkg.com/lodash.restparam/-/lodash.restparam-3.6.1.tgz#936a4e309ef330a7645ed4145986c85ae5b20805" - integrity sha1-k2pOMJ7zMKdkXtQUWYbIWuWyCAU= - lodash.set@4.3.2, lodash.set@^4.3.2: version "4.3.2" resolved "https://registry.yarnpkg.com/lodash.set/-/lodash.set-4.3.2.tgz#d8757b1da807dde24816b0d6a84bea1a76230b23" @@ -29573,6 +29546,13 @@ rollup@^2.35.1: optionalDependencies: fsevents "~2.1.2" +rollup@^2.38.5: + version "2.38.5" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.38.5.tgz#be41ad4fe0c103a8794377afceb5f22b8f603d6a" + integrity sha512-VoWt8DysFGDVRGWuHTqZzT02J0ASgjVq/hPs9QcBOGMd7B+jfTr/iqMVEyOi901rE3xq+Deq66GzIT1yt7sGwQ== + optionalDependencies: + fsevents "~2.3.1" + rst-selector-parser@^2.2.3: version "2.2.3" resolved "https://registry.yarnpkg.com/rst-selector-parser/-/rst-selector-parser-2.2.3.tgz#81b230ea2fcc6066c89e3472de794285d9b03d91" From 098c3027dd0757db797dd79590d605da6571aa83 Mon Sep 17 00:00:00 2001 From: Kukhyeon Heo Date: Thu, 18 Feb 2021 03:46:54 +0900 Subject: [PATCH 005/134] feat: Throw an error when there's a redirection loop. (#14643) --- cli/schema/cypress.schema.json | 5 ++ cli/types/cypress.d.ts | 5 ++ .../cypress/fixtures/redirection-loop-a.html | 15 +++++ .../cypress/fixtures/redirection-loop-b.html | 15 +++++ .../integration/commands/navigation_spec.js | 12 ++++ packages/driver/src/cy/commands/navigation.js | 60 +++++++++++++++---- packages/driver/src/cypress/error_messages.js | 6 ++ packages/server/lib/config_options.ts | 3 + packages/server/test/unit/config_spec.js | 2 + 9 files changed, 112 insertions(+), 11 deletions(-) create mode 100644 packages/driver/cypress/fixtures/redirection-loop-a.html create mode 100644 packages/driver/cypress/fixtures/redirection-loop-b.html diff --git a/cli/schema/cypress.schema.json b/cli/schema/cypress.schema.json index fb11141fcf..0b77b9f851 100644 --- a/cli/schema/cypress.schema.json +++ b/cli/schema/cypress.schema.json @@ -185,6 +185,11 @@ "default": null, "description": "Enables you to override the default user agent the browser sends in all request headers. User agent values are typically used by servers to help identify the operating system, browser, and browser version. See User-Agent MDN Documentation for example user agent values here: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/User-Agent" }, + "redirectionLimit": { + "type": "number", + "default": 20, + "description": "The application under test cannot redirect more than this limit." + }, "blockHosts": { "type": [ "string", diff --git a/cli/types/cypress.d.ts b/cli/types/cypress.d.ts index b0f7dc34ca..675efd64bc 100644 --- a/cli/types/cypress.d.ts +++ b/cli/types/cypress.d.ts @@ -2483,6 +2483,11 @@ declare namespace Cypress { * @default "cypress/plugins/index.js" */ pluginsFile: string | false + /** + * The application under test cannot redirect more than this limit. + * @default 20 + */ + redirectionLimit: number /** * If `nodeVersion === 'system'` and a `node` executable is found, this will be the full filesystem path to that executable. * @default null diff --git a/packages/driver/cypress/fixtures/redirection-loop-a.html b/packages/driver/cypress/fixtures/redirection-loop-a.html new file mode 100644 index 0000000000..2eec7bb0d9 --- /dev/null +++ b/packages/driver/cypress/fixtures/redirection-loop-a.html @@ -0,0 +1,15 @@ + + + Page Redirect Loop + + + + Page A + + diff --git a/packages/driver/cypress/fixtures/redirection-loop-b.html b/packages/driver/cypress/fixtures/redirection-loop-b.html new file mode 100644 index 0000000000..ff575bcc6b --- /dev/null +++ b/packages/driver/cypress/fixtures/redirection-loop-b.html @@ -0,0 +1,15 @@ + + + Page Redirect Loop + + + + Page B + + diff --git a/packages/driver/cypress/integration/commands/navigation_spec.js b/packages/driver/cypress/integration/commands/navigation_spec.js index 40f8aee942..f9977b78a6 100644 --- a/packages/driver/cypress/integration/commands/navigation_spec.js +++ b/packages/driver/cypress/integration/commands/navigation_spec.js @@ -714,6 +714,18 @@ describe('src/cy/commands/navigation', () => { cy.visit('http://localhost:3500/undefined-content-type') }) + // https://github.com/cypress-io/cypress/issues/14445 + it('should eventually fail on assertion despite redirects', (done) => { + cy.on('fail', (err) => { + expect(err.message).to.contain('The application redirected to') + + done() + }) + + cy.visit('fixtures/redirection-loop-a.html') + cy.get('div').should('contain', 'this should fail?') + }) + describe('when only hashes are changing', () => { it('short circuits the visit if the page will not refresh', () => { let count = 0 diff --git a/packages/driver/src/cy/commands/navigation.js b/packages/driver/src/cy/commands/navigation.js index a597ea4979..c5b910cd6b 100644 --- a/packages/driver/src/cy/commands/navigation.js +++ b/packages/driver/src/cy/commands/navigation.js @@ -296,7 +296,39 @@ const stabilityChanged = (Cypress, state, config, stable) => { state('onPageLoadErr', onPageLoadErr) + const getRedirectionCount = (href) => { + // object updated at test:before:run:async + const count = state('redirectionCount') + + if (count[href] === undefined) { + count[href] = 0 + } + + return count[href] + } + + const updateRedirectionCount = (href) => { + const count = state('redirectionCount') + + count[href]++ + } + const loading = () => { + const href = state('window').location.href + const count = getRedirectionCount(href) + const limit = config('redirectionLimit') + + if (count === limit) { + $errUtils.throwErrByPath('navigation.reached_redirection_limit', { + args: { + href, + limit, + }, + }) + } + + updateRedirectionCount(href) + debug('waiting for window:load') return new Promise((resolve) => { @@ -318,18 +350,22 @@ const stabilityChanged = (Cypress, state, config, stable) => { } } - return loading() - .timeout(options.timeout, 'page load') - .catch(Promise.TimeoutError, () => { - // clean this up - cy.state('onPageLoadErr', null) + try { + return loading() + .timeout(options.timeout, 'page load') + .catch(Promise.TimeoutError, () => { + // clean this up + cy.state('onPageLoadErr', null) - try { - return timedOutWaitingForPageLoad(options.timeout, options._log) - } catch (err) { - return reject(err) - } - }) + try { + return timedOutWaitingForPageLoad(options.timeout, options._log) + } catch (err) { + return reject(err) + } + }) + } catch (e) { + return reject(e) + } } // there are really two timeout values - pageLoadTimeout @@ -351,6 +387,8 @@ module.exports = (Commands, Cypress, cy, state, config) => { reset() Cypress.on('test:before:run:async', () => { + state('redirectionCount', {}) + // reset any state on the backend // TODO: this is a bug in e2e it needs to be returned return Cypress.backend('reset:server:state') diff --git a/packages/driver/src/cypress/error_messages.js b/packages/driver/src/cypress/error_messages.js index 4f43b109c0..dc4ee96f61 100644 --- a/packages/driver/src/cypress/error_messages.js +++ b/packages/driver/src/cypress/error_messages.js @@ -908,6 +908,12 @@ module.exports = { When this \`load\` event occurs, Cypress will continue running commands.` }, + reached_redirection_limit ({ href, limit }) { + return stripIndent`\ + The application redirected to \`${href}\` more than ${limit} times. Please check if it's an intended behavior. + + If so, increase \`redirectionLimit\` value in configuration.` + }, }, net_stubbing: { diff --git a/packages/server/lib/config_options.ts b/packages/server/lib/config_options.ts index 9411ec0685..2d84f293c3 100644 --- a/packages/server/lib/config_options.ts +++ b/packages/server/lib/config_options.ts @@ -167,6 +167,9 @@ export const options = [ name: 'projectId', defaultValue: null, validation: v.isString, + }, { + name: 'redirectionLimit', + defaultValue: 20, }, { name: 'reporter', defaultValue: 'spec', diff --git a/packages/server/test/unit/config_spec.js b/packages/server/test/unit/config_spec.js index 18aec91dc6..9b82cabb60 100644 --- a/packages/server/test/unit/config_spec.js +++ b/packages/server/test/unit/config_spec.js @@ -1264,6 +1264,7 @@ describe('lib/config', () => { pluginsFile: { value: 'cypress/plugins', from: 'default' }, port: { value: 1234, from: 'cli' }, projectId: { value: null, from: 'default' }, + redirectionLimit: { value: 20, from: 'default' }, reporter: { value: 'json', from: 'cli' }, reporterOptions: { value: null, from: 'default' }, requestTimeout: { value: 5000, from: 'default' }, @@ -1367,6 +1368,7 @@ describe('lib/config', () => { pluginsFile: { value: 'cypress/plugins', from: 'default' }, port: { value: 2020, from: 'config' }, projectId: { value: 'projectId123', from: 'env' }, + redirectionLimit: { value: 20, from: 'default' }, reporter: { value: 'spec', from: 'default' }, reporterOptions: { value: null, from: 'default' }, requestTimeout: { value: 5000, from: 'default' }, From 5a554ce779fc4146faf73922b56423e781d49688 Mon Sep 17 00:00:00 2001 From: Zach Bloomquist Date: Mon, 22 Feb 2021 19:30:37 +0000 Subject: [PATCH 006/134] fix: updated cy.intercept to automatically parse more JSON MIME types (#15129) --- .../integration/commands/net_stubbing_spec.ts | 74 +++++++++++-------- packages/driver/cypress/plugins/server.js | 4 +- .../src/cy/net-stubbing/events/utils.ts | 2 +- 3 files changed, 47 insertions(+), 33 deletions(-) diff --git a/packages/driver/cypress/integration/commands/net_stubbing_spec.ts b/packages/driver/cypress/integration/commands/net_stubbing_spec.ts index bce32a0ab9..b8460120c4 100644 --- a/packages/driver/cypress/integration/commands/net_stubbing_spec.ts +++ b/packages/driver/cypress/integration/commands/net_stubbing_spec.ts @@ -1061,27 +1061,33 @@ describe('network stubbing', { retries: 2 }, function () { }) context('body parsing', function () { - it('automatically parses JSON request bodies', function () { - const p = Promise.defer() + [ + ['application/json', '{"foo":"bar"}'], + ['application/vnd.api+json', '{}'], + ].forEach(([contentType, expectedBody]) => { + it(`automatically parses ${contentType} request bodies`, function () { + const p = Promise.defer() - cy.intercept('/post-only', (req) => { - expect(req.body).to.deep.eq({ foo: 'bar' }) + cy.intercept('/post-only', (req) => { + expect(req.headers['content-type']).to.eq(contentType) + expect(req.body).to.deep.eq({ foo: 'bar' }) - p.resolve() - }).as('post') - .then(() => { - return $.ajax({ - url: '/post-only', - method: 'POST', - contentType: 'application/json', - data: JSON.stringify({ foo: 'bar' }), + p.resolve() + }).as('post') + .then(() => { + return $.ajax({ + url: '/post-only', + method: 'POST', + contentType, + data: JSON.stringify({ foo: 'bar' }), + }) + }).then((responseText) => { + expect(responseText).to.include(`request body:
${expectedBody}`) + + return p }) - }).then((responseText) => { - expect(responseText).to.include('request body:
{"foo":"bar"}') - - return p + .wait('@post').its('request.body').should('deep.eq', { foo: 'bar' }) }) - .wait('@post').its('request.body').should('deep.eq', { foo: 'bar' }) }) it('doesn\'t automatically parse JSON request bodies if content-type is wrong', function () { @@ -1674,23 +1680,29 @@ describe('network stubbing', { retries: 2 }, function () { }) context('body parsing', function () { - it('automatically parses JSON response bodies', function () { - const p = Promise.defer() + [ + 'application/json', + 'application/vnd.api+json', + ].forEach((contentType) => { + it(`automatically parses ${contentType} response bodies`, function () { + const p = Promise.defer() - cy.intercept('/foo.bar.baz.json', (req) => { - req.reply((res) => { - expect(res.body).to.deep.eq({ quux: 'quuz' }) - p.resolve() + cy.intercept(`/json-content-type`, (req) => { + req.reply((res) => { + expect(res.headers['content-type']).to.eq(contentType) + expect(res.body).to.deep.eq({}) + p.resolve() + }) + }).as('get') + .then(() => { + return $.get(`/json-content-type?contentType=${encodeURIComponent(contentType)}`) + }).then((responseJson) => { + expect(responseJson).to.deep.eq({}) + + return p }) - }).as('get') - .then(() => { - return $.get('/fixtures/foo.bar.baz.json') - }).then((responseJson) => { - expect(responseJson).to.deep.eq({ quux: 'quuz' }) - - return p + .wait('@get').its('response.body').should('deep.eq', {}) }) - .wait('@get').its('response.body').should('deep.eq', { quux: 'quuz' }) }) it('doesn\'t automatically parse JSON response bodies if content-type is wrong', function () { diff --git a/packages/driver/cypress/plugins/server.js b/packages/driver/cypress/plugins/server.js index fa2a22bce3..080e11cedc 100644 --- a/packages/driver/cypress/plugins/server.js +++ b/packages/driver/cypress/plugins/server.js @@ -102,7 +102,9 @@ const createApp = (port) => { }) app.get('/json-content-type', (req, res) => { - return res.send({}) + res.setHeader('content-type', req.query.contentType || 'application/json') + + return res.end('{}') }) app.get('/html-content-type-with-charset-param', (req, res) => { diff --git a/packages/driver/src/cy/net-stubbing/events/utils.ts b/packages/driver/src/cy/net-stubbing/events/utils.ts index 6d6cc9f068..1a837db450 100644 --- a/packages/driver/src/cy/net-stubbing/events/utils.ts +++ b/packages/driver/src/cy/net-stubbing/events/utils.ts @@ -4,7 +4,7 @@ import { CyHttpMessages } from '@packages/net-stubbing/lib/types' export function hasJsonContentType (headers: { [k: string]: string }) { const contentType = find(headers, (v, k) => /^content-type$/i.test(k)) - return contentType && /^application\/json/i.test(contentType) + return contentType && /^application\/.*json/i.test(contentType) } export function parseJsonBody (message: CyHttpMessages.BaseMessage) { From 53f22b080f7725e81796f76598c5668dfbf8a239 Mon Sep 17 00:00:00 2001 From: Kukhyeon Heo Date: Fri, 26 Feb 2021 06:49:27 +0900 Subject: [PATCH 007/134] feat: cy.submit validates inputs before sending data. (#14965) Co-authored-by: Chris Breiding --- packages/driver/cypress/fixtures/dom.html | 21 ++++++++++++ .../commands/actions/submit_spec.js | 15 +++++++++ .../driver/src/cy/commands/actions/submit.js | 20 +++++++++++ packages/driver/src/cypress/error_messages.js | 8 ++++- yarn.lock | 33 ++----------------- 5 files changed, 66 insertions(+), 31 deletions(-) diff --git a/packages/driver/cypress/fixtures/dom.html b/packages/driver/cypress/fixtures/dom.html index be04e3cbcc..330dae3ab6 100644 --- a/packages/driver/cypress/fixtures/dom.html +++ b/packages/driver/cypress/fixtures/dom.html @@ -655,5 +655,26 @@ + +
+
+ + +
+ +
+ + +
+ + +
diff --git a/packages/driver/cypress/integration/commands/actions/submit_spec.js b/packages/driver/cypress/integration/commands/actions/submit_spec.js index d379edbeb8..8b1d740085 100644 --- a/packages/driver/cypress/integration/commands/actions/submit_spec.js +++ b/packages/driver/cypress/integration/commands/actions/submit_spec.js @@ -335,6 +335,21 @@ describe('src/cy/commands/actions/submit', () => { cy.get('form:first').submit().should('have.class', 'submitted') }) + + // https://github.com/cypress-io/cypress/issues/14911 + it('should throw an error when form validation failed', function (done) { + cy.on('fail', (err) => { + expect(err.message).to.include('2 inputs') + expect(err.message).to.include('Please fill out this field.') + + done() + }) + + cy.get('#form-validation').within(() => { + cy.get('input[type=submit]').click() + cy.root().submit() + }) + }) }) describe('.log', () => { diff --git a/packages/driver/src/cy/commands/actions/submit.js b/packages/driver/src/cy/commands/actions/submit.js index 70c67bf702..ed7199ef3e 100644 --- a/packages/driver/src/cy/commands/actions/submit.js +++ b/packages/driver/src/cy/commands/actions/submit.js @@ -54,6 +54,26 @@ module.exports = (Commands, Cypress, cy) => { }) } + // Validate form. + // @see https://github.com/cypress-io/cypress/issues/14911 + if (!form.checkValidity()) { + const elements = form.querySelectorAll(':invalid') + const failures = _.map(elements, (el) => { + const element = $dom.stringify(el) + const message = el.validationMessage + + return ` - \`${element}\`: ${message}` + }) + + $errUtils.throwErrByPath('submit.failed_validation', { + onFail: options._log, + args: { + failures: failures.join('\n'), + suffix: failures.length ? `${failures.length} inputs` : 'input', + }, + }) + } + // calling the native submit method will not actually trigger // a submit event, so we need to dispatch this manually so // native event listeners and jquery can bind to it diff --git a/packages/driver/src/cypress/error_messages.js b/packages/driver/src/cypress/error_messages.js index dc4ee96f61..f462c15b08 100644 --- a/packages/driver/src/cypress/error_messages.js +++ b/packages/driver/src/cypress/error_messages.js @@ -911,7 +911,7 @@ module.exports = { reached_redirection_limit ({ href, limit }) { return stripIndent`\ The application redirected to \`${href}\` more than ${limit} times. Please check if it's an intended behavior. - + If so, increase \`redirectionLimit\` value in configuration.` }, }, @@ -1542,6 +1542,12 @@ module.exports = { message: `${cmd('submit')} can only be called on a \`
\`. Your subject {{word}} a: \`{{node}}\``, docsUrl: 'https://on.cypress.io/submit', }, + failed_validation: { + message: stripIndent`\ + Form validation failed for the following {{suffix}}: + {{failures}}`, + docsUrl: 'https://on.cypress.io/submit', + }, }, task: { diff --git a/yarn.lock b/yarn.lock index ed96501aba..6846345cbf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12998,7 +12998,7 @@ debug@^3.0.0, debug@^3.1.0, debug@^3.1.1, debug@^3.2.5, debug@^3.2.6: dependencies: ms "^2.1.1" -debuglog@*, debuglog@^1.0.1: +debuglog@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492" integrity sha1-qiT/uaw9+aI1GDfPstJ5NgzXhJI= @@ -18930,7 +18930,7 @@ import-local@2.0.0, import-local@^2.0.0: pkg-dir "^3.0.0" resolve-cwd "^2.0.0" -imurmurhash@*, imurmurhash@^0.1.4: +imurmurhash@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= @@ -22112,11 +22112,6 @@ lodash._basecreate@^3.0.0: resolved "https://registry.yarnpkg.com/lodash._basecreate/-/lodash._basecreate-3.0.3.tgz#1bc661614daa7fc311b7d03bf16806a0213cf821" integrity sha1-G8ZhYU2qf8MRt9A78WgGoCE8+CE= -lodash._baseindexof@*: - version "3.1.0" - resolved "https://registry.yarnpkg.com/lodash._baseindexof/-/lodash._baseindexof-3.1.0.tgz#fe52b53a1c6761e42618d654e4a25789ed61822c" - integrity sha1-/lK1OhxnYeQmGNZU5KJXie1hgiw= - lodash._baseuniq@~4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/lodash._baseuniq/-/lodash._baseuniq-4.6.0.tgz#0ebb44e456814af7905c6212fa2c9b2d51b841e8" @@ -22125,29 +22120,12 @@ lodash._baseuniq@~4.6.0: lodash._createset "~4.0.0" lodash._root "~3.0.0" -lodash._bindcallback@*: - version "3.0.1" - resolved "https://registry.yarnpkg.com/lodash._bindcallback/-/lodash._bindcallback-3.0.1.tgz#e531c27644cf8b57a99e17ed95b35c748789392e" - integrity sha1-5THCdkTPi1epnhftlbNcdIeJOS4= - -lodash._cacheindexof@*: - version "3.0.2" - resolved "https://registry.yarnpkg.com/lodash._cacheindexof/-/lodash._cacheindexof-3.0.2.tgz#3dc69ac82498d2ee5e3ce56091bafd2adc7bde92" - integrity sha1-PcaayCSY0u5ePOVgkbr9Ktx73pI= - -lodash._createcache@*: - version "3.1.2" - resolved "https://registry.yarnpkg.com/lodash._createcache/-/lodash._createcache-3.1.2.tgz#56d6a064017625e79ebca6b8018e17440bdcf093" - integrity sha1-VtagZAF2JeeevKa4AY4XRAvc8JM= - dependencies: - lodash._getnative "^3.0.0" - lodash._createset@~4.0.0: version "4.0.3" resolved "https://registry.yarnpkg.com/lodash._createset/-/lodash._createset-4.0.3.tgz#0f4659fbb09d75194fa9e2b88a6644d363c9fe26" integrity sha1-D0ZZ+7CddRlPqeK4imZE02PJ/iY= -lodash._getnative@*, lodash._getnative@^3.0.0: +lodash._getnative@^3.0.0: version "3.9.1" resolved "https://registry.yarnpkg.com/lodash._getnative/-/lodash._getnative-3.9.1.tgz#570bc7dede46d61cdcde687d65d3eecbaa3aaff5" integrity sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U= @@ -22350,11 +22328,6 @@ lodash.reduce@^4.6.0: resolved "https://registry.yarnpkg.com/lodash.reduce/-/lodash.reduce-4.6.0.tgz#f1ab6b839299ad48f784abbf476596f03b914d3b" integrity sha1-8atrg5KZrUj3hKu/R2WW8DuRTTs= -lodash.restparam@*: - version "3.6.1" - resolved "https://registry.yarnpkg.com/lodash.restparam/-/lodash.restparam-3.6.1.tgz#936a4e309ef330a7645ed4145986c85ae5b20805" - integrity sha1-k2pOMJ7zMKdkXtQUWYbIWuWyCAU= - lodash.set@4.3.2, lodash.set@^4.3.2: version "4.3.2" resolved "https://registry.yarnpkg.com/lodash.set/-/lodash.set-4.3.2.tgz#d8757b1da807dde24816b0d6a84bea1a76230b23" From a529ffe5e4f7cdd13ef00ea79239080c110bc348 Mon Sep 17 00:00:00 2001 From: Jennifer Shehane Date: Sun, 28 Feb 2021 08:30:40 -0500 Subject: [PATCH 008/134] feat(breaking): Drop support for Node 10; Require Node 12+ (#15165) Co-authored-by: Zach Bloomquist Co-authored-by: Zach Bloomquist --- circle.yml | 2 +- cli/package.json | 64 +-- .../server/__snapshots__/1_es_modules_spec.js | 2 +- yarn.lock | 475 +++++++++++++----- 4 files changed, 396 insertions(+), 147 deletions(-) diff --git a/circle.yml b/circle.yml index 39dfeb19f0..070f669495 100644 --- a/circle.yml +++ b/circle.yml @@ -1311,7 +1311,7 @@ jobs: test-npm-module-on-minimum-node-version: <<: *defaults docker: - - image: cypress/base:10.0.0 + - image: cypress/base:12.0.0-libgbm steps: - attach_workspace: at: ~/ diff --git a/cli/package.json b/cli/package.json index 821998779e..6332834718 100644 --- a/cli/package.json +++ b/cli/package.json @@ -13,7 +13,7 @@ "size": "t=\"cypress-v0.0.0.tgz\"; yarn pack --filename \"${t}\"; wc -c \"${t}\"; tar tvf \"${t}\"; rm \"${t}\";", "test": "yarn test-unit", "test-debug": "node --inspect-brk $(yarn bin mocha)", - "test-dependencies": "dependency-check . --no-dev", + "test-dependencies": "dependency-check . --missing --no-dev --verbose", "test-unit": "yarn unit", "test-watch": "yarn unit --watch", "types": "yarn dtslint", @@ -23,11 +23,11 @@ "@cypress/listr-verbose-renderer": "^0.4.1", "@cypress/request": "^2.88.5", "@cypress/xvfb": "^1.2.4", - "@types/node": "12.12.50", - "@types/sinonjs__fake-timers": "^6.0.1", + "@types/node": "^12.12.50", + "@types/sinonjs__fake-timers": "^6.0.2", "@types/sizzle": "^2.3.2", - "arch": "^2.1.2", - "blob-util": "2.0.2", + "arch": "^2.2.0", + "blob-util": "^2.0.2", "bluebird": "^3.7.2", "cachedir": "^2.3.0", "chalk": "^4.1.0", @@ -35,65 +35,65 @@ "cli-table3": "~0.6.0", "commander": "^5.1.0", "common-tags": "^1.8.0", - "dayjs": "^1.9.3", + "dayjs": "^1.10.4", "debug": "4.3.2", - "eventemitter2": "^6.4.2", - "execa": "^4.0.2", + "eventemitter2": "^6.4.3", + "execa": "^5.0.0", "executable": "^4.1.1", "extract-zip": "^1.7.0", - "fs-extra": "^9.0.1", + "fs-extra": "^9.1.0", "getos": "^3.2.1", - "is-ci": "^2.0.0", - "is-installed-globally": "^0.3.2", + "is-ci": "^3.0.0", + "is-installed-globally": "~0.4.0", "lazy-ass": "^1.6.0", "listr": "^0.14.3", - "lodash": "^4.17.19", + "lodash": "^4.17.21", "log-symbols": "^4.0.0", "minimist": "^1.2.5", "ospath": "^1.2.2", - "pretty-bytes": "^5.4.1", + "pretty-bytes": "^5.6.0", "ramda": "~0.27.1", "request-progress": "^3.0.0", - "supports-color": "^7.2.0", + "supports-color": "^8.1.1", "tmp": "~0.2.1", "untildify": "^4.0.0", "url": "^0.11.0", "yauzl": "^2.10.0" }, "devDependencies": { - "@babel/cli": "7.8.4", - "@babel/preset-env": "7.9.5", + "@babel/cli": "7.13.0", + "@babel/preset-env": "7.13.5", "@cypress/sinon-chai": "2.9.1", "@packages/root": "0.0.0-development", - "@types/bluebird": "3.5.29", - "@types/chai": "4.2.7", + "@types/bluebird": "3.5.33", + "@types/chai": "4.2.15", "@types/chai-jquery": "1.1.40", "@types/jquery": "3.3.31", - "@types/lodash": "4.14.149", + "@types/lodash": "4.14.168", "@types/minimatch": "3.0.3", "@types/mocha": "5.2.7", "@types/sinon": "7.5.1", - "@types/sinon-chai": "3.2.3", + "@types/sinon-chai": "3.2.5", "chai": "3.5.0", "chai-as-promised": "7.1.1", "chai-string": "1.5.0", - "cross-env": "6.0.3", - "dependency-check": "3.4.1", - "dtslint": "0.9.0", + "cross-env": "7.0.3", + "dependency-check": "4.1.0", + "dtslint": "4.0.7", "execa-wrap": "1.4.0", - "hasha": "5.0.0", + "hasha": "5.2.2", "mocha": "6.2.2", - "mock-fs": "4.12.0", - "mocked-env": "1.2.4", - "nock": "12.0.2", - "postinstall-postinstall": "2.0.0", + "mock-fs": "4.13.0", + "mocked-env": "1.3.2", + "nock": "13.0.7", + "postinstall-postinstall": "2.1.0", "proxyquire": "2.1.3", "resolve-pkg": "2.0.0", - "shelljs": "0.8.3", + "shelljs": "0.8.4", "sinon": "7.2.2", - "snap-shot-it": "7.9.3", + "snap-shot-it": "7.9.6", "spawn-mock": "1.0.0", - "strip-ansi": "4.0.0" + "strip-ansi": "6.0.0" }, "files": [ "bin", @@ -106,7 +106,7 @@ "cypress": "bin/cypress" }, "engines": { - "node": ">=10.0.0" + "node": ">=12.0.0" }, "types": "types" } diff --git a/packages/server/__snapshots__/1_es_modules_spec.js b/packages/server/__snapshots__/1_es_modules_spec.js index 5123208e76..1fb8c0a891 100644 --- a/packages/server/__snapshots__/1_es_modules_spec.js +++ b/packages/server/__snapshots__/1_es_modules_spec.js @@ -92,7 +92,7 @@ Module build failed (from [..]): SyntaxError: /foo/bar/.projects/e2e/lib/fail.js: Unexpected token (2:0) 1 | export default { -> 2 | +> 2 | | ^ @ ./cypress/integration/es_module_import_failing_spec.js 3:0-25 [stack trace lines] diff --git a/yarn.lock b/yarn.lock index 35a9a94dc0..cce0c58e37 100644 --- a/yarn.lock +++ b/yarn.lock @@ -63,21 +63,22 @@ resolved "https://registry.yarnpkg.com/@ant-design/css-animation/-/css-animation-1.7.3.tgz#60a1c970014e86b28f940510d69e503e428f1136" integrity sha512-LrX0OGZtW+W6iLnTAqnTaoIsRelYeuLZWsrmBJFUXDALQphPsN8cE5DCsmoSlL0QYb94BQxINiuS70Ar/8BNgA== -"@babel/cli@7.8.4": - version "7.8.4" - resolved "https://registry.yarnpkg.com/@babel/cli/-/cli-7.8.4.tgz#505fb053721a98777b2b175323ea4f090b7d3c1c" - integrity sha512-XXLgAm6LBbaNxaGhMAznXXaxtCWfuv6PIDJ9Alsy9JYTOh+j2jJz+L/162kkfU1j/pTSxK1xGmlwI4pdIMkoag== +"@babel/cli@7.13.0": + version "7.13.0" + resolved "https://registry.yarnpkg.com/@babel/cli/-/cli-7.13.0.tgz#48e77614e897615ca299bece587b68a70723ff4c" + integrity sha512-y5AohgeVhU+wO5kU1WGMLdocFj83xCxVjsVFa2ilII8NEwmBZvx7Ambq621FbFIK68loYJ9p43nfoi6es+rzSA== dependencies: commander "^4.0.1" convert-source-map "^1.1.0" fs-readdir-recursive "^1.1.0" glob "^7.0.0" - lodash "^4.17.13" + lodash "^4.17.19" make-dir "^2.1.0" slash "^2.0.0" source-map "^0.5.0" optionalDependencies: - chokidar "^2.1.8" + "@nicolo-ribaudo/chokidar-2" "2.1.8-no-fsevents" + chokidar "^3.4.0" "@babel/code-frame@7.10.4": version "7.10.4" @@ -114,7 +115,7 @@ dependencies: "@babel/highlight" "^7.12.13" -"@babel/compat-data@^7.11.0", "@babel/compat-data@^7.13.0", "@babel/compat-data@^7.13.8", "@babel/compat-data@^7.9.0": +"@babel/compat-data@^7.11.0", "@babel/compat-data@^7.13.0", "@babel/compat-data@^7.13.5", "@babel/compat-data@^7.13.8", "@babel/compat-data@^7.9.0": version "7.13.8" resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.13.8.tgz#5b783b9808f15cef71547f1b691f34f8ff6003a6" integrity sha512-EaI33z19T4qN3xLXsGf48M2cDqa6ei9tPZlfLdb2HC+e/cFtREiRd8hdSqDbwdLB0/+gLwqJmCYASH0z2bUdog== @@ -450,7 +451,7 @@ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.13.4.tgz#340211b0da94a351a6f10e63671fa727333d13ab" integrity sha512-uvoOulWHhI+0+1f9L4BoozY7U5cIkZ9PgJqvb041d6vypgUmtVPG4vmGm4pSggjl8BELzvHyUeJSUyEMY6b+qA== -"@babel/plugin-proposal-async-generator-functions@^7.10.4", "@babel/plugin-proposal-async-generator-functions@^7.13.8", "@babel/plugin-proposal-async-generator-functions@^7.2.0", "@babel/plugin-proposal-async-generator-functions@^7.8.3": +"@babel/plugin-proposal-async-generator-functions@^7.10.4", "@babel/plugin-proposal-async-generator-functions@^7.13.5", "@babel/plugin-proposal-async-generator-functions@^7.13.8", "@babel/plugin-proposal-async-generator-functions@^7.2.0", "@babel/plugin-proposal-async-generator-functions@^7.8.3": version "7.13.8" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.13.8.tgz#87aacb574b3bc4b5603f6fe41458d72a5a2ec4b1" integrity sha512-rPBnhj+WgoSmgq+4gQUtXx/vOcU+UYtjy1AA/aeD61Hwj410fwYyqfUcRP3lR8ucgliVJL/G7sXcNUecC75IXA== @@ -517,7 +518,7 @@ "@babel/helper-plugin-utils" "^7.13.0" "@babel/plugin-syntax-decorators" "^7.12.13" -"@babel/plugin-proposal-dynamic-import@^7.10.4", "@babel/plugin-proposal-dynamic-import@^7.13.8", "@babel/plugin-proposal-dynamic-import@^7.8.3": +"@babel/plugin-proposal-dynamic-import@^7.10.4", "@babel/plugin-proposal-dynamic-import@^7.12.17", "@babel/plugin-proposal-dynamic-import@^7.13.8", "@babel/plugin-proposal-dynamic-import@^7.8.3": version "7.13.8" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.13.8.tgz#876a1f6966e1dec332e8c9451afda3bebcdf2e1d" integrity sha512-ONWKj0H6+wIRCkZi9zSbZtE/r73uOhMVHh256ys0UzfM7I3d4n+spZNWjOnJv2gzopumP2Wxi186vI8N0Y2JyQ== @@ -541,7 +542,7 @@ "@babel/helper-plugin-utils" "^7.12.13" "@babel/plugin-syntax-export-namespace-from" "^7.8.3" -"@babel/plugin-proposal-json-strings@^7.10.4", "@babel/plugin-proposal-json-strings@^7.13.8", "@babel/plugin-proposal-json-strings@^7.2.0", "@babel/plugin-proposal-json-strings@^7.8.3": +"@babel/plugin-proposal-json-strings@^7.10.4", "@babel/plugin-proposal-json-strings@^7.12.13", "@babel/plugin-proposal-json-strings@^7.13.8", "@babel/plugin-proposal-json-strings@^7.2.0", "@babel/plugin-proposal-json-strings@^7.8.3": version "7.13.8" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.13.8.tgz#bf1fb362547075afda3634ed31571c5901afef7b" integrity sha512-w4zOPKUFPX1mgvTmL/fcEqy34hrQ1CRcGxdphBc6snDnnqJ47EZDIyop6IwXzAC8G916hsIuXB2ZMBCExC5k7Q== @@ -549,7 +550,7 @@ "@babel/helper-plugin-utils" "^7.13.0" "@babel/plugin-syntax-json-strings" "^7.8.3" -"@babel/plugin-proposal-logical-assignment-operators@^7.11.0", "@babel/plugin-proposal-logical-assignment-operators@^7.13.8": +"@babel/plugin-proposal-logical-assignment-operators@^7.11.0", "@babel/plugin-proposal-logical-assignment-operators@^7.12.13", "@babel/plugin-proposal-logical-assignment-operators@^7.13.8": version "7.13.8" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.13.8.tgz#93fa78d63857c40ce3c8c3315220fd00bfbb4e1a" integrity sha512-aul6znYB4N4HGweImqKn59Su9RS8lbUIqxtXTOcAGtNIDczoEFv+l1EhmX8rUBp3G1jMjKJm8m0jXVp63ZpS4A== @@ -565,7 +566,7 @@ "@babel/helper-plugin-utils" "^7.8.3" "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.0" -"@babel/plugin-proposal-nullish-coalescing-operator@^7.10.4", "@babel/plugin-proposal-nullish-coalescing-operator@^7.13.8", "@babel/plugin-proposal-nullish-coalescing-operator@^7.8.3": +"@babel/plugin-proposal-nullish-coalescing-operator@^7.10.4", "@babel/plugin-proposal-nullish-coalescing-operator@^7.13.0", "@babel/plugin-proposal-nullish-coalescing-operator@^7.13.8", "@babel/plugin-proposal-nullish-coalescing-operator@^7.8.3": version "7.13.8" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.13.8.tgz#3730a31dafd3c10d8ccd10648ed80a2ac5472ef3" integrity sha512-iePlDPBn//UhxExyS9KyeYU7RM9WScAG+D3Hhno0PLJebAEpDZMocbDe64eqynhNAnwz/vZoL/q/QB2T1OH39A== @@ -622,7 +623,7 @@ "@babel/helper-plugin-utils" "^7.8.3" "@babel/plugin-syntax-object-rest-spread" "^7.8.0" -"@babel/plugin-proposal-object-rest-spread@^7.0.0", "@babel/plugin-proposal-object-rest-spread@^7.11.0", "@babel/plugin-proposal-object-rest-spread@^7.13.8", "@babel/plugin-proposal-object-rest-spread@^7.4.4", "@babel/plugin-proposal-object-rest-spread@^7.9.0", "@babel/plugin-proposal-object-rest-spread@^7.9.5": +"@babel/plugin-proposal-object-rest-spread@^7.0.0", "@babel/plugin-proposal-object-rest-spread@^7.11.0", "@babel/plugin-proposal-object-rest-spread@^7.13.0", "@babel/plugin-proposal-object-rest-spread@^7.13.8", "@babel/plugin-proposal-object-rest-spread@^7.4.4", "@babel/plugin-proposal-object-rest-spread@^7.9.0", "@babel/plugin-proposal-object-rest-spread@^7.9.5": version "7.13.8" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.13.8.tgz#5d210a4d727d6ce3b18f9de82cc99a3964eed60a" integrity sha512-DhB2EuB1Ih7S3/IRX5AFVgZ16k3EzfRbq97CxAVI1KSYcW+lexV8VZb7G7L8zuPVSdQMRn0kiBpf/Yzu9ZKH0g== @@ -633,7 +634,7 @@ "@babel/plugin-syntax-object-rest-spread" "^7.8.3" "@babel/plugin-transform-parameters" "^7.13.0" -"@babel/plugin-proposal-optional-catch-binding@^7.10.4", "@babel/plugin-proposal-optional-catch-binding@^7.13.8", "@babel/plugin-proposal-optional-catch-binding@^7.2.0", "@babel/plugin-proposal-optional-catch-binding@^7.8.3": +"@babel/plugin-proposal-optional-catch-binding@^7.10.4", "@babel/plugin-proposal-optional-catch-binding@^7.12.13", "@babel/plugin-proposal-optional-catch-binding@^7.13.8", "@babel/plugin-proposal-optional-catch-binding@^7.2.0", "@babel/plugin-proposal-optional-catch-binding@^7.8.3": version "7.13.8" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.13.8.tgz#3ad6bd5901506ea996fc31bdcf3ccfa2bed71107" integrity sha512-0wS/4DUF1CuTmGo+NiaHfHcVSeSLj5S3e6RivPTg/2k3wOv3jO35tZ6/ZWsQhQMvdgI7CwphjQa/ccarLymHVA== @@ -649,7 +650,7 @@ "@babel/helper-plugin-utils" "^7.8.3" "@babel/plugin-syntax-optional-chaining" "^7.8.0" -"@babel/plugin-proposal-optional-chaining@^7.11.0", "@babel/plugin-proposal-optional-chaining@^7.13.8", "@babel/plugin-proposal-optional-chaining@^7.9.0": +"@babel/plugin-proposal-optional-chaining@^7.11.0", "@babel/plugin-proposal-optional-chaining@^7.13.0", "@babel/plugin-proposal-optional-chaining@^7.13.8", "@babel/plugin-proposal-optional-chaining@^7.9.0": version "7.13.8" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.13.8.tgz#e39df93efe7e7e621841babc197982e140e90756" integrity sha512-hpbBwbTgd7Cz1QryvwJZRo1U0k1q8uyBmeXOSQUjdg/A2TASkhR/rz7AyqZ/kS8kbpsNA80rOYbxySBJAqmhhQ== @@ -964,7 +965,7 @@ "@babel/helper-simple-access" "^7.12.1" babel-plugin-dynamic-import-node "^2.3.3" -"@babel/plugin-transform-modules-commonjs@^7.10.4", "@babel/plugin-transform-modules-commonjs@^7.13.8", "@babel/plugin-transform-modules-commonjs@^7.4.4", "@babel/plugin-transform-modules-commonjs@^7.9.0": +"@babel/plugin-transform-modules-commonjs@^7.10.4", "@babel/plugin-transform-modules-commonjs@^7.13.0", "@babel/plugin-transform-modules-commonjs@^7.13.8", "@babel/plugin-transform-modules-commonjs@^7.4.4", "@babel/plugin-transform-modules-commonjs@^7.9.0": version "7.13.8" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.13.8.tgz#7b01ad7c2dcf2275b06fa1781e00d13d420b3e1b" integrity sha512-9QiOx4MEGglfYZ4XOnU79OHr6vIWUakIj9b4mioN8eQIoEh+pf5p/zEB36JpDFWA12nNMiRf7bfoRvl9Rn79Bw== @@ -974,7 +975,7 @@ "@babel/helper-simple-access" "^7.12.13" babel-plugin-dynamic-import-node "^2.3.3" -"@babel/plugin-transform-modules-systemjs@^7.10.4", "@babel/plugin-transform-modules-systemjs@^7.13.8", "@babel/plugin-transform-modules-systemjs@^7.4.4", "@babel/plugin-transform-modules-systemjs@^7.9.0": +"@babel/plugin-transform-modules-systemjs@^7.10.4", "@babel/plugin-transform-modules-systemjs@^7.12.13", "@babel/plugin-transform-modules-systemjs@^7.13.8", "@babel/plugin-transform-modules-systemjs@^7.4.4", "@babel/plugin-transform-modules-systemjs@^7.9.0": version "7.13.8" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.13.8.tgz#6d066ee2bff3c7b3d60bf28dec169ad993831ae3" integrity sha512-hwqctPYjhM6cWvVIlOIe27jCIBgHCsdH2xCJVAYQm7V5yTMoilbVMi9f6wKg0rpQAOn6ZG4AOyvCqFF/hUh6+A== @@ -1280,6 +1281,80 @@ levenary "^1.1.1" semver "^5.5.0" +"@babel/preset-env@7.13.5": + version "7.13.5" + resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.13.5.tgz#68b3bbc821a97fcdbf4bd0f6895b83d07f84f33e" + integrity sha512-xUeKBIIcbwxGevyWMSWZOW98W1lp7toITvVsMxSddCEQy932yYiF4fCB+CG3E/MXzFX3KbefgvCqEQ7TDoE6UQ== + dependencies: + "@babel/compat-data" "^7.13.5" + "@babel/helper-compilation-targets" "^7.13.0" + "@babel/helper-plugin-utils" "^7.13.0" + "@babel/helper-validator-option" "^7.12.17" + "@babel/plugin-proposal-async-generator-functions" "^7.13.5" + "@babel/plugin-proposal-class-properties" "^7.13.0" + "@babel/plugin-proposal-dynamic-import" "^7.12.17" + "@babel/plugin-proposal-export-namespace-from" "^7.12.13" + "@babel/plugin-proposal-json-strings" "^7.12.13" + "@babel/plugin-proposal-logical-assignment-operators" "^7.12.13" + "@babel/plugin-proposal-nullish-coalescing-operator" "^7.13.0" + "@babel/plugin-proposal-numeric-separator" "^7.12.13" + "@babel/plugin-proposal-object-rest-spread" "^7.13.0" + "@babel/plugin-proposal-optional-catch-binding" "^7.12.13" + "@babel/plugin-proposal-optional-chaining" "^7.13.0" + "@babel/plugin-proposal-private-methods" "^7.13.0" + "@babel/plugin-proposal-unicode-property-regex" "^7.12.13" + "@babel/plugin-syntax-async-generators" "^7.8.0" + "@babel/plugin-syntax-class-properties" "^7.12.13" + "@babel/plugin-syntax-dynamic-import" "^7.8.0" + "@babel/plugin-syntax-export-namespace-from" "^7.8.3" + "@babel/plugin-syntax-json-strings" "^7.8.0" + "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" + "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.0" + "@babel/plugin-syntax-numeric-separator" "^7.10.4" + "@babel/plugin-syntax-object-rest-spread" "^7.8.0" + "@babel/plugin-syntax-optional-catch-binding" "^7.8.0" + "@babel/plugin-syntax-optional-chaining" "^7.8.0" + "@babel/plugin-syntax-top-level-await" "^7.12.13" + "@babel/plugin-transform-arrow-functions" "^7.13.0" + "@babel/plugin-transform-async-to-generator" "^7.13.0" + "@babel/plugin-transform-block-scoped-functions" "^7.12.13" + "@babel/plugin-transform-block-scoping" "^7.12.13" + "@babel/plugin-transform-classes" "^7.13.0" + "@babel/plugin-transform-computed-properties" "^7.13.0" + "@babel/plugin-transform-destructuring" "^7.13.0" + "@babel/plugin-transform-dotall-regex" "^7.12.13" + "@babel/plugin-transform-duplicate-keys" "^7.12.13" + "@babel/plugin-transform-exponentiation-operator" "^7.12.13" + "@babel/plugin-transform-for-of" "^7.13.0" + "@babel/plugin-transform-function-name" "^7.12.13" + "@babel/plugin-transform-literals" "^7.12.13" + "@babel/plugin-transform-member-expression-literals" "^7.12.13" + "@babel/plugin-transform-modules-amd" "^7.13.0" + "@babel/plugin-transform-modules-commonjs" "^7.13.0" + "@babel/plugin-transform-modules-systemjs" "^7.12.13" + "@babel/plugin-transform-modules-umd" "^7.13.0" + "@babel/plugin-transform-named-capturing-groups-regex" "^7.12.13" + "@babel/plugin-transform-new-target" "^7.12.13" + "@babel/plugin-transform-object-super" "^7.12.13" + "@babel/plugin-transform-parameters" "^7.13.0" + "@babel/plugin-transform-property-literals" "^7.12.13" + "@babel/plugin-transform-regenerator" "^7.12.13" + "@babel/plugin-transform-reserved-words" "^7.12.13" + "@babel/plugin-transform-shorthand-properties" "^7.12.13" + "@babel/plugin-transform-spread" "^7.13.0" + "@babel/plugin-transform-sticky-regex" "^7.12.13" + "@babel/plugin-transform-template-literals" "^7.13.0" + "@babel/plugin-transform-typeof-symbol" "^7.12.13" + "@babel/plugin-transform-unicode-escapes" "^7.12.13" + "@babel/plugin-transform-unicode-regex" "^7.12.13" + "@babel/preset-modules" "^0.1.3" + "@babel/types" "^7.13.0" + babel-plugin-polyfill-corejs2 "^0.1.4" + babel-plugin-polyfill-corejs3 "^0.1.3" + babel-plugin-polyfill-regenerator "^0.1.2" + core-js-compat "^3.9.0" + semver "7.0.0" + "@babel/preset-env@7.4.5": version "7.4.5" resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.4.5.tgz#2fad7f62983d5af563b5f3139242755884998a58" @@ -2179,6 +2254,34 @@ dependencies: "@date-io/core" "^1.3.13" +"@definitelytyped/header-parser@latest": + version "0.0.69" + resolved "https://registry.yarnpkg.com/@definitelytyped/header-parser/-/header-parser-0.0.69.tgz#0143e74ec579fc351d2fdfa9dce4516d2ad1cdf3" + integrity sha512-8WKD2I1wtrHWWNvyei7ncBBFJ2DfDUnsAgpS2z/O9JlR8LzoDhqUphH5CaB+EMr3kdHXi7KcAnf6ec7zJ5IsvA== + dependencies: + "@definitelytyped/typescript-versions" "^0.0.69" + "@types/parsimmon" "^1.10.1" + parsimmon "^1.13.0" + +"@definitelytyped/typescript-versions@^0.0.69", "@definitelytyped/typescript-versions@latest": + version "0.0.69" + resolved "https://registry.yarnpkg.com/@definitelytyped/typescript-versions/-/typescript-versions-0.0.69.tgz#3ae4b3995d4080e00b26cae8debcd5ef4a7cb3d6" + integrity sha512-+0KTIEPFbH9+nehW5aZ93so9wqbScAxZ0uI5mstyWWegEz4a9YS9LgIRr6BMdHbt3NjKyfnJ5Kd2oicMN91BZg== + +"@definitelytyped/utils@latest": + version "0.0.69" + resolved "https://registry.yarnpkg.com/@definitelytyped/utils/-/utils-0.0.69.tgz#fcfd595521e80fa664f20a530717c60d95d2e119" + integrity sha512-pd0aGoVilhahj+XRrEW9Q1/fJG9V1k+CkpGxg1nSk0fBdi15njovnPPU3iTSUzFlaUZloNoTs5+czTjgXJfg9Q== + dependencies: + "@definitelytyped/typescript-versions" "^0.0.69" + "@types/node" "^12.12.29" + charm "^1.0.2" + fs-extra "^8.1.0" + fstream "^1.0.12" + npm-registry-client "^8.6.0" + tar "^2.2.2" + tar-stream "^2.1.4" + "@develar/schema-utils@~2.6.5": version "2.6.5" resolved "https://registry.yarnpkg.com/@develar/schema-utils/-/schema-utils-2.6.5.tgz#3ece22c5838402419a6e0425f85742b961d9b6c6" @@ -4250,6 +4353,23 @@ resolved "https://registry.yarnpkg.com/@next/react-refresh-utils/-/react-refresh-utils-9.5.5.tgz#fe559b5ca51c038cb7840e0d669a6d7ef01fe4eb" integrity sha512-Gz5z0+ID+KAGto6Tkgv1a340damEw3HG6ANLKwNi5/QSHqQ3JUAVxMuhz3qnL54505I777evpzL89ofWEMIWKw== +"@nicolo-ribaudo/chokidar-2@2.1.8-no-fsevents": + version "2.1.8-no-fsevents" + resolved "https://registry.yarnpkg.com/@nicolo-ribaudo/chokidar-2/-/chokidar-2-2.1.8-no-fsevents.tgz#da7c3996b8e6e19ebd14d82eaced2313e7769f9b" + integrity sha512-+nb9vWloHNNMFHjGofEam3wopE3m1yuambrrd/fnPc+lFOMB9ROTqQlche9ByFWNkdNqfSgR/kkQtQ8DzEWt2w== + dependencies: + anymatch "^2.0.0" + async-each "^1.0.1" + braces "^2.3.2" + glob-parent "^3.1.0" + inherits "^2.0.3" + is-binary-path "^1.0.0" + is-glob "^4.0.0" + normalize-path "^3.0.0" + path-is-absolute "^1.0.0" + readdirp "^2.2.1" + upath "^1.1.1" + "@nodelib/fs.scandir@2.1.4": version "2.1.4" resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.4.tgz#d4b3549a5db5de2683e0c1071ab4f140904bbf69" @@ -5220,7 +5340,7 @@ dependencies: "@babel/types" "^7.3.0" -"@types/bluebird@*": +"@types/bluebird@*", "@types/bluebird@3.5.33": version "3.5.33" resolved "https://registry.yarnpkg.com/@types/bluebird/-/bluebird-3.5.33.tgz#d79c020f283bd50bd76101d7d300313c107325fc" integrity sha512-ndEo1xvnYeHxm7I/5sF6tBvnsA4Tdi3zj1keRKRs12SP+2ye2A27NDJ1B6PqkfMbGAcT+mqQVqbZRIrhfOp5PQ== @@ -5278,16 +5398,11 @@ "@types/chai" "*" "@types/jquery" "*" -"@types/chai@*": +"@types/chai@*", "@types/chai@4.2.15": version "4.2.15" resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.2.15.tgz#b7a6d263c2cecf44b6de9a051cf496249b154553" integrity sha512-rYff6FI+ZTKAPkJUoyz7Udq3GaoDZnxYDEvdEdFZASiA7PoErltHezDishqQiSDWrGxvxmplH304jyzQmjp0AQ== -"@types/chai@4.2.7": - version "4.2.7" - resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.2.7.tgz#1c8c25cbf6e59ffa7d6b9652c78e547d9a41692d" - integrity sha512-luq8meHGYwvky0O7u0eQZdA7B4Wd9owUCqvbw2m3XCrCU8mplYOujMBbvyS547AxJkC+pGnd0Cm15eNxEUNU8g== - "@types/chalk@2.2.0", "@types/chalk@^2.2.0": version "2.2.0" resolved "https://registry.yarnpkg.com/@types/chalk/-/chalk-2.2.0.tgz#b7f6e446f4511029ee8e3f43075fb5b73fbaa0ba" @@ -5632,7 +5747,7 @@ resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.149.tgz#1342d63d948c6062838fbf961012f74d4e638440" integrity sha512-ijGqzZt/b7BfzcK9vTrS6MFljQRPn5BFWOx8oE0GYxribu6uV+aA9zZuXI1zc/etK9E8nrgdoF2+LgUw7+9tJQ== -"@types/lodash@^4.14.123": +"@types/lodash@4.14.168", "@types/lodash@^4.14.123": version "4.14.168" resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.168.tgz#fe24632e79b7ade3f132891afff86caa5e5ce008" integrity sha512-oVfRvqHV/V6D1yifJbVRU3TMp8OT6o6BG+U9MkwuJ3U8/CsDHvalRpsxBqivn71ztOFZBTfJMvETbqHiaNSj7Q== @@ -5730,7 +5845,7 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-14.6.2.tgz#264b44c5a28dfa80198fc2f7b6d3c8a054b9491f" integrity sha512-onlIwbaeqvZyniGPfdw/TEhKIh79pz66L1q06WUQqJLnAb6wbjvOtepLYTGHTqzdXgBYIE3ZdmqHDGsRsbBz7A== -"@types/node@^12.0.12": +"@types/node@^12.0.12", "@types/node@^12.12.29", "@types/node@^12.12.50": version "12.20.4" resolved "https://registry.yarnpkg.com/@types/node/-/node-12.20.4.tgz#73687043dd00fcb6962c60fbf499553a24d6bdf2" integrity sha512-xRCgeE0Q4pT5UZ189TJ3SpYuX/QGl6QIAOAIeDSbAVAd2gX1NxSZup4jNVK7cxIeP8KDSbJgcckun495isP1jQ== @@ -5770,7 +5885,7 @@ resolved "https://registry.yarnpkg.com/@types/parse5/-/parse5-6.0.0.tgz#38590dc2c3cf5717154064e3ee9b6947ee21b299" integrity sha512-oPwPSj4a1wu9rsXTEGIJz91ISU725t0BmSnUhb57sI+M8XEmvUop84lzuiYdq0Y5M6xLY8DBPg0C2xEQKLyvBA== -"@types/parsimmon@^1.3.0": +"@types/parsimmon@^1.10.1": version "1.10.6" resolved "https://registry.yarnpkg.com/@types/parsimmon/-/parsimmon-1.10.6.tgz#8fcf95990514d2a7624aa5f630c13bf2427f9cdd" integrity sha512-FwAQwMRbkhx0J6YELkwIpciVzCcgEqXEbIrIn3a2P5d3kGEHQ3wVhlN3YdVepYP+bZzCYO6OjmD4o9TGOZ40rA== @@ -5890,6 +6005,14 @@ "@types/chai" "*" "@types/sinon" "*" +"@types/sinon-chai@3.2.5": + version "3.2.5" + resolved "https://registry.yarnpkg.com/@types/sinon-chai/-/sinon-chai-3.2.5.tgz#df21ae57b10757da0b26f512145c065f2ad45c48" + integrity sha512-bKQqIpew7mmIGNRlxW6Zli/QVyc3zikpGzCa797B/tRnD9OtHvZ/ts8sYXV+Ilj9u3QRaUEM8xrjgd1gwm1BpQ== + dependencies: + "@types/chai" "*" + "@types/sinon" "*" + "@types/sinon@*": version "9.0.10" resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-9.0.10.tgz#7fb9bcb6794262482859cab66d59132fca18fcf7" @@ -5902,7 +6025,7 @@ resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-7.5.1.tgz#d27b81af0d1cfe1f9b24eebe7a24f74ae40f5b7c" integrity sha512-EZQUP3hSZQyTQRfiLqelC9NMWd1kqLcmQE0dMiklxBkgi84T+cHOhnKpgk4NnOWpGX863yE6+IaGnOXUNFqDnQ== -"@types/sinonjs__fake-timers@*", "@types/sinonjs__fake-timers@^6.0.1": +"@types/sinonjs__fake-timers@*", "@types/sinonjs__fake-timers@^6.0.2": version "6.0.2" resolved "https://registry.yarnpkg.com/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-6.0.2.tgz#3a84cf5ec3249439015e14049bd3161419bf9eae" integrity sha512-dIPoZ3g5gcx9zZEszaxLSVTvMReD3xxyyDnQUjA6IYDG9Ba2AV0otMPs+77sG9ojB4Qr2N2Vk5RnKeuA0X/0bg== @@ -7389,7 +7512,7 @@ aproba@^1.0.3, aproba@^1.1.1, aproba@^1.1.2: resolved "https://registry.yarnpkg.com/aproba/-/aproba-2.0.0.tgz#52520b8ae5b569215b354efc0caa3fe1e45a8adc" integrity sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ== -arch@^2.1.0, arch@^2.1.1, arch@^2.1.2: +arch@^2.1.0, arch@^2.1.1, arch@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/arch/-/arch-2.2.0.tgz#1bc47818f305764f23ab3306b0bfc086c5a29d11" integrity sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ== @@ -9333,7 +9456,7 @@ 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@2.0.2: +blob-util@2.0.2, 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== @@ -9343,6 +9466,13 @@ blob@0.0.5: resolved "https://registry.yarnpkg.com/blob/-/blob-0.0.5.tgz#d680eeef25f8cd91ad533f5b01eed48e64caf683" integrity sha512-gaqbzQPqOoamawKg0LGVd7SzLgXS+JH61oWprSLH+P+abTczqJbhTR8CmJ2u9/bUYNmHTGJx/UEmn6doAvvuig== +block-stream@*: + version "0.0.9" + resolved "https://registry.yarnpkg.com/block-stream/-/block-stream-0.0.9.tgz#13ebfe778a03205cfe03751481ebb4b3300c126a" + integrity sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo= + dependencies: + inherits "~2.0.0" + bluebird-lst@^1.0.9: version "1.0.9" resolved "https://registry.yarnpkg.com/bluebird-lst/-/bluebird-lst-1.0.9.tgz#a64a0e4365658b9ab5fe875eb9dfb694189bb41c" @@ -10001,13 +10131,6 @@ builtins@^1.0.0, builtins@^1.0.3: resolved "https://registry.yarnpkg.com/builtins/-/builtins-1.0.3.tgz#cb94faeb61c8696451db36534e1422f94f0aee88" integrity sha1-y5T662HIaWRR2zZTThQi+U8K7og= -builtins@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/builtins/-/builtins-2.0.1.tgz#42a4d6fe38973a7c185b435970d13e5e70f70f3c" - integrity sha512-XkkVe5QAb6guWPXTzpSrYpSlN3nqEmrrE2TkAr/tp7idSF6+MONh9WvKrAuR3HiKLvoSgmbs8l1U9IPmMrIoLw== - dependencies: - semver "^6.0.0" - byline@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/byline/-/byline-5.0.0.tgz#741c5216468eadc457b03410118ad77de8c1ddb1" @@ -10627,6 +10750,13 @@ charenc@0.0.2: resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667" integrity sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc= +charm@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/charm/-/charm-1.0.2.tgz#8add367153a6d9a581331052c4090991da995e35" + integrity sha1-it02cVOm2aWBMxBSxAkJkdqZXjU= + dependencies: + inherits "^2.0.1" + charset@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/charset/-/charset-1.0.1.tgz#8d59546c355be61049a8fa9164747793319852bd" @@ -10804,7 +10934,7 @@ chokidar@3.3.0: optionalDependencies: fsevents "~2.1.1" -chokidar@3.5.1, "chokidar@>=2.0.0 <4.0.0", chokidar@^3.0.0, chokidar@^3.3.0, chokidar@^3.4.1: +chokidar@3.5.1, "chokidar@>=2.0.0 <4.0.0", chokidar@^3.0.0, chokidar@^3.3.0, chokidar@^3.4.0, chokidar@^3.4.1: version "3.5.1" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.1.tgz#ee9ce7bbebd2b79f49f304799d5468e31e14e68a" integrity sha512-9+s+Od+W0VJJzawDma/gvBNQqkTiqYTWLuZoyAsivsI4AaWTCzHG06/TMjsf1cYe9Cb97UCEhjz7HvnPk2p/tw== @@ -10892,6 +11022,11 @@ ci-info@^2.0.0: resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46" integrity sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ== +ci-info@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.1.1.tgz#9a32fcefdf7bcdb6f0a7e1c0f8098ec57897b80a" + integrity sha512-kdRWLBIJwdsYJWYJFtAFFYxybguqeF91qpZaggjG5Nf8QKdizFG2hjqvaTXbxFIcYbSaD74KpAXv6BSm17DHEQ== + cidr-regex@^2.0.10: version "2.0.10" resolved "https://registry.yarnpkg.com/cidr-regex/-/cidr-regex-2.0.10.tgz#af13878bd4ad704de77d6dc800799358b3afa70d" @@ -11612,7 +11747,7 @@ concat-map@0.0.1: resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= -concat-stream@1.6.2, concat-stream@^1.4.7, concat-stream@^1.5.0, concat-stream@^1.6.0, concat-stream@^1.6.1, concat-stream@^1.6.2, concat-stream@~1.6.0: +concat-stream@1.6.2, concat-stream@^1.4.7, concat-stream@^1.5.0, concat-stream@^1.5.2, concat-stream@^1.6.0, concat-stream@^1.6.1, concat-stream@^1.6.2, concat-stream@~1.6.0: version "1.6.2" resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34" integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw== @@ -12122,6 +12257,13 @@ cross-env@6.0.3: dependencies: cross-spawn "^7.0.0" +cross-env@7.0.3: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-7.0.3.tgz#865264b29677dc015ba8418918965dd232fc54cf" + integrity sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw== + dependencies: + cross-spawn "^7.0.1" + cross-fetch@3.0.5: version "3.0.5" resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.0.5.tgz#2739d2981892e7ab488a7ad03b92df2816e03f4c" @@ -12181,7 +12323,7 @@ cross-spawn@^5.0.1, cross-spawn@^5.1.0: shebang-command "^1.2.0" which "^1.2.9" -cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3: +cross-spawn@^7.0.0, cross-spawn@^7.0.1, cross-spawn@^7.0.2, cross-spawn@^7.0.3: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== @@ -12926,7 +13068,7 @@ dateformat@^3.0.0, dateformat@^3.0.2: resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-3.0.3.tgz#a6e37499a4d9a9cf85ef5872044d62901c9889ae" integrity sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q== -dayjs@^1.10.3, dayjs@^1.9.3: +dayjs@^1.10.3, dayjs@^1.10.4, dayjs@^1.9.3: version "1.10.4" resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.10.4.tgz#8e544a9b8683f61783f570980a8a80eaf54ab1e2" integrity sha512-RI/Hh4kqRc1UKLOAf/T5zdMMX5DQIlDxwUe3wSyMMnEbGunnpENCdbUgM+dW7kXidZqCttBrmw7BhN4TMddkCw== @@ -13234,22 +13376,6 @@ defined@^1.0.0: resolved "https://registry.yarnpkg.com/defined/-/defined-1.0.0.tgz#c98d9bcef75674188e110969151199e39b1fa693" integrity sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM= -definitelytyped-header-parser@1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/definitelytyped-header-parser/-/definitelytyped-header-parser-1.2.0.tgz#f21374b8a18fabcd3ba4b008a0bcbc9db2b7b4c4" - integrity sha512-xpg8uu/2YD/reaVsZV4oJ4g7UDYFqQGWvT1W9Tsj6q4VtWBSaig38Qgah0ZMnQGF9kAsAim08EXDO1nSi0+Nog== - dependencies: - "@types/parsimmon" "^1.3.0" - parsimmon "^1.2.0" - -definitelytyped-header-parser@^3.8.2: - version "3.9.0" - resolved "https://registry.yarnpkg.com/definitelytyped-header-parser/-/definitelytyped-header-parser-3.9.0.tgz#f992abb8e62f697ca25e1adbfd5f69ef11621644" - integrity sha512-slbwZ5h5lasB12t+9EAGYr060aCMqEXp6cwD7CoTriK40HNDYU56/XQ6S4sbjBK8ReGRMnB/uDx0elKkb4kuQA== - dependencies: - "@types/parsimmon" "^1.3.0" - parsimmon "^1.2.0" - defs@~1.1.0: version "1.1.1" resolved "https://registry.yarnpkg.com/defs/-/defs-1.1.1.tgz#b22609f2c7a11ba7a3db116805c139b1caffa9d2" @@ -13370,18 +13496,18 @@ dependency-check@2.9.1: read-package-json "^2.0.4" resolve "^1.1.7" -dependency-check@3.4.1: - version "3.4.1" - resolved "https://registry.yarnpkg.com/dependency-check/-/dependency-check-3.4.1.tgz#54f3301ac8e1f75c3cfeff8b02c466af8fbedc29" - integrity sha512-YMTTpvHX1wP2FwEMWWFaD2WoiH261iRiXZQPqkUxeBb+5FfZ2XjTN1BJbi2zx/8Y23hzUCTpRlvnyB8tBK2bMg== +dependency-check@4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/dependency-check/-/dependency-check-4.1.0.tgz#d45405cabb50298f8674fe28ab594c8a5530edff" + integrity sha512-nlw+PvhVQwg0gSNNlVUiuRv0765gah9pZEXdQlIFzeSnD85Eex0uM0bkrAWrHdeTzuMGZnR9daxkup/AqqgqzA== dependencies: - builtins "^2.0.0" debug "^4.0.0" detective "^5.0.2" - globby "^9.2.0" + globby "^10.0.1" is-relative "^1.0.0" - micromatch "^3.0.0" + micromatch "^4.0.2" minimist "^1.2.0" + pkg-up "^3.1.0" read-package-json "^2.0.10" resolve "^1.1.7" @@ -13969,28 +14095,33 @@ dotenv@^5.0.1: resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-5.0.1.tgz#a5317459bd3d79ab88cff6e44057a6a3fbb1fcef" integrity sha512-4As8uPrjfwb7VXC+WnLCbXK7y+Ueb2B3zgNCePYfhxS1PYeaO1YTeplffTEcbfLhvFNGLAz90VvJs9yomG7bow== -dts-critic@^2.0.0: - version "2.2.4" - resolved "https://registry.yarnpkg.com/dts-critic/-/dts-critic-2.2.4.tgz#3ef70be891eede2bdf2ff9e293d9ec398b9cf772" - integrity sha512-yGHhVKo66iyPBFUYRyXX1uW+XEG3/HDP1pHCR3VlPl9ho8zRHy6lzS5k+gCSuINqjNsV8UjZSUXUuTuw0wHp7g== +dts-critic@latest: + version "3.3.4" + resolved "https://registry.yarnpkg.com/dts-critic/-/dts-critic-3.3.4.tgz#c15b7d4724190b8afaca7646f38271332f46dad7" + integrity sha512-OjLTrSBCFbi1tDAiOXcP7G20W3HI3eIzkpSpLwvH7oDFZYdqFCMe9lsNhMZFXqsNcSTpRg3+PBS4fF27+h1qew== dependencies: + "@definitelytyped/header-parser" latest command-exists "^1.2.8" - definitelytyped-header-parser "^3.8.2" + rimraf "^3.0.2" semver "^6.2.0" - yargs "^12.0.5" + tmp "^0.2.1" + yargs "^15.3.1" -dtslint@0.9.0: - version "0.9.0" - resolved "https://registry.yarnpkg.com/dtslint/-/dtslint-0.9.0.tgz#d6aab256bdd8bbd9c1d28506e89fdf837573dc95" - integrity sha512-Y2Qdi5AgqYUeiERrBIQQ0VbBQgzFIna9uDnaamsNfgGFi60EYtoUs0ZOwaUqSOJAVeHwg/YkRbYWFVTAKwBAiw== +dtslint@4.0.7: + version "4.0.7" + resolved "https://registry.yarnpkg.com/dtslint/-/dtslint-4.0.7.tgz#dad7c0de4b2f48bcc5ee79432248cadc7a5a1a38" + integrity sha512-gwpBnxky+vUfCL74U5ao+wQf4sw9jD+cZ9ukiTFrkwkhNibqfyOZyg4cnFf1lB0Hm5ZFSQdi09DdjarDQLgofA== dependencies: - definitelytyped-header-parser "1.2.0" - dts-critic "^2.0.0" + "@definitelytyped/header-parser" latest + "@definitelytyped/typescript-versions" latest + "@definitelytyped/utils" latest + dts-critic latest fs-extra "^6.0.1" - request "^2.88.0" + json-stable-stringify "^1.0.1" strip-json-comments "^2.0.1" tslint "5.14.0" - typescript next + tsutils "^2.29.0" + yargs "^15.1.0" duplexer2@^0.1.2, duplexer2@^0.1.4, duplexer2@~0.1.0, duplexer2@~0.1.2: version "0.1.4" @@ -15314,7 +15445,7 @@ eventemitter2@6.4.2: resolved "https://registry.yarnpkg.com/eventemitter2/-/eventemitter2-6.4.2.tgz#f31f8b99d45245f0edbc5b00797830ff3b388970" integrity sha512-r/Pwupa5RIzxIHbEKCkNXqpEQIIT4uQDxmP4G/Lug/NokVUWj0joz/WzWl3OxRpC5kDrH/WdiUJoR+IrwvXJEw== -eventemitter2@^6.4.2: +eventemitter2@^6.4.3: version "6.4.4" resolved "https://registry.yarnpkg.com/eventemitter2/-/eventemitter2-6.4.4.tgz#aa96e8275c4dbeb017a5d0e03780c65612a1202b" integrity sha512-HLU3NDY6wARrLCEwyGKRBvuWYyvW6mHYv72SJJAH3iJN3a6eVUvkjFkcxah1bcTgGVBBrFdIopBJPhCQFMLyXw== @@ -15487,7 +15618,7 @@ execa@4.0.2: signal-exit "^3.0.2" strip-final-newline "^2.0.0" -execa@4.1.0, execa@^4.0.0, execa@^4.0.2: +execa@4.1.0, execa@^4.0.0: version "4.1.0" resolved "https://registry.yarnpkg.com/execa/-/execa-4.1.0.tgz#4e5491ad1572f2f17a77d388c6c857135b22847a" integrity sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA== @@ -16824,7 +16955,7 @@ fs-extra@^6.0.1: jsonfile "^4.0.0" universalify "^0.1.0" -fs-extra@^9.0.0, fs-extra@^9.0.1: +fs-extra@^9.0.0, fs-extra@^9.0.1, fs-extra@^9.1.0: version "9.1.0" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d" integrity sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ== @@ -16918,6 +17049,16 @@ fsevents@~2.3.1: resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== +fstream@^1.0.12: + version "1.0.12" + resolved "https://registry.yarnpkg.com/fstream/-/fstream-1.0.12.tgz#4e8ba8ee2d48be4f7d0de505455548eae5932045" + integrity sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg== + dependencies: + graceful-fs "^4.1.2" + inherits "~2.0.0" + mkdirp ">=0.5 0" + rimraf "2" + fsu@^1.0.2: version "1.1.1" resolved "https://registry.yarnpkg.com/fsu/-/fsu-1.1.1.tgz#bd36d3579907c59d85b257a75b836aa9e0c31834" @@ -17479,6 +17620,13 @@ global-dirs@^2.0.1: dependencies: ini "1.3.7" +global-dirs@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/global-dirs/-/global-dirs-3.0.0.tgz#70a76fe84ea315ab37b1f5576cbde7d48ef72686" + integrity sha512-v8ho2DS5RiCjftj1nD9NmnfaOzTdud7RRnVd9kFNOjqZbISlx5DQ+OrTkywgd0dIt7oFCvKetZSHoHcP3sDdiA== + dependencies: + ini "2.0.0" + global-modules@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-2.0.0.tgz#997605ad2345f27f51539bea26574421215c7780" @@ -18209,7 +18357,7 @@ hasha@5.0.0: is-stream "^1.1.0" type-fest "^0.3.0" -hasha@^5.0.0: +hasha@5.2.2, hasha@^5.0.0: version "5.2.2" resolved "https://registry.yarnpkg.com/hasha/-/hasha-5.2.2.tgz#a48477989b3b327aea3c04f53096d816d97522a1" integrity sha512-Hrp5vIK/xr5SkeN2onO32H0MgNZ0f17HRNH39WfL0SYUNOTZ5Lz1TJ8Pajo/87dYGEFlLMm7mIc/k/s6Bvz9HQ== @@ -19023,7 +19171,7 @@ inflight@^1.0.4, inflight@~1.0.6: once "^1.3.0" wrappy "1" -inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.1, inherits@~2.0.3, inherits@~2.0.4: +inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.0, inherits@~2.0.1, inherits@~2.0.3, inherits@~2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -19043,6 +19191,11 @@ ini@1.3.7: resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.7.tgz#a09363e1911972ea16d7a8851005d84cf09a9a84" integrity sha512-iKpRpXP+CrP2jyrxvg1kMUpXDyRUFDWurxbnVT1vQPx+Wz9uCYsMIqYuSBLV+PAaZG/d7kRLKRFc9oDMsH+mFQ== +ini@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ini/-/ini-2.0.0.tgz#e5fd556ecdd5726be978fa1001862eacb0a94bc5" + integrity sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA== + ini@^1.3.2, ini@^1.3.4, ini@^1.3.5, ini@^1.3.8, ini@~1.3.0, ini@~1.3.3: version "1.3.8" resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" @@ -19403,6 +19556,13 @@ is-ci@^1.0.10: dependencies: ci-info "^1.5.0" +is-ci@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-3.0.0.tgz#c7e7be3c9d8eef7d0fa144390bd1e4b88dc4c994" + integrity sha512-kDXyttuLeslKAHYL/K28F2YkM3x5jvFPEw3yXbRptXydjD9rpLEz+C5K5iutY9ZiUu6AP41JdvRQwF4Iqs4ZCQ== + dependencies: + ci-info "^3.1.1" + is-cidr@^3.0.0: version "3.1.1" resolved "https://registry.yarnpkg.com/is-cidr/-/is-cidr-3.1.1.tgz#e92ef121bdec2782271a77ce487a8b8df3718ab7" @@ -19601,7 +19761,7 @@ is-installed-globally@^0.1.0: global-dirs "^0.1.0" is-path-inside "^1.0.0" -is-installed-globally@^0.3.1, is-installed-globally@^0.3.2: +is-installed-globally@^0.3.1: version "0.3.2" resolved "https://registry.yarnpkg.com/is-installed-globally/-/is-installed-globally-0.3.2.tgz#fd3efa79ee670d1187233182d5b0a1dd00313141" integrity sha512-wZ8x1js7Ia0kecP/CHM/3ABkAmujX7WPvQk6uu3Fly/Mk44pySulQpnHG46OMjHGXApINnV4QhY3SWnECO2z5g== @@ -19609,6 +19769,14 @@ is-installed-globally@^0.3.1, is-installed-globally@^0.3.2: global-dirs "^2.0.1" is-path-inside "^3.0.1" +is-installed-globally@~0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/is-installed-globally/-/is-installed-globally-0.4.0.tgz#9a0fd407949c30f86eb6959ef1b7994ed0b7b520" + integrity sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ== + dependencies: + global-dirs "^3.0.0" + is-path-inside "^3.0.2" + is-integer@^1.0.4: version "1.0.7" resolved "https://registry.yarnpkg.com/is-integer/-/is-integer-1.0.7.tgz#6bde81aacddf78b659b6629d629cadc51a886d5c" @@ -22481,7 +22649,7 @@ lodash@4.17.4: resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae" integrity sha1-eCA6TRwyiuHYbcpkYONptX9AVa4= -"lodash@>=3.5 <5", lodash@^4.13.1, lodash@^4.14.0, lodash@^4.16.2, lodash@^4.16.4, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.1, lodash@^4.3.0, lodash@~4.17.2: +"lodash@>=3.5 <5", lodash@^4.13.1, lodash@^4.14.0, lodash@^4.16.2, lodash@^4.16.4, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.1, lodash@^4.3.0, lodash@~4.17.2: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -23103,7 +23271,7 @@ micromatch@^2.1.5, micromatch@^2.3.7: parse-glob "^3.0.4" regex-cache "^0.4.2" -micromatch@^3.0.0, micromatch@^3.0.4, micromatch@^3.1.10, micromatch@^3.1.4, micromatch@^3.1.8, micromatch@^3.1.9: +micromatch@^3.0.4, micromatch@^3.1.10, micromatch@^3.1.4, micromatch@^3.1.8, micromatch@^3.1.9: version "3.1.10" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23" integrity sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg== @@ -23470,7 +23638,7 @@ mkdirp@0.5.3: dependencies: minimist "^1.2.5" -mkdirp@0.5.5, mkdirp@0.5.x, mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@^0.5.3, mkdirp@^0.5.4, mkdirp@^0.5.5, mkdirp@~0.5.0, mkdirp@~0.5.1: +mkdirp@0.5.5, mkdirp@0.5.x, "mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@^0.5.3, mkdirp@^0.5.4, mkdirp@^0.5.5, mkdirp@~0.5.0, mkdirp@~0.5.1: version "0.5.5" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ== @@ -24376,6 +24544,16 @@ nock@12.0.3: lodash "^4.17.13" propagate "^2.0.0" +nock@13.0.7: + version "13.0.7" + resolved "https://registry.yarnpkg.com/nock/-/nock-13.0.7.tgz#9bc718c66bd0862dfa14601a9ba678a406127910" + integrity sha512-WBz73VYIjdbO6BwmXODRQLtn7B5tldA9pNpWJe5QTtTEscQlY5KXU4srnGzBOK2fWakkXj69gfTnXGzmrsaRWw== + dependencies: + debug "^4.1.0" + json-stringify-safe "^5.0.1" + lodash.set "^4.3.2" + propagate "^2.0.0" + node-abi@^2.7.0: version "2.19.3" resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-2.19.3.tgz#252f5dcab12dad1b5503b2d27eddd4733930282d" @@ -24664,7 +24842,7 @@ normalize-html-whitespace@1.0.0: resolved "https://registry.yarnpkg.com/normalize-html-whitespace/-/normalize-html-whitespace-1.0.0.tgz#5e3c8e192f1b06c3b9eee4b7e7f28854c7601e34" integrity sha512-9ui7CGtOOlehQu0t/OhhlmDyc71mKVlv+4vF+me4iZLPrNtRL2xoquEdfZxasC/bdQi/Hr3iTrpyRKIG+ocabA== -normalize-package-data@^2.0.0, normalize-package-data@^2.3.0, normalize-package-data@^2.3.2, normalize-package-data@^2.3.4, normalize-package-data@^2.3.5, normalize-package-data@^2.4.0, normalize-package-data@^2.5.0: +normalize-package-data@^2.0.0, normalize-package-data@^2.3.0, normalize-package-data@^2.3.2, normalize-package-data@^2.3.4, normalize-package-data@^2.3.5, normalize-package-data@^2.4.0, normalize-package-data@^2.5.0, "normalize-package-data@~1.0.1 || ^2.0.0": version "2.5.0" resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8" integrity sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA== @@ -24806,7 +24984,7 @@ npm-normalize-package-bin@^1.0.0, npm-normalize-package-bin@^1.0.1: resolved "https://registry.yarnpkg.com/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz#6e79a41f23fd235c0623218228da7d9c23b8f6e2" integrity sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA== -"npm-package-arg@^4.0.0 || ^5.0.0 || ^6.0.0", npm-package-arg@^6.0.0, npm-package-arg@^6.1.0, npm-package-arg@^6.1.1: +"npm-package-arg@^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0", "npm-package-arg@^4.0.0 || ^5.0.0 || ^6.0.0", npm-package-arg@^6.0.0, npm-package-arg@^6.1.0, npm-package-arg@^6.1.1: version "6.1.1" resolved "https://registry.yarnpkg.com/npm-package-arg/-/npm-package-arg-6.1.1.tgz#02168cb0a49a2b75bf988a28698de7b529df5cb7" integrity sha512-qBpssaL3IOZWi5vEKUKW0cO7kzLeT+EQO9W8RsLOZf76KF9E/K9+wH0C7t06HXPpaH8WH5xF1MExLuCwbTqRUg== @@ -24843,6 +25021,25 @@ npm-profile@^4.0.2, npm-profile@^4.0.4: figgy-pudding "^3.4.1" npm-registry-fetch "^4.0.0" +npm-registry-client@^8.6.0: + version "8.6.0" + resolved "https://registry.yarnpkg.com/npm-registry-client/-/npm-registry-client-8.6.0.tgz#7f1529f91450732e89f8518e0f21459deea3e4c4" + integrity sha512-Qs6P6nnopig+Y8gbzpeN/dkt+n7IyVd8f45NTMotGk6Qo7GfBmzwYx6jRLoOOgKiMnaQfYxsuyQlD8Mc3guBhg== + dependencies: + concat-stream "^1.5.2" + graceful-fs "^4.1.6" + normalize-package-data "~1.0.1 || ^2.0.0" + npm-package-arg "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0" + once "^1.3.3" + request "^2.74.0" + retry "^0.10.0" + safe-buffer "^5.1.1" + semver "2 >=2.2.1 || 3.x || 4 || 5" + slide "^1.1.3" + ssri "^5.2.4" + optionalDependencies: + npmlog "2 || ^3.1.0 || ^4.0.0" + npm-registry-fetch@^4.0.0, npm-registry-fetch@^4.0.7: version "4.0.7" resolved "https://registry.yarnpkg.com/npm-registry-fetch/-/npm-registry-fetch-4.0.7.tgz#57951bf6541e0246b34c9f9a38ab73607c9449d7" @@ -25025,7 +25222,7 @@ npm@^6.14.9: worker-farm "^1.7.0" write-file-atomic "^2.4.3" -npmlog@^4.0.1, npmlog@^4.1.2, npmlog@~4.1.2: +"npmlog@2 || ^3.1.0 || ^4.0.0", npmlog@^4.0.1, npmlog@^4.1.2, npmlog@~4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b" integrity sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg== @@ -25297,7 +25494,7 @@ on-headers@~1.0.1, on-headers@~1.0.2: resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.2.tgz#772b0ae6aaa525c399e489adfad90c403eb3c28f" integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA== -once@1.x, once@^1.3.0, once@^1.3.1, once@^1.3.2, once@^1.4.0, once@~1.4.0: +once@1.x, once@^1.3.0, once@^1.3.1, once@^1.3.2, once@^1.3.3, once@^1.4.0, once@~1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= @@ -26051,7 +26248,7 @@ parseurl@^1.3.2, parseurl@~1.3.2, parseurl@~1.3.3: resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== -parsimmon@^1.2.0: +parsimmon@^1.13.0: version "1.16.0" resolved "https://registry.yarnpkg.com/parsimmon/-/parsimmon-1.16.0.tgz#2834e3db645b6a855ab2ea14fbaad10d82867e0f" integrity sha512-tekGDz2Lny27SQ/5DzJdIK0lqsWwZ667SCLFIDCxaZM7VNgQjyKLbaL7FYPKpbjdxNAXFV/mSxkq5D2fnkW4pA== @@ -27405,6 +27602,11 @@ postinstall-postinstall@2.0.0: resolved "https://registry.yarnpkg.com/postinstall-postinstall/-/postinstall-postinstall-2.0.0.tgz#7ba6711b4420575c4f561638836a81faad47f43f" integrity sha512-3f6qWexsHiT4WKtZc5DRb0FPLilHtARi5KpY4fqban/DJNn8/YhZH8U7dVKVz51WbOxEnR31gV+qYQhvEdHtdQ== +postinstall-postinstall@2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/postinstall-postinstall/-/postinstall-postinstall-2.1.0.tgz#4f7f77441ef539d1512c40bd04c71b06a4704ca3" + integrity sha512-7hQX6ZlZXIoRiWNrbMQaLzUUfH+sSx39u8EJ9HYuDc1kLo9IXKWjM5RSquZN1ad5GnH8CGFM78fsAAQi3OKEEQ== + prebuild-install@^5.2.4, prebuild-install@^5.3.5: version "5.3.6" resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-5.3.6.tgz#7c225568d864c71d89d07f8796042733a3f54291" @@ -27527,7 +27729,7 @@ prettier@^1.16.4, prettier@^1.18.2, prettier@^1.7.0: resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.19.1.tgz#f7d7f5ff8a9cd872a7be4ca142095956a60797cb" integrity sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew== -pretty-bytes@^5.1.0, pretty-bytes@^5.4.1: +pretty-bytes@^5.1.0, pretty-bytes@^5.6.0: version "5.6.0" resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb" integrity sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg== @@ -29469,7 +29671,7 @@ request-promise@^4.2.2: stealthy-require "^1.1.1" tough-cookie "^2.3.3" -request@^2.87.0, request@^2.88.0, request@^2.88.2: +request@^2.74.0, request@^2.87.0, request@^2.88.0, request@^2.88.2: version "2.88.2" resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3" integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw== @@ -29809,6 +30011,13 @@ right-align@^0.1.1: dependencies: align-text "^0.1.1" +rimraf@2, rimraf@^2.2.8, rimraf@^2.5.2, rimraf@^2.5.4, rimraf@^2.6.2, rimraf@^2.6.3, rimraf@^2.7.1: + version "2.7.1" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" + integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== + dependencies: + glob "^7.1.3" + rimraf@2.6.2: version "2.6.2" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.2.tgz#2ed8150d24a16ea8651e6d6ef0f47c4158ce7a36" @@ -29830,13 +30039,6 @@ rimraf@3.0.2, rimraf@^3.0.0, rimraf@^3.0.2: dependencies: glob "^7.1.3" -rimraf@^2.2.8, rimraf@^2.5.2, rimraf@^2.5.4, rimraf@^2.6.2, rimraf@^2.6.3, rimraf@^2.7.1: - version "2.7.1" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" - integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== - dependencies: - glob "^7.1.3" - ripemd160@^2.0.0, ripemd160@^2.0.1: version "2.0.2" resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.2.tgz#a1c1a6f624751577ba5d07914cbc92850585890c" @@ -30381,7 +30583,7 @@ semver-regex@^2.0.0: resolved "https://registry.yarnpkg.com/semver-regex/-/semver-regex-2.0.0.tgz#a93c2c5844539a770233379107b38c7b4ac9d338" integrity sha512-mUdIBBvdn0PLOeP3TEkMH7HHeUP3GjsXCwKarjv/kGmUFOYg1VqEemKhoQpWMu6X2I8kHeuVdGibLGkVK+/5Qw== -"semver@2 || 3 || 4 || 5", "semver@2.x || 3.x || 4 || 5", "semver@^2.3.0 || 3.x || 4 || 5", semver@^5.0.3, semver@^5.1.0, semver@^5.3.0, semver@^5.4.1, semver@^5.5.0, semver@^5.5.1, semver@^5.6.0, semver@^5.7.0, semver@^5.7.1: +"semver@2 >=2.2.1 || 3.x || 4 || 5", "semver@2 || 3 || 4 || 5", "semver@2.x || 3.x || 4 || 5", "semver@^2.3.0 || 3.x || 4 || 5", semver@^5.0.3, semver@^5.1.0, semver@^5.3.0, semver@^5.4.1, semver@^5.5.0, semver@^5.5.1, semver@^5.6.0, semver@^5.7.0, semver@^5.7.1: version "5.7.1" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== @@ -30683,7 +30885,7 @@ shelljs@0.8.3: interpret "^1.0.0" rechoir "^0.6.2" -shelljs@^0.8.1, shelljs@^0.8.3: +shelljs@0.8.4, shelljs@^0.8.1, shelljs@^0.8.3: version "0.8.4" resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.8.4.tgz#de7684feeb767f8716b326078a8a00875890e3c2" integrity sha512-7gk3UZ9kOfPLIAbslLzyWeGiEqx9e3rxwZM0KE6EL8GlGwjym9Mrlx5/p33bWTu9YG6vcS4MBxYZDHYr5lr8BQ== @@ -30960,7 +31162,7 @@ slice-ansi@^4.0.0: astral-regex "^2.0.0" is-fullwidth-code-point "^3.0.0" -slide@^1.1.5, slide@^1.1.6, slide@~1.1.3, slide@~1.1.6: +slide@^1.1.3, slide@^1.1.5, slide@^1.1.6, slide@~1.1.3, slide@~1.1.6: version "1.1.6" resolved "https://registry.yarnpkg.com/slide/-/slide-1.1.6.tgz#56eb027d65b4d2dce6cb2e2d32c4d4afc9e1d707" integrity sha1-VusCfWW00tzmyy4tMsTUr8nh1wc= @@ -31028,6 +31230,25 @@ snap-shot-core@10.2.1: quote "0.4.0" ramda "0.26.1" +snap-shot-core@10.2.4: + version "10.2.4" + resolved "https://registry.yarnpkg.com/snap-shot-core/-/snap-shot-core-10.2.4.tgz#feacadc77f2e83e9eee52fc4aaff31c25a1790c3" + integrity sha512-A7tkcfmvnRKge4VzFLAWA4UYMkvFY4TZKyL+D6hnHjI3HJ4pTepjG5DfR2ACeDKMzCSTQ5EwR2iOotI+Z37zsg== + dependencies: + arg "4.1.3" + check-more-types "2.24.0" + common-tags "1.8.0" + debug "4.3.1" + escape-quotes "1.0.2" + folktale "2.3.2" + is-ci "2.0.0" + jsesc "2.5.2" + lazy-ass "1.6.0" + mkdirp "1.0.4" + pluralize "8.0.0" + quote "0.4.0" + ramda "0.27.1" + snap-shot-it@7.9.2: version "7.9.2" resolved "https://registry.yarnpkg.com/snap-shot-it/-/snap-shot-it-7.9.2.tgz#575302f8b3881fde851bdaa99c65e1fd9760bb98" @@ -31062,6 +31283,23 @@ snap-shot-it@7.9.3: snap-shot-compare "3.0.0" snap-shot-core "10.2.0" +snap-shot-it@7.9.6: + version "7.9.6" + resolved "https://registry.yarnpkg.com/snap-shot-it/-/snap-shot-it-7.9.6.tgz#042c168980a1dc3ba7ffe2bb2beeaa8e9512772d" + integrity sha512-t/ADZfQ8EUk4J76S5cmynye7qg1ecUFqQfANiOMNy0sFmYUaqfx9K/AWwpdcpr3vFsDptM+zSuTtKD0A1EOLqA== + dependencies: + "@bahmutov/data-driven" "1.0.0" + check-more-types "2.24.0" + common-tags "1.8.0" + debug "4.3.1" + has-only "1.1.1" + its-name "1.0.0" + lazy-ass "1.6.0" + pluralize "8.0.0" + ramda "0.27.1" + snap-shot-compare "3.0.0" + snap-shot-core "10.2.4" + snapdragon-node@^2.0.1: version "2.1.1" resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b" @@ -31636,6 +31874,13 @@ ssl-root-cas@1.3.1: dependencies: "@coolaj86/urequest" "^1.3.6" +ssri@^5.2.4: + version "5.3.0" + resolved "https://registry.yarnpkg.com/ssri/-/ssri-5.3.0.tgz#ba3872c9c6d33a0704a7d71ff045e5ec48999d06" + integrity sha512-XRSIPqLij52MtgoQavH/x/dU1qVKtWUAAZeOHsR9c2Ddi4XerFy3mc1alf+dLJKl9EUIm/Ht+EowFkTUOA6GAQ== + dependencies: + safe-buffer "^5.1.1" + ssri@^6.0.0, ssri@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/ssri/-/ssri-6.0.1.tgz#2a3c41b28dd45b62b63676ecb74001265ae9edd8" @@ -32329,7 +32574,7 @@ supports-color@6.1.0, supports-color@^6.1.0: dependencies: has-flag "^3.0.0" -supports-color@8.1.1: +supports-color@8.1.1, supports-color@^8.1.1: version "8.1.1" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== @@ -32367,7 +32612,7 @@ supports-color@^5.0.0, supports-color@^5.3.0, supports-color@^5.4.0, supports-co dependencies: has-flag "^3.0.0" -supports-color@^7.0.0, supports-color@^7.1.0, supports-color@^7.2.0: +supports-color@^7.0.0, supports-color@^7.1.0: version "7.2.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== @@ -32548,6 +32793,15 @@ tar-stream@^2.1.4: inherits "^2.0.3" readable-stream "^3.1.1" +tar@^2.2.2: + version "2.2.2" + resolved "https://registry.yarnpkg.com/tar/-/tar-2.2.2.tgz#0ca8848562c7299b8b446ff6a4d60cdbb23edc40" + integrity sha512-FCEhQ/4rE1zYv9rYXJw/msRqsnmlje5jHP6huWeBZ704jUTy02c5AZyWujpMR1ax6mVw9NyJMfuK2CMDWVIfgA== + dependencies: + block-stream "*" + fstream "^1.0.12" + inherits "2" + tar@^4.4.10, tar@^4.4.12, tar@^4.4.13, tar@^4.4.8: version "4.4.13" resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.13.tgz#43b364bc52888d555298637b10d60790254ab525" @@ -32986,7 +33240,7 @@ tmp@0.1.0: dependencies: rimraf "^2.6.3" -tmp@~0.2.1: +tmp@^0.2.1, tmp@~0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.1.tgz#8457fc3037dcf4719c251367a1af6500ee1ccf14" integrity sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ== @@ -33538,11 +33792,6 @@ typescript@^3.0.3, typescript@^3.8.3, typescript@^3.9.7: resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.9.tgz#e69905c54bc0681d0518bd4d587cc6f2d0b1a674" integrity sha512-kdMjTiekY+z/ubJCATUPlRDl39vXYiMV9iyeMuEuXZh2we6zz80uovNN2WlAxmmdE/Z/YQe+EbOEXB5RHEED3w== -typescript@next: - version "4.3.0-dev.20210227" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.3.0-dev.20210227.tgz#4bdedd4f10cd4bb10d1a1969b144f020c1d79914" - integrity sha512-b/MDOrhIvJ9l5KSuYLpn9xjgvy8+4RvqCSfgxYCVE1krtPUU9mUwGhyKy3tAc25ipySrIw7Bfq0mJ9OEetPTRQ== - ua-parser-js@^0.7.18: version "0.7.24" resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.24.tgz#8d3ecea46ed4f1f1d63ec25f17d8568105dc027c" @@ -36410,7 +36659,7 @@ yargs@^14.2.2, yargs@^14.2.3: y18n "^4.0.0" yargs-parser "^15.0.1" -yargs@^15.0.1, yargs@^15.0.2: +yargs@^15.0.1, yargs@^15.0.2, yargs@^15.1.0, yargs@^15.3.1: version "15.4.1" resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.4.1.tgz#0d87a16de01aee9d8bec2bfbf74f67851730f4f8" integrity sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A== From 77b922472c91df74c2edc9092900f860a75a26d2 Mon Sep 17 00:00:00 2001 From: Chris Breiding Date: Mon, 1 Mar 2021 09:23:14 -0500 Subject: [PATCH 009/134] fix: Improve uncaught error handling (#14826) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Chris Breiding Co-authored-by: Mateusz Burzyński --- .gitignore | 4 + cli/types/cypress.d.ts | 4 +- cli/types/tests/actions.ts | 3 +- cli/types/tests/cypress-tests.ts | 6 +- .../cypress/integration/app_spec.js | 14 +- .../cypress/integration/release_notes_spec.js | 6 +- packages/desktop-gui/webpack.config.ts | 2 +- packages/driver/cypress/fixtures/errors.html | 69 +++++++ .../fixtures/isolated-runner-inner.html | 16 -- .../integration/commands/screenshot_spec.js | 6 +- .../cypress/integration/commands/xhr_spec.js | 16 +- .../cypress/integration/cy/timers_spec.js | 192 +++++++++--------- .../integration/cypress/error_utils_spec.js | 12 +- .../integration/e2e/uncaught_errors_spec.js | 183 ++++++++--------- packages/driver/cypress/plugins/index.js | 2 +- packages/driver/src/cy/commands/screenshot.js | 29 ++- packages/driver/src/cy/commands/xhr.js | 6 +- packages/driver/src/cy/errors.js | 43 +--- packages/driver/src/cy/listeners.js | 4 +- .../driver/src/cy/net-stubbing/add-command.ts | 2 +- .../src/cy/net-stubbing/events/index.ts | 8 +- packages/driver/src/cy/timers.js | 125 +----------- packages/driver/src/cypress.js | 12 +- packages/driver/src/cypress/cy.js | 84 ++++---- packages/driver/src/cypress/error_messages.js | 9 +- packages/driver/src/cypress/error_utils.js | 77 ++++++- packages/driver/src/cypress/local_storage.js | 3 +- packages/driver/src/cypress/runner.js | 20 +- packages/proxy/lib/http/util/inject.ts | 21 +- packages/proxy/lib/http/util/rewriter.ts | 2 +- packages/reporter/webpack.config.ts | 2 +- packages/runner-ct/webpack.config.ts | 2 +- packages/runner/README.md | 2 + .../cypress/fixtures/errors/hooks_spec.js | 18 ++ .../cypress/fixtures/errors/uncaught_spec.js | 45 +++- .../fixtures/errors/unexpected_spec.js | 10 +- .../cypress/integration/issues/issue-8350.js | 21 -- .../integration/reporter.errors.spec.js | 178 ++++++++++++---- .../cypress/integration/studio.ui.spec.js | 5 + packages/runner/cypress/support/helpers.js | 122 ----------- .../runner/cypress/support/verify-failures.js | 162 +++++++++++++++ packages/runner/injection/.eslintrc.json | 6 + packages/runner/injection/index.js | 33 +++ packages/runner/injection/timers.js | 121 +++++++++++ packages/runner/lib/resolve-dist.js | 5 + packages/runner/package.json | 3 + packages/runner/webpack.config.ts | 27 ++- .../__snapshots__/3_js_error_handling_spec.js | 6 +- .../test/e2e/3_js_error_handling_spec.js | 6 + .../test/integration/http_requests_spec.js | 145 ++++++------- .../server/test/integration/server_spec.js | 33 +-- .../fixtures/server/expected_head_inject.html | 8 +- .../server/expected_https_inject.html | 7 +- .../server/expected_no_head_tag_inject.html | 8 +- packages/ui-components/webpack.config.ts | 2 +- packages/web-config/webpack.config.base.ts | 87 +++++--- scripts/binary/util/testStaticAssets.js | 8 +- 57 files changed, 1214 insertions(+), 838 deletions(-) create mode 100644 packages/driver/cypress/fixtures/errors.html create mode 100644 packages/runner/cypress/fixtures/errors/hooks_spec.js delete mode 100644 packages/runner/cypress/integration/issues/issue-8350.js create mode 100644 packages/runner/cypress/support/verify-failures.js create mode 100644 packages/runner/injection/.eslintrc.json create mode 100644 packages/runner/injection/index.js create mode 100644 packages/runner/injection/timers.js diff --git a/.gitignore b/.gitignore index 820bc5c8c7..e06bbdd9d4 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,10 @@ packages/desktop-gui/src/jsconfig.json packages/driver/cypress/videos packages/driver/cypress/screenshots +# from runner +packages/runner/cypress/videos +packages/runner/cypress/screenshots + # npm packages npm/**/cypress/screenshots diff --git a/cli/types/cypress.d.ts b/cli/types/cypress.d.ts index 0ae257ff90..5b53e0a00f 100644 --- a/cli/types/cypress.d.ts +++ b/cli/types/cypress.d.ts @@ -5158,7 +5158,7 @@ declare namespace Cypress { */ interface Actions { /** - * Fires when an uncaught exception occurs in your application. + * Fires when an uncaught exception or unhandled rejection occurs in your application. If it's an unhandled rejection, the rejected promise will be the 3rd argument. * Cypress will fail the test when this fires. * Return `false` from this event and Cypress will not fail the test. Also useful for debugging purposes because the actual `error` instance is provided to you. * @see https://on.cypress.io/catalog-of-events#App-Events @@ -5184,7 +5184,7 @@ declare namespace Cypress { }) ``` */ - (action: 'uncaught:exception', fn: (error: Error, runnable: Mocha.Runnable) => false | void): Cypress + (action: 'uncaught:exception', fn: (error: Error, runnable: Mocha.Runnable, promise?: Promise) => false | void): Cypress /** * Fires when your app calls the global `window.confirm()` method. * Cypress will auto accept confirmations. Return `false` from this event and the confirmation will be canceled. diff --git a/cli/types/tests/actions.ts b/cli/types/tests/actions.ts index 977076dba9..e31b294e25 100644 --- a/cli/types/tests/actions.ts +++ b/cli/types/tests/actions.ts @@ -1,6 +1,7 @@ -Cypress.on('uncaught:exception', (error, runnable) => { +Cypress.on('uncaught:exception', (error, runnable, promise) => { error // $ExpectType Error runnable // $ExpectType Runnable + promise // $ExpectType Promise | undefined }) Cypress.on('window:confirm', (text) => { diff --git a/cli/types/tests/cypress-tests.ts b/cli/types/tests/cypress-tests.ts index 57b2d28ca7..0881c09e51 100644 --- a/cli/types/tests/cypress-tests.ts +++ b/cli/types/tests/cypress-tests.ts @@ -326,14 +326,16 @@ namespace CypressAUTWindowTests { } namespace CypressOnTests { - Cypress.on('uncaught:exception', (error, runnable) => { + Cypress.on('uncaught:exception', (error, runnable, promise) => { error // $ExpectType Error runnable // $ExpectType Runnable + promise // $ExpectType Promise | undefined }) - cy.on('uncaught:exception', (error, runnable) => { + cy.on('uncaught:exception', (error, runnable, promise) => { error // $ExpectType Error runnable // $ExpectType Runnable + promise // $ExpectType Promise | undefined }) // you can chain multiple callbacks diff --git a/packages/desktop-gui/cypress/integration/app_spec.js b/packages/desktop-gui/cypress/integration/app_spec.js index c56b17b106..69346a20e5 100644 --- a/packages/desktop-gui/cypress/integration/app_spec.js +++ b/packages/desktop-gui/cypress/integration/app_spec.js @@ -38,7 +38,14 @@ describe('App', function () { }) it('sends name, stack, message to gui:error on unhandled rejection', function () { - const err = new Error('foo') + const message = 'intentional error' + + cy.on('uncaught:exception', (err) => { + // we expect the error, so don't let it fail the test + if (err.message.includes(message)) return false + }) + + const err = new Error(message) this.win.foo = () => { return this.win.Promise.reject(err) @@ -46,13 +53,12 @@ describe('App', function () { setTimeout(() => { return this.win.foo() - } - , 0) + }, 0) cy.wrap({}).should(function () { expect(this.ipc.guiError).to.be.calledWithExactly({ name: 'Error', - message: 'foo', + message, stack: err.stack, }) }) diff --git a/packages/desktop-gui/cypress/integration/release_notes_spec.js b/packages/desktop-gui/cypress/integration/release_notes_spec.js index 67e29382a5..5b78bbe762 100644 --- a/packages/desktop-gui/cypress/integration/release_notes_spec.js +++ b/packages/desktop-gui/cypress/integration/release_notes_spec.js @@ -53,8 +53,10 @@ describe('Release Notes', () => { }) it('shows update instructions if getting release notes errors', () => { - getReleaseNotes.reject(new Error('something went wrong')) - cy.get('.update-notice').contains('Learn more').click() + cy.get('.update-notice').contains('Learn more').click().then(() => { + getReleaseNotes.reject(new Error('something went wrong')) + }) + cy.contains('Update to Version 1.2.3').should('be.visible') }) diff --git a/packages/desktop-gui/webpack.config.ts b/packages/desktop-gui/webpack.config.ts index fae1d961ad..06bd469f01 100644 --- a/packages/desktop-gui/webpack.config.ts +++ b/packages/desktop-gui/webpack.config.ts @@ -1,4 +1,4 @@ -import getCommonConfig, { HtmlWebpackPlugin } from '@packages/web-config/webpack.config.base' +import { getCommonConfig, HtmlWebpackPlugin } from '@packages/web-config/webpack.config.base' import path from 'path' import webpack from 'webpack' diff --git a/packages/driver/cypress/fixtures/errors.html b/packages/driver/cypress/fixtures/errors.html new file mode 100644 index 0000000000..c3b6f2b9d0 --- /dev/null +++ b/packages/driver/cypress/fixtures/errors.html @@ -0,0 +1,69 @@ + + + + Page Title + + +
+

Go to Visit Error

+ + + + +
+

+

+ + + + diff --git a/packages/driver/cypress/fixtures/isolated-runner-inner.html b/packages/driver/cypress/fixtures/isolated-runner-inner.html index 895fc7379f..2cd3e6468d 100644 --- a/packages/driver/cypress/fixtures/isolated-runner-inner.html +++ b/packages/driver/cypress/fixtures/isolated-runner-inner.html @@ -4,21 +4,5 @@ Isolated Runner Fixture -
- - -
- - diff --git a/packages/driver/cypress/integration/commands/screenshot_spec.js b/packages/driver/cypress/integration/commands/screenshot_spec.js index 34bbd4ca0e..44e006d6f1 100644 --- a/packages/driver/cypress/integration/commands/screenshot_spec.js +++ b/packages/driver/cypress/integration/commands/screenshot_spec.js @@ -156,6 +156,7 @@ describe('src/cy/commands/screenshot', () => { it('takes screenshot when not isInteractive', function () { Cypress.config('isInteractive', false) + Cypress.config('screenshotOnRunFailure', true) cy.stub(Screenshot, 'getConfig').returns(this.screenshotConfig) Cypress.automation.withArgs('take:screenshot').resolves(this.serverResult) @@ -210,6 +211,7 @@ describe('src/cy/commands/screenshot', () => { const runnable = cy.state('runnable') Cypress.action('runner:runnable:after:run:async', test, runnable) + .delay(1) // before:screenshot promise requires a tick .then(() => { expect(Cypress.automation.withArgs('take:screenshot')).to.be.calledOnce let args = Cypress.automation.withArgs('take:screenshot').args[0][1] @@ -280,6 +282,7 @@ describe('src/cy/commands/screenshot', () => { context('#screenshot', () => { beforeEach(function () { cy.stub(Screenshot, 'getConfig').returns(this.screenshotConfig) + cy.stub(cy, 'pauseTimers').resolves() }) it('sets name to undefined when not passed name', function () { @@ -335,7 +338,6 @@ describe('src/cy/commands/screenshot', () => { it('pauses then unpauses timers if disableTimersAndAnimations is true', function () { Cypress.automation.withArgs('take:screenshot').resolves(this.serverResult) cy.spy(Cypress, 'action').log(false) - cy.spy(cy, 'pauseTimers') cy .screenshot('foo') @@ -353,7 +355,7 @@ describe('src/cy/commands/screenshot', () => { cy .screenshot('foo') .then(() => { - expect(Cypress.action.withArgs('cy:pause:timers')).not.to.be.called + expect(cy.pauseTimers).not.to.be.called }) }) diff --git a/packages/driver/cypress/integration/commands/xhr_spec.js b/packages/driver/cypress/integration/commands/xhr_spec.js index 7d2f5a193f..08034a7866 100644 --- a/packages/driver/cypress/integration/commands/xhr_spec.js +++ b/packages/driver/cypress/integration/commands/xhr_spec.js @@ -1022,19 +1022,12 @@ describe('src/cy/commands/xhr', () => { }) it('sets err on log when caused by code errors', function (done) { - const uncaughtException = cy.stub().returns(true) - - cy.on('uncaught:exception', uncaughtException) - cy.on('fail', (err) => { const { lastLog } = this expect(this.logs.length).to.eq(1) expect(lastLog.get('name')).to.eq('xhr') expect(lastLog.get('error').message).contain('foo is not defined') - // since this is AUT code, we should allow error to be caught in 'uncaught:exception' hook - // https://github.com/cypress-io/cypress/issues/987 - expect(uncaughtException).calledOnce done() }) @@ -1057,8 +1050,8 @@ describe('src/cy/commands/xhr', () => { expect(this.logs.length).to.eq(1) expect(lastLog.get('name')).to.eq('xhr') - expect(err).to.eq(lastLog.get('error')) - expect(err).to.eq(e) + expect(err.message).to.include(lastLog.get('error').message) + expect(err.message).to.include(e.message) done() }) @@ -1227,7 +1220,6 @@ describe('src/cy/commands/xhr', () => { alias: 'getFoo', aliasType: 'route', type: 'parent', - error: err, instrument: 'command', message: '', event: true, @@ -1239,6 +1231,8 @@ describe('src/cy/commands/xhr', () => { expect(value).deep.eq(lastLog.get(key), `expected key: ${key} to eq value: ${value}`) }) + expect(err.message).to.include(lastLog.get('error').message) + done() }) @@ -1950,7 +1944,7 @@ describe('src/cy/commands/xhr', () => { // route + window + xhr log === 3 expect(this.logs.length).to.eq(3) expect(lastLog.get('name')).to.eq('xhr') - expect(err).to.eq(lastLog.get('error')) + expect(err.message).to.include(lastLog.get('error').message) done() }) diff --git a/packages/driver/cypress/integration/cy/timers_spec.js b/packages/driver/cypress/integration/cy/timers_spec.js index 8598788297..0db6a7314b 100644 --- a/packages/driver/cypress/integration/cy/timers_spec.js +++ b/packages/driver/cypress/integration/cy/timers_spec.js @@ -167,49 +167,52 @@ describe('driver/src/cy/timers', () => { const rafStub = cy .stub() - .callsFake(() => { + .callsFake((arg) => { win.bar = 'bar' }) // prevent timers from firing, add to queue - cy.pauseTimers(true) - - const id1 = win.requestAnimationFrame(rafStub) - - expect(id1).to.eq(1) - - cy - .wait(100) - .log('requestAnimationFrame should NOT have fired when paused') - .window().its('bar').should('be.null') - .log('requestAnimationFrame should now fire when unpaused') + return cy.pauseTimers(true) .then(() => { - // now go ahead and run all the queued timers - cy.pauseTimers(false) + const id1 = win.requestAnimationFrame(rafStub) - expect(win.bar).to.eq('bar') + expect(id1).to.eq(1) - // requestAnimationFrame should have passed through - // its high res timestamp from performance.now() - expect(rafStub).to.be.calledWithMatch(Number) + cy + .wait(100) + .log('requestAnimationFrame should NOT have fired when paused') + .window().its('bar').should('be.null') + .log('requestAnimationFrame should now fire when unpaused') + .then(() => { + // now go ahead and run all the queued timers + return cy.pauseTimers(false) + }) + .then(() => { + expect(win.bar).to.eq('bar') + + // requestAnimationFrame should have passed through + // its high res timestamp from performance.now() + expect(rafStub).to.be.calledWithMatch(Number) + }) + .then(() => { + win.bar = 'foo' + + return cy.pauseTimers(true) + }) + .then(() => { + const id2 = win.requestAnimationFrame(rafStub) + + expect(id2).to.eq(2) + + const ret = win.cancelAnimationFrame(id2) + + expect(ret).to.be.undefined + + return cy.pauseTimers(false) + }) + .wait(100) + .window().its('bar').should('eq', 'foo') }) - .then(() => { - win.bar = 'foo' - - cy.pauseTimers(true) - - const id2 = win.requestAnimationFrame(rafStub) - - expect(id2).to.eq(2) - - const ret = win.cancelAnimationFrame(id2) - - expect(ret).to.be.undefined - - cy.pauseTimers(false) - }) - .wait(100) - .window().its('bar').should('eq', 'foo') }) }) @@ -224,40 +227,43 @@ describe('driver/src/cy/timers', () => { } // prevent timers from firing, add to queue - cy.pauseTimers(true) - - const id1 = win.setTimeout(win.setBar, 1) - - expect(id1).to.eq(timerNumber(1)) - - cyWaitTimeout(1) - .log('setTimeout should NOT have fired when paused') - .window().its('bar').should('be.null') - .log('setTimeout should now fire when unpaused') + return cy.pauseTimers(true) .then(() => { - // now go ahead and run all the queued timers - cy.pauseTimers(false) + const id1 = win.setTimeout(win.setBar, 1) - expect(win.bar).to.eq('bar') + expect(id1).to.eq(timerNumber(1)) + + cyWaitTimeout(1) + .log('setTimeout should NOT have fired when paused') + .window().its('bar').should('be.null') + .log('setTimeout should now fire when unpaused') + .then(() => { + // now go ahead and run all the queued timers + return cy.pauseTimers(false) + }) + .then(() => { + expect(win.bar).to.eq('bar') + }) + .then(() => { + win.bar = 'foo' + + return cy.pauseTimers(true) + }) + .then(() => { + const id2 = win.setTimeout(win.setBar, 1) + + expect(id2).to.eq(timerNumber(2)) + + const ret = win.clearTimeout(id2) + + expect(ret).to.be.undefined + + return cy.pauseTimers(false) + }) + + cyWaitTimeout(1) + .window().its('bar').should('eq', 'foo') }) - .then(() => { - win.bar = 'foo' - - cy.pauseTimers(true) - - const id2 = win.setTimeout(win.setBar, 1) - - expect(id2).to.eq(timerNumber(2)) - - const ret = win.clearTimeout(id2) - - expect(ret).to.be.undefined - - cy.pauseTimers(false) - }) - - cyWaitTimeout(1) - .window().its('bar').should('eq', 'foo') }) }) @@ -278,32 +284,35 @@ describe('driver/src/cy/timers', () => { // timers increment and always start at 0 expect(id1).to.eq(timerNumber(1)) - cy.pauseTimers(true) - - cyWaitTimeout(10) - - cy.window().its('bar').should('be.null') - .log('setTimeout should be immediately flushed after unpausing') + return cy.pauseTimers(true) .then(() => { - cy.pauseTimers(false) + cyWaitTimeout(10) - expect(win.bar).to.eq('bar') + cy.window().its('bar').should('be.null') + .log('setTimeout should be immediately flushed after unpausing') + .then(() => { + return cy.pauseTimers(false) + }) + .then(() => { + expect(win.bar).to.eq('bar') + }) + .log('canceling the timeout after timers are paused still cancels') + .then(() => { + win.bar = null + + const id2 = win.setInterval(win.setBar, 10) + + expect(id2).to.eq(timerNumber(2)) + + return cy.pauseTimers(true) + .then(() => { + // clearing interval on a timer is officially supported by browsers + win.clearInterval(id2) + }) + }) + .wait(100) + .window().its('bar').should('be.null') }) - .log('canceling the timeout after timers are paused still cancels') - .then(() => { - win.bar = null - - const id2 = win.setInterval(win.setBar, 10) - - expect(id2).to.eq(timerNumber(2)) - - cy.pauseTimers(true) - - // clearing interval on a timer is officially supported by browsers - win.clearInterval(id2) - }) - .wait(100) - .window().its('bar').should('be.null') }) }) @@ -396,8 +405,9 @@ describe('driver/src/cy/timers', () => { cy .wait(200) .then(() => { - cy.pauseTimers(false) - + return cy.pauseTimers(false) + }) + .then(() => { expect(cancelAfter3Calls).to.be.calledThrice }) }) diff --git a/packages/driver/cypress/integration/cypress/error_utils_spec.js b/packages/driver/cypress/integration/cypress/error_utils_spec.js index c8cc4740cd..976a8037cd 100644 --- a/packages/driver/cypress/integration/cypress/error_utils_spec.js +++ b/packages/driver/cypress/integration/cypress/error_utils_spec.js @@ -465,33 +465,33 @@ describe('driver/src/cypress/error_utils', () => { }) it('mutates the error passed in and returns it', () => { - const result = $errUtils.createUncaughtException('spec', err) + const result = $errUtils.createUncaughtException('spec', 'error', err) expect(result).to.equal(err) }) it('replaces message with wrapper message for spec error', () => { - const result = $errUtils.createUncaughtException('spec', err) + const result = $errUtils.createUncaughtException('spec', 'error', err) expect(result.message).to.include('The following error originated from your test code, not from Cypress') expect(result.message).to.include('> original message') }) it('replaces message with wrapper message for app error', () => { - const result = $errUtils.createUncaughtException('app', err) + const result = $errUtils.createUncaughtException('app', 'error', err) expect(result.message).to.include('The following error originated from your application code, not from Cypress') expect(result.message).to.include('> original message') }) it('replaces original name and message in stack', () => { - const result = $errUtils.createUncaughtException('spec', err) + const result = $errUtils.createUncaughtException('spec', 'error', err) expect(result.stack).not.to.include('Error: original message') }) it('retains the stack of the original error', () => { - const result = $errUtils.createUncaughtException('spec', err) + const result = $errUtils.createUncaughtException('spec', 'error', err) expect(result.stack).to.include('at foo (path/to/file:1:1)') }) @@ -499,7 +499,7 @@ describe('driver/src/cypress/error_utils', () => { it('adds docsUrl for app error and original error', () => { err.docsUrl = 'https://on.cypress.io/orginal-error-docs-url' - const result = $errUtils.createUncaughtException('app', err) + const result = $errUtils.createUncaughtException('app', 'error', err) expect(result.docsUrl).to.eql([ 'https://on.cypress.io/uncaught-exception-from-application', diff --git a/packages/driver/cypress/integration/e2e/uncaught_errors_spec.js b/packages/driver/cypress/integration/e2e/uncaught_errors_spec.js index bd3b3af01d..a01597f2b6 100644 --- a/packages/driver/cypress/integration/e2e/uncaught_errors_spec.js +++ b/packages/driver/cypress/integration/e2e/uncaught_errors_spec.js @@ -1,24 +1,8 @@ -const { _ } = Cypress - describe('uncaught errors', () => { - beforeEach(function () { - this.logs = [] - - cy.on('log:added', (attrs, log) => { - this.lastLog = log - - return this.logs.push(log) - }) - - return null - }) - - it('logs visit failure once', function (done) { + it('runnable does not have timer visit failure', function (done) { const r = cy.state('runnable') cy.on('fail', () => { - expect(this.logs.length).to.eq(1) - // this runnable should not have a timer expect(r.timer).not.to.be.ok @@ -31,16 +15,16 @@ describe('uncaught errors', () => { // when this beforeEach hook fails // it will skip invoking the test // but run the other suite - cy.visit('/fixtures/visit_error.html') + cy.visit('/fixtures/errors.html?error-on-visit') }) - it('can turn off uncaught exception handling via cy', () => { + it('return false from cy.on(uncaught:exception) to pass test', () => { const r = cy.state('runnable') cy.on('uncaught:exception', (err, runnable) => { try { - expect(err.name).to.eq('ReferenceError') - expect(err.message).to.include('foo is not defined') + expect(err.name).to.eq('Error') + expect(err.message).to.include('sync error') expect(err.message).to.include('The following error originated from your application code, not from Cypress.') expect(err.message).to.not.include('https://on.cypress.io/uncaught-exception-from-application') expect(err.docsUrl).to.deep.eq(['https://on.cypress.io/uncaught-exception-from-application']) @@ -52,110 +36,107 @@ describe('uncaught errors', () => { } }) - cy.visit('/fixtures/visit_error.html') + cy.visit('/fixtures/errors.html') + cy.get('.trigger-sync-error').click() }) - it('can turn off uncaught exception handling via Cypress', () => { + it('return false from Cypress.on(uncaught:exception) to pass test', () => { const r = cy.state('runnable') Cypress.once('uncaught:exception', (err, runnable) => { - expect(err.message).to.include('foo is not defined') + expect(err.message).to.include('sync error') expect(runnable === r).to.be.true return false }) - cy.visit('/fixtures/visit_error.html') + cy.visit('/fixtures/errors.html') + cy.get('.trigger-sync-error').click() }) - it('logs click error once', function (done) { - let uncaught = false - - cy.on('uncaught:exception', () => { - uncaught = true - - return true - }) - - cy.on('fail', (err) => { - const { lastLog } = this - - expect(this.logs.length).to.eq(4) - expect(uncaught).to.be.true - expect(err.message).to.include('uncaught click error') - expect(lastLog.get('name')).to.eq('click') - expect(lastLog.get('error')).to.eq(err) + it('sync error triggers uncaught:exception', (done) => { + cy.once('uncaught:exception', (err) => { + expect(err.stack).to.include('sync error') + expect(err.stack).to.include('one') + expect(err.stack).to.include('two') + expect(err.stack).to.include('three') done() }) - cy - .visit('/fixtures/jquery.html') - .window().then((win) => { - return win.$('button:first').on('click', () => { - throw new Error('uncaught click error') - }) - }).get('button:first').click() - }) - - it('logs error on page load when new page has uncaught exception', function (done) { - let uncaught = false - - cy.on('uncaught:exception', () => { - uncaught = true - - return true - }) - - cy.on('fail', (err) => { - const click = _.find(this.logs, (log) => { - return log.get('name') === 'click' - }) - - // visit, window, contains, click, page loading, new url - expect(this.logs.length).to.eq(6) - expect(uncaught).to.be.true - expect(err.message).to.include('foo is not defined') - expect(click.get('name')).to.eq('click') - - // TODO: when there's an uncaught exception event - // we should log this to the command log so then - // we could update this test to always reference - // that command log - // - // FIXME: in firefox this test sometimes fails - // because the cy.click() command resolves before - // the page navigation event occurs and therefore - // the state('current') command is null'd out and - // firefox does not highlight the click command in read - // expect(click.get('error')).to.eq(err) - - done() - }) - - cy - .visit('/fixtures/jquery.html') - .window().then((win) => { - return win.$('visit') - .appendTo(win.document.body) - }) - .contains('visit').click() - - cy.url().should('include', 'visit_error.html') + cy.visit('/fixtures/errors.html') + cy.get('.trigger-sync-error').click() }) // https://github.com/cypress-io/cypress/issues/987 - it('global onerror', (done) => { + it('async error triggers uncaught:exception', (done) => { cy.once('uncaught:exception', (err) => { - expect(err.stack).contain('foo is not defined') - expect(err.stack).contain('one') - expect(err.stack).contain('two') - expect(err.stack).contain('three') + expect(err.stack).to.include('async error') + expect(err.stack).to.include('one') + expect(err.stack).to.include('two') + expect(err.stack).to.include('three') done() }) - cy.visit('/fixtures/global-error.html') + cy.visit('/fixtures/errors.html') + cy.get('.trigger-async-error').click() + }) + + it('unhandled rejection triggers uncaught:exception and has promise as third argument', (done) => { + cy.once('uncaught:exception', (err, runnable, promise) => { + expect(err.stack).to.include('promise rejection') + expect(err.stack).to.include('one') + expect(err.stack).to.include('two') + expect(err.stack).to.include('three') + expect(promise).to.be.a('promise') + + done() + }) + + cy.visit('/fixtures/errors.html') + cy.get('.trigger-unhandled-rejection').click() + }) + + // if we mutate the error, the app's listeners for 'error' or + // 'unhandledrejection' will have our wrapped error instead of the original + it('original error is not mutated for "error"', () => { + cy.once('uncaught:exception', () => false) + + cy.visit('/fixtures/errors.html') + cy.get('.trigger-sync-error').click() + cy.get('.error-one').invoke('text').should('equal', 'sync error') + cy.get('.error-two').invoke('text').should('equal', 'sync error') + }) + + it('original error is not mutated for "unhandledrejection"', () => { + cy.once('uncaught:exception', () => false) + + cy.visit('/fixtures/errors.html') + cy.get('.trigger-unhandled-rejection').click() + cy.get('.error-one').invoke('text').should('equal', 'promise rejection') + cy.get('.error-two').invoke('text').should('equal', 'promise rejection') + }) + + // we used to define window.onerror ourselves for catching uncaught errors, + // so if an app overwrote it, we wouldn't catch them. now we use + // window.addEventListener('error'), so it's no longer an issue + it('fails correctly for uncaught error on a site with window.onerror handler defined', function (done) { + let uncaughtErr = false + + cy.once('uncaught:exception', () => { + uncaughtErr = true + }) + + cy.on('fail', (err) => { + expect(err.message).to.include('sync error') + expect(uncaughtErr).to.eq(true) + done() + }) + + cy.visit('/fixtures/errors.html') + cy.get('.define-window-onerror').click() + cy.get('.trigger-sync-error').click() }) // https://github.com/cypress-io/cypress/issues/7590 diff --git a/packages/driver/cypress/plugins/index.js b/packages/driver/cypress/plugins/index.js index c022233084..0aa63efb50 100644 --- a/packages/driver/cypress/plugins/index.js +++ b/packages/driver/cypress/plugins/index.js @@ -8,7 +8,7 @@ const Promise = require('bluebird') const wp = require('@cypress/webpack-preprocessor') process.env.NO_LIVERELOAD = '1' -const webpackOptions = require('@packages/runner/webpack.config.ts').default +const [webpackOptions] = require('@packages/runner/webpack.config.ts').default // set mode to development which overrides // the 'none' value of the base webpack config diff --git a/packages/driver/src/cy/commands/screenshot.js b/packages/driver/src/cy/commands/screenshot.js index e8957e3150..c9ee8d8937 100644 --- a/packages/driver/src/cy/commands/screenshot.js +++ b/packages/driver/src/cy/commands/screenshot.js @@ -317,19 +317,24 @@ const takeScreenshot = (Cypress, state, screenshotConfig, options = {}) => { } const before = () => { - if (disableTimersAndAnimations) { - cy.pauseTimers(true) - } - - return sendAsync('before:screenshot', getOptions(true)) + return Promise.try(() => { + if (disableTimersAndAnimations) { + return cy.pauseTimers(true) + } + }) + .then(() => { + return sendAsync('before:screenshot', getOptions(true)) + }) } const after = () => { send('after:screenshot', getOptions(false)) - if (disableTimersAndAnimations) { - return cy.pauseTimers(false) - } + return Promise.try(() => { + if (disableTimersAndAnimations) { + return cy.pauseTimers(false) + } + }) } const automationOptions = _.extend({}, options, { @@ -391,7 +396,13 @@ module.exports = function (Commands, Cypress, cy, state, config) { Cypress.on('runnable:after:run:async', (test, runnable) => { const screenshotConfig = $Screenshot.getConfig() - if (!test.err || !screenshotConfig.screenshotOnRunFailure || config('isInteractive') || test.err.isPending || !config('screenshotOnRunFailure')) { + if ( + !test.err + || !screenshotConfig.screenshotOnRunFailure + || config('isInteractive') + || test.err.isPending + || !config('screenshotOnRunFailure') + ) { return } diff --git a/packages/driver/src/cy/commands/xhr.js b/packages/driver/src/cy/commands/xhr.js index 4fb8d4c34e..bd1d77310d 100644 --- a/packages/driver/src/cy/commands/xhr.js +++ b/packages/driver/src/cy/commands/xhr.js @@ -214,9 +214,9 @@ const startXhrServer = (cy, state, config) => { log.snapshot('error').error(err) } - // re-throw the error since this came from AUT code, and needs to - // cause an 'uncaught:exception' event. This error will be caught in - // top.onerror with stack as 5th argument. + // cause an 'uncaught:exception' event, since this error originally + // occurs in the user's application. this will be caught by + // top.addEventListener('error') throw err }, diff --git a/packages/driver/src/cy/errors.js b/packages/driver/src/cy/errors.js index 7d10e1bc62..02f0aee777 100644 --- a/packages/driver/src/cy/errors.js +++ b/packages/driver/src/cy/errors.js @@ -1,11 +1,7 @@ -const _ = require('lodash') - const $dom = require('../dom') const $errUtils = require('../cypress/error_utils') -const crossOriginScriptRe = /^script error/i - -const create = (state, config, log) => { +const create = (state, log) => { const commandErr = (err) => { const current = state('current') @@ -36,42 +32,15 @@ const create = (state, config, log) => { }) } - const createUncaughtException = (type, args) => { - let [message, source, lineno, colno, err] = args // eslint-disable-line no-unused-vars - let docsUrl - - // reset the message on a cross origin script error - // since no details are accessible - if (crossOriginScriptRe.test(message)) { - const crossOriginErr = $errUtils.errByPath('uncaught.cross_origin_script') - - message = crossOriginErr.message - docsUrl = crossOriginErr.docsUrl - } - - // if we have the 5th argument it means we're in a modern browser with an - // error object already provided. otherwise, we create one - // it's possible the error was thrown as a string (throw 'some error') - // so create it in the case it's not already an object - err = _.isObject(err) ? err : $errUtils.errByPath('uncaught.error', { - source, - lineno, - // if the error was thrown as a string (throw 'some error'), `err` is - // the message ('some error') and message is some browser-created - // variant (e.g. 'Uncaught some error') - message: _.isString(err) ? err : message, - }) - - err.docsUrl = docsUrl - - const uncaughtErr = $errUtils.createUncaughtException(type, err) + const createUncaughtException = (frameType, handlerType, originalErr) => { + const err = $errUtils.createUncaughtException(frameType, handlerType, originalErr) const current = state('current') - uncaughtErr.onFail = () => { - current?.getLastLog()?.error(uncaughtErr) + err.onFail = () => { + current?.getLastLog()?.error(err) } - return uncaughtErr + return err } const commandRunningFailed = (err) => { diff --git a/packages/driver/src/cy/listeners.js b/packages/driver/src/cy/listeners.js index fe200dea0b..b593c2f4af 100644 --- a/packages/driver/src/cy/listeners.js +++ b/packages/driver/src/cy/listeners.js @@ -49,8 +49,8 @@ module.exports = { listenersAdded = true - // set onerror global handler - contentWindow.onerror = callbacks.onError + addListener(contentWindow, 'error', callbacks.onError('error')) + addListener(contentWindow, 'unhandledrejection', callbacks.onError('unhandledrejection')) addListener(contentWindow, 'beforeunload', (e) => { // bail if we've canceled this event (from another source) diff --git a/packages/driver/src/cy/net-stubbing/add-command.ts b/packages/driver/src/cy/net-stubbing/add-command.ts index 5e636e1d4e..262d38522c 100644 --- a/packages/driver/src/cy/net-stubbing/add-command.ts +++ b/packages/driver/src/cy/net-stubbing/add-command.ts @@ -151,7 +151,7 @@ function validateRouteMatcherOptions (routeMatcher: RouteMatcherOptions): { isVa } export function addCommand (Commands, Cypress: Cypress.Cypress, cy: Cypress.cy, state: Cypress.State) { - const { emitNetEvent } = registerEvents(Cypress) + const { emitNetEvent } = registerEvents(Cypress, cy) function getNewRouteLog (matcher: RouteMatcherOptions, isStubbed: boolean, alias: string | void, staticResponse?: StaticResponse) { let obj: Partial = { diff --git a/packages/driver/src/cy/net-stubbing/events/index.ts b/packages/driver/src/cy/net-stubbing/events/index.ts index d2049ba72f..7ee4431853 100644 --- a/packages/driver/src/cy/net-stubbing/events/index.ts +++ b/packages/driver/src/cy/net-stubbing/events/index.ts @@ -18,7 +18,7 @@ const netEventHandlers: { [eventName: string]: HandlerFn } = { 'http:request:complete': onRequestComplete, } -export function registerEvents (Cypress: Cypress.Cypress) { +export function registerEvents (Cypress: Cypress.Cypress, cy: Cypress.cy) { const { state } = Cypress function getRoute (routeHandlerId) { @@ -46,11 +46,7 @@ export function registerEvents (Cypress: Cypress.Cypress) { function failCurrentTest (err: Error) { // @ts-ignore - // FIXME: asynchronous errors are not correctly attributed to spec when they come from `top`, must manually attribute - err.fromSpec = true - // @ts-ignore - // FIXME: throw inside of a setImmediate so that the error does not end up as an unhandled ~rejection~, since we do not correctly handle them - setImmediate(() => Cypress.cy.fail(err)) + cy.fail(err) } Cypress.on('test:before:run', () => { diff --git a/packages/driver/src/cy/timers.js b/packages/driver/src/cy/timers.js index 9f61ac51ed..c61a043116 100644 --- a/packages/driver/src/cy/timers.js +++ b/packages/driver/src/cy/timers.js @@ -1,132 +1,15 @@ -const _ = require('lodash') - -const create = () => { - let paused - let flushing - let timerQueue - let canceledTimerIds - +const create = (Cypress) => { const reset = () => { - paused = false - flushing = false - timerQueue = [] - canceledTimerIds = {} + return Cypress.action('app:timers:reset') } - const isPaused = () => { - return paused + const pauseTimers = (shouldPause) => { + return Cypress.action('app:timers:pause', shouldPause) } - const invoke = (contentWindow, fnOrCode, params = []) => { - if (_.isFunction(fnOrCode)) { - return fnOrCode.apply(contentWindow, params) - } - - return contentWindow.eval(fnOrCode) - } - - const flushTimerQueue = () => { - flushing = true - - _.each(timerQueue, (timer) => { - const { timerId, type, fnOrCode, params, contentWindow } = timer - - // if we are a setInterval and we're been canceled - // then just return. this can happen when a setInterval - // queues many callbacks, and from within that callback - // we would have canceled the original setInterval - if (type === 'setInterval' && canceledTimerIds[timerId]) { - return - } - - invoke(contentWindow, fnOrCode, params) - }) - - reset() - } - - const pauseTimers = (pause) => { - paused = Boolean(pause) - - if (!paused) { - flushTimerQueue() - } - } - - const wrap = (contentWindow) => { - const originals = { - setTimeout: contentWindow.setTimeout, - setInterval: contentWindow.setInterval, - requestAnimationFrame: contentWindow.requestAnimationFrame, - clearTimeout: contentWindow.clearTimeout, - clearInterval: contentWindow.clearInterval, - // cancelAnimationFrame: contentWindow.cancelAnimationFrame, - } - - const callThrough = (fnName, args) => { - return originals[fnName].apply(contentWindow, args) - } - - const wrapCancel = (fnName) => { - return (timerId) => { - if (flushing) { - canceledTimerIds[timerId] = true - } - - return callThrough(fnName, [timerId]) - } - } - - const wrapTimer = (fnName) => { - return (...args) => { - let timerId - let [fnOrCode, delay, ...params] = args - - const timerOverride = (...params) => { - // if we're currently paused then we need - // to enqueue this timer callback and invoke - // it immediately once we're unpaused - if (paused) { - timerQueue.push({ - timerId, - fnOrCode, - params, - contentWindow, - type: fnName, - }) - - return - } - - // else go ahead and invoke the real function - // the same way the browser otherwise would - return invoke(contentWindow, fnOrCode, params) - } - - timerId = callThrough(fnName, [timerOverride, delay, ...params]) - - return timerId - } - } - - contentWindow.setTimeout = wrapTimer('setTimeout') - contentWindow.setInterval = wrapTimer('setInterval') - contentWindow.requestAnimationFrame = wrapTimer('requestAnimationFrame') - contentWindow.clearTimeout = wrapCancel('clearTimeout') - contentWindow.clearInterval = wrapCancel('clearInterval') - // contentWindow.cancelAnimationFrame = wrapFn('cancelAnimationFrame') - } - - // always initially reset to set the state - reset() - return { - wrap, - reset, - isPaused, - pauseTimers, } } diff --git a/packages/driver/src/cypress.js b/packages/driver/src/cypress.js index c847c01018..78b8fe725f 100644 --- a/packages/driver/src/cypress.js +++ b/packages/driver/src/cypress.js @@ -210,10 +210,8 @@ class $Cypress { $FirefoxForcedGc.install(this) $scriptUtils.runScripts(specWindow, scripts) - .catch((err) => { - err = $errUtils.createUncaughtException('spec', err) - - this.runner.onScriptError(err) + .catch((error) => { + this.runner.onSpecError('error')({ error }) }) .then(() => { this.cy.initialize(this.$autIframe) @@ -496,6 +494,12 @@ class $Cypress { case 'app:window:unload': return this.emit('window:unload', args[0]) + case 'app:timers:reset': + return this.emitThen('app:timers:reset', ...args) + + case 'app:timers:pause': + return this.emitThen('app:timers:pause', ...args) + case 'app:css:modified': return this.emit('css:modified', args[0]) diff --git a/packages/driver/src/cypress/cy.js b/packages/driver/src/cypress/cy.js index b4252730c8..60ee095faf 100644 --- a/packages/driver/src/cypress/cy.js +++ b/packages/driver/src/cypress/cy.js @@ -66,7 +66,7 @@ function __stackReplacementMarker (fn, ctx, args) { // We only set top.onerror once since we make it configurable:false // but we update cy instance every run (page reload or rerun button) let curCy = null -const setTopOnError = function (cy) { +const setTopOnError = function (Cypress, cy, errors) { if (curCy) { curCy = cy @@ -77,28 +77,41 @@ const setTopOnError = function (cy) { // prevent overriding top.onerror twice when loading more than one // instance of test runner. - if (top.onerror && top.onerror.isCypressHandler) { + if (top.__alreadySetErrorHandlers__) { return } - const onTopError = function () { - return curCy.onUncaughtException.apply(curCy, arguments) + // eslint-disable-next-line @cypress/dev/arrow-body-multiline-braces + const onTopError = (handlerType) => (event) => { + const [err, promise] = handlerType === 'error' ? + $errUtils.errorFromErrorEvent(event) : + $errUtils.errorFromProjectRejectionEvent(event) + + // in some callbacks like for cy.intercept, we catch the errors and then + // rethrow them, causing them to get caught by the top frame + // but they came from the spec, so we need to differentiate them + const isSpecError = $errUtils.isSpecError(Cypress.config('spec'), err) + + if (isSpecError) { + return curCy.onSpecWindowUncaughtException(handlerType, err) + } + + return curCy.onUncaughtException(handlerType, err, promise) } - onTopError.isCypressHandler = true + top.addEventListener('error', onTopError('error')) - top.onerror = onTopError - - // Prevent Mocha from setting top.onerror which would override our handler - // Since the setter will change which event handler gets invoked, we make it a noop - return Object.defineProperty(top, 'onerror', { + // prevent Mocha from setting top.onerror + Object.defineProperty(top, 'onerror', { set () {}, - get () { - return onTopError - }, + get () {}, configurable: false, enumerable: true, }) + + top.addEventListener('unhandledrejection', onTopError('unhandledrejection')) + + top.__alreadySetErrorHandlers__ = true } // NOTE: this makes the cy object an instance @@ -147,14 +160,14 @@ const create = function (specWindow, Cypress, Cookies, state, config, log) { const focused = $Focused.create(state) const keyboard = $Keyboard.create(Cypress, state) const mouse = $Mouse.create(state, keyboard, focused, Cypress) - const timers = $Timers.create() + const timers = $Timers.create(Cypress) const { expect } = $Chai.create(specWindow, state, assertions.assert) const xhrs = $Xhrs.create(state) const aliases = $Aliases.create(cy) - const errors = $Errors.create(state, config, log) + const errors = $Errors.create(state, log) const ensures = $Ensures.create(state, expect) const snapshots = $Snapshots.create($$, state) @@ -176,11 +189,16 @@ const create = function (specWindow, Cypress, Cookies, state, config, log) { const contentWindowListeners = function (contentWindow) { return $Listeners.bindTo(contentWindow, { - onError () { + // eslint-disable-next-line @cypress/dev/arrow-body-multiline-braces + onError: (handlerType) => (event) => { + const [err, promise] = handlerType === 'error' ? + $errUtils.errorFromErrorEvent(event) : + $errUtils.errorFromProjectRejectionEvent(event) + // use a function callback here instead of direct // reference so our users can override this function // if need be - return cy.onUncaughtException.apply(cy, arguments) + return cy.onUncaughtException(handlerType, err, promise) }, onSubmit (e) { return Cypress.action('app:form:submitted', e) @@ -594,7 +612,7 @@ const create = function (specWindow, Cypress, Cookies, state, config, log) { err.name = err.name || 'CypressError' errors.commandRunningFailed(err) - return fail(err, state('runnable')) + return fail(err) }) .finally(cleanup) @@ -764,9 +782,7 @@ const create = function (specWindow, Cypress, Cookies, state, config, log) { // we aren't attached to the cypress command queue // promise chain and throwing the error would only // result in an unhandled rejection - let d - - d = state('done') + const d = state('done') if (d) { // invoke it with err @@ -811,7 +827,7 @@ const create = function (specWindow, Cypress, Cookies, state, config, log) { return } - // else figure out how to finisht this failure + // else figure out how to finish this failure return finish(err) } @@ -1204,13 +1220,11 @@ const create = function (specWindow, Cypress, Cookies, state, config, log) { wrapNativeMethods(contentWindow) snapshots.onBeforeWindowLoad() - - return timers.wrap(contentWindow) }, - onSpecWindowUncaughtException () { - // create the special uncaught exception err - const err = errors.createUncaughtException('spec', arguments) + onSpecWindowUncaughtException (handlerType, err) { + err = errors.createUncaughtException('spec', handlerType, err) + const runnable = state('runnable') if (!runnable) return err @@ -1228,8 +1242,9 @@ const create = function (specWindow, Cypress, Cookies, state, config, log) { } }, - onUncaughtException () { - let r + onUncaughtException (handlerType, err, promise) { + err = errors.createUncaughtException('app', handlerType, err) + const runnable = state('runnable') // don't do anything if we don't have a current runnable @@ -1237,10 +1252,7 @@ const create = function (specWindow, Cypress, Cookies, state, config, log) { return } - // create the special uncaught exception err - const err = errors.createUncaughtException('app', arguments) - - const results = Cypress.action('app:uncaught:exception', err, runnable) + const results = Cypress.action('app:uncaught:exception', err, runnable, promise) // dont do anything if any of our uncaught:exception // listeners returned false @@ -1250,7 +1262,7 @@ const create = function (specWindow, Cypress, Cookies, state, config, log) { // do all the normal fail stuff and promise cancelation // but dont re-throw the error - r = state('reject') + let r = state('reject') if (r) { r(err) @@ -1402,13 +1414,13 @@ const create = function (specWindow, Cypress, Cookies, state, config, log) { // if runnable.fn threw synchronously, then it didnt fail from // a cypress command, but we should still teardown and handle // the error - return fail(err, runnable) + return fail(err) } } }, }) - setTopOnError(cy) + setTopOnError(Cypress, cy, errors) // make cy global in the specWindow specWindow.cy = cy diff --git a/packages/driver/src/cypress/error_messages.js b/packages/driver/src/cypress/error_messages.js index f462c15b08..2094eee446 100644 --- a/packages/driver/src/cypress/error_messages.js +++ b/packages/driver/src/cypress/error_messages.js @@ -1731,14 +1731,9 @@ module.exports = { return msg }, - error (obj) { - const { message, source, lineno } = obj - - return message + (source && lineno ? ` (${source}:${lineno})` : '') - }, fromApp: { message: stripIndent`\ - The following error originated from your application code, not from Cypress. + The following error originated from your application code, not from Cypress.{{promiseAddendum}} > {{errMsg}} @@ -1749,7 +1744,7 @@ module.exports = { }, fromSpec: { message: stripIndent`\ - The following error originated from your test code, not from Cypress. + The following error originated from your test code, not from Cypress.{{promiseAddendum}} > {{errMsg}} diff --git a/packages/driver/src/cypress/error_utils.js b/packages/driver/src/cypress/error_utils.js index b3f3c5549f..78090aaf93 100644 --- a/packages/driver/src/cypress/error_utils.js +++ b/packages/driver/src/cypress/error_utils.js @@ -11,6 +11,8 @@ const $errorMessages = require('./error_messages') const ERROR_PROPS = 'message type name stack sourceMappedStack parsedStack fileName lineNumber columnNumber host uncaught actual expected showDiff isPending docsUrl codeFrame'.split(' ') const ERR_PREPARED_FOR_SERIALIZATION = Symbol('ERR_PREPARED_FOR_SERIALIZATION') +const crossOriginScriptRe = /^script error/i + if (!Error.captureStackTrace) { Error.captureStackTrace = (err, fn) => { const stack = (new Error()).stack @@ -67,6 +69,10 @@ const isCypressErr = (err = {}) => { return err.name === 'CypressError' } +const isSpecError = (spec, err) => { + return _.includes(err.stack, spec.relative) +} + const mergeErrProps = (origErr, ...newProps) => { return _.extend(origErr, ...newProps) } @@ -298,11 +304,11 @@ const errByPath = (msgPath, args) => { }) } -const createUncaughtException = (type, err) => { - // FIXME: `fromSpec` is a dirty hack to get uncaught exceptions in `top` to say they're from the spec - const errPath = (type === 'spec' || err.fromSpec) ? 'uncaught.fromSpec' : 'uncaught.fromApp' +const createUncaughtException = (frameType, handlerType, err) => { + const errPath = frameType === 'spec' ? 'uncaught.fromSpec' : 'uncaught.fromApp' let uncaughtErr = errByPath(errPath, { errMsg: err.message, + promiseAddendum: handlerType === 'unhandledrejection' ? ' It was caused by an unhandled promise rejection.' : '', }) modifyErrMsg(err, uncaughtErr.message, () => uncaughtErr.message) @@ -366,6 +372,68 @@ const processErr = (errObj = {}, config) => { return appendErrMsg(errObj, docsUrl) } +const getStackFromErrArgs = ({ filename, lineno, colno }) => { + if (!filename) return undefined + + const line = lineno != null ? `:${lineno}` : '' + const column = lineno != null && colno != null ? `:${colno}` : '' + + return ` at (${filename}${line}${column})` +} + +const convertErrorEventPropertiesToObject = (args) => { + let { message, filename, lineno, colno, err } = args + + // if the error was thrown as a string (throw 'some error'), `err` is + // the message ('some error') and message is some browser-created + // variant (e.g. 'Uncaught some error') + message = _.isString(err) ? err : message + const stack = getStackFromErrArgs({ filename, lineno, colno }) + + return makeErrFromObj({ + name: 'Error', + message, + stack, + }) +} + +const errorFromErrorEvent = (event) => { + let { message, filename, lineno, colno, error } = event + let docsUrl = error?.docsUrl + + // reset the message on a cross origin script error + // since no details are accessible + if (crossOriginScriptRe.test(message)) { + const crossOriginErr = errByPath('uncaught.cross_origin_script') + + message = crossOriginErr.message + docsUrl = crossOriginErr.docsUrl + } + + // it's possible the error was thrown as a string (throw 'some error') + // so create it in the case it's not already an object + const err = _.isObject(error) ? error : convertErrorEventPropertiesToObject({ + message, filename, lineno, colno, + }) + + err.docsUrl = docsUrl + + // makeErrFromObj clones the error, so the original doesn't get mutated + return [makeErrFromObj(err)] +} + +const errorFromProjectRejectionEvent = (event) => { + // Bluebird triggers "unhandledrejection" with its own custom error event + // where the `promise` and `reason` are attached to event.detail + // http://bluebirdjs.com/docs/api/error-management-configuration.html + if (event.detail) { + event = event.detail + } + + // makeErrFromObj clones the error, so the original doesn't get mutated + return [makeErrFromObj(event.reason), event.promise] +} + module.exports = { appendErrMsg, createUncaughtException, @@ -376,6 +444,7 @@ module.exports = { isAssertionErr, isChaiValidationErr, isCypressErr, + isSpecError, makeErrFromObj, mergeErrProps, modifyErrMsg, @@ -385,4 +454,6 @@ module.exports = { warnByPath, wrapErr, getUserInvocationStack, + errorFromErrorEvent, + errorFromProjectRejectionEvent, } diff --git a/packages/driver/src/cypress/local_storage.js b/packages/driver/src/cypress/local_storage.js index 149dbf1721..043d9474cd 100644 --- a/packages/driver/src/cypress/local_storage.js +++ b/packages/driver/src/cypress/local_storage.js @@ -48,7 +48,8 @@ const $LocalStorage = { }, unsetStorages () { - this.localStorage = (this.remoteStorage = null) + this.localStorage = null + this.remoteStorage = null return this }, diff --git a/packages/driver/src/cypress/runner.js b/packages/driver/src/cypress/runner.js index 49115efe1c..680206ae9b 100644 --- a/packages/driver/src/cypress/runner.js +++ b/packages/driver/src/cypress/runner.js @@ -1003,11 +1003,18 @@ const create = (specWindow, mocha, Cypress, cy) => { return foundTest } - const onScriptError = (err) => { + // eslint-disable-next-line @cypress/dev/arrow-body-multiline-braces + const onSpecError = (handlerType) => (event) => { + const [originalErr] = handlerType === 'error' ? + $errUtils.errorFromErrorEvent(event) : + $errUtils.errorFromProjectRejectionEvent(event) + + let err = cy.onSpecWindowUncaughtException(handlerType, originalErr) + // err will not be returned if cy can associate this // uncaught exception to an existing runnable if (!err) { - return true + return undefined } const todoMsg = () => { @@ -1040,11 +1047,8 @@ const create = (specWindow, mocha, Cypress, cy) => { return undefined } - specWindow.onerror = function () { - const err = cy.onSpecWindowUncaughtException.apply(cy, arguments) - - return onScriptError(err) - } + specWindow.addEventListener('error', onSpecError('error')) + specWindow.addEventListener('unhandledrejection', onSpecError('unhandledrejection')) // hold onto the _runnables for faster lookup later let _test = null @@ -1237,7 +1241,7 @@ const create = (specWindow, mocha, Cypress, cy) => { } return { - onScriptError, + onSpecError, setOnlyTestId, setOnlySuiteId, diff --git a/packages/proxy/lib/http/util/inject.ts b/packages/proxy/lib/http/util/inject.ts index 19b99bb0be..837c6fcbe4 100644 --- a/packages/proxy/lib/http/util/inject.ts +++ b/packages/proxy/lib/http/util/inject.ts @@ -1,4 +1,5 @@ import { oneLine } from 'common-tags' +import runner from '@packages/runner/lib/resolve-dist' export function partial (domain) { return oneLine` @@ -9,17 +10,13 @@ export function partial (domain) { } export function full (domain) { - return oneLine` - - ` + ${contents} + + ` + }) } diff --git a/packages/proxy/lib/http/util/rewriter.ts b/packages/proxy/lib/http/util/rewriter.ts index b5303fa4bf..8b101a1517 100644 --- a/packages/proxy/lib/http/util/rewriter.ts +++ b/packages/proxy/lib/http/util/rewriter.ts @@ -42,7 +42,7 @@ export async function html (html: string, opts: SecurityOpts & InjectionOpts) { return html.replace(re, str) } - const htmlToInject = getHtmlToInject(opts) + const htmlToInject = await Promise.resolve(getHtmlToInject(opts)) // strip clickjacking and framebusting // from the HTML if we've been told to diff --git a/packages/reporter/webpack.config.ts b/packages/reporter/webpack.config.ts index 2bd7dcabac..504d61eaae 100644 --- a/packages/reporter/webpack.config.ts +++ b/packages/reporter/webpack.config.ts @@ -1,4 +1,4 @@ -import getCommonConfig, { HtmlWebpackPlugin } from '@packages/web-config/webpack.config.base' +import { getCommonConfig, HtmlWebpackPlugin } from '@packages/web-config/webpack.config.base' import path from 'path' import webpack from 'webpack' diff --git a/packages/runner-ct/webpack.config.ts b/packages/runner-ct/webpack.config.ts index 40db3028ef..dc833ee054 100644 --- a/packages/runner-ct/webpack.config.ts +++ b/packages/runner-ct/webpack.config.ts @@ -3,7 +3,7 @@ process.env.NO_LIVERELOAD = '1' import _ from 'lodash' import path from 'path' import webpack from 'webpack' -import getCommonConfig, { HtmlWebpackPlugin } from '@packages/web-config/webpack.config.base' +import { getCommonConfig, HtmlWebpackPlugin } from '@packages/web-config/webpack.config.base' const commonConfig = getCommonConfig() diff --git a/packages/runner/README.md b/packages/runner/README.md index e36c0fd2fb..62a73ccbb4 100644 --- a/packages/runner/README.md +++ b/packages/runner/README.md @@ -8,6 +8,8 @@ The runner is the minimal "chrome" around the user's app and has the following r - Managing the viewport size and scale - Showing the currently active URL +This package also includes the majority of the code that gets injected into the App Under Test (AUT) by `packages/proxy`. That bundle's entry point is `injection/index.js`. + ## Developing ### Watching diff --git a/packages/runner/cypress/fixtures/errors/hooks_spec.js b/packages/runner/cypress/fixtures/errors/hooks_spec.js new file mode 100644 index 0000000000..4da8922fee --- /dev/null +++ b/packages/runner/cypress/fixtures/errors/hooks_spec.js @@ -0,0 +1,18 @@ +const log = () => { + const r = cy.state('runnable') + + assert(true, `${r.type} - ${r.parent.title || 'root'}`) +} + +describe('nested_hooks', () => { + describe('nested beforeEach', () => { + before(() => { + log() + beforeEach(() => { + log() + }) + }) + + it('test', log) + }) +}) diff --git a/packages/runner/cypress/fixtures/errors/uncaught_spec.js b/packages/runner/cypress/fixtures/errors/uncaught_spec.js index 8edadd6daf..e3530bb750 100644 --- a/packages/runner/cypress/fixtures/errors/uncaught_spec.js +++ b/packages/runner/cypress/fixtures/errors/uncaught_spec.js @@ -1,6 +1,18 @@ +import Bluebird from 'bluebird' + import './setup' describe('uncaught errors', { defaultCommandTimeout: 0 }, () => { + it('sync app visit exception', () => { + cy.visit('/index.html') + cy.get('.trigger-sync-error').click() + }) + + it('sync app navigates to visit exception', () => { + cy.visit('/index.html') + cy.get('.go-to-visit-error').click() + }) + it('sync app exception', () => { cy.visit('/index.html') cy.get('.trigger-sync-error').click() @@ -12,7 +24,13 @@ describe('uncaught errors', { defaultCommandTimeout: 0 }, () => { cy.wait(10000) }) - it('async exception', () => { + it('app unhandled rejection', () => { + cy.visit('/index.html') + cy.get('.trigger-unhandled-rejection').click() + cy.wait(10000) + }) + + it('async spec exception', () => { setTimeout(() => { ({}).bar() }) @@ -20,9 +38,32 @@ describe('uncaught errors', { defaultCommandTimeout: 0 }, () => { cy.wait(10000) }) - it('async exception with done', (done) => { + it('async spec exception with done', (done) => { setTimeout(() => { ({}).bar() }) }) + + it('spec unhandled rejection', () => { + Promise.reject(new Error('Unhandled promise rejection from the spec')) + + cy.wait(10000) + }) + + it('spec unhandled rejection with done', (done) => { + Promise.reject(new Error('Unhandled promise rejection from the spec')) + }) + + it('spec Bluebird unhandled rejection', () => { + Bluebird.reject(new Error('Unhandled promise rejection from the spec')) + + cy.wait(10000) + }) + + it('spec Bluebird unhandled rejection with done', (done) => { + Bluebird.reject(new Error('Unhandled promise rejection from the spec')) + }) + + // TODO: Cypress.Promise.reject() gets caught by AUT. Can/should + // we handle that somehow? }) diff --git a/packages/runner/cypress/fixtures/errors/unexpected_spec.js b/packages/runner/cypress/fixtures/errors/unexpected_spec.js index 18f869f5b9..e77a3cbef6 100644 --- a/packages/runner/cypress/fixtures/errors/unexpected_spec.js +++ b/packages/runner/cypress/fixtures/errors/unexpected_spec.js @@ -15,11 +15,11 @@ describe('unexpected errors', { defaultCommandTimeout: 0 }, () => { }) it('Cypress method error', () => { - Cypress.LocalStorage.setStorages({ foo: 'foo' }) - - window.autWindow.eval(`Cypress.LocalStorage._isSpecialKeyword = () => { throw new Error('thrown in Cypress-LocalStorage-_isSpecialKeyword') }`) - - Cypress.LocalStorage.clear() + cy.window().then((win) => { + win.localStorage.foo = 'foo' + window.autWindow.eval(`Cypress.LocalStorage._isSpecialKeyword = () => { debugger; throw new Error('thrown in Cypress-LocalStorage-_isSpecialKeyword') }`) + Cypress.LocalStorage.clear() + }) }) it('internal cy error', () => { diff --git a/packages/runner/cypress/integration/issues/issue-8350.js b/packages/runner/cypress/integration/issues/issue-8350.js deleted file mode 100644 index 30f0df0430..0000000000 --- a/packages/runner/cypress/integration/issues/issue-8350.js +++ /dev/null @@ -1,21 +0,0 @@ -const { createCypress } = require('../../support/helpers') -const { verify } = createCypress() - -// https://github.com/cypress-io/cypress/issues/8214 -// https://github.com/cypress-io/cypress/issues/8288 -// https://github.com/cypress-io/cypress/issues/8350 -describe('issue-8350', { viewportHeight: 900 }, () => { - const file = 'nested_hooks_spec.js' - - verify.it('errors when nested hook', { - file, - // firefox points to col 18, chrome 7 - column: '(7|18)', - codeFrameText: 'beforeEach(()=>', - message: `Cypress detected you registered a(n) beforeEach hook while a test was running`, - }) - - afterEach(() => { - cy.percySnapshot() - }) -}) diff --git a/packages/runner/cypress/integration/reporter.errors.spec.js b/packages/runner/cypress/integration/reporter.errors.spec.js index bd71ae369d..4b306fccfa 100644 --- a/packages/runner/cypress/integration/reporter.errors.spec.js +++ b/packages/runner/cypress/integration/reporter.errors.spec.js @@ -1,24 +1,4 @@ -const helpers = require('../support/helpers') - -const { verify } = helpers.createCypress({ - config: { isTextTerminal: true, retries: 0 }, - visitUrl: 'http://localhost:3500/fixtures/isolated-runner-inner.html', -}) - -const verifyInternalFailure = (props) => { - const { method } = props - - cy.get('.runnable-err-message') - .should('include.text', `thrown in ${method.replace(/\./g, '-')}`) - - cy.get('.runnable-err-stack-expander > .collapsible-header').click() - - cy.get('.runnable-err-stack-trace') - .should('include.text', method) - - cy.get('.test-err-code-frame') - .should('not.exist') -} +import { verify, verifyInternalFailure } from '../support/verify-failures' describe('errors ui', () => { describe('assertion failures', () => { @@ -32,7 +12,7 @@ describe('errors ui', () => { verify.it('with assert()', { file, - column: '(5|12)', + column: '(5|12)', // (chrome|firefox) message: `should be true`, }) @@ -59,6 +39,27 @@ describe('errors ui', () => { codeFrameText: `thrownewError('An outside error')`, verifyOpenInIde: false, }) + + verify.it('in spec file outside test with only suite', { + file: 'uncaught_onRunnable_spec.js', + column: 7, + message: 'my error', + codeFrameText: `my error`, + }) + }) + + describe('hooks', { viewportHeight: 900 }, () => { + const file = 'hooks_spec.js' + + // https://github.com/cypress-io/cypress/issues/8214 + // https://github.com/cypress-io/cypress/issues/8288 + // https://github.com/cypress-io/cypress/issues/8350 + verify.it('errors when a hook is nested in another hook', { + file, + column: '(7|18)', // (chrome|firefox) + codeFrameText: 'beforeEach(()=>', + message: `Cypress detected you registered a(n) beforeEach hook while a test was running`, + }) }) describe('commands', () => { @@ -418,7 +419,7 @@ describe('errors ui', () => { verify.it('from chai expect', { file, - column: '(5|12)', // different between chrome & firefox + column: '(5|12)', // (chrome|firefox) message: 'Invalid Chai property: nope', stack: ['proxyGetter', 'From Your Spec Code:'], }) @@ -461,44 +462,142 @@ describe('errors ui', () => { describe('uncaught errors', () => { const file = 'uncaught_spec.js' + verify.it('sync app visit exception', { + file, + command: 'visit', + visitUrl: 'http://localhost:3500/fixtures/errors.html?error-on-visit', + message: [ + 'The following error originated from your application code', + 'visit error', + ], + notInMessage: [ + 'It was caused by an unhandled promise rejection', + ], + regex: /localhost\:\d+\/fixtures\/errors.html\?error-on-visit:\d+:\d+/, + hasCodeFrame: false, + verifyOpenInIde: false, + }) + + verify.it('sync app navigates to visit exception', { + file, + visitUrl: 'http://localhost:3500/fixtures/errors.html', + message: [ + 'The following error originated from your application code', + 'visit error', + ], + notInMessage: [ + 'It was caused by an unhandled promise rejection', + ], + regex: /localhost\:\d+\/fixtures\/errors.html\?error-on-visit:\d+:\d+/, + hasCodeFrame: false, + verifyOpenInIde: false, + }) + verify.it('sync app exception', { file, + command: 'click', + visitUrl: 'http://localhost:3500/fixtures/errors.html', message: [ 'The following error originated from your application code', - 'syncReference is not defined', + 'sync error', ], - regex: /localhost\:\d+\/fixtures\/isolated-runner-inner.html:\d+:\d+/, + notInMessage: [ + 'It was caused by an unhandled promise rejection', + ], + regex: /localhost\:\d+\/fixtures\/errors.html:\d+:\d+/, hasCodeFrame: false, verifyOpenInIde: false, }) - // FIXME: does not get caught and wrapped like it does in real cypress - verify.it.skip('async app exception', { + verify.it('async app exception', { file, + visitUrl: 'http://localhost:3500/fixtures/errors.html', message: [ 'The following error originated from your application code', - 'asyncReference is not defined', + 'async error', ], - regex: /localhost\:\d+\/fixtures\/isolated-runner-inner.html:\d+:\d+/, + notInMessage: [ + 'It was caused by an unhandled promise rejection', + ], + regex: /localhost\:\d+\/fixtures\/errors.html:\d+:\d+/, hasCodeFrame: false, verifyOpenInIde: false, }) - verify.it('async exception', { + verify.it('app unhandled rejection', { + file, + visitUrl: 'http://localhost:3500/fixtures/errors.html', + message: [ + 'The following error originated from your application code', + 'It was caused by an unhandled promise rejection', + 'promise rejection', + ], + regex: /localhost\:\d+\/fixtures\/errors.html:\d+:\d+/, + hasCodeFrame: false, + verifyOpenInIde: false, + }) + + verify.it('async spec exception', { file, column: 12, message: [ - 'bar is not a function', 'The following error originated from your test code', + 'bar is not a function', + ], + notInMessage: [ + 'It was caused by an unhandled promise rejection', ], }) - verify.it('async exception with done', { + verify.it('async spec exception with done', { file, column: 12, message: [ - 'bar is not a function', 'The following error originated from your test code', + 'bar is not a function', + ], + notInMessage: [ + 'It was caused by an unhandled promise rejection', + ], + }) + + verify.it('spec unhandled rejection', { + file, + column: 20, + message: [ + 'The following error originated from your test code', + 'It was caused by an unhandled promise rejection', + 'Unhandled promise rejection from the spec', + ], + }) + + verify.it('spec unhandled rejection with done', { + file, + column: 20, + message: [ + 'The following error originated from your test code', + 'It was caused by an unhandled promise rejection', + 'Unhandled promise rejection from the spec', + ], + }) + + verify.it('spec Bluebird unhandled rejection', { + file, + column: 21, + message: [ + 'The following error originated from your test code', + 'It was caused by an unhandled promise rejection', + 'Unhandled promise rejection from the spec', + ], + }) + + verify.it('spec Bluebird unhandled rejection with done', { + file, + column: 21, + message: [ + 'The following error originated from your test code', + 'It was caused by an unhandled promise rejection', + 'Unhandled promise rejection from the spec', ], }) }) @@ -578,12 +677,14 @@ describe('errors ui', () => { }) // cases where there is a bug in Cypress and we should show cypress internals - // instead of the invocation stack. we do this by monkey-patching internal + // instead of the invocation stack. we test this by monkey-patching internal // methods to make them throw an error describe('unexpected errors', () => { const file = 'unexpected_spec.js' - verify.it('Cypress method error', { + // FIXME: the eval doesn't seem to take effect and overwrite the method + // so it ends up not failing properly + verify.it.skip('Cypress method error', { file, verifyFn: verifyInternalFailure, method: 'Cypress.LocalStorage._isSpecialKeyword', @@ -595,11 +696,4 @@ describe('errors ui', () => { method: 'cy.expect', }) }) - - verify.it('uncaught error during onRunnable w/ onlySuite', { - file: 'uncaught_onRunnable_spec.js', - message: 'my error', - codeFrameText: `my error`, - column: 7, - }) }) diff --git a/packages/runner/cypress/integration/studio.ui.spec.js b/packages/runner/cypress/integration/studio.ui.spec.js index b279494703..0483f05672 100644 --- a/packages/runner/cypress/integration/studio.ui.spec.js +++ b/packages/runner/cypress/integration/studio.ui.spec.js @@ -129,6 +129,11 @@ describe('studio ui', () => { }) it('displays error state when cy.visit() fails on user inputted url', () => { + cy.on('uncaught:exception', (err) => { + // don't let the error we expect fail the test + if (err.message.includes('failed trying to load')) return false + }) + runIsolatedCypress('cypress/fixtures/studio/basic_spec.js', { config: { baseUrl: null, diff --git a/packages/runner/cypress/support/helpers.js b/packages/runner/cypress/support/helpers.js index 56dedcae05..fa4670f204 100644 --- a/packages/runner/cypress/support/helpers.js +++ b/packages/runner/cypress/support/helpers.js @@ -290,50 +290,10 @@ function createCypress (defaultOptions = {}) { }) } - const createVerifyTest = (modifier) => { - return (title, opts, props) => { - if (!props) { - props = opts - opts = null - } - - const verifyFn = props.verifyFn || verifyFailure - - const args = _.compact([title, opts, () => { - return runIsolatedCypress(`cypress/fixtures/errors/${props.file}`, { - onBeforeRun ({ specWindow, win, autCypress }) { - specWindow.testToRun = title - specWindow.autWindow = win - specWindow.autCypress = autCypress - - if (props.onBeforeRun) { - props.onBeforeRun({ specWindow, win }) - } - }, - }) - .then(({ win }) => { - props.codeFrameText = props.codeFrameText || title - props.win = win - verifyFn(props) - }) - }]) - - ;(modifier ? it[modifier] : it)(...args) - } - } - - const verify = { - it: createVerifyTest(), - } - - verify.it['only'] = createVerifyTest('only') - verify.it['skip'] = createVerifyTest('skip') - return { runIsolatedCypress, snapshotMochaEvents, getAutCypress, - verify, } } @@ -539,88 +499,6 @@ const getRunState = (Cypress) => { return _.cloneDeep(s) } -const verifyFailure = (options) => { - const { - hasCodeFrame = true, - verifyOpenInIde = true, - column, - codeFrameText, - message, - stack, - file, - win, - } = options - let { regex, line } = options - - regex = regex || new RegExp(`${file}:${line || '\\d+'}:${column}`) - - const testOpenInIde = () => { - expect(win.runnerWs.emit.withArgs('open:file').lastCall.args[1].file).to.include(file) - } - - win.runnerWs.emit.withArgs('get:user:editor') - .yields({ - preferredOpener: { - id: 'foo-editor', - name: 'Foo', - openerId: 'foo-editor', - isOther: false, - }, - }) - - win.runnerWs.emit.withArgs('open:file') - - cy.contains('View stack trace').click() - - _.each([].concat(message), (msg) => { - cy.get('.runnable-err-message') - .should('include.text', msg) - - cy.get('.runnable-err-stack-trace') - .should('not.include.text', msg) - }) - - cy.get('.runnable-err-stack-trace') - .invoke('text') - .should('match', regex) - - if (stack) { - _.each([].concat(stack), (stackLine) => { - cy.get('.runnable-err-stack-trace') - .should('include.text', stackLine) - }) - } - - cy.get('.runnable-err-stack-trace') - .should('not.include.text', '__stackReplacementMarker') - - if (verifyOpenInIde) { - cy.contains('.runnable-err-stack-trace .runnable-err-file-path a', file) - .click('left') - .should(() => { - testOpenInIde() - }) - } - - if (!hasCodeFrame) return - - cy - .get('.test-err-code-frame .runnable-err-file-path') - .invoke('text') - .should('match', regex) - - cy.get('.test-err-code-frame pre span').should('include.text', codeFrameText) - - if (verifyOpenInIde) { - cy.contains('.test-err-code-frame .runnable-err-file-path a', file) - .click() - .should(() => { - expect(win.runnerWs.emit.withArgs('open:file')).to.be.calledTwice - testOpenInIde() - }) - } -} - module.exports = { generateMochaTestsForWin, createCypress, diff --git a/packages/runner/cypress/support/verify-failures.js b/packages/runner/cypress/support/verify-failures.js new file mode 100644 index 0000000000..aba72cbaa8 --- /dev/null +++ b/packages/runner/cypress/support/verify-failures.js @@ -0,0 +1,162 @@ +import _ from 'lodash' +import helpers from '../support/helpers' + +const { runIsolatedCypress } = helpers.createCypress({ + config: { isTextTerminal: true, retries: 0 }, + visitUrl: 'http://localhost:3500/fixtures/isolated-runner-inner.html', +}) + +const verifyFailure = (options) => { + const { + hasCodeFrame = true, + verifyOpenInIde = true, + column, + codeFrameText, + message = [], + notInMessage = [], + command, + stack, + file, + win, + } = options + let { regex, line } = options + + regex = regex || new RegExp(`${file}:${line || '\\d+'}:${column}`) + + const testOpenInIde = () => { + expect(win.runnerWs.emit.withArgs('open:file').lastCall.args[1].file).to.include(file) + } + + win.runnerWs.emit.withArgs('get:user:editor') + .yields({ + preferredOpener: { + id: 'foo-editor', + name: 'Foo', + openerId: 'foo-editor', + isOther: false, + }, + }) + + win.runnerWs.emit.withArgs('open:file') + + cy.contains('View stack trace').click() + + _.each([].concat(message), (msg) => { + cy.get('.runnable-err-message') + .should('include.text', msg) + + cy.get('.runnable-err-stack-trace') + .should('not.include.text', msg) + }) + + _.each([].concat(notInMessage), (msg) => { + cy.get('.runnable-err-message') + .should('not.include.text', msg) + }) + + cy.get('.runnable-err-stack-trace') + .invoke('text') + .should('match', regex) + + if (stack) { + _.each([].concat(stack), (stackLine) => { + cy.get('.runnable-err-stack-trace') + .should('include.text', stackLine) + }) + } + + cy.get('.runnable-err-stack-trace') + .should('not.include.text', '__stackReplacementMarker') + + if (verifyOpenInIde) { + cy.contains('.runnable-err-stack-trace .runnable-err-file-path a', file) + .click('left') + .should(() => { + testOpenInIde() + }) + } + + if (command) { + cy + .get('.command-state-failed') + .should('have.length', 1) + .find('.command-method') + .invoke('text') + .should('equal', command) + } + + if (!hasCodeFrame) return + + cy + .get('.test-err-code-frame .runnable-err-file-path') + .invoke('text') + .should('match', regex) + + cy.get('.test-err-code-frame pre span').should('include.text', codeFrameText) + + if (verifyOpenInIde) { + cy.contains('.test-err-code-frame .runnable-err-file-path a', file) + .click() + .should(() => { + expect(win.runnerWs.emit.withArgs('open:file')).to.be.calledTwice + testOpenInIde() + }) + } +} + +const createVerifyTest = (modifier) => { + return (title, opts, props) => { + if (!props) { + props = opts + opts = null + } + + const verifyFn = props.verifyFn || verifyFailure + + const args = _.compact([title, opts, () => { + return runIsolatedCypress(`cypress/fixtures/errors/${props.file}`, { + visitUrl: props.visitUrl, + onBeforeRun ({ specWindow, win, autCypress }) { + specWindow.testToRun = title + specWindow.autWindow = win + specWindow.autCypress = autCypress + + if (props.onBeforeRun) { + props.onBeforeRun({ specWindow, win }) + } + }, + }) + .then(({ win }) => { + props.codeFrameText = props.codeFrameText || title + props.win = win + verifyFn(props, verifyFailure) + }) + }]) + +;(modifier ? it[modifier] : it)(...args) + } +} + +export const verify = { + it: createVerifyTest(), +} + +verify.it['only'] = createVerifyTest('only') +verify.it['skip'] = createVerifyTest('skip') + +export const verifyInternalFailure = (props) => { + const { method, stackMethod } = props + + cy.get('.runnable-err-message') + .should('include.text', `thrown in ${method.replace(/\./g, '-')}`) + + cy.get('.runnable-err-stack-expander > .collapsible-header').click() + + cy.get('.runnable-err-stack-trace') + .should('include.text', stackMethod || method) + + // this is an internal cypress error and we can only show code frames + // from specs, so it should not show the code frame + cy.get('.test-err-code-frame') + .should('not.exist') +} diff --git a/packages/runner/injection/.eslintrc.json b/packages/runner/injection/.eslintrc.json new file mode 100644 index 0000000000..12aab8bb50 --- /dev/null +++ b/packages/runner/injection/.eslintrc.json @@ -0,0 +1,6 @@ +{ + "globals": { + "window": true, + "parent": true + } +} diff --git a/packages/runner/injection/index.js b/packages/runner/injection/index.js new file mode 100644 index 0000000000..134cda8fc3 --- /dev/null +++ b/packages/runner/injection/index.js @@ -0,0 +1,33 @@ +/** + * This is the entry point for the script that gets injected into + * the AUT. It gets bundled on its own and injected into the + * of the AUT by `packages/proxy`. + * + * If adding to this bundle, try to keep it light and free of + * dependencies. + */ + +import { createTimers } from './timers' + +const Cypress = window.Cypress = parent.Cypress + +if (!Cypress) { + throw new Error('Something went terribly wrong and we cannot proceed. We expected to find the global Cypress in the parent window but it is missing!. This should never happen and likely is a bug. Please open an issue!') +} + +// We wrap timers in the injection code because if we do it in the driver (like +// we used to do), any uncaught errors thrown in the timer callbacks would +// get picked up by the top frame's 'error' handler instead of the AUT's. +// We need to wrap the timer callbacks in the AUT itself for errors to +// propagate properly. +const timers = createTimers() + +Cypress.removeAllListeners('app:timers:reset') +Cypress.removeAllListeners('app:timers:pause') + +Cypress.on('app:timers:reset', timers.reset) +Cypress.on('app:timers:pause', timers.pause) + +timers.wrap() + +Cypress.action('app:window:before:load', window) diff --git a/packages/runner/injection/timers.js b/packages/runner/injection/timers.js new file mode 100644 index 0000000000..4e3ff03ac5 --- /dev/null +++ b/packages/runner/injection/timers.js @@ -0,0 +1,121 @@ +export const createTimers = () => { + let paused + let flushing + let timerQueue + let canceledTimerIds + + const reset = () => { + paused = false + flushing = false + timerQueue = [] + canceledTimerIds = {} + } + + const invoke = (fnOrCode, params = []) => { + if (typeof fnOrCode === 'function') { + return fnOrCode.apply(window, params) + } + + return window.eval(fnOrCode) + } + + const flushTimerQueue = () => { + flushing = true + + timerQueue.forEach((timer) => { + const { timerId, type, fnOrCode, params } = timer + + // if we are a setInterval and we're been canceled + // then just return. this can happen when a setInterval + // queues many callbacks, and from within that callback + // we would have canceled the original setInterval + if (type === 'setInterval' && canceledTimerIds[timerId]) { + return + } + + invoke(fnOrCode, params) + }) + + reset() + } + + const pause = (shouldPause) => { + paused = Boolean(shouldPause) + + if (!paused) { + flushTimerQueue() + } + } + + const wrap = () => { + const originals = { + setTimeout: window.setTimeout, + setInterval: window.setInterval, + requestAnimationFrame: window.requestAnimationFrame, + clearTimeout: window.clearTimeout, + clearInterval: window.clearInterval, + } + + const callThrough = (fnName, args) => { + return originals[fnName].apply(window, args) + } + + const wrapCancel = (fnName) => { + return (timerId) => { + if (flushing) { + canceledTimerIds[timerId] = true + } + + return callThrough(fnName, [timerId]) + } + } + + const wrapTimer = (fnName) => { + return (...args) => { + let timerId + let [fnOrCode, delay, ...params] = args + + const timerOverride = (...params) => { + // if we're currently paused then we need + // to enqueue this timer callback and invoke + // it immediately once we're unpaused + if (paused) { + timerQueue.push({ + timerId, + fnOrCode, + params, + type: fnName, + }) + + return + } + + // else go ahead and invoke the real function + // the same way the browser otherwise would + return invoke(fnOrCode, params) + } + + timerId = callThrough(fnName, [timerOverride, delay, ...params]) + + return timerId + } + } + + window.setTimeout = wrapTimer('setTimeout') + window.setInterval = wrapTimer('setInterval') + window.requestAnimationFrame = wrapTimer('requestAnimationFrame') + window.clearTimeout = wrapCancel('clearTimeout') + window.clearInterval = wrapCancel('clearInterval') + } + + // start with initial values + reset() + + return { + wrap, + + reset, + + pause, + } +} diff --git a/packages/runner/lib/resolve-dist.js b/packages/runner/lib/resolve-dist.js index ac6b02ff85..d1fc21d8ea 100644 --- a/packages/runner/lib/resolve-dist.js +++ b/packages/runner/lib/resolve-dist.js @@ -1,3 +1,4 @@ +const fs = require('fs-extra') const path = require('path') function dist (...args) { @@ -11,6 +12,10 @@ module.exports = { return dist(...args) }, + getInjectionContents () { + return fs.readFile(dist('injection.js')) + }, + getPathToIndex () { return dist('index.html') }, diff --git a/packages/runner/package.json b/packages/runner/package.json index 11c69a7caf..b8cba8abf6 100644 --- a/packages/runner/package.json +++ b/packages/runner/package.json @@ -16,6 +16,9 @@ "test-watch": "yarn test-unit --watch", "watch": "webpack --watch --progress" }, + "dependencies": { + "fs-extra": "8.1.0" + }, "devDependencies": { "@cypress/react-tooltip": "0.5.3", "@cypress/webpack-preprocessor": "0.0.0-development", diff --git a/packages/runner/webpack.config.ts b/packages/runner/webpack.config.ts index 76202d72c8..6803ea617a 100644 --- a/packages/runner/webpack.config.ts +++ b/packages/runner/webpack.config.ts @@ -1,5 +1,5 @@ import _ from 'lodash' -import getCommonConfig, { HtmlWebpackPlugin } from '@packages/web-config/webpack.config.base' +import { getCommonConfig, getSimpleConfig, HtmlWebpackPlugin } from '@packages/web-config/webpack.config.base' import path from 'path' import webpack from 'webpack' @@ -39,7 +39,7 @@ pngRule.use[0].options = { } // @ts-ignore -const config: webpack.Configuration = { +const mainConfig: webpack.Configuration = { ...commonConfig, module: { rules: [ @@ -57,17 +57,17 @@ const config: webpack.Configuration = { } // @ts-ignore -config.plugins = [ +mainConfig.plugins = [ // @ts-ignore - ...config.plugins, + ...mainConfig.plugins, new HtmlWebpackPlugin({ template: path.resolve(__dirname, './static/index.html'), inject: false, }), ] -config.resolve = { - ...config.resolve, +mainConfig.resolve = { + ...mainConfig.resolve, alias: { 'bluebird': require.resolve('bluebird'), 'lodash': require.resolve('lodash'), @@ -78,4 +78,17 @@ config.resolve = { }, } -export default config +// @ts-ignore +const injectionConfig: webpack.Configuration = { + ...getSimpleConfig(), + mode: 'production', + entry: { + injection: [path.resolve(__dirname, 'injection/index.js')], + }, + output: { + path: path.resolve(__dirname, 'dist'), + filename: '[name].js', + }, +} + +export default [mainConfig, injectionConfig] diff --git a/packages/server/__snapshots__/3_js_error_handling_spec.js b/packages/server/__snapshots__/3_js_error_handling_spec.js index b473013833..d53654a0df 100644 --- a/packages/server/__snapshots__/3_js_error_handling_spec.js +++ b/packages/server/__snapshots__/3_js_error_handling_spec.js @@ -86,7 +86,7 @@ https://on.cypress.io/uncaught-exception-from-application 5) s1 cross origin script errors explains where script errored: - CypressError: The following error originated from your application code, not from Cypress. + Error: The following error originated from your application code, not from Cypress. > Script error. @@ -105,10 +105,6 @@ This behavior is configurable, and you can choose to turn this off by listening https://on.cypress.io/uncaught-exception-from-application https://on.cypress.io/cross-origin-script-error - [stack trace lines] - - - (Results) diff --git a/packages/server/test/e2e/3_js_error_handling_spec.js b/packages/server/test/e2e/3_js_error_handling_spec.js index e91dfb3167..9a7f1b6d1f 100644 --- a/packages/server/test/e2e/3_js_error_handling_spec.js +++ b/packages/server/test/e2e/3_js_error_handling_spec.js @@ -49,5 +49,11 @@ describe('e2e js error handling', () => { spec: 'js_error_handling_failing_spec.js', snapshot: true, expectedExitCode: 5, + onStdout (stdout) { + // firefox has a stack line for the cross-origin error that other browsers don't + return stdout + .replace(/cross-origin-script-error\s+?\(Results/, 'cross-origin-script-error\n\n (Results') + .replace(/cross-origin-script-error\s+at \(http:\/\/localhost:1122\/static\/fail\.js:0:0\)\s+?\(Results/, 'cross-origin-script-error\n\n (Results') + }, }) }) diff --git a/packages/server/test/integration/http_requests_spec.js b/packages/server/test/integration/http_requests_spec.js index 7f38a345e6..ecf035d35b 100644 --- a/packages/server/test/integration/http_requests_spec.js +++ b/packages/server/test/integration/http_requests_spec.js @@ -1,4 +1,4 @@ -('../spec_helper') +require('../spec_helper') const _ = require('lodash') let r = require('@cypress/request') @@ -13,6 +13,7 @@ let zlib = require('zlib') const str = require('underscore.string') const evilDns = require('evil-dns') const Promise = require('bluebird') + const httpsServer = require(`${root}../https-proxy/test/helpers/https_server`) const pkg = require('@packages/root') const SseStream = require('ssestream') @@ -28,6 +29,7 @@ const { fs } = require(`${root}lib/util/fs`) const glob = require(`${root}lib/util/glob`) const CacheBuster = require(`${root}lib/util/cache_buster`) const Fixtures = require(`${root}test/support/helpers/fixtures`) +const runner = require(`${root}../runner/lib/resolve-dist`) zlib = Promise.promisifyAll(zlib) @@ -1256,7 +1258,7 @@ describe('Routes', () => { expect(res.statusCode).to.eq(200) expect(res.body).to.include('') expect(res.body).to.include('gzip') - expect(res.body).to.include('Cypress.') + expect(res.body).to.include('parent.Cypress') expect(res.body).to.include('document.domain = \'github.com\'') expect(res.body).to.include('') @@ -2447,52 +2449,47 @@ describe('Routes', () => { return this.setup('http://www.google.com') }) - it('injects when head has attributes', function () { - const contents = removeWhitespace(Fixtures.get('server/expected_head_inject.html')) - + it('injects when head has attributes', async function () { nock(this.server._remoteOrigin) .get('/bar') .reply(200, ' hello from bar! ', { 'Content-Type': 'text/html', }) - return this.rp({ + const injection = await runner.getInjectionContents() + const contents = removeWhitespace(Fixtures.get('server/expected_head_inject.html').replace('{{injection}}', injection)) + const res = await this.rp({ url: 'http://www.google.com/bar', headers: { 'Cookie': '__cypress.initial=true', }, }) - .then((res) => { - expect(res.statusCode).to.eq(200) + const body = cleanResponseBody(res.body) - const body = cleanResponseBody(res.body) - - expect(body).to.eq(contents) - }) + expect(res.statusCode).to.eq(200) + expect(body).to.eq(contents) }) - it('injects even when head tag is missing', function () { - const contents = removeWhitespace(Fixtures.get('server/expected_no_head_tag_inject.html')) - + it('injects even when head tag is missing', async function () { nock(this.server._remoteOrigin) .get('/bar') .reply(200, ' hello from bar! ', { 'Content-Type': 'text/html', }) - return this.rp({ + const injection = await runner.getInjectionContents() + const contents = removeWhitespace(Fixtures.get('server/expected_no_head_tag_inject.html').replace('{{injection}}', injection)) + + const res = await this.rp({ url: 'http://www.google.com/bar', headers: { 'Cookie': '__cypress.initial=true', }, }) - .then((res) => { - expect(res.statusCode).to.eq(200) + const body = cleanResponseBody(res.body) - const body = cleanResponseBody(res.body) - - expect(body).to.eq(contents) - }) + expect(res.statusCode).to.eq(200) + expect(body).to.eq(contents) }) it('injects when head is capitalized', function () { @@ -2672,7 +2669,7 @@ describe('Routes', () => { expect(res.statusCode).to.eq(200) expect(res.headers['set-cookie']).to.match(/initial=;/) - expect(res.body).to.include('Cypress.action(') + expect(res.body).to.include('parent.Cypress') }) }) }) @@ -2741,25 +2738,21 @@ describe('Routes', () => { }) }) - it('injects into https server', function () { - const contents = removeWhitespace(Fixtures.get('server/expected_https_inject.html')) + it('injects into https server', async function () { + await this.setup('https://localhost:8443') - return this.setup('https://localhost:8443') - .then(() => { - return this.rp({ - url: 'https://localhost:8443/', - headers: { - 'Cookie': '__cypress.initial=true', - }, - }) - .then((res) => { - expect(res.statusCode).to.eq(200) - - const body = cleanResponseBody(res.body) - - expect(body).to.eq(contents) - }) + const injection = await runner.getInjectionContents() + const contents = removeWhitespace(Fixtures.get('server/expected_https_inject.html').replace('{{injection}}', injection)) + const res = await this.rp({ + url: 'https://localhost:8443/', + headers: { + 'Cookie': '__cypress.initial=true', + }, }) + const body = cleanResponseBody(res.body) + + expect(res.statusCode).to.eq(200) + expect(body).to.eq(contents) }) it('injects into https://www.google.com', function () { @@ -2782,7 +2775,7 @@ describe('Routes', () => { .then((res) => { expect(res.statusCode).to.eq(200) - expect(res.body).to.include('Cypress.action(') + expect(res.body).to.include('parent.Cypress') }) }) }) @@ -2812,50 +2805,40 @@ describe('Routes', () => { }) }) - it('works with host swapping', function () { - const contents = removeWhitespace(Fixtures.get('server/expected_https_inject.html')) + it('works with host swapping', async function () { + await this.setup('https://www.foobar.com:8443') + evilDns.add('*.foobar.com', '127.0.0.1') - return this.setup('https://www.foobar.com:8443') - .then(() => { - evilDns.add('*.foobar.com', '127.0.0.1') - - return this.rp({ - url: 'https://www.foobar.com:8443/index.html', - headers: { - 'Cookie': '__cypress.initial=true', - }, - }) - .then((res) => { - expect(res.statusCode).to.eq(200) - - const body = cleanResponseBody(res.body) - - expect(body).to.eq(contents.replace('localhost', 'foobar.com')) - }) + const injection = await runner.getInjectionContents() + const contents = removeWhitespace(Fixtures.get('server/expected_https_inject.html').replace('{{injection}}', injection)) + const res = await this.rp({ + url: 'https://www.foobar.com:8443/index.html', + headers: { + 'Cookie': '__cypress.initial=true', + }, }) + const body = cleanResponseBody(res.body) + + expect(res.statusCode).to.eq(200) + expect(body).to.eq(contents.replace('localhost', 'foobar.com')) }) - it('continues to inject on the same https superdomain but different subdomain', function () { - const contents = removeWhitespace(Fixtures.get('server/expected_https_inject.html')) + it('continues to inject on the same https superdomain but different subdomain', async function () { + await this.setup('https://www.foobar.com:8443') + evilDns.add('*.foobar.com', '127.0.0.1') - return this.setup('https://www.foobar.com:8443') - .then(() => { - evilDns.add('*.foobar.com', '127.0.0.1') - - return this.rp({ - url: 'https://docs.foobar.com:8443/index.html', - headers: { - 'Cookie': '__cypress.initial=true', - }, - }) - .then((res) => { - expect(res.statusCode).to.eq(200) - - const body = cleanResponseBody(res.body) - - expect(body).to.eq(contents.replace('localhost', 'foobar.com')) - }) + const injection = await runner.getInjectionContents() + const contents = removeWhitespace(Fixtures.get('server/expected_https_inject.html').replace('{{injection}}', injection)) + const res = await this.rp({ + url: 'https://docs.foobar.com:8443/index.html', + headers: { + 'Cookie': '__cypress.initial=true', + }, }) + const body = cleanResponseBody(res.body) + + expect(res.statusCode).to.eq(200) + expect(body).to.eq(contents.replace('localhost', 'foobar.com')) }) it('injects document.domain on https requests to same superdomain but different subdomain', function () { @@ -3410,8 +3393,8 @@ describe('Routes', () => { }) .then((res) => { expect(res.statusCode).to.eq(200) - expect(res.body).to.match(/index.html content/) - expect(res.body).to.match(/Cypress\.action/) + expect(res.body).to.include('index.html content') + expect(res.body).to.include('parent.Cypress') expect(res.headers['set-cookie']).to.match(/initial=;/) expect(res.headers['cache-control']).to.eq('no-cache, no-store, must-revalidate') diff --git a/packages/server/test/integration/server_spec.js b/packages/server/test/integration/server_spec.js index ae0c38fef4..2e67413a09 100644 --- a/packages/server/test/integration/server_spec.js +++ b/packages/server/test/integration/server_spec.js @@ -156,7 +156,8 @@ describe('Server', () => { expect(res.body).to.include('index.html content') expect(res.body).to.include('document.domain = \'localhost\'') - expect(res.body).to.include('Cypress.action(\'app:window:before:load\', window); \n ') + expect(res.body).to.include('.action("app:window:before:load",window)') + expect(res.body).to.include('\n ') }) }) }) @@ -440,7 +441,8 @@ describe('Server', () => { expect(res.body).to.include('content') expect(res.body).to.include('document.domain = \'getbootstrap.com\'') - expect(res.body).to.include('Cypress.action(\'app:window:before:load\', window); content') + expect(res.body).to.include('.action("app:window:before:load",window)') + expect(res.body).to.include('content') }) }) }) @@ -569,7 +571,8 @@ describe('Server', () => { expect(res.statusCode).to.eq(200) expect(res.body).to.include('content') expect(res.body).to.include('document.domain = \'go.com\'') - expect(res.body).to.include('Cypress.action(\'app:window:before:load\', window); content') + expect(res.body).to.include('.action("app:window:before:load",window)') + expect(res.body).to.include('content') expect(this.server._getRemoteState()).to.deep.eq({ auth: undefined, @@ -654,7 +657,8 @@ describe('Server', () => { expect(res.statusCode).to.eq(200) expect(res.body).to.include('document.domain') expect(res.body).to.include('go.com') - expect(res.body).to.include('Cypress.action(\'app:window:before:load\', window); espn') + expect(res.body).to.include('.action("app:window:before:load",window)') + expect(res.body).to.include('espn') expect(this.buffers.buffer).to.be.undefined }) @@ -827,7 +831,8 @@ describe('Server', () => { expect(res.body).to.include('content') expect(res.body).to.include('document.domain = \'google.com\'') - expect(res.body).to.include('Cypress.action(\'app:window:before:load\', window); content') + expect(res.body).to.include('.action("app:window:before:load",window)') + expect(res.body).to.include('content') }) }) }) @@ -1032,7 +1037,8 @@ describe('Server', () => { expect(res.body).to.include('document.domain') expect(res.body).to.include('google.com') - expect(res.body).to.include('Cypress.action(\'app:window:before:load\', window); google') + expect(res.body).to.include('.action("app:window:before:load",window)') + expect(res.body).to.include('google') }) }).then(() => { expect(this.server._getRemoteState()).to.deep.eq({ @@ -1071,7 +1077,7 @@ describe('Server', () => { expect(res.body).to.include('document.domain') expect(res.body).to.include('localhost') - expect(res.body).to.include('Cypress.action(\'app:window:before:load\', window); \n ') + expect(res.body).to.include('.action("app:window:before:load",window)') }) }).then(() => { expect(this.server._getRemoteState()).to.deep.eq({ @@ -1104,7 +1110,8 @@ describe('Server', () => { expect(res.body).to.include('document.domain') expect(res.body).to.include('google.com') - expect(res.body).to.include('Cypress.action(\'app:window:before:load\', window); google') + expect(res.body).to.include('.action("app:window:before:load",window)') + expect(res.body).to.include('google') }) }).then(() => { expect(this.server._getRemoteState()).to.deep.eq({ @@ -1147,7 +1154,8 @@ describe('Server', () => { expect(res.body).to.include('document.domain') expect(res.body).to.include('foobar.com') - expect(res.body).to.include('Cypress.action(\'app:window:before:load\', window); https server') + expect(res.body).to.include('.action("app:window:before:load",window)') + expect(res.body).to.include('https server') }) }).then(() => { expect(this.server._getRemoteState()).to.deep.eq({ @@ -1186,7 +1194,7 @@ describe('Server', () => { expect(res.body).to.include('document.domain') expect(res.body).to.include('localhost') - expect(res.body).to.include('Cypress.action(\'app:window:before:load\', window); \n ') + expect(res.body).to.include('.action("app:window:before:load",window)') }) }).then(() => { expect(this.server._getRemoteState()).to.deep.eq({ @@ -1219,7 +1227,8 @@ describe('Server', () => { expect(res.body).to.include('document.domain') expect(res.body).to.include('foobar.com') - expect(res.body).to.include('Cypress.action(\'app:window:before:load\', window); https server') + expect(res.body).to.include('.action("app:window:before:load",window)') + expect(res.body).to.include('https server') }) }).then(() => { expect(this.server._getRemoteState()).to.deep.eq({ @@ -1310,7 +1319,7 @@ describe('Server', () => { expect(res.body).to.include('document.domain') expect(res.body).to.include('localhost') - expect(res.body).to.include('Cypress.action(\'app:window:before:load\', window); \n ') + expect(res.body).to.include('.action("app:window:before:load",window)') }) }).then(() => { expect(this.server._getRemoteState()).to.deep.eq({ diff --git a/packages/server/test/support/fixtures/server/expected_head_inject.html b/packages/server/test/support/fixtures/server/expected_head_inject.html index 1e2cac8c2d..7574cf2b73 100644 --- a/packages/server/test/support/fixtures/server/expected_head_inject.html +++ b/packages/server/test/support/fixtures/server/expected_head_inject.html @@ -3,13 +3,7 @@ diff --git a/packages/server/test/support/fixtures/server/expected_https_inject.html b/packages/server/test/support/fixtures/server/expected_https_inject.html index 26b5f25aba..d4095639cd 100644 --- a/packages/server/test/support/fixtures/server/expected_https_inject.html +++ b/packages/server/test/support/fixtures/server/expected_https_inject.html @@ -1,11 +1,6 @@ https server diff --git a/packages/server/test/support/fixtures/server/expected_no_head_tag_inject.html b/packages/server/test/support/fixtures/server/expected_no_head_tag_inject.html index 1c2f09d60f..c825225cd8 100644 --- a/packages/server/test/support/fixtures/server/expected_no_head_tag_inject.html +++ b/packages/server/test/support/fixtures/server/expected_no_head_tag_inject.html @@ -3,13 +3,7 @@ hello from bar! diff --git a/packages/ui-components/webpack.config.ts b/packages/ui-components/webpack.config.ts index 3a4b08ddff..809b54655e 100644 --- a/packages/ui-components/webpack.config.ts +++ b/packages/ui-components/webpack.config.ts @@ -1,4 +1,4 @@ -import getCommonConfig, { HtmlWebpackPlugin } from '@packages/web-config/webpack.config.base' +import { getCommonConfig, HtmlWebpackPlugin } from '@packages/web-config/webpack.config.base' import path from 'path' import webpack from 'webpack' diff --git a/packages/web-config/webpack.config.base.ts b/packages/web-config/webpack.config.base.ts index 9ace8fd7b1..bbb7f96bba 100644 --- a/packages/web-config/webpack.config.base.ts +++ b/packages/web-config/webpack.config.base.ts @@ -28,7 +28,32 @@ const evalDevToolPlugin = new webpack.EvalDevToolModulePlugin({ evalDevToolPlugin.evalDevToolPlugin = true -const getCommonConfig = () => { +const optimization = { + usedExports: true, + providedExports: true, + sideEffects: true, + namedChunks: true, + namedModules: true, + removeAvailableModules: true, + mergeDuplicateChunks: true, + flagIncludedChunks: true, + removeEmptyChunks: true, +} + +const stats = { + errors: true, + warningsFilter: /node_modules\/mocha\/lib\/mocha.js/, + warnings: true, + all: false, + builtAt: true, + colors: true, + modules: true, + maxModules: 20, + excludeModules: /(main|test-entry).scss/, + timings: true, +} + +export const getCommonConfig = () => { const commonConfig: webpack.Configuration = { mode: 'none', node: { @@ -42,18 +67,8 @@ const getCommonConfig = () => { extensions: ['.ts', '.js', '.jsx', '.tsx', '.scss', '.json'], }, - stats: { - errors: true, - warningsFilter: /node_modules\/mocha\/lib\/mocha.js/, - warnings: true, - all: false, - builtAt: true, - colors: true, - modules: true, - maxModules: 20, - excludeModules: /(main|test-entry).scss/, - timings: true, - }, + stats, + optimization, module: { rules: [ @@ -153,18 +168,6 @@ const getCommonConfig = () => { ], }, - optimization: { - usedExports: true, - providedExports: true, - sideEffects: true, - namedChunks: true, - namedModules: true, - removeAvailableModules: true, - mergeDuplicateChunks: true, - flagIncludedChunks: true, - removeEmptyChunks: true, - }, - plugins: [ new CleanWebpackPlugin({ cleanStaleWebpackAssets: false }), new MiniCSSExtractWebpackPlugin(), @@ -197,6 +200,38 @@ const getCommonConfig = () => { return commonConfig } -export default getCommonConfig +// eslint-disable-next-line @cypress/dev/arrow-body-multiline-braces +export const getSimpleConfig = () => ({ + resolve: { + extensions: ['.js'], + }, + + stats, + optimization, + + cache: true, + + module: { + rules: [ + { + test: /\.(js)$/, + exclude: /node_modules/, + use: { + loader: require.resolve('babel-loader'), + options: { + presets: [ + [require.resolve('@babel/preset-env'), { targets: { 'chrome': 63 } }], + ], + babelrc: false, + }, + }, + }, + ], + }, + + plugins: [ + new CleanWebpackPlugin({ cleanStaleWebpackAssets: false }), + ], +}) export { HtmlWebpackPlugin } diff --git a/scripts/binary/util/testStaticAssets.js b/scripts/binary/util/testStaticAssets.js index 9607e5d108..b95e2c3732 100644 --- a/scripts/binary/util/testStaticAssets.js +++ b/scripts/binary/util/testStaticAssets.js @@ -13,7 +13,7 @@ const globAsync = Promise.promisify(glob) const testStaticAssets = async (buildResourcePath) => { await Promise.all([ testPackageStaticAssets({ - assetGlob: `${buildResourcePath}/packages/runner/dist/*.js`, + assetGlob: `${buildResourcePath}/packages/runner/dist/cypress_runner.js`, badStrings: [ // should only exist during development 'webpack-livereload-plugin', @@ -34,6 +34,12 @@ const testStaticAssets = async (buildResourcePath) => { ], minLineCount: 5000, }), + testPackageStaticAssets({ + assetGlob: `${buildResourcePath}/packages/runner/dist/injection.js`, + goodStrings: [ + 'action("app:window:before:load",window)', + ], + }), testPackageStaticAssets({ assetGlob: `${buildResourcePath}/packages/runner/dist/*.css`, goodStrings: [ From 5ed2aacd9ad17b6ff7bac90c18ade900d9b43f1b Mon Sep 17 00:00:00 2001 From: Jennifer Shehane Date: Mon, 1 Mar 2021 12:33:44 -0500 Subject: [PATCH 010/134] BREAKING CHANGE: empty commit for 7.0.0 semantic release From fd2e363855041d532ecf70a5d4ca85a9245a6666 Mon Sep 17 00:00:00 2001 From: Kukhyeon Heo Date: Tue, 2 Mar 2021 10:51:17 +0900 Subject: [PATCH 011/134] fix: nextUntil failure caused by PR 14965 (#15266) --- packages/driver/cypress/fixtures/dom.html | 42 +++++++++++------------ 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/packages/driver/cypress/fixtures/dom.html b/packages/driver/cypress/fixtures/dom.html index 330dae3ab6..7061e6b30c 100644 --- a/packages/driver/cypress/fixtures/dom.html +++ b/packages/driver/cypress/fixtures/dom.html @@ -654,27 +654,27 @@ Cross domain iframe:
- - -
- - -
- -
- - -
- - - +
+
+ + +
+ +
+ + +
+ + +
+ From e76edd7ffc6a15a03d31679b628970ae8d71f658 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Burzy=C5=84ski?= Date: Wed, 3 Mar 2021 16:27:40 +0100 Subject: [PATCH 012/134] test: add test for async errors being caught by AUT error handlers (#15301) --- packages/driver/cypress/fixtures/errors.html | 2 +- .../integration/e2e/uncaught_errors_spec.js | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/packages/driver/cypress/fixtures/errors.html b/packages/driver/cypress/fixtures/errors.html index c3b6f2b9d0..9e93a1393a 100644 --- a/packages/driver/cypress/fixtures/errors.html +++ b/packages/driver/cypress/fixtures/errors.html @@ -54,7 +54,7 @@ document.querySelector(".trigger-async-error").addEventListener('click', function () { setTimeout(function () { one('async error') - }, 500) + }, 0) }) document.querySelector(".trigger-unhandled-rejection").addEventListener('click', function () { diff --git a/packages/driver/cypress/integration/e2e/uncaught_errors_spec.js b/packages/driver/cypress/integration/e2e/uncaught_errors_spec.js index a01597f2b6..cbc2b12b15 100644 --- a/packages/driver/cypress/integration/e2e/uncaught_errors_spec.js +++ b/packages/driver/cypress/integration/e2e/uncaught_errors_spec.js @@ -83,6 +83,20 @@ describe('uncaught errors', () => { cy.get('.trigger-async-error').click() }) + // we used to wrap timers with "proxy" tracking functions + // this has been called from the top frame + // and thus its error handler has been catching the error and not the one in AUT + it('async error triggers the app-under-test error handler', () => { + // mute auto-failing this test + cy.once('uncaught:exception', () => false) + + cy.visit('/fixtures/errors.html') + cy.get('.trigger-async-error').click() + + cy.get('.error-one').invoke('text').should('equal', 'async error') + cy.get('.error-two').invoke('text').should('equal', 'async error') + }) + it('unhandled rejection triggers uncaught:exception and has promise as third argument', (done) => { cy.once('uncaught:exception', (err, runnable, promise) => { expect(err.stack).to.include('promise rejection') From fb593bebe1247f8a2a42d5a37ac161d4d0634632 Mon Sep 17 00:00:00 2001 From: Chris Breiding Date: Wed, 3 Mar 2021 10:54:17 -0500 Subject: [PATCH 013/134] chore: Revert submit validation (#15302) --- packages/driver/cypress/fixtures/dom.html | 21 ------------------- .../commands/actions/submit_spec.js | 15 ------------- .../driver/src/cy/commands/actions/submit.js | 20 ------------------ packages/driver/src/cypress/error_messages.js | 8 +------ 4 files changed, 1 insertion(+), 63 deletions(-) diff --git a/packages/driver/cypress/fixtures/dom.html b/packages/driver/cypress/fixtures/dom.html index 7061e6b30c..be04e3cbcc 100644 --- a/packages/driver/cypress/fixtures/dom.html +++ b/packages/driver/cypress/fixtures/dom.html @@ -654,27 +654,6 @@ Cross domain iframe:
- -
-
- - -
- -
- - -
- - -
diff --git a/packages/driver/cypress/integration/commands/actions/submit_spec.js b/packages/driver/cypress/integration/commands/actions/submit_spec.js index 8b1d740085..d379edbeb8 100644 --- a/packages/driver/cypress/integration/commands/actions/submit_spec.js +++ b/packages/driver/cypress/integration/commands/actions/submit_spec.js @@ -335,21 +335,6 @@ describe('src/cy/commands/actions/submit', () => { cy.get('form:first').submit().should('have.class', 'submitted') }) - - // https://github.com/cypress-io/cypress/issues/14911 - it('should throw an error when form validation failed', function (done) { - cy.on('fail', (err) => { - expect(err.message).to.include('2 inputs') - expect(err.message).to.include('Please fill out this field.') - - done() - }) - - cy.get('#form-validation').within(() => { - cy.get('input[type=submit]').click() - cy.root().submit() - }) - }) }) describe('.log', () => { diff --git a/packages/driver/src/cy/commands/actions/submit.js b/packages/driver/src/cy/commands/actions/submit.js index ed7199ef3e..70c67bf702 100644 --- a/packages/driver/src/cy/commands/actions/submit.js +++ b/packages/driver/src/cy/commands/actions/submit.js @@ -54,26 +54,6 @@ module.exports = (Commands, Cypress, cy) => { }) } - // Validate form. - // @see https://github.com/cypress-io/cypress/issues/14911 - if (!form.checkValidity()) { - const elements = form.querySelectorAll(':invalid') - const failures = _.map(elements, (el) => { - const element = $dom.stringify(el) - const message = el.validationMessage - - return ` - \`${element}\`: ${message}` - }) - - $errUtils.throwErrByPath('submit.failed_validation', { - onFail: options._log, - args: { - failures: failures.join('\n'), - suffix: failures.length ? `${failures.length} inputs` : 'input', - }, - }) - } - // calling the native submit method will not actually trigger // a submit event, so we need to dispatch this manually so // native event listeners and jquery can bind to it diff --git a/packages/driver/src/cypress/error_messages.js b/packages/driver/src/cypress/error_messages.js index 2094eee446..57544db340 100644 --- a/packages/driver/src/cypress/error_messages.js +++ b/packages/driver/src/cypress/error_messages.js @@ -911,7 +911,7 @@ module.exports = { reached_redirection_limit ({ href, limit }) { return stripIndent`\ The application redirected to \`${href}\` more than ${limit} times. Please check if it's an intended behavior. - + If so, increase \`redirectionLimit\` value in configuration.` }, }, @@ -1542,12 +1542,6 @@ module.exports = { message: `${cmd('submit')} can only be called on a \`
\`. Your subject {{word}} a: \`{{node}}\``, docsUrl: 'https://on.cypress.io/submit', }, - failed_validation: { - message: stripIndent`\ - Form validation failed for the following {{suffix}}: - {{failures}}`, - docsUrl: 'https://on.cypress.io/submit', - }, }, task: { From a7655d3c201b167a8d9298e794133c5dd65c1418 Mon Sep 17 00:00:00 2001 From: Zach Bloomquist Date: Wed, 3 Mar 2021 13:21:52 -0500 Subject: [PATCH 014/134] feat: refactor cy.intercept internals to generic event-based system (#15255) --- NOTES.md | 9 + .../integration/commands/net_stubbing_spec.ts | 44 ++-- .../driver/src/cy/net-stubbing/add-command.ts | 42 ++-- ...{request-complete.ts => after-response.ts} | 22 +- ...{request-received.ts => before-request.ts} | 100 +++++---- .../src/cy/net-stubbing/events/index.ts | 82 +++++-- .../{response-received.ts => response.ts} | 68 +++--- .../cy/net-stubbing/static-response-utils.ts | 19 +- packages/driver/src/cypress/events.ts | 2 + packages/net-stubbing/lib/external-types.ts | 44 ++-- packages/net-stubbing/lib/internal-types.ts | 73 +++---- .../net-stubbing/lib/server/driver-events.ts | 70 ++++-- packages/net-stubbing/lib/server/index.ts | 6 +- .../lib/server/intercept-error.ts | 34 --- .../lib/server/intercept-request.ts | 204 ------------------ .../lib/server/intercept-response.ts | 123 ----------- .../lib/server/intercepted-request.ts | 121 +++++++++++ .../lib/server/middleware/error.ts | 31 +++ .../lib/server/middleware/request.ts | 169 +++++++++++++++ .../lib/server/middleware/response.ts | 81 +++++++ .../net-stubbing/lib/server/route-matching.ts | 3 + packages/net-stubbing/lib/server/state.ts | 2 + packages/net-stubbing/lib/server/types.ts | 40 +--- packages/net-stubbing/lib/server/util.ts | 15 +- packages/net-stubbing/package.json | 1 + .../test/integration/net-stubbing.spec.ts | 90 ++++---- .../cypress/fixtures/errors/intercept_spec.ts | 43 ++++ .../integration/reporter.errors.spec.js | 20 ++ packages/runner/cypress/support/helpers.js | 3 + packages/server/lib/socket-base.ts | 1 - yarn.lock | 87 +++++--- 31 files changed, 926 insertions(+), 723 deletions(-) create mode 100644 NOTES.md rename packages/driver/src/cy/net-stubbing/events/{request-complete.ts => after-response.ts} (67%) rename packages/driver/src/cy/net-stubbing/events/{request-received.ts => before-request.ts} (74%) rename packages/driver/src/cy/net-stubbing/events/{response-received.ts => response.ts} (72%) delete mode 100644 packages/net-stubbing/lib/server/intercept-error.ts delete mode 100644 packages/net-stubbing/lib/server/intercept-request.ts delete mode 100644 packages/net-stubbing/lib/server/intercept-response.ts create mode 100644 packages/net-stubbing/lib/server/intercepted-request.ts create mode 100644 packages/net-stubbing/lib/server/middleware/error.ts create mode 100644 packages/net-stubbing/lib/server/middleware/request.ts create mode 100644 packages/net-stubbing/lib/server/middleware/response.ts create mode 100644 packages/runner/cypress/fixtures/errors/intercept_spec.ts diff --git a/NOTES.md b/NOTES.md new file mode 100644 index 0000000000..ddc59e7215 --- /dev/null +++ b/NOTES.md @@ -0,0 +1,9 @@ +* New `RouteMatcher` option: `middleware: boolean` + * With `middleware: true`, will be called in the order they are defined and chained. + * With `middleware: true`, only dynamic handlers are supported - makes no sense to support `cy.intercept({ middleware: true }, staticResponse)` + * BREAKING CHANGE: `middleware: falsy` handlers will not be chained. For any given request, the most-recently-defined handler is always the one used. +* `req` is now an `EventEmitter` (regardless of `middleware` setting) +* Events on `req`: + * `request - ()` - Request will be sent outgoing. If the response has already been fulfilled by `req.reply`, this event will not be emitted. + * `before-response - (res)` - Response was received. Emitted before the response handler is run. + * `response - (res)` - Response will be sent to the browser. If a response has been supplied via `res.send`, this event will not be emitted. \ No newline at end of file diff --git a/packages/driver/cypress/integration/commands/net_stubbing_spec.ts b/packages/driver/cypress/integration/commands/net_stubbing_spec.ts index 9fe2f41983..0c4ca4d28e 100644 --- a/packages/driver/cypress/integration/commands/net_stubbing_spec.ts +++ b/packages/driver/cypress/integration/commands/net_stubbing_spec.ts @@ -1,4 +1,4 @@ -describe('network stubbing', { retries: 2 }, function () { +describe('network stubbing', { retries: { runMode: 2, openMode: 0 } }, function () { const { $, _, sinon, state, Promise } = Cypress beforeEach(function () { @@ -997,8 +997,8 @@ describe('network stubbing', { retries: 2 }, function () { it('can delay and throttle a StaticResponse', function (done) { const payload = 'A'.repeat(10 * 1024) const throttleKbps = 10 - const delay = 250 - const expectedSeconds = payload.length / (1024 * throttleKbps) + delay / 1000 + const delayMs = 250 + const expectedSeconds = payload.length / (1024 * throttleKbps) + delayMs / 1000 cy.intercept('/timeout', (req) => { this.start = Date.now() @@ -1007,7 +1007,7 @@ describe('network stubbing', { retries: 2 }, function () { statusCode: 200, body: payload, throttleKbps, - delay, + delayMs, }) }).then(() => { return $.get('/timeout').then((responseText) => { @@ -1019,33 +1019,15 @@ describe('network stubbing', { retries: 2 }, function () { }) }) - it('can delay with deprecated delayMs param', function (done) { - const delay = 250 - - cy.intercept('/timeout', (req) => { - this.start = Date.now() - - req.reply({ - delay, - }) - }).then(() => { - return $.get('/timeout').then((responseText) => { - expect(Date.now() - this.start).to.be.closeTo(250 + 100, 100) - - done() - }) - }) - }) - // @see https://github.com/cypress-io/cypress/issues/14446 it('should delay the same amount on every response', () => { - const delay = 250 + const delayMs = 250 const testDelay = () => { const start = Date.now() return $.get('/timeout').then((responseText) => { - expect(Date.now() - start).to.be.closeTo(delay, 50) + expect(Date.now() - start).to.be.closeTo(delayMs, 50) expect(responseText).to.eq('foo') }) } @@ -1053,7 +1035,7 @@ describe('network stubbing', { retries: 2 }, function () { cy.intercept('/timeout', { statusCode: 200, body: 'foo', - delay, + delayMs, }).as('get') .then(() => testDelay()).wait('@get') .then(() => testDelay()).wait('@get') @@ -1636,15 +1618,15 @@ describe('network stubbing', { retries: 2 }, function () { const payload = 'A'.repeat(10 * 1024) const kbps = 20 let expectedSeconds = payload.length / (1024 * kbps) - const delay = 500 + const delayMs = 500 - expectedSeconds += delay / 1000 + expectedSeconds += delayMs / 1000 cy.intercept('/timeout', (req) => { req.reply((res) => { this.start = Date.now() - res.throttle(kbps).delay(delay).send({ + res.throttle(kbps).delay(delayMs).send({ statusCode: 200, body: payload, }) @@ -1880,8 +1862,8 @@ describe('network stubbing', { retries: 2 }, function () { it('can delay and throttle', function (done) { const payload = 'A'.repeat(10 * 1024) const throttleKbps = 50 - const delay = 50 - const expectedSeconds = payload.length / (1024 * throttleKbps) + delay / 1000 + const delayMs = 50 + const expectedSeconds = payload.length / (1024 * throttleKbps) + delayMs / 1000 cy.intercept('/timeout', (req) => { req.reply((res) => { @@ -1892,7 +1874,7 @@ describe('network stubbing', { retries: 2 }, function () { statusCode: 200, body: payload, throttleKbps, - delay, + delayMs, }) }) }).then(() => { diff --git a/packages/driver/src/cy/net-stubbing/add-command.ts b/packages/driver/src/cy/net-stubbing/add-command.ts index 262d38522c..9a87171718 100644 --- a/packages/driver/src/cy/net-stubbing/add-command.ts +++ b/packages/driver/src/cy/net-stubbing/add-command.ts @@ -10,7 +10,7 @@ import { DICT_STRING_MATCHER_FIELDS, AnnotatedRouteMatcherOptions, AnnotatedStringMatcher, - NetEventFrames, + NetEvent, StringMatcher, NumberMatcher, } from '@packages/net-stubbing/lib/types' @@ -217,31 +217,25 @@ export function addCommand (Commands, Cypress: Cypress.Cypress, cy: Cypress.cy, let staticResponse: StaticResponse | undefined = undefined let hasInterceptor = false - switch (true) { - case isHttpRequestInterceptor(handler): - hasInterceptor = true - break - case _.isUndefined(handler): - // user is doing something like cy.intercept('foo').as('foo') to wait on a URL - break - case _.isString(handler): - staticResponse = { body: handler } - break - case _.isObjectLike(handler): - if (!hasStaticResponseKeys(handler)) { - // the user has not supplied any of the StaticResponse keys, assume it's a JSON object - // that should become the body property - handler = { - body: handler, - } + if (isHttpRequestInterceptor(handler)) { + hasInterceptor = true + } else if (_.isString(handler)) { + staticResponse = { body: handler } + } else if (_.isObjectLike(handler)) { + if (!hasStaticResponseKeys(handler)) { + // the user has not supplied any of the StaticResponse keys, assume it's a JSON object + // that should become the body property + handler = { + body: handler, } + } - validateStaticResponse('cy.intercept', handler) + validateStaticResponse('cy.intercept', handler) - staticResponse = handler as StaticResponse - break - default: - return $errUtils.throwErrByPath('net_stubbing.intercept.invalid_handler', { args: { handler } }) + staticResponse = handler as StaticResponse + } else if (!_.isUndefined(handler)) { + // a handler was passed but we dunno what it's supposed to be + return $errUtils.throwErrByPath('net_stubbing.intercept.invalid_handler', { args: { handler } }) } const routeMatcher = annotateMatcherOptionsTypes(matcher) @@ -252,7 +246,7 @@ export function addCommand (Commands, Cypress: Cypress.Cypress, cy: Cypress.cy, routeMatcher.headers = lowercaseFieldNames(routeMatcher.headers) } - const frame: NetEventFrames.AddRoute = { + const frame: NetEvent.ToServer.AddRoute = { handlerId, hasInterceptor, routeMatcher, diff --git a/packages/driver/src/cy/net-stubbing/events/request-complete.ts b/packages/driver/src/cy/net-stubbing/events/after-response.ts similarity index 67% rename from packages/driver/src/cy/net-stubbing/events/request-complete.ts rename to packages/driver/src/cy/net-stubbing/events/after-response.ts index 26c5b5d3f4..e5aceca444 100644 --- a/packages/driver/src/cy/net-stubbing/events/request-complete.ts +++ b/packages/driver/src/cy/net-stubbing/events/after-response.ts @@ -1,20 +1,22 @@ import { get } from 'lodash' -import { NetEventFrames } from '@packages/net-stubbing/lib/types' +import { CyHttpMessages } from '@packages/net-stubbing/lib/types' import { errByPath, makeErrFromObj } from '../../../cypress/error_utils' -import { HandlerFn } from './' +import { HandlerFn } from '.' -export const onRequestComplete: HandlerFn = (Cypress, frame, { failCurrentTest, getRequest, getRoute }) => { +export const onAfterResponse: HandlerFn = (Cypress, frame, userHandler, { getRequest, getRoute }) => { const request = getRequest(frame.routeHandlerId, frame.requestId) + const { data } = frame + if (!request) { - return + return frame.data } - if (frame.error) { - let err = makeErrFromObj(frame.error) + if (data.error) { + let err = makeErrFromObj(data.error) // does this request have a responseHandler that has not run yet? const isAwaitingResponse = !!request.responseHandler && ['Received', 'Intercepted'].includes(request.state) - const isTimeoutError = frame.error.code && ['ESOCKETTIMEDOUT', 'ETIMEDOUT'].includes(frame.error.code) + const isTimeoutError = data.error.code && ['ESOCKETTIMEDOUT', 'ETIMEDOUT'].includes(data.error.code) if (isAwaitingResponse || isTimeoutError) { const errorName = isTimeoutError ? 'timeout' : 'network_error' @@ -34,14 +36,16 @@ export const onRequestComplete: HandlerFn = if (isAwaitingResponse) { // the user is implicitly expecting there to be a successful response from the server, so fail the test // since a network error has occured - return failCurrentTest(err) + throw err } - return + return frame.data } request.state = 'Complete' request.log.fireChangeEvent() request.log.end() + + return frame.data } diff --git a/packages/driver/src/cy/net-stubbing/events/request-received.ts b/packages/driver/src/cy/net-stubbing/events/before-request.ts similarity index 74% rename from packages/driver/src/cy/net-stubbing/events/request-received.ts rename to packages/driver/src/cy/net-stubbing/events/before-request.ts index 0428665ce2..778a9550c3 100644 --- a/packages/driver/src/cy/net-stubbing/events/request-received.ts +++ b/packages/driver/src/cy/net-stubbing/events/before-request.ts @@ -4,21 +4,20 @@ import { Route, Interception, CyHttpMessages, - StaticResponse, SERIALIZABLE_REQ_PROPS, - NetEventFrames, + Subscription, } from '../types' import { parseJsonBody } from './utils' import { validateStaticResponse, - getBackendStaticResponse, parseStaticResponseShorthand, } from '../static-response-utils' import $errUtils from '../../../cypress/error_utils' -import { HandlerFn } from './' +import { HandlerFn } from '.' import Bluebird from 'bluebird' +import { NetEvent } from '@packages/net-stubbing/lib/types' -export const onRequestReceived: HandlerFn = (Cypress, frame, { getRoute, emitNetEvent }) => { +export const onBeforeRequest: HandlerFn = (Cypress, frame, userHandler, { getRoute, emitNetEvent, sendStaticResponse }) => { function getRequestLog (route: Route, request: Omit) { return Cypress.log({ name: 'xhr', @@ -48,22 +47,35 @@ export const onRequestReceived: HandlerFn = } const route = getRoute(frame.routeHandlerId) - const { req, requestId, routeHandlerId } = frame + const { data: req, requestId, routeHandlerId } = frame parseJsonBody(req) - const request: Partial = { + const request: Interception = { id: requestId, routeHandlerId, request: req, state: 'Received', requestWaited: false, responseWaited: false, - } + subscriptions: [], + on (eventName, handler) { + const subscription: Subscription = { + id: _.uniqueId('Subscription'), + routeHandlerId, + eventName, + await: true, + } - const continueFrame: Partial = { - routeHandlerId, - requestId, + request.subscriptions.push({ + subscription, + handler, + }) + + emitNetEvent('subscribe', { requestId, subscription } as NetEvent.ToServer.Subscribe) + + return request + }, } let resolved = false @@ -93,17 +105,20 @@ export const onRequestReceived: HandlerFn = request.responseHandler = responseHandler // signals server to send a http:response:received - continueFrame.hasResponseHandler = true + request.on('response', responseHandler) + userReq.responseTimeout = userReq.responseTimeout || Cypress.config('responseTimeout') return sendContinueFrame() } if (!_.isUndefined(responseHandler)) { - // `replyHandler` is a StaticResponse + // `responseHandler` is a StaticResponse validateStaticResponse('req.reply', responseHandler) - continueFrame.staticResponse = getBackendStaticResponse(responseHandler as StaticResponse) + sendStaticResponse(requestId, responseHandler) + + return finishRequestStage(req) } return sendContinueFrame() @@ -123,6 +138,19 @@ export const onRequestReceived: HandlerFn = let continueSent = false + function finishRequestStage (req) { + if (request) { + request.request = _.cloneDeep(req) + + request.state = 'Intercepted' + request.log && request.log.fireChangeEvent() + } + } + + if (!route) { + return req + } + const sendContinueFrame = () => { if (continueSent) { throw new Error('sendContinueFrame called twice in handler') @@ -130,31 +158,23 @@ export const onRequestReceived: HandlerFn = continueSent = true - if (continueFrame) { - // copy changeable attributes of userReq to req in frame - // @ts-ignore - continueFrame.req = { - ..._.pick(userReq, SERIALIZABLE_REQ_PROPS), - } + // copy changeable attributes of userReq to req + _.merge(req, _.pick(userReq, SERIALIZABLE_REQ_PROPS)) - _.merge(request.request, continueFrame.req) + finishRequestStage(req) - if (_.isObject(continueFrame.req!.body)) { - continueFrame.req!.body = JSON.stringify(continueFrame.req!.body) - } - - emitNetEvent('http:request:continue', continueFrame) + if (_.isObject(req.body)) { + req.body = JSON.stringify(req.body) } - if (request) { - request.state = 'Intercepted' - request.log && request.log.fireChangeEvent() - } + resolve(req) } - if (!route) { - return sendContinueFrame() - } + let resolve: (changedData: CyHttpMessages.IncomingRequest) => void + + const promise: Promise = new Promise((_resolve) => { + resolve = _resolve + }) request.log = getRequestLog(route, request as Omit) @@ -162,24 +182,20 @@ export const onRequestReceived: HandlerFn = route.log.set('numResponses', (route.log.get('numResponses') || 0) + 1) route.requests[requestId] = request as Interception - if (frame.notificationOnly) { - return + if (!_.isFunction(userHandler)) { + // notification-only + return req } route.hitCount++ - if (!_.isFunction(route.handler)) { - return sendContinueFrame() - } - - const handler = route.handler as Function const timeout = Cypress.config('defaultCommandTimeout') const curTest = Cypress.state('test') // if a Promise is returned, wait for it to resolve. if req.reply() // has not been called, continue to the next interceptor return Bluebird.try(() => { - return handler(userReq) + return userHandler(userReq) }) .catch((err) => { $errUtils.throwErrByPath('net_stubbing.request_handling.cb_failed', { @@ -220,8 +236,8 @@ export const onRequestReceived: HandlerFn = if (!replyCalled) { // handler function resolved without resolving request, pass on - continueFrame.tryNextRoute = true sendContinueFrame() } }) + .return(promise) as any as Bluebird } diff --git a/packages/driver/src/cy/net-stubbing/events/index.ts b/packages/driver/src/cy/net-stubbing/events/index.ts index 7ee4431853..352f2584aa 100644 --- a/packages/driver/src/cy/net-stubbing/events/index.ts +++ b/packages/driver/src/cy/net-stubbing/events/index.ts @@ -1,21 +1,21 @@ -import { Route, Interception } from '../types' -import { NetEventFrames } from '@packages/net-stubbing/lib/types' -import { onRequestReceived } from './request-received' -import { onResponseReceived } from './response-received' -import { onRequestComplete } from './request-complete' +import { Route, Interception, StaticResponse, NetEvent } from '../types' +import { onBeforeRequest } from './before-request' +import { onResponse } from './response' +import { onAfterResponse } from './after-response' import Bluebird from 'bluebird' +import { getBackendStaticResponse } from '../static-response-utils' -export type HandlerFn = (Cypress: Cypress.Cypress, frame: Frame, opts: { +export type HandlerFn = (Cypress: Cypress.Cypress, frame: NetEvent.ToDriver.Event, userHandler: (data: D) => void | Promise, opts: { getRequest: (routeHandlerId: string, requestId: string) => Interception | undefined getRoute: (routeHandlerId: string) => Route | undefined emitNetEvent: (eventName: string, frame: any) => Promise - failCurrentTest: (err: Error) => void -}) => Promise | void + sendStaticResponse: (requestId: string, staticResponse: StaticResponse) => void +}) => Promise | D const netEventHandlers: { [eventName: string]: HandlerFn } = { - 'http:request:received': onRequestReceived, - 'http:response:received': onResponseReceived, - 'http:request:complete': onRequestComplete, + 'before:request': onBeforeRequest, + 'response': onResponse, + 'after:response': onAfterResponse, } export function registerEvents (Cypress: Cypress.Cypress, cy: Cypress.cy) { @@ -44,6 +44,13 @@ export function registerEvents (Cypress: Cypress.Cypress, cy: Cypress.cy) { }) } + function sendStaticResponse (requestId: string, staticResponse: StaticResponse) { + emitNetEvent('send:static:response', { + requestId, + staticResponse: getBackendStaticResponse(staticResponse), + }) + } + function failCurrentTest (err: Error) { // @ts-ignore cy.fail(err) @@ -55,16 +62,61 @@ export function registerEvents (Cypress: Cypress.Cypress, cy: Cypress.cy) { state('aliasedRequests', []) }) - Cypress.on('net:event', (eventName, frame: NetEventFrames.BaseHttp) => { - Bluebird.try(() => { + Cypress.on('net:event', (eventName, frame: NetEvent.ToDriver.Event) => { + Bluebird.try(async () => { const handler = netEventHandlers[eventName] - return handler(Cypress, frame, { + if (!handler) { + throw new Error(`received unknown net:event in driver: ${eventName}`) + } + + const emitResolved = (changedData: any) => { + return emitNetEvent('event:handler:resolved', { + eventId: frame.eventId, + changedData, + }) + } + + const route = getRoute(frame.routeHandlerId) + + if (!route) { + if (frame.subscription.await) { + // route not found, just resolve so the request can continue + emitResolved(frame.data) + } + + return + } + + const getUserHandler = () => { + if (eventName === 'before:request' && !frame.subscription.id) { + // users do not explicitly subscribe to the first `before:request` event (req handler) + return route && route.handler + } + + const request = getRequest(frame.routeHandlerId, frame.requestId) + + const subscription = request && request.subscriptions.find(({ subscription }) => { + return subscription.id === frame.subscription.id + }) + + return subscription && subscription.handler + } + + const userHandler = getUserHandler() + + const changedData = await handler(Cypress, frame, userHandler, { getRoute, getRequest, emitNetEvent, - failCurrentTest, + sendStaticResponse, }) + + if (!frame.subscription.await) { + return + } + + return emitResolved(changedData) }) .catch(failCurrentTest) }) diff --git a/packages/driver/src/cy/net-stubbing/events/response-received.ts b/packages/driver/src/cy/net-stubbing/events/response.ts similarity index 72% rename from packages/driver/src/cy/net-stubbing/events/response-received.ts rename to packages/driver/src/cy/net-stubbing/events/response.ts index a2e776a05d..99028839a6 100644 --- a/packages/driver/src/cy/net-stubbing/events/response-received.ts +++ b/packages/driver/src/cy/net-stubbing/events/response.ts @@ -3,21 +3,19 @@ import _ from 'lodash' import { CyHttpMessages, SERIALIZABLE_RES_PROPS, - NetEventFrames, } from '@packages/net-stubbing/lib/types' import { validateStaticResponse, parseStaticResponseShorthand, STATIC_RESPONSE_KEYS, - getBackendStaticResponse, } from '../static-response-utils' import $errUtils from '../../../cypress/error_utils' -import { HandlerFn } from './' +import { HandlerFn } from '.' import Bluebird from 'bluebird' import { parseJsonBody } from './utils' -export const onResponseReceived: HandlerFn = (Cypress, frame, { getRoute, getRequest, emitNetEvent }) => { - const { res, requestId, routeHandlerId } = frame +export const onResponse: HandlerFn = async (Cypress, frame, userHandler, { getRoute, getRequest, sendStaticResponse }) => { + const { data: res, requestId, routeHandlerId } = frame const request = getRequest(frame.routeHandlerId, frame.requestId) parseJsonBody(res) @@ -30,38 +28,24 @@ export const onResponseReceived: HandlerFn request.log.fireChangeEvent() - if (!request.responseHandler) { + if (!userHandler) { // this is notification-only, update the request with the response attributes and end request.response = res - return + return res } } - const continueFrame: NetEventFrames.HttpResponseContinue = { - routeHandlerId, - requestId, - } - - const sendContinueFrame = () => { - // copy changeable attributes of userRes to res in frame - // if the user is setting a StaticResponse, use that instead - // @ts-ignore - continueFrame.res = { - ..._.pick(continueFrame.staticResponse || userRes, SERIALIZABLE_RES_PROPS), - } - + const finishResponseStage = (res) => { if (request) { - request.response = _.clone(continueFrame.res) + request.response = _.cloneDeep(res) request.state = 'ResponseIntercepted' request.log.fireChangeEvent() } + } - if (_.isObject(continueFrame.res!.body)) { - continueFrame.res!.body = JSON.stringify(continueFrame.res!.body) - } - - emitNetEvent('http:response:continue', continueFrame) + if (!request) { + return res } const userRes: CyHttpMessages.IncomingHttpResponse = { @@ -91,32 +75,49 @@ export const onResponseReceived: HandlerFn _.defaults(_staticResponse.headers, res.headers) - continueFrame.staticResponse = getBackendStaticResponse(_staticResponse) + sendStaticResponse(requestId, _staticResponse) + + return finishResponseStage(_staticResponse) } return sendContinueFrame() }, - delay (delay) { - continueFrame.delay = delay + delay (delayMs) { + res.delayMs = delayMs return this }, throttle (throttleKbps) { - continueFrame.throttleKbps = throttleKbps + res.throttleKbps = throttleKbps return this }, } - if (!request) { - return sendContinueFrame() + const sendContinueFrame = () => { + // copy changeable attributes of userRes to res + _.merge(res, _.pick(userRes, SERIALIZABLE_RES_PROPS)) + + finishResponseStage(res) + + if (_.isObject(res.body)) { + res.body = JSON.stringify(res.body) + } + + resolve(_.cloneDeep(res)) } const timeout = Cypress.config('defaultCommandTimeout') const curTest = Cypress.state('test') + let resolve: (changedData: CyHttpMessages.IncomingResponse) => void + + const promise: Promise = new Promise((_resolve) => { + resolve = _resolve + }) + return Bluebird.try(() => { - return request.responseHandler!(userRes) + return userHandler!(userRes) }) .catch((err) => { $errUtils.throwErrByPath('net_stubbing.response_handling.cb_failed', { @@ -159,4 +160,5 @@ export const onResponseReceived: HandlerFn .finally(() => { resolved = true }) + .return(promise) } diff --git a/packages/driver/src/cy/net-stubbing/static-response-utils.ts b/packages/driver/src/cy/net-stubbing/static-response-utils.ts index 35572d0c7f..9a8dada922 100644 --- a/packages/driver/src/cy/net-stubbing/static-response-utils.ts +++ b/packages/driver/src/cy/net-stubbing/static-response-utils.ts @@ -8,14 +8,14 @@ import { import $errUtils from '../../cypress/error_utils' // user-facing StaticResponse only -export const STATIC_RESPONSE_KEYS: (keyof StaticResponse)[] = ['body', 'fixture', 'statusCode', 'headers', 'forceNetworkError', 'throttleKbps', 'delay', 'delayMs'] +export const STATIC_RESPONSE_KEYS: (keyof StaticResponse)[] = ['body', 'fixture', 'statusCode', 'headers', 'forceNetworkError', 'throttleKbps', 'delayMs'] export function validateStaticResponse (cmd: string, staticResponse: StaticResponse): void { const err = (message) => { $errUtils.throwErrByPath('net_stubbing.invalid_static_response', { args: { cmd, message, staticResponse } }) } - const { body, fixture, statusCode, headers, forceNetworkError, throttleKbps, delay, delayMs } = staticResponse + const { body, fixture, statusCode, headers, forceNetworkError, throttleKbps, delayMs } = staticResponse if (forceNetworkError && (body || statusCode || headers)) { err('`forceNetworkError`, if passed, must be the only option in the StaticResponse.') @@ -43,17 +43,9 @@ export function validateStaticResponse (cmd: string, staticResponse: StaticRespo err('`throttleKbps` must be a finite, positive number.') } - if (delayMs && delay) { - err('`delayMs` and `delay` cannot both be set.') - } - if (delayMs && (!_.isFinite(delayMs) || delayMs < 0)) { err('`delayMs` must be a finite, positive number.') } - - if (delay && (!_.isFinite(delay) || delay < 0)) { - err('`delay` must be a finite, positive number.') - } } export function parseStaticResponseShorthand (statusCodeOrBody: number | string | any, bodyOrHeaders: string | { [key: string]: string }, maybeHeaders?: { [key: string]: string }) { @@ -96,12 +88,7 @@ function getFixtureOpts (fixture: string): FixtureOpts { } export function getBackendStaticResponse (staticResponse: Readonly): BackendStaticResponse { - const backendStaticResponse: BackendStaticResponse = _.omit(staticResponse, 'body', 'fixture', 'delayMs') - - if (staticResponse.delayMs) { - // support deprecated `delayMs` usage - backendStaticResponse.delay = staticResponse.delayMs - } + const backendStaticResponse: BackendStaticResponse = _.omit(staticResponse, 'body', 'fixture') if (staticResponse.fixture) { backendStaticResponse.fixture = getFixtureOpts(staticResponse.fixture) diff --git a/packages/driver/src/cypress/events.ts b/packages/driver/src/cypress/events.ts index f33d1d9aff..5be0241eed 100644 --- a/packages/driver/src/cypress/events.ts +++ b/packages/driver/src/cypress/events.ts @@ -54,6 +54,7 @@ export function extend (obj): Events { // array of results return ret1.concat(ret2) case 'emitThen': + case 'emitThenSeries': return Bluebird.join(ret1, ret2, (a, a2) => { // array of results return a.concat(a2) @@ -87,6 +88,7 @@ export function extend (obj): Events { events.emitMap = map(_.map) events.emitThen = map(Bluebird.map) + events.emitThenSeries = map(Bluebird.mapSeries) // is our log enabled and have we not silenced // this specific object? diff --git a/packages/net-stubbing/lib/external-types.ts b/packages/net-stubbing/lib/external-types.ts index cb77458c36..081d85dcd3 100644 --- a/packages/net-stubbing/lib/external-types.ts +++ b/packages/net-stubbing/lib/external-types.ts @@ -68,7 +68,6 @@ type Method = | 'unlink' | 'unlock' | 'unsubscribe' - export namespace CyHttpMessages { export interface BaseMessage { body?: any @@ -81,6 +80,8 @@ export namespace CyHttpMessages { export type IncomingResponse = BaseMessage & { statusCode: number statusMessage: string + delayMs?: number + throttleKbps?: number } export type IncomingHttpResponse = IncomingResponse & { @@ -95,9 +96,9 @@ export namespace CyHttpMessages { */ send(): void /** - * Wait for `delay` milliseconds before sending the response to the client. + * Wait for `delayMs` milliseconds before sending the response to the client. */ - delay: (delay: number) => IncomingHttpResponse + delay: (delayMs: number) => IncomingHttpResponse /** * Serve the response at `throttleKbps` kilobytes per second. */ @@ -145,6 +146,10 @@ export namespace CyHttpMessages { */ redirect(location: string, statusCode?: number): void } + + export interface ResponseComplete { + error?: any + } } export interface DictMatcher { @@ -175,6 +180,16 @@ export type HttpResponseInterceptor = (res: CyHttpMessages.IncomingHttpResponse) */ export type NumberMatcher = number | number[] +/** + * Metadata for a subscription for an interception event. + */ +export interface Subscription { + id?: string + routeHandlerId: string + eventName: string + await: boolean +} + /** * Request/response cycle. */ @@ -182,7 +197,7 @@ export interface Interception { id: string routeHandlerId: string /* @internal */ - log: any + log?: any request: CyHttpMessages.IncomingRequest /** * Was `cy.wait()` used to wait on this request? @@ -203,6 +218,17 @@ export interface Interception { responseWaited: boolean /* @internal */ state: InterceptionState + /* @internal */ + subscriptions: Array<{ + subscription: Subscription + handler: (data: any) => Promise | void + }> + /* @internal */ + on(eventName: 'request', cb: () => void): Interception + /* @internal */ + on(eventName: 'before-response', cb: (res: CyHttpMessages.IncomingHttpResponse) => void): Interception + /* @internal */ + on(eventName: 'response', cb: (res: CyHttpMessages.IncomingHttpResponse) => void): Interception } export type InterceptionState = @@ -292,13 +318,7 @@ export type RouteHandler = string | StaticResponse | RouteHandlerController | ob /** * Describes a response that will be sent back to the browser to fulfill the request. */ -export type StaticResponse = GenericStaticResponse & { - /** - * Milliseconds to delay before the response is sent. - * @deprecated Use `delay` instead of `delayMs`. - */ - delayMs?: number -} +export type StaticResponse = GenericStaticResponse export interface GenericStaticResponse { /** @@ -332,7 +352,7 @@ export interface GenericStaticResponse { /** * Milliseconds to delay before the response is sent. */ - delay?: number + delayMs?: number } /** diff --git a/packages/net-stubbing/lib/internal-types.ts b/packages/net-stubbing/lib/internal-types.ts index 9de57f8d95..30aeff4c27 100644 --- a/packages/net-stubbing/lib/internal-types.ts +++ b/packages/net-stubbing/lib/internal-types.ts @@ -1,8 +1,8 @@ import * as _ from 'lodash' import { RouteMatcherOptionsGeneric, - CyHttpMessages, GenericStaticResponse, + Subscription, } from './external-types' export type FixtureOpts = { @@ -26,6 +26,8 @@ export const SERIALIZABLE_RES_PROPS = _.concat( SERIALIZABLE_REQ_PROPS, 'statusCode', 'statusMessage', + 'delayMs', + 'throttleKbps', ) export const DICT_STRING_MATCHER_FIELDS = ['headers', 'query'] @@ -45,56 +47,41 @@ export interface AnnotatedStringMatcher { */ export type AnnotatedRouteMatcherOptions = RouteMatcherOptionsGeneric -/** Types for messages between driver and server */ - -export declare namespace NetEventFrames { - export interface AddRoute { - routeMatcher: AnnotatedRouteMatcherOptions - staticResponse?: BackendStaticResponse - hasInterceptor: boolean - handlerId?: string - } - - interface BaseHttp { +export declare namespace NetEvent { + export interface Http { requestId: string routeHandlerId: string } - // fired when HTTP proxy receives headers + body of request - export interface HttpRequestReceived extends BaseHttp { - req: CyHttpMessages.IncomingRequest - /** - * Is the proxy expecting the driver to send `HttpRequestContinue`? - */ - notificationOnly: boolean + export namespace ToDriver { + export interface Event extends Http { + subscription: Subscription + eventId: string + data: D + } } - // fired when driver is done modifying request and wishes to pass control back to the proxy - export interface HttpRequestContinue extends BaseHttp { - req: CyHttpMessages.IncomingRequest - staticResponse?: BackendStaticResponse - hasResponseHandler?: boolean - tryNextRoute?: boolean - } + export namespace ToServer { + export interface AddRoute { + routeMatcher: AnnotatedRouteMatcherOptions + staticResponse?: BackendStaticResponse + hasInterceptor: boolean + handlerId?: string + } - // fired when a response is received and the driver has a req.reply callback registered - export interface HttpResponseReceived extends BaseHttp { - res: CyHttpMessages.IncomingResponse - } + export interface Subscribe { + requestId: string + subscription: Subscription + } - // fired when driver is done modifying response or driver callback completes, - // passes control back to proxy - export interface HttpResponseContinue extends BaseHttp { - res?: CyHttpMessages.IncomingResponse - staticResponse?: BackendStaticResponse - // Millisecond timestamp for when the response should continue - delay?: number - throttleKbps?: number - followRedirect?: boolean - } + export interface EventHandlerResolved { + eventId: string + changedData: any + } - // fired when a response has been sent completely by the server to an intercepted request - export interface HttpRequestComplete extends BaseHttp { - error?: Error & { code?: string } + export interface SendStaticResponse { + requestId: string + staticResponse: BackendStaticResponse + } } } diff --git a/packages/net-stubbing/lib/server/driver-events.ts b/packages/net-stubbing/lib/server/driver-events.ts index 6644bd2c18..b1e3e28473 100644 --- a/packages/net-stubbing/lib/server/driver-events.ts +++ b/packages/net-stubbing/lib/server/driver-events.ts @@ -7,20 +7,18 @@ import { } from './types' import { AnnotatedRouteMatcherOptions, - NetEventFrames, RouteMatcherOptions, + NetEvent, } from '../types' import { getAllStringMatcherFields, + sendStaticResponse as _sendStaticResponse, setResponseFromFixture, } from './util' -import { onRequestContinue } from './intercept-request' -import { onResponseContinue } from './intercept-response' -import CyServer from '@packages/server' const debug = Debug('cypress:net-stubbing:server:driver-events') -async function _onRouteAdded (state: NetStubbingState, getFixture: GetFixtureFn, options: NetEventFrames.AddRoute) { +async function onRouteAdded (state: NetStubbingState, getFixture: GetFixtureFn, options: NetEvent.ToServer.AddRoute) { const routeMatcher = _restoreMatcherOptionsTypes(options.routeMatcher) const { staticResponse } = options @@ -37,6 +35,51 @@ async function _onRouteAdded (state: NetStubbingState, getFixture: GetFixtureFn, state.routes.push(route) } +function getRequest (state: NetStubbingState, requestId: string) { + return Object.values(state.requests).find(({ id }) => { + return requestId === id + }) +} + +function subscribe (state: NetStubbingState, options: NetEvent.ToServer.Subscribe) { + const request = getRequest(state, options.requestId) + + if (!request) { + return + } + + // filter out any stub subscriptions that are no longer needed + _.remove(request.subscriptions, ({ eventName, routeHandlerId, id }) => { + return eventName === options.subscription.eventName && routeHandlerId === options.subscription.routeHandlerId && !id + }) + + request.subscriptions.push(options.subscription) +} + +function eventHandlerResolved (state: NetStubbingState, options: NetEvent.ToServer.EventHandlerResolved) { + const pendingEventHandler = state.pendingEventHandlers[options.eventId] + + if (!pendingEventHandler) { + return + } + + delete state.pendingEventHandlers[options.eventId] + + pendingEventHandler(options.changedData) +} + +async function sendStaticResponse (state: NetStubbingState, getFixture: GetFixtureFn, options: NetEvent.ToServer.SendStaticResponse) { + const request = getRequest(state, options.requestId) + + if (!request) { + return + } + + await setResponseFromFixture(getFixture, options.staticResponse) + + _sendStaticResponse(request, options.staticResponse) +} + export function _restoreMatcherOptionsTypes (options: AnnotatedRouteMatcherOptions) { const stringMatcherFields = getAllStringMatcherFields(options) @@ -72,24 +115,25 @@ export function _restoreMatcherOptionsTypes (options: AnnotatedRouteMatcherOptio type OnNetEventOpts = { eventName: string state: NetStubbingState - socket: CyServer.Socket getFixture: GetFixtureFn args: any[] - frame: NetEventFrames.AddRoute | NetEventFrames.HttpRequestContinue | NetEventFrames.HttpResponseContinue + frame: NetEvent.ToServer.AddRoute | NetEvent.ToServer.EventHandlerResolved | NetEvent.ToServer.Subscribe | NetEvent.ToServer.SendStaticResponse } export async function onNetEvent (opts: OnNetEventOpts): Promise { - const { state, socket, getFixture, args, eventName, frame } = opts + const { state, getFixture, args, eventName, frame } = opts debug('received driver event %o', { eventName, args }) switch (eventName) { case 'route:added': - return _onRouteAdded(state, getFixture, frame) - case 'http:request:continue': - return onRequestContinue(state, frame, socket) - case 'http:response:continue': - return onResponseContinue(state, frame) + return onRouteAdded(state, getFixture, frame) + case 'subscribe': + return subscribe(state, frame) + case 'event:handler:resolved': + return eventHandlerResolved(state, frame) + case 'send:static:response': + return sendStaticResponse(state, getFixture, frame) default: throw new Error(`Unrecognized net event: ${eventName}`) } diff --git a/packages/net-stubbing/lib/server/index.ts b/packages/net-stubbing/lib/server/index.ts index e119ef3d70..8a9f97277e 100644 --- a/packages/net-stubbing/lib/server/index.ts +++ b/packages/net-stubbing/lib/server/index.ts @@ -1,10 +1,10 @@ export { onNetEvent } from './driver-events' -export { InterceptError } from './intercept-error' +export { InterceptError } from './middleware/error' -export { InterceptRequest } from './intercept-request' +export { InterceptRequest } from './middleware/request' -export { InterceptResponse } from './intercept-response' +export { InterceptResponse } from './middleware/response' export { NetStubbingState } from './types' diff --git a/packages/net-stubbing/lib/server/intercept-error.ts b/packages/net-stubbing/lib/server/intercept-error.ts deleted file mode 100644 index 18397ec9f2..0000000000 --- a/packages/net-stubbing/lib/server/intercept-error.ts +++ /dev/null @@ -1,34 +0,0 @@ -import Debug from 'debug' - -import { ErrorMiddleware } from '@packages/proxy' -import { NetEventFrames } from '../types' -import { emit } from './util' -import errors from '@packages/server/lib/errors' - -const debug = Debug('cypress:net-stubbing:server:intercept-error') - -export const InterceptError: ErrorMiddleware = function () { - const backendRequest = this.netStubbingState.requests[this.req.requestId] - - if (!backendRequest) { - // the original request was not intercepted, nothing to do - return this.next() - } - - debug('intercepting error %o', { req: this.req, backendRequest }) - - // this may get set back to `true` by another route - backendRequest.waitForResponseContinue = false - backendRequest.continueResponse = this.next - - const frame: NetEventFrames.HttpRequestComplete = { - routeHandlerId: backendRequest.route.handlerId!, - requestId: backendRequest.requestId, - // @ts-ignore - false positive when running type-check? - error: errors.clone(this.error), - } - - emit(this.socket, 'http:request:complete', frame) - - this.next() -} diff --git a/packages/net-stubbing/lib/server/intercept-request.ts b/packages/net-stubbing/lib/server/intercept-request.ts deleted file mode 100644 index afb709b4fe..0000000000 --- a/packages/net-stubbing/lib/server/intercept-request.ts +++ /dev/null @@ -1,204 +0,0 @@ -import _ from 'lodash' -import { concatStream } from '@packages/network' -import Debug from 'debug' -import url from 'url' - -import { - CypressIncomingRequest, - RequestMiddleware, -} from '@packages/proxy' -import { - BackendRoute, - BackendRequest, - NetStubbingState, -} from './types' -import { - CyHttpMessages, - NetEventFrames, - SERIALIZABLE_REQ_PROPS, -} from '../types' -import { getRouteForRequest, matchesRoutePreflight } from './route-matching' -import { - sendStaticResponse, - emit, - setResponseFromFixture, - setDefaultHeaders, -} from './util' -import CyServer from '@packages/server' - -const debug = Debug('cypress:net-stubbing:server:intercept-request') - -/** - * Called when a new request is received in the proxy layer. - */ -export const InterceptRequest: RequestMiddleware = function () { - if (matchesRoutePreflight(this.netStubbingState.routes, this.req)) { - // send positive CORS preflight response - return sendStaticResponse(this, { - statusCode: 204, - headers: { - 'access-control-max-age': '-1', - 'access-control-allow-credentials': 'true', - 'access-control-allow-origin': this.req.headers.origin || '*', - 'access-control-allow-methods': this.req.headers['access-control-request-method'] || '*', - 'access-control-allow-headers': this.req.headers['access-control-request-headers'] || '*', - }, - }) - } - - const route = getRouteForRequest(this.netStubbingState.routes, this.req) - - if (!route) { - // not intercepted, carry on normally... - return this.next() - } - - const requestId = _.uniqueId('interceptedRequest') - - debug('intercepting request %o', { requestId, route, req: _.pick(this.req, 'url') }) - - const request: BackendRequest = { - requestId, - route, - continueRequest: this.next, - onError: this.onError, - onResponse: (incomingRes, resStream) => { - setDefaultHeaders(this.req, incomingRes) - this.onResponse(incomingRes, resStream) - }, - req: this.req, - res: this.res, - } - - // attach requestId to the original req object for later use - this.req.requestId = requestId - - this.netStubbingState.requests[requestId] = request - - _interceptRequest(this.netStubbingState, request, route, this.socket) -} - -function _interceptRequest (state: NetStubbingState, request: BackendRequest, route: BackendRoute, socket: CyServer.Socket) { - const notificationOnly = !route.hasInterceptor - - const frame: NetEventFrames.HttpRequestReceived = { - routeHandlerId: route.handlerId!, - requestId: request.req.requestId, - req: _.extend(_.pick(request.req, SERIALIZABLE_REQ_PROPS), { - url: request.req.proxiedUrl, - }) as CyHttpMessages.IncomingRequest, - notificationOnly, - } - - request.res.once('finish', () => { - emit(socket, 'http:request:complete', { - requestId: request.requestId, - routeHandlerId: route.handlerId!, - }) - - debug('request/response finished, cleaning up %o', { requestId: request.requestId }) - delete state.requests[request.requestId] - }) - - const emitReceived = () => { - emit(socket, 'http:request:received', frame) - } - - const ensureBody = (cb: () => void) => { - if (frame.req.body) { - return cb() - } - - request.req.pipe(concatStream((reqBody) => { - const contentType = frame.req.headers['content-type'] - const isMultipart = contentType && contentType.includes('multipart/form-data') - - request.req.body = frame.req.body = isMultipart ? reqBody : reqBody.toString() - cb() - })) - } - - if (route.staticResponse) { - const { staticResponse } = route - - return ensureBody(() => { - emitReceived() - sendStaticResponse(request, staticResponse) - }) - } - - if (notificationOnly) { - return ensureBody(() => { - emitReceived() - - const nextRoute = getNextRoute(state, request.req, frame.routeHandlerId) - - if (!nextRoute) { - return request.continueRequest() - } - - _interceptRequest(state, request, nextRoute, socket) - }) - } - - ensureBody(emitReceived) -} - -/** - * If applicable, return the route that is next in line after `prevRouteHandlerId` to handle `req`. - */ -function getNextRoute (state: NetStubbingState, req: CypressIncomingRequest, prevRouteHandlerId: string): BackendRoute | undefined { - const prevRoute = _.find(state.routes, { handlerId: prevRouteHandlerId }) - - if (!prevRoute) { - return - } - - return getRouteForRequest(state.routes, req, prevRoute) -} - -export async function onRequestContinue (state: NetStubbingState, frame: NetEventFrames.HttpRequestContinue, socket: CyServer.Socket) { - const backendRequest = state.requests[frame.requestId] - - if (!backendRequest) { - debug('onRequestContinue received but no backendRequest exists %o', { frame }) - - return - } - - frame.req.url = url.resolve(backendRequest.req.proxiedUrl, frame.req.url) - - // modify the original paused request object using what the client returned - _.assign(backendRequest.req, _.pick(frame.req, SERIALIZABLE_REQ_PROPS)) - - // proxiedUrl is used to initialize the new request - backendRequest.req.proxiedUrl = frame.req.url - - // update problematic headers - // update content-length if available - if (backendRequest.req.headers['content-length'] && frame.req.body != null) { - backendRequest.req.headers['content-length'] = Buffer.from(frame.req.body).byteLength.toString() - } - - if (frame.hasResponseHandler) { - backendRequest.waitForResponseContinue = true - } - - if (frame.tryNextRoute) { - const nextRoute = getNextRoute(state, backendRequest.req, frame.routeHandlerId) - - if (!nextRoute) { - return backendRequest.continueRequest() - } - - return _interceptRequest(state, backendRequest, nextRoute, socket) - } - - if (frame.staticResponse) { - await setResponseFromFixture(backendRequest.route.getFixture, frame.staticResponse) - - return sendStaticResponse(backendRequest, frame.staticResponse) - } - - backendRequest.continueRequest() -} diff --git a/packages/net-stubbing/lib/server/intercept-response.ts b/packages/net-stubbing/lib/server/intercept-response.ts deleted file mode 100644 index 79d2aaf087..0000000000 --- a/packages/net-stubbing/lib/server/intercept-response.ts +++ /dev/null @@ -1,123 +0,0 @@ -import _ from 'lodash' -import { concatStream, httpUtils } from '@packages/network' -import Debug from 'debug' -import { Readable, PassThrough } from 'stream' - -import { - ResponseMiddleware, -} from '@packages/proxy' -import { - NetStubbingState, -} from './types' -import { - CyHttpMessages, - NetEventFrames, - SERIALIZABLE_RES_PROPS, -} from '../types' -import { - emit, - sendStaticResponse, - setResponseFromFixture, - getBodyStream, -} from './util' - -const debug = Debug('cypress:net-stubbing:server:intercept-response') - -export const InterceptResponse: ResponseMiddleware = function () { - const backendRequest = this.netStubbingState.requests[this.req.requestId] - - debug('InterceptResponse %o', { req: _.pick(this.req, 'url'), backendRequest }) - - if (!backendRequest) { - // original request was not intercepted, nothing to do - return this.next() - } - - backendRequest.incomingRes = this.incomingRes - - backendRequest.onResponse = (incomingRes, resStream) => { - this.incomingRes = incomingRes - - backendRequest.continueResponse!(resStream) - } - - backendRequest.continueResponse = (newResStream?: Readable) => { - if (newResStream) { - this.incomingResStream = newResStream.on('error', this.onError) - } - - this.next() - } - - const frame: NetEventFrames.HttpResponseReceived = { - routeHandlerId: backendRequest.route.handlerId!, - requestId: backendRequest.requestId, - res: _.extend(_.pick(this.incomingRes, SERIALIZABLE_RES_PROPS), { - url: this.req.proxiedUrl, - }) as CyHttpMessages.IncomingResponse, - } - - const res = frame.res as CyHttpMessages.IncomingResponse - - const emitReceived = () => { - emit(this.socket, 'http:response:received', frame) - } - - this.makeResStreamPlainText() - - new Promise((resolve) => { - if (httpUtils.responseMustHaveEmptyBody(this.req, this.incomingRes)) { - resolve('') - } else { - this.incomingResStream.pipe(concatStream((resBody) => { - resolve(resBody) - })) - } - }) - .then((body) => { - const pt = this.incomingResStream = new PassThrough() - - pt.end(body) - - res.body = String(body) - emitReceived() - - if (!backendRequest.waitForResponseContinue) { - this.next() - } - - // this may get set back to `true` by another route - backendRequest.waitForResponseContinue = false - }) -} - -export async function onResponseContinue (state: NetStubbingState, frame: NetEventFrames.HttpResponseContinue) { - const backendRequest = state.requests[frame.requestId] - - if (typeof backendRequest === 'undefined') { - return - } - - const { res } = backendRequest - - debug('_onResponseContinue %o', { backendRequest: _.omit(backendRequest, 'res.body'), frame: _.omit(frame, 'res.body') }) - - const throttleKbps = _.get(frame, 'staticResponse.throttleKbps') || frame.throttleKbps - const delay = _.get(frame, 'staticResponse.delay') || frame.delay - - if (frame.staticResponse) { - // replacing response with a staticResponse - await setResponseFromFixture(backendRequest.route.getFixture, frame.staticResponse) - - const staticResponse = _.chain(frame.staticResponse).clone().assign({ delay, throttleKbps }).value() - - return sendStaticResponse(backendRequest, staticResponse) - } - - // merge the changed response attributes with our response and continue - _.assign(res, _.pick(frame.res, SERIALIZABLE_RES_PROPS)) - - const bodyStream = getBodyStream(res.body, { throttleKbps, delay }) - - return backendRequest.continueResponse!(bodyStream) -} diff --git a/packages/net-stubbing/lib/server/intercepted-request.ts b/packages/net-stubbing/lib/server/intercepted-request.ts new file mode 100644 index 0000000000..21d3af756e --- /dev/null +++ b/packages/net-stubbing/lib/server/intercepted-request.ts @@ -0,0 +1,121 @@ +import _ from 'lodash' +import { IncomingMessage } from 'http' +import { Readable } from 'stream' +import { + CypressIncomingRequest, + CypressOutgoingResponse, +} from '@packages/proxy' +import { + NetEvent, + Subscription, +} from '../types' +import { NetStubbingState } from './types' +import { emit } from './util' +import CyServer from '@packages/server' + +export class InterceptedRequest { + id: string + onError: (err: Error) => void + /** + * A callback that can be used to make the request go outbound through the rest of the request proxy steps. + */ + continueRequest: Function + /** + * Finish the current request with a response. + */ + onResponse: (incomingRes: IncomingMessage, resStream: Readable) => void + /** + * A callback that can be used to send the response through the rest of the response proxy steps. + */ + continueResponse?: (newResStream?: Readable) => void + req: CypressIncomingRequest + res: CypressOutgoingResponse + incomingRes?: IncomingMessage + subscriptions: Subscription[] + state: NetStubbingState + socket: CyServer.Socket + + constructor (opts: Pick) { + this.id = _.uniqueId('interceptedRequest') + this.req = opts.req + this.res = opts.res + this.continueRequest = opts.continueRequest + this.onError = opts.onError + this.onResponse = opts.onResponse + this.subscriptions = opts.subscriptions + this.state = opts.state + this.socket = opts.socket + } + + /* + * Run all subscriptions for an event in order, awaiting responses if applicable. + * Resolves with the updated object, or the original object if no changes have been made. + */ + async handleSubscriptions ({ eventName, data, mergeChanges }: { + eventName: string + data: D + /* + * Given a `before` snapshot and an `after` snapshot, calculate the modified object. + */ + mergeChanges: (before: D, after: D) => D + }): Promise { + const handleSubscription = async (subscription: Subscription) => { + const eventId = _.uniqueId('event') + const eventFrame: NetEvent.ToDriver.Event = { + eventId, + subscription, + requestId: this.id, + routeHandlerId: subscription.routeHandlerId, + data, + } + + const _emit = () => emit(this.socket, eventName, eventFrame) + + if (!subscription.await) { + _emit() + + return data + } + + const p = new Promise((resolve) => { + this.state.pendingEventHandlers[eventId] = resolve + }) + + _emit() + + const changedData = await p + + return mergeChanges(data, changedData as any) + } + + let lastI = -1 + + const getNextSubscription = () => { + return _.find(this.subscriptions, (v, i) => { + if (i > lastI && v.eventName === eventName) { + lastI = i + + return v + } + + return + }) as Subscription | undefined + } + + const run = async () => { + const subscription = getNextSubscription() + + if (!subscription) { + return + } + + data = await handleSubscription(subscription) + + await run() + } + + await run() + + return data + } +} diff --git a/packages/net-stubbing/lib/server/middleware/error.ts b/packages/net-stubbing/lib/server/middleware/error.ts new file mode 100644 index 0000000000..eb6aa140fd --- /dev/null +++ b/packages/net-stubbing/lib/server/middleware/error.ts @@ -0,0 +1,31 @@ +import Debug from 'debug' + +import { ErrorMiddleware } from '@packages/proxy' +import { CyHttpMessages } from '../../types' +import _ from 'lodash' +import errors from '@packages/server/lib/errors' + +const debug = Debug('cypress:net-stubbing:server:intercept-error') + +export const InterceptError: ErrorMiddleware = function () { + const request = this.netStubbingState.requests[this.req.requestId] + + if (!request) { + // the original request was not intercepted, nothing to do + return this.next() + } + + debug('intercepting error %o', { req: this.req, request }) + + request.continueResponse = this.next + + request.handleSubscriptions({ + eventName: 'after:response', + data: { + error: errors.clone(this.error), + }, + mergeChanges: _.identity, + }) + + this.next() +} diff --git a/packages/net-stubbing/lib/server/middleware/request.ts b/packages/net-stubbing/lib/server/middleware/request.ts new file mode 100644 index 0000000000..8173621615 --- /dev/null +++ b/packages/net-stubbing/lib/server/middleware/request.ts @@ -0,0 +1,169 @@ +import _ from 'lodash' +import { concatStream } from '@packages/network' +import Debug from 'debug' +import url from 'url' +import { getEncoding } from 'istextorbinary' + +import { + RequestMiddleware, +} from '@packages/proxy' +import { + CyHttpMessages, + SERIALIZABLE_REQ_PROPS, +} from '../../types' +import { getRouteForRequest, matchesRoutePreflight } from '../route-matching' +import { + sendStaticResponse, + setDefaultHeaders, +} from '../util' +import { Subscription } from '../../external-types' +import { InterceptedRequest } from '../intercepted-request' + +const debug = Debug('cypress:net-stubbing:server:intercept-request') + +/** + * Called when a new request is received in the proxy layer. + */ +export const InterceptRequest: RequestMiddleware = async function () { + if (matchesRoutePreflight(this.netStubbingState.routes, this.req)) { + // send positive CORS preflight response + return sendStaticResponse(this, { + statusCode: 204, + headers: { + 'access-control-max-age': '-1', + 'access-control-allow-credentials': 'true', + 'access-control-allow-origin': this.req.headers.origin || '*', + 'access-control-allow-methods': this.req.headers['access-control-request-method'] || '*', + 'access-control-allow-headers': this.req.headers['access-control-request-headers'] || '*', + }, + }) + } + + let lastRoute + const subscriptions: Subscription[] = [] + + const addDefaultSubscriptions = (prevRoute?) => { + const route = getRouteForRequest(this.netStubbingState.routes, this.req, prevRoute) + + if (!route) { + return + } + + Array.prototype.push.apply(subscriptions, [{ + eventName: 'before:request', + // req.reply callback? + await: !!route.hasInterceptor, + routeHandlerId: route.handlerId, + }, { + eventName: 'response', + // notification-only + await: false, + routeHandlerId: route.handlerId, + }, { + eventName: 'after:response', + // notification-only + await: false, + routeHandlerId: route.handlerId, + }]) + + lastRoute = route + + addDefaultSubscriptions(route) + } + + addDefaultSubscriptions() + + if (!subscriptions.length) { + // not intercepted, carry on normally... + return this.next() + } + + const request = new InterceptedRequest({ + continueRequest: this.next, + onError: this.onError, + onResponse: (incomingRes, resStream) => { + setDefaultHeaders(this.req, incomingRes) + this.onResponse(incomingRes, resStream) + }, + req: this.req, + res: this.res, + socket: this.socket, + state: this.netStubbingState, + subscriptions, + }) + + debug('intercepting request %o', { requestId: request.id, req: _.pick(this.req, 'url') }) + + // attach requestId to the original req object for later use + this.req.requestId = request.id + + this.netStubbingState.requests[request.id] = request + + const req = _.extend(_.pick(request.req, SERIALIZABLE_REQ_PROPS), { + url: request.req.proxiedUrl, + }) as CyHttpMessages.IncomingRequest + + request.res.once('finish', async () => { + request.handleSubscriptions({ + eventName: 'after:response', + data: {}, + mergeChanges: _.identity, + }) + + debug('request/response finished, cleaning up %o', { requestId: request.id }) + delete this.netStubbingState.requests[request.id] + }) + + const ensureBody = () => { + return new Promise((resolve) => { + if (req.body) { + return resolve() + } + + request.req.pipe(concatStream((reqBody) => { + req.body = reqBody + resolve() + })) + }) + } + + await ensureBody() + + if (!_.isString(req.body) && !_.isBuffer(req.body)) { + throw new Error('req.body must be a string or a Buffer') + } + + if (getEncoding(req.body) !== 'binary') { + req.body = req.body.toString('utf8') + } + + request.req.body = req.body + + const mergeChanges = (before: CyHttpMessages.IncomingRequest, after: CyHttpMessages.IncomingRequest) => { + if (before.headers['content-length'] === after.headers['content-length']) { + // user did not purposely override content-length, let's set it + after.headers['content-length'] = String(Buffer.from(after.body).byteLength) + } + + // resolve and propagate any changes to the URL + request.req.proxiedUrl = after.url = url.resolve(request.req.proxiedUrl, after.url) + + return _.merge(before, _.pick(after, SERIALIZABLE_REQ_PROPS)) + } + + const modifiedReq = await request.handleSubscriptions({ + eventName: 'before:request', + data: req, + mergeChanges, + }) + + if (lastRoute.staticResponse) { + return sendStaticResponse(request, lastRoute.staticResponse) + } + + mergeChanges(req, modifiedReq) + // @ts-ignore + mergeChanges(request.req, req) + + return request.continueRequest() +} diff --git a/packages/net-stubbing/lib/server/middleware/response.ts b/packages/net-stubbing/lib/server/middleware/response.ts new file mode 100644 index 0000000000..d9512bd078 --- /dev/null +++ b/packages/net-stubbing/lib/server/middleware/response.ts @@ -0,0 +1,81 @@ +import _ from 'lodash' +import { concatStream, httpUtils } from '@packages/network' +import Debug from 'debug' +import { Readable } from 'stream' +import { getEncoding } from 'istextorbinary' + +import { + ResponseMiddleware, +} from '@packages/proxy' +import { + CyHttpMessages, + SERIALIZABLE_RES_PROPS, +} from '../../types' +import { + getBodyStream, +} from '../util' + +const debug = Debug('cypress:net-stubbing:server:intercept-response') + +export const InterceptResponse: ResponseMiddleware = async function () { + const request = this.netStubbingState.requests[this.req.requestId] + + debug('InterceptResponse %o', { req: _.pick(this.req, 'url'), request }) + + if (!request) { + // original request was not intercepted, nothing to do + return this.next() + } + + request.incomingRes = this.incomingRes + + request.onResponse = (incomingRes, resStream) => { + this.incomingRes = incomingRes + + request.continueResponse!(resStream) + } + + request.continueResponse = (newResStream?: Readable) => { + if (newResStream) { + this.incomingResStream = newResStream.on('error', this.onError) + } + + this.next() + } + + this.makeResStreamPlainText() + + const body: Buffer | string = await new Promise((resolve) => { + if (httpUtils.responseMustHaveEmptyBody(this.req, this.incomingRes)) { + resolve(Buffer.from('')) + } else { + this.incomingResStream.pipe(concatStream(resolve)) + } + }) + .then((buf) => { + return getEncoding(buf) !== 'binary' ? buf.toString('utf8') : buf + }) + + const res = _.extend(_.pick(this.incomingRes, SERIALIZABLE_RES_PROPS), { + url: this.req.proxiedUrl, + body, + }) as CyHttpMessages.IncomingResponse + + if (!_.isString(res.body) && !_.isBuffer(res.body)) { + throw new Error('res.body must be a string or a Buffer') + } + + const modifiedRes = await request.handleSubscriptions({ + eventName: 'response', + data: res, + mergeChanges: (before, after) => { + return _.merge(before, _.pick(after, SERIALIZABLE_RES_PROPS)) + }, + }) + + _.merge(request.res, modifiedRes) + + const bodyStream = getBodyStream(modifiedRes.body, _.pick(modifiedRes, ['throttleKbps', 'delayMs']) as any) + + return request.continueResponse!(bodyStream) +} diff --git a/packages/net-stubbing/lib/server/route-matching.ts b/packages/net-stubbing/lib/server/route-matching.ts index 203d78a050..5cf12ef1f5 100644 --- a/packages/net-stubbing/lib/server/route-matching.ts +++ b/packages/net-stubbing/lib/server/route-matching.ts @@ -124,6 +124,9 @@ export function _getMatchableForRequest (req: CypressIncomingRequest) { return matchable } +/** + * Try to match a `BackendRoute` to a request, optionally starting after `prevRoute`. + */ export function getRouteForRequest (routes: BackendRoute[], req: CypressIncomingRequest, prevRoute?: BackendRoute) { const possibleRoutes = prevRoute ? routes.slice(_.findIndex(routes, prevRoute) + 1) : routes diff --git a/packages/net-stubbing/lib/server/state.ts b/packages/net-stubbing/lib/server/state.ts index b5a6eefd00..b29b7cb35b 100644 --- a/packages/net-stubbing/lib/server/state.ts +++ b/packages/net-stubbing/lib/server/state.ts @@ -5,6 +5,7 @@ export function state (): NetStubbingState { return { requests: {}, routes: [], + pendingEventHandlers: {}, reset () { // clean up requests that are still pending for (const requestId in this.requests) { @@ -16,6 +17,7 @@ export function state (): NetStubbingState { res.destroy() } + this.pendingEventHandlers = {} this.requests = {} this.routes = [] }, diff --git a/packages/net-stubbing/lib/server/types.ts b/packages/net-stubbing/lib/server/types.ts index c5c82fd4d2..c49f5ab2a3 100644 --- a/packages/net-stubbing/lib/server/types.ts +++ b/packages/net-stubbing/lib/server/types.ts @@ -1,13 +1,10 @@ -import { IncomingMessage } from 'http' -import { Readable } from 'stream' -import { - CypressIncomingRequest, - CypressOutgoingResponse, -} from '@packages/proxy' import { RouteMatcherOptions, BackendStaticResponse, } from '../types' +import { + InterceptedRequest, +} from './intercepted-request' export type GetFixtureFn = (path: string, opts?: { encoding?: string | null }) => Promise @@ -19,35 +16,12 @@ export interface BackendRoute { getFixture: GetFixtureFn } -export interface BackendRequest { - requestId: string - /** - * The route that matched this request. - */ - route: BackendRoute - onError: (err: Error) => void - /** - * A callback that can be used to make the request go outbound. - */ - continueRequest: Function - /** - * A callback that can be used to send the response through the proxy. - */ - continueResponse?: (newResStream?: Readable) => void - onResponse?: (incomingRes: IncomingMessage, resStream: Readable) => void - req: CypressIncomingRequest - res: CypressOutgoingResponse - incomingRes?: IncomingMessage - /** - * Should we wait for the driver to allow the response to continue? - */ - waitForResponseContinue?: boolean -} - export interface NetStubbingState { - // map of request IDs to requests in flight + pendingEventHandlers: { + [eventId: string]: Function + } requests: { - [requestId: string]: BackendRequest + [requestId: string]: InterceptedRequest } routes: BackendRoute[] reset: () => void diff --git a/packages/net-stubbing/lib/server/util.ts b/packages/net-stubbing/lib/server/util.ts index f427ee1856..456e1bc5cb 100644 --- a/packages/net-stubbing/lib/server/util.ts +++ b/packages/net-stubbing/lib/server/util.ts @@ -11,13 +11,14 @@ import { import { Readable, PassThrough } from 'stream' import CyServer from '@packages/server' import { Socket } from 'net' -import { GetFixtureFn, BackendRequest } from './types' +import { GetFixtureFn } from './types' import ThrottleStream from 'throttle' import MimeTypes from 'mime-types' +import { CypressIncomingRequest } from '@packages/proxy' +import { InterceptedRequest } from './intercepted-request' // TODO: move this into net-stubbing once cy.route is removed import { parseContentType } from '@packages/server/lib/controllers/xhrs' -import { CypressIncomingRequest } from '@packages/proxy' const debug = Debug('cypress:net-stubbing:server:util') @@ -145,7 +146,7 @@ export async function setResponseFromFixture (getFixtureFn: GetFixtureFn, static * @param backendRequest BackendRequest object. * @param staticResponse BackendStaticResponse object. */ -export function sendStaticResponse (backendRequest: Pick, staticResponse: BackendStaticResponse) { +export function sendStaticResponse (backendRequest: Pick, staticResponse: BackendStaticResponse) { const { onError, onResponse } = backendRequest if (staticResponse.forceNetworkError) { @@ -165,13 +166,13 @@ export function sendStaticResponse (backendRequest: Pick { @@ -195,7 +196,7 @@ export function getBodyStream (body: Buffer | string | Readable | undefined, opt return writable.end() } - delay ? setTimeout(sendBody, delay) : sendBody() + delayMs ? setTimeout(sendBody, delayMs) : sendBody() return pt } diff --git a/packages/net-stubbing/package.json b/packages/net-stubbing/package.json index 2daeb7d772..7ad8d30500 100644 --- a/packages/net-stubbing/package.json +++ b/packages/net-stubbing/package.json @@ -11,6 +11,7 @@ "dependencies": { "@types/mime-types": "2.1.0", "is-html": "^2.0.0", + "istextorbinary": "5.12.0", "lodash": "4.17.15", "mime-types": "2.1.27", "minimatch": "^3.0.4", diff --git a/packages/proxy/test/integration/net-stubbing.spec.ts b/packages/proxy/test/integration/net-stubbing.spec.ts index ff26d8f964..bc2fba14e6 100644 --- a/packages/proxy/test/integration/net-stubbing.spec.ts +++ b/packages/proxy/test/integration/net-stubbing.spec.ts @@ -165,21 +165,18 @@ context('network stubbing', () => { }) socket.toDriver.callsFake((_, event, data) => { - if (event === 'http:request:received') { + if (event === 'before:request') { onNetEvent({ - eventName: 'http:request:continue', + eventName: 'send:static:response', + // @ts-ignore frame: { - routeHandlerId: '1', requestId: data.requestId, - req: data.req, staticResponse: { + ...data.data, body: 'replaced', }, - hasResponseHandler: false, - tryNextRoute: false, }, state: netStubbingState, - socket, getFixture, args: [], }) @@ -189,49 +186,19 @@ context('network stubbing', () => { return supertest(app) .get(`/http://localhost:${destinationPort}`) .then((res) => { + expect(res.text).to.eq('replaced') expect(res.headers).to.include({ 'access-control-allow-origin': '*', }) - - expect(res.text).to.eq('replaced') }) }) - it('does not modify multipart/form-data files', () => { + it('does not modify multipart/form-data files', async () => { + const png = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==', 'base64') let sendContentLength = '' let receivedContentLength = '' let realContentLength = '' - netStubbingState.routes.push({ - handlerId: '1', - routeMatcher: { - url: '*', - }, - hasInterceptor: true, - getFixture, - }) - - socket.toDriver.callsFake((_, event, data) => { - if (event === 'http:request:received') { - sendContentLength = data.req.headers['content-length'] - onNetEvent({ - eventName: 'http:request:continue', - frame: { - routeHandlerId: '1', - requestId: data.requestId, - req: data.req, - res: data.res, - hasResponseHandler: false, - tryNextRoute: false, - }, - state: netStubbingState, - socket, - getFixture, - args: [], - }) - } - }) - destinationApp.post('/', (req, res) => { const chunks = [] @@ -250,12 +217,45 @@ context('network stubbing', () => { }) }) - return supertest(app) + // capture unintercepted content-length + await supertest(app) .post(`/http://localhost:${destinationPort}`) - .attach('file', Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==', 'base64')) // 1 pixel png image - .then(() => { - expect(sendContentLength).to.eq(receivedContentLength) - expect(sendContentLength).to.eq(realContentLength) + .attach('file', png) + + netStubbingState.routes.push({ + handlerId: '1', + routeMatcher: { + url: '*', + }, + hasInterceptor: true, + getFixture, }) + + socket.toDriver.callsFake((_, event, data) => { + if (event === 'before:request') { + sendContentLength = data.data.headers['content-length'] + onNetEvent({ + eventName: 'send:static:response', + // @ts-ignore + frame: { + requestId: data.requestId, + staticResponse: { + ...data.data, + }, + }, + state: netStubbingState, + getFixture, + args: [], + }) + } + }) + + // capture content-length after intercepting + await supertest(app) + .post(`/http://localhost:${destinationPort}`) + .attach('file', png) + + expect(sendContentLength).to.eq(receivedContentLength) + expect(sendContentLength).to.eq(realContentLength) }) }) diff --git a/packages/runner/cypress/fixtures/errors/intercept_spec.ts b/packages/runner/cypress/fixtures/errors/intercept_spec.ts new file mode 100644 index 0000000000..8092bc8e08 --- /dev/null +++ b/packages/runner/cypress/fixtures/errors/intercept_spec.ts @@ -0,0 +1,43 @@ +import { SinonStub } from "sinon" + +describe('cy.intercept', () => { + const { $, sinon } = Cypress + + it('fails in req callback', () => { + cy.intercept('/json-content-type', () => { + expect('a').to.eq('b') + }) + .then(() => { + console.log('hi2') + Cypress.emit('net:event', 'before:request', { + eventId: '1', + // @ts-ignore + routeHandlerId: Object.keys(Cypress.state('routes'))[0], + subscription: { + await: true, + }, + data: {} + }) + const { $ } = Cypress + $.get('/json-content-type') + }) + }) + + it('fails in res callback', () => { + cy.intercept('/json-content-type', (req) => { + req.reply(() => { + expect('b').to.eq('c') + }) + }) + .then(() => $.get('/json-content-type')) + }) + + it('fails when erroneous response is received while awaiting response', () => { + cy.intercept('/fake', (req) => { + req.reply(() => { + expect('this should not be reached').to.eq('d') + }) + }) + .then(() => $.get('http://foo.invalid/fake')) + }) +}) \ No newline at end of file diff --git a/packages/runner/cypress/integration/reporter.errors.spec.js b/packages/runner/cypress/integration/reporter.errors.spec.js index 4b306fccfa..7848866c62 100644 --- a/packages/runner/cypress/integration/reporter.errors.spec.js +++ b/packages/runner/cypress/integration/reporter.errors.spec.js @@ -275,6 +275,26 @@ describe('errors ui', () => { }) }) + // FIXME: these cy.fail errors are propagating to window.top + describe.skip('cy.intercept', () => { + const file = 'intercept_spec.ts' + + verify.it('fails in req callback', { + file, + message: 'A request callback passed to cy.intercept() threw an error while intercepting a request', + }) + + verify.it('fails in res callback', { + file, + column: 1, + }) + + verify.it('fails when erroneous response is received while awaiting response', { + file, + column: 1, + }) + }) + describe('cy.route', () => { const file = 'route_spec.js' diff --git a/packages/runner/cypress/support/helpers.js b/packages/runner/cypress/support/helpers.js index fa4670f204..500f0934cb 100644 --- a/packages/runner/cypress/support/helpers.js +++ b/packages/runner/cypress/support/helpers.js @@ -250,6 +250,9 @@ function createCypress (defaultOptions = {}) { cb(opts.state) }) + .withArgs('backend:request', 'net') + .yieldsAsync({}) + .withArgs('backend:request', 'reset:server:state') .yieldsAsync({}) diff --git a/packages/server/lib/socket-base.ts b/packages/server/lib/socket-base.ts index 78ba2ba694..8e8a04f0ef 100644 --- a/packages/server/lib/socket-base.ts +++ b/packages/server/lib/socket-base.ts @@ -377,7 +377,6 @@ export class SocketBase { eventName: args[0], frame: args[1], state: options.netStubbingState, - socket: this, getFixture, args, }) diff --git a/yarn.lock b/yarn.lock index ec732ca0cc..5f1b88c41c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2916,7 +2916,7 @@ "@jest/types@^26.3.0", "@jest/types@^26.6.2": version "26.6.2" - resolved "https://registry.yarnpkg.com/@jest/types/-/types-26.6.2.tgz#bef5a532030e1d88a2f5a6d933f84e97226ed48e" + resolved "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz#bef5a532030e1d88a2f5a6d933f84e97226ed48e" integrity sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ== dependencies: "@types/istanbul-lib-coverage" "^2.0.0" @@ -5412,7 +5412,7 @@ "@types/cheerio@*", "@types/cheerio@0.22.21": version "0.22.21" - resolved "https://registry.yarnpkg.com/@types/cheerio/-/cheerio-0.22.21.tgz#5e37887de309ba11b2e19a6e14cad7874b31a8a3" + resolved "https://registry.npmjs.org/@types/cheerio/-/cheerio-0.22.21.tgz#5e37887de309ba11b2e19a6e14cad7874b31a8a3" integrity sha512-aGI3DfswwqgKPiEOTaiHV2ZPC9KEhprpgEbJnv0fZl3SGX0cGgEva1126dGrMC6AJM6v/aihlUgJn9M5DbDZ/Q== dependencies: "@types/node" "*" @@ -5507,7 +5507,7 @@ "@types/enzyme@*", "@types/enzyme@3.10.5": version "3.10.5" - resolved "https://registry.yarnpkg.com/@types/enzyme/-/enzyme-3.10.5.tgz#fe7eeba3550369eed20e7fb565bfb74eec44f1f0" + resolved "https://registry.npmjs.org/@types/enzyme/-/enzyme-3.10.5.tgz#fe7eeba3550369eed20e7fb565bfb74eec44f1f0" integrity sha512-R+phe509UuUYy9Tk0YlSbipRpfVtIzb/9BHn5pTEtjJTF5LXvUjrIQcZvNyANNEyFrd2YGs196PniNT1fgvOQA== dependencies: "@types/cheerio" "*" @@ -9428,6 +9428,11 @@ binary-extensions@^2.0.0: resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== +binaryextensions@^4.15.0: + version "4.15.0" + resolved "https://registry.npmjs.org/binaryextensions/-/binaryextensions-4.15.0.tgz#c63a502e0078ff1b0e9b00a9f74d3c2b0f8bd32e" + integrity sha512-MkUl3szxXolQ2scI1PM14WOT951KnaTNJ0eMKg7WzOI4kvSxyNo/Cygx4LOBNhwyINhAuSQpJW1rYD9aBSxGaw== + bindings@^1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df" @@ -13148,7 +13153,7 @@ debug@^3.0.0, debug@^3.0.1, debug@^3.1.0, debug@^3.1.1, debug@^3.2.5, debug@^3.2 dependencies: ms "^2.1.1" -debuglog@*, debuglog@^1.0.1: +debuglog@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492" integrity sha1-qiT/uaw9+aI1GDfPstJ5NgzXhJI= @@ -14191,6 +14196,14 @@ ecstatic@^3.3.2: minimist "^1.1.0" url-join "^2.0.5" +editions@^6.1.0: + version "6.1.0" + resolved "https://registry.npmjs.org/editions/-/editions-6.1.0.tgz#ba6c6cf9f4bb571d9e53ea34e771a602e5a66549" + integrity sha512-h6nWEyIocfgho9J3sTSuhU/WoFOu1hTX75rPBebNrbF38Y9QFDjCDizYXdikHTySW7Y3mSxli8bpDz9RAtc7rA== + dependencies: + errlop "^4.0.0" + version-range "^1.0.0" + editor@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/editor/-/editor-1.0.0.tgz#60c7f87bd62bcc6a894fa8ccd6afb7823a24f742" @@ -14638,6 +14651,11 @@ err-code@^1.0.0: resolved "https://registry.yarnpkg.com/err-code/-/err-code-1.1.2.tgz#06e0116d3028f6aef4806849eb0ea6a748ae6960" integrity sha1-BuARbTAo9q70gGhJ6w6mp0iuaWA= +errlop@^4.0.0: + version "4.1.0" + resolved "https://registry.npmjs.org/errlop/-/errlop-4.1.0.tgz#8e7b8f4f1bf0a6feafce4d14f0c0cf4bf5ef036b" + integrity sha512-vul6gGBuVt0M2TPi1/WrcL86+Hb3Q2Tpu3TME3sbVhZrYf7J1ZMHCodI25RQKCVurh56qTfvgM0p3w5cT4reSQ== + errno@^0.1.3, errno@~0.1.7: version "0.1.8" resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.8.tgz#8bb3e9c7d463be4976ff888f76b4809ebc2e811f" @@ -19107,7 +19125,7 @@ import-local@2.0.0, import-local@^2.0.0: pkg-dir "^3.0.0" resolve-cwd "^2.0.0" -imurmurhash@*, imurmurhash@^0.1.4: +imurmurhash@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= @@ -20362,6 +20380,15 @@ istanbul@0.4.5: which "^1.1.1" wordwrap "^1.0.0" +istextorbinary@5.12.0: + version "5.12.0" + resolved "https://registry.npmjs.org/istextorbinary/-/istextorbinary-5.12.0.tgz#2f84777838668fdf524c305a2363d6057aaeec84" + integrity sha512-wLDRWD7qpNTYubk04+q3en1+XZGS4vYWK0+SxNSXJLaITMMEK+J3o/TlOMyULeH1qozVZ9uUkKcyMA8odyxz8w== + dependencies: + binaryextensions "^4.15.0" + editions "^6.1.0" + textextensions "^5.11.0" + isurl@^1.0.0-alpha5: version "1.0.0" resolved "https://registry.yarnpkg.com/isurl/-/isurl-1.0.0.tgz#b27f4f49f3cdaa3ea44a0a5b7f3462e6edc39d67" @@ -22309,11 +22336,6 @@ lodash._basecreate@^3.0.0: resolved "https://registry.yarnpkg.com/lodash._basecreate/-/lodash._basecreate-3.0.3.tgz#1bc661614daa7fc311b7d03bf16806a0213cf821" integrity sha1-G8ZhYU2qf8MRt9A78WgGoCE8+CE= -lodash._baseindexof@*: - version "3.1.0" - resolved "https://registry.yarnpkg.com/lodash._baseindexof/-/lodash._baseindexof-3.1.0.tgz#fe52b53a1c6761e42618d654e4a25789ed61822c" - integrity sha1-/lK1OhxnYeQmGNZU5KJXie1hgiw= - lodash._baseuniq@~4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/lodash._baseuniq/-/lodash._baseuniq-4.6.0.tgz#0ebb44e456814af7905c6212fa2c9b2d51b841e8" @@ -22322,29 +22344,12 @@ lodash._baseuniq@~4.6.0: lodash._createset "~4.0.0" lodash._root "~3.0.0" -lodash._bindcallback@*: - version "3.0.1" - resolved "https://registry.yarnpkg.com/lodash._bindcallback/-/lodash._bindcallback-3.0.1.tgz#e531c27644cf8b57a99e17ed95b35c748789392e" - integrity sha1-5THCdkTPi1epnhftlbNcdIeJOS4= - -lodash._cacheindexof@*: - version "3.0.2" - resolved "https://registry.yarnpkg.com/lodash._cacheindexof/-/lodash._cacheindexof-3.0.2.tgz#3dc69ac82498d2ee5e3ce56091bafd2adc7bde92" - integrity sha1-PcaayCSY0u5ePOVgkbr9Ktx73pI= - -lodash._createcache@*: - version "3.1.2" - resolved "https://registry.yarnpkg.com/lodash._createcache/-/lodash._createcache-3.1.2.tgz#56d6a064017625e79ebca6b8018e17440bdcf093" - integrity sha1-VtagZAF2JeeevKa4AY4XRAvc8JM= - dependencies: - lodash._getnative "^3.0.0" - lodash._createset@~4.0.0: version "4.0.3" resolved "https://registry.yarnpkg.com/lodash._createset/-/lodash._createset-4.0.3.tgz#0f4659fbb09d75194fa9e2b88a6644d363c9fe26" integrity sha1-D0ZZ+7CddRlPqeK4imZE02PJ/iY= -lodash._getnative@*, lodash._getnative@^3.0.0: +lodash._getnative@^3.0.0: version "3.9.1" resolved "https://registry.yarnpkg.com/lodash._getnative/-/lodash._getnative-3.9.1.tgz#570bc7dede46d61cdcde687d65d3eecbaa3aaff5" integrity sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U= @@ -22547,11 +22552,6 @@ lodash.reduce@^4.6.0: resolved "https://registry.yarnpkg.com/lodash.reduce/-/lodash.reduce-4.6.0.tgz#f1ab6b839299ad48f784abbf476596f03b914d3b" integrity sha1-8atrg5KZrUj3hKu/R2WW8DuRTTs= -lodash.restparam@*: - version "3.6.1" - resolved "https://registry.yarnpkg.com/lodash.restparam/-/lodash.restparam-3.6.1.tgz#936a4e309ef330a7645ed4145986c85ae5b20805" - integrity sha1-k2pOMJ7zMKdkXtQUWYbIWuWyCAU= - lodash.set@4.3.2, lodash.set@^4.3.2: version "4.3.2" resolved "https://registry.yarnpkg.com/lodash.set/-/lodash.set-4.3.2.tgz#d8757b1da807dde24816b0d6a84bea1a76230b23" @@ -27737,7 +27737,7 @@ pretty-error@^2.0.2, pretty-error@^2.1.1: pretty-format@26.4.0, pretty-format@^23.0.1, pretty-format@^24.9.0, pretty-format@^26.6.2: version "26.4.0" - resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-26.4.0.tgz#c08073f531429e9e5024049446f42ecc9f933a3b" + resolved "https://registry.npmjs.org/pretty-format/-/pretty-format-26.4.0.tgz#c08073f531429e9e5024049446f42ecc9f933a3b" integrity sha512-mEEwwpCseqrUtuMbrJG4b824877pM5xald3AkilJ47Po2YLr97/siejYQHqj2oDQBeJNbu+Q0qUuekJ8F0NAPg== dependencies: "@jest/types" "^26.3.0" @@ -31373,7 +31373,7 @@ socket.io-client@3.0.4: socket.io-parser@4.0.2, socket.io-parser@~3.3.0, socket.io-parser@~3.4.0, socket.io-parser@~4.0.1: version "4.0.2" - resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.0.2.tgz#3d021a9c86671bb079e7c6c806db6a1d9b1bc780" + resolved "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.0.2.tgz#3d021a9c86671bb079e7c6c806db6a1d9b1bc780" integrity sha512-Bs3IYHDivwf+bAAuW/8xwJgIiBNtlvnjYRc4PbXgniLmcP1BrakBoq/QhO24rgtgW7VZ7uAaswRGxutUnlAK7g== dependencies: "@types/component-emitter" "^1.2.10" @@ -33035,6 +33035,11 @@ text-table@0.2.0, text-table@^0.2.0, text-table@~0.2.0: resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ= +textextensions@^5.11.0: + version "5.12.0" + resolved "https://registry.npmjs.org/textextensions/-/textextensions-5.12.0.tgz#b908120b5c1bd4bb9eba41423d75b176011ab68a" + integrity sha512-IYogUDaP65IXboCiPPC0jTLLBzYlhhw2Y4b0a2trPgbHNGGGEfuHE6tds+yDcCf4mpNDaGISFzwSSezcXt+d6w== + thenify-all@^1.0.0: version "1.6.0" resolved "https://registry.yarnpkg.com/thenify-all/-/thenify-all-1.6.0.tgz#1a1918d402d8fc3f98fbf234db0bcc8cc10e9726" @@ -34532,6 +34537,18 @@ verror@1.10.0: core-util-is "1.0.2" extsprintf "^1.2.0" +version-compare@^1.0.0: + version "1.1.0" + resolved "https://registry.npmjs.org/version-compare/-/version-compare-1.1.0.tgz#7b3e67e7e6cec5c72d9c9e586f8854e419ade17c" + integrity sha512-zVKtPOJTC9x23lzS4+4D7J+drq80BXVYAmObnr5zqxxFVH7OffJ1lJlAS7LYsQNV56jx/wtbw0UV7XHLrvd6kQ== + +version-range@^1.0.0: + version "1.1.0" + resolved "https://registry.npmjs.org/version-range/-/version-range-1.1.0.tgz#1c233064202ee742afc9d56e21da3b2e15260acf" + integrity sha512-R1Ggfg2EXamrnrV3TkZ6yBNgITDbclB3viwSjbZ3+eK0VVNK4ajkYJTnDz5N0bIMYDtK9MUBvXJUnKO5RWWJ6w== + dependencies: + version-compare "^1.0.0" + victory-area@^34.3.6: version "34.3.12" resolved "https://registry.yarnpkg.com/victory-area/-/victory-area-34.3.12.tgz#875e261aa67079fb0898c58848561578a5dac6f6" From bc4d5ea7f26d226a6aa9f8465dad95a0625ed0fd Mon Sep 17 00:00:00 2001 From: Zach Bloomquist Date: Tue, 9 Mar 2021 16:25:55 +0000 Subject: [PATCH 015/134] Delete NOTES.md --- NOTES.md | 9 --------- 1 file changed, 9 deletions(-) delete mode 100644 NOTES.md diff --git a/NOTES.md b/NOTES.md deleted file mode 100644 index ddc59e7215..0000000000 --- a/NOTES.md +++ /dev/null @@ -1,9 +0,0 @@ -* New `RouteMatcher` option: `middleware: boolean` - * With `middleware: true`, will be called in the order they are defined and chained. - * With `middleware: true`, only dynamic handlers are supported - makes no sense to support `cy.intercept({ middleware: true }, staticResponse)` - * BREAKING CHANGE: `middleware: falsy` handlers will not be chained. For any given request, the most-recently-defined handler is always the one used. -* `req` is now an `EventEmitter` (regardless of `middleware` setting) -* Events on `req`: - * `request - ()` - Request will be sent outgoing. If the response has already been fulfilled by `req.reply`, this event will not be emitted. - * `before-response - (res)` - Response was received. Emitted before the response handler is run. - * `response - (res)` - Response will be sent to the browser. If a response has been supplied via `res.send`, this event will not be emitted. \ No newline at end of file From c4497752d87470b26a9c94f3c6dc60e993e4a83e Mon Sep 17 00:00:00 2001 From: Chris Breiding Date: Wed, 10 Mar 2021 10:01:05 -0500 Subject: [PATCH 016/134] feat: Target modern ES with default preprocessor (#15274) --- .../index.js | 5 +- .../cypress/integration/retries.ui.spec.js | 7 +- .../__snapshots__/5_spec_isolation_spec.js | 174 +++++++++--------- .../server/__snapshots__/7_record_spec.js | 8 +- packages/server/lib/browsers/chrome.ts | 3 + .../test/integration/http_requests_spec.js | 24 ++- .../projects/ids/cypress/integration/es6.js | 6 + .../server/expected_ids_all_tests_iframe.html | 2 +- packages/web-config/webpack.config.base.ts | 5 +- yarn.lock | 33 +--- 10 files changed, 138 insertions(+), 129 deletions(-) create mode 100644 packages/server/test/support/fixtures/projects/ids/cypress/integration/es6.js diff --git a/npm/webpack-batteries-included-preprocessor/index.js b/npm/webpack-batteries-included-preprocessor/index.js index 202edaf77e..c8b9bb1342 100644 --- a/npm/webpack-batteries-included-preprocessor/index.js +++ b/npm/webpack-batteries-included-preprocessor/index.js @@ -33,7 +33,10 @@ const getDefaultWebpackOptions = (file, options = {}) => { }], ], presets: [ - [require.resolve('@babel/preset-env'), { modules: 'commonjs' }], + // the chrome version should be synced with + // packages/web-config/webpack.config.base.ts and + // packages/server/lib/browsers/chrome.ts + [require.resolve('@babel/preset-env'), { modules: 'commonjs', targets: { 'chrome': '64' } }], require.resolve('@babel/preset-react'), ], }, diff --git a/packages/runner/cypress/integration/retries.ui.spec.js b/packages/runner/cypress/integration/retries.ui.spec.js index 275b95ffb2..264b98e612 100644 --- a/packages/runner/cypress/integration/retries.ui.spec.js +++ b/packages/runner/cypress/integration/retries.ui.spec.js @@ -433,7 +433,7 @@ describe('runner/cypress retries.ui.spec', { viewportWidth: 600, viewportHeight: it('throws when set via this.retries in test', () => { runIsolatedCypress({ suites: { - 'suite 1' () { + 'suite 1': () => { it('tries to set mocha retries', function () { this.retries(null) }) @@ -451,7 +451,7 @@ describe('runner/cypress retries.ui.spec', { viewportWidth: 600, viewportHeight: it('throws when set via this.retries in hook', () => { runIsolatedCypress({ suites: { - 'suite 1' () { + 'suite 1': () => { beforeEach(function () { this.retries(0) }) @@ -471,7 +471,8 @@ describe('runner/cypress retries.ui.spec', { viewportWidth: 600, viewportHeight: it('throws when set via this.retries in suite', () => { runIsolatedCypress({ suites: { - 'suite 1' () { + // eslint-disable-next-line object-shorthand + 'suite 1': function () { this.retries(3) it('test 1', () => { }) diff --git a/packages/server/__snapshots__/5_spec_isolation_spec.js b/packages/server/__snapshots__/5_spec_isolation_spec.js index 6d1c6a44d7..1f52c3dd0f 100644 --- a/packages/server/__snapshots__/5_spec_isolation_spec.js +++ b/packages/server/__snapshots__/5_spec_isolation_spec.js @@ -197,21 +197,21 @@ exports['e2e spec_isolation fails [electron] 1'] = { "title": [ "\"before each\" hook" ], - "body": "function () {\n throw new Error('fail1');\n }" + "body": "() => {\n throw new Error('fail1');\n }" }, { "hookName": "after each", "title": [ "\"after each\" hook" ], - "body": "function () {\n throw new Error('fail2');\n }" + "body": "() => {\n throw new Error('fail2');\n }" }, { "hookName": "after all", "title": [ "\"after all\" hook" ], - "body": "function () {\n throw new Error('fail3');\n }" + "body": "() => {\n throw new Error('fail3');\n }" } ], "tests": [ @@ -222,7 +222,7 @@ exports['e2e spec_isolation fails [electron] 1'] = { "never gets here" ], "state": "failed", - "body": "function () {}", + "body": "() => {}", "displayError": "Error: fail1\n\nBecause this error occurred during a `before each` hook we are skipping the remaining tests in the current suite: `beforeEach hooks`\n [stack trace lines]", "attempts": [ { @@ -283,7 +283,7 @@ exports['e2e spec_isolation fails [electron] 1'] = { "runs this" ], "state": "failed", - "body": "function () {}", + "body": "() => {}", "displayError": "Error: fail2\n\nBecause this error occurred during a `after each` hook we are skipping the remaining tests in the current suite: `afterEach hooks`\n [stack trace lines]", "attempts": [ { @@ -324,7 +324,7 @@ exports['e2e spec_isolation fails [electron] 1'] = { "does not run this" ], "state": "skipped", - "body": "function () {}", + "body": "() => {}", "displayError": null, "attempts": [ { @@ -344,7 +344,7 @@ exports['e2e spec_isolation fails [electron] 1'] = { "runs this" ], "state": "passed", - "body": "function () {}", + "body": "() => {}", "displayError": null, "attempts": [ { @@ -364,7 +364,7 @@ exports['e2e spec_isolation fails [electron] 1'] = { "fails on this" ], "state": "failed", - "body": "function () {}", + "body": "() => {}", "displayError": "Error: fail3\n\nBecause this error occurred during a `after all` hook we are skipping the remaining tests in the current suite: `after hooks`\n [stack trace lines]", "attempts": [ { @@ -440,7 +440,7 @@ exports['e2e spec_isolation fails [electron] 1'] = { "fails1" ], "state": "failed", - "body": "function () {\n cy.wrap(true, {\n timeout: 100\n }).should('be.false');\n }", + "body": "() => {\n cy.wrap(true, {\n timeout: 100\n }).should('be.false');\n }", "displayError": "AssertionError: Timed out retrying after 100ms: expected true to be false\n [stack trace lines]", "attempts": [ { @@ -480,7 +480,7 @@ exports['e2e spec_isolation fails [electron] 1'] = { "fails2" ], "state": "failed", - "body": "function () {\n throw new Error('fails2');\n }", + "body": "() => {\n throw new Error('fails2');\n }", "displayError": "Error: fails2\n [stack trace lines]", "attempts": [ { @@ -554,28 +554,28 @@ exports['e2e spec_isolation fails [electron] 1'] = { "title": [ "\"before all\" hook" ], - "body": "function () {\n cy.wait(100);\n }" + "body": "() => {\n cy.wait(100);\n }" }, { "hookName": "before each", "title": [ "\"before each\" hook" ], - "body": "function () {\n cy.wait(200);\n }" + "body": "() => {\n cy.wait(200);\n }" }, { "hookName": "after each", "title": [ "\"after each\" hook" ], - "body": "function () {\n cy.wait(200);\n }" + "body": "() => {\n cy.wait(200);\n }" }, { "hookName": "after all", "title": [ "\"after all\" hook" ], - "body": "function () {\n cy.wait(100);\n }" + "body": "() => {\n cy.wait(100);\n }" } ], "tests": [ @@ -585,7 +585,7 @@ exports['e2e spec_isolation fails [electron] 1'] = { "t1" ], "state": "passed", - "body": "function () {\n cy.wrap('t1').should('eq', 't1');\n }", + "body": "() => {\n cy.wrap('t1').should('eq', 't1');\n }", "displayError": null, "attempts": [ { @@ -604,7 +604,7 @@ exports['e2e spec_isolation fails [electron] 1'] = { "t2" ], "state": "passed", - "body": "function () {\n cy.wrap('t2').should('eq', 't2');\n }", + "body": "() => {\n cy.wrap('t2').should('eq', 't2');\n }", "displayError": null, "attempts": [ { @@ -623,7 +623,7 @@ exports['e2e spec_isolation fails [electron] 1'] = { "t3" ], "state": "passed", - "body": "function () {\n cy.wrap('t3').should('eq', 't3');\n }", + "body": "() => {\n cy.wrap('t3').should('eq', 't3');\n }", "displayError": null, "attempts": [ { @@ -676,7 +676,7 @@ exports['e2e spec_isolation fails [electron] 1'] = { "title": [ "\"before each\" hook" ], - "body": "function () {\n cy.wait(1000);\n }" + "body": "() => {\n cy.wait(1000);\n }" } ], "tests": [ @@ -686,7 +686,7 @@ exports['e2e spec_isolation fails [electron] 1'] = { "passes" ], "state": "passed", - "body": "function () {\n cy.wrap(true).should('be.true');\n }", + "body": "() => {\n cy.wrap(true).should('be.true');\n }", "displayError": null, "attempts": [ { @@ -761,21 +761,21 @@ exports['e2e spec_isolation fails [chrome] 1'] = { "title": [ "\"before each\" hook" ], - "body": "function () {\n throw new Error('fail1');\n }" + "body": "() => {\n throw new Error('fail1');\n }" }, { "hookName": "after each", "title": [ "\"after each\" hook" ], - "body": "function () {\n throw new Error('fail2');\n }" + "body": "() => {\n throw new Error('fail2');\n }" }, { "hookName": "after all", "title": [ "\"after all\" hook" ], - "body": "function () {\n throw new Error('fail3');\n }" + "body": "() => {\n throw new Error('fail3');\n }" } ], "tests": [ @@ -786,7 +786,7 @@ exports['e2e spec_isolation fails [chrome] 1'] = { "never gets here" ], "state": "failed", - "body": "function () {}", + "body": "() => {}", "displayError": "Error: fail1\n\nBecause this error occurred during a `before each` hook we are skipping the remaining tests in the current suite: `beforeEach hooks`\n [stack trace lines]", "attempts": [ { @@ -847,7 +847,7 @@ exports['e2e spec_isolation fails [chrome] 1'] = { "runs this" ], "state": "failed", - "body": "function () {}", + "body": "() => {}", "displayError": "Error: fail2\n\nBecause this error occurred during a `after each` hook we are skipping the remaining tests in the current suite: `afterEach hooks`\n [stack trace lines]", "attempts": [ { @@ -888,7 +888,7 @@ exports['e2e spec_isolation fails [chrome] 1'] = { "does not run this" ], "state": "skipped", - "body": "function () {}", + "body": "() => {}", "displayError": null, "attempts": [ { @@ -908,7 +908,7 @@ exports['e2e spec_isolation fails [chrome] 1'] = { "runs this" ], "state": "passed", - "body": "function () {}", + "body": "() => {}", "displayError": null, "attempts": [ { @@ -928,7 +928,7 @@ exports['e2e spec_isolation fails [chrome] 1'] = { "fails on this" ], "state": "failed", - "body": "function () {}", + "body": "() => {}", "displayError": "Error: fail3\n\nBecause this error occurred during a `after all` hook we are skipping the remaining tests in the current suite: `after hooks`\n [stack trace lines]", "attempts": [ { @@ -1004,7 +1004,7 @@ exports['e2e spec_isolation fails [chrome] 1'] = { "fails1" ], "state": "failed", - "body": "function () {\n cy.wrap(true, {\n timeout: 100\n }).should('be.false');\n }", + "body": "() => {\n cy.wrap(true, {\n timeout: 100\n }).should('be.false');\n }", "displayError": "AssertionError: Timed out retrying after 100ms: expected true to be false\n [stack trace lines]", "attempts": [ { @@ -1044,7 +1044,7 @@ exports['e2e spec_isolation fails [chrome] 1'] = { "fails2" ], "state": "failed", - "body": "function () {\n throw new Error('fails2');\n }", + "body": "() => {\n throw new Error('fails2');\n }", "displayError": "Error: fails2\n [stack trace lines]", "attempts": [ { @@ -1118,28 +1118,28 @@ exports['e2e spec_isolation fails [chrome] 1'] = { "title": [ "\"before all\" hook" ], - "body": "function () {\n cy.wait(100);\n }" + "body": "() => {\n cy.wait(100);\n }" }, { "hookName": "before each", "title": [ "\"before each\" hook" ], - "body": "function () {\n cy.wait(200);\n }" + "body": "() => {\n cy.wait(200);\n }" }, { "hookName": "after each", "title": [ "\"after each\" hook" ], - "body": "function () {\n cy.wait(200);\n }" + "body": "() => {\n cy.wait(200);\n }" }, { "hookName": "after all", "title": [ "\"after all\" hook" ], - "body": "function () {\n cy.wait(100);\n }" + "body": "() => {\n cy.wait(100);\n }" } ], "tests": [ @@ -1149,7 +1149,7 @@ exports['e2e spec_isolation fails [chrome] 1'] = { "t1" ], "state": "passed", - "body": "function () {\n cy.wrap('t1').should('eq', 't1');\n }", + "body": "() => {\n cy.wrap('t1').should('eq', 't1');\n }", "displayError": null, "attempts": [ { @@ -1168,7 +1168,7 @@ exports['e2e spec_isolation fails [chrome] 1'] = { "t2" ], "state": "passed", - "body": "function () {\n cy.wrap('t2').should('eq', 't2');\n }", + "body": "() => {\n cy.wrap('t2').should('eq', 't2');\n }", "displayError": null, "attempts": [ { @@ -1187,7 +1187,7 @@ exports['e2e spec_isolation fails [chrome] 1'] = { "t3" ], "state": "passed", - "body": "function () {\n cy.wrap('t3').should('eq', 't3');\n }", + "body": "() => {\n cy.wrap('t3').should('eq', 't3');\n }", "displayError": null, "attempts": [ { @@ -1240,7 +1240,7 @@ exports['e2e spec_isolation fails [chrome] 1'] = { "title": [ "\"before each\" hook" ], - "body": "function () {\n cy.wait(1000);\n }" + "body": "() => {\n cy.wait(1000);\n }" } ], "tests": [ @@ -1250,7 +1250,7 @@ exports['e2e spec_isolation fails [chrome] 1'] = { "passes" ], "state": "passed", - "body": "function () {\n cy.wrap(true).should('be.true');\n }", + "body": "() => {\n cy.wrap(true).should('be.true');\n }", "displayError": null, "attempts": [ { @@ -1325,21 +1325,21 @@ exports['e2e spec_isolation fails [firefox] 1'] = { "title": [ "\"before each\" hook" ], - "body": "function () {\n throw new Error('fail1');\n }" + "body": "() => {\n throw new Error('fail1');\n }" }, { "hookName": "after each", "title": [ "\"after each\" hook" ], - "body": "function () {\n throw new Error('fail2');\n }" + "body": "() => {\n throw new Error('fail2');\n }" }, { "hookName": "after all", "title": [ "\"after all\" hook" ], - "body": "function () {\n throw new Error('fail3');\n }" + "body": "() => {\n throw new Error('fail3');\n }" } ], "tests": [ @@ -1350,7 +1350,7 @@ exports['e2e spec_isolation fails [firefox] 1'] = { "never gets here" ], "state": "failed", - "body": "function () {}", + "body": "() => {}", "displayError": "Error: fail1\n\nBecause this error occurred during a `before each` hook we are skipping the remaining tests in the current suite: `beforeEach hooks`\n [stack trace lines]", "attempts": [ { @@ -1411,7 +1411,7 @@ exports['e2e spec_isolation fails [firefox] 1'] = { "runs this" ], "state": "failed", - "body": "function () {}", + "body": "() => {}", "displayError": "Error: fail2\n\nBecause this error occurred during a `after each` hook we are skipping the remaining tests in the current suite: `afterEach hooks`\n [stack trace lines]", "attempts": [ { @@ -1452,7 +1452,7 @@ exports['e2e spec_isolation fails [firefox] 1'] = { "does not run this" ], "state": "skipped", - "body": "function () {}", + "body": "() => {}", "displayError": null, "attempts": [ { @@ -1472,7 +1472,7 @@ exports['e2e spec_isolation fails [firefox] 1'] = { "runs this" ], "state": "passed", - "body": "function () {}", + "body": "() => {}", "displayError": null, "attempts": [ { @@ -1492,7 +1492,7 @@ exports['e2e spec_isolation fails [firefox] 1'] = { "fails on this" ], "state": "failed", - "body": "function () {}", + "body": "() => {}", "displayError": "Error: fail3\n\nBecause this error occurred during a `after all` hook we are skipping the remaining tests in the current suite: `after hooks`\n [stack trace lines]", "attempts": [ { @@ -1568,7 +1568,7 @@ exports['e2e spec_isolation fails [firefox] 1'] = { "fails1" ], "state": "failed", - "body": "function () {\n cy.wrap(true, {\n timeout: 100\n }).should('be.false');\n }", + "body": "() => {\n cy.wrap(true, {\n timeout: 100\n }).should('be.false');\n }", "displayError": "AssertionError: Timed out retrying after 100ms: expected true to be false\n [stack trace lines]", "attempts": [ { @@ -1608,7 +1608,7 @@ exports['e2e spec_isolation fails [firefox] 1'] = { "fails2" ], "state": "failed", - "body": "function () {\n throw new Error('fails2');\n }", + "body": "() => {\n throw new Error('fails2');\n }", "displayError": "Error: fails2\n [stack trace lines]", "attempts": [ { @@ -1682,28 +1682,28 @@ exports['e2e spec_isolation fails [firefox] 1'] = { "title": [ "\"before all\" hook" ], - "body": "function () {\n cy.wait(100);\n }" + "body": "() => {\n cy.wait(100);\n }" }, { "hookName": "before each", "title": [ "\"before each\" hook" ], - "body": "function () {\n cy.wait(200);\n }" + "body": "() => {\n cy.wait(200);\n }" }, { "hookName": "after each", "title": [ "\"after each\" hook" ], - "body": "function () {\n cy.wait(200);\n }" + "body": "() => {\n cy.wait(200);\n }" }, { "hookName": "after all", "title": [ "\"after all\" hook" ], - "body": "function () {\n cy.wait(100);\n }" + "body": "() => {\n cy.wait(100);\n }" } ], "tests": [ @@ -1713,7 +1713,7 @@ exports['e2e spec_isolation fails [firefox] 1'] = { "t1" ], "state": "passed", - "body": "function () {\n cy.wrap('t1').should('eq', 't1');\n }", + "body": "() => {\n cy.wrap('t1').should('eq', 't1');\n }", "displayError": null, "attempts": [ { @@ -1732,7 +1732,7 @@ exports['e2e spec_isolation fails [firefox] 1'] = { "t2" ], "state": "passed", - "body": "function () {\n cy.wrap('t2').should('eq', 't2');\n }", + "body": "() => {\n cy.wrap('t2').should('eq', 't2');\n }", "displayError": null, "attempts": [ { @@ -1751,7 +1751,7 @@ exports['e2e spec_isolation fails [firefox] 1'] = { "t3" ], "state": "passed", - "body": "function () {\n cy.wrap('t3').should('eq', 't3');\n }", + "body": "() => {\n cy.wrap('t3').should('eq', 't3');\n }", "displayError": null, "attempts": [ { @@ -1804,7 +1804,7 @@ exports['e2e spec_isolation fails [firefox] 1'] = { "title": [ "\"before each\" hook" ], - "body": "function () {\n cy.wait(1000);\n }" + "body": "() => {\n cy.wait(1000);\n }" } ], "tests": [ @@ -1814,7 +1814,7 @@ exports['e2e spec_isolation fails [firefox] 1'] = { "passes" ], "state": "passed", - "body": "function () {\n cy.wrap(true).should('be.true');\n }", + "body": "() => {\n cy.wrap(true).should('be.true');\n }", "displayError": null, "attempts": [ { @@ -1889,21 +1889,21 @@ exports['e2e spec_isolation failing with retries enabled [electron] 1'] = { "title": [ "\"before each\" hook" ], - "body": "function () {\n throw new Error('fail1');\n }" + "body": "() => {\n throw new Error('fail1');\n }" }, { "hookName": "after each", "title": [ "\"after each\" hook" ], - "body": "function () {\n throw new Error('fail2');\n }" + "body": "() => {\n throw new Error('fail2');\n }" }, { "hookName": "after all", "title": [ "\"after all\" hook" ], - "body": "function () {\n throw new Error('fail3');\n }" + "body": "() => {\n throw new Error('fail3');\n }" } ], "tests": [ @@ -1914,7 +1914,7 @@ exports['e2e spec_isolation failing with retries enabled [electron] 1'] = { "never gets here" ], "state": "failed", - "body": "function () {}", + "body": "() => {}", "displayError": "Error: fail1\n\nBecause this error occurred during a `before each` hook we are skipping the remaining tests in the current suite: `beforeEach hooks`\n [stack trace lines]", "attempts": [ { @@ -2004,7 +2004,7 @@ exports['e2e spec_isolation failing with retries enabled [electron] 1'] = { "runs this" ], "state": "failed", - "body": "function () {}", + "body": "() => {}", "displayError": "Error: fail2\n\nBecause this error occurred during a `after each` hook we are skipping the remaining tests in the current suite: `afterEach hooks`\n [stack trace lines]", "attempts": [ { @@ -2074,7 +2074,7 @@ exports['e2e spec_isolation failing with retries enabled [electron] 1'] = { "does not run this" ], "state": "skipped", - "body": "function () {}", + "body": "() => {}", "displayError": null, "attempts": [ { @@ -2094,7 +2094,7 @@ exports['e2e spec_isolation failing with retries enabled [electron] 1'] = { "runs this" ], "state": "passed", - "body": "function () {}", + "body": "() => {}", "displayError": null, "attempts": [ { @@ -2114,7 +2114,7 @@ exports['e2e spec_isolation failing with retries enabled [electron] 1'] = { "fails on this" ], "state": "failed", - "body": "function () {}", + "body": "() => {}", "displayError": "Error: fail3\n\nBecause this error occurred during a `after all` hook we are skipping the remaining tests in the current suite: `after hooks`\n\nAlthough you have test retries enabled, we do not retry tests when `before all` or `after all` hooks fail\n [stack trace lines]", "attempts": [ { @@ -2190,7 +2190,7 @@ exports['e2e spec_isolation failing with retries enabled [electron] 1'] = { "t1" ], "state": "failed", - "body": "function () {\n var test = cy.state('test');\n throw new Error(\"\".concat(test.title, \" attempt #\").concat(cy.state('test').currentRetry()));\n }", + "body": "() => {\n const test = cy.state('test');\n throw new Error(`${test.title} attempt #${cy.state('test').currentRetry()}`);\n }", "displayError": "Error: t1 attempt #1\n [stack trace lines]", "attempts": [ { @@ -2259,7 +2259,7 @@ exports['e2e spec_isolation failing with retries enabled [electron] 1'] = { "t2" ], "state": "passed", - "body": "function () {// pass\n }", + "body": "() => {// pass\n }", "displayError": null, "attempts": [ { @@ -2334,21 +2334,21 @@ exports['e2e spec_isolation failing with retries enabled [chrome] 1'] = { "title": [ "\"before each\" hook" ], - "body": "function () {\n throw new Error('fail1');\n }" + "body": "() => {\n throw new Error('fail1');\n }" }, { "hookName": "after each", "title": [ "\"after each\" hook" ], - "body": "function () {\n throw new Error('fail2');\n }" + "body": "() => {\n throw new Error('fail2');\n }" }, { "hookName": "after all", "title": [ "\"after all\" hook" ], - "body": "function () {\n throw new Error('fail3');\n }" + "body": "() => {\n throw new Error('fail3');\n }" } ], "tests": [ @@ -2359,7 +2359,7 @@ exports['e2e spec_isolation failing with retries enabled [chrome] 1'] = { "never gets here" ], "state": "failed", - "body": "function () {}", + "body": "() => {}", "displayError": "Error: fail1\n\nBecause this error occurred during a `before each` hook we are skipping the remaining tests in the current suite: `beforeEach hooks`\n [stack trace lines]", "attempts": [ { @@ -2449,7 +2449,7 @@ exports['e2e spec_isolation failing with retries enabled [chrome] 1'] = { "runs this" ], "state": "failed", - "body": "function () {}", + "body": "() => {}", "displayError": "Error: fail2\n\nBecause this error occurred during a `after each` hook we are skipping the remaining tests in the current suite: `afterEach hooks`\n [stack trace lines]", "attempts": [ { @@ -2519,7 +2519,7 @@ exports['e2e spec_isolation failing with retries enabled [chrome] 1'] = { "does not run this" ], "state": "skipped", - "body": "function () {}", + "body": "() => {}", "displayError": null, "attempts": [ { @@ -2539,7 +2539,7 @@ exports['e2e spec_isolation failing with retries enabled [chrome] 1'] = { "runs this" ], "state": "passed", - "body": "function () {}", + "body": "() => {}", "displayError": null, "attempts": [ { @@ -2559,7 +2559,7 @@ exports['e2e spec_isolation failing with retries enabled [chrome] 1'] = { "fails on this" ], "state": "failed", - "body": "function () {}", + "body": "() => {}", "displayError": "Error: fail3\n\nBecause this error occurred during a `after all` hook we are skipping the remaining tests in the current suite: `after hooks`\n\nAlthough you have test retries enabled, we do not retry tests when `before all` or `after all` hooks fail\n [stack trace lines]", "attempts": [ { @@ -2635,7 +2635,7 @@ exports['e2e spec_isolation failing with retries enabled [chrome] 1'] = { "t1" ], "state": "failed", - "body": "function () {\n var test = cy.state('test');\n throw new Error(\"\".concat(test.title, \" attempt #\").concat(cy.state('test').currentRetry()));\n }", + "body": "() => {\n const test = cy.state('test');\n throw new Error(`${test.title} attempt #${cy.state('test').currentRetry()}`);\n }", "displayError": "Error: t1 attempt #1\n [stack trace lines]", "attempts": [ { @@ -2704,7 +2704,7 @@ exports['e2e spec_isolation failing with retries enabled [chrome] 1'] = { "t2" ], "state": "passed", - "body": "function () {// pass\n }", + "body": "() => {// pass\n }", "displayError": null, "attempts": [ { @@ -2779,21 +2779,21 @@ exports['e2e spec_isolation failing with retries enabled [firefox] 1'] = { "title": [ "\"before each\" hook" ], - "body": "function () {\n throw new Error('fail1');\n }" + "body": "() => {\n throw new Error('fail1');\n }" }, { "hookName": "after each", "title": [ "\"after each\" hook" ], - "body": "function () {\n throw new Error('fail2');\n }" + "body": "() => {\n throw new Error('fail2');\n }" }, { "hookName": "after all", "title": [ "\"after all\" hook" ], - "body": "function () {\n throw new Error('fail3');\n }" + "body": "() => {\n throw new Error('fail3');\n }" } ], "tests": [ @@ -2804,7 +2804,7 @@ exports['e2e spec_isolation failing with retries enabled [firefox] 1'] = { "never gets here" ], "state": "failed", - "body": "function () {}", + "body": "() => {}", "displayError": "Error: fail1\n\nBecause this error occurred during a `before each` hook we are skipping the remaining tests in the current suite: `beforeEach hooks`\n [stack trace lines]", "attempts": [ { @@ -2894,7 +2894,7 @@ exports['e2e spec_isolation failing with retries enabled [firefox] 1'] = { "runs this" ], "state": "failed", - "body": "function () {}", + "body": "() => {}", "displayError": "Error: fail2\n\nBecause this error occurred during a `after each` hook we are skipping the remaining tests in the current suite: `afterEach hooks`\n [stack trace lines]", "attempts": [ { @@ -2964,7 +2964,7 @@ exports['e2e spec_isolation failing with retries enabled [firefox] 1'] = { "does not run this" ], "state": "skipped", - "body": "function () {}", + "body": "() => {}", "displayError": null, "attempts": [ { @@ -2984,7 +2984,7 @@ exports['e2e spec_isolation failing with retries enabled [firefox] 1'] = { "runs this" ], "state": "passed", - "body": "function () {}", + "body": "() => {}", "displayError": null, "attempts": [ { @@ -3004,7 +3004,7 @@ exports['e2e spec_isolation failing with retries enabled [firefox] 1'] = { "fails on this" ], "state": "failed", - "body": "function () {}", + "body": "() => {}", "displayError": "Error: fail3\n\nBecause this error occurred during a `after all` hook we are skipping the remaining tests in the current suite: `after hooks`\n\nAlthough you have test retries enabled, we do not retry tests when `before all` or `after all` hooks fail\n [stack trace lines]", "attempts": [ { @@ -3080,7 +3080,7 @@ exports['e2e spec_isolation failing with retries enabled [firefox] 1'] = { "t1" ], "state": "failed", - "body": "function () {\n var test = cy.state('test');\n throw new Error(\"\".concat(test.title, \" attempt #\").concat(cy.state('test').currentRetry()));\n }", + "body": "() => {\n const test = cy.state('test');\n throw new Error(`${test.title} attempt #${cy.state('test').currentRetry()}`);\n }", "displayError": "Error: t1 attempt #1\n [stack trace lines]", "attempts": [ { @@ -3149,7 +3149,7 @@ exports['e2e spec_isolation failing with retries enabled [firefox] 1'] = { "t2" ], "state": "passed", - "body": "function () {// pass\n }", + "body": "() => {// pass\n }", "displayError": null, "attempts": [ { diff --git a/packages/server/__snapshots__/7_record_spec.js b/packages/server/__snapshots__/7_record_spec.js index ba2569c336..9725db2b94 100644 --- a/packages/server/__snapshots__/7_record_spec.js +++ b/packages/server/__snapshots__/7_record_spec.js @@ -2191,7 +2191,7 @@ exports['e2e record passing passes 2'] = [ "fails 1" ], "state": "failed", - "body": "function () {}", + "body": "() => {}", "displayError": "Error: foo\n\nBecause this error occurred during a `before each` hook we are skipping the remaining tests in the current suite: `record fails`\n [stack trace lines]", "attempts": [ { @@ -2234,7 +2234,7 @@ exports['e2e record passing passes 2'] = [ "is skipped" ], "state": "skipped", - "body": "function () {}", + "body": "() => {}", "displayError": null, "attempts": [ { @@ -2258,7 +2258,7 @@ exports['e2e record passing passes 2'] = [ "title": [ "\"before each\" hook" ], - "body": "function () {\n throw new Error('foo');\n }" + "body": "() => {\n throw new Error('foo');\n }" } ], "screenshots": [ @@ -2304,7 +2304,7 @@ exports['e2e record passing passes 2'] = [ "passes" ], "state": "passed", - "body": "function () {\n cy.visit('/scrollable.html');\n cy.viewport(400, 400);\n cy.get('#box');\n cy.screenshot('yay it passes');\n }", + "body": "() => {\n cy.visit('/scrollable.html');\n cy.viewport(400, 400);\n cy.get('#box');\n cy.screenshot('yay it passes');\n }", "displayError": null, "attempts": [ { diff --git a/packages/server/lib/browsers/chrome.ts b/packages/server/lib/browsers/chrome.ts index 4e792c511b..0ce0408706 100644 --- a/packages/server/lib/browsers/chrome.ts +++ b/packages/server/lib/browsers/chrome.ts @@ -506,6 +506,9 @@ export = { await criClient.ensureMinimumProtocolVersion('1.3') .catch((err) => { + // if this minumum chrome version changes, sync it with + // packages/web-config/webpack.config.base.ts and + // npm/webpack-batteries-included-preprocessor/index.js throw new Error(`Cypress requires at least Chrome 64.\n\nDetails:\n${err.message}`) }) diff --git a/packages/server/test/integration/http_requests_spec.js b/packages/server/test/integration/http_requests_spec.js index ecf035d35b..374f95bc6a 100644 --- a/packages/server/test/integration/http_requests_spec.js +++ b/packages/server/test/integration/http_requests_spec.js @@ -483,7 +483,7 @@ describe('Routes', () => { body, } = res - expect(body.integration).to.have.length(6) + expect(body.integration).to.have.length(7) // remove the absolute path key body.integration = _.map(body.integration, (obj) => { @@ -504,6 +504,10 @@ describe('Routes', () => { name: 'dom.jsx', relative: 'cypress/integration/dom.jsx', }, + { + name: 'es6.js', + relative: 'cypress/integration/es6.js', + }, { name: 'foo.coffee', relative: 'cypress/integration/foo.coffee', @@ -541,7 +545,7 @@ describe('Routes', () => { body, } = res - expect(body.integration).to.have.length(3) + expect(body.integration).to.have.length(4) // remove the absolute path key body.integration = _.map(body.integration, (obj) => { @@ -558,6 +562,10 @@ describe('Routes', () => { name: 'dom.jsx', relative: 'cypress/integration/dom.jsx', }, + { + name: 'es6.js', + relative: 'cypress/integration/es6.js', + }, { name: 'noop.coffee', relative: 'cypress/integration/noop.coffee', @@ -596,6 +604,18 @@ describe('Routes', () => { }) }) + it('processes spec into modern javascript', function () { + return this.rp('http://localhost:2020/__cypress/tests?p=cypress/integration/es6.js') + .then((res) => { + expect(res.statusCode).to.eq(200) + // "modern" features should remain and not be transpiled into es5 + expect(res.body).to.include('const numbers') + expect(res.body).to.include('[...numbers]') + expect(res.body).to.include('async function') + expect(res.body).to.include('await Promise') + }) + }) + it('serves error javascript file when the file is missing', function () { return this.rp('http://localhost:2020/__cypress/tests?p=does/not/exist.coffee') .then((res) => { diff --git a/packages/server/test/support/fixtures/projects/ids/cypress/integration/es6.js b/packages/server/test/support/fixtures/projects/ids/cypress/integration/es6.js new file mode 100644 index 0000000000..8f87716fd0 --- /dev/null +++ b/packages/server/test/support/fixtures/projects/ids/cypress/integration/es6.js @@ -0,0 +1,6 @@ +const numbers = [1, 2, 3] +const sameNumbers = [...numbers] + +async function resolvePromise () { + await Promise.resolve('foo') +} diff --git a/packages/server/test/support/fixtures/server/expected_ids_all_tests_iframe.html b/packages/server/test/support/fixtures/server/expected_ids_all_tests_iframe.html index 13766e186c..69d1fdc78e 100644 --- a/packages/server/test/support/fixtures/server/expected_ids_all_tests_iframe.html +++ b/packages/server/test/support/fixtures/server/expected_ids_all_tests_iframe.html @@ -13,7 +13,7 @@ if (!Cypress) { throw new Error("Tests cannot run without a reference to Cypress!"); } - return Cypress.onSpecWindow(window, [{"absolute":"//ids/cypress/support/index.js","relative":"cypress/support/index.js","relativeUrl":"/__cypress/tests?p=cypress/support/index.js"},{"absolute":"//ids/cypress/integration/bar.js","relative":"cypress/integration/bar.js","relativeUrl":"/__cypress/tests?p=cypress/integration/bar.js"},{"absolute":"//ids/cypress/integration/baz.js","relative":"cypress/integration/baz.js","relativeUrl":"/__cypress/tests?p=cypress/integration/baz.js"},{"absolute":"//ids/cypress/integration/dom.jsx","relative":"cypress/integration/dom.jsx","relativeUrl":"/__cypress/tests?p=cypress/integration/dom.jsx"},{"absolute":"//ids/cypress/integration/foo.coffee","relative":"cypress/integration/foo.coffee","relativeUrl":"/__cypress/tests?p=cypress/integration/foo.coffee"},{"absolute":"//ids/cypress/integration/nested/tmp.js","relative":"cypress/integration/nested/tmp.js","relativeUrl":"/__cypress/tests?p=cypress/integration/nested/tmp.js"},{"absolute":"//ids/cypress/integration/noop.coffee","relative":"cypress/integration/noop.coffee","relativeUrl":"/__cypress/tests?p=cypress/integration/noop.coffee"}]); + return Cypress.onSpecWindow(window, [{"absolute":"//ids/cypress/support/index.js","relative":"cypress/support/index.js","relativeUrl":"/__cypress/tests?p=cypress/support/index.js"},{"absolute":"//ids/cypress/integration/bar.js","relative":"cypress/integration/bar.js","relativeUrl":"/__cypress/tests?p=cypress/integration/bar.js"},{"absolute":"//ids/cypress/integration/baz.js","relative":"cypress/integration/baz.js","relativeUrl":"/__cypress/tests?p=cypress/integration/baz.js"},{"absolute":"//ids/cypress/integration/dom.jsx","relative":"cypress/integration/dom.jsx","relativeUrl":"/__cypress/tests?p=cypress/integration/dom.jsx"},{"absolute":"//ids/cypress/integration/es6.js","relative":"cypress/integration/es6.js","relativeUrl":"/__cypress/tests?p=cypress/integration/es6.js"},{"absolute":"//ids/cypress/integration/foo.coffee","relative":"cypress/integration/foo.coffee","relativeUrl":"/__cypress/tests?p=cypress/integration/foo.coffee"},{"absolute":"//ids/cypress/integration/nested/tmp.js","relative":"cypress/integration/nested/tmp.js","relativeUrl":"/__cypress/tests?p=cypress/integration/nested/tmp.js"},{"absolute":"//ids/cypress/integration/noop.coffee","relative":"cypress/integration/noop.coffee","relativeUrl":"/__cypress/tests?p=cypress/integration/noop.coffee"}]); })(window.opener || window.parent); diff --git a/packages/web-config/webpack.config.base.ts b/packages/web-config/webpack.config.base.ts index bbb7f96bba..635e83337f 100644 --- a/packages/web-config/webpack.config.base.ts +++ b/packages/web-config/webpack.config.base.ts @@ -84,7 +84,10 @@ export const getCommonConfig = () => { [require.resolve('@babel/plugin-proposal-class-properties'), { loose: true }], ], presets: [ - [require.resolve('@babel/preset-env'), { targets: { 'chrome': 63 } }], + // the chrome version should be synced with + // npm/webpack-batteries-included-preprocessor/index.js and + // packages/server/lib/browsers/chrome.ts + [require.resolve('@babel/preset-env'), { targets: { 'chrome': '64' } }], require.resolve('@babel/preset-react'), [require.resolve('@babel/preset-typescript'), { allowNamespaces: true }], ], diff --git a/yarn.lock b/yarn.lock index 687d778852..3befe3ee0b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13214,7 +13214,7 @@ debug@^3.0.0, debug@^3.0.1, debug@^3.1.0, debug@^3.1.1, debug@^3.2.5, debug@^3.2 dependencies: ms "^2.1.1" -debuglog@*, debuglog@^1.0.1: +debuglog@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492" integrity sha1-qiT/uaw9+aI1GDfPstJ5NgzXhJI= @@ -19270,7 +19270,7 @@ import-local@2.0.0, import-local@^2.0.0: pkg-dir "^3.0.0" resolve-cwd "^2.0.0" -imurmurhash@*, imurmurhash@^0.1.4: +imurmurhash@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= @@ -22516,11 +22516,6 @@ lodash._basecreate@^3.0.0: resolved "https://registry.yarnpkg.com/lodash._basecreate/-/lodash._basecreate-3.0.3.tgz#1bc661614daa7fc311b7d03bf16806a0213cf821" integrity sha1-G8ZhYU2qf8MRt9A78WgGoCE8+CE= -lodash._baseindexof@*: - version "3.1.0" - resolved "https://registry.yarnpkg.com/lodash._baseindexof/-/lodash._baseindexof-3.1.0.tgz#fe52b53a1c6761e42618d654e4a25789ed61822c" - integrity sha1-/lK1OhxnYeQmGNZU5KJXie1hgiw= - lodash._baseuniq@~4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/lodash._baseuniq/-/lodash._baseuniq-4.6.0.tgz#0ebb44e456814af7905c6212fa2c9b2d51b841e8" @@ -22529,29 +22524,12 @@ lodash._baseuniq@~4.6.0: lodash._createset "~4.0.0" lodash._root "~3.0.0" -lodash._bindcallback@*: - version "3.0.1" - resolved "https://registry.yarnpkg.com/lodash._bindcallback/-/lodash._bindcallback-3.0.1.tgz#e531c27644cf8b57a99e17ed95b35c748789392e" - integrity sha1-5THCdkTPi1epnhftlbNcdIeJOS4= - -lodash._cacheindexof@*: - version "3.0.2" - resolved "https://registry.yarnpkg.com/lodash._cacheindexof/-/lodash._cacheindexof-3.0.2.tgz#3dc69ac82498d2ee5e3ce56091bafd2adc7bde92" - integrity sha1-PcaayCSY0u5ePOVgkbr9Ktx73pI= - -lodash._createcache@*: - version "3.1.2" - resolved "https://registry.yarnpkg.com/lodash._createcache/-/lodash._createcache-3.1.2.tgz#56d6a064017625e79ebca6b8018e17440bdcf093" - integrity sha1-VtagZAF2JeeevKa4AY4XRAvc8JM= - dependencies: - lodash._getnative "^3.0.0" - lodash._createset@~4.0.0: version "4.0.3" resolved "https://registry.yarnpkg.com/lodash._createset/-/lodash._createset-4.0.3.tgz#0f4659fbb09d75194fa9e2b88a6644d363c9fe26" integrity sha1-D0ZZ+7CddRlPqeK4imZE02PJ/iY= -lodash._getnative@*, lodash._getnative@^3.0.0: +lodash._getnative@^3.0.0: version "3.9.1" resolved "https://registry.yarnpkg.com/lodash._getnative/-/lodash._getnative-3.9.1.tgz#570bc7dede46d61cdcde687d65d3eecbaa3aaff5" integrity sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U= @@ -22754,11 +22732,6 @@ lodash.reduce@^4.6.0: resolved "https://registry.yarnpkg.com/lodash.reduce/-/lodash.reduce-4.6.0.tgz#f1ab6b839299ad48f784abbf476596f03b914d3b" integrity sha1-8atrg5KZrUj3hKu/R2WW8DuRTTs= -lodash.restparam@*: - version "3.6.1" - resolved "https://registry.yarnpkg.com/lodash.restparam/-/lodash.restparam-3.6.1.tgz#936a4e309ef330a7645ed4145986c85ae5b20805" - integrity sha1-k2pOMJ7zMKdkXtQUWYbIWuWyCAU= - lodash.set@4.3.2, lodash.set@^4.3.2: version "4.3.2" resolved "https://registry.yarnpkg.com/lodash.set/-/lodash.set-4.3.2.tgz#d8757b1da807dde24816b0d6a84bea1a76230b23" From b52ac98a6944bc831221ccb730f89c6cc92a4573 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 10 Mar 2021 15:24:00 +0000 Subject: [PATCH 017/134] =?UTF-8?q?feat(deps):=20update=20dependency=20ele?= =?UTF-8?q?ctron=20to=20version=2012.x=20=F0=9F=8C=9F=20(#15292)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Renovate Bot Co-authored-by: Zach Bloomquist --- .gitignore | 2 +- .node-version | 2 +- appveyor.yml | 2 +- circle.yml | 6 +- cli/__snapshots__/spawn_spec.js | 6 +- cli/lib/util.js | 26 ----- cli/package.json | 4 +- cli/test/lib/util_spec.js | 44 -------- npm/design-system/package.json | 6 +- .../component/basic/error-boundary-spec.js | 23 ++-- npm/react/package.json | 4 +- package.json | 4 +- packages/driver/src/cypress/cy.js | 15 ++- packages/electron/lib/electron.js | 4 - packages/electron/package.json | 2 +- packages/https-proxy/lib/ca.js | 2 +- packages/https-proxy/lib/server.js | 7 +- packages/network/lib/agent.ts | 5 +- packages/network/lib/http-utils.ts | 15 +++ packages/server-ct/package.json | 8 +- .../server/__snapshots__/4_request_spec.ts.js | 6 +- packages/server/__snapshots__/4_xhr_spec.js | 22 ++-- packages/server/index.js | 15 +-- .../server/lib/browsers/cdp_automation.ts | 3 + packages/server/lib/file_server.js | 3 +- packages/server/lib/server-base.ts | 4 +- packages/server/lib/util/node_options.ts | 94 ---------------- packages/server/package.json | 2 +- .../test/e2e/4_form_submissions_spec.js | 37 +++--- .../server/test/integration/cypress_spec.js | 2 + .../e2e/cypress/integration/xhr_spec.js | 4 - .../projects/e2e/cypress/plugins/index.js | 8 -- .../server/test/unit/node_options_spec.ts | 63 ----------- packages/ts/index.d.ts | 3 +- scripts/run-docker-local.sh | 2 +- yarn.lock | 106 +++++++++++------- 36 files changed, 181 insertions(+), 380 deletions(-) delete mode 100644 packages/server/lib/util/node_options.ts delete mode 100644 packages/server/test/unit/node_options_spec.ts diff --git a/.gitignore b/.gitignore index 1db40116c4..51c77004f7 100644 --- a/.gitignore +++ b/.gitignore @@ -31,7 +31,7 @@ npm/**/cypress/screenshots # from example packages/example/app packages/example/build -packages/example/cypress +packages/example/cypress/integration # from server packages/server/.cy diff --git a/.node-version b/.node-version index 9cd25a1fec..2a0dc9a810 100644 --- a/.node-version +++ b/.node-version @@ -1 +1 @@ -12.18.3 +14.16.0 diff --git a/appveyor.yml b/appveyor.yml index 554f87d069..6582f7385e 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -9,7 +9,7 @@ branches: # https://www.appveyor.com/docs/lang/nodejs-iojs/ environment: # use matching version of Node.js - nodejs_version: "12.18.3" + nodejs_version: "14.16.0" # encode secure variables which will NOT be used # in pull requests # https://www.appveyor.com/docs/build-configuration/#secure-variables diff --git a/circle.yml b/circle.yml index 6f92e1e8b1..ab2d948eaa 100644 --- a/circle.yml +++ b/circle.yml @@ -44,14 +44,14 @@ executors: # the Docker image with Cypress dependencies and Chrome browser cy-doc: docker: - - image: cypress/browsers:node12.18.3-chrome83-ff77 + - image: cypress/browsers:node14.16.0-chrome89-ff77 environment: PLATFORM: linux # Docker image with non-root "node" user non-root-docker-user: docker: - - image: cypress/browsers:node12.18.3-chrome83-ff77 + - image: cypress/browsers:node14.16.0-chrome89-ff77 user: node environment: PLATFORM: linux @@ -62,7 +62,7 @@ executors: mac: macos: # Executor should have Node >= required version - xcode: "11.3.1" + xcode: "12.2.0" environment: PLATFORM: mac diff --git a/cli/__snapshots__/spawn_spec.js b/cli/__snapshots__/spawn_spec.js index 6d6052a044..8b1b60f166 100644 --- a/cli/__snapshots__/spawn_spec.js +++ b/cli/__snapshots__/spawn_spec.js @@ -4,8 +4,7 @@ exports['lib/exec/spawn .start forces colors and streams when supported 1'] = { "MOCHA_COLORS": "1", "FORCE_STDIN_TTY": "1", "FORCE_STDOUT_TTY": "1", - "FORCE_STDERR_TTY": "1", - "NODE_OPTIONS": "--max-http-header-size=1048576" + "FORCE_STDERR_TTY": "1" } exports['lib/exec/spawn .start does not force colors and streams when not supported 1'] = { @@ -13,8 +12,7 @@ exports['lib/exec/spawn .start does not force colors and streams when not suppor "DEBUG_COLORS": "0", "FORCE_STDIN_TTY": "0", "FORCE_STDOUT_TTY": "0", - "FORCE_STDERR_TTY": "0", - "NODE_OPTIONS": "--max-http-header-size=1048576" + "FORCE_STDERR_TTY": "0" } exports['lib/exec/spawn .start detects kill signal exits with error on SIGKILL 1'] = ` diff --git a/cli/lib/util.js b/cli/lib/util.js index a16ebabc6d..17b1e665bb 100644 --- a/cli/lib/util.js +++ b/cli/lib/util.js @@ -281,35 +281,9 @@ const util = { .mapValues((value) => { // stringify to 1 or 0 return value ? '1' : '0' }) - .extend(util.getNodeOptions(options)) .value() }, - getNodeOptions (options, nodeVersion) { - if (!nodeVersion) { - nodeVersion = Number(process.versions.node.split('.')[0]) - } - - if (options.dev && nodeVersion < 12) { - // `node` is used instead of Electron when --dev is passed, so this won't work if Node is too old - debug('NODE_OPTIONS=--max-http-header-size could not be set because we\'re in dev mode and Node is < 12.0.0') - - return - } - - // https://github.com/cypress-io/cypress/issues/5431 - const NODE_OPTIONS = `--max-http-header-size=${1024 ** 2}` - - if (_.isString(process.env.NODE_OPTIONS)) { - return { - NODE_OPTIONS: `${NODE_OPTIONS} ${process.env.NODE_OPTIONS}`, - ORIGINAL_NODE_OPTIONS: process.env.NODE_OPTIONS || '', - } - } - - return { NODE_OPTIONS } - }, - getForceTty () { return { FORCE_STDIN_TTY: util.isTty(process.stdin.fd), diff --git a/cli/package.json b/cli/package.json index 6332834718..5c04bdd9a9 100644 --- a/cli/package.json +++ b/cli/package.json @@ -23,7 +23,7 @@ "@cypress/listr-verbose-renderer": "^0.4.1", "@cypress/request": "^2.88.5", "@cypress/xvfb": "^1.2.4", - "@types/node": "^12.12.50", + "@types/node": "^14.14.31", "@types/sinonjs__fake-timers": "^6.0.2", "@types/sizzle": "^2.3.2", "arch": "^2.2.0", @@ -38,7 +38,7 @@ "dayjs": "^1.10.4", "debug": "4.3.2", "eventemitter2": "^6.4.3", - "execa": "^5.0.0", + "execa": "4.1.0", "executable": "^4.1.1", "extract-zip": "^1.7.0", "fs-extra": "^9.1.0", diff --git a/cli/test/lib/util_spec.js b/cli/test/lib/util_spec.js index 580e206fef..52b83ef961 100644 --- a/cli/test/lib/util_spec.js +++ b/cli/test/lib/util_spec.js @@ -3,7 +3,6 @@ require('../spec_helper') const os = require('os') const tty = require('tty') const snapshot = require('../support/snapshot') -const mockedEnv = require('mocked-env') const supportsColor = require('supports-color') const proxyquire = require('proxyquire') const hasha = require('hasha') @@ -12,9 +11,6 @@ const la = require('lazy-ass') const util = require(`${lib}/util`) const logger = require(`${lib}/logger`) -// https://github.com/cypress-io/cypress/issues/5431 -const expectedNodeOptions = `--max-http-header-size=${1024 * 1024}` - describe('util', () => { beforeEach(() => { sinon.stub(process, 'exit') @@ -217,7 +213,6 @@ describe('util', () => { FORCE_COLOR: '1', DEBUG_COLORS: '1', MOCHA_COLORS: '1', - NODE_OPTIONS: expectedNodeOptions, }) util.supportsColor.returns(false) @@ -229,49 +224,10 @@ describe('util', () => { FORCE_STDERR_TTY: '0', FORCE_COLOR: '0', DEBUG_COLORS: '0', - NODE_OPTIONS: expectedNodeOptions, }) }) }) - context('.getNodeOptions', () => { - let restoreEnv - - afterEach(() => { - if (restoreEnv) { - restoreEnv() - restoreEnv = null - } - }) - - it('adds required NODE_OPTIONS', () => { - restoreEnv = mockedEnv({ - NODE_OPTIONS: undefined, - }) - - expect(util.getNodeOptions({})).to.deep.eq({ - NODE_OPTIONS: expectedNodeOptions, - }) - }) - - it('includes existing NODE_OPTIONS', () => { - restoreEnv = mockedEnv({ - NODE_OPTIONS: '--foo --bar', - }) - - expect(util.getNodeOptions({})).to.deep.eq({ - NODE_OPTIONS: `${expectedNodeOptions} --foo --bar`, - ORIGINAL_NODE_OPTIONS: '--foo --bar', - }) - }) - - it('does not return if dev is set and version < 12', () => { - expect(util.getNodeOptions({ - dev: true, - }, 11)).to.be.undefined - }) - }) - context('.getForceTty', () => { it('forces when each stream is a tty', () => { sinon.stub(tty, 'isatty') diff --git a/npm/design-system/package.json b/npm/design-system/package.json index 734493e011..0522c7cb82 100644 --- a/npm/design-system/package.json +++ b/npm/design-system/package.json @@ -7,9 +7,9 @@ "build": "rimraf dist && yarn rollup -c rollup.config.js", "build-prod": "yarn build", "cy:open": "node ../../scripts/cypress.js open-ct --project ${PWD}", - "cy:open:debug": "NODE_OPTIONS=--max-http-header-size=1048576 node --inspect-brk ../../scripts/start.js --component-testing --project ${PWD}", + "cy:open:debug": "node --inspect-brk ../../scripts/start.js --component-testing --project ${PWD}", "cy:run": "node ../../scripts/cypress.js run-ct --project ${PWD}", - "cy:run:debug": "NODE_OPTIONS=--max-http-header-size=1048576 node --inspect-brk ../../scripts/start.js --component-testing --run-project ${PWD}", + "cy:run:debug": "node --inspect-brk ../../scripts/start.js --component-testing --run-project ${PWD}", "pretest": "yarn transpile", "test": "yarn cy:run", "transpile": "tsc", @@ -29,7 +29,7 @@ "@rollup/plugin-commonjs": "^17.1.0", "@rollup/plugin-json": "^4.1.0", "@rollup/plugin-node-resolve": "^11.1.1", - "@types/node": "12.12.50", + "@types/node": "14.14.31", "@types/semver": "7.3.4", "babel-loader": "8.0.6", "css-loader": "2.1.1", diff --git a/npm/react/cypress/component/basic/error-boundary-spec.js b/npm/react/cypress/component/basic/error-boundary-spec.js index 48eaa81ab2..b5a4cf0363 100644 --- a/npm/react/cypress/component/basic/error-boundary-spec.js +++ b/npm/react/cypress/component/basic/error-boundary-spec.js @@ -29,15 +29,20 @@ describe('Error Boundary', () => { }) it('on error, display fallback UI', () => { - try { - mount( - - - , - ) - } catch (e) { - // do nothing - } + // Error boundaries do not stop an uncaught error from propagating. + // Cypress will fail on uncaught exceptions by default, so we need to suppress that behavior. + cy.on('uncaught:exception', (err) => { + // Assert that we are only suppressing the default behavior for the error we expect. + expect(err.message.includes('I crashed!')).to.be.true + + return false + }) + + mount( + + + , + ) cy.get('header h1').should('contain', 'Something went wrong.') cy.get('header h3').should('contain', 'ChildWithError failed to load') diff --git a/npm/react/package.json b/npm/react/package.json index 0292ec9d47..f8f260c942 100644 --- a/npm/react/package.json +++ b/npm/react/package.json @@ -7,9 +7,9 @@ "build": "rimraf dist && yarn rollup -c rollup.config.js", "build-prod": "yarn build", "cy:open": "node ../../scripts/cypress.js open-ct --project ${PWD}", - "cy:open:debug": "NODE_OPTIONS=--max-http-header-size=1048576 node --inspect-brk ../../scripts/start.js --component-testing --project ${PWD}", + "cy:open:debug": "node --inspect-brk ../../scripts/start.js --component-testing --project ${PWD}", "cy:run": "node ../../scripts/cypress.js run-ct --project ${PWD}", - "cy:run:debug": "NODE_OPTIONS=--max-http-header-size=1048576 node --inspect-brk ../../scripts/start.js --component-testing --run-project ${PWD}", + "cy:run:debug": "node --inspect-brk ../../scripts/start.js --component-testing --run-project ${PWD}", "pretest": "yarn transpile", "test": "yarn cy:run", "test-ci": "node run-tests.js", diff --git a/package.json b/package.json index 6dbfb7520a..8f0b5d226e 100644 --- a/package.json +++ b/package.json @@ -93,7 +93,7 @@ "@types/markdown-it": "0.0.9", "@types/mini-css-extract-plugin": "0.8.0", "@types/mocha": "8.0.3", - "@types/node": "12.12.50", + "@types/node": "14.14.31", "@types/prismjs": "1.16.0", "@types/ramda": "0.25.47", "@types/react": "16.9.50", @@ -192,7 +192,7 @@ "typescript": "3.7.4" }, "engines": { - "node": ">=12.18.3", + "node": ">=14.16.0", "yarn": ">=1.17.3" }, "productName": "Cypress", diff --git a/packages/driver/src/cypress/cy.js b/packages/driver/src/cypress/cy.js index 60ee095faf..6804d44b7e 100644 --- a/packages/driver/src/cypress/cy.js +++ b/packages/driver/src/cypress/cy.js @@ -93,7 +93,7 @@ const setTopOnError = function (Cypress, cy, errors) { const isSpecError = $errUtils.isSpecError(Cypress.config('spec'), err) if (isSpecError) { - return curCy.onSpecWindowUncaughtException(handlerType, err) + return curCy.onSpecWindowUncaughtException(handlerType, err, promise) } return curCy.onUncaughtException(handlerType, err, promise) @@ -1222,13 +1222,24 @@ const create = function (specWindow, Cypress, Cookies, state, config, log) { snapshots.onBeforeWindowLoad() }, - onSpecWindowUncaughtException (handlerType, err) { + onSpecWindowUncaughtException (handlerType, err, promise) { err = errors.createUncaughtException('spec', handlerType, err) const runnable = state('runnable') if (!runnable) return err + if (config('componentTesting')) { + // in component testing, uncaught exceptions should be catchable, as there is no AUT + const results = Cypress.action('app:uncaught:exception', err, runnable, promise) + + // dont do anything if any of our uncaught:exception + // listeners returned false + if (_.some(results, returnedFalse)) { + return + } + } + try { fail(err) } catch (failErr) { diff --git a/packages/electron/lib/electron.js b/packages/electron/lib/electron.js index a8dec77a48..25e5a16607 100644 --- a/packages/electron/lib/electron.js +++ b/packages/electron/lib/electron.js @@ -128,10 +128,6 @@ module.exports = { } } - // max HTTP header size 8kb -> 1mb - // https://github.com/cypress-io/cypress/issues/76 - argv.unshift(`--max-http-header-size=${1024 * 1024}`) - debug('spawning %s with args', execPath, argv) if (debug.enabled) { diff --git a/packages/electron/package.json b/packages/electron/package.json index d039ea5a54..c298a5cda9 100644 --- a/packages/electron/package.json +++ b/packages/electron/package.json @@ -24,7 +24,7 @@ "minimist": "1.2.5" }, "devDependencies": { - "electron": "11.3.0", + "electron": "12.0.0", "execa": "4.1.0", "mocha": "3.5.3" }, diff --git a/packages/https-proxy/lib/ca.js b/packages/https-proxy/lib/ca.js index e3b1a00283..47d008ed38 100644 --- a/packages/https-proxy/lib/ca.js +++ b/packages/https-proxy/lib/ca.js @@ -269,7 +269,7 @@ class CA { } writeCAVersion () { - return fs.outputFileAsync(this.getCAVersionPath(), CA_VERSION) + return fs.outputFileAsync(this.getCAVersionPath(), String(CA_VERSION)) } assertMinimumCAVersion () { diff --git a/packages/https-proxy/lib/server.js b/packages/https-proxy/lib/server.js index 2a88364c64..8b22c33171 100644 --- a/packages/https-proxy/lib/server.js +++ b/packages/https-proxy/lib/server.js @@ -1,5 +1,5 @@ const _ = require('lodash') -const { allowDestroy, connect } = require('@packages/network') +const { allowDestroy, connect, httpUtils } = require('@packages/network') const debug = require('debug')('cypress:https-proxy') const https = require('https') const net = require('net') @@ -227,7 +227,10 @@ class Server { _listenHttpsServer (data) { return new Promise((resolve, reject) => { - const server = https.createServer(data) + const server = https.createServer({ + ...data, + ...httpUtils.lenientOptions, + }) allowDestroy(server) diff --git a/packages/network/lib/agent.ts b/packages/network/lib/agent.ts index d32921d177..861ce7c6af 100644 --- a/packages/network/lib/agent.ts +++ b/packages/network/lib/agent.ts @@ -6,6 +6,7 @@ import net from 'net' import { getProxyForUrl } from 'proxy-from-env' import url from 'url' import { createRetryingSocket, getAddress } from './connect' +import { lenientOptions } from './http-utils' const debug = debugModule('cypress:network:agent') const CRLF = '\r\n' @@ -151,9 +152,7 @@ export class CombinedAgent { // called by Node.js whenever a new request is made internally addRequest (req: http.ClientRequest, options: http.RequestOptions, port?: number, localAddress?: string) { - // allow requests which contain invalid/malformed headers - // https://github.com/cypress-io/cypress/issues/5602 - req.insecureHTTPParser = true + _.merge(req, lenientOptions) // Legacy API: addRequest(req, host, port, localAddress) // https://github.com/nodejs/node/blob/cb68c04ce1bc4534b2d92bc7319c6ff6dda0180d/lib/_http_agent.js#L148-L155 diff --git a/packages/network/lib/http-utils.ts b/packages/network/lib/http-utils.ts index 6e8ef36bfa..bf4e18b045 100644 --- a/packages/network/lib/http-utils.ts +++ b/packages/network/lib/http-utils.ts @@ -9,3 +9,18 @@ const NO_BODY_STATUS_CODES = [204, 304] export function responseMustHaveEmptyBody (req: IncomingMessage, res: IncomingMessage) { return _.includes(NO_BODY_STATUS_CODES, res.statusCode) || (req.method && req.method.toLowerCase() === 'head') } + +/** + * HTTP options to make Node.js's HTTP libraries behave as leniently as possible. + * + * These should be used whenever Cypress is processing "real-world" HTTP requests - like when setting up a proxy + * server or sending outgoing requests. + */ +export const lenientOptions = { + // increase header buffer for incoming response (ClientRequest) request (Server) headers, from 16KB to 1MB + // @see https://github.com/cypress-io/cypress/issues/76 + maxHeaderSize: 1024 ** 2, + // allow requests which contain invalid/malformed headers + // https://github.com/cypress-io/cypress/issues/5602 + insecureHTTPParser: true, +} diff --git a/packages/server-ct/package.json b/packages/server-ct/package.json index c71ab0a33e..a7a8ab208e 100644 --- a/packages/server-ct/package.json +++ b/packages/server-ct/package.json @@ -5,10 +5,10 @@ "main": "index.js", "scripts": { "build-prod": "tsc", - "cypress:open": "node ./scripts/check-example.js && NODE_OPTIONS=--max-http-header-size=1048576 node ../../scripts/start.js --component-testing --project ${PWD}/crossword-example", - "cypress:open:debug": "node ./scripts/check-example.js && NODE_OPTIONS=--max-http-header-size=1048576 node --inspect-brk ../../scripts/start.js --component-testing --project ${PWD}/crossword-example", - "cypress:run": "node ./scripts/check-example.js && NODE_OPTIONS=--max-http-header-size=1048576 node ../../scripts/start.js --component-testing --run-project ${PWD}/crossword-example", - "cypress:run:debug": "node ./scripts/check-example.js && NODE_OPTIONS=--max-http-header-size=1048576 node --inspect-brk ../../scripts/start.js --component-testing --run-project ${PWD}/crossword-example", + "cypress:open": "node ./scripts/check-example.js && node ../../scripts/start.js --component-testing --project ${PWD}/crossword-example", + "cypress:open:debug": "node ./scripts/check-example.js && node --inspect-brk ../../scripts/start.js --component-testing --project ${PWD}/crossword-example", + "cypress:run": "node ./scripts/check-example.js && node ../../scripts/start.js --component-testing --run-project ${PWD}/crossword-example", + "cypress:run:debug": "node ./scripts/check-example.js && node --inspect-brk ../../scripts/start.js --component-testing --run-project ${PWD}/crossword-example", "test-unit": "mocha -r @packages/ts/register test/**/*.spec.ts --config ./test/.mocharc.js --exit" }, "dependencies": { diff --git a/packages/server/__snapshots__/4_request_spec.ts.js b/packages/server/__snapshots__/4_request_spec.ts.js index ae6d4025de..03b599be76 100644 --- a/packages/server/__snapshots__/4_request_spec.ts.js +++ b/packages/server/__snapshots__/4_request_spec.ts.js @@ -243,7 +243,8 @@ Headers: { "content-length": "19", "etag": "W/13-52060a5f", "date": "Fri, 18 Aug 2017 XX:XX GMT", - "connection": "keep-alive" + "connection": "keep-alive", + "keep-alive": "timeout=5" } Body: Service Unavailable @@ -361,7 +362,8 @@ Headers: { "content-type": "text/html; charset=utf-8", "content-length": "301", "date": "Fri, 18 Aug 2017 XX:XX GMT", - "connection": "keep-alive" + "connection": "keep-alive", + "keep-alive": "timeout=5" } Body: diff --git a/packages/server/__snapshots__/4_xhr_spec.js b/packages/server/__snapshots__/4_xhr_spec.js index 2bf89a4439..41d85aeaa0 100644 --- a/packages/server/__snapshots__/4_xhr_spec.js +++ b/packages/server/__snapshots__/4_xhr_spec.js @@ -24,21 +24,20 @@ exports['e2e xhr / passes in global mode'] = ` ✓ does not inject into json's contents from file server even requesting text/html ✓ works prior to visit ✓ can stub a 100kb response - ✓ spawns tasks with original NODE_OPTIONS server with 1 visit ✓ response body ✓ request body ✓ aborts - 10 passing + 9 passing (Results) ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ - │ Tests: 10 │ - │ Passing: 10 │ + │ Tests: 9 │ + │ Passing: 9 │ │ Failing: 0 │ │ Pending: 0 │ │ Skipped: 0 │ @@ -62,9 +61,9 @@ exports['e2e xhr / passes in global mode'] = ` Spec Tests Passing Failing Pending Skipped ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ - │ ✔ xhr_spec.js XX:XX 10 10 - - - │ + │ ✔ xhr_spec.js XX:XX 9 9 - - - │ └────────────────────────────────────────────────────────────────────────────────────────────────┘ - ✔ All specs passed! XX:XX 10 10 - - - + ✔ All specs passed! XX:XX 9 9 - - - ` @@ -95,21 +94,20 @@ exports['e2e xhr / passes through CLI'] = ` ✓ does not inject into json's contents from file server even requesting text/html ✓ works prior to visit ✓ can stub a 100kb response - ✓ spawns tasks with original NODE_OPTIONS server with 1 visit ✓ response body ✓ request body ✓ aborts - 10 passing + 9 passing (Results) ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ - │ Tests: 10 │ - │ Passing: 10 │ + │ Tests: 9 │ + │ Passing: 9 │ │ Failing: 0 │ │ Pending: 0 │ │ Skipped: 0 │ @@ -133,9 +131,9 @@ exports['e2e xhr / passes through CLI'] = ` Spec Tests Passing Failing Pending Skipped ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ - │ ✔ xhr_spec.js XX:XX 10 10 - - - │ + │ ✔ xhr_spec.js XX:XX 9 9 - - - │ └────────────────────────────────────────────────────────────────────────────────────────────────┘ - ✔ All specs passed! XX:XX 10 10 - - - + ✔ All specs passed! XX:XX 9 9 - - - ` diff --git a/packages/server/index.js b/packages/server/index.js index 96f51f467a..a360b86123 100644 --- a/packages/server/index.js +++ b/packages/server/index.js @@ -36,17 +36,4 @@ process.traceDeprecation = true require('./lib/util/suppress_unauthorized_warning').suppress() -function launchOrFork () { - const nodeOptions = require('./lib/util/node_options') - - if (nodeOptions.needsOptions()) { - // https://github.com/cypress-io/cypress/pull/5492 - return nodeOptions.forkWithCorrectOptions() - } - - nodeOptions.restoreOriginalOptions() - - module.exports = require('./lib/cypress').start(process.argv) -} - -launchOrFork() +module.exports = require('./lib/cypress').start(process.argv) diff --git a/packages/server/lib/browsers/cdp_automation.ts b/packages/server/lib/browsers/cdp_automation.ts index ee482ca709..5b5fe73f51 100644 --- a/packages/server/lib/browsers/cdp_automation.ts +++ b/packages/server/lib/browsers/cdp_automation.ts @@ -144,6 +144,9 @@ export const CdpAutomation = (sendDebuggerCommandFn: SendDebuggerCommand) => { }) .then((result: cdp.Network.GetCookiesResponse) => { return normalizeGetCookies(result.cookies) + .filter((cookie) => { + return !(url.startsWith('http:') && cookie.secure) + }) }) } diff --git a/packages/server/lib/file_server.js b/packages/server/lib/file_server.js index 8b143bd2d5..da04be9c4e 100644 --- a/packages/server/lib/file_server.js +++ b/packages/server/lib/file_server.js @@ -6,6 +6,7 @@ const url = require('url') const http = require('http') const path = require('path') const send = require('send') +const { httpUtils } = require('@packages/network') const { allowDestroy } = require('./util/server_destroy') const random = require('./util/random') const networkFailures = require('./util/network_failures') @@ -52,7 +53,7 @@ module.exports = { return new Promise(((resolve) => { const token = random.id(64) - const srv = http.createServer((req, res) => { + const srv = http.createServer(httpUtils.lenientOptions, (req, res) => { return onRequest(req, res, token, fileServerFolder) }) diff --git a/packages/server/lib/server-base.ts b/packages/server/lib/server-base.ts index e60a172afb..48b35125cf 100644 --- a/packages/server/lib/server-base.ts +++ b/packages/server/lib/server-base.ts @@ -11,7 +11,7 @@ import { AddressInfo } from 'net' import url from 'url' import httpsProxy from '@packages/https-proxy' import { netStubbingState, NetStubbingState } from '@packages/net-stubbing' -import { agent, cors, uri } from '@packages/network' +import { agent, cors, httpUtils, uri } from '@packages/network' import { NetworkProxy } from '@packages/proxy' import { SocketCt } from '@packages/server-ct' import errors from './errors' @@ -237,7 +237,7 @@ export class ServerBase { } _createHttpServer (app): DestroyableHttpServer { - const svr = http.createServer(app) + const svr = http.createServer(httpUtils.lenientOptions, app) allowDestroy(svr) diff --git a/packages/server/lib/util/node_options.ts b/packages/server/lib/util/node_options.ts deleted file mode 100644 index 5f41b1ff20..0000000000 --- a/packages/server/lib/util/node_options.ts +++ /dev/null @@ -1,94 +0,0 @@ -import cp from 'child_process' -import debugModule from 'debug' - -const debug = debugModule('cypress:server:util:node_options') - -export const NODE_OPTIONS = `--max-http-header-size=${1024 ** 2}` - -/** - * If Cypress was not launched via CLI, it may be missing certain startup - * options. This checks that those startup options were applied. - * - * @returns {boolean} does Cypress have the expected NODE_OPTIONS? - */ -export function needsOptions (): boolean { - if ((process.env.NODE_OPTIONS || '').includes(NODE_OPTIONS)) { - debug('NODE_OPTIONS check passed, not forking %o', { NODE_OPTIONS: process.env.NODE_OPTIONS }) - - return false - } - - if (typeof require.main === 'undefined') { - debug('require.main is undefined, this should not happen normally, not forking') - - return false - } - - return true -} - -/** - * Retrieve the current inspect flag, if the process was launched with one. - */ -function getCurrentInspectFlag (): string | undefined { - const flag = process.execArgv.find((v) => v.startsWith('--inspect')) - - return flag ? flag.split('=')[0] : undefined -} - -/** - * Fork the current process using the good NODE_OPTIONS and pipe stdio - * through the current process. On exit, copy the error code too. - */ -export function forkWithCorrectOptions (): void { - // this should only happen when running from global mode, when the CLI couldn't set the NODE_OPTIONS - process.env.ORIGINAL_NODE_OPTIONS = process.env.NODE_OPTIONS || '' - process.env.NODE_OPTIONS = `${NODE_OPTIONS} ${process.env.ORIGINAL_NODE_OPTIONS}` - - debug('NODE_OPTIONS check failed, forking %o', { - NODE_OPTIONS: process.env.NODE_OPTIONS, - ORIGINAL_NODE_OPTIONS: process.env.ORIGINAL_NODE_OPTIONS, - }) - - const launchArgs = process.argv.slice(1) - const inspectFlag = getCurrentInspectFlag() - - if (inspectFlag) { - launchArgs.unshift(`${inspectFlag}=${process.debugPort + 1}`) - } - - cp.spawn( - process.execPath, - launchArgs, - { stdio: 'inherit' }, - ) - .on('error', () => {}) - .on('exit', (code, signal) => { - debug('child exited %o', { code, signal }) - process.exit(code === null ? 1 : code) - }) -} - -/** - * Once the Electron process is launched, restore the user's original NODE_OPTIONS - * environment variables from before the CLI added extra NODE_OPTIONS. - * - * This way, any `node` processes launched by Cypress will retain the user's - * `NODE_OPTIONS` without unexpected modificiations that could cause issues with - * user code. - */ -export function restoreOriginalOptions (): void { - // @ts-ignore - if (!process.versions || !process.versions.electron) { - debug('not restoring NODE_OPTIONS since not yet in Electron') - - return - } - - debug('restoring NODE_OPTIONS %o', { - NODE_OPTIONS: process.env.NODE_OPTIONS, - ORIGINAL_NODE_OPTIONS: process.env.ORIGINAL_NODE_OPTIONS, - }) - - process.env.NODE_OPTIONS = process.env.ORIGINAL_NODE_OPTIONS || '' -} diff --git a/packages/server/package.json b/packages/server/package.json index 28ca4b7047..aa6c18ee49 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -172,7 +172,7 @@ "mock-fs": "4.12.0", "mocked-env": "1.2.4", "mockery": "2.1.0", - "multiparty": "4.2.1", + "multer": "1.4.2", "nock": "12.0.2", "proxyquire": "2.1.3", "react": "16.8.6", diff --git a/packages/server/test/e2e/4_form_submissions_spec.js b/packages/server/test/e2e/4_form_submissions_spec.js index b822b75cfa..6e7d9d316d 100644 --- a/packages/server/test/e2e/4_form_submissions_spec.js +++ b/packages/server/test/e2e/4_form_submissions_spec.js @@ -1,8 +1,7 @@ const rp = require('@cypress/request-promise') const path = require('path') -const Promise = require('bluebird') const bodyParser = require('body-parser') -const multiparty = require('multiparty') +const multer = require('multer') const { fs } = require('../../lib/util/fs') const e2e = require('../support/helpers/e2e').default const Fixtures = require('../support/helpers/fixtures') @@ -32,34 +31,28 @@ const getFormHtml = (formAttrs, textValue = '') => { } const onServer = function (app) { - app.post('/verify-attachment', (req, res) => { - const form = new multiparty.Form() + app.post('/verify-attachment', multer().any(), async (req, res) => { + const file = req.files[0] - return form.parse(req, (err, fields, files) => { - const fixturePath = path.resolve(e2ePath, 'cypress', 'fixtures', fields['foo'][0]) - const filePath = files['bar'][0].path + const fixturePath = path.resolve(e2ePath, 'cypress', 'fixtures', req.body.foo) - return Promise.props({ - fixture: fs.readFileAsync(fixturePath), - upload: fs.readFileAsync(filePath), - }) - .then(({ fixture, upload }) => { - const ret = fixture.compare(upload) + const fixtureBuf = await fs.readFileAsync(fixturePath) + const uploadBuf = file.buffer - if (ret === 0) { - return res.send('files match') - } + const ret = fixtureBuf.compare(uploadBuf) - return res.send( - `\ -file did not match. file at ${fixturePath} did not match ${filePath}. + if (ret === 0) { + return res.send('files match') + } + + return res.send( + `\ +file did not match. file at ${fixturePath} did not match uploaded buf.

buffer compare yielded: ${ret}\ `, - ) - }) - }) + ) }) // all routes below this point will have bodies parsed diff --git a/packages/server/test/integration/cypress_spec.js b/packages/server/test/integration/cypress_spec.js index 70d92e50ac..32ab362b70 100644 --- a/packages/server/test/integration/cypress_spec.js +++ b/packages/server/test/integration/cypress_spec.js @@ -1163,6 +1163,8 @@ describe('lib/cypress', () => { send: sinon.stub(), } + sinon.stub(chromeBrowser, '_writeExtension').resolves() + sinon.stub(chromeBrowser, '_connectToChromeRemoteInterface').resolves(criClient) // the "returns(resolves)" stub is due to curried method // it accepts URL to visit and then waits for actual CRI client reference diff --git a/packages/server/test/support/fixtures/projects/e2e/cypress/integration/xhr_spec.js b/packages/server/test/support/fixtures/projects/e2e/cypress/integration/xhr_spec.js index 9ad7b90494..99bd99695f 100644 --- a/packages/server/test/support/fixtures/projects/e2e/cypress/integration/xhr_spec.js +++ b/packages/server/test/support/fixtures/projects/e2e/cypress/integration/xhr_spec.js @@ -128,10 +128,6 @@ describe('xhrs', () => { }) }) - it('spawns tasks with original NODE_OPTIONS', () => { - cy.task('assert:http:max:header:size', 8192) - }) - describe('server with 1 visit', () => { before(() => { cy.visit('/xhr.html') 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 c758734b11..95545061b8 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 @@ -1,8 +1,6 @@ require('@packages/ts/register') const _ = require('lodash') -const { expect } = require('chai') -const http = require('http') const Jimp = require('jimp') const path = require('path') const Promise = require('bluebird') @@ -180,11 +178,5 @@ module.exports = (on, config) => { 'get:config:value' (key) { return config[key] }, - - 'assert:http:max:header:size' (expectedBytes) { - expect(http.maxHeaderSize).to.eq(expectedBytes) - - return null - }, }) } diff --git a/packages/server/test/unit/node_options_spec.ts b/packages/server/test/unit/node_options_spec.ts deleted file mode 100644 index 130f372d03..0000000000 --- a/packages/server/test/unit/node_options_spec.ts +++ /dev/null @@ -1,63 +0,0 @@ -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) - }) - }) - }) -}) diff --git a/packages/ts/index.d.ts b/packages/ts/index.d.ts index fafbfb28fe..615efa0188 100644 --- a/packages/ts/index.d.ts +++ b/packages/ts/index.d.ts @@ -24,11 +24,12 @@ import { Url } from 'url' } interface ClientRequest { - _header: { [key: string]: string } + _header?: { [key: string]: string } _implicitHeader: () => void output: string[] agent: Agent insecureHTTPParser: boolean + maxHeaderSize?: number } interface RequestOptions extends ClientRequestArgs { diff --git a/scripts/run-docker-local.sh b/scripts/run-docker-local.sh index f34e945547..cee67c0d1d 100755 --- a/scripts/run-docker-local.sh +++ b/scripts/run-docker-local.sh @@ -3,7 +3,7 @@ set e+x echo "This script should be run from cypress's root" -name=cypress/browsers:node12.18.3-chrome83-ff77 +name=cypress/browsers:node14.16.0-chrome89-ff77 echo "Pulling CI container $name" docker pull $name diff --git a/yarn.lock b/yarn.lock index 3befe3ee0b..7c21bdcc79 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2915,7 +2915,7 @@ "@jest/types@^26.3.0", "@jest/types@^26.6.2": version "26.6.2" - resolved "https://registry.yarnpkg.com/@jest/types/-/types-26.6.2.tgz#bef5a532030e1d88a2f5a6d933f84e97226ed48e" + resolved "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz#bef5a532030e1d88a2f5a6d933f84e97226ed48e" integrity sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ== dependencies: "@types/istanbul-lib-coverage" "^2.0.0" @@ -5411,7 +5411,7 @@ "@types/cheerio@*", "@types/cheerio@0.22.21": version "0.22.21" - resolved "https://registry.yarnpkg.com/@types/cheerio/-/cheerio-0.22.21.tgz#5e37887de309ba11b2e19a6e14cad7874b31a8a3" + resolved "https://registry.npmjs.org/@types/cheerio/-/cheerio-0.22.21.tgz#5e37887de309ba11b2e19a6e14cad7874b31a8a3" integrity sha512-aGI3DfswwqgKPiEOTaiHV2ZPC9KEhprpgEbJnv0fZl3SGX0cGgEva1126dGrMC6AJM6v/aihlUgJn9M5DbDZ/Q== dependencies: "@types/node" "*" @@ -5506,7 +5506,7 @@ "@types/enzyme@*", "@types/enzyme@3.10.5": version "3.10.5" - resolved "https://registry.yarnpkg.com/@types/enzyme/-/enzyme-3.10.5.tgz#fe7eeba3550369eed20e7fb565bfb74eec44f1f0" + resolved "https://registry.npmjs.org/@types/enzyme/-/enzyme-3.10.5.tgz#fe7eeba3550369eed20e7fb565bfb74eec44f1f0" integrity sha512-R+phe509UuUYy9Tk0YlSbipRpfVtIzb/9BHn5pTEtjJTF5LXvUjrIQcZvNyANNEyFrd2YGs196PniNT1fgvOQA== dependencies: "@types/cheerio" "*" @@ -5829,16 +5829,11 @@ resolved "https://registry.yarnpkg.com/@types/mustache/-/mustache-4.1.1.tgz#fcfa2db0cee6261e66f2437dc2fe71e26c7856b4" integrity sha512-Sm0NWeLhS2QL7NNGsXvO+Fgp7e3JLHCO6RS3RCnfjAnkw6Y1bsji/AGfISdQZDIR/AeOyzkrxRk9jBkl55zdJw== -"@types/node@*", "@types/node@>= 8", "@types/node@^14.14.7": +"@types/node@*", "@types/node@14.14.31", "@types/node@>= 8", "@types/node@^14.14.31", "@types/node@^14.14.7", "@types/node@^14.6.2": version "14.14.31" resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.31.tgz#72286bd33d137aa0d152d47ec7c1762563d34055" integrity sha512-vFHy/ezP5qI0rFgJ7aQnjDXwAMrG0KqqIH7tQG5PPv3BWBayOPIQNBjVc/P6hhdZfMx51REc6tfDNXHUio893g== -"@types/node@12.12.50": - version "12.12.50" - resolved "https://registry.yarnpkg.com/@types/node/-/node-12.12.50.tgz#e9b2e85fafc15f2a8aa8fdd41091b983da5fd6ee" - integrity sha512-5ImO01Fb8YsEOYpV+aeyGYztcYcjGsBvN4D7G5r1ef2cuQOpymjWNQi5V0rKHE6PC2ru3HkoUr/Br2/8GUA84w== - "@types/node@14.6.2": version "14.6.2" resolved "https://registry.yarnpkg.com/@types/node/-/node-14.6.2.tgz#264b44c5a28dfa80198fc2f7b6d3c8a054b9491f" @@ -5849,7 +5844,7 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-9.6.49.tgz#ab4df6e505db088882c8ce5417ae0bc8cbb7a8a6" integrity sha512-YY0Okyn4QXC4ugJI+Kng5iWjK8A6eIHiQVaGIhJkyn0YL6Iqo0E0tBC8BuhvYcBK87vykBijM5FtMnCqaa5anA== -"@types/node@^12.0.12", "@types/node@^12.12.29", "@types/node@^12.12.50": +"@types/node@^12.12.29": version "12.20.4" resolved "https://registry.yarnpkg.com/@types/node/-/node-12.20.4.tgz#73687043dd00fcb6962c60fbf499553a24d6bdf2" integrity sha512-xRCgeE0Q4pT5UZ189TJ3SpYuX/QGl6QIAOAIeDSbAVAd2gX1NxSZup4jNVK7cxIeP8KDSbJgcckun495isP1jQ== @@ -7506,6 +7501,11 @@ append-buffer@^1.0.2: dependencies: buffer-equal "^1.0.0" +append-field@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz#1e3440e915f0b1203d23748e78edd7b9b5b43e56" + integrity sha1-HjRA6RXwsSA9I3SOeO3XubW0PlY= + append-transform@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/append-transform/-/append-transform-2.0.0.tgz#99d9d29c7b38391e6f428d28ce136551f0b77e12" @@ -10152,6 +10152,14 @@ builtins@^1.0.0, builtins@^1.0.3: resolved "https://registry.yarnpkg.com/builtins/-/builtins-1.0.3.tgz#cb94faeb61c8696451db36534e1422f94f0aee88" integrity sha1-y5T662HIaWRR2zZTThQi+U8K7og= +busboy@^0.2.11: + version "0.2.14" + resolved "https://registry.npmjs.org/busboy/-/busboy-0.2.14.tgz#6c2a622efcf47c57bbbe1e2a9c37ad36c7925453" + integrity sha1-bCpiLvz0fFe7vh4qnDetNseSVFM= + dependencies: + dicer "0.2.5" + readable-stream "1.1.x" + byline@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/byline/-/byline-5.0.0.tgz#741c5216468eadc457b03410118ad77de8c1ddb1" @@ -13801,6 +13809,14 @@ dezalgo@^1.0.0, dezalgo@~1.0.3: asap "^2.0.0" wrappy "1" +dicer@0.2.5: + version "0.2.5" + resolved "https://registry.npmjs.org/dicer/-/dicer-0.2.5.tgz#5996c086bb33218c812c090bddc09cd12facb70f" + integrity sha1-WZbAhrszIYyBLAkL3cCc0S+stw8= + dependencies: + readable-stream "1.1.x" + streamsearch "0.1.2" + diff-match-patch@^1.0.4: version "1.0.5" resolved "https://registry.yarnpkg.com/diff-match-patch/-/diff-match-patch-1.0.5.tgz#abb584d5f10cd1196dfc55aa03701592ae3f7b37" @@ -14410,13 +14426,13 @@ electron-to-chromium@^1.3.247, electron-to-chromium@^1.3.378, electron-to-chromi resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.682.tgz#f4b5c8d4479df96b61e508a721d6c32c1262ef23" integrity sha512-zok2y37qR00U14uM6qBz/3iIjWHom2eRfC2S1StA0RslP7x34jX+j4mxv80t8OEOHLJPVG54ZPeaFxEI7gPrwg== -electron@11.3.0: - version "11.3.0" - resolved "https://registry.yarnpkg.com/electron/-/electron-11.3.0.tgz#87e8528fd23ae53b0eeb3a738f1fe0a3ad27c2db" - integrity sha512-MhdS0gok3wZBTscLBbYrOhLaQybCSAfkupazbK1dMP5c+84eVMxJE/QGohiWQkzs0tVFIJsAHyN19YKPbelNrQ== +electron@12.0.0: + version "12.0.0" + resolved "https://registry.yarnpkg.com/electron/-/electron-12.0.0.tgz#b3b1d88cc64622e59c637521da5a6b6ab4df4eb5" + integrity sha512-p6oxZ4LG82hopPGAsIMOjyoL49fr6cexyFNH0kADA9Yf+mJ72DN7bjvBG+6V7r6QKhwYgsSsW8RpxBeVOUbxVQ== dependencies: "@electron/get" "^1.0.1" - "@types/node" "^12.0.12" + "@types/node" "^14.6.2" extract-zip "^1.0.3" elegant-spinner@^1.0.1: @@ -16202,7 +16218,7 @@ fbjs@^0.8.1, fbjs@^0.8.16, fbjs@^0.8.9: setimmediate "^1.0.5" ua-parser-js "^0.7.18" -fd-slicer@1.1.0, fd-slicer@~1.1.0: +fd-slicer@~1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.1.0.tgz#25c7c89cb1f9077f8891bbe61d8f390eae256f1e" integrity sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4= @@ -18868,7 +18884,7 @@ http-errors@1.7.2: statuses ">= 1.5.0 < 2" toidentifier "1.0.0" -http-errors@1.7.3, http-errors@~1.7.0, http-errors@~1.7.2: +http-errors@1.7.3, http-errors@~1.7.2: version "1.7.3" resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.3.tgz#6c619e4f9c60308c38519498c14fbb10aacebb06" integrity sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw== @@ -24439,6 +24455,20 @@ ms@2.1.3, ms@^2.0.0, ms@^2.1.1: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== +multer@1.4.2: + version "1.4.2" + resolved "https://registry.npmjs.org/multer/-/multer-1.4.2.tgz#2f1f4d12dbaeeba74cb37e623f234bf4d3d2057a" + integrity sha512-xY8pX7V+ybyUpbYMxtjM9KAiD9ixtg5/JkeKUTD6xilfDv0vzzOFcCp4Ljb1UU3tSOM3VTZtKo63OmzOrGi3Cg== + dependencies: + append-field "^1.0.0" + busboy "^0.2.11" + concat-stream "^1.5.2" + mkdirp "^0.5.1" + object-assign "^4.1.1" + on-finished "^2.3.0" + type-is "^1.6.4" + xtend "^4.0.0" + multicast-dns-service-types@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz#899f11d9686e5e05cb91b35d5f0e63b773cfc901" @@ -24462,16 +24492,6 @@ multimatch@^3.0.0: arrify "^1.0.1" minimatch "^3.0.4" -multiparty@4.2.1: - version "4.2.1" - resolved "https://registry.yarnpkg.com/multiparty/-/multiparty-4.2.1.tgz#d9b6c46d8b8deab1ee70c734b0af771dd46e0b13" - integrity sha512-AvESCnNoQlZiOfP9R4mxN8M9csy2L16EIbWIkt3l4FuGti9kXBS8QVzlfyg4HEnarJhrzZilgNFlZtqmoiAIIA== - dependencies: - fd-slicer "1.1.0" - http-errors "~1.7.0" - safe-buffer "5.1.2" - uid-safe "2.1.5" - mustache@4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/mustache/-/mustache-4.1.0.tgz#8c1b042238a982d2eb2d30efc6c14296ae3f699d" @@ -29341,6 +29361,16 @@ read@1, read@~1.0.1, read@~1.0.7: string_decoder "~1.1.1" util-deprecate "~1.0.1" +readable-stream@1.1.x, readable-stream@~1.1.10: + version "1.1.14" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9" + integrity sha1-fPTFTvZI44EwhMY23SB54WbAgdk= + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.1" + isarray "0.0.1" + string_decoder "~0.10.x" + "readable-stream@2 || 3", readable-stream@3, "readable-stream@>= 0.3.0", readable-stream@^3.0.0, readable-stream@^3.0.2, readable-stream@^3.0.6, readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.5.0, readable-stream@^3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" @@ -29360,16 +29390,6 @@ readable-stream@~1.0.2: isarray "0.0.1" string_decoder "~0.10.x" -readable-stream@~1.1.10: - version "1.1.14" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9" - integrity sha1-fPTFTvZI44EwhMY23SB54WbAgdk= - dependencies: - core-util-is "~1.0.0" - inherits "~2.0.1" - isarray "0.0.1" - string_decoder "~0.10.x" - readdir-glob@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/readdir-glob/-/readdir-glob-1.1.1.tgz#f0e10bb7bf7bfa7e0add8baffdc54c3f7dbee6c4" @@ -31610,7 +31630,7 @@ socket.io-client@3.0.4: socket.io-parser@4.0.2, socket.io-parser@~3.3.0, socket.io-parser@~3.4.0, socket.io-parser@~4.0.1: version "4.0.2" - resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.0.2.tgz#3d021a9c86671bb079e7c6c806db6a1d9b1bc780" + resolved "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.0.2.tgz#3d021a9c86671bb079e7c6c806db6a1d9b1bc780" integrity sha512-Bs3IYHDivwf+bAAuW/8xwJgIiBNtlvnjYRc4PbXgniLmcP1BrakBoq/QhO24rgtgW7VZ7uAaswRGxutUnlAK7g== dependencies: "@types/component-emitter" "^1.2.10" @@ -31770,6 +31790,7 @@ source-list-map@^2.0.0: integrity sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw== "source-map-fast@npm:source-map@0.7.3", source-map@0.7.3, source-map@^0.7.3: + name source-map-fast version "0.7.3" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383" integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ== @@ -32330,6 +32351,11 @@ stream-splicer@^2.0.0: inherits "^2.0.1" readable-stream "^2.0.2" +streamsearch@0.1.2: + version "0.1.2" + resolved "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz#808b9d0e56fc273d809ba57338e929919a1a9f1a" + integrity sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo= + strict-uri-encode@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713" @@ -33991,7 +34017,7 @@ type-fest@^0.8.0, type-fest@^0.8.1: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== -type-is@^1.6.16, type-is@~1.6.17, type-is@~1.6.18: +type-is@^1.6.16, type-is@^1.6.4, type-is@~1.6.17, type-is@~1.6.18: version "1.6.18" resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== @@ -34102,7 +34128,7 @@ uid-number@0.0.6: resolved "https://registry.yarnpkg.com/uid-number/-/uid-number-0.0.6.tgz#0ea10e8035e8eb5b8e4449f06da1c730663baa81" integrity sha1-DqEOgDXo61uOREnwbaHHMGY7qoE= -uid-safe@2.1.5, uid-safe@~2.1.5: +uid-safe@~2.1.5: version "2.1.5" resolved "https://registry.yarnpkg.com/uid-safe/-/uid-safe-2.1.5.tgz#2b3d5c7240e8fc2e58f8aa269e5ee49c0857bd3a" integrity sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA== From f31013d5b67eb115472a1d334eaaf6086b5ed6fa Mon Sep 17 00:00:00 2001 From: Jeremy Dorne Date: Wed, 10 Mar 2021 12:06:37 -0800 Subject: [PATCH 018/134] fix(net-stubbing): Provide consistent behavior for falsy static responses (#15235) Co-authored-by: Zach Bloomquist --- .../integration/commands/net_stubbing_spec.ts | 12 ++++++++++++ packages/driver/src/cy/net-stubbing/add-command.ts | 9 ++++----- .../src/cy/net-stubbing/static-response-utils.ts | 2 +- packages/net-stubbing/lib/external-types.ts | 2 +- packages/net-stubbing/lib/server/util.ts | 4 ++-- 5 files changed, 20 insertions(+), 9 deletions(-) diff --git a/packages/driver/cypress/integration/commands/net_stubbing_spec.ts b/packages/driver/cypress/integration/commands/net_stubbing_spec.ts index 0c4ca4d28e..826a83940e 100644 --- a/packages/driver/cypress/integration/commands/net_stubbing_spec.ts +++ b/packages/driver/cypress/integration/commands/net_stubbing_spec.ts @@ -738,6 +738,18 @@ describe('network stubbing', { retries: { runMode: 2, openMode: 0 } }, function }) }) + it('does not drop falsy static responses', function (done) { + cy.intercept({ + url: '*', + }, { body: false }).then(() => { + $.get('/abc123').done((responseText, _, xhr) => { + expect(xhr.status).to.eq(200) + expect(responseText).to.eq(false) + done() + }) + }) + }) + // TODO: flaky - unable to reproduce outside of CI it('still works after a cy.visit', { retries: 2 }, function () { cy.intercept(/foo/, { diff --git a/packages/driver/src/cy/net-stubbing/add-command.ts b/packages/driver/src/cy/net-stubbing/add-command.ts index 9a87171718..df5cf0e66f 100644 --- a/packages/driver/src/cy/net-stubbing/add-command.ts +++ b/packages/driver/src/cy/net-stubbing/add-command.ts @@ -159,7 +159,6 @@ export function addCommand (Commands, Cypress: Cypress.Cypress, cy: Cypress.cy, instrument: 'route', isStubbed, numResponses: 0, - response: staticResponse ? (staticResponse.body || '< empty body >') : (isStubbed ? '< callback function >' : '< passthrough >'), consoleProps: () => { return { Method: obj.method, @@ -188,17 +187,17 @@ export function addCommand (Commands, Cypress: Cypress.Cypress, cy: Cypress.cy, } if (staticResponse.body) { - obj.response = staticResponse.body + obj.response = String(staticResponse.body) } else { - obj.response = '' + obj.response = '< empty body >' } } if (!obj.response) { if (isStubbed) { - obj.response = ' +export type StaticResponse = GenericStaticResponse export interface GenericStaticResponse { /** diff --git a/packages/net-stubbing/lib/server/util.ts b/packages/net-stubbing/lib/server/util.ts index 456e1bc5cb..db153cb1d1 100644 --- a/packages/net-stubbing/lib/server/util.ts +++ b/packages/net-stubbing/lib/server/util.ts @@ -158,7 +158,7 @@ export function sendStaticResponse (backendRequest: Pick Date: Wed, 10 Mar 2021 16:04:44 -0500 Subject: [PATCH 019/134] feat: Add command log entry for uncaught exceptions (#15344) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Zach Bloomquist Co-authored-by: Mateusz Burzyński --- packages/driver/cypress/fixtures/errors.html | 2 +- .../integration/cypress/error_utils_spec.js | 93 +++++++- .../integration/e2e/uncaught_errors_spec.js | 33 ++- packages/driver/src/cy/commands/navigation.js | 75 +++---- packages/driver/src/cy/commands/xhr.js | 18 +- packages/driver/src/cy/errors.js | 71 ------ .../cy/net-stubbing/events/before-request.ts | 6 - .../src/cy/net-stubbing/events/index.ts | 2 +- .../src/cy/net-stubbing/events/response.ts | 6 - packages/driver/src/cypress.js | 2 +- packages/driver/src/cypress/cy.js | 204 +++++++++++------- packages/driver/src/cypress/error_utils.js | 54 ++++- packages/driver/src/cypress/runner.js | 49 ++--- packages/reporter/src/commands/commands.scss | 7 + .../cypress/fixtures/errors/intercept_spec.ts | 111 +++++++--- .../errors/uncaught_onRunnable_spec.js | 12 -- .../uncaught_outside_test_only_suite_spec.js | 14 ++ .../errors/uncaught_outside_test_spec.js | 7 + .../cypress/fixtures/errors/uncaught_spec.js | 12 ++ .../integration/reporter.errors.spec.js | 134 ++++++++---- .../runner/cypress/support/verify-failures.js | 79 +++++-- 21 files changed, 653 insertions(+), 338 deletions(-) delete mode 100644 packages/driver/src/cy/errors.js delete mode 100644 packages/runner/cypress/fixtures/errors/uncaught_onRunnable_spec.js create mode 100644 packages/runner/cypress/fixtures/errors/uncaught_outside_test_only_suite_spec.js create mode 100644 packages/runner/cypress/fixtures/errors/uncaught_outside_test_spec.js diff --git a/packages/driver/cypress/fixtures/errors.html b/packages/driver/cypress/fixtures/errors.html index 9e93a1393a..af9eda1863 100644 --- a/packages/driver/cypress/fixtures/errors.html +++ b/packages/driver/cypress/fixtures/errors.html @@ -54,7 +54,7 @@ document.querySelector(".trigger-async-error").addEventListener('click', function () { setTimeout(function () { one('async error') - }, 0) + }, 100) }) document.querySelector(".trigger-unhandled-rejection").addEventListener('click', function () { diff --git a/packages/driver/cypress/integration/cypress/error_utils_spec.js b/packages/driver/cypress/integration/cypress/error_utils_spec.js index 976a8037cd..df4cea9264 100644 --- a/packages/driver/cypress/integration/cypress/error_utils_spec.js +++ b/packages/driver/cypress/integration/cypress/error_utils_spec.js @@ -458,40 +458,68 @@ describe('driver/src/cypress/error_utils', () => { context('.createUncaughtException', () => { let err + let state beforeEach(() => { err = new Error('original message') err.stack = 'Error: original message\n\nat foo (path/to/file:1:1)' + + state = cy.stub() }) it('mutates the error passed in and returns it', () => { - const result = $errUtils.createUncaughtException('spec', 'error', err) + const result = $errUtils.createUncaughtException({ + frameType: 'spec', + handlerType: 'error', + state, + err, + }) expect(result).to.equal(err) }) it('replaces message with wrapper message for spec error', () => { - const result = $errUtils.createUncaughtException('spec', 'error', err) + const result = $errUtils.createUncaughtException({ + frameType: 'spec', + handlerType: 'error', + state, + err, + }) expect(result.message).to.include('The following error originated from your test code, not from Cypress') expect(result.message).to.include('> original message') }) it('replaces message with wrapper message for app error', () => { - const result = $errUtils.createUncaughtException('app', 'error', err) + const result = $errUtils.createUncaughtException({ + frameType: 'app', + handlerType: 'error', + state, + err, + }) expect(result.message).to.include('The following error originated from your application code, not from Cypress') expect(result.message).to.include('> original message') }) it('replaces original name and message in stack', () => { - const result = $errUtils.createUncaughtException('spec', 'error', err) + const result = $errUtils.createUncaughtException({ + frameType: 'spec', + handlerType: 'error', + state, + err, + }) expect(result.stack).not.to.include('Error: original message') }) it('retains the stack of the original error', () => { - const result = $errUtils.createUncaughtException('spec', 'error', err) + const result = $errUtils.createUncaughtException({ + frameType: 'spec', + handlerType: 'error', + state, + err, + }) expect(result.stack).to.include('at foo (path/to/file:1:1)') }) @@ -499,13 +527,66 @@ describe('driver/src/cypress/error_utils', () => { it('adds docsUrl for app error and original error', () => { err.docsUrl = 'https://on.cypress.io/orginal-error-docs-url' - const result = $errUtils.createUncaughtException('app', 'error', err) + const result = $errUtils.createUncaughtException({ + frameType: 'app', + handlerType: 'error', + state, + err, + }) expect(result.docsUrl).to.eql([ 'https://on.cypress.io/uncaught-exception-from-application', 'https://on.cypress.io/orginal-error-docs-url', ]) }) + + it('logs error with onFail fn when logs exists', () => { + const errorStub = cy.stub() + + state.returns({ + getLastLog: () => ({ error: errorStub }), + }) + + const result = $errUtils.createUncaughtException({ + frameType: 'spec', + handlerType: 'error', + state, + err, + }) + + result.onFail() + expect(errorStub).to.be.calledWith(result) + }) + + it('does not error if no last log', () => { + state.returns({ + getLastLog: () => {}, + }) + + const result = $errUtils.createUncaughtException({ + frameType: 'spec', + handlerType: 'error', + state, + err, + }) + + result.onFail() + // expect no error + }) + + it('does not error if no current command', () => { + state.returns(undefined) + + const result = $errUtils.createUncaughtException({ + frameType: 'spec', + handlerType: 'error', + state, + err, + }) + + result.onFail() + // expect no error + }) }) context('Error.captureStackTrace', () => { diff --git a/packages/driver/cypress/integration/e2e/uncaught_errors_spec.js b/packages/driver/cypress/integration/e2e/uncaught_errors_spec.js index cbc2b12b15..84c59c3030 100644 --- a/packages/driver/cypress/integration/e2e/uncaught_errors_spec.js +++ b/packages/driver/cypress/integration/e2e/uncaught_errors_spec.js @@ -22,18 +22,14 @@ describe('uncaught errors', () => { const r = cy.state('runnable') cy.on('uncaught:exception', (err, runnable) => { - try { - expect(err.name).to.eq('Error') - expect(err.message).to.include('sync error') - expect(err.message).to.include('The following error originated from your application code, not from Cypress.') - expect(err.message).to.not.include('https://on.cypress.io/uncaught-exception-from-application') - expect(err.docsUrl).to.deep.eq(['https://on.cypress.io/uncaught-exception-from-application']) - expect(runnable === r).to.be.true + expect(err.name).to.eq('Error') + expect(err.message).to.include('sync error') + expect(err.message).to.include('The following error originated from your application code, not from Cypress.') + expect(err.message).to.not.include('https://on.cypress.io/uncaught-exception-from-application') + expect(err.docsUrl).to.deep.eq(['https://on.cypress.io/uncaught-exception-from-application']) + expect(runnable === r).to.be.true - return false - } catch (err2) { - return true - } + return false }) cy.visit('/fixtures/errors.html') @@ -62,6 +58,8 @@ describe('uncaught errors', () => { expect(err.stack).to.include('three') done() + + return false }) cy.visit('/fixtures/errors.html') @@ -77,6 +75,8 @@ describe('uncaught errors', () => { expect(err.stack).to.include('three') done() + + return false }) cy.visit('/fixtures/errors.html') @@ -106,6 +106,8 @@ describe('uncaught errors', () => { expect(promise).to.be.a('promise') done() + + return false }) cy.visit('/fixtures/errors.html') @@ -172,4 +174,13 @@ describe('uncaught errors', () => { }) }).get('button:first').click() }) + + it('fails test based on an uncaught error after last command and before completing', (done) => { + cy.on('fail', () => { + done() + }) + + cy.visit('/fixtures/errors.html') + cy.get('.trigger-async-error').click() + }) }) diff --git a/packages/driver/src/cy/commands/navigation.js b/packages/driver/src/cy/commands/navigation.js index c5b910cd6b..b7ba8dcb78 100644 --- a/packages/driver/src/cy/commands/navigation.js +++ b/packages/driver/src/cy/commands/navigation.js @@ -451,19 +451,6 @@ module.exports = (Commands, Cypress, cy, state, config) => { } Cypress.on('window:before:load', (contentWindow) => { - // TODO: just use a closure here - const current = state('current') - - if (!current) { - return - } - - const runnable = state('runnable') - - if (!runnable) { - 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 @@ -480,10 +467,6 @@ module.exports = (Commands, Cypress, cy, state, config) => { 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) }) Commands.addAll({ @@ -785,29 +768,47 @@ module.exports = (Commands, Cypress, cy, state, config) => { const runnable = state('runnable') const changeIframeSrc = (url, event) => { - // when the remote iframe's load event fires - // callback fn - return new Promise((resolve) => { - // if we're listening for hashchange - // events then change the strategy - // to listen to this event emitting - // from the window and not cy - // see issue 652 for why. - // the hashchange events are firing too - // fast for us. They even resolve asynchronously - // before other application's hashchange events - // have even fired. + return new Promise((resolve, reject) => { + let onBeforeLoadError + + const onBeforeLoad = (contentWindow) => { + try { + options.onBeforeLoad?.call(runnable.ctx, contentWindow) + } catch (err) { + err.isCallbackError = true + onBeforeLoadError = err + } + } + + const onEvent = (contentWindow) => { + if (onBeforeLoadError) { + reject(onBeforeLoadError) + } else { + resolve(contentWindow) + } + } + + // hashchange events fire too fast, so we use a different strategy. + // they even resolve asynchronously before the application's + // hashchange events have even fired + // @see https://github.com/cypress-io/cypress/issues/652 + // also, the page doesn't fully reload on hashchange, so we + // can't and don't wait for before:window:load if (event === 'hashchange') { win.addEventListener('hashchange', resolve) } else { - cy.once(event, resolve) + // listen for window:before:load and reject if it errors + // otherwise, resolve once this event fires + cy.once(event, onEvent) + cy.once('window:before:load', onBeforeLoad) } cleanup = () => { if (event === 'hashchange') { win.removeEventListener('hashchange', resolve) } else { - cy.removeListener(event, resolve) + cy.removeListener(event, onEvent) + cy.removeListener('window:before:load', onBeforeLoad) } knownCommandCausedInstability = false @@ -828,9 +829,9 @@ module.exports = (Commands, Cypress, cy, state, config) => { try { options.onLoad?.call(runnable.ctx, win) } catch (err) { - // mark these as onLoad errors, so they're treated differently + // mark these as user callback errors, so they're treated differently // than Node.js errors when caught below - err.isOnLoadError = true + err.isCallbackError = true throw err } } @@ -1047,10 +1048,10 @@ module.exports = (Commands, Cypress, cy, state, config) => { return } - // if it came from the user's onLoad callback, it's not a network - // failure, and we should just throw the original error - if (err.isOnLoadError) { - delete err.isOnLoadError + // if it came from the user's onBeforeLoad or onLoad callback, it's + // not a network failure, and we should throw the original error + if (err.isCallbackError) { + delete err.isCallbackError throw err } diff --git a/packages/driver/src/cy/commands/xhr.js b/packages/driver/src/cy/commands/xhr.js index bd1d77310d..9cec79b4b4 100644 --- a/packages/driver/src/cy/commands/xhr.js +++ b/packages/driver/src/cy/commands/xhr.js @@ -249,20 +249,32 @@ const startXhrServer = (cy, state, config) => { }, onAnyAbort: (route, xhr) => { - if (route && _.isFunction(route.onAbort)) { + if (!route || !_.isFunction(route.onAbort)) return + + try { return route.onAbort.call(cy, xhr) + } catch (err) { + cy.fail(err, { async: true }) } }, onAnyRequest: (route, xhr) => { - if (route && _.isFunction(route.onRequest)) { + if (!route || !_.isFunction(route.onRequest)) return + + try { return route.onRequest.call(cy, xhr) + } catch (err) { + cy.fail(err, { async: true }) } }, onAnyResponse: (route, xhr) => { - if (route && _.isFunction(route.onResponse)) { + if (!route || !_.isFunction(route.onResponse)) return + + try { return route.onResponse.call(cy, xhr) + } catch (err) { + cy.fail(err, { async: true }) } }, }) diff --git a/packages/driver/src/cy/errors.js b/packages/driver/src/cy/errors.js deleted file mode 100644 index 02f0aee777..0000000000 --- a/packages/driver/src/cy/errors.js +++ /dev/null @@ -1,71 +0,0 @@ -const $dom = require('../dom') -const $errUtils = require('../cypress/error_utils') - -const create = (state, log) => { - const commandErr = (err) => { - const current = state('current') - - return log({ - end: true, - snapshot: true, - error: err, - consoleProps () { - if (!current) return - - const obj = {} - const prev = current.get('prev') - - // if type isnt parent then we know its dual or child - // and we can add Applied To if there is a prev command - // and it is a parent - if (current.get('type') !== 'parent' && prev) { - const ret = $dom.isElement(prev.get('subject')) ? - $dom.getElements(prev.get('subject')) - : - prev.get('subject') - - obj['Applied To'] = ret - - return obj - } - }, - }) - } - - const createUncaughtException = (frameType, handlerType, originalErr) => { - const err = $errUtils.createUncaughtException(frameType, handlerType, originalErr) - const current = state('current') - - err.onFail = () => { - current?.getLastLog()?.error(err) - } - - return err - } - - const commandRunningFailed = (err) => { - // allow for our own custom onFail function - if (err.onFail) { - err.onFail(err) - - // clean up this onFail callback - // after its been called - delete err.onFail - } else { - commandErr(err) - } - } - - return { - // submit a generic command error - commandErr, - - commandRunningFailed, - - createUncaughtException, - } -} - -module.exports = { - create, -} diff --git a/packages/driver/src/cy/net-stubbing/events/before-request.ts b/packages/driver/src/cy/net-stubbing/events/before-request.ts index 778a9550c3..c2e14285b2 100644 --- a/packages/driver/src/cy/net-stubbing/events/before-request.ts +++ b/packages/driver/src/cy/net-stubbing/events/before-request.ts @@ -204,12 +204,6 @@ export const onBeforeRequest: HandlerFn = (Cypre req, route: route.options, }, - errProps: { - appendToStack: { - title: 'From request callback', - content: err.stack, - }, - }, }) }) .timeout(timeout) diff --git a/packages/driver/src/cy/net-stubbing/events/index.ts b/packages/driver/src/cy/net-stubbing/events/index.ts index 352f2584aa..561627c36c 100644 --- a/packages/driver/src/cy/net-stubbing/events/index.ts +++ b/packages/driver/src/cy/net-stubbing/events/index.ts @@ -53,7 +53,7 @@ export function registerEvents (Cypress: Cypress.Cypress, cy: Cypress.cy) { function failCurrentTest (err: Error) { // @ts-ignore - cy.fail(err) + cy.fail(err, { async: true }) } Cypress.on('test:before:run', () => { diff --git a/packages/driver/src/cy/net-stubbing/events/response.ts b/packages/driver/src/cy/net-stubbing/events/response.ts index 99028839a6..a79f7621cf 100644 --- a/packages/driver/src/cy/net-stubbing/events/response.ts +++ b/packages/driver/src/cy/net-stubbing/events/response.ts @@ -127,12 +127,6 @@ export const onResponse: HandlerFn = async (Cyp route: _.get(getRoute(routeHandlerId), 'options'), res, }, - errProps: { - appendToStack: { - title: 'From response callback', - content: err.stack, - }, - }, }) }) .timeout(timeout) diff --git a/packages/driver/src/cypress.js b/packages/driver/src/cypress.js index 78b8fe725f..566c47cccd 100644 --- a/packages/driver/src/cypress.js +++ b/packages/driver/src/cypress.js @@ -199,7 +199,7 @@ class $Cypress { this.isCy = this.cy.isCy this.log = $Log.create(this, this.cy, this.state, this.config) this.mocha = $Mocha.create(specWindow, this, this.config) - this.runner = $Runner.create(specWindow, this.mocha, this, this.cy) + this.runner = $Runner.create(specWindow, this.mocha, this, this.cy, this.state) this.downloads = $Downloads.create(this) // wire up command create to cy diff --git a/packages/driver/src/cypress/cy.js b/packages/driver/src/cypress/cy.js index 6804d44b7e..f060e69d8f 100644 --- a/packages/driver/src/cypress/cy.js +++ b/packages/driver/src/cypress/cy.js @@ -2,6 +2,7 @@ const _ = require('lodash') const $ = require('jquery') const Promise = require('bluebird') +const debugErrors = require('debug')('cypress:driver:errors') const $dom = require('../dom') const $utils = require('./utils') @@ -12,7 +13,6 @@ const $Xhrs = require('../cy/xhrs') const $jQuery = require('../cy/jquery') const $Aliases = require('../cy/aliases') const $Events = require('./events') -const $Errors = require('../cy/errors') const $Ensures = require('../cy/ensures') const $Focused = require('../cy/focused') const $Mouse = require('../cy/mouse') @@ -66,7 +66,7 @@ function __stackReplacementMarker (fn, ctx, args) { // We only set top.onerror once since we make it configurable:false // but we update cy instance every run (page reload or rerun button) let curCy = null -const setTopOnError = function (Cypress, cy, errors) { +const setTopOnError = function (Cypress, cy) { if (curCy) { curCy = cy @@ -83,20 +83,27 @@ const setTopOnError = function (Cypress, cy, errors) { // eslint-disable-next-line @cypress/dev/arrow-body-multiline-braces const onTopError = (handlerType) => (event) => { - const [err, promise] = handlerType === 'error' ? - $errUtils.errorFromErrorEvent(event) : - $errUtils.errorFromProjectRejectionEvent(event) + const { originalErr, err, promise } = $errUtils.errorFromUncaughtEvent(handlerType, event) // in some callbacks like for cy.intercept, we catch the errors and then // rethrow them, causing them to get caught by the top frame // but they came from the spec, so we need to differentiate them const isSpecError = $errUtils.isSpecError(Cypress.config('spec'), err) - if (isSpecError) { - return curCy.onSpecWindowUncaughtException(handlerType, err, promise) - } + const handled = curCy.onUncaughtException({ + err, + promise, + handlerType, + frameType: isSpecError ? 'spec' : 'app', + }) - return curCy.onUncaughtException(handlerType, err, promise) + debugErrors('uncaught top error: %o', originalErr) + + $errUtils.logError(Cypress, handlerType, originalErr, handled) + + // return undefined so the browser does its default + // uncaught exception behavior (logging to console) + return undefined } top.addEventListener('error', onTopError('error')) @@ -114,6 +121,46 @@ const setTopOnError = function (Cypress, cy, errors) { top.__alreadySetErrorHandlers__ = true } +const commandRunningFailed = (Cypress, state, err) => { + // allow for our own custom onFail function + if (err.onFail) { + err.onFail(err) + + // clean up this onFail callback after it's been called + delete err.onFail + + return + } + + const current = state('current') + + return Cypress.log({ + end: true, + snapshot: true, + error: err, + consoleProps () { + if (!current) return + + const obj = {} + const prev = current.get('prev') + + // if type isnt parent then we know its dual or child + // and we can add Applied To if there is a prev command + // and it is a parent + if (current.get('type') !== 'parent' && prev) { + const ret = $dom.isElement(prev.get('subject')) ? + $dom.getElements(prev.get('subject')) + : + prev.get('subject') + + obj['Applied To'] = ret + + return obj + } + }, + }) +} + // NOTE: this makes the cy object an instance // TODO: refactor the 'create' method below into this class class $Cy {} @@ -167,7 +214,6 @@ const create = function (specWindow, Cypress, Cookies, state, config, log) { const xhrs = $Xhrs.create(state) const aliases = $Aliases.create(cy) - const errors = $Errors.create(state, log) const ensures = $Ensures.create(state, expect) const snapshots = $Snapshots.create($$, state) @@ -191,14 +237,21 @@ const create = function (specWindow, Cypress, Cookies, state, config, log) { return $Listeners.bindTo(contentWindow, { // eslint-disable-next-line @cypress/dev/arrow-body-multiline-braces onError: (handlerType) => (event) => { - const [err, promise] = handlerType === 'error' ? - $errUtils.errorFromErrorEvent(event) : - $errUtils.errorFromProjectRejectionEvent(event) + const { originalErr, err, promise } = $errUtils.errorFromUncaughtEvent(handlerType, event) + const handled = cy.onUncaughtException({ + err, + promise, + handlerType, + frameType: 'app', + }) - // use a function callback here instead of direct - // reference so our users can override this function - // if need be - return cy.onUncaughtException(handlerType, err, promise) + debugErrors('uncaught AUT error: %o', originalErr) + + $errUtils.logError(Cypress, handlerType, originalErr, handled) + + // return undefined so the browser does its default + // uncaught exception behavior (logging to console) + return undefined }, onSubmit (e) { return Cypress.action('app:form:submitted', e) @@ -606,11 +659,13 @@ const create = function (specWindow, Cypress, Cookies, state, config, log) { state('reject', rejectOuterAndCancelInner) }) .catch((err) => { + debugErrors('caught error in promise chain: %o', err) + // since this failed this means that a // specific command failed and we should // highlight it in red or insert a new command err.name = err.name || 'CypressError' - errors.commandRunningFailed(err) + commandRunningFailed(Cypress, state, err) return fail(err) }) @@ -754,7 +809,20 @@ const create = function (specWindow, Cypress, Cookies, state, config, log) { } } - const fail = (err) => { + const fail = (err, options = {}) => { + // this means the error has already been through this handler and caught + // again. but we don't need to run it through again, so we can re-throw + // it and it will fail the test as-is + if (err && err.hasFailed) { + delete err.hasFailed + + throw err + } + + options = _.defaults(options, { + async: false, + }) + let rets stopped = true @@ -773,24 +841,30 @@ const create = function (specWindow, Cypress, Cookies, state, config, log) { err = $errUtils.processErr(err, config) + err.hasFailed = true + // store the error on state now state('error', err) const finish = function (err) { - // if we have an async done callback - // we have an explicit (done) callback and - // we aren't attached to the cypress command queue - // promise chain and throwing the error would only - // result in an unhandled rejection + // if the test has a (done) callback, we fail the test with that const d = state('done') if (d) { - // invoke it with err return d(err) } - // else we're connected to the promise chain - // and need to throw so this bubbles up + // if this failure was asynchronously called (outside the promise chain) + // but the promise chain is still active, reject it. if we're inside + // the promise chain, this isn't necessary and will actually mess it up + const r = state('reject') + + if (options.async && r) { + return r(err) + } + + // we're in the promise chain, so throw the error and it will + // get caught by mocha and fail the test throw err } @@ -1222,21 +1296,39 @@ const create = function (specWindow, Cypress, Cookies, state, config, log) { snapshots.onBeforeWindowLoad() }, - onSpecWindowUncaughtException (handlerType, err, promise) { - err = errors.createUncaughtException('spec', handlerType, err) + onUncaughtException ({ handlerType, frameType, err, promise }) { + err = $errUtils.createUncaughtException({ + handlerType, + frameType, + state, + err, + }) const runnable = state('runnable') - if (!runnable) return err + // don't do anything if we don't have a current runnable + if (!runnable) return - if (config('componentTesting')) { - // in component testing, uncaught exceptions should be catchable, as there is no AUT - const results = Cypress.action('app:uncaught:exception', err, runnable, promise) + // uncaught exceptions should be only be catchable in the AUT (app) + // or if in component testing mode, since then the spec frame and + // AUT frame are the same + if (frameType === 'app' || config('componentTesting')) { + try { + const results = Cypress.action('app:uncaught:exception', err, runnable, promise) - // dont do anything if any of our uncaught:exception - // listeners returned false - if (_.some(results, returnedFalse)) { - return + // dont do anything if any of our uncaught:exception + // listeners returned false + if (_.some(results, returnedFalse)) { + // return true to signal that the user handled this error + return true + } + } catch (uncaughtExceptionErr) { + err = $errUtils.createUncaughtException({ + err: uncaughtExceptionErr, + handlerType: 'error', + frameType: 'spec', + state, + }) } } @@ -1246,45 +1338,11 @@ const create = function (specWindow, Cypress, Cookies, state, config, log) { const r = state('reject') if (r) { - return r(err) + r(err) } - - return failErr } }, - onUncaughtException (handlerType, err, promise) { - err = errors.createUncaughtException('app', handlerType, err) - - const runnable = state('runnable') - - // don't do anything if we don't have a current runnable - if (!runnable) { - return - } - - const results = Cypress.action('app:uncaught:exception', err, runnable, promise) - - // dont do anything if any of our uncaught:exception - // listeners returned false - if (_.some(results, returnedFalse)) { - return - } - - // do all the normal fail stuff and promise cancelation - // but dont re-throw the error - let r = state('reject') - - if (r) { - r(err) - } - - // per the onerror docs we need to return true here - // https://developer.mozilla.org/en-US/docs/Web/API/GlobalEventHandlers/onerror - // When the function returns true, this prevents the firing of the default event handler. - return true - }, - detachDom (...args) { return snapshots.detachDom(...args) }, @@ -1431,7 +1489,7 @@ const create = function (specWindow, Cypress, Cookies, state, config, log) { }, }) - setTopOnError(Cypress, cy, errors) + setTopOnError(Cypress, cy) // make cy global in the specWindow specWindow.cy = cy diff --git a/packages/driver/src/cypress/error_utils.js b/packages/driver/src/cypress/error_utils.js index 78090aaf93..289f4fc9c7 100644 --- a/packages/driver/src/cypress/error_utils.js +++ b/packages/driver/src/cypress/error_utils.js @@ -304,7 +304,7 @@ const errByPath = (msgPath, args) => { }) } -const createUncaughtException = (frameType, handlerType, err) => { +const createUncaughtException = ({ frameType, handlerType, state, err }) => { const errPath = frameType === 'spec' ? 'uncaught.fromSpec' : 'uncaught.fromApp' let uncaughtErr = errByPath(errPath, { errMsg: err.message, @@ -315,6 +315,12 @@ const createUncaughtException = (frameType, handlerType, err) => { err.docsUrl = _.compact([uncaughtErr.docsUrl, err.docsUrl]) + const current = state('current') + + err.onFail = () => { + current?.getLastLog()?.error(err) + } + return err } @@ -419,7 +425,10 @@ const errorFromErrorEvent = (event) => { err.docsUrl = docsUrl // makeErrFromObj clones the error, so the original doesn't get mutated - return [makeErrFromObj(err)] + return { + originalErr: err, + err: makeErrFromObj(err), + } } const errorFromProjectRejectionEvent = (event) => { @@ -431,7 +440,40 @@ const errorFromProjectRejectionEvent = (event) => { } // makeErrFromObj clones the error, so the original doesn't get mutated - return [makeErrFromObj(event.reason), event.promise] + return { + originalErr: event.reason, + err: makeErrFromObj(event.reason), + promise: event.promise, + } +} + +const errorFromUncaughtEvent = (handlerType, event) => { + return handlerType === 'error' ? + errorFromErrorEvent(event) : + errorFromProjectRejectionEvent(event) +} + +const logError = (Cypress, handlerType, err, handled = false) => { + Cypress.log({ + message: `${err.name}: ${err.message}`, + name: 'uncaught exception', + type: 'parent', + // specifying the error causes the log to be red/failed + // otherwise, if it's been handled, we omit the error so it is grey/passed + error: handled ? undefined : err, + snapshot: true, + event: true, + timeout: 0, + end: true, + consoleProps: () => { + const consoleObj = { + 'Caught By': `"${handlerType}" handler`, + 'Error': err, + } + + return consoleObj + }, + }) } module.exports = { @@ -441,10 +483,13 @@ module.exports = { cypressErrByPath, enhanceStack, errByPath, + errorFromUncaughtEvent, + getUserInvocationStack, isAssertionErr, isChaiValidationErr, isCypressErr, isSpecError, + logError, makeErrFromObj, mergeErrProps, modifyErrMsg, @@ -453,7 +498,4 @@ module.exports = { throwErrByPath, warnByPath, wrapErr, - getUserInvocationStack, - errorFromErrorEvent, - errorFromProjectRejectionEvent, } diff --git a/packages/driver/src/cypress/runner.js b/packages/driver/src/cypress/runner.js index 1694f79334..f17325ea45 100644 --- a/packages/driver/src/cypress/runner.js +++ b/packages/driver/src/cypress/runner.js @@ -21,6 +21,7 @@ const RUNNABLE_LOGS = 'routes agents commands hooks'.split(' ') const RUNNABLE_PROPS = 'id order title root hookName hookId err state failedFromHookId body speed type duration wallClockStartedAt wallClockDuration timings file originalTitle invocationDetails final currentRetry retries'.split(' ') const debug = require('debug')('cypress:driver:runner') +const debugErrors = require('debug')('cypress:driver:errors') const fire = (event, runnable, Cypress) => { debug('fire: %o', { event }) @@ -967,7 +968,7 @@ const _runnerListeners = (_runner, Cypress, _emissions, getTestById, getTest, se }) } -const create = (specWindow, mocha, Cypress, cy) => { +const create = (specWindow, mocha, Cypress, cy, state) => { let _runnableId = 0 let _hookId = 0 let _uncaughtFn = null @@ -1006,43 +1007,43 @@ const create = (specWindow, mocha, Cypress, cy) => { // eslint-disable-next-line @cypress/dev/arrow-body-multiline-braces const onSpecError = (handlerType) => (event) => { - const [originalErr] = handlerType === 'error' ? - $errUtils.errorFromErrorEvent(event) : - $errUtils.errorFromProjectRejectionEvent(event) + let { originalErr, err } = $errUtils.errorFromUncaughtEvent(handlerType, event) - let err = cy.onSpecWindowUncaughtException(handlerType, originalErr) + debugErrors('uncaught spec error: %o', originalErr) + + $errUtils.logError(Cypress, handlerType, originalErr) + + // we can stop here because this error will fail the current test + if (state('runnable')) { + cy.onUncaughtException({ + frameType: 'spec', + handlerType, + err, + }) - // err will not be returned if cy can associate this - // uncaught exception to an existing runnable - if (!err) { return undefined } - const todoMsg = () => { - if (!Cypress.config('isTextTerminal')) { - return 'Check your console for the stack trace or click this message to see where it originated from.' - } - } + err = $errUtils.createUncaughtException({ + frameType: 'spec', + handlerType, + state, + err, + }) - const appendMsg = _.chain([ + // otherwise there's no test to associate this error to + const appendMsg = [ 'Cypress could not associate this error to any specific test.', 'We dynamically generated a new test to display this failure.', - todoMsg(), - ]) - .compact() - .join('\n\n') - .value() + ].join('\n\n') err = $errUtils.appendErrMsg(err, appendMsg) - const throwErr = () => { + // we use this below to create a test and tie this error to it + _uncaughtFn = () => { throw err } - // we could not associate this error - // and shouldn't ever start our run - _uncaughtFn = throwErr - // return undefined so the browser does its default // uncaught exception behavior (logging to console) return undefined diff --git a/packages/reporter/src/commands/commands.scss b/packages/reporter/src/commands/commands.scss index a709a5356e..c9dce8c732 100644 --- a/packages/reporter/src/commands/commands.scss +++ b/packages/reporter/src/commands/commands.scss @@ -415,6 +415,13 @@ } } + .command-name-uncaught-exception { + // need extra spacing between (uncaught exception) and the error message + .command-message { + margin-left: 5px; + } + } + .command-controls { i { padding: 2px; diff --git a/packages/runner/cypress/fixtures/errors/intercept_spec.ts b/packages/runner/cypress/fixtures/errors/intercept_spec.ts index 8092bc8e08..d4601de95f 100644 --- a/packages/runner/cypress/fixtures/errors/intercept_spec.ts +++ b/packages/runner/cypress/fixtures/errors/intercept_spec.ts @@ -1,43 +1,94 @@ -import { SinonStub } from "sinon" +import './setup' describe('cy.intercept', () => { - const { $, sinon } = Cypress + const { $ } = Cypress - it('fails in req callback', () => { - cy.intercept('/json-content-type', () => { - expect('a').to.eq('b') - }) - .then(() => { - console.log('hi2') - Cypress.emit('net:event', 'before:request', { - eventId: '1', - // @ts-ignore - routeHandlerId: Object.keys(Cypress.state('routes'))[0], - subscription: { - await: true, - }, - data: {} - }) - const { $ } = Cypress - $.get('/json-content-type') - }) + it('assertion failure in req callback', () => { + cy.intercept('/json-content-type', () => { + expect('a').to.eq('b') }) + .then(() => { + Cypress.emit('net:event', 'before:request', { + eventId: '1', + // @ts-ignore + routeHandlerId: Object.keys(Cypress.state('routes'))[0], + subscription: { + await: true, + }, + data: {}, + }) + }) + .wait(1000) // ensure the failure happens before test ends + }) - it('fails in res callback', () => { + it('assertion failure in res callback', () => { cy.intercept('/json-content-type', (req) => { - req.reply(() => { - expect('b').to.eq('c') - }) + req.reply(() => { + expect('b').to.eq('c') + }) }) - .then(() => $.get('/json-content-type')) + .then(() => { + Cypress.emit('net:event', 'before:request', { + eventId: '1', + requestId: '1', + // @ts-ignore + routeHandlerId: Object.keys(Cypress.state('routes'))[0], + subscription: { + await: true, + }, + data: {}, + }) + + Cypress.emit('net:event', 'response', { + eventId: '1', + requestId: '1', + // @ts-ignore + routeHandlerId: Object.keys(Cypress.state('routes'))[0], + subscription: { + // @ts-ignore + id: Object.values(Cypress.state('routes'))[0].requests['1'].subscriptions[0].subscription.id, + await: true, + }, + data: {}, + }) + }) + .wait(1000) // ensure the failure happens before test ends }) it('fails when erroneous response is received while awaiting response', () => { cy.intercept('/fake', (req) => { - req.reply(() => { - expect('this should not be reached').to.eq('d') - }) + req.reply(() => { + throw new Error('this should not be reached') + }) }) - .then(() => $.get('http://foo.invalid/fake')) + .then(() => { + Cypress.emit('net:event', 'before:request', { + eventId: '1', + requestId: '1', + // @ts-ignore + routeHandlerId: Object.keys(Cypress.state('routes'))[0], + subscription: { + await: true, + }, + data: {}, + }) + + Cypress.emit('net:event', 'after:response', { + eventId: '1', + requestId: '1', + // @ts-ignore + routeHandlerId: Object.keys(Cypress.state('routes'))[0], + subscription: { + await: true, + }, + data: { + error: { + name: 'ResponseError', + message: 'it errored', + }, + }, + }) + }) + .wait(1000) // ensure the failure happens before test ends }) -}) \ No newline at end of file +}) diff --git a/packages/runner/cypress/fixtures/errors/uncaught_onRunnable_spec.js b/packages/runner/cypress/fixtures/errors/uncaught_onRunnable_spec.js deleted file mode 100644 index 93aa3d3591..0000000000 --- a/packages/runner/cypress/fixtures/errors/uncaught_onRunnable_spec.js +++ /dev/null @@ -1,12 +0,0 @@ -import './setup' - -describe.only('suite', ()=>{ - it('t1', ()=>{ - - }) - it('t2', ()=>{ - - }) -}) - -throw new Error('my error') diff --git a/packages/runner/cypress/fixtures/errors/uncaught_outside_test_only_suite_spec.js b/packages/runner/cypress/fixtures/errors/uncaught_outside_test_only_suite_spec.js new file mode 100644 index 0000000000..7171f5c5e7 --- /dev/null +++ b/packages/runner/cypress/fixtures/errors/uncaught_outside_test_only_suite_spec.js @@ -0,0 +1,14 @@ +import './setup' + +// eslint-disable-next-line mocha/no-exclusive-tests +describe.only('suite', () => { + it('t1', () => { + + }) + + it('t2', () => { + + }) +}) + +throw new Error('error from outside test with only suite') diff --git a/packages/runner/cypress/fixtures/errors/uncaught_outside_test_spec.js b/packages/runner/cypress/fixtures/errors/uncaught_outside_test_spec.js new file mode 100644 index 0000000000..72788393cf --- /dev/null +++ b/packages/runner/cypress/fixtures/errors/uncaught_outside_test_spec.js @@ -0,0 +1,7 @@ +import './setup' + +describe('suite', () => { + it('t1', () => {}) +}) + +throw new Error('error from outside test') diff --git a/packages/runner/cypress/fixtures/errors/uncaught_spec.js b/packages/runner/cypress/fixtures/errors/uncaught_spec.js index e3530bb750..931f12b964 100644 --- a/packages/runner/cypress/fixtures/errors/uncaught_spec.js +++ b/packages/runner/cypress/fixtures/errors/uncaught_spec.js @@ -38,6 +38,7 @@ describe('uncaught errors', { defaultCommandTimeout: 0 }, () => { cy.wait(10000) }) + // eslint-disable-next-line mocha/handle-done-callback it('async spec exception with done', (done) => { setTimeout(() => { ({}).bar() @@ -50,6 +51,7 @@ describe('uncaught errors', { defaultCommandTimeout: 0 }, () => { cy.wait(10000) }) + // eslint-disable-next-line mocha/handle-done-callback it('spec unhandled rejection with done', (done) => { Promise.reject(new Error('Unhandled promise rejection from the spec')) }) @@ -60,10 +62,20 @@ describe('uncaught errors', { defaultCommandTimeout: 0 }, () => { cy.wait(10000) }) + // eslint-disable-next-line mocha/handle-done-callback it('spec Bluebird unhandled rejection with done', (done) => { Bluebird.reject(new Error('Unhandled promise rejection from the spec')) }) // TODO: Cypress.Promise.reject() gets caught by AUT. Can/should // we handle that somehow? + + it('exception inside uncaught:exception', () => { + cy.on('uncaught:exception', () => { + ({}).bar() + }) + + cy.visit('/index.html') + cy.get('.trigger-sync-error').click() + }) }) diff --git a/packages/runner/cypress/integration/reporter.errors.spec.js b/packages/runner/cypress/integration/reporter.errors.spec.js index 7848866c62..65657e177f 100644 --- a/packages/runner/cypress/integration/reporter.errors.spec.js +++ b/packages/runner/cypress/integration/reporter.errors.spec.js @@ -39,13 +39,6 @@ describe('errors ui', () => { codeFrameText: `thrownewError('An outside error')`, verifyOpenInIde: false, }) - - verify.it('in spec file outside test with only suite', { - file: 'uncaught_onRunnable_spec.js', - column: 7, - message: 'my error', - codeFrameText: `my error`, - }) }) describe('hooks', { viewportHeight: 900 }, () => { @@ -275,23 +268,46 @@ describe('errors ui', () => { }) }) - // FIXME: these cy.fail errors are propagating to window.top - describe.skip('cy.intercept', () => { + describe('cy.intercept', () => { const file = 'intercept_spec.ts' - verify.it('fails in req callback', { + verify.it('assertion failure in req callback', { file, - message: 'A request callback passed to cy.intercept() threw an error while intercepting a request', + column: 22, + message: [ + 'A request callback passed to cy.intercept() threw an error while intercepting a request', + `expected 'a' to equal 'b'`, + ], + notInMessage: [ + 'The following error originated from your spec code', + ], }) - verify.it('fails in res callback', { + verify.it('assertion failure in res callback', { file, - column: 1, + column: 24, + codeFrameText: '.reply(()=>{', + message: [ + 'A response callback passed to req.reply() threw an error while intercepting a response:', + `expected 'b' to equal 'c'`, + ], + notInMessage: [ + 'The following error originated from your spec code', + ], }) verify.it('fails when erroneous response is received while awaiting response', { file, - column: 1, + column: 6, + // this fails the active test because it's an asynchronous + // response failure from the network + codeFrameText: '.wait(1000)', + message: [ + 'req.reply() was provided a callback to intercept the upstream response, but a network error occurred while making the request', + ], + notInMessage: [ + 'The following error originated from your spec code', + ], }) }) @@ -344,18 +360,14 @@ describe('errors ui', () => { message: 'bar is not a function', }) - // FIXME: in isolated runner, the error ends up uncaught for - // some reason, which throws off the test - verify.it.skip('onResponse assertion failure', { + verify.it('onResponse assertion failure', { file, column: 29, codeFrameText: 'onResponse', message: `expected 'actual' to equal 'expected'`, }) - // FIXME: in isolated runner, the error ends up uncaught for - // some reason, which throws off the test - verify.it.skip('onResponse exception', { + verify.it('onResponse exception', { file, column: 14, codeFrameText: 'onResponse', @@ -394,18 +406,14 @@ describe('errors ui', () => { message: 'bar is not a function', }) - // FIXME: in isolated runner, the error ends up uncaught for - // some reason, which throws off the test - verify.it.skip('onResponse assertion failure', { + verify.it('onResponse assertion failure', { file, column: 29, codeFrameText: 'onResponse', message: `expected 'actual' to equal 'expected'`, }) - // FIXME: in isolated runner, the error ends up uncaught for - // some reason, which throws off the test - verify.it.skip('onResponse exception', { + verify.it('onResponse exception', { file, column: 14, codeFrameText: 'onResponse', @@ -484,11 +492,12 @@ describe('errors ui', () => { verify.it('sync app visit exception', { file, + uncaught: true, command: 'visit', visitUrl: 'http://localhost:3500/fixtures/errors.html?error-on-visit', + originalMessage: 'visit error', message: [ 'The following error originated from your application code', - 'visit error', ], notInMessage: [ 'It was caused by an unhandled promise rejection', @@ -500,10 +509,11 @@ describe('errors ui', () => { verify.it('sync app navigates to visit exception', { file, + uncaught: true, visitUrl: 'http://localhost:3500/fixtures/errors.html', + originalMessage: 'visit error', message: [ 'The following error originated from your application code', - 'visit error', ], notInMessage: [ 'It was caused by an unhandled promise rejection', @@ -515,11 +525,12 @@ describe('errors ui', () => { verify.it('sync app exception', { file, + uncaught: true, command: 'click', visitUrl: 'http://localhost:3500/fixtures/errors.html', + originalMessage: 'sync error', message: [ 'The following error originated from your application code', - 'sync error', ], notInMessage: [ 'It was caused by an unhandled promise rejection', @@ -531,10 +542,11 @@ describe('errors ui', () => { verify.it('async app exception', { file, + uncaught: true, visitUrl: 'http://localhost:3500/fixtures/errors.html', + originalMessage: 'async error', message: [ 'The following error originated from your application code', - 'async error', ], notInMessage: [ 'It was caused by an unhandled promise rejection', @@ -546,11 +558,12 @@ describe('errors ui', () => { verify.it('app unhandled rejection', { file, + uncaught: true, visitUrl: 'http://localhost:3500/fixtures/errors.html', + originalMessage: 'promise rejection', message: [ 'The following error originated from your application code', 'It was caused by an unhandled promise rejection', - 'promise rejection', ], regex: /localhost\:\d+\/fixtures\/errors.html:\d+:\d+/, hasCodeFrame: false, @@ -559,10 +572,11 @@ describe('errors ui', () => { verify.it('async spec exception', { file, + uncaught: true, column: 12, + originalMessage: 'bar is not a function', message: [ 'The following error originated from your test code', - 'bar is not a function', ], notInMessage: [ 'It was caused by an unhandled promise rejection', @@ -571,10 +585,11 @@ describe('errors ui', () => { verify.it('async spec exception with done', { file, + uncaught: true, column: 12, + originalMessage: 'bar is not a function', message: [ 'The following error originated from your test code', - 'bar is not a function', ], notInMessage: [ 'It was caused by an unhandled promise rejection', @@ -583,43 +598,88 @@ describe('errors ui', () => { verify.it('spec unhandled rejection', { file, + uncaught: true, column: 20, + originalMessage: 'Unhandled promise rejection from the spec', message: [ 'The following error originated from your test code', 'It was caused by an unhandled promise rejection', - 'Unhandled promise rejection from the spec', ], }) verify.it('spec unhandled rejection with done', { file, + uncaught: true, column: 20, + originalMessage: 'Unhandled promise rejection from the spec', message: [ 'The following error originated from your test code', 'It was caused by an unhandled promise rejection', - 'Unhandled promise rejection from the spec', ], }) verify.it('spec Bluebird unhandled rejection', { file, + uncaught: true, column: 21, + originalMessage: 'Unhandled promise rejection from the spec', message: [ 'The following error originated from your test code', 'It was caused by an unhandled promise rejection', - 'Unhandled promise rejection from the spec', ], }) verify.it('spec Bluebird unhandled rejection with done', { file, + uncaught: true, column: 21, + originalMessage: 'Unhandled promise rejection from the spec', message: [ 'The following error originated from your test code', 'It was caused by an unhandled promise rejection', - 'Unhandled promise rejection from the spec', ], }) + + verify.it('exception inside uncaught:exception', { + file, + uncaught: true, + uncaughtMessage: 'sync error', + visitUrl: 'http://localhost:3500/fixtures/errors.html', + column: 12, + originalMessage: 'bar is not a function', + message: [ + 'The following error originated from your test code', + ], + notInMessage: [ + 'It was caused by an unhandled promise rejection', + ], + }) + + // NOTE: the following 2 test don't have uncaught: true because we don't + // display command logs if there are only events and not true commands + // and uncaught: true causes the verification to look for the error + // event command log + verify.it('spec exception outside test', { + file: 'uncaught_outside_test_spec.js', + column: 7, + message: [ + 'The following error originated from your test code', + 'error from outside test', + 'Cypress could not associate this error to any specific test', + ], + codeFrameText: `thrownewError('error from outside test')`, + }) + + verify.it('spec exception outside test with only suite', { + file: 'uncaught_outside_test_only_suite_spec.js', + column: 7, + message: [ + 'error from outside test with only suite', + 'The following error originated from your test code', + 'Cypress could not associate this error to any specific test', + ], + codeFrameText: `thrownewError('error from outside test with only suite')`, + }) }) describe('custom commands', () => { diff --git a/packages/runner/cypress/support/verify-failures.js b/packages/runner/cypress/support/verify-failures.js index aba72cbaa8..d0975113d1 100644 --- a/packages/runner/cypress/support/verify-failures.js +++ b/packages/runner/cypress/support/verify-failures.js @@ -12,19 +12,24 @@ const verifyFailure = (options) => { verifyOpenInIde = true, column, codeFrameText, + originalMessage, message = [], notInMessage = [], command, stack, file, win, + uncaught = false, + uncaughtMessage, } = options let { regex, line } = options regex = regex || new RegExp(`${file}:${line || '\\d+'}:${column}`) const testOpenInIde = () => { - expect(win.runnerWs.emit.withArgs('open:file').lastCall.args[1].file).to.include(file) + cy.log('open in IDE works').then(() => { + expect(win.runnerWs.emit.withArgs('open:file').lastCall.args[1].file).to.include(file) + }) } win.runnerWs.emit.withArgs('get:user:editor') @@ -41,32 +46,66 @@ const verifyFailure = (options) => { cy.contains('View stack trace').click() - _.each([].concat(message), (msg) => { + const messageLines = [].concat(message) + + if (messageLines.length) { + cy.log('message contains expected lines and stack does not include message') + + _.each(messageLines, (msg) => { + cy.get('.runnable-err-message') + .should('include.text', msg) + + cy.get('.runnable-err-stack-trace') + .should('not.include.text', msg) + }) + } + + if (originalMessage) { cy.get('.runnable-err-message') - .should('include.text', msg) + .should('include.text', originalMessage) + } - cy.get('.runnable-err-stack-trace') - .should('not.include.text', msg) - }) + const notInMessageLines = [].concat(notInMessage) - _.each([].concat(notInMessage), (msg) => { - cy.get('.runnable-err-message') - .should('not.include.text', msg) - }) + if (notInMessageLines.length) { + cy.log('message does not contain the specified lines') + _.each(notInMessageLines, (msg) => { + cy.get('.runnable-err-message') + .should('not.include.text', msg) + }) + } + + cy.log('stack trace matches the specified pattern') cy.get('.runnable-err-stack-trace') .invoke('text') .should('match', regex) if (stack) { - _.each([].concat(stack), (stackLine) => { + const stackLines = [].concat(stack) + + if (stackLines.length) { + cy.log('stack contains the expected lines') + } + + _.each(stackLines, (stackLine) => { cy.get('.runnable-err-stack-trace') .should('include.text', stackLine) }) } cy.get('.runnable-err-stack-trace') - .should('not.include.text', '__stackReplacementMarker') + .invoke('text') + .should('not.include', '__stackReplacementMarker') + .should((stackTrace) => { + // if this stack trace has the 'From Your Spec Code' addendum, + // it should only appear once + const match = stackTrace.match(/From Your Spec Code/g) + + if (match && match.length) { + expect(match.length, `'From Your Spec Code' should only be in the stack once, but found ${match.length} instances`).to.equal(1) + } + }) if (verifyOpenInIde) { cy.contains('.runnable-err-stack-trace .runnable-err-file-path a', file) @@ -77,16 +116,30 @@ const verifyFailure = (options) => { } if (command) { + cy.log('the error is attributed to the correct command') cy .get('.command-state-failed') - .should('have.length', 1) + .first() .find('.command-method') .invoke('text') .should('equal', command) } + if (uncaught) { + cy.log('uncaught error has an associated log for the original error') + cy.get('.command-name-uncaught-exception') + .should('have.length', 1) + .should('have.class', 'command-state-failed') + .find('.command-message-text') + .should('include.text', uncaughtMessage || originalMessage) + } else { + cy.log('"caught" error does not have an uncaught error log') + cy.get('.command-name-uncaught-exception').should('not.exist') + } + if (!hasCodeFrame) return + cy.log('code frame matches specified pattern') cy .get('.test-err-code-frame .runnable-err-file-path') .invoke('text') From 5eec9114f6b9a89371b00111423baca7f4114f35 Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Mon, 15 Mar 2021 14:00:21 -0700 Subject: [PATCH 020/134] Added more strict JSX rules --- packages/runner-ct/.eslintrc | 32 ++++++++++++++++++++++++++++++-- packages/runner-ct/package.json | 7 +++++-- yarn.lock | 7 ++++++- 3 files changed, 41 insertions(+), 5 deletions(-) diff --git a/packages/runner-ct/.eslintrc b/packages/runner-ct/.eslintrc index 896389b435..99f068b546 100644 --- a/packages/runner-ct/.eslintrc +++ b/packages/runner-ct/.eslintrc @@ -6,13 +6,27 @@ "extends": [ "plugin:@cypress/dev/general", "plugin:@cypress/dev/tests", - "plugin:@cypress/dev/react" + "plugin:@cypress/dev/react", + "plugin:react/recommended", + "plugin:react-hooks/recommended" ], "parser": "@typescript-eslint/parser", "env": { "cypress/globals": true }, "rules": { + "react/display-name": "off", + "react/function-component-definition": ["error", { + "namedComponents": "arrow-function", + "unnamedComponents": "arrow-function" + }], + "react/jsx-boolean-value": ["error", "always"], + "react/jsx-closing-bracket-location": ["error", "line-aligned"], + "react/jsx-closing-tag-location": "error", + "react/jsx-curly-brace-presence": [ + "error", { "props": "never", "children": "never" }], + "react/jsx-curly-newline": "error", + "react/jsx-filename-extension": [ "warn", { @@ -22,7 +36,21 @@ ".tsx" ] } - ] + ], + "react/jsx-first-prop-new-line": "error", + "react/jsx-max-props-per-line": ["error", {"maximum": 1, "when": "multiline"}], + "react/jsx-no-bind": "error", + "react/jsx-no-useless-fragment": "error", + "react/jsx-one-expression-per-line": "error", + "react/jsx-sort-props": ["error", {"callbacksLast": true, "ignoreCase": true, "noSortAlphabetically": true, "reservedFirst": true}], + "react/jsx-tag-spacing": ["error", { + "closingSlash": "never", + "beforeSelfClosing": "always" + }], + "react/jsx-wrap-multilines": ["error", {"declaration": "parens-new-line", "assignment": "parens-new-line", "return": "parens-new-line", "arrow": "parens-new-line", "condition": "parens-new-line", "logical": "parens-new-line", "prop": "parens-new-line"}], + "react/no-array-index-key": "error", + "react/prop-types": "off", + "quote-props": ["error", "as-needed"] }, "overrides": [ { diff --git a/packages/runner-ct/package.json b/packages/runner-ct/package.json index 69397c6e8e..f91b9f9376 100644 --- a/packages/runner-ct/package.json +++ b/packages/runner-ct/package.json @@ -10,6 +10,7 @@ "clean-deps": "rm -rf node_modules", "cypress:open": "ts-node ../../scripts/cypress.js open-ct --project ${PWD}", "cypress:run": "ts-node ../../scripts/cypress.js run-ct --project ${PWD}", + "lint": "eslint --ext .js,.jsx,.ts,.tsx,.json src", "postinstall": "echo '@packages/runner needs: yarn build'", "test": "ts-node ../../scripts/cypress.js run-ct --project ${PWD}", "watch": "webpack --watch --progress --config webpack.config.ts" @@ -24,7 +25,6 @@ "chai": "^4.2.0", "classnames": "2.2.6", "cypress-real-events": "1.2.0", - "eslint-plugin-mocha": "^8.0.0", "express": "^4.17.1", "globby": "^11.0.1", "hotkeys-js": "3.8.2", @@ -51,6 +51,9 @@ "babel-loader": "8.1.0", "clean-webpack-plugin": "^3.0.0", "dart-sass": "^1.25.0", + "eslint-plugin-mocha": "^8.0.0", + "eslint-plugin-react": "^7.22.0", + "eslint-plugin-react-hooks": "^4.2.0", "html-webpack-plugin": "^4.5.0", "sass-loader": "^10.0.3", "ts-loader": "^8.0.5", @@ -67,4 +70,4 @@ "src", "lib" ] -} +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index da4e06e784..16e4fc19ab 100644 --- a/yarn.lock +++ b/yarn.lock @@ -15117,6 +15117,11 @@ eslint-plugin-react-hooks@^1.6.1: resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-1.7.0.tgz#6210b6d5a37205f0b92858f895a4e827020a7d04" integrity sha512-iXTCFcOmlWvw4+TOE8CLWj6yX1GwzT0Y6cUfHHZqWnSk144VmVIRcVGtUAzrLES7C798lmvnt02C7rxaOX1HNA== +eslint-plugin-react-hooks@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.2.0.tgz#8c229c268d468956334c943bb45fc860280f5556" + integrity sha512-623WEiZJqxR7VdxFCKLI6d6LLpwJkGPYKODnkH3D7WpOG5KM8yWueBd8TLsNAetEJNF5iJmolaAKO3F8yzyVBQ== + eslint-plugin-react@7.14.3: version "7.14.3" resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.14.3.tgz#911030dd7e98ba49e1b2208599571846a66bdf13" @@ -15166,7 +15171,7 @@ eslint-plugin-react@7.19.0: string.prototype.matchall "^4.0.2" xregexp "^4.3.0" -eslint-plugin-react@^7.20.6: +eslint-plugin-react@^7.20.6, eslint-plugin-react@^7.22.0: version "7.22.0" resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.22.0.tgz#3d1c542d1d3169c45421c1215d9470e341707269" integrity sha512-p30tuX3VS+NWv9nQot9xIGAHBXR0+xJVaZriEsHoJrASGCJZDJ8JLNM0YqKqI0AKm6Uxaa1VUHoNEibxRCMQHA== From ecca7da8367b8fc2070d885b21bad5966937a868 Mon Sep 17 00:00:00 2001 From: Zach Bloomquist Date: Tue, 16 Mar 2021 10:25:30 -0400 Subject: [PATCH 021/134] electron@12.0.1 --- packages/electron/package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/electron/package.json b/packages/electron/package.json index c298a5cda9..ff85d22c8d 100644 --- a/packages/electron/package.json +++ b/packages/electron/package.json @@ -24,7 +24,7 @@ "minimist": "1.2.5" }, "devDependencies": { - "electron": "12.0.0", + "electron": "12.0.1", "execa": "4.1.0", "mocha": "3.5.3" }, diff --git a/yarn.lock b/yarn.lock index f6fdd33376..5aa0042e63 100644 --- a/yarn.lock +++ b/yarn.lock @@ -14479,10 +14479,10 @@ electron-to-chromium@^1.3.247, electron-to-chromium@^1.3.378, electron-to-chromi resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.682.tgz#f4b5c8d4479df96b61e508a721d6c32c1262ef23" integrity sha512-zok2y37qR00U14uM6qBz/3iIjWHom2eRfC2S1StA0RslP7x34jX+j4mxv80t8OEOHLJPVG54ZPeaFxEI7gPrwg== -electron@12.0.0: - version "12.0.0" - resolved "https://registry.yarnpkg.com/electron/-/electron-12.0.0.tgz#b3b1d88cc64622e59c637521da5a6b6ab4df4eb5" - integrity sha512-p6oxZ4LG82hopPGAsIMOjyoL49fr6cexyFNH0kADA9Yf+mJ72DN7bjvBG+6V7r6QKhwYgsSsW8RpxBeVOUbxVQ== +electron@12.0.1: + version "12.0.1" + resolved "https://registry.npmjs.org/electron/-/electron-12.0.1.tgz#11afa81dae858cc7cd79107c2f789e8a7f93a31b" + integrity sha512-4bTfLSTmuFkMxq3RMyjd8DxuzbxI1Bde879XDrBA4kFWbKhZ3hfXqHXQz3129eCmcLre5odcNsWq7/xzyJilMA== dependencies: "@electron/get" "^1.0.1" "@types/node" "^14.6.2" From 9f939668b1c0d2221e5084f008c870520c37d971 Mon Sep 17 00:00:00 2001 From: Zach Bloomquist Date: Tue, 16 Mar 2021 18:40:33 +0000 Subject: [PATCH 022/134] fix: add test for cy.intercept having response for multiple aliases (#15528) Co-authored-by: KHeo --- .../integration/commands/net_stubbing_spec.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/packages/driver/cypress/integration/commands/net_stubbing_spec.ts b/packages/driver/cypress/integration/commands/net_stubbing_spec.ts index 826a83940e..86c01a4068 100644 --- a/packages/driver/cypress/integration/commands/net_stubbing_spec.ts +++ b/packages/driver/cypress/integration/commands/net_stubbing_spec.ts @@ -2209,6 +2209,22 @@ describe('network stubbing', { retries: { runMode: 2, openMode: 0 } }, function .wait('@image').its('response.statusCode').should('eq', 304) }) + // https://github.com/cypress-io/cypress/issues/14522 + it('different aliases are used for the same url', () => { + cy.intercept('/status-code').as('status') + .then(() => { + $.get('/status-code?code=204') + }) + .wait('@status').its('response.statusCode').should('eq', 204) + + cy.intercept('/status-code').as('status2') + .then(() => { + $.get('/status-code?code=301') + }) + .wait('@status').its('response.statusCode').should('eq', 301) + .wait('@status2').its('response.statusCode').should('eq', 301) + }) + // https://github.com/cypress-io/cypress/issues/9549 it('should handle aborted requests', () => { cy.intercept('https://jsonplaceholder.typicode.com/todos/1').as('xhr') From 319787bba4958dcc9c9ceaad0a6954fbf28158d7 Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Tue, 16 Mar 2021 12:04:19 -0700 Subject: [PATCH 023/134] Improved CT lint rules and fixed react-hooks --- package.json | 5 +- packages/runner-ct/.eslintrc | 74 ----------- packages/runner-ct/.eslintrc.json | 125 ++++++++++++++++++ .../cypress/component/RunnerCt.spec.tsx | 3 +- .../cypress/component/screenshot.spec.tsx | 12 +- packages/runner-ct/package.json | 2 +- packages/runner-ct/src/.eslintrc.json | 5 - .../runner-ct/src/SpecList/SpecFileItem.tsx | 3 +- .../runner-ct/src/SpecList/SpecGroupItem.tsx | 2 +- packages/runner-ct/src/SpecList/SpecList.tsx | 2 +- packages/runner-ct/src/app/KeyboardHelper.tsx | 12 +- packages/runner-ct/src/app/NoSpecSelected.tsx | 6 +- packages/runner-ct/src/app/Plugins.tsx | 8 +- packages/runner-ct/src/app/RunnerCt.tsx | 9 +- .../src/errors/automation-disconnected.jsx | 12 +- .../runner-ct/src/errors/no-automation.jsx | 21 ++- packages/runner-ct/src/header/header.tsx | 51 +++++-- packages/runner-ct/src/iframe/iframes.jsx | 3 +- .../src/iframe/snapshot-controls.jsx | 2 +- packages/runner-ct/src/lib/ResizableBox.tsx | 2 +- .../src/lib/config-file-formatted.jsx | 34 ++++- packages/runner-ct/src/message/message.tsx | 10 +- .../src/plugins/ReactDevtoolsFallback.tsx | 4 +- .../selector-playground.jsx | 50 ++++--- packages/runner-ct/webpack.config.ts | 16 +-- yarn.lock | 30 +---- 26 files changed, 329 insertions(+), 174 deletions(-) delete mode 100644 packages/runner-ct/.eslintrc create mode 100644 packages/runner-ct/.eslintrc.json delete mode 100644 packages/runner-ct/src/.eslintrc.json diff --git a/package.json b/package.json index 6dbfb7520a..c9174466ad 100644 --- a/package.json +++ b/package.json @@ -126,7 +126,8 @@ "eslint-plugin-cypress": "2.11.2", "eslint-plugin-json-format": "2.0.0", "eslint-plugin-mocha": "6.1.0", - "eslint-plugin-react": "7.18.3", + "eslint-plugin-react": "7.22.0", + "eslint-plugin-react-hooks": "4.2.0", "execa": "4.0.0", "execa-wrap": "1.4.0", "filesize": "4.1.2", @@ -253,4 +254,4 @@ "**/pretty-format": "26.4.0", "**/socket.io-parser": "4.0.2" } -} +} \ No newline at end of file diff --git a/packages/runner-ct/.eslintrc b/packages/runner-ct/.eslintrc deleted file mode 100644 index 99f068b546..0000000000 --- a/packages/runner-ct/.eslintrc +++ /dev/null @@ -1,74 +0,0 @@ -{ - "plugins": [ - "cypress", - "@cypress/dev" - ], - "extends": [ - "plugin:@cypress/dev/general", - "plugin:@cypress/dev/tests", - "plugin:@cypress/dev/react", - "plugin:react/recommended", - "plugin:react-hooks/recommended" - ], - "parser": "@typescript-eslint/parser", - "env": { - "cypress/globals": true - }, - "rules": { - "react/display-name": "off", - "react/function-component-definition": ["error", { - "namedComponents": "arrow-function", - "unnamedComponents": "arrow-function" - }], - "react/jsx-boolean-value": ["error", "always"], - "react/jsx-closing-bracket-location": ["error", "line-aligned"], - "react/jsx-closing-tag-location": "error", - "react/jsx-curly-brace-presence": [ - "error", { "props": "never", "children": "never" }], - "react/jsx-curly-newline": "error", - - "react/jsx-filename-extension": [ - "warn", - { - "extensions": [ - ".js", - ".jsx", - ".tsx" - ] - } - ], - "react/jsx-first-prop-new-line": "error", - "react/jsx-max-props-per-line": ["error", {"maximum": 1, "when": "multiline"}], - "react/jsx-no-bind": "error", - "react/jsx-no-useless-fragment": "error", - "react/jsx-one-expression-per-line": "error", - "react/jsx-sort-props": ["error", {"callbacksLast": true, "ignoreCase": true, "noSortAlphabetically": true, "reservedFirst": true}], - "react/jsx-tag-spacing": ["error", { - "closingSlash": "never", - "beforeSelfClosing": "always" - }], - "react/jsx-wrap-multilines": ["error", {"declaration": "parens-new-line", "assignment": "parens-new-line", "return": "parens-new-line", "arrow": "parens-new-line", "condition": "parens-new-line", "logical": "parens-new-line", "prop": "parens-new-line"}], - "react/no-array-index-key": "error", - "react/prop-types": "off", - "quote-props": ["error", "as-needed"] - }, - "overrides": [ - { - "files": [ - "lib/*" - ], - "rules": { - "no-console": 1 - } - }, - { - "files": [ - "**/*.json" - ], - "rules": { - "quotes": "off", - "comma-dangle": "off" - } - } - ] -} diff --git a/packages/runner-ct/.eslintrc.json b/packages/runner-ct/.eslintrc.json new file mode 100644 index 0000000000..bf32a92ae6 --- /dev/null +++ b/packages/runner-ct/.eslintrc.json @@ -0,0 +1,125 @@ +{ + "plugins": [ + "cypress", + "@cypress/dev" + ], + "extends": [ + "plugin:@cypress/dev/general", + "plugin:@cypress/dev/tests", + "plugin:@cypress/dev/react", + "plugin:react/recommended", + "plugin:react-hooks/recommended", + "../reporter/src/.eslintrc.json" + ], + "parser": "@typescript-eslint/parser", + "env": { + "cypress/globals": true + }, + "rules": { + "no-unused-vars": "error", + "react/display-name": "off", + "react/function-component-definition": [ + "error", + { + "namedComponents": "arrow-function", + "unnamedComponents": "arrow-function" + } + ], + "react/jsx-boolean-value": [ + "error", + "always" + ], + "react/jsx-closing-bracket-location": [ + "error", + "line-aligned" + ], + "react/jsx-closing-tag-location": "error", + "react/jsx-curly-brace-presence": [ + "error", + { + "props": "never", + "children": "never" + } + ], + "react/jsx-curly-newline": "error", + "react/jsx-filename-extension": [ + "warn", + { + "extensions": [ + ".js", + ".jsx", + ".tsx" + ] + } + ], + "react/jsx-first-prop-new-line": "error", + "react/jsx-max-props-per-line": [ + "error", + { + "maximum": 1, + "when": "multiline" + } + ], + "react/jsx-no-bind": [ + "error", + { + "ignoreDOMComponents": true + } + ], + "react/jsx-no-useless-fragment": "error", + "react/jsx-one-expression-per-line": "error", + "react/jsx-sort-props": [ + "error", + { + "callbacksLast": true, + "ignoreCase": true, + "noSortAlphabetically": true, + "reservedFirst": true + } + ], + "react/jsx-tag-spacing": [ + "error", + { + "closingSlash": "never", + "beforeSelfClosing": "always" + } + ], + "react/jsx-wrap-multilines": [ + "error", + { + "declaration": "parens-new-line", + "assignment": "parens-new-line", + "return": "parens-new-line", + "arrow": "parens-new-line", + "condition": "parens-new-line", + "logical": "parens-new-line", + "prop": "parens-new-line" + } + ], + "react/no-array-index-key": "error", + "react/prop-types": "off", + "quote-props": [ + "error", + "as-needed" + ] + }, + "overrides": [ + { + "files": [ + "lib/*" + ], + "rules": { + "no-console": 1 + } + }, + { + "files": [ + "**/*.json" + ], + "rules": { + "quotes": "off", + "comma-dangle": "off" + } + } + ] +} diff --git a/packages/runner-ct/cypress/component/RunnerCt.spec.tsx b/packages/runner-ct/cypress/component/RunnerCt.spec.tsx index 411ed6795b..a69a6a64dc 100644 --- a/packages/runner-ct/cypress/component/RunnerCt.spec.tsx +++ b/packages/runner-ct/cypress/component/RunnerCt.spec.tsx @@ -93,7 +93,8 @@ describe('RunnerCt', () => { state={makeState({ spec: null })} // @ts-ignore - this is difficult to stub. Real one breaks things. eventManager={new FakeEventManager()} - config={fakeConfig} />) + config={fakeConfig} + />) cy.get(selectors.reporter).should('not.be.visible') cy.percySnapshot() diff --git a/packages/runner-ct/cypress/component/screenshot.spec.tsx b/packages/runner-ct/cypress/component/screenshot.spec.tsx index cd4aa86ebb..769d0c2aa9 100644 --- a/packages/runner-ct/cypress/component/screenshot.spec.tsx +++ b/packages/runner-ct/cypress/component/screenshot.spec.tsx @@ -28,9 +28,15 @@ const styles = ` const Layout: React.FC = () => { return (
- -
Body
- + +
+Body +
+
) } diff --git a/packages/runner-ct/package.json b/packages/runner-ct/package.json index f91b9f9376..25bd389eb8 100644 --- a/packages/runner-ct/package.json +++ b/packages/runner-ct/package.json @@ -10,8 +10,8 @@ "clean-deps": "rm -rf node_modules", "cypress:open": "ts-node ../../scripts/cypress.js open-ct --project ${PWD}", "cypress:run": "ts-node ../../scripts/cypress.js run-ct --project ${PWD}", - "lint": "eslint --ext .js,.jsx,.ts,.tsx,.json src", "postinstall": "echo '@packages/runner needs: yarn build'", + "lint": "eslint --ext .js,.jsx,.ts,.tsx,.json src", "test": "ts-node ../../scripts/cypress.js run-ct --project ${PWD}", "watch": "webpack --watch --progress --config webpack.config.ts" }, diff --git a/packages/runner-ct/src/.eslintrc.json b/packages/runner-ct/src/.eslintrc.json deleted file mode 100644 index 3675f61490..0000000000 --- a/packages/runner-ct/src/.eslintrc.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "extends": [ - "../../reporter/src/.eslintrc.json" - ] -} \ No newline at end of file diff --git a/packages/runner-ct/src/SpecList/SpecFileItem.tsx b/packages/runner-ct/src/SpecList/SpecFileItem.tsx index c66122a25a..e9b1d379bd 100644 --- a/packages/runner-ct/src/SpecList/SpecFileItem.tsx +++ b/packages/runner-ct/src/SpecList/SpecFileItem.tsx @@ -46,5 +46,6 @@ export const SpecFileItem: React.FC = (props: SpecFileProps) => { {props.spec.shortName} - ) + + ) } diff --git a/packages/runner-ct/src/SpecList/SpecGroupItem.tsx b/packages/runner-ct/src/SpecList/SpecGroupItem.tsx index 55313cdf90..dac0fd7f1d 100644 --- a/packages/runner-ct/src/SpecList/SpecGroupItem.tsx +++ b/packages/runner-ct/src/SpecList/SpecGroupItem.tsx @@ -23,9 +23,9 @@ export const SpecGroupItem: React.FC = (props) => { className='spec-list__group' > setIsOpen(!open)} className='spec-list__group-name' title={props.group.shortName} + onClick={() => setIsOpen(!open)} > {open ? : } diff --git a/packages/runner-ct/src/SpecList/SpecList.tsx b/packages/runner-ct/src/SpecList/SpecList.tsx index c7a56703b0..82b0d4d00d 100644 --- a/packages/runner-ct/src/SpecList/SpecList.tsx +++ b/packages/runner-ct/src/SpecList/SpecList.tsx @@ -48,8 +48,8 @@ export const SpecList: React.FC = observer((props) => { return (
= return (
  • -

    {description}

    +

    + {' '} + {description} + {' '} +

    {shortcut.map((key) => ( -
    {key === 'Meta' ? metaSymbol : key}
    +
    + {' '} + {key === 'Meta' ? metaSymbol : key} + {' '} +
    ))}
  • diff --git a/packages/runner-ct/src/app/NoSpecSelected.tsx b/packages/runner-ct/src/app/NoSpecSelected.tsx index d6a700fb15..547483f13d 100644 --- a/packages/runner-ct/src/app/NoSpecSelected.tsx +++ b/packages/runner-ct/src/app/NoSpecSelected.tsx @@ -8,7 +8,11 @@ export const NoSpecSelected: React.FC = ({ children }) => { -

    No spec selected.

    +

    + {' '} +No spec selected. + {' '} +

    {children && (
    diff --git a/packages/runner-ct/src/app/Plugins.tsx b/packages/runner-ct/src/app/Plugins.tsx index 79278e09ad..2f7e96e0c8 100644 --- a/packages/runner-ct/src/app/Plugins.tsx +++ b/packages/runner-ct/src/app/Plugins.tsx @@ -30,10 +30,12 @@ export const Plugins = observer( {props.state.plugins.map((plugin) => (
    diff --git a/packages/runner-ct/src/errors/no-automation.jsx b/packages/runner-ct/src/errors/no-automation.jsx index fc2e555880..5bd19923e1 100644 --- a/packages/runner-ct/src/errors/no-automation.jsx +++ b/packages/runner-ct/src/errors/no-automation.jsx @@ -21,7 +21,12 @@ const noBrowsers = () => ( const browser = (browser) => ( - Run {displayName(browser.displayName)} {browser.majorVersion} + +Run + {displayName(browser.displayName)} + {' '} + {browser.majorVersion} + ) @@ -34,13 +39,15 @@ const browserPicker = (browsers, onLaunchBrowser) => { return (
    -

    This browser was not launched through Cypress. Tests cannot run.

    +

    +This browser was not launched through Cypress. Tests cannot run. +

    ) @@ -49,11 +56,15 @@ const browserPicker = (browsers, onLaunchBrowser) => { export default ({ browsers, onLaunchBrowser }) => (
    diff --git a/packages/runner-ct/src/header/header.tsx b/packages/runner-ct/src/header/header.tsx index aea0a3bef1..5538a379d1 100644 --- a/packages/runner-ct/src/header/header.tsx +++ b/packages/runner-ct/src/header/header.tsx @@ -43,8 +43,8 @@ export default class Header extends Component {