├── .gitattributes ├── .github └── workflows │ ├── create-jira-issues-for-community-activities.yml │ ├── publish-cloudsmith.yml │ ├── publish-nuget.yml │ ├── validate-nuget-publish.yml │ └── validate-pull-request.yml ├── .gitignore ├── License.md ├── Lombiq.Tests.UI.AppExtensions ├── Extensions │ ├── ConfigurationExtensions.cs │ └── OrchardCoreBuilderExtensions.cs ├── Lombiq.Tests.UI.AppExtensions.csproj ├── NuGetIcon.png └── Readme.md ├── Lombiq.Tests.UI.Samples ├── .htmlvalidate.json ├── Constants │ └── RecipeIds.cs ├── Extensions │ └── UITestContextExtensions.cs ├── FrontendUITestBase.cs ├── GlobalSuppressions.cs ├── Helpers │ └── SetupHelpers.cs ├── Lombiq.Tests.UI.Samples.csproj ├── Readme.md ├── Recipes │ └── Lombiq.OSOCE.Tests.Elasticsearch.recipe.json ├── Tests │ ├── AccessibilityTest.cs │ ├── AzureBlobStorageTests.cs │ ├── BasicOrchardFeaturesTests.cs │ ├── BasicTests.cs │ ├── BasicVisualVerificationTests.cs │ ├── BasicVisualVerificationTests_VerifyBlogImage_By_ClassName[Contains]_-field-name-blog-image.png │ ├── BasicVisualVerificationTests_VerifyNavbar_By_ClassName[Contains]_-navbar-brand_Unix_chrome.png │ ├── BasicVisualVerificationTests_VerifyNavbar_By_ClassName[Contains]_-navbar-brand_Unix_firefox.png │ ├── BasicVisualVerificationTests_VerifyNavbar_By_ClassName[Contains]_-navbar-brand_Win32NT_chrome-headless-shell.png │ ├── BasicVisualVerificationTests_VerifyNavbar_By_ClassName[Contains]_-navbar-brand_Win32NT_chrome.png │ ├── BasicVisualVerificationTests_VerifyNavbar_By_ClassName[Contains]_-navbar-brand_Win32NT_firefox.png │ ├── CustomZapAutomationFrameworkPlan.yml │ ├── DatabaseSnapshotTests.cs │ ├── ElasticsearchTests.cs │ ├── EmailTests.cs │ ├── ErrorHandlingTests.cs │ ├── FrontendTests.cs │ ├── InteractiveModeTests.cs │ ├── JavaScriptTests.cs │ ├── JavaScriptTests.mjs │ ├── MonkeyTests.cs │ ├── MultiBrowserTests.cs │ ├── RemoteTests.cs │ ├── SecurityScanningTests.cs │ ├── ShiftTimeTests.cs │ ├── SqlServerTests.cs │ └── TenantTests.cs ├── UITestBase.cs └── xunit.runner.json ├── Lombiq.Tests.UI.Shortcuts ├── Controllers │ ├── AccountController.cs │ ├── ApplicationInfoController.cs │ ├── CurrentUserController.cs │ ├── ErrorController.cs │ ├── FeatureToggleTestBenchController.cs │ ├── InteractiveModeController.cs │ ├── MediaCachePurgeController.cs │ ├── ShapeTableController.cs │ └── TimeShiftController.cs ├── Extensions │ └── ApplicationContextExtensions.cs ├── GlobalSuppressions.cs ├── Lombiq.Tests.UI.Shortcuts.csproj ├── Manifest.cs ├── Middlewares │ └── ExceptionContextLoggingMiddleware.cs ├── Models │ └── ApplicationInfo.cs ├── NuGetIcon.png ├── Readme.md ├── Services │ ├── ApplicationInfoInjectingFilter.cs │ ├── CdnDisabler.cs │ ├── IInteractiveModeStatusAccessor.cs │ ├── InteractiveModeStatusAccessor.cs │ └── TimeShiftingClock.cs ├── ShortcutsFeatureIds.cs ├── Startup.cs └── Views │ ├── InteractiveMode │ └── Index.cshtml │ └── _ViewImports.cshtml ├── Lombiq.Tests.UI.Tests.UI ├── Lombiq.Tests.UI.Tests.UI.csproj ├── Readme.md ├── Recipes │ └── Lombiq.Tests.UI.Tests.UI.WorkflowShortcutsTests.recipe.json └── TestCases │ ├── CustomAdminPrefixTestCases.cs │ ├── SecurityShortcutsTestCases.cs │ ├── TimeoutTestCases.cs │ └── WorkflowShortcutsTestCases.cs ├── Lombiq.Tests.UI ├── .config │ └── dotnet-tools.json ├── Attributes │ ├── AllBrowsersAttribute.cs │ ├── Behaviors │ │ └── SetsValueReliablyAttribute.cs │ ├── BrowserAttributeBase.cs │ ├── ChromeAttribute.cs │ ├── EdgeAttribute.cs │ ├── FirefoxAttribute.cs │ └── VisualVerificationApprovedMethodAttribute.cs ├── BasicOrchardFeaturesTesting │ ├── AuditTrailFeatureTestingUITestContextExtensions.cs │ ├── BasicFeaturesTestingUITestContextExtensions.cs │ ├── MediaOperationsTestingUITestContextExtensions.cs │ └── WorkflowsFeatureTestingUITestContextExtensions.cs ├── CloudflareRemoteUITestBase.cs ├── Commands.json ├── Components │ ├── AlertMessage.cs │ ├── ConfirmationModal.cs │ ├── OrchardCoreAdminMenu.cs │ ├── OrchardCoreAdminTopNavbar.cs │ ├── ValidationMessage.cs │ ├── ValidationMessageList.cs │ ├── ValidationSummary.cs │ ├── ValidationSummaryError.cs │ └── ValidationSummaryErrorList.cs ├── Constants │ ├── CommonDisplayResolutions.cs │ ├── DefaultUser.cs │ ├── DirectoryPaths.cs │ ├── TestUser.cs │ └── VisualVerificationMatchNames.cs ├── Delegates │ └── MultiSizeTest.cs ├── Docs │ ├── Attachments │ │ └── ZapReportScreenshot.png │ ├── Configuration.md │ ├── CreatingTests.md │ ├── ExecutingTests.md │ ├── FakeVideoCaptureSource.md │ ├── Limits.md │ ├── Linux.md │ ├── Migration.md │ ├── Projects.md │ ├── SecurityScanning.md │ ├── TestableOrchardCoreApps.md │ ├── Tools.md │ └── Troubleshooting.md ├── EmbeddedResourceProvider.cs ├── Exceptions │ ├── AccessibilityAssertionException.cs │ ├── CreateUserFailedException.cs │ ├── DockerFileCopyException.cs │ ├── HtmlValidationAssertionException.cs │ ├── IAssertionException.cs │ ├── PageChangeAssertionException.cs │ ├── PermissionNotFoundException.cs │ ├── RecipeNotFoundException.cs │ ├── RoleNotFoundException.cs │ ├── SetupFailedFastException.cs │ ├── TestDumpItemAlreadyExistsException.cs │ ├── ThemeNotFoundException.cs │ ├── UserNotFoundException.cs │ ├── VisualVerificationAssertionException.cs │ ├── VisualVerificationBaselineImageNotFoundException.cs │ ├── VisualVerificationCallerMethodNotFoundException.cs │ ├── VisualVerificationSourceInformationNotAvailableException.cs │ └── WorkflowTypeNotFoundException.cs ├── Extensions │ ├── AccessUITestContextExtensions.cs │ ├── AccessibilityCheckingOrchardCoreUITestExecutorConfigurationExtensions.cs │ ├── AccessibilityCheckingUITestContextExtensions.cs │ ├── ApplicationLogEnumerableExtensions.cs │ ├── AssemblyResourceExtensions.cs │ ├── AsyncExtensions.cs │ ├── AxeResultItemExtensions.cs │ ├── BasicWebElementExtensions.cs │ ├── BrowserUITestContextExtensions.cs │ ├── ControlExtensions.cs │ ├── ElementRetrievalUITestContextExtensions.cs │ ├── ElementStyleUITestContextExtensions.cs │ ├── EmailUITestContextExtensions.cs │ ├── EventsOrchardCoreUITestExecutorConfigurationExtensions.cs │ ├── ExtendedLoggingExtensions.cs │ ├── FakeBrowserVideoSourceExtensions.cs │ ├── FileUploadUITestContextExtensions.cs │ ├── FormUITestContextExtensions.cs │ ├── FormWebDriverExtensions.cs │ ├── FrontendOrchardCoreUITestExecutorConfigurationExtensions.cs │ ├── FrontendUITestContextExtensions.cs │ ├── HtmlValidationOrchardCoreUITestExecutorConfigurationExtensions.cs │ ├── HtmlValidationResultExtensions.cs │ ├── HtmlValidationUITestContextExtensions.cs │ ├── HttpClientUITestContextExtensions.cs │ ├── IExtensionManagerExtensions.cs │ ├── ImageSharpImageExtensions.cs │ ├── LocationUITestContextExtensions.cs │ ├── MediaLibraryUITestContextExtensions.cs │ ├── MonkeyTestingUITestContextExtensions.cs │ ├── MulticastDelegateExtensions.cs │ ├── NavigationUITestContextExtensions.cs │ ├── NavigationWebElementExtensions.cs │ ├── OrchardCoreBuilderExtensions.cs │ ├── OrchardCoreConfigurationExtensions.cs │ ├── OrchardCoreDashboardUITestContextExtensions.cs │ ├── ReliabilityUITestContextExtensions.cs │ ├── ResponsivenessUITestContextExtensions.cs │ ├── ScreenshotExtensions.cs │ ├── ScreenshotUITestContextExtensions.cs │ ├── ScriptingUITestContextExtensions.cs │ ├── ScriptingWebDriverExtensions.cs │ ├── ScrollingUITestContextExtensions.cs │ ├── SeleniumEntryExtensions.cs │ ├── SeleniumResponseCompletedEventExtensions.cs │ ├── ShortcutsUITestContextExtensions.cs │ ├── ShouldlyExtensions.cs │ ├── StatusCodeUITestContextExtensions.cs │ ├── TenantsUITestContextExtensions.cs │ ├── TestContextExtensions.cs │ ├── TestDumpUITestContextExtensions.cs │ ├── TestOutputHelperExtensions.cs │ ├── ThemeUITestContextExtensions.cs │ ├── TypedRouteUITestContextExtensions.cs │ ├── UsingScopeWebApplicationInstanceExtensions.cs │ ├── VerificationUITestContextExtensions.cs │ ├── VisibilityUITestContextExtensions.cs │ ├── VisualVerificationUITestContextExtensions.cs │ ├── WebApplicationInstanceExtensions.cs │ ├── WebApplicationInstanceUITestContextExtensions.cs │ └── WebDriverExceptionExtensions.cs ├── GlobalSuppressions.cs ├── Helpers │ ├── AppLogAssertionHelper.cs │ ├── ByHelper.cs │ ├── CloudflareHelper.cs │ ├── ConfigurationHelper.cs │ ├── DirectoryHelper.cs │ ├── FileUploadHelper.cs │ ├── HttpClientHelper.cs │ ├── OrchardCoreDirectoryHelper.cs │ ├── ReliabilityHelper.cs │ ├── UrlCheckHelper.cs │ └── WebAppConfigHelper.cs ├── KillLeftoverProcesses.bat ├── Lombiq.Tests.UI.csproj ├── Models │ ├── DockerConfiguration.cs │ ├── ElasticsearchRunningContext.cs │ ├── FakeBrowserVideoSource.cs │ ├── FakeBrowserVideoSourceFileFormat.cs │ ├── FakeLoggerLogApplicationLog.cs │ ├── FrontendServerContext.cs │ ├── ITestDumpItem.cs │ ├── IWebContentState.cs │ ├── InstanceCommandLineArgs.cs │ ├── JsonHtmlValidationError.cs │ ├── MemoryApplicationLog.cs │ ├── OrchardCoreAppStartContext.cs │ ├── PageNavigationState.cs │ ├── RunningContextContainer.cs │ ├── SafeWaitAsync.cs │ ├── TestDumpItem.cs │ ├── TestDumpItemGeneric.cs │ ├── UITestManifest.cs │ ├── UserRegistrationParameters.cs │ ├── VisualVerificationMatchApprovedConfiguration.cs │ ├── VisualVerificationMatchApprovedContext.cs │ └── VisualVerificationMatchConfiguration.cs ├── MonkeyTesting │ ├── GremlinsScripts.cs │ ├── IMonkeyTestingUrlFilter.cs │ ├── IMonkeyTestingUrlSanitizer.cs │ ├── MonkeyTester.cs │ ├── MonkeyTestingOptions.cs │ ├── PageMonkeyTestInfo.cs │ ├── UrlFilters │ │ ├── AdminMonkeyTestingUrlFilter.cs │ │ ├── MatchesRegexMonkeyTestingUrlFilter.cs │ │ ├── NotAdminMonkeyTestingUrlFilter.cs │ │ ├── NotStartsWithMonkeyTestingUrlFilter.cs │ │ ├── StartsWithBaseUrlMonkeyTestingUrlFilter.cs │ │ └── StartsWithMonkeyTestingUrlFilter.cs │ └── UrlSanitizers │ │ ├── RemovesBaseUrlMonkeyTestingUrlSanitizer.cs │ │ ├── RemovesByRegexMonkeyTestingUrlSanitizer.cs │ │ ├── RemovesFragmentMonkeyTestingUrlSanitizer.cs │ │ └── RemovesQueryParameterMonkeyTestingUrlSanitizer.cs ├── NuGetIcon.png ├── OrchardCoreUITestBase.cs ├── Pages │ ├── OrchardCoreAdminPage.cs │ ├── OrchardCoreContentItemsPage.cs │ ├── OrchardCoreDashboardPage.cs │ ├── OrchardCoreFeaturesPage.cs │ ├── OrchardCoreLoginPage.cs │ ├── OrchardCoreNewPageItemPage.cs │ ├── OrchardCoreRegistrationPage.cs │ ├── OrchardCoreSetupPage.cs │ └── OrchardCoreSetupParameters.cs ├── PermitNoTitleIframes.htmlvalidate.json ├── RemoteUITestBase.cs ├── SampleUploadFiles │ ├── document.pdf │ ├── image.png │ ├── uploadingtestfiledocx.docx │ └── uploadingtestfilexlsx.xlsx ├── SecurityScanning │ ├── AutomationFrameworkPlanFragments │ │ ├── DisplayActiveScanRuleRuntimesScript.yml │ │ ├── RequestorJob.yml │ │ └── SpiderAjaxJob.yml │ ├── AutomationFrameworkPlanFragmentsPaths.cs │ ├── AutomationFrameworkPlanPaths.cs │ ├── AutomationFrameworkPlans │ │ ├── Baseline.yml │ │ ├── FullScan.yml │ │ ├── GraphQL.yml │ │ └── OpenAPI.yml │ ├── OrchardCoreUITestExecutorConfigurationExtensions.cs │ ├── SecurityScanConfiguration.cs │ ├── SecurityScanResult.cs │ ├── SecurityScanningAssertionException.cs │ ├── SecurityScanningConfiguration.cs │ ├── SecurityScanningException.cs │ ├── SecurityScanningUITestContextExtensions.cs │ ├── YamlDocumentExtensions.cs │ ├── YamlHelper.cs │ ├── YamlNodeExtensions.cs │ ├── ZapEnums.cs │ └── ZapManager.cs ├── Services │ ├── AccessibilityCheckingConfiguration.cs │ ├── AtataFactory.cs │ ├── AtataScope.cs │ ├── AzureBlobStorageManager.cs │ ├── BrowserConfiguration.cs │ ├── FrontendServer.cs │ ├── GitHub │ │ ├── GitHubActionsGroupingTestOutputHelper.cs │ │ ├── GitHubActionsOutputConfiguration.cs │ │ ├── GitHubAnnotationWriter.cs │ │ └── GitHubHelper.cs │ ├── HtmlValidationConfiguration.cs │ ├── IApplicationLog.cs │ ├── ITestOutputHelperDecorator.cs │ ├── IWebApplicationInstance.cs │ ├── OrchardCoreHosting │ │ ├── FakeStore.cs │ │ ├── FakeViewCompilerProvider.cs │ │ └── OrchardApplicationFactory.cs │ ├── OrchardCoreInstance.cs │ ├── OrchardCoreSetupConfiguration.cs │ ├── OrchardCoreUITestExecutorConfiguration.cs │ ├── PortLeaseManager.cs │ ├── RemoteInstance.cs │ ├── ShortcutsConfiguration.cs │ ├── SmtpService.cs │ ├── SqlServerManager.cs │ ├── SynchronizingWebApplicationSnapshotManager.cs │ ├── TeamCityMetadataReporter.cs │ ├── TestConfigurationManager.cs │ ├── TestOutputLogConsumer.cs │ ├── TimeoutConfiguration.cs │ ├── UITestContext.cs │ ├── UITestExecutionEvents.cs │ ├── UITestExecutionSession.cs │ ├── UITestExecutor.cs │ ├── UITestExecutorTestDumpConfiguration.cs │ └── WebDriverFactory.cs ├── UITestBase.cs ├── default.htmlvalidate.json ├── package.json ├── pnpm-lock.yaml ├── ui-testing-toolkit.mjs └── xunit.runner.json ├── Lombiq.UITestingToolbox.sln ├── NuGet.config ├── Readme.md └── renovate.json5 /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | 3 | # Enforce Windows newlines for C# files to avoid false positives with IDE0055 warning. 4 | # See https://github.com/Lombiq/Open-Source-Orchard-Core-Extensions/issues/106 for more information. 5 | *.cs text eol=crlf 6 | 7 | # Keep LF line endings in pnpm-lock.yaml files to prevent Git from reporting this file as changed after pnpm touches it. 8 | pnpm-lock.yaml text eol=lf 9 | -------------------------------------------------------------------------------- /.github/workflows/create-jira-issues-for-community-activities.yml: -------------------------------------------------------------------------------- 1 | name: Create Jira issues for community activities 2 | 3 | on: 4 | discussion: 5 | types: [created] 6 | issues: 7 | types: [opened] 8 | pull_request_target: 9 | types: [opened] 10 | 11 | jobs: 12 | create-jira-issues-for-community-activities: 13 | uses: Lombiq/GitHub-Actions/.github/workflows/create-jira-issues-for-community-activities.yml@dev 14 | secrets: 15 | JIRA_BASE_URL: ${{ secrets.DEFAULT_JIRA_BASE_URL }} 16 | JIRA_USER_EMAIL: ${{ secrets.DEFAULT_JIRA_USER_EMAIL }} 17 | JIRA_API_TOKEN: ${{ secrets.DEFAULT_JIRA_API_TOKEN }} 18 | JIRA_PROJECT_KEY: ${{ secrets.DEFAULT_JIRA_PROJECT_KEY }} 19 | DISCUSSION_JIRA_ISSUE_DESCRIPTION: ${{ secrets.DEFAULT_DISCUSSION_JIRA_ISSUE_DESCRIPTION }} 20 | ISSUE_JIRA_ISSUE_DESCRIPTION: ${{ secrets.DEFAULT_ISSUE_JIRA_ISSUE_DESCRIPTION }} 21 | PULL_REQUEST_JIRA_ISSUE_DESCRIPTION: ${{ secrets.DEFAULT_PULL_REQUEST_JIRA_ISSUE_DESCRIPTION }} 22 | with: 23 | issue-component: Lombiq.UITestingToolbox 24 | -------------------------------------------------------------------------------- /.github/workflows/publish-cloudsmith.yml: -------------------------------------------------------------------------------- 1 | name: Publish to Cloudsmith 2 | 3 | on: 4 | push: 5 | tags: 6 | - v*-preview.* 7 | 8 | jobs: 9 | publish-nuget: 10 | name: Publish to Cloudsmith 11 | uses: Lombiq/GitHub-Actions/.github/workflows/publish-nuget.yml@dev 12 | with: 13 | source: https://nuget.cloudsmith.io/lombiq/open-source-orchard-core-extensions/v3/index.json 14 | secrets: 15 | API_KEY: ${{ secrets.CLOUDSMITH_NUGET_PUBLISH_API_KEY }} 16 | -------------------------------------------------------------------------------- /.github/workflows/publish-nuget.yml: -------------------------------------------------------------------------------- 1 | name: Publish to NuGet 2 | 3 | on: 4 | push: 5 | tags: 6 | - v* 7 | 8 | jobs: 9 | publish-nuget: 10 | name: Publish to NuGet 11 | if: ${{ !contains(github.ref_name, '-preview.') }} 12 | uses: Lombiq/GitHub-Actions/.github/workflows/publish-nuget.yml@dev 13 | secrets: 14 | API_KEY: ${{ secrets.DEFAULT_NUGET_PUBLISH_API_KEY }} 15 | -------------------------------------------------------------------------------- /.github/workflows/validate-nuget-publish.yml: -------------------------------------------------------------------------------- 1 | name: Validate NuGet Publish 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - dev 8 | 9 | jobs: 10 | validate-nuget-publish: 11 | name: Validate NuGet Publish 12 | uses: Lombiq/GitHub-Actions/.github/workflows/validate-nuget-publish.yml@dev 13 | -------------------------------------------------------------------------------- /.github/workflows/validate-pull-request.yml: -------------------------------------------------------------------------------- 1 | name: Validate Pull Request 2 | 3 | on: 4 | pull_request_target: 5 | 6 | jobs: 7 | validate-pull-request: 8 | uses: Lombiq/GitHub-Actions/.github/workflows/validate-submodule-pull-request.yml@dev 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vs/ 2 | .idea/ 3 | .vscode/ 4 | obj/ 5 | bin/ 6 | artifacts/ 7 | wwwroot/ 8 | node_modules/ 9 | *.user 10 | .pnpm-debug.log 11 | *.orig 12 | -------------------------------------------------------------------------------- /License.md: -------------------------------------------------------------------------------- 1 | Copyright © 2020, [Lombiq Technologies Ltd.](https://lombiq.com) 2 | 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 6 | 7 | - Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 8 | 9 | - Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 10 | 11 | - Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 12 | 13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 14 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI.AppExtensions/Extensions/ConfigurationExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.Extensions.Configuration; 2 | 3 | public static class ConfigurationExtensions 4 | { 5 | /// 6 | /// Gets a value indicating whether the application is currently running in a UI test. 7 | /// 8 | public static bool IsUITesting(this IConfiguration configuration) => 9 | configuration.GetValue("Lombiq_Tests_UI:IsUITesting", defaultValue: false); 10 | 11 | /// 12 | /// Disables the disabling of CDN usage for static resource. By default, CDN usage is disabled during UI testing, 13 | /// since tests should be possible to run in isolation. You can use this to opt out of this behavior. 14 | /// 15 | public static void DontDisableUseCdn(this IConfiguration configuration) => 16 | configuration["Lombiq_Tests_UI:DontDisableUseCdn"] = "true"; 17 | } 18 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI.AppExtensions/Extensions/OrchardCoreBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Configuration; 2 | 3 | namespace Microsoft.Extensions.DependencyInjection; 4 | 5 | public static class OrchardCoreBuilderExtensions 6 | { 7 | /// 8 | /// Enables the OrchardCore.AutoSetup feature if the doesn't indicate UI 9 | /// testing. 10 | /// 11 | public static OrchardCoreBuilder EnableAutoSetupIfNotUITesting( 12 | this OrchardCoreBuilder orchardCoreBuilder, 13 | IConfiguration configuration) => 14 | !configuration.IsUITesting() 15 | ? orchardCoreBuilder.AddSetupFeatures("OrchardCore.AutoSetup") 16 | : orchardCoreBuilder; 17 | } 18 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI.AppExtensions/Lombiq.Tests.UI.AppExtensions.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | false 6 | $(DefaultItemExcludes);.git*;node_modules\** 7 | 8 | 9 | 10 | Lombiq UI Testing Toolbox - App Extensions 11 | Lombiq Technologies 12 | Copyright © 2020, Lombiq Technologies Ltd. 13 | Lombiq UI Testing Toolbox - App Extensions: UI testing-related configuration extensions for the web app under test. See the project website for detailed documentation. 14 | NuGetIcon.png 15 | OrchardCore;Lombiq;AspNetCore;Selenium;Atata;Shouldly;xUnit;Axe;AccessibilityTesting;UITesting;Testing;Automation 16 | https://github.com/Lombiq/UI-Testing-Toolbox 17 | 18 | BSD-3-Clause 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI.AppExtensions/NuGetIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lombiq/UI-Testing-Toolbox/d8ceab170e310fa30de5d807bbc88eb31f278486/Lombiq.Tests.UI.AppExtensions/NuGetIcon.png -------------------------------------------------------------------------------- /Lombiq.Tests.UI.AppExtensions/Readme.md: -------------------------------------------------------------------------------- 1 | # Lombiq UI Testing Toolbox - App Extensions 2 | 3 | UI testing-related configuration extensions for the web app under test. 4 | 5 | For general details about and on using the Toolbox see the [root Readme](../Readme.md). 6 | 7 | Note that the module depends on [Helpful Libraries](https://github.com/Lombiq/Helpful-Libraries). 8 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI.Samples/.htmlvalidate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "./default.htmlvalidate.json" 4 | ], 5 | 6 | "rules": { 7 | "long-title": "off", 8 | "no-dup-class": "off" 9 | }, 10 | 11 | "root": true 12 | } 13 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI.Samples/Constants/RecipeIds.cs: -------------------------------------------------------------------------------- 1 | namespace Lombiq.Tests.UI.Samples.Constants; 2 | 3 | public static class RecipeIds 4 | { 5 | public const string BasicOrchardFeaturesTests = "Lombiq.OSOCE.BasicOrchardFeaturesTests"; 6 | } 7 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI.Samples/Extensions/UITestContextExtensions.cs: -------------------------------------------------------------------------------- 1 | using Lombiq.Tests.UI.Extensions; 2 | using Lombiq.Tests.UI.Services; 3 | using OpenQA.Selenium; 4 | using Shouldly; 5 | using System.Threading.Tasks; 6 | 7 | namespace Lombiq.Tests.UI.Samples.Extensions; 8 | 9 | public static class UITestContextExtensions 10 | { 11 | public static async Task CheckIfAnonymousHomePageExistsAsync(this UITestContext context) 12 | { 13 | // Is the title correct? 14 | context 15 | .Get(By.ClassName("navbar-brand")) 16 | .Text 17 | .ShouldBe("Lombiq's OSOCE - UI Testing"); 18 | 19 | // Are we logged out? 20 | (await context.GetCurrentUserNameAsync()).ShouldBeNullOrEmpty(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI.Samples/GlobalSuppressions.cs: -------------------------------------------------------------------------------- 1 | // Do you know static code analysis? It can make your life easier and your code better by automatically checking for all 2 | // kinds of formatting mistakes, potential bugs, and security issues. However, one of the analyzers don't really 3 | // understand UI tests so we have to disable it here. If you'd like to learn more about analyzers check out our project: 4 | // https://github.com/Lombiq/.NET-Analyzers. 5 | 6 | using System.Diagnostics.CodeAnalysis; 7 | 8 | // This is disabling this analyzer: https://rules.sonarsource.com/csharp/RSPEC-2699. 9 | [assembly: SuppressMessage( 10 | "Minor Code Smell", 11 | "S2699:Add at least one assertion to this test case.", 12 | Justification = "Assertions are made implicitly in UI tests.", 13 | Scope = "module")] 14 | 15 | // NEXT STATION: Let's start doing something with testing, actually, and configure how tests should work. Head over to 16 | // UITestBase.cs. 17 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI.Samples/Lombiq.Tests.UI.Samples.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | false 6 | Exe 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | PreserveNewest 19 | 20 | 21 | PreserveNewest 22 | true 23 | PreserveNewest 24 | 25 | 26 | PreserveNewest 27 | 28 | 29 | PreserveNewest 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | all 38 | runtime; build; native; contentfiles; analyzers; buildtransitive 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI.Samples/Recipes/Lombiq.OSOCE.Tests.Elasticsearch.recipe.json: -------------------------------------------------------------------------------- 1 | { 2 | // This recipe is only interesting for testing, so keeping it here in the test project, not in the web app one. 3 | "name": "Lombiq.Tests.UI.Samples.Elasticsearch", 4 | "displayName": "TEST: Elasticsearch Configuration", 5 | "description": "Elasticsearch Configuration recipe for automated UI test execution.", 6 | "author": "Lombiq Technologies", 7 | "website": "https://github.com/Lombiq/UI-Testing-Toolbox", 8 | "version": "1.0", 9 | "issetuprecipe": true, 10 | "categories": [ 11 | "test", 12 | "elasticsearch" 13 | ], 14 | "tags": [ 15 | "test", 16 | "elasticsearch" 17 | ], 18 | "steps": [ 19 | { 20 | "name": "feature", 21 | "disable": [], 22 | "enable": [ 23 | "OrchardCore.Search.Elasticsearch", 24 | "OrchardCore.Search" 25 | ] 26 | }, 27 | { 28 | "name": "ElasticIndexSettings", 29 | "Indices": [ 30 | { 31 | "elasticsearchshouldwork": { 32 | "AnalyzerName": "standard", 33 | "IndexLatest": false, 34 | "IndexedContentTypes": [ 35 | "BlogPost" 36 | ], 37 | "Culture": "any", 38 | "StoreSourceData": true 39 | } 40 | } 41 | ] 42 | }, 43 | { 44 | "name": "Settings", 45 | "ElasticSettings": { 46 | "SearchIndex": "elasticsearchshouldwork", 47 | "DefaultSearchFields": [ 48 | "Content.ContentItem.FullText" 49 | ], 50 | "AllowElasticQueryStringQueryInSearch": false 51 | } 52 | }, 53 | { 54 | "name": "recipes", 55 | "Values": [ 56 | { 57 | "executionid": "Lombiq.OSOCE.Web", 58 | "name": "Lombiq.OSOCE.Tests" 59 | } 60 | ] 61 | } 62 | ] 63 | } 64 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI.Samples/Tests/AccessibilityTest.cs: -------------------------------------------------------------------------------- 1 | using Lombiq.Tests.UI.Extensions; 2 | using Lombiq.Tests.UI.Services; 3 | using System.Threading.Tasks; 4 | using Xunit; 5 | 6 | namespace Lombiq.Tests.UI.Samples.Tests; 7 | 8 | // Here we'll see how to check some web accessibility rules. Keeping our app accessible helps people with disabilities 9 | // consume the content of our website more easily. Do note though that only checking rules that can be automatically 10 | // checked is not enough for full compliance. 11 | public class AccessibilityTest : UITestBase 12 | { 13 | public AccessibilityTest(ITestOutputHelper testOutputHelper) 14 | : base(testOutputHelper) 15 | { 16 | } 17 | 18 | [Fact] 19 | public Task FrontendPagesShouldBeAccessible() => 20 | ExecuteTestAfterSetupAsync( 21 | context => 22 | // This is just a simple test that visits two pages: The homepage, where the test will start by default, 23 | // and another one. 24 | context.GoToRelativeUrlAsync("/categories/travel"), 25 | configuration => 26 | { 27 | // We adjust the configuration just for this test but you could do the same globally in UITestBase. 28 | 29 | // With this config, accessibility rules will be checked for each page automatically. 30 | configuration.AccessibilityCheckingConfiguration.RunAccessibilityCheckingAssertionOnAllPageChanges = true; 31 | 32 | // We'll check for the WCAG 2.2 AA level. This is the middle level of the latest accessibility 33 | // guidelines. The footer widget created by the Blog recipe actually has a couple of issues. For the 34 | // sake of this sample we won't try to fix them but rather disable the corresponding rules. 35 | configuration.AccessibilityCheckingConfiguration.AxeBuilderConfigurator += axeBuilder => 36 | AccessibilityCheckingConfiguration.ConfigureWcag22aa(axeBuilder) 37 | .DisableRules("color-contrast", "link-name"); 38 | }); 39 | } 40 | 41 | // END OF TRAINING SECTION: Accessibility tests. 42 | // NEXT STATION: Head over to Tests/SqlServerTests.cs. 43 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI.Samples/Tests/AzureBlobStorageTests.cs: -------------------------------------------------------------------------------- 1 | using Lombiq.Tests.UI.Extensions; 2 | using Lombiq.Tests.UI.Samples.Extensions; 3 | using System.Threading.Tasks; 4 | using Xunit; 5 | 6 | namespace Lombiq.Tests.UI.Samples.Tests; 7 | 8 | // Up until now the Orchard app always used the default local Media storage for managing Media files. However, you may 9 | // use Azure Blob Storage in production. You can also test your app with it! 10 | public class AzureBlobStorageTests : UITestBase 11 | { 12 | public AzureBlobStorageTests(ITestOutputHelper testOutputHelper) 13 | : base(testOutputHelper) 14 | { 15 | } 16 | 17 | // Here we have basically two of the same tests as in BasicTests but now we're using Azure Blob Storage as the 18 | // site's Media storage. If they still work and there are no errors in the log then the app works with Azure Blob 19 | // Storage too. 20 | [Fact] 21 | public Task AnonymousHomePageShouldExistWithAzureBlobStorage() => 22 | ExecuteTestAfterSetupAsync( 23 | context => context.CheckIfAnonymousHomePageExistsAsync(), 24 | // Note the configuration! We could also set this globally in UITestBase. You'll need an accessible Azure 25 | // Blob Storage account. For testing we recommend the Azurite emulator 26 | // (https://docs.microsoft.com/en-us/azure/storage/common/storage-use-azurite) that can be used from tests 27 | // without any further configuration. 28 | configuration => configuration.UseAzureBlobStorage = true); 29 | 30 | [Fact] 31 | public Task TogglingFeaturesShouldWorkWithAzureBlobStorage() => 32 | ExecuteTestAfterSetupAsync( 33 | context => context.ExecuteAndAssertTestFeatureToggleAsync(), 34 | configuration => 35 | { 36 | configuration.UseAzureBlobStorage = true; 37 | 38 | configuration.ResponseLogFilter = e => 39 | e.IsNonSuccessResponseAndNotExpectedNotFoundResponse(ShortcutsUITestContextExtensions.FeatureToggleTestBenchUrl); 40 | }); 41 | } 42 | 43 | // END OF TRAINING SECTION: Using Azure Blob Storage. 44 | // NEXT STATION: Head over to Tests/ErrorHandlingTests.cs. 45 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI.Samples/Tests/BasicOrchardFeaturesTests.cs: -------------------------------------------------------------------------------- 1 | using Lombiq.Tests.UI.BasicOrchardFeaturesTesting; 2 | using Lombiq.Tests.UI.Samples.Constants; 3 | using System.Threading.Tasks; 4 | using Xunit; 5 | 6 | namespace Lombiq.Tests.UI.Samples.Tests; 7 | 8 | // The UI Testing Toolbox includes ready to use tests for some basic Orchard features as well. While the point of 9 | // writing tests for your app is not really about testing Orchard itself but nevertheless it's useful to check if all 10 | // the important features like login work - keep in mind that you can break these from your own code. So, here we run 11 | // the whole test suite. 12 | public class BasicOrchardFeaturesTests : UITestBase 13 | { 14 | public BasicOrchardFeaturesTests(ITestOutputHelper testOutputHelper) 15 | : base(testOutputHelper) 16 | { 17 | } 18 | 19 | // We could reuse the previously specified SetupHelpers.RecipeId const here but it's actually a different recipe for 20 | // this test. 21 | [Fact] 22 | public Task BasicOrchardFeaturesShouldWork() => 23 | ExecuteTestAsync(context => context.TestBasicOrchardFeaturesAsync(RecipeIds.BasicOrchardFeaturesTests)); 24 | } 25 | 26 | // END OF TRAINING SECTION: Basic Orchard features tests. 27 | // NEXT STATION: Head over to Tests/EmailTests.cs. 28 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI.Samples/Tests/BasicVisualVerificationTests_VerifyBlogImage_By_ClassName[Contains]_-field-name-blog-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lombiq/UI-Testing-Toolbox/d8ceab170e310fa30de5d807bbc88eb31f278486/Lombiq.Tests.UI.Samples/Tests/BasicVisualVerificationTests_VerifyBlogImage_By_ClassName[Contains]_-field-name-blog-image.png -------------------------------------------------------------------------------- /Lombiq.Tests.UI.Samples/Tests/BasicVisualVerificationTests_VerifyNavbar_By_ClassName[Contains]_-navbar-brand_Unix_chrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lombiq/UI-Testing-Toolbox/d8ceab170e310fa30de5d807bbc88eb31f278486/Lombiq.Tests.UI.Samples/Tests/BasicVisualVerificationTests_VerifyNavbar_By_ClassName[Contains]_-navbar-brand_Unix_chrome.png -------------------------------------------------------------------------------- /Lombiq.Tests.UI.Samples/Tests/BasicVisualVerificationTests_VerifyNavbar_By_ClassName[Contains]_-navbar-brand_Unix_firefox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lombiq/UI-Testing-Toolbox/d8ceab170e310fa30de5d807bbc88eb31f278486/Lombiq.Tests.UI.Samples/Tests/BasicVisualVerificationTests_VerifyNavbar_By_ClassName[Contains]_-navbar-brand_Unix_firefox.png -------------------------------------------------------------------------------- /Lombiq.Tests.UI.Samples/Tests/BasicVisualVerificationTests_VerifyNavbar_By_ClassName[Contains]_-navbar-brand_Win32NT_chrome-headless-shell.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lombiq/UI-Testing-Toolbox/d8ceab170e310fa30de5d807bbc88eb31f278486/Lombiq.Tests.UI.Samples/Tests/BasicVisualVerificationTests_VerifyNavbar_By_ClassName[Contains]_-navbar-brand_Win32NT_chrome-headless-shell.png -------------------------------------------------------------------------------- /Lombiq.Tests.UI.Samples/Tests/BasicVisualVerificationTests_VerifyNavbar_By_ClassName[Contains]_-navbar-brand_Win32NT_chrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lombiq/UI-Testing-Toolbox/d8ceab170e310fa30de5d807bbc88eb31f278486/Lombiq.Tests.UI.Samples/Tests/BasicVisualVerificationTests_VerifyNavbar_By_ClassName[Contains]_-navbar-brand_Win32NT_chrome.png -------------------------------------------------------------------------------- /Lombiq.Tests.UI.Samples/Tests/BasicVisualVerificationTests_VerifyNavbar_By_ClassName[Contains]_-navbar-brand_Win32NT_firefox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lombiq/UI-Testing-Toolbox/d8ceab170e310fa30de5d807bbc88eb31f278486/Lombiq.Tests.UI.Samples/Tests/BasicVisualVerificationTests_VerifyNavbar_By_ClassName[Contains]_-navbar-brand_Win32NT_firefox.png -------------------------------------------------------------------------------- /Lombiq.Tests.UI.Samples/Tests/DatabaseSnapshotTests.cs: -------------------------------------------------------------------------------- 1 | using Lombiq.Tests.UI.BasicOrchardFeaturesTesting; 2 | using Lombiq.Tests.UI.Extensions; 3 | using Lombiq.Tests.UI.Pages; 4 | using Lombiq.Tests.UI.Samples.Constants; 5 | using System.IO; 6 | using System.Threading.Tasks; 7 | using Xunit; 8 | 9 | namespace Lombiq.Tests.UI.Samples.Tests; 10 | 11 | // We can execute tests on already existing databases. To demo this, we will take a snapshot of the running application, 12 | // and use that, because we don't have a pre-beaked database. Normally this feature is used to run tests when there is 13 | // an already existing database, so you don't need to take a snapshot before using the 14 | // "ExecuteTestFromExistingDBAsync()" method. 15 | public class DatabaseSnapshotTests : UITestBase 16 | { 17 | public DatabaseSnapshotTests(ITestOutputHelper testOutputHelper) 18 | : base(testOutputHelper) 19 | { 20 | } 21 | 22 | // Here, we set up the application, then we take a snapshot of it, then we use the 23 | // "ExecuteTestFromExistingDBAsync()" to run the test on that. Finally, we test the basic Orchard features to check 24 | // that the application was set up correctly. 25 | [Fact] 26 | public async Task BasicOrchardFeaturesShouldWorkWithExistingDatabase() 27 | { 28 | var appForDatabaseTestFolder = Path.Combine("Temp", "AppForDatabaseTest"); 29 | 30 | await ExecuteTestAsync( 31 | async context => 32 | { 33 | await context.GoToSetupPageAndSetupOrchardCoreAsync(RecipeIds.BasicOrchardFeaturesTests); 34 | await context.Application.TakeSnapshotAsync(appForDatabaseTestFolder); 35 | }); 36 | 37 | await ExecuteTestFromExistingDBAsync( 38 | context => context.TestBasicOrchardFeaturesAsync(new OrchardCoreSetupParameters { SkipSetup = true }), 39 | appForDatabaseTestFolder); 40 | } 41 | } 42 | 43 | // END OF TRAINING SECTION: Database snapshot tests. 44 | // NEXT STATION: Head over to Tests/MultiBrowserTests.cs. 45 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI.Samples/Tests/JavaScriptTests.mjs: -------------------------------------------------------------------------------- 1 | import { By, until } from 'selenium-webdriver'; 2 | 3 | // This dependency is copied into the build directory by Lombiq.Tests.UI. 4 | import { runTest, shouldContainText, navigate } from '../ui-testing-toolkit.mjs'; 5 | 6 | // This function automatically handles the command line arguments and sets up a Chrome driver. 7 | await runTest(async (driver, startUrl) => { 8 | // Inside you can use all normal Selenium JavaScript code, e.g.: 9 | // - https://www.selenium.dev/selenium/docs/api/javascript/WebDriver.html 10 | // - https://www.selenium.dev/selenium/docs/api/javascript/By.html 11 | await driver.findElement(By.xpath("//a[@href = '/blog/post-1']")).click(); 12 | 13 | // We also included a shortcut function to safely check text content. 14 | await shouldContainText( 15 | await driver.findElement(By.tagName("h1")), 16 | "Man must explore, and this is exploration at its greatest"); 17 | await shouldContainText( 18 | await driver.findElement(By.className("field-name-blog-post-subtitle")), 19 | "Problems look mighty small from 150 miles up"); 20 | 21 | // And another one to navigate and safely wait for the page to load. 22 | await navigate(driver, startUrl); 23 | await driver.findElement(By.xpath("id('footer')//a[@href='https://lombiq.com/']")); 24 | }); 25 | 26 | // END OF TRAINING SECTION: Executing tests written in JavaScript. 27 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI.Samples/Tests/SqlServerTests.cs: -------------------------------------------------------------------------------- 1 | using Lombiq.Tests.UI.Extensions; 2 | using Lombiq.Tests.UI.Samples.Extensions; 3 | using System.Threading.Tasks; 4 | using Xunit; 5 | 6 | namespace Lombiq.Tests.UI.Samples.Tests; 7 | 8 | // By default, tests are executed with SQLite. However, you can also run them against a full SQL Server instance and 9 | // tests will get their DBs there (if you run your app with SQL Server in production then it's recommended to also test 10 | // with it, should there be any incompatibilities). Note that for this, you need an SQL Server instance running; by 11 | // default, this will be attempted under the default localhost server name. If you're using anything else, check out the 12 | // settings in SqlServerConfiguration and Docs/Configuration.md, especially if you use Docker. 13 | public class SqlServerTests : UITestBase 14 | { 15 | public SqlServerTests(ITestOutputHelper testOutputHelper) 16 | : base(testOutputHelper) 17 | { 18 | } 19 | 20 | // Here we have basically two of the same tests as in BasicTests but now we're using SQL Server as the site's 21 | // database. If they still work and there are no errors in the log then the app works with SQL Server too. 22 | [Fact] 23 | public Task AnonymousHomePageShouldExistWithSqlServer() => 24 | ExecuteTestAfterSetupAsync( 25 | context => context.CheckIfAnonymousHomePageExistsAsync(), 26 | // Note the configuration! We could also set this globally in UITestBase. 27 | configuration => configuration.UseSqlServer = true); 28 | 29 | [Fact] 30 | public Task TogglingFeaturesShouldWorkWithSqlServer() => 31 | ExecuteTestAfterSetupAsync( 32 | context => context.ExecuteAndAssertTestFeatureToggleAsync(), 33 | configuration => 34 | { 35 | configuration.UseSqlServer = true; 36 | 37 | configuration.ResponseLogFilter = e => 38 | e.IsNonSuccessResponseAndNotExpectedNotFoundResponse(ShortcutsUITestContextExtensions.FeatureToggleTestBenchUrl); 39 | }); 40 | } 41 | 42 | // END OF TRAINING SECTION: Using SQL Server. 43 | // NEXT STATION: Head over to Tests/AzureBlobStorageTests.cs. 44 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI.Samples/xunit.runner.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://xunit.net/schema/current/xunit.runner.schema.json", 3 | "parallelizeAssembly": false, 4 | "parallelizeTestCollections": true, 5 | "maxParallelThreads": 3 6 | } 7 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI.Shortcuts/Controllers/AccountController.cs: -------------------------------------------------------------------------------- 1 | using Lombiq.HelpfulLibraries.AspNetCore.Mvc; 2 | using Microsoft.AspNetCore.Authorization; 3 | using Microsoft.AspNetCore.Identity; 4 | using Microsoft.AspNetCore.Mvc; 5 | using OrchardCore.Users; 6 | using System.Threading.Tasks; 7 | 8 | namespace Lombiq.Tests.UI.Shortcuts.Controllers; 9 | 10 | [DevelopmentAndLocalhostOnly] 11 | public sealed class AccountController : Controller 12 | { 13 | private readonly UserManager _userManager; 14 | private readonly SignInManager _userSignInManager; 15 | 16 | public AccountController(UserManager userManager, SignInManager userSignInManager) 17 | { 18 | _userManager = userManager; 19 | _userSignInManager = userSignInManager; 20 | } 21 | 22 | [AllowAnonymous] 23 | public async Task SignInDirectly(string userName) 24 | { 25 | if (string.IsNullOrWhiteSpace(userName)) userName = "admin"; 26 | if (await _userManager.FindByNameAsync(userName) is not { } user) return NotFound(); 27 | 28 | await _userSignInManager.SignInAsync(user, isPersistent: false); 29 | 30 | return Ok(); 31 | } 32 | 33 | public async Task SignOutDirectly() 34 | { 35 | await _userSignInManager.SignOutAsync(); 36 | 37 | return Ok(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI.Shortcuts/Controllers/ApplicationInfoController.cs: -------------------------------------------------------------------------------- 1 | using Lombiq.HelpfulLibraries.AspNetCore.Mvc; 2 | using Microsoft.AspNetCore.Http; 3 | using Microsoft.AspNetCore.Mvc; 4 | using OrchardCore.Modules; 5 | 6 | namespace Lombiq.Tests.UI.Shortcuts.Controllers; 7 | 8 | [ApiController] 9 | [Route("api/ApplicationInfo")] 10 | [DevelopmentAndLocalhostOnly] 11 | public sealed class ApplicationInfoController : ControllerBase 12 | { 13 | private readonly IApplicationContext _applicationContext; 14 | 15 | public ApplicationInfoController(IApplicationContext applicationContext) => _applicationContext = applicationContext; 16 | 17 | [HttpGet] 18 | [ProducesResponseType(StatusCodes.Status200OK)] 19 | public IActionResult Get() => Ok(_applicationContext.GetApplicationInfo()); 20 | } 21 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI.Shortcuts/Controllers/CurrentUserController.cs: -------------------------------------------------------------------------------- 1 | using Lombiq.HelpfulLibraries.AspNetCore.Mvc; 2 | using Microsoft.AspNetCore.Mvc; 3 | 4 | namespace Lombiq.Tests.UI.Shortcuts.Controllers; 5 | 6 | // This needs to be consumed directly from the browser. 7 | [DevelopmentAndLocalhostOnly] 8 | public sealed class CurrentUserController : Controller 9 | { 10 | // Needs to return a string even if there's no user, otherwise it'd return an HTTP 204 without a body, see: 11 | // https://weblog.west-wind.com/posts/2020/Feb/24/Null-API-Responses-and-HTTP-204-Results-in-ASPNET-Core. 12 | public string Index() => User.Identity.IsAuthenticated ? "UserName: " + User.Identity.Name : "Unauthenticated"; 13 | } 14 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI.Shortcuts/Controllers/ErrorController.cs: -------------------------------------------------------------------------------- 1 | using Lombiq.HelpfulLibraries.AspNetCore.Mvc; 2 | using Microsoft.AspNetCore.Authorization; 3 | using Microsoft.AspNetCore.Mvc; 4 | using System; 5 | 6 | namespace Lombiq.Tests.UI.Shortcuts.Controllers; 7 | 8 | [DevelopmentAndLocalhostOnly] 9 | public sealed class ErrorController : Controller 10 | { 11 | public const string ExceptionMessage = "This action intentionally causes an exception!"; 12 | 13 | [AllowAnonymous] 14 | public IActionResult Index() => throw new InvalidOperationException(ExceptionMessage); 15 | } 16 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI.Shortcuts/Controllers/FeatureToggleTestBenchController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using OrchardCore.Modules; 3 | using System.Diagnostics.CodeAnalysis; 4 | 5 | namespace Lombiq.Tests.UI.Shortcuts.Controllers; 6 | 7 | [Feature(ShortcutsFeatureIds.FeatureToggleTestBench)] 8 | public sealed class FeatureToggleTestBenchController : Controller 9 | { 10 | // While the warning doesn't show up in VS it does with dotnet build. 11 | #pragma warning disable IDE0079 // Remove unnecessary suppression 12 | [SuppressMessage( 13 | "Performance", 14 | "CA1822:Mark members as static", 15 | Justification = "It's a controller action that needs to be instance-level.")] 16 | #pragma warning restore IDE0079 // Remove unnecessary suppression 17 | [SuppressMessage( 18 | "Minor Code Smell", 19 | "S3400:Methods should not return constants", 20 | Justification = "Necessary to check that it works when run from a test.")] 21 | public string Index() => "The Feature Toggle Test Bench worked."; 22 | } 23 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI.Shortcuts/Controllers/InteractiveModeController.cs: -------------------------------------------------------------------------------- 1 | using Lombiq.HelpfulLibraries.AspNetCore.Mvc; 2 | using Lombiq.Tests.UI.Shortcuts.Services; 3 | using Microsoft.AspNetCore.Authorization; 4 | using Microsoft.AspNetCore.Mvc; 5 | using Microsoft.AspNetCore.Mvc.Localization; 6 | using OrchardCore.DisplayManagement.Notify; 7 | using System.Threading.Tasks; 8 | 9 | namespace Lombiq.Tests.UI.Shortcuts.Controllers; 10 | 11 | [AllowAnonymous] 12 | [DevelopmentAndLocalhostOnly] 13 | public sealed class InteractiveModeController : Controller 14 | { 15 | private readonly IInteractiveModeStatusAccessor _interactiveModeStatusAccessor; 16 | private readonly INotifier _notifier; 17 | 18 | public InteractiveModeController( 19 | IInteractiveModeStatusAccessor interactiveModeStatusAccessor, 20 | INotifier notifier) 21 | { 22 | _interactiveModeStatusAccessor = interactiveModeStatusAccessor; 23 | _notifier = notifier; 24 | } 25 | 26 | public async Task Index(string notificationHtml) 27 | { 28 | _interactiveModeStatusAccessor.Enabled = true; 29 | 30 | if (!string.IsNullOrWhiteSpace(notificationHtml)) 31 | { 32 | var message = new LocalizedHtmlString(notificationHtml, notificationHtml); 33 | await _notifier.InformationAsync(message); 34 | } 35 | 36 | return View(); 37 | } 38 | 39 | [Route("api/InteractiveMode/IsInteractive")] 40 | [HttpGet] 41 | public IActionResult IsInteractive() => Json(_interactiveModeStatusAccessor.Enabled); 42 | 43 | public IActionResult Continue() 44 | { 45 | _interactiveModeStatusAccessor.Enabled = false; 46 | return Ok(); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI.Shortcuts/Controllers/MediaCachePurgeController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using OrchardCore.Media; 3 | using OrchardCore.Modules; 4 | using System.Threading.Tasks; 5 | 6 | namespace Lombiq.Tests.UI.Shortcuts.Controllers; 7 | 8 | [Feature(ShortcutsFeatureIds.MediaCachePurge)] 9 | public sealed class MediaCachePurgeController : Controller 10 | { 11 | private readonly IMediaFileStoreCache _mediaFileStoreCache; 12 | 13 | public MediaCachePurgeController(IMediaFileStoreCache mediaFileStoreCache) 14 | => _mediaFileStoreCache = mediaFileStoreCache; 15 | 16 | public async Task PurgeMediaCacheDirectly() 17 | { 18 | var hasErrors = await _mediaFileStoreCache.PurgeAsync(); 19 | 20 | return hasErrors ? StatusCode(500) : Ok(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI.Shortcuts/Controllers/ShapeTableController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using OrchardCore.Admin; 4 | using OrchardCore.DisplayManagement.Descriptors; 5 | using OrchardCore.DisplayManagement.Theming; 6 | using System.Threading.Tasks; 7 | 8 | namespace Lombiq.Tests.UI.Shortcuts.Controllers; 9 | 10 | public sealed class ShapeTableController : Controller 11 | { 12 | /// 13 | /// Prepares the shape table for the current site and admin themes. 14 | /// 15 | public async Task Prepare() 16 | { 17 | var provider = HttpContext.RequestServices; 18 | 19 | var shapeTableManager = provider.GetRequiredService(); 20 | var siteTheme = await provider.GetRequiredService().GetThemeAsync(); 21 | var adminTheme = await provider.GetRequiredService().GetAdminThemeAsync(); 22 | 23 | await shapeTableManager.GetShapeTableAsync(themeId: null); 24 | await shapeTableManager.GetShapeTableAsync(siteTheme.Id); 25 | await shapeTableManager.GetShapeTableAsync(adminTheme.Id); 26 | 27 | return Ok(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI.Shortcuts/Controllers/TimeShiftController.cs: -------------------------------------------------------------------------------- 1 | using Lombiq.HelpfulLibraries.AspNetCore.Mvc; 2 | using Lombiq.Tests.UI.Shortcuts.Services; 3 | using Microsoft.AspNetCore.Mvc; 4 | using OrchardCore.Modules; 5 | using System; 6 | 7 | namespace Lombiq.Tests.UI.Shortcuts.Controllers; 8 | 9 | [DevelopmentAndLocalhostOnly] 10 | public class TimeShiftController : Controller 11 | { 12 | private readonly IClock _clock; 13 | 14 | public TimeShiftController(IClock clock) => _clock = clock; 15 | 16 | public IActionResult Set(double days, double seconds) => 17 | SetInner(_ => TimeSpan.FromDays(days) + TimeSpan.FromSeconds(seconds)); 18 | 19 | public IActionResult Add(double days, double seconds) => 20 | SetInner(current => current + TimeSpan.FromDays(days) + TimeSpan.FromSeconds(seconds)); 21 | 22 | private IActionResult SetInner(Func edit) => 23 | TimeShiftingClock.UpdateClock(_clock, edit) is { } totalSeconds 24 | ? Ok(totalSeconds) 25 | : BadRequest($"The clock is {_clock.GetType().FullName} instead of {nameof(TimeShiftingClock)}."); 26 | } 27 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI.Shortcuts/Extensions/ApplicationContextExtensions.cs: -------------------------------------------------------------------------------- 1 | using Lombiq.Tests.UI.Shortcuts.Models; 2 | using System.Linq; 3 | 4 | namespace OrchardCore.Modules; 5 | 6 | public static class ApplicationContextExtensions 7 | { 8 | public static ApplicationInfo GetApplicationInfo(this IApplicationContext applicationContext) 9 | { 10 | var application = applicationContext.Application; 11 | 12 | return new ApplicationInfo 13 | { 14 | AppRoot = application.Root, 15 | AssemblyInfo = new AssemblyInfo 16 | { 17 | AssemblyLocation = application.Assembly.Location, 18 | AssemblyName = application.Assembly.ToString(), 19 | }, 20 | Modules = application.Modules.Select( 21 | module => new ModuleInfo 22 | { 23 | AssemblyLocation = module.Assembly.Location, 24 | AssemblyName = module.Assembly.ToString(), 25 | Assets = module.Assets.Select(asset => asset.ProjectAssetPath), 26 | }), 27 | }; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI.Shortcuts/GlobalSuppressions.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | 3 | [assembly: SuppressMessage( 4 | "Security", 5 | "SCS0027: Potential Open Redirect vulnerability was found.", 6 | Justification = "This is safe because the module is only available in UI tests.", 7 | Scope = "module")] 8 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI.Shortcuts/Middlewares/ExceptionContextLoggingMiddleware.cs: -------------------------------------------------------------------------------- 1 | using Lombiq.HelpfulLibraries.AspNetCore.Mvc; 2 | using Microsoft.AspNetCore.Http; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using Microsoft.Extensions.Logging; 5 | using System; 6 | using System.Diagnostics.CodeAnalysis; 7 | using System.Threading.Tasks; 8 | 9 | namespace Lombiq.Tests.UI.Shortcuts.Middlewares; 10 | 11 | public class ExceptionContextLoggingMiddleware 12 | { 13 | private readonly RequestDelegate _next; 14 | 15 | public ExceptionContextLoggingMiddleware(RequestDelegate next) => _next = next; 16 | 17 | [SuppressMessage( 18 | "Minor Code Smell", 19 | "S6667:Logging in a catch clause should pass the caught exception as a parameter.", 20 | Justification = "This middleware provides additional information beyond what's already logged, which " + 21 | "includes the exception, so that would be redundant.")] 22 | public async Task InvokeAsync(HttpContext context) 23 | { 24 | var logger = context.RequestServices.GetRequiredService>(); 25 | try 26 | { 27 | await _next(context); 28 | } 29 | catch (Exception exception) 30 | { 31 | logger.LogError( 32 | "HTTP request when the exception \"{ExceptionMessage}\" happened:\n{HttpContext}", 33 | exception.Message, 34 | HttpRequestInfo.ToJson(context.Request)); 35 | throw; 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI.Shortcuts/Models/ApplicationInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Diagnostics; 3 | 4 | namespace Lombiq.Tests.UI.Shortcuts.Models; 5 | 6 | public class ApplicationInfo 7 | { 8 | public string AppRoot { get; set; } 9 | public AssemblyInfo AssemblyInfo { get; set; } 10 | public IEnumerable Modules { get; set; } 11 | } 12 | 13 | [DebuggerDisplay("{AssemblyName}")] 14 | public class AssemblyInfo 15 | { 16 | public string AssemblyName { get; set; } 17 | public string AssemblyLocation { get; set; } 18 | } 19 | 20 | public class ModuleInfo : AssemblyInfo 21 | { 22 | public IEnumerable Assets { get; set; } 23 | } 24 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI.Shortcuts/NuGetIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lombiq/UI-Testing-Toolbox/d8ceab170e310fa30de5d807bbc88eb31f278486/Lombiq.Tests.UI.Shortcuts/NuGetIcon.png -------------------------------------------------------------------------------- /Lombiq.Tests.UI.Shortcuts/Readme.md: -------------------------------------------------------------------------------- 1 | # Lombiq UI Testing Toolbox - Shortcuts 2 | 3 | Provides some useful shortcuts for common operations that UI tests might want to do or check, e.g. turning features on or off, or logging in users. This way, UI tests needn't use multi-step UI processes to do these operations (and thus implicitly be coupled with and tests those features). 4 | 5 | For general details about and on using the Toolbox see the [root Readme](../Readme.md). 6 | 7 | Note that the module depends on [Helpful Libraries](https://github.com/Lombiq/Helpful-Libraries). 8 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI.Shortcuts/Services/ApplicationInfoInjectingFilter.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Html; 2 | using Microsoft.AspNetCore.Mvc.Filters; 3 | using Microsoft.Extensions.Configuration; 4 | using OrchardCore.Modules; 5 | using OrchardCore.ResourceManagement; 6 | using System; 7 | using System.Text.Json; 8 | using System.Threading.Tasks; 9 | 10 | namespace Lombiq.Tests.UI.Shortcuts.Services; 11 | 12 | public sealed class ApplicationInfoInjectingFilter : IAsyncResultFilter 13 | { 14 | private static readonly JsonSerializerOptions _indentedJsonSerializerOptions = new() { WriteIndented = true }; 15 | 16 | private readonly IResourceManager _resourceManager; 17 | private readonly IConfiguration _shellConfiguration; 18 | private readonly IApplicationContext _applicationContext; 19 | 20 | public ApplicationInfoInjectingFilter( 21 | IResourceManager resourceManager, 22 | IConfiguration shellConfiguration, 23 | IApplicationContext applicationContext) 24 | { 25 | _resourceManager = resourceManager; 26 | _shellConfiguration = shellConfiguration; 27 | _applicationContext = applicationContext; 28 | } 29 | 30 | public async Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next) 31 | { 32 | if (context.IsNotFullViewRendering() || !_shellConfiguration.GetValue("Lombiq_Tests_UI:InjectApplicationInfo", defaultValue: false)) 33 | { 34 | await next(); 35 | return; 36 | } 37 | 38 | _resourceManager.RegisterHeadScript(new HtmlString( 39 | $"")); 43 | 44 | await next(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI.Shortcuts/Services/CdnDisabler.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Configuration; 2 | using OrchardCore.Modules; 3 | using OrchardCore.Settings; 4 | using System.Threading.Tasks; 5 | 6 | namespace Lombiq.Tests.UI.Shortcuts.Services; 7 | 8 | internal sealed class CdnDisabler : ModularTenantEvents 9 | { 10 | private readonly ISiteService _siteService; 11 | private readonly IConfiguration _shellConfiguration; 12 | 13 | public CdnDisabler(ISiteService siteService, IConfiguration shellConfiguration) 14 | { 15 | _siteService = siteService; 16 | _shellConfiguration = shellConfiguration; 17 | } 18 | 19 | public override async Task ActivatedAsync() 20 | { 21 | if (_shellConfiguration.GetValue("Lombiq_Tests_UI:DontDisableUseCdn")) 22 | { 23 | return; 24 | } 25 | 26 | var site = await _siteService.LoadSiteSettingsAsync(); 27 | site.UseCdn = false; 28 | await _siteService.UpdateSiteSettingsAsync(site); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI.Shortcuts/Services/IInteractiveModeStatusAccessor.cs: -------------------------------------------------------------------------------- 1 | namespace Lombiq.Tests.UI.Shortcuts.Services; 2 | 3 | /// 4 | /// A container for the flag indicating if interactive mode is enabled. 5 | /// 6 | public interface IInteractiveModeStatusAccessor 7 | { 8 | /// 9 | /// Gets or sets a value indicating whether interactive mode is enabled. 10 | /// 11 | bool Enabled { get; set; } 12 | } 13 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI.Shortcuts/Services/InteractiveModeStatusAccessor.cs: -------------------------------------------------------------------------------- 1 | namespace Lombiq.Tests.UI.Shortcuts.Services; 2 | 3 | public class InteractiveModeStatusAccessor : IInteractiveModeStatusAccessor 4 | { 5 | public bool Enabled { get; set; } 6 | } 7 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI.Shortcuts/ShortcutsFeatureIds.cs: -------------------------------------------------------------------------------- 1 | namespace Lombiq.Tests.UI.Shortcuts; 2 | 3 | public static class ShortcutsFeatureIds 4 | { 5 | internal const string DescriptionUiTestWarning = "WARNING: Only enable this feature in the UI testing environment. "; 6 | 7 | public const string Area = "Lombiq.Tests.UI.Shortcuts"; 8 | 9 | public const string Default = Area; 10 | public const string FeatureToggleTestBench = $"{Default}.{nameof(FeatureToggleTestBench)}"; 11 | public const string MediaCachePurge = $"{Default}.{nameof(MediaCachePurge)}"; 12 | public const string ShiftTime = $"{Default}.{nameof(ShiftTime)}"; 13 | public const string Swagger = $"{Default}.{nameof(Swagger)}"; 14 | public const string Workflows = $"{Default}.{nameof(Workflows)}"; 15 | } 16 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI.Shortcuts/Startup.cs: -------------------------------------------------------------------------------- 1 | using Lombiq.HelpfulLibraries.AspNetCore.Extensions; 2 | using Lombiq.Tests.UI.Shortcuts.Middlewares; 3 | using Lombiq.Tests.UI.Shortcuts.Services; 4 | using Microsoft.AspNetCore.Builder; 5 | using Microsoft.AspNetCore.Routing; 6 | using Microsoft.Extensions.DependencyInjection; 7 | using Microsoft.OpenApi.Models; 8 | using OrchardCore.Data.YesSql; 9 | using OrchardCore.Modules; 10 | using System; 11 | 12 | namespace Lombiq.Tests.UI.Shortcuts; 13 | 14 | public sealed class Startup : StartupBase 15 | { 16 | public override void ConfigureServices(IServiceCollection services) 17 | { 18 | services.AddSingleton(); 19 | services.AddAsyncResultFilter(); 20 | services.AddScoped(); 21 | 22 | // To ensure we don't encounter any concurrency issue, enable EnableThreadSafetyChecks for all tests. 23 | services.Configure(options => options.EnableThreadSafetyChecks = true); 24 | } 25 | 26 | public override void Configure(IApplicationBuilder app, IEndpointRouteBuilder routes, IServiceProvider serviceProvider) => 27 | app.UseMiddleware(); 28 | } 29 | 30 | [Feature(ShortcutsFeatureIds.Swagger)] 31 | public sealed class SwaggerStartup : StartupBase 32 | { 33 | public override void ConfigureServices(IServiceCollection services) => 34 | services.AddSwaggerGen(swaggerGenOptions => 35 | swaggerGenOptions.SwaggerDoc("v1", new OpenApiInfo { Title = "Orchard Core API", Version = "v1" })); 36 | 37 | public override void Configure(IApplicationBuilder app, IEndpointRouteBuilder routes, IServiceProvider serviceProvider) => 38 | app.UseSwagger(); 39 | } 40 | 41 | [Feature(ShortcutsFeatureIds.ShiftTime)] 42 | public sealed class SetTimeStartup : StartupBase 43 | { 44 | public override void ConfigureServices(IServiceCollection services) 45 | { 46 | services.RemoveImplementationsOf(); 47 | services.AddSingleton(); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI.Shortcuts/Views/InteractiveMode/Index.cshtml: -------------------------------------------------------------------------------- 1 | 48 | 49 | 50 | 51 | @T["Interactive Mode"] 52 | 53 | @T["If you see this, your code has called the context.SwitchToInteractiveAsync() method."] 54 | @T["This stops the test evaluation and waits until you click the button below."] 55 | 56 | 57 | @T["Continue Test"] 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI.Shortcuts/Views/_ViewImports.cshtml: -------------------------------------------------------------------------------- 1 | @inherits OrchardCore.DisplayManagement.Razor.RazorPage 2 | 3 | @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers 4 | @addTagHelper *, OrchardCore.Contents.TagHelpers 5 | @addTagHelper *, OrchardCore.DisplayManagement 6 | @addTagHelper *, OrchardCore.ResourceManagement 7 | 8 | @using Lombiq.Tests.UI.Shortcuts.Controllers 9 | @using Microsoft.AspNetCore.Mvc.Localization 10 | @using System.Text.Json 11 | @using System.Text.Json.Serialization 12 | @using OrchardCore.Mvc.Core.Utilities 13 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI.Tests.UI/Lombiq.Tests.UI.Tests.UI.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | Never 16 | PreserveNewest 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI.Tests.UI/Readme.md: -------------------------------------------------------------------------------- 1 | # Lombiq UI Testing Toolbox - UI Tests 2 | 3 | UI test project that tests the UI Testing Toolbox itself. The project is intentionally not published on NuGet, since it's only needed for the UI Testing Toolbox. 4 | 5 | For general details about and on using the Toolbox see the [root Readme](../Readme.md). 6 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI.Tests.UI/Recipes/Lombiq.Tests.UI.Tests.UI.WorkflowShortcutsTests.recipe.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Lombiq.Tests.UI.Tests.UI.WorkflowShortcutsTests", 3 | "displayName": "Lombiq UI Testing Toolbox - UI Tests - WorkflowShortcutsTests", 4 | "description": "Test recipe for WorkflowShortcutsTests.", 5 | "author": "Lombiq Technologies", 6 | "website": "https://github.com/Lombiq/UI-Testing-Toolbox", 7 | "version": "1.0", 8 | "issetuprecipe": false, 9 | "categories": [ 10 | "test" 11 | ], 12 | "tags": [ 13 | "hidden" 14 | ], 15 | "variables": { 16 | "testWorkflowTypeId": "testworkflow000000", 17 | "testHttpRequestActivityId": "testhttpevent00000" 18 | }, 19 | "steps": [ 20 | { 21 | "name": "Feature", 22 | "enable": [ 23 | "OrchardCore.Workflows.Http" 24 | ] 25 | }, 26 | { 27 | "name": "WorkflowType", 28 | "data": [ 29 | { 30 | "WorkflowTypeId": "[js: variables('testWorkflowTypeId')]", 31 | "Name": "Test", 32 | "IsEnabled": true, 33 | "IsSingleton": false, 34 | "LockTimeout": 0, 35 | "LockExpiration": 0, 36 | "DeleteFinishedWorkflows": false, 37 | "Activities": [ 38 | { 39 | "ActivityId": "[js: variables('testHttpRequestActivityId')]", 40 | "Name": "HttpRequestEvent", 41 | "X": 20, 42 | "Y": 100, 43 | "IsStart": true, 44 | "Properties": { 45 | "ActivityMetadata": { 46 | "Title": "Test HTTP Request" 47 | }, 48 | "HttpMethod": "GET", 49 | "Url": "/workflows/Invoke?token=CfDJ8BtxLPJD0O1AigqobIx0mBsmvYzTrbwFAac2QNMffPoT5KmTI7-hinxabRodKzV3XOfrrt2st8ekOh3y3i0976hmqhnVYMJ6lLbd0UtmmhqJueJPAhNkhrijb6_nMIhtdRy_y3ixLInICBlX3TJjp0fVpaqGwUWL6B4fw-ldbCm9lq0henb_QH27PuAUnoSTy-PXUyYJmgLml9C9bV6v3l0CMVMmQ8HUiirl_Fx5cp7k", 50 | "ValidateAntiforgeryToken": true, 51 | "TokenLifeSpan": 0 52 | } 53 | } 54 | ], 55 | "Transitions": [] 56 | } 57 | ] 58 | } 59 | ] 60 | } 61 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI.Tests.UI/TestCases/CustomAdminPrefixTestCases.cs: -------------------------------------------------------------------------------- 1 | using Lombiq.Tests.UI.Extensions; 2 | using Lombiq.Tests.UI.Services; 3 | 4 | namespace Lombiq.Tests.UI.Tests.UI.TestCases; 5 | 6 | public static class CustomAdminPrefixTestCases 7 | { 8 | public static Task NavigationWithCustomAdminPrefixShouldWorkAsync( 9 | ExecuteTestAfterSetupAsync executeTestAfterSetupAsync, Browser browser = default) => 10 | executeTestAfterSetupAsync( 11 | async context => 12 | { 13 | context.AdminUrlPrefix = "custom-admin"; 14 | 15 | await context.SignInDirectlyAsync(); 16 | await context.GoToDashboardAsync(); 17 | await context.GoToFeaturesPageAsync(); 18 | await context.GoToContentItemListAsync("Blog"); 19 | await context.GoToContentItemsPageAsync(); 20 | }, 21 | browser, 22 | configuration => 23 | { 24 | configuration.HtmlValidationConfiguration.RunHtmlValidationAssertionOnAllPageChanges = false; 25 | 26 | configuration.OrchardCoreConfiguration.BeforeAppStart += (_, argsBuilder) => 27 | { 28 | argsBuilder.AddWithValue("OrchardCore:OrchardCore_Admin:AdminUrlPrefix", "custom-admin"); 29 | 30 | return Task.CompletedTask; 31 | }; 32 | 33 | // Using a custom setup operation so the UI Testing Toolbox will consider it unique, and not reuse its 34 | // snapshot with other tests if this one ends up running first. This is necessary because the 35 | // AdminUrlPrefix is saved to the DB by the OrchardCore.AdminMenu feature, like for the admin menu's 36 | // Blog item; we don't want the other tests to use that. 37 | var originalSetupOperation = configuration.SetupConfiguration.SetupOperation; 38 | configuration.SetupConfiguration.SetupOperation = context => originalSetupOperation(context); 39 | 40 | return Task.CompletedTask; 41 | }); 42 | } 43 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI.Tests.UI/TestCases/TimeoutTestCases.cs: -------------------------------------------------------------------------------- 1 | using Lombiq.Tests.UI.Services; 2 | using Shouldly; 3 | 4 | namespace Lombiq.Tests.UI.Tests.UI.TestCases; 5 | 6 | public static class TimeoutTestCases 7 | { 8 | public static Task TestRunTimeoutShouldThrowAsync( 9 | ExecuteTestAfterSetupWithoutBrowserAsync executeTestAfterSetupWithoutBrowserAsync, 10 | Browser browser = Browser.None) => 11 | Should.ThrowAsync( 12 | async () => await executeTestAfterSetupWithoutBrowserAsync( 13 | context => Task.Delay(TimeSpan.FromSeconds(1), context.Configuration.TestCancellationToken), 14 | configuration => 15 | { 16 | configuration.HtmlValidationConfiguration.RunHtmlValidationAssertionOnAllPageChanges = false; 17 | configuration.MaxRetryCount = 0; 18 | configuration.TestDumpConfiguration.CreateTestDump = false; 19 | configuration.SetupConfiguration.SetupOperation = context => Task.FromResult(new Uri("http://example.com")); 20 | 21 | configuration.TimeoutConfiguration.TestRunTimeout = TimeSpan.FromMilliseconds(10); 22 | 23 | return Task.CompletedTask; 24 | }), 25 | typeof(TimeoutException)); 26 | } 27 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI.Tests.UI/TestCases/WorkflowShortcutsTestCases.cs: -------------------------------------------------------------------------------- 1 | using Lombiq.Tests.UI.Extensions; 2 | using Lombiq.Tests.UI.Helpers; 3 | using Lombiq.Tests.UI.Services; 4 | using Lombiq.Tests.UI.Shortcuts; 5 | using Shouldly; 6 | 7 | namespace Lombiq.Tests.UI.Tests.UI.TestCases; 8 | 9 | public static class WorkflowShortcutsTestCases 10 | { 11 | public static Task GenerateHttpEventUrlShouldWorkAsync( 12 | ExecuteTestAfterSetupAsync executeTestAfterSetupAsync, Browser browser = default) => 13 | executeTestAfterSetupAsync( 14 | async context => 15 | { 16 | await context.EnableFeatureDirectlyAsync(ShortcutsFeatureIds.Workflows); 17 | await context.ExecuteRecipeDirectlyAsync("Lombiq.Tests.UI.Tests.UI.WorkflowShortcutsTests"); 18 | (await context.GenerateHttpEventUrlAsync("testworkflow000000", "testhttpevent00000")) 19 | .ShouldStartWith("/workflows/Invoke?token="); 20 | }, 21 | browser, 22 | ConfigurationHelper.DisableHtmlValidation); 23 | } 24 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/.config/dotnet-tools.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "isRoot": true, 4 | "tools": { 5 | "rnwood.smtp4dev": { 6 | "version": "3.8.6", 7 | "commands": [ 8 | "smtp4dev" 9 | ] 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/Attributes/AllBrowsersAttribute.cs: -------------------------------------------------------------------------------- 1 | using Lombiq.Tests.UI.Services; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Reflection; 5 | using System.Threading.Tasks; 6 | using Xunit; 7 | using Xunit.Sdk; 8 | using Xunit.v3; 9 | 10 | namespace Lombiq.Tests.UI.Attributes; 11 | 12 | [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)] 13 | public sealed class AllBrowsersAttribute : DataAttribute 14 | { 15 | public override ValueTask> GetData(MethodInfo testMethod, DisposalTracker disposalTracker) 16 | { 17 | var browsers = (IEnumerable)Enum.GetValues(typeof(Browser)); 18 | var dataRows = new List(); 19 | 20 | foreach (var browser in browsers) 21 | { 22 | dataRows.Add(new TheoryDataRow(browser)); 23 | } 24 | 25 | return new ValueTask>(dataRows.AsReadOnly()); 26 | } 27 | 28 | public override bool SupportsDiscoveryEnumeration() => true; 29 | } 30 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/Attributes/Behaviors/SetsValueReliablyAttribute.cs: -------------------------------------------------------------------------------- 1 | using Atata; 2 | using Lombiq.Tests.UI.Extensions; 3 | using Lombiq.Tests.UI.Helpers; 4 | using System; 5 | using System.Threading; 6 | 7 | namespace Lombiq.Tests.UI.Attributes.Behaviors; 8 | 9 | [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)] 10 | public sealed class SetsValueReliablyAttribute : ValueSetBehaviorAttribute 11 | { 12 | public override void Execute(IUIComponent component, string value) 13 | { 14 | var element = component.Scope; 15 | var driver = component.Context.Driver; 16 | 17 | ReliabilityHelper.DoWithRetriesOrFail( 18 | () => driver.TryFillElement(element, value).GetValue() == value, 19 | cancellationToken: CancellationToken.None); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/Attributes/BrowserAttributeBase.cs: -------------------------------------------------------------------------------- 1 | using Lombiq.Tests.UI.Services; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Reflection; 5 | using System.Threading.Tasks; 6 | using Xunit; 7 | using Xunit.Sdk; 8 | using Xunit.v3; 9 | 10 | namespace Lombiq.Tests.UI.Attributes; 11 | 12 | [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)] 13 | public abstract class BrowserAttributeBase : DataAttribute 14 | { 15 | protected abstract Browser Browser { get; } 16 | 17 | public override ValueTask> GetData(MethodInfo testMethod, DisposalTracker disposalTracker) => 18 | new(new[] { new TheoryDataRow(Browser) }.AsReadOnly()); 19 | 20 | public override bool SupportsDiscoveryEnumeration() => true; 21 | } 22 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/Attributes/ChromeAttribute.cs: -------------------------------------------------------------------------------- 1 | using Lombiq.Tests.UI.Services; 2 | using System; 3 | 4 | namespace Lombiq.Tests.UI.Attributes; 5 | 6 | [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)] 7 | public sealed class ChromeAttribute : BrowserAttributeBase 8 | { 9 | protected override Browser Browser => Browser.Chrome; 10 | } 11 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/Attributes/EdgeAttribute.cs: -------------------------------------------------------------------------------- 1 | using Lombiq.Tests.UI.Services; 2 | using System; 3 | 4 | namespace Lombiq.Tests.UI.Attributes; 5 | 6 | [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)] 7 | public sealed class EdgeAttribute : BrowserAttributeBase 8 | { 9 | protected override Browser Browser => Browser.Edge; 10 | } 11 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/Attributes/FirefoxAttribute.cs: -------------------------------------------------------------------------------- 1 | using Lombiq.Tests.UI.Services; 2 | using System; 3 | 4 | namespace Lombiq.Tests.UI.Attributes; 5 | 6 | [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)] 7 | public sealed class FirefoxAttribute : BrowserAttributeBase 8 | { 9 | protected override Browser Browser => Browser.Firefox; 10 | } 11 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/Attributes/VisualVerificationApprovedMethodAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Lombiq.Tests.UI.Attributes; 4 | 5 | /// 6 | /// This attribute is used to annotate the VisualVerificationApproved methods in the call stack to get the consumer 7 | /// method. 8 | /// 9 | [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)] 10 | public sealed class VisualVerificationApprovedMethodAttribute : Attribute 11 | { 12 | } 13 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/Commands.json: -------------------------------------------------------------------------------- 1 | { 2 | "commands": { 3 | "KillLeftoverProcesses": { 4 | "fileName": "cmd.exe", 5 | "workingDirectory": ".", 6 | "arguments": "/c KillLeftoverProcesses.bat" 7 | } 8 | } 9 | } -------------------------------------------------------------------------------- /Lombiq.Tests.UI/Components/AlertMessage.cs: -------------------------------------------------------------------------------- 1 | using Atata; 2 | using System.Linq; 3 | 4 | namespace Lombiq.Tests.UI.Components; 5 | 6 | [ControlDefinition(ContainingClass = "alert")] 7 | public class AlertMessage : Control 8 | where TOwner : PageObject 9 | { 10 | [UseParentScope] 11 | [GetsContentFromSource(ContentSource.FirstChildTextNode)] 12 | public Text Text { get; private set; } 13 | 14 | public ValueProvider IsSuccess => 15 | CreateValueProvider("success state", GetIsSuccess); 16 | 17 | private bool GetIsSuccess() => 18 | DomClasses.Value.Contains("message-success"); 19 | } 20 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/Components/ConfirmationModal.cs: -------------------------------------------------------------------------------- 1 | using Atata; 2 | using Atata.Bootstrap; 3 | 4 | namespace Lombiq.Tests.UI.Components; 5 | 6 | public sealed class ConfirmationModal : BSModal> 7 | where TNavigateTo : PageObject 8 | { 9 | [FindById("modalOkButton")] 10 | public Button> Yes { get; private set; } 11 | 12 | [FindById("modalCancelButton")] 13 | public Button> No { get; private set; } 14 | } 15 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/Components/OrchardCoreAdminMenu.cs: -------------------------------------------------------------------------------- 1 | using Atata; 2 | 3 | namespace Lombiq.Tests.UI.Components; 4 | 5 | [ControlDefinition("ul", ContainingClass = "menu-admin", ComponentTypeName = "menu", IgnoreNameEndings = "Menu")] 6 | public sealed class OrchardCoreAdminMenu : HierarchicalUnorderedList.MenuItem, TOwner> 7 | where TOwner : PageObject 8 | { 9 | public MenuItem FindMenuItem(string menuItemName) => 10 | Descendants.GetByXPathCondition(menuItemName, $"{MenuItem.XPathTo.Title}[.='{menuItemName}']"); 11 | 12 | [ControlDefinition("li", ComponentTypeName = "menu item", Visibility = Visibility.Any)] 13 | [FindSettings(Visibility = Visibility.Any, TargetAllChildren = true)] 14 | public sealed class MenuItem : HierarchicalListItem 15 | { 16 | [FindByXPath(XPathTo.Title)] 17 | [GetsContentFromSource(ContentSource.TextContent)] 18 | public Text Title { get; private set; } 19 | 20 | internal static class XPathTo 21 | { 22 | internal const string Title = "a/span[contains(concat(' ', normalize-space(@class), ' '), ' title ')]"; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/Components/OrchardCoreAdminTopNavbar.cs: -------------------------------------------------------------------------------- 1 | using Atata; 2 | using Atata.Bootstrap; 3 | 4 | namespace Lombiq.Tests.UI.Components; 5 | 6 | [ControlDefinition(ContainingClass = "ta-navbar-top", ComponentTypeName = "navbar")] 7 | public class OrchardCoreAdminTopNavbar : Control 8 | where TOwner : PageObject 9 | { 10 | [FindById("navbarDropdown")] 11 | public AccountDropdown Account { get; private set; } 12 | 13 | public class AccountDropdown : BSDropdownToggle 14 | { 15 | [FindByContent(TermMatch.Contains, TermCase.Sentence)] 16 | public Button LogOff { get; private set; } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/Components/ValidationMessage.cs: -------------------------------------------------------------------------------- 1 | using Atata; 2 | 3 | namespace Lombiq.Tests.UI.Components; 4 | 5 | [ControlDefinition( 6 | "div[contains(concat(' ', normalize-space(@class), ' '), ' mb-3 ')]" + 7 | "//span[contains(concat(' ', normalize-space(@class), ' '), ' field-validation-error ')]")] 8 | public class ValidationMessage : Text 9 | where TOwner : PageObject 10 | { 11 | public new FieldVerificationProvider, TOwner> Should => new(this); 12 | } 13 | 14 | public static class ValidationMessageExtensions 15 | { 16 | public static TOwner BeRequiredError(this IFieldVerificationProvider, TOwner> should) 17 | where TOwner : PageObject => 18 | should.Contain("required"); 19 | 20 | public static TOwner BeInvalidError(this IFieldVerificationProvider, TOwner> should) 21 | where TOwner : PageObject => 22 | should.Contain("invalid"); 23 | } 24 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/Components/ValidationMessageList.cs: -------------------------------------------------------------------------------- 1 | using Atata; 2 | using OpenQA.Selenium; 3 | using System; 4 | 5 | namespace Lombiq.Tests.UI.Components; 6 | 7 | public class ValidationMessageList : ControlList, TOwner> 8 | where TOwner : PageObject 9 | { 10 | public ValidationMessage this[Func> controlSelector] => For(controlSelector); 11 | 12 | public ValidationMessage For(Func> controlSelector) 13 | { 14 | var validationMessageDefinition = UIComponentResolver.GetControlDefinition(typeof(ValidationMessage)); 15 | 16 | var boundControl = controlSelector(Component.Owner); 17 | 18 | var scopeLocator = new PlainScopeLocator(By.XPath("ancestor::" + validationMessageDefinition.ScopeXPath)) 19 | { 20 | SearchContext = boundControl.Scope, 21 | }; 22 | 23 | return Component.Controls.Create>(boundControl.ComponentName, scopeLocator); 24 | } 25 | 26 | public ValidationMessage For(Func> controlSelector, string name) 27 | { 28 | var validationMessageDefinition = UIComponentResolver.GetControlDefinition(typeof(ValidationMessage)); 29 | 30 | var boundControl = controlSelector(Component.Owner); 31 | 32 | var scopeLocator = new PlainScopeLocator(By.XPath("ancestor::" + validationMessageDefinition.ScopeXPath)) 33 | { 34 | SearchContext = boundControl.Scope, 35 | }; 36 | 37 | return Component.Controls.Create>(name, scopeLocator); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/Components/ValidationSummary.cs: -------------------------------------------------------------------------------- 1 | using Atata; 2 | 3 | namespace Lombiq.Tests.UI.Components; 4 | 5 | [ControlDefinition("div[contains(@class, 'validation-summary-errors')]")] 6 | public class ValidationSummary : Text 7 | where TOwner : PageObject 8 | { 9 | } 10 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/Components/ValidationSummaryError.cs: -------------------------------------------------------------------------------- 1 | using Atata; 2 | 3 | namespace Lombiq.Tests.UI.Components; 4 | 5 | [ControlDefinition("div[contains(concat(' ', normalize-space(@class), ' '), ' validation-summary-errors ')]/ul/li")] 6 | public sealed class ValidationSummaryError : Text 7 | where TOwner : PageObject 8 | { 9 | public new FieldVerificationProvider, TOwner> Should => new(this); 10 | } 11 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/Components/ValidationSummaryErrorList.cs: -------------------------------------------------------------------------------- 1 | using Atata; 2 | 3 | namespace Lombiq.Tests.UI.Components; 4 | 5 | public sealed class ValidationSummaryErrorList : ControlList, TOwner> 6 | where TOwner : PageObject 7 | { 8 | } 9 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/Constants/DefaultUser.cs: -------------------------------------------------------------------------------- 1 | namespace Lombiq.Tests.UI.Constants; 2 | 3 | public static class DefaultUser 4 | { 5 | public const string UserName = "admin"; 6 | public const string Email = "admin@admin.com"; 7 | // This is like a real password so browsers don't complain that it's a leaked one. 8 | public const string Password = "hAtRoc&WrImeDrAch1s&e=Huy?9ri!lY"; 9 | } 10 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/Constants/DirectoryPaths.cs: -------------------------------------------------------------------------------- 1 | using Lombiq.Tests.UI.Services; 2 | using System; 3 | using System.IO; 4 | 5 | namespace Lombiq.Tests.UI.Constants; 6 | 7 | public static class DirectoryPaths 8 | { 9 | public const string SetupSnapshot = nameof(SetupSnapshot); 10 | public const string Temp = nameof(Temp); 11 | public const string Screenshots = nameof(Screenshots); 12 | public const string Downloads = nameof(Downloads); 13 | 14 | public static string GetTempDirectoryPath(params string[] subDirectoryNames) => 15 | Path.Combine([Environment.CurrentDirectory, Temp, .. subDirectoryNames]); 16 | 17 | [Obsolete($"Use {nameof(UITestContext)}.{nameof(UITestContext.GetTempSubDirectoryPath)}() instead.")] 18 | public static string GetTempSubDirectoryPath(string contextId, params string[] subDirectoryNames) => 19 | GetTempDirectoryPath([contextId, .. subDirectoryNames]); 20 | 21 | [Obsolete($"Use {nameof(UITestContext)}.{nameof(UITestContext.ScreenshotsDirectoryPath)} instead.")] 22 | public static string GetScreenshotsDirectoryPath(string contextId) => 23 | GetTempSubDirectoryPath(contextId, Screenshots); 24 | } 25 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/Constants/TestUser.cs: -------------------------------------------------------------------------------- 1 | namespace Lombiq.Tests.UI.Constants; 2 | 3 | /// 4 | /// Details of a user account that can be created during tests. 5 | /// 6 | public static class TestUser 7 | { 8 | public const string UserName = "TestUser"; 9 | public const string Email = "testuser@example.com"; 10 | public const string Password = DefaultUser.Password; 11 | } 12 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/Constants/VisualVerificationMatchNames.cs: -------------------------------------------------------------------------------- 1 | namespace Lombiq.Tests.UI.Constants; 2 | 3 | public static class VisualVerificationMatchNames 4 | { 5 | public const string DumpFolderName = "VisualVerification"; 6 | public const string FullScreenImageFileName = "FullScreen.png"; 7 | public const string ElementImageFileName = "Element.png"; 8 | public const string BaselineImageFileName = "Baseline.png"; 9 | public const string CroppedElementImageFileName = "Element-cropped.png"; 10 | public const string CroppedBaselineImageFileName = "Baseline-cropped.png"; 11 | public const string DiffImageFileName = "Diff.png"; 12 | public const string DiffLogFileName = "Diff.log"; 13 | } 14 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/Delegates/MultiSizeTest.cs: -------------------------------------------------------------------------------- 1 | using Lombiq.Tests.UI.Services; 2 | using System.Threading.Tasks; 3 | 4 | namespace Lombiq.Tests.UI.Delegates; 5 | 6 | /// 7 | /// An test action where the window is sized to one of the two screen sizes in . 8 | /// 9 | /// The context of the currently executed UI test. 10 | /// 11 | /// A value indicating the screen size being used. If then the window is set to , otherwise to . 14 | /// 15 | public delegate void MultiSizeTest(UITestContext context, bool isStandardSize); 16 | 17 | /// 18 | public delegate Task MultiSizeTestAsync(UITestContext context, bool isStandardSize); 19 | 20 | public static class MultiSizeTestExtensions 21 | { 22 | public static MultiSizeTestAsync AsCompletedTask(this MultiSizeTest test) => 23 | (context, isStandardSize) => 24 | { 25 | test(context, isStandardSize); 26 | return Task.CompletedTask; 27 | }; 28 | } 29 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/Docs/Attachments/ZapReportScreenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lombiq/UI-Testing-Toolbox/d8ceab170e310fa30de5d807bbc88eb31f278486/Lombiq.Tests.UI/Docs/Attachments/ZapReportScreenshot.png -------------------------------------------------------------------------------- /Lombiq.Tests.UI/Docs/ExecutingTests.md: -------------------------------------------------------------------------------- 1 | # Executing tests 2 | 3 | ## Executing tests from Visual Studio 4 | 5 | Executing tests from Visual Studio is as simple as running them from [Test Explorer](https://docs.microsoft.com/en-us/visualstudio/test/run-unit-tests-with-test-explorer). Make sure that you follow the docs on [creating test projects](CreatingTests.md) so the tests can show up. 6 | 7 | ## Executing tests from the command line 8 | 9 | In a CI environment you'd execute tests with the `dotnet` command line tool. These are the steps we recommend for CI builds: 10 | 11 | 1. Build the solution with `dotnet build` in Release mode. We recommend using our [.NET Analyzers](https://github.com/Lombiq/.NET-Analyzers) for static code analysis and applying the code analysis switches on this step and during the `dotnet publish` ones later. 12 | 2. Publish the web app's project with `dotnet publish` in Release mode, optionally also with [ReadyToRun](https://docs.microsoft.com/en-us/dotnet/core/deploying/ready-to-run). Note that since the web app shouldn't really reference your UI test projects this doesn't publish those. Remove or don't publish the _refs_ folder. That way, Razor Runtime Compilation will be switched off, which removes an unnecessary and slow step when executing UI tests. 13 | 3. Publish the UI test project(s) with `dotnet publish` in Release mode. 14 | 4. Run the UI tests with `dotnet test`. Note that by default, the app will run in the Development environment, which is what we need for testing. 15 | 5. Optionally, if you want to reuse the build agent, kill the following processes that might remain after UI testing (the [_KillLeftoverProcesses.bat_](./KillLeftoverProcesses.bat) script can do this): 16 | - chromedriver.exe 17 | - dotnet.exe 18 | - geckodriver.exe 19 | - msedgedriver.exe 20 | 21 | Also see [what to configure](Configuration.md), especially for multi-agent build machines and tuning parallelization. 22 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/Docs/FakeVideoCaptureSource.md: -------------------------------------------------------------------------------- 1 | # Fake video capture source 2 | 3 | Imagine you have an application that uses video sources to access visual information from the user or the environment using [Media Capture and Streams API](https://developer.mozilla.org/en-US/docs/Web/API/Media_Capture_and_Streams_API). The goal can be QR or bar code scanning, user identification, or other computer vision applications. To make sure that future changes to the code do not break anything, we need a way to automate testing. Here, the fake video capture source comes into play. 4 | 5 | ## Preparing video file 6 | 7 | You can use `y4m` or `mjpeg` video files as a fake video capture source in the Chrome browser. 8 | 9 | If you have a video file in e.g. `mp4` format, use your preferred video tool to convert it to one of the formats mentioned above. If you don't have a preferred tool, simply use `ffmpeg`. 10 | 11 | > ℹ️ The `mjpeg` format will usually result in a smaller file size. 12 | 13 | ```bash 14 | # Convert mp4 to y4m. 15 | ffmpeg -y -i test.mp4 -pix_fmt yuv420p test.y4m 16 | 17 | # Convert with resize to 480p. 18 | ffmpeg -y -i test.mp4 -vf "scale=480:720" -pix_fmt yuv420p test.y4m 19 | 20 | # Convert mp4 to mjpeg. 21 | ffmpeg -y -i test.mp4 test.mjpeg 22 | 23 | # Convert with resize to 480p. 24 | ffmpeg -y -i test.mp4 -vf "scale=480:720" test.mjpeg 25 | ``` 26 | 27 | > ⚠ Using the `-filter:v scale=480:-1` command might "ruin" the video, resulting in a black screen in the browser without warnings. 28 | 29 | ## Sample 30 | 31 | You can find a usage example under [Lombiq Vue.js module for Orchard Core - UI Test Extensions](https://github.com/Lombiq/Orchard-Vue.js/tree/dev/Lombiq.VueJs.Tests.UI). 32 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/Docs/Limits.md: -------------------------------------------------------------------------------- 1 | # Limits on parallel test execution 2 | 3 | Tests need to use ports for running the Orchard Core app and SMTP server with its web UI if necessary. To allow parallelized test execution in the same process, as well as [executing tests in multiple processes](Configuration.md#multi-process-test-execution) the interval of available ports need to be fixed. The current limits are as following: 4 | 5 | - Up to 100 concurrent tests in the same process. 6 | - Up to 10 concurrent processes on the same machine. 7 | 8 | Anything above these will cause random port collisions. 9 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/Docs/Linux.md: -------------------------------------------------------------------------------- 1 | # Linux-specific considerations 2 | 3 | ## Global NPM vs Userspace NPM via Node Version Manager 4 | 5 | As Linux has a stricter access policy you may want to install NPM in the userspace so you can still install global packages (e.g. html-validate) without sudoing. The easiest way to do this is via NVM. If you don't have NVM yet, [follow the guide here](https://github.com/Lombiq/NPM-Targets/tree/dev#global-npm-vs-userspace-npm-via-node-version-manager-on-linux). 6 | 7 | This library configures processes launched by Atata (via `Atata.Cli.ProgramCli`) to use Bash as login shell on non-Windows systems by default, like this: 8 | 9 | ```csharp 10 | ProgramCli.DefaultShellCliCommandFactory = OSDependentShellCliCommandFactory 11 | .UseCmdForWindows() 12 | .UseForOtherOS(new BashShellCliCommandFactory("-login")); 13 | ``` 14 | 15 | If your project has different requirements, you can change it in your `OrchardCoreUITestBase.ExecuteTestAfterSetupAsync` implementation. Set a new value in the configuration function you pass to `base.ExecuteTestAsync`. 16 | 17 | ## SQL Server Usage 18 | 19 | Since 2017, Microsoft SQL Server is available on [RHEL](https://www.redhat.com/en/technologies/linux-platforms/enterprise-linux), [SUSE](https://www.suse.com/products/server/) and [Ubuntu](https://ubuntu.com/) as well as a [Linux-based Docker image](https://hub.docker.com/_/microsoft-mssql-server) that you can run on any OS. 20 | 21 | We suggest using the Docker image even on those OSes. It reduces the number of unknowns and moving parts in your setup, it's easier to reset if something goes wrong, and that's what we support. We have a guide for setting up SQL Server for Linux on Docker [here](Configuration.md#using-sql-server-from-a-docker-container). 22 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/Docs/Projects.md: -------------------------------------------------------------------------------- 1 | # Projects in the UI Testing Toolbox 2 | 3 | The UI Testing Toolbox encompasses the following projects: 4 | 5 | - `Lombiq.Tests.UI`: Contains all features used for UI testing. This is the project that your test projects need to reference. 6 | - [`Lombiq.Tests.UI.AppExtensions`](../../Lombiq.Tests.UI.AppExtensions/Readme.md): UI testing-related extensions for the web app under test. 7 | - [`Lombiq.Tests.UI.Shortcuts`](../../Lombiq.Tests.UI.Shortcuts/Readme.md): Provides some useful shortcuts for common operations that UI tests might want to do or check, e.g. turning features on or off, or logging in users. If you utilize these shortcuts then your web projects needs to reference this project to load as an Orchard Core module. 8 | - [`Lombiq.Tests.UI.Samples`](../../Lombiq.Tests.UI.Samples/Readme.md): Example UI testing project. 9 | - [`Lombiq.Tests.UI.Tests.UI`](../../Lombiq.Tests.UI.Tests.UI/Readme.md): UI Tests for the UI Testing Toolbox. 10 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/Docs/Tools.md: -------------------------------------------------------------------------------- 1 | # Tools we use 2 | 3 | - The tests themselves are written in [xUnit](https://xunit.net/), optionally with [Shouldly](https://github.com/shouldly/shouldly). 4 | - The UI testing framework we use is [Selenium](https://www.selenium.dev/), extended with [Atata](https://atata.io/). 5 | - The browsers that you can use (Chrome, Edge, and Firefox) will be automatically installed by [Selenium Manager](https://www.selenium.dev/documentation/selenium_manager/). 6 | - There are multiple recording tools available for Selenium but the "official" one which works pretty well is [Selenium IDE](https://www.selenium.dev/selenium-ide/) (which is a Chrome/Firefox extension). To fine-tune XPath queries and CSS selectors and also to record tests check out [ChroPath](https://chrome.google.com/webstore/detail/chropath/ljngjbnaijcbncmcnjfhigebomdlkcjo/) (the [Xpath cheat sheet](https://devhints.io/xpath) is a great resource too, and [XmlToolBox](https://xmltoolbox.appspot.com/xpath_generator.html) can help you with quick XPath queries). 7 | - Accessibility checking can be done with [axe](https://github.com/dequelabs/axe-core) via [Selenium.Axe for .NET](https://github.com/TroyWalshProf/SeleniumAxeDotnet). 8 | - HTML markup validation can be done with [html-validate](https://gitlab.com/html-validate/html-validate) via [Atata.HtmlValidation](https://github.com/atata-framework/atata-htmlvalidation). 9 | - When testing e-mail sending, we use [smtp4dev](https://github.com/rnwood/smtp4dev) as a local SMTP server (as well as an IMAP server with basic operations). 10 | - Monkey testing is implemented using [Gremlins.js](https://github.com/marmelab/gremlins.js/) library. 11 | - Visual verification is implemented using [ImageSharpCompare](https://github.com/Codeuctivity/ImageSharp.Compare). 12 | - [Ben.Demystifier](https://github.com/benaadams/Ben.Demystifier) is used to simplify stack traces, mainly around async methods. 13 | - Security scans are done with [Zed Attack Proxy (ZAP)](https://www.zaproxy.org/). 14 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/EmbeddedResourceProvider.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Text; 3 | 4 | namespace Lombiq.Tests.UI; 5 | 6 | internal static class EmbeddedResourceProvider 7 | { 8 | internal static string ReadEmbeddedFile(string fileName) 9 | { 10 | var assembly = typeof(EmbeddedResourceProvider).Assembly; 11 | var resourceStream = assembly.GetManifestResourceStream($"{assembly.GetName().Name}.Resources.{fileName}"); 12 | 13 | using var reader = new StreamReader(resourceStream, Encoding.UTF8); 14 | 15 | return reader.ReadToEnd(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/Exceptions/AccessibilityAssertionException.cs: -------------------------------------------------------------------------------- 1 | using Deque.AxeCore.Commons; 2 | using System; 3 | 4 | namespace Lombiq.Tests.UI.Exceptions; 5 | 6 | public class AccessibilityAssertionException : Exception, IAssertionException 7 | { 8 | public AxeResult AxeResult { get; } 9 | 10 | public AccessibilityAssertionException(AxeResult axeResult, bool createReportOnFailure, Exception innerException) 11 | : base( 12 | "Asserting the accessibility analysis result failed." + 13 | (createReportOnFailure ? " Check the accessibility report failure dump for details." : string.Empty), 14 | innerException) => 15 | AxeResult = axeResult; 16 | 17 | public AccessibilityAssertionException() 18 | { 19 | } 20 | 21 | public AccessibilityAssertionException(string message) 22 | : base(message) 23 | { 24 | } 25 | 26 | public AccessibilityAssertionException(string message, Exception innerException) 27 | : base(message, innerException) 28 | { 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/Exceptions/CreateUserFailedException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Lombiq.Tests.UI.Exceptions; 4 | 5 | public class CreateUserFailedException : Exception 6 | { 7 | public CreateUserFailedException(string message) 8 | : base(message) 9 | { 10 | } 11 | 12 | public CreateUserFailedException(string message, Exception innerException) 13 | : base(message, innerException) 14 | { 15 | } 16 | 17 | public CreateUserFailedException() 18 | { 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/Exceptions/DockerFileCopyException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Lombiq.Tests.UI.Exceptions; 4 | 5 | public class DockerFileCopyException : Exception 6 | { 7 | public DockerFileCopyException(string message) 8 | : base(message) 9 | { 10 | } 11 | 12 | public DockerFileCopyException(string message, Exception innerException) 13 | : base(message, innerException) 14 | { 15 | } 16 | 17 | public DockerFileCopyException() 18 | { 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/Exceptions/HtmlValidationAssertionException.cs: -------------------------------------------------------------------------------- 1 | using Atata.HtmlValidation; 2 | using Lombiq.Tests.UI.Services; 3 | using System; 4 | 5 | namespace Lombiq.Tests.UI.Exceptions; 6 | 7 | public class HtmlValidationAssertionException : Exception, IAssertionException 8 | { 9 | public HtmlValidationResult HtmlValidationResult { get; } 10 | public HtmlValidationConfiguration HtmlValidationConfiguration { get; } 11 | 12 | public HtmlValidationAssertionException( 13 | HtmlValidationResult htmlValidationResult, 14 | HtmlValidationConfiguration validationConfiguration, 15 | Exception innerException) 16 | : base( 17 | validationConfiguration.CreateReportOnFailure 18 | ? $"{innerException.Message} Check the HTML validation report in the failure dump for details." 19 | : innerException.Message, 20 | innerException) 21 | { 22 | HtmlValidationResult = htmlValidationResult; 23 | HtmlValidationConfiguration = validationConfiguration; 24 | } 25 | 26 | public HtmlValidationAssertionException(string message) 27 | : base(message) 28 | { 29 | } 30 | 31 | public HtmlValidationAssertionException(string message, Exception innerException) 32 | : base(message, innerException) 33 | { 34 | } 35 | 36 | public HtmlValidationAssertionException() 37 | { 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/Exceptions/IAssertionException.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | 3 | namespace Lombiq.Tests.UI.Exceptions; 4 | 5 | /// 6 | /// Marker interface for xUnit for assertion failure exceptions, see 8 | /// the xUnit docs. 9 | /// 10 | [SuppressMessage("Design", "CA1040:Avoid empty interfaces", Justification = "See above.")] 11 | public interface IAssertionException 12 | { 13 | } 14 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/Exceptions/PageChangeAssertionException.cs: -------------------------------------------------------------------------------- 1 | using Lombiq.Tests.UI.Extensions; 2 | using Lombiq.Tests.UI.Services; 3 | using System; 4 | 5 | namespace Lombiq.Tests.UI.Exceptions; 6 | 7 | public class PageChangeAssertionException : Exception, IAssertionException 8 | { 9 | public Uri Address { get; } 10 | public string Title { get; } 11 | 12 | public PageChangeAssertionException( 13 | UITestContext context, 14 | Exception innerException) 15 | : base( 16 | $"An assertion during the page change event has failed on page {context.GetPageTitleAndAddress()}.", 17 | innerException) 18 | { 19 | Address = new Uri(context.Driver.Url); 20 | Title = context.Driver.Title; 21 | } 22 | 23 | public PageChangeAssertionException() 24 | { 25 | } 26 | 27 | public PageChangeAssertionException(string message) 28 | : base(message) 29 | { 30 | } 31 | 32 | public PageChangeAssertionException(string message, Exception innerException) 33 | : base(message, innerException) 34 | { 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/Exceptions/PermissionNotFoundException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Lombiq.Tests.UI.Exceptions; 4 | 5 | public class PermissionNotFoundException : Exception 6 | { 7 | public PermissionNotFoundException(string message) 8 | : base(message) 9 | { 10 | } 11 | 12 | public PermissionNotFoundException(string message, Exception innerException) 13 | : base(message, innerException) 14 | { 15 | } 16 | 17 | public PermissionNotFoundException() 18 | { 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/Exceptions/RecipeNotFoundException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Lombiq.Tests.UI.Exceptions; 4 | 5 | public class RecipeNotFoundException : Exception 6 | { 7 | public RecipeNotFoundException(string message) 8 | : base(message) 9 | { 10 | } 11 | 12 | public RecipeNotFoundException(string message, Exception innerException) 13 | : base(message, innerException) 14 | { 15 | } 16 | 17 | public RecipeNotFoundException() 18 | { 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/Exceptions/RoleNotFoundException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Lombiq.Tests.UI.Exceptions; 4 | 5 | public class RoleNotFoundException : Exception 6 | { 7 | public RoleNotFoundException(string message) 8 | : base(message) 9 | { 10 | } 11 | 12 | public RoleNotFoundException(string message, Exception innerException) 13 | : base(message, innerException) 14 | { 15 | } 16 | 17 | public RoleNotFoundException() 18 | { 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/Exceptions/SetupFailedFastException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics.CodeAnalysis; 3 | 4 | namespace Lombiq.Tests.UI.Exceptions; 5 | 6 | [SuppressMessage("Design", "CA1032:Implement standard exception constructors", Justification = "Used in a very specific case.")] 7 | public class SetupFailedFastException : Exception, IAssertionException 8 | { 9 | public int FailureCount { get; } 10 | 11 | public SetupFailedFastException(int failureCount, Exception latestException) 12 | : base( 13 | $"The given setup operation failed {failureCount.ToTechnicalString()} times and won't be retried any " + 14 | $"more. All tests using this operation for setup will instantly fail.", 15 | latestException) => 16 | FailureCount = failureCount; 17 | } 18 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/Exceptions/TestDumpItemAlreadyExistsException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Lombiq.Tests.UI.Exceptions; 4 | 5 | // Here we only need fileName and customMessage. 6 | #pragma warning disable CA1032 // Implement standard exception constructors 7 | public class TestDumpItemAlreadyExistsException : Exception 8 | #pragma warning restore CA1032 // Implement standard exception constructors 9 | { 10 | public TestDumpItemAlreadyExistsException(string fileName) 11 | : this(fileName, customMessage: null, innerException: null) 12 | { 13 | } 14 | 15 | public TestDumpItemAlreadyExistsException(string fileName, Exception innerException) 16 | : this(fileName, customMessage: null, innerException) 17 | { 18 | } 19 | 20 | public TestDumpItemAlreadyExistsException(string fileName, string customMessage) 21 | : this(fileName, customMessage, innerException: null) 22 | { 23 | } 24 | 25 | public TestDumpItemAlreadyExistsException(string fileName, string customMessage, Exception innerException) 26 | : base( 27 | $"A test dump item with the same file name already exists. File name: {fileName}." 28 | + Environment.NewLine 29 | + (customMessage ?? string.Empty), 30 | innerException) 31 | { 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/Exceptions/ThemeNotFoundException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Lombiq.Tests.UI.Exceptions; 4 | 5 | public class ThemeNotFoundException : Exception 6 | { 7 | public ThemeNotFoundException(string message) 8 | : base(message) 9 | { 10 | } 11 | 12 | public ThemeNotFoundException(string message, Exception innerException) 13 | : base(message, innerException) 14 | { 15 | } 16 | 17 | public ThemeNotFoundException() 18 | { 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/Exceptions/UserNotFoundException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Lombiq.Tests.UI.Exceptions; 4 | 5 | public class UserNotFoundException : Exception 6 | { 7 | public UserNotFoundException(string message) 8 | : base(message) 9 | { 10 | } 11 | 12 | public UserNotFoundException(string message, Exception innerException) 13 | : base(message, innerException) 14 | { 15 | } 16 | 17 | public UserNotFoundException() 18 | { 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/Exceptions/VisualVerificationAssertionException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Lombiq.Tests.UI.Exceptions; 4 | 5 | public class VisualVerificationAssertionException : Exception, IAssertionException 6 | { 7 | public VisualVerificationAssertionException(string message) 8 | : base(message) 9 | { 10 | } 11 | 12 | public VisualVerificationAssertionException(string message, Exception innerException) 13 | : base(message, innerException) 14 | { 15 | } 16 | 17 | public VisualVerificationAssertionException() 18 | { 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/Exceptions/VisualVerificationBaselineImageNotFoundException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Lombiq.Tests.UI.Exceptions; 4 | 5 | // Here we only need path instead of message. 6 | #pragma warning disable CA1032 // Implement standard exception constructors 7 | public class VisualVerificationBaselineImageNotFoundException : Exception 8 | #pragma warning restore CA1032 // Implement standard exception constructors 9 | { 10 | public VisualVerificationBaselineImageNotFoundException( 11 | string path, 12 | bool isFinalTry, 13 | Exception innerException = null) 14 | : base(GetExceptionMessage(path, isFinalTry), innerException) 15 | { 16 | } 17 | 18 | private static string GetExceptionMessage(string path, bool isFinalTry) => 19 | isFinalTry 20 | ? $"Baseline image file not found, thus it was created automatically under the path {path}. Please set " + 21 | $"its \"Build action\" to \"Embedded resource\" if you want to deploy a self-contained (like a NuGet " + 22 | $"package) UI testing assembly. If you run the test again, this newly created verification file will " + 23 | $"be asserted against and the assertion will pass (unless the display of the app changed in the " + 24 | $"meantime)." 25 | : $"Baseline image file was not found under the path {path} and this isn't the final try."; 26 | } 27 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/Exceptions/VisualVerificationCallerMethodNotFoundException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Lombiq.Tests.UI.Exceptions; 4 | 5 | public class VisualVerificationCallerMethodNotFoundException : Exception 6 | { 7 | public VisualVerificationCallerMethodNotFoundException(string message) 8 | : base(message) 9 | { 10 | } 11 | 12 | public VisualVerificationCallerMethodNotFoundException(string message, Exception innerException) 13 | : base(message, innerException) 14 | { 15 | } 16 | 17 | public VisualVerificationCallerMethodNotFoundException() 18 | : this(innerException: null) 19 | { 20 | } 21 | 22 | public VisualVerificationCallerMethodNotFoundException(Exception innerException) 23 | : this("Caller method not found", innerException) 24 | { 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/Exceptions/VisualVerificationSourceInformationNotAvailableException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Lombiq.Tests.UI.Exceptions; 4 | 5 | public class VisualVerificationSourceInformationNotAvailableException : Exception 6 | { 7 | public VisualVerificationSourceInformationNotAvailableException(string message) 8 | : base(message) 9 | { 10 | } 11 | 12 | public VisualVerificationSourceInformationNotAvailableException(string message, Exception innerException) 13 | : base(message, innerException) 14 | { 15 | } 16 | 17 | public VisualVerificationSourceInformationNotAvailableException() 18 | { 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/Exceptions/WorkflowTypeNotFoundException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Lombiq.Tests.UI.Exceptions; 4 | 5 | public class WorkflowTypeNotFoundException : Exception 6 | { 7 | public WorkflowTypeNotFoundException(string message) 8 | : base(message) 9 | { 10 | } 11 | 12 | public WorkflowTypeNotFoundException(string message, Exception innerException) 13 | : base(message, innerException) 14 | { 15 | } 16 | 17 | public WorkflowTypeNotFoundException() 18 | { 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/Extensions/AccessibilityCheckingOrchardCoreUITestExecutorConfigurationExtensions.cs: -------------------------------------------------------------------------------- 1 | using Deque.AxeCore.Commons; 2 | using Deque.AxeCore.Selenium; 3 | using Lombiq.Tests.UI.Services; 4 | using System; 5 | using System.Threading.Tasks; 6 | 7 | namespace Lombiq.Tests.UI.Extensions; 8 | 9 | public static class AccessibilityCheckingOrchardCoreUITestExecutorConfigurationExtensions 10 | { 11 | /// 12 | /// Sets up accessibility checking to run every time a page changes (either due to explicit navigation or 13 | /// clicks) and asserts on the validation results. 14 | /// 15 | /// 16 | /// The assertion logic to run on the result of an axe accessibility analysis. If then the 17 | /// assertion supplied in the context will be used. 18 | /// 19 | /// 20 | /// A delegate to configure the instance. Will be applied after the configurator supplied 21 | /// in the context. 22 | /// 23 | public static void SetUpAccessibilityCheckingAssertionOnPageChange( 24 | this OrchardCoreUITestExecutorConfiguration configuration, 25 | Action axeBuilderConfigurator = null, 26 | Action assertAxeResult = null) 27 | { 28 | if (!configuration.CustomConfiguration.TryAdd("AccessibilityCheckingAssertionOnPageChangeWasSetUp", value: true)) return; 29 | 30 | configuration.Events.AfterPageChange += context => 31 | { 32 | if (configuration.AccessibilityCheckingConfiguration.AccessibilityCheckingAndAssertionOnPageChangeRule?.Invoke(context) == true) 33 | { 34 | context.AssertAccessibility(axeBuilderConfigurator, assertAxeResult); 35 | } 36 | 37 | return Task.CompletedTask; 38 | }; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/Extensions/ApplicationLogEnumerableExtensions.cs: -------------------------------------------------------------------------------- 1 | using Lombiq.Tests.UI.Models; 2 | using Lombiq.Tests.UI.Services; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Threading.Tasks; 7 | 8 | namespace Lombiq.Tests.UI.Extensions; 9 | 10 | public static class ApplicationLogEnumerableExtensions 11 | { 12 | public static string ToFormattedStringCached(this IEnumerable logs) 13 | { 14 | var logsArray = logs.ToArray(); 15 | 16 | if (logsArray.Length == 1) 17 | { 18 | return Environment.NewLine + logsArray[0].ToFormattedString(); 19 | } 20 | 21 | var logContents = logsArray.Select(log => 22 | $"# Log name: {log.Name}" + Environment.NewLine + Environment.NewLine + log.ToFormattedString()); 23 | 24 | return string.Join(Environment.NewLine + Environment.NewLine, logContents); 25 | } 26 | 27 | public static async Task ToFormattedStringAsync(this IEnumerable logs) 28 | { 29 | var cached = await logs.AwaitEachAsync(log => MemoryApplicationLog.FromLogAsync(log)); 30 | return cached.ToFormattedStringCached(); 31 | } 32 | 33 | private static string ToFormattedString(this MemoryApplicationLog log) => 34 | string.Join(Environment.NewLine, log.Entries.Select(logEntry => logEntry.ToString())); 35 | } 36 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/Extensions/AssemblyResourceExtensions.cs: -------------------------------------------------------------------------------- 1 | using SixLabors.ImageSharp; 2 | using System.Reflection; 3 | 4 | namespace Lombiq.Tests.UI.Extensions; 5 | 6 | public static class AssemblyResourceExtensions 7 | { 8 | /// 9 | /// Loads resource specified by from the given . 10 | /// 11 | public static Image GetResourceImageSharpImage(this Assembly assembly, string name) 12 | { 13 | if (assembly.GetManifestResourceStream(name) is not { } resourceStream) return null; 14 | 15 | using (resourceStream) 16 | { 17 | return Image.Load(resourceStream); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/Extensions/AsyncExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | 4 | namespace Lombiq.Tests.UI.Extensions; 5 | 6 | public static class AsyncExtensions 7 | { 8 | public static Func AsCompletedTask(this Action action) => 9 | target => 10 | { 11 | action?.Invoke(target); 12 | return Task.CompletedTask; 13 | }; 14 | } 15 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/Extensions/AxeResultItemExtensions.cs: -------------------------------------------------------------------------------- 1 | using Deque.AxeCore.Commons; 2 | using Lombiq.Tests.UI.Services; 3 | using Shouldly; 4 | using System.Collections.Generic; 5 | 6 | namespace Lombiq.Tests.UI.Extensions; 7 | 8 | public static class AxeResultItemExtensions 9 | { 10 | /// 11 | /// Asserts if is empty, and if not then produces an error with s converted into human-readable strings. 13 | /// 14 | public static void AxeResultItemsShouldBeEmpty(this IEnumerable axeResultItems) => 15 | axeResultItems.ShouldBeEmpty(AccessibilityCheckingConfiguration.AxeResultItemsToString(axeResultItems)); 16 | } 17 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/Extensions/BasicWebElementExtensions.cs: -------------------------------------------------------------------------------- 1 | #nullable enable 2 | 3 | namespace OpenQA.Selenium; 4 | 5 | // Move this to Lombiq UI Testing Toolbox in dev. 6 | public static class BasicWebElementExtensions 7 | { 8 | /// 9 | /// Returns the text content of the without surrounding whitespace. 10 | /// 11 | public static string? GetTextTrimmed(this IWebElement element) => element.Text?.Trim(); 12 | 13 | /// 14 | /// Returns a value indicating whether the boolean attribute called exists. This 15 | /// returns even if the value is empty, in accordance to how HTML works (e.g. all of the 16 | /// following are considered true: <input required>, <input required="">, 17 | /// <input required="required">). 18 | /// 19 | public static bool GetBoolAttribute(this IWebElement element, string attributeName) => 20 | element.GetAttribute(attributeName) != null; 21 | 22 | /// 23 | /// Returns a value indicating whether the element has the disabled attribute. 24 | /// 25 | public static bool IsDisabled(this IWebElement element) => element.GetBoolAttribute("disabled"); 26 | } 27 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/Extensions/ControlExtensions.cs: -------------------------------------------------------------------------------- 1 | using Atata; 2 | 3 | namespace Lombiq.Tests.UI.Extensions; 4 | 5 | public static class ControlExtensions 6 | { 7 | public static TOwner ClickAndAssertNoPageChanges(this Control control) 8 | where TOwner : PageObject 9 | { 10 | var pageObject = control.Owner; 11 | 12 | string savedUrl = pageObject.PageUrl; 13 | string savedHtml = pageObject.PageSource; 14 | 15 | control.Click(); 16 | 17 | pageObject.PageUrl.Should.AtOnce.Equal(savedUrl); 18 | pageObject.PageSource.Should.AtOnce.Satisfy(value => value == savedHtml, "equal previous HTML"); 19 | 20 | return pageObject; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/Extensions/ElementStyleUITestContextExtensions.cs: -------------------------------------------------------------------------------- 1 | using Lombiq.Tests.UI.Services; 2 | using OpenQA.Selenium; 3 | 4 | namespace Lombiq.Tests.UI.Extensions; 5 | 6 | public static class ElementStyleUITestContextExtensions 7 | { 8 | /// 9 | /// Sets the element's inline style. 10 | /// 11 | /// Selector for the target element. 12 | /// CSS property name. 13 | /// CSS property value. 14 | public static void SetElementStyle(this UITestContext context, By elementSelector, string property, string value) => 15 | context.ExecuteScript( 16 | "arguments[0].style[arguments[1]] = arguments[2];", 17 | context.Get(elementSelector), 18 | property, 19 | value); 20 | } 21 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/Extensions/EventsOrchardCoreUITestExecutorConfigurationExtensions.cs: -------------------------------------------------------------------------------- 1 | using Lombiq.Tests.UI.Models; 2 | using Lombiq.Tests.UI.Services; 3 | using System.Threading.Tasks; 4 | 5 | namespace Lombiq.Tests.UI.Extensions; 6 | 7 | public static class EventsOrchardCoreUITestExecutorConfigurationExtensions 8 | { 9 | public static void SetUpEvents(this OrchardCoreUITestExecutorConfiguration configuration) 10 | { 11 | if (!configuration.CustomConfiguration.TryAdd("EventsWereSetUp", value: true)) return; 12 | 13 | PageNavigationState navigationState = null; 14 | 15 | configuration.Events.AfterNavigation += (context, _) => context.TriggerAfterPageChangeEventAsync(); 16 | 17 | configuration.Events.BeforeClick += (context, _) => 18 | { 19 | navigationState = context.AsPageNavigationState(); 20 | return Task.CompletedTask; 21 | }; 22 | 23 | configuration.Events.AfterClick += (context, _) => 24 | navigationState.CheckIfNavigationHasOccurred() 25 | ? context.TriggerAfterPageChangeEventAsync() 26 | : Task.CompletedTask; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/Extensions/FakeBrowserVideoSourceExtensions.cs: -------------------------------------------------------------------------------- 1 | using Lombiq.Tests.UI.Constants; 2 | using Lombiq.Tests.UI.Models; 3 | using System; 4 | using System.IO; 5 | 6 | namespace Lombiq.Tests.UI.Extensions; 7 | 8 | public static class FakeBrowserVideoSourceExtensions 9 | { 10 | public static string SaveVideoToTempFolder(this FakeBrowserVideoSource source) 11 | { 12 | using var fakeCameraSource = source.StreamProvider(); 13 | var fakeCameraSourcePath = Path.ChangeExtension( 14 | DirectoryPaths.GetTempDirectoryPath(Guid.NewGuid().ToString()), 15 | GetExtension(source.Format)); 16 | using var fakeCameraSourceFile = new FileStream(fakeCameraSourcePath, FileMode.CreateNew, FileAccess.Write); 17 | 18 | fakeCameraSource.CopyTo(fakeCameraSourceFile); 19 | 20 | return fakeCameraSourcePath; 21 | } 22 | 23 | private static string GetExtension(FakeBrowserVideoSourceFileFormat format) => 24 | format switch 25 | { 26 | FakeBrowserVideoSourceFileFormat.MJpeg => "mjpeg", 27 | FakeBrowserVideoSourceFileFormat.Y4m => "y4m", 28 | _ => throw new ArgumentOutOfRangeException(nameof(format)), 29 | }; 30 | } 31 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/Extensions/FormWebDriverExtensions.cs: -------------------------------------------------------------------------------- 1 | using Atata; 2 | using OpenQA.Selenium; 3 | using System; 4 | 5 | namespace Lombiq.Tests.UI.Extensions; 6 | 7 | public static class FormWebDriverExtensions 8 | { 9 | public static IWebElement TryFillElement(this IWebDriver driver, IWebElement element, string value) 10 | { 11 | element.ClearWithLogging(); 12 | 13 | if (value.Contains('@', StringComparison.Ordinal)) 14 | { 15 | // On some platforms, probably due to keyboard settings, the @ character can be missing from the address 16 | // when entered into a text field so we need to use Actions. The following solution doesn't work: 17 | // https://stackoverflow.com/a/52202594/220230. This needs to be done in addition to the standard 18 | // FillInWith() as without that some forms start to behave strange and not save values. 19 | driver.Perform(actions => actions.SendKeys(element, value)); 20 | } 21 | else 22 | { 23 | element.SendKeysWithLogging(value); 24 | } 25 | 26 | return element; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/Extensions/FrontendOrchardCoreUITestExecutorConfigurationExtensions.cs: -------------------------------------------------------------------------------- 1 | using Lombiq.Tests.UI.Services; 2 | using System; 3 | using System.Collections.Generic; 4 | 5 | namespace Lombiq.Tests.UI.Extensions; 6 | 7 | public static class FrontendOrchardCoreUITestExecutorConfigurationExtensions 8 | { 9 | private const string BackendUri = nameof(BackendUri); 10 | private const string FrontendUri = nameof(FrontendUri); 11 | 12 | /// 13 | /// Returns the start URLs for the frontend and the Orchard Core backend from the . 15 | /// 16 | public static (Uri FrontendUri, Uri BackendUri) GetFrontendAndBackendUris( 17 | this OrchardCoreUITestExecutorConfiguration configuration) => 18 | ( 19 | configuration.CustomConfiguration.GetMaybe(FrontendUri) as Uri, 20 | configuration.CustomConfiguration.GetMaybe(BackendUri) as Uri 21 | ); 22 | 23 | /// 24 | /// Updates the by storing the frontend and 25 | /// the Orchard Core backend URLs as instances. If either parameter is , 26 | /// that value is not changed. 27 | /// 28 | public static void SetFrontendAndBackendUris( 29 | this OrchardCoreUITestExecutorConfiguration configuration, 30 | string frontendUrl, 31 | string backendUrl) 32 | { 33 | if (frontendUrl != null) configuration.CustomConfiguration[FrontendUri] = new Uri(frontendUrl); 34 | if (backendUrl != null) configuration.CustomConfiguration[BackendUri] = new Uri(backendUrl); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/Extensions/HtmlValidationOrchardCoreUITestExecutorConfigurationExtensions.cs: -------------------------------------------------------------------------------- 1 | using Atata.HtmlValidation; 2 | using Lombiq.Tests.UI.Services; 3 | using System; 4 | using System.Threading.Tasks; 5 | 6 | namespace Lombiq.Tests.UI.Extensions; 7 | 8 | public static class HtmlValidationOrchardCoreUITestExecutorConfigurationExtensions 9 | { 10 | /// 11 | /// Sets up HTML validation to run every time a page changes (either due to explicit navigation or clicks) and 12 | /// asserts on the validation results. 13 | /// 14 | /// 15 | /// The assertion logic to run on the result of an HTML markup validation. If then the 16 | /// assertion supplied in the context will be used. 17 | /// 18 | /// 19 | /// A delegate to adjust the instance supplied in the context. 20 | /// 21 | public static void SetUpHtmlValidationAssertionOnPageChange( 22 | this OrchardCoreUITestExecutorConfiguration configuration, 23 | Action htmlValidationOptionsAdjuster = null, 24 | Func assertHtmlValidationResultAsync = null) 25 | { 26 | if (!configuration.CustomConfiguration.TryAdd("HtmlValidationAssertionOnPageChangeWasSetUp", value: true)) return; 27 | 28 | configuration.Events.AfterPageChange += async context => 29 | { 30 | if (configuration.HtmlValidationConfiguration.HtmlValidationAndAssertionOnPageChangeRule?.Invoke(context) == true) 31 | { 32 | await context.AssertHtmlValidityAsync(htmlValidationOptionsAdjuster, assertHtmlValidationResultAsync); 33 | } 34 | }; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/Extensions/IExtensionManagerExtensions.cs: -------------------------------------------------------------------------------- 1 | using OrchardCore.Environment.Extensions; 2 | using OrchardCore.Environment.Extensions.Features; 3 | using System; 4 | using System.Linq; 5 | 6 | namespace Lombiq.Tests.UI.Extensions; 7 | 8 | public static class IExtensionManagerExtensions 9 | { 10 | /// 11 | /// Gets by . 12 | /// 13 | /// instance or null if not exists. 14 | public static IFeatureInfo GetFeature(this IExtensionManager extensionManager, string featureId) => 15 | extensionManager.GetFeatures().FirstOrDefault(feature => feature.Id.Equals(featureId, StringComparison.Ordinal)); 16 | } 17 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/Extensions/ImageSharpImageExtensions.cs: -------------------------------------------------------------------------------- 1 | using Codeuctivity.ImageSharpCompare; 2 | using SixLabors.ImageSharp; 3 | using SixLabors.ImageSharp.Formats.Png; 4 | using SixLabors.ImageSharp.Processing; 5 | using System.IO; 6 | 7 | namespace Lombiq.Tests.UI.Extensions; 8 | 9 | public static class ImageSharpImageExtensions 10 | { 11 | /// 12 | /// Calculates the difference between the given images. 13 | /// 14 | public static ICompareResult CompareTo(this Image actual, Image expected) 15 | { 16 | using var actualStream = actual.ToStream(); 17 | using var expectedStream = expected.ToStream(); 18 | 19 | return ImageSharpCompare.CalcDiff(actualStream, expectedStream); 20 | } 21 | 22 | /// 23 | /// Creates a diff mask of two images. 24 | /// 25 | public static Image CalcDiffImage(this Image actual, Image expected) 26 | { 27 | using var actualStream = actual.ToStream(); 28 | using var expectedStream = expected.ToStream(); 29 | 30 | return ImageSharpCompare.CalcDiffMaskImage(actualStream, expectedStream); 31 | } 32 | 33 | /// 34 | /// Clones the . 35 | /// 36 | /// The source instance. 37 | /// Cloned instance. 38 | public static Image Clone(this Image image) => 39 | image.Clone(processingContext => { }); 40 | 41 | /// 42 | /// Converts the to . 43 | /// 44 | /// The source instance. 45 | public static Stream ToStream(this Image image) 46 | { 47 | var imageStream = new MemoryStream(); 48 | 49 | image.Save(imageStream, new PngEncoder()); 50 | 51 | imageStream.Seek(0, SeekOrigin.Begin); 52 | 53 | return imageStream; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/Extensions/LocationUITestContextExtensions.cs: -------------------------------------------------------------------------------- 1 | using Lombiq.Tests.UI.Services; 2 | 3 | namespace Lombiq.Tests.UI.Extensions; 4 | 5 | public static class LocationUITestContextExtensions 6 | { 7 | public static string GetPageTitleAndAddress(this UITestContext context) 8 | { 9 | if (context.Driver is null) return null; 10 | 11 | var url = context.Driver.Url; 12 | var title = context.Driver.Title; 13 | 14 | return string.IsNullOrEmpty(title) 15 | ? url 16 | : $"{url} ({title})"; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/Extensions/MediaLibraryUITestContextExtensions.cs: -------------------------------------------------------------------------------- 1 | using Lombiq.Tests.UI.Helpers; 2 | using Lombiq.Tests.UI.Services; 3 | using OpenQA.Selenium; 4 | using System.Threading.Tasks; 5 | 6 | namespace Lombiq.Tests.UI.Extensions; 7 | 8 | public static class MediaLibraryUITestContextExtensions 9 | { 10 | /// 11 | /// Selects a file from the Media Library for a media field. 12 | /// 13 | /// ID of the media field. 14 | /// The folder containing the desired file. 15 | /// The file to select. 16 | /// Determines whether to search for the file in the root Media Library folder. 17 | public static async Task SetMediaFieldUsingExistingFileFromMediaLibraryAsync( 18 | this UITestContext context, 19 | string fieldId, 20 | string folderName, 21 | string fileName, 22 | bool useRootFolder = false) 23 | { 24 | await context.ClickReliablyOnAsync(By.XPath($"//div[@id='{fieldId}']//a[@class='btn btn-secondary btn-sm']")); 25 | 26 | if (!useRootFolder) 27 | { 28 | await context.ClickReliablyOnAsync( 29 | By.XPath($"//a[@class='folder-menu-item']//div[contains(., '{folderName}')]")); 30 | } 31 | 32 | await context.ClickReliablyOnAsync(ByHelper.TextContains(fileName, "span")); 33 | await context.ClickReliablyOnAsync(By.XPath("//button[@class='btn btn-primary mediaFieldSelectButton']")); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/Extensions/MulticastDelegateExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | 3 | namespace System; 4 | 5 | public static class MulticastDelegateExtensions 6 | { 7 | /// 8 | /// Removes all instances of the delegate from the given . 10 | /// 11 | public static T RemoveAll(this T multicastDelegate, T delegateToRemove) 12 | where T : MulticastDelegate 13 | { 14 | if (multicastDelegate == null) return default; 15 | 16 | var handlerName = delegateToRemove.Method.Name; 17 | return (T)Delegate.RemoveAll( 18 | multicastDelegate, 19 | multicastDelegate.GetInvocationList().LastOrDefault(handler => handler.Method.Name == handlerName)); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/Extensions/OrchardCoreConfigurationExtensions.cs: -------------------------------------------------------------------------------- 1 | using Lombiq.Tests.UI.Services; 2 | using OrchardCore.Search.Elasticsearch.Core.Recipes; 3 | using System; 4 | using System.Diagnostics.CodeAnalysis; 5 | using System.Threading.Tasks; 6 | 7 | namespace Lombiq.Tests.UI.Extensions; 8 | 9 | [SuppressMessage("Naming", "CA1708:Identifiers should differ by more than case", Justification = "Necessary due to a typo.")] 10 | public static class OrchardCoreConfigurationExtensions 11 | { 12 | // When removing this, also remove the SuppressMessage attribute above. 13 | [Obsolete("Use ConfigureElasticsearchPrefix instead. This method will be removed in a future version.")] 14 | public static void ConfigureElasticSearchPrefix(this OrchardCoreConfiguration configuration, string prefix) => 15 | ConfigureElasticsearchPrefix(configuration, prefix); 16 | 17 | /// 18 | /// Configure the app settings to use the provided in the Elasticsearch indexes created by 19 | /// the . 20 | /// 21 | public static void ConfigureElasticsearchPrefix(this OrchardCoreConfiguration configuration, string prefix) => 22 | configuration.BeforeAppStart += (_, arguments) => 23 | { 24 | arguments.AddWithValue("OrchardCore:OrchardCore_Elasticsearch:IndexPrefix", prefix); 25 | return Task.CompletedTask; 26 | }; 27 | } 28 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/Extensions/ScreenshotExtensions.cs: -------------------------------------------------------------------------------- 1 | using OpenQA.Selenium; 2 | using SixLabors.ImageSharp; 3 | using System.IO; 4 | 5 | namespace Lombiq.Tests.UI.Extensions; 6 | 7 | public static class ScreenshotExtensions 8 | { 9 | /// 10 | /// Converts to . 11 | /// 12 | public static Image ToBitmap(this Screenshot screenshot) 13 | { 14 | using var screenRaw = new MemoryStream(screenshot.AsByteArray); 15 | 16 | return Image.Load(screenRaw); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/Extensions/ScriptingUITestContextExtensions.cs: -------------------------------------------------------------------------------- 1 | using Lombiq.Tests.UI.Services; 2 | using OpenQA.Selenium; 3 | using System.Text.Json; 4 | 5 | namespace Lombiq.Tests.UI.Extensions; 6 | 7 | public static class ScriptingUITestContextExtensions 8 | { 9 | public static object ExecuteScript(this UITestContext context, string script, params object[] args) => 10 | context.ExecuteLogged(nameof(ExecuteScript), script, () => context.Driver.ExecuteScript(script, args)); 11 | 12 | public static object ExecuteAsyncScript(this UITestContext context, string script, params object[] args) => 13 | context.ExecuteLogged(nameof(ExecuteAsyncScript), script, () => context.Driver.ExecuteAsyncScript(script, args)); 14 | 15 | /// 16 | /// Uses JavaScript to set form inputs to values that are hard or impossible by normal means. 17 | /// 18 | public static void SetValueWithScript(this UITestContext context, string id, object value) => 19 | ExecuteScript( 20 | context, 21 | $"document.getElementById({JsonSerializer.Serialize(id)}).value = {JsonSerializer.Serialize(value)};"); 22 | 23 | /// 24 | /// Uses JavaScript to set textarea values that are hard or impossible by normal means. 25 | /// 26 | public static void SetTextContentWithScript(this UITestContext context, string textareaId, object value) => 27 | ExecuteScript( 28 | context, 29 | $"document.getElementById({JsonSerializer.Serialize(textareaId)}).textContent = {JsonSerializer.Serialize(value)};"); 30 | } 31 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/Extensions/ScriptingWebDriverExtensions.cs: -------------------------------------------------------------------------------- 1 | using Atata; 2 | 3 | namespace OpenQA.Selenium; 4 | 5 | public static class ScriptingWebDriverExtensions 6 | { 7 | public static object ExecuteScript(this IWebDriver driver, string script, params object[] arguments) => 8 | driver.AsScriptExecutor().ExecuteScript(script, arguments); 9 | 10 | public static object ExecuteAsyncScript(this IWebDriver driver, string script, params object[] arguments) => 11 | driver.AsScriptExecutor().ExecuteAsyncScript(script, arguments); 12 | } 13 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/Extensions/SeleniumEntryExtensions.cs: -------------------------------------------------------------------------------- 1 | using Lombiq.HelpfulLibraries.Common.Utilities; 2 | using Lombiq.Tests.UI.Services; 3 | using OpenQA.Selenium.BiDi.Modules.Log; 4 | using OpenQA.Selenium.BiDi.Modules.Script; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | 9 | namespace Lombiq.Tests.UI.Extensions; 10 | 11 | public static class SeleniumEntryExtensions 12 | { 13 | public static string ToFormattedString(this IEnumerable logEntries) => 14 | string.Join(Environment.NewLine, logEntries.Select(ToFormattedString)); 15 | 16 | public static string ToFormattedString(this LogEntry entry) => 17 | StringHelper.CreateInvariant($"{entry.Timestamp:yyyy-MM-dd HH:mm:ss} {entry.Level} {entry.Text}{FormatStackTrace(entry.StackTrace)}"); 18 | 19 | public static bool IsNonSuccessBrowserLogEntry(this LogEntry entry) => 20 | OrchardCoreUITestExecutorConfiguration.IsNonSuccessBrowserLogEntry(entry); 21 | 22 | private static string FormatStackTrace(StackTrace stackTrace) 23 | { 24 | if (stackTrace == null) return string.Empty; 25 | 26 | return 27 | Environment.NewLine + 28 | "Stack trace: " + 29 | Environment.NewLine + 30 | string.Join( 31 | Environment.NewLine, 32 | stackTrace.CallFrames.Select(frame => 33 | "- " + 34 | (string.IsNullOrEmpty(frame.FunctionName) ? string.Empty : frame.FunctionName + " at ") + 35 | StringHelper.CreateInvariant($"{frame.Url}:{frame.LineNumber}:{frame.ColumnNumber}"))); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/Extensions/StatusCodeUITestContextExtensions.cs: -------------------------------------------------------------------------------- 1 | using Lombiq.Tests.UI.Services; 2 | using Shouldly; 3 | 4 | namespace Lombiq.Tests.UI.Extensions; 5 | 6 | public static class StatusCodeUITestContextExtensions 7 | { 8 | /// 9 | /// Opens the given URL asynchronously and checks the HTTP response code. 10 | /// 11 | /// Relative URL to open. 12 | /// Status code to assert. 13 | public static void AssertStatusCodeOnUrl(this UITestContext context, string url, int statusCode) => 14 | context.ExecuteAsyncScript( 15 | @"var url = arguments[0]; 16 | var callback = arguments[arguments.length - 1]; 17 | fetch(url).then(function(response) { 18 | callback(response.status); 19 | });", 20 | url) 21 | .ShouldBe(statusCode); 22 | 23 | /// 24 | /// Opens the given URL asynchronously and checks if the HTTP response code is 404. 25 | /// 26 | /// Relative URL to open. 27 | public static void AssertNotFoundResultOnUrl(this UITestContext context, string url) => 28 | context.AssertStatusCodeOnUrl(url, 404); 29 | 30 | /// 31 | /// Opens the given URL asynchronously and checks if the HTTP response code is 200. 32 | /// 33 | /// Relative URL to open. 34 | public static void AssertSuccessResultOnUrl(this UITestContext context, string url) => 35 | context.AssertStatusCodeOnUrl(url, 200); 36 | } 37 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/Extensions/TenantsUITestContextExtensions.cs: -------------------------------------------------------------------------------- 1 | using Lombiq.Tests.UI.Helpers; 2 | using Lombiq.Tests.UI.Services; 3 | using OpenQA.Selenium; 4 | using System.Threading.Tasks; 5 | 6 | namespace Lombiq.Tests.UI.Extensions; 7 | 8 | public static class TenantsUITestContextExtensions 9 | { 10 | public static async Task CreateAndSwitchToTenantManuallyAsync( 11 | this UITestContext context, 12 | string name, 13 | string urlPrefix = "", 14 | string urlHost = "", 15 | string featureProfile = "", 16 | bool navigate = true) 17 | { 18 | await context.CreateTenantManuallyAsync(name, urlPrefix, urlHost, featureProfile, navigate); 19 | 20 | await context.ClickReliablyOnByLinkTextAsync("Setup"); 21 | 22 | context.SwitchCurrentTenant(name, urlPrefix); 23 | } 24 | 25 | public static async Task CreateTenantManuallyAsync( 26 | this UITestContext context, 27 | string name, 28 | string urlPrefix = "", 29 | string urlHost = "", 30 | string featureProfile = "", 31 | bool navigate = true) 32 | { 33 | if (navigate) 34 | { 35 | await context.GoToAdminRelativeUrlAsync("/Tenants"); 36 | } 37 | 38 | await context.ClickReliablyOnByLinkTextAsync("Add Tenant"); 39 | await context.ClickAndFillInWithRetriesAsync(By.Id("Name"), name); 40 | 41 | if (!string.IsNullOrEmpty(urlPrefix)) 42 | { 43 | await context.ClickAndFillInWithRetriesAsync(By.Id("RequestUrlPrefix"), urlPrefix); 44 | } 45 | 46 | if (!string.IsNullOrEmpty(urlHost)) 47 | { 48 | await context.ClickAndFillInWithRetriesAsync(By.Id("RequestUrlHost"), urlHost); 49 | } 50 | 51 | if (!string.IsNullOrEmpty(featureProfile)) 52 | { 53 | await context.ClickReliablyOnAsync(By.XPath($"//option[@value='{featureProfile}']")); 54 | } 55 | 56 | await context.ClickReliablyOnAsync(ByHelper.ButtonText("Create")); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/Extensions/TestContextExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Xunit; 3 | 4 | namespace Lombiq.Tests.UI.Extensions; 5 | 6 | public static class TestContextExtensions 7 | { 8 | [Obsolete("Use GetElasticsearchSafeIndexName instead. This method will be removed in a future version.")] 9 | public static string GetElasticserachSafeIndexName(this ITestContext context, Guid id) => 10 | context.GetElasticsearchSafeIndexName(id); 11 | 12 | /// 13 | /// Gets a which is safe to use as an Elasticsearch index. 14 | /// 15 | /// 16 | /// A unique identifier that stays the same between setup and test. This ensures that leftover data in the test 17 | /// won't be confused with previous runs. 18 | /// 19 | public static string GetElasticsearchSafeIndexName(this ITestContext context, Guid id) 20 | { 21 | // Elasticsearch indexes are lowercase only. 22 | #pragma warning disable CA1308 // Normalize strings to uppercase 23 | var name = context? 24 | .Test? 25 | .TestDisplayName? 26 | .ToLowerInvariant() 27 | .RegexReplace("[^a-z0-9]+", "-") 28 | .Trim('-'); 29 | #pragma warning restore CA1308 // Normalize strings to uppercase 30 | 31 | if (string.IsNullOrWhiteSpace(name)) return id.ToString("N"); 32 | 33 | // An Elasticsearch index can't be longer than 255 character, but that includes the test name, tenant name, GUID 34 | // and relative index name. So altogether 100 characters is a reasonable limit for the test name prefix. 35 | if (name.Length > 100) name = name[..100]; 36 | 37 | return $"{name}-{id:N}"; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/Extensions/TestOutputHelperExtensions.cs: -------------------------------------------------------------------------------- 1 | using CliWrap; 2 | using Xunit; 3 | 4 | namespace Lombiq.Tests.UI.Extensions; 5 | 6 | public static class TestOutputHelperExtensions 7 | { 8 | /// 9 | /// Creates a new delegate pipe target that calls . 11 | /// 12 | public static PipeTarget ToPipeTarget(this ITestOutputHelper testOutputHelper, string name) => 13 | PipeTarget.ToDelegate(line => testOutputHelper.WriteOutputTimestampedAndDebug(name, line)); 14 | } 15 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/Extensions/ThemeUITestContextExtensions.cs: -------------------------------------------------------------------------------- 1 | using Atata; 2 | using Lombiq.Tests.UI.Services; 3 | using OpenQA.Selenium; 4 | using Shouldly; 5 | using System.Threading.Tasks; 6 | 7 | namespace Lombiq.Tests.UI.Extensions; 8 | 9 | /// 10 | /// Provides extension methods for testing themes' UI. 11 | /// 12 | public static class ThemeUITestContextExtensions 13 | { 14 | /// 15 | /// Goes to the home page and checks if the site name is correct, and the credits are visible. 16 | /// 17 | /// The context of the currently executed UI test. 18 | /// The name of the current site. 19 | /// CSS class name of the credits HTML element. 20 | /// CSS class name of the HTML element that has the site name text inside. 21 | public static async Task GoToHomePageAndCheckNavBarAndCreditsAsync( 22 | this UITestContext context, 23 | string siteName = "Test Site", 24 | string creditsClass = "credits", 25 | string siteNameClass = "navbar-brand") 26 | { 27 | await context.GoToHomePageAsync(); 28 | 29 | context.GetText(By.ClassName(siteNameClass)).ShouldBe(siteName); 30 | context.Driver.Exists(By.ClassName(creditsClass).Visible()); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/Extensions/VisibilityUITestContextExtensions.cs: -------------------------------------------------------------------------------- 1 | using Lombiq.Tests.UI.Services; 2 | 3 | namespace Lombiq.Tests.UI.Extensions; 4 | 5 | public static class VisibilityUITestContextExtensions 6 | { 7 | /// 8 | /// Make the native checkboxes visible on the admin so they can be selected and Selenium operations can work on them 9 | /// as usual. This is necessary because the Orchard admin theme uses custom controls which hide the native 10 | /// <input> elements by setting their opacity to 0. Thus they're inaccessible to Selenium unless 11 | /// they're revealed like this. Once interactions with these elements are done it's good practice to revert this 12 | /// change with . 13 | /// 14 | public static void MakeAdminCheckboxesVisible(this UITestContext context) => 15 | context.ExecuteScript("Array.from(document.querySelectorAll('.custom-control-input')).forEach(x => x.style.opacity = 1)"); 16 | 17 | /// 18 | /// Reverts the visibility of admin checkboxes made visible with . 20 | /// 21 | public static void RevertAdminCheckboxesVisibility(this UITestContext context) => 22 | context.ExecuteScript("Array.from(document.querySelectorAll('.custom-control-input')).forEach(x => x.style.opacity = 0)"); 23 | } 24 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/Extensions/WebApplicationInstanceUITestContextExtensions.cs: -------------------------------------------------------------------------------- 1 | using Lombiq.Tests.UI.Services; 2 | using System.Threading.Tasks; 3 | 4 | namespace Lombiq.Tests.UI.Extensions; 5 | 6 | public static class WebApplicationInstanceUITestContextExtensions 7 | { 8 | /// 9 | /// Restarts the application and refreshes the current page to warm it up. 10 | /// 11 | public static async Task RestartAndWarmUpApplicationAsync(this UITestContext context) 12 | { 13 | await context.Application.RestartAsync(); 14 | context.Refresh(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/Extensions/WebDriverExceptionExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace OpenQA.Selenium; 2 | 3 | public static class WebDriverExceptionExtensions 4 | { 5 | /// 6 | /// Checks if the exception is one that's thrown when trying to access an element that's stale. 7 | /// 8 | public static bool IsStateElementLikeException(this WebDriverException exception) => 9 | exception is StaleElementReferenceException || 10 | // This is the same as StaleElementReferenceException but for some reason ChromeDriver randomly throws this 11 | // instead. Also see: 12 | // https://stackoverflow.com/questions/76250688/webdriverexception-unhandled-inspector-error-no-node-with-given-id-found-at-a. 13 | (exception is UnknownErrorException ex && ex.Message.Contains("Node with given id does not belong to the document")); 14 | } 15 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/GlobalSuppressions.cs: -------------------------------------------------------------------------------- 1 | // This file is used by Code Analysis to maintain SuppressMessage attributes that are applied to this project. 2 | // Project-level suppressions either have no target or are given a specific target and scoped to a namespace, type, 3 | // member, etc. 4 | 5 | using System.Diagnostics.CodeAnalysis; 6 | 7 | [assembly: SuppressMessage( 8 | "Security", 9 | "MA0009:Add regex evaluation timeout", 10 | Justification = "Regexes in this project don't use user input.")] 11 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/Helpers/AppLogAssertionHelper.cs: -------------------------------------------------------------------------------- 1 | using Lombiq.Tests.UI.Services; 2 | using System; 3 | using System.Linq.Expressions; 4 | 5 | namespace Lombiq.Tests.UI.Helpers; 6 | 7 | public static class AppLogAssertionHelper 8 | { 9 | /// 10 | /// An wrapping . 11 | /// 12 | public static readonly Expression> NotMediaCacheEntriesPredicate = 13 | logEntry => NotMediaCacheEntries(logEntry); 14 | 15 | /// 16 | /// Creates a predicate that can be used to filter out "Error deleting cache folder" log entries coming from 17 | /// DefaultMediaFileStoreCacheFileProvider. These errors frequently happen during UI testing when using Azure 18 | /// Blob Storage for media storage. They're harmless, though. 19 | /// 20 | public static bool NotMediaCacheEntries(IApplicationLogEntry logEntry) => 21 | logEntry.Category != "OrchardCore.Media.Core.DefaultMediaFileStoreCacheFileProvider" || 22 | !logEntry.Message.StartsWithOrdinalIgnoreCase("Error deleting cache folder"); 23 | } 24 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/Helpers/ConfigurationHelper.cs: -------------------------------------------------------------------------------- 1 | using Lombiq.Tests.UI.Services; 2 | using System; 3 | using System.Threading.Tasks; 4 | 5 | namespace Lombiq.Tests.UI.Helpers; 6 | 7 | public static class ConfigurationHelper 8 | { 9 | public static Func DisableHtmlValidation => 10 | configuration => 11 | { 12 | configuration.HtmlValidationConfiguration.RunHtmlValidationAssertionOnAllPageChanges = false; 13 | return Task.CompletedTask; 14 | }; 15 | } 16 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/Helpers/FileUploadHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | 4 | namespace Lombiq.Tests.UI.Helpers; 5 | 6 | public static class FileUploadHelper 7 | { 8 | private static readonly string BasePath = Path.Combine(Environment.CurrentDirectory, "SampleUploadFiles"); 9 | 10 | public static readonly string SamplePdfFileName = "document.pdf"; 11 | public static readonly string SamplePngFileName = "image.png"; 12 | public static readonly string SampleDocxFileName = "uploadingtestfiledocx.docx"; 13 | public static readonly string SampleXlsxFileName = "uploadingtestfilexlsx.xlsx"; 14 | 15 | public static readonly string SamplePdfPath = Path.Combine(BasePath, SamplePdfFileName); 16 | public static readonly string SamplePngPath = Path.Combine(BasePath, SamplePngFileName); 17 | public static readonly string SampleDocxPath = Path.Combine(BasePath, SampleDocxFileName); 18 | public static readonly string SampleXlsxPath = Path.Combine(BasePath, SampleXlsxFileName); 19 | } 20 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/Helpers/HttpClientHelper.cs: -------------------------------------------------------------------------------- 1 | #nullable enable 2 | 3 | using System; 4 | using System.Diagnostics.CodeAnalysis; 5 | using System.Net.Http; 6 | 7 | namespace Lombiq.Tests.UI.Helpers; 8 | 9 | public static class HttpClientHelper 10 | { 11 | [SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope", Justification = "Disposed by the returned client.")] 12 | public static HttpClient CreateCertificateIgnoringHttpClient(Uri? baseUri = null) => 13 | new(CreateCertificateIgnoringHttpClientHandler()) { BaseAddress = baseUri }; 14 | 15 | /// 16 | /// Creates a normally dangerous HTTP client handler that accepts any certificate, to allow working with self-signed 17 | /// development certificates. 18 | /// 19 | public static HttpClientHandler CreateCertificateIgnoringHttpClientHandler() => 20 | new() 21 | { 22 | ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator, 23 | // Revoked certificates shouldn't be used though. 24 | CheckCertificateRevocationList = true, 25 | }; 26 | } 27 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/Helpers/WebAppConfigHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | 4 | namespace Lombiq.Tests.UI.Helpers; 5 | 6 | public static class WebAppConfigHelper 7 | { 8 | private static readonly string[] Separator = ["src", "test"]; 9 | 10 | /// 11 | /// Retrieves the absolute path to the assembly (DLL) of the application being tested. 12 | /// 13 | /// The web app's project name. 14 | /// 15 | /// The name of the folder that corresponds to the .NET version in the build output folder (e.g. "net8.0"). 16 | /// 17 | /// The absolute path to the assembly (DLL) of the application being tested. 18 | public static string GetAbsoluteApplicationAssemblyPath(string webAppName, string frameworkFolderName = "net8.0") 19 | { 20 | string baseDirectory; 21 | 22 | if (File.Exists(webAppName + ".dll")) 23 | { 24 | baseDirectory = AppContext.BaseDirectory; 25 | } 26 | else 27 | { 28 | var outputFolderContainingPath = Path.Combine( 29 | AppContext.BaseDirectory.Split(Separator, StringSplitOptions.RemoveEmptyEntries)[0], 30 | "src", 31 | webAppName, 32 | "bin"); 33 | 34 | baseDirectory = Path.Combine(outputFolderContainingPath, "Debug", frameworkFolderName); 35 | 36 | if (!Directory.Exists(baseDirectory)) 37 | { 38 | baseDirectory = Path.Combine(outputFolderContainingPath, "Release", frameworkFolderName); 39 | } 40 | } 41 | 42 | return Path.Combine(baseDirectory, webAppName + ".dll"); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/KillLeftoverProcesses.bat: -------------------------------------------------------------------------------- 1 | taskkill /f /im "dotnet.exe" 2 | taskkill /f /im "chromedriver.exe" 3 | taskkill /f /im "geckodriver.exe" 4 | taskkill /f /im "msedgedriver.exe" 5 | taskkill /f /im "Lombiq.UITestingToolbox.AppUnderTest*" 6 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/Models/DockerConfiguration.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace Lombiq.Tests.UI.Models; 4 | 5 | public class DockerConfiguration 6 | { 7 | public string ContainerName { get; set; } 8 | 9 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 10 | public string ContainerSnapshotPath { get; set; } = "/data/Snapshots"; 11 | } 12 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/Models/FakeBrowserVideoSource.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | 4 | namespace Lombiq.Tests.UI.Models; 5 | 6 | public class FakeBrowserVideoSource 7 | { 8 | /// 9 | /// Gets or sets a callback that will be used to obtain the video content and will be saved and used 10 | /// as a fake video capture file for the Chrome browser. The consumer will dispose of the returned 11 | /// after the callback is called. 12 | /// 13 | public Func StreamProvider { get; set; } 14 | 15 | /// 16 | /// Gets or sets the video format of the provided stream. 17 | /// 18 | public FakeBrowserVideoSourceFileFormat Format { get; set; } = FakeBrowserVideoSourceFileFormat.MJpeg; 19 | } 20 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/Models/FakeBrowserVideoSourceFileFormat.cs: -------------------------------------------------------------------------------- 1 | namespace Lombiq.Tests.UI.Models; 2 | 3 | public enum FakeBrowserVideoSourceFileFormat 4 | { 5 | MJpeg, 6 | Y4m, 7 | } 8 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/Models/FakeLoggerLogApplicationLog.cs: -------------------------------------------------------------------------------- 1 | using Lombiq.HelpfulLibraries.Common.Utilities; 2 | using Lombiq.Tests.UI.Services; 3 | using Microsoft.Extensions.Logging; 4 | using Microsoft.Extensions.Logging.Testing; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using System.Threading.Tasks; 9 | 10 | namespace Lombiq.Tests.UI.Models; 11 | 12 | public sealed class FakeLoggerLogApplicationLog : IApplicationLog 13 | { 14 | public string Name => "FakeLog"; 15 | public FakeLogCollector LogCollector { get; init; } 16 | public int EntryCount => LogCollector.Count; 17 | 18 | public Task> GetEntriesAsync() 19 | { 20 | var records = LogCollector.GetSnapshot(); 21 | 22 | return Task.FromResult(records.Select(record => (IApplicationLogEntry)new FakeLoggerApplicationLogEntry(record))); 23 | } 24 | 25 | public Task RemoveAsync() 26 | { 27 | LogCollector.Clear(); 28 | return Task.CompletedTask; 29 | } 30 | } 31 | 32 | public record FakeLoggerApplicationLogEntry(FakeLogRecord LogRecord) : IApplicationLogEntry 33 | { 34 | public LogLevel Level => LogRecord.Level; 35 | public EventId Id => LogRecord.Id; 36 | public Exception Exception => LogRecord.Exception; 37 | public string Message => LogRecord.Message; 38 | public string Category => LogRecord.Category; 39 | public DateTimeOffset Timestamp => LogRecord.Timestamp; 40 | 41 | public override string ToString() => FormatLogRecord(LogRecord); 42 | 43 | public static string FormatLogRecord(FakeLogRecord record) => 44 | StringHelper.CreateInvariant($"{record.Timestamp:yyyy-MM-dd HH:mm:ss.fff} [{record.Level}] {record.Category}: {record.Message}") + 45 | (record.Exception != null ? record.Exception.ToString() : string.Empty); 46 | } 47 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/Models/FrontendServerContext.cs: -------------------------------------------------------------------------------- 1 | using CliWrap; 2 | using System; 3 | using System.Threading.Tasks; 4 | 5 | namespace Lombiq.Tests.UI.Models; 6 | 7 | public class FrontendServerContext 8 | { 9 | public int Port { get; set; } 10 | public CommandTask Task { get; set; } 11 | public Func StopAsync { get; set; } 12 | } 13 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/Models/ITestDumpItem.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Threading.Tasks; 4 | 5 | namespace Lombiq.Tests.UI.Models; 6 | 7 | /// 8 | /// Represents an item, including the corresponding file, that's in the test's dump. 9 | /// 10 | public interface ITestDumpItem : IDisposable 11 | { 12 | /// 13 | /// Gets the that contains the content of the test dump item. 14 | /// 15 | /// The that contains the content of the test dump item. 16 | Task GetStreamAsync(); 17 | } 18 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/Models/IWebContentState.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Lombiq.Tests.UI.Models; 4 | 5 | /// 6 | /// Represents a state of some web content that may change, for example via page navigation or dynamic content after 7 | /// user interaction. 8 | /// 9 | public interface IWebContentState 10 | { 11 | /// 12 | /// Returns if navigation has occurred or the content has changed based on some previously 13 | /// provided content. 14 | /// 15 | bool CheckIfNavigationHasOccurred(); 16 | 17 | /// 18 | /// Waits until evaluates to . 19 | /// 20 | public void Wait(TimeSpan? timeout = null, TimeSpan? interval = null); 21 | } 22 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/Models/InstanceCommandLineArgs.cs: -------------------------------------------------------------------------------- 1 | using Lombiq.HelpfulLibraries.Common.Utilities; 2 | using System.Collections.Generic; 3 | 4 | namespace Lombiq.Tests.UI.Models; 5 | 6 | public class InstanceCommandLineArgumentsBuilder 7 | { 8 | private readonly List _arguments = []; 9 | 10 | public IEnumerable Arguments => _arguments; 11 | 12 | public InstanceCommandLineArgumentsBuilder AddSwitch(string argument) 13 | { 14 | _arguments.Add($"{PrepareArg(argument)}"); 15 | 16 | return this; 17 | } 18 | 19 | public InstanceCommandLineArgumentsBuilder AddWithValue(string key, T value) 20 | { 21 | _arguments.Add(StringHelper.CreateInvariant($"{PrepareArg(key)}={value}")); 22 | 23 | return this; 24 | } 25 | 26 | private static string PrepareArg(string argument) => $"--{argument.TrimStart('-')}"; 27 | } 28 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/Models/JsonHtmlValidationError.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace Lombiq.Tests.UI.Models; 5 | 6 | public class JsonHtmlValidationError 7 | { 8 | [JsonPropertyName("ruleId")] 9 | public string RuleId { get; set; } 10 | [JsonPropertyName("severity")] 11 | public int Severity { get; set; } 12 | [JsonPropertyName("message")] 13 | public string Message { get; set; } 14 | [JsonPropertyName("offset")] 15 | public int Offset { get; set; } 16 | [JsonPropertyName("line")] 17 | public int Line { get; set; } 18 | [JsonPropertyName("column")] 19 | public int Column { get; set; } 20 | [JsonPropertyName("size")] 21 | public int Size { get; set; } 22 | [JsonPropertyName("selector")] 23 | public string Selector { get; set; } 24 | [JsonPropertyName("ruleUrl")] 25 | public string RuleUrl { get; set; } 26 | [JsonPropertyName("context")] 27 | public JsonElement Context { get; set; } 28 | } 29 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/Models/OrchardCoreAppStartContext.cs: -------------------------------------------------------------------------------- 1 | using Lombiq.Tests.UI.Services; 2 | using System; 3 | 4 | namespace Lombiq.Tests.UI.Models; 5 | 6 | public record OrchardCoreAppStartContext(string ContentRootPath, Uri Url, PortLeaseManager PortLeaseManager); 7 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/Models/PageNavigationState.cs: -------------------------------------------------------------------------------- 1 | using Atata; 2 | using Lombiq.Tests.UI.Extensions; 3 | using Lombiq.Tests.UI.Helpers; 4 | using Lombiq.Tests.UI.Services; 5 | using OpenQA.Selenium; 6 | using System; 7 | using System.Threading; 8 | 9 | namespace Lombiq.Tests.UI.Models; 10 | 11 | /// 12 | /// Represents the current web page in terms of whether the browser has navigated away from it yet. 13 | /// 14 | public class PageNavigationState : IWebContentState 15 | { 16 | private readonly IWebElement _root; 17 | 18 | public PageNavigationState(IWebElement root) => _root = root; 19 | 20 | public PageNavigationState(UITestContext context) 21 | : this(context.Get(By.TagName("html").OfAnyVisibility().Safely())) 22 | { 23 | } 24 | 25 | public bool CheckIfNavigationHasOccurred() 26 | { 27 | // The response can be empty, without even an tag. 28 | if (_root == null) return true; 29 | 30 | try 31 | { 32 | // Just any element operation to cause a StaleElementReferenceException if it's stale. If it isn't then this 33 | // will always return false. 34 | return _root.Size.Width < 0; 35 | } 36 | catch (WebDriverException ex) when (ex.IsStateElementLikeException()) 37 | { 38 | return true; 39 | } 40 | } 41 | 42 | public void Wait(TimeSpan? timeout = null, TimeSpan? interval = null) => 43 | ReliabilityHelper.DoWithRetriesOrFail(CheckIfNavigationHasOccurred, timeout, interval, CancellationToken.None); 44 | } 45 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/Models/RunningContextContainer.cs: -------------------------------------------------------------------------------- 1 | using Lombiq.Tests.UI.Services; 2 | 3 | namespace Lombiq.Tests.UI.Models; 4 | 5 | public record RunningContextContainer( 6 | SqlServerRunningContext SqlServerRunningContext, 7 | SmtpServiceRunningContext SmtpServiceRunningContext, 8 | AzureBlobStorageRunningContext AzureBlobStorageRunningContext, 9 | ElasticsearchRunningContext ElasticsearchRunningContext); 10 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/Models/TestDumpItem.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Threading.Tasks; 4 | 5 | namespace Lombiq.Tests.UI.Models; 6 | 7 | public class TestDumpItem : ITestDumpItem 8 | { 9 | private readonly Func> _getStream; 10 | private readonly Action _dispose; 11 | private bool _disposed; 12 | 13 | public TestDumpItem( 14 | Func> getStream, 15 | Action dispose = null) 16 | { 17 | _getStream = getStream ?? throw new ArgumentNullException(nameof(getStream)); 18 | _dispose = dispose; 19 | } 20 | 21 | public Task GetStreamAsync() 22 | { 23 | ObjectDisposedException.ThrowIf(_disposed, this); 24 | 25 | return _getStream(); 26 | } 27 | 28 | protected virtual void Dispose(bool disposing) 29 | { 30 | if (!_disposed) 31 | { 32 | if (disposing) 33 | { 34 | _dispose?.Invoke(); 35 | } 36 | 37 | _disposed = true; 38 | } 39 | } 40 | 41 | public void Dispose() 42 | { 43 | Dispose(disposing: true); 44 | GC.SuppressFinalize(this); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/Models/TestDumpItemGeneric.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Threading.Tasks; 4 | 5 | namespace Lombiq.Tests.UI.Models; 6 | 7 | public class TestDumpItemGeneric : ITestDumpItem 8 | { 9 | private readonly TContent _content; 10 | private readonly Func> _getStream; 11 | private readonly Action _dispose; 12 | private bool _disposed; 13 | 14 | public TestDumpItemGeneric( 15 | TContent content, 16 | Func> getStream = null, 17 | Action dispose = null) 18 | { 19 | if (content is not Stream && getStream == null) 20 | { 21 | throw new ArgumentException( 22 | $"{nameof(content)} must be of type {nameof(Stream)} or {nameof(getStream)} must not be null."); 23 | } 24 | 25 | _content = content; 26 | _getStream = getStream; 27 | _dispose = dispose; 28 | } 29 | 30 | public Task GetStreamAsync() 31 | { 32 | ObjectDisposedException.ThrowIf(_disposed, this); 33 | 34 | if (_content is Stream stream && _getStream == null) 35 | { 36 | return Task.FromResult(stream); 37 | } 38 | 39 | return _getStream(_content); 40 | } 41 | 42 | protected virtual void Dispose(bool disposing) 43 | { 44 | if (!_disposed) 45 | { 46 | if (disposing) 47 | { 48 | if (_content is IDisposable disposable && _dispose == null) 49 | { 50 | disposable.Dispose(); 51 | } 52 | else 53 | { 54 | _dispose?.Invoke(_content); 55 | } 56 | } 57 | 58 | _disposed = true; 59 | } 60 | } 61 | 62 | public void Dispose() 63 | { 64 | Dispose(disposing: true); 65 | GC.SuppressFinalize(this); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/Models/UITestManifest.cs: -------------------------------------------------------------------------------- 1 | using Lombiq.Tests.UI.Services; 2 | using System; 3 | using System.Threading.Tasks; 4 | using Xunit; 5 | using Xunit.Sdk; 6 | 7 | namespace Lombiq.Tests.UI.Models; 8 | 9 | /// 10 | /// Provides data about the currently executing test. 11 | /// 12 | public class UITestManifest 13 | { 14 | public ITest XunitTest => TestContext.Current.Test; 15 | public string Name => XunitTest.TestDisplayName; 16 | public Func TestAsync { get; private set; } 17 | 18 | public UITestManifest(Func testAsync) => TestAsync = testAsync; 19 | } 20 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/Models/UserRegistrationParameters.cs: -------------------------------------------------------------------------------- 1 | #nullable enable 2 | 3 | using Lombiq.Tests.UI.Constants; 4 | using Lombiq.Tests.UI.Pages; 5 | using System; 6 | 7 | namespace Lombiq.Tests.UI.Models; 8 | 9 | public record UserRegistrationParameters( 10 | string UserName, 11 | string Email, 12 | string Password = TestUser.Password, 13 | string? ConfirmPassword = TestUser.Password, 14 | string LoginButtonText = OrchardCoreLoginPage.DefaultLoginButtonText) 15 | { 16 | [Obsolete("Use CreateTest() instead.")] 17 | public static UserRegistrationParameters CreateDefault() => 18 | new("TestUser", "testuser@example.org"); 19 | 20 | public static UserRegistrationParameters CreateTest(string loginButtonText = OrchardCoreLoginPage.DefaultLoginButtonText) => 21 | new(TestUser.UserName, TestUser.Email, LoginButtonText: loginButtonText); 22 | 23 | public static UserRegistrationParameters CreateDefaultUser(string loginButtonText = OrchardCoreLoginPage.DefaultLoginButtonText) => 24 | new(DefaultUser.UserName, DefaultUser.Email, LoginButtonText: loginButtonText); 25 | } 26 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/Models/VisualVerificationMatchConfiguration.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Lombiq.Tests.UI.Models; 4 | 5 | public class VisualVerificationMatchConfiguration 6 | where TSelf : VisualVerificationMatchConfiguration 7 | { 8 | /// 9 | /// Gets the prefix applied to all file names. 10 | /// 11 | public string FileNamePrefix { get; private set; } 12 | 13 | /// 14 | /// Gets the suffix applied to all file names. 15 | /// 16 | public string FileNameSuffix { get; private set; } 17 | 18 | /// 19 | /// Sets . 20 | /// 21 | /// The prefix applied to all file names. 22 | public TSelf WithFileNamePrefix(string value) 23 | { 24 | FileNamePrefix = value; 25 | 26 | return (TSelf)this; 27 | } 28 | 29 | /// 30 | /// Sets . 31 | /// 32 | /// The suffix applied to all file names. 33 | public TSelf WithFileNameSuffix(string value) 34 | { 35 | FileNameSuffix = value; 36 | 37 | return (TSelf)this; 38 | } 39 | 40 | public string WrapFileName(string fileName) => 41 | new[] 42 | { 43 | FileNamePrefix, 44 | fileName, 45 | FileNameSuffix, 46 | }.JoinNotNullOrEmpty("-"); 47 | } 48 | 49 | public class VisualMatchConfiguration : VisualVerificationMatchConfiguration { } 50 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/MonkeyTesting/IMonkeyTestingUrlFilter.cs: -------------------------------------------------------------------------------- 1 | using Lombiq.Tests.UI.Services; 2 | using System; 3 | 4 | namespace Lombiq.Tests.UI.MonkeyTesting; 5 | 6 | /// 7 | /// An URL filter that is used in monkey testing. 8 | /// 9 | public interface IMonkeyTestingUrlFilter 10 | { 11 | /// 12 | /// Determines whether this filter allows the specified URL to be tested. 13 | /// 14 | /// The context. 15 | /// The URL. 16 | /// if URL passes the filter; otherwise, . 17 | bool AllowUrl(UITestContext context, Uri url); 18 | } 19 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/MonkeyTesting/IMonkeyTestingUrlSanitizer.cs: -------------------------------------------------------------------------------- 1 | using Lombiq.Tests.UI.Services; 2 | using System; 3 | 4 | namespace Lombiq.Tests.UI.MonkeyTesting; 5 | 6 | /// 7 | /// An URL sanitizer that is used in monkey testing. 8 | /// 9 | public interface IMonkeyTestingUrlSanitizer 10 | { 11 | /// 12 | /// Sanitizes the specified URL. 13 | /// 14 | /// The context. 15 | /// The URL. 16 | /// A sanitized or original URL. 17 | Uri Sanitize(UITestContext context, Uri url); 18 | } 19 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/MonkeyTesting/PageMonkeyTestInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Lombiq.Tests.UI.MonkeyTesting; 4 | 5 | internal sealed class PageMonkeyTestInfo 6 | { 7 | internal PageMonkeyTestInfo(Uri url, Uri sanitizedUrl, TimeSpan timeToTest) 8 | { 9 | Url = url; 10 | SanitizedUrl = sanitizedUrl; 11 | TimeToTest = timeToTest; 12 | } 13 | 14 | internal Uri Url { get; } 15 | 16 | internal Uri SanitizedUrl { get; } 17 | 18 | internal TimeSpan TimeToTest { get; set; } 19 | 20 | internal bool HasTimeToTest => TimeToTest > TimeSpan.Zero; 21 | } 22 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/MonkeyTesting/UrlFilters/AdminMonkeyTestingUrlFilter.cs: -------------------------------------------------------------------------------- 1 | using Lombiq.Tests.UI.Services; 2 | using System; 3 | 4 | namespace Lombiq.Tests.UI.MonkeyTesting.UrlFilters; 5 | 6 | /// 7 | /// URL filter that matches the URL to see if it's an admin page (i.e. an URL under /admin). 8 | /// 9 | public class AdminMonkeyTestingUrlFilter : IMonkeyTestingUrlFilter 10 | { 11 | private readonly StartsWithMonkeyTestingUrlFilter _startsWithMonkeyTestingUrlFilter; 12 | 13 | public AdminMonkeyTestingUrlFilter(UITestContext context) => 14 | _startsWithMonkeyTestingUrlFilter = new("/admin", context.AdminUrlPrefix); 15 | 16 | public bool AllowUrl(UITestContext context, Uri url) => _startsWithMonkeyTestingUrlFilter.AllowUrl(context, url); 17 | } 18 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/MonkeyTesting/UrlFilters/MatchesRegexMonkeyTestingUrlFilter.cs: -------------------------------------------------------------------------------- 1 | using Lombiq.Tests.UI.Services; 2 | using System; 3 | using System.Diagnostics; 4 | using System.Text.RegularExpressions; 5 | 6 | namespace Lombiq.Tests.UI.MonkeyTesting.UrlFilters; 7 | 8 | /// 9 | /// URL filter that matches the URL against the configured regex pattern. 10 | /// 11 | [DebuggerDisplay("Regex: \"{_regex}\"")] 12 | public class MatchesRegexMonkeyTestingUrlFilter : IMonkeyTestingUrlFilter 13 | { 14 | private readonly Regex _regex; 15 | 16 | public MatchesRegexMonkeyTestingUrlFilter(string regexPattern) 17 | : this(new Regex(regexPattern, RegexOptions.Compiled | RegexOptions.ExplicitCapture)) 18 | { 19 | } 20 | 21 | public MatchesRegexMonkeyTestingUrlFilter(Regex regex) => 22 | _regex = regex ?? throw new ArgumentNullException(nameof(regex)); 23 | 24 | public bool AllowUrl(UITestContext context, Uri url) => _regex.IsMatch(url.AbsoluteUri); 25 | } 26 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/MonkeyTesting/UrlFilters/NotAdminMonkeyTestingUrlFilter.cs: -------------------------------------------------------------------------------- 1 | using Lombiq.Tests.UI.Services; 2 | using System; 3 | 4 | namespace Lombiq.Tests.UI.MonkeyTesting.UrlFilters; 5 | 6 | /// 7 | /// URL filter that matches the URL to see if it's NOT an admin page (i.e. an URL NOT under /admin). 8 | /// 9 | public class NotAdminMonkeyTestingUrlFilter : IMonkeyTestingUrlFilter 10 | { 11 | private readonly AdminMonkeyTestingUrlFilter _adminMonkeyTestingUrlFilter; 12 | 13 | public NotAdminMonkeyTestingUrlFilter(UITestContext context) => _adminMonkeyTestingUrlFilter = new(context); 14 | 15 | public bool AllowUrl(UITestContext context, Uri url) => !_adminMonkeyTestingUrlFilter.AllowUrl(context, url); 16 | } 17 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/MonkeyTesting/UrlFilters/NotStartsWithMonkeyTestingUrlFilter.cs: -------------------------------------------------------------------------------- 1 | using Lombiq.Tests.UI.Services; 2 | using System; 3 | using System.Diagnostics; 4 | 5 | namespace Lombiq.Tests.UI.MonkeyTesting.UrlFilters; 6 | 7 | /// 8 | /// URL filter that matches the URL to see if NOT starts with the configured relative URL(s). 9 | /// 10 | [DebuggerDisplay("Does NOT start with {_relativeUrlNotStartsWith}")] 11 | public class NotStartsWithMonkeyTestingUrlFilter : IMonkeyTestingUrlFilter 12 | { 13 | private readonly string[] _relativeUrlNotStartsWith; 14 | 15 | public NotStartsWithMonkeyTestingUrlFilter(params string[] relativeUrlNotStartsWith) => 16 | _relativeUrlNotStartsWith = relativeUrlNotStartsWith; 17 | 18 | public bool AllowUrl(UITestContext context, Uri url) => 19 | !_relativeUrlNotStartsWith.Exists(url.PathAndQuery.StartsWithOrdinalIgnoreCase); 20 | } 21 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/MonkeyTesting/UrlFilters/StartsWithBaseUrlMonkeyTestingUrlFilter.cs: -------------------------------------------------------------------------------- 1 | using Lombiq.Tests.UI.Services; 2 | using System; 3 | 4 | namespace Lombiq.Tests.UI.MonkeyTesting.UrlFilters; 5 | 6 | /// 7 | /// URL filter that matches the URL against the base URL of the application under test, i.e. it disallows leaving the 8 | /// tenant. 9 | /// 10 | public sealed class StartsWithBaseUrlMonkeyTestingUrlFilter : IMonkeyTestingUrlFilter 11 | { 12 | public bool AllowUrl(UITestContext context, Uri url) => 13 | url.AbsoluteUri.StartsWith(context.Scope.BaseUri.AbsoluteUri, StringComparison.Ordinal); 14 | } 15 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/MonkeyTesting/UrlFilters/StartsWithMonkeyTestingUrlFilter.cs: -------------------------------------------------------------------------------- 1 | using Lombiq.Tests.UI.Services; 2 | using System; 3 | using System.Diagnostics; 4 | 5 | namespace Lombiq.Tests.UI.MonkeyTesting.UrlFilters; 6 | 7 | /// 8 | /// URL filter that matches the URL to see if starts with the configured relative URL(s). 9 | /// 10 | [DebuggerDisplay("Starts with {_relativeUrlStartsWith}")] 11 | public class StartsWithMonkeyTestingUrlFilter : IMonkeyTestingUrlFilter 12 | { 13 | private readonly string[] _relativeUrlStartsWith; 14 | 15 | public StartsWithMonkeyTestingUrlFilter(params string[] relativeUrlStartsWith) => 16 | _relativeUrlStartsWith = relativeUrlStartsWith; 17 | 18 | public bool AllowUrl(UITestContext context, Uri url) => 19 | _relativeUrlStartsWith.Exists(url.PathAndQuery.StartsWithOrdinalIgnoreCase); 20 | } 21 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/MonkeyTesting/UrlSanitizers/RemovesBaseUrlMonkeyTestingUrlSanitizer.cs: -------------------------------------------------------------------------------- 1 | using Lombiq.Tests.UI.Services; 2 | using System; 3 | 4 | namespace Lombiq.Tests.UI.MonkeyTesting.UrlSanitizers; 5 | 6 | /// 7 | /// Represents the URL sanitizer that removes an base URL part if it is present. 8 | /// 9 | public sealed class RemovesBaseUrlMonkeyTestingUrlSanitizer : IMonkeyTestingUrlSanitizer 10 | { 11 | public Uri Sanitize(UITestContext context, Uri url) 12 | { 13 | string baseUrl = context.Scope.BaseUri.AbsoluteUri; 14 | string urlAsString = url.OriginalString; 15 | 16 | if (!string.IsNullOrEmpty(baseUrl) && urlAsString.StartsWith(baseUrl, StringComparison.Ordinal)) 17 | { 18 | urlAsString = urlAsString[baseUrl.Length..]; 19 | if (!urlAsString.StartsWith('/')) urlAsString = '/' + urlAsString; 20 | return new Uri(urlAsString, UriKind.RelativeOrAbsolute); 21 | } 22 | 23 | return url; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/MonkeyTesting/UrlSanitizers/RemovesByRegexMonkeyTestingUrlSanitizer.cs: -------------------------------------------------------------------------------- 1 | using Lombiq.Tests.UI.Services; 2 | using System; 3 | using System.Diagnostics; 4 | using System.Text.RegularExpressions; 5 | 6 | namespace Lombiq.Tests.UI.MonkeyTesting.UrlSanitizers; 7 | 8 | /// 9 | /// URL sanitizer that removes parts that match the specific regex pattern. 10 | /// 11 | [DebuggerDisplay("Regex: \"{_regex}\"")] 12 | public class RemovesByRegexMonkeyTestingUrlSanitizer : IMonkeyTestingUrlSanitizer 13 | { 14 | private readonly Regex _regex; 15 | 16 | public RemovesByRegexMonkeyTestingUrlSanitizer(string regexPattern) 17 | : this(new Regex(regexPattern, RegexOptions.Compiled | RegexOptions.ExplicitCapture)) 18 | { 19 | } 20 | 21 | public RemovesByRegexMonkeyTestingUrlSanitizer(Regex regex) => 22 | _regex = regex ?? throw new ArgumentNullException(nameof(regex)); 23 | 24 | public Uri Sanitize(UITestContext context, Uri url) 25 | { 26 | string urlAsString = url.OriginalString; 27 | 28 | if (_regex.IsMatch(urlAsString)) 29 | { 30 | string processedUrl = _regex.Replace(urlAsString, string.Empty); 31 | return new(processedUrl, UriKind.RelativeOrAbsolute); 32 | } 33 | 34 | return url; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/MonkeyTesting/UrlSanitizers/RemovesFragmentMonkeyTestingUrlSanitizer.cs: -------------------------------------------------------------------------------- 1 | using Lombiq.Tests.UI.Services; 2 | using System; 3 | 4 | namespace Lombiq.Tests.UI.MonkeyTesting.UrlSanitizers; 5 | 6 | /// 7 | /// URL sanitizer that removes a fragment part of an URL (i.e. that comes after the hash: #). 8 | /// 9 | public sealed class RemovesFragmentMonkeyTestingUrlSanitizer : IMonkeyTestingUrlSanitizer 10 | { 11 | public Uri Sanitize(UITestContext context, Uri url) 12 | { 13 | if (!string.IsNullOrEmpty(url.Fragment)) 14 | { 15 | string processedUrl = url.GetComponents(UriComponents.HttpRequestUrl, UriFormat.Unescaped); 16 | 17 | return new(processedUrl, UriKind.RelativeOrAbsolute); 18 | } 19 | 20 | return url; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/MonkeyTesting/UrlSanitizers/RemovesQueryParameterMonkeyTestingUrlSanitizer.cs: -------------------------------------------------------------------------------- 1 | namespace Lombiq.Tests.UI.MonkeyTesting.UrlSanitizers; 2 | 3 | /// 4 | /// URL sanitizer that removes specific query parameter. 5 | /// 6 | public class RemovesQueryParameterMonkeyTestingUrlSanitizer : RemovesByRegexMonkeyTestingUrlSanitizer 7 | { 8 | public RemovesQueryParameterMonkeyTestingUrlSanitizer(string parameterName) 9 | : base(@$"(\b{parameterName}=[^&]*&|[\?&]{parameterName}=[^&]*$)") 10 | { 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/NuGetIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lombiq/UI-Testing-Toolbox/d8ceab170e310fa30de5d807bbc88eb31f278486/Lombiq.Tests.UI/NuGetIcon.png -------------------------------------------------------------------------------- /Lombiq.Tests.UI/Pages/OrchardCoreAdminPage.cs: -------------------------------------------------------------------------------- 1 | using Atata; 2 | using Lombiq.Tests.UI.Components; 3 | using System.Diagnostics.CodeAnalysis; 4 | 5 | namespace Lombiq.Tests.UI.Pages; 6 | 7 | [SuppressMessage( 8 | "Major Code Smell", 9 | "S1144:Unused private types or members should be removed", 10 | Justification = "Atata requires private setters: https://atata.io/examples/page-object-inheritance/.")] 11 | public abstract class OrchardCoreAdminPage : Page 12 | where TOwner : OrchardCoreAdminPage 13 | { 14 | public OrchardCoreAdminTopNavbar TopNavbar { get; private set; } 15 | 16 | public OrchardCoreAdminMenu AdminMenu { get; private set; } 17 | 18 | public ControlList, TOwner> AlertMessages { get; private set; } 19 | 20 | public TOwner ShouldStayOnAdminPage() => AdminMenu.Should.BePresent(); 21 | 22 | public TOwner ShouldLeaveAdminPage() => AdminMenu.Should.Not.BePresent(); 23 | 24 | protected override void OnVerify() 25 | { 26 | base.OnVerify(); 27 | ShouldStayOnAdminPage(); 28 | } 29 | 30 | public TOwner ShouldContainSuccessAlertMessage(TermMatch expectedMatch, string expectedText) => 31 | AlertMessages.Should.Contain(message => message.IsSuccess && expectedMatch.IsMatch(message.Text.Value, expectedText)); 32 | } 33 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/Pages/OrchardCoreContentItemsPage.cs: -------------------------------------------------------------------------------- 1 | using Atata; 2 | using Atata.Bootstrap; 3 | using System.Diagnostics.CodeAnalysis; 4 | 5 | namespace Lombiq.Tests.UI.Pages; 6 | 7 | // Atata convention. 8 | #pragma warning disable IDE0065 // Misplaced using directive 9 | using _ = OrchardCoreContentItemsPage; 10 | #pragma warning restore IDE0065 // Misplaced using directive 11 | 12 | [SuppressMessage( 13 | "Major Code Smell", 14 | "S1144:Unused private types or members should be removed", 15 | Justification = "Atata requires private setters: https://atata.io/examples/page-object-inheritance/.")] 16 | public class OrchardCoreContentItemsPage : OrchardCoreAdminPage<_> 17 | { 18 | [FindById("new-dropdown")] 19 | public NewItemDropdown NewDropdown { get; private set; } 20 | 21 | public Link<_> NewPageLink { get; private set; } 22 | 23 | [FindById("items-form")] 24 | public UnorderedList Items { get; private set; } 25 | 26 | public OrchardCoreNewPageItemPage CreateNewPage() => 27 | (NewPageLink.IsVisible ? NewPageLink : NewDropdown.Page) 28 | .ClickAndGo(); 29 | 30 | public sealed class NewItemDropdown : BSDropdownToggle<_> 31 | { 32 | public Link<_> Page { get; private set; } 33 | } 34 | 35 | [ControlDefinition("li[position() > 1]", ComponentTypeName = "item")] 36 | public sealed class ContentListItem : ListItem<_> 37 | { 38 | [FindByXPath("a")] 39 | public Text<_> Title { get; private set; } 40 | 41 | [FindByClass] 42 | public Link<_> View { get; private set; } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/Pages/OrchardCoreDashboardPage.cs: -------------------------------------------------------------------------------- 1 | namespace Lombiq.Tests.UI.Pages; 2 | 3 | // Atata convention. 4 | #pragma warning disable IDE0065 // Misplaced using directive 5 | using _ = OrchardCoreDashboardPage; 6 | #pragma warning restore IDE0065 // Misplaced using directive 7 | 8 | public sealed class OrchardCoreDashboardPage : OrchardCoreAdminPage<_> 9 | { 10 | } 11 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/Pages/OrchardCoreNewPageItemPage.cs: -------------------------------------------------------------------------------- 1 | using Atata; 2 | 3 | namespace Lombiq.Tests.UI.Pages; 4 | 5 | // Atata convention. 6 | #pragma warning disable IDE0065 // Misplaced using directive 7 | using _ = OrchardCoreNewPageItemPage; 8 | #pragma warning restore IDE0065 // Misplaced using directive 9 | 10 | public class OrchardCoreNewPageItemPage : OrchardCoreAdminPage<_> 11 | { 12 | [FindByName("TitlePart.Title")] 13 | public TextInput<_> Title { get; private set; } 14 | 15 | [FindByName("submit.Publish")] 16 | public Button Publish { get; private set; } 17 | } 18 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/PermitNoTitleIframes.htmlvalidate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "./default.htmlvalidate.json" 4 | ], 5 | 6 | "elements": [ 7 | "html5", 8 | { 9 | "iframe": { 10 | "attributes": { 11 | "title": { 12 | "required": false 13 | } 14 | } 15 | } 16 | } 17 | ], 18 | 19 | "root": true 20 | } 21 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/SampleUploadFiles/document.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lombiq/UI-Testing-Toolbox/d8ceab170e310fa30de5d807bbc88eb31f278486/Lombiq.Tests.UI/SampleUploadFiles/document.pdf -------------------------------------------------------------------------------- /Lombiq.Tests.UI/SampleUploadFiles/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lombiq/UI-Testing-Toolbox/d8ceab170e310fa30de5d807bbc88eb31f278486/Lombiq.Tests.UI/SampleUploadFiles/image.png -------------------------------------------------------------------------------- /Lombiq.Tests.UI/SampleUploadFiles/uploadingtestfiledocx.docx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lombiq/UI-Testing-Toolbox/d8ceab170e310fa30de5d807bbc88eb31f278486/Lombiq.Tests.UI/SampleUploadFiles/uploadingtestfiledocx.docx -------------------------------------------------------------------------------- /Lombiq.Tests.UI/SampleUploadFiles/uploadingtestfilexlsx.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lombiq/UI-Testing-Toolbox/d8ceab170e310fa30de5d807bbc88eb31f278486/Lombiq.Tests.UI/SampleUploadFiles/uploadingtestfilexlsx.xlsx -------------------------------------------------------------------------------- /Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkPlanFragments/DisplayActiveScanRuleRuntimesScript.yml: -------------------------------------------------------------------------------- 1 | - parameters: 2 | action: add 3 | type: standalone 4 | engine: 'ECMAScript : Graal.js' 5 | name: displayRuleRuntimes 6 | target: '' 7 | # It's easier to copy this as-is between this file and the ZAP GUI. 8 | # yamllint disable 9 | inline: "var extAscan = control.getExtensionLoader().getExtension(\n org.zaproxy.zap.extension.ascan.ExtensionActiveScan.NAME);\n\ 10 | \nif (extAscan != null) {\n var lastScan = extAscan.getLastScan();\n if (lastScan\ 11 | \ != null) {\n var hps = lastScan.getHostProcesses().toArray();\n for\ 12 | \ (var i=0; i < hps.length; i++) {\n var hp = hps[i];\n var plugins\ 13 | \ = hp.getCompleted().toArray();\n for (var j=0; j < plugins.length; j++)\ 14 | \ {\n var plugin = plugins[j];\n var timeTaken = plugin.getTimeFinished().getTime()\n\ 15 | \ - plugin.getTimeStarted().getTime();\n print(plugin.getName()\ 16 | \ + \"\\t\" + timeTaken);\n }\n }\n }\n}" 17 | # yamllint enable 18 | name: script 19 | type: script 20 | - parameters: 21 | action: run 22 | type: standalone 23 | engine: '' 24 | name: displayRuleRuntimes 25 | target: '' 26 | inline: '' 27 | name: script 28 | type: script 29 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkPlanFragments/RequestorJob.yml: -------------------------------------------------------------------------------- 1 | parameters: 2 | user: '' 3 | requests: 4 | - url: '' 5 | name: '' 6 | method: '' 7 | httpVersion: '' 8 | headers: [] 9 | data: '' 10 | responseCode: 200 11 | name: requestor 12 | type: requestor 13 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkPlanFragments/SpiderAjaxJob.yml: -------------------------------------------------------------------------------- 1 | parameters: 2 | maxDuration: 60 3 | maxCrawlDepth: 10 4 | numberOfBrowsers: 64 5 | inScopeOnly: true 6 | runOnlyIfModern: true 7 | name: spiderAjax 8 | type: spiderAjax 9 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkPlanFragmentsPaths.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | 3 | namespace Lombiq.Tests.UI.SecurityScanning; 4 | 5 | public static class AutomationFrameworkPlanFragmentsPaths 6 | { 7 | private static readonly string AutomationFrameworkPlanFragmentsPath = 8 | Path.Combine("SecurityScanning", "AutomationFrameworkPlanFragments"); 9 | 10 | public static readonly string DisplayActiveScanRuleRuntimesScriptPath = 11 | Path.Combine(AutomationFrameworkPlanFragmentsPath, "DisplayActiveScanRuleRuntimesScript.yml"); 12 | public static readonly string RequestorJobPath = Path.Combine(AutomationFrameworkPlanFragmentsPath, "RequestorJob.yml"); 13 | public static readonly string SpiderAjaxJobPath = Path.Combine(AutomationFrameworkPlanFragmentsPath, "SpiderAjaxJob.yml"); 14 | } 15 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkPlanPaths.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | 3 | namespace Lombiq.Tests.UI.SecurityScanning; 4 | 5 | public static class AutomationFrameworkPlanPaths 6 | { 7 | private static readonly string AutomationFrameworkPlansPath = Path.Combine("SecurityScanning", "AutomationFrameworkPlans"); 8 | 9 | public static readonly string BaselinePlanPath = Path.Combine(AutomationFrameworkPlansPath, "Baseline.yml"); 10 | public static readonly string FullScanPlanPath = Path.Combine(AutomationFrameworkPlansPath, "FullScan.yml"); 11 | public static readonly string GraphQLPlanPath = Path.Combine(AutomationFrameworkPlansPath, "GraphQL.yml"); 12 | public static readonly string OpenAPIPlanPath = Path.Combine(AutomationFrameworkPlansPath, "OpenAPI.yml"); 13 | } 14 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/SecurityScanning/SecurityScanResult.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis.Sarif; 2 | 3 | namespace Lombiq.Tests.UI.SecurityScanning; 4 | 5 | public class SecurityScanResult 6 | { 7 | public string ReportsDirectoryPath { get; } 8 | public string ZapLogPath { get; set; } 9 | public SarifLog SarifLog { get; } 10 | 11 | public SecurityScanResult(string reportsDirectoryPath, string zapLogPath, SarifLog sarifLog) 12 | { 13 | ReportsDirectoryPath = reportsDirectoryPath; 14 | ZapLogPath = zapLogPath; 15 | SarifLog = sarifLog; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/SecurityScanning/SecurityScanningAssertionException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Lombiq.Tests.UI.Exceptions; 4 | 5 | public class SecurityScanningAssertionException : Exception 6 | { 7 | public SecurityScanningAssertionException(Exception innerException) 8 | : base( 9 | "Asserting the security scan result failed. Check the security scan report in the failure dump for details.", 10 | innerException) 11 | { 12 | } 13 | 14 | public SecurityScanningAssertionException() 15 | { 16 | } 17 | 18 | public SecurityScanningAssertionException(string message) 19 | : base(message) 20 | { 21 | } 22 | 23 | public SecurityScanningAssertionException(string message, Exception innerException) 24 | : base(message, innerException) 25 | { 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/SecurityScanning/SecurityScanningException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Lombiq.Tests.UI.SecurityScanning; 4 | 5 | public class SecurityScanningException : Exception 6 | { 7 | public SecurityScanningException() 8 | { 9 | } 10 | 11 | public SecurityScanningException(string message) 12 | : base(message) 13 | { 14 | } 15 | 16 | public SecurityScanningException(string message, Exception innerException) 17 | : base(message, innerException) 18 | { 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/SecurityScanning/YamlHelper.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using YamlDotNet.RepresentationModel; 3 | 4 | namespace Lombiq.Tests.UI.SecurityScanning; 5 | 6 | public static class YamlHelper 7 | { 8 | public static YamlDocument LoadDocument(string yamlFilePath) 9 | { 10 | using var streamReader = new StreamReader(yamlFilePath); 11 | var yamlStream = new YamlStream(); 12 | yamlStream.Load(streamReader); 13 | return yamlStream.Documents[0]; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/SecurityScanning/YamlNodeExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using YamlDotNet.RepresentationModel; 3 | 4 | namespace Lombiq.Tests.UI.SecurityScanning; 5 | 6 | public static class YamlNodeExtensions 7 | { 8 | /// 9 | /// Sets to the given value. 10 | /// 11 | /// The value to set to. 12 | /// 13 | /// Thrown if the supplied YamlNode can't be cast to YamlScalarNode and thus can't have a value set. 14 | /// 15 | public static void SetValue(this YamlNode yamlNode, string value) 16 | { 17 | if (yamlNode is not YamlScalarNode) 18 | { 19 | throw new ArgumentException( 20 | "The supplied YamlNode can't be cast to YamlScalarNode and thus can't have a value set.", nameof(yamlNode)); 21 | } 22 | 23 | ((YamlScalarNode)yamlNode).Value = value; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/SecurityScanning/ZapEnums.cs: -------------------------------------------------------------------------------- 1 | namespace Lombiq.Tests.UI.SecurityScanning; 2 | 3 | /// 4 | /// Controls how likely ZAP is to report potential vulnerabilities. See the official docs. 6 | /// 7 | public enum ScanRuleThreshold 8 | { 9 | Off, 10 | Default, 11 | Low, 12 | Medium, 13 | High, 14 | } 15 | 16 | /// 17 | /// Controls the number of attacks that ZAP will perform. See the official docs. 19 | /// 20 | public enum ScanRuleStrength 21 | { 22 | Default, 23 | Low, 24 | Medium, 25 | High, 26 | Insane, 27 | } 28 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/Services/AtataScope.cs: -------------------------------------------------------------------------------- 1 | using Atata; 2 | using OpenQA.Selenium; 3 | using System; 4 | 5 | namespace Lombiq.Tests.UI.Services; 6 | 7 | /// 8 | /// A representation of a scope wrapping an Atata-driven UI test. is created in the beginning 9 | /// of a UI test, provides services for the test, and is disposed when the test finishes. 10 | /// 11 | public sealed class AtataScope : IDisposable 12 | { 13 | private Uri _baseUri; 14 | 15 | public AtataContext AtataContext { get; } 16 | 17 | public IWebDriver Driver 18 | { 19 | get 20 | { 21 | var driver = AtataContext.Driver; 22 | IsBrowserRunning = driver != null; 23 | return driver; 24 | } 25 | } 26 | 27 | public bool IsBrowserRunning { get; private set; } 28 | 29 | public Uri BaseUri 30 | { 31 | get => _baseUri; 32 | set 33 | { 34 | ArgumentNullException.ThrowIfNull(value); 35 | _baseUri = value; 36 | AtataContext.BaseUrl = value.ToString(); 37 | } 38 | } 39 | 40 | public AtataScope(AtataContext atataContext, Uri baseUri) 41 | { 42 | AtataContext = atataContext; 43 | _baseUri = baseUri; 44 | } 45 | 46 | /// 47 | /// Sets to the value of . 48 | /// 49 | public void SetContextAsCurrent() => AtataContext.Current = AtataContext; 50 | 51 | public void Dispose() => AtataContext.Dispose(); 52 | } 53 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/Services/GitHub/GitHubActionsOutputConfiguration.cs: -------------------------------------------------------------------------------- 1 | namespace Lombiq.Tests.UI.Services.GitHub; 2 | 3 | public class GitHubActionsOutputConfiguration 4 | { 5 | /// 6 | /// Gets or sets a value indicating whether each tests' output should be wrapped into their own groups () 8 | /// in the GitHub Actions output. This only takes effect when tests are executed from a GitHub Actions workflow. 9 | /// 10 | public bool EnablePerTestOutputGrouping { get; set; } = true; 11 | 12 | /// 13 | /// Gets or sets a value indicating whether errors (exceptions) surfacing from tests should be written as errors () 15 | /// in the GitHub Actions output. This only takes effect when tests are executed from a GitHub Actions workflow. 16 | /// 17 | public bool EnableErrorAnnotations { get; set; } = true; 18 | 19 | /// 20 | /// Gets or sets a value indicating whether test retries should be surfaced as warning annotations () 22 | /// in the GitHub Actions output. This only takes effect when tests are executed from a GitHub Actions workflow. 23 | /// 24 | public bool EnableTestRetryWarningAnnotations { get; set; } = true; 25 | } 26 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/Services/GitHub/GitHubHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Lombiq.Tests.UI.Services.GitHub; 4 | 5 | public static class GitHubHelper 6 | { 7 | private static readonly Lazy _isGitHubEnvironmentLazy = new(() => 8 | !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("GITHUB_ENV"))); 9 | 10 | public static bool IsGitHubEnvironment => _isGitHubEnvironmentLazy.Value; 11 | } 12 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/Services/IApplicationLog.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Threading.Tasks; 5 | 6 | namespace Lombiq.Tests.UI.Services; 7 | 8 | /// 9 | /// An abstraction over a log, be it in the form of a file or something else. 10 | /// 11 | public interface IApplicationLog 12 | { 13 | /// 14 | /// Gets the name of the log, such as the file name. 15 | /// 16 | string Name { get; } 17 | 18 | /// 19 | /// Gets the number of log entries in the log. 20 | /// 21 | int EntryCount { get; } 22 | 23 | /// 24 | /// Returns the content of the log, in case of log files reads the file contents. 25 | /// 26 | /// The contents. 27 | Task> GetEntriesAsync(); 28 | 29 | /// 30 | /// Removes the log if possible. 31 | /// 32 | Task RemoveAsync(); 33 | } 34 | 35 | /// 36 | /// An abstraction over a log entries. 37 | /// 38 | public interface IApplicationLogEntry 39 | { 40 | /// 41 | /// Gets the level of the log entry, like or . 42 | /// 43 | LogLevel Level { get; } 44 | 45 | /// 46 | /// Gets the ID that uniquely identifies the log entry. 47 | /// 48 | EventId Id { get; } 49 | 50 | /// 51 | /// Gets the exception associated with the log entry, if any. 52 | /// 53 | Exception Exception { get; } 54 | 55 | /// 56 | /// Gets the human-readable formatted log message. 57 | /// 58 | string Message { get; } 59 | 60 | /// 61 | /// Gets the category of the log entry. This is the type parameter of . 62 | /// 63 | string Category { get; } 64 | 65 | /// 66 | /// Gets the timestamp of when the log entry was created. 67 | /// 68 | DateTimeOffset Timestamp { get; } 69 | } 70 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/Services/ITestOutputHelperDecorator.cs: -------------------------------------------------------------------------------- 1 | using Xunit; 2 | 3 | namespace Lombiq.Tests.UI.Services; 4 | 5 | /// 6 | /// Defines an that decorates another . 7 | /// 8 | public interface ITestOutputHelperDecorator : ITestOutputHelper 9 | { 10 | /// 11 | /// Gets the decorated instance. 12 | /// 13 | ITestOutputHelper Decorated { get; } 14 | } 15 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/Services/IWebApplicationInstance.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | 6 | namespace Lombiq.Tests.UI.Services; 7 | 8 | /// 9 | /// A web application instance, like an Orchard Core app executing via dotnet. 10 | /// 11 | public interface IWebApplicationInstance : IAsyncDisposable 12 | { 13 | /// 14 | /// Gets the created by the server associated with this 15 | /// . 16 | /// 17 | IServiceProvider Services { get; } 18 | 19 | /// 20 | /// Launches the web application. 21 | /// 22 | /// The starting URL of the web app, such as the home page. 23 | Task StartUpAsync(); 24 | 25 | /// 26 | /// Stops running the application without disposing it. It can be restarted with . 27 | /// 28 | Task PauseAsync(); 29 | 30 | /// 31 | /// Starts the application back up again after it was stopped with . 32 | /// 33 | Task ResumeAsync(); 34 | 35 | /// 36 | /// Pauses (see ) and saves the state of the application. It can be restarted with . 38 | /// 39 | /// The save location. 40 | Task TakeSnapshotAsync(string snapshotDirectoryPath); 41 | 42 | /// 43 | /// Reads all the application logs. 44 | /// 45 | /// The collection of log names and their contents. 46 | Task> GetLogsAsync(CancellationToken cancellationToken = default); 47 | } 48 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/Services/OrchardCoreHosting/FakeStore.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Collections.Generic; 4 | using System.Threading.Tasks; 5 | using YesSql; 6 | using YesSql.Indexes; 7 | 8 | namespace Lombiq.Tests.UI.Services.OrchardCoreHosting; 9 | 10 | public sealed class FakeStore : IStore 11 | { 12 | private readonly ConcurrentBag _createdSessions = []; 13 | private readonly IStore _store; 14 | 15 | public FakeStore(IStore store) => _store = store; 16 | 17 | public IConfiguration Configuration => _store.Configuration; 18 | 19 | public ITypeService TypeNames => _store.TypeNames; 20 | 21 | public ISession CreateSession(bool withTracking = true) 22 | { 23 | var session = _store.CreateSession(); 24 | _createdSessions.Add(session); 25 | 26 | return session; 27 | } 28 | 29 | public IEnumerable Describe(Type target, string collection = null) => 30 | _store.Describe(target, collection); 31 | 32 | public void Dispose() 33 | { 34 | foreach (var session in _createdSessions) 35 | { 36 | try 37 | { 38 | session.Dispose(); 39 | } 40 | catch 41 | #pragma warning disable S108 // Nested blocks of code should not be left empty 42 | { 43 | } 44 | #pragma warning restore S108 // Nested blocks of code should not be left empty 45 | } 46 | 47 | _createdSessions.Clear(); 48 | 49 | _store?.Dispose(); 50 | } 51 | 52 | public Task InitializeAsync() => _store.InitializeAsync(); 53 | 54 | public Task InitializeCollectionAsync(string collection) => _store.InitializeCollectionAsync(collection); 55 | 56 | public IStore RegisterIndexes(IEnumerable indexProviders, string collection = null) => 57 | _store.RegisterIndexes(indexProviders, collection); 58 | } 59 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/Services/OrchardCoreHosting/FakeViewCompilerProvider.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc.Razor.Compilation; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using System; 4 | using System.Linq; 5 | 6 | namespace Lombiq.Tests.UI.Services.OrchardCoreHosting; 7 | 8 | public class FakeViewCompilerProvider : IViewCompilerProvider 9 | { 10 | private readonly IServiceProvider _services; 11 | 12 | public FakeViewCompilerProvider(IServiceProvider services) => _services = services; 13 | 14 | public IViewCompiler GetCompiler() => 15 | _services 16 | .GetServices() 17 | .FirstOrDefault() 18 | .GetCompiler(); 19 | } 20 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/Services/RemoteInstance.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | 7 | namespace Lombiq.Tests.UI.Services; 8 | 9 | public sealed class RemoteInstance : IWebApplicationInstance 10 | { 11 | public IServiceProvider Services => throw new NotSupportedException(); 12 | 13 | private readonly Uri _baseUri; 14 | 15 | public RemoteInstance(Uri baseUri) => _baseUri = baseUri; 16 | 17 | public Task StartUpAsync() => Task.FromResult(_baseUri); 18 | 19 | public Task> GetLogsAsync(CancellationToken cancellationToken = default) => 20 | Task.FromResult(Enumerable.Empty()); 21 | public TService GetRequiredService() => throw new NotSupportedException(); 22 | public Task PauseAsync() => throw new NotSupportedException(); 23 | public Task ResumeAsync() => throw new NotSupportedException(); 24 | public Task TakeSnapshotAsync(string snapshotDirectoryPath) => throw new NotSupportedException(); 25 | 26 | public ValueTask DisposeAsync() => ValueTask.CompletedTask; 27 | } 28 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/Services/ShortcutsConfiguration.cs: -------------------------------------------------------------------------------- 1 | using Lombiq.Tests.UI.Extensions; 2 | 3 | namespace Lombiq.Tests.UI.Services; 4 | 5 | public class ShortcutsConfiguration 6 | { 7 | /// 8 | /// Gets or sets a value indicating whether to inject a comment into the site's HTML output with basic information 9 | /// about the Orchard Core application's executable. Also see for a shortcut for retrieving the same data 11 | /// on-demand. 12 | /// 13 | public bool InjectApplicationInfo { get; set; } 14 | } 15 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/Services/TestOutputLogConsumer.cs: -------------------------------------------------------------------------------- 1 | using Atata; 2 | using Xunit; 3 | 4 | namespace Lombiq.Tests.UI.Services; 5 | 6 | public class TestOutputLogConsumer : TextOutputLogConsumer 7 | { 8 | private readonly ITestOutputHelper _testOutputHelper; 9 | 10 | public TestOutputLogConsumer(ITestOutputHelper testOutputHelper) => _testOutputHelper = testOutputHelper; 11 | 12 | protected override void Write(string completeMessage) => _testOutputHelper.WriteLine(completeMessage); 13 | } 14 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/Services/UITestExecutionEvents.cs: -------------------------------------------------------------------------------- 1 | using OpenQA.Selenium; 2 | using System; 3 | using System.Threading.Tasks; 4 | 5 | namespace Lombiq.Tests.UI.Services; 6 | 7 | public delegate Task NavigationEventHandler(UITestContext context, Uri targetUri); 8 | 9 | public delegate Task ClickEventHandler(UITestContext context, IWebElement targeElement); 10 | 11 | public delegate Task PageChangeEventHandler(UITestContext context); 12 | 13 | public class UITestExecutionEvents 14 | { 15 | /// 16 | /// Gets or sets the event raised before an explicit navigation to an URL happens. 17 | /// 18 | public NavigationEventHandler BeforeNavigation { get; set; } 19 | 20 | /// 21 | /// Gets or sets the event raised after an explicit navigation to an URL happens. 22 | /// 23 | public NavigationEventHandler AfterNavigation { get; set; } 24 | 25 | /// 26 | /// Gets or sets the event raised before clicking an element. 27 | /// 28 | public ClickEventHandler BeforeClick { get; set; } 29 | 30 | /// 31 | /// Gets or sets the event raised after clicking an element. 32 | /// 33 | public ClickEventHandler AfterClick { get; set; } 34 | 35 | /// 36 | /// Gets or sets the event raised after the current page changes. 37 | /// 38 | public PageChangeEventHandler AfterPageChange { get; set; } 39 | 40 | internal UITestExecutionEvents() 41 | { 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/Services/UITestExecutorTestDumpConfiguration.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Lombiq.Tests.UI.Services; 4 | 5 | public class UITestExecutorTestDumpConfiguration 6 | { 7 | /// 8 | /// Gets or sets a value indicating whether the subfolder of each test's dumps will use a shortened name, only 9 | /// containing the name of the test method suffixed with the test name's hash to make it unique, without the name of 10 | /// the test class and its namespace. This is to overcome the 260 character path length limitations on Windows. 11 | /// Defaults to on Windows. 12 | /// 13 | public bool UseShortNames { get; set; } = OperatingSystem.IsWindows(); 14 | 15 | public string DumpsDirectoryPath { get; set; } = "TestDumps"; 16 | public bool CreateTestDump { get; set; } = true; 17 | public bool CaptureAppSnapshot { get; set; } = true; 18 | public bool CaptureScreenshots { get; set; } = true; 19 | public bool CaptureDownloads { get; set; } = true; 20 | public bool CaptureHtmlSource { get; set; } = true; 21 | public bool CaptureResponseLog { get; set; } = true; 22 | public bool CaptureBrowserLog { get; set; } = true; 23 | } 24 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/UITestBase.cs: -------------------------------------------------------------------------------- 1 | using Lombiq.Tests.UI.Models; 2 | using Lombiq.Tests.UI.Services; 3 | using Lombiq.Tests.UI.Services.GitHub; 4 | using System; 5 | using System.Threading.Tasks; 6 | using Xunit; 7 | 8 | namespace Lombiq.Tests.UI; 9 | 10 | public abstract class UITestBase 11 | { 12 | protected ITestOutputHelper _testOutputHelper; 13 | 14 | static UITestBase() => AtataFactory.SetupShellCliCommandFactory(); 15 | 16 | protected UITestBase(ITestOutputHelper testOutputHelper) => _testOutputHelper = testOutputHelper; 17 | 18 | protected async Task ExecuteOrchardCoreTestAsync( 19 | WebApplicationInstanceFactory webApplicationInstanceFactory, 20 | UITestManifest testManifest, 21 | OrchardCoreUITestExecutorConfiguration configuration) 22 | { 23 | var originalTestOutputHelper = _testOutputHelper; 24 | 25 | Action afterTest = null; 26 | if (configuration.ExtendGitHubActionsOutput && 27 | configuration.GitHubActionsOutputConfiguration.EnablePerTestOutputGrouping && 28 | GitHubHelper.IsGitHubEnvironment) 29 | { 30 | (_testOutputHelper, afterTest) = 31 | GitHubActionsGroupingTestOutputHelper.CreateDecorator(_testOutputHelper, testManifest); 32 | configuration.TestOutputHelper = _testOutputHelper; 33 | } 34 | 35 | // Used by many utilities to turn off ANSI escape sequences. 36 | Environment.SetEnvironmentVariable("NO_COLOR", "true"); 37 | 38 | try 39 | { 40 | await UITestExecutor.ExecuteOrchardCoreTestAsync( 41 | webApplicationInstanceFactory, 42 | testManifest, 43 | configuration); 44 | } 45 | finally 46 | { 47 | _testOutputHelper = originalTestOutputHelper; 48 | afterTest?.Invoke(); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/default.htmlvalidate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "html-validate:recommended" 4 | ], 5 | 6 | "rules": { 7 | "attribute-boolean-style": "off", 8 | "form-dup-name": "off", 9 | "no-inline-style": "off", 10 | "no-trailing-whitespace": "off", 11 | "wcag/h30": "off", 12 | "wcag/h32": "off", 13 | "wcag/h36": "off", 14 | "wcag/h37": "off", 15 | "wcag/h67": "off", 16 | "wcag/h71": "off" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "dependencies": { 4 | "gremlins.js": "2.2.0" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: '6.0' 2 | 3 | settings: 4 | autoInstallPeers: true 5 | excludeLinksFromLockfile: false 6 | 7 | dependencies: 8 | gremlins.js: 9 | specifier: 2.2.0 10 | version: 2.2.0 11 | 12 | packages: 13 | 14 | /chance@1.1.8: 15 | resolution: {integrity: sha512-v7fi5Hj2VbR6dJEGRWLmJBA83LJMS47pkAbmROFxHWd9qmE1esHRZW8Clf1Fhzr3rjxnNZVCjOEv/ivFxeIMtg==} 16 | dev: false 17 | 18 | /core-js@3.20.2: 19 | resolution: {integrity: sha512-nuqhq11DcOAbFBV4zCbKeGbKQsUDRqTX0oqx7AttUBuqe3h20ixsE039QHelbL6P4h+9kytVqyEtyZ6gsiwEYw==} 20 | requiresBuild: true 21 | dev: false 22 | 23 | /gremlins.js@2.2.0: 24 | resolution: {integrity: sha512-zVe4+WuCwTheg1OzBOdtiy7nZj52lWyWjCPMw/78wI1uAJdGlc97hVGYsZg0v75Hm+E91y5D+yG4U0lk7j/6xg==} 25 | dependencies: 26 | chance: 1.1.8 27 | core-js: 3.20.2 28 | dev: false 29 | -------------------------------------------------------------------------------- /Lombiq.Tests.UI/xunit.runner.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://xunit.net/schema/current/xunit.runner.schema.json", 3 | "parallelizeAssembly": false, 4 | "parallelizeTestCollections": true, 5 | "maxParallelThreads": 3 6 | } 7 | -------------------------------------------------------------------------------- /NuGet.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | --------------------------------------------------------------------------------
53 | @T["If you see this, your code has called the context.SwitchToInteractiveAsync() method."] 54 | @T["This stops the test evaluation and waits until you click the button below."] 55 |
context.SwitchToInteractiveAsync()