Skip to content

Commit

Permalink
feat(ui): show diff in report panel (fix #2406) (#2423)
Browse files Browse the repository at this point in the history
* feat(ui): show diff on report panel

* refactor: add better type guard

* refactor: move diff to utils

* chore: move utils to ui package

Co-authored-by: Vladimir Sheremet <sleuths.slews0s@icloud.com>
  • Loading branch information
scarf005 and sheremet-va committed Dec 5, 2022
1 parent a430365 commit 8595c0e
Show file tree
Hide file tree
Showing 8 changed files with 119 additions and 4 deletions.
18 changes: 17 additions & 1 deletion packages/ui/client/components/views/ViewReport.vue
@@ -1,6 +1,7 @@
<script setup lang="ts">
import { unifiedDiff } from '../../composables/diff'
import { openInEditor, shouldOpenInEditor } from '../../composables/error'
import type { File, ParsedStack, Suite, Task } from '#types'
import type { ErrorWithDiff, File, ParsedStack, Suite, Task } from '#types'
import { config } from '~/composables/client'
import { isDark } from '~/composables/dark'
import { createAnsiToHtmlFilter } from '~/composables/error'
Expand Down Expand Up @@ -94,6 +95,18 @@ function line(stack: ParsedStack) {
function column(stack: ParsedStack) {
return stack.sourcePos?.column ?? stack.column
}
interface Diff { error: NonNullable<Pick<ErrorWithDiff, 'expected' | 'actual'>> }
type ResultWithDiff = Task['result'] & Diff
function isDiffShowable(result?: Task['result']): result is ResultWithDiff {
return result && result?.error?.expected && result?.error?.actual
}
function diff(result: ResultWithDiff): string {
return unifiedDiff(result.error.expected, result.error.actual, {
outputTruncateLength: 80,
})
}
</script>

<template>
Expand Down Expand Up @@ -125,6 +138,9 @@ function column(stack: ParsedStack) {
@click.passive="openInEditor(stack.file, line(stack), column(stack))"
/>
</div>
<pre v-if="isDiffShowable(task.result)">
{{ `\n${diff(task.result)}` }}
</pre>
</div>
</div>
</div>
Expand Down
96 changes: 96 additions & 0 deletions packages/ui/client/composables/diff.ts
@@ -0,0 +1,96 @@
import * as diff from 'diff'

export interface DiffOptions {
outputTruncateLength?: number
outputDiffLines?: number
showLegend?: boolean
}

function formatLine(line: string, maxWidth: number) {
return line.slice(0, maxWidth) + (line.length > maxWidth ? '…' : '')
}

export function unifiedDiff(actual: string, expected: string, options: DiffOptions = {}) {
if (actual === expected)
return ''

const { outputTruncateLength = 80, outputDiffLines, showLegend = true } = options

const indent = ' '
const diffLimit = outputDiffLines || 15

const counts = {
'+': 0,
'-': 0,
}
let previousState: '-' | '+' | null = null
let previousCount = 0
function preprocess(line: string) {
if (!line || line.match(/\\ No newline/))
return

const char = line[0] as '+' | '-'
if ('-+'.includes(char)) {
if (previousState !== char) {
previousState = char
previousCount = 0
}
previousCount++
counts[char]++
if (previousCount === diffLimit)
return `${char} ...`
else if (previousCount > diffLimit)
return
}
return line
}

const msg = diff.createPatch('string', expected, actual)
const lines = msg.split('\n').slice(5).map(preprocess).filter(Boolean) as string[]
const isCompact = counts['+'] === 1 && counts['-'] === 1 && lines.length === 2

let formatted = lines.map((line: string) => {
line = line.replace(/\\"/g, '"')
if (line[0] === '-') {
line = formatLine(line.slice(1), outputTruncateLength)
if (isCompact)
return line
return `- ${formatLine(line, outputTruncateLength)}`
}
if (line[0] === '+') {
line = formatLine(line.slice(1), outputTruncateLength)
if (isCompact)
return line
return `+ ${formatLine(line, outputTruncateLength)}`
}
if (line.match(/@@/))
return '--'
return ` ${line}`
})

if (showLegend) {
// Compact mode
if (isCompact) {
formatted = [
`- Expected ${formatted[0]}`,
`+ Received ${formatted[1]}`,
]
}
else {
if (formatted[0].includes('"'))
formatted[0] = formatted[0].replace('"', '')

const last = formatted.length - 1
if (formatted[last].endsWith('"'))
formatted[last] = formatted[last].slice(0, formatted[last].length - 1)

formatted.unshift(
`- Expected - ${counts['-']}`,
`+ Received + ${counts['+']}`,
'',
)
}
}

return formatted.map(i => indent + i).join('\n')
}
1 change: 1 addition & 0 deletions packages/ui/package.json
Expand Up @@ -55,6 +55,7 @@
"codemirror-theme-vars": "^0.1.1",
"cypress": "^11.0.1",
"d3-graph-controller": "^2.3.22",
"diff": "^5.1.0",
"flatted": "^3.2.7",
"floating-vue": "^2.0.0-y.0",
"picocolors": "^1.0.0",
Expand Down
2 changes: 1 addition & 1 deletion packages/vitest/src/integrations/chai/jest-expect.ts
Expand Up @@ -5,7 +5,7 @@ import { isMockFunction } from '../spy'
import { addSerializer } from '../snapshot/port/plugins'
import type { Constructable, Test } from '../../types'
import { assertTypes } from '../../utils'
import { unifiedDiff } from '../../node/diff'
import { unifiedDiff } from '../../utils/diff'
import type { ChaiPlugin, MatcherState } from '../../types/chai'
import { arrayBufferEquality, generateToBeMessage, iterableEquality, equals as jestEquals, sparseArrayEquality, subsetEquality, typeEquality } from './jest-utils'
import type { AsymmetricMatcher } from './jest-asymmetric-matchers'
Expand Down
Expand Up @@ -4,7 +4,7 @@
import c from 'picocolors'
import type { PrettyFormatOptions } from 'pretty-format'
import { format as prettyFormat, plugins as prettyFormatPlugins } from 'pretty-format'
import { unifiedDiff } from '../../node/diff'
import { unifiedDiff } from '../../utils/diff'
import type { DiffOptions, MatcherHintOptions } from '../../types/matcher-utils'

export const EXPECTED_COLOR = c.green
Expand Down
2 changes: 1 addition & 1 deletion packages/vitest/src/node/error.ts
Expand Up @@ -8,8 +8,8 @@ import { lineSplitRE, parseStacktrace, posToNumber } from '../utils/source-map'
import { F_POINTER } from '../utils/figures'
import { stringify } from '../integrations/chai/jest-matcher-utils'
import { TypeCheckError } from '../typecheck/typechecker'
import { type DiffOptions, unifiedDiff } from '../utils/diff'
import type { Vitest } from './core'
import { type DiffOptions, unifiedDiff } from './diff'
import { divider } from './reporters/renderers/utils'
import type { Logger } from './logger'

Expand Down
File renamed without changes.
2 changes: 2 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 8595c0e

Please sign in to comment.