From af8e6360ae2407924a488382c526f51832a2762e Mon Sep 17 00:00:00 2001 From: Craig Fowler Date: Wed, 28 Jan 2026 21:15:13 +0000 Subject: [PATCH 1/3] Add some tests to help diagnose #277 These pass, though, so this isn't the problem! --- .../CSF.Screenplay.Tests/Reporting/ReportFormatCreatorTests.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Tests/CSF.Screenplay.Tests/Reporting/ReportFormatCreatorTests.cs b/Tests/CSF.Screenplay.Tests/Reporting/ReportFormatCreatorTests.cs index e7eed01e..24dbbd3d 100644 --- a/Tests/CSF.Screenplay.Tests/Reporting/ReportFormatCreatorTests.cs +++ b/Tests/CSF.Screenplay.Tests/Reporting/ReportFormatCreatorTests.cs @@ -15,6 +15,7 @@ public class ReportFormatCreatorTests [TestCase("}", "}")] [TestCase("{foo} {bar", "{0} {bar")] [TestCase("{{{{foo}}}}", "{{{{foo}}}}")] + [TestCase("{Actor} opens their browser at {UriName}: {Uri}", "{0} opens their browser at {1}: {2}")] public void GetReportFormatShouldReturnTheCorrectFormattedTemplate(string original, string expected) { var sut = new ReportFormatCreator(); @@ -37,6 +38,7 @@ public void GetReportFormatShouldReturnTheCorrectFormattedTemplate(string origin [TestCase("}", "", "", "")] [TestCase("{foo} {bar", "foo", "", "")] [TestCase("{{{{foo}}}}", "", "", "")] + [TestCase("{Actor} opens their browser at {UriName}: {Uri}", "Actor", "UriName", "Uri")] public void GetReportFormatShouldReturnTheCorrectObjectNames(string template, string name1, string name2, string name3) { var expected = new[] { name1, name2, name3 }.Where(x => x.Length > 0).ToList(); From f8e9fc20732500c0dd39699f73a12438cd6ffe31 Mon Sep 17 00:00:00 2001 From: Craig Fowler Date: Wed, 28 Jan 2026 21:57:58 +0000 Subject: [PATCH 2/3] Resolve #277 - Correct reports for OpenUrl --- CSF.Screenplay.Selenium/NamedUri.cs | 28 +++++++++- CSF.Screenplay.Selenium/NamedUriExtensions.cs | 36 +++++++++++++ .../Tasks/OpenUrlRespectingBase.cs | 9 ++-- .../NamedUriTests.cs | 29 +++++++++++ .../Tasks/OpenUrlRespectingBaseTests.cs | 52 +++++++++++++++++++ 5 files changed, 147 insertions(+), 7 deletions(-) create mode 100644 CSF.Screenplay.Selenium/NamedUriExtensions.cs create mode 100644 Tests/CSF.Screenplay.Selenium.Tests/NamedUriTests.cs create mode 100644 Tests/CSF.Screenplay.Selenium.Tests/Tasks/OpenUrlRespectingBaseTests.cs diff --git a/CSF.Screenplay.Selenium/NamedUri.cs b/CSF.Screenplay.Selenium/NamedUri.cs index f14989b4..364446dd 100644 --- a/CSF.Screenplay.Selenium/NamedUri.cs +++ b/CSF.Screenplay.Selenium/NamedUri.cs @@ -27,6 +27,32 @@ public sealed class NamedUri : IHasName /// public Uri Uri { get; } + /// + /// Gets a copy of the current named URI, except 'rebased' using the specified base URI. + /// + /// + /// + /// If the current is then this method has not effect and the named URI which is + /// returned is the unmodified current instance. + /// + /// + /// If the current Uri is not absolute, then the specified base URI is prepended to the current URI, serving as a base. + /// The new URI is then returned from this method. Note that this method will never result in the current instance being + /// mutated, at most it will only return a copy of the current instance, which has the newly-rebased URI. + /// + /// + /// A new base URI + /// A URI which might have been rebased onto the new base URI + public NamedUri RebaseTo(Uri baseUri) + { + if(baseUri == null) throw new ArgumentNullException(nameof(baseUri)); + + if (Uri.IsAbsoluteUri) return this; + + var rebased = new Uri(baseUri, Uri); + return new NamedUri(rebased, Name); + } + /// /// Initializes a new instance of the class. /// @@ -63,6 +89,6 @@ public NamedUri(string uri, string name = null) /// /// The URI to convert. /// A new instance. - public static implicit operator NamedUri(string uri) => uri is null ? null : new NamedUri(new Uri(uri, UriKind.RelativeOrAbsolute)); + public static implicit operator NamedUri(string uri) => uri is null ? null : new NamedUri(uri); } } \ No newline at end of file diff --git a/CSF.Screenplay.Selenium/NamedUriExtensions.cs b/CSF.Screenplay.Selenium/NamedUriExtensions.cs new file mode 100644 index 00000000..a0db5ead --- /dev/null +++ b/CSF.Screenplay.Selenium/NamedUriExtensions.cs @@ -0,0 +1,36 @@ + +using System; + +namespace CSF.Screenplay.Selenium +{ + /// + /// Extension methods for . + /// + public static class NamedUriExtensions + { + /// + /// Gets a copy of the current named URI, except 'rebased' using the specified base URI. + /// + /// + /// + /// If the current is then this method has not effect and the named URI which is + /// returned is the unmodified current instance. + /// + /// + /// If the current Uri is not absolute, then the specified base URI is prepended to the current URI, serving as a base. + /// The new URI is then returned from this method. Note that this method will never result in the current instance being + /// mutated, at most it will only return a copy of the current instance, which has the newly-rebased URI. + /// + /// + /// The named URI to rebase + /// A new base URI + /// A URI which might have been rebased onto the new base URI + public static NamedUri RebaseTo(this NamedUri namedUri, string baseUri) + { + if(namedUri == null) throw new ArgumentNullException(nameof(namedUri)); + if(baseUri == null) throw new ArgumentNullException(nameof(baseUri)); + return namedUri.RebaseTo(new Uri(baseUri, UriKind.Absolute)); + } + } + +} \ No newline at end of file diff --git a/CSF.Screenplay.Selenium/Tasks/OpenUrlRespectingBase.cs b/CSF.Screenplay.Selenium/Tasks/OpenUrlRespectingBase.cs index d4c7ebd8..1da502b3 100644 --- a/CSF.Screenplay.Selenium/Tasks/OpenUrlRespectingBase.cs +++ b/CSF.Screenplay.Selenium/Tasks/OpenUrlRespectingBase.cs @@ -23,14 +23,11 @@ public class OpenUrlRespectingBase : IPerformable, ICanReport /// public ValueTask PerformAsAsync(ICanPerform actor, CancellationToken cancellationToken = default) { - if(uri.Uri.IsAbsoluteUri) - return actor.PerformAsync(new OpenUrl(uri.Uri), cancellationToken); - if(!actor.TryGetAbility(out var ability)) - return actor.PerformAsync(new OpenUrl(uri.Uri), cancellationToken); + return actor.PerformAsync(new OpenUrl(uri), cancellationToken); - var absoluteUri = new Uri(ability.BaseUri, uri.Uri); - return actor.PerformAsync(new OpenUrl(absoluteUri), cancellationToken); + var rebased = uri.RebaseTo(ability.BaseUri); + return actor.PerformAsync(new OpenUrl(rebased), cancellationToken); } /// diff --git a/Tests/CSF.Screenplay.Selenium.Tests/NamedUriTests.cs b/Tests/CSF.Screenplay.Selenium.Tests/NamedUriTests.cs new file mode 100644 index 00000000..42da85f0 --- /dev/null +++ b/Tests/CSF.Screenplay.Selenium.Tests/NamedUriTests.cs @@ -0,0 +1,29 @@ +namespace CSF.Screenplay.Selenium; + +[TestFixture, Parallelizable] +public class NamedUriTests +{ + [Test] + public void RebaseToShouldReturnARebasedUriIfItIsNotAbsolute() + { + var sut = new NamedUri("test.html", "name"); + var rebased = sut.RebaseTo("https://example.com"); + Assert.That(rebased.Uri.ToString(), Is.EqualTo("https://example.com/test.html")); + } + + [Test] + public void RebaseToShouldNotAlterTheUriIfItIsAbsolute() + { + var sut = new NamedUri("https://foobar.example.com/test.html", "name"); + var rebased = sut.RebaseTo("https://example.com"); + Assert.That(rebased.Uri.ToString(), Is.EqualTo("https://foobar.example.com/test.html")); + } + + [Test] + public void RebaseToShouldReturnAUriWithTheSameName() + { + var sut = new NamedUri("test.html", "name"); + var rebased = sut.RebaseTo("https://example.com"); + Assert.That(rebased.Name, Is.EqualTo("name")); + } +} \ No newline at end of file diff --git a/Tests/CSF.Screenplay.Selenium.Tests/Tasks/OpenUrlRespectingBaseTests.cs b/Tests/CSF.Screenplay.Selenium.Tests/Tasks/OpenUrlRespectingBaseTests.cs new file mode 100644 index 00000000..ac7452c1 --- /dev/null +++ b/Tests/CSF.Screenplay.Selenium.Tests/Tasks/OpenUrlRespectingBaseTests.cs @@ -0,0 +1,52 @@ +using System; +using CSF.Extensions.WebDriver; +using CSF.Extensions.WebDriver.Factories; +using CSF.Screenplay.Actors; +using CSF.Screenplay.Reporting; +using CSF.Screenplay.Selenium.Actions; +using Microsoft.Extensions.DependencyInjection; +using Moq; +using OpenQA.Selenium; + +namespace CSF.Screenplay.Selenium.Tasks; + +[TestFixture, Parallelizable] +public class OpenUrlRespectingBaseTests +{ + [Test, AutoMoqData] + public async Task TheActionCreatedByThisTaskShouldContainTheCorrectReport(IWebDriver driver, DriverOptions options) + { + var actor = new Actor("Anthony", Guid.NewGuid()); + IPerformable? performable = null; + + void OnPerform(object? sender, PerformableEventArgs ev) => performable = (IPerformable)ev.Performable; + + var namedUri = new NamedUri("test.html", "the test page"); + var baseUri = "https://example.com"; + actor.IsAbleTo(new UseABaseUri(new Uri(baseUri, UriKind.Absolute))); + actor.IsAbleTo(new BrowseTheWeb(Mock.Of(x => x.GetDefaultWebDriver(It.IsAny>()) == new WebDriverAndOptions(driver, options)))); + var sut = new OpenUrlRespectingBase(namedUri); + var valueFormatterProvider = new ValueFormatterProvider(new ServiceCollection().AddTransient().BuildServiceProvider(), + new ValueFormatterRegistry { typeof(ToStringFormatter) }); + + var formatter = new ReportFragmentFormatter(new ReportFormatCreator(), valueFormatterProvider); + + actor.EndPerformable += OnPerform; + try + { + await sut.PerformAsAsync(actor); + + Assert.Multiple(() => + { + Assert.That(performable, Is.InstanceOf(), "Performable is correct type"); + Assert.That(((OpenUrl) performable!).GetReportFragment(actor, formatter).ToString(), + Is.EqualTo("Anthony opens their browser at the test page: https://example.com/test.html"), + "The report is correct"); + }); + } + finally + { + actor.EndPerformable -= OnPerform; + } + } +} \ No newline at end of file From 6c7c992d8b562ee779e60a5cac0c70e17239879b Mon Sep 17 00:00:00 2001 From: Craig Fowler Date: Wed, 28 Jan 2026 22:23:48 +0000 Subject: [PATCH 3/3] Further test coverage --- CSF.Screenplay.Selenium/Actions/OpenUrl.cs | 4 +-- .../NamedUriTests.cs | 31 ++++++++++++++++ .../Tasks/OpenUrlRespectingBaseTests.cs | 35 +++++++++++++++++++ 3 files changed, 68 insertions(+), 2 deletions(-) diff --git a/CSF.Screenplay.Selenium/Actions/OpenUrl.cs b/CSF.Screenplay.Selenium/Actions/OpenUrl.cs index f10998c1..3bbd7264 100644 --- a/CSF.Screenplay.Selenium/Actions/OpenUrl.cs +++ b/CSF.Screenplay.Selenium/Actions/OpenUrl.cs @@ -15,13 +15,13 @@ public class OpenUrl : IPerformable, ICanReport public ValueTask PerformAsAsync(ICanPerform actor, CancellationToken cancellationToken = default) { var ability = actor.GetAbility(); - ability.WebDriver.Url = uri.Uri.AbsoluteUri; + ability.WebDriver.Url = uri.Uri.ToString(); return default; } /// public ReportFragment GetReportFragment(Actor actor, IFormatsReportFragment formatter) - => formatter.Format("{Actor} opens their browser at {UriName}: {Uri}", actor.Name, uri.Name, uri.Uri.AbsoluteUri); + => formatter.Format("{Actor} opens their browser at {UriName}: {Uri}", actor.Name, uri.Name, uri.Uri.ToString()); /// /// Initializes a new instance of the class with the specified URL. diff --git a/Tests/CSF.Screenplay.Selenium.Tests/NamedUriTests.cs b/Tests/CSF.Screenplay.Selenium.Tests/NamedUriTests.cs index 42da85f0..895e5f32 100644 --- a/Tests/CSF.Screenplay.Selenium.Tests/NamedUriTests.cs +++ b/Tests/CSF.Screenplay.Selenium.Tests/NamedUriTests.cs @@ -1,3 +1,6 @@ +using System; +using CSF.Screenplay.Selenium; + namespace CSF.Screenplay.Selenium; [TestFixture, Parallelizable] @@ -26,4 +29,32 @@ public void RebaseToShouldReturnAUriWithTheSameName() var rebased = sut.RebaseTo("https://example.com"); Assert.That(rebased.Name, Is.EqualTo("name")); } + + [Test] + public void RebaseToShouldThrowIfUriIsNull() + { + var sut = new NamedUri("test.html", "name"); + Assert.That(() => sut.RebaseTo(null), Throws.ArgumentNullException); + } + + [Test] + public void RebaseToShouldThrowIfUriIsNullString() + { + var sut = new NamedUri("test.html", "name"); + Assert.That(() => sut.RebaseTo((string?) null), Throws.ArgumentNullException); + } + + [Test] + public void ImplicitCastFromUriShouldCreateANamedUri() + { + NamedUri uri = new Uri("https://example.com/foo.html"); + Assert.That(uri.Uri.ToString(), Is.EqualTo("https://example.com/foo.html")); + } + + [Test] + public void ImplicitCastFromStringShouldCreateANamedUri() + { + NamedUri uri = "https://example.com/foo.html"; + Assert.That(uri.Uri.ToString(), Is.EqualTo("https://example.com/foo.html")); + } } \ No newline at end of file diff --git a/Tests/CSF.Screenplay.Selenium.Tests/Tasks/OpenUrlRespectingBaseTests.cs b/Tests/CSF.Screenplay.Selenium.Tests/Tasks/OpenUrlRespectingBaseTests.cs index ac7452c1..691e9fee 100644 --- a/Tests/CSF.Screenplay.Selenium.Tests/Tasks/OpenUrlRespectingBaseTests.cs +++ b/Tests/CSF.Screenplay.Selenium.Tests/Tasks/OpenUrlRespectingBaseTests.cs @@ -49,4 +49,39 @@ public async Task TheActionCreatedByThisTaskShouldContainTheCorrectReport(IWebDr actor.EndPerformable -= OnPerform; } } + + [Test, AutoMoqData] + public async Task TheActionCreatedByThisTaskShouldContainTheCorrectReportWhenTheActorDoesNotHaveABaseUrl(IWebDriver driver, DriverOptions options) + { + var actor = new Actor("Anthony", Guid.NewGuid()); + IPerformable? performable = null; + + void OnPerform(object? sender, PerformableEventArgs ev) => performable = (IPerformable)ev.Performable; + + var namedUri = new NamedUri("test.html", "the test page"); + actor.IsAbleTo(new BrowseTheWeb(Mock.Of(x => x.GetDefaultWebDriver(It.IsAny>()) == new WebDriverAndOptions(driver, options)))); + var sut = new OpenUrlRespectingBase(namedUri); + var valueFormatterProvider = new ValueFormatterProvider(new ServiceCollection().AddTransient().BuildServiceProvider(), + new ValueFormatterRegistry { typeof(ToStringFormatter) }); + + var formatter = new ReportFragmentFormatter(new ReportFormatCreator(), valueFormatterProvider); + + actor.EndPerformable += OnPerform; + try + { + await sut.PerformAsAsync(actor); + + Assert.Multiple(() => + { + Assert.That(performable, Is.InstanceOf(), "Performable is correct type"); + Assert.That(((OpenUrl) performable!).GetReportFragment(actor, formatter).ToString(), + Is.EqualTo("Anthony opens their browser at the test page: test.html"), + "The report is correct"); + }); + } + finally + { + actor.EndPerformable -= OnPerform; + } + } } \ No newline at end of file