453de43181c035384d2c7b58696bbcc47338aed9
[portal/sdk.git] /
1 /*
2  * Copyright (c) 2014 DataTorrent, Inc. ALL Rights Reserved.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *   http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16
17 'use strict';
18
19 describe('Directive: dashboard', function () {
20
21   var scope, element, childScope, DashboardState, mockModal, modalOptions, $compile, $q, mockLog;
22
23   // mock UI Sortable
24   beforeEach(function () {
25     angular.module('ui.sortable', []);
26   });
27
28   // load the directive's module
29   beforeEach(module('ui.dashboard', function($provide) {
30     mockModal = {
31       open: function(options) {
32         modalOptions = options;
33       }
34     };
35     mockLog = {
36       info: function() {
37
38       }
39     };
40     $provide.value('$uibModal', mockModal);
41     $provide.value('$log', mockLog);
42   }));
43
44   beforeEach(inject(function (_$compile_, $rootScope, _DashboardState_, _$q_) {
45     // services
46     scope = $rootScope.$new();
47     $compile = _$compile_;
48     DashboardState = _DashboardState_;
49     $q = _$q_;
50
51     // options
52     var widgetDefinitions = [
53       {
54         name: 'wt-one',
55         template: '<div class="wt-one-value">{{2 + 2}}</div>'
56       },
57       {
58         name: 'wt-two',
59         template: '<span class="wt-two-value">{{value}}</span>'
60       }
61     ];
62     var defaultWidgets = _.clone(widgetDefinitions);
63     scope.dashboardOptions = {
64       widgetButtons: true,
65       widgetDefinitions: widgetDefinitions,
66       defaultWidgets: defaultWidgets,
67       sortableOptions: {
68         testProperty: 'foobar'
69       }
70     };
71     scope.value = 10;
72
73     // element setup
74     element = $compile('<div dashboard="dashboardOptions"></div>')(scope);
75     scope.$digest();
76     childScope = element.scope();
77   }));
78
79   it('should have toolbar', function () {
80     var toolbar = element.find('.btn-toolbar');
81     expect(toolbar.length).toEqual(1);
82   });
83
84   it('should have UI.Sortable directive', function () {
85     var widgetArea = element.find('.dashboard-widget-area');
86     expect(widgetArea.attr('ui-sortable')).toBeDefined();
87   });
88
89   it('should render widgets', function () {
90     var widgets = element.find('.widget');
91     expect(widgets.length).toEqual(2);
92   });
93
94   it('should evaluate widget expressions', function () {
95     var divWidget = element.find('.wt-one-value');
96     expect(divWidget.html()).toEqual('4');
97   });
98
99   it('should evaluate scope expressions', function () {
100     var spanWidget = element.find('.wt-two-value');
101     expect(spanWidget.html()).toEqual('10');
102   });
103
104   it('should fill options with defaults', function() {
105     expect(scope.dashboardOptions.stringifyStorage).toEqual(true);
106   });
107
108   it('should not overwrite specified options with defaults', inject(function($compile) {
109     scope.dashboardOptions.stringifyStorage = false;
110     element = $compile('<div dashboard="dashboardOptions"></div>')(scope);
111     $compile(element)(scope);
112     scope.$digest();
113     expect(scope.dashboardOptions.stringifyStorage).toEqual(false);
114   }));
115
116   it('should be able to use a different dashboard template', inject(function($compile, $templateCache) {
117     $templateCache.put(
118       'myCustomTemplate.html',
119         '<div>' +
120         '<div ui-sortable="sortableOptions" ng-model="widgets">' +
121         '<div ng-repeat="widget in widgets" ng-style="widget.style" class="widget-container custom-widget" widget>' +
122         '<h3 class="widget-header">' +
123         '{{widget.title}}' +
124         '<span ng-click="removeWidget(widget);" class="glyphicon glyphicon-remove icon-erase" ng-if="!options.hideWidgetClose"></span>' +
125         '<span ng-click="openWidgetSettings(widget);" class="glyphicon glyphicon-cog icon-settings" ng-if="!options.hideWidgetSettings"></span>' +
126         '</h3>' +
127         '<div class="widget-content"></div>' +
128         '<div class="widget-ew-resizer" ng-mousedown="grabResizer($event)"></div>' +
129         '</div>' +
130         '</div>' +
131         '</div>'
132     );
133     var customElement = $compile('<div dashboard="dashboardOptions" template-url="myCustomTemplate.html"></div>')(scope);
134     scope.$digest();
135     expect(customElement.find('.custom-widget').length).toEqual(2);
136   }));
137
138   it('should set scope.widgets to an empty array if no defaultWidgets are specified', inject(function($compile) {
139     delete scope.dashboardOptions.defaultWidgets;
140     var element2 = $compile('<div dashboard="dashboardOptions"></div>')(scope);
141     scope.$digest();
142     var childScope2 = element2.scope();
143     expect(childScope2.widgets instanceof Array).toEqual(true);
144   }));
145
146   it('should set options.unsavedChangeCount to 0 upon load', function() {
147     expect(scope.dashboardOptions.unsavedChangeCount).toEqual(0);
148   });
149
150   it('should not call saveDashboard on load', inject(function($compile) {
151     spyOn(DashboardState.prototype, 'save');
152     var s = scope.$new();
153     element = $compile('<div dashboard="dashboardOptions"></div>')(s);
154     scope.$digest();
155     expect(DashboardState.prototype.save).not.toHaveBeenCalled();
156   }));
157
158   describe('the sortableOptions', function() {
159
160     it('should exist', function() {
161       expect(typeof childScope.sortableOptions).toEqual('object');
162     });
163
164     it('should be possible to be extendable from the dashboardOptions', function() {
165       expect(childScope.sortableOptions.testProperty).toEqual('foobar');
166     })
167
168     it('should have a stop function that calls $scope.saveDashboard', function() {
169       expect(typeof childScope.sortableOptions.stop).toEqual('function');
170       spyOn(childScope, 'saveDashboard');
171       childScope.sortableOptions.stop();
172       expect(childScope.saveDashboard).toHaveBeenCalled();
173     });
174   });
175
176   describe('the addWidget function', function() {
177
178     var widgetCreated, widgetPassed, widgetDefault;
179
180     beforeEach(function() {
181       childScope.widgets.push = function(w) {
182         widgetCreated = w;
183       }
184     });
185
186     it('should be a function', function() {
187       expect(typeof childScope.addWidget).toEqual('function');
188     });
189
190     it('should throw if no default widgetDefinition was found', function() {
191       spyOn(childScope.widgetDefs, 'getByName').and.returnValue(false);
192       function fn () {
193         childScope.addWidget({ name: 'notReal' });
194       }
195       expect(fn).toThrow();
196     });
197
198     it('should look to the passed widgetToInstantiate object for the title before anything else', function() {
199       spyOn(childScope.widgetDefs, 'getByName').and.returnValue({ title: 'defaultTitle', name: 'A' });
200       childScope.addWidget({ title: 'highestPrecedence', name: 'A' });
201       expect(widgetCreated.title).toEqual('highestPrecedence');
202     });
203
204     it('should use the defaultWidget\'s title second', function() {
205       spyOn(childScope.widgetDefs, 'getByName').and.returnValue({ title: 'defaultTitle', name: 'A' });
206       childScope.addWidget({ name: 'A' });
207       expect(widgetCreated.title).toEqual('defaultTitle');
208     });
209
210     it('should call the saveDashboard method (internal)', function() {
211       spyOn(childScope.widgetDefs, 'getByName').and.returnValue({ title: 'defaultTitle', name: 'A' });
212         spyOn(childScope, 'saveDashboard');
213         childScope.addWidget({ name: 'A' });
214         expect(childScope.saveDashboard).toHaveBeenCalled();
215     });
216
217     it('should support passing just the widget name as a string', function() {
218       spyOn(childScope.widgetDefs, 'getByName').and.returnValue({ title: 'defaultTitle', name: 'A' });
219       childScope.addWidget('A');
220       expect(childScope.widgetDefs.getByName).toHaveBeenCalledWith('A');
221       expect(widgetCreated.title).toEqual('defaultTitle');
222     });
223
224     describe('@awashbrook Test Case', function() {
225       beforeEach(function() {
226         spyOn(childScope.widgetDefs, 'getByName').and.returnValue(widgetDefault = {
227           "name": "nvLineChartAlpha",
228           "directive": "nvd3-line-chart",
229           "dataAttrName": "data",
230           "attrs": {
231             "isArea": true,
232             "height": 400,
233             "showXAxis": true,
234             "showYAxis": true,
235             "xAxisTickFormat": "xAxisTickFormat()",
236             "interactive": true,
237             "useInteractiveGuideline": true,
238             "tooltips": true,
239             "showLegend": true,
240             "noData": "No data for YOU!",
241             "color": "colorFunction()",
242             "forcey": "[0,2]"
243           },
244           "dataModelOptions": {
245             "params": {
246               "from": "-2h",
247               "until": "now"
248             }
249           },
250           "style": {
251             "width": "400px"
252           },
253         });
254         childScope.addWidget(widgetPassed = {
255           "title": "Andy",
256           "name": "nvLineChartAlpha",
257           "style": {
258             "width": "400px"
259           },
260           "dataModelOptions": {
261             "params": {
262               "from": "-1h",
263               "target": [
264               "randomWalk(\"random Andy 1\")",
265               "randomWalk(\"random walk 2\")",
266               "randomWalk(\"random walk 3\")"
267               ]
268             }
269           },
270           "attrs": {
271             "height": 400,
272             "showXAxis": true,
273             "showYAxis": true,
274             "xAxisTickFormat": "xAxisTickFormat()",
275             "interactive": false,
276             "useInteractiveGuideline": true,
277             "tooltips": true,
278             "showLegend": true,
279             "noData": "No data for YOU!",
280             "color": "colorFunction()",
281             "forcey": "[0,2]",
282             "data": "widgetData"
283           }
284         });
285       });
286
287       it('should keep overrides from widgetPassed', function() {
288         expect(widgetCreated.attrs.interactive).toEqual(widgetPassed.attrs.interactive);
289       });
290
291       it('should fill in default attrs', function() {
292         expect(widgetCreated.attrs.isArea).toEqual(widgetDefault.attrs.isArea);
293       });
294
295       it('should override deep options in dataModelOptions', function() {
296         expect(widgetCreated.dataModelOptions.params.from).toEqual(widgetPassed.dataModelOptions.params.from);
297       });
298
299       it('should fill in deep default attrs', function() {
300         expect(widgetCreated.dataModelOptions.params.until).toEqual(widgetDefault.dataModelOptions.params.until);
301       });
302     });
303
304     describe('the doNotSave parameter', function() {
305
306       it('should prevent save from being called if set to true', function() {
307         spyOn(childScope.widgetDefs, 'getByName').and.returnValue({ title: 'defaultTitle', name: 'A' });
308         spyOn(childScope, 'saveDashboard');
309         childScope.addWidget({ name: 'A' }, true);
310         expect(childScope.saveDashboard).not.toHaveBeenCalled();
311       });
312
313     });
314
315   });
316
317   describe('the removeWidget function', function() {
318
319     it('should be a function', function() {
320       expect(typeof childScope.removeWidget).toEqual('function');
321     });
322
323     it('should remove the provided widget from childScope.widgets array', function() {
324       var startingLength = childScope.widgets.length;
325       var expectedLength = startingLength - 1;
326
327       var widgetToRemove = childScope.widgets[0];
328       childScope.removeWidget(widgetToRemove);
329
330       expect(childScope.widgets.length).toEqual(expectedLength);
331       expect(childScope.widgets.indexOf(widgetToRemove)).toEqual(-1);
332     });
333
334     it('should call saveDashboard', function() {
335       spyOn(childScope, 'saveDashboard');
336       var widgetToRemove = childScope.widgets[0];
337       childScope.removeWidget(widgetToRemove);
338       expect(childScope.saveDashboard).toHaveBeenCalled();
339     });
340
341   });
342
343   describe('the saveDashboard function', function() {
344
345     it('should be attached to the options object after initialization', function() {
346       expect(typeof scope.dashboardOptions.saveDashboard).toEqual('function');
347       expect(scope.dashboardOptions.saveDashboard === childScope.externalSaveDashboard).toEqual(true);
348     });
349
350     it('should call scope.dashboardState.save when called internally if explicitSave is falsey', function() {
351       spyOn(childScope.dashboardState, 'save').and.returnValue(true);
352       childScope.saveDashboard();
353       expect(childScope.dashboardState.save).toHaveBeenCalled();
354     });
355
356     it('should not call scope.dashboardState.save when called internally if explicitSave is truthy', function() {
357       scope.dashboardOptions.explicitSave = true;
358       spyOn(childScope.dashboardState, 'save').and.returnValue(true);
359       childScope.saveDashboard();
360       expect(childScope.dashboardState.save).not.toHaveBeenCalled();
361     });
362
363     it('should call scope.dashboardState.save when called externally, no matter what explicitSave value is', function() {
364       spyOn(childScope.dashboardState, 'save').and.returnValue(true);
365
366       scope.dashboardOptions.explicitSave = false;
367       scope.dashboardOptions.saveDashboard();
368       expect(childScope.dashboardState.save.calls.count()).toEqual(1);
369
370       scope.dashboardOptions.explicitSave = true;
371       scope.dashboardOptions.saveDashboard();
372       expect(childScope.dashboardState.save.calls.count()).toEqual(2);
373     });
374
375     it('should keep a count of unsaved changes as unsavedChangeCount', function() {
376       scope.dashboardOptions.explicitSave = true;
377       spyOn(childScope.dashboardState, 'save').and.returnValue(true);
378       childScope.saveDashboard();
379       expect(scope.dashboardOptions.unsavedChangeCount).toEqual(1);
380       childScope.saveDashboard();
381       childScope.saveDashboard();
382       expect(scope.dashboardOptions.unsavedChangeCount).toEqual(3);
383     });
384
385     it('should reset the cound of unsaved changes if a successful force save occurs', function() {
386       scope.dashboardOptions.explicitSave = true;
387       spyOn(childScope.dashboardState, 'save').and.returnValue(true);
388
389       childScope.saveDashboard();
390       childScope.saveDashboard();
391       childScope.saveDashboard();
392
393       childScope.saveDashboard(true);
394
395       expect(scope.dashboardOptions.unsavedChangeCount).toEqual(0);
396     });
397
398   });
399
400   describe('the loadWidgets function', function() {
401
402     it('should be a function', function() {
403       expect(typeof childScope.loadWidgets).toEqual('function');
404     });
405
406     it('should set savedWidgetDefs on scope as passed array', function() {
407       var widgets = [];
408       childScope.loadWidgets(widgets);
409       expect(childScope.savedWidgetDefs === widgets).toEqual(true);
410     });
411
412     it('should call clear on the scope with true as the only argument', function() {
413       spyOn(childScope, 'clear');
414       childScope.loadWidgets([]);
415       expect(childScope.clear).toHaveBeenCalled();
416       expect(childScope.clear.calls.argsFor(0)).toEqual([true]);
417     });
418
419     it('should call addWidget for each widget in the array', function() {
420       spyOn(childScope, 'addWidget').and.returnValue(null);
421       var widgets = [{},{},{}];
422       childScope.loadWidgets(widgets);
423       expect(childScope.addWidget.calls.count()).toEqual(3);
424     });
425
426     it('should call addWidget for each widget with true as the second parameter (doNotSave)', function() {
427       spyOn(childScope, 'addWidget').and.returnValue(null);
428       var widgets = [{},{},{}];
429       childScope.loadWidgets(widgets);
430       expect(childScope.addWidget.calls.argsFor(0)).toEqual( [ widgets[0], true] );
431       expect(childScope.addWidget.calls.argsFor(1)).toEqual( [ widgets[1], true] );
432       expect(childScope.addWidget.calls.argsFor(2)).toEqual( [ widgets[2], true] );
433     });
434
435   });
436
437   describe('the clear function', function() {
438
439     it('should set the scope to an empty array', function() {
440       childScope.clear();
441       expect(childScope.widgets).toEqual([]);
442     });
443
444     it('should not call saveDashboard if first arg is true', function() {
445       spyOn(childScope, 'saveDashboard');
446       childScope.clear(true);
447       expect(childScope.saveDashboard).not.toHaveBeenCalled();
448     });
449
450     it('should call saveDashboard if first arg is not true', function() {
451       spyOn(childScope, 'saveDashboard');
452       childScope.clear();
453       expect(childScope.saveDashboard).toHaveBeenCalled();
454     });
455
456   });
457
458   describe('the openWidgetSettings function', function() {
459
460     it('should be a function', function() {
461       expect(typeof childScope.openWidgetSettings).toEqual('function');
462     });
463
464     it('should call $uibModal.open with default options', function() {
465       var widget = {};
466       spyOn(mockModal, 'open').and.returnValue({
467         result: { then: function(fn) {} }
468       });
469       childScope.openWidgetSettings(widget);
470       expect(mockModal.open).toHaveBeenCalled();
471     });
472
473     it('should have widget in the resolve object', function() {
474       var widget = {};
475       var dfr = $q.defer();
476       spyOn(mockModal, 'open').and.callFake(function(options) {
477         modalOptions = options;
478         return {
479           result: dfr.promise
480         };
481       });
482       childScope.openWidgetSettings(widget);
483       expect(modalOptions.resolve.widget() === widget).toEqual(true);
484     });
485
486     it('should set the templateUrl in modal options to the default ("app/fusion/scripts/view-models/reportdashboard-page/src/components/directives/dashboard/widget-settings-template.html")', function() {
487       var widget = {};
488       var dfr = $q.defer();
489       spyOn(mockModal, 'open').and.callFake(function(options) {
490         modalOptions = options;
491         return {
492           result: dfr.promise
493         };
494       });
495       childScope.openWidgetSettings(widget);
496       expect(modalOptions.templateUrl).toEqual('app/fusion/scripts/view-models/reportdashboard-page/src/components/directives/dashboard/widget-settings-template.html');
497     });
498
499     it('should set the templateUrl in modal options to scope.options.settingsModalOptions.templateUrl', function() {
500       var other;
501       scope.dashboardOptions.settingsModalOptions = {
502         templateUrl: other = 'some/other/url.html'
503       };
504       var widget = {};
505       var dfr = $q.defer();
506       spyOn(mockModal, 'open').and.callFake(function(options) {
507         modalOptions = options;
508         return {
509           result: dfr.promise
510         };
511       });
512       childScope.openWidgetSettings(widget);
513       expect(modalOptions.templateUrl).toEqual(other);
514     });
515
516     it('should set the templateUrl in modal options to widget.settingsModalOptions.templateUrl, if present', function() {
517       var expected;
518       var widget = {
519         settingsModalOptions: {
520           templateUrl: expected = 'specific/template.html'
521         }
522       };
523       var dfr = $q.defer();
524       spyOn(mockModal, 'open').and.callFake(function(options) {
525         modalOptions = options;
526         return {
527           result: dfr.promise
528         };
529       });
530       childScope.openWidgetSettings(widget);
531       expect(modalOptions.templateUrl).toEqual(expected);
532     });
533
534     it('should set the controller in modal options to the default ("WidgetSettingsCtrl")', function() {
535       var widget = {};
536       var dfr = $q.defer();
537       spyOn(mockModal, 'open').and.callFake(function(options) {
538         modalOptions = options;
539         return {
540           result: dfr.promise
541         };
542       });
543       childScope.openWidgetSettings(widget);
544       expect(modalOptions.controller).toEqual('WidgetSettingsCtrl');
545     });
546
547     it('should set the controller in modal options to the default ("WidgetSettingsCtrl"), even when settingsModalOptions is supplied in options', inject(function($rootScope) {
548       
549       scope = $rootScope.$new();
550
551       // options
552       var widgetDefinitions = [
553         {
554           name: 'wt-one',
555           template: '<div class="wt-one-value">{{2 + 2}}</div>'
556         },
557         {
558           name: 'wt-two',
559           template: '<span class="wt-two-value">{{value}}</span>'
560         }
561       ];
562       var defaultWidgets = _.clone(widgetDefinitions);
563       scope.dashboardOptions = {
564         widgetButtons: true,
565         widgetDefinitions: widgetDefinitions,
566         defaultWidgets: defaultWidgets,
567         sortableOptions: {
568           testProperty: 'foobar'
569         },
570         settingsModalOptions: {
571           backdrop: false
572         }
573       };
574       scope.value = 10;
575
576       var dfr = $q.defer();
577       spyOn(mockModal, 'open').and.callFake(function(options) {
578         modalOptions = options;
579         return {
580           result: dfr.promise
581         };
582       });
583
584       // element setup
585       element = $compile('<div dashboard="dashboardOptions"></div>')(scope);
586       scope.$digest();
587       childScope = element.scope();
588
589       childScope.openWidgetSettings({});
590       expect(modalOptions.controller).toEqual('WidgetSettingsCtrl');
591
592     }));
593
594     it('should set the controller in modal options to the default ("WidgetSettingsCtrl"), even when settingsModalOptions is supplied in widget', inject(function($rootScope) {
595       
596       scope = $rootScope.$new();
597
598       // options
599       var widgetDefinitions = [
600         {
601           name: 'wt-one',
602           template: '<div class="wt-one-value">{{2 + 2}}</div>'
603         },
604         {
605           name: 'wt-two',
606           template: '<span class="wt-two-value">{{value}}</span>'
607         }
608       ];
609       var defaultWidgets = _.clone(widgetDefinitions);
610       scope.dashboardOptions = {
611         widgetButtons: true,
612         widgetDefinitions: widgetDefinitions,
613         defaultWidgets: defaultWidgets,
614         sortableOptions: {
615           testProperty: 'foobar'
616         },
617         settingsModalOptions: {
618           backdrop: false
619         }
620       };
621       scope.value = 10;
622
623       var dfr = $q.defer();
624       spyOn(mockModal, 'open').and.callFake(function(options) {
625         modalOptions = options;
626         return {
627           result: dfr.promise
628         };
629       });
630
631       // element setup
632       element = $compile('<div dashboard="dashboardOptions"></div>')(scope);
633       scope.$digest();
634       childScope = element.scope();
635
636       childScope.openWidgetSettings({
637         settingsModalOptions: {
638           templateUrl: 'custom/widget/template.html'
639         }
640       });
641       expect(modalOptions.controller).toEqual('WidgetSettingsCtrl');
642       expect(modalOptions.backdrop).toEqual(false);
643       expect(modalOptions.templateUrl).toEqual('custom/widget/template.html');
644
645     }));
646
647     it('should set the controller to scope.options.settingsModalOptions.controller if provided', function() {
648       scope.dashboardOptions.settingsModalOptions = {};
649       var expected = scope.dashboardOptions.settingsModalOptions.controller = 'MyCustomCtrl';
650       var widget = {};
651       var dfr = $q.defer();
652       spyOn(mockModal, 'open').and.callFake(function(options) {
653         modalOptions = options;
654         return {
655           result: dfr.promise
656         };
657       });
658       childScope.openWidgetSettings(widget);
659       expect(modalOptions.controller).toEqual(expected);
660     });
661
662     it('should set the controller to widget.settingsModalOptions.controller if provided', function() {
663       var expected;
664       var widget = {
665         settingsModalOptions: {
666           controller: expected = 'MyWidgetCtrl'
667         }
668       };
669       var dfr = $q.defer();
670       spyOn(mockModal, 'open').and.callFake(function(options) {
671         modalOptions = options;
672         return {
673           result: dfr.promise
674         };
675       });
676       childScope.openWidgetSettings(widget);
677       expect(modalOptions.controller).toEqual(expected);
678     });
679
680     it('should pass in other modal options set in scope.options.settingsModalOptions', function() {
681       scope.dashboardOptions.settingsModalOptions = {
682         keyboard: false,
683         windowClass: 'my-extra-class'
684       };
685       var widget = {};
686       var dfr = $q.defer();
687       spyOn(mockModal, 'open').and.callFake(function(options) {
688         modalOptions = options;
689         return {
690           result: dfr.promise
691         };
692       });
693       childScope.openWidgetSettings(widget);
694       expect(modalOptions.keyboard).toEqual(false);
695       expect(modalOptions.windowClass).toEqual('my-extra-class');
696     });
697
698     it('should pass in other modal options set in widget.settingsModalOptions', function() {
699       scope.dashboardOptions.settingsModalOptions = {
700         keyboard: false,
701         windowClass: 'my-extra-class'
702       };
703       var widget = {
704         settingsModalOptions: {
705           keyboard: true,
706           size: 'sm'
707         }
708       };
709       var dfr = $q.defer();
710       spyOn(mockModal, 'open').and.callFake(function(options) {
711         modalOptions = options;
712         return {
713           result: dfr.promise
714         };
715       });
716       childScope.openWidgetSettings(widget);
717       expect(modalOptions.keyboard).toEqual(true);
718       expect(modalOptions.size).toEqual('sm');
719       expect(modalOptions.windowClass).toEqual('my-extra-class');
720     });
721
722     it('should emit a "widgetChanged" event on the childScope when the modal promise is called', function(done) {
723       var widget = {};
724       var result = {};
725       var dfr = $q.defer();
726       spyOn(mockModal, 'open').and.callFake(function(options) {
727         modalOptions = options;
728         return {
729           result: dfr.promise
730         };
731       });
732       spyOn(childScope.options, 'onSettingsClose');
733       childScope.openWidgetSettings(widget);
734       childScope.$on('widgetChanged', done);
735       dfr.resolve(result, widget);
736       childScope.$digest();
737     });
738
739     it('should call scope.options.onSettingsClose when the modal promise is resolved by default', function() {
740       var widget = {};
741       var result = {};
742       var dfr = $q.defer();
743       spyOn(mockModal, 'open').and.callFake(function(options) {
744         modalOptions = options;
745         return {
746           result: dfr.promise
747         };
748       });
749       spyOn(childScope.options, 'onSettingsClose');
750       childScope.openWidgetSettings(widget);
751       dfr.resolve(result);
752       childScope.$digest();
753       expect(scope.dashboardOptions.onSettingsClose).toHaveBeenCalledWith(result, widget, childScope);
754     });
755
756     it('should call scope.options.onSettingsDismiss when the modal promise is rejected by default', function() {
757       var widget = {};
758       var result = {};
759       var dfr = $q.defer();
760       spyOn(mockModal, 'open').and.callFake(function(options) {
761         modalOptions = options;
762         return {
763           result: dfr.promise
764         };
765       });
766       spyOn(childScope.options, 'onSettingsDismiss');
767       childScope.openWidgetSettings(widget);
768       dfr.reject('Testing failure');
769       childScope.$digest();
770       expect(scope.dashboardOptions.onSettingsDismiss).toHaveBeenCalledWith('Testing failure', childScope);
771     });
772
773     it('should call widget.onSettingsClose if provided when the modal promise is resolved', function() {
774       var widget = {
775         onSettingsClose: function(result, widget, scope) {
776
777         }
778       };
779       var result = {};
780       var dfr = $q.defer();
781       spyOn(widget, 'onSettingsClose');
782       spyOn(mockModal, 'open').and.callFake(function(options) {
783         modalOptions = options;
784         return {
785           result: dfr.promise
786         };
787       });
788       spyOn(childScope.options, 'onSettingsClose');
789       childScope.openWidgetSettings(widget);
790       dfr.resolve(result);
791       childScope.$digest();
792       expect(scope.dashboardOptions.onSettingsClose).not.toHaveBeenCalled();
793       expect(widget.onSettingsClose).toHaveBeenCalledWith(result, widget, childScope);
794     });
795
796     it('should call widget.onSettingsDismiss if provided when the modal promise is rejected', function() {
797       var widget = {
798         onSettingsDismiss: function(result, widget, scope) {
799
800         }
801       };
802       var result = {};
803       var dfr = $q.defer();
804       spyOn(widget, 'onSettingsDismiss');
805       spyOn(mockModal, 'open').and.callFake(function(options) {
806         modalOptions = options;
807         return {
808           result: dfr.promise
809         };
810       });
811       spyOn(childScope.options, 'onSettingsDismiss');
812       childScope.openWidgetSettings(widget);
813       dfr.reject('Testing failure');
814       childScope.$digest();
815       expect(scope.dashboardOptions.onSettingsDismiss).not.toHaveBeenCalled();
816       expect(widget.onSettingsDismiss).toHaveBeenCalledWith('Testing failure', childScope);
817     });
818
819   });
820
821   describe('the default onSettingsClose callback', function() {
822
823     var onSettingsClose;
824
825     beforeEach(function() {
826       onSettingsClose = childScope.options.onSettingsClose;
827     });
828     
829     it('should exist', function() {
830       expect(typeof onSettingsClose).toEqual('function');
831     });
832
833     it('should deep extend widget with result', function() {
834       var result = {
835         title: 'andy',
836         style: {
837           'float': 'left'
838         }
839       };
840       var widget = {
841         title: 'scott',
842         style: {
843           width: '100px'
844         }
845       };
846       onSettingsClose(result, widget, {});
847       expect(widget).toEqual({
848         title: 'andy',
849         style: {
850           width: '100px',
851           'float': 'left'
852         }
853       });
854     });
855
856   });
857
858   describe('the default onSettingsDismiss callback', function() {
859     
860     var onSettingsDismiss;
861
862     beforeEach(function() {
863       onSettingsDismiss = childScope.options.onSettingsDismiss;
864     });
865
866     it('should exist', function() {
867       expect(typeof onSettingsDismiss).toEqual('function');
868     });
869
870     it('should call $log.info with the reason', function() {
871       spyOn(mockLog, 'info');
872       onSettingsDismiss('dismiss reason');
873       expect(mockLog.info).toHaveBeenCalled();
874     });
875
876   });
877
878 });