Cypress: Not able to stub with a basic example. What might i be missing? - reactjs

For some reason, I am not able to stub this here. I have reduced my code to almost exactly this here. This should work according to the docs, but I'm wondering if I'm missing a finer detail of typescript / react hooks? That doesn't feel at all like the case but who knows. Thanks ahead of time if you're reading this and taking up your time. I appreciate what you do. Here's my example:
// connection.ts (the method to stub)
export const connectFn = async () => {
return 'i should not be here'
}
// ./reactHook.ts
import { connectFn } from './connection'
export const useMyHook = () => {
const handleConnect = async () => {
const result = await connectFn()
console.log(result)
// expected: 'i have been stubbed!'
// actual: 'i should not be here
}
return {
handleConnect
}
}
// ./App.ts
export const App = () => {
const { handleConnect } = useMyHook()
return <button onClick={handleConnect}>Connect</button>
}
// ./cypress/integration/connection.spec.ts
import * as myConnectModule from '../../connection'
describe('stub test', () => {
it('stubs the connectFn', () => {
cy.stub(myConnectModule, 'connectFn').resolves('i have been stubbed!')
cy.get('[data-testid=connect-btn]').click()
// assertions about the view here...
})
})
I thought I understood the cypress and Sinon docs pretty well. I have read that cypress needs an object to a stub from and not the named export directly - hence the * as an import. I have also tried every which way to export it as well. I used their example from the docs directly to no avail. I then thought it may be a typescript issue but it doesn't seem to be the case. I have reduced my actual code to pretty much this example here. I have even removed everything but the logs for testing and I'm still not getting it.

To stub successfully you need to stub (replace) the same instance.
In reactHook.ts
import { connectFn } from './connection'
if (window.Cypress) {
window.connectFn = connectFn
}
In the test
cy.window().then(win => {
cy.stub(win, 'connectFn').resolves('i have been stubbed!')
cy.get('[data-testid=connect-btn]').click()
...
}}
BTW useMyHook implies a React hook, if so you may need a cy.wait(0) to release the main JS thread and allow the hook to run.

Related

Testing a component in Next.js with testing-library that relies on tRCP

I was experimenting with tRCP and diligently followed the setup for my Next.js project described in the official docs over here: https://trpc.io/docs/nextjs
However I noticed that a simple component that relies on tRPC such as this
export const Sample = () => {
const { data } = trpc.useQuery(['hello', { text: 'User' }]);
if (data === undefined) {
return <div>Loading...</div>;
}
return <div>{data.greeting}</div>;
};
cannot be properly tested since the following trivial test
describe('Sample', () => {
it('should render successfully', () => {
const { baseElement } = render(<Sample />);
expect(baseElement).toBeTruthy();
});
});
since there is no setup of provider such as the setup with the withTRCP HOC used for the application itself. As such the test fails claiming client (presumably the trcpClient, unlike the queryClient) is undefined.
I'd like to know how to setup the test correctly, in this case providing a correct client, as well as mocking the queries, since I don't have the respective server-side code running while invoking the tests.
Since you are getting undefined for the trpc client implementation, you can try spying on the query call.
import trpc from 'utils/trpc'; // This is the client implementation
describe('Sample', () => {
it('should render successfully', () => {
jest.spyOn(trpc, 'useQuery')
.mockReturnValue({ greeting: "Greeting" });
const { baseElement } = render(<Sample />);
expect(baseElement).toBeTruthy();
});
});
This is also possible with the mutations but you need to provide a mock implementation for the useMutation response for mutate property.

How to test react-toastify with jest and react-testing-library

