Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/publishDocsWebsite.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,6 @@ jobs:
committer_email: github-actions-workflow@bots.noreply.github.com
fetch: true
message: Publish updated documentation website
push: origin master --force
push: origin master


2 changes: 1 addition & 1 deletion .sonarqube-analysisproperties.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<SonarQubeAnalysisProperties xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns="http://www.sonarsource.com/msbuild/integration/2015/1">
<Property Name="sonar.coverage.exclusions">Tests\**\*,**\*Exception.cs,**\*.spec.js,**\*.config.js</Property>
<Property Name="sonar.coverage.exclusions">Tests\**\*,**\*Exception.cs,**\*.spec.js,**\*.config.js,CSF.Screenplay.Selenium\Resources\**\*.js</Property>
<Property Name="sonar.exclusions">docs\**\*,*_old\**\*</Property>
<Property Name="sonar.cpd.exclusions">Tests\**\*,**\*.spec.js</Property>
<Property Name="sonar.cs.nunit.reportsPaths">Tests\**\TestResults.xml</Property>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using OpenQA.Selenium;

namespace CSF.Screenplay.Selenium.Actions
{
/// <summary>
/// Executes a custom Javascript which begins collection/interception of web browser console log messages.
/// </summary>
/// <remarks>
/// <para>
/// When used, this action should be executed as soon as possible after the current page has completed loading.
/// Ideally, directly after <see cref="Tasks.ClickAndWaitForDocumentReady"/>.
/// Any log messages which have been sent to the native browser console before this action is executed will be missed
/// and will not be available to the counterpart question which retrieves log messages: <see cref="Questions.GetLogsWithJavaScript"/>.
/// </para>
/// <para>
/// Note that this action/the script needs to be re-run after each traditional web page navigation/reload.
/// However, due to the nature of SPAs, it <em>does not need to be re-run</em> following an SPA-style navigation.
/// On supported browsers, there is no harm in re-running this script when it is not needed, except for the impact on
/// performance (wasted network roundtrips).
/// </para>
/// <para>
/// This action is for use only with web browsers which have the <see cref="BrowserQuirks.RequiresJavascriptToGetLogs"/> quirk.
/// </para>
/// </remarks>
public class BeginCollectingLogsWithJavaScript : IPerformable, ICanReport
{
/// <inheritdoc/>
public ReportFragment GetReportFragment(Actor actor, IFormatsReportFragment formatter)
=> formatter.Format("{Actor} begins collecting browser logs from the current page, using a JavaScript technique");

/// <inheritdoc/>
public async ValueTask PerformAsAsync(ICanPerform actor, CancellationToken cancellationToken = default)
{
var driver = actor.GetAbility<BrowseTheWeb>();
if(!driver.WebDriver.HasQuirk(BrowserQuirks.RequiresJavascriptToGetLogs))
throw new NotSupportedException("The WebDriver must have support for retrieving logs using a Javascript workaround.");

await actor.PerformAsync(PerformableBuilder.ExecuteAScript(Scripts.CaptureLogs), cancellationToken);
}
}
}
8 changes: 6 additions & 2 deletions CSF.Screenplay.Selenium/Actions/OpenUrl.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using OpenQA.Selenium;
using static CSF.Screenplay.Selenium.PerformableBuilder;

namespace CSF.Screenplay.Selenium.Actions
{
Expand Down Expand Up @@ -47,13 +49,15 @@ public class OpenUrl : IPerformable, ICanReport
readonly NamedUri uri;

/// <inheritdoc/>
public ValueTask PerformAsAsync(ICanPerform actor, CancellationToken cancellationToken = default)
public async ValueTask PerformAsAsync(ICanPerform actor, CancellationToken cancellationToken = default)
{
var ability = actor.GetAbility<BrowseTheWeb>();
if(!uri.Uri.IsAbsoluteUri)
throw new InvalidOperationException($"The URL to open must be absolute; have you forgotten to grant {actor} the {nameof(UseABaseUri)} ability?");
ability.WebDriver.Url = uri.Uri.ToString();
return default;

if(ability.ShouldCollectLogs && ability.WebDriver.HasQuirk(BrowserQuirks.RequiresJavascriptToGetLogs))
await actor.PerformAsync(BeginCollectingLogsWithJavaScript(), cancellationToken);
}

/// <inheritdoc/>
Expand Down
27 changes: 23 additions & 4 deletions CSF.Screenplay.Selenium/BrowseTheWeb.cs
Original file line number Diff line number Diff line change
Expand Up @@ -84,13 +84,26 @@ namespace CSF.Screenplay.Selenium
public class BrowseTheWeb : ICanReport, IDisposable
{
readonly Lazy<WebDriverAndOptions> webDriverAndOptions;
readonly bool collectLogs;
bool disposedValue;

/// <summary>
/// Gets the Selenium WebDriver associated with the current ability instance.
/// </summary>
public IWebDriver WebDriver => webDriverAndOptions.Value.WebDriver;

/// <summary>
/// Gets a value indicating whether the current WebDriver should go to lengths to collect console log information.
/// </summary>
/// <remarks>
/// <para>
/// When this value is set to <see langword="true"/>, this will trigger usage of <see cref="Actions.BeginCollectingLogsWithJavaScript"/>
/// at points where it is required. This is applicable only when the current <see cref="WebDriver"/> implementation has the
/// quirk <see cref="BrowserQuirks.RequiresJavascriptToGetLogs"/>.
/// </para>
/// </remarks>
public bool ShouldCollectLogs => collectLogs;

/// <summary>
/// Gets the WebDriver options which were used to create <see cref="WebDriver"/>.
/// </summary>
Expand All @@ -116,9 +129,12 @@ static Lazy<WebDriverAndOptions> GetLazyWebDriverAndOptions(IGetsWebDriver webDr

/// <inheritdoc/>
public ReportFragment GetReportFragment(Actor actor, IFormatsReportFragment formatter)
=> formatter.Format("{Actor} is able to browse the web using {BrowserName}",
actor.Name,
WebDriver.GetBrowserId()?.ToString() ?? "a Selenium WebDriver");
{
var logsSuffix = ShouldCollectLogs ? ", and do their best to collect console logs" : string.Empty;
return formatter.Format("{Actor} is able to browse the web using {BrowserName}" + logsSuffix,
actor.Name,
WebDriver.GetBrowserId()?.ToString() ?? "a Selenium WebDriver");
}

/// <summary>
/// Initializes a new instance of the <see cref="BrowseTheWeb"/> class.
Expand All @@ -140,9 +156,12 @@ public ReportFragment GetReportFragment(Actor actor, IFormatsReportFragment form
/// </remarks>
/// <param name="webDriverFactory">A <see cref="IGetsWebDriver">universal WebDriver factory</see> instance</param>
/// <param name="webDriverName">An optional name, specifying the WebDriver configuration (within those available in the factory) to use.</param>
public BrowseTheWeb(IGetsWebDriver webDriverFactory, string webDriverName = null)
/// <param name="collectLogs">An optional value indicating whether the Screenplay Selenium extension
/// should go to lengths to ensure that web browser console logs are available. See <see cref="ShouldCollectLogs"/> for more information.</param>
public BrowseTheWeb(IGetsWebDriver webDriverFactory, string webDriverName = null, bool collectLogs = false)
{
webDriverAndOptions = GetLazyWebDriverAndOptions(webDriverFactory, webDriverName);
this.collectLogs = collectLogs;
}

/// <summary>
Expand Down
73 changes: 64 additions & 9 deletions CSF.Screenplay.Selenium/BrowserQuirks.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ namespace CSF.Screenplay.Selenium
/// <seealso cref="IHasQuirks"/>
public static class BrowserQuirks
{
const string chrome = "chrome", firefox = "firefox", safari = "safari", edge = "edge";

/// <summary>
/// Gets the name of a browser quirk, for browsers which cannot set the value of an <c>&lt;input type="date"&gt;</c> using the
/// "Send Keys" technique.
Expand Down Expand Up @@ -80,7 +82,7 @@ public static class BrowserQuirks
public static readonly string NeedsJavaScriptToGetShadowRoot = "NeedsJavaScriptToGetShadowRoot";

/// <summary>
/// Gets the name of a browser quirk, for browser which cannot get a Shadow Root node at all.
/// Gets the name of a browser quirk, for browsers which cannot get a Shadow Root node at all.
/// </summary>
/// <remarks>
/// <para>
Expand All @@ -90,6 +92,39 @@ public static class BrowserQuirks
/// </remarks>
public static readonly string CannotGetShadowRoot = "CannotGetShadowRoot";

/// <summary>
/// Gets the name of a browser quirk, for browsers which have native support for reading the console logs.
/// </summary>
/// <remarks>
/// <para>
/// Browsers with this quirk can read the console logs via a technique such as:
/// </para>
/// <code>
/// var logs = driver.Manage().Logs.GetLog(LogType.Browser);
/// </code>
/// <para>
/// The log levels recorded depend upon the manner in which the WebDriver is configured.
/// If using the <see cref="Extensions.WebDriver.WebDriverFactory"/>, this may be controlled by
/// the <see cref="Extensions.WebDriver.Factories.WebDriverCreationOptions.BrowserLogLevel"/> property of the configuration.
/// Otherwise, use <see cref="OpenQA.Selenium.DriverOptions.SetLoggingPreference(string, OpenQA.Selenium.LogLevel)"/> to configure
/// logging.
/// </para>
/// </remarks>
public static readonly string HasNativeLogsSupport = "HasNativeLogsSupport";

/// <summary>
/// Gets the name of a browser quirk, for browsers which can read browser logs, but which require a Javascript-based workaround.
/// </summary>
/// <remarks>
/// <para>
/// Browsers with this quirk cannot read logs in the way that those which <see cref="HasNativeLogsSupport"/> can, however JavaScript
/// may be used to intercept logs as they are sent to the console. These may then be stored and retrieved with a different script.
/// </para>
/// </remarks>
/// <seealso cref="Scripts.CaptureLogs"/>
/// <seealso cref="Scripts.GetLogs"/>
public static readonly string RequiresJavascriptToGetLogs = "RequiresJavascriptToGetLogs";

/// <summary>
/// Gets hard-coded information about known browser quirks.
/// </summary>
Expand All @@ -113,8 +148,8 @@ public static QuirksData GetQuirksData()
{
AffectedBrowsers = new HashSet<BrowserInfo>
{
new BrowserInfo { Name = "firefox" },
new BrowserInfo { Name = "safari" },
new BrowserInfo { Name = firefox },
new BrowserInfo { Name = safari },
}
}
},
Expand All @@ -124,7 +159,7 @@ public static QuirksData GetQuirksData()
{
AffectedBrowsers = new HashSet<BrowserInfo>
{
new BrowserInfo { Name = "safari" },
new BrowserInfo { Name = safari },
}
}
},
Expand All @@ -134,10 +169,9 @@ public static QuirksData GetQuirksData()
{
AffectedBrowsers = new HashSet<BrowserInfo>
{
new BrowserInfo { Name = "safari" },
// There is no Chrome 95.1.0.0 but this covers any 95.0.x
// The additional trailing zeroes are to work around https://github.com/csf-dev/CSF.Extensions.WebDriver/issues/56
new BrowserInfo { Name = "chrome", MaxVersion = "95.1.0.0" },
new BrowserInfo { Name = safari },
// There is no Chrome 95.1 but this covers any 95.0.x
new BrowserInfo { Name = chrome, MaxVersion = "95.1" },
}
}
},
Expand All @@ -148,7 +182,28 @@ public static QuirksData GetQuirksData()
AffectedBrowsers = new HashSet<BrowserInfo>
{
// There is no Firefox 112.1 but this covers any 112.0.x
new BrowserInfo { Name = "firefox", MaxVersion = "112.1" }
new BrowserInfo { Name = firefox, MaxVersion = "112.1" },
}
}
},
{
HasNativeLogsSupport,
new BrowserInfoCollection
{
AffectedBrowsers = new HashSet<BrowserInfo>
{
new BrowserInfo { Name = chrome },
new BrowserInfo { Name = edge },
}
}
},
{
RequiresJavascriptToGetLogs,
new BrowserInfoCollection
{
AffectedBrowsers = new HashSet<BrowserInfo>
{
new BrowserInfo { Name = firefox },
}
}
}
Expand Down
32 changes: 32 additions & 0 deletions CSF.Screenplay.Selenium/Builders/GetTheBrowserLogsBuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using CSF.Screenplay.Selenium.Tasks;

