Google jsdom 中加载的地图缺少地图图块和控件

Google Map loaded in jsdom is missing map tiles and controls

我正在尝试获取要在 jsdom so I can test map/mouse events with react-testing-library 中加载的 Google 地图。遗憾的是,<img> 地图图块和 <button> 地图控件未加载到 dom。我没有从 Google 地图或 jsdom 收到任何错误消息,所以我不确定问题出在哪里。

我正在使用以下软件包:

canvas@2.6.1
jest@26.1.0
jsdom@16.2.2
@testing-library/jest-dom@5.11.0
@testing-library/react@10.4.4

这是一个最小的例子,基于 Google 的 synchronous map loading 示例(编辑了 API 键):

import { JSDOM } from 'jsdom';
import '@testing-library/jest-dom/extend-expect';
import { render, waitFor } from '@testing-library/react';

const dom = new JSDOM(`
  <!DOCTYPE html>
  <html>
    <head>
      <title>Synchronous Loading</title>
      <meta name="viewport" content="initial-scale=1.0">
      <meta charset="utf-8">
      <style>
        #map {
          height: 100%;
        }
      </style>
    </head>
    <body>
      <div id="map"></div>
      <script src="https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY"></script>
      <script>
        var map = new google.maps.Map(document.getElementById('map'), {
          center: {lat: -34.397, lng: 150.644},
          zoom: 8
        });
      </script>
    </body>
  </html>
`, {
  pretendToBeVisual: true,
  resources: 'usable',
  runScripts: 'dangerously',
});

global.window = dom.window;
global.document = dom.window.document;

describe('Google Map', () => {
  it('has a zoom button', async () => {
    const { queryByRole } = render();  // just checking the dom
    await waitFor(() => expect(queryByRole('button', { name: 'Zoom in' })).toBeInTheDocument(), { timeout: 5000 });
  });
});

此测试因超时而失败。调试器显示地图 div 有以下内容,缺少很多它应该有的内容(地图图块、地图控件等):

<div id="map" style="position: relative; overflow: hidden;">
  <div style="height: 100%; width: 100%; position: absolute; top: 0px; left: 0px; background-color: rgb(229, 227, 223);">
    <div style="overflow: hidden;"/>
    <div class="gm-style" style="position: absolute; z-index: 0; left: 0px; top: 0px; height: 100%; width: 100%; padding: 0px; border-width: 0px; margin: 0px;">
      <div style="position: absolute; z-index: 0; left: 0px; top: 0px; height: 100%; width: 100%; padding: 0px; border-width: 0px; margin: 0px;cursor: url(https://maps.gstatic.com/mapfiles/openhand_8_8.cur), default;" tabindex="0">
        <div style="z-index: 1; position: absolute; left: 50%; top: 50%; width: 100%; transform: translate(0px,0px);">
          <div style="position: absolute; left: 0px; top: 0px; z-index: 100; width: 100%;">
            <div style="position: absolute; left: 0px; top: 0px; z-index: 0;">
              <div style="position: absolute; z-index: 992; transform: matrix(1,0,0,1,-32,-20);">
                <div style="position: absolute; left: 0px; top: 0px; width: 256px; height: 256px;">
                  <div style="width: 256px; height: 256px;" />
                </div>
              </div>
            </div>
          </div>
          <div style="position: absolute; left: 0px; top: 0px; z-index: 101; width: 100%;" />
          <div style="position: absolute; left: 0px; top: 0px; z-index: 102; width: 100%;" />
          <div style="position: absolute; left: 0px; top: 0px; z-index: 103; width: 100%;" />
          <div style="position: absolute; left: 0px; top: 0px; z-index: 0;" />
        </div>
        <div class="gm-style-pbc" style="z-index: 2; position: absolute; height: 100%; width: 100%; padding: 0px; border-width: 0px; margin: 0px left: 0px; top: 0px; transition-duration: 0; opacity: 0;">
          <p class="gm-style-pbt" />
        </div>
        <div style="z-index: 3; position: absolute; height: 100%; width: 100%; padding: 0px; border-width: 0px; margin: 0px; left: 0px; top: 0px;">
          <div style="z-index: 4; position: absolute; left: 50%; top: 50%; width: 100%; transform: translate(0px,0px);">
            <div style="position: absolute; left: 0px; top: 0px; z-index: 104; width: 100%;" />
            <div style="position: absolute; left: 0px; top: 0px; z-index: 105; width: 100%;" />
            <div style="position: absolute; left: 0px; top: 0px; z-index: 106; width: 100%;" />
            <div style="position: absolute; left: 0px; top: 0px; z-index: 107; width: 100%;" />
          </div>
        </div>
      </div>
      <iframe aria-hidden="true" frameborder="0" style="z-index: -1; position: absolute; width: 100%; height: 100%; top: 0px; left: 0px;" tabindex="-1" />
    </div>
  </div>
