Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions packages/expect/src/jest-extend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ function getMatcherState(
const obj = assertion._obj
const isNot = util.flag(assertion, 'negate') as boolean
const promise = util.flag(assertion, 'promise') || ''
const customMessage = util.flag(assertion, 'message') as string | undefined
const jestUtils = {
...getMatcherUtils(),
diff,
Expand All @@ -52,6 +53,7 @@ function getMatcherState(
state: matcherState,
isNot,
obj,
customMessage,
}
}

Expand All @@ -73,7 +75,7 @@ function JestExtendPlugin(
this: Chai.AssertionStatic & Chai.Assertion,
...args: any[]
) {
const { state, isNot, obj } = getMatcherState(this, expect)
const { state, isNot, obj, customMessage } = getMatcherState(this, expect)

const result = expectAssertion.call(state, obj, ...args)

Expand All @@ -85,15 +87,21 @@ function JestExtendPlugin(
const thenable = result as PromiseLike<SyncExpectationResult>
return thenable.then(({ pass, message, actual, expected }) => {
if ((pass && isNot) || (!pass && !isNot)) {
throw new JestExtendError(message(), actual, expected)
const errorMessage = customMessage != null
? customMessage
: message()
throw new JestExtendError(errorMessage, actual, expected)
}
})
}

const { pass, message, actual, expected } = result as SyncExpectationResult

if ((pass && isNot) || (!pass && !isNot)) {
throw new JestExtendError(message(), actual, expected)
const errorMessage = customMessage != null
? customMessage
: message()
throw new JestExtendError(errorMessage, actual, expected)
}
}

Expand Down
88 changes: 88 additions & 0 deletions test/core/test/expect.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -389,3 +389,91 @@ describe('Temporal equality', () => {
})
})
})

describe('expect with custom message', () => {
describe('built-in matchers', () => {
test('sync matcher throws custom message on failure', () => {
expect(() => expect(1, 'custom message').toBe(2)).toThrow('custom message')
})

test('async rejects matcher throws custom message on failure', async ({ expect }) => {
const asyncAssertion = expect(Promise.reject(new Error('test error')), 'custom async message').rejects.toBe(2)
await expect(asyncAssertion).rejects.toThrow('custom async message')
})

test('async resolves matcher throws custom message on failure', async ({ expect }) => {
const asyncAssertion = expect(Promise.resolve(1), 'custom async message').resolves.toBe(2)
await expect(asyncAssertion).rejects.toThrow('custom async message')
})

test('not matcher throws custom message on failure', () => {
expect(() => expect(1, 'custom message').not.toBe(1)).toThrow('custom message')
})
})

describe('custom matchers with expect.extend', () => {
test('sync custom matcher throws custom message on failure', ({ expect }) => {
expect.extend({
toBeFoo(actual) {
const { isNot } = this
return {
pass: actual === 'foo',
message: () => `${actual} is${isNot ? ' not' : ''} foo`,
}
},
})
expect(() => (expect('bar', 'custom message') as any).toBeFoo()).toThrow('custom message')
})

test('sync custom matcher passes with custom message when assertion succeeds', ({ expect }) => {
expect.extend({
toBeFoo(actual) {
const { isNot } = this
return {
pass: actual === 'foo',
message: () => `${actual} is${isNot ? ' not' : ''} foo`,
}
},
})
expect(() => (expect('foo', 'custom message') as any).toBeFoo()).not.toThrow()
})

test('async custom matcher throws custom message on failure', async ({ expect }) => {
expect.extend({
async toBeFoo(actual) {
const resolvedValue = await actual
return {
pass: resolvedValue === 'foo',
message: () => `${resolvedValue} is not foo`,
}
},
})
const asyncAssertion = (expect(Promise.resolve('bar'), 'custom async message') as any).toBeFoo()
await expect(asyncAssertion).rejects.toThrow('custom async message')
})

test('async custom matcher with not throws custom message on failure', async ({ expect }) => {
expect.extend({
async toBeFoo(actual) {
const resolvedValue = await actual
return {
pass: resolvedValue === 'foo',
message: () => `${resolvedValue} is not foo`,
}
},
})
const asyncAssertion = (expect(Promise.resolve('foo'), 'custom async message') as any).not.toBeFoo()
await expect(asyncAssertion).rejects.toThrow('custom async message')
})
})

describe('edge cases', () => {
test('empty custom message falls back to default matcher message', () => {
expect(() => expect(1, '').toBe(2)).toThrow('expected 1 to be 2 // Object.is equality')
})

test('undefined custom message falls back to default matcher message', () => {
expect(() => expect(1, undefined as any).toBe(2)).toThrow('expected 1 to be 2 // Object.is equality')
})
})
})
Loading