I have a screen with some form, and on submission, I send the request to back-end with axios. After successfully receiving the response, I show a toast with react-toastify. Pretty straight forward screen. However, when I try to test this behavior with an integration test using jest and react testing library, I can't seem to make the toast appear on DOM.
I have a utility renderer like that to render the component that I'm testing with toast container:
import {render} from "#testing-library/react";
import React from "react";
import {ToastContainer} from "react-toastify";
export const renderWithToastify = (component) => (
render(
<div>
{component}
<ToastContainer/>
</div>
)
);
In the test itself, I fill the form with react-testing-library, pressing the submit button, and waiting for the toast to show up. I'm using mock service worker to mock the response. I confirmed that the response is returned OK, but for some reason, the toast refuses to show up. My current test is as follows:
expect(await screen.findByRole("alert")).toBeInTheDocument();
I'm looking for an element with role alert. But this seems to be not working.
Also, I tried doing something like this:
...
beforeAll(() => {
jest.useFakeTimers();
}
...
it("test", () => {
...
act(() =>
jest.runAllTimers();
)
expect(await screen.findByRole("alert")).toBeInTheDocument();
}
I'm kind of new to JS, and the problem is probably due to asynch nature of both axios and react-toastify, but I don't know how to test this behavior. I tried a lot of things, including mocking timers and running them, mocking timers and advancing them, not mocking them and waiting etc. I even tried to mock the call to toast, but I couldn't get it working properly. Plus this seems like an implementation detail, so I don't think I should be mocking that.
I think the problem is I show the toast after the axios promise is resolved, so timers gets confused somehow.
I tried to search many places, but failed to find an answer.
Thanks in advance.
Thank you #Estus Flask, but the problem was much much more stupid :) I had to render ToastContainer before my component, like this:
import {render} from "#testing-library/react";
import React from "react";
import {ToastContainer} from "react-toastify";
export const renderWithToastify = (component) => {
return (
render(
<div>
<ToastContainer/>
{component}
</div>
)
);
};
Then, the test was very simple, I just had to await on the title of the toast:
expect(await screen.findByText("alert text")).toBeInTheDocument();
The findByRole doesn't seem to work for some reason, but I'm too tired to dig deeper :)
I didn't have to use any fake timers or flush the promises. Apperently, RTL already does those when you use await and finBy* queries, only the order of rendering was wrong.
In order to use a mock when you don't have access to the DOM (like a Redux side effect) you can do:
import { toast } from 'react-toastify'
jest.mock('react-toastify', () => ({
toast: {
success: jest.fn(),
},
}))
expect(toast.success).toHaveBeenCalled()
What I would do is mock the method from react-toastify to spy on that method to see what is gets called it, but not the actual component appearing on screen:
// setupTests.js
jest.mock('react-toastify', () => {
const actual = jest.requireActual('react-toastify');
Object.assign(actual, {toast: jest.fn()});
return actual;
});
and then in the actual test:
// test.spec.js
import {toast} from 'react-toastify';
const toastCalls = []
const spy = toast.mockImplementation((...args) => {
toastCalls.push(args)
}
)
describe('...', () => {
it('should ...', () => {
// do something that calls the toast
...
// then
expect(toastCalls).toEqual(...)
}
}
)
Another recommendation would be to put this mockImplementation into a separate helper function which you can easily call for the tests you need it for. This is a bear bones approach:
function startMonitoring() {
const monitor = {toast: [], log: [], api: [], navigation: []};
toast.mockImplementation((...args) => {
monitor.toast.push(args);
});
log.mockImplementation((...args) => {
monitor.log.push(args);
});
api.mockImplementation((...args) => {
monitor.api.push(args);
});
navigation.mockImplementation((...args) => {
monitor.navigation.push(args);
});
return () => monitor;
}
it('should...', () => {
const getSpyCalls = startMonitoring();
// do something
expect(getSpyCalls()).toEqual({
toast: [...],
log: [...],
api: [...],
navigation: [...]
});
});
Here, the solution was use getByText:
await waitFor(() => {
expect(screen.getByText(/Logged!/i)).toBeTruthy()
})

React Testing Library - testing in isolation and userEvent error

I'm writing tests using Jest and React Testing Library. I had failing tests and I realized that if I change the order of the tests, it will work. I'm guessing this is because the tests aren't properly isolated and one test might affect the other.
I am calling:
afterEach(() => {
cleanup()
jest.resetAllMocks()
})
I have a test that looks like this:
it('calls API when submitted', async () => {
render(<SignUp />)
fillAllVerificationFieldsWithTestData()
validateUser.mockResolvedValueOnce({ id: 123 })
const signupButton = screen.getByTestId(
'sign-up-verification-button',
)
userEvent.click(signupButton)
await waitFor(() => expect(validateUser).toHaveBeenCalledTimes(1))
})
If I create the exact same test or run a similar test after this one with a userEvent.click, I get an error:
Unable to fire a "mouseMove" event - please provide a DOM element.
I looked in the #testing-library/user-event library and I see this code:
const userEvent = {
click(element) {
const focusedElement = element.ownerDocument.activeElement;
const wasAnotherElementFocused =
focusedElement !== element.ownerDocument.body &&
focusedElement !== element;
if (wasAnotherElementFocused) {
fireEvent.mouseMove(focusedElement);
fireEvent.mouseLeave(focusedElement);
}
I noticed that element.ownerDocument.activeElement is null wasAnotherElementFocused is true and so it throws the error.
The first time I run the test it isn't null so it works.
Do I need some extra clean up between tests? If I use fireEvent:
fireEvent(signupButton,
new MouseEvent('click', {
bubbles: true,
}),
)
It works but I'm afraid I'm doing something wrong and not isolating my tests correctly.
EDIT:
Here is the code for the fillAllVerificationFieldsWithTestData:
export const fillAllVerificationFieldsWithTestData = () => {
const { given_name, family_name, zip, social, loanNumber } = {
given_name: screen.getByTestId('given_name'),
family_name: screen.getByTestId('family_name'),
zip: screen.getByTestId('zip'),
social: screen.getByTestId('last4ssn'),
loanNumber: screen.getByTestId('loan_number'),
}
userEvent.type(given_name, 'FirstName')
userEvent.type(family_name, 'LastName')
userEvent.type(zip, '77025')
userEvent.type(social, '1234')
userEvent.type(loanNumber, '1112223333')
}
and screen is imported from #testing-library/react and I import validate user like this:
import { validateUser } from '../../../services/auth'
jest.mock('../../../services/auth')
So, I just faced this problem, and what fixed it for me was wrapping any code that causes a React state change (which your userEvent code presumably does) in act. So, your initial test would look like this:
it('calls API when submitted', async () => {
render(<SignUp />)
fillAllVerificationFieldsWithTestData()
validateUser.mockResolvedValueOnce({ id: 123 })
const signupButton = screen.getByTestId(
'sign-up-verification-button',
)
await act(() => userEvent.click(signupButton))
await waitFor(() => expect(validateUser).toHaveBeenCalledTimes(1))
})
Can you give this a try, applying similar changes to your fillAllVerificationFieldsWithTestData function, and let us know how it turned out?
This is a bug that was reported in the testing-library/user-event GitHub and it should be fixed as of v11.0.1. It's specific to userEvent, which is why fireEvent works.
Note that you shouldn't need to call cleanup if you're using Jest because it will get called for you.

Promises inside WebWorker (react) throwing babel/webpack error

I'm trying to offload some intensive data processing to a WebWorker in a react app. If I call any asynchronous function within the onmessage handler, using promises or async/await, I get:
ReferenceError:
_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_0___default is not defined
Here's my worker:
const DataSyncWorker = () => {
self.doSyncing = async () => {
return null;
};
self.onmessage = e => {
if (!e) return;
console.log(`worker received message in listener callback: ${e.data}`);
self.doSyncing();
self.postMessage(`SYNC_COMPLETE`);
};
};
export default DataSyncWorker;
And the setup file for creating the Worker:
export default class WebWorker {
constructor(worker) {
const code = worker.toString();
const blob = new Blob([`(${code})()`]);
return new Worker(URL.createObjectURL(blob));
}
}
And invoking it:
import DataSyncWorker from './workers/DataSyncWorker';
import WebWorker from './workers/workerSetup';
const App = () => {
const dataSyncWorker = new WebWorker(DataSyncWorker);
dataSyncWorker.postMessage(`START_SYNC`);
dataSyncWorker.addEventListener(`message`, event => {
console.log(`index: message from worker: ${e.data}`);
});
}
If I change doSyncing to not be async, everything works fine. This is a simplified example, which I've confirmed still exhibits the same behavior. But I'm not able to use axios, or any other async function. It's my understanding that this should be possible, and, given the error, I am wondering if my issue is related to babel/webpack. Or perhaps I'm doing something else wrong. Any help is greatly appreciated. Thanks.
I solved this with a few changes:
First, I incorporated worker-loader library, using the inline type import, like:
import DataSyncWorker from 'worker-loader!./workers/DataSyncWorker';
And then, I had to unwrap the inner functions from the containing method of the original implementation, so DataSyncWorker now looks like this:
doSyncing = async () => {
return null;
};
self.onmessage = e => {
if (!e) return;
console.log(`worker received message in listener callback: ${e.data}`);
doSyncing();
self.postMessage(`SYNC_COMPLETE`);
};
The other code remains unchanged, and now everything works.
I believe you could use an alternate approach, involving modifying the webpack.config.js, with these additions to the modules.rules section:
{
test: /\.worker\.js$/,
use: ['worker-loader', 'babel-loader'],
include: [path.join(__dirname, 'src/workers')]
},
and then updating the name of your worker file so it's matched by the test condition, and updating it's import, etc, but I haven't tried that yet, and the inline method seemed simpler.

Manual mocks with Jest -- I'm not understanding something

I'm learning jest, and trying to do a manual mock for an api, I'm missing something.
I'm mocking an api call to giphy. I'm seeing a lot of different syntaxes for the manual mocks, and unfortunately, they're not making much sense to me right now. I've been trying to code along with https://hackernoon.com/api-testing-with-jest-d1ab74005c0a and https://facebook.github.io/jest/docs/en/tutorial-async.html, but I'm stuck.
I have a component called GifListContainer that displays 3 gifs, there's search functionality, I just want a manual mock with fake data to learn.
I'm using create-react-app, I see a lot of people using isomorphic-fetch and other packages, but since jest is built in can't I do it without adding anything else?
I can't figure out how to manually write the mock, I feel I'm missing something simple. It's testing fine if I don't use the mock (using different testing syntax because I'm not testing the _ mock _ file). Thank you for your time.
The error I'm getting:
● should load gifs
TypeError: GifContainer.api is not a function
at Object.<anonymous>.it (src/Part_2/GifContainer.test.js:10:23)
✕ should load gifs (6ms)
GifListContainer.js
import {gifApi} from './api'
class GifListContainer extends Component {
state = {
gifs: []
};
componentDidMount() {
this.displayGifs('coding');
}
displayGifs = (query) => {
gifApi(query)
.then(res => res.json())
.then(json => {
let firstThreeGifs = json.data.slice(0, 3);
let urls = firstThreeGifs.map(
gif => gif.images.original.url.split("?")[0]
);
this.setState({
gifs: [...urls]
});
});
};
//after this there's a search function and the render and such.
api.js
const urlPartOne = "http://api.giphy.com/v1/gifs/search?q="
const urlPartTwo = "&api_key=UE0dCN2WofIwVF0RPbpHo0Lz0k9VhqdG"
const gifApi = (query) => {
return fetch(urlPartOne + query + urlPartTwo)
}
export {gifApi}
GifContainer.test.js
import React from 'react'
let mockFunction = jest.mock('./api.js');
import * as GifContainer from './GifContainer';
it('should load gifs', () => {
return GifContainer.displayGifs('sunshine')
.then(data => {
expect(data).toBeDefined()
expect(data.entity.data.type).toEqual('gif')
})
})
_ mocks _/api.js
I'm really just not getting how to write this.
const fs = require('fs')
const api = (query) => new Promise((resolve, reject) => {
fs.readFile(`./src/Part_2/__mockData__/query.json`, 'utf8', (err, data) => {
if (err) reject(err)
resolve({ entity: JSON.parse(data) })
})
})
export default api
and then mock data in a folder _ mockData _/sushine.json
/* http://api.giphy.com/v1/gifs/search?q=sunshine&api_key=UE0dCN2WofIwVF0RPbpHo0Lz0k9VhqdG */
{
"data": [
{
"type": "gif",
}
],
"meta": {
"status": 200,
"msg": "OK",
"response_id": "5b11716a33554c732e0ddf42"
}
}
Thank you!
I don't think so the problem is on the mock itself.
Actually, first of all you need to improve the way you are doing react unit testing.
Nowadays, there are tools like Enzyme(http://airbnb.io/enzyme/) which helps you a lot to test React components.
Have you check the Testing React Apps section for Jest? Specially the DOM Testing part? Take a look here: https://facebook.github.io/jest/docs/en/tutorial-react.html#dom-testing
Back to your problem, I think it's because you are exporting the fn as default in the mock file but haven't done that on the api file. Try to put both equals and let me know!

Resources