</div>

也许它在到达 <iframe> 时遇到错误(因为地图控件应该在那之后出现),但我还没有找到进一步调试问题的方法。有什么方法可以让这个地图在 jsdom?

中完全加载

maps google 如何决定是否创建地图:

Google 地图在确保容器可见且大小 > 0 之前不会装载地图(及其控件)。

(您可以通过在 .html 文件中写入 html 并为容器设置 height: 0 样式来验证它。然后您会看到它不会创建地图(使用 inspector) 直到你设置 height: 400px.)

jsdom pretendToBeVisual: true 的工作原理:

jsdom pretendToBeVisual: true 并没有真正渲染任何东西。所以 getBoundingClientRectclientHeightclientWidth 以及...将 return 0.

Jsdon/Google-maps 渲染问题:

当您同时使用 googl-mapsjsdom 时:google-maps 将加载,但不会创建实际地图,因为它认为容器大小为 0 或者不是可见。

解决方案:

我试图覆盖与 size 相关的 DomElement 方法和属性,例如:offsetHeightclientHeight 以及 getBoundingClientRect(对于所有元素),但是由于有很多attributes/methods和size有关,全部覆盖很费时间,不是明智的选择。 (请注意,要覆盖 offsetHeight 等只读属性,您不能使用普通的覆盖方法。相反,您应该使用 Object.defineProperty 来覆盖它。)

所以我建议尝试寻找具有渲染支持的 jsdom 的替代方案(或使用在真实浏览器上执行的测试库)/或尝试阅读 google-maps 源代码并找到了解如何 resize 事件处理程序检查容器是否可见,并覆盖所有这些方法。

(如果您不关心是否是最新的,您也可以尝试检查是否可以找到旧版本的 google-maps,它始终呈现地图。)

感谢 @yaya 的回答,我能够通过模拟与大小相关的属性和方法来挂载地图。通过添加以下代码按钮测试通过:

const mockWidth = 200;
const mockHeight = 200;

function shouldMockRender(element) {
  const map = dom.window.document.getElementById('map');
  const isMap = element.id === 'map';
  const isChild = map && map.contains(element);
  return isMap || isChild;
}

Object.defineProperty(dom.window.HTMLElement.prototype, 'clientWidth', {
  get() {
    return shouldMockRender(this) ? mockWidth : 0;
  },
});

Object.defineProperty(dom.window.HTMLElement.prototype, 'clientHeight', {
  get() {
    return shouldMockRender(this) ? mockHeight : 0;
  },
});

dom.window.HTMLElement.prototype.getBoundingClientRect = function getBoundingClientRect() {
  return {
    x: 0,
    y: 0,
    width: this.clientWidth,
    height: this.clientHeight,
    top: 0,
    right: this.clientWidth,
    bottom: this.clientHeight,
    left: 0,
  };
};

顺便说一句,即使在测试完成后,进程仍保持 运行。关闭 window 修复了:

describe('Google Map', () => {
  afterAll(() => window.close());

  it('has a zoom button', async () => {
    const { queryByRole } = render(); // just checking the dom
    await waitFor(() => expect(queryByRole('button', { name: 'Zoom in' })).toBeInTheDocument(), { timeout: 5000 });
  });
});