/* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ "use strict"; let contextMenu; const { sinon } = ChromeUtils.importESModule( "resource://testing-common/Sinon.sys.mjs" ); const example_base = // eslint-disable-next-line @microsoft/sdl/no-insecure-url "http://example.com/browser/browser/base/content/test/contextMenu/"; const MAIN_URL = example_base + "subtst_contextmenu_input.html"; add_task(async function test_setup() { await BrowserTestUtils.openNewForegroundTab(gBrowser, MAIN_URL); const chrome_base = "chrome://mochitests/content/browser/browser/base/content/test/contextMenu/"; const contextmenu_common = chrome_base + "contextmenu_common.js"; /* import-globals-from contextmenu_common.js */ Services.scriptloader.loadSubScript(contextmenu_common, this); }); add_task(async function test_text_input_spellcheck() { await test_contextmenu( "#input_spellcheck_no_value", [ "context-undo", false, "context-redo", false, "---", null, "context-cut", null, // ignore the enabled/disabled states; there are race conditions // in the edit commands but they're not relevant for what we're testing. "context-copy", null, "context-paste", null, // ignore clipboard state "context-delete", null, "context-selectall", null, "---", null, "spell-check-enabled", true, "spell-dictionaries", true, [ "spell-check-dictionary-en-US", true, "---", null, "spell-add-dictionaries", true, ], null, ], { waitForSpellCheck: true, async preCheckContextMenuFn() { await SpecialPowers.spawn( gBrowser.selectedBrowser, [], async function () { let doc = content.document; let input = doc.getElementById("input_spellcheck_no_value"); input.setAttribute("spellcheck", "true"); input.clientTop; // force layout flush } ); }, } ); }); add_task(async function test_text_input_spellcheckwrong() { await test_contextmenu( "#input_spellcheck_incorrect", [ "*prodigality", true, // spelling suggestion "spell-add-to-dictionary", true, "---", null, "context-undo", null, "context-redo", null, "---", null, "context-cut", null, "context-copy", null, "context-paste", null, // ignore clipboard state "context-delete", null, "context-selectall", null, "---", null, "spell-check-enabled", true, "spell-dictionaries", true, [ "spell-check-dictionary-en-US", true, "---", null, "spell-add-dictionaries", true, ], null, ], { waitForSpellCheck: true } ); }); const kCorrectItems = [ "context-undo", false, "context-redo", false, "---", null, "context-cut", null, "context-copy", null, "context-paste", null, // ignore clipboard state "context-delete", null, "context-selectall", null, "---", null, "spell-check-enabled", true, "spell-dictionaries", true, [ "spell-check-dictionary-en-US", true, "---", null, "spell-add-dictionaries", true, ], null, ]; add_task(async function test_text_input_spellcheckcorrect() { await test_contextmenu("#input_spellcheck_correct", kCorrectItems, { waitForSpellCheck: true, }); }); add_task(async function test_text_input_spellcheck_deadactor() { await test_contextmenu("#input_spellcheck_correct", kCorrectItems, { waitForSpellCheck: true, keepMenuOpen: true, }); let wgp = gBrowser.selectedBrowser.browsingContext.currentWindowGlobal; // Now the menu is open, and spellcheck is running, switch to another tab and // close the original: let tab = gBrowser.selectedTab; await BrowserTestUtils.openNewForegroundTab(gBrowser, "https://example.org"); BrowserTestUtils.removeTab(tab); // Ensure we've invalidated the actor await TestUtils.waitForCondition( () => wgp.isClosed, "Waiting for actor to be dead after tab closes" ); contextMenu.hidePopup(); // Now go back to the input testcase: BrowserTestUtils.loadURIString(gBrowser.selectedBrowser, MAIN_URL); await BrowserTestUtils.browserLoaded( gBrowser.selectedBrowser, false, MAIN_URL ); // Check the menu still looks the same, keep it open again: await test_contextmenu("#input_spellcheck_correct", kCorrectItems, { waitForSpellCheck: true, keepMenuOpen: true, }); // Now navigate the tab, after ensuring there's an unload listener, so // we don't end up in bfcache: await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () { content.document.body.setAttribute("onunload", ""); }); wgp = gBrowser.selectedBrowser.browsingContext.currentWindowGlobal; const NEW_URL = MAIN_URL.replace(".com", ".org"); BrowserTestUtils.loadURIString(gBrowser.selectedBrowser, NEW_URL); await BrowserTestUtils.browserLoaded( gBrowser.selectedBrowser, false, NEW_URL ); // Ensure we've invalidated the actor await TestUtils.waitForCondition( () => wgp.isClosed, "Waiting for actor to be dead after onunload" ); contextMenu.hidePopup(); // Check the menu *still* looks the same (and keep it open again): await test_contextmenu("#input_spellcheck_correct", kCorrectItems, { waitForSpellCheck: true, keepMenuOpen: true, }); // Check what happens if the actor stays alive by loading the same page // again; now the context menu stuff should be destroyed by the menu // hiding, nothing else. wgp = gBrowser.selectedBrowser.browsingContext.currentWindowGlobal; BrowserTestUtils.loadURIString(gBrowser.selectedBrowser, NEW_URL); await BrowserTestUtils.browserLoaded( gBrowser.selectedBrowser, false, NEW_URL ); contextMenu.hidePopup(); // Check the menu still looks the same: await test_contextmenu("#input_spellcheck_correct", kCorrectItems, { waitForSpellCheck: true, }); // And test it a last time without any navigation: await test_contextmenu("#input_spellcheck_correct", kCorrectItems, { waitForSpellCheck: true, }); }); add_task(async function test_text_input_spellcheck_multilingual() { if (AppConstants.platform == "macosx") { todo( false, "Need macOS support for closemenu attributes in order to " + "stop the spellcheck menu closing, see bug 1796007." ); return; } let sandbox = sinon.createSandbox(); registerCleanupFunction(() => sandbox.restore()); // We need to mock InlineSpellCheckerUI.mRemote's properties, but // InlineSpellCheckerUI.mRemote won't exist until we initialize the context // menu, so do that and then manually reinit the spellcheck bits so // we control them: await test_contextmenu("#input_spellcheck_correct", kCorrectItems, { waitForSpellCheck: true, keepMenuOpen: true, }); sandbox .stub(InlineSpellCheckerUI.mRemote, "dictionaryList") .get(() => ["en-US", "nl-NL"]); let setterSpy = sandbox.spy(); sandbox .stub(InlineSpellCheckerUI.mRemote, "currentDictionaries") .get(() => ["en-US"]) .set(setterSpy); // Re-init the spellcheck items: InlineSpellCheckerUI.clearDictionaryListFromMenu(); gContextMenu.initSpellingItems(); let dictionaryMenu = document.getElementById("spell-dictionaries-menu"); let menuOpen = BrowserTestUtils.waitForPopupEvent(dictionaryMenu, "shown"); dictionaryMenu.parentNode.openMenu(true); await menuOpen; checkMenu(dictionaryMenu, [ "spell-check-dictionary-nl-NL", true, "spell-check-dictionary-en-US", true, "---", null, "spell-add-dictionaries", true, ]); is( dictionaryMenu.children.length, 4, "Should have 2 dictionaries, a separator and 'add more dictionaries' item in the menu." ); let dictionaryEventPromise = BrowserTestUtils.waitForEvent( document, "spellcheck-changed" ); dictionaryMenu.activateItem( dictionaryMenu.querySelector("[data-locale-code*=nl]") ); let event = await dictionaryEventPromise; Assert.deepEqual( event.detail?.dictionaries, ["en-US", "nl-NL"], "Should have sent right dictionaries with event." ); ok(setterSpy.called, "Should have set currentDictionaries"); Assert.deepEqual( setterSpy.firstCall?.args, [["en-US", "nl-NL"]], "Should have called setter with single argument array of 2 dictionaries." ); // Allow for the menu to potentially close: await new Promise(r => Services.tm.dispatchToMainThread(r)); // Check it hasn't: is( dictionaryMenu.closest("menupopup").state, "open", "Main menu should still be open." ); contextMenu.hidePopup(); }); add_task(async function test_cleanup() { BrowserTestUtils.removeTab(gBrowser.selectedTab); });