From fd85c79ea9922e91dcc9aabeeba69053abccfa79 Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Sat, 6 Aug 2022 11:00:05 +0200 Subject: [PATCH] Handle negative ids in rrdom correctly + extra tests (#927) * inline stylesheets when loaded * set empty link elements to loaded by default * Clean up stylesheet manager * Remove attribute mutation code * Update packages/rrweb/test/record.test.ts * Update packages/rrweb/test/record.test.ts * Update packages/rrweb/test/record.test.ts * Update packages/rrweb/scripts/repl.js * Update packages/rrweb/test/record.test.ts * Update packages/rrweb/src/record/index.ts * Add todo * Move require out of time sensitive assert * Add waitForRAF, its more reliable than waitForTimeout * Remove flaky tests * Add recording stylesheets in iframes * Remove variability from flaky test * Make test more robust * Fix naming * Add test cases for inlineImages * Add test cases for inlineImages * Record iframe mutations cross page * Test: should record images inside iframe with blob url after iframe was reloaded * Handle negative ids in rrdom correctly When iframes get inserted they create untracked elements, both on the dom and rrdom side. Because they are untracked they generate negative numbers when fetching the id from mirror. This creates a problem when comparing and fetching ids across mirrors. This commit tries to get away from using negative ids as much as possible in rrdom's comparisons * Update packages/rrdom/src/diff.ts Co-authored-by: Yun Feng * Start unserialized nodes at -2 This way we don't accidentally think of them as mirror misses * Set unserialized id starting number at -2 * Remove duplication Co-authored-by: Yun Feng --- packages/rrdom/src/diff.ts | 33 +- packages/rrdom/src/index.ts | 5 +- .../__snapshots__/virtual-dom.test.ts.snap | 260 +-- packages/rrdom/test/diff.test.ts | 106 ++ packages/rrdom/test/virtual-dom.test.ts | 38 +- packages/rrweb-snapshot/src/snapshot.ts | 3 +- .../test/html/picture-blob-in-frame.html | 5 + .../test/html/picture-blob.html | 16 + .../test/html/picture-in-frame.html | 5 + .../rrweb-snapshot/test/integration.test.ts | 69 + packages/rrweb-snapshot/test/utils.ts | 11 + packages/rrweb/src/record/index.ts | 2 + packages/rrweb/src/record/mutation.ts | 17 +- packages/rrweb/src/types.ts | 2 + .../__snapshots__/integration.test.ts.snap | 1411 +++++++++++++++++ .../test/__snapshots__/replayer.test.ts.snap | 71 +- packages/rrweb/test/html/assets/robot.png | Bin 0 -> 11004 bytes .../rrweb/test/html/frame-image-blob-url.html | 11 + packages/rrweb/test/html/image-blob-url.html | 21 + packages/rrweb/test/integration.test.ts | 79 + packages/rrweb/test/replayer.test.ts | 29 +- packages/rrweb/test/utils.ts | 63 +- 22 files changed, 2064 insertions(+), 193 deletions(-) create mode 100644 packages/rrweb-snapshot/test/html/picture-blob-in-frame.html create mode 100644 packages/rrweb-snapshot/test/html/picture-blob.html create mode 100644 packages/rrweb-snapshot/test/html/picture-in-frame.html create mode 100644 packages/rrweb-snapshot/test/utils.ts create mode 100644 packages/rrweb/test/html/assets/robot.png create mode 100644 packages/rrweb/test/html/frame-image-blob-url.html create mode 100644 packages/rrweb/test/html/image-blob-url.html diff --git a/packages/rrdom/src/diff.ts b/packages/rrdom/src/diff.ts index f0896088..8456dd33 100644 --- a/packages/rrdom/src/diff.ts +++ b/packages/rrdom/src/diff.ts @@ -272,37 +272,57 @@ function diffChildren( let oldIdToIndex: Record | undefined = undefined, indexInOld; while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) { + const oldStartId = replayer.mirror.getId(oldStartNode); + const oldEndId = replayer.mirror.getId(oldEndNode); + const newStartId = rrnodeMirror.getId(newStartNode); + const newEndId = rrnodeMirror.getId(newEndNode); + + // rrdom contains elements with negative ids, we don't want to accidentally match those to a mirror mismatch (-1) id. + // Negative oldStartId happen when nodes are not in the mirror, but are in the DOM. + // eg.iframes come with a document, html, head and body nodes. + // thats why below we always check if an id is negative. + if (oldStartNode === undefined) { oldStartNode = oldChildren[++oldStartIndex]; } else if (oldEndNode === undefined) { oldEndNode = oldChildren[--oldEndIndex]; } else if ( - replayer.mirror.getId(oldStartNode) === rrnodeMirror.getId(newStartNode) + oldStartId !== -1 && + // same first element? + oldStartId === newStartId ) { diff(oldStartNode, newStartNode, replayer, rrnodeMirror); oldStartNode = oldChildren[++oldStartIndex]; newStartNode = newChildren[++newStartIndex]; } else if ( - replayer.mirror.getId(oldEndNode) === rrnodeMirror.getId(newEndNode) + oldEndId !== -1 && + // same last element? + oldEndId === newEndId ) { diff(oldEndNode, newEndNode, replayer, rrnodeMirror); oldEndNode = oldChildren[--oldEndIndex]; newEndNode = newChildren[--newEndIndex]; } else if ( - replayer.mirror.getId(oldStartNode) === rrnodeMirror.getId(newEndNode) + oldStartId !== -1 && + // is the first old element the same as the last new element? + oldStartId === newEndId ) { parentNode.insertBefore(oldStartNode, oldEndNode.nextSibling); diff(oldStartNode, newEndNode, replayer, rrnodeMirror); oldStartNode = oldChildren[++oldStartIndex]; newEndNode = newChildren[--newEndIndex]; } else if ( - replayer.mirror.getId(oldEndNode) === rrnodeMirror.getId(newStartNode) + oldEndId !== -1 && + // is the last old element the same as the first new element? + oldEndId === newStartId ) { parentNode.insertBefore(oldEndNode, oldStartNode); diff(oldEndNode, newStartNode, replayer, rrnodeMirror); oldEndNode = oldChildren[--oldEndIndex]; newStartNode = newChildren[++newStartIndex]; } else { + // none of the elements matched + if (!oldIdToIndex) { oldIdToIndex = {}; for (let i = oldStartIndex; i <= oldEndIndex; i++) { @@ -378,8 +398,11 @@ export function createOrGetNode( domMirror: NodeMirror, rrnodeMirror: Mirror, ): Node { - let node = domMirror.getNode(rrnodeMirror.getId(rrNode)); + const nodeId = rrnodeMirror.getId(rrNode); const sn = rrnodeMirror.getMeta(rrNode); + let node: Node | null = null; + // negative ids shouldn't be compared accross mirrors + if (nodeId > -1) node = domMirror.getNode(nodeId); if (node !== null) return node; switch (rrNode.RRNodeType) { case RRNodeType.Document: diff --git a/packages/rrdom/src/index.ts b/packages/rrdom/src/index.ts index 16da3ed8..0caefa8d 100644 --- a/packages/rrdom/src/index.ts +++ b/packages/rrdom/src/index.ts @@ -33,10 +33,11 @@ import { } from './document'; export class RRDocument extends BaseRRDocumentImpl(RRNode) { + private UNSERIALIZED_STARTING_ID = -2; // In the rrweb replayer, there are some unserialized nodes like the element that stores the injected style rules. // These unserialized nodes may interfere the execution of the diff algorithm. // The id of serialized node is larger than 0. So this value less than 0 is used as id for these unserialized nodes. - private _unserializedId = -1; + private _unserializedId = this.UNSERIALIZED_STARTING_ID; /** * Every time the id is used, it will minus 1 automatically to avoid collisions. @@ -135,7 +136,7 @@ export class RRDocument extends BaseRRDocumentImpl(RRNode) { open() { super.open(); - this._unserializedId = -1; + this._unserializedId = this.UNSERIALIZED_STARTING_ID; } } diff --git a/packages/rrdom/test/__snapshots__/virtual-dom.test.ts.snap b/packages/rrdom/test/__snapshots__/virtual-dom.test.ts.snap index c1dc0024..2286457a 100644 --- a/packages/rrdom/test/__snapshots__/virtual-dom.test.ts.snap +++ b/packages/rrdom/test/__snapshots__/virtual-dom.test.ts.snap @@ -1,118 +1,118 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`RRDocument for browser environment create a RRDocument from a html document can build from a common html 1`] = ` -"-1 RRDocument - -2 RRDocumentType - -3 HTML lang=\\"en\\" - -4 HEAD - -5 RRText text=\\"\\\\n \\" - -6 META charset=\\"UTF-8\\" - -7 RRText text=\\"\\\\n \\" - -8 META name=\\"viewport\\" content=\\"width=device-width, initial-scale=1.0\\" - -9 RRText text=\\"\\\\n \\" - -10 TITLE - -11 RRText text=\\"Main\\" - -12 RRText text=\\"\\\\n \\" - -13 LINK rel=\\"stylesheet\\" href=\\"somelink\\" - -14 RRText text=\\"\\\\n \\" - -15 STYLE - -16 RRText text=\\"\\\\n h1 {\\\\n color: 'black';\\\\n }\\\\n .blocks {\\\\n padding: 0;\\\\n }\\\\n .blocks1 {\\\\n margin: 0;\\\\n }\\\\n #block1 {\\\\n width: 100px;\\\\n height: 200px;\\\\n }\\\\n @import url('main.css');\\\\n \\" - -17 RRText text=\\"\\\\n \\" - -18 RRText text=\\"\\\\n \\" - -19 BODY - -20 RRText text=\\"\\\\n \\" - -21 H1 - -22 RRText text=\\"This is a h1 heading\\" - -23 RRText text=\\"\\\\n \\" - -24 H1 style=\\"font-size: 16px\\" - -25 RRText text=\\"This is a h1 heading with styles\\" - -26 RRText text=\\"\\\\n \\" - -27 DIV id=\\"block1\\" class=\\"blocks blocks1\\" - -28 RRText text=\\"\\\\n \\" - -29 DIV id=\\"block2\\" class=\\"blocks blocks1 :hover\\" - -30 RRText text=\\"\\\\n Text 1\\\\n \\" - -31 DIV id=\\"block3\\" - -32 RRText text=\\"\\\\n \\" - -33 P - -34 RRText text=\\"This is a paragraph\\" - -35 RRText text=\\"\\\\n \\" - -36 BUTTON - -37 RRText text=\\"button1\\" - -38 RRText text=\\"\\\\n \\" - -39 RRText text=\\"\\\\n Text 2\\\\n \\" - -40 RRText text=\\"\\\\n \\" - -41 IMG src=\\"somelink\\" alt=\\"This is an image\\" - -42 RRText text=\\"\\\\n \\" - -43 RRComment text=\\" This is a line of comment \\" - -44 RRText text=\\"\\\\n \\" - -45 FORM - -46 RRText text=\\"\\\\n \\" - -47 INPUT type=\\"text\\" id=\\"input1\\" - -48 RRText text=\\"\\\\n \\" - -49 RRText text=\\"\\\\n \\" - -50 RRText text=\\"\\\\n \\\\n\\\\n\\" +"-2 RRDocument + -3 RRDocumentType + -4 HTML lang=\\"en\\" + -5 HEAD + -6 RRText text=\\"\\\\n \\" + -7 META charset=\\"UTF-8\\" + -8 RRText text=\\"\\\\n \\" + -9 META name=\\"viewport\\" content=\\"width=device-width, initial-scale=1.0\\" + -10 RRText text=\\"\\\\n \\" + -11 TITLE + -12 RRText text=\\"Main\\" + -13 RRText text=\\"\\\\n \\" + -14 LINK rel=\\"stylesheet\\" href=\\"somelink\\" + -15 RRText text=\\"\\\\n \\" + -16 STYLE + -17 RRText text=\\"\\\\n h1 {\\\\n color: 'black';\\\\n }\\\\n .blocks {\\\\n padding: 0;\\\\n }\\\\n .blocks1 {\\\\n margin: 0;\\\\n }\\\\n #block1 {\\\\n width: 100px;\\\\n height: 200px;\\\\n }\\\\n @import url('main.css');\\\\n \\" + -18 RRText text=\\"\\\\n \\" + -19 RRText text=\\"\\\\n \\" + -20 BODY + -21 RRText text=\\"\\\\n \\" + -22 H1 + -23 RRText text=\\"This is a h1 heading\\" + -24 RRText text=\\"\\\\n \\" + -25 H1 style=\\"font-size: 16px\\" + -26 RRText text=\\"This is a h1 heading with styles\\" + -27 RRText text=\\"\\\\n \\" + -28 DIV id=\\"block1\\" class=\\"blocks blocks1\\" + -29 RRText text=\\"\\\\n \\" + -30 DIV id=\\"block2\\" class=\\"blocks blocks1 :hover\\" + -31 RRText text=\\"\\\\n Text 1\\\\n \\" + -32 DIV id=\\"block3\\" + -33 RRText text=\\"\\\\n \\" + -34 P + -35 RRText text=\\"This is a paragraph\\" + -36 RRText text=\\"\\\\n \\" + -37 BUTTON + -38 RRText text=\\"button1\\" + -39 RRText text=\\"\\\\n \\" + -40 RRText text=\\"\\\\n Text 2\\\\n \\" + -41 RRText text=\\"\\\\n \\" + -42 IMG src=\\"somelink\\" alt=\\"This is an image\\" + -43 RRText text=\\"\\\\n \\" + -44 RRComment text=\\" This is a line of comment \\" + -45 RRText text=\\"\\\\n \\" + -46 FORM + -47 RRText text=\\"\\\\n \\" + -48 INPUT type=\\"text\\" id=\\"input1\\" + -49 RRText text=\\"\\\\n \\" + -50 RRText text=\\"\\\\n \\" + -51 RRText text=\\"\\\\n \\\\n\\\\n\\" " `; exports[`RRDocument for browser environment create a RRDocument from a html document can build from a html containing nested shadow doms 1`] = ` -"-1 RRDocument - -2 RRDocumentType - -3 HTML lang=\\"en\\" - -4 HEAD - -5 RRText text=\\"\\\\n \\" - -6 META charset=\\"UTF-8\\" - -7 RRText text=\\"\\\\n \\" - -8 META name=\\"viewport\\" content=\\"width=device-width, initial-scale=1.0\\" - -9 RRText text=\\"\\\\n \\" - -10 TITLE - -11 RRText text=\\"shadow dom\\" - -12 RRText text=\\"\\\\n \\" - -13 RRText text=\\"\\\\n \\" - -14 BODY - -15 RRText text=\\"\\\\n \\" - -16 DIV - -17 SHADOWROOT - -18 RRText text=\\"\\\\n \\" - -19 SPAN - -20 RRText text=\\" shadow dom one \\" - -21 RRText text=\\"\\\\n \\" - -22 DIV - -23 SHADOWROOT - -24 RRText text=\\"\\\\n \\" - -25 SPAN - -26 RRText text=\\" shadow dom two \\" - -27 RRText text=\\"\\\\n \\" - -28 RRText text=\\"\\\\n \\\\n \\" - -29 RRText text=\\"\\\\n \\" - -30 RRText text=\\"\\\\n \\\\n \\" - -31 RRText text=\\"\\\\n \\\\n\\\\n\\" +"-2 RRDocument + -3 RRDocumentType + -4 HTML lang=\\"en\\" + -5 HEAD + -6 RRText text=\\"\\\\n \\" + -7 META charset=\\"UTF-8\\" + -8 RRText text=\\"\\\\n \\" + -9 META name=\\"viewport\\" content=\\"width=device-width, initial-scale=1.0\\" + -10 RRText text=\\"\\\\n \\" + -11 TITLE + -12 RRText text=\\"shadow dom\\" + -13 RRText text=\\"\\\\n \\" + -14 RRText text=\\"\\\\n \\" + -15 BODY + -16 RRText text=\\"\\\\n \\" + -17 DIV + -18 SHADOWROOT + -19 RRText text=\\"\\\\n \\" + -20 SPAN + -21 RRText text=\\" shadow dom one \\" + -22 RRText text=\\"\\\\n \\" + -23 DIV + -24 SHADOWROOT + -25 RRText text=\\"\\\\n \\" + -26 SPAN + -27 RRText text=\\" shadow dom two \\" + -28 RRText text=\\"\\\\n \\" + -29 RRText text=\\"\\\\n \\\\n \\" + -30 RRText text=\\"\\\\n \\" + -31 RRText text=\\"\\\\n \\\\n \\" + -32 RRText text=\\"\\\\n \\\\n\\\\n\\" " `; exports[`RRDocument for browser environment create a RRDocument from a html document can build from a xml page 1`] = ` -"-1 RRDocument - -2 XML - -3 RRCDATASection data=\\"Some data & then some\\" +"-2 RRDocument + -3 XML + -4 RRCDATASection data=\\"Some data & then some\\" " `; exports[`RRDocument for browser environment create a RRDocument from a html document can build from an iframe html 1`] = ` -"-1 RRDocument - -2 RRDocumentType - -3 HTML lang=\\"en\\" - -4 HEAD - -5 RRText text=\\"\\\\n \\" - -6 META charset=\\"UTF-8\\" - -7 RRText text=\\"\\\\n \\" - -8 META name=\\"viewport\\" content=\\"width=device-width, initial-scale=1.0\\" - -9 RRText text=\\"\\\\n \\" - -10 TITLE - -11 RRText text=\\"Iframe\\" - -12 RRText text=\\"\\\\n \\" - -13 RRText text=\\"\\\\n \\" - -14 BODY - -15 RRText text=\\"\\\\n \\" - -16 IFRAME id=\\"iframe1\\" srcdoc=\\" +"-2 RRDocument + -3 RRDocumentType + -4 HTML lang=\\"en\\" + -5 HEAD + -6 RRText text=\\"\\\\n \\" + -7 META charset=\\"UTF-8\\" + -8 RRText text=\\"\\\\n \\" + -9 META name=\\"viewport\\" content=\\"width=device-width, initial-scale=1.0\\" + -10 RRText text=\\"\\\\n \\" + -11 TITLE + -12 RRText text=\\"Iframe\\" + -13 RRText text=\\"\\\\n \\" + -14 RRText text=\\"\\\\n \\" + -15 BODY + -16 RRText text=\\"\\\\n \\" + -17 IFRAME id=\\"iframe1\\" srcdoc=\\" @@ -126,35 +126,35 @@ exports[`RRDocument for browser environment create a RRDocument from a html docu '); + + const iframe = document.querySelector('iframe')!; + // Remove everthing from the iframe but the root html element + // `buildNodeWithSn` injects docType elements to trigger compatMode in iframes + iframe.contentDocument!.write( + '', + ); + + replayer.mirror.add(iframe.contentDocument!, { + id: 1, + type: 0, + childNodes: [ + { + id: 2, + rootId: 1, + type: 2, + tagName: 'html', + childNodes: [], + attributes: {}, + }, + ], + } as serializedNodeWithId); + replayer.mirror.add(iframe.contentDocument!.childNodes[0], { + id: 2, + rootId: 1, + type: 2, + tagName: 'html', + childNodes: [], + attributes: {}, + } as serializedNodeWithId); + + const rrDocument = new RRDocument(); + rrDocument.mirror.add(rrDocument, getDefaultSN(rrDocument, 1)); + const docType = rrDocument.createDocumentType('html', '', ''); + rrDocument.mirror.add(docType, getDefaultSN(docType, 2)); + rrDocument.appendChild(docType); + const htmlEl = rrDocument.createElement('html'); + rrDocument.mirror.add(htmlEl, getDefaultSN(htmlEl, 3)); + rrDocument.appendChild(htmlEl); + const styleEl = rrDocument.createElement('style'); + rrDocument.mirror.add(styleEl, getDefaultSN(styleEl, 4)); + htmlEl.appendChild(styleEl); + const headEl = rrDocument.createElement('head'); + rrDocument.mirror.add(headEl, getDefaultSN(headEl, 5)); + htmlEl.appendChild(headEl); + const bodyEl = rrDocument.createElement('body'); + rrDocument.mirror.add(bodyEl, getDefaultSN(bodyEl, 6)); + htmlEl.appendChild(bodyEl); + + diff(iframe.contentDocument!, rrDocument, replayer); + expect(iframe.contentDocument!.childNodes.length).toBe(2); + const element = iframe.contentDocument!.childNodes[0] as HTMLElement; + expect(element.nodeType).toBe(element.DOCUMENT_TYPE_NODE); + expect(mirror.getId(element)).toEqual(2); + }); + + it('should remove children from document before adding new nodes 3', () => { + document.write(''); + + const iframeInDom = document.querySelector('iframe')!; + + replayer.mirror.add(iframeInDom, { + id: 3, + type: 2, + rootId: 1, + tagName: 'iframe', + childNodes: [], + attributes: {}, + } as serializedNodeWithId); + replayer.mirror.add(iframeInDom.contentDocument!, { + id: 4, + type: 0, + childNodes: [], + } as serializedNodeWithId); + + const rrDocument = new RRDocument(); + + const rrIframeEl = rrDocument.createElement('iframe'); + rrDocument.mirror.add(rrIframeEl, getDefaultSN(rrIframeEl, 3)); + rrDocument.appendChild(rrIframeEl); + rrDocument.mirror.add( + rrIframeEl.contentDocument!, + getDefaultSN(rrIframeEl.contentDocument!, 4), + ); + + const rrDocType = rrDocument.createDocumentType('html', '', ''); + rrIframeEl.contentDocument.appendChild(rrDocType); + const rrHtmlEl = rrDocument.createElement('html'); + rrDocument.mirror.add(rrHtmlEl, getDefaultSN(rrHtmlEl, 6)); + rrIframeEl.contentDocument.appendChild(rrHtmlEl); + const rrHeadEl = rrDocument.createElement('head'); + rrDocument.mirror.add(rrHeadEl, getDefaultSN(rrHeadEl, 8)); + rrHtmlEl.appendChild(rrHeadEl); + const bodyEl = rrDocument.createElement('body'); + rrDocument.mirror.add(bodyEl, getDefaultSN(bodyEl, 9)); + rrHtmlEl.appendChild(bodyEl); + + diff(iframeInDom, rrIframeEl, replayer); + expect(iframeInDom.contentDocument!.childNodes.length).toBe(2); + const element = iframeInDom.contentDocument!.childNodes[0] as HTMLElement; + expect(element.nodeType).toBe(element.DOCUMENT_TYPE_NODE); + expect(mirror.getId(element)).toEqual(-1); + }); }); describe('create or get a Node', () => { diff --git a/packages/rrdom/test/virtual-dom.test.ts b/packages/rrdom/test/virtual-dom.test.ts index b99a3432..57aed3e7 100644 --- a/packages/rrdom/test/virtual-dom.test.ts +++ b/packages/rrdom/test/virtual-dom.test.ts @@ -78,24 +78,24 @@ describe('RRDocument for browser environment', () => { const rrdom = new RRDocument(); let rrNode = buildFromNode(document, rrdom, mirror)!; expect(mirror.getMeta(document)).toBeDefined(); - expect(mirror.getId(document)).toEqual(-1); + expect(mirror.getId(document)).toEqual(-2); expect(rrNode).not.toBeNull(); expect(rrdom.mirror.getMeta(rrNode)).toBeDefined(); expect(rrdom.mirror.getMeta(rrNode)!.type).toEqual(RRNodeType.Document); - expect(rrdom.mirror.getId(rrNode)).toEqual(-1); + expect(rrdom.mirror.getId(rrNode)).toEqual(-2); expect(rrNode).toBe(rrdom); // build from document type expect(mirror.getMeta(document.doctype!)).toBeNull(); rrNode = buildFromNode(document.doctype!, rrdom, mirror)!; expect(mirror.getMeta(document.doctype!)).toBeDefined(); - expect(mirror.getId(document.doctype)).toEqual(-2); + expect(mirror.getId(document.doctype)).toEqual(-3); expect(rrNode).not.toBeNull(); expect(rrdom.mirror.getMeta(rrNode)).toBeDefined(); expect(rrdom.mirror.getMeta(rrNode)!.type).toEqual( RRNodeType.DocumentType, ); - expect(rrdom.mirror.getId(rrNode)).toEqual(-2); + expect(rrdom.mirror.getId(rrNode)).toEqual(-3); // build from element expect(mirror.getMeta(document.documentElement)).toBeNull(); @@ -105,33 +105,33 @@ describe('RRDocument for browser environment', () => { mirror, )!; expect(mirror.getMeta(document.documentElement)).toBeDefined(); - expect(mirror.getId(document.documentElement)).toEqual(-3); + expect(mirror.getId(document.documentElement)).toEqual(-4); expect(rrNode).not.toBeNull(); expect(rrdom.mirror.getMeta(rrNode)).toBeDefined(); expect(rrdom.mirror.getMeta(rrNode)!.type).toEqual(RRNodeType.Element); - expect(rrdom.mirror.getId(rrNode)).toEqual(-3); + expect(rrdom.mirror.getId(rrNode)).toEqual(-4); // build from text const text = document.createTextNode('text'); expect(mirror.getMeta(text)).toBeNull(); rrNode = buildFromNode(text, rrdom, mirror)!; expect(mirror.getMeta(text)).toBeDefined(); - expect(mirror.getId(text)).toEqual(-4); + expect(mirror.getId(text)).toEqual(-5); expect(rrNode).not.toBeNull(); expect(rrdom.mirror.getMeta(rrNode)).toBeDefined(); expect(rrdom.mirror.getMeta(rrNode)!.type).toEqual(RRNodeType.Text); - expect(rrdom.mirror.getId(rrNode)).toEqual(-4); + expect(rrdom.mirror.getId(rrNode)).toEqual(-5); // build from comment const comment = document.createComment('comment'); expect(mirror.getMeta(comment)).toBeNull(); rrNode = buildFromNode(comment, rrdom, mirror)!; expect(mirror.getMeta(comment)).toBeDefined(); - expect(mirror.getId(comment)).toEqual(-5); + expect(mirror.getId(comment)).toEqual(-6); expect(rrNode).not.toBeNull(); expect(rrdom.mirror.getMeta(rrNode)).toBeDefined(); expect(rrdom.mirror.getMeta(rrNode)!.type).toEqual(RRNodeType.Comment); - expect(rrdom.mirror.getId(rrNode)).toEqual(-5); + expect(rrdom.mirror.getId(rrNode)).toEqual(-6); // build from CDATASection const xmlDoc = new DOMParser().parseFromString( @@ -144,11 +144,11 @@ describe('RRDocument for browser environment', () => { expect(mirror.getMeta(cdataSection)).toBeNull(); rrNode = buildFromNode(cdataSection, rrdom, mirror)!; expect(mirror.getMeta(cdataSection)).toBeDefined(); - expect(mirror.getId(cdataSection)).toEqual(-6); + expect(mirror.getId(cdataSection)).toEqual(-7); expect(rrNode).not.toBeNull(); expect(rrdom.mirror.getMeta(rrNode)).toBeDefined(); expect(rrdom.mirror.getMeta(rrNode)!.type).toEqual(RRNodeType.CDATA); - expect(rrdom.mirror.getId(rrNode)).toEqual(-6); + expect(rrdom.mirror.getId(rrNode)).toEqual(-7); expect(rrNode.textContent).toEqual(cdata); }); @@ -184,8 +184,8 @@ describe('RRDocument for browser environment', () => { expect(rrNode).not.toBeNull(); expect(rrdom.mirror.getMeta(rrNode)).toBeDefined(); expect(rrdom.mirror.getMeta(rrNode)!.type).toEqual(RRNodeType.Document); - expect(rrdom.mirror.getId(rrNode)).toEqual(-1); - expect(mirror.getId(iframe.contentDocument)).toEqual(-1); + expect(rrdom.mirror.getId(rrNode)).toEqual(-2); + expect(mirror.getId(iframe.contentDocument)).toEqual(-2); expect(rrNode).toBe(RRIFrame.contentDocument); }); @@ -203,8 +203,8 @@ describe('RRDocument for browser environment', () => { )!; expect(rrNode).not.toBeNull(); expect(rrdom.mirror.getMeta(rrNode)).toBeDefined(); - expect(rrdom.mirror.getId(rrNode)).toEqual(-1); - expect(mirror.getId(div.shadowRoot)).toEqual(-1); + expect(rrdom.mirror.getId(rrNode)).toEqual(-2); + expect(mirror.getId(div.shadowRoot)).toEqual(-2); expect(rrNode.RRNodeType).toEqual(RRNodeType.Element); expect((rrNode as RRElement).tagName).toEqual('SHADOWROOT'); expect(rrNode).toBe(parentRRNode.shadowRoot); @@ -296,7 +296,7 @@ describe('RRDocument for browser environment', () => { describe('RRDocument build for virtual dom', () => { it('can access a unique, decremented unserializedId every time', () => { const node = new RRDocument(); - for (let i = 1; i <= 100; i++) expect(node.unserializedId).toBe(-i); + for (let i = 2; i <= 100; i++) expect(node.unserializedId).toBe(-i); }); it('can create a new RRDocument', () => { @@ -357,12 +357,12 @@ describe('RRDocument for browser environment', () => { const documentType = dom.createDocumentType('html', '', ''); dom.appendChild(documentType); expect(dom.childNodes[0]).toBe(documentType); - expect(dom.unserializedId).toBe(-1); expect(dom.unserializedId).toBe(-2); + expect(dom.unserializedId).toBe(-3); expect(dom.close()); expect(dom.open()); expect(dom.childNodes.length).toEqual(0); - expect(dom.unserializedId).toBe(-1); + expect(dom.unserializedId).toBe(-2); }); it('can execute a dummy getContext function in RRCanvasElement', () => { diff --git a/packages/rrweb-snapshot/src/snapshot.ts b/packages/rrweb-snapshot/src/snapshot.ts index a61f650a..38df3b69 100644 --- a/packages/rrweb-snapshot/src/snapshot.ts +++ b/packages/rrweb-snapshot/src/snapshot.ts @@ -388,7 +388,8 @@ function onceIframeLoaded( // iframe was already loaded, make sure we wait to trigger the listener // till _after_ the mutation that found this iframe has had time to process setTimeout(listener, 0); - return; + + return iframeEl.addEventListener('load', listener); // keep listing for future loads } // use default listener iframeEl.addEventListener('load', listener); diff --git a/packages/rrweb-snapshot/test/html/picture-blob-in-frame.html b/packages/rrweb-snapshot/test/html/picture-blob-in-frame.html new file mode 100644 index 00000000..f6237f22 --- /dev/null +++ b/packages/rrweb-snapshot/test/html/picture-blob-in-frame.html @@ -0,0 +1,5 @@ + + + + + diff --git a/packages/rrweb-snapshot/test/html/picture-blob.html b/packages/rrweb-snapshot/test/html/picture-blob.html new file mode 100644 index 00000000..d2a32658 --- /dev/null +++ b/packages/rrweb-snapshot/test/html/picture-blob.html @@ -0,0 +1,16 @@ + + + This is a robot + + + diff --git a/packages/rrweb-snapshot/test/html/picture-in-frame.html b/packages/rrweb-snapshot/test/html/picture-in-frame.html new file mode 100644 index 00000000..31684d2c --- /dev/null +++ b/packages/rrweb-snapshot/test/html/picture-in-frame.html @@ -0,0 +1,5 @@ + + + + + diff --git a/packages/rrweb-snapshot/test/integration.test.ts b/packages/rrweb-snapshot/test/integration.test.ts index cba669c1..6ee32f94 100644 --- a/packages/rrweb-snapshot/test/integration.test.ts +++ b/packages/rrweb-snapshot/test/integration.test.ts @@ -6,6 +6,7 @@ import * as puppeteer from 'puppeteer'; import * as rollup from 'rollup'; import * as typescript from 'rollup-plugin-typescript2'; import * as assert from 'assert'; +import { waitForRAF } from './utils'; const _typescript = (typescript as unknown) as () => rollup.Plugin; @@ -207,6 +208,74 @@ iframe.contentDocument.querySelector('center').clientHeight assert(snapshot.includes('"rr_dataURL"')); assert(snapshot.includes('data:image/webp;base64,')); }); + + it('correctly saves blob:images offline', async () => { + const page: puppeteer.Page = await browser.newPage(); + + await page.goto('http://localhost:3030/html/picture-blob.html', { + waitUntil: 'load', + }); + await page.waitForSelector('img', { timeout: 1000 }); + await page.evaluate(`${code}var snapshot = rrweb.snapshot(document, { + dataURLOptions: { type: "image/webp", quality: 0.8 }, + inlineImages: true, + inlineStylesheet: false + })`); + await page.waitFor(100); + const snapshot = await page.evaluate('JSON.stringify(snapshot, null, 2);'); + assert(snapshot.includes('"rr_dataURL"')); + assert(snapshot.includes('data:image/webp;base64,')); + }); + + it('correctly saves images in iframes offline', async () => { + const page: puppeteer.Page = await browser.newPage(); + + await page.goto('http://localhost:3030/html/picture-in-frame.html', { + waitUntil: 'load', + }); + await page.waitForSelector('iframe', { timeout: 1000 }); + await waitForRAF(page); // wait for page to render + await page.evaluate(`${code} + rrweb.snapshot(document, { + dataURLOptions: { type: "image/webp", quality: 0.8 }, + inlineImages: true, + inlineStylesheet: false, + onIframeLoad: function(iframe, sn) { + window.snapshot = sn; + } + })`); + await page.waitFor(100); + const snapshot = await page.evaluate( + 'JSON.stringify(window.snapshot, null, 2);', + ); + assert(snapshot.includes('"rr_dataURL"')); + assert(snapshot.includes('data:image/webp;base64,')); + }); + + it('correctly saves blob:images in iframes offline', async () => { + const page: puppeteer.Page = await browser.newPage(); + + await page.goto('http://localhost:3030/html/picture-blob-in-frame.html', { + waitUntil: 'load', + }); + await page.waitForSelector('iframe', { timeout: 1000 }); + await waitForRAF(page); // wait for page to render + await page.evaluate(`${code} + rrweb.snapshot(document, { + dataURLOptions: { type: "image/webp", quality: 0.8 }, + inlineImages: true, + inlineStylesheet: false, + onIframeLoad: function(iframe, sn) { + window.snapshot = sn; + } + })`); + await page.waitFor(100); + const snapshot = await page.evaluate( + 'JSON.stringify(window.snapshot, null, 2);', + ); + assert(snapshot.includes('"rr_dataURL"')); + assert(snapshot.includes('data:image/webp;base64,')); + }); }); describe('iframe integration tests', function (this: ISuite) { diff --git a/packages/rrweb-snapshot/test/utils.ts b/packages/rrweb-snapshot/test/utils.ts new file mode 100644 index 00000000..43d4484b --- /dev/null +++ b/packages/rrweb-snapshot/test/utils.ts @@ -0,0 +1,11 @@ +import * as puppeteer from 'puppeteer'; + +export async function waitForRAF(page: puppeteer.Page) { + return await page.evaluate(() => { + return new Promise((resolve) => { + requestAnimationFrame(() => { + requestAnimationFrame(resolve); + }); + }); + }); +} diff --git a/packages/rrweb/src/record/index.ts b/packages/rrweb/src/record/index.ts index a7b513c2..756c21c5 100644 --- a/packages/rrweb/src/record/index.ts +++ b/packages/rrweb/src/record/index.ts @@ -249,6 +249,7 @@ function record( iframeManager, stylesheetManager, canvasManager, + keepIframeSrcFn, }, mirror, }); @@ -455,6 +456,7 @@ function record( doc, maskInputFn, maskTextFn, + keepIframeSrcFn, blockSelector, slimDOMOptions, mirror, diff --git a/packages/rrweb/src/record/mutation.ts b/packages/rrweb/src/record/mutation.ts index 425445cd..df3b3017 100644 --- a/packages/rrweb/src/record/mutation.ts +++ b/packages/rrweb/src/record/mutation.ts @@ -165,6 +165,7 @@ export default class MutationBuffer { private maskInputOptions: observerParam['maskInputOptions']; private maskTextFn: observerParam['maskTextFn']; private maskInputFn: observerParam['maskInputFn']; + private keepIframeSrcFn: observerParam['keepIframeSrcFn']; private recordCanvas: observerParam['recordCanvas']; private inlineImages: observerParam['inlineImages']; private slimDOMOptions: observerParam['slimDOMOptions']; @@ -186,6 +187,7 @@ export default class MutationBuffer { 'maskInputOptions', 'maskTextFn', 'maskInputFn', + 'keepIframeSrcFn', 'recordCanvas', 'inlineImages', 'slimDOMOptions', @@ -485,6 +487,19 @@ export default class MutationBuffer { let item: attributeCursor | undefined = this.attributes.find( (a) => a.node === m.target, ); + if ( + target.tagName === 'IFRAME' && + m.attributeName === 'src' && + !this.keepIframeSrcFn(value as string) + ) { + if (!(target as HTMLIFrameElement).contentDocument) { + // we can't record it directly as we can't see into it + // preserve the src attribute so a decision can be taken at replay time + m.attributeName = 'rr_src'; + } else { + return; + } + } if (!item) { item = { node: m.target, @@ -528,7 +543,7 @@ export default class MutationBuffer { // overwrite attribute if the mutations was triggered in same time item.attributes[m.attributeName!] = transformAttribute( this.doc, - (m.target as HTMLElement).tagName, + target.tagName, m.attributeName!, value!, ); diff --git a/packages/rrweb/src/types.ts b/packages/rrweb/src/types.ts index 79ae5296..d9a14817 100644 --- a/packages/rrweb/src/types.ts +++ b/packages/rrweb/src/types.ts @@ -276,6 +276,7 @@ export type observerParam = { maskInputOptions: MaskInputOptions; maskInputFn?: MaskInputFn; maskTextFn?: MaskTextFn; + keepIframeSrcFn: KeepIframeSrcFn; inlineStylesheet: boolean; styleSheetRuleCb: styleSheetRuleCallback; styleDeclarationCb: styleDeclarationCallback; @@ -315,6 +316,7 @@ export type MutationBufferParam = Pick< | 'maskInputOptions' | 'maskTextFn' | 'maskInputFn' + | 'keepIframeSrcFn' | 'recordCanvas' | 'inlineImages' | 'slimDOMOptions' diff --git a/packages/rrweb/test/__snapshots__/integration.test.ts.snap b/packages/rrweb/test/__snapshots__/integration.test.ts.snap index 53b82060..b25764af 100644 --- a/packages/rrweb/test/__snapshots__/integration.test.ts.snap +++ b/packages/rrweb/test/__snapshots__/integration.test.ts.snap @@ -9096,6 +9096,1108 @@ exports[`record integration tests should record dynamic CSS changes 1`] = ` ]" `; +exports[`record integration tests should record images inside iframe with blob url 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\\": \\"title\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"Frame with image\\", + \\"id\\": 11 + } + ], + \\"id\\": 10 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 12 + } + ], + \\"id\\": 4 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 13 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 15 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"iframe\\", + \\"attributes\\": { + \\"id\\": \\"four\\", + \\"frameborder\\": \\"0\\" + }, + \\"childNodes\\": [], + \\"id\\": 16 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\", + \\"id\\": 17 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 19 + } + ], + \\"id\\": 18 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\\\n\\\\n\\", + \\"id\\": 20 + } + ], + \\"id\\": 14 + } + ], + \\"id\\": 3 + } + ], + \\"id\\": 1 + }, + \\"initialOffset\\": { + \\"left\\": 0, + \\"top\\": 0 + } + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"adds\\": [ + { + \\"parentId\\": 16, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 0, + \\"childNodes\\": [ + { + \\"type\\": 1, + \\"name\\": \\"html\\", + \\"publicId\\": \\"\\", + \\"systemId\\": \\"\\", + \\"rootId\\": 21, + \\"id\\": 22 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"html\\", + \\"attributes\\": { + \\"lang\\": \\"en\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"head\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"rootId\\": 21, + \\"id\\": 25 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"meta\\", + \\"attributes\\": { + \\"charset\\": \\"UTF-8\\" + }, + \\"childNodes\\": [], + \\"rootId\\": 21, + \\"id\\": 26 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"rootId\\": 21, + \\"id\\": 27 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"meta\\", + \\"attributes\\": { + \\"http-equiv\\": \\"X-UA-Compatible\\", + \\"content\\": \\"IE=edge\\" + }, + \\"childNodes\\": [], + \\"rootId\\": 21, + \\"id\\": 28 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"rootId\\": 21, + \\"id\\": 29 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"meta\\", + \\"attributes\\": { + \\"name\\": \\"viewport\\", + \\"content\\": \\"width=device-width, initial-scale=1.0\\" + }, + \\"childNodes\\": [], + \\"rootId\\": 21, + \\"id\\": 30 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"rootId\\": 21, + \\"id\\": 31 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"title\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"Image with blob:url\\", + \\"rootId\\": 21, + \\"id\\": 33 + } + ], + \\"rootId\\": 21, + \\"id\\": 32 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"rootId\\": 21, + \\"id\\": 34 + } + ], + \\"rootId\\": 21, + \\"id\\": 24 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"rootId\\": 21, + \\"id\\": 35 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"rootId\\": 21, + \\"id\\": 37 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"rootId\\": 21, + \\"id\\": 39 + } + ], + \\"rootId\\": 21, + \\"id\\": 38 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n\\\\n\\", + \\"rootId\\": 21, + \\"id\\": 40 + } + ], + \\"rootId\\": 21, + \\"id\\": 36 + } + ], + \\"rootId\\": 21, + \\"id\\": 23 + } + ], + \\"id\\": 21 + } + } + ], + \\"removes\\": [], + \\"texts\\": [], + \\"attributes\\": [], + \\"isAttachIframe\\": true + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [], + \\"removes\\": [], + \\"adds\\": [ + { + \\"parentId\\": 36, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"img\\", + \\"attributes\\": { + \\"src\\": \\"blob:http://localhost:3030/...\\", + \\"rr_dataURL\\": \\"data:image/png;base64,...\\" + }, + \\"childNodes\\": [], + \\"rootId\\": 21, + \\"id\\": 41 + } + } + ] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [ + { + \\"id\\": 41, + \\"attributes\\": { + \\"crossorigin\\": \\"anonymous\\" + } + } + ], + \\"removes\\": [], + \\"adds\\": [] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [ + { + \\"id\\": 41, + \\"attributes\\": { + \\"crossorigin\\": null + } + } + ], + \\"removes\\": [], + \\"adds\\": [] + } + } +]" +`; + +exports[`record integration tests should record images inside iframe with blob url after iframe was reloaded 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\\": \\"title\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"Frame 2\\", + \\"id\\": 11 + } + ], + \\"id\\": 10 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 12 + } + ], + \\"id\\": 4 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 13 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n frame 2\\\\n \\\\n \\", + \\"id\\": 15 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 17 + } + ], + \\"id\\": 16 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\\\n \\", + \\"id\\": 18 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 20 + } + ], + \\"id\\": 19 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n\\\\n\\", + \\"id\\": 21 + } + ], + \\"id\\": 14 + } + ], + \\"id\\": 3 + } + ], + \\"id\\": 1 + }, + \\"initialOffset\\": { + \\"left\\": 0, + \\"top\\": 0 + } + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [], + \\"removes\\": [], + \\"adds\\": [ + { + \\"parentId\\": 14, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"iframe\\", + \\"attributes\\": { + \\"id\\": \\"five\\" + }, + \\"childNodes\\": [], + \\"id\\": 22 + } + } + ] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"adds\\": [ + { + \\"parentId\\": 22, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 0, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"html\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"head\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"rootId\\": 23, + \\"id\\": 25 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"rootId\\": 23, + \\"id\\": 26 + } + ], + \\"rootId\\": 23, + \\"id\\": 24 + } + ], + \\"compatMode\\": \\"BackCompat\\", + \\"id\\": 23 + } + } + ], + \\"removes\\": [], + \\"texts\\": [], + \\"attributes\\": [], + \\"isAttachIframe\\": true + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"adds\\": [ + { + \\"parentId\\": 22, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 0, + \\"childNodes\\": [ + { + \\"type\\": 1, + \\"name\\": \\"html\\", + \\"publicId\\": \\"\\", + \\"systemId\\": \\"\\", + \\"rootId\\": 27, + \\"id\\": 28 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"html\\", + \\"attributes\\": { + \\"lang\\": \\"en\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"head\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"rootId\\": 27, + \\"id\\": 31 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"meta\\", + \\"attributes\\": { + \\"charset\\": \\"UTF-8\\" + }, + \\"childNodes\\": [], + \\"rootId\\": 27, + \\"id\\": 32 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"rootId\\": 27, + \\"id\\": 33 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"meta\\", + \\"attributes\\": { + \\"http-equiv\\": \\"X-UA-Compatible\\", + \\"content\\": \\"IE=edge\\" + }, + \\"childNodes\\": [], + \\"rootId\\": 27, + \\"id\\": 34 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"rootId\\": 27, + \\"id\\": 35 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"meta\\", + \\"attributes\\": { + \\"name\\": \\"viewport\\", + \\"content\\": \\"width=device-width, initial-scale=1.0\\" + }, + \\"childNodes\\": [], + \\"rootId\\": 27, + \\"id\\": 36 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"rootId\\": 27, + \\"id\\": 37 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"title\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"Image with blob:url\\", + \\"rootId\\": 27, + \\"id\\": 39 + } + ], + \\"rootId\\": 27, + \\"id\\": 38 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"rootId\\": 27, + \\"id\\": 40 + } + ], + \\"rootId\\": 27, + \\"id\\": 30 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"rootId\\": 27, + \\"id\\": 41 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"rootId\\": 27, + \\"id\\": 43 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"rootId\\": 27, + \\"id\\": 45 + } + ], + \\"rootId\\": 27, + \\"id\\": 44 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n\\\\n\\", + \\"rootId\\": 27, + \\"id\\": 46 + } + ], + \\"rootId\\": 27, + \\"id\\": 42 + } + ], + \\"rootId\\": 27, + \\"id\\": 29 + } + ], + \\"id\\": 27 + } + } + ], + \\"removes\\": [], + \\"texts\\": [], + \\"attributes\\": [], + \\"isAttachIframe\\": true + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [], + \\"removes\\": [], + \\"adds\\": [ + { + \\"parentId\\": 42, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"img\\", + \\"attributes\\": { + \\"src\\": \\"blob:http://localhost:3030/...\\", + \\"rr_dataURL\\": \\"data:image/png;base64,...\\" + }, + \\"childNodes\\": [], + \\"rootId\\": 27, + \\"id\\": 47 + } + } + ] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [ + { + \\"id\\": 47, + \\"attributes\\": { + \\"crossorigin\\": \\"anonymous\\" + } + } + ], + \\"removes\\": [], + \\"adds\\": [] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [ + { + \\"id\\": 47, + \\"attributes\\": { + \\"crossorigin\\": null + } + } + ], + \\"removes\\": [], + \\"adds\\": [] + } + } +]" +`; + +exports[`record integration tests should record images with blob url 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\\": \\"Image with blob:url\\", + \\"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\\": \\"script\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 19 + } + ], + \\"id\\": 18 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\", + \\"id\\": 20 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 22 + } + ], + \\"id\\": 21 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\\\n\\\\n\\", + \\"id\\": 23 + } + ], + \\"id\\": 16 + } + ], + \\"id\\": 3 + } + ], + \\"id\\": 1 + }, + \\"initialOffset\\": { + \\"left\\": 0, + \\"top\\": 0 + } + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [], + \\"removes\\": [], + \\"adds\\": [ + { + \\"parentId\\": 16, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"img\\", + \\"attributes\\": { + \\"src\\": \\"blob:http://localhost:3030/...\\", + \\"rr_dataURL\\": \\"data:image/png;base64,...\\" + }, + \\"childNodes\\": [], + \\"id\\": 24 + } + } + ] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [ + { + \\"id\\": 24, + \\"attributes\\": { + \\"crossorigin\\": \\"anonymous\\" + } + } + ], + \\"removes\\": [], + \\"adds\\": [] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [ + { + \\"id\\": 24, + \\"attributes\\": { + \\"crossorigin\\": null + } + } + ], + \\"removes\\": [], + \\"adds\\": [] + } + } +]" +`; + exports[`record integration tests should record input userTriggered values if userTriggeredOnInput is enabled 1`] = ` "[ { @@ -9979,6 +11081,315 @@ exports[`record integration tests should record input userTriggered values if us ]" `; +exports[`record integration tests should record mutations in iframes accross pages 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\\": \\"title\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"Frame 2\\", + \\"id\\": 11 + } + ], + \\"id\\": 10 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 12 + } + ], + \\"id\\": 4 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 13 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n frame 2\\\\n \\\\n \\", + \\"id\\": 15 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 17 + } + ], + \\"id\\": 16 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\\\n \\", + \\"id\\": 18 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 20 + } + ], + \\"id\\": 19 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n\\\\n\\", + \\"id\\": 21 + } + ], + \\"id\\": 14 + } + ], + \\"id\\": 3 + } + ], + \\"id\\": 1 + }, + \\"initialOffset\\": { + \\"left\\": 0, + \\"top\\": 0 + } + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [], + \\"removes\\": [], + \\"adds\\": [ + { + \\"parentId\\": 14, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"iframe\\", + \\"attributes\\": { + \\"id\\": \\"five\\" + }, + \\"childNodes\\": [], + \\"id\\": 22 + } + } + ] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"adds\\": [ + { + \\"parentId\\": 22, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 0, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"html\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"head\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"rootId\\": 23, + \\"id\\": 25 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"rootId\\": 23, + \\"id\\": 26 + } + ], + \\"rootId\\": 23, + \\"id\\": 24 + } + ], + \\"compatMode\\": \\"BackCompat\\", + \\"id\\": 23 + } + } + ], + \\"removes\\": [], + \\"texts\\": [], + \\"attributes\\": [], + \\"isAttachIframe\\": true + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"adds\\": [ + { + \\"parentId\\": 22, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 0, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"html\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"head\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"rootId\\": 27, + \\"id\\": 29 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"rootId\\": 27, + \\"id\\": 30 + } + ], + \\"rootId\\": 27, + \\"id\\": 28 + } + ], + \\"id\\": 27 + } + } + ], + \\"removes\\": [], + \\"texts\\": [], + \\"attributes\\": [], + \\"isAttachIframe\\": true + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [], + \\"removes\\": [], + \\"adds\\": [ + { + \\"parentId\\": 30, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"div\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"rootId\\": 27, + \\"id\\": 31 + } + } + ] + } + } +]" +`; + exports[`record integration tests should record nested iframes and shadow doms 1`] = ` "[ { diff --git a/packages/rrweb/test/__snapshots__/replayer.test.ts.snap b/packages/rrweb/test/__snapshots__/replayer.test.ts.snap index 45f9b9e3..66234059 100644 --- a/packages/rrweb/test/__snapshots__/replayer.test.ts.snap +++ b/packages/rrweb/test/__snapshots__/replayer.test.ts.snap @@ -81,13 +81,80 @@ file-cid-3 `; exports[`replayer can fast forward past StyleSheetRule deletion on virtual elements 1`] = ` -"file-frame-0 +"file-frame-4 - + +
+
+ +
+ + + +file-frame-5 + + + + + + + + + + + string + + + + +file-cid-0 +@charset \\"utf-8\\"; + +.rr-block { background: currentcolor; } + +noscript { display: none !important; } + +html.rrweb-paused *, html.rrweb-paused ::before, html.rrweb-paused ::after { animation-play-state: paused !important; } + + +file-cid-1 +@charset \\"utf-8\\"; + +.css-added-at-400 { padding: 1.3125rem; flex: 0 0 auto; width: 100%; } + + +file-cid-2 +@charset \\"utf-8\\"; + +.css-added-at-200-overwritten-at-3000 { opacity: 1; transform: translateX(0px); } + +.css-added-at-500-overwritten-at-3000 { border: 1px solid blue; } + + +file-cid-3 +@charset \\"utf-8\\"; + +.css-added-at-200 { position: fixed; top: 0px; right: 0px; left: 4rem; z-index: 15; flex-shrink: 0; height: 0.25rem; overflow: hidden; background-color: rgb(17, 171, 209); } + +.css-added-at-200.alt { height: 0.25rem; background-color: rgb(190, 232, 242); opacity: 0; transition: opacity 0.5s ease 0s; } + +.css-added-at-200.alt2 { padding-left: 4rem; } " `; diff --git a/packages/rrweb/test/html/assets/robot.png b/packages/rrweb/test/html/assets/robot.png new file mode 100644 index 0000000000000000000000000000000000000000..cc486cc8b70680b0dfd72828b6530e0a4e9183ad GIT binary patch literal 11004 zcmVMG57cTg)be7&LF-a&Iq(N9eY0ckxvbk2cx7z2w0ZIEpA(uQbq_V zgpyh-t=ipA&dZOK*Vj7>&F&&&7QkiMEq$Y4yw)0&#%Xl*rW^mo2S33m7?6cr==-j1 zb4saYao4dBQE8KABF%(lS=}V!j9xXiCm-}FEeIi(^#E@alGhMpjAOYUe)q4~mPILW z#xtde!O-Fqpxut7Q~;pSAfn7QkB|3Sp`tS z>qYtIXiyw$;%0_Z-%)t${gEON)tu^KuKlL5P&#I<2WS%TI(oIwAPsrNt#AccVS_n zR-H#QanvCg5<<|x>sIEQVXkCZ3rH!YKn#FmSxK5DX$AmNX-a7i03s@-;v}h7=Y>cJ z0ijb=oDgz3)xecCR|3Qs<2qh3ECGPUIS@iAwQSyvQ$*BSAsWUgrPSh_QA#LDlcd{i z10n)2N}3Cc6c}Dc(Q}1;l7KNf=NEE*h=7Qwl_rFAI&qq2T4|$^QNkGUUDt7JN+e0HYNSjE?waJChM1uU{W;Qh`1Lg z9HcQilc^sBo?|(#ZQEA8z7&Q5W&GUKtmFC{H*AYqb)zK#5NVV~4H37s7Jz8G7IpRI zwLSo@w7K@$0Xfpcmvb zsg%~OC=t>iAzEwK=fn|c-hh*WD+>|!c6sF{Q7Tg z+OqMb0|&KM8#isKH`)u;TAFl?)>3OfR{+3vbBTaTqhxTrKR0UiHlLUO3|Btz5C8*9 zSUW#IO%3dO?%DdnRI6DZ9b4u2q33#9%Y3nrDX3NF2&kl+I<7Ogp)5r5h9HDD=fKBe zcC%BTS(th8M+dIHX6NsG@>4+&e)qc%ojrd}ir5&HC2_l1%d%8y4O(v8wSfi(_3Ph2 z{)Aj{kd)9Qi(JR^Ah&w;#_^$Xq0|k#Z@%~5_jJ3_;bX_+R5Tj3c56w=EN!)h*9`Y> z=@W^d^bLY|MM3r$0dX3wU$bRJZr$vmsfC%De4!}PwB6}cD&;%xyldj@nR62-*RNhz zE|ud}yE<20v2IOR$stj%zXSOXTceYM7fBe+GItyfp44iUR$d4z2ly@f97+4B!o`Wm{7W~uz2tN53FCmZQ|frNBZ36Zxk`& z=LSg#rIb<1C|&+r0z@hymB>WWj#`ZI_uT#q|LRvhYa;WRPyZeOXstJF*~V?_$blCq z<>kKeQnO*Z-tBk2Ey(8yL25K5&}z4f<;tGhZoBrH>z0nx^H$lwWi}fsbGoHfnxxA&pYYvbJ4)&DzbIo`33b zB~=_nW807!e3FPTPgjhM&dx6isU6F*IC<-xZ##1MK)FyncIs&V>fy!3S&Lu! zMi!+E(PSbKN`_v3&Dh43L#qbMLzO~bCEph%@i%_(-+uF#e!J7^2EJb|RgxsCFV!lQ zzNvHPrB-j*dsm!v_doqqqgLY|+DIw2RwvG! zb%h_;PGxmDj^g>nIjyy2Sy2?-c+*YSU4Q-a&peSUl#NoJ?};pxDw&&^q9FI~y}QwB zFU&8*NxEv)$}krY#55C9s+(@T{r~>2e|+xz8A{y9N(VLsSt{ve^mzWe(UlOAiIg(y zrX9C_=>CuV(mnsn4V$m(E0!oDX_g9^8Dap*L*IM2RPb-V?RJAE7Y0EPAb>$kv&8qq z```WEBuO5B>`~jblPJ!Tbmi)`<-UsVx?8sG48y?p{cfkzX}A0!EEV&VF+xylER_1c z|Lwm$aq1ZAAaqN9iD{#ZL4em2aeq2ULTDzk_2ZjA_v@d1?``i`Jvi!FOca9ngedHJKfB!(GugBnUUAwK z_w3re=lr?zStk9!cReplv$JPUGj1`?ils7u86F)!di2PFr=KFO+ieJ&6dd37LW?le z$^ia+E%l!cl2RgM_G|C^jWq+~&mBH=^!UlyYOPr53miLD3K0z&&-1_a;Dha@#Sgys z1B=ZzXN*#65RKNpZ54gDzf=eVUmLw)^VWOc^`7;cHpOwY>y}&MBq>+QjvKUEEs+UmF=8Us$Zhai>*l#!b?m@2EuQ`|`}8 z2&Tt-`B^mk(`jdj#&+Cw-j?rweloC=VS(aoX z&V)oX2trERaTez0{*Qn9vBibC(a{xQm={9Ss*5|Xxqj>RZ8z?^pTwzL({?zT+t3VP$lQi16 zdfSe5JLc;1-MHgJ=!#%yeBBmJe1eCR`wR2E~(Am5I?rdjbjOa(PNblru^xWt=gKJGSlEHf1c!gj5O;5K##^U!5OW zF}hHldujhuhY!7Q_RQ(I`I&mXRw?)Gefv9396!=*)%Ne(chCLr;kKP7$@uu%iE|V4 z3$uVG$Q7IO)5ZQl&Mn{1_4W4`^CcI`Ij@*uOX`dg_ESI-AjW9l4L|vjPgjB<(Q2?* zzT>((YMtf}zVqEBxxCB- zsN||^clVV`#bU8oDoswFxiEE#v!K7Qx+CV5d6__Z5hS5$mcHZG`**C_s1XUFJ?e}> z+_n4WBPY&QYxU6M>o#mYedgSb9XmH~*~~d-JvM_uPKW`t80Cx+N(`dXMr#wr38mDv z?M#R)6+Imasg%-$kcIjAVj=JO!ImxCc3yMi-0aNcxihwFA3pHX<}Fv<|DFe`iwjrn z*m>t&Z%;ERr9wl3z#ZuCFBVEj>B7{R>u=gqES3g`h7Z25@0(xvPZLKE9em+OOZ8@D ztYWCRR4y$0s6k^{_UPaW0Ky0X00fZQP)eUabalmY-H zu`wDE5Gep7#OBO(tSl2~kWwm@avjI9Ev>aOMjKo> ze&}P4Ye!N17hm|3R$Qa>{aE*h{DcfFLpeq0OolV;eVYlu}NdJ%dJj zo)>q!QT*}|1tA0o2!pZGLTiGE2HTyOb5<?Uj|7CF~)d~U&s{@Km*|b4k0dQ zQmN5WY0LUGKX~|?M_$^`7`1InNR_3j)+QCQXO@Ua2@pyZf-oqQ=A8MSqqPA5+qQcI z70Q?~Af(QujMG%4!sa{(eb;p?#(PbZT{rLf+~@zxj%%;}o3DN;jWbFqV@!(7_dUn4 z8Kb!%%;j=P99?t$?lY&3H|mS))~@=@=l|X6wd>MUG&`M~)i)5XR7PG(nU_)uQpQ}{ zd0CSH(ik8_01S)S(Kzv zoWx0rXc%XV(H;RbNwaP@CX`yXO@ZWcq3zm)P|LDKniNarPyFtuvn)Gu_#h(szMmwK zZ86t%EZZW43=Iwho;Nf+HZVL=2&{YWxhGB2PP?rQYGo#-CKg+>oLQIlq01n((V?FU z{Qvytgdk#; ziEcO6+PJO*0NP-d$uv!+6y-25Xn+vT83OivSP1F+;r;J^|LM~w(j@EU+$`w^p6l4Q zWpmE0p~1n4iPP_S-!E+6zSU?=7&C~7P+w{vo7$hMCIxn>nq4+mrPW}0Ftja=*u#wg z2t-JY2tbHq+n(ofZW)AbxBbkMk1y5g%}$g^g$AWiTInRqx=Gp*GEPu2Xa@M1zP*X>E)#hB79lSh;dlK3{4!n`xTm^SO4bX& z22pFHwH8t(N!n<2s?{20jB!?vQk!!rWRhgP-KIeVLOAE#;;!SyQ55FFa=FAU9!G88 z^M#PcwhTgF|G>HFg@OKx)?kb=2-VusGe7=eZoGm_DI=v^&LWq5l9cmhxEPNN006N$ zV~k3vmDXtzTbvohB#AZZx!LI-{_y+H?R)OZR z94EbQ4}d9voz}; z7-}rlZHrr+F~%I*$^}8ElboBHaa~&}ZGcWrPQ3W|le`hG39o5PfNT4`{tyQM1SEu5 zmb07@01N`BL?I%gF-8gz#ZjqTQQE{&*J!P^>2|uEPMa|rN3m_!EY2C{w(Z!C!x$%& zFh-3*%9z$hNtvWsqtR-#+DR1Ijw6+{ZAu6MK&7-nBZbVQ)Jmh)#u!8}TDMy*tw!P` z?(6I0-0pUoz846os7)NLE0yx8iRr#_iBUobnV+34kLUXPOVg9n)tP4hnjruLAj{^8 zfc0h-E_&mo-a;>yq-h#=y96Lfvp7irz!-zttX{L$5BwkqIcGqK6hbK_gdiY|F-l1( zWhRxBDoZn^wU7#d-J_R;wbKP<1EY4Bn1GGB}&R}w=+LCOBq|YVS|z? zNs_^#;q&Ltu2@yDEuLh;_g$%s?*+$BO(%l@25ZW*-~G@fPC2} zdsdnWp)STF0)U8@NYQbUG?(g}kvJ7m904H|NZgHrFbKmiU&y4T2^NaPBD_8n~SMiH;v-6Zt$Ftz#snN2z zQcA0h1$B`xj+de@A^_=hI`eZE05MHP-0c!T0BCnw7mFU!+c6SK9LKQ{6CjjQ%d$AP zT-PH6j6noYO6S5LP7W9QDDS-q}gTUL~$wqv_OmWrigXU>nU7#kTG;Y4L3vu)S*na1oD zAQ2g5jdml=RN(MlI5NwIi-=HNTx>2ZdajqGnGmVc${=*wZLPI2XtXwH2qjt>DJ3Er zqdm`cY&(kMG!?$@NhvLhD{TP5vUw&9XEY20V-OJ>$MpiKwUSauk)=sotIpLHX0tSI zG}?y_ym;H5JEW9x6qicH$w}UAH(WOqQc9(Qz!Oq6n~kFf59$`DpvNTOstQ?=6nf4vq;tUZfrH*6w_w`LoP9s8XZoXVD6$<%Y zr{ZOw>}j$Sx0;>iXsKWTFUDAOZGL{P{{6puX!F%O!d$K!b)=LAi4Y==BOx+0%R(tZ zBoqke964i@5+M1UU+^48NThYM6B7VxQ{B~CYh#pUTbxq>V2t{{2Vj(xS}Q3s&ksk( zR#C>JkjIZ6Idl5d=FMA_lu;BH3I)!srKPIx7c!v?VlMQuOgOevU1}Bz1&cA)aoh1c z%`eB4Wld5_(=4eqYviIgA{q_=jDPxH{eE?B`pB~f4?h3=@bFN#+s@KRYm+2#k|c<| zL~MHP?MzAlAaGnaOH(wcq)L;x zw`m4|Bu#zxr$6`m4XREK4a>9vS%Y<3C`+ zwm56Hnpu_sKy`6p&ALrmD>P=gRzyStoG}8RmCEHq)bPk-KYsB4{nq|{&&4WFw&OdFQ!4kLJ9FyUXP&(M&bvEG zhX~zHcjfr1qlXXf*xAPz%YPP?XZ@vZqQc3`L-~I1@*L&ZKNEqk$zHRT~+EYn&m-JAS02!Lp^ER&Rd<*Q%$?C*basaC(V1Yv;s(huPIj*>xCO`-qSbSU#Sdk-@ffP z|H-dvV;qW(!<9B#Yh{Fy#^RJS&luf{JO)uKZP1`WrPW*CddHqUZ;7IAbz!kuU945B ztyW{|{P}vdy12M#IZTLTes*!Gx}^Q4=6?TcTiEzZAn?>nTFN-JYDC4^ZP5R^hc z^7r2v9a}jtFx=}!3=EH!%6(@~pIo_mL!8tNnk-sf5|7&;`V%Prb_0Eg>u$b0EoaCMMTr9dl3l$qR~nt$cr@wP(lLF z4SjE*zvBDO$3FH?e)PyAoUrBYtIX6;x1+t-dA zIr853zrWp$J=ddzW?2?>y90wGM-INUaqG_B(F zLO`LUiml(UDen{uZ@5uOO&JqXBEl`V-!(Nm zQyN%l<)qCSA;kCngD*YjI991|AW34S6%sPHF#qgRKO%&jJbrl1Eh}Yj3l6Uq0$_1_ zetPoGx7;x>Jbdo_1xji2dH6J9ld>y`VTdUl&=DBw4!~r%85Uc5d?A z`LBQDZyK${^Biv5xqRX5@q`!QtXl;bb zcHOjlaA;U+%@_qj(DYz|Ha%nP%|c$hKd1NJpg{xJt9UP7tJ7n~^g6SbtOYK8NYI$I z8@F!QyuH)ue)-E^xG;4d4ZisN(?<_Jcl(|9`9Ua@j^m`+YR=Bh?tA>l=g*$ne(R01 zbFKD5!w>zrsY&MN>aEsLf8XtU_OeO^wcNOM>%^%u`Juel>Q%+SD}g@n)?e7U?V5JC z%?ay;5CA}9bni_Mu`y;@gRj8($~%LWTf3z9by7l*EaT&xGtPTo86dPP@^$*QYpD*Ua+uw3)rGM~0{n5WJjug_1j9LB)bVMmx&i9x3;DG}#I<9A17Gu2E zdNx{@<-iFg?rHwH<%hGRt^-G;byBB8}rIzgkVQ%ZyJEmvO z42_MRK7Fdw=|oXiDCN0MF<;IVijM060Hqe^*2?h}FCExl8Z12U%O702P$jV*8((?! z#OWP7uQ_&p_VFj5%=ZOe$%(t&mx*z&CPolrl<(%&_HQYBmB0>c<|ru%p^US7y(VO4 zl+rz8MKov-kpOCgXbhz^%LL;L!APw|nx;u2mCUkKWSJ1D$TBJA>eXx4uit2KO9+vr z=~BIRVd{K0YImZ}*%PO1%iVL=+Xse6YV}$ehMY3jwo8@%a%I4Aea86t>FI~Q@t2ic z@aw<%d#yxtyX`a+2lnqfarDK-YL%kC;oZ9gw#h4KgFoSu$S9Aq?t!VNDdjHrLMQL> zz@Z*B)MgGf6cuW%l~h8c1`Q$@L`s>~s?n(B`ihf>&s66ZI?blm(hqF{&Gr4m{s!bQocbkWPEaXw~g@azPlUsxvh)Jv~`0l!}$X z$G-c`i6aNfyLSD>Uw`S;>Eo0DB`ixiNvAcwX?@??oFZLf=qp)1KN)xskP^l?LqwsH znv8mdA|Z?t$_QhGqXCEeW4V<^sVIt;7G|4HBM1w}4;;SXAMKv}{+a1h=e@vPzh$Fk z+W^2QWt<}-XMB#$C24x{)UgxC4{y8b+KubCc&?|kO4D?3Xt-D?X%AVy^Xzw@Sify+ zxm?jk6QGo`IPSJujfu&#XHOonIPa$E%;9G(&VbVT+^iQCuf1cJH%zlEG5>(P{#vBo z9>0MRMtfI102rgSkrFeYW_|zWk--tpS(aoCsHRb>V%5K)Z}#|1+=%Xe=e2eT%wfqlQl0U&vB!m#4 zfWU}l4EVn9#=hfv9Ie)T#||yRC}nKh_8r@{Ut6s%aPBb9(=<^+_(7gf!Y!Lpk_&RJ zAXHk-%uc3hylQ-{?*~dL*Y^v9l|%dY_d@#2JYR&Cgz zwPBV+2(#^4p+CRk>NSeSN~lYb=H~H*7Rp7U#Jz7eb*L)q}pkXq_wM zuiv)YbzO^FewYJ9Av4!={UFRzVcV`z3IHr_5keJ_wfdqEVq|26W!bIP(u$$AkOCqy z!jNE?%W=jg&YUi-FRi?3%w$Li>%5>(s9wJc6v@vO#WFk{WyPhAl+mtca3z9en0LFNd#gkL# zEN*YwuxaPDH_o4)ItB*-01?AnP8-7<*QTz>QYADH06;>hGzlg^mI)58^~gUziIe~$ z`cBTa9L_nRyih8gKXAIa&DCwL13xJD4=v2jZ{K;1=lfb~04Nj+QX69+6Qai{p@dSx2@nF55=wrC z@sZavGJ${;D5H!rP6+XX&}bFKDK!?Sb~#sR%{FR{s#cP5?s=i@*xa@mw^ET2!h}ep zZYNEWW}}{^DFF<_f*np8*Fvd8yY-YJg8DSiOHX3z6Xt!HouHbqhvt7b$g9Hcz!je>U qTg~2LrAnpQZV>`Mi|_sS + + + + + Frame with image + + + + + diff --git a/packages/rrweb/test/html/image-blob-url.html b/packages/rrweb/test/html/image-blob-url.html new file mode 100644 index 00000000..4dd3f608 --- /dev/null +++ b/packages/rrweb/test/html/image-blob-url.html @@ -0,0 +1,21 @@ + + + + + + + Image with blob:url + + + + + diff --git a/packages/rrweb/test/integration.test.ts b/packages/rrweb/test/integration.test.ts index 3aec801c..0da1ebdb 100644 --- a/packages/rrweb/test/integration.test.ts +++ b/packages/rrweb/test/integration.test.ts @@ -499,6 +499,57 @@ describe('record integration tests', function (this: ISuite) { assertSnapshot(snapshots); }); + it('should record images with blob url', async () => { + const page: puppeteer.Page = await browser.newPage(); + page.on('console', (msg) => console.log(msg.text())); + await page.goto(`${serverURL}/html`); + page.setContent( + getHtml.call(this, 'image-blob-url.html', { inlineImages: true }), + ); + await page.waitForResponse(`${serverURL}/html/assets/robot.png`); + await page.waitForSelector('img'); // wait for image to get added + await waitForRAF(page); // wait for image to be captured + + const snapshots = await page.evaluate('window.snapshots'); + assertSnapshot(snapshots); + }); + + it('should record images inside iframe with blob url', async () => { + const page: puppeteer.Page = await browser.newPage(); + page.on('console', (msg) => console.log(msg.text())); + await page.goto(`${serverURL}/html`); + await page.setContent( + getHtml.call(this, 'frame-image-blob-url.html', { inlineImages: true }), + ); + await page.waitForResponse(`${serverURL}/html/assets/robot.png`); + await page.waitForTimeout(50); // wait for image to get added + await waitForRAF(page); // wait for image to be captured + + const snapshots = await page.evaluate('window.snapshots'); + assertSnapshot(snapshots); + }); + + it('should record images inside iframe with blob url after iframe was reloaded', async () => { + const page: puppeteer.Page = await browser.newPage(); + page.on('console', (msg) => console.log(msg.text())); + await page.goto(`${serverURL}/html`); + await page.setContent( + getHtml.call(this, 'frame2.html', { inlineImages: true }), + ); + await page.waitForSelector('iframe'); // wait for iframe to get added + await waitForRAF(page); // wait for iframe to load + page.evaluate(() => { + const iframe = document.querySelector('iframe')!; + iframe.setAttribute('src', '/html/image-blob-url.html'); + }); + await page.waitForResponse(`${serverURL}/html/assets/robot.png`); // wait for image to get loaded + await page.waitForTimeout(50); // wait for image to get added + await waitForRAF(page); // wait for image to be captured + + const snapshots = await page.evaluate('window.snapshots'); + assertSnapshot(snapshots); + }); + it('should record shadow DOM', async () => { const page: puppeteer.Page = await browser.newPage(); await page.goto('about:blank'); @@ -589,6 +640,34 @@ describe('record integration tests', function (this: ISuite) { assertSnapshot(snapshots); }); + it('should record mutations in iframes accross pages', async () => { + const page: puppeteer.Page = await browser.newPage(); + await page.goto(`${serverURL}/html`); + page.on('console', (msg) => console.log(msg.text())); + await page.setContent(getHtml.call(this, 'frame2.html')); + + await page.waitForSelector('iframe'); // wait for iframe to get added + await waitForRAF(page); // wait for iframe to load + + page.evaluate((serverURL) => { + const iframe = document.querySelector('iframe')!; + iframe.setAttribute('src', `${serverURL}/html`); // load new page + }, serverURL); + + await page.waitForResponse(`${serverURL}/html`); // wait for iframe to load pt1 + await waitForRAF(page); // wait for iframe to load pt2 + + await page.evaluate(() => { + const iframeDocument = document.querySelector('iframe')!.contentDocument!; + const div = iframeDocument.createElement('div'); + iframeDocument.body.appendChild(div); + }); + + await waitForRAF(page); // wait for snapshot to be updated + const snapshots = await page.evaluate('window.snapshots'); + assertSnapshot(snapshots); + }); + // https://github.com/webcomponents/polyfills/tree/master/packages/shadydom it('should record shadow doms polyfilled by shadydom', async () => { const page: puppeteer.Page = await browser.newPage(); diff --git a/packages/rrweb/test/replayer.test.ts b/packages/rrweb/test/replayer.test.ts index c0c8a078..e011c532 100644 --- a/packages/rrweb/test/replayer.test.ts +++ b/packages/rrweb/test/replayer.test.ts @@ -160,11 +160,7 @@ describe('replayer', function () { ).length, ); - await assertDomSnapshot( - page, - __filename, - 'style-sheet-rule-events-play-at-1500', - ); + await assertDomSnapshot(page); }); it('should apply fast forwarded StyleSheetRules that where added', async () => { @@ -196,11 +192,7 @@ describe('replayer', function () { ).length, ); - await assertDomSnapshot( - page, - __filename, - 'style-sheet-remove-events-play-at-2500', - ); + await assertDomSnapshot(page); }); it('can restore selection', async () => { @@ -221,11 +213,14 @@ describe('replayer', function () { it('can fast forward past StyleSheetRule deletion on virtual elements', async () => { await page.evaluate(`events = ${JSON.stringify(styleSheetRuleEvents)}`); - await assertDomSnapshot( - page, - __filename, - 'style-sheet-rule-events-play-at-2500', - ); + const actionLength = await page.evaluate(` + const { Replayer } = rrweb; + const replayer = new Replayer(events); + replayer.pause(2600); + replayer['timer']['actions'].length; + `); + + await assertDomSnapshot(page); }); it('should delete fast forwarded StyleSheetRules that where removed', async () => { @@ -676,7 +671,7 @@ describe('replayer', function () { `); await page.waitForTimeout(50); - await assertDomSnapshot(page, __filename, 'ordering-events'); + await assertDomSnapshot(page); }); it('replays same timestamp events in correct order (with addAction)', async () => { @@ -690,6 +685,6 @@ describe('replayer', function () { `); await page.waitForTimeout(50); - await assertDomSnapshot(page, __filename, 'ordering-events'); + await assertDomSnapshot(page); }); }); diff --git a/packages/rrweb/test/utils.ts b/packages/rrweb/test/utils.ts index eb271598..5fd249b5 100644 --- a/packages/rrweb/test/utils.ts +++ b/packages/rrweb/test/utils.ts @@ -152,20 +152,54 @@ function stringifySnapshots(snapshots: eventWithTime[]): string { coordinatesReg.lastIndex = 0; // wow, a real wart in ECMAScript } } + + // strip blob:urls as they are different every time + console.log( + a.attributes.src, + 'src' in a.attributes && + a.attributes.src && + typeof a.attributes.src === 'string', + ); }); s.data.adds.forEach((add) => { - if ( - add.node.type === NodeType.Element && - 'style' in add.node.attributes && - typeof add.node.attributes.style === 'string' && - coordinatesReg.test(add.node.attributes.style) - ) { - add.node.attributes.style = add.node.attributes.style.replace( - coordinatesReg, - '$1: Npx', - ); + if (add.node.type === NodeType.Element) { + if ( + 'style' in add.node.attributes && + typeof add.node.attributes.style === 'string' && + coordinatesReg.test(add.node.attributes.style) + ) { + add.node.attributes.style = add.node.attributes.style.replace( + coordinatesReg, + '$1: Npx', + ); + } + coordinatesReg.lastIndex = 0; // wow, a real wart in ECMAScript + + // strip blob:urls as they are different every time + if ( + 'src' in add.node.attributes && + add.node.attributes.src && + typeof add.node.attributes.src === 'string' && + add.node.attributes.src.startsWith('blob:') + ) { + add.node.attributes.src = add.node.attributes.src.replace( + /[\w-]+$/, + '...', + ); + } + + // strip rr_dataURL as they are not consistent + if ( + 'rr_dataURL' in add.node.attributes && + add.node.attributes.rr_dataURL && + typeof add.node.attributes.rr_dataURL === 'string' + ) { + add.node.attributes.rr_dataURL = add.node.attributes.rr_dataURL.replace( + /,.+$/, + ',...', + ); + } } - coordinatesReg.lastIndex = 0; // wow, a real wart in ECMAScript }); } delete (s as Optional).timestamp; @@ -223,11 +257,7 @@ export function replaceLast(str: string, find: string, replace: string) { return str.substring(0, index) + replace + str.substring(index + find.length); } -export async function assertDomSnapshot( - page: puppeteer.Page, - filename: string, - name: string, -) { +export async function assertDomSnapshot(page: puppeteer.Page) { const cdp = await page.target().createCDPSession(); const { data } = await cdp.send('Page.captureSnapshot', { format: 'mhtml', @@ -555,6 +585,7 @@ export function generateRecordSnippet(options: recordOptions) { userTriggeredOnInput: ${options.userTriggeredOnInput}, maskTextFn: ${options.maskTextFn}, recordCanvas: ${options.recordCanvas}, + inlineImages: ${options.inlineImages}, plugins: ${options.plugins} }); `;