diff --git a/packages/core/src/tools/experimentalFeatures.ts b/packages/core/src/tools/experimentalFeatures.ts
index 7c83cd79ca..3c5f7b16d5 100644
--- a/packages/core/src/tools/experimentalFeatures.ts
+++ b/packages/core/src/tools/experimentalFeatures.ts
@@ -18,8 +18,6 @@ export enum ExperimentalFeature {
FEATURE_OPERATION_VITAL = 'feature_operation_vital',
START_STOP_ACTION = 'start_stop_action',
START_STOP_RESOURCE = 'start_stop_resource',
- USE_CHANGE_RECORDS = 'use_change_records',
- USE_INCREMENTAL_CHANGE_RECORDS = 'use_incremental_change_records',
TOO_MANY_REQUESTS_INVESTIGATION = 'too_many_requests_investigation',
TRACK_RESOURCE_HEADERS = 'track_resource_headers',
}
diff --git a/packages/rum/src/boot/datadogRecorder.spec.ts b/packages/rum/src/boot/datadogRecorder.spec.ts
index b220387b31..4dc3c36771 100644
--- a/packages/rum/src/boot/datadogRecorder.spec.ts
+++ b/packages/rum/src/boot/datadogRecorder.spec.ts
@@ -1,13 +1,5 @@
import type { TimeStamp, HttpRequest, HttpRequestEvent, Telemetry } from '@datadog/browser-core'
-import {
- PageExitReason,
- DefaultPrivacyLevel,
- noop,
- DeflateEncoderStreamId,
- Observable,
- ExperimentalFeature,
- addExperimentalFeatures,
-} from '@datadog/browser-core'
+import { PageExitReason, DefaultPrivacyLevel, noop, DeflateEncoderStreamId, Observable } from '@datadog/browser-core'
import type { ViewCreatedEvent } from '@datadog/browser-rum-core'
import { LifeCycle, LifeCycleEventType, startViewHistory } from '@datadog/browser-rum-core'
import type { SessionManagerMock } from '@datadog/browser-core/test'
@@ -111,35 +103,6 @@ describe('startRecording', () => {
})
})
- it('sends recorded segments with valid context when Change records are enabled', async () => {
- addExperimentalFeatures([ExperimentalFeature.USE_CHANGE_RECORDS])
- setupStartRecording()
- flushSegment(lifeCycle)
-
- const requests = await readSentRequests(1)
- expect(requests[0].segment).toEqual(jasmine.any(Object))
- expect(requests[0].event).toEqual({
- application: {
- id: 'appId',
- },
- creation_reason: 'init',
- end: jasmine.stringMatching(/^\d{13}$/),
- has_full_snapshot: true,
- records_count: recordsPerFullSnapshot(),
- session: {
- id: MOCK_SESSION_ID,
- },
- start: jasmine.any(Number),
- raw_segment_size: jasmine.any(Number),
- compressed_segment_size: jasmine.any(Number),
- view: {
- id: 'view-id',
- },
- index_in_view: 0,
- source: 'browser',
- })
- })
-
it('flushes the segment when its compressed data reaches the segment bytes limit', async () => {
setupStartRecording()
const inputCount = 150
@@ -196,7 +159,7 @@ describe('startRecording', () => {
const requests = await readSentRequests(2)
const firstSegment = requests[0].segment
- expect(firstSegment.records[firstSegment.records.length - 2].type).toBe(RecordType.IncrementalSnapshot)
+ expect(firstSegment.records[firstSegment.records.length - 2].type).toBe(RecordType.Change)
expect(firstSegment.records[firstSegment.records.length - 1].type).toBe(RecordType.ViewEnd)
const secondSegment = requests[1].segment
diff --git a/packages/rum/src/domain/record/index.ts b/packages/rum/src/domain/record/index.ts
index db82a1d51d..c31d7480e3 100644
--- a/packages/rum/src/domain/record/index.ts
+++ b/packages/rum/src/domain/record/index.ts
@@ -5,8 +5,6 @@ export {
aggregateSerializationStats,
createChangeDecoder,
createSerializationStats,
- isFullSnapshotChangeRecordsEnabled,
- isIncrementalSnapshotChangeRecordsEnabled,
serializeNode,
} from './serialization'
export { createElementsScrollPositions } from './elementsScrollPositions'
diff --git a/packages/rum/src/domain/record/internalApi.spec.ts b/packages/rum/src/domain/record/internalApi.spec.ts
index 9d6ad46ed9..3764b7f1b4 100644
--- a/packages/rum/src/domain/record/internalApi.spec.ts
+++ b/packages/rum/src/domain/record/internalApi.spec.ts
@@ -1,6 +1,8 @@
-import { NodeType, RecordType, SnapshotFormat } from '../../types'
+import type { BrowserChangeRecord, BrowserFullSnapshotChangeRecord, BrowserRecord } from '../../types'
+import { ChangeType, RecordType, SnapshotFormat } from '../../types'
import { appendElement } from '../../../../rum-core/test'
import { takeFullSnapshot, takeNodeSnapshot } from './internalApi'
+import { createChangeDecoder } from './serialization'
describe('takeFullSnapshot', () => {
it('should produce Meta, Focus, and FullSnapshot records', () => {
@@ -23,14 +25,8 @@ describe('takeFullSnapshot', () => {
timestamp: jasmine.any(Number),
},
{
- data: {
- node: jasmine.any(Object),
- initialOffset: {
- left: jasmine.any(Number),
- top: jasmine.any(Number),
- },
- },
- format: SnapshotFormat.V1,
+ data: jasmine.any(Object),
+ format: SnapshotFormat.Change,
type: RecordType.FullSnapshot,
timestamp: jasmine.any(Number),
},
@@ -56,35 +52,30 @@ describe('takeFullSnapshot', () => {
})
describe('takeNodeSnapshot', () => {
+ function decodeSnapshot(
+ record: BrowserRecord | undefined
+ ): BrowserChangeRecord | BrowserFullSnapshotChangeRecord | undefined {
+ if (!record) {
+ return undefined
+ }
+ if (record.type !== RecordType.FullSnapshot) {
+ throw new Error(`Unexpected record type ${record.type}`)
+ }
+ if (record.format !== SnapshotFormat.Change) {
+ throw new Error(`Unexpected record format ${record.format}`)
+ }
+
+ const decoder = createChangeDecoder()
+ return decoder.decode(record)
+ }
+
it('should serialize nodes', () => {
const node = appendElement('
Hello world
', document.body)
- expect(takeNodeSnapshot(node)).toEqual({
- type: NodeType.Element,
- id: 0,
- tagName: 'div',
- isSVG: undefined,
- attributes: {},
- childNodes: [
- {
- type: NodeType.Text,
- id: 1,
- textContent: 'Hello ',
- },
- {
- type: NodeType.Element,
- id: 2,
- tagName: 'b',
- isSVG: undefined,
- attributes: {},
- childNodes: [
- {
- type: NodeType.Text,
- id: 3,
- textContent: 'world',
- },
- ],
- },
- ],
+ expect(decodeSnapshot(takeNodeSnapshot(node))).toEqual({
+ type: RecordType.FullSnapshot,
+ format: SnapshotFormat.Change,
+ data: [[ChangeType.AddNode, [null, 'DIV'], [1, '#text', 'Hello '], [0, 'B'], [1, '#text', 'world']]],
+ timestamp: jasmine.any(Number),
})
})
@@ -92,32 +83,11 @@ describe('takeNodeSnapshot', () => {
const node = appendElement('Hello
', document.body)
const shadowRoot = node.attachShadow({ mode: 'open' })
shadowRoot.appendChild(document.createTextNode('world'))
- expect(takeNodeSnapshot(node)).toEqual({
- type: NodeType.Element,
- id: 0,
- tagName: 'div',
- isSVG: undefined,
- attributes: {},
- childNodes: [
- {
- type: NodeType.Text,
- id: 1,
- textContent: 'Hello',
- },
- {
- type: NodeType.DocumentFragment,
- id: 2,
- isShadowRoot: true,
- adoptedStyleSheets: undefined,
- childNodes: [
- {
- type: NodeType.Text,
- id: 3,
- textContent: 'world',
- },
- ],
- },
- ],
+ expect(decodeSnapshot(takeNodeSnapshot(node))).toEqual({
+ type: RecordType.FullSnapshot,
+ format: SnapshotFormat.Change,
+ data: [[ChangeType.AddNode, [null, 'DIV'], [1, '#text', 'Hello'], [0, '#shadow-root'], [1, '#text', 'world']]],
+ timestamp: jasmine.any(Number),
})
})
})
diff --git a/packages/rum/src/domain/record/internalApi.ts b/packages/rum/src/domain/record/internalApi.ts
index dc10e7808c..7006c92459 100644
--- a/packages/rum/src/domain/record/internalApi.ts
+++ b/packages/rum/src/domain/record/internalApi.ts
@@ -1,15 +1,20 @@
import { noop, timeStampNow } from '@datadog/browser-core'
import type { RumConfiguration } from '@datadog/browser-rum-core'
import { getNodePrivacyLevel, NodePrivacyLevel } from '@datadog/browser-rum-core'
-import type { BrowserRecord, SerializedNodeWithId } from '../../types'
+import type { BrowserRecord } from '../../types'
import { takeFullSnapshot as doTakeFullSnapshot } from './startFullSnapshots'
import type { ShadowRootsController } from './shadowRootsController'
import type { RecordingScope } from './recordingScope'
import { createRecordingScope } from './recordingScope'
import { createElementsScrollPositions } from './elementsScrollPositions'
import type { EmitRecordCallback } from './record.types'
-import type { SerializationTransaction } from './serialization'
-import { SerializationKind, serializeInTransaction, serializeNode } from './serialization'
+import type { ChangeSerializationTransaction } from './serialization'
+import {
+ createRootInsertionCursor,
+ SerializationKind,
+ serializeChangesInTransaction,
+ serializeNodeAsChange,
+} from './serialization'
/**
* Take a full snapshot of the document, generating the same records that the browser SDK
@@ -47,24 +52,29 @@ export function takeFullSnapshot({
export function takeNodeSnapshot(
node: Node,
{ configuration }: { configuration?: Partial } = {}
-): SerializedNodeWithId | null {
- let serializedNode: SerializedNodeWithId | null = null
+): BrowserRecord | undefined {
+ let nodeSnapshotRecord: BrowserRecord | undefined
+ const emitRecord = (record: BrowserRecord) => {
+ nodeSnapshotRecord = record
+ }
- serializeInTransaction(
+ serializeChangesInTransaction(
SerializationKind.INITIAL_FULL_SNAPSHOT,
- noop,
+ emitRecord,
noop,
createTemporaryRecordingScope(configuration),
- (transaction: SerializationTransaction): void => {
+ timeStampNow(),
+ (transaction: ChangeSerializationTransaction): void => {
const privacyLevel = getNodePrivacyLevel(node, transaction.scope.configuration.defaultPrivacyLevel)
if (privacyLevel === NodePrivacyLevel.HIDDEN || privacyLevel === NodePrivacyLevel.IGNORE) {
return
}
- serializedNode = serializeNode(node, privacyLevel, transaction)
+ const cursor = createRootInsertionCursor(transaction.scope.nodeIds)
+ serializeNodeAsChange(cursor, node, privacyLevel, transaction)
}
)
- return serializedNode
+ return nodeSnapshotRecord
}
function createTemporaryRecordingScope(configuration?: Partial): RecordingScope {
diff --git a/packages/rum/src/domain/record/record.spec.ts b/packages/rum/src/domain/record/record.spec.ts
index a7ddac9c22..6a119e3b13 100644
--- a/packages/rum/src/domain/record/record.spec.ts
+++ b/packages/rum/src/domain/record/record.spec.ts
@@ -2,21 +2,23 @@ import { DefaultPrivacyLevel, findLast, noop } from '@datadog/browser-core'
import type { RumConfiguration, ViewCreatedEvent } from '@datadog/browser-rum-core'
import { LifeCycle, LifeCycleEventType } from '@datadog/browser-rum-core'
import { createNewEvent, collectAsyncCalls, registerCleanupTask } from '@datadog/browser-core/test'
-import { findElement, findFullSnapshotInFormat, findNode, recordsPerFullSnapshot } from '../../../test'
+import { recordsPerFullSnapshot } from '../../../test'
import type {
+ AddNodeChange,
+ BrowserChangeRecord,
BrowserIncrementalSnapshotRecord,
BrowserMutationData,
BrowserRecord,
- DocumentFragmentNode,
- ElementNode,
+ Change,
ScrollData,
} from '../../types'
-import { NodeType, RecordType, IncrementalSource, SnapshotFormat } from '../../types'
+import { ChangeType, RecordType, IncrementalSource, SnapshotFormat } from '../../types'
import { appendElement } from '../../../../rum-core/test'
import { getReplayStats } from '../replayStats'
import type { RecordAPI } from './record'
import { record } from './record'
import type { EmitRecordCallback } from './record.types'
+import { createChangeDecoder } from './serialization'
describe('record', () => {
let recordApi: RecordAPI
@@ -128,8 +130,8 @@ describe('record', () => {
if (window.visualViewport) {
expect(records[i++].type).toEqual(RecordType.VisualViewport)
}
- expect(records[i].type).toEqual(RecordType.IncrementalSnapshot)
- expect((records[i++] as BrowserIncrementalSnapshotRecord).data.source).toEqual(IncrementalSource.Mutation)
+ expect(records[i].type).toEqual(RecordType.Change)
+ expect((records[i++] as BrowserChangeRecord).data.map((change) => change[0])).toContain(ChangeType.AddNode)
expect(records[i++].type).toEqual(RecordType.Meta)
expect(records[i++].type).toEqual(RecordType.Focus)
expect(records[i++].type).toEqual(RecordType.FullSnapshot)
@@ -139,109 +141,83 @@ describe('record', () => {
it('should record a simple mutation inside a shadow root', () => {
const element = appendElement('
', createShadow())
startRecording()
- expect(getEmittedRecords().length).toBe(recordsPerFullSnapshot())
+ expect(getEmittedRecordCount()).toBe(recordsPerFullSnapshot())
element.className = 'titi'
recordApi.flushMutations()
- expect(getEmittedRecords().length).toBe(recordsPerFullSnapshot() + 1)
- const innerMutationData = getLastIncrementalSnapshotData(
- getEmittedRecords(),
- IncrementalSource.Mutation
- )
- expect(innerMutationData.attributes[0].attributes.class).toBe('titi')
+ expect(getEmittedRecordCount()).toBe(recordsPerFullSnapshot() + 1)
+ const [, ...attributeMutations] = getLastChangeOfType(ChangeType.Attribute, getEmittedRecords())
+ expect(attributeMutations[0][1]).toEqual(['class', 'titi'])
})
it('should record a direct removal inside a shadow root', () => {
const element = appendElement('
', createShadow())
startRecording()
- expect(getEmittedRecords().length).toBe(recordsPerFullSnapshot())
+ expect(getEmittedRecordCount()).toBe(recordsPerFullSnapshot())
element.remove()
-
recordApi.flushMutations()
- const fs = findFullSnapshotInFormat(SnapshotFormat.V1, { records: getEmittedRecords() })!
- const shadowRootNode = findNode(
- fs.data.node,
- (node) => node.type === NodeType.DocumentFragment && node.isShadowRoot
- )!
- expect(shadowRootNode).toBeTruthy()
- expect(getEmittedRecords().length).toBe(recordsPerFullSnapshot() + 1)
- const innerMutationData = getLastIncrementalSnapshotData(
- getEmittedRecords(),
- IncrementalSource.Mutation
- )
- expect(innerMutationData.removes.length).toBe(1)
- expect(innerMutationData.removes[0].parentId).toBe(shadowRootNode.id)
+ expect(getEmittedRecordCount()).toBe(recordsPerFullSnapshot() + 1)
+
+ const records = getEmittedRecords()
+ const horizontalRuleId = findNodeId(records, (change) => change[1] === 'HR')
+ const [, ...removeMutations] = getLastChangeOfType(ChangeType.RemoveNode, records)
+ expect(removeMutations.length).toBe(1)
+ expect(removeMutations[0]).toBe(horizontalRuleId)
})
it('should record a direct addition inside a shadow root', () => {
const shadowRoot = createShadow()
appendElement('
', shadowRoot)
startRecording()
- expect(getEmittedRecords().length).toBe(recordsPerFullSnapshot())
+ expect(getEmittedRecordCount()).toBe(recordsPerFullSnapshot())
appendElement('', shadowRoot)
-
recordApi.flushMutations()
- expect(getEmittedRecords().length).toBe(recordsPerFullSnapshot() + 1)
- const fs = findFullSnapshotInFormat(SnapshotFormat.V1, { records: getEmittedRecords() })!
- const shadowRootNode = findNode(
- fs.data.node,
- (node) => node.type === NodeType.DocumentFragment && node.isShadowRoot
- )!
- expect(shadowRootNode).toBeTruthy()
- const innerMutationData = getLastIncrementalSnapshotData(
- getEmittedRecords(),
- IncrementalSource.Mutation
- )
- expect(innerMutationData.adds.length).toBe(1)
- expect(innerMutationData.adds[0].node.type).toBe(2)
- expect(innerMutationData.adds[0].parentId).toBe(shadowRootNode.id)
- const addedNode = innerMutationData.adds[0].node as ElementNode
- expect(addedNode.tagName).toBe('span')
+ expect(getEmittedRecordCount()).toBe(recordsPerFullSnapshot() + 1)
+
+ const records = getEmittedRecords()
+ const [, ...addMutations] = getLastChangeOfType(ChangeType.AddNode, records)
+ expect(addMutations.length).toBe(1)
+ expect(addMutations[0][1]).toBe('SPAN')
})
it('should record mutation inside a shadow root added after the FS', () => {
startRecording()
- expect(getEmittedRecords().length).toBe(recordsPerFullSnapshot())
+ expect(getEmittedRecordCount()).toBe(recordsPerFullSnapshot())
// shadow DOM mutation
const span = appendElement('', createShadow())
recordApi.flushMutations()
- expect(getEmittedRecords().length).toBe(recordsPerFullSnapshot() + 1)
- const hostMutationData = getLastIncrementalSnapshotData(
- getEmittedRecords(),
- IncrementalSource.Mutation
- )
- expect(hostMutationData.adds.length).toBe(1)
- const hostNode = hostMutationData.adds[0].node as ElementNode
- const shadowRoot = hostNode.childNodes[0] as DocumentFragmentNode
- expect(shadowRoot.type).toBe(NodeType.DocumentFragment)
- expect(shadowRoot.isShadowRoot).toBe(true)
+ expect(getEmittedRecordCount()).toBe(recordsPerFullSnapshot() + 1)
+
+ const [, ...addMutations] = getLastChangeOfType(ChangeType.AddNode, getEmittedRecords())
+ expect(addMutations.length).toBe(3)
+ expect(addMutations[0][1]).toBe('DIV')
+ expect(addMutations[1][1]).toBe('#shadow-root')
+ expect(addMutations[2][1]).toBe('SPAN')
// inner mutation
span.className = 'titi'
recordApi.flushMutations()
- expect(getEmittedRecords().length).toBe(recordsPerFullSnapshot() + 2)
- const innerMutationData = getLastIncrementalSnapshotData(
- getEmittedRecords(),
- IncrementalSource.Mutation
- )
- expect(innerMutationData.attributes.length).toBe(1)
- expect(innerMutationData.attributes[0].attributes.class).toBe('titi')
+ expect(getEmittedRecordCount()).toBe(recordsPerFullSnapshot() + 2)
+
+ const [, ...attributeMutations] = getLastChangeOfType(ChangeType.Attribute, getEmittedRecords())
+ expect(attributeMutations[0][1]).toEqual(['class', 'titi'])
})
it('should record the change event inside a shadow root', () => {
const radio = appendElement('', createShadow()) as HTMLInputElement
startRecording()
- expect(getEmittedRecords().length).toBe(recordsPerFullSnapshot())
+ expect(getEmittedRecordCount()).toBe(recordsPerFullSnapshot())
// inner mutation
radio.checked = true
radio.dispatchEvent(createNewEvent('change', { target: radio, composed: false }))
recordApi.flushMutations()
+
const innerMutationData = getLastIncrementalSnapshotData(
getEmittedRecords(),
IncrementalSource.Input
@@ -269,7 +245,7 @@ describe('record', () => {
it('should record the scroll event inside a shadow root', () => {
const div = appendElement('', createShadow()) as HTMLDivElement
startRecording()
- expect(getEmittedRecords().length).toBe(recordsPerFullSnapshot())
+ expect(getEmittedRecordCount()).toBe(recordsPerFullSnapshot())
div.dispatchEvent(createNewEvent('scroll', { target: div, composed: false }))
@@ -280,12 +256,16 @@ describe('record', () => {
)
expect(scrollRecords.length).toBe(1)
- const scrollData = getLastIncrementalSnapshotData(getEmittedRecords(), IncrementalSource.Scroll)
-
- const fs = findFullSnapshotInFormat(SnapshotFormat.V1, { records: getEmittedRecords() })!
- const scrollableNode = findElement(fs.data.node, (node) => node.attributes['unique-selector'] === 'enabled')!
+ const records = getEmittedRecords()
+ const scrollableNodeId = findNodeId(records, (change) => {
+ const [, , ...attributes]: AddNodeChange = change
+ return attributes.some(
+ (attribute) => Array.isArray(attribute) && attribute[0] === 'unique-selector' && attribute[1] === 'enabled'
+ )
+ })
- expect(scrollData.id).toBe(scrollableNode.id)
+ const scrollData = getLastIncrementalSnapshotData(getEmittedRecords(), IncrementalSource.Scroll)
+ expect(scrollData.id).toBe(scrollableNodeId)
})
it('should clean the state once the shadow dom is removed to avoid memory leak', () => {
@@ -294,17 +274,17 @@ describe('record', () => {
startRecording()
spyOn(recordApi.shadowRootsController, 'removeShadowRoot')
- expect(getEmittedRecords().length).toBe(recordsPerFullSnapshot())
+ expect(getEmittedRecordCount()).toBe(recordsPerFullSnapshot())
expect(recordApi.shadowRootsController.removeShadowRoot).toHaveBeenCalledTimes(0)
+
shadowRoot.host.remove()
recordApi.flushMutations()
expect(recordApi.shadowRootsController.removeShadowRoot).toHaveBeenCalledTimes(1)
- expect(getEmittedRecords().length).toBe(recordsPerFullSnapshot() + 1)
- const mutationData = getLastIncrementalSnapshotData(
- getEmittedRecords(),
- IncrementalSource.Mutation
- )
- expect(mutationData.removes.length).toBe(1)
+ expect(getEmittedRecordCount()).toBe(recordsPerFullSnapshot() + 1)
+
+ const records = getEmittedRecords()
+ const [, ...removeMutations] = getLastChangeOfType(ChangeType.RemoveNode, records)
+ expect(removeMutations.length).toBe(1)
})
it('should clean the state when both the parent and the shadow host is removed to avoid memory leak', () => {
@@ -321,19 +301,18 @@ describe('record', () => {
startRecording()
spyOn(recordApi.shadowRootsController, 'removeShadowRoot')
- expect(getEmittedRecords().length).toBe(recordsPerFullSnapshot())
+ expect(getEmittedRecordCount()).toBe(recordsPerFullSnapshot())
expect(recordApi.shadowRootsController.removeShadowRoot).toHaveBeenCalledTimes(0)
parent.remove()
grandParent.remove()
recordApi.flushMutations()
expect(recordApi.shadowRootsController.removeShadowRoot).toHaveBeenCalledTimes(1)
- expect(getEmittedRecords().length).toBe(recordsPerFullSnapshot() + 1)
- const mutationData = getLastIncrementalSnapshotData(
- getEmittedRecords(),
- IncrementalSource.Mutation
- )
- expect(mutationData.removes.length).toBe(1)
+ expect(getEmittedRecordCount()).toBe(recordsPerFullSnapshot() + 1)
+
+ const records = getEmittedRecords()
+ const [, ...removeMutations] = getLastChangeOfType(ChangeType.RemoveNode, records)
+ expect(removeMutations.length).toBe(2)
})
function createShadow() {
@@ -451,8 +430,26 @@ describe('record', () => {
} as ViewCreatedEvent)
}
- function getEmittedRecords() {
- return emitSpy.calls.allArgs().map(([record]) => record)
+ function getEmittedRecordCount(): number {
+ return emitSpy.calls.allArgs().length
+ }
+
+ function getEmittedRecords(): BrowserRecord[] {
+ const changeDecoder = createChangeDecoder()
+
+ const decodedRecords: BrowserRecord[] = []
+ for (const [record] of emitSpy.calls.allArgs()) {
+ if (
+ record.type === RecordType.Change ||
+ (record.type === RecordType.FullSnapshot && record.format === SnapshotFormat.Change)
+ ) {
+ decodedRecords.push(changeDecoder.decode(record))
+ } else {
+ decodedRecords.push(record)
+ }
+ }
+
+ return decodedRecords
}
})
@@ -468,3 +465,47 @@ export function getLastIncrementalSnapshotData(changeType: T, change: Change): change is Extract {
+ return change[0] === changeType
+}
+
+export function getLastChangeOfType(
+ changeType: T,
+ records: BrowserRecord[]
+): Extract {
+ for (let i = records.length - 1; i >= 0; i--) {
+ const record = records[i]
+ if (record.type !== RecordType.Change) {
+ continue
+ }
+ for (const change of record.data) {
+ if (isChangeOfType(changeType, change)) {
+ return change
+ }
+ }
+ }
+ throw new Error(`Could not find Change of type ${changeType} in ${records.length} records`)
+}
+
+function findNodeId(records: BrowserRecord[], predicate: (change: AddNodeChange) => boolean): number {
+ for (const record of records) {
+ const isChangeRecord =
+ record.type === RecordType.Change ||
+ (record.type === RecordType.FullSnapshot && record.format === SnapshotFormat.Change)
+ if (!isChangeRecord) {
+ continue
+ }
+
+ for (const change of record.data) {
+ if (!isChangeOfType(ChangeType.AddNode, change)) {
+ continue
+ }
+
+ const [, ...addMutations] = change
+ return addMutations.findIndex(predicate)
+ }
+ }
+
+ return -1
+}
diff --git a/packages/rum/src/domain/record/serialization/experimentalFeatures.ts b/packages/rum/src/domain/record/serialization/experimentalFeatures.ts
deleted file mode 100644
index 606b8fc1dd..0000000000
--- a/packages/rum/src/domain/record/serialization/experimentalFeatures.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import { ExperimentalFeature, isExperimentalFeatureEnabled } from '@datadog/browser-core'
-
-export function isFullSnapshotChangeRecordsEnabled(): boolean {
- // We don't want to have to support the case where full snapshots use the old format and
- // incremental snapshots use the new one, so we should generate full snapshot Change
- // records if either feature flag is enabled.
- return (
- isExperimentalFeatureEnabled(ExperimentalFeature.USE_CHANGE_RECORDS) ||
- isExperimentalFeatureEnabled(ExperimentalFeature.USE_INCREMENTAL_CHANGE_RECORDS)
- )
-}
-
-export function isIncrementalSnapshotChangeRecordsEnabled(): boolean {
- return isExperimentalFeatureEnabled(ExperimentalFeature.USE_INCREMENTAL_CHANGE_RECORDS)
-}
diff --git a/packages/rum/src/domain/record/serialization/index.ts b/packages/rum/src/domain/record/serialization/index.ts
index fc9ad88686..3c479496f5 100644
--- a/packages/rum/src/domain/record/serialization/index.ts
+++ b/packages/rum/src/domain/record/serialization/index.ts
@@ -5,7 +5,6 @@ export {
createCopyingNodeIdRemapper,
createIdentityNodeIdRemapper,
} from './conversions'
-export { isFullSnapshotChangeRecordsEnabled, isIncrementalSnapshotChangeRecordsEnabled } from './experimentalFeatures'
export { createChildInsertionCursor, createRootInsertionCursor } from './insertionCursor'
export { getElementInputValue } from './serializationUtils'
export { serializeFullSnapshot } from './serializeFullSnapshot'
diff --git a/packages/rum/src/domain/record/startFullSnapshots.spec.ts b/packages/rum/src/domain/record/startFullSnapshots.spec.ts
index 0038de83f3..b59ed2e9bb 100644
--- a/packages/rum/src/domain/record/startFullSnapshots.spec.ts
+++ b/packages/rum/src/domain/record/startFullSnapshots.spec.ts
@@ -1,15 +1,14 @@
import type { ViewCreatedEvent } from '@datadog/browser-rum-core'
import { LifeCycle, LifeCycleEventType } from '@datadog/browser-rum-core'
import type { TimeStamp } from '@datadog/browser-core'
-import { addExperimentalFeatures, ExperimentalFeature, noop } from '@datadog/browser-core'
-import type { BrowserRecord } from '../../types'
+import { noop } from '@datadog/browser-core'
import { RecordType, SnapshotFormat } from '../../types'
import { appendElement } from '../../../../rum-core/test'
import { startFullSnapshots } from './startFullSnapshots'
import type { EmitRecordCallback, EmitStatsCallback } from './record.types'
import { createRecordingScopeForTesting } from './test/recordingScope.specHelper'
-const describeStartFullSnapshotsWithExpectedSnapshot = (fullSnapshotRecord: jasmine.Expected) => {
+describe('startFullSnapshots', () => {
const viewStartClock = { relative: 1, timeStamp: 1 as TimeStamp }
let lifeCycle: LifeCycle
let emitRecordCallback: jasmine.Spy
@@ -74,7 +73,12 @@ const describeStartFullSnapshotsWithExpectedSnapshot = (fullSnapshotRecord: jasm
type: RecordType.Focus,
timestamp: jasmine.any(Number),
},
- fullSnapshotRecord,
+ {
+ data: jasmine.any(Array),
+ type: RecordType.FullSnapshot,
+ format: SnapshotFormat.Change,
+ timestamp: jasmine.any(Number),
+ },
])
)
})
@@ -98,34 +102,4 @@ const describeStartFullSnapshotsWithExpectedSnapshot = (fullSnapshotRecord: jasm
serializationDuration: jasmine.anything(),
})
})
-}
-
-describe('startFullSnapshots', () => {
- describe('when generating BrowserFullSnapshotV1Record', () => {
- describeStartFullSnapshotsWithExpectedSnapshot({
- data: {
- node: jasmine.any(Object),
- initialOffset: {
- left: jasmine.any(Number),
- top: jasmine.any(Number),
- },
- },
- format: SnapshotFormat.V1,
- type: RecordType.FullSnapshot,
- timestamp: jasmine.any(Number),
- })
- })
-
- describe('when generating BrowserFullSnapshotChangeRecord', () => {
- beforeEach(() => {
- addExperimentalFeatures([ExperimentalFeature.USE_CHANGE_RECORDS])
- })
-
- describeStartFullSnapshotsWithExpectedSnapshot({
- data: jasmine.any(Array),
- format: SnapshotFormat.Change,
- type: RecordType.FullSnapshot,
- timestamp: jasmine.any(Number),
- })
- })
})
diff --git a/packages/rum/src/domain/record/startFullSnapshots.ts b/packages/rum/src/domain/record/startFullSnapshots.ts
index bcecd44e21..bcee2dda51 100644
--- a/packages/rum/src/domain/record/startFullSnapshots.ts
+++ b/packages/rum/src/domain/record/startFullSnapshots.ts
@@ -3,34 +3,19 @@ import type { LifeCycle } from '@datadog/browser-rum-core'
import { timeStampNow } from '@datadog/browser-core'
import type { TimeStamp } from '@datadog/browser-core'
import { RecordType } from '../../types'
-import {
- isFullSnapshotChangeRecordsEnabled,
- SerializationKind,
- serializeFullSnapshotAsChange,
- serializeFullSnapshot,
-} from './serialization'
+import { SerializationKind, serializeFullSnapshotAsChange } from './serialization'
import { getVisualViewport } from './viewports'
import type { RecordingScope } from './recordingScope'
import type { EmitRecordCallback, EmitStatsCallback } from './record.types'
-export type SerializeFullSnapshotCallback = (
- timestamp: TimeStamp,
- kind: SerializationKind,
- document: Document,
- emitRecord: EmitRecordCallback,
- emitStats: EmitStatsCallback,
- scope: RecordingScope
-) => void
-
export function startFullSnapshots(
lifeCycle: LifeCycle,
emitRecord: EmitRecordCallback,
emitStats: EmitStatsCallback,
flushMutations: () => void,
- scope: RecordingScope,
- serialize: SerializeFullSnapshotCallback = defaultSerializeFullSnapshotCallback()
+ scope: RecordingScope
) {
- takeFullSnapshot(timeStampNow(), SerializationKind.INITIAL_FULL_SNAPSHOT, emitRecord, emitStats, scope, serialize)
+ takeFullSnapshot(timeStampNow(), SerializationKind.INITIAL_FULL_SNAPSHOT, emitRecord, emitStats, scope)
const { unsubscribe } = lifeCycle.subscribe(LifeCycleEventType.VIEW_CREATED, (view) => {
flushMutations()
@@ -39,8 +24,7 @@ export function startFullSnapshots(
SerializationKind.SUBSEQUENT_FULL_SNAPSHOT,
emitRecord,
emitStats,
- scope,
- serialize
+ scope
)
})
@@ -54,8 +38,7 @@ export function takeFullSnapshot(
kind: SerializationKind,
emitRecord: EmitRecordCallback,
emitStats: EmitStatsCallback,
- scope: RecordingScope,
- serialize: SerializeFullSnapshotCallback = defaultSerializeFullSnapshotCallback()
+ scope: RecordingScope
): void {
const { width, height } = getViewportDimension()
emitRecord({
@@ -76,7 +59,7 @@ export function takeFullSnapshot(
timestamp,
})
- serialize(timestamp, kind, document, emitRecord, emitStats, scope)
+ serializeFullSnapshotAsChange(timestamp, kind, document, emitRecord, emitStats, scope)
if (window.visualViewport) {
emitRecord({
@@ -86,7 +69,3 @@ export function takeFullSnapshot(
})
}
}
-
-function defaultSerializeFullSnapshotCallback(): SerializeFullSnapshotCallback {
- return isFullSnapshotChangeRecordsEnabled() ? serializeFullSnapshotAsChange : serializeFullSnapshot
-}
diff --git a/packages/rum/src/domain/record/trackers/trackMutation.ts b/packages/rum/src/domain/record/trackers/trackMutation.ts
index d60f508429..2e392f64b8 100644
--- a/packages/rum/src/domain/record/trackers/trackMutation.ts
+++ b/packages/rum/src/domain/record/trackers/trackMutation.ts
@@ -5,11 +5,7 @@ import { getMutationObserverConstructor } from '@datadog/browser-rum-core'
import type { RecordingScope } from '../recordingScope'
import { createMutationBatch } from '../mutationBatch'
import type { EmitRecordCallback, EmitStatsCallback } from '../record.types'
-import {
- isIncrementalSnapshotChangeRecordsEnabled,
- serializeMutations,
- serializeMutationsAsChange,
-} from '../serialization'
+import { serializeMutationsAsChange } from '../serialization'
import type { Tracker } from './tracker.types'
export type MutationTracker = Tracker & { flush: () => void }
@@ -30,7 +26,7 @@ export function trackMutation(
emitRecord: EmitRecordCallback,
emitStats: EmitStatsCallback,
scope: RecordingScope,
- serialize: SerializeMutationsCallback = defaultSerializeMutationsCallback()
+ serialize: SerializeMutationsCallback = serializeMutationsAsChange
): MutationTracker {
const MutationObserver = getMutationObserverConstructor()
if (!MutationObserver) {
@@ -68,7 +64,3 @@ export function trackMutation(
},
}
}
-
-function defaultSerializeMutationsCallback(): SerializeMutationsCallback {
- return isIncrementalSnapshotChangeRecordsEnabled() ? serializeMutationsAsChange : serializeMutations
-}
diff --git a/packages/rum/src/domain/segmentCollection/startSegmentTelemetry.spec.ts b/packages/rum/src/domain/segmentCollection/startSegmentTelemetry.spec.ts
index 7d15cfb28f..d6fb2fcab8 100644
--- a/packages/rum/src/domain/segmentCollection/startSegmentTelemetry.spec.ts
+++ b/packages/rum/src/domain/segmentCollection/startSegmentTelemetry.spec.ts
@@ -1,9 +1,8 @@
import type { Telemetry, HttpRequestEvent, BandwidthStats } from '@datadog/browser-core'
-import { addExperimentalFeatures, ExperimentalFeature, Observable } from '@datadog/browser-core'
+import { Observable } from '@datadog/browser-core'
import type { MockTelemetry } from '@datadog/browser-core/test'
import { registerCleanupTask } from '@datadog/browser-core/test'
import { startMockTelemetry } from '../../../../core/test'
-import { isFullSnapshotChangeRecordsEnabled, isIncrementalSnapshotChangeRecordsEnabled } from '../record'
import { startSegmentTelemetry } from './startSegmentTelemetry'
import type { ReplayPayload } from './buildReplayPayload'
@@ -50,60 +49,50 @@ describe('segmentTelemetry', () => {
registerCleanupTask(stopSegmentTelemetry)
}
- for (const [featureFlag, description] of [
- [undefined, 'V1 records enabled'],
- [ExperimentalFeature.USE_CHANGE_RECORDS, 'full snapshot Change records enabled'],
- [ExperimentalFeature.USE_INCREMENTAL_CHANGE_RECORDS, 'incremental snapshot Change records enabled'],
- ] as const) {
- it(`with ${description}, should collect segment telemetry for all full snapshots`, async () => {
- if (featureFlag) {
- addExperimentalFeatures([featureFlag])
- }
-
- setupSegmentTelemetryCollection()
+ it('should collect segment telemetry for all full snapshots', async () => {
+ setupSegmentTelemetryCollection()
- for (const result of ['failure', 'queue-full', 'success'] as const) {
- generateReplayRequest({ result, isFullSnapshot: true })
+ for (const result of ['failure', 'queue-full', 'success'] as const) {
+ generateReplayRequest({ result, isFullSnapshot: true })
- expect(await telemetry.getEvents()).toEqual([
- jasmine.objectContaining({
- type: 'log',
- status: 'debug',
- message: 'Segment network request metrics',
- metrics: {
- cssText: {
- count: 2,
- max: 300,
- sum: 500,
- },
- encoding: {
- fullSnapshot: isFullSnapshotChangeRecordsEnabled() ? 'change' : 'v1',
- incrementalSnapshot: isIncrementalSnapshotChangeRecordsEnabled() ? 'change' : 'v1',
- },
- isFullSnapshot: true,
- ongoingRequests: {
- count: 2,
- totalSize: 3000,
- },
- recordCount: 3,
- result,
- size: {
- compressed: 1000,
- raw: 2000,
- },
- serializationDuration: {
- count: 3,
- max: 65,
- sum: 105,
- },
+ expect(await telemetry.getEvents()).toEqual([
+ jasmine.objectContaining({
+ type: 'log',
+ status: 'debug',
+ message: 'Segment network request metrics',
+ metrics: {
+ cssText: {
+ count: 2,
+ max: 300,
+ sum: 500,
+ },
+ encoding: {
+ fullSnapshot: 'change',
+ incrementalSnapshot: 'change',
+ },
+ isFullSnapshot: true,
+ ongoingRequests: {
+ count: 2,
+ totalSize: 3000,
},
- }),
- ])
+ recordCount: 3,
+ result,
+ size: {
+ compressed: 1000,
+ raw: 2000,
+ },
+ serializationDuration: {
+ count: 3,
+ max: 65,
+ sum: 105,
+ },
+ },
+ }),
+ ])
- telemetry.reset()
- }
- })
- }
+ telemetry.reset()
+ }
+ })
it('should collect segment telemetry for failed incremental mutation requests', async () => {
setupSegmentTelemetryCollection()
@@ -123,8 +112,8 @@ describe('segmentTelemetry', () => {
sum: 500,
},
encoding: {
- fullSnapshot: 'v1',
- incrementalSnapshot: 'v1',
+ fullSnapshot: 'change',
+ incrementalSnapshot: 'change',
},
isFullSnapshot: false,
ongoingRequests: {
diff --git a/packages/rum/src/domain/segmentCollection/startSegmentTelemetry.ts b/packages/rum/src/domain/segmentCollection/startSegmentTelemetry.ts
index ac6dbe6e68..72c3736ded 100644
--- a/packages/rum/src/domain/segmentCollection/startSegmentTelemetry.ts
+++ b/packages/rum/src/domain/segmentCollection/startSegmentTelemetry.ts
@@ -1,6 +1,5 @@
import type { BandwidthStats, Context, HttpRequestEvent, Observable, Telemetry } from '@datadog/browser-core'
import { TelemetryMetrics, addTelemetryMetrics, noop } from '@datadog/browser-core'
-import { isFullSnapshotChangeRecordsEnabled, isIncrementalSnapshotChangeRecordsEnabled } from '../record'
import type { ReplayPayload } from './buildReplayPayload'
interface SegmentMetrics extends Context {
@@ -68,8 +67,8 @@ function createSegmentMetrics(
sum: payload.cssText.sum,
},
encoding: {
- fullSnapshot: isFullSnapshotChangeRecordsEnabled() ? 'change' : 'v1',
- incrementalSnapshot: isIncrementalSnapshotChangeRecordsEnabled() ? 'change' : 'v1',
+ fullSnapshot: 'change',
+ incrementalSnapshot: 'change',
},
isFullSnapshot: payload.isFullSnapshot,
ongoingRequests: {
diff --git a/test/e2e/scenario/recorder/recorder.scenario.ts b/test/e2e/scenario/recorder/recorder.scenario.ts
index dd3ab4e108..621d025bbf 100644
--- a/test/e2e/scenario/recorder/recorder.scenario.ts
+++ b/test/e2e/scenario/recorder/recorder.scenario.ts
@@ -1,5 +1,5 @@
import type { InputData, StyleSheetRuleData, ScrollData } from '@datadog/browser-rum/src/types'
-import { NodeType, IncrementalSource, SnapshotFormat, ChangeType, RecordType } from '@datadog/browser-rum/src/types'
+import { IncrementalSource, ChangeType, RecordType } from '@datadog/browser-rum/src/types'
import { DefaultPrivacyLevel } from '@datadog/browser-core'
@@ -8,15 +8,12 @@ import {
getElementIdsFromFullSnapshot,
getScrollPositionsFromFullSnapshot,
} from '@datadog/browser-rum/test/record/elements'
-import { findElement, findElementWithIdAttribute, findTextContent } from '@datadog/browser-rum/test/record/nodes'
import {
findFullSnapshot,
findIncrementalSnapshot,
findAllIncrementalSnapshots,
findMeta,
- findFullSnapshotInFormat,
} from '@datadog/browser-rum/test/record/segments'
-import { createMutationPayloadValidatorFromSegment } from '@datadog/browser-rum/test/record/mutationPayloadValidator'
import { test, expect } from '@playwright/test'
import { wait } from '@datadog/browser-core/test/wait'
import { createTest, html } from '../../lib/framework'
@@ -77,85 +74,43 @@ test.describe('recorder', () => {
})
test.describe('full snapshot', () => {
- test.describe('obfuscate elements', () => {
- const body = html`displayed
- hidden
- hidden
-
- `
-
- createTest('V1')
- .withRum()
- .withBody(body)
- .run(async ({ intakeRegistry, flushEvents }) => {
- await flushEvents()
-
- expect(intakeRegistry.replaySegments).toHaveLength(1)
-
- const fullSnapshot = findFullSnapshotInFormat(SnapshotFormat.V1, intakeRegistry.replaySegments[0])!
-
- const node = findElementWithIdAttribute(fullSnapshot.data.node, 'not-obfuscated')
- expect(node).toBeTruthy()
- expect(findTextContent(node!)).toBe('displayed')
-
- const hiddenNodeByAttribute = findElement(fullSnapshot.data.node, (node) => node.tagName === 'p')
- expect(hiddenNodeByAttribute).toBeTruthy()
- expect(hiddenNodeByAttribute!.attributes['data-dd-privacy']).toBe('hidden')
- expect(hiddenNodeByAttribute!.childNodes).toHaveLength(0)
-
- const hiddenNodeByClassName = findElement(fullSnapshot.data.node, (node) => node.tagName === 'span')
- expect(hiddenNodeByClassName).toBeTruthy()
- expect(hiddenNodeByClassName!.attributes.class).toBeUndefined()
- expect(hiddenNodeByClassName!.attributes['data-dd-privacy']).toBe('hidden')
- expect(hiddenNodeByClassName!.childNodes).toHaveLength(0)
-
- const inputIgnored = findElementWithIdAttribute(fullSnapshot.data.node, 'input-not-obfuscated')
- expect(inputIgnored).toBeTruthy()
- expect(inputIgnored!.attributes.value).toBe('displayed')
-
- const inputMasked = findElementWithIdAttribute(fullSnapshot.data.node, 'input-masked')
- expect(inputMasked).toBeTruthy()
- expect(inputMasked!.attributes.value).toBe('***')
- })
+ createTest('obfuscate elements')
+ .withRum()
+ .withBody(
+ html`displayed
+ hidden
+ hidden
+
+ `
+ )
+ .run(async ({ intakeRegistry, flushEvents }) => {
+ await flushEvents()
+ expect(intakeRegistry.replaySegments).toHaveLength(1)
- createTest('Change')
- .withRum({
- enableExperimentalFeatures: ['use_incremental_change_records'],
- })
- .withBody(body)
- .run(async ({ intakeRegistry, flushEvents }) => {
- await flushEvents()
- expect(intakeRegistry.replaySegments).toHaveLength(1)
-
- const records = findChangeRecords(intakeRegistry.replaySegments[0].records)
- expect(decodeChangeRecords(records).at(0)!.data).toEqual([
- [
- ChangeType.AddNode,
- [null, '#document'],
- [1, '#doctype', 'html', '', ''],
- [0, 'HTML'],
- [1, 'HEAD'],
- [0, 'BODY'],
- [1, 'DIV', ['id', 'not-obfuscated']],
- [1, '#text', 'displayed'],
- [3, '#text', '\n '],
- [0, 'P', ['data-dd-privacy', 'hidden']],
- [0, '#text', '\n '],
- [0, 'SPAN', ['data-dd-privacy', 'hidden']],
- [0, '#text', '\n '],
- [0, 'INPUT', ['id', 'input-not-obfuscated'], ['value', 'displayed']],
- [0, '#text', '\n '],
- [0, 'INPUT', ['id', 'input-masked'], ['data-dd-privacy', 'mask'], ['value', '***']],
- ],
- [
- ChangeType.Size,
- [8, expect.any(Number), expect.any(Number)],
- [10, expect.any(Number), expect.any(Number)],
- ],
- [ChangeType.ScrollPosition, [0, 0, 0]],
- ])
- })
- })
+ const records = findChangeRecords(intakeRegistry.replaySegments[0].records)
+ expect(decodeChangeRecords(records).at(0)!.data).toEqual([
+ [
+ ChangeType.AddNode,
+ [null, '#document'],
+ [1, '#doctype', 'html', '', ''],
+ [0, 'HTML'],
+ [1, 'HEAD'],
+ [0, 'BODY'],
+ [1, 'DIV', ['id', 'not-obfuscated']],
+ [1, '#text', 'displayed'],
+ [3, '#text', '\n '],
+ [0, 'P', ['data-dd-privacy', 'hidden']],
+ [0, '#text', '\n '],
+ [0, 'SPAN', ['data-dd-privacy', 'hidden']],
+ [0, '#text', '\n '],
+ [0, 'INPUT', ['id', 'input-not-obfuscated'], ['value', 'displayed']],
+ [0, '#text', '\n '],
+ [0, 'INPUT', ['id', 'input-masked'], ['data-dd-privacy', 'mask'], ['value', '***']],
+ ],
+ [ChangeType.Size, [8, expect.any(Number), expect.any(Number)], [10, expect.any(Number), expect.any(Number)]],
+ [ChangeType.ScrollPosition, [0, 0, 0]],
+ ])
+ })
})
test.describe('mutations observer', () => {
@@ -166,546 +121,282 @@ test.describe('recorder', () => {
`
- test.describe('record mutations', () => {
- const mutate = () => {
- const li = document.createElement('li')
- const ul = document.querySelector('ul') as HTMLUListElement
-
- // Make sure mutations occurring in a removed element are not reported
- ul.appendChild(li)
- document.body.removeChild(ul)
-
- const p = document.querySelector('p') as HTMLParagraphElement
- p.appendChild(document.createElement('span'))
- }
-
- createTest('V1')
- .withRum()
- .withBody(body)
- .run(async ({ intakeRegistry, page, flushEvents }) => {
- await page.evaluate(mutate)
- await flushEvents()
-
- const { validate, expectNewNode, expectInitialNode } = createMutationPayloadValidatorFromSegment(
- intakeRegistry.replaySegments[0],
- { expect }
- )
+ createTest('record mutations')
+ .withRum()
+ .withBody(body)
+ .run(async ({ intakeRegistry, page, flushEvents }) => {
+ await page.evaluate(() => {
+ const li = document.createElement('li')
+ const ul = document.querySelector('ul') as HTMLUListElement
- validate({
- adds: [
- {
- parent: expectInitialNode({ tag: 'p' }),
- node: expectNewNode({ type: NodeType.Element, tagName: 'span' }),
- },
- ],
- removes: [
- {
- parent: expectInitialNode({ tag: 'body' }),
- node: expectInitialNode({ tag: 'ul' }),
- },
- ],
- })
- })
+ // Make sure mutations occurring in a removed element are not reported
+ ul.appendChild(li)
+ document.body.removeChild(ul)
- createTest('Change')
- .withRum({
- enableExperimentalFeatures: ['use_incremental_change_records'],
+ const p = document.querySelector('p') as HTMLParagraphElement
+ p.appendChild(document.createElement('span'))
})
- .withBody(body)
- .run(async ({ intakeRegistry, page, flushEvents }) => {
- await page.evaluate(mutate)
- await flushEvents()
-
- const records = findChangeRecords(intakeRegistry.replaySegments[0].records)
- expect(decodeChangeRecords(records).at(-1)!.data).toEqual([
- [ChangeType.AddNode, [8, 'SPAN']],
- [ChangeType.RemoveNode, 9],
- ])
- })
- })
+ await flushEvents()
- test.describe('record character data mutations', () => {
- const mutate = () => {
- const li = document.createElement('li')
- const ul = document.querySelector('ul') as HTMLUListElement
-
- // Make sure mutations occurring in a removed element are not reported
- ul.appendChild(li)
- li.innerText = 'new list item'
- li.innerText = 'new list item edit'
- document.body.removeChild(ul)
-
- const p = document.querySelector('p') as HTMLParagraphElement
- p.innerText = 'mutated'
- }
-
- createTest('V1')
- .withRum()
- .withBody(body)
- .run(async ({ intakeRegistry, page, flushEvents }) => {
- await page.evaluate(mutate)
- await flushEvents()
-
- const { validate, expectNewNode, expectInitialNode } = createMutationPayloadValidatorFromSegment(
- intakeRegistry.replaySegments[0],
- { expect }
- )
+ const records = findChangeRecords(intakeRegistry.replaySegments[0].records)
+ expect(decodeChangeRecords(records).at(-1)!.data).toEqual([
+ [ChangeType.AddNode, [8, 'SPAN']],
+ [ChangeType.RemoveNode, 9],
+ ])
+ })
- validate({
- adds: [
- {
- parent: expectInitialNode({ tag: 'p' }),
- node: expectNewNode({ type: NodeType.Text, textContent: 'mutated' }),
- },
- ],
- removes: [
- {
- parent: expectInitialNode({ tag: 'body' }),
- node: expectInitialNode({ tag: 'ul' }),
- },
- {
- parent: expectInitialNode({ tag: 'p' }),
- node: expectInitialNode({ text: 'mutation observer' }),
- },
- ],
- })
- })
+ createTest('record character data mutations')
+ .withRum()
+ .withBody(body)
+ .run(async ({ intakeRegistry, page, flushEvents }) => {
+ await page.evaluate(() => {
+ const li = document.createElement('li')
+ const ul = document.querySelector('ul') as HTMLUListElement
- createTest('Change')
- .withRum({
- enableExperimentalFeatures: ['use_incremental_change_records'],
- })
- .withBody(body)
- .run(async ({ intakeRegistry, page, flushEvents }) => {
- await page.evaluate(mutate)
- await flushEvents()
-
- const records = findChangeRecords(intakeRegistry.replaySegments[0].records)
- expect(decodeChangeRecords(records).at(-1)!.data).toEqual([
- [ChangeType.AddNode, [8, '#text', 'mutated']],
- [ChangeType.RemoveNode, 9, 7],
- ])
+ // Make sure mutations occurring in a removed element are not reported
+ ul.appendChild(li)
+ li.innerText = 'new list item'
+ li.innerText = 'new list item edit'
+ document.body.removeChild(ul)
+
+ const p = document.querySelector('p') as HTMLParagraphElement
+ p.innerText = 'mutated'
})
- })
+ await flushEvents()
- test.describe('record attributes mutations', () => {
- const mutate = () => {
- const li = document.createElement('li')
- const ul = document.querySelector('ul') as HTMLUListElement
-
- // Make sure mutations occurring in a removed element are not reported
- ul.appendChild(li)
- li.setAttribute('foo', 'bar')
- document.body.removeChild(ul)
-
- document.body.setAttribute('test', 'true')
- }
-
- createTest('V1')
- .withRum()
- .withBody(body)
- .run(async ({ intakeRegistry, page, flushEvents }) => {
- await page.evaluate(mutate)
- await flushEvents()
-
- const { validate, expectInitialNode } = createMutationPayloadValidatorFromSegment(
- intakeRegistry.replaySegments[0],
- { expect }
- )
+ const records = findChangeRecords(intakeRegistry.replaySegments[0].records)
+ expect(decodeChangeRecords(records).at(-1)!.data).toEqual([
+ [ChangeType.AddNode, [8, '#text', 'mutated']],
+ [ChangeType.RemoveNode, 9, 7],
+ ])
+ })
- validate({
- attributes: [
- {
- node: expectInitialNode({ tag: 'body' }),
- attributes: { test: 'true' },
- },
- ],
- removes: [
- {
- parent: expectInitialNode({ tag: 'body' }),
- node: expectInitialNode({ tag: 'ul' }),
- },
- ],
- })
- })
+ createTest('record attributes mutations')
+ .withRum()
+ .withBody(body)
+ .run(async ({ intakeRegistry, page, flushEvents }) => {
+ await page.evaluate(() => {
+ const li = document.createElement('li')
+ const ul = document.querySelector('ul') as HTMLUListElement
- createTest('Change')
- .withRum({
- enableExperimentalFeatures: ['use_incremental_change_records'],
- })
- .withBody(body)
- .run(async ({ intakeRegistry, page, flushEvents }) => {
- await page.evaluate(mutate)
- await flushEvents()
-
- const records = findChangeRecords(intakeRegistry.replaySegments[0].records)
- expect(decodeChangeRecords(records).at(-1)!.data).toEqual([
- [ChangeType.RemoveNode, 9],
- [ChangeType.Attribute, [4, ['test', 'true']]],
- ])
+ // Make sure mutations occurring in a removed element are not reported
+ ul.appendChild(li)
+ li.setAttribute('foo', 'bar')
+ document.body.removeChild(ul)
+
+ document.body.setAttribute('test', 'true')
})
- })
+ await flushEvents()
- test.describe("don't record hidden elements mutations", () => {
- const hiddenBody = html`
+ const records = findChangeRecords(intakeRegistry.replaySegments[0].records)
+ expect(decodeChangeRecords(records).at(-1)!.data).toEqual([
+ [ChangeType.RemoveNode, 9],
+ [ChangeType.Attribute, [4, ['test', 'true']]],
+ ])
+ })
+
+ createTest("don't record hidden elements mutations")
+ .withRum()
+ .withBody(html`
- `
-
- const mutate = () => {
- document.querySelector('div')!.setAttribute('foo', 'bar')
- document.querySelector('li')!.textContent = 'hop'
- document.querySelector('div')!.appendChild(document.createElement('p'))
- }
-
- createTest('V1')
- .withRum()
- .withBody(hiddenBody)
- .run(async ({ intakeRegistry, page, flushEvents }) => {
- await page.evaluate(mutate)
- await flushEvents()
-
- expect(intakeRegistry.replaySegments).toHaveLength(1)
- const segment = intakeRegistry.replaySegments[0]
- expect(findAllIncrementalSnapshots(segment, IncrementalSource.Mutation)).toHaveLength(0)
- })
-
- createTest('Change')
- .withRum({
- enableExperimentalFeatures: ['use_incremental_change_records'],
- })
- .withBody(hiddenBody)
- .run(async ({ intakeRegistry, page, flushEvents }) => {
- await page.evaluate(mutate)
- await flushEvents()
-
- expect(intakeRegistry.replaySegments).toHaveLength(1)
- const records = findChangeRecords(intakeRegistry.replaySegments[0].records)
- expect(records).toHaveLength(1)
- expect(records[0].type === RecordType.FullSnapshot)
+ `)
+ .run(async ({ intakeRegistry, page, flushEvents }) => {
+ await page.evaluate(() => {
+ document.querySelector('div')!.setAttribute('foo', 'bar')
+ document.querySelector('li')!.textContent = 'hop'
+ document.querySelector('div')!.appendChild(document.createElement('p'))
})
- })
+ await flushEvents()
- test.describe('record DOM node movement 1', () => {
- // prettier-ignore
- const body = html`
-
- cdefg
- `
-
- const mutate = () => {
- const div = document.querySelector('div')!
- const p = document.querySelector('p')!
- const span = document.querySelector('span')!
- document.body.removeChild(span)
- p.appendChild(span)
- p.removeChild(span)
- div.appendChild(span)
- }
-
- createTest('V1')
- .withRum()
- .withBody(body)
- .run(async ({ intakeRegistry, page, flushEvents }) => {
- await page.evaluate(mutate)
- await flushEvents()
-
- const { validate, expectInitialNode } = createMutationPayloadValidatorFromSegment(
- intakeRegistry.replaySegments[0],
- { expect }
- )
- validate({
- adds: [
- {
- parent: expectInitialNode({ tag: 'div' }),
- node: expectInitialNode({ tag: 'span' }).withChildren(
- expectInitialNode({ text: 'c' }),
- expectInitialNode({ tag: 'i' }).withChildren(
- expectInitialNode({ text: 'd' }),
- expectInitialNode({ tag: 'b' }).withChildren(expectInitialNode({ text: 'e' })),
- expectInitialNode({ text: 'f' })
- ),
- expectInitialNode({ text: 'g' })
- ),
- },
- ],
- removes: [
- {
- parent: expectInitialNode({ tag: 'body' }),
- node: expectInitialNode({ tag: 'span' }),
- },
- ],
- })
- })
+ expect(intakeRegistry.replaySegments).toHaveLength(1)
+ const records = findChangeRecords(intakeRegistry.replaySegments[0].records)
+ expect(records).toHaveLength(1)
+ expect(records[0].type === RecordType.FullSnapshot)
+ })
- createTest('Change')
- .withRum({
- enableExperimentalFeatures: ['use_incremental_change_records'],
- })
- .withBody(body)
- .run(async ({ intakeRegistry, page, flushEvents }) => {
- await page.evaluate(mutate)
- await flushEvents()
-
- const records = findChangeRecords(intakeRegistry.replaySegments[0].records)
- expect(decodeChangeRecords(records).at(-1)!.data).toEqual([
- [
- ChangeType.AddNode,
- [14, 'SPAN'],
- [1, '#text', 'c'],
- [0, 'I'],
- [1, '#text', 'd'],
- [0, 'B'],
- [1, '#text', 'e'],
- [4, '#text', 'f'],
- [7, '#text', 'g'],
- ],
- [ChangeType.RemoveNode, 11],
- ])
+ createTest('record DOM node movement 1')
+ .withRum()
+ .withBody(
+ // prettier-ignore
+ html`
+
+ cdefg
+ `
+ )
+ .run(async ({ intakeRegistry, page, flushEvents }) => {
+ await page.evaluate(() => {
+ const div = document.querySelector('div')!
+ const p = document.querySelector('p')!
+ const span = document.querySelector('span')!
+ document.body.removeChild(span)
+ p.appendChild(span)
+ p.removeChild(span)
+ div.appendChild(span)
})
- })
-
- test.describe('record DOM node movement 2', () => {
- // prettier-ignore
- const body = html`
- cdefg
- `
-
- const mutate = () => {
- const div = document.createElement('div')
- const span = document.querySelector('span')!
- document.body.appendChild(div)
- div.appendChild(span)
- }
-
- createTest('V1')
- .withRum()
- .withBody(body)
- .run(async ({ intakeRegistry, page, flushEvents }) => {
- await page.evaluate(mutate)
- await flushEvents()
-
- const { validate, expectInitialNode, expectNewNode } = createMutationPayloadValidatorFromSegment(
- intakeRegistry.replaySegments[0],
- { expect }
- )
+ await flushEvents()
- const div = expectNewNode({ type: NodeType.Element, tagName: 'div' })
-
- validate({
- adds: [
- {
- parent: expectInitialNode({ tag: 'body' }),
- node: div.withChildren(
- expectInitialNode({ tag: 'span' }).withChildren(
- expectInitialNode({ text: 'c' }),
- expectInitialNode({ tag: 'i' }).withChildren(
- expectInitialNode({ text: 'd' }),
- expectInitialNode({ tag: 'b' }).withChildren(expectInitialNode({ text: 'e' })),
- expectInitialNode({ text: 'f' })
- ),
- expectInitialNode({ text: 'g' })
- )
- ),
- },
- ],
- removes: [
- {
- parent: expectInitialNode({ tag: 'body' }),
- node: expectInitialNode({ tag: 'span' }),
- },
- ],
- })
- })
+ const records = findChangeRecords(intakeRegistry.replaySegments[0].records)
+ expect(decodeChangeRecords(records).at(-1)!.data).toEqual([
+ [
+ ChangeType.AddNode,
+ [14, 'SPAN'],
+ [1, '#text', 'c'],
+ [0, 'I'],
+ [1, '#text', 'd'],
+ [0, 'B'],
+ [1, '#text', 'e'],
+ [4, '#text', 'f'],
+ [7, '#text', 'g'],
+ ],
+ [ChangeType.RemoveNode, 11],
+ ])
+ })
- createTest('Change')
- .withRum({
- enableExperimentalFeatures: ['use_incremental_change_records'],
- })
- .withBody(body)
- .run(async ({ intakeRegistry, page, flushEvents }) => {
- await page.evaluate(mutate)
- await flushEvents()
-
- const records = findChangeRecords(intakeRegistry.replaySegments[0].records)
- expect(decodeChangeRecords(records).at(-1)!.data).toEqual([
- [
- ChangeType.AddNode,
- [11, 'DIV'],
- [1, 'SPAN'],
- [1, '#text', 'c'],
- [0, 'I'],
- [1, '#text', 'd'],
- [0, 'B'],
- [1, '#text', 'e'],
- [4, '#text', 'f'],
- [7, '#text', 'g'],
- ],
- [ChangeType.RemoveNode, 6],
- ])
+ createTest('record DOM node movement 2')
+ .withRum()
+ .withBody(
+ // prettier-ignore
+ html`
+ cdefg
+ `
+ )
+ .run(async ({ intakeRegistry, page, flushEvents }) => {
+ await page.evaluate(() => {
+ const div = document.createElement('div')
+ const span = document.querySelector('span')!
+ document.body.appendChild(div)
+ div.appendChild(span)
})
- })
+ await flushEvents()
- test.describe('serialize node before record', () => {
- // prettier-ignore
- const body = html`
-
- `
-
- const mutate = () => {
- const ul = document.querySelector('ul') as HTMLUListElement
- let count = 3
- while (count > 0) {
- count--
- const li = document.createElement('li')
- ul.appendChild(li)
- }
- }
-
- createTest('V1')
- .withRum()
- .withBody(body)
- .run(async ({ intakeRegistry, page, flushEvents }) => {
- await page.evaluate(mutate)
- await flushEvents()
-
- const { validate, expectInitialNode, expectNewNode } = createMutationPayloadValidatorFromSegment(
- intakeRegistry.replaySegments[0],
- { expect }
- )
+ const records = findChangeRecords(intakeRegistry.replaySegments[0].records)
+ expect(decodeChangeRecords(records).at(-1)!.data).toEqual([
+ [
+ ChangeType.AddNode,
+ [11, 'DIV'],
+ [1, 'SPAN'],
+ [1, '#text', 'c'],
+ [0, 'I'],
+ [1, '#text', 'd'],
+ [0, 'B'],
+ [1, '#text', 'e'],
+ [4, '#text', 'f'],
+ [7, '#text', 'g'],
+ ],
+ [ChangeType.RemoveNode, 6],
+ ])
+ })
- const ul = expectInitialNode({ tag: 'ul' })
- const li1 = expectNewNode({ type: NodeType.Element, tagName: 'li' })
- const li2 = expectNewNode({ type: NodeType.Element, tagName: 'li' })
- const li3 = expectNewNode({ type: NodeType.Element, tagName: 'li' })
-
- validate({
- adds: [
- {
- parent: ul,
- node: li1,
- },
- {
- next: li1,
- parent: ul,
- node: li2,
- },
- {
- next: li2,
- parent: ul,
- node: li3,
- },
- ],
- })
+ createTest('serialize node before record')
+ .withRum()
+ .withBody(
+ // prettier-ignore
+ html`
+
+ `
+ )
+ .run(async ({ intakeRegistry, page, flushEvents }) => {
+ await page.evaluate(() => {
+ const ul = document.querySelector('ul') as HTMLUListElement
+ let count = 3
+ while (count > 0) {
+ count--
+ const li = document.createElement('li')
+ ul.appendChild(li)
+ }
})
+ await flushEvents()
- createTest('Change')
- .withRum({
- enableExperimentalFeatures: ['use_incremental_change_records'],
- })
- .withBody(body)
- .run(async ({ intakeRegistry, page, flushEvents }) => {
- await page.evaluate(mutate)
- await flushEvents()
-
- const records = findChangeRecords(intakeRegistry.replaySegments[0].records)
- expect(decodeChangeRecords(records).at(-1)!.data).toEqual([
- [ChangeType.AddNode, [3, 'LI'], [4, 'LI'], [5, 'LI']],
- ])
- })
- })
+ const records = findChangeRecords(intakeRegistry.replaySegments[0].records)
+ expect(decodeChangeRecords(records).at(-1)!.data).toEqual([
+ [ChangeType.AddNode, [3, 'LI'], [4, 'LI'], [5, 'LI']],
+ ])
+ })
})
test.describe('input observers', () => {
- test.describe('record input interactions', () => {
- function createTestVariation(name: string, enableExperimentalFeatures: string[]): void {
- createTest(name)
- .withRum({
- defaultPrivacyLevel: DefaultPrivacyLevel.ALLOW,
- enableExperimentalFeatures,
- })
- .withBody(html`
-
- `)
- .run(async ({ intakeRegistry, page, flushEvents }) => {
- const textInput = page.locator('#text-input')
- await textInput.pressSequentially('test')
-
- const radioInput = page.locator('#radio-input')
- await radioInput.click()
-
- const checkboxInput = page.locator('#checkbox-input')
- await checkboxInput.click()
-
- const textarea = page.locator('#textarea')
- await textarea.pressSequentially('textarea test')
-
- const select = page.locator('#select')
- await select.selectOption({ value: '2' })
-
- await flushEvents()
-
- const fullSnapshot = findFullSnapshot({ records: intakeRegistry.replayRecords })!
- const elementIds = getElementIdsFromFullSnapshot(fullSnapshot)
-
- const textInputRecords = filterRecordsByIdAttribute('text-input')
- expect(textInputRecords.length).toBeGreaterThanOrEqual(4)
- expect((textInputRecords[textInputRecords.length - 1].data as { text?: string }).text).toBe('test')
-
- const radioInputRecords = filterRecordsByIdAttribute('radio-input')
- expect(radioInputRecords).toHaveLength(1)
- expect((radioInputRecords[0].data as { text?: string }).text).toBe(undefined)
- expect((radioInputRecords[0].data as { isChecked?: boolean }).isChecked).toBe(true)
-
- const checkboxInputRecords = filterRecordsByIdAttribute('checkbox-input')
- expect(checkboxInputRecords).toHaveLength(1)
- expect((checkboxInputRecords[0].data as { text?: string }).text).toBe(undefined)
- expect((checkboxInputRecords[0].data as { isChecked?: boolean }).isChecked).toBe(true)
-
- const textareaRecords = filterRecordsByIdAttribute('textarea')
- expect(textareaRecords.length).toBeGreaterThanOrEqual(4)
- expect((textareaRecords[textareaRecords.length - 1].data as { text?: string }).text).toBe('textarea test')
-
- const selectRecords = filterRecordsByIdAttribute('select')
- expect(selectRecords).toHaveLength(1)
- expect((selectRecords[0].data as { text?: string }).text).toBe('2')
-
- function filterRecordsByIdAttribute(idAttribute: string) {
- const id = elementIds.get(idAttribute)
- const records = findAllIncrementalSnapshots(
- { records: intakeRegistry.replayRecords },
- IncrementalSource.Input
- ) as Array<{ data: InputData }>
- return records.filter((record) => record.data.id === id)
- }
- })
- }
+ createTest('record input interactions')
+ .withRum({
+ defaultPrivacyLevel: DefaultPrivacyLevel.ALLOW,
+ })
+ .withBody(html`
+
+ `)
+ .run(async ({ intakeRegistry, page, flushEvents }) => {
+ const textInput = page.locator('#text-input')
+ await textInput.pressSequentially('test')
- createTestVariation('V1', [])
- createTestVariation('Change', ['use_incremental_change_records'])
- })
+ const radioInput = page.locator('#radio-input')
+ await radioInput.click()
+
+ const checkboxInput = page.locator('#checkbox-input')
+ await checkboxInput.click()
+
+ const textarea = page.locator('#textarea')
+ await textarea.pressSequentially('textarea test')
+
+ const select = page.locator('#select')
+ await select.selectOption({ value: '2' })
+
+ await flushEvents()
+
+ const fullSnapshot = findFullSnapshot({ records: intakeRegistry.replayRecords })!
+ const elementIds = getElementIdsFromFullSnapshot(fullSnapshot)
+
+ const textInputRecords = filterRecordsByIdAttribute('text-input')
+ expect(textInputRecords.length).toBeGreaterThanOrEqual(4)
+ expect((textInputRecords[textInputRecords.length - 1].data as { text?: string }).text).toBe('test')
+
+ const radioInputRecords = filterRecordsByIdAttribute('radio-input')
+ expect(radioInputRecords).toHaveLength(1)
+ expect((radioInputRecords[0].data as { text?: string }).text).toBe(undefined)
+ expect((radioInputRecords[0].data as { isChecked?: boolean }).isChecked).toBe(true)
+
+ const checkboxInputRecords = filterRecordsByIdAttribute('checkbox-input')
+ expect(checkboxInputRecords).toHaveLength(1)
+ expect((checkboxInputRecords[0].data as { text?: string }).text).toBe(undefined)
+ expect((checkboxInputRecords[0].data as { isChecked?: boolean }).isChecked).toBe(true)
+
+ const textareaRecords = filterRecordsByIdAttribute('textarea')
+ expect(textareaRecords.length).toBeGreaterThanOrEqual(4)
+ expect((textareaRecords[textareaRecords.length - 1].data as { text?: string }).text).toBe('textarea test')
+
+ const selectRecords = filterRecordsByIdAttribute('select')
+ expect(selectRecords).toHaveLength(1)
+ expect((selectRecords[0].data as { text?: string }).text).toBe('2')
+
+ function filterRecordsByIdAttribute(idAttribute: string) {
+ const id = elementIds.get(idAttribute)
+ const records = findAllIncrementalSnapshots(
+ { records: intakeRegistry.replayRecords },
+ IncrementalSource.Input
+ ) as Array<{ data: InputData }>
+ return records.filter((record) => record.data.id === id)
+ }
+ })
createTest("don't record ignored input interactions")
.withRum({
@@ -846,125 +537,117 @@ test.describe('recorder', () => {
})
test.describe('scroll positions', () => {
- test.describe('should be recorded across view changes', () => {
- function createTestVariation(name: string, enableExperimentalFeatures: string[]): void {
- createTest(name)
- .withRum({
- enableExperimentalFeatures,
- // to control initial position before recording
- startSessionReplayRecordingManually: true,
- })
- .withBody(html`
-
-
-
I'm bigger than the container
-
-
- `)
- .run(async ({ intakeRegistry, page, flushEvents }) => {
- function scroll({ windowY, containerX }: { windowY: number; containerX: number }) {
- return page.evaluate(
- ({ windowY, containerX }) =>
- new Promise((resolve) => {
- let scrollCount = 0
-
- document.addEventListener(
- 'scroll',
- () => {
- scrollCount++
- if (scrollCount === 2) {
- // ensure to bypass observer throttling
- setTimeout(resolve, 100)
- }
- },
- { capture: true, passive: true }
- )
-
- window.scrollTo(0, windowY)
- document.getElementById('container')!.scrollTo(containerX, 0)
- }),
- { windowY, containerX }
- )
- }
-
- await page.evaluate(() => {
- document.getElementsByTagName('html')[0].setAttribute('id', 'html')
- })
+ createTest('should be recorded across view changes')
+ .withRum({
+ // to control initial position before recording
+ startSessionReplayRecordingManually: true,
+ })
+ .withBody(html`
+
+
+
I'm bigger than the container
+
+
+ `)
+ .run(async ({ intakeRegistry, page, flushEvents }) => {
+ function scroll({ windowY, containerX }: { windowY: number; containerX: number }) {
+ return page.evaluate(
+ ({ windowY, containerX }) =>
+ new Promise((resolve) => {
+ let scrollCount = 0
+
+ document.addEventListener(
+ 'scroll',
+ () => {
+ scrollCount++
+ if (scrollCount === 2) {
+ // ensure to bypass observer throttling
+ setTimeout(resolve, 100)
+ }
+ },
+ { capture: true, passive: true }
+ )
+
+ window.scrollTo(0, windowY)
+ document.getElementById('container')!.scrollTo(containerX, 0)
+ }),
+ { windowY, containerX }
+ )
+ }
- // initial scroll positions
- await scroll({ windowY: 100, containerX: 10 })
+ await page.evaluate(() => {
+ document.getElementsByTagName('html')[0].setAttribute('id', 'html')
+ })
- await page.evaluate(() => {
- window.DD_RUM!.startSessionReplayRecording()
- })
+ // initial scroll positions
+ await scroll({ windowY: 100, containerX: 10 })
- // wait for recorder to be properly started
- await wait(100)
+ await page.evaluate(() => {
+ window.DD_RUM!.startSessionReplayRecording()
+ })
- // update scroll positions
- await scroll({ windowY: 150, containerX: 20 })
+ // wait for recorder to be properly started
+ await wait(100)
- // trigger new full snapshot
- await page.evaluate(() => {
- window.DD_RUM!.startView()
- })
+ // update scroll positions
+ await scroll({ windowY: 150, containerX: 20 })
- await flushEvents()
+ // trigger new full snapshot
+ await page.evaluate(() => {
+ window.DD_RUM!.startView()
+ })
- expect(intakeRegistry.replaySegments).toHaveLength(2)
- const firstSegment = intakeRegistry.replaySegments[0]
+ await flushEvents()
- {
- const firstFullSnapshot = findFullSnapshot(firstSegment)!
- const elementIds = getElementIdsFromFullSnapshot(firstFullSnapshot)
- const scrollPositions = getScrollPositionsFromFullSnapshot(firstFullSnapshot)
+ expect(intakeRegistry.replaySegments).toHaveLength(2)
+ const firstSegment = intakeRegistry.replaySegments[0]
- const htmlId = elementIds.get('html')
- expect(htmlId).not.toBeUndefined()
- expect(scrollPositions.get(htmlId!)).toEqual({ left: 0, top: 100 })
+ {
+ const firstFullSnapshot = findFullSnapshot(firstSegment)!
+ const elementIds = getElementIdsFromFullSnapshot(firstFullSnapshot)
+ const scrollPositions = getScrollPositionsFromFullSnapshot(firstFullSnapshot)
- const containerId = elementIds.get('container')
- expect(containerId).not.toBeUndefined()
- expect(scrollPositions.get(containerId!)).toEqual({ left: 10, top: 0 })
+ const htmlId = elementIds.get('html')
+ expect(htmlId).not.toBeUndefined()
+ expect(scrollPositions.get(htmlId!)).toEqual({ left: 0, top: 100 })
- const scrollRecords = findAllIncrementalSnapshots(firstSegment, IncrementalSource.Scroll)
- expect(scrollRecords).toHaveLength(2)
- const [windowScrollData, containerScrollData] = scrollRecords.map((record) => record.data as ScrollData)
- expect(windowScrollData.y).toEqual(150)
- expect(containerScrollData.x).toEqual(20)
- }
+ const containerId = elementIds.get('container')
+ expect(containerId).not.toBeUndefined()
+ expect(scrollPositions.get(containerId!)).toEqual({ left: 10, top: 0 })
- {
- const secondFullSnapshot = findFullSnapshot(intakeRegistry.replaySegments.at(-1)!)!
- const elementIds = getElementIdsFromFullSnapshot(secondFullSnapshot)
- const scrollPositions = getScrollPositionsFromFullSnapshot(secondFullSnapshot)
+ const scrollRecords = findAllIncrementalSnapshots(firstSegment, IncrementalSource.Scroll)
+ expect(scrollRecords).toHaveLength(2)
+ const [windowScrollData, containerScrollData] = scrollRecords.map((record) => record.data as ScrollData)
+ expect(windowScrollData.y).toEqual(150)
+ expect(containerScrollData.x).toEqual(20)
+ }
- const htmlId = elementIds.get('html')
- expect(htmlId).not.toBeUndefined()
- expect(scrollPositions.get(htmlId!)).toEqual({ left: 0, top: 150 })
+ {
+ const secondFullSnapshot = findFullSnapshot(intakeRegistry.replaySegments.at(-1)!)!
+ const elementIds = getElementIdsFromFullSnapshot(secondFullSnapshot)
+ const scrollPositions = getScrollPositionsFromFullSnapshot(secondFullSnapshot)
- const containerId = elementIds.get('container')
- expect(containerId).not.toBeUndefined()
- expect(scrollPositions.get(containerId!)).toEqual({ left: 20, top: 0 })
- }
- })
- }
+ const htmlId = elementIds.get('html')
+ expect(htmlId).not.toBeUndefined()
+ expect(scrollPositions.get(htmlId!)).toEqual({ left: 0, top: 150 })
- createTestVariation('V1', [])
- createTestVariation('Change', ['use_incremental_change_records'])
- })
+ const containerId = elementIds.get('container')
+ expect(containerId).not.toBeUndefined()
+ expect(scrollPositions.get(containerId!)).toEqual({ left: 20, top: 0 })
+ }
+ })
})
test.describe('recording of sampled out sessions', () => {
diff --git a/test/e2e/scenario/recorder/shadowDom.scenario.ts b/test/e2e/scenario/recorder/shadowDom.scenario.ts
index 301c91c266..6ce980f9eb 100644
--- a/test/e2e/scenario/recorder/shadowDom.scenario.ts
+++ b/test/e2e/scenario/recorder/shadowDom.scenario.ts
@@ -1,27 +1,8 @@
-import type {
- DocumentFragmentNode,
- MouseInteractionData,
- ScrollData,
- SerializedNodeWithId,
-} from '@datadog/browser-rum/src/types'
-import {
- ChangeType,
- IncrementalSource,
- MouseInteractionType,
- NodeType,
- SnapshotFormat,
-} from '@datadog/browser-rum/src/types'
+import type { MouseInteractionData, ScrollData } from '@datadog/browser-rum/src/types'
+import { ChangeType, IncrementalSource, MouseInteractionType } from '@datadog/browser-rum/src/types'
-import { createMutationPayloadValidatorFromSegment } from '@datadog/browser-rum/test/record/mutationPayloadValidator'
-import {
- findElementWithIdAttribute,
- findNode,
- findTextContent,
- findTextNode,
-} from '@datadog/browser-rum/test/record/nodes'
import {
findFullSnapshot,
- findFullSnapshotInFormat,
findIncrementalSnapshot,
findMouseInteractionRecords,
} from '@datadog/browser-rum/test/record/segments'
@@ -174,352 +155,193 @@ class DivWithStyle extends HTMLElement {
`
test.describe('recorder with shadow DOM', () => {
- test.describe('can record fullsnapshot with the detail inside the shadow root', () => {
- const body = html`
+ createTest('can record fullsnapshot with the detail inside the shadow root')
+ .withRum({ defaultPrivacyLevel: 'allow' })
+ .withBody(html`
${divShadowDom}
- `
-
- createTest('V1')
- .withRum({
- defaultPrivacyLevel: 'allow',
- })
- .withBody(body)
- .run(async ({ flushEvents, intakeRegistry }) => {
- await flushEvents()
- expect(intakeRegistry.replaySegments).toHaveLength(1)
-
- const fullSnapshot = findFullSnapshotInFormat(SnapshotFormat.V1, intakeRegistry.replaySegments[0])!
- expect(fullSnapshot).toBeTruthy()
-
- const textNode = findTextNode(fullSnapshot.data.node, 'toto')
- expect(textNode).toBeTruthy()
- expect(textNode?.textContent).toBe('toto')
- })
-
- createTest('Change')
- .withRum({
- defaultPrivacyLevel: 'allow',
- enableExperimentalFeatures: ['use_incremental_change_records'],
- })
- .withBody(body)
- .run(async ({ flushEvents, intakeRegistry }) => {
- await flushEvents()
- expect(intakeRegistry.replaySegments).toHaveLength(1)
-
- const records = findChangeRecords(intakeRegistry.replaySegments[0].records)
- expect(decodeChangeRecords(records).at(0)!.data).toEqual([
- [
- ChangeType.AddNode,
- [null, '#document'],
- [1, '#doctype', 'html', '', ''],
- [0, 'HTML'],
- [1, 'HEAD'],
- [0, 'BODY'],
- [1, '#text', '\n '],
- [0, '#text', '\n \n '],
- [0, 'MY-DIV'],
- [1, '#text', '\n '],
- [0, '#shadow-root'],
- [1, 'DIV', ['id', 'shadow-child']],
- [1, '#text', 'toto'],
- ],
- [ChangeType.ScrollPosition, [0, 0, 0]],
- ])
- })
- })
-
- test.describe('can record fullsnapshot with adoptedStylesheet', () => {
- const body = html`
+ `)
+ .run(async ({ flushEvents, intakeRegistry }) => {
+ await flushEvents()
+ expect(intakeRegistry.replaySegments).toHaveLength(1)
+
+ const records = findChangeRecords(intakeRegistry.replaySegments[0].records)
+ expect(decodeChangeRecords(records).at(0)!.data).toEqual([
+ [
+ ChangeType.AddNode,
+ [null, '#document'],
+ [1, '#doctype', 'html', '', ''],
+ [0, 'HTML'],
+ [1, 'HEAD'],
+ [0, 'BODY'],
+ [1, '#text', '\n '],
+ [0, '#text', '\n \n '],
+ [0, 'MY-DIV'],
+ [1, '#text', '\n '],
+ [0, '#shadow-root'],
+ [1, 'DIV', ['id', 'shadow-child']],
+ [1, '#text', 'toto'],
+ ],
+ [ChangeType.ScrollPosition, [0, 0, 0]],
+ ])
+ })
+
+ createTest('can record fullsnapshot with adoptedStylesheet')
+ .withRum()
+ .withBody(html`
${divWithStyleShadowDom}
- `
-
- async function skipIfAdoptedStyleSheetsNotSupported(page: Page): Promise {
- const isAdoptedStyleSheetsSupported = await page.evaluate(() => document.adoptedStyleSheets !== undefined)
- test.skip(!isAdoptedStyleSheetsSupported, 'adoptedStyleSheets is not supported in this browser')
- }
-
- createTest('V1')
- .withRum()
- .withBody(body)
- .run(async ({ flushEvents, intakeRegistry, page }) => {
- await skipIfAdoptedStyleSheetsNotSupported(page)
-
- await flushEvents()
- expect(intakeRegistry.replaySegments).toHaveLength(1)
-
- const fullSnapshot = findFullSnapshotInFormat(SnapshotFormat.V1, intakeRegistry.replaySegments[0])!
- expect(fullSnapshot).toBeTruthy()
- const shadowRoot = findNode(
- fullSnapshot.data.node,
- (node) => node.type === NodeType.DocumentFragment
- ) as DocumentFragmentNode
- expect(shadowRoot.isShadowRoot).toBe(true)
- expect(shadowRoot.adoptedStyleSheets).toEqual([{ cssRules: ['div { width: 100%; }'] }])
- })
-
- createTest('Change')
- .withRum({
- enableExperimentalFeatures: ['use_incremental_change_records'],
- })
- .withBody(body)
- .run(async ({ flushEvents, intakeRegistry, page }) => {
- await skipIfAdoptedStyleSheetsNotSupported(page)
-
- await flushEvents()
- expect(intakeRegistry.replaySegments).toHaveLength(1)
-
- const records = findChangeRecords(intakeRegistry.replaySegments[0].records)
- expect(decodeChangeRecords(records).at(-1)!.data).toEqual([
- [
- ChangeType.AddNode,
- [null, '#document'],
- [1, '#doctype', 'html', '', ''],
- [0, 'HTML'],
- [1, 'HEAD'],
- [0, 'BODY'],
- [1, '#text', '\n '],
- [0, '#text', '\n\n '],
- [0, 'DIV-WITH-STYLE'],
- [1, '#text', '\n '],
- [0, '#shadow-root'],
- [1, 'DIV'],
- [1, '#text', 'toto'],
- ],
- [ChangeType.ScrollPosition, [0, 0, 0]],
- [ChangeType.AddStyleSheet, [['div { width: 100%; }']]],
- [ChangeType.AttachedStyleSheets, [9, 0]],
- ])
- })
- })
-
- test.describe('can apply privacy level set from outside or inside the shadow DOM', () => {
- const body = html`
+ `)
+ .run(async ({ flushEvents, intakeRegistry, page }) => {
+ await skipIfAdoptedStyleSheetsNotSupported(page)
+
+ await flushEvents()
+ expect(intakeRegistry.replaySegments).toHaveLength(1)
+
+ const records = findChangeRecords(intakeRegistry.replaySegments[0].records)
+ expect(decodeChangeRecords(records).at(-1)!.data).toEqual([
+ [
+ ChangeType.AddNode,
+ [null, '#document'],
+ [1, '#doctype', 'html', '', ''],
+ [0, 'HTML'],
+ [1, 'HEAD'],
+ [0, 'BODY'],
+ [1, '#text', '\n '],
+ [0, '#text', '\n\n '],
+ [0, 'DIV-WITH-STYLE'],
+ [1, '#text', '\n '],
+ [0, '#shadow-root'],
+ [1, 'DIV'],
+ [1, '#text', 'toto'],
+ ],
+ [ChangeType.ScrollPosition, [0, 0, 0]],
+ [ChangeType.AddStyleSheet, [['div { width: 100%; }']]],
+ [ChangeType.AttachedStyleSheets, [9, 0]],
+ ])
+ })
+
+ createTest('can apply privacy level set from outside or inside the shadow DOM')
+ .withRum({ defaultPrivacyLevel: 'allow' })
+ .withBody(html`
${inputShadowDom}
- `
-
- createTest('V1')
- .withRum({
- defaultPrivacyLevel: 'allow',
- })
- .withBody(body)
- .run(async ({ flushEvents, intakeRegistry }) => {
- await flushEvents()
- expect(intakeRegistry.replaySegments).toHaveLength(1)
-
- const fullSnapshot = findFullSnapshotInFormat(SnapshotFormat.V1, intakeRegistry.replaySegments[0])!
- expect(fullSnapshot).toBeTruthy()
-
- const {
- input: outsideInput,
- shadowRoot: outsideShadowRoot,
- textContent: outsideTextContent,
- } = findElementsInShadowDom(fullSnapshot.data.node, 'privacy-set-outside')
- expect(outsideShadowRoot?.isShadowRoot).toBe(true)
- expect(outsideInput?.attributes.value).toBe('***')
- expect(outsideTextContent).toBe('field privacy-set-outside: ')
-
- const {
- input: insideInput,
- shadowRoot: insideShadowRoot,
- textContent: insideTextContent,
- } = findElementsInShadowDom(fullSnapshot.data.node, 'privacy-set-inside')
- expect(insideShadowRoot?.isShadowRoot).toBe(true)
- expect(insideInput?.attributes.value).toBe('***')
- expect(insideTextContent).toBe('field privacy-set-inside: ')
- })
-
- createTest('Change')
- .withRum({
- defaultPrivacyLevel: 'allow',
- enableExperimentalFeatures: ['use_incremental_change_records'],
- })
- .withBody(body)
- .run(async ({ flushEvents, intakeRegistry }) => {
- await flushEvents()
- expect(intakeRegistry.replaySegments).toHaveLength(1)
-
- const records = findChangeRecords(intakeRegistry.replaySegments[0].records)
- expect(decodeChangeRecords(records).at(0)!.data).toEqual([
- [
- ChangeType.AddNode,
- [null, '#document'],
- [1, '#doctype', 'html', '', ''],
- [0, 'HTML'],
- [1, 'HEAD'],
- [0, 'BODY'],
- [1, '#text', '\n '],
- [0, '#text', '\n \n '],
- [0, 'DIV', ['data-dd-privacy', 'mask-user-input']],
- [1, 'MY-INPUT-FIELD', ['id', 'privacy-set-outside']],
- [1, '#shadow-root'],
- [1, 'DIV'],
- [1, 'LABEL', ['id', 'label-privacy-set-outside']],
- [1, '#text', 'field privacy-set-outside: '],
- [3, 'INPUT', ['id', 'input-privacy-set-outside'], ['value', '***']],
- [10, '#text', '\n '],
- [0, 'MY-INPUT-FIELD', ['privacy', 'mask-user-input'], ['id', 'privacy-set-inside']],
- [1, '#text', '\n '],
- [0, '#shadow-root'],
- [1, 'DIV', ['data-dd-privacy', 'mask-user-input']],
- [1, 'LABEL', ['id', 'label-privacy-set-inside']],
- [1, '#text', 'field privacy-set-inside: '],
- [3, 'INPUT', ['id', 'input-privacy-set-inside'], ['value', '***']],
- ],
- [ChangeType.ScrollPosition, [0, 0, 0]],
- ])
- })
- })
-
- test.describe('can record click with target from inside the shadow root', () => {
- function createTestVariation(name: string, enableExperimentalFeatures: string[]): void {
- createTest(name)
- .withRum({ enableExperimentalFeatures })
- .withBody(html`
- ${divShadowDom}
-
- `)
- .run(async ({ flushEvents, intakeRegistry, page }) => {
- const div = page.locator('my-div #shadow-child')
- await div.click()
- await flushEvents()
- expect(intakeRegistry.replaySegments).toHaveLength(1)
-
- const fullSnapshot = findFullSnapshot({ records: intakeRegistry.replayRecords })!
- const elementIds = getElementIdsFromFullSnapshot(fullSnapshot)
- const shadowChildId = elementIds.get('shadow-child')
-
- const mouseInteraction = findMouseInteractionRecords(
- intakeRegistry.replaySegments[0],
- MouseInteractionType.Click
- )[0]
- expect(mouseInteraction).toBeTruthy()
- expect((mouseInteraction.data as MouseInteractionData).id).toBe(shadowChildId)
- })
- }
-
- createTestVariation('V1', [])
- createTestVariation('Change', ['use_incremental_change_records'])
- })
-
- test.describe('can record mutation from inside the shadow root', () => {
- const body = html`
+ `)
+ .run(async ({ flushEvents, intakeRegistry }) => {
+ await flushEvents()
+ expect(intakeRegistry.replaySegments).toHaveLength(1)
+
+ const records = findChangeRecords(intakeRegistry.replaySegments[0].records)
+ expect(decodeChangeRecords(records).at(0)!.data).toEqual([
+ [
+ ChangeType.AddNode,
+ [null, '#document'],
+ [1, '#doctype', 'html', '', ''],
+ [0, 'HTML'],
+ [1, 'HEAD'],
+ [0, 'BODY'],
+ [1, '#text', '\n '],
+ [0, '#text', '\n \n '],
+ [0, 'DIV', ['data-dd-privacy', 'mask-user-input']],
+ [1, 'MY-INPUT-FIELD', ['id', 'privacy-set-outside']],
+ [1, '#shadow-root'],
+ [1, 'DIV'],
+ [1, 'LABEL', ['id', 'label-privacy-set-outside']],
+ [1, '#text', 'field privacy-set-outside: '],
+ [3, 'INPUT', ['id', 'input-privacy-set-outside'], ['value', '***']],
+ [10, '#text', '\n '],
+ [0, 'MY-INPUT-FIELD', ['privacy', 'mask-user-input'], ['id', 'privacy-set-inside']],
+ [1, '#text', '\n '],
+ [0, '#shadow-root'],
+ [1, 'DIV', ['data-dd-privacy', 'mask-user-input']],
+ [1, 'LABEL', ['id', 'label-privacy-set-inside']],
+ [1, '#text', 'field privacy-set-inside: '],
+ [3, 'INPUT', ['id', 'input-privacy-set-inside'], ['value', '***']],
+ ],
+ [ChangeType.ScrollPosition, [0, 0, 0]],
+ ])
+ })
+
+ createTest('can record click with target from inside the shadow root')
+ .withRum()
+ .withBody(html`
+ ${divShadowDom}
+
+ `)
+ .run(async ({ flushEvents, intakeRegistry, page }) => {
+ const div = page.locator('my-div #shadow-child')
+ await div.click()
+ await flushEvents()
+ expect(intakeRegistry.replaySegments).toHaveLength(1)
+
+ const fullSnapshot = findFullSnapshot({ records: intakeRegistry.replayRecords })!
+ const elementIds = getElementIdsFromFullSnapshot(fullSnapshot)
+ const shadowChildId = elementIds.get('shadow-child')
+
+ const mouseInteraction = findMouseInteractionRecords(
+ intakeRegistry.replaySegments[0],
+ MouseInteractionType.Click
+ )[0]
+ expect(mouseInteraction).toBeTruthy()
+ expect((mouseInteraction.data as MouseInteractionData).id).toBe(shadowChildId)
+ })
+
+ createTest('can record mutation from inside the shadow root')
+ .withRum({ defaultPrivacyLevel: 'allow' })
+ .withBody(html`
${divShadowDom}
- `
-
- const mutate = () => {
- const host = document.body.querySelector('#host') as HTMLElement
- const div = host.shadowRoot!.querySelector('div') as HTMLElement
- div.innerText = 'titi'
- }
-
- createTest('V1')
- .withRum({
- defaultPrivacyLevel: 'allow',
- })
- .withBody(body)
- .run(async ({ flushEvents, intakeRegistry, page }) => {
- await page.evaluate(mutate)
- await flushEvents()
- expect(intakeRegistry.replaySegments).toHaveLength(1)
-
- const { validate, expectInitialNode, expectNewNode } = createMutationPayloadValidatorFromSegment(
- intakeRegistry.replaySegments[0],
- { expect }
- )
- validate({
- adds: [
- {
- parent: expectInitialNode({ tag: 'div' }),
- node: expectNewNode({ type: NodeType.Text, textContent: 'titi' }),
- },
- ],
- removes: [
- {
- parent: expectInitialNode({ tag: 'div' }),
- node: expectInitialNode({ text: 'toto' }),
- },
- ],
- })
+ `)
+ .run(async ({ flushEvents, intakeRegistry, page }) => {
+ await page.evaluate(() => {
+ const host = document.body.querySelector('#host') as HTMLElement
+ const div = host.shadowRoot!.querySelector('div') as HTMLElement
+ div.innerText = 'titi'
})
-
- createTest('Change')
- .withRum({
- defaultPrivacyLevel: 'allow',
- enableExperimentalFeatures: ['use_incremental_change_records'],
- })
- .withBody(body)
- .run(async ({ flushEvents, intakeRegistry, page }) => {
- await page.evaluate(mutate)
- await flushEvents()
- expect(intakeRegistry.replaySegments).toHaveLength(1)
-
- const records = findChangeRecords(intakeRegistry.replaySegments[0].records)
- expect(decodeChangeRecords(records).at(-1)!.data).toEqual([
- [ChangeType.AddNode, [2, '#text', 'titi']],
- [ChangeType.RemoveNode, 11],
- ])
- })
- })
-
- test.describe('can record scroll from inside the shadow root', () => {
- function createTestVariation(name: string, enableExperimentalFeatures: string[]): void {
- createTest(name)
- .withRum({ enableExperimentalFeatures })
- .withBody(html`
- ${scrollableDivShadowDom}
-
- `)
- .run(async ({ flushEvents, intakeRegistry, page }) => {
- const button = page.locator('my-scrollable-div button')
-
- // Triggering scrollTo from the test itself is not allowed
- // Thus, a callback to scroll the div was added to the button 'click' event
- await button.click()
-
- await flushEvents()
- expect(intakeRegistry.replaySegments).toHaveLength(1)
-
- const firstSegment = intakeRegistry.replaySegments[0]
- const fullSnapshot = findFullSnapshot(firstSegment)!
- const elementIds = getElementIdsFromFullSnapshot(fullSnapshot)
-
- const divId = elementIds.get('scrollable-div')
- expect(divId).not.toBeUndefined()
-
- const scrollRecord = findIncrementalSnapshot(firstSegment, IncrementalSource.Scroll)
- expect(scrollRecord).toBeTruthy()
-
- const scrollData = scrollRecord?.data as ScrollData
- expect(scrollData.id).toBe(divId)
- expect(scrollData.y).toBe(250)
- })
- }
-
- createTestVariation('V1', [])
- createTestVariation('Change', ['use_incremental_change_records'])
- })
+ await flushEvents()
+ expect(intakeRegistry.replaySegments).toHaveLength(1)
+
+ const records = findChangeRecords(intakeRegistry.replaySegments[0].records)
+ expect(decodeChangeRecords(records).at(-1)!.data).toEqual([
+ [ChangeType.AddNode, [2, '#text', 'titi']],
+ [ChangeType.RemoveNode, 11],
+ ])
+ })
+
+ createTest('can record scroll from inside the shadow root')
+ .withRum()
+ .withBody(html`
+ ${scrollableDivShadowDom}
+
+ `)
+ .run(async ({ flushEvents, intakeRegistry, page }) => {
+ const button = page.locator('my-scrollable-div button')
+
+ // Triggering scrollTo from the test itself is not allowed
+ // Thus, a callback to scroll the div was added to the button 'click' event
+ await button.click()
+
+ await flushEvents()
+ expect(intakeRegistry.replaySegments).toHaveLength(1)
+
+ const firstSegment = intakeRegistry.replaySegments[0]
+ const fullSnapshot = findFullSnapshot(firstSegment)!
+ const elementIds = getElementIdsFromFullSnapshot(fullSnapshot)
+
+ const divId = elementIds.get('scrollable-div')
+ expect(divId).not.toBeUndefined()
+
+ const scrollRecord = findIncrementalSnapshot(firstSegment, IncrementalSource.Scroll)
+ expect(scrollRecord).toBeTruthy()
+
+ const scrollData = scrollRecord?.data as ScrollData
+ expect(scrollData.id).toBe(divId)
+ expect(scrollData.y).toBe(250)
+ })
})
-function findElementsInShadowDom(node: SerializedNodeWithId, id: string) {
- const shadowHost = findElementWithIdAttribute(node, id)
- expect(shadowHost).toBeTruthy()
-
- const shadowRoot = shadowHost!.childNodes.find(
- (node) => node.type === NodeType.DocumentFragment && node.isShadowRoot
- ) as DocumentFragmentNode
- expect(shadowRoot).toBeTruthy()
-
- const input = findElementWithIdAttribute(node, `input-${id}`)
- expect(input).toBeTruthy()
-
- const text = findElementWithIdAttribute(node, `label-${id}`)!
- expect(text).toBeTruthy()
- const textContent = findTextContent(text)
- expect(textContent).toBeTruthy()
- return { shadowHost, shadowRoot, input, text, textContent }
+async function skipIfAdoptedStyleSheetsNotSupported(page: Page): Promise {
+ const isAdoptedStyleSheetsSupported = await page.evaluate(() => document.adoptedStyleSheets !== undefined)
+ test.skip(!isAdoptedStyleSheetsSupported, 'adoptedStyleSheets is not supported in this browser')
}