import EmberObject from '@ember/object'; import sinon from 'sinon'; import {AjaxError, InvalidError} from 'ember-ajax/errors'; import {ServerUnreachableError} from 'ghost-admin/services/ajax'; import {describe, it} from 'mocha'; import {A as emberA} from '@ember/array'; import {expect} from 'chai'; import {run} from '@ember/runloop'; import {setupTest} from 'ember-mocha'; import {GENERIC_ERROR_MESSAGE} from 'ghost-admin/services/notifications'; // notifications service determines if a notification is a model instance by // checking `notification.constructor.modelName === 'notification'` const NotificationStub = EmberObject.extend(); NotificationStub.modelName = 'notification'; describe('Unit: Service: notifications', function () { setupTest(); beforeEach(function () { this.owner.lookup('service:notifications').set('content', emberA()); this.owner.lookup('service:notifications').set('delayedNotifications', emberA()); }); it('filters alerts/notifications', function () { let notifications = this.owner.lookup('service:notifications'); // wrapped in run-loop to enure alerts/notifications CPs are updated run(() => { notifications.showAlert('Alert'); notifications.showNotification('Notification'); }); expect(notifications.alerts.length).to.equal(1); expect(notifications.alerts.firstObject.message).to.equal('Alert'); expect(notifications.notifications.length).to.equal(1); expect(notifications.notifications.firstObject.message).to.equal('Notification'); }); it('#handleNotification deals with DS.Notification notifications', function () { let notifications = this.owner.lookup('service:notifications'); let notification = NotificationStub.create({message: '

Test

