Skip to content

Commit e265d4b

Browse files
authored
Refactor element handling and browser interaction (#149)
* Refactor element handling and browser interaction Replaced direct XPath-based element retrieval with a new `GetElement` method, supporting both `By` selectors and `Func<HtmlDocument, HtmlNode?>` delegates. Updated `Click` and `Input` methods to accept `IWebElement` directly, reducing redundancy and improving maintainability. Removed deprecated `WaitPageLoaded` and `WaitPageChanged` methods in favor of more generic `Wait` methods. Enhanced error handling with detailed messages for `WebDriverTimeoutException`. Updated the `Retry` class to include more descriptive timeout errors. Refactored all commands to use the new `GetElement` method and streamlined interaction logic. Removed redundant code and unused methods, improving readability and maintainability. Updated the `IChromeBrowser` interface to reflect these changes. Performed general code cleanup, including removing unused variables, redundant comments, and unnecessary method calls. * Enhance debugging and reliability in ChromeBrowser - Added `[CallerArgumentExpression]` to `Wait` and `GetElement` methods for improved error reporting and debugging. - Updated `Wait` in `ChromeBrowser.cs` to include better error handling and context in timeout exceptions. - Modified `Click` to include a `Wait` call for ensuring specific conditions (e.g., visibility of the `logo` element). - Added a new `GetElement` overload in `IChromeBrowser.cs` supporting `By` locators with `[CallerArgumentExpression]`. - Updated `Wait` signature in `IChromeBrowser.cs` to align with the implementation in `ChromeBrowser.cs`. - Introduced `IDelayService` in `LoginTask.cs` to add delays for smoother execution during the login process. - Improved maintainability and reliability across the codebase by enhancing error handling and debugging capabilities. * Add WaitPageChanged method for reliable page transitions Introduced a reusable `WaitPageChanged` method in `ChromeBrowser.cs` and `IChromeBrowser.cs` to handle URL changes and ensure pages are fully loaded. Updated various commands (`LoginCommand.cs`, `HandleUpgradeCommand.cs`, `ToBuildingByLocationCommand.cs`, `ToDorfCommand.cs`) to use this method after navigation actions. Removed redundant `Wait` logic from the `Click` method in `ChromeBrowser.cs`, consolidating it into `WaitPageChanged` for better modularity. Enhanced error handling in `WaitPageChanged` to provide detailed messages for failures. * Refactor task handling in BuildingsModified and JobsModified Refactored the `BuildingsModified` method to improve readability by reordering early exit checks and command executions. In the `JobsModified` method, moved `_taskManager.AddOrUpdate` for `UpgradeBuildingTask.Task` to the beginning of the method to ensure proper task handling before other operations. Removed redundant `_taskManager.AddOrUpdate` call at the end of `JobsModified` to eliminate duplication and improve clarity. * Refactor Wait method and improve error message Simplified the `Wait` method call by condensing the lambda expression into a single line for better readability. Enhanced the error message in the `if (result.IsFailed)` block to include the `CurrentUrl` property, providing additional context for debugging when the URL change wait fails. * fix new building on military&resource tab cannot be built * Refactored the retrieval of the "train button" element to use an asynchronous `GetElement` operation with a document parser (`TrainTroopParser.GetTrainButton`) and a cancellation token. * Refactor train button retrieval logic Replaced the old `TrainTroopParser.GetTrainButton` logic with a more robust approach using `browser.GetElement` and a lambda function. * Refactor error handling and improve logging Enhanced error messages across the codebase for better clarity. Refactored the `Retry` class to simplify and standardize error handling using a new `Retry.Error` property. Introduced the `WithError` method for chaining additional error context. Improved logging to provide more context during execution, including adventure details and specific failure points. Simplified XPath handling by using `nodeGenerator` functions for better readability. Updated tab index validation logic in `SwitchTabCommand.cs` to ensure accurate error reporting. Refactored exception handling in `ChromeBrowser` to use the new `Retry.Error` mechanism. Removed redundant error-handling methods and standardized error reporting with the `Result.Fail(errors).WithError(...)` pattern. Made namespace-specific changes to ensure consistency in error handling and logging. Improved debugging support and code readability by reducing redundancy and adding detailed error messages. Added additional wait logic to enhance automation reliability. * Refactor `Skip` error handling for dynamic messages Refactored the `Skip` class to replace static error instances with a generic `Skip.Error` that supports dynamic error messages via the `WithError` method. Updated all references to use the new approach, enabling more descriptive and context-specific error handling. Removed redundant static `Skip` error instances to improve maintainability and reduce code duplication. * Refactor error handling for consistency Refactored the error handling mechanism across the codebase to use `WithError` and `WithErrors` methods for detailed and extensible error messages. - Replaced specific `Stop` error methods with a generic `Stop.Error` instance, allowing customization via `WithError`. - Made `Stop` constructor private to enforce the use of `Error`. - Updated `Skip` to include a default message and support `WithErrors`. - Replaced hardcoded error messages in various files with the new `WithError` and `WithErrors` methods for better clarity. - Removed redundant error methods like `JobNotAvailable` in `UpgradeBuildingError`. - Improved error propagation in `NPCTask` and `UpgradeBuildingTask`. These changes improve code maintainability and ensure consistent, user-friendly error messages throughout the application.
1 parent 950b401 commit e265d4b

37 files changed

Lines changed: 293 additions & 316 deletions

MainCore.Test/Behaviors/ErrorLoggingBehaviorTest.cs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,6 @@ public async Task ErrorLoggingBehaviorShouldLogCorrectErrorMessage(Result result
6666
new List<object[]>
6767
{
6868
new object[] { Result.Fail(Cancel.Error), "Pause button is pressed" },
69-
new object[] { Result.Fail(Skip.VillageNotFound), "Village not found" },
70-
new object[] { Result.Fail(Stop.EnglishRequired("abcxyz")), "Cannot parse abcxyz. Is language English ?. Bot must stop" },
7169
};
7270
}
7371
}

