Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
196 changes: 196 additions & 0 deletions QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue792Tests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using FluentAssertions;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using QuickFiler.Controllers;
using QuickFiler.Viewers;
using UtilitiesCS;
using UtilitiesCS.OutlookObjects.Folder;

namespace QuickFiler.Test.Controllers
{
/// <summary>
/// Regression tests for issue #792 on <see cref="BreadcrumbBridgeRouter"/>: a failed
/// CoreWebView2 initialization discards the stashed document, clears the selection and
/// navigates the error banner; a later initialization must not replay the stale stash. The
/// arrangement mirrors <c>BreadcrumbBridgeRouterQueueTests</c>. No timers, sleeps or temp files.
/// </summary>
[TestClass]
public sealed class BreadcrumbBridgeRouterIssue792Tests
{
private const string LeafPath = "Inbox\\Projects\\Alpha";
private const string RowSelectedPayload = "{\"type\":\"rowSelected\",\"rowId\":\"row-0\"}";
private const string ErrorBannerToken = "Folder list unavailable";
private const string StaleRowToken = "Alpha";

private Mock<IFolderHierarchyProvider> _provider;
private Mock<IBreadcrumbWebHost> _host;
private bool _initialized;
private List<string> _navigated;
private List<string> _posted;
private BreadcrumbBridgeRouter _router;

[TestInitialize]
public void Setup()
{
_provider = new Mock<IFolderHierarchyProvider>();
_host = new Mock<IBreadcrumbWebHost>();
_initialized = false;
_navigated = new List<string>();
_posted = new List<string>();
_host.SetupGet(h => h.IsCoreInitialized).Returns(() => _initialized);
_host
.Setup(h => h.NavigateToString(It.IsAny<string>()))
.Callback<string>(html => _navigated.Add(html));
_host
.Setup(h => h.PostMessageJson(It.IsAny<string>()))
.Callback<string>(json => _posted.Add(json));
_provider
.Setup(p =>
p.ResolveLeafKeyAsync(It.IsAny<string>(), It.IsAny<CancellationToken>())
)
.ReturnsAsync(
(string path, CancellationToken ct) =>
new FolderTreeNodeKey("store-1", "entry", path)
);
_provider
.Setup(p =>
p.GetAncestorChainAsync(
It.IsAny<FolderTreeNodeKey>(),
It.IsAny<CancellationToken>()
)
)
.ReturnsAsync(
new[] { Segment("Inbox", "Inbox", true), Segment(LeafPath, "Alpha", true) }
);
_router = new BreadcrumbBridgeRouter(
_provider.Object,
_host.Object,
new BreadcrumbMessageCodec(),
new BreadcrumbHtmlRenderer(),
new BreadcrumbOutboundQueue(_host.Object)
);
}

private static FolderBreadcrumbSegment Segment(string path, string name, bool hasChildren)
{
return new FolderBreadcrumbSegment(
new FolderTreeNodeKey("store-1", "entry", path),
name,
path,
hasChildren
);
}

private void Bind()
{
_router
.BindRowsAsync(
new[] { LeafPath },
Enumerable.Empty<FolderScore>(),
CancellationToken.None
)
.GetAwaiter()
.GetResult();
}

private void Inbound(string json)
{
_router.ProcessInboundAsync(json).GetAwaiter().GetResult();
}

/// <summary>
/// AC-U1/AC-U2: with a stashed document and a live selection, the failure entry point
/// navigates exactly one document (the banner, not the stale folder document) and clears
/// the selection, notifying subscribers with null. Before the fix the declaration-only body
/// navigates nothing, so the first assertion fails.
/// </summary>
[TestMethod]
public void NotifyInitializationFailed_ClearsThePendingDocumentAndNavigatesTheErrorBanner()
{
// Arrange: the uninitialized host stashes the bound document; a selection exists.
Bind();
_navigated.Should().BeEmpty("the bound document is stashed while uninitialized");
Inbound(RowSelectedPayload);
_router.SelectedFolderPath.Should().NotBeNull("a selection must exist to be cleared");
string observed = "sentinel";
_router.SelectedFolderPathChanged += (s, path) => observed = path;

// Act
_router.NotifyInitializationFailed(new InvalidOperationException("boom"));

// Assert
_navigated
.Should()
.ContainSingle("the failure must navigate exactly one document, the error banner")
.Which.Should()
.Contain(ErrorBannerToken, "the navigated document must be the error banner")
.And.NotContain(StaleRowToken, "the stashed folder document must be discarded");
_router
.SelectedFolderPath.Should()
.BeNull("a failed initialization clears the selection");
observed.Should().BeNull("clearing a selection must notify subscribers with null");
}

/// <summary>
/// AC-U2: after the failure, a later initialization must find no stash to replay. Before
/// the fix the stash survives, so the only navigation is the stale folder document, which
/// contains the leaf name: the count assertion passes and the content assertion fails.
/// </summary>
[TestMethod]
public void NotifyInitializationFailed_LeavesNoStashForALaterInitialization()
{
// Arrange
Bind();
Inbound(RowSelectedPayload);

// Act
_router.NotifyInitializationFailed(new InvalidOperationException("boom"));
_initialized = true;
_router.NotifyCoreInitialized();

// Assert
_navigated.Should().HaveCount(1, "only the error banner may be navigated");
_navigated[0]
.Should()
.Contain(ErrorBannerToken, "the single navigation must be the error banner")
.And.NotContain(StaleRowToken, "the stale stash must not be replayed");
}

/// <summary>Control: a null failure is rejected at the boundary.</summary>
[TestMethod]
public void NotifyInitializationFailed_WithNullFailure_Throws()
{
// Arrange
Action act = () => _router.NotifyInitializationFailed(null);

// Act and Assert
act.Should()
.Throw<ArgumentNullException>("the failure is required")
.Which.ParamName.Should()
.Be("failure");
}

/// <summary>Retained-behaviour control: a stash is still delivered on initialization.</summary>
[TestMethod]
public void NotifyCoreInitialized_AfterAnEarlierStash_StillNavigatesIt()
{
// Arrange
Bind();
_initialized = true;

// Act
_router.NotifyCoreInitialized();

// Assert
_navigated
.Should()
.ContainSingle("the stashed document is delivered once on initialization")
.Which.Should()
.Contain(StaleRowToken, "the delivered document is the bound folder document");
}
}
}
113 changes: 113 additions & 0 deletions QuickFiler.Test/Controllers/BreadcrumbOutboundQueueIssue792Tests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
using System;
using FluentAssertions;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using QuickFiler.Controllers;
using QuickFiler.Viewers;
using UtilitiesCS.OutlookObjects.Folder;

