Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ui): show diff in report panel (fix #2406) #2423

Merged
merged 4 commits into from Dec 5, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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.