Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/main' into feature-search-keyboa…
Browse files Browse the repository at this point in the history
…rd-shortcuts
  • Loading branch information
jarrodmoldrich committed Jan 17, 2025
2 parents 63cc217 + 1a673e5 commit e069ec5
Show file tree
Hide file tree
Showing 11 changed files with 176 additions and 47 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ jobs:
- name: Run Unit Tests on macOS
if: matrix.platform == 'macOS'
run: xcodebuild test -scheme Kiwix -destination 'platform=macOS'
run: xcodebuild test -scheme Kiwix -destination 'platform=macOS' CODE_SIGNING_ALLOWED=NO

- name: Upload code coverage
uses: codecov/[email protected]
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ Libraries
# ignore xcode project files, they are now generated by XcodeGen
*.xcodeproj
Support/Kiwix.entitlements
Support/Kiwix-unitTest.entitlements

# ignore the lock file, it is not working the same way as lock files in other tools
Brewfile.lock.json
Expand Down
33 changes: 19 additions & 14 deletions Model/CategoriesToLanguage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,23 @@
// You should have received a copy of the GNU General Public License
// along with Kiwix; If not, see https://www.gnu.org/licenses/.

//
// CategoriesToLanguage.swift
// Kiwix
//

import Foundation
import Defaults

struct CategoriesToLanguages {
protocol CategoriesProtocol {
func has(category: Category, inLanguages langCodes: Set<String>) -> Bool
func save(_ dictionary: [Category: Set<String>])
func allCategories() -> [Category]
}

private let dictionary: [Category: Set<String>] = Defaults[.categoriesToLanguages]
struct CategoriesToLanguages: CategoriesProtocol {

private let defaults: Defaulting
private let dictionary: [Category: Set<String>]

init(withDefaults defaults: Defaulting = UDefaults()) {
self.defaults = defaults
self.dictionary = defaults[.categoriesToLanguages]
}

func has(category: Category, inLanguages langCodes: Set<String>) -> Bool {
guard !langCodes.isEmpty, !dictionary.isEmpty else {
Expand All @@ -35,15 +41,14 @@ struct CategoriesToLanguages {
return !languages.isDisjoint(with: langCodes)
}

static func save(_ dictionary: [Category: Set<String>]) {
Defaults[.categoriesToLanguages] = dictionary
func save(_ dictionary: [Category: Set<String>]) {
defaults[.categoriesToLanguages] = dictionary
}

static func allCategories() -> [Category] {
let categoriesToLanguages = CategoriesToLanguages()
let contentLanguages = Defaults[.libraryLanguageCodes]
func allCategories() -> [Category] {
let contentLanguages = defaults[.libraryLanguageCodes]
return Category.allCases.filter { (category: Category) in
categoriesToLanguages.has(category: category, inLanguages: contentLanguages)
has(category: category, inLanguages: contentLanguages)
}
}
}
32 changes: 32 additions & 0 deletions Model/Defaulting.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// This file is part of Kiwix for iOS & macOS.
//
// Kiwix is free software; you can redistribute it and/or modify it
// under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 3 of the License, or
// any later version.
//
// Kiwix is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Kiwix; If not, see https://www.gnu.org/licenses/.

import Foundation
import Defaults

public protocol Defaulting: NSObjectProtocol {
subscript<Value: Defaults.Serializable>(key: Defaults.Key<Value>) -> Value { get set }
}

final class UDefaults: NSObject, Defaulting {
subscript<Value>(key: Defaults.Key<Value>) -> Value where Value: DefaultsSerializable {
get {
Defaults[key]
}
set {
Defaults[key] = newValue
}
}
}
2 changes: 1 addition & 1 deletion SwiftUI/Model/Enum.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ enum ActiveSheet: Hashable, Identifiable {
case safari(url: URL)
}

enum Category: String, CaseIterable, Identifiable, LosslessStringConvertible {
enum Category: String, CaseIterable, Identifiable, LosslessStringConvertible, Hashable {
var description: String { rawValue }

var id: String { rawValue }
Expand Down
47 changes: 38 additions & 9 deletions Tests/LibraryRefreshViewModelTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ final class LibraryRefreshViewModelTest: XCTestCase {
}

private func makeOPDSData(zimFileID: UUID) -> String {
// swiftlint:disable line_length
"""
<feed xmlns="http://www.w3.org/2005/Atom"
xmlns:dc="http://purl.org/dc/terms/"
Expand Down Expand Up @@ -88,6 +89,7 @@ final class LibraryRefreshViewModelTest: XCTestCase {
</entry>
</feed>
"""
// swiftlint:enable line_length
}

/// Test time out fetching library data.
Expand All @@ -96,9 +98,12 @@ final class LibraryRefreshViewModelTest: XCTestCase {
HTTPTestingURLProtocol.handler = { urlProtocol in
urlProtocol.client?.urlProtocol(urlProtocol, didFailWithError: URLError(URLError.Code.timedOut))
}

let testDefaults = TestDefaults()
testDefaults.setup()
let viewModel = LibraryViewModel(urlSession: urlSession,
processFactory: { LibraryProcess() })
processFactory: { LibraryProcess(defaultState: .initial) },
defaults: testDefaults,
categories: CategoriesToLanguages(withDefaults: testDefaults))
await viewModel.start(isUserInitiated: true)
XCTAssert(viewModel.error is LibraryRefreshError)
XCTAssertEqual(
Expand All @@ -119,8 +124,12 @@ final class LibraryRefreshViewModelTest: XCTestCase {
urlProtocol.client?.urlProtocolDidFinishLoading(urlProtocol)
}

let testDefaults = TestDefaults()
testDefaults.setup()
let viewModel = LibraryViewModel(urlSession: urlSession,
processFactory: { LibraryProcess() })
processFactory: { LibraryProcess(defaultState: .initial) },
defaults: testDefaults,
categories: CategoriesToLanguages(withDefaults: testDefaults))
await viewModel.start(isUserInitiated: true)
XCTAssert(viewModel.error is LibraryRefreshError)
XCTAssertEqual(
Expand All @@ -137,13 +146,17 @@ final class LibraryRefreshViewModelTest: XCTestCase {
url: URL.mock(),
statusCode: 200, httpVersion: nil, headerFields: [:]
)!
urlProtocol.client?.urlProtocol(urlProtocol, didLoad: "Invalid OPDS Data".data(using: .utf8)!)
urlProtocol.client?.urlProtocol(urlProtocol, didLoad: Data("Invalid OPDS Data".utf8))
urlProtocol.client?.urlProtocol(urlProtocol, didReceive: response, cacheStoragePolicy: .notAllowed)
urlProtocol.client?.urlProtocolDidFinishLoading(urlProtocol)
}

let testDefaults = TestDefaults()
testDefaults.setup()
let viewModel = LibraryViewModel(urlSession: urlSession,
processFactory: { LibraryProcess() })
processFactory: { LibraryProcess(defaultState: .initial) },
defaults: testDefaults,
categories: CategoriesToLanguages(withDefaults: testDefaults))
await viewModel.start(isUserInitiated: true)
XCTExpectFailure("Requires work in dependency to resolve the issue.")
XCTAssertEqual(
Expand All @@ -154,6 +167,7 @@ final class LibraryRefreshViewModelTest: XCTestCase {

/// Test zim file entity is created, and metadata are saved when new zim file becomes available in online catalog.
@MainActor
// swiftlint:disable:next function_body_length
func testNewZimFileAndProperties() async throws {
let zimFileID = UUID()
HTTPTestingURLProtocol.handler = { urlProtocol in
Expand All @@ -166,9 +180,12 @@ final class LibraryRefreshViewModelTest: XCTestCase {
urlProtocol.client?.urlProtocol(urlProtocol, didReceive: response, cacheStoragePolicy: .notAllowed)
urlProtocol.client?.urlProtocolDidFinishLoading(urlProtocol)
}

let testDefaults = TestDefaults()
testDefaults.setup()
let viewModel = LibraryViewModel(urlSession: urlSession,
processFactory: { LibraryProcess() })
processFactory: { LibraryProcess(defaultState: .initial) },
defaults: testDefaults,
categories: CategoriesToLanguages(withDefaults: testDefaults))
await viewModel.start(isUserInitiated: true)

// check no error has happened
Expand All @@ -185,6 +202,7 @@ final class LibraryRefreshViewModelTest: XCTestCase {
XCTAssertEqual(zimFile.id, zimFileID)
XCTAssertEqual(zimFile.articleCount, 50001)
XCTAssertEqual(zimFile.category, Category.wikipedia.rawValue)
// swiftlint:disable:next force_try
XCTAssertEqual(zimFile.created, try! Date("2023-01-07T00:00:00Z", strategy: .iso8601))
XCTAssertEqual(
zimFile.downloadURL,
Expand All @@ -211,14 +229,21 @@ final class LibraryRefreshViewModelTest: XCTestCase {
XCTAssertEqual(zimFile.persistentID, "wikipedia_en_top")
XCTAssertEqual(zimFile.requiresServiceWorkers, false)
XCTAssertEqual(zimFile.size, 6515656704)

// clean up
context.delete(zimFile)
}

/// Test zim file deprecation
@MainActor
func testZimFileDeprecation() async throws {
let testDefaults = TestDefaults()
testDefaults.setup()
// refresh library for the first time, which should create one zim file
let viewModel = LibraryViewModel(urlSession: urlSession,
processFactory: { LibraryProcess() })
processFactory: { LibraryProcess(defaultState: .initial) },
defaults: testDefaults,
categories: CategoriesToLanguages(withDefaults: testDefaults))
await viewModel.start(isUserInitiated: true)
let context = Database.shared.viewContext
let zimFile1 = try XCTUnwrap(try context.fetch(ZimFile.fetchRequest()).first)
Expand All @@ -231,7 +256,7 @@ final class LibraryRefreshViewModelTest: XCTestCase {
XCTAssertNotEqual(zimFile1.fileID, zimFile2.fileID)

// set fileURLBookmark of zim file 2
zimFile2.fileURLBookmark = "/Users/tester/Downloads/file_url.zim".data(using: .utf8)
zimFile2.fileURLBookmark = Data("/Users/tester/Downloads/file_url.zim".utf8)
try context.save()

// refresh library for the third time
Expand All @@ -241,6 +266,10 @@ final class LibraryRefreshViewModelTest: XCTestCase {
// check there are two zim files in the database, and zim file 2 is not deprecated
XCTAssertEqual(zimFiles.count, 2)
XCTAssertEqual(zimFiles.filter({ $0.fileID == zimFile2.fileID }).count, 1)

// clean up
context.delete(zimFile1)
context.delete(zimFile2)
}
}

Expand Down
41 changes: 41 additions & 0 deletions Tests/TestDefaults.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// This file is part of Kiwix for iOS & macOS.
//
// Kiwix is free software; you can redistribute it and/or modify it
// under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 3 of the License, or
// any later version.
//
// Kiwix is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Kiwix; If not, see https://www.gnu.org/licenses/.

import Foundation
import Defaults
@testable import Kiwix

final class TestDefaults: NSObject, Defaulting {

var dict: [Defaults.AnyKey: any DefaultsSerializable] = [:]

func setup() {
self[.categoriesToLanguages] = [:]
self[.libraryAutoRefresh] = false
self[.libraryETag] = ""
self[.libraryUsingOldISOLangCodes] = false
self[.libraryLanguageCodes] = Set<String>()
}

subscript<Value>(key: Defaults.Key<Value>) -> Value where Value: DefaultsSerializable {
get {
// swiftlint:disable:next force_cast
dict[key] as! Value
}
set {
dict[key] = newValue
}
}
}
Loading

0 comments on commit e069ec5

Please sign in to comment.