From 14d9825fee87b53d7496b79026bbec555c789bd3 Mon Sep 17 00:00:00 2001 From: Jon Perl Date: Sun, 25 Aug 2019 11:03:00 -0600 Subject: [PATCH 1/8] hack together stylesheet observer --- src/record/index.ts | 11 ++++++++++- src/record/observer.ts | 20 ++++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/src/record/index.ts b/src/record/index.ts index c769a4d4dd..696fcf017b 100644 --- a/src/record/index.ts +++ b/src/record/index.ts @@ -1,5 +1,5 @@ import { snapshot } from 'rrweb-snapshot'; -import initObservers from './observer'; +import initObservers, { observeStylesheet } from './observer'; import { mirror, on, @@ -80,9 +80,11 @@ function record(options: recordOptions = {}): listenerHandler | undefined { inlineStylesheet, maskAllInputs, ); + if (!node) { return console.warn('Failed to snapshot the document'); } + mirror.map = idNodeMap; wrappedEmit( wrapEvent({ @@ -181,6 +183,13 @@ function record(options: recordOptions = {}): listenerHandler | undefined { inlineStylesheet, }), ); + + for (var i in mirror.map) { + const node = mirror.map[i]; + if ((node as any).tagName == 'STYLE') { + observeStylesheet(node as any); + } + } }; if ( document.readyState === 'interactive' || diff --git a/src/record/observer.ts b/src/record/observer.ts index 24cfa71ae4..08eb0715b6 100644 --- a/src/record/observer.ts +++ b/src/record/observer.ts @@ -506,6 +506,26 @@ function initInputObserver( }; } +export function observeStylesheet(styleRoot: HTMLLinkElement) { + const handler: ProxyHandler = { + apply: function apply(target, thisArg, argumentsList) { + const result = target.apply(thisArg, argumentsList); + styleRoot.innerHTML = Array.from(thisArg.rules, x => x.cssText).join( + '\n', + ); + + const sheet = styleRoot.sheet as CSSStyleSheet; + sheet.insertRule = new Proxy(sheet.insertRule, handler); + return result; + }, + }; + + const sheet = styleRoot.sheet as CSSStyleSheet; + const proxy = new Proxy(sheet.insertRule, handler); + sheet.insertRule = proxy; + sheet.insertRule('.rr_track_on {}'); +} + export default function initObservers(o: observerParam): listenerHandler { const mutationObserver = initMutationObserver( o.mutationCb, From 3a65d0b9133cf18104b765fcd1770c77f95cbe29 Mon Sep 17 00:00:00 2001 From: David Cramer Date: Fri, 21 Feb 2020 11:38:04 -0800 Subject: [PATCH 2/8] Add test coverage for insertRule/deleteRule on stylesheets --- test/__snapshots__/record.test.ts.snap | 95 ++++++++++++++++++++++++++ test/record.test.ts | 47 +++++++++---- test/replayer.test.ts | 12 ++-- 3 files changed, 137 insertions(+), 17 deletions(-) diff --git a/test/__snapshots__/record.test.ts.snap b/test/__snapshots__/record.test.ts.snap index 7ecfb7003e..91c4ada194 100644 --- a/test/__snapshots__/record.test.ts.snap +++ b/test/__snapshots__/record.test.ts.snap @@ -331,3 +331,98 @@ exports[`custom-event 1`] = ` } ]" `; + +exports[`stylesheet-rules 1`] = ` +"[ + { + \\"type\\": 4, + \\"data\\": { + \\"href\\": \\"about:blank\\", + \\"width\\": 800, + \\"height\\": 600 + } + }, + { + \\"type\\": 2, + \\"data\\": { + \\"node\\": { + \\"type\\": 0, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"html\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"head\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 3 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 5 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"input\\", + \\"attributes\\": { + \\"type\\": \\"text\\" + }, + \\"childNodes\\": [], + \\"id\\": 6 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\\\n \\", + \\"id\\": 7 + } + ], + \\"id\\": 4 + } + ], + \\"id\\": 2 + } + ], + \\"id\\": 1 + }, + \\"initialOffset\\": { + \\"left\\": 0, + \\"top\\": 0 + } + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [], + \\"removes\\": [], + \\"adds\\": [ + { + \\"parentId\\": 3, + \\"previousId\\": null, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"style\\", + \\"attributes\\": { + \\"_cssText\\": \\"body { background: rgb(0, 0, 0); }\\" + }, + \\"childNodes\\": [], + \\"id\\": 8 + } + } + ] + } + } +]" +`; diff --git a/test/record.test.ts b/test/record.test.ts index e321314409..498cc76c51 100644 --- a/test/record.test.ts +++ b/test/record.test.ts @@ -28,6 +28,8 @@ interface IWindow extends Window { emit: (e: eventWithTime) => undefined; } +type WindowAndGlobalThis = IWindow & typeof globalThis; + describe('record', function(this: ISuite) { before(async () => { this.browser = await puppeteer.launch({ @@ -63,18 +65,18 @@ describe('record', function(this: ISuite) { }); afterEach(async () => { - await this.page.close(); + if (this.page) await this.page.close(); }); after(async () => { - await this.browser.close(); + if (this.browser) await this.browser.close(); }); it('will only have one full snapshot without checkout config', async () => { await this.page.evaluate(() => { - const { record } = (window as IWindow).rrweb; + const { record } = (window as WindowAndGlobalThis).rrweb; record({ - emit: (window as IWindow).emit, + emit: (window as WindowAndGlobalThis).emit, }); }); let count = 30; @@ -97,9 +99,9 @@ describe('record', function(this: ISuite) { it('can checkout full snapshot by count', async () => { await this.page.evaluate(() => { - const { record } = (window as IWindow).rrweb; + const { record } = (window as WindowAndGlobalThis).rrweb; record({ - emit: (window as IWindow).emit, + emit: (window as WindowAndGlobalThis).emit, checkoutEveryNth: 10, }); }); @@ -127,9 +129,9 @@ describe('record', function(this: ISuite) { it('can checkout full snapshot by time', async () => { await this.page.evaluate(() => { - const { record } = (window as IWindow).rrweb; + const { record } = (window as WindowAndGlobalThis).rrweb; record({ - emit: (window as IWindow).emit, + emit: (window as WindowAndGlobalThis).emit, checkoutEveryNms: 500, }); }); @@ -158,9 +160,9 @@ describe('record', function(this: ISuite) { it('is safe to checkout during async callbacks', async () => { await this.page.evaluate(() => { - const { record } = (window as IWindow).rrweb; + const { record } = (window as WindowAndGlobalThis).rrweb; record({ - emit: (window as IWindow).emit, + emit: (window as WindowAndGlobalThis).emit, checkoutEveryNth: 2, }); const p = document.createElement('p'); @@ -184,9 +186,9 @@ describe('record', function(this: ISuite) { it('can add custom event', async () => { await this.page.evaluate(() => { - const { record, addCustomEvent } = (window as IWindow).rrweb; + const { record, addCustomEvent } = (window as WindowAndGlobalThis).rrweb; record({ - emit: (window as IWindow).emit, + emit: (window as WindowAndGlobalThis).emit, }); addCustomEvent('tag1', 1); addCustomEvent<{ a: string }>('tag2', { @@ -196,4 +198,25 @@ describe('record', function(this: ISuite) { await this.page.waitFor(50); assertSnapshot(this.events, __filename, 'custom-event'); }); + + it('captures stylesheet rules', async () => { + await this.page.evaluate(() => { + const { record } = (window as WindowAndGlobalThis).rrweb; + + record({ + emit: (window as WindowAndGlobalThis).emit, + }); + + const styleElement = document.createElement('style'); + document.head.appendChild(styleElement); + + const styleSheet = styleElement.sheet; + const ruleIdx0 = styleSheet.insertRule('body { background: #000; }'); + styleSheet.insertRule('body { color: #fff; }'); + styleSheet.deleteRule(ruleIdx0); + styleSheet.insertRule('body { color: #ccc; }'); + }); + await this.page.waitFor(50); + assertSnapshot(this.events, __filename, 'stylesheet-rules'); + }); }); diff --git a/test/replayer.test.ts b/test/replayer.test.ts index ee591f4fe6..c7e035d568 100644 --- a/test/replayer.test.ts +++ b/test/replayer.test.ts @@ -114,6 +114,8 @@ interface IWindow extends Window { }; } +type WindowAndGlobalThis = IWindow & typeof globalThis; + interface ISuite extends Suite { code: string; browser: puppeteer.Browser; @@ -151,7 +153,7 @@ describe('replayer', function(this: ISuite) { it('can get meta data', async () => { const meta = await this.page.evaluate(() => { - const { Replayer } = (window as IWindow).rrweb; + const { Replayer } = (window as WindowAndGlobalThis).rrweb; const replayer = new Replayer(events); return replayer.getMetaData(); }); @@ -162,7 +164,7 @@ describe('replayer', function(this: ISuite) { it('will start actions when play', async () => { const actionLength = await this.page.evaluate(() => { - const { Replayer } = (window as IWindow).rrweb; + const { Replayer } = (window as WindowAndGlobalThis).rrweb; const replayer = new Replayer(events); replayer.play(); return replayer['timer']['actions'].length; @@ -172,7 +174,7 @@ describe('replayer', function(this: ISuite) { it('will clean actions when pause', async () => { const actionLength = await this.page.evaluate(() => { - const { Replayer } = (window as IWindow).rrweb; + const { Replayer } = (window as WindowAndGlobalThis).rrweb; const replayer = new Replayer(events); replayer.play(); replayer.pause(); @@ -183,7 +185,7 @@ describe('replayer', function(this: ISuite) { it('can play at any time offset', async () => { const actionLength = await this.page.evaluate(() => { - const { Replayer } = (window as IWindow).rrweb; + const { Replayer } = (window as WindowAndGlobalThis).rrweb; const replayer = new Replayer(events); replayer.play(1500); return replayer['timer']['actions'].length; @@ -195,7 +197,7 @@ describe('replayer', function(this: ISuite) { it('can resume at any time offset', async () => { const actionLength = await this.page.evaluate(() => { - const { Replayer } = (window as IWindow).rrweb; + const { Replayer } = (window as WindowAndGlobalThis).rrweb; const replayer = new Replayer(events); replayer.play(1500); replayer.pause(); From cbc64dfc6f08d40e5aae2aaeb70d54a0dacd7907 Mon Sep 17 00:00:00 2001 From: David Cramer Date: Fri, 21 Feb 2020 11:40:38 -0800 Subject: [PATCH 3/8] Add new observers --- src/record/index.ts | 19 +++++++++------- src/record/observer.ts | 49 ++++++++++++++++++++++++++++-------------- src/types.ts | 17 +++++++++++++++ 3 files changed, 61 insertions(+), 24 deletions(-) diff --git a/src/record/index.ts b/src/record/index.ts index 696fcf017b..6e7bf00d9b 100644 --- a/src/record/index.ts +++ b/src/record/index.ts @@ -1,5 +1,5 @@ import { snapshot } from 'rrweb-snapshot'; -import initObservers, { observeStylesheet } from './observer'; +import initObservers from './observer'; import { mirror, on, @@ -177,19 +177,22 @@ function record(options: recordOptions = {}): listenerHandler | undefined { }, }), ), + styleSheetRuleCb: v => + wrappedEmit( + wrapEvent({ + type: EventType.IncrementalSnapshot, + data: { + source: IncrementalSource.StyleSheetRule, + ...v, + }, + }), + ), blockClass, ignoreClass, maskAllInputs, inlineStylesheet, }), ); - - for (var i in mirror.map) { - const node = mirror.map[i]; - if ((node as any).tagName == 'STYLE') { - observeStylesheet(node as any); - } - } }; if ( document.readyState === 'interactive' || diff --git a/src/record/observer.ts b/src/record/observer.ts index 08eb0715b6..9e016628e0 100644 --- a/src/record/observer.ts +++ b/src/record/observer.ts @@ -21,6 +21,7 @@ import { MouseInteractions, listenerHandler, scrollCallback, + styleSheetRuleCallback, viewportResizeCallback, inputValue, inputCallback, @@ -506,24 +507,36 @@ function initInputObserver( }; } -export function observeStylesheet(styleRoot: HTMLLinkElement) { - const handler: ProxyHandler = { - apply: function apply(target, thisArg, argumentsList) { - const result = target.apply(thisArg, argumentsList); - styleRoot.innerHTML = Array.from(thisArg.rules, x => x.cssText).join( - '\n', - ); +function initStyleSheetObserver(cb: styleSheetRuleCallback): listenerHandler { + return () => { + const insertRule = CSSStyleSheet.prototype.insertRule; + CSSStyleSheet.prototype.insertRule = function( + rule: string, + index?: number | undefined, + ) { + cb({ + // id: mirror.getId(target as INode); + rule, + index, + }); + return insertRule.apply(rule, index); + }; - const sheet = styleRoot.sheet as CSSStyleSheet; - sheet.insertRule = new Proxy(sheet.insertRule, handler); - return result; - }, - }; + const deleteRule = CSSStyleSheet.prototype.deleteRule; + CSSStyleSheet.prototype.deleteRule = function(index: number) { + cb({ + index, + }); + return deleteRule.apply(index); + }; - const sheet = styleRoot.sheet as CSSStyleSheet; - const proxy = new Proxy(sheet.insertRule, handler); - sheet.insertRule = proxy; - sheet.insertRule('.rr_track_on {}'); + // for (var i in mirror.map) { + // const node = mirror.map[i]; + // if ((node as any).tagName == 'STYLE') { + // observeStylesheet(node as any); + // } + // } + }; } export default function initObservers(o: observerParam): listenerHandler { @@ -546,6 +559,9 @@ export default function initObservers(o: observerParam): listenerHandler { o.ignoreClass, o.maskAllInputs, ); + + const styleSheetObserver = initStyleSheetObserver(o.styleSheetRuleCb); + return () => { mutationObserver.disconnect(); mousemoveHandler(); @@ -553,5 +569,6 @@ export default function initObservers(o: observerParam): listenerHandler { scrollHandler(); viewportResizeHandler(); inputHandler(); + styleSheetObserver(); }; } diff --git a/src/types.ts b/src/types.ts index de82467c4b..8fb1806ca0 100644 --- a/src/types.ts +++ b/src/types.ts @@ -57,6 +57,7 @@ export enum IncrementalSource { MouseMove, MouseInteraction, Scroll, + StyleSheetRule, ViewportResize, Input, TouchMove, @@ -132,6 +133,7 @@ export type observerParam = { ignoreClass: string; maskAllInputs: boolean; inlineStylesheet: boolean; + styleSheetRuleCb: styleSheetRuleCallback; }; export type textCursor = { @@ -219,6 +221,21 @@ export type scrollPosition = { export type scrollCallback = (p: scrollPosition) => void; +export type styleSheetAddRule = { + // id: number; + rule: string; + index?: number | undefined; +}; + +export type styleSheetDeleteRule = { + // id: number; + index: number; +}; + +export type styleSheetRuleCallback = ( + s: styleSheetAddRule | styleSheetDeleteRule, +) => void; + export type viewportResizeDimention = { width: number; height: number; From 5e7f496bca244c11ac2e4ee08ae0dab8870d32e6 Mon Sep 17 00:00:00 2001 From: David Cramer Date: Fri, 21 Feb 2020 11:47:12 -0800 Subject: [PATCH 4/8] update patch based on changes to master --- src/record/observer.ts | 9 ++++++++- src/types.ts | 1 + 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/record/observer.ts b/src/record/observer.ts index 51385fef75..619da9b0ab 100644 --- a/src/record/observer.ts +++ b/src/record/observer.ts @@ -581,6 +581,7 @@ function mergeHooks(o: observerParam, hooks: hooksParam) { viewportResizeCb, inputCb, mediaInteractionCb, + styleSheetRuleCb, } = o; o.mutationCb = (...p: Arguments) => { if (hooks.mutation) { @@ -624,6 +625,12 @@ function mergeHooks(o: observerParam, hooks: hooksParam) { } mediaInteractionCb(...p); }; + o.styleSheetRuleCb = (...p: Arguments) => { + if (hooks.styleSheetRule) { + hooks.styleSheetRule(...p); + } + styleSheetRuleCb(...p); + }; } export default function initObservers( @@ -664,6 +671,6 @@ export default function initObservers( viewportResizeHandler(); inputHandler(); mediaInteractionHandler(); - styleSheetObserver(); + // styleSheetObserver(); }; } diff --git a/src/types.ts b/src/types.ts index 5c5f9c38c6..1fadc18517 100644 --- a/src/types.ts +++ b/src/types.ts @@ -154,6 +154,7 @@ export type hooksParam = { viewportResize?: viewportResizeCallback; input?: inputCallback; mediaInteaction?: mediaInteractionCallback; + styleSheetRule?: styleSheetRuleCallback; }; export type textCursor = { From 1082f5e87edf2c43b4c6bfb93a27a4bb87910820 Mon Sep 17 00:00:00 2001 From: David Cramer Date: Fri, 21 Feb 2020 14:42:26 -0800 Subject: [PATCH 5/8] Functioning event recording --- src/record/index.ts | 4 +- src/record/observer.ts | 53 ++++++++++----------- src/types.ts | 21 +++++--- test/__snapshots__/integration.test.ts.snap | 2 +- test/__snapshots__/record.test.ts.snap | 48 +++++++++++++++---- test/integration.test.ts | 11 +---- test/record.test.ts | 26 +++++----- test/replayer.test.ts | 6 +-- test/utils.ts | 12 +++++ 9 files changed, 114 insertions(+), 69 deletions(-) diff --git a/src/record/index.ts b/src/record/index.ts index 75ce36eebf..0b611361a5 100644 --- a/src/record/index.ts +++ b/src/record/index.ts @@ -190,13 +190,13 @@ function record(options: recordOptions = {}): listenerHandler | undefined { }, }), ), - styleSheetRuleCb: v => + styleSheetRuleCb: r => wrappedEmit( wrapEvent({ type: EventType.IncrementalSnapshot, data: { source: IncrementalSource.StyleSheetRule, - ...v, + ...r, }, }), ), diff --git a/src/record/observer.ts b/src/record/observer.ts index 619da9b0ab..45a56438ae 100644 --- a/src/record/observer.ts +++ b/src/record/observer.ts @@ -521,34 +521,33 @@ function initInputObserver( } function initStyleSheetObserver(cb: styleSheetRuleCallback): listenerHandler { - return () => { - const insertRule = CSSStyleSheet.prototype.insertRule; - CSSStyleSheet.prototype.insertRule = function( - rule: string, - index?: number | undefined, - ) { - cb({ - // id: mirror.getId(target as INode); - rule, - index, - }); - return insertRule.apply(rule, index); - }; + const insertRule = CSSStyleSheet.prototype.insertRule; + CSSStyleSheet.prototype.insertRule = function( + rule: string, + index?: number | undefined, + ) { + console.log('insert rule'); + cb({ + id: mirror.getId(this as INode), + rule, + index, + }); + return insertRule.apply(this, arguments); + }; - const deleteRule = CSSStyleSheet.prototype.deleteRule; - CSSStyleSheet.prototype.deleteRule = function(index: number) { - cb({ - index, - }); - return deleteRule.apply(index); - }; + const deleteRule = CSSStyleSheet.prototype.deleteRule; + CSSStyleSheet.prototype.deleteRule = function(index: number) { + console.error('delete rule'); + cb({ + id: mirror.getId(this as INode), + index, + }); + return deleteRule.apply(this, arguments); + }; - // for (var i in mirror.map) { - // const node = mirror.map[i]; - // if ((node as any).tagName == 'STYLE') { - // observeStylesheet(node as any); - // } - // } + return () => { + CSSStyleSheet.prototype.insertRule = insertRule; + CSSStyleSheet.prototype.deleteRule = deleteRule; }; } @@ -671,6 +670,6 @@ export default function initObservers( viewportResizeHandler(); inputHandler(); mediaInteractionHandler(); - // styleSheetObserver(); + styleSheetObserver(); }; } diff --git a/src/types.ts b/src/types.ts index 1fadc18517..5586a76f91 100644 --- a/src/types.ts +++ b/src/types.ts @@ -52,16 +52,18 @@ export type customEvent = { }; }; +export type styleSheetEvent = {}; + export enum IncrementalSource { Mutation, MouseMove, MouseInteraction, Scroll, - StyleSheetRule, ViewportResize, Input, TouchMove, MediaInteraction, + StyleSheetRule, } export type mutationData = { @@ -94,6 +96,10 @@ export type mediaInteractionData = { source: IncrementalSource.MediaInteraction; } & mediaInteractionParam; +export type styleSheetRuleData = { + source: IncrementalSource.StyleSheetRule; +} & styleSheetRuleParam; + export type incrementalData = | mutationData | mousemoveData @@ -101,7 +107,8 @@ export type incrementalData = | scrollData | viewportResizeData | inputData - | mediaInteractionData; + | mediaInteractionData + | styleSheetRuleData; export type event = | domContentLoadedEvent @@ -243,19 +250,19 @@ export type scrollPosition = { export type scrollCallback = (p: scrollPosition) => void; export type styleSheetAddRule = { - // id: number; + id: number; rule: string; index?: number | undefined; }; export type styleSheetDeleteRule = { - // id: number; + id: number; index: number; }; -export type styleSheetRuleCallback = ( - s: styleSheetAddRule | styleSheetDeleteRule, -) => void; +export type styleSheetRuleParam = styleSheetAddRule | styleSheetDeleteRule; + +export type styleSheetRuleCallback = (s: styleSheetRuleParam) => void; export type viewportResizeDimention = { width: number; diff --git a/test/__snapshots__/integration.test.ts.snap b/test/__snapshots__/integration.test.ts.snap index c16d379a20..afd9ce965f 100644 --- a/test/__snapshots__/integration.test.ts.snap +++ b/test/__snapshots__/integration.test.ts.snap @@ -286,7 +286,7 @@ exports[`block 1`] = ` \\"attributes\\": { \\"class\\": \\"rr-block\\", \\"rr_width\\": \\"1904px\\", - \\"rr_height\\": \\"21px\\" + \\"rr_height\\": \\"19px\\" }, \\"childNodes\\": [], \\"id\\": 18 diff --git a/test/__snapshots__/record.test.ts.snap b/test/__snapshots__/record.test.ts.snap index 91c4ada194..b260b2d78a 100644 --- a/test/__snapshots__/record.test.ts.snap +++ b/test/__snapshots__/record.test.ts.snap @@ -6,8 +6,8 @@ exports[`async-checkout 1`] = ` \\"type\\": 4, \\"data\\": { \\"href\\": \\"about:blank\\", - \\"width\\": 800, - \\"height\\": 600 + \\"width\\": 1920, + \\"height\\": 1080 } }, { @@ -132,8 +132,8 @@ exports[`async-checkout 1`] = ` \\"type\\": 4, \\"data\\": { \\"href\\": \\"about:blank\\", - \\"width\\": 800, - \\"height\\": 600 + \\"width\\": 1920, + \\"height\\": 1080 } }, { @@ -252,8 +252,8 @@ exports[`custom-event 1`] = ` \\"type\\": 4, \\"data\\": { \\"href\\": \\"about:blank\\", - \\"width\\": 800, - \\"height\\": 600 + \\"width\\": 1920, + \\"height\\": 1080 } }, { @@ -338,8 +338,8 @@ exports[`stylesheet-rules 1`] = ` \\"type\\": 4, \\"data\\": { \\"href\\": \\"about:blank\\", - \\"width\\": 800, - \\"height\\": 600 + \\"width\\": 1920, + \\"height\\": 1080 } }, { @@ -399,6 +399,14 @@ exports[`stylesheet-rules 1`] = ` } } }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 8, + \\"id\\": -1, + \\"rule\\": \\"body { background: #000; }\\" + } + }, { \\"type\\": 3, \\"data\\": { @@ -423,6 +431,30 @@ exports[`stylesheet-rules 1`] = ` } ] } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 8, + \\"id\\": -1, + \\"rule\\": \\"body { color: #fff; }\\" + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 8, + \\"id\\": -1, + \\"index\\": 0 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 8, + \\"id\\": -1, + \\"rule\\": \\"body { color: #ccc; }\\" + } } ]" `; diff --git a/test/integration.test.ts b/test/integration.test.ts index b308a2e72d..fe5b9c8622 100644 --- a/test/integration.test.ts +++ b/test/integration.test.ts @@ -1,7 +1,7 @@ import * as fs from 'fs'; import * as path from 'path'; import * as puppeteer from 'puppeteer'; -import { assertSnapshot } from './utils'; +import { assertSnapshot, launchPuppeteer } from './utils'; import { Suite } from 'mocha'; import { recordOptions } from '../src/types'; @@ -35,14 +35,7 @@ describe('record integration tests', function(this: ISuite) { }; before(async () => { - this.browser = await puppeteer.launch({ - defaultViewport: { - width: 1920, - height: 1080, - }, - headless: false, - args: ['--no-sandbox'], - }); + this.browser = await launchPuppeteer(); const bundlePath = path.resolve(__dirname, '../dist/rrweb.min.js'); this.code = fs.readFileSync(bundlePath, 'utf8'); diff --git a/test/record.test.ts b/test/record.test.ts index b77ebd86b7..4d9744b2f6 100644 --- a/test/record.test.ts +++ b/test/record.test.ts @@ -10,7 +10,7 @@ import { eventWithTime, EventType, } from '../src/types'; -import { assertSnapshot } from './utils'; +import { assertSnapshot, launchPuppeteer } from './utils'; import { Suite } from 'mocha'; interface ISuite extends Suite { @@ -30,10 +30,7 @@ interface IWindow extends Window { describe('record', function(this: ISuite) { before(async () => { - this.browser = await puppeteer.launch({ - headless: false, - args: ['--no-sandbox'], - }); + this.browser = await launchPuppeteer(); const bundlePath = path.resolve(__dirname, '../dist/rrweb.min.js'); this.code = fs.readFileSync(bundlePath, 'utf8'); @@ -63,11 +60,11 @@ describe('record', function(this: ISuite) { }); afterEach(async () => { - if (this.page) await this.page.close(); + await this.page.close(); }); after(async () => { - if (this.browser) await this.browser.close(); + await this.browser.close(); }); it('will only have one full snapshot without checkout config', async () => { @@ -210,11 +207,18 @@ describe('record', function(this: ISuite) { const styleSheet = styleElement.sheet; const ruleIdx0 = styleSheet.insertRule('body { background: #000; }'); - styleSheet.insertRule('body { color: #fff; }'); - styleSheet.deleteRule(ruleIdx0); - styleSheet.insertRule('body { color: #ccc; }'); + setTimeout(() => { + styleSheet.insertRule('body { color: #fff; }'); + }, 0); + setTimeout(() => { + styleSheet.deleteRule(ruleIdx0); + }, 5); + setTimeout(() => { + styleSheet.insertRule('body { color: #ccc; }'); + }, 10); }); - await this.page.waitFor(50); + await this.page.waitFor(10); + expect(this.events.length).to.equal(7); assertSnapshot(this.events, __filename, 'stylesheet-rules'); }); }); diff --git a/test/replayer.test.ts b/test/replayer.test.ts index 6fc3cc534a..fe221921fc 100644 --- a/test/replayer.test.ts +++ b/test/replayer.test.ts @@ -12,6 +12,7 @@ import { MouseInteractions, } from '../src/types'; import { Replayer } from '../src'; +import { launchPuppeteer } from './utils'; const now = Date.now(); @@ -122,10 +123,7 @@ interface ISuite extends Suite { describe('replayer', function(this: ISuite) { before(async () => { - this.browser = await puppeteer.launch({ - headless: false, - args: ['--no-sandbox'], - }); + this.browser = await launchPuppeteer(); const bundlePath = path.resolve(__dirname, '../dist/rrweb.min.js'); this.code = fs.readFileSync(bundlePath, 'utf8'); diff --git a/test/utils.ts b/test/utils.ts index d84846fd14..ff0f47d97a 100644 --- a/test/utils.ts +++ b/test/utils.ts @@ -2,6 +2,18 @@ import { SnapshotState, toMatchSnapshot } from 'jest-snapshot'; import { NodeType } from 'rrweb-snapshot'; import { assert } from 'chai'; import { EventType, IncrementalSource, eventWithTime } from '../src/types'; +import * as puppeteer from 'puppeteer'; + +export async function launchPuppeteer() { + return await puppeteer.launch({ + headless: process.env.PUPPETEER_HEADLESS ? true : false, + defaultViewport: { + width: 1920, + height: 1080, + }, + args: ['--no-sandbox'], + }); +} function matchSnapshot(actual: string, testFile: string, testTitle: string) { const snapshotState = new SnapshotState(testFile, { From 2dac8cd0b047a8e783505a4f146eae0cb3bf9138 Mon Sep 17 00:00:00 2001 From: David Cramer Date: Fri, 21 Feb 2020 14:47:40 -0800 Subject: [PATCH 6/8] Remove print statements --- package.json | 1 + src/record/observer.ts | 2 -- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/package.json b/package.json index c579a22d27..3b4f9401d1 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "description": "record and replay the web", "scripts": { "test": "npm run bundle:browser && cross-env TS_NODE_CACHE=false TS_NODE_FILES=true mocha -r ts-node/register test/**/*.test.ts", + "test:watch": "PUPPETEER_HEADLESS=true npm run test -- --watch --watch-extensions js,ts", "repl": "npm run bundle:browser && cross-env TS_NODE_CACHE=false TS_NODE_FILES=true ts-node scripts/repl.ts", "bundle:browser": "cross-env BROWSER_ONLY=true rollup --config", "bundle": "rollup --config", diff --git a/src/record/observer.ts b/src/record/observer.ts index 45a56438ae..4c2ba1a45f 100644 --- a/src/record/observer.ts +++ b/src/record/observer.ts @@ -526,7 +526,6 @@ function initStyleSheetObserver(cb: styleSheetRuleCallback): listenerHandler { rule: string, index?: number | undefined, ) { - console.log('insert rule'); cb({ id: mirror.getId(this as INode), rule, @@ -537,7 +536,6 @@ function initStyleSheetObserver(cb: styleSheetRuleCallback): listenerHandler { const deleteRule = CSSStyleSheet.prototype.deleteRule; CSSStyleSheet.prototype.deleteRule = function(index: number) { - console.error('delete rule'); cb({ id: mirror.getId(this as INode), index, From e5584e76347744d5e651cab5aca3cc51134660aa Mon Sep 17 00:00:00 2001 From: David Cramer Date: Fri, 21 Feb 2020 15:00:33 -0800 Subject: [PATCH 7/8] Fix ID usage and mark add vs remove --- src/record/observer.ts | 9 ++-- src/types.ts | 8 +-- test/__snapshots__/record.test.ts.snap | 68 +++++++++----------------- 3 files changed, 32 insertions(+), 53 deletions(-) diff --git a/src/record/observer.ts b/src/record/observer.ts index 4c2ba1a45f..3eefac38e7 100644 --- a/src/record/observer.ts +++ b/src/record/observer.ts @@ -527,9 +527,8 @@ function initStyleSheetObserver(cb: styleSheetRuleCallback): listenerHandler { index?: number | undefined, ) { cb({ - id: mirror.getId(this as INode), - rule, - index, + id: mirror.getId(this.ownerNode as INode), + adds: [{ rule, index }], }); return insertRule.apply(this, arguments); }; @@ -537,8 +536,8 @@ function initStyleSheetObserver(cb: styleSheetRuleCallback): listenerHandler { const deleteRule = CSSStyleSheet.prototype.deleteRule; CSSStyleSheet.prototype.deleteRule = function(index: number) { cb({ - id: mirror.getId(this as INode), - index, + id: mirror.getId(this.ownerNode as INode), + removes: [{ index }], }); return deleteRule.apply(this, arguments); }; diff --git a/src/types.ts b/src/types.ts index 5586a76f91..543b718563 100644 --- a/src/types.ts +++ b/src/types.ts @@ -250,17 +250,19 @@ export type scrollPosition = { export type scrollCallback = (p: scrollPosition) => void; export type styleSheetAddRule = { - id: number; rule: string; index?: number | undefined; }; export type styleSheetDeleteRule = { - id: number; index: number; }; -export type styleSheetRuleParam = styleSheetAddRule | styleSheetDeleteRule; +export type styleSheetRuleParam = { + id: number; + removes?: styleSheetDeleteRule[]; + adds?: styleSheetAddRule[]; +}; export type styleSheetRuleCallback = (s: styleSheetRuleParam) => void; diff --git a/test/__snapshots__/record.test.ts.snap b/test/__snapshots__/record.test.ts.snap index b260b2d78a..15dd136f42 100644 --- a/test/__snapshots__/record.test.ts.snap +++ b/test/__snapshots__/record.test.ts.snap @@ -204,44 +204,6 @@ exports[`async-checkout 1`] = ` \\"top\\": 0 } } - }, - { - \\"type\\": 3, - \\"data\\": { - \\"source\\": 0, - \\"texts\\": [], - \\"attributes\\": [], - \\"removes\\": [ - { - \\"parentId\\": 8, - \\"id\\": 9 - } - ], - \\"adds\\": [ - { - \\"parentId\\": 4, - \\"previousId\\": 8, - \\"nextId\\": null, - \\"node\\": { - \\"type\\": 2, - \\"tagName\\": \\"span\\", - \\"attributes\\": {}, - \\"childNodes\\": [], - \\"id\\": 9 - } - }, - { - \\"parentId\\": 9, - \\"previousId\\": null, - \\"nextId\\": null, - \\"node\\": { - \\"type\\": 3, - \\"textContent\\": \\"test\\", - \\"id\\": 10 - } - } - ] - } } ]" `; @@ -404,7 +366,11 @@ exports[`stylesheet-rules 1`] = ` \\"data\\": { \\"source\\": 8, \\"id\\": -1, - \\"rule\\": \\"body { background: #000; }\\" + \\"adds\\": [ + { + \\"rule\\": \\"body { background: #000; }\\" + } + ] } }, { @@ -436,24 +402,36 @@ exports[`stylesheet-rules 1`] = ` \\"type\\": 3, \\"data\\": { \\"source\\": 8, - \\"id\\": -1, - \\"rule\\": \\"body { color: #fff; }\\" + \\"id\\": 8, + \\"adds\\": [ + { + \\"rule\\": \\"body { color: #fff; }\\" + } + ] } }, { \\"type\\": 3, \\"data\\": { \\"source\\": 8, - \\"id\\": -1, - \\"index\\": 0 + \\"id\\": 8, + \\"removes\\": [ + { + \\"index\\": 0 + } + ] } }, { \\"type\\": 3, \\"data\\": { \\"source\\": 8, - \\"id\\": -1, - \\"rule\\": \\"body { color: #ccc; }\\" + \\"id\\": 8, + \\"adds\\": [ + { + \\"rule\\": \\"body { color: #ccc; }\\" + } + ] } } ]" From 7b7228ef1c7000bb2bac855f6c73f77d41ccd9c7 Mon Sep 17 00:00:00 2001 From: David Cramer Date: Fri, 21 Feb 2020 19:49:12 -0800 Subject: [PATCH 8/8] Correct type --- src/record/observer.ts | 5 +---- src/types.ts | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/record/observer.ts b/src/record/observer.ts index 3eefac38e7..dc1f8697ec 100644 --- a/src/record/observer.ts +++ b/src/record/observer.ts @@ -522,10 +522,7 @@ function initInputObserver( function initStyleSheetObserver(cb: styleSheetRuleCallback): listenerHandler { const insertRule = CSSStyleSheet.prototype.insertRule; - CSSStyleSheet.prototype.insertRule = function( - rule: string, - index?: number | undefined, - ) { + CSSStyleSheet.prototype.insertRule = function(rule: string, index?: number) { cb({ id: mirror.getId(this.ownerNode as INode), adds: [{ rule, index }], diff --git a/src/types.ts b/src/types.ts index 543b718563..a39ca027f4 100644 --- a/src/types.ts +++ b/src/types.ts @@ -251,7 +251,7 @@ export type scrollCallback = (p: scrollPosition) => void; export type styleSheetAddRule = { rule: string; - index?: number | undefined; + index?: number; }; export type styleSheetDeleteRule = {