Skip to content
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
37 changes: 37 additions & 0 deletions packages/rum-core/src/domain/action/trackClickActions.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { PAGE_ACTIVITY_VALIDATION_DELAY } from '../waitPageActivityEnd'
import type { RumConfiguration } from '../configuration'
import type { BrowserWindow } from '../privacy'
import type { RumMutationRecord } from '../../browser/domMutationObservable'
import { SHADOW_DOM_MARKER } from '../getSelectorFromElement'
import type { ClickAction } from './trackClickActions'
import { finalizeClicks, trackClickActions } from './trackClickActions'
import { MAX_DURATION_BETWEEN_CLICKS } from './clickChain'
Expand Down Expand Up @@ -660,6 +661,42 @@ describe('trackClickActions', () => {
expect(events.length).toBe(1)
expect(events[0].name).toBe('Shadow Button')
})

it('with betaTrackActionsInShadowDom, gets selector with shadow marker from composedPath', () => {
startClickActionsTracking({ betaTrackActionsInShadowDom: true })

emulateClick({
target: shadowHost,
activity: {},
eventProperty: {
composed: true,
composedPath: () => [shadowButton, shadowHost.shadowRoot, shadowHost, document.body, document],
},
})
clock.tick(EXPIRE_DELAY)

expect(events.length).toBe(1)
expect(events[0].target?.selector).toContain(SHADOW_DOM_MARKER)
expect(events[0].target?.selector).toContain('BUTTON')
})

it('without betaTrackActionsInShadowDom, selector uses shadow host', () => {
startClickActionsTracking({ betaTrackActionsInShadowDom: false })

emulateClick({
target: shadowHost,
activity: {},
eventProperty: {
composed: true,
composedPath: () => [shadowButton, shadowHost.shadowRoot, shadowHost, document.body, document],
},
})
clock.tick(EXPIRE_DELAY)

expect(events.length).toBe(1)
expect(events[0].target?.selector).toBe('#shadow-host')
expect(events[0].target?.selector).not.toContain(SHADOW_DOM_MARKER)
})
})
})

