// swiftlint:disable discouraged_optional_boolean import Foundation import Combine import Testing @testable import Defaults func createSuite() -> UserDefaults { UserDefaults(suiteName: UUID().uuidString)! } private let suite_ = createSuite() 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, suite: suite_) static let url = Key("url", default: fixtureURL, suite: suite_) static let file = Key("fileURL", default: fixtureFileURL, suite: suite_) static let data = Key("data", default: Data([]), suite: suite_) static let date = Key("date", default: fixtureDate, suite: suite_) static let uuid = Key("uuid", suite: suite_) static let defaultDynamicDate = Key("defaultDynamicOptionalDate", suite: suite_) { Date(timeIntervalSince1970: 0) } static let defaultDynamicOptionalDate = Key("defaultDynamicOptionalDate", suite: suite_) { Date(timeIntervalSince1970: 1) } } @Suite(.serialized) final class DefaultsTests { init() { Defaults.removeAll(suite: suite_) } deinit { Defaults.removeAll(suite: suite_) } @Test func testKey() { let key = Defaults.Key("independentKey", default: false, suite: suite_) #expect(!Defaults[key]) Defaults[key] = true #expect(Defaults[key]) } @Test func testValidKeyName() { let validKey = Defaults.Key("test", default: false, suite: suite_) let containsDotKey = Defaults.Key("test.a", default: false, suite: suite_) let startsWithAtKey = Defaults.Key("@test", default: false, suite: suite_) #expect(Defaults.isValidKeyPath(name: validKey.name)) #expect(!Defaults.isValidKeyPath(name: containsDotKey.name)) #expect(!Defaults.isValidKeyPath(name: startsWithAtKey.name)) } @Test func testOptionalKey() { let key = Defaults.Key("independentOptionalKey", suite: suite_) let url = Defaults.Key("independentOptionalURLKey", suite: suite_) #expect(Defaults[key] == nil) #expect(Defaults[url] == nil) Defaults[key] = true Defaults[url] = fixtureURL #expect(Defaults[key] == true) #expect(Defaults[url] == fixtureURL) Defaults[key] = nil Defaults[url] = nil #expect(Defaults[key] == nil) #expect(Defaults[url] == nil) Defaults[key] = false Defaults[url] = fixtureURL2 #expect(Defaults[key] == false) #expect(Defaults[url] == fixtureURL2) } @Test func testInitializeDynamicDateKey() { _ = Defaults.Key("independentInitializeDynamicDateKey", suite: suite_) { Issue.record("Init dynamic key should not trigger getter") return Date() } _ = Defaults.Key("independentInitializeDynamicOptionalDateKey", suite: suite_) { Issue.record("Init dynamic optional key should not trigger getter") return Date() } } @Test func testKeyRegistersDefault() { let keyName = "registersDefault" #expect(!suite_.bool(forKey: keyName)) _ = Defaults.Key(keyName, default: true, suite: suite_) #expect(suite_.bool(forKey: keyName)) let keyName2 = "registersDefault2" _ = Defaults.Key(keyName2, default: keyName2, suite: suite_) #expect(suite_.string(forKey: keyName2) == keyName2) } @Test func testKeyWithUserDefaultSubscript() { let key = Defaults.Key("keyWithUserDeaultSubscript", default: false, suite: suite_) #expect(!suite_[key]) suite_[key] = true #expect(suite_[key]) } @Test func testKeys() { #expect(!Defaults[.key]) Defaults[.key] = true #expect(Defaults[.key]) } @Test func testUrlType() { #expect(Defaults[.url] == fixtureURL) let newUrl = URL(string: "https://twitter.com")! Defaults[.url] = newUrl #expect(Defaults[.url] == newUrl) } @Test func testDataType() { #expect(Defaults[.data] == Data([])) let newData = Data([0xFF]) Defaults[.data] = newData #expect(Defaults[.data] == newData) } @Test func testDateType() { #expect(Defaults[.date] == fixtureDate) let newDate = Date() Defaults[.date] = newDate #expect(Defaults[.date] == newDate) } @Test func testDynamicDateType() { #expect(Defaults[.defaultDynamicDate] == Date(timeIntervalSince1970: 0)) let next = Date(timeIntervalSince1970: 1) Defaults[.defaultDynamicDate] = next #expect(Defaults[.defaultDynamicDate] == next) #expect(suite_.object(forKey: Defaults.Key.defaultDynamicDate.name) as! Date == next) Defaults.Key.defaultDynamicDate.reset() #expect(Defaults[.defaultDynamicDate] == Date(timeIntervalSince1970: 0)) } @Test func testDynamicOptionalDateType() { #expect(Defaults[.defaultDynamicOptionalDate] == Date(timeIntervalSince1970: 1)) let next = Date(timeIntervalSince1970: 2) Defaults[.defaultDynamicOptionalDate] = next #expect(Defaults[.defaultDynamicOptionalDate] == next) #expect(suite_.object(forKey: Defaults.Key.defaultDynamicOptionalDate.name) as! Date == next) Defaults[.defaultDynamicOptionalDate] = nil #expect(Defaults[.defaultDynamicOptionalDate] == Date(timeIntervalSince1970: 1)) #expect(suite_.object(forKey: Defaults.Key.defaultDynamicOptionalDate.name) == nil) } @Test func testFileURLType() { #expect(Defaults[.file] == fixtureFileURL) } @Test func testUUIDType() { let fixture = UUID() Defaults[.uuid] = fixture #expect(Defaults[.uuid] == fixture) } @Test func testRemoveAll() { let key = Defaults.Key("removeAll", default: false, suite: suite_) let key2 = Defaults.Key("removeAll2", default: false, suite: suite_) Defaults[key] = true Defaults[key2] = true #expect(Defaults[key]) #expect(Defaults[key2]) Defaults.removeAll(suite: suite_) #expect(!Defaults[key]) #expect(!Defaults[key2]) } @Test func testCustomSuite() { let customSuite = UserDefaults(suiteName: "com.sindresorhus.customSuite")! let key = Defaults.Key("customSuite", default: false, suite: customSuite) #expect(!customSuite[key]) #expect(!Defaults[key]) Defaults[key] = true #expect(customSuite[key]) #expect(Defaults[key]) Defaults.removeAll(suite: customSuite) } @Test func testIsDefaultValue() { let key = Defaults.Key("isDefaultValue", default: false, suite: suite_) #expect(key.isDefaultValue) Defaults[key].toggle() #expect(!key.isDefaultValue) } @available(iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0, visionOS 1.0, *) @Test func testObserveKeyCombine() async throws { let key = Defaults.Key("observeKey", default: false, suite: suite_) var results: [(Bool, Bool)] = [] try await confirmation(expectedCount: 2) { confirmation in let publisher = Defaults .publisher(key, options: []) .map { ($0.oldValue, $0.newValue) } .sink { value in results.append(value) confirmation() } await Task.yield() Defaults[key] = true Defaults.reset(key) try await Task.sleep(for: .milliseconds(100)) publisher.cancel() } let expectedValues = [(false, true), (true, false)] // #expect(results == expectedValues) #expect(results.count == expectedValues.count) for (index, expected) in expectedValues.enumerated() { #expect(results[index].0 == expected.0) #expect(results[index].1 == expected.1) } } @available(iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0, visionOS 1.0, *) @Test func testObserveOptionalKeyCombine() async throws { let key = Defaults.Key("observeOptionalKey", suite: suite_) var results: [(Bool?, Bool?)] = [] try await confirmation(expectedCount: 3) { confirmation in let publisher = Defaults .publisher(key, options: []) .map { ($0.oldValue, $0.newValue) } .sink { value in results.append(value) confirmation() } await Task.yield() Defaults[key] = true Defaults[key] = false Defaults.reset(key) try await Task.sleep(for: .milliseconds(100)) publisher.cancel() } let expectedValues: [(Bool?, Bool?)] = [(nil, true), (true, false), (false, nil)] // #expect(results == expectedValues) #expect(results.count == expectedValues.count) for (index, expected) in expectedValues.enumerated() { #expect(results[index].0 == expected.0) #expect(results[index].1 == expected.1) } } @available(iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0, visionOS 1.0, *) @Test func testDynamicOptionalDateTypeCombine() async throws { let first = Date(timeIntervalSince1970: 0) let second = Date(timeIntervalSince1970: 1) let third = Date(timeIntervalSince1970: 2) let key = Defaults.Key("combineDynamicOptionalDateKey", suite: suite_) { first } var results: [(Date?, Date?)] = [] try await confirmation(expectedCount: 3) { confirmation in let publisher = Defaults .publisher(key, options: []) .map { ($0.oldValue, $0.newValue) } .sink { value in results.append(value) confirmation() } await Task.yield() Defaults[key] = second Defaults[key] = third Defaults.reset(key) try await Task.sleep(for: .milliseconds(100)) publisher.cancel() } let expectedValues: [(Date?, Date?)] = [(first, second), (second, third), (third, first)] // #expect(results == expectedValues) #expect(results.count == expectedValues.count) for (index, expected) in expectedValues.enumerated() { #expect(results[index].0 == expected.0) #expect(results[index].1 == expected.1) } } @available(iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0, visionOS 1.0, *) @Test func testObserveMultipleKeysCombine() async throws { let key1 = Defaults.Key("observeKey1", default: "x", suite: suite_) let key2 = Defaults.Key("observeKey2", default: true, suite: suite_) var count = 0 try await confirmation(expectedCount: 2) { confirmation in let publisher = Defaults.publisher(keys: key1, key2, options: []) .sink { _ in count += 1 confirmation() } await Task.yield() Defaults[key1] = "y" Defaults[key2] = false try await Task.sleep(for: .milliseconds(100)) publisher.cancel() } #expect(count == 2) } @available(iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0, visionOS 1.0, *) @Test func testObserveMultipleOptionalKeysCombine() async throws { let key1 = Defaults.Key("observeOptionalKey1", suite: suite_) let key2 = Defaults.Key("observeOptionalKey2", suite: suite_) var count = 0 try await confirmation(expectedCount: 2) { confirmation in let publisher = Defaults.publisher(keys: key1, key2, options: []) .sink { _ in count += 1 confirmation() } await Task.yield() Defaults[key1] = "x" Defaults[key2] = false try await Task.sleep(for: .milliseconds(100)) publisher.cancel() } #expect(count == 2) } @available(iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0, visionOS 1.0, *) @Test func testReceiveValueBeforeSubscriptionCombine() async throws { let key = Defaults.Key("receiveValueBeforeSubscription", default: "hello", suite: suite_) try await confirmation(expectedCount: 2) { confirmation in let publisher = Defaults .publisher(key) .map(\.newValue) .sink { value in confirmation() print("Received value: \(value)") } // Ensure we're subscribed before changing the value await Task.yield() // Change the value Defaults[key] = "world" // Keep the subscription alive try await Task.sleep(for: .milliseconds(100)) publisher.cancel() } } @Test func testObservePreventPropagationCombine() async throws { let key1 = Defaults.Key("preventPropagation6", default: nil, suite: suite_) await confirmation { confirmation in var wasInside = false let cancellable = Defaults.publisher(key1, options: []).sink { _ in #expect(!wasInside) wasInside = true Defaults.withoutPropagation { Defaults[key1] = true } confirmation() } Defaults[key1] = false cancellable.cancel() } } @Test func testObservePreventPropagationMultipleKeysCombine() async throws { let key1 = Defaults.Key("preventPropagation7", default: nil, suite: suite_) let key2 = Defaults.Key("preventPropagation8", default: nil, suite: suite_) await confirmation { confirmation in var wasInside = false let cancellable = Defaults.publisher(keys: key1, key2, options: []).sink { _ in #expect(!wasInside) wasInside = true Defaults.withoutPropagation { Defaults[key1] = true } confirmation() } Defaults[key2] = false cancellable.cancel() } } @Test func testResetKey() { let defaultFixture1 = "foo1" let defaultFixture2 = 0 let newFixture1 = "bar1" let newFixture2 = 1 let key1 = Defaults.Key("key1", default: defaultFixture1, suite: suite_) let key2 = Defaults.Key("key2", default: defaultFixture2, suite: suite_) Defaults[key1] = newFixture1 Defaults[key2] = newFixture2 Defaults.reset(key1) #expect(Defaults[key1] == defaultFixture1) #expect(Defaults[key2] == newFixture2) } @Test 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, suite: suite_) let key2 = Defaults.Key("akey2", default: defaultFixture2, suite: suite_) let key3 = Defaults.Key("akey3", default: defaultFixture3, suite: suite_) Defaults[key1] = newFixture1 Defaults[key2] = newFixture2 Defaults[key3] = newFixture3 Defaults.reset(key1, key2) #expect(Defaults[key1] == defaultFxiture1) #expect(Defaults[key2] == defaultFixture2) #expect(Defaults[key3] == newFixture3) } @Test func testResetMultipleOptionalKeys() { let newFixture1 = "bar1" let newFixture2 = 1 let newFixture3 = "bar3" let key1 = Defaults.Key("aoptionalKey1", suite: suite_) let key2 = Defaults.Key("aoptionalKey2", suite: suite_) let key3 = Defaults.Key("aoptionalKey3", suite: suite_) Defaults[key1] = newFixture1 Defaults[key2] = newFixture2 Defaults[key3] = newFixture3 Defaults.reset(key1, key2) #expect(Defaults[key1] == nil) #expect(Defaults[key2] == nil) #expect(Defaults[key3] == newFixture3) } @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, visionOS 1.0, *) @Test func testImmediatelyFinishingPublisherCombine() async throws { let key = Defaults.Key("observeKey", default: false, suite: suite_) let result: Void? = try await withThrowingTaskGroup(of: Void.self) { group in let publisher = Defaults .publisher(key, options: [.initial]) .first() group.addTask { for try await _ in publisher.values { return } } return try await group.next() } #expect(result != nil) } @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, visionOS 1.0, *) @Test func testImmediatelyFinishingMultiplePublisherCombine() async throws { let key1 = Defaults.Key("observeKey1", default: false, suite: suite_) let key2 = Defaults.Key("observeKey2", default: "🦄", suite: suite_) let result: Void? = try await withThrowingTaskGroup(of: Void.self) { group in let publisher = Defaults .publisher(keys: [key1, key2], options: [.initial]) .first() group.addTask { for try await _ in publisher.values { return } } return try await group.next() } #expect(result != nil) } @Test func testKeyEquatable() { // swiftlint:disable:next identical_operands #expect(Defaults.Key("equatableKeyTest", default: false, suite: suite_) == Defaults.Key("equatableKeyTest", default: false, suite: suite_)) } @Test func testKeyHashable() { _ = Set([Defaults.Key("hashableKeyTest", default: false, suite: suite_)]) } @available(iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0, visionOS 1.0, *) @Test func testUpdates() async { let key = Defaults.Key("updatesKey", default: false, suite: suite_) async let waiter = Defaults.updates(key, initial: false).first { $0 } try? await Task.sleep(for: .seconds(0.1)) Defaults[key] = true guard let result = await waiter else { Issue.record("Failed to get result") return } #expect(result) } @available(iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0, visionOS 1.0, *) @Test func testUpdatesMultipleKeys() async { let key1 = Defaults.Key("updatesMultipleKey1", default: false, suite: suite_) let key2 = Defaults.Key("updatesMultipleKey2", default: false, suite: suite_) 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(for: .seconds(0.1)) Defaults[key1] = true Defaults[key2] = true await waiter let count = await counter.count #expect(count == 2) } @available(iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0, visionOS 1.0, *) @Test func testUpdatesMultipleKeysVariadic() async { let key1 = Defaults.Key("updatesMultipleKeyVariadic1", default: false, suite: suite_) let key2 = Defaults.Key("updatesMultipleKeyVariadic2", default: false, suite: suite_) 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(for: .seconds(0.1)) Defaults[key1] = true Defaults[key2] = true await waiter let count = await counter.count #expect(count == 2) } } actor Counter { private var _count = 0 var count: Int { _count } func increment() { _count += 1 } } // swiftlint:enable discouraged_optional_boolean