Skip to content

Commit

Permalink
fix: inserted styles lost when moving elements
Browse files Browse the repository at this point in the history
fix code for nodejs tests
change fix direction to avoid issues with duplicate styles

change fix direction to avoid issues with duplicate styles
  • Loading branch information
jaj1014 committed Nov 27, 2023
1 parent 8aea5b0 commit 13ad056
Show file tree
Hide file tree
Showing 3 changed files with 286 additions and 9 deletions.
63 changes: 58 additions & 5 deletions packages/rrdom/src/diff.ts
Original file line number Diff line number Diff line change
Expand Up @@ -372,7 +372,7 @@ function diffChildren(
nodeMatching(oldStartNode, newEndNode, replayer.mirror, rrnodeMirror)
) {
try {
oldTree.insertBefore(oldStartNode, oldEndNode.nextSibling);
handleInsertBefore(oldTree, oldStartNode, oldEndNode.nextSibling);
} catch (e) {
console.warn(e);
}
Expand All @@ -383,7 +383,7 @@ function diffChildren(
nodeMatching(oldEndNode, newStartNode, replayer.mirror, rrnodeMirror)
) {
try {
oldTree.insertBefore(oldEndNode, oldStartNode);
handleInsertBefore(oldTree, oldEndNode, oldStartNode)
} catch (e) {
console.warn(e);
}
Expand All @@ -408,7 +408,7 @@ function diffChildren(
nodeMatching(nodeToMove, newStartNode, replayer.mirror, rrnodeMirror)
) {
try {
oldTree.insertBefore(nodeToMove, oldStartNode);
handleInsertBefore(oldTree, nodeToMove, oldStartNode);
} catch (e) {
console.warn(e);
}
Expand Down Expand Up @@ -442,7 +442,7 @@ function diffChildren(
}

try {
oldTree.insertBefore(newNode, oldStartNode || null);
handleInsertBefore(oldTree, newNode, oldStartNode || null);
} catch (e) {
console.warn(e);
}
Expand All @@ -464,7 +464,8 @@ function diffChildren(
rrnodeMirror,
);
try {
oldTree.insertBefore(newNode, referenceNode);
// oldTree.insertBefore(newNode, referenceNode);
handleInsertBefore(oldTree, newNode, referenceNode);
} catch (e) {
console.warn(e);
}
Expand Down Expand Up @@ -572,3 +573,55 @@ export function nodeMatching(
if (node1Id === -1 || node1Id !== node2Id) return false;
return sameNodeType(node1, node2);
}

/**
* Copies CSSRules and their position from HTML style element which don't exist in it's innerText
*/
export function getInsertedStylesFromElement(styleElement: HTMLStyleElement): Array<{ index: number, cssRuleText: string}> | undefined {
const elementCssRules = styleElement.sheet?.cssRules;
if (!elementCssRules || !elementCssRules.length) return;
// style sheet w/ innerText styles to diff with actual and get only inserted styles
const tempStyleSheet = new CSSStyleSheet();
tempStyleSheet.replaceSync(styleElement.innerText);

const innerTextStylesMap: { [key: string]: CSSRule } = {};

for (let i = 0; i < tempStyleSheet.cssRules.length; i++) {
innerTextStylesMap[tempStyleSheet.cssRules[i].cssText] = tempStyleSheet.cssRules[i];
}

const insertedStylesStyleSheet = [];

for (let i = 0; i < elementCssRules?.length; i++) {
const cssRuleText = elementCssRules[i].cssText;

if (!innerTextStylesMap[cssRuleText]) {
insertedStylesStyleSheet.push({
index: i,
cssRuleText
});
}
}

return insertedStylesStyleSheet;
}

/**
* Conditionally copy insertedStyles for STYLE nodes and apply after calling insertBefore'
* For non-STYLE nodes, just insertBefore
*/
function handleInsertBefore(oldTree: Node, nodeToMove: Node, insertBeforeNode: Node | null): void {
let insertedStyles;

if (nodeToMove.nodeName === 'STYLE') {
insertedStyles = getInsertedStylesFromElement(nodeToMove as HTMLStyleElement);
}

oldTree.insertBefore(nodeToMove, insertBeforeNode);

if (insertedStyles && insertedStyles.length) {
insertedStyles.forEach(({ cssRuleText, index }) => {
(nodeToMove as HTMLStyleElement).sheet?.insertRule(cssRuleText, index);
});
}
}
205 changes: 205 additions & 0 deletions packages/rrweb/test/events/moving-style-sheet-on-diff.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
import { EventType, IncrementalSource } from '@rrweb/types';
import type { eventWithTime } from '@rrweb/types';

const now = Date.now();
const events: eventWithTime[] = [
{
type: EventType.DomContentLoaded,
data: {},
timestamp: now,
},
{
type: EventType.Load,
data: {},
timestamp: now + 10,
},
{
type: EventType.Meta,
data: {
href: 'http://localhost',
width: 1000,
height: 800,
},
timestamp: now + 10,
},
// full snapshot:
{
data: {
node: {
type: 0,
childNodes: [
{
type: 1,
name: 'html',
publicId: '',
systemId: '',
id: 2,
},
{
type: 2,
tagName: 'html',
attributes: {
lang: 'en',
},
childNodes: [
{
type: 2,
tagName: 'head',
attributes: {},
childNodes: [
{
type: 2,
tagName: 'style',
attributes: {},
childNodes: [
{
type: 3,
textContent:
'#wrapper { width: 200px; margin: 50px auto; background-color: gainsboro; padding: 20px; }.target-element { padding: 12px; margin-top: 12px; }',
isStyle: true,
id: 6,
},
],
id: 5,
},
{
type: 2,
tagName: 'style',
attributes: {},
childNodes: [
{
type: 3,
textContent:
'.new-element-class { font-size: 32px; color: tomato; }',
isStyle: true,
id: 8,
},
],
id: 7,
},
],
id: 4,
},
{
type: 2,
tagName: 'body',
attributes: {},
childNodes: [
{
type: 2,
tagName: 'div',
attributes: {
id: 'wrapper',
},
childNodes: [
{
type: 2,
tagName: 'div',
attributes: {
class: 'target-element',
},
childNodes: [
{
type: 2,
tagName: 'p',
attributes: {
class: 'target-element-child',
},
childNodes: [
{
type: 3,
textContent: 'Element to style',
id: 113,
},
],
id: 12,
},
],
id: 11,
},
],
id: 10,
},
],
id: 9,
},
],
id: 3,
},
],
id: 1,
},
initialOffset: {
left: 0,
top: 0,
},
},
type: EventType.FullSnapshot,
timestamp: now + 20,
},
// 1st mutation that applies StyleSheetRule
{
type: EventType.IncrementalSnapshot,
data: {
source: IncrementalSource.StyleSheetRule,
id: 5,
adds: [
{
rule: '.target-element{background-color:teal;}',
},
],
},
timestamp: now + 30,
},
// 2nd mutation inserts new style element to trigger other style element to get moved in diff
{
type: EventType.IncrementalSnapshot,
data: {
source: IncrementalSource.Mutation,
texts: [],
attributes: [],
removes: [
{ parentId: 4, id: 7 }
],
adds: [
{
parentId: 4,
nextId: 5,
node: {
type: 2,
tagName: 'style',
attributes: {},
childNodes: [],
id: 98,
},
},
{
parentId: 98,
nextId: null,
node: {
type: 3,
textContent:
'.new-element-class { font-size: 32px; color: tomato; }',
isStyle: true,
id: 99,
},
},
],
},
timestamp: now + 2000,
},
// dummy event to have somewhere to skip
{
data: {
adds: [],
texts: [],
source: IncrementalSource.Mutation,
removes: [],
attributes: [],
},
type: EventType.IncrementalSnapshot,
timestamp: now + 3000,
},
];

export default events;
27 changes: 23 additions & 4 deletions packages/rrweb/test/replayer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
waitForRAF,
} from './utils';
import styleSheetRuleEvents from './events/style-sheet-rule-events';
import movingStyleSheetOnDiff from './events/moving-style-sheet-on-diff';
import orderingEvents from './events/ordering';
import scrollEvents from './events/scroll';
import inputEvents from './events/input';
Expand Down Expand Up @@ -173,6 +174,24 @@ describe('replayer', function () {
await assertDomSnapshot(page);
});

it('should persist StyleSheetRule changes when skipping triggers parent style element to move in diff', async () => {
await page.evaluate(`events = ${JSON.stringify(movingStyleSheetOnDiff)}`);

const result = await page.evaluate(`
const { Replayer } = rrweb;
const replayer = new Replayer(events);
replayer.pause(3000);
const rules = [...replayer.iframe.contentDocument.styleSheets].map(
(sheet) => [...sheet.rules],
).flat();
rules.map(rule => rule.cssText);
rules.some((x) => x.cssText === '.target-element { background-color: teal; }');
`);

expect(result).toEqual(true);
});

it('should apply fast forwarded StyleSheetRules that where added', async () => {
await page.evaluate(`events = ${JSON.stringify(styleSheetRuleEvents)}`);
const result = await page.evaluate(`
Expand Down Expand Up @@ -224,7 +243,7 @@ describe('replayer', function () {
await waitForRAF(page);

/** check the second selection event */
[startOffset, endOffset] = (await page.evaluate(`
[startOffset, endOffset] = (await page.evaluate(`
replayer.pause(410);
var range = replayer.iframe.contentDocument.getSelection().getRangeAt(0);
[range.startOffset, range.endOffset];
Expand Down Expand Up @@ -656,7 +675,7 @@ describe('replayer', function () {
events = ${JSON.stringify(canvasInIframe)};
const { Replayer } = rrweb;
var replayer = new Replayer(events,{showDebug:true});
replayer.pause(550);
replayer.pause(550);
`);
const replayerIframe = await page.$('iframe');
const contentDocument = await replayerIframe!.contentFrame()!;
Expand Down Expand Up @@ -718,7 +737,7 @@ describe('replayer', function () {
const replayer = new Replayer(events);
replayer.play();
`);
await page.waitForTimeout(50);
await page.waitForTimeout(150);

await assertDomSnapshot(page);
});
Expand All @@ -742,7 +761,7 @@ describe('replayer', function () {
await page.evaluate(`
const { Replayer } = rrweb;
let replayer = new Replayer(events);
replayer.play();
replayer.play();
`);

const replayerWrapperClassName = 'replayer-wrapper';
Expand Down

0 comments on commit 13ad056

Please sign in to comment.