Quellcodebibliothek Statistik Leitseite products/Sources/formale Sprachen/C/Firefox/browser/components/translations/tests/browser/   (Browser von der Mozilla Stiftung Version 136.0.1©)  Datei vom 10.2.2025 mit Größe 112 kB image not shown  

Quelle  head.js   Sprache: JAVA

 
/* Any copyright is dedicated to the Public Domain.
   http://creativecommons.org/publicdomain/zero/1.0/ */


"use strict";

Services.scriptloader.loadSubScript(
  "chrome://mochitests/content/browser/toolkit/components/translations/tests/browser/shared-head.js",
  this
);

/**
 * Converts milliseconds to seconds.
 *
 * @param {number} ms - The duration in milliseconds.
 * @returns {number} The duration in seconds.
 */

function millisecondsToSeconds(ms) {
  return ms / 1000;
}

/**
 * Converts bytes to mebibytes.
 *
 * @param {number} bytes - The size in bytes.
 * @returns {number} The size in mebibytes.
 */

function bytesToMebibytes(bytes) {
  return bytes / (1024 * 1024);
}

/**
 * Calculates the median of a list of numbers.
 *
 * @param {number[]} numbers - An array of numbers to find the median of.
 * @returns {number} The median of the provided numbers.
 */

function median(numbers) {
  numbers = numbers.sort((lhs, rhs) => lhs - rhs);
  const midIndex = Math.floor(numbers.length / 2);

  if (numbers.length & 1) {
    return numbers[midIndex];
  }

  return (numbers[midIndex - 1] + numbers[midIndex]) / 2;
}

/**
 * Opens a new tab in the foreground.
 *
 * @param {string} url
 */

async function addTab(url, message, win = window) {
  logAction(url);
  info(message);
  const tab = await BrowserTestUtils.openNewForegroundTab(
    win.gBrowser,
    url,
    true // Wait for load
  );
  return {
    tab,
    removeTab() {
      BrowserTestUtils.removeTab(tab);
    },
    /**
     * Runs a callback in the content page. The function's contents are serialized as
     * a string, and run in the page. The `translations-test.mjs` module is made
     * available to the page.
     *
     * @param {(TranslationsTest: import("./translations-test.mjs")) => any} callback
     * @returns {Promise<void>}
     */

    runInPage(callback, data = {}) {
      // ContentTask.spawn runs the `Function.prototype.toString` on this function in
      // order to send it into the content process. The following function is doing its
      // own string manipulation in order to load in the TranslationsTest module.
      const fn = new Function(/* js */ `
        const TranslationsTest = ChromeUtils.importESModule(
          "chrome://mochitests/content/browser/toolkit/components/translations/tests/browser/translations-test.mjs"
        );

        // Pass in the values that get injected by the task runner.
        TranslationsTest.setup({Assert, ContentTaskUtils, content});

        const data = ${JSON.stringify(data)};

        return (${callback.toString()})(TranslationsTest, data);
      `);

      return ContentTask.spawn(
        tab.linkedBrowser,
        {}, // Data to inject.
        fn
      );
    },
  };
}

/**
 * Simulates clicking an element with the mouse.
 *
 * @param {element} element - The element to click.
 * @param {string} [message] - A message to log to info.
 */

function click(element, message) {
  logAction(message);
  return new Promise(resolve => {
    element.addEventListener(
      "click",
      function () {
        resolve();
      },
      { once: true }
    );

    EventUtils.synthesizeMouseAtCenter(element, {
      type: "mousedown",
      isSynthesized: false,
    });
    EventUtils.synthesizeMouseAtCenter(element, {
      type: "mouseup",
      isSynthesized: false,
    });
  });
}

function focusElementAndSynthesizeKey(element, key) {
  assertVisibility({ visible: { element } });
  element.focus();
  EventUtils.synthesizeKey(key);
}

/**
 * Focuses the given window object, moving it to the top of all open windows.
 *
 * @param {Window} win
 */

async function focusWindow(win) {
  const windowFocusPromise = BrowserTestUtils.waitForEvent(win, "focus");
  win.focus();
  await windowFocusPromise;
}

/**
 * Get all elements that match the l10n id.
 *
 * @param {string} l10nId
 * @param {Document} doc
 * @returns {Element}
 */

function getAllByL10nId(l10nId, doc = document) {
  const elements = doc.querySelectorAll(`[data-l10n-id="${l10nId}"]`);
  if (elements.length === 0) {
    throw new Error("Could not find the element by l10n id: " + l10nId);
  }
  return elements;
}

/**
 * Retrieves an element by its Id.
 *
 * @param {string} id
 * @param {Document} [doc]
 * @returns {Element}
 * @throws Throws if the element is not visible in the DOM.
 */

function getById(id, doc = document) {
  const element = maybeGetById(id, /* ensureIsVisible */ true, doc);
  if (!element) {
    throw new Error("The element is not visible in the DOM: #" + id);
  }
  return element;
}

/**
 * Get an element by its l10n id, as this is a user-visible way to find an element.
 * The `l10nId` represents the text that a user would actually see.
 *
 * @param {string} l10nId
 * @param {Document} doc
 * @returns {Element}
 */

function getByL10nId(l10nId, doc = document) {
  const elements = doc.querySelectorAll(`[data-l10n-id="${l10nId}"]`);
  if (elements.length === 0) {
    throw new Error("Could not find the element by l10n id: " + l10nId);
  }
  for (const element of elements) {
    if (BrowserTestUtils.isVisible(element)) {
      return element;
    }
  }
  throw new Error("The element is not visible in the DOM: " + l10nId);
}

/**
 * Returns the intl display name of a given language tag.
 *
 * @param {string} langTag - A BCP-47 language tag.
 */

const getIntlDisplayName = (() => {
  let languageDisplayNames = null;

  return langTag => {
    if (!languageDisplayNames) {
      languageDisplayNames = TranslationsParent.createLanguageDisplayNames({
        fallback: "none",
      });
    }
    return languageDisplayNames.of(langTag);
  };
})();

/**
 * Attempts to retrieve an element by its Id.
 *
 * @param {string} id - The Id of the element to retrieve.
 * @param {boolean} [ensureIsVisible=true] - If set to true, the function will return null when the element is not visible.
 * @param {Document} [doc=document] - The document from which to retrieve the element.
 * @returns {Element | null} - The retrieved element.
 * @throws Throws if no element was found by the given Id.
 */

function maybeGetById(id, ensureIsVisible = true, doc = document) {
  const element = doc.getElementById(id);
  if (!element) {
    throw new Error("Could not find the element by id: #" + id);
  }

  if (!ensureIsVisible) {
    return element;
  }

  if (BrowserTestUtils.isVisible(element)) {
    return element;
  }

  return null;
}

/**
 * A non-throwing version of `getByL10nId`.
 *
 * @param {string} l10nId
 * @returns {Element | null}
 */

function maybeGetByL10nId(l10nId, doc = document) {
  const selector = `[data-l10n-id="${l10nId}"]`;
  const elements = doc.querySelectorAll(selector);
  for (const element of elements) {
    if (BrowserTestUtils.isVisible(element)) {
      return element;
    }
  }
  return null;
}

/**
 * Provide a uniform way to log actions. This abuses the Error stack to get the callers
 * of the action. This should help in test debugging.
 */

function logAction(...params) {
  const error = new Error();
  const stackLines = error.stack.split("\n");
  const actionName = stackLines[1]?.split("@")[0] ?? "";
  const taskFileLocation = stackLines[2]?.split("@")[1] ?? "";
  if (taskFileLocation.includes("head.js")) {
    // Only log actions that were done at the test level.
    return;
  }

  info(`Action: ${actionName}(${params.join(", ")})`);
  info(
    `Source: ${taskFileLocation.replace(
      "chrome://mochitests/content/browser/",
      ""
    )}`
  );
}

/**
 * Returns true if Full-Page Translations is currently active, otherwise false.
 *
 * @returns {boolean}
 */

