Browse Source

feat: adds chai plugin

e22m4u 2 days ago
parent
commit
d826f6fd32

+ 2 - 1
.mocharc.json

@@ -1,4 +1,5 @@
 {
 {
   "extension": ["js"],
   "extension": ["js"],
-  "spec": "src/**/*.spec.js"
+  "spec": "src/**/*.spec.js",
+  "require": "./mocha.setup.js"
 }
 }

+ 10 - 10
README.md

@@ -16,7 +16,7 @@
   - [Свойства и методы шпиона](#свойства-и-методы-шпиона)
   - [Свойства и методы шпиона](#свойства-и-методы-шпиона)
     - [spy(...args)](#spyargs)
     - [spy(...args)](#spyargs)
     - [spy.calls](#spycalls)
     - [spy.calls](#spycalls)
-    - [spy.isCalled](#spyiscalled)
+    - [spy.called](#spycalled)
     - [spy.callCount](#spycallcount)
     - [spy.callCount](#spycallcount)
     - [spy.restore()](#spyrestore)
     - [spy.restore()](#spyrestore)
   - [Функция `createSpiesGroup`](#функция-createspiesgroup)
   - [Функция `createSpiesGroup`](#функция-createspiesgroup)
@@ -68,7 +68,7 @@ greetSpy('World');
 greetSpy('JavaScript');
 greetSpy('JavaScript');
 
 
 // количество вызовов
 // количество вызовов
-console.log(greetSpy.isCalled);  // true
+console.log(greetSpy.called);  // true
 console.log(greetSpy.callCount); // 2
 console.log(greetSpy.callCount); // 2
 
 
 // аргументы и возвращаемое значение первого вызова
 // аргументы и возвращаемое значение первого вызова
@@ -124,7 +124,7 @@ calculator.add(2, 1);
 console.log(calculator.value); // 3
 console.log(calculator.value); // 3
 
 
 // количество вызовов
 // количество вызовов
-console.log(addSpy.isCalled);  // true
+console.log(addSpy.called);  // true
 console.log(addSpy.callCount); // 2
 console.log(addSpy.callCount); // 2
 
 
 // аргументы и возвращаемое значение первого вызова
 // аргументы и возвращаемое значение первого вызова
@@ -210,7 +210,7 @@ console.log(loggerSpy.callCount);      // 1
 // восстановление всех шпионов в группе:
 // восстановление всех шпионов в группе:
 //   - оригинальные методы service.fetchData
 //   - оригинальные методы service.fetchData
 //     и service.processItem будут восстановлены
 //     и service.processItem будут восстановлены
-//   - история вызовов (callCount, isCalled, calls и т.д.)
+//   - история вызовов (callCount, called, calls и т.д.)
 //     для fetchDataSpy, processItemSpy и loggerSpy
 //     для fetchDataSpy, processItemSpy и loggerSpy
 //     будет сброшена
 //     будет сброшена
 //   - внутренний список шпионов в группе будет очищен
 //   - внутренний список шпионов в группе будет очищен
@@ -220,7 +220,7 @@ console.log(service.fetchData === fetchDataSpy);
 // false (оригинальный метод восстановлен)
 // false (оригинальный метод восстановлен)
 console.log(fetchDataSpy.callCount);
 console.log(fetchDataSpy.callCount);
 // 0 (история сброшена)
 // 0 (история сброшена)
-console.log(loggerSpy.isCalled);
+console.log(loggerSpy.called);
 // false (история сброшена)
 // false (история сброшена)
 ```
 ```
 
 
@@ -301,16 +301,16 @@ console.log(spy.calls);
 // ]
 // ]
 ```
 ```
 
 
-#### spy.isCalled
+#### spy.called
 
 
 - Тип: `boolean` (только для чтения)
 - Тип: `boolean` (только для чтения)
 - Описание: Указывает, был ли шпион вызван хотя бы один раз.
 - Описание: Указывает, был ли шпион вызван хотя бы один раз.
 
 
 ```js
 ```js
 const spy = createSpy();
 const spy = createSpy();
-console.log(spy.isCalled); // false
+console.log(spy.called); // false
 spy();
 spy();
-console.log(spy.isCalled); // true
+console.log(spy.called); // true
 ```
 ```
 
 
 #### spy.callCount
 #### spy.callCount
@@ -333,7 +333,7 @@ console.log(spy.callCount); // 2
 - Восстанавливает оригинальный метод, если шпион был создан
 - Восстанавливает оригинальный метод, если шпион был создан
   для метода объекта.
   для метода объекта.
 - Сбрасывает историю вызовов шпиона (`callCount` становится 0,
 - Сбрасывает историю вызовов шпиона (`callCount` становится 0,
-  `isCalled` становится `false`, и все записи о вызовах очищаются).
+  `called` становится `false`, и все записи о вызовах очищаются).
 - Если шпион был создан для отдельной функции (а не для метода объекта),
 - Если шпион был создан для отдельной функции (а не для метода объекта),
   восстановление метода не происходит (так как нечего восстанавливать),
   восстановление метода не происходит (так как нечего восстанавливать),
   но история вызовов все равно сбрасывается.
   но история вызовов все равно сбрасывается.
@@ -406,7 +406,7 @@ const obj = {greet: () => 'Hello'};
 const greetSpy = group.on(obj, 'greet');
 const greetSpy = group.on(obj, 'greet');
 // obj.greet теперь шпион, и greetSpy добавлен в группу
 // obj.greet теперь шпион, и greetSpy добавлен в группу
 obj.greet();
 obj.greet();
-console.log(greetSpy.isCalled); // true
+console.log(greetSpy.called); // true
 ```
 ```
 
 
 #### group.restore()
 #### group.restore()

+ 2 - 1
dist/cjs/index.cjs

@@ -146,7 +146,7 @@ function createSpy(target = void 0, methodNameOrImpl = void 0, customImplForMeth
     enumerable: true,
     enumerable: true,
     configurable: false
     configurable: false
   });
   });
-  Object.defineProperty(spy, "isCalled", {
+  Object.defineProperty(spy, "called", {
     get: /* @__PURE__ */ __name(() => callLog.count > 0, "get"),
     get: /* @__PURE__ */ __name(() => callLog.count > 0, "get"),
     enumerable: true,
     enumerable: true,
     configurable: false
     configurable: false
@@ -167,6 +167,7 @@ function createSpy(target = void 0, methodNameOrImpl = void 0, customImplForMeth
   if (isMethodSpy && objToSpyOn) {
   if (isMethodSpy && objToSpyOn) {
     objToSpyOn[methodName] = spy;
     objToSpyOn[methodName] = spy;
   }
   }
+  spy.__isSpy = true;
   return spy;
   return spy;
 }
 }
 __name(createSpy, "createSpy");
 __name(createSpy, "createSpy");

+ 4 - 0
mocha.setup.js

@@ -0,0 +1,4 @@
+import * as chai from 'chai';
+import {chaiSpiesPlugin} from './src/chai/index.js';
+
+chai.use(chaiSpiesPlugin);

+ 343 - 0
src/chai/chai-spies-plugin.d.ts

@@ -0,0 +1,343 @@
+
+declare global {
+  namespace Chai {
+    interface LanguageChains {
+      on: Assertion;
+    }
+    interface Assertion {
+      /**
+       * ####.spy
+       * Asserts that object is a spy.
+       * ```ts
+       * expect(spy).to.be.spy;
+       * spy.should.be.spy;
+       * ```
+       */
+      spy: Assertion;
+
+      /**
+       * ####.called
+       * Assert that a spy has been called. Negation passes through.
+       * ```ts
+       * expect(spy).to.have.been.called();
+       * spy.should.have.been.called();
+       * ```
+       * Note that ```called``` can be used as a chainable method.
+       */
+      called: ChaiSpies.Called;
+
+      /**
+       *  * ####.been
+       * * Assert that something has been spied on. Negation passes through.
+       * * ```ts
+       * * expect(spy).to.have.been.called();
+       * * spy.should.have.been.called();
+       * ```
+       * Note that ```been``` can be used as a chainable method.
+       */
+      been: ChaiSpies.Been;
+
+      /**
+       *  * ####.nth (function)
+       * * Assert that something has been spied on on a certain index. Negation passes through.
+       * * ```ts
+       * * expect(spy).on.nth(5).be.called.with('foobar');
+       * * spy.should.on.nth(5).be.called.with('foobar');
+       * ```
+       * Note that ```nth``` can be used as a chainable method.
+       */
+      nth(index: number): Assertion;
+    }
+
+    namespace ChaiSpies {
+      interface Called {
+        (): Chai.Assertion;
+        with: With;
+        always: Always;
+
+        /**
+         * ####.once
+         * Assert that a spy has been called exactly once.
+         * ```ts
+         * expect(spy).to.have.been.called.once;
+         * expect(spy).to.not.have.been.called.once;
+         * spy.should.have.been.called.once;
+         * spy.should.not.have.been.called.once;
+         * ```
+         */
+        once: Chai.Assertion;
+
+        /**
+         * ####.twice
+         * Assert that a spy has been called exactly twice.
+         * ```ts
+         * expect(spy).to.have.been.called.twice;
+         * expect(spy).to.not.have.been.called.twice;
+         * spy.should.have.been.called.twice;
+         * spy.should.not.have.been.called.twice;
+         * ```
+         */
+        twice: Chai.Assertion;
+
+        /**
+         * ####.exactly(n)
+         * Assert that a spy has been called exactly ```n``` times.
+         * ```ts
+         * expect(spy).to.have.been.called.exactly(3);
+         * expect(spy).to.not.have.been.called.exactly(3);
+         * spy.should.have.been.called.exactly(3);
+         * spy.should.not.have.been.called.exactly(3);
+         * ```
+         */
+        exactly(n: number): Chai.Assertion;
+
+        /**
+         * ####.min(n) / .at.least(n)
+         * Assert that a spy has been called minimum of ```n``` times.
+         * ```ts
+         * expect(spy).to.have.been.called.min(3);
+         * expect(spy).to.not.have.been.called.at.least(3);
+         * spy.should.have.been.called.at.least(3);
+         * spy.should.not.have.been.called.min(3);
+         * ```
+         */
+        min(n: number): Chai.Assertion;
+
+        /**
+         * ####.max(n) / .at.most(n)
+         * Assert that a spy has been called maximum of ```n``` times.
+         * ```ts
+         * expect(spy).to.have.been.called.max(3);
+         * expect(spy).to.not.have.been.called.at.most(3);
+         * spy.should.have.been.called.at.most(3);
+         * spy.should.not.have.been.called.max(3);
+         * ```
+         */
+        max(n: number): Chai.Assertion;
+
+        at: At;
+        /**
+         * ####.above(n) / .gt(n)
+         * Assert that a spy has been called more than ```n``` times.
+         * ```ts
+         * expect(spy).to.have.been.called.above(3);
+         * spy.should.not.have.been.called.above(3);
+         * ```
+         */
+        above(n: number): Chai.Assertion;
+
+        /**
+         * ####.above(n) / .gt(n)
+         * Assert that a spy has been called more than ```n``` times.
+         * ```ts
+         * expect(spy).to.have.been.called.gt(3);
+         * spy.should.not.have.been.called.gt(3);
+         * ```
+         */
+        gt(n: number): Chai.Assertion;
+
+        /**
+         * ####.below(n) / .lt(n)
+         * Assert that a spy has been called fewer than ```n``` times.
+         * ```ts
+         * expect(spy).to.have.been.called.below(3);
+         * spy.should.not.have.been.called.below(3);
+         * ```
+         */
+        below(n: number): Chai.Assertion;
+
+        /**
+         * ####.below(n) / .lt(n)
+         * Assert that a spy has been called fewer than ```n``` times.
+         * ```ts
+         * expect(spy).to.have.been.called.lt(3);
+         * spy.should.not.have.been.called.lt(3);
+         * ```
+         */
+        lt(n: number): Chai.Assertion;
+      }
+
+      interface Been extends Chai.Assertion {
+        (): Chai.Assertion;
+        called: Called;
+
+        /**
+         * ####.first
+         * Assert that a spy has been called first.
+         * ```ts
+         * expect(spy).to.have.been.called.first;
+         * expect(spy).to.not.have.been.called.first;
+         * spy.should.have.been.called.first;
+         * spy.should.not.have.been.called.first;
+         * ```
+         */
+        first: Chai.Assertion;
+
+        /**
+         * ####.second
+         * Assert that a spy has been called second.
+         * ```ts
+         * expect(spy).to.have.been.called.second;
+         * expect(spy).to.not.have.been.called.second;
+         * spy.should.have.been.called.second;
+         * spy.should.not.have.been.called.second;
+         * ```
+         */
+        second: Chai.Assertion;
+
+        /**
+         * ####.third
+         * Assert that a spy has been called third.
+         * ```ts
+         * expect(spy).to.have.been.called.third;
+         * expect(spy).to.not.have.been.called.third;
+         * spy.should.have.been.called.third;
+         * spy.should.not.have.been.called.third;
+         * ```
+         */
+        third: Chai.Assertion;
+      }
+
+      interface With {
+        /**
+         * ####.with
+         * Assert that a spy has been called with a given argument at least once, even if more arguments were provided.
+         * ```ts
+         * spy('foo');
+         * expect(spy).to.have.been.called.with('foo');
+         * spy.should.have.been.called.with('foo');
+         * ```
+         * Will also pass for ```spy('foo', 'bar')``` and ```spy(); spy('foo')```.
+         * If used with multiple arguments, assert that a spy has been called with all the given arguments at least once.
+         * ```ts
+         * spy('foo', 'bar', 1);
+         * expect(spy).to.have.been.called.with('bar', 'foo');
+         * spy.should.have.been.called.with('bar', 'foo');
+         * ```
+         */
+        (
+          a: any,
+          b?: any,
+          c?: any,
+          d?: any,
+          e?: any,
+          f?: any,
+          g?: any,
+          h?: any,
+          i?: any,
+          j?: any,
+        ): Chai.Assertion;
+
+        /**
+         * ####.with.exactly
+         * Similar to .with, but will pass only if the list of arguments is exactly the same as the one provided.
+         * ```ts
+         * spy();
+         * spy('foo', 'bar');
+         * expect(spy).to.have.been.called.with.exactly('foo', 'bar');
+         * spy.should.have.been.called.with.exactly('foo', 'bar');
+         * ```
+         * Will not pass for ```spy('foo')```, ```spy('bar')```, ```spy('bar'); spy('foo')```, ```spy('foo'); spy('bar')```, ```spy('bar', 'foo')``` or ```spy('foo', 'bar', 1)```.
+         * Can be used for calls with a single argument too.
+         */
+
+        exactly(
+          a?: any,
+          b?: any,
+          c?: any,
+          d?: any,
+          e?: any,
+          f?: any,
+          g?: any,
+          h?: any,
+          i?: any,
+          j?: any,
+        ): Chai.Assertion;
+      }
+
+      interface Always {
+        with: AlwaysWith;
+      }
+
+      interface AlwaysWith {
+        /**
+         * ####.always.with
+         * Assert that every time the spy has been called the argument list contained the given arguments.
+         * ```ts
+         * spy('foo');
+         * spy('foo', 'bar');
+         * spy(1, 2, 'foo');
+         * expect(spy).to.have.been.called.always.with('foo');
+         * spy.should.have.been.called.always.with('foo');
+         * ```
+         */
+        (
+          a: any,
+          b?: any,
+          c?: any,
+          d?: any,
+          e?: any,
+          f?: any,
+          g?: any,
+          h?: any,
+          i?: any,
+          j?: any,
+        ): Chai.Assertion;
+
+        /**
+         * ####.always.with.exactly
+         * Assert that the spy has never been called with a different list of arguments than the one provided.
+         * ```ts
+         * spy('foo');
+         * spy('foo');
+         * expect(spy).to.have.been.called.always.with.exactly('foo');
+         * spy.should.have.been.called.always.with.exactly('foo');
+         * ```
+         */
+        exactly(
+          a?: any,
+          b?: any,
+          c?: any,
+          d?: any,
+          e?: any,
+          f?: any,
+          g?: any,
+          h?: any,
+          i?: any,
+          j?: any,
+        ): Chai.Assertion;
+      }
+
+      interface At {
+        /**
+         * ####.min(n) / .at.least(n)
+         * Assert that a spy has been called minimum of ```n``` times.
+         * ```ts
+         * expect(spy).to.have.been.called.min(3);
+         * expect(spy).to.not.have.been.called.at.least(3);
+         * spy.should.have.been.called.at.least(3);
+         * spy.should.not.have.been.called.min(3);
+         * ```
+         */
+        least(n: number): Chai.Assertion;
+
+        /**
+         * ####.max(n) / .at.most(n)
+         * Assert that a spy has been called maximum of ```n``` times.
+         * ```ts
+         * expect(spy).to.have.been.called.max(3);
+         * expect(spy).to.not.have.been.called.at.most(3);
+         * spy.should.have.been.called.at.most(3);
+         * spy.should.not.have.been.called.max(3);
+         * ```
+         */
+        most(n: number): Chai.Assertion;
+      }
+    }
+  }
+}
+
+/**
+ * Chai spies plugin.
+ */
+export declare const chaiSpiesPlugin: any;

+ 326 - 0
src/chai/chai-spies-plugin.js

@@ -0,0 +1,326 @@
+/* eslint-disable jsdoc/require-jsdoc */
+
+/**
+ * Chai spies plugin
+ *
+ * @param {*} chai
+ * @param {*} _
+ */
+/* prettier-ignore */
+export function chaiSpiesPlugin(chai, _) {
+  const Assertion = chai.Assertion;
+
+  Assertion.addProperty('spy', function () {
+    this.assert(
+        this._obj.__isSpy === true
+      , 'expected ' + this._obj + ' to be a spy'
+      , 'expected ' + this._obj + ' to not be a spy');
+    return this;
+  });
+
+  function assertCalled (n) {
+    new Assertion(this._obj).to.be.spy;
+    var spy = this._obj;
+
+    if (n) {
+      this.assert(
+          spy.calls.length === n
+        , 'expected ' + this._obj + ' to have been called #{exp} but got #{act}'
+        , 'expected ' + this._obj + ' to have not been called #{exp}'
+        , n
+        , spy.calls.length
+      );
+    } else {
+      this.assert(
+          spy.called === true
+        , 'expected ' + this._obj + ' to have been called'
+        , 'expected ' + this._obj + ' to not have been called'
+      );
+    }
+  }
+
+  function assertCalledChain () {
+    new Assertion(this._obj).to.be.spy;
+  }
+
+  Assertion.addChainableMethod('called', assertCalled, assertCalledChain);
+
+  Assertion.addProperty('once', function () {
+    new Assertion(this._obj).to.be.spy;
+    this.assert(
+        this._obj.calls.length === 1
+      , 'expected ' + this._obj + ' to have been called once but got #{act}'
+      , 'expected ' + this._obj + ' to not have been called once'
+      , 1
+      , this._obj.calls.length );
+  });
+
+  Assertion.addProperty('twice', function () {
+    new Assertion(this._obj).to.be.spy;
+    this.assert(
+        this._obj.calls.length === 2
+      , 'expected ' + this._obj + ' to have been called twice but got #{act}'
+      , 'expected ' + this._obj + ' to not have been called twice'
+      , 2
+      , this._obj.calls.length
+    );
+  });
+
+  function nthCallWith(spy, n, expArgs) {
+    if (spy.calls.length <= n) return false;
+
+    var actArgs = spy.calls[n].args.slice()
+      , passed = 0;
+
+    expArgs.forEach(function (expArg) {
+      for (var i = 0; i < actArgs.length; i++) {
+        if (_.eql(actArgs[i], expArg)) {
+          passed++;
+          actArgs.splice(i, 1);
+          break;
+        }
+      }
+    });
+
+    return passed === expArgs.length;
+  }
+
+  function numberOfCallsWith(spy, expArgs) {
+    var found = 0
+      , calls = spy.calls;
+
+    for (var i = 0; i < calls.length; i++) {
+      if (nthCallWith(spy, i, expArgs)) {
+        found++;
+      }
+    }
+
+    return found;
+  }
+
+  Assertion.addProperty('first', function () {
+    if (this._obj.__isSpy) {
+      _.flag(this, 'spy nth call with', 1);
+    }
+  });
+
+  Assertion.addProperty('second', function () {
+    if (this._obj.__isSpy) {
+      _.flag(this, 'spy nth call with', 2);
+    }
+  });
+
+  Assertion.addProperty('third', function () {
+    if (this._obj.__isSpy) {
+      _.flag(this, 'spy nth call with', 3);
+    }
+  });
+
+  Assertion.addProperty('on');
+
+  Assertion.addChainableMethod('nth', function (n) {
+    if (this._obj.__isSpy) {
+      _.flag(this, 'spy nth call with', n);
+    }
+  });
+
+  function generateOrdinalNumber(n) {
+    if (n === 1) return 'first';
+    if (n === 2) return 'second';
+    if (n === 3) return 'third';
+    return n + 'th';
+  }
+
+  function assertWith() {
+    new Assertion(this._obj).to.be.spy;
+    var expArgs = [].slice.call(arguments, 0)
+      , spy = this._obj
+      , calls = spy.calls
+      , always = _.flag(this, 'spy always')
+      , nthCall = _.flag(this, 'spy nth call with');
+
+    if (always) {
+      var passed = numberOfCallsWith(spy, expArgs);
+      this.assert(
+          calls.length > 0 && passed === calls.length
+        , 'expected ' + this._obj + ' to have been always called with #{exp} but got ' + passed + ' out of ' + calls.length
+        , 'expected ' + this._obj + ' to have not always been called with #{exp}'
+        , expArgs
+      );
+    } else if (nthCall) {
+      var ordinalNumber = generateOrdinalNumber(nthCall),
+          actArgs = calls[nthCall - 1];
+      new Assertion(this._obj).to.be.have.been.called.min(nthCall);
+      this.assert(
+          nthCallWith(spy, nthCall - 1, expArgs)
+        , 'expected ' + this._obj + ' to have been called at the ' + ordinalNumber + ' time with #{exp} but got #{act}'
+        , 'expected ' + this._obj + ' to have not been called at the ' + ordinalNumber + ' time with #{exp}'
+        , expArgs
+        , actArgs
+      );
+    } else {
+      const passed = numberOfCallsWith(spy, expArgs);
+      this.assert(
+          passed > 0
+        , 'expected ' + this._obj + ' to have been called with #{exp}'
+        , 'expected ' + this._obj + ' to have not been called with #{exp} but got ' + passed + ' times'
+        , expArgs
+      );
+    }
+  }
+
+  function assertWithChain () {
+    if (this._obj.__isSpy) {
+      _.flag(this, 'spy with', true);
+    }
+  }
+
+  Assertion.addChainableMethod('with', assertWith, assertWithChain);
+
+  Assertion.addProperty('always', function () {
+    if (this._obj.__isSpy) {
+      _.flag(this, 'spy always', true);
+    }
+  });
+
+  Assertion.addMethod('exactly', function () {
+    new Assertion(this._obj).to.be.spy;
+    var always = _.flag(this, 'spy always')
+      , _with = _.flag(this, 'spy with')
+      , args = [].slice.call(arguments, 0)
+      , calls = this._obj.calls
+      , nthCall = _.flag(this, 'spy nth call with')
+      , passed;
+
+    if (always && _with) {
+      passed = 0
+      calls.forEach(function (call) {
+        if (call.args.length !== args.length) return;
+        if (_.eql(call.args, args)) passed++;
+      });
+
+      this.assert(
+          calls.length > 0 && passed === calls.length
+        , 'expected ' + this._obj + ' to have been always called with exactly #{exp} but got ' + passed + ' out of ' + calls.length
+        , 'expected ' + this._obj + ' to have not always been called with exactly #{exp}'
+        , args
+      );
+    } else if(_with && nthCall) {
+      var ordinalNumber = generateOrdinalNumber(nthCall),
+          actArgs = calls[nthCall - 1];
+      new Assertion(this._obj).to.be.have.been.called.min(nthCall);
+      this.assert(
+          _.eql(actArgs, args)
+        , 'expected ' + this._obj + ' to have been called at the ' + ordinalNumber + ' time with exactly #{exp} but got #{act}'
+        , 'expected ' + this._obj + ' to have not been called at the ' + ordinalNumber + ' time with exactly #{exp}'
+        , args
+        , actArgs
+      );
+    } else if (_with) {
+      passed = 0;
+      calls.forEach(function (call) {
+        if (call.args.length !== args.length) return;
+        if (_.eql(call.args, args)) passed++;
+      });
+
+      this.assert(
+          passed > 0
+        , 'expected ' + this._obj + ' to have been called with exactly #{exp}'
+        , 'expected ' + this._obj + ' to not have been called with exactly #{exp} but got ' + passed + ' times'
+        , args
+      );
+    } else {
+      this.assert(
+          this._obj.calls.length === args[0]
+        , 'expected ' + this._obj + ' to have been called #{exp} times but got #{act}'
+        , 'expected ' + this._obj + ' to not have been called #{exp} times'
+        , args[0]
+        , this._obj.calls.length
+      );
+    }
+  });
+
+  function above (_super) {
+    return function (n) {
+      if (this._obj.__isSpy) {
+        new Assertion(this._obj).to.be.spy;
+
+        this.assert(
+            this._obj.calls.length > n
+          , 'expected ' + this._obj + ' to have been called more than #{exp} times but got #{act}'
+          , 'expected ' + this._obj + ' to have been called at most #{exp} times but got #{act}'
+          , n
+          , this._obj.calls.length
+        );
+      } else {
+        _super.apply(this, arguments);
+      }
+    }
+  }
+
+  Assertion.overwriteMethod('above', above);
+  Assertion.overwriteMethod('gt', above);
+
+  function below (_super) {
+    return function (n) {
+      if (this._obj.__isSpy) {
+        new Assertion(this._obj).to.be.spy;
+
+        this.assert(
+            this._obj.calls.length <  n
+          , 'expected ' + this._obj + ' to have been called fewer than #{exp} times but got #{act}'
+          , 'expected ' + this._obj + ' to have been called at least #{exp} times but got #{act}'
+          , n
+          , this._obj.calls.length
+        );
+      } else {
+        _super.apply(this, arguments);
+      }
+    }
+  }
+
+  Assertion.overwriteMethod('below', below);
+  Assertion.overwriteMethod('lt', below);
+
+  function min (_super) {
+    return function (n) {
+      if (this._obj.__isSpy) {
+        new Assertion(this._obj).to.be.spy;
+
+        this.assert(
+            this._obj.calls.length >= n
+          , 'expected ' + this._obj + ' to have been called at least #{exp} times but got #{act}'
+          , 'expected ' + this._obj + ' to have been called fewer than #{exp} times but got #{act}'
+          , n
+          , this._obj.calls.length
+        );
+      } else {
+        _super.apply(this, arguments);
+      }
+    }
+  }
+
+  Assertion.overwriteMethod('min', min);
+  Assertion.overwriteMethod('least', min);
+
+  function max (_super) {
+    return function (n) {
+      if (this._obj.__isSpy) {
+        new Assertion(this._obj).to.be.spy;
+
+        this.assert(
+            this._obj.calls.length <=  n
+          , 'expected ' + this._obj + ' to have been called at most #{exp} times but got #{act}'
+          , 'expected ' + this._obj + ' to have been called more than #{exp} times but got #{act}'
+          , n
+          , this._obj.calls.length
+        );
+      } else {
+        _super.apply(this, arguments);
+      }
+    }
+  }
+
+  Assertion.overwriteMethod('max', max);
+  Assertion.overwriteMethod('most', max);
+}

+ 241 - 0
src/chai/chai-spies-plugin.spec.js

@@ -0,0 +1,241 @@
+import {expect} from 'chai';
+import {createSpy} from '../create-spy.js';
+import {chaiSpiesPlugin} from './chai-spies-plugin.js';
+
+describe('chaiSpiesPlugin', function () {
+  it('should be a function', function () {
+    expect(chaiSpiesPlugin).to.be.a('function');
+  });
+
+  it('should assert that object is a spy', function () {
+    const fn = () => undefined;
+    const spy = createSpy(fn);
+    expect(spy).to.be.spy;
+    expect({}).not.to.be.spy;
+  });
+
+  it('should assert that a spy has been called', function () {
+    const fn = () => undefined;
+    const spy = createSpy(fn);
+    expect(spy).to.have.been.not.called();
+    spy();
+    expect(spy).to.have.been.called();
+  });
+
+  it('should assert that something has been spied on on a certain index', function () {
+    const fn = () => undefined;
+    const spy = createSpy(fn);
+    spy('foobar1');
+    spy('foobar2');
+    spy('foobar3');
+    expect(spy).on.nth(3).be.called.with('foobar3');
+  });
+
+  it('should assert that a spy has been called exactly once', function () {
+    const fn = () => undefined;
+    const spy = createSpy(fn);
+    expect(spy).to.have.been.called.not.once;
+    spy();
+    expect(spy).to.have.been.called.once;
+    spy();
+    expect(spy).to.have.been.called.not.once;
+  });
+
+  it('should assert that a spy has been called exactly twice', function () {
+    const fn = () => undefined;
+    const spy = createSpy(fn);
+    expect(spy).to.have.been.called.not.twice;
+    spy();
+    expect(spy).to.have.been.called.not.twice;
+    spy();
+    expect(spy).to.have.been.called.twice;
+    spy();
+    expect(spy).to.have.been.called.not.twice;
+  });
+
+  it('should assert that a spy has been called exactly *n times', function () {
+    const fn = () => undefined;
+    const spy = createSpy(fn);
+    expect(spy).to.have.been.called.not.exactly(2);
+    spy();
+    expect(spy).to.have.been.called.not.exactly(2);
+    spy();
+    expect(spy).to.have.been.called.exactly(2);
+    spy();
+    expect(spy).to.have.been.called.not.exactly(2);
+  });
+
+  it('should assert that a spy has been called minimum of *n times', function () {
+    const fn = () => undefined;
+    const spy = createSpy(fn);
+    expect(spy).to.have.been.called.not.min(2);
+    spy();
+    expect(spy).to.have.been.called.not.min(2);
+    spy();
+    expect(spy).to.have.been.called.min(2);
+    spy();
+    expect(spy).to.have.been.called.min(2);
+  });
+
+  it('should assert that a spy has been called maximum of *n times', function () {
+    const fn = () => undefined;
+    const spy = createSpy(fn);
+    expect(spy).to.have.been.called.max(2);
+    spy();
+    expect(spy).to.have.been.called.max(2);
+    spy();
+    expect(spy).to.have.been.called.max(2);
+    spy();
+    expect(spy).to.have.been.called.not.max(2);
+  });
+
+  it('should assert that a spy has been called more than *n times', function () {
+    const fn = () => undefined;
+    const spy = createSpy(fn);
+    expect(spy).to.have.been.called.not.above(2);
+    spy();
+    expect(spy).to.have.been.called.not.above(2);
+    spy();
+    expect(spy).to.have.been.called.not.above(2);
+    spy();
+    expect(spy).to.have.been.called.above(2);
+  });
+
+  it('should assert that a spy has been called fewer than *n times', function () {
+    const fn = () => undefined;
+    const spy = createSpy(fn);
+    expect(spy).to.have.been.called.below(2);
+    spy();
+    expect(spy).to.have.been.called.below(2);
+    spy();
+    expect(spy).to.have.been.called.not.below(2);
+    spy();
+    expect(spy).to.have.been.called.not.below(2);
+  });
+
+  it('should assert that a spy has been called greater than *n times', function () {
+    const fn = () => undefined;
+    const spy = createSpy(fn);
+    expect(spy).to.have.been.called.not.gt(2);
+    spy();
+    expect(spy).to.have.been.called.not.gt(2);
+    spy();
+    expect(spy).to.have.been.called.not.gt(2);
+    spy();
+    expect(spy).to.have.been.called.gt(2);
+  });
+
+  it('should assert that a spy has been called lower than *n times', function () {
+    const fn = () => undefined;
+    const spy = createSpy(fn);
+    expect(spy).to.have.been.called.lt(2);
+    spy();
+    expect(spy).to.have.been.called.lt(2);
+    spy();
+    expect(spy).to.have.been.called.not.lt(2);
+    spy();
+    expect(spy).to.have.been.called.not.lt(2);
+  });
+
+  it('should assert that a spy has been called first', function () {
+    const fn = () => undefined;
+    const spy = createSpy(fn);
+    expect(spy).to.have.been.not.called.first;
+    spy();
+    expect(spy).to.have.been.called.first;
+    spy();
+    expect(spy).to.have.been.called.first;
+  });
+
+  it('should assert that a spy has been called second', function () {
+    const fn = () => undefined;
+    const spy = createSpy(fn);
+    expect(spy).to.have.been.not.called.second;
+    spy();
+    expect(spy).to.have.been.not.called.second;
+    spy();
+    expect(spy).to.have.been.called.second;
+  });
+
+  it('should assert that a spy has been called third', function () {
+    const fn = () => undefined;
+    const spy = createSpy(fn);
+    expect(spy).to.have.been.not.called.third;
+    spy();
+    expect(spy).to.have.been.not.called.third;
+    spy();
+    expect(spy).to.have.been.not.called.third;
+    spy();
+    expect(spy).to.have.been.called.third;
+  });
+
+  it('should assert that a spy has been called with given arguments at least once, even if more arguments were provided', function () {
+    const fn = () => undefined;
+    const spy = createSpy(fn);
+    expect(spy).to.have.been.not.called.with(1, 2);
+    spy();
+    expect(spy).to.have.been.not.called.with(1, 2);
+    spy(1, 2, 3);
+    expect(spy).to.have.been.called.with(1, 2);
+    expect(spy).to.have.been.called.with(1, 2, 3);
+    expect(spy).to.have.been.not.called.with(1, 2, 3, 4);
+  });
+
+  it('should assert that a spy has been called exactly with given arguments at least once', function () {
+    const fn = () => undefined;
+    const spy = createSpy(fn);
+    expect(spy).to.have.been.not.called.with.exactly(1, 2);
+    spy();
+    expect(spy).to.have.been.not.called.with.exactly(1, 2);
+    spy(1, 2, 3);
+    expect(spy).to.have.been.not.called.with.exactly(1, 2);
+    expect(spy).to.have.been.called.with.exactly(1, 2, 3);
+    expect(spy).to.have.been.not.called.with.exactly(1, 2, 3, 4);
+  });
+
+  it('should assert that every time the spy has been called the argument list contained the given arguments', function () {
+    const fn = () => undefined;
+    const spy = createSpy(fn);
+    expect(spy).to.have.been.not.called.always.with(1, 2);
+    spy(1, 2);
+    expect(spy).to.have.been.called.always.with(1, 2);
+    spy(1, 2, 3);
+    expect(spy).to.have.been.called.always.with(1, 2);
+    expect(spy).to.have.been.not.called.always.with(1, 2, 3);
+  });
+
+  it('should assert that the spy has never been called with a different list of arguments than the one provided', function () {
+    const fn = () => undefined;
+    const spy = createSpy(fn);
+    expect(spy).to.have.been.not.called.always.with.exactly(1, 2);
+    spy(1, 2);
+    expect(spy).to.have.been.called.always.with.exactly(1, 2);
+    spy(1, 2, 3);
+    expect(spy).to.have.been.not.called.always.with.exactly(1, 2);
+    expect(spy).to.have.been.not.called.always.with.exactly(1, 2, 3);
+  });
+
+  it('should assert that a spy has been called at least *n times', function () {
+    const fn = () => undefined;
+    const spy = createSpy(fn);
+    expect(spy).to.not.have.been.called.at.least(2);
+    spy();
+    expect(spy).to.not.have.been.called.at.least(2);
+    spy();
+    expect(spy).to.have.been.called.at.least(2);
+    spy();
+    expect(spy).to.have.been.called.at.least(2);
+  });
+
+  it('should assert that a spy has been called at most *n times', function () {
+    const fn = () => undefined;
+    const spy = createSpy(fn);
+    expect(spy).to.have.been.called.at.most(2);
+    spy();
+    expect(spy).to.have.been.called.at.most(2);
+    spy();
+    expect(spy).to.have.been.called.at.most(2);
+    spy();
+    expect(spy).to.not.have.been.called.at.most(2);
+  });
+});

+ 1 - 0
src/chai/index.js

@@ -0,0 +1 @@
+export * from './chai-spies-plugin.js';

+ 4 - 4
src/create-spies-group.spec.js

@@ -118,13 +118,13 @@ describe('SpiesGroup', function () {
         expect(obj2.method()).to.equal('original2');
         expect(obj2.method()).to.equal('original2');
         // проверка сброса истории
         // проверка сброса истории
         expect(spyObj1.callCount).to.equal(0);
         expect(spyObj1.callCount).to.equal(0);
-        expect(spyObj1.isCalled).to.be.false;
+        expect(spyObj1.called).to.be.false;
         expect(spyFn1.callCount).to.equal(0);
         expect(spyFn1.callCount).to.equal(0);
-        expect(spyFn1.isCalled).to.be.false;
+        expect(spyFn1.called).to.be.false;
         expect(spyObj2.callCount).to.equal(0);
         expect(spyObj2.callCount).to.equal(0);
-        expect(spyObj2.isCalled).to.be.false;
+        expect(spyObj2.called).to.be.false;
         expect(spyFn2.callCount).to.equal(0);
         expect(spyFn2.callCount).to.equal(0);
-        expect(spyFn2.isCalled).to.be.false;
+        expect(spyFn2.called).to.be.false;
       });
       });
 
 
       it('should clear the internal spies array', function () {
       it('should clear the internal spies array', function () {

+ 1 - 1
src/create-spy.d.ts

@@ -64,7 +64,7 @@ export interface Spy<TFunc extends AnyCallable = AnyCallable> {
    *
    *
    * @readonly
    * @readonly
    */
    */
-  readonly isCalled: boolean;
+  readonly called: boolean;
 
 
   /**
   /**
    * Восстанавливает оригинальный метод,
    * Восстанавливает оригинальный метод,

+ 5 - 3
src/create-spy.js

@@ -235,9 +235,9 @@ export function createSpy(
     enumerable: true,
     enumerable: true,
     configurable: false,
     configurable: false,
   });
   });
-  // определение свойства `isCalled` на шпионе,
+  // определение свойства `called` на шпионе,
   // указывающего, был ли шпион вызван
   // указывающего, был ли шпион вызван
-  Object.defineProperty(spy, 'isCalled', {
+  Object.defineProperty(spy, 'called', {
     get: () => callLog.count > 0,
     get: () => callLog.count > 0,
     enumerable: true,
     enumerable: true,
     configurable: false,
     configurable: false,
@@ -269,10 +269,12 @@ export function createSpy(
     callLog.calls = [];
     callLog.calls = [];
   };
   };
   // если создается шпион для метода объекта,
   // если создается шпион для метода объекта,
-  // оригинальный метод немедленно заменяется шпионом
+  // оригинальный метод заменяется шпионом
   if (isMethodSpy && objToSpyOn) {
   if (isMethodSpy && objToSpyOn) {
     objToSpyOn[methodName] = spy;
     objToSpyOn[methodName] = spy;
   }
   }
+  // флаг для определения шпиона
+  spy.__isSpy = true;
   // возврат созданной и настроенной
   // возврат созданной и настроенной
   // функции-шпиона
   // функции-шпиона
   return spy;
   return spy;

+ 20 - 16
src/create-spy.spec.js

@@ -96,8 +96,8 @@ describe('createSpy', function () {
     it('should not be called initially', function () {
     it('should not be called initially', function () {
       // создание шпиона
       // создание шпиона
       const spy = createSpy(function () {});
       const spy = createSpy(function () {});
-      // первоначальное состояние свойства isCalled
-      expect(spy.isCalled).to.be.false;
+      // первоначальное состояние свойства called
+      expect(spy.called).to.be.false;
       // первоначальное значение счетчика вызовов
       // первоначальное значение счетчика вызовов
       expect(spy.callCount).to.be.eq(0);
       expect(spy.callCount).to.be.eq(0);
     });
     });
@@ -112,9 +112,9 @@ describe('createSpy', function () {
       // второй вызов шпиона
       // второй вызов шпиона
       spy(3, 4);
       spy(3, 4);
 
 
-      // состояние свойства isCalled
+      // состояние свойства called
       // после вызовов
       // после вызовов
-      expect(spy.isCalled).to.be.true;
+      expect(spy.called).to.be.true;
       // значение счетчика вызовов
       // значение счетчика вызовов
       expect(spy.callCount).to.be.eq(2);
       expect(spy.callCount).to.be.eq(2);
 
 
@@ -135,7 +135,7 @@ describe('createSpy', function () {
       // проверка возвращенного значения
       // проверка возвращенного значения
       expect(result).to.be.eq('original value');
       expect(result).to.be.eq('original value');
       // проверка того, что шпион был вызван
       // проверка того, что шпион был вызван
-      expect(spy.isCalled).to.be.true;
+      expect(spy.called).to.be.true;
     });
     });
 
 
     it('should use the custom implementation if provided', function () {
     it('should use the custom implementation if provided', function () {
@@ -150,8 +150,8 @@ describe('createSpy', function () {
       // проверка того, что возвращено значение
       // проверка того, что возвращено значение
       // из пользовательской реализации
       // из пользовательской реализации
       expect(result).to.be.eq('custom');
       expect(result).to.be.eq('custom');
-      // проверка свойства isCalled
-      expect(spy.isCalled).to.be.true;
+      // проверка свойства called
+      expect(spy.called).to.be.true;
     });
     });
 
 
     it('should preserve `this` context for the original function', function () {
     it('should preserve `this` context for the original function', function () {
@@ -199,14 +199,14 @@ describe('createSpy', function () {
         // вызов шпиона, чтобы у него была история
         // вызов шпиона, чтобы у него была история
         // @ts-ignore
         // @ts-ignore
         fnSpy('call standalone');
         fnSpy('call standalone');
-        expect(fnSpy.isCalled).to.be.true;
+        expect(fnSpy.called).to.be.true;
         expect(fnSpy.callCount).to.be.eq(1);
         expect(fnSpy.callCount).to.be.eq(1);
         expect(fnSpy.calls[0].args).to.deep.equal(['call standalone']);
         expect(fnSpy.calls[0].args).to.deep.equal(['call standalone']);
         // проверка, что вызов restore не вызывает ошибок
         // проверка, что вызов restore не вызывает ошибок
         expect(() => fnSpy.restore()).to.not.throw();
         expect(() => fnSpy.restore()).to.not.throw();
         // проверки сброса истории
         // проверки сброса истории
         expect(fnSpy.callCount).to.be.eq(0);
         expect(fnSpy.callCount).to.be.eq(0);
-        expect(fnSpy.isCalled).to.be.false;
+        expect(fnSpy.called).to.be.false;
       });
       });
     });
     });
   });
   });
@@ -283,7 +283,7 @@ describe('createSpy', function () {
         const spy = createSpy(obj, 'method');
         const spy = createSpy(obj, 'method');
         // вызов шпиона, чтобы у него была история
         // вызов шпиона, чтобы у него была история
         obj.method('call before restore');
         obj.method('call before restore');
-        expect(spy.isCalled).to.be.true;
+        expect(spy.called).to.be.true;
         expect(spy.callCount).to.be.eq(1);
         expect(spy.callCount).to.be.eq(1);
         expect(obj.method).to.be.eq(spy);
         expect(obj.method).to.be.eq(spy);
         // вызов метода restore на шпионе
         // вызов метода restore на шпионе
@@ -297,7 +297,7 @@ describe('createSpy', function () {
         expect(result).to.be.eq('original: TestObj call after restore');
         expect(result).to.be.eq('original: TestObj call after restore');
         // проверки сброса истории
         // проверки сброса истории
         expect(spy.callCount).to.be.eq(0);
         expect(spy.callCount).to.be.eq(0);
-        expect(spy.isCalled).to.be.false;
+        expect(spy.called).to.be.false;
       });
       });
 
 
       it('should restore an "own" property by assigning the original value back', function () {
       it('should restore an "own" property by assigning the original value back', function () {
@@ -354,6 +354,10 @@ describe('createSpy', function () {
       spy = createSpy(targetFn);
       spy = createSpy(targetFn);
     });
     });
 
 
+    it('should have the __isSpy property with true value', function () {
+      expect(spy['__isSpy']).to.be.true;
+    });
+
     describe('.calls', function () {
     describe('.calls', function () {
       it('should return an array of CallInfo', function () {
       it('should return an array of CallInfo', function () {
         expect(spy.calls).to.be.eql([]);
         expect(spy.calls).to.be.eql([]);
@@ -375,12 +379,12 @@ describe('createSpy', function () {
       });
       });
     });
     });
 
 
-    describe('.callCount and .isCalled', function () {
-      it('should have callCount = 0 and isCalled = false initially', function () {
+    describe('.callCount and .called', function () {
+      it('should have callCount = 0 and called = false initially', function () {
         // начальное состояние счетчика вызовов
         // начальное состояние счетчика вызовов
         expect(spy.callCount).to.be.eq(0);
         expect(spy.callCount).to.be.eq(0);
-        // начальное состояние флага isCalled
-        expect(spy.isCalled).to.be.false;
+        // начальное состояние флага called
+        expect(spy.called).to.be.false;
       });
       });
 
 
       it('should update after calls', function () {
       it('should update after calls', function () {
@@ -388,7 +392,7 @@ describe('createSpy', function () {
         spy(1, 1);
         spy(1, 1);
         // состояние после первого вызова
         // состояние после первого вызова
         expect(spy.callCount).to.be.eq(1);
         expect(spy.callCount).to.be.eq(1);
-        expect(spy.isCalled).to.be.true;
+        expect(spy.called).to.be.true;
         // второй вызов шпиона
         // второй вызов шпиона
         spy(2, 2);
         spy(2, 2);
         // состояние после второго вызова
         // состояние после второго вызова