@@ -7863,6 +7863,248 @@ internal class RumViewScopeTest {
7863
7863
7864
7864
// endregion
7865
7865
7866
+ // region External Refresh Rate
7867
+
7868
+ @Test
7869
+ fun `M send update W handleEvent(UpdateExternalRefreshRate+KeepAlive) { single value }` (
7870
+ forge : Forge
7871
+ ) {
7872
+ // GIVEN
7873
+ val frameTimeNanos = forge.aLong(min = 1_000_000L , max = 50_000_000L ) // 1ms to 50ms
7874
+ val expectedRefreshRate = 1_000_000_000.0 / frameTimeNanos.toDouble()
7875
+
7876
+ // WHEN
7877
+ testedScope.handleEvent(
7878
+ RumRawEvent .UpdateExternalRefreshRate (frameTimeNanos),
7879
+ mockWriter
7880
+ )
7881
+ val result = testedScope.handleEvent(
7882
+ RumRawEvent .KeepAlive (),
7883
+ mockWriter
7884
+ )
7885
+
7886
+ // THEN
7887
+ argumentCaptor<ViewEvent > {
7888
+ verify(mockWriter).write(eq(mockEventBatchWriter), capture(), eq(EventType .DEFAULT ))
7889
+ assertThat(lastValue)
7890
+ .hasRefreshRateMetric(expectedRefreshRate, expectedRefreshRate)
7891
+ }
7892
+ verifyNoMoreInteractions(mockWriter)
7893
+ assertThat(result).isSameAs(testedScope)
7894
+ }
7895
+
7896
+ @Test
7897
+ fun `M send update W handleEvent(UpdateExternalRefreshRate+KeepAlive) { multiple values }` (
7898
+ forge : Forge
7899
+ ) {
7900
+ // GIVEN
7901
+ val frameTimesNanos = forge.aList(size = 5 ) {
7902
+ aLong(min = 8_000_000L , max = 20_000_000L ) // ~50-125 FPS range
7903
+ }
7904
+
7905
+ var sum = 0.0
7906
+ var min = Double .MAX_VALUE
7907
+ var max = - Double .MAX_VALUE
7908
+ val refreshRates = mutableListOf<Double >()
7909
+
7910
+ // WHEN
7911
+ frameTimesNanos.forEach { frameTime ->
7912
+ val refreshRate = 1_000_000_000.0 / frameTime.toDouble()
7913
+ refreshRates.add(refreshRate)
7914
+ sum + = refreshRate
7915
+ min = kotlin.math.min(min, refreshRate)
7916
+ max = kotlin.math.max(max, refreshRate)
7917
+
7918
+ testedScope.handleEvent(
7919
+ RumRawEvent .UpdateExternalRefreshRate (frameTime),
7920
+ mockWriter
7921
+ )
7922
+ }
7923
+
7924
+ val result = testedScope.handleEvent(
7925
+ RumRawEvent .KeepAlive (),
7926
+ mockWriter
7927
+ )
7928
+
7929
+ // THEN
7930
+ val expectedAverage = sum / refreshRates.size
7931
+ argumentCaptor<ViewEvent > {
7932
+ verify(mockWriter).write(eq(mockEventBatchWriter), capture(), eq(EventType .DEFAULT ))
7933
+ assertThat(lastValue)
7934
+ .hasRefreshRateMetric(expectedAverage, min)
7935
+ }
7936
+ verifyNoMoreInteractions(mockWriter)
7937
+ assertThat(result).isSameAs(testedScope)
7938
+ }
7939
+
7940
+ @Test
7941
+ fun `M ignore invalid frame time W handleEvent(UpdateExternalRefreshRate+KeepAlive) { zero frame time }` () {
7942
+ // WHEN
7943
+ testedScope.handleEvent(
7944
+ RumRawEvent .UpdateExternalRefreshRate (0L ),
7945
+ mockWriter
7946
+ )
7947
+ val result = testedScope.handleEvent(
7948
+ RumRawEvent .KeepAlive (),
7949
+ mockWriter
7950
+ )
7951
+
7952
+ // THEN
7953
+ argumentCaptor<ViewEvent > {
7954
+ verify(mockWriter).write(eq(mockEventBatchWriter), capture(), eq(EventType .DEFAULT ))
7955
+ assertThat(lastValue)
7956
+ .hasRefreshRateMetric(null , null )
7957
+ }
7958
+ verifyNoMoreInteractions(mockWriter)
7959
+ assertThat(result).isSameAs(testedScope)
7960
+ }
7961
+
7962
+ @Test
7963
+ fun `M ignore invalid frame time W handleEvent(UpdateExternalRefreshRate+KeepAlive) { negative frame time }` (
7964
+ forge : Forge
7965
+ ) {
7966
+ // GIVEN
7967
+ val negativeFrameTime = - forge.aLong(min = 1L , max = 1_000_000L )
7968
+
7969
+ // WHEN
7970
+ testedScope.handleEvent(
7971
+ RumRawEvent .UpdateExternalRefreshRate (negativeFrameTime),
7972
+ mockWriter
7973
+ )
7974
+ val result = testedScope.handleEvent(
7975
+ RumRawEvent .KeepAlive (),
7976
+ mockWriter
7977
+ )
7978
+
7979
+ // THEN
7980
+ argumentCaptor<ViewEvent > {
7981
+ verify(mockWriter).write(eq(mockEventBatchWriter), capture(), eq(EventType .DEFAULT ))
7982
+ assertThat(lastValue)
7983
+ .hasRefreshRateMetric(null , null )
7984
+ }
7985
+ verifyNoMoreInteractions(mockWriter)
7986
+ assertThat(result).isSameAs(testedScope)
7987
+ }
7988
+
7989
+ @Test
7990
+ fun `M prioritize external data W handleEvent(UpdateExternalRefreshRate+VitalUpdate+KeepAlive)` (
7991
+ forge : Forge
7992
+ ) {
7993
+ // GIVEN
7994
+ val externalFrameTime = forge.aLong(min = 16_000_000L , max = 17_000_000L ) // ~60 FPS
7995
+ val expectedExternalRefreshRate = 1_000_000_000.0 / externalFrameTime.toDouble()
7996
+
7997
+ val internalRefreshRate = forge.aDouble(min = 30.0 , max = 45.0 ) // Different range
7998
+ val listenerCaptor = argumentCaptor<VitalListener > {
7999
+ verify(mockFrameRateVitalMonitor).register(capture())
8000
+ }
8001
+ val vitalListener = listenerCaptor.firstValue
8002
+
8003
+ // WHEN - Add external refresh rate data
8004
+ testedScope.handleEvent(
8005
+ RumRawEvent .UpdateExternalRefreshRate (externalFrameTime),
8006
+ mockWriter
8007
+ )
8008
+
8009
+ // AND - Add internal refresh rate data (should be ignored)
8010
+ vitalListener.onVitalUpdate(VitalInfo (1 , internalRefreshRate, internalRefreshRate, internalRefreshRate))
8011
+
8012
+ val result = testedScope.handleEvent(
8013
+ RumRawEvent .KeepAlive (),
8014
+ mockWriter
8015
+ )
8016
+
8017
+ // THEN - Should use external data, not internal
8018
+ argumentCaptor<ViewEvent > {
8019
+ verify(mockWriter).write(eq(mockEventBatchWriter), capture(), eq(EventType .DEFAULT ))
8020
+ assertThat(lastValue)
8021
+ .hasRefreshRateMetric(expectedExternalRefreshRate, expectedExternalRefreshRate)
8022
+ }
8023
+ verifyNoMoreInteractions(mockWriter)
8024
+ assertThat(result).isSameAs(testedScope)
8025
+ }
8026
+
8027
+ @Test
8028
+ fun `M fallback to internal data W no external data provided` (
8029
+ forge : Forge
8030
+ ) {
8031
+ // GIVEN
8032
+ val internalRefreshRate = forge.aDouble(min = 55.0 , max = 60.0 )
8033
+ val listenerCaptor = argumentCaptor<VitalListener > {
8034
+ verify(mockFrameRateVitalMonitor).register(capture())
8035
+ }
8036
+ val vitalListener = listenerCaptor.firstValue
8037
+
8038
+ // WHEN - Only add internal refresh rate data (no external data)
8039
+ vitalListener.onVitalUpdate(VitalInfo (1 , internalRefreshRate, internalRefreshRate, internalRefreshRate))
8040
+
8041
+ val result = testedScope.handleEvent(
8042
+ RumRawEvent .KeepAlive (),
8043
+ mockWriter
8044
+ )
8045
+
8046
+ // THEN - Should use internal data
8047
+ argumentCaptor<ViewEvent > {
8048
+ verify(mockWriter).write(eq(mockEventBatchWriter), capture(), eq(EventType .DEFAULT ))
8049
+ assertThat(lastValue)
8050
+ .hasRefreshRateMetric(internalRefreshRate, internalRefreshRate)
8051
+ }
8052
+ verifyNoMoreInteractions(mockWriter)
8053
+ assertThat(result).isSameAs(testedScope)
8054
+ }
8055
+
8056
+ @Test
8057
+ fun `M not update external refresh rate W view is stopped` (
8058
+ forge : Forge
8059
+ ) {
8060
+ // GIVEN
8061
+ testedScope.handleEvent(RumRawEvent .StopView (fakeKey, emptyMap()), mockWriter)
8062
+ val frameTimeNanos = forge.aLong(min = 16_000_000L , max = 17_000_000L )
8063
+
8064
+ // WHEN
8065
+ val result = testedScope.handleEvent(
8066
+ RumRawEvent .UpdateExternalRefreshRate (frameTimeNanos),
8067
+ mockWriter
8068
+ )
8069
+
8070
+ // THEN
8071
+ // Should not process external refresh rate updates after view is stopped
8072
+ assertThat(result).isNull() // View scope should be completed
8073
+ }
8074
+
8075
+ @Test
8076
+ fun `M accumulate external refresh rate samples correctly W multiple updates` () {
8077
+ // GIVEN
8078
+ val frameTime1 = 16_666_667L // 60 FPS
8079
+ val frameTime2 = 33_333_333L // 30 FPS
8080
+ val frameTime3 = 11_111_111L // 90 FPS
8081
+
8082
+ val refreshRate1 = 1_000_000_000.0 / frameTime1.toDouble()
8083
+ val refreshRate2 = 1_000_000_000.0 / frameTime2.toDouble()
8084
+ val refreshRate3 = 1_000_000_000.0 / frameTime3.toDouble()
8085
+
8086
+ val expectedAverage = (refreshRate1 + refreshRate2 + refreshRate3) / 3.0
8087
+ val expectedMin = kotlin.math.min(refreshRate2, kotlin.math.min(refreshRate1, refreshRate3))
8088
+
8089
+ // WHEN
8090
+ testedScope.handleEvent(RumRawEvent .UpdateExternalRefreshRate (frameTime1), mockWriter)
8091
+ testedScope.handleEvent(RumRawEvent .UpdateExternalRefreshRate (frameTime2), mockWriter)
8092
+ testedScope.handleEvent(RumRawEvent .UpdateExternalRefreshRate (frameTime3), mockWriter)
8093
+
8094
+ val result = testedScope.handleEvent(RumRawEvent .KeepAlive (), mockWriter)
8095
+
8096
+ // THEN
8097
+ argumentCaptor<ViewEvent > {
8098
+ verify(mockWriter).write(eq(mockEventBatchWriter), capture(), eq(EventType .DEFAULT ))
8099
+ assertThat(lastValue)
8100
+ .hasRefreshRateMetric(expectedAverage, expectedMin)
8101
+ }
8102
+ verifyNoMoreInteractions(mockWriter)
8103
+ assertThat(result).isSameAs(testedScope)
8104
+ }
8105
+
8106
+ // endregion
8107
+
7866
8108
// region Internal attributes
7867
8109
7868
8110
@Test
0 commit comments