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

Monkey testing #109

Merged
merged 85 commits into from
Feb 2, 2022
Merged
Show file tree
Hide file tree
Changes from 61 commits
Commits
Show all changes
85 commits
Select commit Hold shift + click to select a range
42b5db6
Add basic monkey testing functionality
YevgeniyShunevych Dec 3, 2021
82bd0f6
Merge branch 'dev' into monkey-testing
YevgeniyShunevych Dec 3, 2021
e2372d8
Merge branch 'dev' into monkey-testing
YevgeniyShunevych Dec 7, 2021
f929be9
Improve monkey testing functionality
YevgeniyShunevych Dec 9, 2021
909fdc1
Merge branch 'dev' into task/monkey-testing
YevgeniyShunevych Dec 9, 2021
3b61ce0
Improve monkey testing functionality
YevgeniyShunevych Dec 10, 2021
ecfdcc5
Merge branch 'dev' into task/monkey-testing
YevgeniyShunevych Dec 10, 2021
33e20f1
Improve monkey testing functionality
YevgeniyShunevych Dec 10, 2021
3124993
Improve monkey testing functionality
YevgeniyShunevych Dec 10, 2021
dfd3217
Improve monkey testing functionality
YevgeniyShunevych Dec 13, 2021
9b6a759
Update Lombiq.Tests.UI/MonkeyTesting/MonkeyTester.cs
YevgeniyShunevych Dec 15, 2021
60e8926
Update Lombiq.Tests.UI/MonkeyTesting/IMonkeyTestingUrlFilter.cs
YevgeniyShunevych Dec 15, 2021
18c2e19
Update XML comments of `MonkeyTestingUITestContextExtensions`
YevgeniyShunevych Dec 15, 2021
1165e0f
Update Lombiq.Tests.UI.Samples/Tests/MonkeyTests.cs
YevgeniyShunevych Dec 15, 2021
8582e5e
Update Lombiq.Tests.UI/MonkeyTesting/MonkeyTester.cs
YevgeniyShunevych Dec 15, 2021
d84f289
Update Lombiq.Tests.UI/MonkeyTesting/MonkeyTester.cs
YevgeniyShunevych Dec 15, 2021
376438d
Update Lombiq.Tests.UI/MonkeyTesting/MonkeyTester.cs
YevgeniyShunevych Dec 15, 2021
3b51789
Update Lombiq.Tests.UI/MonkeyTesting/MonkeyTester.cs
YevgeniyShunevych Dec 15, 2021
942dd9c
Update Lombiq.Tests.UI/MonkeyTesting/MonkeyTester.cs
YevgeniyShunevych Dec 15, 2021
d247556
Update Lombiq.Tests.UI/MonkeyTesting/MonkeyTester.cs
YevgeniyShunevych Dec 15, 2021
751cb8a
Update Lombiq.Tests.UI/MonkeyTesting/MonkeyTester.cs
YevgeniyShunevych Dec 15, 2021
ae91b36
Refactor `MonkeyTester`
YevgeniyShunevych Dec 15, 2021
a05d1c7
Refactor `MonkeyTestingUITestContextExtensions` and update XML comments
YevgeniyShunevych Dec 15, 2021
f6ffcbe
Update XML comments
YevgeniyShunevych Dec 15, 2021
69540f2
Update MonkeyTestingUrlCleaners
YevgeniyShunevych Dec 15, 2021
8ca2174
Rename `MonkeyTestingUrlCleaner`s to `MonkeyTestingUrlSanitizer`s
YevgeniyShunevych Dec 15, 2021
d5a35c0
Remove try/catch blocks from `MonkeyTester.TestCurrentPageWithRandomS…
YevgeniyShunevych Dec 15, 2021
29e68ae
Add `Driver` null checks to `UITestExecutionSession.ExecuteAsync` and…
YevgeniyShunevych Dec 16, 2021
bacd50c
Update XML comments of `MonkeyTestingOptions.UrlSanitizers`
YevgeniyShunevych Dec 16, 2021
4fa66b7
Refactor MonkeyTestingUrlFilters
YevgeniyShunevych Dec 16, 2021
cb8c427
Refactor `MonkeyTester`
YevgeniyShunevych Dec 16, 2021
cb28aac
Use `NonSecurityRandomizer` in `MonkeyTester`
YevgeniyShunevych Dec 16, 2021
75aed31
Add monkey testing docs
YevgeniyShunevych Dec 16, 2021
d743bb5
Update monkey testing functionality
YevgeniyShunevych Dec 17, 2021
8093f28
Update `MonkeyTester`
YevgeniyShunevych Dec 17, 2021
7f50f3d
Merge branch 'dev' into task/monkey-testing
YevgeniyShunevych Dec 17, 2021
151cb57
Docs
Piedone Dec 17, 2021
20e8cb0
Monkey test as an example to a restricted test
Piedone Dec 17, 2021
8aae2f0
Code styling
Piedone Dec 17, 2021
bbe2f6c
IMonkeyTestingUrlFilter implementation for simple relative URL Starts…
Piedone Dec 17, 2021
88819f6
Docs
Piedone Dec 17, 2021
c448d18
Code styling
Piedone Dec 17, 2021
997133e
Merge branch 'dev' into task/monkey-testing
YevgeniyShunevych Dec 25, 2021
3ab73c2
Rename `Clean` method of `IMonkeyTestingUrlSanitizer` to `Sanitize`
YevgeniyShunevych Dec 28, 2021
a849b83
Update XML comment of `MonkeyTestingOptions.PageMarkerPollingInterval…
YevgeniyShunevych Dec 28, 2021
a8060f9
Merge remote-tracking branch 'origin/dev' into task/monkey-testing
Piedone Dec 29, 2021
9b0ee0d
Fixing new code analysis violations
Piedone Dec 29, 2021
c8674cf
Adding page change events to BasicOrchardFeaturesTests
Piedone Dec 29, 2021
cf6b7da
Adding page change events to GoToPage() methods
Piedone Dec 29, 2021
1fa27a7
Merge remote-tracking branch 'origin/dev' into task/monkey-testing
Piedone Dec 29, 2021
50fc297
Analyzer fixes
Piedone Dec 29, 2021
4788e23
Merge branch 'task/monkey-testing' into issue/OSOE-58
Piedone Dec 29, 2021
f6a7640
Better page change events
Piedone Dec 29, 2021
93f4fda
Moving form-related helpers from NavigationUITestContextExtensions to…
Piedone Dec 29, 2021
f9c0393
Fixing async issue with AtataContext
Piedone Dec 29, 2021
197091e
Adding the registration page as an Orchard page too
Piedone Dec 29, 2021
bf90917
Merge pull request #112 from Lombiq/issue/OSOE-58
Piedone Dec 29, 2021
ce938a6
Add async method versions to `MonkeyTestingUITestContextExtensions`
YevgeniyShunevych Jan 4, 2022
19a647d
Change and rename `TestContensAdminAsMonkeyRecursivelyShouldWorkWithA…
YevgeniyShunevych Jan 4, 2022
eb0bc04
Update monkey testing functionality to stop Gremlins script explicitly
YevgeniyShunevych Jan 5, 2022
4b5f63f
Update monkey testing functionality to embed gremlins.js npm package …
YevgeniyShunevych Jan 6, 2022
69836b7
Use `Lombiq.Npm.Targets` in `Lombiq.Tests.UI` project
YevgeniyShunevych Jan 11, 2022
d66aa61
Add docs for `MonkeyTestingOptions.GremlinsAttackDelay`
YevgeniyShunevych Jan 11, 2022
937346c
Merge branch 'dev' into task/monkey-testing
YevgeniyShunevych Jan 11, 2022
8e1c804
Adding PNPM lock file too
Piedone Jan 12, 2022
09f4993
Docs
Piedone Jan 12, 2022
1a77b1b
Adding monkey tests for troubleshooting
Piedone Jan 12, 2022
edb3164
Fixing that accessibility and HTML validation reports weren't always …
Piedone Jan 12, 2022
b631306
Change `GremlinsScripts.StopGremlinsScript` to remove Gremlins visual…
YevgeniyShunevych Jan 18, 2022
cb61f42
Improve monkey testing functionality
YevgeniyShunevych Jan 22, 2022
d34f340
Merge branch 'dev' into task/monkey-testing
YevgeniyShunevych Jan 22, 2022
54d0737
Code styling
Piedone Jan 24, 2022
6d2c67a
Merge branch 'dev' into task/monkey-testing
Piedone Jan 24, 2022
15774ff
Adding link to monkey tests training section
Piedone Jan 24, 2022
3e587c4
Update monkey testing functionality
YevgeniyShunevych Jan 25, 2022
8970dbc
Merge remote-tracking branch 'origin/dev' into task/monkey-testing
Piedone Jan 25, 2022
e8b02d5
Code styling
Piedone Jan 25, 2022
e660f5b
Note on Gremlin logging
Piedone Jan 25, 2022
9a053c8
Merge branch 'dev' into task/monkey-testing
Piedone Jan 25, 2022
f356563
Fix `MonkeyTester.WaitForGremlinsIndicatorsToDisappear` method
YevgeniyShunevych Jan 26, 2022
7f3bf4c
Fix `MonkeyTester.TestCurrentPageWithRandomSeedAsync` method
YevgeniyShunevych Jan 26, 2022
d8a2b54
Fix `UITestExecutionSession` by unsubscribing from `_configuration.Ev…
YevgeniyShunevych Feb 2, 2022
cbc1b5c
Add temp `MonkeyTests.ShouldFailWith404`
YevgeniyShunevych Feb 2, 2022
3e674f1
Removing log assertion event handler even during UITestExecutionSessi…
Piedone Feb 2, 2022
40db630
Removing debug code
Piedone Feb 2, 2022
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,5 @@ bin/
*.user
Localization/
wwwroot
node_modules/
package-lock.json
1 change: 1 addition & 0 deletions Lombiq.Tests.UI.Samples/Tests/ErrorHandlingTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,4 @@ public Task ErrorOnLoadedPageShouldHaltTest(Browser browser) =>
}