function isFullPageTranslationsActive() {
  try {
    const { requestedLanguagePair } = TranslationsParent.getTranslationsActor(
      gBrowser.selectedBrowser
    ).languageState;
    return !!requestedLanguagePair;
  } catch {
    // Translations actor unavailable, continue on.
  }
  return false;
}

/**
 * Navigate to a URL and indicate a message as to why.
 */

async function navigate(
  message,
  { url, onOpenPanel = null, downloadHandler = null, pivotTranslation = false }
) {
  logAction();
  // When the translations panel is open from the app menu,
  // it doesn't close on navigate the way that it does when it's
  // open from the translations button, so ensure that we always
  // close it when we navigate to a new page.
  await closeAllOpenPanelsAndMenus();

  info(message);

  // Load a blank page first to ensure that tests don't hang.
  // I don't know why this is needed, but it appears to be necessary.
  BrowserTestUtils.startLoadingURIString(gBrowser.selectedBrowser, BLANK_PAGE);
  await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);

  const loadTargetPage = async () => {
    BrowserTestUtils.startLoadingURIString(gBrowser.selectedBrowser, url);
    await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);

    if (downloadHandler) {
      await FullPageTranslationsTestUtils.assertTranslationsButton(
        { button: true, circleArrows: true, locale: false, icon: true },
        "The icon presents the loading indicator."
      );
      await downloadHandler(pivotTranslation ? 2 : 1);
    }
  };

  info(`Loading url: "${url}"`);
  if (onOpenPanel) {
    await FullPageTranslationsTestUtils.waitForPanelPopupEvent(
      "popupshown",
      loadTargetPage,
      onOpenPanel
    );
  } else {
    await loadTargetPage();
  }
}

/**
 * Switches to a given tab.
 *
 * @param {object} tab - The tab to switch to
 * @param {string} name
 */

async function switchTab(tab, name) {
  logAction("tab", name);
  gBrowser.selectedTab = tab;
  await new Promise(resolve => setTimeout(resolve, 0));
}

/**
 * Click the reader-mode button if the reader-mode button is available.
 * Fails if the reader-mode button is hidden.
 */

async function toggleReaderMode() {
  logAction();
  const readerButton = document.getElementById("reader-mode-button");
  await waitForCondition(() => readerButton.hidden === false);

  readerButton.getAttribute("readeractive")
    ? info("Exiting reader mode")
    : info("Entering reader mode");

  const readyPromise = readerButton.getAttribute("readeractive")
    ? waitForCondition(() => !readerButton.getAttribute("readeractive"))
    : BrowserTestUtils.waitForContentEvent(
        gBrowser.selectedBrowser,
        "AboutReaderContentReady"
      );

  click(readerButton, "Clicking the reader-mode button");
  await readyPromise;
}

/**
 * A class for benchmarking translation performance and reporting
 * metrics to our perftest infrastructure.
 */

class TranslationsBencher {
  /**
   * The metric base name for the engine initialization time.
   *
   * @type {string}
   */

  static METRIC_ENGINE_INIT_TIME = "engine-init-time";

  /**
   * The metric base name for words translated per second.
   *
   * @type {string}
   */

  static METRIC_WORDS_PER_SECOND = "words-per-second";

  /**
   * The metric base name for tokens translated per second.
   *
   * @type {string}
   */

  static METRIC_TOKENS_PER_SECOND = "tokens-per-second";

  /**
   * The metric base name for total memory usage in the inference process.
   *
   * @type {string}
   */

  static METRIC_TOTAL_MEMORY_USAGE = "total-memory-usage";

  /**
   * The metric base name for total translation time.
   *
   * @type {string}
   */

  static METRIC_TOTAL_TRANSLATION_TIME = "total-translation-time";

  /**
   * Data required to ensure that peftest metrics are validated and calculated correctly for the
   * given test file. This data can be generated for a test file by running the script located at:
   *
   * toolkit/components/translations/tests/scripts/translations-perf-data.py
   *
   * @type {Record<string, {pageLanguage: string, tokenCount: number, wordCount: number}>}
   */

