Skip to content

Commit 9e0043c

Browse files
authored
Merge pull request #349 from csf-dev/craigfowler/issue309
Resolves #309 - Adds mechanism by which to capture and read browser logs
2 parents 9c605b3 + 758c6ce commit 9e0043c

25 files changed

Lines changed: 921 additions & 26 deletions

.github/workflows/publishDocsWebsite.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,6 @@ jobs:
4141
committer_email: github-actions-workflow@bots.noreply.github.com
4242
fetch: true
4343
message: Publish updated documentation website
44-
push: origin master --force
44+
push: origin master
4545

4646

.sonarqube-analysisproperties.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<SonarQubeAnalysisProperties xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
33
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
44
xmlns="http://www.sonarsource.com/msbuild/integration/2015/1">
5-
<Property Name="sonar.coverage.exclusions">Tests\**\*,**\*Exception.cs,**\*.spec.js,**\*.config.js</Property>
5+
<Property Name="sonar.coverage.exclusions">Tests\**\*,**\*Exception.cs,**\*.spec.js,**\*.config.js,CSF.Screenplay.Selenium\Resources\**\*.js</Property>
66
<Property Name="sonar.exclusions">docs\**\*,*_old\**\*</Property>
77
<Property Name="sonar.cpd.exclusions">Tests\**\*,**\*.spec.js</Property>
88
<Property Name="sonar.cs.nunit.reportsPaths">Tests\**\TestResults.xml</Property>
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
using System;
2+
using System.Threading;
3+
using System.Threading.Tasks;
4+
using OpenQA.Selenium;
5+
6+
namespace CSF.Screenplay.Selenium.Actions
7+
{
8+
/// <summary>
9+
/// Executes a custom Javascript which begins collection/interception of web browser console log messages.
10+
/// </summary>
11+
/// <remarks>
12+
/// <para>
13+
/// When used, this action should be executed as soon as possible after the current page has completed loading.
14+
/// Ideally, directly after <see cref="Tasks.ClickAndWaitForDocumentReady"/>.
15+
/// Any log messages which have been sent to the native browser console before this action is executed will be missed
16+
/// and will not be available to the counterpart question which retrieves log messages: <see cref="Questions.GetLogsWithJavaScript"/>.
17+
/// </para>
18+
/// <para>
19+
/// Note that this action/the script needs to be re-run after each traditional web page navigation/reload.
20+
/// However, due to the nature of SPAs, it <em>does not need to be re-run</em> following an SPA-style navigation.
21+
/// On supported browsers, there is no harm in re-running this script when it is not needed, except for the impact on
22+
/// performance (wasted network roundtrips).
23+
/// </para>
24+
/// <para>
25+
/// This action is for use only with web browsers which have the <see cref="BrowserQuirks.CanGetLogsWithJavascriptWorkaround"/> quirk.
26+
/// </para>
27+
/// </remarks>
28+
public class BeginCollectingLogsWithJavaScript : IPerformable, ICanReport
29+
{
30+
/// <inheritdoc/>
31+
public ReportFragment GetReportFragment(Actor actor, IFormatsReportFragment formatter)
32+
=> formatter.Format("{Actor} begins collecting browser logs from the current page, using a JavaScript technique");
33+
34+
/// <inheritdoc/>
35+
public async ValueTask PerformAsAsync(ICanPerform actor, CancellationToken cancellationToken = default)
36+
{
37+
var driver = actor.GetAbility<BrowseTheWeb>();
38+
if(!driver.WebDriver.HasQuirk(BrowserQuirks.CanGetLogsWithJavascriptWorkaround))
39+
throw new NotSupportedException("The WebDriver must have support for retrieving logs using a Javascript workaround.");
40+
41+
await actor.PerformAsync(PerformableBuilder.ExecuteAScript(Scripts.CaptureLogs), cancellationToken);
42+
}
43+
}
44+
}

