How to "mock" navigator.geolocation in a React Jest Test
Asked Answered
T

10

26

I'm trying to write tests for a react component I've built that utilizes navigator.geolocation.getCurrentPosition() within a method like so (rough example of my component):

class App extends Component {

  constructor() {
    ...
  }

  method() {
    navigator.geolocation.getCurrentPosition((position) => {
       ...code...
    }
  }

  render() {
    return(...)
  }

}

I'm using create-react-app, which includes a test:

it('renders without crashing', () => {
  const div = document.createElement('div');
  ReactDOM.render(<App />, div);
});

This test fails, printing out this in the console:

TypeError: Cannot read property 'getCurrentPosition' of undefined

I'm new to React, but have quite a bit of experience with angular 1.x. In angular it is common to mock out (within the tests in a beforeEach) functions, "services", and global object methods like navigator.geolocation.etc. I spent time researching this issue and this bit of code is the closest I could get to a mock:

global.navigator = {
  geolocation: {
    getCurrentPosition: jest.fn()
  }
}

I put this in my test file for App, but it had no effect.

How can I "mock" out this navigator method and get the test to pass?

EDIT: I looked into using a library called geolocation which supposedly wraps navigator.getCurrentPosition for use in a node environment. If I understand correctly, jest runs tests in a node environment and uses JSDOM to mock out things like window. I haven't been able to find much information on JSDOM's support of navigator. The above mentioned library did not work in my react app. Using the specific method getCurrentPosition would only return undefined even though the library itself was imported correctly and available within the context of the App class.

Trevino answered 24/3, 2017 at 20:51 Comment(2)
Possible duplicate of How do I configure jsdom with jestToddtoddie
@jordan, could you explain why you think it's a duplicate? I looked into that answer and tried to fix this issue by using the geolocation library, which is a similar solution to the "node-friendly stub like 'node-localstorage'." solution described. But within the context of my app, the geolocation.getCurrentPosition returned undefined, not sure why it doesn't work. An actual explanation of how to go about solving this specific issue would be far more helpful.Trevino
M
42

It appears that there is already a global.navigator object and, like you, I wasn't able to reassign it.

I found that mocking the geolocation part and adding it to the existing global.navigator worked for me.

const mockGeolocation = {
  getCurrentPosition: jest.fn(),
  watchPosition: jest.fn()
};

global.navigator.geolocation = mockGeolocation;

I added this to a src/setupTests.js file as described here - https://create-react-app.dev/docs/running-tests#initializing-test-environment

Moody answered 13/5, 2017 at 20:26 Comment(3)
When I tried your solution I have this error : 'TypeError: Cannot set property 'geolocation' of undefined' I tried to defined it like that : "global.navigator = { userAgent: 'node.js' };" And I have this error "ReferenceError: initialize is not defined". I don't know what to do to resolve that. Do you have an idea of what to do in my situation ?Bailly
This solution is still working for me. Make sure you define it in src/setupTests.js github.com/facebook/create-react-app/blob/master/packages/…Moody
Latest documentation can be found here: facebook.github.io/create-react-app/docs/…Straka
A
32

I know this issue might have been solved, but seems that all the solutions above are all wrong, at least for me.

When you do this mock: getCurrentPosition: jest.fn() it returns undefined, if you want to return something, this is the correct implementation:

const mockGeolocation = {
  getCurrentPosition: jest.fn()
    .mockImplementationOnce((success) => Promise.resolve(success({
      coords: {
        latitude: 51.1,
        longitude: 45.3
      }
    })))
};
global.navigator.geolocation = mockGeolocation;

I am using create-react-app

Awake answered 13/8, 2018 at 19:45 Comment(4)
Worked like a charmOverdress
Great! Thank you for that! But do you also know how to trigger the error function?Bedell
with TS, it gives an error: "Cannot assign to 'geolocation' because it is a read-only property."Dixson
@GreatQuestion for the read- only restriction, I believe it is a TypeScript restriction, a potential work around is to disable the Type on that particular line.. As mentioned it's a work around I got my solution from this question: #55713140Barcellona
A
8

A TypeScript version for anyone that was getting Cannot assign to 'geolocation' because it is a read-only property.

In the mockNavigatorGeolocation.ts file (this can live in a test-utils folder or similar)

export const mockNavigatorGeolocation = () => {
  const clearWatchMock = jest.fn();
  const getCurrentPositionMock = jest.fn();
  const watchPositionMock = jest.fn();

  const geolocation = {
    clearWatch: clearWatchMock,
    getCurrentPosition: getCurrentPositionMock,
    watchPosition: watchPositionMock,
  };

  Object.defineProperty(global.navigator, 'geolocation', {
    value: geolocation,
  });

  return { clearWatchMock, getCurrentPositionMock, watchPositionMock };
};

I then import this in my test at the top of the file:

import { mockNavigatorGeolocation } from '../../test-utils';

And then use the function like so:

const { getCurrentPositionMock } = mockNavigatorGeolocation();
getCurrentPositionMock.mockImplementation((success, rejected) =>
  rejected({
    code: '',
    message: '',
    PERMISSION_DENIED: '',
    POSITION_UNAVAILABLE: '',
    TIMEOUT: '',
  })
);
Amphidiploid answered 9/4, 2021 at 10:56 Comment(2)
If I run npm test, it throws an error: 'TypeError: Cannot redefine property: geolocation at Function.defineProperty (<anonymous>)'Dixson
try adding configurable: true to the options object in Object.definePropertyAmphidiploid
K
5

Mocking with setupFiles

// __mocks__/setup.js

jest.mock('Geolocation', () => {
  return {
    getCurrentPosition: jest.fn(),
    watchPosition: jest.fn(),
  }
});

and then in your package.json

"jest": {
  "preset": "react-native",
  "setupFiles": [
    "./__mocks__/setup.js"
  ]
}
Kenay answered 11/8, 2017 at 10:37 Comment(0)
R
4

I followed @madeo's comment above to mock global.navigator.geolocation. It worked!

Additionally I did the following to mock global.navigator.permissions:

