From d4ae75860693a5df66cffed08503a9f32ca3592a Mon Sep 17 00:00:00 2001 From: Yun Feng Date: Wed, 10 Aug 2022 22:15:01 +1000 Subject: [PATCH] fix: errors when fast-forward selection events --- packages/rrweb/src/replay/index.ts | 72 ++++++++++------ packages/rrweb/test/events/selection.ts | 109 +++++++++++------------- packages/rrweb/test/replayer.test.ts | 22 ++++- 3 files changed, 112 insertions(+), 91 deletions(-) diff --git a/packages/rrweb/src/replay/index.ts b/packages/rrweb/src/replay/index.ts index df76ca9eeb..2cb4b91d72 100644 --- a/packages/rrweb/src/replay/index.ts +++ b/packages/rrweb/src/replay/index.ts @@ -59,6 +59,7 @@ import { canvasMutationCommand, canvasMutationParam, canvasEventWithTime, + selectionData, } from '../types'; import { polyfill, @@ -142,6 +143,9 @@ export class Replayer { private mousePos: mouseMovePos | null = null; private touchActive: boolean | null = null; + // In the fast-forward mode, only the last selection data needs to be applied. + private lastSelectionData: selectionData | null = null; + constructor( events: Array, config?: Partial, @@ -239,8 +243,12 @@ export class Replayer { true, this.mousePos.debugData, ); + this.mousePos = null; + } + if (this.lastSelectionData) { + this.applySelection(this.lastSelectionData); + this.lastSelectionData = null; } - this.mousePos = null; }); this.emitter.on(ReplayerEvents.PlayBack, () => { this.firstFullSnapshot = null; @@ -1321,32 +1329,11 @@ export class Replayer { break; } case IncrementalSource.Selection: { - const selectionSet = new Set(); - const ranges = d.ranges.map( - ({ start, startOffset, end, endOffset }) => { - const startContainer = this.mirror.getNode(start); - const endContainer = this.mirror.getNode(end); - - if (!startContainer || !endContainer) return; - - const result = new Range(); - - result.setStart(startContainer, startOffset); - result.setEnd(endContainer, endOffset); - const doc = startContainer.ownerDocument; - const selection = doc?.getSelection(); - selection && selectionSet.add(selection); - - return { - range: result, - selection, - }; - }, - ); - - selectionSet.forEach((s) => s.removeAllRanges()); - - ranges.forEach((r) => r && r.selection?.addRange(r.range)); + if (isSync) { + this.lastSelectionData = d; + break; + } + this.applySelection(d); break; } default: @@ -1759,6 +1746,37 @@ export class Replayer { } } + private applySelection(d: selectionData) { + try { + const selectionSet = new Set(); + const ranges = d.ranges.map(({ start, startOffset, end, endOffset }) => { + const startContainer = this.mirror.getNode(start); + const endContainer = this.mirror.getNode(end); + + if (!startContainer || !endContainer) return; + + const result = new Range(); + + result.setStart(startContainer, startOffset); + result.setEnd(endContainer, endOffset); + const doc = startContainer.ownerDocument; + const selection = doc?.getSelection(); + selection && selectionSet.add(selection); + + return { + range: result, + selection, + }; + }); + + selectionSet.forEach((s) => s.removeAllRanges()); + + ranges.forEach((r) => r && r.selection?.addRange(r.range)); + } catch (error) { + // for safe + } + } + private legacy_resolveMissingNode( map: missingNodeMap, parent: Node | RRNode, diff --git a/packages/rrweb/test/events/selection.ts b/packages/rrweb/test/events/selection.ts index 152350b0d4..146cbc60ea 100644 --- a/packages/rrweb/test/events/selection.ts +++ b/packages/rrweb/test/events/selection.ts @@ -45,11 +45,6 @@ const events: eventWithTime[] = [ tagName: 'head', attributes: {}, childNodes: [ - { - type: 3, - textContent: '\\\\n ', - id: 5, - }, { type: 2, tagName: 'meta', @@ -59,49 +54,27 @@ const events: eventWithTime[] = [ childNodes: [], id: 6, }, - { - type: 3, - textContent: '\\\\n ', - id: 7, - }, ], id: 4, }, - { - type: 3, - textContent: '\\\\n ', - id: 8, - }, { type: 2, tagName: 'body', attributes: {}, childNodes: [ - { - type: 3, - textContent: '\\\\n Lorem, ipsum\\\\n ', - id: 10, - }, { type: 2, tagName: 'span', attributes: { id: 'startNode', }, - childNodes: [ - { - type: 3, - textContent: - '\\\\n Lorem ipsum dolor sit amet consectetur adipisicing elit. Dolores culpa\\\\n corporis voluptas odit nobis recusandae inventore, magni praesentium\\\\n maiores perferendis quaerat excepturi officia minus velit voluptate\\\\n placeat minima? Nesciunt, eum!\\\\n ', - id: 12, - }, - ], + childNodes: [], id: 11, }, { type: 3, textContent: - '\\\\n dolor sit amet consectetur adipisicing elit. Ad repellendus quas hic\\\\n deleniti, delectus consequatur voluptas aliquam dolore voluptates repellat\\\\n perferendis aperiam saepe maxime officia rem corporis beatae, assumenda\\\\n doloribus.\\\\n ', + 'some text between the start node and the end node', id: 13, }, { @@ -110,39 +83,9 @@ const events: eventWithTime[] = [ attributes: { id: 'endNode', }, - childNodes: [ - { - type: 3, - textContent: - '\\\\n Lorem ipsum dolor sit amet consectetur adipisicing elit. Recusandae\\\\n explicabo omnis dolores magni, ea doloribus possimus debitis reiciendis\\\\n distinctio perferendis nihil ipsum officiis pariatur laboriosam quas,\\\\n corrupti vero vitae minus.\\\\n ', - id: 15, - }, - ], + childNodes: [], id: 14, }, - { - type: 3, - textContent: '\\\\n \\\\n ', - id: 16, - }, - { - type: 2, - tagName: 'script', - attributes: {}, - childNodes: [ - { - type: 3, - textContent: 'SCRIPT_PLACEHOLDER', - id: 18, - }, - ], - id: 17, - }, - { - type: 3, - textContent: '\\\\n \\\\n \\\\n\\\\n', - id: 19, - }, ], id: 9, }, @@ -157,8 +100,54 @@ const events: eventWithTime[] = [ top: 0, }, }, + timestamp: now + 250, + }, + // add selection targets through incremental mutation + { + type: EventType.IncrementalSnapshot, + data: { + source: IncrementalSource.Mutation, + texts: [], + attributes: [], + removes: [], + adds: [ + { + parentId: 11, + nextId: null, + node: { + type: 3, + textContent: 'This the text of the start node.', + id: 12, + }, + }, + { + parentId: 14, + nextId: null, + node: { + type: 3, + textContent: 'This the text of the end node.', + id: 15, + }, + }, + ], + }, timestamp: now + 300, }, + { + type: EventType.IncrementalSnapshot, + data: { + source: IncrementalSource.Selection, + ranges: [ + { + start: 12, + startOffset: 5, + end: 15, + endOffset: 15, + }, + ], + }, + timestamp: now + 350, + }, { type: EventType.IncrementalSnapshot, data: { diff --git a/packages/rrweb/test/replayer.test.ts b/packages/rrweb/test/replayer.test.ts index e011c532dd..bad20d777e 100644 --- a/packages/rrweb/test/replayer.test.ts +++ b/packages/rrweb/test/replayer.test.ts @@ -195,14 +195,28 @@ describe('replayer', function () { await assertDomSnapshot(page); }); - it('can restore selection', async () => { + it('can fast forward selection events', async () => { await page.evaluate(`events = ${JSON.stringify(selectionEvents)}`); - const [startOffset, endOffset] = (await page.evaluate(` + + /** check the first selection event */ + let [startOffset, endOffset] = (await page.evaluate(` const { Replayer } = rrweb; const replayer = new Replayer(events); - replayer.pause(1500); - const range = replayer.iframe.contentDocument.getSelection().getRangeAt(0); + replayer.pause(360); + var range = replayer.iframe.contentDocument.getSelection().getRangeAt(0); + [range.startOffset, range.endOffset]; + `)) as [startOffset: number, endOffset: number]; + + expect(startOffset).toEqual(5); + expect(endOffset).toEqual(15); + + await page.evaluate('replayer.play(0);'); + await waitForRAF(page); + /** check the second selection event */ + [startOffset, endOffset] = (await page.evaluate(` + replayer.pause(410); + var range = replayer.iframe.contentDocument.getSelection().getRangeAt(0); [range.startOffset, range.endOffset]; `)) as [startOffset: number, endOffset: number];