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
This commit is contained in:
Francesco Novy
2023-03-20 13:13:23 +01:00
committed by GitHub
parent a82a3b42b1
commit d2582e9a81
9 changed files with 519 additions and 382 deletions

View File

@@ -0,0 +1,6 @@
---
'rrweb-snapshot': minor
'rrweb': minor
---
feat: Ensure password inputs remain masked when switching input type

View File

@@ -674,8 +674,13 @@ function serializeElementNode(
attributes.type !== 'button' && attributes.type !== 'button' &&
value value
) { ) {
const type: string | null = n.hasAttribute('data-rr-is-password')
? 'password'
: typeof attributes.type === 'string'
? attributes.type.toLowerCase()
: null;
attributes.value = maskInputValue({ attributes.value = maskInputValue({
type: attributes.type, type,
tagName, tagName,
value, value,
maskInputOptions, maskInputOptions,

View File

@@ -162,14 +162,16 @@ export function maskInputValue({
}: { }: {
maskInputOptions: MaskInputOptions; maskInputOptions: MaskInputOptions;
tagName: string; tagName: string;
type: string | number | boolean | null; type: string | null;
value: string | null; value: string | null;
maskInputFn?: MaskInputFn; maskInputFn?: MaskInputFn;
}): string { }): string {
let text = value || ''; let text = value || '';
const actualType = type && type.toLowerCase();
if ( if (
maskInputOptions[tagName.toLowerCase() as keyof MaskInputOptions] || maskInputOptions[tagName.toLowerCase() as keyof MaskInputOptions] ||
maskInputOptions[type as keyof MaskInputOptions] (actualType && maskInputOptions[actualType as keyof MaskInputOptions])
) { ) {
if (maskInputFn) { if (maskInputFn) {
text = maskInputFn(text); text = maskInputFn(text);

View File

@@ -29,6 +29,7 @@ import {
isSerializedStylesheet, isSerializedStylesheet,
inDom, inDom,
getShadowHost, getShadowHost,
getInputType,
} from '../utils'; } from '../utils';
type DoubleLinkedListNode = { type DoubleLinkedListNode = {
@@ -488,11 +489,14 @@ export default class MutationBuffer {
const target = m.target as HTMLElement; const target = m.target as HTMLElement;
let attributeName = m.attributeName as string; let attributeName = m.attributeName as string;
let value = (m.target as HTMLElement).getAttribute(attributeName); let value = (m.target as HTMLElement).getAttribute(attributeName);
if (attributeName === 'value') { if (attributeName === 'value') {
const type = getInputType(target);
value = maskInputValue({ value = maskInputValue({
maskInputOptions: this.maskInputOptions, maskInputOptions: this.maskInputOptions,
tagName: (m.target as HTMLElement).tagName, tagName: target.tagName,
type: (m.target as HTMLElement).getAttribute('type'), type,
value, value,
maskInputFn: this.maskInputFn, maskInputFn: this.maskInputFn,
}); });
@@ -527,6 +531,17 @@ export default class MutationBuffer {
}; };
this.attributes.push(item); 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') { if (attributeName === 'style') {
const old = this.doc.createElement('span'); const old = this.doc.createElement('span');
if (m.oldValue) { if (m.oldValue) {

View File

@@ -4,6 +4,7 @@ import {
throttle, throttle,
on, on,
hookSetter, hookSetter,
getInputType,
getWindowScroll, getWindowScroll,
getWindowHeight, getWindowHeight,
getWindowWidth, getWindowWidth,
@@ -338,39 +339,42 @@ function initInputObserver({
userTriggeredOnInput, userTriggeredOnInput,
}: observerParam): listenerHandler { }: observerParam): listenerHandler {
function eventHandler(event: Event) { function eventHandler(event: Event) {
let target = getEventTarget(event); let target = getEventTarget(event) as HTMLElement | null;
const userTriggered = event.isTrusted; 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. * 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. * 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') if (target && tagName === 'OPTION') {
target = (target as Element).parentElement; target = target.parentElement;
}
if ( if (
!target || !target ||
!(target as Element).tagName || !tagName ||
INPUT_TAGS.indexOf((target as Element).tagName) < 0 || INPUT_TAGS.indexOf(tagName) < 0 ||
isBlocked(target as Node, blockClass, blockSelector, true) isBlocked(target as Node, blockClass, blockSelector, true)
) { ) {
return; return;
} }
const type: string | undefined = (target as HTMLInputElement).type;
if ((target as HTMLElement).classList.contains(ignoreClass)) { if (target.classList.contains(ignoreClass)) {
return; return;
} }
let text = (target as HTMLInputElement).value; let text = (target as HTMLInputElement).value;
let isChecked = false; let isChecked = false;
const type: Lowercase<string> = getInputType(target) || '';
if (type === 'radio' || type === 'checkbox') { if (type === 'radio' || type === 'checkbox') {
isChecked = (target as HTMLInputElement).checked; isChecked = (target as HTMLInputElement).checked;
} else if ( } else if (
maskInputOptions[ maskInputOptions[tagName.toLowerCase() as keyof MaskInputOptions] ||
(target as Element).tagName.toLowerCase() as keyof MaskInputOptions
] ||
maskInputOptions[type as keyof MaskInputOptions] maskInputOptions[type as keyof MaskInputOptions]
) { ) {
text = maskInputValue({ text = maskInputValue({
maskInputOptions, maskInputOptions,
tagName: (target as HTMLElement).tagName, tagName,
type, type,
value: text, value: text,
maskInputFn, maskInputFn,

View File

@@ -563,3 +563,17 @@ export function inDom(n: Node): boolean {
if (!doc) return false; if (!doc) return false;
return doc.contains(n) || shadowHostInDom(n); 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<string> | null {
return element.hasAttribute('data-rr-is-password')
? 'password'
: element.hasAttribute('type')
? (element.getAttribute('type')!.toLowerCase() as Lowercase<string>)
: null;
}

View File

@@ -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`] = ` 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`] = ` exports[`record integration tests should nest record iframe 1`] = `
"[ "[
{ {

View File

@@ -8,11 +8,13 @@
</head> </head>
<body> <body>
<input type="password" id="password" /> <input type="password" id="password" />
<button type="button" id="show-password">Toggle show password</button>
<script> <script>
const password = document.getElementById('password'); const password = document.getElementById('password');
password.addEventListener('keyup', (event) => {
password.setAttribute('value', password.value); document.getElementById('show-password').addEventListener('click', function() {
password.setAttribute('type', password.getAttribute('type') === 'password' ? 'text' : 'password');
}); });
</script> </script>
</body> </body>
</html> </html>

View File

@@ -281,7 +281,7 @@ describe('record integration tests', function (this: ISuite) {
assertSnapshot(snapshots); 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(); const page: puppeteer.Page = await browser.newPage();
await page.goto('about:blank'); await page.goto('about:blank');
await page.setContent( 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( const snapshots = (await page.evaluate(
'window.snapshots', 'window.snapshots',