  static #PAGE_DATA = {
    [SPANISH_BENCHMARK_PAGE_URL]: {
      pageLanguage: "es",
      tokenCount: 10966,
      wordCount: 6944,
    },
  };

  /**
   * A class that gathers and reports metrics to perftest.
   */

  static Journal = class {
    #metrics = {};

    /**
     * Pushes a metric value into the journal.
     *
     * @param {string} metricName - The metric name.
     * @param {number} value - The metric value to record.
     */

    pushMetric(metricName, value) {
      if (!this.#metrics[metricName]) {
        this.#metrics[metricName] = [];
      }
      this.#metrics[metricName].push(value);
    }

    /**
     * Pushes multiple metric values into the journal.
     *
     * @param {Array<[string, number]>} metrics - An array of [metricName, value] pairs.
     */

    pushMetrics(metrics) {
      for (const [metricName, value] of metrics) {
        this.pushMetric(metricName, value);
      }
    }

    /**
     * Logs the median value of each collected metric to the console.
     * The log is then picked up by the perftest infrastructure.
     * The logged data must match the schema defined in the test file.
     */

    reportMetrics() {
      const reportedMetrics = {};
      for (const [name, values] of Object.entries(this.#metrics)) {
        reportedMetrics[name] = median(values);
      }
      info(`perfMetrics | ${JSON.stringify(reportedMetrics)}`);
    }
  };

  /**
   * Benchmarks the translation process and reports metrics to perftest.
   *
   * @param {object} options - The benchmark options.
   * @param {string} options.page - The URL of the page to test.
   * @param {number} options.runCount - The number of runs to perform.
   * @param {string} options.sourceLanguage - The BCP-47 language tag for the source language.
   * @param {string} options.targetLanguage - The BCP-47 language tag for the target language.
   *
   * @returns {Promise<void>} Resolves when benchmarking is complete.
   */

  static async benchmarkTranslation({
    page,
    runCount,
    sourceLanguage,
    targetLanguage,
  }) {
    const { wordCount, tokenCount, pageLanguage } =
      TranslationsBencher.#PAGE_DATA[page] ?? {};

    if (!wordCount || !tokenCount || !pageLanguage) {
      const testPageName = page.match(/[^\\/]+$/)[0];
      const testPagePath = page.substring(
        "https://example.com/browser/".length
      );
      const sourceLangName = getIntlDisplayName(sourceLanguage);
      throw new Error(`

        �� Perf test data is not properly defined for ${testPageName} ��

        To enable ${testPageName} for Translations perf tests, please follow these steps:

          1) Ensure ${testPageName} has a proper HTML lang attribute in the markup:

               <html lang="${sourceLanguage}">

          2) Download the ${sourceLanguage}-${PIVOT_LANGUAGE}.vocab.spm model from a ${sourceLangName} row on the following site:

               https://gregtatum.github.io/taskcluster-tools/src/models/

          3) Run the following command to extract the perf metadata from ${testPageName}:

               ❯ python3 toolkit/components/translations/tests/scripts/translations-perf-data.py \\
                 --page_path="${testPagePath}" \\
                 --model_path="..."

          4) Include the resulting metadata for ${testPageName} in the TranslationsBencher.#PAGE_DATA object.
      `);
    }

    if (sourceLanguage !== pageLanguage) {
      throw new Error(
        `Perf test source language '${sourceLanguage}' did not match the expected page language '${pageLanguage}'.`
      );
    }

    const journal = new TranslationsBencher.Journal();

    for (let runNumber = 0; runNumber < runCount; ++runNumber) {
      const { tab, cleanup, runInPage } = await loadTestPage({
        page,
        endToEndTest: true,
        languagePairs: [
          { fromLang: sourceLanguage, toLang: "en" },
          { fromLang: "en", toLang: targetLanguage },
        ],
        prefs: [["browser.translations.logLevel""Error"]],
      });

      await TranslationsBencher.#injectTranslationCompleteObserver(runInPage);

      await FullPageTranslationsTestUtils.assertTranslationsButton(
        { button: true, circleArrows: false, locale: false, icon: true },
        "The button is available."
      );

      await FullPageTranslationsTestUtils.openPanel({
        onOpenPanel: FullPageTranslationsTestUtils.assertPanelViewDefault,
      });

      await FullPageTranslationsTestUtils.changeSelectedFromLanguage({
        langTag: sourceLanguage,
      });
      await FullPageTranslationsTestUtils.changeSelectedToLanguage({
        langTag: targetLanguage,
      });

      const engineReadyTimestampPromise =
        TranslationsBencher.#getEngineReadyTimestampPromise(tab.linkedBrowser);
      const translationCompleteTimestampPromise =
        TranslationsBencher.#getTranslationCompleteTimestampPromise(runInPage);

      const translateButtonClickedTime = performance.now();
      await FullPageTranslationsTestUtils.clickTranslateButton();

      const [engineReadyTime, translationCompleteTime] = await Promise.all([
        engineReadyTimestampPromise,
        translationCompleteTimestampPromise,
      ]);

      const initTimeMilliseconds = engineReadyTime - translateButtonClickedTime;
      const translationTimeSeconds = millisecondsToSeconds(
        translationCompleteTime - engineReadyTime
      );
      const wordsPerSecond = wordCount / translationTimeSeconds;
      const tokensPerSecond = tokenCount / translationTimeSeconds;

      const totalMemoryMB =
        await TranslationsBencher.#getInferenceProcessTotalMemoryUsage();

      const decimalPrecision = 3;
      journal.pushMetrics([
        [
          TranslationsBencher.METRIC_ENGINE_INIT_TIME,
          Number(initTimeMilliseconds.toFixed(decimalPrecision)),
        ],
        [
          TranslationsBencher.METRIC_WORDS_PER_SECOND,
          Number(wordsPerSecond.toFixed(decimalPrecision)),
        ],
        [
          TranslationsBencher.METRIC_TOKENS_PER_SECOND,
          Number(tokensPerSecond.toFixed(decimalPrecision)),
        ],
        [
          TranslationsBencher.METRIC_TOTAL_MEMORY_USAGE,
          Number(totalMemoryMB.toFixed(decimalPrecision)),
        ],
        [
          TranslationsBencher.METRIC_TOTAL_TRANSLATION_TIME,
          Number(translationTimeSeconds.toFixed(decimalPrecision)),
        ],
      ]);

      await cleanup();
    }

    journal.reportMetrics();
  }

  /**
   * Injects a mutation observer into the test page to detect when translation is complete.
   *
   * @param {Function} runInPage - Runs a closure within the content context of the page.
   * @returns {Promise<void>} Resolves when the observer is injected.
   */

  static async #injectTranslationCompleteObserver(runInPage) {
    await runInPage(TranslationsTest => {
      const { getLastParagraph } = TranslationsTest.getSelectors();
      const lastParagraph = getLastParagraph();

      if (!lastParagraph) {
        throw new Error("Unable to find the last paragraph for observation.");
      }

      const observer = new content.MutationObserver(
        (_mutationsList, _observer) => {
          content.document.dispatchEvent(
            new CustomEvent("TranslationComplete")
          );
        }
      );

      observer.observe(lastParagraph, {
        childList: true,
      });
    });
  }

  /**
   * Returns a Promise that resolves with the timestamp when the Translations engine becomes ready.
   *
   * @param {Browser} browser - The browser hosting the translation.
   * @returns {Promise<number>} The timestamp when the engine is ready.
   */

  static async #getEngineReadyTimestampPromise(browser) {
    const { promise, resolve } = Promise.withResolvers();

    function maybeGetTranslationStartTime(event) {
      if (
        event.detail.reason === "isEngineReady" &&
        event.detail.actor.languageState.isEngineReady
      ) {
        browser.removeEventListener(
          "TranslationsParent:LanguageState",
          maybeGetTranslationStartTime
        );
        resolve(performance.now());
      }
    }

    browser.addEventListener(
      "TranslationsParent:LanguageState",
      maybeGetTranslationStartTime
    );

    return promise;
  }

  /**
   * Returns a Promise that resolves with the timestamp after the translation is complete.
   *
   * @param {Function} runInPage - A helper to run code on the test page.
   * @returns {Promise<number>} The timestamp when the translation is complete.
   */

  static async #getTranslationCompleteTimestampPromise(runInPage) {
    await runInPage(async () => {
      const { promise, resolve } = Promise.withResolvers();

      content.document.addEventListener("TranslationComplete", resolve, {
        once: true,
      });

      await promise;
    });

    return performance.now();
  }

  /**
   * Returns the total memory used by the inference process in megabytes.
   *
   * @returns {Promise<number>} The total memory usage in megabytes.
   */

  static async #getInferenceProcessTotalMemoryUsage() {
    let memoryReporterManager = Cc[
      "@mozilla.org/memory-reporter-manager;1"
    ].getService(Ci.nsIMemoryReporterManager);

    let totalBytes = 0;

    const handleReport = (
      aProcess,
      aPath,
      _aKind,
      _aUnits,
      aAmount,
      _aDescription
    ) => {
      if (aProcess.startsWith("inference")) {
        if (aPath.startsWith("explicit")) {
          totalBytes += aAmount;
        }
      }
    };

    await new Promise(resolve =>
      memoryReporterManager.getReports(
        handleReport,
        null// handleReportData
        resolve, // finishReporting
        null// finishReportingData
        false // anonymized
      )
    );

    return bytesToMebibytes(totalBytes);
  }
}

/**
 * A collection of shared functionality utilized by
 * FullPageTranslationsTestUtils and SelectTranslationsTestUtils.
 *
 * Using functions from the aforementioned classes is preferred over
 * using functions from this class directly.
 */

class SharedTranslationsTestUtils {
  /**
   * Asserts that the specified element currently has focus.
   *
   * @param {Element} element - The element to check for focus.
   */

  static _assertHasFocus(element) {
    is(
      document.activeElement,
      element,
      `The element '${element.id}' should have focus.`
    );
  }

  /**
   * Asserts that the given element has the expected L10nId.
   *
   * @param {Element} element - The element to assert against.
   * @param {string} l10nId - The expected localization id.
   */

  static _assertL10nId(element, l10nId) {
    is(
      element.getAttribute("data-l10n-id"),
      l10nId,
      `The element ${element.id} should have L10n Id ${l10nId}.`
    );
  }

  /**
   * Asserts that the mainViewId of the panel matches the given string.
   *
   * @param {FullPageTranslationsPanel | SelectTranslationsPanel} panel
   * @param {string} expectedId - The expected id that mainViewId is set to.
   */

  static _assertPanelMainViewId(panel, expectedId) {
    const mainViewId = panel.elements.multiview.getAttribute("mainViewId");
    is(
      mainViewId,
      expectedId,
      "The mainViewId should match its expected value"
    );
  }

  /**
   * Asserts that the selected language in the menu matches the langTag or l10nId.
   *
   * @param {Element} menuList - The menu list element to check.
   * @param {object} options - Options containing 'langTag' and 'l10nId' to assert against.
   * @param {string} [options.langTag] - The BCP-47 language tag to match.
   * @param {string} [options.l10nId] - The localization Id to match.
   */

  static _assertSelectedLanguage(menuList, { langTag, l10nId }) {
    ok(
      menuList.label,
      `The label for the menulist ${menuList.id} should not be empty.`
    );
    if (langTag !== undefined) {
      is(
        menuList.value,
        langTag,
        `Expected ${menuList.id} selection to match '${langTag}'`
      );
    }
    if (l10nId !== undefined) {
      is(
        menuList.getAttribute("data-l10n-id"),
        l10nId,
        `Expected ${menuList.id} l10nId to match '${l10nId}'`
      );
    }
  }

  /**
   * Asserts the visibility of the given elements based on the given expectations.
   *
   * @param {object} elements - An object containing the elements to be checked for visibility.
   * @param {object} expectations - An object where each property corresponds to a property in elements,
   *                                and its value is a boolean indicating whether the element should
   *                                be visible (true) or hidden (false).
   * @throws Throws if elements does not contain a property for each property in expectations.
   */

