test_quicksuggest_impressionCaps.js (93778B)
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 // Tests impression frequency capping for quick suggest results. 6 7 "use strict"; 8 9 ChromeUtils.defineESModuleGetters(this, { 10 AsyncShutdown: "resource://gre/modules/AsyncShutdown.sys.mjs", 11 setTimeout: "resource://gre/modules/Timer.sys.mjs", 12 }); 13 14 const REMOTE_SETTINGS_RESULTS = [ 15 { 16 id: 1, 17 url: "http://example.com/sponsored", 18 title: "Sponsored suggestion", 19 keywords: ["sponsored"], 20 click_url: "http://example.com/click", 21 impression_url: "http://example.com/impression", 22 advertiser: "TestAdvertiser", 23 iab_category: "22 - Shopping", 24 }, 25 { 26 id: 2, 27 url: "http://example.com/nonsponsored", 28 title: "Non-sponsored suggestion", 29 keywords: ["nonsponsored"], 30 click_url: "http://example.com/click", 31 impression_url: "http://example.com/impression", 32 advertiser: "TestAdvertiser", 33 iab_category: "5 - Education", 34 }, 35 ]; 36 37 const EXPECTED_SPONSORED_URLBAR_RESULT = { 38 type: UrlbarUtils.RESULT_TYPE.URL, 39 source: UrlbarUtils.RESULT_SOURCE.SEARCH, 40 heuristic: false, 41 payload: { 42 telemetryType: "adm_sponsored", 43 url: "http://example.com/sponsored", 44 originalUrl: "http://example.com/sponsored", 45 title: "Sponsored suggestion", 46 icon: null, 47 isSponsored: true, 48 sponsoredImpressionUrl: "http://example.com/impression", 49 sponsoredClickUrl: "http://example.com/click", 50 sponsoredBlockId: 1, 51 sponsoredAdvertiser: "TestAdvertiser", 52 sponsoredIabCategory: "22 - Shopping", 53 descriptionL10n: { id: "urlbar-result-action-sponsored" }, 54 helpUrl: QuickSuggest.HELP_URL, 55 helpL10n: { 56 id: "urlbar-result-menu-learn-more-about-firefox-suggest", 57 }, 58 isBlockable: true, 59 source: "remote-settings", 60 provider: "AdmWikipedia", 61 }, 62 }; 63 64 const EXPECTED_NONSPONSORED_URLBAR_RESULT = { 65 type: UrlbarUtils.RESULT_TYPE.URL, 66 source: UrlbarUtils.RESULT_SOURCE.SEARCH, 67 heuristic: false, 68 payload: { 69 telemetryType: "adm_nonsponsored", 70 url: "http://example.com/nonsponsored", 71 originalUrl: "http://example.com/nonsponsored", 72 title: "Non-sponsored suggestion", 73 icon: null, 74 isSponsored: false, 75 sponsoredImpressionUrl: "http://example.com/impression", 76 sponsoredClickUrl: "http://example.com/click", 77 sponsoredBlockId: 2, 78 sponsoredAdvertiser: "TestAdvertiser", 79 sponsoredIabCategory: "5 - Education", 80 helpUrl: QuickSuggest.HELP_URL, 81 helpL10n: { 82 id: "urlbar-result-menu-learn-more-about-firefox-suggest", 83 }, 84 isBlockable: true, 85 source: "remote-settings", 86 provider: "AdmWikipedia", 87 }, 88 }; 89 90 let gSandbox; 91 let gDateNowStub; 92 let gStartupDateMsStub; 93 94 add_setup(async () => { 95 // Disable search suggestions so we don't hit the network. 96 Services.prefs.setBoolPref("browser.search.suggest.enabled", false); 97 98 await QuickSuggestTestUtils.ensureQuickSuggestInit({ 99 remoteSettingsRecords: [ 100 { 101 type: "data", 102 attachment: REMOTE_SETTINGS_RESULTS, 103 }, 104 ], 105 prefs: [ 106 ["quicksuggest.impressionCaps.sponsoredEnabled", true], 107 ["quicksuggest.impressionCaps.nonSponsoredEnabled", true], 108 ["suggest.quicksuggest.all", true], 109 ["suggest.quicksuggest.sponsored", true], 110 ], 111 }); 112 113 // Set up a sinon stub for the `Date.now()` implementation inside of 114 // UrlbarProviderQuickSuggest. This lets us test searches performed at 115 // specific times. See `doTimedCallbacks()` for more info. 116 gSandbox = sinon.createSandbox(); 117 gDateNowStub = gSandbox.stub( 118 Cu.getGlobalForObject(UrlbarProviderQuickSuggest).Date, 119 "now" 120 ); 121 122 // Set up a sinon stub for `UrlbarProviderQuickSuggest._getStartupDateMs()` to 123 // let the test override the startup date. 124 gStartupDateMsStub = gSandbox.stub( 125 QuickSuggest.impressionCaps, 126 "_getStartupDateMs" 127 ); 128 gStartupDateMsStub.returns(0); 129 }); 130 131 // Tests a single interval. 132 add_task(async function oneInterval() { 133 await doTest({ 134 config: { 135 impression_caps: { 136 sponsored: { 137 custom: [{ interval_s: 3, max_count: 1 }], 138 }, 139 }, 140 }, 141 callback: async () => { 142 await doTimedSearches("sponsored", { 143 0: { 144 results: [[EXPECTED_SPONSORED_URLBAR_RESULT], []], 145 telemetry: { 146 events: [ 147 { 148 object: "hit", 149 extra: { 150 eventDate: "0", 151 intervalSeconds: "3", 152 maxCount: "1", 153 startDate: "0", 154 impressionDate: "0", 155 count: "1", 156 type: "sponsored", 157 eventCount: "1", 158 }, 159 }, 160 ], 161 }, 162 }, 163 1: { 164 results: [[]], 165 }, 166 2: { 167 results: [[]], 168 }, 169 3: { 170 results: [[EXPECTED_SPONSORED_URLBAR_RESULT], []], 171 telemetry: { 172 events: [ 173 { 174 object: "reset", 175 extra: { 176 eventDate: "3000", 177 intervalSeconds: "3", 178 maxCount: "1", 179 startDate: "0", 180 impressionDate: "0", 181 count: "1", 182 type: "sponsored", 183 eventCount: "1", 184 }, 185 }, 186 { 187 object: "hit", 188 extra: { 189 eventDate: "3000", 190 intervalSeconds: "3", 191 maxCount: "1", 192 startDate: "3000", 193 impressionDate: "3000", 194 count: "1", 195 type: "sponsored", 196 eventCount: "1", 197 }, 198 }, 199 ], 200 }, 201 }, 202 4: { 203 results: [[]], 204 }, 205 5: { 206 results: [[]], 207 }, 208 }); 209 }, 210 }); 211 }); 212 213 // Tests multiple intervals. 214 add_task(async function multipleIntervals() { 215 await doTest({ 216 config: { 217 impression_caps: { 218 sponsored: { 219 custom: [ 220 { interval_s: 1, max_count: 1 }, 221 { interval_s: 5, max_count: 3 }, 222 { interval_s: 10, max_count: 5 }, 223 ], 224 }, 225 }, 226 }, 227 callback: async () => { 228 await doTimedSearches("sponsored", { 229 // 0s: 1 new impression; 1 impression total 230 0: { 231 results: [[EXPECTED_SPONSORED_URLBAR_RESULT], []], 232 telemetry: { 233 events: [ 234 // hit: interval_s: 1, max_count: 1 235 { 236 object: "hit", 237 extra: { 238 eventDate: "0", 239 intervalSeconds: "1", 240 maxCount: "1", 241 startDate: "0", 242 impressionDate: "0", 243 count: "1", 244 type: "sponsored", 245 eventCount: "1", 246 }, 247 }, 248 ], 249 }, 250 }, 251 // 1s: 1 new impression; 2 impressions total 252 1: { 253 results: [[EXPECTED_SPONSORED_URLBAR_RESULT], []], 254 telemetry: { 255 events: [ 256 // reset: interval_s: 1, max_count: 1 257 { 258 object: "reset", 259 extra: { 260 eventDate: "1000", 261 intervalSeconds: "1", 262 maxCount: "1", 263 startDate: "0", 264 impressionDate: "0", 265 count: "1", 266 type: "sponsored", 267 eventCount: "1", 268 }, 269 }, 270 // hit: interval_s: 1, max_count: 1 271 { 272 object: "hit", 273 extra: { 274 eventDate: "1000", 275 intervalSeconds: "1", 276 maxCount: "1", 277 startDate: "1000", 278 impressionDate: "1000", 279 count: "1", 280 type: "sponsored", 281 eventCount: "1", 282 }, 283 }, 284 ], 285 }, 286 }, 287 // 2s: 1 new impression; 3 impressions total 288 2: { 289 results: [[EXPECTED_SPONSORED_URLBAR_RESULT], []], 290 telemetry: { 291 events: [ 292 // reset: interval_s: 1, max_count: 1 293 { 294 object: "reset", 295 extra: { 296 eventDate: "2000", 297 intervalSeconds: "1", 298 maxCount: "1", 299 startDate: "1000", 300 impressionDate: "1000", 301 count: "1", 302 type: "sponsored", 303 eventCount: "1", 304 }, 305 }, 306 // hit: interval_s: 1, max_count: 1 307 { 308 object: "hit", 309 extra: { 310 eventDate: "2000", 311 intervalSeconds: "1", 312 maxCount: "1", 313 startDate: "2000", 314 impressionDate: "2000", 315 count: "1", 316 type: "sponsored", 317 eventCount: "1", 318 }, 319 }, 320 // hit: interval_s: 5, max_count: 3 321 { 322 object: "hit", 323 extra: { 324 eventDate: "2000", 325 intervalSeconds: "5", 326 maxCount: "3", 327 startDate: "0", 328 impressionDate: "2000", 329 count: "3", 330 type: "sponsored", 331 eventCount: "1", 332 }, 333 }, 334 ], 335 }, 336 }, 337 // 3s: no new impressions; 3 impressions total 338 3: { 339 results: [[]], 340 telemetry: { 341 events: [ 342 // reset: interval_s: 1, max_count: 1 343 { 344 object: "reset", 345 extra: { 346 eventDate: "3000", 347 intervalSeconds: "1", 348 maxCount: "1", 349 startDate: "2000", 350 impressionDate: "2000", 351 count: "1", 352 type: "sponsored", 353 eventCount: "1", 354 }, 355 }, 356 ], 357 }, 358 }, 359 // 4s: no new impressions; 3 impressions total 360 4: { 361 results: [[]], 362 telemetry: { 363 events: [ 364 // reset: interval_s: 1, max_count: 1 365 { 366 object: "reset", 367 extra: { 368 eventDate: "4000", 369 intervalSeconds: "1", 370 maxCount: "1", 371 startDate: "3000", 372 impressionDate: "2000", 373 count: "0", 374 type: "sponsored", 375 eventCount: "1", 376 }, 377 }, 378 ], 379 }, 380 }, 381 // 5s: 1 new impression; 4 impressions total 382 5: { 383 results: [[EXPECTED_SPONSORED_URLBAR_RESULT], []], 384 telemetry: { 385 events: [ 386 // reset: interval_s: 1, max_count: 1 387 { 388 object: "reset", 389 extra: { 390 eventDate: "5000", 391 intervalSeconds: "1", 392 maxCount: "1", 393 startDate: "4000", 394 impressionDate: "2000", 395 count: "0", 396 type: "sponsored", 397 eventCount: "1", 398 }, 399 }, 400 // reset: interval_s: 5, max_count: 3 401 { 402 object: "reset", 403 extra: { 404 eventDate: "5000", 405 intervalSeconds: "5", 406 maxCount: "3", 407 startDate: "0", 408 impressionDate: "2000", 409 count: "3", 410 type: "sponsored", 411 eventCount: "1", 412 }, 413 }, 414 // hit: interval_s: 1, max_count: 1 415 { 416 object: "hit", 417 extra: { 418 eventDate: "5000", 419 intervalSeconds: "1", 420 maxCount: "1", 421 startDate: "5000", 422 impressionDate: "5000", 423 count: "1", 424 type: "sponsored", 425 eventCount: "1", 426 }, 427 }, 428 ], 429 }, 430 }, 431 // 6s: 1 new impression; 5 impressions total 432 6: { 433 results: [[EXPECTED_SPONSORED_URLBAR_RESULT], []], 434 telemetry: { 435 events: [ 436 // reset: interval_s: 1, max_count: 1 437 { 438 object: "reset", 439 extra: { 440 eventDate: "6000", 441 intervalSeconds: "1", 442 maxCount: "1", 443 startDate: "5000", 444 impressionDate: "5000", 445 count: "1", 446 type: "sponsored", 447 eventCount: "1", 448 }, 449 }, 450 // hit: interval_s: 1, max_count: 1 451 { 452 object: "hit", 453 extra: { 454 eventDate: "6000", 455 intervalSeconds: "1", 456 maxCount: "1", 457 startDate: "6000", 458 impressionDate: "6000", 459 count: "1", 460 type: "sponsored", 461 eventCount: "1", 462 }, 463 }, 464 // hit: interval_s: 10, max_count: 5 465 { 466 object: "hit", 467 extra: { 468 eventDate: "6000", 469 intervalSeconds: "10", 470 maxCount: "5", 471 startDate: "0", 472 impressionDate: "6000", 473 count: "5", 474 type: "sponsored", 475 eventCount: "1", 476 }, 477 }, 478 ], 479 }, 480 }, 481 // 7s: no new impressions; 5 impressions total 482 7: { 483 results: [[]], 484 telemetry: { 485 events: [ 486 // reset: interval_s: 1, max_count: 1 487 { 488 object: "reset", 489 extra: { 490 eventDate: "7000", 491 intervalSeconds: "1", 492 maxCount: "1", 493 startDate: "6000", 494 impressionDate: "6000", 495 count: "1", 496 type: "sponsored", 497 eventCount: "1", 498 }, 499 }, 500 ], 501 }, 502 }, 503 // 8s: no new impressions; 5 impressions total 504 8: { 505 results: [[]], 506 telemetry: { 507 events: [ 508 // reset: interval_s: 1, max_count: 1 509 { 510 object: "reset", 511 extra: { 512 eventDate: "8000", 513 intervalSeconds: "1", 514 maxCount: "1", 515 startDate: "7000", 516 impressionDate: "6000", 517 count: "0", 518 type: "sponsored", 519 eventCount: "1", 520 }, 521 }, 522 ], 523 }, 524 }, 525 // 9s: no new impressions; 5 impressions total 526 9: { 527 results: [[]], 528 telemetry: { 529 events: [ 530 // reset: interval_s: 1, max_count: 1 531 { 532 object: "reset", 533 extra: { 534 eventDate: "9000", 535 intervalSeconds: "1", 536 maxCount: "1", 537 startDate: "8000", 538 impressionDate: "6000", 539 count: "0", 540 type: "sponsored", 541 eventCount: "1", 542 }, 543 }, 544 ], 545 }, 546 }, 547 // 10s: 1 new impression; 6 impressions total 548 10: { 549 results: [[EXPECTED_SPONSORED_URLBAR_RESULT], []], 550 telemetry: { 551 events: [ 552 // reset: interval_s: 1, max_count: 1 553 { 554 object: "reset", 555 extra: { 556 eventDate: "10000", 557 intervalSeconds: "1", 558 maxCount: "1", 559 startDate: "9000", 560 impressionDate: "6000", 561 count: "0", 562 type: "sponsored", 563 eventCount: "1", 564 }, 565 }, 566 // reset: interval_s: 5, max_count: 3 567 { 568 object: "reset", 569 extra: { 570 eventDate: "10000", 571 intervalSeconds: "5", 572 maxCount: "3", 573 startDate: "5000", 574 impressionDate: "6000", 575 count: "2", 576 type: "sponsored", 577 eventCount: "1", 578 }, 579 }, 580 // reset: interval_s: 10, max_count: 5 581 { 582 object: "reset", 583 extra: { 584 eventDate: "10000", 585 intervalSeconds: "10", 586 maxCount: "5", 587 startDate: "0", 588 impressionDate: "6000", 589 count: "5", 590 type: "sponsored", 591 eventCount: "1", 592 }, 593 }, 594 // hit: interval_s: 1, max_count: 1 595 { 596 object: "hit", 597 extra: { 598 eventDate: "10000", 599 intervalSeconds: "1", 600 maxCount: "1", 601 startDate: "10000", 602 impressionDate: "10000", 603 count: "1", 604 type: "sponsored", 605 eventCount: "1", 606 }, 607 }, 608 ], 609 }, 610 }, 611 // 11s: 1 new impression; 7 impressions total 612 11: { 613 results: [[EXPECTED_SPONSORED_URLBAR_RESULT], []], 614 telemetry: { 615 events: [ 616 // reset: interval_s: 1, max_count: 1 617 { 618 object: "reset", 619 extra: { 620 eventDate: "11000", 621 intervalSeconds: "1", 622 maxCount: "1", 623 startDate: "10000", 624 impressionDate: "10000", 625 count: "1", 626 type: "sponsored", 627 eventCount: "1", 628 }, 629 }, 630 // hit: interval_s: 1, max_count: 1 631 { 632 object: "hit", 633 extra: { 634 eventDate: "11000", 635 intervalSeconds: "1", 636 maxCount: "1", 637 startDate: "11000", 638 impressionDate: "11000", 639 count: "1", 640 type: "sponsored", 641 eventCount: "1", 642 }, 643 }, 644 ], 645 }, 646 }, 647 // 12s: 1 new impression; 8 impressions total 648 12: { 649 results: [[EXPECTED_SPONSORED_URLBAR_RESULT], []], 650 telemetry: { 651 events: [ 652 // reset: interval_s: 1, max_count: 1 653 { 654 object: "reset", 655 extra: { 656 eventDate: "12000", 657 intervalSeconds: "1", 658 maxCount: "1", 659 startDate: "11000", 660 impressionDate: "11000", 661 count: "1", 662 type: "sponsored", 663 eventCount: "1", 664 }, 665 }, 666 // hit: interval_s: 1, max_count: 1 667 { 668 object: "hit", 669 extra: { 670 eventDate: "12000", 671 intervalSeconds: "1", 672 maxCount: "1", 673 startDate: "12000", 674 impressionDate: "12000", 675 count: "1", 676 type: "sponsored", 677 eventCount: "1", 678 }, 679 }, 680 // hit: interval_s: 5, max_count: 3 681 { 682 object: "hit", 683 extra: { 684 eventDate: "12000", 685 intervalSeconds: "5", 686 maxCount: "3", 687 startDate: "10000", 688 impressionDate: "12000", 689 count: "3", 690 type: "sponsored", 691 eventCount: "1", 692 }, 693 }, 694 ], 695 }, 696 }, 697 // 13s: no new impressions; 8 impressions total 698 13: { 699 results: [[]], 700 telemetry: { 701 events: [ 702 // reset: interval_s: 1, max_count: 1 703 { 704 object: "reset", 705 extra: { 706 eventDate: "13000", 707 intervalSeconds: "1", 708 maxCount: "1", 709 startDate: "12000", 710 impressionDate: "12000", 711 count: "1", 712 type: "sponsored", 713 eventCount: "1", 714 }, 715 }, 716 ], 717 }, 718 }, 719 // 14s: no new impressions; 8 impressions total 720 14: { 721 results: [[]], 722 telemetry: { 723 events: [ 724 // reset: interval_s: 1, max_count: 1 725 { 726 object: "reset", 727 extra: { 728 eventDate: "14000", 729 intervalSeconds: "1", 730 maxCount: "1", 731 startDate: "13000", 732 impressionDate: "12000", 733 count: "0", 734 type: "sponsored", 735 eventCount: "1", 736 }, 737 }, 738 ], 739 }, 740 }, 741 // 15s: 1 new impression; 9 impressions total 742 15: { 743 results: [[EXPECTED_SPONSORED_URLBAR_RESULT], []], 744 telemetry: { 745 events: [ 746 // reset: interval_s: 1, max_count: 1 747 { 748 object: "reset", 749 extra: { 750 eventDate: "15000", 751 intervalSeconds: "1", 752 maxCount: "1", 753 startDate: "14000", 754 impressionDate: "12000", 755 count: "0", 756 type: "sponsored", 757 eventCount: "1", 758 }, 759 }, 760 // reset: interval_s: 5, max_count: 3 761 { 762 object: "reset", 763 extra: { 764 eventDate: "15000", 765 intervalSeconds: "5", 766 maxCount: "3", 767 startDate: "10000", 768 impressionDate: "12000", 769 count: "3", 770 type: "sponsored", 771 eventCount: "1", 772 }, 773 }, 774 // hit: interval_s: 1, max_count: 1 775 { 776 object: "hit", 777 extra: { 778 eventDate: "15000", 779 intervalSeconds: "1", 780 maxCount: "1", 781 startDate: "15000", 782 impressionDate: "15000", 783 count: "1", 784 type: "sponsored", 785 eventCount: "1", 786 }, 787 }, 788 ], 789 }, 790 }, 791 // 16s: 1 new impression; 10 impressions total 792 16: { 793 results: [[EXPECTED_SPONSORED_URLBAR_RESULT], []], 794 telemetry: { 795 events: [ 796 // reset: interval_s: 1, max_count: 1 797 { 798 object: "reset", 799 extra: { 800 eventDate: "16000", 801 intervalSeconds: "1", 802 maxCount: "1", 803 startDate: "15000", 804 impressionDate: "15000", 805 count: "1", 806 type: "sponsored", 807 eventCount: "1", 808 }, 809 }, 810 // hit: interval_s: 1, max_count: 1 811 { 812 object: "hit", 813 extra: { 814 eventDate: "16000", 815 intervalSeconds: "1", 816 maxCount: "1", 817 startDate: "16000", 818 impressionDate: "16000", 819 count: "1", 820 type: "sponsored", 821 eventCount: "1", 822 }, 823 }, 824 // hit: interval_s: 10, max_count: 5 825 { 826 object: "hit", 827 extra: { 828 eventDate: "16000", 829 intervalSeconds: "10", 830 maxCount: "5", 831 startDate: "10000", 832 impressionDate: "16000", 833 count: "5", 834 type: "sponsored", 835 eventCount: "1", 836 }, 837 }, 838 ], 839 }, 840 }, 841 // 17s: no new impressions; 10 impressions total 842 17: { 843 results: [[]], 844 telemetry: { 845 events: [ 846 // reset: interval_s: 1, max_count: 1 847 { 848 object: "reset", 849 extra: { 850 eventDate: "17000", 851 intervalSeconds: "1", 852 maxCount: "1", 853 startDate: "16000", 854 impressionDate: "16000", 855 count: "1", 856 type: "sponsored", 857 eventCount: "1", 858 }, 859 }, 860 ], 861 }, 862 }, 863 // 18s: no new impressions; 10 impressions total 864 18: { 865 results: [[]], 866 telemetry: { 867 events: [ 868 // reset: interval_s: 1, max_count: 1 869 { 870 object: "reset", 871 extra: { 872 eventDate: "18000", 873 intervalSeconds: "1", 874 maxCount: "1", 875 startDate: "17000", 876 impressionDate: "16000", 877 count: "0", 878 type: "sponsored", 879 eventCount: "1", 880 }, 881 }, 882 ], 883 }, 884 }, 885 // 19s: no new impressions; 10 impressions total 886 19: { 887 results: [[]], 888 telemetry: { 889 events: [ 890 // reset: interval_s: 1, max_count: 1 891 { 892 object: "reset", 893 extra: { 894 eventDate: "19000", 895 intervalSeconds: "1", 896 maxCount: "1", 897 startDate: "18000", 898 impressionDate: "16000", 899 count: "0", 900 type: "sponsored", 901 eventCount: "1", 902 }, 903 }, 904 ], 905 }, 906 }, 907 // 20s: 1 new impression; 11 impressions total 908 20: { 909 results: [[EXPECTED_SPONSORED_URLBAR_RESULT], []], 910 telemetry: { 911 events: [ 912 // reset: interval_s: 1, max_count: 1 913 { 914 object: "reset", 915 extra: { 916 eventDate: "20000", 917 intervalSeconds: "1", 918 maxCount: "1", 919 startDate: "19000", 920 impressionDate: "16000", 921 count: "0", 922 type: "sponsored", 923 eventCount: "1", 924 }, 925 }, 926 // reset: interval_s: 5, max_count: 3 927 { 928 object: "reset", 929 extra: { 930 eventDate: "20000", 931 intervalSeconds: "5", 932 maxCount: "3", 933 startDate: "15000", 934 impressionDate: "16000", 935 count: "2", 936 type: "sponsored", 937 eventCount: "1", 938 }, 939 }, 940 // reset: interval_s: 10, max_count: 5 941 { 942 object: "reset", 943 extra: { 944 eventDate: "20000", 945 intervalSeconds: "10", 946 maxCount: "5", 947 startDate: "10000", 948 impressionDate: "16000", 949 count: "5", 950 type: "sponsored", 951 eventCount: "1", 952 }, 953 }, 954 // hit: interval_s: 1, max_count: 1 955 { 956 object: "hit", 957 extra: { 958 eventDate: "20000", 959 intervalSeconds: "1", 960 maxCount: "1", 961 startDate: "20000", 962 impressionDate: "20000", 963 count: "1", 964 type: "sponsored", 965 eventCount: "1", 966 }, 967 }, 968 ], 969 }, 970 }, 971 }); 972 }, 973 }); 974 }); 975 976 // Tests a lifetime cap. 977 add_task(async function lifetime() { 978 await doTest({ 979 config: { 980 impression_caps: { 981 sponsored: { 982 lifetime: 3, 983 }, 984 }, 985 }, 986 callback: async () => { 987 await doTimedSearches("sponsored", { 988 0: { 989 results: [ 990 [EXPECTED_SPONSORED_URLBAR_RESULT], 991 [EXPECTED_SPONSORED_URLBAR_RESULT], 992 [EXPECTED_SPONSORED_URLBAR_RESULT], 993 [], 994 ], 995 telemetry: { 996 events: [ 997 { 998 object: "hit", 999 extra: { 1000 eventDate: "0", 1001 intervalSeconds: "Infinity", 1002 maxCount: "3", 1003 startDate: "0", 1004 impressionDate: "0", 1005 count: "3", 1006 type: "sponsored", 1007 eventCount: "1", 1008 }, 1009 }, 1010 ], 1011 }, 1012 }, 1013 1: { 1014 results: [[]], 1015 }, 1016 }); 1017 }, 1018 }); 1019 }); 1020 1021 // Tests one interval and a lifetime cap together. 1022 add_task(async function intervalAndLifetime() { 1023 await doTest({ 1024 config: { 1025 impression_caps: { 1026 sponsored: { 1027 lifetime: 3, 1028 custom: [{ interval_s: 1, max_count: 1 }], 1029 }, 1030 }, 1031 }, 1032 callback: async () => { 1033 await doTimedSearches("sponsored", { 1034 // 0s: 1 new impression; 1 impression total 1035 0: { 1036 results: [[EXPECTED_SPONSORED_URLBAR_RESULT], []], 1037 telemetry: { 1038 events: [ 1039 // hit: interval_s: 1, max_count: 1 1040 { 1041 object: "hit", 1042 extra: { 1043 eventDate: "0", 1044 intervalSeconds: "1", 1045 maxCount: "1", 1046 startDate: "0", 1047 impressionDate: "0", 1048 count: "1", 1049 type: "sponsored", 1050 eventCount: "1", 1051 }, 1052 }, 1053 ], 1054 }, 1055 }, 1056 // 1s: 1 new impression; 2 impressions total 1057 1: { 1058 results: [[EXPECTED_SPONSORED_URLBAR_RESULT], []], 1059 telemetry: { 1060 events: [ 1061 // reset: interval_s: 1, max_count: 1 1062 { 1063 object: "reset", 1064 extra: { 1065 eventDate: "1000", 1066 intervalSeconds: "1", 1067 maxCount: "1", 1068 startDate: "0", 1069 impressionDate: "0", 1070 count: "1", 1071 type: "sponsored", 1072 eventCount: "1", 1073 }, 1074 }, 1075 // hit: interval_s: 1, max_count: 1 1076 { 1077 object: "hit", 1078 extra: { 1079 eventDate: "1000", 1080 intervalSeconds: "1", 1081 maxCount: "1", 1082 startDate: "1000", 1083 impressionDate: "1000", 1084 count: "1", 1085 type: "sponsored", 1086 eventCount: "1", 1087 }, 1088 }, 1089 ], 1090 }, 1091 }, 1092 // 2s: 1 new impression; 3 impressions total 1093 2: { 1094 results: [[EXPECTED_SPONSORED_URLBAR_RESULT], []], 1095 telemetry: { 1096 events: [ 1097 // reset: interval_s: 1, max_count: 1 1098 { 1099 object: "reset", 1100 extra: { 1101 eventDate: "2000", 1102 intervalSeconds: "1", 1103 maxCount: "1", 1104 startDate: "1000", 1105 impressionDate: "1000", 1106 count: "1", 1107 type: "sponsored", 1108 eventCount: "1", 1109 }, 1110 }, 1111 // hit: interval_s: 1, max_count: 1 1112 { 1113 object: "hit", 1114 extra: { 1115 eventDate: "2000", 1116 intervalSeconds: "1", 1117 maxCount: "1", 1118 startDate: "2000", 1119 impressionDate: "2000", 1120 count: "1", 1121 type: "sponsored", 1122 eventCount: "1", 1123 }, 1124 }, 1125 // hit: interval_s: Infinity, max_count: 3 1126 { 1127 object: "hit", 1128 extra: { 1129 eventDate: "2000", 1130 intervalSeconds: "Infinity", 1131 maxCount: "3", 1132 startDate: "0", 1133 impressionDate: "2000", 1134 count: "3", 1135 type: "sponsored", 1136 eventCount: "1", 1137 }, 1138 }, 1139 ], 1140 }, 1141 }, 1142 3: { 1143 results: [[]], 1144 telemetry: { 1145 events: [ 1146 // reset: interval_s: 1, max_count: 1 1147 { 1148 object: "reset", 1149 extra: { 1150 eventDate: "3000", 1151 intervalSeconds: "1", 1152 maxCount: "1", 1153 startDate: "2000", 1154 impressionDate: "2000", 1155 count: "1", 1156 type: "sponsored", 1157 eventCount: "1", 1158 }, 1159 }, 1160 ], 1161 }, 1162 }, 1163 }); 1164 }, 1165 }); 1166 }); 1167 1168 // Tests multiple intervals and a lifetime cap together. 1169 add_task(async function multipleIntervalsAndLifetime() { 1170 await doTest({ 1171 config: { 1172 impression_caps: { 1173 sponsored: { 1174 lifetime: 4, 1175 custom: [ 1176 { interval_s: 1, max_count: 1 }, 1177 { interval_s: 5, max_count: 3 }, 1178 ], 1179 }, 1180 }, 1181 }, 1182 callback: async () => { 1183 await doTimedSearches("sponsored", { 1184 // 0s: 1 new impression; 1 impression total 1185 0: { 1186 results: [[EXPECTED_SPONSORED_URLBAR_RESULT], []], 1187 telemetry: { 1188 events: [ 1189 // hit: interval_s: 1, max_count: 1 1190 { 1191 object: "hit", 1192 extra: { 1193 eventDate: "0", 1194 intervalSeconds: "1", 1195 maxCount: "1", 1196 startDate: "0", 1197 impressionDate: "0", 1198 count: "1", 1199 type: "sponsored", 1200 eventCount: "1", 1201 }, 1202 }, 1203 ], 1204 }, 1205 }, 1206 // 1s: 1 new impression; 2 impressions total 1207 1: { 1208 results: [[EXPECTED_SPONSORED_URLBAR_RESULT], []], 1209 telemetry: { 1210 events: [ 1211 // reset: interval_s: 1, max_count: 1 1212 { 1213 object: "reset", 1214 extra: { 1215 eventDate: "1000", 1216 intervalSeconds: "1", 1217 maxCount: "1", 1218 startDate: "0", 1219 impressionDate: "0", 1220 count: "1", 1221 type: "sponsored", 1222 eventCount: "1", 1223 }, 1224 }, 1225 // hit: interval_s: 1, max_count: 1 1226 { 1227 object: "hit", 1228 extra: { 1229 eventDate: "1000", 1230 intervalSeconds: "1", 1231 maxCount: "1", 1232 startDate: "1000", 1233 impressionDate: "1000", 1234 count: "1", 1235 type: "sponsored", 1236 eventCount: "1", 1237 }, 1238 }, 1239 ], 1240 }, 1241 }, 1242 // 2s: 1 new impression; 3 impressions total 1243 2: { 1244 results: [[EXPECTED_SPONSORED_URLBAR_RESULT], []], 1245 telemetry: { 1246 events: [ 1247 // reset: interval_s: 1, max_count: 1 1248 { 1249 object: "reset", 1250 extra: { 1251 eventDate: "2000", 1252 intervalSeconds: "1", 1253 maxCount: "1", 1254 startDate: "1000", 1255 impressionDate: "1000", 1256 count: "1", 1257 type: "sponsored", 1258 eventCount: "1", 1259 }, 1260 }, 1261 // hit: interval_s: 1, max_count: 1 1262 { 1263 object: "hit", 1264 extra: { 1265 eventDate: "2000", 1266 intervalSeconds: "1", 1267 maxCount: "1", 1268 startDate: "2000", 1269 impressionDate: "2000", 1270 count: "1", 1271 type: "sponsored", 1272 eventCount: "1", 1273 }, 1274 }, 1275 // hit: interval_s: 5, max_count: 3 1276 { 1277 object: "hit", 1278 extra: { 1279 eventDate: "2000", 1280 intervalSeconds: "5", 1281 maxCount: "3", 1282 startDate: "0", 1283 impressionDate: "2000", 1284 count: "3", 1285 type: "sponsored", 1286 eventCount: "1", 1287 }, 1288 }, 1289 ], 1290 }, 1291 }, 1292 // 3s: no new impressions; 3 impressions total 1293 3: { 1294 results: [[]], 1295 telemetry: { 1296 events: [ 1297 // reset: interval_s: 1, max_count: 1 1298 { 1299 object: "reset", 1300 extra: { 1301 eventDate: "3000", 1302 intervalSeconds: "1", 1303 maxCount: "1", 1304 startDate: "2000", 1305 impressionDate: "2000", 1306 count: "1", 1307 type: "sponsored", 1308 eventCount: "1", 1309 }, 1310 }, 1311 ], 1312 }, 1313 }, 1314 // 4s: no new impressions; 3 impressions total 1315 4: { 1316 results: [[]], 1317 telemetry: { 1318 events: [ 1319 // reset: interval_s: 1, max_count: 1 1320 { 1321 object: "reset", 1322 extra: { 1323 eventDate: "4000", 1324 intervalSeconds: "1", 1325 maxCount: "1", 1326 startDate: "3000", 1327 impressionDate: "2000", 1328 count: "0", 1329 type: "sponsored", 1330 eventCount: "1", 1331 }, 1332 }, 1333 ], 1334 }, 1335 }, 1336 // 5s: 1 new impression; 4 impressions total 1337 5: { 1338 results: [[EXPECTED_SPONSORED_URLBAR_RESULT], []], 1339 telemetry: { 1340 events: [ 1341 // reset: interval_s: 1, max_count: 1 1342 { 1343 object: "reset", 1344 extra: { 1345 eventDate: "5000", 1346 intervalSeconds: "1", 1347 maxCount: "1", 1348 startDate: "4000", 1349 impressionDate: "2000", 1350 count: "0", 1351 type: "sponsored", 1352 eventCount: "1", 1353 }, 1354 }, 1355 // reset: interval_s: 5, max_count: 3 1356 { 1357 object: "reset", 1358 extra: { 1359 eventDate: "5000", 1360 intervalSeconds: "5", 1361 maxCount: "3", 1362 startDate: "0", 1363 impressionDate: "2000", 1364 count: "3", 1365 type: "sponsored", 1366 eventCount: "1", 1367 }, 1368 }, 1369 // hit: interval_s: 1, max_count: 1 1370 { 1371 object: "hit", 1372 extra: { 1373 eventDate: "5000", 1374 intervalSeconds: "1", 1375 maxCount: "1", 1376 startDate: "5000", 1377 impressionDate: "5000", 1378 count: "1", 1379 type: "sponsored", 1380 eventCount: "1", 1381 }, 1382 }, 1383 // hit: interval_s: Infinity, max_count: 4 1384 { 1385 object: "hit", 1386 extra: { 1387 eventDate: "5000", 1388 intervalSeconds: "Infinity", 1389 maxCount: "4", 1390 startDate: "0", 1391 impressionDate: "5000", 1392 count: "4", 1393 type: "sponsored", 1394 eventCount: "1", 1395 }, 1396 }, 1397 ], 1398 }, 1399 }, 1400 // 6s: no new impressions; 4 impressions total 1401 6: { 1402 results: [[]], 1403 telemetry: { 1404 events: [ 1405 // reset: interval_s: 1, max_count: 1 1406 { 1407 object: "reset", 1408 extra: { 1409 eventDate: "6000", 1410 intervalSeconds: "1", 1411 maxCount: "1", 1412 startDate: "5000", 1413 impressionDate: "5000", 1414 count: "1", 1415 type: "sponsored", 1416 eventCount: "1", 1417 }, 1418 }, 1419 ], 1420 }, 1421 }, 1422 // 7s: no new impressions; 4 impressions total 1423 7: { 1424 results: [[]], 1425 telemetry: { 1426 events: [ 1427 // reset: interval_s: 1, max_count: 1 1428 { 1429 object: "reset", 1430 extra: { 1431 eventDate: "7000", 1432 intervalSeconds: "1", 1433 maxCount: "1", 1434 startDate: "6000", 1435 impressionDate: "5000", 1436 count: "0", 1437 type: "sponsored", 1438 eventCount: "1", 1439 }, 1440 }, 1441 ], 1442 }, 1443 }, 1444 }); 1445 }, 1446 }); 1447 }); 1448 1449 // Smoke test for non-sponsored caps. Most tasks use sponsored results and caps, 1450 // but sponsored and non-sponsored should behave the same since they use the 1451 // same code paths. 1452 add_task(async function nonsponsored() { 1453 await doTest({ 1454 config: { 1455 impression_caps: { 1456 nonsponsored: { 1457 lifetime: 4, 1458 custom: [ 1459 { interval_s: 1, max_count: 1 }, 1460 { interval_s: 5, max_count: 3 }, 1461 ], 1462 }, 1463 }, 1464 }, 1465 callback: async () => { 1466 await doTimedSearches("nonsponsored", { 1467 // 0s: 1 new impression; 1 impression total 1468 0: { 1469 results: [[EXPECTED_NONSPONSORED_URLBAR_RESULT], []], 1470 telemetry: { 1471 events: [ 1472 // hit: interval_s: 1, max_count: 1 1473 { 1474 object: "hit", 1475 extra: { 1476 eventDate: "0", 1477 intervalSeconds: "1", 1478 maxCount: "1", 1479 startDate: "0", 1480 impressionDate: "0", 1481 count: "1", 1482 type: "nonsponsored", 1483 eventCount: "1", 1484 }, 1485 }, 1486 ], 1487 }, 1488 }, 1489 // 1s: 1 new impression; 2 impressions total 1490 1: { 1491 results: [[EXPECTED_NONSPONSORED_URLBAR_RESULT], []], 1492 telemetry: { 1493 events: [ 1494 // reset: interval_s: 1, max_count: 1 1495 { 1496 object: "reset", 1497 extra: { 1498 eventDate: "1000", 1499 intervalSeconds: "1", 1500 maxCount: "1", 1501 startDate: "0", 1502 impressionDate: "0", 1503 count: "1", 1504 type: "nonsponsored", 1505 eventCount: "1", 1506 }, 1507 }, 1508 // hit: interval_s: 1, max_count: 1 1509 { 1510 object: "hit", 1511 extra: { 1512 eventDate: "1000", 1513 intervalSeconds: "1", 1514 maxCount: "1", 1515 startDate: "1000", 1516 impressionDate: "1000", 1517 count: "1", 1518 type: "nonsponsored", 1519 eventCount: "1", 1520 }, 1521 }, 1522 ], 1523 }, 1524 }, 1525 // 2s: 1 new impression; 3 impressions total 1526 2: { 1527 results: [[EXPECTED_NONSPONSORED_URLBAR_RESULT], []], 1528 telemetry: { 1529 events: [ 1530 // reset: interval_s: 1, max_count: 1 1531 { 1532 object: "reset", 1533 extra: { 1534 eventDate: "2000", 1535 intervalSeconds: "1", 1536 maxCount: "1", 1537 startDate: "1000", 1538 impressionDate: "1000", 1539 count: "1", 1540 type: "nonsponsored", 1541 eventCount: "1", 1542 }, 1543 }, 1544 // hit: interval_s: 1, max_count: 1 1545 { 1546 object: "hit", 1547 extra: { 1548 eventDate: "2000", 1549 intervalSeconds: "1", 1550 maxCount: "1", 1551 startDate: "2000", 1552 impressionDate: "2000", 1553 count: "1", 1554 type: "nonsponsored", 1555 eventCount: "1", 1556 }, 1557 }, 1558 // hit: interval_s: 5, max_count: 3 1559 { 1560 object: "hit", 1561 extra: { 1562 eventDate: "2000", 1563 intervalSeconds: "5", 1564 maxCount: "3", 1565 startDate: "0", 1566 impressionDate: "2000", 1567 count: "3", 1568 type: "nonsponsored", 1569 eventCount: "1", 1570 }, 1571 }, 1572 ], 1573 }, 1574 }, 1575 // 3s: no new impressions; 3 impressions total 1576 3: { 1577 results: [[]], 1578 telemetry: { 1579 events: [ 1580 // reset: interval_s: 1, max_count: 1 1581 { 1582 object: "reset", 1583 extra: { 1584 eventDate: "3000", 1585 intervalSeconds: "1", 1586 maxCount: "1", 1587 startDate: "2000", 1588 impressionDate: "2000", 1589 count: "1", 1590 type: "nonsponsored", 1591 eventCount: "1", 1592 }, 1593 }, 1594 ], 1595 }, 1596 }, 1597 // 4s: no new impressions; 3 impressions total 1598 4: { 1599 results: [[]], 1600 telemetry: { 1601 events: [ 1602 // reset: interval_s: 1, max_count: 1 1603 { 1604 object: "reset", 1605 extra: { 1606 eventDate: "4000", 1607 intervalSeconds: "1", 1608 maxCount: "1", 1609 startDate: "3000", 1610 impressionDate: "2000", 1611 count: "0", 1612 type: "nonsponsored", 1613 eventCount: "1", 1614 }, 1615 }, 1616 ], 1617 }, 1618 }, 1619 // 5s: 1 new impression; 4 impressions total 1620 5: { 1621 results: [[EXPECTED_NONSPONSORED_URLBAR_RESULT], []], 1622 telemetry: { 1623 events: [ 1624 // reset: interval_s: 1, max_count: 1 1625 { 1626 object: "reset", 1627 extra: { 1628 eventDate: "5000", 1629 intervalSeconds: "1", 1630 maxCount: "1", 1631 startDate: "4000", 1632 impressionDate: "2000", 1633 count: "0", 1634 type: "nonsponsored", 1635 eventCount: "1", 1636 }, 1637 }, 1638 // reset: interval_s: 5, max_count: 3 1639 { 1640 object: "reset", 1641 extra: { 1642 eventDate: "5000", 1643 intervalSeconds: "5", 1644 maxCount: "3", 1645 startDate: "0", 1646 impressionDate: "2000", 1647 count: "3", 1648 type: "nonsponsored", 1649 eventCount: "1", 1650 }, 1651 }, 1652 // hit: interval_s: 1, max_count: 1 1653 { 1654 object: "hit", 1655 extra: { 1656 eventDate: "5000", 1657 intervalSeconds: "1", 1658 maxCount: "1", 1659 startDate: "5000", 1660 impressionDate: "5000", 1661 count: "1", 1662 type: "nonsponsored", 1663 eventCount: "1", 1664 }, 1665 }, 1666 // hit: interval_s: Infinity, max_count: 4 1667 { 1668 object: "hit", 1669 extra: { 1670 eventDate: "5000", 1671 intervalSeconds: "Infinity", 1672 maxCount: "4", 1673 startDate: "0", 1674 impressionDate: "5000", 1675 count: "4", 1676 type: "nonsponsored", 1677 eventCount: "1", 1678 }, 1679 }, 1680 ], 1681 }, 1682 }, 1683 // 6s: no new impressions; 4 impressions total 1684 6: { 1685 results: [[]], 1686 telemetry: { 1687 events: [ 1688 // reset: interval_s: 1, max_count: 1 1689 { 1690 object: "reset", 1691 extra: { 1692 eventDate: "6000", 1693 intervalSeconds: "1", 1694 maxCount: "1", 1695 startDate: "5000", 1696 impressionDate: "5000", 1697 count: "1", 1698 type: "nonsponsored", 1699 eventCount: "1", 1700 }, 1701 }, 1702 ], 1703 }, 1704 }, 1705 // 7s: no new impressions; 4 impressions total 1706 7: { 1707 results: [[]], 1708 telemetry: { 1709 events: [ 1710 // reset: interval_s: 1, max_count: 1 1711 { 1712 object: "reset", 1713 extra: { 1714 eventDate: "7000", 1715 intervalSeconds: "1", 1716 maxCount: "1", 1717 startDate: "6000", 1718 impressionDate: "5000", 1719 count: "0", 1720 type: "nonsponsored", 1721 eventCount: "1", 1722 }, 1723 }, 1724 ], 1725 }, 1726 }, 1727 }); 1728 }, 1729 }); 1730 }); 1731 1732 // Smoke test for sponsored and non-sponsored caps together. Most tasks use only 1733 // sponsored results and caps, but sponsored and non-sponsored should behave the 1734 // same since they use the same code paths. 1735 add_task(async function sponsoredAndNonsponsored() { 1736 await doTest({ 1737 config: { 1738 impression_caps: { 1739 sponsored: { 1740 lifetime: 2, 1741 }, 1742 nonsponsored: { 1743 lifetime: 3, 1744 }, 1745 }, 1746 }, 1747 callback: async () => { 1748 // 1st searches 1749 await checkSearch({ 1750 name: "sponsored 1", 1751 searchString: "sponsored", 1752 expectedResults: [EXPECTED_SPONSORED_URLBAR_RESULT], 1753 }); 1754 await checkSearch({ 1755 name: "nonsponsored 1", 1756 searchString: "nonsponsored", 1757 expectedResults: [EXPECTED_NONSPONSORED_URLBAR_RESULT], 1758 }); 1759 1760 // 2nd searches 1761 await checkSearch({ 1762 name: "sponsored 2", 1763 searchString: "sponsored", 1764 expectedResults: [EXPECTED_SPONSORED_URLBAR_RESULT], 1765 }); 1766 await checkSearch({ 1767 name: "nonsponsored 2", 1768 searchString: "nonsponsored", 1769 expectedResults: [EXPECTED_NONSPONSORED_URLBAR_RESULT], 1770 }); 1771 1772 // 3rd searches 1773 await checkSearch({ 1774 name: "sponsored 3", 1775 searchString: "sponsored", 1776 expectedResults: [], 1777 }); 1778 await checkSearch({ 1779 name: "nonsponsored 3", 1780 searchString: "nonsponsored", 1781 expectedResults: [EXPECTED_NONSPONSORED_URLBAR_RESULT], 1782 }); 1783 1784 // 4th searches 1785 await checkSearch({ 1786 name: "sponsored 4", 1787 searchString: "sponsored", 1788 expectedResults: [], 1789 }); 1790 await checkSearch({ 1791 name: "nonsponsored 4", 1792 searchString: "nonsponsored", 1793 expectedResults: [], 1794 }); 1795 }, 1796 }); 1797 }); 1798 1799 // Tests with an empty config to make sure results are not capped. 1800 add_task(async function emptyConfig() { 1801 await doTest({ 1802 config: {}, 1803 callback: async () => { 1804 for (let i = 0; i < 2; i++) { 1805 await checkSearch({ 1806 name: "sponsored " + i, 1807 searchString: "sponsored", 1808 expectedResults: [EXPECTED_SPONSORED_URLBAR_RESULT], 1809 }); 1810 await checkSearch({ 1811 name: "nonsponsored " + i, 1812 searchString: "nonsponsored", 1813 expectedResults: [EXPECTED_NONSPONSORED_URLBAR_RESULT], 1814 }); 1815 } 1816 }, 1817 }); 1818 }); 1819 1820 // Tests with sponsored caps disabled. Non-sponsored should still be capped. 1821 add_task(async function sponsoredCapsDisabled() { 1822 UrlbarPrefs.set("quicksuggest.impressionCaps.sponsoredEnabled", false); 1823 await doTest({ 1824 config: { 1825 impression_caps: { 1826 sponsored: { 1827 lifetime: 0, 1828 }, 1829 nonsponsored: { 1830 lifetime: 3, 1831 }, 1832 }, 1833 }, 1834 callback: async () => { 1835 for (let i = 0; i < 3; i++) { 1836 await checkSearch({ 1837 name: "sponsored " + i, 1838 searchString: "sponsored", 1839 expectedResults: [EXPECTED_SPONSORED_URLBAR_RESULT], 1840 }); 1841 await checkSearch({ 1842 name: "nonsponsored " + i, 1843 searchString: "nonsponsored", 1844 expectedResults: [EXPECTED_NONSPONSORED_URLBAR_RESULT], 1845 }); 1846 } 1847 1848 await checkSearch({ 1849 name: "sponsored additional", 1850 searchString: "sponsored", 1851 expectedResults: [EXPECTED_SPONSORED_URLBAR_RESULT], 1852 }); 1853 await checkSearch({ 1854 name: "nonsponsored additional", 1855 searchString: "nonsponsored", 1856 expectedResults: [], 1857 }); 1858 }, 1859 }); 1860 UrlbarPrefs.set("quicksuggest.impressionCaps.sponsoredEnabled", true); 1861 }); 1862 1863 // Tests with non-sponsored caps disabled. Sponsored should still be capped. 1864 add_task(async function nonsponsoredCapsDisabled() { 1865 UrlbarPrefs.set("quicksuggest.impressionCaps.nonSponsoredEnabled", false); 1866 await doTest({ 1867 config: { 1868 impression_caps: { 1869 sponsored: { 1870 lifetime: 3, 1871 }, 1872 nonsponsored: { 1873 lifetime: 0, 1874 }, 1875 }, 1876 }, 1877 callback: async () => { 1878 for (let i = 0; i < 3; i++) { 1879 await checkSearch({ 1880 name: "sponsored " + i, 1881 searchString: "sponsored", 1882 expectedResults: [EXPECTED_SPONSORED_URLBAR_RESULT], 1883 }); 1884 await checkSearch({ 1885 name: "nonsponsored " + i, 1886 searchString: "nonsponsored", 1887 expectedResults: [EXPECTED_NONSPONSORED_URLBAR_RESULT], 1888 }); 1889 } 1890 1891 await checkSearch({ 1892 name: "sponsored additional", 1893 searchString: "sponsored", 1894 expectedResults: [], 1895 }); 1896 await checkSearch({ 1897 name: "nonsponsored additional", 1898 searchString: "nonsponsored", 1899 expectedResults: [EXPECTED_NONSPONSORED_URLBAR_RESULT], 1900 }); 1901 }, 1902 }); 1903 UrlbarPrefs.set("quicksuggest.impressionCaps.nonSponsoredEnabled", true); 1904 }); 1905 1906 // Tests a config change: 1 interval -> same interval with lower cap, with the 1907 // old cap already reached 1908 add_task(async function configChange_sameIntervalLowerCap_1() { 1909 await doTest({ 1910 config: { 1911 impression_caps: { 1912 sponsored: { 1913 custom: [{ interval_s: 3, max_count: 3 }], 1914 }, 1915 }, 1916 }, 1917 callback: async () => { 1918 await doTimedCallbacks({ 1919 0: async () => { 1920 for (let i = 0; i < 3; i++) { 1921 await checkSearch({ 1922 name: "0s " + i, 1923 searchString: "sponsored", 1924 expectedResults: [EXPECTED_SPONSORED_URLBAR_RESULT], 1925 }); 1926 } 1927 await checkSearch({ 1928 name: "0s additional", 1929 searchString: "sponsored", 1930 expectedResults: [], 1931 }); 1932 await QuickSuggestTestUtils.setConfig({ 1933 impression_caps: { 1934 sponsored: { 1935 custom: [{ interval_s: 3, max_count: 1 }], 1936 }, 1937 }, 1938 }); 1939 }, 1940 1: async () => { 1941 await checkSearch({ 1942 name: "1s", 1943 searchString: "sponsored", 1944 expectedResults: [], 1945 }); 1946 }, 1947 3: async () => { 1948 await checkSearch({ 1949 name: "3s 0", 1950 searchString: "sponsored", 1951 expectedResults: [EXPECTED_SPONSORED_URLBAR_RESULT], 1952 }); 1953 await checkSearch({ 1954 name: "3s additional", 1955 searchString: "sponsored", 1956 expectedResults: [], 1957 }); 1958 }, 1959 }); 1960 }, 1961 }); 1962 }); 1963 1964 // Tests a config change: 1 interval -> same interval with lower cap, with the 1965 // old cap not reached 1966 add_task(async function configChange_sameIntervalLowerCap_2() { 1967 await doTest({ 1968 config: { 1969 impression_caps: { 1970 sponsored: { 1971 custom: [{ interval_s: 3, max_count: 3 }], 1972 }, 1973 }, 1974 }, 1975 callback: async () => { 1976 await doTimedCallbacks({ 1977 0: async () => { 1978 for (let i = 0; i < 2; i++) { 1979 await checkSearch({ 1980 name: "0s " + i, 1981 searchString: "sponsored", 1982 expectedResults: [EXPECTED_SPONSORED_URLBAR_RESULT], 1983 }); 1984 } 1985 await QuickSuggestTestUtils.setConfig({ 1986 impression_caps: { 1987 sponsored: { 1988 custom: [{ interval_s: 3, max_count: 1 }], 1989 }, 1990 }, 1991 }); 1992 }, 1993 1: async () => { 1994 await checkSearch({ 1995 name: "1s", 1996 searchString: "sponsored", 1997 expectedResults: [], 1998 }); 1999 }, 2000 3: async () => { 2001 await checkSearch({ 2002 name: "3s 0", 2003 searchString: "sponsored", 2004 expectedResults: [EXPECTED_SPONSORED_URLBAR_RESULT], 2005 }); 2006 await checkSearch({ 2007 name: "3s additional", 2008 searchString: "sponsored", 2009 expectedResults: [], 2010 }); 2011 }, 2012 }); 2013 }, 2014 }); 2015 }); 2016 2017 // Tests a config change: 1 interval -> same interval with higher cap 2018 add_task(async function configChange_sameIntervalHigherCap() { 2019 await doTest({ 2020 config: { 2021 impression_caps: { 2022 sponsored: { 2023 custom: [{ interval_s: 3, max_count: 3 }], 2024 }, 2025 }, 2026 }, 2027 callback: async () => { 2028 await doTimedCallbacks({ 2029 0: async () => { 2030 for (let i = 0; i < 3; i++) { 2031 await checkSearch({ 2032 name: "0s " + i, 2033 searchString: "sponsored", 2034 expectedResults: [EXPECTED_SPONSORED_URLBAR_RESULT], 2035 }); 2036 } 2037 await checkSearch({ 2038 name: "0s additional", 2039 searchString: "sponsored", 2040 expectedResults: [], 2041 }); 2042 await QuickSuggestTestUtils.setConfig({ 2043 impression_caps: { 2044 sponsored: { 2045 custom: [{ interval_s: 3, max_count: 5 }], 2046 }, 2047 }, 2048 }); 2049 }, 2050 1: async () => { 2051 for (let i = 0; i < 2; i++) { 2052 await checkSearch({ 2053 name: "1s " + i, 2054 searchString: "sponsored", 2055 expectedResults: [EXPECTED_SPONSORED_URLBAR_RESULT], 2056 }); 2057 } 2058 await checkSearch({ 2059 name: "1s additional", 2060 searchString: "sponsored", 2061 expectedResults: [], 2062 }); 2063 }, 2064 3: async () => { 2065 for (let i = 0; i < 5; i++) { 2066 await checkSearch({ 2067 name: "3s " + i, 2068 searchString: "sponsored", 2069 expectedResults: [EXPECTED_SPONSORED_URLBAR_RESULT], 2070 }); 2071 } 2072 await checkSearch({ 2073 name: "3s additional", 2074 searchString: "sponsored", 2075 expectedResults: [], 2076 }); 2077 }, 2078 }); 2079 }, 2080 }); 2081 }); 2082 2083 // Tests a config change: 1 interval -> 2 new intervals with higher timeouts. 2084 // Impression counts for the old interval should contribute to the new 2085 // intervals. 2086 add_task(async function configChange_1IntervalTo2NewIntervalsHigher() { 2087 await doTest({ 2088 config: { 2089 impression_caps: { 2090 sponsored: { 2091 custom: [{ interval_s: 3, max_count: 3 }], 2092 }, 2093 }, 2094 }, 2095 callback: async () => { 2096 await doTimedCallbacks({ 2097 0: async () => { 2098 for (let i = 0; i < 3; i++) { 2099 await checkSearch({ 2100 name: "0s " + i, 2101 searchString: "sponsored", 2102 expectedResults: [EXPECTED_SPONSORED_URLBAR_RESULT], 2103 }); 2104 } 2105 await QuickSuggestTestUtils.setConfig({ 2106 impression_caps: { 2107 sponsored: { 2108 custom: [ 2109 { interval_s: 5, max_count: 3 }, 2110 { interval_s: 10, max_count: 5 }, 2111 ], 2112 }, 2113 }, 2114 }); 2115 }, 2116 3: async () => { 2117 await checkSearch({ 2118 name: "3s", 2119 searchString: "sponsored", 2120 expectedResults: [], 2121 }); 2122 }, 2123 4: async () => { 2124 await checkSearch({ 2125 name: "4s", 2126 searchString: "sponsored", 2127 expectedResults: [], 2128 }); 2129 }, 2130 5: async () => { 2131 for (let i = 0; i < 2; i++) { 2132 await checkSearch({ 2133 name: "5s " + i, 2134 searchString: "sponsored", 2135 expectedResults: [EXPECTED_SPONSORED_URLBAR_RESULT], 2136 }); 2137 } 2138 await checkSearch({ 2139 name: "5s additional", 2140 searchString: "sponsored", 2141 expectedResults: [], 2142 }); 2143 }, 2144 }); 2145 }, 2146 }); 2147 }); 2148 2149 // Tests a config change: 2 intervals -> 1 new interval with higher timeout. 2150 // Impression counts for the old intervals should contribute to the new 2151 // interval. 2152 add_task(async function configChange_2IntervalsTo1NewIntervalHigher() { 2153 await doTest({ 2154 config: { 2155 impression_caps: { 2156 sponsored: { 2157 custom: [ 2158 { interval_s: 2, max_count: 2 }, 2159 { interval_s: 4, max_count: 4 }, 2160 ], 2161 }, 2162 }, 2163 }, 2164 callback: async () => { 2165 await doTimedCallbacks({ 2166 0: async () => { 2167 for (let i = 0; i < 2; i++) { 2168 await checkSearch({ 2169 name: "0s " + i, 2170 searchString: "sponsored", 2171 expectedResults: [EXPECTED_SPONSORED_URLBAR_RESULT], 2172 }); 2173 } 2174 }, 2175 2: async () => { 2176 for (let i = 0; i < 2; i++) { 2177 await checkSearch({ 2178 name: "2s " + i, 2179 searchString: "sponsored", 2180 expectedResults: [EXPECTED_SPONSORED_URLBAR_RESULT], 2181 }); 2182 } 2183 await QuickSuggestTestUtils.setConfig({ 2184 impression_caps: { 2185 sponsored: { 2186 custom: [{ interval_s: 6, max_count: 5 }], 2187 }, 2188 }, 2189 }); 2190 }, 2191 4: async () => { 2192 await checkSearch({ 2193 name: "4s 0", 2194 searchString: "sponsored", 2195 expectedResults: [EXPECTED_SPONSORED_URLBAR_RESULT], 2196 }); 2197 await checkSearch({ 2198 name: "4s 1", 2199 searchString: "sponsored", 2200 expectedResults: [], 2201 }); 2202 }, 2203 5: async () => { 2204 await checkSearch({ 2205 name: "5s", 2206 searchString: "sponsored", 2207 expectedResults: [], 2208 }); 2209 }, 2210 6: async () => { 2211 for (let i = 0; i < 5; i++) { 2212 await checkSearch({ 2213 name: "6s " + i, 2214 searchString: "sponsored", 2215 expectedResults: [EXPECTED_SPONSORED_URLBAR_RESULT], 2216 }); 2217 } 2218 await checkSearch({ 2219 name: "6s additional", 2220 searchString: "sponsored", 2221 expectedResults: [], 2222 }); 2223 }, 2224 }); 2225 }, 2226 }); 2227 }); 2228 2229 // Tests a config change: 1 interval -> 1 new interval with lower timeout. 2230 // Impression counts for the old interval should not contribute to the new 2231 // interval since the new interval has a lower timeout. 2232 add_task(async function configChange_1IntervalTo1NewIntervalLower() { 2233 await doTest({ 2234 config: { 2235 impression_caps: { 2236 sponsored: { 2237 custom: [{ interval_s: 5, max_count: 3 }], 2238 }, 2239 }, 2240 }, 2241 callback: async () => { 2242 await doTimedCallbacks({ 2243 0: async () => { 2244 for (let i = 0; i < 3; i++) { 2245 await checkSearch({ 2246 name: "0s " + i, 2247 searchString: "sponsored", 2248 expectedResults: [EXPECTED_SPONSORED_URLBAR_RESULT], 2249 }); 2250 } 2251 await QuickSuggestTestUtils.setConfig({ 2252 impression_caps: { 2253 sponsored: { 2254 custom: [{ interval_s: 3, max_count: 3 }], 2255 }, 2256 }, 2257 }); 2258 }, 2259 1: async () => { 2260 for (let i = 0; i < 3; i++) { 2261 await checkSearch({ 2262 name: "3s " + i, 2263 searchString: "sponsored", 2264 expectedResults: [EXPECTED_SPONSORED_URLBAR_RESULT], 2265 }); 2266 } 2267 await checkSearch({ 2268 name: "3s additional", 2269 searchString: "sponsored", 2270 expectedResults: [], 2271 }); 2272 }, 2273 }); 2274 }, 2275 }); 2276 }); 2277 2278 // Tests a config change: 1 interval -> lifetime. 2279 // Impression counts for the old interval should contribute to the new lifetime 2280 // cap. 2281 add_task(async function configChange_1IntervalToLifetime() { 2282 await doTest({ 2283 config: { 2284 impression_caps: { 2285 sponsored: { 2286 custom: [{ interval_s: 3, max_count: 3 }], 2287 }, 2288 }, 2289 }, 2290 callback: async () => { 2291 await doTimedCallbacks({ 2292 0: async () => { 2293 for (let i = 0; i < 3; i++) { 2294 await checkSearch({ 2295 name: "0s " + i, 2296 searchString: "sponsored", 2297 expectedResults: [EXPECTED_SPONSORED_URLBAR_RESULT], 2298 }); 2299 } 2300 await QuickSuggestTestUtils.setConfig({ 2301 impression_caps: { 2302 sponsored: { 2303 lifetime: 3, 2304 }, 2305 }, 2306 }); 2307 }, 2308 3: async () => { 2309 await checkSearch({ 2310 name: "3s", 2311 searchString: "sponsored", 2312 expectedResults: [], 2313 }); 2314 }, 2315 }); 2316 }, 2317 }); 2318 }); 2319 2320 // Tests a config change: lifetime cap -> higher lifetime cap 2321 add_task(async function configChange_lifetimeCapHigher() { 2322 await doTest({ 2323 config: { 2324 impression_caps: { 2325 sponsored: { 2326 lifetime: 3, 2327 }, 2328 }, 2329 }, 2330 callback: async () => { 2331 await doTimedCallbacks({ 2332 0: async () => { 2333 for (let i = 0; i < 3; i++) { 2334 await checkSearch({ 2335 name: "0s " + i, 2336 searchString: "sponsored", 2337 expectedResults: [EXPECTED_SPONSORED_URLBAR_RESULT], 2338 }); 2339 } 2340 await checkSearch({ 2341 name: "0s additional", 2342 searchString: "sponsored", 2343 expectedResults: [], 2344 }); 2345 await QuickSuggestTestUtils.setConfig({ 2346 impression_caps: { 2347 sponsored: { 2348 lifetime: 5, 2349 }, 2350 }, 2351 }); 2352 }, 2353 1: async () => { 2354 for (let i = 0; i < 2; i++) { 2355 await checkSearch({ 2356 name: "1s " + i, 2357 searchString: "sponsored", 2358 expectedResults: [EXPECTED_SPONSORED_URLBAR_RESULT], 2359 }); 2360 } 2361 await checkSearch({ 2362 name: "1s additional", 2363 searchString: "sponsored", 2364 expectedResults: [], 2365 }); 2366 }, 2367 }); 2368 }, 2369 }); 2370 }); 2371 2372 // Tests a config change: lifetime cap -> lower lifetime cap 2373 add_task(async function configChange_lifetimeCapLower() { 2374 await doTest({ 2375 config: { 2376 impression_caps: { 2377 sponsored: { 2378 lifetime: 3, 2379 }, 2380 }, 2381 }, 2382 callback: async () => { 2383 await doTimedCallbacks({ 2384 0: async () => { 2385 for (let i = 0; i < 3; i++) { 2386 await checkSearch({ 2387 name: "0s " + i, 2388 searchString: "sponsored", 2389 expectedResults: [EXPECTED_SPONSORED_URLBAR_RESULT], 2390 }); 2391 } 2392 await checkSearch({ 2393 name: "0s additional", 2394 searchString: "sponsored", 2395 expectedResults: [], 2396 }); 2397 await QuickSuggestTestUtils.setConfig({ 2398 impression_caps: { 2399 sponsored: { 2400 lifetime: 1, 2401 }, 2402 }, 2403 }); 2404 }, 2405 1: async () => { 2406 await checkSearch({ 2407 name: "1s", 2408 searchString: "sponsored", 2409 expectedResults: [], 2410 }); 2411 }, 2412 }); 2413 }, 2414 }); 2415 }); 2416 2417 // Makes sure stats are serialized to and from the pref correctly. 2418 add_task(async function prefSync() { 2419 await doTest({ 2420 config: { 2421 impression_caps: { 2422 sponsored: { 2423 lifetime: 5, 2424 custom: [ 2425 { interval_s: 3, max_count: 2 }, 2426 { interval_s: 5, max_count: 4 }, 2427 ], 2428 }, 2429 }, 2430 }, 2431 callback: async () => { 2432 for (let i = 0; i < 2; i++) { 2433 await checkSearch({ 2434 name: i, 2435 searchString: "sponsored", 2436 expectedResults: [EXPECTED_SPONSORED_URLBAR_RESULT], 2437 }); 2438 } 2439 2440 let json = UrlbarPrefs.get("quicksuggest.impressionCaps.stats"); 2441 Assert.ok(json, "JSON is non-empty"); 2442 Assert.deepEqual( 2443 JSON.parse(json), 2444 { 2445 sponsored: [ 2446 { 2447 intervalSeconds: 3, 2448 count: 2, 2449 maxCount: 2, 2450 startDateMs: 0, 2451 impressionDateMs: 0, 2452 }, 2453 { 2454 intervalSeconds: 5, 2455 count: 2, 2456 maxCount: 4, 2457 startDateMs: 0, 2458 impressionDateMs: 0, 2459 }, 2460 { 2461 intervalSeconds: null, 2462 count: 2, 2463 maxCount: 5, 2464 startDateMs: 0, 2465 impressionDateMs: 0, 2466 }, 2467 ], 2468 }, 2469 "JSON is correct" 2470 ); 2471 2472 QuickSuggest.impressionCaps._test_reloadStats(); 2473 Assert.deepEqual( 2474 QuickSuggest.impressionCaps._test_stats, 2475 { 2476 sponsored: [ 2477 { 2478 intervalSeconds: 3, 2479 count: 2, 2480 maxCount: 2, 2481 startDateMs: 0, 2482 impressionDateMs: 0, 2483 }, 2484 { 2485 intervalSeconds: 5, 2486 count: 2, 2487 maxCount: 4, 2488 startDateMs: 0, 2489 impressionDateMs: 0, 2490 }, 2491 { 2492 intervalSeconds: Infinity, 2493 count: 2, 2494 maxCount: 5, 2495 startDateMs: 0, 2496 impressionDateMs: 0, 2497 }, 2498 ], 2499 }, 2500 "Impression stats were properly restored from the pref" 2501 ); 2502 }, 2503 }); 2504 }); 2505 2506 // Tests direct changes to the stats pref. 2507 add_task(async function prefDirectlyChanged() { 2508 await doTest({ 2509 config: { 2510 impression_caps: { 2511 sponsored: { 2512 lifetime: 5, 2513 custom: [{ interval_s: 3, max_count: 3 }], 2514 }, 2515 }, 2516 }, 2517 callback: async () => { 2518 let expectedStats = { 2519 sponsored: [ 2520 { 2521 intervalSeconds: 3, 2522 count: 0, 2523 maxCount: 3, 2524 startDateMs: 0, 2525 impressionDateMs: 0, 2526 }, 2527 { 2528 intervalSeconds: Infinity, 2529 count: 0, 2530 maxCount: 5, 2531 startDateMs: 0, 2532 impressionDateMs: 0, 2533 }, 2534 ], 2535 }; 2536 2537 UrlbarPrefs.set("quicksuggest.impressionCaps.stats", "bogus"); 2538 Assert.deepEqual( 2539 QuickSuggest.impressionCaps._test_stats, 2540 expectedStats, 2541 "Expected stats for 'bogus'" 2542 ); 2543 2544 UrlbarPrefs.set("quicksuggest.impressionCaps.stats", JSON.stringify({})); 2545 Assert.deepEqual( 2546 QuickSuggest.impressionCaps._test_stats, 2547 expectedStats, 2548 "Expected stats for {}" 2549 ); 2550 2551 UrlbarPrefs.set( 2552 "quicksuggest.impressionCaps.stats", 2553 JSON.stringify({ sponsored: "bogus" }) 2554 ); 2555 Assert.deepEqual( 2556 QuickSuggest.impressionCaps._test_stats, 2557 expectedStats, 2558 "Expected stats for { sponsored: 'bogus' }" 2559 ); 2560 2561 UrlbarPrefs.set( 2562 "quicksuggest.impressionCaps.stats", 2563 JSON.stringify({ 2564 sponsored: [ 2565 { 2566 intervalSeconds: 3, 2567 count: 0, 2568 maxCount: 3, 2569 startDateMs: 0, 2570 impressionDateMs: 0, 2571 }, 2572 { 2573 intervalSeconds: "bogus", 2574 count: 0, 2575 maxCount: 99, 2576 startDateMs: 0, 2577 impressionDateMs: 0, 2578 }, 2579 { 2580 intervalSeconds: Infinity, 2581 count: 0, 2582 maxCount: 5, 2583 startDateMs: 0, 2584 impressionDateMs: 0, 2585 }, 2586 ], 2587 }) 2588 ); 2589 Assert.deepEqual( 2590 QuickSuggest.impressionCaps._test_stats, 2591 expectedStats, 2592 "Expected stats with intervalSeconds: 'bogus'" 2593 ); 2594 2595 UrlbarPrefs.set( 2596 "quicksuggest.impressionCaps.stats", 2597 JSON.stringify({ 2598 sponsored: [ 2599 { 2600 intervalSeconds: 3, 2601 count: 0, 2602 maxCount: 123, 2603 startDateMs: 0, 2604 impressionDateMs: 0, 2605 }, 2606 { 2607 intervalSeconds: Infinity, 2608 count: 0, 2609 maxCount: 456, 2610 startDateMs: 0, 2611 impressionDateMs: 0, 2612 }, 2613 ], 2614 }) 2615 ); 2616 Assert.deepEqual( 2617 QuickSuggest.impressionCaps._test_stats, 2618 expectedStats, 2619 "Expected stats with `maxCount` values different from caps" 2620 ); 2621 2622 let stats = { 2623 sponsored: [ 2624 { 2625 intervalSeconds: 3, 2626 count: 1, 2627 maxCount: 3, 2628 startDateMs: 99, 2629 impressionDateMs: 99, 2630 }, 2631 { 2632 intervalSeconds: Infinity, 2633 count: 7, 2634 maxCount: 5, 2635 startDateMs: 1337, 2636 impressionDateMs: 1337, 2637 }, 2638 ], 2639 }; 2640 UrlbarPrefs.set( 2641 "quicksuggest.impressionCaps.stats", 2642 JSON.stringify(stats) 2643 ); 2644 Assert.deepEqual( 2645 QuickSuggest.impressionCaps._test_stats, 2646 stats, 2647 "Expected stats with valid JSON" 2648 ); 2649 }, 2650 }); 2651 }); 2652 2653 // Tests multiple interval periods where the cap is not hit. Telemetry should be 2654 // recorded for these periods. 2655 add_task(async function intervalsElapsedButCapNotHit() { 2656 await doTest({ 2657 config: { 2658 impression_caps: { 2659 sponsored: { 2660 custom: [{ interval_s: 1, max_count: 3 }], 2661 }, 2662 }, 2663 }, 2664 callback: async () => { 2665 await doTimedCallbacks({ 2666 // 1s 2667 1: async () => { 2668 await checkSearch({ 2669 name: "1s", 2670 searchString: "sponsored", 2671 expectedResults: [EXPECTED_SPONSORED_URLBAR_RESULT], 2672 }); 2673 }, 2674 // 10s 2675 10: async () => { 2676 QuickSuggest.impressionCaps._test_resetElapsedCounters(); 2677 }, 2678 }); 2679 }, 2680 }); 2681 }); 2682 2683 // Simulates reset events across a restart with the following: 2684 // 2685 // S S R 2686 // >----|----|----|----|----|----|----|----|----|----| 2687 // 0s 1 2 3 4 5 6 7 8 9 10 2688 // 2689 // 1. Startup at 0s 2690 // 2. Caps and stats initialized with interval_s: 1 2691 // 3. Startup at 4.5s 2692 // 4. Reset triggered at 10s 2693 // 2694 // Expected: 2695 // At 10s: 6 batched resets for periods starting at 4s 2696 add_task(async function restart_1() { 2697 await doTest({ 2698 config: { 2699 impression_caps: { 2700 sponsored: { 2701 custom: [{ interval_s: 1, max_count: 1 }], 2702 }, 2703 }, 2704 }, 2705 callback: async () => { 2706 gStartupDateMsStub.returns(4500); 2707 await doTimedCallbacks({ 2708 // 10s: 6 batched resets for periods starting at 4s 2709 10: async () => { 2710 QuickSuggest.impressionCaps._test_resetElapsedCounters(); 2711 }, 2712 }); 2713 }, 2714 }); 2715 gStartupDateMsStub.returns(0); 2716 }); 2717 2718 // Simulates reset events across a restart with the following: 2719 // 2720 // S S R 2721 // >----|----|----|----|----|----|----|----|----|----| 2722 // 0s 1 2 3 4 5 6 7 8 9 10 2723 // 2724 // 1. Startup at 0s 2725 // 2. Caps and stats initialized with interval_s: 1 2726 // 3. Startup at 5s 2727 // 4. Reset triggered at 10s 2728 // 2729 // Expected: 2730 // At 10s: 5 batched resets for periods starting at 5s 2731 add_task(async function restart_2() { 2732 await doTest({ 2733 config: { 2734 impression_caps: { 2735 sponsored: { 2736 custom: [{ interval_s: 1, max_count: 1 }], 2737 }, 2738 }, 2739 }, 2740 callback: async () => { 2741 gStartupDateMsStub.returns(5000); 2742 await doTimedCallbacks({ 2743 // 10s: 5 batched resets for periods starting at 5s 2744 10: async () => { 2745 QuickSuggest.impressionCaps._test_resetElapsedCounters(); 2746 }, 2747 }); 2748 }, 2749 }); 2750 gStartupDateMsStub.returns(0); 2751 }); 2752 2753 // Simulates reset events across a restart with the following: 2754 // 2755 // S S R 2756 // >----|----|----|----|----|----|----|----|----|----| 2757 // 0s 1 2 3 4 5 6 7 8 9 10 2758 // 2759 // 1. Startup at 0s 2760 // 2. Caps and stats initialized with interval_s: 1 2761 // 3. Startup at 5.5s 2762 // 4. Reset triggered at 10s 2763 // 2764 // Expected: 2765 // At 10s: 5 batched resets for periods starting at 5s 2766 add_task(async function restart_3() { 2767 await doTest({ 2768 config: { 2769 impression_caps: { 2770 sponsored: { 2771 custom: [{ interval_s: 1, max_count: 1 }], 2772 }, 2773 }, 2774 }, 2775 callback: async () => { 2776 gStartupDateMsStub.returns(5500); 2777 await doTimedCallbacks({ 2778 // 10s: 5 batched resets for periods starting at 5s 2779 10: async () => { 2780 QuickSuggest.impressionCaps._test_resetElapsedCounters(); 2781 }, 2782 }); 2783 }, 2784 }); 2785 gStartupDateMsStub.returns(0); 2786 }); 2787 2788 // Simulates reset events across a restart with the following: 2789 // 2790 // S S RR RR 2791 // >---------|---------| 2792 // 0s 10 20 2793 // 2794 // 1. Startup at 0s 2795 // 2. Caps and stats initialized with interval_s: 10 2796 // 3. Startup at 5s 2797 // 4. Resets triggered at 9s, 10s, 19s, 20s 2798 // 2799 // Expected: 2800 // At 10s: 1 reset for period starting at 0s 2801 // At 20s: 1 reset for period starting at 10s 2802 add_task(async function restart_4() { 2803 await doTest({ 2804 config: { 2805 impression_caps: { 2806 sponsored: { 2807 custom: [{ interval_s: 10, max_count: 1 }], 2808 }, 2809 }, 2810 }, 2811 callback: async () => { 2812 gStartupDateMsStub.returns(5000); 2813 await doTimedCallbacks({ 2814 // 9s: no resets 2815 9: async () => { 2816 QuickSuggest.impressionCaps._test_resetElapsedCounters(); 2817 }, 2818 // 10s: 1 reset for period starting at 0s 2819 10: async () => { 2820 QuickSuggest.impressionCaps._test_resetElapsedCounters(); 2821 }, 2822 // 19s: no resets 2823 19: async () => { 2824 QuickSuggest.impressionCaps._test_resetElapsedCounters(); 2825 }, 2826 // 20s: 1 reset for period starting at 10s 2827 20: async () => { 2828 QuickSuggest.impressionCaps._test_resetElapsedCounters(); 2829 }, 2830 }); 2831 }, 2832 }); 2833 gStartupDateMsStub.returns(0); 2834 }); 2835 2836 // Simulates reset events across a restart with the following: 2837 // 2838 // S S R 2839 // >---------|---------| 2840 // 0s 10 20 2841 // 2842 // 1. Startup at 0s 2843 // 2. Caps and stats initialized with interval_s: 10 2844 // 3. Startup at 5s 2845 // 4. Reset triggered at 20s 2846 // 2847 // Expected: 2848 // At 20s: 2 batched resets for periods starting at 0s 2849 add_task(async function restart_5() { 2850 await doTest({ 2851 config: { 2852 impression_caps: { 2853 sponsored: { 2854 custom: [{ interval_s: 10, max_count: 1 }], 2855 }, 2856 }, 2857 }, 2858 callback: async () => { 2859 gStartupDateMsStub.returns(5000); 2860 await doTimedCallbacks({ 2861 // 20s: 2 batches resets for periods starting at 0s 2862 20: async () => { 2863 QuickSuggest.impressionCaps._test_resetElapsedCounters(); 2864 }, 2865 }); 2866 }, 2867 }); 2868 gStartupDateMsStub.returns(0); 2869 }); 2870 2871 // Simulates reset events across a restart with the following: 2872 // 2873 // S S RR RR 2874 // >---------|---------|---------| 2875 // 0s 10 20 30 2876 // 2877 // 1. Startup at 0s 2878 // 2. Caps and stats initialized with interval_s: 10 2879 // 3. Startup at 15s 2880 // 4. Resets triggered at 19s, 20s, 29s, 30s 2881 // 2882 // Expected: 2883 // At 20s: 1 reset for period starting at 10s 2884 // At 30s: 1 reset for period starting at 20s 2885 add_task(async function restart_6() { 2886 await doTest({ 2887 config: { 2888 impression_caps: { 2889 sponsored: { 2890 custom: [{ interval_s: 10, max_count: 1 }], 2891 }, 2892 }, 2893 }, 2894 callback: async () => { 2895 gStartupDateMsStub.returns(15000); 2896 await doTimedCallbacks({ 2897 // 19s: no resets 2898 19: async () => { 2899 QuickSuggest.impressionCaps._test_resetElapsedCounters(); 2900 }, 2901 // 20s: 1 reset for period starting at 10s 2902 20: async () => { 2903 QuickSuggest.impressionCaps._test_resetElapsedCounters(); 2904 }, 2905 // 29s: no resets 2906 29: async () => { 2907 QuickSuggest.impressionCaps._test_resetElapsedCounters(); 2908 }, 2909 // 30s: 1 reset for period starting at 20s 2910 30: async () => { 2911 QuickSuggest.impressionCaps._test_resetElapsedCounters(); 2912 }, 2913 }); 2914 }, 2915 }); 2916 gStartupDateMsStub.returns(0); 2917 }); 2918 2919 // Simulates reset events across a restart with the following: 2920 // 2921 // S S R 2922 // >---------|---------|---------| 2923 // 0s 10 20 30 2924 // 2925 // 1. Startup at 0s 2926 // 2. Caps and stats initialized with interval_s: 10 2927 // 3. Startup at 15s 2928 // 4. Reset triggered at 30s 2929 // 2930 // Expected: 2931 // At 30s: 2 batched resets for periods starting at 10s 2932 add_task(async function restart_7() { 2933 await doTest({ 2934 config: { 2935 impression_caps: { 2936 sponsored: { 2937 custom: [{ interval_s: 10, max_count: 1 }], 2938 }, 2939 }, 2940 }, 2941 callback: async () => { 2942 gStartupDateMsStub.returns(15000); 2943 await doTimedCallbacks({ 2944 // 30s: 2 batched resets for periods starting at 10s 2945 30: async () => { 2946 QuickSuggest.impressionCaps._test_resetElapsedCounters(); 2947 }, 2948 }); 2949 }, 2950 }); 2951 gStartupDateMsStub.returns(0); 2952 }); 2953 2954 // Tests reset telemetry recorded on shutdown. 2955 add_task(async function shutdown() { 2956 await doTest({ 2957 config: { 2958 impression_caps: { 2959 sponsored: { 2960 custom: [{ interval_s: 1, max_count: 1 }], 2961 }, 2962 }, 2963 }, 2964 callback: async () => { 2965 // Make `Date.now()` return 10s. Since the cap's `interval_s` is 1s and 2966 // before this `Date.now()` returned 0s, 10 reset events should be 2967 // recorded on shutdown. 2968 gDateNowStub.returns(10000); 2969 2970 // Simulate shutdown. 2971 Services.prefs.setBoolPref("toolkit.asyncshutdown.testing", true); 2972 AsyncShutdown.profileChangeTeardown._trigger(); 2973 2974 gDateNowStub.returns(0); 2975 Services.prefs.clearUserPref("toolkit.asyncshutdown.testing"); 2976 }, 2977 }); 2978 }); 2979 2980 // Tests the reset interval in realtime. 2981 add_task(async function resetInterval() { 2982 // Remove the test stubs so we can test in realtime. 2983 gDateNowStub.restore(); 2984 gStartupDateMsStub.restore(); 2985 2986 await doTest({ 2987 config: { 2988 impression_caps: { 2989 sponsored: { 2990 custom: [{ interval_s: 0.1, max_count: 1 }], 2991 }, 2992 }, 2993 }, 2994 callback: async () => { 2995 // Restart the reset interval now with a 1s period. Since the cap's 2996 // `interval_s` is 0.1s, at least 10 reset events should be recorded the 2997 // first time the reset interval fires. The exact number depends on timing 2998 // and the machine running the test: how many 0.1s intervals elapse 2999 // between when the config is set to when the reset interval fires. For 3000 // that reason, we allow some leeway when checking `eventCount` below to 3001 // avoid intermittent failures. 3002 QuickSuggest.impressionCaps._test_setCountersResetInterval(1000); 3003 3004 // eslint-disable-next-line mozilla/no-arbitrary-setTimeout 3005 await new Promise(r => setTimeout(r, 1100)); 3006 3007 // Restore the reset interval to its default. 3008 QuickSuggest.impressionCaps._test_setCountersResetInterval(); 3009 }, 3010 }); 3011 3012 // Recreate the test stubs. 3013 gDateNowStub = gSandbox.stub( 3014 Cu.getGlobalForObject(UrlbarProviderQuickSuggest).Date, 3015 "now" 3016 ); 3017 gStartupDateMsStub = gSandbox.stub( 3018 QuickSuggest.impressionCaps, 3019 "_getStartupDateMs" 3020 ); 3021 gStartupDateMsStub.returns(0); 3022 }); 3023 3024 /** 3025 * Main test helper. Sets up state, calls your callback, and resets state. 3026 * 3027 * @param {object} options 3028 * Options object. 3029 * @param {object} options.config 3030 * The quick suggest config to use during the test. 3031 * @param {Function} options.callback 3032 * The callback that will be run with the {@link config} 3033 */ 3034 async function doTest({ config, callback }) { 3035 // Make `Date.now()` return 0 to start with. It's necessary to do this before 3036 // calling `withConfig()` because when a new config is set, the provider 3037 // validates its impression stats, whose `startDateMs` values depend on 3038 // `Date.now()`. 3039 gDateNowStub.returns(0); 3040 3041 info(`Clearing stats and setting config`); 3042 UrlbarPrefs.clear("quicksuggest.impressionCaps.stats"); 3043 QuickSuggest.impressionCaps._test_reloadStats(); 3044 await QuickSuggestTestUtils.withConfig({ config, callback }); 3045 } 3046 3047 /** 3048 * Does a series of timed searches and checks their results. This 3049 * function relies on `doTimedCallbacks()`, so it may be helpful to look at it 3050 * too. 3051 * 3052 * @param {string} searchString 3053 * The query that should be timed 3054 * @param {object} expectedBySecond 3055 * An object that maps from seconds to objects that describe the searches to 3056 * perform, their expected results, and the expected telemetry. For a given 3057 * entry `S -> E` in this object, searches are performed S seconds after this 3058 * function is called. `E` is an object that looks like this: 3059 * 3060 * { results } 3061 * 3062 * {array} results 3063 * An array of arrays. A search is performed for each sub-array in 3064 * `results`, and the contents of the sub-array are the expected results 3065 * for that search. 3066 * 3067 * Example: 3068 * 3069 * { 3070 * 0: { 3071 * results: [[R1], []], 3072 * } 3073 * 1: { 3074 * results: [[]], 3075 * }, 3076 * } 3077 * 3078 * 0 seconds after `doTimedSearches()` is called, two searches are 3079 * performed. The first one is expected to return a single result R1, and 3080 * the second search is expected to return no results. 3081 * 3082 * 1 second after `doTimedSearches()` is called, one search is performed. 3083 * It's expected to return no results. 3084 */ 3085 async function doTimedSearches(searchString, expectedBySecond) { 3086 await doTimedCallbacks( 3087 Object.entries(expectedBySecond).reduce((memo, [second, { results }]) => { 3088 memo[second] = async () => { 3089 for (let i = 0; i < results.length; i++) { 3090 let expectedResults = results[i]; 3091 await checkSearch({ 3092 searchString, 3093 expectedResults, 3094 name: `${second}s search ${i + 1} of ${results.length}`, 3095 }); 3096 } 3097 }; 3098 return memo; 3099 }, {}) 3100 ); 3101 } 3102 3103 /** 3104 * Takes a series a callbacks and times at which they should be called, and 3105 * calls them accordingly. This function is specifically designed for 3106 * UrlbarProviderQuickSuggest and its impression capping implementation because 3107 * it works by stubbing `Date.now()` within UrlbarProviderQuickSuggest. The 3108 * callbacks are not actually called at the given times but instead `Date.now()` 3109 * is stubbed so that UrlbarProviderQuickSuggest will think they are being 3110 * called at the given times. 3111 * 3112 * A more general implementation of this helper function that isn't tailored to 3113 * UrlbarProviderQuickSuggest is commented out below, and unfortunately it 3114 * doesn't work properly on macOS. 3115 * 3116 * @param {object} callbacksBySecond 3117 * An object that maps from seconds to callback functions. For a given entry 3118 * `S -> F` in this object, the callback F is called S seconds after 3119 * `doTimedCallbacks()` is called. 3120 */ 3121 async function doTimedCallbacks(callbacksBySecond) { 3122 let entries = Object.entries(callbacksBySecond).sort(([t1], [t2]) => t1 - t2); 3123 for (let [timeoutSeconds, callback] of entries) { 3124 gDateNowStub.returns(1000 * timeoutSeconds); 3125 await callback(); 3126 } 3127 } 3128 3129 /* 3130 // This is the original implementation of `doTimedCallbacks()`, left here for 3131 // reference or in case the macOS problem described below is fixed. Instead of 3132 // stubbing `Date.now()` within UrlbarProviderQuickSuggest, it starts parallel 3133 // timers so that the callbacks are actually called at appropriate times. This 3134 // version of `doTimedCallbacks()` is therefore more generally useful, but it 3135 // has the drawback that your test has to run in real time. e.g., if one of your 3136 // callbacks needs to run 10s from now, the test must actually wait 10s. 3137 // 3138 // Unfortunately macOS seems to have some kind of limit of ~33 total 1-second 3139 // timers during any xpcshell test -- not 33 simultaneous timers but 33 total 3140 // timers. After that, timers fire randomly and with huge timeout periods that 3141 // are often exactly 10s greater than the specified period, as if some 10s 3142 // timeout internal to macOS is being hit. This problem does not seem to happen 3143 // when running the full browser, only during xpcshell tests. In fact the 3144 // problem can be reproduced in an xpcshell test that simply creates an interval 3145 // timer whose period is 1s (e.g., using `setInterval()` from Timer.sys.mjs). 3146 // After ~33 ticks, the timer's period jumps to ~10s. 3147 async function doTimedCallbacks(callbacksBySecond) { 3148 await Promise.all( 3149 Object.entries(callbacksBySecond).map( 3150 ([timeoutSeconds, callback]) => new Promise( 3151 resolve => setTimeout( 3152 () => callback().then(resolve), 3153 1000 * parseInt(timeoutSeconds) 3154 ) 3155 ) 3156 ) 3157 ); 3158 } 3159 */ 3160 3161 /** 3162 * Does a search, triggers an engagement, and checks the results. 3163 * 3164 * @param {object} options 3165 * Options object. 3166 * @param {string} options.name 3167 * This value is the name of the search and will be logged in messages to make 3168 * debugging easier. 3169 * @param {string} options.searchString 3170 * The query that should be searched. 3171 * @param {Array} options.expectedResults 3172 * The results that are expected from the search. 3173 */ 3174 async function checkSearch({ name, searchString, expectedResults }) { 3175 let quickSuggestProviderInstance = UrlbarProvidersManager.getProvider( 3176 UrlbarProviderQuickSuggest.name 3177 ); 3178 info(`Preparing search "${name}" with search string "${searchString}"`); 3179 let context = createContext(searchString, { 3180 providers: [UrlbarProviderQuickSuggest.name], 3181 isPrivate: false, 3182 }); 3183 info(`Doing search: ${name}`); 3184 await check_results({ 3185 context, 3186 matches: expectedResults, 3187 }); 3188 info(`Finished search: ${name}`); 3189 3190 // Impression stats are updated only on engagement, so force one now. 3191 // `selIndex` doesn't really matter but since we're not trying to simulate a 3192 // click on the suggestion, pass in -1 to ensure we don't record a click. 3193 if (quickSuggestProviderInstance._resultFromLastQuery) { 3194 quickSuggestProviderInstance._resultFromLastQuery.isVisible = true; 3195 } 3196 const controller = UrlbarTestUtils.newMockController({ 3197 input: { 3198 isPrivate: true, 3199 onFirstResult() { 3200 return false; 3201 }, 3202 getSearchSource() { 3203 return "dummy-search-source"; 3204 }, 3205 window: { 3206 location: { 3207 href: AppConstants.BROWSER_CHROME_URL, 3208 }, 3209 }, 3210 }, 3211 }); 3212 controller.setView({ 3213 get visibleResults() { 3214 return context.results; 3215 }, 3216 controller: { 3217 removeResult() {}, 3218 }, 3219 }); 3220 3221 // If this test is ever re-enabled, this line will need to be updated for the 3222 // new engagement API (onEngagement()) 3223 quickSuggestProviderInstance.onLegacyEngagement( 3224 "engagement", 3225 context, 3226 { 3227 selIndex: -1, 3228 }, 3229 controller 3230 ); 3231 }