releases/tests/Paca.Tests/App/SpotlightDeepIntegrationAndAnimationTests.cs

274 lines
10 KiB
C#

using System;
using System.IO;
using System.Linq;
using System.Threading;
using Paca.Core.QuickActions;
using Xunit;
namespace Paca.Tests.App;
public class SpotlightDeepIntegrationAndAnimationTests
{
private static readonly string SolutionRoot = Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", ".."));
private readonly string _launcherXamlPath;
private readonly string _launcherCsPath;
private readonly string _mwXamlPath;
private readonly string _mwCsPath;
public SpotlightDeepIntegrationAndAnimationTests()
{
_launcherXamlPath = Path.Combine(SolutionRoot, "src", "Paca.App", "QuickActionLauncherWindow.xaml");
_launcherCsPath = Path.Combine(SolutionRoot, "src", "Paca.App", "QuickActionLauncherWindow.xaml.cs");
_mwXamlPath = Path.Combine(SolutionRoot, "src", "Paca.App", "MainWindow.xaml");
_mwCsPath = Path.Combine(SolutionRoot, "src", "Paca.App", "MainWindow.xaml.cs");
}
[Fact]
public void Gate217_MainWindow_CtrlK_And_SpotlightButton_Wiring_To_ShowQuickActionLauncher()
{
var xaml = File.ReadAllText(_mwXamlPath);
var cs = File.ReadAllText(_mwCsPath);
// 1. MainWindow.xaml must contain a dedicated Spotlight button
Assert.Contains("x:Name=\"SpotlightBtn\"", xaml);
Assert.Contains("SpotlightBtn_Click", xaml);
Assert.Contains("스포트라이트", xaml);
// 2. MainWindow.xaml.cs must route Ctrl+K to ShowQuickActionLauncher
Assert.Contains("Key.K", cs);
Assert.Contains("ShowQuickActionLauncher", cs);
Assert.Contains("SpotlightBtn_Click", cs);
}
[Fact]
public void Gate218_ActionPill_CleanPillBadge_ToolTip_And_ScaleTransform_Audit()
{
var xaml = File.ReadAllText(_launcherXamlPath);
var cs = File.ReadAllText(_launcherCsPath);
// 1. ActionPill elements in XAML: Clean pill without unwanted close button
Assert.DoesNotContain("x:Name=\"ActionPillCloseBtn\"", xaml);
Assert.Contains("x:Name=\"ActionPillScaleTransform\"", xaml);
Assert.Contains("ActionPill_MouseLeftButtonDown", xaml);
Assert.Contains("ActionPillColon", xaml);
// 2. Codebehind must have mouse click and animation handling
Assert.Contains("ActionPill_MouseLeftButtonDown", cs);
Assert.Contains("ActionPillScaleTransform", cs);
}
[Theory]
[InlineData("https://github.com/torvalds/linux", "https://github.com/torvalds/linux")]
[InlineData("http://example.com", "http://example.com")]
[InlineData("www.google.com", "https://www.google.com")]
[InlineData("naver.com", "https://naver.com")]
[InlineData("news.ycombinator.com/news", "https://news.ycombinator.com/news")]
public void Gate219_DirectUrl_Input_In_Spotlight_Generates_DirectNavigate_Result(string input, string expectedUrl)
{
var catalog = new QuickActionCatalog();
var tracker = new QuickActionUsageTracker();
var engine = new QuickActionSearchEngine(catalog, tracker);
var results = engine.Search(input, 10, null, "ko-KR");
Assert.NotEmpty(results);
var first = results[0];
Assert.Equal("browser.direct_navigate", first.Action.Id);
Assert.NotNull(first.Action.SearchUrlTemplate);
Assert.Equal(expectedUrl, first.Action.SearchUrlTemplate);
Assert.Contains("웹 주소로 바로 이동", first.Title);
}
[Fact]
public void Gate220_MainWindow_ExecuteQuickAction_Dispatches_Core_Browser_Actions()
{
var cs = File.ReadAllText(_mwCsPath);
Assert.Contains("ExecuteQuickAction", cs);
Assert.Contains("case \"browser.new_tab\":", cs);
Assert.Contains("case \"browser.close_tab\":", cs);
Assert.Contains("case \"browser.toggle_vertical_tabs\":", cs);
Assert.Contains("case \"browser.toggle_split_view\":", cs);
Assert.Contains("case \"browser.reopen_closed_tab\":", cs);
}
[Fact]
public void Gate221_MainWindow_ExecuteQuickAction_Dispatches_Core_Media_Actions()
{
var cs = File.ReadAllText(_mwCsPath);
Assert.Contains("case \"media.pause_all\":", cs);
Assert.Contains("case \"media.resume_all\":", cs);
Assert.Contains("case \"media.open_output_folder\":", cs);
Assert.Contains("case \"media.speed_step_up\":", cs);
Assert.Contains("case \"media.speed_reset\":", cs);
}
[Fact]
public void Gate222_MainWindow_ExecuteQuickAction_Dispatches_Core_Paca_Actions()
{
var cs = File.ReadAllText(_mwCsPath);
Assert.Contains("case \"paca.settings\":", cs);
Assert.Contains("case \"paca.history\":", cs);
Assert.Contains("case \"paca.bookmarks\":", cs);
Assert.Contains("case \"paca.downloads\":", cs);
Assert.Contains("case \"paca.notes\":", cs);
}
[Fact]
public void Gate223_LiveLauncher_PillMode_CloseButton_And_Animation_Lifecycle()
{
Exception? threadEx = null;
var thread = new Thread(() =>
{
try
{
var catalog = new QuickActionCatalog();
var launcher = new Paca.App.QuickActionLauncherWindow(catalog);
var googleAction = catalog.FindById("search.google");
Assert.NotNull(googleAction);
// 1. Enter Pill Mode
launcher.EnterPillMode(googleAction);
Assert.True(launcher.IsInPillMode);
Assert.Equal(googleAction, launcher.ActivePillAction);
// 2. ActionPill is visible
Assert.Equal(System.Windows.Visibility.Visible, launcher.ActionPill.Visibility);
// 3. Exit via ExitPillMode
launcher.ExitPillMode(restoreActionTitle: true);
Assert.False(launcher.IsInPillMode);
Assert.Equal(System.Windows.Visibility.Collapsed, launcher.ActionPill.Visibility);
launcher.Close();
}
catch (Exception ex)
{
threadEx = ex;
}
});
thread.SetApartmentState(ApartmentState.STA);
thread.Start();
bool finished = thread.Join(8000);
Assert.True(finished, "STA thread must finish within timeout");
if (threadEx != null) throw threadEx;
}
[Fact]
public void Gate224_LiveLauncher_DirectUrl_Execution_Opens_Tab()
{
Exception? threadEx = null;
var thread = new Thread(() =>
{
try
{
var catalog = new QuickActionCatalog();
var launcher = new Paca.App.QuickActionLauncherWindow(catalog);
launcher.QuickActionSearchBox.Text = "https://news.ycombinator.com";
Assert.NotEmpty(launcher.QuickActionResultsList.Items);
var firstResult = (QuickActionSearchResult)launcher.QuickActionResultsList.Items[0];
Assert.Equal("browser.direct_navigate", firstResult.Action.Id);
string? openedUrl = null;
var context = new QuickActionContext(
TabOpener: url => openedUrl = url
);
firstResult.Action.Handler?.Invoke(context);
Assert.Equal("https://news.ycombinator.com", openedUrl);
launcher.Close();
}
catch (Exception ex)
{
threadEx = ex;
}
});
thread.SetApartmentState(ApartmentState.STA);
thread.Start();
bool finished = thread.Join(8000);
Assert.True(finished, "STA thread must finish within timeout");
if (threadEx != null) throw threadEx;
}
[Fact]
public void Gate225_MainWindow_OpenUrlInNewTab_ReusesEmptyNtpTab_And_AvoidsDuplicateTabs()
{
var cs = File.ReadAllText(_mwCsPath);
// 1. ExecuteQuickAction must not duplicate OpenUrlInNewTab when action.Handler is already defined
Assert.Contains("action.Handler == null", cs);
// 2. OpenUrlInNewTab must check whether the active tab is an empty NTP tab and reuse it
Assert.Contains("isCurrentTabEmptyNtp", cs);
Assert.Contains("NewTabPageBuilder.IsNewTabUrl", cs);
}
[Fact]
public void Gate226_Spotlight_Backspace_In_PillMode_Clears_Text_Without_Leaving_Residue()
{
var cs = File.ReadAllText(_launcherCsPath);
// 1. Backspace in pill mode must exit without restoring action title text
Assert.Contains("ExitPillMode(restoreActionTitle: false)", cs);
// 2. Live STA test to verify that Backspace when search box is empty leaves text completely clean
Exception? threadEx = null;
var thread = new Thread(() =>
{
try
{
var catalog = new QuickActionCatalog();
var launcher = new Paca.App.QuickActionLauncherWindow(catalog);
var googleAction = catalog.FindById("search.google");
Assert.NotNull(googleAction);
// Enter pill mode
launcher.EnterPillMode(googleAction);
Assert.True(launcher.IsInPillMode);
// Exit pill mode via clean exit (simulating Backspace)
launcher.ExitPillMode(restoreActionTitle: false);
Assert.False(launcher.IsInPillMode);
Assert.Equal(string.Empty, launcher.QuickActionSearchBox.Text);
launcher.Close();
}
catch (Exception ex)
{
threadEx = ex;
}
});
thread.SetApartmentState(ApartmentState.STA);
thread.Start();
bool finished = thread.Join(8000);
Assert.True(finished, "STA thread must finish within timeout");
if (threadEx != null) throw threadEx;
}
[Fact]
public void Gate227_Spotlight_SearchBox_And_AutocompleteHintText_PixelPerfect_Alignment_Audit()
{
var xaml = File.ReadAllText(_launcherXamlPath);
// 1. Must define dedicated SpotlightSearchBoxStyle to eliminate WPF default content margins
Assert.Contains("x:Key=\"SpotlightSearchBoxStyle\"", xaml);
Assert.Contains("PART_ContentHost", xaml);
// 2. QuickActionSearchBox must use the style with vertical centering and pixel snapping
Assert.Contains("Style=\"{StaticResource SpotlightSearchBoxStyle}\"", xaml);
// 3. AutocompleteHintText and QuickActionSearchBox must share zero-margin pixel-aligned placement
Assert.Contains("x:Name=\"AutocompleteHintText\"", xaml);
Assert.Contains("VerticalContentAlignment", xaml);
}
}