hibiken / react-places-autocomplete

React component for Google Maps Places Autocomplete
https://hibiken.github.io/react-places-autocomplete/
MIT License
1.38k stars 388 forks source link

How to handle loading google for test with enzyme / jest #189

Closed thetwosents closed 6 years ago

thetwosents commented 6 years ago

Do you want to request a feature or report a bug? Bug What is the current behavior? Throws error "Google Maps JavaScript API library must be loaded" when running npm run test.

If the current behavior is a bug, please provide the steps to reproduce and if possible a minimal demo of the problem.

create-react-app, setup react-places-autocomplete then install enzyme and jest. run npm test and it will break.

What is the expected behavior? Be able to run jest / enzyme without breaking app

Which versions of ReactPlacesAutocomplete, and which browser / OS are affected by this issue? Chrome,Mac, RPA6.1.3

hibiken commented 6 years ago

@thetwosents Thank you for opening this issue. Because PlacesAutocomplete component has a dependency on Google Maps Places library it has to be available under the window object. One way to get around this is to mock window.google object in your test. Example:

const setupGoogleMock = () => {
  /*** Mock Google Maps JavaScript API ***/
  const google = {
    maps: {
      places: {
        AutocompleteService: () => {},
        PlacesServiceStatus: {
          INVALID_REQUEST: 'INVALID_REQUEST',
          NOT_FOUND: 'NOT_FOUND',
          OK: 'OK',
          OVER_QUERY_LIMIT: 'OVER_QUERY_LIMIT',
          REQUEST_DENIED: 'REQUEST_DENIED',
          UNKNOWN_ERROR: 'UNKNOWN_ERROR',
          ZERO_RESULTS: 'ZERO_RESULTS',
        },
      },
      Geocoder: () => {},
      GeocoderStatus: {
        ERROR: 'ERROR',
        INVALID_REQUEST: 'INVALID_REQUEST',
        OK: 'OK',
        OVER_QUERY_LIMIT: 'OVER_QUERY_LIMIT',
        REQUEST_DENIED: 'REQUEST_DENIED',
        UNKNOWN_ERROR: 'UNKNOWN_ERROR',
        ZERO_RESULTS: 'ZERO_RESULTS',
      },
    },
  };
  global.window.google = google;
};

// in test file.
beforeAll(() => {
  setupGoogleMock();
});

Hope this helps!

con322 commented 6 years ago

Thanks for providing the above setup, however I get the following error:

TypeError: window.google.maps.Geocoder is not a constructor

Any ideas on how to fix?

chiamtc commented 6 years ago

@con322 I had the same problem before. window.google.maps.Geocoder is a class so just put Geocoder:class{},. If you are using function in google map classes, e.g: fitBounds() under window.google.maps.Map class: Just Map:class{ setTilt(){} fitBounds(){}},

Here's my google map classes mock under setupTests.js

window.google ={
    maps:{
        Marker:class{},
        Map:class{ setTilt(){} fitBounds(){}},
        LatLngBounds:class{},
        places:{
            Autocomplete: class {},
            AutocompleteService:class{},
            PlacesServiceStatus: {
                INVALID_REQUEST: 'INVALID_REQUEST',
                NOT_FOUND: 'NOT_FOUND',
                OK: 'OK',
                OVER_QUERY_LIMIT: 'OVER_QUERY_LIMIT',
                REQUEST_DENIED: 'REQUEST_DENIED',
                UNKNOWN_ERROR: 'UNKNOWN_ERROR',
                ZERO_RESULTS: 'ZERO_RESULTS',
            },
            PlacesAutocomplete:{
                INVALID_REQUEST: 'INVALID_REQUEST',
                NOT_FOUND: 'NOT_FOUND',
                OK: 'OK',
                OVER_QUERY_LIMIT: 'OVER_QUERY_LIMIT',
                REQUEST_DENIED: 'REQUEST_DENIED',
                UNKNOWN_ERROR: 'UNKNOWN_ERROR',
                ZERO_RESULTS: 'ZERO_RESULTS',
            }
        },

        MarkerClusterer:class{},
        Geocoder:class{},
    }
};
tim-rohrer commented 5 years ago

As a newbie, I appreciate this thread, but I continue to bang my head against the wall.

