From 8a487deb9c68f67e5f6e895ebd6c7e912ddde5c5 Mon Sep 17 00:00:00 2001 From: Billy Vong Date: Wed, 22 Feb 2023 19:26:32 -0500 Subject: [PATCH 1/3] fix: textarea duplicate values + add tests --- packages/rrweb-snapshot/src/snapshot.ts | 5 +- packages/rrweb-snapshot/src/utils.ts | 1 + .../__snapshots__/integration.test.ts.snap | 1492 ++++++++++++++++- packages/rrweb/test/html/empty.html | 11 + packages/rrweb/test/integration.test.ts | 164 +- 5 files changed, 1626 insertions(+), 47 deletions(-) create mode 100644 packages/rrweb/test/html/empty.html diff --git a/packages/rrweb-snapshot/src/snapshot.ts b/packages/rrweb-snapshot/src/snapshot.ts index 0f2af24876..90458f16a3 100644 --- a/packages/rrweb-snapshot/src/snapshot.ts +++ b/packages/rrweb-snapshot/src/snapshot.ts @@ -543,10 +543,9 @@ function serializeNode( // form fields if ( tagName === 'input' || - tagName === 'textarea' || tagName === 'select' ) { - const value = (n as HTMLInputElement | HTMLTextAreaElement).value; + const value = (n as HTMLInputElement).value; if ( attributes.type !== 'radio' && attributes.type !== 'checkbox' && @@ -1249,4 +1248,4 @@ export default snapshot; /** We want to skip `autoplay` attribute, as this has weird results when replaying. */ function skipAttribute(tagName: string, attributeName: string, value?: unknown) { return (tagName === 'video' || tagName === 'audio') && attributeName === 'autoplay'; -} \ No newline at end of file +} diff --git a/packages/rrweb-snapshot/src/utils.ts b/packages/rrweb-snapshot/src/utils.ts index 15adc27590..adbd8797b9 100644 --- a/packages/rrweb-snapshot/src/utils.ts +++ b/packages/rrweb-snapshot/src/utils.ts @@ -37,6 +37,7 @@ export function maskInputValue({ if ( maskInputOptions[tagName.toLowerCase() as keyof MaskInputOptions] || maskInputOptions[type as keyof MaskInputOptions] || + (tagName === 'input' && !type && maskInputOptions['text']) || // For inputs without a "type" attribute defined (maskInputSelector && input.matches(maskInputSelector)) ) { if (maskInputFn) { diff --git a/packages/rrweb/test/__snapshots__/integration.test.ts.snap b/packages/rrweb/test/__snapshots__/integration.test.ts.snap index 031226dddf..cd63ca7f34 100644 --- a/packages/rrweb/test/__snapshots__/integration.test.ts.snap +++ b/packages/rrweb/test/__snapshots__/integration.test.ts.snap @@ -3875,9 +3875,7 @@ exports[`record integration tests should mask all text (except unmaskTextSelecto { "type": 2, "tagName": "textarea", - "attributes": { - "value": "mask10" - }, + "attributes": {}, "childNodes": [ { "type": 3, @@ -4260,9 +4258,7 @@ exports[`record integration tests should mask only inputs 1`] = ` { "type": 2, "tagName": "textarea", - "attributes": { - "value": "******" - }, + "attributes": {}, "childNodes": [ { "type": 3, @@ -4645,9 +4641,7 @@ exports[`record integration tests should mask texts 1`] = ` { "type": 2, "tagName": "textarea", - "attributes": { - "value": "mask10" - }, + "attributes": {}, "childNodes": [ { "type": 3, @@ -5030,9 +5024,7 @@ exports[`record integration tests should mask texts using maskTextFn 1`] = ` { "type": 2, "tagName": "textarea", - "attributes": { - "value": "mask10" - }, + "attributes": {}, "childNodes": [ { "type": 3, @@ -7382,6 +7374,238 @@ exports[`record integration tests should not record input events on ignored elem ]" `; +exports[`record integration tests should not record input values if dynamically added and maskAllInputs is true 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": 1, + "name": "html", + "publicId": "", + "systemId": "", + "id": 2 + }, + { + "type": 2, + "tagName": "html", + "attributes": { + "lang": "en" + }, + "childNodes": [ + { + "type": 2, + "tagName": "head", + "attributes": {}, + "childNodes": [ + { + "type": 3, + "textContent": "\\n ", + "id": 5 + }, + { + "type": 2, + "tagName": "meta", + "attributes": { + "charset": "UTF-8" + }, + "childNodes": [], + "id": 6 + }, + { + "type": 3, + "textContent": "\\n ", + "id": 7 + }, + { + "type": 2, + "tagName": "meta", + "attributes": { + "name": "viewport", + "content": "width=device-width, initial-scale=1.0" + }, + "childNodes": [], + "id": 8 + }, + { + "type": 3, + "textContent": "\\n ", + "id": 9 + }, + { + "type": 2, + "tagName": "title", + "attributes": {}, + "childNodes": [ + { + "type": 3, + "textContent": "Empty", + "id": 11 + } + ], + "id": 10 + }, + { + "type": 3, + "textContent": "\\n ", + "id": 12 + } + ], + "id": 4 + }, + { + "type": 3, + "textContent": "\\n ", + "id": 13 + }, + { + "type": 2, + "tagName": "body", + "attributes": {}, + "childNodes": [ + { + "type": 3, + "textContent": "\\n ", + "id": 15 + }, + { + "type": 2, + "tagName": "div", + "attributes": { + "id": "one" + }, + "childNodes": [], + "id": 16 + }, + { + "type": 3, + "textContent": "\\n \\n ", + "id": 17 + }, + { + "type": 2, + "tagName": "script", + "attributes": {}, + "childNodes": [ + { + "type": 3, + "textContent": "SCRIPT_PLACEHOLDER", + "id": 19 + } + ], + "id": 18 + }, + { + "type": 3, + "textContent": "\\n \\n \\n\\n", + "id": 20 + } + ], + "id": 14 + } + ], + "id": 3 + } + ], + "id": 1 + }, + "initialOffset": { + "left": 0, + "top": 0 + } + } + }, + { + "type": 3, + "data": { + "source": 0, + "texts": [], + "attributes": [], + "removes": [], + "adds": [ + { + "parentId": 14, + "nextId": 16, + "node": { + "type": 2, + "tagName": "input", + "attributes": { + "id": "input", + "value": "**********************" + }, + "childNodes": [], + "id": 21 + } + } + ] + } + }, + { + "type": 3, + "data": { + "source": 5, + "text": "**********************", + "isChecked": false, + "id": 21 + } + }, + { + "type": 3, + "data": { + "source": 2, + "type": 5, + "id": 21 + } + }, + { + "type": 3, + "data": { + "source": 5, + "text": "***********************", + "isChecked": false, + "id": 21 + } + }, + { + "type": 3, + "data": { + "source": 5, + "text": "************************", + "isChecked": false, + "id": 21 + } + }, + { + "type": 3, + "data": { + "source": 5, + "text": "*************************", + "isChecked": false, + "id": 21 + } + } +]" +`; + exports[`record integration tests should not record input values if maskAllInputs is enabled 1`] = ` "[ { @@ -8235,7 +8459,7 @@ exports[`record integration tests should not record input values if maskAllInput ]" `; -exports[`record integration tests should only record unblocked elements 1`] = ` +exports[`record integration tests should not record textarea values if dynamically added and maskAllInputs is true 1`] = ` "[ { "type": 0, @@ -8312,21 +8536,6 @@ exports[`record integration tests should only record unblocked elements 1`] = ` "textContent": "\\n ", "id": 9 }, - { - "type": 2, - "tagName": "meta", - "attributes": { - "http-equiv": "X-UA-Compatible", - "content": "ie=edge" - }, - "childNodes": [], - "id": 10 - }, - { - "type": 3, - "textContent": "\\n ", - "id": 11 - }, { "type": 2, "tagName": "title", @@ -8334,16 +8543,16 @@ exports[`record integration tests should only record unblocked elements 1`] = ` "childNodes": [ { "type": 3, - "textContent": "Block record", - "id": 13 + "textContent": "Empty", + "id": 11 } ], - "id": 12 + "id": 10 }, { "type": 3, "textContent": "\\n ", - "id": 14 + "id": 12 } ], "id": 4 @@ -8351,7 +8560,264 @@ exports[`record integration tests should only record unblocked elements 1`] = ` { "type": 3, "textContent": "\\n ", - "id": 15 + "id": 13 + }, + { + "type": 2, + "tagName": "body", + "attributes": {}, + "childNodes": [ + { + "type": 3, + "textContent": "\\n ", + "id": 15 + }, + { + "type": 2, + "tagName": "div", + "attributes": { + "id": "one" + }, + "childNodes": [], + "id": 16 + }, + { + "type": 3, + "textContent": "\\n \\n ", + "id": 17 + }, + { + "type": 2, + "tagName": "script", + "attributes": {}, + "childNodes": [ + { + "type": 3, + "textContent": "SCRIPT_PLACEHOLDER", + "id": 19 + } + ], + "id": 18 + }, + { + "type": 3, + "textContent": "\\n \\n \\n\\n", + "id": 20 + } + ], + "id": 14 + } + ], + "id": 3 + } + ], + "id": 1 + }, + "initialOffset": { + "left": 0, + "top": 0 + } + } + }, + { + "type": 3, + "data": { + "source": 0, + "texts": [], + "attributes": [], + "removes": [], + "adds": [ + { + "parentId": 14, + "nextId": 16, + "node": { + "type": 2, + "tagName": "textarea", + "attributes": { + "id": "textarea" + }, + "childNodes": [], + "id": 21 + } + }, + { + "parentId": 21, + "nextId": null, + "node": { + "type": 2, + "tagName": "br", + "attributes": {}, + "childNodes": [], + "id": 22 + } + }, + { + "parentId": 21, + "nextId": 22, + "node": { + "type": 3, + "textContent": "*************************", + "id": 23 + } + } + ] + } + }, + { + "type": 3, + "data": { + "source": 2, + "type": 5, + "id": 21 + } + }, + { + "type": 3, + "data": { + "source": 5, + "text": "**************************", + "isChecked": false, + "id": 21 + } + }, + { + "type": 3, + "data": { + "source": 5, + "text": "***************************", + "isChecked": false, + "id": 21 + } + }, + { + "type": 3, + "data": { + "source": 5, + "text": "****************************", + "isChecked": false, + "id": 21 + } + } +]" +`; + +exports[`record integration tests should only record unblocked elements 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": 1, + "name": "html", + "publicId": "", + "systemId": "", + "id": 2 + }, + { + "type": 2, + "tagName": "html", + "attributes": { + "lang": "en" + }, + "childNodes": [ + { + "type": 2, + "tagName": "head", + "attributes": {}, + "childNodes": [ + { + "type": 3, + "textContent": "\\n ", + "id": 5 + }, + { + "type": 2, + "tagName": "meta", + "attributes": { + "charset": "UTF-8" + }, + "childNodes": [], + "id": 6 + }, + { + "type": 3, + "textContent": "\\n ", + "id": 7 + }, + { + "type": 2, + "tagName": "meta", + "attributes": { + "name": "viewport", + "content": "width=device-width, initial-scale=1.0" + }, + "childNodes": [], + "id": 8 + }, + { + "type": 3, + "textContent": "\\n ", + "id": 9 + }, + { + "type": 2, + "tagName": "meta", + "attributes": { + "http-equiv": "X-UA-Compatible", + "content": "ie=edge" + }, + "childNodes": [], + "id": 10 + }, + { + "type": 3, + "textContent": "\\n ", + "id": 11 + }, + { + "type": 2, + "tagName": "title", + "attributes": {}, + "childNodes": [ + { + "type": 3, + "textContent": "Block record", + "id": 13 + } + ], + "id": 12 + }, + { + "type": 3, + "textContent": "\\n ", + "id": 14 + } + ], + "id": 4 + }, + { + "type": 3, + "textContent": "\\n ", + "id": 15 }, { "type": 2, @@ -11070,7 +11536,7 @@ exports[`record integration tests should record input userTriggered values if us ]" `; -exports[`record integration tests should record nested iframes and shadow doms 1`] = ` +exports[`record integration tests should record input values if dynamically added and maskAllInputs is false 1`] = ` "[ { "type": 0, @@ -11154,7 +11620,7 @@ exports[`record integration tests should record nested iframes and shadow doms 1 "childNodes": [ { "type": 3, - "textContent": "Frame 2", + "textContent": "Empty", "id": 11 } ], @@ -11180,18 +11646,716 @@ exports[`record integration tests should record nested iframes and shadow doms 1 "childNodes": [ { "type": 3, - "textContent": "\\n frame 2\\n \\n ", + "textContent": "\\n ", "id": 15 }, { "type": 2, - "tagName": "script", - "attributes": {}, - "childNodes": [ - { - "type": 3, - "textContent": "SCRIPT_PLACEHOLDER", - "id": 17 + "tagName": "div", + "attributes": { + "id": "one" + }, + "childNodes": [], + "id": 16 + }, + { + "type": 3, + "textContent": "\\n \\n ", + "id": 17 + }, + { + "type": 2, + "tagName": "script", + "attributes": {}, + "childNodes": [ + { + "type": 3, + "textContent": "SCRIPT_PLACEHOLDER", + "id": 19 + } + ], + "id": 18 + }, + { + "type": 3, + "textContent": "\\n \\n \\n\\n", + "id": 20 + } + ], + "id": 14 + } + ], + "id": 3 + } + ], + "id": 1 + }, + "initialOffset": { + "left": 0, + "top": 0 + } + } + }, + { + "type": 3, + "data": { + "source": 0, + "texts": [], + "attributes": [], + "removes": [], + "adds": [ + { + "parentId": 14, + "nextId": 16, + "node": { + "type": 2, + "tagName": "input", + "attributes": { + "id": "input", + "value": "input should not be masked" + }, + "childNodes": [], + "id": 21 + } + } + ] + } + }, + { + "type": 3, + "data": { + "source": 5, + "text": "input should not be masked", + "isChecked": false, + "id": 21 + } + }, + { + "type": 3, + "data": { + "source": 2, + "type": 5, + "id": 21 + } + }, + { + "type": 3, + "data": { + "source": 5, + "text": "input should not be maskedm", + "isChecked": false, + "id": 21 + } + }, + { + "type": 3, + "data": { + "source": 5, + "text": "input should not be maskedmo", + "isChecked": false, + "id": 21 + } + }, + { + "type": 3, + "data": { + "source": 5, + "text": "input should not be maskedmoo", + "isChecked": false, + "id": 21 + } + } +]" +`; + +exports[`record integration tests should record input values if dynamically added, maskAllInputs is false, and mask selector is used 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": 1, + "name": "html", + "publicId": "", + "systemId": "", + "id": 2 + }, + { + "type": 2, + "tagName": "html", + "attributes": { + "lang": "en" + }, + "childNodes": [ + { + "type": 2, + "tagName": "head", + "attributes": {}, + "childNodes": [ + { + "type": 3, + "textContent": "\\n ", + "id": 5 + }, + { + "type": 2, + "tagName": "meta", + "attributes": { + "charset": "UTF-8" + }, + "childNodes": [], + "id": 6 + }, + { + "type": 3, + "textContent": "\\n ", + "id": 7 + }, + { + "type": 2, + "tagName": "meta", + "attributes": { + "name": "viewport", + "content": "width=device-width, initial-scale=1.0" + }, + "childNodes": [], + "id": 8 + }, + { + "type": 3, + "textContent": "\\n ", + "id": 9 + }, + { + "type": 2, + "tagName": "title", + "attributes": {}, + "childNodes": [ + { + "type": 3, + "textContent": "Empty", + "id": 11 + } + ], + "id": 10 + }, + { + "type": 3, + "textContent": "\\n ", + "id": 12 + } + ], + "id": 4 + }, + { + "type": 3, + "textContent": "\\n ", + "id": 13 + }, + { + "type": 2, + "tagName": "body", + "attributes": {}, + "childNodes": [ + { + "type": 3, + "textContent": "\\n ", + "id": 15 + }, + { + "type": 2, + "tagName": "div", + "attributes": { + "id": "one" + }, + "childNodes": [], + "id": 16 + }, + { + "type": 3, + "textContent": "\\n \\n ", + "id": 17 + }, + { + "type": 2, + "tagName": "script", + "attributes": {}, + "childNodes": [ + { + "type": 3, + "textContent": "SCRIPT_PLACEHOLDER", + "id": 19 + } + ], + "id": 18 + }, + { + "type": 3, + "textContent": "\\n \\n \\n\\n", + "id": 20 + } + ], + "id": 14 + } + ], + "id": 3 + } + ], + "id": 1 + }, + "initialOffset": { + "left": 0, + "top": 0 + } + } + }, + { + "type": 3, + "data": { + "source": 0, + "texts": [], + "attributes": [], + "removes": [], + "adds": [ + { + "parentId": 14, + "nextId": 16, + "node": { + "type": 2, + "tagName": "input", + "attributes": { + "id": "input-masked", + "class": "rr-mask", + "value": "input should be masked" + }, + "childNodes": [], + "id": 21 + } + } + ] + } + }, + { + "type": 3, + "data": { + "source": 5, + "text": "input should be masked", + "isChecked": false, + "id": 21 + } + }, + { + "type": 3, + "data": { + "source": 2, + "type": 5, + "id": 21 + } + }, + { + "type": 3, + "data": { + "source": 5, + "text": "input should be maskedm", + "isChecked": false, + "id": 21 + } + }, + { + "type": 3, + "data": { + "source": 5, + "text": "input should be maskedmo", + "isChecked": false, + "id": 21 + } + }, + { + "type": 3, + "data": { + "source": 5, + "text": "input should be maskedmoo", + "isChecked": false, + "id": 21 + } + } +]" +`; + +exports[`record integration tests should record input values if dynamically added, maskAllInputs is true, and unmask selector is used 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": 1, + "name": "html", + "publicId": "", + "systemId": "", + "id": 2 + }, + { + "type": 2, + "tagName": "html", + "attributes": { + "lang": "en" + }, + "childNodes": [ + { + "type": 2, + "tagName": "head", + "attributes": {}, + "childNodes": [ + { + "type": 3, + "textContent": "\\n ", + "id": 5 + }, + { + "type": 2, + "tagName": "meta", + "attributes": { + "charset": "UTF-8" + }, + "childNodes": [], + "id": 6 + }, + { + "type": 3, + "textContent": "\\n ", + "id": 7 + }, + { + "type": 2, + "tagName": "meta", + "attributes": { + "name": "viewport", + "content": "width=device-width, initial-scale=1.0" + }, + "childNodes": [], + "id": 8 + }, + { + "type": 3, + "textContent": "\\n ", + "id": 9 + }, + { + "type": 2, + "tagName": "title", + "attributes": {}, + "childNodes": [ + { + "type": 3, + "textContent": "Empty", + "id": 11 + } + ], + "id": 10 + }, + { + "type": 3, + "textContent": "\\n ", + "id": 12 + } + ], + "id": 4 + }, + { + "type": 3, + "textContent": "\\n ", + "id": 13 + }, + { + "type": 2, + "tagName": "body", + "attributes": {}, + "childNodes": [ + { + "type": 3, + "textContent": "\\n ", + "id": 15 + }, + { + "type": 2, + "tagName": "div", + "attributes": { + "id": "one" + }, + "childNodes": [], + "id": 16 + }, + { + "type": 3, + "textContent": "\\n \\n ", + "id": 17 + }, + { + "type": 2, + "tagName": "script", + "attributes": {}, + "childNodes": [ + { + "type": 3, + "textContent": "SCRIPT_PLACEHOLDER", + "id": 19 + } + ], + "id": 18 + }, + { + "type": 3, + "textContent": "\\n \\n \\n\\n", + "id": 20 + } + ], + "id": 14 + } + ], + "id": 3 + } + ], + "id": 1 + }, + "initialOffset": { + "left": 0, + "top": 0 + } + } + }, + { + "type": 3, + "data": { + "source": 0, + "texts": [], + "attributes": [], + "removes": [], + "adds": [ + { + "parentId": 14, + "nextId": 16, + "node": { + "type": 2, + "tagName": "input", + "attributes": { + "id": "input-unmasked", + "class": "rr-unmask", + "value": "input should be unmasked" + }, + "childNodes": [], + "id": 21 + } + } + ] + } + }, + { + "type": 3, + "data": { + "source": 5, + "text": "input should be unmasked", + "isChecked": false, + "id": 21 + } + }, + { + "type": 3, + "data": { + "source": 2, + "type": 5, + "id": 21 + } + }, + { + "type": 3, + "data": { + "source": 5, + "text": "input should be unmaskedm", + "isChecked": false, + "id": 21 + } + }, + { + "type": 3, + "data": { + "source": 5, + "text": "input should be unmaskedmo", + "isChecked": false, + "id": 21 + } + }, + { + "type": 3, + "data": { + "source": 5, + "text": "input should be unmaskedmoo", + "isChecked": false, + "id": 21 + } + } +]" +`; + +exports[`record integration tests should record nested iframes and shadow doms 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": 1, + "name": "html", + "publicId": "", + "systemId": "", + "id": 2 + }, + { + "type": 2, + "tagName": "html", + "attributes": { + "lang": "en" + }, + "childNodes": [ + { + "type": 2, + "tagName": "head", + "attributes": {}, + "childNodes": [ + { + "type": 3, + "textContent": "\\n ", + "id": 5 + }, + { + "type": 2, + "tagName": "meta", + "attributes": { + "charset": "UTF-8" + }, + "childNodes": [], + "id": 6 + }, + { + "type": 3, + "textContent": "\\n ", + "id": 7 + }, + { + "type": 2, + "tagName": "meta", + "attributes": { + "name": "viewport", + "content": "width=device-width, initial-scale=1.0" + }, + "childNodes": [], + "id": 8 + }, + { + "type": 3, + "textContent": "\\n ", + "id": 9 + }, + { + "type": 2, + "tagName": "title", + "attributes": {}, + "childNodes": [ + { + "type": 3, + "textContent": "Frame 2", + "id": 11 + } + ], + "id": 10 + }, + { + "type": 3, + "textContent": "\\n ", + "id": 12 + } + ], + "id": 4 + }, + { + "type": 3, + "textContent": "\\n ", + "id": 13 + }, + { + "type": 2, + "tagName": "body", + "attributes": {}, + "childNodes": [ + { + "type": 3, + "textContent": "\\n frame 2\\n \\n ", + "id": 15 + }, + { + "type": 2, + "tagName": "script", + "attributes": {}, + "childNodes": [ + { + "type": 3, + "textContent": "SCRIPT_PLACEHOLDER", + "id": 17 } ], "id": 16 @@ -11855,6 +13019,248 @@ exports[`record integration tests should record shadow DOM 1`] = ` ]" `; +exports[`record integration tests should record textarea values if dynamically added and maskAllInputs is false 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": 1, + "name": "html", + "publicId": "", + "systemId": "", + "id": 2 + }, + { + "type": 2, + "tagName": "html", + "attributes": { + "lang": "en" + }, + "childNodes": [ + { + "type": 2, + "tagName": "head", + "attributes": {}, + "childNodes": [ + { + "type": 3, + "textContent": "\\n ", + "id": 5 + }, + { + "type": 2, + "tagName": "meta", + "attributes": { + "charset": "UTF-8" + }, + "childNodes": [], + "id": 6 + }, + { + "type": 3, + "textContent": "\\n ", + "id": 7 + }, + { + "type": 2, + "tagName": "meta", + "attributes": { + "name": "viewport", + "content": "width=device-width, initial-scale=1.0" + }, + "childNodes": [], + "id": 8 + }, + { + "type": 3, + "textContent": "\\n ", + "id": 9 + }, + { + "type": 2, + "tagName": "title", + "attributes": {}, + "childNodes": [ + { + "type": 3, + "textContent": "Empty", + "id": 11 + } + ], + "id": 10 + }, + { + "type": 3, + "textContent": "\\n ", + "id": 12 + } + ], + "id": 4 + }, + { + "type": 3, + "textContent": "\\n ", + "id": 13 + }, + { + "type": 2, + "tagName": "body", + "attributes": {}, + "childNodes": [ + { + "type": 3, + "textContent": "\\n ", + "id": 15 + }, + { + "type": 2, + "tagName": "div", + "attributes": { + "id": "one" + }, + "childNodes": [], + "id": 16 + }, + { + "type": 3, + "textContent": "\\n \\n ", + "id": 17 + }, + { + "type": 2, + "tagName": "script", + "attributes": {}, + "childNodes": [ + { + "type": 3, + "textContent": "SCRIPT_PLACEHOLDER", + "id": 19 + } + ], + "id": 18 + }, + { + "type": 3, + "textContent": "\\n \\n \\n\\n", + "id": 20 + } + ], + "id": 14 + } + ], + "id": 3 + } + ], + "id": 1 + }, + "initialOffset": { + "left": 0, + "top": 0 + } + } + }, + { + "type": 3, + "data": { + "source": 0, + "texts": [], + "attributes": [], + "removes": [], + "adds": [ + { + "parentId": 14, + "nextId": 16, + "node": { + "type": 2, + "tagName": "textarea", + "attributes": { + "id": "textarea" + }, + "childNodes": [], + "id": 21 + } + }, + { + "parentId": 21, + "nextId": null, + "node": { + "type": 2, + "tagName": "br", + "attributes": {}, + "childNodes": [], + "id": 22 + } + }, + { + "parentId": 21, + "nextId": 22, + "node": { + "type": 3, + "textContent": "textarea should not be masked", + "id": 23 + } + } + ] + } + }, + { + "type": 3, + "data": { + "source": 2, + "type": 5, + "id": 21 + } + }, + { + "type": 3, + "data": { + "source": 5, + "text": "mtextarea should not be masked", + "isChecked": false, + "id": 21 + } + }, + { + "type": 3, + "data": { + "source": 5, + "text": "motextarea should not be masked", + "isChecked": false, + "id": 21 + } + }, + { + "type": 3, + "data": { + "source": 5, + "text": "mootextarea should not be masked", + "isChecked": false, + "id": 21 + } + } +]" +`; + exports[`record integration tests should record webgl canvas mutations 1`] = ` "[ { diff --git a/packages/rrweb/test/html/empty.html b/packages/rrweb/test/html/empty.html new file mode 100644 index 0000000000..6ceadbba12 --- /dev/null +++ b/packages/rrweb/test/html/empty.html @@ -0,0 +1,11 @@ + + + + + + Empty + + +
+ + diff --git a/packages/rrweb/test/integration.test.ts b/packages/rrweb/test/integration.test.ts index fa6a00f5e5..8df44daf1c 100644 --- a/packages/rrweb/test/integration.test.ts +++ b/packages/rrweb/test/integration.test.ts @@ -10,7 +10,7 @@ import { waitForRAF, replaceLast, } from './utils'; -import { recordOptions, eventWithTime, EventType } from '../src/types'; +import { recordOptions, eventWithTime, EventType, IncrementalSource } from '../src/types'; import { visitSnapshot, NodeType } from '@sentry-internal/rrweb-snapshot'; interface ISuite { @@ -24,6 +24,13 @@ interface IMimeType { [key: string]: string; } +/** + * Used to filter scroll events out of snapshots as they are flakey + */ +function isNotScroll(snapshot: eventWithTime) { + return !(snapshot.type === EventType.IncrementalSnapshot && snapshot.data.source === IncrementalSource.Scroll) +} + describe('record integration tests', function (this: ISuite) { jest.setTimeout(10_000); @@ -53,6 +60,7 @@ describe('record integration tests', function (this: ISuite) { maskAllText: ${options.maskAllText}, maskTextFn: ${options.maskTextFn}, unmaskTextSelector: ${JSON.stringify(options.unmaskTextSelector)}, + unmaskInputSelector: ${JSON.stringify(options.unmaskInputSelector)}, blockSelector: ${JSON.stringify(options.blockSelector)}, unblockSelector: ${JSON.stringify(options.unblockSelector)}, recordCanvas: ${options.recordCanvas}, @@ -239,6 +247,160 @@ describe('record integration tests', function (this: ISuite) { assertSnapshot(snapshots); }); + it('should not record input values on selectively masked elements when maskAllInputs is disabled', async () => { + const page: puppeteer.Page = await browser.newPage(); + await page.goto('about:blank'); + await page.setContent( + getHtml.call(this, 'form-masked.html', { maskAllInputs: false, maskInputSelector: '.rr-mask' }), + ); + + await page.type('input[type="text"]', 'test'); + await page.click('input[type="radio"]'); + await page.click('input[type="checkbox"]'); + await page.type('input[type="password"]', 'password'); + await page.type('textarea', 'textarea test'); + await page.select('select', '1'); + + const snapshots = await page.evaluate('window.snapshots'); + assertSnapshot(snapshots); + }); + + it('should record input values if dynamically added and maskAllInputs is false', async () => { + const page: puppeteer.Page = await browser.newPage(); + await page.goto('about:blank'); + await page.setContent( + getHtml.call(this, 'empty.html', { maskAllInputs: false }), + ); + + await page.evaluate(() => { + const el = document.createElement('input'); + el.id = 'input'; + el.value = 'input should not be masked'; + + const nextElement = document.querySelector('#one')!; + nextElement.parentNode!.insertBefore(el, nextElement); + }); + + await page.type('#input', 'moo'); + + const snapshots = await page.evaluate('window.snapshots') as eventWithTime[]; + assertSnapshot(snapshots.filter(isNotScroll)); + }); + + it('should record textarea values if dynamically added and maskAllInputs is false', async () => { + const page: puppeteer.Page = await browser.newPage(); + await page.goto('about:blank'); + await page.setContent( + getHtml.call(this, 'empty.html', { maskAllInputs: false }), + ); + + await page.evaluate(() => { + const el = document.createElement('textarea'); + el.id = 'textarea'; + el.innerText = `textarea should not be masked +`; + + const nextElement = document.querySelector('#one')!; + nextElement.parentNode!.insertBefore(el, nextElement); + }); + + await page.type('#textarea', 'moo'); + + const snapshots = await page.evaluate('window.snapshots') as eventWithTime[]; + assertSnapshot(snapshots.filter(isNotScroll)); + }); + + it('should record input values if dynamically added, maskAllInputs is false, and mask selector is used', async () => { + const page: puppeteer.Page = await browser.newPage(); + await page.goto('about:blank'); + await page.setContent( + getHtml.call(this, 'empty.html', { maskAllInputs: false, maskInputSelector: '.rr-mask' }), + ); + + await page.evaluate(() => { + const el = document.createElement('input'); + el.id = 'input-masked'; + el.className = 'rr-mask'; + el.value = 'input should be masked'; + + const nextElement = document.querySelector('#one')!; + nextElement.parentNode!.insertBefore(el, nextElement); + }); + + await page.type('#input-masked', 'moo'); + + const snapshots = await page.evaluate('window.snapshots') as eventWithTime[]; + assertSnapshot(snapshots.filter(isNotScroll)); + }); + + it('should not record input values if dynamically added and maskAllInputs is true', async () => { + const page: puppeteer.Page = await browser.newPage(); + await page.goto('about:blank'); + await page.setContent( + getHtml.call(this, 'empty.html', { maskAllInputs: true }), + ); + + await page.evaluate(() => { + const el = document.createElement('input'); + el.id = 'input'; + el.value = 'input should be masked'; + + const nextElement = document.querySelector('#one')!; + nextElement.parentNode!.insertBefore(el, nextElement); + }); + + await page.type('#input', 'moo'); + + const snapshots = await page.evaluate('window.snapshots') as eventWithTime[]; + assertSnapshot(snapshots.filter(isNotScroll)); + }); + + it('should not record textarea values if dynamically added and maskAllInputs is true', async () => { + const page: puppeteer.Page = await browser.newPage(); + await page.goto('about:blank'); + await page.setContent( + getHtml.call(this, 'empty.html', { maskAllInputs: true }), + ); + + await page.evaluate(() => { + const el = document.createElement('textarea'); + el.id = 'textarea'; + el.innerText = `textarea should be masked +`; + + const nextElement = document.querySelector('#one')!; + nextElement.parentNode!.insertBefore(el, nextElement); + }); + + await page.type('#textarea', 'moo'); + + const snapshots = await page.evaluate('window.snapshots') as eventWithTime[]; + assertSnapshot(snapshots.filter(isNotScroll)); + }); + + it('should record input values if dynamically added, maskAllInputs is true, and unmask selector is used', async () => { + const page: puppeteer.Page = await browser.newPage(); + await page.goto('about:blank'); + await page.setContent( + getHtml.call(this, 'empty.html', { maskAllInputs: true, unmaskInputSelector: '.rr-unmask'}), + ); + + await page.evaluate(() => { + const el = document.createElement('input'); + el.id = 'input-unmasked'; + el.className = 'rr-unmask'; + el.value = 'input should be unmasked'; + + const nextElement = document.querySelector('#one')!; + nextElement.parentNode!.insertBefore(el, nextElement); + }); + + await page.type('#input-unmasked', 'moo'); + + const snapshots = await page.evaluate('window.snapshots') as eventWithTime[]; + assertSnapshot(snapshots.filter(isNotScroll)); + }); + it('can use maskInputOptions to configure which type of inputs should be masked', async () => { const page: puppeteer.Page = await browser.newPage(); await page.goto('about:blank'); From e149c8d5986e8175b24d71998aa35af67510ba93 Mon Sep 17 00:00:00 2001 From: Billy Vong Date: Thu, 23 Feb 2023 13:48:31 -0500 Subject: [PATCH 2/3] add comments, remove test from different branch --- packages/rrweb-snapshot/src/snapshot.ts | 7 +++---- packages/rrweb/test/integration.test.ts | 18 ------------------ 2 files changed, 3 insertions(+), 22 deletions(-) diff --git a/packages/rrweb-snapshot/src/snapshot.ts b/packages/rrweb-snapshot/src/snapshot.ts index 90458f16a3..64617ef05f 100644 --- a/packages/rrweb-snapshot/src/snapshot.ts +++ b/packages/rrweb-snapshot/src/snapshot.ts @@ -540,7 +540,8 @@ function serializeNode( attributes._cssText = absoluteToStylesheet(cssText, getHref()); } } - // form fields + // form fields, does not include textarea because it should not use the `value` attribute, + // it should have `textContent` instead. if ( tagName === 'input' || tagName === 'select' @@ -717,9 +718,7 @@ function serializeNode( } if (parentTagName === 'TEXTAREA' && textContent) { - // Ensure that textContent === attribute.value - // (masking options can make them different) - // replay will remove duplicate textContent. + // Treat textarea textContent as input textContent = maskInputValue({ input: n.parentNode as HTMLElement, maskInputSelector, diff --git a/packages/rrweb/test/integration.test.ts b/packages/rrweb/test/integration.test.ts index 8df44daf1c..cd3530261d 100644 --- a/packages/rrweb/test/integration.test.ts +++ b/packages/rrweb/test/integration.test.ts @@ -247,24 +247,6 @@ describe('record integration tests', function (this: ISuite) { assertSnapshot(snapshots); }); - it('should not record input values on selectively masked elements when maskAllInputs is disabled', async () => { - const page: puppeteer.Page = await browser.newPage(); - await page.goto('about:blank'); - await page.setContent( - getHtml.call(this, 'form-masked.html', { maskAllInputs: false, maskInputSelector: '.rr-mask' }), - ); - - await page.type('input[type="text"]', 'test'); - await page.click('input[type="radio"]'); - await page.click('input[type="checkbox"]'); - await page.type('input[type="password"]', 'password'); - await page.type('textarea', 'textarea test'); - await page.select('select', '1'); - - const snapshots = await page.evaluate('window.snapshots'); - assertSnapshot(snapshots); - }); - it('should record input values if dynamically added and maskAllInputs is false', async () => { const page: puppeteer.Page = await browser.newPage(); await page.goto('about:blank'); From 06f78b568d89c4449732f87158221be62136628c Mon Sep 17 00:00:00 2001 From: Billy Vong Date: Thu, 23 Feb 2023 14:37:32 -0500 Subject: [PATCH 3/3] remove textarea textContent, only use value --- packages/rrweb-snapshot/src/snapshot.ts | 19 +++-------- .../__snapshots__/integration.test.ts.snap | 5 ++- .../rrweb-snapshot/test/html/form-fields.html | 8 +++-- .../__snapshots__/integration.test.ts.snap | 34 ++++++++++++------- 4 files changed, 37 insertions(+), 29 deletions(-) diff --git a/packages/rrweb-snapshot/src/snapshot.ts b/packages/rrweb-snapshot/src/snapshot.ts index 64617ef05f..e60c96fab6 100644 --- a/packages/rrweb-snapshot/src/snapshot.ts +++ b/packages/rrweb-snapshot/src/snapshot.ts @@ -540,13 +540,13 @@ function serializeNode( attributes._cssText = absoluteToStylesheet(cssText, getHref()); } } - // form fields, does not include textarea because it should not use the `value` attribute, - // it should have `textContent` instead. + // form fields if ( tagName === 'input' || + tagName === 'textarea' || tagName === 'select' ) { - const value = (n as HTMLInputElement).value; + const value = (n as HTMLInputElement | HTMLTextAreaElement).value; if ( attributes.type !== 'radio' && attributes.type !== 'checkbox' && @@ -718,17 +718,8 @@ function serializeNode( } if (parentTagName === 'TEXTAREA' && textContent) { - // Treat textarea textContent as input - textContent = maskInputValue({ - input: n.parentNode as HTMLElement, - maskInputSelector, - unmaskInputSelector, - maskInputOptions, - tagName: parentTagName, - type: null, - value: textContent, - maskInputFn, - }); + // textarea textContent should be masked via `value` attributes + textContent = ''; } else if ( !isStyle && !isScript && diff --git a/packages/rrweb-snapshot/test/__snapshots__/integration.test.ts.snap b/packages/rrweb-snapshot/test/__snapshots__/integration.test.ts.snap index dbcc8c4cae..ad97b9a9f9 100644 --- a/packages/rrweb-snapshot/test/__snapshots__/integration.test.ts.snap +++ b/packages/rrweb-snapshot/test/__snapshots__/integration.test.ts.snap @@ -232,7 +232,10 @@ exports[`integration tests [html file]: form-fields.html 1`] = ` + +