  static _assertPanelElementVisibility(elements, expectations) {
    const hidden = {};
    const visible = {};

    for (const propertyName in expectations) {
      ok(
        elements.hasOwnProperty(propertyName),
        `Expected panel elements to have property ${propertyName}`
      );
      if (expectations[propertyName]) {
        visible[propertyName] = elements[propertyName];
      } else {
        hidden[propertyName] = elements[propertyName];
      }
    }

    assertVisibility({ hidden, visible });
  }

  /**
   * Asserts that the given elements are focusable in order
   * via the tab key, starting with the first element already
   * focused and ending back on that same first element.
   *
   * @param {Element[]} elements - The focusable elements.
   */

  static _assertTabIndexOrder(elements) {
    const activeElementAtStart = document.activeElement;

    if (elements.length) {
      elements[0].focus();
      elements.push(elements[0]);
    }
    for (const element of elements) {
      SharedTranslationsTestUtils._assertHasFocus(element);
      EventUtils.synthesizeKey("KEY_Tab");
    }

    activeElementAtStart.focus();
  }

  /**
   * Executes the provided callback before waiting for the event and then waits for the given event
   * to be fired for the element corresponding to the provided elementId.
   *
   * Optionally executes a postEventAssertion function once the event occurs.
   *
   * @param {string} elementId - The Id of the element to wait for the event on.
   * @param {string} eventName - The name of the event to wait for.
   * @param {Function} callback - A callback function to execute immediately before waiting for the event.
   *                              This is often used to trigger the event on the expected element.
   * @param {Function|null} [postEventAssertion=null] - An optional callback function to execute after
   *                                                    the event has occurred.
   * @param {ChromeWindow} [win]
   * @throws Throws if the element with the specified `elementId` does not exist.
   * @returns {Promise<void>}
   */

  static async _waitForPopupEvent(
    elementId,
    eventName,
    callback,
    postEventAssertion = null,
    win = window
  ) {
    const element = win.document.getElementById(elementId);
    if (!element) {
      throw new Error(
        `Unable to find the ${elementId} element in the document.`
      );
    }
    const promise = BrowserTestUtils.waitForEvent(element, eventName);
    await callback();
    info(`Waiting for the ${elementId} ${eventName} event`);
    await promise;
    if (postEventAssertion) {
      await postEventAssertion();
    }
    // Wait a single tick on the event loop.
    await new Promise(resolve => setTimeout(resolve, 0));
  }
}

/**
 * A class containing test utility functions specific to testing full-page translations.
 */

class FullPageTranslationsTestUtils {
  /**
   * A collection of element visibility expectations for the default panel view.
   */

