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

fix: handle pretty formatting of wide arrays and array-like objects #12402

Merged
merged 11 commits into from Feb 16, 2022
2 changes: 2 additions & 0 deletions CHANGELOG.md
Expand Up @@ -13,6 +13,7 @@
- `[jest-resolver]` [**BREAKING**] Add support for `package.json` `exports` ([11961](https://github.com/facebook/jest/pull/11961))
- `[@jes/schemas]` New module for JSON schemas for Jest's config ([#12384](https://github.com/facebook/jest/pull/12384))
- `[jest-worker]` [**BREAKING**] Allow only absolute `workerPath` ([#12343](https://github.com/facebook/jest/pull/12343))
- `[pretty-format]` New `maxWidth` parameter ([#12402](https://github.com/facebook/jest/pull/12402))

### Fixes

Expand All @@ -21,6 +22,7 @@
- `[jest-config]` Pass `moduleTypes` to `ts-node` to enforce CJS when transpiling ([#12397](https://github.com/facebook/jest/pull/12397))
- `[jest-environment-jsdom]` Make `jsdom` accessible to extending environments again ([#12232](https://github.com/facebook/jest/pull/12232))
- `[jest-jasmine2, jest-types]` [**BREAKING**] Move all `jasmine` specific types from `@jest/types` to its own package ([#12125](https://github.com/facebook/jest/pull/12125))
- `[jest-matcher-utils]` Pass maxWidth to `pretty-format` to avoid printing every element in arrays by default ([#12402](https://github.com/facebook/jest/pull/12402))

### Chore & Maintenance

Expand Down
17 changes: 17 additions & 0 deletions packages/jest-matcher-utils/src/__tests__/index.test.ts
Expand Up @@ -97,6 +97,23 @@ describe('stringify()', () => {
expect(stringify(big)).toBe(prettyFormat(big, {maxDepth: 1, min: true}));
expect(stringify(small)).toBe(prettyFormat(small, {min: true}));
});

test('reduces maxWidth if stringifying very large arrays', () => {
const big: any = [];
const small: any = [];
const testString = Array(1000).join('x');

for (let i = 0; i < 100; i += 1) {
big[i] = testString;
}

for (let i = 0; i < 3; i += 1) {
small[i] = testString;
}

expect(stringify(big)).toBe(prettyFormat(big, {maxWidth: 5, min: true}));
expect(stringify(small)).toBe(prettyFormat(small, {min: true}));
});
});

describe('ensureNumbers()', () => {
Expand Down
18 changes: 14 additions & 4 deletions packages/jest-matcher-utils/src/index.ts
Expand Up @@ -89,28 +89,38 @@ export const SUGGEST_TO_CONTAIN_EQUAL = chalk.dim(
'Looks like you wanted to test for object/array equality with the stricter `toContain` matcher. You probably need to use `toContainEqual` instead.',
);

export const stringify = (object: unknown, maxDepth: number = 10): string => {
export const stringify = (
object: unknown,
maxDepth: number = 10,
maxWidth: number = 10,
): string => {
const MAX_LENGTH = 10000;
let result;

try {
result = prettyFormat(object, {
maxDepth,
maxWidth,
min: true,
plugins: PLUGINS,
});
} catch {
result = prettyFormat(object, {
callToJSON: false,
maxDepth,
maxWidth,
min: true,
plugins: PLUGINS,
});
}

return result.length >= MAX_LENGTH && maxDepth > 1
? stringify(object, Math.floor(maxDepth / 2))
: result;
if (result.length >= MAX_LENGTH && maxDepth > 1) {
return stringify(object, Math.floor(maxDepth / 2), maxWidth);
} else if (result.length >= MAX_LENGTH && maxWidth > 1) {
return stringify(object, maxDepth, Math.floor(maxWidth / 2));
} else {
return result;
}
};

export const highlightTrailingWhitespace = (text: string): string =>
Expand Down
1 change: 1 addition & 0 deletions packages/jest-schemas/src/index.ts
Expand Up @@ -15,6 +15,7 @@ const RawSnapshotFormat = Type.Partial(
highlight: Type.Readonly(Type.Boolean()),
indent: Type.Readonly(Type.Number({minimum: 0})),
maxDepth: Type.Readonly(Type.Number({minimum: 0})),
maxWidth: Type.Readonly(Type.Number({minimum: 0})),
min: Type.Readonly(Type.Boolean()),
printBasicPrototype: Type.Readonly(Type.Boolean()),
printFunctionName: Type.Readonly(Type.Boolean()),
Expand Down
1 change: 1 addition & 0 deletions packages/pretty-format/README.md
Expand Up @@ -75,6 +75,7 @@ console.log(prettyFormat(onClick, options));
| `highlight` | `boolean` | `false` | highlight syntax with colors in terminal (some plugins) |
| `indent` | `number` | `2` | spaces in each level of indentation |
| `maxDepth` | `number` | `Infinity` | levels to print in arrays, objects, elements, and so on |
| `maxWidth` | `number` | `Infinity` | number of elements to print in arrays, sets, and so on |
| `min` | `boolean` | `false` | minimize added space: no indentation nor line breaks |
| `plugins` | `array` | `[]` | plugins to serialize application-specific data types |
| `printBasicPrototype` | `boolean` | `false` | print the prototype for plain objects and arrays |
Expand Down
53 changes: 53 additions & 0 deletions packages/pretty-format/src/__tests__/prettyFormat.test.ts
Expand Up @@ -562,6 +562,59 @@ describe('prettyFormat()', () => {
);
});

describe('maxWidth option', () => {
it('applies to arrays', () => {
const val = Array(1_000_000).fill('x');
expect(prettyFormat(val, {maxWidth: 5})).toEqual(
[
'Array [',
' "x",',
' "x",',
' "x",',
' "x",',
' "x",',
' …',
']',
].join('\n'),
);
});

it('applies to sets', () => {
const val = new Set([1, 2, 3, 4, 5, 6, 7, 8, 9, 0]);
expect(prettyFormat(val, {maxWidth: 5})).toEqual(
['Set {', ' 1,', ' 2,', ' 3,', ' 4,', ' 5,', ' …', '}'].join(
'\n',
),
);
});

it('applies to maps', () => {
const val = new Map();
val.set('a', 1);
val.set('b', 2);
val.set('c', 3);
val.set('d', 4);
val.set('e', 5);
val.set('f', 6);
val.set('g', 7);
val.set('h', 8);
val.set('i', 9);
val.set('j', 10);
expect(prettyFormat(val, {maxWidth: 5})).toEqual(
[
'Map {',
' "a" => 1,',
' "b" => 2,',
' "c" => 3,',
' "d" => 4,',
' "e" => 5,',
' …',
'}',
].join('\n'),
);
});
});

it('can customize the max depth', () => {
const val = [
{
Expand Down
27 changes: 23 additions & 4 deletions packages/pretty-format/src/collections.ts
Expand Up @@ -43,6 +43,7 @@ export function printIteratorEntries(
separator: string = ': ',
): string {
let result = '';
let width = 0;
let current = iterator.next();

if (!current.done) {
Expand All @@ -51,6 +52,13 @@ export function printIteratorEntries(
const indentationNext = indentation + config.indent;

while (!current.done) {
result += indentationNext;

if (width++ === config.maxWidth) {
result += '…';
break;
}

const name = printer(
current.value[0],
config,
Expand All @@ -66,7 +74,7 @@ export function printIteratorEntries(
refs,
);

result += indentationNext + name + separator + value;
result += name + separator + value;

current = iterator.next();

Expand Down Expand Up @@ -97,6 +105,7 @@ export function printIteratorValues(
printer: Printer,
): string {
let result = '';
let width = 0;
let current = iterator.next();

if (!current.done) {
Expand All @@ -105,9 +114,14 @@ export function printIteratorValues(
const indentationNext = indentation + config.indent;

while (!current.done) {
result +=
indentationNext +
printer(current.value, config, indentationNext, depth, refs);
result += indentationNext;

if (width++ === config.maxWidth) {
result += '…';
break;
}

result += printer(current.value, config, indentationNext, depth, refs);

current = iterator.next();

Expand Down Expand Up @@ -147,6 +161,11 @@ export function printListItems(
for (let i = 0; i < list.length; i++) {
result += indentationNext;

if (i === config.maxWidth) {
result += '…';
break;
}

if (i in list) {
result += printer(list[i], config, indentationNext, depth, refs);
}
Expand Down
53 changes: 16 additions & 37 deletions packages/pretty-format/src/index.ts
Expand Up @@ -404,6 +404,7 @@ export const DEFAULT_OPTIONS: Options = {
highlight: false,
indent: 2,
maxDepth: Infinity,
maxWidth: Infinity,
min: false,
plugins: [],
printBasicPrototype: true,
Expand Down Expand Up @@ -465,56 +466,34 @@ const getColorsEmpty = (): Colors =>
}, Object.create(null));

const getPrintFunctionName = (options?: OptionsReceived) =>
options && options.printFunctionName !== undefined
? options.printFunctionName
: DEFAULT_OPTIONS.printFunctionName;
options?.printFunctionName ?? DEFAULT_OPTIONS.printFunctionName;

const getEscapeRegex = (options?: OptionsReceived) =>
options && options.escapeRegex !== undefined
? options.escapeRegex
: DEFAULT_OPTIONS.escapeRegex;
options?.escapeRegex ?? DEFAULT_OPTIONS.escapeRegex;

const getEscapeString = (options?: OptionsReceived) =>
options && options.escapeString !== undefined
? options.escapeString
: DEFAULT_OPTIONS.escapeString;
options?.escapeString ?? DEFAULT_OPTIONS.escapeString;

const getConfig = (options?: OptionsReceived): Config => ({
callToJSON:
options && options.callToJSON !== undefined
? options.callToJSON
: DEFAULT_OPTIONS.callToJSON,
colors:
options && options.highlight
? getColorsHighlight(options)
: getColorsEmpty(),
callToJSON: options?.callToJSON ?? DEFAULT_OPTIONS.callToJSON,
colors: options?.highlight ? getColorsHighlight(options) : getColorsEmpty(),
compareKeys:
options && typeof options.compareKeys === 'function'
typeof options?.compareKeys === 'function'
? options.compareKeys
: DEFAULT_OPTIONS.compareKeys,
escapeRegex: getEscapeRegex(options),
escapeString: getEscapeString(options),
indent:
options && options.min
? ''
: createIndent(
options && options.indent !== undefined
? options.indent
: DEFAULT_OPTIONS.indent,
),
maxDepth:
options && options.maxDepth !== undefined
? options.maxDepth
: DEFAULT_OPTIONS.maxDepth,
min: options && options.min !== undefined ? options.min : DEFAULT_OPTIONS.min,
plugins:
options && options.plugins !== undefined
? options.plugins
: DEFAULT_OPTIONS.plugins,
indent: options?.min
? ''
: createIndent(options?.indent ?? DEFAULT_OPTIONS.indent),
maxDepth: options?.maxDepth ?? DEFAULT_OPTIONS.maxDepth,
maxWidth: options?.maxWidth ?? DEFAULT_OPTIONS.maxWidth,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Functionally, the maxWidth is the only newly introduced behaviour in this block. The rest of this block diff is related to a refactor to make use of the safe navigation and null coalescing operators so that

options && options.foo ? options.foo : DEFAULT_OPTIONS.foo

can become

options?.foo ?? DEFAULT_OPTIONS.foo

min: options?.min ?? DEFAULT_OPTIONS.min,
plugins: options?.plugins ?? DEFAULT_OPTIONS.plugins,
printBasicPrototype: options?.printBasicPrototype ?? true,
printFunctionName: getPrintFunctionName(options),
spacingInner: options && options.min ? ' ' : '\n',
spacingOuter: options && options.min ? '' : '\n',
spacingInner: options?.min ? ' ' : '\n',
spacingOuter: options?.min ? '' : '\n',
});

function createIndent(indent: number): string {
Expand Down
1 change: 1 addition & 0 deletions packages/pretty-format/src/types.ts
Expand Up @@ -45,6 +45,7 @@ export type Config = {
escapeString: boolean;
indent: string;
maxDepth: number;
maxWidth: number;
min: boolean;
plugins: Plugins;
printBasicPrototype: boolean;
Expand Down