From 3dc13f7cd38a545726a7bdd7b12f06d43ef981de Mon Sep 17 00:00:00 2001 From: Jose Luis Leon Date: Tue, 29 Aug 2023 12:51:30 -0500 Subject: [PATCH 1/2] feat(sinon): Add @assertive-ts/sinon plugin --- examples/jest/package.json | 2 +- examples/mocha/package.json | 2 +- package.json | 6 +- packages/README.md | 107 ++++ packages/core/package.json | 4 +- packages/sinon/.mocharc.json | 9 + packages/sinon/.releaserc.json | 15 + packages/sinon/package.json | 71 +++ packages/sinon/src/lib/SinonSpyAssertion.ts | 350 +++++++++++++ .../sinon/src/lib/SinonSpyCallAssertion.ts | 172 +++++++ packages/sinon/src/lib/helpers/messages.ts | 20 + packages/sinon/src/main.ts | 59 +++ packages/sinon/test/helpers/common.ts | 12 + packages/sinon/test/hooks.ts | 9 + .../test/unit/lib/SinonSpyAssertion.test.ts | 478 ++++++++++++++++++ .../unit/lib/SinonSpyCallAssertion.test.ts | 266 ++++++++++ .../test/unit/lib/helpers/messages.test.ts | 79 +++ packages/sinon/test/unit/main.test.ts | 15 + packages/sinon/tsconfig.json | 17 + packages/sinon/tsconfig.prod.json | 11 + packages/sinon/typedoc.json | 28 + yarn.lock | 211 +++++--- 22 files changed, 1858 insertions(+), 85 deletions(-) create mode 100644 packages/README.md create mode 100644 packages/sinon/.mocharc.json create mode 100644 packages/sinon/.releaserc.json create mode 100644 packages/sinon/package.json create mode 100644 packages/sinon/src/lib/SinonSpyAssertion.ts create mode 100644 packages/sinon/src/lib/SinonSpyCallAssertion.ts create mode 100644 packages/sinon/src/lib/helpers/messages.ts create mode 100644 packages/sinon/src/main.ts create mode 100644 packages/sinon/test/helpers/common.ts create mode 100644 packages/sinon/test/hooks.ts create mode 100644 packages/sinon/test/unit/lib/SinonSpyAssertion.test.ts create mode 100644 packages/sinon/test/unit/lib/SinonSpyCallAssertion.test.ts create mode 100644 packages/sinon/test/unit/lib/helpers/messages.test.ts create mode 100644 packages/sinon/test/unit/main.test.ts create mode 100644 packages/sinon/tsconfig.json create mode 100644 packages/sinon/tsconfig.prod.json create mode 100644 packages/sinon/typedoc.json diff --git a/examples/jest/package.json b/examples/jest/package.json index 529911c..e68c43d 100644 --- a/examples/jest/package.json +++ b/examples/jest/package.json @@ -9,7 +9,7 @@ "@assertive-ts/core": "workspace:^", "@examples/symbol-plugin": "workspace:^", "@types/jest": "^29.5.11", - "@types/node": "^20.10.5", + "@types/node": "^20.10.6", "jest": "^29.7.0", "ts-jest": "^29.1.1", "ts-node": "^10.9.2", diff --git a/examples/mocha/package.json b/examples/mocha/package.json index 4504f92..0c0d8ab 100644 --- a/examples/mocha/package.json +++ b/examples/mocha/package.json @@ -9,7 +9,7 @@ "@assertive-ts/core": "workspace:^", "@examples/symbol-plugin": "workspace:^", "@types/mocha": "^10.0.6", - "@types/node": "^20.10.5", + "@types/node": "^20.10.6", "mocha": "^10.2.0", "ts-node": "^10.9.2", "typescript": "^5.3.3" diff --git a/package.json b/package.json index ad64660..bbe462f 100644 --- a/package.json +++ b/package.json @@ -23,13 +23,13 @@ "test": "turbo run test" }, "devDependencies": { - "@typescript-eslint/eslint-plugin": "^6.15.0", - "@typescript-eslint/parser": "^6.15.0", + "@typescript-eslint/eslint-plugin": "^6.17.0", + "@typescript-eslint/parser": "^6.17.0", "eslint": "^8.56.0", "eslint-import-resolver-typescript": "^3.6.1", "eslint-plugin-etc": "^2.0.3", "eslint-plugin-import": "^2.29.1", - "eslint-plugin-jsdoc": "^46.9.1", + "eslint-plugin-jsdoc": "^48.0.2", "eslint-plugin-sonarjs": "^0.23.0", "turbo": "^1.11.2", "typescript": "^5.3.3" diff --git a/packages/README.md b/packages/README.md new file mode 100644 index 0000000..6a0f255 --- /dev/null +++ b/packages/README.md @@ -0,0 +1,107 @@ +# Assertive.ts Sinon + +An Assertive.ts plugin to match over [Sinon.js](https://sinonjs.org/) spies, stubs, mocks, and fakes. + +## Requirements + +- **@assertive-ts/core:** >=2.0.0 +- **sinon:** >=15.2.0 + +## Install + +```sh +npm install --save-dev @assertive-ts/sinon +``` + +Or: + +```sh +yarn add --dev @assertive-ts/sinon +``` + +## API Reference + +You can find the full API reference [here](https://stackbuilders.github.io/assertive-ts/docs/sinon/build/) 📚 + +## Usage + +You just need to load the plugin in a file that runs before the tests execution, like a `setup.ts` or something like that: + +```ts +// setup.ts + +import { usePlugin } from "@assertive-ts/core"; +import { SinonPlugin } from "@assertive-ts/sinon"; + +usePlugin(SinonPlugin); + +// ...rest of your setup +``` + +And that's it! The extra types will be automatically added as well so you won't need to change anything on TypeScript's side. Now you can use the `expect(..)` function to assert over Sinon spies, stubs, mocks, and fakes. + +```ts +import { expect } from "@assertive-ts/core"; +import Sinon from "sinon"; + +const spy = Sinon.spy(launchRockets); + +expect(spy).toBeCalled(3); // exactly 3 times + +expect(spy).toBeCalledTwice(); + +expect(spy).toBeCalledAtLeast(2); + +expect(spy).toBeCalledAtMost(3); + +expect(spy).toHaveArgs(10, "long-range"); + +expect(spy).toThrow(); +``` + +The assertion above act over any of the calls made to the spy. You can get more specific matchers if you assert over a single spy call: + +```ts +import { expect } from "@assertive-ts/core"; +import Sinon from "sinon"; + +const spy = Sinon.spy(launchRockets); + +expect(spy.firstCall).toHaveArgs(5, "short-range"); + +expect(spy.firstCall).toReturn({ status: "ok" }); + +expect(spy.firstCall) // more specific matchers over a single call + .toThrowError(InvarianError) + .toHaveMessage("Something went wrong..."); + +// or... + +expect(spy) + .call(1) + .toThrowError(InvarianError) + .toHaveMessage("Something went wrong..."); + +// or even better... + +expect(spy) + .toBeCalledOnce() + .toThrowError(InvarianError) + .toHaveMessage("Something went wrong..."); +``` + +Notice how `get(..)` and `.toBeCalledOnce()` methods return an assertion over the single call, this way you can chain matchers instead of writing more statements. + +## License + +MIT, see [the LICENSE file](https://github.com/stackbuilders/assertive-ts/blob/main/packages/sinon/LICENSE). + +## Contributing + +Do you want to contribute to this project? Please take a look at our [contributing guideline](https://github.com/stackbuilders/assertive-ts/blob/main/docs/CONTRIBUTING.md) to know how you can help us build it. + +--- + +Stack Builders + +[Check out our libraries](https://github.com/stackbuilders/) | [Join our team](https://www.stackbuilders.com/join-us/) diff --git a/packages/core/package.json b/packages/core/package.json index 8f1cb55..2543018 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -37,7 +37,7 @@ }, "devDependencies": { "@types/mocha": "^10.0.6", - "@types/node": "^20.10.5", + "@types/node": "^20.10.6", "@types/sinon": "^17.0.2", "all-contributors-cli": "^6.26.1", "mocha": "^10.2.0", @@ -45,7 +45,7 @@ "semantic-release-yarn": "^3.0.2", "sinon": "^17.0.1", "ts-node": "^10.9.2", - "typedoc": "^0.25.4", + "typedoc": "^0.25.6", "typedoc-plugin-markdown": "^3.17.1", "typedoc-plugin-merge-modules": "^5.1.0", "typescript": "^5.3.3" diff --git a/packages/sinon/.mocharc.json b/packages/sinon/.mocharc.json new file mode 100644 index 0000000..d4af0c9 --- /dev/null +++ b/packages/sinon/.mocharc.json @@ -0,0 +1,9 @@ +{ + "$schema": "https://json.schemastore.org/mocharc", + "extension": ["ts"], + "recursive": true, + "require": [ + "ts-node/register", + "test/hooks.ts" + ] +} diff --git a/packages/sinon/.releaserc.json b/packages/sinon/.releaserc.json new file mode 100644 index 0000000..d428a59 --- /dev/null +++ b/packages/sinon/.releaserc.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://json.schemastore.org/semantic-release", + "branches": [ + "main" + ], + "plugins": [ + ["@semantic-release/commit-analyzer", { + "releaseRules": [{ "scope": "!sinon", "release": false }] + }], + "@semantic-release/release-notes-generator", + "semantic-release-yarn", + "@semantic-release/github" + ], + "tagFormat": "sinon/v${version}" +} diff --git a/packages/sinon/package.json b/packages/sinon/package.json new file mode 100644 index 0000000..4f51d1c --- /dev/null +++ b/packages/sinon/package.json @@ -0,0 +1,71 @@ +{ + "name": "@assertive-ts/sinon", + "version": "0.0.0", + "description": "Assertive.ts plugin for Sinon assertions", + "repository": "git@github.com:stackbuilders/assertive-ts.git", + "homepage": "https://stackbuilders.github.io/assertive-ts/", + "author": "Stack Builders ", + "license": "MIT", + "keywords": [ + "assertions", + "assertive-ts", + "testing", + "testing-tools", + "type-safety", + "typescript", + "sinon", + "plugin" + ], + "main": "./dist/main.js", + "types": "./dist/main.d.ts", + "files": [ + "dist/", + "src/" + ], + "engines": { + "node": ">=16" + }, + "packageManager": "yarn@3.6.1", + "scripts": { + "build": "tsc -p tsconfig.prod.json", + "check": "yarn compile && yarn test --forbid-only", + "compile": "tsc -p tsconfig.json", + "docs": "typedoc", + "release": "semantic-release", + "test": "NODE_ENV=test mocha" + }, + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "devDependencies": { + "@assertive-ts/core": "workspace:^", + "@types/mocha": "^10.0.6", + "@types/node": "^20.10.6", + "@types/sinon": "^17.0.2", + "mocha": "^10.2.0", + "semantic-release": "^22.0.12", + "semantic-release-yarn": "^3.0.2", + "sinon": "^17.0.1", + "ts-node": "^10.9.2", + "typedoc": "^0.25.7", + "typedoc-plugin-markdown": "^3.17.1", + "typedoc-plugin-merge-modules": "^5.1.0", + "typescript": "^5.3.3" + }, + "peerDependencies": { + "@assertive-ts/core": ">=2.0.0", + "sinon": ">=15.2.0" + }, + "peerDependenciesMeta": { + "@assertive-ts/core": { + "optional": false + }, + "sinon": { + "optional": true + } + }, + "publishConfig": { + "access": "public", + "provenance": true + } +} diff --git a/packages/sinon/src/lib/SinonSpyAssertion.ts b/packages/sinon/src/lib/SinonSpyAssertion.ts new file mode 100644 index 0000000..1bc3188 --- /dev/null +++ b/packages/sinon/src/lib/SinonSpyAssertion.ts @@ -0,0 +1,350 @@ +import { Assertion, AssertionError } from "@assertive-ts/core"; +import isDeepEqual from "fast-deep-equal"; +import { SinonSpy } from "sinon"; + +import { SinonSpyCallAssertion } from "./SinonSpyCallAssertion"; +import { callTimes, numeral, prettify } from "./helpers/messages"; + +/** + * Encapsulates assertion methods applicable to {@link SinonSpy} instances. + * This includes `Sinon.spy(..)`, `Sinon.stub(..)`, `Sinon.mock()` and + * `Sinon.fake(..)` as all of them extend from a SinonSpy. + * + * @param A the arguments type of the spied function + * @param R the type return type of the spied function + */ +export class SinonSpyAssertion extends Assertion> { + + public constructor(actual: SinonSpy) { + super(actual); + } + + /** + * Check if the spy was called exactly a number of times. If the argument is + * omited it defaults to one. + * + * @example + * ``` + * const spy = Sinon.spy(..); // .stub(..) / .mock(..) / .fake(..) + * + * expect(spy).tobeCalled(); // exactly once + * expect(spy).toBeCalled(3); // exacty 3 times + * ``` + * + * @param times the number of times the spy is called. Defaults to `1`. + * @returns the assertion instance + */ + public toBeCalled(times: number = 1): this { + if (times < 0) { + throw new Error("Spy cannot be called less tha zero times!"); + } + + const { name, callCount } = this.actual; + const error = new AssertionError({ + actual: callCount, + expected: times, + message: `Expected <${name}> to be called ${numeral(times)}, but it was ${callTimes(callCount)}`, + }); + const invertedError = new AssertionError({ + actual: this.actual, + message: `Expected <${name}> NOT to be called ${numeral(times)}, but it was ${callTimes(callCount)}`, + }); + + return this.execute({ + assertWhen: callCount === times, + error, + invertedError, + }); + } + + /** + * Check if the spy was called exactly once and return a + * {@link SinonSpyAssertion} instance of the first call. This allows to make + * more specific verifications over the spy. + * + * @example + * ``` + * const spy = Sinon.spy(..); // .stub(..) / .mock(..) / .fake(..) + * + * expect(spy) + * .tobeCalledOnce() + * .toReturn(..); + * ``` + * + * @returns a {@link SinonSpyCallAssertion} instance of the first call + */ + public toBeCalledOnce(): SinonSpyCallAssertion { + const { actual } = this.toBeCalled(1); + + return new SinonSpyCallAssertion(actual.firstCall); + } + + /** + * Check if the spy was called exactly twice. This is a more explicit name of + * calling `.toBeCalled(2)`. + * + * @example + * ``` + * const spy = Sinon.spy(..); // .stub(..) / .mock(..) / .fake(..) + * + * expect(spy).tobeCalledTwice(); + * ``` + * + * @returns the assertion instance + */ + public toBeCalledTwice(): this { + return this.toBeCalled(2); + } + + /** + * Check if the spy was called exactly thrice. This is a more explicit name of + * calling `.toBeCalled(3)`. + * + * @example + * ``` + * const spy = Sinon.spy(..); // .stub(..) / .mock(..) / .fake(..) + * + * expect(spy).toBeCalledThrice(); + * ``` + * + * @returns the assertion instance + */ + public toBeCalledThrice(): this { + return this.toBeCalled(3); + } + + /** + * Check if the spy was called at least a number of times. + * + * @example + * ``` + * const spy = Sinon.spy(..); // .stub(..) / .mock(..) / .fake(..) + * + * expect(spy).toBeCalledAtLeast(3); + * ``` + * + * @param times the number of times the spy is at least called + * @returns the assertion instance + */ + public toBeCalledAtLeast(times: number): this { + if (times < 0) { + throw new Error("Spy cannot be called less tha zero times!"); + } + + const { name, callCount } = this.actual; + const error = new AssertionError({ + actual: callCount, + message: `Expeceted <${name}> to be called at least ${numeral(times)}, but it was ${callTimes(callCount)}`, + }); + const invertedError = new AssertionError({ + actual: callCount, + message: `Expeceted <${name}> NOT to be called at least ${numeral(times)}, but it was ${callTimes(callCount)}`, + }); + + return this.execute({ + assertWhen: callCount >= times, + error, + invertedError, + }); + } + + /** + * Check if the spy was called at most a number of times. + * + * @example + * ``` + * const spy = Sinon.spy(..); // .stub(..) / .mock(..) / .fake(..) + * + * expect(spy).toBeCalledAtMost(2); + * ``` + * + * @param times the number of times the spy is at most called + * @returns the assertion instance + */ + public toBeCalledAtMost(times: number): this { + if (times < 0) { + throw new Error("Spy cannot be called less tha zero times!"); + } + + const { name, callCount } = this.actual; + const error = new AssertionError({ + actual: callCount, + message: `Expeceted <${name}> to be called at most ${numeral(times)}, but it was ${callTimes(callCount)}`, + }); + const invertedError = new AssertionError({ + actual: callCount, + message: `Expeceted <${name}> NOT to be called at most ${numeral(times)}, but it was ${callTimes(callCount)}`, + }); + + return this.execute({ + assertWhen: callCount <= times, + error, + invertedError, + }); + } + + /** + * Check if the spy was never called. + * + * @example + * ``` + * const spy = Sinon.spy(..); // .stub(..) / .mock(..) / .fake(..) + * + * expect(spy).toBeNeverCalled(); + * ``` + * + * @returns the assertion instance + */ + public toBeNeverCalled(): this { + const { name, callCount } = this.actual; + const error = new AssertionError({ + actual: callCount, + expected: 0, + message: `Expected <${name}> to be never called, but it was ${callTimes(callCount)}`, + }); + const invertedError = new AssertionError({ + actual: callCount, + message: `Expected <${name}> NOT to be never called, but it was ${callTimes(callCount)}`, + }); + + return this.execute({ + assertWhen: callCount === 0, + error, + invertedError, + }); + } + + /** + * Check if any of the calls to this spy have the expected arguments. Each + * arguments is compared with a strict-deep-equals strategy and must be in + * the exact same order as called. + * + * @example + * ``` + * const spy = Sinon.spy(..); // .stub(..) / .mock(..) / .fake(..) + * + * expect(spy).toHaveArgs("foo", 3, true); + * ``` + * + * @param expected the expected arguments passed to any call + * @returns the assertion instance + */ + public toHaveArgs(...expected: A): this { + const { name, args } = this.actual; + const prettyArgs = expected.map(prettify).join(", "); + const error = new AssertionError({ + expected, + message: `Expected <${name}> to be called with <${prettyArgs}>`, + }); + const invertedError = new AssertionError({ + actual: args, + message: `Expected <${name}> NOT to be called with <${prettyArgs}>`, + }); + + return this.execute({ + assertWhen: args.some(callArgs => isDeepEqual(callArgs, expected)), + error, + invertedError, + }); + } + + /** + * Check if any of the calls to this spy returns the expected value. The + * value is compared with a strict-deep-equals strategy. + * + * @example + * ``` + * const spy = Sinon.spy(..); // .stub(..) / .mock(..) / .fake(..) + * + * expect(spy).toReturn({ res: "ok" }); + * ``` + * + * @param expected the expected value returned by any call + * @returns the assertion instance + */ + public toReturn(expected: R): this { + const { name, returnValues } = this.actual; + const error = new AssertionError({ + expected, + message: `Expected <${name}> to return <${prettify(expected)}> when called`, + }); + const invertedError = new AssertionError({ + actual: returnValues, + message: `Expected <${name}> NOT to return <${prettify(expected)}> when called`, + }); + + return this.execute({ + assertWhen: returnValues.some(value => isDeepEqual(value, expected)), + error, + invertedError, + }); + } + + /** + * Check if any of the calls to this spy throws an exception. The thrown + * values are compared with a strict-deep-equals strategy. + * + * @example + * ``` + * const spy = Sinon.spy(..); // .stub(..) / .mock(..) / .fake(..) + * + * expect(spy).toThrow("I'm not an error"); + * expect(spy).toThrow(new Error("I'm an error")); + * ``` + * + * @param exception the exception thrown by any call + * @returns the assertion instance + */ + public toThrow(exception?: unknown): this { + const { name, exceptions } = this.actual; + const errorCount = exceptions?.length ?? 0; + const expected = exception !== undefined + ? `<${prettify(exception)}>` + : "when called"; + const error = new AssertionError({ + expected: exception, + message: `Expected <${name}> to throw ${expected}`, + }); + const invertedError = new AssertionError({ + actual: exceptions, + message: `Expected <${name}> NOT to throw ${expected}`, + }); + + return this.execute({ + assertWhen: exception !== undefined + ? exceptions?.some(ex => isDeepEqual(ex, exception)) + : errorCount > 0, + error, + invertedError, + }); + } + + /** + * Retrives a specific call of the spy, checking first if there's at least + * that number of calls. Then returns a {@link SinonSpyCallAssertion} + * instance of that call. This allows to make more specific verifications + * over the spy when this is called more than once. + * + * @example + * ``` + * const spy = Sinon.spy(..); // .stub(..) / .mock(..) / .fake(..) + * + * expect(spy) + * .call(7) + * .toHaveArgs(..); // check over the 7th call to this spy + * ``` + * + * @param count The spy call to retrieve. Where `1` means the first call, `2` + * the second call, and so on. + * @returns a {@link SinonSpyCallAssertion} instance of the call + */ + public call(count: number): SinonSpyCallAssertion { + if (count === 0) { + throw new Error("It's not possible to access no call at all!"); + } + + this.toBeCalledAtLeast(count); + + return new SinonSpyCallAssertion(this.actual.getCall(count - 1)); + } +} diff --git a/packages/sinon/src/lib/SinonSpyCallAssertion.ts b/packages/sinon/src/lib/SinonSpyCallAssertion.ts new file mode 100644 index 0000000..aa44b61 --- /dev/null +++ b/packages/sinon/src/lib/SinonSpyCallAssertion.ts @@ -0,0 +1,172 @@ +import { Assertion, AssertionError } from "@assertive-ts/core"; +import { Constructor } from "@assertive-ts/core/dist/lib/Assertion"; +import { ErrorAssertion } from "@assertive-ts/core/dist/lib/ErrorAssertion"; +import isDeepEqual from "fast-deep-equal"; +import { SinonSpyCall } from "sinon"; + +import { prettify } from "./helpers/messages"; + +/** + * Encapsulates assertion methods applicable to {@link SinonSpyCall} instances. + * This includes single calls of `Sinon.spy(..)`, `Sinon.stub(..)`, + * `Sinon.mock()` and `Sinon.fake(..)` as all of them extend from a SinonSpy. + * + * @param A the arguments type of the spied function + * @param R the type return type of the spied function + */ +export class SinonSpyCallAssertion extends Assertion> { + + private spyName: string; + + public constructor(actual: SinonSpyCall) { + super(actual); + this.spyName = "proxy" in actual && typeof actual.proxy === "function" + ? actual.proxy.name + : "unknown"; + } + + /** + * Check if the call to this spy have the expected arguments. Each arguments + * is compared with a strict-deep-equals strategy and must be in the exact + * same order as called. + * + * @example + * ``` + * const spy = Sinon.spy(..); // .stub(..) or .mock(..) + * + * expect(spy.firstCall).toHaveArgs("foo", 3, true); + * ``` + * + * @param expected the expected arguments passed + * @returns the assertion instance + */ + public toHaveArgs(...expected: A): this { + const { args } = this.actual; + const prettyArgs = expected.map(prettify).join(", "); + const error = new AssertionError({ + expected, + message: `Expected <${this.spyName}> to be called with <${prettyArgs}>`, + }); + const invertedError = new AssertionError({ + actual: args, + message: `Expected <${this.spyName}> NOT to be called with <${prettyArgs}>`, + }); + + return this.execute({ + assertWhen: isDeepEqual(args, expected), + error, + invertedError, + }); + } + + /** + * Check if the call to this spy returns the expected value. The value is + * compared with a strict-deep-equals strategy. + * + * @example + * ``` + * const spy = Sinon.spy(..); // .stub(..) or .mock(..) + * + * expect(spy.firstCall).toReturn({ res: "ok" }); + * ``` + * + * @param expected the expected value returned by the call + * @returns the assertion instance + */ + public toReturn(expected: R): this { + const { returnValue } = this.actual; + const error = new AssertionError({ + actual: returnValue, + expected, + message: `Expected <${this.spyName}> to return <${prettify(expected)}> when called`, + }); + const invertedError = new AssertionError({ + actual: returnValue, + message: `Expected <${this.spyName}> NOT to return <${prettify(expected)}> when called`, + }); + + return this.execute({ + assertWhen: isDeepEqual(returnValue, expected), + error, + invertedError, + }); + } + + /** + * Check if the call to the spy throws an exception. The thrown values are + * compared with a strict-deep-equals strategy. + * + * @example + * ``` + * const spy = Sinon.spy(..); // .stub(..) or .mock(..) + * + * expect(spy.firstCall).toThrow("I'm not an error"); + * expect(spy.firstCall).toThrow(new Error("I'm an error")); + * ``` + * + * @param exception the exception thrown by the call + * @returns the assertion instance + */ + public toThrow(exception?: unknown): this { + const expected = exception !== undefined + ? `<${prettify(exception)}>` + : "when called"; + const error = new AssertionError({ + actual: this.actual.exception, + expected: exception, + message: `Expected <${this.spyName}> to throw ${expected}`, + }); + const invertedError = new AssertionError({ + actual: this.actual.exception, + message: `Expected <${this.spyName}> NOT to throw ${expected}`, + }); + + return this.execute({ + assertWhen: exception !== undefined + ? isDeepEqual(this.actual.exception, exception) + : this.actual.exception !== undefined, + error, + invertedError, + }); + } + + /** + * Check if the call to this spy throws an `Error`. If the `Expected` + * constructor is passed, it also checks if the error is an instance of the + * specific Error type. + * + * @example + * ``` + * const spy = Sinon.spy(..); // .stub(..) or .mock(..) + * + * expect(spy.firstCall).toThrowError(); // any instance of Error + * expect(spy.firstCall) + * .toThrowError(MyCustomError) + * .toHaveMessage("Something went wrong!"); + * ``` + * + * @typeParam E the type of the `Error` + * @param Expected optional error type constructor to check the thrown error + * against. If is not provided, it defaults to {@link Error} + * @returns a new {@link ErrorAssertion} to assert over the error + */ + public toThrowError(Expected?: Constructor): ErrorAssertion { + const ExpectedType = Expected ?? Error; + const error = new AssertionError({ + expected: Expected, + message: `Expected <${this.spyName}> to throw an <${ExpectedType.name}> instance`, + }); + const invertedError = new AssertionError({ + actual: this.actual.exception, + message: `Expected <${this.spyName}> NOT to throw an <${ExpectedType.name}> instance`, + }); + + this.execute({ + assertWhen: this.actual.exception instanceof ExpectedType, + error, + invertedError, + }); + + return new ErrorAssertion(this.actual.exception as E); + } +} diff --git a/packages/sinon/src/lib/helpers/messages.ts b/packages/sinon/src/lib/helpers/messages.ts new file mode 100644 index 0000000..29a0327 --- /dev/null +++ b/packages/sinon/src/lib/helpers/messages.ts @@ -0,0 +1,20 @@ +export { prettify } from "@assertive-ts/core/dist/lib/helpers/messages"; + +export function pluralize(text: string, count: number): string { + return `${text}${count !== 1 ? "s" : ""}`; +} + +export function numeral(num: number): string { + switch (num) { + case 1: return "once"; + case 2: return "twice"; + case 3: return "thrice"; + default: return `${num} times`; + } +} + +export function callTimes(times: number): string { + return times > 0 + ? `called ${times} ${pluralize("time", times)}` + : "never called"; +} diff --git a/packages/sinon/src/main.ts b/packages/sinon/src/main.ts new file mode 100644 index 0000000..0677d3a --- /dev/null +++ b/packages/sinon/src/main.ts @@ -0,0 +1,59 @@ +import { Plugin } from "@assertive-ts/core"; +import { SinonSpy, SinonSpyCall } from "sinon"; + +import { SinonSpyAssertion } from "./lib/SinonSpyAssertion"; +import { SinonSpyCallAssertion } from "./lib/SinonSpyCallAssertion"; + +export type { SinonSpyAssertion } from "./lib/SinonSpyAssertion"; +export type { SinonSpyCallAssertion } from "./lib/SinonSpyCallAssertion"; + +declare module "@assertive-ts/core" { + + export interface Expect { + (actual: SinonSpy): SinonSpyAssertion; + (actual: SinonSpyCall): SinonSpyCallAssertion; + } +} + +function isSinonSpy(actual: unknown): actual is SinonSpy { + return typeof actual === "function" + && "getCall" in actual + && typeof actual.getCall === "function"; +} + +function isSinonSpyCall(actual: unknown): actual is SinonSpyCall { + if (typeof actual === "object" && actual !== null) { + const hasProxy = "proxy" in actual + && typeof actual.proxy === "object" + && actual.proxy !== null + && "isSinonProxy" in actual.proxy + && actual.proxy.isSinonProxy; + + return hasProxy + ? isSinonSpyCall(actual.proxy) + : isSinonSpy(actual); + } + + return false; +} + +function sinonSpyPlugin(): Plugin, SinonSpyAssertion> { + return { + Assertion: SinonSpyAssertion, + insertAt: "top", + predicate: isSinonSpy, + }; +} + +function sinonSpyCallPlugin(): Plugin, SinonSpyCallAssertion> { + return { + Assertion: SinonSpyCallAssertion, + insertAt: "top", + predicate: isSinonSpyCall, + }; +} + +export const SinonPlugin = [ + sinonSpyPlugin(), + sinonSpyCallPlugin(), +]; diff --git a/packages/sinon/test/helpers/common.ts b/packages/sinon/test/helpers/common.ts new file mode 100644 index 0000000..06688cd --- /dev/null +++ b/packages/sinon/test/helpers/common.ts @@ -0,0 +1,12 @@ +/** + * Calls a function that will sneak by any error thrown. + * + * @param fn a function to sneakily call + */ +export function sneakyCall(fn: () => unknown): void { + try { + fn(); + } catch (e) { + // continue... + } +} diff --git a/packages/sinon/test/hooks.ts b/packages/sinon/test/hooks.ts new file mode 100644 index 0000000..ed79f3c --- /dev/null +++ b/packages/sinon/test/hooks.ts @@ -0,0 +1,9 @@ +import Sinon from "sinon"; + +export function mochaHooks(): Mocha.RootHookObject { + return { + afterEach() { + Sinon.restore(); + }, + }; +} diff --git a/packages/sinon/test/unit/lib/SinonSpyAssertion.test.ts b/packages/sinon/test/unit/lib/SinonSpyAssertion.test.ts new file mode 100644 index 0000000..f57ac29 --- /dev/null +++ b/packages/sinon/test/unit/lib/SinonSpyAssertion.test.ts @@ -0,0 +1,478 @@ +import { AssertionError, expect } from "@assertive-ts/core"; +import Sinon from "sinon"; + +import { SinonSpyAssertion } from "../../../src/lib/SinonSpyAssertion"; +import { SinonSpyCallAssertion } from "../../../src/lib/SinonSpyCallAssertion"; +import { sneakyCall } from "../../helpers/common"; + +function sayHello(to: string): string { + if (to === "") { + throw Error("Impossible greet!"); + } + + return `Hello ${to}`; +} + +describe("[Unit] SinonSpyAssertion.test.ts", () => { + describe(".toBeCalled", () => { + context("when the times arg is less than zero", () => { + it("throws an assertion error", () => { + const spy = Sinon.spy(sayHello); + const test = new SinonSpyAssertion(spy); + + expect(() => test.toBeCalled(-1)) + .toThrowError() + .toHaveMessage("Spy cannot be called less tha zero times!"); + }); + }); + + context("when the times arg is greater or equal to zero", () => { + context("and the spy is called the same times as the arg", () => { + it("returns the assertion instance", () => { + const spy = Sinon.spy(sayHello); + const test = new SinonSpyAssertion(spy); + + spy("world!"); + + expect(test.toBeCalled(1)).toBeEqual(test); + expect(() => test.not.toBeCalled(1)) + .toThrowError(AssertionError) + .toHaveMessage("Expected NOT to be called once, but it was called 1 time"); + }); + }); + + context("and the spy is not called the same times as the argument", () => { + it("throws an assertion error", () => { + const spy = Sinon.spy(sayHello); + const test = new SinonSpyAssertion(spy); + + spy("world!"); + + expect(() => test.toBeCalled(2)) + .toThrowError(AssertionError) + .toHaveMessage("Expected to be called twice, but it was called 1 time"); + expect(test.not.toBeCalled(2)).toBeEqual(test); + }); + }); + }); + }); + + describe(".toBeCalledOnce", () => { + it("calls toBeCalled(1)", () => { + const spy = Sinon.spy(sayHello); + const test = new SinonSpyAssertion(spy); + const methodSpy = Sinon.spy(test, "toBeCalled"); + + spy("world!"); + test.toBeCalledOnce(); + + Sinon.assert.calledOnceWithExactly(methodSpy, 1); + }); + + it("returns the a SpyCallAssertion instance of the call", () => { + const spy = Sinon.spy(sayHello); + const test = new SinonSpyAssertion(spy); + + spy("world!"); + + // Cycles are not supported by fast-deep-equal: + // https://github.com/epoberezkin/fast-deep-equal/issues/17 + const { not, ...testCall } = test.toBeCalledOnce(); + const { not: expectedNot, ...expected } = new SinonSpyCallAssertion(spy.firstCall); + + expect(testCall).toBeEqual(expected); + }); + }); + + describe(".toBeCalledTwice", () => { + it("calls toBeCalled(2)", () => { + const spy = Sinon.spy(sayHello); + const test = new SinonSpyAssertion(spy); + const methodSpy = Sinon.spy(test, "toBeCalled"); + + spy("world 1"); + spy("world 2"); + test.toBeCalledTwice(); + + Sinon.assert.calledOnceWithExactly(methodSpy, 2); + }); + }); + + describe(".toBeCalledThrice", () => { + it("calls toBeCalled(2)", () => { + const spy = Sinon.spy(sayHello); + const test = new SinonSpyAssertion(spy); + const methodSpy = Sinon.spy(test, "toBeCalled"); + + spy("world 1"); + spy("world 2"); + spy("world 3"); + test.toBeCalledThrice(); + + Sinon.assert.calledOnceWithExactly(methodSpy, 3); + }); + }); + + describe(".toBeCalledAtLeast", () => { + context("when the times arg is less than zero", () => { + it("throws an assertion error", () => { + const spy = Sinon.spy(sayHello); + const test = new SinonSpyAssertion(spy); + + expect(() => test.toBeCalledAtLeast(-1)) + .toThrowError() + .toHaveMessage("Spy cannot be called less tha zero times!"); + }); + }); + + context("when the times arg is greater or equal to zero", () => { + context("and the spy is called more times than the arg", () => { + it("returns the assertion instance", () => { + const spy = Sinon.spy(sayHello); + const test = new SinonSpyAssertion(spy); + + spy("world!"); + spy("someone"); + spy("everyone"); + + expect(test.toBeCalledAtLeast(2)).toBeEqual(test); + expect(() => test.not.toBeCalledAtLeast(2)) + .toThrowError(AssertionError) + .toHaveMessage("Expeceted NOT to be called at least twice, but it was called 3 times"); + }); + }); + + context("and the spy is called the same times as the arg", () => { + it("returns the assertion instance", () => { + const spy = Sinon.spy(sayHello); + const test = new SinonSpyAssertion(spy); + + spy("world!"); + spy("someone"); + + expect(test.toBeCalledAtLeast(2)).toBeEqual(test); + expect(() => test.not.toBeCalledAtLeast(2)) + .toThrowError(AssertionError) + .toHaveMessage("Expeceted NOT to be called at least twice, but it was called 2 times"); + }); + }); + + context("and the spy is called less times than the arg", () => { + it("throws an assertion error", () => { + const spy = Sinon.spy(sayHello); + const test = new SinonSpyAssertion(spy); + + spy("world!"); + + expect(() => test.toBeCalledAtLeast(2)) + .toThrowError(AssertionError) + .toHaveMessage("Expeceted to be called at least twice, but it was called 1 time"); + expect(test.not.toBeCalledAtLeast(2)).toBeEqual(test); + }); + }); + }); + }); + + describe(".toBeCalledAtMost", () => { + context("when the times arg is less than zero", () => { + it("throws an assertion error", () => { + const spy = Sinon.spy(sayHello); + const test = new SinonSpyAssertion(spy); + + expect(() => test.toBeCalledAtMost(-1)) + .toThrowError() + .toHaveMessage("Spy cannot be called less tha zero times!"); + }); + }); + + context("when the times arg is greater or equal to zero", () => { + context("and the spy is called less times than the arg", () => { + it("returns the assertion instance", () => { + const spy = Sinon.spy(sayHello); + const test = new SinonSpyAssertion(spy); + + spy("world!"); + spy("someone"); + + expect(test.toBeCalledAtMost(3)).toBeEqual(test); + expect(() => test.not.toBeCalledAtMost(3)) + .toThrowError(AssertionError) + .toHaveMessage("Expeceted NOT to be called at most thrice, but it was called 2 times"); + }); + }); + + context("and the spy is called the same times as the arg", () => { + it("returns the assertion instance", () => { + const spy = Sinon.spy(sayHello); + const test = new SinonSpyAssertion(spy); + + spy("world!"); + spy("someone"); + + expect(test.toBeCalledAtMost(2)).toBeEqual(test); + expect(() => test.not.toBeCalledAtMost(2)) + .toThrowError(AssertionError) + .toHaveMessage("Expeceted NOT to be called at most twice, but it was called 2 times"); + }); + }); + + context("and the spy is called more times than the arg", () => { + it("throws an assertion error", () => { + const spy = Sinon.spy(sayHello); + const test = new SinonSpyAssertion(spy); + + spy("world 1"); + spy("world 2"); + spy("world 3"); + + expect(() => test.toBeCalledAtMost(2)) + .toThrowError(AssertionError) + .toHaveMessage("Expeceted to be called at most twice, but it was called 3 times"); + expect(test.not.toBeCalledAtMost(2)).toBeEqual(test); + }); + }); + }); + }); + + describe(".toBeNeverCalled", () => { + context("when the spy is not called", () => { + it("returns the assertion instance", () => { + const spy = Sinon.spy(sayHello); + const test = new SinonSpyAssertion(spy); + + expect(test.toBeNeverCalled()).toBeEqual(test); + expect(() => test.not.toBeNeverCalled()) + .toThrowError(AssertionError) + .toHaveMessage("Expected NOT to be never called, but it was never called"); + }); + }); + + context("when the spy is called", () => { + it("throws an assertion error", () => { + const spy = Sinon.spy(sayHello); + const test = new SinonSpyAssertion(spy); + + spy("world!"); + + expect(() => test.toBeNeverCalled()) + .toThrowError(AssertionError) + .toHaveMessage("Expected to be never called, but it was called 1 time"); + expect(test.not.toBeNeverCalled()).toBeEqual(test); + }); + }); + }); + + describe(".toHaveArgs", () => { + context("when the spy is never called", () => { + it("throws an assertion error", () => { + const spy = Sinon.spy(sayHello); + const test = new SinonSpyAssertion(spy); + + expect(() => test.toHaveArgs("world!")) + .toThrowError(AssertionError) + .toHaveMessage('Expected to be called with <"world!">'); + expect(test.not.toHaveArgs("world!")).toBeEqual(test); + }); + }); + + context("when the spy is called", () => { + context("and the args are equal to the expected", () => { + it("returns the assertion instance", () => { + const spy = Sinon.spy(sayHello); + const test = new SinonSpyAssertion(spy); + + spy("other"); + spy("another"); + spy("world!"); + + expect(test.toHaveArgs("world!")).toBeEqual(test); + expect(() => test.not.toHaveArgs("world!")) + .toThrowError(AssertionError) + .toHaveMessage('Expected NOT to be called with <"world!">'); + }); + }); + + context("when the args are not equal to the expected", () => { + it("throws an assertion error", () => { + const spy = Sinon.spy(sayHello); + const test = new SinonSpyAssertion(spy); + + spy("other"); + spy("another"); + spy("John!"); + + expect(() => test.toHaveArgs("world!")) + .toThrowError(AssertionError) + .toHaveMessage('Expected to be called with <"world!">'); + expect(test.not.toHaveArgs("world!")).toBeEqual(test); + }); + }); + }); + }); + + describe(".toReturn", () => { + context("when the spy returns the expected value", () => { + it("returns the assertion instance", () => { + const spy = Sinon.spy(sayHello); + const test = new SinonSpyAssertion(spy); + + spy("foo"); + spy("world!"); + spy("bar"); + + expect(test.toReturn("Hello world!")).toBeEqual(test); + expect(() => test.not.toReturn("Hello world!")) + .toThrowError(AssertionError) + .toHaveMessage('Expected NOT to return <"Hello world!"> when called'); + }); + }); + + context("when the spy does not return the expected value", () => { + it("throws an assertion error", () => { + const spy = Sinon.spy(sayHello); + const test = new SinonSpyAssertion(spy); + + spy("foo"); + spy("bar"); + spy("baz"); + + expect(() => test.toReturn("Hello world!")) + .toThrowError(AssertionError) + .toHaveMessage('Expected to return <"Hello world!"> when called'); + expect(test.not.toReturn("Hello world!")).toBeEqual(test); + }); + }); + }); + + describe(".toThrow", () => { + context("when the spy throws an exception", () => { + context("and the expected value is present", () => { + context("and the exception is equal to the expected value", () => { + it("returns the assertion instance", () => { + const spy = Sinon.spy(sayHello); + const test = new SinonSpyAssertion(spy); + + sneakyCall(() => { + spy("world!"); + spy("other"); + spy(""); + }); + + expect(test.toThrow(Error("Impossible greet!"))).toBeEqual(test); + expect(() => test.not.toThrow(Error("Impossible greet!"))) + .toThrowError(AssertionError) + .toHaveMessage("Expected NOT to throw "); + }); + }); + + context("and the exception is not equal to the expected value", () => { + it("throws an assertion error", () => { + const spy = Sinon.spy(sayHello); + const test = new SinonSpyAssertion(spy); + + sneakyCall(() => { + spy("world!"); + spy("other"); + spy(""); + }); + + expect(() => test.toThrow("foo")) + .toThrowError(AssertionError) + .toHaveMessage('Expected to throw <"foo">'); + expect(test.not.toThrow("foo")).toBeEqual(test); + }); + }); + }); + + context("and the expected value is not present", () => { + it("returns the assertion instance", () => { + const spy = Sinon.spy(sayHello); + const test = new SinonSpyAssertion(spy); + + sneakyCall(() => { + spy("world!"); + spy("other"); + spy(""); + }); + + expect(test.toThrow()).toBeEqual(test); + expect(() => test.not.toThrow()) + .toThrowError(AssertionError) + .toHaveMessage("Expected NOT to throw when called"); + }); + }); + }); + + context("when the spy does not throws an exception", () => { + context("and the expected value is present", () => { + it("throws an assertion error", () => { + const spy = Sinon.spy(sayHello); + const test = new SinonSpyAssertion(spy); + + spy("world!"); + + expect(() => test.toThrow("foo")) + .toThrowError(AssertionError) + .toHaveMessage('Expected to throw <"foo">'); + expect(test.not.toThrow("foo")).toBeEqual(test); + }); + }); + + context("and the expected value is not present", () => { + it("throws an assertion error", () => { + const spy = Sinon.spy(sayHello); + const test = new SinonSpyAssertion(spy); + + expect(() => test.toThrow()) + .toThrowError(AssertionError) + .toHaveMessage("Expected to throw when called"); + expect(test.not.toThrow("foo")).toBeEqual(test); + }); + }); + }); + }); + + describe(".call", () => { + context("when the index is less than zero", () => { + it("throws an assertion error", () => { + const spy = Sinon.spy(sayHello); + const test = new SinonSpyAssertion(spy); + + expect(() => test.call(-1)) + .toThrowError() + .toHaveMessage("Spy cannot be called less tha zero times!"); + }); + }); + + context("when the index is equal to zero", () => { + it("throws an assertion error", () => { + const spy = Sinon.spy(sayHello); + const test = new SinonSpyAssertion(spy); + + expect(() => test.call(0)) + .toThrowError() + .toHaveMessage("It's not possible to access no call at all!"); + }); + }); + + context("when the index is greater or equal to zero", () => { + context("and the spy call index exists", () => { + it("returns a SpyCallAssertion instance of the call", () => { + const spy = Sinon.spy(sayHello); + const test = new SinonSpyAssertion(spy); + + spy("1"); + spy("2"); + spy("3"); + + // Cycles are not supported by fast-deep-equal: + // https://github.com/epoberezkin/fast-deep-equal/issues/17 + const { not, ...testCall } = test.call(3); + const { not: expectedNot, ...expected } = new SinonSpyCallAssertion(spy.getCall(2)); + + expect(testCall).toBeEqual(expected); + }); + }); + }); + }); +}); diff --git a/packages/sinon/test/unit/lib/SinonSpyCallAssertion.test.ts b/packages/sinon/test/unit/lib/SinonSpyCallAssertion.test.ts new file mode 100644 index 0000000..3b3d349 --- /dev/null +++ b/packages/sinon/test/unit/lib/SinonSpyCallAssertion.test.ts @@ -0,0 +1,266 @@ +/* eslint-disable max-classes-per-file */ +import { AssertionError, expect } from "@assertive-ts/core"; +import { ErrorAssertion } from "@assertive-ts/core/dist/lib/ErrorAssertion"; +import Sinon from "sinon"; + +import { SinonSpyCallAssertion } from "../../../src/lib/SinonSpyCallAssertion"; +import { sneakyCall } from "../../helpers/common"; + +class InvariantError extends Error { + + public constructor(message: string) { + super(`Invariant: ${message}`); + this.name = InvariantError.name; + } +} + +class OtherError extends Error { + +} + +function sayHello(to: string): string { + if (to === "") { + throw Error("Impossible greet!"); + } + + if (to === "invariant") { + throw new InvariantError("That was unexpected!"); + } + + return `Hello ${to}`; +} + +describe("[Unit] SinonSpyCallAssertion.test.ts", () => { + describe(".toHaveArgs", () => { + context("when the args are equal to the expected", () => { + it("returns the assertion instance", () => { + const spy = Sinon.spy(sayHello); + spy("world!"); + + const test = new SinonSpyCallAssertion(spy.firstCall); + + expect(test.toHaveArgs("world!")).toBeEqual(test); + expect(() => test.not.toHaveArgs("world!")) + .toThrowError(AssertionError) + .toHaveMessage('Expected NOT to be called with <"world!">'); + }); + }); + + context("when the args are not equal to the expected", () => { + it("throws an assertion error", () => { + const spy = Sinon.spy(sayHello); + spy("world!"); + + const test = new SinonSpyCallAssertion(spy.firstCall); + + expect(() => test.toHaveArgs("other")) + .toThrowError(AssertionError) + .toHaveMessage('Expected to be called with <"other">'); + expect(test.not.toHaveArgs("other")).toBeEqual(test); + }); + }); + }); + + describe(".toReturn", () => { + context("when the spy returns the expected value", () => { + it("returns the assertion instance", () => { + const spy = Sinon.spy(sayHello); + spy("world!"); + + const test = new SinonSpyCallAssertion(spy.firstCall); + + expect(test.toReturn("Hello world!")).toBeEqual(test); + expect(() => test.not.toReturn("Hello world!")) + .toThrowError(AssertionError) + .toHaveMessage('Expected NOT to return <"Hello world!"> when called'); + }); + }); + + context("when the spy does not return the expected value", () => { + it("throws an assertion error", () => { + const spy = Sinon.spy(sayHello); + spy("world!"); + + const test = new SinonSpyCallAssertion(spy.firstCall); + + expect(() => test.toReturn("Hello everyone!")) + .toThrowError(AssertionError) + .toHaveMessage('Expected to return <"Hello everyone!"> when called'); + expect(test.not.toReturn("Hello everyone!")).toBeEqual(test); + }); + }); + }); + + describe(".toThrow", () => { + context("when the call throws an exception", () => { + context("and the expected value is present", () => { + context("and the exception is equal to the expected value", () => { + it("returns the assertion instance", () => { + const spy = Sinon.spy(sayHello); + + sneakyCall(() => spy("")); + + const test = new SinonSpyCallAssertion(spy.firstCall); + + expect(test.toThrow(Error("Impossible greet!"))).toBeEqual(test); + expect(() => test.not.toThrow(Error("Impossible greet!"))) + .toThrowError(AssertionError) + .toHaveMessage("Expected NOT to throw "); + }); + }); + + context("and the exception is not equal to the expected value", () => { + it("throws an assertion error", () => { + const spy = Sinon.spy(sayHello); + + sneakyCall(() => spy("")); + + const test = new SinonSpyCallAssertion(spy.firstCall); + + expect(() => test.toThrow("foo")) + .toThrowError(AssertionError) + .toHaveMessage('Expected to throw <"foo">'); + expect(test.not.toThrow("foo")).toBeEqual(test); + }); + }); + }); + + context("and the expected value is not present", () => { + it("returns the assertion instance", () => { + const spy = Sinon.spy(sayHello); + + sneakyCall(() => spy("")); + + const test = new SinonSpyCallAssertion(spy.firstCall); + + expect(test.toThrow()).toBeEqual(test); + expect(() => test.not.toThrow()) + .toThrowError(AssertionError) + .toHaveMessage("Expected NOT to throw when called"); + }); + }); + }); + + context("when the call does not throw an exception", () => { + context("and the expected value is present", () => { + it("throws an assertion error", () => { + const spy = Sinon.spy(sayHello); + spy("world!"); + + const test = new SinonSpyCallAssertion(spy.firstCall); + + expect(() => test.toThrow("foo")) + .toThrowError(AssertionError) + .toHaveMessage('Expected to throw <"foo">'); + expect(test.not.toThrow("foo")).toBeEqual(test); + }); + }); + + context("and the expected value is not present", () => { + it("throws an assertion error", () => { + const spy = Sinon.spy(sayHello); + spy("world!"); + + const test = new SinonSpyCallAssertion(spy.firstCall); + + expect(() => test.toThrow()) + .toThrowError(AssertionError) + .toHaveMessage("Expected to throw when called"); + expect(test.not.toThrow()).toBeEqual(test); + }); + }); + }); + }); + + describe(".toThrowError", () => { + context("when the expected constructor is passed", () => { + context("and the spy throws an error", () => { + context("and the error is instance of the constructor", () => { + it("returns an ErrorAssertion instance", () => { + const spy = Sinon.spy(sayHello); + + sneakyCall(() => spy("invariant")); + + const test = new SinonSpyCallAssertion(spy.firstCall); + const { not, ...errorAssertion } = test.toThrowError(InvariantError); + const { not: expectedNot, ...expected } = new ErrorAssertion(new InvariantError("That was unexpected!")); + + expect(errorAssertion).toBeEqual(expected); + expect(() => test.not.toThrowError(InvariantError)) + .toThrowError(AssertionError) + .toHaveMessage("Expected NOT to throw an instance"); + }); + }); + + context("and the error is not instance of the constructor", () => { + it("throws an assertion error", () => { + const spy = Sinon.spy(sayHello); + + sneakyCall(() => spy("invariant")); + + const test = new SinonSpyCallAssertion(spy.firstCall); + + expect(() => test.toThrowError(OtherError)) + .toThrowError(AssertionError) + .toHaveMessage("Expected to throw an instance"); + expect(test.not.toThrowError(OtherError)).toBeInstanceOf(ErrorAssertion); + }); + }); + }); + + context("and the spy does not throw an error", () => { + it("throws an assertion error", () => { + const spy = Sinon.spy(sayHello); + spy("world!"); + + const test = new SinonSpyCallAssertion(spy.firstCall); + + expect(() => test.toThrowError(InvariantError)) + .toThrowError(AssertionError) + .toHaveMessage("Expected to throw an instance"); + expect(test.not.toThrowError(InvariantError)).toBeInstanceOf(ErrorAssertion); + }); + }); + }); + + context("when the expected constructor is not passed", () => { + context("and the spy throws an error", () => { + const variants = [ + ["invariant", new InvariantError("That was unexpected!")], + ["", Error("Impossible greet!")], + ] as const; + + variants.forEach(([arg, error]) => { + it(`[error: ${error.name}] returns an ErrorAssertion instance`, () => { + const spy = Sinon.spy(sayHello); + + sneakyCall(() => spy(arg)); + + const test = new SinonSpyCallAssertion(spy.firstCall); + const { not, ...errorAssertion } = test.toThrowError(); + const { not: expectedNot, ...expected } = new ErrorAssertion(error); + + expect(errorAssertion).toBeEqual(expected); + expect(() => test.not.toThrowError()) + .toThrowError(AssertionError) + .toHaveMessage("Expected NOT to throw an instance"); + }); + }); + }); + + context("and the spy does not throw an error", () => { + it("throws an assertion error", () => { + const spy = Sinon.spy(sayHello); + spy("world!"); + + const test = new SinonSpyCallAssertion(spy.firstCall); + + expect(() => test.toThrowError()) + .toThrowError(AssertionError) + .toHaveMessage("Expected to throw an instance"); + expect(test.not.toThrowError()).toBeInstanceOf(ErrorAssertion); + }); + }); + }); + }); +}); diff --git a/packages/sinon/test/unit/lib/helpers/messages.test.ts b/packages/sinon/test/unit/lib/helpers/messages.test.ts new file mode 100644 index 0000000..f99d78f --- /dev/null +++ b/packages/sinon/test/unit/lib/helpers/messages.test.ts @@ -0,0 +1,79 @@ +import { expect } from "@assertive-ts/core"; + +import { callTimes, numeral, pluralize } from "../../../../src/lib/helpers/messages"; + +describe("[Unit] messages.test.ts", () => { + describe(".pluralize", () => { + context("when the count arg is equal to one", () => { + it("returns the same text", () => { + const result = pluralize("apple", 1); + + expect(result).toBeEqual("apple"); + }); + }); + + context("when the count is not equal to one", () => { + [-1, 0, 2, 3, 4].forEach(count => { + it(`[count: ${count}] returns the text with an 's' appended`, () => { + const result = pluralize("apple", count); + + expect(result).toBeEqual("apples"); + }); + }); + }); + }); + + describe(".numeral", () => { + context("when the numeral exists", () => { + const variants = [ + [1, "once"], + [2, "twice"], + [3, "thrice"], + ] as const; + + variants.forEach(([num, text]) => { + it(`[Num: ${num}] returns the '${text}' numeral`, () => { + const result = numeral(num); + + expect(result).toBeEqual(text); + }); + }); + }); + + context("when the numeral does not exist", () => { + [-1, 0, 4, 5].forEach(num => { + it(`[Num: ${num}] returns '${num} times' instead`, () => { + const result = numeral(num); + + expect(result).toBeEqual(`${num} times`); + }); + }); + }); + }); + + describe(".callTimes", () => { + context("when the times is greater than zero", () => { + const variants = [ + [1, "time"], + [2, "times"], + [3, "times"], + ] as const; + + variants.forEach(([times, text]) => { + it(`[Times: ${times}] returns the called times text`, () => { + const result = callTimes(times); + + expect(result).toBeEqual(`called ${times} ${text}`); + }); + }); + }); + + context("when the times is zero", () => { + it("returs the never called text", () => { + const result = callTimes(0); + + expect(result).toBeEqual("never called"); + }); + }); + }); +}); diff --git a/packages/sinon/test/unit/main.test.ts b/packages/sinon/test/unit/main.test.ts new file mode 100644 index 0000000..3133f4a --- /dev/null +++ b/packages/sinon/test/unit/main.test.ts @@ -0,0 +1,15 @@ +import { expect } from "@assertive-ts/core"; + +import { SinonSpyAssertion } from "../../src/lib/SinonSpyAssertion"; +import { SinonSpyCallAssertion } from "../../src/lib/SinonSpyCallAssertion"; +import { SinonPlugin } from "../../src/main"; + +describe("[Unit] main.test.ts", () => { + describe("SinonPlugin", () => { + it("contains both SinonSpy and SinonSpyCall plugins", () => { + expect(SinonPlugin).toHaveSize(2); + expect(SinonPlugin[0]?.Assertion).toBe(SinonSpyAssertion); + expect(SinonPlugin[1]?.Assertion).toBe(SinonSpyCallAssertion); + }); + }); +}); diff --git a/packages/sinon/tsconfig.json b/packages/sinon/tsconfig.json new file mode 100644 index 0000000..05bc1dc --- /dev/null +++ b/packages/sinon/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "./build", + "typeRoots": [ + "../../node_modules/@types/", + "./typings/" + ] + }, + "exclude": [ + "build/*", + "dist/*" + ], + "ts-node": { + "transpileOnly": true + } +} diff --git a/packages/sinon/tsconfig.prod.json b/packages/sinon/tsconfig.prod.json new file mode 100644 index 0000000..3473c03 --- /dev/null +++ b/packages/sinon/tsconfig.prod.json @@ -0,0 +1,11 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "incremental": false, + "outDir": "./dist" + }, + "include": [ + "src/**/*", + "typings/**/*" + ] +} diff --git a/packages/sinon/typedoc.json b/packages/sinon/typedoc.json new file mode 100644 index 0000000..bf75483 --- /dev/null +++ b/packages/sinon/typedoc.json @@ -0,0 +1,28 @@ +{ + "$schema": "https://typedoc.org/schema.json", + "cleanOutputDir": true, + "entryPoints": ["src/main.ts"], + "entryPointStrategy": "expand", + "gitRevision": "main", + "githubPages": false, + "hideGenerator": true, + "includeVersion": false, + "intentionallyNotExported": [], + "mergeModulesMergeMode": "project", + "mergeModulesRenameDefaults": true, + "name": "Assertive.ts - Sinon API", + "out": "../../docs/sinon/build", + "plugin": [ + "typedoc-plugin-markdown", + "typedoc-plugin-merge-modules" + ], + "readme": "none", + "visibilityFilters": { + "@alpha": false, + "@beta": false, + "external": false, + "inherited": true, + "private": false, + "protected": false + } +} diff --git a/yarn.lock b/yarn.lock index 569a358..23c0337 100644 --- a/yarn.lock +++ b/yarn.lock @@ -27,7 +27,7 @@ __metadata: resolution: "@assertive-ts/core@workspace:packages/core" dependencies: "@types/mocha": "npm:^10.0.6" - "@types/node": "npm:^20.10.5" + "@types/node": "npm:^20.10.6" "@types/sinon": "npm:^17.0.2" all-contributors-cli: "npm:^6.26.1" dedent: "npm:^1.5.1" @@ -37,13 +37,42 @@ __metadata: semantic-release-yarn: "npm:^3.0.2" sinon: "npm:^17.0.1" ts-node: "npm:^10.9.2" - typedoc: "npm:^0.25.4" + typedoc: "npm:^0.25.6" typedoc-plugin-markdown: "npm:^3.17.1" typedoc-plugin-merge-modules: "npm:^5.1.0" typescript: "npm:^5.3.3" languageName: unknown linkType: soft +"@assertive-ts/sinon@workspace:packages/sinon": + version: 0.0.0-use.local + resolution: "@assertive-ts/sinon@workspace:packages/sinon" + dependencies: + "@assertive-ts/core": "workspace:^" + "@types/mocha": "npm:^10.0.6" + "@types/node": "npm:^20.10.6" + "@types/sinon": "npm:^17.0.2" + fast-deep-equal: "npm:^3.1.3" + mocha: "npm:^10.2.0" + semantic-release: "npm:^22.0.12" + semantic-release-yarn: "npm:^3.0.2" + sinon: "npm:^17.0.1" + ts-node: "npm:^10.9.2" + typedoc: "npm:^0.25.7" + typedoc-plugin-markdown: "npm:^3.17.1" + typedoc-plugin-merge-modules: "npm:^5.1.0" + typescript: "npm:^5.3.3" + peerDependencies: + "@assertive-ts/core": ">=2.0.0" + sinon: ">=15.2.0" + peerDependenciesMeta: + "@assertive-ts/core": + optional: false + sinon: + optional: true + languageName: unknown + linkType: soft + "@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.12.13, @babel/code-frame@npm:^7.21.4, @babel/code-frame@npm:^7.22.13, @babel/code-frame@npm:^7.23.5": version: 7.23.5 resolution: "@babel/code-frame@npm:7.23.5" @@ -522,7 +551,7 @@ __metadata: "@assertive-ts/core": "workspace:^" "@examples/symbol-plugin": "workspace:^" "@types/jest": "npm:^29.5.11" - "@types/node": "npm:^20.10.5" + "@types/node": "npm:^20.10.6" jest: "npm:^29.7.0" ts-jest: "npm:^29.1.1" ts-node: "npm:^10.9.2" @@ -537,7 +566,7 @@ __metadata: "@assertive-ts/core": "workspace:^" "@examples/symbol-plugin": "workspace:^" "@types/mocha": "npm:^10.0.6" - "@types/node": "npm:^20.10.5" + "@types/node": "npm:^20.10.6" mocha: "npm:^10.2.0" ts-node: "npm:^10.9.2" typescript: "npm:^5.3.3" @@ -1651,7 +1680,7 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:*, @types/node@npm:^20.10.5": +"@types/node@npm:*": version: 20.10.5 resolution: "@types/node@npm:20.10.5" dependencies: @@ -1660,6 +1689,15 @@ __metadata: languageName: node linkType: hard +"@types/node@npm:^20.10.6": + version: 20.10.6 + resolution: "@types/node@npm:20.10.6" + dependencies: + undici-types: "npm:~5.26.4" + checksum: 08471220d3cbbb6669835c4b78541edf5eface8f2c2e36c550cfa4ff73da73071c90e200a06359fac25d6564127597c23e178128058fb676824ec23d5178a017 + languageName: node + linkType: hard + "@types/normalize-package-data@npm:^2.4.1, @types/normalize-package-data@npm:^2.4.3": version: 2.4.4 resolution: "@types/normalize-package-data@npm:2.4.4" @@ -1713,15 +1751,15 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/eslint-plugin@npm:^6.15.0": - version: 6.15.0 - resolution: "@typescript-eslint/eslint-plugin@npm:6.15.0" +"@typescript-eslint/eslint-plugin@npm:^6.17.0": + version: 6.17.0 + resolution: "@typescript-eslint/eslint-plugin@npm:6.17.0" dependencies: "@eslint-community/regexpp": "npm:^4.5.1" - "@typescript-eslint/scope-manager": "npm:6.15.0" - "@typescript-eslint/type-utils": "npm:6.15.0" - "@typescript-eslint/utils": "npm:6.15.0" - "@typescript-eslint/visitor-keys": "npm:6.15.0" + "@typescript-eslint/scope-manager": "npm:6.17.0" + "@typescript-eslint/type-utils": "npm:6.17.0" + "@typescript-eslint/utils": "npm:6.17.0" + "@typescript-eslint/visitor-keys": "npm:6.17.0" debug: "npm:^4.3.4" graphemer: "npm:^1.4.0" ignore: "npm:^5.2.4" @@ -1734,7 +1772,7 @@ __metadata: peerDependenciesMeta: typescript: optional: true - checksum: 9020370c5e89b52b65ed2373c755d4b70f57ec7ebcf02d3e2f323f31ec81717af110d8e5f903b189b71e0a952f042e0fe2b637e77959c3102907efed4ba55512 + checksum: f2a5774e9cc03e491a5a488501e5622c7eebd766f5a4fc2c30642864a3b89b0807946bde33a678f326ba7032f3f6a51aa0bf9c2d10adc823804fc9fb47db55a6 languageName: node linkType: hard @@ -1749,21 +1787,21 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/parser@npm:^6.15.0": - version: 6.15.0 - resolution: "@typescript-eslint/parser@npm:6.15.0" +"@typescript-eslint/parser@npm:^6.17.0": + version: 6.17.0 + resolution: "@typescript-eslint/parser@npm:6.17.0" dependencies: - "@typescript-eslint/scope-manager": "npm:6.15.0" - "@typescript-eslint/types": "npm:6.15.0" - "@typescript-eslint/typescript-estree": "npm:6.15.0" - "@typescript-eslint/visitor-keys": "npm:6.15.0" + "@typescript-eslint/scope-manager": "npm:6.17.0" + "@typescript-eslint/types": "npm:6.17.0" + "@typescript-eslint/typescript-estree": "npm:6.17.0" + "@typescript-eslint/visitor-keys": "npm:6.17.0" debug: "npm:^4.3.4" peerDependencies: eslint: ^7.0.0 || ^8.0.0 peerDependenciesMeta: typescript: optional: true - checksum: fdd1f584e5068216c36a01e40750950ef309b36a522f6ecde36931690558a319960a702b4b4a806f335fb28ca99f8a07bb206571141550aaab1f6f40066f6605 + checksum: 2ed0ed4a5b30e953430ce3279df3655af09fa1caed2abf11804d239717daefc32a22864f6620ef57bb9c684c74a99a13241384fea5096e961385e3678fc2e920 languageName: node linkType: hard @@ -1777,22 +1815,22 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/scope-manager@npm:6.15.0": - version: 6.15.0 - resolution: "@typescript-eslint/scope-manager@npm:6.15.0" +"@typescript-eslint/scope-manager@npm:6.17.0": + version: 6.17.0 + resolution: "@typescript-eslint/scope-manager@npm:6.17.0" dependencies: - "@typescript-eslint/types": "npm:6.15.0" - "@typescript-eslint/visitor-keys": "npm:6.15.0" - checksum: 168d783c06a99784362e2eaaa56396b31716ee785779707ef984c2abb3e822c56440473efc6580cb8b84b2da508731ad184a00b3618bc7f3f93d8243804f2fcf + "@typescript-eslint/types": "npm:6.17.0" + "@typescript-eslint/visitor-keys": "npm:6.17.0" + checksum: fe09c628553c9336e6a36d32c1d34e78ebd20aa02130a6bf535329621ba5a98aaac171f607bc6e4d17b3478c42e7de6476376636897ce3f227c754eb99acd07e languageName: node linkType: hard -"@typescript-eslint/type-utils@npm:6.15.0": - version: 6.15.0 - resolution: "@typescript-eslint/type-utils@npm:6.15.0" +"@typescript-eslint/type-utils@npm:6.17.0": + version: 6.17.0 + resolution: "@typescript-eslint/type-utils@npm:6.17.0" dependencies: - "@typescript-eslint/typescript-estree": "npm:6.15.0" - "@typescript-eslint/utils": "npm:6.15.0" + "@typescript-eslint/typescript-estree": "npm:6.17.0" + "@typescript-eslint/utils": "npm:6.17.0" debug: "npm:^4.3.4" ts-api-utils: "npm:^1.0.1" peerDependencies: @@ -1800,7 +1838,7 @@ __metadata: peerDependenciesMeta: typescript: optional: true - checksum: 8dabb355f09f57de8b46d726ad95a57593e5b87427dee5182afecb490624424afec02b69a27018b352dcb5f930eb391cb8cdc12cd60a93231d4f04e63e2f2c0b + checksum: dc7938429193acfda61b7282197ec046039e2c4da41cdcddf4daaf300d10229e4e23bb0fcf0503b19c0b99a874849c8a9f5bb35ce106260f56a14106d2b41d8c languageName: node linkType: hard @@ -1811,10 +1849,10 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/types@npm:6.15.0": - version: 6.15.0 - resolution: "@typescript-eslint/types@npm:6.15.0" - checksum: d55de64d532c9016c922cc36b86ab661d7d64d942057486a0bca7a7db07fade95c3de59bfe364bc76ab538fb979ca2e4e6744c3acf8919a2d61e73cc7f544363 +"@typescript-eslint/types@npm:6.17.0": + version: 6.17.0 + resolution: "@typescript-eslint/types@npm:6.17.0" + checksum: 87ab1b5a3270ab34b917c22a2fb90a9ad7d9f3b19d73a337bc9efbe65f924da13482c97e8ccbe3bd3d081aa96039eeff50de41c1da2a2128066429b931cdb21d languageName: node linkType: hard @@ -1836,21 +1874,22 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/typescript-estree@npm:6.15.0": - version: 6.15.0 - resolution: "@typescript-eslint/typescript-estree@npm:6.15.0" +"@typescript-eslint/typescript-estree@npm:6.17.0": + version: 6.17.0 + resolution: "@typescript-eslint/typescript-estree@npm:6.17.0" dependencies: - "@typescript-eslint/types": "npm:6.15.0" - "@typescript-eslint/visitor-keys": "npm:6.15.0" + "@typescript-eslint/types": "npm:6.17.0" + "@typescript-eslint/visitor-keys": "npm:6.17.0" debug: "npm:^4.3.4" globby: "npm:^11.1.0" is-glob: "npm:^4.0.3" + minimatch: "npm:9.0.3" semver: "npm:^7.5.4" ts-api-utils: "npm:^1.0.1" peerDependenciesMeta: typescript: optional: true - checksum: 920f7f3bfe463a9da943e1a686b7f13ac802a5e33be52f39ac711aa53a1e274dbe173b41bba05581c560fabfc3e1fadcfd81ab53a036afe25fb1a76651fcad7a + checksum: 1671b0d2f2fdf07074fb1e2524d61935cec173bd8db6e482cc5b2dcc77aed3ffa831396736ffa0ee2fdbddd8585ae9ca8d6c97bcaea1385b23907a1ec0508f83 languageName: node linkType: hard @@ -1872,20 +1911,20 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/utils@npm:6.15.0": - version: 6.15.0 - resolution: "@typescript-eslint/utils@npm:6.15.0" +"@typescript-eslint/utils@npm:6.17.0": + version: 6.17.0 + resolution: "@typescript-eslint/utils@npm:6.17.0" dependencies: "@eslint-community/eslint-utils": "npm:^4.4.0" "@types/json-schema": "npm:^7.0.12" "@types/semver": "npm:^7.5.0" - "@typescript-eslint/scope-manager": "npm:6.15.0" - "@typescript-eslint/types": "npm:6.15.0" - "@typescript-eslint/typescript-estree": "npm:6.15.0" + "@typescript-eslint/scope-manager": "npm:6.17.0" + "@typescript-eslint/types": "npm:6.17.0" + "@typescript-eslint/typescript-estree": "npm:6.17.0" semver: "npm:^7.5.4" peerDependencies: eslint: ^7.0.0 || ^8.0.0 - checksum: 7895240933ad28295508f8c4286a8b905550a35eda83a11ecf9511e53078e0af07e75a1872f1bc757f165b41fdc84616ea97c1e2e3bf80cff985935f25596228 + checksum: 37c63afcf66124bf84808699997953b8c84a378aa2c490a771b611d82cdac8499c58fac8eeb8378528e97660b59563d99297bfec4b982cd68760b0ffe54aa714 languageName: node linkType: hard @@ -1899,13 +1938,13 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/visitor-keys@npm:6.15.0": - version: 6.15.0 - resolution: "@typescript-eslint/visitor-keys@npm:6.15.0" +"@typescript-eslint/visitor-keys@npm:6.17.0": + version: 6.17.0 + resolution: "@typescript-eslint/visitor-keys@npm:6.17.0" dependencies: - "@typescript-eslint/types": "npm:6.15.0" + "@typescript-eslint/types": "npm:6.17.0" eslint-visitor-keys: "npm:^3.4.1" - checksum: 4641a829485f67a5d9d3558aa0d152e5ab57b468cfd9653168ce9a141e1f051730669a024505183b64f7a7e5d8f62533af4ebd4ad7366b551390461e9c45ec18 + checksum: a2aed0e1437fdab8858ab9c7c8e355f8b72a5fa4b0adc54f28b8a2bbc29d4bb93214968ee940f83d013d0a4b83d00cd4eeeb05fb4c2c7d0ead324c6793f7d6d4 languageName: node linkType: hard @@ -2293,13 +2332,13 @@ __metadata: version: 0.0.0-use.local resolution: "assertive-ts@workspace:." dependencies: - "@typescript-eslint/eslint-plugin": "npm:^6.15.0" - "@typescript-eslint/parser": "npm:^6.15.0" + "@typescript-eslint/eslint-plugin": "npm:^6.17.0" + "@typescript-eslint/parser": "npm:^6.17.0" eslint: "npm:^8.56.0" eslint-import-resolver-typescript: "npm:^3.6.1" eslint-plugin-etc: "npm:^2.0.3" eslint-plugin-import: "npm:^2.29.1" - eslint-plugin-jsdoc: "npm:^46.9.1" + eslint-plugin-jsdoc: "npm:^48.0.2" eslint-plugin-sonarjs: "npm:^0.23.0" turbo: "npm:^1.11.2" typescript: "npm:^5.3.3" @@ -3572,9 +3611,9 @@ __metadata: languageName: node linkType: hard -"eslint-plugin-jsdoc@npm:^46.9.1": - version: 46.9.1 - resolution: "eslint-plugin-jsdoc@npm:46.9.1" +"eslint-plugin-jsdoc@npm:^48.0.2": + version: 48.0.2 + resolution: "eslint-plugin-jsdoc@npm:48.0.2" dependencies: "@es-joy/jsdoccomment": "npm:~0.41.0" are-docs-informative: "npm:^0.0.2" @@ -3586,8 +3625,8 @@ __metadata: semver: "npm:^7.5.4" spdx-expression-parse: "npm:^4.0.0" peerDependencies: - eslint: ^7.0.0 || ^8.0.0 - checksum: 0b176d690738b3e5dad9bceccb8640b95070e8981ce0dce48227969d6c9fe972554aeccfc013bf231cf1eb51dacc3b0987c597b553f4b0e52f637f4587b509be + eslint: ^7.0.0 || ^8.0.0 || ^9.0.0 + checksum: 03f55ea97314759450b5e985344c60559907276a905792e9f31af7770f14baba76b28a9a9d9de47a38bb8617c9a0dc6b53d68a77d14b0f2701469d89ed453165 languageName: node linkType: hard @@ -6214,6 +6253,15 @@ __metadata: languageName: node linkType: hard +"minimatch@npm:9.0.3, minimatch@npm:^9.0.0, minimatch@npm:^9.0.1, minimatch@npm:^9.0.3": + version: 9.0.3 + resolution: "minimatch@npm:9.0.3" + dependencies: + brace-expansion: "npm:^2.0.1" + checksum: c81b47d28153e77521877649f4bab48348d10938df9e8147a58111fe00ef89559a2938de9f6632910c4f7bf7bb5cd81191a546167e58d357f0cfb1e18cecc1c5 + languageName: node + linkType: hard + "minimatch@npm:^3.0.4, minimatch@npm:^3.0.5, minimatch@npm:^3.1.1, minimatch@npm:^3.1.2": version: 3.1.2 resolution: "minimatch@npm:3.1.2" @@ -6223,15 +6271,6 @@ __metadata: languageName: node linkType: hard -"minimatch@npm:^9.0.0, minimatch@npm:^9.0.1, minimatch@npm:^9.0.3": - version: 9.0.3 - resolution: "minimatch@npm:9.0.3" - dependencies: - brace-expansion: "npm:^2.0.1" - checksum: c81b47d28153e77521877649f4bab48348d10938df9e8147a58111fe00ef89559a2938de9f6632910c4f7bf7bb5cd81191a546167e58d357f0cfb1e18cecc1c5 - languageName: node - linkType: hard - "minimist@npm:^1.2.0, minimist@npm:^1.2.5, minimist@npm:^1.2.6": version: 1.2.8 resolution: "minimist@npm:1.2.8" @@ -7874,7 +7913,7 @@ __metadata: languageName: node linkType: hard -"shiki@npm:^0.14.1": +"shiki@npm:^0.14.7": version: 0.14.7 resolution: "shiki@npm:0.14.7" dependencies: @@ -8801,19 +8840,35 @@ __metadata: languageName: node linkType: hard -"typedoc@npm:^0.25.4": - version: 0.25.4 - resolution: "typedoc@npm:0.25.4" +"typedoc@npm:^0.25.6": + version: 0.25.6 + resolution: "typedoc@npm:0.25.6" + dependencies: + lunr: "npm:^2.3.9" + marked: "npm:^4.3.0" + minimatch: "npm:^9.0.3" + shiki: "npm:^0.14.7" + peerDependencies: + typescript: 4.6.x || 4.7.x || 4.8.x || 4.9.x || 5.0.x || 5.1.x || 5.2.x || 5.3.x + bin: + typedoc: bin/typedoc + checksum: 4d3858152859598e0a7ed34f3ed023a4ad83b4fa0c684b14752e23b59c3102d265596eb510f2ab8665e7b8e67b5a601fe3f70c9eab1b0a2fece63c33088ea848 + languageName: node + linkType: hard + +"typedoc@npm:^0.25.7": + version: 0.25.7 + resolution: "typedoc@npm:0.25.7" dependencies: lunr: "npm:^2.3.9" marked: "npm:^4.3.0" minimatch: "npm:^9.0.3" - shiki: "npm:^0.14.1" + shiki: "npm:^0.14.7" peerDependencies: typescript: 4.6.x || 4.7.x || 4.8.x || 4.9.x || 5.0.x || 5.1.x || 5.2.x || 5.3.x bin: typedoc: bin/typedoc - checksum: 638f63d751ba86f1b0e04a303501b88b6e97ba093f82c3fa72a555c207e16fd316ec76c13f7d628e9ee26296f80fddc45b87d4b13714925c5e726047adb76d22 + checksum: fa88c808e9912ef248cc45b4defea49522e93b97b4bb67423670257a4507ccabdc25c1518a39f6058a728d08675ee0947de55944419fa4bb9f870d84ba4db764 languageName: node linkType: hard From 896a4cacd22cafbf8fd1432b1f90a73acfda5bb3 Mon Sep 17 00:00:00 2001 From: Jose Luis Leon Date: Fri, 9 Feb 2024 11:04:59 -0500 Subject: [PATCH 2/2] Address @dalejo96 and @ChristianSama feedback --- packages/{ => sinon}/README.md | 4 ++-- packages/sinon/src/lib/SinonSpyAssertion.ts | 22 +++++++++---------- .../sinon/src/lib/SinonSpyCallAssertion.ts | 6 ++--- .../test/unit/lib/SinonSpyAssertion.test.ts | 18 +++++++-------- 4 files changed, 25 insertions(+), 25 deletions(-) rename packages/{ => sinon}/README.md (89%) diff --git a/packages/README.md b/packages/sinon/README.md similarity index 89% rename from packages/README.md rename to packages/sinon/README.md index 6a0f255..50adcd5 100644 --- a/packages/README.md +++ b/packages/sinon/README.md @@ -59,7 +59,7 @@ expect(spy).toHaveArgs(10, "long-range"); expect(spy).toThrow(); ``` -The assertion above act over any of the calls made to the spy. You can get more specific matchers if you assert over a single spy call: +The assertions above act over any of the calls made to the spy. You can get more specific matchers if you assert over a single spy call: ```ts import { expect } from "@assertive-ts/core"; @@ -90,7 +90,7 @@ expect(spy) .toHaveMessage("Something went wrong..."); ``` -Notice how `get(..)` and `.toBeCalledOnce()` methods return an assertion over the single call, this way you can chain matchers instead of writing more statements. +Notice how `call(..)` and `.toBeCalledOnce()` methods return an assertion over the single call, this way you can chain matchers instead of writing more statements. ## License diff --git a/packages/sinon/src/lib/SinonSpyAssertion.ts b/packages/sinon/src/lib/SinonSpyAssertion.ts index 1bc3188..6b59df7 100644 --- a/packages/sinon/src/lib/SinonSpyAssertion.ts +++ b/packages/sinon/src/lib/SinonSpyAssertion.ts @@ -36,7 +36,7 @@ export class SinonSpyAssertion extends Assertion extends Assertion extends Assertion extends Assertion extends Assertion extends Assertion extends Assertion extends Assertion extends Assertion { expect(() => test.toBeCalled(-1)) .toThrowError() - .toHaveMessage("Spy cannot be called less tha zero times!"); + .toHaveMessage("Spy cannot be called less than zero times!"); }); }); @@ -121,7 +121,7 @@ describe("[Unit] SinonSpyAssertion.test.ts", () => { expect(() => test.toBeCalledAtLeast(-1)) .toThrowError() - .toHaveMessage("Spy cannot be called less tha zero times!"); + .toHaveMessage("Spy cannot be called less than zero times!"); }); }); @@ -181,7 +181,7 @@ describe("[Unit] SinonSpyAssertion.test.ts", () => { expect(() => test.toBeCalledAtMost(-1)) .toThrowError() - .toHaveMessage("Spy cannot be called less tha zero times!"); + .toHaveMessage("Spy cannot be called less than zero times!"); }); }); @@ -234,14 +234,14 @@ describe("[Unit] SinonSpyAssertion.test.ts", () => { }); }); - describe(".toBeNeverCalled", () => { + describe(".toNeverBeCalled", () => { context("when the spy is not called", () => { it("returns the assertion instance", () => { const spy = Sinon.spy(sayHello); const test = new SinonSpyAssertion(spy); - expect(test.toBeNeverCalled()).toBeEqual(test); - expect(() => test.not.toBeNeverCalled()) + expect(test.toNeverBeCalled()).toBeEqual(test); + expect(() => test.not.toNeverBeCalled()) .toThrowError(AssertionError) .toHaveMessage("Expected NOT to be never called, but it was never called"); }); @@ -254,10 +254,10 @@ describe("[Unit] SinonSpyAssertion.test.ts", () => { spy("world!"); - expect(() => test.toBeNeverCalled()) + expect(() => test.toNeverBeCalled()) .toThrowError(AssertionError) .toHaveMessage("Expected to be never called, but it was called 1 time"); - expect(test.not.toBeNeverCalled()).toBeEqual(test); + expect(test.not.toNeverBeCalled()).toBeEqual(test); }); }); }); @@ -440,7 +440,7 @@ describe("[Unit] SinonSpyAssertion.test.ts", () => { expect(() => test.call(-1)) .toThrowError() - .toHaveMessage("Spy cannot be called less tha zero times!"); + .toHaveMessage("Spy cannot be called less than zero times!"); }); });