<!DOCTYPE html>
<html ng-app="Ratings">

  <head>
    <meta charset="utf-8" />
    <title>AngularJS test</title>
    <link data-require="jasmine" data-semver="1.3.1" rel="stylesheet" href="//cdn.jsdelivr.net/jasmine/1.3.1/jasmine.css" />
    <script data-require="json2" data-semver="0.0.2012100-8" src="//cdnjs.cloudflare.com/ajax/libs/json2/20121008/json2.js"></script>
    <script data-require="jasmine" data-semver="1.3.1" src="//cdn.jsdelivr.net/jasmine/1.3.1/jasmine.js"></script>
    <script data-require="jasmine" data-semver="1.3.1" src="//cdn.jsdelivr.net/jasmine/1.3.1/jasmine-html.js"></script>
    <script data-require="angular.js" data-semver="1.4.0-beta.5" src="http://code.angularjs.org/1.4.0-beta.5/angular.js"></script>
    <script data-require="angular-mocks" data-semver="1.4.0-beta.5" src="http://code.angularjs.org/1.4.0-beta.5/angular-mocks.js"></script>
    <link rel="stylesheet" href="rating.directive.css" />
    <script src="rating.directive.js"></script>
    <script src="rating.directive.spec.js"></script>
    <script src="jasmineBootstrap.js"></script>
    <!-- bootstraps Jasmine -->
  </head>

  <body>
    <div id="HTMLReporter" class="jasmine_reporter"></div>
  </body>

</html>
Angular.js Directive Unit Testing with Custom Matchers in Jasmine.

Updated AngularJS to 1.4.0-beta.5 and Jasmine to 1.3.1
'use strict';

describe('Directive: rater', function () {

  // load the directive's module and view
  beforeEach(module('Ratings'));

  var element, scope, starRating, getStar;

  beforeEach(function () {
    /* Function counts number of filled stars */
    starRating= function (element) {
      var elementLength = element.text().length - 1,
          starCount = 0;
      for (var i = 0; i < elementLength; i++) {
        if (!element.find('li').eq(i).hasClass('filled')) {
          starCount = i;
          break;
        }
      }
      return starCount;
    };

    /*
    getStar = function (element, number) {
      return element.find('li').eq(number - 1);
    };
    */
    
    /* Custom Machers Here */
    this.addMatchers({
      toBeStarCount: function (starCount) {
        this.message = function () {
          return "Expected " + starCount + " stars, but counted " + this.actual + ".";
        };

        return this.actual === starCount;
      }
    });
  });

  describe('default rater', function () {

  beforeEach(inject(function ($rootScope, $compile) {
    scope = $rootScope.$new();
    element = angular.element('<rater></rater>');
    element = $compile(element)(scope);
    scope.$digest();
  }));


  it('should display the element on the page with stars', function () {
    expect(element).toBeDefined();
    expect(element.text()).toContain('★');
  });

  it('should have a max of 5 stars', function () {
    expect(element.text()).toBe('★★★★★');
  });

  it('should show a default element with a 3 out of 5 star rating', function () {
    expect(starRating(element)).toBeStarCount(3);
    //expect(element.find('li').eq(0)).toHaveClass('filled'); // 1st star filled
    //expect(element.find('li').eq(2)).toHaveClass('filled'); // 3rd star filled
    //expect(element.find('li').eq(3)).not.toHaveClass('filled'); // 4th star not filled
  });
  
  });


  describe('rating with attributes', function () {

    beforeEach(inject(function ($rootScope, $compile) {
      scope = $rootScope.$new();
      element = angular.element('<rater icon="X" rating="7" max="10"></rater>');
      element = $compile(element)(scope);
      scope.$digest();
    }));

    it('should change the icons to "X"\'s', function () {
      expect(element.text()).toContain("X");
    })

    it('should set the max to 10', function () {
      expect(element.text().length).toBe(10);
    });

    it('should set the rating to 7', function () {
      expect(starRating(element)).toBeStarCount(7);
    });

  });
});
'use strict';

