Repo created

This commit is contained in:
Fr4nz D13trich 2025-11-22 13:58:55 +01:00
parent 4af19165ec
commit 68073add76
12458 changed files with 12350765 additions and 2 deletions

View file

@ -0,0 +1,32 @@
class MockRecentlyDeletedCategoriesManager: NSObject, RecentlyDeletedCategoriesManager, BookmarksObservable {
var categories = [RecentlyDeletedCategory]()
func recentlyDeletedCategoriesCount() -> UInt64 {
UInt64(categories.count)
}
func getRecentlyDeletedCategories() -> [RecentlyDeletedCategory] {
categories
}
func deleteFile(at urls: [URL]) {
categories.removeAll { urls.contains($0.fileURL) }
}
func deleteAllRecentlyDeletedCategories() {
categories.removeAll()
}
func recoverRecentlyDeletedCategories(at urls: [URL]) {
categories.removeAll { urls.contains($0.fileURL) }
}
func deleteRecentlyDeletedCategory(at urls: [URL]) {
categories.removeAll { urls.contains($0.fileURL) }
}
func add(_ observer: any BookmarksObserver) {}
func remove(_ observer: any BookmarksObserver) {}
}

View file

@ -0,0 +1,203 @@
import XCTest
@testable import CoMaps__Debug_
final class RecentlyDeletedCategoriesViewModelTests: XCTestCase {
var viewModel: RecentlyDeletedCategoriesViewModel!
var bookmarksManagerMock: MockRecentlyDeletedCategoriesManager!
override func setUp() {
super.setUp()
bookmarksManagerMock = MockRecentlyDeletedCategoriesManager()
setupBookmarksManagerStubs()
viewModel = RecentlyDeletedCategoriesViewModel(bookmarksManager: bookmarksManagerMock)
}
override func tearDown() {
viewModel = nil
bookmarksManagerMock = nil
super.tearDown()
}
private func setupBookmarksManagerStubs() {
bookmarksManagerMock.categories = [
RecentlyDeletedCategory(title: "test1", fileURL: URL(string: "test1")!, deletionDate: Date()),
RecentlyDeletedCategory(title: "test2", fileURL: URL(string: "test2")!, deletionDate: Date()),
RecentlyDeletedCategory(title: "lol", fileURL: URL(string: "lol")!, deletionDate: Date()),
RecentlyDeletedCategory(title: "te1", fileURL: URL(string: "te1")!, deletionDate: Date()),
]
}
func testInitializationFetchesCategories() {
XCTAssertEqual(viewModel.state, .nothingSelected)
XCTAssertEqual(viewModel.filteredDataSource.flatMap { $0.content }.count, Int(bookmarksManagerMock.recentlyDeletedCategoriesCount()))
}
// MARK: - Selection Tests
func testMultipleSelectionAndDeselection() {
viewModel.selectAllCategories()
let initialSelectedCount = viewModel.selectedIndexPaths.count
XCTAssertEqual(initialSelectedCount, viewModel.filteredDataSource.flatMap { $0.content }.count)
viewModel.deselectAllCategories()
XCTAssertTrue(viewModel.selectedIndexPaths.isEmpty)
}
func testSelectAndDeselectSpecificCategory() {
let specificIndexPath = IndexPath(row: 0, section: 0)
viewModel.selectCategory(at: specificIndexPath)
XCTAssertTrue(viewModel.selectedIndexPaths.contains(specificIndexPath))
viewModel.deselectCategory(at: specificIndexPath)
XCTAssertFalse(viewModel.selectedIndexPaths.contains(specificIndexPath))
}
func testSelectAndDeselectSpecificCategories() {
let indexPath1 = IndexPath(row: 0, section: 0)
let indexPath2 = IndexPath(row: 1, section: 0)
let indexPath3 = IndexPath(row: 2, section: 0)
viewModel.selectCategory(at: indexPath1)
viewModel.selectCategory(at: indexPath2)
viewModel.selectCategory(at: indexPath3)
XCTAssertTrue(viewModel.selectedIndexPaths.contains(indexPath1))
XCTAssertTrue(viewModel.selectedIndexPaths.contains(indexPath2))
XCTAssertTrue(viewModel.selectedIndexPaths.contains(indexPath3))
viewModel.deselectCategory(at: indexPath1)
XCTAssertFalse(viewModel.selectedIndexPaths.contains(indexPath1))
XCTAssertEqual(viewModel.state, .someSelected)
viewModel.deselectCategory(at: indexPath2)
viewModel.deselectCategory(at: indexPath3)
XCTAssertEqual(viewModel.selectedIndexPaths.count, .zero)
XCTAssertEqual(viewModel.state, .nothingSelected)
}
func testStateChangesOnSelection() {
let indexPath = IndexPath(row: 1, section: 0)
viewModel.selectCategory(at: indexPath)
XCTAssertEqual(viewModel.state, .someSelected)
viewModel.deselectCategory(at: indexPath)
XCTAssertEqual(viewModel.state, .nothingSelected)
}
func testStateChangesOnDone() {
let indexPath = IndexPath(row: 1, section: 0)
viewModel.selectCategory(at: indexPath)
XCTAssertEqual(viewModel.state, .someSelected)
viewModel.cancelSelecting()
XCTAssertEqual(viewModel.filteredDataSource.flatMap { $0.content }.count, Int(bookmarksManagerMock.recentlyDeletedCategoriesCount()))
}
// MARK: - Searching Tests
func testSearchWithEmptyString() {
viewModel.search("")
XCTAssertEqual(viewModel.filteredDataSource.flatMap { $0.content }.count, 4)
}
func testSearchWithNoResults() {
viewModel.search("xyz") // Assuming "xyz" matches no category names
XCTAssertTrue(viewModel.filteredDataSource.allSatisfy { $0.content.isEmpty })
}
func testCancelSearchRestoresDataSource() {
let searchText = "test"
viewModel.search(searchText)
XCTAssertEqual(viewModel.state, .searching)
XCTAssertTrue(viewModel.filteredDataSource.allSatisfy { $0.content.allSatisfy { $0.title.localizedCaseInsensitiveContains(searchText) } })
XCTAssertEqual(viewModel.filteredDataSource.flatMap { $0.content }.count, 2)
viewModel.cancelSearching()
XCTAssertEqual(viewModel.state, .nothingSelected)
XCTAssertEqual(viewModel.filteredDataSource.flatMap { $0.content }.count, 4)
}
// MARK: - Deletion Tests
func testDeleteCategory() {
let initialCount = bookmarksManagerMock.categories.count
viewModel.deleteCategory(at: IndexPath(row: 0, section: 0))
XCTAssertEqual(bookmarksManagerMock.categories.count, initialCount - 1)
}
func testDeleteAllWhenNoOneIsSelected() {
viewModel.deleteSelectedCategories()
XCTAssertEqual(bookmarksManagerMock.categories.count, .zero)
}
func testDeleteAllWhenNoSoneAreSelected() {
viewModel.selectCategory(at: IndexPath(row: 0, section: 0))
viewModel.selectCategory(at: IndexPath(row: 1, section: 0))
viewModel.deleteSelectedCategories()
XCTAssertEqual(viewModel.state, .nothingSelected)
XCTAssertEqual(bookmarksManagerMock.categories.count, 2)
XCTAssertEqual(viewModel.filteredDataSource.flatMap { $0.content }.count, 2)
XCTAssertEqual(viewModel.filteredDataSource.flatMap { $0.content }.count, Int(bookmarksManagerMock.recentlyDeletedCategoriesCount()))
}
// MARK: - Recovery Tests
func testRecoverCategory() {
viewModel.recoverCategory(at: IndexPath(row: 0, section: 0))
XCTAssertEqual(viewModel.state, .nothingSelected)
XCTAssertEqual(bookmarksManagerMock.categories.count, 3)
XCTAssertEqual(viewModel.state, .nothingSelected)
}
func testRecoverAll() {
viewModel.recoverSelectedCategories()
XCTAssertEqual(viewModel.state, .nothingSelected)
XCTAssertEqual(bookmarksManagerMock.categories.count, 0)
}
func testRecoverAllWhenSomeAreSelected() {
viewModel.selectCategory(at: IndexPath(row: 0, section: 0))
viewModel.selectCategory(at: IndexPath(row: 1, section: 0))
viewModel.recoverSelectedCategories()
XCTAssertEqual(viewModel.state, .nothingSelected)
XCTAssertEqual(bookmarksManagerMock.categories.count, 2)
XCTAssertEqual(viewModel.filteredDataSource.flatMap { $0.content }.count, Int(bookmarksManagerMock.recentlyDeletedCategoriesCount()))
}
func testSearchFiltersCategories() {
var searchText = "test"
viewModel.search(searchText)
XCTAssertEqual(viewModel.state, .searching)
XCTAssertTrue(viewModel.filteredDataSource.allSatisfy { $0.content.allSatisfy { $0.title.localizedCaseInsensitiveContains(searchText) } })
searchText = "te"
viewModel.search(searchText)
XCTAssertEqual(viewModel.state, .searching)
XCTAssertTrue(viewModel.filteredDataSource.allSatisfy { $0.content.allSatisfy { $0.title.localizedCaseInsensitiveContains(searchText) } })
}
func testDeleteAllCategories() {
viewModel.deleteSelectedCategories()
XCTAssertTrue(bookmarksManagerMock.categories.isEmpty)
}
func testRecoverAllCategories() {
viewModel.recoverSelectedCategories()
XCTAssertTrue(bookmarksManagerMock.categories.isEmpty)
}
func testDeleteAndRecoverAllCategoriesWhenEmpty() {
bookmarksManagerMock.categories = []
viewModel.fetchRecentlyDeletedCategories()
viewModel.deleteSelectedCategories()
viewModel.recoverSelectedCategories()
XCTAssertTrue(viewModel.filteredDataSource.isEmpty)
}
func testMultipleStateTransitions() {
viewModel.startSelecting()
XCTAssertEqual(viewModel.state, .nothingSelected)
viewModel.startSearching()
XCTAssertEqual(viewModel.state, .searching)
viewModel.cancelSearching()
viewModel.cancelSelecting()
XCTAssertEqual(viewModel.state, .nothingSelected)
}
}

