A cross-platform UI framework for .NET
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

635 lines
27 KiB

using Xunit;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Input;
using Avalonia.UnitTests;
using Avalonia.VisualTree;
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<SelectionChangedEventArgs>(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<CalendarDateChangedEventArgs>(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<ArgumentOutOfRangeException>(
() => 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<ArgumentOutOfRangeException>(
() => 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<ArgumentOutOfRangeException>(
() => 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));
}
/// <summary>
/// The days added to the SelectedDates collection.
/// </summary>
private IList<object>? _selectedDatesChangedAddedDays;
/// <summary>
/// The days removed from the SelectedDates collection.
/// </summary>
private IList<object>? _selectedDatesChangedRemovedDays;
/// <summary>
/// The number of times the SelectedDatesChanged event has been fired.
/// </summary>
private int _selectedDatesChangedCount;
/// <summary>
/// Handle the SelectedDatesChanged event.
/// </summary>
/// <param name="sender">The calendar.</param>
/// <param name="e">Event arguments.</param>
private void OnSelectedDatesChanged(object? sender, SelectionChangedEventArgs e)
{
_selectedDatesChangedAddedDays =
e.AddedItems
.Cast<object>()
.ToList();
_selectedDatesChangedRemovedDays =
e.RemovedItems
.Cast<object>()
.ToList();
_selectedDatesChangedCount++;
}
/// <summary>
/// Clear the variables used to track the SelectedDatesChanged event.
/// </summary>
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<SelectionChangedEventArgs>(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<InvalidOperationException>(
() => calendar.SelectedDates.Add(DateTime.Today.AddDays(2)));
}
[Fact]
public void SingleRange_Selection_Behavior()
{
ResetSelectedDatesChanged();
Calendar calendar = new Calendar();
calendar.SelectedDatesChanged += new EventHandler<SelectionChangedEventArgs>(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)!);
}
// --- Week number tests ---
[Fact]
public void IsWeekNumberVisible_Defaults_To_False()
{
var calendar = new Calendar();
Assert.False(calendar.IsWeekNumberVisible);
}
[Fact]
public void WeekNumberRule_Defaults_To_Culture_CalendarWeekRule()
{
var calendar = new Calendar();
Assert.IsType<CalendarWeekRule>(calendar.WeekNumberRule);
}
[Fact]
public void IsWeekNumberVisible_Can_Be_Set()
{
var calendar = new Calendar();
calendar.IsWeekNumberVisible = true;
Assert.True(calendar.IsWeekNumberVisible);
}
[Fact]
public void WeekNumberRule_Can_Be_Set()
{
var calendar = new Calendar();
calendar.WeekNumberRule = CalendarWeekRule.FirstFourDayWeek;
Assert.Equal(CalendarWeekRule.FirstFourDayWeek, calendar.WeekNumberRule);
}
[Theory]
// ISO 8601: week 1 of 2023 starts on Monday 2 Jan 2023
[InlineData(2023, 1, 2, CalendarWeekRule.FirstFourDayWeek, DayOfWeek.Monday, 1)]
// 2022-12-31 is still in ISO week 52 of 2022
[InlineData(2022, 12, 31, CalendarWeekRule.FirstFourDayWeek, DayOfWeek.Monday, 52)]
// .NET bug: 2018-12-31 is a Monday and is ISO week 1 of 2019, not week 53 of 2018
[InlineData(2018, 12, 31, CalendarWeekRule.FirstFourDayWeek, DayOfWeek.Monday, 1)]
// US rule: week 1 always starts on Jan 1
[InlineData(2023, 1, 1, CalendarWeekRule.FirstDay, DayOfWeek.Sunday, 1)]
[InlineData(2023, 12, 31, CalendarWeekRule.FirstDay, DayOfWeek.Sunday, 53)]
public void GetWeekOfYear_Returns_Correct_Week_Number(
int year, int month, int day,
CalendarWeekRule rule,
DayOfWeek firstDayOfWeek,
int expectedWeek)
{
var _calendar = new GregorianCalendar();
var date = new DateTime(year, month, day);
int week = DateTimeHelper.GetWeekOfYear(date, rule, firstDayOfWeek, _calendar);
Assert.Equal(expectedWeek, week);
}
// ------------------------------------------------------------------------
// ISO‑week fallback tests – verify that GetWeekOfYear returns the expected
// week numbers for edge‑case dates.
// ------------------------------------------------------------------------
[Theory]
// 31 Dec 2018 is part of ISO‑week 1 of 2019
[InlineData(2018, 12, 31, CalendarWeekRule.FirstFourDayWeek, DayOfWeek.Monday, 1)]
// 1 Jan 2020 is also ISO‑week 1 (Monday)
[InlineData(2020, 01, 01, CalendarWeekRule.FirstFourDayWeek, DayOfWeek.Monday, 1)]
// 29 Dec 2014 (Monday) should be week 1 of 2015 (ISO)
[InlineData(2014, 12, 29, CalendarWeekRule.FirstFourDayWeek, DayOfWeek.Monday, 1)]
// 30 Dec 2019 (Monday) is week 1 of 2020 (ISO)
[InlineData(2019, 12, 30, CalendarWeekRule.FirstFourDayWeek, DayOfWeek.Monday, 1)]
public void GetWeekOfYear_IsoWeekFallback_Returns_Correct_Week(
int year, int month, int day,
CalendarWeekRule rule,
DayOfWeek firstDay,
int expectedWeek)
{
var _calendar = new GregorianCalendar();
var date = new DateTime(year, month, day);
// DateTimeHelper is the internal helper used by Calendar.
// It falls back to ISO‑week calculation when the culture’s
// CalendarWeekRule does not produce a valid week.
int week = DateTimeHelper.GetWeekOfYear(date, rule, firstDay, _calendar);
Assert.Equal(expectedWeek, week);
}
// ------------------------------------------------------------------------
// Property change refresh tests – ensure the control updates its
// visual state when week‑number related properties are changed.
// ------------------------------------------------------------------------
[Fact]
public void Changing_IsWeekNumberVisible_Toggles_HasWeekNumbers_PseudoClass()
{
var calendar = CreateTestCalendar();
calendar.ApplyTemplate();
calendar.DisplayMode = CalendarMode.Month;
calendar.IsWeekNumberVisible = false;
// Grab the CalendarItem from the visual tree
var calendarItem = calendar.GetVisualDescendants()
.OfType<CalendarItem>()
.FirstOrDefault();
Assert.NotNull(calendarItem);
calendarItem.ApplyTemplate();
// Assert pseudo-class is NOT set
Assert.False(calendarItem.Classes.Contains(":hasweeknumbers"));
// Turn on week numbers
calendar.IsWeekNumberVisible = true;
// Assert pseudo-class IS set
Assert.True(calendarItem.Classes.Contains(":hasweeknumbers"));
}
[Fact]
public void Changing_WeekNumberRule_Refreshes_WeekNumber_Labels()
{
var calendar = CreateTestCalendar();
// Apply template so that the internal CalendarItem is created
calendar.ApplyTemplate();
calendar.DisplayMode = CalendarMode.Month;
calendar.IsWeekNumberVisible = true;
// Use ISO‑rule (FirstFourDayWeek) for the test
calendar.WeekNumberRule = CalendarWeekRule.FirstFourDayWeek;
calendar.FirstDayOfWeek = DayOfWeek.Monday;
calendar.DisplayDate = new DateTime(2021, 01, 04); // First Monday of 2021
// Grab the CalendarItem from the visual tree
var calendarItem = calendar.GetVisualDescendants()
.OfType<CalendarItem>()
.FirstOrDefault();
Assert.NotNull(calendarItem);
calendarItem.ApplyTemplate();
// The first week‑number label should display “1” for the first week of 2021
var weekLabelsGrid = calendarItem.GetVisualDescendants()
.OfType<Grid>()
.FirstOrDefault(x => x.Name == "PART_ElementWeekNumberLabels");
Assert.NotNull(weekLabelsGrid);
var firstLabel = weekLabelsGrid.Children.OfType<ContentControl>().First(x => Grid.GetRow(x) == 2);
Assert.Equal(1, firstLabel.Content);
// Change the rule to use FirstDay (which for 2021‑01‑04 would be week 2)
calendar.WeekNumberRule = CalendarWeekRule.FirstDay;
// Force a layout pass – in unit‑tests this is enough to trigger the update
calendar.InvalidateMeasure();
calendar.UpdateLayout();
// After the rule change the first visible week number should now be “2”
firstLabel = weekLabelsGrid.Children.OfType<ContentControl>().First(x => Grid.GetRow(x) == 2);
Assert.Equal(2, firstLabel.Content);
}
private static Calendar CreateTestCalendar()
{
var cal = new Calendar();
var template = new FuncControlTemplate<Calendar>((c, scope) => new Panel()
{
Name = "PART_Root",
Children =
{
new CalendarItem()
{
Name = "PART_CalendarItem",
Owner = c,
DayTitleTemplate = new FuncTemplate<Control>(() => new TextBlock()),
Template = new FuncControlTemplate<CalendarItem>((_, itemScope) => new Grid()
{
Children =
{
new Grid { Name = "PART_MonthView" }.RegisterInNameScope(itemScope),
new Grid { Name = "PART_YearView" }.RegisterInNameScope(itemScope),
new Grid { Name = "PART_ElementWeekNumberLabels" }.RegisterInNameScope(itemScope)
}
})
}.RegisterInNameScope(scope)
}
}.RegisterInNameScope(scope));
cal.Template = template;
return cal;
}
}
}