head.js (18903B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 // The files in this directory test UrlbarView.#updateResults(). 6 7 "use strict"; 8 9 ChromeUtils.defineESModuleGetters(this, { 10 PlacesTestUtils: "resource://testing-common/PlacesTestUtils.sys.mjs", 11 UrlbarProvidersManager: 12 "moz-src:///browser/components/urlbar/UrlbarProvidersManager.sys.mjs", 13 UrlbarResult: "moz-src:///browser/components/urlbar/UrlbarResult.sys.mjs", 14 UrlbarUtils: "moz-src:///browser/components/urlbar/UrlbarUtils.sys.mjs", 15 UrlbarView: "moz-src:///browser/components/urlbar/UrlbarView.sys.mjs", 16 }); 17 18 ChromeUtils.defineLazyGetter(this, "UrlbarTestUtils", () => { 19 const { UrlbarTestUtils: module } = ChromeUtils.importESModule( 20 "resource://testing-common/UrlbarTestUtils.sys.mjs" 21 ); 22 module.init(this); 23 return module; 24 }); 25 26 // How long to wait for view-update mutations to settle (i.e., to finish 27 // happening) before assuming they're done and moving on with the test. 28 const MUTATION_SETTLE_TIME_MS = 500; 29 30 const MAX_RESULTS = 10; 31 32 add_setup(async function headInit() { 33 await PlacesUtils.history.clear(); 34 await PlacesUtils.bookmarks.eraseEverything(); 35 36 await SpecialPowers.pushPrefEnv({ 37 set: [ 38 // Make absolutely sure the panel stays open during the test. There are 39 // spurious blurs on WebRender TV tests as the test starts that cause the 40 // panel to close and the query to be canceled, resulting in intermittent 41 // failures without this. 42 ["ui.popup.disable_autohide", true], 43 44 // Make sure maxRichResults is 10 for sanity. 45 ["browser.urlbar.maxRichResults", MAX_RESULTS], 46 ], 47 }); 48 49 // Increase the timeout of the remove-stale-rows timer so that it doesn't 50 // interfere with the tests. 51 let originalRemoveStaleRowsTimeout = UrlbarView.removeStaleRowsTimeout; 52 UrlbarView.removeStaleRowsTimeout = 30000; 53 registerCleanupFunction(() => { 54 UrlbarView.removeStaleRowsTimeout = originalRemoveStaleRowsTimeout; 55 }); 56 }); 57 58 /** 59 * A test provider that doesn't finish startQuery() until `finishQueryPromise` 60 * is resolved. 61 */ 62 class DelayingTestProvider extends UrlbarTestUtils.TestProvider { 63 finishQueryPromise = null; 64 async startQuery(context, addCallback) { 65 for (let result of this.results) { 66 addCallback(this, result); 67 } 68 await this.finishQueryPromise; 69 } 70 } 71 72 /** 73 * Makes a result with a suggested index. 74 * 75 * @param {number} suggestedIndex 76 * The preferred index of the result. 77 * @param {number} resultSpan 78 * The result will have this span. 79 * @returns {UrlbarResult} 80 */ 81 function makeSuggestedIndexResult(suggestedIndex, resultSpan = 1) { 82 return new UrlbarResult({ 83 type: UrlbarUtils.RESULT_TYPE.URL, 84 source: UrlbarUtils.RESULT_SOURCE.HISTORY, 85 suggestedIndex, 86 resultSpan, 87 payload: { 88 url: "http://example.com/si", 89 title: "suggested index", 90 helpUrl: "http://example.com/", 91 isBlockable: true, 92 blockL10n: { id: "urlbar-result-menu-remove-from-history" }, 93 }, 94 }); 95 } 96 97 /** 98 * Makes an array of results for the suggestedIndex tests. The array will 99 * include a heuristic followed by the specified results. 100 * 101 * @param {object} options 102 * The options object 103 * @param {number} [options.count] 104 * The number of results to return other than the heuristic. This and 105 * `type` must be given together. 106 * @param {UrlbarUtils.RESULT_TYPE} [options.type] 107 * The type of results to return other than the heuristic. This and `count` 108 * must be given together. 109 * @param {Array} [options.specs] 110 * If you want a mix of result types instead of only one type, then use this 111 * param instead of `count` and `type`. Each item in this array must be an 112 * object with the following properties: 113 * {number} count 114 * The number of results to return for the given `type`. 115 * {UrlbarUtils.RESULT_TYPE} type 116 * The type of results. 117 * @returns {Array} 118 * An array of results. 119 */ 120 function makeProviderResults({ count = 0, type = undefined, specs = [] }) { 121 if (count) { 122 specs.push({ count, type }); 123 } 124 125 let query = "test"; 126 let results = [ 127 new UrlbarResult({ 128 type: UrlbarUtils.RESULT_TYPE.SEARCH, 129 source: UrlbarUtils.RESULT_SOURCE.SEARCH, 130 heuristic: true, 131 payload: { 132 query, 133 engine: Services.search.defaultEngine.name, 134 }, 135 }), 136 ]; 137 138 for (let { count: specCount, type: specType } of specs) { 139 for (let i = 0; i < specCount; i++) { 140 let str = `${query} ${results.length}`; 141 switch (specType) { 142 case UrlbarUtils.RESULT_TYPE.SEARCH: 143 results.push( 144 new UrlbarResult({ 145 type: UrlbarUtils.RESULT_TYPE.SEARCH, 146 source: UrlbarUtils.RESULT_SOURCE.SEARCH, 147 payload: { 148 query, 149 suggestion: str, 150 lowerCaseSuggestion: str.toLowerCase(), 151 engine: Services.search.defaultEngine.name, 152 }, 153 }) 154 ); 155 break; 156 case UrlbarUtils.RESULT_TYPE.URL: 157 results.push( 158 new UrlbarResult({ 159 type: UrlbarUtils.RESULT_TYPE.URL, 160 source: UrlbarUtils.RESULT_SOURCE.HISTORY, 161 payload: { 162 url: "http://example.com/" + i, 163 title: str, 164 helpUrl: "http://example.com/", 165 isBlockable: true, 166 blockL10n: { id: "urlbar-result-menu-remove-from-history" }, 167 }, 168 }) 169 ); 170 break; 171 default: 172 throw new Error(`Unsupported makeProviderResults type: ${specType}`); 173 } 174 } 175 } 176 177 return results; 178 } 179 180 let gSuggestedIndexTaskIndex = 0; 181 182 /** 183 * Adds a suggestedIndex test task. See doSuggestedIndexTest() for params. 184 * 185 * @param {object} options 186 * See doSuggestedIndexTest(). 187 */ 188 function add_suggestedIndex_task(options) { 189 if (!gSuggestedIndexTaskIndex) { 190 initSuggestedIndexTest(); 191 } 192 let testIndex = gSuggestedIndexTaskIndex++; 193 let testName = "test_" + testIndex; 194 let testDesc = JSON.stringify(options); 195 let func = async () => { 196 info(`Running task at index ${testIndex}: ${testDesc}`); 197 await doSuggestedIndexTest(options); 198 }; 199 Object.defineProperty(func, "name", { value: testName }); 200 add_task(func); 201 } 202 203 /** 204 * Initializes suggestedIndex tests. You don't normally need to call this from 205 * your test because add_suggestedIndex_task() calls it automatically. 206 */ 207 function initSuggestedIndexTest() { 208 // These tests can time out on Mac TV WebRender just because they do so much, 209 // so request a longer timeout. 210 if (AppConstants.platform == "macosx") { 211 requestLongerTimeout(3); 212 } 213 registerCleanupFunction(() => { 214 gSuggestedIndexTaskIndex = 0; 215 }); 216 } 217 218 /** 219 * @typedef {object} SuggestedIndexTestOptions 220 * @property {number} [otherCount] 221 * The number of results other than the heuristic and suggestedIndex results 222 * that the provider should return for search 1. This and `otherType` must be 223 * given together. 224 * @property {UrlbarUtils.RESULT_TYPE} [otherType] 225 * The type of results other than the heuristic and suggestedIndex results 226 * that the provider should return for search 1. This and `otherCount` must be 227 * given together. 228 * @property {Array} [other] 229 * If you want the provider to return a mix of result types instead of only 230 * one type, then use this param instead of `otherCount` and `otherType`. Each 231 * item in this array must be an object with the following properties: 232 * {number} count 233 * The number of results to return for the given `type`. 234 * {UrlbarUtils.RESULT_TYPE} type 235 * The type of results. 236 * @property {number} viewCount 237 * The total number of results expected in the view after search 1 finishes, 238 * including the heuristic and suggestedIndex results. 239 * @param {number} [suggestedIndex] 240 * If given, the provider will return a result with this suggested index for 241 * search 1. 242 * @property {number} [resultSpan] 243 * If this and `search1.suggestedIndex` are given, then the suggestedIndex 244 * result for search 1 will have this resultSpan. 245 * @property {Array} [suggestedIndexes] 246 * If you want the provider to return more than one suggestedIndex result for 247 * search 1, then use this instead of `search1.suggestedIndex`. Each item in 248 * this array must be one of the following: 249 * suggestedIndex value 250 * [suggestedIndex, resultSpan] tuple 251 */ 252 253 /** 254 * Runs a suggestedIndex test. Performs two searches and checks the results just 255 * after the view update and after the second search finishes. The caller is 256 * responsible for passing in a description of what the rows should look like 257 * just after the view update finishes but before the second search finishes, 258 * i.e., before stale rows are removed and hidden rows are shown -- this is the 259 * `duringUpdate` param. The important thing this checks is that the rows with 260 * suggested indexes don't move around or appear in the wrong places. 261 * 262 * @param {object} options 263 * The options object 264 * @param {SuggestedIndexTestOptions} options.search1 265 * The first search options object 266 * @param {SuggestedIndexTestOptions} options.search2 267 * This object has the same properties as the `search1` object but it applies 268 * to the second search. 269 * @param {Array<{ count: number, type: UrlbarUtils.RESULT_TYPE, suggestedIndex: ?number, stale: ?boolean, hidden: ?boolean }>} options.duringUpdate 270 * An array of expected row states during the view update. Each item in the 271 * array must be an object with the following properties: 272 * {number} count 273 * The number of rows in the view to which this row state object applies. 274 * {UrlbarUtils.RESULT_TYPE} type 275 * The expected type of the rows. 276 * {number} [suggestedIndex] 277 * The expected suggestedIndex of the row. 278 * {boolean} [stale] 279 * Whether the rows are expected to be stale. Defaults to false. 280 * {boolean} [hidden] 281 * Whether the rows are expected to be hidden. Defaults to false. 282 */ 283 async function doSuggestedIndexTest({ search1, search2, duringUpdate }) { 284 // We use this test provider to test specific results. It has an Infinity 285 // priority so that it provides all results in our test searches, including 286 // the heuristic. That lets us avoid any potential races with the built-in 287 // providers; testing them is not important here. 288 let provider = new DelayingTestProvider({ priority: Infinity }); 289 UrlbarProvidersManager.registerProvider(provider); 290 registerCleanupFunction(() => { 291 UrlbarProvidersManager.unregisterProvider(provider); 292 }); 293 294 // Set up the first search. First, add the non-suggestedIndex results to the 295 // provider. 296 provider.results = makeProviderResults({ 297 specs: search1.other, 298 count: search1.otherCount, 299 type: search1.otherType, 300 }); 301 302 // Set up `suggestedIndexes`. It's an array with [suggestedIndex, resultSpan] 303 // tuples. 304 if (!search1.suggestedIndexes) { 305 search1.suggestedIndexes = []; 306 } 307 search1.suggestedIndexes = search1.suggestedIndexes.map(value => 308 typeof value == "number" ? [value, 1] : value 309 ); 310 if (typeof search1.suggestedIndex == "number") { 311 search1.suggestedIndexes.push([ 312 search1.suggestedIndex, 313 search1.resultSpan || 1, 314 ]); 315 } 316 317 // Add the suggestedIndex results to the provider. 318 for (let [suggestedIndex, resultSpan] of search1.suggestedIndexes) { 319 provider.results.push(makeSuggestedIndexResult(suggestedIndex, resultSpan)); 320 } 321 322 // Do the first search. 323 provider.finishQueryPromise = Promise.resolve(); 324 await UrlbarTestUtils.promiseAutocompleteResultPopup({ 325 window, 326 value: "test", 327 }); 328 329 // Sanity check the results. 330 Assert.equal( 331 UrlbarTestUtils.getResultCount(window), 332 search1.viewCount, 333 "Row count after first search" 334 ); 335 for (let [suggestedIndex, resultSpan] of search1.suggestedIndexes) { 336 let index = 337 suggestedIndex >= 0 338 ? Math.min(search1.viewCount - 1, suggestedIndex) 339 : Math.max(0, search1.viewCount + suggestedIndex); 340 let result = await UrlbarTestUtils.getDetailsOfResultAt(window, index); 341 Assert.equal( 342 result.element.row.result.suggestedIndex, 343 suggestedIndex, 344 "suggestedIndex after first search" 345 ); 346 Assert.equal( 347 UrlbarUtils.getSpanForResult(result.element.row.result), 348 resultSpan, 349 "resultSpan after first search" 350 ); 351 } 352 353 // Set up the second search. First, add the non-suggestedIndex results to the 354 // provider. 355 provider.results = makeProviderResults({ 356 specs: search2.other, 357 count: search2.otherCount, 358 type: search2.otherType, 359 }); 360 361 // Set up `suggestedIndexes`. It's an array with [suggestedIndex, resultSpan] 362 // tuples. 363 if (!search2.suggestedIndexes) { 364 search2.suggestedIndexes = []; 365 } 366 search2.suggestedIndexes = search2.suggestedIndexes.map(value => 367 typeof value == "number" ? [value, 1] : value 368 ); 369 if (typeof search2.suggestedIndex == "number") { 370 search2.suggestedIndexes.push([ 371 search2.suggestedIndex, 372 search2.resultSpan || 1, 373 ]); 374 } 375 376 // Add the suggestedIndex results to the provider. 377 for (let [suggestedIndex, resultSpan] of search2.suggestedIndexes) { 378 provider.results.push(makeSuggestedIndexResult(suggestedIndex, resultSpan)); 379 } 380 381 let rowCountDuringUpdate = duringUpdate.reduce( 382 (count, rowState) => count + rowState.count, 383 0 384 ); 385 386 // Don't allow the search to finish until we check the updated rows. We'll 387 // accomplish that by adding a mutation observer to observe completion of the 388 // update and delaying resolving the provider's finishQueryPromise. 389 // 390 // This promise works like this: We add a mutation observer that observes the 391 // view's entire subtree. Every time we observe a mutation, we set 392 // `lastMutationTime` to the current time. Meanwhile, we run an interval that 393 // compares `now` to `lastMutationTime` every time it fires. When the 394 // difference between `now` and `lastMutationTime` is sufficiently large, we 395 // assume the view update is done, and we resolve the promise. 396 let mutationPromise = new Promise(resolve => { 397 let lastMutationTime = ChromeUtils.now(); 398 let observer = new MutationObserver(() => { 399 info("Observed mutation"); 400 lastMutationTime = ChromeUtils.now(); 401 }); 402 observer.observe(UrlbarTestUtils.getResultsContainer(window), { 403 attributes: true, 404 characterData: true, 405 childList: true, 406 subtree: true, 407 }); 408 409 let interval = setInterval( 410 () => { 411 if (MUTATION_SETTLE_TIME_MS < ChromeUtils.now() - lastMutationTime) { 412 info("No further mutations observed, stopping"); 413 clearInterval(interval); 414 observer.disconnect(); 415 resolve(); 416 } 417 }, 418 Math.ceil(MUTATION_SETTLE_TIME_MS / 10) 419 ); 420 }); 421 422 // Now do the second search but don't wait for it to finish. 423 let resolveQuery; 424 provider.finishQueryPromise = new Promise( 425 resolve => (resolveQuery = resolve) 426 ); 427 let queryPromise = UrlbarTestUtils.promiseAutocompleteResultPopup({ 428 window, 429 value: "test", 430 }); 431 432 // Wait for the update to finish. 433 info("Waiting for mutations to settle"); 434 await mutationPromise; 435 436 // Check the rows. We can't use UrlbarTestUtils.getDetailsOfResultAt() here 437 // because it waits for the search to finish. 438 Assert.equal( 439 UrlbarTestUtils.getResultCount(window), 440 rowCountDuringUpdate, 441 "Row count during update" 442 ); 443 let rows = UrlbarTestUtils.getResultsContainer(window).children; 444 let rowIndex = 0; 445 for (let rowState of duringUpdate) { 446 for (let i = 0; i < rowState.count; i++) { 447 let row = rows[rowIndex]; 448 449 // type 450 if ("type" in rowState) { 451 Assert.equal( 452 row.result.type, 453 rowState.type, 454 `Type at index ${rowIndex} during update` 455 ); 456 } 457 458 // suggestedIndex 459 if ("suggestedIndex" in rowState) { 460 Assert.ok( 461 row.result.hasSuggestedIndex, 462 `Row at index ${rowIndex} has suggestedIndex during update` 463 ); 464 Assert.equal( 465 row.result.suggestedIndex, 466 rowState.suggestedIndex, 467 `suggestedIndex at index ${rowIndex} during update` 468 ); 469 } else { 470 Assert.ok( 471 !row.result.hasSuggestedIndex, 472 `Row at index ${rowIndex} does not have suggestedIndex during update` 473 ); 474 } 475 476 // resultSpan 477 Assert.equal( 478 UrlbarUtils.getSpanForResult(row.result), 479 rowState.resultSpan || 1, 480 `resultSpan at index ${rowIndex} during update` 481 ); 482 483 // stale 484 if (rowState.stale) { 485 Assert.equal( 486 row.getAttribute("stale"), 487 "true", 488 `Row at index ${rowIndex} is stale during update` 489 ); 490 } else { 491 Assert.ok( 492 !row.hasAttribute("stale"), 493 `Row at index ${rowIndex} is not stale during update` 494 ); 495 } 496 497 // visible 498 Assert.equal( 499 BrowserTestUtils.isVisible(row), 500 !rowState.hidden, 501 `Visible at index ${rowIndex} during update` 502 ); 503 504 rowIndex++; 505 } 506 } 507 508 // Finish the search. 509 resolveQuery(); 510 await queryPromise; 511 512 // Check the rows now that the second search is done. First, build a map from 513 // real indexes to suggested index. e.g., if a suggestedIndex = -1, then the 514 // real index = the result count - 1. 515 let suggestedIndexesByRealIndex = new Map(); 516 for (let [suggestedIndex, resultSpan] of search2.suggestedIndexes) { 517 let realIndex = 518 suggestedIndex >= 0 519 ? Math.min(suggestedIndex, search2.viewCount - 1) 520 : Math.max(0, search2.viewCount + suggestedIndex); 521 suggestedIndexesByRealIndex.set(realIndex, [suggestedIndex, resultSpan]); 522 } 523 524 Assert.equal( 525 UrlbarTestUtils.getResultCount(window), 526 search2.viewCount, 527 "Row count after update" 528 ); 529 for (let i = 0; i < search2.viewCount; i++) { 530 let result = rows[i].result; 531 let tuple = suggestedIndexesByRealIndex.get(i); 532 if (tuple) { 533 let [suggestedIndex, resultSpan] = tuple; 534 Assert.ok( 535 result.hasSuggestedIndex, 536 `Row at index ${i} has suggestedIndex after update` 537 ); 538 Assert.equal( 539 result.suggestedIndex, 540 suggestedIndex, 541 `suggestedIndex at index ${i} after update` 542 ); 543 Assert.equal( 544 UrlbarUtils.getSpanForResult(result), 545 resultSpan, 546 `resultSpan at index ${i} after update` 547 ); 548 } else { 549 Assert.ok( 550 !result.hasSuggestedIndex, 551 `Row at index ${i} does not have suggestedIndex after update` 552 ); 553 } 554 } 555 556 await UrlbarTestUtils.promisePopupClose(window); 557 gURLBar.handleRevert(); 558 UrlbarProvidersManager.unregisterProvider(provider); 559 }