  static #defaultViewVisibilityExpectations = {
    cancelButton: true,
    fromMenuList: true,
    fromLabel: true,
    header: true,
    langSelection: true,
    toMenuList: true,
    toLabel: true,
    translateButton: true,
  };

  /**
   * Asserts that the state of a checkbox with a given dataL10nId is
   * checked or not, based on the value of expected being true or false.
   *
   * @param {string} dataL10nId - The data-l10n-id of the checkbox.
   * @param {object} expectations
   * @param {string} expectations.langTag - A BCP-47 language tag.
   * @param {boolean} expectations.checked - Whether the checkbox is expected to be checked.
   * @param {boolean} expectations.disabled - Whether the menuitem is expected to be disabled.
   */

  static async #assertCheckboxState(
    dataL10nId,
    { langTag = null, checked = true, disabled = false }
  ) {
    const menuItems = getAllByL10nId(dataL10nId);
    for (const menuItem of menuItems) {
      if (langTag) {
        const {
          args: { language },
        } = document.l10n.getAttributes(menuItem);
        is(
          language,
          getIntlDisplayName(langTag),
          `Should match expected language display name for ${dataL10nId}`
        );
      }
      is(
        menuItem.disabled,
        disabled,
        `Should match expected disabled state for ${dataL10nId}`
      );
      await waitForCondition(
        () => menuItem.getAttribute("checked") === (checked ? "true" : "false"),
        "Waiting for checkbox state"
      );
      is(
        menuItem.getAttribute("checked"),
        checked ? "true" : "false",
        `Should match expected checkbox state for ${dataL10nId}`
      );
    }
  }

  /**
   * Asserts that the always-offer-translations checkbox matches the expected checked state.
   *
   * @param {boolean} checked
   */

  static async assertIsAlwaysOfferTranslationsEnabled(checked) {
    info(
      `Checking that always-offer-translations is ${
        checked ? "enabled" : "disabled"
      }`
    );
    await FullPageTranslationsTestUtils.#assertCheckboxState(
      "translations-panel-settings-always-offer-translation",
      { checked }
    );
  }

  /**
   * Asserts that the always-translate-language checkbox matches the expected checked state.
   *
   * @param {string} langTag - A BCP-47 language tag
   * @param {object} expectations
   * @param {boolean} expectations.checked - Whether the checkbox is expected to be checked.
   * @param {boolean} expectations.disabled - Whether the menuitem is expected to be disabled.
   */

  static async assertIsAlwaysTranslateLanguage(
    langTag,
    { checked = true, disabled = false }
  ) {
    info(
      `Checking that always-translate is ${
        checked ? "enabled" : "disabled"
      } for "${langTag}"`
    );
    await FullPageTranslationsTestUtils.#assertCheckboxState(
      "translations-panel-settings-always-translate-language",
      { langTag, checked, disabled }
    );
  }

  /**
   * Asserts that the never-translate-language checkbox matches the expected checked state.
   *
   * @param {string} langTag - A BCP-47 language tag
   * @param {object} expectations
   * @param {boolean} expectations.checked - Whether the checkbox is expected to be checked.
   * @param {boolean} expectations.disabled - Whether the menuitem is expected to be disabled.
   */

  static async assertIsNeverTranslateLanguage(
    langTag,
    { checked = true, disabled = false }
  ) {
    info(
      `Checking that never-translate is ${
        checked ? "enabled" : "disabled"
      } for "${langTag}"`
    );
    await FullPageTranslationsTestUtils.#assertCheckboxState(
      "translations-panel-settings-never-translate-language",
      { langTag, checked, disabled }
    );
  }

  /**
   * Asserts that the never-translate-site checkbox matches the expected checked state.
   *
   * @param {string} url - The url of a website
   * @param {object} expectations
   * @param {boolean} expectations.checked - Whether the checkbox is expected to be checked.
   * @param {boolean} expectations.disabled - Whether the menuitem is expected to be disabled.
   */

  static async assertIsNeverTranslateSite(
    url,
    { checked = true, disabled = false }
  ) {
    info(
      `Checking that never-translate is ${
        checked ? "enabled" : "disabled"
      } for "${url}"`
    );
    await FullPageTranslationsTestUtils.#assertCheckboxState(
      "translations-panel-settings-never-translate-site",
      { checked, disabled }
    );
  }

  /**
   * Asserts that the proper language tags are shown on the translations button.
   *
   * @param {string} fromLanguage - The BCP-47 language tag being translated from.
   * @param {string} toLanguage - The BCP-47 language tag being translated into.
   * @param {ChromeWindow} win
   */

  static async assertLangTagIsShownOnTranslationsButton(
    fromLanguage,
    toLanguage,
    win = window
  ) {
    info(
      `Ensuring that the translations button displays the language tag "${toLanguage}"`
    );
    const { button, locale } =
      await FullPageTranslationsTestUtils.assertTranslationsButton(
        { button: true, circleArrows: false, locale: true, icon: true },
        "The icon presents the locale.",
        win
      );
    is(
      locale.innerText,
      toLanguage.split("-")[0],
      `The expected language tag "${toLanguage}" is shown.`
    );
    is(
      button.getAttribute("data-l10n-id"),
      "urlbar-translations-button-translated"
    );
    const fromLangDisplay = getIntlDisplayName(fromLanguage);
    const toLangDisplay = getIntlDisplayName(toLanguage);
    is(
      button.getAttribute("data-l10n-args"),
      `{"fromLanguage":"${fromLangDisplay}","toLanguage":"${toLangDisplay}"}`
    );
  }

  /**
   * Asserts that the Spanish test page has been translated by checking
   * that the H1 element has been modified from its original form.
   *
   * @param {object} options - The options for the assertion.
   *
   * @param {string} options.fromLanguage - The BCP-47 language tag being translated from.
   * @param {string} options.toLanguage - The BCP-47 language tag being translated into.
   * @param {Function} options.runInPage - Allows running a closure in the content page.
   * @param {boolean} [options.endToEndTest=false] - Whether this assertion is for an end-to-end test.
   * @param {string} [options.message] - An optional message to log to info.
   * @param {ChromeWindow} [options.win=window] - The window in which to perform the check (defaults to the current window).
   */

  static async assertPageIsTranslated({
    fromLanguage,
    toLanguage,
    runInPage,
    endToEndTest = false,
    message = null,
    win = window,
  }) {
    if (message) {
      info(message);
    }
    info("Checking that the page is translated");
    let callback;
    if (endToEndTest) {
      callback = async TranslationsTest => {
        const { getH1 } = TranslationsTest.getSelectors();
        await TranslationsTest.assertTranslationResult(
          "The page's H1 is translated.",
          getH1,
          "Don Quixote de La Mancha"
        );
      };
    } else {
      callback = async (TranslationsTest, { fromLang, toLang }) => {
        const { getH1 } = TranslationsTest.getSelectors();
        await TranslationsTest.assertTranslationResult(
          "The page's H1 is translated.",
          getH1,
          `DON QUIJOTE DE LA MANCHA [${fromLang} to ${toLang}, html]`
        );
      };
    }

    await runInPage(callback, { fromLang: fromLanguage, toLang: toLanguage });
    await FullPageTranslationsTestUtils.assertLangTagIsShownOnTranslationsButton(
      fromLanguage,
      toLanguage,
      win
    );
  }

  /**
   * Asserts that the Spanish test page is untranslated by checking
   * that the H1 element is still in its original Spanish form.
   *
   * @param {Function} runInPage - Allows running a closure in the content page.
   * @param {string} message - An optional message to log to info.
   */

  static async assertPageIsUntranslated(runInPage, message = null) {
    if (message) {
      info(message);
    }
    info("Checking that the page is untranslated");
    await runInPage(async TranslationsTest => {
      const { getH1 } = TranslationsTest.getSelectors();
      await TranslationsTest.assertTranslationResult(
        "The page's H1 is untranslated and in the original Spanish.",
        getH1,
        "Don Quijote de La Mancha"
      );
    });
  }

  /**
   * Asserts that for each provided expectation, the visible state of the corresponding
   * element in FullPageTranslationsPanel.elements both exists and matches the visibility expectation.
   *
   * @param {object} expectations
   *   A list of expectations for the visibility of any subset of FullPageTranslationsPanel.elements
   */

  static #assertPanelElementVisibility(expectations = {}) {
    SharedTranslationsTestUtils._assertPanelElementVisibility(
      FullPageTranslationsPanel.elements,
      {
        cancelButton: false,
        changeSourceLanguageButton: false,
        dismissErrorButton: false,
        error: false,
        errorMessage: false,
        errorMessageHint: false,
        errorHintAction: false,
        fromLabel: false,
        fromMenuList: false,
        fromMenuPopup: false,
        header: false,
        intro: false,
        introLearnMoreLink: false,
        langSelection: false,
        restoreButton: false,
        toLabel: false,
        toMenuList: false,
        toMenuPopup: false,
        translateButton: false,
        unsupportedHeader: false,
        unsupportedHint: false,
        unsupportedLearnMoreLink: false,
        // Overwrite any of the above defaults with the passed in expectations.
        ...expectations,
      }
    );
  }

  /**
   * Asserts that the FullPageTranslationsPanel header has the expected l10nId.
   *
   * @param {string} l10nId - The expected data-l10n-id of the header.
   */

  static #assertPanelHeaderL10nId(l10nId) {
    const { header } = FullPageTranslationsPanel.elements;
    SharedTranslationsTestUtils._assertL10nId(header, l10nId);
  }

  /**
   * Asserts that the FullPageTranslationsPanel error has the expected l10nId.
   *
   * @param {string} l10nId - The expected data-l10n-id of the error.
   */

  static #assertPanelErrorL10nId(l10nId) {
    const { errorMessage } = FullPageTranslationsPanel.elements;
    SharedTranslationsTestUtils._assertL10nId(errorMessage, l10nId);
  }

  /**
   * Asserts that the mainViewId of the panel matches the given string.
   *
   * @param {string} expectedId
   */

  static #assertPanelMainViewId(expectedId) {
    SharedTranslationsTestUtils._assertPanelMainViewId(
      FullPageTranslationsPanel,
      expectedId
    );

    const panelView = document.getElementById(expectedId);
    const label = document.getElementById(
      panelView.getAttribute("aria-labelledby")
    );
    ok(label, "The a11y label for the panel view can be found.");
    assertVisibility({ visible: { label } });
  }

  /**
   * Asserts that panel element visibility matches the default panel view.
   */

  static assertPanelViewDefault() {
    info("Checking that the panel shows the default view");
    FullPageTranslationsTestUtils.#assertPanelMainViewId(
      "full-page-translations-panel-view-default"
    );
    FullPageTranslationsTestUtils.#assertPanelElementVisibility({
      ...FullPageTranslationsTestUtils.#defaultViewVisibilityExpectations,
    });
    FullPageTranslationsTestUtils.#assertPanelHeaderL10nId(
      "translations-panel-header"
    );
  }

  /**
   * Asserts that panel element visibility matches the initialization-failure view.
   */

  static assertPanelViewInitFailure() {
    info("Checking that the panel shows the default view");
    const { translateButton } = FullPageTranslationsPanel.elements;
    FullPageTranslationsTestUtils.#assertPanelMainViewId(
      "full-page-translations-panel-view-default"
    );
    FullPageTranslationsTestUtils.#assertPanelElementVisibility({
      cancelButton: true,
      error: true,
      errorMessage: true,
      errorMessageHint: true,
      errorHintAction: true,
      header: true,
      translateButton: true,
    });
    is(
      translateButton.disabled,
      true,
      "The translate button should be disabled."
    );
    FullPageTranslationsTestUtils.#assertPanelHeaderL10nId(
      "translations-panel-header"
    );
  }

  /**
   * Asserts that panel element visibility matches the panel error view.
   */

  static assertPanelViewError() {
    info("Checking that the panel shows the error view");
    FullPageTranslationsTestUtils.#assertPanelMainViewId(
      "full-page-translations-panel-view-default"
    );
    FullPageTranslationsTestUtils.#assertPanelElementVisibility({
      error: true,
      errorMessage: true,
      ...FullPageTranslationsTestUtils.#defaultViewVisibilityExpectations,
    });
    FullPageTranslationsTestUtils.#assertPanelHeaderL10nId(
      "translations-panel-header"
    );
    FullPageTranslationsTestUtils.#assertPanelErrorL10nId(
      "translations-panel-error-translating"
    );
  }

  /**
   * Asserts that the panel element visibility matches the panel loading view.
   */

  static assertPanelViewLoading() {
    info("Checking that the panel shows the loading view");
    FullPageTranslationsTestUtils.assertPanelViewDefault();
    const loadingButton = getByL10nId(
      "translations-panel-translate-button-loading"
    );
    ok(loadingButton, "The loading button is present");
    ok(loadingButton.disabled, "The loading button is disabled");
  }

  /**
   * Asserts that panel element visibility matches the panel first-show view.
   */

  static assertPanelViewFirstShow() {
    info("Checking that the panel shows the first-show view");
    FullPageTranslationsTestUtils.#assertPanelMainViewId(
      "full-page-translations-panel-view-default"
    );
    FullPageTranslationsTestUtils.#assertPanelElementVisibility({
      intro: true,
      introLearnMoreLink: true,
      ...FullPageTranslationsTestUtils.#defaultViewVisibilityExpectations,
    });
    FullPageTranslationsTestUtils.#assertPanelHeaderL10nId(
      "translations-panel-intro-header"
    );
  }

  /**
   * Asserts that panel element visibility matches the panel first-show error view.
   */

  static assertPanelViewFirstShowError() {
    info("Checking that the panel shows the first-show error view");
    FullPageTranslationsTestUtils.#assertPanelMainViewId(
      "full-page-translations-panel-view-default"
    );
    FullPageTranslationsTestUtils.#assertPanelElementVisibility({
      error: true,
      intro: true,
      introLearnMoreLink: true,
      ...FullPageTranslationsTestUtils.#defaultViewVisibilityExpectations,
    });
    FullPageTranslationsTestUtils.#assertPanelHeaderL10nId(
      "translations-panel-intro-header"
    );
  }

  /**
   * Asserts that panel element visibility matches the panel revisit view.
   */

  static assertPanelViewRevisit() {
    info("Checking that the panel shows the revisit view");
    FullPageTranslationsTestUtils.#assertPanelMainViewId(
      "full-page-translations-panel-view-default"
    );
    FullPageTranslationsTestUtils.#assertPanelElementVisibility({
      header: true,
      langSelection: true,
      restoreButton: true,
      toLabel: true,
      toMenuList: true,
      translateButton: true,
    });
    FullPageTranslationsTestUtils.#assertPanelHeaderL10nId(
      "translations-panel-revisit-header"
    );
  }

  /**
   * Asserts that panel element visibility matches the panel unsupported language view.
   */

  static assertPanelViewUnsupportedLanguage() {
    info("Checking that the panel shows the unsupported-language view");
    FullPageTranslationsTestUtils.#assertPanelMainViewId(
      "full-page-translations-panel-view-unsupported-language"
    );
    FullPageTranslationsTestUtils.#assertPanelElementVisibility({
      changeSourceLanguageButton: true,
      dismissErrorButton: true,
      unsupportedHeader: true,
      unsupportedHint: true,
      unsupportedLearnMoreLink: true,
    });
  }

  /**
   * Asserts that the selected from-language matches the provided language tag.
   *
   * @param {object} options - Options containing 'langTag' and 'l10nId' to assert against.
   * @param {string} [options.langTag] - The BCP-47 language tag to match.
   * @param {string} [options.l10nId] - The localization Id to match.
   * @param {ChromeWindow} [options.win]
   *  - An optional ChromeWindow, for multi-window tests.
   */

  static assertSelectedFromLanguage({ langTag, l10nId, win = window }) {
    const { fromMenuList } = win.FullPageTranslationsPanel.elements;
    SharedTranslationsTestUtils._assertSelectedLanguage(fromMenuList, {
      langTag,
      l10nId,
    });
  }

  /**
   * Asserts that the selected to-language matches the provided language tag.
   *
   * @param {object} options - Options containing 'langTag' and 'l10nId' to assert against.
   * @param {string} [options.langTag] - The BCP-47 language tag to match.
   * @param {string} [options.l10nId] - The localization Id to match.
   * @param {ChromeWindow} [options.win]
   *  - An optional ChromeWindow, for multi-window tests.
   */

  static assertSelectedToLanguage({ langTag, l10nId, win = window }) {
    const { toMenuList } = win.FullPageTranslationsPanel.elements;
    SharedTranslationsTestUtils._assertSelectedLanguage(toMenuList, {
      langTag,
      l10nId,
    });
  }

  /**
   * Assert some property about the translations button.
   *
   * @param {Record<string, boolean>} visibleAssertions
   * @param {string} message The message for the assertion.
   * @param {ChromeWindow} [win]
   * @returns {HTMLElement}
   */

  static async assertTranslationsButton(
    visibleAssertions,
    message,
    win = window
  ) {
    const elements = {
      button: win.document.getElementById("translations-button"),
      icon: win.document.getElementById("translations-button-icon"),
      circleArrows: win.document.getElementById(
        "translations-button-circle-arrows"
      ),
      locale: win.document.getElementById("translations-button-locale"),
    };

    for (const [name, element] of Object.entries(elements)) {
      if (!element) {
        throw new Error("Could not find the " + name);
      }
    }

    try {
      // Test that the visibilities match.
      await waitForCondition(() => {
        for (const [name, visible] of Object.entries(visibleAssertions)) {
          if (elements[name].hidden === visible) {
            return false;
          }
        }
        return true;
      }, message);
    } catch (error) {
      // On a mismatch, report it.
      for (const [name, expected] of Object.entries(visibleAssertions)) {
        is(!elements[name].hidden, expected, `Visibility for "${name}"`);
      }
    }

    ok(true, message);

    return elements;
  }

  /**
   * Simulates the effect of clicking the always-offer-translations menuitem.
   * Requires that the settings menu of the translations panel is open,
   * otherwise the test will fail.
   */

  static async clickAlwaysOfferTranslations() {
    logAction();
    await FullPageTranslationsTestUtils.#clickSettingsMenuItemByL10nId(
      "translations-panel-settings-always-offer-translation"
    );
  }

  /**
   * Simulates the effect of clicking the always-translate-language menuitem.
   * Requires that the settings menu of the translations panel is open,
   * otherwise the test will fail.
   */

  static async clickAlwaysTranslateLanguage({
    downloadHandler = null,
    pivotTranslation = false,
  } = {}) {
    logAction();
    await FullPageTranslationsTestUtils.#clickSettingsMenuItemByL10nId(
      "translations-panel-settings-always-translate-language"
    );
    if (downloadHandler) {
      await FullPageTranslationsTestUtils.assertTranslationsButton(
        { button: true, circleArrows: true, locale: false, icon: true },
        "The icon presents the loading indicator."
      );
      await downloadHandler(pivotTranslation ? 2 : 1);
    }
  }

  /**
   * Simulates clicking the cancel button.
   */

  static async clickCancelButton() {
    logAction();
    const { cancelButton } = FullPageTranslationsPanel.elements;
    assertVisibility({ visible: { cancelButton } });
    await FullPageTranslationsTestUtils.waitForPanelPopupEvent(
      "popuphidden",
      () => {
        click(cancelButton, "Clicking the cancel button");
      }
    );
  }

  /**
   * Simulates clicking the change-source-language button.
   *
   * @param {object} config
   * @param {boolean} config.firstShow
   *  - True if the first-show view should be expected
   *    False if the default view should be expected
   */

  static async clickChangeSourceLanguageButton({ firstShow = false } = {}) {
    logAction();
    const { changeSourceLanguageButton } = FullPageTranslationsPanel.elements;
    assertVisibility({ visible: { changeSourceLanguageButton } });
    await FullPageTranslationsTestUtils.waitForPanelPopupEvent(
      "popupshown",
      () => {
        click(
          changeSourceLanguageButton,
          "Click the change-source-language button"
        );
      },
      firstShow
        ? FullPageTranslationsTestUtils.assertPanelViewFirstShow
        : FullPageTranslationsTestUtils.assertPanelViewDefault
    );
  }

  /**
   * Simulates clicking the dismiss-error button.
   */

  static async clickDismissErrorButton() {
    logAction();
    const { dismissErrorButton } = FullPageTranslationsPanel.elements;
    assertVisibility({ visible: { dismissErrorButton } });
    await FullPageTranslationsTestUtils.waitForPanelPopupEvent(
      "popuphidden",
      () => {
        click(dismissErrorButton, "Click the dismiss-error button");
      }
    );
  }

  /**
   * Simulates the effect of clicking the manage-languages menuitem.
   * Requires that the settings menu of the translations panel is open,
   * otherwise the test will fail.
   */

  static async clickManageLanguages() {
    logAction();
    await FullPageTranslationsTestUtils.#clickSettingsMenuItemByL10nId(
      "translations-panel-settings-manage-languages"
    );
  }

  /**
   * Simulates the effect of clicking the never-translate-language menuitem.
   * Requires that the settings menu of the translations panel is open,
   * otherwise the test will fail.
   */

  static async clickNeverTranslateLanguage() {
    logAction();
    await FullPageTranslationsTestUtils.#clickSettingsMenuItemByL10nId(
      "translations-panel-settings-never-translate-language"
    );
  }

  /**
   * Simulates the effect of clicking the never-translate-site menuitem.
   * Requires that the settings menu of the translations panel is open,
   * otherwise the test will fail.
   */

  static async clickNeverTranslateSite() {
    logAction();
    await FullPageTranslationsTestUtils.#clickSettingsMenuItemByL10nId(
      "translations-panel-settings-never-translate-site"
    );
  }

  /**
   * Simulates clicking the restore-page button.
   *
   * @param {ChromeWindow} [win]
   *  - An optional ChromeWindow, for multi-window tests.
   */

  static async clickRestoreButton(win = window) {
    logAction();
    const { restoreButton } = win.FullPageTranslationsPanel.elements;
    assertVisibility({ visible: { restoreButton } });
    await FullPageTranslationsTestUtils.waitForPanelPopupEvent(
      "popuphidden",
      () => {
        click(restoreButton, "Click the restore-page button");
      }
    );
  }

  /*
   * Simulates the effect of toggling a menu item in the translations panel
   * settings menu. Requires that the settings menu is currently open,
   * otherwise the test will fail.
   */

  static async #clickSettingsMenuItemByL10nId(l10nId) {
    info(`Toggling the "${l10nId}" settings menu item.`);
    click(getByL10nId(l10nId), `Clicking the "${l10nId}" settings menu item.`);
    await closeFullPagePanelSettingsMenuIfOpen();
  }

  /**
   * Simulates clicking the translate button.
   *
   * @param {object} config
   * @param {Function} config.downloadHandler
   *  - The function handle expected downloads, resolveDownloads() or rejectDownloads()
   *    Leave as null to test more granularly, such as testing opening the loading view,
   *    or allowing for the automatic downloading of files.
   * @param {boolean} config.pivotTranslation
   *  - True if the expected translation is a pivot translation, otherwise false.
   *    Affects the number of expected downloads.
   * @param {Function} config.onOpenPanel
   *  - A function to run as soon as the panel opens.
   * @param {ChromeWindow} [config.win]
   *  - An optional ChromeWindow, for multi-window tests.
   */

  static async clickTranslateButton({
    downloadHandler = null,
    pivotTranslation = false,
    onOpenPanel = null,
    win = window,
  } = {}) {
    logAction();
    const { translateButton } = win.FullPageTranslationsPanel.elements;
    assertVisibility({ visible: { translateButton } });
    await FullPageTranslationsTestUtils.waitForPanelPopupEvent(
      "popuphidden",
      () => {
        click(translateButton);
      },
      null /* postEventAssertion */,
      win
    );

    let panelOpenCallbackPromise;
    if (onOpenPanel) {
      panelOpenCallbackPromise =
        FullPageTranslationsTestUtils.waitForPanelPopupEvent(
          "popupshown",
          () => {},
          onOpenPanel
        );
    }

    if (downloadHandler) {
      await FullPageTranslationsTestUtils.assertTranslationsButton(
        { button: true, circleArrows: true, locale: false, icon: true },
        "The icon presents the loading indicator.",
        win
      );
      await downloadHandler(pivotTranslation ? 2 : 1);
    }

    await panelOpenCallbackPromise;
  }

  /**
   * Opens the translations panel.
   *
   * @param {object} config
   * @param {Function} config.onOpenPanel
   *  - A function to run as soon as the panel opens.
   * @param {boolean} config.openFromAppMenu
   *  - Open the panel from the app menu. If false, uses the translations button.
   * @param {boolean} config.openWithKeyboard
   *  - Open the panel by synthesizing the keyboard. If false, synthesizes the mouse.
   * @param {string} [config.expectedFromLanguage] - The expected from-language tag.
   * @param {string} [config.expectedToLanguage] - The expected to-language tag.
   * @param {ChromeWindow} [config.win]
   *  - An optional window for multi-window tests.
   */

  static async openPanel({
    onOpenPanel = null,
    openFromAppMenu = false,
    openWithKeyboard = false,
    expectedFromLanguage = undefined,
    expectedToLanguage = undefined,
    win = window,
  }) {
    logAction();
    await closeAllOpenPanelsAndMenus(win);
    if (openFromAppMenu) {
      await FullPageTranslationsTestUtils.#openPanelViaAppMenu({
        win,
        onOpenPanel,
        openWithKeyboard,
      });
    } else {
      await FullPageTranslationsTestUtils.#openPanelViaTranslationsButton({
        win,
        onOpenPanel,
        openWithKeyboard,
      });
    }
    if (expectedFromLanguage !== undefined) {
      FullPageTranslationsTestUtils.assertSelectedFromLanguage({
        win,
        langTag: expectedFromLanguage,
      });
    }
    if (expectedToLanguage !== undefined) {
      FullPageTranslationsTestUtils.assertSelectedToLanguage({
        win,
        langTag: expectedToLanguage,
      });
    }
  }

  /**
   * Opens the translations panel via the app menu.
   *
   * @param {object} config
   * @param {Function} config.onOpenPanel
   *  - A function to run as soon as the panel opens.
   * @param {boolean} config.openWithKeyboard
   *  - Open the panel by synthesizing the keyboard. If false, synthesizes the mouse.
   * @param {ChromeWindow} [config.win]
   */

  static async #openPanelViaAppMenu({
    onOpenPanel = null,
    openWithKeyboard = false,
    win = window,
  }) {
    logAction();
    const appMenuButton = getById("PanelUI-menu-button", win.document);
    if (openWithKeyboard) {
      hitEnterKey(appMenuButton, "Opening the app-menu button with keyboard");
    } else {
      click(appMenuButton, "Opening the app-menu button");
    }
    await BrowserTestUtils.waitForEvent(win.PanelUI.mainView, "ViewShown");

    const translateSiteButton = getById(
      "appMenu-translate-button",
      win.document
    );

    is(
      translateSiteButton.disabled,
      false,
      "The app-menu translate button should be enabled"
    );

    await FullPageTranslationsTestUtils.waitForPanelPopupEvent(
      "popupshown",
      () => {
        if (openWithKeyboard) {
          hitEnterKey(translateSiteButton, "Opening the popup with keyboard");
        } else {
          click(translateSiteButton, "Opening the popup");
        }
      },
      onOpenPanel
    );
  }

  /**
   * Opens the translations panel via the translations button.
   *
   * @param {object} config
   * @param {Function} config.onOpenPanel
   *  - A function to run as soon as the panel opens.
   * @param {boolean} config.openWithKeyboard
   *  - Open the panel by synthesizing the keyboard. If false, synthesizes the mouse.
   * @param {ChromeWindow} [config.win]
   */

  static async #openPanelViaTranslationsButton({
    onOpenPanel = null,
    openWithKeyboard = false,
    win = window,
  }) {
    logAction();
    const { button } =
      await FullPageTranslationsTestUtils.assertTranslationsButton(
        { button: true },
        "The translations button is visible.",
        win
      );
    await FullPageTranslationsTestUtils.waitForPanelPopupEvent(
      "popupshown",
      () => {
        if (openWithKeyboard) {
          hitEnterKey(button, "Opening the popup with keyboard");
        } else {
          click(button, "Opening the popup");
        }
      },
      onOpenPanel,
      win
    );
  }

  /**
   * Opens the translations panel settings menu.
   * Requires that the translations panel is already open.
   */

  static async openTranslationsSettingsMenu() {
    logAction();
    const gearIcons = getAllByL10nId("translations-panel-settings-button");
    for (const gearIcon of gearIcons) {
      if (BrowserTestUtils.isHidden(gearIcon)) {
        continue;
      }
      click(gearIcon, "Open the settings menu");
      info("Waiting for settings menu to open.");
      const manageLanguages = await waitForCondition(() =>
        maybeGetByL10nId("translations-panel-settings-manage-languages")
      );
      ok(
        manageLanguages,
        "The manage languages item should be visible in the settings menu."
      );
      return;
    }
  }

  /**
   * Changes the selected language by opening the dropdown menu for each provided language tag.
   *
   * @param {string} langTag - The BCP-47 language tag to select from the dropdown menu.
   * @param {object} elements - Elements involved in the dropdown language selection process.
   * @param {Element} elements.menuList - The element that triggers the dropdown menu.
   * @param {Element} elements.menuPopup - The dropdown menu element containing selectable languages.
   * @param {ChromeWindow} [win]
   *  - An optional ChromeWindow, for multi-window tests.
   *
   * @returns {Promise<void>}
   */

  static async #changeSelectedLanguage(langTag, elements, win = window) {
    const { menuList, menuPopup } = elements;

    await FullPageTranslationsTestUtils.waitForPanelPopupEvent(
      "popupshown",
      () => click(menuList),
      null /* postEventAssertion */,
      win
    );

    const menuItem = menuPopup.querySelector(`[value="${langTag}"]`);
    await FullPageTranslationsTestUtils.waitForPanelPopupEvent(
      "popuphidden",
      () => {
        click(menuItem);
        // Synthesizing a click on the menuitem isn't closing the popup
        // as a click normally would, so this tab keypress is added to
        // ensure the popup closes.
        EventUtils.synthesizeKey("KEY_Tab", {}, win);
      },
      null /* postEventAssertion */,
      win
    );
  }

  /**
   * Switches the selected from-language to the provided language tag.
   *
   * @param {object} options
   * @param {string} options.langTag - A BCP-47 language tag.
   * @param {ChromeWindow} [options.win]
   *  - An optional ChromeWindow, for multi-window tests.
   */

  static async changeSelectedFromLanguage({ langTag, win = window }) {
    logAction(langTag);
    const { fromMenuList: menuList, fromMenuPopup: menuPopup } =
      win.FullPageTranslationsPanel.elements;
    await FullPageTranslationsTestUtils.#changeSelectedLanguage(
      langTag,
      {
        menuList,
        menuPopup,
      },
      win
    );
  }

  /**
   * Switches the selected to-language to the provided language tag.
   *
   * @param {object} options
   * @param {string} options.langTag - A BCP-47 language tag.
   * @param {ChromeWindow} [options.win]
   *  - An optional ChromeWindow, for multi-window tests.
   */

  static async changeSelectedToLanguage({ langTag, win = window }) {
    logAction(langTag);
    const { toMenuList: menuList, toMenuPopup: menuPopup } =
      win.FullPageTranslationsPanel.elements;
    await FullPageTranslationsTestUtils.#changeSelectedLanguage(
      langTag,
      {
        menuList,
        menuPopup,
      },
      win
    );
  }

  /**
   * XUL popups will fire the popupshown and popuphidden events. These will fire for
   * any type of popup in the browser. This function waits for one of those events, and
   * checks that the viewId of the popup is PanelUI-profiler
   *
   * @param {"popupshown" | "popuphidden"} eventName
   * @param {Function} callback
   * @param {Function} postEventAssertion
   *   An optional assertion to be made immediately after the event occurs.
   * @param {ChromeWindow} [win]
   * @returns {Promise<void>}
   */

  static async waitForPanelPopupEvent(
    eventName,
    callback,
    postEventAssertion = null,
    win = window
  ) {
    // De-lazify the panel elements.
    win.FullPageTranslationsPanel.elements;
    await SharedTranslationsTestUtils._waitForPopupEvent(
      "full-page-translations-panel",
      eventName,
      callback,
      postEventAssertion,
      win
    );
  }
}

