From 760f33410a56a48f060853523e12d6dac84bb57e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Mon, 12 Jun 2023 21:32:10 +0200 Subject: [PATCH 1/6] Code styling --- .../Extensions/FormUITestContextExtensions.cs | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/Lombiq.Tests.UI/Extensions/FormUITestContextExtensions.cs b/Lombiq.Tests.UI/Extensions/FormUITestContextExtensions.cs index 065a1f590..907f6eec6 100644 --- a/Lombiq.Tests.UI/Extensions/FormUITestContextExtensions.cs +++ b/Lombiq.Tests.UI/Extensions/FormUITestContextExtensions.cs @@ -77,7 +77,7 @@ public static Task ClickAndFillInTrumbowygEditorWithRetriesAsync( } /// - /// Uses Javascript to reinitialize the given field's EasyMDE instance and then access the internal CodeMirror + /// Uses JavaScript to reinitialize the given field's EasyMDE instance and then access the internal CodeMirror /// editor to programmatically change the value. This is necessary, because otherwise the editor doesn't expose the /// CodeMirror library globally for editing the existing instance and this editor can't be filled using regular /// Selenium interactions either. @@ -85,17 +85,17 @@ public static Task ClickAndFillInTrumbowygEditorWithRetriesAsync( public static void SetMarkdownEasyMdeWysiwygEditor(this UITestContext context, string id, string text) { var script = $@" - /* First get rid of the existing editor instance. */ - document.querySelector('#{id} + .EasyMDEContainer').remove(); - /* Create a new one using the same call found in OC's MarkdownBodyPart-Wysiwyg.Edit.cshtml */ - var mde = new EasyMDE({{ - element: document.getElementById('{id}'), - forceSync: true, - toolbar: mdeToolbar, - autoDownloadFontAwesome: false, - }}); - /* Finally set the value programmatically. */ - mde.codemirror.setValue({JsonConvert.SerializeObject(text)});"; + /* First get rid of the existing editor instance. */ + document.querySelector('#{id} + .EasyMDEContainer').remove(); + /* Create a new one using the same call found in OC's MarkdownBodyPart-Wysiwyg.Edit.cshtml */ + var mde = new EasyMDE({{ + element: document.getElementById('{id}'), + forceSync: true, + toolbar: mdeToolbar, + autoDownloadFontAwesome: false, + }}); + /* Finally set the value programmatically. */ + mde.codemirror.setValue({JsonConvert.SerializeObject(text)});"; context.ExecuteScript(script); } From 28b65fcaac305df615ec55a4a9e859492358eb30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Mon, 12 Jun 2023 21:34:14 +0200 Subject: [PATCH 2/6] Using the current tenant in shortcuts, even if it's not the Default one --- .../ShortcutsUITestContextExtensions.cs | 282 +++++++++--------- 1 file changed, 149 insertions(+), 133 deletions(-) diff --git a/Lombiq.Tests.UI/Extensions/ShortcutsUITestContextExtensions.cs b/Lombiq.Tests.UI/Extensions/ShortcutsUITestContextExtensions.cs index 602bf672f..14d3f3900 100644 --- a/Lombiq.Tests.UI/Extensions/ShortcutsUITestContextExtensions.cs +++ b/Lombiq.Tests.UI/Extensions/ShortcutsUITestContextExtensions.cs @@ -117,22 +117,23 @@ public static async Task GetCurrentUserNameAsync(this UITestContext cont public static Task SetUserRegistrationTypeAsync( this UITestContext context, UserRegistrationType type, - string tenant = "Default", + string tenant = null, bool activateShell = true) => - context.Application.UsingScopeAsync( - async serviceProvider => - { - var siteService = serviceProvider.GetRequiredService(); - var settings = await siteService.LoadSiteSettingsAsync(); + UsingScopeAsync( + context, + async serviceProvider => + { + var siteService = serviceProvider.GetRequiredService(); + var settings = await siteService.LoadSiteSettingsAsync(); - settings.Alter( - nameof(RegistrationSettings), - registrationSettings => registrationSettings.UsersCanRegister = type); + settings.Alter( + nameof(RegistrationSettings), + registrationSettings => registrationSettings.UsersCanRegister = type); - await siteService.UpdateSiteSettingsAsync(settings); - }, - tenant, - activateShell); + await siteService.UpdateSiteSettingsAsync(settings); + }, + tenant, + activateShell); /// /// Creates a user with the given parameters. @@ -145,35 +146,36 @@ public static Task CreateUserAsync( string userName, string password, string email, - string tenant = "Default", + string tenant = null, bool activateShell = true) => - context.Application.UsingScopeAsync( - async serviceProvider => - { - var userService = serviceProvider.GetRequiredService(); - var errors = new Dictionary(); - var user = await userService.CreateUserAsync( - new User - { - UserName = userName, - Email = email, - EmailConfirmed = true, - IsEnabled = true, - }, - password, - (key, error) => errors.Add(key, error)); - - if (user == null) + UsingScopeAsync( + context, + async serviceProvider => + { + var userService = serviceProvider.GetRequiredService(); + var errors = new Dictionary(); + var user = await userService.CreateUserAsync( + new User { - var exceptionLines = new StringBuilder(); - exceptionLines.AppendLine("User creation error:"); - errors.ForEach(entry => - exceptionLines.AppendLine(CultureInfo.InvariantCulture, $"- {entry.Key}: {entry.Value}")); - throw new CreateUserFailedException(exceptionLines.ToString()); - } - }, - tenant, - activateShell); + UserName = userName, + Email = email, + EmailConfirmed = true, + IsEnabled = true, + }, + password, + (key, error) => errors.Add(key, error)); + + if (user == null) + { + var exceptionLines = new StringBuilder(); + exceptionLines.AppendLine("User creation error:"); + errors.ForEach(entry => + exceptionLines.AppendLine(CultureInfo.InvariantCulture, $"- {entry.Key}: {entry.Value}")); + throw new CreateUserFailedException(exceptionLines.ToString()); + } + }, + tenant, + activateShell); /// /// Adds a user to a role. @@ -184,27 +186,28 @@ public static Task AddUserToRoleAsync( this UITestContext context, string userName, string roleName, - string tenant = "Default", + string tenant = null, bool activateShell = true) => - context.Application.UsingScopeAsync( - async serviceProvider => + UsingScopeAsync( + context, + async serviceProvider => + { + var userManager = serviceProvider.GetRequiredService>(); + if ((await userManager.FindByNameAsync(userName)) is not User user) { - var userManager = serviceProvider.GetRequiredService>(); - if ((await userManager.FindByNameAsync(userName)) is not User user) - { - throw new UserNotFoundException($"User with the name \"{userName}\" not found."); - } + throw new UserNotFoundException($"User with the name \"{userName}\" not found."); + } - var roleManager = serviceProvider.GetRequiredService>(); - if ((await roleManager.FindByNameAsync(roleManager.NormalizeKey(roleName))) is not Role role) - { - throw new RoleNotFoundException($"Role with the name \"{roleName}\" not found."); - } + var roleManager = serviceProvider.GetRequiredService>(); + if ((await roleManager.FindByNameAsync(roleManager.NormalizeKey(roleName))) is not Role role) + { + throw new RoleNotFoundException($"Role with the name \"{roleName}\" not found."); + } - await userManager.AddToRoleAsync(user, role.NormalizedRoleName); - }, - tenant, - activateShell); + await userManager.AddToRoleAsync(user, role.NormalizedRoleName); + }, + tenant, + activateShell); /// /// Adds a permission to a role. @@ -217,35 +220,36 @@ public static Task AddPermissionToRoleAsync( this UITestContext context, string permissionName, string roleName, - string tenant = "Default", + string tenant = null, bool activateShell = true) => - context.Application.UsingScopeAsync( - async serviceProvider => + UsingScopeAsync( + context, + async serviceProvider => + { + var roleManager = serviceProvider.GetRequiredService>(); + if ((await roleManager.FindByNameAsync(roleManager.NormalizeKey(roleName))) is not Role role) { - var roleManager = serviceProvider.GetRequiredService>(); - if ((await roleManager.FindByNameAsync(roleManager.NormalizeKey(roleName))) is not Role role) - { - throw new RoleNotFoundException($"Role with the name \"{roleName}\" not found."); - } + throw new RoleNotFoundException($"Role with the name \"{roleName}\" not found."); + } - var permissionClaim = role.RoleClaims.Find(roleClaim => - roleClaim.ClaimType == Permission.ClaimType - && roleClaim.ClaimValue == permissionName); - if (permissionClaim == null) + var permissionClaim = role.RoleClaims.Find(roleClaim => + roleClaim.ClaimType == Permission.ClaimType + && roleClaim.ClaimValue == permissionName); + if (permissionClaim == null) + { + var permissionProviders = serviceProvider.GetRequiredService>(); + if (!await PermissionExistsAsync(permissionProviders, permissionName)) { - var permissionProviders = serviceProvider.GetRequiredService>(); - if (!await PermissionExistsAsync(permissionProviders, permissionName)) - { - throw new PermissionNotFoundException($"Permission with the name \"{permissionName}\" not found."); - } + throw new PermissionNotFoundException($"Permission with the name \"{permissionName}\" not found."); + } - role.RoleClaims.Add(new() { ClaimType = Permission.ClaimType, ClaimValue = permissionName }); + role.RoleClaims.Add(new() { ClaimType = Permission.ClaimType, ClaimValue = permissionName }); - await roleManager.UpdateAsync(role); - } - }, - tenant, - activateShell); + await roleManager.UpdateAsync(role); + } + }, + tenant, + activateShell); /// /// Enables the feature with the given directly. @@ -253,20 +257,21 @@ public static Task AddPermissionToRoleAsync( public static Task EnableFeatureDirectlyAsync( this UITestContext context, string featureId, - string tenant = "Default", + string tenant = null, bool activateShell = true) => - context.Application.UsingScopeAsync( - serviceProvider => - { - var shellFeatureManager = serviceProvider.GetRequiredService(); - var extensionManager = serviceProvider.GetRequiredService(); + UsingScopeAsync( + context, + serviceProvider => + { + var shellFeatureManager = serviceProvider.GetRequiredService(); + var extensionManager = serviceProvider.GetRequiredService(); - var feature = extensionManager.GetFeature(featureId); + var feature = extensionManager.GetFeature(featureId); - return shellFeatureManager.EnableFeaturesAsync(new[] { feature }, force: true); - }, - tenant, - activateShell); + return shellFeatureManager.EnableFeaturesAsync(new[] { feature }, force: true); + }, + tenant, + activateShell); /// /// Disables the feature with the given directly. @@ -274,20 +279,21 @@ public static Task EnableFeatureDirectlyAsync( public static Task DisableFeatureDirectlyAsync( this UITestContext context, string featureId, - string tenant = "Default", + string tenant = null, bool activateShell = true) => - context.Application.UsingScopeAsync( - serviceProvider => - { - var shellFeatureManager = serviceProvider.GetRequiredService(); - var extensionManager = serviceProvider.GetRequiredService(); + UsingScopeAsync( + context, + serviceProvider => + { + var shellFeatureManager = serviceProvider.GetRequiredService(); + var extensionManager = serviceProvider.GetRequiredService(); - var feature = extensionManager.GetFeature(featureId); + var feature = extensionManager.GetFeature(featureId); - return shellFeatureManager.DisableFeaturesAsync(new[] { feature }, force: true); - }, - tenant, - activateShell); + return shellFeatureManager.DisableFeaturesAsync(new[] { feature }, force: true); + }, + tenant, + activateShell); /// /// Turns the Lombiq.Tests.UI.Shortcuts.FeatureToggleTestBench feature on, then off, and checks if the @@ -349,9 +355,10 @@ private sealed class ExecuteRecipeShortcut { } public static Task ExecuteRecipeDirectlyAsync( this UITestContext context, string recipeName, - string tenant = "Default", + string tenant = null, bool activateShell = true) => - context.Application.UsingScopeAsync( + UsingScopeAsync( + context, async serviceProvider => { try @@ -434,37 +441,38 @@ public interface IShortcutsApi public static Task SelectThemeAsync( this UITestContext context, string id, - string tenant = "Default", + string tenant = null, bool activateShell = true) => - context.Application.UsingScopeAsync( - async serviceProvider => - { - var shellFeatureManager = serviceProvider.GetRequiredService(); - var themeFeature = (await shellFeatureManager.GetAvailableFeaturesAsync()) - .FirstOrDefault(feature => feature.IsTheme() && feature.Id == id) - ?? throw new ThemeNotFoundException($"Theme with the feature ID {id} not found."); + UsingScopeAsync( + context, + async serviceProvider => + { + var shellFeatureManager = serviceProvider.GetRequiredService(); + var themeFeature = (await shellFeatureManager.GetAvailableFeaturesAsync()) + .FirstOrDefault(feature => feature.IsTheme() && feature.Id == id) + ?? throw new ThemeNotFoundException($"Theme with the feature ID {id} not found."); - if (IsAdminTheme(themeFeature.Extension.Manifest)) - { - var adminThemeService = serviceProvider.GetRequiredService(); - await adminThemeService.SetAdminThemeAsync(id); - } - else - { - var siteThemeService = serviceProvider.GetRequiredService(); - await siteThemeService.SetSiteThemeAsync(id); - } + if (IsAdminTheme(themeFeature.Extension.Manifest)) + { + var adminThemeService = serviceProvider.GetRequiredService(); + await adminThemeService.SetAdminThemeAsync(id); + } + else + { + var siteThemeService = serviceProvider.GetRequiredService(); + await siteThemeService.SetSiteThemeAsync(id); + } - var enabledFeatures = await shellFeatureManager.GetEnabledFeaturesAsync(); - var isEnabled = enabledFeatures.Any(feature => feature.Extension.Id == themeFeature.Id); + var enabledFeatures = await shellFeatureManager.GetEnabledFeaturesAsync(); + var isEnabled = enabledFeatures.Any(feature => feature.Extension.Id == themeFeature.Id); - if (!isEnabled) - { - await shellFeatureManager.EnableFeaturesAsync(new[] { themeFeature }, force: true); - } - }, - tenant, - activateShell); + if (!isEnabled) + { + await shellFeatureManager.EnableFeaturesAsync(new[] { themeFeature }, force: true); + } + }, + tenant, + activateShell); /// /// Creates, sets up, switches to (with ), and @@ -545,11 +553,12 @@ public static async Task GenerateHttpEventUrlAsync( string workflowTypeId, string activityId, int tokenLifeSpan = 0, - string tenant = "Default", + string tenant = null, bool activateShell = true) { string eventUrl = null; - await context.Application.UsingScopeAsync( + await UsingScopeAsync( + context, async serviceProvider => { var workflowTypeStore = serviceProvider.GetRequiredService(); @@ -581,4 +590,11 @@ private static async Task PermissionExistsAsync( (await Task.WhenAll(permissionProviders.Select(provider => provider.GetPermissionsAsync()))) .SelectMany(permissions => permissions) .Any(permission => permission.Name == permissionName); + + private static Task UsingScopeAsync( + UITestContext context, + Func execute, + string tenant, + bool activateShell) => + context.Application.UsingScopeAsync(execute, tenant ?? context.TenantName, activateShell); } From 8b6ca9523c36da8d74687e7c041a06364cad7337 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Mon, 12 Jun 2023 21:44:53 +0200 Subject: [PATCH 3/6] Typo --- Lombiq.Tests.UI/Extensions/ScriptingUITestContextExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lombiq.Tests.UI/Extensions/ScriptingUITestContextExtensions.cs b/Lombiq.Tests.UI/Extensions/ScriptingUITestContextExtensions.cs index 820f35f7a..8e9428e1f 100644 --- a/Lombiq.Tests.UI/Extensions/ScriptingUITestContextExtensions.cs +++ b/Lombiq.Tests.UI/Extensions/ScriptingUITestContextExtensions.cs @@ -13,7 +13,7 @@ public static object ExecuteAsyncScript(this UITestContext context, string scrip context.ExecuteLogged(nameof(ExecuteAsyncScript), script, () => context.Driver.ExecuteAsyncScript(script, args)); /// - /// Uses Javascript to set form inputs to values that are hard or impossible by normal means. + /// Uses JavaScript to set form inputs to values that are hard or impossible by normal means. /// public static void SetValueWithScript(this UITestContext context, string id, object value) => ExecuteScript( From 026f0f37a855864f7330f74405bf557423b5eb24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Mon, 12 Jun 2023 23:01:09 +0200 Subject: [PATCH 4/6] Adding SetTextContentWithScript() akin to SetValueWithScript() --- .../Extensions/ScriptingUITestContextExtensions.cs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/Lombiq.Tests.UI/Extensions/ScriptingUITestContextExtensions.cs b/Lombiq.Tests.UI/Extensions/ScriptingUITestContextExtensions.cs index 8e9428e1f..69252faf4 100644 --- a/Lombiq.Tests.UI/Extensions/ScriptingUITestContextExtensions.cs +++ b/Lombiq.Tests.UI/Extensions/ScriptingUITestContextExtensions.cs @@ -18,6 +18,13 @@ 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({JsonConvert.SerializeObject(id)}).value = {JsonConvert.SerializeObject(value)};"); + + /// + /// Uses JavaScript to set textarea values that are hard or impossible by normal means. + /// + public static void SetTextContentWithScript(this UITestContext context, string textareaId, object value) => + ExecuteScript( + context, + $"document.getElementById({JsonConvert.SerializeObject(textareaId)}).textContent = {JsonConvert.SerializeObject(value)};"); } From 441de0966c005dc9ae6f76e2ebb8c2b2d106f1cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Mon, 12 Jun 2023 23:01:19 +0200 Subject: [PATCH 5/6] Adding FillInCodeMirrorEditorWithRetriesAsync() --- .../Extensions/FormUITestContextExtensions.cs | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/Lombiq.Tests.UI/Extensions/FormUITestContextExtensions.cs b/Lombiq.Tests.UI/Extensions/FormUITestContextExtensions.cs index 907f6eec6..cb908d1d3 100644 --- a/Lombiq.Tests.UI/Extensions/FormUITestContextExtensions.cs +++ b/Lombiq.Tests.UI/Extensions/FormUITestContextExtensions.cs @@ -165,6 +165,38 @@ public static Task FillInWithRetriesUntilNotBlankAsync( timeout, interval)); + /// + /// Fills a CodeMirror editor with the given text, and retries if the value doesn't stick. + /// + public static Task FillInCodeMirrorEditorWithRetriesAsync( + this UITestContext context, + By by, + string text, + TimeSpan? timeout = null, + TimeSpan? interval = null) => + context.ExecuteLoggedAsync( + nameof(FillInCodeMirrorEditorWithRetriesAsync), + $"{by} - \"{text}\"", + () => context.DoWithRetriesOrFailAsync( + () => + { + // Approach taken from https://stackoverflow.com/a/57621266/220230. + + // Getting CodeMirror element. + var codeMirrorEditor = context.Get(by); + + // Clicking the first line of code inside CodeMirror to bring it in focus. + codeMirrorEditor.Get(By.ClassName("CodeMirror-line")).Click(); + + // Sending keystrokes to textarea once CodeMirror is in focus. + IWebElement GetTextArea() => codeMirrorEditor.Get(By.CssSelector("textarea").OfAnyVisibility()); + GetTextArea().SendKeys(text); + + return Task.FromResult(GetTextArea().GetValue() == text); + }, + timeout, + interval)); + /// /// Returns a value indicating whether the checkbox of is checked or not. /// From 0ece6ded5d2b1e0e9f978abfc5e3ac3bac4b4fcc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Mon, 12 Jun 2023 23:28:40 +0200 Subject: [PATCH 6/6] Renaming SwitchCurrentTenantToDefault() so it's in line with the base tenant switching method --- Lombiq.Tests.UI.Samples/Tests/TenantTests.cs | 2 +- Lombiq.Tests.UI/Services/UITestContext.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Lombiq.Tests.UI.Samples/Tests/TenantTests.cs b/Lombiq.Tests.UI.Samples/Tests/TenantTests.cs index 221649893..613f7e533 100644 --- a/Lombiq.Tests.UI.Samples/Tests/TenantTests.cs +++ b/Lombiq.Tests.UI.Samples/Tests/TenantTests.cs @@ -55,7 +55,7 @@ await context.CreateAndSwitchToTenantAsync( (await context.GetCurrentUserNameAsync()).ShouldBe(tenantAdminName); context.GetCurrentUri().AbsolutePath.ShouldStartWith($"/{TestTenantUrlPrefix}"); - context.ChangeCurrentTenantToDefault(); + context.SwitchCurrentTenantToDefault(); (await context.GetCurrentUserNameAsync()).ShouldBe(DefaultUser.UserName); context.GetCurrentUri().AbsolutePath.ShouldNotStartWith($"/{TestTenantUrlPrefix}"); }, diff --git a/Lombiq.Tests.UI/Services/UITestContext.cs b/Lombiq.Tests.UI/Services/UITestContext.cs index 089e25d7e..e9806ec4b 100644 --- a/Lombiq.Tests.UI/Services/UITestContext.cs +++ b/Lombiq.Tests.UI/Services/UITestContext.cs @@ -214,7 +214,7 @@ public async Task TriggerAfterPageChangeEventAndRefreshAtataContextAsync() /// /// Changes the current tenant context to the Default one. Note that this doesn't navigate the browser. /// - public void ChangeCurrentTenantToDefault() => SwitchCurrentTenant("Default", string.Empty); + public void SwitchCurrentTenantToDefault() => SwitchCurrentTenant("Default", string.Empty); /// /// Changes the current tenant context to the provided one. Note that this doesn't navigate the browser.