Files
Terminal.Gui/Tests/UnitTestsParallelizable/ViewBase/SubviewTests.cs
Copilot f4b10511c0 Fixes #4495 - Adds SuperViewChanging event to View following Cancellable Work Pattern (#4503)
* Initial plan

* Add SuperViewChanging event infrastructure and tests

Co-authored-by: tig <585482+tig@users.noreply.github.com>

* Refactor SuperViewChanging to follow Cancellable Work Pattern with cancellation support

Co-authored-by: tig <585482+tig@users.noreply.github.com>

* Remove SuperViewChangingEventArgs class and use CancelEventArgs<View?> directly

Co-authored-by: tig <585482+tig@users.noreply.github.com>

* Simplified SuperViewChanged

* Refactor SuperView change to use CWP and event args

Refactored the handling of SuperView changes in the View hierarchy to use the Cancellable Work Pattern (CWP) and standardized event argument types. The SuperView property is now settable only via an internal SetSuperView method, which leverages CWP for property changes and cancellation.

- Updated OnSuperViewChanging to accept ValueChangingEventArgs<View?>.
- SuperViewChanging event now uses EventHandler<ValueChangingEventArgs<View?>>; cancellation is via e.Handled = true.
- OnSuperViewChanged and SuperViewChanged now use ValueChangedEventArgs<View?>.
- All SuperView assignments in Add, Remove, and RemoveAll now use SetSuperView, which returns a bool for cancellation.
- CWPPropertyHelper.ChangeProperty now accepts a sender parameter, passed to event handlers.
- All property changes in View now pass this as sender to ChangeProperty.
- Updated event handler signatures and overrides in MenuBar, StatusBar, TextField, and TextView.
- Updated unit tests to use new event args and cancellation pattern.
- Minor code cleanups and improved comments.

These changes modernize and standardize property change handling, improving API consistency, extensibility, and testability.

* Refactor subview removal and event arg types

Removed redundant index check when removing subviews in View.cs, simplifying the cleanup loop. Updated TextField.OnSuperViewChanged to use a non-nullable ValueChangedEventArgs<View> parameter, enforcing stricter type safety.

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: tig <585482+tig@users.noreply.github.com>
Co-authored-by: Tig <tig@users.noreply.github.com>
2025-12-18 06:32:01 -07:00

987 lines
29 KiB
C#

