Source code

Revision control

Copy as Markdown

Other Tools

Test Info: Warnings

/* Any copyright is dedicated to the Public Domain.
"use strict";
// Tests validation, eligibility and selection of temporal search suggestions.
const TODAY = Temporal.PlainDate.from("2026-11-15");
let gFeature;
let gRandomStub;
add_setup(async function () {
await QuickSuggestTestUtils.ensureQuickSuggestInit();
gFeature = QuickSuggest.getFeature("TemporalSuggestions");
// Selection picks a random index into the queries not yet picked. Returning
// 0 always picks the first one, so queries are picked in the given order and
// the expected picks are deterministic.
gRandomStub = sinon.stub(gFeature, "_random").returns(0);
registerCleanupFunction(() => {
gRandomStub.restore();
gFeature._reset();
});
});
// Returns valid suggestion data, eligible on `TODAY` for the "google" engine,
// with `overrides` applied.
function makeData(overrides = {}) {
return {
group: "test-group",
title: "Test group",
type: "seasonal",
startDate: "2026-11-12",
endDate: "2026-11-27",
engines: ["google"],
query: "one",
...overrides,
};
}
// Wraps data as a dynamic Rust suggestion.
function makeSuggestion(data) {
return {
source: "rust",
provider: "Dynamic",
suggestionType: "temporal",
dismissalKey: data.query,
score: 0.3,
data,
};
}
function eligible(data, today = TODAY, engineId = "google") {
return gFeature.isEligible(data, {
today: Temporal.PlainDate.from(today),
engineId,
});
}
// Returns `count` queries named q1, q2, etc.
function makeQueries(count) {
return Array.from({ length: count }, (_, i) => `q${i + 1}`);
}
add_task(async function invalidData() {
let invalid = [
{ type: "time-of-day" },
{ type: undefined },
{ group: "" },
{ group: undefined },
{ title: "" },
{ title: 1 },
{ query: "" },
{ query: " " },
{ query: undefined },
{ startDate: undefined },
{ startDate: "2026-13-01" },
{ startDate: "2026-02-30" },
{ startDate: "11/12/2026" },
{ startDate: "2026-11-28" },
{ engines: [] },
{ engines: "google" },
{ engines: [""] },
];
Assert.ok(eligible(makeData()), "The unmodified data is valid");
for (let overrides of invalid) {
info("Checking invalid data: " + JSON.stringify(overrides));
Assert.ok(!eligible(makeData(overrides)), "Data is ignored");
}
Assert.ok(!eligible(null), "Null data is ignored");
});
add_task(async function dateBoundaries() {
let data = makeData();
Assert.ok(!eligible(data, "2026-11-11"), "Day before start");
Assert.ok(eligible(data, "2026-11-12"), "Start day");
Assert.ok(eligible(data, "2026-11-27"), "End day");
Assert.ok(!eligible(data, "2026-11-28"), "Day after end");
Assert.ok(
eligible(
makeData({ startDate: "2026-11-12", endDate: "2026-11-12" }),
"2026-11-12"
),
"Single-day group"
);
});
add_task(async function engines() {
let data = makeData({ engines: ["bing", "google"] });
Assert.ok(eligible(data, TODAY, "google"), "First engine");
Assert.ok(eligible(data, TODAY, "bing"), "Second engine");
Assert.ok(!eligible(data, TODAY, "ddg"), "Mismatched engine");
Assert.ok(
!eligible(data, TODAY, "5f1e8c4b-0d4a-4b5f-9c1e-3a7c2b9d8e6f"),
"Non-config engine"
);
});
// Data isn't eligible when the pref for its type is disabled.
add_task(async function typePref() {
UrlbarPrefs.set("suggest.temporal.seasonal", false);
Assert.ok(!eligible(makeData()), "Type pref disabled");
UrlbarPrefs.clear("suggest.temporal.seasonal");
Assert.ok(eligible(makeData()), "Type pref enabled");
});
// Queries already picked are skipped until every query has been picked. Then
// picking starts over, without repeating queries within the same pick.
add_task(async function rotation() {
gFeature._reset();
let queries = makeQueries(7);
Assert.deepEqual(
gFeature.selectQueries(queries, 3),
["q1", "q2", "q3"],
"First pick"
);
Assert.deepEqual(
gFeature.selectQueries(queries, 3),
["q4", "q5", "q6"],
"Picked queries are skipped"
);
Assert.deepEqual(
gFeature.selectQueries(queries, 3),
["q7", "q1", "q2"],
"Picking starts over once every query has been picked"
);
Assert.deepEqual(
gFeature.selectQueries(queries, 3),
["q3", "q4", "q5"],
"Queries picked after starting over are skipped"
);
});
// Fewer queries than the count returns all the queries each time.
add_task(async function fewerQueriesThanCount() {
gFeature._reset();
let queries = makeQueries(2);
Assert.deepEqual(
gFeature.selectQueries(queries, 3),
["q1", "q2"],
"First pick"
);
Assert.deepEqual(
gFeature.selectQueries(queries, 3),
["q1", "q2"],
"Second pick"
);
});
// Queries are returned in the order they were randomly picked, not the order
// they were given in.
add_task(async function pickOrder() {
gFeature._reset();
// Always pick the last remaining query.
gRandomStub.returns(0.99);
Assert.deepEqual(
gFeature.selectQueries(makeQueries(5), 3),
["q5", "q4", "q3"],
"Queries are in pick order"
);
gRandomStub.returns(0);
});
// No more than the requested count is picked.
add_task(async function count() {
gFeature._reset();
let queries = makeQueries(5);
Assert.deepEqual(gFeature.selectQueries(queries, 1), ["q1"], "Count of 1");
Assert.deepEqual(
gFeature.selectQueries(queries, 10),
["q2", "q3", "q4", "q5", "q1"],
"Count larger than the number of queries"
);
});
// `filterSuggestions()` returns the suggestions of picked queries of the first
// eligible group, with the default engine set.
add_task(async function filterSuggestions() {
gFeature._reset();
let engine = await addTestSuggestionsEngine();
let oldDefaultEngine = SearchService.defaultEngine;
await SearchService.setDefault(
engine,
SearchService.CHANGE_REASON.ADDON_INSTALL
);
UrlbarTestUtils.stubNowZonedDateTime("2026-11-15T12:00[America/New_York]");
let suggestions = [
makeSuggestion(
makeData({ engines: [engine.id], query: "invalid", type: "x" })
),
...makeQueries(7).map(query =>
makeSuggestion(makeData({ engines: [engine.id], query }))
),
makeSuggestion(
makeData({ group: "other-group", engines: [engine.id], query: "other" })
),
];
let filtered = await gFeature.filterSuggestions(suggestions);
Assert.deepEqual(
filtered.map(s => s.data.query),
["q1", "q2", "q3", "q4", "q5"],
"rowCount queries of the first eligible group are picked"
);
for (let s of filtered) {
Assert.equal(s.engine, engine.name, "Engine is set");
Assert.equal(s.dismissalKey, s.data.query, "Dismissal key is kept");
Assert.equal(s.score, 0.3, "Score is kept");
}
UrlbarPrefs.set("temporal.rowCount", 1);
filtered = await gFeature.filterSuggestions(suggestions);
Assert.deepEqual(
filtered.map(s => s.data.query),
["q6"],
"rowCount is respected and the picked queries are skipped"
);
UrlbarPrefs.clear("temporal.rowCount");
Assert.deepEqual(
await gFeature.filterSuggestions([]),
[],
"Nothing without suggestions"
);
UrlbarTestUtils.stubNowZonedDateTime("2026-11-28T12:00[America/New_York]");
Assert.deepEqual(
await gFeature.filterSuggestions(suggestions),
[],
"Nothing after the group ends"
);
UrlbarTestUtils.stubNowZonedDateTime(null);
await SearchService.setDefault(
oldDefaultEngine,
SearchService.CHANGE_REASON.ADDON_INSTALL
);
});
// Tests that the context's maxResults uses temporal.zeroPrefixMaxRichResults in
// the zero prefix view when temporal suggestions are enabled, unless the user
// has set maxRichResults.
add_task(function temporalZeroPrefix_maxResults() {
Services.prefs.clearUserPref("browser.urlbar.maxRichResults");
let zeroPrefix = () => createContext("").maxResults;
let typed = () => createContext("test").maxResults;
let searchMode = () =>
createContext("", { searchMode: { engineName: "test" } }).maxResults;
Assert.equal(zeroPrefix(), 10);
UrlbarPrefs.set("temporal.featureGate", true);
Assert.equal(zeroPrefix(), 13);
Assert.equal(typed(), 10);
Assert.equal(searchMode(), 10);
Assert.equal(
createContext("", { maxResults: 1 }).maxResults,
1,
"A maxResults other than maxRichResults is kept"
);
for (let value of [5, 20]) {
UrlbarPrefs.set("maxRichResults", value);
Assert.equal(
zeroPrefix(),
value,
"A user-set maxRichResults is respected in the zero prefix view"
);
Assert.equal(typed(), value);
}
Services.prefs.clearUserPref("browser.urlbar.maxRichResults");
Assert.equal(zeroPrefix(), 13);
for (let sapName of ["smartbar", "searchbar", "newtab_searchbar"]) {
let context = createContext("", { sapName });
Assert.equal(
context.maxResults,
10,
`maxRichResults is used in the ${sapName} zero prefix view`
);
Assert.ok(!context.isTemporalZeroPrefix);
}
let context = createContext("");
context.maxResults = 4;
Assert.equal(context.maxResults, 4, "An assigned maxResults is kept");
UrlbarPrefs.set("temporal.zeroPrefixMaxRichResults", 8);
Assert.equal(zeroPrefix(), 8);
Assert.equal(typed(), 10);
UrlbarPrefs.clear("temporal.zeroPrefixMaxRichResults");
UrlbarPrefs.set("suggest.temporal", false);
Assert.equal(
zeroPrefix(),
10,
"maxRichResults is used when the user turns off temporal suggestions"
);
UrlbarPrefs.clear("suggest.temporal");
UrlbarPrefs.set("suggest.temporal.seasonal", false);
Assert.equal(
zeroPrefix(),
10,
"maxRichResults is used when the user turns off every temporal type"
);
Assert.ok(!createContext("").isTemporalZeroPrefix);
UrlbarPrefs.clear("suggest.temporal.seasonal");
for (let pref of ["suggest.searches", "browser.search.suggest.enabled"]) {
UrlbarPrefs.set(pref, false);
Assert.equal(
zeroPrefix(),
10,
`maxRichResults is used when ${pref} is false`
);
Assert.ok(!createContext("").isTemporalZeroPrefix);
UrlbarPrefs.clear(pref);
}
UrlbarPrefs.clear("temporal.featureGate");
});