import { isIE } from '@datadog/browser-core' import type { RumConfiguration } from '@datadog/browser-rum-core' import { DEFAULT_PROGRAMMATIC_ACTION_NAME_ATTRIBUTE } from '@datadog/browser-rum-core' import { NodePrivacyLevel, PRIVACY_ATTR_NAME, PRIVACY_ATTR_VALUE_ALLOW, PRIVACY_ATTR_VALUE_HIDDEN, PRIVACY_ATTR_VALUE_MASK, PRIVACY_ATTR_VALUE_MASK_USER_INPUT, } from '../../constants' import { HTML, AST_ALLOW, AST_HIDDEN, AST_MASK, AST_MASK_USER_INPUT, generateLeanSerializedDoc, } from '../../../test/htmlAst' import type { ElementNode, SerializedNodeWithId, TextNode } from '../../types' import { NodeType } from '../../types' import { hasSerializedNode } from './serializationUtils' import type { SerializeOptions } from './serialize' import { serializeDocument, serializeNodeWithId, serializeDocumentNode, serializeChildNodes, serializeAttribute, SerializationContextStatus, } from './serialize' import { MAX_ATTRIBUTE_VALUE_CHAR_LENGTH } from './privacy' import type { ElementsScrollPositions } from './elementsScrollPositions' import { createElementsScrollPositions } from './elementsScrollPositions' const DEFAULT_CONFIGURATION = {} as RumConfiguration const DEFAULT_SERIALIZATION_CONTEXT = { status: SerializationContextStatus.INITIAL_FULL_SNAPSHOT, elementsScrollPositions: createElementsScrollPositions(), } const DEFAULT_OPTIONS: SerializeOptions = { parentNodePrivacyLevel: NodePrivacyLevel.ALLOW, serializationContext: DEFAULT_SERIALIZATION_CONTEXT, configuration: DEFAULT_CONFIGURATION, } describe('serializeNodeWithId', () => { let sandbox: HTMLElement beforeEach(() => { if (isIE()) { pending('IE not supported') } sandbox = document.createElement('div') sandbox.id = 'sandbox' document.body.appendChild(sandbox) }) afterEach(() => { sandbox.remove() }) describe('document serialization', () => { it('serializes a document', () => { const document = new DOMParser().parseFromString('foo', 'text/html') expect(serializeDocument(document, DEFAULT_CONFIGURATION, DEFAULT_SERIALIZATION_CONTEXT)).toEqual({ type: NodeType.Document, childNodes: [ jasmine.objectContaining({ type: NodeType.DocumentType, name: 'html', publicId: '', systemId: '' }), jasmine.objectContaining({ type: NodeType.Element, tagName: 'html' }), ], id: jasmine.any(Number) as unknown as number, }) }) }) describe('elements serialization', () => { it('serializes a div', () => { expect(serializeNodeWithId(document.createElement('div'), DEFAULT_OPTIONS)).toEqual({ type: NodeType.Element, tagName: 'div', attributes: {}, isSVG: undefined, childNodes: [], id: jasmine.any(Number) as unknown as number, }) }) it('serializes hidden elements', () => { const element = document.createElement('div') element.setAttribute(PRIVACY_ATTR_NAME, PRIVACY_ATTR_VALUE_HIDDEN) expect(serializeNodeWithId(element, DEFAULT_OPTIONS)).toEqual({ type: NodeType.Element, tagName: 'div', attributes: { rr_width: '0px', rr_height: '0px', [PRIVACY_ATTR_NAME]: PRIVACY_ATTR_VALUE_HIDDEN, }, isSVG: undefined, childNodes: [], id: jasmine.any(Number) as unknown as number, }) }) it('does not serialize hidden element children', () => { const element = document.createElement('div') element.setAttribute(PRIVACY_ATTR_NAME, PRIVACY_ATTR_VALUE_HIDDEN) element.appendChild(document.createElement('hr')) expect((serializeNodeWithId(element, DEFAULT_OPTIONS)! as ElementNode).childNodes).toEqual([]) }) it('serializes attributes', () => { const element = document.createElement('div') element.setAttribute('foo', 'bar') element.setAttribute('data-foo', 'data-bar') element.className = 'zog' element.style.width = '10px' expect((serializeNodeWithId(element, DEFAULT_OPTIONS)! as ElementNode).attributes).toEqual({ foo: 'bar', 'data-foo': 'data-bar', class: 'zog', style: 'width: 10px;', }) }) describe('rr scroll attributes', () => { let element: HTMLDivElement let elementsScrollPositions: ElementsScrollPositions beforeEach(() => { element = document.createElement('div') Object.assign(element.style, { width: '100px', height: '100px', overflow: 'scroll' }) const inner = document.createElement('div') Object.assign(inner.style, { width: '200px', height: '200px' }) element.appendChild(inner) sandbox.appendChild(element) element.scrollBy(10, 20) elementsScrollPositions = createElementsScrollPositions() }) it('should be retrieved from attributes during initial full snapshot', () => { const serializedAttributes = ( serializeNodeWithId(element, { ...DEFAULT_OPTIONS, serializationContext: { status: SerializationContextStatus.INITIAL_FULL_SNAPSHOT, elementsScrollPositions, }, }) as ElementNode ).attributes expect(serializedAttributes).toEqual( jasmine.objectContaining({ rr_scrollLeft: 10, rr_scrollTop: 20, }) ) expect(elementsScrollPositions.get(element)).toEqual({ scrollLeft: 10, scrollTop: 20 }) }) it('should not be retrieved from attributes during subsequent full snapshot', () => { const serializedAttributes = ( serializeNodeWithId(element, { ...DEFAULT_OPTIONS, serializationContext: { status: SerializationContextStatus.SUBSEQUENT_FULL_SNAPSHOT, elementsScrollPositions, }, }) as ElementNode ).attributes expect(serializedAttributes.rr_scrollLeft).toBeUndefined() expect(serializedAttributes.rr_scrollTop).toBeUndefined() expect(elementsScrollPositions.get(element)).toBeUndefined() }) it('should be retrieved from elementsScrollPositions during subsequent full snapshot', () => { elementsScrollPositions.set(element, { scrollLeft: 10, scrollTop: 20 }) const serializedAttributes = ( serializeNodeWithId(element, { ...DEFAULT_OPTIONS, serializationContext: { status: SerializationContextStatus.SUBSEQUENT_FULL_SNAPSHOT, elementsScrollPositions, }, }) as ElementNode ).attributes expect(serializedAttributes).toEqual( jasmine.objectContaining({ rr_scrollLeft: 10, rr_scrollTop: 20, }) ) }) it('should not be retrieved during mutation', () => { elementsScrollPositions.set(element, { scrollLeft: 10, scrollTop: 20 }) const serializedAttributes = ( serializeNodeWithId(element, { ...DEFAULT_OPTIONS, serializationContext: { status: SerializationContextStatus.MUTATION, }, }) as ElementNode ).attributes expect(serializedAttributes.rr_scrollLeft).toBeUndefined() expect(serializedAttributes.rr_scrollTop).toBeUndefined() }) }) it('ignores white space in ', () => { const head = document.createElement('head') head.innerHTML = ' foo ' expect((serializeNodeWithId(head, DEFAULT_OPTIONS)! as ElementNode).childNodes).toEqual([ jasmine.objectContaining({ type: NodeType.Element, tagName: 'title', childNodes: [jasmine.objectContaining({ type: NodeType.Text, textContent: ' foo ' })], }), ]) }) it('serializes text elements value', () => { const input = document.createElement('input') input.value = 'toto' expect(serializeNodeWithId(input, DEFAULT_OPTIONS)! as ElementNode).toEqual( jasmine.objectContaining({ attributes: { value: 'toto' }, }) ) }) it('serializes