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 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) {
|
||||
|
||||
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 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');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user