Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add Cypress.Commands.overwriteQuery #25674

Merged
merged 11 commits into from
Feb 13, 2023
8 changes: 6 additions & 2 deletions cli/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
<!-- See the ../guides/writing-the-cypress-changelog.md for details on writing the changelog. -->
## 12.5.1
<!-- See the ../guides/writing-the-cypress-changelog.md for details on writing the changelog. -->
## 12.6.0

_Released 02/10/2023 (PENDING)_

**Features:**

- It is now possible to overwrite query commands using the newly added `Cypress.Commands.overwriteQuery`. See [the docs](https://on.cypress.io/api/custom-queries) for details. Addressed in [#25674](https://github.com/cypress-io/cypress/pull/25674).

**Dependency Updates:**

- Upgraded [`simple-git`](https://github.com/steveukx/git-js) from `3.15.0` to `3.16.0` to address this [security vulnerability](https://github.com/advisories/GHSA-9p95-fxvg-qgq2) where Remote Code Execution (RCE) via the clone(), pull(), push() and listRemote() methods due to improper input sanitization was possible. Addressed in [#25603](https://github.com/cypress-io/cypress/pull/25603).
Expand Down
9 changes: 9 additions & 0 deletions cli/types/cypress.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ declare namespace Cypress {
interface QueryFn<T extends keyof ChainableMethods> {
(this: Command, ...args: Parameters<ChainableMethods[T]>): (subject: any) => any
}
interface QueryFnWithOriginalFn<T extends keyof Chainable> {
(this: Command, originalFn: QueryFn<T>, ...args: Parameters<ChainableMethods[T]>): (subject: any) => any
}
interface ObjectLike {
[key: string]: any
}
Expand Down Expand Up @@ -648,6 +651,12 @@ declare namespace Cypress {
* @see https://on.cypress.io/api/custom-queries
*/
addQuery<T extends keyof Chainable>(name: T, fn: QueryFn<T>): void

/**
* Overwrite an existing Cypress query with a new implementation
* @see https://on.cypress.io/api/custom-queries
*/
overwriteQuery<T extends keyof Chainable>(name: T, fn: QueryFnWithOriginalFn<T>): void
}

/**
Expand Down
49 changes: 49 additions & 0 deletions packages/driver/cypress/e2e/cypress/cy.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -565,5 +565,54 @@ describe('driver/src/cypress/cy', () => {

cy.get('body').find('#specific-contains').children().should('have.class', 'active')
})

context('overwriting queries', () => {
it('does not allow commands to overwrite queries', () => {
const fn = () => Cypress.Commands.overwrite('get', () => {})

expect(fn).to.throw().with.property('message')
.and.include('Cannot overwite the `get` query. Queries can only be overwritten with `Cypress.Commands.overwriteQuery()`.')

expect(fn).to.throw().with.property('docsUrl')
.and.include('https://on.cypress.io/api')
})

it('does not allow queries to overwrite commands', () => {
const fn = () => Cypress.Commands.overwriteQuery('click', () => {})

expect(fn).to.throw().with.property('message')
.and.include('Cannot overwite the `click` command. Commands can only be overwritten with `Cypress.Commands.overwrite()`.')

expect(fn).to.throw().with.property('docsUrl')
.and.include('https://on.cypress.io/api')
})

it('can call the originalFn', () => {
// Ensure nothing gets confused when we overwrite the same query multiple times.
// Both overwrites should succeed, layered on top of each other.

let overwriteCalled = 0

Cypress.Commands.overwriteQuery('get', function (originalFn, ...args) {
overwriteCalled++

return originalFn.call(this, ...args)
})

let secondOverwriteCalled = 0

Cypress.Commands.overwriteQuery('get', function (originalFn, ...args) {
secondOverwriteCalled++

return originalFn.call(this, ...args)
})

cy.get('button').should('have.length', 24)
cy.then(() => {
expect(overwriteCalled).to.eq(1)
expect(secondOverwriteCalled).to.eq(1)
})
})
})
})
})
38 changes: 28 additions & 10 deletions packages/driver/src/cypress/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,9 @@ const getTypeByPrevSubject = (prevSubject) => {
return 'parent'
}

const internalError = (path, name) => {
const internalError = (path, args) => {
$errUtils.throwErrByPath(path, {
args: {
name,
},
args,
stack: (new cy.state('specWindow').Error('add command stack')).stack,
errProps: {
appendToStack: {
Expand Down Expand Up @@ -88,11 +86,11 @@ export default {

add (name, options, fn) {
if (builtInCommandNames[name]) {
internalError('miscellaneous.invalid_new_command', name)
internalError('miscellaneous.invalid_new_command', { name })
}

if (reservedCommandNames.has(name)) {
internalError('miscellaneous.reserved_command', name)
internalError('miscellaneous.reserved_command', { name })
}

// .hover & .mount are special case commands. allow as builtins so users
Expand Down Expand Up @@ -126,11 +124,11 @@ export default {
const original = commands[name]

if (queries[name]) {
internalError('miscellaneous.invalid_overwrite_query_with_command', name)
internalError('miscellaneous.invalid_overwrite_query_with_command', { name })
}

if (!original) {
internalError('miscellaneous.invalid_overwrite', name)
internalError('miscellaneous.invalid_overwrite', { name, type: 'command' })
}

function originalFn (...args) {
Expand Down Expand Up @@ -159,11 +157,11 @@ export default {

addQuery (name: string, fn: () => QueryFunction) {
if (reservedCommandNames.has(name)) {
internalError('miscellaneous.reserved_command_query', name)
internalError('miscellaneous.reserved_command_query', { name })
}

if (cy[name]) {
internalError('miscellaneous.invalid_new_query', name)
internalError('miscellaneous.invalid_new_query', { name })
}

if (addingBuiltIns) {
Expand All @@ -173,6 +171,26 @@ export default {
queries[name] = fn
cy.addQuery({ name, fn })
},

overwriteQuery (name: string, fn: () => QueryFunction) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why wouldn't this logic be 1:1 with add command beyond the error handling and using addQuery?

Copy link
Contributor Author

@BlueWinds BlueWinds Feb 1, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The main difference is that queries don't have the concept of "parent/child" built into the API the same way that non-query commands do.

The call signature of a parent command is fn(...args) (not accepting a subject at all), while a child command is fn(subject, ...args) (taking the subject as the first argument).

All queries use the signature of fn(...args), so we get to skip the conditional check of slicing the subject off of args - it's never there, and the args require no manipulation.


The other difference that the commands object stores commands[name] = { name, fn, type, prevSubject } while queries stores only queries[name] = fn.

This is because when a command is overwritten, it needs to maintain the same type and previousSubject values as the original; queries have neither of these, and all we need to create the overwrite is the original function.

if (commands[name]) {
internalError('miscellaneous.invalid_overwrite_command_with_query', { name })
}

const original = queries[name]

if (!original) {
internalError('miscellaneous.invalid_overwrite', { name, type: 'command' })
}

queries[name] = function overridden (...args) {
args.unshift(original)

return fn.apply(this, args)
}

cy.addQuery({ name, fn: queries[name] })
},
}

addingBuiltIns = true
Expand Down
12 changes: 8 additions & 4 deletions packages/driver/src/cypress/error_messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -862,12 +862,16 @@ export default {
docsUrl: 'https://on.cypress.io/api/custom-queries',
},
invalid_overwrite: {
message: 'Cannot overwite command for: `{{name}}`. An existing command does not exist by that name.',
docsUrl: 'https://on.cypress.io/api',
message: 'Cannot overwite command for: `{{name}}`. An existing {{type}} does not exist by that name.',
docsUrl: 'https://on.cypress.io/api/custom-commands',
},
invalid_overwrite_command_with_query: {
message: 'Cannot overwite the `{{name}}` command. Commands can only be overwritten with `Cypress.Commands.overwrite()`.',
docsUrl: 'https://on.cypress.io/api/custom-commands',
},
invalid_overwrite_query_with_command: {
message: 'Cannot overwite the `{{name}}` query. Queries cannot be overwritten.',
docsUrl: 'https://on.cypress.io/api',
message: 'Cannot overwite the `{{name}}` query. Queries can only be overwritten with `Cypress.Commands.overwriteQuery()`.',
docsUrl: 'https://on.cypress.io/api/custom-queries',
},
invoking_child_without_parent (obj) {
return stripIndent`\
Expand Down