diff --git a/browser/base/content/browser.js b/browser/base/content/browser.js index 91810c437423..0d330ad23eb9 100644 --- a/browser/base/content/browser.js +++ b/browser/base/content/browser.js @@ -76,7 +76,7 @@ XPCOMUtils.defineLazyModuleGetters(this, { TabModalPrompt: "chrome://global/content/tabprompts.jsm", TabCrashHandler: "resource:///modules/ContentCrashHandlers.jsm", TelemetryEnvironment: "resource://gre/modules/TelemetryEnvironment.jsm", - Translation: "resource:///modules/translation/Translation.jsm", + Translation: "resource:///modules/translation/TranslationParent.jsm", UITour: "resource:///modules/UITour.jsm", UpdateUtils: "resource://gre/modules/UpdateUtils.jsm", UrlbarInput: "resource:///modules/UrlbarInput.jsm", @@ -1805,7 +1805,6 @@ var gBrowserInit = { // loading the frame script to ensure that we don't miss any // message sent between when the frame script is loaded and when // the listener is registered. - LanguageDetectionListener.init(); CaptivePortalWatcher.init(); ZoomUI.init(window); @@ -7394,17 +7393,6 @@ var gPageStyleMenu = { }, }; -var LanguageDetectionListener = { - init() { - window.messageManager.addMessageListener( - "Translation:DocumentState", - msg => { - Translation.documentStateReceived(msg.target, msg.data); - } - ); - }, -}; - // Note that this is also called from non-browser windows on OSX, which do // share menu items but not much else. See nonbrowser-mac.js. var BrowserOffline = { diff --git a/browser/base/content/tab-content.js b/browser/base/content/tab-content.js index 61d6c9d47abb..30cfa891c1fb 100644 --- a/browser/base/content/tab-content.js +++ b/browser/base/content/tab-content.js @@ -29,15 +29,6 @@ ActorManagerChild.attach(this, "browsers"); // BrowserChildGlobal var global = this; -// Keep a reference to the translation content handler to avoid it it being GC'ed. -var trHandler = null; -if (Services.prefs.getBoolPref("browser.translation.detectLanguage")) { - var { TranslationContentHandler } = ChromeUtils.import( - "resource:///modules/translation/TranslationContentHandler.jsm" - ); - trHandler = new TranslationContentHandler(global, docShell); -} - var WebBrowserChrome = { onBeforeLinkTraversal(originalTarget, linkURI, linkNode, isAppTab) { return BrowserUtils.onBeforeLinkTraversal( diff --git a/browser/components/BrowserGlue.jsm b/browser/components/BrowserGlue.jsm index cbcde48f7c6f..ff84e8020c6a 100644 --- a/browser/components/BrowserGlue.jsm +++ b/browser/components/BrowserGlue.jsm @@ -403,6 +403,20 @@ let ACTORS = { allFrames: true, }, + Translation: { + parent: { + moduleURI: "resource:///modules/translation/TranslationParent.jsm", + }, + child: { + moduleURI: "resource:///modules/translation/TranslationChild.jsm", + events: { + pageshow: {}, + load: { mozSystemGroup: true }, + }, + }, + enablePreference: "browser.translation.detectLanguage", + }, + UITour: { parent: { moduleURI: "resource:///modules/UITourParent.jsm", diff --git a/browser/components/preferences/in-content/main.js b/browser/components/preferences/in-content/main.js index ec7beb837c7f..b0b2a36086dd 100644 --- a/browser/components/preferences/in-content/main.js +++ b/browser/components/preferences/in-content/main.js @@ -539,7 +539,7 @@ var gMainPane = { row.removeAttribute("hidden"); // Showing attribution only for Bing Translator. var { Translation } = ChromeUtils.import( - "resource:///modules/translation/Translation.jsm" + "resource:///modules/translation/TranslationParent.jsm" ); if (Translation.translationEngine == "Bing") { document.getElementById("bingAttribution").removeAttribute("hidden"); @@ -1379,7 +1379,7 @@ var gMainPane = { openTranslationProviderAttribution() { var { Translation } = ChromeUtils.import( - "resource:///modules/translation/Translation.jsm" + "resource:///modules/translation/TranslationParent.jsm" ); Translation.openProviderAttribution(); }, diff --git a/browser/components/translation/TranslationChild.jsm b/browser/components/translation/TranslationChild.jsm new file mode 100644 index 000000000000..301d0d76db39 --- /dev/null +++ b/browser/components/translation/TranslationChild.jsm @@ -0,0 +1,165 @@ +/* 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"; + +var EXPORTED_SYMBOLS = ["TranslationChild"]; + +const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); +ChromeUtils.defineModuleGetter( + this, + "LanguageDetector", + "resource:///modules/translation/LanguageDetector.jsm" +); + +const STATE_OFFER = 0; +const STATE_TRANSLATED = 2; +const STATE_ERROR = 3; + +class TranslationChild extends JSWindowActorChild { + handleEvent(aEvent) { + switch (aEvent.type) { + case "pageshow": + // We are only listening to pageshow events. + let content = this.contentWindow; + if (!content.detectedLanguage) { + return; + } + + let data = {}; + let trDoc = content.translationDocument; + if (trDoc) { + data.state = trDoc.translationError ? STATE_ERROR : STATE_TRANSLATED; + data.translatedFrom = trDoc.translatedFrom; + data.translatedTo = trDoc.translatedTo; + data.originalShown = trDoc.originalShown; + } else { + data.state = STATE_OFFER; + data.originalShown = true; + } + data.detectedLanguage = content.detectedLanguage; + + this.sendAsyncMessage("Translation:DocumentState", data); + break; + + case "load": + this.checkForTranslation(); + break; + } + } + + checkForTranslation() { + let url = String(this.document.location); + if (!url.startsWith("http://") && !url.startsWith("https://")) { + return; + } + + let content = this.contentWindow; + if (content.detectedLanguage) { + return; + } + + // Grab a 60k sample of text from the page. + let encoder = Cu.createDocumentEncoder("text/plain"); + encoder.init(this.document, "text/plain", encoder.SkipInvisibleContent); + let string = encoder.encodeToStringWithMaxLength(60 * 1024); + + // Language detection isn't reliable on very short strings. + if (string.length < 100) { + return; + } + + LanguageDetector.detectLanguage(string).then(result => { + // Bail if we're not confident. + if (!result.confident) { + return; + } + + // The window might be gone by now. + if (Cu.isDeadWrapper(content)) { + return; + } + + content.detectedLanguage = result.language; + + let data = { + state: STATE_OFFER, + originalShown: true, + detectedLanguage: result.language, + }; + this.sendAsyncMessage("Translation:DocumentState", data); + }); + } + + async doTranslation(aFrom, aTo) { + var { TranslationDocument } = ChromeUtils.import( + "resource:///modules/translation/TranslationDocument.jsm" + ); + + // If a TranslationDocument already exists for this document, it should + // be used instead of creating a new one so that we can use the original + // content of the page for the new translation instead of the newly + // translated text. + let translationDocument = + this.contentWindow.translationDocument || + new TranslationDocument(this.document); + + let engine = Services.prefs.getCharPref("browser.translation.engine"); + let importScope = ChromeUtils.import( + `resource:///modules/translation/${engine}Translator.jsm`, + {} + ); + let translator = new importScope[engine + "Translator"]( + translationDocument, + aFrom, + aTo + ); + + this.contentWindow.translationDocument = translationDocument; + translationDocument.translatedFrom = aFrom; + translationDocument.translatedTo = aTo; + translationDocument.translationError = false; + + let result; + try { + let translateResult = await translator.translate(); + + result = { + characterCount: translateResult.characterCount, + from: aFrom, + to: aTo, + success: true, + }; + + translationDocument.showTranslation(); + return result; + } catch (ex) { + translationDocument.translationError = true; + result = { success: false }; + if (ex == "unavailable") { + result.unavailable = true; + } + } + + return result; + } + + receiveMessage(aMessage) { + switch (aMessage.name) { + case "Translation:TranslateDocument": { + return this.doTranslation(aMessage.data.from, aMessage.data.to); + } + + case "Translation:ShowOriginal": + this.contentWindow.translationDocument.showOriginal(); + break; + + case "Translation:ShowTranslation": + this.contentWindow.translationDocument.showTranslation(); + break; + } + + return undefined; + } +} diff --git a/browser/components/translation/TranslationContentHandler.jsm b/browser/components/translation/TranslationContentHandler.jsm deleted file mode 100644 index a5734ccbb381..000000000000 --- a/browser/components/translation/TranslationContentHandler.jsm +++ /dev/null @@ -1,193 +0,0 @@ -/* 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"; - -var EXPORTED_SYMBOLS = ["TranslationContentHandler"]; - -const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); -ChromeUtils.defineModuleGetter( - this, - "LanguageDetector", - "resource:///modules/translation/LanguageDetector.jsm" -); - -const STATE_OFFER = 0; -const STATE_TRANSLATED = 2; -const STATE_ERROR = 3; - -var TranslationContentHandler = function(global, docShell) { - let webProgress = docShell - .QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIWebProgress); - webProgress.addProgressListener( - this, - Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT - ); - - global.addEventListener("pageshow", this); - - global.addMessageListener("Translation:TranslateDocument", this); - global.addMessageListener("Translation:ShowTranslation", this); - global.addMessageListener("Translation:ShowOriginal", this); - this.global = global; -}; - -TranslationContentHandler.prototype = { - handleEvent(aEvent) { - // We are only listening to pageshow events. - let target = aEvent.target; - - // Only handle top-level frames. - let win = target.defaultView; - if (win.parent !== win) { - return; - } - - let content = this.global.content; - if (!content.detectedLanguage) { - return; - } - - let data = {}; - let trDoc = content.translationDocument; - if (trDoc) { - data.state = trDoc.translationError ? STATE_ERROR : STATE_TRANSLATED; - data.translatedFrom = trDoc.translatedFrom; - data.translatedTo = trDoc.translatedTo; - data.originalShown = trDoc.originalShown; - } else { - data.state = STATE_OFFER; - data.originalShown = true; - } - data.detectedLanguage = content.detectedLanguage; - - this.global.sendAsyncMessage("Translation:DocumentState", data); - }, - - /* nsIWebProgressListener implementation */ - onStateChange(aWebProgress, aRequest, aStateFlags, aStatus) { - if ( - !aWebProgress.isTopLevel || - !(aStateFlags & Ci.nsIWebProgressListener.STATE_STOP) || - !this.global.content - ) { - return; - } - - try { - let url = aRequest.name; - if (!url.startsWith("http://") && !url.startsWith("https://")) { - return; - } - } catch (e) { - // nsIRequest.name throws NS_ERROR_NOT_IMPLEMENTED for view-source: tabs. - return; - } - - let content = this.global.content; - if (content.detectedLanguage) { - return; - } - - // Grab a 60k sample of text from the page. - let encoder = Cu.createDocumentEncoder("text/plain"); - encoder.init(content.document, "text/plain", encoder.SkipInvisibleContent); - let string = encoder.encodeToStringWithMaxLength(60 * 1024); - - // Language detection isn't reliable on very short strings. - if (string.length < 100) { - return; - } - - LanguageDetector.detectLanguage(string).then(result => { - // Bail if we're not confident. - if (!result.confident) { - return; - } - - // The window might be gone by now. - if (Cu.isDeadWrapper(content)) { - return; - } - - content.detectedLanguage = result.language; - - let data = { - state: STATE_OFFER, - originalShown: true, - detectedLanguage: result.language, - }; - this.global.sendAsyncMessage("Translation:DocumentState", data); - }); - }, - - QueryInterface: ChromeUtils.generateQI([ - Ci.nsIWebProgressListener, - Ci.nsISupportsWeakReference, - ]), - - receiveMessage(msg) { - switch (msg.name) { - case "Translation:TranslateDocument": { - var { TranslationDocument } = ChromeUtils.import( - "resource:///modules/translation/TranslationDocument.jsm" - ); - - // If a TranslationDocument already exists for this document, it should - // be used instead of creating a new one so that we can use the original - // content of the page for the new translation instead of the newly - // translated text. - let translationDocument = - this.global.content.translationDocument || - new TranslationDocument(this.global.content.document); - - let engine = Services.prefs.getCharPref("browser.translation.engine"); - let importScope = ChromeUtils.import( - `resource:///modules/translation/${engine}Translator.jsm`, - {} - ); - let translator = new importScope[engine + "Translator"]( - translationDocument, - msg.data.from, - msg.data.to - ); - - this.global.content.translationDocument = translationDocument; - translationDocument.translatedFrom = msg.data.from; - translationDocument.translatedTo = msg.data.to; - translationDocument.translationError = false; - - translator.translate().then( - result => { - this.global.sendAsyncMessage("Translation:Finished", { - characterCount: result.characterCount, - from: msg.data.from, - to: msg.data.to, - success: true, - }); - translationDocument.showTranslation(); - }, - error => { - translationDocument.translationError = true; - let data = { success: false }; - if (error == "unavailable") { - data.unavailable = true; - } - this.global.sendAsyncMessage("Translation:Finished", data); - } - ); - break; - } - - case "Translation:ShowOriginal": - this.global.content.translationDocument.showOriginal(); - break; - - case "Translation:ShowTranslation": - this.global.content.translationDocument.showTranslation(); - break; - } - }, -}; diff --git a/browser/components/translation/Translation.jsm b/browser/components/translation/TranslationParent.jsm similarity index 82% rename from browser/components/translation/Translation.jsm rename to browser/components/translation/TranslationParent.jsm index 447782e51a13..cf8e310a66f6 100644 --- a/browser/components/translation/Translation.jsm +++ b/browser/components/translation/TranslationParent.jsm @@ -4,7 +4,11 @@ "use strict"; -var EXPORTED_SYMBOLS = ["Translation", "TranslationTelemetry"]; +var EXPORTED_SYMBOLS = [ + "Translation", + "TranslationParent", + "TranslationTelemetry", +]; const TRANSLATION_PREF_SHOWUI = "browser.translation.ui.show"; const TRANSLATION_PREF_DETECT_LANG = "browser.translation.detectLanguage"; @@ -18,6 +22,8 @@ var Translation = { STATE_ERROR: 3, STATE_UNAVAILABLE: 4, + translationListener: null, + serviceUnavailable: false, supportedSourceLanguages: [ @@ -57,6 +63,10 @@ var Translation = { "zh", ], + setListenerForTests(listener) { + this.translationListener = listener; + }, + _defaultTargetLanguage: "", get defaultTargetLanguage() { if (!this._defaultTargetLanguage) { @@ -67,49 +77,6 @@ var Translation = { return this._defaultTargetLanguage; }, - documentStateReceived(aBrowser, aData) { - if (aData.state == this.STATE_OFFER) { - if (aData.detectedLanguage == this.defaultTargetLanguage) { - // Detected language is the same as the user's locale. - return; - } - - if (!this.supportedSourceLanguages.includes(aData.detectedLanguage)) { - // Detected language is not part of the supported languages. - TranslationTelemetry.recordMissedTranslationOpportunity( - aData.detectedLanguage - ); - return; - } - - TranslationTelemetry.recordTranslationOpportunity(aData.detectedLanguage); - } - - if (!Services.prefs.getBoolPref(TRANSLATION_PREF_SHOWUI)) { - return; - } - - if (!aBrowser.translationUI) { - aBrowser.translationUI = new TranslationUI(aBrowser); - } - let trUI = aBrowser.translationUI; - - // Set all values before showing a new translation infobar. - trUI._state = Translation.serviceUnavailable - ? Translation.STATE_UNAVAILABLE - : aData.state; - trUI.detectedLanguage = aData.detectedLanguage; - trUI.translatedFrom = aData.translatedFrom; - trUI.translatedTo = aData.translatedTo; - trUI.originalShown = aData.originalShown; - - trUI.showURLBarIcon(); - - if (trUI.shouldShowInfoBar(aBrowser.contentPrincipal)) { - trUI.showTranslationInfoBar(); - } - }, - openProviderAttribution() { let attribution = this.supportedEngines[this.translationEngine]; const { BrowserWindowTracker } = ChromeUtils.import( @@ -145,10 +112,8 @@ var Translation = { }, }; -/* TranslationUI objects keep the information related to translation for - * a specific browser. This object is passed to the translation - * infobar so that it can initialize itself. The properties exposed to - * the infobar are: +/* Translation objects keep the information related to translation for + * a specific browser. The properties exposed to the infobar are: * - detectedLanguage, code of the language detected on the web page. * - state, the state in which the infobar should be displayed * - translatedFrom, if already translated, source language code. @@ -160,24 +125,65 @@ var Translation = { * - originalShown, boolean indicating if the original or translated * version of the page is shown. */ -function TranslationUI(aBrowser) { - this.browser = aBrowser; -} +class TranslationParent extends JSWindowActorParent { + actorCreated() { + this._state = 0; + this.originalShown = true; + } -TranslationUI.prototype = { get browser() { - return this._browser; - }, - set browser(aBrowser) { - if (this._browser) { - this._browser.messageManager.removeMessageListener( - "Translation:Finished", - this - ); + let browser = this.browsingContext.top.embedderElement; + return browser.outerBrowser ? browser.outerBrowser : browser; + } + + receiveMessage(aMessage) { + switch (aMessage.name) { + case "Translation:DocumentState": + this.documentStateReceived(aMessage.data); + break; } - aBrowser.messageManager.addMessageListener("Translation:Finished", this); - this._browser = aBrowser; - }, + } + + documentStateReceived(aData) { + if (aData.state == Translation.STATE_OFFER) { + if (aData.detectedLanguage == Translation.defaultTargetLanguage) { + // Detected language is the same as the user's locale. + return; + } + + if ( + !Translation.supportedTargetLanguages.includes(aData.detectedLanguage) + ) { + // Detected language is not part of the supported languages. + TranslationTelemetry.recordMissedTranslationOpportunity( + aData.detectedLanguage + ); + return; + } + + TranslationTelemetry.recordTranslationOpportunity(aData.detectedLanguage); + } + + if (!Services.prefs.getBoolPref(TRANSLATION_PREF_SHOWUI)) { + return; + } + + // Set all values before showing a new translation infobar. + this._state = Translation.serviceUnavailable + ? Translation.STATE_UNAVAILABLE + : aData.state; + this.detectedLanguage = aData.detectedLanguage; + this.translatedFrom = aData.translatedFrom; + this.translatedTo = aData.translatedTo; + this.originalShown = aData.originalShown; + + this.showURLBarIcon(); + + if (this.shouldShowInfoBar(this.browser.contentPrincipal)) { + this.showTranslationInfoBar(); + } + } + translate(aFrom, aTo) { if ( aFrom == aTo || @@ -206,11 +212,16 @@ TranslationUI.prototype = { this.translatedFrom = aFrom; this.translatedTo = aTo; - this.browser.messageManager.sendAsyncMessage( - "Translation:TranslateDocument", - { from: aFrom, to: aTo } + this.sendQuery("Translation:TranslateDocument", { + from: aFrom, + to: aTo, + }).then( + result => { + this.translationFinished(result); + }, + () => {} ); - }, + } showURLBarIcon() { let chromeWin = this.browser.ownerGlobal; @@ -229,8 +240,6 @@ TranslationUI.prototype = { let infoBarVisible = this.notificationBox.getNotificationWithValue( "translation" ); - aNewBrowser.translationUI = this; - this.browser = aNewBrowser; if (infoBarVisible) { this.showTranslationInfoBar(); } @@ -261,37 +270,36 @@ TranslationUI.prototype = { null, { dismissed: true, eventCallback: callback } ); - }, + } - _state: 0, get state() { return this._state; - }, + } + set state(val) { let notif = this.notificationBox.getNotificationWithValue("translation"); if (notif) { notif.state = val; } this._state = val; - }, + } - originalShown: true, showOriginalContent() { this.originalShown = true; this.showURLBarIcon(); - this.browser.messageManager.sendAsyncMessage("Translation:ShowOriginal"); + this.sendAsyncMessage("Translation:ShowOriginal"); TranslationTelemetry.recordShowOriginalContent(); - }, + } showTranslatedContent() { this.originalShown = false; this.showURLBarIcon(); - this.browser.messageManager.sendAsyncMessage("Translation:ShowTranslation"); - }, + this.sendAsyncMessage("Translation:ShowTranslation"); + } get notificationBox() { return this.browser.ownerGlobal.gBrowser.getNotificationBox(this.browser); - }, + } showTranslationInfoBar() { let notificationBox = this.notificationBox; @@ -306,7 +314,7 @@ TranslationUI.prototype = { ); notif.init(this); return notif; - }, + } shouldShowInfoBar(aPrincipal) { // Never show the infobar automatically while the translation @@ -335,38 +343,38 @@ TranslationUI.prototype = { } return true; - }, + } - receiveMessage(msg) { - switch (msg.name) { - case "Translation:Finished": - if (msg.data.success) { - this.originalShown = false; - this.state = Translation.STATE_TRANSLATED; - this.showURLBarIcon(); + translationFinished(result) { + if (result.success) { + this.originalShown = false; + this.state = Translation.STATE_TRANSLATED; + this.showURLBarIcon(); - // Record the number of characters translated. - TranslationTelemetry.recordTranslation( - msg.data.from, - msg.data.to, - msg.data.characterCount - ); - } else if (msg.data.unavailable) { - Translation.serviceUnavailable = true; - this.state = Translation.STATE_UNAVAILABLE; - } else { - this.state = Translation.STATE_ERROR; - } - break; + // Record the number of characters translated. + TranslationTelemetry.recordTranslation( + result.from, + result.to, + result.characterCount + ); + } else if (result.unavailable) { + Translation.serviceUnavailable = true; + this.state = Translation.STATE_UNAVAILABLE; + } else { + this.state = Translation.STATE_ERROR; } - }, + + if (Translation.translationListener) { + Translation.translationListener(); + } + } infobarClosed() { if (this.state == Translation.STATE_OFFER) { TranslationTelemetry.recordDeniedTranslationOffer(); } - }, -}; + } +} /** * Uses telemetry histograms for collecting statistics on the usage of the diff --git a/browser/components/translation/content/translation-notification.js b/browser/components/translation/content/translation-notification.js index a5ed209a5fd4..31d6002dc7a3 100644 --- a/browser/components/translation/content/translation-notification.js +++ b/browser/components/translation/content/translation-notification.js @@ -130,6 +130,7 @@ class MozTranslationNotification extends MozElements.Notification { return this._getAnonElt("translationStates").selectedIndex; } + // aTranslation is the TranslationParent actor. init(aTranslation) { this.translation = aTranslation; diff --git a/browser/components/translation/moz.build b/browser/components/translation/moz.build index 11c83b695ce0..5d4c61eb15c9 100644 --- a/browser/components/translation/moz.build +++ b/browser/components/translation/moz.build @@ -15,9 +15,9 @@ EXTRA_JS_MODULES.translation = [ 'cld2/cld-worker.js.mem', 'GoogleTranslator.jsm', 'LanguageDetector.jsm', - 'Translation.jsm', - 'TranslationContentHandler.jsm', + 'TranslationChild.jsm', 'TranslationDocument.jsm', + 'TranslationParent.jsm', 'YandexTranslator.jsm' ] diff --git a/browser/components/translation/test/browser_translation_exceptions.js b/browser/components/translation/test/browser_translation_exceptions.js index a7758cb384cf..956973c1fc6f 100644 --- a/browser/components/translation/test/browser_translation_exceptions.js +++ b/browser/components/translation/test/browser_translation_exceptions.js @@ -5,7 +5,10 @@ // tests the translation infobar, using a fake 'Translation' implementation. var tmp = {}; -ChromeUtils.import("resource:///modules/translation/Translation.jsm", tmp); +ChromeUtils.import( + "resource:///modules/translation/TranslationParent.jsm", + tmp +); const { PermissionTestUtils } = ChromeUtils.import( "resource://testing-common/PermissionTestUtils.jsm" ); @@ -13,16 +16,19 @@ var { Translation } = tmp; 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() { @@ -126,17 +132,20 @@ var gTests = [ desc: "never for language", run: async function checkNeverForLanguage() { // Show the infobar for example.com and fr. - Translation.documentStateReceived(gBrowser.selectedBrowser, { + let actor = gBrowser.selectedBrowser.browsingContext.currentWindowGlobal.getActor( + "Translation" + ); + actor.documentStateReceived({ state: Translation.STATE_OFFER, originalShown: true, detectedLanguage: "fr", }); let notif = await getInfoBar(); ok(notif, "the infobar is visible"); - let ui = gBrowser.selectedBrowser.translationUI; + let principal = gBrowser.selectedBrowser.contentPrincipal; ok( - ui.shouldShowInfoBar(principal, "fr"), + actor.shouldShowInfoBar(principal, "fr"), "check shouldShowInfoBar initially returns true" ); @@ -163,7 +172,7 @@ var gTests = [ is(langs.length, 1, "one language in the exception list"); is(langs[0], "fr", "correct language in the exception list"); ok( - !ui.shouldShowInfoBar(principal, "fr"), + !actor.shouldShowInfoBar(principal, "fr"), "the infobar wouldn't be shown anymore" ); @@ -187,17 +196,19 @@ var gTests = [ desc: "never for site", run: async function checkNeverForSite() { // Show the infobar for example.com and fr. - Translation.documentStateReceived(gBrowser.selectedBrowser, { + let actor = gBrowser.selectedBrowser.browsingContext.currentWindowGlobal.getActor( + "Translation" + ); + actor.documentStateReceived({ state: Translation.STATE_OFFER, originalShown: true, detectedLanguage: "fr", }); let notif = await getInfoBar(); ok(notif, "the infobar is visible"); - let ui = gBrowser.selectedBrowser.translationUI; let principal = gBrowser.selectedBrowser.contentPrincipal; ok( - ui.shouldShowInfoBar(principal, "fr"), + actor.shouldShowInfoBar(principal, "fr"), "check shouldShowInfoBar initially returns true" ); @@ -228,7 +239,7 @@ var gTests = [ "correct site in the exception list" ); ok( - !ui.shouldShowInfoBar(principal, "fr"), + !actor.shouldShowInfoBar(principal, "fr"), "the infobar wouldn't be shown anymore" ); diff --git a/browser/components/translation/test/browser_translation_infobar.js b/browser/components/translation/test/browser_translation_infobar.js index 61bd4081eee0..f050ca97677a 100644 --- a/browser/components/translation/test/browser_translation_infobar.js +++ b/browser/components/translation/test/browser_translation_infobar.js @@ -5,8 +5,11 @@ // tests the translation infobar, using a fake 'Translation' implementation. var tmp = {}; -ChromeUtils.import("resource:///modules/translation/Translation.jsm", tmp); -var { Translation } = tmp; +ChromeUtils.import( + "resource:///modules/translation/TranslationParent.jsm", + tmp +); +var { Translation, TranslationParent } = tmp; const kShowUIPref = "browser.translation.ui.show"; @@ -35,47 +38,55 @@ function waitForCondition(condition, nextTest, errorMsg) { }; } -var TranslationStub = { +// Create a subclass that overrides the translation functions. This can be +// instantiated separately from the normal actor creation process. This will +// allow testing translations even when the browser.translation.detectLanguage +// preference is disabled. +class TranslationStub extends TranslationParent { + constructor(browser) { + super(); + this._browser = browser; + this.actorCreated(); + } + + get browser() { + return this._browser; + } + + sendAsyncMessage(name, data) {} + translate(aFrom, aTo) { this.state = Translation.STATE_TRANSLATING; this.translatedFrom = aFrom; this.translatedTo = aTo; - }, + } _reset() { this.translatedFrom = ""; this.translatedTo = ""; - }, + } failTranslation() { this.state = Translation.STATE_ERROR; this._reset(); - }, + } finishTranslation() { this.showTranslatedContent(); this.state = Translation.STATE_TRANSLATED; this._reset(); - }, -}; + } +} function showTranslationUI(aDetectedLanguage) { let browser = gBrowser.selectedBrowser; - Translation.documentStateReceived(browser, { + let translation = new TranslationStub(browser); + translation.documentStateReceived({ state: Translation.STATE_OFFER, originalShown: true, detectedLanguage: aDetectedLanguage, }); - let ui = browser.translationUI; - for (let name of [ - "translate", - "_reset", - "failTranslation", - "finishTranslation", - ]) { - ui[name] = TranslationStub[name]; - } - return ui.notificationBox.getNotificationWithValue("translation"); + return translation.notificationBox.getNotificationWithValue("translation"); } function hasTranslationInfoBar() { diff --git a/browser/components/translation/test/browser_translation_telemetry.js b/browser/components/translation/test/browser_translation_telemetry.js index 532f12f6083b..20d271bf8776 100644 --- a/browser/components/translation/test/browser_translation_telemetry.js +++ b/browser/components/translation/test/browser_translation_telemetry.js @@ -4,7 +4,10 @@ "use strict"; var tmp = {}; -ChromeUtils.import("resource:///modules/translation/Translation.jsm", tmp); +ChromeUtils.import( + "resource:///modules/translation/TranslationParent.jsm", + tmp +); var { Translation, TranslationTelemetry } = tmp; const Telemetry = Services.telemetry; @@ -108,9 +111,10 @@ var MetricsChecker = { }; function getInfobarElement(browser, anonid) { - let notif = browser.translationUI.notificationBox.getNotificationWithValue( - "translation" + let actor = browser.browsingContext.currentWindowGlobal.getActor( + "Translation" ); + let notif = actor.notificationBox.getNotificationWithValue("translation"); return notif._getAnonElt(anonid); } @@ -120,9 +124,12 @@ var offerTranslationFor = async function(text, from) { let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, dataUrl); let browser = gBrowser.getBrowserForTab(tab); + let actor = browser.browsingContext.currentWindowGlobal.getActor( + "Translation" + ); // Send a translation offer. - Translation.documentStateReceived(browser, { + actor.documentStateReceived({ state: Translation.STATE_OFFER, originalShown: true, detectedLanguage: from, @@ -133,8 +140,10 @@ var offerTranslationFor = async function(text, from) { var acceptTranslationOffer = async function(tab) { let browser = tab.linkedBrowser; + let translationPromise = waitForTranslationDone(); + getInfobarElement(browser, "translate").doCommand(); - await waitForMessage(browser, "Translation:Finished"); + await translationPromise; }; var translate = async function(text, from, closeTab = true) { @@ -147,10 +156,10 @@ var translate = async function(text, from, closeTab = true) { return tab; }; -function waitForMessage({ messageManager }, name) { +function waitForTranslationDone() { return new Promise(resolve => { - messageManager.addMessageListener(name, function onMessage() { - messageManager.removeMessageListener(name, onMessage); + Translation.setListenerForTests(() => { + Translation.setListenerForTests(null); resolve(); }); }); diff --git a/browser/components/translation/test/browser_translation_yandex.js b/browser/components/translation/test/browser_translation_yandex.js index df85f70a47cb..50700bae9cc8 100644 --- a/browser/components/translation/test/browser_translation_yandex.js +++ b/browser/components/translation/test/browser_translation_yandex.js @@ -19,7 +19,7 @@ const kApiKeyPref = "browser.translation.yandex.apiKeyOverride"; const kShowUIPref = "browser.translation.ui.show"; const { Translation } = ChromeUtils.import( - "resource:///modules/translation/Translation.jsm" + "resource:///modules/translation/TranslationParent.jsm" ); add_task(async function setup() { @@ -140,11 +140,14 @@ function promiseTestPageLoad(url) { function showTranslationUI(tab, aDetectedLanguage) { let browser = gBrowser.selectedBrowser; - Translation.documentStateReceived(browser, { + let actor = browser.browsingContext.currentWindowGlobal.getActor( + "Translation" + ); + actor.documentStateReceived({ state: Translation.STATE_OFFER, originalShown: true, detectedLanguage: aDetectedLanguage, }); - let ui = browser.translationUI; - return ui.notificationBox.getNotificationWithValue("translation"); + + return actor.notificationBox.getNotificationWithValue("translation"); }