diff --git a/Behavioral.Automation.API.DemoScenarios/AutomationConfig.json b/Behavioral.Automation.API.DemoScenarios/AutomationConfig.json
new file mode 100644
index 00000000..4885fb96
--- /dev/null
+++ b/Behavioral.Automation.API.DemoScenarios/AutomationConfig.json
@@ -0,0 +1,3 @@
+{
+ "API_HOST": "https://reqres.in/"
+}
\ No newline at end of file
diff --git a/Behavioral.Automation.API.DemoScenarios/Behavioral.Automation.API.DemoScenarios.csproj b/Behavioral.Automation.API.DemoScenarios/Behavioral.Automation.API.DemoScenarios.csproj
new file mode 100644
index 00000000..2048c906
--- /dev/null
+++ b/Behavioral.Automation.API.DemoScenarios/Behavioral.Automation.API.DemoScenarios.csproj
@@ -0,0 +1,37 @@
+
+
+
+ net6.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Always
+
+
+ Always
+
+
+
+
+
+ Always
+
+
+
+
diff --git a/Behavioral.Automation.API.DemoScenarios/Features/Demo.feature b/Behavioral.Automation.API.DemoScenarios/Features/Demo.feature
new file mode 100644
index 00000000..252ffcf2
--- /dev/null
+++ b/Behavioral.Automation.API.DemoScenarios/Features/Demo.feature
@@ -0,0 +1,26 @@
+Feature: reqres test
+
+ Scenario: Get all users
+ When user sends a "GET" request to "api/users" url
+ Then response json path "$..data[?(@.email == 'george.bluth@reqres.in')].first_name" value should be "["George"]"
+
+ Scenario: Get second page
+ When user sends a "GET" request to "api/users" url with the following parameters:
+ | Name | Value | Kind |
+ | page | 2 | Param |
+ | CustomHeader | test | Header |
+
+ Scenario: Complex request
+ Given user creates a "POST" request to "api/users" url with the json:
+ """
+ {
+ "name": "morpheus",
+ "job": "leader"
+ }
+ """
+ When user adds parameters and send request:
+ | Name | Value | Kind |
+ | CustomHeader | test | Header |
+
+ Scenario: Request with json file
+ When user sends a "POST" request to "api/users" url with the json file "TestData\User.json"
\ No newline at end of file
diff --git a/Behavioral.Automation.API.DemoScenarios/TestData/User.json b/Behavioral.Automation.API.DemoScenarios/TestData/User.json
new file mode 100644
index 00000000..1a1b4872
--- /dev/null
+++ b/Behavioral.Automation.API.DemoScenarios/TestData/User.json
@@ -0,0 +1,4 @@
+{
+ "name": "morpheus",
+ "job": "leader"
+}
\ No newline at end of file
diff --git a/Behavioral.Automation.API.DemoScenarios/specflow.json b/Behavioral.Automation.API.DemoScenarios/specflow.json
new file mode 100644
index 00000000..92c9c5a6
--- /dev/null
+++ b/Behavioral.Automation.API.DemoScenarios/specflow.json
@@ -0,0 +1,19 @@
+{
+ "bindingCulture": {
+ "language": "en-us"
+ },
+ "language": {
+ "feature": "en-us"
+ },
+ "runtime": {
+ "missingOrPendingStepsOutcome": "Error"
+ },
+ "stepAssemblies": [
+ {
+ "assembly": "Behavioral.Automation.API"
+ },
+ {
+ "assembly": "Behavioral.Automation.Transformations"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/Behavioral.Automation.API/Behavioral.Automation.API.csproj b/Behavioral.Automation.API/Behavioral.Automation.API.csproj
new file mode 100644
index 00000000..ed3eaba2
--- /dev/null
+++ b/Behavioral.Automation.API/Behavioral.Automation.API.csproj
@@ -0,0 +1,25 @@
+
+
+
+ net6.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Behavioral.Automation.API/Bindings/HttpRequestSteps.cs b/Behavioral.Automation.API/Bindings/HttpRequestSteps.cs
new file mode 100644
index 00000000..6953c2bb
--- /dev/null
+++ b/Behavioral.Automation.API/Bindings/HttpRequestSteps.cs
@@ -0,0 +1,226 @@
+using System.Text;
+using System.Web;
+using Behavioral.Automation.API.Configs;
+using Behavioral.Automation.API.Context;
+using Behavioral.Automation.API.Models;
+using Behavioral.Automation.API.Services;
+using Behavioral.Automation.Configs;
+using Behavioral.Automation.Configs.utils;
+using TechTalk.SpecFlow;
+using TechTalk.SpecFlow.Assist;
+
+namespace Behavioral.Automation.API.Bindings;
+
+[Binding]
+public class HttpRequestSteps
+{
+ private readonly ApiContext _apiContext;
+ private readonly HttpService _httpService;
+
+ public HttpRequestSteps(ApiContext apiContext, HttpService httpService)
+ {
+ _apiContext = apiContext;
+ _httpService = httpService;
+ }
+
+ [When("user sends a \"(.*)\" request to \"(.*)\" url")]
+ public HttpResponseMessage UserSendsHttpRequest(string httpMethod, string url)
+ {
+ var method = new HttpMethod(httpMethod.ToUpper());
+
+ _apiContext.Request = new HttpRequestMessage(method, GetUri(url));
+ _httpService.SendContextRequest();
+
+ return _apiContext.Response;
+ }
+
+ [When("user sends a \"(.*)\" request to \"(.*)\" url with the following parameters:")]
+ public HttpResponseMessage UserSendsHttpRequestWithParameters(string httpMethod, string url, Table tableParameters)
+ {
+ UserCreatesHttpRequestWithParameters(httpMethod, url, tableParameters);
+ _httpService.SendContextRequest();
+
+ return _apiContext.Response;
+ }
+
+ [When("user sends a \"(.*)\" request to \"(.*)\" url with the json:")]
+ public HttpResponseMessage UserSendsHttpRequestWithJson(string httpMethod, string url, string jsonToSend)
+ {
+ //TODO: body can be:
+ // form-data
+ // raw (Text, JavaScript, JSON, HTML, XML)
+ // binary
+ // GraphQL
+ // Consider adding other types
+ var method = new HttpMethod(httpMethod.ToUpper());
+
+ _apiContext.Request = new HttpRequestMessage(method, GetUri(url));
+ _apiContext.Request.Content = new StringContent(jsonToSend, Encoding.UTF8, "application/json");
+
+ _httpService.SendContextRequest();
+
+ return _apiContext.Response;
+ }
+
+ [When("user sends a \"(.*)\" request to \"(.*)\" url with the json file \"(.*)\"")]
+ public HttpResponseMessage UserSendsHttpRequestWithJsonFile(string httpMethod, string url, string filePath)
+ {
+ //TODO: body can be:
+ // form-data
+ // raw (Text, JavaScript, JSON, HTML, XML)
+ // binary
+ // GraphQL
+ // Consider adding other types
+ var method = new HttpMethod(httpMethod.ToUpper());
+
+ _apiContext.Request = new HttpRequestMessage(method, GetUri(url));
+
+ var fullPath = Path.GetFullPath(Path.Join(Directory.GetCurrentDirectory(), filePath)).NormalizePathAccordingOs();
+ if (!File.Exists(fullPath))
+ {
+ throw new FileNotFoundException("The file does not exist", fullPath);
+ }
+
+ var jsonToSend = File.ReadAllText(filePath);
+
+ _apiContext.Request.Content = new StringContent(jsonToSend, Encoding.UTF8, "application/json");
+
+ _httpService.SendContextRequest();
+
+ return _apiContext.Response;
+ }
+
+
+ [When("user sends a \"(.*)\" request to \"(.*)\" url with the application/x-www-form-urlencoded:")]
+ public HttpResponseMessage UserSendsHttpRequestWithFormUrlEncodedContent(string httpMethod, string url, Table parameters)
+ {
+ var method = new HttpMethod(httpMethod.ToUpper());
+
+ _apiContext.Request = new HttpRequestMessage(method, GetUri(url));
+
+ var body = parameters.Rows.Select(row => new KeyValuePair(row[0], row[1]));
+
+ _apiContext.Request.Content = new FormUrlEncodedContent(body);
+
+ _httpService.SendContextRequest();
+
+ return _apiContext.Response;
+ }
+
+ [Given("user creates a \"(.*)\" request to \"(.*)\" url with the json:")]
+ public HttpRequestMessage UserCreatesHttpRequestWithJson(string httpMethod, string url, string jsonToSend)
+ {
+ var method = new HttpMethod(httpMethod.ToUpper());
+
+ _apiContext.Request = new HttpRequestMessage(method, GetUri(url));
+ _apiContext.Request.Content = new StringContent(jsonToSend, Encoding.UTF8, "application/json");
+ return _apiContext.Request;
+ }
+
+ [Given("user creates a \"(.*)\" request to \"(.*)\" url")]
+ public HttpRequestMessage GivenUserCreatesARequestToUrl(string httpMethod, string url)
+ {
+ var method = new HttpMethod(httpMethod.ToUpper());
+
+ _apiContext.Request = new HttpRequestMessage(method, GetUri(url));
+ return _apiContext.Request;
+ }
+
+ [Given("user creates a \"(.*)\" request to \"(.*)\" url with the following parameters:")]
+ public HttpRequestMessage UserCreatesHttpRequestWithParameters(string httpMethod, string url, Table tableParameters)
+ {
+ var method = new HttpMethod(httpMethod.ToUpper());
+
+ _apiContext.Request = new HttpRequestMessage(method, GetUri(url));
+ AddParametersToRequest(_apiContext.Request, tableParameters);
+ return _apiContext.Request;
+ }
+
+ [When("user adds a JSON body and send the request:")]
+ public HttpResponseMessage WhenUserAddsJsonBodyAndSendRequest(string jsonToSend)
+ {
+ _apiContext.Request.Content = new StringContent(jsonToSend, Encoding.UTF8, "application/json");
+
+ _httpService.SendContextRequest();
+
+ return _apiContext.Response;
+ }
+
+ [When("user adds parameters and send request:")]
+ public HttpResponseMessage WhenUserAddsParametersAndSendRequest(Table tableParameters)
+ {
+ AddParametersToRequest(_apiContext.Request, tableParameters);
+ _httpService.SendContextRequest();
+
+ return _apiContext.Response;
+ }
+
+ [When("user sends request")]
+ public HttpResponseMessage WhenUserSendsRequest()
+ {
+ _httpService.SendContextRequest();
+ return _apiContext.Response;
+ }
+
+ [Given("the response status code should be \"(\\d*)\"")]
+ public void ChangeResponseStatusCode(int statusCode)
+ {
+ _apiContext.ExpectedStatusCode = statusCode;
+ }
+
+ private static Uri GetUri(string url)
+ {
+ if (!Uri.IsWellFormedUriString(url, UriKind.Absolute))
+ {
+ url = ConfigManager.GetConfig().ApiHost + url;
+ }
+
+ return new UriBuilder(url).Uri;
+ }
+
+ private static void AddParametersToRequest(HttpRequestMessage request, Table tableParameters)
+ {
+ var uriBuilder = new UriBuilder(request.RequestUri);
+
+ var parameters = tableParameters.CreateSet();
+
+ var headers = new List>>();
+ if (parameters is not null)
+ {
+ var query = HttpUtility.ParseQueryString(uriBuilder.Query);
+ foreach (var parameter in parameters)
+ {
+ var parameterKind = Enum.Parse(parameter.Kind);
+
+ if (parameterKind is RequestParameterKind.Param)
+ {
+ query.Add(parameter.Name, parameter.Value);
+ }
+
+ if (parameterKind is RequestParameterKind.Header)
+ {
+ var headerValue = parameter.Value.Trim().Split(",");
+ headers.Add(new KeyValuePair>(parameter.Name, headerValue));
+ }
+ }
+
+ uriBuilder.Query = query.ToString();
+ }
+
+ request.RequestUri = uriBuilder.Uri;
+
+ if (headers.Any())
+ {
+ foreach (var header in headers)
+ {
+ if (header.Key.Equals("Content-Type", StringComparison.InvariantCultureIgnoreCase))
+ {
+ throw new Exception(
+ "Remove the Content-Type header, please. The Content-Type header is automatically added with request step bindings.");
+ }
+
+ request.Headers.Add(header.Key, header.Value);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Behavioral.Automation.API/Bindings/HttpResponseSteps.cs b/Behavioral.Automation.API/Bindings/HttpResponseSteps.cs
new file mode 100644
index 00000000..5bf49ccd
--- /dev/null
+++ b/Behavioral.Automation.API/Bindings/HttpResponseSteps.cs
@@ -0,0 +1,283 @@
+using System.Text.RegularExpressions;
+using Behavioral.Automation.API.Context;
+using Behavioral.Automation.API.Models;
+using Behavioral.Automation.API.Services;
+using Behavioral.Automation.Configs.utils;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+using NUnit.Framework;
+using Polly;
+using TechTalk.SpecFlow;
+
+namespace Behavioral.Automation.API.Bindings;
+
+[Binding]
+public class HttpResponseSteps
+{
+ private readonly ApiContext _apiContext;
+ private readonly HttpService _httpService;
+ private readonly int _retryCount = 10;
+ private readonly TimeSpan _retryDelay = TimeSpan.FromSeconds(3);
+
+ public HttpResponseSteps(ApiContext apiContext, HttpService httpService)
+ {
+ _apiContext = apiContext;
+ _httpService = httpService;
+ }
+
+ [Then("response attachment filename is \"(.*)\"")]
+ public void ThenResponseAttachmentFilenameIs(string filename)
+ {
+ if (_apiContext.Response is null) throw new Exception("Http response is empty.");
+ var contentDispositionHeader = _apiContext.Response.Content.Headers.ContentDisposition;
+ if (contentDispositionHeader == null)
+ {
+ Assert.Fail("Response header \"ContentDisposition disposition\" is null");
+ }
+ if (!contentDispositionHeader.ToString().StartsWith("attachment"))
+ {
+ Assert.Fail("Content disposition is not attachment?");
+ }
+
+ if (!contentDispositionHeader.FileName.Equals(filename))
+ {
+ Assert.Fail($"filename is wrong.\n\nActual result: {contentDispositionHeader.FileName}\nExpected result: {filename}");
+ }
+ }
+
+ [Given("response attachment is saved as a file \"(.*)\"")]
+ public void GivenResponseAttachmentIsSavedAs(string filePath)
+ {
+ if (_apiContext.Response is null) throw new Exception("Http response is empty.");
+ var fullPath = Path.GetFullPath(Path.Join(Directory.GetCurrentDirectory(), filePath));
+ Directory.CreateDirectory(Path.GetDirectoryName(fullPath));
+
+ var responseContentByteArray = _apiContext.Response.Content.ReadAsByteArrayAsync().Result;
+ File.WriteAllBytes(fullPath, responseContentByteArray);
+ }
+
+ [Then("response json path \"(.*)\" value should match regex \"(.*)\"")]
+ public void ThenResponseJsonPathValueShouldMatchRegexp(string jsonPath, string regex)
+ {
+ var actualJTokens = GetActualJTokensFromResponse(jsonPath);
+ if (actualJTokens.Count != 1)
+ {
+ Assert.Fail($"Error! To check regexp match, json path should return single value. Number of returned values is {actualJTokens.Count}");
+ }
+ var stringToCheck = actualJTokens[0].ToString();
+ if (!Regex.IsMatch(stringToCheck, regex))
+ {
+ Assert.Fail($"Response json value '{stringToCheck}' doesn't match regexp {regex}");
+ }
+ }
+
+ [Then("response json path \"(.*)\" value should (be|become):")]
+ [Then("response json path \"(.*)\" value should (be|become) \"(.*)\"")]
+ public void CheckResponseJsonPath(string jsonPath, AssertionType assertionType, string expected)
+ {
+ expected = AddSquareBrackets(expected);
+
+ JToken parsedExpectedJson;
+ try
+ {
+ parsedExpectedJson = JToken.Parse(expected);
+ }
+ catch (JsonReaderException e)
+ {
+ throw new ArgumentException($"Error while parsing \"{expected}\". Expected value should be a valid json", e);
+ }
+
+ var expectedJTokens = parsedExpectedJson.Select(token => token).ToList();
+
+ var actualJTokens = GetActualJTokensFromResponse(jsonPath);
+
+ if (actualJTokens.Count != expectedJTokens.Count)
+ {
+ if (assertionType == AssertionType.Become)
+ {
+ Policy.HandleResult(count => !count.Equals(expectedJTokens.Count))
+ .WaitAndRetry(_retryCount, _ => _retryDelay).Execute(() =>
+ {
+ _httpService.SendContextRequest();
+ actualJTokens = GetActualJTokensFromResponse(jsonPath);
+ return actualJTokens.Count;
+ });
+ }
+
+ if (actualJTokens.Count != expectedJTokens.Count)
+ FailJTokensAssertion(actualJTokens, expectedJTokens, "Elements count mismatch.");
+ }
+
+ if (!IsJTokenListItemsAreTheSame(expectedJTokens, actualJTokens))
+ {
+ if (assertionType == AssertionType.Become)
+ {
+ Policy.HandleResult>(_ => !IsJTokenListItemsAreTheSame(expectedJTokens, actualJTokens))
+ .WaitAndRetry(_retryCount, _ => _retryDelay).Execute(() =>
+ {
+ _httpService.SendContextRequest();
+ actualJTokens = GetActualJTokensFromResponse(jsonPath);
+ return actualJTokens;
+ });
+ }
+
+ if (!IsJTokenListItemsAreTheSame(expectedJTokens, actualJTokens))
+ FailJTokensAssertion(actualJTokens, expectedJTokens,
+ "The actual result is not equal to the expected result.");
+ }
+ }
+
+ [Then("response json path \"(.*)\" value should not (be|become) empty")]
+ public void CheckResponseJsonPathNotEmpty(string jsonPath, AssertionType assertionType)
+ {
+ var actualJTokens = GetActualJTokensFromResponse(jsonPath);
+
+ if (actualJTokens.Count == 0)
+ {
+ if (assertionType == AssertionType.Become)
+ {
+ Policy.HandleResult(count => count == 0).WaitAndRetry(_retryCount, _ => _retryDelay).Execute(() =>
+ {
+ _httpService.SendContextRequest();
+ actualJTokens = GetActualJTokensFromResponse(jsonPath);
+ return actualJTokens.Count;
+ });
+ }
+
+ if (actualJTokens.Count == 0) Assert.Fail("Expected response json path value is empty");
+ }
+ }
+
+ [Then("response json path \"(.*)\" count should be \"(\\d*)\"")]
+ public void ThenResponseJsonPathValueShouldBecome(string jsonPath, int expectedQuantity)
+ {
+ var actualQuantity = GetActualJTokensFromResponse(jsonPath).Count;
+ Assert.AreEqual(expectedQuantity, actualQuantity);
+ }
+
+
+ [Given("expected response status code is \"(\\d*)\"")]
+ public void ChangeResponseStatusCode(int statusCode)
+ {
+ _apiContext.ExpectedStatusCode = statusCode;
+ }
+
+ [Then("response time is less then \"(.*)\" millis")]
+ public void ThenResponseTimeIsLessThenMillis(string timeoutString)
+ {
+ var timeout = Convert.ToInt64(timeoutString);
+ Assert.Less(_apiContext.ResponseTimeMillis, timeout,
+ $"API response time should be less then {timeout}, but was {_apiContext.ResponseTimeMillis}");
+ }
+
+ [Then("response json path \"(.*)\" should be equal to the file \"(.*)\"")]
+ public void ThenTheResponseJsonPathShouldBeEqualToFile(string jsonPath, string filePath)
+ {
+ var fullPath = Path.GetFullPath(Path.Join(Directory.GetCurrentDirectory(), filePath))
+ .NormalizePathAccordingOs();
+ if (!File.Exists(fullPath))
+ {
+ throw new FileNotFoundException("The file does not exist", fullPath);
+ }
+
+ var actualJTokens = GetActualJTokensFromResponse(jsonPath);
+ var expectedJTokens = GetExpectedJTokensFromFile(fullPath);
+ if (!IsJTokenListItemsAreTheSame(expectedJTokens, actualJTokens))
+ FailJTokensAssertion(actualJTokens, expectedJTokens,
+ "The actual result is not equal to the expected result.");
+ }
+
+ [Then("response should be \"(.*)\"")]
+ [Then("response should be:")]
+ public void ThenResponseValueShouldBe(string expected)
+ {
+ var responseString = _apiContext.Response.Content.ReadAsStringAsync().Result;
+ Assert.That(responseString, Is.EqualTo(expected));
+ }
+
+ private static bool IsJTokenListItemsAreTheSame(List expectedJTokens, List actualJTokens)
+ {
+ if (expectedJTokens.Count == 0 && actualJTokens.Count == 0) return true;
+ bool areEqual = false;
+ foreach (var expectedJToken in expectedJTokens)
+ {
+ foreach (var actualJToken in actualJTokens)
+ {
+ if (JToken.DeepEquals(expectedJToken, actualJToken))
+ {
+ areEqual = true;
+ break;
+ }
+
+ areEqual = false;
+ }
+
+ if (!areEqual)
+ {
+ return false;
+ }
+ }
+
+ return areEqual;
+ }
+
+ private List GetActualJTokensFromResponse(string jsonPath)
+ {
+ if (_apiContext.Response is null) throw new Exception("Http response is empty.");
+ var responseString = _apiContext.Response.Content.ReadAsStringAsync().Result;
+
+ JToken responseJToken;
+ try
+ {
+ responseJToken = JToken.Parse(responseString);
+ }
+ catch (JsonReaderException e)
+ {
+ throw new ArgumentException("Response content is not a valid json", e);
+ }
+
+ var actualJTokens = responseJToken.SelectTokens(jsonPath, false).ToList();
+ return actualJTokens;
+ }
+
+ private static string AddSquareBrackets(string expected)
+ {
+ if (!expected.Trim().StartsWith("["))
+ {
+ expected = $"[{expected}";
+ }
+
+ if (!expected.Trim().EndsWith("]"))
+ {
+ expected = $"{expected}]";
+ }
+
+ return expected;
+ }
+
+ private static void FailJTokensAssertion(List actualJTokens, List expectedJTokens,
+ string? message = null)
+ {
+ var actualJson = JsonConvert.SerializeObject(actualJTokens);
+ var expectedJson = JsonConvert.SerializeObject(expectedJTokens);
+ message = string.IsNullOrEmpty(message) ? message : message + Environment.NewLine;
+ Assert.Fail($"{message}Actual: {actualJson}{Environment.NewLine}Expected: {expectedJson}");
+ }
+
+ private List GetExpectedJTokensFromFile(string filePath)
+ {
+ var expectedString = File.ReadAllText(filePath);
+
+ JToken responseJToken;
+ try
+ {
+ responseJToken = JToken.Parse(expectedString);
+ }
+ catch (JsonReaderException e)
+ {
+ throw new ArgumentException($"File {filePath} is not a valid json", e);
+ }
+
+ return responseJToken.ToList();
+ }
+}
\ No newline at end of file
diff --git a/Behavioral.Automation.API/Bindings/SaveToContextSteps.cs b/Behavioral.Automation.API/Bindings/SaveToContextSteps.cs
new file mode 100644
index 00000000..cc8bbf61
--- /dev/null
+++ b/Behavioral.Automation.API/Bindings/SaveToContextSteps.cs
@@ -0,0 +1,71 @@
+using Behavioral.Automation.API.Context;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+using NUnit.Framework;
+using TechTalk.SpecFlow;
+using TechTalk.SpecFlow.Infrastructure;
+
+namespace Behavioral.Automation.API.Bindings;
+
+[Binding]
+public class SaveToContextSteps
+{
+ private readonly ApiContext _apiContext;
+ private readonly ScenarioContext _scenarioContext;
+ private readonly ISpecFlowOutputHelper _specFlowOutputHelper;
+
+ public SaveToContextSteps(ApiContext apiContext, ScenarioContext scenarioContext, ISpecFlowOutputHelper specFlowOutputHelper)
+ {
+ _apiContext = apiContext;
+ _scenarioContext = scenarioContext;
+ _specFlowOutputHelper = specFlowOutputHelper;
+ }
+
+ [Given("save response json path \"(.*)\" as \"(.*)\"")]
+ [When("save response json path \"(.*)\" as \"(.*)\"")]
+ public void SaveResponseJsonPathAs(string jsonPath, string variableName)
+ {
+ var stringToSave = GetStringByJsonPath(jsonPath);
+ _scenarioContext.Add(variableName, stringToSave);
+ _specFlowOutputHelper.WriteLine($"Saved '{stringToSave}' with key '{variableName}' in scenario context");
+ }
+
+ private string GetStringByJsonPath(string jsonPath)
+ {
+ var actualJTokens = GetActualJTokensFromResponse(jsonPath);
+ var stringToSave = ConvertJTokensToString(actualJTokens);
+ if (stringToSave.Equals("[]"))
+ {
+ Assert.Fail($"Empty value by jsonpath {jsonPath}. Can't save empty string in scenario context.");
+ }
+ return stringToSave;
+ }
+
+ private string ConvertJTokensToString(List tokens)
+ {
+ return tokens.Count == 1 ? tokens[0].ToString() : JsonConvert.SerializeObject(tokens);
+ }
+
+ private List GetActualJTokensFromResponse(string jsonPath)
+ {
+ if (_apiContext.Response is null) throw new NullReferenceException("Http response is empty.");
+ var responseString = _apiContext.Response.Content.ReadAsStringAsync().Result;
+
+ JToken responseJToken;
+ try
+ {
+ responseJToken = JToken.Parse(responseString);
+ }
+ catch (JsonReaderException e)
+ {
+ throw new ArgumentException("Response content is not a valid json", e);
+ }
+
+ var actualJTokens = responseJToken.SelectTokens(jsonPath, false).ToList();
+ if (actualJTokens == null)
+ {
+ Assert.Fail($"No value by json path: {jsonPath}");
+ }
+ return actualJTokens;
+ }
+}
\ No newline at end of file
diff --git a/Behavioral.Automation.API/Configs/Config.cs b/Behavioral.Automation.API/Configs/Config.cs
new file mode 100644
index 00000000..491b1bc3
--- /dev/null
+++ b/Behavioral.Automation.API/Configs/Config.cs
@@ -0,0 +1,9 @@
+using Microsoft.Extensions.Configuration;
+
+namespace Behavioral.Automation.API.Configs;
+
+public class Config
+{
+ [ConfigurationKeyName("API_HOST")]
+ public string ApiHost { get; set; }
+}
\ No newline at end of file
diff --git a/Behavioral.Automation.API/Context/ApiContext.cs b/Behavioral.Automation.API/Context/ApiContext.cs
new file mode 100644
index 00000000..6a97d5f2
--- /dev/null
+++ b/Behavioral.Automation.API/Context/ApiContext.cs
@@ -0,0 +1,9 @@
+namespace Behavioral.Automation.API.Context;
+
+public class ApiContext
+{
+ public HttpRequestMessage Request { get; set; }
+ public HttpResponseMessage Response { get; set; }
+ public long ResponseTimeMillis { get; set; }
+ public int? ExpectedStatusCode { get; set; }
+}
\ No newline at end of file
diff --git a/Behavioral.Automation.API/Hooks.cs b/Behavioral.Automation.API/Hooks.cs
new file mode 100644
index 00000000..dcc30cbb
--- /dev/null
+++ b/Behavioral.Automation.API/Hooks.cs
@@ -0,0 +1,23 @@
+using Behavioral.Automation.API.Services;
+using BoDi;
+using TechTalk.SpecFlow;
+
+namespace Behavioral.Automation.API;
+
+[Binding]
+public class Hooks
+{
+ private readonly IObjectContainer _objectContainer;
+
+ public Hooks(IObjectContainer objectContainer)
+ {
+ _objectContainer = objectContainer;
+ }
+
+ [BeforeScenario(Order = 0)]
+ public void Bootstrap()
+ {
+ _objectContainer.RegisterTypeAs();
+ }
+
+}
\ No newline at end of file
diff --git a/Behavioral.Automation.API/Models/AssertionType.cs b/Behavioral.Automation.API/Models/AssertionType.cs
new file mode 100644
index 00000000..32f33c22
--- /dev/null
+++ b/Behavioral.Automation.API/Models/AssertionType.cs
@@ -0,0 +1,7 @@
+namespace Behavioral.Automation.API.Models;
+
+public enum AssertionType
+{
+ Be,
+ Become
+}
\ No newline at end of file
diff --git a/Behavioral.Automation.API/Models/HttpParameters.cs b/Behavioral.Automation.API/Models/HttpParameters.cs
new file mode 100644
index 00000000..263c9e05
--- /dev/null
+++ b/Behavioral.Automation.API/Models/HttpParameters.cs
@@ -0,0 +1,14 @@
+namespace Behavioral.Automation.API.Models;
+
+public class HttpParameters
+{
+ public string Name;
+ public string Value;
+ public string Kind;
+}
+
+public enum RequestParameterKind
+{
+ Param,
+ Header
+}
\ No newline at end of file
diff --git a/Behavioral.Automation.API/Services/HttpApiClient.cs b/Behavioral.Automation.API/Services/HttpApiClient.cs
new file mode 100644
index 00000000..48c747d8
--- /dev/null
+++ b/Behavioral.Automation.API/Services/HttpApiClient.cs
@@ -0,0 +1,19 @@
+using TechTalk.SpecFlow.Infrastructure;
+
+namespace Behavioral.Automation.API.Services;
+
+public class HttpApiClient : IHttpApiClient
+{
+ private readonly ISpecFlowOutputHelper _specFlowOutputHelper;
+
+ public HttpApiClient(ISpecFlowOutputHelper specFlowOutputHelper)
+ {
+ _specFlowOutputHelper = specFlowOutputHelper;
+ }
+
+ public HttpResponseMessage SendHttpRequest(HttpRequestMessage httpRequestMessage)
+ {
+ HttpClient client = new(new LoggingHandler(new HttpClientHandler(), _specFlowOutputHelper));
+ return client.Send(httpRequestMessage);
+ }
+}
\ No newline at end of file
diff --git a/Behavioral.Automation.API/Services/HttpClientService.cs b/Behavioral.Automation.API/Services/HttpClientService.cs
new file mode 100644
index 00000000..261300c7
--- /dev/null
+++ b/Behavioral.Automation.API/Services/HttpClientService.cs
@@ -0,0 +1,36 @@
+using System.Net;
+using Behavioral.Automation.API.Context;
+using FluentAssertions;
+using NUnit.Framework;
+
+namespace Behavioral.Automation.API.Services;
+
+public class HttpService
+{
+ private readonly IHttpApiClient _client;
+ private readonly ApiContext _apiContext;
+
+ public HttpService(IHttpApiClient client, ApiContext apiContext)
+ {
+ _client = client;
+ _apiContext = apiContext;
+ }
+
+ public HttpResponseMessage SendContextRequest()
+ {
+ var watch = System.Diagnostics.Stopwatch.StartNew();
+ _apiContext.Response = _client.SendHttpRequest(_apiContext.Request.Clone());
+ watch.Stop();
+ if (_apiContext.ExpectedStatusCode is null)
+ {
+ Assert.That((int)_apiContext.Response.StatusCode, Is.InRange(200, 299), "Response status code is not success.");
+ }
+ else
+ {
+ Assert.That((int)_apiContext.Response.StatusCode, Is.EqualTo(_apiContext.ExpectedStatusCode));
+ _apiContext.ExpectedStatusCode = null;
+ }
+ _apiContext.ResponseTimeMillis = watch.ElapsedMilliseconds; //save TTFB + Content download time
+ return _apiContext.Response;
+ }
+}
\ No newline at end of file
diff --git a/Behavioral.Automation.API/Services/HttpRequestMessageExtension.cs b/Behavioral.Automation.API/Services/HttpRequestMessageExtension.cs
new file mode 100644
index 00000000..b39a6e1b
--- /dev/null
+++ b/Behavioral.Automation.API/Services/HttpRequestMessageExtension.cs
@@ -0,0 +1,24 @@
+namespace Behavioral.Automation.API.Services;
+
+public static class HttpRequestMessageExtension
+{
+ public static HttpRequestMessage Clone(this HttpRequestMessage req)
+ {
+ var clone = new HttpRequestMessage(req.Method, req.RequestUri);
+
+ clone.Content = req.Content;
+ clone.Version = req.Version;
+
+ foreach (KeyValuePair prop in req.Properties)
+ {
+ clone.Properties.Add(prop);
+ }
+
+ foreach (KeyValuePair> header in req.Headers)
+ {
+ clone.Headers.TryAddWithoutValidation(header.Key, header.Value);
+ }
+
+ return clone;
+ }
+}
\ No newline at end of file
diff --git a/Behavioral.Automation.API/Services/IHttpApiClient.cs b/Behavioral.Automation.API/Services/IHttpApiClient.cs
new file mode 100644
index 00000000..736549f6
--- /dev/null
+++ b/Behavioral.Automation.API/Services/IHttpApiClient.cs
@@ -0,0 +1,6 @@
+namespace Behavioral.Automation.API.Services;
+
+public interface IHttpApiClient
+{
+ public HttpResponseMessage SendHttpRequest(HttpRequestMessage httpRequestMessage);
+}
\ No newline at end of file
diff --git a/Behavioral.Automation.API/Services/LoggingHandler.cs b/Behavioral.Automation.API/Services/LoggingHandler.cs
new file mode 100644
index 00000000..69933bfc
--- /dev/null
+++ b/Behavioral.Automation.API/Services/LoggingHandler.cs
@@ -0,0 +1,30 @@
+using TechTalk.SpecFlow.Infrastructure;
+
+namespace Behavioral.Automation.API.Services;
+
+public class LoggingHandler : DelegatingHandler
+{
+ private readonly ISpecFlowOutputHelper _specFlowOutputHelper;
+
+ public LoggingHandler(HttpMessageHandler innerHandler, ISpecFlowOutputHelper specFlowOutputHelper)
+ : base(innerHandler)
+ {
+ _specFlowOutputHelper = specFlowOutputHelper;
+ }
+
+ protected override HttpResponseMessage Send(HttpRequestMessage request, CancellationToken cancellationToken)
+ {
+ _specFlowOutputHelper.WriteLine($"Request:{Environment.NewLine}{request}");
+ if (request.Content != null)
+ {
+ _specFlowOutputHelper.WriteLine(request.Content.ReadAsStringAsync(cancellationToken).Result);
+ }
+
+ HttpResponseMessage response = base.Send(request, cancellationToken);
+
+ _specFlowOutputHelper.WriteLine($"Response:{Environment.NewLine}{response}");
+ _specFlowOutputHelper.WriteLine(response.Content.ReadAsStringAsync(cancellationToken).Result);
+
+ return response;
+ }
+}
\ No newline at end of file
diff --git a/Behavioral.Automation.API/StepArgumentTransformations.cs b/Behavioral.Automation.API/StepArgumentTransformations.cs
new file mode 100644
index 00000000..ef2802e3
--- /dev/null
+++ b/Behavioral.Automation.API/StepArgumentTransformations.cs
@@ -0,0 +1,22 @@
+using Behavioral.Automation.API.Models;
+using TechTalk.SpecFlow;
+
+namespace Behavioral.Automation.API;
+
+[Binding]
+public class StepArgumentTransformations
+{
+ [StepArgumentTransformation]
+ public AssertionType ParseBehavior(string verb)
+ {
+ switch (verb)
+ {
+ case "be":
+ return AssertionType.Be;
+ case "become":
+ return AssertionType.Become;
+ default:
+ throw new ArgumentException($"Unknown behaviour verb: {verb}");
+ }
+ }
+}
\ No newline at end of file
diff --git a/Behavioral.Automation.Playwright/Behavioral.Automation.Playwright.DemoScenarios/AutomationConfig.json b/Behavioral.Automation.Playwright/Behavioral.Automation.Playwright.DemoScenarios/AutomationConfig.json
new file mode 100644
index 00000000..00f8b4c4
--- /dev/null
+++ b/Behavioral.Automation.Playwright/Behavioral.Automation.Playwright.DemoScenarios/AutomationConfig.json
@@ -0,0 +1,8 @@
+{
+ "BASE_URL": "https://www.saucedemo.com/",
+ "SEARCH_ATTRIBUTE": "data-test",
+ "ASSERT_TIMEOUT_MILLISECONDS" : 30,
+ "SLOW_MO_MILLISECONDS" : 150,
+ "HEADLESS" : false,
+ "RECORD_VIDEO" : false
+}
\ No newline at end of file
diff --git a/Behavioral.Automation.Playwright/Behavioral.Automation.Playwright.DemoScenarios/Behavioral.Automation.Playwright.DemoScenarios.csproj b/Behavioral.Automation.Playwright/Behavioral.Automation.Playwright.DemoScenarios/Behavioral.Automation.Playwright.DemoScenarios.csproj
new file mode 100644
index 00000000..30491825
--- /dev/null
+++ b/Behavioral.Automation.Playwright/Behavioral.Automation.Playwright.DemoScenarios/Behavioral.Automation.Playwright.DemoScenarios.csproj
@@ -0,0 +1,32 @@
+
+
+
+ net6.0
+ enable
+ enable
+ Behavioral.Automation.Demo
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Always
+
+
+ Always
+
+
+
+
diff --git a/Behavioral.Automation.Playwright/Behavioral.Automation.Playwright.DemoScenarios/Features/Saucedemo.feature b/Behavioral.Automation.Playwright/Behavioral.Automation.Playwright.DemoScenarios/Features/Saucedemo.feature
new file mode 100644
index 00000000..c5569d95
--- /dev/null
+++ b/Behavioral.Automation.Playwright/Behavioral.Automation.Playwright.DemoScenarios/Features/Saucedemo.feature
@@ -0,0 +1,9 @@
+Feature: Saucedemo
+
+ Scenario: Purchase
+ Given application URL is opened
+ And user entered "standard_user" into "Username"
+ And user entered "secret_sauce" into "Password"
+ When user clicks on "Login button"
+ And user clicks on "Add Backpack to Cart"
+ Then the "Shopping cart badge" text should be "1"
\ No newline at end of file
diff --git a/Behavioral.Automation.Playwright/Behavioral.Automation.Playwright.DemoScenarios/specflow.json b/Behavioral.Automation.Playwright/Behavioral.Automation.Playwright.DemoScenarios/specflow.json
new file mode 100644
index 00000000..d529ef50
--- /dev/null
+++ b/Behavioral.Automation.Playwright/Behavioral.Automation.Playwright.DemoScenarios/specflow.json
@@ -0,0 +1,16 @@
+{
+ "bindingCulture": {
+ "language": "en-us"
+ },
+ "language": {
+ "feature": "en-us"
+ },
+ "runtime": {
+ "missingOrPendingStepsOutcome": "Error"
+ },
+ "stepAssemblies": [
+ {
+ "assembly": "Behavioral.Automation.Playwright"
+ }
+ ]
+}
diff --git a/Behavioral.Automation.Playwright/Behavioral.Automation.Playwright/Behavioral.Automation.Playwright.csproj b/Behavioral.Automation.Playwright/Behavioral.Automation.Playwright/Behavioral.Automation.Playwright.csproj
index dd3cb6dd..15121820 100644
--- a/Behavioral.Automation.Playwright/Behavioral.Automation.Playwright/Behavioral.Automation.Playwright.csproj
+++ b/Behavioral.Automation.Playwright/Behavioral.Automation.Playwright/Behavioral.Automation.Playwright.csproj
@@ -29,15 +29,10 @@
-
-
-
-
-
-
+
diff --git a/Behavioral.Automation.Playwright/Behavioral.Automation.Playwright/Bindings/AttributeBinding.cs b/Behavioral.Automation.Playwright/Behavioral.Automation.Playwright/Bindings/AttributeBinding.cs
index 741e73e9..8c0381e7 100644
--- a/Behavioral.Automation.Playwright/Behavioral.Automation.Playwright/Bindings/AttributeBinding.cs
+++ b/Behavioral.Automation.Playwright/Behavioral.Automation.Playwright/Bindings/AttributeBinding.cs
@@ -1,6 +1,6 @@
using System.Linq;
using System.Threading.Tasks;
-using Behavioral.Automation.Playwright.WebElementsWrappers.Interface;
+using Behavioral.Automation.Playwright.WebElementsWrappers;
using Microsoft.Playwright;
using TechTalk.SpecFlow;
@@ -25,7 +25,7 @@ public AttributeBinding(ElementTransformations.ElementTransformations elementTra
/// Then "Test" input should be enabled
[Given(@"the ""(.+?)"" is (enabled|disabled)")]
[Then(@"the ""(.+?)"" should be| (enabled|disabled)")]
- public async Task CheckElementIsDisabled(IWebElementWrapper element, bool enabled)
+ public async Task CheckElementIsDisabled(WebElementWrapper element, bool enabled)
{
if (enabled)
{
diff --git a/Behavioral.Automation.Playwright/Behavioral.Automation.Playwright/Bindings/ClickBinding.cs b/Behavioral.Automation.Playwright/Behavioral.Automation.Playwright/Bindings/ClickBinding.cs
index cdadf9e8..0f9aafa3 100644
--- a/Behavioral.Automation.Playwright/Behavioral.Automation.Playwright/Bindings/ClickBinding.cs
+++ b/Behavioral.Automation.Playwright/Behavioral.Automation.Playwright/Bindings/ClickBinding.cs
@@ -1,5 +1,5 @@
using System.Threading.Tasks;
-using Behavioral.Automation.Playwright.WebElementsWrappers.Interface;
+using Behavioral.Automation.Playwright.WebElementsWrappers;
using TechTalk.SpecFlow;
namespace Behavioral.Automation.Playwright.Bindings;
@@ -14,7 +14,7 @@ public class ClickBinding
/// When user clicks on "Test" button
[Given(@"user clicked on ""(.+?)""")]
[When(@"user clicks on ""(.+?)""")]
- public async Task ClickOnElement(IWebElementWrapper element)
+ public async Task ClickOnElement(WebElementWrapper element)
{
await element.Locator.ClickAsync();
}
@@ -26,7 +26,7 @@ public async Task ClickOnElement(IWebElementWrapper element)
/// When user clicks twice on "Test" button
[Given(@"user clicked twice on ""(.+?)""")]
[When(@"user clicks twice on ""(.+?)""")]
- public async Task ClickTwiceOnElement(IWebElementWrapper element)
+ public async Task ClickTwiceOnElement(WebElementWrapper element)
{
await element.Locator.DblClickAsync();
}
@@ -39,7 +39,7 @@ public async Task ClickTwiceOnElement(IWebElementWrapper element)
/// When user clicks at first element among "Test" buttons (note that numbers from 1 to 10 can be written as words)When user hovers mouse over "Test" button
[Given(@"user hovered mouse over ""(.+?)""")]
[When(@"user hovers mouse over ""(.+?)""")]
- public async Task HoverMouse(IWebElementWrapper element)
+ public async Task HoverMouse(WebElementWrapper element)
{
await element.Locator.HoverAsync();
}
diff --git a/Behavioral.Automation.Playwright/Behavioral.Automation.Playwright/Bindings/DropdownBinding.cs b/Behavioral.Automation.Playwright/Behavioral.Automation.Playwright/Bindings/DropdownBinding.cs
index 8575d858..8215ed11 100644
--- a/Behavioral.Automation.Playwright/Behavioral.Automation.Playwright/Bindings/DropdownBinding.cs
+++ b/Behavioral.Automation.Playwright/Behavioral.Automation.Playwright/Bindings/DropdownBinding.cs
@@ -2,7 +2,7 @@
using System.Linq;
using System.Threading.Tasks;
using Behavioral.Automation.Playwright.Utils;
-using Behavioral.Automation.Playwright.WebElementsWrappers.Interface;
+using Behavioral.Automation.Playwright.WebElementsWrappers;
using Microsoft.Playwright;
using NUnit.Framework;
using TechTalk.SpecFlow;
@@ -19,7 +19,7 @@ public class DropdownBinding
/// Tested web element wrapper
[Given(@"user selected ""(.+?)"" in ""(.+?)""")]
[When(@"user selects ""(.+?)"" in ""(.+?)""")]
- public async Task SelectValueInDropdown(string entry, IDropdownWrapper element)
+ public async Task SelectValueInDropdown(string entry, DropdownWrapper element)
{
await element.SelectValue(entry);
}
@@ -33,7 +33,7 @@ public async Task SelectValueInDropdown(string entry, IDropdownWrapper element)
/// Given the "Test" dropdown selected value is "Test value"
[Given(@"the ""(.+?)"" selected value is ""(.+?)""")]
[Then(@"the ""(.+?)"" selected value should be ""(.+?)""")]
- public async Task CheckSelectedValue(IDropdownWrapper wrapper, string value)
+ public async Task CheckSelectedValue(DropdownWrapper wrapper, string value)
{
await Assertions.Expect(wrapper.Locator).ToHaveValueAsync(value);
}
@@ -51,7 +51,7 @@ public async Task CheckSelectedValue(IDropdownWrapper wrapper, string value)
///
[Given(@"the ""(.+?)"" has the following values:")]
[Then(@"the ""(.*?)"" should have the following values:")]
- public async Task CheckAllItems(IDropdownWrapper wrapper, Table items)
+ public async Task CheckAllItems(DropdownWrapper wrapper, Table items)
{
await CheckDropdownElements(wrapper, items, $"{wrapper.Caption} values");
}
@@ -66,7 +66,7 @@ public async Task CheckAllItems(IDropdownWrapper wrapper, Table items)
[Given(@"the ""(.+?)"" (contains|does not contain) ""(.+?)""")]
[Then(@"the ""(.+?)"" should (contain|not contain) ""(.+?)""")]
public async Task CheckDropdownContainsItems(
- IDropdownWrapper wrapper,
+ DropdownWrapper wrapper,
string behavior,
string value)
{
@@ -96,7 +96,7 @@ public async Task CheckDropdownContainsItems(
///
[Given(@"the ""(.+?)"" (contains|does not contain) the following values:")]
[Then(@"the ""(.+?)"" should (contain|not contain) the following values:")]
- public async Task CheckDropdownContainsMultipleItems(IDropdownWrapper wrapper, string behavior, Table table)
+ public async Task CheckDropdownContainsMultipleItems(DropdownWrapper wrapper, string behavior, Table table)
{
foreach (var row in table.Rows)
{
@@ -117,7 +117,7 @@ public async Task CheckDropdownContainsMultipleItems(IDropdownWrapper wrapper, s
///
[Given(@"user selected multiple entries in ""(.+?)"":")]
[When(@"user selects multiple entries in ""(.+?)"":")]
- public void ClickOnMultipleEntries(IDropdownWrapper wrapper, Table entries)
+ public void ClickOnMultipleEntries(DropdownWrapper wrapper, Table entries)
{
wrapper.SelectValue(entries.Rows.Select(x => x.Values.First()).ToArray());
}
@@ -133,7 +133,7 @@ public void ClickOnMultipleEntries(IDropdownWrapper wrapper, Table entries)
///
[Given(@"the ""(.+?)"" value is (enabled|disabled) in ""(.+?)""")]
[Then(@"the ""(.+?)"" value should be (enabled|disabled) in ""(.+?)""")]
- public async Task CheckValueInDropdownIsEnabled(string value, bool enabled, IDropdownWrapper wrapper)
+ public async Task CheckValueInDropdownIsEnabled(string value, bool enabled, DropdownWrapper wrapper)
{
var optionToCheck = wrapper.GetOption(value);
if (enabled)
@@ -160,7 +160,7 @@ public async Task CheckValueInDropdownIsEnabled(string value, bool enabled, IDro
[Given(@"the following values are (enabled|disabled) in ""(.+?)"":")]
[Then(@"the following values should be (enabled|disabled) in ""(.+?)"":")]
public async Task CheckMultipleValuesInDropdownAreEnabled(bool enabled,
- IDropdownWrapper wrapper, Table table)
+ DropdownWrapper wrapper, Table table)
{
foreach (var row in table.Rows)
{
@@ -168,7 +168,7 @@ public async Task CheckMultipleValuesInDropdownAreEnabled(bool enabled,
}
}
- private async Task CheckDropdownElements(IDropdownWrapper wrapper, Table expectedValues, string valueType)
+ private async Task CheckDropdownElements(DropdownWrapper wrapper, Table expectedValues, string valueType)
{
for (var i = 0; i < expectedValues.Rows.Count; i++)
{
diff --git a/Behavioral.Automation.Playwright/Behavioral.Automation.Playwright/Bindings/ElementCollectionBinding.cs b/Behavioral.Automation.Playwright/Behavioral.Automation.Playwright/Bindings/ElementCollectionBinding.cs
index 55915b1c..c405b089 100644
--- a/Behavioral.Automation.Playwright/Behavioral.Automation.Playwright/Bindings/ElementCollectionBinding.cs
+++ b/Behavioral.Automation.Playwright/Behavioral.Automation.Playwright/Bindings/ElementCollectionBinding.cs
@@ -2,7 +2,7 @@
using System.Linq;
using System.Threading.Tasks;
using Behavioral.Automation.Playwright.Services;
-using Behavioral.Automation.Playwright.WebElementsWrappers.Interface;
+using Behavioral.Automation.Playwright.WebElementsWrappers;
using NUnit.Framework;
using TechTalk.SpecFlow;
@@ -20,7 +20,7 @@ public ElementCollectionBinding(LocatorStorageService locatorStorageService)
[Given(@"user clicked on the ""(.+?)"" with ""(.+?)"" text")]
[When(@"user clicks on the ""(.+?)"" with ""(.+?)"" text")]
- public async Task ClickOnElementByText(IWebElementWrapper element, string text)
+ public async Task ClickOnElementByText(WebElementWrapper element, string text)
{
var allTextContents = await element.Locator.AllTextContentsAsync();
var index = allTextContents.ToList().FindIndex(s => s.Equals(text, StringComparison.InvariantCultureIgnoreCase));
@@ -33,7 +33,7 @@ public async Task ClickOnElementByText(IWebElementWrapper element, string text)
[Given(@"user clicked on every ""(.*)""")]
[When(@"user clicks on every ""(.*)""")]
- public async Task ClickOnEveryElement(IWebElementWrapper element)
+ public async Task ClickOnEveryElement(WebElementWrapper element)
{
var count = await element.Locator.CountAsync();
for (var i = 0; i < count; i++)
@@ -44,7 +44,7 @@ public async Task ClickOnEveryElement(IWebElementWrapper element)
[Given(@"user clicked on ""(.+?)"" element among ""(.+?)""")]
[When(@"user clicks on ""(.+?)"" element among ""(.+?)""")]
- public async Task ClickOnElementByIndex(IWebElementWrapper element, int index)
+ public async Task ClickOnElementByIndex(WebElementWrapper element, int index)
{
await element.Locator.Nth(index).ClickAsync();
}
diff --git a/Behavioral.Automation.Playwright/Behavioral.Automation.Playwright/Bindings/InputBinding.cs b/Behavioral.Automation.Playwright/Behavioral.Automation.Playwright/Bindings/InputBinding.cs
index 02a5eca8..fdd7ba41 100644
--- a/Behavioral.Automation.Playwright/Behavioral.Automation.Playwright/Bindings/InputBinding.cs
+++ b/Behavioral.Automation.Playwright/Behavioral.Automation.Playwright/Bindings/InputBinding.cs
@@ -1,6 +1,6 @@
using System.Linq;
using System.Threading.Tasks;
-using Behavioral.Automation.Playwright.WebElementsWrappers.Interface;
+using Behavioral.Automation.Playwright.WebElementsWrappers;
using TechTalk.SpecFlow;
namespace Behavioral.Automation.Playwright.Bindings;
@@ -21,9 +21,9 @@ public InputBinding(ElementTransformations.ElementTransformations elementTransfo
/// String to enter
/// Tested web element wrapper
/// When user enters "test string" into "Test input"
- [Given(@"user entered (.+?) into ""(.+?)""")]
+ [Given(@"user entered ""(.+?)"" into ""(.+?)""")]
[When(@"user enters ""(.+?)"" into ""(.+?)""")]
- public async Task FillInput(string text, IWebElementWrapper element)
+ public async Task FillInput(string text, WebElementWrapper element)
{
await element.Locator.FillAsync(text);
}
diff --git a/Behavioral.Automation.Playwright/Behavioral.Automation.Playwright/Bindings/LabelBindings.cs b/Behavioral.Automation.Playwright/Behavioral.Automation.Playwright/Bindings/LabelBindings.cs
new file mode 100644
index 00000000..78689bc5
--- /dev/null
+++ b/Behavioral.Automation.Playwright/Behavioral.Automation.Playwright/Bindings/LabelBindings.cs
@@ -0,0 +1,16 @@
+using System.Threading.Tasks;
+using Behavioral.Automation.Playwright.WebElementsWrappers;
+using Microsoft.Playwright;
+using TechTalk.SpecFlow;
+
+namespace Behavioral.Automation.Playwright.Bindings;
+
+[Binding]
+public class LabelBindings
+{
+ [Then(@"the ""(.*)"" text should be ""(.*)""")]
+ public async Task ThenValueIs(WebElementWrapper element, string expectedString)
+ {
+ await Assertions.Expect(element.Locator).ToHaveTextAsync(expectedString);
+ }
+}
\ No newline at end of file
diff --git a/Behavioral.Automation.Playwright/Behavioral.Automation.Playwright/Bindings/TableBinding.cs b/Behavioral.Automation.Playwright/Behavioral.Automation.Playwright/Bindings/TableBinding.cs
index d1189cfd..72c7ed73 100644
--- a/Behavioral.Automation.Playwright/Behavioral.Automation.Playwright/Bindings/TableBinding.cs
+++ b/Behavioral.Automation.Playwright/Behavioral.Automation.Playwright/Bindings/TableBinding.cs
@@ -3,8 +3,7 @@
using System.Threading.Tasks;
using Behavioral.Automation.Configs;
using Behavioral.Automation.Playwright.Configs;
-using Behavioral.Automation.Playwright.Services;
-using Behavioral.Automation.Playwright.WebElementsWrappers.Interface;
+using Behavioral.Automation.Playwright.WebElementsWrappers;
using Microsoft.Playwright;
using NUnit.Framework;
using TechTalk.SpecFlow;
@@ -18,7 +17,7 @@ public class TableBinding
[Given(@"user clicked clicked on ""(.+?)"" header in ""(.+?)""")]
[When(@"user clicks clicked on ""(.+?)"" header in ""(.+?)""")]
- public async Task ClickOnHeaderCell(string headerTitle, ITableWrapper element)
+ public async Task ClickOnHeaderCell(string headerTitle, TableWrapper element)
{
var headerToClick = element.HeaderCells.Filter(new LocatorFilterOptions {HasTextString = headerTitle});
await headerToClick.ClickAsync();
@@ -26,7 +25,7 @@ public async Task ClickOnHeaderCell(string headerTitle, ITableWrapper element)
[Given(@"the ""(.+?)"" has the following rows (with|without) headers:")]
[Then(@"the ""(.+?)"" should have the following rows (with|without) headers:")]
- public async Task CheckTableRows(ITableWrapper element,string countingHeaders, Table expectedTable)
+ public async Task CheckTableRows(TableWrapper element,string countingHeaders, Table expectedTable)
{
var checkHeadersNeeded = countingHeaders != "without";
if (checkHeadersNeeded)
@@ -57,7 +56,7 @@ public async Task CheckTableRows(ITableWrapper element,string countingHeaders, T
[Given(@"the ""(.+?)"" contains the following rows (with|without) headers:")]
[Then(@"the ""(.+?)"" should contain the following rows (with|without) headers:")]
- public async Task CheckTableContainRows(ITableWrapper element, string countingHeaders, Table expectedTable)
+ public async Task CheckTableContainRows(TableWrapper element, string countingHeaders, Table expectedTable)
{
var checkHeadersNeeded = countingHeaders != "without";
if (checkHeadersNeeded)
@@ -87,7 +86,7 @@ public async Task CheckTableContainRows(ITableWrapper element, string countingHe
/// Then "Test" table should have 5 items
[Given(@"the ""(.+?)"" has ""(.+?)"" items")]
[Then(@"the ""(.+?)"" should have ""(.+?)"" items")]
- public async Task CheckTableItemsCount(ITableWrapper element, int expectedRowsCount)
+ public async Task CheckTableItemsCount(TableWrapper element, int expectedRowsCount)
{
await Assertions.Expect(element.Rows).ToHaveCountAsync(expectedRowsCount, new LocatorAssertionsToHaveCountOptions { Timeout = Timeout });
}
diff --git a/Behavioral.Automation.Playwright/Behavioral.Automation.Playwright/Bindings/VisibilityBinding.cs b/Behavioral.Automation.Playwright/Behavioral.Automation.Playwright/Bindings/VisibilityBinding.cs
index b58c177f..466ccbb9 100644
--- a/Behavioral.Automation.Playwright/Behavioral.Automation.Playwright/Bindings/VisibilityBinding.cs
+++ b/Behavioral.Automation.Playwright/Behavioral.Automation.Playwright/Bindings/VisibilityBinding.cs
@@ -1,6 +1,6 @@
using System.Linq;
using System.Threading.Tasks;
-using Behavioral.Automation.Playwright.WebElementsWrappers.Interface;
+using Behavioral.Automation.Playwright.WebElementsWrappers;
using Microsoft.Playwright;
using TechTalk.SpecFlow;
@@ -18,7 +18,7 @@ public VisibilityBinding(ElementTransformations.ElementTransformations elementTr
[Given(@"the ""(.+?)"" (is|is not) visible")]
[Then(@"the ""(.+?)"" should (be|be not) visible")]
- public async Task CheckElementVisibility(IWebElementWrapper element, string condition)
+ public async Task CheckElementVisibility(WebElementWrapper element, string condition)
{
if (!condition.Contains("not"))
{
diff --git a/Behavioral.Automation.Playwright/Behavioral.Automation.Playwright/Services/ElementSelectors/DropdownSelector.cs b/Behavioral.Automation.Playwright/Behavioral.Automation.Playwright/ElementSelectors/DropdownSelector.cs
similarity index 70%
rename from Behavioral.Automation.Playwright/Behavioral.Automation.Playwright/Services/ElementSelectors/DropdownSelector.cs
rename to Behavioral.Automation.Playwright/Behavioral.Automation.Playwright/ElementSelectors/DropdownSelector.cs
index a9a4a2db..6907908a 100644
--- a/Behavioral.Automation.Playwright/Behavioral.Automation.Playwright/Services/ElementSelectors/DropdownSelector.cs
+++ b/Behavioral.Automation.Playwright/Behavioral.Automation.Playwright/ElementSelectors/DropdownSelector.cs
@@ -1,4 +1,4 @@
-namespace Behavioral.Automation.Playwright.Services.ElementSelectors;
+namespace Behavioral.Automation.Playwright.ElementSelectors;
public class DropdownSelector : ElementSelector
{
diff --git a/Behavioral.Automation.Playwright/Behavioral.Automation.Playwright/ElementSelectors/ElementSelector.cs b/Behavioral.Automation.Playwright/Behavioral.Automation.Playwright/ElementSelectors/ElementSelector.cs
new file mode 100644
index 00000000..2620b311
--- /dev/null
+++ b/Behavioral.Automation.Playwright/Behavioral.Automation.Playwright/ElementSelectors/ElementSelector.cs
@@ -0,0 +1,8 @@
+namespace Behavioral.Automation.Playwright.ElementSelectors;
+
+public class ElementSelector
+{
+ public string? IdSelector { get; set; }
+
+ public string? Selector { get; set; }
+}
\ No newline at end of file
diff --git a/Behavioral.Automation.Playwright/Behavioral.Automation.Playwright/Services/ElementSelectors/TableSelector.cs b/Behavioral.Automation.Playwright/Behavioral.Automation.Playwright/ElementSelectors/TableSelector.cs
similarity index 79%
rename from Behavioral.Automation.Playwright/Behavioral.Automation.Playwright/Services/ElementSelectors/TableSelector.cs
rename to Behavioral.Automation.Playwright/Behavioral.Automation.Playwright/ElementSelectors/TableSelector.cs
index dffa5fb6..0c000150 100644
--- a/Behavioral.Automation.Playwright/Behavioral.Automation.Playwright/Services/ElementSelectors/TableSelector.cs
+++ b/Behavioral.Automation.Playwright/Behavioral.Automation.Playwright/ElementSelectors/TableSelector.cs
@@ -1,4 +1,4 @@
-namespace Behavioral.Automation.Playwright.Services.ElementSelectors;
+namespace Behavioral.Automation.Playwright.ElementSelectors;
public class TableSelector : ElementSelector
{
diff --git a/Behavioral.Automation.Playwright/Behavioral.Automation.Playwright/ElementTransformations/ElementTransformations.cs b/Behavioral.Automation.Playwright/Behavioral.Automation.Playwright/ElementTransformations/ElementTransformations.cs
index 840e9fac..b6d4fa0e 100644
--- a/Behavioral.Automation.Playwright/Behavioral.Automation.Playwright/ElementTransformations/ElementTransformations.cs
+++ b/Behavioral.Automation.Playwright/Behavioral.Automation.Playwright/ElementTransformations/ElementTransformations.cs
@@ -1,10 +1,8 @@
using Behavioral.Automation.Playwright.Context;
+using Behavioral.Automation.Playwright.ElementSelectors;
using Behavioral.Automation.Playwright.Services;
-using Behavioral.Automation.Playwright.Services.ElementSelectors;
using Behavioral.Automation.Playwright.Utils;
using Behavioral.Automation.Playwright.WebElementsWrappers;
-using Behavioral.Automation.Playwright.WebElementsWrappers.Interface;
-using JetBrains.Annotations;
using TechTalk.SpecFlow;
namespace Behavioral.Automation.Playwright.ElementTransformations;
@@ -13,42 +11,33 @@ namespace Behavioral.Automation.Playwright.ElementTransformations;
public class ElementTransformations
{
private readonly WebContext _webContext;
- private readonly ILocatorProvider _locatorProvider;
private readonly LocatorStorageService _locatorStorageService;
- public ElementTransformations(WebContext webContext, ILocatorProvider locatorProvider,
- LocatorStorageService locatorStorageService)
+ public ElementTransformations(WebContext webContext, LocatorStorageService locatorStorageService)
{
_webContext = webContext;
- _locatorProvider = locatorProvider;
_locatorStorageService = locatorStorageService;
}
[StepArgumentTransformation]
- public IWebElementWrapper GetElement(string caption)
+ public WebElementWrapper GetElement(string caption)
{
var selector = _locatorStorageService.Get(caption);
- return new WebElementWrapper(_webContext, _locatorProvider.GetLocator(selector), caption);
+ return new WebElementWrapper(_webContext, selector, caption);
}
[StepArgumentTransformation]
- public IDropdownWrapper GetDropdownElement(string caption)
+ public DropdownWrapper GetDropdownElement(string caption)
{
var dropdownSelector = _locatorStorageService.Get(caption);
- return new DropdownWrapper(_webContext, _locatorProvider.GetLocator(dropdownSelector.BaseElementSelector),
- _locatorProvider.GetLocator(dropdownSelector.ItemSelector), caption);
+ return new DropdownWrapper(_webContext, dropdownSelector, caption);
}
[StepArgumentTransformation]
- public ITableWrapper GetTableElement(string caption)
+ public TableWrapper GetTableElement(string caption)
{
var tableSelector = _locatorStorageService.Get(caption);
- return new TableWrapper(_webContext,
- _locatorProvider.GetLocator(tableSelector.BaseElementSelector),
- _locatorProvider.GetLocator(tableSelector.RowSelector),
- tableSelector.CellSelector,
- _locatorProvider.GetLocator(tableSelector.HeaderCellSelector),
- caption);
+ return new TableWrapper(_webContext, tableSelector, caption);
}
///
@@ -67,7 +56,7 @@ public bool ConvertEnabledState(string enabled)
///
/// String with the number which is received from Specflow steps
///
- [StepArgumentTransformation, NotNull]
+ [StepArgumentTransformation]
public int ParseNumber(string number)
{
return number switch
diff --git a/Behavioral.Automation.Playwright/Behavioral.Automation.Playwright/Hooks.cs b/Behavioral.Automation.Playwright/Behavioral.Automation.Playwright/Hooks.cs
index 8c46765d..2979f6dc 100644
--- a/Behavioral.Automation.Playwright/Behavioral.Automation.Playwright/Hooks.cs
+++ b/Behavioral.Automation.Playwright/Behavioral.Automation.Playwright/Hooks.cs
@@ -15,7 +15,6 @@ namespace Behavioral.Automation.Playwright;
[Binding]
public class Hooks
{
- private readonly IObjectContainer _objectContainer;
private readonly WebContext _webContext;
private static IPlaywright? _playwright;
private static IBrowser? _browser;
@@ -23,14 +22,11 @@ public class Hooks
private static readonly float? SlowMoMilliseconds = ConfigManager.GetConfig().SlowMoMilliseconds;
private static readonly bool? Headless = ConfigManager.GetConfig().Headless;
private static readonly bool RecordVideo = ConfigManager.GetConfig().RecordVideo;
- private readonly TestServicesBuilder _testServicesBuilder;
- public Hooks(WebContext webContext, ScenarioContext scenarioContext, IObjectContainer objectContainer)
+ public Hooks(WebContext webContext, ScenarioContext scenarioContext)
{
- _objectContainer = objectContainer;
_webContext = webContext;
_scenarioContext = scenarioContext;
- _testServicesBuilder = new TestServicesBuilder(objectContainer);
}
[BeforeTestRun]
@@ -103,4 +99,4 @@ await _webContext.Page.ScreenshotAsync(new PageScreenshotOptions
SlowMo = SlowMoMilliseconds,
});
}
-}
+}
\ No newline at end of file
diff --git a/Behavioral.Automation.Playwright/Behavioral.Automation.Playwright/Pages/MainPage.cs b/Behavioral.Automation.Playwright/Behavioral.Automation.Playwright/Pages/MainPage.cs
index bea30368..3b93ff4b 100644
--- a/Behavioral.Automation.Playwright/Behavioral.Automation.Playwright/Pages/MainPage.cs
+++ b/Behavioral.Automation.Playwright/Behavioral.Automation.Playwright/Pages/MainPage.cs
@@ -1,5 +1,6 @@
using Behavioral.Automation.Configs;
using Behavioral.Automation.Playwright.Configs;
+using Behavioral.Automation.Playwright.ElementSelectors;
namespace Behavioral.Automation.Playwright.Pages;
@@ -7,4 +8,14 @@ class MainPage : ISelectorStorage
{
private static readonly string Id = ConfigManager.GetConfig().SearchAttribute;
+ //Login
+ public ElementSelector Username = new() { IdSelector = "username"};
+ public ElementSelector Password = new() { IdSelector = "password"};
+ public ElementSelector LoginButton = new() { IdSelector = "login-button"};
+
+ //Items
+ public ElementSelector AddBackpackToCart = new() { IdSelector = "add-to-cart-sauce-labs-backpack" };
+
+ //ShoppingCart
+ public ElementSelector ShoppingCartBadge = new() { Selector = "//span[@class='shopping_cart_badge']"};
}
\ No newline at end of file
diff --git a/Behavioral.Automation.Playwright/Behavioral.Automation.Playwright/Services/BrowserRunner.cs b/Behavioral.Automation.Playwright/Behavioral.Automation.Playwright/Services/BrowserRunner.cs
deleted file mode 100644
index b686d355..00000000
--- a/Behavioral.Automation.Playwright/Behavioral.Automation.Playwright/Services/BrowserRunner.cs
+++ /dev/null
@@ -1,16 +0,0 @@
-using System.Threading.Tasks;
-using Microsoft.Playwright;
-
-namespace Behavioral.Automation.Playwright.Services;
-
-public class BrowserRunner
- {
- public async Task LaunchBrowser()
- {
- using var playwright = await Microsoft.Playwright.Playwright.CreateAsync();
- await using var browser = await playwright.Chromium.LaunchAsync();
- var page = await browser.NewPageAsync();
-
- return page;
- }
- }
\ No newline at end of file
diff --git a/Behavioral.Automation.Playwright/Behavioral.Automation.Playwright/Services/ElementSelectors/ElementSelector.cs b/Behavioral.Automation.Playwright/Behavioral.Automation.Playwright/Services/ElementSelectors/ElementSelector.cs
deleted file mode 100644
index acd578b8..00000000
--- a/Behavioral.Automation.Playwright/Behavioral.Automation.Playwright/Services/ElementSelectors/ElementSelector.cs
+++ /dev/null
@@ -1,8 +0,0 @@
-namespace Behavioral.Automation.Playwright.Services.ElementSelectors;
-
-public class ElementSelector
-{
- public string? IdSelector { get; set; }
-
- public string? XpathSelector { get; set; }
-}
\ No newline at end of file
diff --git a/Behavioral.Automation.Playwright/Behavioral.Automation.Playwright/Services/ILocatorProvider.cs b/Behavioral.Automation.Playwright/Behavioral.Automation.Playwright/Services/ILocatorProvider.cs
deleted file mode 100644
index 77acd2a0..00000000
--- a/Behavioral.Automation.Playwright/Behavioral.Automation.Playwright/Services/ILocatorProvider.cs
+++ /dev/null
@@ -1,9 +0,0 @@
-using Behavioral.Automation.Playwright.Services.ElementSelectors;
-using Microsoft.Playwright;
-
-namespace Behavioral.Automation.Playwright.Services;
-
-public interface ILocatorProvider
-{
- public ILocator GetLocator(ElementSelector selector);
-}
\ No newline at end of file
diff --git a/Behavioral.Automation.Playwright/Behavioral.Automation.Playwright/Services/LocatorProvider.cs b/Behavioral.Automation.Playwright/Behavioral.Automation.Playwright/Services/LocatorProvider.cs
index 4af3c0b2..786cc2e2 100644
--- a/Behavioral.Automation.Playwright/Behavioral.Automation.Playwright/Services/LocatorProvider.cs
+++ b/Behavioral.Automation.Playwright/Behavioral.Automation.Playwright/Services/LocatorProvider.cs
@@ -2,20 +2,18 @@
using Behavioral.Automation.Configs;
using Behavioral.Automation.Playwright.Configs;
using Behavioral.Automation.Playwright.Context;
-using Behavioral.Automation.Playwright.Services.ElementSelectors;
+using Behavioral.Automation.Playwright.ElementSelectors;
using Microsoft.Playwright;
namespace Behavioral.Automation.Playwright.Services;
-public class LocatorProvider : ILocatorProvider
+public class LocatorProvider
{
- private readonly LocatorStorageService _locatorStorageService;
private readonly WebContext _webContext;
private readonly string _searchAttribute = ConfigManager.GetConfig().SearchAttribute;
- public LocatorProvider(LocatorStorageService locatorStorageService, WebContext webContext)
+ public LocatorProvider(WebContext webContext)
{
- _locatorStorageService = locatorStorageService;
_webContext = webContext;
}
@@ -26,7 +24,7 @@ public ILocator GetLocator(ElementSelector selector)
return _webContext.Page.Locator($"//*[@{_searchAttribute}='{selector.IdSelector}']");
}
- return selector.XpathSelector != null ? _webContext.Page.Locator(selector.XpathSelector) :
+ return selector.Selector != null ? _webContext.Page.Locator(selector.Selector) :
throw new NullReferenceException("Element was not found or web context is null");
}
}
\ No newline at end of file
diff --git a/Behavioral.Automation.Playwright/Behavioral.Automation.Playwright/Services/LocatorStorageService.cs b/Behavioral.Automation.Playwright/Behavioral.Automation.Playwright/Services/LocatorStorageService.cs
index b366eda9..0aea999b 100644
--- a/Behavioral.Automation.Playwright/Behavioral.Automation.Playwright/Services/LocatorStorageService.cs
+++ b/Behavioral.Automation.Playwright/Behavioral.Automation.Playwright/Services/LocatorStorageService.cs
@@ -1,12 +1,9 @@
using System;
using Behavioral.Automation.Playwright.Pages;
-using Behavioral.Automation.Playwright.Services.ElementSelectors;
using Behavioral.Automation.Playwright.Utils;
-using JetBrains.Annotations;
namespace Behavioral.Automation.Playwright.Services;
-[UsedImplicitly]
public class LocatorStorageService
{
//TODO: Impl factory
diff --git a/Behavioral.Automation.Playwright/Behavioral.Automation.Playwright/TestServicesBuilder.cs b/Behavioral.Automation.Playwright/Behavioral.Automation.Playwright/TestServicesBuilder.cs
deleted file mode 100644
index 4e87fe17..00000000
--- a/Behavioral.Automation.Playwright/Behavioral.Automation.Playwright/TestServicesBuilder.cs
+++ /dev/null
@@ -1,24 +0,0 @@
-using Behavioral.Automation.Playwright.Services;
-using BoDi;
-using Gherkin.Ast;
-
-namespace Behavioral.Automation.Playwright
-{
- ///
- /// Initialise all necessary objects before test execution
- ///
- public sealed class TestServicesBuilder
- {
- private readonly IObjectContainer _objectContainer;
-
- public TestServicesBuilder(IObjectContainer objectContainer)
- {
- _objectContainer = objectContainer;
- }
-
- public void Build()
- {
- _objectContainer.RegisterTypeAs();
- }
- }
-}
\ No newline at end of file
diff --git a/Behavioral.Automation.Playwright/Behavioral.Automation.Playwright/WebElementsWrappers/DropdownWrapper.cs b/Behavioral.Automation.Playwright/Behavioral.Automation.Playwright/WebElementsWrappers/DropdownWrapper.cs
index 1af9c28a..1680d763 100644
--- a/Behavioral.Automation.Playwright/Behavioral.Automation.Playwright/WebElementsWrappers/DropdownWrapper.cs
+++ b/Behavioral.Automation.Playwright/Behavioral.Automation.Playwright/WebElementsWrappers/DropdownWrapper.cs
@@ -1,27 +1,29 @@
-using System;
using System.Collections.Generic;
-using System.Linq;
using System.Threading.Tasks;
using Behavioral.Automation.Playwright.Context;
-using Behavioral.Automation.Playwright.WebElementsWrappers.Interface;
+using Behavioral.Automation.Playwright.ElementSelectors;
using Microsoft.Playwright;
namespace Behavioral.Automation.Playwright.WebElementsWrappers;
-public class DropdownWrapper : WebElementWrapper, IDropdownWrapper
+public class DropdownWrapper : WebElementWrapper
{
- public DropdownWrapper(WebContext webContext, ILocator locator, ILocator itemLocator, string caption) :
- base(webContext, locator, caption)
+ public readonly DropdownSelector DropdownSelector;
+
+ public DropdownWrapper(WebContext webContext, DropdownSelector dropdownSelector, string caption) :
+ base(webContext, dropdownSelector, caption)
{
- Items = itemLocator;
+ DropdownSelector = dropdownSelector;
}
- public ILocator Items { get; set; }
+
+ public ILocator Items => GetChildLocator(DropdownSelector.ItemSelector);
+ public ILocator Base => GetChildLocator(DropdownSelector.BaseElementSelector);
public Task> ItemsTexts => Items.AllTextContentsAsync();
public async Task SelectValue(params string[] elements)
{
- await Locator.ClickAsync();
+ await Base.ClickAsync();
foreach (var element in elements)
{
var optionToClick = GetOption(element);
@@ -31,6 +33,6 @@ public async Task SelectValue(params string[] elements)
public ILocator GetOption(string option)
{
- return Items.Filter(new LocatorFilterOptions {HasTextString = option});
+ return Items.Filter(new LocatorFilterOptions { HasTextString = option });
}
}
\ No newline at end of file
diff --git a/Behavioral.Automation.Playwright/Behavioral.Automation.Playwright/WebElementsWrappers/Interface/IDropdownWrapper.cs b/Behavioral.Automation.Playwright/Behavioral.Automation.Playwright/WebElementsWrappers/Interface/IDropdownWrapper.cs
deleted file mode 100644
index a8e90f07..00000000
--- a/Behavioral.Automation.Playwright/Behavioral.Automation.Playwright/WebElementsWrappers/Interface/IDropdownWrapper.cs
+++ /dev/null
@@ -1,16 +0,0 @@
-using System.Collections.Generic;
-using System.Threading.Tasks;
-using Microsoft.Playwright;
-
-namespace Behavioral.Automation.Playwright.WebElementsWrappers.Interface;
-
-public interface IDropdownWrapper: IWebElementWrapper
-{
- public ILocator Items { get; set; }
-
- public Task> ItemsTexts { get; }
-
- public Task SelectValue(params string[] elements);
-
- public ILocator GetOption(string option);
-}
\ No newline at end of file
diff --git a/Behavioral.Automation.Playwright/Behavioral.Automation.Playwright/WebElementsWrappers/Interface/ITableWrapper.cs b/Behavioral.Automation.Playwright/Behavioral.Automation.Playwright/WebElementsWrappers/Interface/ITableWrapper.cs
deleted file mode 100644
index 19e6a923..00000000
--- a/Behavioral.Automation.Playwright/Behavioral.Automation.Playwright/WebElementsWrappers/Interface/ITableWrapper.cs
+++ /dev/null
@@ -1,19 +0,0 @@
-using System.Collections.Generic;
-using System.Threading.Tasks;
-using Behavioral.Automation.Playwright.Services.ElementSelectors;
-using Microsoft.Playwright;
-
-namespace Behavioral.Automation.Playwright.WebElementsWrappers.Interface;
-
-public interface ITableWrapper: IWebElementWrapper
-{
- public ILocator Rows { get; set; }
-
- public ElementSelector CellsSelector { get; set; }
-
- public ILocator? HeaderCells { get; set; }
-
- public ILocator GetCellsForRow(ILocator row);
-
- //public Task> HeaderCellsTextAsync { get; }
-}
\ No newline at end of file
diff --git a/Behavioral.Automation.Playwright/Behavioral.Automation.Playwright/WebElementsWrappers/Interface/IWebElementWrapper.cs b/Behavioral.Automation.Playwright/Behavioral.Automation.Playwright/WebElementsWrappers/Interface/IWebElementWrapper.cs
deleted file mode 100644
index 2c35e83c..00000000
--- a/Behavioral.Automation.Playwright/Behavioral.Automation.Playwright/WebElementsWrappers/Interface/IWebElementWrapper.cs
+++ /dev/null
@@ -1,13 +0,0 @@
-using Behavioral.Automation.Playwright.Context;
-using Microsoft.Playwright;
-
-namespace Behavioral.Automation.Playwright.WebElementsWrappers.Interface;
-
-public interface IWebElementWrapper
-{
- public WebContext WebContext { get; }
-
- public string Caption { get;}
-
- public ILocator Locator { get; }
-}
\ No newline at end of file
diff --git a/Behavioral.Automation.Playwright/Behavioral.Automation.Playwright/WebElementsWrappers/TableWrapper.cs b/Behavioral.Automation.Playwright/Behavioral.Automation.Playwright/WebElementsWrappers/TableWrapper.cs
index 5f147cca..628755b9 100644
--- a/Behavioral.Automation.Playwright/Behavioral.Automation.Playwright/WebElementsWrappers/TableWrapper.cs
+++ b/Behavioral.Automation.Playwright/Behavioral.Automation.Playwright/WebElementsWrappers/TableWrapper.cs
@@ -1,38 +1,27 @@
-using System;
-using System.Collections.Generic;
-using System.Threading.Tasks;
using Behavioral.Automation.Playwright.Context;
-using Behavioral.Automation.Playwright.Services.ElementSelectors;
-using Behavioral.Automation.Playwright.WebElementsWrappers.Interface;
+using Behavioral.Automation.Playwright.ElementSelectors;
using Microsoft.Playwright;
-using NUnit.Framework;
namespace Behavioral.Automation.Playwright.WebElementsWrappers;
-public class TableWrapper : WebElementWrapper, ITableWrapper
+public class TableWrapper : WebElementWrapper
{
- public TableWrapper(WebContext webContext,
- ILocator locator,
- ILocator rowLocator,
- ElementSelector cellsSelector,
- ILocator? headerCellsLocator,
- string caption) :
- base(webContext, locator, caption)
+ public readonly TableSelector TableSelector;
+
+ public TableWrapper(WebContext webContext, TableSelector tableSelector, string caption) :
+ base(webContext, tableSelector, caption)
{
- Rows = rowLocator;
- CellsSelector = cellsSelector;
- HeaderCells = headerCellsLocator;
+ TableSelector = tableSelector;
}
-
- public ILocator Rows { get; set; }
-
- public ElementSelector CellsSelector { get; set; }
+
+ public ILocator Rows => GetChildLocator(TableSelector.RowSelector);
+
+ public ILocator CellsSelector => GetChildLocator(TableSelector.CellSelector);
public ILocator? HeaderCells { get; set; }
-
+
public ILocator GetCellsForRow(ILocator row)
{
- var cellSelector = CellsSelector.IdSelector ?? CellsSelector.XpathSelector;
- return row.Locator(cellSelector);
+ return GetChildLocator(row, TableSelector.CellSelector);
}
}
\ No newline at end of file
diff --git a/Behavioral.Automation.Playwright/Behavioral.Automation.Playwright/WebElementsWrappers/WebElementWrapper.cs b/Behavioral.Automation.Playwright/Behavioral.Automation.Playwright/WebElementsWrappers/WebElementWrapper.cs
index d4f894ca..a5032af4 100644
--- a/Behavioral.Automation.Playwright/Behavioral.Automation.Playwright/WebElementsWrappers/WebElementWrapper.cs
+++ b/Behavioral.Automation.Playwright/Behavioral.Automation.Playwright/WebElementsWrappers/WebElementWrapper.cs
@@ -1,19 +1,57 @@
+using System;
+using Behavioral.Automation.Configs;
+using Behavioral.Automation.Playwright.Configs;
using Behavioral.Automation.Playwright.Context;
-using Behavioral.Automation.Playwright.Services;
-using Behavioral.Automation.Playwright.WebElementsWrappers.Interface;
+using Behavioral.Automation.Playwright.ElementSelectors;
using Microsoft.Playwright;
namespace Behavioral.Automation.Playwright.WebElementsWrappers;
-public class WebElementWrapper : IWebElementWrapper
+public class WebElementWrapper
{
- public WebElementWrapper(WebContext webContext, ILocator locator, string caption)
+ private readonly string _searchAttribute = ConfigManager.GetConfig().SearchAttribute;
+ public WebContext WebContext { get; }
+ public ElementSelector Selector { get; }
+ public ILocator Locator => GetLocator(Selector);
+ public string Caption { get; }
+
+ public WebElementWrapper(WebContext webContext, ElementSelector selector, string caption)
{
WebContext = webContext;
- Locator = locator;
+ Selector = selector;
Caption = caption;
}
- public WebContext WebContext { get; }
- public ILocator Locator { get; }
- public string Caption { get; }
+
+ public ILocator GetLocator(ElementSelector selector)
+ {
+ if (selector.IdSelector != null)
+ {
+ return WebContext.Page.Locator($"//*[@{_searchAttribute}='{selector.IdSelector}']");
+ }
+
+ return selector.Selector != null ? WebContext.Page.Locator(selector.Selector) :
+ throw new NullReferenceException("Element was not found or web context is null");
+ }
+
+ public ILocator GetChildLocator(ElementSelector selector)
+ {
+ if (selector.IdSelector != null)
+ {
+ return Locator.Locator($"//*[@{_searchAttribute}='{selector.IdSelector}']");
+ }
+
+ return selector.Selector != null ? Locator.Locator(selector.Selector) :
+ throw new NullReferenceException("Element was not found or web context is null");
+ }
+
+ public ILocator GetChildLocator(ILocator parentLocator, ElementSelector selector)
+ {
+ if (selector.IdSelector != null)
+ {
+ return parentLocator.Locator($"//*[@{_searchAttribute}='{selector.IdSelector}']");
+ }
+
+ return selector.Selector != null ? parentLocator.Locator(selector.Selector) :
+ throw new NullReferenceException("Element was not found or web context is null");
+ }
}
\ No newline at end of file
diff --git a/Behavioral.Automation.Selenium/Behavioral.Automation.DemoScenarios/Behavioral.Automation.DemoScenarios.csproj b/Behavioral.Automation.Selenium/Behavioral.Automation.DemoScenarios/Behavioral.Automation.DemoScenarios.csproj
index 058dd7e8..503e05d1 100644
--- a/Behavioral.Automation.Selenium/Behavioral.Automation.DemoScenarios/Behavioral.Automation.DemoScenarios.csproj
+++ b/Behavioral.Automation.Selenium/Behavioral.Automation.DemoScenarios/Behavioral.Automation.DemoScenarios.csproj
@@ -23,11 +23,6 @@
-
-
-
-
-
diff --git a/Behavioral.Automation.Selenium/Behavioral.Automation/Behavioral.Automation.csproj b/Behavioral.Automation.Selenium/Behavioral.Automation/Behavioral.Automation.csproj
index 41103adf..f18f3e8d 100644
--- a/Behavioral.Automation.Selenium/Behavioral.Automation/Behavioral.Automation.csproj
+++ b/Behavioral.Automation.Selenium/Behavioral.Automation/Behavioral.Automation.csproj
@@ -31,10 +31,6 @@ The whole automation code is divided into the following parts:
-
- all
- runtime; build; native; contentfiles; analyzers; buildtransitive
-
diff --git a/Behavioral.Automation.Transformations/Behavioral.Automation.Transformations.csproj b/Behavioral.Automation.Transformations/Behavioral.Automation.Transformations.csproj
new file mode 100644
index 00000000..f9372046
--- /dev/null
+++ b/Behavioral.Automation.Transformations/Behavioral.Automation.Transformations.csproj
@@ -0,0 +1,18 @@
+
+
+
+ net6.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Behavioral.Automation.Transformations/ScribanFunctions/ConfigFunction.cs b/Behavioral.Automation.Transformations/ScribanFunctions/ConfigFunction.cs
new file mode 100644
index 00000000..db89e391
--- /dev/null
+++ b/Behavioral.Automation.Transformations/ScribanFunctions/ConfigFunction.cs
@@ -0,0 +1,12 @@
+using Behavioral.Automation.Configs;
+using Scriban.Runtime;
+
+namespace Behavioral.Automation.Transformations.ScribanFunctions;
+
+public class ConfigFunction : ScriptObject
+{
+ public static string Config(string configName)
+ {
+ return ConfigManager.GetConfig(configName);
+ }
+}
\ No newline at end of file
diff --git a/Behavioral.Automation.Transformations/Transformations.cs b/Behavioral.Automation.Transformations/Transformations.cs
new file mode 100644
index 00000000..ac0e05bf
--- /dev/null
+++ b/Behavioral.Automation.Transformations/Transformations.cs
@@ -0,0 +1,88 @@
+using Behavioral.Automation.Transformations.ScribanFunctions;
+using Scriban;
+using Scriban.Runtime;
+using TechTalk.SpecFlow;
+using TechTalk.SpecFlow.Infrastructure;
+
+namespace Behavioral.Automation.Transformations;
+
+[Binding]
+public class Transformations
+{
+ private readonly ConfigFunction _configFunction;
+ private readonly ScenarioContext _scenarioContext;
+ private static readonly List customFunctions = new List();
+ private readonly SpecFlowOutputHelper _specFlowOutputHelper;
+
+ public Transformations(ConfigFunction configFunction, ScenarioContext scenarioContext, SpecFlowOutputHelper specFlowOutputHelper)
+ {
+ _configFunction = configFunction;
+ _scenarioContext = scenarioContext;
+ _specFlowOutputHelper = specFlowOutputHelper;
+ }
+
+ public static void AddFunction(IScriptObject function)
+ {
+ customFunctions.Add(function);
+ }
+
+ [StepArgumentTransformation]
+ public string ProcessStringTransformations(string value)
+ {
+ var template = Template.Parse(value);
+ var result = template.Render(GetContext());
+ return result;
+ }
+
+ [StepArgumentTransformation]
+ public Table ProcessTableTransformations(Table table)
+ {
+ var context = GetContext();
+ var newTable = table;
+ if (table.Rows.Count > 0)
+ {
+ foreach (var row in newTable.Rows)
+ {
+ foreach (var value in row)
+ {
+ var template = Template.Parse(value.Value);
+ var result = template.Render(context);
+ row[value.Key] = result;
+ }
+ }
+ }
+
+ return newTable;
+ }
+
+ [Given("save variable \"(.*)\" with value:")]
+ [Given("save variable \"(.*)\" with value \"(.*)\"")]
+ public void SaveStringIntoContext(string variableName, string stringToSave)
+ {
+ _scenarioContext.Add(variableName, stringToSave);
+ _specFlowOutputHelper.WriteLine($"Saved '{stringToSave}' with key '{variableName}' in scenario context");
+ }
+
+ private TemplateContext GetContext()
+ {
+ var specflowContextVariables = new ScriptObject();
+ if (_scenarioContext.Any())
+ {
+ foreach (var item in _scenarioContext)
+ {
+ specflowContextVariables.Add(item.Key, item.Value);
+ }
+ }
+
+ var context = new TemplateContext();
+
+ context.PushGlobal(specflowContextVariables);
+ context.PushGlobal(_configFunction);
+ foreach (var customFunction in customFunctions)
+ {
+ context.PushGlobal(customFunction);
+ }
+
+ return context;
+ }
+}
\ No newline at end of file
diff --git a/Behavioral.Automation.sln b/Behavioral.Automation.sln
index 59f0bd7a..250314aa 100644
--- a/Behavioral.Automation.sln
+++ b/Behavioral.Automation.sln
@@ -22,6 +22,14 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Behavioral.Automation.Selen
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Behavioral.Automation.Playwright", "Behavioral.Automation.Playwright", "{2A26C467-6A03-4942-9F9B-62F77F992F70}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Behavioral.Automation.Playwright.DemoScenarios", "Behavioral.Automation.Playwright\Behavioral.Automation.Playwright.DemoScenarios\Behavioral.Automation.Playwright.DemoScenarios.csproj", "{B9C5C784-7956-40D2-B9A6-4928B1C84CF6}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Behavioral.Automation.API", "Behavioral.Automation.API\Behavioral.Automation.API.csproj", "{C1A9C893-87FB-4262-B55F-4ABC0D89EE6A}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Behavioral.Automation.Transformations", "Behavioral.Automation.Transformations\Behavioral.Automation.Transformations.csproj", "{A9C02EF4-BE20-4A0B-8205-3AD16FC5179E}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Behavioral.Automation.API.DemoScenarios", "Behavioral.Automation.API.DemoScenarios\Behavioral.Automation.API.DemoScenarios.csproj", "{B005408A-0146-45BA-866B-564FC2F1EBEA}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -56,6 +64,22 @@ Global
{7460A60F-85B2-4DCE-B190-000503AE8550}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7460A60F-85B2-4DCE-B190-000503AE8550}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7460A60F-85B2-4DCE-B190-000503AE8550}.Release|Any CPU.Build.0 = Release|Any CPU
+ {B9C5C784-7956-40D2-B9A6-4928B1C84CF6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {B9C5C784-7956-40D2-B9A6-4928B1C84CF6}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {B9C5C784-7956-40D2-B9A6-4928B1C84CF6}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {B9C5C784-7956-40D2-B9A6-4928B1C84CF6}.Release|Any CPU.Build.0 = Release|Any CPU
+ {C1A9C893-87FB-4262-B55F-4ABC0D89EE6A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {C1A9C893-87FB-4262-B55F-4ABC0D89EE6A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {C1A9C893-87FB-4262-B55F-4ABC0D89EE6A}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {C1A9C893-87FB-4262-B55F-4ABC0D89EE6A}.Release|Any CPU.Build.0 = Release|Any CPU
+ {A9C02EF4-BE20-4A0B-8205-3AD16FC5179E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {A9C02EF4-BE20-4A0B-8205-3AD16FC5179E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {A9C02EF4-BE20-4A0B-8205-3AD16FC5179E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {A9C02EF4-BE20-4A0B-8205-3AD16FC5179E}.Release|Any CPU.Build.0 = Release|Any CPU
+ {B005408A-0146-45BA-866B-564FC2F1EBEA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {B005408A-0146-45BA-866B-564FC2F1EBEA}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {B005408A-0146-45BA-866B-564FC2F1EBEA}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {B005408A-0146-45BA-866B-564FC2F1EBEA}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -67,6 +91,7 @@ Global
{4DD5E62E-E447-419D-82BF-C918F08091BD} = {430C8D20-03B9-4268-A6BC-F008F4DC2CD1}
{F1B23009-3334-4B1B-8198-2C618346062D} = {2A26C467-6A03-4942-9F9B-62F77F992F70}
{BB7AC02C-03FD-4F17-B4A9-CDECC1A63DA5} = {430C8D20-03B9-4268-A6BC-F008F4DC2CD1}
+ {B9C5C784-7956-40D2-B9A6-4928B1C84CF6} = {2A26C467-6A03-4942-9F9B-62F77F992F70}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {724FD185-E1F2-44BD-89EA-DFD1DBF6453A}