diff --git a/jsx-runtime/src/index.js b/jsx-runtime/src/index.js index b578b9da7e..40bfe1b395 100644 --- a/jsx-runtime/src/index.js +++ b/jsx-runtime/src/index.js @@ -53,6 +53,8 @@ function createVNode(type, props, key, isStaticChildren, __source, __self) { _hydrating: null, constructor: undefined, _original: --vnodeId, + _flags: 0, + _index: -1, __source, __self }; diff --git a/mangle.json b/mangle.json index 22b96e72bf..4747c8a6f9 100644 --- a/mangle.json +++ b/mangle.json @@ -54,6 +54,10 @@ "$_dom": "__e", "$_hydrating": "__h", "$_component": "__c", + "$_flags": "__u", + "$_insert": "__r", + "$_matched": "__s", + "$_index": "__i", "$__html": "__html", "$_parent": "__", "$_pendingError": "__E", diff --git a/src/component.js b/src/component.js index 88817c2d10..2270391120 100644 --- a/src/component.js +++ b/src/component.js @@ -103,7 +103,7 @@ export function getDomSibling(vnode, childIndex) { // Since updateParentDomPointers keeps _dom pointer correct, // we can rely on _dom to tell us if this subtree contains a // rendered DOM node, and what the first rendered DOM node is - return sibling._nextDom || sibling._dom; + return sibling._dom; } } diff --git a/src/constants.js b/src/constants.js index c30ffc381f..fcc53a3115 100644 --- a/src/constants.js +++ b/src/constants.js @@ -1,4 +1,14 @@ +/** Indicates that this node needs to be inserted while patching children */ +export const INSERT_VNODE = 1 << 16; +export const MATCHED = 1 << 17; + +/** Reset all mode flags */ +export const RESET_MODE = ~(INSERT_VNODE | MATCHED); + export const EMPTY_OBJ = {}; +export const EMPTY_VNODE = /** @type {import('./internal').VNode} */ ( + EMPTY_OBJ +); export const EMPTY_ARR = []; export const IS_NON_DIMENSIONAL = /acit|ex(?:s|g|n|p|$)|rph|grid|ows|mnc|ntw|ine[ch]|zoo|^ord|itera/i; diff --git a/src/create-element.js b/src/create-element.js index b55be840ad..4255cadf6d 100644 --- a/src/create-element.js +++ b/src/create-element.js @@ -72,7 +72,11 @@ export function createVNode(type, props, key, ref, original) { _component: null, _hydrating: null, constructor: undefined, - _original: original == null ? ++vnodeId : original + _original: original == null ? ++vnodeId : original, + // TODO: Consolidate new VNode fields and add mangle.json entries for new + // ones that stay + _flags: 0, + _index: -1 }; // Only invoke the vnode hook if this was *not* a direct copy: diff --git a/src/diff/children.js b/src/diff/children.js index c39e54dc2a..21a1a78a11 100644 --- a/src/diff/children.js +++ b/src/diff/children.js @@ -1,9 +1,18 @@ import { diff, unmount, applyRef } from './index'; import { createVNode, Fragment } from '../create-element'; -import { EMPTY_OBJ, EMPTY_ARR } from '../constants'; +import { + EMPTY_OBJ, + EMPTY_ARR, + EMPTY_VNODE, + MATCHED, + INSERT_VNODE, + RESET_MODE +} from '../constants'; import { isArray } from '../util'; import { getDomSibling } from '../component'; +export const diffChildren = diffChildren2; + /** * Diff the children of a virtual node * @param {import('../internal').PreactElement} parentDom The DOM element whose @@ -25,7 +34,8 @@ import { getDomSibling } from '../component'; * @param {boolean} isHydrating Whether or not we are in hydration * @param {Array} refQueue an array of elements needed to invoke refs */ -export function diffChildren( +// eslint-disable-next-line no-unused-vars +function diffChildren1( parentDom, renderResult, newParentVNode, @@ -69,6 +79,8 @@ export function diffChildren( // or we are rendering a component (e.g. setState) copy the oldVNodes so it can have // it's own DOM & etc. pointers else if ( + // TODO: To make TS happy, I've added this check, but it's not necessary. Remove + typeof childVNode == 'string' || childVNode.constructor == String || typeof childVNode == 'number' || // eslint-disable-next-line valid-typeof @@ -257,6 +269,361 @@ export function diffChildren( } } +/** + * Diff the children of a virtual node + * @param {import('../internal').PreactElement} parentDom The DOM element whose + * children are being diffed + * @param {import('../internal').ComponentChildren[]} renderResult + * @param {import('../internal').VNode} newParentVNode The new virtual + * node whose children should be diff'ed against oldParentVNode + * @param {import('../internal').VNode} oldParentVNode The old virtual + * node whose children should be diff'ed against newParentVNode + * @param {object} globalContext The current context object - modified by getChildContext + * @param {boolean} isSvg Whether or not this DOM node is an SVG node + * @param {Array} excessDomChildren + * @param {Array} commitQueue List of components + * which have callbacks to invoke in commitRoot + * @param {import('../internal').PreactElement} oldDom The current attached DOM + * element any new dom elements should be placed around. Likely `null` on first + * render (except when hydrating). Can be a sibling DOM element when diffing + * Fragments that have siblings. In most cases, it starts out as `oldChildren[0]._dom`. + * @param {boolean} isHydrating Whether or not we are in hydration + * @param {Array} refQueue an array of elements needed to invoke refs + */ +// eslint-disable-next-line no-unused-vars +function diffChildren2( + parentDom, + renderResult, + newParentVNode, + oldParentVNode, + globalContext, + isSvg, + excessDomChildren, + commitQueue, + oldDom, + isHydrating, + refQueue +) { + let i, + /** @type {import('../internal').VNode} */ + oldVNode, + /** @type {import('../internal').VNode} */ + childVNode, + newDom, + firstChildDom; + + // This is a compression of oldParentVNode!=null && oldParentVNode != EMPTY_VNODE && oldParentVNode._children || EMPTY_ARR + // as EMPTY_VNODE._children should be `undefined`. + let oldChildren = (oldParentVNode && oldParentVNode._children) || EMPTY_ARR; + + let oldChildrenLength = oldChildren.length, + newChildrenLength = renderResult.length; + + // TODO: Is there a better way to track oldDom then a ref? Since + // constructNewChildrenArray can unmount DOM nodes while looping (to handle + // null placeholders, i.e. VNode => null in unkeyed children), we need to adjust + // oldDom in that method. + const oldDomRef = { _current: oldDom }; + const newChildren = (newParentVNode._children = constructNewChildrenArray( + newParentVNode, + renderResult, + oldChildren, + oldDomRef + )); + + oldDom = oldDomRef._current; + + // Remove remaining oldChildren if there are any. Loop forwards so that as we + // unmount DOM from the beginning of the oldChildren, we can adjust oldDom to + // point to the next child, which needs to be the first DOM node that won't be + // unmounted. + for (i = 0; i < oldChildrenLength; i++) { + oldVNode = oldChildren[i]; + if (oldVNode != null && (oldVNode._flags & MATCHED) === 0) { + if (oldDom == oldVNode._dom) { + oldDom = getDomSibling(oldVNode); + } + + unmount(oldVNode, oldVNode); + } + } + + for (i = 0; i < newChildrenLength; i++) { + childVNode = newChildren[i]; + + if ( + childVNode == null || + typeof childVNode == 'boolean' || + typeof childVNode == 'function' + ) { + continue; + } + + // At this point, constructNewChildrenArray has assigned _index to be the + // matchingIndex for this VNode's oldVNode (or -1 if there is no oldVNode). + if (childVNode._index === -1) { + oldVNode = EMPTY_VNODE; + } else { + oldVNode = oldChildren[childVNode._index] || EMPTY_VNODE; + } + + // Update childVNode._index to its final index + childVNode._index = i; + + diff( + parentDom, + childVNode, + oldVNode, + globalContext, + isSvg, + excessDomChildren, + commitQueue, + oldDom, + isHydrating, + refQueue + ); + + // Adjust DOM nodes + newDom = childVNode._dom; + if (childVNode.ref && oldVNode.ref != childVNode.ref) { + if (oldVNode.ref) { + applyRef(oldVNode.ref, null, childVNode); + } + refQueue.push( + childVNode.ref, + childVNode._component || newDom, + childVNode + ); + } + + if (newDom != null) { + if (firstChildDom == null) { + firstChildDom = newDom; + } + + // TODO: Golf this `if` block + if ( + childVNode._flags & INSERT_VNODE || + (typeof childVNode.type == 'function' && + childVNode._children === oldVNode._children) + ) { + if (typeof childVNode.type == 'function') { + oldDom = reorderChildren(childVNode, oldDom, parentDom); + } else { + oldDom = placeChild(parentDom, newDom, oldDom); + } + } else if (childVNode._nextDom !== undefined) { + // Only Fragments or components that return Fragment like VNodes will + // have a non-undefined _nextDom. Continue the diff from the sibling + // of last DOM child of this child VNode + oldDom = childVNode._nextDom; + + // Eagerly cleanup _nextDom. 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. Once we store it the nextDOM local var, we + // can clean up the property + // TODO: Should we always set this to undefined to ensure it is cleaned up? + childVNode._nextDom = undefined; + } else { + oldDom = newDom.nextSibling; + } + + // TODO: With new child diffing algo, consider alt ways to diff Fragments. + // Such as dropping oldDom and moving fragments in place + if (typeof newParentVNode.type == 'function') { + // Because the newParentVNode is Fragment-like, we need to set it's + // _nextDom property to the nextSibling of its last child DOM node. + // + // `oldDom` contains the correct value here because if the last child + // is a Fragment-like, then oldDom has already been set to that child's _nextDom. + // If the last child is a DOM VNode, then oldDom will be set to that DOM + // node's nextSibling. + newParentVNode._nextDom = oldDom; + } + } + + // Unset diffing properties + childVNode._flags &= RESET_MODE; + } + + newParentVNode._dom = firstChildDom; +} + +/** + * @param {import('../internal').VNode} newParentVNode + * @param {import('../internal').ComponentChildren[]} renderResult + * @param {import('../internal').VNode[]} oldChildren + * @param {{ _current: import('../internal').PreactElement }} oldDomRef + * @returns {import('../internal').VNode[]} + */ +function constructNewChildrenArray( + newParentVNode, + renderResult, + oldChildren, + oldDomRef +) { + /** @type {number} */ + let i; + /** @type {import('../internal').VNode} */ + let childVNode; + let newChildrenLength = renderResult.length; + let remainingOldChildren = oldChildren.length; + let skew = 0; + + const newChildren = []; + for (i = 0; i < renderResult.length; i++) { + childVNode = /** @type {import('../internal').VNode} */ (renderResult[i]); + + // Convert render results to VNodes (e.g. strings => VNodes, etc.) + if ( + childVNode == null || + typeof childVNode == 'boolean' || + typeof childVNode == 'function' + ) { + childVNode = newChildren[i] = null; + } + // If this newVNode is being reused (e.g.
{reuse}{reuse}
) in the same diff, + // or we are rendering a component (e.g. setState) copy the oldVNodes so it can have + // it's own DOM & etc. pointers + else if ( + // TODO: To make TS happy, I've added this check, but it's not necessary. Remove + typeof childVNode == 'string' || + childVNode.constructor == String || + typeof childVNode == 'number' || + // eslint-disable-next-line valid-typeof + typeof childVNode == 'bigint' + ) { + childVNode = newChildren[i] = createVNode( + null, + childVNode, + null, + null, + childVNode + ); + } else if (isArray(childVNode)) { + childVNode = newChildren[i] = createVNode( + Fragment, + { children: childVNode }, + null, + null, + null + ); + } else if (childVNode._depth > 0) { + // VNode is already in use, clone it. This can happen in the following + // scenario: + // const reuse =
+ //
{reuse}{reuse}
+ childVNode = newChildren[i] = createVNode( + childVNode.type, + childVNode.props, + childVNode.key, + childVNode.ref ? childVNode.ref : null, + childVNode._original + ); + } else { + childVNode = newChildren[i] = childVNode; + } + + // Handle unmounting null placeholders, i.e. VNode => null in unkeyed children + if (childVNode == null) { + /** @type {import('../internal').VNode} */ + let oldVNode = oldChildren[i]; + if (oldVNode && oldVNode.key == null && oldVNode._dom) { + if (oldVNode._dom == oldDomRef._current) { + oldDomRef._current = getDomSibling(oldVNode); + } + + unmount(oldVNode, oldVNode, false); + + // Explicitly nullify this position in oldChildren instead of just + // setting `_match=true` to prevent other routines (e.g. + // `findMatchingIndex` or `getDomSibling`) from thinking VNodes or DOM + // nodes in this position are still available to be used in diffing when + // they have actually already been unmounted. For example, by only + // setting `_match=true` here, the unmounting loop later would attempt + // to unmount this VNode again seeing `_match==true`. Further, + // getDomSibling doesn't know about _match and so would incorrectly + // assume DOM nodes in this subtree are mounted and usable. + oldChildren[i] = null; + remainingOldChildren--; + } + + continue; + } + + childVNode._parent = newParentVNode; + childVNode._depth = newParentVNode._depth + 1; + + let skewedIndex = i + skew; + const matchingIndex = findMatchingIndex( + childVNode, + oldChildren, + skewedIndex, + remainingOldChildren + ); + + // Temporarily store the matchingIndex on the _index property so we can pull + // out the oldVNode in diffChildren. We'll override this to the VNode's + // final index after using this property to get the oldVNode + childVNode._index = matchingIndex; + + if (matchingIndex !== -1) { + remainingOldChildren--; + if (oldChildren[matchingIndex]) { + // TODO: Can we somehow not use this property? or override another + // property? We need it now so we can pull off the matchingIndex in + // diffChildren and when unmounting we can get the next DOM element by + // calling getDomSibling, which needs a complete oldTree + oldChildren[matchingIndex]._flags |= MATCHED; + } + } + + // // Here, we define isMounting for the purposes of the skew diffing + // // algorithm. Nodes that are unsuspending are considered mounting and we detect + // // this by checking if oldVNode._original === null + let isMounting = + matchingIndex === -1 || + oldChildren[matchingIndex] == null || + oldChildren[matchingIndex]._original === null; + + if (isMounting) { + if (matchingIndex == -1) { + skew--; + } + } else if (matchingIndex !== skewedIndex) { + if (matchingIndex === skewedIndex + 1) { + skew++; + } else if (matchingIndex > skewedIndex) { + if (remainingOldChildren > newChildrenLength - skewedIndex) { + skew += matchingIndex - skewedIndex; + } else { + // ### Change from keyed: I think this was missing from the algo... + skew--; + } + } else if (matchingIndex < skewedIndex) { + if (matchingIndex == skewedIndex - 1) { + skew = matchingIndex - skewedIndex; + } else { + skew = 0; + } + } else { + skew = 0; + } + } + + // Move this VNode's DOM if the original index (matchingIndex) doesn't match + // the new skew index (i + skew) or it's a mounting component VNode + if ( + matchingIndex !== i + skew || + (typeof childVNode.type != 'function' && isMounting) + ) { + childVNode._flags |= INSERT_VNODE; + } + } + + return newChildren; +} + function reorderChildren(childVNode, oldDom, parentDom) { // Note: VNodes in nested suspended trees may be missing _children. let c = childVNode._children; @@ -329,17 +696,33 @@ function findMatchingIndex( let x = skewedIndex - 1; let y = skewedIndex + 1; let oldVNode = oldChildren[skewedIndex]; + // We only need to perform a search if there are more children + // (remainingOldChildren) to search. However, if the oldVNode we just looked + // at skewedIndex was not already used in this diff, then there must be at + // least 1 other (so greater than 1) remainingOldChildren to attempt to match + // against. So the following condition checks that ensuring + // remainingOldChildren > 1 if the oldVNode is not already used/matched. Else + // if the oldVNode was null or matched, then there could needs to be at least + // 1 (aka `remainingOldChildren > 0`) children to find and compare against. + let shouldSearch = + remainingOldChildren > + (oldVNode != null && (oldVNode._flags & MATCHED) === 0 ? 1 : 0); if ( oldVNode === null || (oldVNode && key == oldVNode.key && type === oldVNode.type) ) { return skewedIndex; - } else if (remainingOldChildren > (oldVNode != null ? 1 : 0)) { + } else if (shouldSearch) { while (x >= 0 || y < oldChildren.length) { if (x >= 0) { oldVNode = oldChildren[x]; - if (oldVNode && key == oldVNode.key && type === oldVNode.type) { + if ( + oldVNode && + (oldVNode._flags & MATCHED) === 0 && + key == oldVNode.key && + type === oldVNode.type + ) { return x; } x--; @@ -347,7 +730,12 @@ function findMatchingIndex( if (y < oldChildren.length) { oldVNode = oldChildren[y]; - if (oldVNode && key == oldVNode.key && type === oldVNode.type) { + if ( + oldVNode && + (oldVNode._flags & MATCHED) === 0 && + key == oldVNode.key && + type === oldVNode.type + ) { return y; } y++; diff --git a/src/internal.d.ts b/src/internal.d.ts index 57faa0ded0..7ebf580c74 100644 --- a/src/internal.d.ts +++ b/src/internal.d.ts @@ -122,6 +122,9 @@ export interface VNode