Expand Down
10 changes: 5 additions & 5 deletions packages/rum-core/src/domain/action/trackClickActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -231,16 +231,16 @@ function computeClickActionBase(
nodePrivacyLevel: NodePrivacyLevel,
configuration: RumConfiguration
): ClickActionBase {
const selectorTarget = event.target
const rect = selectorTarget.getBoundingClientRect()
const selector = getSelectorFromElement(selectorTarget, configuration.actionNameAttribute)
const target = configuration.betaTrackActionsInShadowDom ? getEventTarget(event) : event.target

const rect = target.getBoundingClientRect()
const selector = getSelectorFromElement(target, configuration.actionNameAttribute)

if (selector) {
updateInteractionSelector(event.timeStamp, selector)
}

const nameTarget = configuration.betaTrackActionsInShadowDom ? getEventTarget(event) : event.target
const { name, nameSource } = getActionNameFromElement(nameTarget, configuration, nodePrivacyLevel)
const { name, nameSource } = getActionNameFromElement(target, configuration, nodePrivacyLevel)

return {
type: ActionType.CLICK,
Expand Down
155 changes: 146 additions & 9 deletions packages/rum-core/src/domain/getSelectorFromElement.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { appendElement } from '../../test'
import { getSelectorFromElement, isSelectorUniqueAmongSiblings } from './getSelectorFromElement'
import { getSelectorFromElement, isSelectorUniqueAmongSiblings, SHADOW_DOM_MARKER } from './getSelectorFromElement'

describe('getSelectorFromElement', () => {
afterEach(() => {
Expand Down Expand Up @@ -163,23 +163,23 @@ describe('getSelectorFromElement', () => {
describe('isSelectorUniqueAmongSiblings', () => {
it('returns true when the element is alone', () => {
const element = appendElement('<div></div>')
expect(isSelectorUniqueAmongSiblings(element, 'DIV', undefined)).toBeTrue()
expect(isSelectorUniqueAmongSiblings(element, document, 'DIV', undefined)).toBeTrue()
})

it('returns false when a sibling element matches the element selector', () => {
const element = appendElement(`
<div target></div>
<div></div>
`)
expect(isSelectorUniqueAmongSiblings(element, 'DIV', undefined)).toBeFalse()
expect(isSelectorUniqueAmongSiblings(element, document, 'DIV', undefined)).toBeFalse()
})

it('returns true when the element selector does not match any sibling', () => {
const element = appendElement(`
<div target></div>
<span></span>
`)
expect(isSelectorUniqueAmongSiblings(element, 'DIV', undefined)).toBeTrue()
expect(isSelectorUniqueAmongSiblings(element, document, 'DIV', undefined)).toBeTrue()
})

it('returns false when the child selector matches an element in a sibling', () => {
Expand All @@ -191,7 +191,7 @@ describe('isSelectorUniqueAmongSiblings', () => {
<hr>
</div>
`)
expect(isSelectorUniqueAmongSiblings(element, 'DIV', 'HR')).toBeFalse()
expect(isSelectorUniqueAmongSiblings(element, document, 'DIV', 'HR')).toBeFalse()
})

it('returns true when the current element selector does not match the sibling', () => {
Expand All @@ -203,7 +203,7 @@ describe('isSelectorUniqueAmongSiblings', () => {
<hr>
</h1>
`)
expect(isSelectorUniqueAmongSiblings(element, 'DIV', 'HR')).toBeTrue()
expect(isSelectorUniqueAmongSiblings(element, document, 'DIV', 'HR')).toBeTrue()
})

it('the selector should not consider elements deep in the tree', () => {
Expand All @@ -217,7 +217,144 @@ describe('isSelectorUniqueAmongSiblings', () => {
</div>
</h1>
`)
expect(isSelectorUniqueAmongSiblings(element, 'DIV', 'HR')).toBeTrue()
expect(isSelectorUniqueAmongSiblings(element, document, 'DIV', 'HR')).toBeTrue()
})
})

describe('getSelectorFromElement with shadow DOM', () => {
Comment thread
rgaignault marked this conversation as resolved.
it('should generate selector with shadow marker for element inside shadow DOM', () => {
const host = appendElement('<div id="shadow-host"></div>')
const shadowRoot = host.attachShadow({ mode: 'open' })
const button = document.createElement('button')
button.classList.add('shadow-button')
shadowRoot.appendChild(button)

const selector = getSelectorFromElement(button, undefined)
expect(selector).toBe(`#shadow-host${SHADOW_DOM_MARKER}BUTTON.shadow-button`)
})

it('should use stable attribute for element inside shadow DOM with shadow marker', () => {
const host = appendElement('<div></div>')
const shadowRoot = host.attachShadow({ mode: 'open' })
const button = document.createElement('button')
button.setAttribute('data-testid', 'shadow-test')
shadowRoot.appendChild(button)

const selector = getSelectorFromElement(button, undefined)
expect(selector).toBe(`BODY>DIV${SHADOW_DOM_MARKER}BUTTON[data-testid="shadow-test"]`)
})

it('should insert shadow marker when traversing shadow boundary', () => {
const host = appendElement('<div id="my-host"></div>')
const shadowRoot = host.attachShadow({ mode: 'open' })
const button = document.createElement('button')
shadowRoot.appendChild(button)

const selector = getSelectorFromElement(button, undefined)
expect(selector).toBe(`#my-host${SHADOW_DOM_MARKER}BUTTON`)
})

it('should handle nested shadow DOMs with multiple markers', () => {
const outerHost = appendElement('<div data-testid="outer-host"></div>')
const outerShadowRoot = outerHost.attachShadow({ mode: 'open' })

const innerHost = document.createElement('div')
innerHost.setAttribute('data-testid', 'inner-host')
outerShadowRoot.appendChild(innerHost)
const innerShadowRoot = innerHost.attachShadow({ mode: 'open' })

const button = document.createElement('button')
button.setAttribute('data-testid', 'deep-button')
innerShadowRoot.appendChild(button)

const selector = getSelectorFromElement(button, undefined)
expect(selector).toBe(
`DIV[data-testid="outer-host"]${SHADOW_DOM_MARKER}DIV[data-testid="inner-host"]${SHADOW_DOM_MARKER}BUTTON[data-testid="deep-button"]`
)
})

it('should use position selector inside shadow DOM with shadow marker', () => {
const host = appendElement('<div></div>')
const shadowRoot = host.attachShadow({ mode: 'open' })

const div1 = document.createElement('div')
const span1 = document.createElement('span')
div1.appendChild(span1)

const div2 = document.createElement('div')
const target = document.createElement('span')
div2.appendChild(target)

shadowRoot.appendChild(div1)
shadowRoot.appendChild(div2)

const selector = getSelectorFromElement(target, undefined)
// Both divs have a span, so DIV>SPAN is not unique, need nth-of-type
expect(selector).toBe(`BODY>DIV${SHADOW_DOM_MARKER}DIV:nth-of-type(2)>SPAN`)
})

it('should generate unique selector when siblings exist inside shadow DOM', () => {
const host = appendElement('<div id="host"></div>')
const shadowRoot = host.attachShadow({ mode: 'open' })

const button1 = document.createElement('button')
button1.classList.add('first')
const button2 = document.createElement('button')
button2.classList.add('second')

shadowRoot.appendChild(button1)
shadowRoot.appendChild(button2)

const selector1 = getSelectorFromElement(button1, undefined)
const selector2 = getSelectorFromElement(button2, undefined)

expect(selector1).toBe(`#host${SHADOW_DOM_MARKER}BUTTON.first`)
expect(selector2).toBe(`#host${SHADOW_DOM_MARKER}BUTTON.second`)
})

it('should NOT add shadow marker for elements in light DOM', () => {
const element = appendElement('<div><button class="light-btn"></button></div>')
const button = element.querySelector('button')!

const selector = getSelectorFromElement(button, undefined)
expect(selector).toBe('BODY>DIV>BUTTON.light-btn')
})

it('should generate DIFFERENT selectors for buttons in two identical shadow hosts', () => {
const container = appendElement('<div id="test-container"></div>')

const host1 = document.createElement('my-button')
const host2 = document.createElement('my-button')
container.appendChild(host1)
container.appendChild(host2)

const shadow1 = host1.attachShadow({ mode: 'open' })
const shadow2 = host2.attachShadow({ mode: 'open' })

const button1 = document.createElement('button')
button1.textContent = 'Button 1'
shadow1.appendChild(button1)

const button2 = document.createElement('button')
button2.textContent = 'Button 2'
shadow2.appendChild(button2)

const selector1 = getSelectorFromElement(button1, undefined)
const selector2 = getSelectorFromElement(button2, undefined)

expect(selector1).toBe(`#test-container>MY-BUTTON:nth-of-type(1)${SHADOW_DOM_MARKER}BUTTON`)
expect(selector2).toBe(`#test-container>MY-BUTTON:nth-of-type(2)${SHADOW_DOM_MARKER}BUTTON`)
})

it('should handle duplicated IDs between light DOM and shadow DOM', () => {
const host = appendElement('<div id="foo"></div>')
const shadowRoot = host.attachShadow({ mode: 'open' })
const button = document.createElement('button')
button.id = 'foo'
shadowRoot.appendChild(button)

const selector = getSelectorFromElement(button, undefined)
expect(selector).toBe(`#foo${SHADOW_DOM_MARKER}#foo`)
})

it('returns false when element is in DocumentFragment with matching siblings', () => {
Expand All @@ -228,14 +365,14 @@ describe('isSelectorUniqueAmongSiblings', () => {
fragment.appendChild(div2)

// The function should return false because div2 matches 'DIV' selector
expect(isSelectorUniqueAmongSiblings(div1, 'DIV', undefined)).toBeFalse()
expect(isSelectorUniqueAmongSiblings(div1, document, 'DIV', undefined)).toBeFalse()
})

it('returns true when element is in DocumentFragment with no matching siblings', () => {
const fragment = document.createDocumentFragment()
const div = document.createElement('div')
fragment.appendChild(div)

expect(isSelectorUniqueAmongSiblings(div, 'DIV', undefined)).toBeTrue()
expect(isSelectorUniqueAmongSiblings(div, document, 'DIV', undefined)).toBeTrue()
})
})
Loading