Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
40 changes: 40 additions & 0 deletions UiAutomationGRPC.Client/Calc/Pages/BasePageObject.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,51 @@ public abstract class BasePageObject<T> where T : BasePageObject<T>
public UiAutomationDriver _driver;
protected VirtualKeyboard Keyboard { get; }
protected VirtualMouse Mouse { get; }

/// <summary>
/// Internal pipeline of queued async actions.
/// Action methods append to this chain and return <c>this</c> synchronously,
/// enabling fluent chaining. The pipeline is flushed (awaited and reset)
/// when a value-returning or page-transitioning method is called.
/// </summary>
private Task _pipeline = Task.CompletedTask;

protected BasePageObject(UiAutomationDriver driver)
{
_driver = driver ?? throw new ArgumentNullException(nameof(driver));
Keyboard = new VirtualKeyboard(driver);
Mouse = new VirtualMouse(driver);
}

/// <summary>
/// Appends an async action to the internal pipeline.
/// The action will execute sequentially after all previously enqueued actions.
/// </summary>
protected void Enqueue(Func<Task> action)
{
var previous = _pipeline;
_pipeline = InternalContinueAsync(previous, action);
}

private static async Task InternalContinueAsync(Task previous, Func<Task> next)
{
await previous.ConfigureAwait(false);
await next().ConfigureAwait(false);
}

/// <summary>
/// Drains the pipeline (awaits all enqueued actions), then executes the
/// result-producing <paramref name="resultAction"/>.
/// Use in every terminal method (value-returning or page-transitioning)
/// to ensure queued actions complete before reading state.
/// </summary>
protected async Task<TResult> ResolveAsync<TResult>(Func<Task<TResult>> resultAction)
{
var pending = _pipeline;
_pipeline = Task.CompletedTask;
await pending.ConfigureAwait(false);
return await resultAction().ConfigureAwait(false);
}

}

22 changes: 10 additions & 12 deletions UiAutomationGRPC.Client/Calc/Pages/CalcNavigationPage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,19 @@ public CalcNavigationPaget(UiAutomationDriver driver, TPage previousPage) : base
_driver = driver ?? throw new ArgumentNullException(nameof(driver));
_previousPage = previousPage;
_locators = new CalcNavigationPagetLocators(driver);
// Optional: Wait for the app to be ready in constructor
_locators.ButtonSettings.WaitForElementExist();
}

public CalcSettingsPage<TPage> ClickSettings()
{
_locators.ButtonSettings.Click();
return new CalcSettingsPage<TPage>(_driver, _previousPage);
}
/// <summary>
/// Waits for the page to be ready. Call after construction.
/// </summary>
public Task<CalcNavigationPaget<TPage>> WaitForReady() =>
ResolveAsync(async () => { await _locators.ButtonSettings.WaitForElementExistAsync(); return this; });

public Task<CalcSettingsPage<TPage>> ClickSettings() =>
ResolveAsync(async () => { await _locators.ButtonSettings.ClickAsync(); return new CalcSettingsPage<TPage>(_driver, _previousPage); });

public TPage ClickNavigationButton()
{
_locators.ButtonNavigation.Click();
return _previousPage;
}
public Task<TPage> ClickNavigationButton() =>
ResolveAsync(async () => { await _locators.ButtonNavigation.ClickAsync(); return _previousPage; });
}

public class CalcNavigationPagetLocators
Expand Down
44 changes: 16 additions & 28 deletions UiAutomationGRPC.Client/Calc/Pages/CalcPage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,64 +10,52 @@ public class CalcPage : BasePageObject<CalcPage>

public CalcPage(UiAutomationDriver driver) : base(driver)
{
// Optional: Wait for the app to be ready in constructor
_driver = driver;
_locators = new CalcPageLocators(driver);
_locators.ResultText.WaitForElementExist();
}

/// <summary>
/// Waits for the page to be ready. Call after construction.
/// </summary>
public Task<CalcPage> WaitForReady() =>
ResolveAsync(async () => { await _locators.ResultText.WaitForElementExistAsync(); return this; });

public CalcPage ClickTwo()
{
_locators.ButtonTwo.Click();
Enqueue(() => _locators.ButtonTwo.ClickAsync());
return this;
}