In my case, I'm trying to mock PlacesService.getDetails. Adding PlacesService: class {} resolves constructor issue, but leaves me with getDetails being undefined.

How do I define getDetails under PlacesService?

UPDATE: I may have gotten it figured out, but I don't understand it :-~

PlacesService: class { getDetails() {}},

OscarBarrett commented 4 years ago

I prefer to mock the library instead. Simple example:

interface Props {
  children: Function;
}

jest.mock('react-places-autocomplete', () => {
  const React = require('react'); // eslint-disable-line
  class PlacesAutocomplete extends React.Component<Props> {
    renderProps = {
      getInputProps: jest.fn(({ placeholder, className }) => ({ placeholder, className })),
      suggestions: [],
      getSuggestionItemProps: jest.fn(),
    };

    render() {
      return <>{this.props.children(this.renderProps)}</>;
    }
  }

  return PlacesAutocomplete;
});

Then you can still test changes to props, changes from within the render function etc that you couldn't do with shallow rendering, but you don't need to worry about internal requirements like the google API.

Buuntu commented 4 years ago

Has the API changed from AutocompleteService to AutocompletionService? I haven't been able to get this to work with cypress and was wondering if the API has changed. On the network tab it shows AutocompletionService.GetPredictions

suvasishm commented 3 years ago

I am using beta version of Maps Javascript API (to call reverse geocode) which returns Promise.

In order to mock google.maps.Geocode and google.maps.Geocode.geocode I did the following in line with the comment above.

export const setupGoogleMock = (): void => {
  window.google = {
    // eslint-disable-next-line @typescript-eslint/ban-ts-comment
    // @ts-ignore
    maps: {
      Geocoder: jest.fn().mockImplementation(() => {
        return {
          geocode: jest.fn().mockResolvedValue({
            results: [
              {
                formatted_address: `State St, Chicago, IL 60601, USA`,
              },
            ],
          }),
        };
      }),
    },
  };
};

test('reverseGeo', async () => {
  setupGoogleMock();
  expect(await reverseGeo({lat: 22.99, lng: 88.22})).toBe(`State St, Chicago, IL 60601, USA`);
});

It works for me to mock test the following function:

export const reverseGeo = async(latlng: google.maps.GeocoderRequest): Promise<string> => {
  const geocoder = new google.maps.Geocoder();
  const { results } = await geocoder.geocode({ location: latlng });
  if (results && results[0]) {
    return results[0].formatted_address;
  }
  return null;
}
aghArdeshir commented 3 years ago

In my case @OscarBarrett 's solution worked, with a small addition: window.google = undefined

So:

interface Props {
  children: Function;
}

jest.mock('react-places-autocomplete', () => {
  const React = require('react'); // eslint-disable-line
  class PlacesAutocomplete extends React.Component<Props> {
    renderProps = {
      getInputProps: jest.fn(({ placeholder, className }) => ({
        placeholder,
        className
      })),
      suggestions: [],
      getSuggestionItemProps: jest.fn()
    };

    render() {
      return <>{this.props.children(this.renderProps)}</>;
    }
  }

  return PlacesAutocomplete;
});

window.google = undefined;
dulcineapena1 commented 2 years ago

@suvasishm I follow your example, it seems to work, but when I check the "coverage", it seems the test did not cover too much...

suvasishm commented 2 years ago

@suvasishm I follow your example, it seems to work, but when I check the "coverage", it seems the test did not cover too much...

Well, the goal was to having a functional mock for the library API. Coverage would probably depend on what your code does with the mocked response.

georgesh96 commented 2 years ago

I am still having issue with the mock, I am a newbee here with jest testing but I get an error of dispatch of null when I mock the useplaceservice I am trying to test getPredictions function

ghost commented 2 years ago

Bless @hibiken and @chiamtc

You guys solved a problem I had for two days until I found your solutions and mixed them for my case. You are heroes! Thank you!

nathan-ch commented 1 year ago

Hi, thank you for the solution. I still had this error : e.LatLng is not a constructor

I fixed it by adding LatLng: class {} but then i have this one that i don't know how to fix :

Cannot read properties of undefined (reading 'match')

moehajj commented 1 year ago

Google has an official maps mocking library now, looks neat.