TASK 7 UI polish batch: scene/source row cleanup, dedup naming, full social handles — scenes are now pure selection rows (edit/trash/visibility icons and inline rename removed with EditSceneCommand/RemoveSceneCommand/ToggleSceneVisibilityCommand + Scene.IsEditing); source rows gained the trio (new EditElementCommand → inline rename via SceneElement.IsEditing, ToggleElementVisibilityCommand → eye flips IsVisible, open/slashed style rebound, hidden rows dim to 45%); duplicate resource names get a no-space incrementing suffix via shared NextSourceName (Image, Image2, Image3…) derived from actual names so deletions never collide (AddSource + AddReusedImage); social bar renders the full validated handle (MaxWidth=200 + TextTrimming removed from SocialBarRenderer and the preview template); side panels stay fixed 220/300; focus-loss capture lag documented in ai.md as a known OS limit (deferred) — the two real-App tests now share RealAppHost, a dedicated STA thread owning the single WPF App, instead of each calling new App(); new SourceNamingTests integration test (Text/Text2/Text3, delete-middle re-add no-collision) — 170 tests passing, 0 warnings
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Windows.Threading;
|
||||
using Xunit;
|
||||
|
||||
namespace ytLive.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Tests that spin up the real WPF App need exactly one Application instance per
|
||||
/// AppDomain (WPF enforces it) — so they share this serial collection and a
|
||||
/// single App created on one dedicated STA thread. Marshal test bodies onto it
|
||||
/// via <see cref="RealAppHost.Run"/>, never `new App()` per test.
|
||||
/// </summary>
|
||||
[CollectionDefinition("RealApp", DisableParallelization = true)]
|
||||
public sealed class RealAppCollection : ICollectionFixture<RealAppHost>
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>Owns the one-and-only WPF App on a dedicated STA thread.</summary>
|
||||
public sealed class RealAppHost : IDisposable
|
||||
{
|
||||
private readonly Thread _thread;
|
||||
private readonly Dispatcher _dispatcher;
|
||||
|
||||
public RealAppHost()
|
||||
{
|
||||
Exception? boot = null;
|
||||
Dispatcher? dispatcher = null;
|
||||
_thread = new Thread(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var app = new App();
|
||||
app.InitializeComponent();
|
||||
dispatcher = Dispatcher.CurrentDispatcher;
|
||||
Dispatcher.Run();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
boot = ex;
|
||||
}
|
||||
});
|
||||
_thread.SetApartmentState(ApartmentState.STA);
|
||||
_thread.Start();
|
||||
while (dispatcher == null && boot == null)
|
||||
Thread.Sleep(5);
|
||||
if (boot != null)
|
||||
throw new Xunit.Sdk.XunitException("Real WPF App failed to start: " + boot);
|
||||
_dispatcher = dispatcher!;
|
||||
}
|
||||
|
||||
/// <summary>Runs <paramref name="action"/> on the App's STA thread and rethrows any failure.</summary>
|
||||
public void Run(Action action)
|
||||
{
|
||||
Exception? failure = null;
|
||||
_dispatcher.Invoke(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
action();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
failure = ex;
|
||||
}
|
||||
});
|
||||
if (failure != null)
|
||||
throw failure;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
try
|
||||
{
|
||||
_dispatcher.InvokeShutdown();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// The App never ran its message loop — shutdown is best-effort.
|
||||
}
|
||||
_thread.Join(2000);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user