tor-browser

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

browser_sentence_case_strings.js (8718B)


      1 /* Any copyright is dedicated to the Public Domain.
      2   http://creativecommons.org/publicdomain/zero/1.0/ */
      3 
      4 "use strict";
      5 
      6 /**
      7 * This test file checks that our en-US builds use sentence case strings
      8 * where appropriate. It's not exhaustive - some panels will show different
      9 * items in different states, and this test doesn't iterate all of them.
     10 */
     11 
     12 /* global PanelUI */
     13 
     14 const { CustomizableUITestUtils } = ChromeUtils.importESModule(
     15  "resource://testing-common/CustomizableUITestUtils.sys.mjs"
     16 );
     17 
     18 const { AppMenuNotifications } = ChromeUtils.importESModule(
     19  "resource://gre/modules/AppMenuNotifications.sys.mjs"
     20 );
     21 
     22 // These are brand names, proper names, or other things that we expect to
     23 // not abide exactly to sentence case. NAMES is for single words, and PHRASES
     24 // is for words in a specific order.
     25 const NAMES = new Set(["Mozilla", "Nightly", "Firefox", "AI"]);
     26 const PHRASES = new Set(["Troubleshoot Mode…"]);
     27 
     28 let gCUITestUtils = new CustomizableUITestUtils(window);
     29 let gLocalization = new Localization(["browser/newtab/asrouter.ftl"], true);
     30 
     31 /**
     32 * This recursive function will take the current main or subview, find all of
     33 * the buttons that navigate to subviews inside it, and click each one
     34 * individually. Upon entering the new view, we recurse. When the subviews
     35 * within a view have been exhausted, we go back up a level.
     36 *
     37 * @generator
     38 * @param {<xul:panelview>} parentView The view to start scanning for
     39 *        subviews.
     40 * @yields {<xul:panelview>} Each found <xul:panelview>, in depth-first search
     41 *         order.
     42 */
     43 async function* iterateSubviews(parentView) {
     44  let navButtons = Array.from(
     45    // Ensure that only enabled buttons are tested
     46    parentView.querySelectorAll(
     47      ".subviewbutton-nav:not([disabled]):not([hidden])"
     48    )
     49  );
     50  if (!navButtons) {
     51    return;
     52  }
     53 
     54  for (let button of navButtons) {
     55    info("Click " + button.id);
     56    let panel = parentView.closest("panel");
     57    let panelmultiview = parentView.closest("panelmultiview");
     58    let promiseViewShown = BrowserTestUtils.waitForEvent(panel, "ViewShown");
     59    button.click();
     60    let viewShownEvent = await promiseViewShown;
     61 
     62    yield viewShownEvent.originalTarget;
     63 
     64    info("Shown " + viewShownEvent.originalTarget.id);
     65    yield* iterateSubviews(viewShownEvent.originalTarget);
     66    promiseViewShown = BrowserTestUtils.waitForEvent(parentView, "ViewShown");
     67    panelmultiview.goBack();
     68    await promiseViewShown;
     69  }
     70 }
     71 
     72 /**
     73 * Given a <xul:panelview>, look for <xul:toolbarbutton> descendants, extract
     74 * any relevant strings from them, and check to see if they are in sentence
     75 * case. By default, labels, textContent, and toolTipText (including dynamic
     76 * toolTipText) are checked.
     77 *
     78 * @param {<xul:panelview>} view The <xul:panelview> to check.
     79 */
     80 function checkToolbarButtons(view) {
     81  let toolbarbuttons = view.querySelectorAll("toolbarbutton");
     82  info("Checking toolbarbuttons in subview with id " + view.id);
     83 
     84  for (let toolbarbutton of toolbarbuttons) {
     85    let strings = [
     86      toolbarbutton.label,
     87      toolbarbutton.textContent,
     88      toolbarbutton.toolTipText,
     89      DynamicShortcutTooltip.getText(toolbarbutton.id),
     90    ];
     91    info("Checking toolbarbutton " + toolbarbutton.id);
     92    for (let string of strings) {
     93      checkSentenceCase(string, toolbarbutton.id);
     94    }
     95  }
     96 }
     97 
     98 function checkSubheaders(view) {
     99  let subheaders = view.querySelectorAll("h2");
    100  info("Checking subheaders in subview with id " + view.id);
    101 
    102  for (let subheader of subheaders) {
    103    checkSentenceCase(subheader.textContent, subheader.id);
    104  }
    105 }
    106 
    107 async function checkUpdateBanner(view) {
    108  let banner = view.querySelector("#appMenu-update-banner");
    109 
    110  const notifications = [
    111    "update-downloading",
    112    "update-available",
    113    "update-manual",
    114    "update-unsupported",
    115    "update-restart",
    116  ];
    117 
    118  for (const notification of notifications) {
    119    // Forcibly remove the label in order to wait for the new label.
    120    banner.removeAttribute("label");
    121 
    122    let labelPromise = BrowserTestUtils.waitForMutationCondition(
    123      banner,
    124      { attributes: true, attributeFilter: ["label"] },
    125      () => !!banner.getAttribute("label")
    126    );
    127 
    128    AppMenuNotifications.showNotification(notification);
    129 
    130    await labelPromise;
    131 
    132    checkSentenceCase(banner.label, banner.id);
    133 
    134    AppMenuNotifications.removeNotification(/.*/);
    135  }
    136 }
    137 
    138 /**
    139 * Asserts whether or not a string matches sentence case.
    140 *
    141 * @param {string} string The string to check for sentence case.
    142 * @param {string} elementID The ID of the element being tested. This is
    143 *        mainly used for the assertion message to make it easier to debug
    144 *        failures, but items without IDs will not be checked (as these are
    145 *        likely using dynamic strings, like bookmarked page titles).
    146 */
    147 function checkSentenceCase(string, elementID) {
    148  if (!string || !elementID) {
    149    return;
    150  }
    151 
    152  info("Testing string: " + string);
    153 
    154  let words = string.trim().split(/\s+/);
    155 
    156  // We expect that the first word is always capitalized. If it isn't,
    157  // there's no need to keep checking the rest of the string, since we're
    158  // going to fail the assertion.
    159  let result = hasExpectedCapitalization(words[0], true);
    160  if (result) {
    161    for (let wordIndex = 1; wordIndex < words.length; ++wordIndex) {
    162      let word = words[wordIndex];
    163 
    164      if (word) {
    165        if (isPartOfPhrase(words, wordIndex)) {
    166          result = hasExpectedCapitalization(word, true);
    167        } else {
    168          let isName = NAMES.has(word);
    169          result = hasExpectedCapitalization(word, isName);
    170        }
    171        if (!result) {
    172          break;
    173        }
    174      }
    175    }
    176  }
    177 
    178  Assert.ok(result, `${string} for ${elementID} should have sentence casing.`);
    179 }
    180 
    181 /**
    182 * Returns true if a word is part of a phrase defined in the PHRASES set.
    183 * The function will see if the word is contained within any of the defined
    184 * PHRASES, and will then scan back and forward within the words array to
    185 * to see if the word is indeed part of the phrase in context.
    186 *
    187 * @param {Array} words The full array of words being checked by the caller.
    188 * @param {number} wordIndex The index of the word being checked within the
    189 *        words array.
    190 * @return {boolean}
    191 */
    192 function isPartOfPhrase(words, wordIndex) {
    193  let word = words[wordIndex];
    194 
    195  info(`Checking if ${word} is part of a phrase`);
    196 
    197  for (let phrase of PHRASES) {
    198    let phraseFragments = phrase.split(" ");
    199    let fragmentIndex = phraseFragments.indexOf(word);
    200 
    201    // If we didn't find the word within this phrase, the candidate phrase
    202    // has more words than what we're analyzing, or the word doesn't have
    203    // enough words before it to match the candidate phrase, then move on.
    204    if (
    205      fragmentIndex == -1 ||
    206      words.length - phraseFragments.length < 0 ||
    207      fragmentIndex > wordIndex
    208    ) {
    209      continue;
    210    }
    211 
    212    let wordsSlice = words.slice(
    213      wordIndex - fragmentIndex,
    214      wordIndex + phraseFragments.length
    215    );
    216    let matches = wordsSlice.every((w, index) => {
    217      return phraseFragments[index] === w;
    218    });
    219 
    220    if (matches) {
    221      info(`${word} is part of phrase ${phrase}`);
    222      return true;
    223    }
    224  }
    225 
    226  return false;
    227 }
    228 
    229 /**
    230 * Tests that the strings under the AppMenu are in sentence case.
    231 */
    232 add_task(async function test_sentence_case_appmenu() {
    233  // Some of these panels are lazy, so it's necessary to open them in
    234  // order for them to be inserted into the DOM.
    235  await gCUITestUtils.openMainMenu();
    236  registerCleanupFunction(async () => {
    237    await gCUITestUtils.hideMainMenu();
    238  });
    239 
    240  checkToolbarButtons(PanelUI.mainView);
    241  checkSubheaders(PanelUI.mainView);
    242 
    243  for await (const view of iterateSubviews(PanelUI.mainView)) {
    244    checkToolbarButtons(view);
    245    checkSubheaders(view);
    246  }
    247 
    248  await checkUpdateBanner(PanelUI.mainView);
    249 });
    250 
    251 /**
    252 * Tests that the strings under the All Tabs panel are in sentence case.
    253 */
    254 add_task(async function test_sentence_case_all_tabs_panel() {
    255  gTabsPanel.init();
    256 
    257  const allTabsView = document.getElementById("allTabsMenu-allTabsView");
    258  let allTabsPopupShownPromise = BrowserTestUtils.waitForEvent(
    259    allTabsView,
    260    "ViewShown"
    261  );
    262  gTabsPanel.showAllTabsPanel();
    263  await allTabsPopupShownPromise;
    264 
    265  registerCleanupFunction(async () => {
    266    let allTabsPopupHiddenPromise = BrowserTestUtils.waitForEvent(
    267      allTabsView.panelMultiView,
    268      "PanelMultiViewHidden"
    269    );
    270    gTabsPanel.hideAllTabsPanel();
    271    await allTabsPopupHiddenPromise;
    272  });
    273 
    274  checkToolbarButtons(gTabsPanel.allTabsView);
    275  checkSubheaders(gTabsPanel.allTabsView);
    276 
    277  for await (const view of iterateSubviews(gTabsPanel.allTabsView)) {
    278    checkToolbarButtons(view);
    279    checkSubheaders(view);
    280  }
    281 });