Make character UI retries transactional

This commit is contained in:
Erik 2026-08-14 21:14:08 +02:00
parent aeac874dab
commit 1dd5706e15
6 changed files with 352 additions and 42 deletions

View file

@ -188,6 +188,66 @@ public sealed class CharacterManagementUiControllerTests
Assert.Equal(3, resourceAttempts);
}
[Fact]
public void MountCoordinator_PostAttachFailuresDisposeBeforeRetryAndRecovery()
{
var host = new UiRoot { Width = 800f, Height = 600f };
var runtime = new FakeRuntime();
using var dialogs = new RetailDialogFactory(
host,
RetailDialogFactoryTests.BuildDialogLayout);
var screens = new List<ImportedLayout>();
int failingAttempts = 2;
using var coordinator = new CharacterManagementUiMountCoordinator(
host,
runtime.Bindings,
() => dialogs,
() =>
{
ImportedLayout screen = BuildScreen();
screens.Add(screen);
bool throwAfterAttach = failingAttempts-- > 0;
return new CharacterManagementUiMountResources(
0x21000004u,
screen,
(_, _) => throwAfterAttach
? throw new InvalidOperationException(
"template failed after root attach")
: BuildRow(),
TestStrings());
});
coordinator.Tick();
Assert.Null(coordinator.Controller);
Assert.Empty(host.Children);
Assert.Single(screens);
AssertDetachedAndUnbound(screens[0]);
coordinator.Tick();
Assert.Null(coordinator.Controller);
Assert.Empty(host.Children);
Assert.Equal(2, screens.Count);
Assert.All(screens, AssertDetachedAndUnbound);
coordinator.Tick();
CharacterManagementUiController mounted = Assert.IsType<
CharacterManagementUiController>(coordinator.Controller);
Assert.Equal(3, screens.Count);
Assert.Single(host.Children);
Assert.Same(mounted.Root, host.Children[0]);
Assert.NotNull(Assert.IsType<UiButton>(screens[2].FindElement(
CharacterManagementUiController.EnterElementId)).OnClick);
coordinator.Tick();
Assert.Equal(3, screens.Count);
Assert.Single(host.Children);
coordinator.Dispose();
Assert.Empty(host.Children);
Assert.Null(coordinator.Controller);
Assert.All(screens, AssertDetachedAndUnbound);
}
[Fact]
public void DeleteConfirmation_IsCaseInsensitive_ThenWaitsThroughAckUntilFreshRoster()
{
@ -509,6 +569,17 @@ public sealed class CharacterManagementUiControllerTests
"Please Wait",
"Entering World");
private static void AssertDetachedAndUnbound(ImportedLayout screen)
{
Assert.Null(screen.Root.Parent);
Assert.Null(Assert.IsType<UiButton>(screen.FindElement(
CharacterManagementUiController.EnterElementId)).OnClick);
Assert.Null(Assert.IsType<UiButton>(screen.FindElement(
CharacterManagementUiController.DeleteElementId)).OnClick);
Assert.Null(Assert.IsType<UiButton>(screen.FindElement(
CharacterManagementUiController.RestoreElementId)).OnClick);
}
private static ImportedLayout BuildScreen(bool includePreview = false)
{
var root = new ElementInfo

View file

@ -542,6 +542,110 @@ public sealed class RetailDialogFactoryTests
Assert.True(factory.CloseDialog(later));
}
[Fact]
public void PriorityRequest_PreemptsOrdinaryRetryAndPreservesOrdinaryFifo()
{
var root = new UiRoot { Width = 800f, Height = 600f };
bool waitsAvailable = false;
using var factory = new RetailDialogFactory(root, type =>
type == RetailDialogType.Wait && !waitsAvailable
? null
: BuildDialogLayout(type));
uint failed = factory.MakeWait("failed ordinary");
uint later = factory.MakeWait("later ordinary");
uint priority = factory.MakeDialog(
Priority(RetailDialogData.Message("priority")));
Assert.Equal(1, factory.ActiveCount);
Assert.Equal(1, factory.RetryCount);
Assert.Equal(1, factory.PendingCount);
Assert.Equal("priority", MessageFromAnyDialog(root.Modal!));
Assert.True(factory.CloseDialog(priority));
Assert.Equal(0, factory.ActiveCount);
Assert.Equal(1, factory.RetryCount);
Assert.Equal(1, factory.PendingCount);
waitsAvailable = true;
factory.Tick();
Assert.Equal("failed ordinary", MessageFromAnyDialog(root.Modal!));
Assert.True(factory.CloseDialog(failed));
Assert.Equal("later ordinary", MessageFromAnyDialog(root.Modal!));
Assert.True(factory.CloseDialog(later));
}
[Fact]
public void FailedPriority_RetriesAheadOfRestoredActiveAndQueuedDialog()
{
var root = new UiRoot { Width = 800f, Height = 600f };
bool priorityAvailable = false;
using var factory = new RetailDialogFactory(root, type =>
type == RetailDialogType.Message && !priorityAvailable
? null
: BuildDialogLayout(type));
uint active = factory.MakeWait("active");
uint queued = factory.MakeWait("queued");
uint priority = factory.MakeDialog(
Priority(RetailDialogData.Message("priority")));
Assert.Equal(1, factory.ActiveCount);
Assert.Equal(1, factory.RetryCount);
Assert.Equal(1, factory.PendingCount);
Assert.Equal("active", MessageFromAnyDialog(root.Modal!));
priorityAvailable = true;
factory.Tick();
Assert.Equal(1, factory.ActiveCount);
Assert.Equal(0, factory.RetryCount);
Assert.Equal(2, factory.PendingCount);
Assert.Equal("priority", MessageFromAnyDialog(root.Modal!));
Assert.True(factory.CloseDialog(priority));
Assert.Equal("active", MessageFromAnyDialog(root.Modal!));
Assert.Equal(1, factory.PendingCount);
Assert.True(factory.CloseDialog(active));
Assert.Equal("queued", MessageFromAnyDialog(root.Modal!));
Assert.True(factory.CloseDialog(queued));
}
[Fact]
public void MultipleRetries_NewestPriorityFirstThenOlderPriorityThenOrdinaryFifo()
{
var root = new UiRoot { Width = 800f, Height = 600f };
bool available = false;
using var factory = new RetailDialogFactory(root, type =>
!available ? null : BuildDialogLayout(type));
uint ordinary = factory.MakeWait("ordinary");
uint later = factory.MakeWait("later");
uint olderPriority = factory.MakeDialog(
Priority(RetailDialogData.Message("older priority")));
uint newerPriority = factory.MakeDialog(Priority(
RetailDialogData.ConfirmationTextInput("newer priority")));
Assert.Equal(0, factory.ActiveCount);
Assert.Equal(3, factory.RetryCount);
Assert.Equal(1, factory.PendingCount);
available = true;
factory.Tick();
Assert.Equal(1, factory.ActiveCount);
Assert.Equal(2, factory.RetryCount);
Assert.Equal("newer priority", MessageFromAnyDialog(root.Modal!));
Assert.True(factory.CloseDialog(newerPriority));
Assert.Equal("older priority", MessageFromAnyDialog(root.Modal!));
Assert.True(factory.CloseDialog(olderPriority));
Assert.Equal("ordinary", MessageFromAnyDialog(root.Modal!));
Assert.True(factory.CloseDialog(ordinary));
Assert.Equal("later", MessageFromAnyDialog(root.Modal!));
Assert.True(factory.CloseDialog(later));
}
private static RetailDialogFactory CreateFactory(
UiRoot root,
List<ImportedLayout> layouts)
@ -575,6 +679,11 @@ public sealed class RetailDialogFactoryTests
.LinesProvider()
.Select(static line => line.Text));
private static RetailDialogData Priority(RetailDialogData data) =>
data.Set(RetailDialogProperty.Priority, true)
.Set(RetailDialogProperty.QueueKey,
RetailDialogFactory.DefaultQueueKey);
internal static ImportedLayout BuildDialogLayout(RetailDialogType type)
{
uint rootId = RetailDialogFactory.RootElementId(type);