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

jest-diff: Add options for colors and symbols #8841

Merged
merged 19 commits into from Aug 22, 2019
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Expand Up @@ -2,6 +2,8 @@

### Features

- `[jest-diff]` Add options for colors and symbols ([#8841](https://github.com/facebook/jest/pull/8841))

### Fixes

### Chore & Maintenance
Expand Down
238 changes: 238 additions & 0 deletions packages/jest-diff/README.md
@@ -0,0 +1,238 @@
# jest-diff

Display differences clearly so people can review changes confidently.

The default export serializes JavaScript **values** and compares them line-by-line.

Two named exports compare **strings** character-by-character:

- `diffStringsAligned` returns a string which includes comparison lines.
Copy link
Contributor

Choose a reason for hiding this comment

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

It took me quite a while (even after reading the whole readme) to understand the difference between those two. I still do not quite understand the naming, which I think was part of what confused me because I associated things like the beginning of lines with "aligned".
Would you say that unified diff vs split diff (like for example in GitHub) is a good analogy for those functions and could be used for naming?

Copy link
Member

Choose a reason for hiding this comment

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

Agreed on being a bit confused about what aligned means. Unified vs split makes more sense to me (that's also the naming bitbucket uses, so I think it's pretty standard)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Brilliant analogy!

  • so-called aligned is like unified diff
  • so-called unaligned is used in Jest to display one-line strings in normal compact report:
Expected: "change from"
Received: "change to"

For multiline strings, a split like GitHub would consist of array of string tuples:

[
  [a0, b0],
  [a1, b1],
  // and so on
]

for corresponding changed chunks. For deleted b is empty, for inserted a is empty.

I am curious, can y’all think of a use case in third-party dependents?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

  • I have renamed diffStringsAligned and diffStringsUnified
  • I think that I will replace diffStringsUnaligned with diffStringsRaw that returns an array of Diff objects, so that it will be reusable to the max

- `diffStringsUnaligned` returns an array of 2 strings.

## Installation

To add this package as a dependency of a project, run either of the following commands:

- `npm install jest-diff`
- `yarn add jest-diff`

## Usage of default export

Given values and optional options, `diffLines(a, b, options?)` does the following:

- **serialize** the values as strings using the `pretty-format` package
- **compare** the strings line-by-line using the `diff-sequences` package
- **format** the changed or common lines using the `chalk` package

To use `diffLines` as the function name, write either of the following:
pedrottimark marked this conversation as resolved.
Show resolved Hide resolved

- `const diffLines = require('jest-diff');` in a CommonJS module
Copy link
Member

Choose a reason for hiding this comment

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

we'll probably go for named exports in jest 25 (we wanna avoid having to use export =), but this is of course fine for now

- `import diffLines from 'jest-diff';` in an ECMAScript module

### Example of default export

```js
const a = ['delete', 'change from', 'common'];
const b = ['change to', 'insert', 'common'];

const difference = diffLines(a, b);
```

The returned **string** consists of:

- annotation lines which describe the change symbols with labels
- blank line
- comparison lines in which `Expected` lines are green, `Received` lines are red, and common lines are dim

```diff
- Expected
SimenB marked this conversation as resolved.
Show resolved Hide resolved
+ Received

Array [
- "delete",
- "change from",
+ "change to",
+ "insert",
"common",
]
```

### Edge cases of default export

Here are edge cases for the return value:

- `' Comparing two different types of values. …'` if the arguments have **different types** according to the `jest-get-type` package (instances of different classes have the same `'object'` type)
- `'Compared values have no visual difference.'` if the arguments have either **referential identity** according to `Object.is` method or **same serialization** according to the `pretty-format` package
- `null` if either argument is a so-called **asymmetric matcher** in Jasmine or Jest

## Usage of diffStringsAligned

Given strings and optional options, `diffStringsAligned(a, b, options?)` does the following:

- **compare** the strings character-by-character using the `diff-sequences` package
- **clean up** small (often coincidental) common substrings, known as “chaff”
- **format** the changed or common lines using the `chalk` package

Although the function is mainly for **multiline** strings, it compares any strings.

Write either of the following:

- `const {diffStringsAligned} = require('jest-diff');` in a CommonJS module
- `import {diffStringsAligned} from 'jest-diff';` in an ECMAScript module

### Example of diffStringsAligned

```js
const a = 'change from\ncommon';
const b = 'change to\ncommon';

const difference = diffStringsAligned(a, b);
```

The returned **string** consists of:

- annotation lines which describe the change symbols with labels
- blank line
- comparison lines in which **changed substrings** have **inverted** foreground and background colors (for example, `from` is white-on-green and `to` is white-on-red)

```diff
- Expected
+ Received

- change from
+ change to
common
```

### Edge cases of diffStringsAligned

Here are edge cases for the return value:

- both `a` and `b` are empty strings: no comparison lines
- only `a` is empty string: all comparison lines have `bColor` and `bSymbol` (see Options)
- only `b` is empty string: all comparison lines have `aColor` and `aSymbol` (see Options)
- `a` and `b` are equal non-empty strings: all comparison lines have `commonColor` and `commonSymbol` (see Options)

### Performance of diffStringsAligned

To get the benefit of **changed substrings** within the comparison lines, a character-by-character comparison has a higher computational cost (in time and space) than a line-by-line comparison.

If the input strings can have **arbitrary length**, we recommend that the calling code set a limit, beyond which it calls the default export instead. For example, Jest falls back to line-by-line comparison if either string has length greater than 20K characters.

## Usage of diffStringsUnaligned

Given strings, `diffStringsUnaligned(a, b)` does the following:

- **compare** the strings character-by-character using the `diff-sequences` package
- **clean up** small (often coincidental) common substrings, known as “chaff”
- **format** the changed substrings using `inverse` from the `chalk` package

Although the function is mainly for **one-line** strings, it compares any strings.

Write either of the following:

- `const {diffStringsUnaligned} = require('jest-diff');` in a CommonJS module
- `import {diffStringsUnaligned} from 'jest-diff';` in an ECMAScript module

### Example of diffStringsUnaligned

```js
const [a, b] = diffStringsUnaligned('change from', 'change to');

// a === 'change ' + chalk.inverse('from')
// b === 'change ' + chalk.inverse('to')
```

The returned **array** of **two strings** corresponds to the two arguments.

Because the caller is responsible to format the returned strings, there are no options.

## Options

The default options are for the report when an assertion fails from the `expect` package used by Jest.

For other applications, you can provide an options object as a third argument:

- `diffLines(a, b, options)`
- `diffStringsAligned(a, b, options)`

### Properties of options object

| name | default |
| :------------- | :------------ |
| `aAnnotation` | `'Expected'` |
| `aColor` | `chalk.green` |
| `aSymbol` | `'-'` |
| `bAnnotation` | `'Received'` |
| `bColor` | `chalk.red` |
| `bSymbol` | `'+'` |
| `commonColor` | `chalk.dim` |
| `commonSymbol` | `' '` |
| `contextLines` | `5` |
| `expand` | `true` |

### Example of options for labels

If the application is code modification, you might replace the labels:

```js
const options = {
aAnnotation: 'Original',
bAnnotation: 'Modified',
};
```

The `jest-diff` package does not assume that the 2 labels have equal length.

### Example of options for colors

For consistency with most diff tools, you might exchange the colors:

```js
import chalk from 'chalk';

const options = {
aColor: chalk.red,
bColor: chalk.green,
};
```

### Example of option to keep the default color

The value of a color option is a function, which given a string, returns a string.

For common lines to keep the default (usually black) color, you might provide an identity function:

```js
const options = {
commonColor: line => line,
};
```

### Example of options for symbols

For consistency with the `diff` command, you might replace the symbols:

```js
const options = {
aSymbol: '<',
bSymbol: '>',
};
```

The `jest-diff` package assumes (but does not enforce) that the 3 symbols have equal length.

### Example of options to limit common lines

By default, the output includes all common lines.

To emphasize the changes, you might limit the number of common “context” lines:

```js
const options = {
contextLines: 1,
expand: false,
};
```

A patch mark like `@@ -12,7 +12,9 @@` accounts for omitted common lines.