import type { ViewportResizeData , ScrollData } from '@datadog/browser-rum/cjs/types' ; import { IncrementalSource } from '@datadog/browser-rum/cjs/types' ; import type { RumInitConfiguration } from '@datadog/browser-rum-core' ; import { findAllIncrementalSnapshots , findAllVisualViewports } from '@datadog/browser-rum/test/utils' ; import type { EventRegistry } from '../../lib/framework' ; import { createTest , bundleSetup , html } from '../../lib/framework' ; import { browserExecute } from '../../lib/helpers/browser' ; import { flushEvents } from '../../lib/helpers/flushEvents' ; const NAVBAR_HEIGHT_CHANGE_UPPER_BOUND = 30 ; const VIEWPORT_META_TAGS = `
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="mobile-web-app-capable" content="yes">
<meta name="viewport"
  content="width=device-width, initial-scale=1.0, maximum-scale=2.75, minimum-scale=1.0, user-scalable=yes"
>
` ; describe ( 'recorder' , ( ) => { beforeEach ( ( ) => { if ( isGestureUnsupported ( ) ) { pending ( 'no touch gesture support' ) ; } } ) ; describe ( 'layout viewport properties' , ( ) => { createTest ( 'getWindowWidth/Height should not be affected by pinch zoom' ) . withRum ( ) . withRumInit ( initRumAndStartRecording ) . withSetup ( bundleSetup ) . withBody ( html `${ VIEWPORT_META_TAGS }` ) . run ( async ( { serverEvents } ) => { await buildScrollablePage ( ) ; const { innerWidth , innerHeight } = await getWindowInnerDimensions ( ) ; await performSignificantZoom ( ) ; await browserExecute ( ( ) => { window . dispatchEvent ( new Event ( 'resize' ) ) ; } ) ; const lastViewportResizeData = ( await getLastRecord ( serverEvents , ( segment ) => findAllIncrementalSnapshots ( segment , IncrementalSource . ViewportResize ) ) ) . data as ViewportResizeData ; const scrollbarThicknessCorrection = await getScrollbarThicknessCorrection ( ) ; expectToBeNearby ( lastViewportResizeData . width , innerWidth - scrollbarThicknessCorrection ) ; expectToBeNearby ( lastViewportResizeData . height , innerHeight - scrollbarThicknessCorrection ) ; } ) ; /**
         * window.ScrollX/Y on some devices/browsers are changed by pinch zoom
         * We need to ensure that our measurements are not affected by pinch zoom
         */ createTest ( 'getScrollX/Y should not be affected by pinch scroll' ) . withRum ( ) . withRumInit ( initRumAndStartRecording ) . withSetup ( bundleSetup ) . withBody ( html `${ VIEWPORT_META_TAGS }` ) . run ( async ( { serverEvents } ) => { const VISUAL_SCROLL_DOWN_PX = 60 ; const LAYOUT_SCROLL_AMOUNT = 20 ; await buildScrollablePage ( ) ; await performSignificantZoom ( ) ; await resetWindowScroll ( ) ; const initialVisualViewport = await getVisualViewport ( ) ; const { scrollX : initialScrollX , scrollY : initialScrollY } = await getWindowScroll ( ) ; await visualScrollVerticallyDown ( VISUAL_SCROLL_DOWN_PX ) ; await layoutScrollTo ( LAYOUT_SCROLL_AMOUNT , LAYOUT_SCROLL_AMOUNT ) ; const nextVisualViewport = await getVisualViewport ( ) ; const { scrollX : nextScrollX , scrollY : nextScrollY } = await getWindowScroll ( ) ; const lastScrollData = ( await getLastRecord ( serverEvents , ( segment ) => findAllIncrementalSnapshots ( segment , IncrementalSource . Scroll ) ) ) . data as ScrollData ; const navBarHeightChange = nextVisualViewport . height - initialVisualViewport . height ; expect ( navBarHeightChange ) . toBeLessThanOrEqual ( NAVBAR_HEIGHT_CHANGE_UPPER_BOUND ) ; expectToBeNearby ( lastScrollData . x , initialScrollX + LAYOUT_SCROLL_AMOUNT ) ; expectToBeNearby ( lastScrollData . y , initialScrollY + LAYOUT_SCROLL_AMOUNT ) ; expectToBeNearby ( lastScrollData . x , nextScrollX ) ; expectToBeNearby ( lastScrollData . y , nextScrollY ) ; } ) ; } ) ; describe ( 'visual viewport properties' , ( ) => { createTest ( 'pinch zoom "scroll" event reports visual viewport position' ) . withRum ( ) . withRumInit ( initRumAndStartRecording ) . withSetup ( bundleSetup ) . withBody ( html `${ VIEWPORT_META_TAGS }` ) . run ( async ( { serverEvents } ) => { const VISUAL_SCROLL_DOWN_PX = 100 ; await buildScrollablePage ( ) ; await performSignificantZoom ( ) ; await visualScrollVerticallyDown ( VISUAL_SCROLL_DOWN_PX ) ; const nextVisualViewportDimension = await getVisualViewport ( ) ; const lastVisualViewportRecord = await getLastRecord ( serverEvents , findAllVisualViewports ) ; expectToBeNearby ( lastVisualViewportRecord . data . pageTop , nextVisualViewportDimension . pageTop ) ; } ) ; createTest ( 'pinch zoom "resize" event reports visual viewport scale' ) . withRum ( ) . withRumInit ( initRumAndStartRecording ) . withSetup ( bundleSetup ) . withBody ( html `${ VIEWPORT_META_TAGS }` ) . run ( async ( { serverEvents } ) => { await performSignificantZoom ( ) ; const nextVisualViewportDimension = await getVisualViewport ( ) ; const lastVisualViewportRecord = await getLastRecord ( serverEvents , findAllVisualViewports ) ; expectToBeNearby ( lastVisualViewportRecord . data . scale , nextVisualViewportDimension . scale ) ; } ) ; } ) ; } ) ; function getLastSegment ( serverEvents ) { return serverEvents . sessionReplay [ serverEvents . sessionReplay . length - 1 ] . segment . data ; } function initRumAndStartRecording ( initConfiguration ) { window . DD_RUM ! . init ( initConfiguration ) ; window . DD_RUM ! . startSessionReplayRecording ( ) ; } const isGestureUnsupported = ( ) => { const { browserName , platformName } = browser . capabilities ; return /firefox|safari|msedge/i . test ( browserName ?? '' ) || /windows|linux/i . test ( platformName ?? '' ) ; } ; function expectToBeNearby ( numA , numB ) { const test = Math . abs ( numA - numB ) <= 5 ; if ( ! test ) { expect ( numA ) . toBe ( numB ) ; } } async function pinchZoom ( xChange ) { const xBase = 180 ; const yBase = 180 ; const xOffsetFingerTwo = 25 ; const moveDurationMs = 400 ; const pauseDurationMs = 150 ; const actions = [ { type : 'pointer' , id : 'finger1' , parameters : { pointerType : 'touch' } , actions : [ { type : 'pointerMove' , duration : 0 , x : xBase , y : yBase } , { type : 'pointerDown' , button : 0 } , { type : 'pause' , duration : pauseDurationMs } , { type : 'pointerMove' , duration : moveDurationMs , origin : 'pointer' , x : - xChange , y : 0 } , { type : 'pointerUp' , button : 0 } , ] , } , { type : 'pointer' , id : 'finger2' , parameters : { pointerType : 'touch' } , actions : [ { type : 'pointerMove' , duration : 0 , x : xBase + xOffsetFingerTwo , y : yBase } , { type : 'pointerDown' , button : 0 } , { type : 'pause' , duration : pauseDurationMs } , { type : 'pointerMove' , duration : moveDurationMs , origin : 'pointer' , x : + xChange , y : 0 } , { type : 'pointerUp' , button : 0 } , ] , } , ] ; await driver . performActions ( actions ) ; } async function performSignificantZoom ( ) { const initialVisualViewport = await getVisualViewport ( ) ; await pinchZoom ( 150 ) ; await pinchZoom ( 150 ) ; const nextVisualViewport = await getVisualViewport ( ) ; expect ( initialVisualViewport . scale < nextVisualViewport . scale ) . toBeTruthy ( ) ; } async function visualScrollVerticallyDown ( yChange ) { const xBase = 180 ; const yBase = 180 ; const moveDurationMs = 800 ; const pauseDurationMs = 150 ; const actions = [ { type : 'pointer' , id : 'finger1' , parameters : { pointerType : 'touch' } , actions : [ { type : 'pointerMove' , duration : 0 , x : xBase , y : yBase } , { type : 'pointerDown' , button : 0 } , { type : 'pause' , duration : pauseDurationMs } , { type : 'pointerMove' , duration : moveDurationMs , origin : 'pointer' , x : 0 , y : - yChange } , { type : 'pointerUp' , button : 0 } , ] , } , ] ; await driver . performActions ( actions ) ; } async function buildScrollablePage ( ) { await browserExecute ( ( ) => { document . documentElement . style . setProperty ( 'width' , '5000px' ) ; document . documentElement . style . setProperty ( 'height' , '5000px' ) ; document . documentElement . style . setProperty ( 'margin' , '0px' ) ; document . documentElement . style . setProperty ( 'padding' , '0px' ) ; document . body . style . setProperty ( 'margin' , '0px' ) ; document . body . style . setProperty ( 'padding' , '0px' ) ; document . body . style . setProperty ( 'width' , '5000px' ) ; document . body . style . setProperty ( 'height' , '5000px' ) ; } ) ; } interface VisualViewportData { scale : number ; width : number ; height : number ; offsetLeft : number ; offsetTop : number ; pageLeft : number ; pageTop : number ; } function getVisualViewport ( ) { return browserExecute ( ( ) => { const visual = window . visualViewport || { } ; return { scale : visual . scale , width : visual . width , height : visual . height , offsetLeft : visual . offsetLeft , offsetTop : visual . offsetTop , pageLeft : visual . pageLeft , pageTop : visual . pageTop , } ; } ) as Promise < VisualViewportData > ; } function getWindowScroll ( ) { return browserExecute ( ( ) => ( { scrollX : window . scrollX , scrollY : window . scrollY , } ) ) as Promise < { scrollX : number ; scrollY : number ; } > ; } function getScrollbarThickness ( ) { return browserExecute ( ( ) => { const outer = document . createElement ( 'div' ) ; outer . style . visibility = 'hidden' ; outer . style . overflow = 'scroll' ; ( outer . style as any ) . msOverflowStyle = 'scrollbar' ; document . body . appendChild ( outer ) ; const inner = document . createElement ( 'div' ) ; outer . appendChild ( inner ) ; const scrollbarThickness = outer . offsetWidth - inner . offsetWidth ; document . body . removeChild ( outer ) ; return scrollbarThickness ; } ) as Promise < number > ; } async function getScrollbarThicknessCorrection ( ) { let scrollbarThickness = 0 ; if ( browser . capabilities . browserName === 'chrome' && browser . capabilities . platformName === 'mac os x' ) { scrollbarThickness = await getScrollbarThickness ( ) ; } return scrollbarThickness ; } async function getLastRecord < T > ( serverEvents , filterMethod ) { await flushEvents ( ) ; const segment = getLastSegment ( serverEvents ) ; const foundRecords = filterMethod ( segment ) ; return foundRecords [ foundRecords . length - 1 ] ; } function getWindowInnerDimensions ( ) { return browserExecute ( ( ) => ( { innerWidth : window . innerWidth , innerHeight : window . innerHeight , } ) ) as Promise < { innerWidth : number ; innerHeight : number ; } > ; } async function resetWindowScroll ( ) { await browserExecute ( ( ) => { window . scrollTo ( - 500 , - 500 ) ; } ) ; const { scrollX : nextScrollX , scrollY : nextScrollY } = await getWindowScroll ( ) ; expect ( nextScrollX ) . toBe ( 0 ) ; expect ( nextScrollY ) . toBe ( 0 ) ; } async function layoutScrollTo ( scrollX , scrollY ) { await browser . execute ( ( x , y ) => { window . scrollTo ( x , y ) ; } , scrollX , scrollY ) ; const { scrollX : nextScrollX , scrollY : nextScrollY } = await getWindowScroll ( ) ; expect ( scrollX ) . toBe ( nextScrollX ) ; expect ( scrollY ) . toBe ( nextScrollY ) ; }