tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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 });