2025-01-27 16:22:49 +00:00
//
// C o p y r i g h t 2 0 2 5 N e w V e c t o r L t d .
//
// S P D X - L i c e n s e - I d e n t i f i e r : A G P L - 3 . 0 - o n l y O R L i c e n s e R e f - E l e m e n t - C o m m e r c i a l
// P l e a s e s e e L I C E N S E f i l e s i n t h e r e p o s i t o r y r o o t f o r f u l l d e t a i l s .
//
@ testable import ElementX
import QuickLook
import XCTest
@ MainActor
class TimelineMediaPreviewDataSourceTests : XCTestCase {
var initialMediaItems : [ EventBasedMessageTimelineItemProtocol ] !
var initialMediaViewStates : [ RoomTimelineItemViewState ] !
let initialItemIndex = 2
var initialPadding = 100
let previewController = QLPreviewController ( )
override func setUp ( ) {
initialMediaItems = newChunk ( )
initialMediaViewStates = initialMediaItems . map { RoomTimelineItemViewState ( item : $0 , groupStyle : . single ) }
}
2025-02-05 13:27:23 +00:00
func testInitialItems ( ) throws -> TimelineMediaPreviewDataSource {
2025-01-27 16:22:49 +00:00
// G i v e n a d a t a s o u r c e b u i l t w i t h t h e i n i t i a l i t e m s .
let dataSource = TimelineMediaPreviewDataSource ( itemViewStates : initialMediaViewStates ,
initialItem : initialMediaItems [ initialItemIndex ] ,
2025-01-29 15:07:23 +00:00
initialPadding : initialPadding ,
paginationState : . initial )
2025-01-27 16:22:49 +00:00
// W h e n t h e p r e v i e w c o n t r o l l e r d i s p l a y s t h e d a t a .
let previewItemCount = dataSource . numberOfPreviewItems ( in : previewController )
2025-02-05 13:27:23 +00:00
let displayedItem = try XCTUnwrap ( dataSource . previewController ( previewController , previewItemAt : dataSource . initialItemIndex ) as ? TimelineMediaPreviewItem . Media ,
" A preview item should be found. " )
2025-01-27 16:22:49 +00:00
// T h e n t h e p r e v i e w c o n t r o l l e r s h o u l d b e s h o w i n g t h e i n i t i a l i t e m a n d t h e d a t a s o u r c e s h o u l d r e f l e c t t h i s .
XCTAssertEqual ( dataSource . initialItemIndex , initialItemIndex + initialPadding , " The initial item index should be padded for the preview controller. " )
2025-02-05 13:27:23 +00:00
XCTAssertEqual ( displayedItem . id , initialMediaItems [ initialItemIndex ] . id . eventOrTransactionID , " The displayed item should be the initial item. " )
XCTAssertEqual ( dataSource . currentMediaItemID , initialMediaItems [ initialItemIndex ] . id . eventOrTransactionID , " The current item should also be the initial item. " )
2025-01-27 16:22:49 +00:00
XCTAssertEqual ( dataSource . previewItems . count , initialMediaViewStates . count , " The initial count of preview items should be correct. " )
XCTAssertEqual ( previewItemCount , initialMediaViewStates . count + ( 2 * initialPadding ) , " The initial item count should be padded for the preview controller. " )
return dataSource
}
2025-02-05 13:27:23 +00:00
func testCurrentUpdateItem ( ) throws {
2025-01-27 16:22:49 +00:00
// G i v e n a d a t a s o u r c e b u i l t w i t h t h e i n i t i a l i t e m s .
2025-01-29 15:07:23 +00:00
let dataSource = TimelineMediaPreviewDataSource ( itemViewStates : initialMediaViewStates ,
initialItem : initialMediaItems [ initialItemIndex ] ,
paginationState : . initial )
2025-01-27 16:22:49 +00:00
// W h e n a d i f f e r e n t i t e m i s d i s p l a y e d .
2025-02-05 13:27:23 +00:00
let previewItem = try XCTUnwrap ( dataSource . previewController ( previewController , previewItemAt : 1 + initialPadding ) as ? TimelineMediaPreviewItem . Media ,
" A preview item should be found. " )
2025-01-29 15:07:23 +00:00
dataSource . updateCurrentItem ( . media ( previewItem ) )
2025-01-27 16:22:49 +00:00
// T h e n t h e d a t a s o u r c e s h o u l d r e f l e c t t h e c h a n g e o f i t e m .
2025-01-29 15:07:23 +00:00
XCTAssertEqual ( dataSource . currentMediaItemID , previewItem . id , " The displayed item should be the initial item. " )
2025-01-27 16:22:49 +00:00
// W h e n a l o a d i n g i t e m i s d i s p l a y e d .
2025-01-29 15:07:23 +00:00
guard let loadingItem = dataSource . previewController ( previewController , previewItemAt : initialPadding - 1 ) as ? TimelineMediaPreviewItem . Loading else {
XCTFail ( " A loading item should be be returned. " )
return
}
dataSource . updateCurrentItem ( . loading ( loadingItem ) )
// T h e n t h e d a t a s o u r c e s h o u l d s h o w a l o a d i n g i t e m
XCTAssertEqual ( dataSource . currentItem , . loading ( loadingItem ) , " The displayed item should be the loading item. " )
2025-01-27 16:22:49 +00:00
}
func testUpdatedItems ( ) async throws {
// G i v e n a d a t a s o u r c e b u i l t w i t h t h e i n i t i a l i t e m s .
2025-02-05 13:27:23 +00:00
let dataSource = try testInitialItems ( )
2025-01-27 16:22:49 +00:00
// W h e n o n e o f t h e i t e m s c h a n g e s b u t n o p a g i n a t i o n h a s o c c u r r e d .
let deferred = deferFailure ( dataSource . previewItemsPaginationPublisher , timeout : 1 ) { _ in true }
dataSource . updatePreviewItems ( itemViewStates : initialMediaViewStates )
// T h e n n o p a g i n a t i o n s h o u l d b e d e t e c t e d a n d n o n e o f t h e d a t a s h o u l d h a v e c h a n g e d .
try await deferred . fulfill ( )
let previewItemCount = dataSource . numberOfPreviewItems ( in : previewController )
2025-02-05 13:27:23 +00:00
let displayedItem = try XCTUnwrap ( dataSource . previewController ( previewController , previewItemAt : dataSource . initialItemIndex ) as ? TimelineMediaPreviewItem . Media )
XCTAssertEqual ( displayedItem . id , initialMediaItems [ initialItemIndex ] . id . eventOrTransactionID , " The displayed item should not change. " )
XCTAssertEqual ( dataSource . currentMediaItemID , initialMediaItems [ initialItemIndex ] . id . eventOrTransactionID , " The current item should not change. " )
2025-01-27 16:22:49 +00:00
XCTAssertEqual ( dataSource . previewItems . count , initialMediaViewStates . count , " The number of items should not change. " )
XCTAssertEqual ( previewItemCount , initialMediaViewStates . count + ( 2 * initialPadding ) , " The padded number of items should not change. " )
}
func testPagination ( ) async throws {
// G i v e n a d a t a s o u r c e b u i l t w i t h t h e i n i t i a l i t e m s .
2025-02-05 13:27:23 +00:00
let dataSource = try testInitialItems ( )
2025-01-27 16:22:49 +00:00
// W h e n m o r e i t e m s a r e l o a d e d i n a b a c k p a g i n a t i o n .
var deferred = deferFulfillment ( dataSource . previewItemsPaginationPublisher ) { _ in true }
let backPaginationChunk = newChunk ( ) . map { RoomTimelineItemViewState ( item : $0 , groupStyle : . single ) }
var newViewStates = backPaginationChunk + initialMediaViewStates
dataSource . updatePreviewItems ( itemViewStates : newViewStates )
// T h e n t h e n e w i t e m s s h o u l d b e a d d e d b u t t h e d i s p l a y e d i t e m s h o u l d n o t c h a n g e o r m o v e i n t h e a r r a y .
try await deferred . fulfill ( )
XCTAssertEqual ( dataSource . previewItems . count , newViewStates . count , " The new items should be added. " )
var previewItemCount = dataSource . numberOfPreviewItems ( in : previewController )
2025-02-05 13:27:23 +00:00
var displayedItem = try XCTUnwrap ( dataSource . previewController ( previewController , previewItemAt : dataSource . initialItemIndex ) as ? TimelineMediaPreviewItem . Media )
XCTAssertEqual ( displayedItem . id , initialMediaItems [ initialItemIndex ] . id . eventOrTransactionID , " The displayed item should not change. " )
XCTAssertEqual ( dataSource . currentMediaItemID , initialMediaItems [ initialItemIndex ] . id . eventOrTransactionID , " The current item should not change. " )
2025-01-27 16:22:49 +00:00
XCTAssertEqual ( previewItemCount , initialMediaViewStates . count + ( 2 * initialPadding ) , " The number of items should not change " )
// W h e n m o r e i t e m s a r e l o a d e d i n a f o r w a r d p a g i n a t i o n o r s y n c .
deferred = deferFulfillment ( dataSource . previewItemsPaginationPublisher ) { _ in true }
let forwardPaginationChunk = newChunk ( ) . map { RoomTimelineItemViewState ( item : $0 , groupStyle : . single ) }
newViewStates += forwardPaginationChunk
dataSource . updatePreviewItems ( itemViewStates : newViewStates )
// T h e n t h e n e w i t e m s s h o u l d b e a d d e d b u t t h e d i s p l a y e d i t e m s h o u l d n o t c h a n g e o r m o v e i n t h e a r r a y .
try await deferred . fulfill ( )
XCTAssertEqual ( dataSource . previewItems . count , newViewStates . count , " The new items should be added. " )
previewItemCount = dataSource . numberOfPreviewItems ( in : previewController )
2025-02-05 13:27:23 +00:00
displayedItem = try XCTUnwrap ( dataSource . previewController ( previewController , previewItemAt : dataSource . initialItemIndex ) as ? TimelineMediaPreviewItem . Media )
XCTAssertEqual ( displayedItem . id , initialMediaItems [ initialItemIndex ] . id . eventOrTransactionID , " The displayed item should not change. " )
XCTAssertEqual ( dataSource . currentMediaItemID , initialMediaItems [ initialItemIndex ] . id . eventOrTransactionID , " The current item should not change. " )
2025-01-27 16:22:49 +00:00
XCTAssertEqual ( previewItemCount , initialMediaViewStates . count + ( 2 * initialPadding ) , " The number of items should not change " )
}
func testPaginationLimits ( ) async throws {
// G i v e n a d a t a s o u r c e w i t h a s m a l l a m o u n t o f p a d d i n g r e m a i n i n g .
initialPadding = 2
2025-02-05 13:27:23 +00:00
let dataSource = try testInitialItems ( )
2025-01-27 16:22:49 +00:00
// W h e n p a g i n a t i n g b a c k w a r d s b y m o r e t h a n t h e a v a i l a b l e p a d d i n g .
var deferred = deferFulfillment ( dataSource . previewItemsPaginationPublisher ) { _ in true }
let backPaginationChunk = newChunk ( ) . map { RoomTimelineItemViewState ( item : $0 , groupStyle : . single ) }
var newViewStates = backPaginationChunk + initialMediaViewStates
XCTAssertTrue ( newViewStates . count > initialPadding )
dataSource . updatePreviewItems ( itemViewStates : newViewStates )
// T h e n a l l t h e i t e m s s h o u l d b e a d d e d b u t t h e p r e v i e w - a b l e c o u n t s h o u l d n ' t g r o w a n d d i s p l a y e d i t e m s h o u l d n o t c h a n g e o r m o v e .
try await deferred . fulfill ( )
XCTAssertEqual ( dataSource . previewItems . count , newViewStates . count , " The new items should be added. " )
var previewItemCount = dataSource . numberOfPreviewItems ( in : previewController )
2025-02-05 13:27:23 +00:00
var displayedItem = try XCTUnwrap ( dataSource . previewController ( previewController , previewItemAt : dataSource . initialItemIndex ) as ? TimelineMediaPreviewItem . Media )
XCTAssertEqual ( displayedItem . id , initialMediaItems [ initialItemIndex ] . id . eventOrTransactionID , " The displayed item should not change. " )
XCTAssertEqual ( dataSource . currentMediaItemID , initialMediaItems [ initialItemIndex ] . id . eventOrTransactionID , " The current item should not change. " )
2025-01-27 16:22:49 +00:00
XCTAssertEqual ( previewItemCount , initialMediaViewStates . count + ( 2 * initialPadding ) , " The number of items should not change " )
// W h e n p a g i n a t i n g f o r w a r d s b y m o r e t h a n t h e a v a i l a b l e p a d d i n g .
deferred = deferFulfillment ( dataSource . previewItemsPaginationPublisher ) { _ in true }
let forwardPaginationChunk = newChunk ( ) . map { RoomTimelineItemViewState ( item : $0 , groupStyle : . single ) }
newViewStates += forwardPaginationChunk
dataSource . updatePreviewItems ( itemViewStates : newViewStates )
// T h e n a l l t h e i t e m s s h o u l d b e a d d e d b u t t h e p r e v i e w - a b l e c o u n t s h o u l d n ' t g r o w a n d d i s p l a y e d i t e m s h o u l d n o t c h a n g e o r m o v e .
try await deferred . fulfill ( )
XCTAssertEqual ( dataSource . previewItems . count , newViewStates . count , " The new items should be added. " )
previewItemCount = dataSource . numberOfPreviewItems ( in : previewController )
2025-02-05 13:27:23 +00:00
displayedItem = try XCTUnwrap ( dataSource . previewController ( previewController , previewItemAt : dataSource . initialItemIndex ) as ? TimelineMediaPreviewItem . Media )
XCTAssertEqual ( displayedItem . id , initialMediaItems [ initialItemIndex ] . id . eventOrTransactionID , " The displayed item should not change. " )
XCTAssertEqual ( dataSource . currentMediaItemID , initialMediaItems [ initialItemIndex ] . id . eventOrTransactionID , " The current item should not change. " )
2025-01-27 16:22:49 +00:00
XCTAssertEqual ( previewItemCount , initialMediaViewStates . count + ( 2 * initialPadding ) , " The number of items should not change " )
}
2025-02-05 13:27:23 +00:00
func testEmptyTimeline ( ) async throws {
// G i v e n a d a t a s o u r c e b u i l t w i t h n o t i m e l i n e i t e m s l o a d e d .
let initialItem = initialMediaItems [ initialItemIndex ]
let dataSource = TimelineMediaPreviewDataSource ( itemViewStates : [ ] ,
initialItem : initialItem ,
initialPadding : initialPadding ,
paginationState : . initial )
// W h e n t h e p r e v i e w c o n t r o l l e r d i s p l a y s t h e d a t a .
var previewItemCount = dataSource . numberOfPreviewItems ( in : previewController )
var displayedItem = try XCTUnwrap ( dataSource . previewController ( previewController , previewItemAt : dataSource . initialItemIndex ) as ? TimelineMediaPreviewItem . Media ,
" A preview item should be found. " )
// T h e n t h e p r e v i e w c o n t r o l l e r s h o u l d a l w a y s s h o w t h e i n i t i a l i t e m .
XCTAssertEqual ( dataSource . previewItems . count , 1 , " The initial item should be in the preview items array. " )
XCTAssertEqual ( previewItemCount , 1 + ( 2 * initialPadding ) , " The initial item count should be padded for the preview controller. " )
XCTAssertEqual ( dataSource . initialItemIndex , initialPadding , " The initial item index should be padded for the preview controller. " )
XCTAssertEqual ( displayedItem . id , initialItem . id . eventOrTransactionID , " The displayed item should be the initial item. " )
XCTAssertEqual ( dataSource . currentMediaItemID , initialItem . id . eventOrTransactionID , " The current item should also be the initial item. " )
// W h e n t h e t i m e l i n e l o a d s t h e i n i t i a l i t e m s .
let deferred = deferFulfillment ( dataSource . previewItemsPaginationPublisher ) { _ in true }
let loadedItems = initialMediaItems . map { RoomTimelineItemViewState ( item : $0 , groupStyle : . single ) }
dataSource . updatePreviewItems ( itemViewStates : loadedItems )
try await deferred . fulfill ( )
// T h e n t h e p r e v i e w c o n t r o l l e r s h o u l d s t i l l s h o w t h e i n i t i a l i t e m w i t h t h e o t h e r i t e m s l o a d e d a r o u n d i t .
previewItemCount = dataSource . numberOfPreviewItems ( in : previewController )
displayedItem = try XCTUnwrap ( dataSource . previewController ( previewController , previewItemAt : dataSource . initialItemIndex ) as ? TimelineMediaPreviewItem . Media ,
" A preview item should be found. " )
XCTAssertEqual ( dataSource . previewItems . count , initialMediaViewStates . count , " The preview items should now be loaded. " )
XCTAssertEqual ( previewItemCount , 1 + ( 2 * initialPadding ) , " The item count should not change as the padding will be reduced. " )
XCTAssertEqual ( dataSource . initialItemIndex , initialPadding , " The item index should not change. " )
XCTAssertEqual ( displayedItem . id , initialMediaItems [ initialItemIndex ] . id . eventOrTransactionID , " The displayed item should not change. " )
XCTAssertEqual ( dataSource . currentMediaItemID , initialMediaItems [ initialItemIndex ] . id . eventOrTransactionID , " The current item should not change. " )
}
func testTimelineUpdateWithoutInitialItem ( ) async throws {
// G i v e n a d a t a s o u r c e b u i l t w i t h n o t i m e l i n e i t e m s l o a d e d .
let initialItem = initialMediaItems [ initialItemIndex ]
let dataSource = TimelineMediaPreviewDataSource ( itemViewStates : [ ] ,
initialItem : initialItem ,
initialPadding : initialPadding ,
paginationState : . initial )
// W h e n t h e p r e v i e w c o n t r o l l e r d i s p l a y s t h e d a t a .
var previewItemCount = dataSource . numberOfPreviewItems ( in : previewController )
var displayedItem = try XCTUnwrap ( dataSource . previewController ( previewController , previewItemAt : dataSource . initialItemIndex ) as ? TimelineMediaPreviewItem . Media ,
" A preview item should be found. " )
// T h e n t h e p r e v i e w c o n t r o l l e r s h o u l d a l w a y s s h o w t h e i n i t i a l i t e m .
XCTAssertEqual ( dataSource . previewItems . count , 1 , " The initial item should be in the preview items array. " )
XCTAssertEqual ( previewItemCount , 1 + ( 2 * initialPadding ) , " The initial item count should be padded for the preview controller. " )
XCTAssertEqual ( dataSource . initialItemIndex , initialPadding , " The initial item index should be padded for the preview controller. " )
XCTAssertEqual ( displayedItem . id , initialItem . id . eventOrTransactionID , " The displayed item should be the initial item. " )
XCTAssertEqual ( dataSource . currentMediaItemID , initialItem . id . eventOrTransactionID , " The current item should also be the initial item. " )
// W h e n t h e t i m e l i n e l o a d s m o r e i t e m s b u t s t i l l d o e s n ' t i n c l u d e t h e i n i t i a l i t e m .
let failure = deferFailure ( dataSource . previewItemsPaginationPublisher , timeout : 1 ) { _ in true }
let loadedItems = newChunk ( ) . map { RoomTimelineItemViewState ( item : $0 , groupStyle : . single ) }
dataSource . updatePreviewItems ( itemViewStates : loadedItems )
try await failure . fulfill ( )
// T h e n t h e p r e v i e w c o n t r o l l e r s h o u l d n ' t u p d a t e t h e a v a i l a b l e p r e v i e w i t e m s .
previewItemCount = dataSource . numberOfPreviewItems ( in : previewController )
displayedItem = try XCTUnwrap ( dataSource . previewController ( previewController , previewItemAt : dataSource . initialItemIndex ) as ? TimelineMediaPreviewItem . Media ,
" A preview item should be found. " )
XCTAssertEqual ( dataSource . previewItems . count , 1 , " No new items should have been added to the array. " )
XCTAssertEqual ( previewItemCount , 1 + ( 2 * initialPadding ) , " The initial item count should not change. " )
XCTAssertEqual ( dataSource . initialItemIndex , initialPadding , " The initial item index should not change. " )
XCTAssertEqual ( displayedItem . id , initialItem . id . eventOrTransactionID , " The displayed item should not change. " )
XCTAssertEqual ( dataSource . currentMediaItemID , initialItem . id . eventOrTransactionID , " The current item not change. " )
}
2025-01-27 16:22:49 +00:00
// MARK: H e l p e r s
func newChunk ( ) -> [ EventBasedMessageTimelineItemProtocol ] {
RoomTimelineItemFixtures . mediaChunk
. compactMap { $0 as ? EventBasedMessageTimelineItemProtocol }
. filter ( \ . supportsMediaCaption ) // V o i c e m e s s a g e s c a n ' t b e p r e v i e w e d ( a n d d o n ' t s u p p o r t c a p t i o n s ) .
}
}
2025-01-29 15:07:23 +00:00
private extension TimelineMediaPreviewDataSource {
2025-02-05 13:27:23 +00:00
var currentMediaItemID : TimelineItemIdentifier . EventOrTransactionID ? {
2025-01-29 15:07:23 +00:00
switch currentItem {
case . media ( let mediaItem ) : mediaItem . id
case . loading : nil
}
}
}