feat(ads/seo/release): v1.2.0 with live Google Ads, isolated ad profile, ads.txt, and SEO/AEO/GEO optimization
This commit is contained in:
parent
a88aafa1e5
commit
0e568f1f0a
975 changed files with 130593 additions and 16783 deletions
203
tests/Paca.Tests/App/QuickActionPillModeAndTabSearchTests.cs
Normal file
203
tests/Paca.Tests/App/QuickActionPillModeAndTabSearchTests.cs
Normal file
|
|
@ -0,0 +1,203 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Paca.Core.QuickActions;
|
||||
using Xunit;
|
||||
|
||||
namespace Paca.Tests.App;
|
||||
|
||||
public class QuickActionPillModeAndTabSearchTests
|
||||
{
|
||||
private static readonly string SolutionRoot = Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", ".."));
|
||||
private readonly string _xamlPath;
|
||||
private readonly string _launcherCsPath;
|
||||
private readonly string _mwCsPath;
|
||||
|
||||
public QuickActionPillModeAndTabSearchTests()
|
||||
{
|
||||
_xamlPath = Path.Combine(SolutionRoot, "src", "Paca.App", "QuickActionLauncherWindow.xaml");
|
||||
_launcherCsPath = Path.Combine(SolutionRoot, "src", "Paca.App", "QuickActionLauncherWindow.xaml.cs");
|
||||
_mwCsPath = Path.Combine(SolutionRoot, "src", "Paca.App", "MainWindow.xaml.cs");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Gate211_KakaoMapSearchAction_GeneratesKakaoMapUrl_AndNavigatesTabOpener()
|
||||
{
|
||||
var catalog = new QuickActionCatalog();
|
||||
var kakaoMapAction = catalog.FindById("search.kakao_map");
|
||||
|
||||
Assert.NotNull(kakaoMapAction);
|
||||
Assert.Equal("카카오맵 장소 검색", kakaoMapAction.Title);
|
||||
Assert.Equal(QuickActionCategory.Search, kakaoMapAction.Category);
|
||||
Assert.NotNull(kakaoMapAction.SearchUrlTemplate);
|
||||
Assert.Contains("map.kakao.com", kakaoMapAction.SearchUrlTemplate);
|
||||
|
||||
// 1. With search parameter
|
||||
string? openedUrlWithParam = null;
|
||||
var ctxWithParam = new QuickActionContext(
|
||||
Parameter: "강남역 맛집",
|
||||
TabOpener: url => openedUrlWithParam = url
|
||||
);
|
||||
|
||||
kakaoMapAction.Handler?.Invoke(ctxWithParam);
|
||||
Assert.NotNull(openedUrlWithParam);
|
||||
Assert.Contains("map.kakao.com", openedUrlWithParam);
|
||||
Assert.Contains(Uri.EscapeDataString("강남역 맛집"), openedUrlWithParam);
|
||||
|
||||
// 2. Without search parameter (direct site landing)
|
||||
string? openedUrlEmpty = null;
|
||||
var ctxEmpty = new QuickActionContext(
|
||||
Parameter: "",
|
||||
TabOpener: url => openedUrlEmpty = url
|
||||
);
|
||||
|
||||
kakaoMapAction.Handler?.Invoke(ctxEmpty);
|
||||
Assert.NotNull(openedUrlEmpty);
|
||||
Assert.Equal("https://map.kakao.com/", openedUrlEmpty);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("search.google", "google.com")]
|
||||
[InlineData("search.youtube", "youtube.com")]
|
||||
[InlineData("search.naver", "search.naver.com")]
|
||||
[InlineData("search.daum", "search.daum.net")]
|
||||
[InlineData("search.namuwiki", "namu.wiki")]
|
||||
[InlineData("search.wikipedia", "wikipedia.org")]
|
||||
[InlineData("search.github", "github.com")]
|
||||
[InlineData("search.naver_map", "map.naver.com")]
|
||||
[InlineData("search.kakao_map", "map.kakao.com")]
|
||||
[InlineData("search.coupang", "coupang.com")]
|
||||
[InlineData("search.duckduckgo", "duckduckgo.com")]
|
||||
public void Gate212_CoreSearchEngines_AllConfiguredWithSearchUrlTemplates(string actionId, string expectedHost)
|
||||
{
|
||||
var catalog = new QuickActionCatalog();
|
||||
var action = catalog.FindById(actionId);
|
||||
|
||||
Assert.NotNull(action);
|
||||
Assert.False(string.IsNullOrWhiteSpace(action.SearchUrlTemplate), $"SearchUrlTemplate must not be empty for {actionId}");
|
||||
Assert.Contains(expectedHost, action.SearchUrlTemplate);
|
||||
Assert.Contains("{query}", action.SearchUrlTemplate);
|
||||
|
||||
string? openedUrl = null;
|
||||
var ctx = new QuickActionContext(
|
||||
Parameter: "test query",
|
||||
TabOpener: url => openedUrl = url
|
||||
);
|
||||
|
||||
action.Handler?.Invoke(ctx);
|
||||
Assert.NotNull(openedUrl);
|
||||
Assert.Contains(expectedHost, openedUrl);
|
||||
Assert.Contains(Uri.EscapeDataString("test query"), openedUrl);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Gate213_QuickActionLauncherXaml_ContainsActionPillElement_AndSubComponents()
|
||||
{
|
||||
Assert.True(File.Exists(_xamlPath), $"XAML must exist at {_xamlPath}");
|
||||
var xaml = File.ReadAllText(_xamlPath);
|
||||
|
||||
// ActionPill container and components
|
||||
Assert.Contains("x:Name=\"ActionPill\"", xaml);
|
||||
Assert.Contains("x:Name=\"ActionPillIcon\"", xaml);
|
||||
Assert.Contains("x:Name=\"ActionPillTitle\"", xaml);
|
||||
Assert.Contains("x:Name=\"ActionPillColon\"", xaml);
|
||||
|
||||
// Pill visual styling tokens
|
||||
Assert.Contains("CornerRadius=\"6\"", xaml);
|
||||
Assert.Contains("AccentFadeBrush", xaml);
|
||||
Assert.Contains("AccentBrush", xaml);
|
||||
Assert.Contains("Visibility=\"Collapsed\"", xaml);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Gate214_QuickActionLauncherCodeBehind_SupportsPillModeLifecycle()
|
||||
{
|
||||
Assert.True(File.Exists(_launcherCsPath), $"Launcher CS must exist at {_launcherCsPath}");
|
||||
var code = File.ReadAllText(_launcherCsPath);
|
||||
|
||||
// Pill mode lifecycle API
|
||||
Assert.Contains("EnterPillMode", code);
|
||||
Assert.Contains("ExitPillMode", code);
|
||||
Assert.Contains("ActivePillAction", code);
|
||||
Assert.Contains("IsInPillMode", code);
|
||||
|
||||
// Key bindings: Tab triggers pill mode, Backspace when empty exits pill mode
|
||||
Assert.Contains("Key.Tab", code);
|
||||
Assert.Contains("Key.Back", code);
|
||||
Assert.Contains("TabOpener", code);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Gate215_MainWindow_Provides_OpenUrlInNewTab()
|
||||
{
|
||||
Assert.True(File.Exists(_mwCsPath), $"MainWindow CS must exist at {_mwCsPath}");
|
||||
var code = File.ReadAllText(_mwCsPath);
|
||||
|
||||
Assert.Contains("OpenUrlInNewTab", code);
|
||||
Assert.Contains("Cv2Navigate", code);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Gate216_LiveLauncher_EndToEnd_PillMode_Tab_Typing_Execution_And_Backspace()
|
||||
{
|
||||
Exception? threadEx = null;
|
||||
var thread = new System.Threading.Thread(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var catalog = new QuickActionCatalog();
|
||||
var launcher = new Paca.App.QuickActionLauncherWindow(catalog);
|
||||
|
||||
// 1. Initial state: Pill is collapsed
|
||||
Assert.False(launcher.IsInPillMode);
|
||||
Assert.Null(launcher.ActivePillAction);
|
||||
|
||||
// 2. Select kakao map action
|
||||
var kakaoAction = catalog.FindById("search.kakao_map");
|
||||
Assert.NotNull(kakaoAction);
|
||||
|
||||
// 3. Enter Pill Mode
|
||||
launcher.EnterPillMode(kakaoAction);
|
||||
Assert.True(launcher.IsInPillMode);
|
||||
Assert.Equal(kakaoAction, launcher.ActivePillAction);
|
||||
|
||||
// 4. Input query text "강남역 맛집"
|
||||
launcher.QuickActionSearchBox.Text = "강남역 맛집";
|
||||
|
||||
// 5. Execute action and verify ActionExecuted event
|
||||
string? executedActionId = null;
|
||||
string? executedQuery = null;
|
||||
launcher.ActionExecuted += (act, q) =>
|
||||
{
|
||||
executedActionId = act.Id;
|
||||
executedQuery = q;
|
||||
};
|
||||
|
||||
launcher.ExecuteSelectedAction();
|
||||
Assert.Equal("search.kakao_map", executedActionId);
|
||||
Assert.Equal("강남역 맛집", executedQuery);
|
||||
Assert.False(launcher.IsInPillMode); // Resets after execution
|
||||
|
||||
// 6. Test Backspace exiting Pill Mode
|
||||
launcher.EnterPillMode(kakaoAction);
|
||||
Assert.True(launcher.IsInPillMode);
|
||||
launcher.QuickActionSearchBox.Text = "";
|
||||
launcher.ExitPillMode(restoreActionTitle: true);
|
||||
Assert.False(launcher.IsInPillMode);
|
||||
Assert.Equal(kakaoAction.Title, launcher.QuickActionSearchBox.Text);
|
||||
|
||||
launcher.Close();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
threadEx = ex;
|
||||
}
|
||||
});
|
||||
thread.SetApartmentState(System.Threading.ApartmentState.STA);
|
||||
thread.Start();
|
||||
bool finished = thread.Join(8000);
|
||||
Assert.True(finished, "STA thread must finish within timeout");
|
||||
|
||||
if (threadEx != null) throw threadEx;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue