Files
LlamaCasty/ytLive.Tests/AlertBoxPreviewVisibilityTests.cs
gramps e2ecc248e0 TASK 47: draw the alert clip — the preview Image was Collapsed for AlertBox
Four builds (89/90 + two) burned proving the alert video was PERFECT: real h264
1280x720, 240 frames decoded, alert audio in the live mix, and the shipped asset
byte-identical (md5 0ee1f496…) to the creator's llamacasty-dancingLlama-thankyou.mp4
with every sampled frame full bright content. Build 90's new diagnostics then showed
the frame reaching BOTH consumers every second for the whole clip —
`Alert preview: frame=680x200 a255` and `Output resolver: frame 680x200
playing=True` — an opaque, correctly-sized frame, handed over and never seen.

Root cause: Controls/PreviewPane.xaml keeps the per-element Image Collapsed unless a
DataTrigger fires, and there was no IsAlertBox trigger (only IsImageSource / IsChatBox /
IsWebSource / IsWebcam). The alert box's Image was therefore Collapsed forever. Chat
boxes rendered because they HAVE a trigger — that asymmetry is the whole clue, and it
is why removing/re-adding the layer could never fix it.

Fix: an IsAlertBox DataTrigger beside the IsChatBox one.

Two Good Dog tests, because the existing fakes had been hiding this:
- AlertBoxPreviewVisibilityTests (red `Expected: Visible / Actual: Collapsed`, now
  green) drives the real MainWindow + PreviewPane and asserts the bound Image is
  Visible — the first alert test that crosses the XAML at all.
- AlertClipOutputTests is the first test in the repo to run a REAL codec: pinned ffmpeg
  generates a clip, the real AlertClipDecoder + AlertOverlayLayer + SceneCompositor
  composite it, and the box rect must be a colourful picture that DIFFERS from idle. It
  passed while the feature was broken in the app — which is exactly why it was needed:
  it exonerated decode+layer+compositor and pointed the hunt at the last hop.

Also keeps the two once-per-second alert diagnostics (resolver + preview) that settled
it, pending the creator's call on whether to keep them.

Notes: the pinned BtbN ffmpeg has no libx264, so the test clip is -c:v mpeg4.
SceneCompositor.Render fills its base with opaque black, so the test compares against an
idle render rather than counting non-zero bytes. MediaSource has the same missing trigger
in PreviewPane.xaml — left unfixed as out of scope, noted in HANDOFF.

Full suite 324/324. Lesson recorded in MyMistakes.md rule (5): a producer that hands over
a correct frame has still delivered nothing — when output arrives and the user still sees
nothing, stop auditing the producer and audit the consumer's VISIBILITY.
2026-09-26 14:44:57 -07:00

113 lines
4.0 KiB
C#

using System;
using System.IO;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using Microsoft.Data.Sqlite;
using Xunit;
using ytLive.Models;
using ytLive.ViewModels;
namespace ytLive.Tests;
/// <summary>
/// TASK 47 Good Dog (take three, 2026-09-26): the alert box's frame must be
/// DRAWN by the preview pane. The 14:26 and 14:28 live runs on build 90 proved
/// everything up to the last hop: <c>Alert preview: frame=680x200 a255</c> and
/// <c>Output resolver: frame 680x200 playing=True</c> every second for the whole
/// clip — a real, opaque, correctly-sized frame (the shipped llama asset decodes
/// bright, 1280x720 h264) handed to the canvas — and the box stayed blank.
/// Root cause: the preview's per-element <c>Image</c> only becomes Visible for
/// IsImageSource / IsChatBox / IsWebSource / IsWebcam, so an AlertBox's Image
/// stayed Collapsed and the bound frame was never rendered.
/// </summary>
[Collection("RealApp")]
public sealed class AlertBoxPreviewVisibilityTests
{
private readonly RealAppHost _app;
public AlertBoxPreviewVisibilityTests(RealAppHost app) => _app = app;
[Fact]
public void AlertBox_HandedAFrame_IsDrawnInThePreviewPane()
{
_app.Run(Run);
}
private void Run()
{
var tempDb = Path.Combine(Path.GetTempPath(), $"ytLlive-test-{Guid.NewGuid():N}.db");
MainViewModel.LayoutPathOverride = tempDb;
var window = new MainWindow();
var vm = (MainViewModel)window.DataContext;
try
{
window.Show();
window.UpdateLayout();
var box = new Source
{
Type = SourceType.AlertBox,
Name = "Stream Alerts",
X = 100,
Y = 100,
Width = 320,
Height = 180,
};
vm.StagedScene!.Elements.Add(box);
vm.SelectedElement = box;
// Exactly what the layer writes onto the source every tick while a clip
// plays: a real, opaque, box-sized frame.
box.VideoImageSource = SolidFrame(320, 180);
window.UpdateLayout();
var pane = (UserControl)window.FindName("PreviewPane")!;
var image = FindImageShowing(pane, box.VideoImageSource);
Assert.True(image != null,
"the preview pane never bound the alert box's frame to an Image");
Assert.Equal(Visibility.Visible, image!.Visibility);
Assert.True(image.IsVisible,
"the alert box's Image is Collapsed, so the frame is never rendered");
}
finally
{
window.Close();
MainViewModel.LayoutPathOverride = null;
SqliteConnection.ClearAllPools();
try { File.Delete(tempDb); } catch { /* best-effort cleanup */ }
}
}
private static WriteableBitmap SolidFrame(int w, int h)
{
var bitmap = new WriteableBitmap(w, h, 96, 96, PixelFormats.Bgra32, null);
var pixels = new byte[w * h * 4];
for (var i = 0; i < pixels.Length; i += 4)
{
pixels[i] = 40; // B
pixels[i + 1] = 90; // G
pixels[i + 2] = 200; // R
pixels[i + 3] = 255; // opaque
}
bitmap.WritePixels(new Int32Rect(0, 0, w, h), pixels, w * 4, 0);
return bitmap;
}
private static Image? FindImageShowing(DependencyObject parent, ImageSource? source)
=> Walk(parent, e => e is Image i && ReferenceEquals(i.Source, source)) as Image;
private static DependencyObject? Walk(DependencyObject parent, Func<DependencyObject, bool> predicate)
{
for (var i = 0; i < VisualTreeHelper.GetChildrenCount(parent); i++)
{
var child = VisualTreeHelper.GetChild(parent, i);
if (predicate(child)) return child;
var found = Walk(child, predicate);
if (found != null) return found;
}
return null;
}
}