// swiftlint:disable discouraged_optional_boolean import Foundation import Combine import XCTest @testable import Defaults let fixtureURL = URL(string: "https://sindresorhus.com")! let fixtureFileURL = URL(string: "file://~/icon.png")! let fixtureURL2 = URL(string: "https://example.com")! let fixtureDate = Date() extension Defaults.Keys { static let key = Key("key", default: false) static let url = Key("url", default: fixtureURL) static let file = Key("fileURL", default: fixtureFileURL) static let data = Key("data", default: Data([])) static let date = Key("date", default: fixtureDate) static let uuid = Key("uuid") static let defaultDynamicDate = Key("defaultDynamicOptionalDate") { Date(timeIntervalSince1970: 0) } static let defaultDynamicOptionalDate = Key("defaultDynamicOptionalDate") { Date(timeIntervalSince1970: 1) } } final class DefaultsTests: XCTestCase { override func setUp() { super.setUp() Defaults.removeAll() } override func tearDown() { super.tearDown() Defaults.removeAll() } func testKey() { let key = Defaults.Key("independentKey", default: false) XCTAssertFalse(Defaults[key]) Defaults[key] = true XCTAssertTrue(Defaults[key]) } func testValidKeyName() { let validKey = Defaults.Key("test", default: false) let containsDotKey = Defaults.Key("test.a", default: false) let startsWithAtKey = Defaults.Key("@test", default: false) XCTAssertTrue(Defaults.isValidKeyPath(name: validKey.name)) XCTAssertFalse(Defaults.isValidKeyPath(name: containsDotKey.name)) XCTAssertFalse(Defaults.isValidKeyPath(name: startsWithAtKey.name)) } func testOptionalKey() { let key = Defaults.Key("independentOptionalKey") let url = Defaults.Key("independentOptionalURLKey") XCTAssertNil(Defaults[key]) XCTAssertNil(Defaults[url]) Defaults[key] = true Defaults[url] = fixtureURL XCTAssertTrue(Defaults[key]!) XCTAssertEqual(Defaults[url], fixtureURL) Defaults[key] = nil Defaults[url] = nil XCTAssertNil(Defaults[key]) XCTAssertNil(Defaults[url]) Defaults[key] = false Defaults[url] = fixtureURL2 XCTAssertFalse(Defaults[key]!) XCTAssertEqual(Defaults[url], fixtureURL2) } func testInitializeDynamicDateKey() { _ = Defaults.Key("independentInitializeDynamicDateKey") { XCTFail("Init dynamic key should not trigger getter") return Date() } _ = Defaults.Key("independentInitializeDynamicOptionalDateKey") { XCTFail("Init dynamic optional key should not trigger getter") return Date() } } func testKeyRegistersDefault() { let keyName = "registersDefault" XCTAssertFalse(UserDefaults.standard.bool(forKey: keyName)) _ = Defaults.Key(keyName, default: true) XCTAssertTrue(UserDefaults.standard.bool(forKey: keyName)) // Test that it works with multiple keys with `Defaults`. let keyName2 = "registersDefault2" _ = Defaults.Key(keyName2, default: keyName2) XCTAssertEqual(UserDefaults.standard.string(forKey: keyName2), keyName2) } func testKeyWithUserDefaultSubscript() { let key = Defaults.Key("keyWithUserDeaultSubscript", default: false) XCTAssertFalse(UserDefaults.standard[key]) UserDefaults.standard[key] = true XCTAssertTrue(UserDefaults.standard[key]) } func testKeys() { XCTAssertFalse(Defaults[.key]) Defaults[.key] = true XCTAssertTrue(Defaults[.key]) } func testUrlType() { XCTAssertEqual(Defaults[.url], fixtureURL) let newUrl = URL(string: "https://twitter.com")! Defaults[.url] = newUrl XCTAssertEqual(Defaults[.url], newUrl) } func testDataType() { XCTAssertEqual(Defaults[.data], Data([])) let newData = Data([0xFF]) Defaults[.data] = newData XCTAssertEqual(Defaults[.data], newData) } func testDateType() { XCTAssertEqual(Defaults[.date], fixtureDate) let newDate = Date() Defaults[.date] = newDate XCTAssertEqual(Defaults[.date], newDate) } func testDynamicDateType() { XCTAssertEqual(Defaults[.defaultDynamicDate], Date(timeIntervalSince1970: 0)) let next = Date(timeIntervalSince1970: 1) Defaults[.defaultDynamicDate] = next XCTAssertEqual(Defaults[.defaultDynamicDate], next) XCTAssertEqual(UserDefaults.standard.object(forKey: Defaults.Key.defaultDynamicDate.name) as! Date, next) Defaults.Key.defaultDynamicDate.reset() XCTAssertEqual(Defaults[.defaultDynamicDate], Date(timeIntervalSince1970: 0)) } func testDynamicOptionalDateType() { XCTAssertEqual(Defaults[.defaultDynamicOptionalDate], Date(timeIntervalSince1970: 1)) let next = Date(timeIntervalSince1970: 2) Defaults[.defaultDynamicOptionalDate] = next XCTAssertEqual(Defaults[.defaultDynamicOptionalDate], next) XCTAssertEqual(UserDefaults.standard.object(forKey: Defaults.Key.defaultDynamicOptionalDate.name) as! Date, next) Defaults[.defaultDynamicOptionalDate] = nil XCTAssertEqual(Defaults[.defaultDynamicOptionalDate], Date(timeIntervalSince1970: 1)) XCTAssertNil(UserDefaults.standard.object(forKey: Defaults.Key.defaultDynamicOptionalDate.name)) } func testFileURLType() { XCTAssertEqual(Defaults[.file], fixtureFileURL) } func testUUIDType() { let fixture = UUID() Defaults[.uuid] = fixture XCTAssertEqual(Defaults[.uuid], fixture) } func testRemoveAll() { let key = Defaults.Key("removeAll", default: false) let key2 = Defaults.Key("removeAll2", default: false) Defaults[key] = true Defaults[key2] = true XCTAssertTrue(Defaults[key]) XCTAssertTrue(Defaults[key2]) Defaults.removeAll() XCTAssertFalse(Defaults[key]) XCTAssertFalse(Defaults[key2]) } func testCustomSuite() { let customSuite = UserDefaults(suiteName: "com.sindresorhus.customSuite")! let key = Defaults.Key("customSuite", default: false, suite: customSuite) XCTAssertFalse(customSuite[key]) XCTAssertFalse(Defaults[key]) Defaults[key] = true XCTAssertTrue(customSuite[key]) XCTAssertTrue(Defaults[key]) Defaults.removeAll(suite: customSuite) } func testObserveKeyCombine() { let key = Defaults.Key("observeKey", default: false) let expect = expectation(description: "Observation closure being called") let publisher = Defaults .publisher(key, options: []) .map { ($0.oldValue, $0.newValue) } .collect(2) let cancellable = publisher.sink { tuples in for (index, expected) in [(false, true), (true, false)].enumerated() { XCTAssertEqual(expected.0, tuples[index].0) XCTAssertEqual(expected.1, tuples[index].1) } expect.fulfill() } Defaults[key] = true Defaults.reset(key) cancellable.cancel() waitForExpectations(timeout: 10) } func testObserveOptionalKeyCombine() { let key = Defaults.Key("observeOptionalKey") let expect = expectation(description: "Observation closure being called") let publisher = Defaults .publisher(key, options: []) .map { ($0.oldValue, $0.newValue) } .collect(3) let expectedValues: [(Bool?, Bool?)] = [(nil, true), (true, false), (false, nil)] let cancellable = publisher.sink { actualValues in for (expected, actual) in zip(expectedValues, actualValues) { XCTAssertEqual(expected.0, actual.0) XCTAssertEqual(expected.1, actual.1) } expect.fulfill() } Defaults[key] = true Defaults[key] = false Defaults.reset(key) cancellable.cancel() waitForExpectations(timeout: 10) } func testDynamicOptionalDateTypeCombine() { let first = Date(timeIntervalSince1970: 0) let second = Date(timeIntervalSince1970: 1) let third = Date(timeIntervalSince1970: 2) let key = Defaults.Key("combineDynamicOptionalDateKey") { first } let expect = expectation(description: "Observation closure being called") let publisher = Defaults .publisher(key, options: []) .map { ($0.oldValue, $0.newValue) } .collect(3) let expectedValues: [(Date?, Date?)] = [(first, second), (second, third), (third, first)] let cancellable = publisher.sink { actualValues in for (expected, actual) in zip(expectedValues, actualValues) { XCTAssertEqual(expected.0, actual.0) XCTAssertEqual(expected.1, actual.1) } expect.fulfill() } Defaults[key] = second Defaults[key] = third Defaults.reset(key) cancellable.cancel() waitForExpectations(timeout: 10) } func testObserveMultipleKeysCombine() { let key1 = Defaults.Key("observeKey1", default: "x") let key2 = Defaults.Key("observeKey2", default: true) let expect = expectation(description: "Observation closure being called") let publisher = Defaults.publisher(keys: key1, key2, options: []).collect(2) let cancellable = publisher.sink { _ in expect.fulfill() } Defaults[key1] = "y" Defaults[key2] = false cancellable.cancel() waitForExpectations(timeout: 10) } func testObserveMultipleOptionalKeysCombine() { let key1 = Defaults.Key("observeOptionalKey1") let key2 = Defaults.Key("observeOptionalKey2") let expect = expectation(description: "Observation closure being called") let publisher = Defaults.publisher(keys: key1, key2, options: []).collect(2) let cancellable = publisher.sink { _ in expect.fulfill() } Defaults[key1] = "x" Defaults[key2] = false cancellable.cancel() waitForExpectations(timeout: 10) } func testReceiveValueBeforeSubscriptionCombine() { let key = Defaults.Key("receiveValueBeforeSubscription", default: "hello") let expect = expectation(description: "Observation closure being called") let publisher = Defaults .publisher(key) .map(\.newValue) .eraseToAnyPublisher() .collect(2) let cancellable = publisher.sink { values in XCTAssertEqual(["hello", "world"], values) expect.fulfill() } Defaults[key] = "world" cancellable.cancel() waitForExpectations(timeout: 10) } func testObserveKey() { let key = Defaults.Key("observeKey", default: false) let expect = expectation(description: "Observation closure being called") var observation: Defaults.Observation! observation = Defaults.observe(key, options: []) { change in XCTAssertFalse(change.oldValue) XCTAssertTrue(change.newValue) observation.invalidate() expect.fulfill() } Defaults[key] = true waitForExpectations(timeout: 10) } func testObserveOptionalKey() { let key = Defaults.Key("observeOptionalKey") let expect = expectation(description: "Observation closure being called") var observation: Defaults.Observation! observation = Defaults.observe(key, options: []) { change in XCTAssertNil(change.oldValue) XCTAssertTrue(change.newValue!) observation.invalidate() expect.fulfill() } Defaults[key] = true waitForExpectations(timeout: 10) } func testObserveMultipleKeys() { let key1 = Defaults.Key("observeKey1", default: "x") let key2 = Defaults.Key("observeKey2", default: true) let expect = expectation(description: "Observation closure being called") var observation: Defaults.Observation! var counter = 0 observation = Defaults.observe(keys: key1, key2, options: []) { counter += 1 if counter == 2 { expect.fulfill() } else if counter > 2 { XCTFail() // swiftlint:disable:this xctfail_message } } Defaults[key1] = "y" Defaults[key2] = false observation.invalidate() waitForExpectations(timeout: 10) } func testObserveKeyURL() { let key = Defaults.Key("observeKeyURL", default: fixtureURL) let expect = expectation(description: "Observation closure being called") var observation: Defaults.Observation! observation = Defaults.observe(key, options: []) { change in XCTAssertEqual(change.oldValue, fixtureURL) XCTAssertEqual(change.newValue, fixtureURL2) observation.invalidate() expect.fulfill() } Defaults[key] = fixtureURL2 waitForExpectations(timeout: 10) } func testObserveDynamicOptionalDateKey() { let first = Date(timeIntervalSince1970: 0) let second = Date(timeIntervalSince1970: 1) let key = Defaults.Key("observeDynamicOptionalDate") { first } let expect = expectation(description: "Observation closure being called") var observation: Defaults.Observation! observation = Defaults.observe(key, options: []) { change in XCTAssertEqual(change.oldValue, first) XCTAssertEqual(change.newValue, second) observation.invalidate() expect.fulfill() } Defaults[key] = second waitForExpectations(timeout: 10) } func testObservePreventPropagation() { let key1 = Defaults.Key("preventPropagation0", default: nil) let expect = expectation(description: "No infinite recursion") var observation: Defaults.Observation! var wasInside = false observation = Defaults.observe(key1, options: []) { _ in XCTAssertFalse(wasInside) wasInside = true Defaults.withoutPropagation { Defaults[key1] = true } expect.fulfill() } Defaults[key1] = false observation.invalidate() waitForExpectations(timeout: 10) } func testObservePreventPropagationMultipleKeys() { let key1 = Defaults.Key("preventPropagation1", default: nil) let key2 = Defaults.Key("preventPropagation2", default: nil) let expect = expectation(description: "No infinite recursion") var observation: Defaults.Observation! var wasInside = false observation = Defaults.observe(keys: key1, key2, options: []) { XCTAssertFalse(wasInside) wasInside = true Defaults.withoutPropagation { Defaults[key1] = true } expect.fulfill() } Defaults[key1] = false observation.invalidate() waitForExpectations(timeout: 10) } // This checks if the callback is still being called if the value is changed on a second thread while the initial thread is doing some long running task. func testObservePreventPropagationMultipleThreads() { let key1 = Defaults.Key("preventPropagation3", default: nil) let expect = expectation(description: "No infinite recursion") var observation: Defaults.Observation! observation = Defaults.observe(key1, options: []) { _ in Defaults.withoutPropagation { Defaults[key1]! += 1 } print("--- Main Thread: \(Thread.isMainThread)") if !Thread.isMainThread { XCTAssertEqual(Defaults[key1]!, 4) expect.fulfill() } else { usleep(300_000) print("--- Release: \(Thread.isMainThread)") } } DispatchQueue.global().asyncAfter(deadline: .now() + 0.05) { Defaults[key1]! += 1 } Defaults[key1] = 1 observation.invalidate() waitForExpectations(timeout: 10) } // Check if propagation prevention works across multiple observations. func testObservePreventPropagationMultipleObservations() { let key1 = Defaults.Key("preventPropagation4", default: nil) let key2 = Defaults.Key("preventPropagation5", default: nil) let expect = expectation(description: "No infinite recursion") let observation1 = Defaults.observe(key2, options: []) { _ in XCTFail() // swiftlint:disable:this xctfail_message } let observation2 = Defaults.observe(keys: key1, key2, options: []) { Defaults.withoutPropagation { Defaults[key2] = true } expect.fulfill() } Defaults[key1] = false observation1.invalidate() observation2.invalidate() waitForExpectations(timeout: 10) } func testObservePreventPropagationCombine() { let key1 = Defaults.Key("preventPropagation6", default: nil) let expect = expectation(description: "No infinite recursion") var wasInside = false let cancellable = Defaults.publisher(key1, options: []).sink { _ in XCTAssertFalse(wasInside) wasInside = true Defaults.withoutPropagation { Defaults[key1] = true } expect.fulfill() } Defaults[key1] = false cancellable.cancel() waitForExpectations(timeout: 10) } func testObservePreventPropagationMultipleKeysCombine() { let key1 = Defaults.Key("preventPropagation7", default: nil) let key2 = Defaults.Key("preventPropagation8", default: nil) let expect = expectation(description: "No infinite recursion") var wasInside = false let cancellable = Defaults.publisher(keys: key1, key2, options: []).sink { _ in XCTAssertFalse(wasInside) wasInside = true Defaults.withoutPropagation { Defaults[key1] = true } expect.fulfill() } Defaults[key2] = false cancellable.cancel() waitForExpectations(timeout: 10) } func testObservePreventPropagationModifiersCombine() { let key1 = Defaults.Key("preventPropagation9", default: nil) let expect = expectation(description: "No infinite recursion") var wasInside = false var cancellable: AnyCancellable! cancellable = Defaults.publisher(key1, options: []) .receive(on: DispatchQueue.main) .delay(for: 0.5, scheduler: DispatchQueue.global()) .sink { _ in XCTAssertFalse(wasInside) wasInside = true Defaults.withoutPropagation { Defaults[key1] = true } expect.fulfill() cancellable.cancel() } Defaults[key1] = false waitForExpectations(timeout: 10) } func testRemoveDuplicatesObserveKeyCombine() { let key = Defaults.Key("observeKey", default: false) let expect = expectation(description: "Observation closure being called") let inputArray = [true, false, false, false, false, false, false, true] let expectedArray = [true, false, true] let cancellable = Defaults .publisher(key, options: []) .removeDuplicates() .map(\.newValue) .collect(expectedArray.count) .sink { result in print("Result array: \(result)") if result == expectedArray { expect.fulfill() } else { XCTFail("Expected Array is not matched") } } inputArray.forEach { Defaults[key] = $0 } Defaults.reset(key) cancellable.cancel() waitForExpectations(timeout: 10) } func testRemoveDuplicatesOptionalObserveKeyCombine() { let key = Defaults.Key("observeOptionalKey", default: nil) let expect = expectation(description: "Observation closure being called") let inputArray = [true, nil, nil, nil, false, false, false, nil] let expectedArray = [true, nil, false, nil] let cancellable = Defaults .publisher(key, options: []) .removeDuplicates() .map(\.newValue) .collect(expectedArray.count) .sink { result in print("Result array: \(result)") if result == expectedArray { expect.fulfill() } else { XCTFail("Expected Array is not matched") } } inputArray.forEach { Defaults[key] = $0 } Defaults.reset(key) cancellable.cancel() waitForExpectations(timeout: 10) } func testResetKey() { let defaultFixture1 = "foo1" let defaultFixture2 = 0 let newFixture1 = "bar1" let newFixture2 = 1 let key1 = Defaults.Key("key1", default: defaultFixture1) let key2 = Defaults.Key("key2", default: defaultFixture2) Defaults[key1] = newFixture1 Defaults[key2] = newFixture2 Defaults.reset(key1) XCTAssertEqual(Defaults[key1], defaultFixture1) XCTAssertEqual(Defaults[key2], newFixture2) } func testResetMultipleKeys() { let defaultFxiture1 = "foo1" let defaultFixture2 = 0 let defaultFixture3 = "foo3" let newFixture1 = "bar1" let newFixture2 = 1 let newFixture3 = "bar3" let key1 = Defaults.Key("akey1", default: defaultFxiture1) let key2 = Defaults.Key("akey2", default: defaultFixture2) let key3 = Defaults.Key("akey3", default: defaultFixture3) Defaults[key1] = newFixture1 Defaults[key2] = newFixture2 Defaults[key3] = newFixture3 Defaults.reset(key1, key2) XCTAssertEqual(Defaults[key1], defaultFxiture1) XCTAssertEqual(Defaults[key2], defaultFixture2) XCTAssertEqual(Defaults[key3], newFixture3) } func testResetMultipleOptionalKeys() { let newFixture1 = "bar1" let newFixture2 = 1 let newFixture3 = "bar3" let key1 = Defaults.Key("aoptionalKey1") let key2 = Defaults.Key("aoptionalKey2") let key3 = Defaults.Key("aoptionalKey3") Defaults[key1] = newFixture1 Defaults[key2] = newFixture2 Defaults[key3] = newFixture3 Defaults.reset(key1, key2) XCTAssertNil(Defaults[key1]) XCTAssertNil(Defaults[key2]) XCTAssertEqual(Defaults[key3], newFixture3) } func testObserveWithLifetimeTie() { let key = Defaults.Key("lifetimeTie", default: false) let expect = expectation(description: "Observation closure being called") weak var observation: Defaults.Observation! observation = Defaults.observe(key, options: []) { _ in observation.invalidate() expect.fulfill() } .tieToLifetime(of: self) Defaults[key] = true waitForExpectations(timeout: 10) } func testObserveWithLifetimeTieManualBreak() { let key = Defaults.Key("lifetimeTieManualBreak", default: false) weak var observation: Defaults.Observation? = Defaults.observe(key, options: []) { _ in }.tieToLifetime(of: self) observation!.removeLifetimeTie() for index in 1...10 { if observation == nil { break } sleep(1) if index == 10 { XCTFail() // swiftlint:disable:this xctfail_message } } } func testImmediatelyFinishingPublisherCombine() { let key = Defaults.Key("observeKey", default: false) let expect = expectation(description: "Observation closure being called without crashing") let cancellable = Defaults .publisher(key, options: [.initial]) .first() .sink { _ in expect.fulfill() } cancellable.cancel() waitForExpectations(timeout: 10) } func testImmediatelyFinishingMultiplePublisherCombine() { let key1 = Defaults.Key("observeKey1", default: false) let key2 = Defaults.Key("observeKey2", default: "🦄") let expect = expectation(description: "Observation closure being called without crashing") let cancellable = Defaults .publisher(keys: [key1, key2], options: [.initial]) .first() .sink { _ in expect.fulfill() } cancellable.cancel() waitForExpectations(timeout: 10) } func testKeyEquatable() { XCTAssertEqual(Defaults.Key("equatableKeyTest", default: false), Defaults.Key("equatableKeyTest", default: false)) } func testKeyHashable() { _ = Set([Defaults.Key("hashableKeyTest", default: false)]) } func testUpdates() async { let key = Defaults.Key("updatesKey", default: false) async let waiter = Defaults.updates(key, initial: false).first { $0 } try? await Task.sleep(seconds: 0.1) Defaults[key] = true guard let result = await waiter else { XCTFail() // swiftlint:disable:this xctfail_message return } XCTAssertTrue(result) } func testUpdatesMultipleKeys() async { let key1 = Defaults.Key("updatesMultipleKey1", default: false) let key2 = Defaults.Key("updatesMultipleKey2", default: false) let counter = Counter() async let waiter: Void = { for await _ in Defaults.updates([key1, key2], initial: false) { await counter.increment() if await counter.count == 2 { break } } }() try? await Task.sleep(seconds: 0.1) Defaults[key1] = true Defaults[key2] = true await waiter let count = await counter.count XCTAssertEqual(count, 2) } } actor Counter { private var _count = 0 var count: Int { _count } func increment() { _count += 1 } } // TODO: Remove when testing on macOS 13. extension Task { static func sleep(seconds: TimeInterval) async throws { try await sleep(nanoseconds: UInt64(seconds * Double(NSEC_PER_SEC))) } } // swiftlint:enable discouraged_optional_boolean