Skip to content

Commit

Permalink
✅ [RUMF-1209] add frustration related tests to trackActions
Browse files Browse the repository at this point in the history
  • Loading branch information
BenoitZugmeyer committed Apr 5, 2022
1 parent a9a69ef commit 176d6d3
Show file tree
Hide file tree
Showing 3 changed files with 134 additions and 46 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { Clock } from '../../../../../core/test/specHelper'
import { createNewEvent } from '../../../../../core/test/specHelper'
import type { TestSetupBuilder } from '../../../../test/specHelper'
import { setup } from '../../../../test/specHelper'
import { RumEventType, ActionType } from '../../../rawRumEvent.types'
import { RumEventType, ActionType, FrustrationType } from '../../../rawRumEvent.types'
import type { RumEvent } from '../../../rumEvent.types'
import { LifeCycleEventType } from '../../lifeCycle'
import { PAGE_ACTIVITY_VALIDATION_DELAY } from '../../waitIdlePage'
Expand All @@ -14,8 +14,6 @@ import { AUTO_ACTION_MAX_DURATION, trackActions } from './trackActions'

// Used to wait some time after the creation of a action
const BEFORE_PAGE_ACTIVITY_VALIDATION_DELAY = PAGE_ACTIVITY_VALIDATION_DELAY * 0.8
// Used to wait some time but it doesn't matter how much.
const SOME_ARBITRARY_DELAY = 50
// A long delay used to wait after any action is finished.
const EXPIRE_DELAY = AUTO_ACTION_MAX_DURATION * 10

Expand All @@ -32,25 +30,16 @@ function eventsCollector<T>() {
}
}

const RAW_ERROR_EVENT = { type: RumEventType.ERROR } as RumEvent & Context

