From 271501b7b96c336a89073f1ca57a3d742a3fa64d Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Wed, 1 Apr 2026 12:00:00 +0800 Subject: [PATCH] feat: Ensure password inputs are masked when switching type (#1170) * feat: Ensure password inputs are masked when switching type Apply formatting changes use data- attribute ref: Ensure type is always lowercased add changeset * extract into util * Apply formatting changes --- .changeset/new-snakes-call.md | 6 + packages/rrweb-snapshot/src/snapshot.ts | 7 +- packages/rrweb-snapshot/src/utils.ts | 6 +- packages/rrweb/src/record/mutation.ts | 19 +- packages/rrweb/src/record/observer.ts | 26 +- packages/rrweb/src/utils.ts | 14 + .../__snapshots__/integration.test.ts.snap | 806 ++++++++++-------- packages/rrweb/test/html/password.html | 8 +- packages/rrweb/test/integration.test.ts | 9 +- 9 files changed, 519 insertions(+), 382 deletions(-) create mode 100644 .changeset/new-snakes-call.md diff --git a/.changeset/new-snakes-call.md b/.changeset/new-snakes-call.md new file mode 100644 index 00000000..c5b9e2a6 --- /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 47e24fc4..f587bd83 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 1c8c7573..35b6d807 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 5a11a7f7..aa351fee 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 23600784..5e7043bd 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 26dc6388..2ed5ba52 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 27aee086..7c6fdfbf 100644 --- a/packages/rrweb/test/__snapshots__/integration.test.ts.snap +++ b/packages/rrweb/test/__snapshots__/integration.test.ts.snap @@ -5102,6 +5102,451 @@ exports[`record integration tests should handle recursive console messages 1`] = ]" `; +exports[`record integration tests should mask password value attribute with maskInputOptions 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\\": { + \\"http-equiv\\": \\"X-UA-Compatible\\", + \\"content\\": \\"IE=edge\\" + }, + \\"childNodes\\": [], + \\"id\\": 8 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 9 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"meta\\", + \\"attributes\\": { + \\"name\\": \\"viewport\\", + \\"content\\": \\"width=device-width, initial-scale=1.0\\" + }, + \\"childNodes\\": [], + \\"id\\": 10 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 11 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"title\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"Document\\", + \\"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\\": \\"input\\", + \\"attributes\\": { + \\"type\\": \\"password\\", + \\"id\\": \\"password\\" + }, + \\"childNodes\\": [], + \\"id\\": 18 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 19 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"button\\", + \\"attributes\\": { + \\"type\\": \\"button\\", + \\"id\\": \\"show-password\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"Toggle show password\\", + \\"id\\": 21 + } + ], + \\"id\\": 20 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 22 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 24 + } + ], + \\"id\\": 23 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\", + \\"id\\": 25 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 27 + } + ], + \\"id\\": 26 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\\\n\\\\n\\", + \\"id\\": 28 + } + ], + \\"id\\": 16 + } + ], + \\"id\\": 3 + } + ], + \\"id\\": 1 + }, + \\"initialOffset\\": { + \\"left\\": 0, + \\"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\\": 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\\": 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`] = ` "[ { @@ -5658,367 +6103,6 @@ exports[`record integration tests should mask texts using maskTextFn 1`] = ` ]" `; -exports[`record integration tests should mask value attribute with maskInputOptions 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\\": { - \\"http-equiv\\": \\"X-UA-Compatible\\", - \\"content\\": \\"IE=edge\\" - }, - \\"childNodes\\": [], - \\"id\\": 8 - }, - { - \\"type\\": 3, - \\"textContent\\": \\"\\\\n \\", - \\"id\\": 9 - }, - { - \\"type\\": 2, - \\"tagName\\": \\"meta\\", - \\"attributes\\": { - \\"name\\": \\"viewport\\", - \\"content\\": \\"width=device-width, initial-scale=1.0\\" - }, - \\"childNodes\\": [], - \\"id\\": 10 - }, - { - \\"type\\": 3, - \\"textContent\\": \\"\\\\n \\", - \\"id\\": 11 - }, - { - \\"type\\": 2, - \\"tagName\\": \\"title\\", - \\"attributes\\": {}, - \\"childNodes\\": [ - { - \\"type\\": 3, - \\"textContent\\": \\"Document\\", - \\"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\\": \\"input\\", - \\"attributes\\": { - \\"type\\": \\"password\\", - \\"id\\": \\"password\\" - }, - \\"childNodes\\": [], - \\"id\\": 18 - }, - { - \\"type\\": 3, - \\"textContent\\": \\"\\\\n \\", - \\"id\\": 19 - }, - { - \\"type\\": 2, - \\"tagName\\": \\"script\\", - \\"attributes\\": {}, - \\"childNodes\\": [ - { - \\"type\\": 3, - \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", - \\"id\\": 21 - } - ], - \\"id\\": 20 - }, - { - \\"type\\": 3, - \\"textContent\\": \\"\\\\n \\\\n \\", - \\"id\\": 22 - }, - { - \\"type\\": 2, - \\"tagName\\": \\"script\\", - \\"attributes\\": {}, - \\"childNodes\\": [ - { - \\"type\\": 3, - \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", - \\"id\\": 24 - } - ], - \\"id\\": 23 - }, - { - \\"type\\": 3, - \\"textContent\\": \\"\\\\n \\\\n \\\\n\\\\n\\", - \\"id\\": 25 - } - ], - \\"id\\": 16 - } - ], - \\"id\\": 3 - } - ], - \\"id\\": 1 - }, - \\"initialOffset\\": { - \\"left\\": 0, - \\"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\\": [] - } - } -]" -`; - exports[`record integration tests should nest record iframe 1`] = ` "[ { diff --git a/packages/rrweb/test/html/password.html b/packages/rrweb/test/html/password.html index 59ab9331..aace66ab 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 b927f52b..fec8c369 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',