using System.IO; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Threading; using XFEToolBox.Client.Utilities; using XFEToolBox.Client.Views.Windows; namespace XFEToolBox.Client.Wpf.Test; public static class QuickSearchInputAlignmentTests { [Test] public static void QuickSearchHintSharesTextMetricsAndCaretOriginWithInput() { Exception? failure = null; var thread = new Thread(() => { var root = Path.Combine(Path.GetTempPath(), "xfetoolbox-search-input-" + Guid.NewGuid().ToString("N")); var scope = Path.Combine(root, "scope"); var cache = Path.Combine(root, "cache"); FileSearchRuntime? runtime = null; CommandPaletteWindow? window = null; try { Directory.CreateDirectory(scope); SynchronizationContext.SetSynchronizationContext(new DispatcherSynchronizationContext(Dispatcher.CurrentDispatcher)); runtime = new FileSearchRuntime(cache); runtime.SetScopeAsync(scope).GetAwaiter().GetResult(); window = new CommandPaletteWindow(runtime) { ShowActivated = false, ShowInTaskbar = false, Topmost = false, Left = -10000, Top = -10000, WindowStartupLocation = WindowStartupLocation.Manual }; window.Resources.MergedDictionaries.Add(new ResourceDictionary { Source = new Uri("/XFEToolBox.WpfCore;component/Resources/Style/ToolThemeResources.xaml", UriKind.Relative) }); window.Resources.MergedDictionaries.Add(new ResourceDictionary { Source = new Uri("/XFEToolBox;component/Resources/Style/MainStyle.xaml", UriKind.Relative) }); window.Show(); var input = (TextBox)window.FindName("SearchBox"); var hint = (TextBlock)window.FindName("SearchHint"); var surface = (Border)window.FindName("SearchInputSurface"); var clear = (Button)window.FindName("ClearSearchButton"); input.Focus(); Pump(); window.UpdateLayout(); Ensure(hint.FontFamily.Equals(input.FontFamily) && hint.FontSize == input.FontSize && hint.FontWeight == input.FontWeight, "提示文本和输入文字仍使用不同字体度量,光标和 Hint 基线不一致。"); var rect = input.GetRectFromCharacterIndex(0); Ensure(!rect.IsEmpty && rect.Height > 0, "输入框空文本光标没有有效的布局尺寸。"); var caretTop = input.TranslatePoint(rect.TopLeft, surface); var hintTop = hint.TranslatePoint(new Point(0, 0), surface); Ensure(Math.Abs(caretTop.X - hintTop.X) <= 1, $"空文本光标与提示左侧未对齐:{caretTop.X:F2}/{hintTop.X:F2}。"); Ensure(Math.Abs(caretTop.Y + rect.Height / 2 - (hintTop.Y + hint.ActualHeight / 2)) <= 1.5, $"光标和提示垂直中心未对齐:{caretTop.Y + rect.Height / 2:F2}/{hintTop.Y + hint.ActualHeight / 2:F2}。"); Ensure(input.BorderThickness == new Thickness(0) && input.FocusVisualStyle is null && !clear.Focusable && !clear.IsTabStop && clear.BorderThickness == new Thickness(0), "整体焦点边框修复回退,或清空按钮重新获得选中边框。"); input.Text = "中文 report"; Pump(); Ensure(hint.Visibility == Visibility.Collapsed, "有输入时提示仍与文字重叠。"); } catch (Exception exception) { failure = exception; } finally { window?.Close(); runtime?.DisposeAsync().AsTask().GetAwaiter().GetResult(); Dispatcher.CurrentDispatcher.InvokeShutdown(); // This fixture only owns its known cache files and the empty scope directory. if (Directory.Exists(cache)) { foreach (var file in Directory.EnumerateFiles(cache)) File.Delete(file); Directory.Delete(cache); } if (Directory.Exists(scope)) Directory.Delete(scope); if (Directory.Exists(root)) Directory.Delete(root); } }) { IsBackground = true }; thread.SetApartmentState(ApartmentState.STA); thread.Start(); Ensure(thread.Join(TimeSpan.FromSeconds(30)), "搜索输入框对齐测试超时。"); if (failure is not null) { Console.WriteLine(failure); throw new InvalidOperationException("搜索输入框 Hint/光标回归失败:" + failure.Message, failure); } } private static void Pump() { var frame = new DispatcherFrame(); Dispatcher.CurrentDispatcher.BeginInvoke(DispatcherPriority.ApplicationIdle, new Action(() => frame.Continue = false)); Dispatcher.PushFrame(frame); } private static void Ensure(bool condition, string message) { if (!condition) throw new InvalidOperationException(message); } }