diff --git a/compat/src/index.js b/compat/src/index.js index be714e1105..865620be41 100644 --- a/compat/src/index.js +++ b/compat/src/index.js @@ -20,8 +20,8 @@ options.event = e => { }; let oldCatchRender = options._catchRender; -options._catchRender = (error, component) => ( - oldCatchRender && oldCatchRender(error, component) || catchRender(error, component) +options._catchRender = (error, newVNode, oldVNode) => ( + oldCatchRender && oldCatchRender(error, newVNode, oldVNode) || catchRender(error, newVNode, oldVNode) ); /** diff --git a/compat/src/suspense.js b/compat/src/suspense.js index 5f6b02d077..933a06011b 100644 --- a/compat/src/suspense.js +++ b/compat/src/suspense.js @@ -3,17 +3,24 @@ import { removeNode } from '../../src/util'; /** * @param {any} error - * @param {import('./internal').VNode} vnode + * @param {import('./internal').VNode} newVNode + * @param {import('./internal').VNode} oldVNode */ -export function catchRender(error, vnode) { +export function catchRender(error, newVNode, oldVNode) { // thrown Promises are meant to suspend... if (error.then) { /** @type {import('./internal').Component} */ let component; + let vnode = newVNode; for (; vnode; vnode = vnode._parent) { if ((component = vnode._component) && component._childDidSuspend) { + if (oldVNode) { + newVNode._dom = oldVNode._dom; + newVNode._children = oldVNode._children; + } + component._childDidSuspend(error); return true; } @@ -27,18 +34,18 @@ function removeDom(children) { for (let i = 0; i < children.length; i++) { let child = children[i]; if (child != null) { - if (child._children) { - removeDom(child._children); - } - if (child._dom) { + if (typeof child.type !== 'function' && child._dom) { removeNode(child._dom); } + else if (child._children) { + removeDom(child._children); + } } } } // having custom inheritance instead of a class here saves a lot of bytes -export function Suspense(props) { +export function Suspense() { // we do not call super here to golf some bytes... this._suspensions = []; } diff --git a/src/component.js b/src/component.js index e6ab7a0aa6..a3bba422bd 100644 --- a/src/component.js +++ b/src/component.js @@ -61,7 +61,7 @@ Component.prototype.setState = function(update, callback) { * re-renderd */ Component.prototype.forceUpdate = function(callback) { - let vnode = this._vnode, dom = this._vnode._dom, parentDom = this._parentDom; + let vnode = this._vnode, oldDom = this._vnode._dom, parentDom = this._parentDom; if (parentDom) { // Set render mode so that we can differantiate where the render request // is coming from. We need this because forceUpdate should never call @@ -69,8 +69,12 @@ Component.prototype.forceUpdate = function(callback) { const force = callback!==false; let mounts = []; - diff(parentDom, vnode, assign({}, vnode), this._context, parentDom.ownerSVGElement!==undefined, null, mounts, force, dom == null ? getDomSibling(vnode) : dom); + let newDom = diff(parentDom, vnode, assign({}, vnode), this._context, parentDom.ownerSVGElement!==undefined, null, mounts, force, oldDom == null ? getDomSibling(vnode) : oldDom); commitRoot(mounts, vnode); + + if (newDom != oldDom) { + updateParentDomPointers(vnode); + } } if (callback) callback(); }; @@ -118,6 +122,24 @@ export function getDomSibling(vnode, childIndex) { return typeof vnode.type === 'function' ? getDomSibling(vnode) : null; } +/** + * @param {import('./internal').VNode} vnode + */ +function updateParentDomPointers(vnode) { + if ((vnode = vnode._parent) != null && vnode._component != null) { + vnode._dom = vnode._component.base = null; + for (let i = 0; i < vnode._children.length; i++) { + let child = vnode._children[i]; + if (child != null && child._dom != null) { + vnode._dom = vnode._component.base = child._dom; + break; + } + } + + return updateParentDomPointers(vnode); + } +} + /** * The render queue * @type {Array} diff --git a/src/diff/children.js b/src/diff/children.js index 247dd5957b..426876248e 100644 --- a/src/diff/children.js +++ b/src/diff/children.js @@ -100,6 +100,11 @@ export function diffChildren(parentDom, newParentVNode, oldParentVNode, context, // have a non-null _lastDomChild. Continue the diff from the end of // this Fragment's DOM tree. newDom = childVNode._lastDomChild; + + // Eagerly cleanup _lastDomChild. We don't need to persist the value because + // it is only used by `diffChildren` to determine where to resume the diff after + // diffing Components and Fragments. + childVNode._lastDomChild = null; } else if (excessDomChildren==oldVNode || newDom!=oldDom || newDom.parentNode==null) { // NOTE: excessDomChildren==oldVNode above: @@ -124,7 +129,9 @@ export function diffChildren(parentDom, newParentVNode, oldParentVNode, context, if (typeof newParentVNode.type == 'function') { // At this point, if childVNode._lastDomChild existed, then - // newDom = childVNode._lastDomChild per line 101 + // newDom = childVNode._lastDomChild per line 101. Else it is + // the same as childVNode._dom, meaning this component returned + // only a single DOM node newParentVNode._lastDomChild = newDom; } } diff --git a/src/diff/index.js b/src/diff/index.js index f7d5c0a068..31659a8627 100644 --- a/src/diff/index.js +++ b/src/diff/index.js @@ -89,7 +89,6 @@ export function diff(parentDom, newVNode, oldVNode, context, isSvg, excessDomChi c._dirty = false; c._vnode = newVNode; newVNode._dom = oldVNode._dom; - newVNode._lastDomChild = oldVNode._lastDomChild; newVNode._children = oldVNode._children; break outer; } @@ -116,7 +115,7 @@ export function diff(parentDom, newVNode, oldVNode, context, isSvg, excessDomChi toChildArray(isTopLevelFragment ? tmp.props.children : tmp, newVNode._children=[], coerceToVNode, true); } catch (e) { - if ((tmp = options._catchRender) && tmp(e, newVNode)) return; + if ((tmp = options._catchRender) && tmp(e, newVNode, oldVNode)) break outer; throw e; } diff --git a/src/internal.d.ts b/src/internal.d.ts index 20fe1e9941..04193cbe58 100644 --- a/src/internal.d.ts +++ b/src/internal.d.ts @@ -12,7 +12,7 @@ export interface Options extends preact.Options { /** Attach a hook that is invoked before a hook's state is queried. */ _hook?(component: Component): void; /** Attach a hook that is invoked after an error is caught in a component but before calling lifecycle hooks */ - catchError?(error: any, vnode: VNode): void; + _catchError?(error: any, vnode: VNode): void; /** * Attach a hook that is invoked after an error is caught while executing render. * @@ -23,7 +23,7 @@ export interface Options extends preact.Options { * @param vnode The VNode whose component's render method threw an error * @return Return a boolean indicating whether the error was handled by the hook or not */ - catchRender?(error: any, vnode: VNode): boolean; + _catchRender?(error: any, newVNode: VNode, oldVNode: VNode): boolean; } export interface FunctionalComponent

extends preact.FunctionComponent

{ diff --git a/test/_util/dom.js b/test/_util/dom.js index ad01b8aaab..1d68abe872 100644 --- a/test/_util/dom.js +++ b/test/_util/dom.js @@ -10,6 +10,12 @@ export const span = contents => `${contents}`; */ export const div = contents => `

${contents}
`; +/** + * A helper to generate innerHTML validation strings containing p + * @param {string | number} contents The contents of the p, as a string + */ +export const p = contents => `

${contents}

`; + /** * A helper to generate innerHTML validation strings containing sections * @param {string | number} contents The contents of the section, as a string diff --git a/test/browser/components.test.js b/test/browser/components.test.js index 18ec0719c3..396fd9e6b4 100644 --- a/test/browser/components.test.js +++ b/test/browser/components.test.js @@ -1,6 +1,7 @@ import { createElement as h, render, Component, Fragment } from '../../src/index'; import { setupRerender } from 'preact/test-utils'; import { setupScratch, teardown, getMixedArray, mixedArrayHTML, serializeHtml } from '../_util/helpers'; +import { div, span, p } from '../_util/dom'; /** @jsx h */ @@ -1573,4 +1574,505 @@ describe('Components', () => { expect(mounted).to.equal(',1,0,3,2,5,4'); expect(unmounted).to.equal(',0,1,2,3'); }); + + describe('c.base', () => { + /* eslint-disable lines-around-comment */ + /** @type {import('../../src').Component} */ + let parentDom1; + /** @type {import('../../src').Component} */ + let parent1; + /** @type {import('../../src').Component} */ + let parent2; + /** @type {import('../../src').Component} */ + let maybe; + /** @type {import('../../src').Component} */ + let child; + /** @type {import('../../src').Component} */ + let sibling; + /** @type {import('../../src').Component} */ + let nullInst; + + /** @type {() => void} */ + let toggleMaybeNull; + /** @type {() => void} */ + let swapChildTag; + + function ParentWithDom(props) { + parentDom1 = this; + return
{props.children}
; + } + + class Parent1 extends Component { + render() { + parent1 = this; + return this.props.children; + } + } + + function Parent2(props) { + parent2 = this; + return props.children; + } + + class MaybeNull extends Component { + constructor(props) { + super(props); + maybe = this; + this.state = { active: props.active || false }; + toggleMaybeNull = () => this.setState(prev => ({ + active: !prev.active + })); + } + render() { + return this.state.active ?
maybe
: null; + } + } + + class Child extends Component { + constructor(props) { + super(props); + child = this; + this.state = { tagName: 'p' }; + swapChildTag = () => this.setState(prev => ({ + tagName: prev.tagName == 'p' ? 'span' : 'p' + })); + + } + render() { + return h(this.state.tagName, null, 'child'); + } + } + + + function Sibling(props) { + sibling = this; + return

; + } + + function Null() { + nullInst = this; + return null; + } + + afterEach(() => { + parentDom1 = null; + parent1 = null; + parent2 = null; + child = null; + sibling = null; + }); + + it('should keep c.base up to date if a nested child component changes DOM nodes', () => { + render(( + + + + + + + + ), scratch); + + expect(scratch.innerHTML).to.equal('

child

'); + expect(child.base).to.equalNode(scratch.firstChild.firstChild); + expect(parent2.base).to.equalNode(child.base); + expect(parent1.base).to.equalNode(child.base); + expect(parentDom1.base).to.equalNode(scratch.firstChild); + + swapChildTag(); + rerender(); + + expect(scratch.innerHTML).to.equal('
child
'); + expect(child.base).to.equalNode(scratch.firstChild.firstChild); + expect(parent2.base).to.equalNode(child.base); + expect(parent1.base).to.equalNode(child.base); + expect(parentDom1.base).to.equalNode(scratch.firstChild); + }); + + it('should not update sibling c.base if child component changes DOM nodes', () => { + let s1 = {}, s2 = {}, s3 = {}, s4 = {}; + + render(( + + + + + + + + + + + + + + ), scratch); + + expect(scratch.innerHTML).to.equal('

child

'); + expect(child.base).to.equalNode(scratch.firstChild.firstChild); + expect(parent2.base).to.equalNode(child.base); + expect(parent1.base).to.equalNode(child.base); + expect(parentDom1.base).to.equalNode(scratch.firstChild); + expect(s1.current.base).to.equalNode(scratch.firstChild.childNodes[1]); + expect(s2.current.base).to.equalNode(scratch.firstChild.childNodes[2]); + expect(s3.current.base).to.equalNode(scratch.firstChild.childNodes[3]); + expect(s4.current.base).to.equalNode(scratch.lastChild); + + swapChildTag(); + rerender(); + + expect(scratch.innerHTML).to.equal('
child

'); + expect(child.base).to.equalNode(scratch.firstChild.firstChild); + expect(parent2.base).to.equalNode(child.base); + expect(parent1.base).to.equalNode(child.base); + expect(parentDom1.base).to.equalNode(scratch.firstChild); + expect(s1.current.base).to.equalNode(scratch.firstChild.childNodes[1]); + expect(s2.current.base).to.equalNode(scratch.firstChild.childNodes[2]); + expect(s3.current.base).to.equalNode(scratch.firstChild.childNodes[3]); + expect(s4.current.base).to.equalNode(scratch.lastChild); + }); + + it('should not update parent c.base if child component changes DOM nodes and it is not first child component', () => { + render(( + + + + + ), scratch); + + expect(scratch.innerHTML).to.equal('

child

'); + expect(child.base).to.equalNode(scratch.lastChild); + expect(sibling.base).to.equalNode(scratch.firstChild); + expect(parent1.base).to.equalNode(sibling.base); + + swapChildTag(); + rerender(); + + expect(scratch.innerHTML).to.equal('

child'); + expect(child.base).to.equalNode(scratch.lastChild); + expect(sibling.base).to.equalNode(scratch.firstChild); + expect(parent1.base).to.equalNode(sibling.base); + }); + + it('should update parent c.base if child component changes DOM nodes and it is first non-null child component', () => { + render(( + + + + + + ), scratch); + + expect(scratch.innerHTML).to.equal('

child

'); + expect(nullInst.base).to.equalNode(null); + expect(child.base).to.equalNode(scratch.firstChild); + expect(sibling.base).to.equalNode(scratch.lastChild); + expect(parent1.base).to.equalNode(child.base); + + swapChildTag(); + rerender(); + + expect(scratch.innerHTML).to.equal('child

'); + expect(nullInst.base).to.equalNode(null); + expect(child.base).to.equalNode(scratch.firstChild); + expect(sibling.base).to.equalNode(scratch.lastChild); + expect(parent1.base).to.equalNode(child.base); + }); + + it('should not update parent c.base if child component changes DOM nodes and a parent is not first child component', () => { + render(( + + + + + + + + + ), scratch); + + expect(scratch.innerHTML).to.equal('

child

'); + expect(child.base).to.equalNode(scratch.firstChild.lastChild); + expect(parent2.base).to.equalNode(child.base); + expect(sibling.base).to.equalNode(scratch.firstChild.firstChild); + expect(parent1.base).to.equalNode(sibling.base); + expect(parentDom1.base).to.equalNode(scratch.firstChild); + + swapChildTag(); + rerender(); + + expect(scratch.innerHTML).to.equal('

child
'); + expect(child.base).to.equalNode(scratch.firstChild.lastChild); + expect(parent2.base).to.equalNode(child.base); + expect(sibling.base).to.equalNode(scratch.firstChild.firstChild); + expect(parent1.base).to.equalNode(sibling.base); + expect(parentDom1.base).to.equalNode(scratch.firstChild); + }); + + it('should update parent c.base if first child becomes null', () => { + render(( + + + + + + + ), scratch); + + expect(scratch.innerHTML).to.equal([ + div('maybe'), + p('child') + ].join('')); + expect(maybe.base).to.equalNode(scratch.firstChild, 'initial - maybe.base'); + expect(child.base).to.equalNode(scratch.lastChild, 'initial - child.base'); + expect(parent2.base).to.equalNode(child.base, 'initial - parent2.base'); + expect(parent1.base).to.equalNode(maybe.base, 'initial - parent1.base'); + + toggleMaybeNull(); + rerender(); + + expect(scratch.innerHTML).to.equal([ + p('child') + ].join('')); + expect(maybe.base).to.equalNode(null, 'toggleMaybe - maybe.base'); + expect(child.base).to.equalNode(scratch.firstChild, 'toggleMaybe - child.base'); + expect(parent2.base).to.equalNode(child.base, 'toggleMaybe - parent2.base'); + expect(parent1.base).to.equalNode(child.base, 'toggleMaybe - parent1.base'); + + swapChildTag(); + rerender(); + + expect(scratch.innerHTML).to.equal([ + span('child') + ].join('')); + expect(maybe.base).to.equalNode(null, 'swapChildTag - maybe.base'); + expect(child.base).to.equalNode(scratch.firstChild, 'swapChildTag - child.base'); + expect(parent2.base).to.equalNode(child.base, 'swapChildTag - parent2.base'); + expect(parent1.base).to.equalNode(child.base, 'swapChildTag - parent1.base'); + }); + + it('should update parent c.base if first child becomes non-null', () => { + render(( + + + + + + + ), scratch); + + expect(scratch.innerHTML).to.equal([ + p('child') + ].join('')); + expect(maybe.base).to.equalNode(null, 'initial - maybe.base'); + expect(child.base).to.equalNode(scratch.firstChild, 'initial - child.base'); + expect(parent2.base).to.equalNode(child.base, 'initial - parent2.base'); + expect(parent1.base).to.equalNode(child.base, 'initial - parent1.base'); + + swapChildTag(); + rerender(); + + expect(scratch.innerHTML).to.equal([ + span('child') + ].join('')); + expect(maybe.base).to.equalNode(null, 'swapChildTag - maybe.base'); + expect(child.base).to.equalNode(scratch.firstChild, 'swapChildTag - child.base'); + expect(parent2.base).to.equalNode(child.base, 'swapChildTag - parent2.base'); + expect(parent1.base).to.equalNode(child.base, 'swapChildTag - parent1.base'); + + toggleMaybeNull(); + rerender(); + + expect(scratch.innerHTML).to.equal([ + div('maybe'), + span('child') + ].join('')); + expect(maybe.base).to.equalNode(scratch.firstChild, 'toggleMaybe - maybe.base'); + expect(child.base).to.equalNode(scratch.lastChild, 'toggleMaybe - child.base'); + expect(parent2.base).to.equalNode(child.base, 'toggleMaybe - parent2.base'); + expect(parent1.base).to.equalNode(maybe.base, 'toggleMaybe - parent1.base'); + }); + + it('should update parent c.base if first non-null child becomes null with multiple null siblings', () => { + render(( + + + + + + + + + ), scratch); + + expect(scratch.innerHTML).to.equal([ + div('maybe'), + p('child') + ].join('')); + expect(maybe.base).to.equalNode(scratch.firstChild, 'initial - maybe.base'); + expect(child.base).to.equalNode(scratch.lastChild, 'initial - child.base'); + expect(parent2.base).to.equalNode(maybe.base, 'initial - parent2.base'); + expect(parent1.base).to.equalNode(maybe.base, 'initial - parent1.base'); + + toggleMaybeNull(); + rerender(); + + expect(scratch.innerHTML).to.equal([ + p('child') + ].join('')); + expect(maybe.base).to.equalNode(null, 'toggleMaybe - maybe.base'); + expect(child.base).to.equalNode(scratch.firstChild, 'toggleMaybe - child.base'); + expect(parent2.base).to.equalNode(child.base, 'toggleMaybe - parent2.base'); + expect(parent1.base).to.equalNode(child.base, 'toggleMaybe - parent1.base'); + + swapChildTag(); + rerender(); + + expect(scratch.innerHTML).to.equal([ + span('child') + ].join('')); + expect(maybe.base).to.equalNode(null, 'swapChildTag - maybe.base'); + expect(child.base).to.equalNode(scratch.firstChild, 'swapChildTag - child.base'); + expect(parent2.base).to.equalNode(child.base, 'swapChildTag - parent2.base'); + expect(parent1.base).to.equalNode(child.base, 'swapChildTag - parent1.base'); + }); + + it('should update parent c.base if a null child returns DOM with multiple null siblings', () => { + render(( + + + + + + + + + ), scratch); + + expect(scratch.innerHTML).to.equal([ + p('child') + ].join('')); + expect(maybe.base).to.equalNode(null, 'initial - maybe.base'); + expect(child.base).to.equalNode(scratch.firstChild, 'initial - child.base'); + expect(parent2.base).to.equalNode(child.base, 'initial - parent2.base'); + expect(parent1.base).to.equalNode(child.base, 'initial - parent1.base'); + + swapChildTag(); + rerender(); + + expect(scratch.innerHTML).to.equal([ + span('child') + ].join('')); + expect(maybe.base).to.equalNode(null, 'swapChildTag - maybe.base'); + expect(child.base).to.equalNode(scratch.firstChild, 'swapChildTag - child.base'); + expect(parent2.base).to.equalNode(child.base, 'swapChildTag - parent2.base'); + expect(parent1.base).to.equalNode(child.base, 'swapChildTag - parent1.base'); + + toggleMaybeNull(); + rerender(); + + expect(scratch.innerHTML).to.equal([ + div('maybe'), + span('child') + ].join('')); + expect(maybe.base).to.equalNode(scratch.firstChild, 'toggleMaybe - maybe.base'); + expect(child.base).to.equalNode(scratch.lastChild, 'toggleMaybe - child.base'); + expect(parent2.base).to.equalNode(maybe.base, 'toggleMaybe - parent2.base'); + expect(parent1.base).to.equalNode(maybe.base, 'toggleMaybe - parent1.base'); + }); + + it('should update parent c.base to null if last child becomes null', () => { + let fragRef = {}; + render(( + + + + + + + + + + + + ), scratch); + + expect(scratch.innerHTML).to.equal([ + div('maybe'), + p('child') + ].join('')); + expect(maybe.base).to.equalNode(scratch.firstChild, 'initial - maybe.base'); + expect(child.base).to.equalNode(scratch.lastChild, 'initial - child.base'); + expect(parent2.base).to.equalNode(maybe.base, 'initial - parent2.base'); + expect(parent1.base).to.equalNode(maybe.base, 'initial - parent1.base'); + expect(fragRef.current.base).to.equalNode(maybe.base, 'initial - fragRef.current.base'); + + toggleMaybeNull(); + rerender(); + + expect(scratch.innerHTML).to.equal([ + p('child') + ].join('')); + expect(maybe.base).to.equalNode(null, 'toggleMaybe - maybe.base'); + expect(child.base).to.equalNode(scratch.firstChild, 'toggleMaybe - child.base'); + expect(parent2.base).to.equalNode(maybe.base, 'toggleMaybe - parent2.base'); + expect(parent1.base).to.equalNode(maybe.base, 'toggleMaybe - parent1.base'); + expect(fragRef.current.base).to.equalNode(child.base, 'toggleMaybe - fragRef.current.base'); + }); + + it('should update parent c.base if last child returns dom', () => { + let fragRef = {}; + render(( + + + + + + + + + + + + ), scratch); + + expect(scratch.innerHTML).to.equal([ + p('child') + ].join('')); + expect(maybe.base).to.equalNode(null, 'initial - maybe.base'); + expect(child.base).to.equalNode(scratch.firstChild, 'initial - child.base'); + expect(parent2.base).to.equalNode(maybe.base, 'initial - parent2.base'); + expect(parent1.base).to.equalNode(maybe.base, 'initial - parent1.base'); + expect(fragRef.current.base).to.equalNode(child.base, 'initial - fragRef.current.base'); + + toggleMaybeNull(); + rerender(); + + expect(scratch.innerHTML).to.equal([ + div('maybe'), + p('child') + ].join('')); + expect(maybe.base).to.equalNode(scratch.firstChild, 'toggleMaybe - maybe.base'); + expect(child.base).to.equalNode(scratch.lastChild, 'toggleMaybe - child.base'); + expect(parent2.base).to.equalNode(maybe.base, 'initial - parent2.base'); + expect(parent1.base).to.equalNode(maybe.base, 'toggleMaybe - parent1.base'); + expect(fragRef.current.base).to.equalNode(maybe.base, 'toggleMaybe - fragRef.current.base'); + }); + + it('should not update parent if it is a DOM node', () => { + let divVNode =
; + render(divVNode, scratch); + + expect(scratch.innerHTML).to.equal('

child

'); + expect(divVNode._dom).to.equalNode(scratch.firstChild, 'initial - divVNode._dom'); + expect(child.base).to.equalNode(scratch.firstChild.firstChild, 'initial - child.base'); + + swapChildTag(); + rerender(); + + expect(scratch.innerHTML).to.equal('
child
'); + expect(divVNode._dom).to.equalNode(scratch.firstChild, 'swapChildTag - divVNode._dom'); + expect(child.base).to.equalNode(scratch.firstChild.firstChild, 'swapChildTag - child.base'); + }); + }); }); diff --git a/test/browser/fragments.test.js b/test/browser/fragments.test.js index b87997879e..333118973d 100644 --- a/test/browser/fragments.test.js +++ b/test/browser/fragments.test.js @@ -1295,7 +1295,7 @@ describe('Fragment', () => { expectDomLogToBe([ '
    012345.insertBefore(
  1. 4,
  2. 0)', '
      401235.insertBefore(
    1. 5,
    2. 0)', - // TODO: see issue #193 - Hmmm why does this extra append happen? + // TODO: Hmmm why does this extra append happen? '
        453012.appendChild(
      1. 3)' ]); @@ -2575,7 +2575,6 @@ describe('Fragment', () => { div('B') ].join(''), 'updateB'); expectDomLogToBe([ - // TODO: Extra appends happen here in actual. Why? '
        .appendChild(#text)', '
        A3A4.appendChild(
        B)' ]); diff --git a/test/polyfills.js b/test/polyfills.js index a61318c6df..f1e990d638 100644 --- a/test/polyfills.js +++ b/test/polyfills.js @@ -28,17 +28,23 @@ chai.use((chai, util) => { Assertion.addMethod('equalNode', function (expectedNode, message) { const obj = this._obj; + message = message || 'equalNode'; if (expectedNode == null) { - new Assertion(obj).to.equal(expectedNode); + this.assert( + obj == null, + `${message}: expected node to "== null" but got #{act} instead.`, + `${message}: expected node to not "!= null".`, + expectedNode, + obj + ); } else { - new Assertion(obj).to.be.instanceof(Node); - // new Assertion(obj).to.have.property('tagName', expectedNode.tagName); + new Assertion(obj).to.be.instanceof(Node, message); this.assert( obj.tagName === expectedNode.tagName, `${message}: expected node to have tagName #{exp} but got #{act} instead.`, - `${message}: expected node to not have tagName #{act} instead.`, + `${message}: expected node to not have tagName #{act}.`, expectedNode.tagName, obj.tagName );