browser_contextmenu_spellcheck.js (9969B)
1 /* Any copyright is dedicated to the Public Domain. 2 http://creativecommons.org/publicdomain/zero/1.0/ */ 3 4 "use strict"; 5 6 let contextMenu; 7 8 const { sinon } = ChromeUtils.importESModule( 9 "resource://testing-common/Sinon.sys.mjs" 10 ); 11 12 const example_base = 13 // eslint-disable-next-line @microsoft/sdl/no-insecure-url 14 "http://example.com/browser/browser/base/content/test/contextMenu/"; 15 const MAIN_URL = example_base + "subtst_contextmenu_input.html"; 16 17 const askChatMenu = [ 18 "context-ask-chat", 19 true, 20 // Need a blank entry here because the Ask Chat submenu is dynamically built with no ids. 21 "", 22 null, 23 ]; 24 25 add_task(async function test_setup() { 26 await BrowserTestUtils.openNewForegroundTab(gBrowser, MAIN_URL); 27 28 const chrome_base = 29 "chrome://mochitests/content/browser/browser/base/content/test/contextMenu/"; 30 const contextmenu_common = chrome_base + "contextmenu_common.js"; 31 /* import-globals-from contextmenu_common.js */ 32 Services.scriptloader.loadSubScript(contextmenu_common, this); 33 }); 34 35 add_task(async function test_text_input_spellcheck() { 36 await test_contextmenu( 37 "#input_spellcheck_no_value", 38 [ 39 "context-undo", 40 false, 41 "context-redo", 42 false, 43 "---", 44 null, 45 "context-cut", 46 null, // ignore the enabled/disabled states; there are race conditions 47 // in the edit commands but they're not relevant for what we're testing. 48 "context-copy", 49 null, 50 "context-paste", 51 null, // ignore clipboard state 52 "context-delete", 53 null, 54 "context-selectall", 55 null, 56 "---", 57 null, 58 ...askChatMenu, 59 "---", 60 null, 61 "spell-check-enabled", 62 true, 63 "spell-dictionaries", 64 true, 65 [ 66 "spell-check-dictionary-en-US", 67 true, 68 "---", 69 null, 70 "spell-add-dictionaries", 71 true, 72 ], 73 null, 74 ], 75 { 76 waitForSpellCheck: true, 77 async preCheckContextMenuFn() { 78 await SpecialPowers.spawn( 79 gBrowser.selectedBrowser, 80 [], 81 async function () { 82 let doc = content.document; 83 let input = doc.getElementById("input_spellcheck_no_value"); 84 input.setAttribute("spellcheck", "true"); 85 input.clientTop; // force layout flush 86 } 87 ); 88 }, 89 awaitOnMenuBuilt: { 90 id: "context-ask-chat", 91 }, 92 } 93 ); 94 }); 95 96 add_task(async function test_text_input_spellcheckwrong() { 97 await test_contextmenu( 98 "#input_spellcheck_incorrect", 99 [ 100 "*prodigality", 101 true, // spelling suggestion 102 "spell-add-to-dictionary", 103 true, 104 "---", 105 null, 106 "context-undo", 107 null, 108 "context-redo", 109 null, 110 "---", 111 null, 112 "context-cut", 113 null, 114 "context-copy", 115 null, 116 "context-paste", 117 null, // ignore clipboard state 118 "context-delete", 119 null, 120 "context-selectall", 121 null, 122 "---", 123 null, 124 ...askChatMenu, 125 "---", 126 null, 127 "spell-check-enabled", 128 true, 129 "spell-dictionaries", 130 true, 131 [ 132 "spell-check-dictionary-en-US", 133 true, 134 "---", 135 null, 136 "spell-add-dictionaries", 137 true, 138 ], 139 null, 140 ], 141 { 142 waitForSpellCheck: true, 143 awaitOnMenuBuilt: { 144 id: "context-ask-chat", 145 }, 146 } 147 ); 148 }); 149 150 const kCorrectItems = [ 151 "context-undo", 152 false, 153 "context-redo", 154 false, 155 "---", 156 null, 157 "context-cut", 158 null, 159 "context-copy", 160 null, 161 "context-paste", 162 null, // ignore clipboard state 163 "context-delete", 164 null, 165 "context-selectall", 166 null, 167 "---", 168 null, 169 ...askChatMenu, 170 "---", 171 null, 172 "spell-check-enabled", 173 true, 174 "spell-dictionaries", 175 true, 176 [ 177 "spell-check-dictionary-en-US", 178 true, 179 "---", 180 null, 181 "spell-add-dictionaries", 182 true, 183 ], 184 null, 185 ]; 186 187 add_task(async function test_text_input_spellcheckcorrect() { 188 await test_contextmenu("#input_spellcheck_correct", kCorrectItems, { 189 waitForSpellCheck: true, 190 awaitOnMenuBuilt: { 191 id: "context-ask-chat", 192 }, 193 }); 194 }); 195 196 add_task(async function test_text_input_spellcheck_deadactor() { 197 await test_contextmenu("#input_spellcheck_correct", kCorrectItems, { 198 waitForSpellCheck: true, 199 keepMenuOpen: true, 200 awaitOnMenuBuilt: { 201 id: "context-ask-chat", 202 }, 203 }); 204 let wgp = gBrowser.selectedBrowser.browsingContext.currentWindowGlobal; 205 206 // Now the menu is open, and spellcheck is running, switch to another tab and 207 // close the original: 208 let tab = gBrowser.selectedTab; 209 await BrowserTestUtils.openNewForegroundTab(gBrowser, "https://example.org"); 210 BrowserTestUtils.removeTab(tab); 211 // Ensure we've invalidated the actor 212 await TestUtils.waitForCondition( 213 () => wgp.isClosed, 214 "Waiting for actor to be dead after tab closes" 215 ); 216 contextMenu.hidePopup(); 217 218 // Now go back to the input testcase: 219 BrowserTestUtils.startLoadingURIString(gBrowser.selectedBrowser, MAIN_URL); 220 await BrowserTestUtils.browserLoaded( 221 gBrowser.selectedBrowser, 222 false, 223 MAIN_URL 224 ); 225 226 // Check the menu still looks the same, keep it open again: 227 await test_contextmenu("#input_spellcheck_correct", kCorrectItems, { 228 waitForSpellCheck: true, 229 keepMenuOpen: true, 230 awaitOnMenuBuilt: { 231 id: "context-ask-chat", 232 }, 233 }); 234 235 // Now navigate the tab, after ensuring there's an unload listener, so 236 // we don't end up in bfcache: 237 await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () { 238 content.document.body.setAttribute("onunload", ""); 239 }); 240 wgp = gBrowser.selectedBrowser.browsingContext.currentWindowGlobal; 241 242 const NEW_URL = MAIN_URL.replace(".com", ".org"); 243 BrowserTestUtils.startLoadingURIString(gBrowser.selectedBrowser, NEW_URL); 244 await BrowserTestUtils.browserLoaded( 245 gBrowser.selectedBrowser, 246 false, 247 NEW_URL 248 ); 249 // Ensure we've invalidated the actor 250 await TestUtils.waitForCondition( 251 () => wgp.isClosed, 252 "Waiting for actor to be dead after onunload" 253 ); 254 contextMenu.hidePopup(); 255 256 // Check the menu *still* looks the same (and keep it open again): 257 await test_contextmenu("#input_spellcheck_correct", kCorrectItems, { 258 waitForSpellCheck: true, 259 keepMenuOpen: true, 260 awaitOnMenuBuilt: { 261 id: "context-ask-chat", 262 }, 263 }); 264 265 // Check what happens if the actor stays alive by loading the same page 266 // again; now the context menu stuff should be destroyed by the menu 267 // hiding, nothing else. 268 wgp = gBrowser.selectedBrowser.browsingContext.currentWindowGlobal; 269 BrowserTestUtils.startLoadingURIString(gBrowser.selectedBrowser, NEW_URL); 270 await BrowserTestUtils.browserLoaded( 271 gBrowser.selectedBrowser, 272 false, 273 NEW_URL 274 ); 275 contextMenu.hidePopup(); 276 277 // Check the menu still looks the same: 278 await test_contextmenu("#input_spellcheck_correct", kCorrectItems, { 279 waitForSpellCheck: true, 280 awaitOnMenuBuilt: { 281 id: "context-ask-chat", 282 }, 283 }); 284 // And test it a last time without any navigation: 285 await test_contextmenu("#input_spellcheck_correct", kCorrectItems, { 286 waitForSpellCheck: true, 287 awaitOnMenuBuilt: { 288 id: "context-ask-chat", 289 }, 290 }); 291 }); 292 293 add_task(async function test_text_input_spellcheck_multilingual() { 294 if (AppConstants.platform == "macosx") { 295 todo( 296 false, 297 "Need macOS support for closemenu attributes in order to " + 298 "stop the spellcheck menu closing, see bug 1796007." 299 ); 300 return; 301 } 302 let sandbox = sinon.createSandbox(); 303 registerCleanupFunction(() => sandbox.restore()); 304 305 // We need to mock InlineSpellCheckerUI.mRemote's properties, but 306 // InlineSpellCheckerUI.mRemote won't exist until we initialize the context 307 // menu, so do that and then manually reinit the spellcheck bits so 308 // we control them: 309 await test_contextmenu("#input_spellcheck_correct", kCorrectItems, { 310 waitForSpellCheck: true, 311 keepMenuOpen: true, 312 awaitOnMenuBuilt: { 313 id: "context-ask-chat", 314 }, 315 }); 316 sandbox 317 .stub(InlineSpellCheckerUI.mRemote, "dictionaryList") 318 .get(() => ["en-US", "nl-NL"]); 319 let setterSpy = sandbox.spy(); 320 sandbox 321 .stub(InlineSpellCheckerUI.mRemote, "currentDictionaries") 322 .get(() => ["en-US"]) 323 .set(setterSpy); 324 // Re-init the spellcheck items: 325 InlineSpellCheckerUI.clearDictionaryListFromMenu(); 326 gContextMenu.initSpellingItems(); 327 328 let dictionaryMenu = document.getElementById("spell-dictionaries-menu"); 329 let menuOpen = BrowserTestUtils.waitForPopupEvent(dictionaryMenu, "shown"); 330 dictionaryMenu.parentNode.openMenu(true); 331 await menuOpen; 332 checkMenu(dictionaryMenu, [ 333 "spell-check-dictionary-nl-NL", 334 true, 335 "spell-check-dictionary-en-US", 336 true, 337 "---", 338 null, 339 "spell-add-dictionaries", 340 true, 341 ]); 342 is( 343 dictionaryMenu.children.length, 344 4, 345 "Should have 2 dictionaries, a separator and 'add more dictionaries' item in the menu." 346 ); 347 348 let dictionaryEventPromise = BrowserTestUtils.waitForEvent( 349 document, 350 "spellcheck-changed" 351 ); 352 dictionaryMenu.activateItem( 353 dictionaryMenu.querySelector("[data-locale-code*=nl]") 354 ); 355 let event = await dictionaryEventPromise; 356 Assert.deepEqual( 357 event.detail?.dictionaries, 358 ["en-US", "nl-NL"], 359 "Should have sent right dictionaries with event." 360 ); 361 ok(setterSpy.called, "Should have set currentDictionaries"); 362 Assert.deepEqual( 363 setterSpy.firstCall?.args, 364 [["en-US", "nl-NL"]], 365 "Should have called setter with single argument array of 2 dictionaries." 366 ); 367 // Allow for the menu to potentially close: 368 await new Promise(r => Services.tm.dispatchToMainThread(r)); 369 // Check it hasn't: 370 is( 371 dictionaryMenu.closest("menupopup").state, 372 "open", 373 "Main menu should still be open." 374 ); 375 contextMenu.hidePopup(); 376 }); 377 378 add_task(async function test_cleanup() { 379 BrowserTestUtils.removeTab(gBrowser.selectedTab); 380 });