mirror of
https://github.com/AtomUI/AtomUI.git
synced 2026-10-27 21:58:05 +08:00
2160 lines
82 KiB
C#
2160 lines
82 KiB
C#
using System.Xml.Linq;
|
|
using AtomUI.Desktop.Controls.CalendarView;
|
|
using AtomUI.Desktop.Controls.CalendarView.Infrastructure;
|
|
using AtomUI.Desktop.Controls.CalendarView.State;
|
|
using AtomUI.Desktop.Controls.Primitives;
|
|
using AtomUI.Localization;
|
|
using Avalonia;
|
|
using Avalonia.Controls;
|
|
using Avalonia.Controls.Metadata;
|
|
using Avalonia.Controls.Primitives;
|
|
using Avalonia.Input;
|
|
using Avalonia.Threading;
|
|
using Avalonia.VisualTree;
|
|
using Shouldly;
|
|
using Xunit;
|
|
using AvaloniaGrid = Avalonia.Controls.Grid;
|
|
using AvaloniaWindow = Avalonia.Controls.Window;
|
|
using PickerCalendar = AtomUI.Desktop.Controls.CalendarView.Calendar;
|
|
using PickerCalendarMode = AtomUI.Desktop.Controls.CalendarView.CalendarMode;
|
|
using PickerCalendarButton = AtomUI.Desktop.Controls.CalendarView.CalendarButton;
|
|
using PickerCalendarItem = AtomUI.Desktop.Controls.CalendarView.CalendarItem;
|
|
using PickerCalendarDayButton = AtomUI.Desktop.Controls.CalendarView.CalendarDayButton;
|
|
using PickerDualMonthCalendarItem = AtomUI.Desktop.Controls.CalendarView.DualMonthCalendarItem;
|
|
|
|
namespace AtomUI.Desktop.Controls.Tests.DatePickers;
|
|
|
|
public class CalendarViewStateTests
|
|
{
|
|
static CalendarViewStateTests()
|
|
{
|
|
AvaloniaTestApp.EnsureInitialized();
|
|
}
|
|
|
|
[Fact]
|
|
public void CalendarItem_Declares_TemplateParts_Used_By_Default_Themes()
|
|
{
|
|
RunOnUIThread(() =>
|
|
{
|
|
var expectedParts = new Dictionary<string, Type>
|
|
{
|
|
["PART_HeaderButton"] = typeof(HeadTextButton),
|
|
["PART_HeaderLayout"] = typeof(Panel),
|
|
["PART_MonthViewLayout"] = typeof(UniformGrid),
|
|
["PART_MonthView"] = typeof(AvaloniaGrid),
|
|
["PART_PreviousButton"] = typeof(IconButton),
|
|
["PART_PreviousMonthButton"] = typeof(IconButton),
|
|
["PART_NextButton"] = typeof(IconButton),
|
|
["PART_NextMonthButton"] = typeof(IconButton),
|
|
["PART_YearView"] = typeof(AvaloniaGrid)
|
|
};
|
|
|
|
AssertTemplateParts(typeof(PickerCalendarItem), expectedParts);
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void DualMonthCalendarItem_Declares_Secondary_TemplateParts()
|
|
{
|
|
RunOnUIThread(() =>
|
|
{
|
|
var expectedParts = new Dictionary<string, Type>
|
|
{
|
|
["PART_SecondaryMonthView"] = typeof(AvaloniaGrid),
|
|
["PART_SecondaryHeaderButton"] = typeof(HeadTextButton),
|
|
["PART_SecondaryPreviousButton"] = typeof(IconButton),
|
|
["PART_SecondaryPreviousMonthButton"] = typeof(IconButton),
|
|
["PART_SecondaryNextButton"] = typeof(IconButton),
|
|
["PART_SecondaryNextMonthButton"] = typeof(IconButton),
|
|
["PART_YearViewLayout"] = typeof(UniformGrid),
|
|
["PART_SecondaryYearView"] = typeof(AvaloniaGrid)
|
|
};
|
|
|
|
AssertTemplateParts(typeof(PickerDualMonthCalendarItem), expectedParts);
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void DualMonthRangeCalendar_Theme_BasedOn_CalendarTheme()
|
|
{
|
|
var source = ReadRepoFile("src/AtomUI.Desktop.Controls/DatePicker/Themes/CalendarView/DualMonthRangeCalendarTheme.axaml");
|
|
|
|
source.ShouldContain("<atom:CalendarTheme TargetType=\"calendarView:DualMonthRangeCalendar\" />");
|
|
source.ShouldNotContain("<atom:CalendarItemTheme TargetType=\"calendarView:DualMonthRangeCalendar\" />");
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("src/AtomUI.Desktop.Controls/DatePicker/Themes/CalendarView/CalendarDayButtonTheme.axaml")]
|
|
[InlineData("src/AtomUI.Desktop.Controls/DatePicker/Themes/CalendarView/CalendarButtonTheme.axaml")]
|
|
public void Calendar_Cell_Themes_Define_Explicit_Disabled_Visuals(string relativePath)
|
|
{
|
|
var source = ReadRepoFile(relativePath);
|
|
var document = XDocument.Parse(source);
|
|
var avaloniaNamespace = XNamespace.Get("https://github.com/avaloniaui");
|
|
var disabledStyle = document.Descendants(avaloniaNamespace + "Style")
|
|
.Single(style => (string?)style.Attribute("Selector") == "^:disabled");
|
|
var disabledBackgroundStyle = disabledStyle.Elements(avaloniaNamespace + "Style")
|
|
.Single(style => (string?)style.Attribute("Selector") ==
|
|
"^ /template/ Border#DisabledBackground");
|
|
|
|
source.ShouldContain("<Style Selector=\"^:disabled\">");
|
|
source.ShouldContain("<Border Name=\"DisabledBackground\"");
|
|
source.ShouldContain("CellBgDisabled");
|
|
source.ShouldContain("ColorTextDisabled");
|
|
var disabledBackground = document.Descendants(avaloniaNamespace + "Border")
|
|
.Single(border => (string?)border.Attribute("Name") == "DisabledBackground");
|
|
disabledBackground.Attribute("CornerRadius").ShouldBeNull();
|
|
disabledBackground.Attribute("Margin").ShouldBeNull();
|
|
disabledBackground.Attribute("Grid.ColumnSpan")?.Value.ShouldBe("2");
|
|
disabledStyle.Elements(avaloniaNamespace + "Setter")
|
|
.Any(setter => (string?)setter.Attribute("Property") == "Background" &&
|
|
((string?)setter.Attribute("Value"))?.Contains("CellBgDisabled", StringComparison.Ordinal) == true)
|
|
.ShouldBeFalse();
|
|
disabledBackgroundStyle.Elements(avaloniaNamespace + "Setter")
|
|
.Any(setter => (string?)setter.Attribute("Property") == "Background" &&
|
|
((string?)setter.Attribute("Value"))?.Contains("CellBgDisabled", StringComparison.Ordinal) == true)
|
|
.ShouldBeTrue();
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("src/AtomUI.Desktop.Controls/DatePicker/Themes/DatePickerPresenterTheme.axaml")]
|
|
[InlineData("src/AtomUI.Desktop.Controls/DatePicker/Themes/DualMonthRangeDatePickerPresenterTheme.axaml")]
|
|
[InlineData("src/AtomUI.Desktop.Controls/DatePicker/Themes/TimedRangeDatePickerPresenterTheme.axaml")]
|
|
public void DatePicker_Presenter_Themes_Do_Not_Bind_Raw_Selected_Values_To_Calendar(string relativePath)
|
|
{
|
|
var source = ReadRepoFile(relativePath);
|
|
|
|
source.ShouldNotContain("SelectedDate=\"{TemplateBinding SelectedDateTime}\"");
|
|
source.ShouldNotContain("SecondarySelectedDate=\"{TemplateBinding SecondarySelectedDateTime}\"");
|
|
}
|
|
|
|
[Fact]
|
|
public void RangeDatePickerPresenter_Now_With_Confirm_Confirms_Active_Range_Part()
|
|
{
|
|
var presenter = new TestRangeDatePickerPresenter
|
|
{
|
|
IsNeedConfirm = true,
|
|
IsRangeStartActive = true
|
|
};
|
|
var rangePartConfirmedCount = 0;
|
|
presenter.RangePartConfirmed += (_, _) => rangePartConfirmedCount++;
|
|
|
|
presenter.NotifyNowButtonClickedForTest();
|
|
|
|
presenter.SelectedDateTime.ShouldNotBeNull();
|
|
rangePartConfirmedCount.ShouldBe(1);
|
|
}
|
|
|
|
[Fact]
|
|
public void RangeDatePickerPresenter_Today_And_Now_Do_Not_Select_Out_Of_Range_Values()
|
|
{
|
|
var presenter = new TestRangeDatePickerPresenter
|
|
{
|
|
MinDate = DateTime.Today.AddDays(1),
|
|
MaxDate = DateTime.Today.AddDays(10)
|
|
};
|
|
var rangePartConfirmedCount = 0;
|
|
presenter.RangePartConfirmed += (_, _) => rangePartConfirmedCount++;
|
|
|
|
presenter.NotifyTodayButtonClickedForTest();
|
|
presenter.NotifyNowButtonClickedForTest();
|
|
|
|
presenter.SelectedDateTime.ShouldBeNull();
|
|
presenter.SecondarySelectedDateTime.ShouldBeNull();
|
|
rangePartConfirmedCount.ShouldBe(0);
|
|
}
|
|
|
|
[Fact]
|
|
public void TimedRangeDatePickerPresenter_Now_With_Confirm_Only_Selects_Active_Date()
|
|
{
|
|
var presenter = new TestTimedRangeDatePickerPresenter
|
|
{
|
|
IsNeedConfirm = true,
|
|
IsRangeStartActive = true
|
|
};
|
|
var rangePartConfirmedCount = 0;
|
|
presenter.RangePartConfirmed += (_, _) => rangePartConfirmedCount++;
|
|
|
|
presenter.NotifyNowButtonClickedForTest();
|
|
|
|
presenter.SelectedDateTime.ShouldNotBeNull();
|
|
rangePartConfirmedCount.ShouldBe(0);
|
|
}
|
|
|
|
[Fact]
|
|
public void CalendarItem_ClearGeneratedMonthView_Releases_Owner_FocusButton()
|
|
{
|
|
RunOnUIThread(() =>
|
|
{
|
|
var calendar = new PickerCalendar();
|
|
var item = new TestCalendarItem();
|
|
var monthView = new AvaloniaGrid();
|
|
var dayButton = new PickerCalendarDayButton
|
|
{
|
|
Owner = calendar,
|
|
IsCurrent = true
|
|
};
|
|
calendar.FocusButton = dayButton;
|
|
monthView.Children.Add(dayButton);
|
|
|
|
item.ClearGeneratedMonthViewForTest(monthView);
|
|
|
|
calendar.FocusButton.ShouldBeNull();
|
|
dayButton.Owner.ShouldBeNull();
|
|
dayButton.IsCurrent.ShouldBeFalse();
|
|
monthView.Children.Count.ShouldBe(0);
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void CalendarItem_UpdateYearViewSelection_Tolerates_Missing_Previous_FocusButton()
|
|
{
|
|
RunOnUIThread(() =>
|
|
{
|
|
var selectedMonth = new DateTime(2026, 8, 1);
|
|
var calendar = new PickerCalendar
|
|
{
|
|
DisplayMode = PickerCalendarMode.Year
|
|
};
|
|
var item = new TestCalendarItem
|
|
{
|
|
Owner = calendar
|
|
};
|
|
var monthButton = new PickerCalendarButton
|
|
{
|
|
DataContext = selectedMonth
|
|
};
|
|
|
|
item.UpdateYearViewSelection(monthButton);
|
|
|
|
calendar.FocusCalendarButton.ShouldBe(monthButton);
|
|
calendar.SelectedMonth.ShouldBe(selectedMonth);
|
|
});
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(DatePickerMode.Month, PickerCalendarMode.Year, 2026, 5, 1, 2027, 8, 1)]
|
|
[InlineData(DatePickerMode.Quarter, PickerCalendarMode.Year, 2026, 4, 1, 2027, 7, 1)]
|
|
[InlineData(DatePickerMode.Year, PickerCalendarMode.Decade, 2026, 1, 1, 2032, 1, 1)]
|
|
public void RangeCalendarItem_CalendarButton_Hover_Updates_Range_Preview_Date(
|
|
DatePickerMode pickerMode,
|
|
PickerCalendarMode displayMode,
|
|
int startYear,
|
|
int startMonth,
|
|
int startDay,
|
|
int hoverYear,
|
|
int hoverMonth,
|
|
int hoverDay)
|
|
{
|
|
RunOnUIThread(() =>
|
|
{
|
|
var startDate = new DateTime(startYear, startMonth, startDay);
|
|
var hoverDate = new DateTime(hoverYear, hoverMonth, hoverDay);
|
|
var calendar = new RangeCalendar
|
|
{
|
|
PickerMode = pickerMode,
|
|
DisplayMode = displayMode,
|
|
SelectedDate = startDate,
|
|
IsSelectRangeStart = false
|
|
};
|
|
var item = new TestRangeCalendarItem
|
|
{
|
|
Owner = calendar
|
|
};
|
|
var hoverButton = new PickerCalendarButton
|
|
{
|
|
DataContext = hoverDate,
|
|
IsEnabled = true
|
|
};
|
|
|
|
item.HandleMonthMouseEnteredForTest(hoverButton);
|
|
|
|
calendar.HoverDateTime.ShouldBe(hoverDate);
|
|
var state = calendar.SyncAndGetCurrentViewState();
|
|
state.RangeSelection.TryGetPreviewRange(out var previewStart, out var previewEnd).ShouldBeTrue();
|
|
previewStart.ShouldBe(startDate);
|
|
previewEnd.ShouldBe(hoverDate);
|
|
});
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(DatePickerMode.Date, PickerCalendarMode.Month)]
|
|
[InlineData(DatePickerMode.Week, PickerCalendarMode.Month)]
|
|
[InlineData(DatePickerMode.Month, PickerCalendarMode.Year)]
|
|
[InlineData(DatePickerMode.Quarter, PickerCalendarMode.Year)]
|
|
[InlineData(DatePickerMode.Year, PickerCalendarMode.Decade)]
|
|
public void Calendar_PickerMode_Change_Uses_Target_DisplayMode(DatePickerMode pickerMode, PickerCalendarMode expectedDisplayMode)
|
|
{
|
|
RunOnUIThread(() =>
|
|
{
|
|
var calendar = new PickerCalendar
|
|
{
|
|
PickerMode = pickerMode
|
|
};
|
|
|
|
calendar.DisplayMode.ShouldBe(expectedDisplayMode);
|
|
});
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(DatePickerMode.Month, 7, 2026, 7, 1)]
|
|
[InlineData(DatePickerMode.Quarter, 8, 2026, 7, 1)]
|
|
public void CalendarItem_Year_Mode_Target_Picker_Selects_Unit_Without_Drilling_Down(
|
|
DatePickerMode pickerMode,
|
|
int sourceMonth,
|
|
int expectedYear,
|
|
int expectedMonth,
|
|
int expectedDay)
|
|
{
|
|
RunOnUIThread(() =>
|
|
{
|
|
var calendar = new PickerCalendar
|
|
{
|
|
PickerMode = pickerMode,
|
|
DisplayMode = PickerCalendarMode.Year
|
|
};
|
|
var item = new TestCalendarItem
|
|
{
|
|
Owner = calendar
|
|
};
|
|
var button = new PickerCalendarButton
|
|
{
|
|
DataContext = new DateTime(2026, sourceMonth, 1)
|
|
};
|
|
DateTime? emittedDate = null;
|
|
calendar.DateSelected += (_, args) => emittedDate = args.Date;
|
|
|
|
item.HandleMonthCalendarButtonMouseUpForTest(button);
|
|
|
|
var expectedDate = new DateTime(expectedYear, expectedMonth, expectedDay);
|
|
calendar.SelectedDate.ShouldBe(expectedDate);
|
|
emittedDate.ShouldBe(expectedDate);
|
|
calendar.DisplayMode.ShouldBe(PickerCalendarMode.Year);
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void CalendarItem_Year_Picker_Selects_Year_From_Decade_Mode()
|
|
{
|
|
RunOnUIThread(() =>
|
|
{
|
|
var calendar = new PickerCalendar
|
|
{
|
|
PickerMode = DatePickerMode.Year,
|
|
DisplayMode = PickerCalendarMode.Decade
|
|
};
|
|
var item = new TestCalendarItem
|
|
{
|
|
Owner = calendar
|
|
};
|
|
var button = new PickerCalendarButton
|
|
{
|
|
DataContext = new DateTime(2028, 1, 1)
|
|
};
|
|
DateTime? emittedDate = null;
|
|
calendar.DateSelected += (_, args) => emittedDate = args.Date;
|
|
|
|
item.HandleMonthCalendarButtonMouseUpForTest(button);
|
|
|
|
var expectedDate = new DateTime(2028, 1, 1);
|
|
calendar.SelectedDate.ShouldBe(expectedDate);
|
|
emittedDate.ShouldBe(expectedDate);
|
|
calendar.DisplayMode.ShouldBe(PickerCalendarMode.Decade);
|
|
});
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(DatePickerMode.Month, PickerCalendarMode.Year, 2026, 8, 1, 2026, 8, 1)]
|
|
[InlineData(DatePickerMode.Quarter, PickerCalendarMode.Year, 2026, 8, 1, 2026, 7, 1)]
|
|
[InlineData(DatePickerMode.Year, PickerCalendarMode.Decade, 2028, 1, 1, 2028, 1, 1)]
|
|
public void Calendar_Enter_Key_Selects_Target_Picker_Unit_Without_Drilling_Down(
|
|
DatePickerMode pickerMode,
|
|
PickerCalendarMode displayMode,
|
|
int sourceYear,
|
|
int sourceMonth,
|
|
int sourceDay,
|
|
int expectedYear,
|
|
int expectedMonth,
|
|
int expectedDay)
|
|
{
|
|
RunOnUIThread(() =>
|
|
{
|
|
var sourceDate = new DateTime(sourceYear, sourceMonth, sourceDay);
|
|
var calendar = new PickerCalendar
|
|
{
|
|
PickerMode = pickerMode,
|
|
DisplayMode = displayMode
|
|
};
|
|
if (displayMode == PickerCalendarMode.Year)
|
|
{
|
|
calendar.SelectedMonth = sourceDate;
|
|
}
|
|
else
|
|
{
|
|
calendar.SelectedYear = sourceDate;
|
|
}
|
|
|
|
DateTime? emittedDate = null;
|
|
calendar.DateSelected += (_, args) => emittedDate = args.Date;
|
|
|
|
var handled = calendar.ProcessCalendarKey(new KeyEventArgs
|
|
{
|
|
Key = Key.Enter,
|
|
PhysicalKey = PhysicalKey.Enter,
|
|
KeyModifiers = KeyModifiers.None
|
|
});
|
|
|
|
var expectedDate = new DateTime(expectedYear, expectedMonth, expectedDay);
|
|
handled.ShouldBeTrue();
|
|
calendar.SelectedDate.ShouldBe(expectedDate);
|
|
emittedDate.ShouldBe(expectedDate);
|
|
calendar.DisplayMode.ShouldBe(displayMode);
|
|
});
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(Key.Down, 2, 5)]
|
|
[InlineData(Key.Up, 5, 2)]
|
|
public void Calendar_Year_Mode_Up_Down_Keys_Follow_Month_Selection_Panel_Columns(
|
|
Key key,
|
|
int sourceMonth,
|
|
int expectedMonth)
|
|
{
|
|
RunOnUIThread(() =>
|
|
{
|
|
var calendar = new PickerCalendar
|
|
{
|
|
PickerMode = DatePickerMode.Month,
|
|
DisplayMode = PickerCalendarMode.Year
|
|
};
|
|
calendar.SelectedMonth = new DateTime(2026, sourceMonth, 1);
|
|
|
|
var handled = calendar.ProcessCalendarKey(new KeyEventArgs
|
|
{
|
|
Key = key,
|
|
PhysicalKey = key == Key.Down ? PhysicalKey.ArrowDown : PhysicalKey.ArrowUp,
|
|
KeyModifiers = KeyModifiers.None
|
|
});
|
|
|
|
handled.ShouldBeTrue();
|
|
calendar.SelectedMonth.ShouldBe(new DateTime(2026, expectedMonth, 1));
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void Calendar_SelectedDate_Change_Refreshes_Selected_Day_Button()
|
|
{
|
|
RunOnUIThread(() =>
|
|
{
|
|
var calendar = new PickerCalendar
|
|
{
|
|
DisplayDate = new DateTime(2026, 6, 1),
|
|
SelectedDate = new DateTime(2026, 6, 10)
|
|
};
|
|
|
|
calendar.SelectedDate.ShouldBe(new DateTime(2026, 6, 10));
|
|
|
|
ShowInWindow(calendar, () =>
|
|
{
|
|
var firstButton = FindDayButton(calendar, new DateTime(2026, 6, 10));
|
|
var state = calendar.SyncAndGetCurrentViewState();
|
|
IsSelected(firstButton).ShouldBeTrue(
|
|
$"SelectedDate={calendar.SelectedDate:yyyy-MM-dd}; StateSelected={state.SelectedDate:yyyy-MM-dd}; " +
|
|
$"StatePickerMode={state.PickerMode}; ButtonIsSelected={firstButton.IsSelected}");
|
|
|
|
calendar.SelectedDate = new DateTime(2026, 6, 12);
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
IsSelected(FindDayButton(calendar, new DateTime(2026, 6, 10))).ShouldBeFalse();
|
|
IsSelected(FindDayButton(calendar, new DateTime(2026, 6, 12))).ShouldBeTrue();
|
|
});
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void RangeCalendar_SecondarySelectedDate_Change_Refreshes_Range_Highlight()
|
|
{
|
|
RunOnUIThread(() =>
|
|
{
|
|
var calendar = new RangeCalendar
|
|
{
|
|
DisplayDate = new DateTime(2026, 6, 1),
|
|
SelectedDate = new DateTime(2026, 6, 10),
|
|
SecondarySelectedDate = new DateTime(2026, 6, 15)
|
|
};
|
|
|
|
ShowInWindow(calendar, () =>
|
|
{
|
|
IsRangeEnd(FindDayButton(calendar, new DateTime(2026, 6, 15))).ShouldBeTrue();
|
|
|
|
calendar.SecondarySelectedDate = new DateTime(2026, 6, 18);
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
IsSelected(FindDayButton(calendar, new DateTime(2026, 6, 15))).ShouldBeFalse();
|
|
IsRangeEnd(FindDayButton(calendar, new DateTime(2026, 6, 15))).ShouldBeFalse();
|
|
IsRangeMiddle(FindDayButton(calendar, new DateTime(2026, 6, 15))).ShouldBeTrue();
|
|
IsSelected(FindDayButton(calendar, new DateTime(2026, 6, 18))).ShouldBeTrue();
|
|
IsRangeEnd(FindDayButton(calendar, new DateTime(2026, 6, 18))).ShouldBeTrue();
|
|
});
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void RangeCalendar_HoverDate_Does_Not_Apply_Committed_Range_End_To_Hover_Button()
|
|
{
|
|
RunOnUIThread(() =>
|
|
{
|
|
var calendar = new RangeCalendar
|
|
{
|
|
DisplayDate = new DateTime(2026, 7, 1),
|
|
SelectedDate = new DateTime(2026, 7, 15),
|
|
HoverDateTime = new DateTime(2026, 7, 16),
|
|
IsSelectRangeStart = false
|
|
};
|
|
|
|
ShowInWindow(calendar, () =>
|
|
{
|
|
var hoverButton = FindDayButton(calendar, new DateTime(2026, 7, 16));
|
|
|
|
IsSelected(hoverButton).ShouldBeFalse();
|
|
IsRangeEnd(hoverButton).ShouldBeFalse();
|
|
IsRangePreviewEnd(hoverButton).ShouldBeTrue();
|
|
});
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void RangeCalendar_Active_Start_HoverDate_Does_Not_Apply_Committed_Range_Start_To_Hover_Button()
|
|
{
|
|
RunOnUIThread(() =>
|
|
{
|
|
var calendar = new RangeCalendar
|
|
{
|
|
DisplayDate = new DateTime(2026, 7, 1),
|
|
SecondarySelectedDate = new DateTime(2026, 7, 16),
|
|
HoverDateTime = new DateTime(2026, 7, 8),
|
|
IsSelectRangeStart = true
|
|
};
|
|
|
|
ShowInWindow(calendar, () =>
|
|
{
|
|
var hoverButton = FindDayButton(calendar, new DateTime(2026, 7, 8));
|
|
var endButton = FindDayButton(calendar, new DateTime(2026, 7, 16));
|
|
|
|
IsSelected(hoverButton).ShouldBeFalse();
|
|
IsRangeStart(hoverButton).ShouldBeFalse();
|
|
IsRangePreviewStart(hoverButton).ShouldBeTrue();
|
|
IsSelected(endButton).ShouldBeFalse();
|
|
IsRangeEnd(endButton).ShouldBeFalse();
|
|
IsRangePreviewEnd(endButton).ShouldBeTrue();
|
|
});
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void Calendar_DisplayDateStart_After_DisplayDateEnd_Clamps_End_To_Start()
|
|
{
|
|
RunOnUIThread(() =>
|
|
{
|
|
var calendar = new PickerCalendar
|
|
{
|
|
DisplayDate = new DateTime(2026, 6, 1),
|
|
DisplayDateEnd = new DateTime(2026, 6, 10),
|
|
DisplayDateStart = new DateTime(2026, 6, 20)
|
|
};
|
|
|
|
calendar.DisplayDateEnd.ShouldBe(new DateTime(2026, 6, 20));
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void Calendar_SelectedDate_Before_DisplayDateStart_Throws_And_Does_Not_Expand_Range()
|
|
{
|
|
RunOnUIThread(() =>
|
|
{
|
|
var calendar = new PickerCalendar
|
|
{
|
|
DisplayDateStart = new DateTime(2026, 6, 10),
|
|
DisplayDateEnd = new DateTime(2026, 6, 20)
|
|
};
|
|
|
|
Should.Throw<ArgumentOutOfRangeException>(() =>
|
|
{
|
|
calendar.SelectedDate = new DateTime(2026, 6, 5);
|
|
});
|
|
|
|
calendar.DisplayDateStart.ShouldBe(new DateTime(2026, 6, 10));
|
|
calendar.DisplayDateEnd.ShouldBe(new DateTime(2026, 6, 20));
|
|
calendar.SelectedDate.ShouldBeNull();
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void Calendar_Type_Initialization_Does_Not_Read_ThemeManager_Static_State()
|
|
{
|
|
var property = PickerCalendar.FirstDayOfWeekProperty;
|
|
|
|
property.Name.ShouldBe(nameof(PickerCalendar.FirstDayOfWeek));
|
|
}
|
|
|
|
[Fact]
|
|
public void CalendarButton_Default_Content_Is_Culture_Free()
|
|
{
|
|
RunOnUIThread(() =>
|
|
{
|
|
var button = new PickerCalendarButton();
|
|
|
|
button.Content.ShouldBe(string.Empty);
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void Calendar_CurrentViewState_Tracks_Normalized_Display_Range()
|
|
{
|
|
RunOnUIThread(() =>
|
|
{
|
|
var calendar = new PickerCalendar
|
|
{
|
|
DisplayDate = new DateTime(2026, 6, 1),
|
|
DisplayDateEnd = new DateTime(2026, 6, 10),
|
|
DisplayDateStart = new DateTime(2026, 6, 20)
|
|
};
|
|
|
|
var state = calendar.SyncAndGetCurrentViewState();
|
|
|
|
state.DisplayDateStart.ShouldBe(new DateTime(2026, 6, 20));
|
|
state.DisplayDateEnd.ShouldBe(new DateTime(2026, 6, 20));
|
|
state.DisplayDate.ShouldBe(new DateTime(2026, 6, 1));
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void Calendar_CurrentViewState_Tracks_Week_Start_And_CrossMonth_SelectedDate()
|
|
{
|
|
RunOnUIThread(() =>
|
|
{
|
|
var calendar = new PickerCalendar
|
|
{
|
|
DisplayDate = new DateTime(2026, 6, 1),
|
|
FirstDayOfWeek = DayOfWeek.Monday
|
|
};
|
|
|
|
calendar.SyncAndGetCurrentViewState().FirstDayOfWeek.ShouldBe(DayOfWeek.Monday);
|
|
|
|
calendar.FirstDayOfWeek = DayOfWeek.Sunday;
|
|
calendar.SelectedDate = new DateTime(2026, 8, 12);
|
|
|
|
var state = calendar.SyncAndGetCurrentViewState();
|
|
|
|
state.FirstDayOfWeek.ShouldBe(DayOfWeek.Sunday);
|
|
state.SelectedDate.ShouldBe(new DateTime(2026, 8, 12));
|
|
state.DisplayDate.ShouldBe(new DateTime(2026, 8, 1));
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void RangeCalendar_CurrentViewState_Tracks_Range_Selection()
|
|
{
|
|
RunOnUIThread(() =>
|
|
{
|
|
var calendar = new RangeCalendar
|
|
{
|
|
DisplayDate = new DateTime(2026, 6, 1),
|
|
SelectedDate = new DateTime(2026, 6, 10),
|
|
SecondarySelectedDate = new DateTime(2026, 6, 15),
|
|
HoverDateTime = new DateTime(2026, 6, 18),
|
|
IsSelectRangeStart = false
|
|
};
|
|
|
|
var state = calendar.SyncAndGetCurrentViewState();
|
|
|
|
state.SelectedDate.ShouldBe(new DateTime(2026, 6, 10));
|
|
state.SecondarySelectedDate.ShouldBe(new DateTime(2026, 6, 15));
|
|
state.RangeSelection.HoverDate.ShouldBe(new DateTime(2026, 6, 18));
|
|
state.RangeSelection.ActivePart.ShouldBe(CalendarRangeActivePart.End);
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void RangeCalendar_End_Selection_Does_Not_Let_Start_Date_Rewind_DisplayDate()
|
|
{
|
|
RunOnUIThread(() =>
|
|
{
|
|
var calendar = new RangeCalendar
|
|
{
|
|
DisplayDate = new DateTime(2026, 8, 1),
|
|
IsSelectRangeStart = false
|
|
};
|
|
|
|
calendar.SelectedDate = new DateTime(2026, 7, 12);
|
|
|
|
calendar.DisplayDate.ShouldBe(new DateTime(2026, 8, 1));
|
|
calendar.SyncAndGetCurrentViewState().DisplayDate.ShouldBe(new DateTime(2026, 8, 1));
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void RangeCalendar_TemplateApply_Does_Not_Let_Start_Date_Rewind_DisplayDate_When_End_Is_Active()
|
|
{
|
|
RunOnUIThread(() =>
|
|
{
|
|
var calendar = new RangeCalendar
|
|
{
|
|
SelectedDate = new DateTime(2026, 7, 12),
|
|
DisplayDate = new DateTime(2026, 8, 1),
|
|
IsSelectRangeStart = false
|
|
};
|
|
|
|
ShowInWindow(calendar, () =>
|
|
{
|
|
calendar.DisplayDate.ShouldBe(new DateTime(2026, 8, 1));
|
|
calendar.SyncAndGetCurrentViewState().DisplayDate.ShouldBe(new DateTime(2026, 8, 1));
|
|
});
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void RangeDatePicker_Open_End_Part_Does_Not_Let_Start_Date_Rewind_Calendar_DisplayDate()
|
|
{
|
|
RunOnUIThread(() =>
|
|
{
|
|
var picker = new TestRangeDatePicker
|
|
{
|
|
RangeStartSelectedDate = new DateTime(2024, 1, 12)
|
|
};
|
|
picker.RangeActivatedPart = RangeActivatedPart.End;
|
|
|
|
var presenter = picker.CreatePickerPresenterForTest();
|
|
picker.NotifyPickerOpenedForTest();
|
|
|
|
ShowInWindow(presenter, () =>
|
|
{
|
|
var calendar = presenter.GetVisualDescendants()
|
|
.OfType<DualMonthRangeCalendar>()
|
|
.Single();
|
|
|
|
calendar.IsSelectRangeStart.ShouldBeFalse();
|
|
DateTimeHelper.CompareYearMonth(calendar.DisplayDate, picker.RangeStartSelectedDate.Value)
|
|
.ShouldNotBe(0);
|
|
});
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void DatePicker_Open_With_PickerDisplayDate_Anchors_Calendar_Without_Selecting_Date()
|
|
{
|
|
RunOnUIThread(() =>
|
|
{
|
|
var displayDate = new DateTime(2026, 10, 20);
|
|
var picker = new TestDatePicker
|
|
{
|
|
PickerDisplayDate = displayDate
|
|
};
|
|
|
|
var presenter = picker.CreatePickerPresenterForTest();
|
|
picker.NotifyPickerOpenedForTest();
|
|
|
|
ShowInWindow(presenter, () =>
|
|
{
|
|
var calendar = presenter.GetVisualDescendants()
|
|
.OfType<PickerCalendar>()
|
|
.Single();
|
|
var expectedSelectedMonth = new DateTime(2026, 10, 1);
|
|
|
|
calendar.DisplayDate.ShouldBe(displayDate);
|
|
calendar.SelectedMonth.ShouldBe(expectedSelectedMonth);
|
|
calendar.SelectedYear.ShouldBe(expectedSelectedMonth);
|
|
calendar.LastSelectedDate.ShouldBe(displayDate);
|
|
calendar.SelectedDate.ShouldBeNull();
|
|
presenter.SelectedDateTime.ShouldBeNull();
|
|
picker.SelectedDateTime.ShouldBeNull();
|
|
});
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void DatePicker_Open_Uses_SelectedDateTime_Before_PickerDisplayDate()
|
|
{
|
|
RunOnUIThread(() =>
|
|
{
|
|
var selectedDate = new DateTime(2026, 2, 11);
|
|
var picker = new TestDatePicker
|
|
{
|
|
PickerDisplayDate = new DateTime(2027, 10, 20),
|
|
SelectedDateTime = selectedDate
|
|
};
|
|
|
|
var presenter = picker.CreatePickerPresenterForTest();
|
|
picker.NotifyPickerOpenedForTest();
|
|
|
|
ShowInWindow(presenter, () =>
|
|
{
|
|
var calendar = presenter.GetVisualDescendants()
|
|
.OfType<PickerCalendar>()
|
|
.Single();
|
|
|
|
calendar.DisplayDate.ShouldBe(selectedDate);
|
|
calendar.SelectedDate.ShouldBe(selectedDate);
|
|
presenter.SelectedDateTime.ShouldBe(selectedDate);
|
|
picker.SelectedDateTime.ShouldBe(selectedDate);
|
|
});
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void DatePicker_Open_Normalizes_PickerDisplayDate_By_PickerMode()
|
|
{
|
|
RunOnUIThread(() =>
|
|
{
|
|
var picker = new TestDatePicker
|
|
{
|
|
PickerMode = DatePickerMode.Quarter,
|
|
PickerDisplayDate = new DateTime(2026, 11, 20, 8, 30, 0)
|
|
};
|
|
|
|
var presenter = picker.CreatePickerPresenterForTest();
|
|
picker.NotifyPickerOpenedForTest();
|
|
|
|
ShowInWindow(presenter, () =>
|
|
{
|
|
var calendar = presenter.GetVisualDescendants()
|
|
.OfType<PickerCalendar>()
|
|
.Single();
|
|
var expectedDisplayDate = new DateTime(2026, 10, 1);
|
|
|
|
calendar.DisplayDate.ShouldBe(expectedDisplayDate);
|
|
calendar.SelectedMonth.ShouldBe(expectedDisplayDate);
|
|
calendar.SelectedYear.ShouldBe(expectedDisplayDate);
|
|
calendar.LastSelectedDate.ShouldBe(expectedDisplayDate);
|
|
calendar.SelectedDate.ShouldBeNull();
|
|
picker.SelectedDateTime.ShouldBeNull();
|
|
});
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void DatePicker_Date_Bounds_Are_Applied_Before_Valid_Calendar_Selection()
|
|
{
|
|
RunOnUIThread(() =>
|
|
{
|
|
var selectedDate = new DateTime(2026, 7, 16, 12, 30, 0);
|
|
var picker = new TestDatePicker
|
|
{
|
|
MinDate = new DateTime(2026, 7, 15, 18, 0, 0),
|
|
MaxDate = new DateTime(2026, 7, 20, 6, 0, 0),
|
|
SelectedDateTime = selectedDate
|
|
};
|
|
|
|
var presenter = picker.CreatePickerPresenterForTest();
|
|
|
|
ShowInWindow(presenter, () =>
|
|
{
|
|
var calendar = presenter.GetVisualDescendants()
|
|
.OfType<PickerCalendar>()
|
|
.Single();
|
|
|
|
presenter.MinDate.ShouldBe(picker.MinDate);
|
|
presenter.MaxDate.ShouldBe(picker.MaxDate);
|
|
calendar.DisplayDateStart.ShouldBe(new DateTime(2026, 7, 15));
|
|
calendar.DisplayDateEnd.ShouldBe(new DateTime(2026, 7, 20));
|
|
calendar.SelectedDate.ShouldBe(selectedDate.Date);
|
|
picker.SelectedDateTime.ShouldBe(selectedDate);
|
|
});
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void DatePicker_Out_Of_Range_Controlled_Value_Remains_External_And_Is_Not_Selected()
|
|
{
|
|
RunOnUIThread(() =>
|
|
{
|
|
var selectedDate = new DateTime(2026, 7, 10);
|
|
var picker = new TestDatePicker
|
|
{
|
|
MinDate = new DateTime(2026, 7, 15),
|
|
MaxDate = new DateTime(2026, 7, 20),
|
|
SelectedDateTime = selectedDate
|
|
};
|
|
|
|
var presenter = picker.CreatePickerPresenterForTest();
|
|
|
|
ShowInWindow(presenter, () =>
|
|
{
|
|
var calendar = presenter.GetVisualDescendants()
|
|
.OfType<PickerCalendar>()
|
|
.Single();
|
|
|
|
calendar.SelectedDate.ShouldBeNull();
|
|
presenter.SelectedDateTime.ShouldBe(selectedDate);
|
|
picker.SelectedDateTime.ShouldBe(selectedDate);
|
|
presenter.GetVisualDescendants()
|
|
.OfType<AtomUI.Desktop.Controls.Button>()
|
|
.Single(button => button.Name == "PART_ConfirmButton")
|
|
.IsEnabled.ShouldBeFalse();
|
|
});
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void DatePicker_Runtime_Boundary_Change_Clears_Only_Calendar_Selection()
|
|
{
|
|
RunOnUIThread(() =>
|
|
{
|
|
var selectedDate = new DateTime(2026, 7, 16);
|
|
var picker = new TestDatePicker
|
|
{
|
|
MinDate = new DateTime(2026, 7, 15),
|
|
SelectedDateTime = selectedDate
|
|
};
|
|
var presenter = picker.CreatePickerPresenterForTest();
|
|
|
|
ShowInWindow(presenter, () =>
|
|
{
|
|
var calendar = presenter.GetVisualDescendants()
|
|
.OfType<PickerCalendar>()
|
|
.Single();
|
|
calendar.SelectedDate.ShouldBe(selectedDate);
|
|
|
|
picker.MinDate = new DateTime(2026, 7, 18);
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
calendar.DisplayDateStart.ShouldBe(new DateTime(2026, 7, 18));
|
|
calendar.SelectedDate.ShouldBeNull();
|
|
presenter.SelectedDateTime.ShouldBe(selectedDate);
|
|
picker.SelectedDateTime.ShouldBe(selectedDate);
|
|
presenter.GetVisualDescendants()
|
|
.OfType<AtomUI.Desktop.Controls.Button>()
|
|
.Single(button => button.Name == "PART_ConfirmButton")
|
|
.IsEnabled.ShouldBeFalse();
|
|
});
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void DatePicker_Open_Clamps_Display_Anchor_To_Effective_Bounds()
|
|
{
|
|
RunOnUIThread(() =>
|
|
{
|
|
var picker = new TestDatePicker
|
|
{
|
|
PickerMode = DatePickerMode.Month,
|
|
PickerDisplayDate = new DateTime(2026, 5, 20),
|
|
MinDate = new DateTime(2026, 7, 15),
|
|
MaxDate = new DateTime(2026, 10, 31)
|
|
};
|
|
var presenter = picker.CreatePickerPresenterForTest();
|
|
picker.NotifyPickerOpenedForTest();
|
|
|
|
ShowInWindow(presenter, () =>
|
|
{
|
|
var calendar = presenter.GetVisualDescendants()
|
|
.OfType<PickerCalendar>()
|
|
.Single();
|
|
|
|
calendar.DisplayDate.ShouldBe(new DateTime(2026, 7, 1));
|
|
calendar.SelectedDate.ShouldBeNull();
|
|
picker.SelectedDateTime.ShouldBeNull();
|
|
});
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void RangeDatePicker_Open_Start_Part_With_PickerDisplayDate_Anchors_Empty_Active_Part()
|
|
{
|
|
RunOnUIThread(() =>
|
|
{
|
|
var displayDate = new DateTime(2026, 9, 23);
|
|
var picker = new TestRangeDatePicker
|
|
{
|
|
PickerDisplayDate = displayDate
|
|
};
|
|
picker.RangeActivatedPart = RangeActivatedPart.Start;
|
|
|
|
var presenter = picker.CreatePickerPresenterForTest();
|
|
picker.NotifyPickerOpenedForTest();
|
|
|
|
ShowInWindow(presenter, () =>
|
|
{
|
|
var calendar = presenter.GetVisualDescendants()
|
|
.OfType<DualMonthRangeCalendar>()
|
|
.Single();
|
|
var expectedSelectedMonth = new DateTime(2026, 9, 1);
|
|
|
|
calendar.DisplayDate.ShouldBe(displayDate);
|
|
calendar.SelectedMonth.ShouldBe(expectedSelectedMonth);
|
|
calendar.SelectedYear.ShouldBe(expectedSelectedMonth);
|
|
calendar.LastSelectedDate.ShouldBe(displayDate);
|
|
calendar.SelectedDate.ShouldBeNull();
|
|
calendar.SecondarySelectedDate.ShouldBeNull();
|
|
picker.RangeStartSelectedDate.ShouldBeNull();
|
|
picker.RangeEndSelectedDate.ShouldBeNull();
|
|
});
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void RangeDatePicker_Projects_Only_Valid_Endpoints_Into_Calendar()
|
|
{
|
|
RunOnUIThread(() =>
|
|
{
|
|
var invalidStart = new DateTime(2026, 7, 10);
|
|
var validEnd = new DateTime(2026, 7, 18);
|
|
var picker = new TestRangeDatePicker
|
|
{
|
|
MinDate = new DateTime(2026, 7, 15),
|
|
MaxDate = new DateTime(2026, 7, 20),
|
|
RangeStartSelectedDate = invalidStart,
|
|
RangeEndSelectedDate = validEnd
|
|
};
|
|
picker.RangeActivatedPart = RangeActivatedPart.End;
|
|
|
|
var presenter = picker.CreatePickerPresenterForTest();
|
|
picker.NotifyPickerOpenedForTest();
|
|
|
|
ShowInWindow(presenter, () =>
|
|
{
|
|
var calendar = presenter.GetVisualDescendants()
|
|
.OfType<DualMonthRangeCalendar>()
|
|
.Single();
|
|
|
|
calendar.DisplayDateStart.ShouldBe(new DateTime(2026, 7, 15));
|
|
calendar.DisplayDateEnd.ShouldBe(new DateTime(2026, 7, 20));
|
|
calendar.SelectedDate.ShouldBeNull();
|
|
calendar.SecondarySelectedDate.ShouldBe(validEnd);
|
|
picker.RangeStartSelectedDate.ShouldBe(invalidStart);
|
|
picker.RangeEndSelectedDate.ShouldBe(validEnd);
|
|
});
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void DatePicker_Today_And_Now_Do_Not_Select_Out_Of_Range_Values()
|
|
{
|
|
RunOnUIThread(() =>
|
|
{
|
|
var presenter = new TestDatePickerPresenter
|
|
{
|
|
MinDate = DateTime.Today.AddDays(1),
|
|
MaxDate = DateTime.Today.AddDays(10)
|
|
};
|
|
|
|
presenter.NotifyTodayButtonClickedForTest();
|
|
presenter.SelectedDateTime.ShouldBeNull();
|
|
|
|
presenter.NotifyNowButtonClickedForTest();
|
|
presenter.SelectedDateTime.ShouldBeNull();
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void DatePicker_Today_And_Now_Buttons_Are_Disabled_Out_Of_Range()
|
|
{
|
|
RunOnUIThread(() =>
|
|
{
|
|
var picker = new TestDatePicker
|
|
{
|
|
MinDate = DateTime.Today.AddDays(1),
|
|
MaxDate = DateTime.Today.AddDays(10)
|
|
};
|
|
var presenter = picker.CreatePickerPresenterForTest();
|
|
|
|
ShowInWindow(presenter, () =>
|
|
{
|
|
presenter.GetVisualDescendants()
|
|
.OfType<AtomUI.Desktop.Controls.Button>()
|
|
.Single(button => button.Name == "PART_TodayButton")
|
|
.IsEnabled.ShouldBeFalse();
|
|
|
|
picker.IsShowTime = true;
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
presenter.GetVisualDescendants()
|
|
.OfType<AtomUI.Desktop.Controls.Button>()
|
|
.Single(button => button.Name == "PART_NowButton")
|
|
.IsEnabled.ShouldBeFalse();
|
|
});
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void Calendar_Navigation_Cannot_Move_Past_Date_Boundaries()
|
|
{
|
|
RunOnUIThread(() =>
|
|
{
|
|
var calendar = new PickerCalendar
|
|
{
|
|
DisplayDate = new DateTime(2026, 7, 1),
|
|
DisplayDateStart = new DateTime(2026, 7, 15),
|
|
DisplayDateEnd = new DateTime(2026, 7, 20)
|
|
};
|
|
|
|
ShowInWindow(calendar, () =>
|
|
{
|
|
calendar.CalendarItem.ShouldNotBeNull().PreviousMonthButton.ShouldNotBeNull().IsEnabled.ShouldBeFalse();
|
|
calendar.CalendarItem.NextMonthButton.ShouldNotBeNull().IsEnabled.ShouldBeFalse();
|
|
var displayDate = calendar.DisplayDate;
|
|
|
|
calendar.OnPreviousMonthClick();
|
|
calendar.OnNextMonthClick();
|
|
calendar.ProcessPageUpKey(false);
|
|
calendar.ProcessPageDownKey(false);
|
|
|
|
calendar.DisplayDate.ShouldBe(displayDate);
|
|
});
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void Calendar_Year_And_Decade_Navigation_Cannot_Move_Past_Date_Boundaries()
|
|
{
|
|
RunOnUIThread(() =>
|
|
{
|
|
var calendar = new PickerCalendar
|
|
{
|
|
DisplayDate = new DateTime(2026, 7, 1),
|
|
DisplayDateStart = new DateTime(2026, 7, 15),
|
|
DisplayDateEnd = new DateTime(2026, 7, 20)
|
|
};
|
|
|
|
ShowInWindow(calendar, () =>
|
|
{
|
|
calendar.SetCurrentValue(PickerCalendar.DisplayModeProperty, PickerCalendarMode.Year);
|
|
calendar.SelectedMonth.ShouldBe(new DateTime(2026, 7, 1));
|
|
calendar.CalendarItem.ShouldNotBeNull().PreviousButton.ShouldNotBeNull().IsEnabled.ShouldBeFalse();
|
|
calendar.CalendarItem.NextButton.ShouldNotBeNull().IsEnabled.ShouldBeFalse();
|
|
var selectedMonth = calendar.SelectedMonth;
|
|
|
|
calendar.OnPreviousClick();
|
|
calendar.OnNextClick();
|
|
calendar.ProcessPageUpKey(true);
|
|
calendar.ProcessPageDownKey(true);
|
|
|
|
calendar.SelectedMonth.ShouldBe(selectedMonth);
|
|
|
|
calendar.SetCurrentValue(PickerCalendar.DisplayModeProperty, PickerCalendarMode.Decade);
|
|
var calendarItem = calendar.CalendarItem.ShouldNotBeNull();
|
|
calendarItem.PreviousButton.ShouldNotBeNull().IsEnabled.ShouldBeFalse();
|
|
calendarItem.NextButton.ShouldNotBeNull().IsEnabled.ShouldBeFalse();
|
|
var selectedYear = calendar.SelectedYear;
|
|
|
|
calendar.OnPreviousClick();
|
|
calendar.OnNextClick();
|
|
calendar.ProcessPageUpKey(true);
|
|
calendar.ProcessPageDownKey(true);
|
|
|
|
calendar.SelectedYear.ShouldBe(selectedYear);
|
|
});
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void RangeDatePicker_Open_End_Part_Uses_Selected_End_Before_PickerDisplayDate()
|
|
{
|
|
RunOnUIThread(() =>
|
|
{
|
|
var selectedEnd = new DateTime(2026, 2, 11);
|
|
var picker = new TestRangeDatePicker
|
|
{
|
|
PickerDisplayDate = new DateTime(2027, 9, 23),
|
|
RangeStartSelectedDate = new DateTime(2026, 1, 12),
|
|
RangeEndSelectedDate = selectedEnd
|
|
};
|
|
picker.RangeActivatedPart = RangeActivatedPart.End;
|
|
|
|
var presenter = picker.CreatePickerPresenterForTest();
|
|
picker.NotifyPickerOpenedForTest();
|
|
|
|
ShowInWindow(presenter, () =>
|
|
{
|
|
var calendar = presenter.GetVisualDescendants()
|
|
.OfType<DualMonthRangeCalendar>()
|
|
.Single();
|
|
|
|
calendar.IsSelectRangeStart.ShouldBeFalse();
|
|
calendar.DisplayDate.ShouldBe(new DateTime(2026, 1, 11));
|
|
calendar.SecondarySelectedDate.ShouldBe(selectedEnd);
|
|
picker.RangeEndSelectedDate.ShouldBe(selectedEnd);
|
|
});
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void RangeDatePicker_Month_End_Hover_Renders_Preview_Range_From_Selected_Start()
|
|
{
|
|
RunOnUIThread(() =>
|
|
{
|
|
var picker = new TestRangeDatePicker
|
|
{
|
|
PickerMode = DatePickerMode.Month,
|
|
RangeStartSelectedDate = new DateTime(2026, 5, 1),
|
|
RangeEndSelectedDate = null
|
|
};
|
|
picker.RangeActivatedPart = RangeActivatedPart.End;
|
|
|
|
var presenter = picker.CreatePickerPresenterForTest();
|
|
picker.NotifyPickerOpenedForTest();
|
|
|
|
ShowInWindow(presenter, () =>
|
|
{
|
|
var calendar = presenter.GetVisualDescendants()
|
|
.OfType<DualMonthRangeCalendar>()
|
|
.Single();
|
|
calendar.SetCurrentValue(PickerCalendar.DisplayDateProperty, new DateTime(2026, 1, 1));
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
var item = calendar.CalendarItem.ShouldBeOfType<PickerDualMonthCalendarItem>();
|
|
var secondaryYearView = FindNamedGrid(item, "PART_SecondaryYearView");
|
|
var hoverButton = FindCalendarButton(secondaryYearView, new DateTime(2027, 8, 1));
|
|
|
|
item.HandleMonthMouseEntered(hoverButton, null!);
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
var state = calendar.SyncAndGetCurrentViewState();
|
|
state.RangeSelection.TryGetPreviewRange(out var previewStart, out var previewEnd).ShouldBeTrue();
|
|
previewStart.ShouldBe(new DateTime(2026, 5, 1));
|
|
previewEnd.ShouldBe(new DateTime(2027, 8, 1));
|
|
|
|
IsRangePreviewStart(FindCalendarButton(item.YearView.ShouldNotBeNull(), new DateTime(2026, 5, 1))).ShouldBeTrue();
|
|
var primaryMiddle = FindCalendarButton(item.YearView, new DateTime(2026, 6, 1));
|
|
var secondaryMiddle = FindCalendarButton(secondaryYearView, new DateTime(2027, 7, 1));
|
|
IsRangePreviewMiddle(primaryMiddle).ShouldBeTrue();
|
|
IsRangePreviewMiddle(secondaryMiddle).ShouldBeTrue();
|
|
FindTemplateBorder(primaryMiddle, "RangeIndicator").IsVisible.ShouldBeTrue();
|
|
FindTemplateBorder(secondaryMiddle, "RangeIndicator").IsVisible.ShouldBeTrue();
|
|
IsRangePreviewEnd(hoverButton).ShouldBeTrue();
|
|
});
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void RangeDatePicker_Month_Reopen_Complete_Range_Anchors_Dual_Panel_To_Active_End()
|
|
{
|
|
RunOnUIThread(() =>
|
|
{
|
|
var startDate = new DateTime(2026, 5, 1);
|
|
var endDate = new DateTime(2028, 9, 1);
|
|
var picker = new TestRangeDatePicker
|
|
{
|
|
PickerMode = DatePickerMode.Month,
|
|
RangeStartSelectedDate = startDate,
|
|
RangeEndSelectedDate = endDate
|
|
};
|
|
picker.RangeActivatedPart = RangeActivatedPart.End;
|
|
|
|
var presenter = picker.CreatePickerPresenterForTest();
|
|
|
|
ShowInWindow(presenter, () =>
|
|
{
|
|
var calendar = presenter.GetVisualDescendants()
|
|
.OfType<DualMonthRangeCalendar>()
|
|
.Single();
|
|
|
|
calendar.SetCurrentValue(PickerCalendar.DisplayDateProperty, endDate);
|
|
calendar.SelectedMonth = endDate;
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
picker.NotifyPickerOpenedForTest();
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
calendar.DisplayDate.Year.ShouldBe(2027);
|
|
calendar.SelectedMonth.Year.ShouldBe(2027);
|
|
|
|
var item = calendar.CalendarItem.ShouldBeOfType<PickerDualMonthCalendarItem>();
|
|
var secondaryYearView = FindNamedGrid(item, "PART_SecondaryYearView");
|
|
|
|
FindCalendarButton(item.YearView.ShouldNotBeNull(), new DateTime(2027, 1, 1));
|
|
FindCalendarButton(secondaryYearView, new DateTime(2028, 1, 1));
|
|
|
|
var endButton = FindCalendarButton(secondaryYearView, endDate);
|
|
|
|
IsRangeEnd(endButton).ShouldBeTrue();
|
|
IsRangePreviewEnd(endButton).ShouldBeFalse();
|
|
});
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void RangeDatePicker_Month_Reopen_Complete_Range_Anchors_Dual_Panel_To_Active_Start()
|
|
{
|
|
RunOnUIThread(() =>
|
|
{
|
|
var startDate = new DateTime(2026, 5, 1);
|
|
var endDate = new DateTime(2028, 9, 1);
|
|
var picker = new TestRangeDatePicker
|
|
{
|
|
PickerMode = DatePickerMode.Month,
|
|
RangeStartSelectedDate = startDate,
|
|
RangeEndSelectedDate = endDate
|
|
};
|
|
picker.RangeActivatedPart = RangeActivatedPart.Start;
|
|
|
|
var presenter = picker.CreatePickerPresenterForTest();
|
|
|
|
ShowInWindow(presenter, () =>
|
|
{
|
|
var calendar = presenter.GetVisualDescendants()
|
|
.OfType<DualMonthRangeCalendar>()
|
|
.Single();
|
|
|
|
calendar.SetCurrentValue(PickerCalendar.DisplayDateProperty, endDate);
|
|
calendar.SelectedMonth = endDate;
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
picker.NotifyPickerOpenedForTest();
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
calendar.DisplayDate.Year.ShouldBe(2026);
|
|
calendar.SelectedMonth.Year.ShouldBe(2026);
|
|
|
|
var item = calendar.CalendarItem.ShouldBeOfType<PickerDualMonthCalendarItem>();
|
|
var startButton = FindCalendarButton(item.YearView.ShouldNotBeNull(), startDate);
|
|
var secondaryYearView = FindNamedGrid(item, "PART_SecondaryYearView");
|
|
|
|
FindCalendarButton(secondaryYearView, new DateTime(2027, 1, 1));
|
|
IsRangeStart(startButton).ShouldBeTrue();
|
|
IsRangePreviewStart(startButton).ShouldBeFalse();
|
|
});
|
|
});
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(DatePickerMode.Month, 2027, 6, 1)]
|
|
[InlineData(DatePickerMode.Quarter, 2027, 4, 1)]
|
|
[InlineData(DatePickerMode.Year, 2027, 1, 1)]
|
|
public void DualMonthRangeCalendar_PickerUnit_Hover_Selected_Endpoint_Does_Not_Render_Degenerate_Preview(
|
|
DatePickerMode pickerMode,
|
|
int selectedYear,
|
|
int selectedMonth,
|
|
int selectedDay)
|
|
{
|
|
RunOnUIThread(() =>
|
|
{
|
|
var selectedDate = new DateTime(selectedYear, selectedMonth, selectedDay);
|
|
var calendar = new DualMonthRangeCalendar
|
|
{
|
|
PickerMode = pickerMode,
|
|
DisplayDate = selectedDate,
|
|
SelectedDate = selectedDate,
|
|
HoverDateTime = selectedDate,
|
|
IsSelectRangeStart = false
|
|
};
|
|
|
|
ShowInWindow(calendar, () =>
|
|
{
|
|
var state = calendar.SyncAndGetCurrentViewState();
|
|
state.RangeSelection.TryGetPreviewRange(out _, out _).ShouldBeFalse();
|
|
|
|
var item = calendar.CalendarItem.ShouldBeOfType<PickerDualMonthCalendarItem>();
|
|
var button = FindCalendarButton(item.YearView.ShouldNotBeNull(), selectedDate);
|
|
|
|
IsSelected(button).ShouldBeTrue();
|
|
IsRangePreviewStart(button).ShouldBeFalse();
|
|
IsRangePreviewEnd(button).ShouldBeFalse();
|
|
IsRangePreviewMiddle(button).ShouldBeFalse();
|
|
FindTemplateBorder(button, "RangeIndicator").IsVisible.ShouldBeFalse();
|
|
FindTemplateBorder(button, "RangeStartIndicator").IsVisible.ShouldBeFalse();
|
|
FindTemplateBorder(button, "RangeEndIndicator").IsVisible.ShouldBeFalse();
|
|
});
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void TimedRangeDatePicker_Open_End_Part_Does_Not_Let_Start_Date_Rewind_Calendar_DisplayDate()
|
|
{
|
|
RunOnUIThread(() =>
|
|
{
|
|
var picker = new TestRangeDatePicker
|
|
{
|
|
IsShowTime = true,
|
|
RangeStartSelectedDate = new DateTime(2024, 1, 12, 10, 30, 0)
|
|
};
|
|
picker.RangeActivatedPart = RangeActivatedPart.End;
|
|
|
|
var presenter = picker.CreatePickerPresenterForTest();
|
|
picker.NotifyPickerOpenedForTest();
|
|
|
|
ShowInWindow(presenter, () =>
|
|
{
|
|
var calendar = presenter.GetVisualDescendants()
|
|
.OfType<RangeCalendar>()
|
|
.Single();
|
|
|
|
calendar.IsSelectRangeStart.ShouldBeFalse();
|
|
DateTimeHelper.CompareYearMonth(calendar.DisplayDate, picker.RangeStartSelectedDate.Value)
|
|
.ShouldNotBe(0);
|
|
});
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void Calendar_FirstDayOfWeek_Change_Rebuilds_Week_Title_Order()
|
|
{
|
|
RunOnUIThread(() =>
|
|
{
|
|
var calendar = new PickerCalendar
|
|
{
|
|
DisplayDate = new DateTime(2026, 6, 1),
|
|
FirstDayOfWeek = DayOfWeek.Monday
|
|
};
|
|
|
|
ShowInWindow(calendar, () =>
|
|
{
|
|
var titles = calendar.CalendarItem.ShouldNotBeNull()
|
|
.MonthView.ShouldNotBeNull()
|
|
.Children
|
|
.Take(7)
|
|
.Select(child => child.DataContext?.ToString())
|
|
.ToArray();
|
|
|
|
titles[0].ShouldNotBeNullOrWhiteSpace();
|
|
calendar.FirstDayOfWeek = DayOfWeek.Sunday;
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
var updatedTitles = calendar.CalendarItem.ShouldNotBeNull()
|
|
.MonthView.ShouldNotBeNull()
|
|
.Children
|
|
.Take(7)
|
|
.Select(child => child.DataContext?.ToString())
|
|
.ToArray();
|
|
|
|
updatedTitles.ShouldNotBe(titles);
|
|
});
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void CalendarItem_LanguageManager_Subscription_Follows_VisualTree_Lifecycle()
|
|
{
|
|
RunOnUIThread(() =>
|
|
{
|
|
var languageManager = Application.Current!.GetLanguageManager().ShouldNotBeNull();
|
|
var originalLanguage = languageManager.Current.CurrentLanguage;
|
|
languageManager.ChangeLanguage(LanguageTags.EnUS);
|
|
var calendar = new PickerCalendar
|
|
{
|
|
DisplayDate = new DateTime(2026, 6, 1)
|
|
};
|
|
var window = new AvaloniaWindow
|
|
{
|
|
Width = 420,
|
|
Height = 360,
|
|
Content = calendar
|
|
};
|
|
|
|
try
|
|
{
|
|
window.Show();
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
languageManager.ChangeLanguage(LanguageTags.ZhCN);
|
|
Dispatcher.UIThread.RunJobs();
|
|
var activeCulture = calendar.SyncAndGetCurrentViewState().Culture;
|
|
activeCulture.ShortDatePattern.ShouldBe(
|
|
languageManager.Current.FormattingCulture.DateTimeFormat.ShortDatePattern);
|
|
|
|
window.Content = null;
|
|
Dispatcher.UIThread.RunJobs();
|
|
languageManager.ChangeLanguage(LanguageTags.EnUS);
|
|
Dispatcher.UIThread.RunJobs();
|
|
calendar.SyncAndGetCurrentViewState().Culture.ShouldBeSameAs(activeCulture);
|
|
|
|
window.Content = calendar;
|
|
Dispatcher.UIThread.RunJobs();
|
|
calendar.SyncAndGetCurrentViewState().Culture.ShortDatePattern.ShouldBe(
|
|
languageManager.Current.FormattingCulture.DateTimeFormat.ShortDatePattern);
|
|
}
|
|
finally
|
|
{
|
|
window.Close();
|
|
languageManager.ChangeLanguage(originalLanguage);
|
|
Dispatcher.UIThread.RunJobs();
|
|
}
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void DualMonthRangeCalendar_Secondary_Panel_Uses_Range_Highlight()
|
|
{
|
|
RunOnUIThread(() =>
|
|
{
|
|
var calendar = new DualMonthRangeCalendar
|
|
{
|
|
DisplayDate = new DateTime(2026, 6, 1),
|
|
SelectedDate = new DateTime(2026, 6, 28),
|
|
SecondarySelectedDate = new DateTime(2026, 7, 3)
|
|
};
|
|
|
|
ShowInWindow(calendar, () =>
|
|
{
|
|
var item = calendar.CalendarItem.ShouldBeOfType<PickerDualMonthCalendarItem>();
|
|
var secondaryButton = item.SecondaryMonthView.ShouldNotBeNull()
|
|
.Children
|
|
.OfType<PickerCalendarDayButton>()
|
|
.Single(button => button.DataContext is DateTime day &&
|
|
DateTimeHelper.CompareDays(day, new DateTime(2026, 7, 3)) == 0);
|
|
|
|
IsRangeEnd(secondaryButton).ShouldBeTrue();
|
|
});
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void DualMonthRangeCalendar_Month_Picker_Uses_Two_Year_Panels()
|
|
{
|
|
RunOnUIThread(() =>
|
|
{
|
|
var calendar = new DualMonthRangeCalendar
|
|
{
|
|
PickerMode = DatePickerMode.Month,
|
|
DisplayDate = new DateTime(2026, 4, 1)
|
|
};
|
|
|
|
ShowInWindow(calendar, () =>
|
|
{
|
|
var item = calendar.CalendarItem.ShouldBeOfType<PickerDualMonthCalendarItem>();
|
|
var secondaryYearView = FindNamedGrid(item, "PART_SecondaryYearView");
|
|
|
|
calendar.DisplayMode.ShouldBe(PickerCalendarMode.Year);
|
|
item.MonthViewLayout.ShouldNotBeNull().IsVisible.ShouldBeFalse();
|
|
item.YearViewLayout.ShouldNotBeNull().IsVisible.ShouldBeTrue();
|
|
item.YearView.ShouldNotBeNull().IsVisible.ShouldBeTrue();
|
|
secondaryYearView.IsVisible.ShouldBeTrue();
|
|
|
|
GetCalendarButtonDates(item.YearView).Where(date => date.Year == 2026).Count().ShouldBe(12);
|
|
GetCalendarButtonDates(secondaryYearView).Where(date => date.Year == 2027).Count().ShouldBe(12);
|
|
item.YearView.RowDefinitions.Count.ShouldBe(4);
|
|
item.YearView.ColumnDefinitions.Count.ShouldBe(3);
|
|
secondaryYearView.RowDefinitions.Count.ShouldBe(4);
|
|
secondaryYearView.ColumnDefinitions.Count.ShouldBe(3);
|
|
});
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void DualMonthRangeCalendar_Month_Hover_End_Renders_Range_Preview_In_Year_Panels()
|
|
{
|
|
RunOnUIThread(() =>
|
|
{
|
|
var calendar = new DualMonthRangeCalendar
|
|
{
|
|
PickerMode = DatePickerMode.Month,
|
|
DisplayDate = new DateTime(2026, 4, 1),
|
|
SelectedDate = new DateTime(2026, 7, 1),
|
|
HoverDateTime = new DateTime(2027, 8, 1),
|
|
IsSelectRangeStart = false
|
|
};
|
|
|
|
ShowInWindow(calendar, () =>
|
|
{
|
|
var item = calendar.CalendarItem.ShouldBeOfType<PickerDualMonthCalendarItem>();
|
|
var primaryYearView = item.YearView.ShouldNotBeNull();
|
|
var secondaryYearView = FindNamedGrid(item, "PART_SecondaryYearView");
|
|
|
|
var start = FindCalendarButton(primaryYearView, new DateTime(2026, 7, 1));
|
|
var primaryMiddle = FindCalendarButton(primaryYearView, new DateTime(2026, 8, 1));
|
|
var secondaryMiddle = FindCalendarButton(secondaryYearView, new DateTime(2027, 7, 1));
|
|
var end = FindCalendarButton(secondaryYearView, new DateTime(2027, 8, 1));
|
|
var afterEnd = FindCalendarButton(secondaryYearView, new DateTime(2027, 9, 1));
|
|
|
|
IsSelected(start).ShouldBeFalse();
|
|
IsRangePreviewStart(start).ShouldBeTrue();
|
|
IsRangePreviewMiddle(primaryMiddle).ShouldBeTrue();
|
|
IsRangePreviewMiddle(secondaryMiddle).ShouldBeTrue();
|
|
IsSelected(end).ShouldBeFalse();
|
|
IsRangePreviewEnd(end).ShouldBeTrue();
|
|
IsRangePreviewMiddle(afterEnd).ShouldBeFalse();
|
|
});
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void DualMonthRangeCalendar_Quarter_Picker_Uses_Two_Compact_Quarter_Panels()
|
|
{
|
|
RunOnUIThread(() =>
|
|
{
|
|
var calendar = new DualMonthRangeCalendar
|
|
{
|
|
PickerMode = DatePickerMode.Quarter,
|
|
DisplayDate = new DateTime(2026, 4, 1)
|
|
};
|
|
|
|
ShowInWindow(calendar, () =>
|
|
{
|
|
var item = calendar.CalendarItem.ShouldBeOfType<PickerDualMonthCalendarItem>();
|
|
var secondaryYearView = FindNamedGrid(item, "PART_SecondaryYearView");
|
|
|
|
calendar.DisplayMode.ShouldBe(PickerCalendarMode.Year);
|
|
item.YearView.ShouldNotBeNull().IsVisible.ShouldBeTrue();
|
|
secondaryYearView.IsVisible.ShouldBeTrue();
|
|
item.YearView.RowDefinitions.Count.ShouldBe(1);
|
|
item.YearView.ColumnDefinitions.Count.ShouldBe(4);
|
|
secondaryYearView.RowDefinitions.Count.ShouldBe(1);
|
|
secondaryYearView.ColumnDefinitions.Count.ShouldBe(4);
|
|
GetVisibleCalendarButtonTexts(item.YearView).ShouldBe(new[] { "Q1", "Q2", "Q3", "Q4" });
|
|
GetVisibleCalendarButtonTexts(secondaryYearView).ShouldBe(new[] { "Q1", "Q2", "Q3", "Q4" });
|
|
GetCalendarButtonDates(item.YearView).ShouldAllBe(date => date.Year == 2026);
|
|
GetCalendarButtonDates(secondaryYearView).ShouldAllBe(date => date.Year == 2027);
|
|
});
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void DualMonthRangeCalendar_Quarter_Hover_End_Renders_Range_Preview_In_Quarter_Panels()
|
|
{
|
|
RunOnUIThread(() =>
|
|
{
|
|
var calendar = new DualMonthRangeCalendar
|
|
{
|
|
PickerMode = DatePickerMode.Quarter,
|
|
DisplayDate = new DateTime(2026, 4, 1),
|
|
SelectedDate = new DateTime(2026, 4, 1),
|
|
HoverDateTime = new DateTime(2027, 7, 1),
|
|
IsSelectRangeStart = false
|
|
};
|
|
|
|
ShowInWindow(calendar, () =>
|
|
{
|
|
var item = calendar.CalendarItem.ShouldBeOfType<PickerDualMonthCalendarItem>();
|
|
var primaryYearView = item.YearView.ShouldNotBeNull();
|
|
var secondaryYearView = FindNamedGrid(item, "PART_SecondaryYearView");
|
|
|
|
var start = FindCalendarButton(primaryYearView, new DateTime(2026, 4, 1));
|
|
var primaryMiddle = FindCalendarButton(primaryYearView, new DateTime(2026, 7, 1));
|
|
var secondaryMiddle = FindCalendarButton(secondaryYearView, new DateTime(2027, 4, 1));
|
|
var end = FindCalendarButton(secondaryYearView, new DateTime(2027, 7, 1));
|
|
var afterEnd = FindCalendarButton(secondaryYearView, new DateTime(2027, 10, 1));
|
|
|
|
IsSelected(start).ShouldBeFalse();
|
|
IsRangePreviewStart(start).ShouldBeTrue();
|
|
IsRangePreviewMiddle(primaryMiddle).ShouldBeTrue();
|
|
IsRangePreviewMiddle(secondaryMiddle).ShouldBeTrue();
|
|
IsSelected(end).ShouldBeFalse();
|
|
IsRangePreviewEnd(end).ShouldBeTrue();
|
|
IsRangePreviewMiddle(afterEnd).ShouldBeFalse();
|
|
});
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void DualMonthRangeCalendar_Year_Picker_Uses_Two_Decade_Panels()
|
|
{
|
|
RunOnUIThread(() =>
|
|
{
|
|
var calendar = new DualMonthRangeCalendar
|
|
{
|
|
PickerMode = DatePickerMode.Year,
|
|
DisplayDate = new DateTime(2026, 4, 1)
|
|
};
|
|
|
|
ShowInWindow(calendar, () =>
|
|
{
|
|
var item = calendar.CalendarItem.ShouldBeOfType<PickerDualMonthCalendarItem>();
|
|
var secondaryYearView = FindNamedGrid(item, "PART_SecondaryYearView");
|
|
|
|
calendar.DisplayMode.ShouldBe(PickerCalendarMode.Decade);
|
|
item.YearView.ShouldNotBeNull().IsVisible.ShouldBeTrue();
|
|
secondaryYearView.IsVisible.ShouldBeTrue();
|
|
GetCalendarButtonDates(item.YearView).Any(date => date.Year == 2020).ShouldBeTrue();
|
|
GetCalendarButtonDates(item.YearView).Any(date => date.Year == 2029).ShouldBeTrue();
|
|
GetCalendarButtonDates(secondaryYearView).Any(date => date.Year == 2030).ShouldBeTrue();
|
|
GetCalendarButtonDates(secondaryYearView).Any(date => date.Year == 2039).ShouldBeTrue();
|
|
});
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void DualMonthRangeCalendar_Year_Hover_End_Renders_Range_Preview_In_Decade_Panels()
|
|
{
|
|
RunOnUIThread(() =>
|
|
{
|
|
var calendar = new DualMonthRangeCalendar
|
|
{
|
|
PickerMode = DatePickerMode.Year,
|
|
DisplayDate = new DateTime(2026, 1, 1),
|
|
SelectedDate = new DateTime(2026, 1, 1),
|
|
HoverDateTime = new DateTime(2032, 1, 1),
|
|
IsSelectRangeStart = false
|
|
};
|
|
|
|
ShowInWindow(calendar, () =>
|
|
{
|
|
var item = calendar.CalendarItem.ShouldBeOfType<PickerDualMonthCalendarItem>();
|
|
var primaryYearView = item.YearView.ShouldNotBeNull();
|
|
var secondaryYearView = FindNamedGrid(item, "PART_SecondaryYearView");
|
|
|
|
var start = FindCalendarButton(primaryYearView, new DateTime(2026, 1, 1));
|
|
var primaryMiddle = FindCalendarButton(primaryYearView, new DateTime(2027, 1, 1));
|
|
var secondaryMiddle = FindCalendarButton(secondaryYearView, new DateTime(2031, 1, 1));
|
|
var end = FindCalendarButton(secondaryYearView, new DateTime(2032, 1, 1));
|
|
var afterEnd = FindCalendarButton(secondaryYearView, new DateTime(2033, 1, 1));
|
|
|
|
IsSelected(start).ShouldBeFalse();
|
|
IsRangePreviewStart(start).ShouldBeTrue();
|
|
IsRangePreviewMiddle(primaryMiddle).ShouldBeTrue();
|
|
IsRangePreviewMiddle(secondaryMiddle).ShouldBeTrue();
|
|
IsSelected(end).ShouldBeFalse();
|
|
IsRangePreviewEnd(end).ShouldBeTrue();
|
|
IsRangePreviewMiddle(afterEnd).ShouldBeFalse();
|
|
});
|
|
});
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(DatePickerMode.Month, PickerCalendarMode.Year)]
|
|
[InlineData(DatePickerMode.Quarter, PickerCalendarMode.Year)]
|
|
[InlineData(DatePickerMode.Year, PickerCalendarMode.Decade)]
|
|
public void DualMonthRangeCalendar_PickerMode_Runtime_Change_Uses_Only_Target_Dual_Panel(
|
|
DatePickerMode pickerMode,
|
|
PickerCalendarMode expectedDisplayMode)
|
|
{
|
|
RunOnUIThread(() =>
|
|
{
|
|
var calendar = new DualMonthRangeCalendar
|
|
{
|
|
DisplayDate = new DateTime(2026, 7, 1)
|
|
};
|
|
|
|
ShowInWindow(calendar, () =>
|
|
{
|
|
var item = calendar.CalendarItem.ShouldBeOfType<PickerDualMonthCalendarItem>();
|
|
AssertDualMonthPanelVisibility(item, monthVisible: true);
|
|
|
|
calendar.PickerMode = pickerMode;
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
calendar.DisplayMode.ShouldBe(expectedDisplayMode);
|
|
AssertDualMonthPanelVisibility(item, monthVisible: false);
|
|
|
|
calendar.PickerMode = DatePickerMode.Week;
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
calendar.DisplayMode.ShouldBe(PickerCalendarMode.Month);
|
|
AssertDualMonthPanelVisibility(item, monthVisible: true);
|
|
});
|
|
});
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(DatePickerMode.Month, PickerCalendarMode.Year)]
|
|
[InlineData(DatePickerMode.Quarter, PickerCalendarMode.Year)]
|
|
[InlineData(DatePickerMode.Year, PickerCalendarMode.Decade)]
|
|
public void Calendar_PickerMode_Runtime_Change_Uses_Only_Target_Panel(
|
|
DatePickerMode pickerMode,
|
|
PickerCalendarMode expectedDisplayMode)
|
|
{
|
|
RunOnUIThread(() =>
|
|
{
|
|
var calendar = new PickerCalendar
|
|
{
|
|
DisplayDate = new DateTime(2026, 7, 1)
|
|
};
|
|
|
|
ShowInWindow(calendar, () =>
|
|
{
|
|
var item = calendar.CalendarItem.ShouldBeOfType<PickerCalendarItem>();
|
|
AssertCalendarPanelVisibility(item, monthVisible: true);
|
|
|
|
calendar.PickerMode = pickerMode;
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
calendar.DisplayMode.ShouldBe(expectedDisplayMode);
|
|
AssertCalendarPanelVisibility(item, monthVisible: false);
|
|
|
|
calendar.PickerMode = DatePickerMode.Week;
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
calendar.DisplayMode.ShouldBe(PickerCalendarMode.Month);
|
|
AssertCalendarPanelVisibility(item, monthVisible: true);
|
|
});
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void DualMonthRangeCalendar_Hover_End_Renders_Continuous_Preview_Range_In_Secondary_Panel()
|
|
{
|
|
RunOnUIThread(() =>
|
|
{
|
|
var calendar = new DualMonthRangeCalendar
|
|
{
|
|
DisplayDate = new DateTime(2026, 7, 1),
|
|
SelectedDate = new DateTime(2026, 7, 17),
|
|
HoverDateTime = new DateTime(2026, 8, 19),
|
|
IsSelectRangeStart = false
|
|
};
|
|
|
|
ShowInWindow(calendar, () =>
|
|
{
|
|
var middleButton = FindSecondaryDayButton(calendar, new DateTime(2026, 8, 18));
|
|
var endButton = FindSecondaryDayButton(calendar, new DateTime(2026, 8, 19));
|
|
|
|
IsRangePreviewMiddle(middleButton).ShouldBeTrue();
|
|
IsSelected(endButton).ShouldBeFalse();
|
|
IsRangeEnd(endButton).ShouldBeFalse();
|
|
IsRangePreviewEnd(endButton).ShouldBeTrue();
|
|
|
|
var endIndicator = FindTemplateBorder(endButton, "RangeEndIndicator");
|
|
endIndicator.Bounds.Width.ShouldBe(endButton.Bounds.Width / 2, 0.5);
|
|
});
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void DualMonthRangeCalendar_Week_Hover_End_Renders_Row_Preview_In_Secondary_Panel()
|
|
{
|
|
RunOnUIThread(() =>
|
|
{
|
|
var calendar = new DualMonthRangeCalendar
|
|
{
|
|
PickerMode = DatePickerMode.Week,
|
|
DisplayDate = new DateTime(2026, 7, 1),
|
|
SelectedDate = new DateTime(2026, 7, 13),
|
|
HoverDateTime = new DateTime(2026, 8, 10),
|
|
IsSelectRangeStart = false
|
|
};
|
|
|
|
ShowInWindow(calendar, () =>
|
|
{
|
|
var rangeMiddleStart = FindSecondaryDayButton(calendar, new DateTime(2026, 8, 3));
|
|
var rangeMiddleEnd = FindSecondaryDayButton(calendar, new DateTime(2026, 8, 9));
|
|
var previewStart = FindSecondaryDayButton(calendar, new DateTime(2026, 8, 10));
|
|
var previewMiddle = FindSecondaryDayButton(calendar, new DateTime(2026, 8, 13));
|
|
var previewEnd = FindSecondaryDayButton(calendar, new DateTime(2026, 8, 16));
|
|
|
|
IsWeekRangeMiddle(rangeMiddleStart).ShouldBeTrue();
|
|
IsWeekRangeEnd(rangeMiddleEnd).ShouldBeTrue();
|
|
IsSelected(previewStart).ShouldBeFalse();
|
|
IsWeekSelectionMiddle(previewStart).ShouldBeTrue();
|
|
IsWeekSelectionMiddle(previewMiddle).ShouldBeTrue();
|
|
IsWeekSelectionEnd(previewEnd).ShouldBeTrue();
|
|
});
|
|
});
|
|
}
|
|
|
|
private static bool IsSelected(PickerCalendarDayButton button)
|
|
{
|
|
return button.Classes.Contains(":selected");
|
|
}
|
|
|
|
private static bool IsRangeEnd(PickerCalendarDayButton button)
|
|
{
|
|
return button.Classes.Contains(":range-end");
|
|
}
|
|
|
|
private static bool IsRangeStart(PickerCalendarDayButton button)
|
|
{
|
|
return button.Classes.Contains(":range-start");
|
|
}
|
|
|
|
private static bool IsRangeMiddle(PickerCalendarDayButton button)
|
|
{
|
|
return button.Classes.Contains(":range-middle");
|
|
}
|
|
|
|
private static bool IsRangePreviewEnd(PickerCalendarDayButton button)
|
|
{
|
|
return button.Classes.Contains(":range-preview-end");
|
|
}
|
|
|
|
private static bool IsRangePreviewStart(PickerCalendarDayButton button)
|
|
{
|
|
return button.Classes.Contains(":range-preview-start");
|
|
}
|
|
|
|
private static bool IsRangePreviewMiddle(PickerCalendarDayButton button)
|
|
{
|
|
return button.Classes.Contains(":range-preview-middle");
|
|
}
|
|
|
|
private static bool IsWeekSelectionMiddle(PickerCalendarDayButton button)
|
|
{
|
|
return button.Classes.Contains(":week-selection-middle");
|
|
}
|
|
|
|
private static bool IsWeekSelectionEnd(PickerCalendarDayButton button)
|
|
{
|
|
return button.Classes.Contains(":week-selection-end");
|
|
}
|
|
|
|
private static bool IsWeekRangeMiddle(PickerCalendarDayButton button)
|
|
{
|
|
return button.Classes.Contains(":week-range-middle");
|
|
}
|
|
|
|
private static bool IsWeekRangeEnd(PickerCalendarDayButton button)
|
|
{
|
|
return button.Classes.Contains(":week-range-end");
|
|
}
|
|
|
|
private static bool IsSelected(PickerCalendarButton button)
|
|
{
|
|
return button.Classes.Contains(":selected");
|
|
}
|
|
|
|
private static bool IsRangeEnd(PickerCalendarButton button)
|
|
{
|
|
return button.Classes.Contains(":range-end");
|
|
}
|
|
|
|
private static bool IsRangeStart(PickerCalendarButton button)
|
|
{
|
|
return button.Classes.Contains(":range-start");
|
|
}
|
|
|
|
private static bool IsRangePreviewEnd(PickerCalendarButton button)
|
|
{
|
|
return button.Classes.Contains(":range-preview-end");
|
|
}
|
|
|
|
private static bool IsRangePreviewStart(PickerCalendarButton button)
|
|
{
|
|
return button.Classes.Contains(":range-preview-start");
|
|
}
|
|
|
|
private static bool IsRangePreviewMiddle(PickerCalendarButton button)
|
|
{
|
|
return button.Classes.Contains(":range-preview-middle");
|
|
}
|
|
|
|
private static void AssertTemplateParts(Type controlType, IReadOnlyDictionary<string, Type> expectedParts)
|
|
{
|
|
var parts = controlType.GetCustomAttributes(typeof(TemplatePartAttribute), false)
|
|
.Cast<TemplatePartAttribute>()
|
|
.ToDictionary(part => part.Name, part => part.Type);
|
|
|
|
foreach (var (name, expectedType) in expectedParts)
|
|
{
|
|
parts.TryGetValue(name, out var actualType).ShouldBeTrue(name);
|
|
actualType.ShouldBe(expectedType);
|
|
}
|
|
}
|
|
|
|
private static PickerCalendarDayButton FindDayButton(PickerCalendar calendar, DateTime date)
|
|
{
|
|
var buttons = calendar.CalendarItem.ShouldNotBeNull()
|
|
.MonthView.ShouldNotBeNull()
|
|
.Children
|
|
.OfType<PickerCalendarDayButton>()
|
|
.ToArray();
|
|
var matchedButton = buttons.SingleOrDefault(button =>
|
|
button.DataContext is DateTime buttonDate &&
|
|
DateTimeHelper.CompareDays(buttonDate, date) == 0);
|
|
if (matchedButton is not null)
|
|
{
|
|
return matchedButton;
|
|
}
|
|
|
|
var actualDates = string.Join(
|
|
", ",
|
|
buttons.Select(button => button.DataContext)
|
|
.OfType<DateTime>()
|
|
.Select(buttonDate => buttonDate.ToString("yyyy-MM-dd")));
|
|
var monthView = calendar.CalendarItem.ShouldNotBeNull().MonthView.ShouldNotBeNull();
|
|
var childTypes = string.Join(
|
|
", ",
|
|
monthView.Children.Select(child => child.GetType().FullName));
|
|
throw new InvalidOperationException(
|
|
$"Date {date:yyyy-MM-dd} was not generated. ChildCount: {monthView.Children.Count}; ButtonCount: {buttons.Length}; Actual dates: {actualDates}; ChildTypes: {childTypes}");
|
|
}
|
|
|
|
private static PickerCalendarDayButton FindSecondaryDayButton(DualMonthRangeCalendar calendar, DateTime date)
|
|
{
|
|
var item = calendar.CalendarItem.ShouldBeOfType<PickerDualMonthCalendarItem>();
|
|
var buttons = item.SecondaryMonthView.ShouldNotBeNull()
|
|
.Children
|
|
.OfType<PickerCalendarDayButton>()
|
|
.ToArray();
|
|
return buttons.Single(button =>
|
|
!button.IsWeekNumber &&
|
|
button.DataContext is DateTime buttonDate &&
|
|
DateTimeHelper.CompareDays(buttonDate, date) == 0);
|
|
}
|
|
|
|
private static AvaloniaGrid FindNamedGrid(Control control, string name)
|
|
{
|
|
return control.GetVisualDescendants()
|
|
.OfType<AvaloniaGrid>()
|
|
.Single(grid => grid.Name == name);
|
|
}
|
|
|
|
private static DateTime[] GetCalendarButtonDates(AvaloniaGrid grid)
|
|
{
|
|
return grid.Children
|
|
.OfType<PickerCalendarButton>()
|
|
.Where(button => button.IsVisible &&
|
|
button.Opacity > 0 &&
|
|
button.DataContext is DateTime)
|
|
.Select(button => (DateTime)button.DataContext!)
|
|
.ToArray();
|
|
}
|
|
|
|
private static string[] GetVisibleCalendarButtonTexts(AvaloniaGrid grid)
|
|
{
|
|
return grid.Children
|
|
.OfType<PickerCalendarButton>()
|
|
.Where(button => button.IsVisible && button.Opacity > 0)
|
|
.Select(button => button.Content?.ToString() ?? string.Empty)
|
|
.ToArray();
|
|
}
|
|
|
|
private static PickerCalendarButton FindCalendarButton(AvaloniaGrid grid, DateTime date)
|
|
{
|
|
return grid.Children
|
|
.OfType<PickerCalendarButton>()
|
|
.Single(button => button.DataContext is DateTime buttonDate &&
|
|
DateTimeHelper.CompareDays(buttonDate, date) == 0);
|
|
}
|
|
|
|
private static void AssertDualMonthPanelVisibility(PickerDualMonthCalendarItem item, bool monthVisible)
|
|
{
|
|
AssertCalendarPanelVisibility(item, monthVisible);
|
|
item.YearViewLayout.ShouldNotBeNull().IsVisible.ShouldBe(!monthVisible);
|
|
FindNamedGrid(item, "PART_SecondaryYearView").IsVisible.ShouldBe(!monthVisible);
|
|
item.SecondaryMonthView.ShouldNotBeNull().IsVisible.ShouldBe(monthVisible);
|
|
}
|
|
|
|
private static void AssertCalendarPanelVisibility(PickerCalendarItem item, bool monthVisible)
|
|
{
|
|
item.MonthViewLayout.ShouldNotBeNull().IsVisible.ShouldBe(monthVisible);
|
|
item.MonthView.ShouldNotBeNull().IsVisible.ShouldBe(monthVisible);
|
|
item.YearView.ShouldNotBeNull().IsVisible.ShouldBe(!monthVisible);
|
|
}
|
|
|
|
private static Border FindTemplateBorder(Control control, string name)
|
|
{
|
|
control.ApplyTemplate();
|
|
Dispatcher.UIThread.RunJobs();
|
|
return control.GetVisualDescendants()
|
|
.OfType<Border>()
|
|
.Single(border => border.Name == name);
|
|
}
|
|
|
|
private static void ShowInWindow(Control content, Action assertion)
|
|
{
|
|
var window = new AvaloniaWindow
|
|
{
|
|
Width = 420,
|
|
Height = 360,
|
|
Content = content
|
|
};
|
|
|
|
try
|
|
{
|
|
window.Show();
|
|
Dispatcher.UIThread.RunJobs();
|
|
assertion();
|
|
}
|
|
finally
|
|
{
|
|
window.Close();
|
|
}
|
|
}
|
|
|
|
private static void RunOnUIThread(Action action)
|
|
{
|
|
Dispatcher.UIThread.Invoke(action);
|
|
}
|
|
|
|
private static string ReadRepoFile(string relativePath)
|
|
{
|
|
var directory = new DirectoryInfo(AppContext.BaseDirectory);
|
|
while (directory is not null)
|
|
{
|
|
var candidate = Path.Combine(directory.FullName, relativePath);
|
|
if (File.Exists(candidate))
|
|
{
|
|
return File.ReadAllText(candidate);
|
|
}
|
|
|
|
directory = directory.Parent;
|
|
}
|
|
|
|
throw new FileNotFoundException($"Could not find repository file: {relativePath}");
|
|
}
|
|
|
|
private sealed class TestCalendarItem : PickerCalendarItem
|
|
{
|
|
public void ClearGeneratedMonthViewForTest(AvaloniaGrid monthView)
|
|
{
|
|
ClearGeneratedMonthView(monthView);
|
|
}
|
|
|
|
public void HandleMonthCalendarButtonMouseUpForTest(PickerCalendarButton button)
|
|
{
|
|
HandleMonthCalendarButtonMouseUp(button, null!);
|
|
}
|
|
}
|
|
|
|
private sealed class TestRangeCalendarItem : RangeCalendarItem
|
|
{
|
|
public void HandleMonthMouseEnteredForTest(PickerCalendarButton button)
|
|
{
|
|
HandleMonthMouseEntered(button, null!);
|
|
}
|
|
}
|
|
|
|
private sealed class TestDatePicker : DatePicker
|
|
{
|
|
public DatePickerPresenter CreatePickerPresenterForTest()
|
|
{
|
|
var presenter = CreatePickerPresenter().ShouldBeAssignableTo<DatePickerPresenter>();
|
|
NotifyPickerPresenterCreated(presenter);
|
|
return presenter;
|
|
}
|
|
|
|
public void NotifyPickerOpenedForTest()
|
|
{
|
|
NotifyPickerOpened();
|
|
}
|
|
|
|
}
|
|
|
|
private sealed class TestDatePickerPresenter : DatePickerPresenter
|
|
{
|
|
public void NotifyTodayButtonClickedForTest()
|
|
{
|
|
NotifyTodayButtonClicked();
|
|
}
|
|
|
|
public void NotifyNowButtonClickedForTest()
|
|
{
|
|
NotifyNowButtonClicked();
|
|
}
|
|
}
|
|
|
|
private sealed class TestRangeDatePicker : RangeDatePicker
|
|
{
|
|
public RangeDatePickerPresenter CreatePickerPresenterForTest()
|
|
{
|
|
var presenter = CreatePickerPresenter().ShouldBeAssignableTo<RangeDatePickerPresenter>();
|
|
NotifyPickerPresenterCreated(presenter);
|
|
return presenter;
|
|
}
|
|
|
|
public void NotifyPickerOpenedForTest()
|
|
{
|
|
NotifyPickerOpened();
|
|
}
|
|
}
|
|
|
|
private sealed class TestRangeDatePickerPresenter : RangeDatePickerPresenter
|
|
{
|
|
public void NotifyNowButtonClickedForTest()
|
|
{
|
|
NotifyNowButtonClicked();
|
|
}
|
|
|
|
public void NotifyTodayButtonClickedForTest()
|
|
{
|
|
NotifyTodayButtonClicked();
|
|
}
|
|
}
|
|
|
|
private sealed class TestTimedRangeDatePickerPresenter : TimedRangeDatePickerPresenter
|
|
{
|
|
public void NotifyNowButtonClickedForTest()
|
|
{
|
|
NotifyNowButtonClicked();
|
|
}
|
|
}
|
|
}
|