CSF.Screenplay.Selenium/Actions/OpenUrl.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
using System;
22
using System.Threading;
33
using System.Threading.Tasks;
4+
using OpenQA.Selenium;
5+
using static CSF.Screenplay.Selenium.PerformableBuilder;
46

57
namespace CSF.Screenplay.Selenium.Actions
68
{
@@ -47,13 +49,15 @@ public class OpenUrl : IPerformable, ICanReport
4749
readonly NamedUri uri;
4850

4951
/// <inheritdoc/>
50-
public ValueTask PerformAsAsync(ICanPerform actor, CancellationToken cancellationToken = default)
52+
public async ValueTask PerformAsAsync(ICanPerform actor, CancellationToken cancellationToken = default)
5153
{
5254
var ability = actor.GetAbility<BrowseTheWeb>();
5355
if(!uri.Uri.IsAbsoluteUri)
5456
throw new InvalidOperationException($"The URL to open must be absolute; have you forgotten to grant {actor} the {nameof(UseABaseUri)} ability?");
5557
ability.WebDriver.Url = uri.Uri.ToString();
56-
return default;
58+
59+
if(ability.ShouldCollectLogs && ability.WebDriver.HasQuirk(BrowserQuirks.CanGetLogsWithJavascriptWorkaround))
60+
await actor.PerformAsync(BeginCollectingLogsWithJavaScript(), cancellationToken);
5761
}
5862

5963
/// <inheritdoc/>

CSF.Screenplay.Selenium/BrowseTheWeb.cs

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -84,13 +84,26 @@ namespace CSF.Screenplay.Selenium
8484
public class BrowseTheWeb : ICanReport, IDisposable
8585
{
8686
readonly Lazy<WebDriverAndOptions> webDriverAndOptions;
87+
readonly bool collectLogs;
8788
bool disposedValue;
8889

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

95+
/// <summary>
96+
/// Gets a value indicating whether the current WebDriver should go to lengths to collect console log information.
97+
/// </summary>
98+
/// <remarks>
99+
/// <para>
100+
/// When this value is set to <see langword="true"/>, this will trigger usage of <see cref="Actions.BeginCollectingLogsWithJavaScript"/>
101+
/// at points where it is required. This is applicable only when the current <see cref="WebDriver"/> implementation has the
102+
/// quirk <see cref="BrowserQuirks.CanGetLogsWithJavascriptWorkaround"/>.
103+
/// </para>
104+
/// </remarks>
105+
public bool ShouldCollectLogs => collectLogs;
106+
94107
/// <summary>
95108
/// Gets the WebDriver options which were used to create <see cref="WebDriver"/>.
96109
/// </summary>
@@ -116,9 +129,12 @@ static Lazy<WebDriverAndOptions> GetLazyWebDriverAndOptions(IGetsWebDriver webDr
116129

117130
/// <inheritdoc/>
118131
public ReportFragment GetReportFragment(Actor actor, IFormatsReportFragment formatter)
119-
=> formatter.Format("{Actor} is able to browse the web using {BrowserName}",
120-
actor.Name,
121-
WebDriver.GetBrowserId()?.ToString() ?? "a Selenium WebDriver");
132+
{
133+
var logsSuffix = ShouldCollectLogs ? ", and do their best to collect console logs" : string.Empty;
134+
return formatter.Format("{Actor} is able to browse the web using {BrowserName}" + logsSuffix,
135+
actor.Name,
136+
WebDriver.GetBrowserId()?.ToString() ?? "a Selenium WebDriver");
137+
}
122138

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

148167
/// <summary>

CSF.Screenplay.Selenium/BrowserQuirks.cs

Lines changed: 66 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ namespace CSF.Screenplay.Selenium
1717
/// <seealso cref="IHasQuirks"/>
1818
public static class BrowserQuirks
1919
{
20+
const string chrome = "chrome", firefox = "firefox", safari = "safari", edge = "edge";
21+
2022
/// <summary>
2123
/// 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
2224
/// "Send Keys" technique.
@@ -80,7 +82,7 @@ public static class BrowserQuirks
8082
public static readonly string NeedsJavaScriptToGetShadowRoot = "NeedsJavaScriptToGetShadowRoot";
8183

8284
/// <summary>
83-
/// Gets the name of a browser quirk, for browser which cannot get a Shadow Root node at all.
85+
/// Gets the name of a browser quirk, for browsers which cannot get a Shadow Root node at all.
8486
/// </summary>
8587
/// <remarks>
8688
/// <para>
@@ -90,6 +92,39 @@ public static class BrowserQuirks
9092
/// </remarks>
9193
public static readonly string CannotGetShadowRoot = "CannotGetShadowRoot";
9294

95+
/// <summary>
96+
/// Gets the name of a browser quirk, for browsers which have native support for reading the console logs.
97+
/// </summary>
98+
/// <remarks>
99+
/// <para>
100+
/// Browsers with this quirk can read the console logs via a technique such as:
101+
/// </para>
102+
/// <code>
103+
/// var logs = driver.Manage().Logs.GetLog(LogType.Browser);
104+
/// </code>
105+
/// <para>
106+
/// The log levels recorded depend upon the manner in which the WebDriver is configured.
107+
/// If using the <see cref="Extensions.WebDriver.WebDriverFactory"/>, this may be controlled by
108+
/// the <see cref="Extensions.WebDriver.Factories.WebDriverCreationOptions.BrowserLogLevel"/> property of the configuration.
109+
/// Otherwise, use <see cref="OpenQA.Selenium.DriverOptions.SetLoggingPreference(string, OpenQA.Selenium.LogLevel)"/> to configure
110+
/// logging.
111+
/// </para>
112+
/// </remarks>
113+
public static readonly string HasNativeLogsSupport = "HasNativeLogsSupport";
114+
115+
/// <summary>
116+
/// Gets the name of a browser quirk, for browsers which can read browser logs using a Javascript-based workaround.
117+
/// </summary>
118+
/// <remarks>
119+
/// <para>
120+
/// Browsers with this quirk should prefer using the techniques provided by <see cref="HasNativeLogsSupport"/> if they also support them.
121+
/// The use of a Javascript workaround is imperfect and can miss log messages, as described in the documentation for <see cref="GetLogsWithJavaScript"/>.
122+
/// </para>
123+
/// </remarks>
124+
/// <seealso cref="Scripts.CaptureLogs"/>
125+
/// <seealso cref="Scripts.GetLogs"/>
126+
public static readonly string CanGetLogsWithJavascriptWorkaround = "CanGetLogsWithJavascriptWorkaround";
127+
93128
/// <summary>
94129
/// Gets hard-coded information about known browser quirks.
95130
/// </summary>
@@ -113,8 +148,8 @@ public static QuirksData GetQuirksData()
113148
{
114149
AffectedBrowsers = new HashSet<BrowserInfo>
115150
{
116-
new BrowserInfo { Name = "firefox" },
117-
new BrowserInfo { Name = "safari" },
151+
new BrowserInfo { Name = firefox },
152+
new BrowserInfo { Name = safari },
118153
}
119154
}
120155
},
@@ -124,7 +159,7 @@ public static QuirksData GetQuirksData()
124159
{
125160
AffectedBrowsers = new HashSet<BrowserInfo>
126161
{
127-
new BrowserInfo { Name = "safari" },
162+
new BrowserInfo { Name = safari },
128163
}
129164
}
130165
},
@@ -134,10 +169,9 @@ public static QuirksData GetQuirksData()
134169
{
135170
AffectedBrowsers = new HashSet<BrowserInfo>
136171
{
137-
new BrowserInfo { Name = "safari" },
138-
// There is no Chrome 95.1.0.0 but this covers any 95.0.x
139-
// The additional trailing zeroes are to work around https://github.com/csf-dev/CSF.Extensions.WebDriver/issues/56
140-
new BrowserInfo { Name = "chrome", MaxVersion = "95.1.0.0" },
172+
new BrowserInfo { Name = safari },
173+
// There is no Chrome 95.1 but this covers any 95.0.x
174+
new BrowserInfo { Name = chrome, MaxVersion = "95.1" },
141175
}
142176
}
143177
},
@@ -148,7 +182,30 @@ public static QuirksData GetQuirksData()
148182
AffectedBrowsers = new HashSet<BrowserInfo>
149183
{
150184
// There is no Firefox 112.1 but this covers any 112.0.x
151-
new BrowserInfo { Name = "firefox", MaxVersion = "112.1" }
185+
new BrowserInfo { Name = firefox, MaxVersion = "112.1" },
186+
}
187+
}
188+
},
189+
{
190+
HasNativeLogsSupport,
191+
new BrowserInfoCollection
192+
{
193+
AffectedBrowsers = new HashSet<BrowserInfo>
194+
{
195+
new BrowserInfo { Name = chrome },
196+
new BrowserInfo { Name = edge },
197+
}
198+
}
199+
},
200+
{
201+
CanGetLogsWithJavascriptWorkaround,
202+
new BrowserInfoCollection
203+
{
204+
AffectedBrowsers = new HashSet<BrowserInfo>
205+
{
206+
new BrowserInfo { Name = firefox },
207+
new BrowserInfo { Name = chrome },
208+
new BrowserInfo { Name = edge },
152209
}
153210
}
154211
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
using System.Collections.Generic;
2+
using CSF.Screenplay.Performables;
3+
using CSF.Screenplay.Selenium.Questions;
4+
using CSF.Screenplay.Selenium.Tasks;
5+
6+
namespace CSF.Screenplay.Selenium.Builders
7+
{
8+
/// <summary>
9+
/// Builder class for creating an instance of <see cref="GetTheBrowserLogs"/>.
10+
/// </summary>
11+
/// <remarks>
12+
/// <para>
13+
/// Primarily, this is concerned with whether the constructed task should throw or silently return an empty
14+
/// collection, should retrieving logs be unsupported by the current WebDriver.
15+
/// </para>
16+
/// </remarks>
17+
public class GetTheBrowserLogsBuilder : IGetsPerformableWithResult<IReadOnlyList<BrowserLog>>
18+
{
19+
/// <summary>
20+
/// Gets a <see cref="GetTheBrowserLogs"/> task.
21+
/// The task will be configured such that - if no viable technique for getting logs is available - it will return
22+
/// an empty collection instead of throwing.
23+
/// </summary>
24+
/// <returns>A performable task</returns>
25+
public GetTheBrowserLogs ButReturnEmptyLogsIfUnsupported()
26+
=> new GetTheBrowserLogs(false);
27+
28+
IPerformableWithResult<IReadOnlyList<BrowserLog>> IGetsPerformableWithResult<IReadOnlyList<BrowserLog>>.GetPerformable()
29+
=> new GetTheBrowserLogs();
30+
}
31+
}

CSF.Screenplay.Selenium/CSF.Screenplay.Selenium.csproj

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
<PackageReference Include="Microsoft.Bcl.HashCode" Version="1.0.0" Condition="'$(TargetFramework)' == 'netstandard2.0' or '$(TargetFramework)' == '$(DotNetFrameworkLegacy)'" />
2222
<PackageReference Include="Selenium.WebDriver" Version="4.0.0" />
2323
<PackageReference Include="Selenium.Support" Version="4.0.0" />
24-
<PackageReference Include="CSF.Extensions.WebDriver" Version="2.0.0-6.beta" />
24+
<PackageReference Include="CSF.Extensions.WebDriver" Version="2.0.0-7.beta" />
2525
<PackageReference Include="CSF.Specifications" Version="2.0.0" />
2626
</ItemGroup>
2727

@@ -32,6 +32,7 @@
3232

3333
<ItemGroup>
3434
<EmbeddedResource Include="**\*.restext" />
35+
<EmbeddedResource Include="Resources\**\*.js" />
3536
</ItemGroup>
3637

3738
</Project>

CSF.Screenplay.Selenium/PerformableBuilder.general.cs

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,5 +208,62 @@ public static NamedWaitBuilder WaitUntil(IBuildsElementPredicates predicate)
208208
/// </summary>
209209
/// <returns>A performable question.</returns>
210210
public static GetWindowTitle ReadTheWindowTitle() => new GetWindowTitle();
211+
212+
/// <summary>
213+
/// Gets a performable action which begins collecting log information with a Javascript workaround.
214+
/// </summary>
215+
/// <remarks>
216+
/// <para>
217+
/// It is usually not required to use this action directly.
218+
/// The recommended way to consume this action is to set <see cref="BrowseTheWeb.ShouldCollectLogs"/> to <see langword="true"/>
219+
/// via the ability's constructor. If the WebDriver has the quirk <see cref="BrowserQuirks.CanGetLogsWithJavascriptWorkaround"/> then
220+
/// this action will be executed automatically at appropriate times, such as immediately after <see cref="ClickAndWaitForDocumentReady"/>
221+
/// or <see cref="OpenUrl"/> is executed.
222+
/// </para>
223+
/// </remarks>
224+
/// <returns>A performable action</returns>
225+
public static BeginCollectingLogsWithJavaScript BeginCollectingLogsWithJavaScript() => new BeginCollectingLogsWithJavaScript();
226+
227+
/// <summary>
228+
/// Gets a performable question which retrieves the native web browser console logs.
229+
/// </summary>
230+
/// <remarks>
231+
/// <para>
232+
/// It is recommended not to invoke this question directly, rather to use <see cref="GetTheBrowserLogs"/>.
233+
/// The general-purpose GetTheBrowserLogs task will retrieve the logs, selecting an appropriate technique for the current WebDriver implementation.
234+
/// </para>
235+
/// </remarks>
236+
/// <returns>A performable question</returns>
237+
public static GetLogsNatively GetNativeBrowserLogs() => new GetLogsNatively();
238+
239+
/// <summary>
240+
/// Gets a performable question which retrieves the web browser console logs using a Javascript workaround.
241+
/// </summary>
242+
/// <remarks>
243+
/// <para>
244+
/// It is recommended not to invoke this question directly, rather to use <see cref="GetTheBrowserLogs"/>.
245+
/// The general-purpose GetTheBrowserLogs task will retrieve the logs, selecting an appropriate technique for the current WebDriver implementation.
246+
/// </para>
247+
/// </remarks>
248+
/// <returns>A performable question</returns>
249+
public static GetLogsWithJavaScript GetBrowserLogsWithJavascript() => new GetLogsWithJavaScript();
250+
251+
/// <summary>
252+
/// Gets a performable task which retrieves the web browser console logs using an appropriate technique.
253+
/// </summary>
254+
/// <remarks>
255+
/// <para>
256+
/// The task returned by this builder method is the recommended way in which to get the browser console logs.
257+
/// It will pick between the available techniques, based upon the current WebDriver's support.
258+
/// </para>
259+
/// <para>
260+
/// Use the builder object returned by this method to select what should happen in case that the current WebDriver
261+
/// offers no viable technique in which to retrieve the console logs. The default behaviour is to throw <see cref="NotSupportedException"/>,
262+
/// but <see cref="GetTheBrowserLogsBuilder.ButReturnEmptyLogsIfUnsupported"/> may be used to alter this. If that method is used,
263+
/// then an empty collection of logs will be returned in that scenario, instead of throwing.
264+
/// </para>
265+
/// </remarks>
266+
/// <returns>A builder for a performable task</returns>
267+
public static GetTheBrowserLogsBuilder GetTheBrowserLogs() => new GetTheBrowserLogsBuilder();
211268
}
212269
}

0 commit comments

Comments
 (0)