View file

@ -0,0 +1,40 @@
import XCTest
@testable import CoMaps__Debug_
final class CarPlayServiceTests: XCTestCase {
var carPlayService: CarPlayService!
override func setUp() {
super.setUp()
carPlayService = CarPlayService()
}
override func tearDown() {
carPlayService = nil
super.tearDown()
}
func testCreateEstimates() {
let routeInfo = RouteInfo(timeToTarget: 100,
targetDistance: 25.2,
targetUnitsIndex: 1, // km
distanceToTurn: 0.5,
turnUnitsIndex: 0, // m
streetName: "Niamiha",
turnImageName: nil,
nextTurnImageName: nil,
speedMps: 40.5,
speedLimitMps: 60,
roundExitNumber: 0)
let estimates = carPlayService.createEstimates(routeInfo: routeInfo)
guard let estimates else {
XCTFail("Estimates should not be nil.")
return
}
XCTAssertEqual(estimates.distanceRemaining, Measurement<UnitLength>(value: 25.2, unit: .kilometers))
XCTAssertEqual(estimates.timeRemaining, 100)
}
}

View file

@ -0,0 +1,28 @@
#import <XCTest/XCTest.h>
#import "MWMTextToSpeech+CPP.h"
@interface MWMTextToSpeechTest : XCTestCase
@end
@implementation MWMTextToSpeechTest
- (void)testAvailableLanguages {
MWMTextToSpeech * tts = [MWMTextToSpeech tts];
std::vector<std::pair<std::string, std::string>> langs = tts.availableLanguages;
decltype(langs)::value_type const defaultLang = std::make_pair("en-US", "English (United States)");
XCTAssertTrue(std::find(langs.begin(), langs.end(), defaultLang) != langs.end());
}
- (void)testTranslateLocaleWithTwineString {
XCTAssertEqual(tts::translateLocale("en"), "English");
}
- (void)testTranslateLocaleWithBcp47String {
XCTAssertEqual(tts::translateLocale("en-US"), "English (United States)");
}
- (void)testTranslateLocaleWithUnknownString {
XCTAssertEqual(tts::translateLocale("unknown"), "");
}
@end

View file

@ -0,0 +1,28 @@
#import <XCTest/XCTest.h>
#import "TTSTester.h"
@interface TTSTesterTest : XCTestCase
@end
@implementation TTSTesterTest
TTSTester * ttsTester;
- (void)setUp {
ttsTester = [[TTSTester alloc] init];
}
- (void)testTestStringsWithEnglish {
XCTAssertTrue([[ttsTester getTestStrings:@"en-US"] containsObject: @"Thank you for using our community-built maps!"]);
}
- (void)testTestStringsWithGerman {
XCTAssertTrue([[ttsTester getTestStrings:@"de-DE"] containsObject: @"Danke, dass du unsere von der Community erstellten Karten benutzt!"]);
}
- (void)testTestStringsWithInvalidLanguage {
XCTAssertNil([ttsTester getTestStrings:@"xxx"]);
}
@end

View file

@ -0,0 +1,195 @@
import XCTest
@testable import CoMaps__Debug_
final class TrackRecordingManagerTests: XCTestCase {
private var trackRecordingManager: TrackRecordingManager!
private var mockTrackRecorder: MockTrackRecorder.Type!
private var mockLocationService: MockLocationService.Type!
private var mockActivityManager: MockTrackRecordingActivityManager!
override func setUp() {
super.setUp()
mockTrackRecorder = MockTrackRecorder.self
mockLocationService = MockLocationService.self
mockActivityManager = MockTrackRecordingActivityManager()
trackRecordingManager = TrackRecordingManager(
trackRecorder: mockTrackRecorder,
locationService: mockLocationService,
activityManager: mockActivityManager
)
}
override func tearDown() {
trackRecordingManager = nil
mockTrackRecorder.reset()
mockLocationService.reset()
mockActivityManager = nil
super.tearDown()
}
func test_GivenInitialSetup_WhenLocationEnabled_ThenStateIsInactive() {
mockLocationService.locationIsProhibited = false
mockTrackRecorder.trackRecordingIsEnabled = false
trackRecordingManager.setup()
XCTAssertTrue(trackRecordingManager.recordingState == .inactive)
}
func test_GivenInitialSetup_WhenLocationDisabled_ThenShouldHandleErrorAndIncativeState() {
mockLocationService.locationIsProhibited = true
trackRecordingManager.setup()
XCTAssertTrue(mockLocationService.checkLocationStatusCalled)
XCTAssertTrue(trackRecordingManager.recordingState == .inactive)
}
func test_GivenStartRecording_WhenLocationEnabled_ThenSuccess() {
mockLocationService.locationIsProhibited = false
mockTrackRecorder.trackRecordingIsEnabled = false
trackRecordingManager.start()
XCTAssertTrue(mockTrackRecorder.startTrackRecordingCalled)
XCTAssertTrue(mockActivityManager.startCalled)
XCTAssertTrue(trackRecordingManager.recordingState == .active)
}
func test_GivenStartRecording_WhenLocationDisabled_ThenShouldFail() {
mockLocationService.locationIsProhibited = true
let expectation = expectation(description: "Location is prohibited")
trackRecordingManager.start() { result in
switch result {
case .success:
XCTFail("Should not succeed")
case .failure(let error):
switch error {
case LocationError.locationIsProhibited:
expectation.fulfill()
default:
XCTFail("Unexpected error: \(error)")
}
}
}
wait(for: [expectation], timeout: 1.0)
XCTAssertFalse(self.mockTrackRecorder.startTrackRecordingCalled)
XCTAssertTrue(trackRecordingManager.recordingState == .inactive)
}
func test_GivenStopRecording_WhenLocationEnabled_ThenSuccess() {
mockTrackRecorder.trackRecordingIsEnabled = true
mockTrackRecorder.trackRecordingIsEmpty = false
trackRecordingManager.stopAndSave(withName: "Test Track") { result in
switch result {
case .success:
XCTAssertTrue(true)
case .trackIsEmpty:
XCTFail("Track should not be empty")
}
}
XCTAssertTrue(mockTrackRecorder.stopTrackRecordingCalled)
XCTAssertTrue(mockTrackRecorder.saveTrackRecordingCalled)
XCTAssertTrue(mockActivityManager.stopCalled)
XCTAssertTrue(trackRecordingManager.recordingState == .inactive)
}
func test_GivenStopRecording_WhenTrackIsEmpty_ThenShouldFail() {
mockTrackRecorder.trackRecordingIsEnabled = true
mockTrackRecorder.trackRecordingIsEmpty = true
let expectation = expectation(description: "Track recording should be empty")
trackRecordingManager.stopAndSave(withName: "Test Track") { result in
switch result {
case .success:
XCTFail("Should not succeed")
case .trackIsEmpty:
expectation.fulfill()
}
}
wait(for: [expectation], timeout: 1.0)
XCTAssertFalse(mockTrackRecorder.saveTrackRecordingCalled)
XCTAssertTrue(trackRecordingManager.recordingState == .inactive)
}
}
// MARK: - Mock Classes
private final class MockTrackRecorder: TrackRecorder {
static var trackRecordingIsEnabled = false
static var trackRecordingIsEmpty = false
static var startTrackRecordingCalled = false
static var stopTrackRecordingCalled = false
static var saveTrackRecordingCalled = false
static func reset() {
trackRecordingIsEnabled = false
trackRecordingIsEmpty = false
startTrackRecordingCalled = false
stopTrackRecordingCalled = false
saveTrackRecordingCalled = false
}
static func isTrackRecordingEnabled() -> Bool {
return trackRecordingIsEnabled
}
static func isTrackRecordingEmpty() -> Bool {
return trackRecordingIsEmpty
}
static func startTrackRecording() {
startTrackRecordingCalled = true
trackRecordingIsEnabled = true
}
static func stopTrackRecording() {
stopTrackRecordingCalled = true
trackRecordingIsEnabled = false
}
static func saveTrackRecording(withName name: String) {
saveTrackRecordingCalled = true
}
static func setTrackRecordingUpdateHandler(_ handler: ((TrackInfo) -> Void)?) {}
static func trackRecordingElevationInfo() -> ElevationProfileData {
ElevationProfileData()
}
}
private final class MockLocationService: LocationService {
static var locationIsProhibited = false
static var checkLocationStatusCalled = false
static func reset() {
locationIsProhibited = false
checkLocationStatusCalled = false
}
static func isLocationProhibited() -> Bool {
return locationIsProhibited
}
static func checkLocationStatus() {
checkLocationStatusCalled = true
}
}
final class MockTrackRecordingActivityManager: TrackRecordingActivityManager {
var startCalled = false
var stopCalled = false
func start(with info: TrackInfo) throws {
startCalled = true
}
func stop() {
stopCalled = true
}
func update(_ info: TrackInfo) {}
}

