Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 2 commits into from
Feb 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/tidy-yaks-joke.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'rrweb': patch
---

Fix: outdated ':hover' styles can't be removed from iframes or shadow doms
6 changes: 5 additions & 1 deletion packages/rrweb/src/replay/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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) {
Expand Down
206 changes: 206 additions & 0 deletions packages/rrweb/test/events/iframe-shadowdom-hover.ts
Original file line number Diff line number Diff line change
@@ -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;
61 changes: 61 additions & 0 deletions packages/rrweb/test/replayer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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');
});
});