diff --git a/Directory.Packages.props b/Directory.Packages.props
index bc42be3347f..1d35ef2cad4 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -54,6 +54,7 @@
+
diff --git a/TUnit.CI.slnx b/TUnit.CI.slnx
index d5cd80e607a..a0688c90109 100644
--- a/TUnit.CI.slnx
+++ b/TUnit.CI.slnx
@@ -112,6 +112,7 @@
+
diff --git a/TUnit.Dev.slnx b/TUnit.Dev.slnx
index 87c935fb3c7..180772f47fb 100644
--- a/TUnit.Dev.slnx
+++ b/TUnit.Dev.slnx
@@ -66,6 +66,7 @@
+
diff --git a/TUnit.slnx b/TUnit.slnx
index ac3754c10a9..40c186ec05f 100644
--- a/TUnit.slnx
+++ b/TUnit.slnx
@@ -88,6 +88,7 @@
+
diff --git a/docs/docs/guides/html-report.md b/docs/docs/guides/html-report.md
index 16c02002948..88fc42e52c9 100644
--- a/docs/docs/guides/html-report.md
+++ b/docs/docs/guides/html-report.md
@@ -39,10 +39,20 @@ Running many test projects and want **one combined report instead of one per pro
### Custom Output Path
+When TUnit is the only HTML report provider, the existing option remains available:
+
```bash
dotnet run -- --report-html-filename my-custom-report.html
```
+When the `Microsoft.Testing.Extensions.HtmlReport` package is referenced, Microsoft owns the
+`--report-html` and `--report-html-filename` options. TUnit automatically switches to its
+namespaced filename option:
+
+```bash
+dotnet run -- --tunit-report-html-filename my-custom-report.html
+```
+
### Disable Report Generation
Set the environment variable:
@@ -57,7 +67,13 @@ For version-controlled project configuration, set `context.Settings.Reporting.Ht
### Deprecated: `--report-html` Flag
-The `--report-html` flag is deprecated since the report is now generated by default. Using it will show a deprecation warning but will not cause an error.
+Without `Microsoft.Testing.Extensions.HtmlReport`, TUnit continues to accept the deprecated
+`--report-html` flag for compatibility. TUnit's report is generated automatically, so the flag
+has no effect beyond displaying a deprecation warning. Use `TUNIT_DISABLE_HTML_REPORTER=true`
+when you need to disable TUnit's report generation.
+
+When `Microsoft.Testing.Extensions.HtmlReport` is referenced, TUnit does not register either
+legacy option; their behavior is provided by the Microsoft extension.
## GitHub Actions Integration
diff --git a/docs/docs/reference/command-line-flags.md b/docs/docs/reference/command-line-flags.md
index ef2d60f2ca9..25e88bab6a7 100644
--- a/docs/docs/reference/command-line-flags.md
+++ b/docs/docs/reference/command-line-flags.md
@@ -136,12 +136,19 @@ Please note that for the coverage and trx report, you need to install [additiona
File name prefix for the JSON report produced by --output-json.
--report-html
- (Deprecated) The HTML report is now generated by default.
+ Available when Microsoft.Testing.Extensions.HtmlReport is not referenced.
+ (Deprecated) The TUnit HTML report is now generated by default.
Disable it with TUNIT_DISABLE_HTML_REPORTER or set
context.Settings.Reporting.HtmlReportEnabled = false.
--report-html-filename
- Path for the HTML test report file
+ Available when Microsoft.Testing.Extensions.HtmlReport is not referenced.
+ Path for the TUnit HTML test report file
+ (default: TestResults/{'{AssemblyName}'}-report.html).
+
+ --tunit-report-html-filename
+ Available when Microsoft.Testing.Extensions.HtmlReport is referenced.
+ Path for the TUnit HTML test report file
(default: TestResults/{'{AssemblyName}'}-report.html).
--junit-output-path
diff --git a/src/TUnit.Engine/CommandLineProviders/HtmlReporterCommandProvider.cs b/src/TUnit.Engine/CommandLineProviders/HtmlReporterCommandProvider.cs
index 2f1b7fd7499..f76e3febe22 100644
--- a/src/TUnit.Engine/CommandLineProviders/HtmlReporterCommandProvider.cs
+++ b/src/TUnit.Engine/CommandLineProviders/HtmlReporterCommandProvider.cs
@@ -1,13 +1,19 @@
using Microsoft.Testing.Platform.CommandLine;
using Microsoft.Testing.Platform.Extensions;
using Microsoft.Testing.Platform.Extensions.CommandLine;
+using TUnit.Engine.Extensions;
namespace TUnit.Engine.CommandLineProviders;
-internal class HtmlReporterCommandProvider(IExtension extension) : ICommandLineOptionsProvider
+internal class HtmlReporterCommandProvider(IExtension extension, HtmlCliMode htmlCliMode = HtmlCliMode.Default) : ICommandLineOptionsProvider
{
public const string ReportHtml = "report-html";
public const string ReportHtmlFilename = "report-html-filename";
+ public const string TUnitReportHtmlFilename = "tunit-report-html-filename";
+
+ public string ReportHtmlFilenameOption => htmlCliMode == HtmlCliMode.Namespaced
+ ? TUnitReportHtmlFilename
+ : ReportHtmlFilename;
public Task IsEnabledAsync() => extension.IsEnabledAsync();
@@ -21,6 +27,18 @@ internal class HtmlReporterCommandProvider(IExtension extension) : ICommandLineO
public IReadOnlyCollection GetCommandLineOptions()
{
+ if (htmlCliMode == HtmlCliMode.Namespaced)
+ {
+ return
+ [
+ new CommandLineOption(
+ TUnitReportHtmlFilename,
+ "Path for the HTML test report file (default: TestResults/{AssemblyName}-report.html)",
+ ArgumentArity.ExactlyOne,
+ false),
+ ];
+ }
+
return
[
new CommandLineOption(
@@ -32,7 +50,7 @@ public IReadOnlyCollection GetCommandLineOptions()
ReportHtmlFilename,
"Path for the HTML test report file (default: TestResults/{AssemblyName}-report.html)",
ArgumentArity.ExactlyOne,
- false)
+ false),
];
}
@@ -40,7 +58,7 @@ public Task ValidateOptionArgumentsAsync(
CommandLineOption commandOption,
string[] arguments)
{
- if (commandOption.Name == ReportHtmlFilename && arguments.Length != 1)
+ if (commandOption.Name == ReportHtmlFilenameOption && arguments.Length != 1)
{
return ValidationResult.InvalidTask("A single output path must be provided for the HTML report");
}
diff --git a/src/TUnit.Engine/Extensions/HtmlCliMode.cs b/src/TUnit.Engine/Extensions/HtmlCliMode.cs
new file mode 100644
index 00000000000..5e160ca7bcc
--- /dev/null
+++ b/src/TUnit.Engine/Extensions/HtmlCliMode.cs
@@ -0,0 +1,7 @@
+namespace TUnit.Engine.Extensions;
+
+public enum HtmlCliMode
+{
+ Default,
+ Namespaced,
+}
diff --git a/src/TUnit.Engine/Extensions/TestApplicationBuilderExtensions.cs b/src/TUnit.Engine/Extensions/TestApplicationBuilderExtensions.cs
index 2a69e0b2901..cc181f856f6 100644
--- a/src/TUnit.Engine/Extensions/TestApplicationBuilderExtensions.cs
+++ b/src/TUnit.Engine/Extensions/TestApplicationBuilderExtensions.cs
@@ -15,7 +15,10 @@ namespace TUnit.Engine.Extensions;
public static class TestApplicationBuilderExtensions
{
- public static void AddTUnit(this ITestApplicationBuilder testApplicationBuilder)
+ public static void AddTUnit(this ITestApplicationBuilder testApplicationBuilder) =>
+ testApplicationBuilder.AddTUnit(HtmlCliMode.Default);
+
+ public static void AddTUnit(this ITestApplicationBuilder testApplicationBuilder, HtmlCliMode htmlCliMode)
{
TUnitExtension extension = new();
@@ -26,7 +29,7 @@ public static void AddTUnit(this ITestApplicationBuilder testApplicationBuilder)
var junitReporterCommandProvider = new JUnitReporterCommandProvider(extension);
var htmlReporter = new Reporters.Html.HtmlReporter(extension);
- var htmlReporterCommandProvider = new HtmlReporterCommandProvider(extension);
+ var htmlReporterCommandProvider = new HtmlReporterCommandProvider(extension, htmlCliMode);
htmlReporter.SetGitHubReporter(githubReporter);
@@ -80,7 +83,7 @@ public static void AddTUnit(this ITestApplicationBuilder testApplicationBuilder)
}
// Set results directory as specified by --results-directory,
- // so it can be used in the default output path if --report-html-filename is not provided
+ // so it can be used in the default output path if --junit-output-path is not provided
junitReporter.SetResultsDirectory(serviceProvider.GetRequiredService().GetTestResultDirectory());
return junitReporter;
@@ -96,14 +99,14 @@ public static void AddTUnit(this ITestApplicationBuilder testApplicationBuilder)
var commandLineOptions = serviceProvider.GetRequiredService();
// Deprecated: --report-html is now a no-op (reporter is always-on)
- if (commandLineOptions.IsOptionSet(HtmlReporterCommandProvider.ReportHtml))
+ if (htmlCliMode == HtmlCliMode.Default && commandLineOptions.IsOptionSet(HtmlReporterCommandProvider.ReportHtml))
{
Console.WriteLine("Warning: --report-html is deprecated. The HTML report is now generated by default. Use TUNIT_DISABLE_HTML_REPORTER=true to disable.");
}
- if (commandLineOptions.TryGetOptionArgumentList(HtmlReporterCommandProvider.ReportHtmlFilename, out var pathArgs))
+ if (commandLineOptions.TryGetOptionArgumentList(htmlReporterCommandProvider.ReportHtmlFilenameOption, out var pathArgs))
{
- htmlReporter.SetOutputPath(Helpers.PathValidator.ValidateAndNormalizePath(pathArgs[0], HtmlReporterCommandProvider.ReportHtmlFilename));
+ htmlReporter.SetOutputPath(Helpers.PathValidator.ValidateAndNormalizePath(pathArgs[0], htmlReporterCommandProvider.ReportHtmlFilenameOption));
}
// Inject the application-level message bus so PublishArtifactAsync works in
@@ -111,7 +114,7 @@ public static void AddTUnit(this ITestApplicationBuilder testApplicationBuilder)
htmlReporter.SetMessageBus(serviceProvider.GetMessageBus());
// Set results directory as specified by --results-directory,
- // so it can be used in the default output path if --report-html-filename is not provided
+ // so it can be used in the default output path if the HTML report filename option is not provided
htmlReporter.SetResultsDirectory(serviceProvider.GetRequiredService().GetTestResultDirectory());
return htmlReporter;
diff --git a/src/TUnit.Engine/Framework/TestingPlatformBuilderHook.cs b/src/TUnit.Engine/Framework/TestingPlatformBuilderHook.cs
index 083a88346eb..975f1818969 100644
--- a/src/TUnit.Engine/Framework/TestingPlatformBuilderHook.cs
+++ b/src/TUnit.Engine/Framework/TestingPlatformBuilderHook.cs
@@ -10,3 +10,11 @@ public static void AddExtensions(
string[] _) =>
testApplicationBuilder.AddTUnit();
}
+
+public static class NamespacedHtmlReportTestingPlatformBuilderHook
+{
+ public static void AddExtensions(
+ ITestApplicationBuilder testApplicationBuilder,
+ string[] _) =>
+ testApplicationBuilder.AddTUnit(HtmlCliMode.Namespaced);
+}
diff --git a/src/TUnit.Engine/TUnit.Engine.props b/src/TUnit.Engine/TUnit.Engine.props
index 296dc59a7ac..eae66ae0cfc 100644
--- a/src/TUnit.Engine/TUnit.Engine.props
+++ b/src/TUnit.Engine/TUnit.Engine.props
@@ -44,4 +44,15 @@
+
+
+
+
+ TUnit.Engine.Framework.NamespacedHtmlReportTestingPlatformBuilderHook
+
+
+
+
diff --git a/tests/TUnit.Engine.Tests/HtmlReportCliTests.cs b/tests/TUnit.Engine.Tests/HtmlReportCliTests.cs
new file mode 100644
index 00000000000..4a1839a641e
--- /dev/null
+++ b/tests/TUnit.Engine.Tests/HtmlReportCliTests.cs
@@ -0,0 +1,132 @@
+using CliWrap;
+using CliWrap.Buffered;
+using Shouldly;
+using TUnit.Engine.Tests.Enums;
+
+namespace TUnit.Engine.Tests;
+
+public class HtmlReportCliTests(TestMode testMode) : InvokableTestBase(testMode)
+{
+ [Test]
+ public async Task Combined_Html_Report_Packages_Accept_TUnit_Namespaced_Option()
+ {
+ await RunTestsWithFilter(
+ "/*/*/BasicTests/SynchronousTest",
+ [
+ result => result.ResultSummary.Outcome.ShouldBe("Completed"),
+ result => result.ResultSummary.Counters.Total.ShouldBe(1),
+ result => result.ResultSummary.Counters.Passed.ShouldBe(1),
+ result => result.ResultSummary.Counters.Failed.ShouldBe(0),
+ ],
+ new RunOptions()
+ .WithArgument("--tunit-report-html-filename")
+ .WithArgument("tunit-report.html"));
+ }
+}
+
+public class DefaultHtmlReportCliTests
+{
+ [Test]
+ [Arguments(false)]
+ [Arguments(true)]
+ public async Task TUnit_Only_Project_Accepts_Legacy_Options(bool reflection)
+ {
+ var tempDirectory = CreateTempDirectory();
+ var reportPath = Path.Combine(tempDirectory, "custom-report.html");
+
+ try
+ {
+ var result = await RunTUnitOnlyProject(
+ tempDirectory,
+ reflection,
+ "--report-html",
+ "--report-html-filename",
+ reportPath);
+
+ AssertSuccessful(result);
+ File.Exists(reportPath).ShouldBeTrue();
+ }
+ finally
+ {
+ Directory.Delete(tempDirectory, recursive: true);
+ }
+ }
+
+ [Test]
+ [Arguments(false)]
+ [Arguments(true)]
+ public async Task TUnit_Only_Project_Uses_Default_Output_Path(bool reflection)
+ {
+ var tempDirectory = CreateTempDirectory();
+
+ try
+ {
+ var result = await RunTUnitOnlyProject(tempDirectory, reflection);
+
+ AssertSuccessful(result);
+
+ var reports = Directory.GetFiles(tempDirectory, "*-report.html");
+ reports.Length.ShouldBe(1);
+ Path.GetFileName(reports[0]).ShouldStartWith("TUnit.TestProject.HtmlReportDefaults-");
+ }
+ finally
+ {
+ Directory.Delete(tempDirectory, recursive: true);
+ }
+ }
+
+ private static string CreateTempDirectory()
+ {
+ var path = Path.Combine(Path.GetTempPath(), $"tunit-html-defaults-{Guid.NewGuid():N}");
+ Directory.CreateDirectory(path);
+ return path;
+ }
+
+ private static Task RunTUnitOnlyProject(
+ string resultsDirectory,
+ bool reflection,
+ params string[] htmlArguments)
+ {
+ var testProject = Sourcy.DotNet.Projects.TUnit_TestProject_HtmlReportDefaults;
+ List arguments =
+ [
+ "run",
+ "--no-build",
+ "--project", testProject.FullName,
+ "--framework", "net10.0",
+ "--configuration", "Release",
+ "--",
+ "--treenode-filter", "/*/*/DefaultHtmlReportTests/Pass",
+ "--results-directory", resultsDirectory,
+ ..htmlArguments,
+ ];
+
+ if (reflection)
+ {
+ arguments.Add("--reflection");
+ }
+
+ return Cli.Wrap("dotnet")
+ .WithArguments(arguments)
+ .WithWorkingDirectory(testProject.DirectoryName!)
+ .WithEnvironmentVariables(new Dictionary
+ {
+ ["TUNIT_DISABLE_HTML_REPORTER"] = "false",
+ ["TUNIT_DISABLE_JSON_REPORT"] = "true",
+ ["TUNIT_DISABLE_ARTIFACT_UPLOAD"] = "true",
+ })
+ .WithValidation(CommandResultValidation.None)
+ .ExecuteBufferedAsync();
+ }
+
+ private static void AssertSuccessful(BufferedCommandResult result)
+ {
+ result.ExitCode.ShouldBe(0, $"""
+ Standard output:
+ {result.StandardOutput}
+
+ Standard error:
+ {result.StandardError}
+ """);
+ }
+}
diff --git a/tests/TUnit.Engine.Tests/HtmlReporterTests.cs b/tests/TUnit.Engine.Tests/HtmlReporterTests.cs
index 05123ed29cc..f47fc4619ab 100644
--- a/tests/TUnit.Engine.Tests/HtmlReporterTests.cs
+++ b/tests/TUnit.Engine.Tests/HtmlReporterTests.cs
@@ -3,11 +3,16 @@
using System.IO.Compression;
using System.Text;
using System.Text.RegularExpressions;
+using Microsoft.Testing.Platform.CommandLine;
+using Microsoft.Testing.Platform.Extensions;
+using Microsoft.Testing.Platform.Extensions.CommandLine;
using Microsoft.Testing.Platform.Extensions.Messages;
using Microsoft.Testing.Platform.TestHost;
using Shouldly;
using TUnit.Core;
using TUnit.Core.Enums;
+using TUnit.Engine.CommandLineProviders;
+using TUnit.Engine.Extensions;
using TUnit.Engine.Reporters;
using TUnit.Engine.Reporters.Html;
@@ -15,6 +20,65 @@ namespace TUnit.Engine.Tests;
public class HtmlReporterTests
{
+ [Test]
+ public void CommandProvider_DefaultMode_Exposes_Legacy_Options()
+ {
+ var provider = new HtmlReporterCommandProvider(new MockExtension());
+
+ var options = provider.GetCommandLineOptions();
+
+ options.Count.ShouldBe(2);
+ options.Single(x => x.Name == "report-html").Arity.ShouldBe(ArgumentArity.Zero);
+ options.Single(x => x.Name == "report-html-filename").Arity.ShouldBe(ArgumentArity.ExactlyOne);
+ options.Select(x => x.Name).ShouldNotContain("tunit-report-html-filename");
+ }
+
+ [Test]
+ public void CommandProvider_NamespacedMode_Exposes_Only_TUnitSpecific_Filename_Option()
+ {
+ var provider = new HtmlReporterCommandProvider(new MockExtension(), HtmlCliMode.Namespaced);
+
+ var options = provider.GetCommandLineOptions();
+
+ options.Count.ShouldBe(1);
+ options.Single().Name.ShouldBe("tunit-report-html-filename");
+ options.Single().Arity.ShouldBe(ArgumentArity.ExactlyOne);
+ options.Select(x => x.Name).ShouldNotContain("report-html");
+ options.Select(x => x.Name).ShouldNotContain("report-html-filename");
+ }
+
+ [Test]
+ public async Task CommandProvider_Accepts_One_Filename_Argument()
+ {
+ foreach (var mode in Enum.GetValues())
+ {
+ var provider = new HtmlReporterCommandProvider(new MockExtension(), mode);
+ var option = provider.GetCommandLineOptions().Single(x => x.Arity == ArgumentArity.ExactlyOne);
+
+ var result = await provider.ValidateOptionArgumentsAsync(option, ["report.html"]);
+
+ result.IsValid.ShouldBeTrue();
+ }
+ }
+
+ [Test]
+ [Arguments(0)]
+ [Arguments(2)]
+ public async Task CommandProvider_Rejects_Filename_Argument_Count_Other_Than_One(int argumentCount)
+ {
+ foreach (var mode in Enum.GetValues())
+ {
+ var provider = new HtmlReporterCommandProvider(new MockExtension(), mode);
+ var option = provider.GetCommandLineOptions().Single(x => x.Arity == ArgumentArity.ExactlyOne);
+ var arguments = Enumerable.Repeat("report.html", argumentCount).ToArray();
+
+ var result = await provider.ValidateOptionArgumentsAsync(option, arguments);
+
+ result.IsValid.ShouldBeFalse();
+ result.ErrorMessage.ShouldBe("A single output path must be provided for the HTML report");
+ }
+ }
+
[Test]
public void HtmlReporter_Implements_IDataProducer()
{
diff --git a/tests/TUnit.Engine.Tests/ReportingSettingsTests.cs b/tests/TUnit.Engine.Tests/ReportingSettingsTests.cs
index 1ca19d4ecfe..9d2f34a94a6 100644
--- a/tests/TUnit.Engine.Tests/ReportingSettingsTests.cs
+++ b/tests/TUnit.Engine.Tests/ReportingSettingsTests.cs
@@ -15,7 +15,7 @@ public async Task Discovery_Hook_Can_Disable_Reporting(CancellationToken cancell
try
{
var options = new RunOptions()
- .WithArgument("--report-html-filename")
+ .WithArgument("--tunit-report-html-filename")
.WithArgument(reportPath)
.WithEnvironmentVariable("TUNIT_DISABLE_HTML_REPORTER", "false")
.WithEnvironmentVariable("TUNIT_DISABLE_JSON_REPORT", "false")
diff --git a/tests/TUnit.Engine.Tests/TUnit.Engine.Tests.csproj b/tests/TUnit.Engine.Tests/TUnit.Engine.Tests.csproj
index a16423b43f1..eae576b9440 100644
--- a/tests/TUnit.Engine.Tests/TUnit.Engine.Tests.csproj
+++ b/tests/TUnit.Engine.Tests/TUnit.Engine.Tests.csproj
@@ -28,6 +28,7 @@
+
diff --git a/tests/TUnit.TestProject.HtmlReportDefaults/DefaultHtmlReportTests.cs b/tests/TUnit.TestProject.HtmlReportDefaults/DefaultHtmlReportTests.cs
new file mode 100644
index 00000000000..f577e26add0
--- /dev/null
+++ b/tests/TUnit.TestProject.HtmlReportDefaults/DefaultHtmlReportTests.cs
@@ -0,0 +1,9 @@
+namespace TUnit.TestProject.HtmlReportDefaults;
+
+public class DefaultHtmlReportTests
+{
+ [Test]
+ public void Pass()
+ {
+ }
+}
diff --git a/tests/TUnit.TestProject.HtmlReportDefaults/TUnit.TestProject.HtmlReportDefaults.csproj b/tests/TUnit.TestProject.HtmlReportDefaults/TUnit.TestProject.HtmlReportDefaults.csproj
new file mode 100644
index 00000000000..9f7b9c9c358
--- /dev/null
+++ b/tests/TUnit.TestProject.HtmlReportDefaults/TUnit.TestProject.HtmlReportDefaults.csproj
@@ -0,0 +1,15 @@
+
+
+
+
+
+ net10.0
+
+
+
+
+
+
+
+
+
diff --git a/tests/TUnit.TestProject/TUnit.TestProject.csproj b/tests/TUnit.TestProject/TUnit.TestProject.csproj
index bb2f9a84854..a18335e32a7 100644
--- a/tests/TUnit.TestProject/TUnit.TestProject.csproj
+++ b/tests/TUnit.TestProject/TUnit.TestProject.csproj
@@ -30,6 +30,7 @@
+