releases/tests/Paca.Tests/ModernBrowsingMilestone9Phase109Tests.cs

416 lines
17 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Paca.Browser.Media;
using Xunit;
namespace Paca.Tests;
public sealed class ModernBrowsingMilestone9Phase109Tests : IDisposable
{
private readonly string _tempDir;
public ModernBrowsingMilestone9Phase109Tests()
{
_tempDir = Path.Combine(Path.GetTempPath(), "paca_m9_test_" + Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(_tempDir);
}
public void Dispose()
{
try
{
if (Directory.Exists(_tempDir)) Directory.Delete(_tempDir, true);
}
catch { }
}
[Fact]
public void Gate159_VideoFrameCaptureCoordinator_CapturesNativeResolutionFrameMetadataAndPngPipeline()
{
var outputDir = Path.Combine(_tempDir, "captures");
var coordinator = new VideoFrameCaptureCoordinator();
// Arrange metadata
var meta = new VideoFrameMetadata(
VideoId: "vid_test_101",
Title: "Epic 4K Nature Clip: Ocean Waves",
NativeWidth: 3840,
NativeHeight: 2160,
CurrentTimeSeconds: 72.45,
DurationSeconds: 180.0,
FrameRate: 60.0,
CapturedAtUtc: DateTime.UtcNow
);
// Act 1: Suggested file name sanitization
var filename = coordinator.GenerateSuggestedFileName(meta.Title, meta.CurrentTimeSeconds);
Assert.False(string.IsNullOrWhiteSpace(filename));
Assert.EndsWith(".png", filename, StringComparison.OrdinalIgnoreCase);
Assert.DoesNotContain(":", filename); // sanitized illegal chars
Assert.Contains("Ocean Waves", filename);
Assert.Contains("72.45s", filename);
// Act 2: Process native resolution capture
var dummyRawPixels = new byte[4 * 4 * 4]; // 4x4 RGBA
for (int i = 0; i < dummyRawPixels.Length; i += 4)
{
dummyRawPixels[i] = 0x22; // R
dummyRawPixels[i + 1] = 0x88; // G
dummyRawPixels[i + 2] = 0xEE; // B
dummyRawPixels[i + 3] = 0xFF; // A
}
var snapshot = coordinator.CreateSnapshot(meta, dummyRawPixels, width: 4, height: 4);
Assert.NotNull(snapshot);
Assert.Equal(meta, snapshot.Metadata);
Assert.True(snapshot.ImageData.Length > 0);
// Valid PNG signature check: 0x89, 'P', 'N', 'G', 0x0D, 0x0A, 0x1A, 0x0A
Assert.Equal(0x89, snapshot.ImageData[0]);
Assert.Equal((byte)'P', snapshot.ImageData[1]);
Assert.Equal((byte)'N', snapshot.ImageData[2]);
Assert.Equal((byte)'G', snapshot.ImageData[3]);
// Act 3: Save to directory
var savedPath = coordinator.SaveSnapshot(snapshot, outputDir);
Assert.True(File.Exists(savedPath));
Assert.Equal(snapshot.ImageData.Length, new FileInfo(savedPath).Length);
// Act 4: Aspect ratio & history
var ratio = VideoFrameCaptureCoordinator.CalculateAspectRatio(3840, 2160);
Assert.Equal("16:9", ratio);
var history = coordinator.GetRecentSnapshots();
Assert.Single(history);
Assert.Equal(snapshot.SuggestedFileName, history[0].SuggestedFileName);
coordinator.ClearHistory();
Assert.Empty(coordinator.GetRecentSnapshots());
}
[Fact]
public void Gate160_MediaSpeedController_AdjustsSpeedInStepPointsAndPreservesPitch()
{
var controller = new MediaSpeedController();
// Assert defaults
Assert.Equal(1.0, controller.CurrentSpeed);
Assert.True(controller.PreservesPitch);
// Act 1: Step up
controller.StepUp();
Assert.Equal(1.1, controller.CurrentSpeed);
// Act 2: Event notification
double notifiedSpeed = 0;
bool notifiedPitch = false;
controller.SpeedChanged += (s, e) =>
{
notifiedSpeed = e.NewSpeed;
notifiedPitch = e.PreservesPitch;
};
controller.SetSpeed(1.5);
Assert.Equal(1.5, controller.CurrentSpeed);
Assert.Equal(1.5, notifiedSpeed);
Assert.True(notifiedPitch);
// Act 3: Boundary clamping (min 0.25, max 4.0)
controller.SetSpeed(5.5);
Assert.Equal(4.0, controller.CurrentSpeed);
controller.SetSpeed(0.1);
Assert.Equal(0.25, controller.CurrentSpeed);
// Act 4: Pitch preservation toggle
controller.SetPreservesPitch(false);
Assert.False(controller.PreservesPitch);
// Act 5: Generate JS injection script
controller.SetSpeed(1.75);
var script = controller.GeneratePlaybackScript();
Assert.Contains("playbackRate = 1.75", script);
Assert.Contains("preservesPitch = false", script);
// Act 6: Reset
controller.Reset();
Assert.Equal(1.0, controller.CurrentSpeed);
Assert.True(controller.PreservesPitch);
}
[Fact]
public void Gate161_AutoplayPolicyEngine_EnforcesAutoplayPoliciesAndDomainExceptions()
{
var configPath = Path.Combine(_tempDir, "autoplay_policy.json");
var engine = new AutoplayPolicyEngine(configPath, defaultPolicy: AutoplayPolicy.BlockAudioOnly);
// Act 1: Default policy evaluation
var decisionSilent = engine.EvaluateAutoplay("https://news.portal.com/article", hasAudioTrack: false);
Assert.False(decisionSilent.IsBlocked);
Assert.False(decisionSilent.IsMuted);
Assert.Equal(AutoplayPolicy.BlockAudioOnly, decisionSilent.AppliedPolicy);
var decisionAudio = engine.EvaluateAutoplay("https://news.portal.com/article", hasAudioTrack: true);
Assert.False(decisionAudio.IsBlocked);
Assert.True(decisionAudio.IsMuted);
// Act 2: Domain exception - BlockAll
engine.SetDomainPolicy("annoying-ads.com", AutoplayPolicy.BlockAll);
var decisionBlockAll = engine.EvaluateAutoplay("https://sub.annoying-ads.com/popup", hasAudioTrack: false);
Assert.True(decisionBlockAll.IsBlocked);
Assert.Equal(AutoplayPolicy.BlockAll, decisionBlockAll.AppliedPolicy);
// Act 3: Domain exception - Allow
engine.SetDomainPolicy("youtube.com", AutoplayPolicy.Allow);
var decisionAllow = engine.EvaluateAutoplay("https://www.youtube.com/watch?v=123", hasAudioTrack: true);
Assert.False(decisionAllow.IsBlocked);
Assert.False(decisionAllow.IsMuted);
Assert.Equal(AutoplayPolicy.Allow, decisionAllow.AppliedPolicy);
// Act 4: Persistence
engine.Save();
var reloaded = new AutoplayPolicyEngine(configPath);
Assert.Equal(AutoplayPolicy.Allow, reloaded.GetDomainPolicy("youtube.com"));
Assert.Equal(AutoplayPolicy.BlockAll, reloaded.GetDomainPolicy("annoying-ads.com"));
// Act 5: Remove exception
reloaded.RemoveDomainPolicy("youtube.com");
Assert.Equal(reloaded.DefaultPolicy, reloaded.GetDomainPolicy("youtube.com"));
}
[Fact]
public void Gate162_LiveCaptionCoordinator_BuffersSpeechAssemblesSentencesAndEmitsEvents()
{
var coordinator = new LiveCaptionCoordinator();
Assert.True(coordinator.IsEnabled);
var liveUpdates = new List<string>();
var completedSentences = new List<string>();
coordinator.CaptionUpdated += (s, e) => liveUpdates.Add(e.CurrentCaption);
coordinator.SentenceCompleted += (s, e) => completedSentences.Add(e.CompletedSentence);
// Act 1: Stream speech tokens
coordinator.PushChunk("Hello", confidence: 0.95, isDelimiter: false);
coordinator.PushChunk("world", confidence: 0.96, isDelimiter: false);
Assert.Equal("Hello world", coordinator.ActiveBufferText);
Assert.Empty(completedSentences);
// Act 2: Delimiter completes sentence
coordinator.PushChunk(".", confidence: 0.98, isDelimiter: true);
Assert.Single(completedSentences);
Assert.Equal("Hello world.", completedSentences[0]);
Assert.Equal(string.Empty, coordinator.ActiveBufferText);
// Act 3: Assemble sentences from text block
coordinator.PushText("Welcome to PACA browser! It is fast and secure.");
Assert.Equal(3, completedSentences.Count);
Assert.Equal("Welcome to PACA browser!", completedSentences[1]);
Assert.Equal("It is fast and secure.", completedSentences[2]);
// Act 4: Full transcript and reset
var transcript = coordinator.GetFullTranscript();
Assert.Contains("Hello world.", transcript);
Assert.Contains("Welcome to PACA browser!", transcript);
coordinator.Clear();
Assert.Equal(string.Empty, coordinator.ActiveBufferText);
Assert.Equal(string.Empty, coordinator.GetFullTranscript());
}
[Fact]
public void Gate163_PageVisualFilterEngine_AppliesCssVisualFiltersAndDomainPreferences()
{
var configPath = Path.Combine(_tempDir, "visual_filters.json");
var engine = new PageVisualFilterEngine(configPath);
// Act 1: CSS rule generation for supported filters
var grayscaleCss = engine.GetFilterCss(VisualFilterKind.Grayscale, 1.0);
Assert.Contains("grayscale(100%)", grayscaleCss);
var sepiaCss = engine.GetFilterCss(VisualFilterKind.Sepia, 0.8);
Assert.Contains("sepia(80%)", sepiaCss);
var invertCss = engine.GetFilterCss(VisualFilterKind.Invert, 1.0);
Assert.Contains("invert(100%)", invertCss);
var highContrastCss = engine.GetFilterCss(VisualFilterKind.HighContrast, 1.0);
Assert.Contains("contrast(", highContrastCss);
var readabilityCss = engine.GetFilterCss(VisualFilterKind.ReadabilityHighlight, 1.0);
Assert.Contains("contrast(", readabilityCss);
Assert.Contains("saturate(", readabilityCss);
// Act 2: Script injection generation
var script = engine.GenerateFilterInjectionScript(VisualFilterKind.Grayscale, 1.0);
Assert.Contains("paca-visual-filter", script);
Assert.Contains("grayscale(100%)", script);
var clearScript = engine.GenerateClearFilterScript();
Assert.Contains("paca-visual-filter", clearScript);
Assert.Contains("remove", clearScript);
// Act 3: Per-site domain preferences & persistence
engine.SetDomainFilter("reading-site.org", VisualFilterKind.Sepia, 0.85);
Assert.Equal(VisualFilterKind.Sepia, engine.GetDomainFilter("reading-site.org").Filter);
Assert.Equal(0.85, engine.GetDomainFilter("reading-site.org").Intensity);
engine.Save();
var reloaded = new PageVisualFilterEngine(configPath);
Assert.Equal(VisualFilterKind.Sepia, reloaded.GetDomainFilter("reading-site.org").Filter);
}
[Fact]
public void Gate164_GpuFallbackPolicy_ManagesSiteAccelerationAndSoftwareRenderingFallbackList()
{
var configPath = Path.Combine(_tempDir, "gpu_fallback.json");
var policy = new GpuFallbackPolicy(configPath);
// Act 1: Default site evaluation
var normalSiteDecision = policy.EvaluateSite("https://example.com");
Assert.Equal(GpuRenderingMode.HardwareAccelerated, normalSiteDecision.Mode);
Assert.False(normalSiteDecision.IsSoftwareFallback);
Assert.True(policy.IsWebGLAllowed("example.com"));
// Act 2: Built-in safe software fallback list for known problematic/crash-prone sites
Assert.True(policy.IsInSoftwareFallbackList("shadertoy.com"));
var fallbackDecision = policy.EvaluateSite("https://www.shadertoy.com/view/123");
Assert.Equal(GpuRenderingMode.SoftwareFallback, fallbackDecision.Mode);
Assert.True(fallbackDecision.IsSoftwareFallback);
// Act 3: Custom domain override & WebGL disable
policy.SetSiteRenderingMode("heavy-app.local", GpuRenderingMode.Disabled);
policy.SetWebGLAllowed("heavy-app.local", false);
var customDecision = policy.EvaluateSite("https://heavy-app.local/dashboard");
Assert.Equal(GpuRenderingMode.Disabled, customDecision.Mode);
Assert.False(policy.IsWebGLAllowed("heavy-app.local"));
// Act 4: Chromium command line switch generation
var flagsHardware = policy.GetChromiumSwitches("example.com");
Assert.DoesNotContain("--disable-gpu", flagsHardware);
var flagsFallback = policy.GetChromiumSwitches("shadertoy.com");
Assert.Contains("--disable-gpu", flagsFallback);
// Act 5: Persistence
policy.AddToSoftwareFallbackList("crashing-canvas.net", "Known GPU driver freeze");
policy.Save();
var reloaded = new GpuFallbackPolicy(configPath);
Assert.True(reloaded.IsInSoftwareFallbackList("crashing-canvas.net"));
Assert.Equal(GpuRenderingMode.Disabled, reloaded.GetSiteRenderingMode("heavy-app.local"));
}
[Fact]
public void Gate165_FontOverridePolicy_EnforcesMinimumFontSizeFloorAndFontFamilyOverrides()
{
var configPath = Path.Combine(_tempDir, "font_override.json");
var policy = new FontOverridePolicy(configPath);
// Assert defaults
Assert.Equal(14, policy.DefaultOptions.MinimumFontSizePx);
Assert.True(policy.DefaultOptions.EnforceMinimumFontSize);
// Act 1: Generate CSS rules with minimum font floor & system font
var options = new FontPolicyOptions(
MinimumFontSizePx: 16,
EnforceMinimumFontSize: true,
EnableFontOverride: true,
PreferredFontFamily: "Segoe UI",
MonospaceFontFamily: "Cascadia Code"
);
var css = policy.GenerateCss(options);
Assert.Contains("min-font-size: 16px", css);
Assert.Contains("font-size:", css);
Assert.Contains("Segoe UI", css);
Assert.Contains("Cascadia Code", css);
// Act 2: Generate injection script
var script = policy.GenerateInjectionScript(options);
Assert.Contains("paca-font-override", script);
Assert.Contains("Segoe UI", script);
// Act 3: Value clamping (e.g. min font size 9px to 32px)
var clampedOptions = policy.SanitizeOptions(new FontPolicyOptions(MinimumFontSizePx: 4));
Assert.Equal(9, clampedOptions.MinimumFontSizePx);
var clampedMaxOptions = policy.SanitizeOptions(new FontPolicyOptions(MinimumFontSizePx: 50));
Assert.Equal(32, clampedMaxOptions.MinimumFontSizePx);
// Act 4: Per-domain font policy
policy.SetDomainPolicy("code.dev", new FontPolicyOptions(
MinimumFontSizePx: 18,
EnforceMinimumFontSize: true,
EnableFontOverride: true,
PreferredFontFamily: "Consolas",
MonospaceFontFamily: "Consolas"
));
var domainPolicy = policy.GetEffectivePolicy("https://sub.code.dev/doc");
Assert.Equal(18, domainPolicy.MinimumFontSizePx);
Assert.Equal("Consolas", domainPolicy.PreferredFontFamily);
// Act 5: Persistence
policy.Save();
var reloaded = new FontOverridePolicy(configPath);
var reloadedDomain = reloaded.GetEffectivePolicy("https://code.dev");
Assert.Equal(18, reloadedDomain.MinimumFontSizePx);
}
[Fact]
public void Gate166_DeviceEmulationCoordinator_EmulatesMobileViewportsAndUserAgents()
{
var coordinator = new DeviceEmulationCoordinator();
// Assert predefined devices (iPhone 16, Galaxy S25, iPad Pro)
var devices = coordinator.GetPredefinedDevices();
Assert.Contains(devices, d => d.Name == "iPhone 16");
Assert.Contains(devices, d => d.Name == "Galaxy S25");
Assert.Contains(devices, d => d.Name == "iPad Pro");
// Act 1: Apply iPhone 16 emulation
coordinator.ApplyEmulation("iPhone 16");
Assert.NotNull(coordinator.ActiveDevice);
Assert.Equal("iPhone 16", coordinator.ActiveDevice.Name);
Assert.Equal(393, coordinator.ActiveDevice.ViewportWidth);
Assert.Equal(852, coordinator.ActiveDevice.ViewportHeight);
Assert.Equal(3.0, coordinator.ActiveDevice.DevicePixelRatio);
Assert.True(coordinator.ActiveDevice.HasTouch);
Assert.True(coordinator.ActiveDevice.IsMobile);
Assert.Contains("iPhone", coordinator.ActiveDevice.UserAgent);
// Act 2: Orientation toggle (Portrait <-> Landscape)
Assert.Equal(ScreenOrientation.Portrait, coordinator.ActiveDevice.Orientation);
coordinator.ToggleOrientation();
Assert.Equal(ScreenOrientation.Landscape, coordinator.ActiveDevice.Orientation);
Assert.Equal(852, coordinator.ActiveDevice.ViewportWidth);
Assert.Equal(393, coordinator.ActiveDevice.ViewportHeight);
// Act 3: Galaxy S25 emulation
coordinator.ApplyEmulation("Galaxy S25");
Assert.NotNull(coordinator.ActiveDevice);
Assert.Equal(412, coordinator.ActiveDevice.ViewportWidth);
Assert.Equal(915, coordinator.ActiveDevice.ViewportHeight);
Assert.Contains("Android", coordinator.ActiveDevice.UserAgent);
// Act 4: iPad Pro emulation
coordinator.ApplyEmulation("iPad Pro");
Assert.NotNull(coordinator.ActiveDevice);
Assert.Equal(1024, coordinator.ActiveDevice.ViewportWidth);
Assert.Equal(1366, coordinator.ActiveDevice.ViewportHeight);
Assert.Contains("iPad", coordinator.ActiveDevice.UserAgent);
// Act 5: Generate Emulation JavaScript injection
var injectionJs = coordinator.GenerateEmulationScript(coordinator.ActiveDevice);
Assert.Contains("navigator.userAgent", injectionJs);
Assert.Contains("window.devicePixelRatio", injectionJs);
Assert.Contains("ontouchstart", injectionJs);
// Act 6: Reset emulation
coordinator.ResetEmulation();
Assert.Null(coordinator.ActiveDevice);
}
}