From 3616a17c3d788806eb0f746cdfa254d19bd9ae15 Mon Sep 17 00:00:00 2001 From: Christophe Geers Date: Fri, 15 Mar 2024 23:53:55 +0100 Subject: [PATCH 1/4] feat: remove unrelated noise from diff for toMatchObject() --- packages/expect/src/jest-expect.ts | 4 +- packages/expect/src/jest-utils.ts | 57 +++++++++++++++- test/core/test/jest-expect.test.ts | 102 ++++++++++++++++++++++++----- 3 files changed, 145 insertions(+), 18 deletions(-) diff --git a/packages/expect/src/jest-expect.ts b/packages/expect/src/jest-expect.ts index c20ade34e381..a06ffd968229 100644 --- a/packages/expect/src/jest-expect.ts +++ b/packages/expect/src/jest-expect.ts @@ -4,7 +4,7 @@ import type { MockInstance } from '@vitest/spy' import { isMockFunction } from '@vitest/spy' import type { Test } from '@vitest/runner' import type { Assertion, ChaiPlugin } from './types' -import { arrayBufferEquality, generateToBeMessage, iterableEquality, equals as jestEquals, sparseArrayEquality, subsetEquality, typeEquality } from './jest-utils' +import { arrayBufferEquality, generateToBeMessage, getObjectSubset, iterableEquality, equals as jestEquals, sparseArrayEquality, subsetEquality, typeEquality } from './jest-utils' import type { AsymmetricMatcher } from './jest-asymmetric-matchers' import { diff, getCustomEqualityTesters, stringify } from './jest-matcher-utils' import { JEST_MATCHERS_OBJECT } from './constants' @@ -166,7 +166,7 @@ export const JestChaiExpect: ChaiPlugin = (chai, utils) => { 'expected #{this} to match object #{exp}', 'expected #{this} to not match object #{exp}', expected, - actual, + getObjectSubset(actual, expected), ) }) def('toMatch', function (expected: string | RegExp) { diff --git a/packages/expect/src/jest-utils.ts b/packages/expect/src/jest-utils.ts index 60322d019815..7b2ce37d9d3d 100644 --- a/packages/expect/src/jest-utils.ts +++ b/packages/expect/src/jest-utils.ts @@ -420,7 +420,7 @@ export function iterableEquality(a: any, b: any, customTesters: Array = /** * Checks if `hasOwnProperty(object, key)` up the prototype chain, stopping at `Object.prototype`. */ -function hasPropertyInObject(object: object, key: string): boolean { +function hasPropertyInObject(object: object, key: string | symbol): boolean { const shouldTerminate = !object || typeof object !== 'object' || object === Object.prototype @@ -540,3 +540,58 @@ export function generateToBeMessage(deepEqualityName: string, expected = '#{this export function pluralize(word: string, count: number): string { return `${count} ${word}${count === 1 ? '' : 's'}` } + +export function getObjectKeys(object: object): Array { + return [ + ...Object.keys(object), + ...Object.getOwnPropertySymbols(object).filter( + s => Object.getOwnPropertyDescriptor(object, s)?.enumerable, + ), + ] +} + +export function getObjectSubset(object: any, subset: any, customTesters: Array = [], seenReferences: WeakMap = new WeakMap()): any { + if (Array.isArray(object)) { + if (Array.isArray(subset) && subset.length === object.length) { + // The map method returns correct subclass of subset. + return subset.map((sub: any, i: number) => + getObjectSubset(object[i], sub, customTesters), + ) + } + } + else if (object instanceof Date) { + return object + } + else if (isObject(object) && isObject(subset)) { + if ( + equals(object, subset, [ + ...customTesters, + iterableEquality, + subsetEquality, + ]) + ) { + // Avoid unnecessary copy which might return Object instead of subclass. + return subset + } + + const trimmed: any = {} + seenReferences.set(object, trimmed) + + for (const key of getObjectKeys(object).filter(key => + hasPropertyInObject(subset, key), + )) { + trimmed[key] = seenReferences.has(object[key]) + ? seenReferences.get(object[key]) + : getObjectSubset( + object[key], + subset[key], + customTesters, + seenReferences, + ) + } + + if (getObjectKeys(trimmed).length > 0) + return trimmed + } + return object +} diff --git a/test/core/test/jest-expect.test.ts b/test/core/test/jest-expect.test.ts index a637793803be..afc6636547c3 100644 --- a/test/core/test/jest-expect.test.ts +++ b/test/core/test/jest-expect.test.ts @@ -903,24 +903,96 @@ it('correctly prints diff with asymmetric matchers', () => { } }) -it('toHaveProperty error diff', () => { - setupColors(getDefaultColors()) +// make it easy for dev who trims trailing whitespace on IDE +function trim(s: string): string { + return s.replaceAll(/ *$/gm, '') +} - // make it easy for dev who trims trailing whitespace on IDE - function trim(s: string): string { - return s.replaceAll(/ *$/gm, '') +function getError(f: () => unknown) { + try { + f() + return expect.unreachable() } - - function getError(f: () => unknown) { - try { - f() - return expect.unreachable() - } - catch (error) { - const processed = processError(error) - return [processed.message, trim(processed.diff)] - } + catch (error) { + const processed = processError(error) + return [processed.message, trim(processed.diff)] } +} + +it('toMatchObject error diff', () => { + setupColors(getDefaultColors()) + + // single property on root + expect(getError(() => expect({ a: 1, b: 2, c: { d: 4 } }).toMatchObject({ b: 3 }))).toMatchInlineSnapshot(` + [ + "expected { a: 1, b: 2, c: { d: 4 } } to match object { b: 3 }", + "- Expected + + Received + + Object { + - "b": 3, + + "b": 2, + }", + ] + `) + + // nested property + expect(getError(() => expect({ a: 1, b: 2, c: { d: 4 } }).toMatchObject({ c: { d: 5 } }))).toMatchInlineSnapshot(` + [ + "expected { a: 1, b: 2, c: { d: 4 } } to match object { c: { d: 5 } }", + "- Expected + + Received + + Object { + "c": Object { + - "d": 5, + + "d": 4, + }, + }", + ] + `) + + // multiple nested properties + expect(getError(() => expect({ a: 1, b: 2, c: { d: 4 }, foo: { value: 'bar' }, bar: { value: 'foo' } }).toMatchObject({ c: { d: 5 }, foo: { value: 'biz' } }))).toMatchInlineSnapshot(` + [ + "expected { a: 1, b: 2, c: { d: 4 }, …(2) } to match object { c: { d: 5 }, foo: { value: 'biz' } }", + "- Expected + + Received + + Object { + "c": Object { + - "d": 5, + + "d": 4, + }, + "foo": Object { + - "value": "biz", + + "value": "bar", + }, + }", + ] + `) + + // property on root, nothing stripped + expect(getError(() => expect({ a: 1, b: 2, c: { d: 4 } }).toMatchObject({ a: 1, b: 3, c: { d: 4 } }))).toMatchInlineSnapshot(` + [ + "expected { a: 1, b: 2, c: { d: 4 } } to match object { a: 1, b: 3, c: { d: 4 } }", + "- Expected + + Received + + Object { + "a": 1, + - "b": 3, + + "b": 2, + "c": Object { + "d": 4, + }, + }", + ] + `) +}) + +it('toHaveProperty error diff', () => { + setupColors(getDefaultColors()) // non match value expect(getError(() => expect({ name: 'foo' }).toHaveProperty('name', 'bar'))).toMatchInlineSnapshot(` From 492d33e0b1f1403b394b8698352074ead895773b Mon Sep 17 00:00:00 2001 From: Christophe Geers Date: Sat, 16 Mar 2024 15:04:20 +0100 Subject: [PATCH 2/4] feat: display number of properties omitted from actual in diff for toMatchObject matcher --- .../expect/src/jest-asymmetric-matchers.ts | 2 +- packages/expect/src/jest-expect.ts | 24 +++-- packages/expect/src/jest-utils.ts | 93 +++++++++++-------- test/core/test/jest-expect.test.ts | 88 ++++++++++++++---- 4 files changed, 144 insertions(+), 63 deletions(-) diff --git a/packages/expect/src/jest-asymmetric-matchers.ts b/packages/expect/src/jest-asymmetric-matchers.ts index 76759c3b8164..52a4f40ce357 100644 --- a/packages/expect/src/jest-asymmetric-matchers.ts +++ b/packages/expect/src/jest-asymmetric-matchers.ts @@ -324,7 +324,7 @@ class CloseTo extends AsymmetricMatcher { return [ this.toString(), this.sample, - `(${pluralize('digit', this.precision)})`, + `(${this.precision} ${pluralize('digit', this.precision)})`, ].join(' ') } } diff --git a/packages/expect/src/jest-expect.ts b/packages/expect/src/jest-expect.ts index a06ffd968229..f9dec36f5108 100644 --- a/packages/expect/src/jest-expect.ts +++ b/packages/expect/src/jest-expect.ts @@ -4,7 +4,7 @@ import type { MockInstance } from '@vitest/spy' import { isMockFunction } from '@vitest/spy' import type { Test } from '@vitest/runner' import type { Assertion, ChaiPlugin } from './types' -import { arrayBufferEquality, generateToBeMessage, getObjectSubset, iterableEquality, equals as jestEquals, sparseArrayEquality, subsetEquality, typeEquality } from './jest-utils' +import { arrayBufferEquality, generateToBeMessage, getObjectSubset, iterableEquality, equals as jestEquals, pluralize, sparseArrayEquality, subsetEquality, typeEquality } from './jest-utils' import type { AsymmetricMatcher } from './jest-asymmetric-matchers' import { diff, getCustomEqualityTesters, stringify } from './jest-matcher-utils' import { JEST_MATCHERS_OBJECT } from './constants' @@ -161,13 +161,23 @@ export const JestChaiExpect: ChaiPlugin = (chai, utils) => { }) def('toMatchObject', function (expected) { const actual = this._obj - return this.assert( - jestEquals(actual, expected, [...customTesters, iterableEquality, subsetEquality]), - 'expected #{this} to match object #{exp}', - 'expected #{this} to not match object #{exp}', - expected, - getObjectSubset(actual, expected), + const pass = jestEquals(actual, expected, [...customTesters, iterableEquality, subsetEquality]) + const isNot = utils.flag(this, 'negate') as boolean + const { subset: actualSubset, stripped } = getObjectSubset(actual, expected) + const msg = utils.getMessage( + this, + [ + pass, + 'expected #{this} to match object #{exp}', + 'expected #{this} to not match object #{exp}', + expected, + actualSubset, + ], ) + if ((pass && isNot) || (!pass && !isNot)) { + const message = stripped === 0 ? msg : `${msg}\n(${stripped} more ${pluralize('property', stripped)} properties in actual)` + throw new AssertionError(message, { showDiff: true, expected, actual: actualSubset }) + } }) def('toMatch', function (expected: string | RegExp) { const actual = this._obj as string diff --git a/packages/expect/src/jest-utils.ts b/packages/expect/src/jest-utils.ts index 7b2ce37d9d3d..3db03eefbb32 100644 --- a/packages/expect/src/jest-utils.ts +++ b/packages/expect/src/jest-utils.ts @@ -537,8 +537,12 @@ export function generateToBeMessage(deepEqualityName: string, expected = '#{this return toBeMessage } +const IRREGULAR_PLURALS: { [key: string]: string } = { + property: 'properties', +} + export function pluralize(word: string, count: number): string { - return `${count} ${word}${count === 1 ? '' : 's'}` + return count === 1 ? word : IRREGULAR_PLURALS[word] ?? `${word}s` } export function getObjectKeys(object: object): Array { @@ -550,48 +554,59 @@ export function getObjectKeys(object: object): Array { ] } -export function getObjectSubset(object: any, subset: any, customTesters: Array = [], seenReferences: WeakMap = new WeakMap()): any { - if (Array.isArray(object)) { - if (Array.isArray(subset) && subset.length === object.length) { - // The map method returns correct subclass of subset. - return subset.map((sub: any, i: number) => - getObjectSubset(object[i], sub, customTesters), - ) +export function getObjectSubset(object: any, subset: any, customTesters: Array = []): { subset: any; stripped: number } { + let stripped = 0 + + const getObjectSubsetWithContext = (seenReferences: WeakMap = new WeakMap()) => (object: any, subset: any): any => { + if (Array.isArray(object)) { + if (Array.isArray(subset) && subset.length === object.length) { + // The map method returns correct subclass of subset. + return subset.map((sub: any, i: number) => + getObjectSubsetWithContext(seenReferences)(object[i], sub), + ) + } } - } - else if (object instanceof Date) { - return object - } - else if (isObject(object) && isObject(subset)) { - if ( - equals(object, subset, [ - ...customTesters, - iterableEquality, - subsetEquality, - ]) - ) { - // Avoid unnecessary copy which might return Object instead of subclass. - return subset + else if (object instanceof Date) { + return object } + else if (isObject(object) && isObject(subset)) { + if ( + equals(object, subset, [ + ...customTesters, + iterableEquality, + subsetEquality, + ]) + ) { + // Avoid unnecessary copy which might return Object instead of subclass. + return subset + } - const trimmed: any = {} - seenReferences.set(object, trimmed) - - for (const key of getObjectKeys(object).filter(key => - hasPropertyInObject(subset, key), - )) { - trimmed[key] = seenReferences.has(object[key]) - ? seenReferences.get(object[key]) - : getObjectSubset( - object[key], - subset[key], - customTesters, - seenReferences, - ) + const trimmed: any = {} + seenReferences.set(object, trimmed) + + for (const key of getObjectKeys(object)) { + if (hasPropertyInObject(subset, key)) { + trimmed[key] = seenReferences.has(object[key]) + ? seenReferences.get(object[key]) + : getObjectSubsetWithContext(seenReferences)(object[key], subset[key]) + } + else { + if (!seenReferences.has(object[key])) { + stripped += 1 + if (isObject(object[key])) + stripped += getObjectKeys(object[key]).length + + getObjectSubsetWithContext(seenReferences)(object[key], subset[key]) + } + } + } + + if (getObjectKeys(trimmed).length > 0) + return trimmed } - if (getObjectKeys(trimmed).length > 0) - return trimmed + return object } - return object + + return { subset: getObjectSubsetWithContext()(object, subset), stripped } } diff --git a/test/core/test/jest-expect.test.ts b/test/core/test/jest-expect.test.ts index afc6636547c3..6040b02a4100 100644 --- a/test/core/test/jest-expect.test.ts +++ b/test/core/test/jest-expect.test.ts @@ -922,10 +922,26 @@ function getError(f: () => unknown) { it('toMatchObject error diff', () => { setupColors(getDefaultColors()) - // single property on root + // single property on root (3 total properties, 1 expected) + expect(getError(() => expect({ a: 1, b: 2, c: 3 }).toMatchObject({ c: 4 }))).toMatchInlineSnapshot(` + [ + "expected { a: 1, b: 2, c: 3 } to match object { c: 4 } + (2 more properties properties in actual)", + "- Expected + + Received + + Object { + - "c": 4, + + "c": 3, + }", + ] + `) + + // single property on root (4 total properties, 1 expected) expect(getError(() => expect({ a: 1, b: 2, c: { d: 4 } }).toMatchObject({ b: 3 }))).toMatchInlineSnapshot(` [ - "expected { a: 1, b: 2, c: { d: 4 } } to match object { b: 3 }", + "expected { a: 1, b: 2, c: { d: 4 } } to match object { b: 3 } + (3 more properties properties in actual)", "- Expected + Received @@ -936,10 +952,11 @@ it('toMatchObject error diff', () => { ] `) - // nested property - expect(getError(() => expect({ a: 1, b: 2, c: { d: 4 } }).toMatchObject({ c: { d: 5 } }))).toMatchInlineSnapshot(` + // nested property (7 total properties, 2 expected) + expect(getError(() => expect({ a: 1, b: 2, c: { d: 4, e: 5 }, f: { g: 6 } }).toMatchObject({ c: { d: 5 } }))).toMatchInlineSnapshot(` [ - "expected { a: 1, b: 2, c: { d: 4 } } to match object { c: { d: 5 } }", + "expected { a: 1, b: 2, c: { d: 4, e: 5 }, …(1) } to match object { c: { d: 5 } } + (5 more properties properties in actual)", "- Expected + Received @@ -952,10 +969,45 @@ it('toMatchObject error diff', () => { ] `) - // multiple nested properties + // 3 total properties, 3 expected (0 stripped) + expect(getError(() => expect({ a: 1, b: 2, c: 3 }).toMatchObject({ a: 1, b: 2, c: 4 }))).toMatchInlineSnapshot(` + [ + "expected { a: 1, b: 2, c: 3 } to match object { a: 1, b: 2, c: 4 }", + "- Expected + + Received + + Object { + "a": 1, + "b": 2, + - "c": 4, + + "c": 3, + }", + ] + `) + + // 4 total properties, 3 expected + expect(getError(() => expect({ a: 1, b: 2, c: { d: 3 } }).toMatchObject({ a: 1, c: { d: 4 } }))).toMatchInlineSnapshot(` + [ + "expected { a: 1, b: 2, c: { d: 3 } } to match object { a: 1, c: { d: 4 } } + (1 more property properties in actual)", + "- Expected + + Received + + Object { + "a": 1, + "c": Object { + - "d": 4, + + "d": 3, + }, + }", + ] + `) + + // 8 total properties, 4 expected expect(getError(() => expect({ a: 1, b: 2, c: { d: 4 }, foo: { value: 'bar' }, bar: { value: 'foo' } }).toMatchObject({ c: { d: 5 }, foo: { value: 'biz' } }))).toMatchInlineSnapshot(` [ - "expected { a: 1, b: 2, c: { d: 4 }, …(2) } to match object { c: { d: 5 }, foo: { value: 'biz' } }", + "expected { a: 1, b: 2, c: { d: 4 }, …(2) } to match object { c: { d: 5 }, foo: { value: 'biz' } } + (4 more properties properties in actual)", "- Expected + Received @@ -972,20 +1024,24 @@ it('toMatchObject error diff', () => { ] `) - // property on root, nothing stripped - expect(getError(() => expect({ a: 1, b: 2, c: { d: 4 } }).toMatchObject({ a: 1, b: 3, c: { d: 4 } }))).toMatchInlineSnapshot(` + // 8 total properties, 3 expected + const characters = { firstName: 'Vladimir', lastName: 'Harkonnen', family: 'House Harkonnen', colors: ['red', 'blue'], children: [{ firstName: 'Jessica', lastName: 'Atreides', colors: ['red', 'green', 'black'] }] } + expect(getError(() => expect(characters).toMatchObject({ family: 'House Atreides', children: [{ firstName: 'Paul' }] }))).toMatchInlineSnapshot(` [ - "expected { a: 1, b: 2, c: { d: 4 } } to match object { a: 1, b: 3, c: { d: 4 } }", + "expected { firstName: 'Vladimir', …(4) } to match object { family: 'House Atreides', …(1) } + (5 more properties properties in actual)", "- Expected + Received Object { - "a": 1, - - "b": 3, - + "b": 2, - "c": Object { - "d": 4, - }, + "children": Array [ + Object { + - "firstName": "Paul", + + "firstName": "Jessica", + }, + ], + - "family": "House Atreides", + + "family": "House Harkonnen", }", ] `) From 725605f5998cc059ed07e27f8d239b6a7f0f6bb8 Mon Sep 17 00:00:00 2001 From: Christophe Geers Date: Sun, 7 Apr 2024 11:18:13 +0200 Subject: [PATCH 3/4] fix: fix assertion error message of toMatchObject --- packages/expect/src/jest-expect.ts | 2 +- test/core/test/jest-expect.test.ts | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/expect/src/jest-expect.ts b/packages/expect/src/jest-expect.ts index f9dec36f5108..807f3bd49acb 100644 --- a/packages/expect/src/jest-expect.ts +++ b/packages/expect/src/jest-expect.ts @@ -175,7 +175,7 @@ export const JestChaiExpect: ChaiPlugin = (chai, utils) => { ], ) if ((pass && isNot) || (!pass && !isNot)) { - const message = stripped === 0 ? msg : `${msg}\n(${stripped} more ${pluralize('property', stripped)} properties in actual)` + const message = stripped === 0 ? msg : `${msg}\n(${stripped} matching ${pluralize('property', stripped)} omitted from actual)` throw new AssertionError(message, { showDiff: true, expected, actual: actualSubset }) } }) diff --git a/test/core/test/jest-expect.test.ts b/test/core/test/jest-expect.test.ts index 6040b02a4100..b1b613cb63fb 100644 --- a/test/core/test/jest-expect.test.ts +++ b/test/core/test/jest-expect.test.ts @@ -926,7 +926,7 @@ it('toMatchObject error diff', () => { expect(getError(() => expect({ a: 1, b: 2, c: 3 }).toMatchObject({ c: 4 }))).toMatchInlineSnapshot(` [ "expected { a: 1, b: 2, c: 3 } to match object { c: 4 } - (2 more properties properties in actual)", + (2 matching properties omitted from actual)", "- Expected + Received @@ -941,7 +941,7 @@ it('toMatchObject error diff', () => { expect(getError(() => expect({ a: 1, b: 2, c: { d: 4 } }).toMatchObject({ b: 3 }))).toMatchInlineSnapshot(` [ "expected { a: 1, b: 2, c: { d: 4 } } to match object { b: 3 } - (3 more properties properties in actual)", + (3 matching properties omitted from actual)", "- Expected + Received @@ -956,7 +956,7 @@ it('toMatchObject error diff', () => { expect(getError(() => expect({ a: 1, b: 2, c: { d: 4, e: 5 }, f: { g: 6 } }).toMatchObject({ c: { d: 5 } }))).toMatchInlineSnapshot(` [ "expected { a: 1, b: 2, c: { d: 4, e: 5 }, …(1) } to match object { c: { d: 5 } } - (5 more properties properties in actual)", + (5 matching properties omitted from actual)", "- Expected + Received @@ -989,7 +989,7 @@ it('toMatchObject error diff', () => { expect(getError(() => expect({ a: 1, b: 2, c: { d: 3 } }).toMatchObject({ a: 1, c: { d: 4 } }))).toMatchInlineSnapshot(` [ "expected { a: 1, b: 2, c: { d: 3 } } to match object { a: 1, c: { d: 4 } } - (1 more property properties in actual)", + (1 matching property omitted from actual)", "- Expected + Received @@ -1007,7 +1007,7 @@ it('toMatchObject error diff', () => { expect(getError(() => expect({ a: 1, b: 2, c: { d: 4 }, foo: { value: 'bar' }, bar: { value: 'foo' } }).toMatchObject({ c: { d: 5 }, foo: { value: 'biz' } }))).toMatchInlineSnapshot(` [ "expected { a: 1, b: 2, c: { d: 4 }, …(2) } to match object { c: { d: 5 }, foo: { value: 'biz' } } - (4 more properties properties in actual)", + (4 matching properties omitted from actual)", "- Expected + Received @@ -1029,7 +1029,7 @@ it('toMatchObject error diff', () => { expect(getError(() => expect(characters).toMatchObject({ family: 'House Atreides', children: [{ firstName: 'Paul' }] }))).toMatchInlineSnapshot(` [ "expected { firstName: 'Vladimir', …(4) } to match object { family: 'House Atreides', …(1) } - (5 more properties properties in actual)", + (5 matching properties omitted from actual)", "- Expected + Received From 1b0edeefad02c3433cd83495c8be76e20a060149 Mon Sep 17 00:00:00 2001 From: Christophe Geers Date: Mon, 8 Apr 2024 11:17:40 +0200 Subject: [PATCH 4/4] refactor: revert breaking change to pluralize function --- packages/expect/src/jest-asymmetric-matchers.ts | 2 +- packages/expect/src/jest-expect.ts | 4 ++-- packages/expect/src/jest-utils.ts | 6 +----- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/packages/expect/src/jest-asymmetric-matchers.ts b/packages/expect/src/jest-asymmetric-matchers.ts index 52a4f40ce357..76759c3b8164 100644 --- a/packages/expect/src/jest-asymmetric-matchers.ts +++ b/packages/expect/src/jest-asymmetric-matchers.ts @@ -324,7 +324,7 @@ class CloseTo extends AsymmetricMatcher { return [ this.toString(), this.sample, - `(${this.precision} ${pluralize('digit', this.precision)})`, + `(${pluralize('digit', this.precision)})`, ].join(' ') } } diff --git a/packages/expect/src/jest-expect.ts b/packages/expect/src/jest-expect.ts index 807f3bd49acb..14d03771c12b 100644 --- a/packages/expect/src/jest-expect.ts +++ b/packages/expect/src/jest-expect.ts @@ -4,7 +4,7 @@ import type { MockInstance } from '@vitest/spy' import { isMockFunction } from '@vitest/spy' import type { Test } from '@vitest/runner' import type { Assertion, ChaiPlugin } from './types' -import { arrayBufferEquality, generateToBeMessage, getObjectSubset, iterableEquality, equals as jestEquals, pluralize, sparseArrayEquality, subsetEquality, typeEquality } from './jest-utils' +import { arrayBufferEquality, generateToBeMessage, getObjectSubset, iterableEquality, equals as jestEquals, sparseArrayEquality, subsetEquality, typeEquality } from './jest-utils' import type { AsymmetricMatcher } from './jest-asymmetric-matchers' import { diff, getCustomEqualityTesters, stringify } from './jest-matcher-utils' import { JEST_MATCHERS_OBJECT } from './constants' @@ -175,7 +175,7 @@ export const JestChaiExpect: ChaiPlugin = (chai, utils) => { ], ) if ((pass && isNot) || (!pass && !isNot)) { - const message = stripped === 0 ? msg : `${msg}\n(${stripped} matching ${pluralize('property', stripped)} omitted from actual)` + const message = stripped === 0 ? msg : `${msg}\n(${stripped} matching ${stripped === 1 ? 'property' : 'properties'} omitted from actual)` throw new AssertionError(message, { showDiff: true, expected, actual: actualSubset }) } }) diff --git a/packages/expect/src/jest-utils.ts b/packages/expect/src/jest-utils.ts index 3db03eefbb32..0d22002b0aac 100644 --- a/packages/expect/src/jest-utils.ts +++ b/packages/expect/src/jest-utils.ts @@ -537,12 +537,8 @@ export function generateToBeMessage(deepEqualityName: string, expected = '#{this return toBeMessage } -const IRREGULAR_PLURALS: { [key: string]: string } = { - property: 'properties', -} - export function pluralize(word: string, count: number): string { - return count === 1 ? word : IRREGULAR_PLURALS[word] ?? `${word}s` + return `${count} ${word}${count === 1 ? '' : 's'}` } export function getObjectKeys(object: object): Array {