Merge "Remove use of scope from action service"

This commit is contained in:
Jenkins 2016-11-29 00:50:31 +00:00 committed by Gerrit Code Review
commit 2394397bfd
14 changed files with 137 additions and 166 deletions

View File

@ -145,8 +145,7 @@
* @description * @description
* Performs initialization of all actions for the given type. * Performs initialization of all actions for the given type.
* *
* This requires the proper scope be passed. If an action does not * If an action does not have an initAction() function, it is ignored.
* have an initScope() function, it is ignored.
*/ */
function initActions(scope) { function initActions(scope) {
angular.forEach(self.itemActions, setActionScope); angular.forEach(self.itemActions, setActionScope);
@ -154,7 +153,15 @@
angular.forEach(self.globalActions, setActionScope); angular.forEach(self.globalActions, setActionScope);
function setActionScope(action) { function setActionScope(action) {
if (action.service.initScope) { if (action.service.initAction) {
action.service.initAction();
} else if (action.service.initScope) {
// The use of scope in action services breaks the singleton nature
// of the services, and should be stopped. State should be held on
// controllers instead; scope is now passed into allow() and perform()
// methods.
$log.warn('The initScope() method is deprecated. ' +
'Invocation of it will stop in Queens.');
action.service.initScope(scope.$new()); action.service.initScope(scope.$new());
} }
} }

View File

@ -169,7 +169,21 @@
type = service.getResourceType('something'); type = service.getResourceType('something');
}); });
it('initActions calls initScope on item and batch actions', function () { it('initActions calls initAction on item and batch actions', function () {
var action = {service: {initAction: angular.noop, initScope: angular.noop}};
spyOn(action.service, 'initAction');
spyOn(action.service, 'initScope');
type.batchActions.push(action);
type.initActions({
'$new': function () {
return 4;
}
});
expect(action.service.initAction).toHaveBeenCalled();
expect(action.service.initScope).not.toHaveBeenCalled();
});
it('initActions calls initScope if initAction is not defined', function () {
var action = {service: {initScope: angular.noop}}; var action = {service: {initScope: angular.noop}};
spyOn(action.service, 'initScope'); spyOn(action.service, 'initScope');
type.batchActions.push(action); type.batchActions.push(action);
@ -181,7 +195,7 @@
expect(action.service.initScope).toHaveBeenCalledWith(4); expect(action.service.initScope).toHaveBeenCalledWith(4);
}); });
it('initActions ignores initScope when not present', function () { it('initActions ignores initAction and initScope when not present', function () {
var action = {service: {}}; var action = {service: {}};
type.batchActions.push(action); type.batchActions.push(action);
var returned = type.initActions({}); var returned = type.initActions({});

View File

@ -19,7 +19,7 @@
.module('horizon.framework.widgets.action-list') .module('horizon.framework.widgets.action-list')
.controller('horizon.framework.widgets.action-list.ActionsController', ActionsController); .controller('horizon.framework.widgets.action-list.ActionsController', ActionsController);
ActionsController.$inject = ['$q']; ActionsController.$inject = ['$q', '$scope'];
/** /**
* @ngdoc controller * @ngdoc controller
@ -30,7 +30,7 @@
* functions and variables within this controller. * functions and variables within this controller.
* *
*/ */
function ActionsController($q) { function ActionsController($q, $scope) {
var ctrl = this; var ctrl = this;
ctrl.disabled = false; ctrl.disabled = false;
ctrl.passThroughCallbacks = {}; ctrl.passThroughCallbacks = {};
@ -93,7 +93,7 @@
ctrl.passThroughCallbacks[dynCallbackName] = function genPassThroughCallback(item) { ctrl.passThroughCallbacks[dynCallbackName] = function genPassThroughCallback(item) {
if (ctrl.disabled) { return undefined; } if (ctrl.disabled) { return undefined; }
preAction(); preAction();
var result = service.perform(item); var result = service.perform(item, $scope.$new());
$q.when(result).then(postAction, postAction); $q.when(result).then(postAction, postAction);
return resultHandler ? resultHandler(result) : result; return resultHandler ? resultHandler(result) : result;
}; };

View File

@ -95,25 +95,35 @@
* A title and description must be provided for the 'detail' type. These are used as * A title and description must be provided for the 'detail' type. These are used as
* the title and description to display in the bootstrap panel. * the title and description to display in the bootstrap panel.
* *
* service: is the service expected to have two functions * service: is the service expected to have two functions defined. Both of these
* 1. allowed: is expected to return a promise that resolves * functions take two arguments: the relevant item or items (if any) and the angular
* scope in which the action button appears.
*
* 1. allowed(item[s], scope): is expected to return a promise that resolves
* if the action is permitted and is rejected if not. If there are multiple promises that * if the action is permitted and is rejected if not. If there are multiple promises that
* need to be resolved, you can $q.all to combine multiple promises into a single promise. * need to be resolved, you can $q.all to combine multiple promises into a single promise.
* When using 'row' or 'detail' type, the current 'item' will be passed to the function. *
* When using 'batch' type, no arguments are provided. * - When using 'row' or 'detail' type, the current 'item' will be passed to the function.
* 2. perform: is what gets called when the button is clicked. Also expected to return a * - When using 'batch' type, the item argument will be undefined.
* promise that resolves when the action completes. *
* When using 'row' or 'detail' type, the current 'item' is evaluated and passed to the * 2. perform(item[s], scope): is what gets called when the button is clicked. It is also
* function. * expected to return a promise that resolves when the action completes.
* When using 'batch' type, 'item' is not passed. *
* When using 'delete-selected' for 'batch' type, all selected rows are passed. * - When using 'row' or 'detail' type, the current 'item' is evaluated and passed
* 3. initScope: actions may perform post-config (in the angular sense) initialization by * to the function.
* providing an initScope method. This might be typically invoked by initActions() * - When using 'batch' type, 'item' will be undefined.
* on a ResourceType. Actions should not perform blocking operations in their * - When using 'delete-selected' for 'batch' type, all selected rows are passed as
* construction, for example API calls, because as injectables their constructor * an array.
* is run during injection, meaning those calls would be executed as the module *
* is initialized. This would mean those calls would be blocking on any * There is a third optional function, initAction (which was previously called initScope)
* Angular context initialization, such as going to the login page. * Actions may perform post-config (in the angular sense) initialization by
* providing an initAction method. This might be typically invoked by initActions()
* on a ResourceType. Actions should not perform blocking operations in their
* construction, for example API calls, because as injectables their constructor
* is run during injection, meaning those calls would be executed as the module
* is initialized. This would mean those calls would be blocking on any
* Angular context initialization, such as going to the login page.
*
* *
* @restrict E * @restrict E
* @scope * @scope
@ -126,9 +136,13 @@
* *
* var batchDeleteService = { * var batchDeleteService = {
* allowed: function() { * allowed: function() {
* // This function ignores the "items" and scope arguments because
* // they're not needed for this action.
* return policy.ifAllowed({ rules: [['image', 'delete_image']] }); * return policy.ifAllowed({ rules: [['image', 'delete_image']] });
* }, * },
* perform: function(images) { * perform: function(images) {
* // This function ignores the second scope argument because there's
* // no context information interesting to this action.
* return $q.all(images.map(function(image){ * return $q.all(images.map(function(image){
* return glanceAPI.deleteImage(image.id); * return glanceAPI.deleteImage(image.id);
* })); * }));
@ -141,16 +155,20 @@
* is executed. * is executed.
* *
* var createService = { * var createService = {
* allowed: function(image) { * allowed: function(ignored, scope) {
* return $q.all( * // This function may use the information in the scope to check whether
* isActive(image), * // an image may be created in a certain context.
* imageServiceEnabledPromise * // We ignore the undefined "item" value passed in because as a "batch"
* ); * // action undefined will be passed in.
* return imageServiceEnabledPromise;
* }, * },
* perform: function() { * perform: function(ignored, scope) {
* //open the modal to create volume and return the modal's result promise * // Open the modal to create volume and return the modal's result promise.
* // This function may use the information in the scope to create the
* // image in a certain context. Again, we ignore the undefined "item"
* // value passed in.
* }, * },
* initScope: function() { * initAction: function() {
* imageServiceEnabledPromise = serviceCatalog.ifTypeEnabled('image'); * imageServiceEnabledPromise = serviceCatalog.ifTypeEnabled('image');
* } * }
* }; * };

View File

@ -47,10 +47,11 @@
.factory('horizon.framework.widgets.modal.wizard-modal.service', WizardModalService); .factory('horizon.framework.widgets.modal.wizard-modal.service', WizardModalService);
WizardModalService.$inject = [ WizardModalService.$inject = [
'$log',
'$modal' '$modal'
]; ];
function WizardModalService($modal) { function WizardModalService($log, $modal) {
var service = { var service = {
modal: modal modal: modal
}; };
@ -60,11 +61,10 @@
//////////////////// ////////////////////
function modal(params) { function modal(params) {
if (params && params.scope && params.workflow && params.submit) { if (params && params.workflow && params.submit) {
var options = { var options = {
size: 'lg', size: 'lg',
controller: 'WizardModalController as modalCtrl', controller: 'WizardModalController as modalCtrl',
scope: params.scope,
template: '<wizard></wizard>', template: '<wizard></wizard>',
backdrop: 'static', backdrop: 'static',
windowClass: 'modal-dialog-wizard', windowClass: 'modal-dialog-wizard',
@ -74,10 +74,20 @@
}, },
submit: function() { submit: function() {
return params.submit; return params.submit;
},
data: function() {
return params.data;
} }
} }
}; };
// backwards compatibility
if (angular.isDefined(params.scope)) {
$log.warn('The "scope" param to modal() is deprecated.' +
'Handling of it will stop in Queens.');
options.scope = params.scope;
}
return $modal.open(options); return $modal.open(options);
} }
} }

View File

@ -24,8 +24,9 @@
WizardModalController.$inject = [ WizardModalController.$inject = [
'$modalInstance', '$modalInstance',
'$scope', '$scope',
'workflow', // modal injected 'workflow', // WizardModalService injected
'submit' // modal injected 'submit', // WizardModalService injected
'data' // WizardModalService injected
]; ];
/** /**
@ -38,13 +39,18 @@
* This controller sets the modal actions and workflow on the given scope * This controller sets the modal actions and workflow on the given scope
* as the Wizard needs them defined on the scope. * as the Wizard needs them defined on the scope.
*/ */
function WizardModalController($modalInstance, $scope, workflow, submit) { function WizardModalController($modalInstance, $scope, workflow, submit, data) {
/* eslint-disable angular/controller-as */ /* eslint-disable angular/controller-as */
$scope.close = close; $scope.close = close;
$scope.cancel = cancel; $scope.cancel = cancel;
$scope.submit = submit; $scope.submit = submit;
$scope.workflow = workflow; $scope.workflow = workflow;
// copy over the data (we copy directly for backwards compatibility of access
// since these properties used to be assigned directly on the scope)
angular.forEach(data, function copy(value, key) {
$scope[key] = value;
});
/* eslint-enable angular/controller-as */ /* eslint-enable angular/controller-as */
function close(args) { function close(args) {

View File

@ -34,13 +34,14 @@
$modalInstance: modalInstance, $modalInstance: modalInstance,
$scope: scope, $scope: scope,
workflow: { steps: 'somestep' }, workflow: { steps: 'somestep' },
submit: { api: 'someapi' } submit: { api: 'someapi' },
data: { spam: 'ham' }
}); });
})); }));
////////// //////////
it('should inject and assign workflow and submit', injectAssign); it('should inject and assign workflow, submit and data', injectAssign);
it('should forward call to modalInstance on close', closeModal); it('should forward call to modalInstance on close', closeModal);
it('should forward call to modalInstance on cancel', cancelModal); it('should forward call to modalInstance on cancel', cancelModal);
@ -49,6 +50,7 @@
function injectAssign() { function injectAssign() {
expect(scope.workflow.steps).toEqual('somestep'); expect(scope.workflow.steps).toEqual('somestep');
expect(scope.submit.api).toEqual('someapi'); expect(scope.submit.api).toEqual('someapi');
expect(scope.spam).toEqual('ham');
} }
function closeModal() { function closeModal() {

View File

@ -50,6 +50,9 @@
var steps = $scope.steps = $scope.workflow.steps || []; var steps = $scope.steps = $scope.workflow.steps || [];
$scope.wizardForm = {}; $scope.wizardForm = {};
// a place to keep each step's captured data, named for their step.formName
$scope.stepModels = {};
$scope.switchTo = switchTo; $scope.switchTo = switchTo;
$scope.showError = showError; $scope.showError = showError;
/*eslint-enable angular/controller-as */ /*eslint-enable angular/controller-as */
@ -111,7 +114,7 @@
// prevent the finish button from being clicked again // prevent the finish button from being clicked again
viewModel.isSubmitting = true; viewModel.isSubmitting = true;
beforeSubmit(); beforeSubmit();
$scope.submit().then(afterSubmit, showError); $scope.submit($scope.stepModels).then(afterSubmit, showError);
} }
function onInitSuccess() { function onInitSuccess() {

View File

@ -56,33 +56,23 @@
toast, toast,
volumeResourceType volumeResourceType
) { ) {
var scope, createVolumePromise, volumeServiceEnabledPromise; var createVolumePromise, volumeServiceEnabledPromise;
var volume = {};
var message = { var message = {
success: gettext('Volume %s was successfully created.') success: gettext('Volume %s was successfully created.')
}; };
var service = { var service = {
initScope: initScope, initAction: initAction,
allowed: allowed, allowed: allowed,
perform: perform perform: perform
}; };
return service; return service;
function initScope(newScope) { function initAction() {
scope = newScope;
var watchVolumeChange = scope.$on(events.VOLUME_CHANGED, onChangedVolume);
scope.$on('$destroy', destroy);
createVolumePromise = policy.ifAllowed({rules: [['volume', 'volume:create']]}); createVolumePromise = policy.ifAllowed({rules: [['volume', 'volume:create']]});
volumeServiceEnabledPromise = serviceCatalog.ifTypeEnabled('volume'); volumeServiceEnabledPromise = serviceCatalog.ifTypeEnabled('volume');
function destroy() {
watchVolumeChange();
}
} }
function allowed(image) { function allowed(image) {
@ -95,16 +85,15 @@
} }
function perform(image) { function perform(image) {
scope.image = image;
return wizardModalService.modal({ return wizardModalService.modal({
scope: scope, data: {image: image},
workflow: createVolumeWorkflowService, workflow: createVolumeWorkflowService,
submit: submit submit: submit
}).result; }).result;
} }
function submit() { function submit(stepModels) {
return cinder.createVolume(volume).then(showSuccessMessage); return cinder.createVolume(stepModels.volumeForm).then(showSuccessMessage);
} }
function showSuccessMessage(response) { function showSuccessMessage(response) {
@ -127,12 +116,5 @@
function imageActive(image) { function imageActive(image) {
return $qExtensions.booleanAsPromise(image.status === 'active'); return $qExtensions.booleanAsPromise(image.status === 'active');
} }
//// scope functions ////
function onChangedVolume(e, newVolume) {
volume = newVolume;
e.stopPropagation();
}
} }
})(); })();

