summaryrefslogtreecommitdiffstats
path: root/browser/base/content/test/static/browser_sentence_case_strings.js
blob: e995f76b1a48f9037ae2285fa53dd15711b231e3 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
/* Any copyright is dedicated to the Public Domain.
   http://creativecommons.org/publicdomain/zero/1.0/ */

"use strict";

/**
 * This test file checks that our en-US builds use sentence case strings
 * where appropriate. It's not exhaustive - some panels will show different
 * items in different states, and this test doesn't iterate all of them.
 */

/* global PanelUI */

const { CustomizableUITestUtils } = ChromeUtils.importESModule(
  "resource://testing-common/CustomizableUITestUtils.sys.mjs"
);

const { AppMenuNotifications } = ChromeUtils.importESModule(
  "resource://gre/modules/AppMenuNotifications.sys.mjs"
);

// These are brand names, proper names, or other things that we expect to
// not abide exactly to sentence case. NAMES is for single words, and PHRASES
// is for words in a specific order.
const NAMES = new Set(["Mozilla", "Nightly", "Firefox"]);
const PHRASES = new Set(["Troubleshoot Mode…"]);

let gCUITestUtils = new CustomizableUITestUtils(window);
let gLocalization = new Localization(["browser/newtab/asrouter.ftl"], true);

/**
 * This recursive function will take the current main or subview, find all of
 * the buttons that navigate to subviews inside it, and click each one
 * individually. Upon entering the new view, we recurse. When the subviews
 * within a view have been exhausted, we go back up a level.
 *
 * @generator
 * @param {<xul:panelview>} parentView The view to start scanning for
 *        subviews.
 * @yields {<xul:panelview>} Each found <xul:panelview>, in depth-first search
 *         order.
 */
async function* iterateSubviews(parentView) {
  let navButtons = Array.from(
    // Ensure that only enabled buttons are tested
    parentView.querySelectorAll(".subviewbutton-nav:not([disabled])")
  );
  if (!navButtons) {
    return;
  }

  for (let button of navButtons) {
    info("Click " + button.id);
    let panel = parentView.closest("panel");
    let panelmultiview = parentView.closest("panelmultiview");
    let promiseViewShown = BrowserTestUtils.waitForEvent(panel, "ViewShown");
    button.click();
    let viewShownEvent = await promiseViewShown;

    yield viewShownEvent.originalTarget;

    info("Shown " + viewShownEvent.originalTarget.id);
    yield* iterateSubviews(viewShownEvent.originalTarget);
    promiseViewShown = BrowserTestUtils.waitForEvent(parentView, "ViewShown");
    panelmultiview.goBack();
    await promiseViewShown;
  }
}

/**
 * Given a <xul:panelview>, look for <xul:toolbarbutton> descendants, extract
 * any relevant strings from them, and check to see if they are in sentence
 * case. By default, labels, textContent, and toolTipText (including dynamic
 * toolTipText) are checked.
 *
 * @param {<xul:panelview>} view The <xul:panelview> to check.
 */
function checkToolbarButtons(view) {
  let toolbarbuttons = view.querySelectorAll("toolbarbutton");
  info("Checking toolbarbuttons in subview with id " + view.id);

  for (let toolbarbutton of toolbarbuttons) {
    let strings = [
      toolbarbutton.label,
      toolbarbutton.textContent,
      toolbarbutton.toolTipText,
      GetDynamicShortcutTooltipText(toolbarbutton.id),
    ];
    info("Checking toolbarbutton " + toolbarbutton.id);
    for (let string of strings) {
      checkSentenceCase(string, toolbarbutton.id);
    }
  }
}

function checkSubheaders(view) {
  let subheaders = view.querySelectorAll("h2");
  info("Checking subheaders in subview with id " + view.id);

  for (let subheader of subheaders) {
    checkSentenceCase(subheader.textContent, subheader.id);
  }
}

async function checkUpdateBanner(view) {
  let banner = view.querySelector("#appMenu-proton-update-banner");

  const notifications = [
    "update-downloading",
    "update-available",
    "update-manual",
    "update-unsupported",
    "update-restart",
  ];

  for (const notification of notifications) {
    // Forcibly remove the label in order to wait for the new label.
    banner.removeAttribute("label");

    let labelPromise = BrowserTestUtils.waitForMutationCondition(
      banner,
      { attributes: true, attributeFilter: ["label"] },
      () => !!banner.getAttribute("label")
    );

    AppMenuNotifications.showNotification(notification);

    await labelPromise;

    checkSentenceCase(banner.label, banner.id);

    AppMenuNotifications.removeNotification(/.*/);
  }
}

