browser_localSearchShortcuts.js (10391B)
1 /* Any copyright is dedicated to the Public Domain. 2 * http://creativecommons.org/publicdomain/zero/1.0/ */ 3 4 /** 5 * Checks the local shortcut rows in the engines list of the search pane. 6 */ 7 8 "use strict"; 9 10 ChromeUtils.defineESModuleGetters(this, { 11 UrlbarPrefs: "moz-src:///browser/components/urlbar/UrlbarPrefs.sys.mjs", 12 UrlbarUtils: "moz-src:///browser/components/urlbar/UrlbarUtils.sys.mjs", 13 UrlbarTokenizer: 14 "moz-src:///browser/components/urlbar/UrlbarTokenizer.sys.mjs", 15 }); 16 17 let gTree; 18 const isRestrictKeywordsFeatureOn = () => 19 UrlbarPrefs.getScotchBonnetPref("searchRestrictKeywords.featureGate"); 20 21 add_setup(async function () { 22 await SpecialPowers.pushPrefEnv({ 23 set: [["browser.urlbar.scotchBonnet.enableOverride", true]], 24 }); 25 let prefs = await openPreferencesViaOpenPreferencesAPI("search", { 26 leaveOpen: true, 27 }); 28 registerCleanupFunction(() => { 29 BrowserTestUtils.removeTab(gBrowser.selectedTab); 30 }); 31 32 Assert.equal( 33 prefs.selectedPane, 34 "paneSearch", 35 "Sanity check: Search pane is selected by default" 36 ); 37 38 gTree = gBrowser.contentDocument.querySelector("#engineList"); 39 gTree.scrollIntoView(); 40 gTree.focus(); 41 }); 42 43 // The rows should be visible and checked by default. 44 add_task(async function visible() { 45 await checkRowVisibility(true); 46 await forEachLocalShortcutRow(async row => { 47 Assert.equal( 48 gTree.view.getCellValue(row, gTree.columns.getNamedColumn("engineShown")), 49 "true", 50 "Row is checked initially" 51 ); 52 }); 53 }); 54 55 // Toggling the browser.urlbar.shortcuts.* prefs should toggle the corresponding 56 // checkboxes in the rows. 57 add_task(async function syncFromPrefs() { 58 let col = gTree.columns.getNamedColumn("engineShown"); 59 await forEachLocalShortcutRow(async (row, shortcut) => { 60 Assert.equal( 61 gTree.view.getCellValue(row, col), 62 "true", 63 "Row is checked initially" 64 ); 65 await SpecialPowers.pushPrefEnv({ 66 set: [[getUrlbarPrefName(shortcut.pref), false]], 67 }); 68 Assert.equal( 69 gTree.view.getCellValue(row, col), 70 "false", 71 "Row is unchecked after disabling pref" 72 ); 73 await SpecialPowers.popPrefEnv(); 74 Assert.equal( 75 gTree.view.getCellValue(row, col), 76 "true", 77 "Row is checked after re-enabling pref" 78 ); 79 }); 80 }); 81 82 // Pressing the space key while a row is selected should toggle its checkbox 83 // and pref. 84 add_task(async function syncToPrefs_spaceKey() { 85 let col = gTree.columns.getNamedColumn("engineShown"); 86 await forEachLocalShortcutRow(async (row, shortcut) => { 87 Assert.ok( 88 UrlbarPrefs.get(shortcut.pref), 89 "Sanity check: Pref is enabled initially" 90 ); 91 Assert.equal( 92 gTree.view.getCellValue(row, col), 93 "true", 94 "Row is checked initially" 95 ); 96 gTree.view.selection.select(row); 97 EventUtils.synthesizeKey(" ", {}, gTree.ownerGlobal); 98 Assert.ok( 99 !UrlbarPrefs.get(shortcut.pref), 100 "Pref is disabled after pressing space key" 101 ); 102 Assert.equal( 103 gTree.view.getCellValue(row, col), 104 "false", 105 "Row is unchecked after pressing space key" 106 ); 107 Services.prefs.clearUserPref(getUrlbarPrefName(shortcut.pref)); 108 }); 109 }); 110 111 // Clicking the checkbox in a local shortcut row should toggle the checkbox and 112 // pref. 113 add_task(async function syncToPrefs_click() { 114 let col = gTree.columns.getNamedColumn("engineShown"); 115 await forEachLocalShortcutRow(async (row, shortcut) => { 116 Assert.ok( 117 UrlbarPrefs.get(shortcut.pref), 118 "Sanity check: Pref is enabled initially" 119 ); 120 Assert.equal( 121 gTree.view.getCellValue(row, col), 122 "true", 123 "Row is checked initially" 124 ); 125 126 let rect = gTree.getCoordsForCellItem(row, col, "cell"); 127 let x = rect.x + rect.width / 2; 128 let y = rect.y + rect.height / 2; 129 EventUtils.synthesizeMouse(gTree.body, x, y, {}, gTree.ownerGlobal); 130 131 Assert.ok( 132 !UrlbarPrefs.get(shortcut.pref), 133 "Pref is disabled after clicking checkbox" 134 ); 135 Assert.equal( 136 gTree.view.getCellValue(row, col), 137 "false", 138 "Row is unchecked after clicking checkbox" 139 ); 140 Services.prefs.clearUserPref(getUrlbarPrefName(shortcut.pref)); 141 }); 142 }); 143 144 // The keyword column should not be editable according to isEditable(). 145 add_task(async function keywordNotEditable_isEditable() { 146 await forEachLocalShortcutRow(async row => { 147 Assert.ok( 148 !gTree.view.isEditable( 149 row, 150 gTree.columns.getNamedColumn("engineKeyword") 151 ), 152 "Keyword column is not editable" 153 ); 154 }); 155 }); 156 157 // Pressing the enter key while a row is selected shouldn't allow the keyword to 158 // be edited. 159 add_task(async function keywordNotEditable_enterKey() { 160 let col = gTree.columns.getNamedColumn("engineKeyword"); 161 await forEachLocalShortcutRow(async (row, shortcut) => { 162 Assert.ok( 163 shortcut.restrict, 164 "Sanity check: Shortcut restriction char is non-empty" 165 ); 166 167 let tokenToKeywords = await UrlbarTokenizer.getL10nRestrictKeywords(); 168 let keywords = tokenToKeywords 169 .get(shortcut.restrict) 170 .map(keyword => `@${keyword.toLowerCase()}`) 171 .join(", "); 172 173 Assert.equal( 174 gTree.view.getCellText(row, col), 175 isRestrictKeywordsFeatureOn() 176 ? `${keywords}, ${shortcut.restrict}` 177 : shortcut.restrict, 178 isRestrictKeywordsFeatureOn() 179 ? "Sanity check: Keyword column has correct restriction keyword and char initially" 180 : "Sanity check: Keyword column has correct restriction char initially" 181 ); 182 183 gTree.view.selection.select(row); 184 EventUtils.synthesizeKey("KEY_Enter", {}, gTree.ownerGlobal); 185 EventUtils.sendString("newkeyword"); 186 EventUtils.synthesizeKey("KEY_Enter", {}, gTree.ownerGlobal); 187 188 // Wait a moment to allow for any possible asynchronicity. 189 // eslint-disable-next-line mozilla/no-arbitrary-setTimeout 190 await new Promise(r => setTimeout(r, 500)); 191 192 Assert.equal( 193 gTree.view.getCellText(row, col), 194 isRestrictKeywordsFeatureOn() 195 ? `${keywords}, ${shortcut.restrict}` 196 : shortcut.restrict, 197 isRestrictKeywordsFeatureOn() 198 ? "Keyword column is still restriction keyword and char" 199 : "Keyword column is still restriction char" 200 ); 201 }); 202 }); 203 204 // Double-clicking the keyword column shouldn't allow the keyword to be edited. 205 add_task(async function keywordNotEditable_click() { 206 let col = gTree.columns.getNamedColumn("engineKeyword"); 207 await forEachLocalShortcutRow(async (row, shortcut) => { 208 let tokenToKeywords = await UrlbarTokenizer.getL10nRestrictKeywords(); 209 let keywords = tokenToKeywords 210 .get(shortcut.restrict) 211 .map(keyword => `@${keyword.toLowerCase()}`) 212 .join(", "); 213 214 Assert.ok( 215 shortcut.restrict, 216 "Sanity check: Shortcut restriction char is non-empty" 217 ); 218 Assert.equal( 219 gTree.view.getCellText(row, col), 220 isRestrictKeywordsFeatureOn() 221 ? `${keywords}, ${shortcut.restrict}` 222 : shortcut.restrict, 223 isRestrictKeywordsFeatureOn() 224 ? "Sanity check: Keyword column has correct restriction keyword and char initially" 225 : "Sanity check: Keyword column has correct restriction char initially" 226 ); 227 228 let rect = gTree.getCoordsForCellItem(row, col, "text"); 229 let x = rect.x + rect.width / 2; 230 let y = rect.y + rect.height / 2; 231 232 let promise = BrowserTestUtils.waitForEvent(gTree, "dblclick"); 233 234 // Click once to select the row. 235 EventUtils.synthesizeMouse( 236 gTree.body, 237 x, 238 y, 239 { clickCount: 1 }, 240 gTree.ownerGlobal 241 ); 242 243 // Now double-click the keyword column. 244 EventUtils.synthesizeMouse( 245 gTree.body, 246 x, 247 y, 248 { clickCount: 2 }, 249 gTree.ownerGlobal 250 ); 251 252 await promise; 253 254 EventUtils.sendString("newkeyword"); 255 EventUtils.synthesizeKey("KEY_Enter", {}, gTree.ownerGlobal); 256 257 // Wait a moment to allow for any possible asynchronicity. 258 // eslint-disable-next-line mozilla/no-arbitrary-setTimeout 259 await new Promise(r => setTimeout(r, 500)); 260 261 Assert.equal( 262 gTree.view.getCellText(row, col), 263 isRestrictKeywordsFeatureOn() 264 ? `${keywords}, ${shortcut.restrict}` 265 : shortcut.restrict, 266 isRestrictKeywordsFeatureOn() 267 ? "Keyword column is still restriction keyword and char" 268 : "Keyword column is still restriction char" 269 ); 270 }); 271 }); 272 273 /** 274 * Asserts that the engine and local shortcut rows are present in the tree. 275 */ 276 async function checkRowVisibility() { 277 let engines = await Services.search.getVisibleEngines(); 278 279 Assert.equal( 280 gTree.view.rowCount, 281 engines.length + UrlbarUtils.LOCAL_SEARCH_MODES.length, 282 "Expected number of tree rows" 283 ); 284 285 // Check the engine rows. 286 for (let row = 0; row < engines.length; row++) { 287 let engine = engines[row]; 288 let text = gTree.view.getCellText( 289 row, 290 gTree.columns.getNamedColumn("engineName") 291 ); 292 Assert.equal( 293 text, 294 engine.name, 295 `Sanity check: Tree row ${row} has expected engine name` 296 ); 297 } 298 299 // Check the shortcut rows. 300 await forEachLocalShortcutRow(async (row, shortcut) => { 301 let text = gTree.view.getCellText( 302 row, 303 gTree.columns.getNamedColumn("engineName") 304 ); 305 let name = UrlbarUtils.getResultSourceName(shortcut.source); 306 let l10nName = await gTree.ownerDocument.l10n.formatValue( 307 `urlbar-search-mode-${name}` 308 ); 309 Assert.ok(l10nName, "Sanity check: l10n name is non-empty"); 310 Assert.equal(text, l10nName, `Tree row ${row} has expected shortcut name`); 311 }); 312 } 313 314 /** 315 * Calls a callback for each local shortcut row in the tree. 316 * 317 * @param {function} callback 318 * Called for each local shortcut row like: callback(rowIndex, shortcutObject) 319 */ 320 async function forEachLocalShortcutRow(callback) { 321 let engines = await Services.search.getVisibleEngines(); 322 for (let i = 0; i < UrlbarUtils.LOCAL_SEARCH_MODES.length; i++) { 323 let shortcut = UrlbarUtils.LOCAL_SEARCH_MODES[i]; 324 let row = engines.length + i; 325 await callback(row, shortcut); 326 } 327 } 328 329 /** 330 * Prepends the `browser.urlbar.` branch to the given relative pref. 331 * 332 * @param {string} relativePref 333 * A pref name relative to the `browser.urlbar.`. 334 * @returns {string} 335 * The full pref name with `browser.urlbar.` prepended. 336 */ 337 function getUrlbarPrefName(relativePref) { 338 return `browser.urlbar.${relativePref}`; 339 }