View File

@ -18,7 +18,7 @@
describe('horizon.app.core.images.actions.create-volume.service', function() { describe('horizon.app.core.images.actions.create-volume.service', function() {
var service, $scope, events, workflow; var service, $scope, workflow;
var wizardModalService = { var wizardModalService = {
modal: function () { modal: function () {
return { return {
@ -72,7 +72,6 @@
beforeEach(inject(function($injector, _$rootScope_) { beforeEach(inject(function($injector, _$rootScope_) {
$scope = _$rootScope_.$new(); $scope = _$rootScope_.$new();
service = $injector.get('horizon.app.core.images.actions.create-volume.service'); service = $injector.get('horizon.app.core.images.actions.create-volume.service');
events = $injector.get('horizon.app.core.images.events');
workflow = $injector.get('horizon.app.core.images.workflows.create-volume.service'); workflow = $injector.get('horizon.app.core.images.workflows.create-volume.service');
})); }));
@ -81,55 +80,31 @@
spyOn(wizardModalService, 'modal').and.callThrough(); spyOn(wizardModalService, 'modal').and.callThrough();
var image = {id: '12'}; var image = {id: '12'};
service.initScope($scope); service.initAction();
service.perform(image); service.perform(image);
expect(wizardModalService.modal).toHaveBeenCalled(); expect(wizardModalService.modal).toHaveBeenCalled();
var modalArgs = wizardModalService.modal.calls.argsFor(0)[0]; var modalArgs = wizardModalService.modal.calls.argsFor(0)[0];
expect(modalArgs.scope).toEqual($scope); expect(modalArgs.data.image).toEqual(image);
expect(modalArgs.workflow).toEqual(workflow); expect(modalArgs.workflow).toEqual(workflow);
expect(modalArgs.submit).toBeDefined(); expect(modalArgs.submit).toBeDefined();
}); });
it('should create volume in cinder and raise event', function() { it('should create volume in cinder', function() {
var volume = { name: 'Test', id: '2' }; var volume = { name: 'Test', id: '2' };
spyOn(cinderAPI, 'createVolume').and.callThrough(); spyOn(cinderAPI, 'createVolume').and.callThrough();
spyOn(wizardModalService, 'modal').and.callThrough(); spyOn(wizardModalService, 'modal').and.callThrough();
service.initScope($scope); service.initAction();
service.perform(); service.perform();
$scope.$emit(events.VOLUME_CHANGED, volume);
$scope.$apply();
var modalArgs = wizardModalService.modal.calls.argsFor(0)[0]; var modalArgs = wizardModalService.modal.calls.argsFor(0)[0];
modalArgs.submit(); modalArgs.submit({volumeForm: volume});
$scope.$apply(); $scope.$apply();
expect(cinderAPI.createVolume).toHaveBeenCalledWith(volume); expect(cinderAPI.createVolume).toHaveBeenCalledWith(volume);
}); });
it('should destroy volume change watcher on exit', function() {
spyOn(cinderAPI, 'createVolume').and.callThrough();
spyOn(wizardModalService, 'modal').and.callThrough();
service.initScope($scope);
service.perform();
var oldVolume = {id: 1};
$scope.$emit(events.VOLUME_CHANGED, oldVolume);
$scope.$emit('$destroy');
var newVolume = {id: 2};
$scope.$emit(events.VOLUME_CHANGED, newVolume);
var modalArgs = wizardModalService.modal.calls.argsFor(0)[0];
modalArgs.submit();
expect(cinderAPI.createVolume).toHaveBeenCalledWith(oldVolume);
});
}); });
describe('allowed', function() { describe('allowed', function() {

View File

@ -267,7 +267,6 @@
*/ */
function events() { function events() {
return { return {
VOLUME_CHANGED: 'horizon.app.core.images.VOLUME_CHANGED',
IMAGE_CHANGED: 'horizon.app.core.images.IMAGE_CHANGED', IMAGE_CHANGED: 'horizon.app.core.images.IMAGE_CHANGED',
IMAGE_METADATA_CHANGED: 'horizon.app.core.images.IMAGE_METADATA_CHANGED', IMAGE_METADATA_CHANGED: 'horizon.app.core.images.IMAGE_METADATA_CHANGED',
IMAGE_UPLOAD_PROGRESS: 'horizon.app.core.images.IMAGE_UPLOAD_PROGRESS' IMAGE_UPLOAD_PROGRESS: 'horizon.app.core.images.IMAGE_UPLOAD_PROGRESS'

View File

@ -25,8 +25,7 @@
'$filter', '$filter',
'horizon.app.core.openstack-service-api.cinder', 'horizon.app.core.openstack-service-api.cinder',
'horizon.app.core.openstack-service-api.nova', 'horizon.app.core.openstack-service-api.nova',
'horizon.framework.widgets.charts.quotaChartDefaults', 'horizon.framework.widgets.charts.quotaChartDefaults'
'horizon.app.core.images.events'
]; ];
/** /**
@ -37,12 +36,11 @@
* @param {Object} cinder * @param {Object} cinder
* @param {Object} nova * @param {Object} nova
* @param {Object} quotaChartDefaults * @param {Object} quotaChartDefaults
* @param {Object} events
* @description * @description
* This controller is use for creating an image. * This controller is use for creating an image.
* @return {undefined} No return value * @return {undefined} No return value
*/ */
function CreateVolumeController($scope, $filter, cinder, nova, quotaChartDefaults, events) { function CreateVolumeController($scope, $filter, cinder, nova, quotaChartDefaults) {
var ctrl = this; var ctrl = this;
ctrl.volumeType = {}; ctrl.volumeType = {};
@ -57,7 +55,9 @@
var numberOfVolumesToAdd = 1; var numberOfVolumesToAdd = 1;
ctrl.volume = { // bind for local access and also hand back up to the wizard controller
// stepModels will be passed to the create volume action submit()
$scope.stepModels.volumeForm = ctrl.volume = {
size: 1, size: 1,
name: ctrl.image.name, name: ctrl.image.name,
description: '', description: '',
@ -149,7 +149,6 @@
ctrl.volumeTypes.forEach(function(volumeType) { ctrl.volumeTypes.forEach(function(volumeType) {
if (volumeType.name === response.name) { if (volumeType.name === response.name) {
ctrl.volumeType = volumeType; ctrl.volumeType = volumeType;
return;
} }
}); });
} }
@ -212,7 +211,6 @@
function volumeChangeEvent() { function volumeChangeEvent() {
ctrl.volume.volume_type = ctrl.volumeType.name || ''; ctrl.volume.volume_type = ctrl.volumeType.name || '';
$scope.$emit(events.VOLUME_CHANGED, ctrl.volume);
} }
function deregisterWatchers() { function deregisterWatchers() {

View File

@ -34,9 +34,7 @@
} }
}; };
}, },
getAbsoluteLimits: function() { getAbsoluteLimits: angular.noop
return;
}
}; };
beforeEach(module('horizon.app.core.images')); beforeEach(module('horizon.app.core.images'));
@ -60,10 +58,10 @@
size: 1024 size: 1024
}; };
$scope.stepModels = {};
$scope.volumeForm = { $scope.volumeForm = {
$setValidity : function() { $setValidity : angular.noop
return;
}
}; };
controller = $injector.get('$controller'); controller = $injector.get('$controller');
@ -276,16 +274,16 @@
expect(graph.maxLimit).toEqual(graph.maxLimit); expect(graph.maxLimit).toEqual(graph.maxLimit);
}); });
it('should emit volumeChanged event when a volume attribute is changed', function() { it('should update volume type from volume name', function() {
var ctrl = createController(); var ctrl = createController();
$scope.$apply(); $scope.$apply();
$scope.$emit.calls.reset();
ctrl.volume.volume_type = 'spam';
ctrl.volume.name = 'nova2'; ctrl.volume.name = 'nova2';
$scope.$apply(); $scope.$apply();
expect($scope.$emit.calls.count()).toEqual(1); expect(ctrl.volume.volume_type).toEqual('lvmdriver-1');
}); });
it('should set the validity of the volume size input field based on the limit', function() { it('should set the validity of the volume size input field based on the limit', function() {
@ -358,29 +356,6 @@
expect($scope.$emit).not.toHaveBeenCalled(); expect($scope.$emit).not.toHaveBeenCalled();
}); });
it('should emit a changed volume event when the user changes the volume', function() {
var ctrl = createController();
$scope.$apply();
ctrl.volume.size = 100;
var emittedEventArgs = $scope.$emit.calls.argsFor(0);
var expectedVolume = {
size: 100,
name: ctrl.image.name,
description: '',
volume_type: 'lvmdriver-1',
availability_zone: 'zone1', // pre-selects first
metadata: {},
image_id: ctrl.image.id,
snapshot_id: null,
source_volid: null
};
expect(emittedEventArgs[0]).toEqual('horizon.app.core.images.VOLUME_CHANGED');
expect(emittedEventArgs[1]).toEqual(expectedVolume);
});
it('not default the availability_zone if none present', function() { it('not default the availability_zone if none present', function() {
nova.getAvailabilityZones = function() { nova.getAvailabilityZones = function() {
@ -394,35 +369,7 @@
$scope.$apply(); $scope.$apply();
ctrl.volume.size = 100; ctrl.volume.size = 100;
expect(ctrl.volume.availability_zone).toEqual('');
var emittedEventArgs = $scope.$emit.calls.argsFor(0);
var expectedVolume = {
size: 100,
name: ctrl.image.name,
description: '',
volume_type: 'lvmdriver-1',
availability_zone: '',
metadata: {},
image_id: ctrl.image.id,
snapshot_id: null,
source_volid: null
};
expect(emittedEventArgs[0]).toEqual('horizon.app.core.images.VOLUME_CHANGED');
expect(emittedEventArgs[1]).toEqual(expectedVolume);
});
it('should emit changed volume events even if the volume name is empty', function() {
var ctrl = createController();
$scope.$apply();
ctrl.volumeType.name = '';
$scope.$emit.calls.reset();
ctrl.volume.name = '';
$scope.$apply();
expect($scope.$emit.calls.count()).toEqual(2);
}); });
it('should not update the graph if wrong values are given for volume size', function () { it('should not update the graph if wrong values are given for volume size', function () {

View File

@ -0,0 +1,10 @@
---
deprecations:
- The initScope method for action services has been
deprecated, with the new method initAction added
which does not get passed a scope. The allowed and
perform method are now passed a scope for context
in addition to the first item/items argument.
The "scope" parameter to the WizardModalService modal()
method has also been deprecated, and will be ignored
in a future release of Horizon.