test_remote_tabs.js (17601B)
1 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- 2 * vim:set ts=2 sw=2 sts=2 et: 3 */ 4 "use strict"; 5 6 const { Weave } = ChromeUtils.importESModule( 7 "resource://services-sync/main.sys.mjs" 8 ); 9 10 // A mock "Tabs" engine which autocomplete will use instead of the real 11 // engine. We pass a constructor that Sync creates. 12 function MockTabsEngine() { 13 this.clients = null; // We'll set this dynamically 14 } 15 16 MockTabsEngine.prototype = { 17 name: "tabs", 18 19 startTracking() {}, 20 getAllClients() { 21 return this.clients; 22 }, 23 }; 24 25 // A clients engine that doesn't need to be a constructor. 26 let MockClientsEngine = { 27 getClientType(guid) { 28 Assert.ok(guid.endsWith("desktop") || guid.endsWith("mobile")); 29 return guid.endsWith("mobile") ? "phone" : "desktop"; 30 }, 31 remoteClientExists(_id) { 32 return true; 33 }, 34 getClientName(id) { 35 return id.endsWith("mobile") ? "My Phone" : "My Desktop"; 36 }, 37 }; 38 39 // Configure the singleton engine for a test. 40 function configureEngine(clients) { 41 // Configure the instance Sync created. 42 let engine = Weave.Service.engineManager.get("tabs"); 43 engine.clients = clients; 44 Weave.Service.clientsEngine = MockClientsEngine; 45 // Send an observer that pretends the engine just finished a sync. 46 Services.obs.notifyObservers(null, "weave:engine:sync:finish", "tabs"); 47 } 48 49 testEngine_setup(); 50 51 add_setup(async function () { 52 // Tell Sync about the mocks. 53 Weave.Service.engineManager.register(MockTabsEngine); 54 55 // Tell the Sync XPCOM service it is initialized. 56 let weaveXPCService = Cc["@mozilla.org/weave/service;1"].getService( 57 Ci.nsISupports 58 ).wrappedJSObject; 59 weaveXPCService.ready = true; 60 61 registerCleanupFunction(async () => { 62 Services.prefs.clearUserPref("services.sync.username"); 63 Services.prefs.clearUserPref("services.sync.registerEngines"); 64 Services.prefs.clearUserPref("browser.urlbar.suggest.searches"); 65 Services.prefs.clearUserPref("browser.urlbar.suggest.quickactions"); 66 await cleanupPlaces(); 67 }); 68 69 Services.prefs.setCharPref("services.sync.username", "someone@somewhere.com"); 70 Services.prefs.setCharPref("services.sync.registerEngines", ""); 71 // Avoid hitting the network. 72 Services.prefs.setBoolPref("browser.urlbar.suggest.searches", false); 73 Services.prefs.setBoolPref("browser.urlbar.suggest.quickactions", false); 74 }); 75 76 add_task(async function test_minimal() { 77 // The minimal client and tabs info we can get away with. 78 configureEngine([ 79 { 80 id: "desktop", 81 tabs: [ 82 { 83 urlHistory: ["http://example.com/"], 84 }, 85 ], 86 }, 87 ]); 88 89 let query = "ex"; 90 let context = createContext(query, { isPrivate: false }); 91 await check_results({ 92 context, 93 matches: [ 94 makeSearchResult(context, { 95 engineName: SUGGESTIONS_ENGINE_NAME, 96 heuristic: true, 97 }), 98 makeRemoteTabResult(context, { 99 uri: "http://example.com/", 100 device: "My Desktop", 101 }), 102 ], 103 }); 104 }); 105 106 add_task(async function test_maximal() { 107 // Every field that could possibly exist on a remote record. 108 configureEngine([ 109 { 110 id: "mobile", 111 tabs: [ 112 { 113 urlHistory: ["http://example.com/"], 114 title: "An Example", 115 icon: "http://favicon", 116 }, 117 ], 118 }, 119 ]); 120 121 let query = "ex"; 122 let context = createContext(query, { isPrivate: false }); 123 await check_results({ 124 context, 125 matches: [ 126 makeSearchResult(context, { 127 engineName: SUGGESTIONS_ENGINE_NAME, 128 heuristic: true, 129 }), 130 makeRemoteTabResult(context, { 131 uri: "http://example.com/", 132 device: "My Phone", 133 title: "An Example", 134 iconUri: "cached-favicon:http://favicon/", 135 }), 136 ], 137 }); 138 }); 139 140 add_task(async function test_noShowIcons() { 141 Services.prefs.setBoolPref("services.sync.syncedTabs.showRemoteIcons", false); 142 configureEngine([ 143 { 144 id: "mobile", 145 tabs: [ 146 { 147 urlHistory: ["http://example.com/"], 148 title: "An Example", 149 icon: "http://favicon", 150 }, 151 ], 152 }, 153 ]); 154 155 let query = "ex"; 156 let context = createContext(query, { isPrivate: false }); 157 await check_results({ 158 context, 159 matches: [ 160 makeSearchResult(context, { 161 engineName: SUGGESTIONS_ENGINE_NAME, 162 heuristic: true, 163 }), 164 makeRemoteTabResult(context, { 165 uri: "http://example.com/", 166 device: "My Phone", 167 title: "An Example", 168 // expecting the default favicon due to that pref. 169 iconUri: "", 170 }), 171 ], 172 }); 173 Services.prefs.clearUserPref("services.sync.syncedTabs.showRemoteIcons"); 174 }); 175 176 add_task(async function test_tabsDisabledInUrlbar() { 177 Services.prefs.setBoolPref("browser.urlbar.suggest.remotetab", false); 178 configureEngine([ 179 { 180 id: "mobile", 181 tabs: [ 182 { 183 urlHistory: ["http://example.com/"], 184 title: "An Example", 185 icon: "http://favicon", 186 }, 187 ], 188 }, 189 ]); 190 191 let context = createContext("ex", { isPrivate: false }); 192 await check_results({ 193 context, 194 matches: [ 195 makeSearchResult(context, { 196 engineName: SUGGESTIONS_ENGINE_NAME, 197 heuristic: true, 198 }), 199 ], 200 }); 201 202 Services.prefs.clearUserPref("browser.urlbar.suggest.remotetab"); 203 }); 204 205 add_task(async function test_matches_title() { 206 // URL doesn't match search expression, should still match the title. 207 configureEngine([ 208 { 209 id: "mobile", 210 tabs: [ 211 { 212 urlHistory: ["http://foo.com/"], 213 title: "An Example", 214 }, 215 ], 216 }, 217 ]); 218 219 let query = "ex"; 220 let context = createContext(query, { isPrivate: false }); 221 await check_results({ 222 context, 223 matches: [ 224 makeSearchResult(context, { 225 engineName: SUGGESTIONS_ENGINE_NAME, 226 heuristic: true, 227 }), 228 makeRemoteTabResult(context, { 229 uri: "http://foo.com/", 230 device: "My Phone", 231 title: "An Example", 232 }), 233 ], 234 }); 235 }); 236 237 add_task(async function test_localtab_matches_override() { 238 // We have an open tab to the same page on a remote device, only "switch to 239 // tab" should appear as duplicate detection removed the remote one. 240 241 // First set up Sync to have the page as a remote tab. 242 configureEngine([ 243 { 244 id: "mobile", 245 tabs: [ 246 { 247 urlHistory: ["http://foo.com/"], 248 title: "An Example", 249 }, 250 ], 251 }, 252 ]); 253 254 // Set up Places to think the tab is open locally. 255 let uri = Services.io.newURI("http://foo.com/"); 256 await PlacesTestUtils.addVisits([{ uri, title: "An Example" }]); 257 await addOpenPages(uri, 1); 258 259 let query = "ex"; 260 let context = createContext(query, { isPrivate: false }); 261 await check_results({ 262 context, 263 matches: [ 264 makeSearchResult(context, { 265 engineName: SUGGESTIONS_ENGINE_NAME, 266 heuristic: true, 267 }), 268 makeTabSwitchResult(context, { 269 uri: "http://foo.com/", 270 title: "An Example", 271 }), 272 ], 273 }); 274 275 await removeOpenPages(uri, 1); 276 await cleanupPlaces(); 277 }); 278 279 add_task(async function test_remotetab_matches_override() { 280 // If we have an history result to the same page, we should only get the 281 // remote tab match. 282 let url = "http://foo.remote.com/"; 283 // First set up Sync to have the page as a remote tab. 284 configureEngine([ 285 { 286 id: "mobile", 287 tabs: [ 288 { 289 urlHistory: [url], 290 title: "An Example", 291 }, 292 ], 293 }, 294 ]); 295 296 // Set up Places to think the tab is in history. 297 await PlacesTestUtils.addVisits(url); 298 299 let query = "ex"; 300 let context = createContext(query, { isPrivate: false }); 301 await check_results({ 302 context, 303 matches: [ 304 makeSearchResult(context, { 305 engineName: SUGGESTIONS_ENGINE_NAME, 306 heuristic: true, 307 }), 308 makeRemoteTabResult(context, { 309 uri: "http://foo.remote.com/", 310 device: "My Phone", 311 title: "An Example", 312 }), 313 ], 314 }); 315 316 await cleanupPlaces(); 317 }); 318 319 add_task(async function test_mixed_result_types() { 320 // In case we have many results, non-remote results should flex to the bottom. 321 let url = "http://foo.remote.com/"; 322 let tabs = Array(6) 323 .fill(0) 324 .map((e, i) => ({ 325 urlHistory: [`${url}${i}`], 326 title: "A title", 327 lastUsed: Math.floor(Date.now() / 1000) - i * 86400, // i days ago. 328 })); 329 // First set up Sync to have the page as a remote tab. 330 configureEngine([{ id: "mobile", tabs }]); 331 332 // Register the page as an open tab. 333 let openTabUrl = url + "openpage/"; 334 let uri = Services.io.newURI(openTabUrl); 335 await PlacesTestUtils.addVisits([{ uri, title: "An Example" }]); 336 await addOpenPages(uri, 1); 337 338 // Also add a local history result. 339 let historyUrl = url + "history/"; 340 await PlacesTestUtils.addVisits(historyUrl); 341 342 let query = "rem"; 343 let context = createContext(query, { isPrivate: false }); 344 await check_results({ 345 context, 346 matches: [ 347 makeSearchResult(context, { 348 engineName: SUGGESTIONS_ENGINE_NAME, 349 heuristic: true, 350 }), 351 makeRemoteTabResult(context, { 352 uri: "http://foo.remote.com/0", 353 device: "My Phone", 354 title: "A title", 355 lastUsed: tabs[0].lastUsed, 356 }), 357 makeRemoteTabResult(context, { 358 uri: "http://foo.remote.com/1", 359 device: "My Phone", 360 title: "A title", 361 lastUsed: tabs[1].lastUsed, 362 }), 363 makeRemoteTabResult(context, { 364 uri: "http://foo.remote.com/2", 365 device: "My Phone", 366 title: "A title", 367 lastUsed: tabs[2].lastUsed, 368 }), 369 makeRemoteTabResult(context, { 370 uri: "http://foo.remote.com/3", 371 device: "My Phone", 372 title: "A title", 373 lastUsed: tabs[3].lastUsed, 374 }), 375 makeRemoteTabResult(context, { 376 uri: "http://foo.remote.com/4", 377 device: "My Phone", 378 title: "A title", 379 lastUsed: tabs[4].lastUsed, 380 }), 381 makeRemoteTabResult(context, { 382 uri: "http://foo.remote.com/5", 383 device: "My Phone", 384 title: "A title", 385 lastUsed: tabs[5].lastUsed, 386 }), 387 makeVisitResult(context, { 388 uri: historyUrl, 389 title: "test visit for " + historyUrl, 390 }), 391 makeTabSwitchResult(context, { 392 uri: openTabUrl, 393 title: "An Example", 394 }), 395 ], 396 }); 397 await removeOpenPages(uri, 1); 398 await cleanupPlaces(); 399 }); 400 401 add_task(async function test_many_remotetab_results() { 402 let url = "http://foo.remote.com/"; 403 let tabs = Array(8) 404 .fill(0) 405 .map((e, i) => ({ 406 urlHistory: [`${url}${i}`], 407 title: "A title", 408 lastUsed: Math.floor(Date.now() / 1000) - i * 86400, // i days old. 409 })); 410 411 // First set up Sync to have the page as a remote tab. 412 configureEngine([ 413 { 414 id: "mobile", 415 tabs, 416 }, 417 ]); 418 419 let query = "rem"; 420 let context = createContext(query, { isPrivate: false }); 421 await check_results({ 422 context, 423 matches: [ 424 makeSearchResult(context, { 425 engineName: SUGGESTIONS_ENGINE_NAME, 426 heuristic: true, 427 }), 428 makeRemoteTabResult(context, { 429 uri: "http://foo.remote.com/0", 430 device: "My Phone", 431 title: "A title", 432 lastUsed: tabs[0].lastUsed, 433 }), 434 makeRemoteTabResult(context, { 435 uri: "http://foo.remote.com/1", 436 device: "My Phone", 437 title: "A title", 438 lastUsed: tabs[1].lastUsed, 439 }), 440 makeRemoteTabResult(context, { 441 uri: "http://foo.remote.com/2", 442 device: "My Phone", 443 title: "A title", 444 lastUsed: tabs[2].lastUsed, 445 }), 446 makeRemoteTabResult(context, { 447 uri: "http://foo.remote.com/3", 448 device: "My Phone", 449 title: "A title", 450 lastUsed: tabs[3].lastUsed, 451 }), 452 makeRemoteTabResult(context, { 453 uri: "http://foo.remote.com/4", 454 device: "My Phone", 455 title: "A title", 456 lastUsed: tabs[4].lastUsed, 457 }), 458 makeRemoteTabResult(context, { 459 uri: "http://foo.remote.com/5", 460 device: "My Phone", 461 title: "A title", 462 lastUsed: tabs[5].lastUsed, 463 }), 464 makeRemoteTabResult(context, { 465 uri: "http://foo.remote.com/6", 466 device: "My Phone", 467 title: "A title", 468 lastUsed: tabs[6].lastUsed, 469 }), 470 makeRemoteTabResult(context, { 471 uri: "http://foo.remote.com/7", 472 device: "My Phone", 473 title: "A title", 474 lastUsed: tabs[7].lastUsed, 475 }), 476 ], 477 }); 478 }); 479 480 add_task(async function multiple_clients() { 481 let url = "http://foo.remote.com/"; 482 let mobileTabs = Array(2) 483 .fill(0) 484 .map((e, i) => ({ 485 urlHistory: [`${url}mobile/${i}`], 486 lastUsed: Date.now() / 1000 - 4 * 86400, // 4 days old (past threshold) 487 })); 488 489 let desktopTabs = Array(3) 490 .fill(0) 491 .map((e, i) => ({ 492 urlHistory: [`${url}desktop/${i}`], 493 lastUsed: Date.now() / 1000 - 1, // Fresh tabs 494 })); 495 496 // mobileTabs has the most recent tab, making it the most recent client. The 497 // rest of its tabs are stale. The tabs in desktopTabs are fresh, but not 498 // as fresh as the most recent tab in mobileTab. 499 mobileTabs.push({ 500 urlHistory: [`${url}mobile/fresh`], 501 lastUsed: Date.now() / 1000, 502 }); 503 504 configureEngine([ 505 { 506 id: "mobile", 507 tabs: mobileTabs, 508 }, 509 { 510 id: "desktop", 511 tabs: desktopTabs, 512 }, 513 ]); 514 515 // We expect that we will show the recent tab from mobileTabs, then all the 516 // tabs from desktopTabs, then the remaining tabs from mobileTabs. 517 let query = "rem"; 518 let context = createContext(query, { isPrivate: false }); 519 await check_results({ 520 context, 521 matches: [ 522 makeSearchResult(context, { 523 engineName: SUGGESTIONS_ENGINE_NAME, 524 heuristic: true, 525 }), 526 makeRemoteTabResult(context, { 527 uri: "http://foo.remote.com/mobile/fresh", 528 device: "My Phone", 529 lastUsed: mobileTabs[2].lastUsed, 530 }), 531 makeRemoteTabResult(context, { 532 uri: "http://foo.remote.com/desktop/0", 533 device: "My Desktop", 534 lastUsed: desktopTabs[0].lastUsed, 535 }), 536 makeRemoteTabResult(context, { 537 uri: "http://foo.remote.com/desktop/1", 538 device: "My Desktop", 539 lastUsed: desktopTabs[1].lastUsed, 540 }), 541 makeRemoteTabResult(context, { 542 uri: "http://foo.remote.com/desktop/2", 543 device: "My Desktop", 544 lastUsed: desktopTabs[2].lastUsed, 545 }), 546 makeRemoteTabResult(context, { 547 uri: "http://foo.remote.com/mobile/0", 548 device: "My Phone", 549 lastUsed: mobileTabs[0].lastUsed, 550 }), 551 makeRemoteTabResult(context, { 552 uri: "http://foo.remote.com/mobile/1", 553 device: "My Phone", 554 lastUsed: mobileTabs[1].lastUsed, 555 }), 556 ], 557 }); 558 }); 559 560 add_task(async function test_restrictionCharacter() { 561 let url = "http://foo.remote.com/"; 562 let tabs = Array(5) 563 .fill(0) 564 .map((e, i) => ({ 565 urlHistory: [`${url}${i}`], 566 title: "A title", 567 lastUsed: Math.floor(Date.now() / 1000) - i, 568 })); 569 configureEngine([ 570 { 571 id: "mobile", 572 tabs, 573 }, 574 ]); 575 576 // Also add an open page. 577 let openTabUrl = url + "openpage/"; 578 let uri = Services.io.newURI(openTabUrl); 579 await PlacesTestUtils.addVisits([{ uri, title: "An Example" }]); 580 await addOpenPages(uri, 1); 581 582 // We expect the open tab to flex to the bottom. 583 let query = UrlbarTokenizer.RESTRICT.OPENPAGE; 584 let context = createContext(query, { isPrivate: false }); 585 await check_results({ 586 context, 587 matches: [ 588 makeSearchResult(context, { 589 engineName: SUGGESTIONS_ENGINE_NAME, 590 heuristic: true, 591 }), 592 makeRemoteTabResult(context, { 593 uri: "http://foo.remote.com/0", 594 device: "My Phone", 595 title: "A title", 596 lastUsed: tabs[0].lastUsed, 597 }), 598 makeRemoteTabResult(context, { 599 uri: "http://foo.remote.com/1", 600 device: "My Phone", 601 title: "A title", 602 lastUsed: tabs[1].lastUsed, 603 }), 604 makeRemoteTabResult(context, { 605 uri: "http://foo.remote.com/2", 606 device: "My Phone", 607 title: "A title", 608 lastUsed: tabs[2].lastUsed, 609 }), 610 makeRemoteTabResult(context, { 611 uri: "http://foo.remote.com/3", 612 device: "My Phone", 613 title: "A title", 614 lastUsed: tabs[3].lastUsed, 615 }), 616 makeRemoteTabResult(context, { 617 uri: "http://foo.remote.com/4", 618 device: "My Phone", 619 title: "A title", 620 lastUsed: tabs[4].lastUsed, 621 }), 622 makeTabSwitchResult(context, { 623 uri: openTabUrl, 624 title: "An Example", 625 }), 626 ], 627 }); 628 await removeOpenPages(uri, 1); 629 await cleanupPlaces(); 630 }); 631 632 add_task(async function test_duplicate_remote_tabs() { 633 let url = "http://foo.remote.com/"; 634 let tabs = Array(3) 635 .fill(0) 636 .map(() => ({ 637 urlHistory: [url], 638 title: "A title", 639 lastUsed: Math.floor(Date.now() / 1000), 640 })); 641 configureEngine([ 642 { 643 id: "mobile", 644 tabs, 645 }, 646 ]); 647 648 // We expect the duplicate tabs to be deduped. 649 let query = UrlbarTokenizer.RESTRICT.OPENPAGE; 650 let context = createContext(query, { isPrivate: false }); 651 await check_results({ 652 context, 653 matches: [ 654 makeSearchResult(context, { 655 engineName: SUGGESTIONS_ENGINE_NAME, 656 heuristic: true, 657 }), 658 makeRemoteTabResult(context, { 659 uri: "http://foo.remote.com/", 660 device: "My Phone", 661 title: "A title", 662 lastUsed: tabs[0].lastUsed, 663 }), 664 ], 665 }); 666 });