// END OF TRAINING SECTION: Error Handling.
// NEXT STATION: Head over to Tests/MonkeyTests.cs.
91 changes: 91 additions & 0 deletions Lombiq.Tests.UI.Samples/Tests/MonkeyTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
using Lombiq.Tests.UI.Attributes;
using Lombiq.Tests.UI.Extensions;
using Lombiq.Tests.UI.MonkeyTesting;
using Lombiq.Tests.UI.Services;
using System;
using System.Threading.Tasks;
using Xunit;
using Xunit.Abstractions;

namespace Lombiq.Tests.UI.Samples.Tests
{
// It's possible to execute monkey tests that walk through site pages and do random interactions with pages, like
// click, scrolling, form filling, etc. Such random actions can uncover bugs that are otherwise difficult to find.
// Use such tests plug holes in your test suite which are not covered by explicit tests.
public class MonkeyTests : UITestBase
Piedone marked this conversation as resolved.
Show resolved Hide resolved
{
// Monkey testing has its own configuration too. Check out the docs of the options too.
private readonly MonkeyTestingOptions _monkeyTestingOptions = new()
{
PageTestTime = TimeSpan.FromSeconds(10),
BaseRandomSeed = 1234,
};

public MonkeyTests(ITestOutputHelper testOutputHelper)
: base(testOutputHelper)
{
}

// The basic idea is that you unleash monkey testing on specific pages or sections of the site, like a contact
// form or the content management UI.
// First, we test a single page.
[Theory, Chrome]
public Task TestCurrentPageAsMonkeyShouldWorkWithConfiguredRandomSeed(Browser browser) =>
ExecuteTestAfterSetupAsync(
context =>
{
// Note how we define the starting point of the test as the homepage.
context.GoToHomePage();
// The specified random see gives you the option to reproduce the random interactions. Otherwise
// it would be calculated from MonkeyTestingOptions.BaseRandomSeed.
context.TestCurrentPageAsMonkey(_monkeyTestingOptions, 12345);
},
browser);

// Recursive testing will just continue testing following the configured rules until it runs out time or new
// pages.
[Theory, Chrome]
public Task TestCurrentPageAsMonkeyRecursivelyShouldWorkWithAnonymousUser(Browser browser) =>
ExecuteTestAfterSetupAsync(
context =>
{
context.GoToHomePage();
context.TestCurrentPageAsMonkeyRecursively(_monkeyTestingOptions);
},
browser);

// Let's test with an authenticated user too.
[Theory, Chrome]
public Task TestCurrentPageAsMonkeyRecursivelyShouldWorkWithAdminUser(Browser browser) =>
ExecuteTestAfterSetupAsync(
context =>
{
// Monkey tests needn't all start from the homepage. This one starts from the Orchard admin
// dashboard.
context.SignInDirectlyAndGoToDashboard();
context.TestCurrentPageAsMonkeyRecursively(_monkeyTestingOptions);
},
browser);

// Let's just test the background tasks management admin area.
[Theory, Chrome]
public Task TestAdminBackgroundTasksAsMonkeyRecursivelyShouldWorkWithAdminUser(Browser browser) =>
ExecuteTestAfterSetupAsync(
context =>
{
// You can fence monkey testing with URL filters: Monkey testing will only be executed if the
// current URL matches. This way, you can restrict monkey testing to just sections of the site. You
// can also use such fencing to have multiple monkey testing methods in multiple test classes, thus
// running them in parallel.
_monkeyTestingOptions.UrlFilters.Add(new StartsWithMonkeyTestingUrlFilter("/Admin/BackgroundTasks"));
// You could also configure the same thing with regex:
////_monkeyTestingOptions.UrlFilters.Add(new MatchesRegexMonkeyTestingUrlFilter(@"\/Admin\/BackgroundTasks"));

context.SignInDirectlyAndGoToRelativeUrl("/Admin/BackgroundTasks");
context.TestCurrentPageAsMonkeyRecursively(_monkeyTestingOptions);
},
browser);
}
}

