如何在React Jest测试中“模拟”navigator.geolocation?

25

我正在尝试为我构建的一个利用navigator.geolocation.getCurrentPosition()方法的React组件编写测试,代码大致如下:

class App extends Component {

  constructor() {
    ...
  }

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

  render() {
    return(...)
  }

}

我正在使用create-react-app,其中包含一个测试:

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

这个测试失败了,在控制台输出如下信息:

TypeError: Cannot read property 'getCurrentPosition' of undefined

我对React还不熟悉,但是在angular 1.x方面有相当多的经验。在angular中,通常会在测试的beforeEach函数中模拟函数、"services"和全局对象方法,例如navigator.geolocation等。我花了时间研究这个问题,这段代码是我能找到的最接近模拟的代码:

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

我将这段代码放在我的App测试文件中,但是它没有产生任何效果。

我该如何“模拟”这个navigator方法并使测试通过?

编辑:我研究了一个名为geolocation的库,它据说可以包装navigator.getCurrentPosition以在node环境中使用。如果我理解正确,jest在node环境中运行测试,并使用JSDOM模拟一些东西,比如window。我没有找到关于JSDOM对navigator的支持的太多信息。上述提到的库在我的react应用程序中不起作用。即使正确导入库并在App类的上下文中可用,使用特定的getCurrentPosition方法仍将返回undefined。


可能是重复的问题,参考如何使用Jest配置jsdom - Jordan Running
1
@jordan,你能解释一下为什么你认为这是一个重复的问题吗?我查看了那个答案,并尝试使用geolocation库来解决这个问题,这是与“类似于'node-localstorage'的节点友好存根”的解决方案相似的解决方案。但在我的应用程序上下文中,geolocation.getCurrentPosition返回未定义,不知道为什么它不起作用。关于如何解决这个特定问题的实际解释将会更有帮助。 - timothym
9个回答

41

看起来已经有一个global.navigator对象,就像你一样,我也无法重新分配它。

我发现模拟地理位置部分并将其添加到现有的global.navigator中对我很有用。

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

global.navigator.geolocation = mockGeolocation;

我按照这里描述的方法把这段代码添加到了src/setupTests.js文件中 - https://create-react-app.dev/docs/running-tests#initializing-test-environment


4
当我尝试使用你的解决方案时,出现了这个错误:“TypeError: Cannot set property 'geolocation' of undefined”。我试图这样定义它: "global.navigator = { userAgent: 'node.js' };" 然后我出现了这个错误:“ReferenceError:initialize未定义”。我不知道该怎么解决。你有任何想法吗? - zagoa
这个解决方案对我仍然有效。请确保您在src/setupTests.js中进行了定义 https://github.com/facebook/create-react-app/blob/master/packages/react-scripts/template/README.md#initializing-test-environment - Joseph Race
1
最新文档可以在这里找到:https://facebook.github.io/create-react-app/docs/running-tests#initializing-test-environment - swapab

31

我知道这个问题可能已经解决了,但似乎上面所有的解决方案都是错误的,至少对我来说是这样。

当你进行这个mock操作:getCurrentPosition: jest.fn()时, 它会返回undefined,如果你想要返回一些东西,这是正确的实现方式:

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

我正在使用create-react-app


1
像魔法般好用 - Marcelo Cardoso
太好了!谢谢你!但是你知道如何触发“error”函数吗? - Juuro
使用TS时,会出现错误:“无法分配给'geolocation',因为它是只读属性。” - philomath
@GreatQuestion 对于只读限制,我认为这是 TypeScript 的限制,一个潜在的解决方法是禁用该特定行上的类型。正如提到的那样,这是一个解决方法,我从这个问题中得到了我的解决方案: https://dev59.com/PlMI5IYBdhLWcg3wn9H6 - tony2tones

8

对于那些遇到“无法赋值给‘geolocation’,因为它是只读属性”的人,这里有一个TypeScript版本。

mockNavigatorGeolocation.ts文件中(可以放在test-utils文件夹或类似位置)。

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 };
};

我会将这个文件在我的测试文件的顶部导入:

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

然后像这样使用该函数:

const { getCurrentPositionMock } = mockNavigatorGeolocation();
getCurrentPositionMock.mockImplementation((success, rejected) =>
  rejected({
    code: '',
    message: '',
    PERMISSION_DENIED: '',
    POSITION_UNAVAILABLE: '',
    TIMEOUT: '',
  })
);

1
如果我运行 npm test,它会抛出一个错误:'TypeError: Cannot redefine property: geolocation at Function.defineProperty (<anonymous>)' - philomath
尝试在Object.defineProperty的选项对象中添加configurable: true - Jamie

5

使用setupFiles进行模拟测试

// __mocks__/setup.js

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

然后在你的package.json文件中进行如下操作:

"jest": {
  "preset": "react-native",
  "setupFiles": [
    "./__mocks__/setup.js"
  ]
}

4
我跟随@madeo的评论来模拟全局变量global.navigator.geolocation。它起作用了!另外,我还通过以下方式模拟了global.navigator.permissions:
  global.navigator.permissions = {
    query: jest
      .fn()
      .mockImplementationOnce(() => Promise.resolve({ state: 'granted' })),
  };

根据需求,将state设置为granteddeniedprompt中的任意一个。


1
无论出于何种原因,我没有定义全局的navigator对象,因此我必须在我的setupTests.js文件中指定它。
const mockGeolocation = {
  getCurrentPosition: jest.fn(),
  watchPosition: jest.fn(),
}
global.navigator = { geolocation: mockGeolocation }

0

除了上面的答案之外,如果你想更新navigator.permissions,这样会起作用。关键在于在模拟之前将writable标记为true

Object.defineProperty(global.navigator, "permissions", {
   writable: true,
   value: {
    query : jest.fn()
    .mockImplementation(() => Promise.resolve({ state: 'granted' }))
   },
});

0

如果你在使用TypeScript,最好实际上使用spy来避免给只读属性赋值。

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

0
我自己在使用Vitest和TypeScript时遇到了这个问题。你可以简单地将vitest(vi)替换为jest。我需要模拟用户接受请求并提供他们的位置。这是我的解决方案:
// 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" }),
            ),
    },
})

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接