public CalcPage ClickPlus()
{
_locators.ButtonPlus.Click();
Enqueue(() => _locators.ButtonPlus.ClickAsync());
return this;
}

public CalcPage ClickEqual()
{
_locators.ButtonEqual.Click();
Enqueue(() => _locators.ButtonEqual.ClickAsync());
return this;
}

public string GetResult()
{
return _locators.ResultText.Name();
}
public Task<string> GetResult() =>
ResolveAsync(() => _locators.ResultText.NameAsync());

public CalcNavigationPaget<CalcPage> ClickNavigationButton()
{
_locators.ResultText.Name();
return new CalcNavigationPaget<CalcPage>(_driver, this);
}
public Task<CalcNavigationPaget<CalcPage>> ClickNavigationButton() =>
ResolveAsync(async () => { await _locators.ResultText.NameAsync(); return new CalcNavigationPaget<CalcPage>(_driver, this); });

public CalcPage ClickResultText()
{
_locators.ResultText.Click();
Enqueue(() => _locators.ResultText.ClickAsync());
return this;
}

/// <summary>
/// Sends a key using the VirtualKeyboard (async).
/// For the fluent sync pattern, this returns immediately after queuing.
/// Sends a key using VirtualKeyboard.
/// </summary>
public CalcPage SendKey(string key)
{
// Fire and forget for fluent pattern - use SendKeyAsync directly for proper async handling
_ = Keyboard.SendKeyAsync(key);
return this;
}

/// <summary>
/// Sends a key asynchronously using VirtualKeyboard.
/// </summary>
public async Task<CalcPage> SendKeyAsync(string key)
{
await Keyboard.SendKeyAsync(key);
Enqueue(() => Keyboard.SendKeyAsync(key));
return this;
}
}
Expand Down
15 changes: 8 additions & 7 deletions UiAutomationGRPC.Client/Calc/Pages/CalcSettingsPage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,16 @@ public CalcSettingsPage(UiAutomationDriver driver, TPage previousPage) : base(dr
{
_driver = driver ?? throw new ArgumentNullException(nameof(driver));
_locators = new CalcSettingsPageLocators(driver);
// Optional: Wait for the app to be ready in constructor
_locators.BackButton.WaitForElementExist();
}

public CalcPage ClickBack()
{
_locators.BackButton.Click();
return new CalcPage(_driver);
}
/// <summary>
/// Waits for the page to be ready. Call after construction.
/// </summary>
public Task<CalcSettingsPage<TPage>> WaitForReady() =>
ResolveAsync(async () => { await _locators.BackButton.WaitForElementExistAsync(); return this; });

public Task<CalcPage> ClickBack() =>
ResolveAsync(async () => { await _locators.BackButton.ClickAsync(); return new CalcPage(_driver); });
}

