feat(html-report): omit redundant test body span from trace timeline#5497
feat(html-report): omit redundant test body span from trace timeline#5497
Conversation
When a test has no before/after hooks, the "test body" span is the only child of the test case span and provides no additional information. Remove it in that case, re-parenting any child spans (e.g. HTTP calls) directly under the test case. Also add a SpanTestBody constant to TUnitActivitySource to eliminate the magic string.
There was a problem hiding this comment.
Code Review
Overall this is a clean, focused change with a sensible UX improvement. The constant extraction and re-parenting logic are both well-designed. One architectural concern worth addressing before merge.
Hardcoded string in JS creates silent coupling risk
File: TUnit.Engine/Reporters/Html/HtmlReportGenerator.cs, the new JS block
The PR correctly extracts SpanTestBody as a C# constant to eliminate magic strings in TestExecutor.cs, but the JavaScript comparison still uses a hardcoded literal:
if (directChildren.length === 1 && directChildren[0].name === 'test body') {If TUnitActivitySource.SpanTestBody is ever renamed (e.g., to "test-body" or "body"), the JS will silently stop matching — the if condition never fires, and the redundant span is no longer removed. There's no compiler error, no test failure, just a quietly broken report.
Since this is an embedded JS template generated from C#, injecting the constant via string interpolation eliminates the coupling entirely:
// in the C# string that builds the JS template:
$"""
if (directChildren.length === 1 && directChildren[0].name === '{TUnitActivitySource.SpanTestBody}') {{
"""Why this is better: the constant and the JS are guaranteed to stay in sync. If SpanTestBody changes, the JS automatically changes with it.
The <= 1 threshold may conflict with the test plan's intent
Line 1497: if (sp.length <= 1) return '';
The test plan says:
Run a test whose body produces child spans (e.g. HttpClient activity) — verify child spans appear directly under the test case span
But with this check, a test body with exactly one child span (one HTTP call) will be silently hidden after re-parenting — sp.length becomes 1, which returns ''.
I see that renderSuiteTrace uses the same <= 1 pattern (line 1575), so this is consistent with existing convention. If the intent is "a single bar on a timeline is not useful enough to show", that's a reasonable product decision — but it's worth making that explicit, since the PR description implies single child spans will remain visible. Either the test plan item 3 should clarify "two or more child spans", or the threshold should be < 1.
Minor: Object.assign vs spread
Object.assign({}, s, {parentSpanId: rootSpanId})The rest of the JS uses modern syntax (arrow functions, spread in [...sp]). The spread equivalent {...s, parentSpanId: rootSpanId} is slightly more idiomatic for immutable updates, though this is low priority.
What's good
- The constant extraction is the right call — the other span names (
SpanTestCase,SpanTestSuite, etc.) are all constants, soSpanTestBodybelongs with them. - The re-parenting logic is correct: filtering out the test body span and re-mapping
parentSpanIdon its children preserves the full subtree while flattening one unnecessary level. - The guard
directChildren.length === 1correctly limits the optimization to the "no hooks" case — if before/after hook spans are present as siblings, the test body span is kept. - The change is fully contained to three files with a clear, single responsibility per change.
Up to standards ✅🟢 Issues
|
| Metric | Results |
|---|---|
| Complexity | 0 |
TIP This summary will be updated as you push new changes. Give us feedback
Use object spread instead of Object.assign for consistency with the rest of the JS. Add a comment linking the JS string literal to TUnitActivitySource.SpanTestBody.
|
Addressed the review findings: 1. Hardcoded string coupling — The JS is a raw string literal ( 2. 3. |
There was a problem hiding this comment.
Follow-up Review
All three concerns from the previous review have been addressed in the follow-up commit.
1. Hardcoded string coupling — The added comment // Note: 'test body' must match TUnitActivitySource.SpanTestBody in C#. is the right pragmatic call. The alternative (C# string interpolation) would require escaping hundreds of {} across ~900 lines of embedded JS — a maintenance burden that outweighs the benefit. Documenting the coupling in-place is a clean solution.
2. <= 1 threshold — The author's clarification is correct. getDescendants includes the root span itself in the returned list, so after stripping the test body, a test with one HTTP child span yields [testCase, httpChild] → length 2 → timeline is shown. The <= 1 guard only hides the timeline when no useful child spans exist at all. My reading of this was wrong.
3. Spread syntax — Fixed; {...s, parentSpanId: rootSpanId} is now consistent with the rest of the JS.
The change is clean, the logic is sound, and all feedback has been incorporated. 🚢
There was a problem hiding this comment.
Final Review
The current state of the PR incorporates all feedback from the previous two review rounds. Everything looks good.
Changes verified:
- Constant extraction (
SpanTestBody) — magic string inTestExecutor.csreplaced withTUnitActivitySource.SpanTestBody. All span name constants are now co-located inTUnitActivitySource. - JS string coupling — documented with
// 'test body' must match TUnitActivitySource.SpanTestBody in C#, the right pragmatic trade-off given the ~900 lines of embedded JS that would require escaping if C# interpolation were used. - Spread syntax —
{...s, parentSpanId: rootSpanId}is consistent with the rest of the JS codebase. <= 1guard — correct as written.getDescendantsincludes the root span, so after stripping the test-body span a test with one child span yields length 2 and is shown; the guard only hides the timeline when no useful child spans exist at all.- Re-parenting logic — filtering the test-body span out and remapping
parentSpanIdon its direct children is the correct structural transform. ThedirectChildren.length === 1guard correctly limits the optimization to the no-hooks case.
The change is clean, well-scoped, and the three commits tell a clear story. 🚢
Updated [TUnit.Core](https://github.com/thomhurst/TUnit) from 1.28.7 to 1.33.0. <details> <summary>Release notes</summary> _Sourced from [TUnit.Core's releases](https://github.com/thomhurst/TUnit/releases)._ ## 1.33.0 <!-- Release notes generated using configuration in .github/release.yml at v1.33.0 --> ## What's Changed ### Other Changes * perf: engine-wide performance optimizations by @thomhurst in thomhurst/TUnit#5520 * feat: Add TUnitSettings static API for programmatic configuration by @thomhurst in thomhurst/TUnit#5522 * perf: reduce allocations and improve hot-path performance by @thomhurst in thomhurst/TUnit#5524 * fix: enforce ParallelLimiter semaphore in TestRunner to prevent DependsOn bypass by @thomhurst in thomhurst/TUnit#5526 ### Dependencies * chore(deps): update tunit to 1.32.0 by @thomhurst in thomhurst/TUnit#5513 **Full Changelog**: thomhurst/TUnit@v1.32.0...v1.33.0 ## 1.32.0 <!-- Release notes generated using configuration in .github/release.yml at v1.32.0 --> ## What's Changed ### Other Changes * fix: auto-register correlated logging for minimal API hosts (#5503) by @thomhurst in thomhurst/TUnit#5511 * fix: cascade HookExecutorAttribute from class/assembly to hooks (#5462) by @thomhurst in thomhurst/TUnit#5512 ### Dependencies * chore(deps): update dependency polyfill to 10.3.0 by @thomhurst in thomhurst/TUnit#5508 * chore(deps): update tunit to 1.31.0 by @thomhurst in thomhurst/TUnit#5510 * chore(deps): update dependency polyfill to 10.3.0 by @thomhurst in thomhurst/TUnit#5509 **Full Changelog**: thomhurst/TUnit@v1.31.0...v1.32.0 ## 1.31.0 <!-- Release notes generated using configuration in .github/release.yml at v1.31.0 --> ## What's Changed ### Other Changes * feat(reporters): overhaul GitHub Actions step summary by @thomhurst in thomhurst/TUnit#5483 * fix: truncate large stdout/stderr in HTML report to prevent JSON serialization failure by @thomhurst in thomhurst/TUnit#5485 * feat(html-report): add failure clustering to test report by @thomhurst in thomhurst/TUnit#5490 * feat(html-report): add chevron affordance to failure cluster headers by @thomhurst in thomhurst/TUnit#5492 * feat(reporters): group GitHub summary failures by exception type by @thomhurst in thomhurst/TUnit#5491 * feat(reporters): add minimap sidebar navigator to HTML report by @thomhurst in thomhurst/TUnit#5494 * feat(html-report): add category/tag filter pills to toolbar by @thomhurst in thomhurst/TUnit#5496 * feat(html-report): omit redundant test body span from trace timeline by @thomhurst in thomhurst/TUnit#5497 * fix(tests): clear reporter env vars before each GitHubReporterTest to fix flaky CI on macOS/Windows by @thomhurst in thomhurst/TUnit#5499 * feat: add TestContext.MakeCurrent() for console output correlation by @thomhurst in thomhurst/TUnit#5502 * feat(html-report): add flaky test detection and summary section by @thomhurst in thomhurst/TUnit#5498 * fix: smarter stack trace filtering that preserves TUnit-internal traces by @thomhurst in thomhurst/TUnit#5506 * feat: add Activity baggage-based test context correlation by @thomhurst in thomhurst/TUnit#5505 ### Dependencies * chore(deps): update actions/github-script action to v9 by @thomhurst in thomhurst/TUnit#5476 * chore(deps): update tunit to 1.30.8 by @thomhurst in thomhurst/TUnit#5477 * chore(deps): update dependency polyfill to 10.2.0 by @thomhurst in thomhurst/TUnit#5482 * chore(deps): update dependency polyfill to 10.2.0 by @thomhurst in thomhurst/TUnit#5481 * chore(deps): update actions/upload-artifact action to v7.0.1 by @thomhurst in thomhurst/TUnit#5495 **Full Changelog**: thomhurst/TUnit@v1.30.8...v1.31.0 ## 1.30.8 <!-- Release notes generated using configuration in .github/release.yml at v1.30.8 --> ## What's Changed ### Other Changes * feat(mocks): migrate to T.Mock() extension syntax by @thomhurst in thomhurst/TUnit#5472 * feat: split TUnit.AspNetCore into Core + meta package by @thomhurst in thomhurst/TUnit#5474 * feat: add async Member() overloads for Task-returning member selectors by @thomhurst in thomhurst/TUnit#5475 ### Dependencies * chore(deps): update aspire to 13.2.2 by @thomhurst in thomhurst/TUnit#5464 * chore(deps): update dependency polyfill to 10.1.1 by @thomhurst in thomhurst/TUnit#5468 * chore(deps): update dependency polyfill to 10.1.1 by @thomhurst in thomhurst/TUnit#5467 * chore(deps): update tunit to 1.30.0 by @thomhurst in thomhurst/TUnit#5469 * chore(deps): update dependency microsoft.playwright to 1.59.0 by @thomhurst in thomhurst/TUnit#5473 **Full Changelog**: thomhurst/TUnit@v1.30.0...v1.30.8 ## 1.30.0 <!-- Release notes generated using configuration in .github/release.yml at v1.30.0 --> ## What's Changed ### Other Changes * perf: eliminate locks from mock invocation and verification hot paths by @thomhurst in thomhurst/TUnit#5422 * feat: TUnit0074 analyzer for redundant hook attributes on overrides by @thomhurst in thomhurst/TUnit#5459 * fix(mocks): respect generic type argument accessibility (#5453) by @thomhurst in thomhurst/TUnit#5460 * fix(mocks): skip inaccessible internal accessors when mocking Azure.Response by @thomhurst in thomhurst/TUnit#5461 * fix: apply CultureAttribute and STAThreadExecutorAttribute to hooks (#5452) by @thomhurst in thomhurst/TUnit#5463 ### Dependencies * chore(deps): update tunit to 1.29.0 by @thomhurst in thomhurst/TUnit#5446 * chore(deps): update react to ^19.2.5 by @thomhurst in thomhurst/TUnit#5457 * chore(deps): update opentelemetry to 1.15.2 by @thomhurst in thomhurst/TUnit#5456 * chore(deps): update dependency qs to v6.15.1 by @thomhurst in thomhurst/TUnit#5458 **Full Changelog**: thomhurst/TUnit@v1.29.0...v1.30.0 ## 1.29.0 <!-- Release notes generated using configuration in .github/release.yml at v1.29.0 --> ## What's Changed ### Other Changes * 🤖 Update Mock Benchmark Results by @thomhurst in thomhurst/TUnit#5420 * fix(mocks): resolve build errors when mocking Azure SDK clients by @thomhurst in thomhurst/TUnit#5440 * fix: deduplicate virtual hook overrides across class hierarchy (#5428) by @thomhurst in thomhurst/TUnit#5441 * fix(mocks): unique __argArray locals per event in RaiseEvent dispatch (#5423) by @thomhurst in thomhurst/TUnit#5442 * refactor(mocks): extract MockTypeModel.Visibility helper by @thomhurst in thomhurst/TUnit#5443 * fix(mocks): preserve nullable annotations on generated event implementations by @thomhurst in thomhurst/TUnit#5444 * fix(mocks): preserve nullability on event handler types (#5425) by @thomhurst in thomhurst/TUnit#5445 ### Dependencies * chore(deps): update tunit to 1.28.7 by @thomhurst in thomhurst/TUnit#5416 * chore(deps): update dependency polyfill to v10 by @thomhurst in thomhurst/TUnit#5417 * chore(deps): update dependency polyfill to v10 by @thomhurst in thomhurst/TUnit#5418 * chore(deps): update dependency mockolate to 2.4.0 by @thomhurst in thomhurst/TUnit#5431 * chore(deps): update mstest to 4.2.1 by @thomhurst in thomhurst/TUnit#5433 * chore(deps): update dependency microsoft.net.test.sdk to 18.4.0 by @thomhurst in thomhurst/TUnit#5435 * chore(deps): update microsoft.testing to 2.2.1 by @thomhurst in thomhurst/TUnit#5432 * chore(deps): update dependency microsoft.testing.extensions.codecoverage to 18.6.2 by @thomhurst in thomhurst/TUnit#5437 * chore(deps): update dependency @docusaurus/theme-mermaid to ^3.10.0 by @thomhurst in thomhurst/TUnit#5438 * chore(deps): update docusaurus to v3.10.0 by @thomhurst in thomhurst/TUnit#5439 **Full Changelog**: thomhurst/TUnit@v1.28.7...v1.29.0 Commits viewable in [compare view](thomhurst/TUnit@v1.28.7...v1.33.0). </details> [](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) --- <details> <summary>Dependabot commands and options</summary> <br /> You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) </details> Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Updated [TUnit](https://github.com/thomhurst/TUnit) from 1.30.8 to 1.33.0. <details> <summary>Release notes</summary> _Sourced from [TUnit's releases](https://github.com/thomhurst/TUnit/releases)._ ## 1.33.0 <!-- Release notes generated using configuration in .github/release.yml at v1.33.0 --> ## What's Changed ### Other Changes * perf: engine-wide performance optimizations by @thomhurst in thomhurst/TUnit#5520 * feat: Add TUnitSettings static API for programmatic configuration by @thomhurst in thomhurst/TUnit#5522 * perf: reduce allocations and improve hot-path performance by @thomhurst in thomhurst/TUnit#5524 * fix: enforce ParallelLimiter semaphore in TestRunner to prevent DependsOn bypass by @thomhurst in thomhurst/TUnit#5526 ### Dependencies * chore(deps): update tunit to 1.32.0 by @thomhurst in thomhurst/TUnit#5513 **Full Changelog**: thomhurst/TUnit@v1.32.0...v1.33.0 ## 1.32.0 <!-- Release notes generated using configuration in .github/release.yml at v1.32.0 --> ## What's Changed ### Other Changes * fix: auto-register correlated logging for minimal API hosts (#5503) by @thomhurst in thomhurst/TUnit#5511 * fix: cascade HookExecutorAttribute from class/assembly to hooks (#5462) by @thomhurst in thomhurst/TUnit#5512 ### Dependencies * chore(deps): update dependency polyfill to 10.3.0 by @thomhurst in thomhurst/TUnit#5508 * chore(deps): update tunit to 1.31.0 by @thomhurst in thomhurst/TUnit#5510 * chore(deps): update dependency polyfill to 10.3.0 by @thomhurst in thomhurst/TUnit#5509 **Full Changelog**: thomhurst/TUnit@v1.31.0...v1.32.0 ## 1.31.0 <!-- Release notes generated using configuration in .github/release.yml at v1.31.0 --> ## What's Changed ### Other Changes * feat(reporters): overhaul GitHub Actions step summary by @thomhurst in thomhurst/TUnit#5483 * fix: truncate large stdout/stderr in HTML report to prevent JSON serialization failure by @thomhurst in thomhurst/TUnit#5485 * feat(html-report): add failure clustering to test report by @thomhurst in thomhurst/TUnit#5490 * feat(html-report): add chevron affordance to failure cluster headers by @thomhurst in thomhurst/TUnit#5492 * feat(reporters): group GitHub summary failures by exception type by @thomhurst in thomhurst/TUnit#5491 * feat(reporters): add minimap sidebar navigator to HTML report by @thomhurst in thomhurst/TUnit#5494 * feat(html-report): add category/tag filter pills to toolbar by @thomhurst in thomhurst/TUnit#5496 * feat(html-report): omit redundant test body span from trace timeline by @thomhurst in thomhurst/TUnit#5497 * fix(tests): clear reporter env vars before each GitHubReporterTest to fix flaky CI on macOS/Windows by @thomhurst in thomhurst/TUnit#5499 * feat: add TestContext.MakeCurrent() for console output correlation by @thomhurst in thomhurst/TUnit#5502 * feat(html-report): add flaky test detection and summary section by @thomhurst in thomhurst/TUnit#5498 * fix: smarter stack trace filtering that preserves TUnit-internal traces by @thomhurst in thomhurst/TUnit#5506 * feat: add Activity baggage-based test context correlation by @thomhurst in thomhurst/TUnit#5505 ### Dependencies * chore(deps): update actions/github-script action to v9 by @thomhurst in thomhurst/TUnit#5476 * chore(deps): update tunit to 1.30.8 by @thomhurst in thomhurst/TUnit#5477 * chore(deps): update dependency polyfill to 10.2.0 by @thomhurst in thomhurst/TUnit#5482 * chore(deps): update dependency polyfill to 10.2.0 by @thomhurst in thomhurst/TUnit#5481 * chore(deps): update actions/upload-artifact action to v7.0.1 by @thomhurst in thomhurst/TUnit#5495 **Full Changelog**: thomhurst/TUnit@v1.30.8...v1.31.0 Commits viewable in [compare view](thomhurst/TUnit@v1.30.8...v1.33.0). </details> [](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) --- <details> <summary>Dependabot commands and options</summary> <br /> You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) </details> Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Summary
SpanTestBodyconstant toTUnitActivitySourcealongside the other span name constants, eliminating the magic string inTestExecutorTest plan