namespace QuickFiler.Test.Controllers
{
/// <summary>
/// Regression tests for issue #792 on <see cref="BreadcrumbOutboundQueue.DiscardPending"/>
/// (AC-U7): when CoreWebView2 initialization fails, buffered payloads are discarded rather
/// than drained, because the host cannot post to a core that does not exist. No timers,
/// sleeps or temp files are used.
/// </summary>
[TestClass]
public sealed class BreadcrumbOutboundQueueIssue792Tests
{
private const string RenderPayload = "{\"type\":\"render\"}";
private const string FocusPayload = "{\"type\":\"focusSearch\"}";
private const string ThemePayload = "{\"type\":\"theme\"}";

private static Mock<IBreadcrumbWebHost> CreateUninitializedHost()
{
var host = new Mock<IBreadcrumbWebHost>();
host.SetupGet(h => h.IsCoreInitialized).Returns(false);
return host;
}

/// <summary>
/// Three buffered payloads are discarded: the method reports three, nothing remains
/// pending and nothing was posted. Before the fix the stub returns zero and keeps them.
/// </summary>
[TestMethod]
public void DiscardPending_ReturnsTheDiscardedCountAndLeavesZeroPending()
{
// Arrange
var host = CreateUninitializedHost();
var queue = new BreadcrumbOutboundQueue(host.Object);
queue.PostOrQueue(RenderPayload);
queue.PostOrQueue(FocusPayload);
queue.PostOrQueue(ThemePayload);
queue.PendingCount.Should().Be(3, "the uninitialized host must buffer every payload");

// Act
int discarded = queue.DiscardPending();

// Assert
discarded.Should().Be(3, "the discarded count must equal the number buffered");
queue.PendingCount.Should().Be(0, "a discard must leave nothing pending");
host.Verify(
h => h.PostMessageJson(It.IsAny<string>()),
Times.Never,
"a discard must not post to a core that does not exist"
);
}

/// <summary>Control: discarding an empty buffer reports zero and posts nothing.</summary>
[TestMethod]
public void DiscardPending_OnAnEmptyQueue_ReturnsZero()
{
// Arrange
var host = CreateUninitializedHost();
var queue = new BreadcrumbOutboundQueue(host.Object);

// Act
int discarded = queue.DiscardPending();

// Assert
discarded.Should().Be(0, "an empty buffer has nothing to discard");
queue.PendingCount.Should().Be(0, "an empty buffer stays empty");
host.Verify(h => h.PostMessageJson(It.IsAny<string>()), Times.Never);
}

/// <summary>
/// AC-U7 through the router: the failure entry point discards the router's own outbound
/// queue without posting. Before the fix the declaration-only body leaves both payloads
/// pending.
/// </summary>
[TestMethod]
public void NotifyInitializationFailed_DiscardsTheOutboundQueueWithoutPosting()
{
// Arrange
var host = CreateUninitializedHost();
var queue = new BreadcrumbOutboundQueue(host.Object);
var provider = new Mock<IFolderHierarchyProvider>(MockBehavior.Loose);
var router = new BreadcrumbBridgeRouter(
provider.Object,
host.Object,
new BreadcrumbMessageCodec(),
new BreadcrumbHtmlRenderer(),
queue
);
queue.PostOrQueue(RenderPayload);
queue.PostOrQueue(FocusPayload);
queue.PendingCount.Should().Be(2, "the uninitialized host must buffer both payloads");

// Act
router.NotifyInitializationFailed(new InvalidOperationException("boom"));

// Assert
queue
.PendingCount.Should()
.Be(0, "a failed initialization must discard the buffered payloads");
host.Verify(
h => h.PostMessageJson(It.IsAny<string>()),
Times.Never,
"discarded payloads must never be posted"
);
}
}
}
Loading
Loading