Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OSOE-365: Support for running tests without a browser #396

Merged
merged 26 commits into from
Aug 5, 2024
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
432b1a4
Disable the default search engine selector splash screen
Piedone Aug 2, 2024
77cd46e
Adding the ability to launch tests without a browser
Piedone Aug 2, 2024
c7127be
Revert "Adding the ability to launch tests without a browser"
Piedone Aug 2, 2024
773f0a0
Making browser initialization on-demand
Piedone Aug 2, 2024
3da1e43
Temporarily disabling WebDriver-using code for security scans
Piedone Aug 2, 2024
d496ecf
Re-adding Browser.None and making it possible to use a browser for se…
Piedone Aug 3, 2024
c61908a
Security scanning tests now don't use a browser
Piedone Aug 3, 2024
dc2f785
Renaming IsBrowserUsed
Piedone Aug 3, 2024
e4c739c
Not using a browser TestRunTimeoutShouldThrowAsync
Piedone Aug 3, 2024
1730abd
Fixing that UsingScopeAsync() didn't work with no-browser tests
Piedone Aug 3, 2024
71947fe
Adding more ExecuteTestAfterBrowserSetupWithoutBrowserAsync overloads
Piedone Aug 3, 2024
94c1ff2
Merge remote-tracking branch 'origin/dev' into issue/OSOE-365
Piedone Aug 3, 2024
2e2251f
IsBrowserRunning now can be flipped back too
Piedone Aug 3, 2024
cecd400
Docs
Piedone Aug 3, 2024
0231bdf
Fixing failure dump generation when not running a browser
Piedone Aug 3, 2024
1b7deaa
Disabling Application Error Disclosure security scan alerts for the S…
Piedone Aug 3, 2024
566aaaa
YAML code styling
Piedone Aug 3, 2024
6146ba9
Refactoring
Piedone Aug 3, 2024
5449a79
Fixing screenshot dump generation
Piedone Aug 3, 2024
1e72359
Docs
Piedone Aug 3, 2024
6b212a0
Code styling
Piedone Aug 3, 2024
007c5be
Fixing SecurityScanningTests sample
Piedone Aug 3, 2024
a902fe7
Code styling
Piedone Aug 4, 2024
8ce4b54
Surfacing the Uri that the setup operation returns as the StartUri
Piedone Aug 4, 2024
adb45ec
Fixing TestStartUri handling
Piedone Aug 4, 2024
cfb8312
Merge remote-tracking branch 'origin/dev' into issue/OSOE-365
Piedone Aug 5, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,10 @@ public SecurityScanningTests(ITestOutputHelper testOutputHelper)
// will fail the scan, but don't worry! You'll get a nice report about the findings in the failure dump.
[Fact]
public Task BasicSecurityScanShouldPass() =>
ExecuteTestAfterSetupAsync(
// Note how we use a method that doesn't launch a browser. Security scanning happens fully in ZAP, and doesn't
// use the browser launched by the UI Testing Toolbox. However, you can use the browser to prepare the app for
// security scanning if necessary.
ExecuteTestAfterBrowserSetupWithoutBrowserAsync(
context => context.RunAndAssertBaselineSecurityScanAsync(),
// You should configure the assertion that checks the app logs to accept some common cases that only should
// appear during security scanning. If you launch a full scan, this is automatically configured by the
Expand All @@ -74,7 +77,7 @@ public Task BasicSecurityScanShouldPass() =>
// are only present to illustrate the type of adjustments you may want for your own site.
[Fact]
public Task SecurityScanWithCustomConfigurationShouldPass() =>
ExecuteTestAfterSetupAsync(
ExecuteTestAfterBrowserSetupWithoutBrowserAsync(
context => context.RunAndAssertBaselineSecurityScanAsync(
configuration => configuration
////.UseAjaxSpider() // This is quite slow so just showing you here but not running it.
Expand Down Expand Up @@ -105,7 +108,7 @@ public Task SecurityScanWithCustomConfigurationShouldPass() =>
// customize them if something you need is not surfaced as configuration.
[Fact]
public Task SecurityScanWithCustomAutomationFrameworkPlanShouldPass() =>
ExecuteTestAfterSetupAsync(
ExecuteTestAfterBrowserSetupWithoutBrowserAsync(
context => context.RunAndAssertSecurityScanAsync(
"Tests/CustomZapAutomationFrameworkPlan.yml",
configuration => configuration
Expand Down
2 changes: 1 addition & 1 deletion Lombiq.Tests.UI.Tests.UI/TestCases/TimeoutTestCases.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ public static class TimeoutTestCases
{
public static Task TestRunTimeoutShouldThrowAsync(
ExecuteTestAfterSetupAsync executeTestAfterSetupAsync,
Browser browser = default) =>
Browser browser = Browser.None) =>
Should.ThrowAsync(
async () => await executeTestAfterSetupAsync(
context => Task.Delay(TimeSpan.FromSeconds(1)),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,15 @@ public static async Task UsingScopeAsync(

try
{
// If there's no Default shell settings then the shell host hasn't been initialized yet. This can happen if
// no request hit the app yet.
var defaultShellSettingExist = shellHost.TryGetSettings("Default", out var _);

if (!defaultShellSettingExist)
{
await shellHost.InitializeAsync();
}

// Injecting a fake HttpContext is required for many things, but it needs to happen before UsingAsync()
// below to avoid NullReferenceExceptions in
// OrchardCore.Recipes.Services.RecipeEnvironmentFeatureProvider.PopulateEnvironmentAsync. Migrations
Expand Down
23 changes: 13 additions & 10 deletions Lombiq.Tests.UI/Extensions/VerificationUITestContextExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public static async Task AssertLogsAsync(this UITestContext context)
var configuration = context.Configuration;
var testOutputHelper = configuration.TestOutputHelper;

await context.UpdateHistoricBrowserLogAsync();
if (context.IsBrowserRunning) await context.UpdateHistoricBrowserLogAsync();

try
{
Expand All @@ -34,16 +34,19 @@ public static async Task AssertLogsAsync(this UITestContext context)
throw;
}

try
{
configuration.AssertBrowserLog?.Invoke(context.HistoricBrowserLog);
}
catch (Exception)
if (context.IsBrowserRunning)
{
testOutputHelper.WriteLine("Browser logs: " + Environment.NewLine);
testOutputHelper.WriteLine(context.HistoricBrowserLog.ToFormattedString());

throw;
try
{
configuration.AssertBrowserLog?.Invoke(context.HistoricBrowserLog);
}
catch (Exception)
{
testOutputHelper.WriteLine("Browser logs: " + Environment.NewLine);
testOutputHelper.WriteLine(context.HistoricBrowserLog.ToFormattedString());

throw;
}
}
}
}
47 changes: 47 additions & 0 deletions Lombiq.Tests.UI/OrchardCoreUITestBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,53 @@ protected virtual Task ExecuteMultiSizeTestAfterSetupAsync(
browser,
changeConfigurationAsync);

protected virtual Task ExecuteTestAfterBrowserSetupWithoutBrowserAsync(
Func<UITestContext, Task> testAsync,
Action<OrchardCoreUITestExecutorConfiguration> changeConfiguration = null) =>
ExecuteTestAfterBrowserSetupWithoutBrowserAsync(testAsync, default, changeConfiguration);

protected Task ExecuteTestAfterBrowserSetupWithoutBrowserAsync(
Func<UITestContext, Task> testAsync,
Func<OrchardCoreUITestExecutorConfiguration, Task> changeConfigurationAsync) =>
ExecuteTestAfterBrowserSetupWithoutBrowserAsync(testAsync, default, changeConfigurationAsync);

protected virtual Task ExecuteTestAfterBrowserSetupWithoutBrowserAsync(
Func<UITestContext, Task> testAsync,
Browser browser,
Action<OrchardCoreUITestExecutorConfiguration> changeConfiguration = null) =>
ExecuteTestAfterBrowserSetupWithoutBrowserAsync(testAsync, changeConfiguration.AsCompletedTask());

protected Task ExecuteTestAfterBrowserSetupWithoutBrowserAsync(
Func<UITestContext, Task> testAsync,
Browser browser,
Func<OrchardCoreUITestExecutorConfiguration, Task> changeConfigurationAsync) =>
ExecuteTestAfterSetupWithoutBrowserAsync(testAsync, async configuration =>
{
configuration.SetupConfiguration.BeforeSetup = configuration =>
{
configuration.BrowserConfiguration.Browser = browser;
return Task.CompletedTask;
};

configuration.SetupConfiguration.AfterSetup = configuration =>
{
configuration.BrowserConfiguration.Browser = Browser.None;
return Task.CompletedTask;
};

if (changeConfigurationAsync != null) await changeConfigurationAsync(configuration);
});

protected virtual Task ExecuteTestAfterSetupWithoutBrowserAsync(
Func<UITestContext, Task> testAsync,
Action<OrchardCoreUITestExecutorConfiguration> changeConfiguration = null) =>
ExecuteTestAfterSetupWithoutBrowserAsync(testAsync, changeConfiguration.AsCompletedTask());

protected Task ExecuteTestAfterSetupWithoutBrowserAsync(
Func<UITestContext, Task> testAsync,
Func<OrchardCoreUITestExecutorConfiguration, Task> changeConfigurationAsync) =>
ExecuteTestAfterSetupAsync(testAsync, Browser.None, changeConfigurationAsync);

protected virtual Task ExecuteTestAfterSetupAsync(
Action<UITestContext> test,
Action<OrchardCoreUITestExecutorConfiguration> changeConfiguration = null) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ public static Task<SecurityScanResult> RunSecurityScanAsync(
Action<SecurityScanConfiguration> configure = null)
{
var configuration = new SecurityScanConfiguration()
.StartAtUri(context.GetCurrentUri());
.StartAtUri(context.IsBrowserRunning ? context.GetCurrentUri() : context.Scope.BaseUri);

// By default ignore /vendor/ or /vendors/ URLs. This is case-insensitive. We have no control over them, and
// they may contain several false positives (e.g. in font-awesome).
Expand Down
28 changes: 18 additions & 10 deletions Lombiq.Tests.UI/Services/AtataFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ public static async Task<AtataScope> StartAtataScopeAsync(
var browserConfiguration = configuration.BrowserConfiguration;

var builder = AtataContext.Configure()
.UseDriver(await CreateDriverAsync(browserConfiguration, timeoutConfiguration, testOutputHelper))
.UseBaseUrl(baseUri.ToString())
.UseCulture(browserConfiguration.AcceptLanguage.ToString())
.UseTestName(configuration.AtataConfiguration.TestName)
Expand All @@ -42,6 +41,17 @@ public static async Task<AtataScope> StartAtataScopeAsync(
.PageSnapshots.UseCdpOrPageSourceStrategy() // #spell-check-ignore-line
.UseArtifactsPathTemplate(contextId); // Necessary to prevent long paths, an issue under Windows.

if (configuration.BrowserConfiguration.Browser != Browser.None)
{
builder
.UseDriverInitializationStage(AtataContextDriverInitializationStage.OnDemand)
.UseDriver(await CreateDriverFactoryAsync(browserConfiguration, timeoutConfiguration, testOutputHelper));
}
else
{
builder.UseDriverInitializationStage(AtataContextDriverInitializationStage.None);
}

builder.LogConsumers.AddDebug();
builder.LogConsumers.Add(new TestOutputLogConsumer(testOutputHelper));

Expand All @@ -55,15 +65,11 @@ public static void SetupShellCliCommandFactory() =>
.UseCmdForWindows()
.UseForOtherOS(new BashShellCliCommandFactory("-login"));

private static async Task<IWebDriver> CreateDriverAsync(
private static async Task<Func<IWebDriver>> CreateDriverFactoryAsync(
BrowserConfiguration browserConfiguration,
TimeoutConfiguration timeoutConfiguration,
ITestOutputHelper testOutputHelper)
{
Task<T> FromAsync<T>(Func<BrowserConfiguration, TimeSpan, Task<T>> factory)
where T : IWebDriver =>
factory(browserConfiguration, timeoutConfiguration.PageLoadTimeout);

// Driver creation can fail with "Cannot start the driver service on http://localhost:56686/" exceptions if the
// machine is under load. Retrying it here so not the whole test needs to be re-run.
const int maxTryCount = 3;
Expand All @@ -79,12 +85,14 @@ Task<T> FromAsync<T>(Func<BrowserConfiguration, TimeSpan, Task<T>> factory)
{
try
{
var pageLoadTimeout = timeoutConfiguration.PageLoadTimeout;

return browserConfiguration.Browser switch
{
Browser.Chrome => await FromAsync(WebDriverFactory.CreateChromeDriverAsync),
Browser.Edge => await FromAsync(WebDriverFactory.CreateEdgeDriverAsync),
Browser.Firefox => await FromAsync(WebDriverFactory.CreateFirefoxDriverAsync),
Browser.InternetExplorer => await FromAsync(WebDriverFactory.CreateInternetExplorerDriverAsync),
Browser.Chrome => await WebDriverFactory.CreateChromeDriverAsync(browserConfiguration, pageLoadTimeout),
Browser.Edge => await WebDriverFactory.CreateEdgeDriverAsync(browserConfiguration, pageLoadTimeout),
Browser.Firefox => await WebDriverFactory.CreateFirefoxDriverAsync(browserConfiguration, pageLoadTimeout),
Browser.InternetExplorer => await WebDriverFactory.CreateInternetExplorerDriverAsync(browserConfiguration, pageLoadTimeout),
_ => throw new InvalidOperationException($"Unknown browser: {browserConfiguration.Browser}."),
};
}
Expand Down
13 changes: 12 additions & 1 deletion Lombiq.Tests.UI/Services/AtataScope.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,18 @@ public sealed class AtataScope : IDisposable
private Uri _baseUri;

public AtataContext AtataContext { get; }
public IWebDriver Driver => AtataContext.Driver;

public IWebDriver Driver
{
get
{
var driver = AtataContext.Driver;
if (driver != null) IsBrowserRunning = true;
return driver;
}
}

public bool IsBrowserRunning { get; private set; }

public Uri BaseUri
{
Expand Down
2 changes: 2 additions & 0 deletions Lombiq.Tests.UI/Services/OrchardCoreSetupConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
namespace Lombiq.Tests.UI.Services;

public delegate Task BeforeSetupHandler(OrchardCoreUITestExecutorConfiguration configuration);
public delegate Task AfterSetupHandler(OrchardCoreUITestExecutorConfiguration configuration);

/// <summary>
/// Configuration for the initial setup of an Orchard Core app.
Expand Down Expand Up @@ -35,4 +36,5 @@ public class OrchardCoreSetupConfiguration
Path.Combine(DirectoryPaths.Temp, DirectoryPaths.SetupSnapshot);

public BeforeSetupHandler BeforeSetup { get; set; }
public AfterSetupHandler AfterSetup { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ public enum Browser
Edge,
Firefox,
InternetExplorer,

/// <summary>
/// No browser will be used. Useful for testing things that don't require a browser, like API endpoints or running
/// security scans.
/// </summary>
None,
}

public class OrchardCoreUITestExecutorConfiguration
Expand Down
16 changes: 16 additions & 0 deletions Lombiq.Tests.UI/Services/UITestContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,22 @@ public class UITestContext
/// </summary>
public IWebDriver Driver => Scope.Driver;

/// <summary>
/// Gets a value indicating whether a browser is currently running for the test. <see langword="false"/> means that
/// no browser was launched. Note that since the browser is only started on demand, with the first operation
/// requiring it, a browser might not be currently running even if <see cref="IsBrowserConfigured"/> suggests it
/// should.
/// </summary>
public bool IsBrowserRunning => Scope.IsBrowserRunning;

/// <summary>
/// Gets a value indicating whether a browser is configured to be used for the test. <see langword="false"/> means
/// that no browser will be launched. Note that since the browser is only started on demand, with the first
/// operation requiring it, a browser might not be currently running even if this suggests it should. Check <see
/// cref="IsBrowserRunning"/>" to check for that.
/// </summary>
public bool IsBrowserConfigured => Configuration.BrowserConfiguration.Browser != Browser.None;

/// <summary>
/// Gets the context for the SMTP service running for the test, if it was requested.
/// </summary>
Expand Down
58 changes: 30 additions & 28 deletions Lombiq.Tests.UI/Services/UITestExecutionSession.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,35 +77,12 @@ public async Task<bool> ExecuteAsync(int retryCount, string dumpRootPath)

if (_hasSetupOperation)
{
var snapshotSubdirectory = "SQLite";
if (_configuration.UseSqlServer)
{
snapshotSubdirectory = _configuration.UseAzureBlobStorage
? "SqlServer-AzureBlob"
: "SqlServer";
}
else if (_configuration.UseAzureBlobStorage)
{
snapshotSubdirectory = "SQLite-AzureBlob";
}

snapshotSubdirectory += "-" + setupConfiguration.SetupOperation!.GetHashCode().ToTechnicalString();

_snapshotDirectoryPath = Path.Combine(setupConfiguration.SetupSnapshotDirectoryPath, snapshotSubdirectory);

_configuration.OrchardCoreConfiguration.SnapshotDirectoryPath = _snapshotDirectoryPath;

_currentSetupSnapshotManager = UITestExecutionSessionsMeta.SetupSnapshotManagers.GetOrAdd(
_snapshotDirectoryPath,
path => new SynchronizingWebApplicationSnapshotManager(path));

sarahelsaig marked this conversation as resolved.
Show resolved Hide resolved
await SetupAsync();
}

// In some cases, there is a temporary setup snapshot directory path but no setup operation. For example,
// when calling the "ExecuteTestAsync()" method without a setup operation.
else if (_setupSnapshotDirectoryContainsApp)
{
// In some cases, there is a temporary setup snapshot directory path but no setup operation. For
// example, when calling the "ExecuteTestAsync()" method without a setup operation.
_configuration.OrchardCoreConfiguration.SnapshotDirectoryPath = setupConfiguration.SetupSnapshotDirectoryPath;
}

Expand All @@ -116,7 +93,7 @@ public async Task<bool> ExecuteAsync(int retryCount, string dumpRootPath)
_context.FailureDumpContainer.Clear();
failureDumpContainer = _context.FailureDumpContainer;

_context.SetDefaultBrowserSize();
if (_context.IsBrowserConfigured) _context.SetDefaultBrowserSize();

await _testManifest.TestAsync(_context);

Expand Down Expand Up @@ -479,6 +456,28 @@ private async Task SetupAsync()
{
var setupConfiguration = _configuration.SetupConfiguration;

var snapshotSubdirectory = "SQLite";
if (_configuration.UseSqlServer)
{
snapshotSubdirectory = _configuration.UseAzureBlobStorage
? "SqlServer-AzureBlob"
: "SqlServer";
}
else if (_configuration.UseAzureBlobStorage)
{
snapshotSubdirectory = "SQLite-AzureBlob";
}

snapshotSubdirectory += "-" + setupConfiguration.SetupOperation!.GetHashCode().ToTechnicalString();

_snapshotDirectoryPath = Path.Combine(setupConfiguration.SetupSnapshotDirectoryPath, snapshotSubdirectory);

_configuration.OrchardCoreConfiguration.SnapshotDirectoryPath = _snapshotDirectoryPath;

_currentSetupSnapshotManager = UITestExecutionSessionsMeta.SetupSnapshotManagers.GetOrAdd(
_snapshotDirectoryPath,
path => new SynchronizingWebApplicationSnapshotManager(path));

try
{
_testOutputHelper.WriteLineTimestampedAndDebug("Starting waiting for the setup operation.");
Expand All @@ -505,11 +504,14 @@ private async Task SetupAsync()
SetupSqlServerSnapshot();
SetupAzureBlobStorageSnapshot();

_context.SetDefaultBrowserSize();
if (_context.IsBrowserConfigured) _context.SetDefaultBrowserSize();

var result = (_context, await setupConfiguration.SetupOperation(_context));

await _context.AssertLogsAsync();

await setupConfiguration.AfterSetup.InvokeAsync<AfterSetupHandler>(handler => handler(_configuration));

_testOutputHelper.WriteLineTimestampedAndDebug("Finished setup operation.");

return result;
Expand All @@ -526,7 +528,7 @@ private async Task SetupAsync()

_context = await CreateContextAsync();

await _context.GoToRelativeUrlAsync(resultUri.PathAndQuery);
if (_context.IsBrowserConfigured) await _context.GoToRelativeUrlAsync(resultUri.PathAndQuery);
}
catch (Exception ex) when (ex is not SetupFailedFastException)
{
Expand Down
Loading