// END OF TRAINING SECTION: Monkey tests.
4 changes: 3 additions & 1 deletion Lombiq.Tests.UI/Docs/CreatingTests.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ Reference `Lombiq.Tests.UI` from your test project, and add a reference to the `

For a sample test project see [`Lombiq.Tests.UI.Samples`](../../Lombiq.Tests.UI.Samples/Readme.md).

We also recommend always running the suite of tests for checking that all the basic Orchard Core features work, like login, registration, and content management. Use `context.TestBasicOrchardFeatures()` to run all such tests but see the other, more granular tests too. This is also demonstrated in `Lombiq.Tests.UI.Samples` and in [this video](https://www.youtube.com/watch?v=jmhq63sRZrI).
We also recommend always running some highly automated tests that need very little configuration:

- The suite of tests for checking that all the basic Orchard Core features work, like login, registration, and content management. Use `context.TestBasicOrchardFeatures()` to run all such tests but see the other, more granular tests too. This is also demonstrated in `Lombiq.Tests.UI.Samples` and in [this video](https://www.youtube.com/watch?v=jmhq63sRZrI).
- [Monkey tests](https://en.wikipedia.org/wiki/Monkey_testing) can also be useful. Use `context.TestCurrentPageAsMonkeyRecursively()` to run a monkey testing process, which walks through site pages and does random interactions with pages, like clicking, scrolling, form filling, etc. It's recommended to have at least 3 monkey tests that execute with different user states: As an admin, as a regular registered user and as an anonymous user. The admin test can start execution on admin dashboard page, while other tests can start on home page.

## Steps for creating a test class

Expand Down
1 change: 1 addition & 0 deletions Lombiq.Tests.UI/Docs/Tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@
- 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 cheatsheet](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).
- Accessibility checking can be done with [axe](https://github.com/dequelabs/axe-core) via [Selenium.Axe for .NET](https://github.com/TroyWalshProf/SeleniumAxeDotnet).
- 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).
- Monkey testing is implemented using [Gremlins.js](https://github.com/marmelab/gremlins.js/) library.
6 changes: 6 additions & 0 deletions Lombiq.Tests.UI/Docs/Troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,9 @@

EXEC sp_executesql @sql
```


