Search code examples
javascriptgoogle-maps-api-3jestjsreact-testing-libraryjsdom

Google Map loaded in jsdom is missing map tiles and controls


I'm trying to get a Google Map to load in jsdom so I can test map/mouse events with react-testing-library. Unfortunately, the <img> map tiles and <button> map controls are not loading into the dom. I'm not getting any error messages from Google Maps or jsdom, so I'm not sure what the problem is.

I'm using the following packages:

[email protected]
[email protected]
[email protected]
@testing-library/[email protected]
@testing-library/[email protected]

Here's a minimal example, based on Google's example of synchronous map loading (with the API key redacted):

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

This test fails with a timeout. The debugger shows the map div has the following content, which is missing a lot of what it should have (map tiles, map controls, etc.):

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

Perhaps it's hitting an error when it gets to the <iframe> (since the map controls should come after that), but I haven't found a way to further debug the issue. Is there any way to get this map to load completely in jsdom?


Solution

  • How google maps decides to create the map or not:

    Google maps will not mount the map (and its controls) until it makes sure the container is visually visible and has size > 0.

    (You can verify it by writing your html in an .html file and setting height: 0 style for container. then you'll see it wont create the map (use inspector) until you set the height: 400px.)

    How jsdom pretendToBeVisual: true works:

    jsdom pretendToBeVisual: true does not really really render anything. so getBoundingClientRect, clientHeight and clientWidth and ... will return 0.

    The Jsdon/Google-maps Rendering Problem:

    When you use googl-maps and jsdom together: google-maps will load, but it doesn't create the actual map, since it thinks the container size is 0 or it's not visible.

    The Solution:

    I tried to override DomElement methods and attributes that are relating to size, like: offsetHeight and clientHeight and getBoundingClientRect (for all elements), but since there is alot of attributes/methods related to size, and it's time consuming to override all of them, it's not a wise choice. (Note that to override read-only properties like offsetHeight, you can't use normal overriting methods. instead you should override it with: Object.defineProperty.)

    So i recomment to try finding an alternative to jsdom with rendering support (or to use a test library that executes on real browser) / or try to read the google-maps source code and find out how resize event handler checks if the container is visible or not, and overwrite all of that methods.

    (also if you don't care about being up-to-date, you can also try to check if you can find an older version of google-maps that it always renders the map.)