using System; using System.Collections; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using System.Threading; using System.Threading.Tasks; using System.Windows.Input; using NUnit.Framework; namespace Xamarin.Forms.Core.UnitTests { [TestFixture] public class ListViewTests : BaseTestFixture { [TearDown] public override void TearDown() { base.TearDown (); Device.PlatformServices = null; Device.Info = null; } [SetUp] public override void Setup () { base.Setup (); Device.PlatformServices = new MockPlatformServices (); Device.Info = new TestDeviceInfo (); } [Test] public void TestConstructor () { var listView = new ListView (); Assert.Null (listView.ItemsSource); Assert.Null (listView.ItemTemplate); Assert.AreEqual (LayoutOptions.FillAndExpand, listView.HorizontalOptions); Assert.AreEqual (LayoutOptions.FillAndExpand, listView.VerticalOptions); } internal class ListItem { public string Name { get; set; } public string Description { get; set; } } [Test] public void TestTemplating () { var cellTemplate = new DataTemplate (typeof (TextCell)); cellTemplate.SetBinding (TextCell.TextProperty, new Binding ("Name")); cellTemplate.SetBinding (TextCell.DetailProperty, new Binding ("Description")); var listView = new ListView { ItemsSource = new[] { new ListItem {Name = "Foo", Description = "Bar"}, new ListItem {Name = "Baz", Description = "Raz"} }, ItemTemplate = cellTemplate }; var cell = (Cell)listView.ItemTemplate.CreateContent (); var textCell = (TextCell)cell; cell.BindingContext = listView.ItemsSource.OfType ().First (); Assert.AreEqual ("Foo", textCell.Text); Assert.AreEqual ("Bar", textCell.Detail); } [Test] public void TemplateNullObject() { var listView = new ListView { ItemsSource = new object[] { null } }; Cell cell = listView.TemplatedItems[0]; Assert.That (cell, Is.Not.Null); Assert.That (cell, Is.InstanceOf()); Assert.That (((TextCell) cell).Text, Is.Null); } [Test] [Description("Setting BindingContext should trickle down to Header and Footer.")] public void SettingBindingContextPassesToHeaderAndFooter() { var bc = new object(); var header = new BoxView(); var footer = new BoxView(); var listView = new ListView { Header = header, Footer = footer, BindingContext = bc, }; Assert.That(header.BindingContext, Is.SameAs(bc)); Assert.That(footer.BindingContext, Is.SameAs(bc)); } [Test] [Description("Setting Header and Footer should pass BindingContext.")] public void SettingHeaderFooterPassesBindingContext() { var bc = new object(); var listView = new ListView { BindingContext = bc, }; var header = new BoxView(); var footer = new BoxView(); listView.Footer = footer; listView.Header = header; Assert.That(header.BindingContext, Is.SameAs(bc)); Assert.That(footer.BindingContext, Is.SameAs(bc)); } [Test] [Description ("Setting GroupDisplayBinding or GroupHeaderTemplate when the other is set should set the other one to null.")] public void SettingGroupHeaderTemplateSetsDisplayBindingToNull() { var listView = new ListView { GroupDisplayBinding = new Binding ("Path") }; listView.GroupHeaderTemplate = new DataTemplate (typeof (TextCell)); Assert.That (listView.GroupDisplayBinding, Is.Null); } [Test] [Description ("Setting GroupDisplayBinding or GroupHeaderTemplate when the other is set should set the other one to null.")] public void SettingGroupDisplayBindingSetsHeaderTemplateToNull() { var listView = new ListView { GroupHeaderTemplate = new DataTemplate (typeof (TextCell)) }; listView.GroupDisplayBinding = new Binding ("Path"); Assert.That (listView.GroupHeaderTemplate, Is.Null); } [Test] [Description ("You should be able to set ItemsSource without having set the other properties first without issue")] public void SettingItemsSourceWithoutBindingsOrItemsSource() { var listView = new ListView { IsGroupingEnabled = true }; Assert.That (() => listView.ItemsSource = new[] { new[] { new object() } }, Throws.Nothing); } [Test] public void DefaultGroupHeaderTemplates() { var items = new[] { new[] { new object() } }; var listView = new ListView { IsGroupingEnabled = true, ItemsSource = items }; var til = (TemplatedItemsList, Cell>)((IList)listView.TemplatedItems)[0]; Cell cell = til.HeaderContent; Assert.That (cell, Is.Not.Null); Assert.That (cell, Is.InstanceOf()); Assert.That (((TextCell) cell).Text, Is.EqualTo (items[0].ToString())); } [Test] [Description ("Tapping a different item (row) that is equal to the current item selection should still raise ItemSelected")] public void NotifyRowTappedDifferentIndex() { string item = "item"; var listView = new ListView { ItemsSource = new[] { item, item } }; listView.NotifyRowTapped (0); bool raised = false; listView.ItemSelected += (sender, arg) => raised = true; listView.NotifyRowTapped (1); Assert.That (raised, Is.True, "ItemSelected was not raised"); } [Test] public void DoesNotCrashWhenAddingToSource () { var items = new ObservableCollection { "Foo", "Bar", "Baz" }; var listView = new ListView { ItemsSource = items, ItemTemplate = new DataTemplate(typeof(TextCell)) }; Assert.DoesNotThrow (() => items.Add ("Blah")); } [Test] public void DoesNotThrowWhenMovingInSource () { var items = new ObservableCollection { "Foo", "Bar", "Baz" }; var listView = new ListView { ItemsSource = items, ItemTemplate = new DataTemplate (typeof (TextCell)) }; Assert.DoesNotThrow (() => items.Move (0, 1)); } [Test] [Description ("A cell being tapped from the UI should raise both tapped events, but not change ItemSelected")] public void NotifyTappedSameItem() { int cellTapped = 0; int itemTapped = 0; int itemSelected = 0; var listView = new ListView { ItemsSource = new[] { "item" }, ItemTemplate = new DataTemplate (() => { var cell = new TextCell(); cell.Tapped += (s, e) => { cellTapped++; }; return cell; }) }; listView.ItemTapped += (sender, arg) => itemTapped++; listView.ItemSelected += (sender, arg) => itemSelected++; listView.NotifyRowTapped (0); Assert.That (cellTapped, Is.EqualTo (1), "Cell.Tapped was not raised"); Assert.That (itemTapped, Is.EqualTo (1), "ListView.ItemTapped was not raised"); Assert.That (itemSelected, Is.EqualTo (1), "ListView.ItemSelected was not raised"); listView.NotifyRowTapped (0); Assert.That (cellTapped, Is.EqualTo (2), "Cell.Tapped was not raised a second time"); Assert.That (itemTapped, Is.EqualTo (2), "ListView.ItemTapped was not raised a second time"); Assert.That (itemSelected, Is.EqualTo (1), "ListView.ItemSelected was raised a second time"); } [Test] public void ScrollTo() { var listView = new ListView { IsPlatformEnabled = true, Platform = new UnitPlatform() }; object item = new object(); bool requested = false; listView.ScrollToRequested += (sender, args) => { requested = true; Assert.That (args.Item, Is.SameAs (item)); Assert.That (args.Group, Is.Null); Assert.That (args.Position, Is.EqualTo (ScrollToPosition.Center)); Assert.That (args.ShouldAnimate, Is.EqualTo (true)); }; listView.ScrollTo (item, ScrollToPosition.Center, animated: true); Assert.That (requested, Is.True); } [Test] public void ScrollToDelayed() { var listView = new ListView(); object item = new object(); bool requested = false; listView.ScrollToRequested += (sender, args) => { requested = true; Assert.That (args.Item, Is.SameAs (item)); Assert.That (args.Group, Is.Null); Assert.That (args.Position, Is.EqualTo (ScrollToPosition.Center)); Assert.That (args.ShouldAnimate, Is.EqualTo (true)); }; listView.ScrollTo (item, ScrollToPosition.Center, animated: true); Assert.That (requested, Is.False); listView.IsPlatformEnabled = true; listView.Platform = new UnitPlatform(); Assert.That (requested, Is.True); } [Test] public void ScrollToGroup() { // Fake a renderer so we pass along messages right away var listView = new ListView { IsPlatformEnabled = true, Platform = new UnitPlatform(), IsGroupingEnabled = true }; object item = new object(); object group = new object(); bool requested = false; listView.ScrollToRequested += (sender, args) => { requested = true; Assert.That (args.Item, Is.SameAs (item)); Assert.That (args.Group, Is.SameAs (group)); Assert.That (args.Position, Is.EqualTo (ScrollToPosition.Center)); Assert.That (args.ShouldAnimate, Is.EqualTo (true)); }; listView.ScrollTo (item, group, ScrollToPosition.Center, animated: true); Assert.That (requested, Is.True); } [Test] public void ScrollToInvalid() { var listView = new ListView { IsPlatformEnabled = true, Platform = new UnitPlatform() }; Assert.That (() => listView.ScrollTo (new object(), (ScrollToPosition) 500, true), Throws.ArgumentException); Assert.That (() => listView.ScrollTo (new object(), new object(), ScrollToPosition.Start, true), Throws.InvalidOperationException); listView.IsGroupingEnabled = true; Assert.That (() => listView.ScrollTo (new object(), new object(), (ScrollToPosition) 500, true), Throws.ArgumentException); } [Test] public void GetSizeRequest () { var listView = new ListView { IsPlatformEnabled = true, Platform = new UnitPlatform (), HasUnevenRows = false, RowHeight = 50, ItemsSource = Enumerable.Range (0, 20).ToList () }; var sizeRequest = listView.GetSizeRequest (double.PositiveInfinity, double.PositiveInfinity); Assert.AreEqual (40, sizeRequest.Minimum.Width); Assert.AreEqual (40, sizeRequest.Minimum.Height); Assert.AreEqual (50, sizeRequest.Request.Width); Assert.AreEqual (50 * 20, sizeRequest.Request.Height); } [Test] public void GetSizeRequestUneven () { var listView = new ListView { IsPlatformEnabled = true, Platform = new UnitPlatform (), HasUnevenRows = true, RowHeight = 50, ItemsSource = Enumerable.Range (0, 20).ToList () }; var sizeRequest = listView.GetSizeRequest (double.PositiveInfinity, double.PositiveInfinity); Assert.AreEqual (40, sizeRequest.Minimum.Width); Assert.AreEqual (40, sizeRequest.Minimum.Height); Assert.AreEqual (50, sizeRequest.Request.Width); Assert.AreEqual (100, sizeRequest.Request.Height); } public class ListItemValue : IComparable { public string Name { get; private set; } public ListItemValue (string name) { Name = name; } int IComparable.CompareTo (ListItemValue value) { return Name.CompareTo (value.Name); } public string Label { get { return Name[0].ToString (); } } } public class ListItemCollection : ObservableCollection { public string Title { get; private set; } public ListItemCollection (string title) { Title = title; } public static List GetSortedData () { var items = ListItems; items.Sort (); return items; } // Data used to populate our list. static readonly List ListItems = new List () { new ListItemValue ("Babbage"), new ListItemValue ("Boole"), new ListItemValue ("Berners-Lee"), new ListItemValue ("Atanasoff"), new ListItemValue ("Allen"), new ListItemValue ("Cormack"), new ListItemValue ("Cray"), new ListItemValue ("Dijkstra"), new ListItemValue ("Dix"), new ListItemValue ("Dewey"), new ListItemValue ("Erdos"), }; } public class TestCell : TextCell { public static int NumberOfCells = 0; public TestCell () { Interlocked.Increment (ref NumberOfCells); } ~TestCell () { Interlocked.Decrement (ref NumberOfCells); } } ObservableCollection SetupList () { var allListItemGroups = new ObservableCollection (); foreach (var item in ListItemCollection.GetSortedData ()) { // Attempt to find any existing groups where theg group title matches the first char of our ListItem's name. var listItemGroup = allListItemGroups.FirstOrDefault (g => g.Title == item.Label); // If the list group does not exist, we create it. if (listItemGroup == null) { listItemGroup = new ListItemCollection (item.Label) { item }; allListItemGroups.Add (listItemGroup); } else { // If the group does exist, we simply add the demo to the existing group. listItemGroup.Add (item); } } return allListItemGroups; } [Test] public void UncollectableHeaderReferences () { var list = new ListView { Platform = new UnitPlatform (), IsPlatformEnabled = true, ItemTemplate = new DataTemplate (typeof (TextCell)) { Bindings = { {TextCell.TextProperty, new Binding ("Name")} } }, GroupHeaderTemplate = new DataTemplate (typeof (TestCell)) { Bindings = { {TextCell.TextProperty, new Binding ("Title")} } }, IsGroupingEnabled = true, ItemsSource = SetupList (), }; Assert.AreEqual (5, TestCell.NumberOfCells); var newList1 = SetupList (); var newList2 = SetupList (); for (var i = 0; i < 400; i++) { list.ItemsSource = i % 2 > 0 ? newList1 : newList2; // grab a header just so we can be sure its reailized var header = list.TemplatedItems.GetGroup (0).HeaderContent; } GC.Collect (); GC.WaitForPendingFinalizers (); // use less or equal because mono will keep the last header var alive no matter what Assert.True (TestCell.NumberOfCells <= 6); var keepAlive = list.ToString (); } [Test] public void CollectionChangedMultipleFires () { var source = new ObservableCollection { "Foo", "Bar" }; var list = new ListView { Platform = new UnitPlatform (), IsPlatformEnabled = true, ItemsSource = source, ItemTemplate = new DataTemplate (typeof (TextCell)) }; int fireCount = 0; list.TemplatedItems.CollectionChanged += (sender, args) => { fireCount++; }; source.Add ("Baz"); Assert.AreEqual (1, fireCount); } [Test] public void GroupedCollectionChangedMultipleFires () { var source = new ObservableCollection> { new ObservableCollection {"Foo"}, new ObservableCollection {"Bar"} }; var list = new ListView { Platform = new UnitPlatform (), IsPlatformEnabled = true, IsGroupingEnabled = true, ItemsSource = source, ItemTemplate = new DataTemplate (typeof (TextCell)) { Bindings = { {TextCell.TextProperty, new Binding (".") } } } }; int fireCount = 0; list.TemplatedItems.GroupedCollectionChanged += (sender, args) => { fireCount++; }; source[0].Add ("Baz"); Assert.AreEqual (1, fireCount); } [Test] public void HeaderAsView() { var label = new Label { Text = "header" }; var lv = new ListView { Header = label }; IListViewController controller = lv; Assert.That (controller.HeaderElement, Is.SameAs (label)); } [Test] public void HeaderTemplated() { var lv = new ListView { Header = "header", HeaderTemplate = new DataTemplate(typeof(Label)) { Bindings = { { Label.TextProperty, new Binding (".") } } } }; IListViewController controller = lv; Assert.That (controller.HeaderElement, Is.Not.Null); Assert.That (controller.HeaderElement, Is.InstanceOf