/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Avoid about:blank's non-standard behavior.
const BLANK_PAGE =
"data:text/html;charset=utf-8,
BlankBlank page";
const URL_COM_PREFIX = "https://example.com/browser/";
const URL_ORG_PREFIX = "https://example.org/browser/";
const CHROME_URL_PREFIX = "chrome://mochitests/content/browser/";
const DIR_PATH = "toolkit/components/translations/tests/browser/";
const TRANSLATIONS_TESTER_EN =
URL_COM_PREFIX + DIR_PATH + "translations-tester-en.html";
const TRANSLATIONS_TESTER_ES =
URL_COM_PREFIX + DIR_PATH + "translations-tester-es.html";
const TRANSLATIONS_TESTER_ES_2 =
URL_COM_PREFIX + DIR_PATH + "translations-tester-es-2.html";
const TRANSLATIONS_TESTER_ES_DOT_ORG =
URL_ORG_PREFIX + DIR_PATH + "translations-tester-es.html";
const TRANSLATIONS_TESTER_NO_TAG =
URL_COM_PREFIX + DIR_PATH + "translations-tester-no-tag.html";
/**
* The mochitest runs in the parent process. This function opens up a new tab,
* opens up about:translations, and passes the test requirements into the content process.
*
* @template T
*
* @param {object} options
*
* @param {T} options.dataForContent
* The data must support structural cloning and will be passed into the
* content process.
*
* @param {(args: { dataForContent: T, selectors: Record }) => Promise} options.runInPage
* This function must not capture any values, as it will be cloned in the content process.
* Any required data should be passed in using the "dataForContent" parameter. The
* "selectors" property contains any useful selectors for the content.
*
* @param {boolean} [options.disabled]
* Disable the panel through a pref.
*
* @param {number} detectedLanguageConfidence
* This is the value for the MockedLanguageIdEngine to give as a confidence score for
* the mocked detected language.
*
* @param {string} detectedLangTag
* This is the BCP 47 language tag for the MockedLanguageIdEngine to return as
* the mocked detected language.
*
* @param {Array<{ fromLang: string, toLang: string, isBeta: boolean }>} options.languagePairs
* The translation languages pairs to mock for the test.
*
* @param {Array<[string, string]>} options.prefs
* Prefs to push on for the test.
*/
async function openAboutTranslations({
dataForContent,
disabled,
runInPage,
detectedLanguageConfidence,
detectedLangTag,
languagePairs = DEFAULT_LANGUAGE_PAIRS,
prefs,
}) {
await SpecialPowers.pushPrefEnv({
set: [
// Enabled by default.
["browser.translations.enable", !disabled],
["browser.translations.logLevel", "All"],
...(prefs ?? []),
],
});
/**
* Collect any relevant selectors for the page here.
*/
const selectors = {
pageHeader: '[data-l10n-id="about-translations-header"]',
fromLanguageSelect: "select#language-from",
toLanguageSelect: "select#language-to",
translationTextarea: "textarea#translation-from",
translationResult: "#translation-to",
translationResultBlank: "#translation-to-blank",
translationInfo: "#translation-info",
noSupportMessage: "[data-l10n-id='about-translations-no-support']",
};
// Start the tab at a blank page.
let tab = await BrowserTestUtils.openNewForegroundTab(
gBrowser,
BLANK_PAGE,
true // waitForLoad
);
const { removeMocks, remoteClients } = await createAndMockRemoteSettings({
languagePairs,
// TODO(Bug 1814168) - Do not test download behavior as this is not robustly
// handled for about:translations yet.
autoDownloadFromRemoteSettings: true,
detectedLangTag,
detectedLanguageConfidence,
});
// Now load the about:translations page, since the actor could be mocked.
BrowserTestUtils.loadURIString(tab.linkedBrowser, "about:translations");
await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
// Resolve the files.
await remoteClients.languageIdModels.resolvePendingDownloads(1);
// The language id and translation engine each have a wasm file, so expect 2 downloads.
await remoteClients.translationsWasm.resolvePendingDownloads(2);
await remoteClients.translationModels.resolvePendingDownloads(
languagePairs.length * FILES_PER_LANGUAGE_PAIR
);
await ContentTask.spawn(
tab.linkedBrowser,
{ dataForContent, selectors },
runInPage
);
removeMocks();
BrowserTestUtils.removeTab(tab);
await SpecialPowers.popPrefEnv();
}
/**
* Naively prettify's html based on the opening and closing tags. This is not robust
* for general usage, but should be adequate for these tests.
* @param {string} html
* @returns {string}
*/
function naivelyPrettify(html) {
let result = "";
let indent = 0;
function addText(actualEndIndex) {
const text = html.slice(startIndex, actualEndIndex).trim();
if (text) {
for (let i = 0; i < indent; i++) {
result += " ";
}
result += text + "\n";
}
startIndex = actualEndIndex;
}
let startIndex = 0;
let endIndex = 0;
for (; endIndex < html.length; endIndex++) {
if (
html[endIndex] === " " ||
html[endIndex] === "\t" ||
html[endIndex] === "n"
) {
// Skip whitespace.
// " foobar
"
// ^^^
startIndex = endIndex;
continue;
}
// Find all of the text.
// "foobar
"
// ^^^^^^
while (endIndex < html.length && html[endIndex] !== "<") {
endIndex++;
}
addText(endIndex);
if (html[endIndex] === "<") {
if (html[endIndex + 1] === "/") {
// "foobar
"
// ^
while (endIndex < html.length && html[endIndex] !== ">") {
endIndex++;
}
indent--;
addText(endIndex + 1);
} else {
// "foobar
"
// ^
while (endIndex < html.length && html[endIndex] !== ">") {
endIndex++;
}
// "foobar
"
// ^
addText(endIndex + 1);
indent++;
}
}
}
return result.trim();
}
/**
* This fake translator reports on the batching of calls by replacing the text
* with a letter. Each call of the function moves the letter forward alphabetically.
*
* So consecutive calls would transform things like:
* "First translation" -> "aaaa aaaaaaaaa"
* "Second translation" -> "bbbbb bbbbbbbbb"
* "Third translation" -> "cccc ccccccccc"
*
* This can visually show what the translation batching behavior looks like.
*/
function createBatchFakeTranslator() {
let letter = "a";
/**
* @param {string} message
*/
return async function fakeTranslator(message) {
/**
* @param {Node} node
*/
function transformNode(node) {
if (typeof node.nodeValue === "string") {
node.nodeValue = node.nodeValue.replace(/\w/g, letter);
}
for (const childNode of node.childNodes) {
transformNode(childNode);
}
}
const parser = new DOMParser();
const translatedDoc = parser.parseFromString(message, "text/html");
transformNode(translatedDoc.body);
// "Increment" the letter.
letter = String.fromCodePoint(letter.codePointAt(0) + 1);
return [translatedDoc.body.innerHTML];
};
}
/**
* This fake translator reorders Nodes to be in alphabetical order, and then
* uppercases the text. This allows for testing the reordering behavior of the
* translation engine.
*
* @param {string} message
*/
async function reorderingTranslator(message) {
/**
* @param {Node} node
*/
function transformNode(node) {
if (typeof node.nodeValue === "string") {
node.nodeValue = node.nodeValue.toUpperCase();
}
const nodes = [...node.childNodes];
nodes.sort((a, b) =>
(a.textContent?.trim() ?? "").localeCompare(b.textContent?.trim() ?? "")
);
for (const childNode of nodes) {
childNode.remove();
}
for (const childNode of nodes) {
// Re-append in sorted order.
node.appendChild(childNode);
transformNode(childNode);
}
}
const parser = new DOMParser();
const translatedDoc = parser.parseFromString(message, "text/html");
transformNode(translatedDoc.body);
return [translatedDoc.body.innerHTML];
}
/**
* @returns {import("../../actors/TranslationsParent.sys.mjs").TranslationsParent}
*/
function getTranslationsParent() {
return gBrowser.selectedBrowser.browsingContext.currentWindowGlobal.getActor(
"Translations"
);
}
/**
* This is for tests that don't need a browser page to run.
*/
async function setupActorTest({
languagePairs,
prefs,
detectedLanguageConfidence,
detectedLangTag,
}) {
await SpecialPowers.pushPrefEnv({
set: [
// Enabled by default.
["browser.translations.enable", true],
["browser.translations.logLevel", "All"],
...(prefs ?? []),
],
});
const { remoteClients, removeMocks } = await createAndMockRemoteSettings({
languagePairs,
detectedLangTag,
detectedLanguageConfidence,
});
// Create a new tab so each test gets a new actor, and doesn't re-use the old one.
const tab = await BrowserTestUtils.openNewForegroundTab(
gBrowser,
BLANK_PAGE,
true // waitForLoad
);
return {
actor: getTranslationsParent(),
remoteClients,
cleanup() {
BrowserTestUtils.removeTab(tab);
removeMocks();
return SpecialPowers.popPrefEnv();
},
};
}
/**
* Provide some default language pairs when none are provided.
*/
const DEFAULT_LANGUAGE_PAIRS = [
{ fromLang: "en", toLang: "es", isBeta: false },
{ fromLang: "es", toLang: "en", isBeta: false },
];
async function createAndMockRemoteSettings({
languagePairs = DEFAULT_LANGUAGE_PAIRS,
detectedLanguageConfidence = 0.5,
detectedLangTag = "en",
autoDownloadFromRemoteSettings = false,
}) {
const remoteClients = {
translationModels: await createTranslationModelsRemoteClient(
autoDownloadFromRemoteSettings,
languagePairs
),
translationsWasm: await createTranslationsWasmRemoteClient(
autoDownloadFromRemoteSettings
),
languageIdModels: await createLanguageIdModelsRemoteClient(
autoDownloadFromRemoteSettings
),
};
TranslationsParent.mockTranslationsEngine(
remoteClients.translationModels.client,
remoteClients.translationsWasm.client
);
TranslationsParent.mockLanguageIdentification(
detectedLangTag,
detectedLanguageConfidence,
remoteClients.languageIdModels.client
);
return {
removeMocks() {
TranslationsParent.unmockTranslationsEngine();
TranslationsParent.unmockLanguageIdentification();
},
remoteClients,
};
}
async function loadTestPage({
languagePairs,
autoDownloadFromRemoteSettings = false,
detectedLanguageConfidence,
detectedLangTag,
page,
prefs,
permissionsUrls = [],
}) {
Services.fog.testResetFOG();
await SpecialPowers.pushPrefEnv({
set: [
// Enabled by default.
["browser.translations.enable", true],
["browser.translations.logLevel", "All"],
...(prefs ?? []),
],
});
await SpecialPowers.pushPermissions(
permissionsUrls.map(url => ({
type: "translations",
allow: true,
context: url,
}))
);
// Start the tab at a blank page.
const tab = await BrowserTestUtils.openNewForegroundTab(
gBrowser,
BLANK_PAGE,
true // waitForLoad
);
const { remoteClients, removeMocks } = await createAndMockRemoteSettings({
languagePairs,
detectedLanguageConfidence,
detectedLangTag,
autoDownloadFromRemoteSettings,
});
BrowserTestUtils.loadURIString(tab.linkedBrowser, page);
await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
return {
tab,
remoteClients,
/**
* @param {number} count - Count of the language pairs expected.
*/
async resolveDownloads(count) {
await remoteClients.translationsWasm.resolvePendingDownloads(1);
await remoteClients.translationModels.resolvePendingDownloads(
FILES_PER_LANGUAGE_PAIR * count
);
},
/**
* @param {number} count - Count of the language pairs expected.
*/
async rejectDownloads(count) {
await remoteClients.translationsWasm.rejectPendingDownloads(1);
await remoteClients.translationModels.rejectPendingDownloads(
FILES_PER_LANGUAGE_PAIR * count
);
},
async resolveLanguageIdDownloads() {
await remoteClients.translationsWasm.resolvePendingDownloads(1);
await remoteClients.languageIdModels.resolvePendingDownloads(1);
},
/**
* @returns {Promise}
*/
cleanup() {
removeMocks();
Services.fog.testResetFOG();
BrowserTestUtils.removeTab(tab);
return Promise.all([
SpecialPowers.popPrefEnv(),
SpecialPowers.popPermissions(),
]);
},
/**
* 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}
*/
runInPage(callback) {
// 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});
return (${callback.toString()})(TranslationsTest);
`);
return ContentTask.spawn(
tab.linkedBrowser,
{}, // Data to inject.
fn
);
},
};
}
/**
* Captures any reported errors in the TranslationsParent.
*
* @param {Function} callback
* @returns {Array<{ error: Error, args: any[] }>}
*/
async function captureTranslationsError(callback) {
const { reportError } = TranslationsParent;
let errors = [];
TranslationsParent.reportError = (error, ...args) => {
errors.push({ error, args });
};
await callback();
// Restore the original function.
TranslationsParent.reportError = reportError;
return errors;
}
/**
* Load a test page and run
* @param {Object} options - The options for `loadTestPage` plus a `runInPage` function.
*/
async function autoTranslatePage(options) {
const { cleanup, runInPage } = await loadTestPage({
autoDownloadFromRemoteSettings: true,
prefs: [
["browser.translations.autoTranslate", true],
...(options.prefs ?? []),
],
...options,
});
await runInPage(options.runInPage);
await cleanup();
}
/**
* @param {RemoteSettingsClient} client
* @param {boolean} autoDownloadFromRemoteSettings - Skip the manual download process,
* and automatically download the files. Normally it's preferrable to manually trigger
* the downloads to trigger the download behavior, but this flag lets you bypass this
* and automatically download the files.
*/
function createAttachmentMock(client, autoDownloadFromRemoteSettings) {
const pendingDownloads = [];
client.attachments.download = record =>
new Promise((resolve, reject) => {
console.log("Download requested:", client.collectionName, record.name);
if (autoDownloadFromRemoteSettings) {
resolve({ buffer: new ArrayBuffer() });
} else {
pendingDownloads.push({ record, resolve, reject });
}
});
function resolvePendingDownloads(expectedDownloadCount) {
info(
`Resolving ${expectedDownloadCount} mocked downloads for "${client.collectionName}"`
);
return downloadHandler(expectedDownloadCount, download =>
download.resolve({ buffer: new ArrayBuffer() })
);
}
async function rejectPendingDownloads(expectedDownloadCount) {
info(
`Intentionally rejecting ${expectedDownloadCount} mocked downloads for "${client.collectionName}"`
);
// Add 1 to account for the original attempt.
const attempts = TranslationsParent.MAX_DOWNLOAD_RETRIES + 1;
return downloadHandler(expectedDownloadCount * attempts, download =>
download.reject(new Error("Intentionally rejecting downloads."))
);
}
async function downloadHandler(expectedDownloadCount, action) {
const names = [];
let maxTries = 100;
while (names.length < expectedDownloadCount && maxTries-- > 0) {
await new Promise(resolve => setTimeout(resolve, 0));
let download = pendingDownloads.shift();
if (!download) {
// Uncomment the following to debug download issues:
// console.log(`No pending download:`, client.collectionName, names.length);
continue;
}
console.log(`Handling download:`, client.collectionName);
action(download);
names.push(download.record.name);
}
// This next check is not guaranteed to catch an unexpected download, but wait
// at least one event loop tick to see if any more downloads were added.
await new Promise(resolve => setTimeout(resolve, 0));
if (pendingDownloads.length) {
throw new Error(
`An unexpected download was found, only expected ${expectedDownloadCount} downloads`
);
}
return names.sort((a, b) => a.localeCompare(b));
}
async function assertNoNewDownloads() {
await new Promise(resolve => setTimeout(resolve, 0));
is(
pendingDownloads.length,
0,
`No downloads happened for "${client.collectionName}"`
);
}
return {
client,
pendingDownloads,
resolvePendingDownloads,
rejectPendingDownloads,
assertNoNewDownloads,
};
}
/**
* The amount of files that are generated per mocked language pair.
*/
const FILES_PER_LANGUAGE_PAIR = 3;
/**
* Creates a local RemoteSettingsClient for use within tests.
*
* @param {boolean} autoDownloadFromRemoteSettings
* @param {Object[]} langPairs
* @returns {RemoteSettingsClient}
*/
async function createTranslationModelsRemoteClient(
autoDownloadFromRemoteSettings,
langPairs
) {
const records = [];
for (const { fromLang, toLang, isBeta } of langPairs) {
const lang = fromLang + toLang;
const models = [
{ fileType: "model", name: `model.${lang}.intgemm.alphas.bin` },
{ fileType: "lex", name: `lex.50.50.${lang}.s2t.bin` },
{ fileType: "vocab", name: `vocab.${lang}.spm` },
];
if (models.length !== FILES_PER_LANGUAGE_PAIR) {
throw new Error("Files per language pair was wrong.");
}
for (const { fileType, name } of models) {
records.push({
id: crypto.randomUUID(),
name,
fromLang,
toLang,
fileType,
version: isBeta ? "0.1" : "1.0",
last_modified: Date.now(),
schema: Date.now(),
});
}
}
const { RemoteSettings } = ChromeUtils.importESModule(
"resource://services-settings/remote-settings.sys.mjs"
);
const client = RemoteSettings("test-translation-models");
const metadata = {};
await client.db.clear();
await client.db.importChanges(metadata, Date.now(), records);
return createAttachmentMock(client, autoDownloadFromRemoteSettings);
}
/**
* Creates a local RemoteSettingsClient for use within tests.
*
* @param {boolean} autoDownloadFromRemoteSettings
* @returns {RemoteSettingsClient}
*/
async function createTranslationsWasmRemoteClient(
autoDownloadFromRemoteSettings
) {
const records = ["bergamot-translator", "fasttext-wasm"].map(name => ({
id: crypto.randomUUID(),
name,
version: "1.0",
last_modified: Date.now(),
schema: Date.now(),
}));
const { RemoteSettings } = ChromeUtils.importESModule(
"resource://services-settings/remote-settings.sys.mjs"
);
const client = RemoteSettings("test-translations-wasm");
const metadata = {};
await client.db.clear();
await client.db.importChanges(metadata, Date.now(), records);
return createAttachmentMock(client, autoDownloadFromRemoteSettings);
}
/**
* Creates a local RemoteSettingsClient for use within tests.
*
* @param {boolean} autoDownloadFromRemoteSettings
* @returns {RemoteSettingsClient}
*/
async function createLanguageIdModelsRemoteClient(
autoDownloadFromRemoteSettings
) {
const records = [
{
id: crypto.randomUUID(),
name: "lid.176.ftz",
version: "1.0",
last_modified: Date.now(),
schema: Date.now(),
},
];
const { RemoteSettings } = ChromeUtils.importESModule(
"resource://services-settings/remote-settings.sys.mjs"
);
const client = RemoteSettings("test-language-id-models");
const metadata = {};
await client.db.clear();
await client.db.importChanges(metadata, Date.now(), records);
return createAttachmentMock(client, autoDownloadFromRemoteSettings);
}
async function selectAboutPreferencesElements() {
const document = gBrowser.selectedBrowser.contentDocument;
const rows = await TestUtils.waitForCondition(() => {
const elements = document.querySelectorAll(".translations-manage-language");
if (elements.length !== 3) {
return false;
}
return elements;
}, "Waiting for manage language rows.");
const [downloadAllRow, frenchRow, spanishRow] = rows;
const downloadAllLabel = downloadAllRow.querySelector("label");
const downloadAll = downloadAllRow.querySelector(
"#translations-manage-install-all"
);
const deleteAll = downloadAllRow.querySelector(
"#translations-manage-delete-all"
);
const frenchLabel = frenchRow.querySelector("label");
const frenchDownload = frenchRow.querySelector(
`[data-l10n-id="translations-manage-download-button"]`
);
const frenchDelete = frenchRow.querySelector(
`[data-l10n-id="translations-manage-delete-button"]`
);
const spanishLabel = spanishRow.querySelector("label");
const spanishDownload = spanishRow.querySelector(
`[data-l10n-id="translations-manage-download-button"]`
);
const spanishDelete = spanishRow.querySelector(
`[data-l10n-id="translations-manage-delete-button"]`
);
return {
document,
downloadAllLabel,
downloadAll,
deleteAll,
frenchLabel,
frenchDownload,
frenchDelete,
spanishLabel,
spanishDownload,
spanishDelete,
};
}
function click(button, message) {
info(message);
if (button.hidden) {
throw new Error("The button was hidden when trying to click it.");
}
button.click();
}
/**
* @param {Object} options
* @param {string} options.message
* @param {Record} options.visible
* @param {Record} options.hidden
*/
async function assertVisibility({ message, visible, hidden }) {
info(message);
try {
// First wait for the condition to be met.
await TestUtils.waitForCondition(() => {
for (const element of Object.values(visible)) {
if (element.hidden) {
return false;
}
}
for (const element of Object.values(hidden)) {
if (!element.hidden) {
return false;
}
}
return true;
});
} catch (error) {
// Ignore, this will get caught below.
}
// Now report the conditions.
for (const [name, element] of Object.entries(visible)) {
ok(!element.hidden, `${name} is visible.`);
}
for (const [name, element] of Object.entries(hidden)) {
ok(element.hidden, `${name} is hidden.`);
}
}
async function setupAboutPreferences(languagePairs) {
await SpecialPowers.pushPrefEnv({
set: [
// Enabled by default.
["browser.translations.enable", true],
["browser.translations.logLevel", "All"],
],
});
const tab = await BrowserTestUtils.openNewForegroundTab(
gBrowser,
BLANK_PAGE,
true // waitForLoad
);
const { remoteClients, removeMocks } = await createAndMockRemoteSettings({
languagePairs,
});
BrowserTestUtils.loadURIString(tab.linkedBrowser, "about:preferences");
await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
const elements = await selectAboutPreferencesElements();
async function cleanup() {
gBrowser.removeCurrentTab();
removeMocks();
await SpecialPowers.popPrefEnv();
}
return {
cleanup,
remoteClients,
elements,
};
}
function waitForAppLocaleChanged() {
new Promise(resolve => {
function onChange() {
Services.obs.removeObserver(onChange, "intl:app-locales-changed");
resolve();
}
Services.obs.addObserver(onChange, "intl:app-locales-changed");
});
}
async function mockLocales({ systemLocales, appLocales, webLanguages }) {
const appLocaleChanged1 = waitForAppLocaleChanged();
TranslationsParent.mockedSystemLocales = systemLocales;
const { availableLocales, requestedLocales } = Services.locale;
info("Mocking locales, so expect potential .ftl resource errors.");
Services.locale.availableLocales = appLocales;
Services.locale.requestedLocales = appLocales;
await appLocaleChanged1;
await SpecialPowers.pushPrefEnv({
set: [["intl.accept_languages", webLanguages.join(",")]],
});
return async () => {
const appLocaleChanged2 = waitForAppLocaleChanged();
// Reset back to the originals.
TranslationsParent.mockedSystemLocales = null;
Services.locale.availableLocales = availableLocales;
Services.locale.requestedLocales = requestedLocales;
await appLocaleChanged2;
await SpecialPowers.popPrefEnv();
};
}
/**
* Helpful test functions for translations telemetry
*/
class TestTranslationsTelemetry {
/**
* Asserts qualities about a counter telemetry metric.
*
* @param {string} name - The name of the metric.
* @param {Object} counter - The Glean counter object.
* @param {Object} expectedCount - The expected value of the counter.
*/
static async assertCounter(name, counter, expectedCount) {
// Ensures that glean metrics are collected from all child processes
// so that calls to testGetValue() are up to date.
await Services.fog.testFlushAllChildren();
const count = counter.testGetValue() ?? 0;
is(
count,
expectedCount,
`Telemetry counter ${name} should have expected count`
);
}
/**
* Asserts qualities about an event telemetry metric.
*
* @param {string} name - The name of the metric.
* @param {Object} event - The Glean event object.
* @param {Object} expectations - The test expectations.
* @param {number} expectations.expectedLength - The expected length of the event.
* @param {Array} [expectations.allValuePredicates=[]]
* - An array of function predicates to assert for all event values.
* @param {Array} [expectations.finalValuePredicates=[]]
* - An array of function predicates to assert for only the final event value.
*/
static async assertEvent(
name,
event,
{ expectedLength, allValuePredicates = [], finalValuePredicates = [] }
) {
// Ensures that glean metrics are collected from all child processes
// so that calls to testGetValue() are up to date.
await Services.fog.testFlushAllChildren();
const values = event.testGetValue() ?? [];
const length = values.length;
is(
length,
expectedLength,
`Telemetry event ${name} should have length ${expectedLength}`
);
if (allValuePredicates.length !== 0) {
is(
length > 0,
true,
`Telemetry event ${name} should contain values if allPredicates are specified`
);
for (const value of values) {
for (const predicate of allValuePredicates) {
is(
predicate(value),
true,
`Telemetry event ${name} allPredicate { ${predicate.toString()} } should pass for each value`
);
}
}
}
if (finalValuePredicates.length !== 0) {
is(
length > 0,
true,
`Telemetry event ${name} should contain values if finalPredicates are specified`
);
for (const predicate of finalValuePredicates) {
is(
predicate(values[length - 1]),
true,
`Telemetry event ${name} finalPredicate { ${predicate.toString()} } should pass for final value`
);
}
}
}
/**
* Asserts qualities about a rate telemetry metric.
*
* @param {string} name - The name of the metric.
* @param {Object} rate - The Glean rate object.
* @param {Object} expectations - The test expectations.
* @param {number} expectations.expectedNumerator - The expected value of the numerator.
* @param {number} expectations.expectedDenominator - The expected value of the denominator.
*/
static async assertRate(
name,
rate,
{ expectedNumerator, expectedDenominator }
) {
// Ensures that glean metrics are collected from all child processes
// so that calls to testGetValue() are up to date.
await Services.fog.testFlushAllChildren();
const { numerator = 0, denominator = 0 } = rate.testGetValue() ?? {};
is(
numerator,
expectedNumerator,
`Telemetry rate ${name} should have expected numerator`
);
is(
denominator,
expectedDenominator,
`Telemetry rate ${name} should have expected denominator`
);
}
}