mirror of
https://github.com/AtomUI/AtomUI.git
synced 2026-10-18 14:37:19 +08:00
309 lines
12 KiB
C#
309 lines
12 KiB
C#
using AtomUI.Controls;
|
|
using Avalonia;
|
|
using Avalonia.Controls;
|
|
using Avalonia.Controls.Presenters;
|
|
using Avalonia.Interactivity;
|
|
using Avalonia.Threading;
|
|
using Avalonia.VisualTree;
|
|
using Shouldly;
|
|
using Xunit;
|
|
using AtomUILineEdit = AtomUI.Desktop.Controls.LineEdit;
|
|
using AtomUIOtpLineEdit = AtomUI.Desktop.Controls.OtpLineEdit;
|
|
using AtomUISearchEdit = AtomUI.Desktop.Controls.SearchEdit;
|
|
using AtomUITextArea = AtomUI.Desktop.Controls.TextArea;
|
|
using AtomUITextBox = AtomUI.Desktop.Controls.TextBox;
|
|
using AvaloniaWindow = Avalonia.Controls.Window;
|
|
|
|
namespace AtomUI.Desktop.Controls.Tests.Input;
|
|
|
|
public class TextInputLifecycleTests
|
|
{
|
|
static TextInputLifecycleTests()
|
|
{
|
|
AvaloniaTestApp.EnsureInitialized();
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(typeof(AtomUITextBox))]
|
|
[InlineData(typeof(AtomUILineEdit))]
|
|
[InlineData(typeof(AtomUISearchEdit))]
|
|
[InlineData(typeof(AtomUITextArea))]
|
|
[InlineData(typeof(EmbeddedTextBox))]
|
|
[InlineData(typeof(SelectFilterTextBox))]
|
|
[InlineData(typeof(InfoPickerTextBox))]
|
|
[InlineData(typeof(QuickJumpEdit))]
|
|
[InlineData(typeof(MentionTextArea))]
|
|
[InlineData(typeof(AutoCompleteLineEditBox))]
|
|
[InlineData(typeof(AutoCompleteSearchEditBox))]
|
|
[InlineData(typeof(AutoCompleteTextAreaBox))]
|
|
public void Template_State_And_Interactions_Remain_Connected_After_Reattach(Type inputType)
|
|
{
|
|
var input = CreateTextInput(inputType);
|
|
var window = CreateWindow(input);
|
|
|
|
try
|
|
{
|
|
var revealButton = FindOptionalTemplatePart<RevealButton>(input, "PART_RevealButton");
|
|
revealButton?.IsVisible.ShouldBeFalse($"{inputType.Name} reveal button must initially be hidden.");
|
|
|
|
Reattach(window, input);
|
|
|
|
revealButton = FindOptionalTemplatePart<RevealButton>(input, "PART_RevealButton");
|
|
revealButton?.IsVisible.ShouldBeFalse($"{inputType.Name} reveal button must remain hidden after reattach.");
|
|
if (revealButton is not null)
|
|
{
|
|
input.IsEnableRevealButton = true;
|
|
Dispatcher.UIThread.RunJobs();
|
|
revealButton.IsVisible.ShouldBeTrue($"{inputType.Name} reveal visibility must remain bound after reattach.");
|
|
|
|
input.RevealPassword = true;
|
|
Dispatcher.UIThread.RunJobs();
|
|
revealButton.IsChecked.ShouldBe(true,
|
|
$"{inputType.Name} reveal checked state must remain bound after reattach.");
|
|
|
|
input.IsEnableRevealButton = false;
|
|
Dispatcher.UIThread.RunJobs();
|
|
revealButton.IsVisible.ShouldBeFalse();
|
|
}
|
|
|
|
input.Text = "value";
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
var clearButton = FindTemplatePart<InputClearIconButton>(input, "PART_ClearButton");
|
|
clearButton.IsVisible.ShouldBeTrue($"{inputType.Name} clear visibility must remain bound after reattach.");
|
|
|
|
if (input is AtomUILineEdit lineEdit)
|
|
{
|
|
var content = new Border();
|
|
lineEdit.InnerRightContent = content;
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
FindTemplatePart<ContentPresenter>(input, "InnerRightContentPresenter")
|
|
.Content.ShouldBeSameAs(content);
|
|
}
|
|
else if (input is AtomUITextArea textArea)
|
|
{
|
|
var content = new Border();
|
|
textArea.InnerRightContent = content;
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
FindTemplatePart<ContentPresenter>(input, "PART_InnerRightContentPresenter")
|
|
.Content.ShouldBeSameAs(content);
|
|
}
|
|
|
|
clearButton.RaiseEvent(new RoutedEventArgs(Avalonia.Controls.Button.ClickEvent));
|
|
Dispatcher.UIThread.RunJobs();
|
|
input.Text.ShouldBeNullOrEmpty($"{inputType.Name} clear interaction must remain connected after reattach.");
|
|
|
|
var presenter = FindTemplatePart<TextPresenter>(input, "PART_TextPresenter");
|
|
var placeholder = FindTemplatePart<AtomUI.Desktop.Controls.TextBlock>(input, "Placeholder");
|
|
presenter.SetCurrentValue(TextPresenter.PreeditTextProperty, "preedit");
|
|
Dispatcher.UIThread.RunJobs();
|
|
placeholder.IsVisible.ShouldBeFalse($"{inputType.Name} IME preedit tracking must remain connected after reattach.");
|
|
}
|
|
finally
|
|
{
|
|
window.Close();
|
|
}
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(typeof(AtomUI.Desktop.Controls.AutoComplete))]
|
|
[InlineData(typeof(AutoCompleteSearchEdit))]
|
|
[InlineData(typeof(AutoCompleteTextArea))]
|
|
public void AutoComplete_Internal_Input_Remains_Connected_After_Reattach(Type autoCompleteType)
|
|
{
|
|
var autoComplete = (Control)Activator.CreateInstance(autoCompleteType)!;
|
|
autoComplete.Width = 240;
|
|
var window = CreateWindow(autoComplete);
|
|
|
|
try
|
|
{
|
|
var input = autoComplete.GetVisualDescendants().OfType<AbstractTextInput>().Single();
|
|
FindOptionalTemplatePart<RevealButton>(input, "PART_RevealButton")?.IsVisible.ShouldBeFalse();
|
|
|
|
Reattach(window, autoComplete);
|
|
|
|
input = autoComplete.GetVisualDescendants().OfType<AbstractTextInput>().Single();
|
|
FindOptionalTemplatePart<RevealButton>(input, "PART_RevealButton")?.IsVisible.ShouldBeFalse(
|
|
$"{autoCompleteType.Name} must not expose a reveal button after its internal input is reattached.");
|
|
|
|
input.IsAllowClear = true;
|
|
input.Text = "value";
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
var clearButton = FindTemplatePart<InputClearIconButton>(input, "PART_ClearButton");
|
|
clearButton.RaiseEvent(new RoutedEventArgs(Avalonia.Controls.Button.ClickEvent));
|
|
Dispatcher.UIThread.RunJobs();
|
|
input.Text.ShouldBeNullOrEmpty(
|
|
$"{autoCompleteType.Name} internal clear interaction must remain connected after reattach.");
|
|
}
|
|
finally
|
|
{
|
|
window.Close();
|
|
}
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("LineEdit")]
|
|
[InlineData("OtpLineEdit")]
|
|
[InlineData("ComboBox")]
|
|
[InlineData("SelectHandle")]
|
|
[InlineData("PickerClearUpButton")]
|
|
public void Feedback_Subscriptions_Reconnect_After_Reattach(string controlKind)
|
|
{
|
|
var feedback = new FormValidateFeedback();
|
|
var lifecycleCase = CreateFeedbackLifecycleCase(controlKind, feedback);
|
|
var window = CreateWindow(lifecycleCase.Control);
|
|
|
|
try
|
|
{
|
|
lifecycleCase.IsFeedbackVisible().ShouldBeFalse();
|
|
|
|
window.Content = null;
|
|
Dispatcher.UIThread.RunJobs();
|
|
feedback.ValidateStatus = FormValidateStatus.Warning;
|
|
window.Content = lifecycleCase.Control;
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
lifecycleCase.IsFeedbackVisible().ShouldBeTrue(
|
|
$"{controlKind} must reconnect to the current feedback state when reattached.");
|
|
|
|
feedback.ValidateStatus = FormValidateStatus.Default;
|
|
Dispatcher.UIThread.RunJobs();
|
|
lifecycleCase.IsFeedbackVisible().ShouldBeFalse(
|
|
$"{controlKind} must continue observing feedback changes after reattach.");
|
|
}
|
|
finally
|
|
{
|
|
window.Close();
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void OtpLineEdit_Clear_Interaction_Remains_Connected_After_Reattach()
|
|
{
|
|
var otp = new AtomUIOtpLineEdit
|
|
{
|
|
Width = 240,
|
|
Text = "123456",
|
|
IsAllowClear = true
|
|
};
|
|
var window = CreateWindow(otp);
|
|
|
|
try
|
|
{
|
|
Reattach(window, otp);
|
|
|
|
var clearButton = FindTemplatePart<InputClearIconButton>(otp, "PART_ClearButton");
|
|
clearButton.RaiseEvent(new RoutedEventArgs(Avalonia.Controls.Button.ClickEvent));
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
otp.Text.ShouldBeNullOrEmpty("OtpLineEdit clear interaction must remain connected after reattach.");
|
|
}
|
|
finally
|
|
{
|
|
window.Close();
|
|
}
|
|
}
|
|
|
|
private static AbstractTextInput CreateTextInput(Type inputType)
|
|
{
|
|
var input = (AbstractTextInput)Activator.CreateInstance(inputType)!;
|
|
input.Width = 240;
|
|
input.Height = input is AtomUITextArea ? 100 : double.NaN;
|
|
input.IsAllowClear = true;
|
|
input.IsEnableRevealButton = false;
|
|
input.IsMotionEnabled = false;
|
|
return input;
|
|
}
|
|
|
|
private static FeedbackLifecycleCase CreateFeedbackLifecycleCase(
|
|
string controlKind,
|
|
FormValidateFeedback feedback)
|
|
{
|
|
return controlKind switch
|
|
{
|
|
"LineEdit" => CreateFormFeedbackLifecycleCase(new AtomUILineEdit { Width = 240 }, feedback),
|
|
"OtpLineEdit" => CreateFormFeedbackLifecycleCase(new AtomUIOtpLineEdit { Width = 240 }, feedback),
|
|
"ComboBox" => CreateFormFeedbackLifecycleCase(
|
|
new AtomUI.Desktop.Controls.ComboBox { Width = 240 }, feedback),
|
|
"SelectHandle" => CreateSelectHandleFeedbackLifecycleCase(feedback),
|
|
"PickerClearUpButton" => CreatePickerClearUpButtonFeedbackLifecycleCase(feedback),
|
|
_ => throw new ArgumentOutOfRangeException(nameof(controlKind), controlKind, null)
|
|
};
|
|
}
|
|
|
|
private static FeedbackLifecycleCase CreateFormFeedbackLifecycleCase(
|
|
Control control,
|
|
FormValidateFeedback feedback)
|
|
{
|
|
((IFormItemFeedbackAware)control).SetFeedbackControl(feedback);
|
|
return control switch
|
|
{
|
|
AbstractTextInput input => new(control, () => input.IsFormFeedbackVisible),
|
|
AtomUIOtpLineEdit otp => new(control, () => otp.IsFormFeedbackVisible),
|
|
AtomUI.Desktop.Controls.ComboBox comboBox => new(control, () => comboBox.IsFormFeedbackVisible),
|
|
_ => throw new ArgumentOutOfRangeException(nameof(control), control.GetType(), null)
|
|
};
|
|
}
|
|
|
|
private static FeedbackLifecycleCase CreateSelectHandleFeedbackLifecycleCase(FormValidateFeedback feedback)
|
|
{
|
|
var control = new SelectHandle
|
|
{
|
|
Width = 240,
|
|
FormFeedback = feedback
|
|
};
|
|
return new(control, () => control.IsFormFeedbackVisible);
|
|
}
|
|
|
|
private static FeedbackLifecycleCase CreatePickerClearUpButtonFeedbackLifecycleCase(FormValidateFeedback feedback)
|
|
{
|
|
var control = new PickerClearUpButton
|
|
{
|
|
FormFeedback = feedback
|
|
};
|
|
return new(control, () => control.IsFormFeedbackVisible);
|
|
}
|
|
|
|
private static AvaloniaWindow CreateWindow(Control content)
|
|
{
|
|
var window = new AvaloniaWindow
|
|
{
|
|
Width = 420,
|
|
Height = 240,
|
|
Content = content
|
|
};
|
|
window.Show();
|
|
Dispatcher.UIThread.RunJobs();
|
|
return window;
|
|
}
|
|
|
|
private static void Reattach(AvaloniaWindow window, Control control)
|
|
{
|
|
window.Content = null;
|
|
Dispatcher.UIThread.RunJobs();
|
|
window.Content = control;
|
|
Dispatcher.UIThread.RunJobs();
|
|
}
|
|
|
|
private static T FindTemplatePart<T>(Control control, string name)
|
|
where T : Control
|
|
{
|
|
return control.GetVisualDescendants()
|
|
.OfType<T>()
|
|
.Single(item => item.Name == name);
|
|
}
|
|
|
|
private static T? FindOptionalTemplatePart<T>(Control control, string name)
|
|
where T : Control
|
|
{
|
|
return control.GetVisualDescendants()
|
|
.OfType<T>()
|
|
.SingleOrDefault(item => item.Name == name);
|
|
}
|
|
|
|
private sealed record FeedbackLifecycleCase(Control Control, Func<bool> IsFeedbackVisible);
|
|
}
|