  global.navigator.permissions = {
    query: jest
      .fn()
      .mockImplementationOnce(() => Promise.resolve({ state: 'granted' })),
  };

Set state to any of granted, denied, prompt as per requirement.

Roughshod answered 24/11, 2020 at 22:16 Comment(0)
T
1

For whatever reason, I did not have the global.navigator object defined, so I had to specify it in my setupTests.js file

const mockGeolocation = {
  getCurrentPosition: jest.fn(),
  watchPosition: jest.fn(),
}
global.navigator = { geolocation: mockGeolocation }
Tench answered 28/1, 2019 at 12:17 Comment(0)
I
1

It is better to actually use spy in this case to avoid assigning to readonly property if you are using typescript.

import { mock } from 'jest-mock-extended'

jest
  .spyOn(global.navigator.geolocation, 'getCurrentPosition')
  .mockImplementation((success) =>
    Promise.resolve(
      success({
        ...mock<GeolocationPosition>(),
        coords: {
          ...mock<GeolocationCoordinates>(),
          latitude: 51.1,
          longitude: 45.3,
        },
      }),
    ),
  )
Indoor answered 29/3, 2023 at 21:11 Comment(0)
P
0

Added to the above answers, if you want to update navigator.permissions, this will work.The key here is to mark writable as true before mocking

Object.defineProperty(global.navigator, "permissions", {
   writable: true,
   value: {
    query : jest.fn()
    .mockImplementation(() => Promise.resolve({ state: 'granted' }))
   },
});
Pantin answered 20/12, 2022 at 14:38 Comment(0)
A
0

I just encountered this myself using Vitest and TypeScript. You can simply replace vitest (vi) with jest. I needed to mock the user accepting the request for their location. This was my solution:

// Mock the geolocation object
const mockedGeolocation = {
    getCurrentPosition: vi.fn((success, _error, _options) => {
        success({
            coords: {
                latitude: 0,
                longitude: 0,
                accuracy: 0,
            },
        });
    }),
    watchPosition: vi.fn(),
};
//Overwrite the properties on naviagtor
Object.defineProperty(global.navigator, "geolocation", {
    writable: true,
    value: mockedGeolocation,
});

Object.defineProperty(global.navigator, "permissions", {
    writable: true,
    value: {
        query: vi
            .fn()
            .mockImplementation(() =>
                Promise.resolve({ state: "granted" }),
            ),
    },
})
Arginine answered 22/9, 2023 at 19:19 Comment(0)
T
0

Some of these answers are helpful, but they don't quite get at the essence of navigator.geolocation.watchPosition(). It does not return a Promise. It returns a Number (a watchId), and periodically executes the provided callback function until .clearWatch() is executed. Here's the test that worked for me to verify the expected functionality:

const mockWatchId = 12345;
const mockResponse = {"mock response": true};
global.navigator.geolocation = {
    "watchPosition": jest.fn().mockImplementation((callback) => {
        callback(mockResponse);
        return mockWatchId;
    });
};

I follow that up with expect() calls to verify that .watchPosition() returns the appropriate watchId and calls the appropriate callback.

Hope this helps!

Torr answered 13/6, 2024 at 14:13 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.