diff --git a/.appveyor.yml b/.appveyor.yml index fa45fc94..5e17168e 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -21,14 +21,18 @@ install: cd CSF.Screenplay.JsonToHtmlReport.Template\src npm ci cd ..\.. + # This was taken from https://stackoverflow.com/questions/60304251/unable-to-open-x-display-when-trying-to-run-google-chrome-on-centos-rhel-7-5 + # It's the minimum dependencies for running Chrome in a headless environment on Linux + - sh: | + sudo apt-get update + sudo apt install -y xorg xvfb gtk2-engines-pixbuf dbus-x11 xfonts-base xfonts-100dpi xfonts-75dpi xfonts-cyrillic xfonts-scalable before_build: - dotnet --version - dotnet restore --verbosity m - dotnet clean - - cmd: > - IF NOT DEFINED APPVEYOR_PULL_REQUEST_HEAD_REPO_BRANCH (SET BranchName=%APPVEYOR_REPO_BRANCH%) - ELSE (SET BranchName=%APPVEYOR_PULL_REQUEST_HEAD_REPO_BRANCH%) + - cmd: Tools\appveyor-setup-sonarscanner.bat + - cmd: Tools\appveyor-setup-selenium.bat - cmd: > dotnet-sonarscanner begin /k:"csf-dev_CSF.Screenplay" @@ -36,9 +40,14 @@ before_build: /o:craigfowler-github /d:sonar.host.url=https://sonarcloud.io /d:sonar.token=%SONARCLOUD_SECRET_KEY% - /d:sonar.branch.name=%BranchName% + /d:%BranchParam%=%BranchName% + %PRParam% /d:sonar.javascript.lcov.reportPaths=%APPVEYOR_BUILD_FOLDER%\CSF.Screenplay.JsonToHtmlReport.Template\src\TestResults\lcov.info /s:%APPVEYOR_BUILD_FOLDER%\.sonarqube-analysisproperties.xml + # Activate Xvfb and export a display so that Chrome can run in Linux + - sh: | + Xvfb -ac :99 -screen 0 1280x1024x16 & + export DISPLAY=:99 build_script: - dotnet build --no-incremental @@ -61,3 +70,7 @@ after_test: - ps: if ($isWindows) { Tools\appveyor-upload-test-results.ps1 } - cmd: dotnet build -c Docs - ps: if ($isWindows) { Tools\appveyor_publish_docs.ps1 } + +artifacts: + - path: Tests\**\ScreenplayReport_*.json + name: Screenplay report diff --git a/.gitignore b/.gitignore index c3b716f9..51db7c89 100644 --- a/.gitignore +++ b/.gitignore @@ -4,5 +4,8 @@ obj/ TestResults/ Tests/**/*.feature.cs node_modules/ +*.orig /CSF.Screenplay.JsonToHtmlReport/template/ /CSF.Screenplay.JsonToHtmlReport.Template/src/output/ +**/ScreenplayReport* +**/TestResult.xml diff --git a/.sonarqube-analysisproperties.xml b/.sonarqube-analysisproperties.xml index f923c073..61a28780 100644 --- a/.sonarqube-analysisproperties.xml +++ b/.sonarqube-analysisproperties.xml @@ -2,9 +2,12 @@ - Tests/**/*,**/*Exception.cs,*_old/**/*,**/*.spec.js,**/*.config.js - Tests/**/*,*_old/**/*,**/*.spec.js + Tests\**\*,**\*Exception.cs,**\*.spec.js,**\*.config.js + docs\**\*,*_old\**\* + Tests\**\*,**\*.spec.js Tests\**\TestResults.xml TestResults\*.opencover.xml false + Tests\**\*,**\*.spec.js + true \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000..074847c3 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,15 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "dotnet", + "projectPath": "${workspaceFolder}/Tests/CSF.Screenplay.Selenium.TestWebapp/CSF.Screenplay.Selenium.TestWebapp.csproj", + "name": "Selenium testing web app", + "request": "launch", + + } + ] +} \ No newline at end of file diff --git a/CSF.Screenplay.Abstractions/Abilities/GetAssetFilePaths.cs b/CSF.Screenplay.Abstractions/Abilities/GetAssetFilePaths.cs new file mode 100644 index 00000000..4ab345a6 --- /dev/null +++ b/CSF.Screenplay.Abstractions/Abilities/GetAssetFilePaths.cs @@ -0,0 +1,39 @@ +using CSF.Screenplay.Reporting; + +namespace CSF.Screenplay.Abilities +{ + /// + /// Screenplay ability which gets the file system path for asset files generated by actors participating in the current performance. + /// + public class GetAssetFilePaths + { + readonly IGetsAssetFilePath pathProvider; + + /// + /// Gets the file system path for the specified asset file. + /// + /// + /// + /// The returned file system path is an absolute path to which the asset file should be written. The path is determined by the + /// logic of the service . This means that the final filename will not be identical to the + /// but will include that base name within it. + /// + /// + /// If this method returns then the asset file should not be written to the file system. + /// + /// + /// A short descriptive file name fragment for the asset file, including the file extension. + /// The asset file path. + /// + public string GetAssetFilePath(string baseName) => pathProvider.GetAssetFilePath(baseName); + + /// + /// Initializes a new instance of the class. + /// + /// The path provider used to get asset file paths. + public GetAssetFilePaths(IGetsAssetFilePath pathProvider) + { + this.pathProvider = pathProvider ?? throw new System.ArgumentNullException(nameof(pathProvider)); + } + } +} \ No newline at end of file diff --git a/CSF.Screenplay.Abstractions/Abilities/UseAStopwatch.cs b/CSF.Screenplay.Abstractions/Abilities/UseAStopwatch.cs index 04184af2..8fc6fd69 100644 --- a/CSF.Screenplay.Abstractions/Abilities/UseAStopwatch.cs +++ b/CSF.Screenplay.Abstractions/Abilities/UseAStopwatch.cs @@ -22,7 +22,7 @@ public class UseAStopwatch : ICanReport public System.Diagnostics.Stopwatch Stopwatch { get; } = new System.Diagnostics.Stopwatch(); /// - public ReportFragment GetReportFragment(IHasName actor, IFormatsReportFragment formatter) + public ReportFragment GetReportFragment(Actor actor, IFormatsReportFragment formatter) => formatter.Format(AbilityReportStrings.UseAStopwatchFormat, actor); } } diff --git a/CSF.Screenplay.Abstractions/Actor.cs b/CSF.Screenplay.Abstractions/Actor.cs index 4cec7968..e1c27a8a 100644 --- a/CSF.Screenplay.Abstractions/Actor.cs +++ b/CSF.Screenplay.Abstractions/Actor.cs @@ -58,6 +58,9 @@ public partial class Actor : IHasName, IHasPerformanceIdentity Guid IHasPerformanceIdentity.PerformanceIdentity => PerformanceIdentity; + /// + public override string ToString() => $"[Actor '{Name}' in Performance {PerformanceIdentity}]"; + /// Initialises a new instance of /// /// diff --git a/CSF.Screenplay.Abstractions/Actor.performer.cs b/CSF.Screenplay.Abstractions/Actor.performer.cs index 8b95e54a..1726e2ee 100644 --- a/CSF.Screenplay.Abstractions/Actor.performer.cs +++ b/CSF.Screenplay.Abstractions/Actor.performer.cs @@ -79,7 +79,8 @@ protected virtual ValueTask PerformAsync(IPerformableWithResult perform PerformableException GetPerformableException(object performable, Exception ex) { - return new PerformableException($"{Name} encountered an unexpected exception whilst performing a performable of type {performable.GetType().FullName}", ex) + + return new PerformableException($"{Name} encountered an unexpected exception whilst executing the performable logic of {performable.GetType().FullName}", ex) { Performable = performable, }; diff --git a/CSF.Screenplay.Abstractions/ActorExtensions.abilities.cs b/CSF.Screenplay.Abstractions/ActorExtensions.abilities.cs index 8c2ac7e3..315e6262 100644 --- a/CSF.Screenplay.Abstractions/ActorExtensions.abilities.cs +++ b/CSF.Screenplay.Abstractions/ActorExtensions.abilities.cs @@ -80,6 +80,42 @@ public static object GetAbility(this ICanPerform actor, Type abilityType) ?? throw new InvalidOperationException($"{((IHasName) actor).Name} must have an ability of type {abilityType.FullName}"); } + /// Tries to get the first ability which the actor has of the specified type + /// The actor from whom to get the ability + /// If this method returns then this exposes the strongly-typed ability; if not then this value is undefined + /// The type of ability desired + /// if the actor has an ability of the specified type; if not. + /// If the is + /// If the actor does not implement + public static bool TryGetAbility(this ICanPerform actor, out T ability) + { + ability = default; + if (!TryGetAbility(actor, typeof(T), out var untypedAbility)) return false; + ability = (T) untypedAbility; + return true; + } + + /// Gets the first ability which the actor has of the specified type + /// The actor from whom to get the ability + /// The type of ability desired + /// If this method returns then this exposes the strongly-typed ability; if not then this value is undefined + /// if the actor has an ability of the specified type; if not. + /// If any parameter is + public static bool TryGetAbility(this ICanPerform actor, Type abilityType, out object ability) + { + if(actor is null) throw new ArgumentNullException(nameof(actor)); + if(abilityType is null) throw new ArgumentNullException(nameof(abilityType)); + + if(!actor.HasAbility(abilityType)) + { + ability = default; + return false; + } + + ability = actor.GetAbility(abilityType); + return true; + } + /// Adds an ability to the specified actor /// The actor from whom to get the ability /// The ability to add to the actor diff --git a/CSF.Screenplay.Abstractions/ICanReport.cs b/CSF.Screenplay.Abstractions/ICanReport.cs index b7b28e0d..8c3f3499 100644 --- a/CSF.Screenplay.Abstractions/ICanReport.cs +++ b/CSF.Screenplay.Abstractions/ICanReport.cs @@ -69,6 +69,6 @@ public interface ICanReport /// A human-readable report fragment. /// An actor for whom to write the report fragment /// A report-formatting service - ReportFragment GetReportFragment(IHasName actor, IFormatsReportFragment formatter); + ReportFragment GetReportFragment(Actor actor, IFormatsReportFragment formatter); } } diff --git a/CSF.Screenplay.Abstractions/ICast.cs b/CSF.Screenplay.Abstractions/ICast.cs index 08857b8f..4e19813f 100644 --- a/CSF.Screenplay.Abstractions/ICast.cs +++ b/CSF.Screenplay.Abstractions/ICast.cs @@ -1,3 +1,5 @@ +using System.Collections.Generic; + namespace CSF.Screenplay { /// A combined registry and factory for instances, useful when coordinating multiple @@ -31,6 +33,25 @@ namespace CSF.Screenplay /// public interface ICast : IHasServiceProvider, IHasPerformanceIdentity { + /// + /// Gets a collection of string names, indicating the collection of actors which this cast object knows about. + /// + /// + /// + /// Because a cast instance serves as both a registry and a factory, whenever it is used to get an for the first + /// time, that actor instance is cached within the cast which created it. This method may be used to get a collection of the names + /// of actors which are cached within the current cast instance. + /// + /// + /// Note that the collection returned by this method is a snapshot in time, correct as-at the time when this method is executed. + /// The collection does not update automatically as new actors are added to the cast. You must execute this method again to get an + /// updated collection. + /// + /// + /// A collection of strings, corresponding to the actors which have been created in the lifetime + /// of the current cast instance. + IReadOnlyCollection GetCastList(); + /// /// Gets a single by their name, creating them if they do not already exist in the cast. /// diff --git a/CSF.Screenplay.Abstractions/IPerformance.cs b/CSF.Screenplay.Abstractions/IPerformance.cs index 5c3c09dc..6d37a2b1 100644 --- a/CSF.Screenplay.Abstractions/IPerformance.cs +++ b/CSF.Screenplay.Abstractions/IPerformance.cs @@ -67,15 +67,6 @@ public interface IPerformance : IHasPerformanceIdentity, /// Alternatively, for a BDD-style testing framework, it could be named based upon human-readable /// feature & scenario names. /// - /// - /// Ideally this property would be immutable after a Performance is created. - /// Unfortunately, some testing frameworks do not expose relevant naming information about a test until after the point - /// of execution where the Performance must be created. - /// Thus, this property is mutable, so that it is possible to 'backfill' missing naming information after the performance has - /// been created. - /// Wherever possible, it is recommended to avoid updating this list of identifier/names and to only set them up when creating the - /// performance, via . - /// /// /// /// @@ -84,7 +75,7 @@ public interface IPerformance : IHasPerformanceIdentity, /// and the second will be named Joe can take out the Trash. /// /// - List NamingHierarchy { get; } + IReadOnlyList NamingHierarchy { get; } /// Gets a value which indicates the state of the current performance. /// diff --git a/CSF.Screenplay.Abstractions/Performables/ReadTheStopwatch.cs b/CSF.Screenplay.Abstractions/Performables/ReadTheStopwatch.cs index 0edfae42..905de004 100644 --- a/CSF.Screenplay.Abstractions/Performables/ReadTheStopwatch.cs +++ b/CSF.Screenplay.Abstractions/Performables/ReadTheStopwatch.cs @@ -18,7 +18,7 @@ namespace CSF.Screenplay.Performables public class ReadTheStopwatch : IPerformableWithResult, ICanReport { /// - public ReportFragment GetReportFragment(IHasName actor, IFormatsReportFragment formatter) + public ReportFragment GetReportFragment(Actor actor, IFormatsReportFragment formatter) => formatter.Format(PerformableReportStrings.ReadTheStopwatchFormat, actor); /// diff --git a/CSF.Screenplay.Abstractions/Performables/ResetTheStopwatch.cs b/CSF.Screenplay.Abstractions/Performables/ResetTheStopwatch.cs index d86845f0..a99f133e 100644 --- a/CSF.Screenplay.Abstractions/Performables/ResetTheStopwatch.cs +++ b/CSF.Screenplay.Abstractions/Performables/ResetTheStopwatch.cs @@ -17,7 +17,7 @@ namespace CSF.Screenplay.Performables public class ResetTheStopwatch : IPerformable, ICanReport { /// - public ReportFragment GetReportFragment(IHasName actor, IFormatsReportFragment formatter) + public ReportFragment GetReportFragment(Actor actor, IFormatsReportFragment formatter) => formatter.Format(PerformableReportStrings.ResetTheStopwatchFormat, actor); /// diff --git a/CSF.Screenplay.Abstractions/Performables/StartTheStopwatch.cs b/CSF.Screenplay.Abstractions/Performables/StartTheStopwatch.cs index 30f54f0a..6158f911 100644 --- a/CSF.Screenplay.Abstractions/Performables/StartTheStopwatch.cs +++ b/CSF.Screenplay.Abstractions/Performables/StartTheStopwatch.cs @@ -17,7 +17,7 @@ namespace CSF.Screenplay.Performables public class StartTheStopwatch : IPerformable, ICanReport { /// - public ReportFragment GetReportFragment(IHasName actor, IFormatsReportFragment formatter) + public ReportFragment GetReportFragment(Actor actor, IFormatsReportFragment formatter) => formatter.Format(PerformableReportStrings.StartTheStopwatchFormat, actor); /// diff --git a/CSF.Screenplay.Abstractions/Performables/StopTheStopwatch.cs b/CSF.Screenplay.Abstractions/Performables/StopTheStopwatch.cs index 20ecad96..00ecc6d4 100644 --- a/CSF.Screenplay.Abstractions/Performables/StopTheStopwatch.cs +++ b/CSF.Screenplay.Abstractions/Performables/StopTheStopwatch.cs @@ -17,7 +17,7 @@ namespace CSF.Screenplay.Performables public class StopTheStopwatch : IPerformable, ICanReport { /// - public ReportFragment GetReportFragment(IHasName actor, IFormatsReportFragment formatter) + public ReportFragment GetReportFragment(Actor actor, IFormatsReportFragment formatter) => formatter.Format(PerformableReportStrings.StopTheStopwatchFormat, actor); /// diff --git a/CSF.Screenplay.Abstractions/Performances/ICreatesPerformance.cs b/CSF.Screenplay.Abstractions/Performances/ICreatesPerformance.cs deleted file mode 100644 index f3e5a7e4..00000000 --- a/CSF.Screenplay.Abstractions/Performances/ICreatesPerformance.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace CSF.Screenplay.Performances -{ - /// An object which creates instances of ; a factory service. - public interface ICreatesPerformance - { - /// Creates a new performance instance. - /// A new performance instance - IPerformance CreatePerformance(); - } -} \ No newline at end of file diff --git a/CSF.Screenplay.Abstractions/Performances/IRelaysPerformanceEvents.cs b/CSF.Screenplay.Abstractions/Performances/IRelaysPerformanceEvents.cs index cd92421a..82e9a4e7 100644 --- a/CSF.Screenplay.Abstractions/Performances/IRelaysPerformanceEvents.cs +++ b/CSF.Screenplay.Abstractions/Performances/IRelaysPerformanceEvents.cs @@ -53,6 +53,9 @@ public interface IRelaysPerformanceEvents /// /// /// Use this method when ending a performance, as a convenience to unsubscribe from all of its actors at once. + /// Note that this method might not result in unsubscribing any actors. If the subscribed actors are also managed + /// by an implementation of then the disposal of the cast will automatically unsubscribe these actors + /// on its own. This method is provided & used as a backup technique, in case the actors are not managed by a cast. /// /// /// The identity of a performance. @@ -103,17 +106,15 @@ public interface IRelaysPerformanceEvents /// /// Invokes an event indicating that a has begun. /// - /// The performance identity - /// The performance's hierarchical name - void InvokePerformanceBegun(Guid performanceIdentity, IList namingHierarchy); + /// The performance + void InvokePerformanceBegun(IPerformance performance); /// /// Invokes an event indicating that a has finished. /// - /// The performance identity - /// The performance's hierarchical name + /// The performance /// A value indicating whether or not the performance was a success - void InvokePerformanceFinished(Guid performanceIdentity, IList namingHierarchy, bool? success); + void InvokePerformanceFinished(IPerformance performance, bool? success); #endregion diff --git a/CSF.Screenplay.Abstractions/Performances/PerformanceCompleteEventArgs.cs b/CSF.Screenplay.Abstractions/Performances/PerformanceCompleteEventArgs.cs index e3b1d6db..40eacacd 100644 --- a/CSF.Screenplay.Abstractions/Performances/PerformanceCompleteEventArgs.cs +++ b/CSF.Screenplay.Abstractions/Performances/PerformanceCompleteEventArgs.cs @@ -34,13 +34,10 @@ public class PerformanceFinishedEventArgs : PerformanceEventArgs public bool? Success { get; } /// Initialises a new instance of - /// The performance identity - /// The scenario hierarchy + /// The performance. /// A value indicating whether or not the scenario completed with a succeess result /// If the scenario hierarchy is - public PerformanceFinishedEventArgs(Guid performanceIdentity, - IReadOnlyList namingHierarchy, - bool? success) : base(performanceIdentity, namingHierarchy) + public PerformanceFinishedEventArgs(IPerformance performance, bool? success) : base(performance) { Success = success; } diff --git a/CSF.Screenplay.Abstractions/Performances/PerformanceEventArgs.cs b/CSF.Screenplay.Abstractions/Performances/PerformanceEventArgs.cs index 6806b4a9..efe6e465 100644 --- a/CSF.Screenplay.Abstractions/Performances/PerformanceEventArgs.cs +++ b/CSF.Screenplay.Abstractions/Performances/PerformanceEventArgs.cs @@ -10,6 +10,11 @@ namespace CSF.Screenplay.Performances /// public class PerformanceEventArgs : PerformanceScopeEventArgs { + /// + /// Gets the to which this event relates. + /// + public IPerformance Performance { get; } + /// Gets an ordered list of identifiers which indicate the 's name within an organisational hierarchy. /// /// @@ -18,15 +23,14 @@ public class PerformanceEventArgs : PerformanceScopeEventArgs /// /// /// - public IReadOnlyList NamingHierarchy { get; } + public IReadOnlyList NamingHierarchy => Performance.NamingHierarchy; /// Initialises a new instance of - /// The performance identity - /// The screenplay naming hierarchy + /// The performance /// If the scenario hierarchy is - public PerformanceEventArgs(Guid performanceIdentity, IReadOnlyList namingHierarchy) : base(performanceIdentity) + public PerformanceEventArgs(IPerformance performance) : base(performance.PerformanceIdentity) { - NamingHierarchy = namingHierarchy ?? throw new ArgumentNullException(nameof(namingHierarchy)); + Performance = performance; } } } \ No newline at end of file diff --git a/CSF.Screenplay.Abstractions/Reporting/IGetsAssetFilePath.cs b/CSF.Screenplay.Abstractions/Reporting/IGetsAssetFilePath.cs new file mode 100644 index 00000000..c783354a --- /dev/null +++ b/CSF.Screenplay.Abstractions/Reporting/IGetsAssetFilePath.cs @@ -0,0 +1,29 @@ +namespace CSF.Screenplay.Reporting +{ + /// + /// A service which gets a filesystem path to which Screenplay asset files should be written, if they are to be written at all. + /// + public interface IGetsAssetFilePath + { + /// + /// Gets the filesystem path to which an asset file should be written. + /// + /// + /// + /// If reporting is disabled, for the same reasons as would return , + /// then this method will also return . + /// In that case, reporting is disabled and no asset files should be written to the file system. + /// + /// + /// If reporting is enabled, then this method should return an absolute file system path to which an asset file should be written, + /// where the asset has the specified 'base name'. That base name should be a short filename fragment which describes the asset. + /// This file name will be embellished with other information by this method, such as to ensure that the file name is unique within + /// the current Screenplay run. + /// + /// + /// A short & descriptive filename fragment, which includes the file extension but no path information + /// An absolute file system path at which the asset file should be saved, or a reference indicating that + /// the asset file should not be saved. + string GetAssetFilePath(string baseName); + } +} \ No newline at end of file diff --git a/CSF.Screenplay.Abstractions/Reporting/IGetsReportPath.cs b/CSF.Screenplay.Abstractions/Reporting/IGetsReportPath.cs new file mode 100644 index 00000000..6ee81c59 --- /dev/null +++ b/CSF.Screenplay.Abstractions/Reporting/IGetsReportPath.cs @@ -0,0 +1,26 @@ +namespace CSF.Screenplay.Reporting +{ + /// + /// A service which gets the path to which the Screenplay report should be written. + /// + public interface IGetsReportPath + { + /// + /// Gets the path to which the report should be written. + /// + /// + /// + /// If the returned path is then Screenplay's reporting functionality should be disabled and no report should be written. + /// Otherwise, implementations of this interface should return an absolute file system path to which the report should be written. + /// This path must be writable by the executing process. + /// + /// + /// Reporting could be disabled if either the Screenplay Options report path is or a whitespace-only string, or if the path + /// indicated by those options is not writable. + /// + /// + /// The report path. + string GetReportPath(); + } +} + diff --git a/CSF.Screenplay.Docs/docfx.json b/CSF.Screenplay.Docs/docfx.json index d4bc2031..144d8cd7 100644 --- a/CSF.Screenplay.Docs/docfx.json +++ b/CSF.Screenplay.Docs/docfx.json @@ -9,8 +9,9 @@ "docs/**", "**/bin/**", "**/obj/**", - "Tests_old/**", - "Tests/**" + "Tests/**", + "*_old/**", + "CSF.Screenplay.JsonToHtmlReport.Template/**" ] } ], diff --git a/CSF.Screenplay.Docs/docs/dependencyInjection/InjectableServices.md b/CSF.Screenplay.Docs/docs/dependencyInjection/InjectableServices.md index a2ef5b5b..2d3ff741 100644 --- a/CSF.Screenplay.Docs/docs/dependencyInjection/InjectableServices.md +++ b/CSF.Screenplay.Docs/docs/dependencyInjection/InjectableServices.md @@ -1,5 +1,40 @@ +--- +uid: InjectableServicesArticle +--- + # Injectable services -TODO: Write this docco +Screenplay explicitly supports dependency-injection of the following services from the Screenplay architecture into your [performance] logic. +These are _in addition to_ any services you may have configured yourself within DI. + +Whilst it may be possible to inject other services from Screenplay's architecture, these are explicitly supported throughout all [integrations]. + +[performance]: xref:CSF.Screenplay.IPerformance +[integrations]: ../../glossary/Integration.md + +## The Stage + +Inject the [`IStage`] into your [performance] logic to get and work with [Actors]. +This is _the recommended way_ to manage any actors involved in a performance. + +The stage provides all the functionality of the Cast (below) as well as control of the [spotlight], should you wish to use it. + +[`IStage`]: xref:CSF.Screenplay.IStage +[Actors]: xref:CSF.Screenplay.Actor +[spotlight]: ../../glossary/Spotlight.md + +## The Cast + +The [`ICast`] is available for dependency injection into your [performance] logic as an alternative mechanism by which to work with [Actors]. +It is recommended to _inject the Stage (above) instead of the Cast_. +The Cast is reachable from the Stage should you need it, via the [`Cast`] property. + +[`ICast`]: xref:CSF.Screenplay.ICast +[`Cast`]: xref:CSF.Screenplay.IStage.Cast + +## The Performance + +You may inject an instance of [`IPerformance`] to gain direct access to the current [performance] object, from within your performance logic. +Whilst supported, _it is usually not required (or recommended)_ to inject this object into your performance logic. -It should be a list of services which Screenplay makes available for DI, such as the cast & stage etc. +[`IPerformance`]: xref:CSF.Screenplay.IPerformance diff --git a/CSF.Screenplay.Docs/glossary/Integration.md b/CSF.Screenplay.Docs/glossary/Integration.md index 1edfa1cd..72536b08 100644 --- a/CSF.Screenplay.Docs/glossary/Integration.md +++ b/CSF.Screenplay.Docs/glossary/Integration.md @@ -6,9 +6,10 @@ uid: IntegrationGlossaryItem An **Integration** refers to an integration library between the Screenplay library and a framework for performing automated tests. -The integration library performs the necessary scaffolding to make the Screenplay types available for dependency injection. +The integration library performs the necessary scaffolding to ensure that [supported Screenplay types are available for dependency injection]. It also deals with the association of **[Scenarios]** with **[Performances]** and the lifetime of the whole **[Screenplay]**, culminating with the production of the **[Report]**. +[supported Screenplay types are available for dependency injection]: ../docs/dependencyInjection/InjectableServices.md [Scenarios]: Scenario.md [Performances]: xref:CSF.Screenplay.IPerformance [Screenplay]: xref:CSF.Screenplay.Screenplay diff --git a/CSF.Screenplay.Docs/glossary/index.md b/CSF.Screenplay.Docs/glossary/index.md index 7b2b4ea2..8d98c293 100644 --- a/CSF.Screenplay.Docs/glossary/index.md +++ b/CSF.Screenplay.Docs/glossary/index.md @@ -4,24 +4,24 @@ Following is a glossary of Screenplay terminology; each term is a link to its ow Many of these terms are implemented directly as .NET types in the Screenplay architecture. Where applicable, the glossary item links directly to the relevant type within the [API documentation]. -| Term | Summary | -| ---- | ------- | -| [Screenplay] | A complete execution of the Screenplay software | -| [Performance] | A single end-to-end script of performables | -| [Performable] | A Screenplay verb; something that an actor can do | -| [Action] | A kind of peformable; the lowest-level interaction that changes the state of the application | -| [Question] | A kind of peformable; the lowest-level interrogation that reads application state | -| [Task] | A composition of actions, questions and/or other tasks to create higher-level performables | -| [Actor] | Typically a human user of the application, directs the use of performables | -| [Ability] | Something that an actor is able to do or has; provides the dependencies for actions/questions | -| [Persona] | A factory or template for consistently creating reusable, well-known actors | -| [Cast] | A factory & registry for actors which facilitates managing multiple actors in a performance | -| [Stage] | Provides situational context; a concept of 'the currently active actor' | -| [Spotlight] | The currently active actor, facilitated by the stage | -| [Report] | An output which details every performance/scenario and the outcomes of theie performables | -| [Scenario] | Typically similar to a performance, this is a single test within a testing framework | -| [Feature] | A logical group of related scenarios, this is a test class or test fixture in some testing frameworks | -| [Integration] | A consumer of the Screenplay framework, such as a testing framework | +| Term | Summary | +| ---- | ------- | +| [Ability] | Something that an actor is able to do or has; provides the dependencies for actions/questions | +| [Action] | A kind of peformable; the lowest-level interaction that changes the state of the application | +| [Actor] | Typically a human user of the application, directs the use of performables | +| [Cast] | A factory & registry for actors which facilitates managing multiple actors in a performance | +| [Feature] | A logical group of related scenarios, this is a test class or test fixture in some testing frameworks | +| [Integration] | A consumer of the Screenplay framework, such as a testing framework | +| [Performable] | A Screenplay verb; something that an actor can do | +| [Performance] | A single end-to-end script of performables | +| [Persona] | A factory or template for consistently creating reusable, well-known actors | +| [Question] | A kind of peformable; the lowest-level interrogation that reads application state | +| [Report] | An output which details every performance/scenario and the outcomes of theie performables | +| [Scenario] | Typically similar to a performance, this is a single test within a testing framework | +| [Screenplay] | A complete execution of the Screenplay software | +| [Spotlight] | The currently active actor, facilitated by the stage | +| [Stage] | Provides situational context; a concept of 'the currently active actor' | +| [Task] | A composition of actions, questions and/or other tasks to create higher-level performables | [API documentation]: xref:CSF.Screenplay [Screenplay]: xref:CSF.Screenplay.Screenplay diff --git a/CSF.Screenplay.Docs/glossary/toc.yml b/CSF.Screenplay.Docs/glossary/toc.yml index f5cd997f..d49ee9bd 100644 --- a/CSF.Screenplay.Docs/glossary/toc.yml +++ b/CSF.Screenplay.Docs/glossary/toc.yml @@ -15,7 +15,7 @@ - name: Performable href: Performable.md - name: Performance - uid: CSF.Screenplay.Performance + uid: CSF.Screenplay.IPerformance - name: Persona href: Persona.md - name: Question @@ -26,6 +26,8 @@ href: Scenario.md - name: Screenplay uid: CSF.Screenplay.Screenplay +- name: Spotlight + href: Spotlight.md - name: Stage uid: CSF.Screenplay.IStage - name: Task diff --git a/CSF.Screenplay.JsonToHtmlReport.Template/src/js/ReportLoader.js b/CSF.Screenplay.JsonToHtmlReport.Template/src/js/ReportLoader.js index 893c6f2a..8b33e496 100644 --- a/CSF.Screenplay.JsonToHtmlReport.Template/src/js/ReportLoader.js +++ b/CSF.Screenplay.JsonToHtmlReport.Template/src/js/ReportLoader.js @@ -19,8 +19,7 @@ export class ReportLoader { const jsonData = JSON.parse(scriptElement.textContent); return jsonData; } catch (error) { - console.error(error); - throw new Error('Failed to parse JSON content'); + throw new Error('Failed to parse JSON content whilst loading a Screenplay report', { cause: error }); } } } diff --git a/CSF.Screenplay.JsonToHtmlReport.Template/src/package-lock.json b/CSF.Screenplay.JsonToHtmlReport.Template/src/package-lock.json index 42daa487..a01c093a 100644 --- a/CSF.Screenplay.JsonToHtmlReport.Template/src/package-lock.json +++ b/CSF.Screenplay.JsonToHtmlReport.Template/src/package-lock.json @@ -39,15 +39,15 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.26.2", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", - "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.25.9", + "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" + "picocolors": "^1.1.1" }, "engines": { "node": ">=6.9.0" @@ -386,9 +386,9 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", - "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", "dev": true, "license": "MIT", "engines": { @@ -396,9 +396,9 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", - "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", "dev": true, "license": "MIT", "engines": { @@ -431,27 +431,27 @@ } }, "node_modules/@babel/helpers": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.0.tgz", - "integrity": "sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", "dev": true, "license": "MIT", "dependencies": { - "@babel/template": "^7.25.9", - "@babel/types": "^7.26.0" + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.26.2", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.2.tgz", - "integrity": "sha512-DWMCZH9WA4Maitz2q21SRKHo9QXZxkDsbNZoVD62gusNtNBBqDg9i7uOhASfTfIGNzW+O+r7+jAlM8dwphcJKQ==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.26.0" + "@babel/types": "^7.28.5" }, "bin": { "parser": "bin/babel-parser.js" @@ -1778,28 +1778,25 @@ } }, "node_modules/@babel/runtime": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.0.tgz", - "integrity": "sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", + "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", "dev": true, "license": "MIT", - "dependencies": { - "regenerator-runtime": "^0.14.0" - }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/template": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz", - "integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==", + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.25.9", - "@babel/parser": "^7.25.9", - "@babel/types": "^7.25.9" + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1825,14 +1822,14 @@ } }, "node_modules/@babel/types": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.0.tgz", - "integrity": "sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9" + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" }, "engines": { "node": ">=6.9.0" @@ -3061,9 +3058,9 @@ "license": "ISC" }, "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "dependencies": { @@ -3134,6 +3131,20 @@ "dev": true, "license": "MIT" }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -3179,9 +3190,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001676", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001676.tgz", - "integrity": "sha512-Qz6zwGCiPghQXGJvgQAem79esjitvJ+CxSbSQkW9H/UX5hg8XM88d4lp2W+MEQ81j+Hip58Il+jGVdazk1z9cw==", + "version": "1.0.30001762", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001762.tgz", + "integrity": "sha512-PxZwGNvH7Ak8WX5iXzoK1KPZttBXNPuaOvI2ZYU7NrlM+d9Ov+TUvlLOBNGzVXAntMSMMlJPd+jY6ovrVjSmUw==", "dev": true, "funding": [ { @@ -3441,9 +3452,9 @@ } }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, "license": "MIT", "dependencies": { @@ -3953,6 +3964,21 @@ "tslib": "^2.0.3" } }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/electron-to-chromium": { "version": "1.5.50", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.50.tgz", @@ -4027,6 +4053,26 @@ "is-arrayish": "^0.2.1" } }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es-module-lexer": { "version": "1.5.4", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.4.tgz", @@ -4034,6 +4080,35 @@ "dev": true, "license": "MIT" }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -4296,14 +4371,16 @@ } }, "node_modules/form-data": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", - "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", "dev": true, "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", "mime-types": "^2.1.12" }, "engines": { @@ -4362,6 +4439,31 @@ "node": "6.* || 8.* || >= 10.*" } }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/get-package-type": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", @@ -4372,6 +4474,20 @@ "node": ">=8.0.0" } }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/get-stream": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", @@ -4424,6 +4540,19 @@ "node": ">=4" } }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -4441,6 +4570,35 @@ "node": ">=8" } }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -5592,9 +5750,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", "dev": true, "license": "MIT", "dependencies": { @@ -5838,6 +5996,16 @@ "tmpl": "1.0.5" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/mdn-data": { "version": "2.0.30", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", @@ -5941,9 +6109,9 @@ "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", - "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", "dev": true, "funding": [ { @@ -6211,9 +6379,9 @@ "license": "MIT" }, "node_modules/picocolors": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", - "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "dev": true, "license": "ISC" }, @@ -6948,13 +7116,6 @@ "node": ">=4" } }, - "node_modules/regenerator-runtime": { - "version": "0.14.1", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", - "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", - "dev": true, - "license": "MIT" - }, "node_modules/regenerator-transform": { "version": "0.15.2", "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.2.tgz", diff --git a/CSF.Screenplay.JsonToHtmlReport/CSF.Screenplay.JsonToHtmlReport.csproj b/CSF.Screenplay.JsonToHtmlReport/CSF.Screenplay.JsonToHtmlReport.csproj index 198c1413..b5b533fb 100644 --- a/CSF.Screenplay.JsonToHtmlReport/CSF.Screenplay.JsonToHtmlReport.csproj +++ b/CSF.Screenplay.JsonToHtmlReport/CSF.Screenplay.JsonToHtmlReport.csproj @@ -7,6 +7,9 @@ NU1903,NU1902 CSF.Screenplay.JsonToHtmlReport $(MSBuildProjectDirectory)\bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml + + false diff --git a/CSF.Screenplay.NUnit/CastAdapter.cs b/CSF.Screenplay.NUnit/CastAdapter.cs new file mode 100644 index 00000000..f940f9f3 --- /dev/null +++ b/CSF.Screenplay.NUnit/CastAdapter.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using Microsoft.Extensions.DependencyInjection; + +namespace CSF.Screenplay +{ + /// + /// An adapter which enables the use of within an NUnit3 test, without needing to parameter-inject the instance + /// as Lazy<ICast>. + /// + /// + /// + /// Due to NUnit architectural limitations, injectable parameters cannot be resolved from DI at the point the test method is built. + /// If we were to attempt this, then the parameter value would not be associated with the correct Screenplay/DI scope (and thus Event Bus). + /// This is due to the two-process model which NUnit uses; one process for building the test methods and another process for running the + /// tests. By using an adapter with Lazy resolution of the real implementation, we ensure that DI resolution is deferred into the test-run + /// process and not the test-building process. + /// + /// + public sealed class CastAdapter : ICast + { + readonly Lazy wrappedCast; + + /// + public IServiceProvider ServiceProvider => wrappedCast.Value.ServiceProvider; + + /// + public Guid PerformanceIdentity => wrappedCast.Value.PerformanceIdentity; + + /// + public Actor GetActor(string name) => wrappedCast.Value.GetActor(name); + + /// + public Actor GetActor(IPersona persona) => wrappedCast.Value.GetActor(persona); + + /// + public IReadOnlyCollection GetCastList() => wrappedCast.Value.GetCastList(); + + /// + /// Creates a new instance of for the specified performance identity. + /// + /// A performance identity, corresponding to . + public CastAdapter(Guid performanceIdentity) + { + wrappedCast = new Lazy(() => ScreenplayLocator.GetScopedPerformance(performanceIdentity).Scope.ServiceProvider.GetRequiredService()); + } + } +} + diff --git a/CSF.Screenplay.NUnit/PerformanceAdapter.cs b/CSF.Screenplay.NUnit/PerformanceAdapter.cs new file mode 100644 index 00000000..c4aa8499 --- /dev/null +++ b/CSF.Screenplay.NUnit/PerformanceAdapter.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; +using CSF.Screenplay.Performances; + +namespace CSF.Screenplay +{ + /// + /// An adapter which enables the use of within an NUnit3 test, without needing to parameter-inject the instance + /// as Lazy<IPerformance>. + /// + /// + /// + /// Due to NUnit architectural limitations, injectable parameters cannot be resolved from DI at the point the test method is built. + /// If we were to attempt this, then the parameter value would not be associated with the correct Screenplay/DI scope (and thus Event Bus). + /// This is due to the two-process model which NUnit uses; one process for building the test methods and another process for running the + /// tests. By using an adapter with Lazy resolution of the real implementation, we ensure that DI resolution is deferred into the test-run + /// process and not the test-building process. + /// + /// + public sealed class PerformanceAdapter : IPerformance + { + readonly Lazy wrappedPerformance; + + /// + public IReadOnlyList NamingHierarchy => wrappedPerformance.Value.NamingHierarchy; + + /// + public PerformanceState PerformanceState => wrappedPerformance.Value.PerformanceState; + + /// + public Guid PerformanceIdentity => wrappedPerformance.Value.PerformanceIdentity; + + /// + public IServiceProvider ServiceProvider => wrappedPerformance.Value.ServiceProvider; + + /// + public void BeginPerformance() => wrappedPerformance.Value.BeginPerformance(); + + /// + public void Dispose() => wrappedPerformance.Value.Dispose(); + + /// + public void FinishPerformance(bool? success) => wrappedPerformance.Value.FinishPerformance(success); + + /// + /// Creates a new instance of for the specified performance identity. + /// + /// A performance identity, corresponding to . + public PerformanceAdapter(Guid performanceIdentity) + { + wrappedPerformance = new Lazy(() => ScreenplayLocator.GetScopedPerformance(performanceIdentity).Performance); + } + } +} + diff --git a/CSF.Screenplay.NUnit/ScreenplayAssemblyAttribute.cs b/CSF.Screenplay.NUnit/ScreenplayAssemblyAttribute.cs index 69984e03..0a219685 100644 --- a/CSF.Screenplay.NUnit/ScreenplayAssemblyAttribute.cs +++ b/CSF.Screenplay.NUnit/ScreenplayAssemblyAttribute.cs @@ -55,10 +55,10 @@ public Screenplay GetScreenplay() public override ActionTargets Targets => ActionTargets.Suite; /// - public override void AfterTest(ITest test) => GetScreenplay().CompleteScreenplay(); + public override void AfterTest(ITest test) => ScreenplayLocator.GetScreenplay(test).CompleteScreenplay(); /// - public override void BeforeTest(ITest test) => GetScreenplay().BeginScreenplay(); + public override void BeforeTest(ITest test) => ScreenplayLocator.GetScreenplay(test).BeginScreenplay(); /// /// Initializes a new instance of . diff --git a/CSF.Screenplay.NUnit/ScreenplayAttribute.cs b/CSF.Screenplay.NUnit/ScreenplayAttribute.cs index 48f66ad0..25d19306 100644 --- a/CSF.Screenplay.NUnit/ScreenplayAttribute.cs +++ b/CSF.Screenplay.NUnit/ScreenplayAttribute.cs @@ -1,14 +1,9 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Reflection; -using CSF.Screenplay.Performances; -using Microsoft.Extensions.DependencyInjection; using NUnit.Framework; using NUnit.Framework.Interfaces; using NUnit.Framework.Internal; -using NUnit.Framework.Internal.Builders; -using static CSF.Screenplay.ScreenplayLocator; namespace CSF.Screenplay { @@ -34,25 +29,16 @@ namespace CSF.Screenplay public class ScreenplayAttribute : Attribute, ITestAction, ITestBuilder { /// - /// The name of an NUnit3 Property for the suite or test description. - /// - internal const string DescriptionPropertyName = "Description"; - - /// - /// A key for an NUnit3 Property which will hold the current . - /// - internal const string CurrentPerformanceKey = "Current Screenplay Performance"; - - /// - /// A key for an NUnit3 Property which will hold the of the current . + /// A key for an NUnit3 Property which will hold the + /// of the current . /// + /// + /// + /// This property value may be used with + /// + /// internal const string CurrentPerformanceIdentityKey = "Current Screenplay Performance identity"; - /// - /// A key for an NUnit3 Property which will hold the current Dependency Injection scope. - /// - internal const string CurrentDiScopeKey = "Current Screenplay DI scope"; - /// /// Gets the targets for the attribute (when performing before/after test actions). /// @@ -63,77 +49,21 @@ public class ScreenplayAttribute : Attribute, ITestAction, ITestBuilder /// public void BeforeTest(ITest test) { - var performance = GetPerformance(test); - BackfillPerformanceNamingHierarchy(performance, test); - performance.BeginPerformance(); + var performance = ScreenplayLocator.GetScopedPerformance(test); + performance.Performance.BeginPerformance(); } /// public void AfterTest(ITest test) { - var performance = GetPerformance(test); - performance.FinishPerformance(GetOutcome()); - - var diScope = test.Properties.Get(CurrentDiScopeKey) as IServiceScope; + var scopeAndPerformance = ScreenplayLocator.GetScopedPerformance(test); + scopeAndPerformance.Performance.FinishPerformance(GetOutcome()); + var diScope = scopeAndPerformance.Scope; diScope?.Dispose(); } /// - public IEnumerable BuildFrom(IMethodInfo method, Test suite) - { - if (method is null) - throw new ArgumentNullException(nameof(method)); - if (suite is null) - throw new ArgumentNullException(nameof(suite)); - - var screenplay = GetScreenplay(method); - var scopeAndPerformance = screenplay.CreateScopedPerformance(); - var testMethod = GetTestMethod(scopeAndPerformance, method, suite); - return new[] { testMethod }; - } - - static IPerformance GetPerformance(ITest test) - { - return (IPerformance)test.Properties.Get(CurrentPerformanceKey) - ?? throw new ArgumentException($"The specified test must contain a property '{CurrentPerformanceKey}' containing an {nameof(IPerformance)}", nameof(test)); - } - - static TestMethod GetTestMethod(ScopeAndPerformance scopeAndPerformance, IMethodInfo method, Test suite) - { - var builder = new NUnitTestCaseBuilder(); - var resolvedTestMethodParameters = (from parameter in method.GetParameters() - select scopeAndPerformance.Performance.ServiceProvider.GetService(parameter.ParameterType)) - .ToArray(); - var testCaseParameters = new TestCaseParameters(resolvedTestMethodParameters); - - var testMethod = builder.BuildTestMethod(method, suite, testCaseParameters); - testMethod.Properties.Add(CurrentPerformanceKey, scopeAndPerformance.Performance); - testMethod.Properties.Add(CurrentPerformanceIdentityKey, scopeAndPerformance.Performance.PerformanceIdentity); - testMethod.Properties.Add(CurrentDiScopeKey, scopeAndPerformance.Scope); - return testMethod; - } - - static void BackfillPerformanceNamingHierarchy(IPerformance performance, ITest test) - { - var namingHierarchy = GetReverseOrderNamingHierarchy(test).ToList(); - - // Reverse it to get it in the correct order - namingHierarchy.Reverse(); - performance.NamingHierarchy.Clear(); - performance.NamingHierarchy.AddRange(namingHierarchy); - } - - static IEnumerable GetReverseOrderNamingHierarchy(ITest suite) - { - for (var currentSuite = suite; - currentSuite != null; - currentSuite = currentSuite.Parent) - { - if (!currentSuite.IsSuite || (currentSuite.Method is null && currentSuite.Fixture is null)) - continue; - yield return new IdentifierAndName(currentSuite.FullName, currentSuite.Properties.Get(DescriptionPropertyName)?.ToString()); - } - } + public IEnumerable BuildFrom(IMethodInfo method, Test suite) => TestMethodBuilder.BuildFrom(method, suite); static bool? GetOutcome() { diff --git a/CSF.Screenplay.NUnit/ScreenplayLocator.cs b/CSF.Screenplay.NUnit/ScreenplayLocator.cs index 713eab14..a78e648e 100644 --- a/CSF.Screenplay.NUnit/ScreenplayLocator.cs +++ b/CSF.Screenplay.NUnit/ScreenplayLocator.cs @@ -1,7 +1,11 @@ using System; using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; using System.Reflection; +using CSF.Screenplay.Performances; using NUnit.Framework.Interfaces; +using NUnit.Framework.Internal; namespace CSF.Screenplay { @@ -18,7 +22,18 @@ namespace CSF.Screenplay /// public static class ScreenplayLocator { + /// + /// The name of an NUnit3 Property for the suite or test description. + /// + /// + /// + /// This is a built-in NUnit property. It is set automatically by the framework if/when a test is decorated with appropriate attributes. + /// + /// + internal const string DescriptionPropertyName = "Description"; + static readonly ConcurrentDictionary screenplayCache = new ConcurrentDictionary(); + static readonly ConcurrentDictionary performanceCache = new ConcurrentDictionary(); /// /// Gets a instance from the specified . @@ -86,6 +101,8 @@ public static Screenplay GetScreenplay(IMethodInfo method) public static Screenplay GetScreenplay(ITest test) { if(test is null) throw new ArgumentNullException(nameof(test)); + if(test is TestAssembly testAssembly) + return GetScreenplay(testAssembly.Assembly); return GetScreenplay(test.Method); } @@ -95,5 +112,75 @@ static Screenplay GetScreenplayFromAssembly(Assembly assembly) ?? throw new ArgumentException($"The assembly {assembly.FullName} must be decorated with {nameof(ScreenplayAssemblyAttribute)}.", nameof(assembly)); return assemblyAttrib.GetScreenplay(); } + + /// + /// Gets a DI scope and for the specified test. + /// + /// + /// + /// This method will return a cached if one exists for the specified test. + /// If one does not yet exist then a new scope will be created, with an associated performance, and added to the cache. + /// + /// + /// An NUnit3 test object. + /// A DI scope and performance. + public static ScopeAndPerformance GetScopedPerformance(ITest test) + { + return performanceCache.GetOrAdd(GetPerformanceIdentity(test), _ => CreateScopedPerformance(test)); + } + + /// + /// Gets a DI scope and matching the specified performance identity. + /// + /// + /// + /// Unlike the other overload of this method, this overload will not create a scoped performance if one does not yet exist. + /// If this method is used with a performance identity which is not yet cached, then an exception will be raised. + /// + /// + /// A GUID performance identity, corresponding to . + /// A DI scope and performance. + /// If no scope & performance exists in the cache, matching the specified identity + public static ScopeAndPerformance GetScopedPerformance(Guid identity) + { + if (!performanceCache.TryGetValue(identity, out var result)) + throw new ArgumentException($"There must be a cached performance with the identity {identity}", nameof(identity)); + return result; + } + + static ScopeAndPerformance CreateScopedPerformance(ITest test) + { + var screenplay = GetScreenplay(test); + return screenplay.CreateScopedPerformance(GetNamingHierarchy(test), GetPerformanceIdentity(test)); + } + + static Guid GetPerformanceIdentity(ITest test) + { + if (test is null) throw new ArgumentNullException(nameof(test)); + if(!test.Properties.ContainsKey(ScreenplayAttribute.CurrentPerformanceIdentityKey)) + throw new ArgumentException($"The test must contain a property by the name of '{ScreenplayAttribute.CurrentPerformanceIdentityKey}', " + + "containing a Guid performance identity", + nameof(test)); + return (Guid) test.Properties.Get(ScreenplayAttribute.CurrentPerformanceIdentityKey); + } + + static IList GetNamingHierarchy(ITest test) + { + var namingHierarchy = GetReverseOrderNamingHierarchy(test).ToList(); + namingHierarchy.Reverse(); + return namingHierarchy; + } + + static IEnumerable GetReverseOrderNamingHierarchy(ITest suite) + { + for (var currentSuite = suite; + currentSuite != null; + currentSuite = currentSuite.Parent) + { + if (!currentSuite.IsSuite || (currentSuite.Method is null && currentSuite.Fixture is null)) + continue; + yield return new IdentifierAndName(currentSuite.FullName, currentSuite.Properties.Get(DescriptionPropertyName)?.ToString()); + } + } } } diff --git a/CSF.Screenplay.NUnit/StageAdapter.cs b/CSF.Screenplay.NUnit/StageAdapter.cs new file mode 100644 index 00000000..8081853c --- /dev/null +++ b/CSF.Screenplay.NUnit/StageAdapter.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using CSF.Screenplay.Performances; +using Microsoft.Extensions.DependencyInjection; + +namespace CSF.Screenplay +{ + /// + /// An adapter which enables the use of within an NUnit3 test, without needing to parameter-inject the instance + /// as Lazy<IStage>. + /// + /// + /// + /// Due to NUnit architectural limitations, injectable parameters cannot be resolved from DI at the point the test method is built. + /// If we were to attempt this, then the parameter value would not be associated with the correct Screenplay/DI scope (and thus Event Bus). + /// This is due to the two-process model which NUnit uses; one process for building the test methods and another process for running the + /// tests. By using an adapter with Lazy resolution of the real implementation, we ensure that DI resolution is deferred into the test-run + /// process and not the test-building process. + /// + /// + public sealed class StageAdapter : IStage + { + readonly Lazy wrappedStage; + + /// + public ICast Cast => wrappedStage.Value.Cast; + + /// + public Actor GetSpotlitActor() => wrappedStage.Value.GetSpotlitActor(); + + /// + public void Spotlight(Actor actor) => wrappedStage.Value.Spotlight(actor); + + /// + public Actor Spotlight(IPersona persona) => wrappedStage.Value.Spotlight(persona); + + /// + public Actor TurnSpotlightOff() => wrappedStage.Value.TurnSpotlightOff(); + + /// + /// Creates a new instance of for the specified performance identity. + /// + /// A performance identity, corresponding to . + public StageAdapter(Guid performanceIdentity) + { + wrappedStage = new Lazy(() => ScreenplayLocator.GetScopedPerformance(performanceIdentity).Scope.ServiceProvider.GetRequiredService()); + } + } +} + diff --git a/CSF.Screenplay.NUnit/TestMethodBuilder.cs b/CSF.Screenplay.NUnit/TestMethodBuilder.cs new file mode 100644 index 00000000..6375d7cb --- /dev/null +++ b/CSF.Screenplay.NUnit/TestMethodBuilder.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using NUnit.Framework.Interfaces; +using NUnit.Framework.Internal; +using NUnit.Framework.Internal.Builders; + +namespace CSF.Screenplay +{ + /// + /// Builder class which is used to create NUnit3 test method instances for a Screenplay-based test. + /// + public static class TestMethodBuilder + { + static readonly Dictionary> supportedInjectableTypes = new Dictionary> + { + { typeof(IStage), id => new StageAdapter(id) }, + { typeof(ICast), id => new CastAdapter(id) }, + { typeof(IPerformance), id => new PerformanceAdapter(id) }, + }; + + /// + /// Gets a collection of NUnit3 TestMethod instances for the specified method and test. + /// + /// + /// + /// This method handles the resolution of the , which may be used in tests by + /// adding them as parameters to the NUnit test method. + /// + /// + /// The NUnit method object for a test + /// The NUnit test suite object + /// A collection of NUnit test method instances + /// If any parameter is + public static IEnumerable BuildFrom(IMethodInfo method, Test suite) + { + if (method is null) throw new ArgumentNullException(nameof(method)); + if (suite is null) throw new ArgumentNullException(nameof(suite)); + + var builder = new NUnitTestCaseBuilder(); + + var performanceId = Guid.NewGuid(); + var testCaseParameters = GetResolvedParameters(method, performanceId); + + var testMethod = builder.BuildTestMethod(method, suite, testCaseParameters); + testMethod.Properties.Add(ScreenplayAttribute.CurrentPerformanceIdentityKey, performanceId); + + return new [] { testMethod }; + } + + static TestCaseParameters GetResolvedParameters(IMethodInfo method, Guid performanceId) + { + var requestedParameters = method.GetParameters(); + if(requestedParameters.Any(param => !supportedInjectableTypes.Keys.Contains(param.ParameterType))) + throw new InvalidOperationException("The test method must not contain any parameters except those of supported injectable types:\n" + + string.Join(", ", supportedInjectableTypes.Keys.Select(x => x.Name)) + + "\nUnfortunately, NUnit's architecture makes it troublesome to support arbitrary injectable parameters."); + var resolvedParameterValues = requestedParameters.Select(param => supportedInjectableTypes[param.ParameterType].Invoke(performanceId)).ToArray(); + return new TestCaseParameters(resolvedParameterValues); + } + } +} \ No newline at end of file diff --git a/CSF.Screenplay.Selenium.BrowserFlags.Tests_old/AutoMoqDataAttribute.cs b/CSF.Screenplay.Selenium.BrowserFlags.Tests_old/AutoMoqDataAttribute.cs new file mode 100644 index 00000000..63115422 --- /dev/null +++ b/CSF.Screenplay.Selenium.BrowserFlags.Tests_old/AutoMoqDataAttribute.cs @@ -0,0 +1,14 @@ +using System; +using Ploeh.AutoFixture; +using Ploeh.AutoFixture.AutoMoq; +using Ploeh.AutoFixture.NUnit3; + +namespace CSF.Screenplay.Selenium.Tests +{ + public class AutoMoqDataAttribute : AutoDataAttribute + { + public AutoMoqDataAttribute() : base(new Fixture().Customize(new AutoMoqCustomization())) + { + } + } +} diff --git a/CSF.Screenplay.Selenium.BrowserFlags.Tests_old/CSF.Screenplay.Selenium.BrowserFlags.Tests.csproj b/CSF.Screenplay.Selenium.BrowserFlags.Tests_old/CSF.Screenplay.Selenium.BrowserFlags.Tests.csproj new file mode 100644 index 00000000..f1d2c1b4 --- /dev/null +++ b/CSF.Screenplay.Selenium.BrowserFlags.Tests_old/CSF.Screenplay.Selenium.BrowserFlags.Tests.csproj @@ -0,0 +1,83 @@ + + + + Debug + AnyCPU + {87454F03-FBB3-4506-9055-551297445891} + Library + CSF.Screenplay.Selenium.Tests + CSF.Screenplay.Selenium.BrowserFlags.Tests + v4.5 + 1.0.0 + + + true + full + false + bin\Debug + DEBUG; + prompt + 4 + + + true + bin\Release + prompt + 4 + + + + + ..\packages\NUnit.3.7.1\lib\net45\nunit.framework.dll + + + ..\packages\AutoFixture.3.50.3\lib\net40\Ploeh.AutoFixture.dll + + + ..\packages\AutoFixture.AutoMoq.3.50.3\lib\net40\Ploeh.AutoFixture.AutoMoq.dll + + + ..\packages\AutoFixture.NUnit3.3.50.3\lib\net40\Ploeh.AutoFixture.NUnit3.dll + + + ..\packages\Castle.Core.4.0.0\lib\net45\Castle.Core.dll + + + ..\packages\Moq.4.7.25\lib\net45\Moq.dll + + + ..\packages\Newtonsoft.Json.10.0.3\lib\net45\Newtonsoft.Json.dll + + + ..\packages\Selenium.WebDriver.3.4.0\lib\net40\WebDriver.dll + + + + ..\packages\CSF.Configuration.1.1.2\lib\net45\CSF.Configuration.dll + + + ..\packages\CSF.WebDriverExtras.1.0.3\lib\net45\CSF.WebDriverExtras.dll + + + + + + + + + + FooBrowser.flags.json + + + + + + + + + {0665F99E-DB05-4208-BCF1-137EF914CBF5} + CSF.Screenplay.Selenium.BrowserFlags + + + + \ No newline at end of file diff --git a/CSF.Screenplay.Selenium.BrowserFlags.Tests_old/FooBrowser.flags.json b/CSF.Screenplay.Selenium.BrowserFlags.Tests_old/FooBrowser.flags.json new file mode 100644 index 00000000..d6c1fd8c --- /dev/null +++ b/CSF.Screenplay.Selenium.BrowserFlags.Tests_old/FooBrowser.flags.json @@ -0,0 +1,8 @@ +[ + { + "browserName": "FooBrowser", + "flags": [ + "SampleFlag1", + ], + }, +] \ No newline at end of file diff --git a/CSF.Screenplay.Selenium.BrowserFlags.Tests_old/GetBrowserFlagsDefinitionsTests.cs b/CSF.Screenplay.Selenium.BrowserFlags.Tests_old/GetBrowserFlagsDefinitionsTests.cs new file mode 100644 index 00000000..cd2fb6ca --- /dev/null +++ b/CSF.Screenplay.Selenium.BrowserFlags.Tests_old/GetBrowserFlagsDefinitionsTests.cs @@ -0,0 +1,89 @@ +using System.IO; +using System.Linq; +using System.Reflection; +using CSF.WebDriverExtras.Flags; +using Moq; +using NUnit.Framework; + +namespace CSF.Screenplay.Selenium.Tests +{ + [TestFixture,Parallelizable(ParallelScope.All)] + public class GetBrowserFlagsDefinitionsTests + { + [Test,AutoMoqData] + public void GetDefinitions_from_test_assembly_returns_one_definition(IReadsFlagsDefinitions definitionReader, + FlagsDefinition def) + { + // Arrange + var testAssembly = Assembly.GetExecutingAssembly(); + Mock.Get(definitionReader) + .Setup(x => x.GetFlagsDefinitions(It.IsAny())) + .Returns(new [] { def }); + var sut = new GetBrowserFlagsDefinitions(definitionReader); + + // Act + var result = sut.GetDefinitions(testAssembly); + + // Assert + Assert.That(result, Has.Length.EqualTo(1)); + } + + [Test,AutoMoqData] + public void GetDefinitions_from_test_assembly_uses_definition_reader(IReadsFlagsDefinitions definitionReader, + FlagsDefinition def) + { + // Arrange + var testAssembly = Assembly.GetExecutingAssembly(); + Mock.Get(definitionReader) + .Setup(x => x.GetFlagsDefinitions(It.IsAny())) + .Returns(new [] { def }); + var sut = new GetBrowserFlagsDefinitions(definitionReader); + + // Act + var result = sut.GetDefinitions(testAssembly); + + // Assert + Mock.Get(definitionReader) + .Verify(x => x.GetFlagsDefinitions(It.IsAny()), Times.Once); + } + + [Test,Category("Integration")] + public void GetDefinitions_integration_test_only_finds_one_definition() + { + // Arrange + var testAssembly = Assembly.GetExecutingAssembly(); + var sut = new GetBrowserFlagsDefinitions(); + + // Act + var result = sut.GetDefinitions(testAssembly); + + // Assert + Assert.That(result, Has.Length.EqualTo(1)); + } + + [Test,Category("Integration")] + public void GetDefinitions_integration_test_finds_definition_for_FooBrowser() + { + // Arrange + var testAssembly = Assembly.GetExecutingAssembly(); + var sut = new GetBrowserFlagsDefinitions(); + + // Act + var result = sut.GetDefinitions(testAssembly).Single(); + + // Assert + Assert.That(result.BrowserNames.First(), Is.EqualTo("FooBrowser")); + } + + [Test,Category("Integration")] + public void FromDefinitionsAssembly_integration_test_returns_more_than_two_flags_definitions() + { + // Act + var result = GetBrowserFlagsDefinitions.FromDefinitionsAssembly(); + + // Assert + Assert.That(result, Is.Not.Null); + Assert.That(result, Has.Length.GreaterThan(2)); + } + } +} diff --git a/CSF.Screenplay.Selenium.BrowserFlags.Tests_old/InvalidFlagsDefinition.json b/CSF.Screenplay.Selenium.BrowserFlags.Tests_old/InvalidFlagsDefinition.json new file mode 100644 index 00000000..c4b1955f --- /dev/null +++ b/CSF.Screenplay.Selenium.BrowserFlags.Tests_old/InvalidFlagsDefinition.json @@ -0,0 +1,8 @@ +[ + { + "browserName": "BarBrowser", + "flags": [ + "SampleFlag1", + ], + }, +] \ No newline at end of file diff --git a/CSF.Screenplay.Selenium.BrowserFlags.Tests_old/app.config b/CSF.Screenplay.Selenium.BrowserFlags.Tests_old/app.config new file mode 100644 index 00000000..437c6629 --- /dev/null +++ b/CSF.Screenplay.Selenium.BrowserFlags.Tests_old/app.config @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + diff --git a/CSF.Screenplay.Selenium.BrowserFlags.Tests_old/packages.config b/CSF.Screenplay.Selenium.BrowserFlags.Tests_old/packages.config new file mode 100644 index 00000000..3ab15217 --- /dev/null +++ b/CSF.Screenplay.Selenium.BrowserFlags.Tests_old/packages.config @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/CSF.Screenplay.Selenium.BrowserFlags_old/CSF.Screenplay.Selenium.BrowserFlags.csproj b/CSF.Screenplay.Selenium.BrowserFlags_old/CSF.Screenplay.Selenium.BrowserFlags.csproj new file mode 100644 index 00000000..4a22cc42 --- /dev/null +++ b/CSF.Screenplay.Selenium.BrowserFlags_old/CSF.Screenplay.Selenium.BrowserFlags.csproj @@ -0,0 +1,80 @@ + + + + Debug + AnyCPU + {0665F99E-DB05-4208-BCF1-137EF914CBF5} + Library + CSF.Screenplay.Selenium + CSF.Screenplay.Selenium.BrowserFlags + v4.5 + 1.0.0 + + + true + full + false + bin\Debug + DEBUG; + prompt + 4 + bin\Debug\CSF.Screenplay.Selenium.BrowserFlags.xml + false + + + true + bin\Release + prompt + 4 + bin\Release\CSF.Screenplay.Selenium.BrowserFlags.xml + false + + + + + ..\packages\Newtonsoft.Json.10.0.3\lib\net45\Newtonsoft.Json.dll + + + ..\packages\Selenium.WebDriver.3.4.0\lib\net40\WebDriver.dll + + + + ..\packages\CSF.Configuration.1.1.2\lib\net45\CSF.Configuration.dll + + + ..\packages\CSF.WebDriverExtras.1.0.3\lib\net45\CSF.WebDriverExtras.dll + + + + + + + + + + + + + + + + + + + AppleSafari.flags.json + + + GoogleChrome.flags.json + + + InternetExplorer.flags.json + + + MicrosoftEdge.flags.json + + + MozillaFirefox.flags.json + + + + \ No newline at end of file diff --git a/CSF.Screenplay.Selenium.BrowserFlags_old/CSF.Screenplay.Selenium.BrowserFlags.nuspec b/CSF.Screenplay.Selenium.BrowserFlags_old/CSF.Screenplay.Selenium.BrowserFlags.nuspec new file mode 100644 index 00000000..7e2b9106 --- /dev/null +++ b/CSF.Screenplay.Selenium.BrowserFlags_old/CSF.Screenplay.Selenium.BrowserFlags.nuspec @@ -0,0 +1,21 @@ + + + + CSF.Screenplay.Selenium.BrowserFlags + 1.0.0 + CSF.Screenplay.Selenium.BrowserFlags + CSF Software Ltd + MIT + https://github.com/csf-dev/CSF.Screenplay.Selenium + false + An assembly containing 'browser flags definitions' for CSF.Screenplay.Selenium. This indicates the quirks and behaviours of various popular web browsers which are compatible with Selenium WebDriver. + Copyright 2018 + + + + + + + + + \ No newline at end of file diff --git a/CSF.Screenplay.Selenium.BrowserFlags_old/Definitions/AppleSafari.flags.json b/CSF.Screenplay.Selenium.BrowserFlags_old/Definitions/AppleSafari.flags.json new file mode 100644 index 00000000..286888f1 --- /dev/null +++ b/CSF.Screenplay.Selenium.BrowserFlags_old/Definitions/AppleSafari.flags.json @@ -0,0 +1,15 @@ +[ + { + "browserName": "Safari", + "flags": [ + ], + }, + { + "browserName": "Safari", + "minVersion": "11", + "flags": [ + "HtmlElements.Select.RequiresUpdatesViaJavaScriptWorkaround", + ], + }, +] + diff --git a/CSF.Screenplay.Selenium.BrowserFlags_old/Definitions/GoogleChrome.flags.json b/CSF.Screenplay.Selenium.BrowserFlags_old/Definitions/GoogleChrome.flags.json new file mode 100644 index 00000000..b62dc273 --- /dev/null +++ b/CSF.Screenplay.Selenium.BrowserFlags_old/Definitions/GoogleChrome.flags.json @@ -0,0 +1,8 @@ +[ + { + "browserName": "Chrome", + "flags": [ + "HtmlElements.InputTypeDate.RequiresEntryUsingLocaleFormat", + ], + }, +] \ No newline at end of file diff --git a/CSF.Screenplay.Selenium.BrowserFlags_old/Definitions/InternetExplorer.flags.json b/CSF.Screenplay.Selenium.BrowserFlags_old/Definitions/InternetExplorer.flags.json new file mode 100644 index 00000000..f92a921c --- /dev/null +++ b/CSF.Screenplay.Selenium.BrowserFlags_old/Definitions/InternetExplorer.flags.json @@ -0,0 +1,7 @@ +[ + { + "browserName": [ "InternetExplorer", "Internet Explorer" ], + "flags": [ + ], + }, +] \ No newline at end of file diff --git a/CSF.Screenplay.Selenium.BrowserFlags_old/Definitions/MicrosoftEdge.flags.json b/CSF.Screenplay.Selenium.BrowserFlags_old/Definitions/MicrosoftEdge.flags.json new file mode 100644 index 00000000..d9fd8749 --- /dev/null +++ b/CSF.Screenplay.Selenium.BrowserFlags_old/Definitions/MicrosoftEdge.flags.json @@ -0,0 +1,10 @@ +[ + { + "browserName": "MicrosoftEdge", + "flags": [ + "Browser.CannotClearDomainCookies", + "HtmlElements.SelectMultiple.RequiresCtrlClickToToggleOptionSelection", + "HtmlElements.InputTypeDate.RequiresInputViaJavaScriptWorkaround", + ], + }, +] \ No newline at end of file diff --git a/CSF.Screenplay.Selenium.BrowserFlags_old/Definitions/MozillaFirefox.flags.json b/CSF.Screenplay.Selenium.BrowserFlags_old/Definitions/MozillaFirefox.flags.json new file mode 100644 index 00000000..c8e38046 --- /dev/null +++ b/CSF.Screenplay.Selenium.BrowserFlags_old/Definitions/MozillaFirefox.flags.json @@ -0,0 +1,8 @@ +[ + { + "browserName": "Firefox", + "flags": [ + "HtmlElements.InputTypeDate.RequiresInputViaJavaScriptWorkaround" + ], + }, +] diff --git a/CSF.Screenplay.Selenium.BrowserFlags_old/Flags.Browser.cs b/CSF.Screenplay.Selenium.BrowserFlags_old/Flags.Browser.cs new file mode 100644 index 00000000..b1cef97f --- /dev/null +++ b/CSF.Screenplay.Selenium.BrowserFlags_old/Flags.Browser.cs @@ -0,0 +1,23 @@ +using System; + +namespace CSF.Screenplay.Selenium +{ + public static partial class Flags + { + /// + /// Flags relating to the web browser itself, such as management of cookies. + /// + public static class Browser + { + /// + /// Indicates that the web driver is incapable of clearing the cookies for the current domain. + /// + /// + /// + /// The current domain is the domain for the page upon which the web driver is currently viewing. + /// + /// + public static readonly string CannotClearDomainCookies = "Browser.CannotClearDomainCookies"; + } + } +} diff --git a/CSF.Screenplay.Selenium.BrowserFlags_old/Flags.HtmlElements.cs b/CSF.Screenplay.Selenium.BrowserFlags_old/Flags.HtmlElements.cs new file mode 100644 index 00000000..3be266d8 --- /dev/null +++ b/CSF.Screenplay.Selenium.BrowserFlags_old/Flags.HtmlElements.cs @@ -0,0 +1,55 @@ +using System; +namespace CSF.Screenplay.Selenium +{ + public static partial class Flags + { + /// + /// Flags relating to specific HTML elements. + /// + public static class HtmlElements + { + /// + /// Flags relating to HTML <input type="date"> elements. + /// + public static class InputTypeDate + { + /// + /// Indicates that the web driver may be used to enter a date using a format that conforms to the web browser's + /// current locale setting. + /// + public static readonly string RequiresEntryUsingLocaleFormat = "HtmlElements.InputTypeDate.RequiresEntryUsingLocaleFormat"; + + /// + /// Indicates that the web driver must use a JavaScript workaround to set the date, because it is impossible to do so + /// by typing keys. + /// + public static readonly string RequiresInputViaJavaScriptWorkaround = "HtmlElements.InputTypeDate.RequiresInputViaJavaScriptWorkaround"; + } + + /// + /// Flags relating to HTML <select multiple> elements. + /// + public static class SelectMultiple + { + /// + /// Indicates that the web driver must send Ctrl+Click in order to toggle the selection of a single + /// option within the select element. Without Ctrl, the click is interpreted as "change entire selection to just + /// the one option clicked". + /// + public static readonly string RequiresCtrlClickToToggleOptionSelection = "HtmlElements.SelectMultiple.RequiresCtrlClickToToggleOptionSelection"; + } + + /// + /// Flags relating to HTML <select> elements. + /// + public static class Select + { + /// + /// Indicates that the browser requires a JavaScript workaround in order to change the selection state + /// of an HTML <select> element. + /// + public static readonly string RequiresUpdatesViaJavaScriptWorkaround = "HtmlElements.Select.RequiresUpdatesViaJavaScriptWorkaround"; + } + } + } +} diff --git a/CSF.Screenplay.Selenium.BrowserFlags_old/Flags.cs b/CSF.Screenplay.Selenium.BrowserFlags_old/Flags.cs new file mode 100644 index 00000000..2a0f2e1c --- /dev/null +++ b/CSF.Screenplay.Selenium.BrowserFlags_old/Flags.cs @@ -0,0 +1,10 @@ +using System; +namespace CSF.Screenplay.Selenium +{ + /// + /// A catalogue of the the various browser flags of which this library is aware. + /// + public static partial class Flags + { + } +} diff --git a/CSF.Screenplay.Selenium.BrowserFlags_old/GetBrowserFlagsDefinitions.cs b/CSF.Screenplay.Selenium.BrowserFlags_old/GetBrowserFlagsDefinitions.cs new file mode 100644 index 00000000..d0a30191 --- /dev/null +++ b/CSF.Screenplay.Selenium.BrowserFlags_old/GetBrowserFlagsDefinitions.cs @@ -0,0 +1,123 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using CSF.WebDriverExtras.Flags; + +namespace CSF.Screenplay.Selenium +{ + /// + /// Helper type which gets browser flags definitions stored inside an assembly as embedded resources, + /// using the resource suffix .flags.json. + /// + public sealed class GetBrowserFlagsDefinitions + { + #region constants + + const string FlagsResourceSuffix = ".flags.json"; + + #endregion + + #region fields + + readonly IReadsFlagsDefinitions definitionReader; + readonly object syncRoot; + IReadOnlyCollection cachedDefinitions; + + #endregion + + #region properties + + Assembly ThisAssembly => Assembly.GetExecutingAssembly(); + + #endregion + + #region public methods + + /// + /// Gets the browser flags definitions contained within the CSF.Screenplay.Selenium.BrowserFlags assembly. + /// + /// The browser flags definitions. + public IReadOnlyCollection GetDefinitions() => GetDefinitions(ThisAssembly); + + /// + /// Gets the browser flags definitions contained within the given assembly. + /// + /// The browser flags definitions. + /// An assembly to search for flags definitions. + public IReadOnlyCollection GetDefinitions(Assembly assembly) + { + if(assembly == null) + throw new ArgumentNullException(nameof(assembly)); + + if(assembly != ThisAssembly) + { + return GetDefinitionsFromAssembly(assembly); + } + + lock(syncRoot) + { + if(cachedDefinitions != null) + return cachedDefinitions; + + cachedDefinitions = GetDefinitionsFromAssembly(assembly); + return cachedDefinitions; + } + } + + #endregion + + #region methods + + IReadOnlyCollection GetDefinitionsFromAssembly(Assembly assembly) + { + var resourceNames = GetDefinitionsResourceNames(assembly); + return resourceNames + .SelectMany(x => GetDefinitions(x, assembly)) + .ToArray(); + } + + IEnumerable GetDefinitionsResourceNames(Assembly assembly) + => assembly + .GetManifestResourceNames() + .Where(x => x.EndsWith(FlagsResourceSuffix, StringComparison.InvariantCulture)); + + IReadOnlyCollection GetDefinitions(string resourceName, Assembly assembly) + { + using(var stream = assembly.GetManifestResourceStream(resourceName)) + return definitionReader.GetFlagsDefinitions(stream); + } + + #endregion + + #region constructor + + /// + /// Initializes a new instance of the class. + /// + public GetBrowserFlagsDefinitions() : this(null) {} + + /// + /// Initializes a new instance of the class. + /// + /// Definition reader. + public GetBrowserFlagsDefinitions(IReadsFlagsDefinitions definitionReader) + { + syncRoot = new object(); + this.definitionReader = definitionReader ?? new DefinitionReader(); + } + + #endregion + + #region static methods + + /// + /// Helper method which gets the flags definitions from the CSF.Screenplay.Selenium.BrowserFlags assembly. + /// + /// The dlags definitions. + public static IReadOnlyCollection FromDefinitionsAssembly() + => new GetBrowserFlagsDefinitions().GetDefinitions(); + + #endregion + } +} diff --git a/CSF.Screenplay.Selenium.BrowserFlags_old/Properties/AssemblyInfo.cs b/CSF.Screenplay.Selenium.BrowserFlags_old/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..d14392e2 --- /dev/null +++ b/CSF.Screenplay.Selenium.BrowserFlags_old/Properties/AssemblyInfo.cs @@ -0,0 +1,26 @@ +using System.Reflection; +using System.Runtime.CompilerServices; + +// Information about this assembly is defined by the following attributes. +// Change them to the values specific to your project. + +[assembly: AssemblyTitle("CSF.Screenplay.Selenium.BrowserFlags")] +[assembly: AssemblyDescription("A repository of pre-defined Browser Flags for CSF.Screenplay.Selenium")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("CSF Software Limited")] +[assembly: AssemblyProduct("")] +[assembly: AssemblyCopyright("Craig Fowler")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// The assembly version has the format "{Major}.{Minor}.{Build}.{Revision}". +// The form "{Major}.{Minor}.*" will automatically update the build and revision, +// and "{Major}.{Minor}.{Build}.*" will update just the revision. + +[assembly: AssemblyVersion("1.0.0.0")] + +// The following attributes are used to specify the signing key for the assembly, +// if desired. See the Mono documentation for more information about signing. + +//[assembly: AssemblyDelaySign(false)] +//[assembly: AssemblyKeyFile("")] diff --git a/CSF.Screenplay.Selenium.BrowserFlags_old/ScreenplayLocator.cs b/CSF.Screenplay.Selenium.BrowserFlags_old/ScreenplayLocator.cs new file mode 100644 index 00000000..713eab14 --- /dev/null +++ b/CSF.Screenplay.Selenium.BrowserFlags_old/ScreenplayLocator.cs @@ -0,0 +1,99 @@ +using System; +using System.Collections.Concurrent; +using System.Reflection; +using NUnit.Framework.Interfaces; + +namespace CSF.Screenplay +{ + /// + /// A small static service locator of sorts, dedicated to getting an appropriate instance of for + /// a specified test object. + /// + /// + /// + /// This type uses reflection to find the which decorates the assembly in which the + /// specified object (a test, a test method or the assembly itself) resides. + /// It additionally caches the results in-memory to avoid repetitive reflection, only to retrieve the same results. + /// + /// + public static class ScreenplayLocator + { + static readonly ConcurrentDictionary screenplayCache = new ConcurrentDictionary(); + + /// + /// Gets a instance from the specified . + /// + /// + /// + /// This method makes use of the which decorates the assembly to get a + /// Screenplay object instance for that assembly. + /// If the specified assembly is not decorated with the Screenplay assembly attribute then this method will raise + /// an exception. + /// + /// + /// The test assembly for which to get a Screenplay object. + /// The Screenplay object for the specified assembly. + /// If is . + /// If the is not decorated with . + public static Screenplay GetScreenplay(Assembly assembly) + { + if (assembly is null) throw new ArgumentNullException(nameof(assembly)); + return screenplayCache.GetOrAdd(assembly, GetScreenplayFromAssembly); + } + + /// + /// Gets a instance from the specified test method. + /// + /// + /// + /// This method makes use of the which decorates the assembly in which the + /// specified method was declared, to get a Screenplay object instance applicable to the test method. + /// If the method's assembly is not decorated with the Screenplay assembly attribute then this method will raise + /// an exception. + /// + /// + /// The test method for which to get a Screenplay object. + /// The Screenplay object for the specified test method. + /// If is . + /// If the 's assembly is or + /// is not decorated with . + public static Screenplay GetScreenplay(IMethodInfo method) + { + if (method is null) throw new ArgumentNullException(nameof(method)); + + var assembly = method.MethodInfo?.DeclaringType?.Assembly; + if (assembly is null) + throw new ArgumentException($"The test method must have an associated {nameof(Assembly)}.", nameof(method)); + return GetScreenplay(assembly); + } + + /// + /// Gets a instance from the specified test. + /// + /// + /// + /// This method makes use of the which decorates the assembly in which the + /// specified test's method was declared, to get a Screenplay object instance applicable to the test method. + /// If the test's method's assembly is not decorated with the Screenplay assembly attribute then this method will raise + /// an exception. + /// + /// + /// The test for which to get a Screenplay object. + /// The Screenplay object for the specified test. + /// If is . + /// If the 's method's assembly is or + /// is not decorated with . + public static Screenplay GetScreenplay(ITest test) + { + if(test is null) throw new ArgumentNullException(nameof(test)); + return GetScreenplay(test.Method); + } + + static Screenplay GetScreenplayFromAssembly(Assembly assembly) + { + var assemblyAttrib = assembly.GetCustomAttribute() + ?? throw new ArgumentException($"The assembly {assembly.FullName} must be decorated with {nameof(ScreenplayAssemblyAttribute)}.", nameof(assembly)); + return assemblyAttrib.GetScreenplay(); + } + } +} diff --git a/CSF.Screenplay.Selenium.BrowserFlags_old/packages.config b/CSF.Screenplay.Selenium.BrowserFlags_old/packages.config new file mode 100644 index 00000000..51dabf90 --- /dev/null +++ b/CSF.Screenplay.Selenium.BrowserFlags_old/packages.config @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/CSF.Screenplay.Selenium.JavaScriptWorkarounds_old/CSF.Screenplay.Selenium.JavaScriptWorkarounds.csproj b/CSF.Screenplay.Selenium.JavaScriptWorkarounds_old/CSF.Screenplay.Selenium.JavaScriptWorkarounds.csproj new file mode 100644 index 00000000..fb372e78 --- /dev/null +++ b/CSF.Screenplay.Selenium.JavaScriptWorkarounds_old/CSF.Screenplay.Selenium.JavaScriptWorkarounds.csproj @@ -0,0 +1,76 @@ + + + + Debug + AnyCPU + {CC20F88D-C4B9-4FDB-B5B1-7349547430FA} + Library + CSF.Screenplay.Selenium + CSF.Screenplay.Selenium.JavaScriptWorkarounds + v4.5 + 1.0.0 + + + true + full + false + bin\Debug + DEBUG; + prompt + 4 + bin\Debug\CSF.Screenplay.Selenium.JavaScriptWorkarounds.xml + false + true + + + true + bin\Release + prompt + 4 + bin\Release\CSF.Screenplay.Selenium.JavaScriptWorkarounds.xml + false + + + + + ..\packages\Selenium.WebDriver.3.4.0\lib\net40\WebDriver.dll + + + + ..\packages\CSF.Reflection.1.0.3\lib\net45\CSF.Reflection.dll + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/CSF.Screenplay.Selenium.JavaScriptWorkarounds_old/Properties/AssemblyInfo.cs b/CSF.Screenplay.Selenium.JavaScriptWorkarounds_old/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..13b84a46 --- /dev/null +++ b/CSF.Screenplay.Selenium.JavaScriptWorkarounds_old/Properties/AssemblyInfo.cs @@ -0,0 +1,26 @@ +using System.Reflection; +using System.Runtime.CompilerServices; + +// Information about this assembly is defined by the following attributes. +// Change them to the values specific to your project. + +[assembly: AssemblyTitle("CSF.Screenplay.Selenium.JavaScriptWorkarounds")] +[assembly: AssemblyDescription("Workarounds, using JavaScript, for browser-specific issues relating to Selenium actions")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("CSF Software Limited")] +[assembly: AssemblyProduct("")] +[assembly: AssemblyCopyright("Craig Fowler")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// The assembly version has the format "{Major}.{Minor}.{Build}.{Revision}". +// The form "{Major}.{Minor}.*" will automatically update the build and revision, +// and "{Major}.{Minor}.{Build}.*" will update just the revision. + +[assembly: AssemblyVersion("1.0.0.0")] + +// The following attributes are used to specify the signing key for the assembly, +// if desired. See the Mono documentation for more information about signing. + +//[assembly: AssemblyDelaySign(false)] +//[assembly: AssemblyKeyFile("")] diff --git a/CSF.Screenplay.Selenium.JavaScriptWorkarounds_old/ScriptResources/ArgumentsArrayValidator.cs b/CSF.Screenplay.Selenium.JavaScriptWorkarounds_old/ScriptResources/ArgumentsArrayValidator.cs new file mode 100644 index 00000000..06e95245 --- /dev/null +++ b/CSF.Screenplay.Selenium.JavaScriptWorkarounds_old/ScriptResources/ArgumentsArrayValidator.cs @@ -0,0 +1,57 @@ +// +// ArgumentsArrayValidator.cs +// +// Author: +// Craig Fowler +// +// Copyright (c) 2018 Craig Fowler +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +using System; +using CSF.Screenplay.Selenium.StoredScripts; + +namespace CSF.Screenplay.Selenium.ScriptResources +{ + /// + /// A stored JavaScript which validates a collection of arguments passed as an array. + /// + public class ArgumentsArrayValidator : ScriptResource + { + const string EntryPointNameConst = "validateArgumentsArray"; + + /// + /// Gets the name of the entry point for this script (which differs from the default). + /// + /// The name of the entry point. + public static string EntryPointName => EntryPointNameConst; + + /// + /// Gets the name of this script. + /// + /// The name. + public override string Name => "a JavaScript which validates function arguments arrays"; + + /// + /// Gets the name of the entry point to the script - this is the function exposed by + /// . + /// + /// The name of the entry point function. + public override string GetEntryPointName() => EntryPointName; + } +} diff --git a/CSF.Screenplay.Selenium.JavaScriptWorkarounds_old/ScriptResources/ArgumentsArrayValidator.js b/CSF.Screenplay.Selenium.JavaScriptWorkarounds_old/ScriptResources/ArgumentsArrayValidator.js new file mode 100644 index 00000000..6b653445 --- /dev/null +++ b/CSF.Screenplay.Selenium.JavaScriptWorkarounds_old/ScriptResources/ArgumentsArrayValidator.js @@ -0,0 +1,87 @@ +var argumentsArrayValidator = function() +{ + 'use strict'; + + function getLengthParams(min, max) + { + if(typeof min === 'number' && max === undefined) + { + return { + count: min, + validateCount: true, + }; + } + + var minCount, validateMinCount = false, maxCount, validateMaxCount = false; + + if(typeof min === 'number') + { + minCount = min; + validateMinCount = true; + } + + if(typeof max === 'number') + { + maxCount = max; + validateMaxCount = true; + } + + return { + min: minCount, + validateMin: validateMinCount, + max: maxCount, + validateMax: validateMaxCount, + validateCount: false, + }; + } + + function validateType(argsArray) + { + if(!argsArray) throw new Error('Arguments must be a non-null array.'); + if(!(argsArray instanceof Array)) throw new Error('Arguments must be an array.'); + } + + function validateLength(argsArray, lengthParams) + { + if(lengthParams.validateCount && argsArray.length !== lengthParams.count) + throw new Error('Arguments array must contain precisely ' + lengthParams.count + ' item(s).'); + + if(lengthParams.validateMin && argsArray.length < lengthParams.min) + throw new Error('Arguments array must contain at least ' + lengthParams.min + ' item(s).'); + + if(lengthParams.validateMax && argsArray.length > lengthParams.max) + throw new Error('Arguments array must contain no more than ' + lengthParams.max + ' item(s).'); + } + + function validate(argsArray, min, max) + { + validateType(argsArray); + var lengthParams = getLengthParams(min, max); + validateLength(argsArray, lengthParams); + + return true; + } + + function selfValidate(argsArray, min, max) + { + try + { + return validate(argsArray, min, max); + } + catch(e) { throw new Error('The call to \'validateArgumentsArray\' raised an error: ' + e.message); } + } + + return { + validate: validate, + selfValidate: selfValidate, + }; +}(); + +function validateArgumentsArray(argsArray) +{ + 'use strict'; + + validator.selfValidate(argsArray, 1, 3); + var arrayToValidate = argsArray[0], min = argsArray[1], max = argsArray[2]; + return validator.validate(arrayToValidate, min, max); +} \ No newline at end of file diff --git a/CSF.Screenplay.Selenium.JavaScriptWorkarounds_old/ScriptResources/GetALocalisedDate.cs b/CSF.Screenplay.Selenium.JavaScriptWorkarounds_old/ScriptResources/GetALocalisedDate.cs new file mode 100644 index 00000000..8cb0206f --- /dev/null +++ b/CSF.Screenplay.Selenium.JavaScriptWorkarounds_old/ScriptResources/GetALocalisedDate.cs @@ -0,0 +1,64 @@ +// +// GetALocalisedDate.cs +// +// Author: +// Craig Fowler +// +// Copyright (c) 2018 Craig Fowler +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +using System; +using CSF.Screenplay.Selenium.StoredScripts; + +namespace CSF.Screenplay.Selenium.ScriptResources +{ + /// + /// Script resource for getting a localised date string + /// + public class GetALocalisedDate : ScriptResource + { + readonly ScriptResource argsValidator; + + /// + /// Gets the name of this script. + /// + /// The name. + public override string Name => "a JavaScript which converts a date to a locale-formatted string"; + + /// + /// Gets a collection of scripts which the current script instance depends upon. + /// + /// The dependencies. + protected override ScriptResource[] GetDependencies() => new [] { argsValidator }; + + /// + /// Initializes a new instance of the class. + /// + public GetALocalisedDate() : this(null) {} + + /// + /// Initializes a new instance of the class. + /// + /// Arguments validator. + public GetALocalisedDate(ScriptResource argsValidator) + { + this.argsValidator = argsValidator ?? new ArgumentsArrayValidator(); + } + } +} diff --git a/CSF.Screenplay.Selenium.JavaScriptWorkarounds_old/ScriptResources/GetALocalisedDate.js b/CSF.Screenplay.Selenium.JavaScriptWorkarounds_old/ScriptResources/GetALocalisedDate.js new file mode 100644 index 00000000..9456b68c --- /dev/null +++ b/CSF.Screenplay.Selenium.JavaScriptWorkarounds_old/ScriptResources/GetALocalisedDate.js @@ -0,0 +1,17 @@ +function executeScript(argsArray) { + 'use strict'; + + argumentsArrayValidator.validate(argsArray, 3); + + var + year = argsArray[0], + month = argsArray[1], + day = argsArray[2], + theDate; + + if(typeof year !== 'number' || typeof month !== 'number' || typeof day !== 'number') + throw new Error('The supplied year, month and day must all be numbers.'); + + theDate = new Date(year, month, day); + return theDate.toLocaleDateString(); +} \ No newline at end of file diff --git a/CSF.Screenplay.Selenium.JavaScriptWorkarounds_old/ScriptResources/GetDocumentReadyState.cs b/CSF.Screenplay.Selenium.JavaScriptWorkarounds_old/ScriptResources/GetDocumentReadyState.cs new file mode 100644 index 00000000..bff50835 --- /dev/null +++ b/CSF.Screenplay.Selenium.JavaScriptWorkarounds_old/ScriptResources/GetDocumentReadyState.cs @@ -0,0 +1,42 @@ +// +// GetDocumentReadyState.cs +// +// Author: +// Craig Fowler +// +// Copyright (c) 2018 Craig Fowler +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +using System; +using CSF.Screenplay.Selenium.StoredScripts; + +namespace CSF.Screenplay.Selenium.ScriptResources +{ + /// + /// Script resource for getting the document ready state. + /// + public class GetDocumentReadyState : ScriptResource + { + /// + /// Gets the name of this script. + /// + /// The name. + public override string Name => "a JavaScript which gets the current web page's ready-state"; + } +} diff --git a/CSF.Screenplay.Selenium.JavaScriptWorkarounds_old/ScriptResources/GetDocumentReadyState.js b/CSF.Screenplay.Selenium.JavaScriptWorkarounds_old/ScriptResources/GetDocumentReadyState.js new file mode 100644 index 00000000..b8953856 --- /dev/null +++ b/CSF.Screenplay.Selenium.JavaScriptWorkarounds_old/ScriptResources/GetDocumentReadyState.js @@ -0,0 +1,4 @@ +function executeScript(argsArray) { + 'use strict'; + return document.readyState; +} \ No newline at end of file diff --git a/CSF.Screenplay.Selenium.JavaScriptWorkarounds_old/ScriptResources/SetAnElementAttribute.cs b/CSF.Screenplay.Selenium.JavaScriptWorkarounds_old/ScriptResources/SetAnElementAttribute.cs new file mode 100644 index 00000000..93126cf8 --- /dev/null +++ b/CSF.Screenplay.Selenium.JavaScriptWorkarounds_old/ScriptResources/SetAnElementAttribute.cs @@ -0,0 +1,48 @@ +// +// SetAnElementAttribute.cs +// +// Author: +// Craig Fowler +// +// Copyright (c) 2018 Craig Fowler +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +using System; +using CSF.Screenplay.Selenium.StoredScripts; + +namespace CSF.Screenplay.Selenium.ScriptResources +{ + /// + /// Script resource for setting an attribute upon an element. + /// + public class SetAnElementAttribute : ScriptResource + { + /// + /// Gets the name of this script. + /// + /// The name. + public override string Name => "a JavaScript which sets an attribute upon an element"; + + /// + /// Gets a collection of scripts which the current script instance depends upon. + /// + /// The dependencies. + protected override ScriptResource[] GetDependencies() => new [] { new ArgumentsArrayValidator() }; + } +} diff --git a/CSF.Screenplay.Selenium.JavaScriptWorkarounds_old/ScriptResources/SetAnElementAttribute.js b/CSF.Screenplay.Selenium.JavaScriptWorkarounds_old/ScriptResources/SetAnElementAttribute.js new file mode 100644 index 00000000..79c5f7e9 --- /dev/null +++ b/CSF.Screenplay.Selenium.JavaScriptWorkarounds_old/ScriptResources/SetAnElementAttribute.js @@ -0,0 +1,24 @@ +function executeScript(argsArray) +{ + 'use strict'; + + argumentsArrayValidator.validate(argsArray, 3); + + var htmlElement = argsArray[0], attributeName = argsArray[1], newValue = argsArray[2]; + + function validateElement(element) + { + if(element === null || element === undefined) + throw new Error('You must provide an HTML element object.'); + if(!(element instanceof HTMLElement)) + throw new Error('The element must be an HTML element.'); + } + + validateElement(htmlElement); + if(newValue === null) + htmlElement.removeAttribute(attributeName); + else + htmlElement.setAttribute(attributeName, newValue); + + return true; +} diff --git a/CSF.Screenplay.Selenium.JavaScriptWorkarounds_old/ScriptResources/SetAnElementValue.cs b/CSF.Screenplay.Selenium.JavaScriptWorkarounds_old/ScriptResources/SetAnElementValue.cs new file mode 100644 index 00000000..3b9279da --- /dev/null +++ b/CSF.Screenplay.Selenium.JavaScriptWorkarounds_old/ScriptResources/SetAnElementValue.cs @@ -0,0 +1,64 @@ +// +// SetAnElementValue.cs +// +// Author: +// Craig Fowler +// +// Copyright (c) 2018 Craig Fowler +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +using System; +using CSF.Screenplay.Selenium.StoredScripts; + +namespace CSF.Screenplay.Selenium.ScriptResources +{ + /// + /// A stored JavaScript which sets the value of an HTML element and then triggers the 'change' event for that element. + /// + public class SetAnElementValue : ScriptResource + { + readonly ScriptResource argsValidator; + + /// + /// Gets the name of this script. + /// + /// The name. + public override string Name => "a JavaScript to set the value of an HTML element"; + + /// + /// Gets a collection of scripts which the current script instance depends upon. + /// + /// The dependencies. + protected override ScriptResource[] GetDependencies() => new [] { argsValidator }; + + /// + /// Initializes a new instance of the class. + /// + public SetAnElementValue() : this(null) {} + + /// + /// Initializes a new instance of the class. + /// + /// Arguments validator. + public SetAnElementValue(ScriptResource argsValidator) + { + this.argsValidator = argsValidator ?? new ArgumentsArrayValidator(); + } + } +} diff --git a/CSF.Screenplay.Selenium.JavaScriptWorkarounds_old/ScriptResources/SetAnElementValue.js b/CSF.Screenplay.Selenium.JavaScriptWorkarounds_old/ScriptResources/SetAnElementValue.js new file mode 100644 index 00000000..d93f5b4f --- /dev/null +++ b/CSF.Screenplay.Selenium.JavaScriptWorkarounds_old/ScriptResources/SetAnElementValue.js @@ -0,0 +1,36 @@ +function executeScript(argsArray) +{ + 'use strict'; + + argumentsArrayValidator.validate(argsArray, 2); + + var htmlElement = argsArray[0], newValue = argsArray[1]; + + function validateElement(element) + { + if(element === null || element === undefined) + throw new Error('You must provide an HTML element object.'); + if(!(element instanceof HTMLElement)) + throw new Error('The element must be an HTML element.'); + if(element.value === undefined) + throw new Error('The element must have a \'value\' property.'); + } + + function setValue(element, val) + { + element.value = val; + } + + function triggerChangeEvent(element) + { + var ev = document.createEvent('Event'); + ev.initEvent('change', true, true); + element.dispatchEvent(ev); + } + + validateElement(htmlElement); + setValue(htmlElement, newValue); + triggerChangeEvent(htmlElement); + + return true; +} diff --git a/CSF.Screenplay.Selenium.JavaScriptWorkarounds_old/ScriptResources/UpdateSelectElementSelection.cs b/CSF.Screenplay.Selenium.JavaScriptWorkarounds_old/ScriptResources/UpdateSelectElementSelection.cs new file mode 100644 index 00000000..ef9a59a6 --- /dev/null +++ b/CSF.Screenplay.Selenium.JavaScriptWorkarounds_old/ScriptResources/UpdateSelectElementSelection.cs @@ -0,0 +1,83 @@ +// +// UpdateSelectElementSelection.cs +// +// Author: +// Craig Fowler +// +// Copyright (c) 2018 Craig Fowler +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +using System; +using CSF.Screenplay.Selenium.StoredScripts; + +namespace CSF.Screenplay.Selenium.ScriptResources +{ + /// + /// A stored JavaScript which sets the selected option(s) for an HTML <select> element. + /// + public class UpdateSelectElementSelection : ScriptResource + { + /// + /// Gets the action name for deselecting every option. + /// + public static readonly string DeselectAllActionName = "deselectAll"; + + /// + /// Gets the action name for selecting an option by its zero-based index. + /// + public static readonly string SelectByIndexActionName = "selectByIndex"; + + /// + /// Gets the action name for selecting an option by its value. + /// + public static readonly string SelectByValueActionName = "selectByValue"; + + /// + /// Gets the action name for selecting an option by its displyed text. + /// + public static readonly string SelectByTextActionName = "selectByText"; + + /// + /// Gets the action name for deselecting an option by its zero-based index. + /// + public static readonly string DeselectByIndexActionName = "deselectByIndex"; + + /// + /// Gets the action name for deselecting an option by its value. + /// + public static readonly string DeselectByValueActionName = "deselectByValue"; + + /// + /// Gets the action name for deselecting an option by its displayed text. + /// + public static readonly string DeselectByTextActionName = "deselectByText"; + + /// + /// Gets the name of this script. + /// + /// The name. + public override string Name => "a JavaScript to set the select options of an HTML element.'); + } + + var optionUpdater = function() + { + function triggerChangeEvent(element) + { + var ev = document.createEvent('Event'); + ev.initEvent('change', true, true); + element.dispatchEvent(ev); + } + + function getOptionByIndex(selectElement, index) + { + return selectElement.item(index); + } + + function getOptionByValue(selectElement, value) + { + var options = selectElement.options; + for(var i = 0, len = options.length; i < len; i++) + if(options[i].value === value) return options[i]; + return null; + } + + function getOptionByText(selectElement, text) + { + var options = selectElement.options; + for(var i = 0, len = options.length; i < len; i++) + if(options[i].text === text) return options[i]; + return null; + } + + function setSelectedByIndex(selectElement, index, selected) + { + var option = getOptionByIndex(selectElement, index); + return setSelected(selectElement, option, selected); + } + + function setSelectedByValue(selectElement, value, selected) + { + var option = getOptionByValue(selectElement, value); + return setSelected(selectElement, option, selected); + } + + function setSelectedByText(selectElement, text, selected) + { + var option = getOptionByText(selectElement, text); + return setSelected(selectElement, option, selected); + } + + function setSelected(selectElement, option, selected) + { + if(!option) return false; + option.selected = selected; + triggerChangeEvent(selectElement); + return true; + } + + function deselectAll(selectElement) + { + var options = selectElement.options; + for(var i = 0, len = options.length; i < len; i++) + options.item(i).selected = false; + triggerChangeEvent(selectElement); + return options.length > 0; + } + + return { + selectByIndex: function(ele, idx) { return setSelectedByIndex(ele, idx, true); }, + selectByValue: function(ele, val) { return setSelectedByValue(ele, val, true); }, + selectByText: function(ele, txt) { return setSelectedByText(ele, txt, true); }, + deselectByIndex: function(ele, idx) { return setSelectedByIndex(ele, idx, false); }, + deselectByValue: function(ele, val) { return setSelectedByValue(ele, val, false); }, + deselectByText: function(ele, txt) { return setSelectedByText(ele, txt, false); }, + deselectAll: deselectAll, + }; + + }(); + + var + element = argsArray[0], + action = argsArray[1], + value = argsArray[2]; + + validateElement(element); + + if(!optionUpdater.hasOwnProperty(action)) + throw new Error("The action '" + action + "' was not recognised."); + + return optionUpdater[action](element, value); +} \ No newline at end of file diff --git a/CSF.Screenplay.Selenium.JavaScriptWorkarounds_old/StoredScripts/ICreatesInvocationScript.cs b/CSF.Screenplay.Selenium.JavaScriptWorkarounds_old/StoredScripts/ICreatesInvocationScript.cs new file mode 100644 index 00000000..339a9f34 --- /dev/null +++ b/CSF.Screenplay.Selenium.JavaScriptWorkarounds_old/StoredScripts/ICreatesInvocationScript.cs @@ -0,0 +1,41 @@ +// +// IInvokesScripts.cs +// +// Author: +// Craig Fowler +// +// Copyright (c) 2018 Craig Fowler +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +using System; +namespace CSF.Screenplay.Selenium.StoredScripts +{ + /// + /// A service which creates a JavaScript which is used for the purpose of invoking other JavaScripts. + /// + public interface ICreatesInvocationScript + { + /// + /// Gets an invocation script for the given entry point name. + /// + /// The invocation script. + /// The name of the entry point which should be invoked. + string GetScript(string entryPoint); + } +} diff --git a/CSF.Screenplay.Selenium.JavaScriptWorkarounds_old/StoredScripts/IProvidesScript.cs b/CSF.Screenplay.Selenium.JavaScriptWorkarounds_old/StoredScripts/IProvidesScript.cs new file mode 100644 index 00000000..9b0461aa --- /dev/null +++ b/CSF.Screenplay.Selenium.JavaScriptWorkarounds_old/StoredScripts/IProvidesScript.cs @@ -0,0 +1,53 @@ +// +// IStoredScript.cs +// +// Author: +// Craig Fowler +// +// Copyright (c) 2018 Craig Fowler +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +using System; +namespace CSF.Screenplay.Selenium.StoredScripts +{ + /// + /// A service which provides a JavaScript fragment with a named entry point. + /// + public interface IProvidesScript + { + /// + /// Gets the name of this script. + /// + /// The name. + string Name { get; } + + /// + /// Gets a JavaScript which includes a named function. The name of that function is exposed via + /// . + /// + /// The script. + string GetScript(); + + /// + /// Gets the name of the entry point to the script - this is the function exposed by . + /// + /// The name of the entry point function. + string GetEntryPointName(); + } +} diff --git a/CSF.Screenplay.Selenium.JavaScriptWorkarounds_old/StoredScripts/IRunsScripts.cs b/CSF.Screenplay.Selenium.JavaScriptWorkarounds_old/StoredScripts/IRunsScripts.cs new file mode 100644 index 00000000..244a2c2c --- /dev/null +++ b/CSF.Screenplay.Selenium.JavaScriptWorkarounds_old/StoredScripts/IRunsScripts.cs @@ -0,0 +1,54 @@ +// +// IRunsStoredScripts.cs +// +// Author: +// Craig Fowler +// +// Copyright (c) 2018 Craig Fowler +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +using System; +using OpenQA.Selenium; + +namespace CSF.Screenplay.Selenium.StoredScripts +{ + /// + /// A service which executes JavaScripts using given parameters. + /// + public interface IRunsScripts + { + /// + /// Executes the script exposed by the given script provider and returns the result. + /// + /// The script result. + /// A JavaScript provider. + /// A web driver. + /// The script arguments. + object ExecuteScript(IProvidesScript script, IWebDriver webDriver, params object[] arguments); + + /// + /// Executes the script and returns the result. + /// + /// The script result. + /// A JavaScript. + /// A web driver. + /// The script arguments. + object ExecuteScript(string script, IWebDriver webDriver, params object[] arguments); + } +} diff --git a/CSF.Screenplay.Selenium.JavaScriptWorkarounds_old/StoredScripts/ScriptInvokerFactory.cs b/CSF.Screenplay.Selenium.JavaScriptWorkarounds_old/StoredScripts/ScriptInvokerFactory.cs new file mode 100644 index 00000000..c3776f33 --- /dev/null +++ b/CSF.Screenplay.Selenium.JavaScriptWorkarounds_old/StoredScripts/ScriptInvokerFactory.cs @@ -0,0 +1,60 @@ +// +// ArgumentsArrayConverter.cs +// +// Author: +// Craig Fowler +// +// Copyright (c) 2018 Craig Fowler +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +using System; +namespace CSF.Screenplay.Selenium.StoredScripts +{ + /// + /// A special stored JavaScript which is used to invoke other JavaScripts. + /// + public class ScriptInvokerFactory : ICreatesInvocationScript + { + readonly ScriptResourceLoader loader; + + /// + /// Gets an invocation script for the given entry point name. + /// + /// The invocation script. + /// The name of the entry point which should be invoked. + public string GetScript(string entryPoint) + { + var invokerService = loader.GetScriptFor(); + var invocationLine = GetInvocationLine(entryPoint); + + return String.Concat(invokerService, Environment.NewLine, invocationLine); + } + + string GetInvocationLine(string entryPoint) + => $"return invoker.invoke({entryPoint}, arguments);"; + + /// + /// Initializes a new instance of the class. + /// + public ScriptInvokerFactory() + { + loader = new ScriptResourceLoader(); + } + } +} diff --git a/CSF.Screenplay.Selenium.JavaScriptWorkarounds_old/StoredScripts/ScriptInvokerFactory.js b/CSF.Screenplay.Selenium.JavaScriptWorkarounds_old/StoredScripts/ScriptInvokerFactory.js new file mode 100644 index 00000000..689a2f9b --- /dev/null +++ b/CSF.Screenplay.Selenium.JavaScriptWorkarounds_old/StoredScripts/ScriptInvokerFactory.js @@ -0,0 +1,24 @@ +var invoker = function() +{ + 'use strict'; + + function validateEntryPoint(entryPoint) + { + if(!entryPoint || typeof entryPoint !== 'function') + throw new Error('The script entry-point must be a function'); + } + + function getArgumentsObjectAsArray(argumentsObject) + { + return Array.prototype.slice.call(argumentsObject); + } + + function invoke(entryPoint, argumentsObject) + { + validateEntryPoint(entryPoint); + var args = getArgumentsObjectAsArray(argumentsObject); + return entryPoint(args); + } + + return { invoke: invoke }; +}(); diff --git a/CSF.Screenplay.Selenium.JavaScriptWorkarounds_old/StoredScripts/ScriptResource.cs b/CSF.Screenplay.Selenium.JavaScriptWorkarounds_old/StoredScripts/ScriptResource.cs new file mode 100644 index 00000000..2f511b50 --- /dev/null +++ b/CSF.Screenplay.Selenium.JavaScriptWorkarounds_old/StoredScripts/ScriptResource.cs @@ -0,0 +1,103 @@ +// +// ScriptResource.cs +// +// Author: +// Craig Fowler +// +// Copyright (c) 2018 Craig Fowler +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +using System; +using System.Linq; + +namespace CSF.Screenplay.Selenium.StoredScripts +{ + /// + /// Abstract implementation of for a JavaScript which is stored as an embedded resource. + /// + public abstract class ScriptResource : IProvidesScript + { + internal const string + DefaultEntryPointName = "executeScript"; + readonly ScriptResourceLoader scriptLoader; + + /// + /// Gets the name of this script. + /// + /// The name. + public virtual string Name => GetType().Name; + + /// + /// Gets the name of the entry point to the script - this is the function exposed by + /// . + /// + /// The name of the entry point function. + public virtual string GetEntryPointName() => DefaultEntryPointName; + + /// + /// Gets a JavaScript which includes a named function. The name of that function is exposed via + /// . + /// + /// The script. + public virtual string GetScript() => CombineScripts(this, GetDependencies()); + + /// + /// Gets the script fragment as a named function (without any dependency scripts, if any). + /// The name of that function is exposed via + /// . + /// + /// The current script. + protected virtual string GetScriptWithoutDependencies() => scriptLoader.GetScriptFor(GetType()); + + /// + /// Gets a collection of scripts which the current script instance depends upon. + /// + /// The dependencies. + protected virtual ScriptResource[] GetDependencies() => new ScriptResource[0]; + + /// + /// Combines the given script (which provides an entry point) with other scripts, creating one long script. + /// + /// The scripts. + /// Entry point provider. + /// Scripts. + protected string CombineScripts(ScriptResource entryPointProvider, params ScriptResource[] scripts) + { + if(entryPointProvider == null) + throw new ArgumentNullException(nameof(entryPointProvider)); + if(scripts == null) + throw new ArgumentNullException(nameof(scripts)); + + var scriptsToCombine = scripts + .Select(x => x.GetScript()) + .Union(new [] { entryPointProvider.GetScriptWithoutDependencies() }) + .ToArray(); + + return String.Join(Environment.NewLine, scriptsToCombine); + } + + /// + /// Initializes a new instance of the class. + /// + public ScriptResource() + { + scriptLoader = new ScriptResourceLoader(); + } + } +} diff --git a/CSF.Screenplay.Selenium.JavaScriptWorkarounds_old/StoredScripts/ScriptResourceLoader.cs b/CSF.Screenplay.Selenium.JavaScriptWorkarounds_old/StoredScripts/ScriptResourceLoader.cs new file mode 100644 index 00000000..55b0f44d --- /dev/null +++ b/CSF.Screenplay.Selenium.JavaScriptWorkarounds_old/StoredScripts/ScriptResourceLoader.cs @@ -0,0 +1,59 @@ +// +// ScriptResourceLoader.cs +// +// Author: +// Craig Fowler +// +// Copyright (c) 2018 Craig Fowler +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +using System; +using CSF.Reflection; + +namespace CSF.Screenplay.Selenium.StoredScripts +{ + /// + /// Loads a string manifest resource representing a JavaScript which matches the name of a System.Type. + /// + public class ScriptResourceLoader + { + /// + /// Gets the JavaScript for the given type. + /// + /// The JavaScript resource. + /// The type for which to load script. + public string GetScriptFor() + => GetScriptFor(typeof(T)); + + /// + /// Gets the JavaScript for the given type. + /// + /// The JavaScript resource. + /// The type for which to load script. + public string GetScriptFor(Type type) + { + if(type == null) + throw new ArgumentNullException(nameof(type)); + + var scriptAssembly = type.Assembly; + + return scriptAssembly.GetManifestResourceText(type, $"{type.Name}.js"); + } + } +} diff --git a/CSF.Screenplay.Selenium.JavaScriptWorkarounds_old/StoredScripts/ScriptRunner.cs b/CSF.Screenplay.Selenium.JavaScriptWorkarounds_old/StoredScripts/ScriptRunner.cs new file mode 100644 index 00000000..769d54ff --- /dev/null +++ b/CSF.Screenplay.Selenium.JavaScriptWorkarounds_old/StoredScripts/ScriptRunner.cs @@ -0,0 +1,106 @@ +// +// ScriptRunner.cs +// +// Author: +// Craig Fowler +// +// Copyright (c) 2018 Craig Fowler +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +using System; +using OpenQA.Selenium; + +namespace CSF.Screenplay.Selenium.StoredScripts +{ + /// + /// Default implementation of + /// + public class ScriptRunner : IRunsScripts + { + readonly ICreatesInvocationScript invoker; + + /// + /// Executes the script and returns the result. + /// + /// The script result. + /// A JavaScript. + /// A web driver. + /// The script arguments. + public object ExecuteScript(string script, IWebDriver webDriver, params object[] arguments) + { + if(script == null) + throw new ArgumentNullException(nameof(script)); + if(webDriver == null) + throw new ArgumentNullException(nameof(webDriver)); + + var javaScriptRunner = GetJavaScriptExecutor(webDriver); + return javaScriptRunner.ExecuteScript(script, arguments); + } + + /// + /// Executes the script exposed by the given script provider and returns the result. + /// + /// The script result. + /// A JavaScript provider. + /// A web driver. + /// The script arguments. + public object ExecuteScript(IProvidesScript script, IWebDriver webDriver, params object[] arguments) + { + if(script == null) + throw new ArgumentNullException(nameof(script)); + if(webDriver == null) + throw new ArgumentNullException(nameof(webDriver)); + + var scriptBody = GetScriptBodyWithInvoker(script); + return ExecuteScript(scriptBody, webDriver, arguments); + } + + string GetScriptBodyWithInvoker(IProvidesScript script) + { + var scriptBody = script.GetScript(); + var invokerBody = invoker.GetScript(script.GetEntryPointName()); + + return String.Concat(scriptBody, Environment.NewLine, invokerBody); + } + + IJavaScriptExecutor GetJavaScriptExecutor(IWebDriver driver) + { + var jsDriver = driver as IJavaScriptExecutor; + + if(jsDriver == null) + throw new ArgumentException($"The {nameof(IWebDriver)} must support the execution of JavaScript.", nameof(driver)); + + return jsDriver; + } + + /// + /// Initializes a new instance of the class. + /// + public ScriptRunner() : this(null) {} + + /// + /// Initializes a new instance of the class. + /// + /// A JavaScript which invokes other scripts via their named entry points. + public ScriptRunner(ICreatesInvocationScript invoker) + { + this.invoker = invoker ?? new ScriptInvokerFactory(); + } + } +} diff --git a/CSF.Screenplay.Selenium.JavaScriptWorkarounds_old/packages.config b/CSF.Screenplay.Selenium.JavaScriptWorkarounds_old/packages.config new file mode 100644 index 00000000..a8aa0123 --- /dev/null +++ b/CSF.Screenplay.Selenium.JavaScriptWorkarounds_old/packages.config @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/CSF.Screenplay.Selenium.Tests_old/Abilities/BrowseTheWebTests.cs b/CSF.Screenplay.Selenium.Tests_old/Abilities/BrowseTheWebTests.cs new file mode 100644 index 00000000..b58ac6a4 --- /dev/null +++ b/CSF.Screenplay.Selenium.Tests_old/Abilities/BrowseTheWebTests.cs @@ -0,0 +1,27 @@ +using NUnit.Framework; +using System; +using CSF.Screenplay.Selenium.Abilities; +using Moq; +using OpenQA.Selenium; +using CSF.Screenplay.Abilities; + +namespace CSF.Screenplay.Selenium.Tests.Abilities +{ + [TestFixture] + public class BrowseTheWebTests + { + [Test] + public void Dispose_should_never_dispose_the_web_driver() + { + // Arrange + var webDriver = Mock.Of(); + IAbility sut = new BrowseTheWeb(webDriver); + + // Act + sut.Dispose(); + + // Assert + Mock.Get(webDriver).Verify(x => x.Dispose(), Times.Never()); + } + } +} diff --git a/CSF.Screenplay.Selenium.Tests_old/Actions/ClearCookiesTests.cs b/CSF.Screenplay.Selenium.Tests_old/Actions/ClearCookiesTests.cs new file mode 100644 index 00000000..345d8113 --- /dev/null +++ b/CSF.Screenplay.Selenium.Tests_old/Actions/ClearCookiesTests.cs @@ -0,0 +1,29 @@ +using System; +using CSF.Screenplay.NUnit; +using CSF.Screenplay.Actors; +using CSF.Screenplay.Selenium.Abilities; +using CSF.Screenplay.Selenium.Builders; +using CSF.Screenplay.Selenium.Tests.Tasks; +using NUnit.Framework; +using static CSF.Screenplay.StepComposer; + +namespace CSF.Screenplay.Selenium.Tests.Actions +{ + [TestFixture] + [Description("The 'clear cookies' action")] + public class ClearCookiesTests + { + [Test,Screenplay] + [Description("Clearing all cookies does not raise an exception.")] + public void Clear_all_cookies_does_not_raise_an_exception(ICast cast, BrowseTheWeb browseTheWeb) + { + var joe = cast.Get("Joe");joe.IsAbleTo(browseTheWeb); + + joe.ShouldIgnoreThisTestIfTheirBrowserHasAnyOfTheFlags(Flags.Browser.CannotClearDomainCookies); + + Given(joe).WasAbleTo(new EnterTextIntoThePageTwoInputField("Some text")); + + Assert.DoesNotThrow(() => When(joe).AttemptsTo(ClearTheirBrowser.Cookies())); + } + } +} diff --git a/CSF.Screenplay.Selenium.Tests_old/Actions/ClearTests.cs b/CSF.Screenplay.Selenium.Tests_old/Actions/ClearTests.cs new file mode 100644 index 00000000..df443cc9 --- /dev/null +++ b/CSF.Screenplay.Selenium.Tests_old/Actions/ClearTests.cs @@ -0,0 +1,33 @@ +using CSF.Screenplay.Selenium.Builders; +using CSF.Screenplay.Selenium.Tests.Pages; +using CSF.Screenplay.NUnit; +using FluentAssertions; +using NUnit.Framework; +using static CSF.Screenplay.StepComposer; +using CSF.Screenplay.Selenium.Tests.Tasks; +using CSF.Screenplay.Actors; +using CSF.Screenplay.Selenium.Abilities; +using System; + +namespace CSF.Screenplay.Selenium.Tests.Actions +{ + [TestFixture] + [Description("The clear action")] + public class ClearTests + { + [Test,Screenplay] + [Description("Clearing an element after entering some text results in an empty string.")] + public void Clear_an_element_after_entering_some_text_results_in_an_empty_string(ICast cast, BrowseTheWeb browseTheWeb) + { + var joe = cast.Get("Joe");joe.IsAbleTo(browseTheWeb); + + Given(joe).WasAbleTo(new EnterTextIntoThePageTwoInputField("Some text")); + + When(joe).AttemptsTo(Clear.TheContentsOf(PageTwo.SpecialInputField)); + + Then(joe).ShouldSee(TheValue.Of(PageTwo.SpecialInputField)) + .Should() + .BeEmpty(); + } + } +} diff --git a/CSF.Screenplay.Selenium.Tests_old/Actions/ClickTests.cs b/CSF.Screenplay.Selenium.Tests_old/Actions/ClickTests.cs new file mode 100644 index 00000000..e46c3d52 --- /dev/null +++ b/CSF.Screenplay.Selenium.Tests_old/Actions/ClickTests.cs @@ -0,0 +1,32 @@ +using CSF.Screenplay.Selenium.Builders; +using CSF.Screenplay.Selenium.Tests.Pages; +using CSF.Screenplay.NUnit; +using FluentAssertions; +using NUnit.Framework; +using static CSF.Screenplay.StepComposer; +using CSF.Screenplay.Actors; +using CSF.Screenplay.Selenium.Abilities; +using System; + +namespace CSF.Screenplay.Selenium.Tests.Actions +{ + [TestFixture] + [Description("The click action")] + public class ClickTests + { + [Test,Screenplay] + [Description("Clicking on the link to page two navigates to the second page.")] + public void Click_OnLinkToPageTwo_navigates_to_second_page(ICast cast, BrowseTheWeb browseTheWeb) + { + var joe = cast.Get("Joe");joe.IsAbleTo(browseTheWeb); + + Given(joe).WasAbleTo(OpenTheirBrowserOn.ThePage()); + + When(joe).AttemptsTo(Click.On(HomePage.SecondPageLink)); + + Then(joe).Should(Wait.ForAtMost(2).Seconds().OrUntil(PageTwo.SpecialInputField).IsVisible()); + + Then(joe).ShouldSee(TheWindow.Title()).Should().Be("Page two"); + } + } +} diff --git a/CSF.Screenplay.Selenium.Tests_old/Actions/DeselectTests.cs b/CSF.Screenplay.Selenium.Tests_old/Actions/DeselectTests.cs new file mode 100644 index 00000000..9dfff5fb --- /dev/null +++ b/CSF.Screenplay.Selenium.Tests_old/Actions/DeselectTests.cs @@ -0,0 +1,69 @@ +using CSF.Screenplay.Selenium.Builders; +using CSF.Screenplay.Selenium.Tests.Pages; +using CSF.Screenplay.NUnit; +using FluentAssertions; +using NUnit.Framework; +using static CSF.Screenplay.StepComposer; +using CSF.Screenplay.Actors; +using CSF.Screenplay.Selenium.Abilities; +using System; + +namespace CSF.Screenplay.Selenium.Tests.Actions +{ + [TestFixture] + [Description("The deselect action")] + public class DeselectTests + { + [Test,Screenplay] + [Description("Deselecting everything leaves nothing selected.")] + public void DeselectAll_leaves_nothing_selected(ICast cast, BrowseTheWeb browseTheWeb) + { + var joe = cast.Get("Joe");joe.IsAbleTo(browseTheWeb); + + Given(joe).WasAbleTo(OpenTheirBrowserOn.ThePage()); + + When(joe).AttemptsTo(Deselect.EverythingFrom(ListsPage.MultiSelectionList)); + + Then(joe).ShouldSee(TheText.Of(ListsPage.MultiSelectionValue)).Should().Be("Nothing!"); + } + + [Test,Screenplay] + [Description("Deselecting by index leaves one item selected.")] + public void DeselectByIndex_leaves_one_item_selected(ICast cast, BrowseTheWeb browseTheWeb) + { + var joe = cast.Get("Joe");joe.IsAbleTo(browseTheWeb); + + Given(joe).WasAbleTo(OpenTheirBrowserOn.ThePage()); + + When(joe).AttemptsTo(Deselect.ItemNumber(2).From(ListsPage.MultiSelectionList)); + + Then(joe).ShouldSee(TheText.Of(ListsPage.MultiSelectionValue)).Should().Be("meat"); + } + + [Test,Screenplay] + [Description("Deselecting by text leaves one item selected.")] + public void DeselectByText_leaves_one_item_selected(ICast cast, BrowseTheWeb browseTheWeb) + { + var joe = cast.Get("Joe");joe.IsAbleTo(browseTheWeb); + + Given(joe).WasAbleTo(OpenTheirBrowserOn.ThePage()); + + When(joe).AttemptsTo(Deselect.Item("Steak").From(ListsPage.MultiSelectionList)); + + Then(joe).ShouldSee(TheText.Of(ListsPage.MultiSelectionValue)).Should().Be("veg"); + } + + [Test,Screenplay] + [Description("Deselecting by value leaves one item selected.")] + public void DeselectByValue_leaves_one_item_selected(ICast cast, BrowseTheWeb browseTheWeb) + { + var joe = cast.Get("Joe");joe.IsAbleTo(browseTheWeb); + + Given(joe).WasAbleTo(OpenTheirBrowserOn.ThePage()); + + When(joe).AttemptsTo(Deselect.ItemValued("meat").From(ListsPage.MultiSelectionList)); + + Then(joe).ShouldSee(TheText.Of(ListsPage.MultiSelectionValue)).Should().Be("veg"); + } + } +} diff --git a/CSF.Screenplay.Selenium.Tests_old/Actions/EnterTests.cs b/CSF.Screenplay.Selenium.Tests_old/Actions/EnterTests.cs new file mode 100644 index 00000000..b4438722 --- /dev/null +++ b/CSF.Screenplay.Selenium.Tests_old/Actions/EnterTests.cs @@ -0,0 +1,43 @@ +using CSF.Screenplay.NUnit; +using CSF.Screenplay.Selenium.Builders; +using CSF.Screenplay.Selenium.Tests.Pages; +using FluentAssertions; +using NUnit.Framework; +using static CSF.Screenplay.StepComposer; +using CSF.Screenplay.Actors; +using CSF.Screenplay.Selenium.Abilities; +using System; + +namespace CSF.Screenplay.Selenium.Tests.Actions +{ + [TestFixture] + [Description("Entering text into elements")] + public class EnterTests + { + [Test,Screenplay] + [Description("Typing text into an input box produces the expected result on the page.")] + public void Type_text_into_an_input_box_produces_expected_result_on_page(ICast cast, BrowseTheWeb browseTheWeb) + { + var joe = cast.Get("Joe");joe.IsAbleTo(browseTheWeb); + + Given(joe).WasAbleTo(OpenTheirBrowserOn.ThePage()); + + When(joe).AttemptsTo(Enter.TheText("The right value").Into(PageTwo.SpecialInputField)); + + Then(joe).ShouldSee(TheText.Of(PageTwo.TheDynamicTextArea)).Should().Be("different value"); + } + + [Test,Screenplay] + [Description("Typing different text into an input box produces the expected result on the page.")] + public void Type_different_text_into_an_input_box_produces_expected_result_on_page(ICast cast, BrowseTheWeb browseTheWeb) + { + var joe = cast.Get("Joe");joe.IsAbleTo(browseTheWeb); + + Given(joe).WasAbleTo(OpenTheirBrowserOn.ThePage()); + + When(joe).AttemptsTo(Enter.TheText("The wrong value").Into(PageTwo.SpecialInputField)); + + Then(joe).ShouldSee(TheText.Of(PageTwo.TheDynamicTextArea)).Should().Be("dynamic value"); + } + } +} diff --git a/CSF.Screenplay.Selenium.Tests_old/Actions/ExecuteScriptTests.cs b/CSF.Screenplay.Selenium.Tests_old/Actions/ExecuteScriptTests.cs new file mode 100644 index 00000000..5eb9630d --- /dev/null +++ b/CSF.Screenplay.Selenium.Tests_old/Actions/ExecuteScriptTests.cs @@ -0,0 +1,59 @@ +using CSF.Screenplay.Selenium.Builders; +using CSF.Screenplay.Selenium.Tests.Pages; +using CSF.Screenplay.NUnit; +using FluentAssertions; +using NUnit.Framework; +using static CSF.Screenplay.StepComposer; +using CSF.Screenplay.Selenium.Tests.Tasks; +using CSF.Screenplay.Actors; +using CSF.Screenplay.Selenium.Abilities; +using System; + +namespace CSF.Screenplay.Selenium.Tests.Actions +{ + [TestFixture] + [Description("The 'Execute Javascript' action")] + public class ExecuteScriptTests + { + [Test,Screenplay] + [Description("Executing some JavaScript affects the page in the expected manner")] + public void ExecuteTheJavaScript_results_in_changed_page_content(ICast cast, BrowseTheWeb browseTheWeb) + { + var joe = cast.Get("Joe");joe.IsAbleTo(browseTheWeb); + + Given(joe).WasAbleTo(OpenTheirBrowserOn.ThePage()); + + When(joe).AttemptsTo(Execute.TheJavaScript("window.myCallableScript();").AndIgnoreTheResult()); + + Then(joe).ShouldSee(TheText.Of(PageTwo.JavaScriptResult)) + .Should() + .Be("myCallableScript called", because: "the Javascript was executed"); + } + + [Test,Screenplay] + [Description("Executing some JavaScript and getting the result returns the expected value")] + public void ExecuteTheJavaScriptAndGetTheResult_returns_correct_value(ICast cast, BrowseTheWeb browseTheWeb) + { + var joe = cast.Get("Joe");joe.IsAbleTo(browseTheWeb); + + Given(joe).WasAbleTo(OpenTheirBrowserOn.ThePage()); + + var result = When(joe).AttemptsTo(Execute.TheJavaScript("return window.addFive();").AndGetTheResult()); + + result.Should().Be(6L, because: "the Javascript was executed"); + } + + [Test,Screenplay] + [Description("Executing some JavaScript with a parameter and getting the result returns the expected value")] + public void ExecuteTheJavaScriptAndGetTheResult_with_params_returns_correct_value(ICast cast, BrowseTheWeb browseTheWeb) + { + var joe = cast.Get("Joe");joe.IsAbleTo(browseTheWeb); + + Given(joe).WasAbleTo(OpenTheirBrowserOn.ThePage()); + + var result = When(joe).AttemptsTo(Execute.TheJavaScript("return window.addFive(arguments[0]);").WithTheParameters(5).AndGetTheResult()); + + result.Should().Be(10L, because: "the Javascript was executed"); + } + } +} diff --git a/CSF.Screenplay.Selenium.Tests_old/Actions/OpenTests.cs b/CSF.Screenplay.Selenium.Tests_old/Actions/OpenTests.cs new file mode 100644 index 00000000..71631d61 --- /dev/null +++ b/CSF.Screenplay.Selenium.Tests_old/Actions/OpenTests.cs @@ -0,0 +1,28 @@ +using System; +using CSF.Screenplay.Actors; +using CSF.Screenplay.NUnit; +using CSF.Screenplay.Selenium.Abilities; +using CSF.Screenplay.Selenium.Builders; +using CSF.Screenplay.Selenium.Tests.Pages; +using FluentAssertions; +using NUnit.Framework; +using static CSF.Screenplay.StepComposer; + +namespace CSF.Screenplay.Selenium.Tests.Actions +{ + [TestFixture] + [Description("Opening the browser on a specified page.")] + public class OpenTests + { + [Test,Screenplay] + [Description("Opening the app home page has the expected window title.")] + public void Open_HomePage_has_expected_window_title(ICast cast, BrowseTheWeb browseTheWeb) + { + var joe = cast.Get("Joe");joe.IsAbleTo(browseTheWeb); + + When(joe).AttemptsTo(OpenTheirBrowserOn.ThePage()); + + Then(joe).ShouldSee(TheWindow.Title()).Should().Be("App home page"); + } + } +} diff --git a/CSF.Screenplay.Selenium.Tests_old/Actions/SelectTests.cs b/CSF.Screenplay.Selenium.Tests_old/Actions/SelectTests.cs new file mode 100644 index 00000000..c8492b12 --- /dev/null +++ b/CSF.Screenplay.Selenium.Tests_old/Actions/SelectTests.cs @@ -0,0 +1,56 @@ +using CSF.Screenplay.Selenium.Builders; +using CSF.Screenplay.Selenium.Tests.Pages; +using CSF.Screenplay.NUnit; +using FluentAssertions; +using NUnit.Framework; +using static CSF.Screenplay.StepComposer; +using CSF.Screenplay.Actors; +using CSF.Screenplay.Selenium.Abilities; +using System; + +namespace CSF.Screenplay.Selenium.Tests.Actions +{ + [TestFixture] + [Description("The select action")] + public class SelectTests + { + [Test,Screenplay] + [Description("Selecting by text generates the expected result on the page.")] + public void SelectByText_generates_expected_result_on_page(ICast cast, BrowseTheWeb browseTheWeb) + { + var joe = cast.Get("Joe");joe.IsAbleTo(browseTheWeb); + + Given(joe).WasAbleTo(OpenTheirBrowserOn.ThePage()); + + When(joe).AttemptsTo(Select.Item("Two").From(ListsPage.SingleSelectionList)); + + Then(joe).ShouldSee(TheText.From(ListsPage.SingleSelectionValue).As()).Should().Be(2); + } + + [Test,Screenplay] + [Description("Selecting by index generates the expected result on the page.")] + public void SelectByIndex_generates_expected_result_on_page(ICast cast, BrowseTheWeb browseTheWeb) + { + var joe = cast.Get("Joe");joe.IsAbleTo(browseTheWeb); + + Given(joe).WasAbleTo(OpenTheirBrowserOn.ThePage()); + + When(joe).AttemptsTo(Select.ItemNumber(3).From(ListsPage.SingleSelectionList)); + + Then(joe).ShouldSee(TheText.From(ListsPage.SingleSelectionValue).As()).Should().Be(3); + } + + [Test,Screenplay] + [Description("Selecting by value generates the expected result on the page.")] + public void SelectByValue_generates_expected_result_on_page(ICast cast, BrowseTheWeb browseTheWeb) + { + var joe = cast.Get("Joe");joe.IsAbleTo(browseTheWeb); + + Given(joe).WasAbleTo(OpenTheirBrowserOn.ThePage()); + + When(joe).AttemptsTo(Select.ItemValued("1").From(ListsPage.SingleSelectionList)); + + Then(joe).ShouldSee(TheText.From(ListsPage.SingleSelectionValue).As()).Should().Be(1); + } + } +} diff --git a/CSF.Screenplay.Selenium.Tests_old/Actions/TakeAScreenshotTests.cs b/CSF.Screenplay.Selenium.Tests_old/Actions/TakeAScreenshotTests.cs new file mode 100644 index 00000000..4e694889 --- /dev/null +++ b/CSF.Screenplay.Selenium.Tests_old/Actions/TakeAScreenshotTests.cs @@ -0,0 +1,70 @@ +// +// TakeAScreenshotTests.cs +// +// Author: +// Craig Fowler +// +// Copyright (c) 2018 Craig Fowler +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +using System; +using CSF.Screenplay.NUnit; +using CSF.Screenplay.Selenium.Abilities; +using CSF.Screenplay.Selenium.Tests.Personas; +using static CSF.Screenplay.StepComposer; +using NUnit.Framework; +using CSF.Screenplay.Selenium.Builders; +using CSF.Screenplay.Selenium.Tests.Pages; + +namespace CSF.Screenplay.Selenium.Tests.Actions +{ + [TestFixture] + public class TakeAScreenshotTests + { + [Test,Screenplay] + public void Taking_and_saving_a_screenshot_should_not_crash(ICast cast, SaveScreenshots saveScreenshots) + { + var joe = cast.Get(); + joe.IsAbleTo(saveScreenshots); + + Given(joe).WasAbleTo(OpenTheirBrowserOn.ThePage()); + + Assert.That(() => { + When(joe).AttemptsTo(TakeAScreenshot.AndSaveItWithTheName("test-screenshot")); + }, Throws.Nothing); + } + + [Test,Screenplay] + public void Taking_and_saving_a_screenshot_two_screenshots_should_not_crash(ICast cast, + SaveScreenshots saveScreenshots) + { + var joe = cast.Get(); + joe.IsAbleTo(saveScreenshots); + + Given(joe).WasAbleTo(OpenTheirBrowserOn.ThePage()); + + Assert.That(() => { + + When(joe).AttemptsTo(TakeAScreenshot.AndSaveItWithTheName("first-screenshot")); + When(joe).AttemptsTo(TakeAScreenshot.AndSaveItWithTheName("second-screenshot")); + + }, Throws.Nothing); + } + } +} diff --git a/CSF.Screenplay.Selenium.Tests_old/Actions/TargetNotFoundTests.cs b/CSF.Screenplay.Selenium.Tests_old/Actions/TargetNotFoundTests.cs new file mode 100644 index 00000000..c2b62286 --- /dev/null +++ b/CSF.Screenplay.Selenium.Tests_old/Actions/TargetNotFoundTests.cs @@ -0,0 +1,80 @@ +using System; +using CSF.Screenplay.Actors; +using CSF.Screenplay.NUnit; +using CSF.Screenplay.Selenium.Abilities; +using CSF.Screenplay.Selenium.Builders; +using CSF.Screenplay.Selenium.Models; +using CSF.Screenplay.Selenium.Tests.Pages; +using NUnit.Framework; +using static CSF.Screenplay.StepComposer; + +namespace CSF.Screenplay.Selenium.Tests.Actions +{ + [TestFixture] + [Description("Behaviours when a target is not found for a desired action")] + public class TargetNotFoundTests + { + [Test,Screenplay] + [Description("Attempting to click on a link which does not exist raises an appropriate 'target not found' exception.")] + public void Click_on_non_existent_element_raises_TargetNotFoundException(ICast cast, BrowseTheWeb browseTheWeb) + { + var joe = cast.Get("Joe");joe.IsAbleTo(browseTheWeb); + + Given(joe).WasAbleTo(OpenTheirBrowserOn.ThePage()); + + Assert.That(() => When(joe).AttemptsTo(Click.On(ListsPage.ListOfItems)), + Throws.TypeOf()); + } + + [Test,Screenplay] + [Description("When a 'target not found' exception is raised, it should have a name which matches the missing target.")] + public void TargetNotFoundException_raised_has_correct_target_name(ICast cast, BrowseTheWeb browseTheWeb) + { + var joe = cast.Get("Joe");joe.IsAbleTo(browseTheWeb); + + Given(joe).WasAbleTo(OpenTheirBrowserOn.ThePage()); + + IHasTargetName target = null; + try + { + When(joe).AttemptsTo(Click.On(ListsPage.ListOfItems)); + Assert.Fail("The action should raise an exception."); + } + catch(TargetNotFoundException ex) + { + target = ex.Target; + } + catch(Exception ex) + { + Assert.Fail($"Wrong exception type caught. Expected {nameof(TargetNotFoundException)}, got:\n{ex.ToString()}"); + } + + Assert.That(target, Is.Not.Null, "Target should not be null."); + Assert.That(target.GetName(), Is.EqualTo(ListsPage.ListOfItems.GetName()), "Target has the correct name"); + } + + [Test,Screenplay] + [Description("A 'target not found' exception should include the target name in its report when the target is provided.")] + public void TargetNotFoundException_includes_target_name_in_report(ICast cast, BrowseTheWeb browseTheWeb) + { + var joe = cast.Get("Joe");joe.IsAbleTo(browseTheWeb); + var ex = new TargetNotFoundException() { Target = ListsPage.ListOfItems }; + + var result = ex.GetReport(joe); + + Assert.That(result, Is.EqualTo("Joe cannot see the list of items on the screen.")); + } + + [Test,Screenplay] + [Description("A 'target not found' exception should include the target name in its report when the target is provided.")] + public void TargetNotFoundException_can_create_a_report_without_target(ICast cast, BrowseTheWeb browseTheWeb) + { + var joe = cast.Get("Joe");joe.IsAbleTo(browseTheWeb); + var ex = new TargetNotFoundException(); + + var result = ex.GetReport(joe); + + Assert.That(result, Is.EqualTo("Joe cannot see the required element on the screen.")); + } + } +} diff --git a/CSF.Screenplay.Selenium.Tests_old/App.AppVeyor.config b/CSF.Screenplay.Selenium.Tests_old/App.AppVeyor.config new file mode 100644 index 00000000..8aa6f9a2 --- /dev/null +++ b/CSF.Screenplay.Selenium.Tests_old/App.AppVeyor.config @@ -0,0 +1,19 @@ + + + +
+ + + + + + + + + + + + + + + diff --git a/CSF.Screenplay.Selenium.Tests_old/App.Travis.config b/CSF.Screenplay.Selenium.Tests_old/App.Travis.config new file mode 100644 index 00000000..ad625e67 --- /dev/null +++ b/CSF.Screenplay.Selenium.Tests_old/App.Travis.config @@ -0,0 +1,20 @@ + + + +
+ + + + + + + + + + + + + + + diff --git a/CSF.Screenplay.Selenium.Tests_old/AssemblyWideTimeouts.cs b/CSF.Screenplay.Selenium.Tests_old/AssemblyWideTimeouts.cs new file mode 100644 index 00000000..e9ad38a3 --- /dev/null +++ b/CSF.Screenplay.Selenium.Tests_old/AssemblyWideTimeouts.cs @@ -0,0 +1,31 @@ +// +// AssemblyWideTimeouts.cs +// +// Author: +// Craig Fowler +// +// Copyright (c) 2018 Craig Fowler +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +// 2-minute timeout on any single test in this assembly. +// Tests should complete much more quickly, but if they +// take a very long time (over 2 min) then I can assume +// that they have failed. +[assembly: NUnit.Framework.Timeout(120000)] \ No newline at end of file diff --git a/CSF.Screenplay.Selenium.Tests_old/BrowserName.cs b/CSF.Screenplay.Selenium.Tests_old/BrowserName.cs new file mode 100644 index 00000000..cdf3098f --- /dev/null +++ b/CSF.Screenplay.Selenium.Tests_old/BrowserName.cs @@ -0,0 +1,13 @@ +using System; +namespace CSF.Screenplay.Selenium.Tests +{ + public static class BrowserName + { + public static readonly string + Chrome = "Chrome", + InternetExplorer = "Internet Explorer", + Firefox = "Firefox", + Edge = "MicrosoftEdge", + Safari = "Safari"; + } +} diff --git a/CSF.Screenplay.Selenium.Tests_old/CSF.Screenplay.Selenium.Tests.csproj b/CSF.Screenplay.Selenium.Tests_old/CSF.Screenplay.Selenium.Tests.csproj new file mode 100644 index 00000000..3decd1e7 --- /dev/null +++ b/CSF.Screenplay.Selenium.Tests_old/CSF.Screenplay.Selenium.Tests.csproj @@ -0,0 +1,168 @@ + + + + Debug + AnyCPU + {09AA41BD-AD31-485F-8912-D687CBA5BD88} + Library + CSF.Screenplay.Selenium.Tests + CSF.Screenplay.Selenium.Tests + v4.5 + 1.0.0 + + + true + full + false + bin\Debug + DEBUG; + prompt + 4 + + + true + bin\Release + prompt + 4 + + + + + ..\packages\NUnit.3.7.1\lib\net45\nunit.framework.dll + nunit + + + ..\packages\Selenium.WebDriver.3.4.0\lib\net40\WebDriver.dll + + + + ..\packages\FluentAssertions.4.19.3\lib\net45\FluentAssertions.Core.dll + + + ..\packages\FluentAssertions.4.19.3\lib\net45\FluentAssertions.dll + + + + + ..\packages\Castle.Core.4.1.1\lib\net45\Castle.Core.dll + + + ..\packages\Moq.4.7.99\lib\net45\Moq.dll + + + ..\packages\Newtonsoft.Json.10.0.3\lib\net45\Newtonsoft.Json.dll + + + ..\packages\CSF.Configuration.1.1.2\lib\net45\CSF.Configuration.dll + + + ..\packages\CSF.WebDriverExtras.1.0.3\lib\net45\CSF.WebDriverExtras.dll + + + ..\packages\CSF.FlexDi.1.0.2\lib\net45\CSF.FlexDi.dll + + + ..\packages\CSF.Utils.6.1.1\lib\net45\CSF.Utils.dll + + + ..\packages\CSF.Reflection.1.0.3\lib\net45\CSF.Reflection.dll + + + ..\packages\CSF.Screenplay.0.14.0-beta\lib\net45\CSF.Screenplay.dll + + + ..\packages\CSF.Screenplay.NUnit.0.14.0-beta\lib\net45\CSF.Screenplay.NUnit.dll + + + ..\packages\CSF.Screenplay.Reporting.0.14.0-beta\lib\net45\CSF.Screenplay.Reporting.dll + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {13E2170A-AF71-40FF-8D8C-44FE3D0BDF3B} + CSF.Screenplay.Selenium + + + {0665F99E-DB05-4208-BCF1-137EF914CBF5} + CSF.Screenplay.Selenium.BrowserFlags + + + {CC20F88D-C4B9-4FDB-B5B1-7349547430FA} + CSF.Screenplay.Selenium.JavaScriptWorkarounds + + + + + + + + \ No newline at end of file diff --git a/CSF.Screenplay.Selenium.Tests_old/Ignore.cs b/CSF.Screenplay.Selenium.Tests_old/Ignore.cs new file mode 100644 index 00000000..6c4a9939 --- /dev/null +++ b/CSF.Screenplay.Selenium.Tests_old/Ignore.cs @@ -0,0 +1,77 @@ +using System; +using System.Linq; +using CSF.Screenplay.Actors; +using CSF.Screenplay.Selenium.Abilities; +using CSF.WebDriverExtras; +using NUnit.Framework; +using OpenQA.Selenium; + +namespace CSF.Screenplay.Selenium.Tests +{ + public static class Ignore + { + public static void UnlessBrowserHasAnyOfTheFlags(BrowseTheWeb browser, + params string[] flags) + { + UnlessBrowserHasAnyOfTheFlags(browser.FlagsDriver, flags); + } + + public static void UnlessBrowserHasAnyOfTheFlags(IHasFlags driver, + params string[] flags) + { + if(driver == null) + throw new ArgumentNullException(nameof(driver)); + if(flags == null || flags.Length == 0) + throw new ArgumentException("You must list at least one flag.", nameof(flags)); + + var hasFlag = flags.Any(x => driver.HasFlag(x)); + if(!hasFlag) + Assert.Ignore("The web browser does not have any of the required flags: {0}", String.Join(", ", flags)); + } + + public static void IfBrowserHasAnyOfTheFlags(BrowseTheWeb browser, + params string[] flags) + { + IfBrowserHasAnyOfTheFlags(browser.FlagsDriver, flags); + } + + public static void IfBrowserHasAnyOfTheFlags(IHasFlags driver, + params string[] flags) + { + if(driver == null) + throw new ArgumentNullException(nameof(driver)); + if(flags == null || flags.Length == 0) + throw new ArgumentException("You must list at least one flag.", nameof(flags)); + + var hasFlag = flags.Any(x => driver.HasFlag(x)); + if(hasFlag) + Assert.Ignore("The web browser has one of a number of disallowed flags: {0}", String.Join(", ", flags)); + } + + public static void ShouldIgnoreThisTestIfTheirBrowserHasAnyOfTheFlags(this IActor actor, + params string[] flags) + { + if(actor == null) + throw new ArgumentNullException(nameof(actor)); + + var browseTheWeb = actor.GetAbility(); + if(browseTheWeb == null) + throw new ArgumentException($"The actor must have the {nameof(BrowseTheWeb)} ability.", nameof(actor)); + + IfBrowserHasAnyOfTheFlags(browseTheWeb, flags); + } + + public static void ShouldIgnoreThisTestUnlessTheirBrowserHasAnyOfTheFlags(this IActor actor, + params string[] flags) + { + if(actor == null) + throw new ArgumentNullException(nameof(actor)); + + var browseTheWeb = actor.GetAbility(); + if(browseTheWeb == null) + throw new ArgumentException($"The actor must have the {nameof(BrowseTheWeb)} ability.", nameof(actor)); + + UnlessBrowserHasAnyOfTheFlags(browseTheWeb, flags); + } + } +} diff --git a/CSF.Screenplay.Selenium.Tests_old/NUnitIntegrationTests.cs b/CSF.Screenplay.Selenium.Tests_old/NUnitIntegrationTests.cs new file mode 100644 index 00000000..68bc0b14 --- /dev/null +++ b/CSF.Screenplay.Selenium.Tests_old/NUnitIntegrationTests.cs @@ -0,0 +1,20 @@ +using System; +using NUnit.Framework; +using CSF.Screenplay.NUnit; +using CSF.Screenplay.Scenarios; + +namespace CSF.Screenplay.Selenium.Tests +{ + [TestFixture] + [Description("The NUnit/Screenplay integration")] + public class NUnitIntegrationTests + { + [Test,Screenplay] + [Description("An NUnit test decorated with `Screenplay' receives the current scenario as an injected parameter")] + public void ScreenplayScenario_is_injected_from_parameter(IScenario scenario) + { + // Assert + Assert.That(scenario, Is.Not.Null); + } + } +} diff --git a/CSF.Screenplay.Selenium.Tests_old/Pages/DateInputPage.cs b/CSF.Screenplay.Selenium.Tests_old/Pages/DateInputPage.cs new file mode 100644 index 00000000..cfd399f6 --- /dev/null +++ b/CSF.Screenplay.Selenium.Tests_old/Pages/DateInputPage.cs @@ -0,0 +1,41 @@ +// +// DateInputPage.cs +// +// Author: +// Craig Fowler +// +// Copyright (c) 2018 Craig Fowler +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +using System; +using CSF.Screenplay.Selenium.Models; + +namespace CSF.Screenplay.Selenium.Tests.Pages +{ + public class DateInputPage : Page + { + public override string GetName() => "the date-input page"; + + public override IUriProvider GetUriProvider() => new AppUri("DateInput"); + + public static ILocatorBasedTarget DateInput = new ElementId("DateInput", "the date input field"); + + public static ILocatorBasedTarget DateOutput = new ElementId("DateOutput", "the date display"); + } +} diff --git a/CSF.Screenplay.Selenium.Tests_old/Pages/ElementsWithAttributesPage.cs b/CSF.Screenplay.Selenium.Tests_old/Pages/ElementsWithAttributesPage.cs new file mode 100644 index 00000000..db45f496 --- /dev/null +++ b/CSF.Screenplay.Selenium.Tests_old/Pages/ElementsWithAttributesPage.cs @@ -0,0 +1,53 @@ +// +// ElementsWithAttributesPage.cs +// +// Author: +// Craig Fowler +// +// Copyright (c) 2018 Craig Fowler +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +using System; +using CSF.Screenplay.Selenium.Models; + +namespace CSF.Screenplay.Selenium.Tests.Pages +{ + public class ElementsWithAttributesPage : Page + { + public override string GetName() => "the elements-with-attributes page"; + + public override IUriProvider GetUriProvider() => new AppUri("ElementsWithAttributes"); + + public static ILocatorBasedTarget DivWithClass = new ElementId("DivWithClass", "a div with a class attribute"); + + public static ILocatorBasedTarget ClassOutput = new ElementId("DisplayDivClass", "the displayed class"); + + public static ILocatorBasedTarget DateInput = new ElementId("DateInput", "the date input field"); + + public static ILocatorBasedTarget DateOutput = new ElementId("DisplayDateReadonly", "the displayed date-readonly attribute"); + + public static ILocatorBasedTarget TextInput = new ElementId("TextInput", "the text input field"); + + public static ILocatorBasedTarget TextOutput = new ElementId("DisplayTextReadonly", "the displayed text-readonly attribute"); + + public static ILocatorBasedTarget TextPlaceholder = new ElementId("DisplayTextPlaceholder", "the displayed text-placeholder attribute"); + + public static ILocatorBasedTarget GetOutputButton = new ElementId("GetOutput", "the button which refreshes the outputs"); + } +} diff --git a/CSF.Screenplay.Selenium.Tests_old/Pages/HomePage.cs b/CSF.Screenplay.Selenium.Tests_old/Pages/HomePage.cs new file mode 100644 index 00000000..659b338b --- /dev/null +++ b/CSF.Screenplay.Selenium.Tests_old/Pages/HomePage.cs @@ -0,0 +1,18 @@ +using System; +using CSF.Screenplay.Selenium.Models; + +namespace CSF.Screenplay.Selenium.Tests.Pages +{ + public class HomePage : Page + { + public override string GetName() => "the app home page"; + + public override IUriProvider GetUriProvider() => new AppUri("Home"); + + public static ITarget SecondPageLink => new ClassName("second_page_link", "the hyperlink to page two"); + + public static ITarget SlowLoadingLink => new ElementId("load_in_2_seconds", "the link to reload with a 2-second delay"); + + public static ITarget LoadDelay => new ElementId("load_delay", "the readout of the page-load delay"); + } +} diff --git a/CSF.Screenplay.Selenium.Tests_old/Pages/ListsPage.cs b/CSF.Screenplay.Selenium.Tests_old/Pages/ListsPage.cs new file mode 100644 index 00000000..f1ba4403 --- /dev/null +++ b/CSF.Screenplay.Selenium.Tests_old/Pages/ListsPage.cs @@ -0,0 +1,49 @@ +// +// ListsPage.cs +// +// Author: +// Craig Fowler +// +// Copyright (c) 2018 Craig Fowler +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +using System; +using CSF.Screenplay.Selenium.Models; + +namespace CSF.Screenplay.Selenium.Tests.Pages +{ + public class ListsPage : Page + { + public override string GetName() => "the lists testing page"; + + public override IUriProvider GetUriProvider() => new AppUri("Lists"); + + public static ITarget SingleSelectionList => new CssSelector("#single_selection", "the single selection list"); + + public static ITarget SingleSelectionValue => new CssSelector("#single_selected_value", "the single-selection value"); + + public static ITarget MultiSelectionList => new CssSelector("#multiple_selection", "the multi selection list"); + + public static ITarget MultiSelectionValue => new CssSelector("#multiple_selected_value", "the multi-selection value"); + + public static ITarget ListOfItems => new CssSelector("#list_of_items", "the list of items"); + + public static ILocatorBasedTarget ItemsInTheList => new CssSelector("#list_of_items li", "items in the list"); + } +} diff --git a/CSF.Screenplay.Selenium.Tests_old/Pages/PageThree.cs b/CSF.Screenplay.Selenium.Tests_old/Pages/PageThree.cs new file mode 100644 index 00000000..9d71c802 --- /dev/null +++ b/CSF.Screenplay.Selenium.Tests_old/Pages/PageThree.cs @@ -0,0 +1,16 @@ +using System; +using CSF.Screenplay.Selenium.Models; + +namespace CSF.Screenplay.Selenium.Tests.Pages +{ + public class PageThree : Page + { + public override string GetName() => "page three"; + + public override IUriProvider GetUriProvider() => new AppUri("PageThree"); + + public static ITarget DelayedButtonOne => new CssSelector("#delay_click_one", "the first delay button"); + + public static ITarget DelayedLinkOne => new CssSelector("#delay_appear_target_one .appeared", "the first delay-appearance link"); + } +} diff --git a/CSF.Screenplay.Selenium.Tests_old/Pages/PageTwo.cs b/CSF.Screenplay.Selenium.Tests_old/Pages/PageTwo.cs new file mode 100644 index 00000000..ee3ed9b2 --- /dev/null +++ b/CSF.Screenplay.Selenium.Tests_old/Pages/PageTwo.cs @@ -0,0 +1,20 @@ +using System; +using CSF.Screenplay.Selenium.Models; + +namespace CSF.Screenplay.Selenium.Tests.Pages +{ + public class PageTwo : Page + { + public override string GetName() => "the second page"; + + public override IUriProvider GetUriProvider() => new AppUri("PageTwo"); + + public static ILocatorBasedTarget SpecialInputField => new CssSelector(".special_text input", "the special input field"); + + public static ITarget SecondTextbox => new CssSelector(".second_textbox input", "the second text box"); + + public static ITarget TheDynamicTextArea => new ElementId("dynamic_value", "the dynamic value"); + + public static ITarget JavaScriptResult => new ElementId("ScriptOutput", "the Javascript output"); + } +} diff --git a/CSF.Screenplay.Selenium.Tests_old/Pages/ReadElementsPage.cs b/CSF.Screenplay.Selenium.Tests_old/Pages/ReadElementsPage.cs new file mode 100644 index 00000000..269d69c3 --- /dev/null +++ b/CSF.Screenplay.Selenium.Tests_old/Pages/ReadElementsPage.cs @@ -0,0 +1,41 @@ +// +// InputPage.cs +// +// Author: +// Craig Fowler +// +// Copyright (c) 2018 Craig Fowler +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +using System; +using CSF.Screenplay.Selenium.Models; + +namespace CSF.Screenplay.Selenium.Tests.Pages +{ + public class ReadElementsPage : Page + { + public override string GetName() => "the page for testing reading HTML elements"; + + public override IUriProvider GetUriProvider() => new AppUri("ReadElements"); + + public static ITarget ImportantString => new ElementId("important_string", "the string element"); + + public static ITarget ImportantNumber => new ElementId("important_number", "the numeric element"); + } +} diff --git a/CSF.Screenplay.Selenium.Tests_old/Pages/ScriptTestingHarness.cs b/CSF.Screenplay.Selenium.Tests_old/Pages/ScriptTestingHarness.cs new file mode 100644 index 00000000..7881c6af --- /dev/null +++ b/CSF.Screenplay.Selenium.Tests_old/Pages/ScriptTestingHarness.cs @@ -0,0 +1,61 @@ +// +// ScriptTestingHarness.cs +// +// Author: +// Craig Fowler +// +// Copyright (c) 2018 Craig Fowler +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +using System; +using CSF.Screenplay.Selenium.Models; +using CSF.Screenplay.Selenium.StoredScripts; + +namespace CSF.Screenplay.Selenium.Tests.Pages +{ + public class ScriptTestingHarness : Page + { + readonly IProvidesScript scriptProvider; + + public override string GetName() => $"the Jasmine test harness for {scriptProvider.Name}"; + + public override IUriProvider GetUriProvider() + => new AppUri($"StoredScriptTest/Index/{scriptProvider.GetType().Name}"); + + public static ILocatorBasedTarget TheResultsBar + => new CssSelector(".jasmine_html-reporter .jasmine-bar", "the Jasmine results bar"); + + public ScriptTestingHarness(IProvidesScript scriptProvider) + { + if(scriptProvider == null) + throw new ArgumentNullException(nameof(scriptProvider)); + + this.scriptProvider = scriptProvider; + } + + public static ScriptTestingHarness For() where TScript : IProvidesScript, new() + => new ScriptTestingHarness(new TScript()); + + public static ScriptTestingHarness For(Type scriptType) + => new ScriptTestingHarness((IProvidesScript) Activator.CreateInstance(scriptType)); + + public static ScriptTestingHarness For(IProvidesScript script) + => new ScriptTestingHarness(script); + } +} diff --git a/CSF.Screenplay.Selenium.Tests_old/Personas/Joe.cs b/CSF.Screenplay.Selenium.Tests_old/Personas/Joe.cs new file mode 100644 index 00000000..bfa62bba --- /dev/null +++ b/CSF.Screenplay.Selenium.Tests_old/Personas/Joe.cs @@ -0,0 +1,41 @@ +// +// Joe.cs +// +// Author: +// Craig Fowler +// +// Copyright (c) 2018 Craig Fowler +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +using System; +using CSF.Screenplay.Selenium.Abilities; + +namespace CSF.Screenplay.Selenium.Tests.Personas +{ + public class Joe : Persona + { + public override string Name => "Joe"; + + public override void GrantAbilities(Actors.ICanReceiveAbilities actor, FlexDi.IResolvesServices resolver) + { + var browseTheWeb = resolver.Resolve(); + actor.IsAbleTo(browseTheWeb); + } + } +} diff --git a/CSF.Screenplay.Selenium.Tests_old/Questions/FindElementsTests.cs b/CSF.Screenplay.Selenium.Tests_old/Questions/FindElementsTests.cs new file mode 100644 index 00000000..c39c8a5d --- /dev/null +++ b/CSF.Screenplay.Selenium.Tests_old/Questions/FindElementsTests.cs @@ -0,0 +1,43 @@ +using CSF.Screenplay.Selenium.Builders; +using CSF.Screenplay.Selenium.Tests.Pages; +using FluentAssertions; +using CSF.Screenplay.NUnit; +using NUnit.Framework; +using static CSF.Screenplay.StepComposer; +using CSF.Screenplay.Actors; +using CSF.Screenplay.Selenium.Abilities; +using System; + +namespace CSF.Screenplay.Selenium.Tests.Questions +{ + [TestFixture] + [Description("Finding HTML elements")] + public class FindElementsTests + { + [Test,Screenplay] + [Description("Finding child elements of the item list detects the correct count of children.")] + public void FindElements_In_gets_expected_count_of_elements(ICast cast, BrowseTheWeb browseTheWeb) + { + var joe = cast.Get("Joe");joe.IsAbleTo(browseTheWeb); + + Given(joe).WasAbleTo(OpenTheirBrowserOn.ThePage()); + + Then(joe) + .ShouldSee(Elements.In(ListsPage.ListOfItems).Get()) + .Elements.Count.Should().Be(5); + } + + [Test,Screenplay] + [Description("Finding elements on the page detects the correct count of children.")] + public void FindElements_OnThePage_gets_expected_count_of_elements(ICast cast, BrowseTheWeb browseTheWeb) + { + var joe = cast.Get("Joe");joe.IsAbleTo(browseTheWeb); + + Given(joe).WasAbleTo(OpenTheirBrowserOn.ThePage()); + + Then(joe) + .ShouldSee(Elements.InThePageBody().ThatAre(ListsPage.ItemsInTheList).Called("the listed items")) + .Elements.Count.Should().Be(5); + } + } +} diff --git a/CSF.Screenplay.Selenium.Tests_old/Questions/GetAttributeTests.cs b/CSF.Screenplay.Selenium.Tests_old/Questions/GetAttributeTests.cs new file mode 100644 index 00000000..d0a90eaf --- /dev/null +++ b/CSF.Screenplay.Selenium.Tests_old/Questions/GetAttributeTests.cs @@ -0,0 +1,28 @@ +using CSF.Screenplay.Selenium.Builders; +using CSF.Screenplay.Selenium.Tests.Pages; +using FluentAssertions; +using CSF.Screenplay.NUnit; +using NUnit.Framework; +using static CSF.Screenplay.StepComposer; +using CSF.Screenplay.Actors; +using CSF.Screenplay.Selenium.Abilities; +using System; + +namespace CSF.Screenplay.Selenium.Tests.Questions +{ + [TestFixture] + [Description("Reading element attributes")] + public class GetAttributeTests + { + [Test,Screenplay] + [Description("Reading the value of a 'title' attribute detects the expected value.")] + public void GetAttribute_returns_expected_value(ICast cast, BrowseTheWeb browseTheWeb) + { + var joe = cast.Get("Joe");joe.IsAbleTo(browseTheWeb); + + Given(joe).WasAbleTo(OpenTheirBrowserOn.ThePage()); + + Then(joe).ShouldSee(TheAttribute.Named("title").From(PageTwo.TheDynamicTextArea)).Should().Be("This is a dynamic value"); + } + } +} diff --git a/CSF.Screenplay.Selenium.Tests_old/Questions/GetCssValueTests.cs b/CSF.Screenplay.Selenium.Tests_old/Questions/GetCssValueTests.cs new file mode 100644 index 00000000..5110b2f0 --- /dev/null +++ b/CSF.Screenplay.Selenium.Tests_old/Questions/GetCssValueTests.cs @@ -0,0 +1,29 @@ +using CSF.Screenplay.Selenium.Builders; +using CSF.Screenplay.Selenium.Tests.Pages; +using FluentAssertions; +using CSF.Screenplay.NUnit; +using NUnit.Framework; +using static CSF.Screenplay.StepComposer; +using CSF.Screenplay.Actors; +using CSF.Screenplay.Selenium.Abilities; +using System; + +namespace CSF.Screenplay.Selenium.Tests.Questions +{ + [TestFixture] + [Description("Reading the value of CSS properties")] + public class GetCssValueTests + { + [Test,Screenplay] + [Description("Reading the value of the 'color' property detects the expected value.")] + public void GetCssValue_for_red_string_gets_correct_colour(ICast cast, BrowseTheWeb browseTheWeb) + { + var joe = cast.Get("Joe");joe.IsAbleTo(browseTheWeb); + + Given(joe).WasAbleTo(OpenTheirBrowserOn.ThePage()); + + Then(joe).ShouldSee(TheCss.Property("color").From(ReadElementsPage.ImportantString)) + .Should().MatchRegex(@"^rgba?\(255, *0, *0(, *1)?\)$"); + } + } +} diff --git a/CSF.Screenplay.Selenium.Tests_old/Questions/GetOptionsTests.cs b/CSF.Screenplay.Selenium.Tests_old/Questions/GetOptionsTests.cs new file mode 100644 index 00000000..660ebbea --- /dev/null +++ b/CSF.Screenplay.Selenium.Tests_old/Questions/GetOptionsTests.cs @@ -0,0 +1,50 @@ +using CSF.Screenplay.Selenium.Builders; +using CSF.Screenplay.Selenium.Tests.Pages; +using FluentAssertions; +using CSF.Screenplay.NUnit; +using NUnit.Framework; +using static CSF.Screenplay.StepComposer; +using CSF.Screenplay.Actors; +using CSF.Screenplay.Selenium.Abilities; +using System; + +namespace CSF.Screenplay.Selenium.Tests.Questions +{ + [TestFixture] + [Description("Reading options from HTML ",a.querySelectorAll("[msallowcapture^='']").length&&q.push("[*^$]="+K+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||q.push("\\["+K+"*(?:value|"+J+")"),a.querySelectorAll("[id~="+u+"-]").length||q.push("~="),a.querySelectorAll(":checked").length||q.push(":checked"),a.querySelectorAll("a#"+u+"+*").length||q.push(".#.+[+~]")}),ja(function(a){a.innerHTML="";var b=n.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&q.push("name"+K+"*[*^$|!~]?="),2!==a.querySelectorAll(":enabled").length&&q.push(":enabled",":disabled"),o.appendChild(a).disabled=!0,2!==a.querySelectorAll(":disabled").length&&q.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),q.push(",.*:")})),(c.matchesSelector=Y.test(s=o.matches||o.webkitMatchesSelector||o.mozMatchesSelector||o.oMatchesSelector||o.msMatchesSelector))&&ja(function(a){c.disconnectedMatch=s.call(a,"*"),s.call(a,"[s!='']:x"),r.push("!=",N)}),q=q.length&&new RegExp(q.join("|")),r=r.length&&new RegExp(r.join("|")),b=Y.test(o.compareDocumentPosition),t=b||Y.test(o.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},B=b?function(a,b){if(a===b)return l=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===n||a.ownerDocument===v&&t(v,a)?-1:b===n||b.ownerDocument===v&&t(v,b)?1:k?I(k,a)-I(k,b):0:4&d?-1:1)}:function(a,b){if(a===b)return l=!0,0;var c,d=0,e=a.parentNode,f=b.parentNode,g=[a],h=[b];if(!e||!f)return a===n?-1:b===n?1:e?-1:f?1:k?I(k,a)-I(k,b):0;if(e===f)return la(a,b);c=a;while(c=c.parentNode)g.unshift(c);c=b;while(c=c.parentNode)h.unshift(c);while(g[d]===h[d])d++;return d?la(g[d],h[d]):g[d]===v?-1:h[d]===v?1:0},n):n},ga.matches=function(a,b){return ga(a,null,null,b)},ga.matchesSelector=function(a,b){if((a.ownerDocument||a)!==n&&m(a),b=b.replace(S,"='$1']"),c.matchesSelector&&p&&!A[b+" "]&&(!r||!r.test(b))&&(!q||!q.test(b)))try{var d=s.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return ga(b,n,null,[a]).length>0},ga.contains=function(a,b){return(a.ownerDocument||a)!==n&&m(a),t(a,b)},ga.attr=function(a,b){(a.ownerDocument||a)!==n&&m(a);var e=d.attrHandle[b.toLowerCase()],f=e&&C.call(d.attrHandle,b.toLowerCase())?e(a,b,!p):void 0;return void 0!==f?f:c.attributes||!p?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},ga.escape=function(a){return(a+"").replace(ba,ca)},ga.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},ga.uniqueSort=function(a){var b,d=[],e=0,f=0;if(l=!c.detectDuplicates,k=!c.sortStable&&a.slice(0),a.sort(B),l){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return k=null,a},e=ga.getText=function(a){var b,c="",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[d++])c+=e(b);return c},d=ga.selectors={cacheLength:50,createPseudo:ia,match:V,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(_,aa),a[3]=(a[3]||a[4]||a[5]||"").replace(_,aa),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),"nth"===a[1].slice(0,3)?(a[3]||ga.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*("even"===a[3]||"odd"===a[3])),a[5]=+(a[7]+a[8]||"odd"===a[3])):a[3]&&ga.error(a[0]),a},PSEUDO:function(a){var b,c=!a[6]&&a[2];return V.CHILD.test(a[0])?null:(a[3]?a[2]=a[4]||a[5]||"":c&&T.test(c)&&(b=g(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(_,aa).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=y[a+" "];return b||(b=new RegExp("(^|"+K+")"+a+"("+K+"|$)"))&&y(a,function(a){return b.test("string"==typeof a.className&&a.className||"undefined"!=typeof a.getAttribute&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=ga.attr(d,a);return null==e?"!="===b:!b||(e+="","="===b?e===c:"!="===b?e!==c:"^="===b?c&&0===e.indexOf(c):"*="===b?c&&e.indexOf(c)>-1:"$="===b?c&&e.slice(-c.length)===c:"~="===b?(" "+e.replace(O," ")+" ").indexOf(c)>-1:"|="===b&&(e===c||e.slice(0,c.length+1)===c+"-"))}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),s=!i&&!h,t=!1;if(q){if(f){while(p){m=b;while(m=m[p])if(h?m.nodeName.toLowerCase()===r:1===m.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&s){m=q,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n&&j[2],m=n&&q.childNodes[n];while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if(1===m.nodeType&&++t&&m===b){k[a]=[w,n,t];break}}else if(s&&(m=b,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n),t===!1)while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if((h?m.nodeName.toLowerCase()===r:1===m.nodeType)&&++t&&(s&&(l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),k[a]=[w,t]),m===b))break;return t-=e,t===d||t%d===0&&t/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||ga.error("unsupported pseudo: "+a);return e[u]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?ia(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=I(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:ia(function(a){var b=[],c=[],d=h(a.replace(P,"$1"));return d[u]?ia(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,e,f){return b[0]=a,d(b,null,f,c),b[0]=null,!c.pop()}}),has:ia(function(a){return function(b){return ga(a,b).length>0}}),contains:ia(function(a){return a=a.replace(_,aa),function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:ia(function(a){return U.test(a||"")||ga.error("unsupported lang: "+a),a=a.replace(_,aa).toLowerCase(),function(b){var c;do if(c=p?b.lang:b.getAttribute("xml:lang")||b.getAttribute("lang"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+"-");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===o},focus:function(a){return a===n.activeElement&&(!n.hasFocus||n.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:oa(!1),disabled:oa(!0),checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return X.test(a.nodeName)},input:function(a){return W.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:pa(function(){return[0]}),last:pa(function(a,b){return[b-1]}),eq:pa(function(a,b,c){return[c<0?c+b:c]}),even:pa(function(a,b){for(var c=0;c=0;)a.push(d);return a}),gt:pa(function(a,b,c){for(var d=c<0?c+b:c;++d1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function va(a,b,c){for(var d=0,e=b.length;d-1&&(f[j]=!(g[j]=l))}}else r=wa(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):G.apply(g,r)})}function ya(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],h=g||d.relative[" "],i=g?1:0,k=ta(function(a){return a===b},h,!0),l=ta(function(a){return I(b,a)>-1},h,!0),m=[function(a,c,d){var e=!g&&(d||c!==j)||((b=c).nodeType?k(a,c,d):l(a,c,d));return b=null,e}];i1&&ua(m),i>1&&sa(a.slice(0,i-1).concat({value:" "===a[i-2].type?"*":""})).replace(P,"$1"),c,i0,e=a.length>0,f=function(f,g,h,i,k){var l,o,q,r=0,s="0",t=f&&[],u=[],v=j,x=f||e&&d.find.TAG("*",k),y=w+=null==v?1:Math.random()||.1,z=x.length;for(k&&(j=g===n||g||k);s!==z&&null!=(l=x[s]);s++){if(e&&l){o=0,g||l.ownerDocument===n||(m(l),h=!p);while(q=a[o++])if(q(l,g||n,h)){i.push(l);break}k&&(w=y)}c&&((l=!q&&l)&&r--,f&&t.push(l))}if(r+=s,c&&s!==r){o=0;while(q=b[o++])q(t,u,g,h);if(f){if(r>0)while(s--)t[s]||u[s]||(u[s]=E.call(i));u=wa(u)}G.apply(i,u),k&&!f&&u.length>0&&r+b.length>1&&ga.uniqueSort(i)}return k&&(w=y,j=v),t};return c?ia(f):f}return h=ga.compile=function(a,b){var c,d=[],e=[],f=A[a+" "];if(!f){b||(b=g(a)),c=b.length;while(c--)f=ya(b[c]),f[u]?d.push(f):e.push(f);f=A(a,za(e,d)),f.selector=a}return f},i=ga.select=function(a,b,c,e){var f,i,j,k,l,m="function"==typeof a&&a,n=!e&&g(a=m.selector||a);if(c=c||[],1===n.length){if(i=n[0]=n[0].slice(0),i.length>2&&"ID"===(j=i[0]).type&&9===b.nodeType&&p&&d.relative[i[1].type]){if(b=(d.find.ID(j.matches[0].replace(_,aa),b)||[])[0],!b)return c;m&&(b=b.parentNode),a=a.slice(i.shift().value.length)}f=V.needsContext.test(a)?0:i.length;while(f--){if(j=i[f],d.relative[k=j.type])break;if((l=d.find[k])&&(e=l(j.matches[0].replace(_,aa),$.test(i[0].type)&&qa(b.parentNode)||b))){if(i.splice(f,1),a=e.length&&sa(i),!a)return G.apply(c,e),c;break}}}return(m||h(a,n))(e,b,!p,c,!b||$.test(a)&&qa(b.parentNode)||b),c},c.sortStable=u.split("").sort(B).join("")===u,c.detectDuplicates=!!l,m(),c.sortDetached=ja(function(a){return 1&a.compareDocumentPosition(n.createElement("fieldset"))}),ja(function(a){return a.innerHTML="","#"===a.firstChild.getAttribute("href")})||ka("type|href|height|width",function(a,b,c){if(!c)return a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&ja(function(a){return a.innerHTML="",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||ka("value",function(a,b,c){if(!c&&"input"===a.nodeName.toLowerCase())return a.defaultValue}),ja(function(a){return null==a.getAttribute("disabled")})||ka(J,function(a,b,c){var d;if(!c)return a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),ga}(a);r.find=x,r.expr=x.selectors,r.expr[":"]=r.expr.pseudos,r.uniqueSort=r.unique=x.uniqueSort,r.text=x.getText,r.isXMLDoc=x.isXML,r.contains=x.contains,r.escapeSelector=x.escape;var y=function(a,b,c){var d=[],e=void 0!==c;while((a=a[b])&&9!==a.nodeType)if(1===a.nodeType){if(e&&r(a).is(c))break;d.push(a)}return d},z=function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c},A=r.expr.match.needsContext;function B(a,b){return a.nodeName&&a.nodeName.toLowerCase()===b.toLowerCase()}var C=/^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i,D=/^.[^:#\[\.,]*$/;function E(a,b,c){return r.isFunction(b)?r.grep(a,function(a,d){return!!b.call(a,d,a)!==c}):b.nodeType?r.grep(a,function(a){return a===b!==c}):"string"!=typeof b?r.grep(a,function(a){return i.call(b,a)>-1!==c}):D.test(b)?r.filter(b,a,c):(b=r.filter(b,a),r.grep(a,function(a){return i.call(b,a)>-1!==c&&1===a.nodeType}))}r.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?r.find.matchesSelector(d,a)?[d]:[]:r.find.matches(a,r.grep(b,function(a){return 1===a.nodeType}))},r.fn.extend({find:function(a){var b,c,d=this.length,e=this;if("string"!=typeof a)return this.pushStack(r(a).filter(function(){for(b=0;b1?r.uniqueSort(c):c},filter:function(a){return this.pushStack(E(this,a||[],!1))},not:function(a){return this.pushStack(E(this,a||[],!0))},is:function(a){return!!E(this,"string"==typeof a&&A.test(a)?r(a):a||[],!1).length}});var F,G=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/,H=r.fn.init=function(a,b,c){var e,f;if(!a)return this;if(c=c||F,"string"==typeof a){if(e="<"===a[0]&&">"===a[a.length-1]&&a.length>=3?[null,a,null]:G.exec(a),!e||!e[1]&&b)return!b||b.jquery?(b||c).find(a):this.constructor(b).find(a);if(e[1]){if(b=b instanceof r?b[0]:b,r.merge(this,r.parseHTML(e[1],b&&b.nodeType?b.ownerDocument||b:d,!0)),C.test(e[1])&&r.isPlainObject(b))for(e in b)r.isFunction(this[e])?this[e](b[e]):this.attr(e,b[e]);return this}return f=d.getElementById(e[2]),f&&(this[0]=f,this.length=1),this}return a.nodeType?(this[0]=a,this.length=1,this):r.isFunction(a)?void 0!==c.ready?c.ready(a):a(r):r.makeArray(a,this)};H.prototype=r.fn,F=r(d);var I=/^(?:parents|prev(?:Until|All))/,J={children:!0,contents:!0,next:!0,prev:!0};r.fn.extend({has:function(a){var b=r(a,this),c=b.length;return this.filter(function(){for(var a=0;a-1:1===c.nodeType&&r.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?r.uniqueSort(f):f)},index:function(a){return a?"string"==typeof a?i.call(r(a),this[0]):i.call(this,a.jquery?a[0]:a):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(r.uniqueSort(r.merge(this.get(),r(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function K(a,b){while((a=a[b])&&1!==a.nodeType);return a}r.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return y(a,"parentNode")},parentsUntil:function(a,b,c){return y(a,"parentNode",c)},next:function(a){return K(a,"nextSibling")},prev:function(a){return K(a,"previousSibling")},nextAll:function(a){return y(a,"nextSibling")},prevAll:function(a){return y(a,"previousSibling")},nextUntil:function(a,b,c){return y(a,"nextSibling",c)},prevUntil:function(a,b,c){return y(a,"previousSibling",c)},siblings:function(a){return z((a.parentNode||{}).firstChild,a)},children:function(a){return z(a.firstChild)},contents:function(a){return B(a,"iframe")?a.contentDocument:(B(a,"template")&&(a=a.content||a),r.merge([],a.childNodes))}},function(a,b){r.fn[a]=function(c,d){var e=r.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=r.filter(d,e)),this.length>1&&(J[a]||r.uniqueSort(e),I.test(a)&&e.reverse()),this.pushStack(e)}});var L=/[^\x20\t\r\n\f]+/g;function M(a){var b={};return r.each(a.match(L)||[],function(a,c){b[c]=!0}),b}r.Callbacks=function(a){a="string"==typeof a?M(a):r.extend({},a);var b,c,d,e,f=[],g=[],h=-1,i=function(){for(e=e||a.once,d=b=!0;g.length;h=-1){c=g.shift();while(++h-1)f.splice(c,1),c<=h&&h--}),this},has:function(a){return a?r.inArray(a,f)>-1:f.length>0},empty:function(){return f&&(f=[]),this},disable:function(){return e=g=[],f=c="",this},disabled:function(){return!f},lock:function(){return e=g=[],c||b||(f=c=""),this},locked:function(){return!!e},fireWith:function(a,c){return e||(c=c||[],c=[a,c.slice?c.slice():c],g.push(c),b||i()),this},fire:function(){return j.fireWith(this,arguments),this},fired:function(){return!!d}};return j};function N(a){return a}function O(a){throw a}function P(a,b,c,d){var e;try{a&&r.isFunction(e=a.promise)?e.call(a).done(b).fail(c):a&&r.isFunction(e=a.then)?e.call(a,b,c):b.apply(void 0,[a].slice(d))}catch(a){c.apply(void 0,[a])}}r.extend({Deferred:function(b){var c=[["notify","progress",r.Callbacks("memory"),r.Callbacks("memory"),2],["resolve","done",r.Callbacks("once memory"),r.Callbacks("once memory"),0,"resolved"],["reject","fail",r.Callbacks("once memory"),r.Callbacks("once memory"),1,"rejected"]],d="pending",e={state:function(){return d},always:function(){return f.done(arguments).fail(arguments),this},"catch":function(a){return e.then(null,a)},pipe:function(){var a=arguments;return r.Deferred(function(b){r.each(c,function(c,d){var e=r.isFunction(a[d[4]])&&a[d[4]];f[d[1]](function(){var a=e&&e.apply(this,arguments);a&&r.isFunction(a.promise)?a.promise().progress(b.notify).done(b.resolve).fail(b.reject):b[d[0]+"With"](this,e?[a]:arguments)})}),a=null}).promise()},then:function(b,d,e){var f=0;function g(b,c,d,e){return function(){var h=this,i=arguments,j=function(){var a,j;if(!(b=f&&(d!==O&&(h=void 0,i=[a]),c.rejectWith(h,i))}};b?k():(r.Deferred.getStackHook&&(k.stackTrace=r.Deferred.getStackHook()),a.setTimeout(k))}}return r.Deferred(function(a){c[0][3].add(g(0,a,r.isFunction(e)?e:N,a.notifyWith)),c[1][3].add(g(0,a,r.isFunction(b)?b:N)),c[2][3].add(g(0,a,r.isFunction(d)?d:O))}).promise()},promise:function(a){return null!=a?r.extend(a,e):e}},f={};return r.each(c,function(a,b){var g=b[2],h=b[5];e[b[1]]=g.add,h&&g.add(function(){d=h},c[3-a][2].disable,c[0][2].lock),g.add(b[3].fire),f[b[0]]=function(){return f[b[0]+"With"](this===f?void 0:this,arguments),this},f[b[0]+"With"]=g.fireWith}),e.promise(f),b&&b.call(f,f),f},when:function(a){var b=arguments.length,c=b,d=Array(c),e=f.call(arguments),g=r.Deferred(),h=function(a){return function(c){d[a]=this,e[a]=arguments.length>1?f.call(arguments):c,--b||g.resolveWith(d,e)}};if(b<=1&&(P(a,g.done(h(c)).resolve,g.reject,!b),"pending"===g.state()||r.isFunction(e[c]&&e[c].then)))return g.then();while(c--)P(e[c],h(c),g.reject);return g.promise()}});var Q=/^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/;r.Deferred.exceptionHook=function(b,c){a.console&&a.console.warn&&b&&Q.test(b.name)&&a.console.warn("jQuery.Deferred exception: "+b.message,b.stack,c)},r.readyException=function(b){a.setTimeout(function(){throw b})};var R=r.Deferred();r.fn.ready=function(a){return R.then(a)["catch"](function(a){r.readyException(a)}),this},r.extend({isReady:!1,readyWait:1,ready:function(a){(a===!0?--r.readyWait:r.isReady)||(r.isReady=!0,a!==!0&&--r.readyWait>0||R.resolveWith(d,[r]))}}),r.ready.then=R.then;function S(){d.removeEventListener("DOMContentLoaded",S), +a.removeEventListener("load",S),r.ready()}"complete"===d.readyState||"loading"!==d.readyState&&!d.documentElement.doScroll?a.setTimeout(r.ready):(d.addEventListener("DOMContentLoaded",S),a.addEventListener("load",S));var T=function(a,b,c,d,e,f,g){var h=0,i=a.length,j=null==c;if("object"===r.type(c)){e=!0;for(h in c)T(a,b,h,c[h],!0,f,g)}else if(void 0!==d&&(e=!0,r.isFunction(d)||(g=!0),j&&(g?(b.call(a,d),b=null):(j=b,b=function(a,b,c){return j.call(r(a),c)})),b))for(;h1,null,!0)},removeData:function(a){return this.each(function(){X.remove(this,a)})}}),r.extend({queue:function(a,b,c){var d;if(a)return b=(b||"fx")+"queue",d=W.get(a,b),c&&(!d||Array.isArray(c)?d=W.access(a,b,r.makeArray(c)):d.push(c)),d||[]},dequeue:function(a,b){b=b||"fx";var c=r.queue(a,b),d=c.length,e=c.shift(),f=r._queueHooks(a,b),g=function(){r.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return W.get(a,c)||W.access(a,c,{empty:r.Callbacks("once memory").add(function(){W.remove(a,[b+"queue",c])})})}}),r.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.length\x20\t\r\n\f]+)/i,la=/^$|\/(?:java|ecma)script/i,ma={option:[1,""],thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};ma.optgroup=ma.option,ma.tbody=ma.tfoot=ma.colgroup=ma.caption=ma.thead,ma.th=ma.td;function na(a,b){var c;return c="undefined"!=typeof a.getElementsByTagName?a.getElementsByTagName(b||"*"):"undefined"!=typeof a.querySelectorAll?a.querySelectorAll(b||"*"):[],void 0===b||b&&B(a,b)?r.merge([a],c):c}function oa(a,b){for(var c=0,d=a.length;c-1)e&&e.push(f);else if(j=r.contains(f.ownerDocument,f),g=na(l.appendChild(f),"script"),j&&oa(g),c){k=0;while(f=g[k++])la.test(f.type||"")&&c.push(f)}return l}!function(){var a=d.createDocumentFragment(),b=a.appendChild(d.createElement("div")),c=d.createElement("input");c.setAttribute("type","radio"),c.setAttribute("checked","checked"),c.setAttribute("name","t"),b.appendChild(c),o.checkClone=b.cloneNode(!0).cloneNode(!0).lastChild.checked,b.innerHTML="",o.noCloneChecked=!!b.cloneNode(!0).lastChild.defaultValue}();var ra=d.documentElement,sa=/^key/,ta=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,ua=/^([^.]*)(?:\.(.+)|)/;function va(){return!0}function wa(){return!1}function xa(){try{return d.activeElement}catch(a){}}function ya(a,b,c,d,e,f){var g,h;if("object"==typeof b){"string"!=typeof c&&(d=d||c,c=void 0);for(h in b)ya(a,h,c,d,b[h],f);return a}if(null==d&&null==e?(e=c,d=c=void 0):null==e&&("string"==typeof c?(e=d,d=void 0):(e=d,d=c,c=void 0)),e===!1)e=wa;else if(!e)return a;return 1===f&&(g=e,e=function(a){return r().off(a),g.apply(this,arguments)},e.guid=g.guid||(g.guid=r.guid++)),a.each(function(){r.event.add(this,b,e,d,c)})}r.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,n,o,p,q=W.get(a);if(q){c.handler&&(f=c,c=f.handler,e=f.selector),e&&r.find.matchesSelector(ra,e),c.guid||(c.guid=r.guid++),(i=q.events)||(i=q.events={}),(g=q.handle)||(g=q.handle=function(b){return"undefined"!=typeof r&&r.event.triggered!==b.type?r.event.dispatch.apply(a,arguments):void 0}),b=(b||"").match(L)||[""],j=b.length;while(j--)h=ua.exec(b[j])||[],n=p=h[1],o=(h[2]||"").split(".").sort(),n&&(l=r.event.special[n]||{},n=(e?l.delegateType:l.bindType)||n,l=r.event.special[n]||{},k=r.extend({type:n,origType:p,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&r.expr.match.needsContext.test(e),namespace:o.join(".")},f),(m=i[n])||(m=i[n]=[],m.delegateCount=0,l.setup&&l.setup.call(a,d,o,g)!==!1||a.addEventListener&&a.addEventListener(n,g)),l.add&&(l.add.call(a,k),k.handler.guid||(k.handler.guid=c.guid)),e?m.splice(m.delegateCount++,0,k):m.push(k),r.event.global[n]=!0)}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,n,o,p,q=W.hasData(a)&&W.get(a);if(q&&(i=q.events)){b=(b||"").match(L)||[""],j=b.length;while(j--)if(h=ua.exec(b[j])||[],n=p=h[1],o=(h[2]||"").split(".").sort(),n){l=r.event.special[n]||{},n=(d?l.delegateType:l.bindType)||n,m=i[n]||[],h=h[2]&&new RegExp("(^|\\.)"+o.join("\\.(?:.*\\.|)")+"(\\.|$)"),g=f=m.length;while(f--)k=m[f],!e&&p!==k.origType||c&&c.guid!==k.guid||h&&!h.test(k.namespace)||d&&d!==k.selector&&("**"!==d||!k.selector)||(m.splice(f,1),k.selector&&m.delegateCount--,l.remove&&l.remove.call(a,k));g&&!m.length&&(l.teardown&&l.teardown.call(a,o,q.handle)!==!1||r.removeEvent(a,n,q.handle),delete i[n])}else for(n in i)r.event.remove(a,n+b[j],c,d,!0);r.isEmptyObject(i)&&W.remove(a,"handle events")}},dispatch:function(a){var b=r.event.fix(a),c,d,e,f,g,h,i=new Array(arguments.length),j=(W.get(this,"events")||{})[b.type]||[],k=r.event.special[b.type]||{};for(i[0]=b,c=1;c=1))for(;j!==this;j=j.parentNode||this)if(1===j.nodeType&&("click"!==a.type||j.disabled!==!0)){for(f=[],g={},c=0;c-1:r.find(e,this,null,[j]).length),g[e]&&f.push(d);f.length&&h.push({elem:j,handlers:f})}return j=this,i\x20\t\r\n\f]*)[^>]*)\/>/gi,Aa=/\s*$/g;function Ea(a,b){return B(a,"table")&&B(11!==b.nodeType?b:b.firstChild,"tr")?r(">tbody",a)[0]||a:a}function Fa(a){return a.type=(null!==a.getAttribute("type"))+"/"+a.type,a}function Ga(a){var b=Ca.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function Ha(a,b){var c,d,e,f,g,h,i,j;if(1===b.nodeType){if(W.hasData(a)&&(f=W.access(a),g=W.set(b,f),j=f.events)){delete g.handle,g.events={};for(e in j)for(c=0,d=j[e].length;c1&&"string"==typeof q&&!o.checkClone&&Ba.test(q))return a.each(function(e){var f=a.eq(e);s&&(b[0]=q.call(this,e,f.html())),Ja(f,b,c,d)});if(m&&(e=qa(b,a[0].ownerDocument,!1,a,d),f=e.firstChild,1===e.childNodes.length&&(e=f),f||d)){for(h=r.map(na(e,"script"),Fa),i=h.length;l")},clone:function(a,b,c){var d,e,f,g,h=a.cloneNode(!0),i=r.contains(a.ownerDocument,a);if(!(o.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||r.isXMLDoc(a)))for(g=na(h),f=na(a),d=0,e=f.length;d0&&oa(g,!i&&na(a,"script")),h},cleanData:function(a){for(var b,c,d,e=r.event.special,f=0;void 0!==(c=a[f]);f++)if(U(c)){if(b=c[W.expando]){if(b.events)for(d in b.events)e[d]?r.event.remove(c,d):r.removeEvent(c,d,b.handle);c[W.expando]=void 0}c[X.expando]&&(c[X.expando]=void 0)}}}),r.fn.extend({detach:function(a){return Ka(this,a,!0)},remove:function(a){return Ka(this,a)},text:function(a){return T(this,function(a){return void 0===a?r.text(this):this.empty().each(function(){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||(this.textContent=a)})},null,a,arguments.length)},append:function(){return Ja(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Ea(this,a);b.appendChild(a)}})},prepend:function(){return Ja(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Ea(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return Ja(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return Ja(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},empty:function(){for(var a,b=0;null!=(a=this[b]);b++)1===a.nodeType&&(r.cleanData(na(a,!1)),a.textContent="");return this},clone:function(a,b){return a=null!=a&&a,b=null==b?a:b,this.map(function(){return r.clone(this,a,b)})},html:function(a){return T(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a&&1===b.nodeType)return b.innerHTML;if("string"==typeof a&&!Aa.test(a)&&!ma[(ka.exec(a)||["",""])[1].toLowerCase()]){a=r.htmlPrefilter(a);try{for(;c1)}});function _a(a,b,c,d,e){return new _a.prototype.init(a,b,c,d,e)}r.Tween=_a,_a.prototype={constructor:_a,init:function(a,b,c,d,e,f){this.elem=a,this.prop=c,this.easing=e||r.easing._default,this.options=b,this.start=this.now=this.cur(),this.end=d,this.unit=f||(r.cssNumber[c]?"":"px")},cur:function(){var a=_a.propHooks[this.prop];return a&&a.get?a.get(this):_a.propHooks._default.get(this)},run:function(a){var b,c=_a.propHooks[this.prop];return this.options.duration?this.pos=b=r.easing[this.easing](a,this.options.duration*a,0,1,this.options.duration):this.pos=b=a,this.now=(this.end-this.start)*b+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),c&&c.set?c.set(this):_a.propHooks._default.set(this),this}},_a.prototype.init.prototype=_a.prototype,_a.propHooks={_default:{get:function(a){var b;return 1!==a.elem.nodeType||null!=a.elem[a.prop]&&null==a.elem.style[a.prop]?a.elem[a.prop]:(b=r.css(a.elem,a.prop,""),b&&"auto"!==b?b:0)},set:function(a){r.fx.step[a.prop]?r.fx.step[a.prop](a):1!==a.elem.nodeType||null==a.elem.style[r.cssProps[a.prop]]&&!r.cssHooks[a.prop]?a.elem[a.prop]=a.now:r.style(a.elem,a.prop,a.now+a.unit)}}},_a.propHooks.scrollTop=_a.propHooks.scrollLeft={set:function(a){a.elem.nodeType&&a.elem.parentNode&&(a.elem[a.prop]=a.now)}},r.easing={linear:function(a){return a},swing:function(a){return.5-Math.cos(a*Math.PI)/2},_default:"swing"},r.fx=_a.prototype.init,r.fx.step={};var ab,bb,cb=/^(?:toggle|show|hide)$/,db=/queueHooks$/;function eb(){bb&&(d.hidden===!1&&a.requestAnimationFrame?a.requestAnimationFrame(eb):a.setTimeout(eb,r.fx.interval),r.fx.tick())}function fb(){return a.setTimeout(function(){ab=void 0}),ab=r.now()}function gb(a,b){var c,d=0,e={height:a};for(b=b?1:0;d<4;d+=2-b)c=ca[d],e["margin"+c]=e["padding"+c]=a;return b&&(e.opacity=e.width=a),e}function hb(a,b,c){for(var d,e=(kb.tweeners[b]||[]).concat(kb.tweeners["*"]),f=0,g=e.length;f1)},removeAttr:function(a){return this.each(function(){r.removeAttr(this,a)})}}),r.extend({attr:function(a,b,c){var d,e,f=a.nodeType;if(3!==f&&8!==f&&2!==f)return"undefined"==typeof a.getAttribute?r.prop(a,b,c):(1===f&&r.isXMLDoc(a)||(e=r.attrHooks[b.toLowerCase()]||(r.expr.match.bool.test(b)?lb:void 0)),void 0!==c?null===c?void r.removeAttr(a,b):e&&"set"in e&&void 0!==(d=e.set(a,c,b))?d:(a.setAttribute(b,c+""),c):e&&"get"in e&&null!==(d=e.get(a,b))?d:(d=r.find.attr(a,b), +null==d?void 0:d))},attrHooks:{type:{set:function(a,b){if(!o.radioValue&&"radio"===b&&B(a,"input")){var c=a.value;return a.setAttribute("type",b),c&&(a.value=c),b}}}},removeAttr:function(a,b){var c,d=0,e=b&&b.match(L);if(e&&1===a.nodeType)while(c=e[d++])a.removeAttribute(c)}}),lb={set:function(a,b,c){return b===!1?r.removeAttr(a,c):a.setAttribute(c,c),c}},r.each(r.expr.match.bool.source.match(/\w+/g),function(a,b){var c=mb[b]||r.find.attr;mb[b]=function(a,b,d){var e,f,g=b.toLowerCase();return d||(f=mb[g],mb[g]=e,e=null!=c(a,b,d)?g:null,mb[g]=f),e}});var nb=/^(?:input|select|textarea|button)$/i,ob=/^(?:a|area)$/i;r.fn.extend({prop:function(a,b){return T(this,r.prop,a,b,arguments.length>1)},removeProp:function(a){return this.each(function(){delete this[r.propFix[a]||a]})}}),r.extend({prop:function(a,b,c){var d,e,f=a.nodeType;if(3!==f&&8!==f&&2!==f)return 1===f&&r.isXMLDoc(a)||(b=r.propFix[b]||b,e=r.propHooks[b]),void 0!==c?e&&"set"in e&&void 0!==(d=e.set(a,c,b))?d:a[b]=c:e&&"get"in e&&null!==(d=e.get(a,b))?d:a[b]},propHooks:{tabIndex:{get:function(a){var b=r.find.attr(a,"tabindex");return b?parseInt(b,10):nb.test(a.nodeName)||ob.test(a.nodeName)&&a.href?0:-1}}},propFix:{"for":"htmlFor","class":"className"}}),o.optSelected||(r.propHooks.selected={get:function(a){var b=a.parentNode;return b&&b.parentNode&&b.parentNode.selectedIndex,null},set:function(a){var b=a.parentNode;b&&(b.selectedIndex,b.parentNode&&b.parentNode.selectedIndex)}}),r.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){r.propFix[this.toLowerCase()]=this});function pb(a){var b=a.match(L)||[];return b.join(" ")}function qb(a){return a.getAttribute&&a.getAttribute("class")||""}r.fn.extend({addClass:function(a){var b,c,d,e,f,g,h,i=0;if(r.isFunction(a))return this.each(function(b){r(this).addClass(a.call(this,b,qb(this)))});if("string"==typeof a&&a){b=a.match(L)||[];while(c=this[i++])if(e=qb(c),d=1===c.nodeType&&" "+pb(e)+" "){g=0;while(f=b[g++])d.indexOf(" "+f+" ")<0&&(d+=f+" ");h=pb(d),e!==h&&c.setAttribute("class",h)}}return this},removeClass:function(a){var b,c,d,e,f,g,h,i=0;if(r.isFunction(a))return this.each(function(b){r(this).removeClass(a.call(this,b,qb(this)))});if(!arguments.length)return this.attr("class","");if("string"==typeof a&&a){b=a.match(L)||[];while(c=this[i++])if(e=qb(c),d=1===c.nodeType&&" "+pb(e)+" "){g=0;while(f=b[g++])while(d.indexOf(" "+f+" ")>-1)d=d.replace(" "+f+" "," ");h=pb(d),e!==h&&c.setAttribute("class",h)}}return this},toggleClass:function(a,b){var c=typeof a;return"boolean"==typeof b&&"string"===c?b?this.addClass(a):this.removeClass(a):r.isFunction(a)?this.each(function(c){r(this).toggleClass(a.call(this,c,qb(this),b),b)}):this.each(function(){var b,d,e,f;if("string"===c){d=0,e=r(this),f=a.match(L)||[];while(b=f[d++])e.hasClass(b)?e.removeClass(b):e.addClass(b)}else void 0!==a&&"boolean"!==c||(b=qb(this),b&&W.set(this,"__className__",b),this.setAttribute&&this.setAttribute("class",b||a===!1?"":W.get(this,"__className__")||""))})},hasClass:function(a){var b,c,d=0;b=" "+a+" ";while(c=this[d++])if(1===c.nodeType&&(" "+pb(qb(c))+" ").indexOf(b)>-1)return!0;return!1}});var rb=/\r/g;r.fn.extend({val:function(a){var b,c,d,e=this[0];{if(arguments.length)return d=r.isFunction(a),this.each(function(c){var e;1===this.nodeType&&(e=d?a.call(this,c,r(this).val()):a,null==e?e="":"number"==typeof e?e+="":Array.isArray(e)&&(e=r.map(e,function(a){return null==a?"":a+""})),b=r.valHooks[this.type]||r.valHooks[this.nodeName.toLowerCase()],b&&"set"in b&&void 0!==b.set(this,e,"value")||(this.value=e))});if(e)return b=r.valHooks[e.type]||r.valHooks[e.nodeName.toLowerCase()],b&&"get"in b&&void 0!==(c=b.get(e,"value"))?c:(c=e.value,"string"==typeof c?c.replace(rb,""):null==c?"":c)}}}),r.extend({valHooks:{option:{get:function(a){var b=r.find.attr(a,"value");return null!=b?b:pb(r.text(a))}},select:{get:function(a){var b,c,d,e=a.options,f=a.selectedIndex,g="select-one"===a.type,h=g?null:[],i=g?f+1:e.length;for(d=f<0?i:g?f:0;d-1)&&(c=!0);return c||(a.selectedIndex=-1),f}}}}),r.each(["radio","checkbox"],function(){r.valHooks[this]={set:function(a,b){if(Array.isArray(b))return a.checked=r.inArray(r(a).val(),b)>-1}},o.checkOn||(r.valHooks[this].get=function(a){return null===a.getAttribute("value")?"on":a.value})});var sb=/^(?:focusinfocus|focusoutblur)$/;r.extend(r.event,{trigger:function(b,c,e,f){var g,h,i,j,k,m,n,o=[e||d],p=l.call(b,"type")?b.type:b,q=l.call(b,"namespace")?b.namespace.split("."):[];if(h=i=e=e||d,3!==e.nodeType&&8!==e.nodeType&&!sb.test(p+r.event.triggered)&&(p.indexOf(".")>-1&&(q=p.split("."),p=q.shift(),q.sort()),k=p.indexOf(":")<0&&"on"+p,b=b[r.expando]?b:new r.Event(p,"object"==typeof b&&b),b.isTrigger=f?2:3,b.namespace=q.join("."),b.rnamespace=b.namespace?new RegExp("(^|\\.)"+q.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,b.result=void 0,b.target||(b.target=e),c=null==c?[b]:r.makeArray(c,[b]),n=r.event.special[p]||{},f||!n.trigger||n.trigger.apply(e,c)!==!1)){if(!f&&!n.noBubble&&!r.isWindow(e)){for(j=n.delegateType||p,sb.test(j+p)||(h=h.parentNode);h;h=h.parentNode)o.push(h),i=h;i===(e.ownerDocument||d)&&o.push(i.defaultView||i.parentWindow||a)}g=0;while((h=o[g++])&&!b.isPropagationStopped())b.type=g>1?j:n.bindType||p,m=(W.get(h,"events")||{})[b.type]&&W.get(h,"handle"),m&&m.apply(h,c),m=k&&h[k],m&&m.apply&&U(h)&&(b.result=m.apply(h,c),b.result===!1&&b.preventDefault());return b.type=p,f||b.isDefaultPrevented()||n._default&&n._default.apply(o.pop(),c)!==!1||!U(e)||k&&r.isFunction(e[p])&&!r.isWindow(e)&&(i=e[k],i&&(e[k]=null),r.event.triggered=p,e[p](),r.event.triggered=void 0,i&&(e[k]=i)),b.result}},simulate:function(a,b,c){var d=r.extend(new r.Event,c,{type:a,isSimulated:!0});r.event.trigger(d,null,b)}}),r.fn.extend({trigger:function(a,b){return this.each(function(){r.event.trigger(a,b,this)})},triggerHandler:function(a,b){var c=this[0];if(c)return r.event.trigger(a,b,c,!0)}}),r.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(a,b){r.fn[b]=function(a,c){return arguments.length>0?this.on(b,null,a,c):this.trigger(b)}}),r.fn.extend({hover:function(a,b){return this.mouseenter(a).mouseleave(b||a)}}),o.focusin="onfocusin"in a,o.focusin||r.each({focus:"focusin",blur:"focusout"},function(a,b){var c=function(a){r.event.simulate(b,a.target,r.event.fix(a))};r.event.special[b]={setup:function(){var d=this.ownerDocument||this,e=W.access(d,b);e||d.addEventListener(a,c,!0),W.access(d,b,(e||0)+1)},teardown:function(){var d=this.ownerDocument||this,e=W.access(d,b)-1;e?W.access(d,b,e):(d.removeEventListener(a,c,!0),W.remove(d,b))}}});var tb=a.location,ub=r.now(),vb=/\?/;r.parseXML=function(b){var c;if(!b||"string"!=typeof b)return null;try{c=(new a.DOMParser).parseFromString(b,"text/xml")}catch(d){c=void 0}return c&&!c.getElementsByTagName("parsererror").length||r.error("Invalid XML: "+b),c};var wb=/\[\]$/,xb=/\r?\n/g,yb=/^(?:submit|button|image|reset|file)$/i,zb=/^(?:input|select|textarea|keygen)/i;function Ab(a,b,c,d){var e;if(Array.isArray(b))r.each(b,function(b,e){c||wb.test(a)?d(a,e):Ab(a+"["+("object"==typeof e&&null!=e?b:"")+"]",e,c,d)});else if(c||"object"!==r.type(b))d(a,b);else for(e in b)Ab(a+"["+e+"]",b[e],c,d)}r.param=function(a,b){var c,d=[],e=function(a,b){var c=r.isFunction(b)?b():b;d[d.length]=encodeURIComponent(a)+"="+encodeURIComponent(null==c?"":c)};if(Array.isArray(a)||a.jquery&&!r.isPlainObject(a))r.each(a,function(){e(this.name,this.value)});else for(c in a)Ab(c,a[c],b,e);return d.join("&")},r.fn.extend({serialize:function(){return r.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var a=r.prop(this,"elements");return a?r.makeArray(a):this}).filter(function(){var a=this.type;return this.name&&!r(this).is(":disabled")&&zb.test(this.nodeName)&&!yb.test(a)&&(this.checked||!ja.test(a))}).map(function(a,b){var c=r(this).val();return null==c?null:Array.isArray(c)?r.map(c,function(a){return{name:b.name,value:a.replace(xb,"\r\n")}}):{name:b.name,value:c.replace(xb,"\r\n")}}).get()}});var Bb=/%20/g,Cb=/#.*$/,Db=/([?&])_=[^&]*/,Eb=/^(.*?):[ \t]*([^\r\n]*)$/gm,Fb=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,Gb=/^(?:GET|HEAD)$/,Hb=/^\/\//,Ib={},Jb={},Kb="*/".concat("*"),Lb=d.createElement("a");Lb.href=tb.href;function Mb(a){return function(b,c){"string"!=typeof b&&(c=b,b="*");var d,e=0,f=b.toLowerCase().match(L)||[];if(r.isFunction(c))while(d=f[e++])"+"===d[0]?(d=d.slice(1)||"*",(a[d]=a[d]||[]).unshift(c)):(a[d]=a[d]||[]).push(c)}}function Nb(a,b,c,d){var e={},f=a===Jb;function g(h){var i;return e[h]=!0,r.each(a[h]||[],function(a,h){var j=h(b,c,d);return"string"!=typeof j||f||e[j]?f?!(i=j):void 0:(b.dataTypes.unshift(j),g(j),!1)}),i}return g(b.dataTypes[0])||!e["*"]&&g("*")}function Ob(a,b){var c,d,e=r.ajaxSettings.flatOptions||{};for(c in b)void 0!==b[c]&&((e[c]?a:d||(d={}))[c]=b[c]);return d&&r.extend(!0,a,d),a}function Pb(a,b,c){var d,e,f,g,h=a.contents,i=a.dataTypes;while("*"===i[0])i.shift(),void 0===d&&(d=a.mimeType||b.getResponseHeader("Content-Type"));if(d)for(e in h)if(h[e]&&h[e].test(d)){i.unshift(e);break}if(i[0]in c)f=i[0];else{for(e in c){if(!i[0]||a.converters[e+" "+i[0]]){f=e;break}g||(g=e)}f=f||g}if(f)return f!==i[0]&&i.unshift(f),c[f]}function Qb(a,b,c,d){var e,f,g,h,i,j={},k=a.dataTypes.slice();if(k[1])for(g in a.converters)j[g.toLowerCase()]=a.converters[g];f=k.shift();while(f)if(a.responseFields[f]&&(c[a.responseFields[f]]=b),!i&&d&&a.dataFilter&&(b=a.dataFilter(b,a.dataType)),i=f,f=k.shift())if("*"===f)f=i;else if("*"!==i&&i!==f){if(g=j[i+" "+f]||j["* "+f],!g)for(e in j)if(h=e.split(" "),h[1]===f&&(g=j[i+" "+h[0]]||j["* "+h[0]])){g===!0?g=j[e]:j[e]!==!0&&(f=h[0],k.unshift(h[1]));break}if(g!==!0)if(g&&a["throws"])b=g(b);else try{b=g(b)}catch(l){return{state:"parsererror",error:g?l:"No conversion from "+i+" to "+f}}}return{state:"success",data:b}}r.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:tb.href,type:"GET",isLocal:Fb.test(tb.protocol),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":Kb,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/\bxml\b/,html:/\bhtml/,json:/\bjson\b/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":JSON.parse,"text xml":r.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(a,b){return b?Ob(Ob(a,r.ajaxSettings),b):Ob(r.ajaxSettings,a)},ajaxPrefilter:Mb(Ib),ajaxTransport:Mb(Jb),ajax:function(b,c){"object"==typeof b&&(c=b,b=void 0),c=c||{};var e,f,g,h,i,j,k,l,m,n,o=r.ajaxSetup({},c),p=o.context||o,q=o.context&&(p.nodeType||p.jquery)?r(p):r.event,s=r.Deferred(),t=r.Callbacks("once memory"),u=o.statusCode||{},v={},w={},x="canceled",y={readyState:0,getResponseHeader:function(a){var b;if(k){if(!h){h={};while(b=Eb.exec(g))h[b[1].toLowerCase()]=b[2]}b=h[a.toLowerCase()]}return null==b?null:b},getAllResponseHeaders:function(){return k?g:null},setRequestHeader:function(a,b){return null==k&&(a=w[a.toLowerCase()]=w[a.toLowerCase()]||a,v[a]=b),this},overrideMimeType:function(a){return null==k&&(o.mimeType=a),this},statusCode:function(a){var b;if(a)if(k)y.always(a[y.status]);else for(b in a)u[b]=[u[b],a[b]];return this},abort:function(a){var b=a||x;return e&&e.abort(b),A(0,b),this}};if(s.promise(y),o.url=((b||o.url||tb.href)+"").replace(Hb,tb.protocol+"//"),o.type=c.method||c.type||o.method||o.type,o.dataTypes=(o.dataType||"*").toLowerCase().match(L)||[""],null==o.crossDomain){j=d.createElement("a");try{j.href=o.url,j.href=j.href,o.crossDomain=Lb.protocol+"//"+Lb.host!=j.protocol+"//"+j.host}catch(z){o.crossDomain=!0}}if(o.data&&o.processData&&"string"!=typeof o.data&&(o.data=r.param(o.data,o.traditional)),Nb(Ib,o,c,y),k)return y;l=r.event&&o.global,l&&0===r.active++&&r.event.trigger("ajaxStart"),o.type=o.type.toUpperCase(),o.hasContent=!Gb.test(o.type),f=o.url.replace(Cb,""),o.hasContent?o.data&&o.processData&&0===(o.contentType||"").indexOf("application/x-www-form-urlencoded")&&(o.data=o.data.replace(Bb,"+")):(n=o.url.slice(f.length),o.data&&(f+=(vb.test(f)?"&":"?")+o.data,delete o.data),o.cache===!1&&(f=f.replace(Db,"$1"),n=(vb.test(f)?"&":"?")+"_="+ub++ +n),o.url=f+n),o.ifModified&&(r.lastModified[f]&&y.setRequestHeader("If-Modified-Since",r.lastModified[f]),r.etag[f]&&y.setRequestHeader("If-None-Match",r.etag[f])),(o.data&&o.hasContent&&o.contentType!==!1||c.contentType)&&y.setRequestHeader("Content-Type",o.contentType),y.setRequestHeader("Accept",o.dataTypes[0]&&o.accepts[o.dataTypes[0]]?o.accepts[o.dataTypes[0]]+("*"!==o.dataTypes[0]?", "+Kb+"; q=0.01":""):o.accepts["*"]);for(m in o.headers)y.setRequestHeader(m,o.headers[m]);if(o.beforeSend&&(o.beforeSend.call(p,y,o)===!1||k))return y.abort();if(x="abort",t.add(o.complete),y.done(o.success),y.fail(o.error),e=Nb(Jb,o,c,y)){if(y.readyState=1,l&&q.trigger("ajaxSend",[y,o]),k)return y;o.async&&o.timeout>0&&(i=a.setTimeout(function(){y.abort("timeout")},o.timeout));try{k=!1,e.send(v,A)}catch(z){if(k)throw z;A(-1,z)}}else A(-1,"No Transport");function A(b,c,d,h){var j,m,n,v,w,x=c;k||(k=!0,i&&a.clearTimeout(i),e=void 0,g=h||"",y.readyState=b>0?4:0,j=b>=200&&b<300||304===b,d&&(v=Pb(o,y,d)),v=Qb(o,v,y,j),j?(o.ifModified&&(w=y.getResponseHeader("Last-Modified"),w&&(r.lastModified[f]=w),w=y.getResponseHeader("etag"),w&&(r.etag[f]=w)),204===b||"HEAD"===o.type?x="nocontent":304===b?x="notmodified":(x=v.state,m=v.data,n=v.error,j=!n)):(n=x,!b&&x||(x="error",b<0&&(b=0))),y.status=b,y.statusText=(c||x)+"",j?s.resolveWith(p,[m,x,y]):s.rejectWith(p,[y,x,n]),y.statusCode(u),u=void 0,l&&q.trigger(j?"ajaxSuccess":"ajaxError",[y,o,j?m:n]),t.fireWith(p,[y,x]),l&&(q.trigger("ajaxComplete",[y,o]),--r.active||r.event.trigger("ajaxStop")))}return y},getJSON:function(a,b,c){return r.get(a,b,c,"json")},getScript:function(a,b){return r.get(a,void 0,b,"script")}}),r.each(["get","post"],function(a,b){r[b]=function(a,c,d,e){return r.isFunction(c)&&(e=e||d,d=c,c=void 0),r.ajax(r.extend({url:a,type:b,dataType:e,data:c,success:d},r.isPlainObject(a)&&a))}}),r._evalUrl=function(a){return r.ajax({url:a,type:"GET",dataType:"script",cache:!0,async:!1,global:!1,"throws":!0})},r.fn.extend({wrapAll:function(a){var b;return this[0]&&(r.isFunction(a)&&(a=a.call(this[0])),b=r(a,this[0].ownerDocument).eq(0).clone(!0),this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstElementChild)a=a.firstElementChild;return a}).append(this)),this},wrapInner:function(a){return r.isFunction(a)?this.each(function(b){r(this).wrapInner(a.call(this,b))}):this.each(function(){var b=r(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){var b=r.isFunction(a);return this.each(function(c){r(this).wrapAll(b?a.call(this,c):a)})},unwrap:function(a){return this.parent(a).not("body").each(function(){r(this).replaceWith(this.childNodes)}),this}}),r.expr.pseudos.hidden=function(a){return!r.expr.pseudos.visible(a)},r.expr.pseudos.visible=function(a){return!!(a.offsetWidth||a.offsetHeight||a.getClientRects().length)},r.ajaxSettings.xhr=function(){try{return new a.XMLHttpRequest}catch(b){}};var Rb={0:200,1223:204},Sb=r.ajaxSettings.xhr();o.cors=!!Sb&&"withCredentials"in Sb,o.ajax=Sb=!!Sb,r.ajaxTransport(function(b){var c,d;if(o.cors||Sb&&!b.crossDomain)return{send:function(e,f){var g,h=b.xhr();if(h.open(b.type,b.url,b.async,b.username,b.password),b.xhrFields)for(g in b.xhrFields)h[g]=b.xhrFields[g];b.mimeType&&h.overrideMimeType&&h.overrideMimeType(b.mimeType),b.crossDomain||e["X-Requested-With"]||(e["X-Requested-With"]="XMLHttpRequest");for(g in e)h.setRequestHeader(g,e[g]);c=function(a){return function(){c&&(c=d=h.onload=h.onerror=h.onabort=h.onreadystatechange=null,"abort"===a?h.abort():"error"===a?"number"!=typeof h.status?f(0,"error"):f(h.status,h.statusText):f(Rb[h.status]||h.status,h.statusText,"text"!==(h.responseType||"text")||"string"!=typeof h.responseText?{binary:h.response}:{text:h.responseText},h.getAllResponseHeaders()))}},h.onload=c(),d=h.onerror=c("error"),void 0!==h.onabort?h.onabort=d:h.onreadystatechange=function(){4===h.readyState&&a.setTimeout(function(){c&&d()})},c=c("abort");try{h.send(b.hasContent&&b.data||null)}catch(i){if(c)throw i}},abort:function(){c&&c()}}}),r.ajaxPrefilter(function(a){a.crossDomain&&(a.contents.script=!1)}),r.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/\b(?:java|ecma)script\b/},converters:{"text script":function(a){return r.globalEval(a),a}}}),r.ajaxPrefilter("script",function(a){void 0===a.cache&&(a.cache=!1),a.crossDomain&&(a.type="GET")}),r.ajaxTransport("script",function(a){if(a.crossDomain){var b,c;return{send:function(e,f){b=r(" + + +
Common page footer
+ + \ No newline at end of file diff --git a/CSF.Screenplay.WebTestWebsite_old/Views/ElementsWithAttributes/Index.pt b/CSF.Screenplay.WebTestWebsite_old/Views/ElementsWithAttributes/Index.pt new file mode 100644 index 00000000..6ba39197 --- /dev/null +++ b/CSF.Screenplay.WebTestWebsite_old/Views/ElementsWithAttributes/Index.pt @@ -0,0 +1,42 @@ + + + +Page two + + + + + + +
Common page header
+
+

Page two

+
+
+

Elements with attributes

+
This has class "foo bar"
+

+ This date has no readonly attribute of its own, but browsers might imply one? +

+ +

+ This input does have an explicit readonly attribute. +

+ +

Button to get output

+ +
+
+

Outputs

+

The div class is .

+

The date input readonly state is .

+

The text input readonly state is .

+

The text input placeholder is .

+
+
+ + +
+
Common page footer
+ + \ No newline at end of file diff --git a/CSF.Screenplay.WebTestWebsite_old/Views/Home/Index.pt b/CSF.Screenplay.WebTestWebsite_old/Views/Home/Index.pt new file mode 100644 index 00000000..7009f95a --- /dev/null +++ b/CSF.Screenplay.WebTestWebsite_old/Views/Home/Index.pt @@ -0,0 +1,28 @@ + + + +App home page + + + + + + +
Common page header
+
+

Home page

+

+ This is the content for the home page of the testing website. + From here you could also navigate to the second page. +

+

Slow loading link

+

+ A delay of 5 seconds was added to the + load-time of this page. + Reload with a 2-second delay. +

+
+
Common page footer
+ + \ No newline at end of file diff --git a/CSF.Screenplay.WebTestWebsite_old/Views/Lists/Index.pt b/CSF.Screenplay.WebTestWebsite_old/Views/Lists/Index.pt new file mode 100644 index 00000000..3057fdbc --- /dev/null +++ b/CSF.Screenplay.WebTestWebsite_old/Views/Lists/Index.pt @@ -0,0 +1,54 @@ + + + +List testing page + + + + + + +
Common page header
+
+

List testing page

+

+ This page has some HTML select elements. +

+
+
+

List

+
    +
  • One
  • +
  • Two
  • +
  • Three
  • +
  • Four
  • +
  • Five
  • +
+

Single selection

+

+ This selection list outputs its currently selected value. +

+ +

Multi-selection

+

+ This selection list outputs its multiple selected values. +

+ +
+
+ + + +
+
Common page footer
+ + \ No newline at end of file diff --git a/CSF.Screenplay.WebTestWebsite_old/Views/PageThree/Index.pt b/CSF.Screenplay.WebTestWebsite_old/Views/PageThree/Index.pt new file mode 100644 index 00000000..773c7e59 --- /dev/null +++ b/CSF.Screenplay.WebTestWebsite_old/Views/PageThree/Index.pt @@ -0,0 +1,26 @@ + + + +Page three + + + + + + +
Common page header
+
+

Page three

+

This is the content for the third page of the testing website.

+

Delayed appearance

+

If you click this button, then an element will appear shortly afterwards.

+ +
+ +
+ + +
+
Common page footer
+ + \ No newline at end of file diff --git a/CSF.Screenplay.WebTestWebsite_old/Views/PageTwo/Index.pt b/CSF.Screenplay.WebTestWebsite_old/Views/PageTwo/Index.pt new file mode 100644 index 00000000..ae6cb2f5 --- /dev/null +++ b/CSF.Screenplay.WebTestWebsite_old/Views/PageTwo/Index.pt @@ -0,0 +1,46 @@ + + + +Page two + + + + + + +
Common page header
+
+

Page two

+
+
+

This is the content for the second page of the testing website.

+

Important string

+

+ There is an important string on this page which may be read by the testing framework.
+ That string is orange! +

+

Text boxes

+

+ This text box can have data entered, if the correct data is entered then this + dynamic value will change. +

+
+ +
+
+ +
+
+
+

Script output

+

+ The following container shows the output from scripts: +

+
+ + + +
+
Common page footer
+ + \ No newline at end of file diff --git a/CSF.Screenplay.WebTestWebsite_old/Views/ReadElements/Index.pt b/CSF.Screenplay.WebTestWebsite_old/Views/ReadElements/Index.pt new file mode 100644 index 00000000..0cfb78f6 --- /dev/null +++ b/CSF.Screenplay.WebTestWebsite_old/Views/ReadElements/Index.pt @@ -0,0 +1,30 @@ + + + +Input testing page + + + + + + +
Common page header
+
+

Input page

+

+ This page contains content related to testing the reading of elements. +

+

Important string

+

+ There is an important string on this page which may be read by the testing framework.
+ That string is banana! +

+

Important number

+

+ There is an important number on this page as well, which is also read by the testing framework.
+ That number is 42. +

+
+
Common page footer
+ + \ No newline at end of file diff --git a/CSF.Screenplay.WebTestWebsite_old/Views/Shared/Footer.pt b/CSF.Screenplay.WebTestWebsite_old/Views/Shared/Footer.pt new file mode 100644 index 00000000..2d5e7e6f --- /dev/null +++ b/CSF.Screenplay.WebTestWebsite_old/Views/Shared/Footer.pt @@ -0,0 +1,25 @@ + + + +Common footer + + + + + + +
Common page header
+
+ Main page content +
+
+

+ This web application is used to test the CSF.Screenplay.Web project, via + the testing project CSF.Screenplay.Web.Tests. +

+

+ This project must be started-up using a web server prior to running those tests. +

+
+ + \ No newline at end of file diff --git a/CSF.Screenplay.WebTestWebsite_old/Views/Shared/Header.pt b/CSF.Screenplay.WebTestWebsite_old/Views/Shared/Header.pt new file mode 100644 index 00000000..49635eb5 --- /dev/null +++ b/CSF.Screenplay.WebTestWebsite_old/Views/Shared/Header.pt @@ -0,0 +1,19 @@ + + + +Common header + + + + + + +
+

CSF.Screenplay.Web testing site

+
+
+ Main page content +
+
Common page footer
+ + \ No newline at end of file diff --git a/CSF.Screenplay.WebTestWebsite_old/Views/Shared/Page.pt b/CSF.Screenplay.WebTestWebsite_old/Views/Shared/Page.pt new file mode 100644 index 00000000..f0a07aeb --- /dev/null +++ b/CSF.Screenplay.WebTestWebsite_old/Views/Shared/Page.pt @@ -0,0 +1,20 @@ + + + +Page title + + + + + + +
Common page header
+
+ Main page content +
+
Common page footer
+ + \ No newline at end of file diff --git a/CSF.Screenplay.WebTestWebsite_old/Views/StoredScriptTest/Index.pt b/CSF.Screenplay.WebTestWebsite_old/Views/StoredScriptTest/Index.pt new file mode 100644 index 00000000..8cdc101e --- /dev/null +++ b/CSF.Screenplay.WebTestWebsite_old/Views/StoredScriptTest/Index.pt @@ -0,0 +1,27 @@ + + + +JavaScript tests: SampleScript + + + + + + + + + + + +
+ + \ No newline at end of file diff --git a/CSF.Screenplay.WebTestWebsite_old/Views/Web.config b/CSF.Screenplay.WebTestWebsite_old/Views/Web.config new file mode 100644 index 00000000..6869de75 --- /dev/null +++ b/CSF.Screenplay.WebTestWebsite_old/Views/Web.config @@ -0,0 +1,30 @@ + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/CSF.Screenplay.WebTestWebsite_old/packages.config b/CSF.Screenplay.WebTestWebsite_old/packages.config new file mode 100644 index 00000000..048d7f90 --- /dev/null +++ b/CSF.Screenplay.WebTestWebsite_old/packages.config @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/CSF.Screenplay.WebTestWebsite_old/web.config b/CSF.Screenplay.WebTestWebsite_old/web.config new file mode 100644 index 00000000..1e0a44f8 --- /dev/null +++ b/CSF.Screenplay.WebTestWebsite_old/web.config @@ -0,0 +1,67 @@ + + + + + +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/CSF.Screenplay.sln b/CSF.Screenplay.sln index afdb2638..c74faf75 100644 --- a/CSF.Screenplay.sln +++ b/CSF.Screenplay.sln @@ -25,6 +25,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CSF.Screenplay.JsonToHtmlRe EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CSF.Screenplay.JsonToHtmlReport.Template", "CSF.Screenplay.JsonToHtmlReport.Template\CSF.Screenplay.JsonToHtmlReport.Template.proj", "{607631EB-020B-45B3-A558-9B65663D7527}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CSF.Screenplay.Selenium", "CSF.Screenplay.Selenium\CSF.Screenplay.Selenium.csproj", "{11195561-663F-4C72-8316-D650441293A5}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CSF.Screenplay.Selenium.Tests", "Tests\CSF.Screenplay.Selenium.Tests\CSF.Screenplay.Selenium.Tests.csproj", "{AEF2B39D-5129-41DA-AD51-02C8617A344F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CSF.Screenplay.Selenium.TestWebapp", "Tests\CSF.Screenplay.Selenium.TestWebapp\CSF.Screenplay.Selenium.TestWebapp.csproj", "{B5C9CDE0-9EAB-4381-8C79-D3F2ED1A09D2}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -98,10 +104,30 @@ Global {607631EB-020B-45B3-A558-9B65663D7527}.Release|Any CPU.Build.0 = Release|Any CPU {607631EB-020B-45B3-A558-9B65663D7527}.Docs|Any CPU.ActiveCfg = Docs|Any CPU {607631EB-020B-45B3-A558-9B65663D7527}.Docs|Any CPU.Build.0 = Docs|Any CPU + {11195561-663F-4C72-8316-D650441293A5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {11195561-663F-4C72-8316-D650441293A5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {11195561-663F-4C72-8316-D650441293A5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {11195561-663F-4C72-8316-D650441293A5}.Release|Any CPU.Build.0 = Release|Any CPU + {11195561-663F-4C72-8316-D650441293A5}.Docs|Any CPU.ActiveCfg = Debug|Any CPU + {11195561-663F-4C72-8316-D650441293A5}.Docs|Any CPU.Build.0 = Debug|Any CPU + {AEF2B39D-5129-41DA-AD51-02C8617A344F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AEF2B39D-5129-41DA-AD51-02C8617A344F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AEF2B39D-5129-41DA-AD51-02C8617A344F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AEF2B39D-5129-41DA-AD51-02C8617A344F}.Release|Any CPU.Build.0 = Release|Any CPU + {AEF2B39D-5129-41DA-AD51-02C8617A344F}.Docs|Any CPU.ActiveCfg = Debug|Any CPU + {AEF2B39D-5129-41DA-AD51-02C8617A344F}.Docs|Any CPU.Build.0 = Debug|Any CPU + {B5C9CDE0-9EAB-4381-8C79-D3F2ED1A09D2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B5C9CDE0-9EAB-4381-8C79-D3F2ED1A09D2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B5C9CDE0-9EAB-4381-8C79-D3F2ED1A09D2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B5C9CDE0-9EAB-4381-8C79-D3F2ED1A09D2}.Release|Any CPU.Build.0 = Release|Any CPU + {B5C9CDE0-9EAB-4381-8C79-D3F2ED1A09D2}.Docs|Any CPU.ActiveCfg = Debug|Any CPU + {B5C9CDE0-9EAB-4381-8C79-D3F2ED1A09D2}.Docs|Any CPU.Build.0 = Debug|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {7369BCFA-DA98-4C05-9708-7949AF3E485B} = {2C3ABE29-B5F7-404E-B20A-E3679E2ECEC5} {F19D7B76-826F-4470-936E-915F72DC2618} = {2C3ABE29-B5F7-404E-B20A-E3679E2ECEC5} {9D416E8D-EB56-4616-890B-71691FC3582E} = {2C3ABE29-B5F7-404E-B20A-E3679E2ECEC5} + {AEF2B39D-5129-41DA-AD51-02C8617A344F} = {2C3ABE29-B5F7-404E-B20A-E3679E2ECEC5} + {B5C9CDE0-9EAB-4381-8C79-D3F2ED1A09D2} = {2C3ABE29-B5F7-404E-B20A-E3679E2ECEC5} EndGlobalSection EndGlobal diff --git a/CSF.Screenplay/Actors/Cast.cs b/CSF.Screenplay/Actors/Cast.cs index b27be5aa..63da5389 100644 --- a/CSF.Screenplay/Actors/Cast.cs +++ b/CSF.Screenplay/Actors/Cast.cs @@ -1,12 +1,14 @@ using System; using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; using CSF.Screenplay.Performances; using Microsoft.Extensions.DependencyInjection; namespace CSF.Screenplay.Actors { /// The default implementation of which serves as a registry/factory for instances. - public sealed class Cast : ICast + public sealed class Cast : ICast, IDisposable { readonly IRelaysPerformanceEvents performanceEventBus; readonly ConcurrentDictionary actors = new ConcurrentDictionary(StringComparer.InvariantCultureIgnoreCase); @@ -38,6 +40,19 @@ public Actor GetActor(IPersona persona) }); } + /// + public IReadOnlyCollection GetCastList() => actors.Keys.ToList(); + + /// + public void Dispose() + { + foreach(var actor in actors.Values) + { + performanceEventBus.UnsubscribeFrom(actor); + actor.Dispose(); + } + } + /// Initialises a new instance of . /// A service provider /// The identity of the current performance diff --git a/CSF.Screenplay/Performance.cs b/CSF.Screenplay/Performance.cs index e967c369..77fe0567 100644 --- a/CSF.Screenplay/Performance.cs +++ b/CSF.Screenplay/Performance.cs @@ -22,7 +22,7 @@ public sealed class Performance : IPerformance, IEquatable public IServiceProvider ServiceProvider { get; } /// - public List NamingHierarchy { get; } = new List(); + public IReadOnlyList NamingHierarchy { get; } /// public PerformanceState PerformanceState @@ -44,7 +44,7 @@ public void BeginPerformance() { if(hasBegun) throw new InvalidOperationException($"An instance of {nameof(Performance)} may be begun only once; performance instances are not reusable."); hasBegun = true; - performanceEventBus.InvokePerformanceBegun(PerformanceIdentity, NamingHierarchy); + performanceEventBus.InvokePerformanceBegun(this); } /// @@ -54,7 +54,7 @@ public void FinishPerformance(bool? success) if(hasCompleted) throw new InvalidOperationException($"An instance of {nameof(Performance)} may be completed only once; performance instances are not reusable."); hasBegun = hasCompleted = true; this.success = success; - performanceEventBus.InvokePerformanceFinished(PerformanceIdentity, NamingHierarchy, success); + performanceEventBus.InvokePerformanceFinished(this, success); } /// @@ -86,11 +86,11 @@ public void Dispose() /// then a new Guid will be generated as the identity for this performance /// If is public Performance(IServiceProvider serviceProvider, - IList namingHierarchy = default, + IList namingHierarchy, Guid performanceIdentity = default) { ServiceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); - NamingHierarchy = namingHierarchy?.ToList() ?? new List(); + NamingHierarchy = namingHierarchy?.ToList() ?? throw new ArgumentNullException(nameof(namingHierarchy)); PerformanceIdentity = performanceIdentity != Guid.Empty ? performanceIdentity : Guid.NewGuid(); performanceEventBus = serviceProvider.GetRequiredService(); } diff --git a/CSF.Screenplay/Performances/PerformanceEventBus.cs b/CSF.Screenplay/Performances/PerformanceEventBus.cs index 01c36e82..27b7394c 100644 --- a/CSF.Screenplay/Performances/PerformanceEventBus.cs +++ b/CSF.Screenplay/Performances/PerformanceEventBus.cs @@ -31,6 +31,23 @@ public class PerformanceEventBus : IHasPerformanceEvents, IRelaysPerformanceEven { readonly ConcurrentDictionary> subscribedActors = new ConcurrentDictionary>(); + /// + /// Gets a unique identifier for this event bus instance. + /// + /// + /// + /// This property is not used by the Screenplay architecture directly, it is primarily for debugging/development purposes. + /// A common problem/developer-mistake which may occur is accidentally working with more than one Event Bus; this can occur when + /// working with a multiple process model, in which it is possible for more than once instance of a type registered in DI as a + /// 'Singleton' to coexist. + /// + /// + /// This property helps developers identify event bus instances (in a debugger, for example), so that they may recognise occasions + /// in which they are dealing with more than one of them. + /// + /// + public Guid EventBusId { get; } = Guid.NewGuid(); + #region Pub: Screenplay /// @@ -145,6 +162,7 @@ public void UnsubscribeFromAllActors(Guid performanceIdentity) { if (!subscribedActors.TryGetValue(performanceIdentity, out var actorsForPerformance)) return; // Copy the source collection with ToList because UnsubscribeFrom would modify it; modifying a collection whilst enumerating it is bad! + // See the remarks on this method, it's quite normal for there to be no actors remaining at the point where this method is called. foreach (var actor in actorsForPerformance.ToList()) UnsubscribeFrom(actor); } @@ -170,12 +188,12 @@ public void InvokeSpotlightTurnedOff(Guid performanceIdentity) #region Sub: Performances /// - public void InvokePerformanceBegun(Guid performanceIdentity, IList namingHierarchy) - => PerformanceBegun?.Invoke(this, new PerformanceEventArgs(performanceIdentity, namingHierarchy?.ToList() ?? new List())); + public void InvokePerformanceBegun(IPerformance performance) + => PerformanceBegun?.Invoke(this, new PerformanceEventArgs(performance)); /// - public void InvokePerformanceFinished(Guid performanceIdentity, IList namingHierarchy, bool? success) - => PerformanceFinished?.Invoke(this, new PerformanceFinishedEventArgs(performanceIdentity, namingHierarchy?.ToList() ?? new List(), success)); + public void InvokePerformanceFinished(IPerformance performance, bool? success) + => PerformanceFinished?.Invoke(this, new PerformanceFinishedEventArgs(performance, success)); #endregion diff --git a/CSF.Screenplay/Performances/PerformanceFactory.cs b/CSF.Screenplay/Performances/PerformanceFactory.cs deleted file mode 100644 index 08adf258..00000000 --- a/CSF.Screenplay/Performances/PerformanceFactory.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System; - -namespace CSF.Screenplay.Performances -{ - /// A factory service for instances of - public class PerformanceFactory : ICreatesPerformance - { - readonly IServiceProvider services; - - /// - public IPerformance CreatePerformance() => new Performance(services, performanceIdentity: Guid.NewGuid()); - - /// Initialises a new instance of - /// Dependency injection services - /// If is - public PerformanceFactory(IServiceProvider services) - { - this.services = services ?? throw new ArgumentNullException(nameof(services)); - } - } -} \ No newline at end of file diff --git a/CSF.Screenplay/Performances/PerformanceProvider.cs b/CSF.Screenplay/Performances/PerformanceProvider.cs new file mode 100644 index 00000000..0e7b296a --- /dev/null +++ b/CSF.Screenplay/Performances/PerformanceProvider.cs @@ -0,0 +1,46 @@ +using System; + +namespace CSF.Screenplay.Performances +{ + /// + /// A class which provides access to the current , which exists within the + /// current dependency injection scope. + /// + /// + /// + /// This container/provider class is required for architectural reasons. Instances of cannot be created upon their + /// first resolution from dependency injection. This is because performances are immutable and require constructor parameters which are provided + /// by the creating logic. However, performance must also be available instance-per-scope from DI. That is why this class is required, so that + /// a created performance may be 'registered' with this class, with . + /// This class is itself registered in DI as instance-per-scope. So, consuming logic may resolve a performance by getting an instance of this + /// provider and then using . + /// + /// + public class PerformanceProvider + { + IPerformance performance; + + /// + /// Gets the current performance from this provider instance. + /// + /// The current performance + /// If this instance does not have a current performance. + public IPerformance GetCurrentPerformance() + => performance ?? throw new InvalidOperationException($"There must be a current performance; use {nameof(SetCurrentPerformance)} first"); + + /// + /// Sets the current performance for this provider instance. + /// + /// The new current performance + /// If is + /// If the current instance already has a current performance. + public void SetCurrentPerformance(IPerformance currentPerformance) + { + if(currentPerformance == null) throw new ArgumentNullException(nameof(currentPerformance)); + if(performance != null) + throw new InvalidOperationException($"An instance of {nameof(PerformanceProvider)} may only have its current performance set " + + "once. Performance providers are not re-usable."); + performance = currentPerformance; + } + } +} \ No newline at end of file diff --git a/CSF.Screenplay/Reporting/AssetPathProvider.cs b/CSF.Screenplay/Reporting/AssetPathProvider.cs new file mode 100644 index 00000000..579911af --- /dev/null +++ b/CSF.Screenplay/Reporting/AssetPathProvider.cs @@ -0,0 +1,73 @@ +using System; +using System.Globalization; +using System.IO; +using System.Linq; +using CSF.Screenplay.Performances; + +namespace CSF.Screenplay.Reporting +{ + /// + /// Implementation of which filename paths for assets. + /// + /// + /// + /// Paths which are generated by this class are in the format: YYYY-MM-DDTHHMMSSZ_PERFORMANCEID_ASSETNUMBER_BASENAME. + /// The first part of the filename is a timestamp, similar to ISO 8601, except that the : characters separating + /// the hours, minutes and seconds are omitted. The second part is the performance identifier, equal to either the + /// of the last item from , or if the naming + /// hierarchy is empty, the . The third part is a zero-padded asset number, to + /// differentiate between multiple assets generated during the same performance. The final part is the base name of the asset + /// as specified by the consuming logic. + /// + /// + /// The path returned from will be in the same directory as the report file, as returned by + /// . If the report path returned by that service is then this + /// method will also return , meaning that the asset file should not be written. + /// + /// + /// This type is somewhat stateful, because it maintains an internal counter in order to provide the asset numbers (described above). + /// It should be consumed from dependency injection as a scoped service, so that each performance has its own instance of this type. + /// + /// + public class AssetPathProvider : IGetsAssetFilePath + { + readonly IGetsReportPath reportPathProvider; + readonly IPerformance performance; + int assetNumber = 1; + + /// + public string GetAssetFilePath(string baseName) + { + if (string.IsNullOrWhiteSpace(baseName)) + throw new ArgumentException($"'{nameof(baseName)}' cannot be null or whitespace.", nameof(baseName)); + var baseFilename = Path.GetFileName(baseName); + if(string.IsNullOrWhiteSpace(baseFilename)) + throw new ArgumentException($"'{nameof(baseName)}' must indicate a non-whitespace filename, not a directory name.", nameof(baseName)); + + var reportPath = reportPathProvider.GetReportPath(); + if(reportPath == null) return null; + + var performanceId = performance.NamingHierarchy.LastOrDefault()?.Identifier ?? performance.PerformanceIdentity.ToString(); + var sanitisedPerformanceId = RemoveInvalidFilenameChars(performanceId); + var sanitisedBaseFilename = RemoveInvalidFilenameChars(baseFilename); + var filename = $"{GetTimestamp()}_{sanitisedPerformanceId}_{assetNumber++:000}_{sanitisedBaseFilename}"; + return Path.Combine(Path.GetDirectoryName(reportPath), filename); + } + + static string RemoveInvalidFilenameChars(string input) + => Path.GetInvalidFileNameChars().Select(c => c.ToString()).Aggregate(input, (current, c) => current.Replace(c, string.Empty)); + + static string GetTimestamp() => DateTime.UtcNow.ToString("yyyy-MM-ddTHHmmssZ", CultureInfo.InvariantCulture); + + /// + /// Initializes a new instance of the class. + /// + /// The report path provider. + /// The performance. + public AssetPathProvider(IGetsReportPath reportPathProvider, IPerformance performance) + { + this.reportPathProvider = reportPathProvider ?? throw new ArgumentNullException(nameof(reportPathProvider)); + this.performance = performance ?? throw new ArgumentNullException(nameof(performance)); + } + } +} \ No newline at end of file diff --git a/CSF.Screenplay/Reporting/HumanizerFormatter.cs b/CSF.Screenplay/Reporting/HumanizerFormatter.cs index 376762bc..6cd72f01 100644 --- a/CSF.Screenplay/Reporting/HumanizerFormatter.cs +++ b/CSF.Screenplay/Reporting/HumanizerFormatter.cs @@ -115,8 +115,18 @@ static bool TryHumanizeAsEnumNonGeneric(object value, out string huamnized) { huamnized = null; if (value is null) return false; - var genericMethpd = tryHumanizeAsEnumOpenGeneric.MakeGenericMethod(value.GetType()); - var result = (string)genericMethpd.Invoke(null, new object[] { value }); + + MethodInfo genericMethod; + try + { + genericMethod = tryHumanizeAsEnumOpenGeneric.MakeGenericMethod(value.GetType()); + } + catch(ArgumentException) + { + return false; + } + + var result = (string) genericMethod.Invoke(null, new object[] { value }); huamnized = result; return result != null; } diff --git a/CSF.Screenplay/Reporting/ReportPathProvider.cs b/CSF.Screenplay/Reporting/ReportPathProvider.cs new file mode 100644 index 00000000..7d365152 --- /dev/null +++ b/CSF.Screenplay/Reporting/ReportPathProvider.cs @@ -0,0 +1,73 @@ +using System; +using System.IO; + +namespace CSF.Screenplay.Reporting +{ + /// + /// Stateful implementation of which caches the outcome of the path-determination logic. + /// + /// + /// + /// This class uses as its primary source of truth. However, if that value is null/whitespace + /// or if the path is not writable then this class will return from , disabling the reporting + /// functionality. + /// + /// + /// If is a relative path then it is combined with the current working directory to form an + /// absolute path, thus (if does not return null), its return value will always be an absolute path. + /// + /// + /// Because of the caching functionality, this class is stateful and should be used as a singleton. + /// + /// + public class ReportPathProvider : IGetsReportPath + { + readonly ScreenplayOptions screenplayOptions; + readonly ITestsPathForWritePermissions permissionsTester; + + bool hasCachedReportPath; + string cachedReportPath; + + /// + public string GetReportPath() + { + if(!hasCachedReportPath) + { + cachedReportPath = ShouldEnableReporting(out var reportPath) ? reportPath : null; + hasCachedReportPath = true; + } + + return cachedReportPath; + } + + /// + /// Contains the core logic which sanitises and determines whether reporting should be enabled, and if so, what the report path should be. + /// + /// Exposes the final/absolute path to the report file, if this method returns . + /// if reporting should be enabled; if not. + bool ShouldEnableReporting(out string reportPath) + { + if (string.IsNullOrWhiteSpace(screenplayOptions.ReportPath)) + { + reportPath = null; + return false; + } + + reportPath = Path.IsPathRooted(screenplayOptions.ReportPath) + ? screenplayOptions.ReportPath + : Path.Combine(Environment.CurrentDirectory, screenplayOptions.ReportPath); + return permissionsTester.HasWritePermission(reportPath); + } + + /// + /// Initializes a new instance of the class. + /// + /// The screenplay options. + /// The permissions tester. + public ReportPathProvider(ScreenplayOptions screenplayOptions, ITestsPathForWritePermissions permissionsTester) + { + this.screenplayOptions = screenplayOptions ?? throw new System.ArgumentNullException(nameof(screenplayOptions)); + this.permissionsTester = permissionsTester ?? throw new System.ArgumentNullException(nameof(permissionsTester)); + } + } +} \ No newline at end of file diff --git a/CSF.Screenplay/Reporting/ScreenplayReportBuilder.cs b/CSF.Screenplay/Reporting/ScreenplayReportBuilder.cs index ace4d2ab..3bff8710 100644 --- a/CSF.Screenplay/Reporting/ScreenplayReportBuilder.cs +++ b/CSF.Screenplay/Reporting/ScreenplayReportBuilder.cs @@ -87,7 +87,7 @@ public PerformanceReport EndPerformanceAndGetReport(Guid performanceIdentifier, { if(!performanceReports.TryRemove(performanceIdentifier, out var builder)) throw new ArgumentException($"This Screenplay report builder does not contain a performance builder with identifier {performanceIdentifier}", nameof(performanceIdentifier)); - + return builder.GetReport(success); } diff --git a/CSF.Screenplay/ScopeAndPerformance.cs b/CSF.Screenplay/ScopeAndPerformance.cs index 5b3fc29a..cbba84a0 100644 --- a/CSF.Screenplay/ScopeAndPerformance.cs +++ b/CSF.Screenplay/ScopeAndPerformance.cs @@ -19,7 +19,11 @@ public sealed class ScopeAndPerformance : IDisposable public IServiceScope Scope { get; } /// - public void Dispose() => Scope.Dispose(); + public void Dispose() + { + Performance.Dispose(); + Scope.Dispose(); + } /// /// Initialises a new instance of . diff --git a/CSF.Screenplay/Screenplay.cs b/CSF.Screenplay/Screenplay.cs index 5a2d84c7..78dfaff5 100644 --- a/CSF.Screenplay/Screenplay.cs +++ b/CSF.Screenplay/Screenplay.cs @@ -31,7 +31,7 @@ namespace CSF.Screenplay /// use the static method. /// /// - /// The Screenplay object is used to create instances of via the . + /// The Screenplay object is used to create instances of via its . /// You may wish to read a . /// /// @@ -162,7 +162,8 @@ public Screenplay(IServiceProvider serviceProvider) /// An optional action which permits further customization of the service collection that is implicitly created by this method. /// An optional action to configure the which is created by this method. /// A Screenplay instance created from a new service collection. - public static Screenplay Create(Action serviceCollectionCustomisations = null, Action options = null) + public static Screenplay Create(Action serviceCollectionCustomisations = null, + Action options = null) { var services = new ServiceCollection(); services.AddScreenplay(options); diff --git a/CSF.Screenplay/ScreenplayExtensions.cs b/CSF.Screenplay/ScreenplayExtensions.cs index 8c4f1ff8..3df58458 100644 --- a/CSF.Screenplay/ScreenplayExtensions.cs +++ b/CSF.Screenplay/ScreenplayExtensions.cs @@ -172,7 +172,7 @@ public static Task ExecuteAsPerformanceAsync(this Screenplay screenplay, } /// - /// Creates a new within its own newly-created Dependency Injection scope. + /// Creates a new , and a new Dependency Injection scope. /// /// /// @@ -184,22 +184,24 @@ public static Task ExecuteAsPerformanceAsync(this Screenplay screenplay, /// /// /// The Screenplay from which to create the performance - /// An optional collection of identifiers and names providing the hierarchical name of this + /// A collection of identifiers and names providing the hierarchical name of this /// performance; see for more information. + /// An optional identity to use for this performance; corresponding to + /// . If omitted then a new identity will be generated automatically. /// A containing the newly-created performance as well as the newly-started DI scope. /// If is . - public static ScopeAndPerformance CreateScopedPerformance(this Screenplay screenplay, IList namingHierarchy = null) + public static ScopeAndPerformance CreateScopedPerformance(this Screenplay screenplay, + IList namingHierarchy, + Guid identity = default) { if (screenplay is null) throw new ArgumentNullException(nameof(screenplay)); var scope = screenplay.ServiceProvider.CreateScope(); - var performance = scope.ServiceProvider.GetRequiredService(); - if(namingHierarchy != null) - { - performance.NamingHierarchy.Clear(); - performance.NamingHierarchy.AddRange(namingHierarchy); - } + IList resolvedNamingHierarchy = namingHierarchy ?? new List(); + var performance = new Performance(scope.ServiceProvider, resolvedNamingHierarchy, identity); + var container = scope.ServiceProvider.GetRequiredService(); + container.SetCurrentPerformance(performance); return new ScopeAndPerformance(performance, scope); } diff --git a/CSF.Screenplay/ScreenplayOptions.cs b/CSF.Screenplay/ScreenplayOptions.cs index fc117549..69c6cfe5 100644 --- a/CSF.Screenplay/ScreenplayOptions.cs +++ b/CSF.Screenplay/ScreenplayOptions.cs @@ -79,6 +79,9 @@ public sealed class ScreenplayOptions /// If this property is set to , or an empty/whitespace-only string, or if the path is not writable, then the reporting functionality /// will be disabled and no report will be written. /// + /// + /// At runtime, do not read this value directly; instead use an implementation of service to get the report path. + /// /// public string ReportPath { get; set; } = $"ScreenplayReport_{DateTime.UtcNow.ToString("yyyy-MM-ddTHHmmssZ", CultureInfo.InvariantCulture)}.json"; diff --git a/CSF.Screenplay/ScreenplayServiceCollectionExtensions.cs b/CSF.Screenplay/ScreenplayServiceCollectionExtensions.cs index 300b3929..2ba67f0d 100644 --- a/CSF.Screenplay/ScreenplayServiceCollectionExtensions.cs +++ b/CSF.Screenplay/ScreenplayServiceCollectionExtensions.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using CSF.Screenplay.Abilities; using CSF.Screenplay.Actors; using CSF.Screenplay.Performances; using CSF.Screenplay.Reporting; @@ -50,10 +51,11 @@ public static IServiceCollection AddScreenplay(this IServiceCollection services, services.AddSingleton(optionsModel); services.AddSingleton(s => s.GetRequiredService().ValueFormatters); services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(s => { - if(!ShouldEnableReporting(s.GetRequiredService(), out var reportPath)) - return new NoOpReporter(); + var reportPath = s.GetRequiredService().GetReportPath(); + if(reportPath is null) return new NoOpReporter(); var stream = File.Create(reportPath); return ActivatorUtilities.CreateInstance(s, stream); @@ -61,38 +63,27 @@ public static IServiceCollection AddScreenplay(this IServiceCollection services, services.AddScoped(s => new Cast(s, s.GetRequiredService().PerformanceIdentity)); services.AddScoped(); - services.AddScoped(s => s.GetRequiredService().CreatePerformance()); + services.AddScoped(); + services.AddScoped(s => s.GetRequiredService().GetCurrentPerformance()); + services.AddScoped(); - services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); services.AddTransient, PerformanceReportBuilder>>(s => { return idsAndNames => ActivatorUtilities.CreateInstance(s, idsAndNames); }); + services.AddTransient(); foreach(var type in optionsModel.ValueFormatters) services.AddTransient(type); return services; } - static bool ShouldEnableReporting(ScreenplayOptions optionsModel, out string reportPath) - { - if (string.IsNullOrWhiteSpace(optionsModel.ReportPath)) - { - reportPath = null; - return false; - } - - reportPath = Path.IsPathRooted(optionsModel.ReportPath) - ? optionsModel.ReportPath - : Path.Combine(Environment.CurrentDirectory, optionsModel.ReportPath); - var permissionsTester = new WritePermissionTester(); - return permissionsTester.HasWritePermission(reportPath); - } } } \ No newline at end of file diff --git a/Tests/CSF.Screenplay.Selenium.TestWebapp/CSF.Screenplay.Selenium.TestWebapp.csproj b/Tests/CSF.Screenplay.Selenium.TestWebapp/CSF.Screenplay.Selenium.TestWebapp.csproj new file mode 100644 index 00000000..1b28a01c --- /dev/null +++ b/Tests/CSF.Screenplay.Selenium.TestWebapp/CSF.Screenplay.Selenium.TestWebapp.csproj @@ -0,0 +1,9 @@ + + + + net8.0 + enable + enable + + + diff --git a/Tests/CSF.Screenplay.Selenium.TestWebapp/Program.cs b/Tests/CSF.Screenplay.Selenium.TestWebapp/Program.cs new file mode 100644 index 00000000..a5ec39e5 --- /dev/null +++ b/Tests/CSF.Screenplay.Selenium.TestWebapp/Program.cs @@ -0,0 +1,5 @@ +var builder = WebApplication.CreateBuilder(args); + +var app = builder.Build(); +app.UseStaticFiles(); +app.Run(); diff --git a/Tests/CSF.Screenplay.Selenium.TestWebapp/Properties/launchSettings.json b/Tests/CSF.Screenplay.Selenium.TestWebapp/Properties/launchSettings.json new file mode 100644 index 00000000..fa4478c2 --- /dev/null +++ b/Tests/CSF.Screenplay.Selenium.TestWebapp/Properties/launchSettings.json @@ -0,0 +1,38 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:22432", + "sslPort": 44346 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:5102", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:7134;http://localhost:5102", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/Tests/CSF.Screenplay.Selenium.TestWebapp/appsettings.Development.json b/Tests/CSF.Screenplay.Selenium.TestWebapp/appsettings.Development.json new file mode 100644 index 00000000..770d3e93 --- /dev/null +++ b/Tests/CSF.Screenplay.Selenium.TestWebapp/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "DetailedErrors": true, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/Tests/CSF.Screenplay.Selenium.TestWebapp/appsettings.json b/Tests/CSF.Screenplay.Selenium.TestWebapp/appsettings.json new file mode 100644 index 00000000..10f68b8c --- /dev/null +++ b/Tests/CSF.Screenplay.Selenium.TestWebapp/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/Tests/CSF.Screenplay.Selenium.TestWebapp/wwwroot/ChildPath/OpenUrlTests.html b/Tests/CSF.Screenplay.Selenium.TestWebapp/wwwroot/ChildPath/OpenUrlTests.html new file mode 100644 index 00000000..4e53f707 --- /dev/null +++ b/Tests/CSF.Screenplay.Selenium.TestWebapp/wwwroot/ChildPath/OpenUrlTests.html @@ -0,0 +1,13 @@ + + + + +

Open URL with base path tests

+

+ This page is hosted at a child path of the root of the app. + It has the same page name but a different path, with slightly different content. + Reading this proves that the base URL ability has been used. +

+

This is content at the deeper path.

+ + \ No newline at end of file diff --git a/Tests/CSF.Screenplay.Selenium.TestWebapp/wwwroot/ClearCookiesTests.html b/Tests/CSF.Screenplay.Selenium.TestWebapp/wwwroot/ClearCookiesTests.html new file mode 100644 index 00000000..7107c8c9 --- /dev/null +++ b/Tests/CSF.Screenplay.Selenium.TestWebapp/wwwroot/ClearCookiesTests.html @@ -0,0 +1,29 @@ + + + + +

Clear Cookies Tests

+

+ Click the button below to list the cookies. + The onload event sets two cookies up; if the clear cookies test is working correctly then these cookies will be cleared, + so clicking the button will reveal an empty list. +

+ +
+ + + \ No newline at end of file diff --git a/Tests/CSF.Screenplay.Selenium.TestWebapp/wwwroot/ClearLocalStorageTests.html b/Tests/CSF.Screenplay.Selenium.TestWebapp/wwwroot/ClearLocalStorageTests.html new file mode 100644 index 00000000..26890f4b --- /dev/null +++ b/Tests/CSF.Screenplay.Selenium.TestWebapp/wwwroot/ClearLocalStorageTests.html @@ -0,0 +1,29 @@ + + + + +

Clear Local Storage Tests

+

+ Click the button below to list the storage. + The onload event sets two items up in storage; if the clear storage test is working correctly then these items will be cleared, + so clicking the button will reveal an empty list. +

+ +
+ + + \ No newline at end of file diff --git a/Tests/CSF.Screenplay.Selenium.TestWebapp/wwwroot/ClearTheContentsTests.html b/Tests/CSF.Screenplay.Selenium.TestWebapp/wwwroot/ClearTheContentsTests.html new file mode 100644 index 00000000..bf0fa749 --- /dev/null +++ b/Tests/CSF.Screenplay.Selenium.TestWebapp/wwwroot/ClearTheContentsTests.html @@ -0,0 +1,9 @@ + + + + +

Clear The Contents Tests

+ + + + \ No newline at end of file diff --git a/Tests/CSF.Screenplay.Selenium.TestWebapp/wwwroot/ClickTests.html b/Tests/CSF.Screenplay.Selenium.TestWebapp/wwwroot/ClickTests.html new file mode 100644 index 00000000..73a18605 --- /dev/null +++ b/Tests/CSF.Screenplay.Selenium.TestWebapp/wwwroot/ClickTests.html @@ -0,0 +1,20 @@ + + + + +

Click Tests

+

+ Click the button below to trigger an event which will alter the displayed text. +

+ +
+ + + \ No newline at end of file diff --git a/Tests/CSF.Screenplay.Selenium.TestWebapp/wwwroot/DeleteTheCookieTests.html b/Tests/CSF.Screenplay.Selenium.TestWebapp/wwwroot/DeleteTheCookieTests.html new file mode 100644 index 00000000..ed271074 --- /dev/null +++ b/Tests/CSF.Screenplay.Selenium.TestWebapp/wwwroot/DeleteTheCookieTests.html @@ -0,0 +1,29 @@ + + + + +

Delete The Cookie Tests

+

+ Click the button below to list the cookies. + The onload event sets two cookies up; if the delete cookie test is working correctly then it's + possible to delete either of these independently, so clicking the button will reveal reduced list. +

+ +
+ + + \ No newline at end of file diff --git a/Tests/CSF.Screenplay.Selenium.TestWebapp/wwwroot/DeselectionTests.html b/Tests/CSF.Screenplay.Selenium.TestWebapp/wwwroot/DeselectionTests.html new file mode 100644 index 00000000..f9a82b6d --- /dev/null +++ b/Tests/CSF.Screenplay.Selenium.TestWebapp/wwwroot/DeselectionTests.html @@ -0,0 +1,37 @@ + + + + +

Deselection Tests

+

+ The <select> element permits multiple selection. The display element responds to changes in the + selection and lists the items which are selected. +

+ +
    + + + \ No newline at end of file diff --git a/Tests/CSF.Screenplay.Selenium.TestWebapp/wwwroot/LocatorTests.html b/Tests/CSF.Screenplay.Selenium.TestWebapp/wwwroot/LocatorTests.html new file mode 100644 index 00000000..79e9a075 --- /dev/null +++ b/Tests/CSF.Screenplay.Selenium.TestWebapp/wwwroot/LocatorTests.html @@ -0,0 +1,24 @@ + + + + +

    Locator Tests

    +

    + This page has a number of elements, which may be selected via various Selenium-based Locators. +

    +

    This is a paragraph of test which is selectable by the element id theParagraph.

    +
    This is a div which is selectable by the class attribute theDiv.
    +

    Below is a div element with its class attribute set to div. It contains a number of child div elements, each with an input element.

    +
    +
    +
    +
    +
    +
      +
    • First text inside a span
    • +
    • This has some text but not <span> element
    • +
    • Second text inside a span
    • +
    • This has some text but not <span> element
    • +
    + + \ No newline at end of file diff --git a/Tests/CSF.Screenplay.Selenium.TestWebapp/wwwroot/OpenUrlTests.html b/Tests/CSF.Screenplay.Selenium.TestWebapp/wwwroot/OpenUrlTests.html new file mode 100644 index 00000000..efccfc49 --- /dev/null +++ b/Tests/CSF.Screenplay.Selenium.TestWebapp/wwwroot/OpenUrlTests.html @@ -0,0 +1,13 @@ + + + Open a URL test page + + +

    Open URL tests

    +

    + This page has some content which can be read by using an OpenUrl action. + It's also reused by some other tests which just need 'any web page', such as those which take a screenshot. +

    +

    This is the page content.

    + + \ No newline at end of file diff --git a/Tests/CSF.Screenplay.Selenium.TestWebapp/wwwroot/QueriesTests.html b/Tests/CSF.Screenplay.Selenium.TestWebapp/wwwroot/QueriesTests.html new file mode 100644 index 00000000..e6efecc0 --- /dev/null +++ b/Tests/CSF.Screenplay.Selenium.TestWebapp/wwwroot/QueriesTests.html @@ -0,0 +1,57 @@ + + + + + +

    Queries tests

    +

    + This page has a number of elements which may be interrogated by queries. +

    +

    This is an HTML input element with type="password":

    +

    + Here are two HTML button elements; the second is disabled: , + +

    +
    + This div has a background color set. +
    +
    + This div is positioned at (700, 120). +
    +
    + This div has a size of 400px by 150px. +
    +
    + This div is invisible! +
    +

    + This multi-select element has various options which are selected and unselected: + +

    + + \ No newline at end of file diff --git a/Tests/CSF.Screenplay.Selenium.TestWebapp/wwwroot/SelectionTests.html b/Tests/CSF.Screenplay.Selenium.TestWebapp/wwwroot/SelectionTests.html new file mode 100644 index 00000000..060f1c2a --- /dev/null +++ b/Tests/CSF.Screenplay.Selenium.TestWebapp/wwwroot/SelectionTests.html @@ -0,0 +1,37 @@ + + + + +

    Selection Tests

    +

    + The <select> element permits multiple selection. The display element responds to changes in the + selection and lists the items which are selected. +

    + +
      + + + \ No newline at end of file diff --git a/Tests/CSF.Screenplay.Selenium.TestWebapp/wwwroot/SendKeysTests.html b/Tests/CSF.Screenplay.Selenium.TestWebapp/wwwroot/SendKeysTests.html new file mode 100644 index 00000000..b98d5be9 --- /dev/null +++ b/Tests/CSF.Screenplay.Selenium.TestWebapp/wwwroot/SendKeysTests.html @@ -0,0 +1,21 @@ + + + + +

      Send Keys Tests

      +

      + Typing into the input box will immediately update the value of the display area, below. +

      + +
      + + + \ No newline at end of file diff --git a/Tests/CSF.Screenplay.Selenium.TestWebapp/wwwroot/WaitTests.html b/Tests/CSF.Screenplay.Selenium.TestWebapp/wwwroot/WaitTests.html new file mode 100644 index 00000000..6793306a --- /dev/null +++ b/Tests/CSF.Screenplay.Selenium.TestWebapp/wwwroot/WaitTests.html @@ -0,0 +1,28 @@ + + + + +

      Wait Tests

      +

      + Click the button below to trigger a delayed event, which will alter the text of the displayed element. + The length of the delay is determined by the value of the input box (in milliseconds). +

      + + +
      + + + \ No newline at end of file diff --git a/Tests/CSF.Screenplay.Selenium.TestWebapp/wwwroot/index.html b/Tests/CSF.Screenplay.Selenium.TestWebapp/wwwroot/index.html new file mode 100644 index 00000000..26eb59a1 --- /dev/null +++ b/Tests/CSF.Screenplay.Selenium.TestWebapp/wwwroot/index.html @@ -0,0 +1,12 @@ + + + Selenium testing webapp + + +

      Selenium testing webapp

      +

      + This web app hosts static pages which allow the testing of CSF.Screenplay.Selenium. + Each web page is named after the test that it supports. +

      + + \ No newline at end of file diff --git a/Tests/CSF.Screenplay.Selenium.Tests/Actions/ClearCookiesTests.cs b/Tests/CSF.Screenplay.Selenium.Tests/Actions/ClearCookiesTests.cs new file mode 100644 index 00000000..282240e4 --- /dev/null +++ b/Tests/CSF.Screenplay.Selenium.Tests/Actions/ClearCookiesTests.cs @@ -0,0 +1,29 @@ + +using CSF.Screenplay.Selenium.Elements; +using static CSF.Screenplay.PerformanceStarter; +using static CSF.Screenplay.Selenium.PerformableBuilder; + +namespace CSF.Screenplay.Selenium.Actions; + +[TestFixture] +public class ClearCookiesTests +{ + static readonly ITarget + listCookiesButton = new ElementId("getCookies", "the cookies-listing button"), + cookieList = new ElementId("cookiesList", "the list of cookies"); + + static readonly NamedUri testPage = new NamedUri("ClearCookiesTests.html", "the test page"); + + [Test, Screenplay] + public async Task ClearCookiesShouldClearCookies(IStage stage) + { + var webster = stage.Spotlight(); + + await Given(webster).WasAbleTo(OpenTheUrl(testPage)); + await When(webster).AttemptsTo(ClearAllDomainCookies()); + await Then(webster).Should(ClickOn(listCookiesButton)); + var cookies = await Then(webster).Should(ReadFromTheElement(cookieList).TheText()); + + Assert.That(cookies, Is.Empty); + } +} \ No newline at end of file diff --git a/Tests/CSF.Screenplay.Selenium.Tests/Actions/ClearLocalStorageTests.cs b/Tests/CSF.Screenplay.Selenium.Tests/Actions/ClearLocalStorageTests.cs new file mode 100644 index 00000000..ea2246f7 --- /dev/null +++ b/Tests/CSF.Screenplay.Selenium.Tests/Actions/ClearLocalStorageTests.cs @@ -0,0 +1,29 @@ + +using CSF.Screenplay.Selenium.Elements; +using static CSF.Screenplay.PerformanceStarter; +using static CSF.Screenplay.Selenium.PerformableBuilder; + +namespace CSF.Screenplay.Selenium.Actions; + +[TestFixture] +public class ClearLocalStorageTests +{ + static readonly ITarget + listItemsButton = new ElementId("getItems", "the items-listing button"), + itemList = new ElementId("itemsList", "the list of storage items"); + + static readonly NamedUri testPage = new NamedUri("ClearLocalStorageTests.html", "the test page"); + + [Test, Screenplay, Ignore("Ignored due to #237, reinstate when that is done")] + public async Task ClearLocalStorageShouldClearItems(IStage stage) + { + var webster = stage.Spotlight(); + + await Given(webster).WasAbleTo(OpenTheUrl(testPage)); + await When(webster).AttemptsTo(ClearLocalStorage()); + await Then(webster).Should(ClickOn(listItemsButton)); + var items = await Then(webster).Should(ReadFromTheElement(itemList).TheText()); + + Assert.That(items, Is.Empty); + } +} \ No newline at end of file diff --git a/Tests/CSF.Screenplay.Selenium.Tests/Actions/ClearTheContentsTests.cs b/Tests/CSF.Screenplay.Selenium.Tests/Actions/ClearTheContentsTests.cs new file mode 100644 index 00000000..c7ae7092 --- /dev/null +++ b/Tests/CSF.Screenplay.Selenium.Tests/Actions/ClearTheContentsTests.cs @@ -0,0 +1,40 @@ + +using CSF.Screenplay.Selenium.Elements; +using static CSF.Screenplay.PerformanceStarter; +using static CSF.Screenplay.Selenium.PerformableBuilder; + +namespace CSF.Screenplay.Selenium.Actions; + +[TestFixture] +public class ClearTheContentsTests +{ + static readonly ITarget + aTextArea = new ElementId("aTextArea", "the textarea"), + anInput = new ElementId("aTextInput", "the text input"); + + static readonly NamedUri testPage = new NamedUri("ClearTheContentsTests.html", "the test page"); + + [Test, Screenplay] + public async Task ClearATextareaShouldEmptyIt(IStage stage) + { + var webster = stage.Spotlight(); + + await Given(webster).WasAbleTo(OpenTheUrl(testPage)); + await When(webster).AttemptsTo(ClearTheContentsOf(aTextArea)); + var contents = await Then(webster).Should(ReadFromTheElement(aTextArea).TheValue()); + + Assert.That(contents, Is.Empty); + } + + [Test, Screenplay] + public async Task ClearATextInputShouldEmptyIt(IStage stage) + { + var webster = stage.Spotlight(); + + await Given(webster).WasAbleTo(OpenTheUrl(testPage)); + await When(webster).AttemptsTo(ClearTheContentsOf(anInput)); + var contents = await Then(webster).Should(ReadFromTheElement(anInput).TheValue()); + + Assert.That(contents, Is.Empty); + } +} \ No newline at end of file diff --git a/Tests/CSF.Screenplay.Selenium.Tests/Actions/ClickTests.cs b/Tests/CSF.Screenplay.Selenium.Tests/Actions/ClickTests.cs new file mode 100644 index 00000000..4c3f2aab --- /dev/null +++ b/Tests/CSF.Screenplay.Selenium.Tests/Actions/ClickTests.cs @@ -0,0 +1,28 @@ + +using CSF.Screenplay.Selenium.Elements; +using static CSF.Screenplay.PerformanceStarter; +using static CSF.Screenplay.Selenium.PerformableBuilder; + +namespace CSF.Screenplay.Selenium.Actions; + +[TestFixture] +public class ClickTests +{ + static readonly ITarget + clickableButton = new ElementId("clickable", "the clickable button"), + displayText = new ElementId("display", "the displayable text"); + + static readonly NamedUri testPage = new NamedUri("ClickTests.html", "the test page"); + + [Test, Screenplay] + public async Task ClickingAButtonShouldTriggerAnEvent(IStage stage) + { + var webster = stage.Spotlight(); + + await Given(webster).WasAbleTo(OpenTheUrl(testPage)); + await When(webster).AttemptsTo(ClickOn(clickableButton)); + var contents = await Then(webster).Should(ReadFromTheElement(displayText).TheText()); + + Assert.That(contents, Is.EqualTo("Clicked!")); + } +} \ No newline at end of file diff --git a/Tests/CSF.Screenplay.Selenium.Tests/Actions/DeleteTheCookieTests.cs b/Tests/CSF.Screenplay.Selenium.Tests/Actions/DeleteTheCookieTests.cs new file mode 100644 index 00000000..e98bc464 --- /dev/null +++ b/Tests/CSF.Screenplay.Selenium.Tests/Actions/DeleteTheCookieTests.cs @@ -0,0 +1,29 @@ + +using CSF.Screenplay.Selenium.Elements; +using static CSF.Screenplay.PerformanceStarter; +using static CSF.Screenplay.Selenium.PerformableBuilder; + +namespace CSF.Screenplay.Selenium.Actions; + +[TestFixture] +public class DeleteTheCookieTests +{ + static readonly ITarget + listCookiesButton = new ElementId("getCookies", "the cookies-listing button"), + cookieList = new ElementId("cookiesList", "the list of cookies"); + + static readonly NamedUri testPage = new NamedUri("DeleteTheCookieTests.html", "the test page"); + + [Test, Screenplay] + public async Task DeleteTheCookieNamedShouldLeaveOneCookie(IStage stage) + { + var webster = stage.Spotlight(); + + await Given(webster).WasAbleTo(OpenTheUrl(testPage)); + await When(webster).AttemptsTo(DeleteTheCookieNamed("sampleCookie")); + await Then(webster).Should(ClickOn(listCookiesButton)); + var cookies = await Then(webster).Should(ReadFromTheElement(cookieList).TheText()); + + Assert.That(cookies, Is.EqualTo("otherCookie=otherValue")); + } +} \ No newline at end of file diff --git a/Tests/CSF.Screenplay.Selenium.Tests/Actions/DeselectAllTests.cs b/Tests/CSF.Screenplay.Selenium.Tests/Actions/DeselectAllTests.cs new file mode 100644 index 00000000..347b0954 --- /dev/null +++ b/Tests/CSF.Screenplay.Selenium.Tests/Actions/DeselectAllTests.cs @@ -0,0 +1,28 @@ + +using CSF.Screenplay.Selenium.Elements; +using static CSF.Screenplay.PerformanceStarter; +using static CSF.Screenplay.Selenium.PerformableBuilder; + +namespace CSF.Screenplay.Selenium.Actions; + +[TestFixture] +public class DeselectAllTests +{ + static readonly ITarget + selectElement = new ElementId("selectElement", "the select element"), + displayText = new ElementId("display", "the displayable text"); + + static readonly NamedUri testPage = new NamedUri("DeselectionTests.html", "the test page"); + + [Test, Screenplay] + public async Task DeselectEverythingFromShouldClearTheSelection(IStage stage) + { + var webster = stage.Spotlight(); + + await Given(webster).WasAbleTo(OpenTheUrl(testPage)); + await When(webster).AttemptsTo(DeselectEverythingFrom(selectElement)); + var contents = await Then(webster).Should(ReadFromTheElement(displayText).TheText()); + + Assert.That(contents, Is.Empty); + } +} \ No newline at end of file diff --git a/Tests/CSF.Screenplay.Selenium.Tests/Actions/DeselectByIndexTests.cs b/Tests/CSF.Screenplay.Selenium.Tests/Actions/DeselectByIndexTests.cs new file mode 100644 index 00000000..c395de28 --- /dev/null +++ b/Tests/CSF.Screenplay.Selenium.Tests/Actions/DeselectByIndexTests.cs @@ -0,0 +1,28 @@ + +using CSF.Screenplay.Selenium.Elements; +using static CSF.Screenplay.PerformanceStarter; +using static CSF.Screenplay.Selenium.PerformableBuilder; + +namespace CSF.Screenplay.Selenium.Actions; + +[TestFixture] +public class DeselectByIndexTests +{ + static readonly ITarget + selectElement = new ElementId("selectElement", "the select element"), + displayText = new ElementId("display", "the displayable text"); + + static readonly NamedUri testPage = new NamedUri("DeselectionTests.html", "the test page"); + + [Test, Screenplay] + public async Task DeselectTheOptionFromShouldLeaveOneSelectedItem(IStage stage) + { + var webster = stage.Spotlight(); + + await Given(webster).WasAbleTo(OpenTheUrl(testPage)); + await When(webster).AttemptsTo(DeselectTheOption(0).From(selectElement)); + var contents = await Then(webster).Should(ReadFromTheElement(displayText).TheText()); + + Assert.That(contents, Is.EqualTo("Third")); + } +} diff --git a/Tests/CSF.Screenplay.Selenium.Tests/Actions/DeselectByTextTests.cs b/Tests/CSF.Screenplay.Selenium.Tests/Actions/DeselectByTextTests.cs new file mode 100644 index 00000000..9e136906 --- /dev/null +++ b/Tests/CSF.Screenplay.Selenium.Tests/Actions/DeselectByTextTests.cs @@ -0,0 +1,28 @@ + +using CSF.Screenplay.Selenium.Elements; +using static CSF.Screenplay.PerformanceStarter; +using static CSF.Screenplay.Selenium.PerformableBuilder; + +namespace CSF.Screenplay.Selenium.Actions; + +[TestFixture] +public class DeselectByTextTests +{ + static readonly ITarget + selectElement = new ElementId("selectElement", "the select element"), + displayText = new ElementId("display", "the displayable text"); + + static readonly NamedUri testPage = new NamedUri("DeselectionTests.html", "the test page"); + + [Test, Screenplay] + public async Task DeselectTheOptionFromShouldLeaveOneSelectedItem(IStage stage) + { + var webster = stage.Spotlight(); + + await Given(webster).WasAbleTo(OpenTheUrl(testPage)); + await When(webster).AttemptsTo(DeselectTheOption("Third").From(selectElement)); + var contents = await Then(webster).Should(ReadFromTheElement(displayText).TheText()); + + Assert.That(contents, Is.EqualTo("First")); + } +} diff --git a/Tests/CSF.Screenplay.Selenium.Tests/Actions/DeselectByValueTests.cs b/Tests/CSF.Screenplay.Selenium.Tests/Actions/DeselectByValueTests.cs new file mode 100644 index 00000000..01392935 --- /dev/null +++ b/Tests/CSF.Screenplay.Selenium.Tests/Actions/DeselectByValueTests.cs @@ -0,0 +1,28 @@ + +using CSF.Screenplay.Selenium.Elements; +using static CSF.Screenplay.PerformanceStarter; +using static CSF.Screenplay.Selenium.PerformableBuilder; + +namespace CSF.Screenplay.Selenium.Actions; + +[TestFixture] +public class DeselectByValueTests +{ + static readonly ITarget + selectElement = new ElementId("selectElement", "the select element"), + displayText = new ElementId("display", "the displayable text"); + + static readonly NamedUri testPage = new NamedUri("DeselectionTests.html", "the test page"); + + [Test, Screenplay] + public async Task DeselectTheOptionWithValueFromShouldLeaveOneSelectedItem(IStage stage) + { + var webster = stage.Spotlight(); + + await Given(webster).WasAbleTo(OpenTheUrl(testPage)); + await When(webster).AttemptsTo(DeselectTheOptionWithValue("Three").From(selectElement)); + var contents = await Then(webster).Should(ReadFromTheElement(displayText).TheText()); + + Assert.That(contents, Is.EqualTo("First")); + } +} \ No newline at end of file diff --git a/Tests/CSF.Screenplay.Selenium.Tests/Actions/OpenUrlTests.cs b/Tests/CSF.Screenplay.Selenium.Tests/Actions/OpenUrlTests.cs new file mode 100644 index 00000000..332d5e71 --- /dev/null +++ b/Tests/CSF.Screenplay.Selenium.Tests/Actions/OpenUrlTests.cs @@ -0,0 +1,37 @@ + +using CSF.Screenplay.Selenium.Elements; +using static CSF.Screenplay.PerformanceStarter; +using static CSF.Screenplay.Selenium.PerformableBuilder; + +namespace CSF.Screenplay.Selenium.Actions; + +[TestFixture] +public class OpenUrlTests +{ + static readonly ITarget + textContent = new ElementId("textContent", "the displayable text"); + + static readonly NamedUri testPage = new NamedUri("OpenUrlTests.html", "the test page"); + + [Test, Screenplay] + public async Task OpenTheUrlShouldShouldYieldExpectedContent(IStage stage) + { + var webster = stage.Spotlight(); + + await Given(webster).WasAbleTo(OpenTheUrl(testPage)); + var contents = await Then(webster).Should(ReadFromTheElement(textContent).TheText()); + + Assert.That(contents, Is.EqualTo("This is the page content.")); + } + + [Test, Screenplay] + public async Task OpenTheUrlWithDifferentBasePathShouldYieldDifferentContent(IStage stage) + { + var pattie = stage.Spotlight(); + + await Given(pattie).WasAbleTo(OpenTheUrl(testPage)); + var contents = await Then(pattie).Should(ReadFromTheElement(textContent).TheText()); + + Assert.That(contents, Is.EqualTo("This is content at the deeper path.")); + } +} \ No newline at end of file diff --git a/Tests/CSF.Screenplay.Selenium.Tests/Actions/SelectByIndexTests.cs b/Tests/CSF.Screenplay.Selenium.Tests/Actions/SelectByIndexTests.cs new file mode 100644 index 00000000..20261bff --- /dev/null +++ b/Tests/CSF.Screenplay.Selenium.Tests/Actions/SelectByIndexTests.cs @@ -0,0 +1,29 @@ + +using System; +using CSF.Screenplay.Selenium.Elements; +using static CSF.Screenplay.PerformanceStarter; +using static CSF.Screenplay.Selenium.PerformableBuilder; + +namespace CSF.Screenplay.Selenium.Actions; + +[TestFixture] +public class SelectByIndexTests +{ + static readonly ITarget + selectElement = new ElementId("selectElement", "the select element"), + displayText = new ElementId("display", "the displayable text"); + + static readonly NamedUri testPage = new NamedUri("SelectionTests.html", "the test page"); + + [Test, Screenplay] + public async Task SelectTheOptionFromShouldAddOneSelectedItem(IStage stage) + { + var webster = stage.Spotlight(); + + await Given(webster).WasAbleTo(OpenTheUrl(testPage)); + await When(webster).AttemptsTo(SelectTheOption(0).From(selectElement)); + var contents = await Then(webster).Should(ReadFromTheElement(displayText).TheText()); + + Assert.That(contents, Is.EqualTo($"First{Environment.NewLine}Third")); + } +} diff --git a/Tests/CSF.Screenplay.Selenium.Tests/Actions/SelectByTextTests.cs b/Tests/CSF.Screenplay.Selenium.Tests/Actions/SelectByTextTests.cs new file mode 100644 index 00000000..459bd6a5 --- /dev/null +++ b/Tests/CSF.Screenplay.Selenium.Tests/Actions/SelectByTextTests.cs @@ -0,0 +1,29 @@ + +using System; +using CSF.Screenplay.Selenium.Elements; +using static CSF.Screenplay.PerformanceStarter; +using static CSF.Screenplay.Selenium.PerformableBuilder; + +namespace CSF.Screenplay.Selenium.Actions; + +[TestFixture] +public class SelectByTextTests +{ + static readonly ITarget + selectElement = new ElementId("selectElement", "the select element"), + displayText = new ElementId("display", "the displayable text"); + + static readonly NamedUri testPage = new NamedUri("SelectionTests.html", "the test page"); + + [Test, Screenplay] + public async Task SelectTheOptionFromShouldAddOneSelectedItem(IStage stage) + { + var webster = stage.Spotlight(); + + await Given(webster).WasAbleTo(OpenTheUrl(testPage)); + await When(webster).AttemptsTo(SelectTheOption("Second").From(selectElement)); + var contents = await Then(webster).Should(ReadFromTheElement(displayText).TheText()); + + Assert.That(contents, Is.EqualTo($"Second{Environment.NewLine}Third")); + } +} diff --git a/Tests/CSF.Screenplay.Selenium.Tests/Actions/SelectByValueTests.cs b/Tests/CSF.Screenplay.Selenium.Tests/Actions/SelectByValueTests.cs new file mode 100644 index 00000000..fe7dbd96 --- /dev/null +++ b/Tests/CSF.Screenplay.Selenium.Tests/Actions/SelectByValueTests.cs @@ -0,0 +1,29 @@ + +using System; +using CSF.Screenplay.Selenium.Elements; +using static CSF.Screenplay.PerformanceStarter; +using static CSF.Screenplay.Selenium.PerformableBuilder; + +namespace CSF.Screenplay.Selenium.Actions; + +[TestFixture] +public class SelectByValueTests +{ + static readonly ITarget + selectElement = new ElementId("selectElement", "the select element"), + displayText = new ElementId("display", "the displayable text"); + + static readonly NamedUri testPage = new NamedUri("SelectionTests.html", "the test page"); + + [Test, Screenplay] + public async Task SelectTheOptionFromShouldAddOneSelectedItem(IStage stage) + { + var webster = stage.Spotlight(); + + await Given(webster).WasAbleTo(OpenTheUrl(testPage)); + await When(webster).AttemptsTo(SelectTheOptionWithValue("Two").From(selectElement)); + var contents = await Then(webster).Should(ReadFromTheElement(displayText).TheText()); + + Assert.That(contents, Is.EqualTo($"Second{Environment.NewLine}Third")); + } +} diff --git a/Tests/CSF.Screenplay.Selenium.Tests/Actions/SendKeysTests.cs b/Tests/CSF.Screenplay.Selenium.Tests/Actions/SendKeysTests.cs new file mode 100644 index 00000000..13ce74c0 --- /dev/null +++ b/Tests/CSF.Screenplay.Selenium.Tests/Actions/SendKeysTests.cs @@ -0,0 +1,27 @@ +using CSF.Screenplay.Selenium.Elements; +using static CSF.Screenplay.PerformanceStarter; +using static CSF.Screenplay.Selenium.PerformableBuilder; + +namespace CSF.Screenplay.Selenium.Actions; + +[TestFixture] +public class SendKeysTests +{ + static readonly ITarget + inputArea = new ElementId("inputArea", "the input area"), + displayText = new ElementId("display", "the displayable text"); + + static readonly NamedUri testPage = new NamedUri("SendKeysTests.html", "the test page"); + + [Test, Screenplay] + public async Task SendingKeysToAnInputAreaShouldUpdateTheDisplay(IStage stage) + { + var webster = stage.Spotlight(); + + await Given(webster).WasAbleTo(OpenTheUrl(testPage)); + await When(webster).AttemptsTo(EnterTheText("Hello World").Into(inputArea)); + var contents = await Then(webster).Should(ReadFromTheElement(displayText).TheText()); + + Assert.That(contents, Is.EqualTo("Hello World")); + } +} \ No newline at end of file diff --git a/Tests/CSF.Screenplay.Selenium.Tests/Actions/WaitTests.cs b/Tests/CSF.Screenplay.Selenium.Tests/Actions/WaitTests.cs new file mode 100644 index 00000000..553e42f0 --- /dev/null +++ b/Tests/CSF.Screenplay.Selenium.Tests/Actions/WaitTests.cs @@ -0,0 +1,103 @@ + +using System; +using CSF.Screenplay.Performables; +using CSF.Screenplay.Selenium.Elements; +using static CSF.Screenplay.PerformanceStarter; +using static CSF.Screenplay.Selenium.PerformableBuilder; + +namespace CSF.Screenplay.Selenium.Actions; + +[TestFixture] +public class WaitTests +{ + static readonly ITarget + delayTimer = new ElementId("delay", "the delay timer"), + clickableButton = new ElementId("clickable", "the clickable button"), + displayText = new ElementId("display", "the displayable text"); + + static readonly NamedUri testPage = new NamedUri("WaitTests.html", "the test page"); + + [Test, Screenplay] + public async Task WaitingForSufficientTimeShouldSucceed(IStage stage) + { + var webster = stage.Spotlight(); + + await Given(webster).WasAbleTo(OpenTheUrl(testPage)); + await Given(webster).WasAbleTo(EnterTheText("250").Into(delayTimer)); + await When(webster).AttemptsTo(ClickOn(clickableButton)); + await Then(webster).Should(WaitUntil(displayText.Has().TextEqualTo("Clicked, and 250ms has elapsed")).ForAtMost(TimeSpan.FromMilliseconds(500)).WithPollingInterval(TimeSpan.FromMilliseconds(150))); + var contents = await Then(webster).Should(ReadFromTheElement(displayText).TheText()); + + Assert.That(contents, Is.EqualTo("Clicked, and 250ms has elapsed")); + } + + [Test, Screenplay] + public async Task WaitingForInsufficientTimeShouldThrow(IStage stage) + { + var webster = stage.Spotlight(); + + await Given(webster).WasAbleTo(OpenTheUrl(testPage)); + await Given(webster).WasAbleTo(EnterTheText("2000").Into(delayTimer)); + await When(webster).AttemptsTo(ClickOn(clickableButton)); + + Assert.That(async () => await Then(webster).Should(WaitUntil(displayText.Has().TextEqualTo("Clicked, and 2000ms has elapsed")).ForAtMost(TimeSpan.FromMilliseconds(500))), + Throws.InstanceOf().And.InnerException.TypeOf()); + } + + [Test, Screenplay] + public async Task WaitingForSufficientTimeUsingDefaultWaitAbilityShouldSucceed(IStage stage) + { + var webster = stage.Spotlight(); + webster.IsAbleTo(new UseADefaultWaitTime(TimeSpan.FromMilliseconds(500))); + + await Given(webster).WasAbleTo(OpenTheUrl(testPage)); + await Given(webster).WasAbleTo(EnterTheText("250").Into(delayTimer)); + await When(webster).AttemptsTo(ClickOn(clickableButton)); + await Then(webster).Should(WaitUntil(displayText.Has().TextEqualTo("Clicked, and 250ms has elapsed"))); + var contents = await Then(webster).Should(ReadFromTheElement(displayText).TheText()); + + Assert.That(contents, Is.EqualTo("Clicked, and 250ms has elapsed")); + } + + [Test, Screenplay] + public async Task WaitingForInsufficientTimeUsingDefaultWaitAbilityShouldThrow(IStage stage) + { + var webster = stage.Spotlight(); + webster.IsAbleTo(new UseADefaultWaitTime(TimeSpan.FromMilliseconds(100))); + + await Given(webster).WasAbleTo(OpenTheUrl(testPage)); + await Given(webster).WasAbleTo(EnterTheText("2000").Into(delayTimer)); + await When(webster).AttemptsTo(ClickOn(clickableButton)); + + Assert.That(async () => await Then(webster).Should(WaitUntil(displayText.Has().TextEqualTo("Clicked, and 2000ms has elapsed"))), + Throws.InstanceOf().And.InnerException.TypeOf()); + } + + [Test, Screenplay] + public async Task WaitingForSufficientTimeWithoutAPredicateShouldSucceed(IStage stage) + { + var webster = stage.Spotlight(); + + await Given(webster).WasAbleTo(OpenTheUrl(testPage)); + await Given(webster).WasAbleTo(EnterTheText("250").Into(delayTimer)); + await When(webster).AttemptsTo(ClickOn(clickableButton)); + await Then(webster).Should(WaitFor(TimeSpan.FromMilliseconds(300))); + var contents = await Then(webster).Should(ReadFromTheElement(displayText).TheText()); + + Assert.That(contents, Is.EqualTo("Clicked, and 250ms has elapsed")); + } + + [Test, Screenplay] + public async Task WaitingForInsufficientTimeWithoutAPredicateShouldYieldIncorrectResult(IStage stage) + { + var webster = stage.Spotlight(); + + await Given(webster).WasAbleTo(OpenTheUrl(testPage)); + await Given(webster).WasAbleTo(EnterTheText("250").Into(delayTimer)); + await When(webster).AttemptsTo(ClickOn(clickableButton)); + await Then(webster).Should(WaitFor(TimeSpan.FromMilliseconds(10))); + var contents = await Then(webster).Should(ReadFromTheElement(displayText).TheText()); + + Assert.That(contents, Is.Not.EqualTo("Clicked, and 250ms has elapsed")); + } +} \ No newline at end of file diff --git a/Tests/CSF.Screenplay.Selenium.Tests/AutoMoqDataAttribute.cs b/Tests/CSF.Screenplay.Selenium.Tests/AutoMoqDataAttribute.cs new file mode 100644 index 00000000..c2b543c9 --- /dev/null +++ b/Tests/CSF.Screenplay.Selenium.Tests/AutoMoqDataAttribute.cs @@ -0,0 +1,12 @@ +using AutoFixture; +using AutoFixture.AutoMoq; + +namespace CSF.Screenplay; + +/// +/// Sets up a test to use Autofixture & Moq together, via . +/// +public class AutoMoqDataAttribute : AutoDataAttribute +{ + public AutoMoqDataAttribute() : base(() => new Fixture().Customize(new AutoMoqCustomization())) {} +} \ No newline at end of file diff --git a/Tests/CSF.Screenplay.Selenium.Tests/Builders/FilterSpecificationBuilderTests.cs b/Tests/CSF.Screenplay.Selenium.Tests/Builders/FilterSpecificationBuilderTests.cs new file mode 100644 index 00000000..afbc604f --- /dev/null +++ b/Tests/CSF.Screenplay.Selenium.Tests/Builders/FilterSpecificationBuilderTests.cs @@ -0,0 +1,323 @@ +using System.Collections.ObjectModel; +using System.Drawing; +using AutoFixture; +using CSF.Screenplay.Selenium.Elements; +using CSF.Specifications; +using Moq; +using OpenQA.Selenium; +using static CSF.Screenplay.Selenium.Builders.FilterSpecificationBuilder; + +namespace CSF.Screenplay.Selenium.Builders; + +[TestFixture, Parallelizable] +public class FilterSpecificationBuilderTests +{ + [Test, AutoMoqData] + public void HaveAttributeValueShouldCreateASpecificationThatUsesGetAttribute(SeleniumElement matchingElement, + SeleniumElement nonMatchingElement) + { + Mock.Get(matchingElement.WebElement) + .Setup(e => e.GetAttribute("data-test")) + .Returns("foobar"); + Mock.Get(nonMatchingElement.WebElement) + .Setup(e => e.GetAttribute("data-test")) + .Returns("baz"); + + var sut = HaveAttributeValue("data-test", v => v.StartsWith("foo")); + + Assert.Multiple(() => + { + Assert.That(sut.Matches(matchingElement), Is.True, "Matching element should match"); + Assert.That(sut.Matches(nonMatchingElement), Is.False, "Non-matching element should not match"); + }); + } + + [Test, AutoMoqData] + public void HaveAttributeValueWithPlainValueShouldCreateASpecificationThatUsesGetAttribute(SeleniumElement matchingElement, + SeleniumElement nonMatchingElement) + { + Mock.Get(matchingElement.WebElement) + .Setup(e => e.GetAttribute("data-test")) + .Returns("foobar"); + Mock.Get(nonMatchingElement.WebElement) + .Setup(e => e.GetAttribute("data-test")) + .Returns("food"); + + var sut = HaveAttributeValue("data-test", "foobar"); + + Assert.Multiple(() => + { + Assert.That(sut.Matches(matchingElement), Is.True, "Matching element should match"); + Assert.That(sut.Matches(nonMatchingElement), Is.False, "Non-matching element should not match"); + }); + } + + [Test, AutoMoqData] + public void HaveAttributeShouldCreateASpecificationThatUsesGetAttribute(SeleniumElement matchingElement, + SeleniumElement nonMatchingElement) + { + Mock.Get(matchingElement.WebElement) + .Setup(e => e.GetAttribute("data-test")) + .Returns("foobar"); + Mock.Get(nonMatchingElement.WebElement) + .Setup(e => e.GetAttribute("data-test")) + .Returns(() => null!); + + var sut = HaveAttribute("data-test"); + + Assert.Multiple(() => + { + Assert.That(sut.Matches(matchingElement), Is.True, "Matching element should match"); + Assert.That(sut.Matches(nonMatchingElement), Is.False, "Non-matching element should not match"); + }); + } + + [Test, AutoMoqData] + public void HaveClassShouldCreateASpecificationThatUsesGetAttribute(SeleniumElement matchingElement, + SeleniumElement nonMatchingElement) + { + Mock.Get(matchingElement.WebElement) + .Setup(e => e.GetAttribute("class")) + .Returns("foobar baz"); + Mock.Get(nonMatchingElement.WebElement) + .Setup(e => e.GetAttribute("class")) + .Returns("baz qux quux"); + + var sut = HaveClass("foobar"); + + Assert.Multiple(() => + { + Assert.That(sut.Matches(matchingElement), Is.True, "Matching element should match"); + Assert.That(sut.Matches(nonMatchingElement), Is.False, "Non-matching element should not match"); + }); + } + + [Test, AutoMoqData] + public void HaveAllClassesShouldCreateASpecificationThatUsesGetAttribute(SeleniumElement matchingElement, + SeleniumElement nonMatchingElement) + { + Mock.Get(matchingElement.WebElement) + .Setup(e => e.GetAttribute("class")) + .Returns("foobar baz"); + Mock.Get(nonMatchingElement.WebElement) + .Setup(e => e.GetAttribute("class")) + .Returns("qux foobar quux"); + + var sut = HaveAllClasses("foobar", "baz"); + + Assert.Multiple(() => + { + Assert.That(sut.Matches(matchingElement), Is.True, "Matching element should match"); + Assert.That(sut.Matches(nonMatchingElement), Is.False, "Non-matching element should not match"); + }); + } + + [Test, AutoMoqData] + public void AreClickableShouldCreateASpecificationThatTestsVisibilityAndEnabledState(SeleniumElement matchingElement, + SeleniumElement nonMatchingElement1, + SeleniumElement nonMatchingElement2, + SeleniumElement nonMatchingElement3) + { + Mock.Get(matchingElement.WebElement).SetupGet(e => e.Displayed).Returns(true); + Mock.Get(matchingElement.WebElement).SetupGet(e => e.Enabled).Returns(true); + Mock.Get(nonMatchingElement1.WebElement).SetupGet(e => e.Displayed).Returns(false); + Mock.Get(nonMatchingElement1.WebElement).SetupGet(e => e.Enabled).Returns(true); + Mock.Get(nonMatchingElement2.WebElement).SetupGet(e => e.Displayed).Returns(true); + Mock.Get(nonMatchingElement2.WebElement).SetupGet(e => e.Enabled).Returns(false); + Mock.Get(nonMatchingElement3.WebElement).SetupGet(e => e.Displayed).Returns(false); + Mock.Get(nonMatchingElement3.WebElement).SetupGet(e => e.Enabled).Returns(false); + + var sut = AreClickable(); + + Assert.Multiple(() => + { + Assert.That(sut.Matches(matchingElement), Is.True, "Matching element should match"); + Assert.That(sut.Matches(nonMatchingElement1), Is.False, "Non-matching element 1 should not match"); + Assert.That(sut.Matches(nonMatchingElement2), Is.False, "Non-matching element 2 should not match"); + Assert.That(sut.Matches(nonMatchingElement3), Is.False, "Non-matching element 3 should not match"); + }); + } + + [Test, AutoMoqData] + public void HaveCssPropertyShouldCreateASpecificationThatUsesGetCssValue(SeleniumElement matchingElement, + SeleniumElement nonMatchingElement) + { + Mock.Get(matchingElement.WebElement) + .Setup(e => e.GetCssValue("color")) + .Returns("rgba(255, 0, 0, 1)"); + Mock.Get(nonMatchingElement.WebElement) + .Setup(e => e.GetCssValue("color")) + .Returns("rgba(0, 0, 255, 1)"); + + var sut = HaveCssProperty("color", v => v == "rgba(255, 0, 0, 1)"); + + Assert.Multiple(() => + { + Assert.That(sut.Matches(matchingElement), Is.True, "Matching element should match"); + Assert.That(sut.Matches(nonMatchingElement), Is.False, "Non-matching element should not match"); + }); + } + + [Test, AutoMoqData] + public void HaveLocationShouldCreateASpecificationThatUsesLocationQuery(SeleniumElement matchingElement, + SeleniumElement nonMatchingElement) + { + Mock.Get(matchingElement.WebElement).SetupGet(e => e.Location).Returns(new Point(100, 200)); + Mock.Get(nonMatchingElement.WebElement).SetupGet(e => e.Location).Returns(new Point(150, 250)); + + var sut = HaveLocation(new Point(100, 200)); + + Assert.Multiple(() => + { + Assert.That(sut.Matches(matchingElement), Is.True, "Matching element should match"); + Assert.That(sut.Matches(nonMatchingElement), Is.False, "Non-matching element should not match"); + }); + } + + [Test, AutoMoqData] + public void HaveSizeShouldCreateASpecificationThatUsesSizeQuery(SeleniumElement matchingElement, + SeleniumElement nonMatchingElement) + { + Mock.Get(matchingElement.WebElement).SetupGet(e => e.Size).Returns(new Size(100, 200)); + Mock.Get(nonMatchingElement.WebElement).SetupGet(e => e.Size).Returns(new Size(150, 250)); + + var sut = HaveSize(new Size(100, 200)); + + Assert.Multiple(() => + { + Assert.That(sut.Matches(matchingElement), Is.True, "Matching element should match"); + Assert.That(sut.Matches(nonMatchingElement), Is.False, "Non-matching element should not match"); + }); + } + + [Test, AutoMoqData] + public void HaveTextShouldCreateASpecificationThatUsesTextQuery(SeleniumElement matchingElement, + SeleniumElement nonMatchingElement) + { + Mock.Get(matchingElement.WebElement).SetupGet(e => e.Text).Returns("Hello World"); + Mock.Get(nonMatchingElement.WebElement).SetupGet(e => e.Text).Returns("Goodbye World"); + + var sut = HaveText("Hello World"); + + Assert.Multiple(() => + { + Assert.That(sut.Matches(matchingElement), Is.True, "Matching element should match"); + Assert.That(sut.Matches(nonMatchingElement), Is.False, "Non-matching element should not match"); + }); + } + + [Test, AutoMoqData] + public void HaveValueShouldCreateASpecificationThatUsesValueQuery(SeleniumElement matchingElement, + SeleniumElement nonMatchingElement) + { + Mock.Get(matchingElement.WebElement).Setup(e => e.GetDomProperty("value")).Returns("Hello World"); + Mock.Get(nonMatchingElement.WebElement).Setup(e => e.GetDomProperty("value")).Returns("Goodbye World"); + + var sut = HaveValue("Hello World"); + + Assert.Multiple(() => + { + Assert.That(sut.Matches(matchingElement), Is.True, "Matching element should match"); + Assert.That(sut.Matches(nonMatchingElement), Is.False, "Non-matching element should not match"); + }); + } + + [Test, AutoMoqData] + public void AreVisibleShouldCreateASpecificationThatUsesVisibilityQuery(SeleniumElement matchingElement, + SeleniumElement nonMatchingElement) + { + Mock.Get(matchingElement.WebElement).SetupGet(e => e.Displayed).Returns(true); + Mock.Get(nonMatchingElement.WebElement).SetupGet(e => e.Displayed).Returns(false); + + var sut = AreVisible(); + + Assert.Multiple(() => + { + Assert.That(sut.Matches(matchingElement), Is.True, "Matching element should match"); + Assert.That(sut.Matches(nonMatchingElement), Is.False, "Non-matching element should not match"); + }); + } + + [Test, AutoMoqData] + public void HaveSelectedOptionsByTextShouldCreateASpecificationThatUsesOptionsQuery(SeleniumElement matchingElement, + SeleniumElement nonMatchingElement) + { + Mock.Get(matchingElement.WebElement) + .Setup(e => e.FindElements(It.Is(b => b.Mechanism == "tag name" && b.Criteria == "option"))) + .Returns(new ReadOnlyCollection([ + Mock.Of(we => we.Text == "Option 1" && we.Selected && we.GetDomProperty("value") == "1"), + Mock.Of(we => we.Text == "Option 2" && !we.Selected && we.GetDomProperty("value") == "2"), + Mock.Of(we => we.Text == "Option 3" && we.Selected && we.GetDomProperty("value") == "3"), + ])); + Mock.Get(nonMatchingElement.WebElement) + .Setup(e => e.FindElements(It.Is(b => b.Mechanism == "tag name" && b.Criteria == "option"))) + .Returns(new ReadOnlyCollection([ + Mock.Of(we => we.Text == "Option 1" && !we.Selected && we.GetDomProperty("value") == "1"), + Mock.Of(we => we.Text == "Option 2" && !we.Selected && we.GetDomProperty("value") == "2"), + Mock.Of(we => we.Text == "Option 3" && we.Selected && we.GetDomProperty("value") == "3"), + ])); + + var sut = HaveSelectedOptionsByText("Option 1", "Option 3"); + + Assert.Multiple(() => + { + Assert.That(sut.Matches(matchingElement), Is.True, "Matching element should match"); + Assert.That(sut.Matches(nonMatchingElement), Is.False, "Non-matching element should not match"); + }); + } + + [Test, AutoMoqData] + public void HaveUnSelectedOptionsByTextShouldCreateASpecificationThatUsesOptionsQuery(SeleniumElement matchingElement, + SeleniumElement nonMatchingElement) + { + Mock.Get(matchingElement.WebElement) + .Setup(e => e.FindElements(It.Is(b => b.Mechanism == "tag name" && b.Criteria == "option"))) + .Returns(new ReadOnlyCollection([ + Mock.Of(we => we.Text == "Option 1" && we.Selected && we.GetDomProperty("value") == "1"), + Mock.Of(we => we.Text == "Option 2" && !we.Selected && we.GetDomProperty("value") == "2"), + Mock.Of(we => we.Text == "Option 3" && we.Selected && we.GetDomProperty("value") == "3"), + ])); + Mock.Get(nonMatchingElement.WebElement) + .Setup(e => e.FindElements(It.Is(b => b.Mechanism == "tag name" && b.Criteria == "option"))) + .Returns(new ReadOnlyCollection([ + Mock.Of(we => we.Text == "Option 1" && !we.Selected && we.GetDomProperty("value") == "1"), + Mock.Of(we => we.Text == "Option 2" && !we.Selected && we.GetDomProperty("value") == "2"), + Mock.Of(we => we.Text == "Option 3" && we.Selected && we.GetDomProperty("value") == "3"), + ])); + + var sut = HaveUnselectedOptionsByText("Option 2"); + + Assert.Multiple(() => + { + Assert.That(sut.Matches(matchingElement), Is.True, "Matching element should match"); + Assert.That(sut.Matches(nonMatchingElement), Is.False, "Non-matching element should not match"); + }); + } + + [Test, AutoMoqData] + public void HaveOptionsByTextShouldCreateASpecificationThatUsesOptionsQuery(SeleniumElement matchingElement, + SeleniumElement nonMatchingElement) + { + Mock.Get(matchingElement.WebElement) + .Setup(e => e.FindElements(It.Is(b => b.Mechanism == "tag name" && b.Criteria == "option"))) + .Returns(new ReadOnlyCollection([ + Mock.Of(we => we.Text == "Option 1" && we.Selected && we.GetDomProperty("value") == "1"), + Mock.Of(we => we.Text == "Option 2" && !we.Selected && we.GetDomProperty("value") == "2"), + Mock.Of(we => we.Text == "Option 3" && we.Selected && we.GetDomProperty("value") == "3"), + ])); + Mock.Get(nonMatchingElement.WebElement) + .Setup(e => e.FindElements(It.Is(b => b.Mechanism == "tag name" && b.Criteria == "option"))) + .Returns(new ReadOnlyCollection([ + Mock.Of(we => we.Text == "Option 1" && !we.Selected && we.GetDomProperty("value") == "1"), + Mock.Of(we => we.Text == "Option 3" && we.Selected && we.GetDomProperty("value") == "3"), + ])); + + var sut = HaveOptionsByText("Option 1", "Option 2", "Option 3"); + + Assert.Multiple(() => + { + Assert.That(sut.Matches(matchingElement), Is.True, "Matching element should match"); + Assert.That(sut.Matches(nonMatchingElement), Is.False, "Non-matching element should not match"); + }); + } +} diff --git a/Tests/CSF.Screenplay.Selenium.Tests/CSF.Screenplay.Selenium.Tests.csproj b/Tests/CSF.Screenplay.Selenium.Tests/CSF.Screenplay.Selenium.Tests.csproj new file mode 100644 index 00000000..00908a6b --- /dev/null +++ b/Tests/CSF.Screenplay.Selenium.Tests/CSF.Screenplay.Selenium.Tests.csproj @@ -0,0 +1,41 @@ + + + + net8.0 + enable + false + true + CSF.Screenplay.Selenium + + + + + + + + + + + + + + + + + + + + + + + + + + + + + PreserveNewest + + + + diff --git a/Tests/CSF.Screenplay.Selenium.Tests/Elements/FilterElementsTests.cs b/Tests/CSF.Screenplay.Selenium.Tests/Elements/FilterElementsTests.cs new file mode 100644 index 00000000..47f7f41a --- /dev/null +++ b/Tests/CSF.Screenplay.Selenium.Tests/Elements/FilterElementsTests.cs @@ -0,0 +1,29 @@ +namespace CSF.Screenplay.Selenium.Elements; + +using CSF.Specifications; +using static CSF.Screenplay.PerformanceStarter; +using static CSF.Screenplay.Selenium.PerformableBuilder; +using static CSF.Screenplay.Selenium.Builders.FilterSpecificationBuilder; + +[TestFixture] +public class FilterElementsTests +{ + static readonly Locator + allInputs = new ClassName("multiInput", "all input elements with the 'multiInput' class"); + static readonly ISpecificationFunction + specialInputs = HaveAttributeValue("class", c => c.Contains("specialInput")); + static readonly NamedUri testPage = new NamedUri("LocatorTests.html", "the test page"); + + [Test, Screenplay] + public async Task FilteringTheInputElementsForOnlySpecialInputsShouldReturnTheCorrectResult(IStage stage) + { + var webster = stage.Spotlight(); + + await Given(webster).WasAbleTo(OpenTheUrl(testPage)); + var elements = await Given(webster).WasAbleTo(FindElementsOnThePage().WhichMatch(allInputs)); + var filteredElements = await When(webster).AttemptsTo(FilterTheElements(elements).ForThoseWhich(specialInputs)); + var values = await Then(webster).Should(ReadFromTheCollectionOfElements(filteredElements).Value()); + + Assert.That(values, Is.EqualTo(new [] {"Second input", "Third input"})); + } +} \ No newline at end of file diff --git a/Tests/CSF.Screenplay.Selenium.Tests/Elements/LocatorTests.cs b/Tests/CSF.Screenplay.Selenium.Tests/Elements/LocatorTests.cs new file mode 100644 index 00000000..51cfc2f9 --- /dev/null +++ b/Tests/CSF.Screenplay.Selenium.Tests/Elements/LocatorTests.cs @@ -0,0 +1,73 @@ +namespace CSF.Screenplay.Selenium.Elements; +using static CSF.Screenplay.PerformanceStarter; +using static CSF.Screenplay.Selenium.PerformableBuilder; + +[TestFixture, Description("Tests for various subclasses of Locator")] +public class LocatorTests +{ + static readonly ITarget + theParagraph = new ElementId("theParagraph", "the unique paragraph element"), + theDiv = new ClassName("theDiv", "the unique div element"), + allInputs = new ClassName("multiInput", "all input elements with the 'multiInput' class"), + specialInputs = new CssSelector("#container .specialInput", "the input element with both 'input' and 'specialInput' classes"), + spansInList = new XPath("//ul[@id='theList']//span", "all span elements within the ul element which has id 'theList'"); + + static readonly NamedUri testPage = new NamedUri("LocatorTests.html", "the test page"); + + [Test, Screenplay] + public async Task GettingTextFromTheParagraphShouldReturnTheCorrectResult(IStage stage) + { + var webster = stage.Spotlight(); + + await Given(webster).WasAbleTo(OpenTheUrl(testPage)); + var result = await When(webster).AttemptsTo(ReadFromTheElement(theParagraph).TheText()); + + Assert.That(result, Is.EqualTo("This is a paragraph of test which is selectable by the element id theParagraph.")); + } + + [Test, Screenplay] + public async Task GettingTextFromTheDivShouldReturnTheCorrectResult(IStage stage) + { + var webster = stage.Spotlight(); + + await Given(webster).WasAbleTo(OpenTheUrl(testPage)); + var result = await When(webster).AttemptsTo(ReadFromTheElement(theDiv).TheText()); + + Assert.That(result, Is.EqualTo("This is a div which is selectable by the class attribute theDiv.")); + } + + [Test, Screenplay] + public async Task GettingTheValueFromTheMultiInputsShouldReturnTheCorrectResult(IStage stage) + { + var webster = stage.Spotlight(); + + await Given(webster).WasAbleTo(OpenTheUrl(testPage)); + var result = await When(webster).AttemptsTo(ReadFromTheCollectionOfElements(allInputs).Value()); + + Assert.That(result, Is.EqualTo(new [] {"First input", "Second input", "Third input"})); + } + + [Test, Screenplay] + public async Task GettingTheValueFromTheSpecialInputsShouldReturnTheCorrectResult(IStage stage) + { + var webster = stage.Spotlight(); + + await Given(webster).WasAbleTo(OpenTheUrl(testPage)); + var result = await When(webster).AttemptsTo(ReadFromTheCollectionOfElements(specialInputs).Value()); + + Assert.That(result, Is.EqualTo(new [] {"Second input", "Third input"})); + } + + [Test, Screenplay] + public async Task GettingTheTextFromTheSpansInTheListShouldReturnTheCorrectResult(IStage stage) + { + var webster = stage.Spotlight(); + + await Given(webster).WasAbleTo(OpenTheUrl(testPage)); + var result = await When(webster).AttemptsTo(ReadFromTheCollectionOfElements(spansInList).Text()); + + Assert.That(result, Is.EqualTo(new [] {"First text inside a span", "Second text inside a span"})); + } + + +} \ No newline at end of file diff --git a/Tests/CSF.Screenplay.Selenium.Tests/GlobalUsings.cs b/Tests/CSF.Screenplay.Selenium.Tests/GlobalUsings.cs new file mode 100644 index 00000000..26aa4cb0 --- /dev/null +++ b/Tests/CSF.Screenplay.Selenium.Tests/GlobalUsings.cs @@ -0,0 +1,4 @@ +global using NUnit.Framework; +global using System.Threading; +global using System.Threading.Tasks; +global using AutoFixture.NUnit3; diff --git a/Tests/CSF.Screenplay.Selenium.Tests/Pattie.cs b/Tests/CSF.Screenplay.Selenium.Tests/Pattie.cs new file mode 100644 index 00000000..90ddd900 --- /dev/null +++ b/Tests/CSF.Screenplay.Selenium.Tests/Pattie.cs @@ -0,0 +1,33 @@ +using System; +using CSF.Extensions.WebDriver; +using CSF.Screenplay.Abilities; +using CSF.Screenplay.Reporting; + +namespace CSF.Screenplay.Selenium; + +/// +/// Pattie is an actor who can browse the web, using a different base path to . +/// +/// A factory which creates ability instances +/// A service which gets the file system path at which asset files should be saved +public class Pattie(IGetsWebDriver webDriverFactory, IGetsAssetFilePath pathProvider) : IPersona +{ + static internal Uri TestWebappBaseUri => new Uri("http://localhost:5102/ChildPath/"); + + public string Name => "Pattie"; + + public Actor GetActor(Guid performanceIdentity) + { + var pattie = new Actor(Name, performanceIdentity); + pattie.IsAbleTo(BrowseTheWeb()); + pattie.IsAbleTo(UseABaseUri()); + pattie.IsAbleTo(GetAssetFilePaths()); + return pattie; + } + + BrowseTheWeb BrowseTheWeb() => new(webDriverFactory); + + GetAssetFilePaths GetAssetFilePaths() => new(pathProvider); + + static UseABaseUri UseABaseUri() => new(TestWebappBaseUri); +} \ No newline at end of file diff --git a/Tests/CSF.Screenplay.Selenium.Tests/Properties/ScreenplayAttributes.cs b/Tests/CSF.Screenplay.Selenium.Tests/Properties/ScreenplayAttributes.cs new file mode 100644 index 00000000..d12942e3 --- /dev/null +++ b/Tests/CSF.Screenplay.Selenium.Tests/Properties/ScreenplayAttributes.cs @@ -0,0 +1,4 @@ +using CSF.Screenplay; +using CSF.Screenplay.Selenium; + +[assembly: ScreenplayAssembly(typeof(ScreenplayFactory))] diff --git a/Tests/CSF.Screenplay.Selenium.Tests/Queries/OptionTests.cs b/Tests/CSF.Screenplay.Selenium.Tests/Queries/OptionTests.cs new file mode 100644 index 00000000..b9fa969e --- /dev/null +++ b/Tests/CSF.Screenplay.Selenium.Tests/Queries/OptionTests.cs @@ -0,0 +1,43 @@ +using NUnit.Framework.Internal; + +namespace CSF.Screenplay.Selenium.Queries; + +[TestFixture, Parallelizable] +public class OptionTests +{ + [Test] + public void EqualsShouldReturnTrueForSameOptions() + { + object option1 = new Option("value1", "Option 1"); + object option2 = new Option("value1", "Option 1"); + + Assert.That(option1, Is.EqualTo(option2)); + } + + [Test] + public void GetHashCodeShouldReturnSameResultForSameOptions() + { + var option1 = new Option("value1", "Option 1"); + var option2 = new Option("value1", "Option 1"); + + Assert.That(option1.GetHashCode(), Is.EqualTo(option2.GetHashCode())); + } + + [Test] + public void EqualsShouldReturnFalseForDifferentOptions() + { + object option1 = new Option("value1", "Option 1"); + object option2 = new Option("value2", "Option 2"); + + Assert.That(option1, Is.Not.EqualTo(option2)); + } + + [Test] + public void GetHashCodeShouldReturnDifferentResultForDifferentOptions() + { + var option1 = new Option("value1", "Option 1"); + var option2 = new Option("value2", "Option 2"); + + Assert.That(option1.GetHashCode(), Is.Not.EqualTo(option2.GetHashCode())); + } +} \ No newline at end of file diff --git a/Tests/CSF.Screenplay.Selenium.Tests/Queries/QueriesTests.cs b/Tests/CSF.Screenplay.Selenium.Tests/Queries/QueriesTests.cs new file mode 100644 index 00000000..6f300c5c --- /dev/null +++ b/Tests/CSF.Screenplay.Selenium.Tests/Queries/QueriesTests.cs @@ -0,0 +1,153 @@ +namespace CSF.Screenplay.Selenium.Queries; + +using System.Drawing; +using System.Linq; +using CSF.Screenplay.Selenium.Elements; +using static CSF.Screenplay.PerformanceStarter; +using static CSF.Screenplay.Selenium.PerformableBuilder; + +[TestFixture, Description("Tests for many classes in the Queries namespace")] +public class QueriesTests +{ + static readonly ITarget + passwordInput = new ElementId("passwordInput", "the password input"), + maybeClickableButtons = new CssSelector("button.maybeClickable", "the maybe-clickable buttons"), + backgroundDiv = new ElementId("backgroundDiv", "the div with a background colour"), + positionedDiv = new ElementId("positionedDiv", "the positioned div"), + sizedDiv = new ElementId("sizedDiv", "the sized div"), + hiddenDiv = new ElementId("hiddenDiv", "the hidden div"), + multiSelect = new ElementId("multiSelect", "the multi-select element"); + static readonly NamedUri testPage = new NamedUri("QueriesTests.html", "the test page"); + + [Test, Screenplay] + public async Task ReadingTheTypeAttributeFromAnElementShouldReturnTheCorrectResult(IStage stage) + { + var webster = stage.Spotlight(); + + await Given(webster).WasAbleTo(OpenTheUrl(testPage)); + var value = await When(webster).AttemptsTo(ReadFromTheElement(passwordInput).TheAttribute("type")); + + Assert.That(value, Is.EqualTo("password")); + } + + [Test, Screenplay] + public async Task ReadingTheClickabilityFromSomeElementsShouldReturnTheCorrectResults(IStage stage) + { + var webster = stage.Spotlight(); + + await Given(webster).WasAbleTo(OpenTheUrl(testPage)); + var values = await When(webster).AttemptsTo(ReadFromTheCollectionOfElements(maybeClickableButtons).Clickability()); + + Assert.That(values, Is.EqualTo(new bool[] { true, false })); + } + + [Test, Screenplay] + public async Task ReadingTheCssPropertyFromAnElementShouldReturnTheCorrectResult(IStage stage) + { + var webster = stage.Spotlight(); + + await Given(webster).WasAbleTo(OpenTheUrl(testPage)); + var result = await When(webster).AttemptsTo(ReadFromTheElement(backgroundDiv).TheCssProperty("background-color")); + + Assert.That(result, Is.EqualTo("rgba(170, 170, 255, 1)")); + } + + [Test, Screenplay] + public async Task ReadingTheLocationOfAnElementShouldReturnTheCorrectResult(IStage stage) + { + var webster = stage.Spotlight(); + + await Given(webster).WasAbleTo(OpenTheUrl(testPage)); + var result = await When(webster).AttemptsTo(ReadFromTheElement(positionedDiv).TheLocation()); + + Assert.That(result, Is.EqualTo(new Point(700, 120))); + } + + [Test, Screenplay] + public async Task ReadingTheSizeOfAnElementShouldReturnTheCorrectResult(IStage stage) + { + var webster = stage.Spotlight(); + + await Given(webster).WasAbleTo(OpenTheUrl(testPage)); + var result = await When(webster).AttemptsTo(ReadFromTheElement(sizedDiv).TheSize()); + + Assert.That(result, Is.EqualTo(new Size(400, 150))); + } + + [Test, Screenplay] + public async Task ReadingTheTextOfAnElementShouldReturnTheCorrectResult(IStage stage) + { + var webster = stage.Spotlight(); + + await Given(webster).WasAbleTo(OpenTheUrl(testPage)); + var result = await When(webster).AttemptsTo(ReadFromTheElement(sizedDiv).TheText()); + + Assert.That(result, Is.EqualTo("This div has a size of 400px by 150px.")); + } + + [Test, Screenplay] + public async Task ReadingTheValueOfAnElementShouldReturnTheCorrectResult(IStage stage) + { + var webster = stage.Spotlight(); + + await Given(webster).WasAbleTo(OpenTheUrl(testPage)); + var result = await When(webster).AttemptsTo(ReadFromTheElement(passwordInput).TheValue()); + + Assert.That(result, Is.EqualTo("secret")); + } + + [Test, Screenplay] + public async Task ReadingTheVisibilityOfAVisibleElementShouldReturnTrue(IStage stage) + { + var webster = stage.Spotlight(); + + await Given(webster).WasAbleTo(OpenTheUrl(testPage)); + var result = await When(webster).AttemptsTo(ReadFromTheElement(passwordInput).TheVisibility()); + + Assert.That(result, Is.True); + } + + [Test, Screenplay] + public async Task ReadingTheVisibilityOfAnInvisibleElementShouldReturnFalse(IStage stage) + { + var webster = stage.Spotlight(); + + await Given(webster).WasAbleTo(OpenTheUrl(testPage)); + var result = await When(webster).AttemptsTo(ReadFromTheElement(hiddenDiv).TheVisibility()); + + Assert.That(result, Is.False); + } + + [Test, Screenplay] + public async Task ReadingAllOptionsFromASelectElementShouldReturnTheCorrectResult(IStage stage) + { + var webster = stage.Spotlight(); + + await Given(webster).WasAbleTo(OpenTheUrl(testPage)); + var result = await When(webster).AttemptsTo(ReadFromTheElement(multiSelect).AllOptions()); + + Assert.That(result.Select(x => x.Text), Is.EqualTo(new string[] { "Option 1", "Option 2", "Option 3", "Option 4" })); + } + + [Test, Screenplay] + public async Task ReadingTheSelectedOptionsFromASelectElementShouldReturnTheCorrectResult(IStage stage) + { + var webster = stage.Spotlight(); + + await Given(webster).WasAbleTo(OpenTheUrl(testPage)); + var result = await When(webster).AttemptsTo(ReadFromTheElement(multiSelect).SelectedOptions()); + + Assert.That(result.Select(x => x.Text), Is.EqualTo(new string[] { "Option 1", "Option 3" })); + } + + [Test, Screenplay] + public async Task ReadingTheUnselectedOptionsFromASelectElementShouldReturnTheCorrectResult(IStage stage) + { + var webster = stage.Spotlight(); + + await Given(webster).WasAbleTo(OpenTheUrl(testPage)); + var result = await When(webster).AttemptsTo(ReadFromTheElement(multiSelect).UnselectedOptions()); + + Assert.That(result.Select(x => x.Text), Is.EqualTo(new string[] { "Option 2", "Option 4" })); + } +} \ No newline at end of file diff --git a/Tests/CSF.Screenplay.Selenium.Tests/Questions/FindElementsTests.cs b/Tests/CSF.Screenplay.Selenium.Tests/Questions/FindElementsTests.cs new file mode 100644 index 00000000..028cef8a --- /dev/null +++ b/Tests/CSF.Screenplay.Selenium.Tests/Questions/FindElementsTests.cs @@ -0,0 +1,52 @@ +using System.Linq; +using CSF.Screenplay.Selenium.Elements; +using static CSF.Screenplay.PerformanceStarter; +using static CSF.Screenplay.Selenium.PerformableBuilder; + +namespace CSF.Screenplay.Selenium.Questions; + +[TestFixture] +public class FindElementsTests +{ + static readonly Locator + container = new ElementId("container"), + special = new ClassName("specialInput"), + theList = new ElementId("theList"), + secondSpan = new CssSelector("li:nth-child(3) > span"), + secondSpanDeepSelector = new CssSelector("#theList li:nth-child(3) > span"); + static readonly NamedUri testPage = new NamedUri("LocatorTests.html", "the test page"); + + [Test, Screenplay] + public async Task FindElementsWithinShouldFindCorrectElements(IStage stage) + { + var webster = stage.Spotlight(); + + await Given(webster).WasAbleTo(OpenTheUrl(testPage)); + var inputs = await When(webster).AttemptsTo(FindElementsWithin(container).WhichMatch(special).AndNameThem("the special inputs")); + + Assert.That(inputs.Select(i => i.WebElement.GetDomProperty("value")).ToList(), Is.EqualTo(new[] { "Second input", "Third input" })); + } + + [Test, Screenplay] + public async Task FindAnElementWithinShouldFindCorrectElement(IStage stage) + { + var webster = stage.Spotlight(); + + await Given(webster).WasAbleTo(OpenTheUrl(testPage)); + var span = await When(webster).AttemptsTo(FindAnElementWithin(theList).WhichMatches(secondSpan).AndNameIt("the second span in the list")); + + Assert.That(span.WebElement.Text, Is.EqualTo("Second text inside a span")); + } + + [Test, Screenplay] + public async Task FindAnElementOnThePageShouldFindCorrectElement(IStage stage) + { + var webster = stage.Spotlight(); + + await Given(webster).WasAbleTo(OpenTheUrl(testPage)); + var span = await When(webster).AttemptsTo(FindAnElementOnThePage().WhichMatches(secondSpanDeepSelector)); + + Assert.That(span.WebElement.Text, Is.EqualTo("Second text inside a span")); + } + +} \ No newline at end of file diff --git a/Tests/CSF.Screenplay.Selenium.Tests/Questions/GetWindowTitleTests.cs b/Tests/CSF.Screenplay.Selenium.Tests/Questions/GetWindowTitleTests.cs new file mode 100644 index 00000000..374f08b9 --- /dev/null +++ b/Tests/CSF.Screenplay.Selenium.Tests/Questions/GetWindowTitleTests.cs @@ -0,0 +1,21 @@ +using static CSF.Screenplay.PerformanceStarter; +using static CSF.Screenplay.Selenium.PerformableBuilder; + +namespace CSF.Screenplay.Selenium.Questions; + +[TestFixture] +public class GetWindowTitleTests +{ + static readonly NamedUri testPage = new NamedUri("OpenUrlTests.html", "the test page"); + + [Test, Screenplay] + public async Task GetWindowTitleShouldGetTheCorrectResult(IStage stage) + { + var webster = stage.Spotlight(); + + await Given(webster).WasAbleTo(OpenTheUrl(testPage)); + var title = await When(webster).AttemptsTo(ReadTheWindowTitle()); + + Assert.That(title, Is.EqualTo("Open a URL test page")); + } +} \ No newline at end of file diff --git a/Tests/CSF.Screenplay.Selenium.Tests/Questions/TakeScreenshotTests.cs b/Tests/CSF.Screenplay.Selenium.Tests/Questions/TakeScreenshotTests.cs new file mode 100644 index 00000000..9a9113ff --- /dev/null +++ b/Tests/CSF.Screenplay.Selenium.Tests/Questions/TakeScreenshotTests.cs @@ -0,0 +1,21 @@ +using static CSF.Screenplay.PerformanceStarter; +using static CSF.Screenplay.Selenium.PerformableBuilder; + +namespace CSF.Screenplay.Selenium.Questions; + +[TestFixture] +public class TakeScreenshotTests +{ + static readonly NamedUri testPage = new NamedUri("OpenUrlTests.html", "the test page"); + + [Test, Screenplay] + public async Task TakeAScreenshotShouldGetASeleniumScreenshot(IStage stage) + { + var webster = stage.Spotlight(); + + await Given(webster).WasAbleTo(OpenTheUrl(testPage)); + var screenshot = await When(webster).AttemptsTo(TakeAScreenshot()); + + Assert.That(screenshot, Is.Not.Null); + } +} \ No newline at end of file diff --git a/Tests/CSF.Screenplay.Selenium.Tests/ScreenplayFactory.cs b/Tests/CSF.Screenplay.Selenium.Tests/ScreenplayFactory.cs new file mode 100644 index 00000000..113e6b73 --- /dev/null +++ b/Tests/CSF.Screenplay.Selenium.Tests/ScreenplayFactory.cs @@ -0,0 +1,31 @@ +using CSF.Extensions.WebDriver; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace CSF.Screenplay.Selenium; + +public class ScreenplayFactory : IGetsScreenplay +{ + public Screenplay GetScreenplay() + { + var screenplay = Screenplay.Create(services => + { + services.AddSingleton(GetConfiguration()); + services.AddWebDriverFactory(); + + services.AddTransient(); + services.AddTransient(); + }, options => + { + options.ValueFormatters.Add(typeof(Reporting.OptionsFormatter)); + options.ValueFormatters.Add(typeof(Reporting.ScreenshotFormatter)); + }); + + return screenplay; + } + + static IConfiguration GetConfiguration() => new ConfigurationBuilder() + .AddJsonFile("appsettings.json") + .AddEnvironmentVariables() + .Build(); +} \ No newline at end of file diff --git a/Tests/CSF.Screenplay.Selenium.Tests/Tasks/TakeAndSaveAScreenshotTests.cs b/Tests/CSF.Screenplay.Selenium.Tests/Tasks/TakeAndSaveAScreenshotTests.cs new file mode 100644 index 00000000..05a93930 --- /dev/null +++ b/Tests/CSF.Screenplay.Selenium.Tests/Tasks/TakeAndSaveAScreenshotTests.cs @@ -0,0 +1,42 @@ +using CSF.Screenplay.Actors; +using static CSF.Screenplay.PerformanceStarter; +using static CSF.Screenplay.Selenium.PerformableBuilder; + +namespace CSF.Screenplay.Selenium.Questions; + +[TestFixture] +public class TakeAndSaveAScreenshotTests +{ + static readonly NamedUri testPage = new NamedUri("OpenUrlTests.html", "the test page"); + + string? screenshotPath; + + [Test, Screenplay] + public async Task TakeAndSaveAScreenshotShouldSaveAFile(IStage stage) + { + var webster = stage.Spotlight(); + + try + { + webster.RecordsAsset += OnRecordsAsset; + + await Given(webster).WasAbleTo(OpenTheUrl(testPage)); + await When(webster).AttemptsTo(TakeAndSaveAScreenshot().WithTheName("test screenshot")); + } + finally + { + webster.RecordsAsset -= OnRecordsAsset; + } + + Assert.Multiple(() => + { + Assert.That(screenshotPath, Is.Not.Null); + Assert.That(screenshotPath, Does.Exist); + }); + } + + void OnRecordsAsset(object? sender, PerformableAssetEventArgs e) + { + screenshotPath = e.FilePath; + } +} \ No newline at end of file diff --git a/Tests/CSF.Screenplay.Selenium.Tests/TestWebappSetupAndTeardown.cs b/Tests/CSF.Screenplay.Selenium.Tests/TestWebappSetupAndTeardown.cs new file mode 100644 index 00000000..95772e57 --- /dev/null +++ b/Tests/CSF.Screenplay.Selenium.Tests/TestWebappSetupAndTeardown.cs @@ -0,0 +1,62 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Net.Http; + +namespace CSF.Screenplay.Selenium; + +[SetUpFixture] +public class TestWebappSetupAndTeardown +{ + const int maxAttempts = 15; + const int secondsDelay = 2; + + static Process? webAppProcess; + + [OneTimeSetUp] + public async Task StartWebAppAsync() + { + webAppProcess = Process.Start("dotnet", $"run --project {GetPathToWebappProject()}"); + await WaitForWebAppToBeAvailableAsync(); + } + + [OneTimeTearDown] + public void StopWebApp() + { + webAppProcess?.Kill(); + webAppProcess?.Dispose(); + } + + /// + /// Waits for the testing web app to be available by attempting to connect to it. + /// + /// + /// + /// This method will make a number of attempts to connect to the web app, with a delay between each attempt. + /// If the web app is not available after the maximum number of attempts then an exception will be thrown. + /// + /// + /// A task which completes when the web app is available. + async static Task WaitForWebAppToBeAvailableAsync() + { + using var client = new HttpClient(); + + for(var attempt = 0; attempt < maxAttempts; attempt++) + { + try + { + await Task.Delay(TimeSpan.FromSeconds(secondsDelay)); + await client.GetAsync(new Uri(Webster.TestWebappBaseUri, "index.html")); + return; + } + catch(Exception) + { + // Intentional no-op; we'll just try again + } + } + + throw new TimeoutException($"The web app was not available after {secondsDelay * maxAttempts} seconds."); + } + + static string GetPathToWebappProject() => Path.Combine("..", "..", "..", "..", "CSF.Screenplay.Selenium.TestWebapp"); +} \ No newline at end of file diff --git a/Tests/CSF.Screenplay.Selenium.Tests/Webster.cs b/Tests/CSF.Screenplay.Selenium.Tests/Webster.cs new file mode 100644 index 00000000..aaf5119b --- /dev/null +++ b/Tests/CSF.Screenplay.Selenium.Tests/Webster.cs @@ -0,0 +1,33 @@ +using System; +using CSF.Extensions.WebDriver; +using CSF.Screenplay.Abilities; +using CSF.Screenplay.Reporting; + +namespace CSF.Screenplay.Selenium; + +/// +/// Webster is an actor who can browse the web. +/// +/// A factory which creates ability instances +/// A service which gets the file system path at which asset files should be saved +public class Webster(IGetsWebDriver webDriverFactory, IGetsAssetFilePath pathProvider) : IPersona +{ + static internal Uri TestWebappBaseUri => new Uri("http://localhost:5102/"); + + public string Name => "Webster"; + + public Actor GetActor(Guid performanceIdentity) + { + var webster = new Actor(Name, performanceIdentity); + webster.IsAbleTo(BrowseTheWeb()); + webster.IsAbleTo(UseABaseUri()); + webster.IsAbleTo(GetAssetFilePaths()); + return webster; + } + + BrowseTheWeb BrowseTheWeb() => new(webDriverFactory); + + GetAssetFilePaths GetAssetFilePaths() => new(pathProvider); + + static UseABaseUri UseABaseUri() => new(TestWebappBaseUri); +} diff --git a/Tests/CSF.Screenplay.Selenium.Tests/appsettings.json b/Tests/CSF.Screenplay.Selenium.Tests/appsettings.json new file mode 100644 index 00000000..e0e48d58 --- /dev/null +++ b/Tests/CSF.Screenplay.Selenium.Tests/appsettings.json @@ -0,0 +1,10 @@ +{ + "WebDriverFactory": { + "DriverConfigurations": { + "DefaultChrome": { + "DriverType": "ChromeDriver" + } + }, + "SelectedConfiguration": "DefaultChrome" + } +} diff --git a/Tests/CSF.Screenplay.SpecFlow.Tests/AddingUp/AddNumbers.cs b/Tests/CSF.Screenplay.SpecFlow.Tests/AddingUp/AddNumbers.cs index ab377d9f..dff5e29d 100644 --- a/Tests/CSF.Screenplay.SpecFlow.Tests/AddingUp/AddNumbers.cs +++ b/Tests/CSF.Screenplay.SpecFlow.Tests/AddingUp/AddNumbers.cs @@ -4,7 +4,7 @@ public class AddNumbers : ICanReport { int currentNumber; - public ReportFragment GetReportFragment(IHasName actor, IFormatsReportFragment formatter) + public ReportFragment GetReportFragment(Actor actor, IFormatsReportFragment formatter) => formatter.Format("{Actor} is able to add numbers to a running total", actor); public void Add(int number) => currentNumber += number; diff --git a/Tests/CSF.Screenplay.SpecFlow.Tests/AddingUp/AddTheNumber.cs b/Tests/CSF.Screenplay.SpecFlow.Tests/AddingUp/AddTheNumber.cs index 66a60603..04fa264c 100644 --- a/Tests/CSF.Screenplay.SpecFlow.Tests/AddingUp/AddTheNumber.cs +++ b/Tests/CSF.Screenplay.SpecFlow.Tests/AddingUp/AddTheNumber.cs @@ -2,7 +2,7 @@ namespace CSF.Screenplay.AddingUp; public class AddTheNumber(int number) : IPerformable, ICanReport { - public ReportFragment GetReportFragment(IHasName actor, IFormatsReportFragment formatter) + public ReportFragment GetReportFragment(Actor actor, IFormatsReportFragment formatter) => formatter.Format("{Actor} adds {Aumber} to the running total", actor, number); public ValueTask PerformAsAsync(ICanPerform actor, CancellationToken cancellationToken = default) diff --git a/Tests/CSF.Screenplay.SpecFlow.Tests/AddingUp/AddThreeNumbers.cs b/Tests/CSF.Screenplay.SpecFlow.Tests/AddingUp/AddThreeNumbers.cs index 7be02805..b3cca04a 100644 --- a/Tests/CSF.Screenplay.SpecFlow.Tests/AddingUp/AddThreeNumbers.cs +++ b/Tests/CSF.Screenplay.SpecFlow.Tests/AddingUp/AddThreeNumbers.cs @@ -4,7 +4,7 @@ namespace CSF.Screenplay.AddingUp { public class AddThreeNumbers(int number1, int number2, int number3) : IPerformable, ICanReport { - public ReportFragment GetReportFragment(IHasName actor, IFormatsReportFragment formatter) + public ReportFragment GetReportFragment(Actor actor, IFormatsReportFragment formatter) => formatter.Format("{Actor} adds three numbers to the running total: {One}, {Two} & {Three}", actor, number1, number2, number3); public async ValueTask PerformAsAsync(ICanPerform actor, CancellationToken cancellationToken = default) diff --git a/Tests/CSF.Screenplay.SpecFlow.Tests/AddingUp/GetTheNumber.cs b/Tests/CSF.Screenplay.SpecFlow.Tests/AddingUp/GetTheNumber.cs index 8d3b3752..56d5806c 100644 --- a/Tests/CSF.Screenplay.SpecFlow.Tests/AddingUp/GetTheNumber.cs +++ b/Tests/CSF.Screenplay.SpecFlow.Tests/AddingUp/GetTheNumber.cs @@ -2,7 +2,7 @@ namespace CSF.Screenplay.AddingUp; public class GetTheNumber() : IPerformableWithResult, ICanReport { - public ReportFragment GetReportFragment(IHasName actor, IFormatsReportFragment formatter) + public ReportFragment GetReportFragment(Actor actor, IFormatsReportFragment formatter) => formatter.Format("{Actor} gets the running total", actor); ValueTask IPerformableWithResult.PerformAsAsync(ICanPerform actor, CancellationToken cancellationToken) diff --git a/Tests/CSF.Screenplay.SpecFlow.Tests/AddingUp/SetTheNumber.cs b/Tests/CSF.Screenplay.SpecFlow.Tests/AddingUp/SetTheNumber.cs index 5459bd15..6d292e6d 100644 --- a/Tests/CSF.Screenplay.SpecFlow.Tests/AddingUp/SetTheNumber.cs +++ b/Tests/CSF.Screenplay.SpecFlow.Tests/AddingUp/SetTheNumber.cs @@ -2,7 +2,7 @@ namespace CSF.Screenplay.AddingUp; public class SetTheNumber(int number) : IPerformable, ICanReport { - public ReportFragment GetReportFragment(IHasName actor, IFormatsReportFragment formatter) + public ReportFragment GetReportFragment(Actor actor, IFormatsReportFragment formatter) => formatter.Format("{Actor} sets the running total to {Number}", actor, number); public ValueTask PerformAsAsync(ICanPerform actor, CancellationToken cancellationToken = default) diff --git a/Tests/CSF.Screenplay.Tests/PerformanceTests.cs b/Tests/CSF.Screenplay.Tests/PerformanceTests.cs index 37ef2724..bf4921a8 100644 --- a/Tests/CSF.Screenplay.Tests/PerformanceTests.cs +++ b/Tests/CSF.Screenplay.Tests/PerformanceTests.cs @@ -10,16 +10,16 @@ public class PerformanceTests public void BeginPerformanceShouldInvokePerformanceBegunOnTheEventBus([Frozen] IRelaysPerformanceEvents performanceEventBus, [AutofixtureServices] IServiceProvider services) { - var sut = new Performance(services); + var sut = new Performance(services, Array.Empty()); sut.BeginPerformance(); Mock.Get(performanceEventBus) - .Verify(x => x.InvokePerformanceBegun(sut.PerformanceIdentity, It.IsAny>()), Times.Once); + .Verify(x => x.InvokePerformanceBegun(sut), Times.Once); } [Test,AutoMoqData] public void BeginPerformanceShouldThrowIfExecutedTwice([AutofixtureServices] IServiceProvider services) { - var sut = new Performance(services); + var sut = new Performance(services, Array.Empty()); sut.BeginPerformance(); Assert.That(() => sut.BeginPerformance(), Throws.InvalidOperationException); } @@ -27,7 +27,7 @@ public void BeginPerformanceShouldThrowIfExecutedTwice([AutofixtureServices] ISe [Test,AutoMoqData] public void FinishPerformanceShouldThrowIfExecutedBeforeBeginPerformance([AutofixtureServices] IServiceProvider services, bool? success) { - var sut = new Performance(services); + var sut = new Performance(services, Array.Empty()); Assert.That(() => sut.FinishPerformance(success), Throws.InvalidOperationException); } @@ -36,17 +36,17 @@ public void FinishPerformanceShouldInvokePerformanceFinishedOnTheEventBus([Froze [AutofixtureServices] IServiceProvider services, bool? success) { - var sut = new Performance(services); + var sut = new Performance(services, Array.Empty()); sut.BeginPerformance(); sut.FinishPerformance(success); Mock.Get(performanceEventBus) - .Verify(x => x.InvokePerformanceFinished(sut.PerformanceIdentity, It.IsAny>(), success), Times.Once); + .Verify(x => x.InvokePerformanceFinished(sut, success), Times.Once); } [Test,AutoMoqData] public void FinishPerformanceShouldThrowIfExecutedTwice([AutofixtureServices] IServiceProvider services, bool? success) { - var sut = new Performance(services); + var sut = new Performance(services, Array.Empty()); sut.BeginPerformance(); sut.FinishPerformance(success); Assert.That(() => sut.FinishPerformance(success), Throws.InvalidOperationException); @@ -55,14 +55,14 @@ public void FinishPerformanceShouldThrowIfExecutedTwice([AutofixtureServices] IS [Test,AutoMoqData] public void PerformanceStateShouldReturnNotStartedBeforeThePerformanceBegins([AutofixtureServices] IServiceProvider services) { - var sut = new Performance(services); + var sut = new Performance(services, Array.Empty()); Assert.That(sut.PerformanceState, Is.EqualTo(PerformanceState.NotStarted)); } [Test,AutoMoqData] public void PerformanceStateShouldReturnInProgressBeforeThePerformanceFinishes([AutofixtureServices] IServiceProvider services) { - var sut = new Performance(services); + var sut = new Performance(services, Array.Empty()); sut.BeginPerformance(); Assert.That(sut.PerformanceState, Is.EqualTo(PerformanceState.InProgress)); } @@ -70,7 +70,7 @@ public void PerformanceStateShouldReturnInProgressBeforeThePerformanceFinishes([ [Test,AutoMoqData] public void PerformanceStateShouldReturnSuccessIfThePerformanceFinishesWithSuccess([AutofixtureServices] IServiceProvider services) { - var sut = new Performance(services); + var sut = new Performance(services, Array.Empty()); sut.BeginPerformance(); sut.FinishPerformance(true); Assert.That(sut.PerformanceState, Is.EqualTo(PerformanceState.Success)); @@ -79,7 +79,7 @@ public void PerformanceStateShouldReturnSuccessIfThePerformanceFinishesWithSucce [Test,AutoMoqData] public void PerformanceStateShouldReturnFailedIfThePerformanceFinishesWithFailure([AutofixtureServices] IServiceProvider services) { - var sut = new Performance(services); + var sut = new Performance(services, Array.Empty()); sut.BeginPerformance(); sut.FinishPerformance(false); Assert.That(sut.PerformanceState, Is.EqualTo(PerformanceState.Failed)); @@ -88,7 +88,7 @@ public void PerformanceStateShouldReturnFailedIfThePerformanceFinishesWithFailur [Test,AutoMoqData] public void PerformanceStateShouldReturnCompletedIfThePerformanceFinishesWithNoResult([AutofixtureServices] IServiceProvider services) { - var sut = new Performance(services); + var sut = new Performance(services, Array.Empty()); sut.BeginPerformance(); sut.FinishPerformance(null); Assert.That(sut.PerformanceState, Is.EqualTo(PerformanceState.Completed)); diff --git a/Tests/CSF.Screenplay.Tests/Reporting/PerformanceReportBuilderTests.cs b/Tests/CSF.Screenplay.Tests/Reporting/PerformanceReportBuilderTests.cs index ce383f6b..fba9d7d7 100644 --- a/Tests/CSF.Screenplay.Tests/Reporting/PerformanceReportBuilderTests.cs +++ b/Tests/CSF.Screenplay.Tests/Reporting/PerformanceReportBuilderTests.cs @@ -219,7 +219,7 @@ public void RecordFailureForCurrentPerformableShouldSetExceptionIsFromConsumedPe public class TaskPerformable : IPerformable, ICanReport { - public ReportFragment GetReportFragment(IHasName actor, IFormatsReportFragment formatter) + public ReportFragment GetReportFragment(Actor actor, IFormatsReportFragment formatter) => formatter.Format("{Actor} starts then stops their stopwatch after a short pause", actor); public async ValueTask PerformAsAsync(ICanPerform actor, CancellationToken cancellationToken = default) diff --git a/Tests/CSF.Screenplay.Tests/WebApis/SendTheHttpRequestAndGetJsonResponseTests.cs b/Tests/CSF.Screenplay.Tests/WebApis/SendTheHttpRequestAndGetJsonResponseTests.cs index d0de8ba8..9a09a50d 100644 --- a/Tests/CSF.Screenplay.Tests/WebApis/SendTheHttpRequestAndGetJsonResponseTests.cs +++ b/Tests/CSF.Screenplay.Tests/WebApis/SendTheHttpRequestAndGetJsonResponseTests.cs @@ -4,7 +4,7 @@ namespace CSF.Screenplay.WebApis; public class SendTheHttpRequestAndGetJsonResponseTests { [Test,AutoMoqData] - public void GetReportFragmentShouldReturnTheCorrectString(IHasName actor, + public void GetReportFragmentShouldReturnTheCorrectString(Actor actor, IFormatsReportFragment formatter, [Frozen] HttpRequestMessageBuilder builder, SendTheHttpRequestAndGetJsonResponse sut, diff --git a/Tests/CSF.Screenplay.Tests/WebApis/SendTheHttpRequestAndGetTheResponseTests.cs b/Tests/CSF.Screenplay.Tests/WebApis/SendTheHttpRequestAndGetTheResponseTests.cs index 7e6f056b..65d8b551 100644 --- a/Tests/CSF.Screenplay.Tests/WebApis/SendTheHttpRequestAndGetTheResponseTests.cs +++ b/Tests/CSF.Screenplay.Tests/WebApis/SendTheHttpRequestAndGetTheResponseTests.cs @@ -4,7 +4,7 @@ namespace CSF.Screenplay.WebApis; public class SendTheHttpRequestAndGetTheResponseTests { [Test,AutoMoqData] - public void GetReportFragmentShouldReturnTheCorrectString(IHasName actor, + public void GetReportFragmentShouldReturnTheCorrectString(Actor actor, IFormatsReportFragment formatter, [Frozen] HttpRequestMessageBuilder builder, SendTheHttpRequestAndGetTheResponse sut, diff --git a/Tests/CSF.Screenplay.Tests/WebApis/SendTheHttpRequestTests.cs b/Tests/CSF.Screenplay.Tests/WebApis/SendTheHttpRequestTests.cs index e0f796d9..62f249ff 100644 --- a/Tests/CSF.Screenplay.Tests/WebApis/SendTheHttpRequestTests.cs +++ b/Tests/CSF.Screenplay.Tests/WebApis/SendTheHttpRequestTests.cs @@ -4,7 +4,7 @@ namespace CSF.Screenplay.WebApis; public class SendTheHttpRequestTests { [Test,AutoMoqData] - public void GetReportFragmentShouldReturnTheCorrectString(IHasName actor, + public void GetReportFragmentShouldReturnTheCorrectString(Actor actor, IFormatsReportFragment formatter, [Frozen] HttpRequestMessageBuilder builder, SendTheHttpRequest sut, diff --git a/Tools/Appveyor.after_deploy.bat b/Tools/Appveyor.after_deploy.bat new file mode 100755 index 00000000..1bf4c01c --- /dev/null +++ b/Tools/Appveyor.after_deploy.bat @@ -0,0 +1,14 @@ +@echo on + +@SET /A exitcode=0 +@SET /A TESTFAILURE_ERROR=1 +@SET /A PUSHARTIFACT_ERROR=2 +@SET /A READREPORT_ERROR=4 + +nunit3-console.exe CSF.Screenplay.Selenium.Tests\bin\Debug\CSF.Screenplay.Selenium.Tests.dll +@IF %ERRORLEVEL% NEQ 0 SET /A exitcode^|=%TESTFAILURE_ERROR% + +appveyor PushArtifact NUnit.report.json +@IF %ERRORLEVEL% NEQ 0 SET /A exitcode^|=%PUSHARTIFACT_ERROR% + +@EXIT /B %exitcode% diff --git a/Tools/Appveyor.before_build.bat b/Tools/Appveyor.before_build.bat new file mode 100755 index 00000000..dc6b1d17 --- /dev/null +++ b/Tools/Appveyor.before_build.bat @@ -0,0 +1,9 @@ +@echo on + +git submodule update --init --recursive + +nuget restore CSF.Screenplay.Selenium.sln + +copy /y CSF.Screenplay.Selenium.Tests\App.AppVeyor.config CSF.Screenplay.Selenium.Tests\app.config + +@echo off \ No newline at end of file diff --git a/Tools/Build.sh b/Tools/Build.sh new file mode 100755 index 00000000..7831ff99 --- /dev/null +++ b/Tools/Build.sh @@ -0,0 +1,70 @@ +#!/bin/bash + +NUNIT_CONSOLE_VERSION="3.7.0" +NUNIT_PATH="./testrunner/NUnit.ConsoleRunner.${NUNIT_CONSOLE_VERSION}/tools/nunit3-console.exe" +TEST_PATTERN="CSF.*.Tests.dll" +UNIT_TESTS="CSF.Screenplay.Selenium.BrowserFlags.Tests" +UNIT_TESTS_PATH="${UNIT_TESTS}/bin/Debug/${UNIT_TESTS}.dll" +WEB_TESTS="CSF.Screenplay.Selenium.Tests" +WEB_TESTS_PATH="${WEB_TESTS}/bin/Debug/${WEB_TESTS}.dll" + +test_outcome=1 + +stop_if_failure() +{ + code="$1" + process="$2" + if [ "$code" -ne "0" ] + then + echo "The process '${process}' failed with exit code $code" + exit "$code" + fi +} + +build_solution() +{ + echo "Building the solution ..." + msbuild /p:Configuration=Debug CSF.Screenplay.Selenium.sln + stop_if_failure $? "Build the solution" +} + +start_webserver() +{ + echo "Starting up the application ..." + bash Tools/Start-webserver.sh + stop_if_failure $? "Starting the application" +} + +run_unit_tests() +{ + echo "Running integration tests ..." + mono "$NUNIT_PATH" --labels=All "$UNIT_TESTS_PATH" + test_outcome=$? +} + +run_integration_tests() +{ + echo "Running integration tests ..." + mono "$NUNIT_PATH" --labels=All "$WEB_TESTS_PATH" + test_outcome=$? +} + +shutdown_webserver() +{ + bash Tools/Stop-webserver.sh +} + +build_solution + +run_unit_tests +if [ "$test_outcome" -ne "0" ] +then + echo "Stopping the build: Unit test failure" + exit 1 +fi + +start_webserver +run_integration_tests +shutdown_webserver + +exit $test_outcome diff --git a/Tools/Start-webserver.sh b/Tools/Start-webserver.sh new file mode 100755 index 00000000..088c120c --- /dev/null +++ b/Tools/Start-webserver.sh @@ -0,0 +1,74 @@ +#!/bin/bash + +SERVER_PORT="8080" +SERVER_ADDR="127.0.0.1" +SERVER_WEB_APP="/:CSF.Screenplay.WebTestWebsite/" +SERVER_PID=".xsp4.pid" +APP_HOMEPAGE="http://localhost:8080/Home" +SECONDS_BETWEEN_CONNECT_ATTEMPTS="1" +MAX_ATTEMPTS="4" + +app_available=1 + +stop_if_already_started() +{ + if [ -f "$SERVER_PID" ] + then + echo "Stopping a running webserver first ..." + Tools/Stop-webserver.sh + fi +} + +start_webserver() +{ + echo "Starting the app on a web server ..." + xsp4 \ + --nonstop \ + --address "$SERVER_ADDR" \ + --port "$SERVER_PORT" \ + --applications "$SERVER_WEB_APP" \ + --pidfile "$SERVER_PID" \ + & +} + +wait_for_app_to_become_available() +{ + echo "Waiting for the app to become available ..." + for attempt in $(seq 1 "$MAX_ATTEMPTS") + do + sleep "$SECONDS_BETWEEN_CONNECT_ATTEMPTS" + echo "Connection attempt $attempt of $MAX_ATTEMPTS ..." + try_web_app_connection + if [ "$app_available" -eq "0" ] + then + echo "Connection successful!" + break + fi + done +} + +try_web_app_connection() +{ + wget \ + -T 90 \ + -O - \ + "$APP_HOMEPAGE" \ + >/dev/null + if [ "$?" -eq "0" ] + then + app_available=0 + fi +} + +stop_if_already_started +start_webserver +wait_for_app_to_become_available + +if [ "$app_available" -ne "0" ] +then + echo "ERROR: Web application did not come up after $MAX_ATTEMPTS attempt(s)" + echo "The webserver is still running, you may attempt to diagnose the problem at:" + echo " $APP_HOMEPAGE" +fi + +exit "$app_available" diff --git a/Tools/Travis.build.sh b/Tools/Travis.build.sh new file mode 100755 index 00000000..45caaffd --- /dev/null +++ b/Tools/Travis.build.sh @@ -0,0 +1,17 @@ +#!/bin/bash + +setup_webdriver_environment_variables() +{ + WebDriver_SauceLabsBuildName="Travis Screenplay.Selenium job ${TRAVIS_JOB_NUMBER}; ${WebDriver_BrowserName} ${WebDriver_BrowserVersion}" + WebDriver_TunnelIdentifier="$TRAVIS_JOB_NUMBER" +} + +setup_webdriver_environment_variables + +export WebDriver_SauceLabsBuildName +export WebDriver_TunnelIdentifier + +Tools/Build.sh +result="$?" + +exit $result \ No newline at end of file diff --git a/Tools/Travis.install.sh b/Tools/Travis.install.sh new file mode 100755 index 00000000..3b5428a0 --- /dev/null +++ b/Tools/Travis.install.sh @@ -0,0 +1,51 @@ +#!/bin/bash + +NUGET_LATEST_DIST="https://dist.nuget.org/win-x86-commandline/latest/nuget.exe" +TRAVIS_TEST_CONFIG_SOURCE="CSF.Screenplay.Selenium.Tests/App.Travis.config" +TEST_CONFIG_DESTINATION="CSF.Screenplay.Selenium.Tests/app.config" +NUNIT_CONSOLE_VERSION="3.7.0" +NUGET_PATH=".nuget/nuget.exe" + +stop_if_failure() +{ + code="$1" + process="$2" + if [ "$code" -ne "0" ] + then + echo "The process '${process}' failed with exit code $code" + exit "$code" + fi +} + +install_latest_nuget() +{ + echo "Downloading the latest version of NuGet ..." + + # Travis uses Xamarin's apt repo which has an ancient nuget version + mkdir -p .nuget + wget -O "$NUGET_PATH" "$NUGET_LATEST_DIST" + stop_if_failure $? "Download NuGet" +} + +echo_nuget_version_to_console() +{ + mono "$NUGET_PATH" +} + +setup_travis_test_config() +{ + echo "Copying Travis-specific test configs ..." + cp "$TRAVIS_TEST_CONFIG_SOURCE" "$TEST_CONFIG_DESTINATION" + stop_if_failure $? "Setup Travis configuration" +} + +install_latest_nuget +echo_nuget_version_to_console +setup_travis_test_config + +export NUGET_PATH +export NUNIT_CONSOLE_VERSION + +Tools/Install.sh + +exit $? \ No newline at end of file diff --git a/Tools/appveyor-setup-selenium.bat b/Tools/appveyor-setup-selenium.bat new file mode 100644 index 00000000..c7e15335 --- /dev/null +++ b/Tools/appveyor-setup-selenium.bat @@ -0,0 +1,20 @@ +REM Set up a Selenium Manager config file, so that a fresh browser and driver are +REM downloaded explicitly, ignoring the pre-installed driver on the CI image. + +mkdir %USERPROFILE%\.cache\selenium +cp Tools\se-config.toml %USERPROFILE%\.cache\selenium + +REM Redefines the PATH environment variable, removing the preinstalled Selenium Webdriver. +REM Modern Selenium downloads/fetches the appropriate driver version for the browser, so +REM having this pre-installed driver in the path actually hurts more than helps. + +setlocal enabledelayedexpansion + +SET UNWANTED_PATH=C:\Tools\WebDriver + +REM Remove the unwanted path (handles all of ";path;", ";path" and "path;" cases) +SET "NEW_PATH=%PATH:;%UNWANTED_PATH%;=;%" +SET "NEW_PATH=!NEW_PATH:;%UNWANTED_PATH%=!" +SET "NEW_PATH=!NEW_PATH:%UNWANTED_PATH%;=!" + +endlocal & SET PATH=%NEW_PATH% diff --git a/Tools/appveyor-setup-sonarscanner.bat b/Tools/appveyor-setup-sonarscanner.bat new file mode 100644 index 00000000..02f08d99 --- /dev/null +++ b/Tools/appveyor-setup-sonarscanner.bat @@ -0,0 +1,13 @@ +REM Set up some env variables which will help SonarScanner identify the current branch + +IF NOT DEFINED APPVEYOR_PULL_REQUEST_HEAD_REPO_BRANCH ( + SET BranchName=%APPVEYOR_REPO_BRANCH% + SET BranchParam=sonar.branch.name + SET PRParam= + echo Not building a PR +) ELSE ( + SET BranchName=%APPVEYOR_PULL_REQUEST_HEAD_REPO_BRANCH% + SET BranchParam=sonar.pullrequest.branch + SET PRParam=/d:sonar.pullrequest.key=%APPVEYOR_PULL_REQUEST_NUMBER% + echo Building a PR +) diff --git a/Tools/appveyor-upload-test-results.ps1 b/Tools/appveyor-upload-test-results.ps1 index 89f70560..e08b6942 100644 --- a/Tools/appveyor-upload-test-results.ps1 +++ b/Tools/appveyor-upload-test-results.ps1 @@ -1,16 +1,17 @@ # Adapted from https://www.appveyor.com/docs/running-tests/#uploading-xml-test-results $SolutionRoot = "$PSScriptRoot\.." -$TestProjects = Get-ChildItem $SolutionRoot\Tests\ +$TestProjects = Get-ChildItem -Path $SolutionRoot\Tests\ -Exclude CSF.Screenplay.Selenium.TestWebapp $wc = New-Object 'System.Net.WebClient' +$TestEndpoint = "https://ci.appveyor.com/api/testresults/nunit/$env:APPVEYOR_JOB_ID" foreach($project in $TestProjects) { - $testResultFile = "$SolutionRoot\Tests\$project\TestResults\TestResults.xml" - Move-Item $testResultFile "$SolutionRoot\TestResults\$project.TestResults.xml" - - # Intentionally using the 'nunit' endpoint and not 'nunit3'. See the following ticket for more info: - # https://help.appveyor.com/discussions/problems/37319-http-500-error-when-uploading-nunit3-test-results-suspected-failure-on-parameterized-test-cases - $wc.UploadFile("https://ci.appveyor.com/api/testresults/nunit/$($env:APPVEYOR_JOB_ID)", (Resolve-Path $SolutionRoot\TestResults\$project.TestResults.xml)) + $projectName = Split-Path $project -Leaf + $testResultFile = "$project\TestResults\TestResults.xml" + Move-Item $testResultFile "$SolutionRoot\TestResults\$projectName.TestResults.xml" + $wc.UploadFile($TestEndpoint, (Resolve-Path $SolutionRoot\TestResults\$projectName.TestResults.xml)) } + +exit $env:TESTS_FAILED diff --git a/Tools/run-tests-with-coverage.ps1 b/Tools/run-tests-with-coverage.ps1 index d54abbb0..b7f20822 100644 --- a/Tools/run-tests-with-coverage.ps1 +++ b/Tools/run-tests-with-coverage.ps1 @@ -1,16 +1,27 @@ $SolutionRoot = "$PSScriptRoot\.." -$TestProjects = Get-ChildItem $SolutionRoot\Tests\ +$TestProjects = Get-ChildItem -Path $SolutionRoot\Tests\ -Exclude CSF.Screenplay.Selenium.TestWebapp $Tfm = "net8.0" $Configuration = "Debug" Remove-Item $SolutionRoot\TestResults\* -ErrorAction Ignore +$TetsFailed = 0 foreach($project in $TestProjects) { + $projectName = Split-Path $project -Leaf + $projectAssembly = "$project\bin\$Configuration\$Tfm\$projectName.dll" coverlet ` - "$SolutionRoot\Tests\$project\bin\$Configuration\$Tfm\$project.dll" ` + "$projectAssembly" ` --target "dotnet" ` - --targetargs "test $SolutionRoot\Tests\$project --no-build --logger:nunit --test-adapter-path:." ` + --targetargs "test $project --no-build --logger:nunit --test-adapter-path:." ` -f=opencover ` - -o="$SolutionRoot\TestResults\$project.opencover.xml" + -o="$SolutionRoot\TestResults\$projectName.opencover.xml" + + if ($LastExitCode -eq 1) { + $TetsFailed = 1 + } + elseif ($LastExitCode -eq 3) { + $TetsFailed = 1 + } } +$env:TESTS_FAILED = $TetsFailed diff --git a/Tools/se-config.toml b/Tools/se-config.toml new file mode 100644 index 00000000..72d87f07 --- /dev/null +++ b/Tools/se-config.toml @@ -0,0 +1,8 @@ +# This config file is required for builds running on AppVeyor +# because AppVeyor images come with chromedriver pre-installed, but its +# usually not the version we want. That's because on the Windows images, +# Chrome will have auto-updated. + +browser = "chrome" +force-browser-download = true +browser-version = "stable" \ No newline at end of file