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];
}
/** * 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 = newFunction(/* 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});
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); returnnew Promise(resolve => {
element.addEventListener( "click", function () {
resolve();
},
{ once: true }
);
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) { thrownew 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) { thrownew 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) { thrownew Error("Could not find the element by l10n id: " + l10nId);
} for (const element of elements) { if (BrowserTestUtils.isVisible(element)) { return element;
}
} thrownew 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;
/** * 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) { thrownew Error("Could not find the element by id: #" + id);
}
if (!ensureIsVisible) { return element;
}
if (BrowserTestUtils.isVisible(element)) { return element;
}
returnnull;
}
/** * 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;
}
} returnnull;
}
/** * 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;
}
/** * 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.
} returnfalse;
}
/** * 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);
/** * 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);
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] ?? {};
4) Include the resulting metadata for ${testPageName} in the TranslationsBencher.#PAGE_DATA object.
`);
}
if (sourceLanguage !== pageLanguage) { thrownew Error(
`Perf test source language '${sourceLanguage}' did not match the expected page language '${pageLanguage}'.`
);
}
const journal = new TranslationsBencher.Journal();
/** * 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) { thrownew Error("Unable to find the last paragraph for observation.");
}
const observer = new content.MutationObserver(
(_mutationsList, _observer) => {
content.document.dispatchEvent( new CustomEvent("TranslationComplete")
);
}
);
/** * 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());
}
}
/** * 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();
/** * 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);
/** * 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) { thrownew 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]`
);
};
}
/** * 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) { thrownew 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) { returnfalse;
}
} returntrue;
}, 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 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 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");
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;
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();
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.