diff --git a/Directory.Packages.props b/Directory.Packages.props
index 05b5418..d28212f 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -14,6 +14,7 @@
+
diff --git a/tests/FSharp.Control.R3.Tests/AsyncObservable/AggregationCategoryTests.fs b/tests/FSharp.Control.R3.Tests/AsyncObservable/AggregationCategoryTests.fs
new file mode 100644
index 0000000..6125b69
--- /dev/null
+++ b/tests/FSharp.Control.R3.Tests/AsyncObservable/AggregationCategoryTests.fs
@@ -0,0 +1,28 @@
+namespace FSharp.Control.R3.Tests.AsyncObservable
+
+open System.Threading.Tasks
+open global.FSharp.Control
+open Microsoft.VisualStudio.TestTools.UnitTesting
+open global.R3
+open FSharp.Control.R3
+open FSharp.Control.R3.Async
+open FSharp.Control.R3.Tests
+
+[]
+type AggregationCategoryTests () =
+ []
+ member _.``aggregate should match direct AggregateAsync`` () : Task = task {
+ let source = TestHelpers.createObservable [| 1; 2; 3 |]
+ let expected =
+ ObservableExtensions.AggregateAsync (source, 0, (fun acc x -> acc + x), TestHelpers.cancellationToken)
+ let! expectedValue = expected
+ let! actual = source |> Observable.aggregate 0 (fun acc x -> acc + x)
+ Assert.AreEqual (expectedValue, actual, "Async aggregate must match direct AggregateAsync result.")
+ }
+
+ []
+ member _.``length should count values`` () : Task = task {
+ let source = TestHelpers.createObservable [| 1; 2; 3; 4 |]
+ let! actual = source |> Observable.length
+ Assert.AreEqual (4, actual, "length must return emitted value count.")
+ }
diff --git a/tests/FSharp.Control.R3.Tests/AsyncObservable/AllSelectionCategoryTests.fs b/tests/FSharp.Control.R3.Tests/AsyncObservable/AllSelectionCategoryTests.fs
new file mode 100644
index 0000000..6b463b2
--- /dev/null
+++ b/tests/FSharp.Control.R3.Tests/AsyncObservable/AllSelectionCategoryTests.fs
@@ -0,0 +1,28 @@
+namespace FSharp.Control.R3.Tests.AsyncObservable
+
+open System.Threading.Tasks
+open global.FSharp.Control
+open Microsoft.VisualStudio.TestTools.UnitTesting
+open global.R3
+open FSharp.Control.R3
+open FSharp.Control.R3.Async
+open FSharp.Control.R3.Tests
+
+[]
+type AllSelectionCategoryTests () =
+ []
+ member _.``all should match direct AllAsync`` () : Task = task {
+ let source = TestHelpers.createObservable [| 2; 4; 6 |]
+ let expected =
+ ObservableExtensions.AllAsync (source, (fun x -> x % 2 = 0), TestHelpers.cancellationToken)
+ let! expectedValue = expected
+ let! actual = source |> Observable.all (fun x -> x % 2 = 0)
+ Assert.AreEqual (expectedValue, actual, "Async all must match direct AllAsync result.")
+ }
+
+ []
+ member _.``existsAsync should detect available values`` () : Task = task {
+ let source = TestHelpers.createObservable [| 1 |]
+ let! actual = source |> Observable.existsAsync
+ Assert.IsTrue (actual, "existsAsync must return true for non-empty sequence.")
+ }
diff --git a/tests/FSharp.Control.R3.Tests/AsyncObservable/ConversionCategoryTests.fs b/tests/FSharp.Control.R3.Tests/AsyncObservable/ConversionCategoryTests.fs
new file mode 100644
index 0000000..07841b7
--- /dev/null
+++ b/tests/FSharp.Control.R3.Tests/AsyncObservable/ConversionCategoryTests.fs
@@ -0,0 +1,25 @@
+namespace FSharp.Control.R3.Tests.AsyncObservable
+
+open System.Threading.Tasks
+open global.FSharp.Control
+open Microsoft.VisualStudio.TestTools.UnitTesting
+open global.R3
+open FSharp.Control.R3
+open FSharp.Control.R3.Async
+open FSharp.Control.R3.Tests
+
+[]
+type ConversionCategoryTests () =
+ []
+ member _.``toArray should return all values`` () : Task = task {
+ let source = TestHelpers.createObservable [| 1; 2; 3 |]
+ let! actual = source |> Observable.toArray
+ CollectionAssert.AreEqual ([| 1; 2; 3 |], actual, "toArray must return all source values.")
+ }
+
+ []
+ member _.``toList should return all values as list`` () : Task = task {
+ let source = TestHelpers.createObservable [| 1; 2; 3 |]
+ let! actual = source |> Observable.toList
+ CollectionAssert.AreEqual ([| 1; 2; 3 |], actual |> List.toArray, "toList must return all source values in order.")
+ }
diff --git a/tests/FSharp.Control.R3.Tests/AsyncObservable/SingleElementCategoryTests.fs b/tests/FSharp.Control.R3.Tests/AsyncObservable/SingleElementCategoryTests.fs
new file mode 100644
index 0000000..c153d4c
--- /dev/null
+++ b/tests/FSharp.Control.R3.Tests/AsyncObservable/SingleElementCategoryTests.fs
@@ -0,0 +1,18 @@
+namespace FSharp.Control.R3.Tests.AsyncObservable
+
+open System.Threading.Tasks
+open global.FSharp.Control
+open Microsoft.VisualStudio.TestTools.UnitTesting
+open global.R3
+open FSharp.Control.R3
+open FSharp.Control.R3.Async
+open FSharp.Control.R3.Tests
+
+[]
+type SingleElementCategoryTests () =
+ []
+ member _.``firstAsync should return first value`` () : Task = task {
+ let source = TestHelpers.createObservable [| 9; 8 |]
+ let! actual = source |> Observable.firstAsync
+ Assert.AreEqual (9, actual, "firstAsync must return the first emitted value.")
+ }
diff --git a/tests/FSharp.Control.R3.Tests/AsyncObservable/TransformationCategoryTests.fs b/tests/FSharp.Control.R3.Tests/AsyncObservable/TransformationCategoryTests.fs
new file mode 100644
index 0000000..afd9560
--- /dev/null
+++ b/tests/FSharp.Control.R3.Tests/AsyncObservable/TransformationCategoryTests.fs
@@ -0,0 +1,61 @@
+namespace FSharp.Control.R3.Tests.AsyncObservable
+
+open System.Threading.Tasks
+open Microsoft.VisualStudio.TestTools.UnitTesting
+open R3
+open FSharp.Control.R3
+open FSharp.Control.R3.Async
+open FSharp.Control.R3.Tests
+
+[]
+type TransformationCategoryTests () =
+ []
+ member _.``mapAsync should match direct SelectAwait`` () : Task = task {
+ let options = ProcessingOptions.Default
+ let source = TestHelpers.createObservable [| 1; 2; 3 |]
+ let! expected =
+ ObservableExtensions.SelectAwait (
+ source,
+ (fun x (_ : System.Threading.CancellationToken) -> ValueTask.FromResult (x + 1)),
+ options.AwaitOperation,
+ options.ConfigureAwait,
+ options.CancelOnCompleted,
+ options.MaxConcurrent
+ )
+ |> TestHelpers.toArrayTask
+
+ let! actual =
+ source
+ |> Observable.mapAsync options (fun x -> async { return x + 1 })
+ |> TestHelpers.toArrayTask
+
+ CollectionAssert.AreEqual (expected, actual, "Async mapAsync must match direct SelectAwait behavior.")
+ }
+
+ []
+ member _.``iter should invoke action for each value`` () : Task = task {
+ let source = TestHelpers.createObservable [| 1; 2; 3 |]
+ let mutable sum = 0
+ do! source |> Observable.iter (fun x -> sum <- sum + x)
+ Assert.AreEqual (6, sum, "iter must invoke action for each emitted value.")
+ }
+
+ []
+ member _.``iterAsync should await async action for each value`` () : Task = task {
+ let options = ProcessingOptions.Default
+ let source = TestHelpers.createObservable [| 1; 2; 3 |]
+ let mutable sum = 0
+ do!
+ source
+ |> Observable.iterAsync options (fun x -> async { sum <- sum + x })
+ Assert.AreEqual (6, sum, "iterAsync must apply asynchronous action to every value.")
+ }
+
+ []
+ member _.``ofAsync should emit computation result`` () : Task = task {
+ let! actual =
+ Observable.ofAsync (async { return 7 })
+ |> TestHelpers.toArrayTask
+
+ CollectionAssert.AreEqual ([| 7 |], actual, "ofAsync must emit the async computation result.")
+ }
diff --git a/tests/FSharp.Control.R3.Tests/BuilderTests.fs b/tests/FSharp.Control.R3.Tests/BuilderTests.fs
deleted file mode 100644
index 68f41c0..0000000
--- a/tests/FSharp.Control.R3.Tests/BuilderTests.fs
+++ /dev/null
@@ -1,53 +0,0 @@
-namespace FSharp.Control.R3.Tests
-
-open System
-open System.Threading.Tasks
-open FSharp.Control.R3.Async
-open FSharp.Control.R3.Observable.Builders
-open Microsoft.VisualStudio.TestTools.UnitTesting
-open Swensen.Unquote
-
-[]
-type BuilderTests () =
-
- []
- member _.``Test builder rxquery`` () =
-
- let mutable hasvisited = false
- use r3Bus = new R3.Subject ()
-
- let interesting = rxquery {
- for i in r3Bus do
- where (i % 2 = 0)
- select i
-
- // Same as:
- // if i % 2 = 0 then
- // yield i
-
- }
-
- // No-one listens yet (vs R3.ReplaySubject
- r3Bus.OnNext 2
-
- use subscription =
- R3.ObservableExtensions.SubscribeAwait (
- interesting,
- fun i cancellationToken ->
- task {
- // Listen events
-
- hasvisited <- true
-
- Assert.AreEqual (4, i)
-
- return ()
- }
- |> System.Threading.Tasks.ValueTask
- )
-
- // Publish some events, "4" should be heard
- [ 3..5 ] |> List.iter r3Bus.OnNext
- // Note: Query will not be awaited, that's why delay.
- System.Threading.Thread.Sleep 300
- Assert.AreEqual (true, hasvisited)
diff --git a/tests/FSharp.Control.R3.Tests/FSharp.Control.R3.Tests.fsproj b/tests/FSharp.Control.R3.Tests/FSharp.Control.R3.Tests.fsproj
index 9c07d08..bc5ecbd 100644
--- a/tests/FSharp.Control.R3.Tests/FSharp.Control.R3.Tests.fsproj
+++ b/tests/FSharp.Control.R3.Tests/FSharp.Control.R3.Tests.fsproj
@@ -5,12 +5,16 @@
-
-
+
+
+
+
+
+
diff --git a/tests/FSharp.Control.R3.Tests/Observable/AggregationCategoryTests.fs b/tests/FSharp.Control.R3.Tests/Observable/AggregationCategoryTests.fs
new file mode 100644
index 0000000..f0a2132
--- /dev/null
+++ b/tests/FSharp.Control.R3.Tests/Observable/AggregationCategoryTests.fs
@@ -0,0 +1,44 @@
+namespace FSharp.Control.R3.Tests.Observable
+
+open System
+open System.Threading.Tasks
+open Microsoft.VisualStudio.TestTools.UnitTesting
+open R3
+open FSharp.Control.R3
+open FSharp.Control.R3.Tests
+
+[]
+type AggregationCategoryTests () =
+ []
+ member _.``concat should append second sequence`` () : Task = task {
+ let first = TestHelpers.createObservable [| 1; 2 |]
+ let second = TestHelpers.createObservable [| 3; 4 |]
+ let! actual = Observable.concat first second |> TestHelpers.toArrayTask
+ CollectionAssert.AreEqual ([| 1; 2; 3; 4 |], actual, "concat must append second observable after first completes.")
+ }
+
+ []
+ member _.``merge should match direct R3 merge`` () : Task = task {
+ let source1 = TestHelpers.createObservable [| 1; 2 |]
+ let source2 = TestHelpers.createObservable [| 10; 20 |]
+ let! expected =
+ ObservableExtensions.Merge (source1, source2)
+ |> TestHelpers.toArrayTask
+ let! actual =
+ Observable.merge (source1, source2)
+ |> TestHelpers.toArrayTask
+ CollectionAssert.AreEqual (expected, actual, "merge wrapper must match direct R3 merge output.")
+ }
+
+ []
+ member _.``catch should continue with fallback observable`` () : Task = task {
+ use subject = new Subject ()
+ let recovered =
+ subject
+ |> Observable.catch (fun (_ : exn) -> Observable.singleton 99)
+ let pending = TestHelpers.toArrayTask recovered
+ subject.OnNext 1
+ subject.OnCompleted (Result.Failure (Exception ("boom")))
+ let! actual = pending
+ CollectionAssert.AreEqual ([| 1; 99 |], actual, "catch must append fallback values after source error.")
+ }
diff --git a/tests/FSharp.Control.R3.Tests/Observable/AllSelectionCategoryTests.fs b/tests/FSharp.Control.R3.Tests/Observable/AllSelectionCategoryTests.fs
new file mode 100644
index 0000000..d8c045d
--- /dev/null
+++ b/tests/FSharp.Control.R3.Tests/Observable/AllSelectionCategoryTests.fs
@@ -0,0 +1,93 @@
+namespace FSharp.Control.R3.Tests.Observable
+
+open System
+open System.Threading.Tasks
+open Microsoft.VisualStudio.TestTools.UnitTesting
+open R3
+open FSharp.Control.R3
+open FSharp.Control.R3.Tests
+
+[]
+type AllSelectionCategoryTests () =
+ []
+ member _.``where should keep matching values`` () : Task = task {
+ let source = TestHelpers.createObservable [| 1; 2; 3; 4 |]
+ let! actual =
+ source
+ |> Observable.where (fun x -> x > 2)
+ |> TestHelpers.toArrayTask
+ CollectionAssert.AreEqual ([| 3; 4 |], actual, "where must keep values matching the predicate.")
+ }
+
+ []
+ member _.``filter should keep matching values`` () : Task = task {
+ let source = TestHelpers.createObservable [| 1; 2; 3; 4 |]
+ let! actual =
+ source
+ |> Observable.filter (fun x -> x % 2 = 0)
+ |> TestHelpers.toArrayTask
+ CollectionAssert.AreEqual ([| 2; 4 |], actual, "filter must keep only matching elements.")
+ }
+
+ []
+ member _.``choose should keep only Some values`` () : Task = task {
+ let source = TestHelpers.createObservable [| 1; 2; 3; 4 |]
+ let! actual =
+ source
+ |> FSharp.Control.R3.Observable.OptionExtensions.Observable.choose (fun x -> if x % 2 = 0 then Some (x * 10) else None)
+ |> TestHelpers.toArrayTask
+ CollectionAssert.AreEqual ([| 20; 40 |], actual, "choose must emit only mapped values from Some results.")
+ }
+
+ []
+ member _.``distinct should remove duplicate values`` () : Task = task {
+ let source = TestHelpers.createObservable [| 1; 1; 2; 2; 3 |]
+ let! actual = source |> Observable.distinct |> TestHelpers.toArrayTask
+ CollectionAssert.AreEqual ([| 1; 2; 3 |], actual, "distinct must emit unique values in encounter order.")
+ }
+
+ []
+ member _.``chunkBySize should split by fixed size`` () : Task = task {
+ let source = TestHelpers.createObservable [| 1; 2; 3; 4; 5 |]
+ let! actual =
+ source
+ |> Observable.chunkBySize 2
+ |> TestHelpers.toArrayTask
+ let chunks = actual |> Array.map Seq.toArray
+ Assert.AreEqual (3, chunks.Length, "chunkBySize should produce expected number of chunks.")
+ CollectionAssert.AreEqual ([| 1; 2 |], chunks[0], "First chunk should contain first two items.")
+ CollectionAssert.AreEqual ([| 3; 4 |], chunks[1], "Second chunk should contain next two items.")
+ CollectionAssert.AreEqual ([| 5 |], chunks[2], "Third chunk should contain the remaining item.")
+ }
+
+ []
+ member _.``chunkBy ChunkCount should match chunkBySize`` () : Task = task {
+ let source = TestHelpers.createObservable [| 1; 2; 3; 4 |]
+ let! expectedChunks =
+ source
+ |> Observable.chunkBySize 2
+ |> TestHelpers.toArrayTask
+ let! actualChunks =
+ source
+ |> Observable.chunkBy (ChunkCount 2)
+ |> TestHelpers.toArrayTask
+ let expected = expectedChunks |> Array.map Seq.toArray
+ let actual = actualChunks |> Array.map Seq.toArray
+ Assert.AreEqual (expected.Length, actual.Length, "chunkBy ChunkCount must produce same chunk count as chunkBySize.")
+ CollectionAssert.AreEqual (expected[0], actual[0], "First chunk must match chunkBySize output.")
+ CollectionAssert.AreEqual (expected[1], actual[1], "Second chunk must match chunkBySize output.")
+ }
+
+ []
+ member _.``skip should skip leading values`` () : Task = task {
+ let source = TestHelpers.createObservable [| 1; 2; 3; 4 |]
+ let! actual = source |> Observable.skip 2 |> TestHelpers.toArrayTask
+ CollectionAssert.AreEqual ([| 3; 4 |], actual, "skip must ignore the configured number of leading values.")
+ }
+
+ []
+ member _.``take should keep leading values`` () : Task = task {
+ let source = TestHelpers.createObservable [| 1; 2; 3; 4 |]
+ let! actual = source |> Observable.take 2 |> TestHelpers.toArrayTask
+ CollectionAssert.AreEqual ([| 1; 2 |], actual, "take must emit only the configured number of leading values.")
+ }
diff --git a/tests/FSharp.Control.R3.Tests/Observable/ConversionCategoryTests.fs b/tests/FSharp.Control.R3.Tests/Observable/ConversionCategoryTests.fs
new file mode 100644
index 0000000..a5f755d
--- /dev/null
+++ b/tests/FSharp.Control.R3.Tests/Observable/ConversionCategoryTests.fs
@@ -0,0 +1,38 @@
+namespace FSharp.Control.R3.Tests.Observable
+
+open System
+open System.Threading.Tasks
+open Microsoft.VisualStudio.TestTools.UnitTesting
+open R3
+open FSharp.Control.R3
+open FSharp.Control.R3.Tests
+
+[]
+type ConversionCategoryTests () =
+ []
+ member _.``asObservable should keep source values`` () : Task = task {
+ let source = TestHelpers.createObservable [| 1; 2; 3 |]
+ let! expected = source |> TestHelpers.toArrayTask
+ let! actual = source |> Observable.asObservable |> TestHelpers.toArrayTask
+ CollectionAssert.AreEqual (expected, actual, "asObservable must preserve source values.")
+ }
+
+ []
+ member _.``cast should convert values to target type`` () : Task = task {
+ let source = TestHelpers.createObservable [| box 1; box 2 |]
+ let! actual =
+ source
+ |> Observable.cast
+ |> TestHelpers.toArrayTask
+ CollectionAssert.AreEqual ([| 1; 2 |], actual, "cast must convert boxed integers.")
+ }
+
+ []
+ member _.``ofType should keep only requested runtime type`` () : Task = task {
+ let source = TestHelpers.createObservable [| box 1; box "x"; box 2 |]
+ let! actual =
+ source
+ |> Observable.ofType
+ |> TestHelpers.toArrayTask
+ CollectionAssert.AreEqual ([| 1; 2 |], actual, "ofType must emit only values of requested type.")
+ }
diff --git a/tests/FSharp.Control.R3.Tests/Observable/SingleElementCategoryTests.fs b/tests/FSharp.Control.R3.Tests/Observable/SingleElementCategoryTests.fs
new file mode 100644
index 0000000..d5192d4
--- /dev/null
+++ b/tests/FSharp.Control.R3.Tests/Observable/SingleElementCategoryTests.fs
@@ -0,0 +1,22 @@
+namespace FSharp.Control.R3.Tests.Observable
+
+open System
+open System.Threading.Tasks
+open Microsoft.VisualStudio.TestTools.UnitTesting
+open R3
+open FSharp.Control.R3
+open FSharp.Control.R3.Tests
+
+[]
+type SingleElementCategoryTests () =
+ []
+ member _.``singleton should emit one value`` () : Task = task {
+ let! actual = Observable.singleton 42 |> TestHelpers.toArrayTask
+ CollectionAssert.AreEqual ([| 42 |], actual, "singleton must emit exactly one value.")
+ }
+
+ []
+ member _.``empty should emit no values`` () : Task = task {
+ let! actual = Observable.empty () |> TestHelpers.toArrayTask
+ Assert.AreEqual (0, actual.Length, "empty must complete without values.")
+ }
diff --git a/tests/FSharp.Control.R3.Tests/Observable/TransformationCategoryTests.fs b/tests/FSharp.Control.R3.Tests/Observable/TransformationCategoryTests.fs
new file mode 100644
index 0000000..3de64c1
--- /dev/null
+++ b/tests/FSharp.Control.R3.Tests/Observable/TransformationCategoryTests.fs
@@ -0,0 +1,46 @@
+namespace FSharp.Control.R3.Tests.Observable
+
+open System
+open System.Threading.Tasks
+open Microsoft.VisualStudio.TestTools.UnitTesting
+open R3
+open FSharp.Control.R3
+open FSharp.Control.R3.Tests
+
+[]
+type TransformationCategoryTests () =
+ []
+ member _.``map should transform each value`` () : Task = task {
+ let source = TestHelpers.createObservable [| 1; 2; 3 |]
+ let! actual =
+ source
+ |> Observable.map (fun x -> x * 10)
+ |> TestHelpers.toArrayTask
+ CollectionAssert.AreEqual ([| 10; 20; 30 |], actual, "map must transform each emitted value.")
+ }
+
+ []
+ member _.``bind should match SelectMany behavior`` () : Task = task {
+ let source = TestHelpers.createObservable [| 1; 2 |]
+ let! expected =
+ ObservableExtensions.SelectMany (source, fun x -> Observable.Return (x * 10))
+ |> TestHelpers.toArrayTask
+ let! actual =
+ source
+ |> Observable.bind (fun x -> Observable.singleton (x * 10))
+ |> TestHelpers.toArrayTask
+ CollectionAssert.AreEqual (expected, actual, "bind must match SelectMany behavior.")
+ }
+
+ []
+ member _.``mapi should pass index as first argument`` () : Task = task {
+ let source = TestHelpers.createObservable [| 4; 5; 6 |]
+ let! expected =
+ ObservableExtensions.Select (source, fun value index -> (index * 10) + value)
+ |> TestHelpers.toArrayTask
+ let! actual =
+ source
+ |> Observable.mapi (fun index value -> (index * 10) + value)
+ |> TestHelpers.toArrayTask
+ CollectionAssert.AreEqual (expected, actual, "mapi must pass index first and value second.")
+ }
diff --git a/tests/FSharp.Control.R3.Tests/ObservableTests.fs b/tests/FSharp.Control.R3.Tests/ObservableTests.fs
deleted file mode 100644
index e45d2ce..0000000
--- a/tests/FSharp.Control.R3.Tests/ObservableTests.fs
+++ /dev/null
@@ -1,65 +0,0 @@
-namespace FSharp.Control.R3.Tests
-
-open System
-open System.Threading.Tasks
-open FSharp.Control.R3.Async
-open Microsoft.VisualStudio.TestTools.UnitTesting
-open Swensen.Unquote
-
-[]
-type ObservableTests () =
-
- []
- member _.``Test length`` () : Task =
-
- async {
- use r3Bus = new R3.Subject ()
-
- r3Bus.OnNext 1
-
- let lengthObs = Observable.length r3Bus
-
- r3Bus.OnNext 2
- r3Bus.OnNext 3
- r3Bus.OnCompleted (R3.Result.Success)
-
- let! res = lengthObs
-
- Assert.AreEqual (0, res)
-
- }
- |> Async.StartImmediateAsTask
- :> Task
-
-
- []
- member _.``Test filter`` () =
-
- let mutable hasvisited = false
- use r3Bus = new R3.Subject ()
- let interesting =
- r3Bus
- |> FSharp.Control.R3.Observable.filter (fun x -> x % 2 = 0)
-
- // No-one listens yet (vs R3.ReplaySubject
- r3Bus.OnNext 2
-
- use subscription =
- R3.ObservableExtensions.SubscribeAwait (
- interesting,
- fun i cancellationToken ->
- task {
- // Listen events
-
- hasvisited <- true
-
- Assert.AreEqual (4, i)
-
- return ()
- }
- |> System.Threading.Tasks.ValueTask
- )
-
- // Publish some events, "4" should be heard
- [ 3..5 ] |> List.iter r3Bus.OnNext
- Assert.AreEqual (true, hasvisited)
diff --git a/tests/FSharp.Control.R3.Tests/ProcessingOptionsTests.fs b/tests/FSharp.Control.R3.Tests/ProcessingOptionsTests.fs
new file mode 100644
index 0000000..ca94fb7
--- /dev/null
+++ b/tests/FSharp.Control.R3.Tests/ProcessingOptionsTests.fs
@@ -0,0 +1,57 @@
+namespace FSharp.Control.R3.Tests
+
+open System
+open System.Threading
+open System.Threading.Tasks
+open Microsoft.VisualStudio.TestTools.UnitTesting
+open FSharp.Control.R3
+open R3
+
+[]
+type ProcessingOptionsTests () =
+ []
+ member _.``TimeSpan should use default time provider`` () =
+ match ChunkConfiguration.TimeSpan (TimeSpan.FromMilliseconds 10.) with
+ | ChunkTimeSpan (windowTime, provider) ->
+ Assert.AreEqual (TimeSpan.FromMilliseconds 10., windowTime, "TimeSpan helper must keep provided window time.")
+ Assert.AreSame (ObservableSystem.DefaultTimeProvider, provider, "TimeSpan helper must use default time provider.")
+ | _ -> Assert.Fail ("TimeSpan helper must create ChunkTimeSpan configuration.")
+
+ []
+ member _.``TimeSpanCount should use default time provider`` () =
+ match ChunkConfiguration.TimeSpanCount (TimeSpan.FromMilliseconds 10.) 3 with
+ | ChunkTimeSpanCount (windowTime, windowLength, provider) ->
+ Assert.AreEqual (TimeSpan.FromMilliseconds 10., windowTime, "TimeSpanCount helper must keep provided window time.")
+ Assert.AreEqual (3, windowLength, "TimeSpanCount helper must keep provided window length.")
+ Assert.AreSame (ObservableSystem.DefaultTimeProvider, provider, "TimeSpanCount helper must use default time provider.")
+ | _ -> Assert.Fail ("TimeSpanCount helper must create ChunkTimeSpanCount configuration.")
+
+ []
+ member _.``Milliseconds should use default time provider`` () =
+ match ChunkConfiguration.Milliseconds 15 with
+ | ChunkMilliseconds (windowTime, provider) ->
+ Assert.AreEqual (15, windowTime, "Milliseconds helper must keep provided value.")
+ Assert.AreSame (ObservableSystem.DefaultTimeProvider, provider, "Milliseconds helper must use default time provider.")
+ | _ -> Assert.Fail ("Milliseconds helper must create ChunkMilliseconds configuration.")
+
+ []
+ member _.``MillisecondsCount should use default time provider`` () =
+ match ChunkConfiguration.MillisecondsCount 20 4 with
+ | ChunkMillisecondsCount (windowTime, windowLength, provider) ->
+ Assert.AreEqual (20, windowTime, "MillisecondsCount helper must keep provided window time.")
+ Assert.AreEqual (4, windowLength, "MillisecondsCount helper must keep provided window length.")
+ Assert.AreSame (ObservableSystem.DefaultTimeProvider, provider, "MillisecondsCount helper must use default time provider.")
+ | _ -> Assert.Fail ("MillisecondsCount helper must create ChunkMillisecondsCount configuration.")
+
+ []
+ member _.``AsyncWindow should wrap async callback`` () : Task = task {
+ let mutable observed = 0
+ let configuration = ChunkConfiguration.AsyncWindow (fun value -> async { observed <- value })
+
+ match configuration with
+ | ChunkAsyncWindow (callback, configureAwait) ->
+ do! callback.Invoke (11, CancellationToken.None)
+ Assert.AreEqual (11, observed, "AsyncWindow helper must invoke wrapped callback with value.")
+ Assert.IsTrue (configureAwait, "AsyncWindow helper must set configureAwait to true.")
+ | _ -> Assert.Fail ("AsyncWindow helper must create ChunkAsyncWindow configuration.")
+ }
diff --git a/tests/FSharp.Control.R3.Tests/TaskObservable/AggregationCategoryTests.fs b/tests/FSharp.Control.R3.Tests/TaskObservable/AggregationCategoryTests.fs
new file mode 100644
index 0000000..968c44b
--- /dev/null
+++ b/tests/FSharp.Control.R3.Tests/TaskObservable/AggregationCategoryTests.fs
@@ -0,0 +1,26 @@
+namespace FSharp.Control.R3.Tests.TaskObservable
+
+open System.Threading.Tasks
+open Microsoft.VisualStudio.TestTools.UnitTesting
+open R3
+open FSharp.Control.R3
+open FSharp.Control.R3.Task
+open FSharp.Control.R3.Tests
+
+[]
+type AggregationCategoryTests () =
+ []
+ member _.``aggregate should match direct AggregateAsync`` () : Task = task {
+ let source = TestHelpers.createObservable [| 1; 2; 3 |]
+ let! actual =
+ source
+ |> Observable.aggregate TestHelpers.cancellationToken 0 (fun acc x -> acc + x)
+ Assert.AreEqual (6, actual, "Task aggregate must return aggregated sum.")
+ }
+
+ []
+ member _.``length should count values`` () : Task = task {
+ let source = TestHelpers.createObservable [| 1; 2; 3; 4 |]
+ let! actual = source |> Observable.length TestHelpers.cancellationToken
+ Assert.AreEqual (4, actual, "Task length must return source value count.")
+ }
diff --git a/tests/FSharp.Control.R3.Tests/TaskObservable/AllSelectionCategoryTests.fs b/tests/FSharp.Control.R3.Tests/TaskObservable/AllSelectionCategoryTests.fs
new file mode 100644
index 0000000..4b90160
--- /dev/null
+++ b/tests/FSharp.Control.R3.Tests/TaskObservable/AllSelectionCategoryTests.fs
@@ -0,0 +1,28 @@
+namespace FSharp.Control.R3.Tests.TaskObservable
+
+open System.Threading.Tasks
+open Microsoft.VisualStudio.TestTools.UnitTesting
+open R3
+open FSharp.Control.R3
+open FSharp.Control.R3.Task
+open FSharp.Control.R3.Tests
+
+[]
+type AllSelectionCategoryTests () =
+ []
+ member _.``all should return true when all values match`` () : Task = task {
+ let source = TestHelpers.createObservable [| 2; 4; 6 |]
+ let! actual =
+ source
+ |> Observable.all TestHelpers.cancellationToken (fun x -> x % 2 = 0)
+ Assert.IsTrue (actual, "Task all must return true when all elements satisfy predicate.")
+ }
+
+ []
+ member _.``existsAsync should detect available values`` () : Task = task {
+ let source = TestHelpers.createObservable [| 1 |]
+ let! actual =
+ source
+ |> Observable.existsAsync TestHelpers.cancellationToken
+ Assert.IsTrue (actual, "Task existsAsync must return true for non-empty source.")
+ }
diff --git a/tests/FSharp.Control.R3.Tests/TaskObservable/SingleElementCategoryTests.fs b/tests/FSharp.Control.R3.Tests/TaskObservable/SingleElementCategoryTests.fs
new file mode 100644
index 0000000..afa1bc7
--- /dev/null
+++ b/tests/FSharp.Control.R3.Tests/TaskObservable/SingleElementCategoryTests.fs
@@ -0,0 +1,19 @@
+namespace FSharp.Control.R3.Tests.TaskObservable
+
+open System.Threading.Tasks
+open Microsoft.VisualStudio.TestTools.UnitTesting
+open R3
+open FSharp.Control.R3
+open FSharp.Control.R3.Task
+open FSharp.Control.R3.Tests
+
+[]
+type SingleElementCategoryTests () =
+ []
+ member _.``firstAsync should return first value`` () : Task = task {
+ let source = TestHelpers.createObservable [| 5; 6 |]
+ let! actual =
+ source
+ |> Observable.firstAsync TestHelpers.cancellationToken
+ Assert.AreEqual (5, actual, "Task firstAsync must return first emitted value.")
+ }
diff --git a/tests/FSharp.Control.R3.Tests/TaskObservable/TransformationCategoryTests.fs b/tests/FSharp.Control.R3.Tests/TaskObservable/TransformationCategoryTests.fs
new file mode 100644
index 0000000..165a20e
--- /dev/null
+++ b/tests/FSharp.Control.R3.Tests/TaskObservable/TransformationCategoryTests.fs
@@ -0,0 +1,43 @@
+namespace FSharp.Control.R3.Tests.TaskObservable
+
+open System.Threading
+open System.Threading.Tasks
+open Microsoft.VisualStudio.TestTools.UnitTesting
+open R3
+open FSharp.Control.R3
+open FSharp.Control.R3.Task
+open FSharp.Control.R3.Tests
+
+[]
+type TransformationCategoryTests () =
+ []
+ member _.``mapAsync should transform each value`` () : Task = task {
+ let options = ProcessingOptions.Default
+ let source = TestHelpers.createObservable [| 1; 2; 3 |]
+ let! actual =
+ source
+ |> Observable.mapAsync options (fun (_ : CancellationToken) x -> Task.FromResult (x + 1))
+ |> TestHelpers.toArrayTask
+ CollectionAssert.AreEqual ([| 2; 3; 4 |], actual, "Task mapAsync must transform each source value.")
+ }
+
+ []
+ member _.``iter should invoke action for each value`` () : Task = task {
+ let source = TestHelpers.createObservable [| 1; 2; 3 |]
+ let mutable sum = 0
+ do!
+ source
+ |> Observable.iter TestHelpers.cancellationToken (fun x -> sum <- sum + x)
+ Assert.AreEqual (6, sum, "Task iter must invoke action for each value.")
+ }
+
+ []
+ member _.``iterAsync should await action for each value`` () : Task = task {
+ let options = ProcessingOptions.Default
+ let source = TestHelpers.createObservable [| 1; 2; 3 |]
+ let mutable sum = 0
+ do!
+ source
+ |> Observable.iterAsync TestHelpers.cancellationToken options (fun (_ : CancellationToken) x -> task { sum <- sum + x })
+ Assert.AreEqual (6, sum, "Task iterAsync must run action for each emitted value.")
+ }
diff --git a/tests/FSharp.Control.R3.Tests/TestHelpers.fs b/tests/FSharp.Control.R3.Tests/TestHelpers.fs
new file mode 100644
index 0000000..28f3985
--- /dev/null
+++ b/tests/FSharp.Control.R3.Tests/TestHelpers.fs
@@ -0,0 +1,13 @@
+module FSharp.Control.R3.Tests.TestHelpers
+
+open System
+open System.Threading
+open R3
+
+let createObservable (values : 'T array) = Observable.ToObservable values
+
+let toArrayTask (source : Observable<'T>) = ObservableExtensions.ToArrayAsync source
+
+let cancellationToken = CancellationToken.None
+
+let stringComparer = StringComparer.OrdinalIgnoreCase