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
{