Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
signalapp
GitHub Repository: signalapp/Signal-iOS
Path: blob/main/Signal/test/ViewControllers/MediaGalleryCollectionViewUpdaterTest.swift
1 views
//
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//

import XCTest

@testable import Signal

class CollectionViewLogger: MediaGalleryCollectionViewUpdaterDelegate {
    enum Mod: Equatable, CustomDebugStringConvertible {
        var debugDescription: String {
            switch self {
            case .deleteSections(let sections):
                return "delete sections at indexes \(Array(sections))"
            case .deleteItems(let paths):
                return "delete items at paths \(Array(paths))"
            case .insertSections(let sections):
                return "insert sections at indexes \(Array(sections))"
            case .reloadItems(let paths):
                return "reload items at paths \(Array(paths))"
            case .reloadSections(let sections):
                return "reload sections at indexes \(Array(sections))"
            }
        }

        case deleteSections(IndexSet)
        case deleteItems([IndexPath])
        case insertSections(IndexSet)
        case reloadItems([IndexPath])
        case reloadSections(IndexSet)
    }

    private(set) var log = [Mod]()

    func updaterDeleteSections(_ sections: Signal.MediaGallerySectionIndexSet) {
        log.append(.deleteSections(sections.indexSet))
    }

    func updaterDeleteItems(at indexPaths: [Signal.MediaGalleryIndexPath]) {
        log.append(.deleteItems(indexPaths.map { $0.indexPath }))
    }

    func updaterInsertSections(_ sections: Signal.MediaGallerySectionIndexSet) {
        log.append(.insertSections(sections.indexSet))
    }

    func updaterReloadItems(at indexPaths: [Signal.MediaGalleryIndexPath]) {
        log.append(.reloadItems(indexPaths.map { $0.indexPath }))
    }

    func updaterReloadSections(_ sections: Signal.MediaGallerySectionIndexSet) {
        log.append(.reloadSections(sections.indexSet))
    }

    func updaterDidFinish(numberOfSectionsBefore: Int, numberOfSectionsAfter: Int) {
    }

}

final class MediaGalleryCollectionViewUpdaterTest: XCTestCase {
    private var logger: CollectionViewLogger!
    private var updater: MediaGalleryCollectionViewUpdater!

    override func setUp() {
        super.setUp()
        logger = CollectionViewLogger()
    }

    func makeUpdater(_ itemCounts: [Int]) {
        updater = MediaGalleryCollectionViewUpdater(itemCounts: itemCounts)
        updater.delegate = logger
    }

    func testItemDelete() {
        makeUpdater([10, 20, 30])
        updater.update([.modify(index: 1, changes: [.removeItem(index: 5)])])

        XCTAssertEqual(logger.log, [.deleteItems([IndexPath(item: 5, section: 1)])])
    }

    func testItemDeleteUsesOriginalSectionIndex() {
        makeUpdater([10, 20, 30])
        updater.update([
            .remove(index: 0),
            .modify(index: 0, changes: [.removeItem(index: 5)]),
        ])

        XCTAssertEqual(logger.log, [
            .deleteItems([IndexPath(item: 5, section: 1)]),
            .deleteSections(IndexSet([0])),
        ])
    }

    func testSectionDelete() {
        makeUpdater([10, 20, 30])
        updater.update([.remove(index: 1)])

        XCTAssertEqual(logger.log, [.deleteSections(IndexSet(integer: 1))])
    }

    func testSectionPrepend() {
        makeUpdater([10, 20, 30])
        updater.update([.prepend])

        XCTAssertEqual(logger.log, [.insertSections(IndexSet(integer: 0))])
    }

    func testAllChangesArePrepends() {
        makeUpdater([])
        updater.update([.prepend, .prepend, .prepend])
        XCTAssertEqual(logger.log, [.insertSections(IndexSet([0, 1, 2]))])
    }

    func testSectionAppend() {
        makeUpdater([10])
        updater.update([.append])

        XCTAssertEqual(logger.log, [.insertSections(IndexSet(integer: 1))])
    }

    func testItemUpdateUsesOriginalSectionIndex() {
        makeUpdater([10, 20, 30])
        updater.update([
            .prepend,
            .modify(index: 1, changes: [.updateItem(index: 5)]),
        ])
        XCTAssertEqual(
            logger.log,
            [
                .reloadItems([IndexPath(item: 5, section: 0)]),
                .insertSections(IndexSet(integer: 0)),
            ],
        )
    }

