diff --git a/dotnet/src/webdriver/BiDi/EventStreamExtensions.cs b/dotnet/src/webdriver/BiDi/EventStreamExtensions.cs new file mode 100644 index 0000000000000..9036d9ba7be89 --- /dev/null +++ b/dotnet/src/webdriver/BiDi/EventStreamExtensions.cs @@ -0,0 +1,51 @@ +// +// Licensed to the Software Freedom Conservancy (SFC) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The SFC licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + +using System.Runtime.CompilerServices; + +namespace OpenQA.Selenium.BiDi; + +public static class EventStreamExtensions +{ + /// + /// Configures how awaits on the tasks returned from an iteration of the event stream are performed. + /// + /// + /// + /// implements both and + /// , which makes a plain .ConfigureAwait(bool) call ambiguous + /// (CS0121) because TaskAsyncEnumerableExtensions provides an overload for each interface. + /// This extension method resolves the ambiguity by explicitly routing to the + /// overload, which is the behavior callers need when using + /// await foreach. + /// + /// + /// The event-args type produced by the stream. + /// The event stream to configure. + /// + /// to capture and marshal continuation back to the original context; + /// to continue on a thread-pool thread. + /// + /// A configured enumerable that applies the specified context-capture behavior. + public static ConfiguredCancelableAsyncEnumerable ConfigureAwait( + this IEventStream stream, + bool continueOnCapturedContext) + where TEventArgs : EventArgs + => ((IAsyncEnumerable)stream).ConfigureAwait(continueOnCapturedContext); +} diff --git a/dotnet/test/webdriver/BiDi/EventStreamExtensionsTests.cs b/dotnet/test/webdriver/BiDi/EventStreamExtensionsTests.cs new file mode 100644 index 0000000000000..7a98bfcf714c2 --- /dev/null +++ b/dotnet/test/webdriver/BiDi/EventStreamExtensionsTests.cs @@ -0,0 +1,84 @@ +// +// Licensed to the Software Freedom Conservancy (SFC) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The SFC licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + +using System.Runtime.CompilerServices; +using OpenQA.Selenium.BiDi; + +namespace OpenQA.Selenium.Tests.BiDi; + +[Parallelizable(ParallelScope.All)] +[FixtureLifeCycle(LifeCycle.InstancePerTestCase)] +class EventStreamExtensionsTests +{ + private IBiDi _bidi; + private FakeTransport _transport; + + [SetUp] + public async Task SetUp() + { + _transport = new FakeTransport(); + _bidi = await Selenium.BiDi.BiDi.ConnectAsync(new Uri("ws://fake"), opts => opts.UseTransport(() => _transport)); + } + + [TearDown] + public async Task TearDown() + { + await _bidi.DisposeAsync(); + } + + [Test] + public async Task ConfigureAwait_ReturnsConfiguredCancelableAsyncEnumerable() + { + var stream = await _bidi.Script.RealmDestroyed.StreamAsync() + .WithResponse(_transport, """{"subscription":"sub-1"}"""); + + // This line previously failed to compile (CS0121) because IEventStream implements + // both IAsyncEnumerable and IAsyncDisposable and both have a matching ConfigureAwait overload. + // EventStreamExtensions.ConfigureAwait disambiguates toward IAsyncEnumerable. + var configured = stream.ConfigureAwait(false); + + Assert.That(configured, Is.InstanceOf>()); + + await stream.DisposeAsync().WithResponse(_transport); + } + + [Test] + public async Task ConfigureAwait_DeliverEventsThroughConfiguredEnumerable() + { + var stream = await _bidi.Script.RealmDestroyed.StreamAsync() + .WithResponse(_transport, """{"subscription":"sub-1"}"""); + + _transport.EnqueueEvent("script.realmDestroyed", """{"realm":"r-1"}"""); + + var received = new List(); + + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); + + await foreach (var e in stream.ConfigureAwait(false).WithCancellation(cts.Token)) + { + received.Add(e); + break; + } + + Assert.That(received, Has.Count.EqualTo(1)); + Assert.That(received[0].Realm.Id, Is.EqualTo("r-1")); + + await stream.DisposeAsync().WithResponse(_transport); + } +}