Fix rrdom bugs (#1222)
* fix: rrdom bug 1. fix one bug of the diff algorithm 2. omit srcdoc attribute of iframe * use linked list to iterate children * fix the bug that the children of shadowRoot don't get diffed * add test cases * add change log
This commit is contained in:
8
.changeset/cold-eyes-hunt.md
Normal file
8
.changeset/cold-eyes-hunt.md
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
---
|
||||||
|
'rrdom': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Fix: rrdom bugs
|
||||||
|
|
||||||
|
1. Fix a bug in the diff algorithm.
|
||||||
|
2. Omit the 'srcdoc' attribute of iframes to avoid overwriting content.
|
||||||
@@ -116,17 +116,7 @@ export function diff(
|
|||||||
rrnodeMirror,
|
rrnodeMirror,
|
||||||
);
|
);
|
||||||
|
|
||||||
const oldChildren = oldTree.childNodes;
|
diffChildren(oldTree, newTree, replayer, rrnodeMirror);
|
||||||
const newChildren = newTree.childNodes;
|
|
||||||
if (oldChildren.length > 0 || newChildren.length > 0) {
|
|
||||||
diffChildren(
|
|
||||||
Array.from(oldChildren),
|
|
||||||
newChildren,
|
|
||||||
oldTree,
|
|
||||||
replayer,
|
|
||||||
rrnodeMirror,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
diffAfterUpdatingChildren(oldTree, newTree, replayer, rrnodeMirror);
|
diffAfterUpdatingChildren(oldTree, newTree, replayer, rrnodeMirror);
|
||||||
}
|
}
|
||||||
@@ -196,18 +186,13 @@ function diffBeforeUpdatingChildren(
|
|||||||
}
|
}
|
||||||
if (newRRElement.shadowRoot) {
|
if (newRRElement.shadowRoot) {
|
||||||
if (!oldElement.shadowRoot) oldElement.attachShadow({ mode: 'open' });
|
if (!oldElement.shadowRoot) oldElement.attachShadow({ mode: 'open' });
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
diffChildren(
|
||||||
const oldChildren = oldElement.shadowRoot!.childNodes;
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
const newChildren = newRRElement.shadowRoot.childNodes;
|
oldElement.shadowRoot!,
|
||||||
if (oldChildren.length > 0 || newChildren.length > 0)
|
newRRElement.shadowRoot,
|
||||||
diffChildren(
|
replayer,
|
||||||
Array.from(oldChildren),
|
rrnodeMirror,
|
||||||
newChildren,
|
);
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
||||||
oldElement.shadowRoot!,
|
|
||||||
replayer,
|
|
||||||
rrnodeMirror,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -335,7 +320,8 @@ function diffProps(
|
|||||||
ctx.drawImage(image, 0, 0, image.width, image.height);
|
ctx.drawImage(image, 0, 0, image.width, image.height);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
} else oldTree.setAttribute(name, newValue);
|
} else if (newTree.tagName === 'IFRAME' && name === 'srcdoc') continue;
|
||||||
|
else oldTree.setAttribute(name, newValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const { name } of Array.from(oldAttributes))
|
for (const { name } of Array.from(oldAttributes))
|
||||||
@@ -346,12 +332,14 @@ function diffProps(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function diffChildren(
|
function diffChildren(
|
||||||
oldChildren: (Node | undefined)[],
|
oldTree: Node,
|
||||||
newChildren: IRRNode[],
|
newTree: IRRNode,
|
||||||
parentNode: Node,
|
|
||||||
replayer: ReplayerHandler,
|
replayer: ReplayerHandler,
|
||||||
rrnodeMirror: Mirror,
|
rrnodeMirror: Mirror,
|
||||||
) {
|
) {
|
||||||
|
const oldChildren: (Node | undefined)[] = Array.from(oldTree.childNodes);
|
||||||
|
const newChildren = newTree.childNodes;
|
||||||
|
if (oldChildren.length === 0 && newChildren.length === 0) return;
|
||||||
let oldStartIndex = 0,
|
let oldStartIndex = 0,
|
||||||
oldEndIndex = oldChildren.length - 1,
|
oldEndIndex = oldChildren.length - 1,
|
||||||
newStartIndex = 0,
|
newStartIndex = 0,
|
||||||
@@ -371,14 +359,12 @@ function diffChildren(
|
|||||||
// same first node?
|
// same first node?
|
||||||
nodeMatching(oldStartNode, newStartNode, replayer.mirror, rrnodeMirror)
|
nodeMatching(oldStartNode, newStartNode, replayer.mirror, rrnodeMirror)
|
||||||
) {
|
) {
|
||||||
diff(oldStartNode, newStartNode, replayer, rrnodeMirror);
|
|
||||||
oldStartNode = oldChildren[++oldStartIndex];
|
oldStartNode = oldChildren[++oldStartIndex];
|
||||||
newStartNode = newChildren[++newStartIndex];
|
newStartNode = newChildren[++newStartIndex];
|
||||||
} else if (
|
} else if (
|
||||||
// same last node?
|
// same last node?
|
||||||
nodeMatching(oldEndNode, newEndNode, replayer.mirror, rrnodeMirror)
|
nodeMatching(oldEndNode, newEndNode, replayer.mirror, rrnodeMirror)
|
||||||
) {
|
) {
|
||||||
diff(oldEndNode, newEndNode, replayer, rrnodeMirror);
|
|
||||||
oldEndNode = oldChildren[--oldEndIndex];
|
oldEndNode = oldChildren[--oldEndIndex];
|
||||||
newEndNode = newChildren[--newEndIndex];
|
newEndNode = newChildren[--newEndIndex];
|
||||||
} else if (
|
} else if (
|
||||||
@@ -386,11 +372,10 @@ function diffChildren(
|
|||||||
nodeMatching(oldStartNode, newEndNode, replayer.mirror, rrnodeMirror)
|
nodeMatching(oldStartNode, newEndNode, replayer.mirror, rrnodeMirror)
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
parentNode.insertBefore(oldStartNode, oldEndNode.nextSibling);
|
oldTree.insertBefore(oldStartNode, oldEndNode.nextSibling);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn(e);
|
console.warn(e);
|
||||||
}
|
}
|
||||||
diff(oldStartNode, newEndNode, replayer, rrnodeMirror);
|
|
||||||
oldStartNode = oldChildren[++oldStartIndex];
|
oldStartNode = oldChildren[++oldStartIndex];
|
||||||
newEndNode = newChildren[--newEndIndex];
|
newEndNode = newChildren[--newEndIndex];
|
||||||
} else if (
|
} else if (
|
||||||
@@ -398,11 +383,10 @@ function diffChildren(
|
|||||||
nodeMatching(oldEndNode, newStartNode, replayer.mirror, rrnodeMirror)
|
nodeMatching(oldEndNode, newStartNode, replayer.mirror, rrnodeMirror)
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
parentNode.insertBefore(oldEndNode, oldStartNode);
|
oldTree.insertBefore(oldEndNode, oldStartNode);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn(e);
|
console.warn(e);
|
||||||
}
|
}
|
||||||
diff(oldEndNode, newStartNode, replayer, rrnodeMirror);
|
|
||||||
oldEndNode = oldChildren[--oldEndIndex];
|
oldEndNode = oldChildren[--oldEndIndex];
|
||||||
newStartNode = newChildren[++newStartIndex];
|
newStartNode = newChildren[++newStartIndex];
|
||||||
} else {
|
} else {
|
||||||
@@ -424,11 +408,10 @@ function diffChildren(
|
|||||||
nodeMatching(nodeToMove, newStartNode, replayer.mirror, rrnodeMirror)
|
nodeMatching(nodeToMove, newStartNode, replayer.mirror, rrnodeMirror)
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
parentNode.insertBefore(nodeToMove, oldStartNode);
|
oldTree.insertBefore(nodeToMove, oldStartNode);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn(e);
|
console.warn(e);
|
||||||
}
|
}
|
||||||
diff(nodeToMove, newStartNode, replayer, rrnodeMirror);
|
|
||||||
oldChildren[indexInOld] = undefined;
|
oldChildren[indexInOld] = undefined;
|
||||||
} else {
|
} else {
|
||||||
const newNode = createOrGetNode(
|
const newNode = createOrGetNode(
|
||||||
@@ -438,7 +421,7 @@ function diffChildren(
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
parentNode.nodeName === '#document' &&
|
oldTree.nodeName === '#document' &&
|
||||||
oldStartNode &&
|
oldStartNode &&
|
||||||
/**
|
/**
|
||||||
* Special case 1: one document isn't allowed to have two doctype nodes at the same time, so we need to remove the old one first before inserting the new one.
|
* Special case 1: one document isn't allowed to have two doctype nodes at the same time, so we need to remove the old one first before inserting the new one.
|
||||||
@@ -453,14 +436,13 @@ function diffChildren(
|
|||||||
(newNode.nodeType === newNode.ELEMENT_NODE &&
|
(newNode.nodeType === newNode.ELEMENT_NODE &&
|
||||||
oldStartNode.nodeType === oldStartNode.ELEMENT_NODE))
|
oldStartNode.nodeType === oldStartNode.ELEMENT_NODE))
|
||||||
) {
|
) {
|
||||||
parentNode.removeChild(oldStartNode);
|
oldTree.removeChild(oldStartNode);
|
||||||
replayer.mirror.removeNodeFromMap(oldStartNode);
|
replayer.mirror.removeNodeFromMap(oldStartNode);
|
||||||
oldStartNode = oldChildren[++oldStartIndex];
|
oldStartNode = oldChildren[++oldStartIndex];
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
parentNode.insertBefore(newNode, oldStartNode || null);
|
oldTree.insertBefore(newNode, oldStartNode || null);
|
||||||
diff(newNode, newStartNode, replayer, rrnodeMirror);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn(e);
|
console.warn(e);
|
||||||
}
|
}
|
||||||
@@ -482,8 +464,7 @@ function diffChildren(
|
|||||||
rrnodeMirror,
|
rrnodeMirror,
|
||||||
);
|
);
|
||||||
try {
|
try {
|
||||||
parentNode.insertBefore(newNode, referenceNode);
|
oldTree.insertBefore(newNode, referenceNode);
|
||||||
diff(newNode, newChildren[newStartIndex], replayer, rrnodeMirror);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn(e);
|
console.warn(e);
|
||||||
}
|
}
|
||||||
@@ -491,15 +472,24 @@ function diffChildren(
|
|||||||
} else if (newStartIndex > newEndIndex) {
|
} else if (newStartIndex > newEndIndex) {
|
||||||
for (; oldStartIndex <= oldEndIndex; oldStartIndex++) {
|
for (; oldStartIndex <= oldEndIndex; oldStartIndex++) {
|
||||||
const node = oldChildren[oldStartIndex];
|
const node = oldChildren[oldStartIndex];
|
||||||
if (!node || node.parentNode !== parentNode) continue;
|
if (!node || node.parentNode !== oldTree) continue;
|
||||||
try {
|
try {
|
||||||
parentNode.removeChild(node);
|
oldTree.removeChild(node);
|
||||||
replayer.mirror.removeNodeFromMap(node);
|
replayer.mirror.removeNodeFromMap(node);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn(e);
|
console.warn(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Recursively diff the children of the old tree and the new tree with their props and deeper structures.
|
||||||
|
let oldChild = oldTree.firstChild;
|
||||||
|
let newChild = newTree.firstChild;
|
||||||
|
while (oldChild !== null && newChild !== null) {
|
||||||
|
diff(oldChild, newChild, replayer, rrnodeMirror);
|
||||||
|
oldChild = oldChild.nextSibling;
|
||||||
|
newChild = newChild.nextSibling;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createOrGetNode(
|
export function createOrGetNode(
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
Mirror as RRNodeMirror,
|
Mirror as RRNodeMirror,
|
||||||
RRDocument,
|
RRDocument,
|
||||||
RRMediaElement,
|
RRMediaElement,
|
||||||
|
printRRDom,
|
||||||
} from '../src';
|
} from '../src';
|
||||||
import {
|
import {
|
||||||
createOrGetNode,
|
createOrGetNode,
|
||||||
@@ -106,6 +107,7 @@ function shuffle(list: number[]) {
|
|||||||
describe('diff algorithm for rrdom', () => {
|
describe('diff algorithm for rrdom', () => {
|
||||||
let mirror: NodeMirror;
|
let mirror: NodeMirror;
|
||||||
let replayer: ReplayerHandler;
|
let replayer: ReplayerHandler;
|
||||||
|
let warn: jest.SpyInstance;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mirror = createMirror();
|
mirror = createMirror();
|
||||||
@@ -118,6 +120,14 @@ describe('diff algorithm for rrdom', () => {
|
|||||||
afterAppend: () => {},
|
afterAppend: () => {},
|
||||||
};
|
};
|
||||||
document.write('<!DOCTYPE html><html><head></head><body></body></html>');
|
document.write('<!DOCTYPE html><html><head></head><body></body></html>');
|
||||||
|
// Mock the original console.warn function to make the test fail once console.warn is called.
|
||||||
|
warn = jest.spyOn(console, 'warn');
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
// Check that warn was not called (fail on warning)
|
||||||
|
expect(warn).not.toBeCalled();
|
||||||
|
warn.mockRestore();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('diff single node', () => {
|
describe('diff single node', () => {
|
||||||
@@ -437,6 +447,19 @@ describe('diff algorithm for rrdom', () => {
|
|||||||
expect(document.createElement).toHaveBeenCalledWith('img');
|
expect(document.createElement).toHaveBeenCalledWith('img');
|
||||||
jest.restoreAllMocks();
|
jest.restoreAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('can omit srcdoc attribute of iframe element', () => {
|
||||||
|
// If srcdoc attribute is set, the content of iframe recorded by rrweb will be override.
|
||||||
|
const element = document.createElement('iframe');
|
||||||
|
const rrDocument = new RRDocument();
|
||||||
|
const rrIframe = rrDocument.createElement('iframe');
|
||||||
|
const sn = Object.assign({}, elementSn, { tagName: 'iframe' });
|
||||||
|
rrDocument.mirror.add(rrIframe, sn);
|
||||||
|
rrIframe.attributes['srcdoc'] = '<html></html>';
|
||||||
|
|
||||||
|
diff(element, rrIframe, replayer);
|
||||||
|
expect(element.getAttribute('srcdoc')).toBe(null);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('diff children', () => {
|
describe('diff children', () => {
|
||||||
@@ -1054,6 +1077,57 @@ describe('diff algorithm for rrdom', () => {
|
|||||||
const liChild = spanChild.childNodes[0] as HTMLElement;
|
const liChild = spanChild.childNodes[0] as HTMLElement;
|
||||||
expect(liChild.tagName).toEqual('LI');
|
expect(liChild.tagName).toEqual('LI');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should handle corner case with children removed during diff process', () => {
|
||||||
|
/**
|
||||||
|
* This test case is to simulate the following scenario:
|
||||||
|
* The old tree structure:
|
||||||
|
* 0 P
|
||||||
|
* 1 SPAN
|
||||||
|
* 2 SPAN
|
||||||
|
* The new tree structure:
|
||||||
|
* 0 P
|
||||||
|
* 1 SPAN
|
||||||
|
* 2 SPAN
|
||||||
|
* 3 SPAN
|
||||||
|
*/
|
||||||
|
const node = createTree(
|
||||||
|
{
|
||||||
|
tagName: 'p',
|
||||||
|
id: 0,
|
||||||
|
children: [1, 2].map((c) => ({ tagName: 'span', id: c })),
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
mirror,
|
||||||
|
) as Node;
|
||||||
|
expect(node.childNodes.length).toEqual(2);
|
||||||
|
const rrdom = new RRDocument();
|
||||||
|
const rrNode = createTree(
|
||||||
|
{
|
||||||
|
tagName: 'p',
|
||||||
|
id: 0,
|
||||||
|
children: [
|
||||||
|
{ tagName: 'span', id: 1, children: [{ tagName: 'span', id: 2 }] },
|
||||||
|
{ tagName: 'span', id: 3 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
rrdom,
|
||||||
|
) as RRNode;
|
||||||
|
expect(printRRDom(rrNode, rrdom.mirror)).toMatchInlineSnapshot(`
|
||||||
|
"0 P
|
||||||
|
1 SPAN
|
||||||
|
2 SPAN
|
||||||
|
3 SPAN
|
||||||
|
"
|
||||||
|
`);
|
||||||
|
diff(node, rrNode, replayer);
|
||||||
|
|
||||||
|
expect(node.childNodes.length).toEqual(2);
|
||||||
|
expect(node.childNodes[0].childNodes.length).toEqual(1);
|
||||||
|
expect(mirror.getId(node.childNodes[1])).toEqual(3);
|
||||||
|
expect(node.childNodes[0].childNodes.length).toEqual(1);
|
||||||
|
expect(mirror.getId(node.childNodes[0].childNodes[0])).toEqual(2);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('diff shadow dom', () => {
|
describe('diff shadow dom', () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user