416 lines
17 KiB
C#
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);
|
|
}
|
|
}
|