/**
 * A class containing test utility functions specific to testing select translations.
 */

class SelectTranslationsTestUtils {
  /**
   * Opens the context menu then asserts properties of the translate-selection item in the context menu.
   *
   * @param {Function} runInPage - A content-exposed function to run within the context of the page.
   * @param {object} options - Options for how to open the context menu and what properties to assert about the translate-selection item.
   *
   * @param {boolean} options.expectMenuItemVisible - Whether the select-translations menu item should be present in the context menu.
   * @param {boolean} options.expectedTargetLanguage - The expected target language to be shown in the context menu.
   *
   * The following options will work on all test pages that have an <h1> element.
   *
   * @param {boolean} options.selectH1 - Selects the first H1 element of the page.
   * @param {boolean} options.openAtH1 - Opens the context menu at the first H1 element of the page.
   *
   * The following options will work only in the PDF_TEST_PAGE_URL.
   *
   * @param {boolean} options.selectPdfSpan - Selects the first span of text on the first page of a pdf.
   * @param {boolean} options.openAtPdfSpan - Opens the context menu at the first span of text on the first page of a pdf.
   *
   * The following options will only work when testing SELECT_TEST_PAGE_URL.
   *
   * @param {boolean} options.selectFrenchSection - Selects the section of French text.
   * @param {boolean} options.selectEnglishSection - Selects the section of English text.
   * @param {boolean} options.selectSpanishSection - Selects the section of Spanish text.
   * @param {boolean} options.selectFrenchSentence - Selects a French sentence.
   * @param {boolean} options.selectEnglishSentence - Selects an English sentence.
   * @param {boolean} options.selectSpanishSentence - Selects a Spanish sentence.
   * @param {boolean} options.openAtFrenchSection - Opens the context menu at the section of French text.
   * @param {boolean} options.openAtEnglishSection - Opens the context menu at the section of English text.
   * @param {boolean} options.openAtSpanishSection - Opens the context menu at the section of Spanish text.
   * @param {boolean} options.openAtFrenchSentence - Opens the context menu at a French sentence.
   * @param {boolean} options.openAtEnglishSentence - Opens the context menu at an English sentence.
   * @param {boolean} options.openAtSpanishSentence - Opens the context menu at a Spanish sentence.
   * @param {boolean} options.openAtFrenchHyperlink - Opens the context menu at a hyperlinked French text.
   * @param {boolean} options.openAtEnglishHyperlink - Opens the context menu at a hyperlinked English text.
   * @param {boolean} options.openAtSpanishHyperlink - Opens the context menu at a hyperlinked Spanish text.
   * @param {boolean} options.openAtURLHyperlink - Opens the context menu at a hyperlinked URL text.
   * @param {string} [message] - A message to log to info.
   * @throws Throws an error if the properties of the translate-selection item do not match the expected options.
   */

