Skip to content
Closed
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
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"dependencies": {
"chalk": "^2.4.1",
"css": "^2.2.3",
"dom-path-utils": "0.2.0",
"jest-diff": "^23.6.0",
"jest-matcher-utils": "^23.6.0",
"lodash": "^4.17.11",
Expand Down
82 changes: 82 additions & 0 deletions src/__tests__/to-be-empty-node-list.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import {render} from './helpers/test-utils'

describe('NodeList .toBeEmpty', () => {
test('runs without failing.', () => {
const {container} = render(`
<div>
<span></span>
<span></span>
</div>`)

const emptyNodes = container.querySelectorAll('span')
expect(emptyNodes).toBeEmpty()
})

test('runs inverted without failing', () => {
const {container} = render(`
<div>
<span>a</span>
<span>a</span>
</div>`)

const emptyNodes = container.querySelectorAll('span')
expect(emptyNodes).not.toBeEmpty()
})

test('fails correctly', () => {
expect(() => {
const {container} = render(`
<div>
<span></span>
<span>a</span>
</div>`)

const emptyNodes = container.querySelectorAll('span')
expect(emptyNodes).toBeEmpty()
}).toThrowError()
})

test('fails inverted correctly', () => {
expect(() => {
const {container} = render(`
<div>
<span></span>
<span>a</span>
</div>`)

const emptyNodes = container.querySelectorAll('span')
expect(emptyNodes).not.toBeEmpty()
}).toThrowError()
})

test('fails large amount of elements', () => {
expect(() => {
const {container} = render(`
<div>
<span>a</span><span>a</span><span>a</span><span>a</span><span>a</span>
<span>a</span><span>a</span><span>a</span><span>a</span><span>a</span>
<span>a</span><span>a</span><span>a</span><span>a</span><span>a</span>
<span>a</span><span>a</span><span>a</span><span>a</span><span>a</span>
<span>a</span><span>a</span><span>a</span><span>a</span><span>a</span>
<span>a</span><span>a</span><span>a</span><span>a</span><span>a</span>
<span>a</span><span>a</span><span>a</span><span>a</span><span>a</span>
<span>a</span><span>a</span><span>a</span><span>a</span><span>a</span>
<span>a</span><span>a</span><span>a</span><span>a</span><span>a</span>
<span>a</span><span>a</span><span>a</span><span>a</span><span>a</span>
<span>a</span><span>a</span><span>a</span><span>a</span><span>a</span>
<span>a</span><span>a</span><span>a</span><span>a</span><span>a</span>
<span>a</span><span>a</span><span>a</span><span>a</span><span>a</span>
<span>a</span><span>a</span><span>a</span><span>a</span><span>a</span>
<span>a</span><span>a</span><span>a</span><span>a</span><span>a</span>
<span>a</span><span>a</span><span>a</span><span>a</span><span>a</span>
<span>a</span><span>a</span><span>a</span><span>a</span><span>a</span>
<span>a</span><span>a</span><span>a</span><span>a</span><span>a</span>
<span>a</span><span>a</span><span>a</span><span>a</span><span>a</span>
<span>a</span><span>a</span><span>a</span><span>a</span><span>a</span>
</div>`)

const emptyNodes = container.querySelectorAll('span')
expect(emptyNodes).toBeEmpty()
}).toThrowError()
})
})
41 changes: 28 additions & 13 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,31 @@
import {toBeInTheDOM} from './to-be-in-the-dom'
import {toBeInTheDocument} from './to-be-in-the-document'
import {toBeEmpty} from './to-be-empty'
import {toContainElement} from './to-contain-element'
import {toContainHTML} from './to-contain-html'
import {toHaveTextContent} from './to-have-text-content'
import {toHaveAttribute} from './to-have-attribute'
import {toHaveClass} from './to-have-class'
import {toHaveStyle} from './to-have-style'
import {toHaveFocus} from './to-have-focus'
import {toHaveFormValues} from './to-have-form-values'
import {toBeVisible} from './to-be-visible'
import {toBeDisabled} from './to-be-disabled'
import {toBeInTheDOM as toBeInTheDOMDirect} from './to-be-in-the-dom'
import {toBeInTheDocument as toBeInTheDocumentDirect} from './to-be-in-the-document'
import {toBeEmpty as toBeEmptyDirect} from './to-be-empty'
import {toContainElement as toContainElementDirect} from './to-contain-element'
import {toContainHTML as toContainHTMLDirect} from './to-contain-html'
import {toHaveTextContent as toHaveTextContentDirect} from './to-have-text-content'
import {toHaveAttribute as toHaveAttributeDirect} from './to-have-attribute'
import {toHaveClass as toHaveClassDirect} from './to-have-class'
import {toHaveStyle as toHaveStyleDirect} from './to-have-style'
import {toHaveFocus as toHaveFocusDirect} from './to-have-focus'
import {toHaveFormValues as toHaveFormValuesDirect} from './to-have-form-values'
import {toBeVisible as toBeVisibleDirect} from './to-be-visible'
import {toBeDisabled as toBeDisabledDirect} from './to-be-disabled'
import {withNodeList} from './with-node-list'

const toBeInTheDOM = withNodeList(toBeInTheDOMDirect)
const toBeInTheDocument = withNodeList(toBeInTheDocumentDirect)
const toBeEmpty = withNodeList(toBeEmptyDirect)
const toContainElement = withNodeList(toContainElementDirect)
const toContainHTML = withNodeList(toContainHTMLDirect)
const toHaveTextContent = withNodeList(toHaveTextContentDirect)
const toHaveAttribute = withNodeList(toHaveAttributeDirect)
const toHaveClass = withNodeList(toHaveClassDirect)
const toHaveStyle = withNodeList(toHaveStyleDirect)
const toHaveFocus = withNodeList(toHaveFocusDirect)
const toHaveFormValues = withNodeList(toHaveFormValuesDirect)
const toBeVisible = withNodeList(toBeVisibleDirect)
const toBeDisabled = withNodeList(toBeDisabledDirect)

export {
toBeInTheDOM,
Expand Down
113 changes: 113 additions & 0 deletions src/with-node-list.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
// ============================
// NodeList Matcher HOC
// ============================

import {stringify, RECEIVED_COLOR as colorAsError} from 'jest-matcher-utils'
import {getSelectorPath} from 'dom-path-utils'

// ----------------------------
// Generic Utils
// ----------------------------

const conditional = (condition, trueValue, falseValue) =>
condition ? trueValue : falseValue

// ----------------------------
// Messaging
// ----------------------------

const getMatcherMessage = result => result.message()
const getFirstMatcherMessage = results => getMatcherMessage(results[0])
const getErrorElement = (index, element) =>
colorAsError(`[${index}] ${element}`)
const getErrorMessage = (index, element, path) =>
`\t${getErrorElement(index, element)}\n\t${path}\n`

// ----------------------------
// Results Logic
// ----------------------------

const markErrors = nodeList => (result, index) => {
const element = nodeList[index]

return conditional(
result.pass,
null,
getErrorMessage(
index,
stringify(element.cloneNode(false)),
getSelectorPath(element, ['id', 'class', 'data-testid']),
),
)
}

const createResults = (results, nodeList, isNot) => {
const errors = results.map(markErrors(nodeList)).filter(Boolean)
const allMatchersPassed = isNot
? errors.length === results.length
: errors.length === 0

// When isNot is set, jest expects a "false" to pass
const pass = isNot ? !allMatchersPassed : allMatchersPassed

return {
pass,
message: () =>
`${errors.length} of ${
results.length
} NodeList elements failed this test. ${
errors.length > 3 ? 'Displaying subset of failing elements:' : ''
}

${errors.slice(0, 3).join(`\n`)}${errors.length > 3 ? '\n\t...\n' : ''}
${colorAsError('Error message for element [0]: \n============================')}

${getFirstMatcherMessage(results)}

${colorAsError('============================')}
`,
}
}

// ----------------------------
// HOC Matcher Creation
// ----------------------------

function wrapMatcher(matcher, ...rest) {
return function elementWalker(element) {
return matcher.call(this, element, ...rest)
}.bind(this)
}

function nodeListMatcher(matcher) {
return function matcherParameters(nodeList, ...rest) {
const wrappedMatcher = wrapMatcher.call(this, matcher, ...rest)

return createResults(
Array.prototype.map.call(nodeList, wrappedMatcher),
nodeList,
this.isNot,
)
}.bind(this)
}

// ----------------------------
// Logic Units
// ----------------------------

const isNodeList = elementSelection =>
!!elementSelection && elementSelection.constructor.name === 'NodeList'

// ----------------------------
// Main Export
// ----------------------------

export function withNodeList(matcher) {
return function initialMatcherCall(nodeListOrElement, ...rest) {
return conditional(
isNodeList(nodeListOrElement),
nodeListMatcher.call(this, matcher),
matcher.bind(this),
)(nodeListOrElement, ...rest)
}
}