Skip to content

Commit 8c93575

Browse files
authored
chore: Use addQuery from Cypress 12 (#238)
Use Commands.addQuery rather than Commands.add `addQuery` cleans up code and fixes "Detached DOM" errors. BREAKING CHANGE: Use addQuery interface, which is only present in Cypress 12+.
1 parent 8d66009 commit 8c93575

File tree

6 files changed

+59
-107
lines changed

6 files changed

+59
-107
lines changed

cypress.config.js

+1-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
const {defineConfig} = require('cypress')
22

33
module.exports = defineConfig({
4-
video: false,
5-
64
e2e: {},
5+
video: false,
76
})

package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -44,13 +44,13 @@
4444
"@testing-library/dom": "^8.1.0"
4545
},
4646
"devDependencies": {
47-
"cypress": "^10.0.0",
47+
"cypress": "^12.0.0",
4848
"kcd-scripts": "^11.2.0",
4949
"npm-run-all": "^4.1.5",
5050
"typescript": "^4.3.5"
5151
},
5252
"peerDependencies": {
53-
"cypress": "^2.1.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0"
53+
"cypress": "^12.0.0"
5454
},
5555
"eslintConfig": {
5656
"extends": "./node_modules/kcd-scripts/eslint.js",

src/__tests__/add-commands.js

+5-4
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,17 @@ import {commands} from '../'
22

33
test('adds commands to Cypress', () => {
44
const addMock = jest.fn().mockName('Cypress.Commands.add')
5-
global.Cypress = {Commands: {add: addMock}}
5+
const addQueryMock = jest.fn().mockName('Cypress.Commands.addQuery')
6+
global.Cypress = {Commands: {add: addMock, addQuery: addQueryMock}}
67
global.cy = {}
78

89
require('../add-commands')
910

10-
expect(addMock).toHaveBeenCalledTimes(commands.length + 1) // we're also adding a configuration command
11+
expect(addQueryMock).toHaveBeenCalledTimes(commands.length)
12+
expect(addMock).toHaveBeenCalledTimes(1) // we're also adding a configuration command
1113
commands.forEach(({name}, index) => {
12-
expect(addMock.mock.calls[index]).toMatchObject([
14+
expect(addQueryMock.mock.calls[index]).toMatchObject([
1315
name,
14-
{},
1516
// We get a new function that is `command.bind(null, cy)` i.e. global `cy` passed into the first argument.
1617
// The commands themselves will be tested separately in the Cypress end-to-end tests.
1718
expect.any(Function),

src/add-commands.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import {configure, commands} from './'
22

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

77
Cypress.Commands.add('configureCypressTestingLibrary', config => {

src/index.js

+47-82
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {configure as configureDTL, queries} from '@testing-library/dom'
2-
import {getContainer} from './utils'
2+
import {getFirstElement} from './utils'
33

44
function configure({fallbackRetryWithoutPreviousSubject, ...config}) {
55
return configureDTL(config)
@@ -9,66 +9,79 @@ const findRegex = /^find/
99
const queryNames = Object.keys(queries).filter(q => findRegex.test(q))
1010

1111
const commands = queryNames.map(queryName => {
12-
return createCommand(queryName, queryName.replace(findRegex, 'get'))
12+
return createQuery(queryName, queryName.replace(findRegex, 'get'))
1313
})
1414

15-
function createCommand(queryName, implementationName) {
15+
function createQuery(queryName, implementationName) {
1616
return {
1717
name: queryName,
18-
options: {prevSubject: ['optional']},
19-
command: (prevSubject, ...args) => {
18+
command(...args) {
2019
const lastArg = args[args.length - 1]
21-
const defaults = {
22-
timeout: Cypress.config().defaultCommandTimeout,
23-
log: true,
24-
}
25-
const options =
26-
typeof lastArg === 'object' ? {...defaults, ...lastArg} : defaults
20+
const options = typeof lastArg === 'object' ? {...lastArg} : {}
2721

28-
const queryImpl = queries[implementationName]
29-
const baseCommandImpl = container => {
30-
return queryImpl(getContainer(container), ...args)
31-
}
32-
const commandImpl = container => baseCommandImpl(container)
22+
this.set('timeout', options.timeout)
3323

24+
const queryImpl = queries[implementationName]
3425
const inputArr = args.filter(filterInputs)
3526

36-
const getSelector = () => `${queryName}(${queryArgument(args)})`
37-
38-
const win = cy.state('window')
27+
const selector = `${queryName}(${queryArgument(args)})`
3928

4029
const consoleProps = {
4130
// TODO: Would be good to completely separate out the types of input into their own properties
4231
input: inputArr,
43-
Selector: getSelector(),
44-
'Applied To': getContainer(
45-
options.container || prevSubject || win.document,
46-
),
32+
Selector: selector,
4733
}
4834

49-
if (options.log) {
50-
options._log = Cypress.log({
51-
type: prevSubject ? 'child' : 'parent',
35+
const log =
36+
options.log !== false &&
37+
Cypress.log({
5238
name: queryName,
39+
type:
40+
this.get('prev').get('chainerId') === this.get('chainerId')
41+
? 'child'
42+
: 'parent',
5343
message: inputArr,
5444
timeout: options.timeout,
5545
consoleProps: () => consoleProps,
5646
})
57-
}
5847

59-
const getValue = (
60-
container = options.container || prevSubject || win.document,
61-
) => {
62-
const value = commandImpl(container)
48+
const withinSubject = cy.state('withinSubjectChain')
49+
50+
let error
51+
this.set('onFail', err => {
52+
if (error) {
53+
err.message = error.message
54+
}
55+
})
56+
57+
return subject => {
58+
const container = getFirstElement(
59+
options.container ||
60+
subject ||
61+
cy.getSubjectFromChain(withinSubject) ||
62+
cy.state('window').document,
63+
)
64+
consoleProps['Applied To'] = container
65+
66+
let value
67+
68+
try {
69+
value = queryImpl(container, ...args)
70+
} catch (e) {
71+
error = e
72+
value = Cypress.$()
73+
value.selector = selector
74+
}
6375

6476
const result = Cypress.$(value)
65-
if (value && options._log) {
66-
options._log.set('$el', result)
77+
78+
if (value && log) {
79+
log.set('$el', result)
6780
}
6881

6982
// Overriding the selector of the jquery object because it's displayed in the long message of .should('exist') failure message
7083
// Hopefully it makes it clearer, because I find the normal response of "Expected to find element '', but never found it" confusing
71-
result.selector = getSelector()
84+
result.selector = selector
7285

7386
consoleProps.elements = result.length
7487
if (result.length === 1) {
@@ -86,54 +99,6 @@ function createCommand(queryName, implementationName) {
8699

87100
return result
88101
}
89-
90-
let error
91-
92-
// Errors will be thrown by @testing-library/dom, but a query might be followed by `.should('not.exist')`
93-
// We just need to capture the error thrown by @testing-library/dom and return an empty jQuery NodeList
94-
// to allow Cypress assertions errors to happen naturally. If an assertion fails, we'll have a helpful
95-
// error message handy to pass on to the user
96-
const catchQueryError = err => {
97-
error = err
98-
const result = Cypress.$()
99-
result.selector = getSelector()
100-
return result
101-
}
102-
103-
const resolveValue = () => {
104-
// retry calling "getValue" until following assertions pass or this command times out
105-
return Cypress.Promise.try(getValue)
106-
.catch(catchQueryError)
107-
.then(value => {
108-
return cy.verifyUpcomingAssertions(value, options, {
109-
onRetry: resolveValue,
110-
onFail: () => {
111-
// We want to override Cypress's normal non-existence message with @testing-library/dom's more helpful ones
112-
if (error) {
113-
options.error.message = error.message
114-
}
115-
},
116-
})
117-
})
118-
}
119-
120-
return resolveValue()
121-
.then(subject => {
122-
// Remove the error that occurred because it is irrelevant now
123-
if (consoleProps.error) {
124-
delete consoleProps.error
125-
}
126-
if (options._log) {
127-
options._log.snapshot()
128-
}
129-
130-
return subject
131-
})
132-
.finally(() => {
133-
if (options._log) {
134-
options._log.end()
135-
}
136-
})
137102
},
138103
}
139104
}

src/utils.js

+2-15
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,6 @@ function getFirstElement(jqueryOrElement) {
55
return jqueryOrElement
66
}
77

8-
function getContainer(container) {
9-
// Cypress 10 deprecated cy.state('subject') usage and suggest to use new cy.currentSubject.
10-
// https://docs.cypress.io/guides/references/changelog#10-5-0
11-
// Below change ensures we do not get spam of warnings and are backward compatible with older cypress versions.
12-
const subject = cy.currentSubject ? cy.currentSubject() : cy.state('subject');
13-
const withinContainer = cy.state('withinSubject')
8+
export {getFirstElement}
149

15-
if (!subject && withinContainer) {
16-
return getFirstElement(withinContainer)
17-
}
18-
return getFirstElement(container)
19-
}
20-
21-
export {getFirstElement, getContainer}
22-
23-
/* globals Cypress, cy */
10+
/* globals Cypress */

0 commit comments

Comments
 (0)