From 0f96a3d8988eaad837fe92db804435ed549150f6 Mon Sep 17 00:00:00 2001 From: Gleb Bahmutov Date: Tue, 15 Oct 2024 10:43:00 -0400 Subject: [PATCH] feat: support assertions with two values (#112) * feat: support assertions with two values * bump upload artifact action * update v11 code * store screenshots and videos on failure * updated the tests * debugging * fix the last else branch --- .github/workflows/ci.yml | 18 +++++++- README.md | 11 +++++ cypress.config.js | 1 + cypress/e2e/has-attribute.cy.js | 58 ++++++++++++++++++++++++++ cypress/e2e/terms-and-conditions.cy.js | 3 ++ cypress/terms.html | 5 +++ src/index-v11.js | 31 ++++++++++---- src/index.d.ts | 3 +- src/index.js | 32 ++++++++++---- 9 files changed, 143 insertions(+), 19 deletions(-) create mode 100644 cypress/e2e/has-attribute.cy.js diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 976d657..9fa1d22 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,6 +14,21 @@ jobs: # check if the types agree build: npm run types + # https://github.com/actions/upload-artifact + - uses: actions/upload-artifact@v4 + name: Store any error screenshots 🖼 + if: failure() + with: + name: cypress-screenshots + path: cypress/screenshots + + - uses: actions/upload-artifact@v4 + name: Store any videos 🖼 + if: failure() + with: + name: cypress-videos + path: cypress/videos + # there was a breaking change under the hood in Cypress v11.1.0 # so make sure this plugin still works for older versions test-cypress-v11-0: @@ -45,7 +60,8 @@ jobs: with: working-directory: cypress-v9 - - uses: actions/upload-artifact@v2 + # https://github.com/actions/upload-artifact + - uses: actions/upload-artifact@v4 name: Store any v9 screenshots 🖼 if: failure() with: diff --git a/README.md b/README.md index 8fb82a1..7e4412e 100644 --- a/README.md +++ b/README.md @@ -268,6 +268,17 @@ cy.wrap(42).if('not.null') // takes IF path See spec [null.cy.js](./cypress/e2e/null.cy.js) +## Multiple values + +Some assertions need two values, for example: + +```js +// only checks the presence of the "data-x" HTML attribute +.if('have.attr', 'data-x') +// checks if the "data-x" attribute present AND has value "123" +.if('have.attr', 'data-x', '123') +``` + ## raise This plugin includes a utility custom command `cy.raise` that lets you conveniently throw an error. diff --git a/cypress.config.js b/cypress.config.js index 2c7d51c..08ec56e 100644 --- a/cypress.config.js +++ b/cypress.config.js @@ -8,6 +8,7 @@ module.exports = defineConfig({ viewportWidth: 200, viewportHeight: 200, defaultCommandTimeout: 1000, + video: true, setupNodeEvents(on, config) { // implement node event listeners here on('task', { diff --git a/cypress/e2e/has-attribute.cy.js b/cypress/e2e/has-attribute.cy.js new file mode 100644 index 0000000..5c97dd4 --- /dev/null +++ b/cypress/e2e/has-attribute.cy.js @@ -0,0 +1,58 @@ +/// +// @ts-check + +import '../../src' + +describe('has attribute assertion', () => { + beforeEach(() => { + cy.visit('cypress/terms.html') + }) + + it('has attribute present', () => { + cy.get('#submit') + .if('have.attr', 'id') + .log('button has an id') + .else() + .raise(new Error('button should have an id')) + }) + + it( + 'has attribute present after delay', + { defaultCommandTimeout: 2000 }, + () => { + cy.get('#submit').should('have.attr', 'data-x') + cy.get('#submit') + .if('have.attr', 'data-x') + .invoke('attr', 'data-x') + .should('equal', '123') + .else() + .raise(new Error('data-x not found')) + }, + ) + + it( + 'has attribute with matching value present after delay', + { defaultCommandTimeout: 2000 }, + () => { + cy.get('#submit').should('have.attr', 'data-x') + cy.get('#submit') + .if('have.attr', 'data-x', '123') + .log('data-X found') + .else() + .raise(new Error('data-x not found')) + }, + ) + + it( + 'has attribute with a different value', + { defaultCommandTimeout: 2000 }, + () => { + cy.get('#submit').should('have.attr', 'data-x') + cy.get('#submit') + // the attribute is present, but has a different value + .if('have.attr', 'data-x', '99') + .raise(new Error('data-x has wrong value')) + .else('data-x value is correct') + }, + ) +}) diff --git a/cypress/e2e/terms-and-conditions.cy.js b/cypress/e2e/terms-and-conditions.cy.js index 173f4b5..aab60d0 100644 --- a/cypress/e2e/terms-and-conditions.cy.js +++ b/cypress/e2e/terms-and-conditions.cy.js @@ -5,8 +5,11 @@ import '../../src' it('submits the terms forms', () => { cy.visit('cypress/terms.html') cy.get('#agreed') + cy.get('#agreed') + .should('be.visible') .if('not.checked') .click() + .log('clicked the checkbox') .else() .log('The user already agreed') cy.get('button#submit').click() diff --git a/cypress/terms.html b/cypress/terms.html index 2ec85b9..00862c9 100644 --- a/cypress/terms.html +++ b/cypress/terms.html @@ -26,6 +26,11 @@ } else { console.log('checked = false') } + + // set an attribute after a delay + setTimeout(() => { + document.getElementById('submit').setAttribute('data-X', '123') + }, 1000) diff --git a/src/index-v11.js b/src/index-v11.js index 1d9b81b..3f16959 100644 --- a/src/index-v11.js +++ b/src/index-v11.js @@ -50,7 +50,7 @@ function getCypressCurrentSubject() { Cypress.Commands.add( 'if', { prevSubject: true }, - function (subject, assertion, assertionValue) { + function (subject, assertion, assertionValue1, assertionValue2) { const cmd = cy.state('current') debug('if', cmd.attributes, 'subject', subject, 'assertion?', assertion) debug('next command', cmd.next) @@ -86,18 +86,33 @@ Cypress.Commands.add( const parts = assertion.split('.') let assertionReduced = expect(subject).to parts.forEach((assertionPart, k) => { - if ( - k === parts.length - 1 && - typeof assertionValue !== 'undefined' - ) { - assertionReduced = assertionReduced[assertionPart](assertionValue) + if (k === parts.length - 1) { + if ( + typeof assertionValue1 !== 'undefined' && + typeof assertionValue2 !== 'undefined' + ) { + assertionReduced = assertionReduced[assertionPart]( + assertionValue1, + assertionValue2, + ) + } else if (typeof assertionValue1 !== 'undefined') { + assertionReduced = + assertionReduced[assertionPart](assertionValue1) + } else { + assertionReduced = assertionReduced[assertionPart] + } } else { assertionReduced = assertionReduced[assertionPart] } }) } else { - if (typeof assertionValue !== 'undefined') { - expect(subject).to.be[assertion](assertionValue) + if ( + typeof assertionValue1 !== 'undefined' && + typeof assertionValue2 !== 'undefined' + ) { + expect(subject).to.be[assertion](assertionValue1, assertionValue2) + } else if (typeof assertionValue1 !== 'undefined') { + expect(subject).to.be[assertion](assertionValue1) } else { expect(subject).to.be[assertion] } diff --git a/src/index.d.ts b/src/index.d.ts index a0415b5..26de1a8 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -24,7 +24,8 @@ declare namespace Cypress { if( this: Chainable, assertion?: string, - value?: any, + value1?: any, + value2?: any, ): Chainable /** diff --git a/src/index.js b/src/index.js index 9ce2d86..3146acc 100644 --- a/src/index.js +++ b/src/index.js @@ -54,7 +54,7 @@ if (major < 12) { Cypress.Commands.add( 'if', { prevSubject: true }, - function (subject, assertion, assertionValue) { + function (subject, assertion, assertionValue1, assertionValue2) { const cmd = cy.state('current') debug('if', cmd.attributes, 'subject', subject, 'assertion?', assertion) debug('next command', cmd.next) @@ -90,19 +90,33 @@ if (major < 12) { const parts = assertion.split('.') let assertionReduced = expect(subject).to parts.forEach((assertionPart, k) => { - if ( - k === parts.length - 1 && - typeof assertionValue !== 'undefined' - ) { - assertionReduced = - assertionReduced[assertionPart](assertionValue) + if (k === parts.length - 1) { + if ( + typeof assertionValue1 !== 'undefined' && + typeof assertionValue2 !== 'undefined' + ) { + assertionReduced = assertionReduced[assertionPart]( + assertionValue1, + assertionValue2, + ) + } else if (typeof assertionValue1 !== 'undefined') { + assertionReduced = + assertionReduced[assertionPart](assertionValue1) + } else { + assertionReduced = assertionReduced[assertionPart] + } } else { assertionReduced = assertionReduced[assertionPart] } }) } else { - if (typeof assertionValue !== 'undefined') { - expect(subject).to.be[assertion](assertionValue) + if ( + typeof assertionValue1 !== 'undefined' && + typeof assertionValue2 !== 'undefined' + ) { + expect(subject).to.be[assertion](assertionValue1, assertionValue2) + } else if (typeof assertionValue1 !== 'undefined') { + expect(subject).to.be[assertion](assertionValue1) } else { expect(subject).to.be[assertion] }