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