diff --git a/InstantSearch.podspec b/InstantSearch.podspec index 7204c4cd..a7ea1126 100644 --- a/InstantSearch.podspec +++ b/InstantSearch.podspec @@ -1,7 +1,7 @@ Pod::Spec.new do |s| s.name = "InstantSearch" s.module_name = 'InstantSearch' - s.version = "5.0.0" + s.version = "5.1.0" s.summary = "A library of widgets and helpers to build instant-search applications on iOS." s.homepage = "https://github.com/algolia/instantsearch-ios" s.license = { type: 'Apache 2.0', file: 'LICENSE.md' } diff --git a/InstantSearchTestsHost/Info.plist b/InstantSearchTestsHost/Info.plist index ee3c849e..0288e224 100644 --- a/InstantSearchTestsHost/Info.plist +++ b/InstantSearchTestsHost/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 4.0.0 + 5.1.0 CFBundleVersion 1 LSRequiresIPhoneOS diff --git a/Sources/Controller/Hits/CollectionView/HitsCollectionController.swift b/Sources/Controller/Hits/CollectionView/HitsCollectionController.swift index 6c89d300..7da199ea 100644 --- a/Sources/Controller/Hits/CollectionView/HitsCollectionController.swift +++ b/Sources/Controller/Hits/CollectionView/HitsCollectionController.swift @@ -22,14 +22,14 @@ public class HitsCollectionController: NSObject, HitsControl public weak var hitsSource: Source? - private var dataSource: HitsCollectionViewDataSource? { + public var dataSource: HitsCollectionViewDataSource? { didSet { dataSource?.hitsSource = hitsSource collectionView.dataSource = dataSource } } - private var delegate: HitsCollectionViewDelegate? { + public var delegate: HitsCollectionViewDelegate? { didSet { delegate?.hitsSource = hitsSource collectionView.delegate = delegate diff --git a/Sources/Controller/Hits/CollectionView/HitsCollectionViewDataSource.swift b/Sources/Controller/Hits/CollectionView/HitsCollectionViewDataSource.swift index 23a1fced..8c254373 100644 --- a/Sources/Controller/Hits/CollectionView/HitsCollectionViewDataSource.swift +++ b/Sources/Controller/Hits/CollectionView/HitsCollectionViewDataSource.swift @@ -11,21 +11,36 @@ import UIKit open class HitsCollectionViewDataSource: NSObject, UICollectionViewDataSource { public var cellConfigurator: CollectionViewCellConfigurator + public var templateCellProvider: () -> UICollectionViewCell public weak var hitsSource: DataSource? public init(cellConfigurator: @escaping CollectionViewCellConfigurator) { self.cellConfigurator = cellConfigurator + self.templateCellProvider = { return .init() } } open func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { - return hitsSource?.numberOfHits() ?? 0 + + guard let hitsSource = hitsSource else { + fatalError("Missing hits source") + } + + return hitsSource.numberOfHits() + } open func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { - guard let hit = hitsSource?.hit(atIndex: indexPath.row) else { - return UICollectionViewCell() + + guard let hitsSource = hitsSource else { + fatalError("Missing hits source") + } + + guard let hit = hitsSource.hit(atIndex: indexPath.row) else { + return templateCellProvider() } + return cellConfigurator(collectionView, hit, indexPath) + } } diff --git a/Sources/Controller/Hits/CollectionView/HitsCollectionViewDelegate.swift b/Sources/Controller/Hits/CollectionView/HitsCollectionViewDelegate.swift index a42e35c9..90b65d4a 100644 --- a/Sources/Controller/Hits/CollectionView/HitsCollectionViewDelegate.swift +++ b/Sources/Controller/Hits/CollectionView/HitsCollectionViewDelegate.swift @@ -18,10 +18,16 @@ open class HitsCollectionViewDelegate: NSObject, UIColle } open func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { - guard let hit = hitsSource?.hit(atIndex: indexPath.row) else { + + guard let hitsSource = hitsSource else { + fatalError("Missing hits source") + } + + guard let hit = hitsSource.hit(atIndex: indexPath.row) else { return } clickHandler(collectionView, hit, indexPath) + } } diff --git a/Sources/Controller/Hits/TableView/HitsTableViewDataSource.swift b/Sources/Controller/Hits/TableView/HitsTableViewDataSource.swift index 6024931c..d8f0ec7d 100644 --- a/Sources/Controller/Hits/TableView/HitsTableViewDataSource.swift +++ b/Sources/Controller/Hits/TableView/HitsTableViewDataSource.swift @@ -11,21 +11,36 @@ import UIKit open class HitsTableViewDataSource: NSObject, UITableViewDataSource { public var cellConfigurator: TableViewCellConfigurator + public var templateCellProvider: () -> UITableViewCell public weak var hitsSource: DataSource? public init(cellConfigurator: @escaping TableViewCellConfigurator) { self.cellConfigurator = cellConfigurator + self.templateCellProvider = { return .init() } } open func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return hitsSource?.numberOfHits() ?? 0 + + guard let hitsSource = hitsSource else { + fatalError("Missing hits source") + } + + return hitsSource.numberOfHits() + } open func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - guard let hit = hitsSource?.hit(atIndex: indexPath.row) else { - return UITableViewCell() + + guard let hitsSource = hitsSource else { + fatalError("Missing hits source") + } + + guard let hit = hitsSource.hit(atIndex: indexPath.row) else { + return templateCellProvider() } + return cellConfigurator(tableView, hit, indexPath) + } } diff --git a/Sources/Controller/Hits/TableView/HitsTableViewDelegate.swift b/Sources/Controller/Hits/TableView/HitsTableViewDelegate.swift index f87d450b..9be28033 100644 --- a/Sources/Controller/Hits/TableView/HitsTableViewDelegate.swift +++ b/Sources/Controller/Hits/TableView/HitsTableViewDelegate.swift @@ -18,10 +18,16 @@ open class HitsTableViewDelegate: NSObject, UITableViewD } open func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - guard let hit = hitsSource?.hit(atIndex: indexPath.row) else { + + guard let hitsSource = hitsSource else { + fatalError("Missing hits source") + } + + guard let hit = hitsSource.hit(atIndex: indexPath.row) else { return } clickHandler(tableView, hit, indexPath) + } } diff --git a/Sources/Controller/MultiIndexHits/CollectionView/MultiIndexHitsCollectionController.swift b/Sources/Controller/MultiIndexHits/CollectionView/MultiIndexHitsCollectionController.swift index 3dbf171a..7659abd2 100644 --- a/Sources/Controller/MultiIndexHits/CollectionView/MultiIndexHitsCollectionController.swift +++ b/Sources/Controller/MultiIndexHits/CollectionView/MultiIndexHitsCollectionController.swift @@ -16,21 +16,21 @@ public class MultiIndexHitsCollectionController: NSObject, MultiIndexHitsControl public weak var hitsSource: MultiIndexHitsSource? { didSet { - dataSource?.hitsDataSource = hitsSource - delegate?.hitsDataSource = hitsSource + dataSource?.hitsSource = hitsSource + delegate?.hitsSource = hitsSource } } public var dataSource: MultiIndexHitsCollectionViewDataSource? { didSet { - dataSource?.hitsDataSource = hitsSource + dataSource?.hitsSource = hitsSource collectionView.dataSource = dataSource } } public var delegate: MultiIndexHitsCollectionViewDelegate? { didSet { - delegate?.hitsDataSource = hitsSource + delegate?.hitsSource = hitsSource collectionView.delegate = delegate } } diff --git a/Sources/Controller/MultiIndexHits/CollectionView/MultiIndexHitsCollectionViewDataSource.swift b/Sources/Controller/MultiIndexHits/CollectionView/MultiIndexHitsCollectionViewDataSource.swift index c415d62c..abe2b5de 100644 --- a/Sources/Controller/MultiIndexHits/CollectionView/MultiIndexHitsCollectionViewDataSource.swift +++ b/Sources/Controller/MultiIndexHits/CollectionView/MultiIndexHitsCollectionViewDataSource.swift @@ -12,7 +12,7 @@ open class MultiIndexHitsCollectionViewDataSource: NSObject { private typealias CellConfigurator = (UICollectionView, Int) throws -> UICollectionViewCell - public weak var hitsDataSource: MultiIndexHitsSource? + public weak var hitsSource: MultiIndexHitsSource? private var cellConfigurators: [Int: CellConfigurator] @@ -21,14 +21,23 @@ open class MultiIndexHitsCollectionViewDataSource: NSObject { super.init() } - public func setCellConfigurator(forSection section: Int, _ cellConfigurator: @escaping CollectionViewCellConfigurator) { + public func setCellConfigurator(forSection section: Int, + templateCellProvider: @escaping () -> UICollectionViewCell = { return .init() }, + _ cellConfigurator: @escaping CollectionViewCellConfigurator) { cellConfigurators[section] = { [weak self] (collectionView, row) in - guard let dataSource = self?.hitsDataSource else { return UICollectionViewCell() } - guard let hit: Hit = try dataSource.hit(atIndex: row, inSection: section) else { - assertionFailure("Invalid state: Attempt to deqeue a cell for a missing hit in a hits Interactor") - return UICollectionViewCell() + guard let dataSource = self else { + return .init() } - return cellConfigurator(collectionView, hit, IndexPath(item: row, section: section)) + + guard let hitsSource = dataSource.hitsSource else { + fatalError("Missing hits source") + } + + guard let hit: Hit = try hitsSource.hit(atIndex: row, inSection: section) else { + return templateCellProvider() + } + + return cellConfigurator(collectionView, hit, IndexPath(row: row, section: section)) } } @@ -37,22 +46,25 @@ open class MultiIndexHitsCollectionViewDataSource: NSObject { extension MultiIndexHitsCollectionViewDataSource: UICollectionViewDataSource { open func numberOfSections(in collectionView: UICollectionView) -> Int { - guard let numberOfSections = hitsDataSource?.numberOfSections() else { - return 0 + guard let hitsSource = hitsSource else { + fatalError("Missing hits source") } - return numberOfSections + return hitsSource.numberOfSections() } open func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { - guard let numberOfRows = hitsDataSource?.numberOfHits(inSection: section) else { - return 0 + guard let hitsSource = hitsSource else { + fatalError("Missing hits source") } - return numberOfRows + return hitsSource.numberOfHits(inSection: section) } open func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + guard let cellConfigurator = cellConfigurators[indexPath.section] else { + fatalError("No cell configurator found for section \(indexPath.section)") + } do { - return try cellConfigurators[indexPath.section]?(collectionView, indexPath.row) ?? UICollectionViewCell() + return try cellConfigurator(collectionView, indexPath.row) } catch let error { fatalError("\(error)") } diff --git a/Sources/Controller/MultiIndexHits/CollectionView/MultiIndexHitsCollectionViewDelegate.swift b/Sources/Controller/MultiIndexHits/CollectionView/MultiIndexHitsCollectionViewDelegate.swift index 7bce3f25..9f0272ab 100644 --- a/Sources/Controller/MultiIndexHits/CollectionView/MultiIndexHitsCollectionViewDelegate.swift +++ b/Sources/Controller/MultiIndexHits/CollectionView/MultiIndexHitsCollectionViewDelegate.swift @@ -12,7 +12,7 @@ open class MultiIndexHitsCollectionViewDelegate: NSObject { typealias ClickHandler = (UICollectionView, Int) throws -> Void - public weak var hitsDataSource: MultiIndexHitsSource? + public weak var hitsSource: MultiIndexHitsSource? private var clickHandlers: [Int: ClickHandler] @@ -23,11 +23,18 @@ open class MultiIndexHitsCollectionViewDelegate: NSObject { public func setClickHandler(forSection section: Int, _ clickHandler: @escaping CollectionViewClickHandler) { clickHandlers[section] = { [weak self] (collectionView, row) in - guard let hit: Hit = try self?.hitsDataSource?.hit(atIndex: row, inSection: section) else { - assertionFailure("Invalid state: Attempt to process a click of a cell for a missing hit in a hits Interactor") + guard let delegate = self else { return } + + guard let hitsSource = delegate.hitsSource else { + fatalError("Missing hits source") + } + + guard let hit: Hit = try hitsSource.hit(atIndex: row, inSection: section) else { return } + clickHandler(collectionView, hit, IndexPath(item: row, section: section)) + } } @@ -36,8 +43,11 @@ open class MultiIndexHitsCollectionViewDelegate: NSObject { extension MultiIndexHitsCollectionViewDelegate: UICollectionViewDelegate { open func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + guard let clickHandler = clickHandlers[indexPath.section] else { + fatalError("No click handler found for section \(indexPath.section)") + } do { - try clickHandlers[indexPath.section]?(collectionView, indexPath.row) + try clickHandler(collectionView, indexPath.row) } catch let error { fatalError("\(error)") } diff --git a/Sources/Controller/MultiIndexHits/TableView/MultiIndexHitsTableViewDataSource.swift b/Sources/Controller/MultiIndexHits/TableView/MultiIndexHitsTableViewDataSource.swift index 0b4687a7..c25aedc5 100644 --- a/Sources/Controller/MultiIndexHits/TableView/MultiIndexHitsTableViewDataSource.swift +++ b/Sources/Controller/MultiIndexHits/TableView/MultiIndexHitsTableViewDataSource.swift @@ -21,12 +21,22 @@ open class MultiIndexHitsTableViewDataSource: NSObject { super.init() } - public func setCellConfigurator(forSection section: Int, _ cellConfigurator: @escaping TableViewCellConfigurator) { + public func setCellConfigurator(forSection section: Int, + templateCellProvider: @escaping () -> UITableViewCell = { return .init() }, + _ cellConfigurator: @escaping TableViewCellConfigurator) { cellConfigurators[section] = { [weak self] (tableView, row) in - guard let hit: Hit = try self?.hitsSource?.hit(atIndex: row, inSection: section) else { - assertionFailure("Invalid state: Attempt to deqeue a cell for a missing hit in a hits Interactor") - return UITableViewCell() + guard let dataSource = self else { + return .init() } + + guard let hitsSource = dataSource.hitsSource else { + fatalError("Missing hits source") + } + + guard let hit: Hit = try hitsSource.hit(atIndex: row, inSection: section) else { + return templateCellProvider() + } + return cellConfigurator(tableView, hit, IndexPath(row: row, section: section)) } } @@ -36,22 +46,25 @@ open class MultiIndexHitsTableViewDataSource: NSObject { extension MultiIndexHitsTableViewDataSource: UITableViewDataSource { open func numberOfSections(in tableView: UITableView) -> Int { - guard let numberOfSections = hitsSource?.numberOfSections() else { - return 0 + guard let hitsSource = hitsSource else { + fatalError("Missing hits source") } - return numberOfSections + return hitsSource.numberOfSections() } open func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - guard let numberOfRows = hitsSource?.numberOfHits(inSection: section) else { - return 0 + guard let hitsSource = hitsSource else { + fatalError("Missing hits source") } - return numberOfRows + return hitsSource.numberOfHits(inSection: section) } open func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + guard let cellConfigurator = cellConfigurators[indexPath.section] else { + fatalError("No cell configurator found for section \(indexPath.section)") + } do { - return try cellConfigurators[indexPath.section]?(tableView, indexPath.row) ?? UITableViewCell() + return try cellConfigurator(tableView, indexPath.row) } catch let error { fatalError("\(error)") } diff --git a/Sources/Controller/MultiIndexHits/TableView/MultiIndexHitsTableViewDelegate.swift b/Sources/Controller/MultiIndexHits/TableView/MultiIndexHitsTableViewDelegate.swift index b640bf2c..35856795 100644 --- a/Sources/Controller/MultiIndexHits/TableView/MultiIndexHitsTableViewDelegate.swift +++ b/Sources/Controller/MultiIndexHits/TableView/MultiIndexHitsTableViewDelegate.swift @@ -23,11 +23,18 @@ open class MultiIndexHitsTableViewDelegate: NSObject { public func setClickHandler(forSection section: Int, _ clickHandler: @escaping TableViewClickHandler) { clickHandlers[section] = { [weak self] (tableView, row) in - guard let hit: Hit = try self?.hitsSource?.hit(atIndex: row, inSection: section) else { - assertionFailure("Invalid state: Attempt to process a click of a cell for a missing hit in a hits Interactor") + guard let delegate = self else { return } + + guard let hitsSource = delegate.hitsSource else { + fatalError("Missing hits source") + } + + guard let hit: Hit = try hitsSource.hit(atIndex: row, inSection: section) else { return } - clickHandler(tableView, hit, IndexPath(row: row, section: section)) + + clickHandler(tableView, hit, IndexPath(item: row, section: section)) + } } @@ -36,8 +43,11 @@ open class MultiIndexHitsTableViewDelegate: NSObject { extension MultiIndexHitsTableViewDelegate: UITableViewDelegate { open func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + guard let clickHandler = clickHandlers[indexPath.section] else { + fatalError("No click handler found for section \(indexPath.section)") + } do { - try clickHandlers[indexPath.section]?(tableView, indexPath.row) + try clickHandler(tableView, indexPath.row) } catch let error { fatalError("\(error)") } diff --git a/Sources/FatalErrorUtil.swift b/Sources/FatalErrorUtil.swift new file mode 100644 index 00000000..9038fab3 --- /dev/null +++ b/Sources/FatalErrorUtil.swift @@ -0,0 +1,41 @@ +// +// FatalErrorUtil.swift +// InstantSearch +// +// Created by Vladislav Fitc on 04/09/2019. +// + +import Foundation + +// overrides Swift global `fatalError` +public func fatalError(_ message: @autoclosure () -> String = "", file: StaticString = #file, line: UInt = #line) -> Never { + FatalErrorUtil.fatalErrorClosure(message(), file, line) + unreachable() +} + +/// This is a `noreturn` function that pauses forever +public func unreachable() -> Never { + repeat { + RunLoop.current.run() + } while (true) +} + +/// Utility functions that can replace and restore the `fatalError` global function. +public struct FatalErrorUtil { + + // Called by the custom implementation of `fatalError`. + static var fatalErrorClosure: (String, StaticString, UInt) -> Never = defaultFatalErrorClosure + + // backup of the original Swift `fatalError` + private static let defaultFatalErrorClosure = { Swift.fatalError($0, file: $1, line: $2) } + + /// Replace the `fatalError` global function with something else. + public static func replaceFatalError(closure: @escaping (String, StaticString, UInt) -> Never) { + fatalErrorClosure = closure + } + + /// Restore the `fatalError` global function back to the original Swift implementation + public static func restoreFatalError() { + fatalErrorClosure = defaultFatalErrorClosure + } +} diff --git a/Sources/Info.plist b/Sources/Info.plist index 60b9c008..5313a0db 100644 --- a/Sources/Info.plist +++ b/Sources/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 1.0.0 + 5.1.0 CFBundleSignature ???? CFBundleVersion diff --git a/Tests/CollectionViewHitsControllerTests.swift b/Tests/CollectionViewHitsControllerTests.swift new file mode 100644 index 00000000..69798bc9 --- /dev/null +++ b/Tests/CollectionViewHitsControllerTests.swift @@ -0,0 +1,133 @@ +// +// CollectionViewHitsControllerTests.swift +// InstantSearchTests +// +// Created by Vladislav Fitc on 04/09/2019. +// + +@testable import InstantSearch +import Foundation +import XCTest + + +class TestTemplateCollectionViewCell: UICollectionViewCell {} + +class CollectionViewHitsControllerTests: XCTestCase { + + func testMissingDataSource() { + + let collectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout()) + + let dataSource = HitsCollectionViewDataSource { (_, hit, _) in + let cell = TestCollectionViewCell() + cell.content = hit + return cell + } + + expectFatalError(expectedMessage: "Missing hits source") { + _ = dataSource.collectionView(collectionView, cellForItemAt: .init()) + } + + expectFatalError(expectedMessage: "Missing hits source") { + _ = dataSource.collectionView(collectionView, numberOfItemsInSection: .init()) + } + + let delegate = HitsCollectionViewDelegate { _,_,_ in } + + expectFatalError(expectedMessage: "Missing hits source") { + _ = delegate.collectionView(collectionView, didSelectItemAt: .init()) + } + + } + + func testTemplate() { + + let collectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout()) + + let hitsDataSource = TestHitsSource(hits: ["t1", "t2", "t3"]) + + let dataSource = HitsCollectionViewDataSource { (_, hit, _) -> UICollectionViewCell in + let cell = TestCollectionViewCell() + cell.content = hit + return cell + } + + dataSource.hitsSource = hitsDataSource + + dataSource.templateCellProvider = { return TestTemplateCollectionViewCell() } + + XCTAssert(dataSource.collectionView(collectionView, cellForItemAt: IndexPath(item: 4, section: 0)) is TestTemplateCollectionViewCell, "") + + } + + func testDataSource() { + + let collectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout()) + + let dataSource = HitsCollectionViewDataSource { (_, hit, _) -> UICollectionViewCell in + let cell = TestCollectionViewCell() + cell.content = hit + return cell + } + + let hitsDataSource = TestHitsSource(hits: ["t1", "t2", "t3"]) + + dataSource.hitsSource = hitsDataSource + + XCTAssertEqual(dataSource.collectionView(collectionView, numberOfItemsInSection: 0), 3) + XCTAssertEqual((dataSource.collectionView(collectionView, cellForItemAt: IndexPath(item: 1, section: 0)) as? TestCollectionViewCell)?.content, "t2") + + } + + func testDelegate() { + + let collectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout()) + + let itemToSelect = 2 + + let hitsDataSource = TestHitsSource(hits: ["t1", "t2", "t3"]) + + let exp = expectation(description: "Hit selection") + + let delegate = HitsCollectionViewDelegate { (_, hit, _) in + XCTAssertEqual(hit, hitsDataSource.hits[itemToSelect]) + exp.fulfill() + } + + delegate.hitsSource = hitsDataSource + + delegate.collectionView(collectionView, didSelectItemAt: IndexPath(item: itemToSelect, section: 0)) + + waitForExpectations(timeout: 1, handler: .none) + + } + + func testWidget() { + + let collectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout()) + + let vm = HitsInteractor() + + let dataSource = HitsCollectionViewDataSource> { (_, hit, _) -> UICollectionViewCell in + let cell = TestCollectionViewCell() + cell.content = hit + return cell + } + + dataSource.hitsSource = vm + + let delegate = HitsCollectionViewDelegate> { (_, _, _) in } + + delegate.hitsSource = vm + + let widget = HitsCollectionController>(collectionView: collectionView) + + widget.dataSource = dataSource + widget.delegate = delegate + + XCTAssertTrue(collectionView.delegate === delegate) + XCTAssertTrue(collectionView.dataSource === dataSource) + + } + +} diff --git a/Tests/CollectionViewMultiIndexHitsControllerTests.swift b/Tests/CollectionViewMultiIndexHitsControllerTests.swift new file mode 100644 index 00000000..bf80dc25 --- /dev/null +++ b/Tests/CollectionViewMultiIndexHitsControllerTests.swift @@ -0,0 +1,119 @@ +// +// CollectionViewMultiIndexHitsControllerTests.swift +// InstantSearch +// +// Created by Vladislav Fitc on 04/09/2019. +// + +import Foundation + +@testable import InstantSearch +import InstantSearchCore +import Foundation +import XCTest + +class TestCollectionViewCell: UICollectionViewCell { + var content: String? +} + +class CollectionViewMultiIndexHitsControllerTests: XCTestCase { + + func testDataSource() { + + let collectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout()) + + let hitsSource = TestMultiHitsDataSource(hitsBySection: [["t11", "t12"], ["t21", "t22", "t23"]]) + + let dataSource = MultiIndexHitsCollectionViewDataSource() + + dataSource.setCellConfigurator(forSection: 0) { (_, h: String, _) in + let cell = TestCollectionViewCell() + cell.content = h + return cell + } + + dataSource.hitsSource = hitsSource + + XCTAssertEqual(dataSource.numberOfSections(in: collectionView), 2) + XCTAssertEqual(dataSource.collectionView(collectionView, numberOfItemsInSection: 0), 2) + + } + + func testDelegate() { + + let hitsSource = TestMultiHitsDataSource(hitsBySection: [["t11", "t12"], ["t21", "t22", "t23"]]) + + let delegate = MultiIndexHitsTableViewDelegate() + delegate.hitsSource = hitsSource + + } + + func testMissingHitsSource() { + + let collectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout()) + + let dataSource = MultiIndexHitsCollectionViewDataSource() + + dataSource.setCellConfigurator(forSection: 0) { (_, h: String, _) in + let cell = TestCollectionViewCell() + cell.content = h + return cell + } + + let delegate = MultiIndexHitsCollectionViewDelegate() + + delegate.setClickHandler(forSection: 0) { (_, h: String, _) in + } + + expectFatalError(expectedMessage: "Missing hits source") { + _ = dataSource.numberOfSections(in: collectionView) + } + + expectFatalError(expectedMessage: "Missing hits source") { + _ = dataSource.collectionView(collectionView, numberOfItemsInSection: 0) + } + + expectFatalError(expectedMessage: "Missing hits source") { + _ = dataSource.collectionView(collectionView, cellForItemAt: IndexPath(item: 0, section: 0)) + } + + expectFatalError(expectedMessage: "Missing hits source") { + delegate.collectionView(collectionView, didSelectItemAt: IndexPath(item: 0, section: 0)) + } + + } + + + func testMissingCellHandler() { + + let collectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout()) + + let dataSource = MultiIndexHitsCollectionViewDataSource() + + let hitsSource = TestMultiHitsDataSource(hitsBySection: [["t11", "t12"], ["t21", "t22", "t23"]]) + + dataSource.hitsSource = hitsSource + + expectFatalError(expectedMessage: "No cell configurator found for section 0") { + _ = dataSource.collectionView(collectionView, cellForItemAt: IndexPath(item: 0, section: 0)) + } + + } + + func testMissingClickHandler() { + + let collectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout()) + + let delegate = MultiIndexHitsCollectionViewDelegate() + + let hitsSource = TestMultiHitsDataSource(hitsBySection: [["t11", "t12"], ["t21", "t22", "t23"]]) + + delegate.hitsSource = hitsSource + + expectFatalError(expectedMessage: "No click handler found for section 0") { + _ = delegate.collectionView(collectionView, didSelectItemAt: IndexPath(row: 0, section: 0)) + } + + } + +} diff --git a/Tests/FatalErrorTest.swift b/Tests/FatalErrorTest.swift new file mode 100644 index 00000000..b0ac9401 --- /dev/null +++ b/Tests/FatalErrorTest.swift @@ -0,0 +1,38 @@ +// +// FatalErrorTest.swift +// InstantSearch +// +// Created by Vladislav Fitc on 04/09/2019. +// + +@testable import InstantSearch +import Foundation +import XCTest + +extension XCTestCase { + + func expectFatalError(expectedMessage: String, testcase: @escaping () -> Void) { + + // arrange + let expectation = self.expectation(description: "expectingFatalError") + var assertionMessage: String? = nil + + // override fatalError. This will pause forever when fatalError is called. + FatalErrorUtil.replaceFatalError { message, _, _ in + assertionMessage = message + expectation.fulfill() + unreachable() + } + + // act, perform on separate thead because a call to fatalError pauses forever + DispatchQueue.global(qos: .userInitiated).async(execute: testcase) + + waitForExpectations(timeout: 10) { _ in + // assert + XCTAssertEqual(assertionMessage, expectedMessage) + + // clean up + FatalErrorUtil.restoreFatalError() + } + } +} diff --git a/Tests/Info.plist b/Tests/Info.plist index 18f6dd6b..48f7188c 100644 --- a/Tests/Info.plist +++ b/Tests/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 4.0.0 + 5.1.0 CFBundleSignature ???? CFBundleVersion diff --git a/Tests/TableViewHitsControllerTests.swift b/Tests/TableViewHitsControllerTests.swift index be392e74..bb8a188e 100644 --- a/Tests/TableViewHitsControllerTests.swift +++ b/Tests/TableViewHitsControllerTests.swift @@ -10,27 +10,55 @@ import InstantSearchCore import Foundation import XCTest -class TestHitsSource: HitsSource { - - typealias Hit = String +class TestTemplateCell: UITableViewCell {} - let hits: [String] - - init(hits: [String]) { - self.hits = hits - } - - func numberOfHits() -> Int { - return hits.count - } +class TableViewHitsControllerTests: XCTestCase { - func hit(atIndex index: Int) -> String? { - return hits[index] + func testMissingDataSource() { + + let tableView = UITableView() + + let dataSource = HitsTableViewDataSource { (_, hit, _) -> UITableViewCell in + let cell = UITableViewCell() + cell.textLabel?.text = hit + return cell + } + + expectFatalError(expectedMessage: "Missing hits source") { + _ = dataSource.tableView(tableView, cellForRowAt: .init()) + } + + expectFatalError(expectedMessage: "Missing hits source") { + _ = dataSource.tableView(tableView, numberOfRowsInSection: .init()) + } + + let delegate = HitsTableViewDelegate { _,_,_ in } + + expectFatalError(expectedMessage: "Missing hits source") { + _ = delegate.tableView(tableView, didSelectRowAt: .init()) + } + } -} + func testTemplate() { + + let tableView = UITableView() + + let hitsDataSource = TestHitsSource(hits: ["t1", "t2", "t3"]) + + let dataSource = HitsTableViewDataSource { (_, hit, _) -> UITableViewCell in + let cell = UITableViewCell() + cell.textLabel?.text = hit + return cell + } + + dataSource.hitsSource = hitsDataSource + + dataSource.templateCellProvider = { return TestTemplateCell() } -class TableViewHitsControllerTests: XCTestCase { + XCTAssert(dataSource.tableView(tableView, cellForRowAt: IndexPath(row: 4, section: 0)) is TestTemplateCell, "") + + } func testDataSource() { diff --git a/Tests/TableViewMultiIndexHitsControllerTests.swift b/Tests/TableViewMultiIndexHitsControllerTests.swift index 9ab3aabe..8d1f61b9 100644 --- a/Tests/TableViewMultiIndexHitsControllerTests.swift +++ b/Tests/TableViewMultiIndexHitsControllerTests.swift @@ -10,28 +10,6 @@ import InstantSearchCore import Foundation import XCTest -class TestMultiHitsDataSource: MultiIndexHitsSource { - - let hitsBySection: [[String]] - - init(hitsBySection: [[String]]) { - self.hitsBySection = hitsBySection - } - - func numberOfSections() -> Int { - return hitsBySection.count - } - - func numberOfHits(inSection section: Int) -> Int { - return hitsBySection[section].count - } - - func hit(atIndex index: Int, inSection section: Int) throws -> R? { - return hitsBySection[section][index] as? R - } - -} - class TableViewMultiIndexHitsControllerTests: XCTestCase { func testDataSource() { @@ -64,8 +42,72 @@ class TableViewMultiIndexHitsControllerTests: XCTestCase { } - func testWidget() { + func testMissingHitsSource() { + + let tableView = UITableView() + let dataSource = MultiIndexHitsTableViewDataSource() + + dataSource.setCellConfigurator(forSection: 0) { (_, h: String, _) -> UITableViewCell in + let cell = UITableViewCell() + cell.textLabel?.text = h + return cell + } + + let delegate = MultiIndexHitsTableViewDelegate() + + delegate.setClickHandler(forSection: 0) { (_, h: String, _) in + } + + expectFatalError(expectedMessage: "Missing hits source") { + _ = dataSource.numberOfSections(in: tableView) + } + + expectFatalError(expectedMessage: "Missing hits source") { + _ = dataSource.tableView(tableView, numberOfRowsInSection: 0) + } + + expectFatalError(expectedMessage: "Missing hits source") { + _ = dataSource.tableView(tableView, cellForRowAt: IndexPath(item: 0, section: 0)) + } + + expectFatalError(expectedMessage: "Missing hits source") { + delegate.tableView(tableView, didSelectRowAt: IndexPath(item: 0, section: 0)) + } + + } + + + func testMissingCellHandler() { + + let tableView = UITableView() + + let dataSource = MultiIndexHitsTableViewDataSource() + + let hitsSource = TestMultiHitsDataSource(hitsBySection: [["t11", "t12"], ["t21", "t22", "t23"]]) + + dataSource.hitsSource = hitsSource + + expectFatalError(expectedMessage: "No cell configurator found for section 0") { + _ = dataSource.tableView(tableView, cellForRowAt: IndexPath(item: 0, section: 0)) + } + + } + + func testMissingClickHandler() { + + let tableView = UITableView() + + let delegate = MultiIndexHitsTableViewDelegate() + + let hitsSource = TestMultiHitsDataSource(hitsBySection: [["t11", "t12"], ["t21", "t22", "t23"]]) + + delegate.hitsSource = hitsSource + + expectFatalError(expectedMessage: "No click handler found for section 0") { + _ = delegate.tableView(tableView, didSelectRowAt: IndexPath(row: 0, section: 0)) + } + } } diff --git a/Tests/TestHitsSource.swift b/Tests/TestHitsSource.swift new file mode 100644 index 00000000..08ca33b7 --- /dev/null +++ b/Tests/TestHitsSource.swift @@ -0,0 +1,30 @@ +// +// TestHitsSource.swift +// InstantSearchTests +// +// Created by Vladislav Fitc on 04/09/2019. +// + +@testable import InstantSearch +import Foundation + +class TestHitsSource: HitsSource { + + typealias Hit = String + + let hits: [String] + + init(hits: [String]) { + self.hits = hits + } + + func numberOfHits() -> Int { + return hits.count + } + + func hit(atIndex index: Int) -> String? { + guard index < hits.count else { return nil } + return hits[index] + } + +} diff --git a/Tests/TestMultiHitsDataSource.swift b/Tests/TestMultiHitsDataSource.swift new file mode 100644 index 00000000..e7815396 --- /dev/null +++ b/Tests/TestMultiHitsDataSource.swift @@ -0,0 +1,31 @@ +// +// TestMultiHitsDataSource.swift +// InstantSearchTests +// +// Created by Vladislav Fitc on 04/09/2019. +// + +@testable import InstantSearch +import Foundation + +class TestMultiHitsDataSource: MultiIndexHitsSource { + + let hitsBySection: [[String]] + + init(hitsBySection: [[String]]) { + self.hitsBySection = hitsBySection + } + + func numberOfSections() -> Int { + return hitsBySection.count + } + + func numberOfHits(inSection section: Int) -> Int { + return hitsBySection[section].count + } + + func hit(atIndex index: Int, inSection section: Int) throws -> R? { + return hitsBySection[section][index] as? R + } + +} diff --git a/instantsearch.xcodeproj/project.pbxproj b/instantsearch.xcodeproj/project.pbxproj index 2f10c31f..5082f180 100644 --- a/instantsearch.xcodeproj/project.pbxproj +++ b/instantsearch.xcodeproj/project.pbxproj @@ -23,6 +23,12 @@ AF3DE598224AA00400C8BC08 /* MultiIndexHitsCollectionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF3DE596224AA00400C8BC08 /* MultiIndexHitsCollectionController.swift */; }; AF3DE5BD224BBF7000C8BC08 /* TableViewHitsControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF3DE5A9224BBE6700C8BC08 /* TableViewHitsControllerTests.swift */; }; AF3DE5BE224BBF7000C8BC08 /* TableViewMultiIndexHitsControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF3DE5B1224BBEC000C8BC08 /* TableViewMultiIndexHitsControllerTests.swift */; }; + AF58089123200CCA00C5BDFA /* FatalErrorUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF58089023200CCA00C5BDFA /* FatalErrorUtil.swift */; }; + AF58089423200D0200C5BDFA /* FatalErrorTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF58089223200CFE00C5BDFA /* FatalErrorTest.swift */; }; + AF5808982320158700C5BDFA /* TestMultiHitsDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF5808972320158700C5BDFA /* TestMultiHitsDataSource.swift */; }; + AF58089A232016E300C5BDFA /* CollectionViewMultiIndexHitsControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF5808952320155500C5BDFA /* CollectionViewMultiIndexHitsControllerTests.swift */; }; + AF58089C2320170900C5BDFA /* TestHitsSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF58089B2320170900C5BDFA /* TestHitsSource.swift */; }; + AF58089E2320172A00C5BDFA /* CollectionViewHitsControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF58089D2320172A00C5BDFA /* CollectionViewHitsControllerTests.swift */; }; AF5D103022F45EB300DE13AA /* FacetListTableController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF5D102F22F45EB300DE13AA /* FacetListTableController.swift */; }; AF6778B522FB235600850ACC /* SegmentedController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF6778B422FB235600850ACC /* SegmentedController.swift */; }; AF67793C2301B9C000850ACC /* FilterListTableController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF67793B2301B9C000850ACC /* FilterListTableController.swift */; }; @@ -84,6 +90,12 @@ AF3DE596224AA00400C8BC08 /* MultiIndexHitsCollectionController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MultiIndexHitsCollectionController.swift; sourceTree = ""; }; AF3DE5A9224BBE6700C8BC08 /* TableViewHitsControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableViewHitsControllerTests.swift; sourceTree = ""; }; AF3DE5B1224BBEC000C8BC08 /* TableViewMultiIndexHitsControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableViewMultiIndexHitsControllerTests.swift; sourceTree = ""; }; + AF58089023200CCA00C5BDFA /* FatalErrorUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FatalErrorUtil.swift; sourceTree = ""; }; + AF58089223200CFE00C5BDFA /* FatalErrorTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FatalErrorTest.swift; sourceTree = ""; }; + AF5808952320155500C5BDFA /* CollectionViewMultiIndexHitsControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionViewMultiIndexHitsControllerTests.swift; sourceTree = ""; }; + AF5808972320158700C5BDFA /* TestMultiHitsDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestMultiHitsDataSource.swift; sourceTree = ""; }; + AF58089B2320170900C5BDFA /* TestHitsSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestHitsSource.swift; sourceTree = ""; }; + AF58089D2320172A00C5BDFA /* CollectionViewHitsControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionViewHitsControllerTests.swift; sourceTree = ""; }; AF5D102F22F45EB300DE13AA /* FacetListTableController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FacetListTableController.swift; sourceTree = ""; }; AF6778B422FB235600850ACC /* SegmentedController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SegmentedController.swift; sourceTree = ""; }; AF67793B2301B9C000850ACC /* FilterListTableController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = FilterListTableController.swift; path = Sources/Controller/FilterList/FilterListTableController.swift; sourceTree = SOURCE_ROOT; }; @@ -168,8 +180,13 @@ children = ( E25FB0A21ECF550C006B8CED /* Info.plist */, E2A477021EC62031007CA367 /* InstantSearch-Tests-Bridging-Header.h */, + AF58089B2320170900C5BDFA /* TestHitsSource.swift */, AF3DE5A9224BBE6700C8BC08 /* TableViewHitsControllerTests.swift */, + AF58089D2320172A00C5BDFA /* CollectionViewHitsControllerTests.swift */, + AF5808972320158700C5BDFA /* TestMultiHitsDataSource.swift */, AF3DE5B1224BBEC000C8BC08 /* TableViewMultiIndexHitsControllerTests.swift */, + AF5808952320155500C5BDFA /* CollectionViewMultiIndexHitsControllerTests.swift */, + AF58089223200CFE00C5BDFA /* FatalErrorTest.swift */, ); path = Tests; sourceTree = ""; @@ -177,6 +194,7 @@ 28F828971C494B4200330CF4 /* Sources */ = { isa = PBXGroup; children = ( + AF58089023200CCA00C5BDFA /* FatalErrorUtil.swift */, AF6C6F7A2253C5A1001E5E3C /* Controller */, 28F828821C494B2C00330CF4 /* Info.plist */, E2A477001EC61B43007CA367 /* InstantSearch-Bridging-Header.h */, @@ -473,6 +491,7 @@ }; 28F828861C494B2C00330CF4 = { CreatedOnToolsVersion = 7.2; + DevelopmentTeam = JF98Q6D3JZ; LastSwiftMigration = 0900; TestTargetID = E27B67E01ED339290001FFA7; }; @@ -574,6 +593,7 @@ AF3DE594224A9B7A00C8BC08 /* MultiIndexHitsTableController.swift in Sources */, AF9396812298140D00757257 /* ActivityIndicatorController.swift in Sources */, AF67793C2301B9C000850ACC /* FilterListTableController.swift in Sources */, + AF58089123200CCA00C5BDFA /* FatalErrorUtil.swift in Sources */, E2EFA5F022AEB980008C9CAA /* NumericStepperController.swift in Sources */, AF3DE597224AA00400C8BC08 /* HitsCollectionController.swift in Sources */, AF14E31022F43C710088D2A7 /* MultiIndexHitsTableViewDelegate.swift in Sources */, @@ -605,7 +625,12 @@ buildActionMask = 2147483647; files = ( AF3DE5BD224BBF7000C8BC08 /* TableViewHitsControllerTests.swift in Sources */, + AF58089423200D0200C5BDFA /* FatalErrorTest.swift in Sources */, + AF58089C2320170900C5BDFA /* TestHitsSource.swift in Sources */, + AF5808982320158700C5BDFA /* TestMultiHitsDataSource.swift in Sources */, + AF58089A232016E300C5BDFA /* CollectionViewMultiIndexHitsControllerTests.swift in Sources */, AF3DE5BE224BBF7000C8BC08 /* TableViewMultiIndexHitsControllerTests.swift in Sources */, + AF58089E2320172A00C5BDFA /* CollectionViewHitsControllerTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -839,6 +864,7 @@ buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CLANG_ENABLE_MODULES = YES; + DEVELOPMENT_TEAM = JF98Q6D3JZ; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Carthage/Build/iOS", @@ -860,6 +886,7 @@ buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CLANG_ENABLE_MODULES = YES; + DEVELOPMENT_TEAM = JF98Q6D3JZ; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Carthage/Build/iOS",