View file

@ -0,0 +1,134 @@
import XCTest
@testable import CoMaps__Debug_
final class DefaultLocalDirectoryMonitorTests: XCTestCase {
let fileManager = FileManager.default
let tempDirectory = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
var directoryMonitor: FileSystemDispatchSourceMonitor!
var mockDelegate: LocalDirectoryMonitorDelegateMock!
override func setUpWithError() throws {
try super.setUpWithError()
// Setup with a temporary directory and a mock delegate
directoryMonitor = try FileSystemDispatchSourceMonitor(fileManager: fileManager, directory: tempDirectory)
mockDelegate = LocalDirectoryMonitorDelegateMock()
directoryMonitor.delegate = mockDelegate
}
override func tearDownWithError() throws {
directoryMonitor.stop()
mockDelegate = nil
try? fileManager.removeItem(at: tempDirectory)
try super.tearDownWithError()
}
func testInitialization() {
XCTAssertEqual(directoryMonitor.directory, tempDirectory, "Monitor initialized with incorrect directory.")
XCTAssertTrue(directoryMonitor.state == .stopped, "Monitor should be stopped initially.")
}
func testStartMonitoring() {
let startExpectation = expectation(description: "Start monitoring")
directoryMonitor.start { result in
switch result {
case .success:
XCTAssertTrue(self.directoryMonitor.state == .started, "Monitor should be started.")
case .failure(let error):
XCTFail("Monitoring failed to start with error: \(error)")
}
startExpectation.fulfill()
}
wait(for: [startExpectation], timeout: 5.0)
}
func testStopMonitoring() {
directoryMonitor.start()
directoryMonitor.stop()
XCTAssertTrue(directoryMonitor.state == .stopped, "Monitor should be stopped.")
}
func testPauseAndResumeMonitoring() {
directoryMonitor.start()
directoryMonitor.pause()
XCTAssertTrue(directoryMonitor.state == .paused, "Monitor should be paused.")
directoryMonitor.resume()
XCTAssertTrue(directoryMonitor.state == .started, "Monitor should be started.")
}
func testDelegateDidFinishGathering() {
mockDelegate.didFinishGatheringExpectation = expectation(description: "didFinishGathering called")
directoryMonitor.start()
wait(for: [mockDelegate.didFinishGatheringExpectation!], timeout: 5.0)
}
func testDelegateDidReceiveError() {
mockDelegate.didReceiveErrorExpectation = expectation(description: "didReceiveLocalMonitorError called")
let error = NSError(domain: NSCocoaErrorDomain, code: NSFileNoSuchFileError, userInfo: nil)
directoryMonitor.delegate?.didReceiveLocalMonitorError(error)
wait(for: [mockDelegate.didReceiveErrorExpectation!], timeout: 1.0)
}
func testContentUpdateDetection() {
let startExpectation = expectation(description: "Start monitoring")
let didFinishGatheringExpectation = expectation(description: "didFinishGathering called")
let didUpdateExpectation = expectation(description: "didUpdate called")
mockDelegate.didFinishGatheringExpectation = didFinishGatheringExpectation
mockDelegate.didUpdateExpectation = didUpdateExpectation
directoryMonitor.start { result in
if case .success = result {
XCTAssertTrue(self.directoryMonitor.state == .started, "Monitor should be started.")
}
startExpectation.fulfill()
}
wait(for: [startExpectation], timeout: 5)
let fileURL = tempDirectory.appendingPathComponent("test.kml")
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
self.fileManager.createFile(atPath: fileURL.path, contents: Data(), attributes: nil)
}
wait(for: [didFinishGatheringExpectation, didUpdateExpectation], timeout: 20)
}
func testFileWithIncorrectExtension() {
let startExpectation = expectation(description: "Start monitoring")
let didFinishGatheringExpectation = expectation(description: "didFinishGathering called")
mockDelegate.didFinishGatheringExpectation = didFinishGatheringExpectation
let file1URL = tempDirectory.appendingPathComponent("test.kml.tmp")
let file2URL = tempDirectory.appendingPathComponent("test2.tmp")
let file3URL = tempDirectory.appendingPathComponent("test3.jpg")
let correctFileURL = tempDirectory.appendingPathComponent("test.kml")
let fileData = Data(count: 12)
try! fileData.write(to: file1URL, options: .atomic)
try! fileData.write(to: file2URL, options: .atomic)
try! fileData.write(to: file3URL, options: .atomic)
try! fileData.write(to: correctFileURL, options: .atomic)
directoryMonitor.start { result in
switch result {
case .failure(let error):
XCTFail("Monitoring failed to start with error: \(error)")
case .success:
XCTAssertTrue(self.directoryMonitor.state == .started, "Monitor should be started.")
startExpectation.fulfill()
}
}
wait(for: [startExpectation, didFinishGatheringExpectation], timeout: 5)
let contents = self.mockDelegate.contents.map { $0.fileUrl }
XCTAssertFalse(contents.contains(file1URL), "File with incorrect extension should not be included")
XCTAssertFalse(contents.contains(file2URL), "File with incorrect extension should not be included")
XCTAssertFalse(contents.contains(file3URL), "File with incorrect extension should not be included")
XCTAssertTrue(contents.contains(correctFileURL), "File with correct extension should be included")
}
}

View file

@ -0,0 +1,24 @@
import XCTest
@testable import CoMaps__Debug_
class LocalDirectoryMonitorDelegateMock: LocalDirectoryMonitorDelegate {
var contents = LocalContents()
var didFinishGatheringExpectation: XCTestExpectation?
var didUpdateExpectation: XCTestExpectation?
var didReceiveErrorExpectation: XCTestExpectation?
func didFinishGathering(_ contents: LocalContents) {
self.contents = contents
didFinishGatheringExpectation?.fulfill()
}
func didUpdate(_ contents: LocalContents, _ update: LocalContentsUpdate) {
self.contents = contents
didUpdateExpectation?.fulfill()
}
func didReceiveLocalMonitorError(_ error: Error) {
didReceiveErrorExpectation?.fulfill()
}
}

View file

@ -0,0 +1,30 @@
@testable import CoMaps__Debug_
extension LocalMetadataItem {
static func stub(fileName: String,
lastModificationDate: TimeInterval) -> LocalMetadataItem {
let item = LocalMetadataItem(fileName: fileName,
fileUrl: URL(string: "url")!,
lastModificationDate: lastModificationDate)
return item
}
}
extension CloudMetadataItem {
static func stub(fileName: String,
lastModificationDate: TimeInterval,
isDownloaded: Bool = true,
percentDownloaded: NSNumber = 100.0,
hasUnresolvedConflicts: Bool = false) -> CloudMetadataItem {
let item = CloudMetadataItem(fileName: fileName,
fileUrl: URL(string: "url")!,
isDownloaded: isDownloaded,
percentDownloaded: percentDownloaded,
lastModificationDate: lastModificationDate,
downloadingError: nil,
uploadingError: nil,
hasUnresolvedConflicts: hasUnresolvedConflicts)
return item
}
}

View file