MainCore/Behaviors/AccountTaskBehavior.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,14 +30,14 @@ public override async ValueTask<TResponse> HandleAsync(TRequest request, Cancell
3030
{
3131
if (!LoginParser.IsLoginPage(_browser.Html))
3232
{
33-
return (TResponse)Stop.NotTravianPage;
33+
return (TResponse)Stop.Error.WithError("Travian is not ingame nor login page. Please check browser");
3434
}
3535

3636
if (request is not LoginTask.Task)
3737
{
3838
_taskManager.AddOrUpdate<LoginTask.Task>(new(accountId), first: true);
3939
request.ExecuteAt = request.ExecuteAt.AddSeconds(1);
40-
return (TResponse)Skip.AccountLogout;
40+
return (TResponse)Skip.Error.WithError("Account is logout. Re-login now");
4141
}
4242
}
4343

MainCore/Commands/Features/ClaimQuest/ClaimQuestCommand.cs

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,14 +34,22 @@ private static async ValueTask<Result> HandleAsync(
3434
quest = QuestParser.GetQuestCollectButton(browser.Html);
3535
if (quest is null) return Result.Ok();
3636

37-
result = await browser.Click(By.XPath(quest.XPath), cancellationToken);
37+
var (_, isFailed, element, errors) = await browser.GetElement(By.XPath(quest.XPath), cancellationToken);
38+
if (isFailed) return Result.Fail(errors);
39+
40+
result = await browser.Click(element, cancellationToken);
3841
if (result.IsFailed) return result;
3942
continue;
4043
}
44+
else
45+
{
46+
var (_, isFailed, element, errors) = await browser.GetElement(By.XPath(quest.XPath), cancellationToken);
47+
if (isFailed) return Result.Fail(errors);
4148

42-
result = await browser.Click(By.XPath(quest.XPath), cancellationToken);
43-
if (result.IsFailed) return result;
44-
await delayService.DelayClick(cancellationToken);
49+
result = await browser.Click(element, cancellationToken);
50+
if (result.IsFailed) return result;
51+
await delayService.DelayClick(cancellationToken);
52+
}
4553
}
4654
while (QuestParser.IsQuestClaimable(browser.Html));
4755

