From c3147df277fdf04e1360060e180c6a62e1af5a3e Mon Sep 17 00:00:00 2001 From: Emily Rohrbough Date: Thu, 28 Apr 2022 14:40:40 -0500 Subject: [PATCH] feat: update .within to use internal log group api (#20980) --- .../e2e/commands/querying/within.cy.js | 31 ++++ .../driver/src/cy/commands/querying/within.ts | 166 +++++++++--------- packages/driver/src/cypress/command.ts | 2 +- 3 files changed, 117 insertions(+), 82 deletions(-) diff --git a/packages/driver/cypress/e2e/commands/querying/within.cy.js b/packages/driver/cypress/e2e/commands/querying/within.cy.js index bda7f8eaf5..d759ab8092 100644 --- a/packages/driver/cypress/e2e/commands/querying/within.cy.js +++ b/packages/driver/cypress/e2e/commands/querying/within.cy.js @@ -48,6 +48,23 @@ describe('src/cy/commands/querying/within', () => { }) }) + it('can be chained off an alias', () => { + const form = cy.$$('#by-name') + + cy.get('#by-name').as('nameForm') + .within(() => {}) + .then(($form) => { + expect($form.get(0)).to.eq(form.get(0)) + }) + + cy.get('#by-name').as('nameForm') + .within(() => { + cy.get('input').should('be.visible') + }) + + cy.get('@nameForm').should('be.visible') + }) + it('can call child commands after within on the same subject', () => { const input = cy.$$('#by-name input:first') @@ -199,6 +216,20 @@ describe('src/cy/commands/querying/within', () => { expect(lastLog.get('snapshots')[0]).to.be.an('object') }) }) + + it('provides additional information in console prop', () => { + cy.get('div').within(() => {}) + .then(function () { + const { lastLog } = this + + const consoleProps = lastLog.get('consoleProps')() + + expect(consoleProps).to.be.an('object') + expect(consoleProps.Command).to.eq('within') + expect(consoleProps.Yielded).to.not.be.null + expect(consoleProps.Yielded).to.have.length(55) + }) + }) }) describe('errors', { diff --git a/packages/driver/src/cy/commands/querying/within.ts b/packages/driver/src/cy/commands/querying/within.ts index ad5ed66c7d..a2f3092040 100644 --- a/packages/driver/src/cy/commands/querying/within.ts +++ b/packages/driver/src/cy/commands/querying/within.ts @@ -2,12 +2,86 @@ import _ from 'lodash' import { $Command } from '../../../cypress/command' import $errUtils from '../../../cypress/error_utils' +import group from '../../logGroup' export default (Commands, Cypress, cy, state) => { + const withinFn = (subject, fn) => { + // reference the next command after this + // within. when that command runs we'll + // know to remove withinSubject + const next = state('current').get('next') + + // backup the current withinSubject + // this prevents a bug where we null out + // withinSubject when there are nested .withins() + // we want the inner within to restore the outer + // once its done + const prevWithinSubject = state('withinSubject') + + state('withinSubject', subject) + + // https://github.com/cypress-io/cypress/pull/8699 + // An internal command is inserted to create a divider between + // commands inside within() callback and commands chained to it. + const restoreCmdIndex = state('index') + 1 + + cy.queue.insert(restoreCmdIndex, $Command.create({ + args: [subject], + name: 'within-restore', + fn: (subject) => subject, + })) + + state('index', restoreCmdIndex) + + fn.call(cy.state('ctx'), subject) + + 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 + // around after the within callback is done executing + // so when each command starts, check to see if this + // is the command which references our 'next' and + // if so, remove the within subject + const setWithinSubject = (obj) => { + if (obj !== next) { + return + } + + // okay so what we're doing here is creating a property + // which stores the 'next' command which will reset the + // withinSubject. If two 'within' commands reference the + // exact same 'next' command, then this prevents accidentally + // resetting withinSubject more than once. If they point + // to different 'next's then its okay + if (next !== state('nextWithinSubject')) { + state('withinSubject', prevWithinSubject || null) + state('nextWithinSubject', next) + } + + // regardless nuke this listeners + cleanup() + } + + // if next is defined then we know we'll eventually + // unbind these listeners + if (next) { + cy.on('command:start', setWithinSubject) + } else { + // remove our listener if we happen to reach the end + // event which will finalize cleanup if there was no next obj + cy.once('command:queue:before:end', () => { + cleanup() + state('withinSubject', null) + }) + } + + return subject + } + Commands.addAll({ prevSubject: ['element', 'document'] }, { within (subject, options, fn) { let userOptions = options - const ctx = this if (_.isUndefined(fn)) { fn = userOptions @@ -16,90 +90,20 @@ export default (Commands, Cypress, cy, state) => { options = _.defaults({}, userOptions, { log: true }) - if (options.log) { - options._log = Cypress.log({ - $el: subject, - message: '', - timeout: options.timeout, - }) + const groupOptions: Cypress.LogGroup.Config = { + log: options.log, + $el: subject, + message: '', + timeout: options.timeout, } - if (!_.isFunction(fn)) { - $errUtils.throwErrByPath('within.invalid_argument', { onFail: options._log }) - } - - // reference the next command after this - // within. when that command runs we'll - // know to remove withinSubject - const next = state('current').get('next') - - // backup the current withinSubject - // this prevents a bug where we null out - // withinSubject when there are nested .withins() - // we want the inner within to restore the outer - // once its done - const prevWithinSubject = state('withinSubject') - - state('withinSubject', subject) - - // https://github.com/cypress-io/cypress/pull/8699 - // An internal command is inserted to create a divider between - // commands inside within() callback and commands chained to it. - const restoreCmdIndex = state('index') + 1 - - cy.queue.insert(restoreCmdIndex, $Command.create({ - args: [subject], - name: 'within-restore', - fn: (subject) => subject, - })) - - state('index', restoreCmdIndex) - - fn.call(ctx, subject) - - 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 - // around after the within callback is done executing - // so when each command starts, check to see if this - // is the command which references our 'next' and - // if so, remove the within subject - const setWithinSubject = (obj) => { - if (obj !== next) { - return + return group(Cypress, groupOptions, (log) => { + if (!_.isFunction(fn)) { + $errUtils.throwErrByPath('within.invalid_argument', { onFail: log }) } - // okay so what we're doing here is creating a property - // which stores the 'next' command which will reset the - // withinSubject. If two 'within' commands reference the - // exact same 'next' command, then this prevents accidentally - // resetting withinSubject more than once. If they point - // to differnet 'next's then its okay - if (next !== state('nextWithinSubject')) { - state('withinSubject', prevWithinSubject || null) - state('nextWithinSubject', next) - } - - // regardless nuke this listeners - cleanup() - } - - // if next is defined then we know we'll eventually - // unbind these listeners - if (next) { - cy.on('command:start', setWithinSubject) - } else { - // remove our listener if we happen to reach the end - // event which will finalize cleanup if there was no next obj - cy.once('command:queue:before:end', () => { - cleanup() - - state('withinSubject', null) - }) - } - - return subject + return withinFn(subject, fn) + }) }, }) } diff --git a/packages/driver/src/cypress/command.ts b/packages/driver/src/cypress/command.ts index 12b824d538..f4b3380cad 100644 --- a/packages/driver/src/cypress/command.ts +++ b/packages/driver/src/cypress/command.ts @@ -82,7 +82,7 @@ export class $Command { const arg = args[i] if (_.isObject(arg)) { - // filter out any properties which arent primitives + // filter out any properties which aren't primitives // to prevent accidental mutations const opts = _.omitBy(arg, _.isObject)