How to test a Saga with an API call? - react-native

I have a saga
export function* mysaga(api, action) {
const response = yield call(api.service, action);
yield put(NavActions.goTo('Page', { success: response.ok }));
}
That calls an API and with return value navigate to another screen passing the API call result (response.ok).
it('test', () => {
// ...
const gen = mysaga(api, action);
const step = () => gen.next().value;
// doesn't actually run the api
const response = call(api.service, {});
expect(step()).toMatchObject(response); // ok
// error, Cannot read property 'ok' of undefined
expect(step()).toMatchObject(
put(NavActions.goTo('Page', { success: response.ok }))
);
});
Since it's not actually running the API call response doesn't get defined.
I don't know what I should do to test this scenario.
How do I test the second step of my saga?

By default, the yield expression resolves to whatever it yielded. However, you can pass another value to the gen.next method and then the yield expression is resolved to what you passed there.
So this should do the trick (untested):
const gen = rootSaga(api, action);
const step = (val) => gen.next(val).value;
const mockResponse = { ok: true };
const response = call(api.service, {});
expect(step(mockResponse)).toMatchObject(response); // ok
expect(step()).toMatchObject(
put(NavActions.goTo('Page', { success: true }))
);

Related

Spy on window function with testcafe

I want to test with testcafe if a function on a window object is executed with certain parameters. Is it possible with Testcafe?
The function call looks like this:
window.myObject.myFunction({customObject: true});
You can use the ClientFunction API to create a spy function in a window object. Please look at the following test example:
import { ClientFunction } from 'testcafe';
fixture `New Fixture`
.page `https://cf51n.csb.app/`;
const spyOn = ClientFunction(() => {
// Create an array where we store information about `myFunction` calls
window.myFunctionSpyData = [];
// Store the original `myFunction` value
window.orirginalFunction = window.myObject.myFunction;
window.myObject.myFunction = function() {
// Save data about the current call
window.myFunctionSpyData.push(...arguments);
// Call the original `myFunction` value
window.orirginalFunction(...arguments);
}
});
const getSpyData = ClientFunction(() => {
// Retrieve data about myFunction calls from client
return window.myFunctionSpyData;
});
const spyOff = ClientFunction(() => {
// Restore the original myFunction value
window.myObject.myFunction = window.orirginalFunction;
delete window.spyData;
});
test('New Test', async t => {
await spyOn();
await t.click('#btn');
const data = await getSpyData();
await spyOff();
await t
.expect(data.length).eql(2)
.expect(data[0]).eql('1')
.expect(data[1]).eql('2');
});

React Native listen to variable changes

I am trying to make an app that will allow me to update a textbox after receiving a change in a variable. However the variable takes a long time to update, and await does not work to wait for my variable to update probably because of the timeout function I used. How do I create a listener or something of that sort to check for any variable changes?
Below is my code snippet
const [banana, setBanana] = useState(-1);
const updateLocation = async() => {
const majorSig = scheduledScan();
setBanana(majorSig);
}
const scheduledScan = async() => {
beaconScan();
// Scans for 20 seconds
setTimeout( async()=> {
beaconStop();
await getAndUpdateUserLoc();
// console.log("Loggged: ", await getUserLoc());
// console.log("major: ", await getSignificantMajor());
currentMajor = await getSignificantMajor();
return currentMajor;
}, 20000);
}
When I run updateLocation(), my code is supposed to run for 20 second. I want it to wait until it finishes running scheduledScan() and returns a value to majorSig before it runs the setState function. However right now all it does is run scheduledScan() and update setState immediately to a wrong value. What should I do to make it behave in the way I want?
Thank you.
Firstly, in your async updateLocation function, your await statement is missing. Let's add it appropriately:
const updateLocation = async () => {
const majorSig = await scheduledScan();
setBanana(majorSig);
};
Then, It would be a good idea if you follow a promise approach in your time-limited function by using a Promise.race which lets your function either time out or successfully return a value:
const scheduledScan = async () => {
beaconScan();
return Promise.race([
async () => {
beaconStop();
await getAndUpdateUserLoc();
// console.log("Loggged: ", await getUserLoc());
// console.log("major: ", await getSignificantMajor());
currentMajor = await getSignificantMajor();
return currentMajor;
},
new Promise((_, reject) => setTimeout(() => reject(new Error('Request timed out')), 20000)),
]);
};

How to use debounce with Vuex?

I am trying to debounce a method within a Vuex action that requires an external API.
// Vuex action:
async load ({ state, commit, dispatch }) {
const params = {
period: state.option.period,
from: state.option.from,
to: state.option.to
}
commit('SET_EVENTS_LOADING', true)
const res = loadDebounced.bind(this)
const data = await res(params)
console.log(data)
commit('SET_EVENTS', data.collection)
commit('SET_PAGINATION', data.pagination)
commit('SET_EVENTS_LOADING', false)
return data
}
// Debounced method
const loadDebounced = () => {
return debounce(async (params) => {
const { data } = await this.$axios.get('events', { params })
return data
}, 3000)
}
The output of the log is:
[Function] {
cancel: [Function]
}
It is not actually executing the debounced function, but returning to me another function.
I would like to present a custom debounce method which you can use in your vuex store as
let ongoingRequest = undefined;
const loadDebounced = () => {
clearTimeout(ongoingRequest);
ongoingRequest = setTimeout(_ => {
axios.get(<<your URL>>).then(({ data }) => data);
}, 3000);
}
This method first ensures to cancel any ongoing setTimeout in the pipeline and then executes it again.
This can be seen in action HERE

