Created
July 10, 2025 15:15
-
-
Save jimevans/c41e4ec46783c596adf2c73b6d749f74 to your computer and use it in GitHub Desktop.
WebDriver BiDi element click with wait for navigation behavior
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using WebDriverBiDi; | |
using WebDriverBiDi.BrowsingContext; | |
using WebDriverBiDi.Client.Inputs; | |
using WebDriverBiDi.Input; | |
using WebDriverBiDi.Script; | |
using WebDriverBiDi.Session; | |
/// <summary> | |
/// Enumerates the expected behavior for page navigation after clicking on an element. | |
/// </summary> | |
public enum ClickNavigationBehavior | |
{ | |
/// <summary> | |
/// Expect no navigation, omit waiting for navigation after a click. | |
/// </summary> | |
None, | |
/// <summary> | |
/// Expect a navigation on click, wait for that navigation to begin after click. | |
/// </summary> | |
WaitForNavigationStart, | |
/// <summary> | |
/// Expect a navigation on click, wait for the resulting page to fire the domContentLoaded event after click. | |
/// </summary> | |
WaitForDomContentLoadedEvent, | |
/// <summary> | |
/// Expect a navigation on click, wait for the resulting page to fire the load event after click. | |
/// </summary> | |
WaitForLoadEvent, | |
} | |
/// <summary> | |
/// Represents a locator for finding an element on the page. Includes methods for interacting with the located element. | |
/// </summary> | |
public class Locator | |
{ | |
private readonly BiDiDriver driver; | |
private string browsingContextId; | |
private Func<Task<IList<RemoteValue>>> locatorFunction; | |
private Locator(BiDiDriver driver, string browsingContextId, Func<Task<IList<RemoteValue>>> locationFunction) | |
{ | |
this.driver = driver; | |
this.browsingContextId = browsingContextId; | |
this.locatorFunction = locationFunction; | |
} | |
/// <summary> | |
/// Locates an element using CSS selectors. | |
/// </summary> | |
/// <param name="driver">The driver instance to use.</param> | |
/// <param name="browsingContextId">The ID of the browsing context in which to locate the element.</param> | |
/// <param name="cssSelector">The CSS selector to use to locate the element.</param> | |
/// <returns>The Locator instance.</returns> | |
public static Locator UsingCss(BiDiDriver driver, string browsingContextId, string cssSelector) | |
{ | |
return new Locator(driver, browsingContextId, async () => | |
{ | |
LocateNodesCommandParameters parameters = new(browsingContextId, new CssLocator(cssSelector)); | |
LocateNodesCommandResult result = await driver.BrowsingContext.LocateNodesAsync(parameters); | |
return result.Nodes; | |
}); | |
} | |
/// <summary> | |
/// Locates an element using XPath. | |
/// </summary> | |
/// <param name="driver">The driver instance to use.</param> | |
/// <param name="browsingContextId">The ID of the browsing context in which to locate the element.</param> | |
/// <param name="xpath">The XPath expression to use to locate the element.</param> | |
/// <returns>The Locator instance.</returns> | |
public static Locator UsingXPath(BiDiDriver driver, string browsingContextId, string xpath) | |
{ | |
return new Locator(driver, browsingContextId, async () => | |
{ | |
LocateNodesCommandParameters parameters = new(browsingContextId, new XPathLocator(xpath)); | |
LocateNodesCommandResult result = await driver.BrowsingContext.LocateNodesAsync(parameters); | |
return result.Nodes; | |
}); | |
} | |
/// <summary> | |
/// Asynchronously clicks the element specified by this Locator. | |
/// </summary> | |
/// <param name="clickNavigationBehavior">The expected behavior for navigation after a click.</param> | |
/// <returns>A Task representing information about the operation.</returns> | |
/// <exception cref="WebDriverBiDiException"> | |
/// Thrown if this Locator resolves to zero elements or more than one element. | |
/// </exception> | |
public async Task Click(ClickNavigationBehavior clickNavigationBehavior = ClickNavigationBehavior.None) | |
{ | |
ManualResetEventSlim navigationStartEvent = new(false); | |
ManualResetEventSlim navigationCompleteEvent = new(false); | |
string subscriptionId = string.Empty; | |
EventObserver<NavigationEventArgs>? navigationStartObserver = null; | |
EventObserver<NavigationEventArgs>? navigationCompletedObserver = null; | |
IList<RemoteValue> nodes = await this.Locate(); | |
if (nodes.Count != 1) | |
{ | |
string message = nodes.Count == 0 ? "No nodes found for locator" : "Locator not unique, returns multiple nodes"; | |
throw new WebDriverBiDiException(message); | |
} | |
if (clickNavigationBehavior != ClickNavigationBehavior.None) | |
{ | |
// If not waiting on navigation, set the events so that no wait is required. | |
navigationStartEvent.Set(); | |
navigationCompleteEvent.Set(); | |
} | |
else | |
{ | |
// Add observers and events for monitoring navigation. | |
List<string> events = new() { "browsingContext.navigationStarted" }; | |
navigationStartObserver = this.driver.BrowsingContext.OnNavigationStarted.AddObserver((e) => navigationStartEvent.Set()); | |
if (clickNavigationBehavior == ClickNavigationBehavior.WaitForDomContentLoadedEvent) | |
{ | |
events.Add("browsingContext.domContentLoaded"); | |
navigationCompletedObserver = this.driver.BrowsingContext.OnDomContentLoaded.AddObserver((e) => navigationCompleteEvent.Set()); | |
} | |
else if (clickNavigationBehavior == ClickNavigationBehavior.WaitForLoadEvent) | |
{ | |
events.Add("browsingContext.load"); | |
navigationCompletedObserver = this.driver.BrowsingContext.OnLoad.AddObserver((e) => navigationCompleteEvent.Set()); | |
} | |
else | |
{ | |
// Not waiting for navigation to complete. | |
navigationCompleteEvent.Set(); | |
} | |
List<string> contextList = new() { this.browsingContextId }; | |
SubscribeCommandParameters subscribeParameters = new SubscribeCommandParameters(events, contextList); | |
SubscribeCommandResult subscribeResult = await this.driver.Session.SubscribeAsync(subscribeParameters); | |
subscriptionId = subscribeResult.SubscriptionId; | |
} | |
InputBuilder inputBuilder = new(); | |
AddClickOnElementAction(inputBuilder, nodes[0].ToSharedReference()); | |
PerformActionsCommandParameters actionsParams = new(this.browsingContextId); | |
actionsParams.Actions.AddRange(inputBuilder.Build()); | |
await this.driver.Input.PerformActionsAsync(actionsParams); | |
if (clickNavigationBehavior != ClickNavigationBehavior.None) | |
{ | |
// Wait for navigation to start for a half-second, which should | |
// yield enough time for a navigation to start after a click. | |
navigationStartEvent.Wait(TimeSpan.FromMilliseconds(500)); | |
// Wait for navigation to complete; this timeout should be made configurable. | |
navigationCompleteEvent.Wait(TimeSpan.FromSeconds(30)); | |
// Clean up the observers and events. | |
navigationStartObserver?.Unobserve(); | |
navigationCompletedObserver?.Unobserve(); | |
UnsubscribeByIdsCommandParameters unsubscribeParameters = new(); | |
unsubscribeParameters.SubscriptionIds.Add(subscriptionId); | |
await this.driver.Session.UnsubscribeAsync(unsubscribeParameters); | |
} | |
} | |
private static void AddClickOnElementAction(InputBuilder builder, SharedReference elementReference) | |
{ | |
builder.AddAction(builder.DefaultPointerInputSource.CreatePointerMove(0, 0, Origin.Element(new ElementOrigin(elementReference)))) | |
.AddAction(builder.DefaultPointerInputSource.CreatePointerDown(PointerButton.Left)) | |
.AddAction(builder.DefaultPointerInputSource.CreatePointerUp()); | |
} | |
private async Task<IList<RemoteValue>> Locate() | |
{ | |
return await this.locatorFunction(); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment