Skip to content

Troubleshooting

Flacial edited this page Dec 12, 2022 · 9 revisions

Here's a list of the most common errors you might encounter and how you could fix them.

Setup

  • 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

Testing

Prerequisites

Understand both of these docs

  1. How to test React Component using Apollo queries
  2. 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'

Found multiple elements with the text: {TEXT}

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:

image

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)

Assertion fails when an async code is run before it

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()
    })
}    

Errors occur after updating GetApp query

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

% Funcs column in Jest coverage is low

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:

image

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%.

The query doesn't remain in the loading state

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.

Modules state conflict

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:

  1. To check if the GraphQL Playground object is passed correctly

    • {
          playground: {
            settings: {
              'request.credentials': 'include'
            }
          },
          introspection: isDevEnv
        }
  2. To check if it's not enabled when process.env.VERCEL_ENV and process.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.