aboutsummaryrefslogtreecommitdiffstats
path: root/test/servicesSpec.js
blob: b3f6ec1080b261508e4892f6192ae8ab844f3f59 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
generated by cgit v1.2.3 (git 2.25.1) at 2025-12-26 14:16:02 +0000
 


n900'>900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
describe("service", function(){
  var scope, $xhrError, $log, mockServices, $browser, $browserXhr, $xhrBulk, $xhr;

  beforeEach(function(){
    $xhrError = jasmine.createSpy('$xhr.error');
    $log = {};
    scope = createScope({}, angularService, {
      '$xhr.error': $xhrError,
      '$log': $log
    });
    $browser = scope.$service('$browser');
    $browserXhr = $browser.xhr;
    $xhrBulk = scope.$service('$xhr.bulk');
    $xhr = scope.$service('$xhr');
  });

  afterEach(function(){
    dealoc(scope);
  });



  it("should inject $window", function(){
    expect(scope.$service('$window')).toEqual(window);
  });

  describe("$log", function(){
    it('should use console if present', function(){
      var logger = "";
      function log(){ logger+= 'log;'; }
      function warn(){ logger+= 'warn;'; }
      function info(){ logger+= 'info;'; }
      function error(){ logger+= 'error;'; }
      var scope = createScope({}, {$log: $logFactory},
                                  {$exceptionHandler: rethrow,
                                   $window: {console: {log: log,
                                                       warn: warn,
                                                       info: info,
                                                       error: error}}}),
          $log = scope.$service('$log');

      $log.log();
      $log.warn();
      $log.info();
      $log.error();
      expect(logger).toEqual('log;warn;info;error;');
    });

    it('should use console.log() if other not present', function(){
      var logger = "";
      function log(){ logger+= 'log;'; }
      var scope = createScope({}, {$log: $logFactory},
                                  {$window: {console:{log:log}},
                                   $exceptionHandler: rethrow});
      var $log = scope.$service('$log');
      $log.log();
      $log.warn();
      $log.info();
      $log.error();
      expect(logger).toEqual('log;log;log;log;');
    });

    it('should use noop if no console', function(){
      var scope = createScope({}, {$log: $logFactory},
                                  {$window: {},
                                   $exceptionHandler: rethrow}),
          $log = scope.$service('$log');
      $log.log();
      $log.warn();
      $log.info();
      $log.error();
    });

    describe('error', function(){
      var e, $log, errorArgs;
      beforeEach(function(){
        e = new Error('');
        e.message = undefined;
        e.sourceURL = undefined;
        e.line = undefined;
        e.stack = undefined;

        $log = $logFactory({console:{error:function(){
          errorArgs = arguments;
        }}});
      });

      it('should pass error if does not have trace', function(){
        $log.error('abc', e);
        expect(errorArgs).toEqual(['abc', e]);
      });

      it('should print stack', function(){
        e.stack = 'stack';
        $log.error('abc', e);
        expect(errorArgs).toEqual(['abc', 'stack']);
      });

      it('should print line', function(){
        e.message = 'message';
        e.sourceURL = 'sourceURL';
        e.line = '123';
        $log.error('abc', e);
        expect(errorArgs).toEqual(['abc', 'message\nsourceURL:123']);
      });

    });

  });

  describe("$exceptionHandler", function(){
    it('should log errors', function(){
      var scope = createScope({}, {$exceptionHandler: $exceptionHandlerFactory},
                                  {$log: $logMock}),
          $log = scope.$service('$log'),
          $exceptionHandler = scope.$service('$exceptionHandler');

      $exceptionHandler('myError');
      expect($log.error.logs.shift()).toEqual(['myError']);
    });
  });

  describe("$location", function(){
    var $location;

    beforeEach(function() {
      $location = scope.$service('$location');
    });


    it("should update location object immediately when update is called", function() {
      var href = 'http://host:123/p/a/t/h.html?query=value#path?key=value&flag&key2=';
      $location.update(href);
      expect($location.href).toEqual(href);
      expect($location.protocol).toEqual("http");
      expect($location.host).toEqual("host");
      expect($location.port).toEqual("123");
      expect($location.path).toEqual("/p/a/t/h.html");
      expect($location.search).toEqual({query:'value'});
      expect($location.hash).toEqual('path?key=value&flag&key2=');
      expect($location.hashPath).toEqual('path');
      expect($location.hashSearch).toEqual({key: 'value', flag: true, key2: ''});
    });


    it('should update location when browser url changed', function() {
      var origUrl = $location.href;
      expect(origUrl).toEqual($browser.getUrl());

      var newUrl = 'http://somenew/url#foo';
      $browser.setUrl(newUrl);
      $browser.poll();
      expect($location.href).toEqual(newUrl);
    });


    it('should update browser at the end of $eval', function() {
      var origBrowserUrl = $browser.getUrl();
      $location.update('http://www.angularjs.org/');
      $location.update({path: '/a/b'});
      expect($location.href).toEqual('http://www.angularjs.org/a/b');
      expect($browser.getUrl()).toEqual(origBrowserUrl);
      scope.$eval();
      expect($browser.getUrl()).toEqual('http://www.angularjs.org/a/b');
    });


    it('should update hashPath and hashSearch on hash update', function(){
      $location.update('http://server/#path?a=b');
      expect($location.hashPath).toEqual('path');
      expect($location.hashSearch).toEqual({a:'b'});

      $location.update({hash: ''});
      expect($location.hashPath).toEqual('');
      expect($location.hashSearch).toEqual({});
    });


    it('should update hash on hashPath or hashSearch update', function() {
      $location.update('http://server/#path?a=b');
      scope.$eval();
      $location.update({hashPath: '', hashSearch: {}});

      expect($location.hash).toEqual('');
    });


    it('should update hashPath and hashSearch on $location.hash change upon eval', function(){
      $location.update('http://server/#path?a=b');
      scope.$eval();

      $location.hash = '';
      scope.$eval();

      expect($location.href).toEqual('http://server/');
      expect($location.hashPath).toEqual('');
      expect($location.hashSearch).toEqual({});
    });


    it('should update hash on $location.hashPath or $location.hashSearch change upon eval',
        function() {
      $location.update('http://server/#path?a=b');
      scope.$eval();
      $location.hashPath = '';
      $location.hashSearch = {};

      scope.$eval();

      expect($location.href).toEqual('http://server/');
      expect($location.hash).toEqual('');
    });


    it('should sync $location upon eval before watches are fired', function(){
      scope.$location = scope.$service('$location'); //publish to the scope for $watch

      var log = '';
      scope.$watch('$location.hash', function(){
        log += this.$location.hashPath + ';';
      });
      expect(log).toEqual(';');

      log = '';
      scope.$location.hash = '/abc';
      scope.$eval();
      expect(scope.$location.hash).toEqual('/abc');
      expect(log).toEqual('/abc;');
    });


    describe('sync', function() {
      it('should update hash with escaped hashPath', function() {
        $location.hashPath = 'foo=bar';
        scope.$eval();
        expect($location.hash).toBe('foo%3Dbar');
      });


      it('should give $location.href the highest precedence', function() {
        $location.hashPath = 'hashPath';
        $location.hashSearch = {hash:'search'};
        $location.hash = 'hash';
        $location.port = '333';
        $location.host = 'host';
        $location.href = 'https://hrefhost:23/hrefpath';

        scope.$eval();

        expect($location).toEqualData({href: 'https://hrefhost:23/hrefpath',
                                       protocol: 'https',
                                       host: 'hrefhost',
                                       port: '23',
                                       path: '/hrefpath',
                                       search: {},
                                       hash: '',
                                       hashPath: '',
                                       hashSearch: {}
                                      });
      });


      it('should give $location.hash second highest precedence', function() {
        $location.hashPath = 'hashPath';
        $location.hashSearch = {hash:'search'};
        $location.hash = 'hash';
        $location.port = '333';
        $location.host = 'host';
        $location.path = '/path';

        scope.$eval();

        expect($location).toEqualData({href: 'http://host:333/path#hash',
                                       protocol: 'http',
                                       host: 'host',
                                       port: '333',
                                       path: '/path',
                                       search: {},
                                       hash: 'hash',
                                       hashPath: 'hash',
                                       hashSearch: {}
                                      });
      });
    });

    describe('update()', function() {
      it('should accept hash object and update only given properties', function() {
        $location.update("http://host:123/p/a/t/h.html?query=value#path?key=value&flag&key2=");
        $location.update({host: 'new', port: 24});

        expect($location.host).toEqual('new');
        expect($location.port).toEqual(24);
        expect($location.protocol).toEqual('http');
        expect($location.href).toEqual("http://new:24/p/a/t/h.html?query=value#path?key=value&flag&key2=");
      });

      it('should remove # if hash is empty', function() {
        $location.update('http://www.angularjs.org/index.php#');
        expect($location.href).toEqual('http://www.angularjs.org/index.php');
      });

      it('should clear hash when updating to hash-less URL', function() {
        $location.update('http://server');
        expect($location.href).toBe('http://server');
        expect($location.hash).toBe('');
      });
    });


    describe('updateHash()', function() {
      it('should accept single string argument to update path', function() {
        $location.updateHash('path');
        expect($location.hash).toEqual('path');
        expect($location.hashPath).toEqual('path');
      });

      it('should reset hashSearch when updating with a single string', function() {
        $location.updateHash({foo:'bar'}); //set some initial state for hashSearch

        $location.updateHash('path');
        expect($location.hashPath).toEqual('path');
        expect($location.hashSearch).toEqual({});
      });

      it('should accept single object argument to update search', function() {
        $location.updateHash({a: 'b'});
        expect($location.hash).toEqual('?a=b');
        expect($location.hashSearch).toEqual({a: 'b'});
      });

      it('should accept path string and search object arguments to update both', function() {
        $location.updateHash('path', {a: 'b'});
        expect($location.hash).toEqual('path?a=b');
        expect($location.hashSearch).toEqual({a: 'b'});
        expect($location.hashPath).toEqual('path');
      });

      it('should update href and hash when updating to empty string', function() {
        $location.updateHash('');
        expect($location.href).toBe('http://server');
        expect($location.hash).toBe('');

        scope.$eval();

        expect($location.href).toBe('http://server');
        expect($location.hash).toBe('');
      });
    });
  });

  describe("$invalidWidgets", function(){
    it("should count number of invalid widgets", function(){
      scope = compile('<input name="price" ng:required ng:validate="number"></input>');
      jqLite(document.body).append(scope.$element);
      scope.$init();
      var $invalidWidgets = scope.$service('$invalidWidgets');
      expect($invalidWidgets.length).toEqual(1);

      scope.price = 123;
      scope.$eval();
      expect($invalidWidgets.length).toEqual(0);

      scope.$element.remove();
      scope.price = 'abc';
      scope.$eval();
      expect($invalidWidgets.length).toEqual(0);

      jqLite(document.body).append(scope.$element);
      scope.price = 'abcd'; //force revalidation, maybe this should be done automatically?
      scope.$eval();
      expect($invalidWidgets.length).toEqual(1);

      jqLite(document.body).html('');
      scope.$eval();
      expect($invalidWidgets.length).toEqual(0);
    });
  });


  describe("$route", function(){
    it('should route and fire change event', function(){
      var log = '',
          $location, $route;

      function BookChapter() {
        this.log = '<init>';
      }
      scope = compile('<div></div>').$init();
      $location = scope.$service('$location');
      $route = scope.$service('$route');
      $route.when('/Book/:book/Chapter/:chapter', {controller: BookChapter, template:'Chapter.html'});
      $route.when('/Blank');
      $route.onChange(function(){
        log += 'onChange();';
      });
      $location.update('http://server#/Book/Moby/Chapter/Intro?p=123');
      scope.$eval();
      expect(log).toEqual('onChange();');
      expect($route.current.params).toEqual({book:'Moby', chapter:'Intro', p:'123'});
      expect($route.current.scope.log).toEqual('<init>');
      var lastId = $route.current.scope.$id;

      log = '';
      $location.update('http://server#/Blank?ignore');
      scope.$eval();
      expect(log).toEqual('onChange();');
      expect($route.current.params).toEqual({ignore:true});
      expect($route.current.scope.$id).not.toEqual(lastId);

      log = '';
      $location.update('http://server#/NONE');
      scope.$eval();
      expect(log).toEqual('onChange();');
      expect($route.current).toEqual(null);

      $route.when('/NONE', {template:'instant update'});
      scope.$eval();
      expect($route.current.template).toEqual('instant update');
    });

    it('should return fn registered with onChange()', function() {
      var scope = angular.scope(),
          $route = scope.$service('$route'),
          fn = function() {};

      expect($route.onChange(fn)).toBe(fn);
    });

    it('should allow routes to be defined with just templates without controllers', function() {
      var scope = angular.scope(),
          $location = scope.$service('$location'),
          $route = scope.$service('$route'),
          onChangeSpy = jasmine.createSpy('onChange');

      $route.when('/foo', {template: 'foo.html'});
      $route.onChange(onChangeSpy);
      expect($route.current).toBeNull();
      expect(onChangeSpy).not.toHaveBeenCalled();

      $location.updateHash('/foo');
      scope.$eval();

      expect($route.current.template).toEqual('foo.html');
      expect($route.current.controller).toBeUndefined();
      expect(onChangeSpy).toHaveBeenCalled();
    });

    it('should handle unknown routes with "otherwise" route definition', function() {
      var scope = angular.scope(),
          $location = scope.$service('$location'),
          $route = scope.$service('$route'),
          onChangeSpy = jasmine.createSpy('onChange');

      function NotFoundCtrl() {this.notFoundProp = 'not found!'}

      $route.when('/foo', {template: 'foo.html'});
      $route.otherwise({template: '404.html', controller: NotFoundCtrl});
      $route.onChange(onChangeSpy);
      expect($route.current).toBeNull();
      expect(onChangeSpy).not.toHaveBeenCalled();

      $location.updateHash('/unknownRoute');
      scope.$eval();

      expect($route.current.template).toBe('404.html');
      expect($route.current.controller).toBe(NotFoundCtrl);
      expect($route.current.scope.notFoundProp).toBe('not found!');
      expect(onChangeSpy).toHaveBeenCalled();

      onChangeSpy.reset();
      $location.updateHash('/foo');
      scope.$eval();

      expect($route.current.template).toEqual('foo.html');
      expect($route.current.controller).toBeUndefined();
      expect($route.current.scope.notFoundProp).toBeUndefined();
      expect(onChangeSpy).toHaveBeenCalled();
    });

    it('should support redirection via redirectTo property by updating $location', function() {
      var scope = angular.scope(),
          $location = scope.$service('$location'),
          $browser = scope.$service('$browser'),
          $route = scope.$service('$route'),
          onChangeSpy = jasmine.createSpy('onChange');

      $route.when('', {redirectTo: '/foo'});
      $route.when('/foo', {template: 'foo.html'});
      $route.when('/bar', {template: 'bar.html'});
      $route.when('/baz', {redirectTo: '/bar'});
      $route.otherwise({template: '404.html'});
      $route.onChange(onChangeSpy);
      expect($route.current).toBeNull();
      expect(onChangeSpy).not.toHaveBeenCalled();

      scope.$eval(); //triggers initial route change - match the redirect route
      $browser.defer.flush(); //triger route change - match the route we redirected to

      expect($location.hash).toBe('/foo');
      expect($route.current.template).toBe('foo.html');
      expect(onChangeSpy.callCount).toBe(1);

      onChangeSpy.reset();
      $location.updateHash('');
      scope.$eval(); //match the redirect route + update $browser
      $browser.defer.flush(); //match the route we redirected to

      expect($location.hash).toBe('/foo');
      expect($route.current.template).toBe('foo.html');
      expect(onChangeSpy.callCount).toBe(1);

      onChangeSpy.reset();
      $location.updateHash('/baz');
      scope.$eval(); //match the redirect route + update $browser
      $browser.defer.flush(); //match the route we redirected to

      expect($location.hash).toBe('/bar');
      expect($route.current.template).toBe('bar.html');
      expect(onChangeSpy.callCount).toBe(1);
    });

    it('should make parentScope configurable via parent()', function() {
      var scope = angular.scope(),
          parentScope = scope.$new(),
          $location = scope.$service('$location'),
          $route = scope.$service('$route');

      $route.parent(parentScope);
      $route.when('/foo', {template: 'foo.html'});
      $route.otherwise({template: '404.html'});

      scope.$eval();

      expect($route.current.template).toBe('404.html');
      expect($route.current.scope.$parent).toBe(parentScope);

      $location.updateHash('/foo');
      scope.$eval();

      expect($route.current.template).toBe('foo.html');
      expect($route.current.scope.$parent).toBe(parentScope);
    });

    it('should reload routes when reload() is called', function() {
      var scope = angular.scope(),
          $location = scope.$service('$location'),
          $route = scope.$service('$route'),
          onChangeSpy = jasmine.createSpy('onChange');

      $route.when('', {template: 'foo.html'});
      $route.onChange(onChangeSpy);
      expect($route.current).toBeNull();
      expect(onChangeSpy).not.toHaveBeenCalled();

      scope.$eval();

      expect($location.hash).toBe('');
      expect($route.current.template).toBe('foo.html');
      expect(onChangeSpy.callCount).toBe(1);

      $route.reload();
      scope.$eval();

      expect($location.hash).toBe('');
      expect($route.current.template).toBe('foo.html');
      expect(onChangeSpy.callCount).toBe(2);
    });
  });


  describe('$defer', function() {
    var $defer, $exceptionHandler;

    beforeEach(function(){
      scope = createScope({}, angularService, {
        '$exceptionHandler': jasmine.createSpy('$exceptionHandler')
      });

      $browser = scope.$service('$browser');
      $defer = scope.$service('$defer');
      $exceptionHandler = scope.$service('$exceptionHandler');
    });


    it('should delegate functions to $browser.defer', function() {
      var counter = 0;
      $defer(function() { counter++; });

      expect(counter).toBe(0);

      $browser.defer.flush();
      expect(counter).toBe(1);

      $browser.defer.flush(); //does nothing
      expect(counter).toBe(1);

      expect($exceptionHandler).not.toHaveBeenCalled();
    });


    it('should delegate exception to the $exceptionHandler service', function() {
      $defer(function() {throw "Test Error";});
      expect($exceptionHandler).not.toHaveBeenCalled();

      $browser.defer.flush();
      expect($exceptionHandler).toHaveBeenCalledWith("Test Error");
    });


    it('should call eval after each callback is executed', function() {
      var eval = this.spyOn(scope, '$eval').andCallThrough();

      $defer(function() {});
      expect(eval).wasNotCalled();

      $browser.defer.flush();
      expect(eval).wasCalled();

      eval.reset(); //reset the spy;

      $defer(function() {});
      $defer(function() {});
      $browser.defer.flush();
      expect(eval.callCount).toBe(2);
    });


    it('should call eval even if an exception is thrown in callback', function() {
      var eval = this.spyOn(scope, '$eval').andCallThrough();

      $defer(function() {throw "Test Error";});
      expect(eval).wasNotCalled();

      $browser.defer.flush();
      expect(eval).wasCalled();
    });
  });


  describe('$xhr', function(){
    var log;
    function callback(code, response) {
      expect(code).toEqual(200);
      log = log + toJson(response) + ';';
    }

    beforeEach(function(){
      log = '';
    });

    it('should forward the request to $browser and decode JSON', function(){
      $browserXhr.expectGET('/reqGET').respond('first');
      $browserXhr.expectGET('/reqGETjson').respond('["second"]');
      $browserXhr.expectPOST('/reqPOST', {post:'data'}).respond('third');

      $xhr('GET', '/reqGET', null, callback);
      $xhr('GET', '/reqGETjson', null, callback);
      $xhr('POST', '/reqPOST', {post:'data'}, callback);

      $browserXhr.flush();

      expect(log).toEqual('"third";["second"];"first";');
    });

    it('should handle non 200 status codes by forwarding to error handler', function(){
      $browserXhr.expectPOST('/req', 'MyData').respond(500, 'MyError');
      $xhr('POST', '/req', 'MyData', callback);
      $browserXhr.flush();
      var cb = $xhrError.mostRecentCall.args[0].callback;
      expect(typeof cb).toEqual($function);
      expect($xhrError).wasCalledWith(
          {url:'/req', method:'POST', data:'MyData', callback:cb},
          {status:500, body:'MyError'});
    });

    it('should handle exceptions in callback', function(){
      $log.error = jasmine.createSpy('$log.error');
      $browserXhr.expectGET('/reqGET').respond('first');
      $xhr('GET', '/reqGET', null, function(){ throw "MyException"; });
      $browserXhr.flush();

      expect($log.error).wasCalledWith("MyException");
    });

    describe('bulk', function(){
      it('should collect requests', function(){
        $xhrBulk.urls["/"] = {match:/.*/};
        $xhrBulk('GET', '/req1', null, callback);
        $xhrBulk('POST', '/req2', {post:'data'}, callback);

        $browserXhr.expectPOST('/', {
          requests:[{method:'GET',  url:'/req1', data: null},
                    {method:'POST', url:'/req2', data:{post:'data'} }]
        }).respond([
          {status:200, response:'first'},
          {status:200, response:'second'}
        ]);
        $xhrBulk.flush(function(){ log += 'DONE';});
        $browserXhr.flush();
        expect(log).toEqual('"first";"second";DONE');
      });

      it('should handle non 200 status code by forwarding to error handler', function(){
        $xhrBulk.urls['/'] = {match:/.*/};
        $xhrBulk('GET', '/req1', null, callback);
        $xhrBulk('POST', '/req2', {post:'data'}, callback);

        $browserXhr.expectPOST('/', {
          requests:[{method:'GET',  url:'/req1', data: null},
                    {method:'POST', url:'/req2', data:{post:'data'} }]
        }).respond([
          {status:404, response:'NotFound'},
          {status:200, response:'second'}
        ]);
        $xhrBulk.flush(function(){ log += 'DONE';});
        $browserXhr.flush();

        expect($xhrError).wasCalled();
        var cb = $xhrError.mostRecentCall.args[0].callback;
        expect(typeof cb).toEqual($function);
        expect($xhrError).wasCalledWith(
            {url:'/req1', method:'GET', data:null, callback:cb},
            {status:404, response:'NotFound'});

        expect(log).toEqual('"second";DONE');
      });
    });

    describe('cache', function(){
      var cache;
      beforeEach(function(){ cache = scope.$service('$xhr.cache'); });

      it('should cache requests', function(){
        $browserXhr.expectGET('/url').respond('first');
        cache('GET', '/url', null, callback);
        $browserXhr.flush();

        $browserXhr.expectGET('/url').respond('ERROR');
        cache('GET', '/url', null, callback);
        $browser.defer.flush();
        expect(log).toEqual('"first";"first";');

        cache('GET', '/url', null, callback, false);
        $browser.defer.flush();
        expect(log).toEqual('"first";"first";"first";');
      });

      it('should first return cache request, then return server request', function(){
        $browserXhr.expectGET('/url').respond('first');
        cache('GET', '/url', null, callback, true);
        $browserXhr.flush();

        $browserXhr.expectGET('/url').respond('ERROR');
        cache('GET', '/url', null, callback, true);
        $browser.defer.flush();
        expect(log).toEqual('"first";"first";');

        $browserXhr.flush();
        expect(log).toEqual('"first";"first";"ERROR";');
      });

      it('should serve requests from cache', function(){
        cache.data.url = {value:'123'};
        cache('GET', 'url', null, callback);
        $browser.defer.flush();
        expect(log).toEqual('"123";');

        cache('GET', 'url', null, callback, false);
        $browser.defer.flush();
        expect(log).toEqual('"123";"123";');
      });

      it('should keep track of in flight requests and request only once', function(){
        scope.$service('$xhr.bulk').urls['/bulk'] = {
          match:function(url){
            return url == '/url';
          }
        };
        $browserXhr.expectPOST('/bulk', {
          requests:[{method:'GET',  url:'/url', data: null}]
        }).respond([
          {status:200, response:'123'}
        ]);
        cache('GET', '/url', null, callback);
        cache('GET', '/url', null, callback);
        cache.delegate.flush();
        $browserXhr.flush();
        expect(log).toEqual('"123";"123";');
      });

      it('should clear cache on non GET', function(){
        $browserXhr.expectPOST('abc', {}).respond({});
        cache.data.url = {value:123};
        cache('POST', 'abc', {});
        expect(cache.data.url).toBeUndefined();
      });

      it('should call callback asynchronously for both cache hit and cache miss', function() {
        $browserXhr.expectGET('/url').respond('+');
        cache('GET', '/url', null, callback);
        expect(log).toEqual(''); //callback hasn't executed

        $browserXhr.flush();
        expect(log).toEqual('"+";'); //callback has executed

        cache('GET', '/url', null, callback);
        expect(log).toEqual('"+";'); //callback hasn't executed

        $browser.defer.flush();
        expect(log).toEqual('"+";"+";'); //callback has executed
      });

      it('should call eval after callbacks for both cache hit and cache miss execute', function() {
        var eval = this.spyOn(scope, '$eval').andCallThrough();

        $browserXhr.expectGET('/url').respond('+');
        cache('GET', '/url', null, callback);
        expect(eval).wasNotCalled();

        $browserXhr.flush();
        expect(eval).wasCalled();

        eval.reset(); //reset the spy

        cache('GET', '/url', null, callback);
        expect(eval).wasNotCalled();

        $browser.defer.flush();
        expect(eval).wasCalled();
      });
    });

  });


  describe('$cookies', function() {

    var scope, $browser;

    beforeEach(function() {
      $browser = new MockBrowser();
      $browser.cookieHash['preexisting'] = 'oldCookie';
      scope = createScope(null, angularService, {$browser: $browser});
      scope.$cookies = scope.$service('$cookies');
    });


    it('should provide access to existing cookies via object properties and keep them in sync',
        function(){
      expect(scope.$cookies).toEqual({'preexisting': 'oldCookie'});

      // access internal cookie storage of the browser mock directly to simulate behavior of
      // document.cookie
      $browser.cookieHash['brandNew'] = 'cookie';
      $browser.poll();

      expect(scope.$cookies).toEqual({'preexisting': 'oldCookie', 'brandNew':'cookie'});

      $browser.cookieHash['brandNew'] = 'cookie2';
      $browser.poll();
      expect(scope.$cookies).toEqual({'preexisting': 'oldCookie', 'brandNew':'cookie2'});

      delete $browser.cookieHash['brandNew'];
      $browser.poll();
      expect(scope.$cookies).toEqual({'preexisting': 'oldCookie'});
    });


    it('should create or update a cookie when a value is assigned to a property', function() {
      scope.$cookies.oatmealCookie = 'nom nom';
      scope.$eval();

      expect($browser.cookies()).
        toEqual({'preexisting': 'oldCookie', 'oatmealCookie':'nom nom'});

      scope.$cookies.oatmealCookie = 'gone';
      scope.$eval();

      expect($browser.cookies()).
        toEqual({'preexisting': 'oldCookie', 'oatmealCookie': 'gone'});
    });


    it('should drop or reset any cookie that was set to a non-string value', function() {
      scope.$cookies.nonString = [1, 2, 3];
      scope.$cookies.nullVal = null;
      scope.$cookies.undefVal = undefined;
      scope.$cookies.preexisting = function(){};
      scope.$eval();
      expect($browser.cookies()).toEqual({'preexisting': 'oldCookie'});
      expect(scope.$cookies).toEqual({'preexisting': 'oldCookie'});
    });


    it('should remove a cookie when a $cookies property is deleted', function() {
      scope.$cookies.oatmealCookie = 'nom nom';
      scope.$eval();
      $browser.poll();
      expect($browser.cookies()).
        toEqual({'preexisting': 'oldCookie', 'oatmealCookie':'nom nom'});

      delete scope.$cookies.oatmealCookie;
      scope.$eval();

      expect($browser.cookies()).toEqual({'preexisting': 'oldCookie'});
    });


    it('should drop or reset cookies that browser refused to store', function() {
      var i, longVal;

      for (i=0; i<5000; i++) {
        longVal += '*';
      }

      //drop if no previous value
      scope.$cookies.longCookie = longVal;
      scope.$eval();
      expect(scope.$cookies).toEqual({'preexisting': 'oldCookie'});


      //reset if previous value existed
      scope.$cookies.longCookie = 'shortVal';
      scope.$eval();
      expect(scope.$cookies).toEqual({'preexisting': 'oldCookie', 'longCookie': 'shortVal'});
      scope.$cookies.longCookie = longVal;
      scope.$eval();
      expect(scope.$cookies).toEqual({'preexisting': 'oldCookie', 'longCookie': 'shortVal'});
    });
  });


  describe('$cookieStore', function() {

    it('should serialize objects to json', function() {
      scope.$service('$cookieStore').put('objectCookie', {id: 123, name: 'blah'});
      scope.$eval(); //force eval in test
      expect($browser.cookies()).toEqual({'objectCookie': '{"id":123,"name":"blah"}'});
    });


    it('should deserialize json to object', function() {
      $browser.cookies('objectCookie', '{"id":123,"name":"blah"}');
      $browser.poll();
      expect(scope.$service('$cookieStore').get('objectCookie')).toEqual({id: 123, name: 'blah'});
    });


    it('should delete objects from the store when remove is called', function() {
      scope.$service('$cookieStore').put('gonner', { "I'll":"Be Back"});
      scope.$eval(); //force eval in test
      expect($browser.cookies()).toEqual({'gonner': '{"I\'ll":"Be Back"}'});
    });

  });


  describe('URL_MATCH', function() {

    it('should parse basic url', function() {
      var match = URL_MATCH.exec('http://www.angularjs.org/path?search#hash?x=x');

      expect(match[1]).toEqual('http');
      expect(match[3]).toEqual('www.angularjs.org');
      expect(match[6]).toEqual('/path');
      expect(match[8]).toEqual('search');
      expect(match[10]).toEqual('hash?x=x');
    });

    it('should parse file://', function(){
      var match = URL_MATCH.exec('file:///Users/Shared/misko/work/angular.js/scenario/widgets.html');

      expect(match[1]).toEqual('file');
      expect(match[3]).toEqual('');
      expect(match[5]).toBeFalsy();
      expect(match[6]).toEqual('/Users/Shared/misko/work/angular.js/scenario/widgets.html');
      expect(match[8]).toBeFalsy();
    });

    it('should parse url with "-" in host', function(){
      var match = URL_MATCH.exec('http://a-b1.c-d.09/path');

      expect(match[1]).toEqual('http');
      expect(match[3]).toEqual('a-b1.c-d.09');
      expect(match[5]).toBeFalsy();
      expect(match[6]).toEqual('/path');
      expect(match[8]).toBeFalsy();
    });

    it('should parse host without "/" at the end', function() {
      var match = URL_MATCH.exec('http://host.org');
      expect(match[3]).toEqual('host.org');

      match = URL_MATCH.exec('http://host.org#');
      expect(match[3]).toEqual('host.org');

      match = URL_MATCH.exec('http://host.org?');
      expect(match[3]).toEqual('host.org');
    });

    it('should match with just "/" path', function() {
      var match = URL_MATCH.exec('http://server/#?book=moby');

      expect(match[10]).toEqual('?book=moby');
    });
  });

  describe('$updateView', function(){
    var scope, browser, evalCount, $updateView;

    beforeEach(function(){
      browser = new MockBrowser();
      // Pretend that you are real Browser so that we see the delays
      browser.isMock = false;
      browser.defer = jasmine.createSpy('defer');

      scope = angular.scope(null, null, {$browser:browser});
      $updateView = scope.$service('$updateView');
      scope.$onEval(function(){ evalCount++; });
      evalCount = 0;
    });

    it('should eval root scope after a delay', function(){
      $updateView();
      expect(evalCount).toEqual(0);
      expect(browser.defer).toHaveBeenCalled();
      expect(browser.defer.mostRecentCall.args[1]).toEqual(25);
      browser.defer.mostRecentCall.args[0]();
      expect(evalCount).toEqual(1);
    });

    it('should allow changing of delay time', function(){
      var oldValue = angular.service('$updateView').delay;
      angular.service('$updateView').delay = 50;
      $updateView();
      expect(evalCount).toEqual(0);
      expect(browser.defer).toHaveBeenCalled();
      expect(browser.defer.mostRecentCall.args[1]).toEqual(50);
      angular.service('$updateView').delay = oldValue;
    });

    it('should ignore multiple requests for update', function(){
      $updateView();
      $updateView();
      expect(evalCount).toEqual(0);
      expect(browser.defer).toHaveBeenCalled();
      expect(browser.defer.callCount).toEqual(1);
      browser.defer.mostRecentCall.args[0]();
      expect(evalCount).toEqual(1);
    });

    it('should update immediatelly in test/mock mode', function(){
      scope = angular.scope();
      scope.$onEval(function(){ evalCount++; });
      expect(evalCount).toEqual(0);
      scope.$service('$updateView')();
      expect(evalCount).toEqual(1);
    });
  });
});