diff --git a/circle.yml b/circle.yml index 80f537aa058e..1c1bc377f560 100644 --- a/circle.yml +++ b/circle.yml @@ -27,7 +27,7 @@ mainBuildFilters: &mainBuildFilters branches: only: - develop - - tbiethman/electron-19 + - revert-22742 # usually we don't build Mac app - it takes a long time # but sometimes we want to really confirm we are doing the right thing @@ -36,7 +36,7 @@ macWorkflowFilters: &darwin-workflow-filters when: or: - equal: [ develop, << pipeline.git.branch >> ] - - equal: [ 'tbiethman/electron-19', << pipeline.git.branch >> ] + - equal: [ 'revert-22742', << pipeline.git.branch >> ] - matches: pattern: "-release$" value: << pipeline.git.branch >> @@ -45,7 +45,7 @@ linuxArm64WorkflowFilters: &linux-arm64-workflow-filters when: or: - equal: [ develop, << pipeline.git.branch >> ] - - equal: [ 'tbiethman/electron-19', << pipeline.git.branch >> ] + - equal: [ 'revert-22742', << pipeline.git.branch >> ] - matches: pattern: "-release$" value: << pipeline.git.branch >> @@ -129,7 +129,7 @@ commands: - run: name: Check current branch to persist artifacts command: | - if [[ "$CIRCLE_BRANCH" != "develop" && "$CIRCLE_BRANCH" != "tbiethman/electron-19" ]]; then + if [[ "$CIRCLE_BRANCH" != "develop" && "$CIRCLE_BRANCH" != "revert-22742" ]]; then echo "Not uploading artifacts or posting install comment for this branch." circleci-agent step halt fi @@ -883,7 +883,7 @@ commands: fi curl -L https://raw.githubusercontent.com/cypress-io/cypress/$branch/scripts/ensure-node.sh --output ci-ensure-node.sh - else + else # if no .node-version file exists, we no-op the node script and use the global yarn echo '' > ci-ensure-node.sh fi diff --git a/packages/driver/cypress/e2e/commands/assertions.cy.js b/packages/driver/cypress/e2e/commands/assertions.cy.js index f69f28289212..74a48e597732 100644 --- a/packages/driver/cypress/e2e/commands/assertions.cy.js +++ b/packages/driver/cypress/e2e/commands/assertions.cy.js @@ -806,7 +806,7 @@ describe('src/cy/commands/assertions', () => { }) cy.get('body').then(() => { - expect(cy.currentSubject()).to.match('body') + expect(cy.state('subject')).to.match('body') }) }) diff --git a/packages/driver/cypress/e2e/commands/commands.cy.js b/packages/driver/cypress/e2e/commands/commands.cy.js index 3ce58a8f55c2..f873b502f58e 100644 --- a/packages/driver/cypress/e2e/commands/commands.cy.js +++ b/packages/driver/cypress/e2e/commands/commands.cy.js @@ -58,8 +58,7 @@ describe('src/cy/commands/commands', () => { cy .get('input:first') .parent() - .command('login', 'brian@foo.com') - .then(($input) => { + .command('login', 'brian@foo.com').then(($input) => { expect($input.get(0)).to.eq(input.get(0)) }) }) diff --git a/packages/driver/cypress/e2e/commands/connectors.cy.js b/packages/driver/cypress/e2e/commands/connectors.cy.js index 9d04419a777f..fd4d23e3c5e0 100644 --- a/packages/driver/cypress/e2e/commands/connectors.cy.js +++ b/packages/driver/cypress/e2e/commands/connectors.cy.js @@ -158,7 +158,7 @@ describe('src/cy/commands/connectors', () => { }) }) - it('should pass the eventually resolved thenable value downstream', () => { + it('should pass the eventual resolved thenable value downstream', () => { cy .wrap({ foo: 'bar' }) .then((obj) => { @@ -166,8 +166,7 @@ describe('src/cy/commands/connectors', () => { .wait(10) .then(() => { return obj.foo - }) - .then((value) => { + }).then((value) => { expect(value).to.eq('bar') return value @@ -310,7 +309,7 @@ describe('src/cy/commands/connectors', () => { return $div }) .then(function () { - expect(cy.currentSubject()).not.to.be.instanceof(this.remoteWindow.$) + expect(cy.state('subject')).not.to.be.instanceof(this.remoteWindow.$) }) }) }) @@ -424,6 +423,7 @@ describe('src/cy/commands/connectors', () => { cy.get('div:first').invoke('parent').then(function ($parent) { expect($parent).to.be.instanceof(this.remoteWindow.$) + expect(cy.state('subject')).to.match(parent) }) }) }) diff --git a/packages/driver/cypress/e2e/cypress/cy.cy.js b/packages/driver/cypress/e2e/cypress/cy.cy.js index 2fc6dbfbf507..8caac130fd2d 100644 --- a/packages/driver/cypress/e2e/cypress/cy.cy.js +++ b/packages/driver/cypress/e2e/cypress/cy.cy.js @@ -133,16 +133,6 @@ describe('driver/src/cypress/cy', () => { expect(userInvocationStack).to.include('.cy.js') }) }) - - it('supports cy.state(\'subject\') for backwards compatability', () => { - cy.stub(Cypress.utils, 'warning') - const a = {} - - cy.wrap(a).then(() => { - expect(cy.state('subject')).to.equal(a) - expect(Cypress.utils.warning).to.be.calledWith('`cy.state(\'subject\')` has been deprecated and will be removed in a future release. Consider migrating to `cy.currentSubject()` instead.') - }) - }) }) context('custom commands', () => { diff --git a/packages/driver/src/cross-origin/origin_fn.ts b/packages/driver/src/cross-origin/origin_fn.ts index 505284c4a4cf..fbb4416cb9d0 100644 --- a/packages/driver/src/cross-origin/origin_fn.ts +++ b/packages/driver/src/cross-origin/origin_fn.ts @@ -105,7 +105,7 @@ export const handleOriginFn = (Cypress: Cypress.Cypress, cy: $Cy) => { queueFinished = true setRunnableStateToPassed() Cypress.specBridgeCommunicator.toPrimary('queue:finished', { - subject: cy.currentSubject(), + subject: cy.state('subject'), }, { syncGlobals: true, }) diff --git a/packages/driver/src/cy/commands/connectors.ts b/packages/driver/src/cy/commands/connectors.ts index 2fcdf235e227..22dd18dfb5e0 100644 --- a/packages/driver/src/cy/commands/connectors.ts +++ b/packages/driver/src/cy/commands/connectors.ts @@ -100,9 +100,47 @@ export default function (Commands, Cypress, cy, state) { cy.once('command:enqueued', enqueuedCommand) + // this code helps juggle subjects forward + // the same way that promises work + const current = state('current') + const next = current.get('next') + + // TODO: this code may no longer be necessary + // if the next command is chained to us then when it eventually + // runs we need to reset the subject to be the return value of the + // previous command so the subject is continuously juggled forward + if (next && next.get('chainerId') === current.get('chainerId')) { + const checkSubject = (newSubject, args) => { + if (state('current') !== next) { + return + } + + // get whatever the previous commands return + // value is. this likely does not match the 'var current' + // command in the case of nested cy commands + const s = next.get('prev').get('subject') + + // find the new subject and splice it out + // with our existing subject + const index = _.indexOf(args, newSubject) + + if (index > -1) { + args.splice(index, 1, s) + } + + return cy.removeListener('next:subject:prepared', checkSubject) + } + + cy.on('next:subject:prepared', checkSubject) + } + const getRet = () => { let ret = fn.apply(ctx, args) + if (cy.isCy(ret)) { + ret = undefined + } + if (ret && invokedCyCommand && !ret.then) { $errUtils.throwErrByPath('then.callback_mixes_sync_and_async', { onFail: options._log, @@ -123,11 +161,6 @@ export default function (Commands, Cypress, cy, state) { return subject } - // If the user callback returned a non-null value, we break cypress' subject chaining - // logic, so that we can use this subject as-is rather than the subject generated by - // any chainers inside the callback (if any exist). - cy.breakSubjectLinksToCurrentChainer() - return ret }).catch(Promise.TimeoutError, () => { return $errUtils.throwErrByPath('invoke_its.timed_out', { @@ -465,16 +498,54 @@ export default function (Commands, Cypress, cy, state) { $errUtils.throwErrByPath('each.invalid_argument') } - if (subject?.length === undefined) { + const nonArray = () => { return $errUtils.throwErrByPath('each.non_array', { args: { subject: $utils.stringify(subject) }, }) } + try { + if (!('length' in subject)) { + nonArray() + } + } catch (e) { + nonArray() + } + if (subject.length === 0) { return subject } + // if we have a next command then we need to + // slice in this existing subject as its subject + // due to the way we queue promises + const next = state('current').get('next') + + if (next) { + const checkSubject = (newSubject, args, firstCall) => { + if (state('current') !== next) { + return + } + + // https://github.com/cypress-io/cypress/issues/4921 + // When dual commands like contains() is used as the firstCall (cy.contains() style), + // we should not prepend subject. + if (!firstCall) { + // find the new subject and splice it out + // with our existing subject + const index = _.indexOf(args, newSubject) + + if (index > -1) { + args.splice(index, 1, subject) + } + } + + return cy.removeListener('next:subject:prepared', checkSubject) + } + + cy.on('next:subject:prepared', checkSubject) + } + let endEarly = false const yieldItem = (el, index) => { @@ -502,16 +573,11 @@ export default function (Commands, Cypress, cy, state) { // generate a real array since bluebird is finicky and // doesnt want an 'array-like' structure like jquery instances + // need to take into account regular arrays here by first checking + // if its an array instance return Promise .each(_.toArray(subject), yieldItem) - .then(() => { - // cy.each does *not* want to use any subjects that the user's callback generated - therefore we break - // cypress' subject chaining logic, which by default would override this with any subjects generated by - // the callback function. - cy.breakSubjectLinksToCurrentChainer() - - return subject - }) + .return(subject) }, }) diff --git a/packages/driver/src/cy/commands/misc.ts b/packages/driver/src/cy/commands/misc.ts index cc6a616979de..54d862663cc6 100644 --- a/packages/driver/src/cy/commands/misc.ts +++ b/packages/driver/src/cy/commands/misc.ts @@ -32,7 +32,7 @@ export default (Commands, Cypress, cy, state) => { const restoreCmdIndex = state('index') + 1 cy.queue.insert(restoreCmdIndex, $Command.create({ - args: [cy.currentSubject()], + args: [state('subject')], name: 'log-restore', fn: (subject) => subject, })) diff --git a/packages/driver/src/cy/commands/querying/within.ts b/packages/driver/src/cy/commands/querying/within.ts index 7b894e0f0066..a2f3092040bc 100644 --- a/packages/driver/src/cy/commands/querying/within.ts +++ b/packages/driver/src/cy/commands/querying/within.ts @@ -35,9 +35,7 @@ export default (Commands, Cypress, cy, state) => { fn.call(cy.state('ctx'), subject) - const cleanup = () => { - cy.removeListener('command:start', setWithinSubject) - } + const cleanup = () => cy.removeListener('command:start', setWithinSubject) // we need a mechanism to know when we should remove // our withinSubject so we dont accidentally keep it @@ -78,10 +76,6 @@ export default (Commands, Cypress, cy, state) => { }) } - // TODO: Rework cy.within to use chainer-based subject chaining, rather than its custom withinSubject state. - // For now, we leave this logic in place and just ensure that the new rules don't interfere with it. - cy.breakSubjectLinksToCurrentChainer() - return subject } diff --git a/packages/driver/src/cy/logGroup.ts b/packages/driver/src/cy/logGroup.ts index 0382e2c03178..908ea08dad12 100644 --- a/packages/driver/src/cy/logGroup.ts +++ b/packages/driver/src/cy/logGroup.ts @@ -27,7 +27,6 @@ export default (Cypress, userOptions: Cypress.LogGroup.Config, fn: Cypress.LogGr const endLogGroupCmd = $Command.create({ name: 'end-logGroup', injected: true, - args: [], }) const forwardYieldedSubject = () => { diff --git a/packages/driver/src/cypress.ts b/packages/driver/src/cypress.ts index ea1ba2ccfbf4..f5e74110bdcc 100644 --- a/packages/driver/src/cypress.ts +++ b/packages/driver/src/cypress.ts @@ -218,24 +218,6 @@ class $Cypress { _.extend(this, browserInfo(config)) this.state = $SetterGetter.create({}) as unknown as StateFunc - - /* - * As part of the Detached DOM effort, we're changing the way subjects are determined in Cypress. - * While we usually consider cy.state() to be internal, in the case of cy.state('subject'), - * cypress-testing-library, one of our most popular plugins, relies on it. - * https://github.com/testing-library/cypress-testing-library/blob/1af9f2f28b2ca62936da8a8acca81fc87e2192f7/src/utils.js#L9 - * - * Therefore, we've added this shim to continue to support them. The library is actively maintained, so this - * shouldn't need to stick around too long (written 07/22). - */ - Object.defineProperty(this.state(), 'subject', { - get: () => { - $errUtils.warnByPath('subject.state_subject_deprecated') - - return cy.currentSubject() - }, - }) - this.originalConfig = _.cloneDeep(config) this.config = $SetterGetter.create(config, (config) => { if (this.isCrossOriginSpecBridge ? !window.__cySkipValidateConfig : !window.top!.__cySkipValidateConfig) { @@ -658,6 +640,9 @@ class $Cypress { case 'cy:url:changed': return this.emit('url:changed', args[0]) + case 'cy:next:subject:prepared': + return this.emit('next:subject:prepared', ...args) + case 'cy:collect:run:state': return this.emitThen('collect:run:state') diff --git a/packages/driver/src/cypress/chainer.ts b/packages/driver/src/cypress/chainer.ts index 96c63dcf05f6..ecd89a385640 100644 --- a/packages/driver/src/cypress/chainer.ts +++ b/packages/driver/src/cypress/chainer.ts @@ -4,6 +4,7 @@ import $stackUtils from './stack_utils' export class $Chainer { specWindow: Window chainerId: string + firstCall: boolean constructor (specWindow) { this.specWindow = specWindow @@ -11,6 +12,14 @@ export class $Chainer { // collisions when chainers created in a secondary origin are passed // to the primary origin for the command log, etc. this.chainerId = _.uniqueId(`ch-${window.location.origin}-`) + + // firstCall is used to throw a useful error if the user leads off with a + // parent command. + + // TODO: Refactor firstCall out of the chainer and into the command function, + // since cy.ts already has all the necessary information to throw this error + // without an instance variable, in one localized place in the code. + this.firstCall = true } static remove (key) { diff --git a/packages/driver/src/cypress/command_queue.ts b/packages/driver/src/cypress/command_queue.ts index 4275b797a51d..166a7e2ea7a2 100644 --- a/packages/driver/src/cypress/command_queue.ts +++ b/packages/driver/src/cypress/command_queue.ts @@ -15,8 +15,8 @@ import type { ITimeouts } from '../cy/timeouts' const debugErrors = Debug('cypress:driver:errors') -const __stackReplacementMarker = (fn, args) => { - return fn(...args) +const __stackReplacementMarker = (fn, ctx, args) => { + return fn.apply(ctx, args) } const commandRunningFailed = (Cypress, state, err) => { @@ -165,11 +165,9 @@ export class CommandQueue extends Queue<$Command> { Cypress.once('command:enqueued', commandEnqueued) } - args = [command.get('chainerId'), ...args] - // run the command's fn with runnable's context try { - ret = __stackReplacementMarker(command.get('fn'), args) + ret = __stackReplacementMarker(command.get('fn'), this.state('ctx'), args) } catch (err) { throw err } finally { @@ -249,7 +247,7 @@ export class CommandQueue extends Queue<$Command> { // we're finished with the current command so set it back to null this.state('current', null) - cy.setSubjectForChainer(command.get('chainerId'), subject) + this.state('subject', subject) return subject }) @@ -280,8 +278,7 @@ export class CommandQueue extends Queue<$Command> { }) this.state('index', index + 1) - - cy.setSubjectForChainer(command.get('chainerId'), command.get('subject')) + this.state('subject', command.get('subject')) Cypress.action('cy:skipped:command:end', command) diff --git a/packages/driver/src/cypress/commands.ts b/packages/driver/src/cypress/commands.ts index c99e3c52567e..261c20b9c5f8 100644 --- a/packages/driver/src/cypress/commands.ts +++ b/packages/driver/src/cypress/commands.ts @@ -13,6 +13,75 @@ const builtInCommands = [ addNetstubbingCommand, ] +const reservedCommandNames = { + addAlias: true, + addCommand: true, + addCommandSync: true, + aliasNotFoundFor: true, + assert: true, + clearTimeout: true, + config: true, + createSnapshot: true, + detachDom: true, + devices: true, + documentHasFocus: true, + ensureAttached: true, + ensureDescendents: true, + ensureDocument: true, + ensureElDoesNotHaveCSS: true, + ensureElExistence: true, + ensureElement: true, + ensureElementIsNotAnimating: true, + ensureNotDisabled: true, + ensureNotHiddenByAncestors: true, + ensureNotReadonly: true, + ensureRunnable: true, + ensureScrollability: true, + ensureStrictVisibility: true, + ensureSubjectByType: true, + ensureValidPosition: true, + ensureVisibility: true, + ensureWindow: true, + expect: true, + fail: true, + fireBlur: true, + fireFocus: true, + getFocused: true, + getIndexedXhrByAlias: true, + getNextAlias: true, + getRemoteLocation: true, + getRemotejQueryInstance: true, + getRequestsByAlias: true, + getStyles: true, + getXhrTypeByAlias: true, + id: true, + initialize: true, + interceptBlur: true, + interceptFocus: true, + isCy: true, + isStable: true, + isStopped: true, + needsFocus: true, + now: true, + onBeforeAppWindowLoad: true, + onBeforeWindowLoad: true, + onCssModified: true, + onUncaughtException: true, + pauseTimers: true, + queue: true, + replayCommandsFrom: true, + reset: true, + resetTimer: true, + retry: true, + setRunnable: true, + state: true, + stop: true, + timeout: true, + validateAlias: true, + verifyUpcomingAssertions: true, + whenStable: true, +} + const getTypeByPrevSubject = (prevSubject) => { if (prevSubject === 'optional') { return 'dual' @@ -27,7 +96,6 @@ const getTypeByPrevSubject = (prevSubject) => { export default { create: (Cypress, cy, state, config) => { - const reservedCommandNames = new Set(Object.keys(cy)) // create a single instance // of commands const commands = {} @@ -146,7 +214,7 @@ export default { }) } - if (reservedCommandNames.has(name)) { + if (reservedCommandNames[name]) { $errUtils.throwErrByPath('miscellaneous.reserved_command', { args: { name, @@ -186,6 +254,11 @@ export default { }) }, + addSelector (name, fn) { + // TODO: Add overriding stuff. + return cy.addSelector(name, fn) + }, + overwrite (name, fn) { return storeOverride(name, fn) }, diff --git a/packages/driver/src/cypress/cy.ts b/packages/driver/src/cypress/cy.ts index f2f5e8719f02..0341c82ca3fe 100644 --- a/packages/driver/src/cypress/cy.ts +++ b/packages/driver/src/cypress/cy.ts @@ -695,33 +695,37 @@ export class $Cy extends EventEmitter2 implements ITimeouts, IStability, IAssert addCommand ({ name, fn, type, prevSubject }) { const cy = this + // TODO: prob don't need this anymore this.commandFns[name] = fn const wrap = function (firstCall) { + fn = cy.commandFns[name] + const wrapped = wrapByType(fn, firstCall) + + wrapped.originalFn = fn + + return wrapped + } + + const wrapByType = function (fn, firstCall) { if (type === 'parent') { - return (chainerId, ...args) => fn.apply(cy.runnableCtx(name), args) + return fn } - const wrapped = function (chainerId, ...args) { + // child, dual, assertion, utility command + // pushes the previous subject into them + // after verifying its of the correct type + return function (...args) { // push the subject into the args - if (firstCall) { - cy.validateFirstCall(name, args, prevSubject) - } - - args = cy.pushSubject(name, args, prevSubject, chainerId) + args = cy.pushSubjectAndValidate(name, args, firstCall, prevSubject) return fn.apply(cy.runnableCtx(name), args) } - - wrapped.originalFn = fn - - return wrapped } - const cyFn = wrap(true) - const chainerFn = wrap(false) + const callback = (chainer, userInvocationStack, args) => { + const { firstCall, chainerId } = chainer - const callback = (chainer, userInvocationStack, args, firstCall = false) => { // dont enqueue / inject any new commands if // onInjectCommand returns false const onInjectCommand = cy.state('onInjectCommand') @@ -737,11 +741,13 @@ export class $Cy extends EventEmitter2 implements ITimeouts, IStability, IAssert name, args, type, - chainerId: chainer.chainerId, + chainerId, userInvocationStack, injected, - fn: firstCall ? cyFn : chainerFn, + fn: wrap(firstCall), }) + + chainer.firstCall = false } $Chainer.add(name, callback) @@ -753,13 +759,9 @@ export class $Cy extends EventEmitter2 implements ITimeouts, IStability, IAssert // so create a new chainer instance const chainer = new $Chainer(cy.specWindow) - if (cy.state('chainerId')) { - cy.linkSubject(chainer.chainerId, cy.state('chainerId')) - } - const userInvocationStack = $stackUtils.captureUserInvocationStack(cy.specWindow.Error) - callback(chainer, userInvocationStack, args, true) + callback(chainer, userInvocationStack, args) // if we are in the middle of a command // and its return value is a promise @@ -1220,36 +1222,32 @@ export class $Cy extends EventEmitter2 implements ITimeouts, IStability, IAssert return memo } - // A workaround to ensure that when looking back, aliases don't extend beyond the current - // chainer. This whole area (`replayCommandsFrom` for aliases) will be replaced with subject chains as - // part of the detached DOM work. - const prev = command.get('prev') - - if (prev.get('chainerId') !== command.get('chainerId')) { - return memo - } - - return this.getCommandsUntilFirstParentOrValidSubject(prev, memo) + return this.getCommandsUntilFirstParentOrValidSubject(command.get('prev'), memo) } - private validateFirstCall (name, args, prevSubject: string[]) { - // if we have a prevSubject then error - // since we're invoking this improperly - if (prevSubject && !([] as string[]).concat(prevSubject).includes('optional')) { - const stringifiedArg = $utils.stringifyActual(args[0]) - - $errUtils.throwErrByPath('miscellaneous.invoking_child_without_parent', { - args: { - cmd: name, - args: _.isString(args[0]) ? `\"${stringifiedArg}\"` : stringifiedArg, - }, - }) + // 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 && !([] as string[]).concat(prevSubject).includes('optional')) { + const stringifiedArg = $utils.stringifyActual(args[0]) + + $errUtils.throwErrByPath('miscellaneous.invoking_child_without_parent', { + args: { + cmd: name, + args: _.isString(args[0]) ? `\"${stringifiedArg}\"` : stringifiedArg, + }, + }) + } + + // else if this is the very first call + // on the chainer then make the first + // argument undefined (we have no subject) + this.state('subject', undefined) } - } - // TODO: make string[] more - private pushSubject (name, args, prevSubject: string[], chainerId) { - const subject = this.currentSubject(chainerId) + const subject = this.state('subject') if (prevSubject) { // make sure our current subject is valid for @@ -1259,98 +1257,9 @@ export class $Cy extends EventEmitter2 implements ITimeouts, IStability, IAssert args.unshift(subject) - return args - } + this.Cypress.action('cy:next:subject:prepared', subject, args, firstCall) - /* - * Use `currentSubject()` to get the subject. It reads from cy.state('subjects'), but the format and details of - * determining this should be considered an internal implementation detail of Cypress, subject to change at any time. - * - * Currently, state('subjects') is an object, mapping chainerIds to the current subject for that chainer. For - * example, it might look like: - * - * { - * 'chainer2': 'foobar', - * 'chainer4': , - * } - * - * Do not read this directly; Prefer currentSubject() instead. - */ - currentSubject (chainerId = this.state('chainerId')) { - return (this.state('subjects') || {})[chainerId] - } - - /* - * Cypress executes commands asynchronously, and those commands can contain other commands - this means that there - * are times when an outer chainer might have as its subject the (as of yet unresolved) return value of the inner - * chain of commands. - * - * cy.state('subjectLinks') is where we store that connection. The exact contents should be considered an internal - * implementation detail - do not read or alter it directly, but prefer the public interface (linkSubject and - * breakSubjectLinksToCurrentChainer). - * - * In the current implementation, subjectLinks might look like: - * { - * 'chainer4': 'chainer2', - * } - * - * indicating that when we eventually resolve the subject of chainer4, it should *also* be used as the subject for - * chainer2 - for example, `cy.then(() => { return cy.get('foo') }).log()`. The inner chainer (chainer4, - * `cy.get('foo')`) is linked to the outer chainer (chainer2) - when we eventually .get('foo'), the resolved value - * becomes the new subject for the outer chainer. - * - * Whenever we are in the middle of resolving one chainer and a new one is created, Cypress links the inner chainer - * to the outer one. This is *usually* desireable, allowing simple constructs like - * `cy.then(() => { return cy.get('foo') }).log()` to function intuitively. - * - * But you don't always want to use the inner chainer's subject for the outer chainer. Consider: - * `cy.then(() => { cy.get('foo').click(); return 'success' }).log()` - * - * In this case, we want to break the connection between the inner chainer and the outer one, so that we can - * instead use the return value as the new subject. Is this case, you'll want cy.breakSubjectLinksToCurrentChainer(). - */ - linkSubject (fromChainerId, toChainerId) { - const links = this.state('subjectLinks') || {} - - links[fromChainerId] = toChainerId - this.state('subjectLinks', links) - } - - /* - * You should call breakSubjectLinksToCurrentChainer() when the following are *both* true: - * 1. A command callback may contain cypress commands. - * 2. You do not want to use the subject of those commands as the new subject of the parent command chain. - * - * In this case, call the function directly before returning the new subject, after any new cypress commands have - * been added to the queue. See `cy.linkSubject()` for more details about how links are created. - */ - breakSubjectLinksToCurrentChainer () { - const chainerId = this.state('chainerId') - const links = this.state('subjectLinks') || {} - - this.state('subjectLinks', _.omitBy(links, (l) => l === chainerId)) - } - - /* - * setSubjectForChainer should be considered an internal implementation detail of Cypress. Do not use it directly - * outside of the Cypress codebase. It is currently used only by the command_queue, and if you think it's needed - * elsewhere, consider carefully before adding additional uses. - * - * The command_queue calls setSubjectForChainer after a command has finished resolving, when we have the final - * (non-$Chainer, non-promise) return value. This value becomes the current $Chainer's new subject - and the new - * subject for any chainers it's linked to (see cy.linkSubject for details on that process). - */ - setSubjectForChainer (chainerId: string, subject: any) { - const cySubject = this.state('subjects') || {} - - cySubject[chainerId] = subject - this.state('subjects', cySubject) - - const links = this.state('subjectLinks') || {} - - if (links[chainerId]) { - this.setSubjectForChainer(links[chainerId], subject) - } + return args } private doneEarly () { diff --git a/packages/driver/src/cypress/error_messages.ts b/packages/driver/src/cypress/error_messages.ts index 380f835bf3a9..63479cc6a7e3 100644 --- a/packages/driver/src/cypress/error_messages.ts +++ b/packages/driver/src/cypress/error_messages.ts @@ -1858,9 +1858,6 @@ export default { > ${cmd(obj.previous)}` }, - state_subject_deprecated: { - message: `${cmd('state', '\'subject\'')} has been deprecated and will be removed in a future release. Consider migrating to ${cmd('currentSubject')} instead.`, - }, }, submit: { diff --git a/packages/driver/src/cypress/log.ts b/packages/driver/src/cypress/log.ts index 7346dbf178fa..6bec5bf54100 100644 --- a/packages/driver/src/cypress/log.ts +++ b/packages/driver/src/cypress/log.ts @@ -166,9 +166,7 @@ const defaults = function (state: StateFunc, config, obj) { // so it can conditionally return either // parent or child (useful in assertions) if (_.isFunction(obj.type)) { - const chainerId = current && current.get('chainerId') - - obj.type = obj.type(current, (state('subjects') || {})[chainerId]) + obj.type = obj.type(current, state('subject')) } } diff --git a/packages/driver/src/util/serialization/index.ts b/packages/driver/src/util/serialization/index.ts index 16a2a6cface0..2d4bce4cfb0f 100644 --- a/packages/driver/src/util/serialization/index.ts +++ b/packages/driver/src/util/serialization/index.ts @@ -72,7 +72,7 @@ const convertObjectToSerializableLiteral = (obj): typeof obj => { }) currentObjectRef = Object.getPrototypeOf(currentObjectRef) - } while (currentObjectRef && currentObjectRef !== Object.prototype && currentObjectRef !== Date.prototype) + } while (currentObjectRef) const objectAsLiteral = {} diff --git a/scripts/run-webpack.js b/scripts/run-webpack.js index 6cf39d48bd1b..1d179c4525e8 100644 --- a/scripts/run-webpack.js +++ b/scripts/run-webpack.js @@ -21,10 +21,10 @@ if (process.versions && semver.satisfies(process.versions.node, '>=17.0.0') && s } function buildCommand () { - const file = process.argv.slice(2) + const file = process.argv.slice(2).join(' ') ?? '' let program = `node "${webpackCli}"` - return file.length ? `${program } "${file.join('" "')}"` : program + return file ? `${program } "${file}"` : program } const program = buildCommand()