angular.module('Ratings', [])
    .directive('rater', function () {
      return {
        restrict: 'E',
        scope: {
          icon: '@',
          max: '=?',
          rating: '=?',
          readOnly: '@',
          functionOnSelect: '@'
        },
        controller: function ($scope) {
          $scope.max = $scope.max || 5;
          $scope.rating = $scope.rating || Math.round($scope.max)/2;


          // function-on-select
          $scope.runFunction = function (newRating) {
            alert('Adding rating of '  + newRating + ' to server.');
          };
        },
        template: '<ul class="rating">' +
            '<li ng-repeat="item in items" ng-class="item" ng-click="toggle($index)">' +
            '{{icon}}' +
            '</li>' +
            '</ul>',
        link: function (scope, element, attrs) {

          var update = function () {
            scope.items = [];
            for (var i = 0; i < scope.max; i++) {
              scope.items.push({filled: i < scope.rating});
            }
            scope.icon = scope.icon || '★';
            // any properties here will update on click
          };

          scope.toggle = function (index) {
            if (scope.readOnly && scope.readOnly === 'true') {
              return;
            }
            scope.rating = index + 1;
            if (attrs.functionOnSelect) {
              scope.runFunction(scope.rating);
            }
          };

          scope.$watch('rating', function (oldVal, newVal) {
            if (newVal) { update(); }
          });

        }
      };
    });
/* Taken from this demo: http://www.befundoo.com/university/tutorials/angularjs-directives-tutorial/ */

.rating{
  color: #a9a9a9;
  margin: 0;
  padding: 0;
}

ul.rating {
  display: inline-block;
}

.rating li {
  list-style-type: none;
  display: inline-block;
  padding: 1px;
  text-align: center;
  font-weight: bold;
  cursor: pointer;
}

.rating .filled {
  color: #21568b;
}
module.exports = function(config) {
  'use strict';
  config.set({
 
    // base path, that will be used to resolve files and exclude
    basePath: '',
 
    frameworks: ['jasmine'],
 
    plugins: [
      'karma-jasmine',
      'karma-phantomjs-launcher'
    ],
 
    // list of files / patterns to load in the browser
    files: [
      'bower_components/angular/angular.js',
      'bower_components/angular-mocks/angular-mocks.js',
      'rating.directive.js',
      'rating.directive.spec.js'
    ],
 
 
    // list of files to exclude
    exclude: [],
 
 
    // test results reporter to use
    // possible values: 'dots', 'progress', 'junit'
    reporters: ['progress'],
 
 
    // web server port
    port: 9876,
 
 
    // cli runner port
    runnerPort: 9100,
 
 
    // enable / disable colors in the output (reporters and logs)
    colors: true,
 
 
    // level of logging
    // possible values: LOG_DISABLE || LOG_ERROR || LOG_WARN || LOG_INFO || LOG_DEBUG
    logLevel: config.LOG_INFO,
 
 
    // enable / disable watching file and executing tests whenever any file changes
    autoWatch: true,
 
 
    // Start these browsers, currently available:
    // - Chrome
    // - ChromeCanary
    // - Firefox
    // - Opera
    // - Safari (only Mac)
    // - PhantomJS
    // - IE (only Windows)
    browsers: ['PhantomJS'],
 
 
    // If browser does not capture in given timeout [ms], kill it
    captureTimeout: 60000,
 
 
    // Continuous Integration mode
    // if true, it capture browsers, run tests and exit
    singleRun: true
 
  });
};
{
    "name": "sampleProvider Test Pattern",
    "version": "1.0.0",
    "dependencies": {
        "angular": "~1.2.0"
    },
    "devDependencies": {
        "angular-mocks": "~1.2.0",
        "angular-scenario": "~1.2.0"
    }
}
{
  "name": "sampleProvider",
  "description": "Shows an example of how you can test your AngularJS providers.",
  "version": "1.0.0",
  "dependencies": {},
  "devDependencies": {
    "karma": "~0.10",
    "karma-jasmine": "~0.1",
    "karma-phantomjs-launcher": "~0.1.1"
  },
  "engines": {
    "node": ">=0.10.0"
  }
}
(function() {
  var jasmineEnv = jasmine.getEnv();
  jasmineEnv.updateInterval = 250;

  /**
   Create the `HTMLReporter`, which Jasmine calls to provide results of each spec and each suite. The Reporter is responsible for presenting results to the user.
   */
  var htmlReporter = new jasmine.HtmlReporter();
  jasmineEnv.addReporter(htmlReporter);

  /**
   Delegate filtering of specs to the reporter. Allows for clicking on single suites or specs in the results to only run a subset of the suite.
   */
  jasmineEnv.specFilter = function(spec) {
    return htmlReporter.specFilter(spec);
  };

  /**
   Run all of the tests when the page finishes loading - and make sure to run any previous `onload` handler

   ### Test Results

   Scroll down to see the results of all of these specs.
   */
  var currentWindowOnload = window.onload;
  window.onload = function() {
    if (currentWindowOnload) {
      currentWindowOnload();
    }

    //document.querySelector('.version').innerHTML = jasmineEnv.versionString();
    execJasmine();
  };

  function execJasmine() {
    jasmineEnv.execute();
  }
})();