diff --git a/sources/EncodingChecker.GuiSmoke/EcGuiDriver.cs b/sources/EncodingChecker.GuiSmoke/EcGuiDriver.cs
index 31c6acd..9752b23 100644
--- a/sources/EncodingChecker.GuiSmoke/EcGuiDriver.cs
+++ b/sources/EncodingChecker.GuiSmoke/EcGuiDriver.cs
@@ -105,9 +105,9 @@ private AutomationElement OpenSelectedReview(int expectedFiles)
SetToggle(MainWindow, "chkSelectDeselectAll", true);
WaitUntil(
() => CheckedResultCount() == expectedFiles,
- $"Select all did not check {expectedFiles} result row(s). " +
- $"Observed {CheckedResultCount()} checked row(s). " +
- DescribeResultItems());
+ () => $"Select all did not check {expectedFiles} result row(s). "
+ + $"Observed {CheckedResultCount()} checked row(s). "
+ + DescribeResultItems());
Invoke(MainWindow, "btnConvert");
return WaitForReview();
@@ -200,10 +200,13 @@ internal bool ReviewContainsControl(AutomationElement review, string automationI
/// prove the label exists. What matters is the wording a reader actually sees, so
/// this reads the rendered text rather than a control's presence.
///
- internal string ReviewText(AutomationElement review) =>
+ internal string ReviewText(AutomationElement review) => VisibleText(review);
+
+ /// Every non-blank name under an element, joined one per line.
+ private static string VisibleText(AutomationElement root) =>
string.Join(
"\n",
- review.FindAll(TreeScope.Descendants, Condition.TrueCondition)
+ root.FindAll(TreeScope.Descendants, Condition.TrueCondition)
.Cast()
.Select(element => element.Current.Name)
.Where(name => !string.IsNullOrWhiteSpace(name)));
@@ -215,13 +218,7 @@ internal string ReviewText(AutomationElement review) =>
/// alone would not. Waiting is a different job - see , which
/// reads only the status bar and does not throw.
///
- internal string StatusText() =>
- string.Join(
- "\n",
- MainWindow.FindAll(TreeScope.Descendants, Condition.TrueCondition)
- .Cast()
- .Select(element => element.Current.Name)
- .Where(name => !string.IsNullOrWhiteSpace(name)));
+ internal string StatusText() => VisibleText(MainWindow);
///
/// Starts the conversion and cancels it once the status bar shows progress.
@@ -238,9 +235,8 @@ internal void ProceedThenCancel(AutomationElement review, Func writingHasB
Invoke(review, "btnProceedConversion");
WaitUntil(() => !WindowExists(handle), "The conversion review did not close.");
- // Timed against real progress rather than a sleep, so the phase does not depend
- // on how fast the machine converts. Cancelling before the first write would
- // exercise the declined-review path instead, which phase A already covers.
+ // Cancelling before the first write would exercise the declined-review path
+ // instead, which phase A already covers.
WaitForOperationOutcome(
() => writingHasBegun() || ConversionHasFinished(),
"The conversion neither began writing nor reported that it had finished.");
@@ -285,7 +281,7 @@ private void CancelAndConfirmStopped()
// at. Progress would then depend on the button disappearing rather than
// on the run reporting, and a button that lingered after the run ended
// would keep being refused with the answer already on screen.
- if (StatusLine() is string status && IsFinalConversionStatus(status))
+ if (FinalConversionStatus() is string status)
return status;
if (!pressAttempted)
@@ -414,9 +410,7 @@ internal void WaitForStatus(string fragment)
/// any final conversion status. A status outlives the action that wrote it - the
/// window clears it only when the next action starts - so accepting any of them lets
/// a wait be satisfied by the previous action's report and return before the current
- /// one has finished. Every phase drives one action per window today, which is the
- /// only reason that has not bitten; it is the shape EC-26 was about, in the one
- /// helper that fix did not reach.
+ /// one has finished, so each caller names the headline its own action produces.
///
private void WaitForMainReady(string expectedHeadline)
{
@@ -434,8 +428,7 @@ private void WaitForMainReady(string expectedHeadline)
throw Expired(
$"EncodingChecker did not go idle: no '{expectedHeadline}' was reported."
- + Safely(() => " The status showed: " + StatusText(),
- " The status could not be read")
+ + DescribeStatusSafely()
+ DescribeIdleState(),
lastError);
}
@@ -493,8 +486,7 @@ private void WaitForOperationOutcome(Func evidence, string what)
}
throw Expired(
- what + Safely(() => " The status showed: " + StatusText(),
- " The status could not be read"),
+ what + DescribeStatusSafely(),
lastError);
}
@@ -525,12 +517,7 @@ private bool ScanHasFinished() =>
if (bar is null)
return null;
- return string.Join(
- "\n",
- bar.FindAll(TreeScope.Descendants, Condition.TrueCondition)
- .Cast()
- .Select(element => element.Current.Name)
- .Where(name => !string.IsNullOrWhiteSpace(name)));
+ return VisibleText(bar);
}
catch (Exception ex) when (
ex is ElementNotAvailableException or InvalidOperationException or COMException)
@@ -539,19 +526,19 @@ private bool ScanHasFinished() =>
}
}
+ private bool ConversionHasFinished() => FinalConversionStatus() is not null;
+
+ ///
+ /// The status a finished run reported, or null while one is still running.
+ ///
+ private string? FinalConversionStatus() =>
+ StatusLine() is string status && IsFinalConversionStatus(status) ? status : null;
+
///
/// Every way a conversion or preview can end writes one of these, including the paths
/// where nothing was modified. Matching the headline rather than the counts keeps this
/// independent of what the run actually did.
///
- private bool ConversionHasFinished()
- {
- if (StatusLine() is not string status)
- return false;
-
- return IsFinalConversionStatus(status);
- }
-
private static bool IsFinalConversionStatus(string status) =>
status.Contains("Conversion complete", StringComparison.Ordinal) ||
status.Contains("Conversion stopped", StringComparison.Ordinal) ||
@@ -590,6 +577,10 @@ private AutomationElement WaitForReview(int previousHandle = 0)
/// important one. A listing that fails says why, alongside that error rather than
/// instead of it.
///
+ private string DescribeStatusSafely() =>
+ Safely(() => " The status showed: " + StatusText(),
+ " The status could not be read");
+
private string DescribeWindowsSafely() =>
Safely(() => " EC exposed these windows: " + DescribeTopLevelWindows(),
" The windows could not be listed either");
@@ -616,20 +607,10 @@ private static string Safely(Func describe, string whenItFails)
FindProcessElementById("ConversionConfirmationForm") ??
FindProcessElementByTitle("Review conversion");
- private int ResultCount()
- {
- AutomationElement? list = FindById(MainWindow, "lstResults");
-
- if (list is null)
- return 0;
-
- AutomationElementCollection children = list.FindAll(
- TreeScope.Children, Condition.TrueCondition);
-
- return children.Cast().Count(element =>
- element.Current.ControlType is var type &&
- (type == ControlType.DataItem || type == ControlType.ListItem));
- }
+ private int ResultCount() =>
+ FindById(MainWindow, "lstResults") is AutomationElement list
+ ? ResultItems(list).Count()
+ : 0;
private void SetRefusedFileChecked(
AutomationElement review,
@@ -772,8 +753,7 @@ private void SetToggle(
toggle.Toggle();
WaitUntil(
- () => ((TogglePattern)element.GetCurrentPattern(TogglePattern.Pattern))
- .Current.ToggleState == (value ? ToggleState.On : ToggleState.Off),
+ () => toggle.Current.ToggleState == (value ? ToggleState.On : ToggleState.Off),
$"'{automationId}' did not reach the requested state.");
}
@@ -1011,7 +991,20 @@ private static AutomationElement WaitForElement(
return null;
}
- private static void WaitUntil(Func predicate, string timeoutMessage)
+ private static void WaitUntil(Func predicate, string timeoutMessage) =>
+ WaitUntil(predicate, () => timeoutMessage);
+
+ ///
+ /// Waits, building the failure message only if there is a failure to describe.
+ ///
+ ///
+ /// A message assembled up front runs whatever it interpolates on every passing
+ /// call, and an automation call in there can fail a wait whose predicate was
+ /// satisfied. It also describes the state before the wait rather than when it
+ /// gave up. Building it here fixes both, and routes it through Safely so a
+ /// description that throws cannot replace the timeout it exists to explain.
+ ///
+ private static void WaitUntil(Func predicate, Func timeoutMessage)
{
if (WaitFor(
() => predicate() ? new object() : null,
@@ -1021,7 +1014,9 @@ private static void WaitUntil(Func predicate, string timeoutMessage)
return;
}
- throw Expired(timeoutMessage, lastError);
+ throw Expired(
+ Safely(timeoutMessage, "The wait expired and could not be described"),
+ lastError);
}
///