Fix HTML report sort to also reorder groups#5103
Conversation
There was a problem hiding this comment.
Code Review
Overall this is a clean, focused fix that addresses two separate issues in a minimal diff. Good work.
Favicon Fix
sb.AppendLine("<link rel=\"icon\" href=\"data:,\">");This is the canonical trick for suppressing favicon 404s. data:, is a valid empty data URI — correct and well-known.
sortGroups() — Logic & Correctness
The function is sound: it spreads the array before sorting to avoid mutating the original, handles all three sort modes, and returns the original reference for the default case (no unnecessary copy). The group duration sort using the max test duration is reasonable since the PR description explicitly calls this out as "slowest test (descending)".
One edge case worth noting: if a group has no tests, Math.max.apply(null, []) returns -Infinity, which is harmless (empty groups sort to the end), but it could be made explicit with a guard.
Style Inconsistency — function vs Arrow Functions
The map callbacks inside sortGroups use the old function keyword while the sort comparator already uses arrow syntax:
// Current — mixed styles
a.tests.map(function(t){return t.durationMs})
// Prefer — consistent arrow functions
a.tests.map(t => t.durationMs)Since this file already uses arrow functions extensively in the surrounding JS, adopting them here keeps things consistent. Also consider Math.max(...arr) instead of Math.max.apply(null, arr) for the same reason.
Suggested cleaner version:
function sortGroups(grps) {
if (sortMode === 'duration') {
return [...grps].sort((a, b) => {
const aMax = Math.max(...a.tests.map(t => t.durationMs));
const bMax = Math.max(...b.tests.map(t => t.durationMs));
return bMax - aMax;
});
}
if (sortMode === 'name') return [...grps].sort((a, b) => a.label.localeCompare(b.label));
return grps;
}Filter vs Sort Ordering
sortGroups is called on the unfiltered groups (before the filter is applied inside render()), while sortTests operates on the already-filtered test list. This is actually the correct behaviour — group sort order should remain stable regardless of which tests are currently visible — but it's a subtle point worth keeping in mind if filter logic evolves.
Summary
| Area | Assessment |
|---|---|
| Favicon fix | Correct |
| Group sorting logic | Correct and non-mutating |
| Arrow function consistency | Minor — prefer arrow style throughout |
| Empty group edge case | Harmless, could be guarded explicitly |
| Filter/sort interaction | Correct by design |
The changes are minimal and well-scoped to the problem. The only suggestion is the style consistency in the JS map callbacks — not a blocker, but worth a quick fix to keep the generated JS idiomatic.
bfa2b65 to
0e620a2
Compare
There was a problem hiding this comment.
Follow-up Code Review
This review compares the current PR state against the previous review.
Previous Review Issues — All Addressed ✓
The prior review flagged three points. The current commit resolves all of them:
| Previous Concern | Status |
|---|---|
| Use arrow functions consistently | ✅ Fixed — grps.map(g => ...) and g.tests.map(t => t.durationMs) already use arrow syntax |
Use Math.max(...arr) instead of Math.max.apply() |
✅ Fixed — spread syntax used: Math.max(...g.tests.map(t => t.durationMs)) |
| Empty group edge case | ✅ Fixed — g.tests.length ? Math.max(...) : 0 guards empty arrays |
Current Code Quality
The pre-computation of max durations into a before sorting is a good pattern — it avoids recomputing on every comparison step (which in a naive implementation would be O(n²) across all tests). Concise and correct.
One Minor Observation (non-blocking)
The Map uses group object references as keys. This works correctly here because entries are object references that remain stable during the sort. Worth a comment only as a heads-up, not a concern.
Favicon Fix
Still the right approach — is the canonical empty data URI for suppressing favicon 404s. No changes needed.
The PR is in good shape. All feedback from the previous review has been incorporated. Ready to merge.
There was a problem hiding this comment.
Follow-up Code Review
This review compares the current PR state against the previous review.
Previous Review Issues — All Addressed ✓
The prior review flagged three points. The current commit resolves all of them:
| Previous Concern | Status |
|---|---|
| Use arrow functions consistently | ✅ Fixed — grps.map(g => ...) and g.tests.map(t => t.durationMs) already use arrow syntax |
Use Math.max(...arr) instead of Math.max.apply() |
✅ Fixed — spread syntax used: Math.max(...g.tests.map(t => t.durationMs)) |
| Empty group edge case | ✅ Fixed — g.tests.length ? Math.max(...) : 0 guards empty arrays |
Current Code Quality
The pre-computation of max durations into a Map before sorting is a notably good pattern:
const maxDur = new Map(grps.map(g => [g, g.tests.length ? Math.max(...g.tests.map(t => t.durationMs)) : 0]));
return [...grps].sort((a,b) => maxDur.get(b) - maxDur.get(a));A naive implementation would call Math.max on every comparison step, making the sort O(n × m log n) where m is the average number of tests per group. Pre-computing into a Map reduces that to O(n × m) upfront + O(n log n) for the sort — clearly better for groups with many tests.
The Map keyed on object references works correctly here because group object identity is stable throughout the sort.
Favicon Fix
sb.AppendLine("<link rel=\"icon\" href=\"data:,\">");Still the right approach — data:, is the canonical empty data URI for suppressing favicon 404s.
All feedback from the previous review has been incorporated. The code is clean, correct, and well-optimised. Ready to merge.
0e620a2 to
dfc9684
Compare
The sort toggle in the HTML report only sorted tests within each group but never reordered the groups themselves. Added sortGroups() to also sort groups by duration (slowest first) or name (alphabetical). Also added an empty favicon link to prevent 404 errors when the report is served from a remote host.
dfc9684 to
9140f1e
Compare
There was a problem hiding this comment.
Follow-up Code Review
Reviewing against the previous two rounds of feedback (last review at commit 0e620a22511cc3c916a90fba5df7f6709b5bc364).
Previous Review Issues — All Addressed ✓
All three points from the original review (arrow functions, spread syntax, empty-group guard) were confirmed resolved in the prior follow-up. No regression on those.
New Finding: Default Sort Behaviour Contradicts PR Description
The sortGroups fall-through case (when sortMode === 'default') now sorts groups by worst status, not by original insertion order:
// sortMode defaults to 'default' (line 1146)
// Falls through to here:
const minStatus = new Map(grps.map(g => [g, g.tests.length ? Math.min(...g.tests.map(t => statusOrder[t.status] ?? 9)) : 9]));
return [...grps].sort((a,b) => minStatus.get(a) - minStatus.get(b));But the PR description's test plan explicitly states:
Verify "Default" sort preserves original group order
These are contradictory. The code does NOT preserve original order in default mode — it surfaces groups with failing tests first. One of two things is true:
- The test plan is stale and the status-based default sort is intentional (arguably a useful UX improvement — failing groups float to the top automatically). If so, the Summary and test plan should be updated to reflect this.
- The behaviour is unintentional and the default case should return
grpsunchanged (matching the original behaviour before this PR).
The intent should be clarified. If option 1 is correct, the implementation is fine — pre-computing the Map before sorting is the right pattern, and using ?? 9 is actually more correct than the existing sortTests which uses || 9 (which would incorrectly treat status order 0 — failed/error — as falsy and fall back to 9).
Minor: Inconsistency With sortTests Nullish Coalescing
// New code (sortGroups) — correct
statusOrder[t.status] ?? 9
// Existing code (sortTests, line 1208) — latent bug
(statusOrder[a.status]||9)statusOrder.failed = 0 and statusOrder.error = 0. With || 9, failed/error tests evaluate as 0 || 9 = 9 — which accidentally places them last in the test sort. The new ?? 9 avoids this. This is a pre-existing issue, but now that we have the correct pattern in sortGroups, it may be worth fixing sortTests for consistency (or at least filing a follow-up). Not a blocker for this PR.
Summary
| Area | Assessment |
|---|---|
| Duration sort | ✅ Correct, well-optimised with pre-computed Map |
| Name sort | ✅ Correct |
| Default sort | |
| Favicon fix | ✅ Correct |
| Arrow functions / spread / empty guard | ✅ All addressed |
The implementation is otherwise clean. The only blocker is clarifying whether the default-mode status sort is intentional, and updating the PR description/test plan accordingly.
There was a problem hiding this comment.
Code Review (Follow-up)
Comparing against previous reviews — all prior concerns have been addressed.
Changes at a glance
| Change | Assessment |
|---|---|
<link rel=\"icon\" href=\"data:,\"> favicon fix |
Correct — canonical empty data URI, suppresses 404s |
||9 → ??9 in sortTests |
Good — ?? is semantically correct (falsy 0 would be mishandled by ||, though not a real issue here since status keys are strings) |
New sortGroups() function |
Well-implemented (see below) |
sortGroups(computeDisplayGroups()) in render() |
Correct integration point |
sortGroups — Final Assessment
The implementation is clean and correct:
- Duration sort: Pre-computes
maxDurinto aMapbefore sorting — avoids recomputingMath.maxon every comparator call (O(n×m) upfront vs O(n×m×log n) naive). Group object references are stable during the sort, so Map keying on object identity is safe. - Name sort: Simple
localeCompare— correct. - Default sort:
minStatusapproach (group ordered by its worst-status test) is a sensible and consistent choice that mirrors the test-level behaviour. - Empty group guard:
g.tests.length ? ... : 0correctly handles empty groups for bothmaxDurandminStatus. - Non-mutating: All paths spread before sorting.
No further issues found. The PR is clean, focused, and correct. Ready to merge.
There was a problem hiding this comment.
Code Review (Follow-up)
Comparing against previous reviews — all prior concerns have been addressed.
Changes at a glance
| Change | Assessment |
|---|---|
<link rel="icon" href="data:,"> favicon fix |
Correct — canonical empty data URI, suppresses 404s |
| ` | |
New sortGroups() function |
Well-implemented (see below) |
sortGroups(computeDisplayGroups()) in render() |
Correct integration point |
sortGroups — Final Assessment
The implementation is clean and correct:
- Duration sort: Pre-computes
maxDurinto aMapbefore sorting — avoids recomputingMath.maxon every comparator call (O(n×m) upfront vs O(n×m×log n) naive). Group object references are stable during the sort, so Map keying on object identity is safe. - Name sort: Simple
localeCompare— correct. - Default sort:
minStatusapproach (group ordered by its worst-status test) is a sensible and consistent choice that mirrors the test-level behaviour. - Empty group guard:
g.tests.length ? ... : 0correctly handles empty groups for bothmaxDurandminStatus. - Non-mutating: All paths spread before sorting.
No further issues found. The PR is clean, focused, and correct. Ready to merge.
Updated [TUnit](https://github.com/thomhurst/TUnit) from 1.18.37 to 1.19.16. <details> <summary>Release notes</summary> _Sourced from [TUnit's releases](https://github.com/thomhurst/TUnit/releases)._ ## 1.19.16 <!-- Release notes generated using configuration in .github/release.yml at v1.19.16 --> ## What's Changed ### Other Changes * Truncate exceptions in GitHub summary tables by @thomhurst in thomhurst/TUnit#5108 ### Dependencies * chore(deps): update tunit to 1.19.11 by @thomhurst in thomhurst/TUnit#5106 * chore(deps): bump dompurify from 3.3.0 to 3.3.2 in /docs by @dependabot[bot] in thomhurst/TUnit#5096 * chore(deps): bump svgo from 3.2.0 to 3.3.3 in /docs by @dependabot[bot] in thomhurst/TUnit#5086 **Full Changelog**: thomhurst/TUnit@v1.19.11...v1.19.16 ## 1.19.11 <!-- Release notes generated using configuration in .github/release.yml at v1.19.11 --> ## What's Changed ### Other Changes * Fix HTML report sort to also reorder groups by @thomhurst in thomhurst/TUnit#5103 * fix: correct expand-all icon SVG in HTML report by @slang25 in thomhurst/TUnit#5105 * Avoid some redundant list allocations by @SimonCropp in thomhurst/TUnit#4963 * avoid some redundant enumerable alloc by @SimonCropp in thomhurst/TUnit#4972 * use char instead of string for joins by @SimonCropp in thomhurst/TUnit#4989 ### Dependencies * chore(deps): update dependency nunit to 4.5.1 by @thomhurst in thomhurst/TUnit#5097 * chore(deps): update tunit to 1.19.0 by @thomhurst in thomhurst/TUnit#5099 * chore(deps): update dependency humanizer to 3.0.10 by @thomhurst in thomhurst/TUnit#5101 * chore(deps): update dependency nunit.analyzers to 4.12.0 by @thomhurst in thomhurst/TUnit#5102 **Full Changelog**: thomhurst/TUnit@v1.19.0...v1.19.11 ## 1.19.0 <!-- Release notes generated using configuration in .github/release.yml at v1.19.0 --> ## What's Changed ### Other Changes * fix: improve CreateTestVariant API and fix void/ValueTask return types by @thomhurst in thomhurst/TUnit#5095 ### Dependencies * chore(deps): update tunit to 1.18.37 by @thomhurst in thomhurst/TUnit#5094 **Full Changelog**: thomhurst/TUnit@v1.18.37...v1.19.0 Commits viewable in [compare view](thomhurst/TUnit@v1.18.37...v1.19.16). </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
<link rel="icon">to prevent browsers from requesting/favicon.icowhen the report is hosted remotely.Test plan