diff --git a/CHANGELOG.md b/CHANGELOG.md index cb29594..af67866 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -74,6 +74,18 @@ is not part of this repository. - "Show the list" on the machine overview is a blue button, so it is easy to find the first time the window opens. - In the plan panel, the name of the entry stands out in bold in the title and in every step. +- The search box has a magnifier and a lighter fill. Before, it was darker than the window. +- Every text box has a light blue line under it while you type in it. Before, the line took the + accent colour of your Windows. +- A mistake in the query, or a note about something still being read, no longer takes a line under + the search box, so the window no longer moves when you start typing. While you are in the box, + a panel under it shows every message in full, over the filters. When you leave the box, the box + keeps its red edge and a short form of the message at its right end. +- The column headings are semibold and grey, with a line under them, so they no longer look like + one more row. +- A button that cannot be pressed has no fill, and one that can has a slightly lighter fill than + before. +- The state and startup type marks in the list and in the details panel are a little larger. ### Removed @@ -83,6 +95,7 @@ is not part of this repository. ### Fixed - The details panel no longer stays open over the machine overview. +- The numbers on the machine overview light up under the pointer where you can see it. - Opening the details panel on another entry no longer shows the previous entry's "no longer in the listing" notice. diff --git a/README.md b/README.md index 9c13943..40ab2a5 100644 --- a/README.md +++ b/README.md @@ -491,12 +491,12 @@ the third ends the process. `BetterWindowsServices.exe`, one argument it knows: `--catalogue` opens a developer's sheet that shows every component of the window in every state, and reads nothing from your machine. -Three lists on the switch above the search box - *Services*, *Drivers*, *Everything* - each saying +Three lists on the switch above the search box - *Services*, *Drivers*, *All* - each saying how big it is. The search box takes the query language and suggests as you type. The filter buttons write into the box. *Columns* chooses what the list shows, a right-click on a column heading narrows the list to that value or puts the column away, and the layout you leave is the layout it -opens in. A row's menu previews every operation before offering it, and copies the name, the -display name, the description or everything. *Export...* writes the rows on screen, in the columns +opens in. A row's menu previews every operation before offering it, and copies the name or +everything about the entry, display name and description included. *Export...* writes the rows on screen, in the columns you have on and the order you sorted them into, to a CSV file. Ctrl+C over the list copies everything about the chosen entry. Escape backs out of the innermost thing first. *Donate*, at the right end of the row above the search box, opens the project's support page in your browser - and diff --git a/src/Bws.Gui/App.xaml b/src/Bws.Gui/App.xaml index 3a482d6..1fd9d5d 100644 --- a/src/Bws.Gui/App.xaml +++ b/src/Bws.Gui/App.xaml @@ -107,6 +107,8 @@ templates to add - point 9 of `docs/11` 2.14. --> + + @@ -359,10 +360,13 @@ WHAT AN OPERATION OVER THE SELECTION WOULD DO - a LAYER OVER THE WHOLE WINDOW since 2026-09-02 rather than a cell beside the list, still mutually exclusive with the details panel. LAST IN THIS FILE ON PURPOSE: siblings paint in document order, so one place - higher would have put the status row over its dimming, elevate button and all. + higher would have put the status row over its dimming, elevate button and all. AND ON + THE TOP LAYER since the search row stands on one of its own - a layer outranks the + order, Values.xaml says how that was found. --> diff --git a/src/Bws.Gui/OverviewView.xaml b/src/Bws.Gui/OverviewView.xaml index 69e3bf6..ddee35e 100644 --- a/src/Bws.Gui/OverviewView.xaml +++ b/src/Bws.Gui/OverviewView.xaml @@ -118,37 +118,8 @@ Click="AskThis" ToolTip="{Binding Query}"> - - - - - + - - + + + + + + + + + + + + - + - - - - - - + + + + + + + + + + + + + + + + Width="{Binding ActualWidth, ElementName=SearchField}"> - - - - - + + + + + + + + + + diff --git a/src/Bws.Gui/Themes/Colours.xaml b/src/Bws.Gui/Themes/Colours.xaml index 22e670e..1fdf4ab 100644 --- a/src/Bws.Gui/Themes/Colours.xaml +++ b/src/Bws.Gui/Themes/Colours.xaml @@ -132,6 +132,14 @@ --> #A0A0A0 + + #CCCCCC + + + + + diff --git a/src/Bws.Gui/Themes/SearchField.xaml b/src/Bws.Gui/Themes/SearchField.xaml new file mode 100644 index 0000000..9c2fc1a --- /dev/null +++ b/src/Bws.Gui/Themes/SearchField.xaml @@ -0,0 +1,175 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Bws.Gui/Themes/Spacing.xaml b/src/Bws.Gui/Themes/Spacing.xaml index 80e0e7e..3f46b30 100644 --- a/src/Bws.Gui/Themes/Spacing.xaml +++ b/src/Bws.Gui/Themes/Spacing.xaml @@ -84,6 +84,16 @@ --> 16,0,0,0 + + 10,0,6,0 + 0,4,8,4 + 0,0,10,0 + 0,4,0,0 + 0,8,0,0 0,0,0,8 + #4A4A4A + + + + + + + + + + + + + + + + #65A3D8 + + + diff --git a/src/Bws.Gui/Themes/Text.xaml b/src/Bws.Gui/Themes/Text.xaml index 943846e..ca5572f 100644 --- a/src/Bws.Gui/Themes/Text.xaml +++ b/src/Bws.Gui/Themes/Text.xaml @@ -97,6 +97,19 @@ + + + - 8 + 10 0,1,0,0 + + 0,0,0,1 + + + 14 + + + 420 + + + 0,0,0,2 + + + 0 + 420 @@ -365,4 +384,19 @@ three lines would be 72 and still do not fit. --> 56 + + + 1 + 2 diff --git a/tests/Bws.Architecture.Tests/AppearanceGuards.cs b/tests/Bws.Architecture.Tests/AppearanceGuards.cs index 21b2f0f..fcc33fb 100644 --- a/tests/Bws.Architecture.Tests/AppearanceGuards.cs +++ b/tests/Bws.Architecture.Tests/AppearanceGuards.cs @@ -161,7 +161,11 @@ public sealed class AppearanceGuards // Actions.xaml, 2026-09-23: the write buttons of the action bar and the shield they // wear. Controls.xaml stood two lines under its ceiling. Added here in the same edit as // the file, for the reason every entry here gives. - "Controls.xaml", "Menus.xaml", "Actions.xaml", "Suggestions.xaml", + // + // SearchField.xaml, 2026-09-25: the search field's frame, text box, sentence and panel, + // out of Suggestions.xaml when the size ratchet counted it among the files near the + // ceiling. Added here in the same edit as the file. + "Controls.xaml", "Menus.xaml", "Actions.xaml", "Suggestions.xaml", "SearchField.xaml", // PlanLines.xaml left Plan.xaml on 2026-09-07 when the markup ratchet fired on THAT // one - a failure that can be escalated needed a template of its own. Added here in diff --git a/tests/Bws.Gui.Tests/AnswerLineGuards.cs b/tests/Bws.Gui.Tests/AnswerLineGuards.cs index ea2c315..53c5143 100644 --- a/tests/Bws.Gui.Tests/AnswerLineGuards.cs +++ b/tests/Bws.Gui.Tests/AnswerLineGuards.cs @@ -29,23 +29,28 @@ public async Task The_search_box_wears_the_problem_colour_on_its_edge_while_its_ { var (window, model) = await Opened(); - var fine = RedInsideTheBox(window, "fine"); + var (fine, _) = RedInsideTheBox(window, "fine"); WpfHost.On(() => model.QueryText = "stat:runing"); WpfHost.Settled(); - var wrong = RedInsideTheBox(window, "wrong"); + var (wrong, tall) = RedInsideTheBox(window, "wrong"); Assert.True( WpfHost.On(() => Validation.GetHasError(window.Search.Box)), "the binding on the search box never heard that the query is wrong - INotifyDataErrorInfo " + "is not reaching Validation.HasError, so no style can colour the edge"); + // HALF THE FIELD'S HEIGHT, NOT A NUMBER - review of PR 21. The edge runs the height of the + // field less its two rounded corners, and a smoothed pixel is not counted, so what it can + // reach follows the field. It asked for more than 20 until then: 22 of a field about 30 tall on + // the machine it was written on, and 18 on the CI runner, whose picture is not kept - why + // it counted four fewer was not measured. Assert.Equal(0, fine); Assert.True( - wrong > 20, - $"only {wrong} pixels of the problem colour were painted inside the search box while it " - + "holds 'stat:runing'. The style sets BorderBrush and the library's template is not drawing it."); + wrong * 2 > tall, + $"only {wrong} pixels of the problem colour down the left edge of a field {tall} pixels " + + "tall while it holds 'stat:runing'. The frame's style sets BorderBrush and it is not reaching the pixel."); WpfHost.On(window.Close); } @@ -147,21 +152,59 @@ private static TextBlock Line(MainWindow window, string automationId) => /// /// The search row drawn at 1000 by 200, the picture left in artifacts/gui, and the problem - /// colour counted inside the box's own rectangle - the line under the box wears the same red, - /// and a count over the whole row would pass on the sentence alone. + /// colour counted in a strip down the LEFT EDGE of the field's frame. + /// + /// The edge rather than the field, since 2026-09-25. The frame draws the red edge now, not + /// the text box's error template - the text box is the middle third of the field - and the short + /// sentence at the field's right end wears the same red, so a count over the whole field would + /// pass on the sentence alone. Nothing but the edge is red at the left: the magnifier is grey. /// - private static int RedInsideTheBox(MainWindow window, string state) + private static (int Red, int Tall) RedInsideTheBox(MainWindow window, string state) { var drawn = Drawn.Of(window.Search, 1000, 200); - var box = drawn.Around(window.Search.Box); + var frame = drawn.Around(window.Search.SearchField); - Assert.True(box.Width > 0 && box.Height > 0, "the search box has no rectangle, so it was never laid out"); + Assert.True(frame.Width > 0 && frame.Height > 0, "the search field has no rectangle, so it was never laid out"); drawn.Save($"search-row-{state}-1000x200.png"); - return drawn.Count(WpfHost.Declared("MeaningRejected"), box); + return (drawn.Count(WpfHost.Declared("MeaningRejected"), new Int32Rect(frame.X, frame.Y, EdgeStrip, frame.Height)), frame.Height); } + /// How far in from the frame's left edge the red is counted - the edge and its antialiasing, short of the magnifier. + private const int EdgeStrip = 4; + + /// + /// THE SEARCH ROW KEEPS ITS HEIGHT WHATEVER THE ANSWER SAYS - owner, 2026-09-25: the window jumped + /// while he typed. tools/gui-probe/typing-jump.ps1 measured it at 31 device pixels on the first + /// character: the sentence about the query took a line of its own under the box. It stands in the + /// field now, and the full text in a panel that measures to nothing, so a row that grows with what + /// the answer says is that fault back. Asked with the longest thing it can say - a mistake, whose + /// sentence lists every value a field accepts. + /// + [Fact] + public async Task The_search_row_keeps_its_height_whatever_the_answer_says() + { + var (window, model) = await Opened(); + + var empty = Height(window); + + WpfHost.On(() => model.QueryText = "stat:runing"); + WpfHost.Settled(); + + Assert.NotEqual(string.Empty, WpfHost.On(() => model.Says.AnswerLine)); + Assert.Equal(empty, Height(window)); + + WpfHost.On(window.Close); + } + + private static double Height(MainWindow window) => WpfHost.On(() => + { + window.Search.Measure(new Size(1000, double.PositiveInfinity)); + + return window.Search.DesiredSize.Height; + }); + /// /// The LOGICAL tree rather than the visual one, which is the difference between this file and /// the six others with a helper of this name: the window here is never shown, so no template diff --git a/tests/Bws.Gui.Tests/ContrastFloors.cs b/tests/Bws.Gui.Tests/ContrastFloors.cs index 6a280eb..04b1392 100644 --- a/tests/Bws.Gui.Tests/ContrastFloors.cs +++ b/tests/Bws.Gui.Tests/ContrastFloors.cs @@ -83,6 +83,15 @@ internal sealed record Floor(double Ratio, string? Against = null); // stronger of the two signals rather than competing with the furniture. ["SurfaceRowLine"] = new(1.25), + // THE LINE UNDER THE COLUMN HEADINGS, 2026-09-25, and it answers to more than the line + // between two rows on purpose: it separates the names of the columns from what is in them, + // and the owner's complaint was that the headings read as one more row. 1.84 measured. + ["SurfaceHeadingRule"] = new(1.5), + + // THE SECOND TEXT COLOUR, 2026-09-25 - the column headings. Fluent's secondary text laid on + // the window, and it is text, so it answers to WCAG's text floor: 10.15 measured. + ["TextSecondary"] = new(ForText), + // THE SCROLLBAR THUMB ASLEEP AND AWAKE, 2026-09-02, AND THE PAIR IS THE POINT. The bar was // one colour at 2.84 and the owner said it was too big and ugly - the width was half of // that and the brightness was the other half. At rest the thumb answers "where am I in the @@ -158,6 +167,12 @@ internal sealed record Floor(double Ratio, string? Against = null); // worst of four surfaces and lives in its own test further down. ["FocusRing"] = new(ForState), + // THE SAME SHAPE FOR THE LINE UNDER A TEXT FIELD WITH THE KEYBOARD IN IT, review of PR 21. + // Against the window here, where it clears at 6.04. It is drawn on the field's own fill, + // which is translucent, so its real floor is every fill composed over the window and over + // the plan sheet - its own test in ContrastGuards, with the ring's. + ["FocusLine"] = new(ForState), + // THE FOUR START TYPES, 2026-08-17. SC 1.4.11 rather than the text floor, and the // distinction is not a rounding: nobody reads these, they are rings beside a word that // carries the same answer in text. What they have to do is be TELLABLE from the window and @@ -206,7 +221,7 @@ internal sealed record Floor(double Ratio, string? Against = null); /// internal static readonly string[] CarriesNoText = [ - "SurfaceRowLine", "SurfacePanelEdge", "SurfaceChipEdge", + "SurfaceRowLine", "SurfaceHeadingRule", "SurfacePanelEdge", "SurfaceChipEdge", "SurfaceScrollThumb", "SurfaceScrollThumbAwake", "SurfaceScrim" ]; } diff --git a/tests/Bws.Gui.Tests/ContrastGuards.cs b/tests/Bws.Gui.Tests/ContrastGuards.cs index db9c805..47bb787 100644 --- a/tests/Bws.Gui.Tests/ContrastGuards.cs +++ b/tests/Bws.Gui.Tests/ContrastGuards.cs @@ -181,6 +181,64 @@ public void The_focus_ring_clears_its_ratio_against_every_surface_a_row_can_take + Environment.NewLine + string.Join(Environment.NewLine, short_)); } + /// + /// The line under a text field with the keyboard in it clears 3:1 against every fill a text box + /// takes, on every surface a text box stands on - review of PR 21. + /// + /// It was the selection blue, 3.09 against the window and 2.51 against the fill it is + /// actually drawn on. The fills are translucent white, so what the line meets is the fill + /// composed over what the field stands on: the window for the search field, the plan sheet for + /// the box of seconds and the box a confirmation is typed into. Resolved out of the running theme rather + /// than read from the files, because the fills are written as attributes and + /// sees only the element form - backlog 459. + /// + /// And the library's key has to carry the same colour, since every text box but the + /// search field draws its line through it and an alias of FocusLine does not reach that template. + /// + [Fact] + public void The_focus_line_clears_its_ratio_against_every_fill_a_text_box_takes() + { + var line = WpfHost.Declared("FocusLine"); + + Assert.Equal(line, WpfHost.Declared("TextControlFocusedBorderBrush")); + + var surfaces = new[] { ("the window", WindowBackground()), ("the plan sheet", WpfHost.Declared("SurfacePanel")) }; + var short_ = new List(); + + foreach (var (surface, under) in surfaces) + { + foreach (var fill in FillsOfATextBox) + { + var beneath = Composed(WpfHost.Declared(fill), under); + var ratio = Contrast(line, beneath); + + if (ratio < ForState) + { + short_.Add(string.Create( + System.Globalization.CultureInfo.InvariantCulture, + $" {fill} on {surface} reads {Hex(beneath)}, the line measures {ratio:F2} and needs {ForState:F1}")); + } + } + } + + Assert.True( + short_.Count == 0, + "The focus line is drawn on the field's own fill, so it has to be tellable from all of them:" + + Environment.NewLine + string.Join(Environment.NewLine, short_)); + } + + /// The library's text box fills, all three of which Surfaces.xaml overrides. + private static readonly string[] FillsOfATextBox = + ["TextControlBackground", "TextControlBackgroundFocused", "TextControlBackgroundPointerOver"]; + + /// A translucent colour laid over an opaque one, channel by channel, the way it reaches the pixel. + private static Color Composed(Color over, Color under) + { + byte Channel(byte top, byte bottom) => (byte)Math.Round(((top * over.A) + (bottom * (255 - over.A))) / 255.0); + + return Color.FromRgb(Channel(over.R, under.R), Channel(over.G, under.G), Channel(over.B, under.B)); + } + [Fact] public void No_colour_is_declared_without_a_ratio_it_has_to_clear() { diff --git a/tests/Bws.Gui.Tests/CopyingAndPanelGuards.cs b/tests/Bws.Gui.Tests/CopyingAndPanelGuards.cs index ed4ccae..6f9b553 100644 --- a/tests/Bws.Gui.Tests/CopyingAndPanelGuards.cs +++ b/tests/Bws.Gui.Tests/CopyingAndPanelGuards.cs @@ -90,6 +90,11 @@ public async Task The_context_menu_copies_what_each_item_promises() ("gui.menu.copyAll", WpfHost.On(() => Copying.Everything(Picked(window)))) }; + // THE FIXTURE'S OWN WORDS, NOT Copying's - a wanted text computed by the function under test + // agrees with it whatever it drops, and these two fields lost their items to this one. + Assert.Contains("Print Spooler", promises[1].Wanted, StringComparison.Ordinal); + Assert.Contains("Spooler description", promises[1].Wanted, StringComparison.Ordinal); + foreach (var (key, wanted) in promises) { if (string.IsNullOrWhiteSpace(wanted)) diff --git a/tests/Bws.Gui.Tests/PlanViewGuards.cs b/tests/Bws.Gui.Tests/PlanViewGuards.cs index e961c63..3f84d3c 100644 --- a/tests/Bws.Gui.Tests/PlanViewGuards.cs +++ b/tests/Bws.Gui.Tests/PlanViewGuards.cs @@ -1,6 +1,7 @@ using System.Globalization; using System.Windows; using System.Windows.Controls; +using System.Windows.Documents; using System.Windows.Media; using Bws.Core.Planning; using Bws.Gui.ViewModels; @@ -331,6 +332,7 @@ public void It_takes_no_focus_of_its_own_and_still_takes_clicks() public async Task Every_word_on_the_panel_can_be_read_on_the_surface_it_is_drawn_on() { var window = await Ready(); + var model = WpfHost.On(() => (MainViewModel)window.DataContext); Assert.True(await WpfHost.On(() => window.Preview(ActionKind.Stop))); WpfHost.Settled(); @@ -353,21 +355,30 @@ public async Task Every_word_on_the_panel_can_be_read_on_the_surface_it_is_drawn var found = new List(); Collect(window.PlanPanel, found); - var words = found.Where(text => !string.IsNullOrWhiteSpace(text.Text)).ToList(); + // THE RUNS, NOT TextBlock.Text, which is EMPTY for a block built from runs - and since + // 2026-09-24 the title and every step are (review of PR 20). A run is measured in the + // colour it ended up with, its block's unless it names its own. + var runs = found + .SelectMany(text => text.Inlines.OfType()) + .Where(run => !string.IsNullOrWhiteSpace(run.Text)) + .ToList(); - return (words - .Select(text => (text.Text, Ratio(Ink(text), surface))) + return (runs + .Select(run => (run.Text, Ratio(Ink(run.Foreground), surface))) .Where(pair => pair.Item2 < 4.5) .Select(pair => string.Create( CultureInfo.InvariantCulture, $"{pair.Item2:F2} {pair.Text}")) - .ToList(), words.Count); + .ToList(), found.Select(text => string.Concat(text.Inlines.OfType().Select(run => run.Text))).ToList()); }); // A GUARD SATISFIED BY ABSENCE IS SATISFIED FOR AS LONG AS NOBODY BUILDS ANYTHING, and this // project has that lesson written in three other files. The panel shows a title, a name, a - // state, a heading and a step at the very least. - Assert.True(read >= 5, $"Only {read} lines were found on the panel, so nothing was measured."); + // state, a heading and a step at the very least - and the title and a step by name, because + // those two are the blocks built from runs. + Assert.True(read.Count(line => !string.IsNullOrWhiteSpace(line)) >= 5, "Too few lines were found on the panel, so nothing was measured."); + Assert.Contains(WpfHost.On(() => model.Planned.Heading.Text), read); + Assert.Contains(WpfHost.On(() => model.Planned.Steps[0].Text), read); Assert.True( thin.Count == 0, @@ -379,9 +390,9 @@ public async Task Every_word_on_the_panel_can_be_read_on_the_surface_it_is_drawn WpfHost.On(window.Close); } - /// The colour a TextBlock ended up with, however it got there. - private static Color Ink(TextBlock text) => - text.Foreground is SolidColorBrush brush ? brush.Color : Colors.Black; + /// The colour a run ended up with, however it got there. + private static Color Ink(Brush ink) => + ink is SolidColorBrush brush ? brush.Color : Colors.Black; /// Every TextBlock under something, including the ones a template built. private static void Collect(DependencyObject from, List into) diff --git a/tests/Bws.Gui.Tests/WindowLayerGuards.cs b/tests/Bws.Gui.Tests/WindowLayerGuards.cs new file mode 100644 index 0000000..6b8038e --- /dev/null +++ b/tests/Bws.Gui.Tests/WindowLayerGuards.cs @@ -0,0 +1,100 @@ +using System.Windows; +using System.Windows.Media; +using Bws.Core.Planning; +using Bws.Gui.ViewModels; + +namespace Bws.Gui.Tests; + +/// +/// The layers of the window - which part of MainWindow.xaml's grid is drawn over which, asked of a +/// click on a shown window. Values.xaml names the layers and says why there are two. +/// +/// Its own file because the question is the window's, not a panel's. The plan panel and the +/// search row each have a file of guards, and neither can see the other - the fault this file +/// exists for stood between them. +/// +public sealed class WindowLayerGuards +{ + /// + /// THE PLAN LIES OVER THE WHOLE WINDOW, THE SEARCH FIELD INCLUDED - review of PR 21. + /// + /// The search row went up a layer on 2026-09-25 so the panel of messages under the field + /// lies over the filters, and a layer outranks the order siblings are declared in. The plan, + /// last in MainWindow.xaml so it would cover everything, was drawn under the search field, and + /// the field took clicks and keys through the dimming while a plan waited to be carried out. + /// + /// Asked with the framework's own hit test at the centre of the search field on a shown + /// window, the way a click asks it. Hit testing walks the layers in the order painting does, so + /// the one answer covers what is seen as well as what is clicked. + /// + [Fact] + public async Task The_plan_lies_over_the_whole_window_the_search_field_included() + { + var window = await WithOnePicked(); + + Assert.True(await WpfHost.On(() => window.Preview(ActionKind.Stop))); + WpfHost.Settled(); + + var (hit, onThePlan) = WpfHost.On(() => + { + // SHOWN, off screen, the way PlanViewGuards shows the panel it reads colours from: hit + // testing needs a laid out window, and a window built here is never laid out otherwise. + window.Width = 1100; + window.Height = 700; + window.WindowStyle = WindowStyle.None; + window.ShowInTaskbar = false; + window.Left = -4000; + window.Show(); + window.UpdateLayout(); + + var field = window.Search.SearchField; + var centre = field.TranslatePoint(new Point(field.ActualWidth / 2, field.ActualHeight / 2), window); + var found = window.InputHitTest(centre) as DependencyObject; + + return (found?.GetType().Name ?? "nothing", IsInside(found, window.PlanPanel)); + }); + + Assert.True( + onThePlan, + $"a click at the centre of the search field lands on {hit} while a plan is open, not on " + + "the plan's dimming - the search row is drawn over the plan and can be typed into behind it"); + + WpfHost.On(window.Close); + } + + private static bool IsInside(DependencyObject? hit, DependencyObject target) + { + for (var at = hit; at is not null; at = at is Visual ? VisualTreeHelper.GetParent(at) : LogicalTreeHelper.GetParent(at)) + { + if (ReferenceEquals(at, target)) + { + return true; + } + } + + return false; + } + + /// + /// A window with one entry picked, so there is a plan to open. Read before the model reaches + /// the window, the order PlanViewGuards keeps and for its reason. + /// + private static async Task WithOnePicked() + { + var model = new MainViewModel(new LiveMachine(Rows.Entry("Spooler", "Print Spooler")), new SteppedClock()); + + await model.LoadAsync(); + + var window = WpfHost.Window(model); + + WpfHost.On(() => + { + window.Entries.ItemsSource = model.Rows; + window.Entries.SelectedItem = model.Rows.First(row => row.ServiceName == "Spooler"); + }); + + WpfHost.Settled(); + + return window; + } +} diff --git a/tests/Bws.Gui.Tests/WpfHost.cs b/tests/Bws.Gui.Tests/WpfHost.cs index 8091b11..7fda03e 100644 --- a/tests/Bws.Gui.Tests/WpfHost.cs +++ b/tests/Bws.Gui.Tests/WpfHost.cs @@ -286,7 +286,9 @@ internal static Color Declared(string name) => // Suggestions.xaml beside it, after Text.xaml whose names it resolves. // Actions.xaml after Menus.xaml, as in App.xaml - it extends the implicit Button // style the same way. The write buttons and their shield, 2026-09-23. - "Controls.xaml", "Menus.xaml", "Actions.xaml", "Suggestions.xaml", + // SearchField.xaml after Suggestions.xaml, as in App.xaml - its panel of + // messages wears SuggestionPanel. The search field, 2026-09-25. + "Controls.xaml", "Menus.xaml", "Actions.xaml", "Suggestions.xaml", "SearchField.xaml", // PlanLines.xaml before Plan.xaml, for the same reason and from the same // list in App.xaml. They were one file until 2026-09-07. PlanOffers.xaml