diff --git a/src/replay/index.ts b/src/replay/index.ts index 26b6c892..ce1722a0 100644 --- a/src/replay/index.ts +++ b/src/replay/index.ts @@ -88,6 +88,7 @@ export class Replayer { * @param timeOffset number */ public play(timeOffset = 0) { + this.timer.clear(); this.baselineTime = this.events[0].timestamp + timeOffset; const actions = new Array(); for (const event of this.events) { @@ -113,7 +114,7 @@ export class Replayer { const actions = new Array(); for (const event of this.events) { if ( - event.timestamp < this.lastPlayedEvent.timestamp || + event.timestamp <= this.lastPlayedEvent.timestamp || event === this.lastPlayedEvent ) { continue; diff --git a/test/replayer.test.ts b/test/replayer.test.ts new file mode 100644 index 00000000..a899352c --- /dev/null +++ b/test/replayer.test.ts @@ -0,0 +1,202 @@ +/* tslint:disable no-string-literal */ + +import * as fs from 'fs'; +import * as path from 'path'; +import * as puppeteer from 'puppeteer'; +import { expect } from 'chai'; +import { + EventType, + eventWithTime, + IncrementalSource, + MouseInteractions, +} from '../src/types'; +import { Replayer } from '../src'; + +const now = Date.now(); + +const events: eventWithTime[] = [ + { + type: EventType.DomContentLoaded, + data: {}, + timestamp: now, + }, + { + type: EventType.Load, + data: {}, + timestamp: now + 1000, + }, + { + type: EventType.Meta, + data: { + href: 'http://localhost', + width: 1000, + height: 800, + }, + timestamp: now + 1000, + }, + { + type: EventType.FullSnapshot, + data: { + node: { + type: 0, + childNodes: [ + { + type: 2, + tagName: 'html', + attributes: {}, + childNodes: [ + { + type: 2, + tagName: 'head', + attributes: {}, + childNodes: [], + id: 3, + }, + { + type: 2, + tagName: 'body', + attributes: {}, + childNodes: [], + id: 4, + }, + ], + id: 2, + }, + ], + id: 1, + }, + initialOffset: { + top: 0, + left: 0, + }, + }, + timestamp: now + 1000, + }, + { + type: EventType.IncrementalSnapshot, + data: { + source: IncrementalSource.MouseInteraction, + type: MouseInteractions.Click, + id: 1, + x: 0, + y: 0, + }, + timestamp: now + 2000, + }, + { + type: EventType.IncrementalSnapshot, + data: { + source: IncrementalSource.MouseInteraction, + type: MouseInteractions.Click, + id: 1, + x: 0, + y: 0, + }, + timestamp: now + 3000, + }, + { + type: EventType.IncrementalSnapshot, + data: { + source: IncrementalSource.MouseInteraction, + type: MouseInteractions.Click, + id: 1, + x: 0, + y: 0, + }, + timestamp: now + 4000, + }, +]; + +interface IWindow extends Window { + rrweb: { + Replayer: typeof Replayer; + }; +} + +describe('replayer', () => { + before(async () => { + this.browser = await puppeteer.launch({ + headless: false, + args: ['--no-sandbox'], + }); + + const bundlePath = path.resolve(__dirname, '../dist/rrweb.min.js'); + this.code = fs.readFileSync(bundlePath, 'utf8'); + }); + + beforeEach(async () => { + const page: puppeteer.Page = await this.browser.newPage(); + await page.goto('about:blank'); + await page.evaluate(this.code); + await page.evaluate(`const events = ${JSON.stringify(events)}`); + this.page = page; + + page.on('console', msg => console.log('PAGE LOG:', msg.text())); + }); + + afterEach(async () => { + await this.page.close(); + }); + + after(async () => { + await this.browser.close(); + }); + + it('can get meta data', async () => { + const meta = await this.page.evaluate(() => { + const { Replayer } = (window as IWindow).rrweb; + const replayer = new Replayer(events); + return replayer.getMetaData(); + }); + expect(meta).to.deep.equal({ + totalTime: events[events.length - 1].timestamp - events[0].timestamp, + }); + }); + + it('will start actions when play', async () => { + const actionLength = await this.page.evaluate(() => { + const { Replayer } = (window as IWindow).rrweb; + const replayer = new Replayer(events); + replayer.play(); + return replayer['timer']['actions'].length; + }); + expect(actionLength).to.equal(events.length); + }); + + it('will clean actions when pause', async () => { + const actionLength = await this.page.evaluate(() => { + const { Replayer } = (window as IWindow).rrweb; + const replayer = new Replayer(events); + replayer.play(); + replayer.pause(); + return replayer['timer']['actions'].length; + }); + expect(actionLength).to.equal(0); + }); + + it('can play at any time offset', async () => { + const actionLength = await this.page.evaluate(() => { + const { Replayer } = (window as IWindow).rrweb; + const replayer = new Replayer(events); + replayer.play(1500); + return replayer['timer']['actions'].length; + }); + expect(actionLength).to.equal( + events.filter(e => e.timestamp - events[0].timestamp >= 1500).length, + ); + }); + + it('can resume at any time offset', async () => { + const actionLength = await this.page.evaluate(() => { + const { Replayer } = (window as IWindow).rrweb; + const replayer = new Replayer(events); + replayer.play(1500); + replayer.pause(); + replayer.resume(1500); + return replayer['timer']['actions'].length; + }); + expect(actionLength).to.equal( + events.filter(e => e.timestamp - events[0].timestamp >= 1500).length, + ); + }); +});