namespace ViewBaseTests.Hierarchy;
[Collection ("Global Test Setup")]
public class SubViewTests
{
[Fact]
public void SuperViewChanged_Raised_On_Add ()
{
var super = new View { };
var sub = new View ();
int superRaisedCount = 0;
int subRaisedCount = 0;
super.SuperViewChanged += (s, e) =>
{
superRaisedCount++;
};
sub.SuperViewChanged += (s, e) =>
{
if (sub.SuperView is { })
{
subRaisedCount++;
}
};
super.Add (sub);
Assert.True (super.SubViews.Count == 1);
Assert.Equal (super, sub.SuperView);
Assert.Equal (0, superRaisedCount);
Assert.Equal (1, subRaisedCount);
}
[Fact]
public void SuperViewChanged_Raised_On_Remove ()
{
var super = new View { };
var sub = new View ();
int superRaisedCount = 0;
int subRaisedCount = 0;
super.SuperViewChanged += (s, e) =>
{
superRaisedCount++;
};
sub.SuperViewChanged += (s, e) =>
{
if (sub.SuperView is null)
{
subRaisedCount++;
}
};
super.Add (sub);
Assert.True (super.SubViews.Count == 1);
Assert.Equal (super, sub.SuperView);
Assert.Equal (0, superRaisedCount);
Assert.Equal (0, subRaisedCount);
super.Remove (sub);
Assert.Empty (super.SubViews);
Assert.NotEqual (super, sub.SuperView);
Assert.Equal (0, superRaisedCount);
Assert.Equal (1, subRaisedCount);
}
[Fact]
public void SuperView_Set_On_Add_Remove ()
{
var superView = new View ();
var view = new View ();
Assert.Null (view.SuperView);
superView.Add (view);
Assert.Equal (superView, view.SuperView);
superView.Remove (view);
Assert.Null (view.SuperView);
}
// TODO: Consider a feature that will change the ContentSize to fit the subviews.
[Fact]
public void Add_Does_Not_Impact_ContentSize ()
{
var view = new View ();
view.SetContentSize (new Size (1, 1));
var subview = new View
{
X = 10,
Y = 10
};
Assert.Equal (new (1, 1), view.GetContentSize ());
view.Add (subview);
Assert.Equal (new (1, 1), view.GetContentSize ());
}
[Fact]
public void Remove_Does_Not_Impact_ContentSize ()
{
var view = new View ();
view.SetContentSize (new Size (1, 1));
var subview = new View
{
X = 10,
Y = 10
};
Assert.Equal (new (1, 1), view.GetContentSize ());
view.Add (subview);
Assert.Equal (new (1, 1), view.GetContentSize ());
view.SetContentSize (new Size (5, 5));
Assert.Equal (new (5, 5), view.GetContentSize ());
view.Remove (subview);
Assert.Equal (new (5, 5), view.GetContentSize ());
}
[Theory]
[InlineData (ViewArrangement.Fixed)]
[InlineData (ViewArrangement.Overlapped)]
public void MoveSubViewToEnd_ViewArrangement (ViewArrangement arrangement)
{
View superView = new () { Arrangement = arrangement };
var subview1 = new View
{
Id = "subview1"
};
var subview2 = new View
{
Id = "subview2"
};
var subview3 = new View
{
Id = "subview3"
};
superView.Add (subview1, subview2, subview3);
superView.MoveSubViewToEnd (subview1);
Assert.Equal ([subview2, subview3, subview1], superView.SubViews.ToArray ());
superView.MoveSubViewToEnd (subview2);
Assert.Equal ([subview3, subview1, subview2], superView.SubViews.ToArray ());
superView.MoveSubViewToEnd (subview3);
Assert.Equal ([subview1, subview2, subview3], superView.SubViews.ToArray ());
}
[Fact]
public void MoveSubViewToStart ()
{
View superView = new ();
var subview1 = new View
{
Id = "subview1"
};
var subview2 = new View
{
Id = "subview2"
};
var subview3 = new View
{
Id = "subview3"
};
superView.Add (subview1, subview2, subview3);
superView.MoveSubViewToStart (subview2);
Assert.Equal (subview2, superView.SubViews.ElementAt (0));
superView.MoveSubViewToStart (subview3);
Assert.Equal (subview3, superView.SubViews.ElementAt (0));
}
[Fact]
public void MoveSubViewTowardsFront ()
{
View superView = new ();
var subview1 = new View
{
Id = "subview1"
};
var subview2 = new View
{
Id = "subview2"
};
var subview3 = new View
{
Id = "subview3"
};
superView.Add (subview1, subview2, subview3);
superView.MoveSubViewTowardsStart (subview2);
Assert.Equal (subview2, superView.SubViews.ElementAt (0));
superView.MoveSubViewTowardsStart (subview3);
Assert.Equal (subview3, superView.SubViews.ElementAt (1));
// Already at front, what happens?
superView.MoveSubViewTowardsStart (subview2);
Assert.Equal (subview2, superView.SubViews.ElementAt (0));
}
[Fact]
public void MoveSubViewToEnd ()
{
View superView = new ();
var subview1 = new View
{
Id = "subview1"
};
var subview2 = new View
{
Id = "subview2"
};
var subview3 = new View
{
Id = "subview3"
};
superView.Add (subview1, subview2, subview3);
superView.MoveSubViewToEnd (subview1);
Assert.Equal (subview1, superView.SubViews.ToArray () [^1]);
superView.MoveSubViewToEnd (subview2);
Assert.Equal (subview2, superView.SubViews.ToArray () [^1]);
}
[Fact]
public void MoveSubViewTowardsEnd ()
{
View superView = new ();
var subview1 = new View
{
Id = "subview1"
};
var subview2 = new View
{
Id = "subview2"
};
var subview3 = new View
{
Id = "subview3"
};
superView.Add (subview1, subview2, subview3);
superView.MoveSubViewTowardsEnd (subview2);
Assert.Equal (subview2, superView.SubViews.ToArray () [^1]);
superView.MoveSubViewTowardsEnd (subview1);
Assert.Equal (subview1, superView.SubViews.ToArray () [1]);
// Already at end, what happens?
superView.MoveSubViewTowardsEnd (subview2);
Assert.Equal (subview2, superView.SubViews.ToArray () [^1]);
}
[Fact]
public void IsInHierarchy_ViewIsNull_ReturnsFalse ()
{
// Arrange
var start = new View ();
// Act
bool result = View.IsInHierarchy (start, null);
// Assert
Assert.False (result);
}
[Fact]
public void IsInHierarchy_StartIsNull_ReturnsFalse ()
{
// Arrange
var view = new View ();
// Act
bool result = View.IsInHierarchy (null, view);
// Assert
Assert.False (result);
}
[Fact]
public void IsInHierarchy_ViewIsStart_ReturnsTrue ()
{
// Arrange
var start = new View ();
// Act
bool result = View.IsInHierarchy (start, start);
// Assert
Assert.True (result);
}
[Fact]
public void IsInHierarchy_ViewIsDirectSubView_ReturnsTrue ()
{
// Arrange
var start = new View ();
var subview = new View ();
start.Add (subview);
// Act
bool result = View.IsInHierarchy (start, subview);
// Assert
Assert.True (result);
}
[Fact]
public void IsInHierarchy_ViewIsNestedSubView_ReturnsTrue ()
{
// Arrange
var start = new View ();
var subview = new View ();
var nestedSubView = new View ();
start.Add (subview);
subview.Add (nestedSubView);
// Act
bool result = View.IsInHierarchy (start, nestedSubView);
// Assert
Assert.True (result);
}
[Fact]
public void IsInHierarchy_ViewIsNotInHierarchy_ReturnsFalse ()
{
// Arrange
var start = new View ();
var subview = new View ();
// Act
bool result = View.IsInHierarchy (start, subview);
// Assert
Assert.False (result);
}
[Theory]
[CombinatorialData]
public void IsInHierarchy_ViewIsInAdornments_ReturnsTrue (bool includeAdornments)
{
// Arrange
var start = new View
{
Id = "start"
};
var inPadding = new View
{
Id = "inPadding"
};
start.Padding!.Add (inPadding);
// Act
bool result = View.IsInHierarchy (start, inPadding, includeAdornments);
// Assert
Assert.Equal (includeAdornments, result);
}
[Fact]
public void SuperView_Set_Raises_SuperViewChangedEvents ()
{
// Arrange
var view = new View ();
var superView = new View ();
int superViewChangedCount = 0;
int superViewChangingCount = 0;
view.SuperViewChanged += (s, e) =>
{
superViewChangedCount++;
};
view.SuperViewChanging += (s, e) =>
{
superViewChangingCount++;
};
// Act
superView.Add (view);
// Assert
Assert.Equal (1, superViewChangingCount);
Assert.Equal (1, superViewChangedCount);
}
[Fact]
public void GetTopSuperView_Test ()
{
var v1 = new View ();
var fv1 = new FrameView ();
fv1.Add (v1);
var tf1 = new TextField ();
var w1 = new Window ();
w1.Add (fv1, tf1);
var top1 = new Runnable ();
top1.Add (w1);
var v2 = new View ();
var fv2 = new FrameView ();
fv2.Add (v2);
var tf2 = new TextField ();
var w2 = new Window ();
w2.Add (fv2, tf2);
var top2 = new Runnable ();
top2.Add (w2);
Assert.Equal (top1, v1.GetTopSuperView ());
Assert.Equal (top2, v2.GetTopSuperView ());
v1.Dispose ();
fv1.Dispose ();
tf1.Dispose ();
w1.Dispose ();
top1.Dispose ();
v2.Dispose ();
fv2.Dispose ();
tf2.Dispose ();
w2.Dispose ();
top2.Dispose ();
}
[Fact]
public void Initialized_Event_Comparing_With_Added_Event ()
{
var top = new Runnable { Id = "0" }; // Frame: 0, 0, 80, 25; Viewport: 0, 0, 80, 25
var winAddedToTop = new Window
{
Id = "t", Width = Dim.Fill (), Height = Dim.Fill ()
}; // Frame: 0, 0, 80, 25; Viewport: 0, 0, 78, 23
var v1AddedToWin = new View
{
Id = "v1", Width = Dim.Fill (), Height = Dim.Fill ()
}; // Frame: 1, 1, 78, 23 (because Windows has a border)
var v2AddedToWin = new View
{
Id = "v2", Width = Dim.Fill (), Height = Dim.Fill ()
}; // Frame: 1, 1, 78, 23 (because Windows has a border)
var svAddedTov1 = new View
{
Id = "sv1", Width = Dim.Fill (), Height = Dim.Fill ()
}; // Frame: 1, 1, 78, 23 (same as it's superview v1AddedToWin)
int tc = 0, wc = 0, v1c = 0, v2c = 0, sv1c = 0;
winAddedToTop.SubViewAdded += (s, e) =>
{
Assert.Equal (e.SuperView!.Frame.Width, winAddedToTop.Frame.Width);
Assert.Equal (e.SuperView.Frame.Height, winAddedToTop.Frame.Height);
};
v1AddedToWin.SubViewAdded += (s, e) =>
{
Assert.Equal (e.SuperView!.Frame.Width, v1AddedToWin.Frame.Width);
Assert.Equal (e.SuperView.Frame.Height, v1AddedToWin.Frame.Height);
};
v2AddedToWin.SubViewAdded += (s, e) =>
{
Assert.Equal (e.SuperView!.Frame.Width, v2AddedToWin.Frame.Width);
Assert.Equal (e.SuperView.Frame.Height, v2AddedToWin.Frame.Height);
};
svAddedTov1.SubViewAdded += (s, e) =>
{
Assert.Equal (e.SuperView!.Frame.Width, svAddedTov1.Frame.Width);
Assert.Equal (e.SuperView.Frame.Height, svAddedTov1.Frame.Height);
};
top.Initialized += (s, e) =>
{
tc++;
Assert.Equal (1, tc);
Assert.Equal (1, wc);
Assert.Equal (1, v1c);
Assert.Equal (1, v2c);
Assert.Equal (1, sv1c);
Assert.True (top.CanFocus);
Assert.True (winAddedToTop.CanFocus);
Assert.False (v1AddedToWin.CanFocus);
Assert.False (v2AddedToWin.CanFocus);
Assert.False (svAddedTov1.CanFocus);
top.Layout ();
};
winAddedToTop.Initialized += (s, e) =>
{
wc++;
Assert.Equal (top.Viewport.Width, winAddedToTop.Frame.Width);
Assert.Equal (top.Viewport.Height, winAddedToTop.Frame.Height);
};
v1AddedToWin.Initialized += (s, e) =>
{
v1c++;
// Top.Frame: 0, 0, 80, 25; Top.Viewport: 0, 0, 80, 25
// BUGBUG: This is wrong, it should be 78, 23. This test has always been broken.
// in no way should the v1AddedToWin.Frame be the same as the Top.Frame/Viewport
// as it is a subview of winAddedToTop, which has a border!
//Assert.Equal (top.Viewport.Width, v1AddedToWin.Frame.Width);
//Assert.Equal (top.Viewport.Height, v1AddedToWin.Frame.Height);
};
v2AddedToWin.Initialized += (s, e) =>
{
v2c++;
// Top.Frame: 0, 0, 80, 25; Top.Viewport: 0, 0, 80, 25
// BUGBUG: This is wrong, it should be 78, 23. This test has always been broken.
// in no way should the v2AddedToWin.Frame be the same as the Top.Frame/Viewport
// as it is a subview of winAddedToTop, which has a border!
//Assert.Equal (top.Viewport.Width, v2AddedToWin.Frame.Width);
//Assert.Equal (top.Viewport.Height, v2AddedToWin.Frame.Height);
};
svAddedTov1.Initialized += (s, e) =>
{
sv1c++;
// Top.Frame: 0, 0, 80, 25; Top.Viewport: 0, 0, 80, 25
// BUGBUG: This is wrong, it should be 78, 23. This test has always been broken.
// in no way should the svAddedTov1.Frame be the same as the Top.Frame/Viewport
// because sv1AddedTov1 is a subview of v1AddedToWin, which is a subview of
// winAddedToTop, which has a border!
//Assert.Equal (top.Viewport.Width, svAddedTov1.Frame.Width);
//Assert.Equal (top.Viewport.Height, svAddedTov1.Frame.Height);
Assert.False (svAddedTov1.CanFocus);
//Assert.Throws<InvalidOperationException> (() => svAddedTov1.CanFocus = true);
Assert.False (svAddedTov1.CanFocus);
};
v1AddedToWin.Add (svAddedTov1);
winAddedToTop.Add (v1AddedToWin, v2AddedToWin);
top.Add (winAddedToTop);
top.BeginInit ();
top.EndInit ();
Assert.Equal (1, tc);
Assert.Equal (1, wc);
Assert.Equal (1, v1c);
Assert.Equal (1, v2c);
Assert.Equal (1, sv1c);
Assert.True (top.CanFocus);
Assert.True (winAddedToTop.CanFocus);
Assert.False (v1AddedToWin.CanFocus);
Assert.False (v2AddedToWin.CanFocus);
Assert.False (svAddedTov1.CanFocus);
v1AddedToWin.CanFocus = true;
Assert.False (svAddedTov1.CanFocus); // False because sv1 was disposed and it isn't a subview of v1.
}
[Fact]
public void SuperViewChanged_Raised_On_SubViewAdded_SubViewRemoved ()
{
var isAdded = false;
View superView = new () { Id = "superView" };
View subView = new () { Id = "subView" };
superView.SubViewAdded += (s, e) =>
{
Assert.True (isAdded);
Assert.Equal (superView, subView.SuperView);
Assert.Equal (subView, e.SubView);
Assert.Equal (superView, e.SuperView);
};
superView.SubViewRemoved += (s, e) =>
{
Assert.False (isAdded);
Assert.NotEqual (superView, subView.SuperView);
Assert.Equal (subView, e.SubView);
Assert.Equal (superView, e.SuperView);
};
subView.SuperViewChanged += (s, e) => { isAdded = subView.SuperView == superView; };
superView.Add (subView);
Assert.True (isAdded);
Assert.Equal (superView, subView.SuperView);
Assert.Single (superView.SubViews);
superView.Remove (subView);
Assert.False (isAdded);
Assert.NotEqual (superView, subView.SuperView);
Assert.Empty (superView.SubViews);
}
[Fact]
public void RemoveAll_Removes_All_SubViews ()
{
// Arrange
var superView = new View ();
var subView1 = new View ();
var subView2 = new View ();
var subView3 = new View ();
superView.Add (subView1, subView2, subView3);
// Act
var removedViews = superView.RemoveAll ();
// Assert
Assert.Empty (superView.SubViews);
Assert.Equal (3, removedViews.Count);
Assert.Contains (subView1, removedViews);
Assert.Contains (subView2, removedViews);
Assert.Contains (subView3, removedViews);
}
[Fact]
public void RemoveAllTView_Removes_All_SubViews_Of_Specific_Type ()
{
// Arrange
var superView = new View ();
var subView1 = new View ();
var subView2 = new View ();
var subView3 = new View ();
var subView4 = new Button ();
superView.Add (subView1, subView2, subView3, subView4);
// Act
var removedViews = superView.RemoveAll<Button> ();
// Assert
Assert.Equal (3, superView.SubViews.Count);
Assert.DoesNotContain (subView4, superView.SubViews);
Assert.Single (removedViews);
Assert.Contains (subView4, removedViews);
}
[Fact]
public void RemoveAllTView_Does_Not_Remove_Other_Types ()
{
// Arrange
var superView = new View ();
var subView1 = new View ();
var subView2 = new Button ();
var subView3 = new Label ();
superView.Add (subView1, subView2, subView3);
// Act
var removedViews = superView.RemoveAll<Button> ();
// Assert
Assert.Equal (2, superView.SubViews.Count);
Assert.Contains (subView1, superView.SubViews);
Assert.Contains (subView3, superView.SubViews);
Assert.Single (removedViews);
Assert.Contains (subView2, removedViews);
}
[Fact]
public void SuperViewChanging_Raised_Before_SuperViewChanged ()
{
// Arrange
var superView = new View ();
var subView = new View ();
var events = new List<string> ();
subView.SuperViewChanging += (s, e) => { events.Add ("SuperViewChanging"); };
subView.SuperViewChanged += (s, e) => { events.Add ("SuperViewChanged"); };
// Act
superView.Add (subView);
// Assert
Assert.Equal (2, events.Count);
Assert.Equal ("SuperViewChanging", events [0]);
Assert.Equal ("SuperViewChanged", events [1]);
}
[Fact]
public void SuperViewChanging_Provides_OldSuperView_On_Add ()
{
// Arrange
var superView = new View ();
var subView = new View ();
View? currentValueInEvent = new View (); // Set to non-null to ensure it gets updated
View? newValueInEvent = null;
subView.SuperViewChanging += (s, e) =>
{
currentValueInEvent = e.CurrentValue;
newValueInEvent = e.NewValue;
};
// Act
superView.Add (subView);
// Assert
Assert.Null (currentValueInEvent); // Was null before add
Assert.Equal (superView, newValueInEvent); // Will be superView after add
}
[Fact]
public void SuperViewChanging_Provides_OldSuperView_On_Remove ()
{
// Arrange
var superView = new View ();
var subView = new View ();
superView.Add (subView);
View? currentValueInEvent = null;
View? newValueInEvent = new View (); // Set to non-null to ensure it gets updated
subView.SuperViewChanging += (s, e) =>
{
currentValueInEvent = e.CurrentValue;
newValueInEvent = e.NewValue;
};
// Act
superView.Remove (subView);
// Assert
Assert.Equal (superView, currentValueInEvent); // Was superView before remove
Assert.Null (newValueInEvent); // Will be null after remove
}
[Fact]
public void SuperViewChanging_Allows_Access_To_App_Before_Remove ()
{
// Arrange
using IApplication app = Application.Create ();
var runnable = new Runnable<bool> ();
var subView = new View ();
runnable.Add (subView);
SessionToken? token = app.Begin (runnable);
IApplication? appInEvent = null;
subView.SuperViewChanging += (s, e) =>
{
Assert.NotNull (s);
// At this point, SuperView is still set, so App should be accessible
appInEvent = (s as View)?.App;
};
Assert.NotNull (runnable.App);
// Act
runnable.Remove (subView);
// Assert
Assert.NotNull (appInEvent);
Assert.Equal (app, appInEvent);
app.End (token!);
runnable.Dispose ();
}
[Fact]
public void OnSuperViewChanging_Called_Before_OnSuperViewChanged ()
{
// Arrange
var superView = new View ();
var events = new List<string> ();
var subView = new TestViewWithSuperViewEvents (events);
// Act
superView.Add (subView);
// Assert
Assert.Equal (2, events.Count);
Assert.Equal ("OnSuperViewChanging", events [0]);
Assert.Equal ("OnSuperViewChanged", events [1]);
}
[Fact]
public void SuperViewChanging_Raised_When_Changing_Between_SuperViews ()
{
// Arrange
var superView1 = new View ();
var superView2 = new View ();
var subView = new View ();
superView1.Add (subView);
View? currentValueInEvent = null;
View? newValueInEvent = null;
subView.SuperViewChanging += (s, e) =>
{
currentValueInEvent = e.CurrentValue;
newValueInEvent = e.NewValue;
};
// Act
superView2.Add (subView);
// Assert
Assert.Equal (superView1, currentValueInEvent);
Assert.Equal (superView2, newValueInEvent);
}
// Helper class for testing virtual method calls
private class TestViewWithSuperViewEvents : View
{
private readonly List<string> _events;
public TestViewWithSuperViewEvents (List<string> events) { _events = events; }
protected override bool OnSuperViewChanging (ValueChangingEventArgs<View?> args)
{
_events.Add ("OnSuperViewChanging");
return base.OnSuperViewChanging (args);
}
protected override void OnSuperViewChanged (ValueChangedEventArgs<View?> args)
{
_events.Add ("OnSuperViewChanged");
base.OnSuperViewChanged (args);
}
}
[Fact]
public void SuperViewChanging_Can_Be_Cancelled_Via_Event ()
{
// Arrange
var superView = new View ();
var subView = new View ();
subView.SuperViewChanging += (s, e) =>
{
e.Handled = true; // Cancel the change
};
// Act
superView.Add (subView);
// Assert - SuperView should not be set because the change was cancelled
Assert.Null (subView.SuperView);
Assert.Empty (superView.SubViews);
}
[Fact]
public void SuperViewChanging_Can_Be_Cancelled_Via_Virtual_Method ()
{
// Arrange
var superView = new View ();
var subView = new TestViewThatCancelsChange ();
// Act
superView.Add (subView);
// Assert - SuperView should not be set because the change was cancelled
Assert.Null (subView.SuperView);
Assert.Empty (superView.SubViews);
}
[Fact]
public void SuperViewChanging_Virtual_Method_Cancellation_Prevents_Event ()
{
// Arrange
var superView = new View ();
var subView = new TestViewThatCancelsChange ();
var eventRaised = false;
subView.SuperViewChanging += (s, e) =>
{
eventRaised = true;
};
// Act
superView.Add (subView);
// Assert - Event should not be raised because virtual method cancelled first
Assert.False (eventRaised);
Assert.Null (subView.SuperView);
}
[Fact]
public void SuperViewChanging_Cancellation_On_Remove ()
{
// Arrange
var superView = new View ();
var subView = new View ();
superView.Add (subView);
Assert.Equal (superView, subView.SuperView);
subView.SuperViewChanging += (s, e) =>
{
// Cancel removal if trying to set to null
if (e.NewValue is null)
{
e.Handled = true;
}
};
// Act
superView.Remove (subView);
// Assert - SuperView should still be set because removal was cancelled
Assert.Equal (superView, subView.SuperView);
Assert.Single (superView.SubViews);
}
[Fact]
public void SuperViewChanging_Cancellation_When_Changing_Between_SuperViews ()
{
// Arrange
var superView1 = new View ();
var superView2 = new View ();
var subView = new View ();
superView1.Add (subView);
subView.SuperViewChanging += (s, e) =>
{
// Cancel if trying to move to superView2
if (e.NewValue == superView2)
{
e.Handled = true;
}
};
// Act
superView2.Add (subView);
// Assert - Should still be in superView1 because change was cancelled
Assert.Equal (superView1, subView.SuperView);
Assert.Single (superView1.SubViews);
Assert.Empty (superView2.SubViews);
}
// Helper class for testing cancellation
private class TestViewThatCancelsChange : View
{
protected override bool OnSuperViewChanging (ValueChangingEventArgs<View?> args)
{
return true; // Always cancel the change
}
}
}