@ -0,0 +1,573 @@
import XCTest
@testable import CoMaps__Debug_
final class SynchronizationtateManagerTests: XCTestCase {
var syncStateManager: SynchronizationStateResolver!
var outgoingEvents: [OutgoingSynchronizationEvent] = []
override func setUp() {
super.setUp()
syncStateManager = iCloudSynchronizationStateResolver(isInitialSynchronization: false)
}
override func tearDown() {
syncStateManager = nil
outgoingEvents.removeAll()
super.tearDown()
}
// MARK: - Test didFinishGathering without errors and on initial synchronization
func testInitialSynchronization() {
syncStateManager = iCloudSynchronizationStateResolver(isInitialSynchronization: true)
let localItem1 = LocalMetadataItem.stub(fileName: "file1", lastModificationDate: TimeInterval(1))
let localItem2 = LocalMetadataItem.stub(fileName: "file2", lastModificationDate: TimeInterval(3)) // Local only item
let cloudItem1 = CloudMetadataItem.stub(fileName: "file1", lastModificationDate: TimeInterval(2)) // Conflicting item
let cloudItem3 = CloudMetadataItem.stub(fileName: "file3", lastModificationDate: TimeInterval(4)) // Cloud only item
let localItems: LocalContents = [localItem1, localItem2]
let cloudItems: CloudContents = [cloudItem1, cloudItem3]
outgoingEvents.append(contentsOf: syncStateManager.resolveEvent(.didFinishGatheringLocalContents(localItems)))
outgoingEvents.append(contentsOf: syncStateManager.resolveEvent(.didFinishGatheringCloudContents(cloudItems)))
XCTAssertTrue(outgoingEvents.contains { event in
if case .resolveInitialSynchronizationConflict(let item) = event, item == localItem1 {
return true
}
return false
}, "Expected to resolve initial synchronization conflict for localItem1")
XCTAssertTrue(outgoingEvents.contains { event in
if case .createLocalItem(let item) = event, item == cloudItem3 {
return true
}
return false
}, "Expected to create local item for cloudItem3")
XCTAssertTrue(outgoingEvents.contains { event in
if case .createCloudItem(let item) = event, item == localItem2 {
return true
}
return false
}, "Expected to create cloud item for localItem2")
XCTAssertTrue(outgoingEvents.contains { event in
if case .didFinishInitialSynchronization = event {
return true
}
return false
}, "Expected to finish initial synchronization")
}
func testInitialSynchronizationWithNewerCloudItem() {
syncStateManager = iCloudSynchronizationStateResolver(isInitialSynchronization: true)
let localItem = LocalMetadataItem.stub(fileName: "file", lastModificationDate: TimeInterval(1))
let cloudItem = CloudMetadataItem.stub(fileName: "file", lastModificationDate: TimeInterval(2))
outgoingEvents.append(contentsOf: syncStateManager.resolveEvent(.didFinishGatheringLocalContents([localItem])))
outgoingEvents.append(contentsOf: syncStateManager.resolveEvent(.didFinishGatheringCloudContents([cloudItem])))
XCTAssertTrue(outgoingEvents.contains { if case .resolveInitialSynchronizationConflict(_) = $0 { return true } else { return false } }, "Expected conflict resolution for a newer cloud item")
}
func testInitialSynchronizationWithNewerLocalItem() {
syncStateManager = iCloudSynchronizationStateResolver(isInitialSynchronization: true)
let localItem = LocalMetadataItem.stub(fileName: "file", lastModificationDate: TimeInterval(2))
let cloudItem = CloudMetadataItem.stub(fileName: "file", lastModificationDate: TimeInterval(1))
outgoingEvents.append(contentsOf: syncStateManager.resolveEvent(.didFinishGatheringLocalContents([localItem])))
outgoingEvents.append(contentsOf: syncStateManager.resolveEvent(.didFinishGatheringCloudContents([cloudItem])))
XCTAssertTrue(outgoingEvents.contains { if case .resolveInitialSynchronizationConflict(_) = $0 { return true } else { return false } }, "Expected conflict resolution for a newer local item")
}
func testInitialSynchronizationWithNonConflictingItems() {
syncStateManager = iCloudSynchronizationStateResolver(isInitialSynchronization: true)
let localItem = LocalMetadataItem.stub(fileName: "localFile", lastModificationDate: TimeInterval(1))
let cloudItem = CloudMetadataItem.stub(fileName: "cloudFile", lastModificationDate: TimeInterval(2))
outgoingEvents.append(contentsOf: syncStateManager.resolveEvent(.didFinishGatheringLocalContents([localItem])))
outgoingEvents.append(contentsOf: syncStateManager.resolveEvent(.didFinishGatheringCloudContents([cloudItem])))
XCTAssertTrue(outgoingEvents.contains { if case .createLocalItem(_) = $0 { return true } else { return false } }, "Expected creation of local item for cloudFile")
XCTAssertTrue(outgoingEvents.contains { if case .createCloudItem(_) = $0 { return true } else { return false } }, "Expected creation of cloud item for localFile")
}
func testInitialSynchronizationWhenCloudFilesAreNotDownloadedTheDownloadingShouldStart () {
syncStateManager = iCloudSynchronizationStateResolver(isInitialSynchronization: true)
let localItem1 = LocalMetadataItem.stub(fileName: "file1", lastModificationDate: TimeInterval(1))
let cloudItem1 = CloudMetadataItem.stub(fileName: "file1", lastModificationDate: TimeInterval(2))
let cloudItem2 = CloudMetadataItem.stub(fileName: "file2", lastModificationDate: TimeInterval(3), isDownloaded: false, percentDownloaded: 0.0)
let cloudItem3 = CloudMetadataItem.stub(fileName: "file3", lastModificationDate: TimeInterval(4))
let localItems = LocalContents([localItem1])
let cloudItems = CloudContents([cloudItem1, cloudItem2, cloudItem3])
outgoingEvents.append(contentsOf: syncStateManager.resolveEvent(.didFinishGatheringLocalContents(localItems)))
outgoingEvents.append(contentsOf: syncStateManager.resolveEvent(.didFinishGatheringCloudContents(cloudItems)))
XCTAssertEqual(outgoingEvents.count, 5)
outgoingEvents.forEach { event in
switch event {
case .resolveInitialSynchronizationConflict(let item):
// copy local file with a new name and replace the original with the cloud file
XCTAssertEqual(item, localItem1)
case .updateLocalItem(let item):
XCTAssertEqual(item, cloudItem1)
case .startDownloading(let item):
XCTAssertEqual(item, cloudItem2)
case .createLocalItem(let item):
XCTAssertEqual(item, cloudItem3)
case .didFinishInitialSynchronization:
XCTAssertTrue(event == outgoingEvents.last)
default:
XCTFail()
}
}
// update the cloud items with the new downloaded status
let cloudItem2Downloaded = CloudMetadataItem.stub(fileName: "file2", lastModificationDate: TimeInterval(3))
let newCloudItems = [cloudItem1, cloudItem2Downloaded, cloudItem3]
let cloudUpdate = CloudContentsUpdate(added: [], updated: [cloudItem2Downloaded], removed: [])
outgoingEvents = syncStateManager.resolveEvent(.didUpdateCloudContents(contents: newCloudItems, update: cloudUpdate))
XCTAssertEqual(outgoingEvents.count, 1)
outgoingEvents.forEach { event in
switch event {
case .createLocalItem(let item):
XCTAssertEqual(item, cloudItem2Downloaded)
default:
XCTFail()
}
}
}
// MARK: - Test didFinishGathering without errors and after initial synchronization
func testDidFinishGatheringWhenCloudAndLocalIsEmpty() {
let localItems: LocalContents = []
let cloudItems: CloudContents = []
outgoingEvents = syncStateManager.resolveEvent(.didFinishGatheringCloudContents(cloudItems))
XCTAssertEqual(outgoingEvents.count, 0)
outgoingEvents = syncStateManager.resolveEvent(.didFinishGatheringLocalContents(localItems))
XCTAssertEqual(outgoingEvents.count, 0)
}
func testDidFinishGatheringWhenOnlyCloudIsEmpty() {
let localItem1 = LocalMetadataItem.stub(fileName: "file1", lastModificationDate: TimeInterval(1))
let localItem2 = LocalMetadataItem.stub(fileName: "file2", lastModificationDate: TimeInterval(2))
let localItem3 = LocalMetadataItem.stub(fileName: "file3", lastModificationDate: TimeInterval(3))
let localItems: LocalContents = LocalContents([localItem1, localItem2, localItem3])
let cloudItems: CloudContents = []
outgoingEvents = syncStateManager.resolveEvent(.didFinishGatheringCloudContents(cloudItems))
XCTAssertEqual(outgoingEvents.count, 0)
outgoingEvents = syncStateManager.resolveEvent(.didFinishGatheringLocalContents(localItems))
outgoingEvents.forEach { event in
switch event {
case .createCloudItem(let item):
XCTAssertTrue(localItems.containsByName(item))
default:
XCTFail()
}
}
}
func testDidFinishGatheringWhenOnlyLocalIsEmpty() {
let cloudItem1 = CloudMetadataItem.stub(fileName: "file1", lastModificationDate: TimeInterval(1))
let cloudItem2 = CloudMetadataItem.stub(fileName: "file2", lastModificationDate: TimeInterval(2))
let cloudItem3 = CloudMetadataItem.stub(fileName: "file3", lastModificationDate: TimeInterval(3))
let localItems = LocalContents()
let cloudItems = CloudContents([cloudItem1, cloudItem2, cloudItem3])
outgoingEvents = syncStateManager.resolveEvent(.didFinishGatheringCloudContents(cloudItems))
XCTAssertEqual(outgoingEvents.count, 0)
outgoingEvents = syncStateManager.resolveEvent(.didFinishGatheringLocalContents(localItems))
outgoingEvents.forEach { event in
switch event {
case .createLocalItem(let item):
XCTAssertTrue(cloudItems.containsByName(item))
default:
XCTFail()
}
}
}
func testDidFinishGatheringWhenTreCloudIsEmpty() {
let localItem1 = LocalMetadataItem.stub(fileName: "file1", lastModificationDate: TimeInterval(1))
let localItem2 = LocalMetadataItem.stub(fileName: "file2", lastModificationDate: TimeInterval(2))
let localItem3 = LocalMetadataItem.stub(fileName: "file3", lastModificationDate: TimeInterval(3))
let localItems = [localItem1, localItem2, localItem3]
outgoingEvents = syncStateManager.resolveEvent(.didFinishGatheringCloudContents([]))
outgoingEvents = syncStateManager.resolveEvent(.didFinishGatheringLocalContents(localItems))
XCTAssertEqual(outgoingEvents.count, 3)
outgoingEvents.forEach { event in
switch event {
case .createCloudItem(let item):
XCTAssertTrue(localItems.containsByName(item))
default:
XCTFail()
}
}
}
func testDidFinishGatheringWhenLocalIsEmpty() {
let cloudItem1 = CloudMetadataItem.stub(fileName: "file1", lastModificationDate: TimeInterval(1))
let cloudItem2 = CloudMetadataItem.stub(fileName: "file2", lastModificationDate: TimeInterval(2))
let cloudItem3 = CloudMetadataItem.stub(fileName: "file3", lastModificationDate: TimeInterval(3))
let localItems = LocalContents()
let cloudItems = CloudContents([cloudItem1, cloudItem2, cloudItem3])
outgoingEvents = syncStateManager.resolveEvent(.didFinishGatheringCloudContents(cloudItems))
XCTAssertEqual(outgoingEvents.count, 0)
outgoingEvents = syncStateManager.resolveEvent(.didFinishGatheringLocalContents(localItems))
XCTAssertEqual(outgoingEvents.count, 3)
outgoingEvents.forEach { event in
switch event {
case .createLocalItem(let item):
XCTAssertTrue(cloudItems.containsByName(item))
default:
XCTFail()
}
}
}
func testDidFinishGatheringWhenLocalAndCloudAreNotEmptyAndAllFilesEqual() {
let localItem1 = LocalMetadataItem.stub(fileName: "file1", lastModificationDate: TimeInterval(1))
let localItem2 = LocalMetadataItem.stub(fileName: "file2", lastModificationDate: TimeInterval(2))
let localItem3 = LocalMetadataItem.stub(fileName: "file3", lastModificationDate: TimeInterval(3))
let cloudItem1 = CloudMetadataItem.stub(fileName: "file1", lastModificationDate: TimeInterval(1))
let cloudItem2 = CloudMetadataItem.stub(fileName: "file2", lastModificationDate: TimeInterval(2))
let cloudItem3 = CloudMetadataItem.stub(fileName: "file3", lastModificationDate: TimeInterval(3))
let localItems = LocalContents([localItem1, localItem2, localItem3])
let cloudItems = CloudContents([cloudItem1, cloudItem2, cloudItem3])
outgoingEvents = syncStateManager.resolveEvent(.didFinishGatheringLocalContents(localItems))
XCTAssertEqual(outgoingEvents.count, 0)
outgoingEvents = syncStateManager.resolveEvent(.didFinishGatheringCloudContents(cloudItems))
XCTAssertEqual(outgoingEvents.count, 0)
}
func testDidFinishGatheringWhenLocalAndCloudAreNotEmptyAndSomeLocalItemsAreNewer() {
let localItem1 = LocalMetadataItem.stub(fileName: "file1", lastModificationDate: TimeInterval(1))
let localItem2 = LocalMetadataItem.stub(fileName: "file2", lastModificationDate: TimeInterval(3))
let localItem3 = LocalMetadataItem.stub(fileName: "file3", lastModificationDate: TimeInterval(4))
let cloudItem1 = CloudMetadataItem.stub(fileName: "file1", lastModificationDate: TimeInterval(1))
let cloudItem2 = CloudMetadataItem.stub(fileName: "file2", lastModificationDate: TimeInterval(2))
let cloudItem3 = CloudMetadataItem.stub(fileName: "file3", lastModificationDate: TimeInterval(3))
let localItems = LocalContents([localItem1, localItem2, localItem3])
let cloudItems = CloudContents([cloudItem1, cloudItem2, cloudItem3])
outgoingEvents = syncStateManager.resolveEvent(.didFinishGatheringLocalContents(localItems))
XCTAssertEqual(outgoingEvents.count, 0)
outgoingEvents = syncStateManager.resolveEvent(.didFinishGatheringCloudContents(cloudItems))
XCTAssertEqual(outgoingEvents.count, 2)
outgoingEvents.forEach { event in
switch event {
case .updateCloudItem(let item):
XCTAssertTrue([localItem2, localItem3].containsByName(item))
default:
XCTFail()
}
}
}
func testDidFinishGatheringWhenLocalAndCloudAreNotEmptyAndSomeCloudItemsAreNewer() {
let localItem1 = LocalMetadataItem.stub(fileName: "file1", lastModificationDate: TimeInterval(1))
let localItem2 = LocalMetadataItem.stub(fileName: "file2", lastModificationDate: TimeInterval(2))
let localItem3 = LocalMetadataItem.stub(fileName: "file3", lastModificationDate: TimeInterval(3))
let cloudItem1 = CloudMetadataItem.stub(fileName: "file1", lastModificationDate: TimeInterval(4))
let cloudItem2 = CloudMetadataItem.stub(fileName: "file2", lastModificationDate: TimeInterval(2))
let cloudItem3 = CloudMetadataItem.stub(fileName: "file3", lastModificationDate: TimeInterval(7))
let localItems = LocalContents([localItem1, localItem2, localItem3])
let cloudItems = CloudContents([cloudItem1, cloudItem2, cloudItem3])
outgoingEvents = syncStateManager.resolveEvent(.didFinishGatheringLocalContents(localItems))
XCTAssertEqual(outgoingEvents.count, 0)
outgoingEvents = syncStateManager.resolveEvent(.didFinishGatheringCloudContents(cloudItems))
XCTAssertEqual(outgoingEvents.count, 2)
outgoingEvents.forEach { event in
switch event {
case .updateLocalItem(let item):
XCTAssertTrue([cloudItem1, cloudItem3].containsByName(item))
default:
XCTFail()
}
}
}
func testDidFinishGatheringWhenCloudFileNewerThanLocal() {
let localItem = LocalMetadataItem.stub(fileName: "file3", lastModificationDate: TimeInterval(3))
let cloudItem = CloudMetadataItem.stub(fileName: "file3", lastModificationDate: TimeInterval(8))
let localItems = LocalContents([localItem])
let cloudItems = CloudContents([cloudItem])
outgoingEvents = syncStateManager.resolveEvent(.didFinishGatheringLocalContents(localItems))
XCTAssertEqual(outgoingEvents.count, 0)
outgoingEvents = syncStateManager.resolveEvent(.didFinishGatheringCloudContents(cloudItems))
XCTAssertEqual(outgoingEvents.count, 1)
outgoingEvents.forEach { event in
switch event {
case .updateLocalItem(let item):
XCTAssertEqual(item, cloudItem)
default:
XCTFail()
}
}
}
func testDidFinishGatheringWhenCloudHaveSameFileBothInTrashedAndNotAndTrashedBotLocalIsNewer() {
let localItem3 = LocalMetadataItem.stub(fileName: "file3", lastModificationDate: TimeInterval(9))
let cloudItem3 = CloudMetadataItem.stub(fileName: "file3", lastModificationDate: TimeInterval(2))
let cloudItem3Trashed = CloudMetadataItem.stub(fileName: "file3", lastModificationDate: TimeInterval(6))
let localItems = LocalContents([localItem3])
let cloudItems = CloudContents([cloudItem3, cloudItem3Trashed])
outgoingEvents = syncStateManager.resolveEvent(.didFinishGatheringLocalContents(localItems))
XCTAssertEqual(outgoingEvents.count, 0)
outgoingEvents = syncStateManager.resolveEvent(.didFinishGatheringCloudContents(cloudItems))
XCTAssertEqual(outgoingEvents.count, 1)
outgoingEvents.forEach { event in
switch event {
case .updateCloudItem(let item):
XCTAssertEqual(item, localItem3)
default:
XCTFail()
}
}
}
// MARK: - Test didUpdateLocalContents
func testDidUpdateLocalContentsWhenContentWasNotChanged() {
let localItem1 = LocalMetadataItem.stub(fileName: "file1", lastModificationDate: TimeInterval(1))
let localItem2 = LocalMetadataItem.stub(fileName: "file2", lastModificationDate: TimeInterval(2))
let localItem3 = LocalMetadataItem.stub(fileName: "file3", lastModificationDate: TimeInterval(3))
let localItems = LocalContents([localItem1, localItem2, localItem3])
let cloudItems = CloudContents([])
outgoingEvents = syncStateManager.resolveEvent(.didFinishGatheringLocalContents(localItems))
outgoingEvents = syncStateManager.resolveEvent(.didFinishGatheringCloudContents(cloudItems))
XCTAssertEqual(outgoingEvents.count, 3)
let newLocalItems = LocalContents([localItem1, localItem2, localItem3])
let update = LocalContentsUpdate(added: [], updated: [], removed: [])
outgoingEvents = syncStateManager.resolveEvent(.didUpdateLocalContents(contents: newLocalItems, update: update))
XCTAssertEqual(outgoingEvents.count, 0)
}
func testDidUpdateLocalContentsWhenNewLocalItemWasAdded() {
let localItem1 = LocalMetadataItem.stub(fileName: "file1", lastModificationDate: TimeInterval(1))
let localItem2 = LocalMetadataItem.stub(fileName: "file2", lastModificationDate: TimeInterval(2))
let localItem3 = LocalMetadataItem.stub(fileName: "file3", lastModificationDate: TimeInterval(3))
let cloudItem1 = CloudMetadataItem.stub(fileName: "file1", lastModificationDate: TimeInterval(1))
let cloudItem2 = CloudMetadataItem.stub(fileName: "file2", lastModificationDate: TimeInterval(2))
let cloudItem3 = CloudMetadataItem.stub(fileName: "file3", lastModificationDate: TimeInterval(3))
let localItems = LocalContents([localItem1, localItem2, localItem3])
let cloudItems = CloudContents([cloudItem1, cloudItem2, cloudItem3])
outgoingEvents = syncStateManager.resolveEvent(.didFinishGatheringLocalContents(localItems))
outgoingEvents = syncStateManager.resolveEvent(.didFinishGatheringCloudContents(cloudItems))
XCTAssertEqual(outgoingEvents.count, 0)
let localItem4 = LocalMetadataItem.stub(fileName: "file4", lastModificationDate: TimeInterval(4))
let newLocalItems = LocalContents([localItem1, localItem2, localItem3])
let update = LocalContentsUpdate(added: [localItem4], updated: [], removed: [])
outgoingEvents = syncStateManager.resolveEvent(.didUpdateLocalContents(contents: newLocalItems, update: update))
XCTAssertEqual(outgoingEvents.count, 1)
outgoingEvents.forEach { event in
switch event {
case .createCloudItem(let item):
XCTAssertEqual(item, localItem4)
default:
XCTFail()
}
}
}
func testDidUpdateLocalContentsWhenLocalItemWasUpdated() {
let localItem1 = LocalMetadataItem.stub(fileName: "file1", lastModificationDate: TimeInterval(1))
let localItem2 = LocalMetadataItem.stub(fileName: "file2", lastModificationDate: TimeInterval(2))
let localItem3 = LocalMetadataItem.stub(fileName: "file3", lastModificationDate: TimeInterval(3))
let cloudItem1 = CloudMetadataItem.stub(fileName: "file1", lastModificationDate: TimeInterval(1))
let cloudItem2 = CloudMetadataItem.stub(fileName: "file2", lastModificationDate: TimeInterval(2))
let cloudItem3 = CloudMetadataItem.stub(fileName: "file3", lastModificationDate: TimeInterval(3))
let localItems = LocalContents([localItem1, localItem2, localItem3])
let cloudItems = CloudContents([cloudItem1, cloudItem2, cloudItem3])
outgoingEvents = syncStateManager.resolveEvent(.didFinishGatheringLocalContents(localItems))
outgoingEvents = syncStateManager.resolveEvent(.didFinishGatheringCloudContents(cloudItems))
XCTAssertEqual(outgoingEvents.count, 0)
let localItem2Updated = LocalMetadataItem.stub(fileName: "file2", lastModificationDate: TimeInterval(3))
let localItem3Updated = LocalMetadataItem.stub(fileName: "file3", lastModificationDate: TimeInterval(4))
let newLocalItems = LocalContents([localItem1, localItem2Updated, localItem3Updated])
let update = LocalContentsUpdate(added: [], updated: [localItem2Updated, localItem3Updated], removed: [])
outgoingEvents = syncStateManager.resolveEvent(.didUpdateLocalContents(contents: newLocalItems, update: update))
XCTAssertEqual(outgoingEvents.count, 2)
outgoingEvents.forEach { event in
switch event {
case .updateCloudItem(let item):
XCTAssertTrue([localItem2Updated, localItem3Updated].containsByName(item))
default:
XCTFail()
}
}
}
func testDidUpdateLocalContentsWhenLocalItemWasRemoved() {
let localItem1 = LocalMetadataItem.stub(fileName: "file1", lastModificationDate: TimeInterval(1))
let localItem2 = LocalMetadataItem.stub(fileName: "file2", lastModificationDate: TimeInterval(2))
let localItem3 = LocalMetadataItem.stub(fileName: "file3", lastModificationDate: TimeInterval(3))
let cloudItem1 = CloudMetadataItem.stub(fileName: "file1", lastModificationDate: TimeInterval(1))
let cloudItem2 = CloudMetadataItem.stub(fileName: "file2", lastModificationDate: TimeInterval(2))
let cloudItem3 = CloudMetadataItem.stub(fileName: "file3", lastModificationDate: TimeInterval(3))
let localItems = LocalContents([localItem1, localItem2, localItem3])
let cloudItems = CloudContents([cloudItem1, cloudItem2, cloudItem3])
outgoingEvents = syncStateManager.resolveEvent(.didFinishGatheringLocalContents(localItems))
outgoingEvents = syncStateManager.resolveEvent(.didFinishGatheringCloudContents(cloudItems))
XCTAssertEqual(outgoingEvents.count, 0)
let newLocalItems = LocalContents([localItem1, localItem2])
let update = LocalContentsUpdate(added: [], updated: [], removed: [localItem3])
outgoingEvents = syncStateManager.resolveEvent(.didUpdateLocalContents(contents: newLocalItems, update: update))
XCTAssertEqual(outgoingEvents.count, 1)
outgoingEvents.forEach { event in
switch event {
case .removeCloudItem(let item):
XCTAssertEqual(item, cloudItem3)
default:
XCTFail()
}
}
}
// TODO: Test didUpdateCloudContents
func testDidUpdateCloudContentsWhenContentWasNotChanged() {
let localItem1 = LocalMetadataItem.stub(fileName: "file1", lastModificationDate: TimeInterval(1))
let localItem2 = LocalMetadataItem.stub(fileName: "file2", lastModificationDate: TimeInterval(2))
let localItem3 = LocalMetadataItem.stub(fileName: "file3", lastModificationDate: TimeInterval(3))
let cloudItem1 = CloudMetadataItem.stub(fileName: "file1", lastModificationDate: TimeInterval(1))
let cloudItem2 = CloudMetadataItem.stub(fileName: "file2", lastModificationDate: TimeInterval(2))
let cloudItem3 = CloudMetadataItem.stub(fileName: "file3", lastModificationDate: TimeInterval(3))
let localItems = LocalContents([localItem1, localItem2, localItem3])
let cloudItems = CloudContents([cloudItem1, cloudItem2, cloudItem3])
outgoingEvents = syncStateManager.resolveEvent(.didFinishGatheringLocalContents(localItems))
outgoingEvents = syncStateManager.resolveEvent(.didFinishGatheringCloudContents(cloudItems))
XCTAssertEqual(outgoingEvents.count, 0)
let newCloudItems = CloudContents([cloudItem1, cloudItem2, cloudItem3])
let update = CloudContentsUpdate(added: [], updated: [], removed: [])
outgoingEvents = syncStateManager.resolveEvent(.didUpdateCloudContents(contents: newCloudItems, update: update))
XCTAssertEqual(outgoingEvents.count, 0)
}
func testDidUpdateCloudContentsWhenContentItemWasAdded() {
let localItem1 = LocalMetadataItem.stub(fileName: "file1", lastModificationDate: TimeInterval(1))
let localItem2 = LocalMetadataItem.stub(fileName: "file2", lastModificationDate: TimeInterval(2))
let localItem3 = LocalMetadataItem.stub(fileName: "file3", lastModificationDate: TimeInterval(3))
let cloudItem1 = CloudMetadataItem.stub(fileName: "file1", lastModificationDate: TimeInterval(1))
let cloudItem2 = CloudMetadataItem.stub(fileName: "file2", lastModificationDate: TimeInterval(2))
let cloudItem3 = CloudMetadataItem.stub(fileName: "file3", lastModificationDate: TimeInterval(3))
let localItems = LocalContents([localItem1, localItem2, localItem3])
var cloudItems = CloudContents([cloudItem1, cloudItem2, cloudItem3])
outgoingEvents = syncStateManager.resolveEvent(.didFinishGatheringLocalContents(localItems))
outgoingEvents = syncStateManager.resolveEvent(.didFinishGatheringCloudContents(cloudItems))
XCTAssertEqual(outgoingEvents.count, 0)
var cloudItem4 = CloudMetadataItem.stub(fileName: "file4", lastModificationDate: TimeInterval(3), isDownloaded: false, percentDownloaded: 0.0)
cloudItems.append(cloudItem4)
var update = CloudContentsUpdate(added: [cloudItem4], updated: [], removed: [])
outgoingEvents = syncStateManager.resolveEvent(.didUpdateCloudContents(contents: cloudItems, update: update))
XCTAssertEqual(outgoingEvents.count, 1)
outgoingEvents.forEach { event in
switch event {
case .startDownloading(let cloudMetadataItem):
XCTAssertEqual(cloudMetadataItem, cloudItem4)
default:
XCTFail()
}
}
cloudItem4.percentDownloaded = 50.0
update = CloudContentsUpdate(added: [], updated: [cloudItem4], removed: [])
outgoingEvents = syncStateManager.resolveEvent(.didUpdateCloudContents(contents: cloudItems, update: update))
XCTAssertEqual(outgoingEvents.count, 0)
cloudItem4.percentDownloaded = 100.0
update = CloudContentsUpdate(added: [], updated: [cloudItem4], removed: [])
outgoingEvents = syncStateManager.resolveEvent(.didUpdateCloudContents(contents: cloudItems, update: update))
XCTAssertEqual(outgoingEvents.count, 0)
cloudItem4.isDownloaded = true
// recreate collection
cloudItems = [cloudItem1, cloudItem2, cloudItem3, cloudItem4]
update = CloudContentsUpdate(added: [], updated: [cloudItem4], removed: [])
outgoingEvents = syncStateManager.resolveEvent(.didUpdateCloudContents(contents: cloudItems, update: update))
XCTAssertEqual(outgoingEvents.count, 1)
outgoingEvents.forEach { event in
switch event {
case .createLocalItem(let cloudMetadataItem):
XCTAssertEqual(cloudMetadataItem, cloudItem4)
default:
XCTFail()
}
}
}
}

