274 lines
10 KiB
C#
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);
|
|
}
|
|
}
|
|
|