-
Notifications
You must be signed in to change notification settings - Fork 774
Fixes #5356. Add tab fan-out layout/draw diagnostic tests #5364
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
harder
merged 3 commits into
gui-cs:develop
from
harder:fix-5356-tab-fanout-diagnostics
May 22, 2026
Merged
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,179 @@ | ||
| using System.Text; | ||
| using AppTestHelpers; | ||
|
|
||
| namespace IntegrationTests; | ||
|
|
||
| // Claude - Opus 4.7 | ||
|
|
||
| /// <summary> | ||
| /// Integration counterpart to <c>TabsFanOutDiagnosticTests</c>. Drives the active tab via real | ||
| /// key injection through the driver's input processor → command dispatch → main-loop | ||
| /// <c>LayoutAndDraw</c> path, instead of mutating <see cref="View.Viewport"/> directly. This | ||
| /// verifies the fan-out from issue #4973 / #5356 is observable end-to-end, not just under | ||
| /// synthetic <see cref="View.Layout()"/> / <see cref="View.Draw"/> calls. | ||
| /// </summary> | ||
| /// <remarks> | ||
| /// Instrumentation-only. The per-tab counters are attached to event subscriptions on | ||
| /// <see cref="View.DrawComplete"/>, <see cref="View.SubViewsLaidOut"/>, and | ||
| /// <see cref="View.ClearedViewport"/>; no rendering or invalidation behavior is changed. | ||
| /// </remarks> | ||
| public class TabsFanOutIntegrationTests (ITestOutputHelper outputHelper) : TestsAllDrivers | ||
| { | ||
| private readonly TextWriter _out = new TestOutputWriter (outputHelper); | ||
|
|
||
| /// <summary> | ||
| /// A <see cref="Code"/> that registers <see cref="Command.ScrollDown"/> / | ||
| /// <see cref="Command.ScrollUp"/> so <see cref="Key.PageDown"/> / <see cref="Key.PageUp"/> | ||
| /// drive vertical scrolling through the normal command pipeline. Used only by this test — | ||
| /// <see cref="Code"/> doesn't expose <c>AddCommand</c> publicly, so a subclass is the | ||
| /// simplest way to wire a real input → scroll path without modifying production code. | ||
| /// </summary> | ||
| private sealed class ScrollableCode : Code | ||
| { | ||
| public ScrollableCode () | ||
| { | ||
| AddCommand (Command.ScrollDown, () => ScrollVertical (1)); | ||
| AddCommand (Command.ScrollUp, () => ScrollVertical (-1)); | ||
|
|
||
| KeyBindings.Add (Key.PageDown, Command.ScrollDown); | ||
| KeyBindings.Add (Key.PageUp, Command.ScrollUp); | ||
| } | ||
| } | ||
|
|
||
| private sealed class Counters | ||
| { | ||
| public int SubViewsLaidOut; | ||
| public int DrawComplete; | ||
| public int ClearedViewport; | ||
| } | ||
|
|
||
| private static string MakeText (string prefix, int lines) | ||
| { | ||
| StringBuilder sb = new (); | ||
|
|
||
| for (var i = 1; i <= lines; i++) | ||
| { | ||
| sb.Append (prefix); | ||
| sb.Append (' '); | ||
| sb.Append (i); | ||
|
|
||
| if (i < lines) | ||
| { | ||
| sb.Append ('\n'); | ||
| } | ||
| } | ||
|
|
||
| return sb.ToString (); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// End-to-end fan-out check: a real <see cref="Key.PageDown"/> on the active tab causes | ||
| /// layout/draw activity on inactive tabs. | ||
| /// </summary> | ||
| [Theory] | ||
| [MemberData (nameof (GetAllDriverNames))] | ||
| public void Integration_RealPageDown_OnActiveTab_FansOutToInactiveTabs (string driverName) | ||
| { | ||
| const int TabCount = 4; | ||
|
|
||
| Tabs tabs = new () { Width = Dim.Fill (), Height = Dim.Fill () }; | ||
| ScrollableCode [] codes = new ScrollableCode [TabCount]; | ||
|
|
||
| for (var i = 0; i < TabCount; i++) | ||
| { | ||
| codes [i] = new ScrollableCode | ||
| { | ||
| Title = $"Tab{i + 1}", | ||
| Text = MakeText ($"Tab{i + 1} line", 80), | ||
| Language = null, | ||
| SyntaxHighlighter = null, | ||
| Width = Dim.Fill (), | ||
| Height = Dim.Fill () | ||
| }; | ||
|
|
||
| tabs.Add (codes [i]); | ||
| } | ||
|
|
||
| Counters [] perTab = new Counters [TabCount]; | ||
| Counters tabsContainer = new (); | ||
|
|
||
| for (var i = 0; i < TabCount; i++) | ||
| { | ||
| int captured = i; | ||
| perTab [i] = new Counters (); | ||
| codes [i].SubViewsLaidOut += (_, _) => perTab [captured].SubViewsLaidOut++; | ||
| codes [i].DrawComplete += (_, _) => perTab [captured].DrawComplete++; | ||
| codes [i].ClearedViewport += (_, _) => perTab [captured].ClearedViewport++; | ||
| } | ||
|
|
||
| tabs.SubViewsLaidOut += (_, _) => tabsContainer.SubViewsLaidOut++; | ||
| tabs.DrawComplete += (_, _) => tabsContainer.DrawComplete++; | ||
| tabs.ClearedViewport += (_, _) => tabsContainer.ClearedViewport++; | ||
|
|
||
| ScrollableCode active = codes [0]; | ||
|
|
||
| using AppTestHelper helper = With.A<Window> (60, 20, driverName, _out) | ||
| .Add (tabs) | ||
| .Focus (active) | ||
| .Then ( | ||
| _ => | ||
| { | ||
| for (var i = 0; i < TabCount; i++) | ||
| { | ||
| perTab [i].SubViewsLaidOut = 0; | ||
| perTab [i].DrawComplete = 0; | ||
| perTab [i].ClearedViewport = 0; | ||
| } | ||
|
|
||
| tabsContainer.SubViewsLaidOut = 0; | ||
| tabsContainer.DrawComplete = 0; | ||
| tabsContainer.ClearedViewport = 0; | ||
| }) | ||
| .KeyDown (Key.PageDown) | ||
| .KeyDown (Key.PageDown) | ||
| .KeyDown (Key.PageDown); | ||
|
|
||
| outputHelper.WriteLine ($"Driver: {driverName}"); | ||
| outputHelper.WriteLine ($"Active tab viewport Y after 3 PageDowns: {active.Viewport.Y}"); | ||
| outputHelper.WriteLine ("Per-tab counters (after 3 PageDowns on active tab):"); | ||
| outputHelper.WriteLine (" tab laidOut drawComplete clearedViewport"); | ||
| outputHelper.WriteLine ($" Tabs {tabsContainer.SubViewsLaidOut,7} {tabsContainer.DrawComplete,12} {tabsContainer.ClearedViewport,15}"); | ||
|
|
||
| for (var i = 0; i < TabCount; i++) | ||
| { | ||
| outputHelper.WriteLine ($" Code{i + 1,-6} {perTab [i].SubViewsLaidOut,7} {perTab [i].DrawComplete,12} {perTab [i].ClearedViewport,15}"); | ||
| } | ||
|
|
||
| Assert.True ( | ||
| active.Viewport.Y > 0, | ||
| $"PageDown should have scrolled the active tab via real input → command path. Got Viewport.Y={active.Viewport.Y}."); | ||
|
|
||
| Assert.True ( | ||
| perTab [0].DrawComplete > 0, | ||
| $"Active tab must draw in response to real PageDown, got DrawComplete={perTab [0].DrawComplete}."); | ||
|
|
||
| int inactiveDraws = 0; | ||
| int inactiveLayouts = 0; | ||
|
|
||
| for (var i = 1; i < TabCount; i++) | ||
| { | ||
| inactiveDraws += perTab [i].DrawComplete; | ||
| inactiveLayouts += perTab [i].SubViewsLaidOut; | ||
| } | ||
|
|
||
| outputHelper.WriteLine ($"Sum inactive DrawComplete = {inactiveDraws}"); | ||
| outputHelper.WriteLine ($"Sum inactive SubViewsLaidOut = {inactiveLayouts}"); | ||
|
|
||
| // CURRENT BEHAVIOR (issue #4973): inactive tabs receive draw and layout work when active scrolls, | ||
| // even through the real input → command → main-loop path. After #4973 lands, flip these to == 0. | ||
| Assert.True ( | ||
| inactiveDraws > 0, | ||
| $"Documents issue #4973 (integration-level): inactive_total DrawComplete={inactiveDraws}. " + | ||
| "Flip to Assert.Equal(0, inactiveDraws) after fix lands."); | ||
|
|
||
| Assert.True ( | ||
| inactiveLayouts > 0, | ||
| $"Documents issue #4973 (integration-level): inactive_total SubViewsLaidOut={inactiveLayouts}. " + | ||
| "Flip to Assert.Equal(0, inactiveLayouts) after fix lands."); | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.