I try to use cypress-wait-until for a simple case. https://github.com/NoriSte/cypress-wait-until
Visit a page
Check if an element is here
If not, reload the page until this element is found.
Once found, assert the element exists
Working code (cypress-wait-until not used)
before(() => {
cy.visit('http://localhost:8080/en/registration');
});
describe('Foo', () => {
it('should check that registration button is displayed', () => {
const selector = 'button[data-test=startRegistration-individual-button]';
cy.get(selector).should('exist');
});
});
Not working, timed out retrying
before(() => {
cy.visit('http://localhost:8080/en/registration');
});
describe('Foo', () => {
it('should check that registration button is displayed', () => {
const options = { timeout: 8000, interval: 4000 };
const selector = 'button[data-test=startRegistration-individual-button]';
cy.waitUntil(() => cy.reload().then(() => Cypress.$(selector).length), options);
cy.get(selector).should('exist');
});
});
Not working, see error below
before(() => {
cy.visit('http://localhost:8080/en/registration');
});
describe('Foo', () => {
it('should check that registration button is displayed', () => {
const options = { timeout: 8000, interval: 4000 };
const selector = 'button[data-test=startRegistration-individual-button]';
cy.waitUntil(() => {
cy.reload();
return Cypress.$(selector).length;
}, options);
cy.get(selector).should('exist');
});
For the two versions not working as soon as I remove cy.reload(), it starts to work.
Question
What can I do to make it work with a reload?
EDIT
This command I wrote works correctly.
Cypress.Commands.add('refreshUntil', (selector: string, opts?: { retries: number; waitAfterRefresh: number }) => {
const defaultOptions = {
retries: 10,
waitAfterRefresh: 2500,
};
const options = { ...defaultOptions, ...opts };
function check(selector: string): any {
if (Cypress.$(selector).length) { // Element is there
return true;
}
if (options.retries === 0) {
throw Error(`${selector} not found`);
}
options.retries -= 1;
cy.log(`Element ${selector} not found. Remaining attempts: ${options.retries}`);
cy.reload();
// Wait a some time for the server to respond
return cy.wait(options.waitAfterRefresh).then(() => check(selector));
}
check(parsedSelector);
});
I could see two potential difference with waitUntil from cypress-wait-until
Cypress.$(selector).length would be new on each try
There is a wait time after the reload before checking again if the element is there
EDIT 2
Here is the working solution using cypress-wait-until
cy.waitUntil(() => cy.reload().wait(2500).then(() => Cypress.$(selector).length), options);
Cypress rules apply inside cy.waitUntil() as well as outside so .wait(2500) would be bad practice.
It would be better to change your non-retying Cypress.$(selector).length into a proper Cypress retrying command. That way you get 4 seconds (default) retry but only wait as long as needed.
Particulary since cy.waitUntil() repeats n times, you are hard-waiting (wasting) a lot of seconds.
cy.waitUntil(() => { cy.reload(); cy.get(selector) }, options)
// NB retry period for `cy.get()` is > 2.5 seconds
Here is the working solution using cypress-wait-until
cy.waitUntil(() => cy.reload().wait(2500).then(() => Cypress.$(selector).length), options);
I ended up writing my own method (inspired from cypress-wait-until ) without the need to have a hard wait time
/**
* Run a check, and then refresh wait until an element is displayed.
* Retries for a specified amount of time.
*
* #function
* #param {function} firstCheckFunction - The function to run before checking if the element is displayed.
* #param {string|{ selector: string, type: string }} selector - The selector to search for. Can be a string or an object with selector and type properties.
* #param {WaitUntilOptions} [opts={timeout: 5000, interval: 500}] - The options object, with timeout and interval properties.
* #throws {Error} if the firstWaitFunction parameter is not a function.
* #throws {Error} if the specified element is not found after all retries.
* #example
* cy.refreshUntilDisplayed('#element-id', () => {...});
* cy.refreshUntilDisplayed({ selector: 'element-id', type: 'div' }, () => {...});
*/
Cypress.Commands.add('waitFirstRefreshUntilDisplayed', (firstCheckFunction, selector: string | { selector: string, type: string }, opts = {}) => {
if (!(firstCheckFunction instanceof Function)) {
throw new Error(`\`firstCheckFunction\` parameter should be a function. Found: ${firstCheckFunction}`);
}
let parsedSelector = '';
// Define the default options for the underlying `cy.wait` command
const defaultOptions = {
timeout: 5000,
interval: 500,
};
const options = { ...defaultOptions, ...opts };
// Calculate the number of retries to wait for the element to be displayed
let retries = Math.floor(options.timeout / options.interval);
const totalRetries = retries;
if (typeof selector === 'string') {
parsedSelector = selector;
}
if (typeof selector !== 'string' && selector.selector && selector.type) {
parsedSelector = `${selector.type}[data-test=${selector.selector}]`;
}
// Define the check function that will be called recursively until the element is displayed
function check(selector: string): boolean {
if (Cypress.$(selector).length) { // Element exists
return true;
}
if (retries < 1) {
throw Error(`${selector} not found`);
}
if (totalRetries !== retries) { // we don't reload first time
cy.log(`Element ${parsedSelector} not found. ${retries} left`);
cy.reload();
}
// Waits for the firstCheckFunction to return true,
// then pause for the time define in options.interval
// and call recursively the check function
cy.waitUntil(firstCheckFunction, options).then(() => { // We first for firstCheckFunction to be true
cy.wait(options.interval).then(() => { // Then we loop until the selector is displayed
retries -= 1;
return check(selector);
});
});
return false;
}
check(parsedSelector);
});
Related
Can a custom command overload be done the same way as a function overload?
There is no answer to this in the documentation.
For example:
Cypress.Commands.add('navigateAndWaitForApi',
(relativePath: string, apisPath: string[], timeout?: number) => {
let options = {};
if (timeout !== undefined) {
options = { timeout: TIMEOUT };
}
apisPath.forEach((api)=> {
cy.intercept(`/api/${api}`).as(api);
})
cy.visit(`/${relativePath}`);
cy.wait(apisPath.map(apiPath => `#${apiPath}`), options);
});
Cypress.Commands.add('navigateAndWaitForApi',
(relativePath: string, apiPath: string, timeout?: number) => {
cy.navigateAndWaitForApi(relativePath, [apiPath], timeout);
});
It does not appear so. The command name navigateAndWaitForApi is the total signature.
Add this after the command definitions
console.log(Cypress.Commands._commands)
shows commands are stored in an object, keyed by the command name.
Adding the same command twice, the second overwrites the first.
It's possible to type-check the params at runtime.
Cypress.Commands.add('navigateAndWaitForApi',
(relativePath: string, apiPaths: string|string[], timeout?: number) => {
if (typeof apiPaths === 'string') {
apiPaths = [apiPaths]
}
let options = {};
if (timeout !== undefined) {
options = { timeout: TIMEOUT };
}
apiPaths.forEach((api)=> {
cy.intercept(`/api/${api}`).as(api);
})
cy.visit(`/${relativePath}`);
// cy.wait(apiPaths.map(apiPath => `#${apiPath}`), options); // look dubious
apiPaths.forEach(apiPath => {
cy.wait(`#${apiPath}`), options)
})
});
I have the recursive function: repeatAlert that is called again if data.answered === null:
....
Edit
this.repeatAlert(id).subscribe( val => console.log(val));
console.log('1stCall Alert: ', new Date().getMinutes());
....
find(id: number): Observable<any> {
return this.http.get(`${this.resourceUrl}ByAlertId/${id}`
}
repeatAlert(id: number) {
this.find(id).subscribe((data: AlertInt) => {
if (data.answered === null ) {
this.sendNotification('Alert ', data.text);
console.log('Call Alert: ', new Date().getMinutes(), data.id);
setTimeout(() => {
if (data.answered === null) {
this.repeatAlert(id);
}
}, data.repeating * 1000 * 60);
}
});
}
When I change the value of data.answered in the database, I can't read with this observable find(id) the change of data.answered. So it keeps calling repeatAlert forever ...
What am I doing wrong?
Extra question: Is it better a loop or recursive function ?
You are doing polling. I suggest something like following:
find(id: number): Observable<any> {
return this.http.get(`${this.resourceUrl}ByAlertId/${id}`;
}
repeatAlert(id: number) {
// request data from the endpoint and execute necessary code
const data$ = this.find(id).pipe(
tap(data => {
if (data.answered === null) {
this.sendNotification('Alert ', data.text);
}
})
);
// polling will start if on the first try we don't have data.answered
const startPolling = (duration: number) => timer(duration, duration).pipe(
//take(10), // let's say we want to stop after 10 tries
concatMap(() => data$),
takeWhile(data => data.answered === null), // when to stop polling
);
// if data.answered is null on the first try switch to polling otherwise end
return data$.pipe(
switchMap(data => data.answered === null ?
startPolling(data.repeating * 1000 * 60) :
of(data)
),
);
}
Also note that I changed your repeatAlert, it's better to return Observable from the method and subscribe yourself to avoid memory leaks. You should subscribe and unsubscribe yourself. Also, I suggest you to use take(10) for example so that polling doesn't continue indefinitely, it's up to you.
timer(dueTime, period) works like this: It will emit first event after dueTime and continue emitting events after every period.
Edit takeWhile condition is true and not condition is false
I turns out that this code is also working
repeatAlert(id: number) {
this.alertServ.find(id).subscribe((data: AlertInt) => {
if (data.answered === null) {
this.sendNotification( 'Alert ', data.text);
setTimeout(() => {
this.repeatAlert(id);
}, data.repeating * 1000 * 60);
}
});
}
I forget in the backend to send the data.answered field ... so was always null
I want to test an effect that works as follows:
Effect starts if LoadEntriesSucces action was dispatched
It waits for 5 seconds
After 5 seconds passes http request is send
When response arrives, new action is dispatched (depending, whether response was succes or error).
Effect's code looks like this:
#Effect()
continuePollingEntries$ = this.actions$.pipe(
ofType(SubnetBrowserApiActions.SubnetBrowserApiActionTypes.LoadEntriesSucces),
delay(5000),
switchMap(() => {
return this.subnetBrowserService.getSubnetEntries().pipe(
map((entries) => {
return new SubnetBrowserApiActions.LoadEntriesSucces({ entries });
}),
catchError((error) => {
return of(new SubnetBrowserApiActions.LoadEntriesFailure({ error }));
}),
);
}),
);
What I want to test is whether an effect is dispatched after 5 seconds:
it('should dispatch action after 5 seconds', () => {
const entries: SubnetEntry[] = [{
type: 'type',
userText: 'userText',
ipAddress: '0.0.0.0'
}];
const action = new SubnetBrowserApiActions.LoadEntriesSucces({entries});
const completion = new SubnetBrowserApiActions.LoadEntriesSucces({entries});
actions$ = hot('-a', { a: action });
const response = cold('-a', {a: entries});
const expected = cold('- 5s b ', { b: completion });
subnetBrowserService.getSubnetEntries = () => (response);
expect(effects.continuePollingEntries$).toBeObservable(expected);
});
However this test does not work for me. Output from test looks like this:
Expected $.length = 0 to equal 3.
Expected $[0] = undefined to equal Object({ frame: 20, notification: Notification({ kind: 'N', value: undefined, error: undefined, hasValue: true }) }).
Expected $[1] = undefined to equal Object({ frame: 30, notification: Notification({ kind: 'N', value: undefined, error: undefined, hasValue: true }) }).
Expected $[2] = undefined to equal Object({ frame: 50, notification: Notification({ kind: 'N', value: LoadEntriesSucces({ payload: Object({ entries: [ Object({ type: 'type', userText: 'userText', ipAddress: '0.0.0.0' }) ] }), type: '[Subnet Browser API] Load Entries Succes' }), error: undefined, hasValue: true }) }).
What should I do to make this test work?
Like mentioned in another answer, one way to test that effect would be by using the TestScheduler but it can be done in a simpler way.
We can test our asynchronous RxJS code synchronously and deterministically by virtualizing time using the TestScheduler. ASCII marble diagrams provide a visual way for us to represent the behavior of an Observable. We can use them to assert that a particular Observable behaves as expected, as well as to create hot and cold Observables we can use as mocks.
For example, let's unit test the following effect:
effectWithDelay$ = createEffect(() => {
return this.actions$.pipe(
ofType(fromFooActions.doSomething),
delay(5000),
switchMap(({ payload }) => {
const { someData } = payload;
return this.fooService.someMethod(someData).pipe(
map(() => {
return fromFooActions.doSomethingSuccess();
}),
catchError(() => {
return of(fromFooActions.doSomethinfError());
}),
);
}),
);
});
The effect just waits 5 seconds after an initial action, and calls a service which would then dispatch a success or error action. The code to unit test that effect would be the following:
import { TestBed } from "#angular/core/testing";
import { provideMockActions } from "#ngrx/effects/testing";
import { Observable } from "rxjs";
import { TestScheduler } from "rxjs/testing";
import { FooEffects } from "./foo.effects";
import { FooService } from "../services/foo.service";
import * as fromFooActions from "../actions/foo.actions";
// ...
describe("FooEffects", () => {
let actions$: Observable<unknown>;
let testScheduler: TestScheduler; // <-- instance of the test scheduler
let effects: FooEffects;
let fooServiceMock: jasmine.SpyObj<FooService>;
beforeEach(() => {
// Initialize the TestScheduler instance passing a function to
// compare if two objects are equal
testScheduler = new TestScheduler((actual, expected) => {
expect(actual).toEqual(expected);
});
TestBed.configureTestingModule({
imports: [],
providers: [
FooEffects,
provideMockActions(() => actions$),
// Mock the service so that we can test if it was called
// and if the right data was sent
{
provide: FooService,
useValue: jasmine.createSpyObj("FooService", {
someMethod: jasmine.createSpy(),
}),
},
],
});
effects = TestBed.inject(FooEffects);
fooServiceMock = TestBed.inject(FooService);
});
describe("effectWithDelay$", () => {
it("should dispatch doSomethingSuccess after 5 seconds if success", () => {
const someDataMock = { someData: Math.random() * 100 };
const initialAction = fromFooActions.doSomething(someDataMock);
const expectedAction = fromFooActions.doSomethingSuccess();
testScheduler.run((helpers) => {
// When the code inside this callback is being executed, any operator
// that uses timers/AsyncScheduler (like delay, debounceTime, etc) will
// **automatically** use the TestScheduler instead, so that we have
// "virtual time". You do not need to pass the TestScheduler to them,
// like in the past.
// https://rxjs-dev.firebaseapp.com/guide/testing/marble-testing
const { hot, cold, expectObservable } = helpers;
// Actions // -a-
// Service // -b|
// Results // 5s --c
// Actions
actions$ = hot("-a-", { a: initialAction });
// Service
fooServiceMock.someMethod.and.returnValue(cold("-b|", { b: null }));
// Results
expectObservable(effects.effectWithDelay$).toBe("5s --c", {
c: expectedAction,
});
});
// This needs to be outside of the run() callback
// since it's executed synchronously :O
expect(fooServiceMock.someMethod).toHaveBeenCalled();
expect(fooServiceMock.someMethod).toHaveBeenCalledTimes(1);
expect(fooServiceMock.someMethod).toHaveBeenCalledWith(someDataMock.someData);
});
});
});
Please notice that in the code I'm using expectObservable to test the effect using the "virtual time" from the TestScheduler instance.
you could use the done callback from jasmine
it('should dispatch action after 5 seconds', (done) => {
const resMock = 'resMock';
const entries: SubnetEntry[] = [{
type: 'type',
userText: 'userText',
ipAddress: '0.0.0.0'
}];
const action = new SubnetBrowserApiActions.LoadEntriesSucces({entries});
const completion = new SubnetBrowserApiActions.LoadEntriesSucces({entries});
actions$ = hot('-a', { a: action });
const response = cold('-a', {a: entries});
const expected = cold('- 5s b ', { b: completion });
subnetBrowserService.getSubnetEntries = () => (response);
effects.continuePollingEntries$.subscribe((res)=>{
expect(res).toEqual(resMock);
done()
})
});
The second notation doesn't work with jasmine-marbles, use dashes instead:
const expected = cold('------b ', { b: completion });
You will need to do 3 things
1- Inside your beforeEach, you need to override the internal scheduler of RxJs as follows:
import { async } from 'rxjs/internal/scheduler/async';
import { cold, hot, getTestScheduler } from 'jasmine-marbles';
beforeEach(() => {.....
const testScheduler = getTestScheduler();
async.schedule = (work, delay, state) => testScheduler.schedule(work, delay, state);
})
2- Replace delay, with delayWhen as follows:
delayWhen(_x => (true ? interval(50) : of(undefined)))
3- Use frames, I am not really sure how to use seconds for this, so I used frames. Each frame is 10ms. So for example my delay above is 50ms and my frame is -b, so that is the expected 10 ms + I needed another 50ms so this equals extra 5 frames which was ------b so as follows:
const expected = cold('------b ', { b: outcome });
I have an initializer method calling another method that returns a promise, like:
initStuffAfterLoad() {
const _this = this;
const theInterval = window.setInterval(function() {
if (thing) {
window.clearInterval(theInterval);
_this.getBanana()
.then(response => {
_this.getApple(response, _this);
});
}
}, 100);
}
and am needing to test whether getBanana was called (jest/sinon). So far I have:
test('init function calls getBanana', () => {
let thing = true
const getBananaSpy = sinon.spy();
sinon.stub(TheClass.prototype, 'getBanana').callsFake(getBananaSpy).resolves();
jest.useFakeTimers();
TheClass.prototype.initStuffAfterLoad();
jest.runOnlylPendingTimers();
expect(getBananaSpy.called).toBeTruthy();
TheClass.prototype.getBanana.restore();
});
However it still receives false at the assertion. I figure I'm not handling the Promise part correctly - what is the best practice way to do this?
I am not familiar with sinon, but here is a way to achieve your need with pure jest (even better it also checks that getApple is called when getBanana reseolves :))
jest.useFakeTimers()
const _this = {
getBanana: () => {},
getApple: () => {}
}
const initStuffAfterLoad = () => {
const theInterval = window.setInterval(function() {
window.clearInterval(theInterval);
_this.getBanana().then(response => {
_this.getApple(response, _this)
});
}, 100);
}
test('', () => {
let result
_this.getBanana = jest.fn(() => {
result = new Promise( resolve => { resolve() } )
return result
})
_this.getApple = jest.fn()
initStuffAfterLoad()
jest.runAllTimers()
expect(_this.getBanana.mock.calls.length).toBe(1)
return result.then(() => {
expect(_this.getApple.mock.calls.length).toBe(1)
})
})
code tested :)
PASS test\temp.test.js √ (25ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 2.489s
I have the following code:
export const myEpic = (action$, store) =>
action$.ofType("SOME_ACTION")
.switchMap(action => {
const {siteId, selectedProgramId} = action;
const state = store.getState();
const siteProgram$ = Observable.fromPromise(axios.get(`/url/${siteId}/programs`))
.catch(error =>{
return Observable.of({
type: 'PROGRAM_FAILURE'
error
});
});
const programType$ = Observable.fromPromise(axios.get('url2'))
.catch(error =>{
return Observable.of({
type: "OTHER_FAILURE",
error
});
});
so far so good, when there is an error I catch it, and (maybe this is wrong) map it to an action (indicating something failed).
now the question begins, I have another observable which is the result of the zip operator of the two observables from above:
const siteProgram$result$ = Observable.zip(siteProgram$, programType$)
.map(siteProgramsAndProgramTypes => siteProgramsAndProgramTypesToFinalSiteProgramsActionMapper(siteProgramsAndProgramTypes, siteId));
the problem is that I still get to this observable as if everything is fine.
is there a way to "understand" that one of the "zipped" observables errored and then not get to the "next" of siteProgram$result$.
I think I am missing something trivial...
I don't want to have to perform this check:
const siteProgramsAndProgramTypesToFinalSiteProgramsActionMapper = (siteProgramsAndProgramTypesArray, siteId) => {
const [programsResponse, programTypesResponse] = siteProgramsAndProgramTypesArray;
if (programsResponse.error || programTypesResponse.error){
return {
type: 'GENERAL_ERROR',
};
}
everytime I have an observable which is a result of an operator on other observable that might have errored.
in pure rxjs (not in redux observable) I think I could subscribe to it passing it an object
{
next: val => some logic,
error: err => do what ever I want :) //this is what I am missing in redux observable,
complete: () => some logic
}
// some more logic
return Observable.concat(programType$Result$, selectedProgramId$, siteProgram$result$);
What is the right way to attack this in redux observable?
Thanks.
Here is a detailed example with an API wrapper to facilitate what you're trying to achieve.
The gist is available on GitHub here
Here is the API wrapper which wraps Observable.ajax and lets you dispatch single actions or array of actions and handles both XHR and Application level generated errors that stem from the requests made with Observable.ajax
import * as Rx from 'rxjs';
import queryString from 'query-string';
/**
* This function simply transforms any actions into an array of actions
* This enables us to use the synthax Observable.of(...actions)
* If an array is passed to this function it will be returned automatically instead
* Example: mapObservables({ type: ACTION_1 }) -> will return: [{ type: ACTION_1 }]
* Example2: mapObservables([{ type: ACTION_1 }, { type: ACTION_2 }]) -> will return: [{ type: ACTION_1 }, { type: ACTION_2 }]
*/
function mapObservables(observables) {
if (observables === null) {
return null;
} else if (Array.isArray(observables)) {
return observables;
}
return [observables];
}
/**
* Possible Options:
* params (optional): Object of parameters to be appended to query string of the uri e.g: { foo: bar } (Used with GET requests)
* headers (optional): Object of headers to be appended to the request headers
* data (optional): Any type of data you want to be passed to the body of the request (Used for POST, PUT, PATCH, DELETE requests)
* uri (required): Uri to be appended to our API base url
*/
function makeRequest(method, options) {
let uri = options.uri;
if (method === 'get' && options.params) {
uri += `?${queryString.stringify(options.params)}`;
}
return Rx.Observable.ajax({
headers: {
'Content-Type': 'application/json;charset=utf-8',
...options.headers,
},
responseType: 'json',
timeout: 60000,
body: options.data || null,
method,
url: `http://www.website.com/api/v1/${uri}`,
// Most often you have a fixed API url so we just append a URI here to our fixed URL instead of repeating the API URL everywhere.
})
.flatMap(({ response }) => {
/**
* Here we handle our success callback, anyt actions returned from it will be dispatched.
* You can return a single action or an array of actions to be dispatched eg. [{ type: ACTION_1 }, { type: ACTION_2 }].
*/
if (options.onSuccess) {
const observables = mapObservables(options.onSuccess(response));
if (observables) {
// This is only being called if our onSuccess callback returns any actions in which case we have to dispatch them
return Rx.Observable.of(...observables);
}
}
return Rx.Observable.of();
})
.catch((error) => {
/**
* This if case is to handle non-XHR errors gracefully that may be coming from elsewhere in our application when we fire
* an Observable.ajax request
*/
if (!error.xhr) {
if (options.onError) {
const observables = mapObservables(options.onError(null)); // Note we pass null to our onError callback because it's not an XHR error
if (observables) {
// This is only being called if our onError callback returns any actions in which case we have to dispatch them
return Rx.Observable.of(...observables);
}
}
// You always have to ensure that you return an Observable, even if it's empty from all your Observables.
return Rx.Observable.of();
}
const { xhr } = error;
const { response } = error.xhr;
const actions = [];
const resArg = response || null;
let message = null;
if (xhr.status === 0) {
message = 'Server is not responding.';
} else if (xhr.status === 401) {
// For instance we handle a 401 here, if you use react-router-redux you can simply push actions here to your router
actions.push(
replace('/login'),
);
} else if (
response
&& response.errorMessage
) {
/*
* In this case the errorMessage parameter would refer to SampleApiResponse.json 400 example
* { "errorMessage": "Invalid parameter." }
*/
message = response.errorMessage;
}
if (options.onError) {
// Here if our options contain an onError callback we can map the returned Actions and push them into our action payload
mapObservables(options.onError(resArg)).forEach(o => actions.push(o));
}
if (message) {
actions.push(showMessageAction(message));
}
/**
* You can return multiple actions in one observable by adding arguments Rx.Observable.of(action1, action2, ...)
* The actions always have to have a type { type: 'ACTION_1' }
*/
return Rx.Observable.of(...actions);
});
}
const API = {
get: options => makeRequest('get', options),
post: options => makeRequest('post', options),
put: options => makeRequest('put', options),
patch: options => makeRequest('patch', options),
delete: options => makeRequest('delete', options),
};
export default API;
Here are the actions, action creators and epics:
import API from 'API';
const FETCH_PROFILE = 'FETCH_PROFILE';
const FETCH_PROFILE_SUCCESS = 'FETCH_PROFILE_SUCCESS';
const FETCH_PROFILE_ERROR = 'FETCH_PROFILE_ERROR';
const FETCH_OTHER_THING = 'FETCH_OTHER_THING';
const FETCH_OTHER_THING_SUCCESS = 'FETCH_OTHER_THING_SUCCESS';
const FETCH_OTHER_THING_ERROR = 'FETCH_OTHER_THING_ERROR';
function fetchProfile(id) {
return {
type: FETCH_PROFILE,
id,
};
}
function fetchProfileSuccess(data) {
return {
type: FETCH_PROFILE_SUCCESS,
data,
};
}
function fetchProfileError(error) {
return {
type: FETCH_PROFILE_ERROR,
error,
};
}
function fetchOtherThing(id) {
return {
type: FETCH_OTHER_THING,
id,
};
}
const fetchProfileEpic = action$ => action$.
ofType(FETCH_PROFILE)
.switchMap(({ id }) => API.get({
uri: 'profile',
params: {
id,
},
/*
* We could also dispatch multiple actions using an array here you could dispatch another API request if needed
* We can redispatch another action to fire another epic if we want also.
* In both onSuccess and onError you can return a single action, an array of actions or null
* Note here we fire fetchOtherThing(data.someOtherThingId) which will trigger our fetchOtherThingEpic!
* In this case the data parameter would refer to SampleApiResponse.json 200 example
* { firstName: "John", lastName: "Doe" }
*/
onSuccess: ({ data }) => [fetchProfileSuccess(data), fetchOtherThing(data.someOtherThingId)],
onError: error => fetchProfileError(error),
})
const fetchOtherThingEpic = action$ => action$.
ofType(FETCH_OTHER_THING)
.switchMap(({ id }) => API.get({
uri: 'other-thing',
params: {
id,
},
onSuccess: ...
onError: ...
});
Here is a data sample that works with the examples shown above:
/*
* If possible, you should standardize your API response which will make error/data handling a lot easier on the client side
* Note, this data format is to work with the example code above
*/
/**
* Status code: 401
* This error would be caught in the .catch() method of our API wrapper
*/
{
"errorMessage": "Please login to perform this operation",
"data": null,
}
/**
* Status code: 400
* This error would be caught in the .catch() method of our API wrapper
*
*/
{
"errorMessage": "Invalid parameter.",
"data": null
}
/**
* Status code: 200
* This would be passed to our onSuccess function specified in our API options
*/
{
"errorMessage": 'Please login to perform this operation',
"data": {
"firstName": "John",
"lastName": "Doe"
}
}