Skip to content

Commit e32c8e4

Browse files
tlrobinsonKent C. Dodds
authored andcommitted
feat: support running commands against the previous subject. (#100)
Also clean up similar within/container tests
1 parent 90a8ad9 commit e32c8e4

File tree

4 files changed

+84
-69
lines changed

4 files changed

+84
-69
lines changed

cypress/integration/find.spec.js

Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,11 @@ describe('find* dom-testing-library commands', () => {
3030
.click()
3131
.should('contain', 'Button Clicked')
3232
})
33-
33+
3434
it('findAllByText', () => {
3535
cy.findAllByText(/^Button Text \d$/)
3636
.should('have.length', 2)
37-
.click({ multiple: true })
37+
.click({multiple: true})
3838
.should('contain', 'Button Clicked')
3939
})
4040

@@ -44,10 +44,9 @@ describe('find* dom-testing-library commands', () => {
4444
.clear()
4545
.type('Some new text')
4646
})
47-
47+
4848
it('findAllByDisplayValue', () => {
49-
cy.findAllByDisplayValue(/^Display Value \d$/)
50-
.should('have.length', 2)
49+
cy.findAllByDisplayValue(/^Display Value \d$/).should('have.length', 2)
5150
})
5251

5352
it('findByAltText', () => {
@@ -79,27 +78,44 @@ describe('find* dom-testing-library commands', () => {
7978
})
8079

8180
it('findAllByTestId', () => {
82-
cy.findAllByTestId(/^image-with-random-alt-tag-\d$/).should('have.length', 2)
81+
cy.findAllByTestId(/^image-with-random-alt-tag-\d$/).should(
82+
'have.length',
83+
2,
84+
)
8385
})
8486

8587
/* Test the behaviour around these queries */
8688

87-
it('findByText with should(\'not.exist\')', () => {
89+
it("findByText with should('not.exist')", () => {
8890
cy.findAllByText(/^Button Text \d$/).should('exist')
89-
cy.findByText('Non-existing Button Text', {timeout: 100}).should('not.exist')
91+
cy.findByText('Non-existing Button Text', {timeout: 100}).should(
92+
'not.exist',
93+
)
94+
})
95+
96+
it('findByText with a previous subject', () => {
97+
cy.get('#nested')
98+
.findByText('Button Text 1')
99+
.should('not.exist')
100+
cy.get('#nested')
101+
.findByText('Button Text 2')
102+
.should('exist')
90103
})
91104

92105
it('findByText within', () => {
93106
cy.get('#nested').within(() => {
94-
cy.findByText('Button Text 2').click()
107+
cy.findByText('Button Text 1').should('not.exist')
108+
cy.findByText('Button Text 2').should('exist')
95109
})
96110
})
97111

98112
it('findByText in container', () => {
99-
return cy.get('#nested')
100-
.then(subject => {
101-
cy.findByText(/^Button Text/, {container: subject}).click()
102-
})
113+
// NOTE: Cypress' `then` doesn't actually return a promise
114+
// eslint-disable-next-line jest/valid-expect-in-promise
115+
cy.get('#nested').then(subject => {
116+
cy.findByText('Button Text 1', {container: subject}).should('not.exist')
117+
cy.findByText('Button Text 2', {container: subject}).should('exist')
118+
})
103119
})
104120

105121
it('findByText works when another page loads', () => {

src/__tests__/add-commands.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ test('adds commands to Cypress', () => {
1111
commands.forEach(({name}, index) => {
1212
expect(addMock.mock.calls[index]).toMatchObject([
1313
name,
14+
{},
1415
// We get a new function that is `command.bind(null, cy)` i.e. global `cy` passed into the first argument.
1516
// The commands themselves will be tested separately in the Cypress end-to-end tests.
1617
expect.any(Function),

src/add-commands.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import {commands} from './'
22

3-
commands.forEach(({name, command}) => {
4-
Cypress.Commands.add(name, command)
3+
commands.forEach(({name, command, options = {}}) => {
4+
Cypress.Commands.add(name, options, command)
55
})
66

77
/* global Cypress */

src/index.js

Lines changed: 52 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -7,86 +7,94 @@ const getDefaultCommandOptions = () => {
77
}
88
}
99

10-
const queryNames = Object.keys(queries);
10+
const queryNames = Object.keys(queries)
1111

12-
const getRegex = /^get/;
13-
const queryRegex = /^query/;
14-
const findRegex = /^find/;
12+
const getRegex = /^get/
13+
const queryRegex = /^query/
14+
const findRegex = /^find/
1515

16-
const getQueryNames = queryNames.filter(q => getRegex.test(q));
17-
const queryQueryNames = queryNames.filter(q => queryRegex.test(q));
18-
const findQueryNames = queryNames.filter(q => findRegex.test(q));
16+
const getQueryNames = queryNames.filter(q => getRegex.test(q))
17+
const queryQueryNames = queryNames.filter(q => queryRegex.test(q))
18+
const findQueryNames = queryNames.filter(q => findRegex.test(q))
1919

2020
const getCommands = getQueryNames.map(queryName => {
2121
return {
2222
name: queryName,
2323
command: () => {
2424
Cypress.log({
25-
name: queryName
26-
});
27-
28-
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.`)
29-
}
25+
name: queryName,
26+
})
27+
28+
throw new Error(
29+
`You used '${queryName}' which has been removed from Cypress Testing Library because it does not make sense in this context. Please use '${queryName.replace(
30+
getRegex,
31+
'find',
32+
)}' instead.`,
33+
)
34+
},
3035
}
3136
})
3237

3338
const queryCommands = queryQueryNames.map(queryName => {
34-
return createCommand(queryName, queryName);
39+
return createCommand(queryName, queryName)
3540
})
3641

3742
const findCommands = findQueryNames.map(queryName => {
3843
// dom-testing-library find* queries use a promise to look for an element, but that doesn't work well with Cypress retryability
3944
// Use the query* commands so that we can lean on Cypress to do the retry for us
4045
// When it does return a null or empty array, Cypress will retry until the assertions are satisfied or the command times out
41-
return createCommand(queryName, queryName.replace(findRegex, 'query'));
46+
return createCommand(queryName, queryName.replace(findRegex, 'query'))
4247
})
4348

4449
function createCommand(queryName, implementationName) {
4550
return {
4651
name: queryName,
47-
command: (...args) => {
52+
options: {prevSubject: ['optional', 'document', 'element', 'window']},
53+
command: (prevSubject, ...args) => {
4854
const lastArg = args[args.length - 1]
4955
const defaults = getDefaultCommandOptions()
5056
const waitOptions =
5157
typeof lastArg === 'object' ? {...defaults, ...lastArg} : defaults
5258

5359
const queryImpl = queries[implementationName]
5460
const baseCommandImpl = doc => {
55-
const container = getContainer(waitOptions.container || doc)
61+
const container = getContainer(
62+
waitOptions.container || prevSubject || doc,
63+
)
5664
return queryImpl(container, ...args)
5765
}
5866
const commandImpl = doc => baseCommandImpl(doc)
5967

60-
const inputArr = args.filter(filterInputs);
68+
const inputArr = args.filter(filterInputs)
6169

6270
const consoleProps = {
6371
// TODO: Would be good to completely separate out the types of input into their own properties
64-
input: inputArr
72+
input: inputArr,
6573
}
6674

6775
Cypress.log({
6876
$el: inputArr,
6977
name: queryName,
7078
message: inputArr,
71-
consoleProps: () => consoleProps
72-
});
79+
consoleProps: () => consoleProps,
80+
})
7381

7482
return cy
7583
.window({log: false})
76-
.then({timeout: waitOptions.timeout + 100}, (thenArgs) => {
84+
.then({timeout: waitOptions.timeout + 100}, thenArgs => {
7785
const getValue = () => {
78-
const value = commandImpl(thenArgs.document);
79-
const result = Cypress.$(value);
80-
86+
const value = commandImpl(thenArgs.document)
87+
const result = Cypress.$(value)
88+
8189
// Overriding the selector of the jquery object because it's displayed in the long message of .should('exist') failure message
8290
// Hopefully it makes it clearer, because I find the normal response of "Expected to find element '', but never found it" confusing
83-
result.selector = `${queryName}(${queryArgument(args)})`;
91+
result.selector = `${queryName}(${queryArgument(args)})`
8492

8593
if (result.length > 0) {
8694
consoleProps.yielded = result.toArray()
8795
}
8896

89-
return result;
97+
return result
9098
}
9199

92100
const resolveValue = () => {
@@ -100,19 +108,17 @@ function createCommand(queryName, implementationName) {
100108

101109
if (queryRegex.test(queryName)) {
102110
// For get* queries, do not retry
103-
return getValue();
111+
return getValue()
104112
}
105113

106-
return resolveValue()
107-
.then(subject => {
108-
109-
// Remove the error that occurred because it is irrelevant now
110-
if (consoleProps.error) {
111-
delete consoleProps.error;
112-
}
113-
114-
return subject;
115-
})
114+
return resolveValue().then(subject => {
115+
// Remove the error that occurred because it is irrelevant now
116+
if (consoleProps.error) {
117+
delete consoleProps.error
118+
}
119+
120+
return subject
121+
})
116122
})
117123
},
118124
}
@@ -125,33 +131,25 @@ function filterInputs(value) {
125131
if (value instanceof RegExp) {
126132
return value.toString()
127133
}
128-
if (
129-
typeof value === 'object' &&
130-
Object.keys(value).length === 0
131-
) {
134+
if (typeof value === 'object' && Object.keys(value).length === 0) {
132135
return false
133136
}
134137
return Boolean(value)
135138
}
136139

137140
function queryArgument(args) {
138-
const input = args
139-
.find(value => {
140-
return (value instanceof RegExp) || (typeof value === 'string')
141-
});
141+
const input = args.find(value => {
142+
return value instanceof RegExp || typeof value === 'string'
143+
})
142144

143-
if (input && typeof input === 'string') {
144-
return `\`${input}\``;
145-
}
145+
if (input && typeof input === 'string') {
146+
return `\`${input}\``
147+
}
146148

147-
return input;
149+
return input
148150
}
149151

150-
const commands = [
151-
...getCommands,
152-
...findCommands,
153-
...queryCommands
154-
];
152+
const commands = [...getCommands, ...findCommands, ...queryCommands]
155153

156154
export {commands, configure}
157155

0 commit comments

Comments
 (0)