chromium/third_party/blink/web_tests/http/tests/resources/geolocation-mock.js

/*
 * geolocation-mock contains a mock implementation of Geolocation and
 * PermissionService.
 */

import {GeolocationReceiver} from '/gen/services/device/public/mojom/geolocation.mojom.m.js';
import {GeopositionErrorCode} from '/gen/services/device/public/mojom/geoposition.mojom.m.js';
import {GeolocationService, GeolocationServiceReceiver} from '/gen/third_party/blink/public/mojom/geolocation/geolocation_service.mojom.m.js';
import {PermissionStatus} from '/gen/third_party/blink/public/mojom/permissions/permission_status.mojom.m.js';

export class GeolocationMock {
  constructor() {
    this.geolocationServiceInterceptor_ =
        new MojoInterfaceInterceptor(GeolocationService.$interfaceName);
    this.geolocationServiceInterceptor_.oninterfacerequest =
        e => this.connectGeolocationService_(e.handle);
    this.geolocationServiceInterceptor_.start();

    /**
     * The next result to return in response to a queryNextPosition()
     * call.
    */
    this.result_ = null;

    /**
     * While true, position requests will result in a timeout error.
     */
    this.shouldTimeout_ = false;

    /**
     * A pending request for permission awaiting a decision to be set via a
     * setGeolocationPermission call.
     *
     * @type {?Function}
     */
    this.pendingPermissionRequest_ = null;

    /**
     * The status to respond to permission requests with. If set to ASK, then
     * permission requests will block until setGeolocationPermission is called
     * to allow or deny permission requests.
     *
     * @type {!PermissionStatus}
     */
    this.permissionStatus_ = PermissionStatus.ASK;
    this.rejectGeolocationServiceConnections_ = false;

    this.systemPermissionStatus_ = PermissionStatus.GRANTED;

    /**
     * Set by interceptQueryNextPosition() and used to resolve the promise
     * returned by that call once the next incoming queryNextPosition() is
     * received.
     */
    this.queryNextPositionIntercept_ = null;

    this.geolocationReceiver_ = new GeolocationReceiver(this);
    this.geolocationServiceReceiver_ = new GeolocationServiceReceiver(this);
  }

  connectGeolocationService_(handle) {
    if (this.rejectGeolocationServiceConnections_) {
      handle.close();
      return;
    }
    this.geolocationServiceReceiver_.$.bindHandle(handle);
  }

  setHighAccuracy(highAccuracy) {
    // FIXME: We need to add some tests regarding "high accuracy" mode.
    // See https://bugs.webkit.org/show_bug.cgi?id=49438
  }

  /**
   * Waits for the next queryPosition() call, and returns a function which can
   * be used to respond to it. This allows tests to have fine-grained control
   * over exactly when and how the mock responds to a specific request.
   */
  async interceptQueryNextPosition() {
    if (this.queryNextPositionIntercept_) {
      throw new Error(
          'interceptQueryNextPosition called twice in a row, with no interim ' +
          'queryPosition');
    }
    return new Promise(resolve => {
      this.queryNextPositionIntercept_ = resolver => {
        this.queryNextPositionIntercept_ = null;
        resolve(result => { resolver({result}); });
      };
    });
  }

  /**
   * A mock implementation of GeolocationService.queryNextPosition(). This
   * returns the position set by a call to setGeolocationPosition() or
   * setGeolocationPositionUnavailableError().
   */
  queryNextPosition() {
    if (this.shouldTimeout_) {
      // Return a promise that will never be resolved. Since no result is
      // returned, the request will eventually time out.
      return new Promise((resolve, reject) => {});
    }
    if (this.queryNextPositionIntercept_) {
      return new Promise(resolve => {
        this.queryNextPositionIntercept_(resolve);
      });
    }

    if (this.systemPermissionStatus_ != PermissionStatus.GRANTED) {
      const error = {
        errorMessage: "User has not allowed access to system location.",
        errorCode: GeopositionErrorCode.kPermissionDenied,
        errorTechnical: "",
      };
      this.result_ = {error};
    }

    if (!this.result_) {
      this.setGeolocationPositionUnavailableError(
          'Test error: position not set before call to queryNextPosition()');
    }
    let result = this.result_;
    this.result_ = null;
    return Promise.resolve({result});
  }

