diff --git a/cypress/integration/find.spec.js b/cypress/integration/find.spec.js index 3a43048..9187561 100644 --- a/cypress/integration/find.spec.js +++ b/cypress/integration/find.spec.js @@ -30,11 +30,11 @@ describe('find* dom-testing-library commands', () => { .click() .should('contain', 'Button Clicked') }) - + it('findAllByText', () => { cy.findAllByText(/^Button Text \d$/) .should('have.length', 2) - .click({ multiple: true }) + .click({multiple: true}) .should('contain', 'Button Clicked') }) @@ -44,10 +44,9 @@ describe('find* dom-testing-library commands', () => { .clear() .type('Some new text') }) - + it('findAllByDisplayValue', () => { - cy.findAllByDisplayValue(/^Display Value \d$/) - .should('have.length', 2) + cy.findAllByDisplayValue(/^Display Value \d$/).should('have.length', 2) }) it('findByAltText', () => { @@ -79,27 +78,44 @@ describe('find* dom-testing-library commands', () => { }) it('findAllByTestId', () => { - cy.findAllByTestId(/^image-with-random-alt-tag-\d$/).should('have.length', 2) + cy.findAllByTestId(/^image-with-random-alt-tag-\d$/).should( + 'have.length', + 2, + ) }) /* Test the behaviour around these queries */ - it('findByText with should(\'not.exist\')', () => { + it("findByText with should('not.exist')", () => { cy.findAllByText(/^Button Text \d$/).should('exist') - cy.findByText('Non-existing Button Text', {timeout: 100}).should('not.exist') + cy.findByText('Non-existing Button Text', {timeout: 100}).should( + 'not.exist', + ) + }) + + it('findByText with a previous subject', () => { + cy.get('#nested') + .findByText('Button Text 1') + .should('not.exist') + cy.get('#nested') + .findByText('Button Text 2') + .should('exist') }) it('findByText within', () => { cy.get('#nested').within(() => { - cy.findByText('Button Text 2').click() + cy.findByText('Button Text 1').should('not.exist') + cy.findByText('Button Text 2').should('exist') }) }) it('findByText in container', () => { - return cy.get('#nested') - .then(subject => { - cy.findByText(/^Button Text/, {container: subject}).click() - }) + // NOTE: Cypress' `then` doesn't actually return a promise + // eslint-disable-next-line jest/valid-expect-in-promise + cy.get('#nested').then(subject => { + cy.findByText('Button Text 1', {container: subject}).should('not.exist') + cy.findByText('Button Text 2', {container: subject}).should('exist') + }) }) it('findByText works when another page loads', () => { diff --git a/src/__tests__/add-commands.js b/src/__tests__/add-commands.js index e754820..0a9e590 100644 --- a/src/__tests__/add-commands.js +++ b/src/__tests__/add-commands.js @@ -11,6 +11,7 @@ test('adds commands to Cypress', () => { commands.forEach(({name}, index) => { expect(addMock.mock.calls[index]).toMatchObject([ name, + {}, // We get a new function that is `command.bind(null, cy)` i.e. global `cy` passed into the first argument. // The commands themselves will be tested separately in the Cypress end-to-end tests. expect.any(Function), diff --git a/src/add-commands.js b/src/add-commands.js index 43e8f14..1b2171c 100644 --- a/src/add-commands.js +++ b/src/add-commands.js @@ -1,7 +1,7 @@ import {commands} from './' -commands.forEach(({name, command}) => { - Cypress.Commands.add(name, command) +commands.forEach(({name, command, options = {}}) => { + Cypress.Commands.add(name, options, command) }) /* global Cypress */ diff --git a/src/index.js b/src/index.js index 6afc909..650d170 100644 --- a/src/index.js +++ b/src/index.js @@ -7,44 +7,50 @@ const getDefaultCommandOptions = () => { } } -const queryNames = Object.keys(queries); +const queryNames = Object.keys(queries) -const getRegex = /^get/; -const queryRegex = /^query/; -const findRegex = /^find/; +const getRegex = /^get/ +const queryRegex = /^query/ +const findRegex = /^find/ -const getQueryNames = queryNames.filter(q => getRegex.test(q)); -const queryQueryNames = queryNames.filter(q => queryRegex.test(q)); -const findQueryNames = queryNames.filter(q => findRegex.test(q)); +const getQueryNames = queryNames.filter(q => getRegex.test(q)) +const queryQueryNames = queryNames.filter(q => queryRegex.test(q)) +const findQueryNames = queryNames.filter(q => findRegex.test(q)) const getCommands = getQueryNames.map(queryName => { return { name: queryName, command: () => { Cypress.log({ - name: queryName - }); - - throw new Error(`You used '${queryName}' which has been removed from Cypress Testing Library because it does not make sense in this context. Please use '${queryName.replace(getRegex, 'find')}' instead.`) - } + name: queryName, + }) + + throw new Error( + `You used '${queryName}' which has been removed from Cypress Testing Library because it does not make sense in this context. Please use '${queryName.replace( + getRegex, + 'find', + )}' instead.`, + ) + }, } }) const queryCommands = queryQueryNames.map(queryName => { - return createCommand(queryName, queryName); + return createCommand(queryName, queryName) }) const findCommands = findQueryNames.map(queryName => { // dom-testing-library find* queries use a promise to look for an element, but that doesn't work well with Cypress retryability // Use the query* commands so that we can lean on Cypress to do the retry for us // When it does return a null or empty array, Cypress will retry until the assertions are satisfied or the command times out - return createCommand(queryName, queryName.replace(findRegex, 'query')); + return createCommand(queryName, queryName.replace(findRegex, 'query')) }) function createCommand(queryName, implementationName) { return { name: queryName, - command: (...args) => { + options: {prevSubject: ['optional', 'document', 'element', 'window']}, + command: (prevSubject, ...args) => { const lastArg = args[args.length - 1] const defaults = getDefaultCommandOptions() const waitOptions = @@ -52,41 +58,43 @@ function createCommand(queryName, implementationName) { const queryImpl = queries[implementationName] const baseCommandImpl = doc => { - const container = getContainer(waitOptions.container || doc) + const container = getContainer( + waitOptions.container || prevSubject || doc, + ) return queryImpl(container, ...args) } const commandImpl = doc => baseCommandImpl(doc) - const inputArr = args.filter(filterInputs); + const inputArr = args.filter(filterInputs) const consoleProps = { // TODO: Would be good to completely separate out the types of input into their own properties - input: inputArr + input: inputArr, } Cypress.log({ $el: inputArr, name: queryName, message: inputArr, - consoleProps: () => consoleProps - }); + consoleProps: () => consoleProps, + }) return cy .window({log: false}) - .then({timeout: waitOptions.timeout + 100}, (thenArgs) => { + .then({timeout: waitOptions.timeout + 100}, thenArgs => { const getValue = () => { - const value = commandImpl(thenArgs.document); - const result = Cypress.$(value); - + const value = commandImpl(thenArgs.document) + const result = Cypress.$(value) + // Overriding the selector of the jquery object because it's displayed in the long message of .should('exist') failure message // Hopefully it makes it clearer, because I find the normal response of "Expected to find element '', but never found it" confusing - result.selector = `${queryName}(${queryArgument(args)})`; + result.selector = `${queryName}(${queryArgument(args)})` if (result.length > 0) { consoleProps.yielded = result.toArray() } - return result; + return result } const resolveValue = () => { @@ -100,19 +108,17 @@ function createCommand(queryName, implementationName) { if (queryRegex.test(queryName)) { // For get* queries, do not retry - return getValue(); + return getValue() } - return resolveValue() - .then(subject => { - - // Remove the error that occurred because it is irrelevant now - if (consoleProps.error) { - delete consoleProps.error; - } - - return subject; - }) + return resolveValue().then(subject => { + // Remove the error that occurred because it is irrelevant now + if (consoleProps.error) { + delete consoleProps.error + } + + return subject + }) }) }, } @@ -125,33 +131,25 @@ function filterInputs(value) { if (value instanceof RegExp) { return value.toString() } - if ( - typeof value === 'object' && - Object.keys(value).length === 0 - ) { + if (typeof value === 'object' && Object.keys(value).length === 0) { return false } return Boolean(value) } function queryArgument(args) { - const input = args - .find(value => { - return (value instanceof RegExp) || (typeof value === 'string') - }); + const input = args.find(value => { + return value instanceof RegExp || typeof value === 'string' + }) - if (input && typeof input === 'string') { - return `\`${input}\``; - } + if (input && typeof input === 'string') { + return `\`${input}\`` + } - return input; + return input } -const commands = [ - ...getCommands, - ...findCommands, - ...queryCommands -]; +const commands = [...getCommands, ...findCommands, ...queryCommands] export {commands, configure}