From 59effef18fc3350651398badc4350e4f56d6c524 Mon Sep 17 00:00:00 2001 From: Sami Samhuri Date: Sat, 26 Apr 2025 20:10:17 -0700 Subject: [PATCH] Add tests --- Readme.md | 3 + .../BetterNotification.swift | 32 +++- .../BetterNotificationTests.swift | 138 +++++++++++++++++- .../HitchhikersNotification.swift | 5 + 4 files changed, 169 insertions(+), 9 deletions(-) create mode 100644 Tests/BetterNotificationTests/HitchhikersNotification.swift diff --git a/Readme.md b/Readme.md index 27972b7..4f51c31 100644 --- a/Readme.md +++ b/Readme.md @@ -14,8 +14,11 @@ tktk ## Installation +Honestly you should probably just copy [BetterNotification.swift]() into your project. + The only way to install this package is with Swift Package Manager (SPM). Please [file a new issue][] or submit a pull-request if you want to use something else. +[BetterNotification.swift]: https://github.com/samsonjs/BetterNotification/blob/main/Sources/BetterNotification/BetterNotification.swift [file a new issue]: https://github.com/samsonjs/BetterNotification/issues/new ### Supported Platforms diff --git a/Sources/BetterNotification/BetterNotification.swift b/Sources/BetterNotification/BetterNotification.swift index 4f41bf8..05882b4 100644 --- a/Sources/BetterNotification/BetterNotification.swift +++ b/Sources/BetterNotification/BetterNotification.swift @@ -24,6 +24,8 @@ public extension BetterNotification { static var userInfoKey: String { notificationName.rawValue } } +// MARK: - + public extension Notification { /// Creates a `Notification` instance for the given `BetterNotification` value. /// @@ -41,8 +43,26 @@ public extension Notification { userInfo: [BN.userInfoKey: betterNotification] ) } + + /// Extracts a `BetterNotification` value of the specified type from this `Notification`'s `userInfo`. + /// + /// - Returns: The extracted `BetterNotification` value when found, otherwise `nil`. + func better() -> BN? { + guard let object = userInfo?[BN.userInfoKey] else { + NSLog("[\(BN.self)] Object missing from userInfo[\"\(BN.userInfoKey)\"]") + return nil + } + guard let betterNotification = object as? BN else { + NSLog("[\(BN.self)] Failed to cast \(object) to \(BN.self)") + return nil + } + + return betterNotification + } } +// MARK: - + public extension NotificationCenter { /// Returns an `AsyncSequence` of notifications of a specific `BetterNotification` type. /// @@ -54,18 +74,18 @@ public extension NotificationCenter { for betterType: BN.Type ) -> any AsyncSequence { notifications(named: BN.notificationName) - .compactMap { $0.userInfo?[BN.userInfoKey] as? BN } + .compactMap { $0.better() } } - /// Returns a Combine publisher that emits `BetterNotification` values of a specific type. + /// Returns a Combine publisher that emits `BetterNotification` values of the given type. /// /// - Parameter betterType: The `BetterNotification` type to observe. /// - Returns: A publisher emitting `BetterNotification` values. - func publisher( - for betterType: T.Type - ) -> AnyPublisher { + func publisher( + for betterType: BN.Type + ) -> AnyPublisher { publisher(for: betterType.notificationName) - .compactMap { $0.userInfo?[T.userInfoKey] as? T } + .compactMap { $0.better() } .eraseToAnyPublisher() } } diff --git a/Tests/BetterNotificationTests/BetterNotificationTests.swift b/Tests/BetterNotificationTests/BetterNotificationTests.swift index 1995410..7caff40 100644 --- a/Tests/BetterNotificationTests/BetterNotificationTests.swift +++ b/Tests/BetterNotificationTests/BetterNotificationTests.swift @@ -1,6 +1,138 @@ -import Testing @testable import BetterNotification +import Combine +import Foundation +import Testing -@Test func example() async throws { - // Write your test here and use APIs like `#expect(...)` to check expected conditions. +struct BetterNotificationTests { + @Test func notificationName() { + #expect(HitchhikersNotification.notificationName.rawValue == "BetterNotification:HitchhikersNotification") + } + + @Test func userInfoKey() { + #expect(HitchhikersNotification.userInfoKey == "BetterNotification:HitchhikersNotification") + } + + // MARK: - Notification extensions + + @Test func buildNotification() { + let hitchhikers = HitchhikersNotification(answer: 42) + let notification = Notification.better(hitchhikers) + #expect(notification.name == HitchhikersNotification.notificationName) + #expect(notification.userInfo?.count == 1) + let key = HitchhikersNotification.userInfoKey + let userInfoValue = notification.userInfo?[key] as? HitchhikersNotification + #expect(userInfoValue == hitchhikers) + } + + @Test func extractBetterPayload() { + let hitchhikers = HitchhikersNotification(answer: 42) + let notification = Notification.better(hitchhikers) + let extracted: HitchhikersNotification? = notification.better() + #expect(extracted == hitchhikers) + } + + @Test func extractBetterPayloadFailsOnWrongType() { + let imposter = Notification( + name: HitchhikersNotification.notificationName, + object: nil, + userInfo: [HitchhikersNotification.userInfoKey: "imposter"] + ) + let extracted: HitchhikersNotification? = imposter.better() + #expect(extracted == nil) + } + + @Test func extractBetterPayloadFailsOnMissingPayload() { + let wrongNotification = Notification(name: HitchhikersNotification.notificationName) + let extracted: HitchhikersNotification? = wrongNotification.better() + #expect(extracted == nil) + } + + // MARK: NotificationCenter extensions + + // It's important that the tests and the notification-observing tasks are not on the same actor, + // so we make the tests @MainActor and observe notifications on another actor. Otherwise it's + // a deadlock. + + @Test(.timeLimit(.minutes(1))) + @MainActor func notificationCenterAsyncSequence() async throws { + let center = NotificationCenter() + nonisolated(unsafe) var received = false + Task { + for await hitchhikers in center.notifications(for: HitchhikersNotification.self) { + #expect(hitchhikers.answer == 42) + received = true + break + } + } + await Task.yield() + + let hitchhikers = HitchhikersNotification(answer: 42) + center.post(.better(hitchhikers)) + while !received { await Task.yield() } + } + + @Test(.timeLimit(.minutes(1))) + @MainActor func notificationCenterAsyncSequenceIgnoresInvalidPayloads() async throws { + let center = NotificationCenter() + nonisolated(unsafe) var received = false + let task = Task { + for await _ in center.notifications(for: HitchhikersNotification.self) { + received = true + } + } + defer { task.cancel() } + await Task.yield() + + // Post an invalid one that should be ignored. + let imposter = Notification( + name: HitchhikersNotification.notificationName, + object: nil, + userInfo: [HitchhikersNotification.userInfoKey: "imposter"] + ) + center.post(imposter) + + try await Task.sleep(for: .milliseconds(10)) + #expect(!received) + } + + @Test(.timeLimit(.minutes(1))) + @MainActor func notificationCenterPublisher() async { + var cancellables = Set() + let center = NotificationCenter() + nonisolated(unsafe) var received = false + center.publisher(for: HitchhikersNotification.self) + .sink { hitchhikers in + #expect(hitchhikers.answer == 42) + received = true + }.store(in: &cancellables) + await Task.yield() + + let hitchhikers = HitchhikersNotification(answer: 42) + center.post(.better(hitchhikers)) + while !received { await Task.yield() } + } + + @Test(.timeLimit(.minutes(1))) + @MainActor func notificationCenterPublisherIgnoresInvalidPayloads() async throws { + var cancellables = Set() + let center = NotificationCenter() + nonisolated(unsafe) var received = false + center.publisher(for: HitchhikersNotification.self) + .sink { hitchhikers in + #expect(hitchhikers.answer == 42) + received = true + }.store(in: &cancellables) + await Task.yield() + + // Post an invalid one that should be ignored. + let imposter = Notification( + name: HitchhikersNotification.notificationName, + object: nil, + userInfo: [HitchhikersNotification.userInfoKey: "imposter"] + ) + center.post(imposter) + + try await Task.sleep(for: .milliseconds(10)) + #expect(!received) + } } diff --git a/Tests/BetterNotificationTests/HitchhikersNotification.swift b/Tests/BetterNotificationTests/HitchhikersNotification.swift new file mode 100644 index 0000000..ddc3a4e --- /dev/null +++ b/Tests/BetterNotificationTests/HitchhikersNotification.swift @@ -0,0 +1,5 @@ +import BetterNotification + +struct HitchhikersNotification: BetterNotification, Equatable { + let answer: Int +}