Add tests

This commit is contained in:
Sami Samhuri 2025-04-26 20:10:17 -07:00
parent ba2c16d298
commit 59effef18f
No known key found for this signature in database
4 changed files with 169 additions and 9 deletions

View file

@ -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

View file

@ -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,7 +43,25 @@ 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: BetterNotification>() -> 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<BN, Never> {
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<T: BetterNotification>(
for betterType: T.Type
) -> AnyPublisher<T, Never> {
func publisher<BN: BetterNotification>(
for betterType: BN.Type
) -> AnyPublisher<BN, Never> {
publisher(for: betterType.notificationName)
.compactMap { $0.userInfo?[T.userInfoKey] as? T }
.compactMap { $0.better() }
.eraseToAnyPublisher()
}
}

View file

@ -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<AnyCancellable>()
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<AnyCancellable>()
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)
}
}

View file

@ -0,0 +1,5 @@
import BetterNotification
struct HitchhikersNotification: BetterNotification, Equatable {
let answer: Int
}