View file

@ -0,0 +1,13 @@
class FileManagerMock: FileManager {
var stubUbiquityIdentityToken: UbiquityIdentityToken?
var shouldReturnContainerURL: Bool = true
var stubCloudDirectory: URL?
override var ubiquityIdentityToken: (any UbiquityIdentityToken)? {
return stubUbiquityIdentityToken
}
override func url(forUbiquityContainerIdentifier identifier: String?) -> URL? {
return shouldReturnContainerURL ? stubCloudDirectory ?? URL(fileURLWithPath: NSTemporaryDirectory()) : nil
}
}

View file

@ -0,0 +1,31 @@
import XCTest
@testable import CoMaps__Debug_
class UbiquitousDirectoryMonitorDelegateMock: CloudDirectoryMonitorDelegate {
var didFinishGatheringCalled = false
var didUpdateCalled = false
var didReceiveErrorCalled = false
var didFinishGatheringExpectation: XCTestExpectation?
var didUpdateExpectation: XCTestExpectation?
var didReceiveErrorExpectation: XCTestExpectation?
var contents = CloudContents()
func didFinishGathering(_ contents: CloudContents) {
didFinishGatheringCalled = true
didFinishGatheringExpectation?.fulfill()
self.contents = contents
}
func didUpdate(_ contents: CloudContents, _ update: CloudContentsUpdate) {
didUpdateCalled = true
didUpdateExpectation?.fulfill()
self.contents = contents
}
func didReceiveCloudMonitorError(_ error: Error) {
didReceiveErrorCalled = true
didReceiveErrorExpectation?.fulfill()
}
}

