From 094e3d03cc9f379965a7f0fb09a9e1cf44992014 Mon Sep 17 00:00:00 2001 From: Blue F Date: Tue, 24 Jan 2023 08:02:45 -0800 Subject: [PATCH] feat: Add 'type' option to `.as` to store aliases by value (#25251) * feat: Add 'type' option to `.as` to store aliases by value Co-authored-by: Chris Breiding Co-authored-by: Emily Rohrbough --- cli/types/cypress.d.ts | 69 +++++++++++-------- cli/types/tests/cypress-tests.ts | 2 + .../cypress/e2e/commands/aliasing.cy.js | 51 ++++++++++++-- .../e2e/commands/querying/querying.cy.js | 2 +- .../driver/cypress/e2e/commands/window.cy.js | 4 +- .../e2e/e2e/origin/commands/actions.cy.ts | 4 +- .../e2e/e2e/origin/commands/aliasing.cy.ts | 2 +- packages/driver/src/cy/commands/aliasing.ts | 22 +++++- packages/driver/src/cypress/error_messages.ts | 2 + 9 files changed, 118 insertions(+), 40 deletions(-) diff --git a/cli/types/cypress.d.ts b/cli/types/cypress.d.ts index f0f4e202e6b9..6aa84eb495a8 100644 --- a/cli/types/cypress.d.ts +++ b/cli/types/cypress.d.ts @@ -827,29 +827,13 @@ declare namespace Cypress { * @see https://on.cypress.io/variables-and-aliases * @see https://on.cypress.io/get * @example - ``` - // Get the aliased 'todos' elements - cy.get('ul#todos').as('todos') - //...hack hack hack... - // later retrieve the todos - cy.get('@todos') - ``` - */ - as(alias: string): Chainable - - /** - * Select a file with the given element, or drag and drop a file over any DOM subject. + * // Get the aliased 'todos' elements + * cy.get('ul#todos').as('todos') * - * @param {FileReference} files - The file(s) to select or drag onto this element. - * @see https://on.cypress.io/selectfile - * @example - * cy.get('input[type=file]').selectFile(Cypress.Buffer.from('text')) - * cy.get('input[type=file]').selectFile({ - * fileName: 'users.json', - * contents: [{name: 'John Doe'}] - * }) + * // later retrieve the todos + * cy.get('@todos') */ - selectFile(files: FileReference | FileReference[], options?: Partial): Chainable + as(alias: string, options?: Partial): Chainable /** * Blur a focused element. This element must currently be in focus. @@ -1915,6 +1899,20 @@ declare namespace Cypress { */ select(valueOrTextOrIndex: string | number | Array, options?: Partial): Chainable + /** + * Select a file with the given element, or drag and drop a file over any DOM subject. + * + * @param {FileReference} files - The file(s) to select or drag onto this element. + * @see https://on.cypress.io/selectfile + * @example + * cy.get('input[type=file]').selectFile(Cypress.Buffer.from('text')) + * cy.get('input[type=file]').selectFile({ + * fileName: 'users.json', + * contents: [{name: 'John Doe'}] + * }) + */ + selectFile(files: FileReference | FileReference[], options?: Partial): Chainable + /** * Set a browser cookie. * @@ -2650,6 +2648,7 @@ declare namespace Cypress { waitForAnimations: boolean /** * The distance in pixels an element must exceed over time to be considered animating + * * @default 5 */ animationDistanceThreshold: number @@ -2661,15 +2660,20 @@ declare namespace Cypress { scrollBehavior: scrollBehaviorOptions } - interface SelectFileOptions extends Loggable, Timeoutable, ActionableOptions { + /** + * Options to affect how an alias is stored + * + * @see https://on.cypress.io/as + */ + interface AsOptions { /** - * Which user action to perform. `select` matches selecting a file while - * `drag-drop` matches dragging files from the operating system into the - * document. + * The type of alias to store, which impacts how the value is retrieved later in the test. + * If an alias should be a 'query' (re-runs all queries leading up to the resulting value so it's alway up-to-date) or a + * 'static' (read once when the alias is saved and is never updated). `type` has no effect when aliasing intercepts, spies, and stubs. * - * @default 'select' + * @default 'query' */ - action: 'select' | 'drag-drop' + type: 'query' | 'static' } interface BlurOptions extends Loggable, Timeoutable, Forceable { } @@ -3515,6 +3519,17 @@ declare namespace Cypress { type SameSiteStatus = 'no_restriction' | 'strict' | 'lax' + interface SelectFileOptions extends Loggable, Timeoutable, ActionableOptions { + /** + * Which user action to perform. `select` matches selecting a file while + * `drag-drop` matches dragging files from the operating system into the + * document. + * + * @default 'select' + */ + action: 'select' | 'drag-drop' + } + interface SetCookieOptions extends Loggable, Timeoutable { path: string domain: string diff --git a/cli/types/tests/cypress-tests.ts b/cli/types/tests/cypress-tests.ts index d31223d59680..ec6bb1b9facc 100644 --- a/cli/types/tests/cypress-tests.ts +++ b/cli/types/tests/cypress-tests.ts @@ -546,6 +546,8 @@ cy.stub().withArgs('').log(false).as('foo') cy.spy().withArgs('').log(false).as('foo') +cy.get('something').as('foo', {type: 'static'}) + cy.wrap('foo').then(subject => { subject // $ExpectType string return cy.wrap(subject) diff --git a/packages/driver/cypress/e2e/commands/aliasing.cy.js b/packages/driver/cypress/e2e/commands/aliasing.cy.js index 8c0f64c1b58b..a862d3fdffae 100644 --- a/packages/driver/cypress/e2e/commands/aliasing.cy.js +++ b/packages/driver/cypress/e2e/commands/aliasing.cy.js @@ -65,6 +65,20 @@ describe('src/cy/commands/aliasing', () => { cy.get('@obj').should('deep.eq', { foo: 'bar' }) }) + it('allows users to store a static value', () => { + const obj = { foo: 'bar' } + + cy.wrap(obj).its('foo').as('alias1', { type: 'static' }) + cy.wrap(obj).its('foo').as('alias2', { type: 'query' }) + + cy.then(() => { + obj.foo = 'baz' + }) + + cy.get('@alias1').should('eq', 'bar') + cy.get('@alias2').should('eq', 'baz') + }) + it('allows dot in alias names', () => { cy.get('body').as('body.foo').then(() => { expect(cy.state('aliases')['body.foo']).to.exist @@ -236,6 +250,28 @@ describe('src/cy/commands/aliasing', () => { cy.get('div:first').as(reserved) }) }) + + it('throws when given non-object options', (done) => { + cy.on('fail', (err) => { + expect(err.message).to.eq(`\`cy.as()\` only accepts an options object for its second argument. You passed: \`wut?\``) + expect(err.docsUrl).to.eq('https://on.cypress.io/as') + + done() + }) + + cy.wrap({}).as('value', 'wut?') + }) + + it('throws when given invalid `type`', (done) => { + cy.on('fail', (err) => { + expect(err.message).to.eq(`\`cy.as()\` only accepts a \`type\` of \`'query'\` or \`'static'\`. You passed: \`wut?\``) + expect(err.docsUrl).to.eq('https://on.cypress.io/as') + + done() + }) + + cy.wrap({}).as('value', { type: 'wut?' }) + }) }) describe('log', () => { @@ -284,11 +320,19 @@ describe('src/cy/commands/aliasing', () => { const { lastLog } = this assertLogLength(this.logs, 1) - expect(lastLog.get('alias')).to.eq('foo') + expect(lastLog.get('alias')).to.eq('@foo') expect(lastLog.get('aliasType')).to.eq('dom') }) }) + it('includes the alias `type` when set to `static`', () => { + cy.wrap({}).as('foo', { type: 'static' }).then(function () { + const { lastLog } = this + + expect(lastLog.get('alias')).to.eq('@foo (static)') + }) + }) + it('does not match alias when the alias has already been applied', () => { cy .visit('/fixtures/commands.html') @@ -495,9 +539,8 @@ describe('src/cy/commands/aliasing', () => { }) }) - // TODO: Re-enable as part of https://github.com/cypress-io/cypress/issues/23902 - it.skip('maintains .within() context while reading aliases', () => { - cy.get('#specific-contains').within(() => { + it('maintains .within() context while reading aliases', () => { + cy.get('#nested-div').within(() => { cy.get('span').as('spanWithin').should('have.length', 1) }) diff --git a/packages/driver/cypress/e2e/commands/querying/querying.cy.js b/packages/driver/cypress/e2e/commands/querying/querying.cy.js index abb5f7a8c370..d4238d3774b2 100644 --- a/packages/driver/cypress/e2e/commands/querying/querying.cy.js +++ b/packages/driver/cypress/e2e/commands/querying/querying.cy.js @@ -419,7 +419,7 @@ describe('src/cy/commands/querying', () => { state: 'passed', name: 'get', message: 'body', - alias: 'b', + alias: '@b', aliasType: 'dom', referencesAlias: undefined, } diff --git a/packages/driver/cypress/e2e/commands/window.cy.js b/packages/driver/cypress/e2e/commands/window.cy.js index 3acdc2122ffd..332174e97dcc 100644 --- a/packages/driver/cypress/e2e/commands/window.cy.js +++ b/packages/driver/cypress/e2e/commands/window.cy.js @@ -145,7 +145,7 @@ describe('src/cy/commands/window', () => { expect(win).to.eq(this.win) - expect(this.logs[0].get('alias')).to.eq('win') + expect(this.logs[0].get('alias')).to.eq('@win') expect(this.logs[0].get('aliasType')).to.eq('primitive') expect(this.logs[2].get('aliasType')).to.eq('primitive') @@ -329,7 +329,7 @@ describe('src/cy/commands/window', () => { expect(doc).to.eq(this.doc) - expect(logs[0].get('alias')).to.eq('doc') + expect(logs[0].get('alias')).to.eq('@doc') expect(logs[0].get('aliasType')).to.eq('primitive') expect(logs[2].get('aliasType')).to.eq('primitive') diff --git a/packages/driver/cypress/e2e/e2e/origin/commands/actions.cy.ts b/packages/driver/cypress/e2e/e2e/origin/commands/actions.cy.ts index ad997e4182d9..f9ecee1de39b 100644 --- a/packages/driver/cypress/e2e/e2e/origin/commands/actions.cy.ts +++ b/packages/driver/cypress/e2e/e2e/origin/commands/actions.cy.ts @@ -243,7 +243,7 @@ context('cy.origin actions', { browser: '!webkit' }, () => { }) }) - it('.alias()', () => { + it('.as()', () => { cy.get('a[data-cy="dom-link"]').click() cy.origin('http://www.foobar.com:3500', () => { cy.get('#button').as('buttonAlias') @@ -255,7 +255,7 @@ context('cy.origin actions', { browser: '!webkit' }, () => { // make sure $el is in fact a jquery instance to keep the logs happy expect($el.jquery).to.be.ok - expect(alias).to.equal('buttonAlias') + expect(alias).to.equal('@buttonAlias') expect(aliasType).to.equal('dom') expect(consoleProps.Command).to.equal('get') expect(consoleProps.Elements).to.equal(1) diff --git a/packages/driver/cypress/e2e/e2e/origin/commands/aliasing.cy.ts b/packages/driver/cypress/e2e/e2e/origin/commands/aliasing.cy.ts index 4c7557279a6a..d3187574391a 100644 --- a/packages/driver/cypress/e2e/e2e/origin/commands/aliasing.cy.ts +++ b/packages/driver/cypress/e2e/e2e/origin/commands/aliasing.cy.ts @@ -53,7 +53,7 @@ context('cy.origin aliasing', { browser: '!webkit' }, () => { // make sure $el is in fact a jquery instance to keep the logs happy expect($el.jquery).to.be.ok - expect(alias).to.equal('buttonAlias') + expect(alias).to.equal('@buttonAlias') expect(aliasType).to.equal('dom') expect(consoleProps.Command).to.equal('get') expect(consoleProps.Elements).to.equal(1) diff --git a/packages/driver/src/cy/commands/aliasing.ts b/packages/driver/src/cy/commands/aliasing.ts index 74e6560784ac..63cb95c611f3 100644 --- a/packages/driver/src/cy/commands/aliasing.ts +++ b/packages/driver/src/cy/commands/aliasing.ts @@ -1,18 +1,34 @@ import _ from 'lodash' import $dom from '../../dom' +import $errUtils from '../../cypress/error_utils' export default function (Commands, Cypress, cy) { - Commands.addQuery('as', function asFn (alias) { + Commands.addQuery('as', function asFn (alias, options = {} as Partial) { Cypress.ensure.isChildCommand(this, [alias], cy) cy.validateAlias(alias) + if (!_.isPlainObject(options)) { + $errUtils.throwErrByPath('as.invalid_options', { args: { arg: options } }) + } + + if (options.type && !['query', 'static'].includes(options.type)) { + $errUtils.throwErrByPath('as.invalid_options_type', { args: { type: options.type } }) + } + const prevCommand = cy.state('current').get('prev') prevCommand.set('alias', alias) // Shallow clone of the existing subject chain, so that future commands running on the same chainer // don't apply here as well. - const subjectChain = [...cy.subjectChain()] + let subjectChain = [...cy.subjectChain()] + + // If the user wants us to store a specific static value, rather than + // requery it live, we replace the subject chain with a resolved value. + // https://github.com/cypress-io/cypress/issues/25173 + if (options.type === 'static') { + subjectChain = [cy.getSubjectFromChain(subjectChain)] + } const fileName = prevCommand.get('fileName') @@ -42,7 +58,7 @@ export default function (Commands, Cypress, cy) { if (!alreadyAliasedLog && log) { log.set({ - alias, + alias: `@${alias}${options.type === 'static' ? ` (${ options.type })` : ''}`, aliasType: $dom.isElement(subject) ? 'dom' : 'primitive', }) } diff --git a/packages/driver/src/cypress/error_messages.ts b/packages/driver/src/cypress/error_messages.ts index 9ceb81b290eb..f89958dd2d8a 100644 --- a/packages/driver/src/cypress/error_messages.ts +++ b/packages/driver/src/cypress/error_messages.ts @@ -141,6 +141,8 @@ export default { reserved_word: { message: `${cmd('as')} cannot be aliased as: \`{{alias}}\`. This word is reserved.`, }, + invalid_options: `${cmd('as')} only accepts an options object for its second argument. You passed: \`{{arg}}\``, + invalid_options_type: `${cmd('as')} only accepts a \`type\` of \`'query'\` or \`'static'\`. You passed: \`{{type}}\``, }, blur: {