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:
5
.changeset/tidy-yaks-joke.md
Normal file
5
.changeset/tidy-yaks-joke.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
'rrweb': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Fix: outdated ':hover' styles can't be removed from iframes or shadow doms
|
||||||
@@ -149,6 +149,9 @@ export class Replayer {
|
|||||||
private mousePos: mouseMovePos | null = null;
|
private mousePos: mouseMovePos | null = null;
|
||||||
private touchActive: boolean | 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.
|
// In the fast-forward mode, only the last selection data needs to be applied.
|
||||||
private lastSelectionData: selectionData | null = null;
|
private lastSelectionData: selectionData | null = null;
|
||||||
|
|
||||||
@@ -2091,11 +2094,12 @@ export class Replayer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private hoverElements(el: Element) {
|
private hoverElements(el: Element) {
|
||||||
this.iframe.contentDocument
|
(this.lastHoveredRootNode || this.iframe.contentDocument)
|
||||||
?.querySelectorAll('.\\:hover')
|
?.querySelectorAll('.\\:hover')
|
||||||
.forEach((hoveredEl) => {
|
.forEach((hoveredEl) => {
|
||||||
hoveredEl.classList.remove(':hover');
|
hoveredEl.classList.remove(':hover');
|
||||||
});
|
});
|
||||||
|
this.lastHoveredRootNode = el.getRootNode() as Document | ShadowRoot;
|
||||||
let currentEl: Element | null = el;
|
let currentEl: Element | null = el;
|
||||||
while (currentEl) {
|
while (currentEl) {
|
||||||
if (currentEl.classList) {
|
if (currentEl.classList) {
|
||||||
|
|||||||
206
packages/rrweb/test/events/iframe-shadowdom-hover.ts
Normal file
206
packages/rrweb/test/events/iframe-shadowdom-hover.ts
Normal 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;
|
||||||
@@ -21,6 +21,7 @@ import canvasInIframe from './events/canvas-in-iframe';
|
|||||||
import adoptedStyleSheet from './events/adopted-style-sheet';
|
import adoptedStyleSheet from './events/adopted-style-sheet';
|
||||||
import adoptedStyleSheetModification from './events/adopted-style-sheet-modification';
|
import adoptedStyleSheetModification from './events/adopted-style-sheet-modification';
|
||||||
import documentReplacementEvents from './events/document-replacement';
|
import documentReplacementEvents from './events/document-replacement';
|
||||||
|
import hoverInIframeShadowDom from './events/iframe-shadowdom-hover';
|
||||||
import { ReplayerEvents } from '@rrweb/types';
|
import { ReplayerEvents } from '@rrweb/types';
|
||||||
|
|
||||||
interface ISuite {
|
interface ISuite {
|
||||||
@@ -1015,4 +1016,64 @@ describe('replayer', function () {
|
|||||||
// No errors should be thrown.
|
// No errors should be thrown.
|
||||||
expect(errorThrown).not.toHaveBeenCalled();
|
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');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user