Files

1539 lines
45 KiB
C#

using System.Collections.ObjectModel;
using System.Collections.Specialized;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
using Avalonia.Headless;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Media;
using Avalonia.Threading;
using Avalonia.VisualTree;
using AtomUI.Controls.Primitives;
using AtomUI.MotionScene;
using Shouldly;
using Xunit;
using AtomUICollapse = AtomUI.Desktop.Controls.Collapse;
using AtomUICollapseItem = AtomUI.Desktop.Controls.CollapseItem;
using AvaloniaWindow = Avalonia.Controls.Window;
namespace AtomUI.Desktop.Controls.Tests.CollapseControl;
public class CollapseBehaviorTests
{
static CollapseBehaviorTests()
{
AvaloniaTestApp.EnsureInitialized();
}
[Fact]
public void Item_Padding_Overrides_Update_Template_Padding_Without_Overriding_Item_Local_Values()
{
var normalItem = new AtomUICollapseItem
{
Header = "Normal",
Content = "Content",
IsSelected = true
};
var explicitItem = new AtomUICollapseItem
{
Header = "Explicit",
Content = "Content",
IsSelected = true,
HeaderPadding = new Thickness(3),
ContentPadding = new Thickness(4)
};
var collapse = new AtomUICollapse
{
Items =
{
normalItem,
explicitItem
}
};
var window = new AvaloniaWindow
{
Width = 360,
Height = 240,
Content = collapse
};
try
{
window.Show();
Dispatcher.UIThread.RunJobs();
collapse.ItemHeaderPadding = new Thickness(7);
collapse.ItemContentPadding = new Thickness(9);
Dispatcher.UIThread.RunJobs();
GetHeaderDecorator(normalItem).Padding.ShouldBe(new Thickness(7));
GetContentFrame(normalItem).Padding.ShouldBe(new Thickness(9));
GetHeaderDecorator(explicitItem).Padding.ShouldBe(new Thickness(3));
GetContentFrame(explicitItem).Padding.ShouldBe(new Thickness(4));
collapse.ItemHeaderPadding = new Thickness(11);
collapse.ItemContentPadding = new Thickness(13);
Dispatcher.UIThread.RunJobs();
GetHeaderDecorator(normalItem).Padding.ShouldBe(new Thickness(11));
GetContentFrame(normalItem).Padding.ShouldBe(new Thickness(13));
GetHeaderDecorator(explicitItem).Padding.ShouldBe(new Thickness(3));
GetContentFrame(explicitItem).Padding.ShouldBe(new Thickness(4));
}
finally
{
window.Close();
}
}
[Fact]
public void Ghost_And_Borderless_Mode_Update_Frame_Border_At_Runtime()
{
var collapse = new AtomUICollapse
{
BorderThickness = new Thickness(5),
Items =
{
new AtomUICollapseItem
{
Header = "Header",
Content = "Content"
}
}
};
var window = new AvaloniaWindow
{
Width = 360,
Height = 180,
Content = collapse
};
try
{
window.Show();
Dispatcher.UIThread.RunJobs();
var frame = FindVisualByName<PixelAlignedBorder>(collapse, "PART_Frame");
frame.ShouldNotBeNull();
frame!.BorderThickness.ShouldBe(new Thickness(5));
collapse.IsGhostStyle = true;
Dispatcher.UIThread.RunJobs();
frame.BorderThickness.ShouldBe(new Thickness(0));
collapse.IsGhostStyle = false;
collapse.IsBorderless = true;
Dispatcher.UIThread.RunJobs();
frame.BorderThickness.ShouldBe(new Thickness(0));
collapse.IsBorderless = false;
Dispatcher.UIThread.RunJobs();
frame.BorderThickness.ShouldBe(new Thickness(5));
}
finally
{
window.Close();
}
}
[Fact]
public void Bordered_Items_Keep_Separators_On_Stable_Shells_And_Content_Frames()
{
var firstItem = new AtomUICollapseItem
{
Header = "First",
Content = "Content",
IsSelected = true
};
var middleItem = new AtomUICollapseItem
{
Header = "Middle",
Content = "Content",
IsSelected = true
};
var lastItem = new AtomUICollapseItem
{
Header = "Last",
Content = "Content",
IsSelected = true
};
var collapse = new AtomUICollapse
{
BorderThickness = new Thickness(1),
IsMotionEnabled = false,
Items =
{
firstItem,
middleItem,
lastItem
}
};
var window = new AvaloniaWindow
{
Width = 360,
Height = 260,
Content = collapse
};
try
{
window.Show();
Dispatcher.UIThread.RunJobs();
GetItemShell(firstItem).BorderThickness.ShouldBe(new Thickness(0, 0, 0, 1));
GetItemShell(middleItem).BorderThickness.ShouldBe(new Thickness(0, 0, 0, 1));
GetItemShell(lastItem).BorderThickness.ShouldBe(default(Thickness));
GetContentFrame(firstItem).BorderThickness.ShouldBe(new Thickness(0, 1, 0, 0));
GetContentFrame(middleItem).BorderThickness.ShouldBe(new Thickness(0, 1, 0, 0));
GetContentFrame(lastItem).BorderThickness.ShouldBe(new Thickness(0, 1, 0, 0));
GetHeaderDecorator(firstItem).BorderThickness.ShouldBe(default(Thickness));
GetHeaderDecorator(middleItem).BorderThickness.ShouldBe(default(Thickness));
GetHeaderDecorator(lastItem).BorderThickness.ShouldBe(default(Thickness));
}
finally
{
window.Close();
}
}
[Fact]
public void Selection_Does_Not_Change_Item_Separator_Ownership()
{
var firstItem = new AtomUICollapseItem
{
Header = "First",
Content = "Content",
IsSelected = true
};
var middleItem = new AtomUICollapseItem
{
Header = "Middle",
Content = "Content",
IsSelected = true
};
var lastItem = new AtomUICollapseItem
{
Header = "Last",
Content = "Content",
IsSelected = true
};
var collapse = new AtomUICollapse
{
BorderThickness = new Thickness(1),
IsMotionEnabled = false,
Items =
{
firstItem,
middleItem,
lastItem
}
};
var window = new AvaloniaWindow
{
Width = 360,
Height = 320,
Content = collapse
};
try
{
window.Show();
Dispatcher.UIThread.RunJobs();
firstItem.IsSelected = true;
middleItem.IsSelected = false;
Dispatcher.UIThread.RunJobs();
GetItemShell(firstItem).BorderThickness.ShouldBe(new Thickness(0, 0, 0, 1));
GetItemShell(middleItem).BorderThickness.ShouldBe(new Thickness(0, 0, 0, 1));
GetItemShell(lastItem).BorderThickness.ShouldBe(default(Thickness));
firstItem.ContentBorderThickness.ShouldBe(new Thickness(0, 1, 0, 0));
middleItem.ContentBorderThickness.ShouldBe(new Thickness(0, 1, 0, 0));
lastItem.ContentBorderThickness.ShouldBe(new Thickness(0, 1, 0, 0));
GetHeaderDecorator(firstItem).BorderThickness.ShouldBe(default(Thickness));
GetHeaderDecorator(middleItem).BorderThickness.ShouldBe(default(Thickness));
GetHeaderDecorator(lastItem).BorderThickness.ShouldBe(default(Thickness));
}
finally
{
window.Close();
}
}
[Fact]
public void Borderless_And_Ghost_Styles_Update_Structural_Separators_At_Runtime()
{
var firstItem = new AtomUICollapseItem
{
Header = "First",
Content = "Content",
IsSelected = true
};
var lastItem = new AtomUICollapseItem
{
Header = "Last",
Content = "Content",
IsSelected = true
};
var collapse = new AtomUICollapse
{
BorderThickness = new Thickness(1),
Items =
{
firstItem,
lastItem
}
};
var window = new AvaloniaWindow
{
Width = 360,
Height = 260,
Content = collapse
};
try
{
window.Show();
Dispatcher.UIThread.RunJobs();
collapse.IsBorderless = true;
Dispatcher.UIThread.RunJobs();
GetItemShell(firstItem).BorderThickness.ShouldBe(new Thickness(0, 0, 0, 1));
GetContentFrame(firstItem).BorderThickness.ShouldBe(default(Thickness));
GetContentFrame(lastItem).BorderThickness.ShouldBe(default(Thickness));
collapse.IsBorderless = false;
Dispatcher.UIThread.RunJobs();
GetItemShell(firstItem).BorderThickness.ShouldBe(new Thickness(0, 0, 0, 1));
GetContentFrame(firstItem).BorderThickness.ShouldBe(new Thickness(0, 1, 0, 0));
GetContentFrame(lastItem).BorderThickness.ShouldBe(new Thickness(0, 1, 0, 0));
collapse.IsGhostStyle = true;
Dispatcher.UIThread.RunJobs();
GetItemShell(firstItem).BorderThickness.ShouldBe(default(Thickness));
GetItemShell(lastItem).BorderThickness.ShouldBe(default(Thickness));
GetContentFrame(firstItem).BorderThickness.ShouldBe(default(Thickness));
GetContentFrame(lastItem).BorderThickness.ShouldBe(default(Thickness));
collapse.IsGhostStyle = false;
Dispatcher.UIThread.RunJobs();
GetItemShell(firstItem).BorderThickness.ShouldBe(new Thickness(0, 0, 0, 1));
GetContentFrame(firstItem).BorderThickness.ShouldBe(new Thickness(0, 1, 0, 0));
GetContentFrame(lastItem).BorderThickness.ShouldBe(new Thickness(0, 1, 0, 0));
}
finally
{
window.Close();
}
}
[Fact]
public void Accordion_Mode_Normalizes_Initial_Selected_Items_To_First_Active_Item()
{
var firstItem = new AtomUICollapseItem
{
Header = "First",
Content = "Content",
IsSelected = true
};
var secondItem = new AtomUICollapseItem
{
Header = "Second",
Content = "Content",
IsSelected = true
};
var thirdItem = new AtomUICollapseItem
{
Header = "Third",
Content = "Content"
};
var collapse = new AtomUICollapse
{
IsAccordion = true,
IsMotionEnabled = false,
Items =
{
firstItem,
secondItem,
thirdItem
}
};
var window = new AvaloniaWindow
{
Width = 360,
Height = 320,
Content = collapse
};
try
{
window.Show();
Dispatcher.UIThread.RunJobs();
firstItem.IsSelected.ShouldBeTrue();
secondItem.IsSelected.ShouldBeFalse();
thirdItem.IsSelected.ShouldBeFalse();
}
finally
{
window.Close();
}
}
[Fact]
public void Accordion_Expand_Button_Click_Closes_Previous_Item_And_Opens_Target()
{
var firstItem = new AtomUICollapseItem
{
Header = "First",
Content = "Content",
IsSelected = true
};
var secondItem = new AtomUICollapseItem
{
Header = "Second",
Content = "Content"
};
var collapse = new AtomUICollapse
{
IsAccordion = true,
IsMotionEnabled = false,
Items =
{
firstItem,
secondItem
}
};
var window = new AvaloniaWindow
{
Width = 360,
Height = 260,
Content = collapse
};
try
{
window.Show();
Dispatcher.UIThread.RunJobs();
var expandButton = GetExpandButton(secondItem);
expandButton.RaiseEvent(new RoutedEventArgs(Button.ClickEvent, expandButton));
Dispatcher.UIThread.RunJobs();
firstItem.IsSelected.ShouldBeFalse();
secondItem.IsSelected.ShouldBeTrue();
}
finally
{
window.Close();
}
}
[Fact]
public void Accordion_Clicking_Active_Item_Collapses_To_No_Active_Item()
{
var item = new AtomUICollapseItem
{
Header = "First",
Content = "Content",
IsSelected = true
};
var collapse = new AtomUICollapse
{
IsAccordion = true,
IsMotionEnabled = false,
Items =
{
item
}
};
var window = new AvaloniaWindow
{
Width = 360,
Height = 180,
Content = collapse
};
try
{
window.Show();
Dispatcher.UIThread.RunJobs();
collapse.UpdateSelectionFromEvent(item, new RoutedEventArgs());
Dispatcher.UIThread.RunJobs();
item.IsSelected.ShouldBeFalse();
}
finally
{
window.Close();
}
}
[Fact]
public void Accordion_Mode_Uses_Toggleable_Single_Selection()
{
var collapse = new TestCollapse { IsAccordion = true };
collapse.ExposedSelectionMode.ShouldBe(SelectionMode.Single | SelectionMode.Toggle);
}
[Fact]
public void Accordion_Selection_Model_Replaces_The_Previous_Item()
{
var first = new AtomUICollapseItem { Header = "First", Content = "Content" };
var second = new AtomUICollapseItem { Header = "Second", Content = "Content" };
var collapse = new TestCollapse
{
IsAccordion = true,
IsMotionEnabled = false,
Items = { first, second }
};
var window = new AvaloniaWindow
{
Width = 360,
Height = 240,
Content = collapse
};
try
{
window.Show();
Dispatcher.UIThread.RunJobs();
collapse.SelectIndex(0);
Dispatcher.UIThread.RunJobs();
collapse.ExposedSelectedIndex.ShouldBe(0);
collapse.SelectIndex(1);
Dispatcher.UIThread.RunJobs();
collapse.ExposedSelectedIndex.ShouldBe(1);
first.IsSelected.ShouldBeFalse();
second.IsSelected.ShouldBeTrue();
}
finally
{
window.Close();
}
}
[Fact]
public void Switching_To_Accordion_Retains_The_First_Selected_Index()
{
var first = new AtomUICollapseItem { Header = "First", Content = "Content" };
var second = new AtomUICollapseItem { Header = "Second", Content = "Content" };
var collapse = new TestCollapse
{
IsMotionEnabled = false,
Items = { first, second }
};
var window = new AvaloniaWindow
{
Width = 360,
Height = 240,
Content = collapse
};
try
{
window.Show();
Dispatcher.UIThread.RunJobs();
collapse.SelectIndex(0);
collapse.SelectIndex(1);
Dispatcher.UIThread.RunJobs();
collapse.IsAccordion = true;
Dispatcher.UIThread.RunJobs();
collapse.ExposedSelectedIndex.ShouldBe(0);
first.IsSelected.ShouldBeTrue();
second.IsSelected.ShouldBeFalse();
}
finally
{
window.Close();
}
}
[Fact]
public void Switching_From_Accordion_To_Normal_Allows_Additional_Selection()
{
var first = new AtomUICollapseItem { Header = "First", Content = "Content" };
var second = new AtomUICollapseItem { Header = "Second", Content = "Content" };
var collapse = new TestCollapse
{
IsAccordion = true,
IsMotionEnabled = false,
Items = { first, second }
};
var window = new AvaloniaWindow
{
Width = 360,
Height = 240,
Content = collapse
};
try
{
window.Show();
Dispatcher.UIThread.RunJobs();
collapse.SelectIndex(0);
Dispatcher.UIThread.RunJobs();
collapse.IsAccordion = false;
collapse.SelectIndex(1);
Dispatcher.UIThread.RunJobs();
first.IsSelected.ShouldBeTrue();
second.IsSelected.ShouldBeTrue();
}
finally
{
window.Close();
}
}
[Fact]
public void Normal_Mode_Expand_Buttons_Toggle_Items_Independently()
{
var first = new AtomUICollapseItem { Header = "First", Content = "Content" };
var second = new AtomUICollapseItem { Header = "Second", Content = "Content" };
var collapse = new AtomUICollapse
{
IsMotionEnabled = false,
Items = { first, second }
};
ShowInWindow(collapse, window =>
{
Click(GetExpandButton(first), window);
Click(GetExpandButton(second), window);
first.IsSelected.ShouldBeTrue();
second.IsSelected.ShouldBeTrue();
Click(GetExpandButton(first), window);
first.IsSelected.ShouldBeFalse();
second.IsSelected.ShouldBeTrue();
});
}
[Fact]
public void ItemsSource_Replace_Clears_Selection_And_Replacement_Container_Starts_Collapsed()
{
var original = new AtomUICollapseItem { Header = "Original", Content = "Content" };
var replacement = new AtomUICollapseItem { Header = "Replacement", Content = "Content" };
var source = new ObservableCollection<AtomUICollapseItem> { original };
var collapse = new TestCollapse
{
IsMotionEnabled = false,
ItemsSource = source
};
ShowInWindow(collapse, window =>
{
Click(GetExpandButton(original), window);
collapse.ExposedSelectedIndex.ShouldBe(0);
source[0] = replacement;
Dispatcher.UIThread.RunJobs();
collapse.ExposedSelectedIndex.ShouldBe(-1);
replacement.IsSelected.ShouldBeFalse();
});
}
[Fact]
public void ItemsSource_Reset_Clears_Selection_And_Replacement_Containers_Start_Collapsed()
{
var original = new AtomUICollapseItem { Header = "Original", Content = "Content" };
var replacement = new AtomUICollapseItem { Header = "Replacement", Content = "Content" };
var source = new ResettableObservableCollection<AtomUICollapseItem> { original };
var collapse = new TestCollapse
{
IsMotionEnabled = false,
ItemsSource = source
};
ShowInWindow(collapse, window =>
{
Click(GetExpandButton(original), window);
collapse.ExposedSelectedIndex.ShouldBe(0);
source.Reset(replacement);
Dispatcher.UIThread.RunJobs();
collapse.ExposedSelectedIndex.ShouldBe(-1);
replacement.IsSelected.ShouldBeFalse();
});
}
[Fact]
public void Header_And_Icon_Trigger_Modes_Use_Their_Expected_Pointer_Targets()
{
var item = new AtomUICollapseItem { Header = "Header", Content = "Content" };
var collapse = new AtomUICollapse
{
IsMotionEnabled = false,
Items = { item }
};
ShowInWindow(collapse, window =>
{
Click(GetHeaderPresenter(item), window);
item.IsSelected.ShouldBeTrue();
Click(GetHeaderPresenter(item), window);
item.IsSelected.ShouldBeFalse();
collapse.TriggerType = CollapseTriggerType.Icon;
Dispatcher.UIThread.RunJobs();
Click(GetHeaderPresenter(item), window);
item.IsSelected.ShouldBeFalse();
Click(GetExpandButton(item), window);
item.IsSelected.ShouldBeTrue();
});
}
[Fact]
public void Header_Keyboard_Enter_And_Space_Toggle_Selection()
{
var item = new AtomUICollapseItem { Header = "Header", Content = "Content" };
var collapse = new AtomUICollapse
{
IsMotionEnabled = false,
Items = { item }
};
ShowInWindow(collapse, window =>
{
item.Focus(NavigationMethod.Tab).ShouldBeTrue();
PressKey(window, Key.Return, PhysicalKey.Enter);
item.IsSelected.ShouldBeTrue();
PressKey(window, Key.Space, PhysicalKey.Space);
item.IsSelected.ShouldBeFalse();
});
}
[Fact]
public void Disabled_Item_Rejects_Pointer_And_Keyboard_Selection()
{
var item = new AtomUICollapseItem
{
Header = "Header",
Content = "Content",
IsEnabled = false
};
var collapse = new AtomUICollapse
{
IsMotionEnabled = false,
Items = { item }
};
ShowInWindow(collapse, window =>
{
Click(GetExpandButton(item), window);
PressKey(window, Key.Enter, PhysicalKey.Enter);
item.IsSelected.ShouldBeFalse();
});
}
[Fact]
public void Disabled_Collapse_Rejects_Pointer_And_Keyboard_Selection()
{
var item = new AtomUICollapseItem { Header = "Header", Content = "Content" };
var collapse = new AtomUICollapse
{
IsMotionEnabled = false,
Items = { item }
};
ShowInWindow(collapse, window =>
{
item.Focus(NavigationMethod.Tab).ShouldBeTrue();
collapse.IsEnabled = false;
Dispatcher.UIThread.RunJobs();
Click(GetExpandButton(item), window);
PressKey(window, Key.Space, PhysicalKey.Space);
item.IsSelected.ShouldBeFalse();
});
}
[Fact]
public void Accordion_Runtime_Selected_Item_Replaces_The_Current_Item()
{
var first = new AtomUICollapseItem
{
Header = "First",
Content = "Content",
IsSelected = true
};
var collapse = new AtomUICollapse
{
IsAccordion = true,
IsMotionEnabled = false,
Items = { first }
};
var window = new AvaloniaWindow
{
Width = 360,
Height = 240,
Content = collapse
};
try
{
window.Show();
Dispatcher.UIThread.RunJobs();
var second = new AtomUICollapseItem
{
Header = "Second",
Content = "Content",
IsSelected = true
};
collapse.Items.Add(second);
Dispatcher.UIThread.RunJobs();
first.IsSelected.ShouldBeFalse();
second.IsSelected.ShouldBeTrue();
}
finally
{
window.Close();
}
}
[Fact]
public void Removing_Selected_Item_Clears_The_Selection_Model()
{
var item = new AtomUICollapseItem { Header = "First", Content = "Content" };
var collapse = new TestCollapse
{
IsMotionEnabled = false,
Items = { item }
};
var window = new AvaloniaWindow
{
Width = 360,
Height = 180,
Content = collapse
};
try
{
window.Show();
Dispatcher.UIThread.RunJobs();
collapse.SelectIndex(0);
Dispatcher.UIThread.RunJobs();
collapse.Items.Remove(item);
Dispatcher.UIThread.RunJobs();
collapse.ExposedSelectedIndex.ShouldBe(-1);
item.IsSelected.ShouldBeTrue();
}
finally
{
window.Close();
}
}
[Fact]
public void Clearing_Items_Clears_The_Selection_Model()
{
var collapse = new TestCollapse
{
IsMotionEnabled = false,
Items =
{
new AtomUICollapseItem { Header = "First", Content = "Content" },
new AtomUICollapseItem { Header = "Second", Content = "Content" }
}
};
var window = new AvaloniaWindow
{
Width = 360,
Height = 240,
Content = collapse
};
try
{
window.Show();
Dispatcher.UIThread.RunJobs();
collapse.SelectIndex(0);
Dispatcher.UIThread.RunJobs();
collapse.Items.Clear();
Dispatcher.UIThread.RunJobs();
collapse.ExposedSelectedIndex.ShouldBe(-1);
}
finally
{
window.Close();
}
}
[Fact]
public void Item_Motion_Reconciles_Final_Visibility_When_Selection_Changes_During_Animation()
{
var item = new AtomUICollapseItem
{
Header = "First",
Content = "Content",
MotionDuration = TimeSpan.FromMilliseconds(1)
};
var collapse = new AtomUICollapse
{
IsMotionEnabled = true,
Items =
{
item
}
};
var window = new AvaloniaWindow
{
Width = 360,
Height = 180,
Content = collapse
};
try
{
window.Show();
Dispatcher.UIThread.RunJobs();
item.IsSelected = true;
item.IsSelected = false;
Dispatcher.UIThread.RunJobs();
var motionActor = GetContentMotionActor(item);
WaitForLayoutCondition(() => !motionActor.IsAnimating(Visual.OpacityProperty) &&
motionActor.IsVisible == item.IsSelected);
item.IsSelected.ShouldBeFalse();
motionActor.IsVisible.ShouldBeFalse();
}
finally
{
window.Close();
}
}
[Fact]
public void Item_Motion_Latest_Selection_Wins_Without_Changing_Structural_Borders()
{
var item = new AtomUICollapseItem
{
Header = "First",
Content = "Content",
MotionDuration = TimeSpan.FromMilliseconds(100)
};
var collapse = new AtomUICollapse
{
BorderThickness = new Thickness(1),
IsMotionEnabled = true,
Items =
{
item
}
};
var window = new AvaloniaWindow
{
Width = 360,
Height = 180,
Content = collapse
};
try
{
window.Show();
Dispatcher.UIThread.RunJobs();
var motionActor = GetContentMotionActor(item);
var initialItemBorder = item.ItemBorderThickness;
var initialContentBorder = item.ContentBorderThickness;
var itemBorderChangeCount = 0;
var contentBorderChangeCount = 0;
using var itemBorderSubscription = item.GetObservable(AtomUICollapseItem.ItemBorderThicknessProperty)
.Subscribe(_ => itemBorderChangeCount++);
using var contentBorderSubscription = item.GetObservable(AtomUICollapseItem.ContentBorderThicknessProperty)
.Subscribe(_ => contentBorderChangeCount++);
itemBorderChangeCount = 0;
contentBorderChangeCount = 0;
item.IsSelected = true;
Dispatcher.UIThread.RunJobs();
motionActor.IsAnimating(Visual.OpacityProperty).ShouldBeTrue();
item.IsSelected = false;
item.IsSelected = true;
Dispatcher.UIThread.RunJobs();
WaitForLayoutCondition(() => motionActor.IsVisible &&
!motionActor.IsAnimating(Visual.OpacityProperty) &&
motionActor.Opacity == 1);
item.IsSelected.ShouldBeTrue();
motionActor.IsVisible.ShouldBeTrue();
motionActor.IsAnimating(Visual.OpacityProperty).ShouldBeFalse();
motionActor.MotionTransform.ShouldBeNull();
motionActor.Transitions.ShouldBeNull();
itemBorderChangeCount.ShouldBe(0);
contentBorderChangeCount.ShouldBe(0);
item.ItemBorderThickness.ShouldBe(initialItemBorder);
item.ContentBorderThickness.ShouldBe(initialContentBorder);
}
finally
{
window.Close();
}
}
[Fact]
public void Item_Template_Reapply_Unsubscribes_Old_Expand_Button()
{
var item = new TestCollapseItem();
var oldButton = new IconButton();
var newButton = new IconButton();
item.ApplyTemplateParts(oldButton, new LayoutAwareMotionActor());
item.ApplyTemplateParts(newButton, new LayoutAwareMotionActor());
oldButton.RaiseEvent(new RoutedEventArgs(Button.ClickEvent, oldButton));
item.IsSelected.ShouldBeFalse();
newButton.RaiseEvent(new RoutedEventArgs(Button.ClickEvent, newButton));
item.IsSelected.ShouldBeTrue();
}
[Fact]
public void Item_Template_Reapply_Clears_Old_Content_Motion_Actor_Values()
{
var item = new TestCollapseItem
{
Header = "First",
Content = "Content",
MotionDuration = TimeSpan.FromMilliseconds(100)
};
var collapse = new AtomUICollapse
{
IsMotionEnabled = true,
Items = { item }
};
ShowInWindow(collapse, _ =>
{
item.IsSelected = true;
Dispatcher.UIThread.RunJobs();
var oldMotionActor = GetContentMotionActor(item);
oldMotionActor.IsAnimating(Visual.OpacityProperty).ShouldBeTrue();
item.ApplyTemplateParts(new IconButton(), new LayoutAwareMotionActor());
oldMotionActor.IsAnimating(Visual.OpacityProperty).ShouldBeFalse();
oldMotionActor.IsVisible.ShouldBeTrue();
oldMotionActor.Opacity.ShouldBe(1);
oldMotionActor.MotionTransform.ShouldBeNull();
oldMotionActor.MotionTransformOperations.ShouldBeNull();
oldMotionActor.Transitions.ShouldBeNull();
oldMotionActor.Height.ShouldBe(double.NaN);
});
}
[Fact]
public void Item_Detach_Clears_Active_Content_Motion_Values()
{
var item = new AtomUICollapseItem
{
Header = "First",
Content = "Content",
MotionDuration = TimeSpan.FromMilliseconds(100)
};
var collapse = new AtomUICollapse
{
IsMotionEnabled = true,
Items =
{
item
}
};
var window = new AvaloniaWindow
{
Width = 360,
Height = 180,
Content = collapse
};
try
{
window.Show();
Dispatcher.UIThread.RunJobs();
item.IsSelected = true;
Dispatcher.UIThread.RunJobs();
var motionActor = GetContentMotionActor(item);
motionActor.IsAnimating(Visual.OpacityProperty).ShouldBeTrue();
window.Content = null;
Dispatcher.UIThread.RunJobs();
motionActor.IsAnimating(Visual.OpacityProperty).ShouldBeFalse();
motionActor.IsVisible.ShouldBeTrue();
motionActor.Opacity.ShouldBe(1);
motionActor.MotionTransform.ShouldBeNull();
motionActor.MotionTransformOperations.ShouldBeNull();
motionActor.Transitions.ShouldBeNull();
motionActor.Height.ShouldBe(double.NaN);
}
finally
{
window.Close();
}
}
[Fact]
public void Item_Content_Motion_Animates_Viewport_Without_Overriding_Height_Or_Scaling_Content()
{
var item = new AtomUICollapseItem
{
Header = "First",
Content = "Content",
MotionDuration = TimeSpan.FromMilliseconds(100)
};
var collapse = new AtomUICollapse
{
IsMotionEnabled = true,
Items =
{
item
}
};
var window = new AvaloniaWindow
{
Width = 360,
Height = 180,
Content = collapse
};
try
{
window.Show();
Dispatcher.UIThread.RunJobs();
item.IsSelected = true;
Dispatcher.UIThread.RunJobs();
var motionActor = GetContentMotionActor(item);
motionActor.IsAnimating(Visual.OpacityProperty).ShouldBeTrue();
motionActor.MotionTransform.ShouldBeNull();
motionActor.Height.ShouldBe(double.NaN);
}
finally
{
window.Close();
}
}
[Fact]
public void Item_Content_Motion_Completes_With_Natural_Height()
{
var item = new AtomUICollapseItem
{
Header = "First",
Content = "Content",
MotionDuration = TimeSpan.FromMilliseconds(1)
};
var collapse = new AtomUICollapse
{
IsMotionEnabled = true,
Items =
{
item
}
};
var window = new AvaloniaWindow
{
Width = 360,
Height = 180,
Content = collapse
};
try
{
window.Show();
Dispatcher.UIThread.RunJobs();
item.IsSelected = true;
Dispatcher.UIThread.RunJobs();
var motionActor = GetContentMotionActor(item);
WaitForLayoutCondition(() => !motionActor.IsAnimating(Visual.OpacityProperty) &&
motionActor.IsVisible == item.IsSelected);
motionActor.Opacity.ShouldBe(1);
motionActor.Height.ShouldBe(double.NaN);
}
finally
{
window.Close();
}
}
[Fact]
public void Item_Content_Motion_Starts_Growing_For_Control_Content()
{
var wrappedContent = new TextBlock
{
Text = "Long wrapped collapse content that needs multiple lines when measured with the item width.",
TextWrapping = TextWrapping.Wrap
};
var item = new AtomUICollapseItem
{
Header = "First",
Content = wrappedContent,
MotionDuration = TimeSpan.FromMilliseconds(100)
};
var collapse = new AtomUICollapse
{
IsMotionEnabled = true,
Items =
{
item
}
};
var window = new AvaloniaWindow
{
Width = 180,
Height = 260,
Content = collapse
};
try
{
window.Show();
Dispatcher.UIThread.RunJobs();
item.IsSelected = true;
Dispatcher.UIThread.RunJobs();
var motionActor = GetContentMotionActor(item);
WaitForLayoutCondition(() => motionActor.Bounds.Height > 0.0);
motionActor.Bounds.Height.ShouldBeGreaterThan(0.0);
}
finally
{
window.Close();
}
}
[Fact]
public void Item_Content_Motion_Starts_Growing_For_String_Content()
{
var item = new AtomUICollapseItem
{
Header = "First",
Content = "String content",
MotionDuration = TimeSpan.FromMilliseconds(100)
};
var collapse = new AtomUICollapse
{
IsMotionEnabled = true,
Items =
{
item
}
};
var window = new AvaloniaWindow
{
Width = 240,
Height = 180,
Content = collapse
};
try
{
window.Show();
Dispatcher.UIThread.RunJobs();
item.IsSelected = true;
Dispatcher.UIThread.RunJobs();
var motionActor = GetContentMotionActor(item);
WaitForLayoutCondition(() => motionActor.Bounds.Height > 0.0);
motionActor.Bounds.Height.ShouldBeGreaterThan(0.0);
}
finally
{
window.Close();
}
}
[Fact]
public void Item_Shell_Separators_Follow_Item_Collection_Structure()
{
var firstItem = new AtomUICollapseItem { Header = "First", Content = "Content" };
var lastItem = new AtomUICollapseItem { Header = "Last", Content = "Content" };
var collapse = new AtomUICollapse
{
BorderThickness = new Thickness(1),
IsMotionEnabled = false,
Items = { firstItem, lastItem }
};
var window = new AvaloniaWindow
{
Width = 360,
Height = 260,
Content = collapse
};
try
{
window.Show();
Dispatcher.UIThread.RunJobs();
GetItemShell(firstItem).BorderThickness.ShouldBe(new Thickness(0, 0, 0, 1));
GetItemShell(lastItem).BorderThickness.ShouldBe(default(Thickness));
collapse.Items.Remove(lastItem);
Dispatcher.UIThread.RunJobs();
GetItemShell(firstItem).BorderThickness.ShouldBe(default(Thickness));
collapse.Items.Add(lastItem);
Dispatcher.UIThread.RunJobs();
GetItemShell(firstItem).BorderThickness.ShouldBe(new Thickness(0, 0, 0, 1));
GetItemShell(lastItem).BorderThickness.ShouldBe(default(Thickness));
}
finally
{
window.Close();
}
}
[Fact]
public void Item_Shell_Separators_Refresh_After_ItemsSource_Reset()
{
var initialFirstItem = new AtomUICollapseItem { Header = "Initial first", Content = "Content" };
var initialLastItem = new AtomUICollapseItem { Header = "Initial last", Content = "Content" };
var resetFirstItem = new AtomUICollapseItem { Header = "Reset first", Content = "Content" };
var resetLastItem = new AtomUICollapseItem { Header = "Reset last", Content = "Content" };
var source = new ResettableObservableCollection<AtomUICollapseItem>
{
initialFirstItem,
initialLastItem
};
var collapse = new AtomUICollapse
{
BorderThickness = new Thickness(1),
IsMotionEnabled = false,
ItemsSource = source
};
var window = new AvaloniaWindow
{
Width = 360,
Height = 260,
Content = collapse
};
try
{
window.Show();
Dispatcher.UIThread.RunJobs();
GetItemShell(initialFirstItem).BorderThickness.ShouldBe(new Thickness(0, 0, 0, 1));
GetItemShell(initialLastItem).BorderThickness.ShouldBe(default(Thickness));
source.Reset(resetFirstItem, resetLastItem);
Dispatcher.UIThread.RunJobs();
GetItemShell(resetFirstItem).BorderThickness.ShouldBe(new Thickness(0, 0, 0, 1));
GetItemShell(resetLastItem).BorderThickness.ShouldBe(default(Thickness));
}
finally
{
window.Close();
}
}
private static PixelAlignedBorder GetItemShell(AtomUICollapseItem item)
{
var shell = item.GetVisualChildren().OfType<PixelAlignedBorder>().SingleOrDefault();
shell.ShouldNotBeNull();
return shell!;
}
private sealed class ResettableObservableCollection<T> : ObservableCollection<T>
{
public void Reset(params T[] items)
{
Items.Clear();
foreach (var item in items)
{
Items.Add(item);
}
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
}
private static PixelAlignedBorder GetHeaderDecorator(AtomUICollapseItem item)
{
var header = FindVisualByName<PixelAlignedBorder>(item, "PART_HeaderDecorator");
header.ShouldNotBeNull();
return header!;
}
private static PixelAlignedBorder GetContentFrame(AtomUICollapseItem item)
{
var frame = FindVisualByName<PixelAlignedBorder>(item, "PART_ContentFrame");
frame.ShouldNotBeNull();
return frame!;
}
private static IconButton GetExpandButton(AtomUICollapseItem item)
{
var button = FindTemplatePart<IconButton>(item, "PART_ExpandButton");
button.ShouldNotBeNull();
return button!;
}
private static ContentPresenter GetHeaderPresenter(AtomUICollapseItem item)
{
var presenter = FindTemplatePart<ContentPresenter>(item, "PART_HeaderPresenter");
presenter.ShouldNotBeNull();
return presenter!;
}
private static BaseMotionActor GetContentMotionActor(AtomUICollapseItem item)
{
var motionActor = FindTemplatePart<BaseMotionActor>(item, "PART_ContentMotionActor");
motionActor.ShouldNotBeNull();
return motionActor!;
}
private static void WaitForLayoutCondition(Func<bool> condition, int timeoutMilliseconds = 500)
{
var deadline = DateTime.UtcNow.AddMilliseconds(timeoutMilliseconds);
while (DateTime.UtcNow < deadline)
{
Dispatcher.UIThread.RunJobs();
AvaloniaHeadlessPlatform.ForceRenderTimerTick();
Dispatcher.UIThread.RunJobs();
if (condition())
{
return;
}
Thread.Sleep(10);
}
AvaloniaHeadlessPlatform.ForceRenderTimerTick();
Dispatcher.UIThread.RunJobs();
condition().ShouldBeTrue("The expected layout or animation state did not settle before the timeout.");
}
private static void Click(Control control, AvaloniaWindow window)
{
var point = control.TranslatePoint(
new Point(control.Bounds.Width / 2, control.Bounds.Height / 2),
window);
point.ShouldNotBeNull();
window.MouseMove(point.Value);
window.MouseDown(point.Value, MouseButton.Left);
window.MouseUp(point.Value, MouseButton.Left);
Dispatcher.UIThread.RunJobs();
}
private static void PressKey(AvaloniaWindow window, Key key, PhysicalKey physicalKey)
{
window.KeyPress(key, RawInputModifiers.None, physicalKey, null);
Dispatcher.UIThread.RunJobs();
}
private static void ShowInWindow(Control content, Action<AvaloniaWindow> assertion)
{
var window = new AvaloniaWindow
{
Width = 360,
Height = 260,
Content = content
};
try
{
window.Show();
Dispatcher.UIThread.RunJobs();
assertion(window);
}
finally
{
window.Close();
}
}
private static T? FindTemplatePart<T>(AtomUICollapseItem item, string name)
where T : Control
{
return item.GetSelfAndVisualDescendants()
.OfType<T>()
.FirstOrDefault(control => control.Name == name &&
ReferenceEquals(control.TemplatedParent, item));
}
private static T? FindVisualByName<T>(Control root, string name)
where T : Control
{
return root.GetSelfAndVisualDescendants()
.OfType<T>()
.FirstOrDefault(control => control.Name == name);
}
private sealed class TestCollapse : AtomUICollapse
{
protected override Type StyleKeyOverride => typeof(AtomUICollapse);
public SelectionMode ExposedSelectionMode => SelectionMode;
public int ExposedSelectedIndex => Selection.SelectedIndex;
public void SelectIndex(int index)
{
var container = Items[index] as Control;
container.ShouldNotBeNull();
UpdateSelectionFromEvent(container!, new RoutedEventArgs());
}
}
private sealed class TestCollapseItem : AtomUICollapseItem
{
protected override Type StyleKeyOverride => typeof(AtomUICollapseItem);
public void ApplyTemplateParts(IconButton expandButton, BaseMotionActor motionActor)
{
var nameScope = new NameScope();
nameScope.Register("PART_ExpandButton", expandButton);
nameScope.Register("PART_ContentMotionActor", motionActor);
OnApplyTemplate(new TemplateAppliedEventArgs(nameScope));
}
}
}