diff --git a/packages/driver/patches/unfetch+4.1.0.dev.patch b/packages/driver/patches/unfetch+4.1.0.dev.patch index 263ad9691c2d..5361b2a5644e 100644 --- a/packages/driver/patches/unfetch+4.1.0.dev.patch +++ b/packages/driver/patches/unfetch+4.1.0.dev.patch @@ -64,6 +64,17 @@ index 57a2761..0000000 @@ -1 +0,0 @@ -{"version":3,"file":"unfetch.umd.js","sources":["../src/index.mjs"],"sourcesContent":["export default function(url, options) {\n\toptions = options || {};\n\treturn new Promise( (resolve, reject) => {\n\t\tconst request = new XMLHttpRequest();\n\t\tconst keys = [];\n\t\tconst all = [];\n\t\tconst headers = {};\n\n\t\tconst response = () => ({\n\t\t\tok: (request.status/100|0) == 2,\t\t// 200-299\n\t\t\tstatusText: request.statusText,\n\t\t\tstatus: request.status,\n\t\t\turl: request.responseURL,\n\t\t\ttext: () => Promise.resolve(request.responseText),\n\t\t\tjson: () => Promise.resolve(JSON.parse(request.responseText)),\n\t\t\tblob: () => Promise.resolve(new Blob([request.response])),\n\t\t\tclone: response,\n\t\t\theaders: {\n\t\t\t\tkeys: () => keys,\n\t\t\t\tentries: () => all,\n\t\t\t\tget: n => headers[n.toLowerCase()],\n\t\t\t\thas: n => n.toLowerCase() in headers\n\t\t\t}\n\t\t});\n\n\t\trequest.open(options.method || 'get', url, true);\n\n\t\trequest.onload = () => {\n\t\t\trequest.getAllResponseHeaders().replace(/^(.*?):[^\\S\\n]*([\\s\\S]*?)$/gm, (m, key, value) => {\n\t\t\t\tkeys.push(key = key.toLowerCase());\n\t\t\t\tall.push([key, value]);\n\t\t\t\theaders[key] = headers[key] ? `${headers[key]},${value}` : value;\n\t\t\t});\n\t\t\tresolve(response());\n\t\t};\n\n\t\trequest.onerror = reject;\n\n\t\trequest.withCredentials = options.credentials=='include';\n\n\t\tfor (const i in options.headers) {\n\t\t\trequest.setRequestHeader(i, options.headers[i]);\n\t\t}\n\n\t\trequest.send(options.body || null);\n\t});\n}\n"],"names":["url","options","Promise","resolve","reject","request","XMLHttpRequest","keys","all","headers","response","ok","status","statusText","responseURL","text","responseText","json","JSON","parse","blob","Blob","clone","entries","get","n","toLowerCase","has","const","i","open","method","onload","getAllResponseHeaders","replace","m","key","value","push","onerror","withCredentials","credentials","setRequestHeader","send","body"],"mappings":"6KAAe,SAASA,EAAKC,UAC5BA,EAAUA,GAAW,GACd,IAAIC,iBAAUC,EAASC,OACvBC,EAAU,IAAIC,eACdC,EAAO,GACPC,EAAM,GACNC,EAAU,GAEVC,oBACLC,GAA8B,IAAzBN,EAAQO,OAAO,IAAI,GACxBC,WAAYR,EAAQQ,WACpBD,OAAQP,EAAQO,OAChBZ,IAAKK,EAAQS,YACbC,uBAAYb,QAAQC,QAAQE,EAAQW,eACpCC,uBAAYf,QAAQC,QAAQe,KAAKC,MAAMd,EAAQW,gBAC/CI,uBAAYlB,QAAQC,QAAQ,IAAIkB,KAAK,CAAChB,EAAQK,aAC9CY,MAAOZ,EACPD,QAAS,CACRF,uBAAYA,GACZgB,0BAAef,GACfgB,aAAKC,UAAKhB,EAAQgB,EAAEC,gBACpBC,aAAKF,UAAKA,EAAEC,gBAAiBjB,UAmB1BmB,IAAMC,KAfXxB,EAAQyB,KAAK7B,EAAQ8B,QAAU,MAAO/B,GAAK,GAE3CK,EAAQ2B,kBACP3B,EAAQ4B,wBAAwBC,QAAQ,wCAAiCC,EAAGC,EAAKC,GAChF9B,EAAK+B,KAAKF,EAAMA,EAAIV,eACpBlB,EAAI8B,KAAK,CAACF,EAAKC,IACf5B,EAAQ2B,GAAO3B,EAAQ2B,GAAU3B,EAAQ2B,OAAQC,EAAUA,IAE5DlC,EAAQO,MAGTL,EAAQkC,QAAUnC,EAElBC,EAAQmC,gBAAuC,WAArBvC,EAAQwC,YAElBxC,EAAQQ,QACvBJ,EAAQqC,iBAAiBb,EAAG5B,EAAQQ,QAAQoB,IAG7CxB,EAAQsC,KAAK1C,EAAQ2C,MAAQ"} \ No newline at end of file +diff --git a/node_modules/unfetch/src/index.d.ts b/node_modules/unfetch/src/index.d.ts +index 0c53ad9..8dd2d54 100644 +--- a/node_modules/unfetch/src/index.d.ts ++++ b/node_modules/unfetch/src/index.d.ts +@@ -14,4 +14,6 @@ declare namespace unfetch { + + declare const unfetch: typeof fetch; + ++export function registerFetch(window: Window): unfetch; ++ + export default unfetch; diff --git a/node_modules/unfetch/src/index.mjs b/node_modules/unfetch/src/index.mjs deleted file mode 100644 index 783ad42..0000000 diff --git a/packages/driver/src/cy/assertions.ts b/packages/driver/src/cy/assertions.ts index 55458097a1d9..ee25ccf7304d 100644 --- a/packages/driver/src/cy/assertions.ts +++ b/packages/driver/src/cy/assertions.ts @@ -1,5 +1,3 @@ -// @ts-nocheck - import _ from 'lodash' import Promise from 'bluebird' @@ -54,12 +52,18 @@ const isDomSubjectAndMatchesValue = (value, subject) => { return false } +type Parsed = { + subject?: JQuery + actual?: any + expected?: any +} + // Rules: // 1. always remove value // 2. if value is a jquery object set a subject // 3. if actual is undefined or its not expected remove both actual + expected const parseValueActualAndExpected = (value, actual, expected) => { - const obj = { actual, expected } + const obj: Parsed = { actual, expected } if ($dom.isJquery(value)) { obj.subject = value @@ -77,7 +81,7 @@ export const create = (Cypress, cy) => { const getUpcomingAssertions = () => { const index = cy.state('index') + 1 - const assertions = [] + const assertions: any[] = [] // grab the rest of the queue'd commands for (let cmd of cy.queue.slice(index)) { @@ -137,7 +141,11 @@ export const create = (Cypress, cy) => { message = message.replace(stackTracesRe, '\n') } - let obj = parseValueActualAndExpected(value, actual, expected) + let parsed = parseValueActualAndExpected(value, actual, expected) + // TODO: make it more specific after defining the type for Cypress.log(). + let obj: Record = { + ...parsed, + } if ($dom.isElement(value)) { obj.$el = $dom.wrap(value) @@ -216,10 +224,18 @@ export const create = (Cypress, cy) => { }) } + type VerifyUpcomingAssertionsCallbacks = { + ensureExistenceFor?: 'subject' | 'dom' | boolean + onPass?: Function + onFail?: (err?, isDefaultAssertionErr?: boolean, cmds?: any[]) => void + onRetry?: () => any + } + return { finishAssertions, - verifyUpcomingAssertions (subject, options = {}, callbacks = {}) { + // TODO: define the specific type of options + verifyUpcomingAssertions (subject, options: Record = {}, callbacks: VerifyUpcomingAssertionsCallbacks = {}) { const cmds = getUpcomingAssertions() cy.state('upcomingAssertions', cmds) @@ -433,12 +449,13 @@ export const create = (Cypress, cy) => { cy.state('onBeforeLog', setCommandLog) // send verify=true as the last arg - return assertFn.apply(this, args.concat(true)) + return assertFn.apply(this, args.concat(true) as any) } const fns = injectAssertionFns(cmds) - const subjects = [] + // TODO: remove any when the type of subject, the first argument of this function is specified. + const subjects: any[] = [] // iterate through each subject // and force the assertion to return diff --git a/packages/driver/src/cy/chai.ts b/packages/driver/src/cy/chai.ts index dfaf6e0d822a..57bf42c49a6c 100644 --- a/packages/driver/src/cy/chai.ts +++ b/packages/driver/src/cy/chai.ts @@ -1,4 +1,3 @@ -// @ts-nocheck /* eslint-disable prefer-rest-params */ // tests in driver/cypress/integration/commands/assertions_spec.js @@ -31,20 +30,6 @@ const whitespace = /\s/g const valueHasLeadingOrTrailingWhitespaces = /\*\*'\s+|\s+'\*\*/g const imageMarkdown = /!\[.*?\]\(.*?\)/g -let assertProto = null -let matchProto = null -let lengthProto = null -let containProto = null -let existProto = null -let getMessage = null -let chaiUtils = null -let replaceArgMessages = null -let removeOrKeepSingleQuotesBetweenStars = null -let setSpecWindowGlobals = null -let restoreAsserts = null -let overrideExpect = null -let overrideChaiAsserts = null - type CreateFunc = ((specWindow, state, assertFn) => ({ chai: Chai.ChaiStatic expect: (val: any, message?: string) => Chai.Assertion @@ -55,7 +40,7 @@ export let create: CreateFunc | null = null chai.use(sinonChai) chai.use((chai, u) => { - chaiUtils = u + const chaiUtils = u $chaiJquery(chai, chaiUtils, { onInvalid (method, obj) { @@ -87,14 +72,14 @@ chai.use((chai, u) => { }, }) - assertProto = chai.Assertion.prototype.assert - matchProto = chai.Assertion.prototype.match - lengthProto = chai.Assertion.prototype.__methods.length.method - containProto = chai.Assertion.prototype.__methods.contain.method - existProto = Object.getOwnPropertyDescriptor(chai.Assertion.prototype, 'exist').get - const { objDisplay } = chai.util; + const assertProto = chai.Assertion.prototype.assert + const matchProto = (chai.Assertion.prototype as any).match + const lengthProto = (chai.Assertion.prototype as any).__methods.length.method + const containProto = (chai.Assertion.prototype as any).__methods.contain.method + const existProto = Object.getOwnPropertyDescriptor(chai.Assertion.prototype, 'exist')!.get + const { objDisplay } = chai.util - ({ getMessage } = chai.util) + const getMessage = chai.util.getMessage const _inspect = chai.util.inspect const { inspect, setFormatValueHook } = chaiInspect.create(chai) @@ -108,17 +93,19 @@ chai.use((chai, u) => { try { val && val.document val && val.inspect - } catch (e) { + } catch (e: any) { if (e.stack.indexOf('cross-origin') !== -1 || // chrome e.message.indexOf('cross-origin') !== -1) { // firefox return `[window]` } } + + return }) // remove any single quotes between our **, // except escaped quotes, empty strings and number strings. - removeOrKeepSingleQuotesBetweenStars = (message) => { + const removeOrKeepSingleQuotesBetweenStars = (message) => { // remove any single quotes between our **, preserving escaped quotes // and if an empty string, put the quotes back return message.replace(allBetweenFourStars, (match) => { @@ -149,8 +136,8 @@ chai.use((chai, u) => { return message.replace(imageMarkdown, '``$&``') } - replaceArgMessages = (args, str) => { - return _.reduce(args, (memo, value, index) => { + const replaceArgMessages = (args, str) => { + return _.reduce(args, (memo: string[], value, index) => { if (_.isString(value)) { value = value .replace(allWordsBetweenCurlyBraces, '**$1**') @@ -166,18 +153,17 @@ chai.use((chai, u) => { } return memo - } - , []) + }, []) } - restoreAsserts = function () { + const restoreAsserts = function () { chai.util.inspect = _inspect chai.util.getMessage = getMessage chai.util.objDisplay = objDisplay - chai.Assertion.prototype.assert = assertProto - chai.Assertion.prototype.match = matchProto - chai.Assertion.prototype.__methods.length.method = lengthProto - chai.Assertion.prototype.__methods.contain.method = containProto + chai.Assertion.prototype.assert = assertProto; + (chai.Assertion.prototype as any).match = matchProto; + (chai.Assertion.prototype as any).__methods.length.method = lengthProto; + (chai.Assertion.prototype as any).__methods.contain.method = containProto return Object.defineProperty(chai.Assertion.prototype, 'exist', { get: existProto }) } @@ -218,7 +204,7 @@ chai.use((chai, u) => { } } - overrideChaiAsserts = function (specWindow, state, assertFn) { + const overrideChaiAsserts = function (specWindow, state, assertFn) { chai.Assertion.prototype.assert = createPatchedAssert(specWindow, state, assertFn) const _origGetmessage = function (obj, args) { @@ -248,6 +234,9 @@ chai.use((chai, u) => { return (flagMsg ? `${flagMsg}: ${msg}` : msg) } + // There are 2 types of getMessage. And we're overriding the second one. + // But TypeScript wants us to do both. So we're ignoring this. + // @ts-ignore chaiUtils.getMessage = function (assert, args) { const obj = assert._obj @@ -307,13 +296,16 @@ chai.use((chai, u) => { }) } - const containFn2 = (_super) => { + const makeMethodChainable = (_super) => { return (function () { return _super.apply(this, arguments) }) } - chai.Assertion.overwriteChainableMethod('contain', containFn1, containFn2) + // `makeMethodChainable` doesn't match any type definition, + // but it is necessary to make the method chainable. + // @ts-ignore + chai.Assertion.overwriteChainableMethod('contain', containFn1, makeMethodChainable) chai.Assertion.overwriteChainableMethod('length', (_super) => { @@ -346,7 +338,7 @@ chai.use((chai, u) => { length, obj.length, ) - } catch (e1) { + } catch (e1: any) { e1.node = node e1.negated = chaiUtils.flag(this, 'negate') e1.type = 'length' @@ -377,21 +369,21 @@ chai.use((chai, u) => { } }) }, - - (_super) => { - return (function () { - return _super.apply(this, arguments) - }) - }) - - return chai.Assertion.overwriteProperty('exist', (_super) => { + // `makeMethodChainable` doesn't match any type definition, + // but it is necessary to make the method chainable. + // @ts-ignore + makeMethodChainable) + + // _super is not documented. + // @ts-ignore + chai.Assertion.overwriteProperty('exist', (_super) => { return (function () { const obj = this._obj if (!($dom.isJquery(obj) || $dom.isElement(obj))) { try { return _super.apply(this, arguments) - } catch (e) { + } catch (e: any) { e.type = 'existence' throw e } @@ -412,7 +404,7 @@ chai.use((chai, u) => { node, node, ) - } catch (e1) { + } catch (e1: any) { e1.node = node e1.negated = chaiUtils.flag(this, 'negate') e1.type = 'existence' @@ -456,20 +448,20 @@ chai.use((chai, u) => { const createPatchedAssert = (specWindow, state, assertFn) => { return (function (...args) { let err - const passed = chaiUtils.test(this, args) + const passed = chaiUtils.test(this, args as Chai.AssertionArgs) const value = chaiUtils.flag(this, 'object') const expected = args[3] const customArgs = replaceArgMessages(args, this._obj) - let message = chaiUtils.getMessage(this, customArgs) - const actual = chaiUtils.getActual(this, customArgs) + let message = chaiUtils.getMessage(this, customArgs as Chai.AssertionArgs) + const actual = chaiUtils.getActual(this, customArgs as Chai.AssertionArgs) message = removeOrKeepSingleQuotesBetweenStars(message) message = escapeMarkdown(message) try { - assertProto.apply(this, args) + assertProto.apply(this, args as Chai.AssertionArgs) } catch (e) { err = e } @@ -489,7 +481,7 @@ chai.use((chai, u) => { }) } - overrideExpect = (specWindow, state) => { + const overrideExpect = (specWindow, state) => { // only override assertions for this specific // expect function instance so we do not affect // the outside world @@ -523,7 +515,7 @@ chai.use((chai, u) => { return fn } - setSpecWindowGlobals = function (specWindow, state) { + const setSpecWindowGlobals = function (specWindow, state) { const expect = overrideExpect(specWindow, state) const assert = overrideAssert(specWindow, state) @@ -552,12 +544,3 @@ chai.use((chai, u) => { export interface IChai { expect: ReturnType['expect'] } - -export default { - replaceArgMessages, - removeOrKeepSingleQuotesBetweenStars, - setSpecWindowGlobals, - restoreAsserts, - overrideExpect, - overrideChaiAsserts, -} diff --git a/packages/driver/src/cy/keyboard.ts b/packages/driver/src/cy/keyboard.ts index bf4446aeb3a2..20420d8a7ce6 100644 --- a/packages/driver/src/cy/keyboard.ts +++ b/packages/driver/src/cy/keyboard.ts @@ -114,6 +114,13 @@ export type KeyEventType = | 'textInput' | 'beforeinput' +export type ModifiersEventOptions = { + altKey: boolean + ctrlKey: boolean + metaKey: boolean + shiftKey: boolean +} + const toModifiersEventOptions = (modifiers: KeyboardModifiers) => { return { altKey: modifiers.alt, diff --git a/packages/driver/src/cy/mouse.ts b/packages/driver/src/cy/mouse.ts index 2bbab38a75c4..658e5444354c 100644 --- a/packages/driver/src/cy/mouse.ts +++ b/packages/driver/src/cy/mouse.ts @@ -1,9 +1,8 @@ -// @ts-nocheck import $ from 'jquery' import _ from 'lodash' import $dom from '../dom' import $elements from '../dom/elements' -import $Keyboard from './keyboard' +import $Keyboard, { ModifiersEventOptions } from './keyboard' import $selection from '../dom/selection' import debugFn from 'debug' @@ -16,7 +15,7 @@ const debug = debugFn('cypress:driver:mouse') * @property {Document} doc */ -const getLastHoveredEl = (state) => { +const getLastHoveredEl = (state): HTMLElement | null => { let lastHoveredEl = state('mouseLastHoveredEl') const lastHoveredElAttached = lastHoveredEl && $elements.isAttachedEl(lastHoveredEl) @@ -42,6 +41,12 @@ const getMouseCoords = (state) => { return state('mouseCoords') } +type DefaultMouseOptions = ModifiersEventOptions & CoordsEventOptions & { + view: Window + composed: boolean + relatedTarget: HTMLElement | null +} + export const create = (state, keyboard, focused, Cypress) => { const isFirefox = Cypress.browser.family === 'firefox' @@ -65,7 +70,7 @@ export const create = (state, keyboard, focused, Cypress) => { return sendPointerEvent(el, evtOptions, 'pointerup', true, true) } - const sendPointerdown = (el, evtOptions) => { + const sendPointerdown = (el, evtOptions): {} | SentEvent => { if (isFirefox && el.disabled) { return {} } @@ -75,7 +80,7 @@ export const create = (state, keyboard, focused, Cypress) => { const sendPointermove = (el, evtOptions) => { return sendPointerEvent(el, evtOptions, 'pointermove', true, true) } - const sendPointerover = (el, evtOptions) => { + const sendPointerover = (el, evtOptions: DefaultMouseOptions) => { return sendPointerEvent(el, evtOptions, 'pointerover', true, true) } const sendPointerenter = (el, evtOptions) => { @@ -95,7 +100,7 @@ export const create = (state, keyboard, focused, Cypress) => { return sendMouseEvent(el, evtOptions, 'mouseup', true, true) } - const sendMousedown = (el, evtOptions) => { + const sendMousedown = (el, evtOptions): {} | SentEvent => { if (isFirefox && el.disabled) { return {} } @@ -105,7 +110,7 @@ export const create = (state, keyboard, focused, Cypress) => { const sendMousemove = (el, evtOptions) => { return sendMouseEvent(el, evtOptions, 'mousemove', true, true) } - const sendMouseover = (el, evtOptions) => { + const sendMouseover = (el, evtOptions: DefaultMouseOptions) => { return sendMouseEvent(el, evtOptions, 'mouseover', true, true) } const sendMouseenter = (el, evtOptions) => { @@ -117,7 +122,7 @@ export const create = (state, keyboard, focused, Cypress) => { const sendMouseout = (el, evtOptions) => { return sendMouseEvent(el, evtOptions, 'mouseout', true, true) } - const sendClick = (el, evtOptions, opts = {}) => { + const sendClick = (el, evtOptions, opts: { force?: boolean } = {}) => { // send the click event if firefox and force (needed for force check checkbox) if (!opts.force && isFirefox && el.disabled) { return {} @@ -154,7 +159,7 @@ export const create = (state, keyboard, focused, Cypress) => { return !_.isEqual(xy(fromElViewport), xy(coords)) } - const shouldMoveCursorToEndAfterMousedown = (el) => { + const shouldMoveCursorToEndAfterMousedown = (el: HTMLElement) => { const _debug = debug.extend(':shouldMoveCursorToEnd') _debug('shouldMoveToEnd?', el) @@ -182,7 +187,7 @@ export const create = (state, keyboard, focused, Cypress) => { } const mouse = { - _getDefaultMouseOptions (x, y, win) { + _getDefaultMouseOptions (x, y, win): DefaultMouseOptions { const _activeModifiers = keyboard.getActiveModifiers() const modifiersEventOptions = $Keyboard.toModifiersEventOptions(_activeModifiers) const coordsEventOptions = toCoordsEventOptions(x, y, win) @@ -201,7 +206,7 @@ export const create = (state, keyboard, focused, Cypress) => { * @param {Coords} coords * @param {HTMLElement} forceEl */ - move (fromElViewport, forceEl) { + move (fromElViewport, forceEl?) { debug('mouse.move', fromElViewport) const lastHoveredEl = getLastHoveredEl(state) @@ -259,16 +264,21 @@ export const create = (state, keyboard, focused, Cypress) => { skipped: formatReasonNotFired('Already on Coordinates'), } } + + type EventFunc = + | (() => { skipped: string }) + | (() => SentEvent) + let pointerout = _.noop let pointerleave = _.noop - let pointerover = notFired + let pointerover: EventFunc = notFired let pointerenter = _.noop let mouseout = _.noop let mouseleave = _.noop - let mouseover = notFired + let mouseover: EventFunc = notFired let mouseenter = _.noop - let pointermove = notFired - let mousemove = notFired + let pointermove: EventFunc = notFired + let mousemove: EventFunc = notFired const lastHoveredEl = getLastHoveredEl(state) @@ -285,9 +295,9 @@ export const create = (state, keyboard, focused, Cypress) => { sendMouseout(lastHoveredEl, _.extend({}, defaultMouseOptions, { relatedTarget: el })) } - let curParent = lastHoveredEl + let curParent: Node | null = lastHoveredEl - const elsToSendMouseleave = [] + const elsToSendMouseleave: Node[] = [] while (curParent && curParent.ownerDocument && curParent !== commonAncestor) { elsToSendMouseleave.push(curParent) @@ -317,8 +327,8 @@ export const create = (state, keyboard, focused, Cypress) => { return sendPointerover(el, _.extend({}, defaultPointerOptions, { relatedTarget: lastHoveredEl })) } - let curParent = el - const elsToSendMouseenter = [] + let curParent: Node | null = el + const elsToSendMouseenter: Node[] = [] while (curParent && curParent.ownerDocument && curParent !== commonAncestor) { elsToSendMouseenter.push(curParent) @@ -349,7 +359,8 @@ export const create = (state, keyboard, focused, Cypress) => { return sendMousemove(el, defaultMouseOptions) } - const events = [] + // TODO: make `type` below more specific. + const events: Array & { type: string }> = [] pointerout() pointerleave() @@ -419,7 +430,7 @@ export const create = (state, keyboard, focused, Cypress) => { let pointerdown = sendPointerdown( el, pointerEvtOptions, - ) + ) as Partial const pointerdownPrevented = pointerdown.preventedDefault const elIsDetached = $elements.isDetachedEl(el) @@ -461,7 +472,7 @@ export const create = (state, keyboard, focused, Cypress) => { // el we just send pointerdown const el = mouseDownPhase.targetEl - if (mouseDownPhase.events.pointerdown.preventedDefault || mouseDownPhase.events.mousedown.preventedDefault || !$elements.isAttachedEl(el)) { + if (mouseDownPhase.events.pointerdown.preventedDefault || (mouseDownPhase.events.mousedown as Partial).preventedDefault || !$elements.isAttachedEl(el)) { return mouseDownPhase } @@ -488,6 +499,8 @@ export const create = (state, keyboard, focused, Cypress) => { if (shouldMoveCursorToEndAfterMousedown(el)) { debug('moveSelectionToEnd due to click', el) + // It's a curried function, so the 2 arguments are valid. + // @ts-ignore $selection.moveSelectionToEnd(el, { onlyIfEmptySelection: true }) } @@ -532,7 +545,7 @@ export const create = (state, keyboard, focused, Cypress) => { const mouseDownPhase = mouse.down(fromElViewport, forceEl, pointerEvtOptionsExtend, mouseEvtOptionsExtend) - const skipMouseupEvent = mouseDownPhase.events.pointerdown.skipped || mouseDownPhase.events.pointerdown.preventedDefault + const skipMouseupEvent = mouseDownPhase.events.pointerdown.preventedDefault const mouseUpPhase = mouse.up(fromElViewport, forceEl, skipMouseupEvent, pointerEvtOptionsExtend, mouseEvtOptionsExtend) @@ -638,7 +651,7 @@ export const create = (state, keyboard, focused, Cypress) => { return { click } }, - _contextmenuEvent (fromElViewport, forceEl, mouseEvtOptionsExtend) { + _contextmenuEvent (fromElViewport, forceEl, mouseEvtOptionsExtend?) { const el = forceEl || mouse.moveToCoords(fromElViewport) const win = $dom.getWindowByElement(el) @@ -695,7 +708,7 @@ export const create = (state, keyboard, focused, Cypress) => { const contextmenuEvent = mouse._contextmenuEvent(fromElViewport, forceEl) - const skipMouseupEvent = mouseDownPhase.events.pointerdown.skipped || mouseDownPhase.events.pointerdown.preventedDefault + const skipMouseupEvent = mouseDownPhase.events.pointerdown.preventedDefault const mouseUpPhase = mouse.up(fromElViewport, forceEl, skipMouseupEvent, pointerEvtOptionsExtend, mouseEvtOptionsExtend) const clickEvents = _.extend({}, mouseDownPhase.events, mouseUpPhase.events) @@ -709,7 +722,14 @@ export const create = (state, keyboard, focused, Cypress) => { const { stopPropagation } = window.MouseEvent.prototype -const sendEvent = (evtName, el, evtOptions, bubbles = false, cancelable = false, Constructor, composed = false) => { +type SentEvent = { + stoppedPropagation: boolean + preventedDefault: boolean + el: HTMLElement + modifiers: string +} + +const sendEvent = (evtName, el, evtOptions, bubbles = false, cancelable = false, Constructor, composed = false): SentEvent => { evtOptions = _.extend({}, evtOptions, { bubbles, cancelable }) const _eventModifiers = $Keyboard.fromModifierEventOptions(evtOptions) const modifiers = $Keyboard.modifiersToString(_eventModifiers) @@ -720,6 +740,9 @@ const sendEvent = (evtName, el, evtOptions, bubbles = false, cancelable = false, evt.stopPropagation = function (...args) { evt._hasStoppedPropagation = true + // stopPropagation doesn't have any arguments. So, we cannot type-safely pass the second argument. + // But we're passing it just in case. + // @ts-ignore return stopPropagation.apply(this, ...args) } } @@ -740,6 +763,19 @@ const formatReasonNotFired = (reason) => { return `⚠️ not fired (${reason})` } +type CoordsEventOptions = { + x: number + y: number + clientX: number + clientY: number + screenX: number + screenY: number + pageX: number + pageY: number + layerX: number + layerY: number +} + const toCoordsEventOptions = (x, y, win) => { // these are the coords from the element's window, // ignoring scroll position diff --git a/packages/driver/src/cypress/cy.ts b/packages/driver/src/cypress/cy.ts index f0e2ddafef88..0a91b222b92c 100644 --- a/packages/driver/src/cypress/cy.ts +++ b/packages/driver/src/cypress/cy.ts @@ -1,5 +1,3 @@ -// @ts-nocheck - /* eslint-disable prefer-rest-params */ import _ from 'lodash' import Promise from 'bluebird' @@ -8,14 +6,14 @@ import { registerFetch } from 'unfetch' import $dom from '../dom' import $utils from './utils' -import $errUtils from './error_utils' +import $errUtils, { ErrorFromProjectRejectionEvent } from './error_utils' import $stackUtils from './stack_utils' import { create as createChai, IChai } from '../cy/chai' import { create as createXhr, IXhr } from '../cy/xhrs' import { create as createJQuery, IJQuery } from '../cy/jquery' import { create as createAliases, IAliases } from '../cy/aliases' -import * as $Events from './events' +import { extend as extendEvents } from './events' import { create as createEnsures, IEnsures } from '../cy/ensures' import { create as createFocused, IFocused } from '../cy/focused' import { create as createMouse, Mouse } from '../cy/mouse' @@ -35,6 +33,7 @@ import { CommandQueue } from './command_queue' import { initVideoRecorder } from '../cy/video-recorder' import { TestConfigOverride } from '../cy/testConfigOverrides' import { historyNavigationTriggeredHashChange } from '../cy/navigation' +import { EventEmitter2 } from 'eventemitter2' const debugErrors = debugFn('cypress:driver:errors') @@ -56,7 +55,7 @@ function __stackReplacementMarker (fn, ctx, args) { return fn.apply(ctx, args) } -declare let top: WindowProxy & { __alreadySetErrorHandlers__: boolean } | null +declare let top: WindowProxy & { __alreadySetErrorHandlers__: boolean } // We only set top.onerror once since we make it configurable:false // but we update cy instance every run (page reload or rerun button) @@ -78,7 +77,7 @@ const setTopOnError = function (Cypress, cy: $Cy) { // eslint-disable-next-line @cypress/dev/arrow-body-multiline-braces const onTopError = (handlerType) => (event) => { - const { originalErr, err, promise } = $errUtils.errorFromUncaughtEvent(handlerType, event) + const { originalErr, err, promise } = $errUtils.errorFromUncaughtEvent(handlerType, event) as ErrorFromProjectRejectionEvent // in some callbacks like for cy.intercept, we catch the errors and then // rethrow them, causing them to get caught by the top frame @@ -116,7 +115,7 @@ const setTopOnError = function (Cypress, cy: $Cy) { top.__alreadySetErrorHandlers__ = true } -export class $Cy implements ITimeouts, IStability, IAssertions, IRetries, IJQuery, ILocation, ITimer, IChai, IXhr, IAliases, IEnsures, ISnapshots, IFocused { +export class $Cy extends EventEmitter2 implements ITimeouts, IStability, IAssertions, IRetries, IJQuery, ILocation, ITimer, IChai, IXhr, IAliases, IEnsures, ISnapshots, IFocused { id: string specWindow: any state: any @@ -201,6 +200,8 @@ export class $Cy implements ITimeouts, IStability, IAssertions, IRetries, IJQuer private commandFns: Record = {} constructor (specWindow, Cypress, Cookies, state, config) { + super() + state('specWindow', specWindow) this.specWindow = specWindow @@ -248,7 +249,7 @@ export class $Cy implements ITimeouts, IStability, IAssertions, IRetries, IJQuer this.verifyUpcomingAssertions = assertions.verifyUpcomingAssertions const onFinishAssertions = function () { - return assertions.finishAssertions.apply(window, arguments) + return assertions.finishAssertions.apply(window, arguments as any) } const retries = createRetries(Cypress, state, this.timeout, this.clearTimeout, this.whenStable, onFinishAssertions) @@ -342,7 +343,7 @@ export class $Cy implements ITimeouts, IStability, IAssertions, IRetries, IJQuer // make cy global in the specWindow specWindow.cy = this - $Events.extend(this) + extendEvents(this) } $$ (selector, context) { @@ -361,7 +362,7 @@ export class $Cy implements ITimeouts, IStability, IAssertions, IRetries, IJQuer return this.queue.stopped } - fail (err, options = {}) { + fail (err, options: { async?: boolean } = {}) { // 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 @@ -443,7 +444,7 @@ export class $Cy implements ITimeouts, IStability, IAssertions, IRetries, IJQuer try { // collect all of the callbacks for 'fail' rets = this.Cypress.action('cy:fail', err, this.state('runnable')) - } catch (cyFailErr) { + } catch (cyFailErr: any) { // and if any of these throw synchronously immediately error cyFailErr.isCyFailErr = true @@ -506,7 +507,7 @@ export class $Cy implements ITimeouts, IStability, IAssertions, IRetries, IJQuer // about:blank in a visit, we do need these this.contentWindowListeners(getContentWindow($autIframe)) - cy.Cypress.action('app:window:load', this.state('window')) + this.Cypress.action('app:window:load', this.state('window')) // we are now stable again which is purposefully // the last event we call here, to give our event @@ -823,6 +824,8 @@ export class $Cy implements ITimeouts, IStability, IAssertions, IRetries, IJQuer r(err) } } + + return } setRunnable (runnable, hookId) { @@ -1076,7 +1079,7 @@ export class $Cy implements ITimeouts, IStability, IAssertions, IRetries, IJQuer $Listeners.bindTo(contentWindow, { // eslint-disable-next-line @cypress/dev/arrow-body-multiline-braces onError: (handlerType) => (event) => { - const { originalErr, err, promise } = $errUtils.errorFromUncaughtEvent(handlerType, event) + const { originalErr, err, promise } = $errUtils.errorFromUncaughtEvent(handlerType, event) as ErrorFromProjectRejectionEvent const handled = cy.onUncaughtException({ err, promise, @@ -1166,7 +1169,8 @@ export class $Cy implements ITimeouts, IStability, IAssertions, IRetries, IJQuer return this.Cypress.action('cy:command:enqueued', obj) } - private getCommandsUntilFirstParentOrValidSubject (command, memo = []) { + // TODO: Replace any with Command type. + private getCommandsUntilFirstParentOrValidSubject (command, memo: any[] = []) { if (!command) { return null } @@ -1182,11 +1186,12 @@ export class $Cy implements ITimeouts, IStability, IAssertions, IRetries, IJQuer return this.getCommandsUntilFirstParentOrValidSubject(command.get('prev'), memo) } - private pushSubjectAndValidate (name, args, firstCall, prevSubject) { + // TODO: make string[] more + private pushSubjectAndValidate (name, args, firstCall, prevSubject: string[]) { if (firstCall) { // if we have a prevSubject then error // since we're invoking this improperly - if (prevSubject && ![].concat(prevSubject).includes('optional')) { + if (prevSubject && !([] as string[]).concat(prevSubject).includes('optional')) { const stringifiedArg = $utils.stringifyActual(args[0]) $errUtils.throwErrByPath('miscellaneous.invoking_child_without_parent', { @@ -1208,7 +1213,7 @@ export class $Cy implements ITimeouts, IStability, IAssertions, IRetries, IJQuer if (prevSubject) { // make sure our current subject is valid for // what we expect in this command - this.ensureSubjectByType(subject, prevSubject, name) + this.ensureSubjectByType(subject, prevSubject) } args.unshift(subject) diff --git a/packages/driver/src/cypress/error_utils.ts b/packages/driver/src/cypress/error_utils.ts index 3a589d5168f8..0645822e71f8 100644 --- a/packages/driver/src/cypress/error_utils.ts +++ b/packages/driver/src/cypress/error_utils.ts @@ -265,6 +265,7 @@ export class InternalCypressError extends Error { export class CypressError extends Error { docsUrl?: string + retry?: boolean constructor (message) { super(message) @@ -469,7 +470,12 @@ const convertErrorEventPropertiesToObject = (args) => { }) } -const errorFromErrorEvent = (event) => { +export interface ErrorFromErrorEvent { + originalErr: Error + err: Error +} + +const errorFromErrorEvent = (event): ErrorFromErrorEvent => { let { message, filename, lineno, colno, error } = event let docsUrl = error?.docsUrl @@ -497,7 +503,11 @@ const errorFromErrorEvent = (event) => { } } -const errorFromProjectRejectionEvent = (event) => { +export interface ErrorFromProjectRejectionEvent extends ErrorFromErrorEvent { + promise: Promise +} + +const errorFromProjectRejectionEvent = (event): ErrorFromProjectRejectionEvent => { // 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 diff --git a/packages/driver/src/cypress/stack_utils.ts b/packages/driver/src/cypress/stack_utils.ts index 49028d2405fd..0a3d2a188d3d 100644 --- a/packages/driver/src/cypress/stack_utils.ts +++ b/packages/driver/src/cypress/stack_utils.ts @@ -135,7 +135,7 @@ const getCodeFrameFromSource = (sourceCode, { line, column, relativeFile, absolu } } -const captureUserInvocationStack = (ErrorConstructor, userInvocationStack) => { +const captureUserInvocationStack = (ErrorConstructor, userInvocationStack?) => { if (!userInvocationStack) { const newErr = new ErrorConstructor('userInvocationStack') diff --git a/packages/driver/src/dom/elements/detached.ts b/packages/driver/src/dom/elements/detached.ts index 96074001cc1e..6864d45fefc4 100644 --- a/packages/driver/src/dom/elements/detached.ts +++ b/packages/driver/src/dom/elements/detached.ts @@ -44,10 +44,10 @@ export const isAttached = function ($el) { }) } -export const isDetachedEl = (el: HTMLElement) => { +export const isDetachedEl = (el: HTMLElement | JQuery) => { return !isAttachedEl(el) } -export const isAttachedEl = function (el: HTMLElement) { +export const isAttachedEl = function (el: HTMLElement | JQuery) { return isAttached($(el)) }