improve rrdom performance (#1127)
* add more check to rrdom to make diff algorithm more robust * fix: selector match in iframe is case-insensitive add try catch to some fragile points * test: increase timeout value for Jest * improve code style * fix: failed to execute insertBefore on Node in the diff function this happens when ids of doctype or html element are changed in the virtual dom also improve the code quality * refactor diff function to make the code cleaner * fix: virtual nodes are passed to plugin's onBuild function * refactor the diff function and adjust the order of diff work. * call afterAppend hook in a consistent traversal order * improve the performance of the "contains" function reduce the complexity from O(n) to O(logn) a specific benchmark is needed to add further * add a real events for benchmark * refactor: change the data structure of childNodes from array to linked list * remove legacy code in rrweb package * update unit tests * update change log
This commit is contained in:
@@ -3,8 +3,8 @@ import { parseCSSText, camelize, toCSSText } from './style';
|
||||
export interface IRRNode {
|
||||
parentElement: IRRNode | null;
|
||||
parentNode: IRRNode | null;
|
||||
childNodes: IRRNode[];
|
||||
ownerDocument: IRRDocument;
|
||||
readonly childNodes: IRRNode[];
|
||||
readonly ELEMENT_NODE: number;
|
||||
readonly TEXT_NODE: number;
|
||||
// corresponding nodeType value of standard HTML Node
|
||||
@@ -16,8 +16,11 @@ export interface IRRNode {
|
||||
|
||||
lastChild: IRRNode | null;
|
||||
|
||||
previousSibling: IRRNode | null;
|
||||
|
||||
nextSibling: IRRNode | null;
|
||||
|
||||
// If the node is a document or a doctype, textContent returns null.
|
||||
textContent: string | null;
|
||||
|
||||
contains(node: IRRNode): boolean;
|
||||
@@ -127,11 +130,16 @@ type ConstrainedConstructor<T = Record<string, unknown>> = new (
|
||||
* This is designed as an abstract class so it should never be instantiated.
|
||||
*/
|
||||
export class BaseRRNode implements IRRNode {
|
||||
public childNodes: IRRNode[] = [];
|
||||
public parentElement: IRRNode | null = null;
|
||||
public parentNode: IRRNode | null = null;
|
||||
public textContent: string | null;
|
||||
public ownerDocument: IRRDocument;
|
||||
public firstChild: IRRNode | null = null;
|
||||
public lastChild: IRRNode | null = null;
|
||||
public previousSibling: IRRNode | null = null;
|
||||
public nextSibling: IRRNode | null = null;
|
||||
|
||||
public textContent: string | null;
|
||||
|
||||
public readonly ELEMENT_NODE: number = NodeType.ELEMENT_NODE;
|
||||
public readonly TEXT_NODE: number = NodeType.TEXT_NODE;
|
||||
// corresponding nodeType value of standard HTML Node
|
||||
@@ -144,26 +152,24 @@ export class BaseRRNode implements IRRNode {
|
||||
//
|
||||
}
|
||||
|
||||
public get firstChild(): IRRNode | null {
|
||||
return this.childNodes[0] || null;
|
||||
}
|
||||
|
||||
public get lastChild(): IRRNode | null {
|
||||
return this.childNodes[this.childNodes.length - 1] || null;
|
||||
}
|
||||
|
||||
public get nextSibling(): IRRNode | null {
|
||||
const parentNode = this.parentNode;
|
||||
if (!parentNode) return null;
|
||||
const siblings = parentNode.childNodes;
|
||||
const index = siblings.indexOf(this);
|
||||
return siblings[index + 1] || null;
|
||||
public get childNodes(): IRRNode[] {
|
||||
const childNodes: IRRNode[] = [];
|
||||
let childIterator: IRRNode | null = this.firstChild;
|
||||
while (childIterator) {
|
||||
childNodes.push(childIterator);
|
||||
childIterator = childIterator.nextSibling;
|
||||
}
|
||||
return childNodes;
|
||||
}
|
||||
|
||||
public contains(node: IRRNode) {
|
||||
if (node === this) return true;
|
||||
for (const child of this.childNodes) {
|
||||
if (child.contains(node)) return true;
|
||||
if (!(node instanceof BaseRRNode)) return false;
|
||||
else if (node.ownerDocument !== this.ownerDocument) return false;
|
||||
else if (node === this) return true;
|
||||
|
||||
while (node.parentNode) {
|
||||
if (node.parentNode === this) return true;
|
||||
node = node.parentNode;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -202,11 +208,11 @@ export function BaseRRDocumentImpl<
|
||||
public readonly nodeName: '#document' = '#document';
|
||||
public readonly compatMode: 'BackCompat' | 'CSS1Compat' = 'CSS1Compat';
|
||||
public readonly RRNodeType = RRNodeType.Document;
|
||||
public textContent: string | null = null;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
constructor(...args: any[]) {
|
||||
super(args);
|
||||
this.textContent = null;
|
||||
this.ownerDocument = this;
|
||||
}
|
||||
|
||||
@@ -248,8 +254,8 @@ export function BaseRRDocumentImpl<
|
||||
return this.documentElement;
|
||||
}
|
||||
|
||||
public appendChild(childNode: IRRNode): IRRNode {
|
||||
const nodeType = childNode.RRNodeType;
|
||||
public appendChild(newChild: IRRNode): IRRNode {
|
||||
const nodeType = newChild.RRNodeType;
|
||||
if (
|
||||
nodeType === RRNodeType.Element ||
|
||||
nodeType === RRNodeType.DocumentType
|
||||
@@ -262,11 +268,10 @@ export function BaseRRDocumentImpl<
|
||||
);
|
||||
}
|
||||
}
|
||||
childNode.parentElement = null;
|
||||
childNode.parentNode = this;
|
||||
childNode.ownerDocument = this.ownerDocument;
|
||||
this.childNodes.push(childNode);
|
||||
return childNode;
|
||||
|
||||
const child = appendChild(this, newChild);
|
||||
child.parentElement = null;
|
||||
return child;
|
||||
}
|
||||
|
||||
public insertBefore(newChild: IRRNode, refChild: IRRNode | null): IRRNode {
|
||||
@@ -283,33 +288,19 @@ export function BaseRRDocumentImpl<
|
||||
);
|
||||
}
|
||||
}
|
||||
if (refChild === null) return this.appendChild(newChild);
|
||||
const childIndex = this.childNodes.indexOf(refChild);
|
||||
if (childIndex == -1)
|
||||
throw new Error(
|
||||
"Failed to execute 'insertBefore' on 'RRNode': The RRNode before which the new node is to be inserted is not a child of this RRNode.",
|
||||
);
|
||||
this.childNodes.splice(childIndex, 0, newChild);
|
||||
newChild.parentElement = null;
|
||||
newChild.parentNode = this;
|
||||
newChild.ownerDocument = this.ownerDocument;
|
||||
return newChild;
|
||||
|
||||
const child = insertBefore(this, newChild, refChild);
|
||||
child.parentElement = null;
|
||||
return child;
|
||||
}
|
||||
|
||||
public removeChild(node: IRRNode) {
|
||||
const indexOfChild = this.childNodes.indexOf(node);
|
||||
if (indexOfChild === -1)
|
||||
throw new Error(
|
||||
"Failed to execute 'removeChild' on 'RRDocument': The RRNode to be removed is not a child of this RRNode.",
|
||||
);
|
||||
this.childNodes.splice(indexOfChild, 1);
|
||||
node.parentElement = null;
|
||||
node.parentNode = null;
|
||||
return node;
|
||||
public removeChild(node: IRRNode): IRRNode {
|
||||
return removeChild(this, node);
|
||||
}
|
||||
|
||||
public open() {
|
||||
this.childNodes = [];
|
||||
this.firstChild = null;
|
||||
this.lastChild = null;
|
||||
}
|
||||
|
||||
public close() {
|
||||
@@ -415,7 +406,6 @@ export function BaseRRDocumentTypeImpl<
|
||||
public readonly name: string;
|
||||
public readonly publicId: string;
|
||||
public readonly systemId: string;
|
||||
public textContent: string | null = null;
|
||||
|
||||
constructor(qualifiedName: string, publicId: string, systemId: string) {
|
||||
super();
|
||||
@@ -423,6 +413,7 @@ export function BaseRRDocumentTypeImpl<
|
||||
this.publicId = publicId;
|
||||
this.systemId = systemId;
|
||||
this.nodeName = qualifiedName;
|
||||
this.textContent = null;
|
||||
}
|
||||
|
||||
toString() {
|
||||
@@ -459,7 +450,9 @@ export function BaseRRElementImpl<
|
||||
}
|
||||
|
||||
public set textContent(textContent: string) {
|
||||
this.childNodes = [this.ownerDocument.createTextNode(textContent)];
|
||||
this.firstChild = null;
|
||||
this.lastChild = null;
|
||||
this.appendChild(this.ownerDocument.createTextNode(textContent));
|
||||
}
|
||||
|
||||
public get classList(): ClassList {
|
||||
@@ -528,37 +521,15 @@ export function BaseRRElementImpl<
|
||||
}
|
||||
|
||||
public appendChild(newChild: IRRNode): IRRNode {
|
||||
this.childNodes.push(newChild);
|
||||
newChild.parentNode = this;
|
||||
newChild.parentElement = this;
|
||||
newChild.ownerDocument = this.ownerDocument;
|
||||
return newChild;
|
||||
return appendChild(this, newChild);
|
||||
}
|
||||
|
||||
public insertBefore(newChild: IRRNode, refChild: IRRNode | null): IRRNode {
|
||||
if (refChild === null) return this.appendChild(newChild);
|
||||
const childIndex = this.childNodes.indexOf(refChild);
|
||||
if (childIndex == -1)
|
||||
throw new Error(
|
||||
"Failed to execute 'insertBefore' on 'RRNode': The RRNode before which the new node is to be inserted is not a child of this RRNode.",
|
||||
);
|
||||
this.childNodes.splice(childIndex, 0, newChild);
|
||||
newChild.parentElement = this;
|
||||
newChild.parentNode = this;
|
||||
newChild.ownerDocument = this.ownerDocument;
|
||||
return newChild;
|
||||
return insertBefore(this, newChild, refChild);
|
||||
}
|
||||
|
||||
public removeChild(node: IRRNode): IRRNode {
|
||||
const indexOfChild = this.childNodes.indexOf(node);
|
||||
if (indexOfChild === -1)
|
||||
throw new Error(
|
||||
"Failed to execute 'removeChild' on 'RRElement': The RRNode to be removed is not a child of this RRNode.",
|
||||
);
|
||||
this.childNodes.splice(indexOfChild, 1);
|
||||
node.parentElement = null;
|
||||
node.parentNode = null;
|
||||
return node;
|
||||
return removeChild(this, node);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
@@ -741,6 +712,65 @@ export type CSSStyleDeclaration = Record<string, string> & {
|
||||
removeProperty: (name: string) => string;
|
||||
};
|
||||
|
||||
function appendChild(parent: IRRNode, newChild: IRRNode) {
|
||||
if (parent.lastChild) {
|
||||
parent.lastChild.nextSibling = newChild;
|
||||
newChild.previousSibling = parent.lastChild;
|
||||
} else {
|
||||
parent.firstChild = newChild;
|
||||
newChild.previousSibling = null;
|
||||
}
|
||||
parent.lastChild = newChild;
|
||||
newChild.nextSibling = null;
|
||||
newChild.parentNode = parent;
|
||||
newChild.parentElement = parent;
|
||||
newChild.ownerDocument = parent.ownerDocument;
|
||||
return newChild;
|
||||
}
|
||||
|
||||
function insertBefore(
|
||||
parent: IRRNode,
|
||||
newChild: IRRNode,
|
||||
refChild: IRRNode | null,
|
||||
) {
|
||||
if (!refChild) return appendChild(parent, newChild);
|
||||
|
||||
if (refChild.parentNode !== parent)
|
||||
throw new Error(
|
||||
"Failed to execute 'insertBefore' on 'RRNode': The RRNode before which the new node is to be inserted is not a child of this RRNode.",
|
||||
);
|
||||
|
||||
newChild.previousSibling = refChild.previousSibling;
|
||||
refChild.previousSibling = newChild;
|
||||
newChild.nextSibling = refChild;
|
||||
|
||||
if (newChild.previousSibling) newChild.previousSibling.nextSibling = newChild;
|
||||
else parent.firstChild = newChild;
|
||||
|
||||
newChild.parentElement = parent;
|
||||
newChild.parentNode = parent;
|
||||
newChild.ownerDocument = parent.ownerDocument;
|
||||
return newChild;
|
||||
}
|
||||
|
||||
function removeChild(parent: IRRNode, child: IRRNode) {
|
||||
if (child.parentNode !== parent)
|
||||
throw new Error(
|
||||
"Failed to execute 'removeChild' on 'RRNode': The RRNode to be removed is not a child of this RRNode.",
|
||||
);
|
||||
if (child.previousSibling)
|
||||
child.previousSibling.nextSibling = child.nextSibling;
|
||||
else parent.firstChild = child.nextSibling;
|
||||
if (child.nextSibling)
|
||||
child.nextSibling.previousSibling = child.previousSibling;
|
||||
else parent.lastChild = child.previousSibling;
|
||||
child.previousSibling = null;
|
||||
child.nextSibling = null;
|
||||
child.parentElement = null;
|
||||
child.parentNode = null;
|
||||
return child;
|
||||
}
|
||||
|
||||
// Enumerate nodeType value of standard HTML Node.
|
||||
export enum NodeType {
|
||||
PLACEHOLDER, // This isn't a node type. Enum type value starts from zero but NodeType value starts from 1.
|
||||
|
||||
@@ -131,7 +131,8 @@ export class RRDocument extends BaseRRDocumentImpl(RRNode) {
|
||||
}
|
||||
|
||||
destroyTree() {
|
||||
this.childNodes = [];
|
||||
this.firstChild = null;
|
||||
this.lastChild = null;
|
||||
this.mirror.reset();
|
||||
}
|
||||
|
||||
|
||||
@@ -1271,7 +1271,7 @@ describe('diff algorithm for rrdom', () => {
|
||||
expect(rrdom.mirror.getId(rrdom)).toBe(-2);
|
||||
expect(rrdom.mirror.getId(rrdom.body)).toBe(-6);
|
||||
|
||||
rrdom.childNodes = [];
|
||||
while (rrdom.firstChild) rrdom.removeChild(rrdom.firstChild);
|
||||
/**
|
||||
* Rebuild the rrdom and make it looks like this:
|
||||
* -7 RRDocument
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
BaseRRMediaElementImpl,
|
||||
BaseRRNode,
|
||||
IRRDocumentType,
|
||||
IRRNode,
|
||||
} from '../src/document';
|
||||
|
||||
describe('Basic RRDocument implementation', () => {
|
||||
@@ -34,6 +35,7 @@ describe('Basic RRDocument implementation', () => {
|
||||
expect(node.TEXT_NODE).toBe(document.TEXT_NODE);
|
||||
expect(node.firstChild).toBeNull();
|
||||
expect(node.lastChild).toBeNull();
|
||||
expect(node.previousSibling).toBeNull();
|
||||
expect(node.nextSibling).toBeNull();
|
||||
expect(node.contains).toBeDefined();
|
||||
expect(node.appendChild).toBeDefined();
|
||||
@@ -42,56 +44,103 @@ describe('Basic RRDocument implementation', () => {
|
||||
expect(node.toString()).toEqual('RRNode');
|
||||
});
|
||||
|
||||
it('can get first child node', () => {
|
||||
it('can get and set first child node', () => {
|
||||
const parentNode = new RRNode();
|
||||
const childNode1 = new RRNode();
|
||||
const childNode2 = new RRNode();
|
||||
expect(parentNode.firstChild).toBeNull();
|
||||
parentNode.childNodes = [childNode1];
|
||||
parentNode.firstChild = childNode1;
|
||||
expect(parentNode.firstChild).toBe(childNode1);
|
||||
parentNode.childNodes = [childNode1, childNode2];
|
||||
expect(parentNode.firstChild).toBe(childNode1);
|
||||
parentNode.childNodes = [childNode2, childNode1];
|
||||
expect(parentNode.firstChild).toBe(childNode2);
|
||||
parentNode.firstChild = null;
|
||||
expect(parentNode.firstChild).toBeNull();
|
||||
});
|
||||
|
||||
it('can get last child node', () => {
|
||||
it('can get and set last child node', () => {
|
||||
const parentNode = new RRNode();
|
||||
const childNode1 = new RRNode();
|
||||
const childNode2 = new RRNode();
|
||||
expect(parentNode.lastChild).toBeNull();
|
||||
parentNode.childNodes = [childNode1];
|
||||
expect(parentNode.lastChild).toBe(childNode1);
|
||||
parentNode.childNodes = [childNode1, childNode2];
|
||||
expect(parentNode.lastChild).toBe(childNode2);
|
||||
parentNode.childNodes = [childNode2, childNode1];
|
||||
parentNode.lastChild = childNode1;
|
||||
expect(parentNode.lastChild).toBe(childNode1);
|
||||
parentNode.lastChild = null;
|
||||
expect(parentNode.lastChild).toBeNull();
|
||||
});
|
||||
|
||||
it('can get nextSibling', () => {
|
||||
it('can get and set preSibling', () => {
|
||||
const node1 = new RRNode();
|
||||
const node2 = new RRNode();
|
||||
expect(node1.previousSibling).toBeNull();
|
||||
node1.previousSibling = node2;
|
||||
expect(node1.previousSibling).toBe(node2);
|
||||
node1.previousSibling = null;
|
||||
expect(node1.previousSibling).toBeNull();
|
||||
});
|
||||
|
||||
it('can get and set nextSibling', () => {
|
||||
const node1 = new RRNode();
|
||||
const node2 = new RRNode();
|
||||
expect(node1.nextSibling).toBeNull();
|
||||
node1.nextSibling = node2;
|
||||
expect(node1.nextSibling).toBe(node2);
|
||||
node1.nextSibling = null;
|
||||
expect(node1.nextSibling).toBeNull();
|
||||
});
|
||||
|
||||
it('can get childNodes', () => {
|
||||
const parentNode = new RRNode();
|
||||
expect(parentNode.childNodes).toBeInstanceOf(Array);
|
||||
expect(parentNode.childNodes.length).toBe(0);
|
||||
|
||||
const childNode1 = new RRNode();
|
||||
parentNode.firstChild = childNode1;
|
||||
parentNode.lastChild = childNode1;
|
||||
expect(parentNode.childNodes).toEqual([childNode1]);
|
||||
|
||||
const childNode2 = new RRNode();
|
||||
expect(parentNode.nextSibling).toBeNull();
|
||||
expect(childNode1.nextSibling).toBeNull();
|
||||
childNode1.parentNode = parentNode;
|
||||
parentNode.childNodes = [childNode1];
|
||||
expect(childNode1.nextSibling).toBeNull();
|
||||
childNode2.parentNode = parentNode;
|
||||
parentNode.childNodes = [childNode1, childNode2];
|
||||
expect(childNode1.nextSibling).toBe(childNode2);
|
||||
expect(childNode2.nextSibling).toBeNull();
|
||||
parentNode.lastChild = childNode2;
|
||||
childNode1.nextSibling = childNode2;
|
||||
childNode2.previousSibling = childNode1;
|
||||
expect(parentNode.childNodes).toEqual([childNode1, childNode2]);
|
||||
|
||||
const childNode3 = new RRNode();
|
||||
parentNode.lastChild = childNode3;
|
||||
childNode2.nextSibling = childNode3;
|
||||
childNode3.previousSibling = childNode2;
|
||||
expect(parentNode.childNodes).toEqual([
|
||||
childNode1,
|
||||
childNode2,
|
||||
childNode3,
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return whether the node contains another node', () => {
|
||||
const parentNode = new RRNode();
|
||||
expect(parentNode.contains(parentNode)).toBeTruthy();
|
||||
expect(parentNode.contains(null as unknown as IRRNode)).toBeFalsy();
|
||||
expect(parentNode.contains(undefined as unknown as IRRNode)).toBeFalsy();
|
||||
expect(parentNode.contains({} as unknown as IRRNode)).toBeFalsy();
|
||||
expect(
|
||||
parentNode.contains(new RRDocument().createElement('div')),
|
||||
).toBeFalsy();
|
||||
const childNode1 = new RRNode();
|
||||
const childNode2 = new RRNode();
|
||||
parentNode.childNodes = [childNode1];
|
||||
parentNode.firstChild = childNode1;
|
||||
parentNode.lastChild = childNode1;
|
||||
childNode1.parentNode = parentNode;
|
||||
expect(parentNode.contains(childNode1)).toBeTruthy();
|
||||
expect(parentNode.contains(childNode2)).toBeFalsy();
|
||||
childNode1.childNodes = [childNode2];
|
||||
|
||||
parentNode.lastChild = childNode2;
|
||||
childNode1.nextSibling = childNode2;
|
||||
childNode2.previousSibling = childNode1;
|
||||
childNode2.parentNode = childNode1;
|
||||
expect(parentNode.contains(childNode1)).toBeTruthy();
|
||||
expect(parentNode.contains(childNode2)).toBeTruthy();
|
||||
|
||||
const childNode3 = new RRNode();
|
||||
expect(parentNode.contains(childNode3)).toBeFalsy();
|
||||
childNode2.firstChild = childNode3;
|
||||
childNode2.lastChild = childNode3;
|
||||
childNode3.parentNode = childNode2;
|
||||
expect(parentNode.contains(childNode3)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should not implement appendChild', () => {
|
||||
@@ -143,6 +192,7 @@ describe('Basic RRDocument implementation', () => {
|
||||
expect(node.TEXT_NODE).toBe(document.TEXT_NODE);
|
||||
expect(node.firstChild).toBeNull();
|
||||
expect(node.lastChild).toBeNull();
|
||||
expect(node.previousSibling).toBeNull();
|
||||
expect(node.nextSibling).toBeNull();
|
||||
expect(node.contains).toBeDefined();
|
||||
expect(node.appendChild).toBeDefined();
|
||||
@@ -282,7 +332,7 @@ describe('Basic RRDocument implementation', () => {
|
||||
expect(() =>
|
||||
node.removeChild(node.createElement('div')),
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Failed to execute 'removeChild' on 'RRDocument': The RRNode to be removed is not a child of this RRNode."`,
|
||||
`"Failed to execute 'removeChild' on 'RRNode': The RRNode to be removed is not a child of this RRNode."`,
|
||||
);
|
||||
expect(node.removeChild(documentType)).toBe(documentType);
|
||||
expect(documentType.parentNode).toBeNull();
|
||||
@@ -369,6 +419,7 @@ describe('Basic RRDocument implementation', () => {
|
||||
expect(node.TEXT_NODE).toBe(document.TEXT_NODE);
|
||||
expect(node.firstChild).toBeNull();
|
||||
expect(node.lastChild).toBeNull();
|
||||
expect(node.previousSibling).toBeNull();
|
||||
expect(node.nextSibling).toBeNull();
|
||||
expect(node.contains).toBeDefined();
|
||||
expect(node.appendChild).toBeDefined();
|
||||
@@ -404,6 +455,7 @@ describe('Basic RRDocument implementation', () => {
|
||||
expect(node.TEXT_NODE).toBe(document.TEXT_NODE);
|
||||
expect(node.firstChild).toBeNull();
|
||||
expect(node.lastChild).toBeNull();
|
||||
expect(node.previousSibling).toBeNull();
|
||||
expect(node.nextSibling).toBeNull();
|
||||
expect(node.contains).toBeDefined();
|
||||
expect(node.appendChild).toBeDefined();
|
||||
@@ -686,23 +738,39 @@ describe('Basic RRDocument implementation', () => {
|
||||
const node = document.createElement('div');
|
||||
expect(node.childNodes.length).toBe(0);
|
||||
|
||||
const child1 = document.createComment('span');
|
||||
const child1 = document.createElement('span');
|
||||
expect(node.appendChild(child1)).toBe(child1);
|
||||
expect(node.childNodes[0]).toEqual(child1);
|
||||
expect(node.childNodes[0]).toBe(child1);
|
||||
expect(node.firstChild).toBe(child1);
|
||||
expect(node.lastChild).toBe(child1);
|
||||
expect(child1.previousSibling).toBeNull();
|
||||
expect(child1.nextSibling).toBeNull();
|
||||
expect(child1.parentElement).toBe(node);
|
||||
expect(child1.parentNode).toBe(node);
|
||||
expect(child1.ownerDocument).toBe(document);
|
||||
expect(node.contains(child1)).toBeTruthy();
|
||||
|
||||
const child2 = document.createElement('p');
|
||||
expect(node.appendChild(child2)).toBe(child2);
|
||||
expect(node.childNodes[1]).toEqual(child2);
|
||||
expect(node.childNodes[1]).toBe(child2);
|
||||
expect(node.firstChild).toBe(child1);
|
||||
expect(node.lastChild).toBe(child2);
|
||||
expect(child1.previousSibling).toBeNull();
|
||||
expect(child1.nextSibling).toBe(child2);
|
||||
expect(child2.previousSibling).toBe(child1);
|
||||
expect(child2.nextSibling).toBeNull();
|
||||
expect(child2.parentElement).toBe(node);
|
||||
expect(child2.parentNode).toBe(node);
|
||||
expect(child2.ownerDocument).toBe(document);
|
||||
expect(node.contains(child1)).toBeTruthy();
|
||||
expect(node.contains(child2)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('can insert new child before an existing child', () => {
|
||||
const node = document.createElement('div');
|
||||
const child1 = document.createElement('h1');
|
||||
const child2 = document.createElement('h2');
|
||||
const child3 = document.createElement('h3');
|
||||
expect(() =>
|
||||
node.insertBefore(node, child1),
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
@@ -710,42 +778,119 @@ describe('Basic RRDocument implementation', () => {
|
||||
);
|
||||
expect(node.insertBefore(child1, null)).toBe(child1);
|
||||
expect(node.childNodes[0]).toBe(child1);
|
||||
expect(node.childNodes.length).toBe(1);
|
||||
expect(node.firstChild).toBe(child1);
|
||||
expect(node.lastChild).toBe(child1);
|
||||
expect(child1.previousSibling).toBeNull();
|
||||
expect(child1.nextSibling).toBeNull();
|
||||
expect(child1.parentNode).toBe(node);
|
||||
expect(child1.parentElement).toBe(node);
|
||||
expect(child1.ownerDocument).toBe(document);
|
||||
expect(node.contains(child1)).toBeTruthy();
|
||||
|
||||
expect(node.insertBefore(child2, child1)).toBe(child2);
|
||||
expect(node.childNodes.length).toBe(2);
|
||||
expect(node.childNodes[0]).toBe(child2);
|
||||
expect(node.childNodes[1]).toBe(child1);
|
||||
expect(node.childNodes).toEqual([child2, child1]);
|
||||
expect(node.firstChild).toBe(child2);
|
||||
expect(node.lastChild).toBe(child1);
|
||||
expect(child1.previousSibling).toBe(child2);
|
||||
expect(child1.nextSibling).toBeNull();
|
||||
expect(child2.previousSibling).toBeNull();
|
||||
expect(child2.nextSibling).toBe(child1);
|
||||
expect(child2.parentNode).toBe(node);
|
||||
expect(child2.parentElement).toBe(node);
|
||||
expect(child2.ownerDocument).toBe(document);
|
||||
expect(node.contains(child2)).toBeTruthy();
|
||||
expect(node.contains(child1)).toBeTruthy();
|
||||
|
||||
expect(node.insertBefore(child3, child1)).toBe(child3);
|
||||
expect(node.childNodes).toEqual([child2, child3, child1]);
|
||||
expect(node.firstChild).toBe(child2);
|
||||
expect(node.lastChild).toBe(child1);
|
||||
expect(child1.previousSibling).toBe(child3);
|
||||
expect(child1.nextSibling).toBeNull();
|
||||
expect(child3.previousSibling).toBe(child2);
|
||||
expect(child3.nextSibling).toBe(child1);
|
||||
expect(child2.previousSibling).toBeNull();
|
||||
expect(child2.nextSibling).toBe(child3);
|
||||
expect(child3.parentNode).toBe(node);
|
||||
expect(child3.parentElement).toBe(node);
|
||||
expect(child3.ownerDocument).toBe(document);
|
||||
expect(node.contains(child2)).toBeTruthy();
|
||||
expect(node.contains(child3)).toBeTruthy();
|
||||
expect(node.contains(child1)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('can remove an existing child', () => {
|
||||
const node = document.createElement('div');
|
||||
const child1 = document.createElement('h1');
|
||||
const child2 = document.createElement('h2');
|
||||
const child3 = document.createElement('h3');
|
||||
node.appendChild(child1);
|
||||
node.appendChild(child2);
|
||||
expect(node.childNodes.length).toBe(2);
|
||||
expect(child1.parentNode).toBe(node);
|
||||
expect(child2.parentNode).toBe(node);
|
||||
expect(child1.parentElement).toBe(node);
|
||||
expect(child2.parentElement).toBe(node);
|
||||
node.appendChild(child3);
|
||||
expect(node.childNodes).toEqual([child1, child2, child3]);
|
||||
|
||||
expect(() =>
|
||||
node.removeChild(document.createElement('div')),
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Failed to execute 'removeChild' on 'RRElement': The RRNode to be removed is not a child of this RRNode."`,
|
||||
`"Failed to execute 'removeChild' on 'RRNode': The RRNode to be removed is not a child of this RRNode."`,
|
||||
);
|
||||
expect(node.removeChild(child1)).toBe(child1);
|
||||
expect(child1.parentNode).toBeNull();
|
||||
expect(child1.parentElement).toBeNull();
|
||||
expect(node.childNodes.length).toBe(1);
|
||||
// Remove the middle child.
|
||||
expect(node.removeChild(child2)).toBe(child2);
|
||||
expect(node.childNodes.length).toBe(0);
|
||||
expect(node.childNodes).toEqual([child1, child3]);
|
||||
expect(node.contains(child2)).toBeFalsy();
|
||||
expect(node.firstChild).toBe(child1);
|
||||
expect(node.lastChild).toBe(child3);
|
||||
expect(child1.previousSibling).toBeNull();
|
||||
expect(child1.nextSibling).toBe(child3);
|
||||
expect(child3.previousSibling).toBe(child1);
|
||||
expect(child3.nextSibling).toBeNull();
|
||||
expect(child2.previousSibling).toBeNull();
|
||||
expect(child2.nextSibling).toBeNull();
|
||||
expect(child2.parentNode).toBeNull();
|
||||
expect(child2.parentElement).toBeNull();
|
||||
|
||||
// Remove the previous child.
|
||||
expect(node.removeChild(child1)).toBe(child1);
|
||||
expect(node.childNodes).toEqual([child3]);
|
||||
expect(node.contains(child1)).toBeFalsy();
|
||||
expect(node.firstChild).toBe(child3);
|
||||
expect(node.lastChild).toBe(child3);
|
||||
expect(child3.previousSibling).toBeNull();
|
||||
expect(child3.nextSibling).toBeNull();
|
||||
expect(child1.previousSibling).toBeNull();
|
||||
expect(child1.nextSibling).toBeNull();
|
||||
expect(child1.parentNode).toBeNull();
|
||||
expect(child1.parentElement).toBeNull();
|
||||
|
||||
node.insertBefore(child1, child3);
|
||||
expect(node.childNodes).toEqual([child1, child3]);
|
||||
// Remove the next child.
|
||||
expect(node.removeChild(child3)).toBe(child3);
|
||||
expect(node.childNodes).toEqual([child1]);
|
||||
expect(node.contains(child3)).toBeFalsy();
|
||||
expect(node.contains(child1)).toBeTruthy();
|
||||
expect(node.firstChild).toBe(child1);
|
||||
expect(node.lastChild).toBe(child1);
|
||||
expect(child1.previousSibling).toBeNull();
|
||||
expect(child1.nextSibling).toBeNull();
|
||||
expect(child3.previousSibling).toBeNull();
|
||||
expect(child3.nextSibling).toBeNull();
|
||||
expect(child3.parentNode).toBeNull();
|
||||
expect(child3.parentElement).toBeNull();
|
||||
|
||||
// Remove all children.
|
||||
expect(node.removeChild(child1)).toBe(child1);
|
||||
expect(node.childNodes).toEqual([]);
|
||||
expect(node.contains(child1)).toBeFalsy();
|
||||
expect(node.contains(child2)).toBeFalsy();
|
||||
expect(node.contains(child3)).toBeFalsy();
|
||||
expect(node.firstChild).toBeNull();
|
||||
expect(node.lastChild).toBeNull();
|
||||
expect(child1.previousSibling).toBeNull();
|
||||
expect(child1.nextSibling).toBeNull();
|
||||
expect(child1.parentNode).toBeNull();
|
||||
expect(child1.parentElement).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -768,6 +913,7 @@ describe('Basic RRDocument implementation', () => {
|
||||
expect(node.TEXT_NODE).toBe(document.TEXT_NODE);
|
||||
expect(node.firstChild).toBeNull();
|
||||
expect(node.lastChild).toBeNull();
|
||||
expect(node.previousSibling).toBeNull();
|
||||
expect(node.nextSibling).toBeNull();
|
||||
expect(node.contains).toBeDefined();
|
||||
expect(node.appendChild).toBeDefined();
|
||||
@@ -803,6 +949,7 @@ describe('Basic RRDocument implementation', () => {
|
||||
expect(node.TEXT_NODE).toBe(document.TEXT_NODE);
|
||||
expect(node.firstChild).toBeNull();
|
||||
expect(node.lastChild).toBeNull();
|
||||
expect(node.previousSibling).toBeNull();
|
||||
expect(node.nextSibling).toBeNull();
|
||||
expect(node.contains).toBeDefined();
|
||||
expect(node.appendChild).toBeDefined();
|
||||
@@ -838,6 +985,7 @@ describe('Basic RRDocument implementation', () => {
|
||||
expect(node.TEXT_NODE).toBe(document.TEXT_NODE);
|
||||
expect(node.firstChild).toBeNull();
|
||||
expect(node.lastChild).toBeNull();
|
||||
expect(node.previousSibling).toBeNull();
|
||||
expect(node.nextSibling).toBeNull();
|
||||
expect(node.contains).toBeDefined();
|
||||
expect(node.appendChild).toBeDefined();
|
||||
@@ -870,6 +1018,7 @@ describe('Basic RRDocument implementation', () => {
|
||||
expect(node.ELEMENT_NODE).toBe(document.ELEMENT_NODE);
|
||||
expect(node.TEXT_NODE).toBe(document.TEXT_NODE);
|
||||
expect(node.firstChild).toBeNull();
|
||||
expect(node.previousSibling).toBeNull();
|
||||
expect(node.nextSibling).toBeNull();
|
||||
expect(node.contains).toBeDefined();
|
||||
expect(node.appendChild).toBeDefined();
|
||||
|
||||
Reference in New Issue
Block a user