From 3bfa800b6f5a07618332adec6bf5b8d23e2491d9 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 | 159 +- 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 | 1413 ++++++++++++++--- .../test/__snapshots__/record.test.ts.snap | 59 +- packages/rrweb/test/html/unmask-text.html | 23 + packages/rrweb/test/integration.test.ts | 25 + packages/rrweb/test/utils.ts | 2 + packages/types/src/index.ts | 1 + 12 files changed, 1680 insertions(+), 266 deletions(-) create mode 100644 packages/rrweb/test/html/unmask-text.html diff --git a/guide.md b/guide.md index d7807bf00d..036af9aa61 100644 --- a/guide.md +++ b/guide.md @@ -145,8 +145,11 @@ The parameter of `rrweb.record` accepts the following options. | ignoreClass | 'rr-ignore' | Use a string or RegExp to configure which elements should be ignored, refer to the [privacy](#privacy) chapter | | ignoreSelector | null | Use a string to configure which selector 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 | @@ -173,6 +176,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 0963e6cfef..e3b3f87313 100644 --- a/packages/rrweb-snapshot/src/snapshot.ts +++ b/packages/rrweb-snapshot/src/snapshot.ts @@ -290,26 +290,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 { try { const el: HTMLElement | null = @@ -318,21 +381,25 @@ 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 maskDistance >= 0 + ? unmaskDistance >= 0 + ? maskDistance <= unmaskDistance + : true + : unmaskDistance >= 0 + ? false + : !!maskAllText; } catch (e) { // } - return false; + + return !!maskAllText; } // https://stackoverflow.com/a/36155560 @@ -426,8 +493,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; @@ -447,8 +517,11 @@ function serializeNode( mirror, blockClass, blockSelector, + maskAllText, maskTextClass, + unmaskTextClass, maskTextSelector, + unmaskTextSelector, inlineStylesheet, maskInputOptions = {}, maskTextFn, @@ -500,8 +573,11 @@ function serializeNode( }); case n.TEXT_NODE: return serializeTextNode(n as Text, { + maskAllText, maskTextClass, + unmaskTextClass, maskTextSelector, + unmaskTextSelector, maskTextFn, rootId, }); @@ -531,13 +607,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; @@ -572,7 +659,14 @@ function serializeTextNode( !isStyle && !isScript && textContent && - needMaskingText(n, maskTextClass, maskTextSelector) + needMaskingText( + n, + maskTextClass, + maskTextSelector, + unmaskTextClass, + unmaskTextSelector, + maskAllText, + ) ) { textContent = maskTextFn ? maskTextFn(textContent, n.parentElement) @@ -922,11 +1016,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; @@ -953,8 +1050,11 @@ export function serializeNodeWithId( mirror, blockClass, blockSelector, + maskAllText, maskTextClass, + unmaskTextClass, maskTextSelector, + unmaskTextSelector, skipChild = false, inlineStylesheet = true, maskInputOptions = {}, @@ -978,8 +1078,11 @@ export function serializeNodeWithId( mirror, blockClass, blockSelector, + maskAllText, maskTextClass, + unmaskTextClass, maskTextSelector, + unmaskTextSelector, inlineStylesheet, maskInputOptions, maskTextFn, @@ -1050,8 +1153,11 @@ export function serializeNodeWithId( mirror, blockClass, blockSelector, + maskAllText, maskTextClass, + unmaskTextClass, maskTextSelector, + unmaskTextSelector, skipChild, inlineStylesheet, maskInputOptions, @@ -1110,8 +1216,11 @@ export function serializeNodeWithId( mirror, blockClass, blockSelector, + maskAllText, maskTextClass, + unmaskTextClass, maskTextSelector, + unmaskTextSelector, skipChild: false, inlineStylesheet, maskInputOptions, @@ -1157,8 +1266,11 @@ export function serializeNodeWithId( mirror, blockClass, blockSelector, + maskAllText, maskTextClass, + unmaskTextClass, maskTextSelector, + unmaskTextSelector, skipChild: false, inlineStylesheet, maskInputOptions, @@ -1198,8 +1310,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; @@ -1227,8 +1342,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, @@ -1293,8 +1411,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 7308ac6d04..c10438c5f3 100644 --- a/packages/rrweb/src/record/index.ts +++ b/packages/rrweb/src/record/index.ts @@ -65,8 +65,11 @@ function record( blockSelector = null, ignoreClass = 'rr-ignore', ignoreSelector = null, + maskAllText = false, maskTextClass = 'rr-mask', + unmaskTextClass = 'rr-unmask', maskTextSelector = null, + unmaskTextSelector = null, inlineStylesheet = true, maskAllInputs, maskInputOptions: _maskInputOptions, @@ -325,8 +328,11 @@ function record( bypassOptions: { blockClass, blockSelector, + maskAllText, maskTextClass, + unmaskTextClass, maskTextSelector, + unmaskTextSelector, inlineStylesheet, maskInputOptions, dataURLOptions, @@ -371,8 +377,11 @@ function record( mirror, blockClass, blockSelector, + maskAllText, maskTextClass, + unmaskTextClass, maskTextSelector, + unmaskTextSelector, inlineStylesheet, maskAllInputs: maskInputOptions, maskTextFn, @@ -528,8 +537,11 @@ function record( blockClass, ignoreClass, ignoreSelector, + 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 80943d96ab..c1aa909e04 100644 --- a/packages/rrweb/src/record/mutation.ts +++ b/packages/rrweb/src/record/mutation.ts @@ -171,8 +171,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']; @@ -196,8 +199,11 @@ export default class MutationBuffer { 'mutationCb', 'blockClass', 'blockSelector', + 'maskAllText', 'maskTextClass', + 'unmaskTextClass', 'maskTextSelector', + 'unmaskTextSelector', 'inlineStylesheet', 'maskInputOptions', 'maskTextFn', @@ -298,8 +304,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, @@ -520,6 +529,9 @@ export default class MutationBuffer { m.target, this.maskTextClass, this.maskTextSelector, + this.unmaskTextClass, + this.unmaskTextSelector, + this.maskAllText, ) && value ? this.maskTextFn ? this.maskTextFn(value, closestElementOfNode(m.target)) diff --git a/packages/rrweb/src/types.ts b/packages/rrweb/src/types.ts index 1ceb44222b..cd23e3205f 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, @@ -47,8 +48,11 @@ export type recordOptions = { blockSelector?: string; ignoreClass?: string; ignoreSelector?: string; + maskAllText?: boolean; maskTextClass?: maskTextClass; + unmaskTextClass?: unmaskTextClass; maskTextSelector?: string; + unmaskTextSelector?: string; maskAllInputs?: boolean; maskInputOptions?: MaskInputOptions; maskInputFn?: MaskInputFn; @@ -87,8 +91,11 @@ export type observerParam = { blockSelector: string | null; ignoreClass: string; ignoreSelector: string | null; + maskAllText: boolean; maskTextClass: maskTextClass; + unmaskTextClass: unmaskTextClass; maskTextSelector: string | null; + unmaskTextSelector: string | null; maskInputOptions: MaskInputOptions; maskInputFn?: MaskInputFn; maskTextFn?: MaskTextFn; @@ -130,8 +137,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 e320254111..5b656bfa89 100644 --- a/packages/rrweb/test/__snapshots__/integration.test.ts.snap +++ b/packages/rrweb/test/__snapshots__/integration.test.ts.snap @@ -3574,6 +3574,349 @@ exports[`record integration tests can record style changes compactly and preserv ]" `; +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\\": \\"****** ****\\", + \\"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\\": \\"*******\\\\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`] = ` "[ { @@ -4057,34 +4400,864 @@ 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, + \\"pointerType\\": 0 + } + }, + { + \\"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, + \\"pointerType\\": 0 + } + }, + { + \\"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\\", + \\"data-unmask-example\\": \\"true\\" + }, + \\"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 } @@ -4218,115 +5391,7 @@ exports[`record integration tests can use maskInputOptions to configure which ty \\"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\\", + \\"text\\": \\"**********\\", \\"isChecked\\": false, \\"id\\": 42 } @@ -4351,70 +5416,7 @@ exports[`record integration tests can use maskInputOptions to configure which ty \\"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\\": \\"********\\", + \\"text\\": \\"**********\\", \\"isChecked\\": false, \\"id\\": 59 } @@ -9233,6 +10235,15 @@ exports[`record integration tests should not record input values if dynamically \\"id\\": 21 } }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 3, + \\"id\\": 21, + \\"x\\": 6, + \\"y\\": 0 + } + }, { \\"type\\": 3, \\"data\\": { diff --git a/packages/rrweb/test/__snapshots__/record.test.ts.snap b/packages/rrweb/test/__snapshots__/record.test.ts.snap index cb40f3328d..27bcc46b5e 100644 --- a/packages/rrweb/test/__snapshots__/record.test.ts.snap +++ b/packages/rrweb/test/__snapshots__/record.test.ts.snap @@ -3168,6 +3168,16 @@ exports[`record loading stylesheets captures stylesheets in iframes that are sti \\"textContent\\": \\"\\\\n \\", \\"rootId\\": 19, \\"id\\": 32 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"link\\", + \\"attributes\\": { + \\"_cssText\\": \\"body { color: pink; }\\" + }, + \\"childNodes\\": [], + \\"rootId\\": 19, + \\"id\\": 33 } ], \\"rootId\\": 19, @@ -3177,7 +3187,7 @@ exports[`record loading stylesheets captures stylesheets in iframes that are sti \\"type\\": 3, \\"textContent\\": \\"\\\\n \\", \\"rootId\\": 19, - \\"id\\": 33 + \\"id\\": 34 }, { \\"type\\": 2, @@ -3188,11 +3198,11 @@ exports[`record loading stylesheets captures stylesheets in iframes that are sti \\"type\\": 3, \\"textContent\\": \\"\\\\n Hello world!\\\\n \\\\n\\\\n\\", \\"rootId\\": 19, - \\"id\\": 35 + \\"id\\": 36 } ], \\"rootId\\": 19, - \\"id\\": 34 + \\"id\\": 35 } ], \\"rootId\\": 19, @@ -3208,49 +3218,6 @@ exports[`record loading stylesheets captures stylesheets in iframes that are sti \\"attributes\\": [], \\"isAttachIframe\\": true } - }, - { - \\"type\\": 3, - \\"data\\": { - \\"source\\": 0, - \\"texts\\": [], - \\"attributes\\": [], - \\"removes\\": [], - \\"adds\\": [ - { - \\"parentId\\": 22, - \\"nextId\\": null, - \\"node\\": { - \\"type\\": 2, - \\"tagName\\": \\"link\\", - \\"attributes\\": { - \\"rel\\": \\"stylesheet\\", - \\"href\\": \\"http://localhost:3030/html/assets/style.css\\" - }, - \\"childNodes\\": [], - \\"rootId\\": 19, - \\"id\\": 36 - } - } - ] - } - }, - { - \\"type\\": 3, - \\"data\\": { - \\"source\\": 0, - \\"adds\\": [], - \\"removes\\": [], - \\"texts\\": [], - \\"attributes\\": [ - { - \\"id\\": 36, - \\"attributes\\": { - \\"_cssText\\": \\"body { color: pink; }\\" - } - } - ] - } } ]" `; 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 c627be84cb..48d051dcc5 100644 --- a/packages/rrweb/test/integration.test.ts +++ b/packages/rrweb/test/integration.test.ts @@ -1213,6 +1213,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/rrweb/test/utils.ts b/packages/rrweb/test/utils.ts index 5a90f62031..a32932c308 100644 --- a/packages/rrweb/test/utils.ts +++ b/packages/rrweb/test/utils.ts @@ -690,7 +690,9 @@ export function generateRecordSnippet(options: recordOptions) { }, ignoreSelector: ${JSON.stringify(options.ignoreSelector)}, maskTextSelector: ${JSON.stringify(options.maskTextSelector)}, + unmaskTextSelector: ${JSON.stringify(options.unmaskTextSelector)}, maskAllInputs: ${options.maskAllInputs}, + maskAllText: ${options.maskAllText}, maskInputOptions: ${JSON.stringify(options.maskAllInputs)}, userTriggeredOnInput: ${options.userTriggeredOnInput}, maskTextFn: ${options.maskTextFn}, diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index e6f6f15cd3..eb9b9b618a 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<{ /**