Strategy-aware diff output for failing tests#127
Strategy-aware diff output for failing tests#127DmitrySharabin wants to merge 10 commits intomainfrom
Conversation
✅ Deploy Preview for h-test ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
acabfcf to
c6e7e4a
Compare
New src/format-diff.js picks an output strategy by value shape: - Short values: inline char-level diff (`Got X, expected Y`) - Long single-line text (>40 chars, contains spaces): word-level diff in two-line ` Actual:` / ` Expected:` layout - Multi-line stringified values: unified -/+ block under ` Actual ↔ Expected:` header, with token highlighting on isolated -/+ pairs and elision of matching runs (2 lines context) - Type mismatches: short header plus both values, no diff - Whitespace-only changes get background highlight TestResult.js delegates to the new module; legacy formatDiff helper in src/util.js removed. Tests in tests/format-diff.js cover strategy boundaries and edge cases.
c6e7e4a to
cc5c6f5
Compare
|
Good idea. In fact, for some diffs I can see value in a "compact" view where you only see the unchanged bits once and the changed bits are inline. Perhaps the default could take into account size of total output vs % of changes. I also wonder if it would be useful to have an optional override that lets users explicitly control this. |
Agreed. Let's concentrate on this first. This will already be a big improvement to what we have now.
Good idea. I'm afraid, though, that even with this feature in place, the interactive output might be broken on long diffs. I'd fix the terminal UX first. I wonder if we can do something like LLMs do in the terminal. Is there anything better than |
| const { diffChars } = await import(IS_NODEJS ? "diff" : "https://cdn.jsdelivr.net/npm/diff@7.0.0/lib/index.es6.js"); | ||
| import { stripFormatting } from "../format-console.js"; | ||
| import { delay, formatDuration, interceptConsole, pluralize, stringify } from "../util.js"; | ||
| import { formatDiff } from "../format-diff.js"; |
There was a problem hiding this comment.
Should this be in a util/ directory?
There was a problem hiding this comment.
We don't have the util/ directory for now. format-console.js should probably live there as well. We can add it starting from this PR and move the rest of the util methods there in the follow-up PRs.
Address PR #127 review feedback from @LeaVerou: - colorize(): single `<bg side-color><b>` primitive for all changed runs; drop per-token fg color and the whitespace-only branch. - lineDiff() multiline changed lines wrap in `<bg lightblack>`; chunk bg stacks over the neutral line bg so changed chars pop. - Isolated -/+ pair switches from diffWords to diffChars so small changes in long stringified lines no longer repaint whole tokens. sideBySide long-line path keeps diffWords — prose-like content where word unit matches semantic unit.
Non-isolated `-`/`+` blocks previously rendered plain. Now they interleave char-diff pairs when counts match AND every paired line clears the `PAIR_SIMILARITY` threshold — per-char overlap ratio via `diffChars` common runs. Forced char-diff on unrelated lines would paint coincidental overlaps as "common" and mislead, so the check is all-or-nothing per block. Other changes: - `colorize` simplified: single `<bg side-color><b>` primitive for all changed runs; no fg color; no whitespace-only branch. - `lineDiff` uses `formatBlock(removed, added)` which subsumes the old `isolatedPair` special case (1:1 similar pair via diffChars, everything else plain). - `diffChars` result is reused between similarity check and rendering — halves diff calls on the paired-block path. - File reorganized: public entry, strategies, formatting helpers, pure utilities.
Three fixtures cover the block-level pair decision in formatBlock: - Similar lines pair up — char-diff interleaved - Dissimilar lines stay plain — removes then adds, no char-diff - Unequal counts stay plain — no safe pairing Also updates expected strings of existing fixtures to match the new bg-based diff highlighting.
Summary
Replaces the old single-layout char-diff for failing tests with a strategy-aware
formatDiffmodule that picks a layout based on value shape, and acleanupSemanticpost-pass that suppresses coincidental char-diff noise.Layouts
Got X, expected Y).Actual:/Expected:block.-/+block under anActual ↔ Expected:header, with:… N matching lines ….lightblack) background so the eye can spot at a glance which lines differ; red/green per-chunk highlights stack on top of that tint and mark the precise edits. Gray is used rather thanlightred/lightgreenfor two reasons — with the 16-color ANSI palette we're limited to, the per-chunk red/green highlights wouldn't pop against a same-hue line background, and a green/red line tint would read as "the whole line was added/removed" even when only part of it changed.(values are stringified identically but not equal)hint. Hit most often when a custom shallow check sees distinct references with identical content.Cleanup pass
diffChars(Myers) paints coincidental shared chars as "common" — e.g. foralignmentvspaddingit findsa/i/nand shreds the swap into 1-char islands. AcleanupSemantichelper post-processesdiffCharsoutput: a common chunk survives as a boundary only when it's longer than the edits on either adjacent side; shorter commons dissolve into surrounding edits so the region collapses to one removed + one added block. Same idea as diff-match-patch'sdiff_cleanupSemantic, narrowed to our shape.This removes the need for a similarity threshold in
formatBlock— matched-count pairs always go through the paired path; cleanup keeps single-char precision where the change is genuinely localized (usernamevsuserName,v1.2.3vsv1.2.4) and collapses the swap where it isn't (alignment/padding).Wire-up
TestResult.jsdelegates failure formatting toformatDiff(actual, expected, unmapped). The legacyformatDiffhelper insrc/util.jsis removed. Pre-mapvalues are passed through an optionalunmappedargument and rendered alongside the diff in each layout.Tests
tests/format-diff.jscovers every strategy boundary: type mismatch, stringified-identical, inline char-diff (with and without unmapped), long word-diff (with and without unmapped), inline multi-line string, elision, paired char-diff with unmapped, noisy-swap collapse via cleanup, multiple paired lines, unequal counts, pure add, pure remove.Dependencies
diffbumped to^8.0.4(CDN URL kept in sync withpackage.json).diffWordsWithSpaceis used for the long-single-line layout so whitespace survives as part of token boundaries.