fix: outdated ':hover' styles can't be removed from iframes or shadow doms (#1121)

* fix :hover class can't be removed in iframes

* add test case and change log
This commit is contained in:
Yun Feng
2023-02-12 23:25:21 +11:00
committed by GitHub
parent 8d209a62f3
commit 502d15df9f
4 changed files with 277 additions and 1 deletions

View File

@@ -0,0 +1,5 @@
---
'rrweb': patch
---
Fix: outdated ':hover' styles can't be removed from iframes or shadow doms

View File

@@ -149,6 +149,9 @@ export class Replayer {
private mousePos: mouseMovePos | null = null;
private touchActive: boolean | 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.
private lastHoveredRootNode: Document | ShadowRoot;
// In the fast-forward mode, only the last selection data needs to be applied.
private lastSelectionData: selectionData | null = null;
@@ -2091,11 +2094,12 @@ export class Replayer {
}
private hoverElements(el: Element) {
this.iframe.contentDocument
(this.lastHoveredRootNode || this.iframe.contentDocument)
?.querySelectorAll('.\\:hover')
.forEach((hoveredEl) => {
hoveredEl.classList.remove(':hover');
});
this.lastHoveredRootNode = el.getRootNode() as Document | ShadowRoot;
let currentEl: Element | null = el;
while (currentEl) {
if (currentEl.classList) {

View File

@@ -0,0 +1,206 @@
import { EventType, IncrementalSource } from '@rrweb/types';
import type { eventWithTime } from '@rrweb/types';
const now = Date.now();
const events: eventWithTime[] = [
{
type: EventType.DomContentLoaded,
data: {},
timestamp: now,
},
{
type: EventType.Load,
data: {},
timestamp: now + 100,
},
{
type: EventType.Meta,
data: {
href: 'http://localhost',
width: 1200,
height: 500,
},
timestamp: now + 100,
},
{
type: EventType.FullSnapshot,
data: {
node: {
id: 1,
type: 0,
childNodes: [
{ id: 2, name: 'html', type: 1, publicId: '', systemId: '' },
{
id: 3,
type: 2,
tagName: 'html',
attributes: { lang: 'en' },
childNodes: [
{
id: 4,
type: 2,
tagName: 'head',
attributes: {},
childNodes: [],
},
{
id: 5,
type: 2,
tagName: 'body',
attributes: {},
childNodes: [
{
id: 6,
type: 2,
tagName: 'iframe',
attributes: {},
childNodes: [],
},
],
},
],
},
],
},
initialOffset: { top: 0, left: 0 },
},
timestamp: now + 200,
},
// add iframe
{
type: EventType.IncrementalSnapshot,
data: {
source: IncrementalSource.Mutation,
adds: [
{
parentId: 6,
nextId: null,
node: {
type: 0,
childNodes: [
{
type: 1,
name: 'html',
publicId: '',
systemId: '',
rootId: 7,
id: 8,
},
{
type: 2,
tagName: 'html',
attributes: { lang: 'en' },
childNodes: [
{
type: 2,
tagName: 'head',
attributes: {},
childNodes: [],
rootId: 7,
id: 10,
},
{
type: 2,
tagName: 'body',
attributes: {},
childNodes: [
{
type: 2,
tagName: 'div',
attributes: {},
childNodes: [
{
type: 2,
tagName: 'div',
attributes: {},
childNodes: [],
rootId: 7,
id: 13,
isShadow: true,
},
],
isShadowHost: true,
rootId: 7,
id: 12,
},
{
type: 2,
tagName: 'span',
attributes: {},
childNodes: [],
rootId: 7,
id: 14,
},
{ type: 3, textContent: '\t\n', rootId: 7, id: 15 },
],
rootId: 7,
id: 11,
},
],
rootId: 7,
id: 9,
},
],
id: 7,
},
},
],
removes: [],
texts: [],
attributes: [],
isAttachIframe: true,
},
timestamp: now + 500,
},
// hover element in iframe
{
type: EventType.IncrementalSnapshot,
data: {
source: IncrementalSource.MouseMove,
positions: [
{
x: 0,
y: 0,
id: 14,
timeOffset: 0,
},
],
},
timestamp: now + 500,
},
// hover element in shadow dom
{
type: EventType.IncrementalSnapshot,
data: {
source: IncrementalSource.MouseMove,
positions: [
{
x: 0,
y: 0,
id: 13,
timeOffset: 0,
},
],
},
timestamp: now + 1000,
},
// hover element in iframe again
{
type: EventType.IncrementalSnapshot,
data: {
source: IncrementalSource.MouseMove,
positions: [
{
x: 0,
y: 0,
id: 14,
timeOffset: 0,
},
],
},
timestamp: now + 1500,
},
];
export default events;

View File

@@ -21,6 +21,7 @@ import canvasInIframe from './events/canvas-in-iframe';
import adoptedStyleSheet from './events/adopted-style-sheet';
import adoptedStyleSheetModification from './events/adopted-style-sheet-modification';
import documentReplacementEvents from './events/document-replacement';
import hoverInIframeShadowDom from './events/iframe-shadowdom-hover';
import { ReplayerEvents } from '@rrweb/types';
interface ISuite {
@@ -1015,4 +1016,64 @@ describe('replayer', function () {
// No errors should be thrown.
expect(errorThrown).not.toHaveBeenCalled();
});
it('should remove outdated hover styles in iframes and shadow doms', async () => {
await page.evaluate(`events = ${JSON.stringify(hoverInIframeShadowDom)}`);
await page.evaluate(`
const { Replayer } = rrweb;
const replayer = new Replayer(events);
replayer.pause(550);
`);
const replayerIframe = await page.$('iframe');
const contentDocument = await replayerIframe!.contentFrame()!;
const iframe = await contentDocument!.$('iframe');
expect(iframe).not.toBeNull();
const docInIFrame = await iframe?.contentFrame();
expect(docInIFrame).not.toBeNull();
// hover element in iframe at 500ms
expect(
await docInIFrame?.evaluate(
() => document.querySelector('span')?.className,
),
).toBe(':hover');
// At this time, there should be no class name in shadow dom
expect(
await docInIFrame?.evaluate(() => {
const shadowRoot = document.querySelector('div')?.shadowRoot;
return (shadowRoot?.childNodes[0] as HTMLElement).className;
}),
).toBe('');
// hover element in shadow dom at 1000ms
await page.evaluate('replayer.pause(1050);');
// :hover style should be removed from iframe
expect(
await docInIFrame?.evaluate(
() => document.querySelector('span')?.className,
),
).toBe('');
expect(
await docInIFrame?.evaluate(() => {
const shadowRoot = document.querySelector('div')?.shadowRoot;
return (shadowRoot?.childNodes[0] as HTMLElement).className;
}),
).toBe(':hover');
// hover element in iframe at 1500ms again
await page.evaluate('replayer.pause(1550);');
// hover style should be removed from shadow dom
expect(
await docInIFrame?.evaluate(() => {
const shadowRoot = document.querySelector('div')?.shadowRoot;
return (shadowRoot?.childNodes[0] as HTMLElement).className;
}),
).toBe('');
expect(
await docInIFrame?.evaluate(
() => document.querySelector('span')?.className,
),
).toBe(':hover');
});
});