在 Cypress 中存根 Google Maps JS API 加载器

Stubbing Google Maps JS API loader in Cypress

我正在使用 Google Maps JS API in my app for autocompletion and geocoding. The API is loaded using the Google Maps JS API Loader。当我使用 Cypress 进行端到端测试时,我试图模拟 API,但我不知道如何让它工作。

我在一个单独的模块中抽象了 API。这是它的加载和实例化方式(代码简化为要点):

import {Loader} from "@googlemaps/js-api-loader";

// The Google Maps API has to be loaded dynamically.
// We have to do this here as it is loaded asynchronously and otherwise we won't
//  be able to just create the class below.
let loading: Promise<typeof google>;
try {
  loading = new Loader({
    apiKey: "private key",
    libraries: ["places"],
  }).load();
} catch (e) {
  console.error("Failed to load Google Maps API");
}

class GoogleMapsAPI {
  private autocompleteService: google.maps.places.AutocompleteService;
  private readonly sessionToken: google.maps.places.AutocompleteSessionToken;
  private predictions: google.maps.places.AutocompletePrediction[];

  constructor() {
    this.autocompleteService = new google.maps.places.AutocompleteService();

    // As an instance is created every time we use this, we set the session here
    this.sessionToken = new google.maps.places.AutocompleteSessionToken();
    this.predictions = []
  }

  public async getPredictions(input: string): Promise<google.maps.places.AutocompletePrediction[]> {
    // Search for the input string and store the returned results
    try {
      const result = await this.autocompleteService.getPlacePredictions({
        input,
        sessionToken: this.sessionToken
      });

      // Replace the previous predictions
      this.predictions = result.results;
      return this.predictions;
    } catch (e: any) {
      console.error(`Autocomplete call failed: ${e}`);
      return [];
    }
  }
}

async function loadGoogleMapsAPI(): Promise<GoogleMapsAPI | null> {
  // Wait for the loader to load the API, then return the API class.
  try {
    await loading;

    return new GoogleMapsAPI();
  } catch (e) {
    console.error(`Google Maps API could not be loaded`);
  }

  return null;
}

这就是我在 React 应用程序中使用 API 的方式:

import React, {useEffect, useState} from "react";

import {GoogleMapsAPI, loadGoogleMapsAPI} from "../../services/google";

export default function Site() {
  const [api, setAPI] = useState<GoogleMapsAPI | null>(null);

  useEffect(() => {
    loadGoogleMapsAPI()
      .then((api) => {
        setAPI(api);
      })
  });

  return <div>API valid: {api ? "yes" : "no"}</div>;
}

这就是我尝试测试它的方式:

import {Loader} from "@googlemaps/js-api-loader";


describe("Test", () => {
  it("Google Maps not found", () => {
    cy.visit(
      "/",
      {
        onBeforeLoad(win: Cypress.AUTWindow) {
          // Mock the load method of Loader to prevent it from ever requesting
          //  the Google Maps API script file
          cy.stub(Loader.prototype, "load").resolves(true);

          // Replace the Google Maps API created by the Google Maps API Loader
          //  with our mocks
          win.google = {
            maps: {
              places: {
                AutocompleteSessionToken: class {},
                AutocompleteService: class {
                  async getPlacePredictions(request: any) {
                    return Promise.resolve({ predictions: [] });
                  }
                } as any,
              } as any
            } as any
          };
        }
      }
    );

    cy.get("API valid: yes"); // Fails consistently
  });
});

我正在删除 Loader class 的 load 方法,然后尝试向 window 添加模拟 Google 地图服务.

我的问题是加载器存根都不起作用——根据 Cypress 的说法,存根从未被调用过,当然,promise 也没有解决。 我尝试的另一种方法是存根 loadGoogleMapsAPI 并手动 return GoogleMapsAPI 的实例,但问题仍然相同 - 我从未得到 return 由 [=19] 编辑的预测=] 通过 getPlacePredictions.

由于我对使用 Cypress 进行测试和一般测试前端代码还很陌生,这可能只是对 Cypress 中存根的工作方式或代码在 JS 中的加载方式的误解,但我几乎被困在这里并且将不胜感激!

我已经通过摆脱加载器来支持 inline loading 和以下代码解决了这个问题:

let ready = false;

// This is the callback that is called by the Google Maps API loader script
//  loaded in index.html.
window.initGoogleMapsAPI = function() {
  ready = true;
}

async function waitForGoogleMapsAPI(): Promise<void> {
  return new Promise<void>((resolve) => {
    if (ready) {
      resolve();
    }
  })
}

...

async function loadGoogleMapsAPI(): Promise<GoogleMapsAPI | null> {
  // Wait for the loader to load the API, then return the API class.
  try {
    await waitForGoogleMapsAPI;

    return new GoogleMapsAPI();
  } catch (e) {
    console.error(`Google Maps API could not be loaded: ${e}`);
  }

  return null;
}

然后我导出waitForGoogleMapsAPI并在测试中模拟它。