import { noop } from '@openobserve/browser-core' import type { RumConfiguration, BrowserWindow } from '@openobserve/browser-rum-core' import { isAdoptedStyleSheetsSupported, registerCleanupTask } from '@openobserve/browser-core/test' import { NodePrivacyLevel, PRIVACY_ATTR_NAME, PRIVACY_ATTR_VALUE_ALLOW, PRIVACY_ATTR_VALUE_HIDDEN, PRIVACY_ATTR_VALUE_MASK, PRIVACY_ATTR_VALUE_MASK_USER_INPUT, PRIVACY_ATTR_VALUE_MASK_UNLESS_ALLOWLISTED, isAllowlisted, } from '@openobserve/browser-rum-core' import type { ElementNode, SerializedNodeWithId } from '../../../types' import { NodeType } from '../../../types' import { appendElement } from '../../../../../rum-core/test' import type { ElementsScrollPositions } from '../elementsScrollPositions' import { createElementsScrollPositions } from '../elementsScrollPositions' import type { ShadowRootCallBack, ShadowRootsController } from '../shadowRootsController' import { createNodeIds } from '../nodeIds' import { HTML, generateLeanSerializedDoc, AST_HIDDEN, AST_MASK, AST_MASK_USER_INPUT, AST_MASK_UNLESS_ALLOWLISTED, AST_ALLOW, } from './htmlAst.specHelper' import { serializeDocument } from './serializeDocument' import type { SerializationContext, SerializeOptions } from './serialization.types' import { SerializationContextStatus } from './serialization.types' import { serializeChildNodes, serializeDocumentNode, serializeNodeWithId } from './serializeNode' import type { SerializationScope } from './serializationScope' import { createSerializationScope } from './serializationScope' import { createSerializationStats } from './serializationStats' const DEFAULT_CONFIGURATION = {} as RumConfiguration const DEFAULT_SHADOW_ROOT_CONTROLLER: ShadowRootsController = { flush: noop, stop: noop, addShadowRoot: noop, removeShadowRoot: noop, } function getDefaultSerializationContext(): SerializationContext { return { serializationStats: createSerializationStats(), shadowRootsController: DEFAULT_SHADOW_ROOT_CONTROLLER, status: SerializationContextStatus.INITIAL_FULL_SNAPSHOT, elementsScrollPositions: createElementsScrollPositions(), } } describe('serializeNodeWithId', () => { let addShadowRootSpy: jasmine.Spy let scope: SerializationScope const getDefaultOptions = (): SerializeOptions => ({ serializationContext: getDefaultSerializationContext(), configuration: DEFAULT_CONFIGURATION, scope, }) beforeEach(() => { addShadowRootSpy = jasmine.createSpy() scope = createSerializationScope(createNodeIds()) }) describe('document serialization', () => { it('serializes a document', () => { const document = new DOMParser().parseFromString('foo', 'text/html') expect(serializeDocument(document, DEFAULT_CONFIGURATION, scope, getDefaultSerializationContext())).toEqual({ type: NodeType.Document, childNodes: [ jasmine.objectContaining({ type: NodeType.DocumentType, name: 'html', publicId: '', systemId: '' }), jasmine.objectContaining({ type: NodeType.Element, tagName: 'html' }), ], adoptedStyleSheets: undefined, id: jasmine.any(Number) as unknown as number, }) }) }) describe('elements serialization', () => { function serializeElement( node: Element, options: SerializeOptions | undefined = undefined ): (ElementNode & { id: number }) | null { return serializeNodeWithId(node, NodePrivacyLevel.ALLOW, options ?? getDefaultOptions()) as | (ElementNode & { id: number }) | null } it('serializes a div', () => { expect(serializeElement(document.createElement('div'))).toEqual({ type: NodeType.Element, tagName: 'div', attributes: {}, isSVG: undefined, childNodes: [], id: jasmine.any(Number), }) }) it('serializes hidden elements', () => { const element = document.createElement('div') element.setAttribute(PRIVACY_ATTR_NAME, PRIVACY_ATTR_VALUE_HIDDEN) expect(serializeElement(element)).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), }) }) 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(serializeElement(element)!.childNodes).toEqual([]) }) it('serializes attributes', () => { const element = appendElement('
') element.className = 'zog' element.style.width = '10px' expect(serializeElement(element)!.attributes).toEqual({ foo: 'bar', 'data-foo': 'data-bar', class: 'zog', style: 'width: 10px;', }) }) describe('rr scroll attributes', () => { let element: HTMLElement let elementsScrollPositions: ElementsScrollPositions beforeEach(() => { element = appendElement( '
' ) element.scrollBy(10, 20) elementsScrollPositions = createElementsScrollPositions() }) it('should be retrieved from attributes during initial full snapshot', () => { const serializedAttributes = serializeElement(element, { ...getDefaultOptions(), serializationContext: { serializationStats: createSerializationStats(), shadowRootsController: DEFAULT_SHADOW_ROOT_CONTROLLER, status: SerializationContextStatus.INITIAL_FULL_SNAPSHOT, elementsScrollPositions, }, })!.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 = serializeElement(element, { ...getDefaultOptions(), serializationContext: { serializationStats: createSerializationStats(), shadowRootsController: DEFAULT_SHADOW_ROOT_CONTROLLER, status: SerializationContextStatus.SUBSEQUENT_FULL_SNAPSHOT, elementsScrollPositions, }, })!.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 = serializeElement(element, { ...getDefaultOptions(), serializationContext: { serializationStats: createSerializationStats(), shadowRootsController: DEFAULT_SHADOW_ROOT_CONTROLLER, status: SerializationContextStatus.SUBSEQUENT_FULL_SNAPSHOT, elementsScrollPositions, }, })!.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 = serializeElement(element, { ...getDefaultOptions(), serializationContext: { serializationStats: createSerializationStats(), shadowRootsController: DEFAULT_SHADOW_ROOT_CONTROLLER, status: SerializationContextStatus.MUTATION, }, })!.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(serializeElement(head)!.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(serializeElement(input)!).toEqual( jasmine.objectContaining({ attributes: { value: 'toto' }, }) ) }) it('serializes