## Monkey testing
Piedone marked this conversation as resolved.
Show resolved Hide resolved

- Errors uncovered by monkey testing functionality can be reproduced locally by executing the same test with the same random seed by setting `MonkeyTestingOptions.BaseRandomSeed` with the value the test failed. If `BaseRandomSeed` is generated then you can see it in the log; if you specified it then nothing else to do.
- If you want to test the failed page granularly, you can write a test that navigates to that page and executes `context.TestCurrentPageAsMonkey(_monkeyTestingOptions, 12345);`, where `12345` is the random seed number that can be found in a failed test log.
18 changes: 18 additions & 0 deletions Lombiq.Tests.UI/EmbeddedResourceProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using System.IO;
using System.Text;

namespace Lombiq.Tests.UI
{
internal static class EmbeddedResourceProvider
Piedone marked this conversation as resolved.
Show resolved Hide resolved
{
internal static string ReadEmbeddedFile(string fileName)
{
var assembly = typeof(EmbeddedResourceProvider).Assembly;
var resourceStream = assembly.GetManifestResourceStream($"{assembly.GetName().Name}.Resources.{fileName}");

using var reader = new StreamReader(resourceStream, Encoding.UTF8);

return reader.ReadToEnd();
Piedone marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ public static UITestContext TestLogin(
() =>
{
context.GoToLoginPage()
.LogInWith(userName, password)
.LogInWith(context, userName, password)
.ShouldLeaveLoginPage();

context.GetCurrentUserName().ShouldBe(userName);
Expand Down Expand Up @@ -168,7 +168,7 @@ public static UITestContext TestLoginWithInvalidData(
context.SignOutDirectly();

context.GoToLoginPage()
.LogInWith(userName, password)
.LogInWith(context, userName, password)
.ShouldStayOnLoginPage()
.ValidationSummaryErrors.Should.Not.BeEmpty();

Expand All @@ -193,6 +193,8 @@ public static UITestContext TestLogout(this UITestContext context) =>
.TopNavbar.Account.LogOff.Click()
.ShouldLeaveAdminPage();

context.TriggerAfterPageChangeEventAsync().Wait();

context.GetCurrentUserName().ShouldBeNullOrEmpty();
});

Expand Down Expand Up @@ -220,14 +222,14 @@ public static UITestContext TestRegistration(this UITestContext context, UserReg
context.GoToLoginPage()
.RegisterAsNewUser.Should.BeVisible()
.RegisterAsNewUser.ClickAndGo()
.RegisterWith(parameters)
.RegisterWith(context, parameters)
.ShouldLeaveRegistrationPage();

context.GetCurrentUserName().ShouldBe(parameters.UserName);
context.SignOutDirectly();

context.GoToLoginPage()
.LogInWith(parameters.UserName, parameters.Password);
context.GoToLoginPage().LogInWith(context, parameters.UserName, parameters.Password);
context.TriggerAfterPageChangeEventAsync().Wait();
context.GetCurrentUserName().ShouldBe(parameters.UserName);
context.SignOutDirectly();
});
Expand Down Expand Up @@ -259,7 +261,7 @@ public static UITestContext TestRegistrationWithInvalidData(this UITestContext c
"Test registration with invalid data",
() => context
.GoToRegistrationPage()
.RegisterWith(parameters)
.RegisterWith(context, parameters)
.ShouldStayOnRegistrationPage()
.ValidationMessages.Should.Not.BeEmpty());
}
Expand Down Expand Up @@ -288,7 +290,7 @@ public static UITestContext TestRegistrationWithAlreadyRegisteredEmail(
"Test registration with already registered email",
() => context
.GoToRegistrationPage()
.RegisterWith(parameters)
.RegisterWith(context, parameters)
.ShouldStayOnRegistrationPage()
.ValidationMessages[page => page.Email].Should.BeVisible());
}
Expand Down Expand Up @@ -323,6 +325,8 @@ public static UITestContext TestContentOperations(this UITestContext context, st
.AlertMessages.Should.Contain(message => message.IsSuccess)
.Items[item => item.Title == pageTitle].View.Click();

context.TriggerAfterPageChangeEventAsync().Wait();

context.Scope.AtataContext.Go.ToNextWindow(new OrdinaryPage(pageTitle))
.AggregateAssert(page => page
.PageTitle.Should.Contain(pageTitle)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
using Lombiq.Tests.UI.Exceptions;
using Lombiq.Tests.UI.Models;
using Lombiq.Tests.UI.Services;
using OpenQA.Selenium;
using System;
using System.Threading.Tasks;

namespace Lombiq.Tests.UI.Extensions
Expand All @@ -15,7 +12,7 @@ public static void SetUpEvents(this OrchardCoreUITestExecutorConfiguration confi

PageNavigationState navigationState = null;

configuration.Events.AfterNavigation += (context, _) => OnEventsAfterNavigationAsync(context);
configuration.Events.AfterNavigation += (context, _) => context.TriggerAfterPageChangeEventAsync();

configuration.Events.BeforeClick += (context, _) =>
{
Expand All @@ -25,40 +22,8 @@ public static void SetUpEvents(this OrchardCoreUITestExecutorConfiguration confi

configuration.Events.AfterClick += (context, _) =>
navigationState.CheckIfNavigationHasOccurred()
? OnEventsAfterNavigationAsync(context)
? context.TriggerAfterPageChangeEventAsync()
: Task.CompletedTask;
}

private static bool IsNoAlert(UITestContext context)
{
// If there's an alert (which can happen mostly after a click but also after navigating) then all other
// driver operations, even retrieving the current URL, will throw an UnhandledAlertException. Thus we
// need to check if an alert is present and that's only possible by catching exceptions.
try
{
context.Driver.SwitchTo().Alert();
return false;
}
catch (NoAlertPresentException)
{
return true;
}
}

private static async Task OnEventsAfterNavigationAsync(UITestContext context)
{
if (IsNoAlert(context) &&
context.Configuration.Events.AfterPageChange is { } afterPageChange)
{
try
{
await afterPageChange.Invoke(context);
}
catch (Exception exception)
{
throw new PageChangeAssertionException(context, exception);
}
}
}
}
}
100 changes: 100 additions & 0 deletions Lombiq.Tests.UI/Extensions/FormUITestContextExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
using Lombiq.Tests.UI.Services;
using OpenQA.Selenium;
using System;
using System.Globalization;
using System.Linq;

// Using the Atata namespace because that'll surely be among the using declarations of the test. OpenQA.Selenium not
// necessarily.
Expand Down Expand Up @@ -175,6 +177,104 @@ public static string GetSelectedTabText(
return title;
}

public static void SetDropdown<T>(this UITestContext context, string selectId, T value)
where T : Enum
{
context.ClickReliablyOn(By.Id(selectId));
context.Get(By.CssSelector(FormattableString.Invariant($"#{selectId} option[value='{(int)(object)value}']"))).Click();
}

public static void SetDropdownByText(this UITestContext context, string selectId, string value) =>
SetDropdownByText(context, By.Id(selectId), value);

public static void SetDropdownByText(this UITestContext context, By selectBy, string value)
{
context.ClickReliablyOn(selectBy);
context.Get(selectBy).Get(By.XPath($".//option[contains(., '{value}')]")).Click();
}

/// <summary>
/// Sets the value of the date picker via JavaScript and then raises the <c>change</c> event.
/// </summary>
public static void SetDatePicker(this UITestContext context, string id, DateTime value) =>
context.ExecuteScript(
FormattableString.Invariant($"document.getElementById('{id}').value = '{value:yyyy-MM-dd}';") +
$"document.getElementById('{id}').dispatchEvent(new Event('change'));");

public static DateTime GetDatePicker(this UITestContext context, string id) =>
DateTime.ParseExact(
context.Get(By.Id(id)).GetAttribute("value"),
"yyyy-MM-dd",
CultureInfo.InvariantCulture);

/// <summary>
/// Finds the first submit button and clicks on it reliably.
/// </summary>
public static void ClickReliablyOnSubmit(this UITestContext context) =>
context.ClickReliablyOn(By.CssSelector("button[type='submit']"));

/// <summary>
/// Finds the "Add New" button.
/// </summary>
public static IWebElement GetAddNewButton(this UITestContext context) =>
context.Get(By.XPath("//button[contains(.,'Add New')]"));

/// <summary>
/// Opens the dropdown belonging to the "Add New" button. If <paramref name="byLocalMenuItem"/> is not <see
/// langword="null"/> it will click on that element within the dropdown context as well.
/// </summary>
public static void SelectAddNewDropdown(this UITestContext context, By byLocalMenuItem = null) =>
context.SelectFromBootstrapDropdownReliably(GetAddNewButton(context), byLocalMenuItem);

/// <summary>
/// Clicks on the <paramref name="dropdownButton"/> until the Bootstrap dropdown menu appears (up to 3 tries)
/// and then clicks on the <paramref name="byLocalMenuItem"/> within the dropdown menu's context.
/// </summary>
/// <param name="context">The current UI test context.</param>
/// <param name="dropdownButton">The button that reveals the Bootstrap dropdown menu.</param>
/// <param name="byLocalMenuItem">
/// The path inside the dropdown menu. If <see langword="null"/> then no selection (clicking) will be made, and
/// the dropdown is left open.
/// </param>
/// <exception cref="InvalidOperationException">
/// Thrown if clicking on the button didn't yield a dropdown menu even after retries.
/// </exception>
public static void SelectFromBootstrapDropdownReliably(
this UITestContext context,
IWebElement dropdownButton,
By byLocalMenuItem)
{
var byDropdownMenu = By.XPath("./following-sibling::*[contains(@class, 'dropdown-menu')]");

for (var i = 0; i < 3; i++)
{
dropdownButton.ClickReliably(context);

var dropdownMenu = dropdownButton.GetAll(byDropdownMenu).SingleOrDefault();
if (dropdownMenu != null)
{
if (byLocalMenuItem != null) dropdownMenu.Get(byLocalMenuItem).ClickReliably(context);
return;
}
}

throw new InvalidOperationException($"Couldn't open dropdown menu in 3 tries.");
}

/// <summary>
/// Clicks on the <paramref name="byDropdownButton"/> until the Bootstrap dropdown menu appears (up to 3 tries)
/// and then clicks on the menu item with the <paramref name="menuItemLinkText"/> within the dropdown menu's
/// context.
/// </summary>
/// <param name="context">The current UI test context.</param>
/// <param name="byDropdownButton">The path of the button that reveals the Bootstrap dropdown menu.</param>
/// <param name="menuItemLinkText">The text of the dropdown menu item.</param>
public static void SelectFromBootstrapDropdownReliably(
this UITestContext context,
By byDropdownButton,
string menuItemLinkText) =>
SelectFromBootstrapDropdownReliably(context, context.Get(byDropdownButton), By.LinkText(menuItemLinkText));

private static IWebElement TryFillElement(UITestContext context, By by, string text)
{
var element = context.Get(by);
Expand Down
Loading