UNPKG

15.8 kBJavaScriptView Raw
1/*!
2 * chai-jq
3 * -------
4 * An alternate jQuery assertion library for Chai.
5 */
6(function () {
7 var root = this;
8
9 /*!
10 * Chai jQuery plugin implementation.
11 */
12 function chaiJq(chai, utils) {
13 "use strict";
14
15 // ------------------------------------------------------------------------
16 // Variables
17 // ------------------------------------------------------------------------
18 var flag = utils.flag,
19 toString = Object.prototype.toString;
20
21 // ------------------------------------------------------------------------
22 // Helpers
23 // ------------------------------------------------------------------------
24 /*!
25 * Give a more useful element name.
26 */
27 var _elName = function ($el) {
28 // Detect if completely empty.
29 if (!$el || $el.length === 0) {
30 return "<EMPTY OBJECT>";
31 }
32
33 var name = "",
34 id = $el.attr("id"),
35 cls = $el.attr("class") || "";
36
37 // Try CSS selector id.
38 if (id) {
39 name += "#" + id;
40 }
41 if (cls) {
42 name += "." + cls.split(" ").join(".");
43 }
44 if (name) {
45 return "'" + name + "'";
46 }
47
48 // Give up.
49 return $el;
50 };
51
52 // ------------------------------------------------------------------------
53 // Type Inference
54 //
55 // (Inspired by Underscore)
56 // ------------------------------------------------------------------------
57 var _isRegExp = function (val) {
58 return toString.call(val) === "[object RegExp]";
59 };
60
61 // ------------------------------------------------------------------------
62 // Comparisons
63 // ------------------------------------------------------------------------
64 var _equals = function (exp, act) {
65 return exp === act;
66 };
67
68 var _contains = function (exp, act) {
69 return act.indexOf(exp) !== -1;
70 };
71
72 var _exists = function (exp, act) {
73 return act !== undefined;
74 };
75
76 var _regExpMatch = function (expRe, act) {
77 return expRe.exec(act);
78 };
79
80 // ------------------------------------------------------------------------
81 // Assertions (Internal)
82 // ------------------------------------------------------------------------
83 /*!
84 * Wrap assert function and add properties.
85 */
86 var _jqAssert = function (fn) {
87 return function (exp, msg) {
88 // Set properties.
89 this._$el = flag(this, "object");
90 this._name = _elName(this._$el);
91
92 // Flag message.
93 if (msg) {
94 flag(this, "message", msg);
95 }
96
97 // Invoke assertion function.
98 fn.apply(this, arguments);
99 };
100 };
101
102 /*!
103 * Base for the boolean is("selector") method call.
104 *
105 * @see http://api.jquery.com/is/]
106 *
107 * @param {String} selector jQuery selector to match against
108 */
109 var _isMethod = function (jqSelector) {
110 // Make selector human readable.
111 var selectorDesc = jqSelector.replace(/:/g, "");
112
113 // Return decorated assert.
114 return _jqAssert(function () {
115 this.assert(
116 this._$el.is(jqSelector),
117 "expected " + this._name + " to be " + selectorDesc,
118 "expected " + this._name + " to not be " + selectorDesc
119 );
120 });
121 };
122
123 /*!
124 * Abstract base for a "containable" method call.
125 *
126 * @param {String} jQuery method name.
127 * @param {Object} opts options
128 * @param {String} opts.hasArg takes argument for method
129 * @param {String} opts.isProperty switch assert context to property if no
130 * expected val
131 * @param {String} opts.hasContains is "contains" applicable
132 * @param {String} opts.altGet alternate function to get value if none
133 */
134 var _containMethod = function (jqMeth, opts) {
135 // Unpack options.
136 opts = opts || /* istanbul ignore next */ {};
137 opts.hasArg = !!opts.hasArg;
138 opts.isProperty = !!opts.isProperty;
139 opts.hasContains = !!opts.hasContains;
140 opts.defaultAct = undefined;
141
142 // Return decorated assert.
143 return _jqAssert(function () {
144 // Arguments.
145 var exp = arguments[opts.hasArg ? 1 : 0],
146 arg = opts.hasArg ? arguments[0] : undefined,
147
148 // Switch context to property / check mere presence.
149 noExp = arguments.length === (opts.hasArg ? 1 : 0),
150 isProp = opts.isProperty && noExp,
151
152 // Method.
153 act = (opts.hasArg ? this._$el[jqMeth](arg) : this._$el[jqMeth]()),
154 meth = opts.hasArg ? jqMeth + "('" + arg + "')" : jqMeth,
155
156 // Assertion type.
157 contains = !isProp && opts.hasContains && flag(this, "contains"),
158 have = contains ? "contain" : "have",
159 comp = _equals;
160
161 // Set comparison.
162 if (isProp) {
163 comp = _exists;
164 } else if (contains) {
165 comp = _contains;
166 }
167
168 // Second chance getter.
169 if (opts.altGet && !act) {
170 act = opts.altGet(this._$el, arg);
171 }
172
173 // Default actual value on undefined.
174 if (typeof act === "undefined") {
175 act = opts.defaultAct;
176 }
177
178 // Same context assertion.
179 this.assert(
180 comp(exp, act),
181 "expected " + this._name + " to " + have + " " + meth +
182 (isProp ? "" : " #{exp} but found #{act}"),
183 "expected " + this._name + " not to " + have + " " + meth +
184 (isProp ? "" : " #{exp}"),
185 exp,
186 act
187 );
188
189 // Change context if property and not negated.
190 if (isProp && !flag(this, "negate")) {
191 flag(this, "object", act);
192 }
193 });
194 };
195
196 // ------------------------------------------------------------------------
197 // API
198 // ------------------------------------------------------------------------
199
200 /**
201 * Asserts that the element is visible.
202 *
203 * *Node.js/JsDom Note*: JsDom does not currently infer zero-sized or
204 * hidden parent elements as hidden / visible appropriately.
205 *
206 * ```js
207 * expect($("<div>&nbsp;</div>"))
208 * .to.be.$visible;
209 * ```
210 *
211 * @see http://api.jquery.com/visible-selector/
212 *
213 * @api public
214 */
215 var $visible = _isMethod(":visible");
216
217 chai.Assertion.addProperty("$visible", $visible);
218
219 /**
220 * Asserts that the element is hidden.
221 *
222 * *Node.js/JsDom Note*: JsDom does not currently infer zero-sized or
223 * hidden parent elements as hidden / visible appropriately.
224 *
225 * ```js
226 * expect($("<div style=\"display: none\" />"))
227 * .to.be.$hidden;
228 * ```
229 *
230 * @see http://api.jquery.com/hidden-selector/
231 *
232 * @api public
233 */
234 var $hidden = _isMethod(":hidden");
235
236 chai.Assertion.addProperty("$hidden", $hidden);
237
238 /**
239 * Asserts that the element value matches a string or regular expression.
240 *
241 * ```js
242 * expect($("<input value='foo' />"))
243 * .to.have.$val("foo").and
244 * .to.have.$val(/^foo/);
245 * ```
246 *
247 * @see http://api.jquery.com/val/
248 *
249 * @param {String|RegExp} expected value
250 * @param {String} message failure message (_optional_)
251 * @api public
252 */
253 var $val = _jqAssert(function (exp) {
254 // Manually check empty elements for `.val` call b/c ie9 can otherwise
255 // report `Unspecified error.` at least in Sauce Labs.
256 var act = this._$el && this._$el.length > 0 ? this._$el.val() : undefined,
257 comp = _isRegExp(exp) ? _regExpMatch : _equals;
258
259 this.assert(
260 comp(exp, act),
261 "expected " + this._name + " to have val #{exp} but found #{act}",
262 "expected " + this._name + " not to have val #{exp}",
263 exp,
264 typeof act === "undefined" ? "undefined" : act
265 );
266 });
267
268 chai.Assertion.addMethod("$val", $val);
269
270 /**
271 * Asserts that the element has a class match.
272 *
273 * ```js
274 * expect($("<div class='foo bar' />"))
275 * .to.have.$class("foo").and
276 * .to.have.$class("bar");
277 * ```
278 *
279 * @see http://api.jquery.com/hasClass/
280 *
281 * @param {String} expected class name
282 * @param {String} message failure message (_optional_)
283 * @api public
284 */
285 var $class = _jqAssert(function (exp) {
286 var act = this._$el.attr("class") || "";
287
288 this.assert(
289 this._$el.hasClass(exp),
290 "expected " + this._name + " to have class #{exp} but found #{act}",
291 "expected " + this._name + " not to have class #{exp}",
292 exp,
293 act
294 );
295 });
296
297 chai.Assertion.addMethod("$class", $class);
298
299 /**
300 * Asserts that the target has exactly the given named attribute, or
301 * asserts the target contains a subset of the attribute when using the
302 * `include` or `contain` modifiers.
303 *
304 * ```js
305 * expect($("<div id=\"hi\" foo=\"bar time\" />"))
306 * .to.have.$attr("id", "hi").and
307 * .to.contain.$attr("foo", "bar");
308 * ```
309 *
310 * Changes context to attribute string *value* when no expected value is
311 * provided:
312 *
313 * ```js
314 * expect($("<div id=\"hi\" foo=\"bar time\" />"))
315 * .to.have.$attr("foo").and
316 * .to.equal("bar time").and
317 * .to.match(/^b/);
318 * ```
319 *
320 * @see http://api.jquery.com/attr/
321 *
322 * @param {String} name attribute name
323 * @param {String} expected attribute content (_optional_)
324 * @param {String} message failure message (_optional_)
325 * @returns current object or attribute string value
326 * @api public
327 */
328 var $attr = _containMethod("attr", {
329 hasArg: true,
330 hasContains: true,
331 isProperty: true
332 });
333
334 chai.Assertion.addMethod("$attr", $attr);
335
336 /**
337 * Asserts that the target has exactly the given named
338 * data-attribute, or asserts the target contains a subset
339 * of the data-attribute when using the
340 * `include` or `contain` modifiers.
341 *
342 * ```js
343 * expect($("<div data-id=\"hi\" data-foo=\"bar time\" />"))
344 * .to.have.$data("id", "hi").and
345 * .to.contain.$data("foo", "bar");
346 * ```
347 *
348 * Changes context to data-attribute string *value* when no
349 * expected value is provided:
350 *
351 * ```js
352 * expect($("<div data-id=\"hi\" data-foo=\"bar time\" />"))
353 * .to.have.$data("foo").and
354 * .to.equal("bar time").and
355 * .to.match(/^b/);
356 * ```
357 *
358 * @see http://api.jquery.com/data/
359 *
360 * @param {String} name data-attribute name
361 * @param {String} expected data-attribute content (_optional_)
362 * @param {String} message failure message (_optional_)
363 * @returns current object or attribute string value
364 * @api public
365 */
366 var $data = _containMethod("data", {
367 hasArg: true,
368 hasContains: true,
369 isProperty: true
370 });
371
372 chai.Assertion.addMethod("$data", $data);
373
374 /**
375 * Asserts that the target has exactly the given named property.
376 *
377 * ```js
378 * expect($("<input type=\"checkbox\" checked=\"checked\" />"))
379 * .to.have.$prop("checked", true).and
380 * .to.have.$prop("type", "checkbox");
381 * ```
382 *
383 * Changes context to property string *value* when no expected value is
384 * provided:
385 *
386 * ```js
387 * expect($("<input type=\"checkbox\" checked=\"checked\" />"))
388 * .to.have.$prop("type").and
389 * .to.equal("checkbox").and
390 * .to.match(/^c.*x$/);
391 * ```
392 *
393 * @see http://api.jquery.com/prop/
394 *
395 * @param {String} name property name
396 * @param {Object} expected property value (_optional_)
397 * @param {String} message failure message (_optional_)
398 * @returns current object or property string value
399 * @api public
400 */
401 var $prop = _containMethod("prop", {
402 hasArg: true,
403 isProperty: true
404 });
405
406 chai.Assertion.addMethod("$prop", $prop);
407
408 /**
409 * Asserts that the target has exactly the given HTML, or
410 * asserts the target contains a subset of the HTML when using the
411 * `include` or `contain` modifiers.
412 *
413 * ```js
414 * expect($("<div><span>foo</span></div>"))
415 * .to.have.$html("<span>foo</span>").and
416 * .to.contain.$html("foo");
417 * ```
418 *
419 * @see http://api.jquery.com/html/
420 *
421 * @param {String} expected HTML content
422 * @param {String} message failure message (_optional_)
423 * @api public
424 */
425 var $html = _containMethod("html", {
426 hasContains: true
427 });
428
429 chai.Assertion.addMethod("$html", $html);
430
431 /**
432 * Asserts that the target has exactly the given text, or
433 * asserts the target contains a subset of the text when using the
434 * `include` or `contain` modifiers.
435 *
436 * ```js
437 * expect($("<div><span>foo</span> bar</div>"))
438 * .to.have.$text("foo bar").and
439 * .to.contain.$text("foo");
440 * ```
441 *
442 * @see http://api.jquery.com/text/
443 *
444 * @name $text
445 * @param {String} expected text content
446 * @param {String} message failure message (_optional_)
447 * @api public
448 */
449 var $text = _containMethod("text", {
450 hasContains: true
451 });
452
453 chai.Assertion.addMethod("$text", $text);
454
455 /**
456 * Asserts that the target has exactly the given CSS property, or
457 * asserts the target contains a subset of the CSS when using the
458 * `include` or `contain` modifiers.
459 *
460 * *Node.js/JsDom Note*: Computed CSS properties are not correctly
461 * inferred as of JsDom v0.8.8. Explicit ones should get matched exactly.
462 *
463 * *Browser Note*: Explicit CSS properties are sometimes not matched
464 * (in contrast to Node.js), so the plugin performs an extra check against
465 * explicit `style` properties for a match. May still have other wonky
466 * corner cases.
467 *
468 * *PhantomJS Note*: PhantomJS also is fairly wonky and unpredictable with
469 * respect to CSS / styles, especially those that come from CSS classes
470 * and not explicity `style` attributes.
471 *
472 * ```js
473 * expect($("<div style=\"width: 50px; border: 1px dotted black;\" />"))
474 * .to.have.$css("width", "50px").and
475 * .to.have.$css("border-top-style", "dotted");
476 * ```
477 *
478 * @see http://api.jquery.com/css/
479 *
480 * @name $css
481 * @param {String} expected CSS property content
482 * @param {String} message failure message (_optional_)
483 * @api public
484 */
485 var $css = _containMethod("css", {
486 hasArg: true,
487 hasContains: true,
488
489 // Alternate Getter: If no match, go for explicit property.
490 altGet: function ($el, prop) { return $el.prop("style")[prop]; }
491 });
492
493 chai.Assertion.addMethod("$css", $css);
494 }
495
496 /* istanbul ignore next */
497 /*!
498 * Wrap AMD, etc. using boilerplate.
499 */
500 function wrap(plugin) {
501 "use strict";
502 /* global module:false, define:false */
503
504 if (typeof require === "function" &&
505 typeof exports === "object" &&
506 typeof module === "object") {
507 // NodeJS
508 module.exports = plugin;
509
510 } else if (typeof define === "function" && define.amd) {
511 // AMD: Assumes importing `chai` and `jquery`. Returns a function to
512 // inject with `chai.use()`.
513 //
514 // See: https://github.com/chaijs/chai-jquery/issues/27
515 define(["jquery"], function ($) {
516 return function (chai, utils) {
517 return plugin(chai, utils, $);
518 };
519 });
520
521 } else {
522 // Other environment (usually <script> tag): plug in to global chai
523 // instance directly.
524 root.chai.use(function (chai, utils) {
525 return plugin(chai, utils, root.jQuery);
526 });
527 }
528 }
529
530 // Hook it all together.
531 wrap(chaiJq);
532}());