Skip to content

Commit

Permalink
feat: add support for iselectioninfo to itemsrepeater extensions
Browse files Browse the repository at this point in the history
  • Loading branch information
kazo0 committed Mar 22, 2024
1 parent 7f3037b commit ed21d51
Show file tree
Hide file tree
Showing 6 changed files with 418 additions and 46 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using ItemsRepeater = Microsoft.UI.Xaml.Controls.ItemsRepeater;
using Uno.Toolkit.UI;

#if IS_WINUI
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Controls.Primitives;
#else
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Controls.Primitives;
#endif

namespace Uno.Toolkit.RuntimeTests.Extensions
{
internal static class ItemsRepeaterTestExtensions
{
public static void FakeTapItemAt(this ItemsRepeater ir, int index)
{
if (ir.TryGetElement(index) is { } element)
{
// Fake local tap handler on ToggleButton level.
// For SelectorItem, nothing will happen on tap unless nested under a Selector, which isnt the case here.
(element as ToggleButton)?.Toggle();

// This is what's called in ItemsRepeater::Tapped handler.
// Note that the handler will not trigger from a "fake tap" like the line above, so we have to manually invoke here.
ItemsRepeaterExtensions.ToggleItemSelectionAtCoerced(ir, index);
}
else
{
throw new InvalidOperationException($"Element at index={index} is not yet materialized or out of range.");
}
}
}
}
28 changes: 5 additions & 23 deletions src/Uno.Toolkit.RuntimeTests/Tests/ItemsRepeaterChipTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ public async Task VariousMode_TapSelection(ItemsSelectionMode mode, int[] select

foreach (var i in selectionSequence)
{
FakeTapItemAt(SUT, i);
SUT.FakeTapItemAt(i);
}
expected = (expectation switch
{
Expand All @@ -83,7 +83,7 @@ public async Task SingleMode_Selection()
actual = GetChipsSelectionState(SUT);
Assert.IsTrue(actual.All(x => x == false));

FakeTapItemAt(SUT, 1);
SUT.FakeTapItemAt(1);
actual = GetChipsSelectionState(SUT);
CollectionAssert.AreEqual(expected, actual);
}
Expand Down Expand Up @@ -116,7 +116,7 @@ public async Task SingleOrNoneToSingle_Selected_ShouldPreserveSelection()
bool?[] expected = new bool?[] { false, false, true }, actual;

await UnitTestUIContentHelperEx.SetContentAndWait(SUT);
FakeTapItemAt(SUT, 2);
SUT.FakeTapItemAt(2);
actual = GetChipsSelectionState(SUT);
CollectionAssert.AreEqual(expected, actual);

Expand All @@ -133,8 +133,8 @@ public async Task MultiToSingle_Selected_ShouldPreserveFirstSelection()
bool?[] expected = new bool?[] { false, true, true }, actual;

await UnitTestUIContentHelperEx.SetContentAndWait(SUT);
FakeTapItemAt(SUT, 1);
FakeTapItemAt(SUT, 2);
SUT.FakeTapItemAt(1);
SUT.FakeTapItemAt(2);
actual = GetChipsSelectionState(SUT);
CollectionAssert.AreEqual(expected, actual);

Expand Down Expand Up @@ -176,22 +176,4 @@ internal static ItemsRepeater SetupItemsRepeater(object source, ItemsSelectionMo
.Select((_, i) => (ir.TryGetElement(i) as ChipControl)?.IsChecked)
.ToArray() ?? new bool?[0];
}

internal static void FakeTapItemAt(ItemsRepeater ir, int index)
{
if (ir.TryGetElement(index) is { } element)
{
// Fake local tap handler on ToggleButton level.
// For SelectorItem, nothing will happen on tap unless nested under a Selector, which isnt the case here.
(element as ToggleButton)?.Toggle();

// This is whats called in ItemsRepeater::Tapped handler.
// Note that the handler will not trigger from a "fake tap" like the line above, so we have to manually invoke here.
ItemsRepeaterExtensions.ToggleItemSelectionAtCoerced(ir, index);
}
else
{
throw new InvalidOperationException($"Element at index={index} is not yet materialized or out of range.");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Uno.Toolkit.RuntimeTests.Helpers;
using Uno.Toolkit.UI;
using Uno.UI.RuntimeTests;
using ItemsRepeater = Microsoft.UI.Xaml.Controls.ItemsRepeater;
using static Uno.Toolkit.RuntimeTests.Tests.ItemsRepeaterChipTests; // to borrow helper methods
using Uno.Extensions;
using Uno.Toolkit.RuntimeTests.Extensions;
using Windows.UI.Xaml.Controls.Primitives;

Check failure on line 15 in src/Uno.Toolkit.RuntimeTests/Tests/ItemsRepeaterExtensionTests.ISelectionInfo.cs

View workflow job for this annotation

GitHub Actions / Build and Deploy Job

The type or namespace name 'Xaml' does not exist in the namespace 'Windows.UI' (are you missing an assembly reference?)

Check failure on line 15 in src/Uno.Toolkit.RuntimeTests/Tests/ItemsRepeaterExtensionTests.ISelectionInfo.cs

View workflow job for this annotation

GitHub Actions / Build and Deploy Job

The type or namespace name 'Xaml' does not exist in the namespace 'Windows.UI' (are you missing an assembly reference?)



#if IS_WINUI
using Microsoft.UI.Xaml.Data;
#else
using Windows.UI.Xaml.Data;
#endif

namespace Uno.Toolkit.RuntimeTests.Tests
{
[TestClass]
[RunsOnUIThread]
partial class ItemsRepeaterExtensionsTests
{
[TestMethod]
[DataRow(ItemsSelectionMode.SingleOrNone, null, false, null, DisplayName = $"Select: {nameof(ItemsSelectionMode.SingleOrNone)} none")]
[DataRow(ItemsSelectionMode.SingleOrNone, new[] { 0 }, false, new[] { 0 }, DisplayName = $"Select: {nameof(ItemsSelectionMode.SingleOrNone)} 0")]
[DataRow(ItemsSelectionMode.Multiple, new[] { 0, 2 }, false, new[] { 0, 2 }, DisplayName = $"Select: {nameof(ItemsSelectionMode.Multiple)} (0, 2)")]
[DataRow(ItemsSelectionMode.SingleOrNone, null, true, new[] { 0 }, DisplayName = $"Deselect: {nameof(ItemsSelectionMode.SingleOrNone)} none")]
[DataRow(ItemsSelectionMode.SingleOrNone, new[] { 0 }, true, null, DisplayName = $"Deselect: {nameof(ItemsSelectionMode.SingleOrNone)} 0")]
[DataRow(ItemsSelectionMode.Multiple, new[] { 0, 2 }, true, new[] { 1 }, DisplayName = $"Deselect: {nameof(ItemsSelectionMode.Multiple)} (0, 2)")]
public async Task When_Tapped_With_ISelectionInfo(ItemsSelectionMode mode, int[]? tapSequence, bool isDeselecting, int[]? expected)
{
var selected = isDeselecting ? new HashSet<int>() { 0, 1, 2 } : new HashSet<int>();

var source = SelectionSource.Create(3, isPreselected: x => isDeselecting);
source.DeselectRangeOverride = x => DeselectRangeOverride(x, selected);
source.SelectRangeOverride = x => SelectRangeOverride(x, selected);

var SUT = SetupItemsRepeater(source, mode);

await UnitTestUIContentHelperEx.SetContentAndWait(SUT);

foreach (var i in tapSequence.Safe())
{
SUT.FakeTapItemAt(i);
}

Assert.IsTrue(AreEqual(expected.Safe(), selected));
}

[TestMethod]
[DataRow(ItemsSelectionMode.None, ItemsSelectionMode.Single, null, new[] { 0 }, DisplayName = $"{nameof(ItemsSelectionMode.None)} to {nameof(ItemsSelectionMode.Single)} with [] selected")]
[DataRow(ItemsSelectionMode.None, ItemsSelectionMode.SingleOrNone, null, null, DisplayName = $"{nameof(ItemsSelectionMode.None)} to {nameof(ItemsSelectionMode.SingleOrNone)} with [] selected")]
[DataRow(ItemsSelectionMode.None, ItemsSelectionMode.Multiple, null, null, DisplayName = $"{nameof(ItemsSelectionMode.None)} to {nameof(ItemsSelectionMode.Multiple)} with [] selected")]
[DataRow(ItemsSelectionMode.Single, ItemsSelectionMode.None, new[] { 1 }, null, DisplayName = $"{nameof(ItemsSelectionMode.Single)} to {nameof(ItemsSelectionMode.None)} with [1] selected")]
[DataRow(ItemsSelectionMode.Single, ItemsSelectionMode.SingleOrNone, new[] { 1 }, new[] { 1 }, DisplayName = $"{nameof(ItemsSelectionMode.Single)} to {nameof(ItemsSelectionMode.SingleOrNone)} with [1] selected")]
[DataRow(ItemsSelectionMode.Single, ItemsSelectionMode.Multiple, new[] { 1 }, new[] { 1 }, DisplayName = $"{nameof(ItemsSelectionMode.Single)} to {nameof(ItemsSelectionMode.Multiple)} with [1] selected")]
[DataRow(ItemsSelectionMode.SingleOrNone, ItemsSelectionMode.None, new[] { 1 }, null, DisplayName = $"{nameof(ItemsSelectionMode.SingleOrNone)} to {nameof(ItemsSelectionMode.None)} with [1] selected")]
[DataRow(ItemsSelectionMode.SingleOrNone, ItemsSelectionMode.Single, new[] { 1 }, new[] { 1 }, DisplayName = $"{nameof(ItemsSelectionMode.SingleOrNone)} to {nameof(ItemsSelectionMode.Single)} with [1] selected")]
[DataRow(ItemsSelectionMode.SingleOrNone, ItemsSelectionMode.Multiple, new[] { 1 }, new[] { 1 }, DisplayName = $"{nameof(ItemsSelectionMode.SingleOrNone)} to {nameof(ItemsSelectionMode.Multiple)} with [1] selected")]
[DataRow(ItemsSelectionMode.SingleOrNone, ItemsSelectionMode.None, null, null, DisplayName = $"{nameof(ItemsSelectionMode.SingleOrNone)} to {nameof(ItemsSelectionMode.None)} with [] selected")]
[DataRow(ItemsSelectionMode.SingleOrNone, ItemsSelectionMode.Single, null, new[] { 0 }, DisplayName = $"{nameof(ItemsSelectionMode.SingleOrNone)} to {nameof(ItemsSelectionMode.Single)} with [] selected")]
[DataRow(ItemsSelectionMode.SingleOrNone, ItemsSelectionMode.Multiple, null, null, DisplayName = $"{nameof(ItemsSelectionMode.SingleOrNone)} to {nameof(ItemsSelectionMode.Multiple)} with [] selected")]
[DataRow(ItemsSelectionMode.Multiple, ItemsSelectionMode.None, new[] { 1, 2 }, null, DisplayName = $"{nameof(ItemsSelectionMode.Multiple)} to {nameof(ItemsSelectionMode.None)} with [1, 2] selected")]
[DataRow(ItemsSelectionMode.Multiple, ItemsSelectionMode.Single, new[] { 1, 2 }, new[] { 1 }, DisplayName = $"{nameof(ItemsSelectionMode.Multiple)} to {nameof(ItemsSelectionMode.Single)} with [1, 2] selected")]
[DataRow(ItemsSelectionMode.Multiple, ItemsSelectionMode.SingleOrNone, new[] { 1, 2 }, new[] { 1 }, DisplayName = $"{nameof(ItemsSelectionMode.Multiple)} to {nameof(ItemsSelectionMode.SingleOrNone)} with [1, 2] selected")]
public async Task When_Mode_Changed_ISelectionInfo(ItemsSelectionMode originalMode, ItemsSelectionMode newMode, int[]? selectedIndexes, int[]? expectedIndexes)
{
var selected = new HashSet<int>();

var source = SelectionSource.Create(3, isPreselected: x => selectedIndexes.Safe().Contains(x));
source.DeselectRangeOverride = x => DeselectRangeOverride(x, selected);
source.SelectRangeOverride = x => SelectRangeOverride(x, selected);

var SUT = SetupItemsRepeater(source, originalMode);

await UnitTestUIContentHelperEx.SetContentAndWait(SUT);

ItemsRepeaterExtensions.SetSelectionMode(SUT, newMode);

Assert.IsTrue(AreEqual(expectedIndexes.Safe(), selected));
}

[TestMethod]
public async Task When_Source_Changed_With_ISelectionInfo()
{
var selected = new HashSet<int>();

var evenSource = SelectionSource.Create(4, isPreselected: x => x % 2 == 0);
var oddSource = SelectionSource.Create(4, isPreselected: x => x % 2 == 1);
oddSource.DeselectRangeOverride = x => DeselectRangeOverride(x, selected);
oddSource.SelectRangeOverride = x => SelectRangeOverride(x, selected);

var SUT = SetupItemsRepeater(evenSource, ItemsSelectionMode.Multiple);

await UnitTestUIContentHelperEx.SetContentAndWait(SUT);

Assert.IsTrue(ItemsRepeaterExtensions.GetSelectedIndex(SUT) == 0);
Assert.IsTrue(AreEqual(ItemsRepeaterExtensions.GetSelectedIndexes(SUT), new[] { 0, 2 } ));

SUT.ItemsSource = oddSource;

Assert.IsTrue(ItemsRepeaterExtensions.GetSelectedIndex(SUT) == 1);
Assert.IsTrue(AreEqual(ItemsRepeaterExtensions.GetSelectedIndexes(SUT), new[] { 1, 3 }));
}


// Checks equality of two lists based on values, ignoring order
private static bool AreEqual(IEnumerable<int> expected, IEnumerable<int> actual)
{
return Enumerable.SequenceEqual(expected.OrderBy(x => x), actual.OrderBy(x => x));
}

private static void DeselectRangeOverride(ItemIndexRange range, ICollection<int> selected)
{
range.ExpandRange().ForEach(idx => selected.Remove(idx));
}

private static void SelectRangeOverride(ItemIndexRange range, ICollection<int> selected)
{
selected.AddRange(range.ExpandRange());
}
}
}

public class SelectionData
{
public int Value { get; set; }
public bool Selected { get; set; }

public override string ToString() => Value.ToString();
}
public class SelectionSource : List<SelectionData>, ISelectionInfo
{
public Action<ItemIndexRange> SelectRangeOverride { get; set; }
public Action<ItemIndexRange> DeselectRangeOverride { get; set; }
public Func<int, bool> IsSelectedOverride { get; set; }
public Func<IReadOnlyList<ItemIndexRange>> GetSelectedRangesOverride { get; set; }

public SelectionSource(IEnumerable<SelectionData> source) : base(source)
{
this.SelectRangeOverride = SelectRangeImpl;
this.DeselectRangeOverride = DeselectRangeImpl;
this.IsSelectedOverride = IsSelectedImpl;
this.GetSelectedRangesOverride = GetSelectedRangesImpl;
}
public static SelectionSource Create(int count, Func<int, bool> isPreselected) => Create(Enumerable.Range(0, count), isPreselected);
public static SelectionSource Create(IEnumerable<int> source, Func<int, bool> isPreselected)
{
return new(source.Select(x => new SelectionData
{
Value = x,
Selected = isPreselected(x),
}));
}

// ISelectionInfo
public void SelectRange(ItemIndexRange itemIndexRange) => SelectRangeOverride(itemIndexRange);
public void DeselectRange(ItemIndexRange itemIndexRange) => DeselectRangeOverride(itemIndexRange);
public bool IsSelected(int index) => IsSelectedOverride(index);
public IReadOnlyList<ItemIndexRange> GetSelectedRanges() => GetSelectedRangesOverride();

// ISelectionInfo impl
internal void SelectRangeImpl(ItemIndexRange itemIndexRange)
{
foreach (var index in ExpandRange(itemIndexRange))
{
this[index].Selected = true;
}
}
internal void DeselectRangeImpl(ItemIndexRange itemIndexRange)
{
foreach (var index in ExpandRange(itemIndexRange))
{
this[index].Selected = false;
}
}
internal bool IsSelectedImpl(int index) => this[index].Selected;
internal IReadOnlyList<ItemIndexRange> GetSelectedRangesImpl()
{
return ReduceToRange(this
.Select((x, i) => (Index: i, x.Selected))
.Where(x => x.Selected)
.Select(x => x.Index)
).ToArray();
}

// helper methods
internal static IEnumerable<ItemIndexRange> ReduceToRange(IEnumerable<int> indexes)
{
int first = int.MinValue;
uint n = 0;
foreach (var i in indexes.OrderBy(x => x))
{
if (first + n == i)
{
n++;
}
else
{
if (n > 0) yield return new(first, n);

first = i;
n = 1;
}
}

if (n > 0)
{
yield return new(first, n);
}
}
internal static IEnumerable<int> ExpandRange(ItemIndexRange range) => Enumerable.Range(range.FirstIndex, (int)range.Length);
}

Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,21 @@
using Uno.UI.RuntimeTests;
using ItemsRepeater = Microsoft.UI.Xaml.Controls.ItemsRepeater;
using static Uno.Toolkit.RuntimeTests.Tests.ItemsRepeaterChipTests; // to borrow helper methods
using Uno.Extensions;
using Uno.Toolkit.RuntimeTests.Extensions;


#if IS_WINUI
using Microsoft.UI.Xaml.Data;
#else
using Windows.UI.Xaml.Data;
#endif

namespace Uno.Toolkit.RuntimeTests.Tests;

[TestClass]
[RunsOnUIThread]
internal class ItemsRepeaterExtensionTests
internal partial class ItemsRepeaterExtensionTests
{
[TestMethod]
[DataRow(nameof(ItemsRepeaterExtensions.SelectedItemProperty))]
Expand Down
Loading

0 comments on commit ed21d51

Please sign in to comment.