Prove a click on a pixel runs the handler, with a real mouse - #608
Prove a click on a pixel runs the handler, with a real mouse#608kellylford wants to merge 2 commits into
Conversation
The mouse work shipped with its logic under test and its wiring asserted by reading MainWindow's source as text. Neither can answer the question the folder bug (#601) actually turned on: does a click on that pixel end up running that handler? Every piece there was correct and clicking a folder still did nothing. MouseClickInputTests sends real SendInput clicks at real screen coordinates against a real MainWindow, and asserts on what the ViewModel did: a folder row opens that folder, a child row opens the child and not its parent, the chevron collapses without opening, empty space opens nothing, a double-click opens once, a message row opens, a press released over another row opens neither, and a Ctrl+click extends the selection without opening. Gated behind QUICKMAIL_RUN_INPUT_TESTS=1 like the synthesized-keystroke tests, and the CI guard that asserts those actually executed now covers these too, so they cannot quietly stop running. Every click first confirms Windows puts the window under test at that screen point AND that WPF's input hit test finds the intended element there; a mismatch fails without pressing, so a coordinate error cannot become a stray click on someone's desktop. Press and release are paired in a finally: an early version failed mid-drag, left the machine's button held down, and wedged the rest of the run behind it. Four things the harness had to learn, all written up where the next person will be standing when they matter: - The window runs on its own thread with a real Dispatcher.Run() loop. Pumping it from the test thread with DispatcherFrame/PushFrame instead is a nested message loop over a live WebView2 - the deadlock the Modal Dialog Rules describe - and it hung the suite with no output. - SendInput only queues. Waiting for the dispatcher to go idle races the input it is meant to be waiting for. - InputHitTest, never VisualTreeHelper.HitTest, to ask what a click would land on: the latter is geometric and returns visuals inside a collapsed subtree, still carrying their last-arranged bounds, so a hidden pane answers for the visible one underneath it. - The fixture needs an account. With none, start-up opens the Account Manager modally, which disables the window under test - it then receives no input at all while remaining the window WindowFromPoint names and hit-testing correctly in WPF, so every click looks exactly like a click the app ignored. Two assertions written first were wrong about the app rather than the app being wrong, and say so where they sit: a WPF ListBox has no native drag-select, and the app prefetches the details of the whole visible list when a folder opens, so store-load counts cannot tell you a message was opened. The message tests compare MessageDetail identity instead. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The guard that is supposed to make a real click safe to run on someone's desktop did not do it. It asked whether our window was at the point the test AIMED at, and whether a click there would hit the intended element - but that coordinate came from the window's own layout, so it agreed with itself no matter where the pointer actually went. Everything the file claimed about refusing to click when the coordinates are wrong was untrue. It now asks about where the pointer really is: that it landed where it was sent, that Windows puts our window there, and that a click there resolves to the intended row. The rest of the review, in order of how much it mattered: - A throw from the fixture constructor skipped the whole cleanup, stranding a topmost window with a live message loop for the rest of the process - which then fails every later mouse test on "another window is in front" and takes the other windowed classes with it. Seeding asserts, so this was reachable. - Dispose caught only TimeoutException, so anything MainWindow.OnClosed threw skipped the dispatcher shutdown and the join AND replaced the real test failure with a secondary one from the cleanup path. - The double-click test waited for a full folder load between its two presses, which on a slow enough machine puts the second one past GetDoubleClickTime and quietly turns it into two single clicks. The presses now go back to back, and the test asserts the window was actually given ClickCount 2 - otherwise it passes without ever reaching the line it exists to guard. - The empty-space test could not tell empty space from a row: its guard was satisfied by anything inside the tree, a row included. It now asks the app's own row lookup and requires it to find nothing. - ClickingAFolderRow read the load count before the load it counts could have finished, so a second activation queued behind the first - the defect the count is for - would not have been counted yet. - Math.Round in the absolute-coordinate normalization lands a pixel short, since Windows maps the value back with a floor. Ceiling, and clamped. - The DPI awareness context is restored in a finally; CloseToTray is set explicitly rather than relied on; the startup wait is bounded so a hang inside Build reports instead of hanging the run with no output; IsExpanded is read on the window's thread like everything else; and a failure message can no longer throw on its way out and replace the diagnosis with a timeout. Then CI found two more, both races this machine is too fast to lose: - The guard compared visual INSTANCES. Containers get recycled, so the ViewModel finishing a load between resolving a row and moving onto it leaves the same row on screen as a different object, and the guard rejects a pointer that is exactly where it should be. It now compares the DATA the hit resolves to. - The seed lost to the ViewModel's own startup load, which lands later there: seeding asserted five messages and the list was empty by the time a test looked. Seeding now repeats until it holds. And one that was mine all along, reported earlier as somebody else's flakiness: six AccountDialogHintTests and one Pop3DialogTests failed whenever these ran in the same process. Clicking makes this process foreground; closing the window afterwards leaves the foreground wherever it falls; and those tests focus a control on a window shown with ShowActivated = false, which needs the process to hold it. The foreground window is now restored alongside the pointer. Full suite with the mouse tests on: 3190 passed, 0 failed, twice. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Independent review applied, plus two more that CI found and one that turned out to be mine. The central guard was not doing its job. It asked whether our window was at the point the test aimed at, and whether a click there would hit the intended element — but that coordinate came from the window's own layout, so it agreed with itself no matter where the pointer actually went. Everything this PR claimed about refusing to click when coordinates are wrong was untrue. It now asks about where the pointer really is. Also from the review: a constructor throw skipped all cleanup and stranded a topmost window for the rest of the process; CI found two races this machine is too fast to lose. The guard compared visual instances, which go stale when containers are recycled — the ViewModel finishing a load between resolving a row and moving onto it left the same row on screen as a different object. And the seed lost to the runner's slower startup load: seeding asserted five messages and the list was empty by the time a test looked. The guard now compares the data the hit resolves to, and seeding repeats until it holds. One was mine and I had called it someone else's. Six Full suite with the mouse tests on: 3190 passed, 0 failed, twice. |
The mouse work shipped with its logic under test and its wiring asserted by reading
MainWindow's source as text. Neither can answer the question the folder bug (#601) actually turned on: does a click on that pixel end up running that handler? Every piece there was correct and clicking a folder still did nothing.What this adds
MouseClickInputTestssends realSendInputclicks at real screen coordinates against a realMainWindow, and asserts on what the ViewModel did:Gated behind
QUICKMAIL_RUN_INPUT_TESTS=1like the synthesized-keystroke tests, and the CI guard that asserts those actually executed now covers these too — so they cannot quietly stop running.Safety on a real desktop
Every click first confirms Windows puts the window under test at that screen point and that WPF's input hit test finds the intended element there. A mismatch fails without pressing, so a coordinate error cannot become a stray click on someone's desktop. Press and release are paired in
finallyblocks: an early version failed mid-drag, left the machine's button held down, and wedged the rest of the run behind it.Four things the harness had to learn
Written up in the file headers and in CLAUDE.md, where the next person will be standing when they matter:
Dispatcher.Run()loop. Pumping it from the test thread withDispatcherFrame/PushFrameinstead is a nested message loop over a live WebView2 — the deadlock the Modal Dialog Rules describe — and it hung the suite with no output.SendInputonly queues. Waiting for the dispatcher to go idle races the input it is meant to be waiting for.InputHitTest, neverVisualTreeHelper.HitTest, to ask what a click would land on: the latter is geometric and returns visuals inside a collapsed subtree, still carrying their last-arranged bounds, so a hidden pane answers for the visible one underneath it.WindowFromPointnames and hit-testing correctly in WPF, so every click looks exactly like a click the app ignored.Two assertions that were wrong about the app
Both now say so where they sit: a WPF
ListBoxhas no native drag-select (so a drag selects one row, and what the test guards is that the release activates nothing), and the app prefetches the details of the whole visible list when a folder opens (so store-load counts cannot tell you a message was opened — the message tests compareMessageDetailidentity instead).Verification
8/8 pass on two consecutive runs; 8/8 skip with a visible reason when the env var is absent. The rest of the suite is unchanged by this — the 7 failures currently on this branch (
AccountDialogHintTests,Pop3DialogTests) fail identically with this class excluded and are being looked at separately.🤖 Generated with Claude Code