Files
LlamaCasty/ytLive.Tests/WebView2ManagerTests.cs
T
gramps b4bba4bf68 fix(web): harden the transparency injection to the OBS-standard element-wide !important wipe
The real-widget dumps (12:54/13:50) proved the OLD inline
element.style.background='transparent' injection holds only while the page has
nothing to paint: the capture was alpha-transparent, yet the recording showed a
black opaque box over the whole element rect (1231,679 705x396) once the widget
connected and repainted a container background-COLOR — CapturePreviewAsync always
honors page CSS (MicrosoftEdge/WebView2Feedback specs/BackgroundColor.md), so any
page-painted background wins over DefaultBackgroundColor.

This is the OBS-solved class (all web-uri resources paint their own background):
browser sources use a Custom CSS override, and the decade-validated formula for
arbitrary pages is a pre-parse <style> with 'background-color: transparent
!important' — https://obsproject.com/forum/threads/translucent-transparent-browser-source.59549/
('body { background-color: rgba(0,0,0,0) !important }') plus the div-level variant
for stubborn widgets (woahtech.com OBS custom-CSS guide).

Injection is now an idempotent pre-parse style element wiping background-color on
html,body,html * with !important (outranks every page rule, runs before page parse
via AddScriptToExecuteOnDocumentCreatedAsync). Only background-COLOR is targeted —
background images and widget art survive. Regression test asserts the element-wide
!important form and that the losing inline form is gone.

9/9 WebView2Manager tests, 0 warnings.
2026-09-10 14:04:11 -07:00

201 lines
6.1 KiB
C#

using System.Drawing;
using System.Linq;
using System.Windows.Controls;
using System.Windows.Threading;
using Microsoft.Web.WebView2.Wpf;
using Xunit;
using ytLive.Models;
using ytLive.Services;
namespace ytLive.Tests;
[Collection("RealApp")]
public sealed class WebView2ManagerTests
{
private readonly RealAppHost _app;
public WebView2ManagerTests(RealAppHost app)
{
_app = app;
}
[Fact]
public void WebView2_Control_Size_Is_Canvas_Resolution_Regardless_Of_Source()
{
_app.Run(() =>
{
var panel = new Canvas();
var source = new Source
{
Type = SourceType.WebSource,
Width = 640,
Height = 480,
};
using var manager = new WebView2Manager(panel, Dispatcher.CurrentDispatcher);
manager.Register(source);
var webView = panel.Children.OfType<WebView2>().First();
Assert.Equal(1920.0, webView.Width);
Assert.Equal(1080.0, webView.Height);
});
}
[Fact]
public void WebView2_Control_Size_Does_Not_Track_Box_After_Register()
{
_app.Run(() =>
{
var panel = new Canvas();
var source = new Source
{
Type = SourceType.WebSource,
Width = 640,
Height = 480,
};
using var manager = new WebView2Manager(panel, Dispatcher.CurrentDispatcher);
manager.Register(source);
var webView = panel.Children.OfType<WebView2>().First();
source.Width = 1024;
source.Height = 768;
Assert.Equal(1920.0, webView.Width);
Assert.Equal(1080.0, webView.Height);
});
}
[Fact]
public void WebView2_Control_Size_Ignores_Zero_Sized_Source()
{
_app.Run(() =>
{
var panel = new Canvas();
var source = new Source
{
Type = SourceType.WebSource,
Width = 0,
Height = 0,
};
using var manager = new WebView2Manager(panel, Dispatcher.CurrentDispatcher);
manager.Register(source);
var webView = panel.Children.OfType<WebView2>().First();
Assert.Equal(1920.0, webView.Width);
Assert.Equal(1080.0, webView.Height);
});
}
[Fact]
public void WebView2_DefaultBackgroundColor_Is_Transparent()
{
_app.Run(() =>
{
var panel = new Canvas();
var source = new Source
{
Type = SourceType.WebSource,
Width = 640,
Height = 480,
};
using var manager = new WebView2Manager(panel, Dispatcher.CurrentDispatcher);
manager.Register(source);
var webView = panel.Children.OfType<WebView2>().First();
Assert.Equal(System.Drawing.Color.Transparent, webView.DefaultBackgroundColor);
});
}
[Fact]
public void TransparentBackgroundScript_Is_A_Important_Element_Wide_Wipe()
{
// The regression the inline style lost to: a widget repainting a container (or
// html/body) opaque after connect re-backs the capture → "black box" in the
// recording while early frames stayed transparent. The OBS-validated formula is a
// pre-parse <style> with !important outranking every page rule (OBS Forums 2016).
var script = WebView2Manager.TransparentBackgroundScript;
Assert.Contains("background-color:transparent!important", script);
Assert.Contains("html,body,html *", script);
Assert.Contains("document.createElement('style')", script);
Assert.DoesNotContain(".style.background=", script);
}
[Fact]
public void FindContentBounds_Returns_Bounding_Box_Of_NonTransparent_Pixels()
{
const int w = 10, h = 6;
var pixels = new byte[w * h * 4];
for (int yy = 2; yy <= 4; yy++)
for (int xx = 3; xx <= 6; xx++)
{
var i = (yy * w + xx) * 4;
pixels[i] = 255; // B
pixels[i + 1] = 0; // G
pixels[i + 2] = 0; // R
pixels[i + 3] = 255; // A
}
var (x, y, bw, bh) = WebView2Manager.FindContentBounds(pixels, w, h, w * 4);
Assert.Equal((3, 2, 4, 3), (x, y, bw, bh));
}
[Fact]
public void FindContentBounds_No_Alpha_Means_No_Crop()
{
const int w = 10, h = 6;
var pixels = new byte[w * h * 4];
var (x, y, bw, bh) = WebView2Manager.FindContentBounds(pixels, w, h, w * 4);
Assert.Equal((0, 0, w, h), (x, y, bw, bh));
}
[Fact]
public void FindContentBounds_Full_Opacity_Means_No_Crop()
{
const int w = 10, h = 6;
var pixels = new byte[w * h * 4];
for (var i = 3; i < pixels.Length; i += 4)
pixels[i] = 255;
var (x, y, bw, bh) = WebView2Manager.FindContentBounds(pixels, w, h, w * 4);
Assert.Equal((0, 0, w, h), (x, y, bw, bh));
}
[Fact]
public async Task CaptureScheduler_Drops_Ticks_While_Capture_InFlight_And_Resumes()
{
// The ~30Hz recording cadence would stack concurrent CapturePreviewAsync calls
// (full-HD PNG encode is ~10-30ms) without the latest-wins drop: ticks that
// arrive while a capture is in flight must be skipped, never queued, and a
// capture can always start once the previous one has finished.
var calls = 0;
var signal = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
var scheduler = new CaptureScheduler(Dispatcher.CurrentDispatcher, 200, async () =>
{
calls++;
await signal.Task;
});
scheduler.Tick();
scheduler.Tick();
scheduler.Tick();
Assert.Equal(1, calls); // overlapping ticks dropped, never queued
signal.SetResult();
await scheduler.Pending; // durable: Pending completes only after the drop resets
scheduler.Tick();
Assert.Equal(2, calls); // once idle, the next tick captures again
await scheduler.Pending;
}
}