namespace CSF.Screenplay.Selenium.Builders
{
/// <summary>
/// Builder class for creating an instance of <see cref="GetTheBrowserLogs"/>.
/// </summary>
/// <remarks>
/// <para>
/// Primarily, this is concerned with whether the constructed task should throw or silently return an empty
/// collection, should retrieving logs be unsupported by the current WebDriver.
/// </para>
/// </remarks>
public class GetTheBrowserLogsBuilder
{
/// <summary>
/// Gets a <see cref="GetTheBrowserLogs"/> task.
/// The task will be configured such that - if no viable technique for getting logs is available - it will return
/// an empty collection instead of throwing.
/// </summary>
/// <returns>A performable task</returns>
public GetTheBrowserLogs ButReturnEmptyLogsIfUnsupported()

Check warning on line 22 in CSF.Screenplay.Selenium/Builders/GetTheBrowserLogsBuilder.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Member 'ButReturnEmptyLogsIfUnsupported' does not access instance data and can be marked as static

See more on https://sonarcloud.io/project/issues?id=csf-dev_CSF.Screenplay&issues=AZ5UnVdIXSDqH0AcCZbF&open=AZ5UnVdIXSDqH0AcCZbF&pullRequest=349

Check warning on line 22 in CSF.Screenplay.Selenium/Builders/GetTheBrowserLogsBuilder.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Make 'ButReturnEmptyLogsIfUnsupported' a static method.

See more on https://sonarcloud.io/project/issues?id=csf-dev_CSF.Screenplay&issues=AZ5UnVdIXSDqH0AcCZbE&open=AZ5UnVdIXSDqH0AcCZbE&pullRequest=349
=> new GetTheBrowserLogs(false);

/// <summary>
/// Implicitly converts the builder to an instance of <see cref="GetTheBrowserLogs"/>.
/// The task will throw if no viable technique for getting logs is available.
/// </summary>
public static implicit operator GetTheBrowserLogs(GetTheBrowserLogsBuilder _)
=> new GetTheBrowserLogs();
}
}
2 changes: 1 addition & 1 deletion CSF.Screenplay.Selenium/CSF.Screenplay.Selenium.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
<PackageReference Include="Microsoft.Bcl.HashCode" Version="1.0.0" Condition="'$(TargetFramework)' == 'netstandard2.0' or '$(TargetFramework)' == '$(DotNetFrameworkLegacy)'" />
<PackageReference Include="Selenium.WebDriver" Version="4.0.0" />
<PackageReference Include="Selenium.Support" Version="4.0.0" />
<PackageReference Include="CSF.Extensions.WebDriver" Version="2.0.0-6.beta" />
<PackageReference Include="CSF.Extensions.WebDriver" Version="2.0.0-7.beta" />
<PackageReference Include="CSF.Specifications" Version="2.0.0" />
</ItemGroup>

