Bug 1545916 - Make quantumbar match highlighting case insensitive. r=dao

Differential Revision: https://phabricator.services.mozilla.com/D28751

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Drew Willcoxon 2019-04-25 18:12:06 +00:00
parent 0082cd8b08
commit aa16935357
6 changed files with 288 additions and 9 deletions

View File

@ -272,6 +272,7 @@ function filterTokens(tokens) {
let token = tokens[i];
let tokenObj = {
value: token,
lowerCaseValue: token.toLocaleLowerCase(),
type: UrlbarTokenizer.TYPE.TEXT,
};
let restrictionType = CHAR_TO_TYPE_MAP.get(token);

View File

@ -228,9 +228,10 @@ var UrlbarUtils = {
},
/**
* Returns a list of all the token substring matches in a string. Each match
* in the list is a tuple: [matchIndex, matchLength]. matchIndex is the index
* in the string of the match, and matchLength is the length of the match.
* Returns a list of all the token substring matches in a string. Matching is
* case insensitive. Each match in the returned list is a tuple: [matchIndex,
* matchLength]. matchIndex is the index in the string of the match, and
* matchLength is the length of the match.
*
* @param {array} tokens The tokens to search for.
* @param {string} str The string to match against.
@ -243,14 +244,15 @@ var UrlbarUtils = {
* The array is sorted by match indexes ascending.
*/
getTokenMatches(tokens, str) {
str = str.toLocaleLowerCase();
// To generate non-overlapping ranges, we start from a 0-filled array with
// the same length of the string, and use it as a collision marker, setting
// 1 where a token matches.
let hits = new Array(str.length).fill(0);
for (let token of tokens) {
for (let { lowerCaseValue } of tokens) {
// Ideally we should never hit the empty token case, but just in case
// the value check protects us from an infinite loop.
for (let index = 0, needle = token.value; index >= 0 && needle;) {
// the `needle` check protects us from an infinite loop.
for (let index = 0, needle = lowerCaseValue; index >= 0 && needle;) {
index = str.indexOf(needle, index);
if (index >= 0) {
hits.fill(1, index, index + needle.length);

View File

@ -20,12 +20,13 @@ add_task(async function setup() {
async function testResult(input, expected) {
const ESCAPED_URL = encodeURI(input.url);
await PlacesUtils.history.clear();
await PlacesTestUtils.addVisits({
uri: input.url,
title: input.title,
});
await promiseAutocompleteResultPopup("\u6e2C\u8a66");
await promiseAutocompleteResultPopup(input.query);
let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
Assert.equal(result.url, ESCAPED_URL,
@ -100,3 +101,145 @@ add_task(async function test_url_result_no_trimming() {
Services.prefs.clearUserPref("browser.urlbar.trimURLs");
});
add_task(async function test_case_insensitive_highlights_1() {
await testResult({
query: "exam",
title: "The examPLE URL EXAMple",
url: "http://example.com/ExAm",
}, {
displayedUrl: "example.com/ExAm",
highlightedTitle: [
["The ", false],
["exam", true],
["PLE URL ", false],
["EXAM", true],
["ple", false],
],
highlightedUrl: [
["exam", true],
["ple.com/", false],
["ExAm", true],
],
});
});
add_task(async function test_case_insensitive_highlights_2() {
await testResult({
query: "EXAM",
title: "The examPLE URL EXAMple",
url: "http://example.com/ExAm",
}, {
displayedUrl: "example.com/ExAm",
highlightedTitle: [
["The ", false],
["exam", true],
["PLE URL ", false],
["EXAM", true],
["ple", false],
],
highlightedUrl: [
["exam", true],
["ple.com/", false],
["ExAm", true],
],
});
});
add_task(async function test_case_insensitive_highlights_3() {
await testResult({
query: "eXaM",
title: "The examPLE URL EXAMple",
url: "http://example.com/ExAm",
}, {
displayedUrl: "example.com/ExAm",
highlightedTitle: [
["The ", false],
["exam", true],
["PLE URL ", false],
["EXAM", true],
["ple", false],
],
highlightedUrl: [
["exam", true],
["ple.com/", false],
["ExAm", true],
],
});
});
add_task(async function test_case_insensitive_highlights_4() {
await testResult({
query: "ExAm",
title: "The examPLE URL EXAMple",
url: "http://example.com/ExAm",
}, {
displayedUrl: "example.com/ExAm",
highlightedTitle: [
["The ", false],
["exam", true],
["PLE URL ", false],
["EXAM", true],
["ple", false],
],
highlightedUrl: [
["exam", true],
["ple.com/", false],
["ExAm", true],
],
});
});
add_task(async function test_case_insensitive_highlights_5() {
await testResult({
query: "exam foo",
title: "The examPLE URL foo EXAMple FOO",
url: "http://example.com/ExAm/fOo",
}, {
displayedUrl: "example.com/ExAm/fOo",
highlightedTitle: [
["The ", false],
["exam", true],
["PLE URL ", false],
["foo", true],
[" ", false],
["EXAM", true],
["ple ", false],
["FOO", true],
],
highlightedUrl: [
["exam", true],
["ple.com/", false],
["ExAm", true],
["/", false],
["fOo", true],
],
});
});
add_task(async function test_case_insensitive_highlights_6() {
await testResult({
query: "EXAM FOO",
title: "The examPLE URL foo EXAMple FOO",
url: "http://example.com/ExAm/fOo",
}, {
displayedUrl: "example.com/ExAm/fOo",
highlightedTitle: [
["The ", false],
["exam", true],
["PLE URL ", false],
["foo", true],
[" ", false],
["EXAM", true],
["ple ", false],
["FOO", true],
],
highlightedUrl: [
["exam", true],
["ple.com/", false],
["ExAm", true],
["/", false],
["fOo", true],
],
});
});

View File

@ -14,11 +14,41 @@ add_task(function test() {
phrase: "mozilla is for the Open Web",
expected: [[0, 7], [8, 2]],
},
{
tokens: ["mozilla", "is", "i"],
phrase: "MOZILLA IS for the Open Web",
expected: [[0, 7], [8, 2]],
},
{
tokens: ["mozilla", "is", "i"],
phrase: "MoZiLlA Is for the Open Web",
expected: [[0, 7], [8, 2]],
},
{
tokens: ["MOZILLA", "IS", "I"],
phrase: "mozilla is for the Open Web",
expected: [[0, 7], [8, 2]],
},
{
tokens: ["MoZiLlA", "Is", "I"],
phrase: "mozilla is for the Open Web",
expected: [[0, 7], [8, 2]],
},
{
tokens: ["mo", "b"],
phrase: "mozilla is for the Open Web",
expected: [[0, 2], [26, 1]],
},
{
tokens: ["mo", "b"],
phrase: "MOZILLA is for the OPEN WEB",
expected: [[0, 2], [26, 1]],
},
{
tokens: ["MO", "B"],
phrase: "mozilla is for the Open Web",
expected: [[0, 2], [26, 1]],
},
{
tokens: ["mo", ""],
phrase: "mozilla is for the Open Web",
@ -29,6 +59,36 @@ add_task(function test() {
phrase: "mozilla",
expected: [[0, 7]],
},
{
tokens: ["mozilla"],
phrase: "MOZILLA",
expected: [[0, 7]],
},
{
tokens: ["mozilla"],
phrase: "MoZiLlA",
expected: [[0, 7]],
},
{
tokens: ["mozilla"],
phrase: "mOzIlLa",
expected: [[0, 7]],
},
{
tokens: ["MOZILLA"],
phrase: "mozilla",
expected: [[0, 7]],
},
{
tokens: ["MoZiLlA"],
phrase: "mozilla",
expected: [[0, 7]],
},
{
tokens: ["mOzIlLa"],
phrase: "mozilla",
expected: [[0, 7]],
},
{
tokens: ["\u9996"],
phrase: "Test \u9996\u9875 Test",
@ -39,6 +99,31 @@ add_task(function test() {
phrase: "mozilla",
expected: [[0, 7]],
},
{
tokens: ["mo", "zilla"],
phrase: "MOZILLA",
expected: [[0, 7]],
},
{
tokens: ["mo", "zilla"],
phrase: "MoZiLlA",
expected: [[0, 7]],
},
{
tokens: ["mo", "zilla"],
phrase: "mOzIlLa",
expected: [[0, 7]],
},
{
tokens: ["MO", "ZILLA"],
phrase: "mozilla",
expected: [[0, 7]],
},
{
tokens: ["Mo", "Zilla"],
phrase: "mozilla",
expected: [[0, 7]],
},
{
tokens: ["moz", "zilla"],
phrase: "mozilla",
@ -54,9 +139,22 @@ add_task(function test() {
phrase: "mozilla mozzarella momo",
expected: [[0, 2], [8, 2], [19, 4]],
},
{
tokens: ["mo", "om"],
phrase: "MOZILLA MOZZARELLA MOMO",
expected: [[0, 2], [8, 2], [19, 4]],
},
{
tokens: ["MO", "OM"],
phrase: "mozilla mozzarella momo",
expected: [[0, 2], [8, 2], [19, 4]],
},
];
for (let {tokens, phrase, expected} of tests) {
tokens = tokens.map(t => ({value: t}));
tokens = tokens.map(t => ({
value: t,
lowerCaseValue: t.toLocaleLowerCase(),
}));
Assert.deepEqual(UrlbarUtils.getTokenMatches(tokens, phrase), expected,
`Match "${tokens.map(t => t.value).join(", ")}" on "${phrase}"`);
}

View File

@ -240,10 +240,45 @@ add_task(async function test_tokenizer() {
{ value: "%E6%97%A5%E6%9C%AC", type: UrlbarTokenizer.TYPE.TEXT },
],
},
{ desc: "Uppercase",
searchString: "TEST",
expectedTokens: [
{ value: "TEST", type: UrlbarTokenizer.TYPE.POSSIBLE_ORIGIN },
],
},
{ desc: "Mixed case 1",
searchString: "TeSt",
expectedTokens: [
{ value: "TeSt", type: UrlbarTokenizer.TYPE.POSSIBLE_ORIGIN },
],
},
{ desc: "Mixed case 2",
searchString: "tEsT",
expectedTokens: [
{ value: "tEsT", type: UrlbarTokenizer.TYPE.POSSIBLE_ORIGIN },
],
},
{ desc: "Uppercase with spaces",
searchString: "TEST EXAMPLE",
expectedTokens: [
{ value: "TEST", type: UrlbarTokenizer.TYPE.POSSIBLE_ORIGIN },
{ value: "EXAMPLE", type: UrlbarTokenizer.TYPE.POSSIBLE_ORIGIN },
],
},
{ desc: "Mixed case with spaces",
searchString: "TeSt eXaMpLe",
expectedTokens: [
{ value: "TeSt", type: UrlbarTokenizer.TYPE.POSSIBLE_ORIGIN },
{ value: "eXaMpLe", type: UrlbarTokenizer.TYPE.POSSIBLE_ORIGIN },
],
},
];
for (let queryContext of testContexts) {
info(queryContext.desc);
for (let token of queryContext.expectedTokens) {
token.lowerCaseValue = token.value.toLocaleLowerCase();
}
let newQueryContext = UrlbarTokenizer.tokenize(queryContext);
Assert.equal(queryContext, newQueryContext,
"The queryContext object is the same");

View File

@ -78,7 +78,7 @@ It is augmented as it progresses through the system, with various information:
preselected; // {boolean} whether the first result should be preselected.
results; // {array} list of UrlbarResult objects.
tokens; // {array} tokens extracted from the searchString, each token is an
// object in the form {type, value}.
// object in the form {type, value, lowerCaseValue}.
}