/**
 * Asserts whether or not a string matches sentence case.
 *
 * @param {String} string The string to check for sentence case.
 * @param {String} elementID The ID of the element being tested. This is
 *        mainly used for the assertion message to make it easier to debug
 *        failures, but items without IDs will not be checked (as these are
 *        likely using dynamic strings, like bookmarked page titles).
 */
function checkSentenceCase(string, elementID) {
  if (!string || !elementID) {
    return;
  }

  info("Testing string: " + string);

  let words = string.trim().split(/\s+/);

  // We expect that the first word is always capitalized. If it isn't,
  // there's no need to keep checking the rest of the string, since we're
  // going to fail the assertion.
  let result = hasExpectedCapitalization(words[0], true);
  if (result) {
    for (let wordIndex = 1; wordIndex < words.length; ++wordIndex) {
      let word = words[wordIndex];

      if (word) {
        if (isPartOfPhrase(words, wordIndex)) {
          result = hasExpectedCapitalization(word, true);
        } else {
          let isName = NAMES.has(word);
          result = hasExpectedCapitalization(word, isName);
        }
        if (!result) {
          break;
        }
      }
    }
  }

  Assert.ok(result, `${string} for ${elementID} should have sentence casing.`);
}

/**
 * Returns true if a word is part of a phrase defined in the PHRASES set.
 * The function will see if the word is contained within any of the defined
 * PHRASES, and will then scan back and forward within the words array to
 * to see if the word is indeed part of the phrase in context.
 *
 * @param {Array} words The full array of words being checked by the caller.
 * @param {Number} wordIndex The index of the word being checked within the
 *        words array.
 * @return {Boolean}
 */
function isPartOfPhrase(words, wordIndex) {
  let word = words[wordIndex];

  info(`Checking if ${word} is part of a phrase`);

  for (let phrase of PHRASES) {
    let phraseFragments = phrase.split(" ");
    let fragmentIndex = phraseFragments.indexOf(word);

    // If we didn't find the word within this phrase, the candidate phrase
    // has more words than what we're analyzing, or the word doesn't have
    // enough words before it to match the candidate phrase, then move on.
    if (
      fragmentIndex == -1 ||
      words.length - phraseFragments.length < 0 ||
      fragmentIndex > wordIndex
    ) {
      continue;
    }

    let wordsSlice = words.slice(
      wordIndex - fragmentIndex,
      wordIndex + phraseFragments.length
    );
    let matches = wordsSlice.every((w, index) => {
      return phraseFragments[index] === w;
    });

    if (matches) {
      info(`${word} is part of phrase ${phrase}`);
      return true;
    }
  }

  return false;
}

/**
 * Tests that the strings under the AppMenu are in sentence case.
 */
add_task(async function test_sentence_case_appmenu() {
  // Some of these panels are lazy, so it's necessary to open them in
  // order for them to be inserted into the DOM.
  await gCUITestUtils.openMainMenu();
  registerCleanupFunction(async () => {
    await gCUITestUtils.hideMainMenu();
  });

  checkToolbarButtons(PanelUI.mainView);
  checkSubheaders(PanelUI.mainView);

  for await (const view of iterateSubviews(PanelUI.mainView)) {
    checkToolbarButtons(view);
    checkSubheaders(view);
  }

  await checkUpdateBanner(PanelUI.mainView);
});

/**
 * Tests that the strings under the All Tabs panel are in sentence case.
 */
add_task(async function test_sentence_case_all_tabs_panel() {
  gTabsPanel.init();

  const allTabsView = document.getElementById("allTabsMenu-allTabsView");
  let allTabsPopupShownPromise = BrowserTestUtils.waitForEvent(
    allTabsView,
    "ViewShown"
  );
  gTabsPanel.showAllTabsPanel();
  await allTabsPopupShownPromise;

  registerCleanupFunction(async () => {
    let allTabsPopupHiddenPromise = BrowserTestUtils.waitForEvent(
      allTabsView.panelMultiView,
      "PanelMultiViewHidden"
    );
    gTabsPanel.hideAllTabsPanel();
    await allTabsPopupHiddenPromise;
  });

  checkToolbarButtons(gTabsPanel.allTabsView);
  checkSubheaders(gTabsPanel.allTabsView);

  for await (const view of iterateSubviews(gTabsPanel.allTabsView)) {
    checkToolbarButtons(view);
    checkSubheaders(view);
  }
});