Skip to content

Commit b58f747

Browse files
committed
feat: add ability for queries to inherit previous subject
* Fixes #109 without breaking change caused by #100
1 parent c9a25b6 commit b58f747

File tree

4 files changed

+54
-17
lines changed

4 files changed

+54
-17
lines changed

cypress/integration/find.spec.js

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -110,15 +110,28 @@ describe('find* dom-testing-library commands', () => {
110110
)
111111
})
112112

113+
it('findByText with a previous subject', () => {
114+
cy.get('#nested')
115+
.findByText('Button Text 1', { fallbackToPreviousFunctionality: false })
116+
.should('not.exist')
117+
cy.get('#nested')
118+
.findByText('Button Text 2')
119+
.should('exist')
120+
})
121+
113122
it('findByText within', () => {
114123
cy.get('#nested').within(() => {
115-
cy.findByText('Button Text 2').click()
124+
cy.findByText('Button Text 1').should('not.exist')
125+
cy.findByText('Button Text 2').should('exist')
116126
})
117127
})
118128

119129
it('findByText in container', () => {
120-
return cy.get('#nested').then(subject => {
121-
cy.findByText(/^Button Text/, {container: subject}).click()
130+
// NOTE: Cypress' `then` doesn't actually return a promise
131+
// eslint-disable-next-line jest/valid-expect-in-promise
132+
cy.get('#nested').then(subject => {
133+
cy.findByText('Button Text 1', {container: subject}).should('not.exist')
134+
cy.findByText('Button Text 2', {container: subject}).should('exist')
122135
})
123136
})
124137

@@ -181,6 +194,12 @@ describe('find* dom-testing-library commands', () => {
181194

182195
cy.findByText(/^Button Text/i, {timeout: 100})
183196
})
197+
198+
it('findByText should not break existing code', () => {
199+
cy.window()
200+
.findByText('Button Text 1')
201+
.should('exist')
202+
})
184203
})
185204

186205
/* global cy */

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: 29 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {getContainer} from './utils'
44
const getDefaultCommandOptions = () => {
55
return {
66
timeout: Cypress.config().defaultCommandTimeout,
7+
fallbackToPreviousFunctionality: true,
78
log: true,
89
}
910
}
@@ -37,27 +38,24 @@ const queryCommands = queryQueryNames.map(queryName => {
3738
})
3839

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

4644
function createCommand(queryName, implementationName) {
4745
return {
4846
name: queryName,
49-
command: (...args) => {
47+
options: {prevSubject: ['optional', 'document', 'element', 'window']},
48+
command: (prevSubject, ...args) => {
5049
const lastArg = args[args.length - 1]
5150
const defaults = getDefaultCommandOptions()
5251
const options =
5352
typeof lastArg === 'object' ? {...defaults, ...lastArg} : defaults
5453

5554
const queryImpl = queries[implementationName]
56-
const baseCommandImpl = doc => {
57-
const container = getContainer(options.container || doc)
58-
return queryImpl(container, ...args)
55+
const baseCommandImpl = container => {
56+
return queryImpl(getContainer(container), ...args)
5957
}
60-
const commandImpl = doc => baseCommandImpl(doc)
58+
const commandImpl = container => baseCommandImpl(container)
6159

6260
const inputArr = args.filter(filterInputs)
6361

@@ -70,7 +68,7 @@ function createCommand(queryName, implementationName) {
7068
input: inputArr,
7169
Selector: getSelector(),
7270
'Applied To': getContainer(
73-
options.container || win.document,
71+
options.container || prevSubject || win.document,
7472
)
7573
}
7674

@@ -82,8 +80,8 @@ function createCommand(queryName, implementationName) {
8280
})
8381
}
8482

85-
const getValue = () => {
86-
const value = commandImpl(win.document)
83+
const getValue = (container = options.container || prevSubject || win.document) => {
84+
const value = commandImpl(container)
8785

8886
const result = Cypress.$(value)
8987
if (value && options._log) {
@@ -115,20 +113,35 @@ function createCommand(queryName, implementationName) {
115113
}
116114

117115
let error
116+
let failedNewFunctionality = false
117+
let failedOldFunctionality = false
118118
// Errors will be thrown by @testing-library/dom, but a query might be followed by `.should('not.exist')`
119119
// We just need to capture the error thrown by @testing-library/dom and return an empty jQuery NodeList
120120
// to allow Cypress assertions errors to happen naturally. If an assertion fails, we'll have a helpful
121121
// error message handy to pass on to the user
122122
const catchQueryError = err => {
123123
error = err
124+
failedOldFunctionality = true
124125
const result = Cypress.$()
125126
result.selector = getSelector()
126127
return result
127128
}
128129

130+
// Before https://github.com/testing-library/cypress-testing-library/pull/100,
131+
// queries were run without being scoped to previous subjects. There is code now that depends
132+
// on functionality before #100. See if we can succeed using old functionality before finally failing
133+
// This function can be removed as a breaking change
134+
const catchAndTryOldFunctionality = err => {
135+
error = err
136+
failedNewFunctionality = true
137+
const container = options.fallbackToPreviousFunctionality ? options.container || win.document : undefined
138+
return getValue(container)
139+
}
140+
129141
const resolveValue = () => {
130142
// retry calling "getValue" until following assertions pass or this command times out
131143
return Cypress.Promise.try(getValue)
144+
.catch(catchAndTryOldFunctionality)
132145
.catch(catchQueryError)
133146
.then(value => {
134147
return cy.verifyUpcomingAssertions(value, options, {
@@ -155,7 +168,11 @@ function createCommand(queryName, implementationName) {
155168
return subject
156169
}).finally(() => {
157170
if (options._log) {
158-
options._log.end()
171+
if (failedNewFunctionality && !failedOldFunctionality) {
172+
options._log.error(Error(`@testing-library/cypress will soon only use previous subjects when queries are added to a chain of commands. We've detected an instance where the this functionality failed, but the old functionality passed. Please use cy.${queryName}(${queryArgument(args)}) instead of continuing from a previous chain.`))
173+
} else {
174+
options._log.end()
175+
}
159176
}
160177
})
161178
},

0 commit comments

Comments
 (0)