Skip to content

Instantly share code, notes, and snippets.

@ryyppy
Last active October 30, 2018 13:03
Show Gist options
  • Save ryyppy/484aff4b3da4c045c0e177d27bd00761 to your computer and use it in GitHub Desktop.
Save ryyppy/484aff4b3da4c045c0e177d27bd00761 to your computer and use it in GitHub Desktop.
/* @flow */
import { takeLatest } from 'redux-saga';
import { call, put } from 'redux-saga/effects';
/**
* STATIC ENVIRONMENT
*/
export const config = {
notificationDelay: 5000,
};
/**
* ACTION CONSTANTS
*/
export const FETCH_MESSAGES = 'FETCH_MESSAGES';
export const UPDATE_MESSAGE_LIST = 'UPDATE_MESSAGE_LIST';
export const SHOW_ERROR = 'SHOW_ERROR';
/**
* ACTION-CREATORS
*/
export function showError(msg: string): Action {
return { type: SHOW_ERROR, msg };
}
export function hideError(): Action {
return { type: 'HIDE_ERROR' };
}
export function updateMessageList(
messages: Array<Object>,
sortField: string,
sortAscending: boolean,
paging: PagingParam): Action {
// Cannot infer how this action really looks like ;-)
return {
type: UPDATE_MESSAGE_LIST,
messages,
sortField,
sortAscending,
...paging,
};
}
/**
* UTILITY FUNCTIONS / SIDE-EFFECTS
*/
export function delay(timeout: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, timeout));
}
export function fetchData(
serviceUrl: string,
folderName: string,
sortField: string,
sortAscending: boolean,
page: string): Promise<FetchResult> {
// I allow myself to return a static result,
// we just pretend this will be returned
return Promise.resolve({
messages: [
{ subject: 'foo' },
{ subject: 'bar' },
{ subject: 'baz' },
],
paging: { page, size: 1, total: 1 },
});
}
/**
* TYPE DECLARATIONS
*/
type Action = {
type: string,
}
type Message = {
subject: string,
selected ?: boolean,
};
type SagaParams = {
sortField: string,
sortAscending: boolean,
page: number,
}
type PagingParam = {
page: string,
size: number,
total: number,
};
type FetchResult = {
messages: Array<Message>,
paging: PagingParam,
}
/**
* SAGAS / SAGA WRAPPER
*/
export function *fetchMessagesSaga(
serviceUrl: string,
folderName: string,
{ sortField, sortAscending, page }: SagaParams): Generator<Object, void, FetchResult> {
try {
const { messages, paging } = yield call(
fetchData,
serviceUrl,
folderName,
sortField,
sortAscending,
page
);
const updateMessageListAction = updateMessageList(
messages.map(message => ({ ...message, selected: false })),
sortField,
sortAscending,
{ ...paging, page },
);
yield put(updateMessageListAction);
} catch (e) {
yield put(showError(`Fehler und so: ${e.message}`));
yield call(delay, config.notificationDelay);
yield put(hideError());
}
}
export default function *(serviceUrl: string, folderName: string): Generator<Object, void, FetchResult> {
yield* takeLatest(FETCH_MESSAGES, fetchMessagesSaga, serviceUrl, folderName);
}
/* @flow */
// We don't test the takeLatest wrapper, since it just passes the
// parameters to the fetchMessagesSaga anyways
import { fetchMessagesSaga } from './fetchMessages';
import watchFetchMessageSaga from './fetchMessages';
// Pretend the env to come from a different file
import { config } from './fetchMessages';
// Pretend the constants to come from a different file
import { FETCH_MESSAGES } from './fetchMessages';
// Pretend the action creators to come from a different file
import {
showError,
hideError,
updateMessageList,
} from './fetchMessages';
// Pretend the side-effect fns to come from a different file
import {
delay,
fetchData, // NOTE: Here we use the same real implementation FN
} from '../../src/sagas/fetchMessages';
import { take, put, call, fork } from 'redux-saga/effects';
describe('sagas/fetchMessagesSaga', () => {
const _oldNotificationDelay = config.notificationDelay;
before(() => {
config.notificationDelay = 4000;
});
after(() => {
// That's why I don't like global dependencies ;-)
// especially in my testing framework
config.notificationDelay = _oldNotificationDelay;
});
it('should yield call & put on success', () => {
const iter = fetchMessagesSaga('shmurl', 'shmolder name', { sortField: 'shmort field', sortAscending: true, page: 3 });
let ret = iter.next();
expect(ret.value).to.deep.equal(call(fetchData, 'shmurl', 'shmolder name', 'shmort field', true, 3));
ret = iter.next({
messages: [
{ subject: 'some subject' },
],
paging: {
total: 12,
size: 111,
},
});
const updateMessageListAction = updateMessageList(
[{ subject: 'some subject', selected: false }],
'shmort field',
true,
{ page: 3, total: 12, size: 111 });
expect(ret.value).to.deep.equal(put(updateMessageListAction));
// After the last yield, the result should reach
// the end of the generator, hence .done should be true
ret = iter.next();
expect(ret.done).to.equal(true);
});
it('should yield call, put(err), call(delay) and put(hideError) on error', () => {
const iter = fetchMessagesSaga('shmurl', 'shmolder name', { sortField: 'shmort field', sortAscending: true, page: 3 });
let ret = iter.next();
expect(ret.value).to.deep.equal(call(fetchData, 'shmurl', 'shmolder name', 'shmort field', true, 3));
// Here, we want to provoke a fail for the `call` effect,
// so we use the standardised `throw` iterator function
// This will also automatically yield the next effect
// in our catch() branch
ret = iter.throw(new Error('some IO error'));
expect(ret.value).to.deep.equal(put(showError('Fehler und so: some IO error')));
ret = iter.next();
expect(ret.value).to.deep.equal(call(delay, 4000));
ret = iter.next();
expect(ret.value).to.deep.equal(put(hideError()));
ret = iter.next();
expect(ret.done).to.equal(true);
});
});
/**
* BONUS: Testing the saga wrapper (takeLatest)
*/
describe('sagas/fetchMessagesSaga', () => {
it('should yield forks on take effects', () => {
const iter = watchFetchMessageSaga('shmurl', 'shmolder name', { sortField: 'shmort field', sortAscending: true, page: 3 });
// on initialization, takeLatest yields a take() and fork()
// see the official API for implementation details
let ret = iter.next();
expect(ret.value).to.deep.equal(take(FETCH_MESSAGES));
ret = iter.next({ type: FETCH_MESSAGES });
// takeLatests passes the captured action as last parameter to fetchMessagesSaga
// but the saga doesn't really need it... we just test it for completeness reasons
expect(ret.value).to.deep.equal(fork(fetchMessagesSaga, 'shmurl', 'shmolder name', { type: FETCH_MESSAGES }));
console.log(ret.value);
// takeLatests does a while(true) loop, so we check another time
ret = iter.next();
expect(ret.value).to.deep.equal(take(FETCH_MESSAGES));
// .... and so on and on,...
});
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment