-
Notifications
You must be signed in to change notification settings - Fork 69
Troubleshooting
Here's a list of the most common errors you might encounter and how you could fix them.
-
You most likely installed a new module by using npm instead of yarn. While in, theory these tools should be interchangeable, in practice, they can result in such weird errors. Recloning the git repo should help.
-
If you get an error with
code: 'ERR_OSSL_EVP_UNSUPPORTED'
when running the storybook, run this command:export NODE_OPTIONS=--openssl-legacy-provider
on linux/osx, on Powershell:$env:NODE_OPTIONS="--openssl-legacy-provider"
- This error mostly exists in node v17. Downgrading to v16 could help
-
If you get the following error on Windows:
title="November 6, 2000 12:00 AM" title="November 5, 2000 7:00 PM"
Upgrade node to >= v16.15.1
Understand both of these docs
- How to test React Component using Apollo queries
- Handling async methods (e.g, sending a GraphQL query)
screen
is an object that has all the queries to retrieve elements from the DOM.
- Import it with
import { screen } from '@testing-library/react'
This error occurs when multiple elements in the document have the same text. First, we'll see the good solution and then the BETTER one.
To fix it, you could try screen.getAll*(TEXT)
(e.g., screen.getAllByText(TEXT)
) then select the index for the element.
To find the element index, run yarn jest
but with the DEBUG_PRINT_LIMIT=100000
so it prints all the HTML document.
For the HTML document to show, the function must throw an error. Try to use await screen.findByText(TEXT)
that will throw an error as there are multiple occurrences of the element then locate the element.
Example:
// Throws error because there are 2 elements with this text
const dropdownBtn = await screen.findByText('Select a module')
await userEvent.click(dropdownBtn)
Now we run yarn DEBUG_PRINT_LIMIT=100000 jest <TEST_PATH>
When we intentionally throw an error with screen.findByText
, it prints this HTML:
As we can see, there are two elements with the text Select a module
so it throws an error.
We want the second element that has the text Select a module
which is the dropdown menu. Therefore, we can update our code to:
// [1] will select the dropdown menu
const dropdownBtn = await screen.findAllByText('Select a module')[1]
await userEvent.click(dropdownBtn)
A better way is to give the component data-testid
prop and find it by it:
<DropdownMenu>
<DropdownToggle data-testid="dropdown-toggle">
Select a module
</DropdownToggle>
</DropdownMenu>
Then in the test file:
const dropdownBtn = await screen.findByTestId('dropdown-toggle')
await userEvent.click(dropdownBtn)
This happens when when the component has an async
code that executes when a certain action happen (e.g., click a button).
To fix it, we can use an waitFor
or if it's an element we're looking for, we can use screen.find*
(e.g, screen.findByText
):
findByText
it('should successfully add an exercise', async () => {
const submitButton = screen.queryByText('Save exercise')
// A mutation is run when the submit button is clicked
// Therefore, we must wait for it as it is async and we are awaiting it
// The mutation code: `await addExercise()`
await userEvent.click(submitButton)
expect(await screen.findByText('Added the exercise successfully!'))
}
waitFor
from '@testing-library/react'
it('should not submit when inputs are empty', async () => {
const submitButton = await screen.findByText('Save exercise')
await userEvent.click(submitButton)
// Alt for findAllByText
await waitFor(() => {
expect(screen.queryAllByText('Required')[0]).toBeInTheDocument()
})
}
After updating getApp
query, you must update its dummy data in order for the mocked queries to return the data of the query correctly.
If you added the following field to the query, you must add it for each lesson object in the dummy data:
getApp.ts
// ...
modules {
id
name
content
order
}
/ ...
lessonData.ts
const dummyLessonsData: Lesson[] = [
// Some lesson
{
// ...
modules: [
{
id: 1,
name: 'module1',
content: 'this is module1',
order: 1,
author: moduleAuthor,
lesson: {
title: 'Foundations of JavaScript',
order: 0,
slug: 'js0',
id: 5,
description: 'A super simple introduction to help you get started!',
challenges: []
}
},
]
// ...
}
]
This isn't the most efficient way to create fake data for our queries. Here's one solution
This happens when some of the functions are not used. Dead functions.
To find these functions, open the HTML page under coverage/lcov-report/index.html
and go to the test file.
In our case, it was under /coverage/lcov-report/pages/admin/lessons/[lessonSlug]/[pageName]/index.tsx.html
.
Press n
to go to the next uncovered line.
In our case, they were these lines from the QueryInfo component:
In order for these lines to be covered, they need to be used. To achieve this, we'll write two test cases that closes (dismiss) the alert by clicking the X
icon.
it('Should close success alert', async () => {
expect.assertions(1)
render(
<MockedProvider mocks={mocks}>
<LessonPage />
</MockedProvider>
)
// Used to make the queries resolve
await act(() => new Promise(res => setTimeout(res, 0)))
await fillOutIntroductionForms()
fireEvent.click(screen.getByText('Save changes'))
await act(() => new Promise(res => setTimeout(res, 0)))
// The label text for the X icon is "Close alert"
await userEvent.click(await screen.findByLabelText('Close alert'))
expect(screen.queryByRole('alert')).not.toBeInTheDocument()
})
it('Should close error alert', async () => {
expect.assertions(1)
render(
<MockedProvider mocks={mocksWithError}>
<LessonPage />
</MockedProvider>
)
// Used to make the queries resolve
await act(() => new Promise(res => setTimeout(res, 0)))
await fillOutIntroductionForms()
fireEvent.click(screen.getByText('Save changes'))
await act(() => new Promise(res => setTimeout(res, 0)))
// The label text for the X icon is "Close alert"
await userEvent.click(await screen.findByLabelText('Close alert'))
expect(screen.queryByRole('alert')).not.toBeInTheDocument()
})
By adding these tests that'll make the functions for onDismissData
and onDismissError
be called, the column % Funcs
is now 100%
.
In order for it to remain in the loading state, we must declare the delay
property
const updateLessonMutationMockWithLoading = {
request: { query: UPDATE_LESSON, variables: js1 },
result: jest.fn(() => ({
data: {
updateLesson: js1
}
})),
delay: 100_000_000_000_000 // ~3170 years
}
The delay
property prevents the query from resolving for x milliseconds.
Sometimes, you might want to test how a module behaves when it's using global objects such as process.env
.
In our codebase, the GraphQL middleware is a good example of the issue. In this line, we're using process.env
to check if the environment the app is running on is either Vercel's preview deployment or local development.
It's being used in these lines. Based on isDevEnv
value truthiness, it's enabling the GraphQL Playground.
In the test file, we'd want to have two tests:
-
To check if the GraphQL Playground object is passed correctly
-
{ playground: { settings: { 'request.credentials': 'include' } }, introspection: isDevEnv }
-
-
To check if it's not enabled when
process.env.VERCEL_ENV
andprocess.env.NODE_ENV
is false
Our test file will look something like this:
// ...imports
describe('graphql api', () => {
const OLD_ENV = { ...process.env };
let apolloServerInput;
// Setting the process.env object back before each test
beforeEach(() => {
process.env = OLD_ENV;
// Mock (new ApolloServer()) function
asm.ApolloServer = function (data) {
// Sets apolloServerInput to whatever is being passed as ApolloServer options
apolloServerInput = data;
// ...
};
});
// Setting the process.env object back after each test
afterEach(() => {
process.env = OLD_ENV;
});
it('test1', () => {
process.env = { ...process.env, VERCEL_ENV: 'preview', NODE_ENV: 'development' };
// Import the file we'd want to execute
require('api/graphql.ts');
expect(apolloServerInput.someOption).toBeTruthy();
});
it('test2', () => {
process.env = { ...process.env, VERCEL_ENV: '', NODE_ENV: '' };
// Import the file we'd want to execute
require('api/graphql.ts');
expect(apolloServerInput.someOption).toBeFalsey();
});
});
When test1
runs, the process.env
that api/graphql.ts
reads is set to { ...otherProps, VERCEL_ENV: 'preview', NODE_ENV: 'development' }
.
Based on the functions beforeEach
and afterEach
, the process.env
value should return back to its original value that doesn't have VERCEL_ENV
and NODE_ENV
; which happens.
When test2
runs, the file isn't re-imported or in other words, doesn't re-execute its code because, in some sense, it got cached. test2
fails because apolloServerInput
value is still the same value set in test1
.
To prevent the module or file from being cached, Jest provides a useful function called isolateModules
.
isolateModules
creates a sandbox registry for the modules that are loaded inside the callback function. This is useful to isolate specific modules for every test so that the local module state doesn't conflict between tests.
To fix our code, we'll wrap both the body of the test with jest.isolateModules
:
describe('graphql api', () => {
// ...
it('Should enable GraphQL Playground', () => {
jest.isolateModules(() => {
process.env = {
...process.env,
VERCEL_ENV: 'preview',
NODE_ENV: 'development',
};
// Import the file we'd want to execute
require('api/graphql.ts');
expect(apolloServerInput.someOption).toBeTruthy();
});
});
it('Should not enable GraphQL Playground', () => {
jest.isolateModules(() => {
process.env = { ...process.env, VERCEL_ENV: '', NODE_ENV: '' };
// Import the file we'd want to execute
require('api/graphql.ts');
expect(apolloServerInput.someOption).toBeFalsey();
});
});
});
Now, whenever api/graphql.ts
is imported, it's treated like a new import with a different state, and it'll execute its code.