diff --git a/docs/api/expect.md b/docs/api/expect.md
index 3ae4f95a7..c5e022046 100644
--- a/docs/api/expect.md
+++ b/docs/api/expect.md
@@ -1559,23 +1559,19 @@ test('spy nth called with', () => {
## returned 4.1.0 {#returned}
-- **Type:** `Assertion` (property, not a method)
-
-Chai-style assertion that checks if a spy returned successfully at least once. This is equivalent to `toHaveReturned()`.
+- **Type:** `(value: any) => void`
-::: tip
-This is a property assertion following sinon-chai conventions. Access it without parentheses: `expect(spy).to.have.returned`
-:::
+Chai-style assertion that checks if a spy returned a specific value at least once. This is equivalent to `toHaveReturnedWith(value)`.
```ts
import { expect, test, vi } from 'vitest'
test('spy returned', () => {
- const spy = vi.fn(() => 'result')
+ const spy = vi.fn(() => 'value')
spy()
- expect(spy).to.have.returned
+ expect(spy).to.have.returned('value')
})
```
diff --git a/docs/guide/migration.md b/docs/guide/migration.md
index 9e88e89e8..e5742cc8b 100644
--- a/docs/guide/migration.md
+++ b/docs/guide/migration.md
@@ -742,8 +742,7 @@ Vitest supports all common sinon-chai assertions:
| `spy.callCount(n)` | `callCount(n)` | Spy was called n times |
| `spy.calledWith(...)` | `calledWith(...)` | Spy was called with specific args |
| `spy.calledOnceWith(...)` | `calledOnceWith(...)` | Spy was called once with specific args |
-| `spy.returned` | `returned` | Spy returned successfully |
-| `spy.returnedWith(value)` | `returnedWith(value)` | Spy returned specific value |
+| `spy.returned(value)` | `returned` | Spy returned specific value |
See the [Chai-Style Spy Assertions](/api/expect#chai-style-spy-assertions) documentation for the complete list.
diff --git a/packages/expect/src/chai-style-assertions.ts b/packages/expect/src/chai-style-assertions.ts
index 85a1749a4..e9fbe39c4 100644
--- a/packages/expect/src/chai-style-assertions.ts
+++ b/packages/expect/src/chai-style-assertions.ts
@@ -44,7 +44,7 @@ export const ChaiStyleAssertions: ChaiPlugin = (chai, utils) => {
name: keyof Assertion,
delegateTo: keyof Assertion,
) {
- utils.addChainableMethod(
+ utils.addMethod(
chai.Assertion.prototype,
name,
function (this: Chai.AssertionStatic & Assertion, ...args: any[]) {
@@ -56,13 +56,13 @@ export const ChaiStyleAssertions: ChaiPlugin = (chai, utils) => {
}
return jestMethod.call(this, ...args)
},
- () => {},
)
}
+ // API to (somewhat) mirror sinon-chai
+ // https://github.com/chaijs/sinon-chai
defProperty('called', 'toHaveBeenCalled')
defProperty('calledOnce', 'toHaveBeenCalledOnce')
- defProperty('returned', 'toHaveReturned')
defPropertyWithArgs('calledTwice', 'toHaveBeenCalledTimes', 2)
defPropertyWithArgs('calledThrice', 'toHaveBeenCalledTimes', 3)
@@ -71,10 +71,14 @@ export const ChaiStyleAssertions: ChaiPlugin = (chai, utils) => {
defMethod('calledOnceWith', 'toHaveBeenCalledExactlyOnceWith')
defMethod('lastCalledWith', 'toHaveBeenLastCalledWith')
defMethod('nthCalledWith', 'toHaveBeenNthCalledWith')
+ defMethod('returned', 'toHaveReturned')
defMethod('returnedWith', 'toHaveReturnedWith')
defMethod('returnedTimes', 'toHaveReturnedTimes')
defMethod('lastReturnedWith', 'toHaveLastReturnedWith')
defMethod('nthReturnedWith', 'toHaveNthReturnedWith')
defMethod('calledBefore', 'toHaveBeenCalledBefore')
defMethod('calledAfter', 'toHaveBeenCalledAfter')
+
+ // TODO: implement
+ // defMethod('thrown', 'toHaveThrown')
}
diff --git a/packages/expect/src/types.ts b/packages/expect/src/types.ts
index 7f84cbe69..b5a7864a9 100644
--- a/packages/expect/src/types.ts
+++ b/packages/expect/src/types.ts
@@ -836,13 +836,13 @@ export interface ChaiMockAssertion {
nthCalledWith: (n: number, ...args: E) => void
/**
- * Checks that a spy returned successfully at least once.
- * Chai-style equivalent of `toHaveReturned`.
+ * Checks that a spy returned a specific value at least once.
+ * Chai-style equivalent of `toHaveReturnedWith`.
*
* @example
- * expect(spy).to.have.returned
+ * expect(spy).to.have.returned('value')
*/
- readonly returned: Assertion
+ returned: (value: E) => void
/**
* Checks that a spy returned a specific value at least once.
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index bd29eac78..2fbfbbbd5 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -108,6 +108,12 @@ catalogs:
playwright:
specifier: ^1.58.2
version: 1.58.2
+ sinon:
+ specifier: ^21.0.3
+ version: 21.0.3
+ sinon-chai:
+ specifier: ^4.0.1
+ version: 4.0.1
sirv:
specifier: ^3.0.2
version: 3.0.2
@@ -1512,6 +1518,12 @@ importers:
react-18:
specifier: npm:react@18.3.1
version: react@18.3.1
+ sinon:
+ specifier: 'catalog:'
+ version: 21.0.3
+ sinon-chai:
+ specifier: 'catalog:'
+ version: 4.0.1(chai@6.2.2)(sinon@21.0.3)
temporal-polyfill:
specifier: ~0.3.0
version: 0.3.0
@@ -4971,6 +4983,12 @@ packages:
'@sinonjs/fake-timers@15.0.0':
resolution: {integrity: sha512-dlUB2oL+hDIYkIq/OWFBDhQAuU6kDey3eeMiYpVb7UXHhkMq/r1HloKXAbJwJZpYWkFWsydLjMqDpueMUEOjXQ==}
+ '@sinonjs/fake-timers@15.1.1':
+ resolution: {integrity: sha512-cO5W33JgAPbOh07tvZjUOJ7oWhtaqGHiZw+11DPbyqh2kHTBc3eF/CjJDeQ4205RLQsX6rxCuYOroFQwl7JDRw==}
+
+ '@sinonjs/samsam@9.0.3':
+ resolution: {integrity: sha512-ZgYY7Dc2RW+OUdnZ1DEHg00lhRt+9BjymPKHog4PRFzr1U3MbK57+djmscWyKxzO1qfunHqs4N45WWyKIFKpiQ==}
+
'@standard-schema/spec@1.1.0':
resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
@@ -6639,6 +6657,10 @@ packages:
resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
+ diff@8.0.3:
+ resolution: {integrity: sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ==}
+ engines: {node: '>=0.3.1'}
+
dom-accessibility-api@0.5.16:
resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==}
@@ -9502,6 +9524,15 @@ packages:
simple-swizzle@0.2.2:
resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==}
+ sinon-chai@4.0.1:
+ resolution: {integrity: sha512-xMKEEV3cYHC1G+boyr7QEqi80gHznYsxVdC9CdjP5JnCWz/jPGuXQzJz3PtBcb0CcHAxar15Y5sjLBoAs6a0yA==}
+ peerDependencies:
+ chai: ^5.0.0 || ^6.0.0
+ sinon: '>=4.0.0'
+
+ sinon@21.0.3:
+ resolution: {integrity: sha512-0x8TQFr8EjADhSME01u1ZK31yv2+bd6Z5NrBCHVM+n4qL1wFqbxftmeyi3bwlr49FbbzRfrqSFOpyHCOh/YmYA==}
+
sirv@3.0.2:
resolution: {integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==}
engines: {node: '>=18'}
@@ -9952,6 +9983,10 @@ packages:
resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==}
engines: {node: '>=4'}
+ type-detect@4.1.0:
+ resolution: {integrity: sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==}
+ engines: {node: '>=4'}
+
type-fest@0.16.0:
resolution: {integrity: sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg==}
engines: {node: '>=10'}
@@ -13416,6 +13451,15 @@ snapshots:
dependencies:
'@sinonjs/commons': 3.0.1
+ '@sinonjs/fake-timers@15.1.1':
+ dependencies:
+ '@sinonjs/commons': 3.0.1
+
+ '@sinonjs/samsam@9.0.3':
+ dependencies:
+ '@sinonjs/commons': 3.0.1
+ type-detect: 4.1.0
+
'@standard-schema/spec@1.1.0': {}
'@stylistic/eslint-plugin@5.9.0(eslint@10.0.3(jiti@2.6.1))':
@@ -15276,6 +15320,8 @@ snapshots:
diff-sequences@29.6.3: {}
+ diff@8.0.3: {}
+
dom-accessibility-api@0.5.16: {}
dom-accessibility-api@0.6.3: {}
@@ -18839,6 +18885,19 @@ snapshots:
dependencies:
is-arrayish: 0.3.2
+ sinon-chai@4.0.1(chai@6.2.2)(sinon@21.0.3):
+ dependencies:
+ chai: 6.2.2
+ sinon: 21.0.3
+
+ sinon@21.0.3:
+ dependencies:
+ '@sinonjs/commons': 3.0.1
+ '@sinonjs/fake-timers': 15.1.1
+ '@sinonjs/samsam': 9.0.3
+ diff: 8.0.3
+ supports-color: 7.2.0
+
sirv@3.0.2:
dependencies:
'@polka/url': 1.0.0-next.24
@@ -19317,6 +19376,8 @@ snapshots:
type-detect@4.0.8: {}
+ type-detect@4.1.0: {}
+
type-fest@0.16.0: {}
type-fest@2.13.0: {}
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
index 2264f9ae9..5f317b55a 100644
--- a/pnpm-workspace.yaml
+++ b/pnpm-workspace.yaml
@@ -75,6 +75,8 @@ catalog:
obug: ^2.1.1
pathe: ^2.0.3
playwright: ^1.58.2
+ sinon: ^21.0.3
+ sinon-chai: ^4.0.1
sirv: ^3.0.2
std-env: ^4.0.0-rc.1
strip-literal: ^3.1.0
diff --git a/test/core/package.json b/test/core/package.json
index ff850bb52..75d99e282 100644
--- a/test/core/package.json
+++ b/test/core/package.json
@@ -38,6 +38,8 @@
"obug": "^2.1.1",
"react": "^19.2.4",
"react-18": "npm:react@18.3.1",
+ "sinon": "catalog:",
+ "sinon-chai": "catalog:",
"temporal-polyfill": "~0.3.0",
"tinyrainbow": "catalog:",
"tinyspy": "^4.0.4",
diff --git a/test/core/test/chai-style-assertions-sinon.test.ts b/test/core/test/chai-style-assertions-sinon.test.ts
new file mode 100644
index 000000000..966a7f000
--- /dev/null
+++ b/test/core/test/chai-style-assertions-sinon.test.ts
@@ -0,0 +1,181 @@
+/* eslint-disable ts/no-unused-expressions */
+// @ts-expect-error no type override otherwise vitest ones broken
+import sinon from 'sinon'
+// @ts-expect-error same
+import sinonChai from 'sinon-chai'
+import { chai, describe, expect, it } from 'vitest'
+
+chai.use(sinonChai)
+
+describe('sinon-chai assertions', () => {
+ describe('called', () => {
+ it('passes when spy was called', () => {
+ const spy = sinon.spy()
+ spy()
+ expect(spy).to.have.been.called
+ })
+
+ it('negated: passes when spy was not called', () => {
+ const spy = sinon.spy()
+ expect(spy).to.not.have.been.called
+ })
+ })
+
+ describe('callCount', () => {
+ it('passes when spy was called exact number of times', () => {
+ const spy = sinon.spy()
+ spy()
+ spy()
+ spy()
+ expect(spy).to.have.callCount(3)
+ })
+
+ it('negated: passes when spy was called different number of times', () => {
+ const spy = sinon.spy()
+ spy()
+ expect(spy).to.not.have.callCount(3)
+ })
+ })
+
+ describe('calledWith', () => {
+ it('passes when spy was called with specific arguments', () => {
+ const spy = sinon.spy()
+ spy('arg1', 'arg2')
+ expect(spy).to.have.been.calledWith('arg1', 'arg2')
+ })
+
+ it('passes when spy was called with arguments among other calls', () => {
+ const spy = sinon.spy()
+ spy('other')
+ spy('arg1', 'arg2')
+ spy('another')
+ expect(spy).to.have.been.calledWith('arg1', 'arg2')
+ })
+
+ it('negated: passes when spy was not called with specific arguments', () => {
+ const spy = sinon.spy()
+ spy('other', 'args')
+ expect(spy).to.not.have.been.calledWith('arg1', 'arg2')
+ })
+ })
+
+ describe('calledOnce', () => {
+ it('passes when spy was called exactly once', () => {
+ const spy = sinon.spy()
+ spy()
+ expect(spy).to.have.been.calledOnce
+ })
+
+ it('negated: passes when spy was not called once', () => {
+ const spy = sinon.spy()
+ spy()
+ spy()
+ expect(spy).to.not.have.been.calledOnce
+ })
+ })
+
+ describe('calledOnceWith', () => {
+ it('passes when spy was called exactly once with specific arguments', () => {
+ const spy = sinon.spy()
+ spy('arg1', 'arg2')
+ expect(spy).to.have.been.calledOnceWith('arg1', 'arg2')
+ })
+
+ it('negated: passes when spy was not called once with specific arguments', () => {
+ const spy = sinon.spy()
+ spy('arg1', 'arg2')
+ spy('arg1', 'arg2')
+ expect(spy).to.not.have.been.calledOnceWith('arg1', 'arg2')
+ })
+ })
+
+ describe('calledTwice', () => {
+ it('passes when spy was called exactly twice', () => {
+ const spy = sinon.spy()
+ spy()
+ spy()
+ expect(spy).to.have.been.calledTwice
+ })
+
+ it('negated: passes when spy was not called twice', () => {
+ const spy = sinon.spy()
+ spy()
+ expect(spy).to.not.have.been.calledTwice
+ })
+ })
+
+ describe('calledThrice', () => {
+ it('passes when spy was called exactly three times', () => {
+ const spy = sinon.spy()
+ spy()
+ spy()
+ spy()
+ expect(spy).to.have.been.calledThrice
+ })
+
+ it('negated: passes when spy was not called three times', () => {
+ const spy = sinon.spy()
+ spy()
+ spy()
+ expect(spy).to.not.have.been.calledThrice
+ })
+ })
+
+ describe('calledBefore / calledAfter', () => {
+ it('passes when spy was called before another spy', () => {
+ const spy1 = sinon.spy()
+ const spy2 = sinon.spy()
+ spy1()
+ spy2()
+ expect(spy1).to.have.been.calledBefore(spy2)
+ })
+
+ it('passes when spy was called after another spy', () => {
+ const spy1 = sinon.spy()
+ const spy2 = sinon.spy()
+ spy1()
+ spy2()
+ expect(spy2).to.have.been.calledAfter(spy1)
+ })
+ })
+
+ describe('returned', () => {
+ it('passes when spy returned specific value', () => {
+ const spy = sinon.spy(() => 'value')
+ spy()
+ expect(spy).to.have.returned('value')
+ })
+
+ it('passes with no arguments (checks for undefined)', () => {
+ const spy = sinon.spy(() => {})
+ spy()
+ expect(spy).to.have.returned(undefined)
+ })
+
+ it('negated: passes when spy did not return specific value', () => {
+ const spy = sinon.spy(() => 'other')
+ spy()
+ expect(spy).to.not.have.returned('value')
+ })
+ })
+
+ // TODO: implement `thrown`
+ // describe('thrown', () => {
+ // it('passes when spy threw', () => {
+ // const spy = sinon.spy(() => {
+ // throw new Error('test error')
+ // })
+ // try {
+ // spy()
+ // }
+ // catch {}
+ // expect(spy).to.have.thrown()
+ // })
+
+ // it('negated: passes when spy did not throw', () => {
+ // const spy = sinon.spy(() => 'value')
+ // spy()
+ // expect(spy).to.not.have.thrown()
+ // })
+ // })
+})
diff --git a/test/core/test/chai-style-assertions.test.ts b/test/core/test/chai-style-assertions.test.ts
index aecdb9ad3..241a838af 100644
--- a/test/core/test/chai-style-assertions.test.ts
+++ b/test/core/test/chai-style-assertions.test.ts
@@ -206,7 +206,7 @@ describe('Chai-style assertions', () => {
it('passes when spy returned successfully', () => {
const spy = vi.fn(() => 'value')
spy()
- expect(spy).to.have.returned
+ expect(spy).to.have.returned('value')
})
it('fails when spy threw an error', () => {
@@ -218,7 +218,7 @@ describe('Chai-style assertions', () => {
}
catch {}
expect(() => {
- expect(spy).to.have.returned
+ expect(spy).to.have.returned('value')
}).toThrow(/expected "testSpy" to be successfully called at least once/)
})