how to receive a take with runSaga / redux-saga

I created a recordSaga function, its target is to record what actions have been dispatched during the saga.
export const recordSaga = async (saga, initialAction, state) => {
const dispatched = [];
const done = await runSaga(
{
dispatch: action => dispatched.push(action),
getState: () => state,
},
saga,
initialAction,
).done;
return {
dispatched,
done,
};
};
so let's say my saga is this one
export function* mySaga() {
const needToSave = yield select(needToSaveDocument);
if (needToSave) {
yield put(saveDocument());
yield take(SAVE_DOCUMENT_SUCCESS);
}
yield put(doSomethingElse())
}
I want to write two tests, which I expect to be the following
describe('mySaga', async () => {
it('test 1: no need to save', async () => {
const state = { needToSave: false }
const { dispatched } = await recordSaga(mySaga, {}, state);
expect(dispatched).toEqual([
doSomethingElse()
])
})
it('test 2: need to save', async () => {
const state = { needToSave: true }
const { dispatched } = await recordSaga(mySaga, {}, state);
expect(dispatched).toEqual([
saveDocument(),
doSomethingElse()
])
})
})
However, for the test 2 where there is a take in between, and of course jest (or its girlfriend jasmine) is yelling at me: Timeout - Async callback was not invoked within timeout specified by jasmine.DEFAULT_TIMEOUT_INTERVAL.
I know it is because runSaga is waiting for the take(SAVE_DOCUMENT_SUCCESS), but how can I mock that up ?
The answer stdChannel().put({type, payload})
Why ?
Using stdChannel you can dispatch after the first run.
How ?
import stdChannel;
add to the first param in runSaga;
call stdChannel().put(SAVE_DOCUMENT_SUCCESS);
Example
what worked for me
I left the first test as it is the expected final result, but the solution comes on the last 2.
import { runSaga, stdchannel } from 'redux-saga'
let dispatchedActions = [];
let channel;
let fakeStore;
beforeEach(() => {
channel = stdChannel(); // you have to declare the channel to have access to it later
fakeStore = {
channel, // add it to the store in runSaga
getState: () => "initial",
dispatch: (action) => dispatchedActions.push(action),
};
});
afterEach(() => {
global.fetch.mockClear();
});
it("executes getData correctly", async () => {
await runSaga(fakeStore, getData, getAsyncData("test")).toPromise();
expect(global.fetch.mock.calls.length).toEqual(1);
expect(dispatchedActions[0]).toEqual(setData(set_value));
});
it("triggers takeLatest and call getData(), but unfortunately doesn't resolve promise", async () => {
await runSaga(fakeStore, rootSaga)// .toPromise() cannot be used here, as will throw Timeout error
channel.put(getAsyncData("test")); // if remove this line, the next 2 expects() will fail
expect(global.fetch.mock.calls.length).toEqual(1);
// expect(dispatchedActions[1]).toEqual(setData(set_value)); // will fail here, but pass on the next it()
});
it("takes the promised data from test above", () => {
expect(dispatchedActions[1]).toEqual(setData(set_value));
});
this answer (about true code, not tests) helped me
By looking at recordSaga:
export const recordSaga = async (saga, initialAction, state) => {
It seems that you should pass {type: SAVE_DOCUMENT_SUCCESS} as a second argument (i.e initialAction). That should trigger the take effect.

Jest Vue Expected mock function to have been called with, but not called

I am trying to mock an api call using Jest and Vue but I get the error "Expected mock function to have been called with: ... but not called"
I have tried to find a solution but haven't found anything yet.
import DocumentService from "../../src/services/document";
import mockedData from "../__mockData__/documents";
import axios from "axios";
it("should call Document Service and download a document", () => {
let catchFn = jest.fn(),
thenFn = jest.fn();
DocumentService.downloadDocumentById(jwtToken, DocumentURL, id)
.then(thenFn)
.then(catchFn);
// expect(axios.get).toHaveBeenCalledWith(DocumentURL + "/" + id + "/content", {
// headers: { Authorization: "Bearer " + jwtToken, "X-role": "SuperUser" }
// });
expect(axios.get).toHaveBeenCalledWith(DocumentURL);
let responseObj = { data: mockedData };
axios.get.Mockresponse(responseObj);
expect(thenFn).toHaveBeenCalledWith(mockedData);
expect(catchFn).not.toHaveBeenCalled();
});
The test runs synchronously and the expect runs and fails before the Promise callbacks have a chance to run.
Make sure you await the Promise returned by DocumentService.downloadDocumentById to give the callbacks a chance to run:
it("should call Document Service and download a document", async () => { // use an async test function
let catchFn = jest.fn(),
thenFn = jest.fn();
const promise = DocumentService.downloadDocumentById(jwtToken, DocumentURL, id)
.then(thenFn)
.then(catchFn); // remember the Promise
expect(axios.get).toHaveBeenCalledWith(DocumentURL);
let responseObj = { data: mockedData };
axios.get.Mockresponse(responseObj);
await promise; // wait for the Promise
expect(thenFn).toHaveBeenCalledWith(mockedData); // SUCCESS
expect(catchFn).not.toHaveBeenCalled(); // SUCCESS
});
Had the same trouble, made it this way:
import axios from 'axios';
in test axios.get = jest.fn();
expect( axios.get ).toBeCalledWith( yourUrl );