MainCore/Commands/Features/ClaimQuest/ToQuestPageCommand.cs

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,20 +12,19 @@ private static async ValueTask<Result> HandleAsync(
1212
IChromeBrowser browser,
1313
CancellationToken cancellationToken)
1414
{
15-
var adventure = QuestParser.GetQuestMaster(browser.Html);
16-
if (adventure is null) return Retry.ButtonNotFound("quest master");
15+
var (_, isFailed, element, errors) = await browser.GetElement(doc => QuestParser.GetQuestMaster(doc), cancellationToken);
16+
if (isFailed) return Result.Fail(errors);
17+
18+
var result = await browser.Click(element, cancellationToken);
19+
if (result.IsFailed) return result;
1720

1821
static bool TableShow(IWebDriver driver)
1922
{
2023
var doc = new HtmlDocument();
2124
doc.LoadHtml(driver.PageSource);
2225
return QuestParser.IsQuestPage(doc);
2326
}
24-
25-
var result = await browser.Click(By.XPath(adventure.XPath), cancellationToken);
26-
if (result.IsFailed) return result;
27-
28-
result = await browser.WaitPageChanged("tasks", TableShow, cancellationToken);
27+
result = await browser.Wait(TableShow, cancellationToken);
2928
if (result.IsFailed) return result;
3029

3130
return Result.Ok();

MainCore/Commands/Features/CompleteImmediately/CompleteImmediatelyCommand.cs

Lines changed: 6 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -16,26 +16,16 @@ private static async ValueTask<Result> HandleAsync(
1616

1717
if (oldQueueCount == 0) return Result.Ok();
1818

19-
var completeNowButton = CompleteImmediatelyParser.GetCompleteButton(browser.Html);
20-
if (completeNowButton is null) return Retry.ButtonNotFound("complete now");
19+
var (_, isFailed, element, errors) = await browser.GetElement(doc => CompleteImmediatelyParser.GetCompleteButton(doc), cancellationToken);
20+
if (isFailed) return Result.Fail(errors);
2121

22-
var result = await browser.Click(By.XPath(completeNowButton.XPath), cancellationToken);
22+
var result = await browser.Click(element, cancellationToken);
2323
if (result.IsFailed) return result;
2424

25-
static bool ConfirmShown(IWebDriver driver)
26-
{
27-
var doc = new HtmlDocument();
28-
doc.LoadHtml(driver.PageSource);
29-
var confirmButton = CompleteImmediatelyParser.GetConfirmButton(doc);
30-
return confirmButton is not null;
31-
}
32-
result = await browser.Wait(ConfirmShown, cancellationToken);
33-
if (result.IsFailed) return result;
34-
35-
var confirmButton = CompleteImmediatelyParser.GetConfirmButton(browser.Html);
36-
if (confirmButton is null) return Retry.ButtonNotFound("confirm complete now");
25+
(_, isFailed, element, errors) = await browser.GetElement(doc => CompleteImmediatelyParser.GetConfirmButton(doc), cancellationToken);
26+
if (isFailed) return Result.Fail(errors);
3727

38-
result = await browser.Click(By.XPath(confirmButton.XPath), cancellationToken);
28+
result = await browser.Click(element, cancellationToken);
3929
if (result.IsFailed) return result;
4030

4131
static bool QueueDifferent(IWebDriver driver, int oldQueueCount)

MainCore/Commands/Features/DisableContextualHelp/DisableContextualHelpCommand.cs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,16 @@ private static async ValueTask<Result> HandleAsync(
1313
CancellationToken cancellationToken
1414
)
1515
{
16-
var option = OptionParser.GetHideContextualHelpOption(browser.Html);
17-
if (option is null) return Retry.NotFound("hide contextual help", "option");
16+
var (_, isFailed, element, errors) = await browser.GetElement(doc => OptionParser.GetHideContextualHelpOption(doc), cancellationToken);
17+
if (isFailed) return Result.Fail(errors);
1818

19-
var result = await browser.Click(By.XPath(option.XPath), cancellationToken);
19+
var result = await browser.Click(element, cancellationToken);
2020
if (result.IsFailed) return result;
2121

22-
var button = OptionParser.GetSubmitButton(browser.Html);
23-
if (button is null) return Retry.ButtonNotFound("submit");
22+
(_, isFailed, element, errors) = await browser.GetElement(doc => OptionParser.GetSubmitButton(doc), cancellationToken);
23+
if (isFailed) return Result.Fail(errors);
2424

25-
result = await browser.Click(By.XPath(button.XPath), cancellationToken);
25+
result = await browser.Click(element, cancellationToken);
2626
if (result.IsFailed) return result;
2727

2828
return Result.Ok();

MainCore/Commands/Features/DisableContextualHelp/ToOptionsPageCommand.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,10 @@ private static async ValueTask<Result> HandleAsync(
1313
CancellationToken cancellationToken
1414
)
1515
{
16-
var button = OptionParser.GetOptionButton(browser.Html);
17-
if (button is null) return Retry.ButtonNotFound("options");
16+
var (_, isFailed, element, errors) = await browser.GetElement(doc => OptionParser.GetOptionButton(doc), cancellationToken);
17+
if (isFailed) return Result.Fail(errors);
1818

19-
var result = await browser.Click(By.XPath(button.XPath), cancellationToken);
19+
var result = await browser.Click(element, cancellationToken);
2020
if (result.IsFailed) return result;
2121

2222
return Result.Ok();

MainCore/Commands/Features/LoginCommand.cs

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,22 +13,25 @@ private static async ValueTask<Result> HandleAsync(
1313
{
1414
if (LoginParser.IsIngamePage(browser.Html)) return Result.Ok();
1515

16-
var buttonNode = LoginParser.GetLoginButton(browser.Html);
17-
if (buttonNode is null) return Retry.ButtonNotFound("login");
18-
var usernameNode = LoginParser.GetUsernameInput(browser.Html);
19-
if (usernameNode is null) return Retry.TextboxNotFound("username");
20-
var passwordNode = LoginParser.GetPasswordInput(browser.Html);
21-
if (passwordNode is null) return Retry.TextboxNotFound("password");
22-
2316
var (username, password) = GetLoginInfo(command.AccountId, context);
2417

2518
Result result;
26-
result = await browser.Input(By.XPath(usernameNode.XPath), username, cancellationToken);
19+
20+
var (_, isFailed, element, errors) = await browser.GetElement(doc => LoginParser.GetUsernameInput(doc), cancellationToken);
21+
if (isFailed) return Result.Fail(errors);
22+
result = await browser.Input(element, username, cancellationToken);
2723
if (result.IsFailed) return result;
28-
result = await browser.Input(By.XPath(passwordNode.XPath), password, cancellationToken);
24+
25+
(_, isFailed, element, errors) = await browser.GetElement(doc => LoginParser.GetPasswordInput(doc), cancellationToken);
26+
if (isFailed) return Result.Fail(errors);
27+
result = await browser.Input(element, password, cancellationToken);
2928
if (result.IsFailed) return result;
30-
result = await browser.Click(By.XPath(buttonNode.XPath), cancellationToken);
29+
30+
(_, isFailed, element, errors) = await browser.GetElement(doc => LoginParser.GetLoginButton(doc), cancellationToken);
31+
if (isFailed) return Result.Fail(errors);
32+
result = await browser.Click(element, cancellationToken);
3133
if (result.IsFailed) return result;
34+
3235
result = await browser.WaitPageChanged("dorf", cancellationToken);
3336
if (result.IsFailed) return result;
3437

MainCore/Commands/Features/NpcResource/NpcResourceCommand.cs

Lines changed: 15 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,6 @@ private static async ValueTask<Result> HandleAsync(
7474
if (result.IsFailed) return result;
7575

7676
await Task.Delay(5000);
77-
result = await browser.WaitPageLoaded(cancellationToken);
78-
if (result.IsFailed) return result;
7977

8078
browser.Logger.Information("After NPC:");
8179
LogResource(browser);
@@ -116,8 +114,11 @@ private static bool CanStart(IChromeBrowser browser, AppDbContext context, Villa
116114

117115
private static async Task<Result> OpenNPCDialog(IChromeBrowser browser, CancellationToken cancellationToken)
118116
{
119-
var button = NpcResourceParser.GetExchangeResourcesButton(browser.Html);
120-
if (button is null) return Retry.ButtonNotFound("Exchange resources");
117+
var (_, isFailed, element, errors) = await browser.GetElement(doc => NpcResourceParser.GetExchangeResourcesButton(doc), cancellationToken);
118+
if (isFailed) return Result.Fail(errors);
119+
120+
var result = await browser.Click(element, cancellationToken);
121+
if (result.IsFailed) return result;
121122

122123
static bool DialogShown(IWebDriver driver)
123124
{
@@ -126,9 +127,6 @@ static bool DialogShown(IWebDriver driver)
126127
return NpcResourceParser.IsNpcDialog(doc);
127128
}
128129

129-
var result = await browser.Click(By.XPath(button.XPath), cancellationToken);
130-
if (result.IsFailed) return result;
131-
132130
result = await browser.Wait(DialogShown, cancellationToken);
133131
if (result.IsFailed) return result;
134132

@@ -141,7 +139,10 @@ private static async Task<Result> InputAmount(IChromeBrowser browser, long[] val
141139

142140
for (var i = 0; i < 4; i++)
143141
{
144-
var result = await browser.Input(By.XPath(inputs[i].XPath), $"{values[i]}", cancellationToken);
142+
var (_, isFailed, element, errors) = await browser.GetElement(By.XPath(inputs[i].XPath), cancellationToken);
143+
if (isFailed) return Result.Fail(errors);
144+
145+
var result = await browser.Input(element, $"{values[i]}", cancellationToken);
145146
if (result.IsFailed) return result;
146147
}
147148

@@ -183,46 +184,21 @@ private static long[] GetRatio(Dictionary<VillageSettingEnums, int> settings)
183184

184185
private static async Task<Result> Distribute(IChromeBrowser browser, CancellationToken cancellationToken)
185186
{
186-
var result = await browser.Wait(driver =>
187-
{
188-
var doc = new HtmlDocument();
189-
doc.LoadHtml(driver.PageSource);
190-
var button = NpcResourceParser.GetDistributeButton(browser.Html);
191-
if (button is null) return false;
187+
var (_, isFailed, element, errors) = await browser.GetElement(doc => NpcResourceParser.GetDistributeButton(doc), cancellationToken);
188+
if (isFailed) return Result.Fail(errors);
192189

193-
var elements = driver.FindElements(By.XPath(button.XPath));
194-
return elements.Count > 0 && elements[0].Enabled;
195-
}, cancellationToken);
196-
if (result.IsFailed) return result;
197-
198-
var button = NpcResourceParser.GetDistributeButton(browser.Html);
199-
if (button is null) return Retry.ButtonNotFound("distribute");
200-
201-
result = await browser.Click(By.XPath(button.XPath), cancellationToken);
190+
var result = await browser.Click(element, cancellationToken);
202191
if (result.IsFailed) return result;
203192

204193
return Result.Ok();
205194
}
206195

207196
private static async Task<Result> Redeem(IChromeBrowser browser, CancellationToken cancellationToken)
208197
{
209-
var result = await browser.Wait(driver =>
210-
{
211-
var doc = new HtmlDocument();
212-
doc.LoadHtml(driver.PageSource);
213-
var button = NpcResourceParser.GetRedeemButton(doc);
214-
215-
if (button is null) return false;
216-
217-
var elements = driver.FindElements(By.XPath(button.XPath));
218-
return elements.Count > 0 && elements[0].Enabled;
219-
}, cancellationToken);
220-
if (result.IsFailed) return result;
221-
222-
var button = NpcResourceParser.GetRedeemButton(browser.Html);
223-
if (button is null) return Retry.ButtonNotFound("redeem");
198+
var (_, isFailed, element, errors) = await browser.GetElement(doc => NpcResourceParser.GetRedeemButton(doc), cancellationToken);
199+
if (isFailed) return Result.Fail(errors);
224200

225-
result = await browser.Click(By.XPath(button.XPath), cancellationToken);
201+
var result = await browser.Click(element, cancellationToken);
226202
if (result.IsFailed) return result;
227203

228204
return Result.Ok();

MainCore/Commands/Features/StartAdventure/ExploreAdventureCommand.cs

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,26 +13,28 @@ private static async ValueTask<Result> HandleAsync(
1313
ILogger logger,
1414
CancellationToken cancellationToken)
1515
{
16-
if (!AdventureParser.CanStartAdventure(browser.Html)) return Skip.NoAdventure;
16+
if (!AdventureParser.CanStartAdventure(browser.Html)) return Skip.Error.WithError("No adventure available");
1717

1818
var adventureButton = AdventureParser.GetAdventureButton(browser.Html);
19-
if (adventureButton is null) return Retry.ButtonNotFound("adventure");
19+
if (adventureButton is null) return Retry.Error.WithError($"Failed to find adventure button");
20+
2021
logger.Information("Start adventure {Adventure}", AdventureParser.GetAdventureInfo(adventureButton));
2122

23+
var (_, isFailed, element, errors) = await browser.GetElement(By.XPath(adventureButton.XPath), cancellationToken);
24+
if (isFailed) return Result.Fail(errors).WithError($"Failed to find adventure button [{adventureButton.XPath}]");
25+
26+
var result = await browser.Click(element, cancellationToken);
27+
if (result.IsFailed) return result;
28+
2229
static bool ContinueShow(IWebDriver driver)
2330
{
2431
var doc = new HtmlDocument();
2532
doc.LoadHtml(driver.PageSource);
2633
var continueButton = AdventureParser.GetContinueButton(doc);
2734
return continueButton is not null;
2835
}
29-
30-
var result = await browser.Click(By.XPath(adventureButton.XPath), cancellationToken);
31-
if (result.IsFailed) return result;
32-
3336
result = await browser.Wait(ContinueShow, cancellationToken);
3437
if (result.IsFailed) return result;
35-
3638
return Result.Ok();
3739
}
3840
}

0 commit comments

Comments
 (0)