From 7adc4ca0f792ead52638c6b6d4b1bcd52fc51245 Mon Sep 17 00:00:00 2001 From: Rahul Date: Thu, 31 Mar 2022 21:57:18 +0530 Subject: [PATCH 1/3] Fix mutation edge case when blocked class gets unblocked --- packages/rrweb/src/record/mutation.ts | 3 ++- packages/rrweb/src/utils.ts | 4 ++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/rrweb/src/record/mutation.ts b/packages/rrweb/src/record/mutation.ts index 1e4a8fbe07..f8c7829f22 100644 --- a/packages/rrweb/src/record/mutation.ts +++ b/packages/rrweb/src/record/mutation.ts @@ -23,6 +23,7 @@ import { isBlocked, isAncestorRemoved, isIgnored, + isSerialized, isIframeINode, hasShadowRoot, } from '../utils'; @@ -532,7 +533,7 @@ export default class MutationBuffer { const parentId = isShadowRoot(m.target) ? this.mirror.getId((m.target.host as unknown) as INode) : this.mirror.getId(m.target as INode); - if (isBlocked(m.target, this.blockClass) || isIgnored(n)) { + if (isBlocked(m.target, this.blockClass) || isIgnored(n) || !isSerialized(n)) { return; } // removed node has not been serialized yet, just remove it from the Set diff --git a/packages/rrweb/src/utils.ts b/packages/rrweb/src/utils.ts index 91376444ef..d9584b41ea 100644 --- a/packages/rrweb/src/utils.ts +++ b/packages/rrweb/src/utils.ts @@ -251,6 +251,10 @@ export function isBlocked(node: Node | null, blockClass: blockClass): boolean { return isBlocked(node.parentNode, blockClass); } +export function isSerialized(n: Node | INode): boolean { + return '__sn' in n; +} + export function isIgnored(n: Node | INode): boolean { if ('__sn' in n) { return (n as INode).__sn.id === IGNORED_NODE; From b798019aa36d566449bfcfed37a46611ac3ac5cf Mon Sep 17 00:00:00 2001 From: Rahul Date: Thu, 14 Apr 2022 12:44:33 +0530 Subject: [PATCH 2/3] Add integration test --- .../__snapshots__/integration.test.ts.snap | 744 ++++++++++++++++++ .../rrweb/test/html/blocked-unblocked.html | 88 +++ packages/rrweb/test/integration.test.ts | 15 + 3 files changed, 847 insertions(+) create mode 100644 packages/rrweb/test/html/blocked-unblocked.html diff --git a/packages/rrweb/test/__snapshots__/integration.test.ts.snap b/packages/rrweb/test/__snapshots__/integration.test.ts.snap index 06290e6068..9a4fa49a15 100644 --- a/packages/rrweb/test/__snapshots__/integration.test.ts.snap +++ b/packages/rrweb/test/__snapshots__/integration.test.ts.snap @@ -3546,6 +3546,750 @@ exports[`record integration tests can use maskInputOptions to configure which ty ]" `; +exports[`record integration tests mutations should work when blocked class is unblocked 1`] = ` +"[ + { + \\"type\\": 0, + \\"data\\": {} + }, + { + \\"type\\": 1, + \\"data\\": {} + }, + { + \\"type\\": 4, + \\"data\\": { + \\"href\\": \\"about:blank\\", + \\"width\\": 1920, + \\"height\\": 1080 + } + }, + { + \\"type\\": 2, + \\"data\\": { + \\"node\\": { + \\"type\\": 0, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"html\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"head\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 4 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"title\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"Uber Application for Codegen Testing\\", + \\"id\\": 6 + } + ], + \\"id\\": 5 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n\\\\n\\", + \\"id\\": 7 + } + ], + \\"id\\": 3 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n\\\\n\\", + \\"id\\": 8 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 10 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 12 + } + ], + \\"id\\": 11 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 13 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"br\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 14 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 15 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"h1\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n Verify that block class bugs are fixed\\\\n \\", + \\"id\\": 17 + } + ], + \\"id\\": 16 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 18 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"br\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 19 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 20 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"div\\", + \\"attributes\\": { + \\"class\\": \\"first\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 22 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"div\\", + \\"attributes\\": { + \\"class\\": \\"visible\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 24 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"button\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"VISIBLE\\", + \\"id\\": 26 + } + ], + \\"id\\": 25 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 27 + } + ], + \\"id\\": 23 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 28 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"br\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 29 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"br\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 30 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"br\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 31 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 32 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"div\\", + \\"attributes\\": { + \\"class\\": \\"rr-block\\", + \\"rr_width\\": \\"1904px\\", + \\"rr_height\\": \\"21px\\" + }, + \\"childNodes\\": [], + \\"id\\": 33 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 34 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"br\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 35 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"br\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 36 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"br\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 37 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 38 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"button\\", + \\"attributes\\": { + \\"onclick\\": \\"mutate1()\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"MUTATE\\", + \\"id\\": 40 + } + ], + \\"id\\": 39 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 41 + } + ], + \\"id\\": 21 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 42 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"br\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 43 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"br\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 44 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"br\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 45 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 46 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"div\\", + \\"attributes\\": { + \\"class\\": \\"second\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 48 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"div\\", + \\"attributes\\": { + \\"class\\": \\"visible2\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 50 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"button\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"VISIBLE\\", + \\"id\\": 52 + } + ], + \\"id\\": 51 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 53 + } + ], + \\"id\\": 49 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 54 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"br\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 55 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"br\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 56 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"br\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 57 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 58 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"div\\", + \\"attributes\\": { + \\"class\\": \\"rr-block\\", + \\"rr_width\\": \\"1904px\\", + \\"rr_height\\": \\"21px\\" + }, + \\"childNodes\\": [], + \\"id\\": 59 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 60 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"br\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 61 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"br\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 62 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"br\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 63 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 64 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"button\\", + \\"attributes\\": { + \\"onclick\\": \\"mutate2()\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"MUTATE\\", + \\"id\\": 66 + } + ], + \\"id\\": 65 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 67 + } + ], + \\"id\\": 47 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n\\\\n \\", + \\"id\\": 68 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 70 + } + ], + \\"id\\": 69 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\\\n\\", + \\"id\\": 71 + } + ], + \\"id\\": 9 + } + ], + \\"id\\": 2 + } + ], + \\"compatMode\\": \\"BackCompat\\", + \\"id\\": 1 + }, + \\"initialOffset\\": { + \\"left\\": 0, + \\"top\\": 0 + } + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 1, + \\"id\\": 39 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 5, + \\"id\\": 39 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 0, + \\"id\\": 39 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 2, + \\"id\\": 39 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [ + { + \\"id\\": 33, + \\"attributes\\": { + \\"class\\": \\"notB\\" + } + } + ], + \\"removes\\": [], + \\"adds\\": [ + { + \\"parentId\\": 23, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"div\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 72 + } + }, + { + \\"parentId\\": 72, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"div\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 73 + } + }, + { + \\"parentId\\": 73, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"button\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 74 + } + }, + { + \\"parentId\\": 74, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 3, + \\"textContent\\": \\"I1I2 VISIBLE\\", + \\"id\\": 75 + } + }, + { + \\"parentId\\": 72, + \\"nextId\\": 73, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"div\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 76 + } + }, + { + \\"parentId\\": 76, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"button\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 77 + } + }, + { + \\"parentId\\": 77, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 3, + \\"textContent\\": \\"I1I1 VISIBLE\\", + \\"id\\": 78 + } + } + ] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 1, + \\"id\\": 65 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 6, + \\"id\\": 39 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 5, + \\"id\\": 65 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 0, + \\"id\\": 65 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 2, + \\"id\\": 65 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [ + { + \\"id\\": 59, + \\"attributes\\": { + \\"class\\": \\"notB\\" + } + } + ], + \\"removes\\": [], + \\"adds\\": [ + { + \\"parentId\\": 49, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"div\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 79 + } + }, + { + \\"parentId\\": 79, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"div\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 80 + } + }, + { + \\"parentId\\": 80, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"button\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 81 + } + }, + { + \\"parentId\\": 81, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 3, + \\"textContent\\": \\"I1I2 VISIBLE\\", + \\"id\\": 82 + } + }, + { + \\"parentId\\": 79, + \\"nextId\\": 80, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"div\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 83 + } + }, + { + \\"parentId\\": 83, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"button\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 84 + } + }, + { + \\"parentId\\": 84, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 3, + \\"textContent\\": \\"I1I1 VISIBLE\\", + \\"id\\": 85 + } + } + ] + } + } +]" +`; + exports[`record integration tests should mask texts 1`] = ` "[ { diff --git a/packages/rrweb/test/html/blocked-unblocked.html b/packages/rrweb/test/html/blocked-unblocked.html new file mode 100644 index 0000000000..ff87e546dd --- /dev/null +++ b/packages/rrweb/test/html/blocked-unblocked.html @@ -0,0 +1,88 @@ + + Uber Application for Codegen Testing + + + + + +
+

+ Verify that block class bugs are fixed +

+
+
+
+ +
+


+
+ +
+


+ +
+


+
+
+ +
+


+
+ +
+


+ +
+ diff --git a/packages/rrweb/test/integration.test.ts b/packages/rrweb/test/integration.test.ts index 6999e3caee..3fa0f0a34d 100644 --- a/packages/rrweb/test/integration.test.ts +++ b/packages/rrweb/test/integration.test.ts @@ -326,6 +326,21 @@ describe('record integration tests', function (this: ISuite) { assertSnapshot(snapshots); }); + it('mutations should work when blocked class is unblocked', async () => { + const page: puppeteer.Page = await browser.newPage(); + await page.goto('about: blank'); + await page.setContent(getHtml.call(this, 'blocked-unblocked.html')); + + const elements1 = await page.$x('/html/body/div[1]/button'); + await elements1[0].click(); + + const elements2 = await page.$x('/html/body/div[2]/button'); + await elements2[0].click(); + + const snapshots = await page.evaluate('window.snapshots'); + assertSnapshot(snapshots); + }); + it('should record DOM node movement 1', async () => { const page: puppeteer.Page = await browser.newPage(); await page.goto('about:blank'); From d1b0d2236ca99af4abd1da2545ac13049bedfa26 Mon Sep 17 00:00:00 2001 From: Rahul Date: Thu, 14 Apr 2022 13:44:04 +0530 Subject: [PATCH 3/3] Update isSerialized logic --- packages/rrweb/src/record/mutation.ts | 2 +- packages/rrweb/src/utils.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/rrweb/src/record/mutation.ts b/packages/rrweb/src/record/mutation.ts index 4ad4593cef..f2e5ed69ba 100644 --- a/packages/rrweb/src/record/mutation.ts +++ b/packages/rrweb/src/record/mutation.ts @@ -527,7 +527,7 @@ export default class MutationBuffer { if ( isBlocked(m.target, this.blockClass) || isIgnored(n, this.mirror) || - !isSerialized(n) + !isSerialized(n, this.mirror) ) { return; } diff --git a/packages/rrweb/src/utils.ts b/packages/rrweb/src/utils.ts index 19f2c10342..c60174133e 100644 --- a/packages/rrweb/src/utils.ts +++ b/packages/rrweb/src/utils.ts @@ -213,8 +213,8 @@ export function isBlocked(node: Node | null, blockClass: blockClass): boolean { return isBlocked(node.parentNode, blockClass); } -export function isSerialized(n: Node): boolean { - return '__sn' in n; +export function isSerialized(n: Node, mirror: Mirror): boolean { + return mirror.getId(n) !== -1; } export function isIgnored(n: Node, mirror: Mirror): boolean {