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
|
|
@ -0,0 +1,177 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using Paca.Core.QuickActions;
|
||||
using Xunit;
|
||||
|
||||
namespace Paca.Tests.App;
|
||||
|
||||
public class QuickActionNextGenUxAndAnimationGateTests
|
||||
{
|
||||
private static readonly string SolutionRoot = Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", ".."));
|
||||
private readonly string _xamlPath;
|
||||
private readonly string _csPath;
|
||||
|
||||
public QuickActionNextGenUxAndAnimationGateTests()
|
||||
{
|
||||
_xamlPath = Path.Combine(SolutionRoot, "src", "Paca.App", "QuickActionLauncherWindow.xaml");
|
||||
_csPath = Path.Combine(SolutionRoot, "src", "Paca.App", "QuickActionLauncherWindow.xaml.cs");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Gate01_QuickActionFuzzyMatcher_MatchesSubsequenceAndTypos()
|
||||
{
|
||||
// Subsequence abbreviation tests
|
||||
Assert.True(QuickActionFuzzyMatcher.IsSubsequenceMatch("sct", "screenshot"));
|
||||
Assert.True(QuickActionFuzzyMatcher.IsSubsequenceMatch("dwn", "download_manager"));
|
||||
Assert.True(QuickActionFuzzyMatcher.IsSubsequenceMatch("clp", "clipboard"));
|
||||
Assert.False(QuickActionFuzzyMatcher.IsSubsequenceMatch("xyz", "screenshot"));
|
||||
|
||||
// Levenshtein distance tests
|
||||
int dist1 = QuickActionFuzzyMatcher.LevenshteinDistance("screenshot", "screnshot"); // 1 deletion
|
||||
Assert.Equal(1, dist1);
|
||||
|
||||
int dist2 = QuickActionFuzzyMatcher.LevenshteinDistance("download", "downlaod"); // 2 transposition
|
||||
Assert.True(dist2 <= 2);
|
||||
|
||||
// Search score evaluation
|
||||
int scoreSub = QuickActionFuzzyMatcher.CalculateFuzzyScore("sct", "screenshot");
|
||||
Assert.True(scoreSub > 0, "Subsequence should yield positive score");
|
||||
|
||||
int scoreTypo = QuickActionFuzzyMatcher.CalculateFuzzyScore("screnshot", "screenshot");
|
||||
Assert.True(scoreTypo > 0, "Typo with distance 1 should yield positive score");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Gate02_QuickActionUsageTracker_RecordsExecutionAndBoostsScore()
|
||||
{
|
||||
string tempFile = Path.Combine(Path.GetTempPath(), $"paca_usage_test_{Guid.NewGuid():N}.json");
|
||||
try
|
||||
{
|
||||
var tracker = new QuickActionUsageTracker(tempFile);
|
||||
Assert.Equal(0, tracker.GetScoreBoost("browser.new_tab"));
|
||||
|
||||
// Record execution
|
||||
tracker.RecordExecution("browser.new_tab");
|
||||
tracker.RecordExecution("browser.new_tab");
|
||||
tracker.RecordExecution("sys.screenshot_full");
|
||||
|
||||
// Verify count & boosts
|
||||
Assert.True(tracker.GetScoreBoost("browser.new_tab") >= 100);
|
||||
Assert.True(tracker.GetScoreBoost("sys.screenshot_full") >= 50);
|
||||
Assert.Equal(0, tracker.GetScoreBoost("media.unrecorded_action"));
|
||||
|
||||
// Verify persistence
|
||||
Assert.True(File.Exists(tempFile));
|
||||
var tracker2 = new QuickActionUsageTracker(tempFile);
|
||||
Assert.True(tracker2.GetScoreBoost("browser.new_tab") >= 100);
|
||||
|
||||
// Recent actions list
|
||||
var recent = tracker2.GetRecentActionIds(5);
|
||||
Assert.Contains("browser.new_tab", recent);
|
||||
Assert.Contains("sys.screenshot_full", recent);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (File.Exists(tempFile)) File.Delete(tempFile);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Gate03_QuickActionParameterRouter_RoutesSubCommandsCorrectly()
|
||||
{
|
||||
var router = new QuickActionParameterRouter();
|
||||
|
||||
// 1. Open URL
|
||||
Assert.True(router.TryParseCommand("open https://example.com", out var openAction));
|
||||
Assert.NotNull(openAction);
|
||||
Assert.Contains("https://example.com", openAction.Title);
|
||||
|
||||
// 2. Base64 encode
|
||||
Assert.True(router.TryParseCommand("b64 hello", out var b64Action));
|
||||
Assert.NotNull(b64Action);
|
||||
string? copied = null;
|
||||
var ctx = new QuickActionContext(ClipboardSetter: text => copied = text);
|
||||
Assert.NotNull(b64Action.Handler);
|
||||
var result = await b64Action.Handler.Invoke(ctx);
|
||||
Assert.True(result.Success);
|
||||
Assert.Equal("aGVsbG8=", copied);
|
||||
|
||||
// 3. Base64 decode
|
||||
Assert.True(router.TryParseCommand("b64d aGVsbG8=", out var b64dAction));
|
||||
Assert.NotNull(b64dAction);
|
||||
Assert.NotNull(b64dAction.Handler);
|
||||
var resultD = await b64dAction.Handler.Invoke(ctx);
|
||||
Assert.True(resultD.Success);
|
||||
Assert.Equal("hello", copied);
|
||||
|
||||
// 4. SHA-256 hash
|
||||
Assert.True(router.TryParseCommand("hash test", out var hashAction));
|
||||
Assert.NotNull(hashAction);
|
||||
Assert.True(hashAction.Title.Contains("SHA-256") || hashAction.Description?.Contains("SHA-256") == true);
|
||||
|
||||
// 5. Normal non-command query should return false
|
||||
Assert.False(router.TryParseCommand("새 탭 열기", out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Gate04_QuickActionLauncher_ContainsScaleAndFadeAnimationResources()
|
||||
{
|
||||
Assert.True(File.Exists(_xamlPath), $"XAML not found at {_xamlPath}");
|
||||
string xaml = File.ReadAllText(_xamlPath);
|
||||
|
||||
// 1. Check window width is 860 for split view
|
||||
Assert.Contains("Width=\"860\"", xaml);
|
||||
|
||||
// 2. Check ScaleTransform and TranslateTransform are defined in RootCard
|
||||
Assert.Contains("<ScaleTransform", xaml);
|
||||
Assert.Contains("<TranslateTransform", xaml);
|
||||
|
||||
// 3. Check CubicEase or QuadraticEase is used for smooth animations
|
||||
Assert.Contains("CubicEase", xaml);
|
||||
|
||||
// 4. Check animation duration <= 250ms for vestibulo-ocular reflex protection (WCAG SC 2.3.3)
|
||||
var durationMatches = Regex.Matches(xaml, @"Duration=""0:0:0\.([0-9]+)""");
|
||||
Assert.NotEmpty(durationMatches);
|
||||
foreach (Match match in durationMatches)
|
||||
{
|
||||
int ms = int.Parse(match.Groups[1].Value.PadRight(3, '0')[..3]);
|
||||
Assert.True(ms <= 250, $"Animation duration {ms}ms exceeds safe threshold (250ms)");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Gate05_QuickActionLauncher_ContainsActionDetailPreviewPane()
|
||||
{
|
||||
Assert.True(File.Exists(_xamlPath), $"XAML not found at {_xamlPath}");
|
||||
string xaml = File.ReadAllText(_xamlPath);
|
||||
|
||||
// Check for ActionDetailPane elements
|
||||
Assert.Contains("ActionDetailPane", xaml);
|
||||
Assert.Contains("DetailActionTitle", xaml);
|
||||
Assert.Contains("DetailActionId", xaml);
|
||||
Assert.Contains("DetailActionDesc", xaml);
|
||||
Assert.Contains("DetailCategoryBadge", xaml);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Gate06_QuickActionLauncher_ContainsDragMoveAndMouseHoverGuards()
|
||||
{
|
||||
Assert.True(File.Exists(_csPath), $"Code-behind not found at {_csPath}");
|
||||
string cs = File.ReadAllText(_csPath);
|
||||
|
||||
// 1. DragMove support for dragging launcher window
|
||||
Assert.Contains("DragMove()", cs);
|
||||
|
||||
// 2. Mouse move guard to avoid keyboard navigation conflict
|
||||
Assert.Contains("_lastMousePosition", cs);
|
||||
|
||||
// 3. Micro-feedback for action completion / clipboard copy
|
||||
Assert.Contains("ShowMicroFeedback", cs);
|
||||
|
||||
// 4. Animated show / hide
|
||||
Assert.Contains("ShowLauncherAnimated", cs);
|
||||
Assert.Contains("HideLauncherAnimated", cs);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue