Merge pull request #3392 from ulferts/feature/wp_error_notifications

Feature/wp error notifications
pull/3423/head
Jan Sandbrink 9 years ago
commit 38d3619b00
  1. 18
      app/assets/stylesheets/layout/_work_package.sass
  2. 1
      app/views/layouts/angular.html.erb
  3. 2
      app/views/layouts/base.html.erb
  4. 45
      frontend/app/services/api-notifications-service.js
  5. 6
      frontend/app/services/index.js
  6. 5
      frontend/app/services/notifications-service.js
  7. 25
      frontend/app/services/work-package-service.js
  8. 8
      frontend/app/templates/components/flash_message.html
  9. 2
      frontend/app/time_entries/controllers/index.js
  10. 12
      frontend/app/time_entries/controllers/time-entries-controller.js
  11. 6
      frontend/app/ui_components/index.js
  12. 24
      frontend/app/work_packages/controllers/dialogs/save.js
  13. 14
      frontend/app/work_packages/controllers/dialogs/settings.js
  14. 16
      frontend/app/work_packages/controllers/dialogs/share.js
  15. 6
      frontend/app/work_packages/controllers/index.js
  16. 4
      frontend/app/work_packages/controllers/menus/index.js
  17. 18
      frontend/app/work_packages/controllers/menus/settings-dropdown-menu-controller.js
  18. 15
      frontend/app/work_packages/controllers/work-package-details-controller.js
  19. 9
      frontend/app/work_packages/controllers/work-packages-list-controller.js
  20. 4
      frontend/app/work_packages/directives/inplace_editor/inplace-editor-edit-pane-directive.js
  21. 20
      frontend/app/work_packages/helpers/api-helper.js
  22. 2
      frontend/app/work_packages/helpers/index.js
  23. 10
      frontend/app/work_packages/view_models/children-relations-handler.js
  24. 19
      frontend/app/work_packages/view_models/common-relations-handler.js
  25. 17
      frontend/app/work_packages/view_models/index.js
  26. 6
      frontend/app/work_packages/view_models/parent-relations-handler.js
  27. 1
      frontend/public/index.html
  28. 91
      frontend/tests/unit/tests/services/api-notifications-service-test.js
  29. 29
      frontend/tests/unit/tests/services/notifications-service-test.js
  30. 126
      frontend/tests/unit/tests/ui_components/flash-message-directive-test.js
  31. 20
      frontend/tests/unit/tests/work_packages/controllers/dialogs/settings-test.js
  32. 23
      frontend/tests/unit/tests/work_packages/helpers/api-helper-test.js
  33. 6
      spec/features/menu_items/query_menu_item_spec.rb
  34. 43
      spec/features/page_objects/notification.rb

@ -32,31 +32,21 @@
#content #content
$flash-margins-padding: 10px $flash-margins-padding: 10px
// HACK: workaround to ensure correct height applied to child elements // HACK: workaround to ensure correct height applied to child elements.
// the dom looks like this // The dom looks like this:
// flash (generated from rails - if it was generated when rendering the page) // flash (generated from rails - if it was generated when rendering the page)
// flash (generated from angular - exists always but is hidden with ng-hide if not needed)
// div[ui-view] (main content we want to adjust the height for) // div[ui-view] (main content we want to adjust the height for)
// div style="clear:both;" (is pushed to overflow - of no relevance) // div style="clear:both;" (is pushed to overflow - of no relevance)
// //
// There can be at most two flash messages visible on the page.
// We need to adjust the hight of the div[ui-view] depending on the amount of flash messages.
//
// This makes use of more specific rules overwriting less specific ones. // This makes use of more specific rules overwriting less specific ones.
// Per default, the height is always 100% // Per default, the height is always 100%
.flash + div[ui-view] & > div[ui-view]
height: 100% height: 100%
// If there is only one flash message shown:
// Subtract the height of the flash message // Subtract the height of the flash message
.flash:not(.ng-hide) ~ div[ui-view] .flash ~ div[ui-view]
height: calc(100% - #{($content-flash-height + $flash-margins-padding)}) height: calc(100% - #{($content-flash-height + $flash-margins-padding)})
// If there are two flash messages shown:
// Subtract the height of the two flash messages
.flash:not(.ng-hide) ~ .flash:not(.ng-hide) ~ div[ui-view]
height: calc(100% - #{2 * ($content-flash-height + $flash-margins-padding)})
// HACK: workaround to ensure correct height applied to child elements // HACK: workaround to ensure correct height applied to child elements
#work-packages-index #work-packages-index
height: 100% height: 100%

@ -122,7 +122,6 @@ See doc/COPYRIGHT.rdoc for more details.
ng-class="{ 'hidden-navigation': !showNavigation }"> ng-class="{ 'hidden-navigation': !showNavigation }">
<h1 class="hidden-for-sighted"><%= l(:label_content) %></h1> <h1 class="hidden-for-sighted"><%= l(:label_content) %></h1>
<%= render_flash_messages %> <%= render_flash_messages %>
<flash-message></flash-message>
<div ui-view></div> <div ui-view></div>
<%= call_hook :view_layouts_base_content %> <%= call_hook :view_layouts_base_content %>
<div style="clear:both;">&nbsp;</div> <div style="clear:both;">&nbsp;</div>

@ -118,7 +118,7 @@ See doc/COPYRIGHT.rdoc for more details.
ng-class="{ 'hidden-navigation': !showNavigation }"> ng-class="{ 'hidden-navigation': !showNavigation }">
<h1 class="hidden-for-sighted"><%= l(:label_content) %></h1> <h1 class="hidden-for-sighted"><%= l(:label_content) %></h1>
<%= render_flash_messages %> <%= render_flash_messages %>
<flash-message></flash-message> <notifications></notifications>
<!-- Action menu --> <!-- Action menu -->
<%= render :partial => 'layouts/action_menu' %> <%= render :partial => 'layouts/action_menu' %>
<%= yield %> <%= yield %>

@ -26,37 +26,26 @@
// See doc/COPYRIGHT.rdoc for more details. // See doc/COPYRIGHT.rdoc for more details.
//++ //++
// TODO move to UI components module.exports = function(NotificationsService, ApiHelper) {
module.exports = function($rootScope, $timeout, ConfigurationService) { 'use strict';
return { var addError = function(error) {
restrict: 'E', var messages = ApiHelper.getErrorMessages(error);
replace: true,
scope: {},
templateUrl: '/templates/components/flash_message.html',
link: function(scope, element, attrs) {
$rootScope.$on('flashMessage', function(event, message) {
scope.message = message;
scope.flashType = 'notice';
scope.flashId = 'flash-notice';
var fadeOutTime = attrs.fadeOutTime || 3000;
if (message.isError) { if (messages.length > 1) {
scope.flashType = "errorExplanation"; NotificationsService.addError('', messages);
scope.flashId = "errorExplanation";
} }
else {
// not using $timeout to allow capybara to not wait until timeout is done with NotificationsService.addError(messages[0]);
// scope apply
if (!ConfigurationService.accessibilityModeEnabled() && !message.isPermanent) {
setTimeout(function() {
scope.$apply(function() {
scope.message = undefined;
});
}, fadeOutTime);
}
});
} }
}; };
var addSuccess = function(text) {
NotificationsService.addSuccess(text);
};
return {
addError: addError,
addSuccess: addSuccess
};
}; };

@ -103,6 +103,7 @@ angular.module('openproject.services')
'AuthorisationService', 'AuthorisationService',
'EditableFieldsState', 'EditableFieldsState',
'WorkPackageFieldService', 'WorkPackageFieldService',
'NotificationsService',
require('./work-package-service') require('./work-package-service')
]) ])
.service('NotificationsService', [ .service('NotificationsService', [
@ -110,4 +111,9 @@ angular.module('openproject.services')
'$rootScope', '$rootScope',
require('./notifications-service.js') require('./notifications-service.js')
]) ])
.service('ApiNotificationsService', [
'NotificationsService',
'ApiHelper',
require('./api-notifications-service.js')
])
.service('ConversionService', require('./conversion-service.js')); .service('ConversionService', require('./conversion-service.js'));

@ -42,12 +42,9 @@ module.exports = function(I18n, $rootScope) {
return _.extend(createNotification(message), { type: 'warning' }); return _.extend(createNotification(message), { type: 'warning' });
}, },
createErrorNotification = function(message, errors) { createErrorNotification = function(message, errors) {
if(!errors) {
throw new Error('Cannot create an error notification without errors!');
}
return _.extend(createNotification(message), { return _.extend(createNotification(message), {
type: 'error', type: 'error',
errors: errors errors: errors || []
}); });
}, },
createWorkPackageUploadNotification = function(message, uploads) { createWorkPackageUploadNotification = function(message, uploads) {

@ -38,7 +38,8 @@ module.exports = function($http,
$q, $q,
AuthorisationService, AuthorisationService,
EditableFieldsState, EditableFieldsState,
WorkPackageFieldService WorkPackageFieldService,
NotificationsService
) { ) {
var workPackage; var workPackage;
@ -298,11 +299,9 @@ module.exports = function($http,
return response.data; return response.data;
}, },
function(failedResponse) { function(failedResponse) {
$rootScope.$emit('flashMessage', { NotificationsService.addError(
isError: true, I18n.t('js.work_packages.query.errors.unretrievable_query')
isPermanent: true, );
text: I18n.t('js.work_packages.query.errors.unretrievable_query')
});
} }
); );
}, },
@ -320,18 +319,16 @@ module.exports = function($http,
if (defaultHandling) { if (defaultHandling) {
promise.success(function(data, status) { promise.success(function(data, status) {
// TODO wire up to API and process API response // TODO wire up to API and process API response
$rootScope.$emit('flashMessage', { NotificationsService.addSuccess(
isError: false, I18n.t('js.work_packages.message_successful_bulk_delete')
text: I18n.t('js.work_packages.message_successful_bulk_delete') );
});
$rootScope.$emit('workPackagesRefreshRequired'); $rootScope.$emit('workPackagesRefreshRequired');
}) })
.error(function(data, status) { .error(function(data, status) {
// TODO wire up to API and processs API response // TODO wire up to API and processs API response
$rootScope.$emit('flashMessage', { NotificationsService.addError(
isError: true, I18n.t('js.work_packages.message_error_during_bulk_delete')
text: I18n.t('js.work_packages.message_error_during_bulk_delete') );
});
}); });
} }

@ -1,8 +0,0 @@
<div ng-show="message !== undefined"
style="display: block;"
class="flash"
ng-class="['icon', 'icon-'+flashType, flashType]"
id="{{flashId}}"
role="alert"
ng-bind="message.text">
</div>

@ -28,6 +28,6 @@
angular.module('openproject.timeEntries.controllers') angular.module('openproject.timeEntries.controllers')
.controller('TimeEntriesController', ['$scope', '$http', 'PathHelper', .controller('TimeEntriesController', ['$scope', '$http', 'PathHelper',
'SortService', 'PaginationService', 'SortService', 'PaginationService', 'NotificationsService',
require('./time-entries-controller') require('./time-entries-controller')
]); ]);

@ -26,7 +26,7 @@
// See doc/COPYRIGHT.rdoc for more details. // See doc/COPYRIGHT.rdoc for more details.
//++ //++
module.exports = function($scope, $http, PathHelper, SortService, PaginationService) { module.exports = function($scope, $http, PathHelper, SortService, PaginationService, NotificationsService) {
$scope.PathHelper = PathHelper; $scope.PathHelper = PathHelper;
$scope.timeEntries = gon.timeEntries; $scope.timeEntries = gon.timeEntries;
$scope.totalEntryCount = gon.total_count; $scope.totalEntryCount = gon.total_count;
@ -57,11 +57,11 @@ module.exports = function($scope, $http, PathHelper, SortService, PaginationServ
$scope.deleteTimeEntry = function(id) { $scope.deleteTimeEntry = function(id) {
if (window.confirm(I18n.t('js.text_are_you_sure'))) { if (window.confirm(I18n.t('js.text_are_you_sure'))) {
$http['delete'](PathHelper.timeEntryPath(id)) $http['delete'](PathHelper.timeEntryPath(id))
.success(function(data, status, headers, config) { .success(function(data) {
var index = 0; var index = 0;
for (var i = 0; i < $scope.timeEntries.length; i++) { for (var i = 0; i < $scope.timeEntries.length; i++) {
if ($scope.timeEntries[i].id == id) { if ($scope.timeEntries[i].id === id) {
index = i; index = i;
break; break;
} }
@ -69,10 +69,10 @@ module.exports = function($scope, $http, PathHelper, SortService, PaginationServ
$scope.timeEntries.splice(index, 1); $scope.timeEntries.splice(index, 1);
$scope.$emit('flashMessage', data); NotificationsService.addSuccess(data.text);
}) })
.error(function(data, status, headers, config) { .error(function(data) {
$scope.$emit('flashMessage', data); NotificationsService.addError(data.text);
}); });
} }
}; };

@ -49,12 +49,6 @@ angular.module('openproject.uiComponents')
.constant('ENTER_KEY', 13) .constant('ENTER_KEY', 13)
.directive('executeOnEnter', ['ENTER_KEY', require( .directive('executeOnEnter', ['ENTER_KEY', require(
'./execute-on-enter-directive')]) './execute-on-enter-directive')])
.directive('flashMessage', [
'$rootScope',
'$timeout',
'ConfigurationService',
require('./flash-message-directive')
])
.directive('expandableSearch', ['ENTER_KEY', require('./expandable-search')]) .directive('expandableSearch', ['ENTER_KEY', require('./expandable-search')])
.directive('focus', ['FocusHelper', require('./focus-directive')]) .directive('focus', ['FocusHelper', require('./focus-directive')])
.constant('FOCUSABLE_SELECTOR', 'a, button, :input, [tabindex], select') .constant('FOCUSABLE_SELECTOR', 'a, button, :input, [tabindex], select')

@ -26,7 +26,14 @@
// See doc/COPYRIGHT.rdoc for more details. // See doc/COPYRIGHT.rdoc for more details.
//++ //++
module.exports = function($scope, saveModal, QueryService, AuthorisationService, $state) { module.exports = function(
$scope,
saveModal,
QueryService,
AuthorisationService,
$state,
NotificationsService
) {
this.name = 'Save'; this.name = 'Save';
this.closeMe = saveModal.deactivate; this.closeMe = saveModal.deactivate;
@ -34,14 +41,23 @@ module.exports = function($scope, saveModal, QueryService, AuthorisationService,
$scope.saveQueryAs = function(name) { $scope.saveQueryAs = function(name) {
QueryService.saveQueryAs(name) QueryService.saveQueryAs(name)
.then(function(data){ .then(function(data){
if (data.status.isError){
NotificationsService.addError(data.status.text);
}
else {
// push query id to URL without reinitializing work-packages-list-controller // push query id to URL without reinitializing work-packages-list-controller
if (data.query) { if (data.query) {
$state.go('work-packages.list', { query_id: data.query.id, query: null }, { notify: false }); $state.go('work-packages.list',
AuthorisationService.initModelAuth("query", data.query._links); { query_id: data.query.id, query: null },
{ notify: false });
AuthorisationService.initModelAuth('query', data.query._links);
} }
saveModal.deactivate(); saveModal.deactivate();
$scope.$emit('flashMessage', data.status);
NotificationsService.addSuccess(data.status.text);
}
}); });
}; };
}; };

@ -26,7 +26,15 @@
// See doc/COPYRIGHT.rdoc for more details. // See doc/COPYRIGHT.rdoc for more details.
//++ //++
module.exports = function($scope, settingsModal, QueryService, AuthorisationService, $rootScope, QUERY_MENU_ITEM_TYPE) { module.exports = function(
$scope,
settingsModal,
QueryService,
AuthorisationService,
$rootScope,
QUERY_MENU_ITEM_TYPE,
NotificationsService
) {
var query = QueryService.getQuery(); var query = QueryService.getQuery();
@ -43,7 +51,7 @@ module.exports = function($scope, settingsModal, QueryService, AuthorisationServ
}) })
.then(function(data) { .then(function(data) {
settingsModal.deactivate(); settingsModal.deactivate();
$scope.$emit('flashMessage', data.status); NotificationsService.addSuccess(data.status.text);
$rootScope.$broadcast('openproject.layout.renameQueryMenuItem', { $rootScope.$broadcast('openproject.layout.renameQueryMenuItem', {
itemType: QUERY_MENU_ITEM_TYPE, itemType: QUERY_MENU_ITEM_TYPE,
@ -52,7 +60,7 @@ module.exports = function($scope, settingsModal, QueryService, AuthorisationServ
}); });
if(data.query) { if(data.query) {
AuthorisationService.initModelAuth("query", data.query._links); AuthorisationService.initModelAuth('query', data.query._links);
} }
}); });
}; };

@ -26,7 +26,15 @@
// See doc/COPYRIGHT.rdoc for more details. // See doc/COPYRIGHT.rdoc for more details.
//++ //++
module.exports = function($scope, shareModal, QueryService, AuthorisationService, queryMenuItemFactory, PathHelper) { module.exports = function(
$scope,
shareModal,
QueryService,
AuthorisationService,
queryMenuItemFactory,
PathHelper,
NotificationsService
) {
this.name = 'Share'; this.name = 'Share';
this.closeMe = shareModal.deactivate; this.closeMe = shareModal.deactivate;
@ -38,7 +46,7 @@ module.exports = function($scope, shareModal, QueryService, AuthorisationService
function closeAndReport(message) { function closeAndReport(message) {
shareModal.deactivate(); shareModal.deactivate();
$scope.$emit('flashMessage', message); NotificationsService.addSuccess(message.text);
} }
$scope.cannot = AuthorisationService.cannot; $scope.cannot = AuthorisationService.cannot;
@ -49,11 +57,11 @@ module.exports = function($scope, shareModal, QueryService, AuthorisationService
.then(function(data){ .then(function(data){
messageObject = data.status; messageObject = data.status;
if(data.query) { if(data.query) {
AuthorisationService.initModelAuth("query", data.query._links); AuthorisationService.initModelAuth('query', data.query._links);
} }
}) })
.then(function(data){ .then(function(data){
if($scope.query.starred != $scope.shareSettings.starred){ if($scope.query.starred !== $scope.shareSettings.starred){
QueryService.toggleQueryStarred($scope.query) QueryService.toggleQueryStarred($scope.query)
.then(function(data){ .then(function(data){
closeAndReport(data.status || messageObject); closeAndReport(data.status || messageObject);

@ -90,7 +90,7 @@ angular.module('openproject.workPackages.controllers')
'CommonRelationsHandler', 'CommonRelationsHandler',
'ChildrenRelationsHandler', 'ChildrenRelationsHandler',
'ParentRelationsHandler', 'ParentRelationsHandler',
'EditableFieldsState', 'NotificationsService',
require('./work-package-details-controller') require('./work-package-details-controller')
]) ])
.controller('WorkPackageNewController', [ .controller('WorkPackageNewController', [
@ -134,6 +134,7 @@ angular.module('openproject.workPackages.controllers')
'PathHelper', 'PathHelper',
'Query', 'Query',
'OPERATORS_AND_LABELS_BY_FILTER_TYPE', 'OPERATORS_AND_LABELS_BY_FILTER_TYPE',
'NotificationsService',
require('./work-packages-list-controller') require('./work-packages-list-controller')
]) ])
.factory('columnsModal', ['btfModal', function(btfModal) { .factory('columnsModal', ['btfModal', function(btfModal) {
@ -197,6 +198,7 @@ angular.module('openproject.workPackages.controllers')
'QueryService', 'QueryService',
'AuthorisationService', 'AuthorisationService',
'$state', '$state',
'NotificationsService',
require('./dialogs/save') require('./dialogs/save')
]) ])
.factory('settingsModal', ['btfModal', function(btfModal) { .factory('settingsModal', ['btfModal', function(btfModal) {
@ -214,6 +216,7 @@ angular.module('openproject.workPackages.controllers')
'AuthorisationService', 'AuthorisationService',
'$rootScope', '$rootScope',
'QUERY_MENU_ITEM_TYPE', 'QUERY_MENU_ITEM_TYPE',
'NotificationsService',
require('./dialogs/settings') require('./dialogs/settings')
]) ])
.factory('shareModal', ['btfModal', function(btfModal) { .factory('shareModal', ['btfModal', function(btfModal) {
@ -231,6 +234,7 @@ angular.module('openproject.workPackages.controllers')
'AuthorisationService', 'AuthorisationService',
'queryMenuItemFactory', 'queryMenuItemFactory',
'PathHelper', 'PathHelper',
'NotificationsService',
require('./dialogs/share') require('./dialogs/share')
]) ])
.factory('sortingModal', ['btfModal', function(btfModal) { .factory('sortingModal', ['btfModal', function(btfModal) {

@ -73,7 +73,9 @@ angular.module('openproject.workPackages')
'AuthorisationService', 'AuthorisationService',
'$window', '$window',
'$state', '$state',
'$timeout', require('./settings-dropdown-menu-controller') '$timeout',
'NotificationsService',
require('./settings-dropdown-menu-controller')
]) ])
.factory('TasksDropdownMenu', [ .factory('TasksDropdownMenu', [
'ngContextMenu', 'ngContextMenu',

@ -31,7 +31,8 @@ module.exports = function(
exportModal, saveModal, settingsModal, exportModal, saveModal, settingsModal,
shareModal, sortingModal, groupingModal, shareModal, sortingModal, groupingModal,
QueryService, AuthorisationService, QueryService, AuthorisationService,
$window, $state, $timeout) { $window, $state, $timeout,
NotificationsService) {
$scope.$watch('query.displaySums', function(newValue) { $scope.$watch('query.displaySums', function(newValue) {
$timeout(function() { $timeout(function() {
$scope.displaySumsLabel = (newValue) ? I18n.t('js.toolbar.settings.hide_sums') $scope.displaySumsLabel = (newValue) ? I18n.t('js.toolbar.settings.hide_sums')
@ -53,10 +54,15 @@ module.exports = function(
if( allowQueryAction(event, 'update') ) { if( allowQueryAction(event, 'update') ) {
QueryService.saveQuery() QueryService.saveQuery()
.then(function(data){ .then(function(data){
$scope.$emit('flashMessage', data.status); if (data.status.isError) {
NotificationsService.addError(data.status.text);
}
else {
NotificationsService.addSuccess(data.status.text);
$state.go('work-packages.list', $state.go('work-packages.list',
{ 'query_id': $scope.query.id, 'query_props': null }, { 'query_id': $scope.query.id, 'query_props': null },
{ notify: false }); { notify: false });
}
}); });
} }
} }
@ -67,11 +73,15 @@ module.exports = function(
if( allowQueryAction(event, 'delete') && preventNewQueryAction(event) && deleteConfirmed() ){ if( allowQueryAction(event, 'delete') && preventNewQueryAction(event) && deleteConfirmed() ){
QueryService.deleteQuery() QueryService.deleteQuery()
.then(function(data){ .then(function(data){
settingsModal.deactivate(); if (data.status.isError) {
$scope.$emit('flashMessage', data.status); NotificationsService.addError(data.status.text);
}
else {
NotificationsService.addSuccess(data.status.text);
$state.go('work-packages.list', $state.go('work-packages.list',
{ 'query_id': null, 'query_props': null }, { 'query_id': null, 'query_props': null },
{ reload: true }); { reload: true });
}
}); });
} }
}; };

@ -42,7 +42,8 @@ module.exports = function($scope,
WorkPackageService, WorkPackageService,
CommonRelationsHandler, CommonRelationsHandler,
ChildrenRelationsHandler, ChildrenRelationsHandler,
ParentRelationsHandler ParentRelationsHandler,
NotificationsService
) { ) {
$scope.$on('$stateChangeSuccess', function(event, toState){ $scope.$on('$stateChangeSuccess', function(event, toState){
latestTab.registerState(toState.name); latestTab.registerState(toState.name);
@ -75,14 +76,16 @@ module.exports = function($scope,
$scope.$emit('workPackgeLoaded'); $scope.$emit('workPackgeLoaded');
function outputMessage(message, isError) { function outputMessage(message, isError) {
$scope.$emit('flashMessage', { if (!!isError) {
isError: !!isError, NotificationsService.addError(message);
text: message }
}); else {
NotificationsService.addSuccess(message);
}
} }
function outputError(error) { function outputError(error) {
outputMessage(error.message, true); NotificationsService.addError(error.message);
} }
$scope.outputMessage = outputMessage; // expose to child controllers $scope.outputMessage = outputMessage; // expose to child controllers

@ -30,7 +30,7 @@ module.exports = function($scope, $rootScope, $state, $location, latestTab,
I18n, WorkPackagesTableService, I18n, WorkPackagesTableService,
WorkPackageService, ProjectService, QueryService, PaginationService, WorkPackageService, ProjectService, QueryService, PaginationService,
AuthorisationService, UrlParamsHelper, PathHelper, Query, AuthorisationService, UrlParamsHelper, PathHelper, Query,
OPERATORS_AND_LABELS_BY_FILTER_TYPE) { OPERATORS_AND_LABELS_BY_FILTER_TYPE, NotificationsService) {
// Setup // Setup
function initialSetup() { function initialSetup() {
@ -84,10 +84,9 @@ module.exports = function($scope, $rootScope, $state, $location, latestTab,
return WorkPackageService.getWorkPackages($scope.projectIdentifier, queryFromParams, PaginationService.getPaginationOptions()); return WorkPackageService.getWorkPackages($scope.projectIdentifier, queryFromParams, PaginationService.getPaginationOptions());
} catch(e) { } catch(e) {
$scope.$emit('flashMessage', { NotificationsService.addError(
isError: true, I18n.t('js.work_packages.query.errors.unretrievable_query')
text: I18n.t('js.work_packages.query.errors.unretrievable_query') );
});
clearUrlQueryParams(); clearUrlQueryParams();
return WorkPackageService.getWorkPackages($scope.projectIdentifier); return WorkPackageService.getWorkPackages($scope.projectIdentifier);

@ -42,7 +42,7 @@ module.exports = function(
if (_.isEmpty(_.keys(errors))) { if (_.isEmpty(_.keys(errors))) {
return; return;
} }
var errorMessages = _.map(errors); var errorMessages = _.flatten(_.map(errors), true);
NotificationsService.addError(I18n.t('js.label_validation_error'), errorMessages); NotificationsService.addError(I18n.t('js.label_validation_error'), errorMessages);
}; };
@ -157,7 +157,7 @@ module.exports = function(
function setFailure(e) { function setFailure(e) {
afterError(); afterError();
EditableFieldsState.errors = { EditableFieldsState.errors = {
'_common': ApiHelper.getErrorMessage(e) '_common': ApiHelper.getErrorMessages(e)
}; };
showErrors(); showErrors();
} }

@ -28,20 +28,12 @@
module.exports = function() { module.exports = function() {
var ApiHelper = { var ApiHelper = {
handleError: function(scope, error) { getErrorMessages: function(error) {
scope.$emit('flashMessage', { if(error.status === 500) {
isError: true, return [error.statusText];
text: ApiHelper.getErrorMessage(error)
});
},
getErrorMessage: function(error) {
if(error.status == 500) {
return error.statusText;
} else { } else {
var response = JSON.parse(error.responseText); var response = JSON.parse(error.responseText);
var messages = []; var messages = [];
var message;
if (ApiHelper.isMultiErrorMessage(response)) { if (ApiHelper.isMultiErrorMessage(response)) {
angular.forEach(response._embedded.errors, function(error) { angular.forEach(response._embedded.errors, function(error) {
@ -51,14 +43,12 @@ module.exports = function() {
messages.push(response.message); messages.push(response.message);
} }
message = messages.join(' '); return messages;
return message;
} }
}, },
isMultiErrorMessage: function(error) { isMultiErrorMessage: function(error) {
return error.errorIdentifier == 'urn:openproject-org:api:v3:errors:MultipleErrors'; return error.errorIdentifier === 'urn:openproject-org:api:v3:errors:MultipleErrors';
} }
}; };

@ -27,7 +27,7 @@
//++ //++
angular.module('openproject.workPackages.helpers') angular.module('openproject.workPackages.helpers')
.factory('ApiHelper', require('./api-helper')) .factory('ApiHelper', ['NotificationsService', require('./api-helper')])
.factory('FiltersHelper', ['I18n', require('./filters-helper')]) .factory('FiltersHelper', ['I18n', require('./filters-helper')])
.constant('ACTIVE_USER_STATUSES', ['active', 'registered']) .constant('ACTIVE_USER_STATUSES', ['active', 'registered'])
.factory('UsersHelper', ['ACTIVE_USER_STATUSES', require('./users-helper')]) .factory('UsersHelper', ['ACTIVE_USER_STATUSES', require('./users-helper')])

@ -26,11 +26,15 @@
// See doc/COPYRIGHT.rdoc for more details. // See doc/COPYRIGHT.rdoc for more details.
//++ //++
module.exports = function(PathHelper, CommonRelationsHandler, WorkPackageService) { module.exports = function(
CommonRelationsHandler,
WorkPackageService,
ApiNotificationsService
) {
function ChildrenRelationsHandler(workPackage, children) { function ChildrenRelationsHandler(workPackage, children) {
var handler = new CommonRelationsHandler(workPackage, children, undefined); var handler = new CommonRelationsHandler(workPackage, children, undefined);
handler.type = "child"; handler.type = 'child';
handler.applyCustomExtensions = undefined; handler.applyCustomExtensions = undefined;
handler.canAddRelation = function() { return !!this.workPackage.links.addChild; }; handler.canAddRelation = function() { return !!this.workPackage.links.addChild; };
@ -55,7 +59,7 @@ module.exports = function(PathHelper, CommonRelationsHandler, WorkPackageService
scope.updateFocus(index); scope.updateFocus(index);
scope.$emit('workPackageRefreshRequired'); scope.$emit('workPackageRefreshRequired');
}, function(error) { }, function(error) {
ApiHelper.handleError(scope, error); ApiNotificationsService.addError(error);
}); });
}; };

@ -26,7 +26,11 @@
// See doc/COPYRIGHT.rdoc for more details. // See doc/COPYRIGHT.rdoc for more details.
//++ //++
module.exports = function($timeout, WorkPackageService, ApiHelper, PathHelper, MAX_AUTOCOMPLETER_ADDITION_ITERATIONS) { module.exports = function(
$timeout,
WorkPackageService,
ApiNotificationsService
) {
function CommonRelationsHandler(workPackage, function CommonRelationsHandler(workPackage,
relations, relations,
relationsId) { relationsId) {
@ -34,7 +38,7 @@ module.exports = function($timeout, WorkPackageService, ApiHelper, PathHelper, M
this.relations = relations; this.relations = relations;
this.relationsId = relationsId; this.relationsId = relationsId;
this.type = "relation"; this.type = 'relation';
this.isSingletonRelation = false; this.isSingletonRelation = false;
} }
@ -56,12 +60,15 @@ module.exports = function($timeout, WorkPackageService, ApiHelper, PathHelper, M
}, },
addRelation: function(scope) { addRelation: function(scope) {
WorkPackageService.addWorkPackageRelation(this.workPackage, scope.relationToAddId, this.relationsId).then(function(relation) { WorkPackageService.addWorkPackageRelation(this.workPackage,
scope.relationToAddId,
this.relationsId)
.then(function() {
scope.relationToAddId = ''; scope.relationToAddId = '';
scope.updateFocus(-1); scope.updateFocus(-1);
scope.$emit('workPackageRefreshRequired'); scope.$emit('workPackageRefreshRequired');
}, function(error) { }, function(error) {
ApiHelper.handleError(scope, error); ApiNotificationsService.addError(error);
}); });
}, },
@ -74,7 +81,7 @@ module.exports = function($timeout, WorkPackageService, ApiHelper, PathHelper, M
scope.updateFocus(index); scope.updateFocus(index);
scope.$emit('workPackageRefreshRequired'); scope.$emit('workPackageRefreshRequired');
}, function(error) { }, function(error) {
ApiHelper.handleError(scope, error); ApiNotificationsService.addError(scope, error);
}); });
}, },
@ -89,7 +96,7 @@ module.exports = function($timeout, WorkPackageService, ApiHelper, PathHelper, M
getRelatedWorkPackage: function(workPackage, relation) { getRelatedWorkPackage: function(workPackage, relation) {
var self = workPackage.links.self.href; var self = workPackage.links.self.href;
if (relation.links.relatedTo.href == self) { if (relation.links.relatedTo.href === self) {
return relation.links.relatedFrom.fetch(); return relation.links.relatedFrom.fetch();
} else { } else {
return relation.links.relatedTo.fetch(); return relation.links.relatedTo.fetch();

@ -27,20 +27,21 @@
//++ //++
angular.module('openproject.viewModels') angular.module('openproject.viewModels')
.constant('MAX_AUTOCOMPLETER_ADDITION_ITERATIONS', 3)
.factory('CommonRelationsHandler', [ .factory('CommonRelationsHandler', [
'$timeout', '$timeout',
'WorkPackageService', 'WorkPackageService',
'ApiHelper', 'ApiNotificationsService',
'PathHelper', require('./common-relations-handler')
'MAX_AUTOCOMPLETER_ADDITION_ITERATIONS', require(
'./common-relations-handler')
]) ])
.factory('ChildrenRelationsHandler', ['PathHelper', 'CommonRelationsHandler', .factory('ChildrenRelationsHandler', [
'CommonRelationsHandler',
'WorkPackageService', 'WorkPackageService',
'ApiNotificationsService',
require('./children-relations-handler') require('./children-relations-handler')
]) ])
.factory('ParentRelationsHandler', ['CommonRelationsHandler', .factory('ParentRelationsHandler', [
'WorkPackageService', 'ApiHelper', 'CommonRelationsHandler',
'WorkPackageService',
'ApiNotificationsService',
require('./parent-relations-handler') require('./parent-relations-handler')
]); ]);

@ -26,7 +26,7 @@
// See doc/COPYRIGHT.rdoc for more details. // See doc/COPYRIGHT.rdoc for more details.
//++ //++
module.exports = function(CommonRelationsHandler, WorkPackageService, ApiHelper) { module.exports = function(CommonRelationsHandler, WorkPackageService, ApiNotificationsService) {
function ParentRelationsHandler(workPackage, parents, relationsId) { function ParentRelationsHandler(workPackage, parents, relationsId) {
var relations = parents.filter(function(parent) { var relations = parents.filter(function(parent) {
return parent.props.id !== workPackage.props.id; return parent.props.id !== workPackage.props.id;
@ -52,7 +52,7 @@ module.exports = function(CommonRelationsHandler, WorkPackageService, ApiHelper)
scope.updateFocus(-1); scope.updateFocus(-1);
scope.$emit('workPackageRefreshRequired'); scope.$emit('workPackageRefreshRequired');
}, function(error) { }, function(error) {
ApiHelper.handleError(scope, error); ApiNotificationsService.addError(error);
}); });
}; };
handler.removeRelation = function(scope) { handler.removeRelation = function(scope) {
@ -69,7 +69,7 @@ module.exports = function(CommonRelationsHandler, WorkPackageService, ApiHelper)
scope.updateFocus(index); scope.updateFocus(index);
scope.$emit('workPackageRefreshRequired'); scope.$emit('workPackageRefreshRequired');
}, function(error) { }, function(error) {
ApiHelper.handleError(scope, error); ApiNotificationsService.addError(error);
}); });
}; };

@ -85,7 +85,6 @@ See doc/COPYRIGHT.rdoc for more details.
<h1 class="hidden-for-sighted">Content</h1> <h1 class="hidden-for-sighted">Content</h1>
<flash-message></flash-message>
<div ui-view></div> <div ui-view></div>
<div style="clear:both;">&nbsp;</div> <div style="clear:both;">&nbsp;</div>

@ -0,0 +1,91 @@
//-- copyright
// OpenProject is a project management system.
// Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License version 3.
//
// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
// Copyright (C) 2006-2013 Jean-Philippe Lang
// Copyright (C) 2010-2013 the ChiliProject Team
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
//
// See doc/COPYRIGHT.rdoc for more details.
//++
describe('NotificationsService', function() {
'use strict';
var ApiNotificationsService,
NotificationsService,
ApiHelper;
beforeEach(module('openproject.services'));
beforeEach(inject(function(_ApiNotificationsService_, _NotificationsService_, _ApiHelper_){
ApiNotificationsService = _ApiNotificationsService_;
NotificationsService = _NotificationsService_;
ApiHelper = _ApiHelper_;
}));
describe('#addError', function() {
var error = {},
messages = [];
beforeEach(function() {
sinon.spy(NotificationsService, 'addError');
});
describe('with only one error message', function() {
beforeEach(function() {
messages = ['Oh my - Error'];
ApiHelper.getErrorMessages = sinon.stub().returns(messages);
});
it('adds the error to the notification service', function() {
ApiNotificationsService.addError(error);
expect(NotificationsService.addError).to.have.been.calledWith('Oh my - Error');
});
});
describe('with multiple error messages', function() {
beforeEach(function() {
messages = ['Oh my - Error', 'Hey you - Error'];
ApiHelper.getErrorMessages = sinon.stub().returns(messages);
});
it('adds the error to the notification service', function() {
ApiNotificationsService.addError(error);
expect(NotificationsService.addError).to.have.been.calledWith('', messages);
});
});
});
describe('#addSuccess', function() {
var message = 'Great success';
beforeEach(function() {
sinon.spy(NotificationsService, 'addSuccess');
});
it('delegates to NotificationService', function() {
ApiNotificationsService.addSuccess(message);
expect(NotificationsService.addSuccess).to.have.been.calledWith(message);
});
});
});

@ -48,18 +48,6 @@ describe('NotificationsService', function() {
expect(notification).to.eql({ message: 'warning!', type: 'warning' }); expect(notification).to.eql({ message: 'warning!', type: 'warning' });
}); });
it('should throw an Error if trying to create an error without errors', function() {
expect(function() {
NotificationsService.addError('error!');
}).to.throw(Error);
});
it('should throw an Error if trying to create an upload without uploads', function() {
expect(function() {
NotificationsService.addWorkPackageUpload('themUploads');
}).to.throw(Error);
});
it('should be able to create error messages with errors', function() { it('should be able to create error messages with errors', function() {
var notification = NotificationsService.addError('a super cereal error', ['fooo', 'baarr']); var notification = NotificationsService.addError('a super cereal error', ['fooo', 'baarr']);
expect(notification).to.eql({ expect(notification).to.eql({
@ -69,7 +57,16 @@ describe('NotificationsService', function() {
}); });
}); });
it('should be able to create error messages with errors', function() { it('should be able to create error messages with only a message', function() {
var notification = NotificationsService.addError('a super cereal error');
expect(notification).to.eql({
message: 'a super cereal error',
errors: [],
type: 'error'
});
});
it('should be able to create upload messages with uploads', function() {
var notification = NotificationsService.addWorkPackageUpload('uploading...', [0, 1, 2]); var notification = NotificationsService.addWorkPackageUpload('uploading...', [0, 1, 2]);
expect(notification).to.eql({ expect(notification).to.eql({
message: 'uploading...', message: 'uploading...',
@ -77,4 +74,10 @@ describe('NotificationsService', function() {
uploads: [0, 1, 2] uploads: [0, 1, 2]
}); });
}); });
it('should throw an Error if trying to create an upload without uploads', function() {
expect(function() {
NotificationsService.addWorkPackageUpload('themUploads');
}).to.throw(Error);
});
}); });

@ -1,126 +0,0 @@
//-- copyright
// OpenProject is a project management system.
// Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License version 3.
//
// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
// Copyright (C) 2006-2013 Jean-Philippe Lang
// Copyright (C) 2010-2013 the ChiliProject Team
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
//
// See doc/COPYRIGHT.rdoc for more details.
//++
/*jshint expr: true*/
describe('flashMessage Directive', function() {
var compile, element, rootScope, scope;
beforeEach(angular.mock.module('openproject.uiComponents', function($provide) {
var configurationService = {};
configurationService.accessibilityModeEnabled = sinon.stub().returns(true);
$provide.constant('ConfigurationService', configurationService);
}));
beforeEach(module('openproject.templates'));
beforeEach(inject(function($rootScope, $compile) {
var html = '<flash-message></flash-message>';
element = angular.element(html);
rootScope = $rootScope;
scope = $rootScope.$new();
compile = function() {
$compile(element)(scope);
scope.$digest();
};
}));
context('with no message', function() {
beforeEach(function() {
compile();
});
it('should render no message initially', function() {
expect(element.text()).to.be.equal('');
});
it('should be hidden', function() {
expect(element.hasClass('ng-hide')).to.be.true;
});
});
context('with flash messages', function() {
beforeEach(function() {
compile();
});
describe('info message', function() {
var message = {
text: 'für deine Informationen',
isError: false
};
beforeEach(function() {
rootScope.$emit('flashMessage', message);
scope.$apply();
});
it('should render message', function() {
expect(element.text().trim()).to.equal('für deine Informationen');
});
it('should be visible', function() {
expect(element.hasClass('ng-hide')).to.be.false;
});
it('should style as an info message', function() {
expect(element.attr('class').split(' ')).to
.include.members(['flash', 'icon-notice', 'notice']);
});
});
describe('error message', function() {
var message = {
text: '¡Alerta! WARNING! Achtung!',
isError: true
};
beforeEach(function() {
rootScope.$emit('flashMessage', message);
scope.$apply();
});
it('should render message', function() {
expect(element.text().trim()).to.equal('¡Alerta! WARNING! Achtung!');
});
it('should be visible', function() {
expect(element.hasClass('ng-hide')).to.be.false;
});
it('should style as an error message', function() {
expect(element.attr('class').split(' ')).to
.include.members(['flash', 'icon-errorExplanation', 'errorExplanation']);
});
});
});
});

@ -29,7 +29,7 @@
/*jshint expr: true*/ /*jshint expr: true*/
describe('SettingsModalController', function() { describe('SettingsModalController', function() {
var scope, $q, defer, settingsModal, QueryService; var scope, $q, defer, settingsModal, QueryService, NotificationsService;
var ctrl, buildController; var ctrl, buildController;
beforeEach(module('openproject.workPackages.controllers')); beforeEach(module('openproject.workPackages.controllers'));
@ -51,12 +51,16 @@ describe('SettingsModalController', function() {
}; };
settingsModal = { deactivate: angular.noop }; settingsModal = { deactivate: angular.noop };
NotificationsService = {
addSuccess: function() {}
};
buildController = function() { buildController = function() {
ctrl = $controller("SettingsModalController", { ctrl = $controller("SettingsModalController", {
$scope: scope, $scope: scope,
settingsModal: settingsModal, settingsModal: settingsModal,
QueryService: QueryService QueryService: QueryService,
NotificationsService: NotificationsService
}); });
}; };
})); }));
@ -68,9 +72,14 @@ describe('SettingsModalController', function() {
sinon.spy(scope, "$emit"); sinon.spy(scope, "$emit");
sinon.spy(settingsModal, "deactivate"); sinon.spy(settingsModal, "deactivate");
sinon.spy(QueryService, "updateHighlightName"); sinon.spy(QueryService, "updateHighlightName");
sinon.spy(NotificationsService, 'addSuccess');
scope.updateQuery(); scope.updateQuery();
defer.resolve({status: "Query updated!"}); defer.resolve({
status: {
text: 'Query updated!'
}
});
scope.$digest(); scope.$digest();
}); });
@ -79,9 +88,8 @@ describe('SettingsModalController', function() {
expect(settingsModal.deactivate).to.have.been.called; expect(settingsModal.deactivate).to.have.been.called;
}); });
it('should update the flash message', function() { it('should notfify success', function() {
expect(scope.$emit).to.have.been.calledWith("flashMessage", expect(NotificationsService.addSuccess).to.have.been.calledWith('Query updated!');
"Query updated!");
}); });
it ('should update the query menu name', function() { it ('should update the query menu name', function() {

@ -47,11 +47,12 @@ describe('API helper', function() {
return error; return error;
} }
describe('.getErrorMessages', function() {
describe('500', function() { describe('500', function() {
var error = createErrorObject(500, "Internal Server Error"); var error = createErrorObject(500, 'Internal Server Error');
it('should return status text', function() { it('should return status text in an array', function() {
expect(ApiHelper.getErrorMessage(error)).to.eq(error.statusText); expect(ApiHelper.getErrorMessages(error)).to.eql([error.statusText]);
}); });
}); });
@ -60,7 +61,9 @@ describe('API helper', function() {
var apiError = {}; var apiError = {};
apiError._type = 'Error'; apiError._type = 'Error';
apiError.errorIdentifier = (multiple) ? 'urn:openproject-org:api:v3:errors:MultipleErrors' : errorIdentifier; apiError.errorIdentifier = (multiple) ?
'urn:openproject-org:api:v3:errors:MultipleErrors' :
errorIdentifier;
apiError.message = message; apiError.message = message;
if (multiple) { if (multiple) {
@ -79,8 +82,8 @@ describe('API helper', function() {
var error = createErrorObject(404, null, JSON.stringify(apiError)); var error = createErrorObject(404, null, JSON.stringify(apiError));
var expectedResult = 'Not found.'; var expectedResult = 'Not found.';
it('should return api error message', function() { it('should return api error message in an arry', function() {
expect(ApiHelper.getErrorMessage(error)).to.eq(expectedResult); expect(ApiHelper.getErrorMessages(error)).to.eql([expectedResult]);
}); });
}); });
@ -89,11 +92,11 @@ describe('API helper', function() {
var apiError = createApiErrorObject('PropertyIsReadOnly', errorMessage, true); var apiError = createApiErrorObject('PropertyIsReadOnly', errorMessage, true);
var error = createErrorObject(404, null, JSON.stringify(apiError)); var error = createErrorObject(404, null, JSON.stringify(apiError));
it('should return concatenated api error messages', function() { it('should return an array of messages', function() {
var messages = []; var expectedResult = [errorMessage, errorMessage];
var expectedResult = errorMessage + ' ' + errorMessage;
expect(ApiHelper.getErrorMessage(error)).to.eq(expectedResult); expect(ApiHelper.getErrorMessages(error)).to.eql(expectedResult);
});
}); });
}); });
}); });

@ -27,6 +27,7 @@
#++ #++
require 'spec_helper' require 'spec_helper'
require 'features/page_objects/notification'
require 'features/work_packages/shared_contexts' require 'features/work_packages/shared_contexts'
require 'features/work_packages/work_packages_page' require 'features/work_packages/work_packages_page'
@ -34,6 +35,7 @@ feature 'Query menu items' do
let(:user) { FactoryGirl.create :admin } let(:user) { FactoryGirl.create :admin }
let(:project) { FactoryGirl.create :project } let(:project) { FactoryGirl.create :project }
let(:work_packages_page) { WorkPackagesPage.new(project) } let(:work_packages_page) { WorkPackagesPage.new(project) }
let(:notification) { PageObjects::Notifications.new(page) }
def visit_index_page(query) def visit_index_page(query)
work_packages_page.select_query(query) work_packages_page.select_query(query)
@ -72,7 +74,7 @@ feature 'Query menu items' do
check 'show_in_menu' check 'show_in_menu'
click_on 'Save' click_on 'Save'
expect(page).to have_selector('.flash', text: 'Successful update') notification.expect_success('Successful update')
expect(page).to have_selector('a', text: query.name) expect(page).to have_selector('a', text: query.name)
end end
@ -103,7 +105,7 @@ feature 'Query menu items' do
end end
it 'displaying a success message', js: true do it 'displaying a success message', js: true do
expect(page).to have_selector('.flash', text: 'Successful update') notification.expect_success('Successful update')
end end
it 'is renaming and reordering the list', js: true do it 'is renaming and reordering the list', js: true do

@ -0,0 +1,43 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
module PageObjects
class Notifications
include RSpec::Matchers
attr_accessor :page
def initialize(page)
@page = page
end
def expect_success(message)
expect(page).to have_selector('.notification-box.-success', text: message)
end
end
end
Loading…
Cancel
Save