From 1209595e36d604387a25febf9e5897bfd87e0fef Mon Sep 17 00:00:00 2001 From: Billy Vong Date: Thu, 6 Jul 2023 19:59:36 -0400 Subject: [PATCH] feat: add forked sentry tests This adds tests for the features that Sentry has added --- .../rrweb/test/events/shadow-dom-sentry.ts | 438 ++++++++++++++++++ packages/rrweb/test/html/attributes-mask.html | 29 ++ packages/rrweb/test/html/block.html | 8 + packages/rrweb/test/html/form-masked.html | 41 ++ packages/rrweb/test/html/form.html | 28 +- packages/rrweb/test/html/mask-text.html | 10 + .../rrweb/test/integration-sentry.test.ts | 406 ++++++++++++++++ packages/rrweb/test/integration.test.ts | 20 +- packages/rrweb/test/replayer.test.ts | 15 + packages/rrweb/test/utils.ts | 8 + 10 files changed, 992 insertions(+), 11 deletions(-) create mode 100644 packages/rrweb/test/events/shadow-dom-sentry.ts create mode 100644 packages/rrweb/test/html/attributes-mask.html create mode 100644 packages/rrweb/test/html/form-masked.html create mode 100644 packages/rrweb/test/integration-sentry.test.ts diff --git a/packages/rrweb/test/events/shadow-dom-sentry.ts b/packages/rrweb/test/events/shadow-dom-sentry.ts new file mode 100644 index 0000000000..5160f789f6 --- /dev/null +++ b/packages/rrweb/test/events/shadow-dom-sentry.ts @@ -0,0 +1,438 @@ +import { EventType, eventWithTime, IncrementalSource } from '@rrweb/types'; + +const now = Date.now(); + +const events: eventWithTime[] = [ + { type: EventType.DomContentLoaded, data: {}, timestamp: now }, + { type: EventType.Load, data: {}, timestamp: now + 100}, + { + type: EventType.Meta, + data: { href: 'https://localhost', width: 655, height: 846 }, + timestamp: now + 100, + }, + { + type: EventType.FullSnapshot, + 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: 2, + tagName: 'meta', + attributes: { charset: 'utf-8' }, + childNodes: [], + id: 5, + }, + ], + id: 4, + }, + { + type: 2, + tagName: 'body', + attributes: {}, + childNodes: [ + { + type: 2, + tagName: 'editable-list', + attributes: { + title: 'TODO', + 'list-item-0': 'First item on the list', + 'list-item-1': 'Second item on the list', + 'list-item-2': 'Third item on the list', + 'list-item-3': 'Fourth item on the list', + 'list-item-4': 'Fifth item on the list', + listitem: 'This will not appear', + 'add-item-text': 'Add new list item:', + }, + childNodes: [ + ], + id: 12, + isShadowHost: true, + }, + ], + id: 10, + }, + ], + id: 3, + }, + ], + id: 1, + }, + initialOffset: { left: 0, top: 0 }, + }, + timestamp: now + 100, + }, + { + type: EventType.IncrementalSnapshot, + data: { + source: IncrementalSource.Mutation, + texts: [], + attributes: [], + removes: [], + adds: [ + { + parentId: 12, + nextId: null, + node: { + type: 2, + tagName: 'div', + attributes: { class: 'editable-list' }, + childNodes: [], + id: 14, + isShadow: true, + }, + }, + { + parentId: 14, + nextId: 21, + node: { + type: 2, + tagName: 'h3', + attributes: {}, + childNodes: [], + id: 19, + }, + }, + { + parentId: 19, + nextId: null, + node: { type: 3, textContent: 'TODO', id: 20 }, + }, + { + parentId: 14, + nextId: 22, + node: { type: 3, textContent: '\n ', id: 21 }, + }, + { + parentId: 14, + nextId: 54, + node: { + type: 2, + tagName: 'ul', + attributes: { class: 'item-list' }, + childNodes: [], + id: 22, + }, + }, + { + parentId: 22, + nextId: 24, + node: { type: 3, textContent: '\n \n ', id: 23 }, + }, + { + parentId: 22, + nextId: 29, + node: { + type: 2, + tagName: 'li', + attributes: {}, + childNodes: [], + id: 24, + }, + }, + { + parentId: 24, + nextId: 26, + node: { + type: 3, + textContent: 'First item on the list\n ', + id: 25, + }, + }, + { + parentId: 24, + nextId: 28, + node: { + type: 2, + tagName: 'button', + attributes: { class: 'editable-list-remove-item icon' }, + childNodes: [], + id: 26, + }, + }, + { + parentId: 26, + nextId: null, + node: { type: 3, textContent: '⊖', id: 27 }, + }, + { + parentId: 24, + nextId: null, + node: { type: 3, textContent: '\n ', id: 28 }, + }, + { + parentId: 22, + nextId: 30, + node: { type: 3, textContent: '\n \n ', id: 29 }, + }, + { + parentId: 22, + nextId: 35, + node: { + type: 2, + tagName: 'li', + attributes: {}, + childNodes: [], + id: 30, + }, + }, + { + parentId: 30, + nextId: 32, + node: { + type: 3, + textContent: 'Second item on the list\n ', + id: 31, + }, + }, + { + parentId: 30, + nextId: 34, + node: { + type: 2, + tagName: 'button', + attributes: { class: 'editable-list-remove-item icon' }, + childNodes: [], + id: 32, + }, + }, + { + parentId: 32, + nextId: null, + node: { type: 3, textContent: '⊖', id: 33 }, + }, + { + parentId: 30, + nextId: null, + node: { type: 3, textContent: '\n ', id: 34 }, + }, + { + parentId: 22, + nextId: 36, + node: { type: 3, textContent: '\n \n ', id: 35 }, + }, + { + parentId: 22, + nextId: 41, + node: { + type: 2, + tagName: 'li', + attributes: {}, + childNodes: [], + id: 36, + }, + }, + { + parentId: 36, + nextId: 38, + node: { + type: 3, + textContent: 'Third item on the list\n ', + id: 37, + }, + }, + { + parentId: 36, + nextId: 40, + node: { + type: 2, + tagName: 'button', + attributes: { class: 'editable-list-remove-item icon' }, + childNodes: [], + id: 38, + }, + }, + { + parentId: 38, + nextId: null, + node: { type: 3, textContent: '⊖', id: 39 }, + }, + { + parentId: 36, + nextId: null, + node: { type: 3, textContent: '\n ', id: 40 }, + }, + { + parentId: 22, + nextId: 42, + node: { type: 3, textContent: '\n \n ', id: 41 }, + }, + { + parentId: 22, + nextId: 47, + node: { + type: 2, + tagName: 'li', + attributes: {}, + childNodes: [], + id: 42, + }, + }, + { + parentId: 42, + nextId: 44, + node: { + type: 3, + textContent: 'Fourth item on the list\n ', + id: 43, + }, + }, + { + parentId: 42, + nextId: 46, + node: { + type: 2, + tagName: 'button', + attributes: { class: 'editable-list-remove-item icon' }, + childNodes: [], + id: 44, + }, + }, + { + parentId: 44, + nextId: null, + node: { type: 3, textContent: '⊖', id: 45 }, + }, + { + parentId: 42, + nextId: null, + node: { type: 3, textContent: '\n ', id: 46 }, + }, + { + parentId: 22, + nextId: 48, + node: { type: 3, textContent: '\n \n ', id: 47 }, + }, + { + parentId: 22, + nextId: 53, + node: { + type: 2, + tagName: 'li', + attributes: {}, + childNodes: [], + id: 48, + }, + }, + { + parentId: 48, + nextId: 50, + node: { + type: 3, + textContent: 'Fifth item on the list\n ', + id: 49, + }, + }, + { + parentId: 48, + nextId: 52, + node: { + type: 2, + tagName: 'button', + attributes: { class: 'editable-list-remove-item icon' }, + childNodes: [], + id: 50, + }, + }, + { + parentId: 50, + nextId: null, + node: { type: 3, textContent: '⊖', id: 51 }, + }, + { + parentId: 14, + nextId: 65, + node: { + type: 2, + tagName: 'div', + attributes: {}, + childNodes: [], + id: 55, + }, + }, + { + parentId: 55, + nextId: 57, + node: { type: 3, textContent: '\n ', id: 56 }, + }, + { + parentId: 55, + nextId: 59, + node: { + type: 2, + tagName: 'label', + attributes: {}, + childNodes: [], + id: 57, + }, + }, + { + parentId: 57, + nextId: null, + node: { type: 3, textContent: 'Add new list item:', id: 58 }, + }, + { + parentId: 55, + nextId: 60, + node: { type: 3, textContent: '\n ', id: 59 }, + }, + { + parentId: 55, + nextId: 61, + node: { + type: 2, + tagName: 'input', + attributes: { class: 'add-new-list-item-input', type: 'text' }, + childNodes: [], + id: 60, + }, + }, + { + parentId: 55, + nextId: 62, + node: { type: 3, textContent: '\n ', id: 61 }, + }, + { + parentId: 55, + nextId: 64, + node: { + type: 2, + tagName: 'button', + attributes: { class: 'editable-list-add-item icon' }, + childNodes: [], + id: 62, + }, + }, + { + parentId: 62, + nextId: null, + node: { type: 3, textContent: '⊕', id: 63 }, + }, + { + parentId: 55, + nextId: null, + node: { type: 3, textContent: '\n ', id: 64 }, + }, + { + parentId: 14, + nextId: null, + node: { type: 3, textContent: '\n ', id: 65 }, + }, + ], + }, + timestamp: now + 400, + } +]; + +export default events; diff --git a/packages/rrweb/test/html/attributes-mask.html b/packages/rrweb/test/html/attributes-mask.html new file mode 100644 index 0000000000..cab12e9bf3 --- /dev/null +++ b/packages/rrweb/test/html/attributes-mask.html @@ -0,0 +1,29 @@ + + + + + + + attributes mask + + + +
+
+ Test content +
+ +
+ Test content 2 +
+ + + + + + + + +
+ + diff --git a/packages/rrweb/test/html/block.html b/packages/rrweb/test/html/block.html index 6fee77f7bb..6ddc233177 100644 --- a/packages/rrweb/test/html/block.html +++ b/packages/rrweb/test/html/block.html @@ -10,5 +10,13 @@
+ + + + + + + + diff --git a/packages/rrweb/test/html/form-masked.html b/packages/rrweb/test/html/form-masked.html new file mode 100644 index 0000000000..bdd57ef0b1 --- /dev/null +++ b/packages/rrweb/test/html/form-masked.html @@ -0,0 +1,41 @@ + + + + + + + form fields + + + +
+ + + + + + + + +
+ + diff --git a/packages/rrweb/test/html/form.html b/packages/rrweb/test/html/form.html index 9125e983fa..d0ddbe9b2e 100644 --- a/packages/rrweb/test/html/form.html +++ b/packages/rrweb/test/html/form.html @@ -13,26 +13,42 @@ + + + + + + diff --git a/packages/rrweb/test/html/mask-text.html b/packages/rrweb/test/html/mask-text.html index 2abaaaa511..7610310985 100644 --- a/packages/rrweb/test/html/mask-text.html +++ b/packages/rrweb/test/html/mask-text.html @@ -16,5 +16,15 @@
mask3
+
+ mask4 +
+
+ mask5 +
+ +
+ + diff --git a/packages/rrweb/test/integration-sentry.test.ts b/packages/rrweb/test/integration-sentry.test.ts new file mode 100644 index 0000000000..115b6fb5c1 --- /dev/null +++ b/packages/rrweb/test/integration-sentry.test.ts @@ -0,0 +1,406 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import type * as puppeteer from 'puppeteer'; +import { + assertSnapshot, + startServer, + getServerURL, + launchPuppeteer, + replaceLast, + generateRecordSnippet, + ISuite, +} from './utils'; +import type { recordOptions } from '../src/types'; +import { eventWithTime, EventType, IncrementalSource } from '@rrweb/types'; + +/** + * Used to filter scroll events out of snapshots as they are flakey + */ +function isNotScroll(snapshot: eventWithTime) { + return !( + snapshot.type === EventType.IncrementalSnapshot && + snapshot.data.source === IncrementalSource.Scroll + ); +} + +describe('record integration tests', function (this: ISuite) { + jest.setTimeout(10_000); + + const getHtml = ( + fileName: string, + options: recordOptions = {}, + ): string => { + const filePath = path.resolve(__dirname, `./html/${fileName}`); + const html = fs.readFileSync(filePath, 'utf8'); + return replaceLast( + html, + '', + ` + + + `, + ); + }; + + let server: ISuite['server']; + let serverURL: string; + let code: ISuite['code']; + let browser: ISuite['browser']; + + beforeAll(async () => { + server = await startServer(); + serverURL = getServerURL(server); + browser = await launchPuppeteer(); + + const bundlePath = path.resolve(__dirname, '../dist/rrweb.min.js'); + const pluginsCode = [ + path.resolve(__dirname, '../dist/plugins/console-record.min.js'), + ] + .map((path) => fs.readFileSync(path, 'utf8')) + .join(); + code = fs.readFileSync(bundlePath, 'utf8') + pluginsCode; + }); + + afterAll(async () => { + await browser.close(); + server.close(); + }); + + it('can configure onMutation', async () => { + const page: puppeteer.Page = await browser.newPage(); + await page.goto('about:blank'); + + await page.setContent( + getHtml.call(this, 'mutation-observer.html', { + onMutation: `(mutations) => { window.lastMutationsLength = mutations.length; return mutations.length < 500 }`, + }), + ); + + await page.evaluate(() => { + const ul = document.querySelector('ul') as HTMLUListElement; + + for (let i = 0; i < 2000; i++) { + const li = document.createElement('li'); + ul.appendChild(li); + const p = document.querySelector('p') as HTMLParagraphElement; + p.appendChild(document.createElement('span')); + } + }); + + const snapshots = await page.evaluate('window.snapshots'); + assertSnapshot(snapshots); + + const lastMutationsLength = await page.evaluate( + 'window.lastMutationsLength', + ); + expect(lastMutationsLength).toBe(4000); + }); + + it('should not record input values on selectively masked elements when maskAllInputs is disabled', async () => { + const page: puppeteer.Page = await browser.newPage(); + await page.goto('about:blank'); + await page.setContent( + getHtml.call(this, 'form-masked.html', { + maskAllInputs: false, + maskInputSelector: '.rr-mask', + }), + ); + + await page.type('input[type="text"]', 'test'); + await page.click('input[type="radio"]'); + await page.click('input[type="checkbox"]'); + await page.type('input[type="password"]', 'password'); + await page.type('textarea', 'textarea test'); + await page.select('select', '1'); + await page.type('#empty', 'test'); + + const snapshots = await page.evaluate('window.snapshots'); + assertSnapshot(snapshots); + }); + + it('correctly masks & unmasks attribute values', async () => { + const page: puppeteer.Page = await browser.newPage(); + await page.goto('about:blank'); + await page.setContent( + getHtml.call(this, 'attributes-mask.html', { + maskAllText: true, + unmaskTextSelector: '.rr-unmask', + }), + ); + + // Change attributes, should still be masked + await page.evaluate(() => { + document + .querySelectorAll('body [title]') + .forEach((el) => el.setAttribute('title', 'new title')); + document + .querySelectorAll('body [aria-label]') + .forEach((el) => el.setAttribute('aria-label', 'new aria label')); + document + .querySelectorAll('body [placeholder]') + .forEach((el) => el.setAttribute('placeholder', 'new placeholder')); + document + .querySelectorAll('input[type="button"],input[type="submit"]') + .forEach((el) => el.setAttribute('value', 'new value')); + }); + + const snapshots = await page.evaluate('window.snapshots'); + assertSnapshot(snapshots); + }); + + it('should record input values if dynamically added and maskAllInputs is false', async () => { + const page: puppeteer.Page = await browser.newPage(); + await page.goto('about:blank'); + await page.setContent( + getHtml.call(this, 'empty.html', { maskAllInputs: false }), + ); + + await page.evaluate(() => { + const el = document.createElement('input'); + el.id = 'input'; + el.value = 'input should not be masked'; + + const nextElement = document.querySelector('#one')!; + nextElement.parentNode!.insertBefore(el, nextElement); + }); + + await page.type('#input', 'moo'); + + const snapshots = (await page.evaluate( + 'window.snapshots', + )) as eventWithTime[]; + assertSnapshot(snapshots.filter(isNotScroll)); + }); + + it('should record textarea values if dynamically added and maskAllInputs is false', async () => { + const page: puppeteer.Page = await browser.newPage(); + await page.goto('about:blank'); + await page.setContent( + getHtml.call(this, 'empty.html', { maskAllInputs: false }), + ); + + await page.evaluate(() => { + const el = document.createElement('textarea'); + el.id = 'textarea'; + el.innerText = `textarea should not be masked +`; + + const nextElement = document.querySelector('#one')!; + nextElement.parentNode!.insertBefore(el, nextElement); + }); + + await page.type('#textarea', 'moo'); + + const snapshots = (await page.evaluate( + 'window.snapshots', + )) as eventWithTime[]; + assertSnapshot(snapshots.filter(isNotScroll)); + }); + + it('should record input values if dynamically added, maskAllInputs is false, and mask selector is used', async () => { + const page: puppeteer.Page = await browser.newPage(); + await page.goto('about:blank'); + await page.setContent( + getHtml.call(this, 'empty.html', { + maskAllInputs: false, + maskInputSelector: '.rr-mask', + }), + ); + + await page.evaluate(() => { + const el = document.createElement('input'); + el.id = 'input-masked'; + el.className = 'rr-mask'; + el.value = 'input should be masked'; + + const nextElement = document.querySelector('#one')!; + nextElement.parentNode!.insertBefore(el, nextElement); + }); + + await page.type('#input-masked', 'moo'); + + const snapshots = (await page.evaluate( + 'window.snapshots', + )) as eventWithTime[]; + assertSnapshot(snapshots.filter(isNotScroll)); + }); + + it('should not record textarea values if dynamically added and maskAllInputs is true', async () => { + const page: puppeteer.Page = await browser.newPage(); + await page.goto('about:blank'); + await page.setContent( + getHtml.call(this, 'empty.html', { maskAllInputs: true }), + ); + + await page.evaluate(() => { + const el = document.createElement('textarea'); + el.id = 'textarea'; + el.innerText = `textarea should be masked +`; + + const nextElement = document.querySelector('#one')!; + nextElement.parentNode!.insertBefore(el, nextElement); + }); + + await page.type('#textarea', 'moo'); + + const snapshots = (await page.evaluate( + 'window.snapshots', + )) as eventWithTime[]; + assertSnapshot(snapshots.filter(isNotScroll)); + }); + + it('should record input values if dynamically added, maskAllInputs is true, and unmask selector is used', async () => { + const page: puppeteer.Page = await browser.newPage(); + await page.goto('about:blank'); + await page.setContent( + getHtml.call(this, 'empty.html', { + maskAllInputs: true, + unmaskInputSelector: '.rr-unmask', + }), + ); + + await page.evaluate(() => { + const el = document.createElement('input'); + el.id = 'input-unmasked'; + el.className = 'rr-unmask'; + el.value = 'input should be unmasked'; + + const nextElement = document.querySelector('#one')!; + nextElement.parentNode!.insertBefore(el, nextElement); + }); + + await page.type('#input-unmasked', 'moo'); + + const snapshots = (await page.evaluate( + 'window.snapshots', + )) as eventWithTime[]; + assertSnapshot(snapshots.filter(isNotScroll)); + }); + + it('should always mask value attribute of passwords', async () => { + const page: puppeteer.Page = await browser.newPage(); + await page.goto('about:blank'); + await page.setContent( + getHtml.call(this, 'password.html', { + maskInputOptions: {}, + }), + ); + + 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'); + assertSnapshot(snapshots); + }); + + it('should mask text in form elements', async () => { + const page: puppeteer.Page = await browser.newPage(); + await page.goto('about:blank'); + await page.setContent( + getHtml.call(this, 'form.html', { maskAllText: true }), + ); + + // Ensure also masked when we change stuff + await page.evaluate(() => { + document + .querySelector('input[type="submit"]') + ?.setAttribute('value', 'new value'); + }); + + const snapshots = await page.evaluate('window.snapshots'); + assertSnapshot(snapshots); + }); + + it('should not record blocked elements from blockSelector, when dynamically added', async () => { + const page: puppeteer.Page = await browser.newPage(); + await page.goto('about:blank'); + await page.setContent( + getHtml.call(this, 'block.html', { + blockSelector: 'video', + }), + ); + + await page.evaluate(() => { + const el2 = document.createElement('video'); + el2.className = 'rr-block'; + el2.style.width = '100px'; + el2.style.height = '100px'; + const source2 = document.createElement('source'); + source2.src = 'file:///foo.mp4'; + // These aren't valid, but doing this for testing + source2.style.width = '100px'; + source2.style.height = '100px'; + el2.appendChild(source2); + + const el = document.createElement('video'); + el.style.width = '100px'; + el.style.height = '100px'; + const source = document.createElement('source'); + source.src = 'file:///foo.mp4'; + // These aren't valid, but doing this for testing + source.style.width = '100px'; + source.style.height = '100px'; + el.appendChild(source); + + const nextElement = document.querySelector('.rr-block')!; + nextElement.parentNode!.insertBefore(el, nextElement); + nextElement.parentNode!.insertBefore(el2, nextElement); + }); + + const snapshots = await page.evaluate('window.snapshots'); + assertSnapshot(snapshots); + }); + + it('should only record unblocked elements', async () => { + const page: puppeteer.Page = await browser.newPage(); + await page.goto('about:blank'); + await page.setContent( + getHtml.call(this, 'block.html', { + blockSelector: 'img,svg', + unblockSelector: '.rr-unblock', + }), + ); + + const snapshots = await page.evaluate('window.snapshots'); + assertSnapshot(snapshots); + }); + + + it('should mask only inputs', async () => { + const page: puppeteer.Page = await browser.newPage(); + await page.goto('about:blank'); + await page.setContent( + getHtml.call(this, 'mask-text.html', { + maskAllInputs: true, + maskAllText: false, + }), + ); + + const snapshots = await page.evaluate('window.snapshots'); + assertSnapshot(snapshots); + }); + + it('should mask all text (except unmaskTextSelector), using maskAllText ', async () => { + const page: puppeteer.Page = await browser.newPage(); + await page.goto('about:blank'); + await page.setContent( + getHtml.call(this, 'mask-text.html', { + maskTextClass: 'none', + maskAllText: true, + unmaskTextSelector: '.rr-unmask', + }), + ); + + const snapshots = await page.evaluate('window.snapshots'); + assertSnapshot(snapshots); + }); +}); diff --git a/packages/rrweb/test/integration.test.ts b/packages/rrweb/test/integration.test.ts index 39e32dbcd6..7d37666486 100644 --- a/packages/rrweb/test/integration.test.ts +++ b/packages/rrweb/test/integration.test.ts @@ -12,7 +12,7 @@ import { ISuite, } from './utils'; import type { recordOptions } from '../src/types'; -import { eventWithTime, EventType, RecordPlugin } from '@rrweb/types'; +import { eventWithTime, EventType, RecordPlugin, IncrementalSource } from '@rrweb/types'; import { visitSnapshot, NodeType } from 'rrweb-snapshot'; describe('record integration tests', function (this: ISuite) { @@ -122,7 +122,7 @@ describe('record integration tests', function (this: ISuite) { assertSnapshot(snapshots); }); - it('can record character data muatations', async () => { + it('can record character data mutations', async () => { const page: puppeteer.Page = await browser.newPage(); await page.goto('about:blank'); await page.setContent(getHtml.call(this, 'mutation-observer.html')); @@ -167,7 +167,12 @@ describe('record integration tests', function (this: ISuite) { it('handles null attribute values', async () => { const page: puppeteer.Page = await browser.newPage(); await page.goto('about:blank'); - await page.setContent(getHtml.call(this, 'mutation-observer.html', {})); + await page.setContent( + getHtml.call(this, 'mutation-observer.html', { + maskAllInputs: true, + maskAllText: true, + }), + ); await page.evaluate(() => { const li = document.createElement('li'); @@ -262,7 +267,10 @@ describe('record integration tests', function (this: ISuite) { const page: puppeteer.Page = await browser.newPage(); await page.goto('about:blank'); await page.setContent( - getHtml.call(this, 'form.html', { maskAllInputs: true }), + getHtml.call(this, 'form.html', { + maskAllInputs: true, + unmaskTextSelector: '.rr-unmask', + }), ); await page.type('input[type="text"]', 'test'); @@ -271,6 +279,7 @@ describe('record integration tests', function (this: ISuite) { await page.type('input[type="password"]', 'password'); await page.type('textarea', 'textarea test'); await page.select('select', '1'); + await page.type('#empty', 'test'); const snapshots = (await page.evaluate( 'window.snapshots', @@ -286,12 +295,13 @@ describe('record integration tests', function (this: ISuite) { maskInputOptions: { text: false, textarea: false, - password: true, + color: true, }, }), ); await page.type('input[type="text"]', 'test'); + await page.type('input[type="color"]', '#FF0000'); await page.click('input[type="radio"]'); await page.click('input[type="checkbox"]'); await page.type('textarea', 'textarea test'); diff --git a/packages/rrweb/test/replayer.test.ts b/packages/rrweb/test/replayer.test.ts index 7756710410..38214cab99 100644 --- a/packages/rrweb/test/replayer.test.ts +++ b/packages/rrweb/test/replayer.test.ts @@ -16,6 +16,7 @@ import inputEvents from './events/input'; import iframeEvents from './events/iframe'; import selectionEvents from './events/selection'; import shadowDomEvents from './events/shadow-dom'; +import shadowDomEventsSentry from './events/shadow-dom-sentry'; import StyleSheetTextMutation from './events/style-sheet-text-mutation'; import canvasInIframe from './events/canvas-in-iframe'; import adoptedStyleSheet from './events/adopted-style-sheet'; @@ -1076,4 +1077,18 @@ describe('replayer', function () { ), ).toBe(':hover'); }); + + it('should have `:defined` web components', async () => { + await page.evaluate(`events = ${JSON.stringify(shadowDomEventsSentry)}`); + const result = await page.evaluate(` + const { Replayer } = rrweb; + const replayer = new Replayer(events); + replayer.play(); + replayer.pause(1000); + replayer.iframe.contentDocument.querySelectorAll(':not(:defined)').length; + `); + await page.waitForTimeout(200); + + expect(result).toEqual(0); + }); }); diff --git a/packages/rrweb/test/utils.ts b/packages/rrweb/test/utils.ts index dd5a8cf7cc..81ce3e1425 100644 --- a/packages/rrweb/test/utils.ts +++ b/packages/rrweb/test/utils.ts @@ -595,11 +595,19 @@ export function generateRecordSnippet(options: recordOptions) { window.snapshots.push(event); }, maskTextSelector: ${JSON.stringify(options.maskTextSelector)}, + blockSelector: ${JSON.stringify(options.blockSelector)}, maskAllInputs: ${options.maskAllInputs}, maskInputOptions: ${JSON.stringify(options.maskAllInputs)}, + maskInputSelector: ${JSON.stringify(options.maskInputSelector)}, userTriggeredOnInput: ${options.userTriggeredOnInput}, + onMutation: ${options.onMutation || undefined}, + maskAllText: ${options.maskAllText}, maskTextFn: ${options.maskTextFn}, maskInputFn: ${options.maskInputFn}, + unmaskTextSelector: ${JSON.stringify(options.unmaskTextSelector)}, + unmaskInputSelector: ${JSON.stringify(options.unmaskInputSelector)}, + blockSelector: ${JSON.stringify(options.blockSelector)}, + unblockSelector: ${JSON.stringify(options.unblockSelector)}, recordCanvas: ${options.recordCanvas}, recordAfter: '${options.recordAfter || 'load'}', inlineImages: ${options.inlineImages},