From a078566bd68d6d922b5090e5da29aeb58ab373de Mon Sep 17 00:00:00 2001 From: Craig Fowler Date: Sat, 23 May 2026 11:03:20 +0100 Subject: [PATCH 1/3] WIP #309 - Add the basic plumbing for this But as-yet, no tests for it. That will need some further work. --- .github/workflows/publishDocsWebsite.yml | 2 +- .../BeginCollectingLogsWithJavaScript.cs | 44 +++++++++++ CSF.Screenplay.Selenium/Actions/OpenUrl.cs | 8 +- CSF.Screenplay.Selenium/BrowseTheWeb.cs | 27 ++++++- CSF.Screenplay.Selenium/BrowserQuirks.cs | 73 ++++++++++++++++--- .../Builders/GetTheBrowserLogsBuilder.cs | 33 +++++++++ .../CSF.Screenplay.Selenium.csproj | 2 +- .../PerformableBuilder.general.cs | 57 +++++++++++++++ .../Questions/BrowserLog.cs | 40 ++++++++++ .../Questions/GetLogsNatively.cs | 40 ++++++++++ .../Questions/GetLogsWithJavaScript.cs | 51 +++++++++++++ .../Resources/CaptureLogs.js | 43 +++++++++++ CSF.Screenplay.Selenium/Resources/GetLogs.js | 8 ++ .../Resources/ScriptResources.cs | 32 +++++++- CSF.Screenplay.Selenium/Scripts.cs | 14 ++++ .../Tasks/ClickAndWaitForDocumentReady.cs | 7 ++ .../Tasks/GetTheBrowserLogs.cs | 65 +++++++++++++++++ .../appsettings.json | 3 +- 18 files changed, 530 insertions(+), 19 deletions(-) create mode 100644 CSF.Screenplay.Selenium/Actions/BeginCollectingLogsWithJavaScript.cs create mode 100644 CSF.Screenplay.Selenium/Builders/GetTheBrowserLogsBuilder.cs create mode 100644 CSF.Screenplay.Selenium/Questions/BrowserLog.cs create mode 100644 CSF.Screenplay.Selenium/Questions/GetLogsNatively.cs create mode 100644 CSF.Screenplay.Selenium/Questions/GetLogsWithJavaScript.cs create mode 100644 CSF.Screenplay.Selenium/Resources/CaptureLogs.js create mode 100644 CSF.Screenplay.Selenium/Resources/GetLogs.js create mode 100644 CSF.Screenplay.Selenium/Tasks/GetTheBrowserLogs.cs diff --git a/.github/workflows/publishDocsWebsite.yml b/.github/workflows/publishDocsWebsite.yml index 72fd0edf..d814609e 100644 --- a/.github/workflows/publishDocsWebsite.yml +++ b/.github/workflows/publishDocsWebsite.yml @@ -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 diff --git a/CSF.Screenplay.Selenium/Actions/BeginCollectingLogsWithJavaScript.cs b/CSF.Screenplay.Selenium/Actions/BeginCollectingLogsWithJavaScript.cs new file mode 100644 index 00000000..576bd96a --- /dev/null +++ b/CSF.Screenplay.Selenium/Actions/BeginCollectingLogsWithJavaScript.cs @@ -0,0 +1,44 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using OpenQA.Selenium; + +namespace CSF.Screenplay.Selenium.Actions +{ + /// + /// Executes a custom Javascript which begins collection/interception of web browser console log messages. + /// + /// + /// + /// When used, this action should be executed as soon as possible after the current page has completed loading. + /// Ideally, directly after . + /// 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: . + /// + /// + /// 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 does not need to be re-run 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). + /// + /// + /// This action is for use only with web browsers which have the quirk. + /// + /// + public class BeginCollectingLogsWithJavaScript : IPerformable, ICanReport + { + /// + public ReportFragment GetReportFragment(Actor actor, IFormatsReportFragment formatter) + => formatter.Format("{Actor} begins collecting browser logs from the current page, using a JavaScript technique"); + + /// + public async ValueTask PerformAsAsync(ICanPerform actor, CancellationToken cancellationToken = default) + { + var driver = actor.GetAbility(); + 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); + } + } +} \ No newline at end of file diff --git a/CSF.Screenplay.Selenium/Actions/OpenUrl.cs b/CSF.Screenplay.Selenium/Actions/OpenUrl.cs index ee74b16d..e9081d2f 100644 --- a/CSF.Screenplay.Selenium/Actions/OpenUrl.cs +++ b/CSF.Screenplay.Selenium/Actions/OpenUrl.cs @@ -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 { @@ -47,13 +49,15 @@ public class OpenUrl : IPerformable, ICanReport readonly NamedUri uri; /// - public ValueTask PerformAsAsync(ICanPerform actor, CancellationToken cancellationToken = default) + public async ValueTask PerformAsAsync(ICanPerform actor, CancellationToken cancellationToken = default) { var ability = actor.GetAbility(); 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); } /// diff --git a/CSF.Screenplay.Selenium/BrowseTheWeb.cs b/CSF.Screenplay.Selenium/BrowseTheWeb.cs index e93d785d..a6bf9672 100644 --- a/CSF.Screenplay.Selenium/BrowseTheWeb.cs +++ b/CSF.Screenplay.Selenium/BrowseTheWeb.cs @@ -84,6 +84,7 @@ namespace CSF.Screenplay.Selenium public class BrowseTheWeb : ICanReport, IDisposable { readonly Lazy webDriverAndOptions; + readonly bool collectLogs; bool disposedValue; /// @@ -91,6 +92,18 @@ public class BrowseTheWeb : ICanReport, IDisposable /// public IWebDriver WebDriver => webDriverAndOptions.Value.WebDriver; + /// + /// Gets a value indicating whether the current WebDriver should go to lengths to collect console log information. + /// + /// + /// + /// When this value is set to , this will trigger usage of + /// at points where it is required. This is applicable only when the current implementation has the + /// quirk . + /// + /// + public bool ShouldCollectLogs => collectLogs; + /// /// Gets the WebDriver options which were used to create . /// @@ -116,9 +129,12 @@ static Lazy GetLazyWebDriverAndOptions(IGetsWebDriver webDr /// 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"); + } /// /// Initializes a new instance of the class. @@ -140,9 +156,12 @@ public ReportFragment GetReportFragment(Actor actor, IFormatsReportFragment form /// /// A universal WebDriver factory instance /// An optional name, specifying the WebDriver configuration (within those available in the factory) to use. - public BrowseTheWeb(IGetsWebDriver webDriverFactory, string webDriverName = null) + /// An optional value indicating whether the Screenplay Selenium extension + /// should go to lengths to ensure that web browser console logs are available. See for more information. + public BrowseTheWeb(IGetsWebDriver webDriverFactory, string webDriverName = null, bool collectLogs = false) { webDriverAndOptions = GetLazyWebDriverAndOptions(webDriverFactory, webDriverName); + this.collectLogs = collectLogs; } /// diff --git a/CSF.Screenplay.Selenium/BrowserQuirks.cs b/CSF.Screenplay.Selenium/BrowserQuirks.cs index 48c50116..160244ba 100644 --- a/CSF.Screenplay.Selenium/BrowserQuirks.cs +++ b/CSF.Screenplay.Selenium/BrowserQuirks.cs @@ -17,6 +17,8 @@ namespace CSF.Screenplay.Selenium /// public static class BrowserQuirks { + const string chrome = "chrome", firefox = "firefox", safari = "safari", edge = "edge"; + /// /// Gets the name of a browser quirk, for browsers which cannot set the value of an <input type="date"> using the /// "Send Keys" technique. @@ -80,7 +82,7 @@ public static class BrowserQuirks public static readonly string NeedsJavaScriptToGetShadowRoot = "NeedsJavaScriptToGetShadowRoot"; /// - /// 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. /// /// /// @@ -90,6 +92,39 @@ public static class BrowserQuirks /// public static readonly string CannotGetShadowRoot = "CannotGetShadowRoot"; + /// + /// Gets the name of a browser quirk, for browsers which have native support for reading the console logs. + /// + /// + /// + /// Browsers with this quirk can read the console logs via a technique such as: + /// + /// + /// var logs = driver.Manage().Logs.GetLog(LogType.Browser); + /// + /// + /// The log levels recorded depend upon the manner in which the WebDriver is configured. + /// If using the , this may be controlled by + /// the property of the configuration. + /// Otherwise, use to configure + /// logging. + /// + /// + public static readonly string HasNativeLogsSupport = "HasNativeLogsSupport"; + + /// + /// Gets the name of a browser quirk, for browsers which can read browser logs, but which require a Javascript-based workaround. + /// + /// + /// + /// Browsers with this quirk cannot read logs in the way that those which 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. + /// + /// + /// + /// + public static readonly string RequiresJavascriptToGetLogs = "RequiresJavascriptToGetLogs"; + /// /// Gets hard-coded information about known browser quirks. /// @@ -113,8 +148,8 @@ public static QuirksData GetQuirksData() { AffectedBrowsers = new HashSet { - new BrowserInfo { Name = "firefox" }, - new BrowserInfo { Name = "safari" }, + new BrowserInfo { Name = firefox }, + new BrowserInfo { Name = safari }, } } }, @@ -124,7 +159,7 @@ public static QuirksData GetQuirksData() { AffectedBrowsers = new HashSet { - new BrowserInfo { Name = "safari" }, + new BrowserInfo { Name = safari }, } } }, @@ -134,10 +169,9 @@ public static QuirksData GetQuirksData() { AffectedBrowsers = new HashSet { - 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" }, } } }, @@ -148,7 +182,28 @@ public static QuirksData GetQuirksData() AffectedBrowsers = new HashSet { // 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 + { + new BrowserInfo { Name = chrome }, + new BrowserInfo { Name = edge }, + } + } + }, + { + RequiresJavascriptToGetLogs, + new BrowserInfoCollection + { + AffectedBrowsers = new HashSet + { + new BrowserInfo { Name = firefox }, } } } diff --git a/CSF.Screenplay.Selenium/Builders/GetTheBrowserLogsBuilder.cs b/CSF.Screenplay.Selenium/Builders/GetTheBrowserLogsBuilder.cs new file mode 100644 index 00000000..a6d47242 --- /dev/null +++ b/CSF.Screenplay.Selenium/Builders/GetTheBrowserLogsBuilder.cs @@ -0,0 +1,33 @@ +using CSF.Screenplay.Selenium.Tasks; + +namespace CSF.Screenplay.Selenium.Builders +{ + /// + /// Builder class for creating an instance of . + /// + /// + /// + /// 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. + /// + /// + public class GetTheBrowserLogsBuilder + { + /// + /// Gets a 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. + /// + /// A performable task + public GetTheBrowserLogs ButReturnEmptyLogsIfUnsupported() + => new GetTheBrowserLogs(false); + + /// + /// Implicitly converts the builder to an instance of . + /// The task will throw if no viable technique for getting logs is available. + /// + /// The builder instance + public static implicit operator GetTheBrowserLogs(GetTheBrowserLogsBuilder builder) + => new GetTheBrowserLogs(); + } +} \ No newline at end of file diff --git a/CSF.Screenplay.Selenium/CSF.Screenplay.Selenium.csproj b/CSF.Screenplay.Selenium/CSF.Screenplay.Selenium.csproj index b854109e..a0f5a8ac 100644 --- a/CSF.Screenplay.Selenium/CSF.Screenplay.Selenium.csproj +++ b/CSF.Screenplay.Selenium/CSF.Screenplay.Selenium.csproj @@ -21,7 +21,7 @@ - + diff --git a/CSF.Screenplay.Selenium/PerformableBuilder.general.cs b/CSF.Screenplay.Selenium/PerformableBuilder.general.cs index 19004ebf..cbb5a0dd 100644 --- a/CSF.Screenplay.Selenium/PerformableBuilder.general.cs +++ b/CSF.Screenplay.Selenium/PerformableBuilder.general.cs @@ -208,5 +208,62 @@ public static NamedWaitBuilder WaitUntil(IBuildsElementPredicates predicate) /// /// A performable question. public static GetWindowTitle ReadTheWindowTitle() => new GetWindowTitle(); + + /// + /// Gets a performable action which begins collecting log information with a Javascript workaround. + /// + /// + /// + /// It is usually not required to use this action directly. + /// The recommended way to consume this action is to set to + /// via the ability's constructor. If the WebDriver has the quirk then + /// this action will be executed automatically at appropriate times, such as immediately after + /// or is executed. + /// + /// + /// A performable action + public static BeginCollectingLogsWithJavaScript BeginCollectingLogsWithJavaScript() => new BeginCollectingLogsWithJavaScript(); + + /// + /// Gets a performable question which retrieves the native web browser console logs. + /// + /// + /// + /// It is recommended not to invoke this question directly, rather to use . + /// The general-purpose GetTheBrowserLogs task will retrieve the logs, selecting an appropriate technique for the current WebDriver implementation. + /// + /// + /// A performable question + public static GetLogsNatively GetNativeBrowserLogs() => new GetLogsNatively(); + + /// + /// Gets a performable question which retrieves the web browser console logs using a Javascript workaround. + /// + /// + /// + /// It is recommended not to invoke this question directly, rather to use . + /// The general-purpose GetTheBrowserLogs task will retrieve the logs, selecting an appropriate technique for the current WebDriver implementation. + /// + /// + /// A performable question + public static GetLogsWithJavaScript GetBrowserLogsWithJavascript() => new GetLogsWithJavaScript(); + + /// + /// Gets a performable task which retrieves the web browser console logs using an appropriate technique. + /// + /// + /// + /// 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. + /// + /// + /// 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 , + /// but 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. + /// + /// + /// A builder for a performable task + public static GetTheBrowserLogsBuilder GetTheBrowserLogs() => new GetTheBrowserLogsBuilder(); } } \ No newline at end of file diff --git a/CSF.Screenplay.Selenium/Questions/BrowserLog.cs b/CSF.Screenplay.Selenium/Questions/BrowserLog.cs new file mode 100644 index 00000000..211ec98a --- /dev/null +++ b/CSF.Screenplay.Selenium/Questions/BrowserLog.cs @@ -0,0 +1,40 @@ +using System; + +namespace CSF.Screenplay.Selenium.Questions +{ + /// + /// Model which represents a single web browser log entry. + /// + public sealed class BrowserLog + { + /// + /// Gets a string description of the 'severity' level at which this item was recorded. + /// + /// + /// + /// The levels and their precise meanings could potentially be implementation/WebDriver-specific. + /// Generally speaking they are quite predictable, and correspond to strings which are equivalent to the members of + /// . + /// + /// + public string Level { get; set; } + + /// + /// The log message, as recorded in the console log. + /// + /// + /// + /// Note that log messages are retrieved from web browsers as pure strings, wheras they may be recorded as mixed strings, values and objects. + /// When using techniques such as , it's important to ensure that the logs retrieved are readable and + /// useful when converted to pure strings. This is a limitation which exists due to the cross-domain (web browser/Javascript and .NET) communication. + /// There is no good one size fits all solution to interpreting arbitrary objects within browser logs, which convert them to useful strings. + /// + /// + public string Message { get; set; } + + /// + /// Gets the timestamp at which the log message was recorded. + /// + public DateTime Timestamp { get; set; } + } +} \ No newline at end of file diff --git a/CSF.Screenplay.Selenium/Questions/GetLogsNatively.cs b/CSF.Screenplay.Selenium/Questions/GetLogsNatively.cs new file mode 100644 index 00000000..d9458315 --- /dev/null +++ b/CSF.Screenplay.Selenium/Questions/GetLogsNatively.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using OpenQA.Selenium; + +namespace CSF.Screenplay.Selenium.Questions +{ + /// + /// A performable which retrieves web browser logs using a native technique. + /// + /// + /// + /// At present, this technique is only verified to work in Chromium-based browsers (Chrome, Edge etc). + /// Note that the logs exposed to this technique are affected by the WebDriver's logging preferences. + /// See for more info. + /// + /// + public class GetLogsNatively : IPerformableWithResult>, ICanReport + { + /// + public ReportFragment GetReportFragment(Actor actor, IFormatsReportFragment formatter) + => formatter.Format("{Actor} reads the logs from the web browser using the native technique"); + + /// + public ValueTask> PerformAsAsync(ICanPerform actor, CancellationToken cancellationToken = default) + { + var driver = actor.GetAbility(); + if(!driver.WebDriver.HasQuirk(BrowserQuirks.HasNativeLogsSupport)) + throw new NotSupportedException("The WebDriver must have support for native log retrieval."); + + var logProvider = driver.WebDriver.Manage()?.Logs; + var logs = logProvider.GetLog(LogType.Browser); + return new ValueTask>(logs + .Select(x => new BrowserLog { Level = x.Level.ToString(), Message = x.Message, Timestamp = x.Timestamp }) + .ToArray()); + } + } +} \ No newline at end of file diff --git a/CSF.Screenplay.Selenium/Questions/GetLogsWithJavaScript.cs b/CSF.Screenplay.Selenium/Questions/GetLogsWithJavaScript.cs new file mode 100644 index 00000000..28017e26 --- /dev/null +++ b/CSF.Screenplay.Selenium/Questions/GetLogsWithJavaScript.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using OpenQA.Selenium; + +namespace CSF.Screenplay.Selenium.Questions +{ + /// + /// A Screenplay Question which retrieves the logs which have been intercepted using a Javascript workaround, via + /// . + /// + /// + /// + /// When used, this question retrieves all logs which have been intercepted since the last time this question was used, + /// or since was first executed on the current web page, whichever is + /// more recent. + /// Repeated use of this question will not return duplicate log messages; messages are marked as 'consumed' as they are returned + /// and previously-consumed messages are filtered-out by the Javascript. + /// + /// + /// Note that because of the imperfect nature of the JavaScript workaround, it is possible for some messages to be missed/lost + /// if they occur very early on in the page-loading sequence, before collection of messages has begun. This is a limitation of + /// the Javascript technique; use a browser which if you need to be sure you are + /// not missing any messages. + /// + /// + /// This action is for use only with web browsers which have the quirk. + /// + /// + public class GetLogsWithJavaScript : IPerformableWithResult>, ICanReport + { + /// + public ReportFragment GetReportFragment(Actor actor, IFormatsReportFragment formatter) + => formatter.Format("{Actor} reads the logs from the web browser using JavaScript"); + + /// + public async ValueTask> PerformAsAsync(ICanPerform actor, CancellationToken cancellationToken = default) + { + var driver = actor.GetAbility(); + if(!driver.WebDriver.HasQuirk(BrowserQuirks.RequiresJavascriptToGetLogs)) + throw new NotSupportedException("The WebDriver must have support for retrieving logs using a Javascript workaround."); + + var result = await actor.PerformAsync(PerformableBuilder.ExecuteAScript(Scripts.GetLogs), cancellationToken); + return result + .Select(x => new BrowserLog { Level = x["Level"].ToString(), Message = x["Message"].ToString(), Timestamp = (DateTime) x["Timestamp"] }) + .ToArray(); + } + } +} \ No newline at end of file diff --git a/CSF.Screenplay.Selenium/Resources/CaptureLogs.js b/CSF.Screenplay.Selenium/Resources/CaptureLogs.js new file mode 100644 index 00000000..5450b8e3 --- /dev/null +++ b/CSF.Screenplay.Selenium/Resources/CaptureLogs.js @@ -0,0 +1,43 @@ +(function() { + if(window['__csfScreenplayLogs']) return; + + window['__csfScreenplayLogs'] = {originalFunctions:{},messages:[]}; + const logs = window['__csfScreenplayLogs']; + + logs.originalFunctions.log = window.console.log; + logs.originalFunctions.info = window.console.info; + logs.originalFunctions.warn = window.console.warn; + logs.originalFunctions.error = window.console.error; + logs.originalFunctions.debug = window.console.debug; + logs.originalFunctions.clear = window.console.clear; + + window.console.log = function(...args) { + captureMessage('Info', args); + logs.originalFunctions.log(...args); + } + window.console.info = function(...args) { + captureMessage('Info', args); + logs.originalFunctions.info(...args); + } + window.console.warn = function(...args) { + captureMessage('Warning', args); + logs.originalFunctions.warn(...args); + } + window.console.error = function(...args) { + captureMessage('Severe', args); + logs.originalFunctions.error(...args); + } + window.console.debug = function(...args) { + captureMessage('Debug', args); + logs.originalFunctions.debug(...args); + } + window.console.clear = function() { + for(const message of logs.messages) + message.Consumed = true; + logs.originalFunctions.clear(); + } + + function captureMessage(level, args) { + logs.messages.push({Level:level,Message:args,Timestamp:new Date()}); + } +})(); diff --git a/CSF.Screenplay.Selenium/Resources/GetLogs.js b/CSF.Screenplay.Selenium/Resources/GetLogs.js new file mode 100644 index 00000000..c49b4522 --- /dev/null +++ b/CSF.Screenplay.Selenium/Resources/GetLogs.js @@ -0,0 +1,8 @@ +return function() { + if(!window['__csfScreenplayLogs']) throw new Error('The CaptureLogs script must have been executed on the current page before GetLogs may be used'); + + const logs = window['__csfScreenplayLogs'].messages.filter(x => !x.Consumed); + for(const log of logs) + log.Consumed = true; + return logs; +}(); diff --git a/CSF.Screenplay.Selenium/Resources/ScriptResources.cs b/CSF.Screenplay.Selenium/Resources/ScriptResources.cs index e85923a1..e9f6dbe8 100644 --- a/CSF.Screenplay.Selenium/Resources/ScriptResources.cs +++ b/CSF.Screenplay.Selenium/Resources/ScriptResources.cs @@ -1,3 +1,5 @@ +using System.IO; +using System.Reflection; using System.Resources; namespace CSF.Screenplay.Selenium.Resources @@ -7,7 +9,8 @@ namespace CSF.Screenplay.Selenium.Resources /// static class ScriptResources { - static readonly ResourceManager resourceManager = new ResourceManager(typeof(ScriptResources).FullName, typeof(ScriptResources).Assembly); + static readonly Assembly thisAssembly = typeof(ScriptResources).Assembly; + static readonly ResourceManager resourceManager = new ResourceManager(typeof(ScriptResources).FullName, thisAssembly); /// Gets a short JavaScript for . internal static string ClearLocalStorage => resourceManager.GetString("ClearLocalStorage"); @@ -23,5 +26,32 @@ static class ScriptResources /// Gets a short JavaScript which gets a Shadow Root node from a Shadow Host element. internal static string GetShadowRoot => resourceManager.GetString("GetShadowRoot"); + + /// Gets a JavaScript which begins capturing logs sent to the browser console. + /// + /// + /// Note that the script cannot read or see any logs which were written before this script was executed. + /// + /// + internal static string CaptureLogs + { + get + { + using (var stream = thisAssembly.GetManifestResourceStream("CSF.Screenplay.Selenium.Resources.CaptureLogs.js")) + using (var reader = new StreamReader(stream)) + return reader.ReadToEnd(); + } + } + + /// Gets a JavaScript which reads logs since either or this method was last called, whichever was more recent. + internal static string GetLogs + { + get + { + using (var stream = thisAssembly.GetManifestResourceStream("CSF.Screenplay.Selenium.Resources.GetLogs.js")) + using (var reader = new StreamReader(stream)) + return reader.ReadToEnd(); + } + } } } \ No newline at end of file diff --git a/CSF.Screenplay.Selenium/Scripts.cs b/CSF.Screenplay.Selenium/Scripts.cs index 486720a7..d9df3d6d 100644 --- a/CSF.Screenplay.Selenium/Scripts.cs +++ b/CSF.Screenplay.Selenium/Scripts.cs @@ -1,3 +1,5 @@ +using System.Collections.Generic; + namespace CSF.Screenplay.Selenium { /// @@ -47,5 +49,17 @@ public static NamedScriptWithResult GetTheDocumentReadyState /// public static NamedScriptWithResult GetShadowRoot => new NamedScriptWithResult(Resources.ScriptResources.GetShadowRoot, "get a shadow root"); + + /// + /// Gets a which begins capturing logs from the web browser console. + /// + public static NamedScript CaptureLogs + => new NamedScript(Resources.ScriptResources.CaptureLogs, "begin capturing logs"); + + /// + /// Gets a which reads log entries which have previously been captured with . + /// + public static NamedScriptWithResult>> GetLogs + => new NamedScriptWithResult>>(Resources.ScriptResources.GetLogs, "read the captured logs"); } } \ No newline at end of file diff --git a/CSF.Screenplay.Selenium/Tasks/ClickAndWaitForDocumentReady.cs b/CSF.Screenplay.Selenium/Tasks/ClickAndWaitForDocumentReady.cs index 6d5b5c8f..84f6d649 100644 --- a/CSF.Screenplay.Selenium/Tasks/ClickAndWaitForDocumentReady.cs +++ b/CSF.Screenplay.Selenium/Tasks/ClickAndWaitForDocumentReady.cs @@ -80,6 +80,13 @@ public ReportFragment GetReportFragment(Actor actor, Lazy eleme public async ValueTask PerformAsAsync(ICanPerform actor, IWebDriver webDriver, Lazy element, CancellationToken cancellationToken = default) { await actor.PerformAsync(ClickOn(element.Value), cancellationToken); + await WaitForPageLoad(actor, webDriver, element, cancellationToken); + if(actor.GetAbility().ShouldCollectLogs && webDriver.HasQuirk(BrowserQuirks.RequiresJavascriptToGetLogs)) + await actor.PerformAsync(BeginCollectingLogsWithJavaScript(), cancellationToken); + } + + async ValueTask WaitForPageLoad(ICanPerform actor, IWebDriver webDriver, Lazy element, CancellationToken cancellationToken) + { if(!webDriver.HasQuirk(BrowserQuirks.NeedsToWaitAfterPageLoad)) return; var timeout = GetTimeout(actor); diff --git a/CSF.Screenplay.Selenium/Tasks/GetTheBrowserLogs.cs b/CSF.Screenplay.Selenium/Tasks/GetTheBrowserLogs.cs new file mode 100644 index 00000000..737f91d4 --- /dev/null +++ b/CSF.Screenplay.Selenium/Tasks/GetTheBrowserLogs.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using CSF.Screenplay.Selenium.Questions; +using OpenQA.Selenium; +using static CSF.Screenplay.Selenium.PerformableBuilder; + +namespace CSF.Screenplay.Selenium.Tasks +{ + /// + /// A screenplay task which attempts to get the web browser console logs using the best technique available. + /// + /// + /// + /// The order of precedence is: + /// + /// + /// If the WebDriver has the quirk/capability then + /// is used. + /// If the WebDriver has the quirk/capability then + /// is used. + /// If the current instance has been constructed in such a manner as to throw if getting logs is unsupported + /// then this task will throw . + /// If the current instance has been constructed not to throw if unsupported, then if neither technique + /// of getting logs is available, this task will always return an empty collection of log entries. + /// + /// + public class GetTheBrowserLogs : IPerformableWithResult>, ICanReport + { + readonly bool throwIfUnsupported; + + /// + public ReportFragment GetReportFragment(Actor actor, IFormatsReportFragment formatter) + { + throw new System.NotImplementedException(); + } + + /// + public ValueTask> PerformAsAsync(ICanPerform actor, CancellationToken cancellationToken = default) + { + var ability = actor.GetAbility(); + if(ability.WebDriver.HasQuirk(BrowserQuirks.HasNativeLogsSupport)) + return actor.PerformAsync(GetNativeBrowserLogs(), cancellationToken); + + if(ability.WebDriver.HasQuirk(BrowserQuirks.RequiresJavascriptToGetLogs)) + return actor.PerformAsync(GetBrowserLogsWithJavascript(), cancellationToken); + + if(throwIfUnsupported) + throw new NotSupportedException("The current WebDriver does not support retrieving console logs, and throwIfUnsupported is set to true"); + + return new ValueTask>(Array.Empty()); + } + + /// + /// Constructs an instance of . + /// + /// If true, performing this task will throw when the current WebDriver does not support retrieving + /// console logs; otherwise an empty collection will be returned. + public GetTheBrowserLogs(bool throwIfUnsupported = true) + { + this.throwIfUnsupported = throwIfUnsupported; + } + } +} \ No newline at end of file diff --git a/Tests/CSF.Screenplay.Selenium.Tests/appsettings.json b/Tests/CSF.Screenplay.Selenium.Tests/appsettings.json index 09804c0a..f1cc7c96 100644 --- a/Tests/CSF.Screenplay.Selenium.Tests/appsettings.json +++ b/Tests/CSF.Screenplay.Selenium.Tests/appsettings.json @@ -2,7 +2,8 @@ "WebDriverFactory": { "DriverConfigurations": { "DefaultChrome": { - "DriverType": "ChromeDriver" + "DriverType": "ChromeDriver", + "BrowserLogLevel": "Debug" }, "VerboseChrome": { "DriverType": "ChromeDriver", From abf9edf04e411e51031d2a9164a67d636a61e85b Mon Sep 17 00:00:00 2001 From: Craig Fowler Date: Sat, 23 May 2026 20:29:27 +0100 Subject: [PATCH 2/3] Fix tech issue: globalThis --- .../Resources/CaptureLogs.js | 30 +++++++++---------- CSF.Screenplay.Selenium/Resources/GetLogs.js | 4 +-- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/CSF.Screenplay.Selenium/Resources/CaptureLogs.js b/CSF.Screenplay.Selenium/Resources/CaptureLogs.js index 5450b8e3..1b52fe3f 100644 --- a/CSF.Screenplay.Selenium/Resources/CaptureLogs.js +++ b/CSF.Screenplay.Selenium/Resources/CaptureLogs.js @@ -1,37 +1,37 @@ (function() { - if(window['__csfScreenplayLogs']) return; + if(globalThis['__csfScreenplayLogs']) return; - window['__csfScreenplayLogs'] = {originalFunctions:{},messages:[]}; - const logs = window['__csfScreenplayLogs']; + globalThis['__csfScreenplayLogs'] = {originalFunctions:{},messages:[]}; + const logs = globalThis['__csfScreenplayLogs']; - logs.originalFunctions.log = window.console.log; - logs.originalFunctions.info = window.console.info; - logs.originalFunctions.warn = window.console.warn; - logs.originalFunctions.error = window.console.error; - logs.originalFunctions.debug = window.console.debug; - logs.originalFunctions.clear = window.console.clear; + logs.originalFunctions.log = globalThis.console.log; + logs.originalFunctions.info = globalThis.console.info; + logs.originalFunctions.warn = globalThis.console.warn; + logs.originalFunctions.error = globalThis.console.error; + logs.originalFunctions.debug = globalThis.console.debug; + logs.originalFunctions.clear = globalThis.console.clear; - window.console.log = function(...args) { + globalThis.console.log = function(...args) { captureMessage('Info', args); logs.originalFunctions.log(...args); } - window.console.info = function(...args) { + globalThis.console.info = function(...args) { captureMessage('Info', args); logs.originalFunctions.info(...args); } - window.console.warn = function(...args) { + globalThis.console.warn = function(...args) { captureMessage('Warning', args); logs.originalFunctions.warn(...args); } - window.console.error = function(...args) { + globalThis.console.error = function(...args) { captureMessage('Severe', args); logs.originalFunctions.error(...args); } - window.console.debug = function(...args) { + globalThis.console.debug = function(...args) { captureMessage('Debug', args); logs.originalFunctions.debug(...args); } - window.console.clear = function() { + globalThis.console.clear = function() { for(const message of logs.messages) message.Consumed = true; logs.originalFunctions.clear(); diff --git a/CSF.Screenplay.Selenium/Resources/GetLogs.js b/CSF.Screenplay.Selenium/Resources/GetLogs.js index c49b4522..9e201dc9 100644 --- a/CSF.Screenplay.Selenium/Resources/GetLogs.js +++ b/CSF.Screenplay.Selenium/Resources/GetLogs.js @@ -1,7 +1,7 @@ return function() { - if(!window['__csfScreenplayLogs']) throw new Error('The CaptureLogs script must have been executed on the current page before GetLogs may be used'); + if(!globalThis['__csfScreenplayLogs']) throw new Error('The CaptureLogs script must have been executed on the current page before GetLogs may be used'); - const logs = window['__csfScreenplayLogs'].messages.filter(x => !x.Consumed); + const logs = globalThis['__csfScreenplayLogs'].messages.filter(x => !x.Consumed); for(const log of logs) log.Consumed = true; return logs; From de55905d9648a4f35095140d5832367c52217c8d Mon Sep 17 00:00:00 2001 From: Craig Fowler Date: Sun, 24 May 2026 09:37:14 +0100 Subject: [PATCH 3/3] WIP #309 - Add test exclusion and some minor cleanup --- .sonarqube-analysisproperties.xml | 2 +- CSF.Screenplay.Selenium/Builders/GetTheBrowserLogsBuilder.cs | 3 +-- CSF.Screenplay.Selenium/Questions/GetLogsNatively.cs | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/.sonarqube-analysisproperties.xml b/.sonarqube-analysisproperties.xml index 61a28780..161b3332 100644 --- a/.sonarqube-analysisproperties.xml +++ b/.sonarqube-analysisproperties.xml @@ -2,7 +2,7 @@ - Tests\**\*,**\*Exception.cs,**\*.spec.js,**\*.config.js + Tests\**\*,**\*Exception.cs,**\*.spec.js,**\*.config.js,CSF.Screenplay.Selenium\Resources\**\*.js docs\**\*,*_old\**\* Tests\**\*,**\*.spec.js Tests\**\TestResults.xml diff --git a/CSF.Screenplay.Selenium/Builders/GetTheBrowserLogsBuilder.cs b/CSF.Screenplay.Selenium/Builders/GetTheBrowserLogsBuilder.cs index a6d47242..dd64ad36 100644 --- a/CSF.Screenplay.Selenium/Builders/GetTheBrowserLogsBuilder.cs +++ b/CSF.Screenplay.Selenium/Builders/GetTheBrowserLogsBuilder.cs @@ -26,8 +26,7 @@ public GetTheBrowserLogs ButReturnEmptyLogsIfUnsupported() /// Implicitly converts the builder to an instance of . /// The task will throw if no viable technique for getting logs is available. /// - /// The builder instance - public static implicit operator GetTheBrowserLogs(GetTheBrowserLogsBuilder builder) + public static implicit operator GetTheBrowserLogs(GetTheBrowserLogsBuilder _) => new GetTheBrowserLogs(); } } \ No newline at end of file diff --git a/CSF.Screenplay.Selenium/Questions/GetLogsNatively.cs b/CSF.Screenplay.Selenium/Questions/GetLogsNatively.cs index d9458315..e998a164 100644 --- a/CSF.Screenplay.Selenium/Questions/GetLogsNatively.cs +++ b/CSF.Screenplay.Selenium/Questions/GetLogsNatively.cs @@ -30,7 +30,7 @@ public ValueTask> PerformAsAsync(ICanPerform actor, Ca if(!driver.WebDriver.HasQuirk(BrowserQuirks.HasNativeLogsSupport)) throw new NotSupportedException("The WebDriver must have support for native log retrieval."); - var logProvider = driver.WebDriver.Manage()?.Logs; + var logProvider = driver.WebDriver.Manage().Logs; var logs = logProvider.GetLog(LogType.Browser); return new ValueTask>(logs .Select(x => new BrowserLog { Level = x.Level.ToString(), Message = x.Message, Timestamp = x.Timestamp })