diff --git a/cli/CHANGELOG.md b/cli/CHANGELOG.md index 8511518c698f..bda5585b6b81 100644 --- a/cli/CHANGELOG.md +++ b/cli/CHANGELOG.md @@ -1,10 +1,11 @@ - + ## 12.6.0 _Released 02/14/2023 (PENDING)_ **Features:** +- It is now possible to overwrite query commands using [`Cypress.Commands.overwriteQuery`](https://on.cypress.io/api/custom-queries). Addressed in [#25674](https://github.com/cypress-io/cypress/pull/25674). - Added the "Open in IDE" feature for failed tests reported from the Debug page. Addressed in [#25691](https://github.com/cypress-io/cypress/pull/25691). - Added a new CLI flag, called [`--auto-cancel-after-failures`](https://docs.cypress.io/guides/guides/command-line#Options), that overrides the project-level CI ["Auto Cancellation"](https://docs.cypress.io/guides/cloud/smart-orchestration#Auto-Cancellation) value when recording to the Cloud. This gives Cloud users on Business and Enterprise plans the flexibility to alter the auto-cancellation value per run. Addressed in [#25237](https://github.com/cypress-io/cypress/pull/25237). - Added `Cypress.require()` for including dependencies within the `cy.origin()` callback. Removed support for `require()` and `import()` within the callback. Addresses [#24976](https://github.com/cypress-io/cypress/issues/24976). diff --git a/cli/types/cypress.d.ts b/cli/types/cypress.d.ts index 09eb24d746ae..60ad410061e2 100644 --- a/cli/types/cypress.d.ts +++ b/cli/types/cypress.d.ts @@ -53,6 +53,9 @@ declare namespace Cypress { interface QueryFn { (this: Command, ...args: Parameters): (subject: any) => any } + interface QueryFnWithOriginalFn { + (this: Command, originalFn: QueryFn, ...args: Parameters): (subject: any) => any + } interface ObjectLike { [key: string]: any } @@ -648,6 +651,12 @@ declare namespace Cypress { * @see https://on.cypress.io/api/custom-queries */ addQuery(name: T, fn: QueryFn): void + + /** + * Overwrite an existing Cypress query with a new implementation + * @see https://on.cypress.io/api/custom-queries + */ + overwriteQuery(name: T, fn: QueryFnWithOriginalFn): void } /** diff --git a/packages/driver/cypress/e2e/cypress/cy.cy.js b/packages/driver/cypress/e2e/cypress/cy.cy.js index fd7f5291d006..52aa44df2b6d 100644 --- a/packages/driver/cypress/e2e/cypress/cy.cy.js +++ b/packages/driver/cypress/e2e/cypress/cy.cy.js @@ -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) + }) + }) + }) }) }) diff --git a/packages/driver/src/cypress/commands.ts b/packages/driver/src/cypress/commands.ts index c4e51904bafa..36bf924f3085 100644 --- a/packages/driver/src/cypress/commands.ts +++ b/packages/driver/src/cypress/commands.ts @@ -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: { @@ -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 @@ -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) { @@ -157,13 +155,13 @@ export default { return cy.addCommand(overridden) }, - addQuery (name: string, fn: () => QueryFunction) { + addQuery (name: string, fn: (...args: any[]) => 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) { @@ -173,6 +171,26 @@ export default { queries[name] = fn cy.addQuery({ name, fn }) }, + + overwriteQuery (name: string, fn: (...args: any[]) => QueryFunction) { + 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 diff --git a/packages/driver/src/cypress/error_messages.ts b/packages/driver/src/cypress/error_messages.ts index a730d87826a4..008304d7017b 100644 --- a/packages/driver/src/cypress/error_messages.ts +++ b/packages/driver/src/cypress/error_messages.ts @@ -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`\