Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
45cf382
docs(efc-464): record plan base-drift addendum against the real execu…
drmoisan Aug 27, 2026
0023359
docs(efc-464): record binding upstream constraints from merged 484 an…
drmoisan Aug 27, 2026
6d614b7
docs(efc-464): capture Phase 0 baseline and toolchain bootstrap
drmoisan Aug 27, 2026
5d567e7
fix(efc-464): remove dead EfcItemController surface (#459, #464 D, #4…
drmoisan Aug 28, 2026
345f684
fix(efc-464): remove dead EfcViewer surface and EfcViewer3 orphans (#…
drmoisan Aug 28, 2026
b8a7200
fix(efc-464): remove the dead conversation-expanded handler (#461)
drmoisan Aug 28, 2026
abb825d
fix(efc-464): correct the WebView2 incognito argument at both live si…
drmoisan Aug 28, 2026
2592467
Merge remote-tracking branch 'origin/epic/quickfiler-bug-family-integ…
drmoisan Aug 28, 2026
23e953c
chore(efc-464): record post-merge toolchain verification before Phase 5
drmoisan Aug 28, 2026
ace2247
fix(efc-464): RC1 post-teardown null-state contract and RC2 timer dis…
drmoisan Aug 28, 2026
35c6bb1
fix(efc-464): contain the #464 fault boundaries and preserve the reth…
drmoisan Aug 28, 2026
93629f6
fix(efc-464): correct the #465 action paths (RC8, RC9, RC7)
drmoisan Aug 28, 2026
28d244e
fix(efc-464): restore the Alt+F and Alt+M menu mnemonics (#467)
drmoisan Aug 28, 2026
2b99d7e
docs(efc-464): record Phase 9 scope, ownership and contract verification
drmoisan Aug 28, 2026
bdf804f
test(efc-464): record the Phase 10 final QC toolchain loop
drmoisan Aug 28, 2026
7c9b02e
docs(efc-464): complete acceptance criteria and record the Phase 11 h…
drmoisan Aug 28, 2026
7c5303c
docs(efc-464): capture feature-review audit artifacts
drmoisan Aug 28, 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
317 changes: 317 additions & 0 deletions QuickFiler.Test/Controllers/EfcFormControllerTests.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using System;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using FluentAssertions;
using Microsoft.VisualStudio.TestTools.UnitTesting;
Expand Down Expand Up @@ -156,6 +158,312 @@ public async Task Issue439BindBreadcrumbRowsAsync_SubmitsArchiveRootToRealRouter
globals.VerifyGet(value => value.Ol, Times.Once);
}

// RC1 (#460 A/C, #464 A, #465 A): the theme/dark-mode accessors must be readable on the
// all-fields-null post-Cleanup() state that CreateMinimalController reproduces.
[TestMethod]
public void FormDarkMode_OnAllFieldsNullController_ReturnsFalseAndDoesNotThrow()
{
// Arrange
var controller = CreateMinimalController();

// Act
Func<bool> act = () => controller.DarkMode;

// Assert
act.Should().NotThrow("DarkMode must be readable after Cleanup nulled _globals");
controller.DarkMode.Should().BeFalse("the _darkMode backing field defaults to false");
}

[TestMethod]
public void FormActiveTheme_OnAllFieldsNullController_ReturnsBackingFieldAndDoesNotThrow()
{
// Arrange
var controller = CreateMinimalController();
var backingField = (string)GetPrivateField(controller, "_activeTheme");

// Act
Func<string> act = () => controller.ActiveTheme;

// Assert
act.Should().NotThrow("ActiveTheme must be readable after Cleanup nulled _themes");
controller.ActiveTheme.Should().Be(backingField, "the getter returns the field");
}

[TestMethod]
public void FormLoadTheme_OnAllFieldsNullController_DoesNotThrow()
{
// Arrange
var controller = CreateMinimalController();

// Act
Func<string> act = () => controller.LoadTheme();

// Assert
act.Should().NotThrow("LoadTheme must not apply a theme when _themes is null");
}

// RC1: Cleanup() must be idempotent and must not double-invoke the parent callback.
[TestMethod]
public void FormCleanup_CalledTwice_DoesNotThrow()
{
// Arrange
var controller = CreateMinimalController();

// Act
Action act = () =>
{
controller.Cleanup();
controller.Cleanup();
};

// Assert
act.Should().NotThrow("Cleanup must be idempotent on a partial controller");
}

[TestMethod]
public void FormCleanup_InvokesParentCleanupExactlyOnce()
{
// Arrange
var controller = CreateMinimalController();
var parentCleanup = new Mock<Action>();
SetPrivateField(controller, "_parentCleanup", parentCleanup.Object);

// Act
controller.Cleanup();
controller.Cleanup();

// Assert
parentCleanup.Verify(c => c(), Times.Once(), "the parent runs exactly once");
GetPrivateField(controller, "_parentCleanup")
.Should()
.BeNull("the field is cleared before the captured local is invoked");
}

// #464 B (RC3). Each extracted boundary member must log through the sink and contain the
// fault rather than rethrow it out of an async void rim. Per D9 each [DataRow] is a
// distinct named result.
[DataTestMethod]
[DataRow("ButtonCancelClickAsync")]
[DataRow("ButtonOkClickAsync")]
[DataRow("ButtonRefreshClickAsync")]
[DataRow("ButtonCreateClickAsync")]
[DataRow("ButtonDeleteClickAsync")]
public async Task AsyncVoidBoundary_WhenFaulted_LogsOnceAndDoesNotThrow(string memberName)
{
// Arrange: the all-fields-null state is the fault injection. The first statement each
// member reaches dereferences the null _formViewer or awaits its null UiSyncContext.
SynchronizationContext previousContext = SynchronizationContext.Current;
SynchronizationContext.SetSynchronizationContext(null);
try
{
var controller = CreateMinimalController();
var sinkCallCount = 0;
controller.BoundaryErrorSink = (message, exception) => sinkCallCount++;
MethodInfo member = typeof(EfcFormController).GetMethod(
memberName,
BindingFlags.Instance | BindingFlags.NonPublic
);
member.Should().NotBeNull($"{memberName} must exist as an internal async Task");

// Act
Func<Task> act = () => (Task)member.Invoke(controller, Array.Empty<object>());

// Assert
await act.Should()
.NotThrowAsync($"{memberName} must contain the fault, not rethrow it");
sinkCallCount.Should().Be(1, $"{memberName} must report the fault exactly once");
}
finally
{
SynchronizationContext.SetSynchronizationContext(previousContext);
}
}

[TestMethod]
public void BoundaryErrorSink_DefaultDelegate_InvokesWithoutThrowing()
{
// Arrange: the default delegate is left in place, so this covers its body.
var controller = CreateMinimalController();

// Act
Action act = () =>
controller.BoundaryErrorSink("smoke", new InvalidOperationException());

// Assert
act.Should().NotThrow("the default sink must be safe on a released controller");
}

// #464 C (RC3). Both call sites discard the result, so a fault becomes an unobserved
// faulted Task. The uninitialized EfcViewer clears the null-viewer early return while
// running no constructor; _dataModel stays null so the first collaborator call faults.
[TestMethod]
public async Task PopulateFolderCombobox_WhenDataModelFaults_LogsOnceAndDoesNotFault()
{
// Arrange
var controller = CreateMinimalController();
var viewer = (EfcViewer)
System.Runtime.Serialization.FormatterServices.GetUninitializedObject(
typeof(EfcViewer)
);
SetPrivateField(controller, "_formViewer", viewer);
var sinkCallCount = 0;
controller.BoundaryErrorSink = (message, exception) => sinkCallCount++;

// Act
Func<Task> act = () => controller.PopulateFolderCombobox();

// Assert
await act.Should()
.NotThrowAsync(
"a fire-and-forget call site cannot observe a faulted Task, so the method must"
+ " contain its own fault instead of returning one"
);
sinkCallCount
.Should()
.Be(
1,
"the contained fault must be reported through the boundary error sink exactly"
+ " once"
);
}

// #465 B (RC8). The extracted pure matching helper is exercised with no EfcViewer and no
// controller, which is what makes the relocated read testable at all.
[TestMethod]
public void MatchesForSearchText_WithRepresentativeInput_ReturnsExpectedMatches()
{
// Arrange
const string searchText = "north";
var expected = new[] { @"Clients\North", @"Clients\Northeast", @"Archive\Northwind" };
string observedArgument = null;
Func<string, string[]> findMatches = text =>
{
observedArgument = text;
return text == searchText ? expected : Array.Empty<string>();
};

// Act / Assert
EfcFormController
.MatchesForSearchText(findMatches, searchText)
.Should()
.Equal(expected, "the helper returns the delegate result verbatim");
EfcFormController
.MatchesForSearchText(null, searchText)
.Should()
.BeEmpty("a null delegate yields an empty array rather than a null dereference");
EfcFormController
.MatchesForSearchText(findMatches, null)
.Should()
.BeEmpty("a null search text yields no matches");
observedArgument
.Should()
.BeEmpty("a null search text is passed through to the delegate as an empty string");
}

// #465 C (RC9). WithTrashRow is the pure half of the delete gesture.
[TestMethod]
public void WithTrashRow_AppliedTwice_YieldsExactlyOneTrashRow()
{
// Arrange
var rows = new[] { @"Clients\North", @"Clients\South" };

// Act
string[] once = EfcFormController.WithTrashRow(rows);
string[] twice = EfcFormController.WithTrashRow(once);

// Assert
twice
.Where(row => row == EfcFormController.TrashRowText)
.Should()
.HaveCount(1, "the trash row must not accumulate on a repeated delete gesture");
twice
.Should()
.BeSameAs(once, "an input already carrying the trash row is returned as-is");
twice.Should().Contain(rows, "the presented rows survive the gesture");
}

// #465 C (RC9). Drives the criterion's literal instrument, ActionDeleteAsync itself.
// Injecting any non-null SynchronizationContext satisfies the SynchronizationContextAwaiter
// null guard, so the awaited UI-thread marshal completes headlessly; the uninitialized
// EfcViewer runs no constructor so it has no handle, no controls and no message pump; and
// with _router left null BindFolderRows returns at its guard without touching the
// breadcrumb host, while ApplyDeleteGesture has already assigned _folderRows.
[TestMethod]
public async Task ActionDeleteAsync_AwaitedTwice_LeavesExactlyOneTrashRowInFolderRows()
{
// Arrange
var rows = new[] { @"Clients\North", @"Clients\South" };
var controller = CreateMinimalController();
var viewer = (EfcViewer)
System.Runtime.Serialization.FormatterServices.GetUninitializedObject(
typeof(EfcViewer)
);
SetPrivateField(viewer, "_context", new SynchronizationContext());
SetPrivateField(controller, "_formViewer", viewer);
SetPrivateField(controller, "_folderRows", rows);

// Act
await controller.ActionDeleteAsync();
await controller.ActionDeleteAsync();

// Assert
var folderRows = (string[])GetPrivateField(controller, "_folderRows");
folderRows
.Where(row => row == EfcFormController.TrashRowText)
.Should()
.HaveCount(1, "a repeated delete gesture must not accumulate trash rows");
folderRows.Should().Contain(rows, "both original rows survive the gesture");
}

// #465 D (RC7). The producers emit a four-character banner prefix
// (BreadcrumbRowBuilder.BannerPrefix), so a three-equals row is shorter than the prefix.
[TestMethod]
public void IsBannerRow_ClassifiesByTheFourCharacterPrefix()
{
// Act / Assert
EfcFormController
.IsBannerRow("===")
.Should()
.BeFalse("a three-equals row is shorter than the producer prefix");
EfcFormController
.IsBannerRow("====")
.Should()
.BeTrue("a four-equals row carries the producer prefix");
BreadcrumbRowBuilder
.BannerPrefix.Should()
.Be("====", "IsBannerRow classifies by the prefix the row producers emit");
}

[TestMethod]
public void IsBannerRow_NullOrShortRow_ReturnsFalseWithoutThrowing()
{
// Act / Assert
foreach (var row in new[] { null, string.Empty, "=" })
{
Func<bool> act = () => EfcFormController.IsBannerRow(row);
act.Should().NotThrow($"a row of length {row?.Length ?? -1} must not throw");
act().Should().BeFalse("a row shorter than the prefix is not a banner");
}
}

// Both EFC classification sites must agree. The creation path is IsValidSelection, which is
// IsSelectableFolder; the filing path is the guard expression ActionOkAsync composes. The
// guard is reproduced rather than driven, because ActionOkAsync shows a MessageBox.
[TestMethod]
public void IsSelectableFolder_AndIsBannerRow_ClassifyThreeAndFourEqualsRowsIdentically()
{
// Act / Assert
foreach (var row in new[] { "===", "====" })
{
bool creationPath = EfcFormController.IsSelectableFolder(row);
bool filingPath =
!EfcFormController.IsBannerRow(row)
&& EfcSelectionGuard.IsValidFilingSelection(row);
creationPath.Should().Be(filingPath, $"both sites must classify {row} alike");
creationPath.Should().BeFalse($"{row} is rejected at both sites");
}
}

private static void SetPrivateField(object target, string fieldName, object value)
{
var field = target
Expand All @@ -164,5 +472,14 @@ private static void SetPrivateField(object target, string fieldName, object valu
field.Should().NotBeNull($"{fieldName} must remain available for this headless seam");
field.SetValue(target, value);
}

private static object GetPrivateField(object target, string fieldName)
{
var field = target
.GetType()
.GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic);
field.Should().NotBeNull($"{fieldName} must remain available for this headless seam");
return field.GetValue(target);
}
}
}
Loading
Loading