View file

@ -0,0 +1,40 @@
import XCTest
@testable import CoMaps__Debug_
typealias UbiquityIdentityToken = NSCoding & NSCopying & NSObjectProtocol
class iCloudDirectoryMonitorTests: XCTestCase {
var cloudMonitor: iCloudDocumentsMonitor!
var mockFileManager: FileManagerMock!
var mockDelegate: UbiquitousDirectoryMonitorDelegateMock!
var cloudContainerIdentifier: String = "iCloud.app.comaps.debug"
override func setUp() {
super.setUp()
mockFileManager = FileManagerMock()
mockDelegate = UbiquitousDirectoryMonitorDelegateMock()
cloudMonitor = iCloudDocumentsMonitor(fileManager: mockFileManager, cloudContainerIdentifier: cloudContainerIdentifier, fileType: .kml)
cloudMonitor.delegate = mockDelegate
}
override func tearDown() {
cloudMonitor = nil
mockFileManager = nil
mockDelegate = nil
super.tearDown()
}
func testInitialization() {
XCTAssertNotNil(cloudMonitor)
XCTAssertEqual(cloudMonitor.containerIdentifier, cloudContainerIdentifier)
}
func testCloudAvailability() {
mockFileManager.stubUbiquityIdentityToken = NSString(string: "mockToken")
XCTAssertTrue(cloudMonitor.isCloudAvailable())
mockFileManager.stubUbiquityIdentityToken = nil
XCTAssertFalse(cloudMonitor.isCloudAvailable())
}
}

