React Testing with data fetching not resolving promise - javascript

I refer to the following tutorial regarding data fetching on ReactJS.org, because I wanted to use it as a template for my own test.
I use npm test, which calls react-scripts test, and, as far as i know, uses jasmine.
I created the user.js and user.test.js as described in the tutorial.
// user.test.js
import React from "react";
import { render, unmountComponentAtNode } from "react-dom";
import { act } from "react-dom/test-utils";
import User from "./user";
let container = null;
beforeEach(() => {
// setup a DOM element as a render target
container = document.createElement("div");
document.body.appendChild(container);
});
afterEach(() => {
// cleanup on exiting
unmountComponentAtNode(container);
container.remove();
container = null;
});
it("renders user data", async () => {
const fakeUser = {
name: "Joni Baez",
age: "32",
address: "123, Charming Avenue"
};
jest.spyOn(global, "fetch").mockImplementation(() =>
Promise.resolve({
json: () => Promise.resolve(fakeUser)
})
);
// Use the asynchronous version of act to apply resolved promises
await act(async () => {
render(<User id="123" />, container);
});
expect(container.querySelector("summary").textContent).toBe(fakeUser.name);
expect(container.querySelector("strong").textContent).toBe(fakeUser.age);
expect(container.textContent).toContain(fakeUser.address);
// remove the mock to ensure tests are completely isolated
global.fetch.mockRestore();
});
When I run the test I get Timeout - Async callback was not invoked within the 5000ms timeout specified by jest.
setTimeout.Error
For some reason, it does not seem to resolve the promise.
What am I doing wrong? I can't believe this tutorial is bugged! Any help is appreciated!

It's working now. You strictly need react-dom >= 16.9.0

Related

How can I reset the jsdom instance when using vitest in order to test a History-based router?

I'd like to do some integration testing of my svelte + page.js-based router using vitest, but I'm running into an issue where the jsdom instance only updates correctly once per test file.
In the following setup, either test will pass when run with .only or if I split each test into its own file. But when they run in sequence, the second one will always fail. Inspecting the DOM with screen.debug() reveals that it's empty, and calls to act or tick don't seem to do anything.
I suspect it has something to do with how jsdom is interacting with the History API, but I'm not sure where to go from here.
Root.svelte
<script>
import page from 'page'
import SignIn from './SignIn/SignIn.svelte'
import Upload from './Upload/Upload.svelte'
import { authenticationToken } from './Root.stores.js'
let currentPage
page('/', () => {
page.redirect('/sign-in')
})
page('/sign-in', () => {
currentPage = SignIn
})
page('/upload', () => {
if ($authenticationToken === null) {
return page.redirect('/sign-in')
}
currentPage = Upload
})
page.start()
</script>
<svelte:component this={ currentPage } />
Root.svelte.test.js
import page from 'page'
import Root from './Root.svelte'
import { authenticationToken } from './Root.stores.js'
it('redirects to sign in when not authenticated', async () => {
vi.spyOn(page, 'redirect')
authenticationToken.set(null)
const { act } = setupComponent(Root)
await act(() => page('/upload'))
expect(page.redirect).toHaveBeenCalledWith('/sign-in')
})
it('displays the upload screen when authenticated', async () => {
authenticationToken.set('token')
const { act } = setupComponent(Root)
await act(() => page('/upload'))
expect(document.getElementById('upload')).toBeInTheDocument()
})
Other Research
The issue is similar to this one in the jest project. In that issue, the recommendation was to call jsdom.reconfigure() in a beforeEach block, but I don't know how to get a hold of the jsdom instance in vitest in order to try that.
Any ideas or alternative approaches welcome, thanks!

Test set cookies function with Jest

Does someone knows how can I test this function in Jest? I don't have any ideas at this moment, maybe I need to mock Cookies ?
import Cookies from "js-cookie";
import { v4 as uuidv4 } from "uuid";
const setUserCookie = () => {
if (!Cookies.get("UserToken")) {
Cookies.set("UserToken", uuidv4(), { expires: 10 });
}
};
export default setUserCookie;
I tried this for now, but I don't know if this is correct, I don't think it tests the functionality of my function:
import Cookies from 'js-cookie';
import setCookie from './setCookie';
describe("setCookie", () => {
it("should set cookie", () => {
const mockSet = jest.fn();
Cookies.set = mockSet;
Cookies.set('testCookie', 'testValue');
setCookie()
expect(mockSet).toBeCalled();
});
});
Best way to test this is to utilize the actual logic, so I would change your test to the following:
it("should set cookie", () => {
// execute actual logic
setCookie();
// retrieve the result
const resultCookie = Cookies.get();
// expects here
expect(resultCookie["UserToken"]).toBeTruthy();
// expects for other values here...
});
To note, uuidv4() will generate a new value for every new test run, meaning that you cannot expect the same value for the "UserToken" property. Instead, you can use the following approach to tackle this problem:
First set up a spy for it:
import { v4 as uuidv4 } from "uuid";
jest.mock('uuid');
Then add its mock implementation with the expected result into the unit test:
const expectedUUIDV4 = 'testId';
uuidv4.mockImplementation(() => expectedUUIDV4);
// then expecting that in the result
expect(resultCookie["UserToken"]).toEqual(expectedUUIDV4);

How to update a Vue ref using a Watch inside a composable?

I have a Vue composable named getDocument that gets a Firebase document.
The code inside onSnapshot callback runs asynchronously. And I'm trying to update the document and error refs with values returned from onSnapshot.
But I want to avoid using a Watch outside the getDocument if possible, because always having to wrap my code in a Watch is a pain.
Instead I want to put a Watch inside the getDocument.ts and have it update the document and error refs there.
This is what I have so far, with no Watch inside getDocument.ts.
src/composable/getDocument.ts
import { ref, watchEffect, watch } from 'vue';
import { db } from 'src/firebase/config';
import {
doc,
onSnapshot,
DocumentSnapshot,
DocumentData,
} from 'firebase/firestore';
const getDocument = (collectionString: string, documentId: string) => {
const error = ref<string | undefined>();
const document = ref<DocumentData | undefined>();
const docRef = doc(db, collectionString, documentId);
const unsubscribe = onSnapshot(
docRef,
(doc: DocumentSnapshot<DocumentData>) => {
if (doc.data()) {
document.value = {
...doc.data(),
id: doc.id,
};
error.value = undefined;
} else {
error.value = "That document doesn't exist";
}
},
(err) => {
console.log(err.message);
error.value = 'Could not fetch documents';
}
);
// Cancel the listener when composable not in use
watchEffect((onInvalidate) => {
onInvalidate(() => {
unsubscribe();
});
});
// Maybe use a "Watch" here to update the doucment and error refs? But I can't get it working.
return { document, error };
};
export default getDocument;
Now when importing the getDocument composable I could wrap everything in a Watch to make sure the ref has a value. But I would rather do that inside getDocument instead.
For example:
src/composable/anotherComposable.ts
import getDocument from 'src/composables/getDocument';
const { document, error } = getDocument('users', 'USER_ID_HERE');
// I could wrap all my code here in a Watch, but I was hoping to avoid that. I want to use the Watch inside the getDocument composable to do the same thing.
watch(document, () => {
console.log(document.value);
});
// This is how I would like to ultimately use the document ref after the Watch is moved inside the getDocument composable. Currently this will show as undefined. So I need to somehow put a Watch inside the getDocument composable to make this have a value.
console.log(document.value);

Why are dynamic imports unexpectedly coupled between tests when using mock-fs?

