From 17770e25c5c0a4fece84bc4b7bcfbcf4c9d0fe4d Mon Sep 17 00:00:00 2001 From: Seth MacPherson Date: Fri, 24 Aug 2018 08:03:57 -0500 Subject: [PATCH 1/7] #52: Added prototype concept for nodeList injection into matchers --- src/inject-node-list.js | 44 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 src/inject-node-list.js diff --git a/src/inject-node-list.js b/src/inject-node-list.js new file mode 100644 index 00000000..1eb1645a --- /dev/null +++ b/src/inject-node-list.js @@ -0,0 +1,44 @@ +const createErrorMessage = error => { + return `[${error.index}] ${error.element}` +} + +const displayResults = (results, nodeList) => { + const originalMessage = results[0].message + + const errors = results + .map((result, index) => { + return result.pass + ? null + : { + index, + element: nodeList[index].cloneNode(false), + } + }) + .filter(Boolean) + + return { + pass: errors.length === 0, + message: `The following elements failed:\n\n${errors + .map(createErrorMessage) + .join(`\n`)}\n\n${originalMessage}`, + } +} + +const createNodeListTest = callback => element => callback.bind(null, element) + +const nodeListCallback = callback => (nodeList, ...rest) => { + const matchers = Array.prototype.map.call( + nodeList, + createNodeListTest(callback), + ) + const results = matchers.map(matcher => matcher(...rest)) + + return displayResults(results, nodeList) +} + +const isNodeList = nodeList => nodeList instanceof NodeList + +export const withNodeList = elementCallback => (nodeListOrElement, ...rest) => + isNodeList(nodeListOrElement) + ? nodeListCallback(elementCallback)(nodeListOrElement, ...rest) + : elementCallback(nodeListOrElement, ...rest) From be886de4659c67f16f7828b9bd7a4eef72eddde6 Mon Sep 17 00:00:00 2001 From: Seth MacPherson Date: Fri, 24 Aug 2018 08:13:52 -0500 Subject: [PATCH 2/7] #52: Minor cleanup on prototype HOF concept --- src/inject-node-list.js | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/inject-node-list.js b/src/inject-node-list.js index 1eb1645a..d67e11d8 100644 --- a/src/inject-node-list.js +++ b/src/inject-node-list.js @@ -26,19 +26,18 @@ const displayResults = (results, nodeList) => { const createNodeListTest = callback => element => callback.bind(null, element) -const nodeListCallback = callback => (nodeList, ...rest) => { - const matchers = Array.prototype.map.call( +const nodeListMatcher = matcher => (nodeList, ...rest) => { + const results = Array.prototype.map.call( nodeList, - createNodeListTest(callback), + createNodeListTest(matcher)(...rest), ) - const results = matchers.map(matcher => matcher(...rest)) return displayResults(results, nodeList) } const isNodeList = nodeList => nodeList instanceof NodeList -export const withNodeList = elementCallback => (nodeListOrElement, ...rest) => +export const withNodeList = matcher => (nodeListOrElement, ...rest) => isNodeList(nodeListOrElement) - ? nodeListCallback(elementCallback)(nodeListOrElement, ...rest) - : elementCallback(nodeListOrElement, ...rest) + ? nodeListMatcher(matcher)(nodeListOrElement, ...rest) + : matcher(nodeListOrElement, ...rest) From 05f87efbe27f80ef3e434e10508bb6e85d85fe80 Mon Sep 17 00:00:00 2001 From: Seth MacPherson Date: Fri, 24 Aug 2018 09:00:29 -0500 Subject: [PATCH 3/7] #52: Further cleanup and added sections for code review --- src/inject-node-list.js | 82 +++++++++++++++++++++++++++-------------- 1 file changed, 54 insertions(+), 28 deletions(-) diff --git a/src/inject-node-list.js b/src/inject-node-list.js index d67e11d8..e3746c6d 100644 --- a/src/inject-node-list.js +++ b/src/inject-node-list.js @@ -1,43 +1,69 @@ -const createErrorMessage = error => { - return `[${error.index}] ${error.element}` -} +// ============================ +// NodeList HOC Matcher +// ============================ + +// ---------------------------- +// Generic Utils +// ---------------------------- + +const conditional = (condition, trueValue, falseValue) => + condition ? trueValue : falseValue + +// ---------------------------- +// Messaging +// ---------------------------- + +const getMatcherMessage = result => result.message +const getFirstMatcherMessage = results => getMatcherMessage(results[0]) + +const getErrorMessage = (index, element) => `[${index}] ${element}` + +// ---------------------------- +// Results Logic +// ---------------------------- -const displayResults = (results, nodeList) => { - const originalMessage = results[0].message +const markErrors = nodeList => (result, index) => + conditional( + result.pass, + null, + getErrorMessage(index, nodeList[index].cloneNode(false)), + ) - const errors = results - .map((result, index) => { - return result.pass - ? null - : { - index, - element: nodeList[index].cloneNode(false), - } - }) - .filter(Boolean) +const createResults = (results, nodeList) => { + const errors = results.map(markErrors(nodeList)).filter(Boolean) return { pass: errors.length === 0, - message: `The following elements failed:\n\n${errors - .map(createErrorMessage) - .join(`\n`)}\n\n${originalMessage}`, + message: `The following elements failed:\n\n${errors.join( + `\n`, + )}\n\n${getFirstMatcherMessage(nodeList)}`, } } -const createNodeListTest = callback => element => callback.bind(null, element) +// ---------------------------- +// HOC Matcher Creation +// ---------------------------- -const nodeListMatcher = matcher => (nodeList, ...rest) => { - const results = Array.prototype.map.call( - nodeList, - createNodeListTest(matcher)(...rest), - ) +const createMatcher = matcher => element => matcher.bind(null, element) - return displayResults(results, nodeList) +const nodeListMatcher = matcher => (nodeList, ...rest) => { + return createResults( + Array.prototype.map.call(nodeList, createMatcher(matcher)(...rest)), + )(nodeList) } +// ---------------------------- +// Logic Units +// ---------------------------- + const isNodeList = nodeList => nodeList instanceof NodeList +// ---------------------------- +// Main Export +// ---------------------------- + export const withNodeList = matcher => (nodeListOrElement, ...rest) => - isNodeList(nodeListOrElement) - ? nodeListMatcher(matcher)(nodeListOrElement, ...rest) - : matcher(nodeListOrElement, ...rest) + conditional(isNodeList(nodeListOrElement), nodeListMatcher(matcher), matcher)( + nodeListOrElement, + ...rest, + ) From afec4ef76f321eec5c40f471dabf8ddba2caddcc Mon Sep 17 00:00:00 2001 From: Seth MacPherson Date: Fri, 24 Aug 2018 09:04:26 -0500 Subject: [PATCH 4/7] #52: More cleanup and minor logic fixes --- src/inject-node-list.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/inject-node-list.js b/src/inject-node-list.js index e3746c6d..67ddda1e 100644 --- a/src/inject-node-list.js +++ b/src/inject-node-list.js @@ -1,5 +1,5 @@ // ============================ -// NodeList HOC Matcher +// NodeList Matcher HOC // ============================ // ---------------------------- @@ -49,7 +49,8 @@ const createMatcher = matcher => element => matcher.bind(null, element) const nodeListMatcher = matcher => (nodeList, ...rest) => { return createResults( Array.prototype.map.call(nodeList, createMatcher(matcher)(...rest)), - )(nodeList) + nodeList, + ) } // ---------------------------- From 7266d7d87ee014925823ea025af86360f2537e18 Mon Sep 17 00:00:00 2001 From: Seth MacPherson Date: Mon, 3 Sep 2018 07:48:17 -0500 Subject: [PATCH 5/7] Set NodeList wrapper to basic working state --- package.json | 1 + src/__tests__/to-be-empty-node-list.js | 70 ++++++++++++++++ src/index.js | 38 ++++++--- src/inject-node-list.js | 70 ---------------- src/with-node-list.js | 112 +++++++++++++++++++++++++ 5 files changed, 209 insertions(+), 82 deletions(-) create mode 100644 src/__tests__/to-be-empty-node-list.js delete mode 100644 src/inject-node-list.js create mode 100644 src/with-node-list.js diff --git a/package.json b/package.json index 56c38e5e..8dd953ca 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "redent": "^2.0.0" }, "devDependencies": { + "dom-path-utils": "0.1.0", "kcd-scripts": "^0.37.0" }, "eslintConfig": { diff --git a/src/__tests__/to-be-empty-node-list.js b/src/__tests__/to-be-empty-node-list.js new file mode 100644 index 00000000..f2b9d8d5 --- /dev/null +++ b/src/__tests__/to-be-empty-node-list.js @@ -0,0 +1,70 @@ +describe('NodeList .toBeEmpty', () => { + test('runs without failing.', () => { + document.body.innerHTML = ` +
+ + +
` + + const emptyNodes = document.querySelectorAll('span') + expect(emptyNodes).toBeEmpty() + }) + + test('runs inverted without failing', () => { + document.body.innerHTML = ` +
+ a + a +
` + + const emptyNodes = document.querySelectorAll('span') + expect(emptyNodes).not.toBeEmpty() + }) + + test('fails normally correctly', () => { + expect(() => { + document.body.innerHTML = ` +
+ + a +
` + + const emptyNodes = document.querySelectorAll('span') + expect(emptyNodes).toBeEmpty() + }).toThrowError() + }) + + test('fails inverted correctly', () => { + expect(() => { + document.body.innerHTML = ` +
+ + a +
` + + const emptyNodes = document.querySelectorAll('span') + expect(emptyNodes).not.toBeEmpty() + }).toThrowError() + }) + + test('fails a large amount of elements', () => { + expect(() => { + document.body.innerHTML = ` +
+ aaaaa + aaaaa + aaaaa + aaaaa + aaaaa + aaaaa + aaaaa + aaaaa + aaaaa + aaaaa +
` + + const emptyNodes = document.querySelectorAll('span') + expect(emptyNodes).toBeEmpty() + }).toThrowError() + }) +}) diff --git a/src/index.js b/src/index.js index 2c7e58df..c6b68d7e 100644 --- a/src/index.js +++ b/src/index.js @@ -1,15 +1,29 @@ -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 {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 {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 toBeVisible = withNodeList(toBeVisibleDirect) +const toBeDisabled = withNodeList(toBeDisabledDirect) export { toBeInTheDOM, diff --git a/src/inject-node-list.js b/src/inject-node-list.js deleted file mode 100644 index 67ddda1e..00000000 --- a/src/inject-node-list.js +++ /dev/null @@ -1,70 +0,0 @@ -// ============================ -// NodeList Matcher HOC -// ============================ - -// ---------------------------- -// Generic Utils -// ---------------------------- - -const conditional = (condition, trueValue, falseValue) => - condition ? trueValue : falseValue - -// ---------------------------- -// Messaging -// ---------------------------- - -const getMatcherMessage = result => result.message -const getFirstMatcherMessage = results => getMatcherMessage(results[0]) - -const getErrorMessage = (index, element) => `[${index}] ${element}` - -// ---------------------------- -// Results Logic -// ---------------------------- - -const markErrors = nodeList => (result, index) => - conditional( - result.pass, - null, - getErrorMessage(index, nodeList[index].cloneNode(false)), - ) - -const createResults = (results, nodeList) => { - const errors = results.map(markErrors(nodeList)).filter(Boolean) - - return { - pass: errors.length === 0, - message: `The following elements failed:\n\n${errors.join( - `\n`, - )}\n\n${getFirstMatcherMessage(nodeList)}`, - } -} - -// ---------------------------- -// HOC Matcher Creation -// ---------------------------- - -const createMatcher = matcher => element => matcher.bind(null, element) - -const nodeListMatcher = matcher => (nodeList, ...rest) => { - return createResults( - Array.prototype.map.call(nodeList, createMatcher(matcher)(...rest)), - nodeList, - ) -} - -// ---------------------------- -// Logic Units -// ---------------------------- - -const isNodeList = nodeList => nodeList instanceof NodeList - -// ---------------------------- -// Main Export -// ---------------------------- - -export const withNodeList = matcher => (nodeListOrElement, ...rest) => - conditional(isNodeList(nodeListOrElement), nodeListMatcher(matcher), matcher)( - nodeListOrElement, - ...rest, - ) diff --git a/src/with-node-list.js b/src/with-node-list.js new file mode 100644 index 00000000..86a5794d --- /dev/null +++ b/src/with-node-list.js @@ -0,0 +1,112 @@ +// ============================ +// 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 = nodeList => nodeList instanceof 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) + } +} From bb0df51c0b01a5b046a6f89a30ecdddf278a2ac6 Mon Sep 17 00:00:00 2001 From: Seth MacPherson Date: Mon, 3 Sep 2018 07:53:31 -0500 Subject: [PATCH 6/7] 52: Minor update to basic working state of NodeList --- src/__tests__/to-be-empty-node-list.js | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/__tests__/to-be-empty-node-list.js b/src/__tests__/to-be-empty-node-list.js index f2b9d8d5..ecf853e0 100644 --- a/src/__tests__/to-be-empty-node-list.js +++ b/src/__tests__/to-be-empty-node-list.js @@ -21,7 +21,7 @@ describe('NodeList .toBeEmpty', () => { expect(emptyNodes).not.toBeEmpty() }) - test('fails normally correctly', () => { + test('fails correctly', () => { expect(() => { document.body.innerHTML = `
@@ -47,7 +47,7 @@ describe('NodeList .toBeEmpty', () => { }).toThrowError() }) - test('fails a large amount of elements', () => { + test('fails large amount of elements', () => { expect(() => { document.body.innerHTML = `
@@ -61,6 +61,16 @@ describe('NodeList .toBeEmpty', () => { aaaaa aaaaa aaaaa + aaaaa + aaaaa + aaaaa + aaaaa + aaaaa + aaaaa + aaaaa + aaaaa + aaaaa + aaaaa
` const emptyNodes = document.querySelectorAll('span') From 0c8f554fc83506c6af9f0a18d666f4185a4bc696 Mon Sep 17 00:00:00 2001 From: Seth MacPherson Date: Fri, 12 Oct 2018 12:34:40 -0500 Subject: [PATCH 7/7] Updated matcher to handle non-global document setup --- package.json | 2 +- src/__tests__/to-be-empty-node-list.js | 32 ++++++++++++++------------ src/with-node-list.js | 3 ++- 3 files changed, 20 insertions(+), 17 deletions(-) diff --git a/package.json b/package.json index 8dc24699..fa19b477 100644 --- a/package.json +++ b/package.json @@ -35,13 +35,13 @@ "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", "pretty-format": "^23.6.0", "redent": "^2.0.0" }, "devDependencies": { - "dom-path-utils": "0.1.0", "jsdom": "^12.2.0", "kcd-scripts": "^0.44.0" }, diff --git a/src/__tests__/to-be-empty-node-list.js b/src/__tests__/to-be-empty-node-list.js index ecf853e0..e4b4b0ad 100644 --- a/src/__tests__/to-be-empty-node-list.js +++ b/src/__tests__/to-be-empty-node-list.js @@ -1,55 +1,57 @@ +import {render} from './helpers/test-utils' + describe('NodeList .toBeEmpty', () => { test('runs without failing.', () => { - document.body.innerHTML = ` + const {container} = render(`
-
` +
`) - const emptyNodes = document.querySelectorAll('span') + const emptyNodes = container.querySelectorAll('span') expect(emptyNodes).toBeEmpty() }) test('runs inverted without failing', () => { - document.body.innerHTML = ` + const {container} = render(`
a a -
` + `) - const emptyNodes = document.querySelectorAll('span') + const emptyNodes = container.querySelectorAll('span') expect(emptyNodes).not.toBeEmpty() }) test('fails correctly', () => { expect(() => { - document.body.innerHTML = ` + const {container} = render(`
a -
` + `) - const emptyNodes = document.querySelectorAll('span') + const emptyNodes = container.querySelectorAll('span') expect(emptyNodes).toBeEmpty() }).toThrowError() }) test('fails inverted correctly', () => { expect(() => { - document.body.innerHTML = ` + const {container} = render(`
a -
` + `) - const emptyNodes = document.querySelectorAll('span') + const emptyNodes = container.querySelectorAll('span') expect(emptyNodes).not.toBeEmpty() }).toThrowError() }) test('fails large amount of elements', () => { expect(() => { - document.body.innerHTML = ` + const {container} = render(`
aaaaa aaaaa @@ -71,9 +73,9 @@ describe('NodeList .toBeEmpty', () => { aaaaa aaaaa aaaaa -
` + `) - const emptyNodes = document.querySelectorAll('span') + const emptyNodes = container.querySelectorAll('span') expect(emptyNodes).toBeEmpty() }).toThrowError() }) diff --git a/src/with-node-list.js b/src/with-node-list.js index 86a5794d..71241afb 100644 --- a/src/with-node-list.js +++ b/src/with-node-list.js @@ -95,7 +95,8 @@ function nodeListMatcher(matcher) { // Logic Units // ---------------------------- -const isNodeList = nodeList => nodeList instanceof NodeList +const isNodeList = elementSelection => + !!elementSelection && elementSelection.constructor.name === 'NodeList' // ---------------------------- // Main Export