diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
commit | 36d22d82aa202bb199967e9512281e9a53db42c9 (patch) | |
tree | 105e8c98ddea1c1e4784a60a5a6410fa416be2de /browser/components/translation/test | |
parent | Initial commit. (diff) | |
download | firefox-esr-upstream.tar.xz firefox-esr-upstream.zip |
Adding upstream version 115.7.0esr.upstream/115.7.0esrupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
10 files changed, 1428 insertions, 0 deletions
diff --git a/browser/components/translation/test/bing.sjs b/browser/components/translation/test/bing.sjs new file mode 100644 index 0000000000..3769517e36 --- /dev/null +++ b/browser/components/translation/test/bing.sjs @@ -0,0 +1,250 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const CC = Components.Constructor; +const BinaryInputStream = CC( + "@mozilla.org/binaryinputstream;1", + "nsIBinaryInputStream", + "setInputStream" +); +Cu.importGlobalProperties(["DOMParser", "TextEncoder"]); + +function handleRequest(req, res) { + try { + reallyHandleRequest(req, res); + } catch (ex) { + res.setStatusLine("1.0", 200, "AlmostOK"); + let msg = "Error handling request: " + ex + "\n" + ex.stack; + log(msg); + res.write(msg); + } +} + +function log(msg) { + // dump("BING-SERVER-MOCK: " + msg + "\n"); +} + +const statusCodes = { + 400: "Bad Request", + 401: "Unauthorized", + 403: "Forbidden", + 404: "Not Found", + 405: "Method Not Allowed", + 500: "Internal Server Error", + 501: "Not Implemented", + 503: "Service Unavailable", +}; + +function HTTPError(code = 500, message) { + this.code = code; + this.name = statusCodes[code] || "HTTPError"; + this.message = message || this.name; +} +HTTPError.prototype = new Error(); +HTTPError.prototype.constructor = HTTPError; + +function sendError(res, err) { + if (!(err instanceof HTTPError)) { + err = new HTTPError( + typeof err == "number" ? err : 500, + err.message || typeof err == "string" ? err : "" + ); + } + res.setStatusLine("1.1", err.code, err.name); + res.write(err.message); +} + +function parseQuery(query) { + let ret = {}; + for (let param of query.replace(/^[?&]/, "").split("&")) { + param = param.split("="); + if (!param[0]) { + continue; + } + ret[unescape(param[0])] = unescape(param[1]); + } + return ret; +} + +function getRequestBody(req) { + let avail; + let bytes = []; + let body = new BinaryInputStream(req.bodyInputStream); + + while ((avail = body.available()) > 0) { + Array.prototype.push.apply(bytes, body.readByteArray(avail)); + } + + return String.fromCharCode.apply(null, bytes); +} + +function sha1(str) { + // `data` is an array of bytes. + let data = new TextEncoder().encode(str); + let ch = Cc["@mozilla.org/security/hash;1"].createInstance(Ci.nsICryptoHash); + ch.init(ch.SHA1); + ch.update(data, data.length); + let hash = ch.finish(false); + + // Return the two-digit hexadecimal code for a byte. + function toHexString(charCode) { + return ("0" + charCode.toString(16)).slice(-2); + } + + // Convert the binary hash data to a hex string. + return Array.from(hash, (c, i) => toHexString(hash.charCodeAt(i))).join(""); +} + +function parseXml(body) { + let parser = new DOMParser(); + let xml = parser.parseFromString(body, "text/xml"); + if (xml.documentElement.localName == "parsererror") { + throw new Error("Invalid XML"); + } + return xml; +} + +function getInputStream(path) { + let file = Services.dirsvc.get("CurWorkD", Ci.nsIFile); + for (let part of path.split("/")) { + file.append(part); + } + let fileStream = Cc[ + "@mozilla.org/network/file-input-stream;1" + ].createInstance(Ci.nsIFileInputStream); + fileStream.init(file, 1, 0, false); + return fileStream; +} + +function checkAuth(req) { + let err = new Error("Authorization failed"); + err.code = 401; + + if (!req.hasHeader("Authorization")) { + throw new HTTPError(401, "No Authorization header provided."); + } + + let auth = req.getHeader("Authorization"); + if (!auth.startsWith("Bearer ")) { + throw new HTTPError( + 401, + "Invalid Authorization header content: '" + auth + "'" + ); + } + + // Rejecting inactive subscriptions. + if (auth.includes("inactive")) { + const INACTIVE_STATE_RESPONSE = + "<html><body><h1>TranslateApiException</h1><p>Method: TranslateArray()</p><p>Message: The Azure Market Place Translator Subscription associated with the request credentials is not in an active state.</p><code></code><p>message id=5641.V2_Rest.TranslateArray.48CC6470</p></body></html>"; + throw new HTTPError(401, INACTIVE_STATE_RESPONSE); + } +} + +function reallyHandleRequest(req, res) { + log("method: " + req.method); + if (req.method != "POST") { + sendError( + res, + "Bing only deals with POST requests, not '" + req.method + "'." + ); + return; + } + + let body = getRequestBody(req); + log("body: " + body); + + // First, we'll see if we're dealing with an XML body: + let contentType = req.hasHeader("Content-Type") + ? req.getHeader("Content-Type") + : null; + log("contentType: " + contentType); + + if (contentType.startsWith("text/xml")) { + try { + // For all these requests the client needs to supply the correct + // authentication headers. + checkAuth(req); + + let xml = parseXml(body); + let method = xml.documentElement.localName; + log("invoking method: " + method); + // If the requested method is supported, delegate it to its handler. + if (methodHandlers[method]) { + methodHandlers[method](res, xml); + } else { + throw new HTTPError(501); + } + } catch (ex) { + sendError(res, ex, ex.code); + } + } else { + // Not XML, so it must be a query-string. + let params = parseQuery(body); + + // Delegate an authentication request to the correct handler. + if ("grant_type" in params && params.grant_type == "client_credentials") { + methodHandlers.authenticate(res, params); + } else { + sendError(res, 501); + } + } +} + +const methodHandlers = { + authenticate(res, params) { + // Validate a few required parameters. + if (params.scope != "http://api.microsofttranslator.com") { + sendError(res, "Invalid scope."); + return; + } + if (!params.client_id) { + sendError(res, "Missing client_id param."); + return; + } + if (!params.client_secret) { + sendError(res, "Missing client_secret param."); + return; + } + + // Defines the tokens for certain client ids. + const TOKEN_MAP = { + testInactive: "inactive", + testClient: "test", + }; + let token = "test"; // Default token. + if (params.client_id in TOKEN_MAP) { + token = TOKEN_MAP[params.client_id]; + } + let content = JSON.stringify({ + access_token: token, + expires_in: 600, + }); + + res.setStatusLine("1.1", 200, "OK"); + res.setHeader("Content-Length", String(content.length)); + res.setHeader("Content-Type", "application/json"); + res.write(content); + }, + + TranslateArrayRequest(res, xml, body) { + let from = xml.querySelector("From").firstChild.nodeValue; + let to = xml.querySelector("To").firstChild.nodeValue; + log("translating from '" + from + "' to '" + to + "'"); + + res.setStatusLine("1.1", 200, "OK"); + res.setHeader("Content-Type", "text/xml"); + + let hash = sha1(body).substr(0, 10); + log("SHA1 hash of content: " + hash); + let inputStream = getInputStream( + "browser/browser/components/translation/test/fixtures/result-" + + hash + + ".txt" + ); + res.bodyOutputStream.writeFrom(inputStream, inputStream.available()); + inputStream.close(); + }, +}; diff --git a/browser/components/translation/test/browser.ini b/browser/components/translation/test/browser.ini new file mode 100644 index 0000000000..2c09e9992a --- /dev/null +++ b/browser/components/translation/test/browser.ini @@ -0,0 +1,13 @@ +[DEFAULT] +support-files = + bing.sjs + yandex.sjs + fixtures/bug1022725-fr.html + fixtures/result-da39a3ee5e.txt + fixtures/result-yandex-d448894848.json + +[browser_translation_bing.js] +[browser_translation_exceptions.js] +https_first_disabled = true +[browser_translation_yandex.js] +[browser_translations_settings.js] diff --git a/browser/components/translation/test/browser_translation_bing.js b/browser/components/translation/test/browser_translation_bing.js new file mode 100644 index 0000000000..8b4620f059 --- /dev/null +++ b/browser/components/translation/test/browser_translation_bing.js @@ -0,0 +1,160 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Test the Bing Translator client against a mock Bing service, bing.sjs. + +"use strict"; + +const kClientIdPref = "browser.translation.bing.clientIdOverride"; +const kClientSecretPref = "browser.translation.bing.apiKeyOverride"; + +const { BingTranslator } = ChromeUtils.import( + "resource:///modules/translation/BingTranslator.jsm" +); +const { TranslationDocument } = ChromeUtils.import( + "resource:///modules/translation/TranslationDocument.jsm" +); + +add_setup(async function () { + Services.prefs.setCharPref(kClientIdPref, "testClient"); + Services.prefs.setCharPref(kClientSecretPref, "testSecret"); + + registerCleanupFunction(function () { + Services.prefs.clearUserPref(kClientIdPref); + Services.prefs.clearUserPref(kClientSecretPref); + }); +}); + +/** + * Checks if the translation is happening. + */ +add_task(async function test_bing_translation() { + // Ensure the correct client id is used for authentication. + Services.prefs.setCharPref(kClientIdPref, "testClient"); + + // Loading the fixture page. + let url = constructFixtureURL("bug1022725-fr.html"); + let tab = await promiseTestPageLoad(url); + + // Translating the contents of the loaded tab. + gBrowser.selectedTab = tab; + let browser = tab.linkedBrowser; + + await SpecialPowers.spawn(browser, [], async function () { + // eslint-disable-next-line no-shadow + const { BingTranslator } = ChromeUtils.import( + "resource:///modules/translation/BingTranslator.jsm" + ); + // eslint-disable-next-line no-shadow + const { TranslationDocument } = ChromeUtils.import( + "resource:///modules/translation/TranslationDocument.jsm" + ); + + let client = new BingTranslator( + new TranslationDocument(content.document), + "fr", + "en" + ); + let result = await client.translate(); + + // XXXmikedeboer; here you would continue the test/ content inspection. + Assert.ok(result, "There should be a result"); + }); + + gBrowser.removeTab(tab); +}); + +/** + * Ensures that the BingTranslator handles out-of-valid-key response + * correctly. Sometimes Bing Translate replies with + * "request credentials is not in an active state" error. BingTranslator + * should catch this error and classify it as Service Unavailable. + * + */ +add_task(async function test_handling_out_of_valid_key_error() { + // Simulating request from inactive subscription. + Services.prefs.setCharPref(kClientIdPref, "testInactive"); + + // Loading the fixture page. + let url = constructFixtureURL("bug1022725-fr.html"); + let tab = await promiseTestPageLoad(url); + + // Translating the contents of the loaded tab. + gBrowser.selectedTab = tab; + let browser = tab.linkedBrowser; + + await SpecialPowers.spawn(browser, [], async function () { + // eslint-disable-next-line no-shadow + const { BingTranslator } = ChromeUtils.import( + "resource:///modules/translation/BingTranslator.jsm" + ); + // eslint-disable-next-line no-shadow + const { TranslationDocument } = ChromeUtils.import( + "resource:///modules/translation/TranslationDocument.jsm" + ); + + let client = new BingTranslator( + new TranslationDocument(content.document), + "fr", + "en" + ); + client._resetToken(); + try { + await client.translate(); + } catch (ex) { + // It is alright that the translation fails. + } + client._resetToken(); + + // Checking if the client detected service and unavailable. + Assert.ok( + client._serviceUnavailable, + "Service should be detected unavailable." + ); + }); + + // Cleaning up. + Services.prefs.setCharPref(kClientIdPref, "testClient"); + gBrowser.removeTab(tab); +}); + +/** + * A helper function for constructing a URL to a page stored in the + * local fixture folder. + * + * @param filename Name of a fixture file. + */ +function constructFixtureURL(filename) { + // Deduce the Mochitest server address in use from a pref that was pre-processed. + let server = Services.prefs + .getCharPref("browser.translation.bing.authURL") + .replace("http://", ""); + server = server.substr(0, server.indexOf("/")); + let url = + "http://" + + server + + "/browser/browser/components/translation/test/fixtures/" + + filename; + return url; +} + +/** + * A helper function to open a new tab and wait for its content to load. + * + * @param String url A URL to be loaded in the new tab. + */ +function promiseTestPageLoad(url) { + return new Promise(resolve => { + let tab = (gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, url)); + let browser = gBrowser.selectedBrowser; + BrowserTestUtils.browserLoaded( + browser, + false, + loadurl => loadurl != "about:blank" + ).then(() => { + info("Page loaded: " + browser.currentURI.spec); + resolve(tab); + }); + }); +} diff --git a/browser/components/translation/test/browser_translation_exceptions.js b/browser/components/translation/test/browser_translation_exceptions.js new file mode 100644 index 0000000000..c0d325c6f7 --- /dev/null +++ b/browser/components/translation/test/browser_translation_exceptions.js @@ -0,0 +1,239 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// TODO (Bug 1817084) Remove this file when we disable the extension +// tests the translation infobar, using a fake 'Translation' implementation. + +const { PermissionTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/PermissionTestUtils.sys.mjs" +); + +const kLanguagesPref = "browser.translation.neverForLanguages"; +const kShowUIPref = "browser.translation.ui.show"; +const kEnableTranslationPref = "browser.translation.detectLanguage"; + +function test() { + waitForExplicitFinish(); + + Services.prefs.setBoolPref(kShowUIPref, true); + Services.prefs.setBoolPref(kEnableTranslationPref, true); + let tab = BrowserTestUtils.addTab(gBrowser); + gBrowser.selectedTab = tab; + registerCleanupFunction(function () { + gBrowser.removeTab(tab); + Services.prefs.clearUserPref(kShowUIPref); + Services.prefs.clearUserPref(kEnableTranslationPref); + }); + BrowserTestUtils.browserLoaded(tab.linkedBrowser).then(() => { + (async function () { + for (let testCase of gTests) { + info(testCase.desc); + await testCase.run(); + } + })().then(finish, ex => { + ok(false, "Unexpected Exception: " + ex); + finish(); + }); + }); + + BrowserTestUtils.loadURIString( + gBrowser.selectedBrowser, + "http://example.com/" + ); +} + +function getLanguageExceptions() { + let langs = Services.prefs.getCharPref(kLanguagesPref); + return langs ? langs.split(",") : []; +} + +function getDomainExceptions() { + let results = []; + for (let perm of Services.perms.all) { + if ( + perm.type == "translate" && + perm.capability == Services.perms.DENY_ACTION + ) { + results.push(perm.principal); + } + } + + return results; +} + +function openPopup(aPopup) { + return new Promise(resolve => { + aPopup.addEventListener( + "popupshown", + function () { + TestUtils.executeSoon(resolve); + }, + { once: true } + ); + + aPopup.focus(); + // One down event to open the popup. + EventUtils.synthesizeKey("VK_DOWN", { + altKey: !navigator.platform.includes("Mac"), + }); + }); +} + +function waitForWindowLoad(aWin) { + return new Promise(resolve => { + aWin.addEventListener( + "load", + function () { + TestUtils.executeSoon(resolve); + }, + { capture: true, once: true } + ); + }); +} + +var gTests = [ + { + desc: "clean exception lists at startup", + run: function checkNeverForLanguage() { + is( + getLanguageExceptions().length, + 0, + "we start with an empty list of languages to never translate" + ); + is( + getDomainExceptions().length, + 0, + "we start with an empty list of sites to never translate" + ); + }, + }, + + { + desc: "language exception list", + run: async function checkLanguageExceptions() { + // Put 2 languages in the pref before opening the window to check + // the list is displayed on load. + Services.prefs.setCharPref(kLanguagesPref, "fr,de"); + + // Open the translation exceptions dialog. + let win = openDialog( + "chrome://browser/content/preferences/dialogs/translationExceptions.xhtml", + "Browser:TranslationExceptions", + "", + null + ); + await waitForWindowLoad(win); + + // Check that the list of language exceptions is loaded. + let getById = win.document.getElementById.bind(win.document); + let tree = getById("languagesTree"); + let remove = getById("removeLanguage"); + let removeAll = getById("removeAllLanguages"); + is(tree.view.rowCount, 2, "The language exceptions list has 2 items"); + ok(remove.disabled, "The 'Remove Language' button is disabled"); + ok(!removeAll.disabled, "The 'Remove All Languages' button is enabled"); + + // Select the first item. + tree.view.selection.select(0); + ok(!remove.disabled, "The 'Remove Language' button is enabled"); + + // Click the 'Remove' button. + remove.click(); + is(tree.view.rowCount, 1, "The language exceptions now contains 1 item"); + is(getLanguageExceptions().length, 1, "One exception in the pref"); + + // Clear the pref, and check the last item is removed from the display. + Services.prefs.setCharPref(kLanguagesPref, ""); + is(tree.view.rowCount, 0, "The language exceptions list is empty"); + ok(remove.disabled, "The 'Remove Language' button is disabled"); + ok(removeAll.disabled, "The 'Remove All Languages' button is disabled"); + + // Add an item and check it appears. + Services.prefs.setCharPref(kLanguagesPref, "fr"); + is(tree.view.rowCount, 1, "The language exceptions list has 1 item"); + ok(remove.disabled, "The 'Remove Language' button is disabled"); + ok(!removeAll.disabled, "The 'Remove All Languages' button is enabled"); + + // Click the 'Remove All' button. + removeAll.click(); + is(tree.view.rowCount, 0, "The language exceptions list is empty"); + ok(remove.disabled, "The 'Remove Language' button is disabled"); + ok(removeAll.disabled, "The 'Remove All Languages' button is disabled"); + is(Services.prefs.getCharPref(kLanguagesPref), "", "The pref is empty"); + + win.close(); + }, + }, + + { + desc: "domains exception list", + run: async function checkDomainExceptions() { + // Put 2 exceptions before opening the window to check the list is + // displayed on load. + PermissionTestUtils.add( + "http://example.org", + "translate", + Services.perms.DENY_ACTION + ); + PermissionTestUtils.add( + "http://example.com", + "translate", + Services.perms.DENY_ACTION + ); + + // Open the translation exceptions dialog. + let win = openDialog( + "chrome://browser/content/preferences/dialogs/translationExceptions.xhtml", + "Browser:TranslationExceptions", + "", + null + ); + await waitForWindowLoad(win); + + // Check that the list of language exceptions is loaded. + let getById = win.document.getElementById.bind(win.document); + let tree = getById("sitesTree"); + let remove = getById("removeSite"); + let removeAll = getById("removeAllSites"); + is(tree.view.rowCount, 2, "The sites exceptions list has 2 items"); + ok(remove.disabled, "The 'Remove Site' button is disabled"); + ok(!removeAll.disabled, "The 'Remove All Sites' button is enabled"); + + // Select the first item. + tree.view.selection.select(0); + ok(!remove.disabled, "The 'Remove Site' button is enabled"); + + // Click the 'Remove' button. + remove.click(); + is(tree.view.rowCount, 1, "The site exceptions now contains 1 item"); + is(getDomainExceptions().length, 1, "One exception in the permissions"); + + // Clear the permissions, and check the last item is removed from the display. + PermissionTestUtils.remove("http://example.org", "translate"); + PermissionTestUtils.remove("http://example.com", "translate"); + is(tree.view.rowCount, 0, "The site exceptions list is empty"); + ok(remove.disabled, "The 'Remove Site' button is disabled"); + ok(removeAll.disabled, "The 'Remove All Site' button is disabled"); + + // Add an item and check it appears. + PermissionTestUtils.add( + "http://example.com", + "translate", + Services.perms.DENY_ACTION + ); + is(tree.view.rowCount, 1, "The site exceptions list has 1 item"); + ok(remove.disabled, "The 'Remove Site' button is disabled"); + ok(!removeAll.disabled, "The 'Remove All Sites' button is enabled"); + + // Click the 'Remove All' button. + removeAll.click(); + is(tree.view.rowCount, 0, "The site exceptions list is empty"); + ok(remove.disabled, "The 'Remove Site' button is disabled"); + ok(removeAll.disabled, "The 'Remove All Sites' button is disabled"); + is(getDomainExceptions().length, 0, "No exceptions in the permissions"); + + win.close(); + }, + }, +]; diff --git a/browser/components/translation/test/browser_translation_yandex.js b/browser/components/translation/test/browser_translation_yandex.js new file mode 100644 index 0000000000..c6c97571de --- /dev/null +++ b/browser/components/translation/test/browser_translation_yandex.js @@ -0,0 +1,134 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Test the Yandex Translator client against a mock Yandex service, yandex.sjs. + +"use strict"; + +// The folllowing rejection is left unhandled in some cases. This bug should be +// fixed, but for the moment this file allows a class of rejections. +// +// NOTE: Allowing a whole class of rejections should be avoided. Normally you +// should use "expectUncaughtRejection" to flag individual failures. +const { PromiseTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/PromiseTestUtils.sys.mjs" +); +PromiseTestUtils.allowMatchingRejectionsGlobally(/NS_ERROR_ILLEGAL_VALUE/); + +const kEnginePref = "browser.translation.engine"; +const kApiKeyPref = "browser.translation.yandex.apiKeyOverride"; +const kDetectLanguagePref = "browser.translation.detectLanguage"; +const kShowUIPref = "browser.translation.ui.show"; + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + [kEnginePref, "Yandex"], + [kApiKeyPref, "yandexValidKey"], + [kDetectLanguagePref, true], + [kShowUIPref, true], + ], + }); +}); + +/** + * Ensure that the translation engine behaives as expected when translating + * a sample page. + */ +add_task(async function test_yandex_translation() { + // Loading the fixture page. + let url = constructFixtureURL("bug1022725-fr.html"); + let tab = await promiseTestPageLoad(url); + + // Translating the contents of the loaded tab. + gBrowser.selectedTab = tab; + let browser = tab.linkedBrowser; + + await SpecialPowers.spawn(browser, [], async function () { + const { TranslationDocument } = ChromeUtils.import( + "resource:///modules/translation/TranslationDocument.jsm" + ); + const { YandexTranslator } = ChromeUtils.import( + "resource:///modules/translation/YandexTranslator.jsm" + ); + + let client = new YandexTranslator( + new TranslationDocument(content.document), + "fr", + "en" + ); + let result = await client.translate(); + + Assert.ok(result, "There should be a result."); + }); + + gBrowser.removeTab(tab); +}); + +add_task(async function test_preference_attribution() { + let prefUrl = "about:preferences#general"; + let waitPrefLoaded = TestUtils.topicObserved("sync-pane-loaded", () => true); + let tab = await promiseTestPageLoad(prefUrl); + await waitPrefLoaded; + let browser = gBrowser.getBrowserForTab(tab); + let win = browser.contentWindow; + let bingAttribution = win.document.getElementById("bingAttribution"); + ok(bingAttribution, "Bing attribution should exist."); + ok(bingAttribution.hidden, "Bing attribution should be hidden."); + + gBrowser.removeTab(tab); +}); + +/** + * A helper function for constructing a URL to a page stored in the + * local fixture folder. + * + * @param filename Name of a fixture file. + */ +function constructFixtureURL(filename) { + // Deduce the Mochitest server address in use from a pref that was pre-processed. + let server = Services.prefs + .getCharPref("browser.translation.yandex.translateURLOverride") + .replace("http://", ""); + server = server.substr(0, server.indexOf("/")); + let url = + "http://" + + server + + "/browser/browser/components/translation/test/fixtures/" + + filename; + return url; +} + +/** + * A helper function to open a new tab and wait for its content to load. + * + * @param String url A URL to be loaded in the new tab. + */ +function promiseTestPageLoad(url) { + return new Promise(resolve => { + let tab = (gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, url)); + let browser = gBrowser.selectedBrowser; + BrowserTestUtils.browserLoaded( + browser, + false, + loadurl => loadurl != "about:blank" + ).then(() => { + info("Page loaded: " + browser.currentURI.spec); + resolve(tab); + }); + }); +} + +function showTranslationUI(tab, aDetectedLanguage) { + let browser = gBrowser.selectedBrowser; + let actor = + browser.browsingContext.currentWindowGlobal.getActor("Translation"); + actor.documentStateReceived({ + state: Translation.STATE_OFFER, + originalShown: true, + detectedLanguage: aDetectedLanguage, + }); + + return actor.notificationBox.getNotificationWithValue("translation"); +} diff --git a/browser/components/translation/test/browser_translations_settings.js b/browser/components/translation/test/browser_translations_settings.js new file mode 100644 index 0000000000..87860ad9bf --- /dev/null +++ b/browser/components/translation/test/browser_translations_settings.js @@ -0,0 +1,384 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// tests the translation infobar, using a fake 'Translation' implementation. + +const { PermissionTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/PermissionTestUtils.sys.mjs" +); + +const TRANSLATIONS_PERMISSION = "translations"; +const ENABLE_TRANSLATIONS_PREF = "browser.translations.enable"; +const ALWAYS_TRANSLATE_LANGS_PREF = + "browser.translations.alwaysTranslateLanguages"; +const NEVER_TRANSLATE_LANGS_PREF = + "browser.translations.neverTranslateLanguages"; + +function test() { + waitForExplicitFinish(); + Services.prefs.setBoolPref(ENABLE_TRANSLATIONS_PREF, true); + Services.prefs.setCharPref(ALWAYS_TRANSLATE_LANGS_PREF, ""); + Services.prefs.setCharPref(NEVER_TRANSLATE_LANGS_PREF, ""); + let tab = BrowserTestUtils.addTab(gBrowser); + gBrowser.selectedTab = tab; + registerCleanupFunction(function () { + gBrowser.removeTab(tab); + Services.prefs.clearUserPref(ENABLE_TRANSLATIONS_PREF); + Services.prefs.clearUserPref(ALWAYS_TRANSLATE_LANGS_PREF); + Services.prefs.clearUserPref(NEVER_TRANSLATE_LANGS_PREF); + }); + BrowserTestUtils.browserLoaded(tab.linkedBrowser).then(() => { + (async function () { + for (let testCase of gTests) { + info(testCase.desc); + await testCase.run(); + } + })().then(finish, ex => { + ok(false, "Unexpected Exception: " + ex); + finish(); + }); + }); + + BrowserTestUtils.loadURIString( + gBrowser.selectedBrowser, + "https://example.com/" + ); +} + +/** + * Retrieves the always-translate language list as an array. + * @returns {Array<string>} + */ +function getAlwaysTranslateLanguages() { + let langs = Services.prefs.getCharPref(ALWAYS_TRANSLATE_LANGS_PREF); + return langs ? langs.split(",") : []; +} + +/** + * Retrieves the always-translate language list as an array. + * @returns {Array<string>} + */ +function getNeverTranslateLanguages() { + let langs = Services.prefs.getCharPref(NEVER_TRANSLATE_LANGS_PREF); + return langs ? langs.split(",") : []; +} + +/** + * Retrieves the always-translate site list as an array. + * @returns {Array<string>} + */ +function getNeverTranslateSites() { + let results = []; + for (let perm of Services.perms.all) { + if ( + perm.type == TRANSLATIONS_PERMISSION && + perm.capability == Services.perms.DENY_ACTION + ) { + results.push(perm.principal); + } + } + + return results; +} + +function openPopup(aPopup) { + return new Promise(resolve => { + aPopup.addEventListener( + "popupshown", + function () { + TestUtils.executeSoon(resolve); + }, + { once: true } + ); + + aPopup.focus(); + // One down event to open the popup. + EventUtils.synthesizeKey("VK_DOWN"); + }); +} + +function waitForWindowLoad(aWin) { + return new Promise(resolve => { + aWin.addEventListener( + "load", + function () { + TestUtils.executeSoon(resolve); + }, + { capture: true, once: true } + ); + }); +} + +var gTests = [ + { + desc: "ensure lists are empty on startup", + run: function checkPreferencesAreEmpty() { + is( + getAlwaysTranslateLanguages().length, + 0, + "we start with an empty list of languages to always translate" + ); + is( + getNeverTranslateLanguages().length, + 0, + "we start with an empty list of languages to never translate" + ); + is( + getNeverTranslateSites().length, + 0, + "we start with an empty list of sites to never translate" + ); + }, + }, + + { + desc: "ensure always-translate languages function correctly", + run: async function testAlwaysTranslateLanguages() { + // Put 2 languages in the pref before opening the window to check + // the list is displayed on load. + Services.prefs.setCharPref(ALWAYS_TRANSLATE_LANGS_PREF, "fr,de"); + + // Open the translations settings dialog. + let win = openDialog( + "chrome://browser/content/preferences/dialogs/translations.xhtml", + "Browser:TranslationsPreferences", + "", + null + ); + await waitForWindowLoad(win); + + // Check that the list of always-translate languages is loaded. + let getById = win.document.getElementById.bind(win.document); + let tree = getById("alwaysTranslateLanguagesTree"); + let remove = getById("removeAlwaysTranslateLanguage"); + let removeAll = getById("removeAllAlwaysTranslateLanguages"); + is( + tree.view.rowCount, + 2, + "The always-translate languages list has 2 items" + ); + ok(remove.disabled, "The 'Remove Language' button is disabled"); + ok(!removeAll.disabled, "The 'Remove All Languages' button is enabled"); + + // Select the first item. + tree.view.selection.select(0); + ok(!remove.disabled, "The 'Remove Language' button is enabled"); + + // Click the 'Remove' button. + remove.click(); + is( + tree.view.rowCount, + 1, + "The always-translate languages list now contains 1 item" + ); + is( + getAlwaysTranslateLanguages().length, + 1, + "One language tag in the pref" + ); + + // Clear the pref, and check the last item is removed from the display. + Services.prefs.setCharPref(ALWAYS_TRANSLATE_LANGS_PREF, ""); + is(tree.view.rowCount, 0, "The always-translate languages list is empty"); + ok(remove.disabled, "The 'Remove Language' button is disabled"); + ok(removeAll.disabled, "The 'Remove All Languages' button is disabled"); + + // Add an item and check it appears. + Services.prefs.setCharPref(ALWAYS_TRANSLATE_LANGS_PREF, "fr,en,es"); + is( + tree.view.rowCount, + 3, + "The always-translate languages list has 3 items" + ); + ok(remove.disabled, "The 'Remove Language' button is disabled"); + ok(!removeAll.disabled, "The 'Remove All Languages' button is enabled"); + + // Click the 'Remove All' button. + removeAll.click(); + is(tree.view.rowCount, 0, "The always-translate languages list is empty"); + ok(remove.disabled, "The 'Remove Language' button is disabled"); + ok(removeAll.disabled, "The 'Remove All Languages' button is disabled"); + is( + Services.prefs.getCharPref(ALWAYS_TRANSLATE_LANGS_PREF), + "", + "The pref is empty" + ); + + win.close(); + }, + }, + + { + desc: "ensure never-translate languages function correctly", + run: async function testNeverTranslateLanguages() { + // Put 2 languages in the pref before opening the window to check + // the list is displayed on load. + Services.prefs.setCharPref(NEVER_TRANSLATE_LANGS_PREF, "fr,de"); + + // Open the translations settings dialog. + let win = openDialog( + "chrome://browser/content/preferences/dialogs/translations.xhtml", + "Browser:TranslationsPreferences", + "", + null + ); + await waitForWindowLoad(win); + + // Check that the list of never-translate languages is loaded. + let getById = win.document.getElementById.bind(win.document); + let tree = getById("neverTranslateLanguagesTree"); + let remove = getById("removeNeverTranslateLanguage"); + let removeAll = getById("removeAllNeverTranslateLanguages"); + is( + tree.view.rowCount, + 2, + "The never-translate languages list has 2 items" + ); + ok(remove.disabled, "The 'Remove Language' button is disabled"); + ok(!removeAll.disabled, "The 'Remove All Languages' button is enabled"); + + // Select the first item. + tree.view.selection.select(0); + ok(!remove.disabled, "The 'Remove Language' button is enabled"); + + // Click the 'Remove' button. + remove.click(); + is( + tree.view.rowCount, + 1, + "The never-translate language list now contains 1 item" + ); + is(getNeverTranslateLanguages().length, 1, "One langtag in the pref"); + + // Clear the pref, and check the last item is removed from the display. + Services.prefs.setCharPref(NEVER_TRANSLATE_LANGS_PREF, ""); + is(tree.view.rowCount, 0, "The never-translate languages list is empty"); + ok(remove.disabled, "The 'Remove Language' button is disabled"); + ok(removeAll.disabled, "The 'Remove All Languages' button is disabled"); + + // Add an item and check it appears. + Services.prefs.setCharPref(NEVER_TRANSLATE_LANGS_PREF, "fr,en,es"); + is( + tree.view.rowCount, + 3, + "The never-translate languages list has 3 items" + ); + ok(remove.disabled, "The 'Remove Language' button is disabled"); + ok(!removeAll.disabled, "The 'Remove All Languages' button is enabled"); + + // Click the 'Remove All' button. + removeAll.click(); + is(tree.view.rowCount, 0, "The never-translate languages list is empty"); + ok(remove.disabled, "The 'Remove Language' button is disabled"); + ok(removeAll.disabled, "The 'Remove All Languages' button is disabled"); + is( + Services.prefs.getCharPref(NEVER_TRANSLATE_LANGS_PREF), + "", + "The pref is empty" + ); + + win.close(); + }, + }, + + { + desc: "ensure never-translate sites function correctly", + run: async function testNeverTranslateSites() { + // Add two deny permissions before opening the window to + // check the list is displayed on load. + PermissionTestUtils.add( + "https://example.org", + TRANSLATIONS_PERMISSION, + Services.perms.DENY_ACTION + ); + PermissionTestUtils.add( + "https://example.com", + TRANSLATIONS_PERMISSION, + Services.perms.DENY_ACTION + ); + + // Open the translations settings dialog. + let win = openDialog( + "chrome://browser/content/preferences/dialogs/translations.xhtml", + "Browser:TranslationsPreferences", + "", + null + ); + await waitForWindowLoad(win); + + // Check that the list of never-translate sites is loaded. + let getById = win.document.getElementById.bind(win.document); + let tree = getById("neverTranslateSitesTree"); + let remove = getById("removeNeverTranslateSite"); + let removeAll = getById("removeAllNeverTranslateSites"); + is(tree.view.rowCount, 2, "The never-translate sites list has 2 items"); + ok(remove.disabled, "The 'Remove Site' button is disabled"); + ok(!removeAll.disabled, "The 'Remove All Sites' button is enabled"); + + // Select the first item. + tree.view.selection.select(0); + ok(!remove.disabled, "The 'Remove Site' button is enabled"); + + // Click the 'Remove' button. + remove.click(); + is( + tree.view.rowCount, + 1, + "The never-translate sites list now contains 1 item" + ); + is( + getNeverTranslateSites().length, + 1, + "One domain in the site permissions" + ); + + // Clear the permissions, and check the last item is removed from the display. + PermissionTestUtils.remove( + "https://example.org", + TRANSLATIONS_PERMISSION + ); + PermissionTestUtils.remove( + "https://example.com", + TRANSLATIONS_PERMISSION + ); + is(tree.view.rowCount, 0, "The never-translate sites list is empty"); + ok(remove.disabled, "The 'Remove Site' button is disabled"); + ok(removeAll.disabled, "The 'Remove All Site' button is disabled"); + + // Add items back and check that they appear + PermissionTestUtils.add( + "https://example.org", + TRANSLATIONS_PERMISSION, + Services.perms.DENY_ACTION + ); + PermissionTestUtils.add( + "https://example.com", + TRANSLATIONS_PERMISSION, + Services.perms.DENY_ACTION + ); + PermissionTestUtils.add( + "https://example.net", + TRANSLATIONS_PERMISSION, + Services.perms.DENY_ACTION + ); + + is(tree.view.rowCount, 3, "The never-translate sites list has 3 item"); + ok(remove.disabled, "The 'Remove Site' button is disabled"); + ok(!removeAll.disabled, "The 'Remove All Sites' button is enabled"); + + // Click the 'Remove All' button. + removeAll.click(); + is(tree.view.rowCount, 0, "The never-translate sites list is empty"); + ok(remove.disabled, "The 'Remove Site' button is disabled"); + ok(removeAll.disabled, "The 'Remove All Sites' button is disabled"); + is( + getNeverTranslateSites().length, + 0, + "No domains in the site permissions" + ); + + win.close(); + }, + }, +]; diff --git a/browser/components/translation/test/fixtures/bug1022725-fr.html b/browser/components/translation/test/fixtures/bug1022725-fr.html new file mode 100644 index 0000000000..f30edf52eb --- /dev/null +++ b/browser/components/translation/test/fixtures/bug1022725-fr.html @@ -0,0 +1,15 @@ +<!doctype html> +<html lang="fr"> + <head> + <!-- + - Text retrieved from http://fr.wikipedia.org/wiki/Coupe_du_monde_de_football_de_2014 + - at 06/13/2014, Creative Commons Attribution-ShareAlike License. + --> + <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> + <title>test</title> + </head> + <body> + <h1>Coupe du monde de football de 2014</h1> + <div>La Coupe du monde de football de 2014 est la 20e édition de la Coupe du monde de football, compétition organisée par la FIFA et qui réunit les trente-deux meilleures sélections nationales. Sa phase finale a lieu à l'été 2014 au Brésil. Avec le pays organisateur, toutes les équipes championnes du monde depuis 1930 (Uruguay, Italie, Allemagne, Angleterre, Argentine, France et Espagne) se sont qualifiées pour cette compétition. Elle est aussi la première compétition internationale de la Bosnie-Herzégovine.</div> + </body> +</html> diff --git a/browser/components/translation/test/fixtures/result-da39a3ee5e.txt b/browser/components/translation/test/fixtures/result-da39a3ee5e.txt new file mode 100644 index 0000000000..d2d14c7885 --- /dev/null +++ b/browser/components/translation/test/fixtures/result-da39a3ee5e.txt @@ -0,0 +1,22 @@ +<ArrayOfTranslateArrayResponse xmlns="http://schemas.datacontract.org/2004/07/Microsoft.MT.Web.Service.V2" xmlns:i="http://www.w3.org/2001/XMLSchema-instance"> + <TranslateArrayResponse> + <From>fr</From> + <OriginalTextSentenceLengths xmlns:a="http://schemas.microsoft.com/2003/10/Serialization/Arrays"> + <a:int>34</a:int> + </OriginalTextSentenceLengths> + <TranslatedText>Football's 2014 World Cup</TranslatedText> + <TranslatedTextSentenceLengths xmlns:a="http://schemas.microsoft.com/2003/10/Serialization/Arrays"> + <a:int>25</a:int> + </TranslatedTextSentenceLengths> + </TranslateArrayResponse> + <TranslateArrayResponse> + <From>fr</From> + <OriginalTextSentenceLengths xmlns:a="http://schemas.microsoft.com/2003/10/Serialization/Arrays"> + <a:int>508</a:int> + </OriginalTextSentenceLengths> + <TranslatedText>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus diam sem, porttitor eget neque sit amet, ultricies posuere metus. Cras placerat rutrum risus, nec dignissim magna dictum vitae. Fusce eleifend fermentum lacinia. Nulla sagittis cursus nibh. Praesent adipiscing, elit at pulvinar dapibus, neque massa tincidunt sapien, eu consectetur lectus metus sit amet odio. Proin blandit consequat porttitor. Pellentesque vehicula justo sed luctus vestibulum. Donec metus.</TranslatedText> + <TranslatedTextSentenceLengths xmlns:a="http://schemas.microsoft.com/2003/10/Serialization/Arrays"> + <a:int>475</a:int> + </TranslatedTextSentenceLengths> + </TranslateArrayResponse> +</ArrayOfTranslateArrayResponse> diff --git a/browser/components/translation/test/fixtures/result-yandex-d448894848.json b/browser/components/translation/test/fixtures/result-yandex-d448894848.json new file mode 100644 index 0000000000..9287611105 --- /dev/null +++ b/browser/components/translation/test/fixtures/result-yandex-d448894848.json @@ -0,0 +1,8 @@ +{ + "code": 200, + "lang": "fr-en", + "text": [ + "Football's 2014 World Cup", + "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus diam sem, porttitor eget neque sit amet, ultricies posuere metus. Cras placerat rutrum risus, nec dignissim magna dictum vitae. Fusce eleifend fermentum lacinia. Nulla sagittis cursus nibh. Praesent adipiscing, elit at pulvinar dapibus, neque massa tincidunt sapien, eu consectetur lectus metus sit amet odio. Proin blandit consequat porttitor. Pellentesque vehicula justo sed luctus vestibulum. Donec metus." + ] +} diff --git a/browser/components/translation/test/yandex.sjs b/browser/components/translation/test/yandex.sjs new file mode 100644 index 0000000000..24dd0b6e7c --- /dev/null +++ b/browser/components/translation/test/yandex.sjs @@ -0,0 +1,203 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const CC = Components.Constructor; +const BinaryInputStream = CC( + "@mozilla.org/binaryinputstream;1", + "nsIBinaryInputStream", + "setInputStream" +); +Cu.importGlobalProperties(["TextEncoder"]); + +function handleRequest(req, res) { + try { + reallyHandleRequest(req, res); + } catch (ex) { + res.setStatusLine("1.0", 200, "AlmostOK"); + let msg = "Error handling request: " + ex + "\n" + ex.stack; + log(msg); + res.write(msg); + } +} + +function log(msg) { + dump("YANDEX-SERVER-MOCK: " + msg + "\n"); +} + +const statusCodes = { + 400: "Bad Request", + 401: "Invalid API key", + 402: "This API key has been blocked", + 403: "Daily limit for requests reached", + 404: "Daily limit for chars reached", + 413: "The text size exceeds the maximum", + 422: "The text could not be translated", + 500: "Internal Server Error", + 501: "The specified translation direction is not supported", + 503: "Service Unavailable", +}; + +function HTTPError(code = 500, message) { + this.code = code; + this.name = statusCodes[code] || "HTTPError"; + this.message = message || this.name; +} +HTTPError.prototype = new Error(); +HTTPError.prototype.constructor = HTTPError; + +function sendError(res, err) { + if (!(err instanceof HTTPError)) { + err = new HTTPError( + typeof err == "number" ? err : 500, + err.message || typeof err == "string" ? err : "" + ); + } + res.setStatusLine("1.1", err.code, err.name); + res.write(err.message); +} + +// Based on the code borrowed from: +// http://stackoverflow.com/questions/901115/how-can-i-get-query-string-values-in-javascript +function parseQuery(query) { + let match, + params = {}, + pl = /\+/g, + search = /([^&=]+)=?([^&]*)/g, + decode = function (s) { + return decodeURIComponent(s.replace(pl, " ")); + }; + + while ((match = search.exec(query))) { + let k = decode(match[1]), + v = decode(match[2]); + if (k in params) { + if (params[k] instanceof Array) { + params[k].push(v); + } else { + params[k] = [params[k], v]; + } + } else { + params[k] = v; + } + } + + return params; +} + +function sha1(str) { + // `data` is an array of bytes. + let data = new TextEncoder().encode(str); + let ch = Cc["@mozilla.org/security/hash;1"].createInstance(Ci.nsICryptoHash); + ch.init(ch.SHA1); + ch.update(data, data.length); + let hash = ch.finish(false); + + // Return the two-digit hexadecimal code for a byte. + function toHexString(charCode) { + return ("0" + charCode.toString(16)).slice(-2); + } + + // Convert the binary hash data to a hex string. + return Array.from(hash, (c, i) => toHexString(hash.charCodeAt(i))).join(""); +} + +function getRequestBody(req) { + let avail; + let bytes = []; + let body = new BinaryInputStream(req.bodyInputStream); + + while ((avail = body.available()) > 0) { + Array.prototype.push.apply(bytes, body.readByteArray(avail)); + } + + return String.fromCharCode.apply(null, bytes); +} + +function getInputStream(path) { + let file = Services.dirsvc.get("CurWorkD", Ci.nsIFile); + for (let part of path.split("/")) { + file.append(part); + } + let fileStream = Cc[ + "@mozilla.org/network/file-input-stream;1" + ].createInstance(Ci.nsIFileInputStream); + fileStream.init(file, 1, 0, false); + return fileStream; +} + +/** + * Yandex Requests have to be signed with an API Key. This mock server + * supports the following keys: + * + * yandexValidKey Always passes the authentication, + * yandexInvalidKey Never passes authentication and fails with 401 code, + * yandexBlockedKey Never passes authentication and fails with 402 code, + * yandexOutOfRequestsKey Never passes authentication and fails with 403 code, + * yandexOutOfCharsKey Never passes authentication and fails with 404 code. + * + * If any other key is used the server reponds with 401 error code. + */ +function checkAuth(params) { + if (!("key" in params)) { + throw new HTTPError(400); + } + + let key = params.key; + if (key === "yandexValidKey") { + return true; + } + + let invalidKeys = { + yandexInvalidKey: 401, + yandexBlockedKey: 402, + yandexOutOfRequestsKey: 403, + yandexOutOfCharsKey: 404, + }; + + if (key in invalidKeys) { + throw new HTTPError(invalidKeys[key]); + } + + throw new HTTPError(401); +} + +function reallyHandleRequest(req, res) { + try { + // Preparing the query parameters. + let params = {}; + if (req.method == "POST") { + params = parseQuery(getRequestBody(req)); + } + + // Extracting the API key and attempting to authenticate the request. + log(JSON.stringify(params)); + + checkAuth(params); + methodHandlers.translate(res, params); + } catch (ex) { + sendError(res, ex, ex.code); + } +} + +const methodHandlers = { + translate(res, params) { + res.setStatusLine("1.1", 200, "OK"); + res.setHeader("Content-Type", "application/json"); + + let hash = sha1(JSON.stringify(params)).substr(0, 10); + log("SHA1 hash of content: " + hash); + + let fixture = + "browser/browser/components/translation/test/fixtures/result-yandex-" + + hash + + ".json"; + log("PATH: " + fixture); + + let inputStream = getInputStream(fixture); + res.bodyOutputStream.writeFrom(inputStream, inputStream.available()); + inputStream.close(); + }, +}; |