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}