')
98 | .attr('id', this.containerId)
99 | .html(html)
100 |
101 | $(document.body).append(container)
102 | return container
103 | }
104 |
105 | jasmine.Fixtures.prototype.addToContainer_ = function (html){
106 | var container = $(document.body).find('#'+this.containerId).append(html)
107 |
108 | if (!container.length) {
109 | this.createContainer_(html)
110 | }
111 | }
112 |
113 | jasmine.Fixtures.prototype.getFixtureHtml_ = function (url) {
114 | if (typeof this.fixturesCache_[url] === 'undefined') {
115 | this.loadFixtureIntoCache_(url)
116 | }
117 | return this.fixturesCache_[url]
118 | }
119 |
120 | jasmine.Fixtures.prototype.loadFixtureIntoCache_ = function (relativeUrl) {
121 | var self = this
122 | , url = this.makeFixtureUrl_(relativeUrl)
123 | , htmlText = ''
124 | , request = $.ajax({
125 | async: false, // must be synchronous to guarantee that no tests are run before fixture is loaded
126 | cache: false,
127 | url: url,
128 | success: function (data, status, $xhr) {
129 | htmlText = $xhr.responseText
130 | }
131 | }).fail(function () {
132 | throw new Error('Fixture could not be loaded: ' + url + ' (status: ' + status + ', message: ' + errorThrown.message + ')')
133 | })
134 |
135 | var scripts = $($.parseHTML(htmlText, true)).find('script[src]') || [];
136 |
137 | scripts.each(function(){
138 | $.ajax({
139 | async: false, // must be synchronous to guarantee that no tests are run before fixture is loaded
140 | cache: false,
141 | dataType: 'script',
142 | url: $(this).attr('src'),
143 | success: function (data, status, $xhr) {
144 | htmlText += ''
145 | },
146 | error: function (jqXHR, status, errorThrown) {
147 | throw new Error('Script could not be loaded: ' + scriptSrc + ' (status: ' + status + ', message: ' + errorThrown.message + ')')
148 | }
149 | });
150 | })
151 |
152 | self.fixturesCache_[relativeUrl] = htmlText;
153 | }
154 |
155 | jasmine.Fixtures.prototype.makeFixtureUrl_ = function (relativeUrl){
156 | return this.fixturesPath.match('/$') ? this.fixturesPath + relativeUrl : this.fixturesPath + '/' + relativeUrl
157 | }
158 |
159 | jasmine.Fixtures.prototype.proxyCallTo_ = function (methodName, passedArguments) {
160 | return this[methodName].apply(this, passedArguments)
161 | }
162 |
163 |
164 | jasmine.StyleFixtures = function () {
165 | this.fixturesCache_ = {}
166 | this.fixturesNodes_ = []
167 | this.fixturesPath = 'spec/javascripts/fixtures'
168 | }
169 |
170 | jasmine.StyleFixtures.prototype.set = function (css) {
171 | this.cleanUp()
172 | this.createStyle_(css)
173 | }
174 |
175 | jasmine.StyleFixtures.prototype.appendSet = function (css) {
176 | this.createStyle_(css)
177 | }
178 |
179 | jasmine.StyleFixtures.prototype.preload = function () {
180 | this.read_.apply(this, arguments)
181 | }
182 |
183 | jasmine.StyleFixtures.prototype.load = function () {
184 | this.cleanUp()
185 | this.createStyle_(this.read_.apply(this, arguments))
186 | }
187 |
188 | jasmine.StyleFixtures.prototype.appendLoad = function () {
189 | this.createStyle_(this.read_.apply(this, arguments))
190 | }
191 |
192 | jasmine.StyleFixtures.prototype.cleanUp = function () {
193 | while(this.fixturesNodes_.length) {
194 | this.fixturesNodes_.pop().remove()
195 | }
196 | }
197 |
198 | jasmine.StyleFixtures.prototype.createStyle_ = function (html) {
199 | var styleText = $('
').html(html).text()
200 | , style = $('')
201 |
202 | this.fixturesNodes_.push(style)
203 | $('head').append(style)
204 | }
205 |
206 | jasmine.StyleFixtures.prototype.clearCache = jasmine.Fixtures.prototype.clearCache
207 | jasmine.StyleFixtures.prototype.read_ = jasmine.Fixtures.prototype.read
208 | jasmine.StyleFixtures.prototype.getFixtureHtml_ = jasmine.Fixtures.prototype.getFixtureHtml_
209 | jasmine.StyleFixtures.prototype.loadFixtureIntoCache_ = jasmine.Fixtures.prototype.loadFixtureIntoCache_
210 | jasmine.StyleFixtures.prototype.makeFixtureUrl_ = jasmine.Fixtures.prototype.makeFixtureUrl_
211 | jasmine.StyleFixtures.prototype.proxyCallTo_ = jasmine.Fixtures.prototype.proxyCallTo_
212 |
213 | jasmine.getJSONFixtures = function () {
214 | return jasmine.currentJSONFixtures_ = jasmine.currentJSONFixtures_ || new jasmine.JSONFixtures()
215 | }
216 |
217 | jasmine.JSONFixtures = function () {
218 | this.fixturesCache_ = {}
219 | this.fixturesPath = 'spec/javascripts/fixtures/json'
220 | }
221 |
222 | jasmine.JSONFixtures.prototype.load = function () {
223 | this.read.apply(this, arguments)
224 | return this.fixturesCache_
225 | }
226 |
227 | jasmine.JSONFixtures.prototype.read = function () {
228 | var fixtureUrls = arguments
229 |
230 | for(var urlCount = fixtureUrls.length, urlIndex = 0; urlIndex < urlCount; urlIndex++) {
231 | this.getFixtureData_(fixtureUrls[urlIndex])
232 | }
233 |
234 | return this.fixturesCache_
235 | }
236 |
237 | jasmine.JSONFixtures.prototype.clearCache = function () {
238 | this.fixturesCache_ = {}
239 | }
240 |
241 | jasmine.JSONFixtures.prototype.getFixtureData_ = function (url) {
242 | if (!this.fixturesCache_[url]) this.loadFixtureIntoCache_(url)
243 | return this.fixturesCache_[url]
244 | }
245 |
246 | jasmine.JSONFixtures.prototype.loadFixtureIntoCache_ = function (relativeUrl) {
247 | var self = this
248 | , url = this.fixturesPath.match('/$') ? this.fixturesPath + relativeUrl : this.fixturesPath + '/' + relativeUrl
249 |
250 | $.ajax({
251 | async: false, // must be synchronous to guarantee that no tests are run before fixture is loaded
252 | cache: false,
253 | dataType: 'json',
254 | url: url,
255 | success: function (data) {
256 | self.fixturesCache_[relativeUrl] = data
257 | },
258 | error: function (jqXHR, status, errorThrown) {
259 | throw new Error('JSONFixture could not be loaded: ' + url + ' (status: ' + status + ', message: ' + errorThrown.message + ')')
260 | }
261 | })
262 | }
263 |
264 | jasmine.JSONFixtures.prototype.proxyCallTo_ = function (methodName, passedArguments) {
265 | return this[methodName].apply(this, passedArguments)
266 | }
267 |
268 | jasmine.jQuery = function () {}
269 |
270 | jasmine.jQuery.browserTagCaseIndependentHtml = function (html) {
271 | return $('
').append(html).html()
272 | }
273 |
274 | jasmine.jQuery.elementToString = function (element) {
275 | return $(element).map(function () { return this.outerHTML; }).toArray().join(', ')
276 | }
277 |
278 | var data = {
279 | spiedEvents: {}
280 | , handlers: []
281 | }
282 |
283 | jasmine.jQuery.events = {
284 | spyOn: function (selector, eventName) {
285 | var handler = function (e) {
286 | data.spiedEvents[jasmine.spiedEventsKey(selector, eventName)] = jasmine.util.argsToArray(arguments)
287 | }
288 |
289 | $(selector).on(eventName, handler)
290 | data.handlers.push(handler)
291 |
292 | return {
293 | selector: selector,
294 | eventName: eventName,
295 | handler: handler,
296 | reset: function (){
297 | delete data.spiedEvents[jasmine.spiedEventsKey(selector, eventName)]
298 | }
299 | }
300 | },
301 |
302 | args: function (selector, eventName) {
303 | var actualArgs = data.spiedEvents[jasmine.spiedEventsKey(selector, eventName)]
304 |
305 | if (!actualArgs) {
306 | throw "There is no spy for " + eventName + " on " + selector.toString() + ". Make sure to create a spy using spyOnEvent."
307 | }
308 |
309 | return actualArgs
310 | },
311 |
312 | wasTriggered: function (selector, eventName) {
313 | return !!(data.spiedEvents[jasmine.spiedEventsKey(selector, eventName)])
314 | },
315 |
316 | wasTriggeredWith: function (selector, eventName, expectedArgs, util, customEqualityTesters) {
317 | var actualArgs = jasmine.jQuery.events.args(selector, eventName).slice(1)
318 |
319 | if (Object.prototype.toString.call(expectedArgs) !== '[object Array]')
320 | actualArgs = actualArgs[0]
321 |
322 | return util.equals(expectedArgs, actualArgs, customEqualityTesters)
323 | },
324 |
325 | wasPrevented: function (selector, eventName) {
326 | var args = data.spiedEvents[jasmine.spiedEventsKey(selector, eventName)]
327 | , e = args ? args[0] : undefined
328 |
329 | return e && e.isDefaultPrevented()
330 | },
331 |
332 | wasStopped: function (selector, eventName) {
333 | var args = data.spiedEvents[jasmine.spiedEventsKey(selector, eventName)]
334 | , e = args ? args[0] : undefined
335 | return e && e.isPropagationStopped()
336 | },
337 |
338 | cleanUp: function () {
339 | data.spiedEvents = {}
340 | data.handlers = []
341 | }
342 | }
343 |
344 | var hasProperty = function (actualValue, expectedValue) {
345 | if (expectedValue === undefined)
346 | return actualValue !== undefined
347 |
348 | return actualValue === expectedValue
349 | }
350 |
351 | beforeEach(function () {
352 | jasmine.addMatchers({
353 | toHaveClass: function () {
354 | return {
355 | compare: function (actual, className) {
356 | return { pass: $(actual).hasClass(className) }
357 | }
358 | }
359 | },
360 |
361 | toHaveCss: function () {
362 | return {
363 | compare: function (actual, css) {
364 | for (var prop in css){
365 | var value = css[prop]
366 | // see issue #147 on gh
367 | ;if (value === 'auto' && $(actual).get(0).style[prop] === 'auto') continue
368 | if ($(actual).css(prop) !== value) return { pass: false }
369 | }
370 | return { pass: true }
371 | }
372 | }
373 | },
374 |
375 | toBeVisible: function () {
376 | return {
377 | compare: function (actual) {
378 | return { pass: $(actual).is(':visible') }
379 | }
380 | }
381 | },
382 |
383 | toBeHidden: function () {
384 | return {
385 | compare: function (actual) {
386 | return { pass: $(actual).is(':hidden') }
387 | }
388 | }
389 | },
390 |
391 | toBeSelected: function () {
392 | return {
393 | compare: function (actual) {
394 | return { pass: $(actual).is(':selected') }
395 | }
396 | }
397 | },
398 |
399 | toBeChecked: function () {
400 | return {
401 | compare: function (actual) {
402 | return { pass: $(actual).is(':checked') }
403 | }
404 | }
405 | },
406 |
407 | toBeEmpty: function () {
408 | return {
409 | compare: function (actual) {
410 | return { pass: $(actual).is(':empty') }
411 | }
412 | }
413 | },
414 |
415 | toBeInDOM: function () {
416 | return {
417 | compare: function (actual) {
418 | return { pass: $.contains(document.documentElement, $(actual)[0]) }
419 | }
420 | }
421 | },
422 |
423 | toExist: function () {
424 | return {
425 | compare: function (actual) {
426 | return { pass: $(actual).length }
427 | }
428 | }
429 | },
430 |
431 | toHaveLength: function () {
432 | return {
433 | compare: function (actual, length) {
434 | return { pass: $(actual).length === length }
435 | }
436 | }
437 | },
438 |
439 | toHaveAttr: function () {
440 | return {
441 | compare: function (actual, attributeName, expectedAttributeValue) {
442 | return { pass: hasProperty($(actual).attr(attributeName), expectedAttributeValue) }
443 | }
444 | }
445 | },
446 |
447 | toHaveProp: function () {
448 | return {
449 | compare: function (actual, propertyName, expectedPropertyValue) {
450 | return { pass: hasProperty($(actual).prop(propertyName), expectedPropertyValue) }
451 | }
452 | }
453 | },
454 |
455 | toHaveId: function () {
456 | return {
457 | compare: function (actual, id) {
458 | return { pass: $(actual).attr('id') == id }
459 | }
460 | }
461 | },
462 |
463 | toHaveHtml: function () {
464 | return {
465 | compare: function (actual, html) {
466 | return { pass: $(actual).html() == jasmine.jQuery.browserTagCaseIndependentHtml(html) }
467 | }
468 | }
469 | },
470 |
471 | toContainHtml: function () {
472 | return {
473 | compare: function (actual, html) {
474 | var actualHtml = $(actual).html()
475 | , expectedHtml = jasmine.jQuery.browserTagCaseIndependentHtml(html)
476 |
477 | return { pass: (actualHtml.indexOf(expectedHtml) >= 0) }
478 | }
479 | }
480 | },
481 |
482 | toHaveText: function () {
483 | return {
484 | compare: function (actual, text) {
485 | var trimmedText = $.trim($(actual).text())
486 |
487 | if (text && $.isFunction(text.test)) {
488 | return { pass: text.test(trimmedText) }
489 | } else {
490 | return { pass: trimmedText == text }
491 | }
492 | }
493 | }
494 | },
495 |
496 | toContainText: function () {
497 | return {
498 | compare: function (actual, text) {
499 | var trimmedText = $.trim($(actual).text())
500 |
501 | if (text && $.isFunction(text.test)) {
502 | return { pass: text.test(trimmedText) }
503 | } else {
504 | return { pass: trimmedText.indexOf(text) != -1 }
505 | }
506 | }
507 | }
508 | },
509 |
510 | toHaveValue: function () {
511 | return {
512 | compare: function (actual, value) {
513 | return { pass: $(actual).val() === value }
514 | }
515 | }
516 | },
517 |
518 | toHaveData: function () {
519 | return {
520 | compare: function (actual, key, expectedValue) {
521 | return { pass: hasProperty($(actual).data(key), expectedValue) }
522 | }
523 | }
524 | },
525 |
526 | toContainElement: function () {
527 | return {
528 | compare: function (actual, selector) {
529 | if (window.debug) debugger
530 | return { pass: $(actual).find(selector).length }
531 | }
532 | }
533 | },
534 |
535 | toBeMatchedBy: function () {
536 | return {
537 | compare: function (actual, selector) {
538 | return { pass: $(actual).filter(selector).length }
539 | }
540 | }
541 | },
542 |
543 | toBeDisabled: function () {
544 | return {
545 | compare: function (actual, selector) {
546 | return { pass: $(actual).is(':disabled') }
547 | }
548 | }
549 | },
550 |
551 | toBeFocused: function (selector) {
552 | return {
553 | compare: function (actual, selector) {
554 | return { pass: $(actual)[0] === $(actual)[0].ownerDocument.activeElement }
555 | }
556 | }
557 | },
558 |
559 | toHandle: function () {
560 | return {
561 | compare: function (actual, event) {
562 | var events = $._data($(actual).get(0), "events")
563 |
564 | if (!events || !event || typeof event !== "string") {
565 | return { pass: false }
566 | }
567 |
568 | var namespaces = event.split(".")
569 | , eventType = namespaces.shift()
570 | , sortedNamespaces = namespaces.slice(0).sort()
571 | , namespaceRegExp = new RegExp("(^|\\.)" + sortedNamespaces.join("\\.(?:.*\\.)?") + "(\\.|$)")
572 |
573 | if (events[eventType] && namespaces.length) {
574 | for (var i = 0; i < events[eventType].length; i++) {
575 | var namespace = events[eventType][i].namespace
576 |
577 | if (namespaceRegExp.test(namespace))
578 | return { pass: true }
579 | }
580 | } else {
581 | return { pass: (events[eventType] && events[eventType].length > 0) }
582 | }
583 |
584 | return { pass: false }
585 | }
586 | }
587 | },
588 |
589 | toHandleWith: function () {
590 | return {
591 | compare: function (actual, eventName, eventHandler) {
592 | var normalizedEventName = eventName.split('.')[0]
593 | , stack = $._data($(actual).get(0), "events")[normalizedEventName]
594 |
595 | for (var i = 0; i < stack.length; i++) {
596 | if (stack[i].handler == eventHandler) return { pass: true }
597 | }
598 |
599 | return { pass: false }
600 | }
601 | }
602 | },
603 |
604 | toHaveBeenTriggeredOn: function () {
605 | return {
606 | compare: function (actual, selector) {
607 | var result = { pass: jasmine.jQuery.events.wasTriggered(selector, actual) }
608 |
609 | result.message = result.pass ?
610 | "Expected event " + $(actual) + " not to have been triggered on " + selector :
611 | "Expected event " + $(actual) + " to have been triggered on " + selector
612 |
613 | return result;
614 | }
615 | }
616 | },
617 |
618 | toHaveBeenTriggered: function (){
619 | return {
620 | compare: function (actual) {
621 | var eventName = actual.eventName
622 | , selector = actual.selector
623 | , result = { pass: jasmine.jQuery.events.wasTriggered(selector, eventName) }
624 |
625 | result.message = result.pass ?
626 | "Expected event " + eventName + " not to have been triggered on " + selector :
627 | "Expected event " + eventName + " to have been triggered on " + selector
628 |
629 | return result
630 | }
631 | }
632 | },
633 |
634 | toHaveBeenTriggeredOnAndWith: function (j$, customEqualityTesters) {
635 | return {
636 | compare: function (actual, selector, expectedArgs) {
637 | var wasTriggered = jasmine.jQuery.events.wasTriggered(selector, actual)
638 | , result = { pass: wasTriggered && jasmine.jQuery.events.wasTriggeredWith(selector, actual, expectedArgs, j$, customEqualityTesters) }
639 |
640 | if (wasTriggered) {
641 | var actualArgs = jasmine.jQuery.events.args(selector, actual, expectedArgs)[1]
642 | result.message = result.pass ?
643 | "Expected event " + actual + " not to have been triggered with " + jasmine.pp(expectedArgs) + " but it was triggered with " + jasmine.pp(actualArgs) :
644 | "Expected event " + actual + " to have been triggered with " + jasmine.pp(expectedArgs) + " but it was triggered with " + jasmine.pp(actualArgs)
645 |
646 | } else {
647 | // todo check on this
648 | result.message = result.pass ?
649 | "Expected event " + actual + " not to have been triggered on " + selector :
650 | "Expected event " + actual + " to have been triggered on " + selector
651 | }
652 |
653 | return result
654 | }
655 | }
656 | },
657 |
658 | toHaveBeenPreventedOn: function () {
659 | return {
660 | compare: function (actual, selector) {
661 | var result = { pass: jasmine.jQuery.events.wasPrevented(selector, actual) }
662 |
663 | result.message = result.pass ?
664 | "Expected event " + actual + " not to have been prevented on " + selector :
665 | "Expected event " + actual + " to have been prevented on " + selector
666 |
667 | return result
668 | }
669 | }
670 | },
671 |
672 | toHaveBeenPrevented: function () {
673 | return {
674 | compare: function (actual) {
675 | var eventName = actual.eventName
676 | , selector = actual.selector
677 | , result = { pass: jasmine.jQuery.events.wasPrevented(selector, eventName) }
678 |
679 | result.message = result.pass ?
680 | "Expected event " + eventName + " not to have been prevented on " + selector :
681 | "Expected event " + eventName + " to have been prevented on " + selector
682 |
683 | return result
684 | }
685 | }
686 | },
687 |
688 | toHaveBeenStoppedOn: function () {
689 | return {
690 | compare: function (actual, selector) {
691 | var result = { pass: jasmine.jQuery.events.wasStopped(selector, actual) }
692 |
693 | result.message = result.pass ?
694 | "Expected event " + actual + " not to have been stopped on " + selector :
695 | "Expected event " + actual + " to have been stopped on " + selector
696 |
697 | return result;
698 | }
699 | }
700 | },
701 |
702 | toHaveBeenStopped: function () {
703 | return {
704 | compare: function (actual) {
705 | var eventName = actual.eventName
706 | , selector = actual.selector
707 | , result = { pass: jasmine.jQuery.events.wasStopped(selector, eventName) }
708 |
709 | result.message = result.pass ?
710 | "Expected event " + eventName + " not to have been stopped on " + selector :
711 | "Expected event " + eventName + " to have been stopped on " + selector
712 |
713 | return result
714 | }
715 | }
716 | }
717 | })
718 |
719 | jasmine.getEnv().addCustomEqualityTester(function(a, b) {
720 | if (a && b) {
721 | if (a instanceof $ || jasmine.isDomNode(a)) {
722 | var $a = $(a)
723 |
724 | if (b instanceof $)
725 | return $a.length == b.length && a.is(b)
726 |
727 | return $a.is(b);
728 | }
729 |
730 | if (b instanceof $ || jasmine.isDomNode(b)) {
731 | var $b = $(b)
732 |
733 | if (a instanceof jQuery)
734 | return a.length == $b.length && $b.is(a)
735 |
736 | return $(b).is(a);
737 | }
738 | }
739 | })
740 |
741 | jasmine.getEnv().addCustomEqualityTester(function (a, b) {
742 | if (a instanceof jQuery && b instanceof jQuery && a.size() == b.size())
743 | return a.is(b)
744 | })
745 | })
746 |
747 | afterEach(function () {
748 | jasmine.getFixtures().cleanUp()
749 | jasmine.getStyleFixtures().cleanUp()
750 | jasmine.jQuery.events.cleanUp()
751 | })
752 |
753 | window.readFixtures = function () {
754 | return jasmine.getFixtures().proxyCallTo_('read', arguments)
755 | }
756 |
757 | window.preloadFixtures = function () {
758 | jasmine.getFixtures().proxyCallTo_('preload', arguments)
759 | }
760 |
761 | window.loadFixtures = function () {
762 | jasmine.getFixtures().proxyCallTo_('load', arguments)
763 | }
764 |
765 | window.appendLoadFixtures = function () {
766 | jasmine.getFixtures().proxyCallTo_('appendLoad', arguments)
767 | }
768 |
769 | window.setFixtures = function (html) {
770 | return jasmine.getFixtures().proxyCallTo_('set', arguments)
771 | }
772 |
773 | window.appendSetFixtures = function () {
774 | jasmine.getFixtures().proxyCallTo_('appendSet', arguments)
775 | }
776 |
777 | window.sandbox = function (attributes) {
778 | return jasmine.getFixtures().sandbox(attributes)
779 | }
780 |
781 | window.spyOnEvent = function (selector, eventName) {
782 | return jasmine.jQuery.events.spyOn(selector, eventName)
783 | }
784 |
785 | window.preloadStyleFixtures = function () {
786 | jasmine.getStyleFixtures().proxyCallTo_('preload', arguments)
787 | }
788 |
789 | window.loadStyleFixtures = function () {
790 | jasmine.getStyleFixtures().proxyCallTo_('load', arguments)
791 | }
792 |
793 | window.appendLoadStyleFixtures = function () {
794 | jasmine.getStyleFixtures().proxyCallTo_('appendLoad', arguments)
795 | }
796 |
797 | window.setStyleFixtures = function (html) {
798 | jasmine.getStyleFixtures().proxyCallTo_('set', arguments)
799 | }
800 |
801 | window.appendSetStyleFixtures = function (html) {
802 | jasmine.getStyleFixtures().proxyCallTo_('appendSet', arguments)
803 | }
804 |
805 | window.loadJSONFixtures = function () {
806 | return jasmine.getJSONFixtures().proxyCallTo_('load', arguments)
807 | }
808 |
809 | window.getJSONFixture = function (url) {
810 | return jasmine.getJSONFixtures().proxyCallTo_('read', arguments)[url]
811 | }
812 | }(window, window.jasmine, window.jQuery);
813 |
--------------------------------------------------------------------------------
/spec/javascripts/wow-spec.js:
--------------------------------------------------------------------------------
1 | (function() {
2 | describe('WOW', function() {
3 | var timeout, winHeight;
4 | window.console = {
5 | warn: function() {}
6 | };
7 | timeout = 100;
8 | winHeight = 300;
9 | describe('smoke test', function() {
10 | it('exists', function() {
11 | return expect(WOW).toBeDefined();
12 | });
13 | return it("has an 'init' method", function() {
14 | return expect(new WOW().init).toBeDefined();
15 | });
16 | });
17 | describe('simple test environment', function() {
18 | beforeEach(function() {
19 | return loadFixtures('simple.html');
20 | });
21 | it('emulates window height', function() {
22 | return expect(document.documentElement.clientHeight).toBe(winHeight);
23 | });
24 | return it('has boxes set up for testing', function() {
25 | var boxCount, boxHeight, offset, style;
26 | boxHeight = 200;
27 | boxCount = $('#simple').children().length;
28 | expect($('#simple').height()).toBe(boxHeight * boxCount);
29 | expect($('#simple-1').height()).toBe(boxHeight);
30 | expect($('#simple-2').height()).toBe(boxHeight);
31 | expect($('#simple-3').height()).toBe(boxHeight);
32 | expect($('#simple-4').height()).toBe(boxHeight);
33 | expect($('#simple-5').height()).toBe(boxHeight);
34 | offset = $('#simple').offset().top;
35 | expect($('#simple-1').offset().top).toBe(offset + boxHeight * 0);
36 | expect($('#simple-2').offset().top).toBe(offset + boxHeight * 1);
37 | expect($('#simple-3').offset().top).toBe(offset + boxHeight * 2);
38 | expect($('#simple-4').offset().top).toBe(offset + boxHeight * 3);
39 | expect($('#simple-5').offset().top).toBe(offset + boxHeight * 4);
40 | style = $('#simple-5')[0].style;
41 | expect(style.background).toBe('yellow');
42 | return expect(style.color).toBe('red');
43 | });
44 | });
45 | describe('library behaviour', function() {
46 | var wow;
47 | wow = null;
48 | beforeEach(function(done) {
49 | loadFixtures('simple.html');
50 | (wow = new WOW).init();
51 | return setTimeout(function() {
52 | return done();
53 | }, timeout);
54 | });
55 | it('animates elements that are fully visible on the page', function() {
56 | expect($('#simple-1')).toHaveClass('animated');
57 | return expect($('#simple-1').css('visibility')).toBe('visible');
58 | });
59 | it("does not touch elements that don't have the marker class", function() {
60 | expect($('#simple-2')).not.toHaveClass('animated');
61 | return expect($('#simple-2').css('visibility')).toBe('visible');
62 | });
63 | it('does not animate elements not yet visible on the page', function() {
64 | expect($('#simple-3')).not.toHaveClass('animated');
65 | expect($('#simple-3').css('visibility')).not.toBe('visible');
66 | expect($('#simple-4')).not.toHaveClass('animated');
67 | return expect($('#simple-4').css('visibility')).not.toBe('visible');
68 | });
69 | it('animates elements after scrolling down and they become visible', function(done) {
70 | window.scrollTo(0, $('#simple-3').offset().top - winHeight + 150);
71 | return setTimeout(function() {
72 | expect($('#simple-3')).toHaveClass('animated');
73 | expect($('#simple-3').css('visibility')).toBe('visible');
74 | expect($('#simple-4')).not.toHaveClass('animated');
75 | expect($('#simple-4').css('visibility')).not.toBe('visible');
76 | window.scrollTo(0, $('#simple-4').offset().top - winHeight + 150);
77 | return setTimeout(function() {
78 | expect($('#simple-4')).toHaveClass('animated');
79 | expect($('#simple-4').css('visibility')).toBe('visible');
80 | return done();
81 | }, timeout);
82 | }, timeout);
83 | });
84 | it('does not tamper with the style attribute', function(done) {
85 | window.scrollTo(0, $('#simple-5').offset().top - winHeight + 150);
86 | return setTimeout(function() {
87 | expect($('#simple-5')).toHaveClass('animated');
88 | expect($('#simple-5').css('visibility')).toBe('visible');
89 | expect($('#simple-5')[0].style.background).toBe('yellow');
90 | expect($('#simple-5')[0].style.color).toBe('red');
91 | return done();
92 | }, timeout);
93 | });
94 | it('works with asynchronously loaded content', function(done) {
95 | $('#simple').append($('
', {
96 | id: 'simple-6',
97 | "class": 'wow'
98 | }));
99 | wow.sync();
100 | window.scrollTo(0, $('#simple-6').offset().top - winHeight + 150);
101 | return setTimeout(function() {
102 | expect($('#simple-6')).toHaveClass('animated');
103 | expect($('#simple-6').css('visibility')).toBe('visible');
104 | return done();
105 | }, timeout);
106 | });
107 | return it('works with asynchronously loaded nested content', function(done) {
108 | $('#simple').append($('
')).children().first().append($('
', {
109 | id: 'simple-7',
110 | "class": 'wow'
111 | }));
112 | wow.sync();
113 | window.scrollTo(0, $('#simple-7').offset().top - winHeight + 150);
114 | return setTimeout(function() {
115 | expect($('#simple-7')).toHaveClass('animated');
116 | expect($('#simple-7').css('visibility')).toBe('visible');
117 | return done();
118 | }, timeout);
119 | });
120 | });
121 | describe('custom test environment', function() {
122 | beforeEach(function() {
123 | return loadFixtures('custom.html');
124 | });
125 | it('emulates window height', function() {
126 | return expect(document.documentElement.clientHeight).toBe(winHeight);
127 | });
128 | return it('has boxes set up for testing', function() {
129 | var offset;
130 | expect($('#custom').height()).toBe(800);
131 | expect($('#custom-1').height()).toBe(200);
132 | expect($('#custom-2').height()).toBe(200);
133 | expect($('#custom-3').height()).toBe(200);
134 | expect($('#custom-4').height()).toBe(200);
135 | offset = $('#custom').offset().top;
136 | expect($('#custom-1').offset().top).toBe(offset + 200 * 0);
137 | expect($('#custom-2').offset().top).toBe(offset + 200 * 1);
138 | expect($('#custom-3').offset().top).toBe(offset + 200 * 2);
139 | return expect($('#custom-4').offset().top).toBe(offset + 200 * 3);
140 | });
141 | });
142 | return describe('library behaviour with custom settings', function() {
143 | var called;
144 | called = false;
145 | beforeEach(function(done) {
146 | called = false;
147 | loadFixtures('custom.html');
148 | new WOW({
149 | boxClass: 'block',
150 | animateClass: 'fancy',
151 | offset: 10,
152 | callback: function() {
153 | return called = true;
154 | }
155 | }).init();
156 | $('.block').on('block', function() {
157 | return $(this).addClass('triggered');
158 | });
159 | return setTimeout(function() {
160 | return done();
161 | }, timeout);
162 | });
163 | it("creates two instances of the WOW.js with different configs", function() {
164 | var wow1, wow2;
165 | wow1 = new WOW({
166 | boxClass: 'block1',
167 | animateClass: 'fancy1',
168 | offset: 10
169 | });
170 | wow2 = new WOW({
171 | boxClass: 'block2',
172 | animateClass: 'fancy2',
173 | offset: 20
174 | });
175 | expect(wow1.config.boxClass).toBe("block1");
176 | expect(wow1.config.animateClass).toBe("fancy1");
177 | return expect(wow1.config.offset).toBe(10);
178 | });
179 | it("does not touch elements that don't have the marker class", function(done) {
180 | window.scrollTo(0, $('#custom-1').offset().top - winHeight + 15);
181 | return setTimeout(function() {
182 | expect($('#custom-1')).not.toHaveClass('fancy');
183 | return done();
184 | }, timeout);
185 | });
186 | it("animates elements that are partially visible on the page based on the 'offset' config", function(done) {
187 | return setTimeout(function() {
188 | window.scrollTo(0, $('#custom-2').offset().top - winHeight + 5);
189 | expect($('#custom-2')).not.toHaveClass('fancy');
190 | window.scrollTo(0, $('#custom-2').offset().top - winHeight + 15);
191 | return setTimeout(function() {
192 | expect($('#custom-2')).toHaveClass('fancy');
193 | expect($('#custom-2').css('visibility')).toBe('visible');
194 | return done();
195 | }, timeout);
196 | }, timeout);
197 | });
198 | it('does not animate elements not yet visible on the page', function() {
199 | expect($('#custom-3')).not.toHaveClass('fancy');
200 | return expect($('#custom-4')).not.toHaveClass('fancy');
201 | });
202 | it('animates elements after scrolling down and they become visible', function(done) {
203 | window.scrollTo(0, $('#custom-3').offset().top - winHeight + 150);
204 | return setTimeout(function() {
205 | expect($('#custom-3')).toHaveClass('fancy');
206 | expect($('#custom-3').css('visibility')).toBe('visible');
207 | expect($('#custom-3')[0].style.webkitAnimationIterationCount).toBe('2');
208 | expect($('#custom-4')).not.toHaveClass('fancy');
209 | window.scrollTo(0, $('#custom-4').offset().top - winHeight + 150);
210 | return setTimeout(function() {
211 | expect($('#custom-4')).toHaveClass('fancy');
212 | expect($('#custom-4').css('visibility')).toBe('visible');
213 | expect($('#custom-4')[0].style.webkitAnimationIterationCount).toBe('infinite');
214 | expect($('#custom-4')[0].style.webkitAnimationDuration).toBe('2s');
215 | expect($('#custom-4')[0].style.webkitAnimationDelay).toBe('1s');
216 | return done();
217 | }, timeout);
218 | }, timeout);
219 | });
220 | it("fires the callback", function(done) {
221 | called = false;
222 | window.scrollTo(0, $('#custom-3').offset().top - winHeight + 150);
223 | return setTimeout(function() {
224 | expect(called).toBe(true);
225 | return done();
226 | }, timeout);
227 | });
228 | return it('fires the callback on the visible element', function(done) {
229 | window.scrollTo(0, $('#custom-3').offset().top - winHeight + 150);
230 | return setTimeout(function() {
231 | expect($('#custom-3')).toHaveClass('triggered');
232 | expect($('#custom-4')).not.toHaveClass('triggered');
233 | window.scrollTo(0, $('#custom-4').offset().top - winHeight + 150);
234 | return setTimeout(function() {
235 | expect($('#custom-3')).toHaveClass('triggered');
236 | expect($('#custom-4')).toHaveClass('triggered');
237 | return done();
238 | }, timeout);
239 | }, timeout);
240 | });
241 | });
242 | });
243 |
244 | }).call(this);
245 |
--------------------------------------------------------------------------------
/src/wow.coffee:
--------------------------------------------------------------------------------
1 | #
2 | # Name : wow
3 | # Author : Matt Delac, https://www.delac.io/, @mattdelac_
4 | # Version : 1.1.2
5 | # Repo : https://github.com/matthieua/WOW
6 | # Website : https://www.delac.io/WOW
7 | #
8 |
9 |
10 | class Util
11 | extend: (custom, defaults) ->
12 | custom[key] ?= value for key, value of defaults
13 | custom
14 |
15 | isMobile: (agent) ->
16 | /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(agent)
17 |
18 | createEvent: (event, bubble = false, cancel = false, detail = null) ->
19 | if document.createEvent? # W3C DOM
20 | customEvent = document.createEvent('CustomEvent')
21 | customEvent.initCustomEvent(event, bubble, cancel, detail)
22 | else if document.createEventObject? # IE DOM < 9
23 | customEvent = document.createEventObject()
24 | customEvent.eventType = event
25 | else
26 | customEvent.eventName = event
27 |
28 | customEvent
29 |
30 | emitEvent: (elem, event) ->
31 | if elem.dispatchEvent? # W3C DOM
32 | elem.dispatchEvent(event)
33 | else if event of elem?
34 | elem[event]()
35 | else if "on#{event}" of elem?
36 | elem["on#{event}"]()
37 |
38 | addEvent: (elem, event, fn) ->
39 | if elem.addEventListener? # W3C DOM
40 | elem.addEventListener event, fn, false
41 | else if elem.attachEvent? # IE DOM
42 | elem.attachEvent "on#{event}", fn
43 | else # fallback
44 | elem[event] = fn
45 |
46 | removeEvent: (elem, event, fn) ->
47 | if elem.removeEventListener? # W3C DOM
48 | elem.removeEventListener event, fn, false
49 | else if elem.detachEvent? # IE DOM
50 | elem.detachEvent "on#{event}", fn
51 | else # fallback
52 | delete elem[event]
53 |
54 | innerHeight: ->
55 | if 'innerHeight' of window
56 | window.innerHeight
57 | else document.documentElement.clientHeight
58 |
59 | # Minimalistic WeakMap shim, just in case.
60 | WeakMap = @WeakMap or @MozWeakMap or \
61 | class WeakMap
62 | constructor: ->
63 | @keys = []
64 | @values = []
65 |
66 | get: (key) ->
67 | for item, i in @keys
68 | if item is key
69 | return @values[i]
70 |
71 | set: (key, value) ->
72 | for item, i in @keys
73 | if item is key
74 | @values[i] = value
75 | return
76 | @keys.push(key)
77 | @values.push(value)
78 |
79 | # Dummy MutationObserver, to avoid raising exceptions.
80 | MutationObserver = @MutationObserver or @WebkitMutationObserver or @MozMutationObserver or \
81 | class MutationObserver
82 | constructor: ->
83 | console?.warn 'MutationObserver is not supported by your browser.'
84 | console?.warn 'WOW.js cannot detect dom mutations, please call .sync() after loading new content.'
85 |
86 | @notSupported: true
87 |
88 | observe: ->
89 |
90 | # getComputedStyle shim, from http://stackoverflow.com/a/21797294
91 | getComputedStyle = @getComputedStyle or \
92 | (el, pseudo) ->
93 | @getPropertyValue = (prop) ->
94 | prop = 'styleFloat' if prop is 'float'
95 | prop.replace(getComputedStyleRX, (_, _char)->
96 | _char.toUpperCase()
97 | ) if getComputedStyleRX.test prop
98 | el.currentStyle?[prop] or null
99 | @
100 | getComputedStyleRX = /(\-([a-z]){1})/g
101 |
102 | class @WOW
103 | defaults:
104 | boxClass: 'wow'
105 | animateClass: 'animated'
106 | offset: 0
107 | mobile: true
108 | live: true
109 | callback: null
110 | scrollContainer: null
111 |
112 | constructor: (options = {}) ->
113 | @scrolled = true
114 | @config = @util().extend(options, @defaults)
115 | if options.scrollContainer?
116 | @config.scrollContainer = document.querySelector(options.scrollContainer)
117 | # Map of elements to animation names:
118 | @animationNameCache = new WeakMap()
119 | @wowEvent = @util().createEvent(@config.boxClass)
120 |
121 | init: ->
122 | @element = window.document.documentElement
123 | if document.readyState in ["interactive", "complete"]
124 | @start()
125 | else
126 | @util().addEvent document, 'DOMContentLoaded', @start
127 | @finished = []
128 |
129 | start: =>
130 | @stopped = false
131 | @boxes = (box for box in @element.querySelectorAll(".#{@config.boxClass}"))
132 | @all = (box for box in @boxes)
133 | if @boxes.length
134 | if @disabled()
135 | @resetStyle()
136 | else
137 | @applyStyle(box, true) for box in @boxes
138 | if !@disabled()
139 | @util().addEvent @config.scrollContainer || window, 'scroll', @scrollHandler
140 | @util().addEvent window, 'resize', @scrollHandler
141 | @interval = setInterval @scrollCallback, 50
142 | if @config.live
143 | new MutationObserver (records) =>
144 | for record in records
145 | @doSync(node) for node in record.addedNodes or []
146 | .observe document.body,
147 | childList: true
148 | subtree: true
149 |
150 | # unbind the scroll event
151 | stop: ->
152 | @stopped = true
153 | @util().removeEvent @config.scrollContainer || window, 'scroll', @scrollHandler
154 | @util().removeEvent window, 'resize', @scrollHandler
155 | clearInterval @interval if @interval?
156 |
157 | sync: (element) ->
158 | @doSync(@element) if MutationObserver.notSupported
159 |
160 | doSync: (element) ->
161 | element ?= @element
162 | return unless element.nodeType is 1
163 | element = element.parentNode or element
164 | for box in element.querySelectorAll(".#{@config.boxClass}")
165 | unless box in @all
166 | @boxes.push box
167 | @all.push box
168 | if @stopped or @disabled()
169 | @resetStyle()
170 | else
171 | @applyStyle(box, true)
172 | @scrolled = true
173 |
174 | # show box element
175 | show: (box) ->
176 | @applyStyle(box)
177 | box.className = "#{box.className} #{@config.animateClass}"
178 | @config.callback(box) if @config.callback?
179 | @util().emitEvent(box, @wowEvent)
180 |
181 | @util().addEvent(box, 'animationend', @resetAnimation)
182 | @util().addEvent(box, 'oanimationend', @resetAnimation)
183 | @util().addEvent(box, 'webkitAnimationEnd', @resetAnimation)
184 | @util().addEvent(box, 'MSAnimationEnd', @resetAnimation)
185 |
186 | box
187 |
188 | applyStyle: (box, hidden) ->
189 | duration = box.getAttribute('data-wow-duration')
190 | delay = box.getAttribute('data-wow-delay')
191 | iteration = box.getAttribute('data-wow-iteration')
192 |
193 | @animate => @customStyle(box, hidden, duration, delay, iteration)
194 |
195 | animate: (->
196 | if 'requestAnimationFrame' of window
197 | (callback) ->
198 | window.requestAnimationFrame callback
199 | else
200 | (callback) ->
201 | callback()
202 | )()
203 |
204 | resetStyle: ->
205 | box.style.visibility = 'visible' for box in @boxes
206 |
207 | resetAnimation: (event) =>
208 | if event.type.toLowerCase().indexOf('animationend') >= 0
209 | target = event.target || event.srcElement
210 | target.className = target.className.replace(@config.animateClass, '').trim()
211 |
212 | customStyle: (box, hidden, duration, delay, iteration) ->
213 | @cacheAnimationName(box) if hidden
214 | box.style.visibility = if hidden then 'hidden' else 'visible'
215 |
216 | @vendorSet box.style, animationDuration: duration if duration
217 | @vendorSet box.style, animationDelay: delay if delay
218 | @vendorSet box.style, animationIterationCount: iteration if iteration
219 | @vendorSet box.style, animationName: if hidden then 'none' else @cachedAnimationName(box)
220 |
221 | box
222 |
223 | vendors: ["moz", "webkit"]
224 | vendorSet: (elem, properties) ->
225 | for name, value of properties
226 | elem["#{name}"] = value
227 | elem["#{vendor}#{name.charAt(0).toUpperCase()}#{name.substr 1}"] = value for vendor in @vendors
228 | vendorCSS: (elem, property) ->
229 | style = getComputedStyle(elem)
230 | result = style.getPropertyCSSValue(property)
231 | result = result or style.getPropertyCSSValue("-#{vendor}-#{property}") for vendor in @vendors
232 | result
233 |
234 | animationName: (box) ->
235 | try
236 | animationName = @vendorCSS(box, 'animation-name').cssText
237 | catch # Opera, fall back to plain property value
238 | animationName = getComputedStyle(box).getPropertyValue('animation-name')
239 | if animationName is 'none'
240 | '' # SVG/Firefox, unable to get animation name?
241 | else
242 | animationName
243 |
244 | cacheAnimationName: (box) ->
245 | # https://bugzilla.mozilla.org/show_bug.cgi?id=921834
246 | # box.dataset is not supported for SVG elements in Firefox
247 | @animationNameCache.set(box, @animationName(box))
248 | cachedAnimationName: (box) ->
249 | @animationNameCache.get(box)
250 |
251 | # fast window.scroll callback
252 | scrollHandler: =>
253 | @scrolled = true
254 |
255 | scrollCallback: =>
256 | if @scrolled
257 | @scrolled = false
258 | @boxes = for box in @boxes when box
259 | if @isVisible(box)
260 | @show(box)
261 | continue
262 | box
263 | @stop() unless @boxes.length or @config.live
264 |
265 |
266 | # Calculate element offset top
267 | offsetTop: (element) ->
268 | # SVG elements don't have an offsetTop in Firefox.
269 | # This will use their nearest parent that has an offsetTop.
270 | # Also, using ('offsetTop' of element) causes an exception in Firefox.
271 | element = element.parentNode while element.offsetTop is undefined
272 | top = element.offsetTop
273 | top += element.offsetTop while element = element.offsetParent
274 | top
275 |
276 | # check if box is visible
277 | isVisible: (box) ->
278 | offset = box.getAttribute('data-wow-offset') or @config.offset
279 | viewTop = (@config.scrollContainer && @config.scrollContainer.scrollTop) || window.pageYOffset
280 | viewBottom = viewTop + Math.min(@element.clientHeight, @util().innerHeight()) - offset
281 | top = @offsetTop(box)
282 | bottom = top + box.clientHeight
283 |
284 | top <= viewBottom and bottom >= viewTop
285 |
286 | util: ->
287 | @_util ?= new Util()
288 |
289 | disabled: ->
290 | not @config.mobile and @util().isMobile(navigator.userAgent)
291 |
--------------------------------------------------------------------------------