1

I know I need to pass the context to the component under test and I've tried a few different ways, but I can't seem to make it work. Under this setup, I'm getting this error:

    TypeError: _react.default.useContext is not a function or its return value is not iterable

       7 |
       8 | function MyComponent(props) {
    >  9 |   const [locale, setLocale] = React.useContext(LocaleContext);
         |                                     ^

Any advice on what I'm doing wrong? Am I better off switching to enzyme or react-test-renderer?

App.js

import React from "react";
import { IntlProvider } from "react-intl";

import MyComponent from "./components/MyComponent ";

import "./App.css";

import { LocaleContext } from "./LocaleContext";
const messages = { zh: require("./translations/zh") };

function App() {
  const [locale] = React.useContext(LocaleContext);

  return (
    <IntlProvider locale={locale} messages={messages[locale]}>
      <div className="App">
        <MyComponent />
      </div>
    </IntlProvider>
  );
}

export default App;

LocaleContext.js

import React, { useContext } from "react";
export const LocaleContext = React.createContext();
export const useLocaleContext = () => useContext(LocaleContext);

export const LocaleContextProvider = props => {
  const [locale, setLocale] = React.useState("en");
  return (
    <LocaleContext.Provider value={[locale, setLocale]}>
      {props.children}
    </LocaleContext.Provider>
  );
};

MyComponent.js

import React from "react";
import { FormattedMessage } from "react-intl";

import { LocaleContext } from "../LocaleContext";

import logo from "../logo.svg";

function MyComponent(props) {
  const [locale, setLocale] = React.useContext(LocaleContext);
  const nextLocale = locale === "en" ? "zh" : "en";

  return (
    <header className="App-header">
      <img src={logo} className="App-logo" alt="logo" />
      <h1>
        <FormattedMessage id="title" defaultMessage="Hello World!" />
      </h1>
      <h2>
        <FormattedMessage id="subtitle" defaultMessage="Welcome to our app" />
      </h2>
      <button onClick={() => setLocale(nextLocale)}>
        Change language to {nextLocale}
      </button>
    </header>
  );
}

export default MyComponent;

MyComponent.test.js

import React from "react";
import { render } from "@testing-library/react";

import * as LocaleContext from "../LocaleContext";
import MyComponentfrom "./MyComponent";

test("renders `hello world` heading", () => {
  const contextValues = { title: "Hey There" };
  jest
    .spyOn(LocaleContext, "useLocaleContext")
    .mockImplementation(() => contextValues);

  const { getByText } = render(<MyComponent/>);
  const helloWorldText = getByText(/hello world/i);
  expect(helloWorldText).toBeInTheDocument();
});

package.json

{
  "scripts": {
    ...
    "test": "react-scripts test"
  },
  "dependencies": {
    "@testing-library/jest-dom": "^4.2.4",
    "@testing-library/react": "^9.3.2",
    "react": "^16.13.1",
    "react-intl": "^4.6.9",
    "react-scripts": "3.4.1"
    ...
  }
  ...
}
carpiediem
  • 1,918
  • 22
  • 41
  • 1
    Consider removing error message from the title because it was truncated to the point it becomes misleading. The most important part is `its return value is not iterable`. useContext doesn't return an array, this causes the error. And it doesn't return an array because no value for the context was provided. It needs LocaleContextProvider, as the answer suggests. – Estus Flask Jun 20 '20 at 08:02
  • Thanks for the heads up. Fact is, I assumed I had set something up wrong and it genuinely wasn't a function. I'll fix the title for others' benefit. – carpiediem Jun 22 '20 at 05:32
  • Yes, the error is ambiguous, I addressed it some time ago, https://stackoverflow.com/questions/61772822/the-meaning-of-x-is-not-a-function-or-its-return-value-is-not-iterable-error . The error would be the same if useContext was indeed not a function, but since it's not possible for React 16.13 or higher, this can be ruled out. – Estus Flask Jun 22 '20 at 07:20

2 Answers2

3

drew-reese gets credit for the answer, but I wanted to follow up the code I used, since it didn't end up using LocaleContext in the wrapper.

This also involved installing an extra dependency to compile with ICU.

npm i --save-dev full-icu

LocaleContext.js

import React, { useContext } from "react";
import { IntlProvider } from "react-intl";

const messages = { zh: require("./translations/zh") };

export const LocaleContext = React.createContext();
export const useLocaleContext = () => useContext(LocaleContext);

export const LocaleContextProvider = props => {
  const [locale, setLocale] = React.useState("en");
  return (
    <LocaleContext.Provider value={[locale, setLocale]}>
      {props.children}
    </LocaleContext.Provider>
  );
};

export const intlEnWrapper = {
  wrapper: ({ children }) => <IntlProvider locale="en" messages={messages.en}>{children}</IntlProvider>
};
export const intlZhWrapper = {
  wrapper: ({ children }) => <IntlProvider locale="zh" messages={messages.zh}>{children}</IntlProvider>
};

MyComponent.test.js

import React from "react";
import { render } from "@testing-library/react";

import { intlEnWrapper, intlZhWrapper } from "../../LocaleContext";
import MyComponentfrom "./index";

describe("For en locale", () => {
  test("renders `Title Text` heading", () => {
    const { getByText } = render(<MyComponent/>, intlEnWrapper);
    const titleText = getByText(/title text/i);
    expect(titleText).toBeInTheDocument();
  });
});

describe("For zh locale", () => {
  test("renders `Title Text` heading", () => {
    const { getByText } = render(<MyComponent />, intlZhWrapper);
    const titleText = getByText(/標題文字/i);
    expect(titleText).toBeInTheDocument();
  });
});

package.json

{
  "scripts": {
    ...
    "test": "cross-env NODE_ICU_DATA=node_modules/full-icu react-scripts test"
  }
}
    
HoldOffHunger
  • 18,769
  • 10
  • 104
  • 133
carpiediem
  • 1,918
  • 22
  • 41
2

You may want to study the wrapper api, and setup for a custom render, but the gist is you create a test wrapper that provides the context provider for testing.

For example, I use react-intl so for testing I have a test utility intlWrapper

import React from 'react';
import { IntlProvider } from 'react-intl';

export const intlWrapper = ({ children }) => (
  <IntlProvider locale="en">{children}</IntlProvider>
);

And to test a component it is used as such

const {/* query selectors */} = render(
  <ComponentUsingIntl />,
  { wrapper: intlWrapper },
);

To suit your needs I think you should create a wrapper for your LocaleContextProvider

import { LocaleContextProvider } from '../LocaleContext';

export const contextWrapper = ({ children }) => (
  <LocaleContextProvider>{children}</LocaleContextProvider>
);

You can now import your test contextWrapper and use

const { getByText } = render(<MyComponent/>, { wrapper: contextWrapper });
Drew Reese
  • 165,259
  • 14
  • 153
  • 181