FormidableLabs/chai-jq

View on GitHub
chai-jq.js

Summary

Maintainability
C
7 hrs
Test Coverage
/*eslint-disable strict*/
/*!
 * chai-jq
 * -------
 * An alternate jQuery assertion library for Chai.
 */
(function () {
  /*eslint-disable consistent-this*/
  var root = this;
  /*eslint-enable consistent-this*/

  /*!
   * Chai jQuery plugin implementation.
   */
  /*eslint-disable max-statements*/
  var chaiJq = function (chai, utils) {
    // ------------------------------------------------------------------------
    // Variables
    // ------------------------------------------------------------------------
    var flag = utils.flag;
    var toString = Object.prototype.toString;

    // ------------------------------------------------------------------------
    // Helpers
    // ------------------------------------------------------------------------
    /*!
     * Give a more useful element name.
     */
    var _elName = function ($el) {
      // Detect if completely empty.
      if (!$el || $el.length === 0) {
        return "<EMPTY OBJECT>";
      }

      var name = "";
      var id = $el.attr("id");
      var cls = $el.attr("class") || "";

      // Try CSS selector id.
      if (id) {
        name += "#" + id;
      }
      if (cls) {
        name += "." + cls.split(" ").join(".");
      }
      if (name) {
        return "'" + name + "'";
      }

      // Give up.
      return $el;
    };

    // ------------------------------------------------------------------------
    // Type Inference
    //
    // (Inspired by Underscore)
    // ------------------------------------------------------------------------
    var _isRegExp = function (val) {
      return toString.call(val) === "[object RegExp]";
    };

    // ------------------------------------------------------------------------
    // Comparisons
    // ------------------------------------------------------------------------
    var _equals = function (exp, act) {
      return exp === act;
    };

    var _contains = function (exp, act) {
      return act.indexOf(exp) !== -1;
    };

    var _exists = function (exp, act) {
      return act !== undefined;
    };

    var _regExpMatch = function (expRe, act) {
      return expRe.exec(act);
    };

    // ------------------------------------------------------------------------
    // Assertions (Internal)
    // ------------------------------------------------------------------------
    /*!
     * Wrap assert function and add properties.
     */
    var _jqAssert = function (fn) {
      return function (exp, msg) {
        /*eslint-disable no-invalid-this*/
        // Set properties.
        this._$el = flag(this, "object");
        this._name = _elName(this._$el);

        // Flag message.
        if (msg) {
          flag(this, "message", msg);
        }

        // Invoke assertion function.
        fn.apply(this, arguments);
      };
    };

    /*!
     * Base for the boolean is("selector") method call.
     *
     * @see http://api.jquery.com/is/]
     *
     * @param {String} selector jQuery selector to match against
     */
    var _isMethod = function (jqSelector) {
      // Make selector human readable.
      var selectorDesc = jqSelector.replace(/:/g, "");

      // Return decorated assert.
      return _jqAssert(function () {
        /*eslint-disable no-invalid-this*/
        this.assert(
          this._$el.is(jqSelector),
          "expected " + this._name + " to be " + selectorDesc,
          "expected " + this._name + " to not be " + selectorDesc
        );
      });
    };

    /*!
     * Abstract base for a "containable" method call.
     *
     * @param {String} jQuery           method name.
     * @param {Object} opts             options
     * @param {String} opts.hasArg      takes argument for method
     * @param {String} opts.isProperty  switch assert context to property if no
     *                                  expected val
     * @param {String} opts.hasContains is "contains" applicable
     * @param {String} opts.altGet      alternate function to get value if none
     */
    var _containMethod = function (jqMeth, opts) {
      // Unpack options.
      opts = opts || /* istanbul ignore next */ {};
      opts.hasArg = !!opts.hasArg;
      opts.isProperty = !!opts.isProperty;
      opts.hasContains = !!opts.hasContains;
      opts.defaultAct = undefined;

      // Return decorated assert.
      /*eslint-disable complexity*/
      return _jqAssert(function () {
        /*eslint-disable no-invalid-this*/
        // Arguments.
        var exp = arguments[opts.hasArg ? 1 : 0];
        var arg = opts.hasArg ? arguments[0] : undefined;

        // Switch context to property / check mere presence.
        var noExp = arguments.length === (opts.hasArg ? 1 : 0);
        var isProp = opts.isProperty && noExp;

        // Method.
        var act = opts.hasArg ? this._$el[jqMeth](arg) : this._$el[jqMeth]();
        var meth = opts.hasArg ? jqMeth + "('" + arg + "')" : jqMeth;

        // Assertion type.
        var contains = !isProp && opts.hasContains && flag(this, "contains");
        var have = contains ? "contain" : "have";
        var comp = _equals;

        // Set comparison.
        if (isProp) {
          comp = _exists;
        } else if (contains) {
          comp = _contains;
        }

        // Second chance getter.
        if (opts.altGet && !act) {
          act = opts.altGet(this._$el, arg);
        }

        // Default actual value on undefined.
        if (typeof act === "undefined") {
          act = opts.defaultAct;
        }

        // Same context assertion.
        this.assert(
          comp(exp, act),
          "expected " + this._name + " to " + have + " " + meth +
            (isProp ? "" : " #{exp} but found #{act}"),
          "expected " + this._name + " not to " + have + " " + meth +
            (isProp ? "" : " #{exp}"),
          exp,
          act
        );

        // Change context if property and not negated.
        if (isProp && !flag(this, "negate")) {
          flag(this, "object", act);
        }
      });
      /*eslint-enable complexity*/
    };

    // ------------------------------------------------------------------------
    // API
    // ------------------------------------------------------------------------

    /**
     * Asserts that the element is visible.
     *
     * *Node.js/JsDom Note*: JsDom does not currently infer zero-sized or
     * hidden parent elements as hidden / visible appropriately.
     *
     * ```js
     * expect($("<div>&nbsp;</div>"))
     *   .to.be.$visible;
     * ```
     *
     * @see http://api.jquery.com/visible-selector/
     *
     * @api public
     */
    var $visible = _isMethod(":visible");

    chai.Assertion.addProperty("$visible", $visible);

    /**
     * Asserts that the element is hidden.
     *
     * *Node.js/JsDom Note*: JsDom does not currently infer zero-sized or
     * hidden parent elements as hidden / visible appropriately.
     *
     * ```js
     * expect($("<div style=\"display: none\" />"))
     *   .to.be.$hidden;
     * ```
     *
     * @see http://api.jquery.com/hidden-selector/
     *
     * @api public
     */
    var $hidden = _isMethod(":hidden");

    chai.Assertion.addProperty("$hidden", $hidden);

    /**
     * Asserts that the element value matches a string or regular expression.
     *
     * ```js
     * expect($("<input value='foo' />"))
     *   .to.have.$val("foo").and
     *   .to.have.$val(/^foo/);
     * ```
     *
     * @see http://api.jquery.com/val/
     *
     * @param {String|RegExp} expected  value
     * @param {String}        message   failure message (_optional_)
     * @api public
     */
    var $val = _jqAssert(function (exp) {
      /*eslint-disable no-invalid-this*/
      // Manually check empty elements for `.val` call b/c ie9 can otherwise
      // report `Unspecified error.` at least in Sauce Labs.
      var act = this._$el && this._$el.length > 0 ? this._$el.val() : undefined;
      var comp = _isRegExp(exp) ? _regExpMatch : _equals;

      this.assert(
        comp(exp, act),
        "expected " + this._name + " to have val #{exp} but found #{act}",
        "expected " + this._name + " not to have val #{exp}",
        exp,
        typeof act === "undefined" ? "undefined" : act
      );
    });

    chai.Assertion.addMethod("$val", $val);

    /**
     * Asserts that the element has a class match.
     *
     * ```js
     * expect($("<div class='foo bar' />"))
     *   .to.have.$class("foo").and
     *   .to.have.$class("bar");
     * ```
     *
     * @see http://api.jquery.com/hasClass/
     *
     * @param {String} expected class name
     * @param {String} message  failure message (_optional_)
     * @api public
     */
    var $class = _jqAssert(function (exp) {
      /*eslint-disable no-invalid-this*/
      var act = this._$el.attr("class") || "";

      this.assert(
        this._$el.hasClass(exp),
        "expected " + this._name + " to have class #{exp} but found #{act}",
        "expected " + this._name + " not to have class #{exp}",
        exp,
        act
      );
    });

    chai.Assertion.addMethod("$class", $class);

    /**
     * Asserts that the target has exactly the given named attribute, or
     * asserts the target contains a subset of the attribute when using the
     * `include` or `contain` modifiers.
     *
     * ```js
     * expect($("<div id=\"hi\" foo=\"bar time\" />"))
     *   .to.have.$attr("id", "hi").and
     *   .to.contain.$attr("foo", "bar");
     * ```
     *
     * Changes context to attribute string *value* when no expected value is
     * provided:
     *
     * ```js
     * expect($("<div id=\"hi\" foo=\"bar time\" />"))
     *   .to.have.$attr("foo").and
     *     .to.equal("bar time").and
     *     .to.match(/^b/);
     * ```
     *
     * @see http://api.jquery.com/attr/
     *
     * @param {String} name     attribute name
     * @param {String} expected attribute content (_optional_)
     * @param {String} message  failure message (_optional_)
     * @returns current object or attribute string value
     * @api public
     */
    var $attr = _containMethod("attr", {
      hasArg: true,
      hasContains: true,
      isProperty: true
    });

    chai.Assertion.addMethod("$attr", $attr);

    /**
     * Asserts that the target has exactly the given named
     * data-attribute, or asserts the target contains a subset
     * of the data-attribute when using the
     * `include` or `contain` modifiers.
     *
     * ```js
     * expect($("<div data-id=\"hi\" data-foo=\"bar time\" />"))
     *   .to.have.$data("id", "hi").and
     *   .to.contain.$data("foo", "bar");
     * ```
     *
     * Changes context to data-attribute string *value* when no
     * expected value is provided:
     *
     * ```js
     * expect($("<div data-id=\"hi\" data-foo=\"bar time\" />"))
     *   .to.have.$data("foo").and
     *     .to.equal("bar time").and
     *     .to.match(/^b/);
     * ```
     *
     * @see http://api.jquery.com/data/
     *
     * @param {String} name     data-attribute name
     * @param {String} expected data-attribute content (_optional_)
     * @param {String} message  failure message (_optional_)
     * @returns current object or attribute string value
     * @api public
     */
    var $data = _containMethod("data", {
      hasArg: true,
      hasContains: true,
      isProperty: true
    });

    chai.Assertion.addMethod("$data", $data);

    /**
     * Asserts that the target has exactly the given named property.
     *
     * ```js
     * expect($("<input type=\"checkbox\" checked=\"checked\" />"))
     *   .to.have.$prop("checked", true).and
     *   .to.have.$prop("type", "checkbox");
     * ```
     *
     * Changes context to property string *value* when no expected value is
     * provided:
     *
     * ```js
     * expect($("<input type=\"checkbox\" checked=\"checked\" />"))
     *   .to.have.$prop("type").and
     *     .to.equal("checkbox").and
     *     .to.match(/^c.*x$/);
     * ```
     *
     * @see http://api.jquery.com/prop/
     *
     * @param {String} name     property name
     * @param {Object} expected property value (_optional_)
     * @param {String} message  failure message (_optional_)
     * @returns current object or property string value
     * @api public
     */
    var $prop = _containMethod("prop", {
      hasArg: true,
      isProperty: true
    });

    chai.Assertion.addMethod("$prop", $prop);

    /**
     * Asserts that the target has exactly the given HTML, or
     * asserts the target contains a subset of the HTML when using the
     * `include` or `contain` modifiers.
     *
     * ```js
     * expect($("<div><span>foo</span></div>"))
     *   .to.have.$html("<span>foo</span>").and
     *   .to.contain.$html("foo");
     * ```
     *
     * @see http://api.jquery.com/html/
     *
     * @param {String} expected HTML content
     * @param {String} message  failure message (_optional_)
     * @api public
     */
    var $html = _containMethod("html", {
      hasContains: true
    });

    chai.Assertion.addMethod("$html", $html);

    /**
     * Asserts that the target has exactly the given text, or
     * asserts the target contains a subset of the text when using the
     * `include` or `contain` modifiers.
     *
     * ```js
     * expect($("<div><span>foo</span> bar</div>"))
     *   .to.have.$text("foo bar").and
     *   .to.contain.$text("foo");
     * ```
     *
     * @see http://api.jquery.com/text/
     *
     * @name $text
     * @param {String} expected text content
     * @param {String} message  failure message (_optional_)
     * @api public
     */
    var $text = _containMethod("text", {
      hasContains: true
    });

    chai.Assertion.addMethod("$text", $text);

    /**
     * Asserts that the target has exactly the given CSS property, or
     * asserts the target contains a subset of the CSS when using the
     * `include` or `contain` modifiers.
     *
     * *Node.js/JsDom Note*: Computed CSS properties are not correctly
     * inferred as of JsDom v0.8.8. Explicit ones should get matched exactly.
     *
     * *Browser Note*: Explicit CSS properties are sometimes not matched
     * (in contrast to Node.js), so the plugin performs an extra check against
     * explicit `style` properties for a match. May still have other wonky
     * corner cases.
     *
     * *PhantomJS Note*: PhantomJS also is fairly wonky and unpredictable with
     * respect to CSS / styles, especially those that come from CSS classes
     * and not explicity `style` attributes.
     *
     * ```js
     * expect($("<div style=\"width: 50px; border: 1px dotted black;\" />"))
     *   .to.have.$css("width", "50px").and
     *   .to.have.$css("border-top-style", "dotted");
     * ```
     *
     * @see http://api.jquery.com/css/
     *
     * @name $css
     * @param {String} expected CSS property content
     * @param {String} message  failure message (_optional_)
     * @api public
     */
    var $css = _containMethod("css", {
      hasArg: true,
      hasContains: true,

      // Alternate Getter: If no match, go for explicit property.
      altGet: function ($el, prop) { return $el.prop("style")[prop]; }
    });

    chai.Assertion.addMethod("$css", $css);
  };

  /* istanbul ignore next */
  /*!
   * Wrap AMD, etc. using boilerplate.
   */
  var wrap = function (plugin) {
    /* global module:false, define:false */

    if (typeof require === "function" &&
        typeof exports === "object" &&
        typeof module === "object") {
      // NodeJS
      module.exports = plugin;

    } else if (typeof define === "function" && define.amd) {
      // AMD: Assumes importing `chai` and `jquery`. Returns a function to
      //      inject with `chai.use()`.
      //
      // See: https://github.com/chaijs/chai-jquery/issues/27
      define(["jquery"], function ($) {
        return function (chai, utils) {
          return plugin(chai, utils, $);
        };
      });

    } else {
      // Other environment (usually <script> tag): plug in to global chai
      // instance directly.
      root.chai.use(function (chai, utils) {
        return plugin(chai, utils, root.jQuery);
      });
    }
  };
  /*eslint-enable max-statements*/

  // Hook it all together.
  wrap(chaiJq);
}());