Expand Down
57 changes: 57 additions & 0 deletions CSF.Screenplay.Selenium/PerformableBuilder.general.cs
Original file line number Diff line number Diff line change
Expand Up @@ -208,5 +208,62 @@ public static NamedWaitBuilder WaitUntil(IBuildsElementPredicates predicate)
/// </summary>
/// <returns>A performable question.</returns>
public static GetWindowTitle ReadTheWindowTitle() => new GetWindowTitle();

/// <summary>
/// Gets a performable action which begins collecting log information with a Javascript workaround.
/// </summary>
/// <remarks>
/// <para>
/// It is usually not required to use this action directly.
/// The recommended way to consume this action is to set <see cref="BrowseTheWeb.ShouldCollectLogs"/> to <see langword="true"/>
/// via the ability's constructor. If the WebDriver has the quirk <see cref="BrowserQuirks.RequiresJavascriptToGetLogs"/> then
/// this action will be executed automatically at appropriate times, such as immediately after <see cref="ClickAndWaitForDocumentReady"/>
/// or <see cref="OpenUrl"/> is executed.
/// </para>
/// </remarks>
/// <returns>A performable action</returns>
public static BeginCollectingLogsWithJavaScript BeginCollectingLogsWithJavaScript() => new BeginCollectingLogsWithJavaScript();

/// <summary>
/// Gets a performable question which retrieves the native web browser console logs.
/// </summary>
/// <remarks>
/// <para>
/// It is recommended not to invoke this question directly, rather to use <see cref="GetTheBrowserLogs"/>.
/// The general-purpose GetTheBrowserLogs task will retrieve the logs, selecting an appropriate technique for the current WebDriver implementation.
/// </para>
/// </remarks>
/// <returns>A performable question</returns>
public static GetLogsNatively GetNativeBrowserLogs() => new GetLogsNatively();

/// <summary>
/// Gets a performable question which retrieves the web browser console logs using a Javascript workaround.
/// </summary>
/// <remarks>
/// <para>
/// It is recommended not to invoke this question directly, rather to use <see cref="GetTheBrowserLogs"/>.
/// The general-purpose GetTheBrowserLogs task will retrieve the logs, selecting an appropriate technique for the current WebDriver implementation.
/// </para>
/// </remarks>
/// <returns>A performable question</returns>
public static GetLogsWithJavaScript GetBrowserLogsWithJavascript() => new GetLogsWithJavaScript();

/// <summary>
/// Gets a performable task which retrieves the web browser console logs using an appropriate technique.
/// </summary>
/// <remarks>
/// <para>
/// The task returned by this builder method is the recommended way in which to get the browser console logs.
/// It will pick between the available techniques, based upon the current WebDriver's support.
/// </para>
/// <para>
/// Use the builder object returned by this method to select what should happen in case that the current WebDriver
/// offers no viable technique in which to retrieve the console logs. The default behaviour is to throw <see cref="NotSupportedException"/>,
/// but <see cref="GetTheBrowserLogsBuilder.ButReturnEmptyLogsIfUnsupported"/> may be used to alter this. If that method is used,
/// then an empty collection of logs will be returned in that scenario, instead of throwing.
/// </para>
/// </remarks>
/// <returns>A builder for a performable task</returns>
public static GetTheBrowserLogsBuilder GetTheBrowserLogs() => new GetTheBrowserLogsBuilder();
}
}
Loading
Loading