extends preact.VNode

{ _hydrating: boolean | null; constructor: undefined; _original: number; + // New properties + _flags: number; + _index: number; } export interface Component

extends preact.Component { diff --git a/test/browser/createContext.test.js b/test/browser/createContext.test.js index f4610ed372..b27f20b625 100644 --- a/test/browser/createContext.test.js +++ b/test/browser/createContext.test.js @@ -877,8 +877,8 @@ describe('createContext', () => { expect(events).to.deep.equal([ 'render 0', 'mount 0', - 'render 1', 'unmount 0', + 'render 1', 'mount 1' ]); }); diff --git a/test/browser/fragments.test.js b/test/browser/fragments.test.js index f04a80d3a7..20fed865e4 100644 --- a/test/browser/fragments.test.js +++ b/test/browser/fragments.test.js @@ -235,9 +235,9 @@ describe('Fragment', () => { expect(scratch.innerHTML).to.equal(div([div(1), span(2), span(2)])); expectDomLogToBe([ + '1.remove()', '

.appendChild(#text)', - '
122.insertBefore(
1, 1)', - '1.remove()' + '
22.insertBefore(
1, 2)' ]); }); @@ -357,9 +357,9 @@ describe('Fragment', () => { expect(ops).to.deep.equal([]); expect(scratch.innerHTML).to.equal('
Hello
'); expectDomLogToBe([ + '
Hello.remove()', '
.appendChild(#text)', - '
Hello.insertBefore(
Hello,
Hello)', - '
Hello.remove()' + '
.appendChild(
Hello)' ]); clearLog(); @@ -368,10 +368,10 @@ describe('Fragment', () => { expect(ops).to.deep.equal([]); expect(scratch.innerHTML).to.equal('
Hello
'); expectDomLogToBe([ + '
Hello.remove()', '
.appendChild(#text)', // Re-append the Stateful DOM since it has been re-parented - '
Hello.insertBefore(
Hello,
Hello)', - '
Hello.remove()' + '
.appendChild(
Hello)' ]); }); @@ -396,9 +396,9 @@ describe('Fragment', () => { expect(ops).to.deep.equal([]); expect(scratch.innerHTML).to.equal('
Hello
'); expectDomLogToBe([ + '
Hello.remove()', '
.appendChild(#text)', - '
Hello.insertBefore(
Hello,
Hello)', - '
Hello.remove()' + '
.appendChild(
Hello)' ]); clearLog(); @@ -407,9 +407,9 @@ describe('Fragment', () => { expect(ops).to.deep.equal([]); expect(scratch.innerHTML).to.equal('
Hello
'); expectDomLogToBe([ + '
Hello.remove()', '
.appendChild(#text)', - '
Hello.insertBefore(
Hello,
Hello)', - '
Hello.remove()' + '
.appendChild(
Hello)' ]); }); @@ -843,12 +843,12 @@ describe('Fragment', () => { expect(ops).to.deep.equal([]); // Component should not have updated (empty op log) expect(scratch.innerHTML).to.equal(html); expectDomLogToBe([ + '1.remove()', + '
Hello.remove()', '.appendChild(#text)', - '
1Hello2.insertBefore(1, 1)', + '
2.insertBefore(1, 2)', '
.appendChild(#text)', - '
11Hello2.insertBefore(
Hello, 1)', - '1.remove()', - '
Hello.remove()' + '
12.insertBefore(
Hello, 2)' ]); clearLog(); @@ -857,12 +857,12 @@ describe('Fragment', () => { expect(ops).to.deep.equal([]); // Component should not have updated (empty op log) expect(scratch.innerHTML).to.equal(html); expectDomLogToBe([ + '1.remove()', + '
Hello.remove()', '.appendChild(#text)', - '
1Hello2.insertBefore(1, 1)', + '
2.insertBefore(1, 2)', '
.appendChild(#text)', - '
11Hello2.insertBefore(
Hello, 1)', - '1.remove()', - '
Hello.remove()' + '
12.insertBefore(
Hello, 2)' ]); }); @@ -925,9 +925,9 @@ describe('Fragment', () => { expect(scratch.innerHTML).to.equal('foobar'); expectDomLogToBe([ - '
spamfoobar.insertBefore(#text, #text)', '#text.remove()', - '#text.remove()' + '#text.remove()', + '
foo.appendChild(#text)' ]); }); @@ -1449,14 +1449,14 @@ describe('Fragment', () => { 'rendering from false to true' ); expectDomLogToBe([ + // Remove 3 & 4 (replaced by null) + '
  • 3.remove()', + '
  • 4.remove()', // Insert 0 and 1 '
  • .appendChild(#text)', - '
      2234.insertBefore(
    1. 0,
    2. 2)', + '
        22.insertBefore(
      1. 0,
      2. 2)', '
      3. .appendChild(#text)', - '
          02234.insertBefore(
        1. 1,
        2. 2)', - // Remove 3 & 4 (replaced by null) - '
        3. 3.remove()', - '
        4. 4.remove()' + '
            022.insertBefore(
          1. 1,
          2. 2)' ]); }); @@ -1520,14 +1520,14 @@ describe('Fragment', () => { 'rendering from false to true' ); expectDomLogToBe([ + // Remove 4 & 5 (replaced by null) + '
          3. 4.remove()', + '
          4. 5.remove()', // Insert 0 and 1 back into the DOM '
          5. .appendChild(#text)', - '
              2345.insertBefore(
            1. 0,
            2. 2)', + '
                23.insertBefore(
              1. 0,
              2. 2)', '
              3. .appendChild(#text)', - '
                  02345.insertBefore(
                1. 1,
                2. 2)', - // Remove 4 & 5 (replaced by null) - '
                3. 4.remove()', - '
                4. 5.remove()' + '
                    023.insertBefore(
                  1. 1,
                  2. 2)' ]); }); @@ -1581,10 +1581,10 @@ describe('Fragment', () => { ); expectDomLogToBe( [ - '
                    barHellobeepboop.insertBefore(
                    bar,
                    beep)', - '
                    Hellobarbeepboop.insertBefore(
                    Hello,
                    boop)', - '
                    barbeepHelloboop.insertBefore(
                    bar,
                    boop)', - '
                    boop.remove()' + '
                    boop.remove()', + '
                    barHellobeep.insertBefore(
                    bar,
                    beep)', + '
                    Hellobarbeep.appendChild(
                    Hello)', + '
                    barbeepHello.appendChild(
                    bar)' ], 'rendering from true to false' ); @@ -1776,12 +1776,12 @@ describe('Fragment', () => { expect(scratch.innerHTML).to.equal(htmlForFalse); expectDomLogToBe( [ + '
                    2.remove()', + '#text.remove()', '
                    .appendChild(#text)', - '
                    1.insertBefore(
                    3, #text)', + '
                    .appendChild(
                    3)', '
                    .appendChild(#text)', - '
                    31.insertBefore(
                    4, #text)', - '#text.remove()', - '
                    2.remove()' + '
                    3.appendChild(
                    4)' ], 'rendering from true to false' ); @@ -1792,9 +1792,9 @@ describe('Fragment', () => { expect(scratch.innerHTML).to.equal(htmlForTrue); expectDomLogToBe( [ - '
                    34.insertBefore(#text,
                    3)', - '
                    4.remove()', '
                    3.remove()', + '
                    4.remove()', + '
                    .appendChild(#text)', '
                    .appendChild(#text)', '
                    1.appendChild(
                    2)' ], @@ -2056,9 +2056,9 @@ describe('Fragment', () => { `
                    A
                    B2
                    C
                    ` ); expectDomLogToBe([ + '
                    B1.remove()', '
                    .appendChild(#text)', - '
                    AB1C.insertBefore(
                    B2,
                    B1)', - '
                    B1.remove()' + '
                    AC.insertBefore(
                    B2,
                    C)' ]); }); @@ -2106,12 +2106,12 @@ describe('Fragment', () => { div([div('A'), section('B3'), section('B4'), div('C')]) ); expectDomLogToBe([ + '
                    B1.remove()', + '
                    B2.remove()', '
                    .appendChild(#text)', - '
                    AB1B2C.insertBefore(
                    B3,
                    B1)', + '
                    AC.insertBefore(
                    B3,
                    C)', '
                    .appendChild(#text)', - '
                    AB3B1B2C.insertBefore(
                    B4,
                    B1)', - '
                    B2.remove()', - '
                    B1.remove()' + '
                    AB3C.insertBefore(
                    B4,
                    C)' ]); }); @@ -2424,9 +2424,9 @@ describe('Fragment', () => { expect(scratch.innerHTML).to.eql(`
                    A2
                    C
                    `); expectDomLogToBe([ + '
                    A.remove()', '.appendChild(#text)', - '
                    AC.insertBefore(A2,
                    A)', - '
                    A.remove()' + '
                    C.insertBefore(A2,
                    C)' ]); }); @@ -2491,12 +2491,12 @@ describe('Fragment', () => { `
                    A3A4
                    C
                    ` ); expectDomLogToBe([ + '
                    A1.remove()', + '
                    A2.remove()', '.appendChild(#text)', - '
                    A1A2C.insertBefore(A3,
                    A1)', + '
                    C.insertBefore(A3,
                    C)', '.appendChild(#text)', - '
                    A3A1A2C.insertBefore(A4,
                    A1)', - '
                    A2.remove()', - '
                    A1.remove()' + '
                    A3C.insertBefore(A4,
                    C)' ]); }); @@ -2572,9 +2572,9 @@ describe('Fragment', () => { 'updateA' ); expectDomLogToBe([ + '
                    A.remove()', '.appendChild(#text)', - '
                    ABC.insertBefore(A2,
                    A)', - '
                    A.remove()' + '
                    BC.insertBefore(A2,
                    B)' ]); }); @@ -2629,12 +2629,12 @@ describe('Fragment', () => { 'updateA' ); expectDomLogToBe([ + '
                    A1.remove()', + '
                    A2.remove()', '.appendChild(#text)', - '
                    A1A2.insertBefore(A3,
                    A1)', + '
                    .appendChild(A3)', '.appendChild(#text)', - '
                    A3A1A2.insertBefore(A4,
                    A1)', - '
                    A2.remove()', - '
                    A1.remove()' + '
                    A3.appendChild(A4)' ]); clearLog(); @@ -2652,7 +2652,8 @@ describe('Fragment', () => { }); it('should properly place conditional elements around strictly equal vnodes', () => { - let set; + /** @type {() => void} */ + let toggle; const Children = () => ( @@ -2665,7 +2666,7 @@ describe('Fragment', () => { constructor(props) { super(props); this.state = { panelPosition: 'bottom' }; - set = this.tooglePanelPosition = this.tooglePanelPosition.bind(this); + toggle = this.tooglePanelPosition = this.tooglePanelPosition.bind(this); } tooglePanelPosition() { @@ -2699,17 +2700,17 @@ describe('Fragment', () => { expect(scratch.innerHTML).to.equal(bottom); clearLog(); - set(); + toggle(); rerender(); expect(scratch.innerHTML).to.equal(top); expectDomLogToBe([ + '
                    bottom panel.remove()', '
                    .appendChild(#text)', - '
                    NavigationContentbottom panel.insertBefore(
                    top panel,
                    Navigation)', - '
                    bottom panel.remove()' + '
                    NavigationContent.insertBefore(
                    top panel,
                    Navigation)' ]); clearLog(); - set(); + toggle(); rerender(); expect(scratch.innerHTML).to.equal(bottom); expectDomLogToBe([ @@ -2719,13 +2720,13 @@ describe('Fragment', () => { ]); clearLog(); - set(); + toggle(); rerender(); expect(scratch.innerHTML).to.equal(top); expectDomLogToBe([ + '
                    bottom panel.remove()', '
                    .appendChild(#text)', - '
                    NavigationContentbottom panel.insertBefore(
                    top panel,
                    Navigation)', - '
                    bottom panel.remove()' + '
                    NavigationContent.insertBefore(
                    top panel,
                    Navigation)' ]); }); @@ -2941,10 +2942,10 @@ describe('Fragment', () => { div([div(1), div(4), div('A'), div('B')]) ); expectDomLogToBe([ - '
                    .appendChild(#text)', - '
                    123AB.insertBefore(
                    4,
                    2)', '
                    2.remove()', - '
                    3.remove()' + '
                    3.remove()', + '
                    .appendChild(#text)', + '
                    1AB.insertBefore(
                    4,
                    A)' ]); }); @@ -3039,11 +3040,11 @@ describe('Fragment', () => { expect(scratch.innerHTML).to.equal(div([span(1), div('A'), div('B')])); expectDomLogToBe([ - '.appendChild(#text)', - '
                    123AB.insertBefore(1,
                    1)', + '
                    1.remove()', '
                    2.remove()', '
                    3.remove()', - '
                    1.remove()' + '.appendChild(#text)', + '
                    AB.insertBefore(1,
                    A)' ]); }); diff --git a/test/browser/keys.test.js b/test/browser/keys.test.js index 7bc883418d..6ad8789ca0 100644 --- a/test/browser/keys.test.js +++ b/test/browser/keys.test.js @@ -252,9 +252,9 @@ describe('keys', () => { render(, scratch); expect(scratch.textContent).to.equal('abcd'); expect(getLog()).to.deep.equal([ - '
                  3. z.remove()', + '
                  4. x.remove()', '
                  5. y.remove()', - '
                  6. x.remove()' + '
                  7. z.remove()' ]); }); @@ -462,8 +462,8 @@ describe('keys', () => { expect(scratch.innerHTML).to.equal(expectedHtml); expect(ops).to.deep.equal([ - 'Unmount Stateful2', 'Unmount Stateful1', + 'Unmount Stateful2', 'Mount Stateful1', 'Mount Stateful2' ]); @@ -475,8 +475,8 @@ describe('keys', () => { expect(scratch.innerHTML).to.equal(expectedHtml); expect(ops).to.deep.equal([ - 'Unmount Stateful2', 'Unmount Stateful1', + 'Unmount Stateful2', 'Mount Stateful1', 'Mount Stateful2' ]); @@ -731,8 +731,8 @@ describe('keys', () => { expect(scratch.innerHTML).to.equal(expectedHtml); expect(ops).to.deep.equal([ - 'Unmount Stateful2', 'Unmount Stateful1', + 'Unmount Stateful2', 'Mount Stateful1', 'Mount Stateful2' ]); @@ -744,8 +744,8 @@ describe('keys', () => { expect(scratch.innerHTML).to.equal(expectedHtml); expect(ops).to.deep.equal([ - 'Unmount Stateful2', 'Unmount Stateful1', + 'Unmount Stateful2', 'Mount Stateful1', 'Mount Stateful2' ]); diff --git a/test/browser/render.test.js b/test/browser/render.test.js index 6dab0019ab..a8be04376c 100644 --- a/test/browser/render.test.js +++ b/test/browser/render.test.js @@ -1064,7 +1064,7 @@ describe('render()', () => { it('should not remove iframe', () => { let setState; const Iframe = () => { - return
                  8. ' + '
                    Test3
                    Test2
                    ' ); clearLog(); setState({ value: false }); rerender(); expect(scratch.innerHTML).to.equal( - '
                    ' + '
                    ' ); expect(getLog()).to.deep.equal([ '
                    Test3.remove()',