refactor(app): compose interaction and retained UI startup
This commit is contained in:
parent
f663b04a54
commit
aa6ffa5176
15 changed files with 2157 additions and 467 deletions
|
|
@ -0,0 +1,223 @@
|
|||
using System.Numerics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using AcDream.App.Composition;
|
||||
using AcDream.App.Input;
|
||||
using AcDream.App.Interaction;
|
||||
using AcDream.App.Net;
|
||||
using AcDream.App.Rendering;
|
||||
using AcDream.App.UI;
|
||||
using AcDream.App.UI.Layout;
|
||||
using AcDream.Core.Net;
|
||||
using AcDream.Core.World;
|
||||
using AcDream.UI.Abstractions;
|
||||
|
||||
namespace AcDream.App.Tests.Composition;
|
||||
|
||||
public sealed class InteractionUiRuntimeSourcesTests
|
||||
{
|
||||
[Fact]
|
||||
public void SessionAuthorityDefaultsBindUnbindRebindAndDeactivateExactly()
|
||||
{
|
||||
var source = new DeferredLiveSessionUiAuthority();
|
||||
var first = new SessionTarget(true);
|
||||
var second = new SessionTarget(false);
|
||||
|
||||
Assert.False(source.IsInWorld);
|
||||
Assert.Same(NullCommandBus.Instance, source.Commands);
|
||||
|
||||
IDisposable stale = source.Bind(first);
|
||||
Assert.True(source.IsInWorld);
|
||||
Assert.Same(first.Commands, source.Commands);
|
||||
Assert.Throws<InvalidOperationException>(() => source.Bind(second));
|
||||
|
||||
stale.Dispose();
|
||||
IDisposable current = source.Bind(second);
|
||||
stale.Dispose();
|
||||
Assert.False(source.IsInWorld);
|
||||
Assert.Same(second.Commands, source.Commands);
|
||||
|
||||
source.Deactivate();
|
||||
current.Dispose();
|
||||
Assert.Same(NullCommandBus.Instance, source.Commands);
|
||||
Assert.Throws<ObjectDisposedException>(() => source.Bind(first));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SelectionAuthorityUsesRetailSafeDefaultsAndExactBindingLifetime()
|
||||
{
|
||||
var source = new DeferredSelectionUiAuthority();
|
||||
var query = new SelectionQuery();
|
||||
SelectionInteractionController interactions =
|
||||
Stub<SelectionInteractionController>();
|
||||
|
||||
Assert.True(source.IsWithinExternalContainerUseRange(1u));
|
||||
Assert.False(source.ShouldShowHealth(1u));
|
||||
Assert.Null(source.ResolveVividTargetInfo(1u));
|
||||
|
||||
IDisposable binding = source.Bind(query, interactions);
|
||||
Assert.False(source.IsWithinExternalContainerUseRange(1u));
|
||||
Assert.True(source.ShouldShowHealth(1u));
|
||||
Assert.Equal(3f, source.ResolveVividTargetInfo(1u)?.SelectionSphereRadius);
|
||||
Assert.Throws<InvalidOperationException>(() => source.Bind(query, interactions));
|
||||
|
||||
binding.Dispose();
|
||||
Assert.True(source.IsWithinExternalContainerUseRange(1u));
|
||||
source.Deactivate();
|
||||
Assert.Throws<ObjectDisposedException>(() => source.Bind(query, interactions));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RadarNeverCachesAnUnboundBootstrapSnapshot()
|
||||
{
|
||||
var source = new DeferredRadarSnapshotSource();
|
||||
Assert.Same(UiRadarSnapshot.Empty, source.Snapshot());
|
||||
|
||||
var provider = new RadarSnapshotProvider(
|
||||
new AcDream.Core.Items.ClientObjectTable(),
|
||||
static () => new Dictionary<uint, WorldEntity>(),
|
||||
static () => new Dictionary<uint, WorldSession.EntitySpawn>(),
|
||||
static () => 0u,
|
||||
static () => 0f,
|
||||
static () => 0u,
|
||||
static () => null,
|
||||
static () => false,
|
||||
static () => true);
|
||||
IDisposable binding = source.Bind(provider);
|
||||
|
||||
Assert.True(source.Snapshot().UiLocked);
|
||||
binding.Dispose();
|
||||
Assert.False(source.Snapshot().UiLocked);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AutomationProxyIsInertUntilExactRuntimeBinds()
|
||||
{
|
||||
var source = new DeferredWorldLifecycleAutomationRuntime();
|
||||
Assert.False(source.TryWriteCheckpoint("early", out string earlyError));
|
||||
Assert.Contains("not bound", earlyError);
|
||||
|
||||
var target = new AutomationRuntime();
|
||||
IDisposable binding = source.Bind(target);
|
||||
Assert.True(source.IsWorldReady);
|
||||
Assert.True(source.TryWriteCheckpoint("ready", out _));
|
||||
Assert.Equal("ready", target.LastCheckpoint);
|
||||
|
||||
binding.Dispose();
|
||||
Assert.False(source.IsWorldReady);
|
||||
source.Deactivate();
|
||||
Assert.Throws<ObjectDisposedException>(() => source.Bind(target));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExpectedOwnerBindingReleasesOnce()
|
||||
{
|
||||
object target = new();
|
||||
int releases = 0;
|
||||
var binding = new ExpectedOwnerBinding<object>(target, value =>
|
||||
{
|
||||
Assert.Same(target, value);
|
||||
releases++;
|
||||
});
|
||||
|
||||
binding.Dispose();
|
||||
binding.Dispose();
|
||||
|
||||
Assert.Equal(1, releases);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RetainedInputCaptureBindingCannotClearAReplacement()
|
||||
{
|
||||
var slot = new RetainedUiInputCaptureSlot();
|
||||
var first = new UiRoot();
|
||||
var second = new UiRoot();
|
||||
|
||||
IDisposable stale = slot.Bind(first);
|
||||
Assert.Same(first, slot.Root);
|
||||
Assert.Throws<InvalidOperationException>(() => slot.Bind(second));
|
||||
stale.Dispose();
|
||||
IDisposable current = slot.Bind(second);
|
||||
stale.Dispose();
|
||||
Assert.Same(second, slot.Root);
|
||||
current.Dispose();
|
||||
Assert.Null(slot.Root);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LateBindingCleanupRetriesOnlyTheFailedSuffix()
|
||||
{
|
||||
var bindings = new InteractionUiLateBindings();
|
||||
var first = new RetryBinding(failures: 0);
|
||||
var second = new RetryBinding(failures: 1);
|
||||
bindings.AdoptLateOwnerBinding("first", first);
|
||||
bindings.AdoptLateOwnerBinding("second", second);
|
||||
|
||||
Assert.Throws<AggregateException>(bindings.Dispose);
|
||||
Assert.Equal(1, first.Calls);
|
||||
Assert.Equal(1, second.Calls);
|
||||
|
||||
bindings.Dispose();
|
||||
Assert.Equal(1, first.Calls);
|
||||
Assert.Equal(2, second.Calls);
|
||||
Assert.Throws<ObjectDisposedException>(() =>
|
||||
bindings.AdoptLateOwnerBinding("late", new RetryBinding(0)));
|
||||
}
|
||||
|
||||
private sealed class SessionTarget(bool inWorld) : ILiveUiSessionTarget
|
||||
{
|
||||
public bool IsInWorld { get; } = inWorld;
|
||||
public WorldSession? CurrentSession => null;
|
||||
public ICommandBus Commands { get; } = new LiveCommandBus();
|
||||
}
|
||||
|
||||
private sealed class SelectionQuery : IRetainedUiSelectionQuery
|
||||
{
|
||||
public bool ShouldShowHealth(uint serverGuid) => true;
|
||||
public VividTargetInfo? ResolveVividTargetInfo(uint serverGuid) =>
|
||||
new(Vector3.Zero, 3f, 0u, 0u);
|
||||
public bool IsWithinExternalContainerUseRange(uint serverGuid) => false;
|
||||
}
|
||||
|
||||
private sealed class AutomationRuntime
|
||||
: AcDream.App.UI.Testing.IRetailUiAutomationRuntime
|
||||
{
|
||||
public bool IsWorldReady => true;
|
||||
public bool IsWorldViewportVisible => true;
|
||||
public int PortalMaterializationCount => 2;
|
||||
public string? LastCheckpoint { get; private set; }
|
||||
|
||||
public bool TryWriteCheckpoint(string name, out string error)
|
||||
{
|
||||
LastCheckpoint = name;
|
||||
error = string.Empty;
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryRequestScreenshot(string name, out string error)
|
||||
{
|
||||
error = string.Empty;
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool IsScreenshotComplete(string name) => true;
|
||||
}
|
||||
|
||||
private sealed class RetryBinding(int failures) : IDisposable
|
||||
{
|
||||
private int _failures = failures;
|
||||
public int Calls { get; private set; }
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Calls++;
|
||||
if (_failures > 0)
|
||||
{
|
||||
_failures--;
|
||||
throw new InvalidOperationException("retry");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static T Stub<T>() where T : class =>
|
||||
(T)RuntimeHelpers.GetUninitializedObject(typeof(T));
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue