Commit 913de3ae authored by Erik Nordin's avatar Erik Nordin Committed by enordin@mozilla.com
Browse files

Bug 1820252 - Add TranslationsDocument test helpers r=translations-reviewers,gregtatum

This patch adds a few public functsion to the TranslationsDocument
as well as the TranslationsChild actor that are primarily intended
for use during testing. These functions help all of our tests not
only ensure that the content is translated correctly, but also that
the TranslationsDocument is upholding all of its invariants.

Differential Revision: https://phabricator.services.mozilla.com/D249985
parent 1d04b52a
Loading
Loading
Loading
Loading
+67 −0
Original line number Diff line number Diff line
@@ -62,6 +62,73 @@ export class TranslationsChild extends JSWindowActorChild {
    this.#translatedDoc = null;
  }

  /**
   * Returns true if the TranslationsDocument has any callbacks pending to run on
   * the event loop, otherwise false.
   *
   * @returns {boolean}
   */
  hasPendingCallbackOnEventLoop() {
    if (!this.#translatedDoc) {
      // Full-Page Translations has not been requested yet, so there is no callback.
      return false;
    }

    return this.#translatedDoc.hasPendingCallbackOnEventLoop();
  }

  /**
   * Returns true if the TranslationsDocument has any pending translation requests, otherwise false.
   *
   * Having no pending request does NOT mean that the entire page is translated, nor does it mean
   * that more requests won't come in via mutations or intersection observations. It simply means
   * that there are no pending requests at this exact moment.
   *
   * @returns {boolean}
   */
  hasPendingTranslationRequests() {
    if (!this.#translatedDoc) {
      // Full-Page Translations has not been requested yet, so there are no requests.
      return false;
    }

    return this.#translatedDoc.hasPendingTranslationRequests();
  }

  /**
   * Returns true if the TranslationsDocument is observing any element for content translation, otherwise false.
   *
   * Having no observed elements means that at the current point in time, until any further mutations occur,
   * every content translation request has been fulfilled.
   *
   * @returns {boolean}
   */
  isObservingAnyElementForContentIntersection() {
    if (!this.#translatedDoc) {
      // Full-Page Translations has not been requested yet, so we are not observing.
      return false;
    }

    return this.#translatedDoc.isObservingAnyElementForContentIntersection();
  }

  /**
   * Returns true if the TranslationsDocument is observing any element for attribute translation, otherwise false.
   *
   * Having no observed elements means that at the current point in time, until any further mutations occur,
   * every attribute translation request has been fulfilled.
   *
   * @returns {boolean}
   */
  isObservingAnyElementForAttributeIntersection() {
    if (!this.#translatedDoc) {
      // Full-Page Translations has not been requested yet, so we are not observing.
      return false;
    }

    return this.#translatedDoc.isObservingAnyElementForAttributeIntersection();
  }

  addProfilerMarker(message, startTime) {
    ChromeUtils.addProfilerMarker(
      "TranslationsChild",
+127 −0
Original line number Diff line number Diff line
@@ -1755,6 +1755,84 @@ export class TranslationsDocument {
    this.#maybePrioritizeRequestsAndSubmitToScheduler();
  }

  /**
   * This is a test-only function that simulates intersection observation
   * by running through all of the observed nodes and enqueuing them for
   * prioritization if they are not already associated with a pending
   * translation request.
   *
   * This function may only be used in testing contexts where the viewport
   * is effectively non-existent, such that the intersection observers will
   * not observe nodes as intended.
   *
   * @throws If this function is called outside of automated testing.
   * @throws If the viewport is not zero-width or zero-height.
   */
  simulateIntersectionObservationForNonPendingNodes() {
    lazy.console.debug("Simulating intersection observations for test.");

    if (!Cu.isInAutomation) {
      // There is no scenario in which we should call this function outside of an
      // automated test that requires it.
      throw new Error(
        "Attempt to manually simulate intersection observation outside of test."
      );
    }

    const window = ensureExists(this.#sourceDocument.ownerGlobal);
    const { visualViewport } = window;
    if (visualViewport.width > 0 && visualViewport.height > 0) {
      // The only time we should call this function is in test cases where the
      // intersection observers will not function because a viewport dimension is zero.
      // If a viewport dimension is not actually zero, then this was called in error.
      throw new Error(
        "Attempt to manually simulate intersection observation with a valid viewport."
      );
    }

    // This should never be called as the first intersection observation.
    // See #waitForFirstIntersectionObservation for an explanation why.
    //
    // The code is written so that the first intersection observation is
    // guaranteed to be fulfilled when adding the initial root elements.
    //
    // If you are modifying this code, and this promise hangs, then the
    // code has been modified incorrectly such that the first observation
    // guarantee is no longer upheld.
    /** @type {PromiseWithResolvers<void>} */
    const firstIntersectionObservationsTimeout = Promise.withResolvers();
    lazy.setTimeout(
      () =>
        firstIntersectionObservationsTimeout.reject(
          new Error(
            "The TranslationDocument's first intersection observations failed to resolve."
          )
        ),
      2000
    );

    Promise.race([
      firstIntersectionObservationsTimeout.promise,
      this.#waitForFirstIntersectionObservations(),
    ]).then(() => {
      firstIntersectionObservationsTimeout.resolve();

      for (const element of this.#intersectionObservedContentElements.keys()) {
        if (!this.#pendingContentTranslations.has(element)) {
          this.#enqueueForIntersectionPrunableContentPrioritization(element);
        }
      }

      for (const element of this.#intersectionObservedAttributeElements.keys()) {
        if (!this.#pendingAttributeTranslations.has(element)) {
          this.#enqueueForIntersectionPrunableAttributePrioritization(element);
        }
      }

      this.#maybePrioritizeRequestsAndSubmitToScheduler();
    });
  }

  /**
   * The first intersection observation is critical to the flow of the TranslationsDocument.
   *
@@ -3401,6 +3479,55 @@ export class TranslationsDocument {
    return this.#scheduler.engineStatus;
  }

  /**
   * Returns true if the TranslationsDocument has any pending translation requests
   * that are actively being handled by the TranslationScheduler, otherwise false.
   *
   * @returns {boolean}
   */
  hasPendingTranslationRequests() {
    return (
      this.#pendingContentTranslations.size > 0 ||
      this.#pendingAttributeTranslations.size > 0
    );
  }

  /**
   * Returns true if the TranslationsDocument has any pending callback on the event loop
   * that has not yet completed, otherwise false.
   *
   * @returns {boolean}
   */
  hasPendingCallbackOnEventLoop() {
    return (
      this.#hasPendingMutatedNodesCallback ||
      this.#hasPendingPrioritizationCallback ||
      this.#hasPendingUpdateAttributesCallback ||
      this.#hasPendingUpdateContentCallback ||
      this.#scheduler.hasPendingScheduleRequestsCallback()
    );
  }

  /**
   * Returns true if the TranslationsDocument is observing at least one
   * element for intersection to translate its content, otherwise false.
   *
   * @returns {boolean}
   */
  isObservingAnyElementForContentIntersection() {
    return this.#intersectionObservedContentElements.size > 0;
  }

  /**
   * Returns true if the TranslationsDocument is observing at least one
   * element for intersection to translate its attributes, otherwise false.
   *
   * @returns {boolean}
   */
  isObservingAnyElementForAttributeIntersection() {
    return this.#intersectionObservedAttributeElements.size > 0;
  }

  /**
   * An event handler for when the user scrolls around the page.
   * Uses the scrollY position to determine if the user is scrolling up or down.