Skip to content

Commit

Permalink
[REPLAY] Add support for shadow DOM (#1787)
Browse files Browse the repository at this point in the history
* Extract dom node utils

* [REPLAY] Serialize element from shadow root

* [REPLAY] Capture mutation from inside shadow DOM

* [REPLAY] Support target in event for shadow DOM

* [REPLAY] Fix privacy setting across shadow DOM

* [REPLAY] Support change event in shadow DOM

* [REPLAY] Add e2e tests for shadow DOM

* [REPLAY] Move shadowDom support behind experimental flag

* [REPLAY] Improve shadow dom serialization

* [REPLAY] Improve shadow dom mutations capture

* [REPLAY] Improve shadow DOM e2e tests

* [REPLAY] Add isShadowHost util

* [REPLAY] Flush mutation correctly for shadow DOM

* [REPLAY] Fix type guards

* [REPLAY] Fix event target capture

* [REPLAY] Use type guards

* [REPLAY] rename flag to use snake case

* [REPLAY] Remove mutation controller

* [REPLAY] Extract shadow DOM logic in its file

* [REPLAY] run shadow dom callback on unfiltered mutation

* [REPLAY] Move 'isIE' from describe to beforeEach

* [REPLAY] Fix e2e tests

* [REPLAY] Fix typo in test label

* [REPLAY] Refactor flush mutations system

* [REPLAY] Serialize shadow root as document fragment

* [REPLAY] Update misplaced comment

Co-authored-by: Bastien Caudan <1331991+bcaudan@users.noreply.github.com>

* [REPLAY] Factorize export

* [REPLAY] Refactor shadow dom controller

* [REPLAY] Update replay event

* [REPLAY] Use shadow dom controller interface instead of callbacks

* [REPLAY] Improve e2e test for shadow Dom

* [REPLAY] Remove e2e test that duplicated

* [REPLAY] Prefer push over reassign

Co-authored-by: Benoît <benoit.zugmeyer@datadoghq.com>

* [REPLAY] Reuse existing helpers

Co-authored-by: Benoît <benoit.zugmeyer@datadoghq.com>

* [REPLAY] Update test labels

Co-authored-by: Benoît <benoit.zugmeyer@datadoghq.com>

* [REPLAY] Skip unit tests on IE because of shadow DOM

* [REPLAY] Fix  typo

Co-authored-by: Bastien Caudan <1331991+bcaudan@users.noreply.github.com>

* [REPLAY] Update labels

* [REPLAY] Rename file to match function inside

* [REPLAY] Inlines function

* [REPLAY] Rename types in shadow root controller

Co-authored-by: Bastien Caudan <1331991+bcaudan@users.noreply.github.com>
Co-authored-by: Benoît <benoit.zugmeyer@datadoghq.com>
  • Loading branch information
3 people committed Jan 3, 2023
1 parent 1dcc3a9 commit 9f4cf2f
Show file tree
Hide file tree
Showing 22 changed files with 1,358 additions and 204 deletions.
166 changes: 166 additions & 0 deletions packages/rum-core/src/browser/htmlDomUtils.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
import { isIE } from '@datadog/browser-core'
import {
isTextNode,
isCommentNode,
isElementNode,
isNodeShadowRoot,
getChildNodes,
getParentNode,
isNodeShadowHost,
} from './htmlDomUtils'

describe('isTextNode', () => {
const parameters: Array<[Node, boolean]> = [
[document.createTextNode('hello'), true],
[document.createElement('div'), false],
[document.body, false],
[document.createComment('hello'), false],
['hello' as unknown as Node, false],
]

parameters.forEach(([element, result]) => {
it(`should return ${String(result)} for "${String(element)}"`, () => {
expect(isTextNode(element)).toBe(result)
})
})
})

describe('isCommentNode', () => {
const parameters: Array<[Node, boolean]> = [
[document.createComment('hello'), true],
[document.createTextNode('hello'), false],
[document.createElement('div'), false],
[document.body, false],
['hello' as unknown as Node, false],
]

parameters.forEach(([element, result]) => {
it(`should return ${String(result)} for "${String(element)}"`, () => {
expect(isCommentNode(element)).toBe(result)
})
})
})

describe('isElementNode', () => {
const parameters: Array<[Node, boolean]> = [
[document.createElement('div'), true],
[document.body, true],
[document.createTextNode('hello'), false],
[document.createComment('hello'), false],
['hello' as unknown as Node, false],
]

parameters.forEach(([element, result]) => {
it(`should return ${String(result)} for "${String(element)}"`, () => {
expect(isElementNode(element)).toBe(result)
})
})
})

describe('isShadowRoot', () => {
if (isIE()) {
return
}

const parent = document.createElement('div')
parent.attachShadow({ mode: 'open' })
const parameters: Array<[Node, boolean]> = [
[parent.shadowRoot!, true],
[parent, false],
[document.body, false],
[document.createTextNode('hello'), false],
[document.createComment('hello'), false],
]

parameters.forEach(([element, result]) => {
it(`should return ${String(result)} for "${String(element)}"`, () => {
expect(isNodeShadowRoot(element)).toBe(result)
})
})
})

describe('isShadowHost', () => {
if (isIE()) {
return
}
const host = document.createElement('div')
host.attachShadow({ mode: 'open' })
const parameters: Array<[Node, boolean]> = [
[host, true],
[host.shadowRoot!, false],
[document.body, false],
[document.createTextNode('hello'), false],
[document.createComment('hello'), false],
]

parameters.forEach(([element, result]) => {
it(`should return ${String(result)} for "${String(element)}"`, () => {
expect(isNodeShadowHost(element)).toBe(result)
})
})
})

describe('getChildNodes', () => {
it('should return the direct children for a normal node', () => {
if (isIE()) {
pending('IE not supported')
}

const children: Node[] = [
document.createTextNode('toto'),
document.createElement('span'),
document.createComment('oops'),
]
const container = document.createElement('div')
children.forEach((node) => {
container.appendChild(node)
})

expect(getChildNodes(container).length).toBe(children.length)
})

it('should return the children of the shadow root for a node that is a host', () => {
if (isIE()) {
pending('IE not supported')
}

const children: Node[] = [
document.createTextNode('toto'),
document.createElement('span'),
document.createComment('oops'),
]
const container = document.createElement('div')
container.attachShadow({ mode: 'open' })

children.forEach((node) => {
container.shadowRoot!.appendChild(node)
})

expect(getChildNodes(container).length).toBe(children.length)
})
})

describe('getParentNode', () => {
if (isIE()) {
return
}

const orphanDiv = document.createElement('div')
const parentWithShadowRoot = document.createElement('div')
const shadowRoot = parentWithShadowRoot.attachShadow({ mode: 'open' })

const parentWithoutShadowRoot = document.createElement('div')
const child = document.createElement('span')
parentWithoutShadowRoot.appendChild(child)

const parameters: Array<[string, Node, Node | null]> = [
['return null if without parent', orphanDiv, null],
['return the host for a shadow root', shadowRoot, parentWithShadowRoot],
['return the parent for normal child', child, parentWithoutShadowRoot],
]
parameters.forEach(([label, element, result]) => {
it(`should ${label}`, () => {
expect(getParentNode(element)).toBe(result)
})
})
})
31 changes: 31 additions & 0 deletions packages/rum-core/src/browser/htmlDomUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
export function isTextNode(node: Node): node is Text {
return node.nodeType === Node.TEXT_NODE
}

export function isCommentNode(node: Node): node is Comment {
return node.nodeType === Node.COMMENT_NODE
}

export function isElementNode(node: Node): node is Element {
return node.nodeType === Node.ELEMENT_NODE
}

export function isNodeShadowHost(node: Node): node is Element & { shadowRoot: ShadowRoot } {
return isElementNode(node) && node.shadowRoot !== null
}

export function isNodeShadowRoot(node: Node): node is ShadowRoot {
const shadowRoot = node as ShadowRoot
return !!shadowRoot.host && isElementNode(shadowRoot.host)
}

export function getChildNodes(node: Node) {
return isNodeShadowHost(node) ? node.shadowRoot.childNodes : node.childNodes
}

/**
* Return `host` in case if the current node is a shadow root otherwise will return the `parentNode`
*/
export function getParentNode(node: Node): Node | null {
return isNodeShadowRoot(node) ? node.host : node.parentNode
}
9 changes: 1 addition & 8 deletions packages/rum-core/src/domain/tracing/getDocumentTraceId.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { TimeStamp } from '@datadog/browser-core'
import { dateNow, findCommaSeparatedValue, ONE_MINUTE } from '@datadog/browser-core'
import { isCommentNode, isTextNode } from '../../browser/htmlDomUtils'

interface DocumentTraceData {
traceId: string
Expand Down Expand Up @@ -87,11 +88,3 @@ function getTraceCommentFromNode(node: Node | null) {
}
}
}

function isCommentNode(node: Node): node is Comment {
return node.nodeName === '#comment'
}

function isTextNode(node: Node): node is Text {
return node.nodeName === '#text'
}
1 change: 1 addition & 0 deletions packages/rum-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,4 @@ export { initViewportObservable, getViewportDimension } from './browser/viewport
export { RumInitConfiguration, RumConfiguration } from './domain/configuration'
export { DEFAULT_PROGRAMMATIC_ACTION_NAME_ATTRIBUTE } from './domain/rumEventsCollection/action/getActionNameFromElement'
export { STABLE_ATTRIBUTES } from './domain/rumEventsCollection/action/getSelectorFromElement'
export * from './browser/htmlDomUtils'
Loading

0 comments on commit 9f4cf2f

Please sign in to comment.