diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9b18231..38348d9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,6 +11,9 @@ All notable changes to this project will be documented in this file.
- [0.2.0](#020)
- [0.1.0](#010)
+## Development Branch
+
+- **New**: `availableElements` expectation
## 0.5.0
diff --git a/README.md b/README.md
index ce63627..d691cac 100644
--- a/README.md
+++ b/README.md
@@ -18,7 +18,7 @@ CombineExpectations aims at streamlining those tests. It defines an XCTestCase m
- [Usage]
- [Installation]
-- [Publisher Expectations]: [completion], [elements], [finished], [last], [next()], [next(count)], [prefix(maxLength)], [recording], [single]
+- [Publisher Expectations]: [availableElements], [completion], [elements], [finished], [last], [next()], [next(count)], [prefix(maxLength)], [recording], [single]
---
@@ -112,7 +112,6 @@ class PublisherTests: XCTestCase {
```
-
## Installation
Add a dependency for CombineExpectations to your [Swift Package](https://swift.org/package-manager/) test targets:
@@ -138,6 +137,7 @@ Add a dependency for CombineExpectations to your [Swift Package](https://swift.o
There are various publisher expectations. Each one waits for a specific publisher aspect:
+- [availableElements]: all published elements until timeout expiration
- [completion]: the publisher completion
- [elements]: all published elements until successful completion
- [finished]: the publisher successful completion
@@ -150,6 +150,30 @@ There are various publisher expectations. Each one waits for a specific publishe
---
+### availableElements
+
+:clock230: `recorder.availableElements` waits for the expectation to expire, or the recorded publisher to complete.
+
+:x: When waiting for this expectation, the publisher error is thrown if the publisher fails before the expectation has expired.
+
+:white_check_mark: Otherwise, an array of all elements published before the expectation has expired is returned.
+
+:arrow_right: Related expectations: [elements], [prefix(maxLength)].
+
+Unlike other expectations, `availableElements` does not make a test fail on timeout expiration. It just returns the elements published so far.
+
+Example:
+
+```swift
+// SUCCESS: no timeout, no error
+func testTimerPublishesIncreasingDates() throws {
+ let publisher = Timer.publish(every: 0.01, on: .main, in: .common).autoconnect()
+ let recorder = publisher.record()
+ let dates = try wait(for: recorder.availableElements, timeout: ...)
+ XCTAssertEqual(dates.sorted(), dates)
+}
+```
+
### completion
:clock230: `recorder.completion` waits for the recorded publisher to complete.
@@ -210,7 +234,7 @@ func testCompletionTimeout() throws {
:white_check_mark: Otherwise, an array of published elements is returned.
-:arrow_right: Related expectations: [last], [prefix(maxLength)], [recording], [single].
+:arrow_right: Related expectations: [availableElements], [last], [prefix(maxLength)], [recording], [single].
Example:
@@ -588,7 +612,7 @@ func testNextCountNotEnoughElementsError() throws {
:white_check_mark: Otherwise, an array of received elements is returned, containing at most `maxLength` elements, or less if the publisher completes early.
-:arrow_right: Related expectations: [elements], [next(count)].
+:arrow_right: Related expectations: [availableElements], [elements], [next(count)].
Example:
@@ -817,3 +841,4 @@ func testSingleNotEnoughElementsError() throws {
[elements]: #elements
[last]: #last
[single]: #single
+[availableElements]: #availableElements
diff --git a/Sources/CombineExpectations/PublisherExpectation.swift b/Sources/CombineExpectations/PublisherExpectation.swift
index 51bcf79..fbf1bab 100644
--- a/Sources/CombineExpectations/PublisherExpectation.swift
+++ b/Sources/CombineExpectations/PublisherExpectation.swift
@@ -5,10 +5,21 @@ public enum PublisherExpectations { }
/// The base protocol for PublisherExpectation. It is an implementation detail
/// that you are not supposed to use, as shown by the underscore prefix.
+///
+/// :nodoc:
public protocol _PublisherExpectationBase {
/// Sets up an XCTestExpectation. This method is an implementation detail
/// that you are not supposed to use, as shown by the underscore prefix.
func _setup(_ expectation: XCTestExpectation)
+
+ /// Returns an object that waits for the expectation. If nil, expectation
+ /// is waited by the XCTestCase.
+ func _makeWaiter() -> XCTWaiter?
+}
+
+extension _PublisherExpectationBase {
+ /// :nodoc:
+ public func _makeWaiter() -> XCTWaiter? { nil }
}
/// The protocol for publisher expectations.
@@ -93,7 +104,11 @@ extension XCTestCase {
{
let expectation = self.expectation(description: description)
publisherExpectation._setup(expectation)
- wait(for: [expectation], timeout: timeout)
+ if let waiter = publisherExpectation._makeWaiter() {
+ waiter.wait(for: [expectation], timeout: timeout)
+ } else {
+ wait(for: [expectation], timeout: timeout)
+ }
return try publisherExpectation.get()
}
}
diff --git a/Sources/CombineExpectations/PublisherExpectations/AvailableElements.swift b/Sources/CombineExpectations/PublisherExpectations/AvailableElements.swift
new file mode 100644
index 0000000..3de5bfe
--- /dev/null
+++ b/Sources/CombineExpectations/PublisherExpectations/AvailableElements.swift
@@ -0,0 +1,59 @@
+import XCTest
+
+extension PublisherExpectations {
+ /// A publisher expectation which waits for the timeout to expire, or
+ /// the recorded publisher to complete.
+ ///
+ /// When waiting for this expectation, the publisher error is thrown if
+ /// the publisher fails before the expectation has expired.
+ ///
+ /// Otherwise, an array of all elements published before the expectation
+ /// has expired is returned.
+ ///
+ /// Unlike other expectations, `AvailableElements` does not make a test fail
+ /// on timeout expiration. It just returns the elements published so far.
+ ///
+ /// For example:
+ ///
+ /// // SUCCESS: no timeout, no error
+ /// func testTimerPublishesIncreasingDates() throws {
+ /// let publisher = Timer.publish(every: 0.01, on: .main, in: .common).autoconnect()
+ /// let recorder = publisher.record()
+ /// let dates = try wait(for: recorder.availableElements, timeout: ...)
+ /// XCTAssertEqual(dates.sorted(), dates)
+ /// }
+ public struct AvailableElements: PublisherExpectation {
+ let recorder: Recorder
+
+ public func _makeWaiter() -> XCTWaiter? { Waiter() }
+
+ public func _setup(_ expectation: XCTestExpectation) {
+ recorder.fulfillOnCompletion(expectation)
+ }
+
+ /// Returns all elements published so far, or throws an error if the
+ /// publisher has failed.
+ public func get() throws -> [Input] {
+ try recorder.value { (elements, completion, remainingElements, consume) in
+ if case let .failure(error) = completion {
+ throw error
+ }
+ consume(remainingElements.count)
+ return elements
+ }
+ }
+
+ /// A waiter that waits but never fails
+ private class Waiter: XCTWaiter, XCTWaiterDelegate {
+ init() {
+ super.init(delegate: nil)
+ delegate = self
+ }
+
+ func waiter(_ waiter: XCTWaiter, didTimeoutWithUnfulfilledExpectations unfulfilledExpectations: [XCTestExpectation]) { }
+ func waiter(_ waiter: XCTWaiter, fulfillmentDidViolateOrderingConstraintsFor expectation: XCTestExpectation, requiredExpectation: XCTestExpectation) { }
+ func waiter(_ waiter: XCTWaiter, didFulfillInvertedExpectation expectation: XCTestExpectation) { }
+ func nestedWaiter(_ waiter: XCTWaiter, wasInterruptedByTimedOutWaiter outerWaiter: XCTWaiter) { }
+ }
+ }
+}
diff --git a/Sources/CombineExpectations/Recorder.swift b/Sources/CombineExpectations/Recorder.swift
index 60d79ff..8af2e4f 100644
--- a/Sources/CombineExpectations/Recorder.swift
+++ b/Sources/CombineExpectations/Recorder.swift
@@ -274,20 +274,45 @@ public class Recorder: Subscriber {
// MARK: - Publisher Expectations
extension PublisherExpectations {
- /// The type of the publisher expectation returned by Recorder.completion
+ /// The type of the publisher expectation returned by `Recorder.completion`.
public typealias Completion = Map, Subscribers.Completion>
- /// The type of the publisher expectation returned by Recorder.elements
+ /// The type of the publisher expectation returned by `Recorder.elements`.
public typealias Elements = Map, [Input]>
- /// The type of the publisher expectation returned by Recorder.last
+ /// The type of the publisher expectation returned by `Recorder.last`.
public typealias Last = Map, Input?>
- /// The type of the publisher expectation returned by Recorder.single
+ /// The type of the publisher expectation returned by `Recorder.single`.
public typealias Single = Map, Input>
}
extension Recorder {
+ /// Returns a publisher expectation which waits for the timeout to expire,
+ /// or the recorded publisher to complete.
+ ///
+ /// When waiting for this expectation, the publisher error is thrown if
+ /// the publisher fails before the expectation has expired.
+ ///
+ /// Otherwise, an array of all elements published before the expectation
+ /// has expired is returned.
+ ///
+ /// Unlike other expectations, `availableElements` does not make a test fail
+ /// on timeout expiration. It just returns the elements published so far.
+ ///
+ /// For example:
+ ///
+ /// // SUCCESS: no timeout, no error
+ /// func testTimerPublishesIncreasingDates() throws {
+ /// let publisher = Timer.publish(every: 0.01, on: .main, in: .common).autoconnect()
+ /// let recorder = publisher.record()
+ /// let dates = try wait(for: recorder.availableElements, timeout: ...)
+ /// XCTAssertEqual(dates.sorted(), dates)
+ /// }
+ public var availableElements: PublisherExpectations.AvailableElements {
+ PublisherExpectations.AvailableElements(recorder: self)
+ }
+
/// Returns a publisher expectation which waits for the recorded publisher
/// to complete.
///
diff --git a/Tests/CombineExpectationsTests/RecorderTests.swift b/Tests/CombineExpectationsTests/RecorderTests.swift
index fba7109..fdca65c 100644
--- a/Tests/CombineExpectationsTests/RecorderTests.swift
+++ b/Tests/CombineExpectationsTests/RecorderTests.swift
@@ -16,6 +16,59 @@ class RecorderTests: XCTestCase {
XCTAssertTrue(subscribed)
}
+ // MARK: - availableElements
+
+ func testAvailableElementsSync() throws {
+ do {
+ let publisher = [1, 2, 3].publisher
+ let recorder = publisher.record()
+ let availableElements = try recorder.availableElements.get()
+ XCTAssertEqual(availableElements, [1, 2, 3])
+ }
+ do {
+ let publisher = PassthroughSubject()
+ let recorder = publisher.record()
+ publisher.send(1)
+ publisher.send(2)
+ publisher.send(3)
+ let availableElements = try recorder.availableElements.get()
+ XCTAssertEqual(availableElements, [1, 2, 3])
+ }
+ do {
+ let publisher = PassthroughSubject()
+ let recorder = publisher.record()
+ publisher.send(1)
+ publisher.send(completion: .failure(TestError()))
+ _ = try recorder.availableElements.get()
+ XCTFail("Expected TestError")
+ } catch is TestError { }
+ }
+
+ func testAvailableElementsAsync() throws {
+ do {
+ let publisher = Timer.publish(every: 0.01, on: .main, in: .common).autoconnect()
+ let recorder = publisher.record()
+ let dates = try wait(for: recorder.availableElements, timeout: 0.1)
+ XCTAssertTrue(dates.count > 2)
+ XCTAssertEqual(dates.sorted(), dates)
+ }
+ }
+
+ func testAvailableElementsStopsOnPublisherCompletion() throws {
+ do {
+ let publisher = PassthroughSubject()
+ let recorder = publisher.record()
+
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
+ publisher.send(completion: .finished)
+ }
+ let start = Date()
+ _ = try wait(for: recorder.availableElements, timeout: 2)
+ let duration = Date().timeIntervalSince(start)
+ XCTAssertLessThan(duration, 1)
+ }
+ }
+
// MARK: - elementsAndCompletion
func testElementsAndCompletionSync() throws {