Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
da9902b
docs(796): prepare QuickFiler folder drop-down close-ordering bug fix
drmoisan Sep 7, 2026
c7ae69f
Merge remote-tracking branch 'origin/main' into bug/quickfiler-folder…
drmoisan Sep 7, 2026
336e30d
docs(796): re-anchor plan diff gates after merging main, fix stale ci…
drmoisan Sep 7, 2026
0dfcb40
instrument(796): add AC6 debug logging at the two named close-orderin…
drmoisan Sep 7, 2026
ec674e0
docs(796): record the Phase 1 commit evidence and plan check-offs
drmoisan Sep 7, 2026
d78ae7f
Merge remote-tracking branch 'origin/main' into bug/quickfiler-folder…
drmoisan Sep 7, 2026
bcaad72
docs(796): record the manual close-ordering observation and re-anchor…
drmoisan Sep 7, 2026
8d36d48
docs(796): verify the manual close-ordering observation and derive th…
drmoisan Sep 7, 2026
1f57bc1
fix(796): gate the deactivation cancel loop on a self-inflicted-popup…
drmoisan Sep 7, 2026
b357544
fix(796): suppress the uncommitted-reason cancel while a commit is in…
drmoisan Sep 7, 2026
6af8c6c
fix(796): give the search box dismissal ownership of only the popups …
drmoisan Sep 7, 2026
d4ec2d6
test(796): add the AC1 open guard and the AC5 row-set-refresh regress…
drmoisan Sep 7, 2026
5b8e0bf
Merge remote-tracking branch 'origin/main' into bug/quickfiler-folder…
drmoisan Sep 7, 2026
8e427fe
test(796): re-pin the search-dismissal suite to AC4 and record the Ph…
drmoisan Sep 7, 2026
e4c53ec
docs(796): add feature review artifacts and correct the AC3 mark
drmoisan Sep 7, 2026
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
57 changes: 57 additions & 0 deletions QuickFiler.Test/Controllers/QfcFormControllerDeactivateTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -167,11 +167,18 @@ public void FormDeactivated_NoWebView2Focus_DoesNotPark()
/// <summary>
/// Every item's breadcrumb selector is cancelled, so no open <c>ToolStripDropDown</c> — and
/// therefore no WinForms modal-menu-mode message filter — can outlive deactivation.
/// <para>
/// Issue #796 (AC2) made that cancel conditional: it now happens on a GENUINE deactivation
/// and not on one this form's own breadcrumb popup caused. The condition is stated
/// explicitly in the Arrange block below rather than left to the mock default, so the #677
/// contract this test pins remains visible as a contract about the genuine case.
/// </para>
/// </summary>
[TestMethod]
public void FormDeactivated_CancelsSelectorOnEveryItemController()
{
// Arrange
_mockFormViewer.SetupGet(x => x.IsDeactivationSelfInflictedByOwnPopup).Returns(false);
var first = new Mock<IQfcItemController>();
var second = new Mock<IQfcItemController>();
QfcFormController controller = CreateController();
Expand Down Expand Up @@ -244,5 +251,55 @@ public void FormDeactivated_ItemCancelThrows_DoesNotPropagateAndContinues()
act.Should().NotThrow();
second.Verify(x => x.CancelBreadcrumbSelector(), Times.Once());
}

/// <summary>
/// Issue #796 (AC6). Scenario: the pure deactivation formatter is called with a fixed
/// argument tuple. Expected outcome: the returned line carries all three discriminating
/// field labels and the supplied group count, so the Phase 2 transcript can identify a
/// self-inflicted deactivation without inference.
/// </summary>
[TestMethod]
public void FormatDeactivationDiagnostics_IncludesEveryDiscriminatingField()
{
// Arrange
const int GroupCount = 3;

// Act
string line = QfcFormController.FormatDeactivationDiagnostics(
webView2Focused: true,
activeFormIsNull: true,
groupCount: GroupCount
);

// Assert
line.Should().Contain("WebView2Focused=");
line.Should().Contain("ActiveFormNull=");
line.Should().Contain("Groups=");
line.Should().Contain(GroupCount.ToString());
}

