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); } }