From 32a90974080bbdeb0bcfdbaceccb765473fab32f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1vid=20El-Saig?= Date: Sat, 17 Jun 2023 14:50:14 +0200 Subject: [PATCH 1/8] Add helpers to create By for content picker and interpolated CSS. --- .../NavigationUITestContextExtensions.cs | 14 +++++++------- Lombiq.Tests.UI/Helpers/ByHelper.cs | 18 ++++++++++++++++++ 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/Lombiq.Tests.UI/Extensions/NavigationUITestContextExtensions.cs b/Lombiq.Tests.UI/Extensions/NavigationUITestContextExtensions.cs index f0fc64889..8199b51e0 100644 --- a/Lombiq.Tests.UI/Extensions/NavigationUITestContextExtensions.cs +++ b/Lombiq.Tests.UI/Extensions/NavigationUITestContextExtensions.cs @@ -1,6 +1,7 @@ using Atata; using Lombiq.HelpfulLibraries.Common.Utilities; using Lombiq.Tests.UI.Constants; +using Lombiq.Tests.UI.Helpers; using Lombiq.Tests.UI.Pages; using Lombiq.Tests.UI.Services; using OpenQA.Selenium; @@ -229,26 +230,25 @@ public static bool WaitForPageLoad(this UITestContext context) => public static Task SetTaxonomyFieldByIndexAsync(this UITestContext context, string taxonomyId, int index) { - var baseSelector = StringHelper.CreateInvariant($".tags[data-taxonomy-content-item-id='{taxonomyId}']"); + var baseSelector = ByHelper.Css($".tags[data-taxonomy-content-item-id='{taxonomyId}']"); return SetFieldDropdownByIndexAsync(context, baseSelector, index); } public static Task SetContentPickerByIndexAsync(this UITestContext context, string part, string field, int index) { - var baseSelector = StringHelper.CreateInvariant($"*[data-part='{part}'][data-field='{field}']"); + var baseSelector = ByHelper.GetContentPickerSelector(part, field); return SetFieldDropdownByIndexAsync(context, baseSelector, index); } - private static async Task SetFieldDropdownByIndexAsync(UITestContext context, string baseSelector, int index) + private static async Task SetFieldDropdownByIndexAsync(UITestContext context, By baseSelector, int index) { - var byItem = - By.CssSelector(StringHelper.CreateInvariant( - $"{baseSelector} .multiselect__element:nth-child({index + 1}) .multiselect__option")) + var byItem = baseSelector + .Then(ByHelper.Css($".multiselect__element:nth-child({index + 1}) .multiselect__option")) .Visible(); while (!context.Exists(byItem.Safely())) { - await context.ClickReliablyOnAsync(By.CssSelector(baseSelector + " .multiselect__select")); + await context.ClickReliablyOnAsync(baseSelector.Then(By.CssSelector(".multiselect__select"))); } await context.ClickReliablyOnAsync(byItem); diff --git a/Lombiq.Tests.UI/Helpers/ByHelper.cs b/Lombiq.Tests.UI/Helpers/ByHelper.cs index 87df213ea..e254f730b 100644 --- a/Lombiq.Tests.UI/Helpers/ByHelper.cs +++ b/Lombiq.Tests.UI/Helpers/ByHelper.cs @@ -1,7 +1,10 @@ using Atata; +using Lombiq.HelpfulLibraries.Common.Utilities; using Newtonsoft.Json; using OpenQA.Selenium; using System; +using System.Globalization; +using System.Runtime.CompilerServices; namespace Lombiq.Tests.UI.Helpers; @@ -31,4 +34,19 @@ public static By Text(string innerText, string element = "*") => /// public static By TextContains(string innerText, string element = "*") => By.XPath($"//{element}[contains(., {JsonConvert.SerializeObject(innerText)})]"); + + /// + /// Creates a from an interpolated string with the invariant culture. This prevents culture- + /// sensitive formatting of interpolated values. + /// + public static By Css(this DefaultInterpolatedStringHandler value) => + By.CssSelector(string.Create(CultureInfo.InvariantCulture, ref value)); + + /// + /// Returns a CSS selector that looks up a content picker field. + /// + /// The name of the content part. + /// The name of the content picker field. + public static By GetContentPickerSelector(string part, string field) => + Css($"*[data-part='{part}'][data-field='{field}']"); } From ba49ec234ee99046ee3116ca226792a0a8146af4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1vid=20El-Saig?= Date: Sat, 17 Jun 2023 14:51:35 +0200 Subject: [PATCH 2/8] Add SetContentPickerByDisplayText --- .../NavigationUITestContextExtensions.cs | 30 +++++++++++++++++++ Lombiq.Tests.UI/Lombiq.Tests.UI.csproj | 1 + 2 files changed, 31 insertions(+) diff --git a/Lombiq.Tests.UI/Extensions/NavigationUITestContextExtensions.cs b/Lombiq.Tests.UI/Extensions/NavigationUITestContextExtensions.cs index 8199b51e0..27202db71 100644 --- a/Lombiq.Tests.UI/Extensions/NavigationUITestContextExtensions.cs +++ b/Lombiq.Tests.UI/Extensions/NavigationUITestContextExtensions.cs @@ -4,9 +4,18 @@ using Lombiq.Tests.UI.Helpers; using Lombiq.Tests.UI.Pages; using Lombiq.Tests.UI.Services; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using OpenQA.Selenium; using OpenQA.Selenium.Support.UI; +using OrchardCore.ContentFields.ViewModels; using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Http; using System.Threading.Tasks; namespace Lombiq.Tests.UI.Extensions; @@ -234,6 +243,27 @@ public static Task SetTaxonomyFieldByIndexAsync(this UITestContext context, stri return SetFieldDropdownByIndexAsync(context, baseSelector, index); } + public static async Task SetContentPickerByDisplayText(this UITestContext context, string part, string field, string text) + { + var searchUrl = new Uri( + new Uri(context.Driver.Url), + context.Get(ByHelper.GetContentPickerSelector(part, field)).GetAttribute("data-search-url")); + + int index; + + using (var client = new HttpClient()) + using (var response = await client.GetAsync(searchUrl)) + await using (var stream = await response.Content.ReadAsStreamAsync()) + using (var textReader = new StreamReader(stream)) + await using (var jsonReader = new JsonTextReader(textReader)) + { + var result = new JsonSerializer().Deserialize>(jsonReader); + index = result.IndexOf(result.First(item => item.DisplayText == text)); + } + + await context.SetContentPickerByIndexAsync(part, field, index); + } + public static Task SetContentPickerByIndexAsync(this UITestContext context, string part, string field, int index) { var baseSelector = ByHelper.GetContentPickerSelector(part, field); diff --git a/Lombiq.Tests.UI/Lombiq.Tests.UI.csproj b/Lombiq.Tests.UI/Lombiq.Tests.UI.csproj index b417be8bc..309fe6c20 100644 --- a/Lombiq.Tests.UI/Lombiq.Tests.UI.csproj +++ b/Lombiq.Tests.UI/Lombiq.Tests.UI.csproj @@ -63,6 +63,7 @@ + From a13f2ce1cad27eeb869911ef70556ed4c5d2fb8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1vid=20El-Saig?= Date: Sat, 17 Jun 2023 14:52:21 +0200 Subject: [PATCH 3/8] Add Async to the name --- Lombiq.Tests.UI/Extensions/NavigationUITestContextExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lombiq.Tests.UI/Extensions/NavigationUITestContextExtensions.cs b/Lombiq.Tests.UI/Extensions/NavigationUITestContextExtensions.cs index 27202db71..dfedfc6d2 100644 --- a/Lombiq.Tests.UI/Extensions/NavigationUITestContextExtensions.cs +++ b/Lombiq.Tests.UI/Extensions/NavigationUITestContextExtensions.cs @@ -243,7 +243,7 @@ public static Task SetTaxonomyFieldByIndexAsync(this UITestContext context, stri return SetFieldDropdownByIndexAsync(context, baseSelector, index); } - public static async Task SetContentPickerByDisplayText(this UITestContext context, string part, string field, string text) + public static async Task SetContentPickerByDisplayTextAsync(this UITestContext context, string part, string field, string text) { var searchUrl = new Uri( new Uri(context.Driver.Url), From 9e0d6bbbedb276f357bb54789d613b4141d03ffc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1vid=20El-Saig?= Date: Sat, 17 Jun 2023 15:21:34 +0200 Subject: [PATCH 4/8] Add GetCookieContainer extension. --- .../BrowserUITestContextExtensions.cs | 35 +++++++++++++++++++ .../NavigationUITestContextExtensions.cs | 9 +++-- 2 files changed, 39 insertions(+), 5 deletions(-) create mode 100644 Lombiq.Tests.UI/Extensions/BrowserUITestContextExtensions.cs diff --git a/Lombiq.Tests.UI/Extensions/BrowserUITestContextExtensions.cs b/Lombiq.Tests.UI/Extensions/BrowserUITestContextExtensions.cs new file mode 100644 index 000000000..51bc39434 --- /dev/null +++ b/Lombiq.Tests.UI/Extensions/BrowserUITestContextExtensions.cs @@ -0,0 +1,35 @@ +using Lombiq.Tests.UI.Services; +using System.Net.Http; + +namespace System.Net; + +public static class BrowserUITestContextExtensions +{ + /// + /// Gets all cookies from the browser and converts them into .NET instances in a . This can be useful if you want to make web requests using + /// while using the login and other cookies from the browser. + /// + public static CookieContainer GetCookieContainer(this UITestContext context) + { + var cookieContainer = new CookieContainer(); + foreach (var seleniumCookie in context.Driver.Manage().Cookies.AllCookies) + { + var netCookie = new Cookie + { + Domain = seleniumCookie.Domain, + HttpOnly = seleniumCookie.IsHttpOnly, + Name = seleniumCookie.Name, + Path = seleniumCookie.Path, + Secure = seleniumCookie.Secure, + Value = seleniumCookie.Value, + }; + + if (seleniumCookie.Expiry.HasValue) netCookie.Expires = seleniumCookie.Expiry.Value; + + cookieContainer.Add(netCookie); + } + + return cookieContainer; + } +} diff --git a/Lombiq.Tests.UI/Extensions/NavigationUITestContextExtensions.cs b/Lombiq.Tests.UI/Extensions/NavigationUITestContextExtensions.cs index dfedfc6d2..e1fe753eb 100644 --- a/Lombiq.Tests.UI/Extensions/NavigationUITestContextExtensions.cs +++ b/Lombiq.Tests.UI/Extensions/NavigationUITestContextExtensions.cs @@ -251,13 +251,12 @@ public static async Task SetContentPickerByDisplayTextAsync(this UITestContext c int index; - using (var client = new HttpClient()) + using (var handler = new HttpClientHandler { CookieContainer = context.GetCookieContainer() }) + using (var client = new HttpClient(handler)) using (var response = await client.GetAsync(searchUrl)) - await using (var stream = await response.Content.ReadAsStreamAsync()) - using (var textReader = new StreamReader(stream)) - await using (var jsonReader = new JsonTextReader(textReader)) { - var result = new JsonSerializer().Deserialize>(jsonReader); + var json = await response.Content.ReadAsStringAsync(); + var result = JsonConvert.DeserializeObject>(json); index = result.IndexOf(result.First(item => item.DisplayText == text)); } From 614ab982fb993fde6cc3c290e195765ea455f5ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1vid=20El-Saig?= Date: Sat, 17 Jun 2023 15:54:01 +0200 Subject: [PATCH 5/8] unusing --- .../Extensions/NavigationUITestContextExtensions.cs | 4 ---- Lombiq.Tests.UI/Helpers/ByHelper.cs | 1 - 2 files changed, 5 deletions(-) diff --git a/Lombiq.Tests.UI/Extensions/NavigationUITestContextExtensions.cs b/Lombiq.Tests.UI/Extensions/NavigationUITestContextExtensions.cs index e1fe753eb..48dee3d86 100644 --- a/Lombiq.Tests.UI/Extensions/NavigationUITestContextExtensions.cs +++ b/Lombiq.Tests.UI/Extensions/NavigationUITestContextExtensions.cs @@ -1,18 +1,14 @@ using Atata; -using Lombiq.HelpfulLibraries.Common.Utilities; using Lombiq.Tests.UI.Constants; using Lombiq.Tests.UI.Helpers; using Lombiq.Tests.UI.Pages; using Lombiq.Tests.UI.Services; using Newtonsoft.Json; -using Newtonsoft.Json.Linq; using OpenQA.Selenium; using OpenQA.Selenium.Support.UI; using OrchardCore.ContentFields.ViewModels; using System; using System.Collections.Generic; -using System.Globalization; -using System.IO; using System.Linq; using System.Net; using System.Net.Http; diff --git a/Lombiq.Tests.UI/Helpers/ByHelper.cs b/Lombiq.Tests.UI/Helpers/ByHelper.cs index e254f730b..d9685fd7b 100644 --- a/Lombiq.Tests.UI/Helpers/ByHelper.cs +++ b/Lombiq.Tests.UI/Helpers/ByHelper.cs @@ -1,5 +1,4 @@ using Atata; -using Lombiq.HelpfulLibraries.Common.Utilities; using Newtonsoft.Json; using OpenQA.Selenium; using System; From d17a7d74c400187b3a733dfe82c7ad79d6c326f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1vid=20El-Saig?= Date: Sat, 17 Jun 2023 16:07:23 +0200 Subject: [PATCH 6/8] Add FetchWithBrowserContextAsync extension method. --- .../BrowserUITestContextExtensions.cs | 20 ++++++++++++++++ .../NavigationUITestContextExtensions.cs | 24 ++++++++----------- 2 files changed, 30 insertions(+), 14 deletions(-) diff --git a/Lombiq.Tests.UI/Extensions/BrowserUITestContextExtensions.cs b/Lombiq.Tests.UI/Extensions/BrowserUITestContextExtensions.cs index 51bc39434..042fd321f 100644 --- a/Lombiq.Tests.UI/Extensions/BrowserUITestContextExtensions.cs +++ b/Lombiq.Tests.UI/Extensions/BrowserUITestContextExtensions.cs @@ -1,5 +1,6 @@ using Lombiq.Tests.UI.Services; using System.Net.Http; +using System.Threading.Tasks; namespace System.Net; @@ -32,4 +33,23 @@ public static CookieContainer GetCookieContainer(this UITestContext context) return cookieContainer; } + + public static async Task FetchWithBrowserContextAsync( + this UITestContext context, + HttpMethod method, + string address, + Func> processResponseAsync) + { + var searchUrl = new Uri(new Uri(context.Driver.Url), address); + + // Certificate checking is not necessary because the request URL is relative to where the browser already is. +#pragma warning disable CA5399 // HttpClient is created without enabling CheckCertificateRevocationList + using var handler = new HttpClientHandler { CookieContainer = context.GetCookieContainer() }; + using var client = new HttpClient(handler); + using var request = new HttpRequestMessage(method, searchUrl); + using var response = await client.SendAsync(request); +#pragma warning restore CA5399 + + return await processResponseAsync(response); + } } diff --git a/Lombiq.Tests.UI/Extensions/NavigationUITestContextExtensions.cs b/Lombiq.Tests.UI/Extensions/NavigationUITestContextExtensions.cs index 48dee3d86..99b15cda7 100644 --- a/Lombiq.Tests.UI/Extensions/NavigationUITestContextExtensions.cs +++ b/Lombiq.Tests.UI/Extensions/NavigationUITestContextExtensions.cs @@ -241,20 +241,16 @@ public static Task SetTaxonomyFieldByIndexAsync(this UITestContext context, stri public static async Task SetContentPickerByDisplayTextAsync(this UITestContext context, string part, string field, string text) { - var searchUrl = new Uri( - new Uri(context.Driver.Url), - context.Get(ByHelper.GetContentPickerSelector(part, field)).GetAttribute("data-search-url")); - - int index; - - using (var handler = new HttpClientHandler { CookieContainer = context.GetCookieContainer() }) - using (var client = new HttpClient(handler)) - using (var response = await client.GetAsync(searchUrl)) - { - var json = await response.Content.ReadAsStringAsync(); - var result = JsonConvert.DeserializeObject>(json); - index = result.IndexOf(result.First(item => item.DisplayText == text)); - } + var searchUrl = context.Get(ByHelper.GetContentPickerSelector(part, field)).GetAttribute("data-search-url"); + var index = await context.FetchWithBrowserContextAsync( + HttpMethod.Get, + searchUrl, + async response => + { + var json = await response.Content.ReadAsStringAsync(); + var result = JsonConvert.DeserializeObject>(json); + return result.IndexOf(result.First(item => item.DisplayText == text)); + }); await context.SetContentPickerByIndexAsync(part, field, index); } From a327b7a48cc539dceeec118885ef8e137230dbf3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1vid=20El-Saig?= Date: Sat, 17 Jun 2023 16:47:18 +0200 Subject: [PATCH 7/8] Actually disable client checking. --- .../BrowserUITestContextExtensions.cs | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/Lombiq.Tests.UI/Extensions/BrowserUITestContextExtensions.cs b/Lombiq.Tests.UI/Extensions/BrowserUITestContextExtensions.cs index 042fd321f..615981821 100644 --- a/Lombiq.Tests.UI/Extensions/BrowserUITestContextExtensions.cs +++ b/Lombiq.Tests.UI/Extensions/BrowserUITestContextExtensions.cs @@ -1,4 +1,5 @@ using Lombiq.Tests.UI.Services; +using System.Diagnostics.CodeAnalysis; using System.Net.Http; using System.Threading.Tasks; @@ -34,21 +35,29 @@ public static CookieContainer GetCookieContainer(this UITestContext context) return cookieContainer; } + [SuppressMessage( + "Security", + "SCS0004: Certificate Validation has been disabled.", + Justification = "Necessary for local testing.")] + [SuppressMessage( + "Security", + "CA5399: HttpClient is created without enabling CheckCertificateRevocationList.", + Justification = "Necessary for local testing.")] public static async Task FetchWithBrowserContextAsync( this UITestContext context, HttpMethod method, string address, Func> processResponseAsync) { - var searchUrl = new Uri(new Uri(context.Driver.Url), address); + using var handler = new HttpClientHandler + { + CookieContainer = context.GetCookieContainer(), + ServerCertificateCustomValidationCallback = (_, _, _, _) => true, + }; - // Certificate checking is not necessary because the request URL is relative to where the browser already is. -#pragma warning disable CA5399 // HttpClient is created without enabling CheckCertificateRevocationList - using var handler = new HttpClientHandler { CookieContainer = context.GetCookieContainer() }; using var client = new HttpClient(handler); - using var request = new HttpRequestMessage(method, searchUrl); + using var request = new HttpRequestMessage(method, new Uri(new Uri(context.Driver.Url), address)); using var response = await client.SendAsync(request); -#pragma warning restore CA5399 return await processResponseAsync(response); } From 38ec58b43c2c1f662ee48434d3b8df028d954849 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1vid=20El-Saig?= Date: Sat, 17 Jun 2023 22:59:40 +0200 Subject: [PATCH 8/8] Code cleanup. --- Lombiq.Tests.UI/Extensions/BrowserUITestContextExtensions.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Lombiq.Tests.UI/Extensions/BrowserUITestContextExtensions.cs b/Lombiq.Tests.UI/Extensions/BrowserUITestContextExtensions.cs index 615981821..6e4695686 100644 --- a/Lombiq.Tests.UI/Extensions/BrowserUITestContextExtensions.cs +++ b/Lombiq.Tests.UI/Extensions/BrowserUITestContextExtensions.cs @@ -1,4 +1,5 @@ -using Lombiq.Tests.UI.Services; +using Lombiq.Tests.UI.Extensions; +using Lombiq.Tests.UI.Services; using System.Diagnostics.CodeAnalysis; using System.Net.Http; using System.Threading.Tasks; @@ -56,7 +57,7 @@ public static async Task FetchWithBrowserContextAsync( }; using var client = new HttpClient(handler); - using var request = new HttpRequestMessage(method, new Uri(new Uri(context.Driver.Url), address)); + using var request = new HttpRequestMessage(method, new Uri(context.GetCurrentUri(), address)); using var response = await client.SendAsync(request); return await processResponseAsync(response);