using Xunit; using System; using System.Collections.Generic; using System.Linq; using Avalonia.Controls.Primitives; using Avalonia.Input; using Avalonia.UnitTests; namespace Avalonia.Controls.UnitTests { public class CalendarTests : ScopedTestBase { private static bool CompareDates(DateTime first, DateTime second) { return first.Year == second.Year && first.Month == second.Month && first.Day == second.Day; } [Fact] public void SelectedDatesChanged_Should_Fire_When_SelectedDate_Set() { bool handled = false; Calendar calendar = new Calendar(); calendar.SelectionMode = CalendarSelectionMode.SingleDate; calendar.SelectedDatesChanged += new EventHandler(delegate { handled = true; }); DateTime value = new DateTime(2000, 10, 10); calendar.SelectedDate = value; Assert.True(handled); } [Fact] public void DisplayDateChanged_Should_Fire_When_DisplayDate_Set() { bool handled = false; Calendar calendar = new Calendar(); calendar.SelectionMode = CalendarSelectionMode.SingleDate; calendar.DisplayDateChanged += new EventHandler(delegate { handled = true; }); DateTime value = new DateTime(2000, 10, 10); calendar.DisplayDate = value; Assert.True(handled); } [Fact] public void Setting_Selected_Date_To_Blackout_Date_Should_Throw() { Calendar calendar = new Calendar(); calendar.BlackoutDates.AddDatesInPast(); Assert.ThrowsAny( () => calendar.SelectedDate = DateTime.Today.AddDays(-1)); } [Fact] public void Setting_Selected_Date_To_Blackout_Date_Should_Throw_Range() { Calendar calendar = new Calendar(); calendar.BlackoutDates.Add(new CalendarDateRange(DateTime.Today, DateTime.Today.AddDays(10))); calendar.SelectedDate = DateTime.Today.AddDays(-1); Assert.True(CompareDates(calendar.SelectedDate.Value, DateTime.Today.AddDays(-1))); Assert.True(CompareDates(calendar.SelectedDate.Value, calendar.SelectedDates[0])); calendar.SelectedDate = DateTime.Today.AddDays(11); Assert.True(CompareDates(calendar.SelectedDate.Value, DateTime.Today.AddDays(11))); Assert.True(CompareDates(calendar.SelectedDate.Value, calendar.SelectedDates[0])); Assert.ThrowsAny( () => calendar.SelectedDate = DateTime.Today.AddDays(5)); } [Fact] public void Adding_Blackout_Dates_Containing_Selected_Date_Should_Throw() { Calendar calendar = new Calendar(); calendar.SelectedDate = DateTime.Today.AddDays(5); Assert.ThrowsAny( () => calendar.BlackoutDates.Add(new CalendarDateRange(DateTime.Today, DateTime.Today.AddDays(10)))); } [Fact] public void DisplayDateStartEnd_Should_Constrain_Display_Date() { Calendar calendar = new Calendar(); calendar.SelectionMode = CalendarSelectionMode.SingleDate; calendar.DisplayDateStart = new DateTime(2005, 12, 30); DateTime value = new DateTime(2005, 12, 15); calendar.DisplayDate = value; Assert.True(CompareDates(calendar.DisplayDate, calendar.DisplayDateStart.Value)); value = new DateTime(2005, 12, 30); calendar.DisplayDate = value; Assert.True(CompareDates(calendar.DisplayDate, value)); value = DateTime.MaxValue; calendar.DisplayDate = value; Assert.True(CompareDates(calendar.DisplayDate, value)); calendar.DisplayDateEnd = new DateTime(2010, 12, 30); Assert.True(CompareDates(calendar.DisplayDate, calendar.DisplayDateEnd.Value)); } [Fact] public void Setting_DisplayDateEnd_Should_Alter_DispalyDate_And_DisplayDateStart() { Calendar calendar = new Calendar(); DateTime value = new DateTime(2000, 1, 30); calendar.DisplayDate = value; calendar.DisplayDateEnd = value; calendar.DisplayDateStart = value; Assert.True(CompareDates(calendar.DisplayDateStart.Value, value)); Assert.True(CompareDates(calendar.DisplayDateEnd.Value, value)); value = value.AddMonths(2); calendar.DisplayDateStart = value; Assert.True(CompareDates(calendar.DisplayDateStart.Value, value)); Assert.True(CompareDates(calendar.DisplayDateEnd.Value, value)); Assert.True(CompareDates(calendar.DisplayDate, value)); } [Fact] public void Display_Date_Range_End_Will_Contain_SelectedDate() { Calendar calendar = new Calendar(); calendar.SelectionMode = CalendarSelectionMode.SingleDate; calendar.SelectedDate = DateTime.MaxValue; Assert.True(CompareDates((DateTime)calendar.SelectedDate, DateTime.MaxValue)); calendar.DisplayDateEnd = DateTime.MaxValue.AddDays(-1); Assert.True(CompareDates((DateTime)calendar.DisplayDateEnd, DateTime.MaxValue)); } /// /// The days added to the SelectedDates collection. /// private IList? _selectedDatesChangedAddedDays; /// /// The days removed from the SelectedDates collection. /// private IList? _selectedDatesChangedRemovedDays; /// /// The number of times the SelectedDatesChanged event has been fired. /// private int _selectedDatesChangedCount; /// /// Handle the SelectedDatesChanged event. /// /// The calendar. /// Event arguments. private void OnSelectedDatesChanged(object? sender, SelectionChangedEventArgs e) { _selectedDatesChangedAddedDays = e.AddedItems .Cast() .ToList(); _selectedDatesChangedRemovedDays = e.RemovedItems .Cast() .ToList(); _selectedDatesChangedCount++; } /// /// Clear the variables used to track the SelectedDatesChanged event. /// private void ResetSelectedDatesChanged() { if (_selectedDatesChangedAddedDays != null) { _selectedDatesChangedAddedDays.Clear(); } if (_selectedDatesChangedRemovedDays != null) { _selectedDatesChangedRemovedDays.Clear(); } _selectedDatesChangedCount = 0; } [Fact] public void SingleDate_Selection_Behavior() { ResetSelectedDatesChanged(); Calendar calendar = new Calendar(); calendar.SelectedDatesChanged += new EventHandler(OnSelectedDatesChanged); calendar.SelectionMode = CalendarSelectionMode.SingleDate; calendar.SelectedDate = DateTime.Today; Assert.True(CompareDates(calendar.SelectedDate.Value, DateTime.Today)); Assert.True(calendar.SelectedDates.Count == 1); Assert.True(CompareDates(calendar.SelectedDates[0], DateTime.Today)); Assert.True(_selectedDatesChangedCount == 1); Assert.NotNull(_selectedDatesChangedAddedDays); Assert.True(_selectedDatesChangedAddedDays.Count == 1); Assert.NotNull(_selectedDatesChangedRemovedDays); Assert.True(_selectedDatesChangedRemovedDays.Count == 0); ResetSelectedDatesChanged(); calendar.SelectedDate = DateTime.Today; Assert.True(CompareDates(calendar.SelectedDate.Value, DateTime.Today)); Assert.True(calendar.SelectedDates.Count == 1); Assert.True(CompareDates(calendar.SelectedDates[0], DateTime.Today)); Assert.True(_selectedDatesChangedCount == 0); calendar.ClearValue(Calendar.SelectedDateProperty); calendar.SelectionMode = CalendarSelectionMode.None; Assert.True(calendar.SelectedDates.Count == 0); Assert.Null(calendar.SelectedDate); calendar.SelectionMode = CalendarSelectionMode.SingleDate; calendar.SelectedDates.Add(DateTime.Today.AddDays(1)); Assert.True(CompareDates(calendar.SelectedDate.Value, DateTime.Today.AddDays(1))); Assert.True(calendar.SelectedDates.Count == 1); Assert.ThrowsAny( () => calendar.SelectedDates.Add(DateTime.Today.AddDays(2))); } [Fact] public void SingleRange_Selection_Behavior() { ResetSelectedDatesChanged(); Calendar calendar = new Calendar(); calendar.SelectedDatesChanged += new EventHandler(OnSelectedDatesChanged); calendar.SelectionMode = CalendarSelectionMode.SingleRange; calendar.SelectedDate = DateTime.Today; Assert.True(CompareDates(calendar.SelectedDate.Value, DateTime.Today)); Assert.True(calendar.SelectedDates.Count == 1); Assert.True(CompareDates(calendar.SelectedDates[0], DateTime.Today)); Assert.True(_selectedDatesChangedCount == 1); Assert.NotNull(_selectedDatesChangedAddedDays); Assert.True(_selectedDatesChangedAddedDays.Count == 1); Assert.NotNull(_selectedDatesChangedRemovedDays); Assert.True(_selectedDatesChangedRemovedDays.Count == 0); ResetSelectedDatesChanged(); calendar.SelectedDates.Clear(); Assert.Null(calendar.SelectedDate); ResetSelectedDatesChanged(); calendar.SelectedDates.AddRange(DateTime.Today, DateTime.Today.AddDays(10)); Assert.True(CompareDates(calendar.SelectedDate.Value, DateTime.Today)); Assert.True(calendar.SelectedDates.Count == 11); ResetSelectedDatesChanged(); calendar.SelectedDates.AddRange(DateTime.Today, DateTime.Today.AddDays(10)); Assert.True(calendar.SelectedDates.Count == 11); Assert.True(_selectedDatesChangedCount == 0); ResetSelectedDatesChanged(); calendar.SelectedDates.AddRange(DateTime.Today.AddDays(-20), DateTime.Today); Assert.True(CompareDates(calendar.SelectedDate.Value, DateTime.Today.AddDays(-20))); Assert.True(calendar.SelectedDates.Count == 21); Assert.True(_selectedDatesChangedCount == 1); Assert.True(_selectedDatesChangedAddedDays.Count == 21); Assert.True(_selectedDatesChangedRemovedDays.Count == 11); ResetSelectedDatesChanged(); calendar.SelectedDates.Add(DateTime.Today.AddDays(100)); Assert.True(CompareDates(calendar.SelectedDate.Value, DateTime.Today.AddDays(100))); Assert.True(calendar.SelectedDates.Count == 1); } [Fact] public void AllowTapRangeSelection_Should_Disable_TapToSelectRange() { var calendar = new Calendar(); Assert.True(calendar.AllowTapRangeSelection); // Default should be true calendar.AllowTapRangeSelection = false; Assert.False(calendar.AllowTapRangeSelection); } [Fact] public void TapRangeSelection_Should_Work_In_SingleRange_Mode() { var calendar = new Calendar(); calendar.SelectionMode = CalendarSelectionMode.SingleRange; calendar.AllowTapRangeSelection = true; var startDate = new DateTime(2023, 10, 10); var endDate = new DateTime(2023, 10, 15); // First tap should select start date var firstTapResult = calendar.ProcessTapRangeSelection(startDate); Assert.True(firstTapResult); Assert.Equal(1, calendar.SelectedDates.Count); Assert.True(calendar.SelectedDates.Contains(startDate)); // Second tap should complete the range var secondTapResult = calendar.ProcessTapRangeSelection(endDate); Assert.True(secondTapResult); Assert.Equal(6, calendar.SelectedDates.Count); // 5 days inclusive Assert.True(calendar.SelectedDates.Contains(startDate)); Assert.True(calendar.SelectedDates.Contains(endDate)); } [Fact] public void TapRangeSelection_Should_Not_Work_In_SingleDate_Mode() { var calendar = new Calendar(); calendar.SelectionMode = CalendarSelectionMode.SingleDate; calendar.AllowTapRangeSelection = true; var date = new DateTime(2023, 10, 10); var result = calendar.ProcessTapRangeSelection(date); Assert.False(result); // Should not handle tap range selection } [Fact] public void TapRangeSelection_Should_Handle_Blackout_Dates() { var calendar = new Calendar(); calendar.SelectionMode = CalendarSelectionMode.SingleRange; calendar.AllowTapRangeSelection = true; var startDate = new DateTime(2023, 10, 10); var blackoutDate = new DateTime(2023, 10, 12); var endDate = new DateTime(2023, 10, 15); // Add blackout date in the middle calendar.BlackoutDates.Add(new CalendarDateRange(blackoutDate, blackoutDate)); // First tap calendar.ProcessTapRangeSelection(startDate); Assert.Equal(1, calendar.SelectedDates.Count); // Second tap should restart selection due to blackout date calendar.ProcessTapRangeSelection(endDate); Assert.Equal(1, calendar.SelectedDates.Count); Assert.True(calendar.SelectedDates.Contains(endDate)); Assert.False(calendar.SelectedDates.Contains(startDate)); } [Fact] public void TapRangeSelection_Should_Handle_Reverse_Order_Dates() { var calendar = new Calendar(); calendar.SelectionMode = CalendarSelectionMode.SingleRange; calendar.AllowTapRangeSelection = true; var laterDate = new DateTime(2023, 10, 15); var earlierDate = new DateTime(2023, 10, 10); // First tap on later date calendar.ProcessTapRangeSelection(laterDate); Assert.Equal(1, calendar.SelectedDates.Count); // Second tap on earlier date should still create correct range calendar.ProcessTapRangeSelection(earlierDate); Assert.Equal(6, calendar.SelectedDates.Count); Assert.True(calendar.SelectedDates.Contains(earlierDate)); Assert.True(calendar.SelectedDates.Contains(laterDate)); } [Fact] public void CalendarItem_Should_Reset_Mouse_Down_Flag_On_Detach_From_Visual_Tree() { var calendar = new Calendar(); calendar.SelectionMode = CalendarSelectionMode.SingleDate; var calendarItem = new CalendarItem(); calendarItem.Owner = calendar; // Attach CalendarItem to a visual tree var root = new TestRoot(calendarItem); // Create a day button and simulate mouse left button down, // which sets the internal _isMouseLeftButtonDown flag to true. var date1 = new DateTime(2024, 1, 15); var dayButton1 = new CalendarDayButton { DataContext = date1 }; var pointer = new Pointer(Pointer.GetNextFreeId(), PointerType.Mouse, true); var props = new PointerPointProperties(RawInputModifiers.LeftMouseButton, PointerUpdateKind.LeftButtonPressed); var pressArgs = new PointerPressedEventArgs(dayButton1, pointer, root, default, 0, props, KeyModifiers.None); calendarItem.Cell_MouseLeftButtonDown(dayButton1, pressArgs); // date1 should now be selected Assert.Equal(1, calendar.SelectedDates.Count); Assert.Equal(date1, calendar.SelectedDates[0]); // Detach CalendarItem from visual tree (simulates popup closing // during date selection without a PointerReleased event). root.Child = null; // Create a different day button and simulate mouse enter. // Before the fix, _isMouseLeftButtonDown would still be true, // causing hover to auto-select dates. var date2 = new DateTime(2024, 1, 20); var dayButton2 = new CalendarDayButton { DataContext = date2 }; calendarItem.Cell_MouseEntered(dayButton2, null!); // The selected date should NOT have changed to date2, // because the mouse-down flag was reset when detaching. Assert.Equal(1, calendar.SelectedDates.Count); Assert.Equal(date1, calendar.SelectedDates[0]); } [Fact] public void CalendarItem_Should_Reset_YearView_Mouse_Down_Flag_On_Detach_From_Visual_Tree() { var calendar = new Calendar(); var calendarItem = new CalendarItem(); calendarItem.Owner = calendar; // Attach CalendarItem to a visual tree var root = new TestRoot(calendarItem); // Use reflection to set the _isMouseLeftButtonDownYearView flag, // since Month_CalendarButtonMouseDown is private. var field = typeof(CalendarItem).GetField("_isMouseLeftButtonDownYearView", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); Assert.NotNull(field); field!.SetValue(calendarItem, true); Assert.True((bool)field.GetValue(calendarItem)!); // Detach CalendarItem from visual tree root.Child = null; // Verify the flag was reset Assert.False((bool)field.GetValue(calendarItem)!); } } }