Add Microsoft.Testing.Extensions.HtmlReport extension#8283
Conversation
Closes #5754. New MTP extension `Microsoft.Testing.Extensions.HtmlReport` that produces a single, self-contained HTML test report at the end of a test session. CLI options (mirroring TrxReport conventions): --report-html enable the HTML report --report-html-filename override the file name (must end with `.html`, no path component) The report is published as a SessionFileArtifact so other extensions and dotnet test can pick it up. UX - Single file, no CDN: all CSS/JS/data are inlined. Easy to attach to PR comments, e-mail, archive as CI artifact. - Native CSS variables, automatic light/dark theme via `prefers-color-scheme`. - Summary cards, free text search, outcome filter pills (failed first by default), sortable columns. - Group by: None / Class / Duration / Trait <key> (one option per distinct trait key in the report). - Duration buckets: <10 ms, 10-100 ms, 100 ms-1 s, 1-10 s, 10-60 s, >=1 min. - Expandable per-row detail panel with error message, stack trace, standard output, standard error. - Pagination (200 rows / page) for large runs. - UI state (filter / sort / search / grouping) persisted in sessionStorage. Handling of duplicate UIDs Some test frameworks emit multiple terminal-state results that share the same `TestNode.Uid` (parameterized rows, in-process retries, broken UID generators). The consumer never deduplicates: every observation is preserved, and the engine annotates each row with `attemptIndex` / `attemptOf` so the UI can display a `#N of M` badge on the affected rows. Security The embedded JSON island is built with a hand-rolled HTML-safe JSON encoder that escapes `<`, `>`, `&`, `'`, `\u2028`, `\u2029` as `\uXXXX`, so test-controlled content (display names, stack traces, stdout / stderr) cannot break out of the `<script type= "application/json">` block. The renderer uses `textContent` only. A regression test verifies that hostile content such as `</script><img onerror=alert(1)>` never appears literally in the produced HTML. Performance - No reflection-based JSON serialization (works on netstandard2.0 without depending on System.Text.Json). - Per-test stdout / stderr (32 KiB) and stack traces (32 KiB) are truncated with a UI-visible suffix to keep huge runs usable. - Browser-side pagination at 200 rows / page by default. Future hook (live mode) The page exposes `window.mtpRender(newReport)`. A future OTel / Server-Sent Events extension can stream deltas and call this function to refresh the page in place without reloading. Repo wiring - TestFx.slnx: project registered under `/src/1 - Platform and Extensions/`. - Microsoft.Testing.Platform.csproj: `InternalsVisibleTo` added. - HelpInfoAllExtensionsTests / MSBuild.KnownExtensionRegistration: expectations updated for the new `--report-html` options and the new auto-registered builder hook. - 21 new unit tests covering CLI validation, JSON shape, traits, duplicate-UID handling, output truncation and HTML/JS injection. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Adds a new Microsoft.Testing.Platform HTML report extension that can be auto-registered through MSBuild, exposes --report-html CLI options, generates a self-contained HTML session artifact, and adds unit/acceptance coverage for the new package.
Changes:
- Introduces
Microsoft.Testing.Extensions.HtmlReportwith CLI registration, report generation, embedded HTML template, resources, packaging, and public API declarations. - Registers the extension in the solution, platform internals, MSBuild hook tests, and all-extension help/info acceptance expectations.
- Adds unit tests for filename validation, JSON/report generation, duplicate UIDs, traits, truncation, and injection escaping.
Show a summary per file
| File | Description |
|---|---|
| TestFx.slnx | Registers the new HtmlReport project. |
| test/UnitTests/Microsoft.Testing.Extensions.UnitTests/Microsoft.Testing.Extensions.UnitTests.csproj | Adds test project reference to HtmlReport. |
| test/UnitTests/Microsoft.Testing.Extensions.UnitTests/HtmlReportGeneratorCommandLineTests.cs | Adds CLI validation tests. |
| test/UnitTests/Microsoft.Testing.Extensions.UnitTests/HtmlReportEngineTests.cs | Adds report engine/unit generation tests. |
| test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/MSBuild.KnownExtensionRegistration.cs | Adds MSBuild auto-registration assertions. |
| test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/HelpInfoAllExtensionsTests.cs | Updates help/info expectations and asset package references. |
| src/Platform/Microsoft.Testing.Platform/Microsoft.Testing.Platform.csproj | Grants internals access to HtmlReport. |
| src/Platform/Microsoft.Testing.Extensions.HtmlReport/TestingPlatformBuilderHook.cs | Adds MSBuild builder hook. |
| src/Platform/Microsoft.Testing.Extensions.HtmlReport/Templates/report-template.html | Adds embedded HTML/CSS/JS report UI. |
| src/Platform/Microsoft.Testing.Extensions.HtmlReport/Resources/ExtensionResources.resx | Adds localizable extension strings. |
| src/Platform/Microsoft.Testing.Extensions.HtmlReport/Resources/xlf/ExtensionResources.cs.xlf | Adds Czech localization stub. |
| src/Platform/Microsoft.Testing.Extensions.HtmlReport/Resources/xlf/ExtensionResources.de.xlf | Adds German localization stub. |
| src/Platform/Microsoft.Testing.Extensions.HtmlReport/Resources/xlf/ExtensionResources.es.xlf | Adds Spanish localization stub. |
| src/Platform/Microsoft.Testing.Extensions.HtmlReport/Resources/xlf/ExtensionResources.fr.xlf | Adds French localization stub. |
| src/Platform/Microsoft.Testing.Extensions.HtmlReport/Resources/xlf/ExtensionResources.it.xlf | Adds Italian localization stub. |
| src/Platform/Microsoft.Testing.Extensions.HtmlReport/Resources/xlf/ExtensionResources.ja.xlf | Adds Japanese localization stub. |
| src/Platform/Microsoft.Testing.Extensions.HtmlReport/Resources/xlf/ExtensionResources.ko.xlf | Adds Korean localization stub. |
| src/Platform/Microsoft.Testing.Extensions.HtmlReport/Resources/xlf/ExtensionResources.pl.xlf | Adds Polish localization stub. |
| src/Platform/Microsoft.Testing.Extensions.HtmlReport/Resources/xlf/ExtensionResources.pt-BR.xlf | Adds Portuguese Brazil localization stub. |
| src/Platform/Microsoft.Testing.Extensions.HtmlReport/Resources/xlf/ExtensionResources.ru.xlf | Adds Russian localization stub. |
| src/Platform/Microsoft.Testing.Extensions.HtmlReport/Resources/xlf/ExtensionResources.tr.xlf | Adds Turkish localization stub. |
| src/Platform/Microsoft.Testing.Extensions.HtmlReport/Resources/xlf/ExtensionResources.zh-Hans.xlf | Adds Simplified Chinese localization stub. |
| src/Platform/Microsoft.Testing.Extensions.HtmlReport/Resources/xlf/ExtensionResources.zh-Hant.xlf | Adds Traditional Chinese localization stub. |
| src/Platform/Microsoft.Testing.Extensions.HtmlReport/PublicAPI/PublicAPI.Unshipped.txt | Declares new public API surface. |
| src/Platform/Microsoft.Testing.Extensions.HtmlReport/PublicAPI/PublicAPI.Shipped.txt | Initializes shipped API file. |
| src/Platform/Microsoft.Testing.Extensions.HtmlReport/PACKAGE.md | Adds NuGet package documentation. |
| src/Platform/Microsoft.Testing.Extensions.HtmlReport/Microsoft.Testing.Extensions.HtmlReport.csproj | Defines project/package layout and resources. |
| src/Platform/Microsoft.Testing.Extensions.HtmlReport/HtmlReportGeneratorCommandLine.cs | Adds CLI option provider and validation. |
| src/Platform/Microsoft.Testing.Extensions.HtmlReport/HtmlReportGenerator.cs | Adds data consumer/lifetime handler that emits the artifact. |
| src/Platform/Microsoft.Testing.Extensions.HtmlReport/HtmlReportExtensions.cs | Adds builder extension registration method. |
| src/Platform/Microsoft.Testing.Extensions.HtmlReport/HtmlReportEngine.cs | Generates HTML-safe JSON and writes the report file. |
| src/Platform/Microsoft.Testing.Extensions.HtmlReport/buildTransitive/Microsoft.Testing.Extensions.HtmlReport.props | Adds transitive package import. |
| src/Platform/Microsoft.Testing.Extensions.HtmlReport/buildMultiTargeting/Microsoft.Testing.Extensions.HtmlReport.props | Adds MSBuild self-registration metadata. |
| src/Platform/Microsoft.Testing.Extensions.HtmlReport/build/Microsoft.Testing.Extensions.HtmlReport.props | Adds build package import. |
| src/Platform/Microsoft.Testing.Extensions.HtmlReport/BannedSymbols.txt | Adds analyzer banned-symbol rules. |
Copilot's findings
- Files reviewed: 35/40 changed files
- Comments generated: 12
Evangelink
left a comment
There was a problem hiding this comment.
Review Summary — PR #8283: Microsoft.Testing.Extensions.HtmlReport
| # | Dimension | Status |
|---|---|---|
| 1 | Algorithmic Correctness | ✅ LGTM |
| 2 | Threading & Concurrency | ✅ LGTM — ConsumeAsync is always invoked from a single reader task (SingleReader = true); _tests and _testStartTime are not subject to concurrent mutation |
| 3 | Security & IPC Contract Safety | ✅ LGTM — AppendString correctly escapes <, >, &, ', \u2028, \u2029; IsValidPureFileName blocks path traversal |
| 4 | Public API & Binary Compatibility | ✅ LGTM — all public types declared in PublicAPI.Unshipped.txt, XML docs present, no init accessors |
| 5 | Performance & Allocations | OfType<TestMetadataProperty>() allocates per test node in BuildJson hot path |
| 6 | Cross-TFM Compatibility | ✅ LGTM — #if NETCOREAPP guards, polyfills included, no missing API usage |
| 7 | Resource & IDisposable Management | ✅ LGTM — IFileStream wrapped in using |
| 8 | Localization & Resources | ✅ LGTM — all user-facing strings in .resx, no hardcoded UI strings |
| 9 | Test Completeness & Coverage | 🚫 MAJOR — no unit tests ship with this new extension |
Blocking Concern
The PR adds a non-trivial new extension — custom JSON serializer with HTML escaping, filename validation with six rejection branches, duplicate-UID attempt tracking, field truncation — but ships zero unit tests. The codebase's test infrastructure (TestFramework.ForTestingMSTest) is already available. Both IsValidPureFileName and BuildJson are well-suited to pure unit testing (deterministic inputs/outputs, no I/O).
Generated by Expert Code Review (on open) for issue #8283 · ● 19.4M
Addresses both the Copilot reviewer findings and the Expert reviewer critique on PR #8283. Engine & generator - Project incoming TestNodeUpdateMessage into a capped DTO (CapturedTestResult) immediately at consume time, so we no longer retain entire test node payloads for the whole session. Truncation caps protect the test host process, not only the file. - Replace OfType<TestMetadataProperty>() with a plain foreach + 'is' check to avoid the per-test LINQ enumerator allocation. - Add a retry loop for default file names: if the second-precision default file already exists, append "_1", "_2", ... until a unique name is found (5 s overall budget), mirroring TrxReportEngine. - Emit a stable per-row rowKey so the UI can use it as the expand- state key. This removes the synthetic "uid#attempt" key, which could collide with a UID that genuinely contains "#N". Filename validation - Replace Path.GetInvalidFileNameChars() (OS-dependent) with a hard- coded list that is a strict superset of Windows + Unix invalid file name characters. The same input is now rejected regardless of host. - Reject Windows reserved device names (CON, PRN, AUX, NUL, COM0..9, LPT0..9) up-front so the option doesn't pass validation only to fail later during file creation. HTML template - Default filter: when only errored or only timed-out tests are present (no real failures), open the report on that filter instead of "failed" (which would have shown an empty list). - Convert filter pills and sortable column headers from <span>/<div> to <button>, with aria-pressed / aria-sort state. - Make expandable test rows and group headers focusable (tabindex=0, role=button, aria-expanded) with Enter/Space keyboard activation. - Cap rendered rows per group (default 200) with "Show more" / "Show all" controls so switching to grouped view doesn't materialize tens of thousands of DOM nodes on a huge run. Tests - Add the HtmlReportGeneratorCommandLine to ExtensionVersionTests. - Add unit tests for Windows-invalid characters (*, ?, ", <, >, |), reserved Windows device names (CON, PRN, ...) on any OS, and a positive case for names like CONfig.html that just start with a reserved prefix. - Add unit tests for TestResultCapture: outcome classification for every well-known state, truncation at + 7 over the boundary, no-truncation when exactly at the boundary, null result for discovered/in-progress states. - Add HTML/JS escape tests for each of <, >, &, ', U+2028, U+2029. - Add a duplicate-UID test asserting attemptIndex / attemptOf emission and that unique UIDs do NOT get those fields. - Add a stable per-row rowKey test. - Add a default file-name retry test that asserts WriteAsync is called twice when the first file already exists and the resulting path has the "_1" disambiguation suffix. Build is clean: 0 warnings, 0 errors across the extension, the platform, the integration test project, and the extension unit test project on net462, net472, net8.0 and net9.0. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Thanks for the thorough review — addressed every finding. Force-pushed
Code changes
Test counts
Screenshots refreshedThe screenshots in |
|
@copilot address review comments |
…eptance test Co-authored-by: Evangelink <11340282+Evangelink@users.noreply.github.com>
Addressed all three remaining review comments in commit
|
|
@copilot address review comments |
…elledTestNodeStateProperty exception, add --report-html-filename acceptance test Co-authored-by: Evangelink <11340282+Evangelink@users.noreply.github.com>
There was a problem hiding this comment.
Copilot's findings
Comments suppressed due to low confidence (2)
src/Platform/Microsoft.Testing.Extensions.HtmlReport/Templates/report-template.html:932
- Group count badges render the raw outcome keys (for example
timedOut) instead of the user-facing labels used elsewhere (Timed out). This makes grouped headers inconsistent and exposes implementation casing in the report UI.
src/Platform/Microsoft.Testing.Extensions.HtmlReport/Templates/report-template.html:994 - The per-row outcome pill displays the raw serialized outcome (
timedOut) and relies on uppercase styling, so timeout results render asTIMEDOUTrather than the human-readableTimed outused by the filter and summary. Map outcomes to display labels before assigning the text.
- Files reviewed: 39/44 changed files
- Comments generated: 7
Adds [Experimental("TPEXP")] on HtmlReportExtensions, updates the public
API surface to use the [TPEXP] prefix, suppresses TPEXP in the acceptance
test asset and adds an experimental notice to PACKAGE.md.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Update: extension marked as experimentalPushed Review comments statusI re-checked each open review comment against the current code; all 21 inline comments are addressed: Inline comments from Copilot reviewer (12)
Inline comments from expert reviewer (3)
Inline comments from follow-up Copilot review (6)
Let me know if anything still looks off ΓÇö happy to iterate. |
|
@copilot resolve the merge conflicts in this pull request |
Co-authored-by: Evangelink <11340282+Evangelink@users.noreply.github.com>
Merge conflicts resolved in commit |
- Distinguish file-exists IOException from other I/O failures in retry loop (only retry when ExistFile reports the candidate exists; rethrow disk-full, permission, etc.). - Keep test rows as role=row inside role=grid; move expand/collapse control to a child <button> chevron (focusable, aria-expanded). Restores grid semantics. - Don't override an explicit user 'all' filter with the default-failure heuristic on report reload (track loadedSavedState before applying default). - Add rel=noopener noreferrer on external footer link. - Truncate identity fields (Uid, DisplayName, ClassName, MethodName) and trait keys/values in TestResultCapture so test-controlled strings cannot grow the in-memory result list and generated HTML unboundedly. - Remove unused local 'var v' in summary card builder. - Drive-by: fix CA1305 in AssertTests.That from main merge so the repo builds. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The previous Path.Combine(...).Replace(@\"\\\", @\"\\\\\") approach corrupted the regex on Windows: the .Replace doubled the backslash in \.html turning it into \\.html (literal \ + any char), and required two separators between the path and filename when only one is present. Switch to Regex.Escape + an explicit Path.DirectorySeparatorChar, matching the pattern already used by the sibling test for the custom-filename case. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
# Conflicts: # TestFx.slnx # test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/HelpInfoAllExtensionsTests.cs
The Linux CI build uses Microsoft.Testing.Platform.slnf which did not include Microsoft.Testing.Extensions.HtmlReport, so the .nupkg never made it into the local NuGet feeds and the HtmlReportTests / HelpInfoAllExtensions acceptance tests failed with NU1101. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Closes #5754.
Summary
Adds a new MTP extension
Microsoft.Testing.Extensions.HtmlReportthat produces a single, self-contained HTML test report at the end of a test session.--report-html— enable the HTML report.--report-html-filename <name>.html— override the file name. Must be a pure file name (no path, no drive letter, no.., no leading/trailing whitespace, no invalid file-name char).SessionFileArtifact, so other extensions anddotnet testpick it up like a TRX file.Screenshots
Default view — failures first, dynamic toolbar
Grouped by Class
Grouped by Duration
Six buckets:
< 10 ms,10 ms – 100 ms,100 ms – 1 s,1 s – 10 s,10 s – 1 min,≥ 1 min.Grouped by Trait — dark theme
The toolbar gets one
Trait: <key>option per distinct trait/category found in the report.Multiple results for the same
TestNode.UidSome test frameworks emit several distinct terminal-state results sharing the same
TestNode.Uid(parameterized rows, data-driven theories, in-process retries, framework bugs). The extension never deduplicates — every observation is preserved and the affected rows get a#N of Mbadge so users can tell them apart.UX details
prefers-color-scheme.Trait: <key>(one option per distinct trait key in the report). Group headers are collapsible and show per-outcome counts.sessionStorageso refreshing the page keeps your view.Security: handling test-controlled content safely
The data is embedded as a JSON island inside
<script type="application/json">. A hand-rolled HTML-safe JSON encoder escapes<,>,&,',\u2028,\u2029and all control characters as\uXXXXso that test-controlled content (display names, stack traces, stdout/stderr) cannot break out of the script block. The renderer always usestextContent, neverinnerHTML.A regression test (
GenerateReportAsync_EscapesScriptInjection_InDisplayName) verifies that hostile payloads such as</script><img onerror=alert(1)>never appear literally in the produced HTML.Duplicate UIDs
Some test frameworks misuse
TestNode.Uid(or legitimately reuse it across parameterized rows / in-process retries). The consumer therefore:attemptIndex/attemptOfonly on rows whose UID is non-unique.#N of Mbadge on those rows and uses a synthetic per-row key for expand/collapse state so the rows stay independent.Standard output / standard error
Already handled — captured from
StandardOutputProperty/StandardErrorPropertyand rendered in the expandable detail panel (truncated to 32 KiB with a UI-visible marker for very large streams).Retries
For host-level retries (
--retry-failed-tests), each attempt runs in its own results directory, so each gets its own HTML report — same behaviour as TRX. Within a single host run, in-process retries are surfaced via the#N of Mmechanism above.Performance
netstandard2.0without aSystem.Text.Jsondependency, AOT/trim safe.Future hook: live updates (out of scope for this PR)
The page exposes a global
window.mtpRender(newReport)function. A future OpenTelemetry / SSE / WebSocket integration can stream deltas and call this function to refresh the page in place without reloading. None of the current code is structured in a way that closes the door on this.Files
src/Platform/Microsoft.Testing.Extensions.HtmlReport/Microsoft.Testing.Extensions.HtmlReport.csproj(multi-targetsnetstandard2.0;$(SupportedNetFrameworks)).HtmlReportExtensions.AddHtmlReportProvider()+TestingPlatformBuilderHook(auto-registered via MSBuildprops).HtmlReportGeneratorCommandLine,HtmlReportGenerator(IDataConsumer,ITestSessionLifetimeHandler,IDataProducer,IOutputDeviceDataProducer),HtmlReportEngine.Templates/report-template.html(embedded resource).Resources/ExtensionResources.resx+ 12.xlffiles generated viadotnet msbuild ... /t:UpdateXlf.PACKAGE.md,BannedSymbols.txt,PublicAPI/,build/,buildMultiTargeting/,buildTransitive/.TestFx.slnx: project registered.src/Platform/Microsoft.Testing.Platform/Microsoft.Testing.Platform.csproj:InternalsVisibleToadded.test/IntegrationTests/.../HelpInfoAllExtensionsTests.cs: expectations updated for--report-html/--report-html-filenamelines, the newHtmlReportGeneratorCommandLine--infoblock, and the newPackageReferencein the AllExtensions test asset.test/IntegrationTests/.../MSBuild.KnownExtensionRegistration.cs: assertion added for the newTestingPlatformBuilderHookand--report-html, plus a newPackageReference.test/UnitTests/.../HtmlReportGeneratorCommandLineTests.csandHtmlReportEngineTests.cs: 21 new tests covering CLI validation, JSON shape, traits, duplicate-UID handling, output truncation and HTML/JS injection.docs/images/html-report/— screenshots used in this PR description.Public API
Only two public types / two members:
Both declared in
PublicAPI.Unshipped.txt. Noinitaccessors.Validation
Microsoft.Testing.Extensions.HtmlReportbuilds clean acrossnetstandard2.0;net8.0;net9.0;net462;net472.Microsoft.Testing.PlatformandMicrosoft.Testing.Platform.Acceptance.IntegrationTestsbuild clean (0 warnings, 0 errors).Microsoft.Testing.Extensions.UnitTestspass onnet9.0(84 pre-existing + 21 new)../build.cmd -packto publish the new package before they can run end-to-end; the expectation files are updated so those tests will match on the next pack.