diff --git a/TUnit.Engine.Tests/HtmlReporterTests.cs b/TUnit.Engine.Tests/HtmlReporterTests.cs index e5faada495..0006979d1f 100644 --- a/TUnit.Engine.Tests/HtmlReporterTests.cs +++ b/TUnit.Engine.Tests/HtmlReporterTests.cs @@ -262,6 +262,43 @@ public void GenerateHtml_RoundTrips_ClassTimeline_CustomProperty_OnTest() embedded.ShouldContain("\"value\":\"FullExecution\""); } + [Test] + public void ExtractTestResult_SortsTestMetadataProperty_Into_Categories_And_CustomProperties() + { + // Regression: Microsoft.Testing.Platform's VSTestBridge convention emits categories as + // TestMetadataProperty(name, "") — the category name lives in Key, Value is empty. + // Traits/custom properties use the (key, value) form with a non-empty Value. + // Earlier code keyed off IsNullOrEmpty(Key), which inverted both branches and silently + // misclassified every category as a custom property — leaving the HTML report's + // category-pill UI permanently empty. + var node = new TestNode + { + Uid = new TestNodeUid("extract-1"), + DisplayName = "Test", + Properties = new PropertyBag( + PassedTestNodeStateProperty.CachedInstance, + new TestMethodIdentifierProperty( + @namespace: "TestNamespace", + assemblyFullName: "TestAssembly", + typeName: "SampleTests", + methodName: "Test", + parameterTypeFullNames: [], + returnTypeFullName: "System.Void", + methodArity: 0), + new TestMetadataProperty("Async", string.Empty), + new TestMetadataProperty("Integration", string.Empty), + new TestMetadataProperty("Owner", "TeamA")) + }; + + var result = HtmlReporter.ExtractTestResult("extract-1", node, traceId: null, spanId: null, retryAttempt: 0, additionalTraceIds: null); + + result.Categories.ShouldBe(["Async", "Integration"], ignoreOrder: true); + result.CustomProperties.ShouldNotBeNull(); + result.CustomProperties!.Length.ShouldBe(1); + result.CustomProperties[0].Key.ShouldBe("Owner"); + result.CustomProperties[0].Value.ShouldBe("TeamA"); + } + private static ReportTestResult CreateTestResultWithStartTime(string displayName, string? startTime) => new() { Id = displayName, diff --git a/TUnit.Engine/Reporters/Html/HtmlReporter.cs b/TUnit.Engine/Reporters/Html/HtmlReporter.cs index ecb724b4ea..922bff23e8 100644 --- a/TUnit.Engine/Reporters/Html/HtmlReporter.cs +++ b/TUnit.Engine/Reporters/Html/HtmlReporter.cs @@ -484,7 +484,7 @@ private static DateTimeOffset ParseStartTimeForSort(string? raw) : DateTimeOffset.MaxValue; } - private static ReportTestResult ExtractTestResult(string testId, TestNode testNode, string? traceId, string? spanId, int retryAttempt, string[]? additionalTraceIds) + internal static ReportTestResult ExtractTestResult(string testId, TestNode testNode, string? traceId, string? spanId, int retryAttempt, string[]? additionalTraceIds) { IProperty? stateProperty = null; TestMethodIdentifierProperty? testMethodIdentifier = null; @@ -518,10 +518,13 @@ private static ReportTestResult ExtractTestResult(string testId, TestNode testNo stdErr = TruncateOutput(e.StandardError); break; case TestMetadataProperty meta: - if (string.IsNullOrEmpty(meta.Key)) + // MTP convention (matches Microsoft.Testing.Extensions.VSTestBridge): categories are + // emitted as TestMetadataProperty(category, "") — name in Key, empty Value. Traits/ + // custom properties use (key, value) with a non-empty Value. + if (string.IsNullOrEmpty(meta.Value)) { categories ??= []; - categories.Add(meta.Value); + categories.Add(meta.Key); } else {