@@ -577,6 +577,120 @@ class SentrySessionReplayTests: XCTestCase {
577577 XCTAssertTrue ( SentrySessionReplay . shouldEnableSessionReplay ( environmentChecker: environmentChecker, experimentalOptions: experimentalOptions) )
578578 }
579579
580+ // MARK: - Frame Rate Tests
581+
582+ func testFrameRate_1FPS_takesScreenshotsAtCorrectInterval( ) {
583+ // Arrange
584+ let fixture = Fixture ( )
585+ let options = SentryReplayOptions ( sessionSampleRate: 1 , onErrorSampleRate: 1 )
586+ options. frameRate = 1
587+ let sut = fixture. getSut ( options: options)
588+ sut. start ( rootView: fixture. rootView, fullSession: true )
589+
590+ fixture. screenshotProvider. lastImageCall = nil
591+
592+ // Act & Assert - advance by 0.9 seconds, screenshot should NOT be taken
593+ fixture. dateProvider. advance ( by: 0.9 )
594+ Dynamic ( sut) . newFrame ( nil )
595+ XCTAssertNil ( fixture. screenshotProvider. lastImageCall, " Screenshot should not be taken before 1 second interval " )
596+
597+ // Act & Assert - advance to exactly 1.0 seconds, screenshot SHOULD be taken
598+ fixture. dateProvider. advance ( by: 0.1 )
599+ Dynamic ( sut) . newFrame ( nil )
600+ XCTAssertNotNil ( fixture. screenshotProvider. lastImageCall, " Screenshot should be taken at 1 second interval for 1 FPS " )
601+ }
602+
603+ func testFrameRate_2FPS_takesScreenshotsAtCorrectInterval( ) {
604+ // Arrange
605+ let fixture = Fixture ( )
606+ let options = SentryReplayOptions ( sessionSampleRate: 1 , onErrorSampleRate: 1 )
607+ options. frameRate = 2
608+ let sut = fixture. getSut ( options: options)
609+ sut. start ( rootView: fixture. rootView, fullSession: true )
610+
611+ fixture. screenshotProvider. lastImageCall = nil
612+
613+ // Act & Assert - advance by 0.4 seconds, screenshot should NOT be taken
614+ fixture. dateProvider. advance ( by: 0.4 )
615+ Dynamic ( sut) . newFrame ( nil )
616+ XCTAssertNil ( fixture. screenshotProvider. lastImageCall, " Screenshot should not be taken before 0.5 second interval " )
617+
618+ // Act & Assert - advance to 0.5 seconds, screenshot SHOULD be taken
619+ fixture. dateProvider. advance ( by: 0.1 )
620+ Dynamic ( sut) . newFrame ( nil )
621+ XCTAssertNotNil ( fixture. screenshotProvider. lastImageCall, " Screenshot should be taken at 0.5 second interval for 2 FPS " )
622+
623+ // Act & Assert - reset and test second screenshot
624+ fixture. screenshotProvider. lastImageCall = nil
625+ fixture. dateProvider. advance ( by: 0.4 )
626+ Dynamic ( sut) . newFrame ( nil )
627+ XCTAssertNil ( fixture. screenshotProvider. lastImageCall, " Screenshot should not be taken before another 0.5 seconds " )
628+
629+ fixture. dateProvider. advance ( by: 0.1 )
630+ Dynamic ( sut) . newFrame ( nil )
631+ XCTAssertNotNil ( fixture. screenshotProvider. lastImageCall, " Screenshot should be taken at next 0.5 second interval " )
632+ }
633+
634+ func testFrameRate_10FPS_takesScreenshotsAtCorrectInterval( ) {
635+ // Arrange
636+ let fixture = Fixture ( )
637+ let options = SentryReplayOptions ( sessionSampleRate: 1 , onErrorSampleRate: 1 )
638+ options. frameRate = 10
639+ let sut = fixture. getSut ( options: options)
640+ sut. start ( rootView: fixture. rootView, fullSession: true )
641+
642+ // Expected interval: 1.0 / 10.0 = 0.1 seconds
643+ // Take first screenshot to establish baseline
644+ fixture. dateProvider. advance ( by: 0.1 )
645+ Dynamic ( sut) . newFrame ( nil )
646+ XCTAssertNotNil ( fixture. screenshotProvider. lastImageCall, " First screenshot should be taken " )
647+
648+ fixture. screenshotProvider. lastImageCall = nil
649+
650+ // Act & Assert - advance by 0.09 seconds, screenshot should NOT be taken
651+ fixture. dateProvider. advance ( by: 0.09 )
652+ Dynamic ( sut) . newFrame ( nil )
653+ XCTAssertNil ( fixture. screenshotProvider. lastImageCall, " Screenshot should not be taken before 0.1 second interval " )
654+
655+ // Act & Assert - advance to reach 0.1 second interval, screenshot SHOULD be taken
656+ fixture. dateProvider. advance ( by: 0.01 )
657+ Dynamic ( sut) . newFrame ( nil )
658+ XCTAssertNotNil ( fixture. screenshotProvider. lastImageCall, " Screenshot should be taken at 0.1 second interval for 10 FPS " )
659+ }
660+
661+ func testFrameRate_multipleScreenshots_respectsInterval( ) {
662+ // Arrange
663+ let fixture = Fixture ( )
664+ let options = SentryReplayOptions ( sessionSampleRate: 1 , onErrorSampleRate: 1 )
665+ options. frameRate = 5
666+ let sut = fixture. getSut ( options: options)
667+ sut. start ( rootView: fixture. rootView, fullSession: true )
668+
669+ // Expected interval: 1.0 / 5.0 = 0.2 seconds
670+ var screenshotCount = 0
671+
672+ // Act & Assert - take 5 screenshots over 1 second
673+ // Each screenshot resets the timer, so we need to advance by the full interval each time
674+ for i in 0 ..< 5 {
675+ // Advance by full interval
676+ fixture. dateProvider. advance ( by: 0.2 )
677+ Dynamic ( sut) . newFrame ( nil )
678+
679+ XCTAssertNotNil ( fixture. screenshotProvider. lastImageCall, " Screenshot # \( i + 1 ) should be taken at \( Double ( i + 1 ) * 0.2 ) seconds " )
680+ screenshotCount += 1
681+ fixture. screenshotProvider. lastImageCall = nil
682+
683+ // Advance by less than interval and verify no screenshot
684+ if i < 4 { // Don't test after the last screenshot
685+ fixture. dateProvider. advance ( by: 0.1 )
686+ Dynamic ( sut) . newFrame ( nil )
687+ XCTAssertNil ( fixture. screenshotProvider. lastImageCall, " No screenshot should be taken at \( Double ( i + 1 ) * 0.2 + 0.1 ) seconds " )
688+ }
689+ }
690+
691+ XCTAssertEqual ( screenshotCount, 5 , " Should have taken exactly 5 screenshots in 1 second for 5 FPS " )
692+ }
693+
580694 // MARK: - Helpers
581695
582696 private func assertFullSession( _ sessionReplay: SentrySessionReplay , expected: Bool ) {
0 commit comments