diff --git a/src/LogExpert.Tests/LogExpert.Tests.csproj b/src/LogExpert.Tests/LogExpert.Tests.csproj
index d8bfb785..d55c5437 100644
--- a/src/LogExpert.Tests/LogExpert.Tests.csproj
+++ b/src/LogExpert.Tests/LogExpert.Tests.csproj
@@ -29,6 +29,8 @@
+
+
diff --git a/src/LogExpert.Tests/Services/TabControllerTests.cs b/src/LogExpert.Tests/Services/TabControllerTests.cs
index bcd23ac9..c55f5054 100644
--- a/src/LogExpert.Tests/Services/TabControllerTests.cs
+++ b/src/LogExpert.Tests/Services/TabControllerTests.cs
@@ -1,8 +1,14 @@
using System.Runtime.Versioning;
+using LogExpert.Core.Config;
+using LogExpert.Core.Entities;
+using LogExpert.Core.Interfaces;
using LogExpert.UI.Controls.LogWindow;
+using LogExpert.UI.Interface;
using LogExpert.UI.Services.TabControllerService;
+using Moq;
+
using NUnit.Framework;
using WeifenLuo.WinFormsUI.Docking;
@@ -11,11 +17,8 @@ namespace LogExpert.Tests.Services;
///
/// Unit tests for TabController.
-/// Note: Many tests are limited because LogWindow is a complex WinForms control that cannot be easily mocked or
-/// subclassed. Tests that require actual LogWindow instances would need to be run as integration tests with full UI
-/// infrastructure.
-/// These tests focus on the core TabController functionality that can be tested without instantiating LogWindow
-/// objects.
+/// Most tests focus on behavior that does not require LogWindow instances.
+/// Tests that verify DockPanel tab order create real LogWindow instances through CreateLogWindow.
///
[TestFixture]
[SupportedOSPlatform("windows")]
@@ -36,7 +39,10 @@ public void Setup ()
_dockPanel = new DockPanel
{
Dock = DockStyle.Fill,
- DocumentStyle = DocumentStyle.DockingMdi
+ // Match LogExpert's document style; DockingMdi required test form to be an MDI container.
+ // Using DockingWindow both matches production behavior and allows the test to create real tabs without setting up an artificial MDI container.
+ DocumentStyle = DocumentStyle.DockingWindow,
+ Theme = new VS2015LightTheme()
};
_testForm.Controls.Add(_dockPanel);
_testForm.Show(); // Must show form for DockPanel to work
@@ -191,9 +197,53 @@ public void GetAllWindowsFromDockPanel_ReturnsReadOnlyList ()
Assert.That(result, Is.InstanceOf>());
}
+ [Test]
+ public void GetAllWindowsFromDockPanel_ReturnsDisplayedWindowsInTabOrder ()
+ {
+ // Arrange
+ using var firstWindow = CreateLogWindow("first.log");
+ using var secondWindow = CreateLogWindow("second.log");
+ using var thirdWindow = CreateLogWindow("third.log");
+
+ _tabController.AddWindow(firstWindow, "first.log");
+ _tabController.AddWindow(secondWindow, "second.log");
+ _tabController.AddWindow(thirdWindow, "third.log");
+
+ // Act
+ var result = _tabController.GetAllWindowsFromDockPanel();
+
+ // Assert
+ Assert.That(result, Is.EqualTo(new[] { firstWindow, secondWindow, thirdWindow }));
+ }
+
#endregion
- #region GetAllWindows Tests
+ #region Helpers
+
+ private static LogWindow CreateLogWindow (string fileName)
+ {
+ var coordinatorMock = new Mock();
+ _ = coordinatorMock.Setup(coordinator => coordinator.ResolveHighlightGroup(It.IsAny(), It.IsAny())).Returns(new HighlightGroup());
+ _ = coordinatorMock.SetupGet(coordinator => coordinator.SearchParams).Returns(new SearchParams());
+
+ var configManagerMock = new Mock();
+ _ = configManagerMock.SetupGet(configManager => configManager.Settings).Returns(new Settings());
+
+ var pluginRegistryMock = new Mock();
+ _ = pluginRegistryMock.SetupGet(pluginRegistry => pluginRegistry.RegisteredColumnizers).Returns([new DefaultLogfileColumnizer()]);
+
+ return new LogWindow(
+ coordinatorMock.Object,
+ fileName,
+ isTempFile: false,
+ forcePersistenceLoading: false,
+ configManagerMock.Object,
+ pluginRegistryMock.Object);
+ }
+
+ #endregion
+
+ #region GetAllWindows Tests
[Test]
public void GetAllWindows_WhenEmpty_ReturnsEmptyList ()
@@ -315,10 +365,8 @@ public void AddWindow_WhenNotInitialized_ThrowsInvalidOperationException ()
// Arrange
using var controller = new TabController();
- // Create a mock-like object that's not null to avoid ArgumentNullException
- // We need to test that the "not initialized" check happens
- // Unfortunately, LogWindow cannot be instantiated without its dependencies
- // So we can only verify the ArgumentNullException is thrown first for null
+ // CreateLogWindow can now provide a non-null LogWindow when needed.
+ // This test verifies that null argument validation happens before the initialization check.
var ex = Assert.Throws(() => controller.AddWindow(null, "Test"));
Assert.That(ex.ParamName, Is.EqualTo("window"));
}
diff --git a/src/LogExpert.UI/Services/TabControllerService/TabController.cs b/src/LogExpert.UI/Services/TabControllerService/TabController.cs
index b91a1a24..4bc9a023 100644
--- a/src/LogExpert.UI/Services/TabControllerService/TabController.cs
+++ b/src/LogExpert.UI/Services/TabControllerService/TabController.cs
@@ -461,12 +461,29 @@ protected virtual void Dispose (bool disposing)
/// Read-only list of all LogWindows in the DockPanel
public IReadOnlyList GetAllWindowsFromDockPanel ()
{
- return !_initialized || _dockPanel == null
- ? []
- : _dockPanel.Panes
- .SelectMany(pane => pane.DisplayingContents.OfType())
- .ToList()
- .AsReadOnly();
+ if (!_initialized || _dockPanel == null)
+ {
+ return [];
+ }
+
+ var windows = new List();
+
+ foreach (DockPane pane in _dockPanel.Panes)
+ {
+ var displayingContents = pane.DisplayingContents;
+
+ // Use 'for' instead of 'foreach': DisplayingContents exposes displayed tabs through Count and its indexer.
+ // 'foreach' uses the inherited ReadOnlyCollection enumerator and does not return displayed tabs.
+ for (int index = 0; index < displayingContents.Count; index++)
+ {
+ if (displayingContents[index] is LogWindow logWindow)
+ {
+ windows.Add(logWindow);
+ }
+ }
+ }
+
+ return windows.AsReadOnly();
}
#endregion