  makeGeoposition(latitude, longitude, accuracy, altitude = undefined,
                  altitudeAccuracy = undefined, heading = undefined,
                  speed = undefined) {
    // The new Date().getTime() returns the number of milliseconds since the
    // UNIX epoch (1970-01-01 00::00:00 UTC), while |internalValue| of the
    // device.mojom.Geoposition represents the value of microseconds since the
    // Windows FILETIME epoch (1601-01-01 00:00:00 UTC). So add the delta when
    // sets the |internalValue|. See more info in //base/time/time.h.
    const windowsEpoch = Date.UTC(1601,0,1,0,0,0,0);
    const unixEpoch = Date.UTC(1970,0,1,0,0,0,0);
    // |epochDeltaInMs| equals to base::Time::kTimeTToMicrosecondsOffset.
    const epochDeltaInMs = unixEpoch - windowsEpoch;
    const timestamp =
        {internalValue: BigInt((new Date().getTime() + epochDeltaInMs) * 1000)};
    return {latitude, longitude, accuracy, altitude, altitudeAccuracy, heading,
            speed, timestamp};
  }

  /**
   * Sets the position to return to the next queryNextPosition() call. If any
   * queryNextPosition() requests are outstanding, they will all receive the
   * position set by this call.
   */
  setGeolocationPosition(latitude, longitude, accuracy, altitude,
                         altitudeAccuracy, heading, speed) {
    const position = this.makeGeoposition(latitude, longitude, accuracy,
        altitude, altitudeAccuracy, heading, speed);
    this.result_ = {position};
  }

  /**
   * Sets the error message to return to the next queryNextPosition() call. If
   * any queryNextPosition() requests are outstanding, they will all receive
   * the error set by this call.
   */
  setGeolocationPositionUnavailableError(message) {
    const error = {
      errorMessage: message,
      errorCode: GeopositionErrorCode.kPositionUnavailable,
      errorTechnical: "",
    };
    this.result_ = {error};
  }

  /**
   * Sets whether geolocation requests should cause timeout errors.
   */
  setGeolocationTimeoutError(shouldTimeout) {
    this.shouldTimeout_ = shouldTimeout;
  }

  /**
   * Reject any connection requests for the geolocation service. This will
   * trigger a connection error in the client.
   */
  rejectGeolocationServiceConnections() {
    this.rejectGeolocationServiceConnections_ = true;
  }

  /**
   * A mock implementation of GeolocationService.createGeolocation().
   * This accepts the request as long as the permission has been set to
   * granted.
   */
  createGeolocation(receiver, user_gesture) {
    switch (this.permissionStatus_) {
     case PermissionStatus.ASK:
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          resolve(this.createGeolocation(receiver, user_gesture));
        }, 50);
      });
      setTimeout(() => { this.createGeolocation(receiver, user_gesture)}, 50);
      break;

     case PermissionStatus.GRANTED:
      this.geolocationReceiver_.$.bindHandle(receiver.handle);
      break;

     default:
      receiver.handle.close();
    }
    return Promise.resolve(this.permissionStatus_);
  }

  /**
   * Sets whether the next geolocation permission request should be allowed.
   */
  setGeolocationPermission(allowed) {
    this.permissionStatus_ = allowed ? PermissionStatus.GRANTED
                                     : PermissionStatus.DENIED;
  }

  setSystemGeolocationPermission(allowed) {
    this.systemPermissionStatus_ = allowed ? PermissionStatus.GRANTED
                                           : PermissionStatus.DENIED;
  }
}