    func testUpdateOriginalItemAfterRemovingPredecessor() {
        makeUpdater([10, 20, 30])
        updater.update([
            .modify(
                index: 0,
                changes: [
                    .removeItem(index: 1),
                    .updateItem(index: 2), // NOTE: This corresponds to the item with original index 3.
                ],
            ),
        ])
        XCTAssertEqual(
            logger.log,
            [
                .reloadItems([IndexPath(item: 3, section: 0)]),
                .deleteItems([IndexPath(item: 1, section: 0)]),
            ],
        )
    }

    func testUpdateDeletedItem() {
        makeUpdater([10, 20, 30])
        updater.update([
            .modify(
                index: 0,
                changes: [
                    .updateItem(index: 9),
                    .removeItem(index: 9),
                ],
            ),
        ])
        XCTAssertEqual(
            logger.log,
            [.deleteItems([IndexPath(item: 9, section: 0)])],
        )
    }

    func testUpdateItems() {
        makeUpdater([10, 20, 30])
        updater.update([.modify(index: 1, changes: [.updateItem(index: 5)])])

        XCTAssertEqual(logger.log, [.reloadItems([IndexPath(item: 5, section: 1)])])
    }

    func testReloadSection() {
        makeUpdater([10, 20, 30])
        // After a reload, nothing else you do to this section has an effect.
        updater.update([.modify(
            index: 1,
            changes: [
                .removeItem(index: 0),
                .updateItem(index: 1),
                .reloadSection,
                .updateItem(index: 1),
                .removeItem(index: 2),
            ],
        )])
        XCTAssertEqual(logger.log, [.reloadSections(IndexSet(integer: 1))])
    }

    func testRemoveAll() {
        makeUpdater([10, 20, 30])
        updater.update([
            .append,
            .prepend,
            .removeAll,
        ])
        XCTAssertEqual(logger.log, [.deleteSections(IndexSet([0, 1, 2]))])
    }

    func testSurvivingSectionsNotAllAppended() {
        makeUpdater([10, 20, 30])
        updater.update([
            .append,
            .prepend,
            .removeAll,
            .append,
            .prepend,
        ])
        XCTAssertEqual(logger.log, [
            .deleteSections(IndexSet([0, 1, 2])),
            .insertSections(IndexSet([0, 1])),
        ])
    }

    func testAllSurvivingSectionsAppended() {
        makeUpdater([10, 20, 30])
        updater.update([
            .removeAll,
            .append,
            .append,
        ])
        XCTAssertEqual(logger.log, [
            .deleteSections(IndexSet([0, 1, 2])),
            .insertSections(IndexSet([0, 1])),
        ])
    }

    func testEverything() {
        makeUpdater([10, 20, 30])
        updater.update([
            .remove(index: 0), // 10,20,30 -> 20,30
            .prepend, // 20,30 -> ?,20,30
            .remove(index: 1), // ?,20,30 -> ?,30
            .append, // ?,30 -> ?,30,?
            .modify(index: 1, changes: [
                .updateItem(index: 29),
                .removeItem(index: 2), // ?,30,? -> ?,29,?
                .removeItem(index: 3), // ?,30,? -> ?,28,?
                .removeItem(index: 4), // ?,30,? -> ?,27,?
            ]), // ?,28,?
            // These two have no effect because they operate on unreported sections.
            .modify(index: 0, changes: [
                .removeItem(index: 1),
                .updateItem(index: 1),
            ]),
            .modify(index: 2, changes: [
                .removeItem(index: 1),
                .updateItem(index: 1),
            ]),
            .append, // ?,28,? -> ?,28,?,?
            .modify(index: 3, changes: [.removeItem(index: 0)]), //  Modifying a novel section so no change
            .remove(index: 3), // ?,28,?,? -> ?,28,?
        ])

        XCTAssertEqual(
            logger.log,
            [
                .reloadItems([IndexPath(item: 29, section: 2)]),
                .deleteItems([
                    IndexPath(item: 2, section: 2),
                    IndexPath(item: 4, section: 2),
                    IndexPath(item: 6, section: 2),
                ]),
                .deleteSections(IndexSet([0, 1])),
                .insertSections(IndexSet([0, 2])),
            ],
        )

    }

    func testNThIndex1() {
        XCTAssertEqual(IndexSet([5, 6, 15, 16]).nthIndex(0), 5)
    }

    func testNThIndex2() {
        XCTAssertEqual(IndexSet([5, 6, 15, 16]).nthIndex(1), 6)
    }

    func testNThIndex3() {
        XCTAssertEqual(IndexSet([5, 6, 15, 16]).nthIndex(2), 15)
    }

    func testNThIndex4() {
        XCTAssertEqual(IndexSet([5, 6, 15, 16]).nthIndex(3), 16)
    }

    func testNThIndex5() {
        XCTAssertEqual(IndexSet([5, 6, 15, 16]).nthIndex(4), nil)
    }
}