diff --git a/.changeset/new-snakes-call.md b/.changeset/new-snakes-call.md new file mode 100644 index 0000000000..c5b9e2a68e --- /dev/null +++ b/.changeset/new-snakes-call.md @@ -0,0 +1,6 @@ +--- +'rrweb-snapshot': minor +'rrweb': minor +--- + +feat: Ensure password inputs remain masked when switching input type diff --git a/packages/rrweb-snapshot/src/snapshot.ts b/packages/rrweb-snapshot/src/snapshot.ts index 47e24fc453..f587bd83ed 100644 --- a/packages/rrweb-snapshot/src/snapshot.ts +++ b/packages/rrweb-snapshot/src/snapshot.ts @@ -674,8 +674,13 @@ function serializeElementNode( attributes.type !== 'button' && value ) { + const type: string | null = n.hasAttribute('data-rr-is-password') + ? 'password' + : typeof attributes.type === 'string' + ? attributes.type.toLowerCase() + : null; attributes.value = maskInputValue({ - type: attributes.type, + type, tagName, value, maskInputOptions, diff --git a/packages/rrweb-snapshot/src/utils.ts b/packages/rrweb-snapshot/src/utils.ts index 1c8c757363..35b6d8075e 100644 --- a/packages/rrweb-snapshot/src/utils.ts +++ b/packages/rrweb-snapshot/src/utils.ts @@ -162,14 +162,16 @@ export function maskInputValue({ }: { maskInputOptions: MaskInputOptions; tagName: string; - type: string | number | boolean | null; + type: string | null; value: string | null; maskInputFn?: MaskInputFn; }): string { let text = value || ''; + const actualType = type && type.toLowerCase(); + if ( maskInputOptions[tagName.toLowerCase() as keyof MaskInputOptions] || - maskInputOptions[type as keyof MaskInputOptions] + (actualType && maskInputOptions[actualType as keyof MaskInputOptions]) ) { if (maskInputFn) { text = maskInputFn(text); diff --git a/packages/rrweb/src/record/mutation.ts b/packages/rrweb/src/record/mutation.ts index 5a11a7f73d..aa351fee62 100644 --- a/packages/rrweb/src/record/mutation.ts +++ b/packages/rrweb/src/record/mutation.ts @@ -29,6 +29,7 @@ import { isSerializedStylesheet, inDom, getShadowHost, + getInputType, } from '../utils'; type DoubleLinkedListNode = { @@ -488,11 +489,14 @@ export default class MutationBuffer { const target = m.target as HTMLElement; let attributeName = m.attributeName as string; let value = (m.target as HTMLElement).getAttribute(attributeName); + if (attributeName === 'value') { + const type = getInputType(target); + value = maskInputValue({ maskInputOptions: this.maskInputOptions, - tagName: (m.target as HTMLElement).tagName, - type: (m.target as HTMLElement).getAttribute('type'), + tagName: target.tagName, + type, value, maskInputFn: this.maskInputFn, }); @@ -527,6 +531,17 @@ export default class MutationBuffer { }; this.attributes.push(item); } + + // Keep this property on inputs that used to be password inputs + // This is used to ensure we do not unmask value when using e.g. a "Show password" type button + if ( + attributeName === 'type' && + target.tagName === 'INPUT' && + (m.oldValue || '').toLowerCase() === 'password' + ) { + target.setAttribute('data-rr-is-password', 'true'); + } + if (attributeName === 'style') { const old = this.doc.createElement('span'); if (m.oldValue) { diff --git a/packages/rrweb/src/record/observer.ts b/packages/rrweb/src/record/observer.ts index 2360078445..5e7043bd63 100644 --- a/packages/rrweb/src/record/observer.ts +++ b/packages/rrweb/src/record/observer.ts @@ -4,6 +4,7 @@ import { throttle, on, hookSetter, + getInputType, getWindowScroll, getWindowHeight, getWindowWidth, @@ -338,39 +339,42 @@ function initInputObserver({ userTriggeredOnInput, }: observerParam): listenerHandler { function eventHandler(event: Event) { - let target = getEventTarget(event); + let target = getEventTarget(event) as HTMLElement | null; const userTriggered = event.isTrusted; + const tagName = target && target.tagName; + /** * If a site changes the value 'selected' of an option element, the value of its parent element, usually a select element, will be changed as well. * We can treat this change as a value change of the select element the current target belongs to. */ - if (target && (target as Element).tagName === 'OPTION') - target = (target as Element).parentElement; + if (target && tagName === 'OPTION') { + target = target.parentElement; + } if ( !target || - !(target as Element).tagName || - INPUT_TAGS.indexOf((target as Element).tagName) < 0 || + !tagName || + INPUT_TAGS.indexOf(tagName) < 0 || isBlocked(target as Node, blockClass, blockSelector, true) ) { return; } - const type: string | undefined = (target as HTMLInputElement).type; - if ((target as HTMLElement).classList.contains(ignoreClass)) { + + if (target.classList.contains(ignoreClass)) { return; } let text = (target as HTMLInputElement).value; let isChecked = false; + const type: Lowercase = getInputType(target) || ''; + if (type === 'radio' || type === 'checkbox') { isChecked = (target as HTMLInputElement).checked; } else if ( - maskInputOptions[ - (target as Element).tagName.toLowerCase() as keyof MaskInputOptions - ] || + maskInputOptions[tagName.toLowerCase() as keyof MaskInputOptions] || maskInputOptions[type as keyof MaskInputOptions] ) { text = maskInputValue({ maskInputOptions, - tagName: (target as HTMLElement).tagName, + tagName, type, value: text, maskInputFn, diff --git a/packages/rrweb/src/utils.ts b/packages/rrweb/src/utils.ts index 26dc63888e..2ed5ba52d3 100644 --- a/packages/rrweb/src/utils.ts +++ b/packages/rrweb/src/utils.ts @@ -563,3 +563,17 @@ export function inDom(n: Node): boolean { if (!doc) return false; return doc.contains(n) || shadowHostInDom(n); } + +/** + * Get the type of an input element. + * This takes care of the case where a password input is changed to a text input. + * In this case, we continue to consider this of type password, in order to avoid leaking sensitive data + * where passwords should be masked. + */ +export function getInputType(element: HTMLElement): Lowercase | null { + return element.hasAttribute('data-rr-is-password') + ? 'password' + : element.hasAttribute('type') + ? (element.getAttribute('type')!.toLowerCase() as Lowercase) + : null; +} diff --git a/packages/rrweb/test/__snapshots__/integration.test.ts.snap b/packages/rrweb/test/__snapshots__/integration.test.ts.snap index 27aee086a9..7c6fdfbfda 100644 --- a/packages/rrweb/test/__snapshots__/integration.test.ts.snap +++ b/packages/rrweb/test/__snapshots__/integration.test.ts.snap @@ -5102,7 +5102,7 @@ exports[`record integration tests should handle recursive console messages 1`] = ]" `; -exports[`record integration tests should mask texts 1`] = ` +exports[`record integration tests should mask password value attribute with maskInputOptions 1`] = ` "[ { \\"type\\": 0, @@ -5168,8 +5168,8 @@ exports[`record integration tests should mask texts 1`] = ` \\"type\\": 2, \\"tagName\\": \\"meta\\", \\"attributes\\": { - \\"name\\": \\"viewport\\", - \\"content\\": \\"width=device-width, initial-scale=1.0\\" + \\"http-equiv\\": \\"X-UA-Compatible\\", + \\"content\\": \\"IE=edge\\" }, \\"childNodes\\": [], \\"id\\": 8 @@ -5183,8 +5183,8 @@ exports[`record integration tests should mask texts 1`] = ` \\"type\\": 2, \\"tagName\\": \\"meta\\", \\"attributes\\": { - \\"http-equiv\\": \\"X-UA-Compatible\\", - \\"content\\": \\"ie=edge\\" + \\"name\\": \\"viewport\\", + \\"content\\": \\"width=device-width, initial-scale=1.0\\" }, \\"childNodes\\": [], \\"id\\": 10 @@ -5201,7 +5201,7 @@ exports[`record integration tests should mask texts 1`] = ` \\"childNodes\\": [ { \\"type\\": 3, - \\"textContent\\": \\"Mask text\\", + \\"textContent\\": \\"Document\\", \\"id\\": 13 } ], @@ -5232,117 +5232,57 @@ exports[`record integration tests should mask texts 1`] = ` }, { \\"type\\": 2, - \\"tagName\\": \\"p\\", + \\"tagName\\": \\"input\\", \\"attributes\\": { - \\"class\\": \\"rr-mask\\" + \\"type\\": \\"password\\", + \\"id\\": \\"password\\" }, - \\"childNodes\\": [ - { - \\"type\\": 3, - \\"textContent\\": \\"*****\\", - \\"id\\": 19 - } - ], + \\"childNodes\\": [], \\"id\\": 18 }, { \\"type\\": 3, \\"textContent\\": \\"\\\\n \\", - \\"id\\": 20 + \\"id\\": 19 }, { \\"type\\": 2, - \\"tagName\\": \\"div\\", + \\"tagName\\": \\"button\\", \\"attributes\\": { - \\"class\\": \\"rr-mask\\" + \\"type\\": \\"button\\", + \\"id\\": \\"show-password\\" }, \\"childNodes\\": [ { \\"type\\": 3, - \\"textContent\\": \\"\\\\n \\", - \\"id\\": 22 - }, - { - \\"type\\": 2, - \\"tagName\\": \\"span\\", - \\"attributes\\": {}, - \\"childNodes\\": [ - { - \\"type\\": 3, - \\"textContent\\": \\"*****\\", - \\"id\\": 24 - } - ], - \\"id\\": 23 - }, - { - \\"type\\": 3, - \\"textContent\\": \\"\\\\n \\", - \\"id\\": 25 + \\"textContent\\": \\"Toggle show password\\", + \\"id\\": 21 } ], - \\"id\\": 21 + \\"id\\": 20 }, { \\"type\\": 3, \\"textContent\\": \\"\\\\n \\", - \\"id\\": 26 + \\"id\\": 22 }, { \\"type\\": 2, - \\"tagName\\": \\"div\\", - \\"attributes\\": { - \\"data-masking\\": \\"true\\" - }, + \\"tagName\\": \\"script\\", + \\"attributes\\": {}, \\"childNodes\\": [ { \\"type\\": 3, - \\"textContent\\": \\"\\\\n \\", - \\"id\\": 28 - }, - { - \\"type\\": 2, - \\"tagName\\": \\"div\\", - \\"attributes\\": {}, - \\"childNodes\\": [ - { - \\"type\\": 3, - \\"textContent\\": \\"\\\\n \\", - \\"id\\": 30 - }, - { - \\"type\\": 2, - \\"tagName\\": \\"div\\", - \\"attributes\\": {}, - \\"childNodes\\": [ - { - \\"type\\": 3, - \\"textContent\\": \\"*****\\", - \\"id\\": 32 - } - ], - \\"id\\": 31 - }, - { - \\"type\\": 3, - \\"textContent\\": \\"\\\\n \\", - \\"id\\": 33 - } - ], - \\"id\\": 29 - }, - { - \\"type\\": 3, - \\"textContent\\": \\"\\\\n \\", - \\"id\\": 34 + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 24 } ], - \\"id\\": 27 + \\"id\\": 23 }, { \\"type\\": 3, \\"textContent\\": \\"\\\\n \\\\n \\", - \\"id\\": 35 + \\"id\\": 25 }, { \\"type\\": 2, @@ -5352,15 +5292,15 @@ exports[`record integration tests should mask texts 1`] = ` { \\"type\\": 3, \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", - \\"id\\": 37 + \\"id\\": 27 } ], - \\"id\\": 36 + \\"id\\": 26 }, { \\"type\\": 3, \\"textContent\\": \\"\\\\n \\\\n \\\\n\\\\n\\", - \\"id\\": 38 + \\"id\\": 28 } ], \\"id\\": 16 @@ -5376,148 +5316,375 @@ exports[`record integration tests should mask texts 1`] = ` \\"top\\": 0 } } - } -]" -`; - -exports[`record integration tests should mask texts using maskTextFn 1`] = ` -"[ + }, { - \\"type\\": 0, - \\"data\\": {} + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 5, + \\"id\\": 18 + } }, { - \\"type\\": 1, - \\"data\\": {} + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"*\\", + \\"isChecked\\": false, + \\"id\\": 18 + } }, { - \\"type\\": 4, + \\"type\\": 3, \\"data\\": { - \\"href\\": \\"about:blank\\", - \\"width\\": 1920, - \\"height\\": 1080 + \\"source\\": 5, + \\"text\\": \\"**\\", + \\"isChecked\\": false, + \\"id\\": 18 } }, { - \\"type\\": 2, + \\"type\\": 3, \\"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\\": \\"Mask 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\\": \\"p\\", - \\"attributes\\": { - \\"class\\": \\"rr-mask\\" + \\"source\\": 5, + \\"text\\": \\"***\\", + \\"isChecked\\": false, + \\"id\\": 18 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"****\\", + \\"isChecked\\": false, + \\"id\\": 18 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"*****\\", + \\"isChecked\\": false, + \\"id\\": 18 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"******\\", + \\"isChecked\\": false, + \\"id\\": 18 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 1, + \\"id\\": 20 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 6, + \\"id\\": 18 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 5, + \\"id\\": 20 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 0, + \\"id\\": 20 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 2, + \\"id\\": 20 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [ + { + \\"id\\": 18, + \\"attributes\\": { + \\"type\\": \\"text\\" + } + } + ], + \\"removes\\": [], + \\"adds\\": [] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [ + { + \\"id\\": 18, + \\"attributes\\": { + \\"data-rr-is-password\\": \\"true\\" + } + } + ], + \\"removes\\": [], + \\"adds\\": [] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 6, + \\"id\\": 20 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 5, + \\"id\\": 18 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"*******\\", + \\"isChecked\\": false, + \\"id\\": 18 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"********\\", + \\"isChecked\\": false, + \\"id\\": 18 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 1, + \\"id\\": 20 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 6, + \\"id\\": 18 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 5, + \\"id\\": 20 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 0, + \\"id\\": 20 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 2, + \\"id\\": 20 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [ + { + \\"id\\": 18, + \\"attributes\\": { + \\"type\\": \\"password\\" + } + } + ], + \\"removes\\": [], + \\"adds\\": [] + } + } +]" +`; + +exports[`record integration tests should mask texts 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\\": \\"****1\\", + \\"textContent\\": \\"Mask 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\\": \\"p\\", + \\"attributes\\": { + \\"class\\": \\"rr-mask\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"*****\\", \\"id\\": 19 } ], @@ -5547,7 +5714,7 @@ exports[`record integration tests should mask texts using maskTextFn 1`] = ` \\"childNodes\\": [ { \\"type\\": 3, - \\"textContent\\": \\"****2\\", + \\"textContent\\": \\"*****\\", \\"id\\": 24 } ], @@ -5595,7 +5762,7 @@ exports[`record integration tests should mask texts using maskTextFn 1`] = ` \\"childNodes\\": [ { \\"type\\": 3, - \\"textContent\\": \\"****3\\", + \\"textContent\\": \\"*****\\", \\"id\\": 32 } ], @@ -5658,7 +5825,7 @@ exports[`record integration tests should mask texts using maskTextFn 1`] = ` ]" `; -exports[`record integration tests should mask value attribute with maskInputOptions 1`] = ` +exports[`record integration tests should mask texts using maskTextFn 1`] = ` "[ { \\"type\\": 0, @@ -5724,8 +5891,8 @@ exports[`record integration tests should mask value attribute with maskInputOpti \\"type\\": 2, \\"tagName\\": \\"meta\\", \\"attributes\\": { - \\"http-equiv\\": \\"X-UA-Compatible\\", - \\"content\\": \\"IE=edge\\" + \\"name\\": \\"viewport\\", + \\"content\\": \\"width=device-width, initial-scale=1.0\\" }, \\"childNodes\\": [], \\"id\\": 8 @@ -5739,8 +5906,8 @@ exports[`record integration tests should mask value attribute with maskInputOpti \\"type\\": 2, \\"tagName\\": \\"meta\\", \\"attributes\\": { - \\"name\\": \\"viewport\\", - \\"content\\": \\"width=device-width, initial-scale=1.0\\" + \\"http-equiv\\": \\"X-UA-Compatible\\", + \\"content\\": \\"ie=edge\\" }, \\"childNodes\\": [], \\"id\\": 10 @@ -5757,7 +5924,7 @@ exports[`record integration tests should mask value attribute with maskInputOpti \\"childNodes\\": [ { \\"type\\": 3, - \\"textContent\\": \\"Document\\", + \\"textContent\\": \\"Mask text\\", \\"id\\": 13 } ], @@ -5784,40 +5951,121 @@ exports[`record integration tests should mask value attribute with maskInputOpti { \\"type\\": 3, \\"textContent\\": \\"\\\\n \\", - \\"id\\": 17 + \\"id\\": 17 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"p\\", + \\"attributes\\": { + \\"class\\": \\"rr-mask\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"****1\\", + \\"id\\": 19 + } + ], + \\"id\\": 18 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 20 }, { \\"type\\": 2, - \\"tagName\\": \\"input\\", + \\"tagName\\": \\"div\\", \\"attributes\\": { - \\"type\\": \\"password\\", - \\"id\\": \\"password\\" + \\"class\\": \\"rr-mask\\" }, - \\"childNodes\\": [], - \\"id\\": 18 + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 22 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"span\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"****2\\", + \\"id\\": 24 + } + ], + \\"id\\": 23 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 25 + } + ], + \\"id\\": 21 }, { \\"type\\": 3, \\"textContent\\": \\"\\\\n \\", - \\"id\\": 19 + \\"id\\": 26 }, { \\"type\\": 2, - \\"tagName\\": \\"script\\", - \\"attributes\\": {}, + \\"tagName\\": \\"div\\", + \\"attributes\\": { + \\"data-masking\\": \\"true\\" + }, \\"childNodes\\": [ { \\"type\\": 3, - \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", - \\"id\\": 21 + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 28 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"div\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 30 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"div\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"****3\\", + \\"id\\": 32 + } + ], + \\"id\\": 31 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 33 + } + ], + \\"id\\": 29 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 34 } ], - \\"id\\": 20 + \\"id\\": 27 }, { \\"type\\": 3, \\"textContent\\": \\"\\\\n \\\\n \\", - \\"id\\": 22 + \\"id\\": 35 }, { \\"type\\": 2, @@ -5827,15 +6075,15 @@ exports[`record integration tests should mask value attribute with maskInputOpti { \\"type\\": 3, \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", - \\"id\\": 24 + \\"id\\": 37 } ], - \\"id\\": 23 + \\"id\\": 36 }, { \\"type\\": 3, \\"textContent\\": \\"\\\\n \\\\n \\\\n\\\\n\\", - \\"id\\": 25 + \\"id\\": 38 } ], \\"id\\": 16 @@ -5851,170 +6099,6 @@ exports[`record integration tests should mask value attribute with maskInputOpti \\"top\\": 0 } } - }, - { - \\"type\\": 3, - \\"data\\": { - \\"source\\": 2, - \\"type\\": 5, - \\"id\\": 18 - } - }, - { - \\"type\\": 3, - \\"data\\": { - \\"source\\": 5, - \\"text\\": \\"*\\", - \\"isChecked\\": false, - \\"id\\": 18 - } - }, - { - \\"type\\": 3, - \\"data\\": { - \\"source\\": 0, - \\"texts\\": [], - \\"attributes\\": [ - { - \\"id\\": 18, - \\"attributes\\": { - \\"value\\": \\"*\\" - } - } - ], - \\"removes\\": [], - \\"adds\\": [] - } - }, - { - \\"type\\": 3, - \\"data\\": { - \\"source\\": 5, - \\"text\\": \\"**\\", - \\"isChecked\\": false, - \\"id\\": 18 - } - }, - { - \\"type\\": 3, - \\"data\\": { - \\"source\\": 0, - \\"texts\\": [], - \\"attributes\\": [ - { - \\"id\\": 18, - \\"attributes\\": { - \\"value\\": \\"**\\" - } - } - ], - \\"removes\\": [], - \\"adds\\": [] - } - }, - { - \\"type\\": 3, - \\"data\\": { - \\"source\\": 5, - \\"text\\": \\"***\\", - \\"isChecked\\": false, - \\"id\\": 18 - } - }, - { - \\"type\\": 3, - \\"data\\": { - \\"source\\": 0, - \\"texts\\": [], - \\"attributes\\": [ - { - \\"id\\": 18, - \\"attributes\\": { - \\"value\\": \\"***\\" - } - } - ], - \\"removes\\": [], - \\"adds\\": [] - } - }, - { - \\"type\\": 3, - \\"data\\": { - \\"source\\": 5, - \\"text\\": \\"****\\", - \\"isChecked\\": false, - \\"id\\": 18 - } - }, - { - \\"type\\": 3, - \\"data\\": { - \\"source\\": 0, - \\"texts\\": [], - \\"attributes\\": [ - { - \\"id\\": 18, - \\"attributes\\": { - \\"value\\": \\"****\\" - } - } - ], - \\"removes\\": [], - \\"adds\\": [] - } - }, - { - \\"type\\": 3, - \\"data\\": { - \\"source\\": 5, - \\"text\\": \\"*****\\", - \\"isChecked\\": false, - \\"id\\": 18 - } - }, - { - \\"type\\": 3, - \\"data\\": { - \\"source\\": 0, - \\"texts\\": [], - \\"attributes\\": [ - { - \\"id\\": 18, - \\"attributes\\": { - \\"value\\": \\"*****\\" - } - } - ], - \\"removes\\": [], - \\"adds\\": [] - } - }, - { - \\"type\\": 3, - \\"data\\": { - \\"source\\": 5, - \\"text\\": \\"******\\", - \\"isChecked\\": false, - \\"id\\": 18 - } - }, - { - \\"type\\": 3, - \\"data\\": { - \\"source\\": 0, - \\"texts\\": [], - \\"attributes\\": [ - { - \\"id\\": 18, - \\"attributes\\": { - \\"value\\": \\"******\\" - } - } - ], - \\"removes\\": [], - \\"adds\\": [] - } } ]" `; diff --git a/packages/rrweb/test/html/password.html b/packages/rrweb/test/html/password.html index 59ab933101..aace66abf4 100644 --- a/packages/rrweb/test/html/password.html +++ b/packages/rrweb/test/html/password.html @@ -8,11 +8,13 @@ + + diff --git a/packages/rrweb/test/integration.test.ts b/packages/rrweb/test/integration.test.ts index b927f52bb1..fec8c36902 100644 --- a/packages/rrweb/test/integration.test.ts +++ b/packages/rrweb/test/integration.test.ts @@ -281,7 +281,7 @@ describe('record integration tests', function (this: ISuite) { assertSnapshot(snapshots); }); - it('should mask value attribute with maskInputOptions', async () => { + it('should mask password value attribute with maskInputOptions', async () => { const page: puppeteer.Page = await browser.newPage(); await page.goto('about:blank'); await page.setContent( @@ -292,7 +292,12 @@ describe('record integration tests', function (this: ISuite) { }), ); - await page.type('input[type="password"]', 'secr3t'); + await page.type('#password', 'secr3t'); + + // Change type to text (simulate "show password") + await page.click('#show-password'); + await page.type('#password', 'XY'); + await page.click('#show-password'); const snapshots = (await page.evaluate( 'window.snapshots',