Skip to content
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

Cypress 12.0.0 compatibility #238

Merged
merged 8 commits into from Dec 13, 2022
3 changes: 1 addition & 2 deletions cypress.config.js
@@ -1,7 +1,6 @@
const {defineConfig} = require('cypress')

module.exports = defineConfig({
video: false,

e2e: {},
video: false,
})
4 changes: 2 additions & 2 deletions package.json
Expand Up @@ -44,13 +44,13 @@
"@testing-library/dom": "^8.1.0"
},
"devDependencies": {
"cypress": "^10.0.0",
"cypress": "^12.0.0",
"kcd-scripts": "^11.2.0",
"npm-run-all": "^4.1.5",
"typescript": "^4.3.5"
},
"peerDependencies": {
"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"
"cypress": "^12.0.0"
},
"eslintConfig": {
"extends": "./node_modules/kcd-scripts/eslint.js",
Expand Down
9 changes: 5 additions & 4 deletions src/__tests__/add-commands.js
Expand Up @@ -2,16 +2,17 @@ import {commands} from '../'

test('adds commands to Cypress', () => {
const addMock = jest.fn().mockName('Cypress.Commands.add')
global.Cypress = {Commands: {add: addMock}}
const addQueryMock = jest.fn().mockName('Cypress.Commands.addQuery')
global.Cypress = {Commands: {add: addMock, addQuery: addQueryMock}}
global.cy = {}

require('../add-commands')

expect(addMock).toHaveBeenCalledTimes(commands.length + 1) // we're also adding a configuration command
expect(addQueryMock).toHaveBeenCalledTimes(commands.length)
expect(addMock).toHaveBeenCalledTimes(1) // we're also adding a configuration command
commands.forEach(({name}, index) => {
expect(addMock.mock.calls[index]).toMatchObject([
expect(addQueryMock.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
@@ -1,7 +1,7 @@
import {configure, commands} from './'

commands.forEach(({name, command, options = {}}) => {
Cypress.Commands.add(name, options, command)
commands.forEach(({name, command}) => {
Cypress.Commands.addQuery(name, command)
})

Cypress.Commands.add('configureCypressTestingLibrary', config => {
Expand Down
129 changes: 47 additions & 82 deletions src/index.js
@@ -1,5 +1,5 @@
import {configure as configureDTL, queries} from '@testing-library/dom'
import {getContainer} from './utils'
import {getFirstElement} from './utils'

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

const commands = queryNames.map(queryName => {
return createCommand(queryName, queryName.replace(findRegex, 'get'))
return createQuery(queryName, queryName.replace(findRegex, 'get'))
})

function createCommand(queryName, implementationName) {
function createQuery(queryName, implementationName) {
return {
name: queryName,
options: {prevSubject: ['optional']},
command: (prevSubject, ...args) => {
command(...args) {
const lastArg = args[args.length - 1]
const defaults = {
timeout: Cypress.config().defaultCommandTimeout,
log: true,
}
const options =
typeof lastArg === 'object' ? {...defaults, ...lastArg} : defaults
const options = typeof lastArg === 'object' ? {...lastArg} : {}

const queryImpl = queries[implementationName]
const baseCommandImpl = container => {
return queryImpl(getContainer(container), ...args)
}
const commandImpl = container => baseCommandImpl(container)
this.set('timeout', options.timeout)

const queryImpl = queries[implementationName]
const inputArr = args.filter(filterInputs)

const getSelector = () => `${queryName}(${queryArgument(args)})`

const win = cy.state('window')
const selector = `${queryName}(${queryArgument(args)})`

const consoleProps = {
// TODO: Would be good to completely separate out the types of input into their own properties
input: inputArr,
Selector: getSelector(),
'Applied To': getContainer(
options.container || prevSubject || win.document,
),
Selector: selector,
}

if (options.log) {
options._log = Cypress.log({
type: prevSubject ? 'child' : 'parent',
const log =
options.log !== false &&
Cypress.log({
name: queryName,
type:
this.get('prev').get('chainerId') === this.get('chainerId')
? 'child'
: 'parent',
message: inputArr,
timeout: options.timeout,
consoleProps: () => consoleProps,
})
}

const getValue = (
container = options.container || prevSubject || win.document,
) => {
const value = commandImpl(container)
const withinSubject = cy.state('withinSubjectChain')

let error
this.set('onFail', err => {
if (error) {
err.message = error.message
}
})

return subject => {
const container = getFirstElement(
options.container ||
subject ||
cy.getSubjectFromChain(withinSubject) ||
cy.state('window').document,
)
consoleProps['Applied To'] = container

let value

try {
value = queryImpl(container, ...args)
} catch (e) {
error = e
value = Cypress.$()
value.selector = selector
}

const result = Cypress.$(value)
if (value && options._log) {
options._log.set('$el', result)

if (value && log) {
log.set('$el', result)
}

// 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 = getSelector()
result.selector = selector

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

return result
}

let error

// Errors will be thrown by @testing-library/dom, but a query might be followed by `.should('not.exist')`
// We just need to capture the error thrown by @testing-library/dom and return an empty jQuery NodeList
// to allow Cypress assertions errors to happen naturally. If an assertion fails, we'll have a helpful
// error message handy to pass on to the user
const catchQueryError = err => {
error = err
const result = Cypress.$()
result.selector = getSelector()
return result
}

const resolveValue = () => {
// retry calling "getValue" until following assertions pass or this command times out
return Cypress.Promise.try(getValue)
.catch(catchQueryError)
.then(value => {
return cy.verifyUpcomingAssertions(value, options, {
onRetry: resolveValue,
onFail: () => {
// We want to override Cypress's normal non-existence message with @testing-library/dom's more helpful ones
if (error) {
options.error.message = error.message
}
},
})
})
}

return resolveValue()
.then(subject => {
// Remove the error that occurred because it is irrelevant now
if (consoleProps.error) {
delete consoleProps.error
}
if (options._log) {
options._log.snapshot()
}

return subject
})
.finally(() => {
if (options._log) {
options._log.end()
}
})
},
}
}
Expand Down
17 changes: 2 additions & 15 deletions src/utils.js
Expand Up @@ -5,19 +5,6 @@ function getFirstElement(jqueryOrElement) {
return jqueryOrElement
}

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

if (!subject && withinContainer) {
return getFirstElement(withinContainer)
}
return getFirstElement(container)
}

export {getFirstElement, getContainer}

/* globals Cypress, cy */
/* globals Cypress */