import { createElement, hydrate, Fragment } from 'preact';
import {
setupScratch,
teardown,
sortAttributes,
serializeHtml,
spyOnElementAttributes,
createEvent
} from '../_util/helpers';
import { ul, li, div } from '../_util/dom';
import { logCall, clearLog, getLog } from '../_util/logCall';
/** @jsx createElement */
describe('hydrate()', () => {
/** @type {HTMLElement} */
let scratch;
let attributesSpy;
const List = ({ children }) =>
;
const ListItem = ({ children, onClick = null }) => (
{children}
);
let resetAppendChild;
let resetInsertBefore;
let resetRemoveChild;
let resetRemove;
let resetSetAttribute;
let resetRemoveAttribute;
before(() => {
resetAppendChild = logCall(Element.prototype, 'appendChild');
resetInsertBefore = logCall(Element.prototype, 'insertBefore');
resetRemoveChild = logCall(Element.prototype, 'removeChild');
resetRemove = logCall(Element.prototype, 'remove');
resetSetAttribute = logCall(Element.prototype, 'setAttribute');
resetRemoveAttribute = logCall(Element.prototype, 'removeAttribute');
});
after(() => {
resetAppendChild();
resetInsertBefore();
resetRemoveChild();
resetRemove();
resetSetAttribute();
resetRemoveAttribute();
});
beforeEach(() => {
scratch = setupScratch();
attributesSpy = spyOnElementAttributes();
});
afterEach(() => {
teardown(scratch);
clearLog();
});
it('should reuse existing DOM', () => {
const onClickSpy = sinon.spy();
const html = ul([li('1'), li('2'), li('3')]);
scratch.innerHTML = html;
clearLog();
hydrate(
,
scratch
);
expect(scratch.innerHTML).to.equal(html);
expect(getLog()).to.deep.equal([]);
expect(onClickSpy).not.to.have.been.called;
scratch.querySelector('li:last-child').dispatchEvent(createEvent('click'));
expect(onClickSpy).to.have.been.called.calledOnce;
});
it('should reuse existing DOM when given components', () => {
const onClickSpy = sinon.spy();
const html = ul([li('1'), li('2'), li('3')]);
scratch.innerHTML = html;
clearLog();
hydrate(
1
2
3
,
scratch
);
expect(scratch.innerHTML).to.equal(html);
expect(getLog()).to.deep.equal([]);
expect(onClickSpy).not.to.have.been.called;
scratch.querySelector('li:last-child').dispatchEvent(createEvent('click'));
expect(onClickSpy).to.have.been.called.calledOnce;
});
it('should properly set event handlers to existing DOM when given components', () => {
const proto = Element.prototype;
sinon.spy(proto, 'addEventListener');
const clickHandlers = [sinon.spy(), sinon.spy(), sinon.spy()];
const html = ul([li('1'), li('2'), li('3')]);
scratch.innerHTML = html;
clearLog();
hydrate(
1
2
3
,
scratch
);
expect(scratch.innerHTML).to.equal(html);
expect(getLog()).to.deep.equal([]);
expect(proto.addEventListener).to.have.been.calledThrice;
expect(clickHandlers[2]).not.to.have.been.called;
scratch.querySelector('li:last-child').dispatchEvent(createEvent('click'));
expect(clickHandlers[2]).to.have.been.calledOnce;
});
it('should add missing nodes to existing DOM when hydrating', () => {
const html = ul([li('1')]);
scratch.innerHTML = html;
clearLog();
hydrate(
1
2
3
,
scratch
);
expect(scratch.innerHTML).to.equal(ul([li('1'), li('2'), li('3')]));
expect(getLog()).to.deep.equal([
'.appendChild(#text)',
'1.appendChild(2)',
' .appendChild(#text)',
'12.appendChild(3)'
]);
});
it('should remove extra nodes from existing DOM when hydrating', () => {
const html = ul([li('1'), li('2'), li('3'), li('4')]);
scratch.innerHTML = html;
clearLog();
hydrate(
1
2
3
,
scratch
);
expect(scratch.innerHTML).to.equal(ul([li('1'), li('2'), li('3')]));
expect(getLog()).to.deep.equal([' 4.remove()']);
});
it('should not update attributes on existing DOM', () => {
scratch.innerHTML =
'Test
';
let vnode = (
Test
);
clearLog();
hydrate(vnode, scratch);
// IE11 doesn't support spying on Element.prototype
if (!/Trident/.test(navigator.userAgent)) {
expect(attributesSpy.get).to.not.have.been.called;
}
expect(serializeHtml(scratch)).to.equal(
sortAttributes(
'Test
'
)
);
expect(getLog()).to.deep.equal([]);
});
it('should update class attribute via className prop', () => {
scratch.innerHTML = 'bar
';
hydrate(bar
, scratch);
expect(scratch.innerHTML).to.equal('bar
');
});
it('should correctly hydrate with Fragments', () => {
const html = ul([li('1'), li('2'), li('3'), li('4')]);
scratch.innerHTML = html;
clearLog();
const clickHandlers = [sinon.spy(), sinon.spy(), sinon.spy(), sinon.spy()];
hydrate(
1
2
3
4
,
scratch
);
expect(scratch.innerHTML).to.equal(html);
expect(getLog()).to.deep.equal([]);
expect(clickHandlers[2]).not.to.have.been.called;
scratch
.querySelector('li:nth-child(3)')
.dispatchEvent(createEvent('click'));
expect(clickHandlers[2]).to.have.been.called.calledOnce;
});
it('should correctly hydrate root Fragments', () => {
const html = [
ul([li('1'), li('2'), li('3'), li('4')]),
div('sibling')
].join('');
scratch.innerHTML = html;
clearLog();
const clickHandlers = [
sinon.spy(),
sinon.spy(),
sinon.spy(),
sinon.spy(),
sinon.spy()
];
hydrate(
1
2
3
4
sibling
,
scratch
);
expect(scratch.innerHTML).to.equal(html);
expect(getLog()).to.deep.equal([]);
expect(clickHandlers[2]).not.to.have.been.called;
scratch
.querySelector('li:nth-child(3)')
.dispatchEvent(createEvent('click'));
expect(clickHandlers[2]).to.have.been.calledOnce;
expect(clickHandlers[4]).not.to.have.been.called;
scratch.querySelector('div').dispatchEvent(createEvent('click'));
expect(clickHandlers[2]).to.have.been.calledOnce;
expect(clickHandlers[4]).to.have.been.calledOnce;
});
// Failing because the following condition in diffElementNodes doesn't evaluate to true
// when hydrating a dom node which is not correct
// dom===d && newVNode.text!==oldVNode.text
// We don't set `d` when hydrating. If we did, then newVNode.text would never equal
// oldVNode.text since oldVNode is always EMPTY_OBJ when hydrating
it.skip('should override incorrect pre-existing DOM with VNodes passed into render', () => {
const initialHtml = [
div('sibling'),
ul([li('1'), li('4'), li('3'), li('2')])
].join('');
scratch.innerHTML = initialHtml;
clearLog();
hydrate(
1
2
3
4
sibling
,
scratch
);
const finalHtml = [
ul([li('1'), li('2'), li('3'), li('4')]),
div('sibling')
].join('');
expect(scratch.innerHTML).to.equal(finalHtml);
// TODO: Fill in with proper log once this test is passing
expect(getLog()).to.deep.equal([]);
});
it('should not merge attributes with node created by the DOM', () => {
const html = htmlString => {
const div = document.createElement('div');
div.innerHTML = htmlString;
return div.firstChild;
};
// prettier-ignore
const DOMElement = html``;
scratch.appendChild(DOMElement);
const preactElement = (
);
hydrate(preactElement, scratch);
// IE11 doesn't support spies on built-in prototypes
if (!/Trident/.test(navigator.userAgent)) {
expect(attributesSpy.get).to.not.have.been.called;
}
expect(scratch).to.have.property(
'innerHTML',
''
);
});
it('should attach event handlers', () => {
let spy = sinon.spy();
scratch.innerHTML = 'Test ';
let vnode = Test ;
hydrate(vnode, scratch);
scratch.firstChild.click();
expect(spy).to.be.calledOnce;
});
// #2237
it('should not redundantly add text nodes', () => {
scratch.innerHTML = '';
const element = document.getElementById('test');
const Component = props => hello {props.foo}
;
hydrate( , element);
expect(element.innerHTML).to.equal('hello bar
');
});
it('should not remove values', () => {
scratch.innerHTML =
'Zero Two ';
const App = () => {
const options = [
{
value: '0',
label: 'Zero'
},
{
value: '2',
label: 'Two'
}
];
return (
{options.map(({ disabled, label, value }) => (
{label}
))}
);
};
hydrate( , scratch);
expect(sortAttributes(scratch.innerHTML)).to.equal(
sortAttributes(
'Zero Two '
)
);
});
it('should deopt for trees introduced in hydrate (append)', () => {
scratch.innerHTML = '';
const Component = props => hello {props.foo}
;
const element = document.getElementById('test');
hydrate(
,
element
);
expect(element.innerHTML).to.equal(
'hello bar
hello baz
'
);
});
it('should deopt for trees introduced in hydrate (insert before)', () => {
scratch.innerHTML = '';
const Component = props => hello {props.foo}
;
const element = document.getElementById('test');
hydrate(
,
element
);
expect(element.innerHTML).to.equal(
'hello baz
hello bar
'
);
});
it('should skip comment nodes', () => {
scratch.innerHTML = 'hello foo
';
hydrate(hello {'foo'}
, scratch);
expect(scratch.innerHTML).to.equal('hello foo
');
});
});