mirror of
https://github.com/keymanapp/keyman.git
synced 2026-09-01 13:17:41 +00:00
feat(mac): add protocols, stubs and unit tests
Some checks are pending
Keyman Build Summary / Summarize build status checks (push) Waiting to run
Some checks are pending
Keyman Build Summary / Summarize build status checks (push) Waiting to run
added suite for Settings Container and stubs to test it without accessing disk or user defaults
This commit is contained in:
parent
af09f89bc9
commit
a7bcceff47
10 changed files with 353 additions and 104 deletions
|
|
@ -20,8 +20,8 @@ struct KeyboardListView: View {
|
|||
ForEach($keyboards) { $keyboard in
|
||||
HStack {
|
||||
Toggle("", isOn: Binding(
|
||||
get: { settings.isKeyboardEnabled(packageId: packageId, keyboardId: keyboard.keyboardId) },
|
||||
set: { newValue in settings.setKeyboardEnabled(packageId: packageId, keyboardId: keyboard.keyboardId, enabled: newValue)
|
||||
get: { settings.isKeyboardEnabled(packageId: packageId, keyboardKey: keyboard.keyboardKey) },
|
||||
set: { newValue in settings.setKeyboardEnabled(packageId: packageId, keyboardKey: keyboard.keyboardKey, enabled: newValue)
|
||||
settings.objectWillChange.send() }
|
||||
))
|
||||
Text(keyboard.keyboardId)
|
||||
|
|
|
|||
19
mac/KeymanSettings/Sources/KeymanSettings/DefaultsRepo.swift
Normal file
19
mac/KeymanSettings/Sources/KeymanSettings/DefaultsRepo.swift
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* Keyman is copyright (C) SIL Global. MIT License.
|
||||
*
|
||||
* Created by Shawn Schantz on 2026-04-28
|
||||
*
|
||||
* DefaultsRepo is a protocol that exposes the ability to read, write and remove
|
||||
* Keyman UserDefaults settings.
|
||||
*
|
||||
*/
|
||||
|
||||
import Foundation
|
||||
|
||||
public protocol DefaultsRepo {
|
||||
func readEnabledKeyboards() -> Set<String>
|
||||
func writeEnabledKeyboards(enabledKeyboardsArray: [String])
|
||||
func readSelectedKeyboard() -> String
|
||||
func logDefaults()
|
||||
func clearDefaults()
|
||||
}
|
||||
18
mac/KeymanSettings/Sources/KeymanSettings/PackageRepo.swift
Normal file
18
mac/KeymanSettings/Sources/KeymanSettings/PackageRepo.swift
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* Keyman is copyright (C) SIL Global. MIT License.
|
||||
*
|
||||
* Created by Shawn Schantz on 2025-12-10
|
||||
*
|
||||
* PackageRepo is a protocol that exposes the ability to read, write and remove
|
||||
* Keyman packages.
|
||||
*
|
||||
*/
|
||||
|
||||
import Foundation
|
||||
|
||||
public protocol PackageRepo {
|
||||
func keyman19SharedDataDirectoryExists() -> Bool
|
||||
func createKeyman19SharedDataDirectories()
|
||||
func loadPackages() -> [KeymanPackage]
|
||||
func deletePackage(package: KeymanPackage)
|
||||
}
|
||||
|
|
@ -7,6 +7,15 @@
|
|||
* Provides a place for the config app can bind directly to the settings
|
||||
* and update when changes are made
|
||||
*
|
||||
* The Settings consist of two types of data:
|
||||
* - Keyman packages that have been installed on disk in the Group Containers directory
|
||||
* - Some lightwieght settings that are stored in the macOS UserDefaults Database
|
||||
*
|
||||
* Both the packages and the defaults are in a shared location that can be accessed by
|
||||
* the Keyman config app and the Keyman input method.
|
||||
*
|
||||
* This class accesses the two types of data through PackageRepository and DefaultsRepository
|
||||
* and publishes the combined data to the UI through the `installedPackages` array.
|
||||
*/
|
||||
|
||||
import Foundation
|
||||
|
|
@ -20,11 +29,11 @@ public class SettingsContainer : ObservableObject {
|
|||
// packages are loaded from disk, each package may contain one or more keyboard
|
||||
@Published public var installedPackages: [KeymanPackage]
|
||||
|
||||
fileprivate let packageRepository: PackageRepository
|
||||
fileprivate let settingsRepository: SettingsRepository
|
||||
fileprivate let packageRepository: PackageRepo
|
||||
fileprivate let defaultsRepository: DefaultsRepo
|
||||
|
||||
// the selected keyboard is stored in the UserDefaults
|
||||
fileprivate var currentKeyboard: String
|
||||
fileprivate var selectedKeyboard: String
|
||||
#warning("does the config app need to be aware of this?")
|
||||
|
||||
public init() {
|
||||
|
|
@ -32,7 +41,7 @@ public class SettingsContainer : ObservableObject {
|
|||
|
||||
// create the settings repository, gaining access to the app group UserDefaults
|
||||
do {
|
||||
try self.settingsRepository = SettingsRepository(suiteName: KeymanPaths.groupId)
|
||||
try self.defaultsRepository = DefaultsRepository(suiteName: KeymanPaths.groupId)
|
||||
print("Found group container")
|
||||
} catch UserDefaultsError.unknownSuite {
|
||||
fatalError("Group container not found.")
|
||||
|
|
@ -40,19 +49,25 @@ public class SettingsContainer : ObservableObject {
|
|||
fatalError("Unable to access settings in group container.")
|
||||
}
|
||||
|
||||
self.currentKeyboard = self.settingsRepository.readSelectedKeyboard()
|
||||
self.selectedKeyboard = self.defaultsRepository.readSelectedKeyboard()
|
||||
|
||||
|
||||
// first load all the installed packages from disk
|
||||
self.installedPackages = []
|
||||
if let persistedPackages = self.loadPackages() {
|
||||
self.installedPackages = persistedPackages
|
||||
}
|
||||
self.loadPackages()
|
||||
|
||||
// next, apply the settings to the packages
|
||||
// this mainly consists of marking them as enabled or not
|
||||
self.applySettingsToInstalledPackages()
|
||||
}
|
||||
|
||||
public init(defaultsRepo: DefaultsRepo, packageRepo: PackageRepo) {
|
||||
self.defaultsRepository = defaultsRepo
|
||||
self.packageRepository = packageRepo
|
||||
self.selectedKeyboard = self.defaultsRepository.readSelectedKeyboard()
|
||||
|
||||
self.installedPackages = []
|
||||
}
|
||||
|
||||
#warning("delete test code")
|
||||
public func debug() {
|
||||
|
|
@ -67,17 +82,17 @@ public class SettingsContainer : ObservableObject {
|
|||
* for debugging: prints UserDefaults values
|
||||
*/
|
||||
public func logSettings() {
|
||||
self.settingsRepository.logSettings()
|
||||
self.defaultsRepository.logDefaults()
|
||||
}
|
||||
|
||||
public func clearSettings() {
|
||||
self.settingsRepository.clearSettings()
|
||||
self.defaultsRepository.clearDefaults()
|
||||
}
|
||||
|
||||
public func findPackage(packageId: UUID) throws -> KeymanPackage {
|
||||
public func findPackage(packageId: UUID) -> KeymanPackage? {
|
||||
guard let package = self.installedPackages.first(where: { $0.id == packageId }) else {
|
||||
print ("Error: could not find package with ID: \(packageId)")
|
||||
throw SettingsError.unknownPackage
|
||||
return nil
|
||||
}
|
||||
|
||||
return package
|
||||
|
|
@ -104,15 +119,14 @@ public class SettingsContainer : ObservableObject {
|
|||
* returns true if the keyboard is enabled
|
||||
* when enabled, the keyboard appears in the Keyman sub menu in the mac
|
||||
*/
|
||||
public func isKeyboardEnabled(packageId: UUID, keyboardId: String) -> Bool {
|
||||
var enabled = false;
|
||||
|
||||
if let package = try? self.findPackage(packageId: packageId) {
|
||||
enabled = package.isKeyboardEnabled(keyboardId: keyboardId)
|
||||
print ("isEnabled for \(keyboardId) returning with \(enabled)")
|
||||
} else {
|
||||
print ("Could not read keyboard state for package: \(packageId) and keyboard: \(keyboardId)")
|
||||
public func isKeyboardEnabled(packageId: UUID, keyboardKey: String) -> Bool {
|
||||
guard let package = self.findPackage(packageId: packageId) else {
|
||||
print ("Could not read keyboard state for package: \(packageId) and keyboard: \(keyboardKey)")
|
||||
return false
|
||||
}
|
||||
|
||||
let enabled = package.isKeyboardEnabled(keyboardKey: keyboardKey)
|
||||
print ("isEnabled for \(keyboardKey) returning with \(enabled)")
|
||||
|
||||
return enabled
|
||||
}
|
||||
|
|
@ -120,14 +134,14 @@ public class SettingsContainer : ObservableObject {
|
|||
/**
|
||||
* enable or disable the keyboard
|
||||
*/
|
||||
public func setKeyboardEnabled(packageId: UUID, keyboardId: String, enabled: Bool) {
|
||||
if let package = try? self.findPackage(packageId: packageId) {
|
||||
// update state of Keyboard
|
||||
print ("setKeyboardEnabled for \(keyboardId) setting to \(enabled)")
|
||||
package.enableKeyboard(keyboardId: keyboardId, enabled: enabled)
|
||||
} else {
|
||||
print ("Could not read keyboard state for package: \(packageId) and keyboard: \(keyboardId)")
|
||||
public func setKeyboardEnabled(packageId: UUID, keyboardKey: String, enabled: Bool) {
|
||||
guard let package = self.findPackage(packageId: packageId) else {
|
||||
print ("Could not read keyboard state for package: \(packageId) and keyboard: \(keyboardKey)")
|
||||
return
|
||||
}
|
||||
|
||||
print ("setKeyboardEnabled for \(keyboardKey) setting to \(enabled)")
|
||||
package.enableKeyboard(keyboardKey: keyboardKey, enabled: enabled)
|
||||
|
||||
// update persisted state in UserDefaults enabledKeyboards array
|
||||
self.persistKeyboardState()
|
||||
|
|
@ -138,23 +152,25 @@ public class SettingsContainer : ObservableObject {
|
|||
*/
|
||||
func persistKeyboardState() {
|
||||
let enabledKeyboards = self.getAllEnabledKeyboardSettingsKeys()
|
||||
self.settingsRepository.writeEnabledKeyboards(enabledKeyboardsArray: Array(enabledKeyboards))
|
||||
self.defaultsRepository.writeEnabledKeyboards(enabledKeyboardsArray: Array(enabledKeyboards))
|
||||
}
|
||||
|
||||
/**
|
||||
* read the Keyman packages from the group container directory and store in the keyboardPackages array
|
||||
*/
|
||||
func loadPackages() -> [KeymanPackage]? {
|
||||
var packagesArray = nil as [KeymanPackage]?
|
||||
func loadPackages() {
|
||||
var packagesArray = nil as [KeymanPackage]?
|
||||
|
||||
// load keyboards from disk
|
||||
// read keyboards from disk
|
||||
if (self.packageRepository.keyman19SharedDataDirectoryExists()) {
|
||||
packagesArray = self.packageRepository.loadPackages()
|
||||
} else {
|
||||
self.packageRepository.createKeyman19SharedDataDirectories()
|
||||
}
|
||||
|
||||
return packagesArray
|
||||
if let persistedPackages = packagesArray {
|
||||
self.installedPackages = persistedPackages
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -166,7 +182,7 @@ public class SettingsContainer : ObservableObject {
|
|||
// loop through all the installed packages and for each of the package's keyboards,
|
||||
// insert the settings key for the keyboard
|
||||
self.installedPackages.forEach { $0.keyboards.forEach
|
||||
{settingsKeys.insert($0.keyboardSettingsKey)}
|
||||
{settingsKeys.insert($0.keyboardKey)}
|
||||
}
|
||||
|
||||
return settingsKeys
|
||||
|
|
@ -182,7 +198,7 @@ public class SettingsContainer : ObservableObject {
|
|||
// insert the settings key for every enabled keyboard
|
||||
self.installedPackages.forEach { $0.keyboards.forEach {
|
||||
if ($0.enabled) {
|
||||
settingsKeys.insert($0.keyboardSettingsKey)
|
||||
settingsKeys.insert($0.keyboardKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -195,14 +211,14 @@ public class SettingsContainer : ObservableObject {
|
|||
*/
|
||||
func validateSettings() {
|
||||
let installedKeyboardKeys = self.getAllKeyboardSettingsKeys()
|
||||
let enabledKeyboardKeys = self.settingsRepository.readEnabledKeyboards()
|
||||
let enabledKeyboardKeys = self.defaultsRepository.readEnabledKeyboards()
|
||||
|
||||
if (enabledKeyboardKeys.isSubset(of: installedKeyboardKeys)) {
|
||||
print("only installed keyboards are listed as enabled: no need to synchronize")
|
||||
} else {
|
||||
print("enabled keyboards list contains uninstalled keyboards: synchronize enabled keyboards list")
|
||||
let installedEnabledKeyboardKeys = enabledKeyboardKeys.intersection(installedKeyboardKeys)
|
||||
self.settingsRepository.writeEnabledKeyboards(enabledKeyboardsArray: Array(installedEnabledKeyboardKeys))
|
||||
self.defaultsRepository.writeEnabledKeyboards(enabledKeyboardsArray: Array(installedEnabledKeyboardKeys))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -212,11 +228,16 @@ public class SettingsContainer : ObservableObject {
|
|||
func applySettingsToInstalledPackages() {
|
||||
self.validateSettings()
|
||||
|
||||
let enabledKeyboards = self.settingsRepository.readEnabledKeyboards()
|
||||
let enabledKeyboards = self.defaultsRepository.readEnabledKeyboards()
|
||||
|
||||
// set enabled flag if the keyboard is contained in the set of enabledKeyboards
|
||||
self.installedPackages.forEach { $0.keyboards.forEach
|
||||
{$0.enabled = enabledKeyboards.contains($0.keyboardSettingsKey)}
|
||||
{
|
||||
let keyboard = $0
|
||||
print("before keyboard \(keyboard.name) has enabled status: \(keyboard.enabled)")
|
||||
$0.enabled = enabledKeyboards.contains($0.keyboardKey)
|
||||
print("after keyboard \(keyboard.name) has enabled status: \(keyboard.enabled)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,10 +22,11 @@ public class Keyboard: Identifiable, Hashable, Equatable {
|
|||
public var keyboardDirectoryUrl: URL
|
||||
// the URL of the .kmx file for the package
|
||||
public let kmxFileUrl: URL
|
||||
// the UserDefaults key for this package, used for the selected Keyboard and enabled keyboards
|
||||
// a key to uniquely identify the keyboard
|
||||
// in the UserDefaults this key is used for the selected Keyboard and enabled keyboards properties
|
||||
// the key is in the form "/[package directory]/[package name].kmx"
|
||||
// for example, "/khmer_angkor/khmer_angkor.kmx"
|
||||
public let keyboardSettingsKey: String
|
||||
public let keyboardKey: String
|
||||
|
||||
public init(keyboardSource: KeyboardSource, directoryUrl: URL) {
|
||||
self.enabled = true
|
||||
|
|
@ -33,12 +34,23 @@ public class Keyboard: Identifiable, Hashable, Equatable {
|
|||
self.keyboardId = keyboardSource.id
|
||||
self.keyboardDirectoryUrl = directoryUrl
|
||||
self.kmxFileUrl = Keyboard.deriveKmxFileUrl(from: self.keyboardDirectoryUrl, keyboardId: self.keyboardId)
|
||||
self.keyboardSettingsKey = Keyboard.deriveKeyboardSettingsKey(from: self.keyboardDirectoryUrl, keyboardId: self.keyboardId)
|
||||
self.keyboardKey = Keyboard.deriveKeyboardSettingsKey(from: self.keyboardDirectoryUrl, keyboardId: self.keyboardId)
|
||||
|
||||
// ConfigLogger.shared.testLogger.debug("keyboard created for: \(keyboardSource.id)")
|
||||
print("keyboard created for: \(keyboardSource.id) \r with kmxFileUrl: \(self.kmxFileUrl) \r and settingsKey: \(self.keyboardSettingsKey)")
|
||||
print("keyboard created for: \(keyboardSource.id) \r with kmxFileUrl: \(self.kmxFileUrl) \r and settingsKey: \(self.keyboardKey)")
|
||||
}
|
||||
|
||||
/**
|
||||
* initializer that does not rely on package source -- provided to create unit test data
|
||||
*/
|
||||
public init(name: String, keyboardId: String, keyboardDirectoryUrl: URL, enabled: Bool) {
|
||||
self.name = name
|
||||
self.keyboardId = keyboardId
|
||||
self.keyboardDirectoryUrl = keyboardDirectoryUrl
|
||||
self.kmxFileUrl = Keyboard.deriveKmxFileUrl(from: self.keyboardDirectoryUrl, keyboardId: self.keyboardId)
|
||||
self.enabled = enabled
|
||||
self.keyboardKey = Keyboard.deriveKeyboardSettingsKey(from: self.keyboardDirectoryUrl, keyboardId: self.keyboardId)
|
||||
}
|
||||
|
||||
public static func == (lhs: Keyboard, rhs: Keyboard) -> Bool {
|
||||
return lhs.keyboardId == rhs.keyboardId && lhs.enabled == rhs.enabled
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,34 +22,25 @@ public class KeymanPackage: Identifiable, Hashable, Equatable {
|
|||
}()
|
||||
|
||||
public var id = UUID()
|
||||
let source: PackageSource
|
||||
public var keyboards: [Keyboard]
|
||||
|
||||
|
||||
// the URL of the directory in which the package is contained
|
||||
public let sourceDirectoryUrl: URL
|
||||
// the URL of the kmp.json file for the package
|
||||
|
||||
public var keyboards: [Keyboard]
|
||||
public let packageName: String
|
||||
public let packageVersion: String
|
||||
|
||||
public let copyright: String?
|
||||
public let jsonFileUrl: URL
|
||||
public let readmeFileUrl: URL?
|
||||
public let graphicFileUrl: URL?
|
||||
public let graphicImage: NSImage?
|
||||
|
||||
// computed properties for convenience
|
||||
public var packageName: String {
|
||||
return source.info.name.description
|
||||
}
|
||||
public var packageVersion: String {
|
||||
return source.info.version.description
|
||||
}
|
||||
public var copyright: String? {
|
||||
if let copy = source.info.copyright?.description {
|
||||
return copy
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
init(packageSource: PackageSource) {
|
||||
self.source = packageSource
|
||||
self.packageName = packageSource.info.name.description
|
||||
self.packageVersion = packageSource.info.version.description
|
||||
self.copyright = packageSource.info.copyright?.description
|
||||
self.sourceDirectoryUrl = packageSource.directoryUrl!
|
||||
self.jsonFileUrl = packageSource.jsonFileUrl!
|
||||
|
||||
|
|
@ -65,7 +56,7 @@ public class KeymanPackage: Identifiable, Hashable, Equatable {
|
|||
|
||||
var keyboardsArray = [Keyboard]()
|
||||
|
||||
if let keyboards = source.keyboards {
|
||||
if let keyboards = packageSource.keyboards {
|
||||
for keyboardSource in keyboards {
|
||||
let keyboard = Keyboard(keyboardSource: keyboardSource, directoryUrl: self.sourceDirectoryUrl)
|
||||
keyboardsArray.append(keyboard)
|
||||
|
|
@ -73,21 +64,38 @@ public class KeymanPackage: Identifiable, Hashable, Equatable {
|
|||
}
|
||||
self.keyboards = keyboardsArray
|
||||
|
||||
// ConfigLogger.shared.testLogger.debug("package created for: \(packageSource.packageName)")
|
||||
print("package created for: \(packageSource.packageName)")
|
||||
}
|
||||
|
||||
public func isKeyboardEnabled(keyboardId: String) -> Bool {
|
||||
|
||||
/**
|
||||
* initializer that does not rely on package source -- provided to create unit test data
|
||||
*/
|
||||
public init(sourceDirectoryUrl: URL, keyboards: [Keyboard], packageName: String, packageVersion: String, copyright: String? = nil, jsonFileUrl: URL, readmeFileUrl: URL? = nil, graphicFileUrl: URL? = nil, graphicImage: NSImage? = nil) {
|
||||
self.sourceDirectoryUrl = sourceDirectoryUrl
|
||||
self.keyboards = keyboards
|
||||
self.packageName = packageName
|
||||
self.packageVersion = packageVersion
|
||||
self.copyright = copyright
|
||||
self.jsonFileUrl = jsonFileUrl
|
||||
self.readmeFileUrl = readmeFileUrl
|
||||
self.graphicFileUrl = graphicFileUrl
|
||||
self.graphicImage = graphicImage
|
||||
}
|
||||
|
||||
public func isKeyboardEnabled(keyboardKey: String) -> Bool {
|
||||
var enabled = false
|
||||
if let keyboard = self.keyboards.first(where: { $0.keyboardId == keyboardId }) {
|
||||
if let keyboard = self.keyboards.first(where: { $0.keyboardKey == keyboardKey }) {
|
||||
let akeyboard = keyboard
|
||||
print("keyboard: \(akeyboard.keyboardKey) is enabled: \(keyboard.enabled)")
|
||||
enabled = keyboard.enabled
|
||||
}
|
||||
|
||||
print("returning: \(enabled)")
|
||||
return enabled
|
||||
}
|
||||
|
||||
public func enableKeyboard(keyboardId: String, enabled: Bool) {
|
||||
let keyboard = self.keyboards.first(where: { $0.keyboardId == keyboardId })
|
||||
public func enableKeyboard(keyboardKey: String, enabled: Bool) {
|
||||
let keyboard = self.keyboards.first(where: { $0.keyboardKey == keyboardKey })
|
||||
if (keyboard != nil) {
|
||||
keyboard!.enabled = enabled
|
||||
}
|
||||
|
|
@ -97,7 +105,7 @@ public class KeymanPackage: Identifiable, Hashable, Equatable {
|
|||
guard let keyboard = self.keyboards.first(where: { $0.keyboardId == keyboardId }) else {
|
||||
return nil
|
||||
}
|
||||
return keyboard.keyboardSettingsKey
|
||||
return keyboard.keyboardKey
|
||||
}
|
||||
|
||||
public func getEnabledKeyboardsSettingsKeys() -> [String] {
|
||||
|
|
@ -105,7 +113,7 @@ public class KeymanPackage: Identifiable, Hashable, Equatable {
|
|||
|
||||
self.keyboards.forEach { keyboard in
|
||||
if (keyboard.enabled) {
|
||||
settingsKeyArray.append(keyboard.keyboardSettingsKey)
|
||||
settingsKeyArray.append(keyboard.keyboardKey)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@
|
|||
*
|
||||
* Created by Shawn Schantz on 2025-12-10
|
||||
*
|
||||
* SettingsRepository is responsible for reading, writing and removing
|
||||
* settings stored in the UserDefaults
|
||||
* DefaultsRepository is responsible for reading, writing and removing
|
||||
* values stored in the UserDefaults
|
||||
*
|
||||
*/
|
||||
|
||||
|
|
@ -14,7 +14,7 @@ public enum UserDefaultsError: Error {
|
|||
case unknownSuite
|
||||
}
|
||||
|
||||
public struct SettingsRepository {
|
||||
public class DefaultsRepository: DefaultsRepo {
|
||||
fileprivate let pathUtil: KeymanPaths
|
||||
let defaultsSuiteName: String
|
||||
let defaults: UserDefaults
|
||||
|
|
@ -52,6 +52,7 @@ public struct SettingsRepository {
|
|||
|
||||
/**
|
||||
* update the list of enabled keyboards in the UserDefaults
|
||||
* each String in the array must be formatted `/[packageName]/[keyboardName].kmx`
|
||||
*/
|
||||
public func writeEnabledKeyboards(enabledKeyboardsArray: [String]) {
|
||||
self.defaults.set(enabledKeyboardsArray, forKey: kEnabledKeyboardsKey)
|
||||
|
|
@ -67,13 +68,14 @@ public struct SettingsRepository {
|
|||
|
||||
/**
|
||||
* update the selected keyboard keyboards in the UserDefaults
|
||||
* `keyboardName` must be formatted `/[packageName]/[keyboardName].kmx`
|
||||
*/
|
||||
public func writeSelectedKeyboard(keyboardName: String) {
|
||||
self.defaults.set(keyboardName, forKey: kSelectedKeyboardKey)
|
||||
}
|
||||
|
||||
/**
|
||||
* read the boolean setting which allows a Sentry error to be generated for testing
|
||||
* read the boolean value that allows a Sentry error to be generated for testing
|
||||
* This value is set in the **standard** application UserDefaults -- not the shared app group defaults.
|
||||
* This is necessary because it must be set to true from the command line.
|
||||
*/
|
||||
|
|
@ -82,7 +84,7 @@ public struct SettingsRepository {
|
|||
}
|
||||
|
||||
/**
|
||||
* read the data model version for the settings to know what values and format to expect
|
||||
* read the data model version to know what values and format to expect
|
||||
*/
|
||||
public func readDataModelVersion() -> Int {
|
||||
// note that zero is returned if key is not found in UserDefaults,
|
||||
|
|
@ -90,7 +92,7 @@ public struct SettingsRepository {
|
|||
}
|
||||
|
||||
/**
|
||||
* read the boolean setting which indicates whether the OSK is opened when the input method is activated
|
||||
* read the boolean value that indicates whether the OSK is opened when the input method is activated
|
||||
* There may be no need to read this from with the config app.
|
||||
*/
|
||||
public func readShowOskOnActivate() -> Bool {
|
||||
|
|
@ -114,7 +116,7 @@ public struct SettingsRepository {
|
|||
* with app group UserDefaults, there is no way to view from the command line
|
||||
* (unlike standard application-level UserDefaults)
|
||||
*/
|
||||
public func logSettings() {
|
||||
public func logDefaults() {
|
||||
print("UserDefaults:")
|
||||
print("\(kSelectedKeyboardKey): \(self.readSelectedKeyboard())")
|
||||
print("\(kDataModelVersionKey): \(self.readDataModelVersion())")
|
||||
|
|
@ -128,7 +130,7 @@ public struct SettingsRepository {
|
|||
* for debugging: clear all the entries for the app group UserDefaults
|
||||
* unlike standard application-level UserDefaults, there is no way to view from the command line
|
||||
*/
|
||||
public func clearSettings() {
|
||||
public func clearDefaults() {
|
||||
self.defaults.dictionaryRepresentation().keys.forEach { key in
|
||||
self.defaults.removeObject(forKey: key)
|
||||
}
|
||||
|
|
@ -3,14 +3,14 @@
|
|||
*
|
||||
* Created by Shawn Schantz on 2025-12-10
|
||||
*
|
||||
* DataRepository is responsible for reading, writing and removing
|
||||
* PackageRepository is responsible for reading, writing and removing
|
||||
* Keyman data on disk.
|
||||
*
|
||||
*/
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct PackageRepository {
|
||||
public class PackageRepository: PackageRepo {
|
||||
fileprivate let packageFileName = "kmp.json"
|
||||
fileprivate let pathUtil: KeymanPaths
|
||||
|
||||
|
|
@ -18,7 +18,10 @@ public struct PackageRepository {
|
|||
self.pathUtil = KeymanPaths()
|
||||
}
|
||||
|
||||
func loadPackages() -> [KeymanPackage] {
|
||||
/**
|
||||
* after reading the Keyman packages from disk, wrap each package as a `KeymanPackage` object
|
||||
*/
|
||||
public func loadPackages() -> [KeymanPackage] {
|
||||
var installedPackages: [KeymanPackage] = []
|
||||
let packageSourceArray = self.readKeymanPackagesForKeyman19()
|
||||
|
||||
|
|
@ -38,7 +41,7 @@ public struct PackageRepository {
|
|||
/**
|
||||
* delete the package from disk
|
||||
*/
|
||||
func deletePackage(package: KeymanPackage) {
|
||||
public func deletePackage(package: KeymanPackage) {
|
||||
print("deleting package: \(package.sourceDirectoryUrl)")
|
||||
do {
|
||||
try FileManager.default.removeItem(at: package.sourceDirectoryUrl)
|
||||
|
|
@ -51,7 +54,7 @@ public struct PackageRepository {
|
|||
/**
|
||||
* Creates the directory tree where keyboards are stored under the standard 'Group Containers' directory
|
||||
*/
|
||||
func createKeyman19SharedDataDirectories() {
|
||||
public func createKeyman19SharedDataDirectories() {
|
||||
if let keyboardDirectory = pathUtil.keyman19KeyboardsDirectory {
|
||||
|
||||
do {
|
||||
|
|
@ -71,10 +74,11 @@ public struct PackageRepository {
|
|||
}
|
||||
}
|
||||
|
||||
// TODO: delete
|
||||
/**
|
||||
* for group container testing purposes to check directory access
|
||||
*/
|
||||
public func writeTestFileToContainer() {
|
||||
func writeTestFileToContainer() {
|
||||
if let keyboardDirectory = self.pathUtil.keyman19KeyboardsDirectory {
|
||||
print("About to write to: \(keyboardDirectory.path)")
|
||||
|
||||
|
|
@ -113,7 +117,10 @@ public struct PackageRepository {
|
|||
return exists && isDirectory.boolValue
|
||||
}
|
||||
|
||||
public func readKeymanPackagesForKeyman19() -> [PackageSource] {
|
||||
/**
|
||||
* read keyboard packages at Keyman 19 location, inside Group Containers directory
|
||||
*/
|
||||
func readKeymanPackagesForKeyman19() -> [PackageSource] {
|
||||
guard let keyboardsUrl = self.pathUtil.keyman19KeyboardsDirectory else {
|
||||
return []
|
||||
}
|
||||
|
|
@ -121,7 +128,10 @@ public struct PackageRepository {
|
|||
return readKeyboardPackageSource(keyboardDirectoryUrl: keyboardsUrl)
|
||||
}
|
||||
|
||||
public func readKeyboardPackageSource(keyboardDirectoryUrl: URL) -> [PackageSource] {
|
||||
/**
|
||||
* loop through all the sub-directories in the keyboards directory and try to read them as packages
|
||||
*/
|
||||
func readKeyboardPackageSource(keyboardDirectoryUrl: URL) -> [PackageSource] {
|
||||
var packages: [PackageSource] = []
|
||||
|
||||
do {
|
||||
|
|
@ -148,6 +158,9 @@ public struct PackageRepository {
|
|||
return packages
|
||||
}
|
||||
|
||||
/**
|
||||
* check the specified directory for kmp.json file and read it if it exists
|
||||
*/
|
||||
func readKeyboardPackageFromDirectory(keyboardDirectoryUrl: URL) -> PackageSource? {
|
||||
var packageSource: PackageSource?
|
||||
let lastPathComponent = keyboardDirectoryUrl.lastPathComponent
|
||||
|
|
@ -167,7 +180,10 @@ public struct PackageRepository {
|
|||
return packageSource
|
||||
}
|
||||
|
||||
public func readKeyboardPackage(_ kmpFileUrl: URL) -> PackageSource? {
|
||||
/**
|
||||
* read the kmp.json file at the specified URL
|
||||
*/
|
||||
func readKeyboardPackage(_ kmpFileUrl: URL) -> PackageSource? {
|
||||
var packageSource: PackageSource?
|
||||
do {
|
||||
let jsonData = try Data(contentsOf: kmpFileUrl, options: .mappedIfSafe)
|
||||
|
|
|
|||
|
|
@ -2,6 +2,63 @@ import Testing
|
|||
import Foundation
|
||||
@testable import KeymanSettings
|
||||
|
||||
|
||||
@Suite("Settings Container") struct SettingsContainersTests {
|
||||
|
||||
fileprivate init() async throws {
|
||||
print("init")
|
||||
}
|
||||
|
||||
@Test("Check settings creation ") func testSettingsCreation() async throws {
|
||||
let defaultsRepo = DefaultsRepoStub(enabledKeyboards: Set([moabiteKeyboardKey, hittiteKeyboardKey]))
|
||||
let packageRepo = PackageRepoStub()
|
||||
let settingsContainer = SettingsContainer(defaultsRepo: defaultsRepo, packageRepo: packageRepo)
|
||||
|
||||
#expect(settingsContainer.installedPackages.isEmpty)
|
||||
|
||||
settingsContainer.loadPackages()
|
||||
|
||||
let package = try #require(settingsContainer.findPackage(packageId: packageRepo.testPackageId))
|
||||
|
||||
#expect(defaultsRepo.readEnabledKeyboards().contains(moabiteKeyboardKey))
|
||||
}
|
||||
|
||||
/**
|
||||
* include uninstalled keyboard in the list of enabled keyboards and verify that it is removed after `validateSettings()` is called
|
||||
*/
|
||||
@Test("Check settings validation ") func testSettingsValidation() async throws {
|
||||
let defaultsRepo = DefaultsRepoStub(enabledKeyboards: Set([moabiteKeyboardKey, uninstalledKeyboardKey]))
|
||||
let packageRepo = PackageRepoStub()
|
||||
let settingsContainer = SettingsContainer(defaultsRepo: defaultsRepo, packageRepo: packageRepo)
|
||||
|
||||
settingsContainer.loadPackages()
|
||||
settingsContainer.validateSettings()
|
||||
let enabledKeyboards = defaultsRepo.readEnabledKeyboards()
|
||||
|
||||
#expect(!enabledKeyboards.contains(uninstalledKeyboardKey))
|
||||
#expect(enabledKeyboards.contains(moabiteKeyboardKey))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* see that keyboard state changes from disabled to enabled when settings are applied
|
||||
*/
|
||||
@Test("Check settings applied ") func testSettingsApplied() async throws {
|
||||
let defaultsRepo = DefaultsRepoStub(enabledKeyboards: Set([moabiteKeyboardKey, hittiteKeyboardKey]))
|
||||
let packageRepo = PackageRepoStub()
|
||||
let settingsContainer = SettingsContainer(defaultsRepo: defaultsRepo, packageRepo: packageRepo)
|
||||
|
||||
settingsContainer.loadPackages()
|
||||
let package = try #require(settingsContainer.findPackage(packageId: packageRepo.testPackageId))
|
||||
|
||||
#expect(!package.isKeyboardEnabled(keyboardKey: hittiteKeyboardKey))
|
||||
|
||||
settingsContainer.applySettingsToInstalledPackages()
|
||||
|
||||
#expect(package.isKeyboardEnabled(keyboardKey: hittiteKeyboardKey))
|
||||
}
|
||||
|
||||
|
||||
@Suite("Check Keyman paths") struct KeymanPathsTests {
|
||||
|
||||
fileprivate init() async throws {
|
||||
|
|
@ -53,12 +110,12 @@ import Foundation
|
|||
// defaults read test.suite.name
|
||||
|
||||
@Suite("Settings check") struct KeymanSettingsRepositoryTests {
|
||||
var settingsRepo: SettingsRepository
|
||||
var settingsRepo: DefaultsRepository
|
||||
|
||||
fileprivate init() async throws {
|
||||
print("init Settings")
|
||||
do {
|
||||
try self.settingsRepo = SettingsRepository(suiteName: "test.suite.name")
|
||||
try self.settingsRepo = DefaultsRepository(suiteName: "test.suite.name")
|
||||
print("Found group container")
|
||||
} catch UserDefaultsError.unknownSuite {
|
||||
fatalError("Group container not found.")
|
||||
|
|
@ -91,49 +148,49 @@ import Foundation
|
|||
}
|
||||
|
||||
@Test("Read package") func loadPackageSource() async throws {
|
||||
let dataRepo = PackageRepository()
|
||||
let packageRepo = PackageRepository()
|
||||
|
||||
let kmpUrl = try #require(self.getAmharicKmpUrl())
|
||||
let _ = try #require(dataRepo.readKeyboardPackage(kmpUrl))
|
||||
let _ = try #require(packageRepo.readKeyboardPackage(kmpUrl))
|
||||
}
|
||||
|
||||
@Test("Read package name") func readPackageName() async throws {
|
||||
let dataRepo = PackageRepository()
|
||||
let packageRepo = PackageRepository()
|
||||
|
||||
let kmpUrl = try #require(self.getAmharicKmpUrl())
|
||||
let source = try #require(dataRepo.readKeyboardPackage(kmpUrl))
|
||||
let source = try #require(packageRepo.readKeyboardPackage(kmpUrl))
|
||||
#expect(source.packageName == "GFF Amharic Keyboard")
|
||||
}
|
||||
|
||||
@Test("Read package version") func readPackageVersion() async throws {
|
||||
let dataRepo = PackageRepository()
|
||||
let packageRepo = PackageRepository()
|
||||
|
||||
let kmpUrl = try #require(self.getAmharicKmpUrl())
|
||||
let source = try #require(dataRepo.readKeyboardPackage(kmpUrl))
|
||||
let source = try #require(packageRepo.readKeyboardPackage(kmpUrl))
|
||||
#expect(source.packageVersion == "3.1.2")
|
||||
}
|
||||
|
||||
@Test("Read copyright") func readCopyright() async throws {
|
||||
let dataRepo = PackageRepository()
|
||||
let packageRepo = PackageRepository()
|
||||
|
||||
let kmpUrl = try #require(self.getAmharicKmpUrl())
|
||||
let source = try #require(dataRepo.readKeyboardPackage(kmpUrl))
|
||||
let source = try #require(packageRepo.readKeyboardPackage(kmpUrl))
|
||||
#expect(source.copyright == "© Geʾez Frontier Foundation, SIL International")
|
||||
}
|
||||
|
||||
@Test("Read keyboard count") func readKeyboardCount() async throws {
|
||||
let dataRepo = PackageRepository()
|
||||
let packageRepo = PackageRepository()
|
||||
|
||||
let kmpUrl = try #require(self.getAmharicKmpUrl())
|
||||
let source = try #require(dataRepo.readKeyboardPackage(kmpUrl))
|
||||
let source = try #require(packageRepo.readKeyboardPackage(kmpUrl))
|
||||
#expect(source.keyboards?.count == 1)
|
||||
}
|
||||
|
||||
@Test("Read keyboard name") func readKeyboardName() async throws {
|
||||
let dataRepo = PackageRepository()
|
||||
let packageRepo = PackageRepository()
|
||||
|
||||
let kmpUrl = try #require(self.getAmharicKmpUrl())
|
||||
let source = try #require(dataRepo.readKeyboardPackage(kmpUrl))
|
||||
let source = try #require(packageRepo.readKeyboardPackage(kmpUrl))
|
||||
let keyboard = try #require(source.keyboards?.first)
|
||||
#expect(keyboard.name == "አማርኛ (Amharic)")
|
||||
}
|
||||
|
|
|
|||
96
mac/KeymanSettings/Tests/KeymanSettingsTests/RepoStubs.swift
Normal file
96
mac/KeymanSettings/Tests/KeymanSettingsTests/RepoStubs.swift
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
/*
|
||||
* Keyman is copyright (C) SIL Global. MIT License.
|
||||
*
|
||||
* Created by Shawn Schantz on 2026-04-29
|
||||
*
|
||||
* Contains stubs for the repository classes to support
|
||||
* unit tests that do not access the file system or UserDefaults database. *
|
||||
*/
|
||||
|
||||
import Foundation
|
||||
import KeymanSettings
|
||||
|
||||
/**
|
||||
* conforms to DefaultsRepo but takes a set of enabled keyboard keys in the initializer and
|
||||
* keeps everything in memory rather than accessing UserDefaults.
|
||||
*/
|
||||
|
||||
public let moabiteKeyboardKey = "/extinctpackage/moabite_basic.kmx"
|
||||
public let hittiteKeyboardKey = "/extinctpackage/hittite_basic.kmx"
|
||||
public let uninstalledKeyboardKey = "/extinctpackage/girgashite_basic.kmx"
|
||||
|
||||
class DefaultsRepoStub: DefaultsRepo {
|
||||
private var selectedKeyboard: String
|
||||
private var enabledKeyboards: Set<String>
|
||||
|
||||
init(enabledKeyboards: Set<String>) {
|
||||
self.enabledKeyboards = enabledKeyboards
|
||||
|
||||
// selectedKeyboard not relevant yet in config app
|
||||
// but may need validation later
|
||||
selectedKeyboard = moabiteKeyboardKey
|
||||
}
|
||||
|
||||
func readEnabledKeyboards() -> Set<String> {
|
||||
return enabledKeyboards
|
||||
}
|
||||
|
||||
func writeEnabledKeyboards(enabledKeyboardsArray: [String]) {
|
||||
self.enabledKeyboards = Set(enabledKeyboardsArray)
|
||||
}
|
||||
|
||||
func readSelectedKeyboard() -> String {
|
||||
self.selectedKeyboard = hittiteKeyboardKey
|
||||
return self.selectedKeyboard
|
||||
}
|
||||
|
||||
func logDefaults() {
|
||||
print("UserDefaults:")
|
||||
print("\("KMSelectedKeyboardsKey"): \(self.readSelectedKeyboard())")
|
||||
print("\("KMEnabledKeyboardsKey"): \(self.readEnabledKeyboards())")
|
||||
}
|
||||
|
||||
func clearDefaults() {
|
||||
selectedKeyboard = ""
|
||||
enabledKeyboards = []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* conforms to PackageRepo but creates a package with two keyboards and
|
||||
* stores them in memory with no access to disk
|
||||
*/
|
||||
class PackageRepoStub: PackageRepo {
|
||||
public var testPackageId = UUID(uuid: (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0))
|
||||
|
||||
func keyman19SharedDataDirectoryExists() -> Bool {
|
||||
true
|
||||
}
|
||||
|
||||
func createKeyman19SharedDataDirectories() {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
/**
|
||||
* creates a package containing two keyboards, one enabled and one disabled
|
||||
*/
|
||||
func loadPackages() -> [KeymanPackage] {
|
||||
let packagesDirectoryUrl = URL(filePath: "/Users/shawn-sil/Library/Group%20Containers/group.com.keyman/Library/Application%20Support/Keyman-Packages/")!
|
||||
let testPackageDirectoryName = "extinctpackage"
|
||||
let testPackageUrl = packagesDirectoryUrl.appendingPathComponent(testPackageDirectoryName)
|
||||
let moabiteKeyboardId = "moabite_basic"
|
||||
let moabiteKeyboard = Keyboard(name: "moabite basic", keyboardId: moabiteKeyboardId, keyboardDirectoryUrl: testPackageUrl, enabled: true)
|
||||
let hittiteKeyboardId = "hittite_basic"
|
||||
let hittiteKeyboard = Keyboard(name: "hittite basic", keyboardId: hittiteKeyboardId, keyboardDirectoryUrl: testPackageUrl, enabled: false)
|
||||
|
||||
let testPackage = KeymanPackage(sourceDirectoryUrl: testPackageUrl,
|
||||
keyboards: [moabiteKeyboard, hittiteKeyboard], packageName: "Extinct Languages", packageVersion: "1.1.03", jsonFileUrl: testPackageUrl.appendingPathComponent("kmp.json"))
|
||||
testPackageId = testPackage.id
|
||||
|
||||
return [testPackage]
|
||||
}
|
||||
|
||||
func deletePackage(package: KeymanPackage) {
|
||||
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue