Skip to content

feat(html-report): omit redundant test body span from trace timeline#5497

Merged
thomhurst merged 3 commits intomainfrom
omit-redundant-test-body-span
Apr 10, 2026
Merged

feat(html-report): omit redundant test body span from trace timeline#5497
thomhurst merged 3 commits intomainfrom
omit-redundant-test-body-span

Conversation

@thomhurst
Copy link
Copy Markdown
Owner

Summary

  • When a test has no before/after hooks, the "test body" span is the only child of the test case span and adds no useful information to the trace timeline — omit it
  • If the test body has child spans (e.g. HTTP calls, DB queries), those are re-parented directly under the test case span so they remain visible
  • Add SpanTestBody constant to TUnitActivitySource alongside the other span name constants, eliminating the magic string in TestExecutor

Test plan

  • Run a test with no hooks — verify the trace timeline is hidden (no redundant "test body" bar)
  • Run a test with before/after hooks — verify all spans (hooks + test body) still appear
  • Run a test whose body produces child spans (e.g. HttpClient activity) — verify child spans appear directly under the test case span

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.
Copy link
Copy Markdown
Contributor

@claude claude bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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, so SpanTestBody belongs with them.
  • The re-parenting logic is correct: filtering out the test body span and re-mapping parentSpanId on its children preserves the full subtree while flattening one unnecessary level.
  • The guard directChildren.length === 1 correctly 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.

@codacy-production
Copy link
Copy Markdown

codacy-production bot commented Apr 10, 2026

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 0 complexity

Metric Results
Complexity 0

View in Codacy

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.
@thomhurst
Copy link
Copy Markdown
Owner Author

Addressed the review findings:

1. Hardcoded string coupling — The JS is a raw string literal ("""), so C# interpolation would require escaping hundreds of {/} across ~900 lines of JS — not practical. Added a comment linking the JS literal to TUnitActivitySource.SpanTestBody so the coupling is documented. (The same coupling already exists for 'test case' on line 1566.)

2. <= 1 threshold — This is actually fine as-is. getDescendants includes the root span itself, so after removing test body from a test with 1 child span: sp = [testCase, httpChild] → length 2 → the trace IS shown. The <= 1 only hides the timeline when only the bare test case span remains with no children at all.

3. Object.assign vs spread — Fixed to {...s, parentSpanId: rootSpanId} for consistency with the rest of the JS.

Copy link
Copy Markdown
Contributor

@claude claude bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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. 🚢

Copy link
Copy Markdown
Contributor

@claude claude bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 in TestExecutor.cs replaced with TUnitActivitySource.SpanTestBody. All span name constants are now co-located in TUnitActivitySource.
  • 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.
  • <= 1 guard — correct as written. getDescendants includes 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 parentSpanId on its direct children is the correct structural transform. The directChildren.length === 1 guard correctly limits the optimization to the no-hooks case.

The change is clean, well-scoped, and the three commits tell a clear story. 🚢

github-actions bot pushed a commit to IntelliTect/CodingGuidelines that referenced this pull request Apr 13, 2026
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>

[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=TUnit.Core&package-manager=nuget&previous-version=1.28.7&new-version=1.33.0)](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>
This was referenced Apr 13, 2026
intellitect-bot pushed a commit to IntelliTect/EssentialCSharp.Web that referenced this pull request Apr 14, 2026
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>

[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=TUnit&package-manager=nuget&previous-version=1.30.8&new-version=1.33.0)](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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant