Skip to content

Commit

Permalink
Merge branch 'preload-image'
Browse files Browse the repository at this point in the history
  • Loading branch information
LouisBarranqueiro committed Jul 27, 2016
2 parents daefcd6 + 2515227 commit 09c2742
Show file tree
Hide file tree
Showing 6 changed files with 158 additions and 16 deletions.
14 changes: 14 additions & 0 deletions src/helpers/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,17 @@ export function treatNotification(notification) {
}
return notification;
}

/**
* Preload an image
* @param {String} url url of image to load
* @param {Function} onload Function called when image is loaded
* @returns {void}
*/
export function preloadImage(url, onload) {
var image = new Image();
image.src = url;
image.onload = onload;
return image;
}

50 changes: 41 additions & 9 deletions src/store/notifications.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {handleActions, createAction} from 'redux-actions';
import {treatNotification} from '../helpers';
import {treatNotification, preloadImage} from '../helpers';

// An array to store notifications object
const INITIAL_DATA = [];
// Action types
Expand All @@ -18,20 +19,53 @@ const REMOVE_NOTIFICATION = 'REMOVE_NOTIFICATION';
export const addNotification = (notification) => (dispatch) => {
notification.id = new Date().getTime();
notification = treatNotification(notification);
dispatch(_addNotification(notification));
// if there is an image, we preload it
// and add notification when image is loaded
if (notification.image) {
preloadImage(notification.image, dispatch.bind(this, _addNotification(notification)));
}
else {
dispatch(_addNotification(notification));
}
return notification;
};

// Add a notification (action creator)
const _addNotification = createAction(ADD_NOTIFICATION);

// Update a notification (action creator)
export const updateNotification = createAction(UPDATE_NOTIFICATION, (notification) => {
/**
* Update a notification (thunk action creator)
*
* We use a thunk here to create an UPDATE_NOTIFICATION action
* and only return the notification object.
* @param {Object} notification
* @returns {Object} notification
*/
export const updateNotification = (notification) => (dispatch, getState) => {
if (!notification.id) {
throw new Error('A notification must have an `id` property to be updated');
}
return treatNotification(notification);
});

const notifications = getState().notifications;
const index = notifications.findIndex((oldNotification) => oldNotification.id === notification.id);
const currNotification = notifications[index];

notification = treatNotification(notification);

// if image is different, then we preload it
// and update notification when image is loaded
if (notification.image && (!currNotification.image || (currNotification.image &&
notification.image !== currNotification.image))) {
preloadImage(notification.image, dispatch.bind(this, _updateNotification(notification)));
}
else {
dispatch(_updateNotification(notification));
}
return notification;
};

// Update a notification (action creator)
const _updateNotification = createAction(UPDATE_NOTIFICATION);

