The problem you will initially run into is this: I just want to test an effect, but I don't know how to run the effect on its own. Well, maybe you don't need to run the effect in isolation, and run it through a test app. This will keep your test reflecting the reality of how it is used, while also helping you isolate how the effect affects the state.
Let's take the Custom Effects section example of notificationPermission and notification effects.
Copy import { effects } from 'ferp'
export const notificationPermissionEffect = (
grantEffect ,
denyEffect = effects .none ,
dismissEffect = effects .none ,
errorEffect = effects .none
) => effects .defer (
Notification .requestPermission ()
.then ((result) => {
switch (result) {
case 'granted' :
return grantEffect ();
case 'denied' :
return denyEffect ();
default :
return dismissEffect ();
}
})
.catch (errorEffect)
);
export const notificationEffect = (title , options = {} , resultEffect = effects .none) => effects .thunk (() =>
resultEffect ( new Notification (title , options))
);
In this case, we have some browser specific code (ie the Notification class), so we'll just make a global/controllable mock for it. If you are building a browser-based ferp app, you may want to look into a modern browser based test framework. Let's set up a test using describe/it blocks, similar to what you'd seen in mocha or jest:
notificationEffects.test.js
Copy import { app , effects } from 'ferp' ;
import { notificationPermissionEffect , notificationEffect } from './notificationEffects.js' ;
describe ( 'Notification effects' , () => {
describe ( 'notificationPermissionEffect' , () => {
const injectNotificationClass = (permissionResponse) => {
global .Notification = class {
static requestPermission () {
return permissionResponse;
}
}
};
afterEach (() => {
delete global .Notification;
});
const createTestApp = (onComplete) => {
const setterEffect = (value) => () => value;
const detach = app ({
init : [
null ,
notificationPermissionEffect (
setterEffect ( 'grantEffect' ) ,
setterEffect ( 'denyEffect' ) ,
setterEffect ( 'dismissEffect' ) ,
setterEffect ( 'errorEffect' ) ,
) ,
] ,
update : (message , state) => {
onComplete (message);
detach ();
return [state , effects .none ()];
} ,
});
};
it ( 'calls the grantEffect' , (done) => {
injectNotificationClass ( Promise .resolve ( 'grant' ));
createTestApp ((whichEffect) => {
expect (whichEffect) .toBe ( 'grantEffect' );
done ();
});
});
it ( 'calls the denyEffect' , (done) => {
injectNotificationClass ( Promise .resolve ( 'deny' ));
createTestApp ((whichEffect) => {
expect (whichEffect) .toBe ( 'denyEffect' );
done ();
});
});
it ( 'calls the dismissEffect' , (done) => {
injectNotificationClass ( Promise .resolve ( 'dismiss' ));
createTestApp ((whichEffect) => {
expect (whichEffect) .toBe ( 'dismissEffect' );
done ();
});
});
it ( 'calls the errorEffect' , (done) => {
injectNotificationClass ( Promise .reject ());
createTestApp ((whichEffect) => {
expect (whichEffect) .toBe ( 'errorEffect' );
done ();
});
});
});
describe ( 'notificationEffect' , () => {
const injectNotificationClass = () => {
global .Notification = class {
constructor (title , options) {
this .constructorArgs = [title , options];
}
}
};
afterEach (() => {
delete global .Notification;
});
const createTestApp = (title , options , onComplete) => {
app ({
init : [
null ,
notificationEffect (title , options , onComplete) ,
] ,
});
};
it ( 'creates a notification' , (done) => {
injectNotificationClass ();
createTestApp ( 'Hello world' , { body : 'From Ferp.JS' } , (notification) => {
expect (notification) .toBeInstanceOf (Notification);
expect ( notification .constructorArgs) .toEqual ([ 'Hellow world' , { body : 'From Ferp.JS' }]);
done ();
return effects .none ();
});
});
});
});