Skip to content

Commit 6ab709f

Browse files
billyvgjaj1014jaj1014
committed
fix: inserted styles lost when moving elements (#233)
fix code for nodejs tests change fix direction to avoid issues with duplicate styles format issues swap waitForTimeout for waitForRAF in test that flaked Add unit tests for new functions Fix broken test causes by file formatting removing spaced --------- Co-authored-by: jaj1014 <[email protected]> Co-authored-by: jaj1014 <[email protected]>
1 parent 4d840b6 commit 6ab709f

File tree

4 files changed

+357
-10
lines changed

4 files changed

+357
-10
lines changed

packages/rrdom/src/diff.ts

Lines changed: 66 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -390,7 +390,7 @@ function diffChildren(
390390
nodeMatching(oldStartNode, newEndNode, replayer.mirror, rrnodeMirror)
391391
) {
392392
try {
393-
oldTree.insertBefore(oldStartNode, oldEndNode.nextSibling);
393+
handleInsertBefore(oldTree, oldStartNode, oldEndNode.nextSibling);
394394
} catch (e) {
395395
console.warn(e);
396396
}
@@ -401,7 +401,7 @@ function diffChildren(
401401
nodeMatching(oldEndNode, newStartNode, replayer.mirror, rrnodeMirror)
402402
) {
403403
try {
404-
oldTree.insertBefore(oldEndNode, oldStartNode);
404+
handleInsertBefore(oldTree, oldEndNode, oldStartNode);
405405
} catch (e) {
406406
console.warn(e);
407407
}
@@ -426,7 +426,7 @@ function diffChildren(
426426
nodeMatching(nodeToMove, newStartNode, replayer.mirror, rrnodeMirror)
427427
) {
428428
try {
429-
oldTree.insertBefore(nodeToMove, oldStartNode);
429+
handleInsertBefore(oldTree, nodeToMove, oldStartNode);
430430
} catch (e) {
431431
console.warn(e);
432432
}
@@ -460,7 +460,7 @@ function diffChildren(
460460
}
461461

462462
try {
463-
oldTree.insertBefore(newNode, oldStartNode || null);
463+
handleInsertBefore(oldTree, newNode, oldStartNode || null);
464464
} catch (e) {
465465
console.warn(e);
466466
}
@@ -482,7 +482,7 @@ function diffChildren(
482482
rrnodeMirror,
483483
);
484484
try {
485-
oldTree.insertBefore(newNode, referenceNode);
485+
handleInsertBefore(oldTree, newNode, referenceNode);
486486
} catch (e) {
487487
console.warn(e);
488488
}
@@ -590,3 +590,64 @@ export function nodeMatching(
590590
if (node1Id === -1 || node1Id !== node2Id) return false;
591591
return sameNodeType(node1, node2);
592592
}
593+
594+
/**
595+
* Copies CSSRules and their position from HTML style element which don't exist in it's innerText
596+
*/
597+
function getInsertedStylesFromElement(
598+
styleElement: HTMLStyleElement,
599+
): Array<{ index: number; cssRuleText: string }> | undefined {
600+
const elementCssRules = styleElement.sheet?.cssRules;
601+
if (!elementCssRules || !elementCssRules.length) return;
602+
// style sheet w/ innerText styles to diff with actual and get only inserted styles
603+
const tempStyleSheet = new CSSStyleSheet();
604+
tempStyleSheet.replaceSync(styleElement.innerText);
605+
606+
const innerTextStylesMap: { [key: string]: CSSRule } = {};
607+
608+
for (let i = 0; i < tempStyleSheet.cssRules.length; i++) {
609+
innerTextStylesMap[tempStyleSheet.cssRules[i].cssText] =
610+
tempStyleSheet.cssRules[i];
611+
}
612+
613+
const insertedStylesStyleSheet = [];
614+
615+
for (let i = 0; i < elementCssRules?.length; i++) {
616+
const cssRuleText = elementCssRules[i].cssText;
617+
618+
if (!innerTextStylesMap[cssRuleText]) {
619+
insertedStylesStyleSheet.push({
620+
index: i,
621+
cssRuleText,
622+
});
623+
}
624+
}
625+
626+
return insertedStylesStyleSheet;
627+
}
628+
629+
/**
630+
* Conditionally copy insertedStyles for STYLE nodes and apply after calling insertBefore'
631+
* For non-STYLE nodes, just insertBefore
632+
*/
633+
export function handleInsertBefore(
634+
oldTree: Node,
635+
nodeToMove: Node,
636+
insertBeforeNode: Node | null,
637+
): void {
638+
let insertedStyles;
639+
640+
if (nodeToMove.nodeName === 'STYLE') {
641+
insertedStyles = getInsertedStylesFromElement(
642+
nodeToMove as HTMLStyleElement,
643+
);
644+
}
645+
646+
oldTree.insertBefore(nodeToMove, insertBeforeNode);
647+
648+
if (insertedStyles && insertedStyles.length) {
649+
insertedStyles.forEach(({ cssRuleText, index }) => {
650+
(nodeToMove as HTMLStyleElement).sheet?.insertRule(cssRuleText, index);
651+
});
652+
}
653+
}

packages/rrdom/test/diff.test.ts

Lines changed: 68 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
ReplayerHandler,
2424
nodeMatching,
2525
sameNodeType,
26+
handleInsertBefore,
2627
} from '../src/diff';
2728
import type { IRRElement, IRRNode } from '../src/document';
2829
import { Replayer } from '@sentry-internal/rrweb';
@@ -1469,7 +1470,7 @@ describe('diff algorithm for rrdom', () => {
14691470
const rrHtmlEl = rrDocument.createElement('html');
14701471
rrDocument.mirror.add(rrHtmlEl, rrdom.getDefaultSN(rrHtmlEl, ${htmlElId}));
14711472
rrIframeEl.contentDocument.appendChild(rrHtmlEl);
1472-
1473+
14731474
const replayer = {
14741475
mirror: rrdom.createMirror(),
14751476
applyCanvas: () => {},
@@ -1478,7 +1479,7 @@ describe('diff algorithm for rrdom', () => {
14781479
applyStyleSheetMutation: () => {},
14791480
};
14801481
rrdom.diff(iframeEl, rrIframeEl, replayer);
1481-
1482+
14821483
iframeEl.contentDocument.documentElement.className =
14831484
'${className.toLowerCase()}';
14841485
iframeEl.contentDocument.childNodes.length === 2 &&
@@ -2000,4 +2001,69 @@ describe('diff algorithm for rrdom', () => {
20002001
expect(nodeMatching(node1, node2, NodeMirror, rrdomMirror)).toBeFalsy();
20012002
});
20022003
});
2004+
2005+
describe('test handleInsertBefore function', () => {
2006+
it('should insert nodeToMove before insertBeforeNode in oldTree for non-style elements', () => {
2007+
const oldTree = document.createElement('div');
2008+
const nodeToMove = document.createElement('div');
2009+
const insertBeforeNode = document.createElement('div');
2010+
oldTree.appendChild(insertBeforeNode);
2011+
2012+
expect(oldTree.children.length).toEqual(1);
2013+
2014+
handleInsertBefore(oldTree, nodeToMove, insertBeforeNode);
2015+
2016+
expect(oldTree.children.length).toEqual(2);
2017+
expect(oldTree.children[0]).toEqual(nodeToMove);
2018+
});
2019+
2020+
it('should not drop inserted styles when moving a style element with inserted styles', async () => {
2021+
function MockCSSStyleSheet() {
2022+
this.replaceSync = jest.fn();
2023+
this.cssRules = [{ cssText: baseStyle }];
2024+
}
2025+
2026+
jest
2027+
.spyOn(window, 'CSSStyleSheet')
2028+
.mockImplementationOnce(MockCSSStyleSheet as any);
2029+
2030+
const baseStyle = 'body {margin: 0;}';
2031+
const insertedStyle = 'div {display: flex;}';
2032+
2033+
document.write('<html></html>');
2034+
2035+
const insertBeforeNode = document.createElement('style');
2036+
document.documentElement.appendChild(insertBeforeNode);
2037+
2038+
const nodeToMove = document.createElement('style');
2039+
nodeToMove.appendChild(document.createTextNode(baseStyle));
2040+
document.documentElement.appendChild(nodeToMove);
2041+
nodeToMove.sheet?.insertRule(insertedStyle);
2042+
2043+
// validate dom prior to moving element
2044+
expect(document.documentElement.children.length).toEqual(4);
2045+
expect(document.documentElement.children[2]).toEqual(insertBeforeNode);
2046+
expect(document.documentElement.children[3]).toEqual(nodeToMove);
2047+
expect(nodeToMove.sheet?.cssRules.length).toEqual(2);
2048+
expect(nodeToMove.sheet?.cssRules[0].cssText).toEqual(insertedStyle);
2049+
expect(nodeToMove.sheet?.cssRules[1].cssText).toEqual(baseStyle);
2050+
2051+
// move the node
2052+
handleInsertBefore(
2053+
document.documentElement,
2054+
nodeToMove,
2055+
insertBeforeNode,
2056+
);
2057+
2058+
// nodeToMove was inserted before
2059+
expect(document.documentElement.children.length).toEqual(4);
2060+
expect(document.documentElement.children[2]).toEqual(nodeToMove);
2061+
expect(document.documentElement.children[3]).toEqual(insertBeforeNode);
2062+
// styles persisted on the moved element
2063+
// w/ document.documentElement.insertBefore(nodeToMove, insertBeforeNode) insertedStyle wouldn't be copied
2064+
expect(nodeToMove.sheet?.cssRules.length).toEqual(2);
2065+
expect(nodeToMove.sheet?.cssRules[0].cssText).toEqual(insertedStyle);
2066+
expect(nodeToMove.sheet?.cssRules[1].cssText).toEqual(baseStyle);
2067+
});
2068+
});
20032069
});

0 commit comments

Comments
 (0)