diff --git a/Lombiq.Tests.UI.AppExtensions/Lombiq.Tests.UI.AppExtensions.csproj b/Lombiq.Tests.UI.AppExtensions/Lombiq.Tests.UI.AppExtensions.csproj index 8cf7b1060..1197c424e 100644 --- a/Lombiq.Tests.UI.AppExtensions/Lombiq.Tests.UI.AppExtensions.csproj +++ b/Lombiq.Tests.UI.AppExtensions/Lombiq.Tests.UI.AppExtensions.csproj @@ -33,8 +33,8 @@ - - + + diff --git a/Lombiq.Tests.UI.Samples/Tests/BasicTests.cs b/Lombiq.Tests.UI.Samples/Tests/BasicTests.cs index 1b58f4c1a..c68601944 100644 --- a/Lombiq.Tests.UI.Samples/Tests/BasicTests.cs +++ b/Lombiq.Tests.UI.Samples/Tests/BasicTests.cs @@ -50,8 +50,8 @@ public Task LoginShouldWork() => // Let's fill out the login form. In UI tests, nothing is certain. If you fill out a form it's not // actually sure that the values are indeed there! To make things more reliable, we've added a lot of // useful methods like FillInWithRetriesAsync(). - await context.FillInWithRetriesAsync(By.Id("UserName"), DefaultUser.UserName); - await context.FillInWithRetriesAsync(By.Id("Password"), DefaultUser.Password); + await context.FillInWithRetriesAsync(By.Id("LoginForm_UserName"), DefaultUser.UserName); + await context.FillInWithRetriesAsync(By.Id("LoginForm_Password"), DefaultUser.Password); // Even clicking can be unreliable thus we have a helper for that too. await context.ClickReliablyOnSubmitAsync(); diff --git a/Lombiq.Tests.UI.Samples/Tests/BasicVisualVerificationTests_VerifyNavbar_By_ClassName[Contains]_-navbar-brand_Win32NT_msedge.png b/Lombiq.Tests.UI.Samples/Tests/BasicVisualVerificationTests_VerifyNavbar_By_ClassName[Contains]_-navbar-brand_Win32NT_MicrosoftEdge.png similarity index 100% rename from Lombiq.Tests.UI.Samples/Tests/BasicVisualVerificationTests_VerifyNavbar_By_ClassName[Contains]_-navbar-brand_Win32NT_msedge.png rename to Lombiq.Tests.UI.Samples/Tests/BasicVisualVerificationTests_VerifyNavbar_By_ClassName[Contains]_-navbar-brand_Win32NT_MicrosoftEdge.png diff --git a/Lombiq.Tests.UI.Samples/Tests/EmailTests.cs b/Lombiq.Tests.UI.Samples/Tests/EmailTests.cs index e29877117..928170eb6 100644 --- a/Lombiq.Tests.UI.Samples/Tests/EmailTests.cs +++ b/Lombiq.Tests.UI.Samples/Tests/EmailTests.cs @@ -1,7 +1,5 @@ -using Atata; using Lombiq.Tests.UI.Extensions; using Lombiq.Tests.UI.Helpers; -using OpenQA.Selenium; using System.Threading.Tasks; using Xunit; using Xunit.Abstractions; @@ -25,31 +23,13 @@ public Task SendingTestEmailShouldWork() => // A shortcut to sign in without going through (and thus testing) the login screen. await context.SignInDirectlyAsync(); - // Let's go to the "Test settings" option of the e-mail admin page. The default sender is configured in - // the test recipe so we can use the test feature. - await context.GoToAdminRelativeUrlAsync("/Email/Index"); - - // Let's send a basic e-mail. - await context.FillInWithRetriesAsync(By.Id("To"), "recipient@example.com"); - await context.FillInWithRetriesAsync(By.Id("Subject"), "Test message"); - await context.FillInWithRetriesAsync(By.Id("Body"), "Hi, this is a test."); - - // With the button being under the fold in the configured screen size, we need to make sure it's - // actually clicked. Scrolling there first doesn't work for some reason. - await ReliabilityHelper.DoWithRetriesOrFailAsync( - async () => - { - try - { - await context.ClickReliablyOnAsync(By.Id("emailtestsend")); // #spell-check-ignore-line - return true; - } - catch (WebDriverException ex) when (ex.Message.Contains("move target out of bounds")) - { - return false; - } - }); + // Set up the SMTP port. This is a dynamic value unique to each UI test so it can't come from a recipe. + await context.ConfigureSmtpPortAsync(); + // Let's go to the "Test settings" option of the e-mail admin page and send a basic e-mail. The default + // sender is configured in the test recipe so we can use the test feature. + await context.GoToEmailTestAsync(); + await context.FillEmailTestFormAsync("Test message"); context.ShouldBeSuccess(); // The SMTP service running behind the scenes also has a web UI that we can access to see all outgoing diff --git a/Lombiq.Tests.UI.Samples/Tests/MonkeyTests.cs b/Lombiq.Tests.UI.Samples/Tests/MonkeyTests.cs index 363c31c2a..5e0362711 100644 --- a/Lombiq.Tests.UI.Samples/Tests/MonkeyTests.cs +++ b/Lombiq.Tests.UI.Samples/Tests/MonkeyTests.cs @@ -90,7 +90,8 @@ public Task TestAdminBackgroundTasksAsMonkeyRecursivelyShouldWorkWithAdminUser() // You could also configure the same thing with regex: ////_monkeyTestingOptions.UrlFilters.Add(new MatchesRegexMonkeyTestingUrlFilter(@"\/Admin\/BackgroundTasks")); - await context.SignInDirectlyAndGoToRelativeUrlAsync("/Admin/BackgroundTasks"); + await context.SignInDirectlyAsync(); + await context.GoToAdminRelativeUrlAsync("/BackgroundTasks"); await context.TestCurrentPageAsMonkeyRecursivelyAsync(monkeyTestingOptions); }, configuration => configuration.AssertBrowserLog = (logEntries) => logEntries diff --git a/Lombiq.Tests.UI.Shortcuts/Lombiq.Tests.UI.Shortcuts.csproj b/Lombiq.Tests.UI.Shortcuts/Lombiq.Tests.UI.Shortcuts.csproj index 571b9d3ce..3b7f8662f 100644 --- a/Lombiq.Tests.UI.Shortcuts/Lombiq.Tests.UI.Shortcuts.csproj +++ b/Lombiq.Tests.UI.Shortcuts/Lombiq.Tests.UI.Shortcuts.csproj @@ -34,15 +34,15 @@ - - - - - - - - - + + + + + + + + + diff --git a/Lombiq.Tests.UI.Shortcuts/Services/ApplicationInfoInjectingFilter.cs b/Lombiq.Tests.UI.Shortcuts/Services/ApplicationInfoInjectingFilter.cs index 3822b2768..c24598d49 100644 --- a/Lombiq.Tests.UI.Shortcuts/Services/ApplicationInfoInjectingFilter.cs +++ b/Lombiq.Tests.UI.Shortcuts/Services/ApplicationInfoInjectingFilter.cs @@ -1,16 +1,18 @@ using Microsoft.AspNetCore.Html; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.Extensions.Configuration; -using Newtonsoft.Json; using OrchardCore.Modules; using OrchardCore.ResourceManagement; using System; +using System.Text.Json; using System.Threading.Tasks; namespace Lombiq.Tests.UI.Shortcuts.Services; public class ApplicationInfoInjectingFilter : IAsyncResultFilter { + private static readonly JsonSerializerOptions _indentedJsonSerializerOptions = new() { WriteIndented = true }; + private readonly IResourceManager _resourceManager; private readonly IConfiguration _shellConfiguration; private readonly IApplicationContext _applicationContext; @@ -35,7 +37,7 @@ public async Task OnResultExecutionAsync(ResultExecutingContext context, ResultE _resourceManager.RegisterHeadScript(new HtmlString( $"")); diff --git a/Lombiq.Tests.UI.Shortcuts/Startup.cs b/Lombiq.Tests.UI.Shortcuts/Startup.cs index 537724e21..4da8574a8 100644 --- a/Lombiq.Tests.UI.Shortcuts/Startup.cs +++ b/Lombiq.Tests.UI.Shortcuts/Startup.cs @@ -1,6 +1,7 @@ using Lombiq.HelpfulLibraries.AspNetCore.Extensions; using Lombiq.Tests.UI.Shortcuts.Services; using Microsoft.Extensions.DependencyInjection; +using OrchardCore.Data.YesSql; using OrchardCore.Modules; namespace Lombiq.Tests.UI.Shortcuts; @@ -11,5 +12,8 @@ public override void ConfigureServices(IServiceCollection services) { services.AddSingleton(); services.AddAsyncResultFilter(); + + // To ensure we don't encounter any concurrency issue, enable EnableThreadSafetyChecks for all tests. + services.Configure(options => options.EnableThreadSafetyChecks = true); } } diff --git a/Lombiq.Tests.UI.Shortcuts/Views/_ViewImports.cshtml b/Lombiq.Tests.UI.Shortcuts/Views/_ViewImports.cshtml index be43650df..000b84fbc 100644 --- a/Lombiq.Tests.UI.Shortcuts/Views/_ViewImports.cshtml +++ b/Lombiq.Tests.UI.Shortcuts/Views/_ViewImports.cshtml @@ -7,6 +7,6 @@ @using Lombiq.Tests.UI.Shortcuts.Controllers @using Microsoft.AspNetCore.Mvc.Localization -@using Newtonsoft.Json -@using Newtonsoft.Json.Serialization +@using System.Text.Json +@using System.Text.Json.Serialization @using OrchardCore.Mvc.Core.Utilities diff --git a/Lombiq.Tests.UI/BasicOrchardFeaturesTesting/BasicFeaturesTestingUITestContextExtensions.cs b/Lombiq.Tests.UI/BasicOrchardFeaturesTesting/BasicFeaturesTestingUITestContextExtensions.cs index bba29c32c..ae7446aab 100644 --- a/Lombiq.Tests.UI/BasicOrchardFeaturesTesting/BasicFeaturesTestingUITestContextExtensions.cs +++ b/Lombiq.Tests.UI/BasicOrchardFeaturesTesting/BasicFeaturesTestingUITestContextExtensions.cs @@ -4,6 +4,7 @@ using Lombiq.Tests.UI.Models; using Lombiq.Tests.UI.Pages; using Lombiq.Tests.UI.Services; +using OpenQA.Selenium; using Shouldly; using System; using System.Threading.Tasks; @@ -416,8 +417,8 @@ public static Task TestRegistrationWithInvalidDataAsync( async () => { var registrationPage = await context.GoToRegistrationPageAsync(); - registrationPage = await registrationPage.RegisterWithAsync(context, parameters); - registrationPage.ShouldStayOnRegistrationPage().ValidationMessages.Should.Not.BeEmpty(); + await registrationPage.RegisterWithAsync(context, parameters); + context.Exists(By.XPath("//div[contains(concat(' ', normalize-space(@class), ' '), ' validation-summary-errors ')]//li")); }); } @@ -446,11 +447,13 @@ public static Task TestRegistrationWithAlreadyRegisteredEmailAsync( async () => { var registrationPage = await context.GoToRegistrationPageAsync(); - registrationPage = await registrationPage.RegisterWithAsync(context, parameters); + await registrationPage.RegisterWithAsync(context, parameters); context.RefreshCurrentAtataContext(); - registrationPage - .ShouldStayOnRegistrationPage() - .ValidationMessages[page => page.Email].Should.BeVisible(); + + context + .Get(By.CssSelector(".text-danger.field-validation-error")) + .Text + .ShouldContain("A user with the same username already exists."); }); } @@ -544,22 +547,23 @@ public static Task TestTurningFeatureOnAndOffAsync( context.RefreshCurrentAtataContext(); + featuresPage.SearchForFeature(featureName).IsEnabled.Get(out var originalEnabledState); + featuresPage.Features[featureName].CheckBox.Check(); + featuresPage.BulkActions.Toggle.Click(); + + featuresPage + .AggregateAssert(page => page + .ShouldContainSuccessAlertMessage(TermMatch.Contains, featureName) + .AdminMenu.FindMenuItem(featureName).IsPresent.Should.Equal(!originalEnabledState) + .SearchForFeature(featureName).IsEnabled.Should.Equal(!originalEnabledState)); + featuresPage.Features[featureName].CheckBox.Check(); + featuresPage.BulkActions.Toggle.Click(); + featuresPage - .SearchForFeature(featureName).IsEnabled.Get(out bool originalEnabledState) - .Features[featureName].CheckBox.Check() - .BulkActions.Toggle.Click() - - .AggregateAssert(page => page - .ShouldContainSuccessAlertMessage(TermMatch.Contains, featureName) - .AdminMenu.FindMenuItem(featureName).IsPresent.Should.Equal(!originalEnabledState) - .SearchForFeature(featureName).IsEnabled.Should.Equal(!originalEnabledState)) - .Features[featureName].CheckBox.Check() - .BulkActions.Toggle.Click() - - .AggregateAssert(page => page - .ShouldContainSuccessAlertMessage(TermMatch.Contains, featureName) - .AdminMenu.FindMenuItem(featureName).IsPresent.Should.Equal(originalEnabledState) - .SearchForFeature(featureName).IsEnabled.Should.Equal(originalEnabledState)); + .AggregateAssert(page => page + .ShouldContainSuccessAlertMessage(TermMatch.Contains, featureName) + .AdminMenu.FindMenuItem(featureName).IsPresent.Should.Equal(originalEnabledState) + .SearchForFeature(featureName).IsEnabled.Should.Equal(originalEnabledState)); }); /// diff --git a/Lombiq.Tests.UI/BasicOrchardFeaturesTesting/MediaOperationsTestingUITestContextExtensions.cs b/Lombiq.Tests.UI/BasicOrchardFeaturesTesting/MediaOperationsTestingUITestContextExtensions.cs index ad1ab5648..827a3f85c 100644 --- a/Lombiq.Tests.UI/BasicOrchardFeaturesTesting/MediaOperationsTestingUITestContextExtensions.cs +++ b/Lombiq.Tests.UI/BasicOrchardFeaturesTesting/MediaOperationsTestingUITestContextExtensions.cs @@ -32,7 +32,7 @@ public static Task TestMediaOperationsAsync(this UITestContext context) => context.Exists(By.XPath($"//span[contains(text(), '{imageName}')]")); await context - .Get(By.CssSelector($"a[href=\"/media/{imageName}\"]").OfAnyVisibility()) + .Get(By.CssSelector($"a[href^=\"/media/{imageName}\"]").OfAnyVisibility()) .ClickReliablyAsync(context); context.SwitchToFirstWindow(); @@ -52,7 +52,7 @@ await context .ClickReliablyAsync(context); await context - .Get(By.CssSelector($"a[href=\"/media/{documentName}\"]")) + .Get(By.CssSelector($"a.btn-link[href^=\"/media/{documentName}\"]")) .ClickReliablyAsync(context); context.SwitchToFirstWindow(); diff --git a/Lombiq.Tests.UI/BasicOrchardFeaturesTesting/WorkflowsFeatureTestingUITestContextExtensions.cs b/Lombiq.Tests.UI/BasicOrchardFeaturesTesting/WorkflowsFeatureTestingUITestContextExtensions.cs index 433cef42f..88c708184 100644 --- a/Lombiq.Tests.UI/BasicOrchardFeaturesTesting/WorkflowsFeatureTestingUITestContextExtensions.cs +++ b/Lombiq.Tests.UI/BasicOrchardFeaturesTesting/WorkflowsFeatureTestingUITestContextExtensions.cs @@ -28,14 +28,14 @@ public static Task TestWorkflowsAsync(this UITestContext context) => await context.ClickReliablyOnAsync(By.XPath("//button[@data-activity-type='Event']")); await context.ClickReliablyOnAsync(By.XPath("//a[contains(@href, 'ContentPublishedEvent')]")); - await context.ClickAndFillInWithRetriesAsync(By.Id("IActivity_Title"), "Content Published Trigger"); + await context.ClickAndFillInWithRetriesAsync(By.Id("IActivity_ActivityMetadata_Title"), "Content Published Trigger"); await context.SetCheckboxValueAsync(By.XPath("//input[@value='Page']")); await context.ClickReliablyOnSubmitAsync(); await context.ClickReliablyOnAsync(By.XPath("//button[@data-activity-type='Task']")); await context.ClickReliablyOnAsync(By.XPath("//a[contains(@href, 'NotifyTask')]")); - await context.ClickAndFillInWithRetriesAsync(By.Id("IActivity_Title"), "Content Published Notification"); + await context.ClickAndFillInWithRetriesAsync(By.Id("IActivity_ActivityMetadata_Title"), "Content Published Notification"); await context.ClickAndFillInWithRetriesAsync(By.Id("NotifyTask_Message"), contentItemPublishTestSuccessMessage); await context.ClickReliablyOnSubmitAsync(); diff --git a/Lombiq.Tests.UI/Exceptions/VisualVerificationBaselineImageNotFoundException.cs b/Lombiq.Tests.UI/Exceptions/VisualVerificationBaselineImageNotFoundException.cs index 6020c70a6..d8a5ef5d1 100644 --- a/Lombiq.Tests.UI/Exceptions/VisualVerificationBaselineImageNotFoundException.cs +++ b/Lombiq.Tests.UI/Exceptions/VisualVerificationBaselineImageNotFoundException.cs @@ -1,3 +1,4 @@ +using Lombiq.Tests.UI.Services.GitHub; using System; namespace Lombiq.Tests.UI.Exceptions; @@ -16,11 +17,11 @@ public VisualVerificationBaselineImageNotFoundException( } private static string GetExceptionMessage(string path, int maxRetryCount) => - maxRetryCount == 0 ? $"Baseline image file not found, thus it was created automatically under the path {path}." - + " Please set its \"Build action\" to \"Embedded resource\" if you want to deploy a self-contained" - + " (like a NuGet package) UI testing assembly. If you run the test again, this newly created verification" - + " file will be asserted against and the assertion will pass (unless the display of the app changed in the" - + " meantime)." - : $"Baseline image file was not found under the path {path} and maxRetryCount is set to " - + $"{maxRetryCount.ToTechnicalString()}, so it won't be generated. Set maxRetryCount to 0 to generate images."; + maxRetryCount == 0 || GitHubHelper.IsGitHubEnvironment + ? $"Baseline image file not found, thus it was created automatically under the path {path}. Please set " + + $"its \"Build action\" to \"Embedded resource\" if you want to deploy a self-contained (like a NuGet " + + $"package) UI testing assembly. If you run the test again, this newly created verification file will " + + $"be asserted against and the assertion will pass (unless the display of the app changed in the meantime)." + : $"Baseline image file was not found under the path {path} and maxRetryCount is set to " + + $"{maxRetryCount.ToTechnicalString()}, so it won't be generated. Set maxRetryCount to 0 to generate images."; } diff --git a/Lombiq.Tests.UI/Extensions/ElementRetrievalUITestContextExtensions.cs b/Lombiq.Tests.UI/Extensions/ElementRetrievalUITestContextExtensions.cs index c202be07e..fd3f8038e 100644 --- a/Lombiq.Tests.UI/Extensions/ElementRetrievalUITestContextExtensions.cs +++ b/Lombiq.Tests.UI/Extensions/ElementRetrievalUITestContextExtensions.cs @@ -1,6 +1,7 @@ using Atata; using Lombiq.HelpfulLibraries.Common.Utilities; using Lombiq.Tests.UI.Services; +using Microsoft.IdentityModel.Tokens; using OpenQA.Selenium; using Shouldly; using System; @@ -120,7 +121,8 @@ public static void ErrorMessageExists(this UITestContext context, string errorMe /// public static void VerifyElementTexts(this UITestContext context, By by, params object[] toMatch) { - context.Exists(by); // Ensure content is loaded first. + // Ensure content is loaded first. + context.DoWithRetriesOrFail(() => !context.Get(by).Text.Trim().IsNullOrEmpty()); var dontCare = toMatch .Select((item, index) => item == null ? index : -1) diff --git a/Lombiq.Tests.UI/Extensions/EmailUITestContextExtensions.cs b/Lombiq.Tests.UI/Extensions/EmailUITestContextExtensions.cs index 2e70f4c74..bfe9909d5 100644 --- a/Lombiq.Tests.UI/Extensions/EmailUITestContextExtensions.cs +++ b/Lombiq.Tests.UI/Extensions/EmailUITestContextExtensions.cs @@ -53,4 +53,65 @@ public static async Task FindSpecificEmailInInboxAsync( return currentlySelectedEmail; } + + /// + /// Navigates to the /Admin/Settings/email page. + /// + public static Task GoToEmailSettingsAsync(this UITestContext context) => + context.GoToAdminRelativeUrlAsync("/Settings/email"); + + /// + /// Navigates to the /Admin/Email/Test page. + /// + public static Task GoToEmailTestAsync(this UITestContext context) => + context.GoToAdminRelativeUrlAsync("/Email/Test"); + + /// + /// Fills out the form on the email test page by specifying the recipient address, subject and message body. If the + /// is , it also clicks on the send button. + /// + public static async Task FillEmailTestFormAsync( + this UITestContext context, + string to, + string subject, + string body, + bool submit = true) + { + await context.FillInWithRetriesAsync(By.Id("To"), to); + await context.FillInWithRetriesAsync(By.Id("Subject"), subject); + await context.FillInWithRetriesAsync(By.Id("Body"), body); + + if (submit) await context.ClickReliablyOnSubmitAsync(); + } + + /// + /// A simplified version of where the + /// sender if "recipient@example.com" and the message body is "Hi, this is a test.". + /// + public static Task FillEmailTestFormAsync(this UITestContext context, string subject) => + context.FillEmailTestFormAsync("recipient@example.com", subject, "Hi, this is a test."); + + /// + /// Goes to the Email settings and sets the SMTP port to the value of . If it's then the value in the current configuration (in ) is used instead. + /// The OrchardCore.Email.Smtp feature must be enabled, but if the SMTP provider is not turned on, this will + /// automatically do it as well. + /// + public static async Task ConfigureSmtpPortAsync(this UITestContext context, int? port = null, bool publish = true) + { + await context.GoToEmailSettingsAsync(); + await context.ClickReliablyOnAsync(By.CssSelector("a[href='#tab-s-m-t-p']")); + + var byIsEnabled = By.Id("ISite_SmtpSettings_IsEnabled").OfAnyVisibility(); + if (context.Get(byIsEnabled).GetAttribute("checked") == null) + { + await context.SetCheckboxValueAsync(byIsEnabled, isChecked: true); + } + + var smtpPort = (port ?? context.Configuration.SmtpServiceConfiguration.Context.Port).ToTechnicalString(); + await context.ClickAndFillInWithRetriesAsync(By.Id("ISite_SmtpSettings_Port"), smtpPort); + + if (publish) await context.ClickReliablyOnAsync(By.ClassName("save")); + } } diff --git a/Lombiq.Tests.UI/Extensions/FormUITestContextExtensions.cs b/Lombiq.Tests.UI/Extensions/FormUITestContextExtensions.cs index 2bdf1c4d7..2d1938451 100644 --- a/Lombiq.Tests.UI/Extensions/FormUITestContextExtensions.cs +++ b/Lombiq.Tests.UI/Extensions/FormUITestContextExtensions.cs @@ -2,11 +2,11 @@ using Atata; using Lombiq.HelpfulLibraries.Common.Utilities; using Lombiq.Tests.UI.Services; -using Newtonsoft.Json; using OpenQA.Selenium; using System; using System.Globalization; using System.Linq; +using System.Text.Json; using System.Threading.Tasks; // Using the Atata namespace because that'll surely be among the using declarations of the test. OpenQA.Selenium not @@ -95,7 +95,7 @@ public static void SetMarkdownEasyMdeWysiwygEditor(this UITestContext context, s autoDownloadFontAwesome: false, }}); /* Finally set the value programmatically. */ - mde.codemirror.setValue({JsonConvert.SerializeObject(text)});"; + mde.codemirror.setValue({JsonSerializer.Serialize(text)});"; context.ExecuteScript(script); } @@ -110,7 +110,7 @@ public static void FillInMonacoEditor( { var script = $@" monaco.editor.getEditors().find((element) => - element.getContainerDomNode().id == {JsonConvert.SerializeObject(editorId)}).setValue({JsonConvert.SerializeObject(text)});"; + element.getContainerDomNode().id == {JsonSerializer.Serialize(editorId)}).setValue({JsonSerializer.Serialize(text)});"; context.ExecuteScript(script); } @@ -124,7 +124,7 @@ public static string GetMonacoEditorText( { var script = $@" return monaco.editor.getEditors().find((element) => - element.getContainerDomNode().id == {JsonConvert.SerializeObject(editorId)}).getValue();"; + element.getContainerDomNode().id == {JsonSerializer.Serialize(editorId)}).getValue();"; return context.ExecuteScript(script) as string; } diff --git a/Lombiq.Tests.UI/Extensions/NavigationUITestContextExtensions.cs b/Lombiq.Tests.UI/Extensions/NavigationUITestContextExtensions.cs index 373a7a0cf..1862f6de7 100644 --- a/Lombiq.Tests.UI/Extensions/NavigationUITestContextExtensions.cs +++ b/Lombiq.Tests.UI/Extensions/NavigationUITestContextExtensions.cs @@ -3,7 +3,6 @@ using Lombiq.Tests.UI.Helpers; using Lombiq.Tests.UI.Pages; using Lombiq.Tests.UI.Services; -using Newtonsoft.Json; using OpenQA.Selenium; using OpenQA.Selenium.Interactions; using OpenQA.Selenium.Support.UI; @@ -13,6 +12,7 @@ using System.Linq; using System.Net; using System.Net.Http; +using System.Text.Json; using System.Threading.Tasks; namespace Lombiq.Tests.UI.Extensions; @@ -255,7 +255,7 @@ public static async Task SetContentPickerByDisplayTextAsync(this UITestContext c async response => { var json = await response.Content.ReadAsStringAsync(); - var result = JsonConvert.DeserializeObject>(json); + var result = JsonSerializer.Deserialize>(json, JOptions.Default); return result.IndexOf(result.First(item => item.DisplayText == text)); }); diff --git a/Lombiq.Tests.UI/Extensions/ScriptingUITestContextExtensions.cs b/Lombiq.Tests.UI/Extensions/ScriptingUITestContextExtensions.cs index 69252faf4..af9b295f6 100644 --- a/Lombiq.Tests.UI/Extensions/ScriptingUITestContextExtensions.cs +++ b/Lombiq.Tests.UI/Extensions/ScriptingUITestContextExtensions.cs @@ -1,6 +1,6 @@ using Lombiq.Tests.UI.Services; -using Newtonsoft.Json; using OpenQA.Selenium; +using System.Text.Json; namespace Lombiq.Tests.UI.Extensions; @@ -18,7 +18,7 @@ public static object ExecuteAsyncScript(this UITestContext context, string scrip public static void SetValueWithScript(this UITestContext context, string id, object value) => ExecuteScript( context, - $"document.getElementById({JsonConvert.SerializeObject(id)}).value = {JsonConvert.SerializeObject(value)};"); + $"document.getElementById({JsonSerializer.Serialize(id)}).value = {JsonSerializer.Serialize(value)};"); /// /// Uses JavaScript to set textarea values that are hard or impossible by normal means. @@ -26,5 +26,5 @@ public static void SetValueWithScript(this UITestContext context, string id, obj public static void SetTextContentWithScript(this UITestContext context, string textareaId, object value) => ExecuteScript( context, - $"document.getElementById({JsonConvert.SerializeObject(textareaId)}).textContent = {JsonConvert.SerializeObject(value)};"); + $"document.getElementById({JsonSerializer.Serialize(textareaId)}).textContent = {JsonSerializer.Serialize(value)};"); } diff --git a/Lombiq.Tests.UI/Extensions/ShortcutsUITestContextExtensions.cs b/Lombiq.Tests.UI/Extensions/ShortcutsUITestContextExtensions.cs index e99b64b9e..908265f44 100644 --- a/Lombiq.Tests.UI/Extensions/ShortcutsUITestContextExtensions.cs +++ b/Lombiq.Tests.UI/Extensions/ShortcutsUITestContextExtensions.cs @@ -1,5 +1,4 @@ using Lombiq.HelpfulLibraries.OrchardCore.Mvc; -using Lombiq.HelpfulLibraries.Refit.Helpers; using Lombiq.Tests.UI.Constants; using Lombiq.Tests.UI.Exceptions; using Lombiq.Tests.UI.Pages; @@ -42,6 +41,7 @@ using System.Linq; using System.Net.Http; using System.Text; +using System.Text.Json; using System.Threading; using System.Threading.Tasks; @@ -419,7 +419,7 @@ private static IShortcutsApi GetApi(this UITestContext context) => BaseAddress = context.Scope.BaseUri, }; - return RefitHelper.WithNewtonsoftJson(httpClient); + return RestService.For(httpClient); }); /// @@ -666,4 +666,34 @@ private static Task UsingScopeAsync( string tenant, bool activateShell) => context.Application.UsingScopeAsync(execute, tenant ?? context.TenantName, activateShell); + + /// + /// Places the provided into a recipe and executes it with JSON Import. + /// + public static async Task ExecuteJsonRecipeAsync(this UITestContext context, params object[] steps) + { + await context.GoToAdminRelativeUrlAsync("/DeploymentPlan/Import/Json"); + + var json = JsonSerializer.Serialize(new { steps }); + await context.FillInCodeMirrorEditorWithRetriesAsync(By.ClassName("CodeMirror"), json); + await context.ClickReliablyOnAsync(By.CssSelector(".ta-content button[type='submit']")); + context.ShouldBeSuccess(); + } + + /// + /// Executes JSON Import in the admin menu with a single settings step containing the provided which may include multiple named site settings. + /// + public static Task ExecuteJsonRecipeSiteSettingsAsync(this UITestContext context, IDictionary settingsContent) + { + settingsContent["name"] = "settings"; + return context.ExecuteJsonRecipeAsync(settingsContent); + } + + /// + /// Executes JSON Import in the admin menu with a single settings step containing the provided . + /// + public static Task ExecuteJsonRecipeSiteSettingAsync(this UITestContext context, T setting) => + context.ExecuteJsonRecipeSiteSettingsAsync(new Dictionary { [typeof(T).Name] = setting }); } diff --git a/Lombiq.Tests.UI/Extensions/ShouldlyExtensions.cs b/Lombiq.Tests.UI/Extensions/ShouldlyExtensions.cs index 1d200966c..edaca482c 100644 --- a/Lombiq.Tests.UI/Extensions/ShouldlyExtensions.cs +++ b/Lombiq.Tests.UI/Extensions/ShouldlyExtensions.cs @@ -8,6 +8,8 @@ namespace Shouldly; public static class ShouldlyExtensions { + private static readonly char[] _newlineCharacters = ['\n', '\r']; + /// /// Calls on both and (unless /// they are ) and checks if they are the same. @@ -58,4 +60,13 @@ public static void ShouldBeEmptyWhen( : JsonSerializer.Serialize(results.Select(messageTransform), jsonSerializerOptions); results.ShouldBeEmpty(message); // This will always throw at this point. } + + /// + /// Compares the with the string by splitting both by lines + /// (ignoring the empty lines) and comparing the resulting arrays. + /// + public static void ShouldBeByLine(this string actual, string expected) => + actual + .Split(_newlineCharacters, StringSplitOptions.RemoveEmptyEntries) + .ShouldBe(expected.Split(_newlineCharacters, StringSplitOptions.RemoveEmptyEntries)); } diff --git a/Lombiq.Tests.UI/Extensions/VisualVerificationUITestContextExtensions.cs b/Lombiq.Tests.UI/Extensions/VisualVerificationUITestContextExtensions.cs index bd4852947..df8601d5b 100644 --- a/Lombiq.Tests.UI/Extensions/VisualVerificationUITestContextExtensions.cs +++ b/Lombiq.Tests.UI/Extensions/VisualVerificationUITestContextExtensions.cs @@ -5,6 +5,7 @@ using Lombiq.Tests.UI.Exceptions; using Lombiq.Tests.UI.Models; using Lombiq.Tests.UI.Services; +using Lombiq.Tests.UI.Services.GitHub; using OpenQA.Selenium; using Shouldly; using SixLabors.ImageSharp; @@ -389,7 +390,7 @@ private static void AssertVisualVerificationApproved( if (!File.Exists(approvedContext.BaselineImagePath)) { - if (context.Configuration.MaxRetryCount == 0) + if (context.Configuration.MaxRetryCount == 0 || GitHubHelper.IsGitHubEnvironment) { context.SaveSuggestedImage( element, diff --git a/Lombiq.Tests.UI/Helpers/ByHelper.cs b/Lombiq.Tests.UI/Helpers/ByHelper.cs index d9685fd7b..f315cfb72 100644 --- a/Lombiq.Tests.UI/Helpers/ByHelper.cs +++ b/Lombiq.Tests.UI/Helpers/ByHelper.cs @@ -1,9 +1,9 @@ using Atata; -using Newtonsoft.Json; using OpenQA.Selenium; using System; using System.Globalization; using System.Runtime.CompilerServices; +using System.Text.Json; namespace Lombiq.Tests.UI.Helpers; @@ -17,7 +17,7 @@ public static class ByHelper /// public static By SmtpInboxRow(string text) => By - .XPath($"//tr[contains(@class,'el-table__row')]//div[contains(@class,'cell')][contains(text(), {JsonConvert.SerializeObject(text)})]") + .XPath($"//tr[contains(@class,'el-table__row')]//div[contains(@class,'cell')][contains(text(), {JsonSerializer.Serialize(text)})]") .Within(TimeSpan.FromMinutes(2)); /// @@ -25,14 +25,14 @@ public static By SmtpInboxRow(string text) => /// element name restriction. /// public static By Text(string innerText, string element = "*") => - By.XPath($"//{element}[. = {JsonConvert.SerializeObject(innerText)}]"); + By.XPath($"//{element}[. = {JsonSerializer.Serialize(innerText)}]"); /// /// Returns an XPath selector that looks up elements whose text contains with optional /// element name restriction. /// public static By TextContains(string innerText, string element = "*") => - By.XPath($"//{element}[contains(., {JsonConvert.SerializeObject(innerText)})]"); + By.XPath($"//{element}[contains(., {JsonSerializer.Serialize(innerText)})]"); /// /// Creates a from an interpolated string with the invariant culture. This prevents culture- diff --git a/Lombiq.Tests.UI/Lombiq.Tests.UI.csproj b/Lombiq.Tests.UI/Lombiq.Tests.UI.csproj index ba2500bf7..1e263752c 100644 --- a/Lombiq.Tests.UI/Lombiq.Tests.UI.csproj +++ b/Lombiq.Tests.UI/Lombiq.Tests.UI.csproj @@ -72,10 +72,10 @@ - - - - + + + + diff --git a/Lombiq.Tests.UI/Models/DockerConfiguration.cs b/Lombiq.Tests.UI/Models/DockerConfiguration.cs index bee60652d..d8ac516e2 100644 --- a/Lombiq.Tests.UI/Models/DockerConfiguration.cs +++ b/Lombiq.Tests.UI/Models/DockerConfiguration.cs @@ -1,4 +1,4 @@ -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace Lombiq.Tests.UI.Models; @@ -6,6 +6,6 @@ public class DockerConfiguration { public string ContainerName { get; set; } - [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string ContainerSnapshotPath { get; set; } = "/data/Snapshots"; } diff --git a/Lombiq.Tests.UI/Pages/OrchardCoreFeaturesPage.cs b/Lombiq.Tests.UI/Pages/OrchardCoreFeaturesPage.cs index e01d5658a..8349b8c62 100644 --- a/Lombiq.Tests.UI/Pages/OrchardCoreFeaturesPage.cs +++ b/Lombiq.Tests.UI/Pages/OrchardCoreFeaturesPage.cs @@ -1,6 +1,8 @@ using Atata; using Atata.Bootstrap; using Lombiq.Tests.UI.Components; +using System; +using System.Linq; namespace Lombiq.Tests.UI.Pages; @@ -32,14 +34,16 @@ public sealed class BulkActionsDropdown : BSDropdownToggle<_> public Link<_> Toggle { get; private set; } } - [ControlDefinition("li[not(contains(@class, 'd-none'))]", ContainingClass = "list-group-item", ComponentTypeName = "feature")] + [ControlDefinition( + "li[contains(@class, 'list-group-item') and not(contains(@class, 'd-none')) and .//label[contains(@class, 'form-check-label')]]", + ComponentTypeName = "feature")] public sealed class FeatureItem : Control<_> { [FindFirst(Visibility = Visibility.Any)] [ClicksUsingActions] public CheckBox<_> CheckBox { get; private set; } - [FindByXPath("h6", "label")] + [FindByXPath("label")] public Text<_> Name { get; private set; } [FindById(TermMatch.StartsWith, "btn-enable")] @@ -59,6 +63,6 @@ public _ DisableWithConfirmation() => public sealed class FeatureItemList : ControlList { public FeatureItem this[string featureName] => - GetItem(featureName, item => item.Name == featureName); + GetAll().First(item => item.Name.Content.Value.ContainsOrdinalIgnoreCase(featureName)); } } diff --git a/Lombiq.Tests.UI/Pages/OrchardCoreLoginPage.cs b/Lombiq.Tests.UI/Pages/OrchardCoreLoginPage.cs index d307a8ff3..c6fce42fc 100644 --- a/Lombiq.Tests.UI/Pages/OrchardCoreLoginPage.cs +++ b/Lombiq.Tests.UI/Pages/OrchardCoreLoginPage.cs @@ -17,10 +17,10 @@ public class OrchardCoreLoginPage : Page<_> { private const string DefaultUrl = "Login"; - [FindById] + [FindById("LoginForm_UserName", nameof(UserName))] public TextInput<_> UserName { get; private set; } - [FindById] + [FindById("LoginForm_Password", nameof(Password))] public PasswordInput<_> Password { get; private set; } [FindByAttribute("type", "submit")] diff --git a/Lombiq.Tests.UI/Pages/OrchardCoreRegistrationPage.cs b/Lombiq.Tests.UI/Pages/OrchardCoreRegistrationPage.cs index 5f698b6c0..65903c7e9 100644 --- a/Lombiq.Tests.UI/Pages/OrchardCoreRegistrationPage.cs +++ b/Lombiq.Tests.UI/Pages/OrchardCoreRegistrationPage.cs @@ -19,20 +19,20 @@ public class OrchardCoreRegistrationPage : Page<_> { public const string DefaultUrl = "Register"; - [FindByName] + [FindById("RegisterUserForm_UserName")] public TextInput<_> UserName { get; private set; } - [FindByName] + [FindById("RegisterUserForm_Email")] [SetsValueReliably] public TextInput<_> Email { get; private set; } - [FindByName] + [FindById("RegisterUserForm_Password")] public PasswordInput<_> Password { get; private set; } - [FindByName] + [FindById("RegisterUserForm_ConfirmPassword")] public PasswordInput<_> ConfirmPassword { get; private set; } - [FindByName("RegistrationCheckbox")] + [FindById("RegisterUserForm_RegistrationCheckbox")] public CheckBox<_> PrivacyPolicyAgreement { get; private set; } [FindByAttribute("type", "submit")] diff --git a/Lombiq.Tests.UI/Services/OrchardCoreHosting/FakeStore.cs b/Lombiq.Tests.UI/Services/OrchardCoreHosting/FakeStore.cs index 894d6d2f3..b4cad4e1c 100644 --- a/Lombiq.Tests.UI/Services/OrchardCoreHosting/FakeStore.cs +++ b/Lombiq.Tests.UI/Services/OrchardCoreHosting/FakeStore.cs @@ -18,7 +18,7 @@ public sealed class FakeStore : IStore public ITypeService TypeNames => _store.TypeNames; - public ISession CreateSession() + public ISession CreateSession(bool withTracking = true) { var session = _store.CreateSession(); _createdSessions.Add(session); diff --git a/Lombiq.Tests.UI/Services/SmtpService.cs b/Lombiq.Tests.UI/Services/SmtpService.cs index 28b4d8a8a..68111bea2 100644 --- a/Lombiq.Tests.UI/Services/SmtpService.cs +++ b/Lombiq.Tests.UI/Services/SmtpService.cs @@ -1,8 +1,8 @@ using CliWrap; using Lombiq.HelpfulLibraries.Cli; -using Newtonsoft.Json.Linq; using System; using System.IO; +using System.Text.Json.Nodes; using System.Threading; using System.Threading.Tasks; @@ -10,6 +10,7 @@ namespace Lombiq.Tests.UI.Services; public class SmtpServiceConfiguration { + public SmtpServiceRunningContext Context { get; set; } } public class SmtpServiceRunningContext @@ -64,10 +65,10 @@ public async Task StartAsync() _cancellationTokenSource = new CancellationTokenSource(); var token = _cancellationTokenSource.Token; - var manifest = JObject.Parse(await File.ReadAllTextAsync(dotnetToolsConfigFilePath, token)); + var manifest = JsonNode.Parse(await File.ReadAllTextAsync(dotnetToolsConfigFilePath, token)); // Verify that an smtp4dev configuration is in place. - if ((manifest["tools"] as JObject)?["rnwood.smtp4dev"] == null) + if (manifest?["tools"]?["rnwood.smtp4dev"] == null) { throw new InvalidOperationException("There was no smtp4dev configuration in the .NET CLI local tool manifest file."); } diff --git a/Lombiq.Tests.UI/Services/UITestExecutionSession.cs b/Lombiq.Tests.UI/Services/UITestExecutionSession.cs index 2149641a7..219748eef 100644 --- a/Lombiq.Tests.UI/Services/UITestExecutionSession.cs +++ b/Lombiq.Tests.UI/Services/UITestExecutionSession.cs @@ -10,13 +10,13 @@ using Lombiq.Tests.UI.Services.GitHub; using Microsoft.VisualBasic.FileIO; using Mono.Unix; -using Newtonsoft.Json.Linq; using Selenium.Axe; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Text.Json.Nodes; using System.Threading.Tasks; using Xunit.Abstractions; using Xunit.Sdk; @@ -711,7 +711,7 @@ async Task SqlServerManagerBeforeAppStartHandlerAsync(string contentRootPath, In " wasn't found. This most possibly means that the tenant's setup failed."); } - var appSettings = JObject.Parse(await File.ReadAllTextAsync(appSettingsPath)); + var appSettings = JsonNode.Parse(await File.ReadAllTextAsync(appSettingsPath))!; appSettings[nameof(sqlServerContext.ConnectionString)] = sqlServerContext.ConnectionString; await File.WriteAllTextAsync(appSettingsPath, appSettings.ToString()); } @@ -764,6 +764,7 @@ private async Task StartSmtpServiceAsync() { _smtpService = new SmtpService(_configuration.SmtpServiceConfiguration); var smtpContext = await _smtpService.StartAsync(); + _configuration.SmtpServiceConfiguration.Context = smtpContext; Task SmtpServiceBeforeAppStartHandlerAsync(string contentRootPath, InstanceCommandLineArgumentsBuilder arguments) {