describe('trackActions', () => {
const { events, pushEvent } = eventsCollector<AutoAction>()
let button: HTMLButtonElement
let emptyElement: HTMLHRElement
let input: HTMLInputElement
let setupBuilder: TestSetupBuilder
let findActionId: ActionContexts['findActionId']

function mockValidatedClickAction(
domMutationObservable: Observable<void>,
clock: Clock,
target: HTMLElement = button,
actionDuration: number = BEFORE_PAGE_ACTIVITY_VALIDATION_DELAY
) {
target.dispatchEvent(createNewEvent('click'))
clock.tick(actionDuration)
// Since we don't collect dom mutations for this test, manually dispatch one
domMutationObservable.notify()
}

beforeEach(() => {
button = document.createElement('button')
button.type = 'button'
Expand All @@ -60,6 +49,10 @@ describe('trackActions', () => {
emptyElement = document.createElement('hr')
document.body.appendChild(emptyElement)

input = document.createElement('input')
input.value = 'foo bar'
document.body.appendChild(input)

setupBuilder = setup()
.withFakeClock()
.beforeBuild(({ lifeCycle, domMutationObservable, configuration }) => {
Expand All @@ -73,12 +66,13 @@ describe('trackActions', () => {
afterEach(() => {
button.parentNode!.removeChild(button)
emptyElement.parentNode!.removeChild(emptyElement)
input.parentNode!.removeChild(input)
setupBuilder.cleanup()
})

it('starts a action when clicking on an element', () => {
const { domMutationObservable, clock } = setupBuilder.build()
mockValidatedClickAction(domMutationObservable, clock)
emulateClickWithActivity(domMutationObservable, clock)
expect(findActionId()).not.toBeUndefined()
clock.tick(EXPIRE_DELAY)
expect(events).toEqual([
Expand All @@ -101,7 +95,7 @@ describe('trackActions', () => {

it('discards a pending action with a negative duration', () => {
const { domMutationObservable, clock } = setupBuilder.build()
mockValidatedClickAction(domMutationObservable, clock, button, -1)
emulateClickWithActivity(domMutationObservable, clock, button, -1)
expect(findActionId()).not.toBeUndefined()
clock.tick(EXPIRE_DELAY)

Expand All @@ -111,7 +105,7 @@ describe('trackActions', () => {

it('should keep track of previously validated actions', () => {
const { domMutationObservable, clock } = setupBuilder.build()
mockValidatedClickAction(domMutationObservable, clock)
emulateClickWithActivity(domMutationObservable, clock)
const actionStartTime = relativeNow()
clock.tick(EXPIRE_DELAY)

Expand All @@ -120,17 +114,16 @@ describe('trackActions', () => {

it('counts errors occurring during the action', () => {
const { lifeCycle, domMutationObservable, clock } = setupBuilder.build()
const collectedRumEvent = { type: RumEventType.ERROR } as RumEvent & Context

mockValidatedClickAction(domMutationObservable, clock)
emulateClickWithActivity(domMutationObservable, clock)

lifeCycle.notify(LifeCycleEventType.RUM_EVENT_COLLECTED, collectedRumEvent)
lifeCycle.notify(LifeCycleEventType.RUM_EVENT_COLLECTED, RAW_ERROR_EVENT)
clock.tick(BEFORE_PAGE_ACTIVITY_VALIDATION_DELAY)
domMutationObservable.notify()
lifeCycle.notify(LifeCycleEventType.RUM_EVENT_COLLECTED, collectedRumEvent)
lifeCycle.notify(LifeCycleEventType.RUM_EVENT_COLLECTED, RAW_ERROR_EVENT)

clock.tick(EXPIRE_DELAY)
lifeCycle.notify(LifeCycleEventType.RUM_EVENT_COLLECTED, collectedRumEvent)
lifeCycle.notify(LifeCycleEventType.RUM_EVENT_COLLECTED, RAW_ERROR_EVENT)

expect(events.length).toBe(1)
const action = events[0]
Expand All @@ -147,7 +140,7 @@ describe('trackActions', () => {
.build()

button.setAttribute('data-my-custom-attribute', 'test-1')
mockValidatedClickAction(domMutationObservable, clock)
emulateClickWithActivity(domMutationObservable, clock)

clock.tick(EXPIRE_DELAY)
expect(events.length).toBe(1)
Expand All @@ -157,7 +150,7 @@ describe('trackActions', () => {
describe('without frustration-signals flag', () => {
it('discards pending action on view created', () => {
const { lifeCycle, domMutationObservable, clock } = setupBuilder.build()
mockValidatedClickAction(domMutationObservable, clock)
emulateClickWithActivity(domMutationObservable, clock)
expect(findActionId()).not.toBeUndefined()

lifeCycle.notify(LifeCycleEventType.VIEW_CREATED, {
Expand All @@ -174,13 +167,42 @@ describe('trackActions', () => {
const { domMutationObservable, clock } = setupBuilder.build()

const firstClickTimeStamp = timeStampNow()
mockValidatedClickAction(domMutationObservable, clock)
mockValidatedClickAction(domMutationObservable, clock)
emulateClickWithActivity(domMutationObservable, clock)
emulateClickWithActivity(domMutationObservable, clock)

clock.tick(EXPIRE_DELAY)
expect(events.length).toBe(1)
expect(events[0].startClocks.timeStamp).toBe(firstClickTimeStamp)
})

it('discards a action when nothing happens after a click', () => {
const { clock } = setupBuilder.build()
emulateClickWithoutActivity()

clock.tick(EXPIRE_DELAY)
expect(events).toEqual([])
expect(findActionId()).toBeUndefined()
})

it('ignores a actions if it fails to find a name', () => {
const { domMutationObservable, clock } = setupBuilder.build()
emulateClickWithActivity(domMutationObservable, clock, emptyElement)
expect(findActionId()).toBeUndefined()
clock.tick(EXPIRE_DELAY)

expect(events).toEqual([])
})

it('does not populate the frustrationTypes array', () => {
const { lifeCycle, domMutationObservable, clock } = setupBuilder.build()

emulateClickWithActivity(domMutationObservable, clock)
lifeCycle.notify(LifeCycleEventType.RUM_EVENT_COLLECTED, RAW_ERROR_EVENT)

clock.tick(EXPIRE_DELAY)
expect(events.length).toBe(1)
expect(events[0].frustrationTypes).toEqual([])
})
})

describe('with frustration-signals flag', () => {
Expand All @@ -193,7 +215,7 @@ describe('trackActions', () => {

it("doesn't discard pending action on view created", () => {
const { lifeCycle, domMutationObservable, clock } = setupBuilder.build()
mockValidatedClickAction(domMutationObservable, clock)
emulateClickWithActivity(domMutationObservable, clock)
expect(findActionId()).not.toBeUndefined()

lifeCycle.notify(LifeCycleEventType.VIEW_CREATED, {
Expand All @@ -209,33 +231,95 @@ describe('trackActions', () => {
const { domMutationObservable, clock } = setupBuilder.build()

const firstClickTimeStamp = timeStampNow()
mockValidatedClickAction(domMutationObservable, clock)
emulateClickWithActivity(domMutationObservable, clock)
const secondClickTimeStamp = timeStampNow()
mockValidatedClickAction(domMutationObservable, clock)
emulateClickWithActivity(domMutationObservable, clock)

clock.tick(EXPIRE_DELAY)
expect(events.length).toBe(2)
expect(events[0].startClocks.timeStamp).toBe(firstClickTimeStamp)
expect(events[1].startClocks.timeStamp).toBe(secondClickTimeStamp)
})
})

it('discards a action when nothing happens after a click', () => {
const { clock } = setupBuilder.build()
clock.tick(SOME_ARBITRARY_DELAY)
button.click()
it('collect actions even if nothing happens after a click (dead click)', () => {
const { clock } = setupBuilder.build()
emulateClickWithoutActivity()

clock.tick(EXPIRE_DELAY)
expect(events).toEqual([])
expect(findActionId()).toBeUndefined()
})
clock.tick(EXPIRE_DELAY)
expect(events.length).toBe(1)
expect(events[0].frustrationTypes).toEqual([FrustrationType.DEAD])
expect(findActionId()).toEqual([])
})

it('ignores a actions if it fails to find a name', () => {
const { domMutationObservable, clock } = setupBuilder.build()
mockValidatedClickAction(domMutationObservable, clock, emptyElement)
expect(findActionId()).toBeUndefined()
clock.tick(EXPIRE_DELAY)
it('collect actions even if it fails to find a name', () => {
const { domMutationObservable, clock } = setupBuilder.build()
emulateClickWithActivity(domMutationObservable, clock, emptyElement)
expect(findActionId()!.length).toBeGreaterThan(0)
clock.tick(EXPIRE_DELAY)

expect(events).toEqual([])
expect(events.length).toBe(1)
})

describe('error clicks', () => {
it('considers a "click with activity" followed by an error as an action with "error" frustration type', () => {
const { lifeCycle, domMutationObservable, clock } = setupBuilder.build()

emulateClickWithActivity(domMutationObservable, clock)
lifeCycle.notify(LifeCycleEventType.RUM_EVENT_COLLECTED, RAW_ERROR_EVENT)

clock.tick(EXPIRE_DELAY)
expect(events.length).toBe(1)
expect(events[0].frustrationTypes).toEqual([FrustrationType.ERROR])
})

// eslint-disable-next-line max-len
it('considers a "click without activity" followed by an error as an action with "error" (and "dead") frustration type', () => {
const { lifeCycle, clock } = setupBuilder.build()

emulateClickWithoutActivity()
lifeCycle.notify(LifeCycleEventType.RUM_EVENT_COLLECTED, RAW_ERROR_EVENT)

clock.tick(EXPIRE_DELAY)
expect(events.length).toBe(1)
expect(events[0].frustrationTypes).toEqual(
jasmine.arrayWithExactContents([FrustrationType.ERROR, FrustrationType.DEAD])
)
})
})

describe('dead clicks', () => {
it('considers a "click without activity" as a dead click', () => {
const { clock } = setupBuilder.build()

emulateClickWithoutActivity()

clock.tick(EXPIRE_DELAY)
expect(events.length).toBe(1)
expect(events[0].frustrationTypes).toEqual([FrustrationType.DEAD])
})
})
})

function emulateClickWithActivity(
domMutationObservable: Observable<void>,
clock: Clock,
target: HTMLElement = button,
actionDuration: number = BEFORE_PAGE_ACTIVITY_VALIDATION_DELAY
) {
emulateClickWithoutActivity(target)
clock.tick(actionDuration)
// Since we don't collect dom mutations for this test, manually dispatch one
domMutationObservable.notify()
}

function emulateClickWithoutActivity(target: HTMLElement = button) {
const targetPosition = target.getBoundingClientRect()
target.dispatchEvent(
createNewEvent('click', {
target,
clientX: targetPosition.x + targetPosition.width / 2,
clientY: targetPosition.y + targetPosition.height / 2,
})
)
}
})
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,9 @@ function newPotentialAction(

const frustrationTypes: FrustrationType[] = []
frustrations.forEach((frustration) => {
frustrationTypes.push(frustration)
if (collectedFrustrations.has(frustration)) {
frustrationTypes.push(frustration)
}
})
const { resourceCount, errorCount, longTaskCount } = eventCountsSubscription.eventCounts
const action: AutoAction = assign(
Expand Down
2 changes: 2 additions & 0 deletions test/e2e/scenario/rum/actions.scenario.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ describe('action collection', () => {
name: 'click me',
},
type: 'click',
frustration_type: [],
})
})

Expand Down Expand Up @@ -79,6 +80,7 @@ describe('action collection', () => {
name: 'click me',
},
type: 'click',
frustration_type: [],
})

expect(resourceEvents.length).toBe(1)
Expand Down

0 comments on commit 176d6d3

Please sign in to comment.