From 07ff4db2a6d396ff6faac7ba60f5861a08932f2d Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Sat, 17 Oct 2020 10:37:32 +0200 Subject: [PATCH] Add support for StylesheetRule in document fragment (#293) * add failing test * add stylesheet to dom to manipulate the rules * cleanup --- src/replay/index.ts | 21 ++++++ test/events/style-sheet-rule-events.ts | 94 ++++++++++++++++++++++++++ test/replayer.test.ts | 14 ++++ 3 files changed, 129 insertions(+) create mode 100644 test/events/style-sheet-rule-events.ts diff --git a/src/replay/index.ts b/src/replay/index.ts index 4a547f9b..c5c147e5 100644 --- a/src/replay/index.ts +++ b/src/replay/index.ts @@ -746,6 +746,22 @@ export class Replayer { } const styleEl = (target as Node) as HTMLStyleElement; + const parent = ((target.parentNode as unknown) as INode); + const usingVirtualParent = this.fragmentParentMap.has(parent); + let placeholderNode; + + if (usingVirtualParent) { + /** + * styleEl.sheet is only accessible if the styleEl is part of the + * dom. This doesn't work on DocumentFragments so we have to re-add + * it to the dom temporarily. + */ + const domParent = this.fragmentParentMap.get((target.parentNode as unknown) as INode); + placeholderNode = document.createTextNode(''); + parent.replaceChild(placeholderNode, target); + domParent!.appendChild(target); + } + const styleSheet = styleEl.sheet; if (d.adds) { @@ -776,6 +792,11 @@ export class Replayer { } }); } + + if (usingVirtualParent && placeholderNode) { + parent.replaceChild(target, placeholderNode); + } + break; } case IncrementalSource.CanvasMutation: { diff --git a/test/events/style-sheet-rule-events.ts b/test/events/style-sheet-rule-events.ts new file mode 100644 index 00000000..e0e9682e --- /dev/null +++ b/test/events/style-sheet-rule-events.ts @@ -0,0 +1,94 @@ +import { + EventType, + eventWithTime, + IncrementalSource +} from '../../src/types'; + +const now = Date.now(); +const events: eventWithTime[] = [ + { + type: EventType.DomContentLoaded, + data: {}, + timestamp: now, + }, + { + type: EventType.Load, + data: {}, + timestamp: now + 100, + }, + { + type: EventType.Meta, + data: { + href: 'http://localhost', + width: 1000, + height: 800, + }, + timestamp: now + 100, + }, + // full snapshot: + { + "data": { + "node": { + "id": 1, "type": 0, "childNodes": [{ "id": 2, "name": "html", "type": 1, "publicId": "", "systemId": "" }, { + "id": 3, "type": 2, "tagName": "html", "attributes": { "lang": "en" }, "childNodes": [{ + "id": 4, "type": 2, "tagName": "head", "attributes": {}, "childNodes": [ + { + "id": 101, "type": 2, "tagName": "style", "attributes": { "data-jss": "", "data-meta": "sk, Unthemed, Static" }, "childNodes": [{ "id": 102, "type": 3, "isStyle": true, "textContent": "\n.c01x {\n opacity: 1;\n transform: translateX(0);\n}\n" }] + }, + { + "id": 105, "type": 2, "tagName": "style", "attributes": + { "_cssText": ".css-1uxxxx3 { position: fixed; top: 0px; right: 0px; left: 4rem; z-index: 15; flex-shrink: 0; height: 0.25rem; overflow: hidden; background-color: rgb(17, 171, 209); }.css-1c9xxxx { height: 0.25rem; background-color: rgb(190, 232, 242); opacity: 0; transition: opacity 0.5s ease 0s; }.css-lsxxx { padding-left: 4rem; }", "data-emotion": "css" }, "childNodes": [{ "id": 106, "type": 3, "isStyle": true, "textContent": "" }] + }] + }, { + "id": 107, "type": 2, "tagName": "body", "attributes": {}, "childNodes": [] + }] + }] + }, "initialOffset": { "top": 0, "left": 0 } + }, + "type": EventType.FullSnapshot, + "timestamp": now + 100 + }, + // mutation that adds stylesheet + { + "data": { + "adds": [ + { + "node": { + "id": 255, "type": 2, "tagName": "style", "attributes": { "data-jss": "", "data-meta": "Col, Themed, Dynamic" }, "childNodes": [] + }, + "nextId": 101, + "parentId": 4 + }, + { + "node": { + "id": 256, "type": 3, "isStyle": true, "textContent": "\n.c011xx {\n padding: 1.3125rem;\n flex: none;\n width: 100%;\n}\n" + }, + "nextId": null, + "parentId": 255 + }, + ], + "texts": [], + "source": IncrementalSource.Mutation, + "removes": [], + "attributes": [] + }, + "type": EventType.IncrementalSnapshot, + "timestamp": now + 500 + }, + // adds StyleSheetRule + { + "data": { + "id": 105, "adds": [ + { + "rule": ".css-1fbxx79{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;min-width:60rem;min-height:100vh;}", + "index": 2 + } + ], + "source": IncrementalSource.StyleSheetRule + }, + "type": EventType.IncrementalSnapshot, + "timestamp": now + 1000 + } +]; + +export default events; \ No newline at end of file diff --git a/test/replayer.test.ts b/test/replayer.test.ts index 97fec985..af7a3243 100644 --- a/test/replayer.test.ts +++ b/test/replayer.test.ts @@ -10,6 +10,7 @@ import { sampleEvents as events, sampleStyleSheetRemoveEvents as stylesheetRemoveEvents } from './utils'; +import styleSheetRuleEvents from './events/style-sheet-rule-events'; interface ISuite extends Suite { code: string; @@ -135,6 +136,19 @@ describe('replayer', function (this: ISuite) { expect(currentState).to.equal('paused'); }); + it('can fast forward past StyleSheetRule changes on virtual elements', async () => { + await this.page.evaluate(`events = ${JSON.stringify(styleSheetRuleEvents)}`); + const actionLength = await this.page.evaluate(` + const { Replayer } = rrweb; + const replayer = new Replayer(events); + replayer.play(1500); + replayer['timer']['actions'].length; + `); + expect(actionLength).to.equal( + styleSheetRuleEvents.filter((e) => e.timestamp - styleSheetRuleEvents[0].timestamp >= 1500).length, + ); + }); + it('can handle removing style elements', async () => { await this.page.evaluate(`events = ${JSON.stringify(stylesheetRemoveEvents)}`); const actionLength = await this.page.evaluate(`