public class CalcSettingsPageLocators
Expand Down
11 changes: 6 additions & 5 deletions UiAutomationGRPC.Client/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ private static async Task PerformCalculatorInteractions(UiAutomationDriver drive

// CalcPage internally uses CalcPageLocators where selectors are defined.
var calcPage = new CalcPage(driver);
await calcPage.WaitForReady();

// A. Click Interactions
try
Expand All @@ -114,7 +115,7 @@ private static async Task PerformCalculatorInteractions(UiAutomationDriver drive
.ClickTwo()
.ClickEqual();

var resultName = calcPage.GetResult();
var resultName = await calcPage.GetResult();
Console.WriteLine($"Click Result: {resultName}");
}
catch (Exception ex)
Expand All @@ -138,7 +139,7 @@ private static async Task PerformCalculatorInteractions(UiAutomationDriver drive

await Task.Delay(1000); // Wait for calculation

var resultName = calcPage.GetResult();
var resultName = await calcPage.GetResult();
Console.WriteLine($"Keyboard Result Name: {resultName}");
}
catch (Exception ex)
Expand All @@ -165,7 +166,7 @@ async Task ClickElementGlobally(UiAutomationGRPC.Library.Elements.IAutomationEle
{
try
{
var rect = element.GetRectangle();
var rect = await element.GetRectangleAsync();
int centerX = rect.Left + rect.Width / 2;
int centerY = rect.Top + rect.Height / 2;

Expand All @@ -188,7 +189,7 @@ async Task ClickElementGlobally(UiAutomationGRPC.Library.Elements.IAutomationEle
await ClickElementGlobally(locators.ButtonEqual, "Button Equal");

await Task.Delay(1000);
var resultName = calcPage.GetResult();
var resultName = await calcPage.GetResult();
Console.WriteLine($"Global Mouse Result: {resultName}");

Console.WriteLine("Mouse interactions completed.");
Expand All @@ -211,7 +212,7 @@ private static async Task TakeScreenshots(UiAutomationDriver driver)
var btn2 = locators.ButtonTwo;

// Get Runtime ID to identify the specific element for the screenshot service
string btnId = btn2.GetRuntimeId();
string btnId = await btn2.GetRuntimeIdAsync();
Console.WriteLine($"Button Two Runtime ID: {btnId}");

// 1. Element Screenshot
Expand Down
39 changes: 21 additions & 18 deletions UiAutomationGRPC.Library/Elements/IAutomationElement.cs
Original file line number Diff line number Diff line change
@@ -1,90 +1,93 @@
using UiAutomationGRPC.Library.Helpers;
using System.Drawing;
using System.Drawing;
using UiAutomationGRPC.Library.Helpers;

namespace UiAutomationGRPC.Library.Elements
{
/// <summary>
/// Interface for automation elements.
/// All methods are async-only because every operation involves a gRPC network call
/// to the server. Async prevents thread-blocking during network I/O and avoids
/// deadlocks when consumed from UI threads or synchronization-context-bound callers.
/// </summary>
public interface IAutomationElement
{
/// <summary>
/// Processes a click on an element.
/// </summary>
void Click();
Task ClickAsync();

/// <summary>
/// Processes a click on an element relative to Top Left coordinates.
/// Processes a click at specific screen coordinates (moves cursor to x,y then clicks).
/// </summary>
void Click(int x, int y);
Task ClickAsync(int x, int y);

/// <summary>
/// Processes a double click on an element.
/// </summary>
void DoubleClick();
Task DoubleClickAsync();

/// <summary>
/// Processes a hover on an element.
/// </summary>
void Hover();
Task HoverAsync();

/// <summary>
/// Returns the Name field of the element.
/// </summary>
string Name();
Task<string> NameAsync();

/// <summary>
/// Returns the ClassName field of the element.
/// </summary>
string ClassName();
Task<string> ClassNameAsync();

/// <summary>
/// Returns the AutomationId field of the element.
/// </summary>
string AutomationId();
Task<string> AutomationIdAsync();

/// <summary>
/// Waits until the element becomes clickable/interactable.
/// </summary>
void WaitForElementIsClickable();
Task WaitForElementIsClickableAsync();

/// <summary>
/// Waits until the element exists.
/// </summary>
void WaitForElementExist();
Task WaitForElementExistAsync();

/// <summary>
/// Checks if the element exists.
/// </summary>
bool IsElementExist();
Task<bool> IsElementExistAsync();

/// <summary>
/// Checks if the element is clickable.
/// </summary>
bool IsElementClicable();
Task<bool> IsElementClickableAsync();

/// <summary>
/// Checks if the element exists for a period of time.
/// </summary>
/// <param name="status">Expected status.</param>
/// <param name="time">Time in seconds to wait.</param>
bool WaitElementExistStatusForTime(bool status, int time);
Task<bool> WaitElementExistStatusForTimeAsync(bool status, int time);

/// <summary>
/// Checks if the element is clickable for a period of time.
/// </summary>
/// <param name="status">Expected status.</param>
/// <param name="time">Time in seconds to wait.</param>
bool WaitElementClickableStatusForTime(bool status, int time = UsabilityTimeLimits.ApplicationLoadLimit);
Task<bool> WaitElementClickableStatusForTimeAsync(bool status, int time = UsabilityTimeLimits.ApplicationLoadLimit);

/// <summary>
/// Returns the rectangle of the element.
/// </summary>
Rectangle GetRectangle();
Task<Rectangle> GetRectangleAsync();

/// <summary>
/// Returns the runtime ID of the element.
/// </summary>
string GetRuntimeId();
Task<string> GetRuntimeIdAsync();
}
}
Loading
Loading