browser_search_engineList.js (10885B)
1 /* Any copyright is dedicated to the Public Domain. 2 * http://creativecommons.org/publicdomain/zero/1.0/ */ 3 4 // This tests the search engine list in about:preferences#search. 5 6 "use strict"; 7 8 const { SearchTestUtils } = ChromeUtils.importESModule( 9 "resource://testing-common/SearchTestUtils.sys.mjs" 10 ); 11 const { SearchUtils } = ChromeUtils.importESModule( 12 "moz-src:///toolkit/components/search/SearchUtils.sys.mjs" 13 ); 14 const { sinon } = ChromeUtils.importESModule( 15 "resource://testing-common/Sinon.sys.mjs" 16 ); 17 18 SearchTestUtils.init(this); 19 20 const CONFIG = [ 21 { identifier: "engine1" }, 22 { identifier: "engine2" }, 23 { 24 identifier: "de_only_engine", 25 base: { 26 urls: { 27 search: { 28 base: "https://moz.test/", 29 searchTermParamName: "search", 30 }, 31 }, 32 }, 33 variants: [ 34 { 35 environment: { locales: ["de"] }, 36 }, 37 ], 38 }, 39 ]; 40 41 let userEngine; 42 let extensionEngine; 43 let installedEngines; 44 let userInstalledAppEngine; 45 46 function getCellText(tree, i, cellName) { 47 return tree.view.getCellText(i, tree.columns.getNamedColumn(cellName)); 48 } 49 50 async function engine_list_test(fn) { 51 let task = async () => { 52 let prefs = await openPreferencesViaOpenPreferencesAPI("search", { 53 leaveOpen: true, 54 }); 55 Assert.equal( 56 prefs.selectedPane, 57 "paneSearch", 58 "Search pane is selected by default" 59 ); 60 let doc = gBrowser.contentDocument; 61 let tree = doc.querySelector("#engineList"); 62 Assert.ok( 63 !tree.hidden, 64 "The search engine list should be visible when Search is requested" 65 ); 66 // Scroll the treeview into view since mouse operations 67 // off screen can act confusingly. 68 tree.scrollIntoView(); 69 await fn(tree, doc); 70 BrowserTestUtils.removeTab(gBrowser.selectedTab); 71 }; 72 73 // Make sure the name of the passed function is used in logs. 74 Object.defineProperty(task, "name", { value: fn.name }); 75 add_task(task); 76 } 77 async function selectEngine(tree, index) { 78 let rect = tree.getCoordsForCellItem( 79 index, 80 tree.columns.getNamedColumn("engineName"), 81 "text" 82 ); 83 let x = rect.x + rect.width / 2; 84 let y = rect.y + rect.height / 2; 85 let promise = BrowserTestUtils.waitForEvent(tree, "click"); 86 EventUtils.synthesizeMouse( 87 tree.body, 88 x, 89 y, 90 { clickCount: 1 }, 91 tree.ownerGlobal 92 ); 93 return promise; 94 } 95 96 add_setup(async function () { 97 await SearchTestUtils.updateRemoteSettingsConfig(CONFIG); 98 installedEngines = await Services.search.getAppProvidedEngines(); 99 100 await SearchTestUtils.installSearchExtension({ 101 keyword: ["testing", "customkeyword"], 102 search_url: "https://example.com/engine1", 103 search_url_get_params: "search={searchTerms}", 104 name: "Extension Engine", 105 }); 106 extensionEngine = Services.search.getEngineByName("Extension Engine"); 107 installedEngines.push(extensionEngine); 108 109 userEngine = await Services.search.addUserEngine({ 110 name: "User Engine", 111 url: "https://example.com/user?q={searchTerms}&b=ff", 112 alias: "u", 113 }); 114 installedEngines.push(userEngine); 115 116 userInstalledAppEngine = 117 await Services.search.findContextualSearchEngineByHost("moz.test"); 118 119 await Services.search.addSearchEngine(userInstalledAppEngine); 120 // The added engines are removed in the last test. 121 }); 122 123 engine_list_test(async function test_engine_list(tree) { 124 let userEngineIndex = installedEngines.length - 1; 125 for (let i = 0; i < installedEngines.length; i++) { 126 let engine = installedEngines[i]; 127 Assert.equal( 128 getCellText(tree, i, "engineName"), 129 engine.name, 130 "Search engine " + engine.name + " displayed correctly." 131 ); 132 Assert.equal( 133 tree.view.isEditable(i, tree.columns.getNamedColumn("engineName")), 134 i == userEngineIndex, 135 "Only user engine name is editable." 136 ); 137 } 138 }); 139 140 engine_list_test(async function test_change_keyword(tree) { 141 let extensionEngineIndex = installedEngines.length - 2; 142 Assert.equal( 143 getCellText(tree, extensionEngineIndex, "engineKeyword"), 144 "testing, customkeyword", 145 "Internal keywords are displayed." 146 ); 147 let rect = tree.getCoordsForCellItem( 148 extensionEngineIndex, 149 tree.columns.getNamedColumn("engineKeyword"), 150 "text" 151 ); 152 153 // Test editing keyword of extension engine because it 154 // has user-defined and extension-provided keywords. 155 let x = rect.x + rect.width / 2; 156 let y = rect.y + rect.height / 2; 157 let win = tree.ownerGlobal; 158 159 let promise = BrowserTestUtils.waitForEvent(tree, "dblclick"); 160 EventUtils.synthesizeMouse(tree.body, x, y, { clickCount: 1 }, win); 161 EventUtils.synthesizeMouse(tree.body, x, y, { clickCount: 2 }, win); 162 await promise; 163 164 promise = SearchTestUtils.promiseSearchNotification( 165 SearchUtils.MODIFIED_TYPE.CHANGED, 166 SearchUtils.TOPIC_ENGINE_MODIFIED 167 ); 168 EventUtils.sendString("newkeyword"); 169 EventUtils.sendKey("RETURN"); 170 await promise; 171 172 Assert.equal( 173 getCellText(tree, extensionEngineIndex, "engineKeyword"), 174 "newkeyword, testing, customkeyword", 175 "User-defined keyword was added." 176 ); 177 178 // Test duplicated keywords (different capitalization). 179 tree.view.setCellText( 180 0, 181 tree.columns.getNamedColumn("engineKeyword"), 182 "keyword" 183 ); 184 await TestUtils.waitForTick(); 185 186 let keywordBefore = getCellText(tree, 1, "engineKeyword"); 187 let alertSpy = sinon.spy(win, "alert"); 188 tree.view.setCellText( 189 1, 190 tree.columns.getNamedColumn("engineKeyword"), 191 "Keyword" 192 ); 193 await TestUtils.waitForTick(); 194 195 Assert.ok(alertSpy.calledOnce, "Warning was shown."); 196 Assert.equal( 197 getCellText(tree, 1, "engineKeyword"), 198 keywordBefore, 199 "Did not modify keywords." 200 ); 201 alertSpy.restore(); 202 }); 203 204 engine_list_test(async function test_rename_engines(tree) { 205 // Test editing name of user search engine because 206 // only the names of user engines can be edited. 207 let userEngineIndex = installedEngines.length - 1; 208 let rect = tree.getCoordsForCellItem( 209 userEngineIndex, 210 tree.columns.getNamedColumn("engineName"), 211 "text" 212 ); 213 let x = rect.x + rect.width / 2; 214 let y = rect.y + rect.height / 2; 215 let win = tree.ownerGlobal; 216 217 let promise = BrowserTestUtils.waitForEvent(tree, "dblclick"); 218 EventUtils.synthesizeMouse(tree.body, x, y, { clickCount: 1 }, win); 219 EventUtils.synthesizeMouse(tree.body, x, y, { clickCount: 2 }, win); 220 await promise; 221 222 EventUtils.sendString("User Engine 2"); 223 promise = SearchTestUtils.promiseSearchNotification( 224 SearchUtils.MODIFIED_TYPE.CHANGED, 225 SearchUtils.TOPIC_ENGINE_MODIFIED 226 ); 227 EventUtils.sendKey("RETURN"); 228 await promise; 229 230 Assert.equal(userEngine.name, "User Engine 2", "Engine was renamed."); 231 232 // Avoid duplicated engine names. 233 let alertSpy = sinon.spy(win, "alert"); 234 tree.view.setCellText( 235 userEngineIndex, 236 tree.columns.getNamedColumn("engineName"), 237 "Extension Engine" 238 ); 239 await TestUtils.waitForTick(); 240 241 Assert.ok(alertSpy.calledOnce, "Warning was shown."); 242 Assert.equal( 243 getCellText(tree, userEngineIndex, "engineName"), 244 "User Engine 2", 245 "Name was not modified." 246 ); 247 248 alertSpy.restore(); 249 }); 250 251 engine_list_test(async function test_remove_button_disabled_state(tree, doc) { 252 let appProvidedEngines = await Services.search.getAppProvidedEngines(); 253 for (let i = 0; i < appProvidedEngines.length; i++) { 254 let engine = appProvidedEngines[i]; 255 let isDefaultSearchEngine = 256 engine.id == Services.search.defaultEngine.id || 257 engine.id == Services.search.defaultPrivateEngine.id; 258 259 await selectEngine(tree, i); 260 Assert.equal( 261 doc.querySelector("#removeEngineButton").disabled, 262 isDefaultSearchEngine, 263 "Remove button is in correct disabled state." 264 ); 265 } 266 }); 267 268 engine_list_test(async function test_remove_button(tree, doc) { 269 let win = tree.ownerGlobal; 270 let alertSpy = sinon.stub(win, "alert"); 271 272 info("Removing user engine."); 273 let userEngineIndex = installedEngines.findIndex(e => e.id == userEngine.id); 274 await selectEngine(tree, userEngineIndex); 275 276 let promptPromise = PromptTestUtils.handleNextPrompt( 277 gBrowser.selectedBrowser, 278 { modalType: Services.prompt.MODAL_TYPE_CONTENT }, 279 { buttonNumClick: 0 } // 0 = cancel, 1 = remove 280 ); 281 let removedPromise = SearchTestUtils.promiseSearchNotification( 282 SearchUtils.MODIFIED_TYPE.REMOVED, 283 SearchUtils.TOPIC_ENGINE_MODIFIED 284 ); 285 286 doc.querySelector("#removeEngineButton").click(); 287 await promptPromise; 288 let removedEngine = await removedPromise; 289 Assert.equal( 290 removedEngine.id, 291 userEngine.id, 292 "User engine was removed after a prompt." 293 ); 294 295 // Re-fetch the engines since removing the user engine changed it. 296 installedEngines = await Services.search.getEngines(); 297 298 info("Removing extension engine."); 299 let extensionEngineIndex = installedEngines.findIndex( 300 e => e.id == extensionEngine.id 301 ); 302 await selectEngine(tree, extensionEngineIndex); 303 304 doc.querySelector("#removeEngineButton").click(); 305 await TestUtils.waitForCondition(() => alertSpy.calledOnce); 306 Assert.ok(true, "Alert is shown when attempting to remove extension engine."); 307 308 info("Removing user installed app engine."); 309 let index = installedEngines.findIndex( 310 e => e.id == userInstalledAppEngine.id 311 ); 312 313 await selectEngine(tree, index); 314 315 promptPromise = PromptTestUtils.handleNextPrompt( 316 gBrowser.selectedBrowser, 317 { modalType: Services.prompt.MODAL_TYPE_CONTENT }, 318 { buttonNumClick: 0 } // 0 = cancel, 1 = remove 319 ); 320 removedPromise = SearchTestUtils.promiseSearchNotification( 321 SearchUtils.MODIFIED_TYPE.REMOVED, 322 SearchUtils.TOPIC_ENGINE_MODIFIED 323 ); 324 doc.querySelector("#removeEngineButton").click(); 325 await promptPromise; 326 removedEngine = await removedPromise; 327 Assert.equal( 328 removedEngine.id, 329 userInstalledAppEngine.id, 330 "User installed app engine was removed after a prompt." 331 ); 332 333 info("Removing (last) app provided engine."); 334 let appProvidedEngines = await Services.search.getAppProvidedEngines(); 335 let lastAppEngine = appProvidedEngines[appProvidedEngines.length - 1]; 336 let lastAppEngineIndex = installedEngines.findIndex( 337 e => e.id == lastAppEngine.id 338 ); 339 await selectEngine(tree, lastAppEngineIndex); 340 341 doc.querySelector("#removeEngineButton").click(); 342 removedEngine = await SearchTestUtils.promiseSearchNotification( 343 SearchUtils.MODIFIED_TYPE.REMOVED, 344 SearchUtils.TOPIC_ENGINE_MODIFIED 345 ); 346 Assert.equal( 347 removedEngine.id, 348 lastAppEngine.id, 349 "Last app provided engine was removed without a prompt." 350 ); 351 352 // Cleanup. 353 alertSpy.restore(); 354 let updatedPromise = SearchTestUtils.promiseSearchNotification( 355 SearchUtils.MODIFIED_TYPE.CHANGED, 356 SearchUtils.TOPIC_ENGINE_MODIFIED 357 ); 358 doc.getElementById("restoreDefaultSearchEngines").click(); 359 await updatedPromise; 360 // The user engine is purposefully not re-added. 361 // The extension engine is removed automatically on cleanup. 362 });