  static async assertContextMenuTranslateSelectionItem(
    runInPage,
    {
      expectMenuItemVisible,
      expectedTargetLanguage,
      selectH1,
      selectPdfSpan,
      selectFrenchSection,
      selectEnglishSection,
      selectSpanishSection,
      selectFrenchSentence,
      selectEnglishSentence,
      selectSpanishSentence,
      openAtH1,
      openAtPdfSpan,
      openAtFrenchSection,
      openAtEnglishSection,
      openAtSpanishSection,
      openAtFrenchSentence,
      openAtEnglishSentence,
      openAtSpanishSentence,
      openAtFrenchHyperlink,
      openAtEnglishHyperlink,
      openAtSpanishHyperlink,
      openAtURLHyperlink,
    },
    message
  ) {
    logAction();

    if (message) {
      info(message);
    }

--> --------------------

--> maximum size reached

--> --------------------

Messung V0.5
C=95 H=90 G=92

¤ Dauer der Verarbeitung: 0.54 Sekunden  (vorverarbeitet)  ¤

*© Formatika GbR, Deutschland






Wurzel

Suchen

Beweissystem der NASA

Beweissystem Isabelle

NIST Cobol Testsuite

Cephes Mathematical Library

Wiener Entwicklungsmethode

Haftungshinweis

Die Informationen auf dieser Webseite wurden nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit, noch Qualität der bereit gestellten Informationen zugesichert.

Bemerkung:

Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.