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, 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("(() => 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 { ["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(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(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); } }