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
532
tests/Paca.Tests/ModernBrowsingMilestone11Phase111Tests.cs
Normal file
532
tests/Paca.Tests/ModernBrowsingMilestone11Phase111Tests.cs
Normal file
|
|
@ -0,0 +1,532 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Paca.Browser.Url;
|
||||
using Xunit;
|
||||
|
||||
namespace Paca.Tests;
|
||||
|
||||
public sealed class ModernBrowsingMilestone11Phase111Tests : IDisposable
|
||||
{
|
||||
private readonly string _tempDir;
|
||||
|
||||
public ModernBrowsingMilestone11Phase111Tests()
|
||||
{
|
||||
_tempDir = Path.Combine(Path.GetTempPath(), "paca_m11_test_" + Guid.NewGuid().ToString("N"));
|
||||
Directory.CreateDirectory(_tempDir);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (Directory.Exists(_tempDir)) Directory.Delete(_tempDir, true);
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Gate175_CleanLinkExtractor_StripsTrackingParamsAndUnpacksAmpUrls()
|
||||
{
|
||||
// Arrange
|
||||
var extractor = new CleanLinkExtractor();
|
||||
|
||||
// Act 1: Strip UTM and social tracking parameters while keeping content params
|
||||
var rawSocialUrl = "https://example.com/article?utm_source=twitter&utm_medium=social&utm_campaign=autumn_sale&id=456&utm_content=logolink";
|
||||
var cleanedResult = extractor.Clean(rawSocialUrl);
|
||||
|
||||
// Assert 1
|
||||
Assert.True(cleanedResult.WasModified);
|
||||
Assert.False(cleanedResult.WasAmpUnpacked);
|
||||
Assert.Equal("https://example.com/article?id=456", cleanedResult.CleanedUrl);
|
||||
Assert.Contains("utm_source", cleanedResult.StrippedParameters);
|
||||
Assert.Contains("utm_medium", cleanedResult.StrippedParameters);
|
||||
Assert.Contains("utm_campaign", cleanedResult.StrippedParameters);
|
||||
Assert.Contains("utm_content", cleanedResult.StrippedParameters);
|
||||
Assert.DoesNotContain("id", cleanedResult.StrippedParameters);
|
||||
|
||||
// Act 2: Strip multiple ad tracker params (fbclid, gclid, msclkid, mc_cid)
|
||||
var rawAdUrl = "https://shop.example.com/item/shoes?fbclid=IwAR123&gclid=Cj0K456&msclkid=789&mc_cid=abc&mc_eid=def";
|
||||
var adResult = extractor.Clean(rawAdUrl);
|
||||
|
||||
// Assert 2: All query params were tracking, so clean URL has no query string
|
||||
Assert.True(adResult.WasModified);
|
||||
Assert.Equal("https://shop.example.com/item/shoes", adResult.CleanedUrl);
|
||||
Assert.Equal(5, adResult.StrippedParameters.Count);
|
||||
|
||||
// Act 3: Preserve URL fragment/hash
|
||||
var rawWithHash = "https://example.com/docs?utm_source=newsletter#section-installation";
|
||||
var hashResult = extractor.Clean(rawWithHash);
|
||||
Assert.Equal("https://example.com/docs#section-installation", hashResult.CleanedUrl);
|
||||
|
||||
// Act 4: Unpack Google AMP URLs to canonical destination
|
||||
var googleAmpHttps = "https://www.google.com/amp/s/news.example.com/politics/2026-election";
|
||||
var unpackedResult = extractor.Clean(googleAmpHttps);
|
||||
|
||||
Assert.True(unpackedResult.WasModified);
|
||||
Assert.True(unpackedResult.WasAmpUnpacked);
|
||||
Assert.Equal("https://news.example.com/politics/2026-election", unpackedResult.CleanedUrl);
|
||||
Assert.Equal("https://news.example.com/politics/2026-election", unpackedResult.UnpackedCanonicalUrl);
|
||||
|
||||
// Act 5: Unpack AMP cache CDN and simultaneously strip tracking params
|
||||
var ampCdnWithTracking = "https://example-com.cdn.ampproject.org/c/s/example.com/story?utm_source=amp_share&fbclid=xyz&page=2";
|
||||
var combinedResult = extractor.Clean(ampCdnWithTracking);
|
||||
|
||||
Assert.True(combinedResult.WasModified);
|
||||
Assert.True(combinedResult.WasAmpUnpacked);
|
||||
Assert.Equal("https://example.com/story?page=2", combinedResult.CleanedUrl);
|
||||
Assert.Contains("utm_source", combinedResult.StrippedParameters);
|
||||
Assert.Contains("fbclid", combinedResult.StrippedParameters);
|
||||
|
||||
// Act 6: Clean non-tracking URL returns untouched
|
||||
var pristineUrl = "https://clean.example.org/search?q=csharp&page=1";
|
||||
var pristineResult = extractor.Clean(pristineUrl);
|
||||
Assert.False(pristineResult.WasModified);
|
||||
Assert.Equal(pristineUrl, pristineResult.CleanedUrl);
|
||||
Assert.Empty(pristineResult.StrippedParameters);
|
||||
|
||||
// Act 7: Custom tracking parameters support
|
||||
var customExtractor = new CleanLinkExtractor(new[] { "custom_trk", "affiliate_tracker" });
|
||||
var customUrl = "https://store.example.com/book?custom_trk=99&title=tales";
|
||||
var customResult = customExtractor.Clean(customUrl);
|
||||
Assert.Equal("https://store.example.com/book?title=tales", customResult.CleanedUrl);
|
||||
Assert.Contains("custom_trk", customResult.StrippedParameters);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Gate176_TextFragmentDeepLinker_GeneratesAndParsesTextFragments()
|
||||
{
|
||||
// Arrange
|
||||
var linker = new TextFragmentDeepLinker();
|
||||
|
||||
// Act 1: Generate simple text fragment
|
||||
var simpleLink = linker.GenerateLink("https://en.wikipedia.org/wiki/Browser", "Web browser");
|
||||
Assert.Equal("https://en.wikipedia.org/wiki/Browser#:~:text=Web%20browser", simpleLink);
|
||||
|
||||
// Act 2: Generate text fragment with start and end range
|
||||
var rangeLink = linker.GenerateLink("https://example.com/doc", "Getting Started", "Next Steps");
|
||||
Assert.Equal("https://example.com/doc#:~:text=Getting%20Started,Next%20Steps", rangeLink);
|
||||
|
||||
// Act 3: Generate text fragment with context prefix and suffix
|
||||
var contextLink = linker.GenerateLink(
|
||||
"https://example.com/manual",
|
||||
textStart: "Configure PACA",
|
||||
textEnd: "Restart application",
|
||||
prefix: "Step 1 -",
|
||||
suffix: "- Complete");
|
||||
Assert.Contains("#:~:text=", contextLink);
|
||||
Assert.Contains("Step%201%20--", contextLink);
|
||||
Assert.Contains("Configure%20PACA,Restart%20application", contextLink);
|
||||
Assert.Contains("--%20Complete", contextLink);
|
||||
|
||||
// Act 4: Generate link preserving existing fragment hash
|
||||
var hashBase = "https://example.com/terms#privacy";
|
||||
var hashLink = linker.GenerateLink(hashBase, "Data Collection");
|
||||
Assert.Equal("https://example.com/terms#privacy:~:text=Data%20Collection", hashLink);
|
||||
|
||||
// Act 5: Parse multi-directive text fragment URL
|
||||
var complexUrl = "https://example.com/article#section1:~:text=Important%20notice&text=prefix-,highlight%20start,highlight%20end,-suffix";
|
||||
var parsed = linker.Parse(complexUrl);
|
||||
|
||||
Assert.True(parsed.HasTextFragment);
|
||||
Assert.Equal("https://example.com/article#section1", parsed.BaseUrlWithoutDirective);
|
||||
Assert.Equal("section1", parsed.TraditionalFragment);
|
||||
Assert.Equal(2, parsed.Directives.Count);
|
||||
|
||||
// Directive 1
|
||||
Assert.Equal("Important notice", parsed.Directives[0].TextStart);
|
||||
Assert.Null(parsed.Directives[0].TextEnd);
|
||||
Assert.Null(parsed.Directives[0].Prefix);
|
||||
Assert.Null(parsed.Directives[0].Suffix);
|
||||
|
||||
// Directive 2
|
||||
Assert.Equal("prefix", parsed.Directives[1].Prefix);
|
||||
Assert.Equal("highlight start", parsed.Directives[1].TextStart);
|
||||
Assert.Equal("highlight end", parsed.Directives[1].TextEnd);
|
||||
Assert.Equal("suffix", parsed.Directives[1].Suffix);
|
||||
|
||||
// Act 6: Strip text fragment directive, preserving traditional hash
|
||||
var strippedWithHash = linker.RemoveTextFragments("https://example.com/doc#summary:~:text=Summary%20Text");
|
||||
Assert.Equal("https://example.com/doc#summary", strippedWithHash);
|
||||
|
||||
var strippedWithoutHash = linker.RemoveTextFragments("https://example.com/doc#:~:text=Alone");
|
||||
Assert.Equal("https://example.com/doc", strippedWithoutHash);
|
||||
|
||||
// Act 7: Generate DOM highlight script
|
||||
var highlightScript = linker.GenerateHighlightScript(parsed.Directives);
|
||||
Assert.Contains("window.find", highlightScript);
|
||||
Assert.Contains("Important notice", highlightScript);
|
||||
Assert.Contains("highlight start", highlightScript);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Gate177_UrlBreadcrumbNavigator_SplitsHierarchyAndGeneratesBreadcrumbs()
|
||||
{
|
||||
// Arrange
|
||||
var navigator = new UrlBreadcrumbNavigator();
|
||||
|
||||
// Act 1: Parse hierarchical URL
|
||||
var rawUrl = "https://github.com/dotnet/runtime/tree/main/src/libraries";
|
||||
var breadcrumbs = navigator.ParseBreadcrumbs(rawUrl);
|
||||
|
||||
// Assert 1: 7 levels of breadcrumbs
|
||||
Assert.Equal(7, breadcrumbs.Count);
|
||||
|
||||
Assert.Equal("github.com", breadcrumbs[0].Label);
|
||||
Assert.Equal("https://github.com", breadcrumbs[0].Url);
|
||||
Assert.Equal(0, breadcrumbs[0].Level);
|
||||
Assert.True(breadcrumbs[0].IsHost);
|
||||
Assert.False(breadcrumbs[0].IsCurrent);
|
||||
|
||||
Assert.Equal("dotnet", breadcrumbs[1].Label);
|
||||
Assert.Equal("https://github.com/dotnet", breadcrumbs[1].Url);
|
||||
Assert.Equal(1, breadcrumbs[1].Level);
|
||||
Assert.False(breadcrumbs[1].IsHost);
|
||||
|
||||
Assert.Equal("runtime", breadcrumbs[2].Label);
|
||||
Assert.Equal("https://github.com/dotnet/runtime", breadcrumbs[2].Url);
|
||||
|
||||
Assert.Equal("tree", breadcrumbs[3].Label);
|
||||
Assert.Equal("https://github.com/dotnet/runtime/tree", breadcrumbs[3].Url);
|
||||
|
||||
Assert.Equal("main", breadcrumbs[4].Label);
|
||||
Assert.Equal("https://github.com/dotnet/runtime/tree/main", breadcrumbs[4].Url);
|
||||
|
||||
Assert.Equal("src", breadcrumbs[5].Label);
|
||||
Assert.Equal("https://github.com/dotnet/runtime/tree/main/src", breadcrumbs[5].Url);
|
||||
|
||||
Assert.Equal("libraries", breadcrumbs[6].Label);
|
||||
Assert.Equal("https://github.com/dotnet/runtime/tree/main/src/libraries", breadcrumbs[6].Url);
|
||||
Assert.Equal(6, breadcrumbs[6].Level);
|
||||
Assert.True(breadcrumbs[6].IsCurrent);
|
||||
|
||||
// Act 2: Root URL breadcrumb
|
||||
var rootBreadcrumbs = navigator.ParseBreadcrumbs("https://paca.browser.dev/");
|
||||
Assert.Single(rootBreadcrumbs);
|
||||
Assert.Equal("paca.browser.dev", rootBreadcrumbs[0].Label);
|
||||
Assert.True(rootBreadcrumbs[0].IsHost);
|
||||
Assert.True(rootBreadcrumbs[0].IsCurrent);
|
||||
|
||||
// Act 3: Parent URL navigation
|
||||
var parentUrl = navigator.GetParentUrl("https://github.com/dotnet/runtime/tree");
|
||||
Assert.Equal("https://github.com/dotnet/runtime", parentUrl);
|
||||
|
||||
var topParentUrl = navigator.GetParentUrl("https://github.com/dotnet");
|
||||
Assert.Equal("https://github.com", topParentUrl);
|
||||
|
||||
var rootParentUrl = navigator.GetParentUrl("https://github.com");
|
||||
Assert.Null(rootParentUrl);
|
||||
|
||||
// Act 4: Ancestor URLs collection
|
||||
var ancestors = navigator.GetAncestorUrls("https://github.com/dotnet/runtime");
|
||||
Assert.Equal(2, ancestors.Count);
|
||||
Assert.Equal("https://github.com", ancestors[0]);
|
||||
Assert.Equal("https://github.com/dotnet", ancestors[1]);
|
||||
|
||||
// Act 5: Percent-encoded segment decoding in labels
|
||||
var encodedUrl = "https://example.org/files/My%20Project%20Files/Reports%26Docs";
|
||||
var encodedCrumbs = navigator.ParseBreadcrumbs(encodedUrl);
|
||||
Assert.Equal(4, encodedCrumbs.Count);
|
||||
Assert.Equal("My Project Files", encodedCrumbs[2].Label);
|
||||
Assert.Equal("Reports&Docs", encodedCrumbs[3].Label);
|
||||
Assert.Equal("https://example.org/files/My%20Project%20Files", encodedCrumbs[2].Url);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Gate178_OmniboxQrCoordinator_GeneratesSvgQrCodeAndMobileHandoffLinks()
|
||||
{
|
||||
// Arrange
|
||||
var coordinator = new OmniboxQrCoordinator();
|
||||
|
||||
// Act 1: Generate valid QR code SVG payload
|
||||
var testUrl = "https://github.com/paca-browser/desktop";
|
||||
var svg = coordinator.GenerateQrSvg(testUrl, moduleSize: 8);
|
||||
|
||||
// Assert 1
|
||||
Assert.False(string.IsNullOrWhiteSpace(svg));
|
||||
Assert.Contains("<svg", svg, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.Contains("http://www.w3.org/2000/svg", svg);
|
||||
Assert.Contains("</svg>", svg, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
// Act 2: Custom theme styling (Dark mode SVG)
|
||||
var darkSvg = coordinator.GenerateQrSvg(
|
||||
testUrl,
|
||||
moduleSize: 10,
|
||||
foregroundColorHex: "#CDD6F4",
|
||||
backgroundColorHex: "#1E1E2E");
|
||||
Assert.Contains("#CDD6F4", darkSvg);
|
||||
Assert.Contains("#1E1E2E", darkSvg);
|
||||
|
||||
// Act 3: Mobile handoff payload generation
|
||||
var handoff = coordinator.CreateHandoffPayload(
|
||||
"https://news.ycombinator.com/item?id=12345",
|
||||
title: "Hacker News Story",
|
||||
deviceId: "desktop-paca-win");
|
||||
|
||||
// Assert 3
|
||||
Assert.Equal("https://news.ycombinator.com/item?id=12345", handoff.TargetUrl);
|
||||
Assert.Equal("Hacker News Story", handoff.Title);
|
||||
Assert.Equal("desktop-paca-win", handoff.DeviceId);
|
||||
Assert.StartsWith("paca://handoff?", handoff.HandoffDeepLink);
|
||||
Assert.Contains("url=" + Uri.EscapeDataString("https://news.ycombinator.com/item?id=12345"), handoff.HandoffDeepLink);
|
||||
Assert.Contains("<svg", handoff.QrCodeSvg);
|
||||
Assert.True(handoff.Timestamp <= DateTimeOffset.UtcNow);
|
||||
|
||||
// Act 4: Parse back mobile handoff deep link
|
||||
var parseSuccess = coordinator.TryParseHandoffLink(
|
||||
handoff.HandoffDeepLink,
|
||||
out var parsedUrl,
|
||||
out var parsedTitle,
|
||||
out var parsedDeviceId);
|
||||
|
||||
Assert.True(parseSuccess);
|
||||
Assert.Equal(handoff.TargetUrl, parsedUrl);
|
||||
Assert.Equal(handoff.Title, parsedTitle);
|
||||
Assert.Equal(handoff.DeviceId, parsedDeviceId);
|
||||
|
||||
// Act 5: Validate URL
|
||||
Assert.True(coordinator.ValidateUrl("https://example.com"));
|
||||
Assert.True(coordinator.ValidateUrl("http://localhost:5000/api"));
|
||||
Assert.False(coordinator.ValidateUrl("invalid-not-a-url"));
|
||||
Assert.False(coordinator.ValidateUrl(""));
|
||||
Assert.Throws<ArgumentException>(() => coordinator.GenerateQrSvg("not a valid url"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Gate179_QuickDictionaryLookup_ResolvesWordDefinitionsAndTranslations()
|
||||
{
|
||||
// Arrange
|
||||
var lookup = new QuickDictionaryLookup();
|
||||
|
||||
// Act 1: Text sanitization (double-click trims punctuation, quotes, whitespace)
|
||||
var sanitized = lookup.SanitizeWord(" \"omnibox,\" ");
|
||||
Assert.Equal("omnibox", sanitized);
|
||||
|
||||
var hyphenated = lookup.SanitizeWord("(browser-engine!)");
|
||||
Assert.Equal("browser-engine", hyphenated);
|
||||
|
||||
// Act 2: Built-in vocabulary lookup (English -> Korean translation)
|
||||
var omniboxResult = lookup.Lookup("omnibox", targetLanguage: "ko");
|
||||
|
||||
// Assert 2
|
||||
Assert.True(omniboxResult.Found);
|
||||
Assert.Equal("omnibox", omniboxResult.Word);
|
||||
Assert.NotNull(omniboxResult.Phonetic);
|
||||
Assert.NotEmpty(omniboxResult.Meanings);
|
||||
Assert.Contains(omniboxResult.Meanings, m => m.PartOfSpeech.Equals("noun", StringComparison.OrdinalIgnoreCase));
|
||||
Assert.True(omniboxResult.Translations.ContainsKey("ko"));
|
||||
Assert.Contains("주소창", omniboxResult.Translations["ko"]);
|
||||
|
||||
// Act 3: Lookup standard word with definitions and sample sentences
|
||||
var browserResult = lookup.Lookup("browser", targetLanguage: "ko");
|
||||
Assert.True(browserResult.Found);
|
||||
Assert.NotNull(browserResult.Phonetic);
|
||||
Assert.Contains(browserResult.Meanings, m => !string.IsNullOrEmpty(m.Example));
|
||||
Assert.Equal("웹 브라우저", browserResult.Translations["ko"]);
|
||||
|
||||
// Act 4: Custom vocabulary registration
|
||||
lookup.RegisterDefinition(
|
||||
word: "hypersync",
|
||||
phonetic: "/ˈhaɪ.pər.sɪŋk/",
|
||||
meanings: new[]
|
||||
{
|
||||
new WordMeaning("noun", "A real-time state synchronization protocol for distributed browsers.", "PACA uses hypersync for tab handoff.")
|
||||
},
|
||||
translations: new Dictionary<string, string>
|
||||
{
|
||||
["ko"] = "고속 동기화",
|
||||
["ja"] = "高速同期"
|
||||
});
|
||||
|
||||
var hypersyncResult = lookup.Lookup("hypersync", targetLanguage: "ja");
|
||||
Assert.True(hypersyncResult.Found);
|
||||
Assert.Equal("高速同期", hypersyncResult.Translations["ja"]);
|
||||
|
||||
// Act 5: Unknown word graceful fallback
|
||||
var unknownResult = lookup.Lookup("xyznonexistentword999");
|
||||
Assert.False(unknownResult.Found);
|
||||
Assert.Empty(unknownResult.Meanings);
|
||||
Assert.Empty(unknownResult.Translations);
|
||||
|
||||
// Act 6: Async lookup with cancellation token
|
||||
var asyncResult = await lookup.LookupAsync("cache", "ko");
|
||||
Assert.True(asyncResult.Found);
|
||||
Assert.Contains("캐시", asyncResult.Translations["ko"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Gate180_CaretBrowsingCoordinator_HandlesF7ToggleAndCursorNavigation()
|
||||
{
|
||||
// Arrange
|
||||
var coordinator = new CaretBrowsingCoordinator();
|
||||
string tabId = "tab-window-77";
|
||||
|
||||
// Assert initial state: Caret browsing disabled
|
||||
Assert.False(coordinator.IsEnabled(tabId));
|
||||
|
||||
// Act 1: Toggle Caret Browsing with F7 key event
|
||||
var eventFired = false;
|
||||
CaretState? observedState = null;
|
||||
coordinator.StateChanged += (sender, e) =>
|
||||
{
|
||||
eventFired = true;
|
||||
observedState = e.State;
|
||||
};
|
||||
|
||||
var keyResult1 = coordinator.HandleKeyDown(tabId, "F7");
|
||||
|
||||
// Assert 1: Toggled ON
|
||||
Assert.True(keyResult1.Handled);
|
||||
Assert.True(coordinator.IsEnabled(tabId));
|
||||
Assert.True(eventFired);
|
||||
Assert.NotNull(observedState);
|
||||
Assert.True(observedState.IsEnabled);
|
||||
|
||||
// Act 2: Move cursor with arrow keys
|
||||
coordinator.SetPosition(tabId, line: 10, column: 5, offset: 120);
|
||||
var initialPos = coordinator.GetPosition(tabId);
|
||||
Assert.Equal(10, initialPos.Line);
|
||||
Assert.Equal(5, initialPos.Column);
|
||||
Assert.Equal(120, initialPos.AbsoluteOffset);
|
||||
|
||||
// Right arrow moves column forward
|
||||
var rightResult = coordinator.HandleKeyDown(tabId, "ArrowRight");
|
||||
Assert.True(rightResult.Handled);
|
||||
var posAfterRight = coordinator.GetPosition(tabId);
|
||||
Assert.Equal(6, posAfterRight.Column);
|
||||
Assert.Equal(121, posAfterRight.AbsoluteOffset);
|
||||
|
||||
// Left arrow moves column backward
|
||||
coordinator.HandleKeyDown(tabId, "ArrowLeft");
|
||||
var posAfterLeft = coordinator.GetPosition(tabId);
|
||||
Assert.Equal(5, posAfterLeft.Column);
|
||||
|
||||
// Down arrow moves line down
|
||||
coordinator.HandleKeyDown(tabId, "ArrowDown");
|
||||
var posAfterDown = coordinator.GetPosition(tabId);
|
||||
Assert.Equal(11, posAfterDown.Line);
|
||||
|
||||
// Up arrow moves line up
|
||||
coordinator.HandleKeyDown(tabId, "ArrowUp");
|
||||
var posAfterUp = coordinator.GetPosition(tabId);
|
||||
Assert.Equal(10, posAfterUp.Line);
|
||||
|
||||
// Act 3: Shift + Arrow navigation expands selection
|
||||
coordinator.SetPosition(tabId, line: 1, column: 0, offset: 0);
|
||||
coordinator.HandleKeyDown(tabId, "ArrowRight", shiftKey: true);
|
||||
coordinator.HandleKeyDown(tabId, "ArrowRight", shiftKey: true);
|
||||
coordinator.HandleKeyDown(tabId, "ArrowRight", shiftKey: true);
|
||||
|
||||
var selection = coordinator.GetSelection(tabId);
|
||||
Assert.Equal(0, selection.StartOffset);
|
||||
Assert.Equal(3, selection.EndOffset);
|
||||
Assert.Equal(3, selection.Length);
|
||||
|
||||
// Regular navigation without shift clears selection
|
||||
coordinator.HandleKeyDown(tabId, "ArrowRight", shiftKey: false);
|
||||
var clearedSelection = coordinator.GetSelection(tabId);
|
||||
Assert.Equal(0, clearedSelection.Length);
|
||||
|
||||
// Act 4: Generate WebView2 injection script for caret overlay
|
||||
var enableScript = coordinator.GenerateInjectionScript(enabled: true);
|
||||
Assert.Contains("paca-caret-cursor", enableScript);
|
||||
Assert.Contains("addEventListener('keydown'", enableScript);
|
||||
|
||||
var disableScript = coordinator.GenerateInjectionScript(enabled: false);
|
||||
Assert.Contains("remove()", disableScript);
|
||||
|
||||
// Act 5: Toggle Caret Browsing OFF
|
||||
var keyResult2 = coordinator.HandleKeyDown(tabId, "F7");
|
||||
Assert.True(keyResult2.Handled);
|
||||
Assert.False(coordinator.IsEnabled(tabId));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Gate181_HttpResponseHeadersViewer_ParsesAndAnalyzesSecurityAndCacheHeaders()
|
||||
{
|
||||
// Arrange
|
||||
var viewer = new HttpResponseHeadersViewer();
|
||||
|
||||
// Act 1: Analyze secure response headers (HSTS, CSP, X-Frame-Options, X-Content-Type, Cache-Control)
|
||||
var secureHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains; preload",
|
||||
["Content-Security-Policy"] = "default-src 'self'; script-src 'self' https://trusted.cdn.com; frame-ancestors 'none'; upgrade-insecure-requests",
|
||||
["X-Frame-Options"] = "DENY",
|
||||
["X-Content-Type-Options"] = "nosniff",
|
||||
["Referrer-Policy"] = "strict-origin-when-cross-origin",
|
||||
["Permissions-Policy"] = "camera=(), microphone=(), geolocation=()",
|
||||
["Cache-Control"] = "public, max-age=86400, must-revalidate",
|
||||
["Content-Type"] = "text/html; charset=utf-8",
|
||||
["Server"] = "cloudflare"
|
||||
};
|
||||
|
||||
var analysis = viewer.Analyze(200, "OK", secureHeaders, protocol: "HTTP/2");
|
||||
|
||||
// Assert 1: Status & Summary
|
||||
Assert.Equal(200, analysis.StatusCode);
|
||||
Assert.Equal("OK", analysis.StatusDescription);
|
||||
Assert.Equal("HTTP/2", analysis.Protocol);
|
||||
Assert.NotEmpty(analysis.FormattedHeaders);
|
||||
|
||||
// Assert 1: Cache analysis
|
||||
Assert.True(analysis.CacheAnalysis.IsCacheable);
|
||||
Assert.True(analysis.CacheAnalysis.IsPublic);
|
||||
Assert.False(analysis.CacheAnalysis.IsNoStore);
|
||||
Assert.True(analysis.CacheAnalysis.IsMustRevalidate);
|
||||
Assert.Equal(86400, analysis.CacheAnalysis.MaxAgeSeconds);
|
||||
|
||||
// Assert 1: Security audit & scoring
|
||||
var sec = analysis.SecurityAudit;
|
||||
Assert.True(sec.Hsts.IsPresent);
|
||||
Assert.Equal(31536000, sec.Hsts.MaxAgeSeconds);
|
||||
Assert.True(sec.Hsts.IncludeSubDomains);
|
||||
Assert.True(sec.Hsts.Preload);
|
||||
Assert.True(sec.Hsts.MeetsStrictCriteria);
|
||||
|
||||
Assert.True(sec.Csp.IsPresent);
|
||||
Assert.True(sec.Csp.RestrictsScriptSrc);
|
||||
Assert.True(sec.Csp.RestrictsFrameAncestors);
|
||||
Assert.True(sec.Csp.UpgradesInsecureRequests);
|
||||
|
||||
Assert.Equal("DENY", sec.XFrameOptions);
|
||||
Assert.Equal("nosniff", sec.XContentTypeOptions);
|
||||
Assert.Equal("strict-origin-when-cross-origin", sec.ReferrerPolicy);
|
||||
|
||||
Assert.True(sec.SecurityScore >= 90);
|
||||
Assert.Equal("A+", sec.Grade);
|
||||
Assert.NotEmpty(sec.Strengths);
|
||||
Assert.Empty(sec.Recommendations);
|
||||
|
||||
// Act 2: Analyze insecure / missing headers response
|
||||
var insecureHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["Cache-Control"] = "no-store, no-cache",
|
||||
["Content-Type"] = "text/html"
|
||||
};
|
||||
|
||||
var insecureAnalysis = viewer.Analyze(200, "OK", insecureHeaders, protocol: "HTTP/1.1");
|
||||
|
||||
// Assert 2: Insecure analysis
|
||||
Assert.False(insecureAnalysis.CacheAnalysis.IsCacheable);
|
||||
Assert.True(insecureAnalysis.CacheAnalysis.IsNoStore);
|
||||
Assert.True(insecureAnalysis.CacheAnalysis.IsNoCache);
|
||||
|
||||
var insecureSec = insecureAnalysis.SecurityAudit;
|
||||
Assert.False(insecureSec.Hsts.IsPresent);
|
||||
Assert.False(insecureSec.Csp.IsPresent);
|
||||
Assert.Null(insecureSec.XFrameOptions);
|
||||
Assert.True(insecureSec.SecurityScore < 35);
|
||||
Assert.Equal("F", insecureSec.Grade);
|
||||
Assert.Contains(insecureSec.Recommendations, r => r.Contains("HSTS", StringComparison.OrdinalIgnoreCase));
|
||||
Assert.Contains(insecureSec.Recommendations, r => r.Contains("CSP", StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
// Act 3: Formatted raw text summary output
|
||||
var rawText = viewer.FormatAsRawText(200, "OK", secureHeaders, protocol: "HTTP/2");
|
||||
Assert.StartsWith("HTTP/2 200 OK", rawText);
|
||||
Assert.Contains("Strict-Transport-Security:", rawText);
|
||||
Assert.Contains("Cache-Control:", rawText);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue