Skip to content

Commit 59cc17f

Browse files
committed
fix: raise OnViewClosed when a view's life cycle does not end normally
MVCManager.ShowAsync raised OnViewClosed after the layer branch returned, so the event was skipped whenever that branch threw. Cancellation is the common case -- it is caught and swallowed a few lines below -- but every layer branch tears the view down in its own finally block, so the view is hidden either way. What subscribers were left with was an OnViewShowed that never resolved. UpscalingController is the visible casualty: it counts open UIs and lowers the render scale while any of them is up, so a single cancelled show leaves currentUIOpened above zero and the scale pinned at the UI-open value for the rest of the session. SidebarController and ChatMainSharedAreaController track state off the same pair of events. Moving the invocation into a finally block pairs every OnViewShowed with exactly one OnViewClosed. Nothing can now raise it twice: the call was moved rather than added, and the early return for a controller that is not hidden sits ahead of the try, where neither event fires. MVCManagerShould covers the four ways ShowAsync can end -- the life cycle completing, being cancelled, failing with anything else, and the early return -- asserting the exact event sequence in each rather than a count, so a duplicate fails the same way a missing one does. Events are only exercised on the Fullscreen layer; the finally block is in ShowAsync, shared by all four.
1 parent c276373 commit 59cc17f

2 files changed

Lines changed: 98 additions & 2 deletions

File tree

Explorer/Assets/DCL/Infrastructure/MVC/Manager/MVCManager.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -128,14 +128,17 @@ public async UniTask ShowAsync<TView, TInputData>(ShowCommand<TView, TInputData>
128128
await ShowOverlayAsync(command, controller, ct);
129129
break;
130130
}
131-
132-
OnViewClosed?.Invoke(controller);
133131
}
134132
catch (OperationCanceledException)
135133
{
136134
// TODO (Vit) : handle revert of command. Proposal - extend WizardCommands interface with Revert method and call it in case of cancellation.
137135
ReportHub.LogWarning(ReportCategory.MVC, $"ShowAsync was cancelled for {controller.GetType()}");
138136
}
137+
finally
138+
{
139+
// Raised here so that every OnViewShowed is followed by exactly one OnViewClosed
140+
OnViewClosed?.Invoke(controller);
141+
}
139142
}
140143

141144
private async UniTask ShowOverlayAsync<TView, TInputData>(ShowCommand<TView, TInputData> command, IController controller, CancellationToken ct)

Explorer/Assets/DCL/Infrastructure/MVC/Tests/MVCManagerShould.cs

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ public class MVCManagerShould
1414
private IWindowsStackManager windowsStackManager;
1515
private MVCManager mvcManager;
1616
private IPopupCloserView popupCloserView;
17+
private List<IController> showed;
18+
private List<IController> closed;
1719

1820
[SetUp]
1921
public void Setup()
@@ -22,6 +24,12 @@ public void Setup()
2224
windowsStackManager.PushFullscreen(Arg.Any<IController>()).Returns(new FullscreenPushInfo(new List<(IController, int)>(), new CanvasOrdering(), new UniTaskCompletionSource()));
2325
popupCloserView = Substitute.For<IPopupCloserView>();
2426
mvcManager = new MVCManager(windowsStackManager, new CancellationTokenSource(), popupCloserView);
27+
28+
showed = new List<IController>();
29+
closed = new List<IController>();
30+
31+
mvcManager.OnViewShowed += showed.Add;
32+
mvcManager.OnViewClosed += closed.Add;
2533
}
2634

2735
[Test]
@@ -84,6 +92,91 @@ public async Task Show(CanvasOrdering.SortingLayer layer)
8492
break;
8593
}
8694
}
95+
96+
[Test]
97+
public async Task RaiseViewClosedWhenTheViewLifeCycleIsCancelled()
98+
{
99+
IController<ITestView, TestInputData> controller = Substitute.For<IController<ITestView, TestInputData>>();
100+
controller.Layer.Returns(CanvasOrdering.SortingLayer.Fullscreen);
101+
102+
var viewLifeCycle = new UniTaskCompletionSource();
103+
104+
controller.LaunchViewLifeCycleAsync(Arg.Any<CanvasOrdering>(), Arg.Any<TestInputData>(), Arg.Any<CancellationToken>())
105+
.Returns(viewLifeCycle.Task);
106+
107+
mvcManager.RegisterController(controller);
108+
109+
UniTask show = mvcManager.ShowAsync(new ShowCommand<ITestView, TestInputData>());
110+
111+
viewLifeCycle.TrySetCanceled();
112+
113+
await show;
114+
115+
// Guards the test itself: without this the assertion below would also hold for a run that
116+
// never entered the show path at all.
117+
Assert.That(showed, Is.EqualTo(new[] { controller }));
118+
119+
// ShowFullScreenAsync tears the view down in a finally block and pops it off the stack, so a
120+
// cancelled life cycle still ends with the view hidden. Subscribers are told about every other
121+
// way it can end, and they have no other signal to pair with OnViewShowed.
122+
Assert.That(closed, Is.EqualTo(new[] { controller }));
123+
}
124+
125+
[Test]
126+
public async Task RaiseViewClosedOnceWhenTheViewLifeCycleCompletes()
127+
{
128+
IController<ITestView, TestInputData> controller = Substitute.For<IController<ITestView, TestInputData>>();
129+
controller.Layer.Returns(CanvasOrdering.SortingLayer.Fullscreen);
130+
131+
mvcManager.RegisterController(controller);
132+
133+
await mvcManager.ShowAsync(new ShowCommand<ITestView, TestInputData>());
134+
135+
// Exact sequences rather than counts: the show path leaves the view hidden the ordinary way
136+
// too, and the pairing is one to one, so a second OnViewClosed here would be a duplicate.
137+
Assert.That(showed, Is.EqualTo(new[] { controller }));
138+
Assert.That(closed, Is.EqualTo(new[] { controller }));
139+
}
140+
141+
[Test]
142+
public void RaiseViewClosedWhenTheViewLifeCycleFails()
143+
{
144+
IController<ITestView, TestInputData> controller = Substitute.For<IController<ITestView, TestInputData>>();
145+
controller.Layer.Returns(CanvasOrdering.SortingLayer.Fullscreen);
146+
147+
var viewLifeCycle = new UniTaskCompletionSource();
148+
149+
controller.LaunchViewLifeCycleAsync(Arg.Any<CanvasOrdering>(), Arg.Any<TestInputData>(), Arg.Any<CancellationToken>())
150+
.Returns(viewLifeCycle.Task);
151+
152+
mvcManager.RegisterController(controller);
153+
154+
UniTask show = mvcManager.ShowAsync(new ShowCommand<ITestView, TestInputData>());
155+
156+
viewLifeCycle.TrySetException(new InvalidOperationException());
157+
158+
// Only cancellation is swallowed, so this one travels out of ShowAsync — and the view is torn
159+
// down on the way, which is what subscribers are told about.
160+
Assert.ThrowsAsync<InvalidOperationException>(async () => await show);
161+
Assert.That(closed, Is.EqualTo(new[] { controller }));
162+
}
163+
164+
[Test]
165+
public async Task RaiseNoViewEventsForAControllerThatIsNotHidden()
166+
{
167+
IController<ITestView, TestInputData> controller = Substitute.For<IController<ITestView, TestInputData>>();
168+
controller.Layer.Returns(CanvasOrdering.SortingLayer.Fullscreen);
169+
controller.State.Returns(ControllerState.ViewFocused);
170+
171+
mvcManager.RegisterController(controller);
172+
173+
await mvcManager.ShowAsync(new ShowCommand<ITestView, TestInputData>());
174+
175+
// ShowAsync returns before showing anything in this case, so there is no session to report
176+
// either end of.
177+
Assert.That(showed, Is.Empty);
178+
Assert.That(closed, Is.Empty);
179+
}
87180
}
88181

89182
public class TestInputData { }

0 commit comments

Comments
 (0)