From 5b92a358a562a02400115387ac10d3abcd36b284 Mon Sep 17 00:00:00 2001 From: Michael Dellanoce Date: Fri, 6 Jan 2023 14:14:11 -0500 Subject: [PATCH] support for mask all and unmask by selector and class #1096 --- guide.md | 4 + packages/rrweb-snapshot/src/snapshot.ts | 157 +- packages/rrweb-snapshot/test/snapshot.test.ts | 226 +++ packages/rrweb/src/record/index.ts | 12 + packages/rrweb/src/record/mutation.ts | 12 + packages/rrweb/src/types.ts | 10 + .../__snapshots__/integration.test.ts.snap | 1725 ++++++++++++++--- packages/rrweb/test/html/unmask-text.html | 23 + packages/rrweb/test/integration.test.ts | 25 + packages/types/src/index.ts | 1 + 10 files changed, 1923 insertions(+), 272 deletions(-) create mode 100644 packages/rrweb/test/html/unmask-text.html diff --git a/guide.md b/guide.md index e2dbf0d23f..7200df1f9e 100644 --- a/guide.md +++ b/guide.md @@ -144,8 +144,11 @@ The parameter of `rrweb.record` accepts the following options. | blockSelector | null | Use a string to configure which selector should be blocked, refer to the [privacy](#privacy) chapter | | ignoreClass | 'rr-ignore' | Use a string or RegExp to configure which elements should be ignored, refer to the [privacy](#privacy) chapter | | ignoreCSSAttributes | null | array of CSS attributes that should be ignored | +| maskAllText | false | mask all text content as \* | | maskTextClass | 'rr-mask' | Use a string or RegExp to configure which elements should be masked, refer to the [privacy](#privacy) chapter | +| unmaskTextClass | 'rr-unmask' | Use a string or RegExp to configure which elements should be unmasked, refer to the [privacy](#privacy) chapter | | maskTextSelector | null | Use a string to configure which selector should be masked, refer to the [privacy](#privacy) chapter | +| unmaskTextSelector | null | Use a string to configure which selector should be unmasked, refer to the [privacy](#privacy) chapter | | maskAllInputs | false | mask all input content as \* | | maskInputOptions | { password: true } | mask some kinds of input \*
refer to the [list](https://github.com/rrweb-io/rrweb/blob/588164aa12f1d94576f89ae0210b98f6e971c895/packages/rrweb-snapshot/src/types.ts#L77-L95) | | maskInputFn | - | customize mask input content recording logic | @@ -172,6 +175,7 @@ You may find some contents on the webpage which are not willing to be recorded, - An element with the class name `.rr-block` will not be recorded. Instead, it will replay as a placeholder with the same dimension. - An element with the class name `.rr-ignore` will not record its input events. - All text of elements with the class name `.rr-mask` and their children will be masked. +- All text of elements with the class name `.rr-unmask` and their children will be unmasked, unless any child is marked with `.rr-mask`. - `input[type="password"]` will be masked by default. - Mask options to mask the content in input elements. diff --git a/packages/rrweb-snapshot/src/snapshot.ts b/packages/rrweb-snapshot/src/snapshot.ts index f587bd83ed..95ccd7af76 100644 --- a/packages/rrweb-snapshot/src/snapshot.ts +++ b/packages/rrweb-snapshot/src/snapshot.ts @@ -292,26 +292,89 @@ export function classMatchesRegex( regex: RegExp, checkAncestors: boolean, ): boolean { - if (!node) return false; + return distanceToClassRegexMatch(node, regex, checkAncestors) >= 0; +} + +function distanceToClassRegexMatch( + node: Node | null, + regex: RegExp, + checkAncestors: boolean, + distance = 0, +): number { + if (!node) return -1; if (node.nodeType !== node.ELEMENT_NODE) { - if (!checkAncestors) return false; - return classMatchesRegex(node.parentNode, regex, checkAncestors); + if (!checkAncestors) return -1; + return distanceToClassRegexMatch(node.parentNode, regex, checkAncestors); } for (let eIndex = (node as HTMLElement).classList.length; eIndex--; ) { const className = (node as HTMLElement).classList[eIndex]; if (regex.test(className)) { - return true; + return distance; } } - if (!checkAncestors) return false; - return classMatchesRegex(node.parentNode, regex, checkAncestors); + if (!checkAncestors) return -1; + return distanceToClassRegexMatch( + node.parentNode, + regex, + checkAncestors, + distance + 1, + ); +} + +function distanceToSelectorMatch(el: HTMLElement, selector: string): number { + if (!el) return -1; + if (el.matches(selector)) return 0; + const closestParent = el.closest(selector); + if (closestParent) { + let current = el; + let distance = 0; + while (current && current !== closestParent) { + current = current.parentNode as HTMLElement; + if (!current) { + return -1; + } + distance++; + } + return distance; + } + return -1; +} + +function distanceToMatch( + el: HTMLElement, + className: string | RegExp, + selector: string | null, +): number { + let classDistance = -1; + let selectorDistance = -1; + + if (typeof className === 'string') { + classDistance = distanceToSelectorMatch(el, `.${className}`); + } else { + classDistance = distanceToClassRegexMatch(el, className, true); + } + + if (selector) { + selectorDistance = distanceToSelectorMatch(el, selector); + } + + return selectorDistance >= 0 + ? classDistance >= 0 + ? Math.min(classDistance, selectorDistance) + : selectorDistance + : classDistance >= 0 + ? classDistance + : -1; } export function needMaskingText( node: Node, maskTextClass: string | RegExp, maskTextSelector: string | null, + unmaskTextClass: string | RegExp, + unmaskTextSelector: string | null, + maskAllText: boolean, ): boolean { const el: HTMLElement | null = node.nodeType === node.ELEMENT_NODE @@ -319,18 +382,20 @@ export function needMaskingText( : node.parentElement; if (el === null) return false; - if (typeof maskTextClass === 'string') { - if (el.classList.contains(maskTextClass)) return true; - if (el.closest(`.${maskTextClass}`)) return true; - } else { - if (classMatchesRegex(el, maskTextClass, true)) return true; - } + const maskDistance = distanceToMatch(el, maskTextClass, maskTextSelector); + const unmaskDistance = distanceToMatch( + el, + unmaskTextClass, + unmaskTextSelector, + ); - if (maskTextSelector) { - if (el.matches(maskTextSelector)) return true; - if (el.closest(maskTextSelector)) return true; - } - return false; + return maskDistance >= 0 + ? unmaskDistance >= 0 + ? maskDistance <= unmaskDistance + : true + : unmaskDistance >= 0 + ? false + : !!maskAllText; } // https://stackoverflow.com/a/36155560 @@ -424,8 +489,11 @@ function serializeNode( mirror: Mirror; blockClass: string | RegExp; blockSelector: string | null; + maskAllText: boolean; maskTextClass: string | RegExp; + unmaskTextClass: string | RegExp; maskTextSelector: string | null; + unmaskTextSelector: string | null; inlineStylesheet: boolean; maskInputOptions: MaskInputOptions; maskTextFn: MaskTextFn | undefined; @@ -445,8 +513,11 @@ function serializeNode( mirror, blockClass, blockSelector, + maskAllText, maskTextClass, + unmaskTextClass, maskTextSelector, + unmaskTextSelector, inlineStylesheet, maskInputOptions = {}, maskTextFn, @@ -498,8 +569,11 @@ function serializeNode( }); case n.TEXT_NODE: return serializeTextNode(n as Text, { + maskAllText, maskTextClass, + unmaskTextClass, maskTextSelector, + unmaskTextSelector, maskTextFn, rootId, }); @@ -529,13 +603,24 @@ function getRootId(doc: Document, mirror: Mirror): number | undefined { function serializeTextNode( n: Text, options: { + maskAllText: boolean; maskTextClass: string | RegExp; + unmaskTextClass: string | RegExp; maskTextSelector: string | null; + unmaskTextSelector: string | null; maskTextFn: MaskTextFn | undefined; rootId: number | undefined; }, ): serializedNode { - const { maskTextClass, maskTextSelector, maskTextFn, rootId } = options; + const { + maskAllText, + maskTextClass, + unmaskTextClass, + maskTextSelector, + unmaskTextSelector, + maskTextFn, + rootId, + } = options; // The parent node may not be a html element which has a tagName attribute. // So just let it be undefined which is ok in this use case. const parentTagName = n.parentNode && (n.parentNode as HTMLElement).tagName; @@ -570,7 +655,14 @@ function serializeTextNode( !isStyle && !isScript && textContent && - needMaskingText(n, maskTextClass, maskTextSelector) + needMaskingText( + n, + maskTextClass, + maskTextSelector, + unmaskTextClass, + unmaskTextSelector, + maskAllText, + ) ) { textContent = maskTextFn ? maskTextFn(textContent) @@ -923,11 +1015,14 @@ export function serializeNodeWithId( blockClass: string | RegExp; blockSelector: string | null; maskTextClass: string | RegExp; + unmaskTextClass: string | RegExp; maskTextSelector: string | null; + unmaskTextSelector: string | null; skipChild: boolean; inlineStylesheet: boolean; newlyAddedElement?: boolean; maskInputOptions?: MaskInputOptions; + maskAllText: boolean; maskTextFn: MaskTextFn | undefined; maskInputFn: MaskInputFn | undefined; slimDOMOptions: SlimDOMOptions; @@ -954,8 +1049,11 @@ export function serializeNodeWithId( mirror, blockClass, blockSelector, + maskAllText, maskTextClass, + unmaskTextClass, maskTextSelector, + unmaskTextSelector, skipChild = false, inlineStylesheet = true, maskInputOptions = {}, @@ -979,8 +1077,11 @@ export function serializeNodeWithId( mirror, blockClass, blockSelector, + maskAllText, maskTextClass, + unmaskTextClass, maskTextSelector, + unmaskTextSelector, inlineStylesheet, maskInputOptions, maskTextFn, @@ -1051,8 +1152,11 @@ export function serializeNodeWithId( mirror, blockClass, blockSelector, + maskAllText, maskTextClass, + unmaskTextClass, maskTextSelector, + unmaskTextSelector, skipChild, inlineStylesheet, maskInputOptions, @@ -1111,8 +1215,11 @@ export function serializeNodeWithId( mirror, blockClass, blockSelector, + maskAllText, maskTextClass, + unmaskTextClass, maskTextSelector, + unmaskTextSelector, skipChild: false, inlineStylesheet, maskInputOptions, @@ -1158,8 +1265,11 @@ export function serializeNodeWithId( mirror, blockClass, blockSelector, + maskAllText, maskTextClass, + unmaskTextClass, maskTextSelector, + unmaskTextSelector, skipChild: false, inlineStylesheet, maskInputOptions, @@ -1199,8 +1309,11 @@ function snapshot( mirror?: Mirror; blockClass?: string | RegExp; blockSelector?: string | null; + maskAllText?: boolean; maskTextClass?: string | RegExp; + unmaskTextClass?: string | RegExp; maskTextSelector?: string | null; + unmaskTextSelector?: string | null; inlineStylesheet?: boolean; maskAllInputs?: boolean | MaskInputOptions; maskTextFn?: MaskTextFn; @@ -1228,8 +1341,11 @@ function snapshot( mirror = new Mirror(), blockClass = 'rr-block', blockSelector = null, + maskAllText = false, maskTextClass = 'rr-mask', + unmaskTextClass = 'rr-unmask', maskTextSelector = null, + unmaskTextSelector = null, inlineStylesheet = true, inlineImages = false, recordCanvas = false, @@ -1294,8 +1410,11 @@ function snapshot( mirror, blockClass, blockSelector, + maskAllText, maskTextClass, + unmaskTextClass, maskTextSelector, + unmaskTextSelector, skipChild: false, inlineStylesheet, maskInputOptions, diff --git a/packages/rrweb-snapshot/test/snapshot.test.ts b/packages/rrweb-snapshot/test/snapshot.test.ts index 75d635e0c0..7785b5bb06 100644 --- a/packages/rrweb-snapshot/test/snapshot.test.ts +++ b/packages/rrweb-snapshot/test/snapshot.test.ts @@ -6,6 +6,7 @@ import { absoluteToStylesheet, serializeNodeWithId, _isBlockedElement, + needMaskingText, } from '../src/snapshot'; import { serializedNodeWithId } from '../src/types'; import { Mirror } from '../src/utils'; @@ -143,8 +144,11 @@ describe('style elements', () => { mirror: new Mirror(), blockClass: 'blockblock', blockSelector: null, + maskAllText: false, maskTextClass: 'maskmask', + unmaskTextClass: 'unmaskmask', maskTextSelector: null, + unmaskTextSelector: null, skipChild: false, inlineStylesheet: true, maskTextFn: undefined, @@ -188,8 +192,11 @@ describe('scrollTop/scrollLeft', () => { mirror: new Mirror(), blockClass: 'blockblock', blockSelector: null, + maskAllText: false, maskTextClass: 'maskmask', + unmaskTextClass: 'unmaskmask', maskTextSelector: null, + unmaskTextSelector: null, skipChild: false, inlineStylesheet: true, maskTextFn: undefined, @@ -218,3 +225,222 @@ describe('scrollTop/scrollLeft', () => { }); }); }); + +describe('needMaskingText', () => { + const render = (html: string): HTMLDivElement => { + document.write(html); + return document.querySelector('div')!; + }; + + it('should not mask by default', () => { + const el = render(`
Lorem ipsum
`); + expect( + needMaskingText(el, 'maskmask', null, 'unmaskmask', null, false), + ).toEqual(false); + }); + + it('should mask if the masking class is matched', () => { + const el = render(`
Lorem ipsum
`); + expect( + needMaskingText(el, 'maskmask', null, 'unmaskmask', null, false), + ).toEqual(true); + expect( + needMaskingText(el, /^maskmask$/, null, /^unmaskmask$/, null, false), + ).toEqual(true); + }); + + it('should mask if the masking class is matched on an ancestor', () => { + const el = render( + `
Lorem ipsum
`, + ); + expect( + needMaskingText( + el.children[0], + 'maskmask', + null, + 'unmaskmask', + null, + false, + ), + ).toEqual(true); + expect( + needMaskingText( + el.children[0], + /^maskmask$/, + null, + /^unmaskmask$/, + null, + false, + ), + ).toEqual(true); + }); + + it('should mask if the masking selector is matched', () => { + const el = render(`
Lorem ipsum
`); + expect( + needMaskingText(el, 'maskmask', '.foo', 'unmaskmask', null, false), + ).toEqual(true); + }); + + it('should mask if the masking selector is matched on an ancestor', () => { + const el = render(`
Lorem ipsum
`); + expect( + needMaskingText( + el.children[0], + 'maskmask', + '.foo', + 'unmaskmask', + null, + false, + ), + ).toEqual(true); + }); + + it('should mask by default', () => { + const el = render(`
Lorem ipsum
`); + expect( + needMaskingText(el, 'maskmask', null, 'unmaskmask', null, true), + ).toEqual(true); + }); + + it('should not mask if the un-masking class is matched', () => { + const el = render(`
Lorem ipsum
`); + expect( + needMaskingText(el, 'maskmask', null, 'unmaskmask', null, true), + ).toEqual(false); + expect( + needMaskingText(el, /^maskmask$/, null, /^unmaskmask$/, null, true), + ).toEqual(false); + }); + + it('should not mask if the un-masking class is matched on an ancestor', () => { + const el = render( + `
Lorem ipsum
`, + ); + expect( + needMaskingText( + el.children[0], + 'maskmask', + null, + 'unmaskmask', + null, + true, + ), + ).toEqual(false); + expect( + needMaskingText( + el.children[0], + /^maskmask$/, + null, + /^unmaskmask$/, + null, + true, + ), + ).toEqual(false); + }); + + it('should mask if the masking class is more specific than the unmasking class', () => { + const el = render( + `
Lorem ipsum
`, + ); + expect( + needMaskingText( + el.children[0].children[0], + 'maskmask', + null, + 'unmaskmask', + null, + false, + ), + ).toEqual(true); + expect( + needMaskingText( + el.children[0].children[0], + /^maskmask$/, + null, + /^unmaskmask$/, + null, + false, + ), + ).toEqual(true); + }); + + it('should not mask if the unmasking class is more specific than the masking class', () => { + const el = render( + `
Lorem ipsum
`, + ); + expect( + needMaskingText( + el.children[0].children[0], + 'maskmask', + null, + 'unmaskmask', + null, + false, + ), + ).toEqual(false); + expect( + needMaskingText( + el.children[0].children[0], + /^maskmask$/, + null, + /^unmaskmask$/, + null, + false, + ), + ).toEqual(false); + }); + + it('should not mask if the unmasking selector is matched', () => { + const el = render(`
Lorem ipsum
`); + expect( + needMaskingText(el, 'maskmask', null, 'unmaskmask', '.foo', true), + ).toEqual(false); + }); + + it('should not mask if the unmasking selector is matched on an ancestor', () => { + const el = render(`
Lorem ipsum
`); + expect( + needMaskingText( + el.children[0], + 'maskmask', + null, + 'unmaskmask', + '.foo', + true, + ), + ).toEqual(false); + }); + + it('should mask if the masking selector is more specific than the unmasking selector', () => { + const el = render( + `
Lorem ipsum
`, + ); + expect( + needMaskingText( + el.children[0].children[0], + 'maskmask', + '.bar', + 'unmaskmask', + '.foo', + false, + ), + ).toEqual(true); + }); + + it('should not mask if the unmasking selector is more specific than the masking selector', () => { + const el = render( + `
Lorem ipsum
`, + ); + expect( + needMaskingText( + el.children[0].children[0], + 'maskmask', + '.bar', + 'unmaskmask', + '.foo', + false, + ), + ).toEqual(false); + }); +}); diff --git a/packages/rrweb/src/record/index.ts b/packages/rrweb/src/record/index.ts index c0e69a025f..e5c772cd35 100644 --- a/packages/rrweb/src/record/index.ts +++ b/packages/rrweb/src/record/index.ts @@ -63,8 +63,11 @@ function record( blockClass = 'rr-block', blockSelector = null, ignoreClass = 'rr-ignore', + maskAllText = false, maskTextClass = 'rr-mask', + unmaskTextClass = 'rr-unmask', maskTextSelector = null, + unmaskTextSelector = null, inlineStylesheet = true, maskAllInputs, maskInputOptions: _maskInputOptions, @@ -322,8 +325,11 @@ function record( bypassOptions: { blockClass, blockSelector, + maskAllText, maskTextClass, + unmaskTextClass, maskTextSelector, + unmaskTextSelector, inlineStylesheet, maskInputOptions, dataURLOptions, @@ -365,8 +371,11 @@ function record( mirror, blockClass, blockSelector, + maskAllText, maskTextClass, + unmaskTextClass, maskTextSelector, + unmaskTextSelector, inlineStylesheet, maskAllInputs: maskInputOptions, maskTextFn, @@ -521,8 +530,11 @@ function record( }, blockClass, ignoreClass, + maskAllText, maskTextClass, + unmaskTextClass, maskTextSelector, + unmaskTextSelector, maskInputOptions, inlineStylesheet, sampling, diff --git a/packages/rrweb/src/record/mutation.ts b/packages/rrweb/src/record/mutation.ts index aa351fee62..30488a9ec8 100644 --- a/packages/rrweb/src/record/mutation.ts +++ b/packages/rrweb/src/record/mutation.ts @@ -162,8 +162,11 @@ export default class MutationBuffer { private mutationCb: observerParam['mutationCb']; private blockClass: observerParam['blockClass']; private blockSelector: observerParam['blockSelector']; + private maskAllText: observerParam['maskAllText']; private maskTextClass: observerParam['maskTextClass']; + private unmaskTextClass: observerParam['unmaskTextClass']; private maskTextSelector: observerParam['maskTextSelector']; + private unmaskTextSelector: observerParam['unmaskTextSelector']; private inlineStylesheet: observerParam['inlineStylesheet']; private maskInputOptions: observerParam['maskInputOptions']; private maskTextFn: observerParam['maskTextFn']; @@ -187,8 +190,11 @@ export default class MutationBuffer { 'mutationCb', 'blockClass', 'blockSelector', + 'maskAllText', 'maskTextClass', + 'unmaskTextClass', 'maskTextSelector', + 'unmaskTextSelector', 'inlineStylesheet', 'maskInputOptions', 'maskTextFn', @@ -288,8 +294,11 @@ export default class MutationBuffer { mirror: this.mirror, blockClass: this.blockClass, blockSelector: this.blockSelector, + maskAllText: this.maskAllText, maskTextClass: this.maskTextClass, + unmaskTextClass: this.unmaskTextClass, maskTextSelector: this.maskTextSelector, + unmaskTextSelector: this.unmaskTextSelector, skipChild: true, newlyAddedElement: true, inlineStylesheet: this.inlineStylesheet, @@ -475,6 +484,9 @@ export default class MutationBuffer { m.target, this.maskTextClass, this.maskTextSelector, + this.unmaskTextClass, + this.unmaskTextSelector, + this.maskAllText, ) && value ? this.maskTextFn ? this.maskTextFn(value) diff --git a/packages/rrweb/src/types.ts b/packages/rrweb/src/types.ts index dd9a516709..9c4cbd3600 100644 --- a/packages/rrweb/src/types.ts +++ b/packages/rrweb/src/types.ts @@ -25,6 +25,7 @@ import type { KeepIframeSrcFn, listenerHandler, maskTextClass, + unmaskTextClass, mediaInteractionCallback, mouseInteractionCallBack, mousemoveCallBack, @@ -46,8 +47,11 @@ export type recordOptions = { blockClass?: blockClass; blockSelector?: string; ignoreClass?: string; + maskAllText?: boolean; maskTextClass?: maskTextClass; + unmaskTextClass?: unmaskTextClass; maskTextSelector?: string; + unmaskTextSelector?: string; maskAllInputs?: boolean; maskInputOptions?: MaskInputOptions; maskInputFn?: MaskInputFn; @@ -84,8 +88,11 @@ export type observerParam = { blockClass: blockClass; blockSelector: string | null; ignoreClass: string; + maskAllText: boolean; maskTextClass: maskTextClass; + unmaskTextClass: unmaskTextClass; maskTextSelector: string | null; + unmaskTextSelector: string | null; maskInputOptions: MaskInputOptions; maskInputFn?: MaskInputFn; maskTextFn?: MaskTextFn; @@ -126,8 +133,11 @@ export type MutationBufferParam = Pick< | 'mutationCb' | 'blockClass' | 'blockSelector' + | 'maskAllText' | 'maskTextClass' + | 'unmaskTextClass' | 'maskTextSelector' + | 'unmaskTextSelector' | 'inlineStylesheet' | 'maskInputOptions' | 'maskTextFn' diff --git a/packages/rrweb/test/__snapshots__/integration.test.ts.snap b/packages/rrweb/test/__snapshots__/integration.test.ts.snap index 7c6fdfbfda..fcc3d7a55d 100644 --- a/packages/rrweb/test/__snapshots__/integration.test.ts.snap +++ b/packages/rrweb/test/__snapshots__/integration.test.ts.snap @@ -3106,6 +3106,349 @@ exports[`record integration tests can record node mutations 1`] = ` ]" `; +exports[`record integration tests can selectively unmask parts of the page 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\\": \\"Unmask text\\", + \\"id\\": 13 + } + ], + \\"id\\": 12 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 14 + } + ], + \\"id\\": 4 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 15 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 17 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"div\\", + \\"attributes\\": { + \\"class\\": \\"rr-unmask\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"unmask1\\\\n \\", + \\"id\\": 19 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"div\\", + \\"attributes\\": { + \\"class\\": \\"rr-mask\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 21 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"span\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"*****\\", + \\"id\\": 23 + } + ], + \\"id\\": 22 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 24 + } + ], + \\"id\\": 20 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 25 + } + ], + \\"id\\": 18 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 26 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"div\\", + \\"attributes\\": { + \\"data-masking\\": \\"false\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 28 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"div\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 30 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"div\\", + \\"attributes\\": { + \\"data-masking\\": \\"true\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"*****\\", + \\"id\\": 32 + } + ], + \\"id\\": 31 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 33 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"div\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"unmask2\\", + \\"id\\": 35 + } + ], + \\"id\\": 34 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 36 + } + ], + \\"id\\": 29 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 37 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"ul\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 38 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 39 + } + ], + \\"id\\": 27 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\", + \\"id\\": 40 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 42 + } + ], + \\"id\\": 41 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\\\n\\\\n\\", + \\"id\\": 43 + } + ], + \\"id\\": 16 + } + ], + \\"id\\": 3 + } + ], + \\"id\\": 1 + }, + \\"initialOffset\\": { + \\"left\\": 0, + \\"top\\": 0 + } + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [], + \\"removes\\": [], + \\"adds\\": [ + { + \\"parentId\\": 38, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"li\\", + \\"attributes\\": { + \\"class\\": \\"rr-mask\\" + }, + \\"childNodes\\": [], + \\"id\\": 44 + } + }, + { + \\"parentId\\": 44, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 3, + \\"textContent\\": \\"*** **** ****\\", + \\"id\\": 45 + } + } + ] + } + } +]" +`; + exports[`record integration tests can use maskInputOptions to configure which type of inputs should be masked 1`] = ` "[ { @@ -3588,34 +3931,861 @@ exports[`record integration tests can use maskInputOptions to configure which ty \\"type\\": 3, \\"data\\": { \\"source\\": 5, - \\"text\\": \\"t\\", - \\"isChecked\\": false, - \\"id\\": 22 - } - }, - { - \\"type\\": 3, - \\"data\\": { - \\"source\\": 5, - \\"text\\": \\"te\\", - \\"isChecked\\": false, - \\"id\\": 22 - } - }, - { - \\"type\\": 3, - \\"data\\": { - \\"source\\": 5, - \\"text\\": \\"tes\\", - \\"isChecked\\": false, - \\"id\\": 22 - } - }, - { - \\"type\\": 3, - \\"data\\": { - \\"source\\": 5, - \\"text\\": \\"test\\", + \\"text\\": \\"t\\", + \\"isChecked\\": false, + \\"id\\": 22 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"te\\", + \\"isChecked\\": false, + \\"id\\": 22 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"tes\\", + \\"isChecked\\": false, + \\"id\\": 22 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"test\\", + \\"isChecked\\": false, + \\"id\\": 22 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 1, + \\"id\\": 27 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 6, + \\"id\\": 22 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 5, + \\"id\\": 27 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 0, + \\"id\\": 27 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 2, + \\"id\\": 27 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"on\\", + \\"isChecked\\": true, + \\"id\\": 27 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"off\\", + \\"isChecked\\": false, + \\"id\\": 32 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 1, + \\"id\\": 37 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 6, + \\"id\\": 27 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 5, + \\"id\\": 37 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 0, + \\"id\\": 37 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 2, + \\"id\\": 37 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"on\\", + \\"isChecked\\": true, + \\"id\\": 37 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 6, + \\"id\\": 37 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 5, + \\"id\\": 42 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"t\\", + \\"isChecked\\": false, + \\"id\\": 42 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"te\\", + \\"isChecked\\": false, + \\"id\\": 42 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"tex\\", + \\"isChecked\\": false, + \\"id\\": 42 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"text\\", + \\"isChecked\\": false, + \\"id\\": 42 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"texta\\", + \\"isChecked\\": false, + \\"id\\": 42 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"textar\\", + \\"isChecked\\": false, + \\"id\\": 42 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"textare\\", + \\"isChecked\\": false, + \\"id\\": 42 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"textarea\\", + \\"isChecked\\": false, + \\"id\\": 42 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"textarea \\", + \\"isChecked\\": false, + \\"id\\": 42 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"textarea t\\", + \\"isChecked\\": false, + \\"id\\": 42 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"textarea te\\", + \\"isChecked\\": false, + \\"id\\": 42 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"textarea tes\\", + \\"isChecked\\": false, + \\"id\\": 42 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"textarea test\\", + \\"isChecked\\": false, + \\"id\\": 42 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 6, + \\"id\\": 42 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 5, + \\"id\\": 59 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"*\\", + \\"isChecked\\": false, + \\"id\\": 59 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"**\\", + \\"isChecked\\": false, + \\"id\\": 59 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"***\\", + \\"isChecked\\": false, + \\"id\\": 59 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"****\\", + \\"isChecked\\": false, + \\"id\\": 59 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"*****\\", + \\"isChecked\\": false, + \\"id\\": 59 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"******\\", + \\"isChecked\\": false, + \\"id\\": 59 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"*******\\", + \\"isChecked\\": false, + \\"id\\": 59 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"********\\", + \\"isChecked\\": false, + \\"id\\": 59 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"1\\", + \\"isChecked\\": false, + \\"id\\": 47 + } + } +]" +`; + +exports[`record integration tests can use maskTextSelector to configure which inputs should be masked 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\\": \\"form fields\\", + \\"id\\": 13 + } + ], + \\"id\\": 12 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 14 + } + ], + \\"id\\": 4 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n\\\\n \\", + \\"id\\": 15 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 17 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"form\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 19 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"label\\", + \\"attributes\\": { + \\"for\\": \\"text\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 21 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"input\\", + \\"attributes\\": { + \\"type\\": \\"text\\" + }, + \\"childNodes\\": [], + \\"id\\": 22 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 23 + } + ], + \\"id\\": 20 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 24 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"label\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 26 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"input\\", + \\"attributes\\": { + \\"type\\": \\"radio\\", + \\"name\\": \\"toggle\\", + \\"value\\": \\"on\\" + }, + \\"childNodes\\": [], + \\"id\\": 27 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 28 + } + ], + \\"id\\": 25 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 29 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"label\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 31 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"input\\", + \\"attributes\\": { + \\"type\\": \\"radio\\", + \\"name\\": \\"toggle\\", + \\"value\\": \\"off\\", + \\"checked\\": true + }, + \\"childNodes\\": [], + \\"id\\": 32 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 33 + } + ], + \\"id\\": 30 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 34 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"label\\", + \\"attributes\\": { + \\"for\\": \\"checkbox\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 36 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"input\\", + \\"attributes\\": { + \\"type\\": \\"checkbox\\" + }, + \\"childNodes\\": [], + \\"id\\": 37 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 38 + } + ], + \\"id\\": 35 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 39 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"label\\", + \\"attributes\\": { + \\"for\\": \\"textarea\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 41 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"textarea\\", + \\"attributes\\": { + \\"name\\": \\"\\", + \\"id\\": \\"\\", + \\"cols\\": \\"30\\", + \\"rows\\": \\"10\\" + }, + \\"childNodes\\": [], + \\"id\\": 42 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 43 + } + ], + \\"id\\": 40 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 44 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"label\\", + \\"attributes\\": { + \\"for\\": \\"select\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 46 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"select\\", + \\"attributes\\": { + \\"name\\": \\"\\", + \\"id\\": \\"\\", + \\"value\\": \\"1\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 48 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"option\\", + \\"attributes\\": { + \\"value\\": \\"1\\", + \\"selected\\": true + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"1\\", + \\"id\\": 50 + } + ], + \\"id\\": 49 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 51 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"option\\", + \\"attributes\\": { + \\"value\\": \\"2\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"2\\", + \\"id\\": 53 + } + ], + \\"id\\": 52 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 54 + } + ], + \\"id\\": 47 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 55 + } + ], + \\"id\\": 45 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 56 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"label\\", + \\"attributes\\": { + \\"for\\": \\"password\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 58 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"input\\", + \\"attributes\\": { + \\"type\\": \\"password\\" + }, + \\"childNodes\\": [], + \\"id\\": 59 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 60 + } + ], + \\"id\\": 57 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 61 + } + ], + \\"id\\": 18 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\", + \\"id\\": 62 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 64 + } + ], + \\"id\\": 63 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\\\n\\\\n\\", + \\"id\\": 65 + } + ], + \\"id\\": 16 + } + ], + \\"id\\": 3 + } + ], + \\"id\\": 1 + }, + \\"initialOffset\\": { + \\"left\\": 0, + \\"top\\": 0 + } + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 5, + \\"id\\": 22 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"**********\\", \\"isChecked\\": false, \\"id\\": 22 } @@ -3623,319 +4793,148 @@ exports[`record integration tests can use maskInputOptions to configure which ty { \\"type\\": 3, \\"data\\": { - \\"source\\": 2, - \\"type\\": 1, - \\"id\\": 27 - } - }, - { - \\"type\\": 3, - \\"data\\": { - \\"source\\": 2, - \\"type\\": 6, - \\"id\\": 22 - } - }, - { - \\"type\\": 3, - \\"data\\": { - \\"source\\": 2, - \\"type\\": 5, - \\"id\\": 27 - } - }, - { - \\"type\\": 3, - \\"data\\": { - \\"source\\": 2, - \\"type\\": 0, - \\"id\\": 27 - } - }, - { - \\"type\\": 3, - \\"data\\": { - \\"source\\": 2, - \\"type\\": 2, - \\"id\\": 27 - } - }, - { - \\"type\\": 3, - \\"data\\": { - \\"source\\": 5, - \\"text\\": \\"on\\", - \\"isChecked\\": true, - \\"id\\": 27 - } - }, - { - \\"type\\": 3, - \\"data\\": { - \\"source\\": 5, - \\"text\\": \\"off\\", - \\"isChecked\\": false, - \\"id\\": 32 - } - }, - { - \\"type\\": 3, - \\"data\\": { - \\"source\\": 2, - \\"type\\": 1, - \\"id\\": 37 - } - }, - { - \\"type\\": 3, - \\"data\\": { - \\"source\\": 2, - \\"type\\": 6, - \\"id\\": 27 - } - }, - { - \\"type\\": 3, - \\"data\\": { - \\"source\\": 2, - \\"type\\": 5, - \\"id\\": 37 - } - }, - { - \\"type\\": 3, - \\"data\\": { - \\"source\\": 2, - \\"type\\": 0, - \\"id\\": 37 - } - }, - { - \\"type\\": 3, - \\"data\\": { - \\"source\\": 2, - \\"type\\": 2, - \\"id\\": 37 - } - }, - { - \\"type\\": 3, - \\"data\\": { - \\"source\\": 5, - \\"text\\": \\"on\\", - \\"isChecked\\": true, - \\"id\\": 37 - } - }, - { - \\"type\\": 3, - \\"data\\": { - \\"source\\": 2, - \\"type\\": 6, - \\"id\\": 37 - } - }, - { - \\"type\\": 3, - \\"data\\": { - \\"source\\": 2, - \\"type\\": 5, - \\"id\\": 42 - } - }, - { - \\"type\\": 3, - \\"data\\": { - \\"source\\": 5, - \\"text\\": \\"t\\", - \\"isChecked\\": false, - \\"id\\": 42 - } - }, - { - \\"type\\": 3, - \\"data\\": { - \\"source\\": 5, - \\"text\\": \\"te\\", - \\"isChecked\\": false, - \\"id\\": 42 - } - }, - { - \\"type\\": 3, - \\"data\\": { - \\"source\\": 5, - \\"text\\": \\"tex\\", - \\"isChecked\\": false, - \\"id\\": 42 - } - }, - { - \\"type\\": 3, - \\"data\\": { - \\"source\\": 5, - \\"text\\": \\"text\\", - \\"isChecked\\": false, - \\"id\\": 42 - } - }, - { - \\"type\\": 3, - \\"data\\": { - \\"source\\": 5, - \\"text\\": \\"texta\\", - \\"isChecked\\": false, - \\"id\\": 42 + \\"source\\": 2, + \\"type\\": 1, + \\"id\\": 27 } }, { \\"type\\": 3, \\"data\\": { - \\"source\\": 5, - \\"text\\": \\"textar\\", - \\"isChecked\\": false, - \\"id\\": 42 + \\"source\\": 2, + \\"type\\": 6, + \\"id\\": 22 } }, { \\"type\\": 3, \\"data\\": { - \\"source\\": 5, - \\"text\\": \\"textare\\", - \\"isChecked\\": false, - \\"id\\": 42 + \\"source\\": 2, + \\"type\\": 5, + \\"id\\": 27 } }, { \\"type\\": 3, \\"data\\": { - \\"source\\": 5, - \\"text\\": \\"textarea\\", - \\"isChecked\\": false, - \\"id\\": 42 + \\"source\\": 2, + \\"type\\": 0, + \\"id\\": 27 } }, { \\"type\\": 3, \\"data\\": { - \\"source\\": 5, - \\"text\\": \\"textarea \\", - \\"isChecked\\": false, - \\"id\\": 42 + \\"source\\": 2, + \\"type\\": 2, + \\"id\\": 27 } }, { \\"type\\": 3, \\"data\\": { \\"source\\": 5, - \\"text\\": \\"textarea t\\", - \\"isChecked\\": false, - \\"id\\": 42 + \\"text\\": \\"on\\", + \\"isChecked\\": true, + \\"id\\": 27 } }, { \\"type\\": 3, \\"data\\": { \\"source\\": 5, - \\"text\\": \\"textarea te\\", + \\"text\\": \\"off\\", \\"isChecked\\": false, - \\"id\\": 42 + \\"id\\": 32 } }, { \\"type\\": 3, \\"data\\": { - \\"source\\": 5, - \\"text\\": \\"textarea tes\\", - \\"isChecked\\": false, - \\"id\\": 42 + \\"source\\": 2, + \\"type\\": 1, + \\"id\\": 37 } }, { \\"type\\": 3, \\"data\\": { - \\"source\\": 5, - \\"text\\": \\"textarea test\\", - \\"isChecked\\": false, - \\"id\\": 42 + \\"source\\": 2, + \\"type\\": 6, + \\"id\\": 27 } }, { \\"type\\": 3, \\"data\\": { \\"source\\": 2, - \\"type\\": 6, - \\"id\\": 42 + \\"type\\": 5, + \\"id\\": 37 } }, { \\"type\\": 3, \\"data\\": { \\"source\\": 2, - \\"type\\": 5, - \\"id\\": 59 + \\"type\\": 0, + \\"id\\": 37 } }, { \\"type\\": 3, \\"data\\": { - \\"source\\": 5, - \\"text\\": \\"*\\", - \\"isChecked\\": false, - \\"id\\": 59 + \\"source\\": 2, + \\"type\\": 2, + \\"id\\": 37 } }, { \\"type\\": 3, \\"data\\": { \\"source\\": 5, - \\"text\\": \\"**\\", - \\"isChecked\\": false, - \\"id\\": 59 + \\"text\\": \\"on\\", + \\"isChecked\\": true, + \\"id\\": 37 } }, { \\"type\\": 3, \\"data\\": { - \\"source\\": 5, - \\"text\\": \\"***\\", - \\"isChecked\\": false, - \\"id\\": 59 + \\"source\\": 2, + \\"type\\": 6, + \\"id\\": 37 } }, { \\"type\\": 3, \\"data\\": { - \\"source\\": 5, - \\"text\\": \\"****\\", - \\"isChecked\\": false, - \\"id\\": 59 + \\"source\\": 2, + \\"type\\": 5, + \\"id\\": 42 } }, { \\"type\\": 3, \\"data\\": { \\"source\\": 5, - \\"text\\": \\"*****\\", + \\"text\\": \\"**********\\", \\"isChecked\\": false, - \\"id\\": 59 + \\"id\\": 42 } }, { \\"type\\": 3, \\"data\\": { - \\"source\\": 5, - \\"text\\": \\"******\\", - \\"isChecked\\": false, - \\"id\\": 59 + \\"source\\": 2, + \\"type\\": 6, + \\"id\\": 42 } }, { \\"type\\": 3, \\"data\\": { - \\"source\\": 5, - \\"text\\": \\"*******\\", - \\"isChecked\\": false, + \\"source\\": 2, + \\"type\\": 5, \\"id\\": 59 } }, @@ -3943,7 +4942,7 @@ exports[`record integration tests can use maskInputOptions to configure which ty \\"type\\": 3, \\"data\\": { \\"source\\": 5, - \\"text\\": \\"********\\", + \\"text\\": \\"**********\\", \\"isChecked\\": false, \\"id\\": 59 } @@ -5083,6 +6082,226 @@ exports[`record integration tests should handle recursive console messages 1`] = } } }, + { + \\"type\\": 6, + \\"data\\": { + \\"plugin\\": \\"rrweb/console@1\\", + \\"payload\\": { + \\"level\\": \\"warn\\", + \\"trace\\": [ + \\"Object.get (__puppeteer_evaluation_script__:13:33)\\", + \\"isObjTooDeep (:8503:25)\\", + \\"isObjTooDeep (:8503:35)\\", + \\"shouldIgnore (:8567:31)\\", + \\"Object. (:8536:13)\\", + \\"stringify (:8517:19)\\", + \\":8656:47\\" + ], + \\"payload\\": [ + \\"\\\\\\"proxied was accessed so triggering a console.warn\\\\\\"\\", + \\"\\\\\\"[object Object]\\\\\\"\\" + ] + } + } + }, + { + \\"type\\": 6, + \\"data\\": { + \\"plugin\\": \\"rrweb/console@1\\", + \\"payload\\": { + \\"level\\": \\"warn\\", + \\"trace\\": [ + \\"Object.get (__puppeteer_evaluation_script__:13:33)\\", + \\"isObjTooDeep (:8503:25)\\", + \\"isObjTooDeep (:8503:35)\\", + \\"shouldIgnore (:8567:31)\\", + \\"Object. (:8536:13)\\", + \\"stringify (:8517:19)\\", + \\":8656:47\\" + ], + \\"payload\\": [ + \\"\\\\\\"proxied was accessed so triggering a console.warn\\\\\\"\\", + \\"\\\\\\"[object Object]\\\\\\"\\" + ] + } + } + }, + { + \\"type\\": 6, + \\"data\\": { + \\"plugin\\": \\"rrweb/console@1\\", + \\"payload\\": { + \\"level\\": \\"warn\\", + \\"trace\\": [ + \\"Object.get (__puppeteer_evaluation_script__:13:33)\\", + \\"isObjTooDeep (:8503:25)\\", + \\"isObjTooDeep (:8503:35)\\", + \\"shouldIgnore (:8567:31)\\", + \\"Object. (:8536:13)\\", + \\"stringify (:8517:19)\\", + \\":8656:47\\" + ], + \\"payload\\": [ + \\"\\\\\\"proxied was accessed so triggering a console.warn\\\\\\"\\", + \\"\\\\\\"[object Object]\\\\\\"\\" + ] + } + } + }, + { + \\"type\\": 6, + \\"data\\": { + \\"plugin\\": \\"rrweb/console@1\\", + \\"payload\\": { + \\"level\\": \\"warn\\", + \\"trace\\": [ + \\"Object.get (__puppeteer_evaluation_script__:13:33)\\", + \\"isObjTooDeep (:8503:25)\\", + \\"isObjTooDeep (:8503:35)\\", + \\"shouldIgnore (:8567:31)\\", + \\"Object. (:8536:13)\\", + \\"stringify (:8517:19)\\", + \\":8656:47\\" + ], + \\"payload\\": [ + \\"\\\\\\"proxied was accessed so triggering a console.warn\\\\\\"\\", + \\"\\\\\\"[object Object]\\\\\\"\\" + ] + } + } + }, + { + \\"type\\": 6, + \\"data\\": { + \\"plugin\\": \\"rrweb/console@1\\", + \\"payload\\": { + \\"level\\": \\"warn\\", + \\"trace\\": [ + \\"Object.get (__puppeteer_evaluation_script__:13:33)\\", + \\"isObjTooDeep (:8503:25)\\", + \\"isObjTooDeep (:8503:35)\\", + \\"shouldIgnore (:8567:31)\\", + \\"Object. (:8536:13)\\", + \\"stringify (:8517:19)\\", + \\":8656:47\\" + ], + \\"payload\\": [ + \\"\\\\\\"proxied was accessed so triggering a console.warn\\\\\\"\\", + \\"\\\\\\"[object Object]\\\\\\"\\" + ] + } + } + }, + { + \\"type\\": 6, + \\"data\\": { + \\"plugin\\": \\"rrweb/console@1\\", + \\"payload\\": { + \\"level\\": \\"warn\\", + \\"trace\\": [ + \\"Object.get (__puppeteer_evaluation_script__:13:33)\\", + \\"isObjTooDeep (:8503:25)\\", + \\"isObjTooDeep (:8503:35)\\", + \\"shouldIgnore (:8567:31)\\", + \\"Object. (:8536:13)\\", + \\"stringify (:8517:19)\\", + \\":8656:47\\" + ], + \\"payload\\": [ + \\"\\\\\\"proxied was accessed so triggering a console.warn\\\\\\"\\", + \\"\\\\\\"[object Object]\\\\\\"\\" + ] + } + } + }, + { + \\"type\\": 6, + \\"data\\": { + \\"plugin\\": \\"rrweb/console@1\\", + \\"payload\\": { + \\"level\\": \\"warn\\", + \\"trace\\": [ + \\"Object.get (__puppeteer_evaluation_script__:13:33)\\", + \\"isObjTooDeep (:8503:25)\\", + \\"isObjTooDeep (:8503:35)\\", + \\"shouldIgnore (:8567:31)\\", + \\"Object. (:8536:13)\\", + \\"stringify (:8517:19)\\", + \\":8656:47\\" + ], + \\"payload\\": [ + \\"\\\\\\"proxied was accessed so triggering a console.warn\\\\\\"\\", + \\"\\\\\\"[object Object]\\\\\\"\\" + ] + } + } + }, + { + \\"type\\": 6, + \\"data\\": { + \\"plugin\\": \\"rrweb/console@1\\", + \\"payload\\": { + \\"level\\": \\"warn\\", + \\"trace\\": [ + \\"Object.get (__puppeteer_evaluation_script__:13:33)\\", + \\"isObjTooDeep (:8503:25)\\", + \\"isObjTooDeep (:8503:35)\\", + \\"shouldIgnore (:8567:31)\\", + \\"Object. (:8536:13)\\", + \\"stringify (:8517:19)\\", + \\":8656:47\\" + ], + \\"payload\\": [ + \\"\\\\\\"proxied was accessed so triggering a console.warn\\\\\\"\\", + \\"\\\\\\"[object Object]\\\\\\"\\" + ] + } + } + }, + { + \\"type\\": 6, + \\"data\\": { + \\"plugin\\": \\"rrweb/console@1\\", + \\"payload\\": { + \\"level\\": \\"warn\\", + \\"trace\\": [ + \\"Object.get (__puppeteer_evaluation_script__:13:33)\\", + \\"isObjTooDeep (:8503:25)\\", + \\"isObjTooDeep (:8503:35)\\", + \\"shouldIgnore (:8567:31)\\", + \\"Object. (:8536:13)\\", + \\"stringify (:8517:19)\\", + \\":8656:47\\" + ], + \\"payload\\": [ + \\"\\\\\\"proxied was accessed so triggering a console.warn\\\\\\"\\", + \\"\\\\\\"[object Object]\\\\\\"\\" + ] + } + } + }, + { + \\"type\\": 6, + \\"data\\": { + \\"plugin\\": \\"rrweb/console@1\\", + \\"payload\\": { + \\"level\\": \\"warn\\", + \\"trace\\": [ + \\"Object.get (__puppeteer_evaluation_script__:13:33)\\", + \\"isObjTooDeep (:8503:25)\\", + \\"shouldIgnore (:8567:31)\\", + \\"Object. (:8536:13)\\", + \\"stringify (:8517:19)\\", + \\":8656:47\\", + \\"console.log (:8656:36)\\" + ], + \\"payload\\": [ + \\"\\\\\\"proxied was accessed so triggering a console.warn\\\\\\"\\", + \\"\\\\\\"[object Object]\\\\\\"\\" + ] + } + } + }, { \\"type\\": 6, \\"data\\": { @@ -8877,6 +10096,10 @@ exports[`record integration tests should record after DOMContentLoaded event 1`] \\"type\\": 0, \\"data\\": {} }, + { + \\"type\\": 1, + \\"data\\": {} + }, { \\"type\\": 4, \\"data\\": { @@ -8946,10 +10169,6 @@ exports[`record integration tests should record after DOMContentLoaded event 1`] \\"top\\": 0 } } - }, - { - \\"type\\": 1, - \\"data\\": {} } ]" `; diff --git a/packages/rrweb/test/html/unmask-text.html b/packages/rrweb/test/html/unmask-text.html new file mode 100644 index 0000000000..a71dc04205 --- /dev/null +++ b/packages/rrweb/test/html/unmask-text.html @@ -0,0 +1,23 @@ + + + + + + + Unmask text + + +
unmask1 +
+ mask1 +
+
+
+
+
mask2
+
unmask2
+
+
    +
    + + diff --git a/packages/rrweb/test/integration.test.ts b/packages/rrweb/test/integration.test.ts index fec8c36902..90d24d2089 100644 --- a/packages/rrweb/test/integration.test.ts +++ b/packages/rrweb/test/integration.test.ts @@ -1064,6 +1064,31 @@ describe('record integration tests', function (this: ISuite) { assertSnapshot(snapshots); }); + it('can selectively unmask parts of the page', async () => { + const page: puppeteer.Page = await browser.newPage(); + await page.goto('about:blank'); + await page.setContent( + getHtml.call(this, 'unmask-text.html', { + maskAllText: true, + maskTextSelector: '[data-masking="true"]', + unmaskTextSelector: '[data-masking="false"]', + }), + ); + + await page.evaluate(() => { + const li = document.createElement('li'); + const ul = document.querySelector('ul') as HTMLUListElement; + li.className = 'rr-mask'; + ul.appendChild(li); + li.innerText = 'new list item'; + }); + + const snapshots = (await page.evaluate( + 'window.snapshots', + )) as eventWithTime[]; + assertSnapshot(snapshots); + }); + it('should record after DOMContentLoaded event', async () => { const page: puppeteer.Page = await browser.newPage(); await page.goto('about:blank'); diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 1188ef2fb7..283eeeea63 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -179,6 +179,7 @@ export type canvasEventWithTime = eventWithTime & { export type blockClass = string | RegExp; export type maskTextClass = string | RegExp; +export type unmaskTextClass = string | RegExp; export type SamplingStrategy = Partial<{ /**