using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading; using Paca.Browser.Search; using Paca.Core.QuickActions; using Xunit; namespace Paca.Tests.App; public class SpotlightRecentQueryAndScopeGateTests : IDisposable { private readonly string _tempHistoryPath; public SpotlightRecentQueryAndScopeGateTests() { _tempHistoryPath = Path.Combine(Path.GetTempPath(), $"paca_query_history_test_{Guid.NewGuid():N}.json"); } public void Dispose() { try { if (File.Exists(_tempHistoryPath)) File.Delete(_tempHistoryPath); } catch { } } [Fact] public void Gate228_QueryHistoryStore_Saves_Retrieves_And_Deduplicates_Queries() { var store = new QuickActionQueryHistoryStore(_tempHistoryPath); // 1. Initial should be empty Assert.Empty(store.GetQueries("search.kakaomap")); // 2. Add queries store.AddQuery("search.kakaomap", "판교 현대백화점"); store.AddQuery("search.kakaomap", "강남역 맛집"); store.AddQuery("search.google", "C# WPF tutorial"); var kakaoQueries = store.GetQueries("search.kakaomap"); Assert.Equal(2, kakaoQueries.Count); Assert.Equal("강남역 맛집", kakaoQueries[0]); Assert.Equal("판교 현대백화점", kakaoQueries[1]); // 3. Deduplication and bump to top store.AddQuery("search.kakaomap", "판교 현대백화점"); kakaoQueries = store.GetQueries("search.kakaomap"); Assert.Equal(2, kakaoQueries.Count); Assert.Equal("판교 현대백화점", kakaoQueries[0]); Assert.Equal("강남역 맛집", kakaoQueries[1]); // 4. Persistence reload test var reloaded = new QuickActionQueryHistoryStore(_tempHistoryPath); var reloadedKakao = reloaded.GetQueries("search.kakaomap"); Assert.Equal(2, reloadedKakao.Count); Assert.Equal("판교 현대백화점", reloadedKakao[0]); } [Fact] public void Gate229_Catalog_Contains_Scoped_Navigation_Actions() { var catalog = new QuickActionCatalog(); var tabScope = catalog.FindById("scope.tabs"); Assert.NotNull(tabScope); Assert.Contains("@tabs", tabScope.Keywords); Assert.Contains("@탭", tabScope.Keywords); Assert.Equal(QuickActionCategory.Browser, tabScope.Category); var bmScope = catalog.FindById("scope.bookmarks"); Assert.NotNull(bmScope); Assert.Contains("@bookmarks", bmScope.Keywords); Assert.Contains("@북마크", bmScope.Keywords); var histScope = catalog.FindById("scope.history"); Assert.NotNull(histScope); Assert.Contains("@history", histScope.Keywords); Assert.Contains("@방문기록", histScope.Keywords); } [Fact] public void Gate230_LiveLauncher_PillMode_Shows_Recent_Query_History() { Exception? threadEx = null; var thread = new Thread(() => { try { var store = new QuickActionQueryHistoryStore(_tempHistoryPath); store.AddQuery("search.kakao_map", "선릉역 라멘"); var catalog = new QuickActionCatalog(); var launcher = new Paca.App.QuickActionLauncherWindow(catalog, store); var kakaoAction = catalog.FindById("search.kakao_map"); Assert.NotNull(kakaoAction); // Enter Pill Mode with empty query launcher.EnterPillMode(kakaoAction); // Results list should contain the recent query! Assert.NotEmpty(launcher.QuickActionResultsList.Items); var first = launcher.QuickActionResultsList.Items[0] as QuickActionSearchResult; Assert.NotNull(first); Assert.Equal("선릉역 라멘", first.Title); Assert.Equal("최근 검색", first.CategoryName); 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 new Exception($"STA Thread failed: {threadEx}"); } } [Fact] public void Gate231_LiveLauncher_Scoped_Tabs_Filtering_And_Activation() { Exception? threadEx = null; var thread = new Thread(() => { try { var store = new QuickActionQueryHistoryStore(_tempHistoryPath); var catalog = new QuickActionCatalog(); var launcher = new Paca.App.QuickActionLauncherWindow(catalog, store); // Mock open tabs var mockTabs = new List { new ScopedSearchItem("네이버 지도", "https://map.naver.com", OmniboxSearchScope.Tabs, "tab-1"), new ScopedSearchItem("GitHub - Paca", "https://github.com/torvalds/linux", OmniboxSearchScope.Tabs, "tab-2"), new ScopedSearchItem("YouTube - 음악 재생", "https://youtube.com", OmniboxSearchScope.Tabs, "tab-3") }; launcher.TabProvider = () => mockTabs; string? activatedTabId = null; launcher.ScopedItemOpener = item => activatedTabId = item.AdditionalInfo; var tabsAction = catalog.FindById("scope.tabs"); Assert.NotNull(tabsAction); // Enter Pill Mode for @tabs launcher.EnterPillMode(tabsAction); // All open tabs should be listed initially Assert.Equal(3, launcher.QuickActionResultsList.Items.Count); // Filter by "GitHub" launcher.QuickActionSearchBox.Text = "GitHub"; Assert.Single(launcher.QuickActionResultsList.Items); var matched = (QuickActionSearchResult)launcher.QuickActionResultsList.Items[0]; Assert.Contains("GitHub", matched.Title); // Execute selected launcher.ExecuteSelectedAction(); Assert.Equal("tab-2", activatedTabId); 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; } }