// Remove a notification (action creator)
export const removeNotification = createAction(REMOVE_NOTIFICATION);
Expand All @@ -57,9 +91,7 @@ export default handleActions({
},
[UPDATE_NOTIFICATION]: (state, {payload}) => {
// get index of the notification
const index = state.findIndex((notification) => {
return notification.id === payload.id;
});
const index = state.findIndex((notification) => notification.id === payload.id);
// replace the old notification by the new one
state[index] = Object.assign({}, payload);
return [...state];
Expand Down
1 change: 1 addition & 0 deletions test/helpers/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ describe('Helpers', () => {
require('./convertStatus.test.js');
require('./Timer.test.js');
require('./treatNotification.test.js');
require('./preloadImage.test.js');
});
22 changes: 22 additions & 0 deletions test/helpers/preloadImage.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import {preloadImage} from '../../src/helpers';

describe('preloadImage', () => {
it('should preload image', () => {
const url = 'http://placehold.it/40x40';
const image = preloadImage(url, function() {
return 1;
});
expect(image.nodeName.toLowerCase()).toEqual('img');
expect(image.src).toEqual(url);
expect(image.onload()).toEqual(1);
});

it('should preload image (call `onload` event)', (done) => {
const spy = expect.createSpy();
preloadImage('http://placehold.it/40x40', spy);
setTimeout(() => {
expect(spy).toHaveBeenCalled();
done();
}, 500);
});
});
85 changes: 79 additions & 6 deletions test/store/notifications.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ describe('notifications', () => {
notification.id = null;
// we remove the image, otherwise `treatNotification()` helper will update
// status of notification
notification.image = '';
notification.image = null;
// here we simulate an HTTP success status code (200 = OK)
notification.status = 200;
const notificationAdded = store.dispatch(addNotification(notification));
Expand All @@ -42,9 +42,28 @@ describe('notifications', () => {
expect(store.getActions()).toEqual(expectedAction);
});

it('should load image then create an action to add a notification (new image)', (done) => {
const notificationAdded = store.dispatch(addNotification(notification));
const expectedAction = [{
type: types.ADD_NOTIFICATION,
payload: notificationAdded
}];

// image not loaded yet, so store should be empty
expect(store.getActions()).toEqual([]);

setTimeout(() => {
// image should be loaded now, so store should contains the notification updated
expect(store.getActions()).toEqual(expectedAction);
done();
}, 500);
});

it('should create an action to add a notification ' +
'(add `id` property and don\'t convert status)', () => {
notification.id = null;
// we remove image to not wait loading of image (preload feature)
notification.image = null;
const notificationAdded = store.dispatch(addNotification(notification));
const expectedAction = [{
type: types.ADD_NOTIFICATION,
Expand All @@ -57,25 +76,79 @@ describe('notifications', () => {
});

describe('updateNotification()', () => {
it('should create an action to update a notification', () => {
const expectedAction = {
let store = null;

beforeEach('init store', () => {
store = mockStore({notifications: [notification]});
});

it('shouldn\'t wait to load image and create an action to update a notification (same image)', () => {
const expectedAction = [{
type: types.UPDATE_NOTIFICATION,
payload: notification
};
expect(updateNotification(notification)).toEqual(expectedAction);
}];

store.dispatch(updateNotification(notification));
expect(store.getActions()).toEqual(expectedAction);
});

it('shouldn\'t create an action to update a notification ' +
'(notification without `id` property)', () => {
notification.id = null;
// we remove image to not wait loading of image (preload feature)
notification.image = null;
const expectedAction = {
type: types.UPDATE_NOTIFICATION,
payload: notification
};
expect(updateNotification.bind(updateNotification, notification))

expect(store.dispatch.bind(store, updateNotification(notification)))
.toThrow('A notification must have an `id` property to be updated')
.toNotEqual(expectedAction);
});

it('should load image then create an action to update a notification (new image)', (done) => {
// add a notification without image in the store
notification = genNotification({image: null});
store = mockStore({notifications: [notification]});
// update notification with an image
const notificationUpdated = Object.assign({}, notification, {image: 'http://placehold.it/45x45'});
const expectedAction = [{
type: types.UPDATE_NOTIFICATION,
payload: notificationUpdated
}];

expect(store.getActions()).toEqual([]);
store.dispatch(updateNotification(notificationUpdated));
// image not loaded yet, so store should be empty
expect(store.getActions()).toEqual([]);

setTimeout(() => {
// image should be loaded now, so store should contains the notification updated
expect(store.getActions()).toEqual(expectedAction);
done();
}, 500);
});

it('should load image then create an action to update a notification (image is different)', (done) => {
// update notification image url
const notificationUpdated = Object.assign({}, notification, {image: 'http://placehold.it/45x45'});
const expectedAction = [{
type: types.UPDATE_NOTIFICATION,
payload: notificationUpdated
}];

expect(store.getActions()).toEqual([]);
store.dispatch(updateNotification(notificationUpdated));
// image not loaded yet, so store should be empty
expect(store.getActions()).toEqual([]);

setTimeout(() => {
// image should be loaded now, so store should contains the notification updated
expect(store.getActions()).toEqual(expectedAction);
done();
}, 500);
});
});

describe('removeNotification()', () => {
Expand Down
2 changes: 1 addition & 1 deletion test/utils/fixtures.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export function genNotification(notification = {}) {
id: faker.random.number(),
title: faker.lorem.sentence(),
message: faker.lorem.sentence(),
image: faker.lorem.words(),
image: 'http://placehold.it/40x40',
position: faker.random.objectElement(POSITIONS),
status: faker.random.objectElement(STATUS),
dismissible: faker.random.boolean(),
Expand Down

0 comments on commit 09c2742

Please sign in to comment.