Bug 1836582 - Update the matching behavior of addon suggestions. r=daisuke
This implements the new required matching behavior, which isn't based on min keyword length anymore. This is how it works: * Use the full keywords in remote settings to generate keywords that contain the first word plus each possible substring after the first word. For example if a full keyword is "video download", then generate these keywords: "video", "video ", "video d", "video do", etc. If a full keyword is only one word, then use it as is. The keywords never change even when the user clicks "Show less frequently". This is implemented in `onRemoteSettingsSync()`, and I modified `SuggestionsMap.add()` to make it easy to generate new keywords from the strings in `suggestion.keywords`. * Keep track of the number of times the user clicked "Show less frequently" in `showLessFrequentlyCount`. * When a suggestion is fetched from the suggestions map, filter it out if the search string isn't long enough given the `showLessFrequentlyCount`. This is done in `makeResult()`. Other changes: * I made some of the private properties in `AddonSuggestions` public so that the xpcshell test can easily use them. I think it's OK for them to be public. * Added `show_less_frequently_cap` to the RS config object so that we can specify a cap in RS as well as Nimbus. * mv'ed test_quicksuggest_addResults.js to test_suggestionsMap.js, since I modified this file. I should have done that back when I replaced `addResults()` with `SuggestionsMap`. * Fixed a bug in `SuggestionsMap.add()` where the same suggestion could be added multiple times to the array stored in the map, if it had duplicate keywords. Differential Revision: https://phabricator.services.mozilla.com/D179867
This commit is contained in:
@@ -35,9 +35,9 @@ const PREF_URLBAR_DEFAULTS = new Map([
|
||||
// Feature gate pref for addon suggestions in the urlbar.
|
||||
["addons.featureGate", false],
|
||||
|
||||
// The minimum prefix length of addons keyword the user must type to trigger
|
||||
// the suggestion. 0 means the min length should be taken from Nimbus.
|
||||
["addons.minKeywordLength", 0],
|
||||
// The number of times the user has clicked the "Show less frequently" command
|
||||
// for addon suggestions.
|
||||
["addons.showLessFrequentlyCount", 0],
|
||||
|
||||
// "Autofill" is the name of the feature that automatically completes domains
|
||||
// and URLs that the user has visited as the user is typing them in the urlbar
|
||||
@@ -453,8 +453,7 @@ const PREF_OTHER_DEFAULTS = new Map([
|
||||
// Variables with fallback prefs do not need to be defined here because their
|
||||
// defaults are the values of their fallbacks.
|
||||
const NIMBUS_DEFAULTS = {
|
||||
addonsKeywordsMinimumLength: 0,
|
||||
addonsKeywordsMinimumLengthCap: 0,
|
||||
addonsShowLessFrequentlyCap: 0,
|
||||
addonsUITreatment: "a",
|
||||
experimentType: "",
|
||||
isBestMatchExperiment: false,
|
||||
|
||||
@@ -175,7 +175,20 @@ export class AddonSuggestions extends BaseFeature {
|
||||
}
|
||||
|
||||
const results = JSON.parse(new TextDecoder("utf-8").decode(buffer));
|
||||
await suggestionsMap.add(results);
|
||||
|
||||
// The keywords in remote settings are full keywords. Map each one to an
|
||||
// array containing the full keyword's first word plus every subsequent
|
||||
// prefix of the full keyword.
|
||||
await suggestionsMap.add(results, fullKeyword => {
|
||||
let keywords = [fullKeyword];
|
||||
let spaceIndex = fullKeyword.search(/\s/);
|
||||
if (spaceIndex >= 0) {
|
||||
for (let i = spaceIndex; i < fullKeyword.length; i++) {
|
||||
keywords.push(fullKeyword.substring(0, i));
|
||||
}
|
||||
}
|
||||
return keywords;
|
||||
});
|
||||
if (rs != lazy.QuickSuggestRemoteSettings.rs) {
|
||||
return;
|
||||
}
|
||||
@@ -185,10 +198,26 @@ export class AddonSuggestions extends BaseFeature {
|
||||
}
|
||||
|
||||
async makeResult(queryContext, suggestion, searchString) {
|
||||
if (!this.isEnabled || searchString.length < this.#minKeywordLength) {
|
||||
if (!this.isEnabled) {
|
||||
// The feature is disabled on the client, but Merino may still return
|
||||
// addon suggestions anyway, and we filter them out here.
|
||||
return null;
|
||||
}
|
||||
|
||||
// If the user hasn't clicked the "Show less frequently" command, the
|
||||
// suggestion can be shown. Otherwise, the suggestion can be shown if the
|
||||
// user typed more than one word with at least `showLessFrequentlyCount`
|
||||
// characters after the first word, including spaces.
|
||||
if (this.showLessFrequentlyCount) {
|
||||
let spaceIndex = searchString.search(/\s/);
|
||||
if (
|
||||
spaceIndex < 0 ||
|
||||
searchString.length - spaceIndex < this.showLessFrequentlyCount
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// If is_top_pick is not specified, handle it as top pick suggestion.
|
||||
suggestion.is_top_pick = suggestion.is_top_pick ?? true;
|
||||
|
||||
@@ -294,7 +323,7 @@ export class AddonSuggestions extends BaseFeature {
|
||||
getResultCommands(result) {
|
||||
const commands = [];
|
||||
|
||||
if (this.#canIncrementMinKeywordLength) {
|
||||
if (this.canShowLessFrequently) {
|
||||
commands.push({
|
||||
name: RESULT_MENU_COMMAND.SHOW_LESS_FREQUENTLY,
|
||||
l10n: {
|
||||
@@ -349,7 +378,7 @@ export class AddonSuggestions extends BaseFeature {
|
||||
break;
|
||||
case RESULT_MENU_COMMAND.SHOW_LESS_FREQUENTLY:
|
||||
queryContext.view.acknowledgeFeedback(result);
|
||||
this.#incrementMinKeywordLength();
|
||||
this.incrementShowLessFrequentlyCount();
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -369,26 +398,26 @@ export class AddonSuggestions extends BaseFeature {
|
||||
return "full";
|
||||
}
|
||||
|
||||
#incrementMinKeywordLength() {
|
||||
if (this.#canIncrementMinKeywordLength) {
|
||||
incrementShowLessFrequentlyCount() {
|
||||
if (this.canShowLessFrequently) {
|
||||
lazy.UrlbarPrefs.set(
|
||||
"addons.minKeywordLength",
|
||||
this.#minKeywordLength + 1
|
||||
"addons.showLessFrequentlyCount",
|
||||
this.showLessFrequentlyCount + 1
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
get #minKeywordLength() {
|
||||
const minLength =
|
||||
lazy.UrlbarPrefs.get("addons.minKeywordLength") ||
|
||||
lazy.UrlbarPrefs.get("addonsKeywordsMinimumLength") ||
|
||||
0;
|
||||
return Math.max(minLength, 0);
|
||||
get showLessFrequentlyCount() {
|
||||
const count = lazy.UrlbarPrefs.get("addons.showLessFrequentlyCount") || 0;
|
||||
return Math.max(count, 0);
|
||||
}
|
||||
|
||||
get #canIncrementMinKeywordLength() {
|
||||
const cap = lazy.UrlbarPrefs.get("addonsKeywordsMinimumLengthCap") || 0;
|
||||
return !cap || this.#minKeywordLength < cap;
|
||||
get canShowLessFrequently() {
|
||||
const cap =
|
||||
lazy.UrlbarPrefs.get("addonsShowLessFrequentlyCap") ||
|
||||
lazy.QuickSuggestRemoteSettings.config.show_less_frequently_cap ||
|
||||
0;
|
||||
return !cap || this.showLessFrequentlyCount < cap;
|
||||
}
|
||||
|
||||
#suggestionsMap = null;
|
||||
|
||||
@@ -81,6 +81,7 @@ class _QuickSuggestRemoteSettings {
|
||||
* ],
|
||||
* },
|
||||
* },
|
||||
* show_less_frequently_cap,
|
||||
* }
|
||||
*/
|
||||
get config() {
|
||||
@@ -303,12 +304,21 @@ export class SuggestionsMap {
|
||||
|
||||
/**
|
||||
* Adds a list of suggestion objects to the results map. Each suggestion must
|
||||
* have a `keywords` property.
|
||||
* have a `keywords` property whose value is an array of keyword strings. The
|
||||
* suggestion's keywords will be taken from this array either exactly as they
|
||||
* are specified or by generating new keywords from them; see `mapKeyword`.
|
||||
*
|
||||
* @param {Array} suggestions
|
||||
* Array of suggestion objects.
|
||||
* @param {Function} mapKeyword
|
||||
* If null, each suggestion's keywords will be taken from its `keywords`
|
||||
* array exactly as they are specified. Otherwise, as each suggestion is
|
||||
* processed, this function will be called for each string in its `keywords`
|
||||
* array. The function should return an array of strings. The suggestion's
|
||||
* final list of keywords will be all the keywords returned by this function
|
||||
* as it is called for each string in `keywords`.
|
||||
*/
|
||||
async add(suggestions) {
|
||||
async add(suggestions, mapKeyword = null) {
|
||||
// There can be many suggestions, and each suggestion can have many
|
||||
// keywords. To avoid blocking the main thread for too long, update the map
|
||||
// in chunks, and to avoid blocking the UI and other higher priority work,
|
||||
@@ -329,22 +339,32 @@ export class SuggestionsMap {
|
||||
) {
|
||||
let suggestion = suggestions[suggestionIndex];
|
||||
if (keywordIndex == suggestion.keywords.length) {
|
||||
// We've added entries for all keywords of the current suggestion.
|
||||
// Move on to the next suggestion.
|
||||
suggestionIndex++;
|
||||
keywordIndex = 0;
|
||||
continue;
|
||||
}
|
||||
// If the keyword's only suggestion is `suggestion`, store it
|
||||
// directly as the value. Otherwise store an array of suggestions.
|
||||
// For details, see the `#suggestionsByKeyword` comment.
|
||||
let keyword = suggestion.keywords[keywordIndex];
|
||||
let object = this.#suggestionsByKeyword.get(keyword);
|
||||
if (!object) {
|
||||
this.#suggestionsByKeyword.set(keyword, suggestion);
|
||||
} else if (!Array.isArray(object)) {
|
||||
this.#suggestionsByKeyword.set(keyword, [object, suggestion]);
|
||||
} else {
|
||||
object.push(suggestion);
|
||||
|
||||
let originalKeyword = suggestion.keywords[keywordIndex];
|
||||
let keywords = mapKeyword?.(originalKeyword) ?? [originalKeyword];
|
||||
for (let keyword of keywords) {
|
||||
// If the keyword's only suggestion is `suggestion`, store it
|
||||
// directly as the value. Otherwise store an array of unique
|
||||
// suggestions. See the `#suggestionsByKeyword` comment.
|
||||
let object = this.#suggestionsByKeyword.get(keyword);
|
||||
if (!object) {
|
||||
this.#suggestionsByKeyword.set(keyword, suggestion);
|
||||
} else {
|
||||
let isArray = Array.isArray(object);
|
||||
if (!isArray && object != suggestion) {
|
||||
this.#suggestionsByKeyword.set(keyword, [object, suggestion]);
|
||||
} else if (isArray && !object.includes(suggestion)) {
|
||||
object.push(suggestion);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
keywordIndex++;
|
||||
indexInChunk++;
|
||||
}
|
||||
|
||||
@@ -251,111 +251,60 @@ add_task(async function resultMenu_showLessFrequently() {
|
||||
await SpecialPowers.pushPrefEnv({
|
||||
set: [
|
||||
["browser.urlbar.addons.featureGate", true],
|
||||
["browser.urlbar.addons.minKeywordLength", 0],
|
||||
["browser.urlbar.addons.showLessFrequentlyCount", 0],
|
||||
],
|
||||
});
|
||||
|
||||
const cleanUpNimbus = await UrlbarTestUtils.initNimbusFeature({
|
||||
addonsKeywordsMinimumLengthCap: 3,
|
||||
addonsShowLessFrequentlyCap: 3,
|
||||
});
|
||||
|
||||
// Sanity check.
|
||||
Assert.equal(UrlbarPrefs.get("addonsKeywordsMinimumLengthCap"), 3);
|
||||
Assert.equal(UrlbarPrefs.get("addonsKeywordsMinimumLength"), 0);
|
||||
Assert.equal(UrlbarPrefs.get("addons.minKeywordLength"), 0);
|
||||
Assert.equal(UrlbarPrefs.get("addonsShowLessFrequentlyCap"), 3);
|
||||
Assert.equal(UrlbarPrefs.get("addons.showLessFrequentlyCount"), 0);
|
||||
|
||||
await doShowLessFrequently({
|
||||
input: "12",
|
||||
input: "aaa b",
|
||||
expected: {
|
||||
isSuggestionShown: true,
|
||||
isMenuItemShown: true,
|
||||
},
|
||||
});
|
||||
Assert.equal(UrlbarPrefs.get("addons.minKeywordLength"), 1);
|
||||
Assert.equal(UrlbarPrefs.get("addons.showLessFrequentlyCount"), 1);
|
||||
|
||||
await doShowLessFrequently({
|
||||
input: "12",
|
||||
input: "aaa b",
|
||||
expected: {
|
||||
isSuggestionShown: true,
|
||||
isMenuItemShown: true,
|
||||
},
|
||||
});
|
||||
Assert.equal(UrlbarPrefs.get("addons.minKeywordLength"), 2);
|
||||
Assert.equal(UrlbarPrefs.get("addons.showLessFrequentlyCount"), 2);
|
||||
|
||||
await doShowLessFrequently({
|
||||
input: "12",
|
||||
input: "aaa b",
|
||||
expected: {
|
||||
isSuggestionShown: true,
|
||||
isMenuItemShown: true,
|
||||
},
|
||||
});
|
||||
Assert.equal(UrlbarPrefs.get("addons.minKeywordLength"), 3);
|
||||
Assert.equal(UrlbarPrefs.get("addons.showLessFrequentlyCount"), 3);
|
||||
|
||||
await doShowLessFrequently({
|
||||
input: "12",
|
||||
input: "aaa b",
|
||||
expected: {
|
||||
// The suggestion should not display since addons.minKeywordLength is 3
|
||||
// and the text length is 2,
|
||||
// The suggestion should not display since addons.showLessFrequentlyCount
|
||||
// is 3 and the substring (" b") after the first word ("aaa") is 2 chars
|
||||
// long.
|
||||
isSuggestionShown: false,
|
||||
},
|
||||
});
|
||||
|
||||
await doShowLessFrequently({
|
||||
input: "123",
|
||||
input: "aaa bb",
|
||||
expected: {
|
||||
// The suggestion should display, but item should not shown since the
|
||||
// addons.minKeywordLength reached to addonsKeywordsMinimumLengthCap
|
||||
// already.
|
||||
isSuggestionShown: true,
|
||||
isMenuItemShown: false,
|
||||
},
|
||||
});
|
||||
|
||||
await cleanUpNimbus();
|
||||
await SpecialPowers.popPrefEnv();
|
||||
});
|
||||
|
||||
add_task(async function resultMenu_showLessFrequentlyWithNimbusMinimumLength() {
|
||||
await SpecialPowers.pushPrefEnv({
|
||||
set: [
|
||||
["browser.urlbar.addons.featureGate", true],
|
||||
["browser.urlbar.addons.minKeywordLength", 0],
|
||||
],
|
||||
});
|
||||
|
||||
const cleanUpNimbus = await UrlbarTestUtils.initNimbusFeature({
|
||||
addonsKeywordsMinimumLengthCap: 3,
|
||||
addonsKeywordsMinimumLength: 2,
|
||||
});
|
||||
|
||||
// Sanity check.
|
||||
Assert.equal(UrlbarPrefs.get("addonsKeywordsMinimumLengthCap"), 3);
|
||||
Assert.equal(UrlbarPrefs.get("addonsKeywordsMinimumLength"), 2);
|
||||
Assert.equal(UrlbarPrefs.get("addons.minKeywordLength"), 0);
|
||||
|
||||
await doShowLessFrequently({
|
||||
input: "12",
|
||||
expected: {
|
||||
isSuggestionShown: true,
|
||||
isMenuItemShown: true,
|
||||
},
|
||||
});
|
||||
Assert.equal(UrlbarPrefs.get("addons.minKeywordLength"), 3);
|
||||
|
||||
await doShowLessFrequently({
|
||||
input: "12",
|
||||
expected: {
|
||||
// The suggestion should not display since addons.minKeywordLength is 3
|
||||
// and the text length is 2,
|
||||
isSuggestionShown: false,
|
||||
},
|
||||
});
|
||||
|
||||
await doShowLessFrequently({
|
||||
input: "123",
|
||||
expected: {
|
||||
// The suggestion should display, but item should not shown since the
|
||||
// addons.minKeywordLength reached to addonsKeywordsMinimumLengthCap
|
||||
// addons.showLessFrequentlyCount reached to addonsShowLessFrequentlyCap
|
||||
// already.
|
||||
isSuggestionShown: true,
|
||||
isMenuItemShown: false,
|
||||
@@ -524,16 +473,6 @@ async function doDismissTest(command) {
|
||||
set: [["browser.urlbar.addons.featureGate", true]],
|
||||
});
|
||||
|
||||
const cleanUpNimbus = await UrlbarTestUtils.initNimbusFeature({
|
||||
addonsKeywordsMinimumLengthCap: 0,
|
||||
addonsKeywordsMinimumLength: 0,
|
||||
});
|
||||
|
||||
// Sanity check.
|
||||
Assert.equal(UrlbarPrefs.get("addonsKeywordsMinimumLengthCap"), 0);
|
||||
Assert.equal(UrlbarPrefs.get("addonsKeywordsMinimumLength"), 0);
|
||||
Assert.equal(UrlbarPrefs.get("addons.minKeywordLength"), 0);
|
||||
|
||||
await UrlbarTestUtils.promiseAutocompleteResultPopup({
|
||||
window,
|
||||
value: "123",
|
||||
@@ -616,7 +555,6 @@ async function doDismissTest(command) {
|
||||
|
||||
await UrlbarTestUtils.promisePopupClose(window);
|
||||
|
||||
await cleanUpNimbus();
|
||||
await SpecialPowers.popPrefEnv();
|
||||
UrlbarPrefs.clear("suggest.addons");
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ const REMOTE_SETTINGS_RESULTS = [
|
||||
icon: "https://example.com/first-addon.svg",
|
||||
title: "First Addon",
|
||||
rating: "4.7",
|
||||
keywords: ["first", "1st"],
|
||||
keywords: ["first", "1st", "two words", "a b c"],
|
||||
description: "Description for the First Addon",
|
||||
number_of_ratings: 1256,
|
||||
is_top_pick: true,
|
||||
@@ -249,6 +249,22 @@ add_task(async function hideIfAlreadyInstalled() {
|
||||
|
||||
add_task(async function remoteSettings() {
|
||||
const testCases = [
|
||||
{
|
||||
input: "f",
|
||||
expected: null,
|
||||
},
|
||||
{
|
||||
input: "fi",
|
||||
expected: null,
|
||||
},
|
||||
{
|
||||
input: "fir",
|
||||
expected: null,
|
||||
},
|
||||
{
|
||||
input: "firs",
|
||||
expected: null,
|
||||
},
|
||||
{
|
||||
input: "first",
|
||||
expected: makeExpectedResult({
|
||||
@@ -265,6 +281,110 @@ add_task(async function remoteSettings() {
|
||||
isTopPick: true,
|
||||
}),
|
||||
},
|
||||
{
|
||||
input: "t",
|
||||
expected: null,
|
||||
},
|
||||
{
|
||||
input: "tw",
|
||||
expected: null,
|
||||
},
|
||||
{
|
||||
input: "two",
|
||||
expected: makeExpectedResult({
|
||||
suggestion: REMOTE_SETTINGS_RESULTS[0].attachment[0],
|
||||
source: "remote-settings",
|
||||
isTopPick: true,
|
||||
}),
|
||||
},
|
||||
{
|
||||
input: "two ",
|
||||
expected: makeExpectedResult({
|
||||
suggestion: REMOTE_SETTINGS_RESULTS[0].attachment[0],
|
||||
source: "remote-settings",
|
||||
isTopPick: true,
|
||||
}),
|
||||
},
|
||||
{
|
||||
input: "two w",
|
||||
expected: makeExpectedResult({
|
||||
suggestion: REMOTE_SETTINGS_RESULTS[0].attachment[0],
|
||||
source: "remote-settings",
|
||||
isTopPick: true,
|
||||
}),
|
||||
},
|
||||
{
|
||||
input: "two wo",
|
||||
expected: makeExpectedResult({
|
||||
suggestion: REMOTE_SETTINGS_RESULTS[0].attachment[0],
|
||||
source: "remote-settings",
|
||||
isTopPick: true,
|
||||
}),
|
||||
},
|
||||
{
|
||||
input: "two wor",
|
||||
expected: makeExpectedResult({
|
||||
suggestion: REMOTE_SETTINGS_RESULTS[0].attachment[0],
|
||||
source: "remote-settings",
|
||||
isTopPick: true,
|
||||
}),
|
||||
},
|
||||
{
|
||||
input: "two word",
|
||||
expected: makeExpectedResult({
|
||||
suggestion: REMOTE_SETTINGS_RESULTS[0].attachment[0],
|
||||
source: "remote-settings",
|
||||
isTopPick: true,
|
||||
}),
|
||||
},
|
||||
{
|
||||
input: "two words",
|
||||
expected: makeExpectedResult({
|
||||
suggestion: REMOTE_SETTINGS_RESULTS[0].attachment[0],
|
||||
source: "remote-settings",
|
||||
isTopPick: true,
|
||||
}),
|
||||
},
|
||||
{
|
||||
input: "a",
|
||||
expected: makeExpectedResult({
|
||||
suggestion: REMOTE_SETTINGS_RESULTS[0].attachment[0],
|
||||
source: "remote-settings",
|
||||
isTopPick: true,
|
||||
}),
|
||||
},
|
||||
{
|
||||
input: "a ",
|
||||
expected: makeExpectedResult({
|
||||
suggestion: REMOTE_SETTINGS_RESULTS[0].attachment[0],
|
||||
source: "remote-settings",
|
||||
isTopPick: true,
|
||||
}),
|
||||
},
|
||||
{
|
||||
input: "a b",
|
||||
expected: makeExpectedResult({
|
||||
suggestion: REMOTE_SETTINGS_RESULTS[0].attachment[0],
|
||||
source: "remote-settings",
|
||||
isTopPick: true,
|
||||
}),
|
||||
},
|
||||
{
|
||||
input: "a b ",
|
||||
expected: makeExpectedResult({
|
||||
suggestion: REMOTE_SETTINGS_RESULTS[0].attachment[0],
|
||||
source: "remote-settings",
|
||||
isTopPick: true,
|
||||
}),
|
||||
},
|
||||
{
|
||||
input: "a b c",
|
||||
expected: makeExpectedResult({
|
||||
suggestion: REMOTE_SETTINGS_RESULTS[0].attachment[0],
|
||||
source: "remote-settings",
|
||||
isTopPick: true,
|
||||
}),
|
||||
},
|
||||
{
|
||||
input: "second",
|
||||
expected: makeExpectedResult({
|
||||
@@ -297,26 +417,22 @@ add_task(async function remoteSettings() {
|
||||
isTopPick: true,
|
||||
}),
|
||||
},
|
||||
{
|
||||
// Merino result.
|
||||
input: "not rs",
|
||||
expected: makeExpectedResult({
|
||||
suggestion: MERINO_SUGGESTIONS[0],
|
||||
source: "merino",
|
||||
isTopPick: true,
|
||||
}),
|
||||
},
|
||||
];
|
||||
|
||||
// Disable Merino so we trigger only remote settings suggestions.
|
||||
UrlbarPrefs.set("quicksuggest.dataCollection.enabled", false);
|
||||
|
||||
for (const { input, expected } of testCases) {
|
||||
await check_results({
|
||||
context: createContext(input, {
|
||||
providers: [UrlbarProviderQuickSuggest.name],
|
||||
isPrivate: false,
|
||||
}),
|
||||
matches: [expected],
|
||||
matches: expected ? [expected] : [],
|
||||
});
|
||||
}
|
||||
|
||||
UrlbarPrefs.set("quicksuggest.dataCollection.enabled", true);
|
||||
});
|
||||
|
||||
add_task(async function merinoIsTopPick() {
|
||||
@@ -357,6 +473,226 @@ add_task(async function merinoIsTopPick() {
|
||||
});
|
||||
});
|
||||
|
||||
// Tests "show less frequently" with the cap set in remote settings.
|
||||
add_task(async function showLessFrequently_rs() {
|
||||
await doShowLessFrequentlyTest({
|
||||
rs: {
|
||||
show_less_frequently_cap: 3,
|
||||
},
|
||||
tests: [
|
||||
{
|
||||
showLessFrequentlyCount: 0,
|
||||
canShowLessFrequently: true,
|
||||
searches: {
|
||||
f: false,
|
||||
fi: false,
|
||||
fir: false,
|
||||
firs: false,
|
||||
first: true,
|
||||
t: false,
|
||||
tw: false,
|
||||
two: true,
|
||||
"two ": true,
|
||||
"two w": true,
|
||||
"two wo": true,
|
||||
"two wor": true,
|
||||
"two word": true,
|
||||
"two words": true,
|
||||
a: true,
|
||||
"a ": true,
|
||||
"a b": true,
|
||||
"a b ": true,
|
||||
"a b c": true,
|
||||
},
|
||||
},
|
||||
{
|
||||
showLessFrequentlyCount: 1,
|
||||
canShowLessFrequently: true,
|
||||
searches: {
|
||||
first: false,
|
||||
two: false,
|
||||
a: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
showLessFrequentlyCount: 2,
|
||||
canShowLessFrequently: true,
|
||||
searches: {
|
||||
"two ": false,
|
||||
"a ": false,
|
||||
},
|
||||
},
|
||||
{
|
||||
showLessFrequentlyCount: 3,
|
||||
canShowLessFrequently: false,
|
||||
searches: {
|
||||
"two w": false,
|
||||
"a b": false,
|
||||
},
|
||||
},
|
||||
{
|
||||
showLessFrequentlyCount: 3,
|
||||
canShowLessFrequently: false,
|
||||
searches: {},
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
// Tests "show less frequently" with the cap set in both Nimbus and remote
|
||||
// settings. Nimbus should override remote settings.
|
||||
add_task(async function showLessFrequently_nimbus() {
|
||||
await doShowLessFrequentlyTest({
|
||||
nimbus: {
|
||||
addonsShowLessFrequentlyCap: 3,
|
||||
},
|
||||
rs: {
|
||||
show_less_frequently_cap: 10,
|
||||
},
|
||||
tests: [
|
||||
{
|
||||
showLessFrequentlyCount: 0,
|
||||
canShowLessFrequently: true,
|
||||
searches: {
|
||||
a: true,
|
||||
"a ": true,
|
||||
"a b": true,
|
||||
"a b ": true,
|
||||
"a b c": true,
|
||||
},
|
||||
},
|
||||
{
|
||||
showLessFrequentlyCount: 1,
|
||||
canShowLessFrequently: true,
|
||||
searches: {
|
||||
a: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
showLessFrequentlyCount: 2,
|
||||
canShowLessFrequently: true,
|
||||
searches: {
|
||||
"a ": false,
|
||||
},
|
||||
},
|
||||
{
|
||||
showLessFrequentlyCount: 3,
|
||||
canShowLessFrequently: false,
|
||||
searches: {
|
||||
"a b": false,
|
||||
},
|
||||
},
|
||||
{
|
||||
showLessFrequentlyCount: 3,
|
||||
canShowLessFrequently: false,
|
||||
searches: {},
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Does a group of searches, increments the `showLessFrequentlyCount`, and
|
||||
* repeats until all groups are done. The cap can be set by remote settings
|
||||
* config and/or Nimbus.
|
||||
*
|
||||
* @param {object} options
|
||||
* Options object.
|
||||
* @param {object} options.tests
|
||||
* An array where each item describes a group of searches to perform and
|
||||
* expected state. Each item should look like this:
|
||||
* `{ showLessFrequentlyCount, canShowLessFrequently, searches }`
|
||||
*
|
||||
* {number} showLessFrequentlyCount
|
||||
* The expected value of `showLessFrequentlyCount` before the group of
|
||||
* searches is performed.
|
||||
* {boolean} canShowLessFrequently
|
||||
* The expected value of `canShowLessFrequently` before the group of
|
||||
* searches is performed.
|
||||
* {object} searches
|
||||
* An object that maps each search string to a boolean that indicates
|
||||
* whether the first remote settings suggestion should be triggered by the
|
||||
* search string. `searches` objects are cumulative: The intended use is to
|
||||
* pass a large initial group of searches in the first search group, and
|
||||
* then each following `searches` is a diff against the previous.
|
||||
* @param {object} options.rs
|
||||
* The remote settings config to set.
|
||||
* @param {object} options.nimbus
|
||||
* The Nimbus variables to set.
|
||||
*/
|
||||
async function doShowLessFrequentlyTest({ tests, rs = {}, nimbus = {} }) {
|
||||
// Disable Merino so we trigger only remote settings suggestions.
|
||||
UrlbarPrefs.set("quicksuggest.dataCollection.enabled", false);
|
||||
|
||||
// We'll be testing with the first remote settings suggestion.
|
||||
let suggestion = REMOTE_SETTINGS_RESULTS[0].attachment[0];
|
||||
|
||||
let addonSuggestions = QuickSuggest.getFeature("AddonSuggestions");
|
||||
|
||||
// Set Nimbus variables and RS config.
|
||||
let cleanUpNimbus = await UrlbarTestUtils.initNimbusFeature(nimbus);
|
||||
await QuickSuggestTestUtils.withConfig({
|
||||
config: rs,
|
||||
callback: async () => {
|
||||
let cumulativeSearches = {};
|
||||
|
||||
for (let {
|
||||
showLessFrequentlyCount,
|
||||
canShowLessFrequently,
|
||||
searches,
|
||||
} of tests) {
|
||||
Assert.equal(
|
||||
addonSuggestions.showLessFrequentlyCount,
|
||||
showLessFrequentlyCount,
|
||||
"showLessFrequentlyCount should be correct initially"
|
||||
);
|
||||
Assert.equal(
|
||||
UrlbarPrefs.get("addons.showLessFrequentlyCount"),
|
||||
showLessFrequentlyCount,
|
||||
"Pref should be correct initially"
|
||||
);
|
||||
Assert.equal(
|
||||
addonSuggestions.canShowLessFrequently,
|
||||
canShowLessFrequently,
|
||||
"canShowLessFrequently should be correct initially"
|
||||
);
|
||||
|
||||
// Merge the current `searches` object into the cumulative object.
|
||||
cumulativeSearches = {
|
||||
...cumulativeSearches,
|
||||
...searches,
|
||||
};
|
||||
|
||||
for (let [searchString, isExpected] of Object.entries(
|
||||
cumulativeSearches
|
||||
)) {
|
||||
await check_results({
|
||||
context: createContext(searchString, {
|
||||
providers: [UrlbarProviderQuickSuggest.name],
|
||||
isPrivate: false,
|
||||
}),
|
||||
matches: !isExpected
|
||||
? []
|
||||
: [
|
||||
makeExpectedResult({
|
||||
suggestion,
|
||||
source: "remote-settings",
|
||||
isTopPick: true,
|
||||
}),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
addonSuggestions.incrementShowLessFrequentlyCount();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
await cleanUpNimbus();
|
||||
UrlbarPrefs.clear("addons.showLessFrequentlyCount");
|
||||
UrlbarPrefs.set("quicksuggest.dataCollection.enabled", true);
|
||||
}
|
||||
|
||||
function makeExpectedResult({ suggestion, source, isTopPick }) {
|
||||
let rating;
|
||||
let number_of_ratings;
|
||||
|
||||
@@ -125,7 +125,7 @@ add_task(async function duplicateKeywords() {
|
||||
let suggestions = [
|
||||
{
|
||||
title: "suggestion 0",
|
||||
keywords: ["a", "b", "c"],
|
||||
keywords: ["a", "a", "a", "b", "b", "c"],
|
||||
},
|
||||
{
|
||||
title: "suggestion 1",
|
||||
@@ -137,7 +137,7 @@ add_task(async function duplicateKeywords() {
|
||||
},
|
||||
{
|
||||
title: "suggestion 3",
|
||||
keywords: ["f"],
|
||||
keywords: ["f", "f"],
|
||||
},
|
||||
];
|
||||
|
||||
@@ -161,3 +161,57 @@ add_task(async function duplicateKeywords() {
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
add_task(async function mapKeywords() {
|
||||
let suggestions = [
|
||||
{
|
||||
title: "suggestion 0",
|
||||
keywords: ["a", "a", "a", "b", "b", "c"],
|
||||
},
|
||||
{
|
||||
title: "suggestion 1",
|
||||
keywords: ["b", "c", "d"],
|
||||
},
|
||||
{
|
||||
title: "suggestion 2",
|
||||
keywords: ["c", "d", "e"],
|
||||
},
|
||||
{
|
||||
title: "suggestion 3",
|
||||
keywords: ["f", "f"],
|
||||
},
|
||||
];
|
||||
|
||||
let expectedIndexesByKeyword = {
|
||||
a: [],
|
||||
b: [],
|
||||
c: [],
|
||||
d: [],
|
||||
e: [],
|
||||
f: [],
|
||||
ax: [0],
|
||||
bx: [0, 1],
|
||||
cx: [0, 1, 2],
|
||||
dx: [1, 2],
|
||||
ex: [2],
|
||||
fx: [3],
|
||||
fy: [3],
|
||||
fz: [3],
|
||||
};
|
||||
|
||||
let map = new SuggestionsMap();
|
||||
await map.add(suggestions, keyword => {
|
||||
if (keyword == "f") {
|
||||
return [keyword + "x", keyword + "y", keyword + "z"];
|
||||
}
|
||||
return [keyword + "x"];
|
||||
});
|
||||
|
||||
for (let [keyword, indexes] of Object.entries(expectedIndexesByKeyword)) {
|
||||
Assert.deepEqual(
|
||||
map.get(keyword),
|
||||
indexes.map(i => suggestions[i]),
|
||||
"get() with keyword: " + keyword
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -7,7 +7,6 @@ firefox-appdir = browser
|
||||
[test_merinoClient_sessions.js]
|
||||
[test_quicksuggest.js]
|
||||
[test_quicksuggest_addons.js]
|
||||
[test_quicksuggest_addResults.js]
|
||||
[test_quicksuggest_bestMatch.js]
|
||||
[test_quicksuggest_dynamicWikipedia.js]
|
||||
[test_quicksuggest_impressionCaps.js]
|
||||
@@ -19,5 +18,6 @@ firefox-appdir = browser
|
||||
[test_quicksuggest_offlineDefault.js]
|
||||
[test_quicksuggest_positionInSuggestions.js]
|
||||
[test_quicksuggest_topPicks.js]
|
||||
[test_suggestionsMap.js]
|
||||
[test_weather.js]
|
||||
[test_weather_keywords.js]
|
||||
|
||||
@@ -146,27 +146,14 @@ urlbar:
|
||||
fallbackPref: browser.urlbar.addons.featureGate
|
||||
description: >-
|
||||
Feature gate that controls whether all aspects of the addons suggestion
|
||||
feature are exposed to the user. See also `addonsKeywordsMinimumLength`
|
||||
and `addonsKeywordsMinimumLengthCap`.
|
||||
addonsKeywordsMinimumLength:
|
||||
feature are exposed to the user.
|
||||
addonsShowLessFrequentlyCap:
|
||||
type: int
|
||||
description: >-
|
||||
If defined and non-zero, the addons suggestion will be triggered only by
|
||||
the query that has number of chars more than this variable.
|
||||
However, if there is a value in browser.urlbar.addons.minKeywordLength,
|
||||
it will take priority.
|
||||
addonsKeywordsMinimumLengthCap:
|
||||
type: int
|
||||
description: >-
|
||||
If defined and non-zero, the user will not be able to increment the
|
||||
minimum keyword length beyond this value. e.g., if this value is 6, the
|
||||
current minimum length is 5, and the user clicks "Show less frequently",
|
||||
then the minimum length will be incremented to 6, the "Show less
|
||||
frequently" command will be hidden, and the user can continue to trigger
|
||||
the weather suggestion by typing 6 characters, but they will not be able
|
||||
to increment the minimum length any further.
|
||||
If not defines a cap, no cap will be used, and the user will be able to
|
||||
increment the minimum length without any limit.
|
||||
If defined and non-zero, this is the maximum number of times the user
|
||||
will be able to click the "Show less frequently" command for addon
|
||||
suggestions. If undefined or zero, the user will be able to click the
|
||||
command without any limit.
|
||||
addonsUITreatment:
|
||||
type: string
|
||||
enum:
|
||||
|
||||
Reference in New Issue
Block a user