e22m4u 2 дней назад
Родитель
Сommit
d826f6fd32

+ 2 - 1
.mocharc.json

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

+ 2 - 1
dist/cjs/index.cjs

@@ -146,7 +146,7 @@ function createSpy(target = void 0, methodNameOrImpl = void 0, customImplForMeth
     enumerable: true,
     configurable: false
   });
-  Object.defineProperty(spy, "isCalled", {
+  Object.defineProperty(spy, "called", {
     get: /* @__PURE__ */ __name(() => callLog.count > 0, "get"),
     enumerable: true,
     configurable: false
@@ -167,6 +167,7 @@ function createSpy(target = void 0, methodNameOrImpl = void 0, customImplForMeth
   if (isMethodSpy && objToSpyOn) {
     objToSpyOn[methodName] = spy;
   }
+  spy.__isSpy = true;
   return spy;
 }
 __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(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.isCalled).to.be.false;
+        expect(spyFn1.called).to.be.false;
         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.isCalled).to.be.false;
+        expect(spyFn2.called).to.be.false;
       });
 
       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 isCalled: boolean;
+  readonly called: boolean;
 
   /**
    * Восстанавливает оригинальный метод,

+ 5 - 3
src/create-spy.js

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

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

@@ -96,8 +96,8 @@ describe('createSpy', function () {
     it('should not be called initially', 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);
     });
@@ -112,9 +112,9 @@ describe('createSpy', function () {
       // второй вызов шпиона
       spy(3, 4);
 
-      // состояние свойства isCalled
+      // состояние свойства called
       // после вызовов
-      expect(spy.isCalled).to.be.true;
+      expect(spy.called).to.be.true;
       // значение счетчика вызовов
       expect(spy.callCount).to.be.eq(2);
 
@@ -135,7 +135,7 @@ describe('createSpy', function () {
       // проверка возвращенного значения
       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 () {
@@ -150,8 +150,8 @@ describe('createSpy', function () {
       // проверка того, что возвращено значение
       // из пользовательской реализации
       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 () {
@@ -199,14 +199,14 @@ describe('createSpy', function () {
         // вызов шпиона, чтобы у него была история
         // @ts-ignore
         fnSpy('call standalone');
-        expect(fnSpy.isCalled).to.be.true;
+        expect(fnSpy.called).to.be.true;
         expect(fnSpy.callCount).to.be.eq(1);
         expect(fnSpy.calls[0].args).to.deep.equal(['call standalone']);
         // проверка, что вызов restore не вызывает ошибок
         expect(() => fnSpy.restore()).to.not.throw();
         // проверки сброса истории
         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');
         // вызов шпиона, чтобы у него была история
         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(obj.method).to.be.eq(spy);
         // вызов метода restore на шпионе
@@ -297,7 +297,7 @@ describe('createSpy', function () {
         expect(result).to.be.eq('original: TestObj call after restore');
         // проверки сброса истории
         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 () {
@@ -354,6 +354,10 @@ describe('createSpy', function () {
       spy = createSpy(targetFn);
     });
 
+    it('should have the __isSpy property with true value', function () {
+      expect(spy['__isSpy']).to.be.true;
+    });
+
     describe('.calls', function () {
       it('should return an array of CallInfo', function () {
         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);
-        // начальное состояние флага isCalled
-        expect(spy.isCalled).to.be.false;
+        // начальное состояние флага called
+        expect(spy.called).to.be.false;
       });
 
       it('should update after calls', function () {
@@ -388,7 +392,7 @@ describe('createSpy', function () {
         spy(1, 1);
         // состояние после первого вызова
         expect(spy.callCount).to.be.eq(1);
-        expect(spy.isCalled).to.be.true;
+        expect(spy.called).to.be.true;
         // второй вызов шпиона
         spy(2, 2);
         // состояние после второго вызова