Skip to content

Support running commands against the previous yielded subject #100

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

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 29 additions & 13 deletions cypress/integration/find.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,11 @@ describe('find* dom-testing-library commands', () => {
.click()
.should('contain', 'Button Clicked')
})

Copy link
Member

Choose a reason for hiding this comment

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

Sorry about all these. Don't know how those slipped through in the first place

it('findAllByText', () => {
cy.findAllByText(/^Button Text \d$/)
.should('have.length', 2)
.click({ multiple: true })
.click({multiple: true})
.should('contain', 'Button Clicked')
})

Expand All @@ -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', () => {
Expand Down Expand Up @@ -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')
Comment on lines +96 to +102
Copy link
Member

Choose a reason for hiding this comment

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

Love this

})

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
Copy link
Member

Choose a reason for hiding this comment

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

We should probably disable the jest plugin for these tests. But we can do that in another PR :)

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', () => {
Expand Down
1 change: 1 addition & 0 deletions src/__tests__/add-commands.js
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
4 changes: 2 additions & 2 deletions src/add-commands.js
Original file line number Diff line number Diff line change
@@ -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 */
106 changes: 52 additions & 54 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,86 +7,94 @@ 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 =
typeof lastArg === 'object' ? {...defaults, ...lastArg} : defaults

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 = () => {
Expand All @@ -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
})
})
},
}
Expand All @@ -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}

Expand Down