View file

@ -0,0 +1,311 @@
import XCTest
@testable import CoMaps__Debug_
final class SearchOnMapTests: XCTestCase {
private var presenter: SearchOnMapPresenter!
private var interactor: SearchOnMapInteractor!
private var view: SearchOnMapViewMock!
private var searchManager: SearchManagerMock.Type!
private var currentState: SearchOnMapState = .searching
override func setUp() {
super.setUp()
searchManager = SearchManagerMock.self
presenter = SearchOnMapPresenter(isRouting: false,
didChangeState: { [weak self] in self?.currentState = $0 })
interactor = SearchOnMapInteractor(presenter: presenter, searchManager: searchManager)
view = SearchOnMapViewMock()
presenter.view = view
}
override func tearDown() {
presenter = nil
interactor = nil
view = nil
searchManager.results = .empty
searchManager = nil
super.tearDown()
}
func test_GivenViewIsLoading_WhenViewLoads_ThenShowsHistoryAndCategory() {
interactor.handle(.openSearch)
XCTAssertEqual(currentState, .searching)
XCTAssertEqual(view.viewModel.presentationStep, .fullScreen)
XCTAssertEqual(view.viewModel.contentState, .historyAndCategory)
XCTAssertEqual(view.viewModel.searchingText, nil)
XCTAssertEqual(view.viewModel.isTyping, true)
}
func test_GivenInitialState_WhenSelectCategory_ThenUpdateSearchResultsAndShowMap() {
interactor.handle(.openSearch)
let query = SearchQuery("category", source: .category)
interactor.handle(.didSelect(query))
XCTAssertEqual(view.viewModel.presentationStep, .halfScreen)
XCTAssertEqual(view.viewModel.contentState, .searching)
XCTAssertEqual(view.viewModel.searchingText, query.text)
XCTAssertEqual(view.viewModel.isTyping, false)
let results = SearchResult.stubResults()
searchManager.results = results
XCTAssertEqual(currentState, .searching)
XCTAssertEqual(view.viewModel.presentationStep, .halfScreen)
XCTAssertEqual(view.viewModel.contentState, .results(results))
XCTAssertEqual(view.viewModel.searchingText, nil)
XCTAssertEqual(view.viewModel.isTyping, false)
}
func test_GivenInitialState_WhenTypeText_ThenUpdateSearchResults() {
interactor.handle(.openSearch)
let query = SearchQuery("text", source: .typedText)
interactor.handle(.didType(query))
XCTAssertEqual(view.viewModel.presentationStep, .fullScreen)
XCTAssertEqual(view.viewModel.contentState, .searching)
XCTAssertEqual(view.viewModel.searchingText, nil)
XCTAssertEqual(view.viewModel.isTyping, true)
let results = SearchResult.stubResults()
searchManager.results = results
XCTAssertEqual(currentState, .searching)
XCTAssertEqual(view.viewModel.presentationStep, .fullScreen)
XCTAssertEqual(view.viewModel.contentState, .results(results))
XCTAssertEqual(view.viewModel.searchingText, nil)
XCTAssertEqual(view.viewModel.isTyping, true)
}
func test_GivenInitialState_WhenTapSearch_ThenUpdateSearchResultsAndShowMap() {
interactor.handle(.openSearch)
let query = SearchQuery("text", source: .typedText)
interactor.handle(.didType(query))
let results = SearchResult.stubResults()
searchManager.results = results
XCTAssertEqual(view.viewModel.presentationStep, .fullScreen)
XCTAssertEqual(view.viewModel.contentState, .results(results))
XCTAssertEqual(view.viewModel.searchingText, nil)
XCTAssertEqual(view.viewModel.isTyping, true)
interactor.handle(.searchButtonDidTap(query))
XCTAssertEqual(currentState, .searching)
XCTAssertEqual(view.viewModel.presentationStep, .halfScreen)
XCTAssertEqual(view.viewModel.contentState, .results(results))
XCTAssertEqual(view.viewModel.searchingText, nil)
XCTAssertEqual(view.viewModel.isTyping, false)
}
func test_GivenSearchIsOpened_WhenMapIsDragged_ThenCollapseSearchScreen() {
interactor.handle(.openSearch)
XCTAssertEqual(view.viewModel.presentationStep, .fullScreen)
interactor.handle(.didStartDraggingMap)
XCTAssertEqual(view.viewModel.presentationStep, .compact)
}
func test_GivenSearchIsOpened_WhenModalPresentationScreenIsDragged_ThenDisableTyping() {
interactor.handle(.openSearch)
XCTAssertEqual(view.viewModel.isTyping, true)
interactor.handle(.didStartDraggingSearch)
XCTAssertEqual(view.viewModel.isTyping, false)
}
func test_GivenResultsOnScreen_WhenSelectResult_ThenHideSearch() {
interactor.handle(.openSearch)
XCTAssertEqual(view.viewModel.isTyping, true)
let query = SearchQuery("text", source: .typedText)
interactor.handle(.didSelect(query))
let results = SearchResult.stubResults()
searchManager.results = results
interactor.handle(.didSelectResult(results[0], withQuery: query))
if isiPad {
XCTAssertEqual(currentState, .searching)
XCTAssertEqual(view.viewModel.presentationStep, .fullScreen)
} else {
XCTAssertEqual(currentState, .hidden)
XCTAssertEqual(view.viewModel.presentationStep, .hidden)
}
}
func test_GivenSearchIsActive_WhenSelectPlaceOnMap_ThenHideSearch() {
interactor.handle(.openSearch)
XCTAssertEqual(view.viewModel.presentationStep, .fullScreen)
interactor.handle(.didSelectPlaceOnMap)
if isiPad {
XCTAssertNotEqual(view.viewModel.presentationStep, .hidden)
} else {
XCTAssertEqual(view.viewModel.presentationStep, .hidden)
}
}
func test_GivenSearchIsHidden_WhenPPDeselected_ThenShowSearch() {
interactor.handle(.openSearch)
XCTAssertEqual(view.viewModel.isTyping, true)
let query = SearchQuery("text", source: .typedText)
interactor.handle(.didSelect(query))
let results = SearchResult.stubResults()
searchManager.results = results
interactor.handle(.didSelectResult(results[0], withQuery: query))
if isiPad {
XCTAssertEqual(currentState, .searching)
XCTAssertEqual(view.viewModel.presentationStep, .fullScreen)
} else {
XCTAssertEqual(currentState, .hidden)
XCTAssertEqual(view.viewModel.presentationStep, .hidden)
}
interactor.handle(.didDeselectPlaceOnMap)
XCTAssertEqual(currentState, .searching)
XCTAssertEqual(view.viewModel.presentationStep, .halfScreen)
}
func test_GivenSearchIsOpen_WhenCloseSearch_ThenHideSearch() {
interactor.handle(.openSearch)
XCTAssertEqual(view.viewModel.presentationStep, .fullScreen)
interactor.handle(.closeSearch)
XCTAssertEqual(currentState, .closed)
}
func test_GivenSearchHasText_WhenClearSearch_ThenShowHistoryAndCategory() {
interactor.handle(.openSearch)
let query = SearchQuery("text", source: .typedText)
interactor.handle(.didSelect(query))
interactor.handle(.clearButtonDidTap)
XCTAssertEqual(view.viewModel.presentationStep, .fullScreen)
XCTAssertEqual(view.viewModel.contentState, .historyAndCategory)
XCTAssertEqual(view.viewModel.searchingText, "")
XCTAssertEqual(view.viewModel.isTyping, true)
}
func test_GivenSearchExecuted_WhenNoResults_ThenShowNoResults() {
interactor.handle(.openSearch)
let query = SearchQuery("text", source: .typedText)
interactor.handle(.didSelect(query))
searchManager.results = SearchOnMap.SearchResults([])
interactor.onSearchCompleted()
XCTAssertEqual(view.viewModel.contentState, .noResults)
}
func test_GivenSearchIsActive_WhenSelectSuggestion_ThenReplaceWithSuggestion() {
interactor.handle(.openSearch)
let query = SearchQuery("ca", source: .typedText)
interactor.handle(.didType(query))
let result = SearchResult(titleText: "", type: .suggestion, suggestion: "cafe")
interactor.handle(.didSelectResult(result, withQuery: query))
XCTAssertEqual(view.viewModel.searchingText, "cafe")
XCTAssertEqual(view.viewModel.presentationStep, .fullScreen)
XCTAssertEqual(view.viewModel.contentState, .searching)
XCTAssertEqual(view.viewModel.isTyping, true)
}
func test_GivenSearchIsActive_WhenPasteDeeplink_ThenShowResult() {
interactor.handle(.openSearch)
let query = SearchQuery("om://search?cll=42.0,44.0&query=Toilet", source: .deeplink)
interactor.handle(.didSelect(query))
let result = SearchResult(titleText: "some result", type: .regular, suggestion: "")
let results = SearchOnMap.SearchResults([result])
searchManager.results = results
interactor.onSearchCompleted()
XCTAssertEqual(view.viewModel.contentState, .results(results))
XCTAssertEqual(view.viewModel.presentationStep, .halfScreen)
XCTAssertEqual(view.viewModel.isTyping, false) // No typing when deeplink is used
}
func test_GivenSearchIsActive_WhenPresentationStepUpdate_ThenUpdateSearchMode() {
interactor.handle(.openSearch)
XCTAssertEqual(searchManager.searchMode(), isiPad ? .everywhereAndViewport : .everywhere)
interactor.handle(.didUpdatePresentationStep(.halfScreen))
XCTAssertEqual(searchManager.searchMode(), .everywhereAndViewport)
interactor.handle(.didUpdatePresentationStep(.compact))
XCTAssertEqual(searchManager.searchMode(), .everywhereAndViewport)
interactor.handle(.didUpdatePresentationStep(.hidden))
XCTAssertEqual(searchManager.searchMode(), .viewport)
interactor.handle(.didUpdatePresentationStep(.fullScreen))
XCTAssertEqual(searchManager.searchMode(), isiPad ? .everywhereAndViewport : .everywhere)
}
}
// MARK: - Mocks
private class SearchOnMapViewMock: SearchOnMapView {
var viewModel: SearchOnMap.ViewModel = .initial
var scrollViewDelegate: (any SearchOnMapScrollViewDelegate)?
func render(_ viewModel: SearchOnMap.ViewModel) {
self.viewModel = viewModel
}
func close() {
}
func show() {
}
}
private class SearchManagerMock: SearchManager {
static var observers = ListenerContainer<MWMSearchObserver>()
static var results = SearchOnMap.SearchResults.empty {
didSet {
observers.forEach { observer in
observer.onSearchCompleted?()
}
}
}
private static var _searchMode: SearchMode = .everywhere
static func add(_ observer: any MWMSearchObserver) {
self.observers.addListener(observer)
}
static func remove(_ observer: any MWMSearchObserver) {
self.observers.removeListener(observer)
}
static func save(_ query: SearchQuery) {}
static func searchQuery(_ query: SearchQuery) {}
static func showResult(at index: UInt) {}
static func clear() {}
static func getResults() -> [SearchResult] { results.results }
static func searchMode() -> SearchMode { _searchMode }
static func setSearchMode(_ mode: SearchMode) { _searchMode = mode }
}
private extension SearchResult {
static func stubResults() -> SearchOnMap.SearchResults {
SearchOnMap.SearchResults([
SearchResult(),
SearchResult(),
SearchResult()
])
}
}