/// <summary>
/// Issue #796 (AC2). Scenario: the form loses activation because a breadcrumb popup this
/// form owns took it. Expected outcome: no item controller's selector is cancelled, so the
/// popup the gesture just opened survives its own opening.
/// </summary>
[TestMethod]
public void FormDeactivated_SelfInflictedByOwnPopup_DoesNotCancelAnySelector()
{
// Arrange
_mockFormViewer.SetupGet(x => x.IsDeactivationSelfInflictedByOwnPopup).Returns(true);
var first = new Mock<IQfcItemController>();
var second = new Mock<IQfcItemController>();
QfcFormController controller = CreateController();
InjectGroups(controller, first, second);
controller.RegisterFormEventHandlers();

// Act
_mockFormViewer.Raise(x => x.FormDeactivated += null, EventArgs.Empty);

// Assert
first.Verify(x => x.CancelBreadcrumbSelector(), Times.Never());
second.Verify(x => x.CancelBreadcrumbSelector(), Times.Never());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -69,13 +69,20 @@ public void TextBoxSearchKeyDown_EscapeWhileDropDownClosed_RoutesNoIntentAndLeav
/// <summary>
/// The search textbox losing focus while the drop-down is open routes exactly one close
/// intent — the dismissal WinForms menu mode used to provide for a capturing popup.
/// <para>
/// Issue #796 (AC4) narrowed the condition rather than removing it: the leave dismisses only
/// a drop-down this search box itself opened. The Arrange therefore establishes that
/// search-driven ownership before the leave is raised. A mouse-driven open carries no
/// ownership and is deliberately no longer dismissed by this path.
/// </para>
/// </summary>
[TestMethod]
public void TextBoxSearchLeave_WhileDropDownOpen_RoutesExactlyOneCloseIntent()
{
// Arrange
Mock<IItemViewer> viewer = BuildViewer(isOpen: true);
HarnessController controller = BuildController(viewer);
QfcItemControllerTestSupport.SetField(controller, "_searchOwnedDismissal", true);

// Act
controller.TextBoxSearch_Leave(null, EventArgs.Empty);
Expand Down
102 changes: 102 additions & 0 deletions QuickFiler.Test/Controllers/QfcItemController.SearchLeaveLatchTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
using System;
using System.Collections.Generic;
using FluentAssertions;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using UtilitiesCS;

namespace QuickFiler.Controllers.Tests
{
/// <summary>
/// Issue #796 (AC4): the SearchOwnedDismissalLatch. <c>TextBoxSearch_Leave</c> previously took
/// dismissal ownership of the folder drop-down regardless of which gesture opened it, so a
/// mouse-driven open was dismissed by a leave the mouse gesture itself provoked. The latch
/// records that the open drop-down is one this search box opened, and the leave handler
/// dismisses only then.
/// <para>
/// Class name follows the convention of the sibling file
/// QuickFiler.Test/Controllers/QfcItemController.EventHandlersTests.cs, which declares
/// QfcItemController_EventHandlersTests. No window, no external process, no temporary file.
/// </para>
/// </summary>
[TestClass]
public class QfcItemController_SearchLeaveLatchTests
{
/// <summary>
/// Builds a folder-search handler whose <c>FindFolder</c> returns a fixed result, so the
/// search-driven open path can be driven without a live Outlook or COM host.
/// </summary>
private static Mock<IFolderSearchHandler> BuildFolderHandler(string[] matched)
{
Mock<IFolderSearchHandler> folderHandler = new Mock<IFolderSearchHandler>();
folderHandler
.Setup(f =>
f.FindFolder(
It.IsAny<string>(),
It.IsAny<object>(),
It.IsAny<bool>(),
It.IsAny<List<string>>(),
It.IsAny<bool>(),
It.IsAny<
IEnumerable<(string root, string excludedFolder, bool excludeChildren)>
>()
)
)
.Returns(matched);
return folderHandler;
}

/// <summary>
/// Issue #796 (AC4). Scenario: the drop-down is open because a mouse gesture opened it, and
/// the folder search box then loses focus. Expected outcome: the leave handler does not
/// dismiss the drop-down, because the search box never took dismissal ownership of it.
/// </summary>
[TestMethod]
public void SearchLeaveAfterMouseDrivenOpen_DoesNotCloseDropDown()
{
// Arrange — the drop-down is open, but no search-driven open path ever ran, which is
// exactly the state a mouse gesture on the collapsed breadcrumb produces.
Mock<IItemViewer> viewer = new Mock<IItemViewer>();
viewer.SetupGet(v => v.IsFolderDropDownOpen).Returns(true);
HarnessController controller = new HarnessController();
QfcItemControllerTestSupport.SetField(controller, "_itemViewer", viewer.Object);

// Act
controller.TextBoxSearch_Leave(null, EventArgs.Empty);

// Assert
viewer.Verify(v => v.SetFolderDroppedDown(false), Times.Never());
}

/// <summary>
/// Issue #796 (AC4), paired positive. Scenario: the drop-down is open because typing in the
/// folder search box opened it, and the search box then loses focus. Expected outcome: the
/// leave handler still dismisses the drop-down exactly once, so the issue #680 dismissal
/// responsibility for a non-capturing search-driven popup is preserved.
/// </summary>
[TestMethod]
public void SearchLeaveAfterSearchDrivenOpen_ClosesDropDown()
{
// Arrange
string[] matched = { @"\\A\one", @"\\A\two" };
Mock<IItemViewer> viewer = new Mock<IItemViewer>();
viewer.SetupGet(v => v.SearchText).Returns("query");
viewer.SetupGet(v => v.IsFolderDropDownOpen).Returns(true);
HarnessController controller = new HarnessController();
QfcItemControllerTestSupport.SetField(controller, "_itemViewer", viewer.Object);
QfcItemControllerTestSupport.SetField(
controller,
"_folderHandler",
BuildFolderHandler(matched).Object
);

// Act — the search-driven open path, then the leave it eventually provokes.
controller.TextBoxSearch_TextChanged(null, EventArgs.Empty);
controller.TextBoxSearch_Leave(null, EventArgs.Empty);

// Assert
viewer.Verify(v => v.PresentFolderSearchResults(matched), Times.Once());
viewer.Verify(v => v.SetFolderDroppedDown(false), Times.Once());
}
}
}
2 changes: 2 additions & 0 deletions QuickFiler.Test/QuickFiler.Test.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@
<Compile Include="Viewers\BreadcrumbDuplicateIdentityIntegrationTests.cs" />
<Compile Include="Viewers\BreadcrumbMessengerHubTests.cs" />
<Compile Include="Viewers\BreadcrumbPopupPlacementTests.cs" />
<Compile Include="Viewers\BreadcrumbDropDownCloseOrderingTests.cs" />
<Compile Include="Viewers\BreadcrumbDropDownHostTests.cs" />
<Compile Include="Viewers\BreadcrumbDropDownHostTests.Part2.cs" />
<Compile Include="Viewers\BreadcrumbDropDownHostTests.Part3.cs" />
Expand Down Expand Up @@ -155,6 +156,7 @@
<Compile Include="Controllers\QfcFormControllerSeamTests.cs" />
<Compile Include="Controllers\QfcFormControllerCleanupTests.cs" />
<Compile Include="Controllers\QfcFormControllerDeactivateTests.cs" />
<Compile Include="Controllers\QfcItemController.SearchLeaveLatchTests.cs" />
<Compile Include="Controllers\QfcItemController.CancelBreadcrumbSelectorTests.cs" />
<Compile Include="Controllers\QfcFormKeyHandlerTests.cs" />
<Compile Include="Controllers\QfcHighConfidencePreFilterTests.cs" />
Expand Down
Loading
Loading