= result.computed && (result = {value : value, computed : computed});
227 | });
228 | return result.value;
229 | };
230 |
231 | // Return the minimum element (or element-based computation).
232 | _.min = function(obj, iterator, context) {
233 | if (!iterator && _.isArray(obj)) return Math.min.apply(Math, obj);
234 | var result = {computed : Infinity};
235 | each(obj, function(value, index, list) {
236 | var computed = iterator ? iterator.call(context, value, index, list) : value;
237 | computed < result.computed && (result = {value : value, computed : computed});
238 | });
239 | return result.value;
240 | };
241 |
242 | // Sort the object's values by a criterion produced by an iterator.
243 | _.sortBy = function(obj, iterator, context) {
244 | return _.pluck(_.map(obj, function(value, index, list) {
245 | return {
246 | value : value,
247 | criteria : iterator.call(context, value, index, list)
248 | };
249 | }).sort(function(left, right) {
250 | var a = left.criteria, b = right.criteria;
251 | return a < b ? -1 : a > b ? 1 : 0;
252 | }), 'value');
253 | };
254 |
255 | // Use a comparator function to figure out at what index an object should
256 | // be inserted so as to maintain order. Uses binary search.
257 | _.sortedIndex = function(array, obj, iterator) {
258 | iterator = iterator || _.identity;
259 | var low = 0, high = array.length;
260 | while (low < high) {
261 | var mid = (low + high) >> 1;
262 | iterator(array[mid]) < iterator(obj) ? low = mid + 1 : high = mid;
263 | }
264 | return low;
265 | };
266 |
267 | // Safely convert anything iterable into a real, live array.
268 | _.toArray = function(iterable) {
269 | if (!iterable) return [];
270 | if (iterable.toArray) return iterable.toArray();
271 | if (_.isArray(iterable)) return iterable;
272 | if (_.isArguments(iterable)) return slice.call(iterable);
273 | return _.values(iterable);
274 | };
275 |
276 | // Return the number of elements in an object.
277 | _.size = function(obj) {
278 | return _.toArray(obj).length;
279 | };
280 |
281 | // Array Functions
282 | // ---------------
283 |
284 | // Get the first element of an array. Passing **n** will return the first N
285 | // values in the array. Aliased as `head`. The **guard** check allows it to work
286 | // with `_.map`.
287 | _.first = _.head = function(array, n, guard) {
288 | return n && !guard ? slice.call(array, 0, n) : array[0];
289 | };
290 |
291 | // Returns everything but the first entry of the array. Aliased as `tail`.
292 | // Especially useful on the arguments object. Passing an **index** will return
293 | // the rest of the values in the array from that index onward. The **guard**
294 | // check allows it to work with `_.map`.
295 | _.rest = _.tail = function(array, index, guard) {
296 | return slice.call(array, _.isUndefined(index) || guard ? 1 : index);
297 | };
298 |
299 | // Get the last element of an array.
300 | _.last = function(array) {
301 | return array[array.length - 1];
302 | };
303 |
304 | // Trim out all falsy values from an array.
305 | _.compact = function(array) {
306 | return _.filter(array, function(value){ return !!value; });
307 | };
308 |
309 | // Return a completely flattened version of an array.
310 | _.flatten = function(array) {
311 | return _.reduce(array, function(memo, value) {
312 | if (_.isArray(value)) return memo.concat(_.flatten(value));
313 | memo[memo.length] = value;
314 | return memo;
315 | }, []);
316 | };
317 |
318 | // Return a version of the array that does not contain the specified value(s).
319 | _.without = function(array) {
320 | var values = slice.call(arguments, 1);
321 | return _.filter(array, function(value){ return !_.include(values, value); });
322 | };
323 |
324 | // Produce a duplicate-free version of the array. If the array has already
325 | // been sorted, you have the option of using a faster algorithm.
326 | // Aliased as `unique`.
327 | _.uniq = _.unique = function(array, isSorted) {
328 | return _.reduce(array, function(memo, el, i) {
329 | if (0 == i || (isSorted === true ? _.last(memo) != el : !_.include(memo, el))) memo[memo.length] = el;
330 | return memo;
331 | }, []);
332 | };
333 |
334 | // Produce an array that contains every item shared between all the
335 | // passed-in arrays.
336 | _.intersect = function(array) {
337 | var rest = slice.call(arguments, 1);
338 | return _.filter(_.uniq(array), function(item) {
339 | return _.every(rest, function(other) {
340 | return _.indexOf(other, item) >= 0;
341 | });
342 | });
343 | };
344 |
345 | // Zip together multiple lists into a single array -- elements that share
346 | // an index go together.
347 | _.zip = function() {
348 | var args = slice.call(arguments);
349 | var length = _.max(_.pluck(args, 'length'));
350 | var results = new Array(length);
351 | for (var i = 0; i < length; i++) results[i] = _.pluck(args, "" + i);
352 | return results;
353 | };
354 |
355 | // If the browser doesn't supply us with indexOf (I'm looking at you, **MSIE**),
356 | // we need this function. Return the position of the first occurrence of an
357 | // item in an array, or -1 if the item is not included in the array.
358 | // Delegates to **ECMAScript 5**'s native `indexOf` if available.
359 | // If the array is large and already in sort order, pass `true`
360 | // for **isSorted** to use binary search.
361 | _.indexOf = function(array, item, isSorted) {
362 | if (array == null) return -1;
363 | if (isSorted) {
364 | var i = _.sortedIndex(array, item);
365 | return array[i] === item ? i : -1;
366 | }
367 | if (nativeIndexOf && array.indexOf === nativeIndexOf) return array.indexOf(item);
368 | for (var i = 0, l = array.length; i < l; i++) if (array[i] === item) return i;
369 | return -1;
370 | };
371 |
372 |
373 | // Delegates to **ECMAScript 5**'s native `lastIndexOf` if available.
374 | _.lastIndexOf = function(array, item) {
375 | if (array == null) return -1;
376 | if (nativeLastIndexOf && array.lastIndexOf === nativeLastIndexOf) return array.lastIndexOf(item);
377 | var i = array.length;
378 | while (i--) if (array[i] === item) return i;
379 | return -1;
380 | };
381 |
382 | // Generate an integer Array containing an arithmetic progression. A port of
383 | // the native Python `range()` function. See
384 | // [the Python documentation](http://docs.python.org/library/functions.html#range).
385 | _.range = function(start, stop, step) {
386 | var args = slice.call(arguments),
387 | solo = args.length <= 1,
388 | start = solo ? 0 : args[0],
389 | stop = solo ? args[0] : args[1],
390 | step = args[2] || 1,
391 | len = Math.max(Math.ceil((stop - start) / step), 0),
392 | idx = 0,
393 | range = new Array(len);
394 | while (idx < len) {
395 | range[idx++] = start;
396 | start += step;
397 | }
398 | return range;
399 | };
400 |
401 | // Function (ahem) Functions
402 | // ------------------
403 |
404 | // Create a function bound to a given object (assigning `this`, and arguments,
405 | // optionally). Binding with arguments is also known as `curry`.
406 | _.bind = function(func, obj) {
407 | var args = slice.call(arguments, 2);
408 | return function() {
409 | return func.apply(obj || {}, args.concat(slice.call(arguments)));
410 | };
411 | };
412 |
413 | // Bind all of an object's methods to that object. Useful for ensuring that
414 | // all callbacks defined on an object belong to it.
415 | _.bindAll = function(obj) {
416 | var funcs = slice.call(arguments, 1);
417 | if (funcs.length == 0) funcs = _.functions(obj);
418 | each(funcs, function(f) { obj[f] = _.bind(obj[f], obj); });
419 | return obj;
420 | };
421 |
422 | // Memoize an expensive function by storing its results.
423 | _.memoize = function(func, hasher) {
424 | var memo = {};
425 | hasher = hasher || _.identity;
426 | return function() {
427 | var key = hasher.apply(this, arguments);
428 | return key in memo ? memo[key] : (memo[key] = func.apply(this, arguments));
429 | };
430 | };
431 |
432 | // Delays a function for the given number of milliseconds, and then calls
433 | // it with the arguments supplied.
434 | _.delay = function(func, wait) {
435 | var args = slice.call(arguments, 2);
436 | return setTimeout(function(){ return func.apply(func, args); }, wait);
437 | };
438 |
439 | // Defers a function, scheduling it to run after the current call stack has
440 | // cleared.
441 | _.defer = function(func) {
442 | return _.delay.apply(_, [func, 1].concat(slice.call(arguments, 1)));
443 | };
444 |
445 | // Internal function used to implement `_.throttle` and `_.debounce`.
446 | var limit = function(func, wait, debounce) {
447 | var timeout;
448 | return function() {
449 | var context = this, args = arguments;
450 | var throttler = function() {
451 | timeout = null;
452 | func.apply(context, args);
453 | };
454 | if (debounce) clearTimeout(timeout);
455 | if (debounce || !timeout) timeout = setTimeout(throttler, wait);
456 | };
457 | };
458 |
459 | // Returns a function, that, when invoked, will only be triggered at most once
460 | // during a given window of time.
461 | _.throttle = function(func, wait) {
462 | return limit(func, wait, false);
463 | };
464 |
465 | // Returns a function, that, as long as it continues to be invoked, will not
466 | // be triggered. The function will be called after it stops being called for
467 | // N milliseconds.
468 | _.debounce = function(func, wait) {
469 | return limit(func, wait, true);
470 | };
471 |
472 | // Returns the first function passed as an argument to the second,
473 | // allowing you to adjust arguments, run code before and after, and
474 | // conditionally execute the original function.
475 | _.wrap = function(func, wrapper) {
476 | return function() {
477 | var args = [func].concat(slice.call(arguments));
478 | return wrapper.apply(this, args);
479 | };
480 | };
481 |
482 | // Returns a function that is the composition of a list of functions, each
483 | // consuming the return value of the function that follows.
484 | _.compose = function() {
485 | var funcs = slice.call(arguments);
486 | return function() {
487 | var args = slice.call(arguments);
488 | for (var i=funcs.length-1; i >= 0; i--) {
489 | args = [funcs[i].apply(this, args)];
490 | }
491 | return args[0];
492 | };
493 | };
494 |
495 | // Object Functions
496 | // ----------------
497 |
498 | // Retrieve the names of an object's properties.
499 | // Delegates to **ECMAScript 5**'s native `Object.keys`
500 | _.keys = nativeKeys || function(obj) {
501 | if (_.isArray(obj)) return _.range(0, obj.length);
502 | var keys = [];
503 | for (var key in obj) if (hasOwnProperty.call(obj, key)) keys[keys.length] = key;
504 | return keys;
505 | };
506 |
507 | // Retrieve the values of an object's properties.
508 | _.values = function(obj) {
509 | return _.map(obj, _.identity);
510 | };
511 |
512 | // Return a sorted list of the function names available on the object.
513 | // Aliased as `methods`
514 | _.functions = _.methods = function(obj) {
515 | return _.filter(_.keys(obj), function(key){ return _.isFunction(obj[key]); }).sort();
516 | };
517 |
518 | // Extend a given object with all the properties in passed-in object(s).
519 | _.extend = function(obj) {
520 | each(slice.call(arguments, 1), function(source) {
521 | for (var prop in source) obj[prop] = source[prop];
522 | });
523 | return obj;
524 | };
525 |
526 | // Create a (shallow-cloned) duplicate of an object.
527 | _.clone = function(obj) {
528 | return _.isArray(obj) ? obj.slice() : _.extend({}, obj);
529 | };
530 |
531 | // Invokes interceptor with the obj, and then returns obj.
532 | // The primary purpose of this method is to "tap into" a method chain, in
533 | // order to perform operations on intermediate results within the chain.
534 | _.tap = function(obj, interceptor) {
535 | interceptor(obj);
536 | return obj;
537 | };
538 |
539 | // Perform a deep comparison to check if two objects are equal.
540 | _.isEqual = function(a, b) {
541 | // Check object identity.
542 | if (a === b) return true;
543 | // Different types?
544 | var atype = typeof(a), btype = typeof(b);
545 | if (atype != btype) return false;
546 | // Basic equality test (watch out for coercions).
547 | if (a == b) return true;
548 | // One is falsy and the other truthy.
549 | if ((!a && b) || (a && !b)) return false;
550 | // Unwrap any wrapped objects.
551 | if (a._chain) a = a._wrapped;
552 | if (b._chain) b = b._wrapped;
553 | // One of them implements an isEqual()?
554 | if (a.isEqual) return a.isEqual(b);
555 | // Check dates' integer values.
556 | if (_.isDate(a) && _.isDate(b)) return a.getTime() === b.getTime();
557 | // Both are NaN?
558 | if (_.isNaN(a) && _.isNaN(b)) return false;
559 | // Compare regular expressions.
560 | if (_.isRegExp(a) && _.isRegExp(b))
561 | return a.source === b.source &&
562 | a.global === b.global &&
563 | a.ignoreCase === b.ignoreCase &&
564 | a.multiline === b.multiline;
565 | // If a is not an object by this point, we can't handle it.
566 | if (atype !== 'object') return false;
567 | // Check for different array lengths before comparing contents.
568 | if (a.length && (a.length !== b.length)) return false;
569 | // Nothing else worked, deep compare the contents.
570 | var aKeys = _.keys(a), bKeys = _.keys(b);
571 | // Different object sizes?
572 | if (aKeys.length != bKeys.length) return false;
573 | // Recursive comparison of contents.
574 | for (var key in a) if (!(key in b) || !_.isEqual(a[key], b[key])) return false;
575 | return true;
576 | };
577 |
578 | // Is a given array or object empty?
579 | _.isEmpty = function(obj) {
580 | if (_.isArray(obj) || _.isString(obj)) return obj.length === 0;
581 | for (var key in obj) if (hasOwnProperty.call(obj, key)) return false;
582 | return true;
583 | };
584 |
585 | // Is a given value a DOM element?
586 | _.isElement = function(obj) {
587 | return !!(obj && obj.nodeType == 1);
588 | };
589 |
590 | // Is a given value an array?
591 | // Delegates to ECMA5's native Array.isArray
592 | _.isArray = nativeIsArray || function(obj) {
593 | return toString.call(obj) === '[object Array]';
594 | };
595 |
596 | // Is a given variable an arguments object?
597 | _.isArguments = function(obj) {
598 | return !!(obj && hasOwnProperty.call(obj, 'callee'));
599 | };
600 |
601 | // Is a given value a function?
602 | _.isFunction = function(obj) {
603 | return !!(obj && obj.constructor && obj.call && obj.apply);
604 | };
605 |
606 | // Is a given value a string?
607 | _.isString = function(obj) {
608 | return !!(obj === '' || (obj && obj.charCodeAt && obj.substr));
609 | };
610 |
611 | // Is a given value a number?
612 | _.isNumber = function(obj) {
613 | return !!(obj === 0 || (obj && obj.toExponential && obj.toFixed));
614 | };
615 |
616 | // Is the given value `NaN`? `NaN` happens to be the only value in JavaScript
617 | // that does not equal itself.
618 | _.isNaN = function(obj) {
619 | return obj !== obj;
620 | };
621 |
622 | // Is a given value a boolean?
623 | _.isBoolean = function(obj) {
624 | return obj === true || obj === false;
625 | };
626 |
627 | // Is a given value a date?
628 | _.isDate = function(obj) {
629 | return !!(obj && obj.getTimezoneOffset && obj.setUTCFullYear);
630 | };
631 |
632 | // Is the given value a regular expression?
633 | _.isRegExp = function(obj) {
634 | return !!(obj && obj.test && obj.exec && (obj.ignoreCase || obj.ignoreCase === false));
635 | };
636 |
637 | // Is a given value equal to null?
638 | _.isNull = function(obj) {
639 | return obj === null;
640 | };
641 |
642 | // Is a given variable undefined?
643 | _.isUndefined = function(obj) {
644 | return obj === void 0;
645 | };
646 |
647 | // Utility Functions
648 | // -----------------
649 |
650 | // Run Underscore.js in *noConflict* mode, returning the `_` variable to its
651 | // previous owner. Returns a reference to the Underscore object.
652 | _.noConflict = function() {
653 | root._ = previousUnderscore;
654 | return this;
655 | };
656 |
657 | // Keep the identity function around for default iterators.
658 | _.identity = function(value) {
659 | return value;
660 | };
661 |
662 | // Run a function **n** times.
663 | _.times = function (n, iterator, context) {
664 | for (var i = 0; i < n; i++) iterator.call(context, i);
665 | };
666 |
667 | // Add your own custom functions to the Underscore object, ensuring that
668 | // they're correctly added to the OOP wrapper as well.
669 | _.mixin = function(obj) {
670 | each(_.functions(obj), function(name){
671 | addToWrapper(name, _[name] = obj[name]);
672 | });
673 | };
674 |
675 | // Generate a unique integer id (unique within the entire client session).
676 | // Useful for temporary DOM ids.
677 | var idCounter = 0;
678 | _.uniqueId = function(prefix) {
679 | var id = idCounter++;
680 | return prefix ? prefix + id : id;
681 | };
682 |
683 | // By default, Underscore uses ERB-style template delimiters, change the
684 | // following template settings to use alternative delimiters.
685 | _.templateSettings = {
686 | evaluate : /<%([\s\S]+?)%>/g,
687 | interpolate : /<%=([\s\S]+?)%>/g
688 | };
689 |
690 | // JavaScript micro-templating, similar to John Resig's implementation.
691 | // Underscore templating handles arbitrary delimiters, preserves whitespace,
692 | // and correctly escapes quotes within interpolated code.
693 | _.template = function(str, data) {
694 | var c = _.templateSettings;
695 | var tmpl = 'var __p=[],print=function(){__p.push.apply(__p,arguments);};' +
696 | 'with(obj||{}){__p.push(\'' +
697 | str.replace(/\\/g, '\\\\')
698 | .replace(/'/g, "\\'")
699 | .replace(c.interpolate, function(match, code) {
700 | return "'," + code.replace(/\\'/g, "'") + ",'";
701 | })
702 | .replace(c.evaluate || null, function(match, code) {
703 | return "');" + code.replace(/\\'/g, "'")
704 | .replace(/[\r\n\t]/g, ' ') + "__p.push('";
705 | })
706 | .replace(/\r/g, '\\r')
707 | .replace(/\n/g, '\\n')
708 | .replace(/\t/g, '\\t')
709 | + "');}return __p.join('');";
710 | var func = new Function('obj', tmpl);
711 | return data ? func(data) : func;
712 | };
713 |
714 | // The OOP Wrapper
715 | // ---------------
716 |
717 | // If Underscore is called as a function, it returns a wrapped object that
718 | // can be used OO-style. This wrapper holds altered versions of all the
719 | // underscore functions. Wrapped objects may be chained.
720 | var wrapper = function(obj) { this._wrapped = obj; };
721 |
722 | // Expose `wrapper.prototype` as `_.prototype`
723 | _.prototype = wrapper.prototype;
724 |
725 | // Helper function to continue chaining intermediate results.
726 | var result = function(obj, chain) {
727 | return chain ? _(obj).chain() : obj;
728 | };
729 |
730 | // A method to easily add functions to the OOP wrapper.
731 | var addToWrapper = function(name, func) {
732 | wrapper.prototype[name] = function() {
733 | var args = slice.call(arguments);
734 | unshift.call(args, this._wrapped);
735 | return result(func.apply(_, args), this._chain);
736 | };
737 | };
738 |
739 | // Add all of the Underscore functions to the wrapper object.
740 | _.mixin(_);
741 |
742 | // Add all mutator Array functions to the wrapper.
743 | each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) {
744 | var method = ArrayProto[name];
745 | wrapper.prototype[name] = function() {
746 | method.apply(this._wrapped, arguments);
747 | return result(this._wrapped, this._chain);
748 | };
749 | });
750 |
751 | // Add all accessor Array functions to the wrapper.
752 | each(['concat', 'join', 'slice'], function(name) {
753 | var method = ArrayProto[name];
754 | wrapper.prototype[name] = function() {
755 | return result(method.apply(this._wrapped, arguments), this._chain);
756 | };
757 | });
758 |
759 | // Start chaining a wrapped Underscore object.
760 | wrapper.prototype.chain = function() {
761 | this._chain = true;
762 | return this;
763 | };
764 |
765 | // Extracts the result from a wrapped and chained object.
766 | wrapper.prototype.value = function() {
767 | return this._wrapped;
768 | };
769 |
770 | })();
771 |
--------------------------------------------------------------------------------
/public/music/blue.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/angular/peepcode-tunes/cc3cd4a44b48e34afe9f34d60810be95716fd589/public/music/blue.mp3
--------------------------------------------------------------------------------
/public/music/jazz.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/angular/peepcode-tunes/cc3cd4a44b48e34afe9f34d60810be95716fd589/public/music/jazz.mp3
--------------------------------------------------------------------------------
/public/music/minimalish.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/angular/peepcode-tunes/cc3cd4a44b48e34afe9f34d60810be95716fd589/public/music/minimalish.mp3
--------------------------------------------------------------------------------
/public/music/slower.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/angular/peepcode-tunes/cc3cd4a44b48e34afe9f34d60810be95716fd589/public/music/slower.mp3
--------------------------------------------------------------------------------
/public/style/fancypants.css:
--------------------------------------------------------------------------------
1 | body {
2 | font-family: "Helvetica Neue", "Helvetica", "Calibri", "sans"
3 | }
4 |
5 | .albums {
6 | }
7 |
8 | .album {
9 | font-style: italic;
10 | }
11 |
12 | .album .artist-name {
13 | font-style: normal;
14 | font-weight: lighter;
15 | color: #999;
16 | }
17 |
18 | .album button.queue {
19 | }
20 |
21 | .playlist {
22 | color: #eee;
23 | background-color: #333;
24 | }
25 |
26 | .playlist button.control {
27 | }
28 |
29 | .playlist button.play {
30 | }
31 |
32 | .playlist button.play:hover {
33 | }
34 |
35 | .playlist button.pause {
36 | }
37 |
38 | .playlist button.pause:hover {
39 | }
40 |
41 | .playlist button.prev {
42 | }
43 |
44 | .playlist button.prev:hover {
45 | }
46 |
47 | .playlist button.next {
48 | }
49 |
50 | .playlist button.next:hover {
51 | }
52 |
53 | .playlist .album .queue.add {
54 | }
55 |
56 | .playlist .album.current {
57 | background-color: #383838;
58 | }
59 |
60 | .playlist .album.current .tracks .current {
61 | background-color: #995599;
62 | }
63 |
64 | .playlist .album .tracks li {
65 | font-style: normal;
66 | padding: .3em;
67 | }
68 |
69 | .library {
70 | }
71 |
72 | .library .album .queue.remove {
73 | }
74 |
75 | .library .album .tracks {
76 | }
77 |
78 | /* vim:set sw=4:ts=4:expandtab */
79 |
--------------------------------------------------------------------------------
/public/style/screen.css:
--------------------------------------------------------------------------------
1 | html, body {
2 | height: 100%;
3 | }
4 |
5 | body {
6 | margin: 0;
7 | padding: 0;
8 | }
9 |
10 | #container {
11 | height: auto;
12 | min-height: 100%;
13 | position: relative;
14 | }
15 |
16 | .albums {
17 | list-style: none;
18 | padding: 0 2%;
19 | }
20 |
21 | .album {
22 | list-style: none;
23 | display: inline-block;
24 | position: relative;
25 | width: 80%;
26 | margin: 0;
27 | padding: 1em 2em;
28 | }
29 |
30 | .track {
31 | cursor: pointer;
32 | }
33 |
34 | .album .tracks {
35 | margin: 0;
36 | padding: .5em 0 0 1.5em;
37 | }
38 |
39 | .album button.queue {
40 | position: absolute;
41 | left: 0;
42 | width: 16px;
43 | height: 16px;
44 | border: 0;
45 | padding: 0;
46 | margin: 0;
47 | background: transparent;
48 | cursor: pointer;
49 | }
50 |
51 | .album .queue.add {
52 | background-image: url('../images/add.png');
53 | }
54 |
55 | .album .queue.remove {
56 | background-image: url('../images/remove.png');
57 | }
58 |
59 | .album-title {
60 | margin-right: 10px;
61 | }
62 |
63 | .playlist {
64 | position: absolute;
65 | width: 30%;
66 | padding: 0 3%;
67 | min-height: 100%;
68 | }
69 |
70 | nav {
71 | width: 180px;
72 | margin: auto;
73 | padding: 0;
74 | }
75 |
76 | nav button.control {
77 | text-indent: -9999px;
78 | width: 55px;
79 | height: 50px;
80 | border: 0;
81 | padding: 10px;
82 | margin: 0;
83 | background: transparent;
84 | cursor: pointer;
85 | }
86 |
87 | nav button.play {
88 | background: url('../images/control_play.png') center no-repeat;
89 | }
90 |
91 | nav button.play:hover {
92 | background: url('../images/control_play_hover.png') center no-repeat;
93 | }
94 |
95 | nav button.pause {
96 | background: url('../images/control_pause.png') center no-repeat;
97 | }
98 |
99 | nav button.pause:hover {
100 | background: url('../images/control_pause_hover.png') center no-repeat;
101 | }
102 |
103 | nav button.prev {
104 | background: url('../images/control_start.png') center no-repeat;
105 | }
106 |
107 | nav button.prev:hover {
108 | background: url('../images/control_start_hover.png') center no-repeat;
109 | }
110 |
111 | nav button.next {
112 | background: url('../images/control_end.png') center no-repeat;
113 | }
114 |
115 | nav button.next:hover {
116 | background: url('../images/control_end_hover.png') center no-repeat;
117 | }
118 |
119 | .library {
120 | margin-left: 36%;
121 | padding: 0.1em 2%;
122 | }
123 |
124 | /* vim:set sw=4:ts=4:expandtab */
125 |
--------------------------------------------------------------------------------
/server.js:
--------------------------------------------------------------------------------
1 | var sys = require('sys'),
2 | http = require('http'),
3 | fs = require('fs'),
4 | url = require('url'),
5 | events = require('events');
6 |
7 | var DEFAULT_PORT = 9292;
8 |
9 | function main(argv) {
10 | new HttpServer({
11 | 'GET': createServlet(StaticServlet),
12 | 'HEAD': createServlet(StaticServlet)
13 | }).start(Number(argv[2]) || DEFAULT_PORT);
14 | }
15 |
16 | function escapeHtml(value) {
17 | return value.toString().
18 | replace('<', '<').
19 | replace('>', '>').
20 | replace('"', '"');
21 | }
22 |
23 | function createServlet(Class) {
24 | var servlet = new Class();
25 | return servlet.handleRequest.bind(servlet);
26 | }
27 |
28 | /**
29 | * An Http server implementation that uses a map of methods to decide
30 | * action routing.
31 | *
32 | * @param {Object} Map of method => Handler function
33 | */
34 | function HttpServer(handlers) {
35 | this.handlers = handlers;
36 | this.server = http.createServer(this.handleRequest_.bind(this));
37 | }
38 |
39 | HttpServer.prototype.start = function(port) {
40 | this.port = port;
41 | this.server.listen(port);
42 | sys.puts('Http Server running at http://127.0.0.1:' + port + '/');
43 | };
44 |
45 | HttpServer.prototype.parseUrl_ = function(urlString) {
46 | var parsed = url.parse(urlString);
47 | parsed.pathname = url.resolve('/', parsed.pathname);
48 | return url.parse(url.format(parsed), true);
49 | };
50 |
51 | HttpServer.prototype.handleRequest_ = function(req, res) {
52 | var logEntry = req.method + ' ' + req.url;
53 | if (req.headers['user-agent']) {
54 | logEntry += ' ' + req.headers['user-agent'];
55 | }
56 | sys.puts(logEntry);
57 | req.url = this.parseUrl_(req.url);
58 | var handler = this.handlers[req.method];
59 | if (!handler) {
60 | res.writeHead(501);
61 | res.end();
62 | } else {
63 | handler.call(this, req, res);
64 | }
65 | };
66 |
67 | /**
68 | * Handles static content.
69 | */
70 | function StaticServlet() {}
71 |
72 | StaticServlet.MimeMap = {
73 | 'txt': 'text/plain',
74 | 'html': 'text/html',
75 | 'css': 'text/css',
76 | 'xml': 'application/xml',
77 | 'json': 'application/json',
78 | 'js': 'application/javascript',
79 | 'jpg': 'image/jpeg',
80 | 'jpeg': 'image/jpeg',
81 | 'gif': 'image/gif',
82 | 'png': 'image/png',
83 | 'manifest': 'text/cache-manifest'
84 | };
85 |
86 | StaticServlet.prototype.handleRequest = function(req, res) {
87 | var self = this;
88 | var path = ('./' + req.url.pathname).replace('//','/').replace(/%(..)/g, function(match, hex){
89 | return String.fromCharCode(parseInt(hex, 16));
90 | });
91 | var parts = path.split('/');
92 | if (parts[parts.length-1].charAt(0) === '.')
93 | return self.sendForbidden_(req, res, path);
94 |
95 | // docs rewriting
96 | if (path == './') path = 'index.html';
97 | if (path == './albums') path = 'albums.json';
98 | path = 'public/' + path;
99 | // end of docs rewriting
100 |
101 | fs.stat(path, function(err, stat) {
102 | if (err)
103 | return self.sendMissing_(req, res, path);
104 | if (stat.isDirectory())
105 | return fs.stat(path + 'index.html', function(err, stat) {
106 | // send index.html if exists
107 | if (!err)
108 | return self.sendFile_(req, res, path + 'index.html');
109 |
110 | // list files otherwise
111 | return self.sendDirectory_(req, res, path);
112 | });
113 |
114 | return self.sendFile_(req, res, path);
115 | });
116 | };
117 |
118 | StaticServlet.prototype.sendError_ = function(req, res, error) {
119 | res.writeHead(500, {
120 | 'Content-Type': 'text/html'
121 | });
122 | res.write('\n');
123 | res.write('Internal Server Error\n');
124 | res.write('Internal Server Error
');
125 | res.write('' + escapeHtml(sys.inspect(error)) + '
');
126 | sys.puts('500 Internal Server Error');
127 | sys.puts(sys.inspect(error));
128 | };
129 |
130 | StaticServlet.prototype.sendMissing_ = function(req, res, path) {
131 | path = path.substring(1);
132 | res.writeHead(404, {
133 | 'Content-Type': 'text/html'
134 | });
135 | res.write('\n');
136 | res.write('404 Not Found\n');
137 | res.write('Not Found
');
138 | res.write(
139 | 'The requested URL ' +
140 | escapeHtml(path) +
141 | ' was not found on this server.
'
142 | );
143 | res.end();
144 | sys.puts('404 Not Found: ' + path);
145 | };
146 |
147 | StaticServlet.prototype.sendForbidden_ = function(req, res, path) {
148 | path = path.substring(1);
149 | res.writeHead(403, {
150 | 'Content-Type': 'text/html'
151 | });
152 | res.write('\n');
153 | res.write('403 Forbidden\n');
154 | res.write('Forbidden
');
155 | res.write(
156 | 'You do not have permission to access ' +
157 | escapeHtml(path) + ' on this server.
'
158 | );
159 | res.end();
160 | sys.puts('403 Forbidden: ' + path);
161 | };
162 |
163 | StaticServlet.prototype.sendRedirect_ = function(req, res, redirectUrl) {
164 | res.writeHead(301, {
165 | 'Content-Type': 'text/html',
166 | 'Location': redirectUrl
167 | });
168 | res.write('\n');
169 | res.write('301 Moved Permanently\n');
170 | res.write('Moved Permanently
');
171 | res.write(
172 | 'The document has moved here.
'
175 | );
176 | res.end();
177 | sys.puts('401 Moved Permanently: ' + redirectUrl);
178 | };
179 |
180 | StaticServlet.prototype.sendFile_ = function(req, res, path) {
181 | var self = this;
182 | var file = fs.createReadStream(path);
183 | res.writeHead(200, {
184 | 'Content-Type': StaticServlet.
185 | MimeMap[path.split('.').pop()] || 'text/plain'
186 | });
187 | if (req.method === 'HEAD') {
188 | res.end();
189 | } else {
190 | file.on('data', res.write.bind(res));
191 | file.on('close', function() {
192 | res.end();
193 | });
194 | file.on('error', function(error) {
195 | self.sendError_(req, res, error);
196 | });
197 | }
198 | };
199 |
200 | StaticServlet.prototype.sendDirectory_ = function(req, res, path) {
201 | var self = this;
202 | if (path.match(/[^\/]$/)) {
203 | req.url.pathname += '/';
204 | var redirectUrl = url.format(url.parse(url.format(req.url)));
205 | return self.sendRedirect_(req, res, redirectUrl);
206 | }
207 | fs.readdir(path, function(err, files) {
208 | if (err)
209 | return self.sendError_(req, res, error);
210 |
211 | if (!files.length)
212 | return self.writeDirectoryIndex_(req, res, path, []);
213 |
214 | var remaining = files.length;
215 | files.forEach(function(fileName, index) {
216 | fs.stat(path + '/' + fileName, function(err, stat) {
217 | if (err)
218 | return self.sendError_(req, res, err);
219 | if (stat.isDirectory()) {
220 | files[index] = fileName + '/';
221 | }
222 | if (!(--remaining))
223 | return self.writeDirectoryIndex_(req, res, path, files);
224 | });
225 | });
226 | });
227 | };
228 |
229 | StaticServlet.prototype.writeDirectoryIndex_ = function(req, res, path, files) {
230 | path = path.substring(1);
231 | res.writeHead(200, {
232 | 'Content-Type': 'text/html'
233 | });
234 | if (req.method === 'HEAD') {
235 | res.end();
236 | return;
237 | }
238 | res.write('\n');
239 | res.write('' + escapeHtml(path) + '\n');
240 | res.write('\n');
243 | res.write('Directory: ' + escapeHtml(path) + '
');
244 | res.write('');
245 | files.forEach(function(fileName) {
246 | if (fileName.charAt(0) !== '.') {
247 | res.write('- ' +
249 | escapeHtml(fileName) + '
');
250 | }
251 | });
252 | res.write('
');
253 | res.end();
254 | };
255 |
256 | // Must be last,
257 | main(process.argv);
258 |
--------------------------------------------------------------------------------
/test-js/SpecRunner.html:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 | Jasmine Test Runner
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/test-js/lib/jasmine/MIT.LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2008-2011 Pivotal Labs
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining
4 | a copy of this software and associated documentation files (the
5 | "Software"), to deal in the Software without restriction, including
6 | without limitation the rights to use, copy, modify, merge, publish,
7 | distribute, sublicense, and/or sell copies of the Software, and to
8 | permit persons to whom the Software is furnished to do so, subject to
9 | the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be
12 | included in all copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/test-js/lib/jasmine/jasmine-html.js:
--------------------------------------------------------------------------------
1 | jasmine.TrivialReporter = function(doc) {
2 | this.document = doc || document;
3 | this.suiteDivs = {};
4 | this.logRunningSpecs = false;
5 | };
6 |
7 | jasmine.TrivialReporter.prototype.createDom = function(type, attrs, childrenVarArgs) {
8 | var el = document.createElement(type);
9 |
10 | for (var i = 2; i < arguments.length; i++) {
11 | var child = arguments[i];
12 |
13 | if (typeof child === 'string') {
14 | el.appendChild(document.createTextNode(child));
15 | } else {
16 | if (child) { el.appendChild(child); }
17 | }
18 | }
19 |
20 | for (var attr in attrs) {
21 | if (attr == "className") {
22 | el[attr] = attrs[attr];
23 | } else {
24 | el.setAttribute(attr, attrs[attr]);
25 | }
26 | }
27 |
28 | return el;
29 | };
30 |
31 | jasmine.TrivialReporter.prototype.reportRunnerStarting = function(runner) {
32 | var showPassed, showSkipped;
33 |
34 | this.outerDiv = this.createDom('div', { className: 'jasmine_reporter' },
35 | this.createDom('div', { className: 'banner' },
36 | this.createDom('div', { className: 'logo' },
37 | this.createDom('span', { className: 'title' }, "Jasmine"),
38 | this.createDom('span', { className: 'version' }, runner.env.versionString())),
39 | this.createDom('div', { className: 'options' },
40 | "Show ",
41 | showPassed = this.createDom('input', { id: "__jasmine_TrivialReporter_showPassed__", type: 'checkbox' }),
42 | this.createDom('label', { "for": "__jasmine_TrivialReporter_showPassed__" }, " passed "),
43 | showSkipped = this.createDom('input', { id: "__jasmine_TrivialReporter_showSkipped__", type: 'checkbox' }),
44 | this.createDom('label', { "for": "__jasmine_TrivialReporter_showSkipped__" }, " skipped")
45 | )
46 | ),
47 |
48 | this.runnerDiv = this.createDom('div', { className: 'runner running' },
49 | this.createDom('a', { className: 'run_spec', href: '?' }, "run all"),
50 | this.runnerMessageSpan = this.createDom('span', {}, "Running..."),
51 | this.finishedAtSpan = this.createDom('span', { className: 'finished-at' }, ""))
52 | );
53 |
54 | this.document.body.appendChild(this.outerDiv);
55 |
56 | var suites = runner.suites();
57 | for (var i = 0; i < suites.length; i++) {
58 | var suite = suites[i];
59 | var suiteDiv = this.createDom('div', { className: 'suite' },
60 | this.createDom('a', { className: 'run_spec', href: '?spec=' + encodeURIComponent(suite.getFullName()) }, "run"),
61 | this.createDom('a', { className: 'description', href: '?spec=' + encodeURIComponent(suite.getFullName()) }, suite.description));
62 | this.suiteDivs[suite.id] = suiteDiv;
63 | var parentDiv = this.outerDiv;
64 | if (suite.parentSuite) {
65 | parentDiv = this.suiteDivs[suite.parentSuite.id];
66 | }
67 | parentDiv.appendChild(suiteDiv);
68 | }
69 |
70 | this.startedAt = new Date();
71 |
72 | var self = this;
73 | showPassed.onclick = function(evt) {
74 | if (showPassed.checked) {
75 | self.outerDiv.className += ' show-passed';
76 | } else {
77 | self.outerDiv.className = self.outerDiv.className.replace(/ show-passed/, '');
78 | }
79 | };
80 |
81 | showSkipped.onclick = function(evt) {
82 | if (showSkipped.checked) {
83 | self.outerDiv.className += ' show-skipped';
84 | } else {
85 | self.outerDiv.className = self.outerDiv.className.replace(/ show-skipped/, '');
86 | }
87 | };
88 | };
89 |
90 | jasmine.TrivialReporter.prototype.reportRunnerResults = function(runner) {
91 | var results = runner.results();
92 | var className = (results.failedCount > 0) ? "runner failed" : "runner passed";
93 | this.runnerDiv.setAttribute("class", className);
94 | //do it twice for IE
95 | this.runnerDiv.setAttribute("className", className);
96 | var specs = runner.specs();
97 | var specCount = 0;
98 | for (var i = 0; i < specs.length; i++) {
99 | if (this.specFilter(specs[i])) {
100 | specCount++;
101 | }
102 | }
103 | var message = "" + specCount + " spec" + (specCount == 1 ? "" : "s" ) + ", " + results.failedCount + " failure" + ((results.failedCount == 1) ? "" : "s");
104 | message += " in " + ((new Date().getTime() - this.startedAt.getTime()) / 1000) + "s";
105 | this.runnerMessageSpan.replaceChild(this.createDom('a', { className: 'description', href: '?'}, message), this.runnerMessageSpan.firstChild);
106 |
107 | this.finishedAtSpan.appendChild(document.createTextNode("Finished at " + new Date().toString()));
108 | };
109 |
110 | jasmine.TrivialReporter.prototype.reportSuiteResults = function(suite) {
111 | var results = suite.results();
112 | var status = results.passed() ? 'passed' : 'failed';
113 | if (results.totalCount === 0) { // todo: change this to check results.skipped
114 | status = 'skipped';
115 | }
116 | this.suiteDivs[suite.id].className += " " + status;
117 | };
118 |
119 | jasmine.TrivialReporter.prototype.reportSpecStarting = function(spec) {
120 | if (this.logRunningSpecs) {
121 | this.log('>> Jasmine Running ' + spec.suite.description + ' ' + spec.description + '...');
122 | }
123 | };
124 |
125 | jasmine.TrivialReporter.prototype.reportSpecResults = function(spec) {
126 | var results = spec.results();
127 | var status = results.passed() ? 'passed' : 'failed';
128 | if (results.skipped) {
129 | status = 'skipped';
130 | }
131 | var specDiv = this.createDom('div', { className: 'spec ' + status },
132 | this.createDom('a', { className: 'run_spec', href: '?spec=' + encodeURIComponent(spec.getFullName()) }, "run"),
133 | this.createDom('a', {
134 | className: 'description',
135 | href: '?spec=' + encodeURIComponent(spec.getFullName()),
136 | title: spec.getFullName()
137 | }, spec.description));
138 |
139 |
140 | var resultItems = results.getItems();
141 | var messagesDiv = this.createDom('div', { className: 'messages' });
142 | for (var i = 0; i < resultItems.length; i++) {
143 | var result = resultItems[i];
144 |
145 | if (result.type == 'log') {
146 | messagesDiv.appendChild(this.createDom('div', {className: 'resultMessage log'}, result.toString()));
147 | } else if (result.type == 'expect' && result.passed && !result.passed()) {
148 | messagesDiv.appendChild(this.createDom('div', {className: 'resultMessage fail'}, result.message));
149 |
150 | if (result.trace.stack) {
151 | messagesDiv.appendChild(this.createDom('div', {className: 'stackTrace'}, result.trace.stack));
152 | }
153 | }
154 | }
155 |
156 | if (messagesDiv.childNodes.length > 0) {
157 | specDiv.appendChild(messagesDiv);
158 | }
159 |
160 | this.suiteDivs[spec.suite.id].appendChild(specDiv);
161 | };
162 |
163 | jasmine.TrivialReporter.prototype.log = function() {
164 | var console = jasmine.getGlobal().console;
165 | if (console && console.log) {
166 | if (console.log.apply) {
167 | console.log.apply(console, arguments);
168 | } else {
169 | console.log(arguments); // ie fix: console.log.apply doesn't exist on ie
170 | }
171 | }
172 | };
173 |
174 | jasmine.TrivialReporter.prototype.getLocation = function() {
175 | return this.document.location;
176 | };
177 |
178 | jasmine.TrivialReporter.prototype.specFilter = function(spec) {
179 | var paramMap = {};
180 | var params = this.getLocation().search.substring(1).split('&');
181 | for (var i = 0; i < params.length; i++) {
182 | var p = params[i].split('=');
183 | paramMap[decodeURIComponent(p[0])] = decodeURIComponent(p[1]);
184 | }
185 |
186 | if (!paramMap.spec) {
187 | return true;
188 | }
189 | return spec.getFullName().indexOf(paramMap.spec) === 0;
190 | };
191 |
--------------------------------------------------------------------------------
/test-js/lib/jasmine/jasmine.css:
--------------------------------------------------------------------------------
1 | body {
2 | font-family: "Helvetica Neue Light", "Lucida Grande", "Calibri", "Arial", sans-serif;
3 | }
4 |
5 |
6 | .jasmine_reporter a:visited, .jasmine_reporter a {
7 | color: #303;
8 | }
9 |
10 | .jasmine_reporter a:hover, .jasmine_reporter a:active {
11 | color: blue;
12 | }
13 |
14 | .run_spec {
15 | float:right;
16 | padding-right: 5px;
17 | font-size: .8em;
18 | text-decoration: none;
19 | }
20 |
21 | .jasmine_reporter {
22 | margin: 0 5px;
23 | }
24 |
25 | .banner {
26 | color: #303;
27 | background-color: #fef;
28 | padding: 5px;
29 | }
30 |
31 | .logo {
32 | float: left;
33 | font-size: 1.1em;
34 | padding-left: 5px;
35 | }
36 |
37 | .logo .version {
38 | font-size: .6em;
39 | padding-left: 1em;
40 | }
41 |
42 | .runner.running {
43 | background-color: yellow;
44 | }
45 |
46 |
47 | .options {
48 | text-align: right;
49 | font-size: .8em;
50 | }
51 |
52 |
53 |
54 |
55 | .suite {
56 | border: 1px outset gray;
57 | margin: 5px 0;
58 | padding-left: 1em;
59 | }
60 |
61 | .suite .suite {
62 | margin: 5px;
63 | }
64 |
65 | .suite.passed {
66 | background-color: #dfd;
67 | }
68 |
69 | .suite.failed {
70 | background-color: #fdd;
71 | }
72 |
73 | .spec {
74 | margin: 5px;
75 | padding-left: 1em;
76 | clear: both;
77 | }
78 |
79 | .spec.failed, .spec.passed, .spec.skipped {
80 | padding-bottom: 5px;
81 | border: 1px solid gray;
82 | }
83 |
84 | .spec.failed {
85 | background-color: #fbb;
86 | border-color: red;
87 | }
88 |
89 | .spec.passed {
90 | background-color: #bfb;
91 | border-color: green;
92 | }
93 |
94 | .spec.skipped {
95 | background-color: #bbb;
96 | }
97 |
98 | .messages {
99 | border-left: 1px dashed gray;
100 | padding-left: 1em;
101 | padding-right: 1em;
102 | }
103 |
104 | .passed {
105 | background-color: #cfc;
106 | display: none;
107 | }
108 |
109 | .failed {
110 | background-color: #fbb;
111 | }
112 |
113 | .skipped {
114 | color: #777;
115 | background-color: #eee;
116 | display: none;
117 | }
118 |
119 |
120 | /*.resultMessage {*/
121 | /*white-space: pre;*/
122 | /*}*/
123 |
124 | .resultMessage span.result {
125 | display: block;
126 | line-height: 2em;
127 | color: black;
128 | }
129 |
130 | .resultMessage .mismatch {
131 | color: black;
132 | }
133 |
134 | .stackTrace {
135 | white-space: pre;
136 | font-size: .8em;
137 | margin-left: 10px;
138 | max-height: 5em;
139 | overflow: auto;
140 | border: 1px inset red;
141 | padding: 1em;
142 | background: #eef;
143 | }
144 |
145 | .finished-at {
146 | padding-left: 1em;
147 | font-size: .6em;
148 | }
149 |
150 | .show-passed .passed,
151 | .show-skipped .skipped {
152 | display: block;
153 | }
154 |
155 |
156 | #jasmine_content {
157 | position:fixed;
158 | right: 100%;
159 | }
160 |
161 | .runner {
162 | border: 1px solid gray;
163 | display: block;
164 | margin: 5px 0;
165 | padding: 2px 0 2px 10px;
166 | }
167 |
--------------------------------------------------------------------------------
/test-js/lib/jasmine/jasmine_favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/angular/peepcode-tunes/cc3cd4a44b48e34afe9f34d60810be95716fd589/test-js/lib/jasmine/jasmine_favicon.png
--------------------------------------------------------------------------------
/test-js/lib/jasmine/version.txt:
--------------------------------------------------------------------------------
1 | 1.1.0
2 |
--------------------------------------------------------------------------------
/test-js/spec/TunesSpec.js:
--------------------------------------------------------------------------------
1 | var albumData = [{
2 | "title": "Album A",
3 | "artist": "Artist A",
4 | "tracks": [
5 | {
6 | "title": "Track A",
7 | "url": "/music/Album A Track A.mp3"
8 | },
9 | {
10 | "title": "Track B",
11 | "url": "/music/Album A Track B.mp3"
12 | }]
13 | }, {
14 | "title": "Album B",
15 | "artist": "Artist B",
16 | "tracks": [
17 | {
18 | "title": "Track A",
19 | "url": "/music/Album B Track A.mp3"
20 | },
21 | {
22 | "title": "Track B",
23 | "url": "/music/Album B Track B.mp3"
24 | }]
25 | }];
26 |
27 |
28 | beforeEach(module('tunesApp'));
29 |
30 | describe('TunesCtrl', function() {
31 |
32 | it('should initialize the scope for the view', inject(function($rootScope, $httpBackend, $controller) {
33 | var ctrlScope;
34 |
35 | $httpBackend.expectGET('albums.json').respond(albumData);
36 | ctrlScope = $rootScope.$new();
37 | $controller(TunesCtrl, {$scope: ctrlScope});
38 |
39 | expect(ctrlScope.player.playlist.length).toBe(0);
40 | expect(ctrlScope.albums).toBeUndefined();
41 |
42 | $httpBackend.flush();
43 |
44 | expect(ctrlScope.albums).toBe(albumData);
45 | }));
46 | });
47 |
48 |
49 | describe('player service', function() {
50 | var player,
51 | audioMock;
52 |
53 |
54 | beforeEach(module(function($provide) {
55 | audioMock = {
56 | play: jasmine.createSpy('play'),
57 | pause: jasmine.createSpy('pause'),
58 | src: undefined,
59 | addEventListener: jasmine.createSpy('addEventListener').andCallFake(
60 | function(event, fn, capture) {
61 | expect(event).toBe('ended');
62 | expect(capture).toBe(false);
63 | audioMock.endedFn = fn;
64 | })
65 | }
66 | $provide.value('audio', audioMock);
67 | }));
68 |
69 |
70 | beforeEach(inject(function($injector) {
71 | player = $injector.get('player');
72 | }));
73 |
74 |
75 | it('should initialize the player', function() {
76 | expect(player.playlist.length).toBe(0);
77 | expect(player.playing).toBe(false);
78 | expect(player.current).toEqual({album: 0, track: 0});
79 | });
80 |
81 |
82 | it('should register an ended event listener on adio', function() {
83 | expect(audioMock.addEventListener).toHaveBeenCalled();
84 | });
85 |
86 |
87 | it('should call player.next() when the ended event fires', function() {
88 | player.playlist.add(albumData[0]);
89 | player.play();
90 | expect(audioMock.src).toBe("/music/Album A Track A.mp3");
91 | audioMock.endedFn();
92 | expect(audioMock.src).toBe("/music/Album A Track B.mp3");
93 | });
94 |
95 |
96 | describe('play', function() {
97 |
98 | it('should not do anything if playlist is empty', function() {
99 | player.play();
100 | expect(player.playing).toBe(false);
101 | expect(audioMock.play).not.toHaveBeenCalled();
102 | });
103 |
104 |
105 | it('should play the currently selected song', function() {
106 | player.playlist.add(albumData[0]);
107 | player.play();
108 | expect(player.playing).toBe(true);
109 | expect(audioMock.play).toHaveBeenCalled();
110 | expect(audioMock.src).toBe("/music/Album A Track A.mp3");
111 | });
112 |
113 |
114 | it('should resume playing a song after paused', function() {
115 | player.playlist.add(albumData[0]);
116 | player.play();
117 | player.pause();
118 | audioMock.play.reset();
119 | audioMock.src = 'test'; // player must not touch the src property when resuming play
120 | player.play();
121 | expect(player.playing).toBe(true);
122 | expect(audioMock.play).toHaveBeenCalled();
123 | expect(audioMock.src).toBe('test');
124 | });
125 | });
126 |
127 |
128 | describe('pause', function() {
129 |
130 | it('should not do anything if player is not playing', function() {
131 | player.pause();
132 | expect(player.playing).toBe(false);
133 | expect(audioMock.pause).not.toHaveBeenCalled();
134 | });
135 |
136 | it('should pause the player when playing', function() {
137 | player.playlist.add(albumData[0]);
138 | player.play();
139 | expect(player.playing).toBe(true);
140 | player.pause();
141 | expect(player.playing).toBe(false);
142 | expect(audioMock.pause).toHaveBeenCalled();
143 | });
144 | });
145 |
146 |
147 | describe('reset', function() {
148 |
149 | it('should stop currently playing song and reset the internal state', function() {
150 | player.playlist.add(albumData[0]);
151 | player.current.track = 1;
152 | player.play();
153 | expect(player.playing).toBe(true);
154 |
155 | player.reset();
156 | expect(player.playing).toBe(false);
157 | expect(audioMock.pause).toHaveBeenCalled();
158 | expect(player.current).toEqual({album: 0, track: 0});
159 | });
160 | });
161 |
162 |
163 | describe('next', function() {
164 |
165 | it('should do nothing if playlist is empty', function() {
166 | player.next();
167 | expect(player.current).toEqual({album: 0, track: 0});
168 | });
169 |
170 | it('should advance to the next song in the album', function() {
171 | player.playlist.add(albumData[0]);
172 | player.next();
173 | expect(player.current).toEqual({album: 0, track: 1});
174 | });
175 |
176 | it('should wrap around when on last song and there is just one album in playlist', function() {
177 | player.playlist.add(albumData[0]);
178 | player.next();
179 | player.next();
180 | expect(player.current).toEqual({album: 0, track: 0});
181 | });
182 |
183 | it('should wrap around when on last song and there are multiple albums in playlist', function() {
184 | player.playlist.add(albumData[0]);
185 | player.playlist.add(albumData[1]);
186 | player.current.album = 1;
187 | player.current.track = 1;
188 | player.next();
189 | expect(player.current).toEqual({album: 0, track: 0});
190 | });
191 |
192 | it('should start playing the next song if currently playing', function() {
193 | player.playlist.add(albumData[0]);
194 | player.play();
195 | audioMock.play.reset();
196 | player.next();
197 | expect(player.playing).toBe(true);
198 | expect(audioMock.play).toHaveBeenCalled();
199 | expect(audioMock.src).toBe('/music/Album A Track B.mp3');
200 | });
201 | });
202 |
203 |
204 | describe('previous', function() {
205 |
206 | it('should do nothing if playlist is empty', function() {
207 | player.previous();
208 | expect(player.current).toEqual({album: 0, track: 0});
209 | });
210 |
211 | it('should move to the previous song in the album', function() {
212 | player.playlist.add(albumData[0]);
213 | player.next();
214 | player.previous();
215 | expect(player.current).toEqual({album: 0, track: 0});
216 | });
217 |
218 | it('should wrap around when on first song and there is just one album in playlist', function() {
219 | player.playlist.add(albumData[0]);
220 | player.previous();
221 | expect(player.current).toEqual({album: 0, track: 1});
222 | });
223 |
224 | it('should wrap around when on first song and there are multiple albums in playlist', function() {
225 | player.playlist.add(albumData[0]);
226 | player.playlist.add(albumData[1]);
227 | player.previous();
228 | expect(player.current).toEqual({album: 1, track: 1});
229 | });
230 |
231 | it('should start playing the next song if currently playing', function() {
232 | player.playlist.add(albumData[0]);
233 | player.play();
234 | audioMock.play.reset();
235 | player.previous();
236 | expect(player.playing).toBe(true);
237 | expect(audioMock.play).toHaveBeenCalled();
238 | expect(audioMock.src).toBe('/music/Album A Track B.mp3');
239 | });
240 | });
241 |
242 |
243 | describe('playlist', function() {
244 |
245 | it('should be a simple array', function() {
246 | expect(player.playlist.constructor).toBe([].constructor);
247 | });
248 |
249 |
250 | describe('add', function() {
251 |
252 | it("should add an album to the playlist if it's not present there already", function() {
253 | expect(player.playlist.length).toBe(0);
254 | player.playlist.add(albumData[0]);
255 | expect(player.playlist.length).toBe(1);
256 | player.playlist.add(albumData[1]);
257 | expect(player.playlist.length).toBe(2);
258 | player.playlist.add(albumData[0]);
259 | expect(player.playlist.length).toBe(2); // nothing happened, already there
260 | });
261 | });
262 |
263 |
264 | describe('remove', function() {
265 |
266 | it('should remove an album from the playlist if present', function() {
267 | player.playlist.add(albumData[0]);
268 | player.playlist.add(albumData[1]);
269 | expect(player.playlist.length).toBe(2);
270 |
271 | player.playlist.remove(albumData[0]);
272 | expect(player.playlist.length).toBe(1);
273 | expect(player.playlist[0].title).toBe('Album B');
274 |
275 | player.playlist.remove(albumData[1]);
276 | expect(player.playlist.length).toBe(0);
277 |
278 | player.playlist.remove(albumData[0]); // nothing happend, not in the playlist
279 | expect(player.playlist.length).toBe(0);
280 | });
281 | });
282 | });
283 | });
284 |
285 |
286 | describe('audio service', function() {
287 |
288 | it('should create and return html5 audio element', inject(function(audio) {
289 | expect(audio.nodeName).toBe('AUDIO');
290 | }));
291 | });
--------------------------------------------------------------------------------