Fix: Trigger mouse movement & hover with mouse up/down in sync mode (#1191)
* Trigger mouse movement & hover with mouse up/down in sync mode * Trigger touchActive and mouseDown on flush
This commit is contained in:
5
.changeset/sixty-impalas-laugh.md
Normal file
5
.changeset/sixty-impalas-laugh.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
'rrweb': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Only apply touch-active styling on flush
|
||||||
5
.changeset/violet-zebras-cry.md
Normal file
5
.changeset/violet-zebras-cry.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
'rrweb': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Trigger mouse movement and hover with mouse up and mouse down events when replayer.pause(...) is called.
|
||||||
@@ -148,6 +148,7 @@ export class Replayer {
|
|||||||
|
|
||||||
private mousePos: mouseMovePos | null = null;
|
private mousePos: mouseMovePos | null = null;
|
||||||
private touchActive: boolean | null = null;
|
private touchActive: boolean | null = null;
|
||||||
|
private lastMouseDownEvent: [Node, Event] | null = null;
|
||||||
|
|
||||||
// Keep the rootNode of the last hovered element. So when hovering a new element, we can remove the last hovered element's :hover style.
|
// Keep the rootNode of the last hovered element. So when hovering a new element, we can remove the last hovered element's :hover style.
|
||||||
private lastHoveredRootNode: Document | ShadowRoot;
|
private lastHoveredRootNode: Document | ShadowRoot;
|
||||||
@@ -299,6 +300,20 @@ export class Replayer {
|
|||||||
);
|
);
|
||||||
this.mousePos = null;
|
this.mousePos = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.touchActive === true) {
|
||||||
|
this.mouse.classList.add('touch-active');
|
||||||
|
} else if (this.touchActive === false) {
|
||||||
|
this.mouse.classList.remove('touch-active');
|
||||||
|
}
|
||||||
|
this.touchActive = null;
|
||||||
|
|
||||||
|
if (this.lastMouseDownEvent) {
|
||||||
|
const [target, event] = this.lastMouseDownEvent;
|
||||||
|
target.dispatchEvent(event);
|
||||||
|
}
|
||||||
|
this.lastMouseDownEvent = null;
|
||||||
|
|
||||||
if (this.lastSelectionData) {
|
if (this.lastSelectionData) {
|
||||||
this.applySelection(this.lastSelectionData);
|
this.applySelection(this.lastSelectionData);
|
||||||
this.lastSelectionData = null;
|
this.lastSelectionData = null;
|
||||||
@@ -614,12 +629,6 @@ export class Replayer {
|
|||||||
const castFn = this.getCastFn(event, true);
|
const castFn = this.getCastFn(event, true);
|
||||||
castFn();
|
castFn();
|
||||||
}
|
}
|
||||||
if (this.touchActive === true) {
|
|
||||||
this.mouse.classList.add('touch-active');
|
|
||||||
} else if (this.touchActive === false) {
|
|
||||||
this.mouse.classList.remove('touch-active');
|
|
||||||
}
|
|
||||||
this.touchActive = null;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
private getCastFn = (event: eventWithTime, isSync = false) => {
|
private getCastFn = (event: eventWithTime, isSync = false) => {
|
||||||
@@ -1108,7 +1117,7 @@ export class Replayer {
|
|||||||
/**
|
/**
|
||||||
* Same as the situation of missing input target.
|
* Same as the situation of missing input target.
|
||||||
*/
|
*/
|
||||||
if (d.id === -1 || isSync) {
|
if (d.id === -1) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
const event = new Event(MouseInteractions[d.type].toLowerCase());
|
const event = new Event(MouseInteractions[d.type].toLowerCase());
|
||||||
@@ -1137,12 +1146,19 @@ export class Replayer {
|
|||||||
case MouseInteractions.Click:
|
case MouseInteractions.Click:
|
||||||
case MouseInteractions.TouchStart:
|
case MouseInteractions.TouchStart:
|
||||||
case MouseInteractions.TouchEnd:
|
case MouseInteractions.TouchEnd:
|
||||||
|
case MouseInteractions.MouseDown:
|
||||||
|
case MouseInteractions.MouseUp:
|
||||||
if (isSync) {
|
if (isSync) {
|
||||||
if (d.type === MouseInteractions.TouchStart) {
|
if (d.type === MouseInteractions.TouchStart) {
|
||||||
this.touchActive = true;
|
this.touchActive = true;
|
||||||
} else if (d.type === MouseInteractions.TouchEnd) {
|
} else if (d.type === MouseInteractions.TouchEnd) {
|
||||||
this.touchActive = false;
|
this.touchActive = false;
|
||||||
}
|
}
|
||||||
|
if (d.type === MouseInteractions.MouseDown) {
|
||||||
|
this.lastMouseDownEvent = [target, event];
|
||||||
|
} else if (d.type === MouseInteractions.MouseUp) {
|
||||||
|
this.lastMouseDownEvent = null;
|
||||||
|
}
|
||||||
this.mousePos = {
|
this.mousePos = {
|
||||||
x: d.x,
|
x: d.x,
|
||||||
y: d.y,
|
y: d.y,
|
||||||
@@ -1172,6 +1188,9 @@ export class Replayer {
|
|||||||
this.mouse.classList.add('touch-active');
|
this.mouse.classList.add('touch-active');
|
||||||
} else if (d.type === MouseInteractions.TouchEnd) {
|
} else if (d.type === MouseInteractions.TouchEnd) {
|
||||||
this.mouse.classList.remove('touch-active');
|
this.mouse.classList.remove('touch-active');
|
||||||
|
} else {
|
||||||
|
// for MouseDown & MouseUp also invoke default behavior
|
||||||
|
target.dispatchEvent(event);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|||||||
113
packages/rrweb/test/events/hover.ts
Normal file
113
packages/rrweb/test/events/hover.ts
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
import { IncrementalSource, MouseInteractions } from '@rrweb/types';
|
||||||
|
import type { eventWithTime } from '../../../types/src';
|
||||||
|
|
||||||
|
const events: eventWithTime[] = [
|
||||||
|
{
|
||||||
|
type: 4,
|
||||||
|
data: {
|
||||||
|
href: '',
|
||||||
|
width: 1600,
|
||||||
|
height: 900,
|
||||||
|
},
|
||||||
|
timestamp: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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: [
|
||||||
|
{
|
||||||
|
id: 101,
|
||||||
|
type: 2,
|
||||||
|
tagName: 'style',
|
||||||
|
attributes: {},
|
||||||
|
childNodes: [
|
||||||
|
{
|
||||||
|
id: 102,
|
||||||
|
type: 3,
|
||||||
|
isStyle: true,
|
||||||
|
textContent: 'div:hover { background-color: gold; }',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
id: 4,
|
||||||
|
},
|
||||||
|
{ type: 3, textContent: '\n ', id: 13 },
|
||||||
|
{
|
||||||
|
type: 2,
|
||||||
|
tagName: 'body',
|
||||||
|
attributes: {},
|
||||||
|
childNodes: [
|
||||||
|
{ type: 3, textContent: '\n ', id: 15 },
|
||||||
|
{
|
||||||
|
type: 2,
|
||||||
|
tagName: 'div',
|
||||||
|
attributes: {
|
||||||
|
style:
|
||||||
|
'border: 1px solid #000000; width: 100px; height: 100px;',
|
||||||
|
},
|
||||||
|
childNodes: [{ type: 3, textContent: '\n ', id: 17 }],
|
||||||
|
id: 16,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
id: 14,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
id: 3,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
id: 1,
|
||||||
|
},
|
||||||
|
initialOffset: { left: 0, top: 0 },
|
||||||
|
},
|
||||||
|
timestamp: 10,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 3,
|
||||||
|
data: {
|
||||||
|
source: IncrementalSource.MouseInteraction,
|
||||||
|
type: MouseInteractions.MouseDown,
|
||||||
|
id: 16,
|
||||||
|
x: 30,
|
||||||
|
y: 30,
|
||||||
|
},
|
||||||
|
timestamp: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 3,
|
||||||
|
data: {
|
||||||
|
source: IncrementalSource.MouseInteraction,
|
||||||
|
type: MouseInteractions.MouseUp,
|
||||||
|
id: 16,
|
||||||
|
x: 30,
|
||||||
|
y: 30,
|
||||||
|
},
|
||||||
|
timestamp: 150,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 3,
|
||||||
|
data: {
|
||||||
|
source: IncrementalSource.MouseInteraction,
|
||||||
|
type: MouseInteractions.Click,
|
||||||
|
id: 16,
|
||||||
|
x: 30,
|
||||||
|
y: 30,
|
||||||
|
},
|
||||||
|
timestamp: 155,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default events;
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
71
packages/rrweb/test/replay/hover.test.ts
Normal file
71
packages/rrweb/test/replay/hover.test.ts
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import { launchPuppeteer, waitForRAF } from '../utils';
|
||||||
|
import { toMatchImageSnapshot } from 'jest-image-snapshot';
|
||||||
|
import type * as puppeteer from 'puppeteer';
|
||||||
|
import events from '../events/hover';
|
||||||
|
|
||||||
|
interface ISuite {
|
||||||
|
code: string;
|
||||||
|
styles: string;
|
||||||
|
browser: puppeteer.Browser;
|
||||||
|
page: puppeteer.Page;
|
||||||
|
}
|
||||||
|
|
||||||
|
expect.extend({ toMatchImageSnapshot });
|
||||||
|
|
||||||
|
describe('replayer', function () {
|
||||||
|
jest.setTimeout(10_000);
|
||||||
|
|
||||||
|
let code: ISuite['code'];
|
||||||
|
let styles: ISuite['styles'];
|
||||||
|
let browser: ISuite['browser'];
|
||||||
|
let page: ISuite['page'];
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
browser = await launchPuppeteer({ devtools: true });
|
||||||
|
|
||||||
|
const bundlePath = path.resolve(__dirname, '../../dist/rrweb.js');
|
||||||
|
const stylePath = path.resolve(
|
||||||
|
__dirname,
|
||||||
|
'../../src/replay/styles/style.css',
|
||||||
|
);
|
||||||
|
code = fs.readFileSync(bundlePath, 'utf8');
|
||||||
|
styles = fs.readFileSync(stylePath, 'utf8');
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
page = await browser.newPage();
|
||||||
|
await page.goto('about:blank');
|
||||||
|
await page.addStyleTag({
|
||||||
|
content: styles,
|
||||||
|
});
|
||||||
|
await page.evaluate(code);
|
||||||
|
await page.evaluate(`let events = ${JSON.stringify(events)}`);
|
||||||
|
|
||||||
|
page.on('console', (msg) => console.log('PAGE LOG:', msg.text()));
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await page.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await browser.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('hover', () => {
|
||||||
|
it('should trigger hover on mouseDown', async () => {
|
||||||
|
await page.evaluate(`
|
||||||
|
const { Replayer } = rrweb;
|
||||||
|
const replayer = new Replayer(events);
|
||||||
|
replayer.pause(110); // mouseDown event is at 100
|
||||||
|
`);
|
||||||
|
|
||||||
|
await waitForRAF(page);
|
||||||
|
|
||||||
|
const image = await page.screenshot();
|
||||||
|
expect(image).toMatchImageSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user