I'm trying to use mock-fs to unit test code which uses ES6 dynamic imports.
There seems to be an unexpected coupling between tests when I'm using dynamic imports, even though I call restore() after each test. It appears as though fs.readFile() behaves as expected between tests (no coupling), but await import() has coupling (it returns the result from the previous test).
I've created a minimal Jest test case that reproduces the issue. The tests pass individually, but not when run together. I notice that if I change the directory value so it's different between each test, then they pass together.
Can you help me understand why this doesn't work, whether it's a bug, and what I should do here?
import path from 'path';
import { promises as fs } from 'fs';
import mockFs from 'mock-fs';
const fsMockModules = {
node_modules: mockFs.load(path.resolve(__dirname, '../node_modules')),
};
describe('Reproduce dynamic import coupling between tests', () => {
afterEach(() => {
mockFs.restore();
});
it('first test', async () => {
const directory = 'some/path';
mockFs({
...fsMockModules,
[directory]: {
'index.js': ``,
},
});
await import(path.resolve(`${directory}/index.js`));
//not testing anything here, just illustrating the coupling for next test
});
it('second tests works in isolation but not together with first test', async () => {
const directory = 'some/path';
mockFs({
...fsMockModules,
[directory]: {
'index.js': `export {default as migrator} from './migrator.js';`,
'migrator.js':
'export default (payload) => ({...payload, xyz: 123});',
},
});
const indexFile = await fs.readFile(`${directory}/index.js`, 'utf-8');
expect(indexFile.includes('export {default as migrator}')).toBe(true);
const migrations = await import(path.resolve(`${directory}/index.js`));
expect(typeof migrations.migrator).toBe('function');
});
});

How testing my API calls in differents groups of test?

Im starting with react-testing-library, and Im trying to test API calls. I have two sets, one for success request and another for error request.
import React from "react";
import { render, waitForElementToBeRemoved } from "#testing-library/react";
import user from "#testing-library/user-event";
import App from "./App";
import { getUser } from "./serviceGithub";
jest.mock("./serviceGithub");
//Mock data for success and error, Im using the github api
const dataSuccess = {
id: "2231231",
name: "enzouu",
};
const dataError = {
message: "not found",
};
const renderInit = () => {
const utils = render(<App />);
const inputUser = utils.getByPlaceholderText("ingrese usuario", {
exact: false,
});
const buttonSearch = utils.getByRole("button", { name: /buscar/i });
return { utils, buttonSearch, inputUser };
};
test("should success request to api", async () => {
getUser.mockResolvedValue([dataSuccess]);
const { utils, buttonSearch, inputUser } = renderInit();
expect(utils.getByText(/esperando/i)).toBeInTheDocument();
expect(buttonSearch).toBeDisabled();
user.type(inputUser, "enzzoperez");
expect(buttonSearch).toBeEnabled();
user.click(buttonSearch);
await waitForElementToBeRemoved(() =>
utils.getByText("cargando", { exact: false })
);
expect(getUser).toHaveBeenCalledWith("enzzoperez");
expect(getUser).toHaveBeenCalledTimes(1);
expect(utils.getByText("enzouu", { exact: false })).toBeInTheDocument();
});
test("should error request to api", async () => {
getUser.mockResolvedValue(dataError)
const { utils, buttonSearch, inputUser } = renderInit();
expect(buttonSearch).toBeDisabled();
user.type(inputUser, "i4334jnrkni43");
expect(buttonSearch).toBeEnabled();
user.click(buttonSearch)
await waitForElementToBeRemoved(()=>utils.getByText(/cargando/i))
expect(getUser).toHaveBeenCalledWith('i4334jnrkni43')
expect(getUser).toHaveBeenCalledTimes(1)
});
The problem here is that in the second test the last line expect(getUser).toHaveBeenCalledTimes(1) get error because getUseris calling 2 times, but if I comment the first test, the second pass..
So, how should I do to test this case? Its ok the way that Im doing the tests?
Thanks!
You can use jest.mockClear() with beforeEach() or afterEach()
For clean-up purpose, afterEach() would be more appropriate.
mockClear resets all the information stored in the mockFn.mock.calls which means that for every test, you can expect getUser being called, started from zero times.
afterEach(() => {
jest.clearAllMocks()
})
Furthermore, use screen from #testing-library/react instead of returned value of render when using queries. Also, mockResolvedValueOnce would be better in this case.

Categories

Resources