', status: 'alert'}); notifications.handleNotification(notification); notification = notifications.alerts[0]; // alerts received from the server should be marked html safe expect(notification.message).to.have.property('toHTML'); }); it('#handleNotification defaults to notification if no status supplied', function () { let notifications = this.owner.lookup('service:notifications'); notifications.handleNotification({message: 'Test'}, false); expect(notifications.content) .to.deep.include({message: 'Test', status: 'notification'}); }); it('#handleNotification shows generic error message when a word matches built-in error type', function () { let notifications = this.owner.lookup('service:notifications'); notifications.handleNotification({message: 'TypeError test'}); expect(notifications.content[0].message).to.equal(GENERIC_ERROR_MESSAGE); notifications.clearAll(); expect(notifications.content.length).to.equal(0); notifications.handleNotification({message: 'TypeError: Testing'}); expect(notifications.content[0].message).to.equal(GENERIC_ERROR_MESSAGE); notifications.clearAll(); notifications.handleNotification({message: 'Unknown error - TypeError, cannot save invite.'}); expect(notifications.content[0].message).to.equal(GENERIC_ERROR_MESSAGE); }); it('#showAlert adds POJO alerts', function () { let notifications = this.owner.lookup('service:notifications'); run(() => { notifications.showAlert('Test Alert', {type: 'error'}); }); expect(notifications.alerts) .to.deep.include({message: 'Test Alert', status: 'alert', type: 'error', key: undefined, actions: undefined, description: undefined, icon: undefined}); }); it('#showAlert adds delayed notifications', function () { let notifications = this.owner.lookup('service:notifications'); run(() => { notifications.showNotification('Test Alert', {type: 'error', delayed: true}); }); expect(notifications.delayedNotifications) .to.deep.include({message: 'Test Alert', status: 'notification', type: 'error', key: undefined, actions: undefined, description: undefined, icon: undefined}); }); // in order to cater for complex keys that are suitable for i18n // we split on the second period and treat the resulting base as // the key for duplicate checking it('#showAlert clears duplicates using keys', function () { let notifications = this.owner.lookup('service:notifications'); run(() => { notifications.showAlert('Kept'); notifications.showAlert('Duplicate', {key: 'duplicate.key.fail'}); }); expect(notifications.alerts.length).to.equal(2); run(() => { notifications.showAlert('Duplicate with new message', {key: 'duplicate.key.success'}); }); expect(notifications.alerts.length).to.equal(2); expect(notifications.alerts.lastObject.message).to.equal('Duplicate with new message'); }); it('#showAlert clears duplicates using message text', function () { let notifications = this.owner.lookup('service:notifications'); notifications.showAlert('Not duplicate'); notifications.showAlert('Duplicate', {key: 'duplicate'}); notifications.showAlert('Duplicate'); expect(notifications.alerts.length).to.equal(2); expect(notifications.alerts.lastObject.key).to.not.exist; }); it('#showNotification adds POJO notifications', function () { let notifications = this.owner.lookup('service:notifications'); run(() => { notifications.showNotification('Test Notification', {type: 'success'}); }); expect(notifications.notifications) .to.deep.include({message: 'Test Notification', status: 'notification', type: 'success', key: undefined, actions: undefined, description: undefined, icon: undefined}); }); it('#showNotification adds delayed notifications', function () { let notifications = this.owner.lookup('service:notifications'); run(() => { notifications.showNotification('Test Notification', {delayed: true}); }); expect(notifications.delayedNotifications) .to.deep.include({message: 'Test Notification', status: 'notification', type: undefined, key: undefined, actions: undefined, description: undefined, icon: undefined}); }); it('#showAPIError handles single json response error', function () { let notifications = this.owner.lookup('service:notifications'); let error = new AjaxError({errors: [{message: 'Single error'}]}); run(() => { notifications.showAPIError(error); }); let [alert] = notifications.alerts; expect(alert.message).to.equal('Single error'); expect(alert.status).to.equal('alert'); expect(alert.type).to.equal('error'); expect(alert.key).to.equal('api-error'); }); it('#showAPIError handles multiple json response errors', function () { let notifications = this.owner.lookup('service:notifications'); let error = new AjaxError({errors: [ {title: 'First error', message: 'First error message'}, {title: 'Second error', message: 'Second error message'} ]}); run(() => { notifications.showAPIError(error); }); expect(notifications.alerts.length).to.equal(2); let [alert1, alert2] = notifications.alerts; expect(alert1).to.deep.equal({message: 'First error message', status: 'alert', type: 'error', key: 'api-error.first-error', actions: undefined, description: undefined, icon: undefined}); expect(alert2).to.deep.equal({message: 'Second error message', status: 'alert', type: 'error', key: 'api-error.second-error', actions: undefined, description: undefined, icon: undefined}); }); it('#showAPIError displays default error text if response has no error/message', function () { let notifications = this.owner.lookup('service:notifications'); let resp = false; run(() => { notifications.showAPIError(resp); }); expect(notifications.content).to.deep.equal([ {message: GENERIC_ERROR_MESSAGE, status: 'alert', type: 'error', key: 'api-error', actions: undefined, description: undefined, icon: undefined} ]); notifications.set('content', emberA()); run(() => { notifications.showAPIError(resp, {defaultErrorText: 'Overridden default'}); }); expect(notifications.content).to.deep.equal([ {message: 'Overridden default', status: 'alert', type: 'error', key: 'api-error', actions: undefined, description: undefined, icon: undefined} ]); }); it('#showAPIError sets correct key when passed a base key', function () { let notifications = this.owner.lookup('service:notifications'); run(() => { notifications.showAPIError('Test', {key: 'test.alert'}); }); expect(notifications.alerts.firstObject.key).to.equal('api-error.test.alert'); }); it('#showAPIError sets correct key when not passed a key', function () { let notifications = this.owner.lookup('service:notifications'); run(() => { notifications.showAPIError('Test'); }); expect(notifications.alerts.firstObject.key).to.equal('api-error'); }); it('#showAPIError parses default ember-ajax errors correctly', function () { let notifications = this.owner.lookup('service:notifications'); let error = new InvalidError(); run(() => { notifications.showAPIError(error); }); let notification = notifications.alerts.firstObject; expect(notification.message).to.equal('Request was rejected because it was invalid'); expect(notification.status).to.equal('alert'); expect(notification.type).to.equal('error'); expect(notification.key).to.equal('api-error'); }); it('#showAPIError parses custom ember-ajax errors correctly', function () { let notifications = this.owner.lookup('service:notifications'); let error = new ServerUnreachableError(); run(() => { notifications.showAPIError(error); }); let notification = notifications.alerts.firstObject; expect(notification.message).to.equal('Server was unreachable'); expect(notification.status).to.equal('alert'); expect(notification.type).to.equal('error'); expect(notification.key).to.equal('api-error'); }); it('#showAPIError adds error context to message if available', function () { let notifications = this.owner.lookup('service:notifications'); let error = new AjaxError({errors: [{ message: 'Authorization Error.', context: 'Please sign in.' }]}); run(() => { notifications.showAPIError(error); }); let [alert] = notifications.alerts; expect(alert.message).to.equal('Authorization Error. Please sign in.'); expect(alert.status).to.equal('alert'); expect(alert.type).to.equal('error'); expect(alert.key).to.equal('api-error'); }); it('#showAPIError does not add context to message if it duplicates the message', function () { let notifications = this.owner.lookup('service:notifications'); let error = new AjaxError({errors: [{ message: 'Authorization Error.', context: 'Authorization Error.' }]}); run(() => { notifications.showAPIError(error); }); let [alert] = notifications.alerts; expect(alert.message).to.equal('Authorization Error.'); expect(alert.status).to.equal('alert'); expect(alert.type).to.equal('error'); expect(alert.key).to.equal('api-error'); }); it('#showAPIError shows generic error for built-in error types', function () { let notifications = this.owner.lookup('service:notifications'); const error = new TypeError('Testing'); notifications.showAPIError(error); expect(notifications.alerts[0].message).to.equal(GENERIC_ERROR_MESSAGE); }); it('#displayDelayed moves delayed notifications into content', function () { let notifications = this.owner.lookup('service:notifications'); run(() => { notifications.showNotification('First', {delayed: true}); notifications.showNotification('Second', {delayed: true}); notifications.showNotification('Third', {delayed: false}); notifications.displayDelayed(); }); expect(notifications.notifications).to.deep.equal([ {message: 'Third', status: 'notification', type: undefined, key: undefined, actions: undefined, description: undefined, icon: undefined}, {message: 'First', status: 'notification', type: undefined, key: undefined, actions: undefined, description: undefined, icon: undefined}, {message: 'Second', status: 'notification', type: undefined, key: undefined, actions: undefined, description: undefined, icon: undefined} ]); }); it('#closeNotification removes POJO notifications', function () { let notification = {message: 'Close test', status: 'notification'}; let notifications = this.owner.lookup('service:notifications'); run(() => { notifications.handleNotification(notification); }); expect(notifications.notifications) .to.include(notification); run(() => { notifications.closeNotification(notification); }); expect(notifications.notifications) .to.not.include(notification); }); it('#closeNotification removes and deletes DS.Notification records', function () { let notifications = this.owner.lookup('service:notifications'); let notification = NotificationStub.create({message: 'Close test', status: 'alert'}); notification.deleteRecord = function () {}; sinon.spy(notification, 'deleteRecord'); notification.save = function () { return { finally(callback) { return callback(notification); } }; }; sinon.spy(notification, 'save'); run(() => { notifications.handleNotification(notification); }); expect(notifications.alerts).to.include(notification); run(() => { notifications.closeNotification(notification); }); expect(notification.deleteRecord.calledOnce).to.be.true; expect(notification.save.calledOnce).to.be.true; expect(notifications.alerts).to.not.include(notification); }); it('#closeNotifications only removes notifications', function () { let notifications = this.owner.lookup('service:notifications'); run(() => { notifications.showAlert('First alert'); notifications.showNotification('First notification'); notifications.showNotification('Second notification'); }); expect(notifications.alerts.length, 'alerts count').to.equal(1); expect(notifications.notifications.length, 'notifications count').to.equal(2); run(() => { notifications.closeNotifications(); }); expect(notifications.alerts.length, 'alerts count').to.equal(1); expect(notifications.notifications.length, 'notifications count').to.equal(0); }); it('#closeNotifications only closes notifications with specified key', function () { let notifications = this.owner.lookup('service:notifications'); run(() => { notifications.showAlert('First alert'); // using handleNotification as showNotification will auto-prune duplicates notifications.handleNotification({message: 'First notification', key: 'test.close', status: 'notification'}); notifications.handleNotification({message: 'Second notification', key: 'test.keep', status: 'notification'}); notifications.handleNotification({message: 'Third notification', key: 'test.close', status: 'notification'}); }); run(() => { notifications.closeNotifications('test.close'); }); expect(notifications.notifications.length, 'notifications count').to.equal(1); expect(notifications.notifications.firstObject.message, 'notification message').to.equal('Second notification'); expect(notifications.alerts.length, 'alerts count').to.equal(1); }); it('#clearAll removes everything without deletion', function () { let notifications = this.owner.lookup('service:notifications'); let notificationModel = EmberObject.create({message: 'model'}); notificationModel.deleteRecord = function () {}; sinon.spy(notificationModel, 'deleteRecord'); notificationModel.save = function () { return { finally(callback) { return callback(notificationModel); } }; }; sinon.spy(notificationModel, 'save'); notifications.handleNotification(notificationModel); notifications.handleNotification({message: 'pojo'}); notifications.clearAll(); expect(notifications.content).to.be.empty; expect(notificationModel.deleteRecord.called).to.be.false; expect(notificationModel.save.called).to.be.false; }); it('#closeAlerts only removes alerts', function () { let notifications = this.owner.lookup('service:notifications'); notifications.showNotification('First notification'); notifications.showAlert('First alert'); notifications.showAlert('Second alert'); run(() => { notifications.closeAlerts(); }); expect(notifications.alerts.length).to.equal(0); expect(notifications.notifications.length).to.equal(1); }); it('#closeAlerts closes only alerts with specified key', function () { let notifications = this.owner.lookup('service:notifications'); notifications.showNotification('First notification'); notifications.showAlert('First alert', {key: 'test.close'}); notifications.showAlert('Second alert', {key: 'test.keep'}); notifications.showAlert('Third alert', {key: 'test.close'}); run(() => { notifications.closeAlerts('test.close'); }); expect(notifications.alerts.length).to.equal(1); expect(notifications.alerts.firstObject.message).to.equal('Second alert'); expect(notifications.notifications.length).to.equal(1); }); });